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",