diff --git a/qt/aqt/browser/browser.py b/qt/aqt/browser/browser.py index a50d4642f..c7510fe9b 100644 --- a/qt/aqt/browser/browser.py +++ b/qt/aqt/browser/browser.py @@ -380,29 +380,30 @@ class Browser(QMainWindow): self.form.gridLayout.addWidget(switch, 0, 0) def setupEditor(self) -> None: - def add_preview_button(leftbuttons: List[str], editor: Editor) -> None: - preview_shortcut = "Ctrl+Shift+P" - leftbuttons.insert( - 0, - editor.addButton( - None, - "preview", - lambda _editor: self.onTogglePreview(), - tr.browsing_preview_selected_card( - val=shortcut(preview_shortcut), - ), - tr.actions_preview(), - id="previewButton", - keys=preview_shortcut, - disables=False, - rightside=False, - toggleable=True, - ), + def add_preview_button(editor: Editor) -> None: + preview_shortcut = "Ctrl+Shift+P" # TODO + + editor._links["preview"] = lambda _editor: self.onTogglePreview() + editor.web.eval( + f""" +$editorToolbar.addButton({{ + component: editorToolbar.LabelButton, + label: `{tr.actions_preview()}`, + tooltip: `{tr.browsing_preview_selected_card(val=shortcut(preview_shortcut))}`, + onClick: () => bridgeCommand("preview"), + disables: false, +}}, "notetype"); +""" ) - gui_hooks.editor_did_init_left_buttons.append(add_preview_button) + def add_preview_shortcut(cuts: List[Tuple], editor: Editor) -> None: + cuts.append(("Ctrl+Shift+P", self.onTogglePreview, True)) + + gui_hooks.editor_did_init.append(add_preview_button) + gui_hooks.editor_did_init_shortcuts.append(add_preview_shortcut) self.editor = aqt.editor.Editor(self.mw, self.form.fieldsArea, self) - gui_hooks.editor_did_init_left_buttons.remove(add_preview_button) + gui_hooks.editor_did_init_shortcuts.remove(add_preview_shortcut) + gui_hooks.editor_did_init.remove(add_preview_button) @ensure_editor_saved def onRowChanged( diff --git a/qt/aqt/data/web/css/BUILD.bazel b/qt/aqt/data/web/css/BUILD.bazel index e29acf7ed..cb92658a0 100644 --- a/qt/aqt/data/web/css/BUILD.bazel +++ b/qt/aqt/data/web/css/BUILD.bazel @@ -26,11 +26,20 @@ copy_files_into_group( package = "//ts/editor", ) +copy_files_into_group( + name = "editor-toolbar", + srcs = [ + "editor-toolbar.css", + ], + package = "//ts/editor-toolbar", +) + filegroup( name = "css", srcs = [ "css_local", "editor", + "editor-toolbar", ], visibility = ["//qt:__subpackages__"], ) diff --git a/qt/aqt/data/web/js/BUILD.bazel b/qt/aqt/data/web/js/BUILD.bazel index b138bd31b..83f4718fc 100644 --- a/qt/aqt/data/web/js/BUILD.bazel +++ b/qt/aqt/data/web/js/BUILD.bazel @@ -37,11 +37,20 @@ copy_files_into_group( package = "//ts/editor", ) +copy_files_into_group( + name = "editor-toolbar", + srcs = [ + "editor-toolbar.js", + ], + package = "//ts/editor-toolbar", +) + filegroup( name = "js", srcs = [ "aqt_es5", "editor", + "editor-toolbar", "mathjax.js", "//qt/aqt/data/web/js/vendor", ], diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index e6db3b05f..97b416c1b 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -82,9 +82,7 @@ _html = """ }
-
- %s -
+
@@ -137,111 +135,62 @@ class Editor: self.web.set_bridge_command(self.onBridgeCmd, self) self.outerLayout.addWidget(self.web, 1) - lefttopbtns: List[str] = [ - self._addButton( - None, - "fields", - tr.editing_customize_fields(), - f"{tr.editing_fields()}...", - disables=False, - rightside=False, - ), - self._addButton( - None, - "cards", - tr.editing_customize_card_templates_ctrlandl(), - f"{tr.editing_cards()}...", - disables=False, - rightside=False, - ), - ] - - gui_hooks.editor_did_init_left_buttons(lefttopbtns, self) - - righttopbtns: List[str] = [ - self._addButton( - "text_bold", "bold", tr.editing_bold_text_ctrlandb(), id="bold" - ), - self._addButton( - "text_italic", - "italic", - tr.editing_italic_text_ctrlandi(), - id="italic", - ), - self._addButton( - "text_under", - "underline", - tr.editing_underline_text_ctrlandu(), - id="underline", - ), - self._addButton( - "text_super", - "super", - tr.editing_superscript_ctrlandand(), - id="superscript", - ), - self._addButton( - "text_sub", "sub", tr.editing_subscript_ctrland(), id="subscript" - ), - self._addButton( - "text_clear", "clear", tr.editing_remove_formatting_ctrlandr() - ), - self._addButton( - None, - "colour", - tr.editing_set_foreground_colour_f7(), - """ - -""", - ), - self._addButton( - None, - "changeCol", - tr.editing_change_colour_f8(), - """ - -""", - ), - self._addButton( - "text_cloze", "cloze", tr.editing_cloze_deletion_ctrlandshiftandc() - ), - self._addButton( - "paperclip", "attach", tr.editing_attach_picturesaudiovideo_f3() - ), - self._addButton("media-record", "record", tr.editing_record_audio_f5()), - self._addButton("more", "more"), - ] - - gui_hooks.editor_did_init_buttons(righttopbtns, self) - # legacy filter - righttopbtns = runFilter("setupEditorButtons", righttopbtns, self) - - topbuts = """ -
- %(leftbts)s -
-
- %(rightbts)s -
- """ % dict( - leftbts="".join(lefttopbtns), - rightbts="".join(righttopbtns), - ) bgcol = self.mw.app.palette().window().color().name() # type: ignore # then load page self.web.stdHtml( - _html % (bgcol, topbuts, tr.editing_show_duplicates()), + _html % (bgcol, tr.editing_show_duplicates()), css=[ "css/editor.css", + "css/editor-toolbar.css", ], js=[ "js/vendor/jquery.min.js", + "js/vendor/protobuf.min.js", "js/editor.js", + "js/editor-toolbar.js", ], context=self, default_css=False, ) - self.web.eval("preventButtonFocus();") + + lefttopbtns: List[str] = [] + gui_hooks.editor_did_init_left_buttons(lefttopbtns, self) + + lefttopbtns_defs = [ + f"$editorToolbar.addButton({{ component: editorToolbar.RawButton, html: `{button}` }}, 'notetype');" + for button in lefttopbtns + ] + lefttopbtns_js = "\n".join(lefttopbtns_defs) + + righttopbtns: List[str] = [] + gui_hooks.editor_did_init_buttons(righttopbtns, self) + # legacy filter + righttopbtns = runFilter("setupEditorButtons", righttopbtns, self) + + righttopbtns_defs = "\n".join( + [ + f"{{ component: editorToolbar.RawButton, html: `{button}` }}," + for button in righttopbtns + ] + ) + righttopbtns_js = ( + f""" +$editorToolbar.addButtonGroup({{ + id: "addons", + buttons: [ {righttopbtns_defs} ] +}}); +""" + if righttopbtns_defs + else "" + ) + + self.web.eval( + f""" +$editorToolbar = document.getElementById("editorToolbar"); +{lefttopbtns_js} +{righttopbtns_js} +""" + ) # Top buttons ###################################################################### @@ -353,6 +302,7 @@ class Editor: type="button" title="{tip}" onclick="pycmd('{cmd}');{togglesc}return false;" + onmousedown="window.event.preventDefault();" > {imgelm} {labelelm} @@ -801,7 +751,8 @@ class Editor: self._wrapWithColour(self.fcolour) def _updateForegroundButton(self) -> None: - self.web.eval(f"setFGButton('{self.fcolour}')") + # self.web.eval(f"setFGButton('{self.fcolour}')") + pass def onColourChanged(self) -> None: self._updateForegroundButton() @@ -1112,6 +1063,10 @@ class Editor: dupes=showDupes, paste=onPaste, cutOrCopy=onCutOrCopy, + htmlEdit=onHtmlEdit, + mathjaxInline=insertMathjaxInline, + mathjaxBlock=insertMathjaxBlock, + mathjaxChemistry=insertMathjaxChemistry, ) @@ -1346,3 +1301,17 @@ gui_hooks.editor_will_use_font_for_field.append(fontMungeHack) gui_hooks.editor_will_munge_html.append(munge_html) gui_hooks.editor_will_munge_html.append(remove_null_bytes) gui_hooks.editor_will_munge_html.append(reverse_url_quoting) + + +def set_cloze_button(editor: Editor) -> None: + if editor.note.model()["type"] == MODEL_CLOZE: + editor.web.eval( + 'document.getElementById("editorToolbar").showButton("template", "cloze"); ' + ) + else: + editor.web.eval( + 'document.getElementById("editorToolbar").hideButton("template", "cloze"); ' + ) + + +gui_hooks.editor_did_load_note.append(set_cloze_button) diff --git a/ts/deckconfig/BUILD.bazel b/ts/deckconfig/BUILD.bazel index 7aa188b90..48bc75add 100644 --- a/ts/deckconfig/BUILD.bazel +++ b/ts/deckconfig/BUILD.bazel @@ -7,8 +7,8 @@ load("//ts:vendor.bzl", "copy_bootstrap_icons") load("//ts:compile_sass.bzl", "compile_sass") compile_sass( - srcs = ["deckconfig-base.scss"], group = "base_css", + srcs = ["deckconfig-base.scss"], visibility = ["//visibility:public"], deps = [ "//ts/sass:base_lib", diff --git a/ts/editor-toolbar/BUILD.bazel b/ts/editor-toolbar/BUILD.bazel new file mode 100644 index 000000000..3c3071d1e --- /dev/null +++ b/ts/editor-toolbar/BUILD.bazel @@ -0,0 +1,150 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_library") +load("//ts/svelte:svelte.bzl", "compile_svelte", "svelte_check") +load("//ts:prettier.bzl", "prettier_test") +load("//ts:eslint.bzl", "eslint_test") +load("//ts:esbuild.bzl", "esbuild") +load("//ts:compile_sass.bzl", "compile_sass") +load("//ts:vendor.bzl", "copy_bootstrap_icons", "copy_mdi_icons") + +svelte_files = glob(["*.svelte"]) + +svelte_names = [f.replace(".svelte", "") for f in svelte_files] + +compile_svelte( + name = "svelte", + srcs = svelte_files, + deps = [ + "//ts/sass:button_mixins_lib", + "//ts/sass/bootstrap", + ], +) + +compile_sass( + group = "local_css", + srcs = [ + "color.scss", + "legacy.scss", + "bootstrap.scss", + ], + deps = [ + "//ts/sass:button_mixins_lib", + "//ts/sass/bootstrap", + ], + visibility = ["//visibility:public"], +) + +ts_library( + name = "index", + srcs = ["index.ts"], + deps = [ + "EditorToolbar", + "lib", + "//ts/lib", + "//ts/sveltelib", + "@npm//svelte", + "@npm//svelte2tsx", + ], +) + +ts_library( + name = "lib", + srcs = glob( + ["*.ts"], + exclude = ["index.ts"], + ), + deps = [ + "//ts/lib", + "//ts/lib:backend_proto", + "//ts/sveltelib", + "//ts:image_module_support", + "@npm//svelte", + "@npm//bootstrap", + "@npm//@popperjs/core", + "@npm//@types/bootstrap", + ], +) + +copy_bootstrap_icons( + name = "bootstrap-icons", + icons = [ + "type-bold.svg", + "type-italic.svg", + "type-underline.svg", + "eraser.svg", + "square-fill.svg", + "paperclip.svg", + "mic.svg", + ], +) + +copy_mdi_icons( + name = "mdi-icons", + icons = [ + "format-superscript.svg", + "format-subscript.svg", + "function-variant.svg", + "code-brackets.svg", + "xml.svg", + ], +) + +esbuild( + name = "editor-toolbar", + srcs = [ + "//ts:protobuf-shim.js", + ], + args = [ + "--global-name=editorToolbar", + "--inject:$(location //ts:protobuf-shim.js)", + "--resolve-extensions=.mjs,.js", + "--loader:.svg=text", + ], + entry_point = "index.ts", + external = [ + "protobufjs/light", + ], + output_css = "editor-toolbar.css", + visibility = ["//visibility:public"], + deps = [ + "//ts/lib", + "//ts/lib:backend_proto", + "//ts:image_module_support", + "index", + "bootstrap-icons", + "mdi-icons", + "local_css", + ] + svelte_names, +) + +# Tests +################ + +prettier_test( + name = "format_check", + srcs = glob([ + "*.ts", + "*.svelte", + ]), +) + +eslint_test( + name = "eslint", + srcs = glob( + [ + "*.ts", + ], + ), +) + +svelte_check( + name = "svelte_check", + srcs = glob([ + "*.ts", + "*.svelte", + ]) + [ + "//ts/sass:button_mixins_lib", + "//ts/sass/bootstrap", + "@npm//@types/bootstrap", + ], +) + diff --git a/ts/editor-toolbar/ButtonDropdown.svelte b/ts/editor-toolbar/ButtonDropdown.svelte new file mode 100644 index 000000000..fa1c5b26c --- /dev/null +++ b/ts/editor-toolbar/ButtonDropdown.svelte @@ -0,0 +1,19 @@ + + + + diff --git a/ts/editor-toolbar/ButtonGroup.d.ts b/ts/editor-toolbar/ButtonGroup.d.ts new file mode 100644 index 000000000..e4a2d7ce5 --- /dev/null +++ b/ts/editor-toolbar/ButtonGroup.d.ts @@ -0,0 +1,9 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +import type { ToolbarItem } from "./types"; + +export interface ButtonGroupProps { + id: string; + className?: string; + buttons: ToolbarItem[]; +} diff --git a/ts/editor-toolbar/ButtonGroup.svelte b/ts/editor-toolbar/ButtonGroup.svelte new file mode 100644 index 000000000..18ff29fe0 --- /dev/null +++ b/ts/editor-toolbar/ButtonGroup.svelte @@ -0,0 +1,85 @@ + + + + + +
    + {#each buttons as button} + {#if !button.hidden} +
  • + +
  • + {/if} + {/each} +
diff --git a/ts/editor-toolbar/ColorPicker.d.ts b/ts/editor-toolbar/ColorPicker.d.ts new file mode 100644 index 000000000..fff74056e --- /dev/null +++ b/ts/editor-toolbar/ColorPicker.d.ts @@ -0,0 +1,8 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +export interface ColorPickerProps { + id?: string; + className?: string; + tooltip: string; + onChange: (event: Event) => void; +} diff --git a/ts/editor-toolbar/ColorPicker.svelte b/ts/editor-toolbar/ColorPicker.svelte new file mode 100644 index 000000000..f491531c8 --- /dev/null +++ b/ts/editor-toolbar/ColorPicker.svelte @@ -0,0 +1,69 @@ + + + + + + diff --git a/ts/editor-toolbar/CommandIconButton.d.ts b/ts/editor-toolbar/CommandIconButton.d.ts new file mode 100644 index 000000000..2fe43d032 --- /dev/null +++ b/ts/editor-toolbar/CommandIconButton.d.ts @@ -0,0 +1,10 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +export interface CommandIconButtonProps { + id?: string; + className?: string; + tooltip: string; + icon: string; + command: string; + activatable?: boolean; +} diff --git a/ts/editor-toolbar/CommandIconButton.svelte b/ts/editor-toolbar/CommandIconButton.svelte new file mode 100644 index 000000000..ec53827fc --- /dev/null +++ b/ts/editor-toolbar/CommandIconButton.svelte @@ -0,0 +1,80 @@ + + + + + + + {@html icon} + diff --git a/ts/editor-toolbar/DropdownItem.d.ts b/ts/editor-toolbar/DropdownItem.d.ts new file mode 100644 index 000000000..81c9138aa --- /dev/null +++ b/ts/editor-toolbar/DropdownItem.d.ts @@ -0,0 +1,11 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +export interface DropdownItemProps { + id?: string; + className?: string; + tooltip: string; + + onClick: (event: MouseEvent) => void; + label: string; + endLabel: string; +} diff --git a/ts/editor-toolbar/DropdownItem.svelte b/ts/editor-toolbar/DropdownItem.svelte new file mode 100644 index 000000000..3a772503e --- /dev/null +++ b/ts/editor-toolbar/DropdownItem.svelte @@ -0,0 +1,67 @@ + + + + + + diff --git a/ts/editor-toolbar/DropdownMenu.d.ts b/ts/editor-toolbar/DropdownMenu.d.ts new file mode 100644 index 000000000..9e43bc953 --- /dev/null +++ b/ts/editor-toolbar/DropdownMenu.d.ts @@ -0,0 +1,8 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +import type { ToolbarItem } from "./types"; + +export interface DropdownMenuProps { + id: string; + menuItems: ToolbarItem[]; +} diff --git a/ts/editor-toolbar/DropdownMenu.svelte b/ts/editor-toolbar/DropdownMenu.svelte new file mode 100644 index 000000000..4ddb2f888 --- /dev/null +++ b/ts/editor-toolbar/DropdownMenu.svelte @@ -0,0 +1,35 @@ + + + + + + diff --git a/ts/editor-toolbar/EditorToolbar.svelte b/ts/editor-toolbar/EditorToolbar.svelte new file mode 100644 index 000000000..13a0d9b7c --- /dev/null +++ b/ts/editor-toolbar/EditorToolbar.svelte @@ -0,0 +1,78 @@ + + + + + + + +
+ {#each _menus as menu} + + {/each} +
+ + diff --git a/ts/editor-toolbar/IconButton.d.ts b/ts/editor-toolbar/IconButton.d.ts new file mode 100644 index 000000000..3de549cbe --- /dev/null +++ b/ts/editor-toolbar/IconButton.d.ts @@ -0,0 +1,9 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +export interface IconButtonProps { + id?: string; + className?: string; + tooltip: string; + icon: string; + onClick: (event: MouseEvent) => void; +} diff --git a/ts/editor-toolbar/IconButton.svelte b/ts/editor-toolbar/IconButton.svelte new file mode 100644 index 000000000..ff9e0a0d4 --- /dev/null +++ b/ts/editor-toolbar/IconButton.svelte @@ -0,0 +1,20 @@ + + + + + {@html icon} + diff --git a/ts/editor-toolbar/LabelButton.d.ts b/ts/editor-toolbar/LabelButton.d.ts new file mode 100644 index 000000000..14b713dd7 --- /dev/null +++ b/ts/editor-toolbar/LabelButton.d.ts @@ -0,0 +1,11 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +export interface LabelButtonProps { + id?: string; + className?: string; + + label: string; + tooltip: string; + onClick: (event: MouseEvent) => void; + disables?: boolean; +} diff --git a/ts/editor-toolbar/LabelButton.svelte b/ts/editor-toolbar/LabelButton.svelte new file mode 100644 index 000000000..0b838ce2e --- /dev/null +++ b/ts/editor-toolbar/LabelButton.svelte @@ -0,0 +1,69 @@ + + + + + + diff --git a/ts/editor-toolbar/RawButton.svelte b/ts/editor-toolbar/RawButton.svelte new file mode 100644 index 000000000..cdee475ea --- /dev/null +++ b/ts/editor-toolbar/RawButton.svelte @@ -0,0 +1,14 @@ + + + +{@html html} diff --git a/ts/editor-toolbar/SelectButton.svelte b/ts/editor-toolbar/SelectButton.svelte new file mode 100644 index 000000000..559ab8cfc --- /dev/null +++ b/ts/editor-toolbar/SelectButton.svelte @@ -0,0 +1,70 @@ + + + + + + diff --git a/ts/editor-toolbar/SelectOption.svelte b/ts/editor-toolbar/SelectOption.svelte new file mode 100644 index 000000000..c8ea91390 --- /dev/null +++ b/ts/editor-toolbar/SelectOption.svelte @@ -0,0 +1,11 @@ + + + + diff --git a/ts/editor-toolbar/SquareButton.svelte b/ts/editor-toolbar/SquareButton.svelte new file mode 100644 index 000000000..f0fc17e1b --- /dev/null +++ b/ts/editor-toolbar/SquareButton.svelte @@ -0,0 +1,88 @@ + + + + + + diff --git a/ts/editor-toolbar/WithDropdownMenu.d.ts b/ts/editor-toolbar/WithDropdownMenu.d.ts new file mode 100644 index 000000000..d0f163568 --- /dev/null +++ b/ts/editor-toolbar/WithDropdownMenu.d.ts @@ -0,0 +1,8 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +import type { ToolbarItem } from "./types"; + +export interface WithDropdownMenuProps { + button: ToolbarItem; + menuId: string; +} diff --git a/ts/editor-toolbar/WithDropdownMenu.svelte b/ts/editor-toolbar/WithDropdownMenu.svelte new file mode 100644 index 000000000..bf48a4152 --- /dev/null +++ b/ts/editor-toolbar/WithDropdownMenu.svelte @@ -0,0 +1,53 @@ + + + + diff --git a/ts/editor-toolbar/bootstrap.scss b/ts/editor-toolbar/bootstrap.scss new file mode 100644 index 000000000..0988a6de7 --- /dev/null +++ b/ts/editor-toolbar/bootstrap.scss @@ -0,0 +1,8 @@ +@import "ts/sass/bootstrap/functions"; +@import "ts/sass/bootstrap/variables"; +@import "ts/sass/bootstrap/mixins"; + +$btn-disabled-opacity: 0.4; + +@import "ts/sass/bootstrap/buttons"; +@import "ts/sass/bootstrap/dropdown"; diff --git a/ts/editor-toolbar/cloze.ts b/ts/editor-toolbar/cloze.ts new file mode 100644 index 000000000..55fd8ea60 --- /dev/null +++ b/ts/editor-toolbar/cloze.ts @@ -0,0 +1,48 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +import IconButton from "./IconButton.svelte"; +import type { IconButtonProps } from "./IconButton"; + +import { DynamicSvelteComponent, dynamicComponent } from "sveltelib/dynamicComponent"; +import * as tr from "anki/i18n"; + +import bracketsIcon from "./code-brackets.svg"; + +const clozePattern = /\{\{c(\d+)::/gu; +function getCurrentHighestCloze(increment: boolean): number { + let highest = 0; + + // @ts-expect-error + forEditorField([], (field) => { + const matches = field.editingArea.editable.fieldHTML.matchAll(clozePattern); + highest = Math.max( + highest, + ...[...matches].map((match: RegExpMatchArray): number => Number(match[1])) + ); + }); + + if (increment) { + highest++; + } + + return Math.max(1, highest); +} + +function onCloze(event: MouseEvent): void { + const highestCloze = getCurrentHighestCloze(!event.altKey); + + // @ts-expect-error + wrap(`{{c${highestCloze}::`, "}}"); +} + +const iconButton = dynamicComponent(IconButton); + +export function getClozeButton(): DynamicSvelteComponent & + IconButtonProps { + return iconButton({ + id: "cloze", + icon: bracketsIcon, + onClick: onCloze, + tooltip: tr.editingClozeDeletionCtrlandshiftandc(), + }); +} diff --git a/ts/editor-toolbar/color.scss b/ts/editor-toolbar/color.scss new file mode 100644 index 000000000..3dcd5e3e8 --- /dev/null +++ b/ts/editor-toolbar/color.scss @@ -0,0 +1,7 @@ +:root { + --foreground-color: black; +} + +.forecolor { + color: var(--foreground-color) !important; +} diff --git a/ts/editor-toolbar/color.ts b/ts/editor-toolbar/color.ts new file mode 100644 index 000000000..2ed6f932b --- /dev/null +++ b/ts/editor-toolbar/color.ts @@ -0,0 +1,53 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +import IconButton from "./IconButton.svelte"; +import type { IconButtonProps } from "./IconButton"; +import ColorPicker from "./ColorPicker.svelte"; +import type { ColorPickerProps } from "./ColorPicker"; +import ButtonGroup from "./ButtonGroup.svelte"; +import type { ButtonGroupProps } from "./ButtonGroup"; + +import { DynamicSvelteComponent, dynamicComponent } from "sveltelib/dynamicComponent"; +import * as tr from "anki/i18n"; + +import squareFillIcon from "./square-fill.svg"; +import "./color.css"; + +const foregroundColorKeyword = "--foreground-color"; + +function setForegroundColor(color: string): void { + document.documentElement.style.setProperty(foregroundColorKeyword, color); +} + +function getForecolor(): string { + return document.documentElement.style.getPropertyValue(foregroundColorKeyword); +} + +function wrapWithForecolor(color: string): void { + document.execCommand("forecolor", false, color); +} + +const iconButton = dynamicComponent(IconButton); +const colorPicker = dynamicComponent(ColorPicker); +const buttonGroup = dynamicComponent(ButtonGroup); + +export function getColorGroup(): DynamicSvelteComponent & + ButtonGroupProps { + const forecolorButton = iconButton({ + icon: squareFillIcon, + className: "forecolor", + onClick: () => wrapWithForecolor(getForecolor()), + tooltip: tr.editingSetForegroundColourF7(), + }); + + const colorpickerButton = colorPicker({ + onChange: ({ currentTarget }) => + setForegroundColor((currentTarget as HTMLInputElement).value), + tooltip: tr.editingChangeColourF8(), + }); + + return buttonGroup({ + id: "color", + buttons: [forecolorButton, colorpickerButton], + }); +} diff --git a/ts/editor-toolbar/contextKeys.ts b/ts/editor-toolbar/contextKeys.ts new file mode 100644 index 000000000..0d3dc587f --- /dev/null +++ b/ts/editor-toolbar/contextKeys.ts @@ -0,0 +1,4 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +export const nightModeKey = Symbol("nightMode"); +export const disabledKey = Symbol("disabled"); diff --git a/ts/editor-toolbar/format.ts b/ts/editor-toolbar/format.ts new file mode 100644 index 000000000..b555f8ae8 --- /dev/null +++ b/ts/editor-toolbar/format.ts @@ -0,0 +1,74 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +import CommandIconButton from "./CommandIconButton.svelte"; +import type { CommandIconButtonProps } from "./CommandIconButton"; +import ButtonGroup from "./ButtonGroup.svelte"; +import type { ButtonGroupProps } from "./ButtonGroup"; + +import { DynamicSvelteComponent, dynamicComponent } from "sveltelib/dynamicComponent"; +import * as tr from "anki/i18n"; + +import boldIcon from "./type-bold.svg"; +import italicIcon from "./type-italic.svg"; +import underlineIcon from "./type-underline.svg"; +import superscriptIcon from "./format-superscript.svg"; +import subscriptIcon from "./format-subscript.svg"; +import eraserIcon from "./eraser.svg"; + +const commandIconButton = dynamicComponent< + typeof CommandIconButton, + CommandIconButtonProps +>(CommandIconButton); +const buttonGroup = dynamicComponent(ButtonGroup); + +export function getFormatGroup(): DynamicSvelteComponent & + ButtonGroupProps { + const boldButton = commandIconButton({ + icon: boldIcon, + command: "bold", + tooltip: tr.editingBoldTextCtrlandb(), + }); + + const italicButton = commandIconButton({ + icon: italicIcon, + command: "italic", + tooltip: tr.editingItalicTextCtrlandi(), + }); + + const underlineButton = commandIconButton({ + icon: underlineIcon, + command: "underline", + tooltip: tr.editingUnderlineTextCtrlandu(), + }); + + const superscriptButton = commandIconButton({ + icon: superscriptIcon, + command: "superscript", + tooltip: tr.editingSuperscriptCtrlandand(), + }); + + const subscriptButton = commandIconButton({ + icon: subscriptIcon, + command: "subscript", + tooltip: tr.editingSubscriptCtrland(), + }); + + const removeFormatButton = commandIconButton({ + icon: eraserIcon, + command: "removeFormat", + activatable: false, + tooltip: tr.editingRemoveFormattingCtrlandr(), + }); + + return buttonGroup({ + id: "format", + buttons: [ + boldButton, + italicButton, + underlineButton, + superscriptButton, + subscriptButton, + removeFormatButton, + ], + }); +} diff --git a/ts/editor-toolbar/identifiable.ts b/ts/editor-toolbar/identifiable.ts new file mode 100644 index 000000000..41030f094 --- /dev/null +++ b/ts/editor-toolbar/identifiable.ts @@ -0,0 +1,49 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +export interface Identifiable { + id?: string; +} + +function normalize( + values: T[], + idOrIndex: string | number +): number { + const normalizedIndex = + typeof idOrIndex === "string" + ? values.findIndex((value) => value.id === idOrIndex) + : idOrIndex >= 0 + ? idOrIndex + : values.length + idOrIndex; + + return normalizedIndex >= values.length ? -1 : normalizedIndex; +} + +export function search( + values: T[], + idOrIndex: string | number +): T | null { + const index = normalize(values, idOrIndex); + return index >= 0 ? values[index] : null; +} + +export function insert( + values: T[], + value: T, + idOrIndex: string | number +): T[] { + const index = normalize(values, idOrIndex); + return index >= 0 + ? [...values.slice(0, index), value, ...values.slice(index)] + : values; +} + +export function add( + values: T[], + value: T, + idOrIndex: string | number +): T[] { + const index = normalize(values, idOrIndex); + return index >= 0 + ? [...values.slice(0, index + 1), value, ...values.slice(index + 1)] + : values; +} diff --git a/ts/editor-toolbar/index.ts b/ts/editor-toolbar/index.ts new file mode 100644 index 000000000..1d6e0f4b0 --- /dev/null +++ b/ts/editor-toolbar/index.ts @@ -0,0 +1,216 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +import type { SvelteComponentDev } from "svelte/internal"; +import type { ToolbarItem } from "./types"; + +import ButtonGroup from "./ButtonGroup.svelte"; +import type { ButtonGroupProps } from "./ButtonGroup"; + +import { dynamicComponent } from "sveltelib/dynamicComponent"; +import { Writable, writable } from "svelte/store"; + +import EditorToolbarSvelte from "./EditorToolbar.svelte"; + +import { setupI18n, ModuleName } from "anki/i18n"; + +import "./bootstrap.css"; + +import { getNotetypeGroup } from "./notetype"; +import { getFormatGroup } from "./format"; +import { getColorGroup } from "./color"; +import { getTemplateGroup, getTemplateMenus } from "./template"; +import { Identifiable, search, add, insert } from "./identifiable"; + +interface Hideable { + hidden?: boolean; +} + +function showComponent(component: Hideable): void { + component.hidden = false; +} + +function hideComponent(component: Hideable): void { + component.hidden = true; +} + +function toggleComponent(component: Hideable): void { + component.hidden = !component.hidden; +} + +const buttonGroup = dynamicComponent(ButtonGroup); + +let buttonsResolve: ( + value: Writable<(ToolbarItem & ButtonGroupProps)[]> +) => void; +let menusResolve: (value: Writable) => void; + +class EditorToolbar extends HTMLElement { + component?: SvelteComponentDev; + + buttonsPromise: Promise< + Writable<(ToolbarItem & ButtonGroupProps)[]> + > = new Promise((resolve) => { + buttonsResolve = resolve; + }); + menusPromise: Promise> = new Promise((resolve): void => { + menusResolve = resolve; + }); + + connectedCallback(): void { + setupI18n({ modules: [ModuleName.EDITING] }).then(() => { + const buttons = writable([ + getNotetypeGroup(), + getFormatGroup(), + getColorGroup(), + getTemplateGroup(), + ]); + const menus = writable([...getTemplateMenus()]); + + this.component = new EditorToolbarSvelte({ + target: this, + props: { + buttons, + menus, + nightMode: document.documentElement.classList.contains( + "night-mode" + ), + }, + }); + + buttonsResolve(buttons); + menusResolve(menus); + }); + } + + updateButtonGroup( + update: ( + component: ToolbarItem & ButtonGroupProps & T + ) => void, + group: string | number + ): void { + this.buttonsPromise.then((buttons) => { + buttons.update((buttonGroups) => { + const foundGroup = search(buttonGroups, group); + + if (foundGroup) { + update( + foundGroup as ToolbarItem & + ButtonGroupProps & + T + ); + } + + return buttonGroups; + }); + + return buttons; + }); + } + + showButtonGroup(group: string | number): void { + this.updateButtonGroup(showComponent, group); + } + + hideButtonGroup(group: string | number): void { + this.updateButtonGroup(hideComponent, group); + } + + toggleButtonGroup(group: string | number): void { + this.updateButtonGroup(toggleComponent, group); + } + + insertButtonGroup(newGroup: ButtonGroupProps, group: string | number = 0): void { + this.buttonsPromise.then((buttons) => { + buttons.update((buttonGroups) => { + const newButtonGroup = buttonGroup(newGroup); + return insert(buttonGroups, newButtonGroup, group); + }); + + return buttons; + }); + } + + addButtonGroup(newGroup: ButtonGroupProps, group: string | number = -1): void { + this.buttonsPromise.then((buttons) => { + buttons.update((buttonGroups) => { + const newButtonGroup = buttonGroup(newGroup); + return add(buttonGroups, newButtonGroup, group); + }); + + return buttons; + }); + } + + updateButton( + update: (component: ToolbarItem) => void, + group: string | number, + button: string | number + ): void { + this.updateButtonGroup((foundGroup) => { + const foundButton = search(foundGroup.buttons, button); + + if (foundButton) { + update(foundButton); + } + }, group); + } + + showButton(group: string | number, button: string | number): void { + this.updateButton(showComponent, group, button); + } + + hideButton(group: string | number, button: string | number): void { + this.updateButton(hideComponent, group, button); + } + + toggleButton(group: string | number, button: string | number): void { + this.updateButton(toggleComponent, group, button); + } + + insertButton( + newButton: ToolbarItem & Identifiable, + group: string | number, + button: string | number = 0 + ): void { + this.updateButtonGroup((component) => { + component.buttons = insert( + component.buttons as (ToolbarItem & Identifiable)[], + newButton, + button + ); + }, group); + } + + addButton( + newButton: ToolbarItem & Identifiable, + group: string | number, + button: string | number = -1 + ): void { + this.updateButtonGroup((component) => { + component.buttons = add( + component.buttons as (ToolbarItem & Identifiable)[], + newButton, + button + ); + }, group); + } +} + +customElements.define("anki-editor-toolbar", EditorToolbar); + +/* Exports for editor + * @ts-expect-error */ +export { updateActiveButtons, clearActiveButtons } from "./CommandIconButton.svelte"; +export { enableButtons, disableButtons } from "./EditorToolbar.svelte"; + +/* Exports for add-ons */ +export { default as RawButton } from "./RawButton.svelte"; +export { default as LabelButton } from "./LabelButton.svelte"; +export { default as IconButton } from "./IconButton.svelte"; +export { default as CommandIconButton } from "./CommandIconButton.svelte"; +export { default as SelectButton } from "./SelectButton.svelte"; + +export { default as DropdownMenu } from "./DropdownMenu.svelte"; +export { default as DropdownItem } from "./DropdownItem.svelte"; +export { default as ButtonDropdown } from "./DropdownMenu.svelte"; +export { default as WithDropdownMenu } from "./WithDropdownMenu.svelte"; diff --git a/ts/editor-toolbar/legacy.scss b/ts/editor-toolbar/legacy.scss new file mode 100644 index 000000000..767ca7677 --- /dev/null +++ b/ts/editor-toolbar/legacy.scss @@ -0,0 +1,10 @@ +.linkb { + display: inline-block; +} + +.topbut { + display: inline-block; + vertical-align: middle; + width: calc(var(--toolbar-size) - 12px); + height: calc(var(--toolbar-size) - 12px); +} diff --git a/ts/editor-toolbar/notetype.ts b/ts/editor-toolbar/notetype.ts new file mode 100644 index 000000000..80a14095d --- /dev/null +++ b/ts/editor-toolbar/notetype.ts @@ -0,0 +1,35 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +import LabelButton from "./LabelButton.svelte"; +import type { LabelButtonProps } from "./LabelButton"; +import ButtonGroup from "./ButtonGroup.svelte"; +import type { ButtonGroupProps } from "./ButtonGroup"; + +import { DynamicSvelteComponent, dynamicComponent } from "sveltelib/dynamicComponent"; +import { bridgeCommand } from "anki/bridgecommand"; +import * as tr from "anki/i18n"; + +const labelButton = dynamicComponent(LabelButton); +const buttonGroup = dynamicComponent(ButtonGroup); + +export function getNotetypeGroup(): DynamicSvelteComponent & + ButtonGroupProps { + const fieldsButton = labelButton({ + onClick: () => bridgeCommand("fields"), + disables: false, + label: `${tr.editingFields()}...`, + tooltip: tr.editingCustomizeFields(), + }); + + const cardsButton = labelButton({ + onClick: () => bridgeCommand("cards"), + disables: false, + label: `${tr.editingCards()}...`, + tooltip: tr.editingCustomizeCardTemplatesCtrlandl(), + }); + + return buttonGroup({ + id: "notetype", + buttons: [fieldsButton, cardsButton], + }); +} diff --git a/ts/editor-toolbar/template.ts b/ts/editor-toolbar/template.ts new file mode 100644 index 000000000..90ca847a2 --- /dev/null +++ b/ts/editor-toolbar/template.ts @@ -0,0 +1,144 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +import IconButton from "./IconButton.svelte"; +import type { IconButtonProps } from "./IconButton"; +import DropdownMenu from "./DropdownMenu.svelte"; +import type { DropdownMenuProps } from "./DropdownMenu"; +import DropdownItem from "./DropdownItem.svelte"; +import type { DropdownItemProps } from "./DropdownItem"; +import WithDropdownMenu from "./WithDropdownMenu.svelte"; +import type { WithDropdownMenuProps } from "./WithDropdownMenu"; +import ButtonGroup from "./ButtonGroup.svelte"; +import type { ButtonGroupProps } from "./ButtonGroup"; + +import { bridgeCommand } from "anki/bridgecommand"; +import { DynamicSvelteComponent, dynamicComponent } from "sveltelib/dynamicComponent"; +import * as tr from "anki/i18n"; + +import paperclipIcon from "./paperclip.svg"; +import micIcon from "./mic.svg"; +import functionIcon from "./function-variant.svg"; +import xmlIcon from "./xml.svg"; + +import { getClozeButton } from "./cloze"; + +function onAttachment(): void { + bridgeCommand("attach"); +} + +function onRecord(): void { + bridgeCommand("record"); +} + +function onHtmlEdit(): void { + bridgeCommand("htmlEdit"); +} + +const mathjaxMenuId = "mathjaxMenu"; + +const iconButton = dynamicComponent(IconButton); +const withDropdownMenu = dynamicComponent< + typeof WithDropdownMenu, + WithDropdownMenuProps +>(WithDropdownMenu); +const dropdownMenu = dynamicComponent( + DropdownMenu +); +const dropdownItem = dynamicComponent( + DropdownItem +); +const buttonGroup = dynamicComponent(ButtonGroup); + +export function getTemplateGroup(): DynamicSvelteComponent & + ButtonGroupProps { + const attachmentButton = iconButton({ + icon: paperclipIcon, + onClick: onAttachment, + tooltip: tr.editingAttachPicturesaudiovideoF3(), + }); + + const recordButton = iconButton({ + icon: micIcon, + onClick: onRecord, + tooltip: tr.editingRecordAudioF5(), + }); + + const mathjaxButton = iconButton({ + icon: functionIcon, + foo: 5, + }); + + const mathjaxButtonWithMenu = withDropdownMenu({ + button: mathjaxButton, + menuId: mathjaxMenuId, + }); + + const htmlButton = iconButton({ + icon: xmlIcon, + onClick: onHtmlEdit, + tooltip: tr.editingHtmlEditor, + }); + + return buttonGroup({ + id: "template", + buttons: [ + attachmentButton, + recordButton, + getClozeButton(), + mathjaxButtonWithMenu, + htmlButton, + ], + }); +} + +export function getTemplateMenus(): (DynamicSvelteComponent & + DropdownMenuProps)[] { + const mathjaxMenuItems = [ + dropdownItem({ + // @ts-expect-error + onClick: () => wrap("\\(", "\\)"), + label: tr.editingMathjaxInline(), + endLabel: "Ctrl+M, M", + }), + dropdownItem({ + // @ts-expect-error + onClick: () => wrap("\\[", "\\]"), + label: tr.editingMathjaxBlock(), + endLabel: "Ctrl+M, E", + }), + dropdownItem({ + // @ts-expect-error + onClick: () => wrap("\\(\\ce{", "}\\)"), + label: tr.editingMathjaxChemistry(), + endLabel: "Ctrl+M, C", + }), + ]; + + const latexMenuItems = [ + dropdownItem({ + // @ts-expect-error + onClick: () => wrap("[latex]", "[/latex]"), + label: tr.editingLatex(), + endLabel: "Ctrl+T, T", + }), + dropdownItem({ + // @ts-expect-error + onClick: () => wrap("[$]", "[/$]"), + label: tr.editingLatexEquation(), + endLabel: "Ctrl+T, E", + }), + dropdownItem({ + // @ts-expect-error + onClick: () => wrap("[$$]", "[/$$]"), + label: tr.editingLatexMathEnv(), + endLabel: "Ctrl+T, M", + }), + ]; + + const mathjaxMenu = dropdownMenu({ + id: mathjaxMenuId, + menuItems: [...mathjaxMenuItems, ...latexMenuItems], + }); + + return [mathjaxMenu]; +} diff --git a/ts/editor-toolbar/types.d.ts b/ts/editor-toolbar/types.d.ts new file mode 100644 index 000000000..a272d405d --- /dev/null +++ b/ts/editor-toolbar/types.d.ts @@ -0,0 +1,10 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +import type { DynamicSvelteComponent } from "sveltelib/dynamicComponent"; +import type { SvelteComponentDev } from "svelte/internal"; + +interface ToolbarItem + extends DynamicSvelteComponent { + id?: string; + hidden?: boolean; +} diff --git a/ts/editor/editingArea.ts b/ts/editor/editingArea.ts index 737493f6b..5002ba817 100644 --- a/ts/editor/editingArea.ts +++ b/ts/editor/editingArea.ts @@ -6,7 +6,6 @@ import type { Editable } from "./editable"; import { bridgeCommand } from "./lib"; import { onInput, onKey, onKeyUp } from "./inputHandlers"; import { onFocus, onBlur } from "./focusHandlers"; -import { updateButtonState } from "./toolbar"; function onPaste(evt: ClipboardEvent): void { bridgeCommand("paste"); @@ -60,7 +59,8 @@ export class EditingArea extends HTMLDivElement { this.addEventListener("paste", onPaste); this.addEventListener("copy", onCutOrCopy); this.addEventListener("oncut", onCutOrCopy); - this.addEventListener("mouseup", updateButtonState); + // @ts-expect-error + this.addEventListener("mouseup", editorToolbar.updateActiveButtons); const baseStyleSheet = this.baseStyle.sheet as CSSStyleSheet; baseStyleSheet.insertRule("anki-editable {}", 0); @@ -75,7 +75,8 @@ export class EditingArea extends HTMLDivElement { this.removeEventListener("paste", onPaste); this.removeEventListener("copy", onCutOrCopy); this.removeEventListener("oncut", onCutOrCopy); - this.removeEventListener("mouseup", updateButtonState); + // @ts-expect-error + this.removeEventListener("mouseup", editorToolbar.updateActiveButtons); } initialize(color: string, content: string): void { diff --git a/ts/editor/editor.scss b/ts/editor/editor.scss index a23c4bbfe..b8745c982 100644 --- a/ts/editor/editor.scss +++ b/ts/editor/editor.scss @@ -2,7 +2,6 @@ * License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */ @use 'ts/sass/base'; -@use 'ts/sass/buttons'; @use 'ts/sass/scrollbar'; .nightMode { @@ -29,97 +28,6 @@ padding: 0; } -#topbutsOuter { - display: flex; - flex-wrap: wrap; - justify-content: space-between; - - position: sticky; - top: 0; - left: 0; - z-index: 5; - padding: 2px; - - background: var(--bg-color); - font-size: 13px; -} - -.topbuts { - margin-bottom: 2px; - - & > * { - margin: 0 1px; - - &:first-child { - margin-left: 0; - } - - &:last-child { - margin-right: 0; - } - } -} - -.topbut { - display: inline-block; - width: 16px; - height: 16px; - margin-top: 4px; - vertical-align: -0.125em; -} - -.rainbow { - background-image: -webkit-gradient( - linear, - left top, - left bottom, - color-stop(0, #f77), - color-stop(50%, #7f7), - color-stop(100%, #77f) - ); -} - -button.linkb { - -webkit-appearance: none; - margin-bottom: -3px; - border: 0; - box-shadow: none; - padding: 0px 2px; - background: transparent; - - &:disabled { - opacity: 0.3; - cursor: not-allowed; - } - - .nightMode & > img { - filter: invert(180); - } -} - -button:focus { - outline: none; -} - -button.highlighted { - #topbutsleft & { - background-color: lightgrey; - - .nightMode & { - background: linear-gradient(0deg, #333333 0%, #434343 100%); - } - } - - #topbutsright & { - border-bottom: 3px solid black; - border-radius: 3px; - - .nightMode & { - border-bottom-color: white; - } - } -} - #dupes { position: sticky; bottom: 0; diff --git a/ts/editor/focusHandlers.ts b/ts/editor/focusHandlers.ts index 6ca0528dd..fb9f44b8c 100644 --- a/ts/editor/focusHandlers.ts +++ b/ts/editor/focusHandlers.ts @@ -5,13 +5,13 @@ import type { EditingArea } from "./editingArea"; import { saveField } from "./changeTimer"; import { bridgeCommand } from "./lib"; -import { enableButtons, disableButtons } from "./toolbar"; export function onFocus(evt: FocusEvent): void { const currentField = evt.currentTarget as EditingArea; currentField.focusEditable(); bridgeCommand(`focus:${currentField.ord}`); - enableButtons(); + // @ts-expect-error + editorToolbar.enableButtons(); } export function onBlur(evt: FocusEvent): void { @@ -19,5 +19,6 @@ export function onBlur(evt: FocusEvent): void { const currentFieldUnchanged = previousFocus === document.activeElement; saveField(previousFocus, currentFieldUnchanged ? "key" : "blur"); - disableButtons(); + // @ts-expect-error + editorToolbar.disableButtons(); } diff --git a/ts/editor/index.ts b/ts/editor/index.ts index 44101244d..c7e2121b8 100644 --- a/ts/editor/index.ts +++ b/ts/editor/index.ts @@ -5,7 +5,6 @@ import { filterHTML } from "html-filter"; import { caretToEnd } from "./helpers"; import { saveField } from "./changeTimer"; -import { updateButtonState, disableButtons } from "./toolbar"; import { EditorField } from "./editorField"; import { LabelContainer } from "./labelContainer"; @@ -13,7 +12,6 @@ import { EditingArea } from "./editingArea"; import { Editable } from "./editable"; export { setNoteId, getNoteId } from "./noteId"; -export { preventButtonFocus, toggleEditorButton, setFGButton } from "./toolbar"; export { saveNow } from "./changeTimer"; export { wrap, wrapIntoText } from "./wrap"; @@ -43,7 +41,8 @@ export function focusField(n: number): void { if (field) { field.editingArea.focusEditable(); caretToEnd(field.editingArea); - updateButtonState(); + // @ts-expect-error + editorToolbar.updateActiveButtons(); } } @@ -123,7 +122,8 @@ export function setFields(fields: [string, string][]): void { if (!getCurrentField()) { // when initial focus of the window is not on editor (e.g. browser) - disableButtons(); + // @ts-expect-error + editorToolbar.disableButtons(); } } @@ -158,6 +158,7 @@ export function setFormat(cmd: string, arg?: any, nosave: boolean = false): void document.execCommand(cmd, false, arg); if (!nosave) { saveField(getCurrentField() as EditingArea, "key"); - updateButtonState(); + // @ts-expect-error + editorToolbar.updateActiveButtons(); } } diff --git a/ts/editor/inputHandlers.ts b/ts/editor/inputHandlers.ts index afb38bf0f..f4a929763 100644 --- a/ts/editor/inputHandlers.ts +++ b/ts/editor/inputHandlers.ts @@ -4,7 +4,6 @@ import { EditingArea } from "./editingArea"; import { caretToEnd, nodeIsElement } from "./helpers"; import { triggerChangeTimer } from "./changeTimer"; -import { updateButtonState } from "./toolbar"; function inListItem(currentField: EditingArea): boolean { const anchor = currentField.getSelection()!.anchorNode!; @@ -22,7 +21,8 @@ function inListItem(currentField: EditingArea): boolean { export function onInput(event: Event): void { // make sure IME changes get saved triggerChangeTimer(event.currentTarget as EditingArea); - updateButtonState(); + // @ts-ignore + editorToolbar.updateActiveButtons(); } export function onKey(evt: KeyboardEvent): void { @@ -69,7 +69,8 @@ globalThis.addEventListener("keydown", (evt: KeyboardEvent) => { const newFocusTarget = evt.target; if (newFocusTarget instanceof EditingArea) { caretToEnd(newFocusTarget); - updateButtonState(); + // @ts-ignore + editorToolbar.updateActiveButtons(); } }, { once: true } diff --git a/ts/editor/toolbar.ts b/ts/editor/toolbar.ts deleted file mode 100644 index 1e6cdb274..000000000 --- a/ts/editor/toolbar.ts +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright: Ankitects Pty Ltd and contributors -// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - -const highlightButtons = ["bold", "italic", "underline", "superscript", "subscript"]; - -export function updateButtonState(): void { - for (const name of highlightButtons) { - const elem = document.querySelector(`#${name}`) as HTMLElement; - elem.classList.toggle("highlighted", document.queryCommandState(name)); - } - - // fixme: forecolor - // 'col': document.queryCommandValue("forecolor") -} - -function clearButtonHighlight(): void { - for (const name of highlightButtons) { - const elem = document.querySelector(`#${name}`) as HTMLElement; - elem.classList.remove("highlighted"); - } -} - -export function preventButtonFocus(): void { - for (const element of document.querySelectorAll("button.linkb")) { - element.addEventListener("mousedown", (evt: Event) => { - evt.preventDefault(); - }); - } -} - -export function enableButtons(): void { - const buttons = document.querySelectorAll( - "button.linkb" - ) as NodeListOf; - buttons.forEach((elem: HTMLButtonElement): void => { - elem.disabled = false; - }); - updateButtonState(); -} - -export function disableButtons(): void { - const buttons = document.querySelectorAll( - "button.linkb:not(.perm)" - ) as NodeListOf; - buttons.forEach((elem: HTMLButtonElement): void => { - elem.disabled = true; - }); - clearButtonHighlight(); -} - -export function setFGButton(col: string): void { - document.getElementById("forecolor")!.style.backgroundColor = col; -} - -export function toggleEditorButton(buttonOrId: string | HTMLElement): void { - const button = - typeof buttonOrId === "string" - ? (document.getElementById(buttonOrId) as HTMLElement) - : buttonOrId; - button.classList.toggle("highlighted"); -} diff --git a/ts/graphs/AddedGraph.svelte b/ts/graphs/AddedGraph.svelte index e8e789df5..0adab0121 100644 --- a/ts/graphs/AddedGraph.svelte +++ b/ts/graphs/AddedGraph.svelte @@ -4,6 +4,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html --> + +{#if !$loading} + +{/if} diff --git a/ts/sveltelib/dynamicComponent.ts b/ts/sveltelib/dynamicComponent.ts new file mode 100644 index 000000000..4504e8908 --- /dev/null +++ b/ts/sveltelib/dynamicComponent.ts @@ -0,0 +1,19 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +import type { SvelteComponentDev } from "svelte/internal"; + +export interface DynamicSvelteComponent< + T extends typeof SvelteComponentDev = typeof SvelteComponentDev +> { + component: T; + [k: string]: unknown; +} + +export const dynamicComponent = < + Comp extends typeof SvelteComponentDev, + DefaultProps = NonNullable[0]["props"]> +>( + component: Comp +) => (props: Props): DynamicSvelteComponent & Props => { + return { component, ...props }; +}; diff --git a/ts/sveltelib/preferences.ts b/ts/sveltelib/preferences.ts new file mode 100644 index 000000000..a53505f0c --- /dev/null +++ b/ts/sveltelib/preferences.ts @@ -0,0 +1,85 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +// languageServerHost taken from MIT sources - see below. + +import { Writable, writable, get } from "svelte/store"; + +// import pb from "anki/backend_proto"; +// export async function getGraphPreferences(): Promise { +// export async function setGraphPreferences(prefs: PreferencePayload): Promise { +// pb.BackendProto.GraphPreferences.toObject(Preferences, { + +export interface CustomStore extends Writable { + subscribe: (getter: (value: T) => void) => () => void; + set: (value: T) => void; +} + +export type PreferenceStore = { + [K in keyof Omit]: CustomStore; +}; + +export type PreferencePayload = { + [K in keyof Omit]: T[K]; +}; + +export type PreferenceRaw = { + [K in keyof T]: T[K]; +}; + +function createPreference( + initialValue: T, + savePreferences: () => void +): CustomStore { + const { subscribe, set, update } = writable(initialValue); + + return { + subscribe, + set: (value: T): void => { + set(value); + savePreferences(); + }, + update: (updater: (value: T) => T): void => { + update(updater); + savePreferences(); + }, + }; +} + +function preparePreferences( + Preferences: T, + setter: (payload: PreferencePayload) => Promise, + toObject: (preferences: T, options: { defaults: boolean }) => PreferenceRaw +): PreferenceStore { + const preferences: Partial> = {}; + + function constructPreferences(): PreferencePayload { + const payload: Partial> = {}; + + for (const key in preferences as PreferenceStore) { + payload[key] = get(preferences[key]); + } + + return payload as PreferencePayload; + } + + function savePreferences(): void { + setter(constructPreferences()); + } + + for (const [key, value] of Object.entries( + toObject(Preferences, { defaults: true }) + )) { + preferences[key] = createPreference(value, savePreferences); + } + + return preferences as PreferenceStore; +} + +export async function getPreferences( + getter: () => Promise, + setter: (payload: PreferencePayload) => Promise, + toObject: (preferences: T, options: { defaults: boolean }) => PreferenceRaw +): Promise> { + const initialPreferences = await getter(); + return preparePreferences(initialPreferences, setter, toObject); +} diff --git a/ts/vendor.bzl b/ts/vendor.bzl index 9eb4dcde6..13b948adc 100644 --- a/ts/vendor.bzl +++ b/ts/vendor.bzl @@ -4,8 +4,11 @@ Helpers to copy runtime dependencies from node_modules. load("//ts:copy.bzl", "copy_select_files") +def _npm_base_from_name(name): + return "external/npm/node_modules/{}/".format(name) + def _vendor_js_lib_impl(ctx): - base = ctx.attr.base or "external/npm/node_modules/{}/".format(ctx.attr.name) + base = ctx.attr.base or _npm_base_from_name(ctx.attr.name) return copy_select_files( ctx = ctx, files = ctx.attr.pkg.files, @@ -27,7 +30,8 @@ vendor_js_lib = rule( ) def pkg_from_name(name): - return "@npm//{0}:{0}__files".format(name) + tail = name.split("/")[-1] + return "@npm//{0}:{1}__files".format(name, tail) # # These could be defined directly in BUILD files, but defining them as @@ -126,3 +130,14 @@ def copy_bootstrap_icons(name = "bootstrap-icons", icons = [], visibility = ["// strip_prefix = "icons/", visibility = visibility, ) + +def copy_mdi_icons(name = "mdi-icons", icons = [], visibility = ["//visibility:public"]): + vendor_js_lib( + name = name, + pkg = pkg_from_name("@mdi/svg"), + base = _npm_base_from_name("@mdi/svg"), + include = ["svg/{}".format(icon) for icon in icons], + strip_prefix = "svg/", + visibility = visibility, + ) + diff --git a/ts/yarn.lock b/ts/yarn.lock index 5d928e4de..a3bd50347 100644 --- a/ts/yarn.lock +++ b/ts/yarn.lock @@ -503,6 +503,11 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" +"@mdi/svg@^5.9.55": + version "5.9.55" + resolved "https://registry.yarnpkg.com/@mdi/svg/-/svg-5.9.55.tgz#7cba058135afd5d8a3da977f51b71ffc6a3a3699" + integrity sha512-gO0ZpKIeCn9vFg46QduK9MM+n1fuCNwSdcdlBTtbafnnuvwLveK2uj+byhdLtg/8VJGXDhp+DJ35QUMbeWeULA== + "@jest/types@^27.0.0-next.8": version "27.0.0-next.8" resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.0.0-next.8.tgz#bbc9f2acad3fea3e71444bfe06af522044a38951" @@ -514,6 +519,21 @@ "@types/yargs" "^16.0.0" chalk "^4.0.0" +"@mdi/svg@^5.9.55": + version "5.9.55" + resolved "https://registry.yarnpkg.com/@mdi/svg/-/svg-5.9.55.tgz#7cba058135afd5d8a3da977f51b71ffc6a3a3699" + integrity sha512-gO0ZpKIeCn9vFg46QduK9MM+n1fuCNwSdcdlBTtbafnnuvwLveK2uj+byhdLtg/8VJGXDhp+DJ35QUMbeWeULA== + +"@popperjs/core@2.6.0": + version "2.6.0" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.6.0.tgz#f022195afdfc942e088ee2101285a1d31c7d727f" + integrity sha512-cPqjjzuFWNK3BSKLm0abspP0sp/IGOli4p5I5fKFAzdS8fvjdOwDCfZqAaIiXd9lPkOWi3SUUfZof3hEb7J/uw== + +"@popperjs/core@^2.9.2": + version "2.9.2" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.2.tgz#adea7b6953cbb34651766b0548468e743c6a2353" + integrity sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q== + "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" @@ -629,6 +649,14 @@ dependencies: "@babel/types" "^7.3.0" +"@types/bootstrap@^5.0.12": + version "5.0.12" + resolved "https://registry.yarnpkg.com/@types/bootstrap/-/bootstrap-5.0.12.tgz#d044b6404bf3c89fc90df2822a86dfcd349db522" + integrity sha512-iowwPfp9Au6aoxS2hOgeRjXE25xdfLrTpmxzQSUs21z5qY3UZpmjSIWF4h8jPYPEXgZioIKLB2OSU8oWzzJAcQ== + dependencies: + "@popperjs/core" "2.6.0" + "@types/jquery" "*" + "@types/d3-array@*": version "2.9.0" resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-2.9.0.tgz#fb6c3d7d7640259e68771cd90cc5db5ac1a1a012"