diff --git a/qt/aqt/data/web/css/BUILD.bazel b/qt/aqt/data/web/css/BUILD.bazel index cb92658a0..e29acf7ed 100644 --- a/qt/aqt/data/web/css/BUILD.bazel +++ b/qt/aqt/data/web/css/BUILD.bazel @@ -26,20 +26,11 @@ 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 83f4718fc..b138bd31b 100644 --- a/qt/aqt/data/web/js/BUILD.bazel +++ b/qt/aqt/data/web/js/BUILD.bazel @@ -37,20 +37,11 @@ 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 97b416c1b..420653512 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -141,13 +141,11 @@ class Editor: _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, @@ -184,13 +182,7 @@ $editorToolbar.addButtonGroup({{ else "" ) - self.web.eval( - f""" -$editorToolbar = document.getElementById("editorToolbar"); -{lefttopbtns_js} -{righttopbtns_js} -""" - ) + self.web.eval(f"{lefttopbtns_js} {righttopbtns_js}") # Top buttons ###################################################################### diff --git a/ts/compile_sass.bzl b/ts/compile_sass.bzl index 4e0090dac..c8d96cfb3 100644 --- a/ts/compile_sass.bzl +++ b/ts/compile_sass.bzl @@ -1,6 +1,6 @@ load("@io_bazel_rules_sass//:defs.bzl", "sass_binary") -def compile_sass(group, srcs, visibility, deps): +def compile_sass(group, srcs, deps = [], visibility = ["//visibility:private"]): css_files = [] for scss_file in srcs: base = scss_file.replace(".scss", "") diff --git a/ts/editor-toolbar/BUILD.bazel b/ts/editor-toolbar/BUILD.bazel index b8bf53313..975f48ec4 100644 --- a/ts/editor-toolbar/BUILD.bazel +++ b/ts/editor-toolbar/BUILD.bazel @@ -4,12 +4,17 @@ 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] +filegroup( + name = "svelte_components", + srcs = svelte_names, + visibility = ["//visibility:public"], +) + compile_svelte( name = "svelte", srcs = svelte_files, @@ -17,43 +22,32 @@ compile_svelte( "//ts/sass:button_mixins_lib", "//ts/sass/bootstrap", ], + visibility = ["//visibility:public"], ) compile_sass( srcs = [ "bootstrap.scss", - "color.scss", "legacy.scss", ], group = "local_css", - visibility = ["//visibility:public"], 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", + name = "editor-toolbar", + module_name = "editor-toolbar", srcs = glob( ["*.ts"], - exclude = ["index.ts"], + exclude = ["*.test.ts"], ), + tsconfig = "//ts:tsconfig.json", + visibility = ["//visibility:public"], deps = [ - "//ts:image_module_support", "//ts/lib", "//ts/lib:backend_proto", "//ts/sveltelib", @@ -64,71 +58,6 @@ ts_library( ], ) -copy_bootstrap_icons( - name = "bootstrap-icons", - icons = [ - # inline formatting - "type-bold.svg", - "type-italic.svg", - "type-underline.svg", - "eraser.svg", - "square-fill.svg", - "paperclip.svg", - "mic.svg", - - # block formatting - "list-ul.svg", - "list-ol.svg", - "text-paragraph.svg", - "justify.svg", - "text-left.svg", - "text-right.svg", - "text-center.svg", - "text-indent-left.svg", - "text-indent-right.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", - "--log-level=warning", - ], - 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 ################ diff --git a/ts/editor-toolbar/CommandIconButton.d.ts b/ts/editor-toolbar/CommandIconButton.d.ts index 2fe43d032..a64bfd4e6 100644 --- a/ts/editor-toolbar/CommandIconButton.d.ts +++ b/ts/editor-toolbar/CommandIconButton.d.ts @@ -5,6 +5,12 @@ export interface CommandIconButtonProps { className?: string; tooltip: string; icon: string; + command: string; - activatable?: boolean; + onClick: (event: MouseEvent) => void; + + onUpdate: (event: Event) => boolean; + + disables?: boolean; + dropdownToggle?: boolean; } diff --git a/ts/editor-toolbar/CommandIconButton.svelte b/ts/editor-toolbar/CommandIconButton.svelte index 42343fb19..b78025274 100644 --- a/ts/editor-toolbar/CommandIconButton.svelte +++ b/ts/editor-toolbar/CommandIconButton.svelte @@ -5,19 +5,24 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html {@html icon} diff --git a/ts/editor-toolbar/IconButton.svelte b/ts/editor-toolbar/IconButton.svelte index ff9e0a0d4..849b33575 100644 --- a/ts/editor-toolbar/IconButton.svelte +++ b/ts/editor-toolbar/IconButton.svelte @@ -8,11 +8,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html export let id: string; export let className = ""; export let tooltip: string; + export let icon: string; + + export let onClick: (event: MouseEvent) => void; + export let disables = true; export let dropdownToggle = false; - - export let icon = ""; - export let onClick: (event: MouseEvent) => void; diff --git a/ts/editor-toolbar/dynamicComponents.ts b/ts/editor-toolbar/dynamicComponents.ts new file mode 100644 index 000000000..72fc9de30 --- /dev/null +++ b/ts/editor-toolbar/dynamicComponents.ts @@ -0,0 +1,57 @@ +// 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 IconButton from "./IconButton.svelte"; +import type { IconButtonProps } from "./IconButton"; +import CommandIconButton from "./CommandIconButton.svelte"; +import type { CommandIconButtonProps } from "./CommandIconButton"; +import ColorPicker from "./ColorPicker.svelte"; +import type { ColorPickerProps } from "./ColorPicker"; +import ButtonGroup from "./ButtonGroup.svelte"; +import type { ButtonGroupProps } from "./ButtonGroup"; + +import ButtonDropdown from "./ButtonDropdown.svelte"; +import type { ButtonDropdownProps } from "./ButtonDropdown"; +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 { dynamicComponent } from "sveltelib/dynamicComponent"; + +export const labelButton = dynamicComponent( + LabelButton +); +export const iconButton = dynamicComponent( + IconButton +); +export const commandIconButton = dynamicComponent< + typeof CommandIconButton, + CommandIconButtonProps +>(CommandIconButton); +export const colorPicker = dynamicComponent( + ColorPicker +); + +export const buttonGroup = dynamicComponent( + ButtonGroup +); +export const buttonDropdown = dynamicComponent< + typeof ButtonDropdown, + ButtonDropdownProps +>(ButtonDropdown); + +export const dropdownMenu = dynamicComponent( + DropdownMenu +); +export const dropdownItem = dynamicComponent( + DropdownItem +); + +export const withDropdownMenu = dynamicComponent< + typeof WithDropdownMenu, + WithDropdownMenuProps +>(WithDropdownMenu); diff --git a/ts/editor-toolbar/editorToolbar.d.ts b/ts/editor-toolbar/editorToolbar.d.ts new file mode 100644 index 000000000..4fd9afb11 --- /dev/null +++ b/ts/editor-toolbar/editorToolbar.d.ts @@ -0,0 +1,5 @@ +import type { EditorToolbar } from "."; + +declare namespace globalThis { + const $editorToolbar: EditorToolbar; +} diff --git a/ts/editor-toolbar/index.ts b/ts/editor-toolbar/index.ts index b411cf8f8..aab58bcca 100644 --- a/ts/editor-toolbar/index.ts +++ b/ts/editor-toolbar/index.ts @@ -11,15 +11,8 @@ 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 { getFormatInlineGroup } from "./formatInline"; -import { getFormatBlockGroup, getFormatBlockMenus } from "./formatBlock"; -import { getColorGroup } from "./color"; -import { getTemplateGroup, getTemplateMenus } from "./template"; import { Identifiable, search, add, insert } from "./identifiable"; interface Hideable { @@ -45,7 +38,7 @@ let buttonsResolve: ( ) => void; let menusResolve: (value: Writable) => void; -class EditorToolbar extends HTMLElement { +export class EditorToolbar extends HTMLElement { component?: SvelteComponentDev; buttonsPromise: Promise< @@ -58,30 +51,22 @@ class EditorToolbar extends HTMLElement { }); connectedCallback(): void { - setupI18n({ modules: [ModuleName.EDITING] }).then(() => { - const buttons = writable([ - getNotetypeGroup(), - getFormatInlineGroup(), - getFormatBlockGroup(), - getColorGroup(), - getTemplateGroup(), - ]); - const menus = writable([...getTemplateMenus(), ...getFormatBlockMenus()]); + globalThis.$editorToolbar = this; - this.component = new EditorToolbarSvelte({ - target: this, - props: { - buttons, - menus, - nightMode: document.documentElement.classList.contains( - "night-mode" - ), - }, - }); + const buttons = writable([]); + const menus = writable([]); - buttonsResolve(buttons); - menusResolve(menus); + this.component = new EditorToolbarSvelte({ + target: this, + props: { + buttons, + menus, + nightMode: document.documentElement.classList.contains("night-mode"), + }, }); + + buttonsResolve(buttons); + menusResolve(menus); } updateButtonGroup( @@ -200,19 +185,8 @@ class EditorToolbar extends HTMLElement { customElements.define("anki-editor-toolbar", EditorToolbar); -/* Exports for editor - * @ts-expect-error */ +/* Exports for editor */ +// @ts-expect-error export { updateActiveButtons, clearActiveButtons } from "./CommandIconButton.svelte"; +// @ts-expect-error 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/BUILD.bazel b/ts/editor/BUILD.bazel index bdcf754fa..92ce959a2 100644 --- a/ts/editor/BUILD.bazel +++ b/ts/editor/BUILD.bazel @@ -2,13 +2,24 @@ load("@npm//@bazel/typescript:index.bzl", "ts_library") load("//ts:prettier.bzl", "prettier_test") load("//ts:eslint.bzl", "eslint_test") load("//ts:esbuild.bzl", "esbuild") -load("//ts:vendor.bzl", "copy_bootstrap_icons") +load("//ts:vendor.bzl", "copy_bootstrap_icons", "copy_mdi_icons") load("//ts:compile_sass.bzl", "compile_sass") compile_sass( srcs = [ "editable.scss", - "editor.scss", + ], + group = "editable_scss", + visibility = ["//visibility:public"], + deps = [ + "//ts/sass:scrollbar_lib", + ], +) + +compile_sass( + srcs = [ + "fields.scss", + "color.scss", ], group = "base_css", visibility = ["//visibility:public"], @@ -25,28 +36,78 @@ ts_library( tsconfig = "//ts:tsconfig.json", deps = [ "//ts:image_module_support", + "//ts/lib", + "//ts/sveltelib", "//ts/html-filter", + "//ts/editor-toolbar", + "@npm//svelte", ], ) copy_bootstrap_icons( name = "bootstrap-icons", - icons = ["pin-angle.svg"], + icons = [ + "pin-angle.svg", + + # inline formatting + "type-bold.svg", + "type-italic.svg", + "type-underline.svg", + "eraser.svg", + "square-fill.svg", + "paperclip.svg", + "mic.svg", + + # block formatting + "list-ul.svg", + "list-ol.svg", + "text-paragraph.svg", + "justify.svg", + "text-left.svg", + "text-right.svg", + "text-center.svg", + "text-indent-left.svg", + "text-indent-right.svg", + ], + visibility = ["//visibility:public"], +) + +copy_mdi_icons( + name = "mdi-icons", + icons = [ + "format-superscript.svg", + "format-subscript.svg", + "function-variant.svg", + "code-brackets.svg", + "xml.svg", + ], + visibility = ["//visibility:public"], ) esbuild( name = "editor", + srcs = [ + "//ts:protobuf-shim.js", + ], args = [ "--loader:.svg=text", + "--inject:$(location //ts:protobuf-shim.js)", "--resolve-extensions=.mjs,.js", "--log-level=warning", ], + output_css = "editor.css", + external = [ + "protobufjs/light", + ], entry_point = "index_wrapper.ts", visibility = ["//visibility:public"], deps = [ "base_css", - ":bootstrap-icons", - ":editor_ts", + "bootstrap-icons", + "mdi-icons", + "editor_ts", + "//ts/editor-toolbar:local_css", + "//ts/editor-toolbar:svelte_components", ], ) diff --git a/ts/editor/addons.ts b/ts/editor/addons.ts new file mode 100644 index 000000000..4cd3af6be --- /dev/null +++ b/ts/editor/addons.ts @@ -0,0 +1,24 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +import { default as RawButton } from "editor-toolbar/RawButton.svelte"; +import { default as LabelButton } from "editor-toolbar/LabelButton.svelte"; +import { default as IconButton } from "editor-toolbar/IconButton.svelte"; +import { default as CommandIconButton } from "editor-toolbar/CommandIconButton.svelte"; +import { default as SelectButton } from "editor-toolbar/SelectButton.svelte"; + +import { default as DropdownMenu } from "editor-toolbar/DropdownMenu.svelte"; +import { default as DropdownItem } from "editor-toolbar/DropdownItem.svelte"; +import { default as ButtonDropdown } from "editor-toolbar/DropdownMenu.svelte"; +import { default as WithDropdownMenu } from "editor-toolbar/WithDropdownMenu.svelte"; + +export const editorToolbar = { + RawButton, + LabelButton, + IconButton, + CommandIconButton, + SelectButton, + DropdownMenu, + DropdownItem, + ButtonDropdown, + WithDropdownMenu, +}; diff --git a/ts/editor-toolbar/cloze.ts b/ts/editor/cloze.ts similarity index 57% rename from ts/editor-toolbar/cloze.ts rename to ts/editor/cloze.ts index 55fd8ea60..8a3a6491a 100644 --- a/ts/editor-toolbar/cloze.ts +++ b/ts/editor/cloze.ts @@ -1,24 +1,31 @@ // 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 type IconButton from "editor-toolbar/IconButton.svelte"; +import type { IconButtonProps } from "editor-toolbar/IconButton"; +import type { DynamicSvelteComponent } from "sveltelib/dynamicComponent"; -import { DynamicSvelteComponent, dynamicComponent } from "sveltelib/dynamicComponent"; import * as tr from "anki/i18n"; +import { iconButton } from "editor-toolbar/dynamicComponents"; import bracketsIcon from "./code-brackets.svg"; +import { forEditorField } from "."; +import { wrap } from "./wrap"; + 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])) - ); + const fieldHTML = field.editingArea.editable.fieldHTML; + const matches: number[] = []; + let match: RegExpMatchArray | null = null; + + while ((match = clozePattern.exec(fieldHTML))) { + matches.push(Number(match[1])); + } + + highest = Math.max(highest, ...matches); }); if (increment) { @@ -30,13 +37,9 @@ function getCurrentHighestCloze(increment: boolean): number { 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({ diff --git a/ts/editor-toolbar/color.scss b/ts/editor/color.scss similarity index 100% rename from ts/editor-toolbar/color.scss rename to ts/editor/color.scss diff --git a/ts/editor-toolbar/color.ts b/ts/editor/color.ts similarity index 66% rename from ts/editor-toolbar/color.ts rename to ts/editor/color.ts index 2ed6f932b..2e3a36d55 100644 --- a/ts/editor-toolbar/color.ts +++ b/ts/editor/color.ts @@ -1,13 +1,10 @@ // 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 type ButtonGroup from "editor-toolbar/ButtonGroup.svelte"; +import type { ButtonGroupProps } from "editor-toolbar/ButtonGroup"; +import type { DynamicSvelteComponent } from "sveltelib/dynamicComponent"; -import { DynamicSvelteComponent, dynamicComponent } from "sveltelib/dynamicComponent"; +import { iconButton, colorPicker, buttonGroup } from "editor-toolbar/dynamicComponents"; import * as tr from "anki/i18n"; import squareFillIcon from "./square-fill.svg"; @@ -27,10 +24,6 @@ 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({ diff --git a/ts/editor/editable.scss b/ts/editor/editable.scss index 9cd7186f3..7489a48a4 100644 --- a/ts/editor/editable.scss +++ b/ts/editor/editable.scss @@ -16,6 +16,16 @@ anki-editable { } } +p { + margin-top: 0; + margin-bottom: 1rem; + + &:empty::after { + content: "\a"; + white-space: pre; + } +} + :host-context(.nightMode) * { @include scrollbar.night-mode; } diff --git a/ts/editor/editingArea.ts b/ts/editor/editingArea.ts index 5002ba817..a87161683 100644 --- a/ts/editor/editingArea.ts +++ b/ts/editor/editingArea.ts @@ -3,6 +3,7 @@ import type { Editable } from "./editable"; +import { updateActiveButtons } from "editor-toolbar"; import { bridgeCommand } from "./lib"; import { onInput, onKey, onKeyUp } from "./inputHandlers"; import { onFocus, onBlur } from "./focusHandlers"; @@ -59,8 +60,7 @@ export class EditingArea extends HTMLDivElement { this.addEventListener("paste", onPaste); this.addEventListener("copy", onCutOrCopy); this.addEventListener("oncut", onCutOrCopy); - // @ts-expect-error - this.addEventListener("mouseup", editorToolbar.updateActiveButtons); + this.addEventListener("mouseup", updateActiveButtons); const baseStyleSheet = this.baseStyle.sheet as CSSStyleSheet; baseStyleSheet.insertRule("anki-editable {}", 0); @@ -75,8 +75,7 @@ export class EditingArea extends HTMLDivElement { this.removeEventListener("paste", onPaste); this.removeEventListener("copy", onCutOrCopy); this.removeEventListener("oncut", onCutOrCopy); - // @ts-expect-error - this.removeEventListener("mouseup", editorToolbar.updateActiveButtons); + this.removeEventListener("mouseup", updateActiveButtons); } initialize(color: string, content: string): void { diff --git a/ts/editor/editor.scss b/ts/editor/fields.scss similarity index 100% rename from ts/editor/editor.scss rename to ts/editor/fields.scss diff --git a/ts/editor/focusHandlers.ts b/ts/editor/focusHandlers.ts index fb9f44b8c..6bcadd8bd 100644 --- a/ts/editor/focusHandlers.ts +++ b/ts/editor/focusHandlers.ts @@ -1,6 +1,7 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +import { enableButtons, disableButtons } from "editor-toolbar"; import type { EditingArea } from "./editingArea"; import { saveField } from "./changeTimer"; @@ -10,8 +11,7 @@ export function onFocus(evt: FocusEvent): void { const currentField = evt.currentTarget as EditingArea; currentField.focusEditable(); bridgeCommand(`focus:${currentField.ord}`); - // @ts-expect-error - editorToolbar.enableButtons(); + enableButtons(); } export function onBlur(evt: FocusEvent): void { @@ -19,6 +19,5 @@ export function onBlur(evt: FocusEvent): void { const currentFieldUnchanged = previousFocus === document.activeElement; saveField(previousFocus, currentFieldUnchanged ? "key" : "blur"); - // @ts-expect-error - editorToolbar.disableButtons(); + disableButtons(); } diff --git a/ts/editor-toolbar/formatBlock.ts b/ts/editor/formatBlock.ts similarity index 66% rename from ts/editor-toolbar/formatBlock.ts rename to ts/editor/formatBlock.ts index 38339d7b0..9810ee1d4 100644 --- a/ts/editor-toolbar/formatBlock.ts +++ b/ts/editor/formatBlock.ts @@ -1,19 +1,23 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -import ButtonGroup from "./ButtonGroup.svelte"; -import type { ButtonGroupProps } from "./ButtonGroup"; -import ButtonDropdown from "./ButtonDropdown.svelte"; -import type { ButtonDropdownProps } from "./ButtonDropdown"; -import WithDropdownMenu from "./WithDropdownMenu.svelte"; -import type { WithDropdownMenuProps } from "./WithDropdownMenu"; +import type ButtonGroup from "editor-toolbar/ButtonGroup.svelte"; +import type { ButtonGroupProps } from "editor-toolbar/ButtonGroup"; +import type ButtonDropdown from "editor-toolbar/ButtonDropdown.svelte"; +import type { ButtonDropdownProps } from "editor-toolbar/ButtonDropdown"; +import type { DynamicSvelteComponent } from "sveltelib/dynamicComponent"; -import CommandIconButton from "./CommandIconButton.svelte"; -import type { CommandIconButtonProps } from "./CommandIconButton"; -import IconButton from "./IconButton.svelte"; -import type { IconButtonProps } from "./IconButton"; +import type { EditingArea } from "./editingArea"; -import { DynamicSvelteComponent, dynamicComponent } from "sveltelib/dynamicComponent"; import * as tr from "anki/i18n"; +import { + commandIconButton, + iconButton, + buttonGroup, + buttonDropdown, + withDropdownMenu, +} from "editor-toolbar/dynamicComponents"; + +import { getListItem } from "./helpers"; import ulIcon from "./list-ul.svg"; import olIcon from "./list-ol.svg"; @@ -27,20 +31,19 @@ import justifyCenterIcon from "./text-center.svg"; import indentIcon from "./text-indent-left.svg"; import outdentIcon from "./text-indent-right.svg"; -const commandIconButton = dynamicComponent< - typeof CommandIconButton, - CommandIconButtonProps ->(CommandIconButton); +const outdentListItem = () => { + const currentField = document.activeElement as EditingArea; + if (getListItem(currentField.shadowRoot!)) { + document.execCommand("outdent"); + } +}; -const buttonGroup = dynamicComponent(ButtonGroup); -const buttonDropdown = dynamicComponent( - ButtonDropdown -); - -const withDropdownMenu = dynamicComponent< - typeof WithDropdownMenu, - WithDropdownMenuProps ->(WithDropdownMenu); +const indentListItem = () => { + const currentField = document.activeElement as EditingArea; + if (getListItem(currentField.shadowRoot!)) { + document.execCommand("indent"); + } +}; export function getFormatBlockMenus(): (DynamicSvelteComponent & ButtonDropdownProps)[] { @@ -78,18 +81,16 @@ export function getFormatBlockMenus(): (DynamicSvelteComponent(IconButton); - export function getFormatBlockGroup(): DynamicSvelteComponent & ButtonGroupProps { const ulButton = commandIconButton({ @@ -131,7 +130,7 @@ export function getFormatBlockGroup(): DynamicSvelteComponent(CommandIconButton); -const buttonGroup = dynamicComponent(ButtonGroup); - export function getFormatInlineGroup(): DynamicSvelteComponent & ButtonGroupProps { const boldButton = commandIconButton({ icon: boldIcon, - command: "bold", tooltip: tr.editingBoldTextCtrlandb(), + command: "bold", }); const italicButton = commandIconButton({ icon: italicIcon, - command: "italic", tooltip: tr.editingItalicTextCtrlandi(), + command: "italic", }); const underlineButton = commandIconButton({ icon: underlineIcon, - command: "underline", tooltip: tr.editingUnderlineTextCtrlandu(), + command: "underline", }); const superscriptButton = commandIconButton({ icon: superscriptIcon, - command: "superscript", tooltip: tr.editingSuperscriptCtrlandand(), + command: "superscript", }); const subscriptButton = commandIconButton({ icon: subscriptIcon, - command: "subscript", tooltip: tr.editingSubscriptCtrland(), + command: "subscript", }); - const removeFormatButton = commandIconButton({ + const removeFormatButton = iconButton({ icon: eraserIcon, - command: "removeFormat", - activatable: false, tooltip: tr.editingRemoveFormattingCtrlandr(), + onClick: () => { + document.execCommand("removeFormat"); + }, }); return buttonGroup({ - id: "formatInline", + id: "inlineFormatting", buttons: [ boldButton, italicButton, diff --git a/ts/editor/helpers.ts b/ts/editor/helpers.ts index bfed2ed7a..cffcb1480 100644 --- a/ts/editor/helpers.ts +++ b/ts/editor/helpers.ts @@ -77,3 +77,36 @@ export function caretToEnd(currentField: EditingArea): void { selection.removeAllRanges(); selection.addRange(range); } + +const getAnchorParent = ( + predicate: (element: Element) => element is T +) => (currentField: DocumentOrShadowRoot): T | null => { + const anchor = currentField.getSelection()?.anchorNode; + + if (!anchor) { + return null; + } + + let anchorParent: T | null = null; + let element = nodeIsElement(anchor) ? anchor : anchor.parentElement; + + while (element) { + anchorParent = anchorParent || (predicate(element) ? element : null); + element = element.parentElement; + } + + return anchorParent; +}; + +const isListItem = (element: Element): element is HTMLLIElement => + window.getComputedStyle(element).display === "list-item"; +const isParagraph = (element: Element): element is HTMLParamElement => + element.tagName === "P"; +const isBlockElement = ( + element: Element +): element is HTMLLIElement & HTMLParamElement => + isListItem(element) || isParagraph(element); + +export const getListItem = getAnchorParent(isListItem); +export const getParagraph = getAnchorParent(isParagraph); +export const getBlockElement = getAnchorParent(isBlockElement); diff --git a/ts/editor/index.ts b/ts/editor/index.ts index c7e2121b8..a08fcbc3c 100644 --- a/ts/editor/index.ts +++ b/ts/editor/index.ts @@ -2,6 +2,10 @@ // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import { filterHTML } from "html-filter"; +import { updateActiveButtons, disableButtons } from "editor-toolbar"; +import { setupI18n, ModuleName } from "anki/i18n"; + +import "./fields.css"; import { caretToEnd } from "./helpers"; import { saveField } from "./changeTimer"; @@ -10,11 +14,14 @@ import { EditorField } from "./editorField"; import { LabelContainer } from "./labelContainer"; import { EditingArea } from "./editingArea"; import { Editable } from "./editable"; +import { initToolbar } from "./toolbar"; export { setNoteId, getNoteId } from "./noteId"; export { saveNow } from "./changeTimer"; export { wrap, wrapIntoText } from "./wrap"; +export * from "./addons"; + declare global { interface Selection { modify(s: string, t: string, u: string): void; @@ -24,6 +31,7 @@ declare global { } } +import "editor-toolbar"; customElements.define("anki-editable", Editable); customElements.define("anki-editing-area", EditingArea, { extends: "div" }); customElements.define("anki-label-container", LabelContainer, { extends: "div" }); @@ -41,8 +49,7 @@ export function focusField(n: number): void { if (field) { field.editingArea.focusEditable(); caretToEnd(field.editingArea); - // @ts-expect-error - editorToolbar.updateActiveButtons(); + updateActiveButtons(); } } @@ -122,8 +129,7 @@ export function setFields(fields: [string, string][]): void { if (!getCurrentField()) { // when initial focus of the window is not on editor (e.g. browser) - // @ts-expect-error - editorToolbar.disableButtons(); + disableButtons(); } } @@ -158,7 +164,10 @@ export function setFormat(cmd: string, arg?: any, nosave: boolean = false): void document.execCommand(cmd, false, arg); if (!nosave) { saveField(getCurrentField() as EditingArea, "key"); - // @ts-expect-error - editorToolbar.updateActiveButtons(); + updateActiveButtons(); } } + +const i18n = setupI18n({ modules: [ModuleName.EDITING] }); + +initToolbar(i18n); diff --git a/ts/editor/inputHandlers.ts b/ts/editor/inputHandlers.ts index f4a929763..6d938f86c 100644 --- a/ts/editor/inputHandlers.ts +++ b/ts/editor/inputHandlers.ts @@ -1,28 +1,15 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +import { updateActiveButtons } from "editor-toolbar"; import { EditingArea } from "./editingArea"; -import { caretToEnd, nodeIsElement } from "./helpers"; +import { caretToEnd, nodeIsElement, getBlockElement } from "./helpers"; import { triggerChangeTimer } from "./changeTimer"; -function inListItem(currentField: EditingArea): boolean { - const anchor = currentField.getSelection()!.anchorNode!; - - let inList = false; - let n = nodeIsElement(anchor) ? anchor : anchor.parentElement; - while (n) { - inList = inList || window.getComputedStyle(n).display == "list-item"; - n = n.parentElement; - } - - return inList; -} - export function onInput(event: Event): void { // make sure IME changes get saved triggerChangeTimer(event.currentTarget as EditingArea); - // @ts-ignore - editorToolbar.updateActiveButtons(); + updateActiveButtons(); } export function onKey(evt: KeyboardEvent): void { @@ -35,7 +22,10 @@ export function onKey(evt: KeyboardEvent): void { } // prefer
instead of
- if (evt.code === "Enter" && !inListItem(currentField)) { + if ( + evt.code === "Enter" && + !getBlockElement(currentField.shadowRoot!) !== evt.shiftKey + ) { evt.preventDefault(); document.execCommand("insertLineBreak"); } @@ -69,8 +59,7 @@ globalThis.addEventListener("keydown", (evt: KeyboardEvent) => { const newFocusTarget = evt.target; if (newFocusTarget instanceof EditingArea) { caretToEnd(newFocusTarget); - // @ts-ignore - editorToolbar.updateActiveButtons(); + updateActiveButtons(); } }, { once: true } diff --git a/ts/editor-toolbar/notetype.ts b/ts/editor/notetype.ts similarity index 64% rename from ts/editor-toolbar/notetype.ts rename to ts/editor/notetype.ts index 80a14095d..d11aefa79 100644 --- a/ts/editor-toolbar/notetype.ts +++ b/ts/editor/notetype.ts @@ -1,16 +1,12 @@ // 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 type ButtonGroup from "editor-toolbar/ButtonGroup.svelte"; +import type { ButtonGroupProps } from "editor-toolbar/ButtonGroup"; +import type { DynamicSvelteComponent } from "sveltelib/dynamicComponent"; -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); +import { labelButton, buttonGroup } from "editor-toolbar/dynamicComponents"; export function getNotetypeGroup(): DynamicSvelteComponent & ButtonGroupProps { diff --git a/ts/editor-toolbar/template.ts b/ts/editor/template.ts similarity index 69% rename from ts/editor-toolbar/template.ts rename to ts/editor/template.ts index 90ca847a2..0f958cfe7 100644 --- a/ts/editor-toolbar/template.ts +++ b/ts/editor/template.ts @@ -1,20 +1,23 @@ // 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 type DropdownMenu from "editor-toolbar/DropdownMenu.svelte"; +import type { DropdownMenuProps } from "editor-toolbar/DropdownMenu"; +import type ButtonGroup from "editor-toolbar/ButtonGroup.svelte"; +import type { ButtonGroupProps } from "editor-toolbar/ButtonGroup"; +import type { DynamicSvelteComponent } from "sveltelib/dynamicComponent"; import { bridgeCommand } from "anki/bridgecommand"; -import { DynamicSvelteComponent, dynamicComponent } from "sveltelib/dynamicComponent"; +import { + iconButton, + withDropdownMenu, + dropdownMenu, + dropdownItem, + buttonGroup, +} from "editor-toolbar/dynamicComponents"; import * as tr from "anki/i18n"; +import { wrap } from "./wrap"; + import paperclipIcon from "./paperclip.svg"; import micIcon from "./mic.svg"; import functionIcon from "./function-variant.svg"; @@ -36,19 +39,6 @@ function onHtmlEdit(): void { 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({ @@ -95,19 +85,16 @@ 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", @@ -116,19 +103,16 @@ export function getTemplateMenus(): (DynamicSvelteComponent 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", diff --git a/ts/editor/toolbar.ts b/ts/editor/toolbar.ts new file mode 100644 index 000000000..d03e20490 --- /dev/null +++ b/ts/editor/toolbar.ts @@ -0,0 +1,45 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +import type { ToolbarItem } from "editor-toolbar/types"; +import type ButtonGroup from "editor-toolbar/ButtonGroup.svelte"; +import type { ButtonGroupProps } from "editor-toolbar/ButtonGroup"; +import type { Writable } from "svelte/store"; + +import { getNotetypeGroup } from "./notetype"; +import { getFormatInlineGroup } from "./formatInline"; +import { getFormatBlockGroup, getFormatBlockMenus } from "./formatBlock"; +import { getColorGroup } from "./color"; +import { getTemplateGroup, getTemplateMenus } from "./template"; + +export function initToolbar(i18n: Promise): void { + document.addEventListener("DOMContentLoaded", () => { + i18n.then(() => { + globalThis.$editorToolbar.buttonsPromise.then( + ( + buttons: Writable< + (ToolbarItem & ButtonGroupProps)[] + > + ): Writable<(ToolbarItem & ButtonGroupProps)[]> => { + buttons.update(() => [ + getNotetypeGroup(), + getFormatInlineGroup(), + getFormatBlockGroup(), + getColorGroup(), + getTemplateGroup(), + ]); + return buttons; + } + ); + + globalThis.$editorToolbar.menusPromise.then( + (menus: Writable): Writable => { + menus.update(() => [ + ...getFormatBlockMenus(), + ...getTemplateMenus(), + ]); + return menus; + } + ); + }); + }); +} diff --git a/ts/html-filter/BUILD.bazel b/ts/html-filter/BUILD.bazel index d4fed4512..cf5e71d7e 100644 --- a/ts/html-filter/BUILD.bazel +++ b/ts/html-filter/BUILD.bazel @@ -5,11 +5,11 @@ load("//ts:eslint.bzl", "eslint_test") ts_library( name = "html-filter", + module_name = "html-filter", srcs = glob( ["*.ts"], exclude = ["*.test.ts"], ), - module_name = "html-filter", tsconfig = "//ts:tsconfig.json", visibility = ["//visibility:public"], deps = [], diff --git a/ts/svelte/svelte.bzl b/ts/svelte/svelte.bzl index 9b9b1c9e6..149447286 100644 --- a/ts/svelte/svelte.bzl +++ b/ts/svelte/svelte.bzl @@ -63,17 +63,19 @@ svelte = rule( }, ) -def compile_svelte(name, srcs, deps = []): +def compile_svelte(name, srcs, deps = [], visibility = ["//visibility:private"]): for src in srcs: svelte( name = src.replace(".svelte", ""), entry_point = src, deps = deps, + visibility = visibility, ) native.filegroup( name = name, srcs = srcs, + visibility = visibility, ) def svelte_check(name = "svelte_check", srcs = []): diff --git a/ts/tsconfig.json b/ts/tsconfig.json index 1895c002b..bc98946fd 100644 --- a/ts/tsconfig.json +++ b/ts/tsconfig.json @@ -8,7 +8,8 @@ "paths": { "anki/*": ["../bazel-bin/ts/lib/*"], "sveltelib/*": ["../bazel-bin/ts/sveltelib/*"], - "html-filter/*": ["../bazel-bin/ts/html-filter/*"] + "html-filter/*": ["../bazel-bin/ts/html-filter/*"], + "editor-toolbar/*": ["../bazel-bin/ts/editor-toolbar/*"] }, "importsNotUsedAsValues": "error", "outDir": "dist",