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