Merge pull request #1140 from hgiesel/paragrapho

Restructure editor-toolbar and editor + Change Enter behavior within paragraphs + Restrict indent / outdent to <li>
This commit is contained in:
Damien Elmes 2021-04-22 08:39:28 +10:00 committed by GitHub
commit e3a7d1a9e3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 442 additions and 342 deletions

View file

@ -26,20 +26,11 @@ copy_files_into_group(
package = "//ts/editor", package = "//ts/editor",
) )
copy_files_into_group(
name = "editor-toolbar",
srcs = [
"editor-toolbar.css",
],
package = "//ts/editor-toolbar",
)
filegroup( filegroup(
name = "css", name = "css",
srcs = [ srcs = [
"css_local", "css_local",
"editor", "editor",
"editor-toolbar",
], ],
visibility = ["//qt:__subpackages__"], visibility = ["//qt:__subpackages__"],
) )

View file

@ -37,20 +37,11 @@ copy_files_into_group(
package = "//ts/editor", package = "//ts/editor",
) )
copy_files_into_group(
name = "editor-toolbar",
srcs = [
"editor-toolbar.js",
],
package = "//ts/editor-toolbar",
)
filegroup( filegroup(
name = "js", name = "js",
srcs = [ srcs = [
"aqt_es5", "aqt_es5",
"editor", "editor",
"editor-toolbar",
"mathjax.js", "mathjax.js",
"//qt/aqt/data/web/js/vendor", "//qt/aqt/data/web/js/vendor",
], ],

View file

@ -141,13 +141,11 @@ class Editor:
_html % (bgcol, tr.editing_show_duplicates()), _html % (bgcol, tr.editing_show_duplicates()),
css=[ css=[
"css/editor.css", "css/editor.css",
"css/editor-toolbar.css",
], ],
js=[ js=[
"js/vendor/jquery.min.js", "js/vendor/jquery.min.js",
"js/vendor/protobuf.min.js", "js/vendor/protobuf.min.js",
"js/editor.js", "js/editor.js",
"js/editor-toolbar.js",
], ],
context=self, context=self,
default_css=False, default_css=False,
@ -184,13 +182,7 @@ $editorToolbar.addButtonGroup({{
else "" else ""
) )
self.web.eval( self.web.eval(f"{lefttopbtns_js} {righttopbtns_js}")
f"""
$editorToolbar = document.getElementById("editorToolbar");
{lefttopbtns_js}
{righttopbtns_js}
"""
)
# Top buttons # Top buttons
###################################################################### ######################################################################

View file

@ -1,6 +1,6 @@
load("@io_bazel_rules_sass//:defs.bzl", "sass_binary") 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 = [] css_files = []
for scss_file in srcs: for scss_file in srcs:
base = scss_file.replace(".scss", "") base = scss_file.replace(".scss", "")

View file

@ -4,12 +4,17 @@ load("//ts:prettier.bzl", "prettier_test")
load("//ts:eslint.bzl", "eslint_test") load("//ts:eslint.bzl", "eslint_test")
load("//ts:esbuild.bzl", "esbuild") load("//ts:esbuild.bzl", "esbuild")
load("//ts:compile_sass.bzl", "compile_sass") load("//ts:compile_sass.bzl", "compile_sass")
load("//ts:vendor.bzl", "copy_bootstrap_icons", "copy_mdi_icons")
svelte_files = glob(["*.svelte"]) svelte_files = glob(["*.svelte"])
svelte_names = [f.replace(".svelte", "") for f in svelte_files] svelte_names = [f.replace(".svelte", "") for f in svelte_files]
filegroup(
name = "svelte_components",
srcs = svelte_names,
visibility = ["//visibility:public"],
)
compile_svelte( compile_svelte(
name = "svelte", name = "svelte",
srcs = svelte_files, srcs = svelte_files,
@ -17,43 +22,32 @@ compile_svelte(
"//ts/sass:button_mixins_lib", "//ts/sass:button_mixins_lib",
"//ts/sass/bootstrap", "//ts/sass/bootstrap",
], ],
visibility = ["//visibility:public"],
) )
compile_sass( compile_sass(
srcs = [ srcs = [
"bootstrap.scss", "bootstrap.scss",
"color.scss",
"legacy.scss", "legacy.scss",
], ],
group = "local_css", group = "local_css",
visibility = ["//visibility:public"],
deps = [ deps = [
"//ts/sass:button_mixins_lib", "//ts/sass:button_mixins_lib",
"//ts/sass/bootstrap", "//ts/sass/bootstrap",
], ],
visibility = ["//visibility:public"],
) )
ts_library( ts_library(
name = "index", name = "editor-toolbar",
srcs = ["index.ts"], module_name = "editor-toolbar",
deps = [
"EditorToolbar",
"lib",
"//ts/lib",
"//ts/sveltelib",
"@npm//svelte",
"@npm//svelte2tsx",
],
)
ts_library(
name = "lib",
srcs = glob( srcs = glob(
["*.ts"], ["*.ts"],
exclude = ["index.ts"], exclude = ["*.test.ts"],
), ),
tsconfig = "//ts:tsconfig.json",
visibility = ["//visibility:public"],
deps = [ deps = [
"//ts:image_module_support",
"//ts/lib", "//ts/lib",
"//ts/lib:backend_proto", "//ts/lib:backend_proto",
"//ts/sveltelib", "//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 # Tests
################ ################

View file

@ -5,6 +5,12 @@ export interface CommandIconButtonProps {
className?: string; className?: string;
tooltip: string; tooltip: string;
icon: string; icon: string;
command: string; command: string;
activatable?: boolean; onClick: (event: MouseEvent) => void;
onUpdate: (event: Event) => boolean;
disables?: boolean;
dropdownToggle?: boolean;
} }

View file

@ -5,19 +5,24 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<script lang="typescript" context="module"> <script lang="typescript" context="module">
import { writable } from "svelte/store"; import { writable } from "svelte/store";
const commandMap = writable(new Map<string, boolean>()); type UpdateMap = Map<string, (event: Event) => boolean>;
type ActiveMap = Map<string, boolean>;
function updateButton(key: string): void { const updateMap = new Map() as UpdateMap;
commandMap.update( const activeMap = new Map() as ActiveMap;
(map: Map<string, boolean>): Map<string, boolean> => const activeStore = writable(activeMap);
new Map([...map, [key, document.queryCommandState(key)]])
function updateButton(key: string, event: MouseEvent): void {
activeStore.update(
(map: ActiveMap): ActiveMap =>
new Map([...map, [key, updateMap.get(key)(event)]])
); );
} }
function updateButtons(callback: (key: string) => boolean): void { function updateButtons(callback: (key: string) => boolean): void {
commandMap.update( activeStore.update(
(map: Map<string, boolean>): Map<string, boolean> => { (map: ActiveMap): ActiveMap => {
const newMap = new Map<string, boolean>(); const newMap = new Map() as ActiveMap;
for (const key of map.keys()) { for (const key of map.keys()) {
newMap.set(key, callback(key)); newMap.set(key, callback(key));
@ -28,8 +33,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
); );
} }
export function updateActiveButtons() { export function updateActiveButtons(event: Event) {
updateButtons((key: string): boolean => document.queryCommandState(key)); updateButtons((key: string): boolean => updateMap.get(key)(event));
} }
export function clearActiveButtons() { export function clearActiveButtons() {
@ -43,28 +48,32 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export let id: string; export let id: string;
export let className = ""; export let className = "";
export let tooltip: string; export let tooltip: string;
export let icon: string; export let icon: string;
export let command: string; export let command: string;
export let activatable = true; export let onClick = (_event: MouseEvent) => {
export let disables = true; document.execCommand(command);
export let dropdownToggle = false; };
function onClickWrapped(event: MouseEvent): void {
onClick(event);
updateButton(command, event);
}
export let onUpdate = (_event: Event) => document.queryCommandState(command);
updateMap.set(command, onUpdate);
let active = false; let active = false;
if (activatable) { activeStore.subscribe((map: ActiveMap): (() => void) => {
updateButton(command);
commandMap.subscribe((map: Map<string, boolean>): (() => void) => {
active = Boolean(map.get(command)); active = Boolean(map.get(command));
return () => map.delete(command); return () => map.delete(command);
}); });
} activeMap.set(command, active);
function onClick(): void { export let disables = true;
document.execCommand(command); export let dropdownToggle = false;
updateButton(command);
}
</script> </script>
<SquareButton <SquareButton
@ -74,7 +83,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
{active} {active}
{disables} {disables}
{dropdownToggle} {dropdownToggle}
{onClick} onClick={onClickWrapped}
on:mount> on:mount>
{@html icon} {@html icon}
</SquareButton> </SquareButton>

View file

@ -8,11 +8,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export let id: string; export let id: string;
export let className = ""; export let className = "";
export let tooltip: string; export let tooltip: string;
export let icon: string;
export let onClick: (event: MouseEvent) => void;
export let disables = true; export let disables = true;
export let dropdownToggle = false; export let dropdownToggle = false;
export let icon = "";
export let onClick: (event: MouseEvent) => void;
</script> </script>
<SquareButton {id} {className} {tooltip} {onClick} {disables} {dropdownToggle} on:mount> <SquareButton {id} {className} {tooltip} {onClick} {disables} {dropdownToggle} on:mount>

View file

@ -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<typeof LabelButton, LabelButtonProps>(
LabelButton
);
export const iconButton = dynamicComponent<typeof IconButton, IconButtonProps>(
IconButton
);
export const commandIconButton = dynamicComponent<
typeof CommandIconButton,
CommandIconButtonProps
>(CommandIconButton);
export const colorPicker = dynamicComponent<typeof ColorPicker, ColorPickerProps>(
ColorPicker
);
export const buttonGroup = dynamicComponent<typeof ButtonGroup, ButtonGroupProps>(
ButtonGroup
);
export const buttonDropdown = dynamicComponent<
typeof ButtonDropdown,
ButtonDropdownProps
>(ButtonDropdown);
export const dropdownMenu = dynamicComponent<typeof DropdownMenu, DropdownMenuProps>(
DropdownMenu
);
export const dropdownItem = dynamicComponent<typeof DropdownItem, DropdownItemProps>(
DropdownItem
);
export const withDropdownMenu = dynamicComponent<
typeof WithDropdownMenu,
WithDropdownMenuProps
>(WithDropdownMenu);

5
ts/editor-toolbar/editorToolbar.d.ts vendored Normal file
View file

@ -0,0 +1,5 @@
import type { EditorToolbar } from ".";
declare namespace globalThis {
const $editorToolbar: EditorToolbar;
}

View file

@ -11,15 +11,8 @@ import { Writable, writable } from "svelte/store";
import EditorToolbarSvelte from "./EditorToolbar.svelte"; import EditorToolbarSvelte from "./EditorToolbar.svelte";
import { setupI18n, ModuleName } from "anki/i18n";
import "./bootstrap.css"; 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"; import { Identifiable, search, add, insert } from "./identifiable";
interface Hideable { interface Hideable {
@ -45,7 +38,7 @@ let buttonsResolve: (
) => void; ) => void;
let menusResolve: (value: Writable<ToolbarItem[]>) => void; let menusResolve: (value: Writable<ToolbarItem[]>) => void;
class EditorToolbar extends HTMLElement { export class EditorToolbar extends HTMLElement {
component?: SvelteComponentDev; component?: SvelteComponentDev;
buttonsPromise: Promise< buttonsPromise: Promise<
@ -58,30 +51,22 @@ class EditorToolbar extends HTMLElement {
}); });
connectedCallback(): void { connectedCallback(): void {
setupI18n({ modules: [ModuleName.EDITING] }).then(() => { globalThis.$editorToolbar = this;
const buttons = writable([
getNotetypeGroup(), const buttons = writable([]);
getFormatInlineGroup(), const menus = writable([]);
getFormatBlockGroup(),
getColorGroup(),
getTemplateGroup(),
]);
const menus = writable([...getTemplateMenus(), ...getFormatBlockMenus()]);
this.component = new EditorToolbarSvelte({ this.component = new EditorToolbarSvelte({
target: this, target: this,
props: { props: {
buttons, buttons,
menus, menus,
nightMode: document.documentElement.classList.contains( nightMode: document.documentElement.classList.contains("night-mode"),
"night-mode"
),
}, },
}); });
buttonsResolve(buttons); buttonsResolve(buttons);
menusResolve(menus); menusResolve(menus);
});
} }
updateButtonGroup<T>( updateButtonGroup<T>(
@ -200,19 +185,8 @@ class EditorToolbar extends HTMLElement {
customElements.define("anki-editor-toolbar", EditorToolbar); customElements.define("anki-editor-toolbar", EditorToolbar);
/* Exports for editor /* Exports for editor */
* @ts-expect-error */ // @ts-expect-error
export { updateActiveButtons, clearActiveButtons } from "./CommandIconButton.svelte"; export { updateActiveButtons, clearActiveButtons } from "./CommandIconButton.svelte";
// @ts-expect-error
export { enableButtons, disableButtons } from "./EditorToolbar.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";

View file

@ -2,13 +2,24 @@ load("@npm//@bazel/typescript:index.bzl", "ts_library")
load("//ts:prettier.bzl", "prettier_test") load("//ts:prettier.bzl", "prettier_test")
load("//ts:eslint.bzl", "eslint_test") load("//ts:eslint.bzl", "eslint_test")
load("//ts:esbuild.bzl", "esbuild") 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") load("//ts:compile_sass.bzl", "compile_sass")
compile_sass( compile_sass(
srcs = [ srcs = [
"editable.scss", "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", group = "base_css",
visibility = ["//visibility:public"], visibility = ["//visibility:public"],
@ -25,28 +36,78 @@ ts_library(
tsconfig = "//ts:tsconfig.json", tsconfig = "//ts:tsconfig.json",
deps = [ deps = [
"//ts:image_module_support", "//ts:image_module_support",
"//ts/lib",
"//ts/sveltelib",
"//ts/html-filter", "//ts/html-filter",
"//ts/editor-toolbar",
"@npm//svelte",
], ],
) )
copy_bootstrap_icons( copy_bootstrap_icons(
name = "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( esbuild(
name = "editor", name = "editor",
srcs = [
"//ts:protobuf-shim.js",
],
args = [ args = [
"--loader:.svg=text", "--loader:.svg=text",
"--inject:$(location //ts:protobuf-shim.js)",
"--resolve-extensions=.mjs,.js", "--resolve-extensions=.mjs,.js",
"--log-level=warning", "--log-level=warning",
], ],
output_css = "editor.css",
external = [
"protobufjs/light",
],
entry_point = "index_wrapper.ts", entry_point = "index_wrapper.ts",
visibility = ["//visibility:public"], visibility = ["//visibility:public"],
deps = [ deps = [
"base_css", "base_css",
":bootstrap-icons", "bootstrap-icons",
":editor_ts", "mdi-icons",
"editor_ts",
"//ts/editor-toolbar:local_css",
"//ts/editor-toolbar:svelte_components",
], ],
) )

24
ts/editor/addons.ts Normal file
View file

@ -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,
};

View file

@ -1,24 +1,31 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import IconButton from "./IconButton.svelte"; import type IconButton from "editor-toolbar/IconButton.svelte";
import type { IconButtonProps } from "./IconButton"; 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 * as tr from "anki/i18n";
import { iconButton } from "editor-toolbar/dynamicComponents";
import bracketsIcon from "./code-brackets.svg"; import bracketsIcon from "./code-brackets.svg";
import { forEditorField } from ".";
import { wrap } from "./wrap";
const clozePattern = /\{\{c(\d+)::/gu; const clozePattern = /\{\{c(\d+)::/gu;
function getCurrentHighestCloze(increment: boolean): number { function getCurrentHighestCloze(increment: boolean): number {
let highest = 0; let highest = 0;
// @ts-expect-error
forEditorField([], (field) => { forEditorField([], (field) => {
const matches = field.editingArea.editable.fieldHTML.matchAll(clozePattern); const fieldHTML = field.editingArea.editable.fieldHTML;
highest = Math.max( const matches: number[] = [];
highest, let match: RegExpMatchArray | null = null;
...[...matches].map((match: RegExpMatchArray): number => Number(match[1]))
); while ((match = clozePattern.exec(fieldHTML))) {
matches.push(Number(match[1]));
}
highest = Math.max(highest, ...matches);
}); });
if (increment) { if (increment) {
@ -30,13 +37,9 @@ function getCurrentHighestCloze(increment: boolean): number {
function onCloze(event: MouseEvent): void { function onCloze(event: MouseEvent): void {
const highestCloze = getCurrentHighestCloze(!event.altKey); const highestCloze = getCurrentHighestCloze(!event.altKey);
// @ts-expect-error
wrap(`{{c${highestCloze}::`, "}}"); wrap(`{{c${highestCloze}::`, "}}");
} }
const iconButton = dynamicComponent<typeof IconButton, IconButtonProps>(IconButton);
export function getClozeButton(): DynamicSvelteComponent<typeof IconButton> & export function getClozeButton(): DynamicSvelteComponent<typeof IconButton> &
IconButtonProps { IconButtonProps {
return iconButton({ return iconButton({

View file

@ -1,13 +1,10 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import IconButton from "./IconButton.svelte"; import type ButtonGroup from "editor-toolbar/ButtonGroup.svelte";
import type { IconButtonProps } from "./IconButton"; import type { ButtonGroupProps } from "editor-toolbar/ButtonGroup";
import ColorPicker from "./ColorPicker.svelte"; import type { DynamicSvelteComponent } from "sveltelib/dynamicComponent";
import type { ColorPickerProps } from "./ColorPicker";
import ButtonGroup from "./ButtonGroup.svelte";
import type { ButtonGroupProps } from "./ButtonGroup";
import { DynamicSvelteComponent, dynamicComponent } from "sveltelib/dynamicComponent"; import { iconButton, colorPicker, buttonGroup } from "editor-toolbar/dynamicComponents";
import * as tr from "anki/i18n"; import * as tr from "anki/i18n";
import squareFillIcon from "./square-fill.svg"; import squareFillIcon from "./square-fill.svg";
@ -27,10 +24,6 @@ function wrapWithForecolor(color: string): void {
document.execCommand("forecolor", false, color); document.execCommand("forecolor", false, color);
} }
const iconButton = dynamicComponent<typeof IconButton, IconButtonProps>(IconButton);
const colorPicker = dynamicComponent<typeof ColorPicker, ColorPickerProps>(ColorPicker);
const buttonGroup = dynamicComponent<typeof ButtonGroup, ButtonGroupProps>(ButtonGroup);
export function getColorGroup(): DynamicSvelteComponent<typeof ButtonGroup> & export function getColorGroup(): DynamicSvelteComponent<typeof ButtonGroup> &
ButtonGroupProps { ButtonGroupProps {
const forecolorButton = iconButton({ const forecolorButton = iconButton({

View file

@ -16,6 +16,16 @@ anki-editable {
} }
} }
p {
margin-top: 0;
margin-bottom: 1rem;
&:empty::after {
content: "\a";
white-space: pre;
}
}
:host-context(.nightMode) * { :host-context(.nightMode) * {
@include scrollbar.night-mode; @include scrollbar.night-mode;
} }

View file

@ -3,6 +3,7 @@
import type { Editable } from "./editable"; import type { Editable } from "./editable";
import { updateActiveButtons } from "editor-toolbar";
import { bridgeCommand } from "./lib"; import { bridgeCommand } from "./lib";
import { onInput, onKey, onKeyUp } from "./inputHandlers"; import { onInput, onKey, onKeyUp } from "./inputHandlers";
import { onFocus, onBlur } from "./focusHandlers"; import { onFocus, onBlur } from "./focusHandlers";
@ -59,8 +60,7 @@ export class EditingArea extends HTMLDivElement {
this.addEventListener("paste", onPaste); this.addEventListener("paste", onPaste);
this.addEventListener("copy", onCutOrCopy); this.addEventListener("copy", onCutOrCopy);
this.addEventListener("oncut", onCutOrCopy); this.addEventListener("oncut", onCutOrCopy);
// @ts-expect-error this.addEventListener("mouseup", updateActiveButtons);
this.addEventListener("mouseup", editorToolbar.updateActiveButtons);
const baseStyleSheet = this.baseStyle.sheet as CSSStyleSheet; const baseStyleSheet = this.baseStyle.sheet as CSSStyleSheet;
baseStyleSheet.insertRule("anki-editable {}", 0); baseStyleSheet.insertRule("anki-editable {}", 0);
@ -75,8 +75,7 @@ export class EditingArea extends HTMLDivElement {
this.removeEventListener("paste", onPaste); this.removeEventListener("paste", onPaste);
this.removeEventListener("copy", onCutOrCopy); this.removeEventListener("copy", onCutOrCopy);
this.removeEventListener("oncut", onCutOrCopy); this.removeEventListener("oncut", onCutOrCopy);
// @ts-expect-error this.removeEventListener("mouseup", updateActiveButtons);
this.removeEventListener("mouseup", editorToolbar.updateActiveButtons);
} }
initialize(color: string, content: string): void { initialize(color: string, content: string): void {

View file

@ -1,6 +1,7 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // 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 type { EditingArea } from "./editingArea";
import { saveField } from "./changeTimer"; import { saveField } from "./changeTimer";
@ -10,8 +11,7 @@ export function onFocus(evt: FocusEvent): void {
const currentField = evt.currentTarget as EditingArea; const currentField = evt.currentTarget as EditingArea;
currentField.focusEditable(); currentField.focusEditable();
bridgeCommand(`focus:${currentField.ord}`); bridgeCommand(`focus:${currentField.ord}`);
// @ts-expect-error enableButtons();
editorToolbar.enableButtons();
} }
export function onBlur(evt: FocusEvent): void { export function onBlur(evt: FocusEvent): void {
@ -19,6 +19,5 @@ export function onBlur(evt: FocusEvent): void {
const currentFieldUnchanged = previousFocus === document.activeElement; const currentFieldUnchanged = previousFocus === document.activeElement;
saveField(previousFocus, currentFieldUnchanged ? "key" : "blur"); saveField(previousFocus, currentFieldUnchanged ? "key" : "blur");
// @ts-expect-error disableButtons();
editorToolbar.disableButtons();
} }

View file

@ -1,19 +1,23 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import ButtonGroup from "./ButtonGroup.svelte"; import type ButtonGroup from "editor-toolbar/ButtonGroup.svelte";
import type { ButtonGroupProps } from "./ButtonGroup"; import type { ButtonGroupProps } from "editor-toolbar/ButtonGroup";
import ButtonDropdown from "./ButtonDropdown.svelte"; import type ButtonDropdown from "editor-toolbar/ButtonDropdown.svelte";
import type { ButtonDropdownProps } from "./ButtonDropdown"; import type { ButtonDropdownProps } from "editor-toolbar/ButtonDropdown";
import WithDropdownMenu from "./WithDropdownMenu.svelte"; import type { DynamicSvelteComponent } from "sveltelib/dynamicComponent";
import type { WithDropdownMenuProps } from "./WithDropdownMenu";
import CommandIconButton from "./CommandIconButton.svelte"; import type { EditingArea } from "./editingArea";
import type { CommandIconButtonProps } from "./CommandIconButton";
import IconButton from "./IconButton.svelte";
import type { IconButtonProps } from "./IconButton";
import { DynamicSvelteComponent, dynamicComponent } from "sveltelib/dynamicComponent";
import * as tr from "anki/i18n"; 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 ulIcon from "./list-ul.svg";
import olIcon from "./list-ol.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 indentIcon from "./text-indent-left.svg";
import outdentIcon from "./text-indent-right.svg"; import outdentIcon from "./text-indent-right.svg";
const commandIconButton = dynamicComponent< const outdentListItem = () => {
typeof CommandIconButton, const currentField = document.activeElement as EditingArea;
CommandIconButtonProps if (getListItem(currentField.shadowRoot!)) {
>(CommandIconButton); document.execCommand("outdent");
}
};
const buttonGroup = dynamicComponent<typeof ButtonGroup, ButtonGroupProps>(ButtonGroup); const indentListItem = () => {
const buttonDropdown = dynamicComponent<typeof ButtonDropdown, ButtonDropdownProps>( const currentField = document.activeElement as EditingArea;
ButtonDropdown if (getListItem(currentField.shadowRoot!)) {
); document.execCommand("indent");
}
const withDropdownMenu = dynamicComponent< };
typeof WithDropdownMenu,
WithDropdownMenuProps
>(WithDropdownMenu);
export function getFormatBlockMenus(): (DynamicSvelteComponent<typeof ButtonDropdown> & export function getFormatBlockMenus(): (DynamicSvelteComponent<typeof ButtonDropdown> &
ButtonDropdownProps)[] { ButtonDropdownProps)[] {
@ -78,18 +81,16 @@ export function getFormatBlockMenus(): (DynamicSvelteComponent<typeof ButtonDrop
], ],
}); });
const outdentButton = commandIconButton({ const outdentButton = iconButton({
icon: outdentIcon, icon: outdentIcon,
command: "outdent", onClick: outdentListItem,
tooltip: tr.editingOutdent(), tooltip: tr.editingOutdent(),
activatable: false,
}); });
const indentButton = commandIconButton({ const indentButton = iconButton({
icon: indentIcon, icon: indentIcon,
command: "indent", onClick: indentListItem,
tooltip: tr.editingIndent(), tooltip: tr.editingIndent(),
activatable: false,
}); });
const indentationGroup = buttonGroup({ const indentationGroup = buttonGroup({
@ -105,8 +106,6 @@ export function getFormatBlockMenus(): (DynamicSvelteComponent<typeof ButtonDrop
return [formattingOptions]; return [formattingOptions];
} }
const iconButton = dynamicComponent<typeof IconButton, IconButtonProps>(IconButton);
export function getFormatBlockGroup(): DynamicSvelteComponent<typeof ButtonGroup> & export function getFormatBlockGroup(): DynamicSvelteComponent<typeof ButtonGroup> &
ButtonGroupProps { ButtonGroupProps {
const ulButton = commandIconButton({ const ulButton = commandIconButton({
@ -131,7 +130,7 @@ export function getFormatBlockGroup(): DynamicSvelteComponent<typeof ButtonGroup
}); });
return buttonGroup({ return buttonGroup({
id: "formatBlock", id: "blockFormatting",
buttons: [ulButton, olButton, listFormatting], buttons: [ulButton, olButton, listFormatting],
}); });
} }

View file

@ -1,12 +1,15 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import CommandIconButton from "./CommandIconButton.svelte"; import type ButtonGroup from "editor-toolbar/ButtonGroup.svelte";
import type { CommandIconButtonProps } from "./CommandIconButton"; import type { ButtonGroupProps } from "editor-toolbar/ButtonGroup";
import ButtonGroup from "./ButtonGroup.svelte"; import type { DynamicSvelteComponent } from "sveltelib/dynamicComponent";
import type { ButtonGroupProps } from "./ButtonGroup";
import { DynamicSvelteComponent, dynamicComponent } from "sveltelib/dynamicComponent";
import * as tr from "anki/i18n"; import * as tr from "anki/i18n";
import {
commandIconButton,
iconButton,
buttonGroup,
} from "editor-toolbar/dynamicComponents";
import boldIcon from "./type-bold.svg"; import boldIcon from "./type-bold.svg";
import italicIcon from "./type-italic.svg"; import italicIcon from "./type-italic.svg";
@ -15,53 +18,48 @@ import superscriptIcon from "./format-superscript.svg";
import subscriptIcon from "./format-subscript.svg"; import subscriptIcon from "./format-subscript.svg";
import eraserIcon from "./eraser.svg"; import eraserIcon from "./eraser.svg";
const commandIconButton = dynamicComponent<
typeof CommandIconButton,
CommandIconButtonProps
>(CommandIconButton);
const buttonGroup = dynamicComponent<typeof ButtonGroup, ButtonGroupProps>(ButtonGroup);
export function getFormatInlineGroup(): DynamicSvelteComponent<typeof ButtonGroup> & export function getFormatInlineGroup(): DynamicSvelteComponent<typeof ButtonGroup> &
ButtonGroupProps { ButtonGroupProps {
const boldButton = commandIconButton({ const boldButton = commandIconButton({
icon: boldIcon, icon: boldIcon,
command: "bold",
tooltip: tr.editingBoldTextCtrlandb(), tooltip: tr.editingBoldTextCtrlandb(),
command: "bold",
}); });
const italicButton = commandIconButton({ const italicButton = commandIconButton({
icon: italicIcon, icon: italicIcon,
command: "italic",
tooltip: tr.editingItalicTextCtrlandi(), tooltip: tr.editingItalicTextCtrlandi(),
command: "italic",
}); });
const underlineButton = commandIconButton({ const underlineButton = commandIconButton({
icon: underlineIcon, icon: underlineIcon,
command: "underline",
tooltip: tr.editingUnderlineTextCtrlandu(), tooltip: tr.editingUnderlineTextCtrlandu(),
command: "underline",
}); });
const superscriptButton = commandIconButton({ const superscriptButton = commandIconButton({
icon: superscriptIcon, icon: superscriptIcon,
command: "superscript",
tooltip: tr.editingSuperscriptCtrlandand(), tooltip: tr.editingSuperscriptCtrlandand(),
command: "superscript",
}); });
const subscriptButton = commandIconButton({ const subscriptButton = commandIconButton({
icon: subscriptIcon, icon: subscriptIcon,
command: "subscript",
tooltip: tr.editingSubscriptCtrland(), tooltip: tr.editingSubscriptCtrland(),
command: "subscript",
}); });
const removeFormatButton = commandIconButton({ const removeFormatButton = iconButton({
icon: eraserIcon, icon: eraserIcon,
command: "removeFormat",
activatable: false,
tooltip: tr.editingRemoveFormattingCtrlandr(), tooltip: tr.editingRemoveFormattingCtrlandr(),
onClick: () => {
document.execCommand("removeFormat");
},
}); });
return buttonGroup({ return buttonGroup({
id: "formatInline", id: "inlineFormatting",
buttons: [ buttons: [
boldButton, boldButton,
italicButton, italicButton,

View file

@ -77,3 +77,36 @@ export function caretToEnd(currentField: EditingArea): void {
selection.removeAllRanges(); selection.removeAllRanges();
selection.addRange(range); selection.addRange(range);
} }
const getAnchorParent = <T extends Element>(
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);

View file

@ -2,6 +2,10 @@
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { filterHTML } from "html-filter"; 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 { caretToEnd } from "./helpers";
import { saveField } from "./changeTimer"; import { saveField } from "./changeTimer";
@ -10,11 +14,14 @@ import { EditorField } from "./editorField";
import { LabelContainer } from "./labelContainer"; import { LabelContainer } from "./labelContainer";
import { EditingArea } from "./editingArea"; import { EditingArea } from "./editingArea";
import { Editable } from "./editable"; import { Editable } from "./editable";
import { initToolbar } from "./toolbar";
export { setNoteId, getNoteId } from "./noteId"; export { setNoteId, getNoteId } from "./noteId";
export { saveNow } from "./changeTimer"; export { saveNow } from "./changeTimer";
export { wrap, wrapIntoText } from "./wrap"; export { wrap, wrapIntoText } from "./wrap";
export * from "./addons";
declare global { declare global {
interface Selection { interface Selection {
modify(s: string, t: string, u: string): void; 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-editable", Editable);
customElements.define("anki-editing-area", EditingArea, { extends: "div" }); customElements.define("anki-editing-area", EditingArea, { extends: "div" });
customElements.define("anki-label-container", LabelContainer, { extends: "div" }); customElements.define("anki-label-container", LabelContainer, { extends: "div" });
@ -41,8 +49,7 @@ export function focusField(n: number): void {
if (field) { if (field) {
field.editingArea.focusEditable(); field.editingArea.focusEditable();
caretToEnd(field.editingArea); caretToEnd(field.editingArea);
// @ts-expect-error updateActiveButtons();
editorToolbar.updateActiveButtons();
} }
} }
@ -122,8 +129,7 @@ export function setFields(fields: [string, string][]): void {
if (!getCurrentField()) { if (!getCurrentField()) {
// when initial focus of the window is not on editor (e.g. browser) // when initial focus of the window is not on editor (e.g. browser)
// @ts-expect-error disableButtons();
editorToolbar.disableButtons();
} }
} }
@ -158,7 +164,10 @@ export function setFormat(cmd: string, arg?: any, nosave: boolean = false): void
document.execCommand(cmd, false, arg); document.execCommand(cmd, false, arg);
if (!nosave) { if (!nosave) {
saveField(getCurrentField() as EditingArea, "key"); saveField(getCurrentField() as EditingArea, "key");
// @ts-expect-error updateActiveButtons();
editorToolbar.updateActiveButtons();
} }
} }
const i18n = setupI18n({ modules: [ModuleName.EDITING] });
initToolbar(i18n);

View file

@ -1,28 +1,15 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { updateActiveButtons } from "editor-toolbar";
import { EditingArea } from "./editingArea"; import { EditingArea } from "./editingArea";
import { caretToEnd, nodeIsElement } from "./helpers"; import { caretToEnd, nodeIsElement, getBlockElement } from "./helpers";
import { triggerChangeTimer } from "./changeTimer"; 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 { export function onInput(event: Event): void {
// make sure IME changes get saved // make sure IME changes get saved
triggerChangeTimer(event.currentTarget as EditingArea); triggerChangeTimer(event.currentTarget as EditingArea);
// @ts-ignore updateActiveButtons();
editorToolbar.updateActiveButtons();
} }
export function onKey(evt: KeyboardEvent): void { export function onKey(evt: KeyboardEvent): void {
@ -35,7 +22,10 @@ export function onKey(evt: KeyboardEvent): void {
} }
// prefer <br> instead of <div></div> // prefer <br> instead of <div></div>
if (evt.code === "Enter" && !inListItem(currentField)) { if (
evt.code === "Enter" &&
!getBlockElement(currentField.shadowRoot!) !== evt.shiftKey
) {
evt.preventDefault(); evt.preventDefault();
document.execCommand("insertLineBreak"); document.execCommand("insertLineBreak");
} }
@ -69,8 +59,7 @@ globalThis.addEventListener("keydown", (evt: KeyboardEvent) => {
const newFocusTarget = evt.target; const newFocusTarget = evt.target;
if (newFocusTarget instanceof EditingArea) { if (newFocusTarget instanceof EditingArea) {
caretToEnd(newFocusTarget); caretToEnd(newFocusTarget);
// @ts-ignore updateActiveButtons();
editorToolbar.updateActiveButtons();
} }
}, },
{ once: true } { once: true }

View file

@ -1,16 +1,12 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import LabelButton from "./LabelButton.svelte"; import type ButtonGroup from "editor-toolbar/ButtonGroup.svelte";
import type { LabelButtonProps } from "./LabelButton"; import type { ButtonGroupProps } from "editor-toolbar/ButtonGroup";
import ButtonGroup from "./ButtonGroup.svelte"; import type { DynamicSvelteComponent } from "sveltelib/dynamicComponent";
import type { ButtonGroupProps } from "./ButtonGroup";
import { DynamicSvelteComponent, dynamicComponent } from "sveltelib/dynamicComponent";
import { bridgeCommand } from "anki/bridgecommand"; import { bridgeCommand } from "anki/bridgecommand";
import * as tr from "anki/i18n"; import * as tr from "anki/i18n";
import { labelButton, buttonGroup } from "editor-toolbar/dynamicComponents";
const labelButton = dynamicComponent<typeof LabelButton, LabelButtonProps>(LabelButton);
const buttonGroup = dynamicComponent<typeof ButtonGroup, ButtonGroupProps>(ButtonGroup);
export function getNotetypeGroup(): DynamicSvelteComponent<typeof ButtonGroup> & export function getNotetypeGroup(): DynamicSvelteComponent<typeof ButtonGroup> &
ButtonGroupProps { ButtonGroupProps {

View file

@ -1,20 +1,23 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import IconButton from "./IconButton.svelte"; import type DropdownMenu from "editor-toolbar/DropdownMenu.svelte";
import type { IconButtonProps } from "./IconButton"; import type { DropdownMenuProps } from "editor-toolbar/DropdownMenu";
import DropdownMenu from "./DropdownMenu.svelte"; import type ButtonGroup from "editor-toolbar/ButtonGroup.svelte";
import type { DropdownMenuProps } from "./DropdownMenu"; import type { ButtonGroupProps } from "editor-toolbar/ButtonGroup";
import DropdownItem from "./DropdownItem.svelte"; import type { DynamicSvelteComponent } from "sveltelib/dynamicComponent";
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 { 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 * as tr from "anki/i18n";
import { wrap } from "./wrap";
import paperclipIcon from "./paperclip.svg"; import paperclipIcon from "./paperclip.svg";
import micIcon from "./mic.svg"; import micIcon from "./mic.svg";
import functionIcon from "./function-variant.svg"; import functionIcon from "./function-variant.svg";
@ -36,19 +39,6 @@ function onHtmlEdit(): void {
const mathjaxMenuId = "mathjaxMenu"; const mathjaxMenuId = "mathjaxMenu";
const iconButton = dynamicComponent<typeof IconButton, IconButtonProps>(IconButton);
const withDropdownMenu = dynamicComponent<
typeof WithDropdownMenu,
WithDropdownMenuProps
>(WithDropdownMenu);
const dropdownMenu = dynamicComponent<typeof DropdownMenu, DropdownMenuProps>(
DropdownMenu
);
const dropdownItem = dynamicComponent<typeof DropdownItem, DropdownItemProps>(
DropdownItem
);
const buttonGroup = dynamicComponent<typeof ButtonGroup, ButtonGroupProps>(ButtonGroup);
export function getTemplateGroup(): DynamicSvelteComponent<typeof ButtonGroup> & export function getTemplateGroup(): DynamicSvelteComponent<typeof ButtonGroup> &
ButtonGroupProps { ButtonGroupProps {
const attachmentButton = iconButton({ const attachmentButton = iconButton({
@ -95,19 +85,16 @@ export function getTemplateMenus(): (DynamicSvelteComponent<typeof DropdownMenu>
DropdownMenuProps)[] { DropdownMenuProps)[] {
const mathjaxMenuItems = [ const mathjaxMenuItems = [
dropdownItem({ dropdownItem({
// @ts-expect-error
onClick: () => wrap("\\(", "\\)"), onClick: () => wrap("\\(", "\\)"),
label: tr.editingMathjaxInline(), label: tr.editingMathjaxInline(),
endLabel: "Ctrl+M, M", endLabel: "Ctrl+M, M",
}), }),
dropdownItem({ dropdownItem({
// @ts-expect-error
onClick: () => wrap("\\[", "\\]"), onClick: () => wrap("\\[", "\\]"),
label: tr.editingMathjaxBlock(), label: tr.editingMathjaxBlock(),
endLabel: "Ctrl+M, E", endLabel: "Ctrl+M, E",
}), }),
dropdownItem({ dropdownItem({
// @ts-expect-error
onClick: () => wrap("\\(\\ce{", "}\\)"), onClick: () => wrap("\\(\\ce{", "}\\)"),
label: tr.editingMathjaxChemistry(), label: tr.editingMathjaxChemistry(),
endLabel: "Ctrl+M, C", endLabel: "Ctrl+M, C",
@ -116,19 +103,16 @@ export function getTemplateMenus(): (DynamicSvelteComponent<typeof DropdownMenu>
const latexMenuItems = [ const latexMenuItems = [
dropdownItem({ dropdownItem({
// @ts-expect-error
onClick: () => wrap("[latex]", "[/latex]"), onClick: () => wrap("[latex]", "[/latex]"),
label: tr.editingLatex(), label: tr.editingLatex(),
endLabel: "Ctrl+T, T", endLabel: "Ctrl+T, T",
}), }),
dropdownItem({ dropdownItem({
// @ts-expect-error
onClick: () => wrap("[$]", "[/$]"), onClick: () => wrap("[$]", "[/$]"),
label: tr.editingLatexEquation(), label: tr.editingLatexEquation(),
endLabel: "Ctrl+T, E", endLabel: "Ctrl+T, E",
}), }),
dropdownItem({ dropdownItem({
// @ts-expect-error
onClick: () => wrap("[$$]", "[/$$]"), onClick: () => wrap("[$$]", "[/$$]"),
label: tr.editingLatexMathEnv(), label: tr.editingLatexMathEnv(),
endLabel: "Ctrl+T, M", endLabel: "Ctrl+T, M",

45
ts/editor/toolbar.ts Normal file
View file

@ -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>): void {
document.addEventListener("DOMContentLoaded", () => {
i18n.then(() => {
globalThis.$editorToolbar.buttonsPromise.then(
(
buttons: Writable<
(ToolbarItem<typeof ButtonGroup> & ButtonGroupProps)[]
>
): Writable<(ToolbarItem<typeof ButtonGroup> & ButtonGroupProps)[]> => {
buttons.update(() => [
getNotetypeGroup(),
getFormatInlineGroup(),
getFormatBlockGroup(),
getColorGroup(),
getTemplateGroup(),
]);
return buttons;
}
);
globalThis.$editorToolbar.menusPromise.then(
(menus: Writable<ToolbarItem[]>): Writable<ToolbarItem[]> => {
menus.update(() => [
...getFormatBlockMenus(),
...getTemplateMenus(),
]);
return menus;
}
);
});
});
}

View file

@ -5,11 +5,11 @@ load("//ts:eslint.bzl", "eslint_test")
ts_library( ts_library(
name = "html-filter", name = "html-filter",
module_name = "html-filter",
srcs = glob( srcs = glob(
["*.ts"], ["*.ts"],
exclude = ["*.test.ts"], exclude = ["*.test.ts"],
), ),
module_name = "html-filter",
tsconfig = "//ts:tsconfig.json", tsconfig = "//ts:tsconfig.json",
visibility = ["//visibility:public"], visibility = ["//visibility:public"],
deps = [], deps = [],

View file

@ -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: for src in srcs:
svelte( svelte(
name = src.replace(".svelte", ""), name = src.replace(".svelte", ""),
entry_point = src, entry_point = src,
deps = deps, deps = deps,
visibility = visibility,
) )
native.filegroup( native.filegroup(
name = name, name = name,
srcs = srcs, srcs = srcs,
visibility = visibility,
) )
def svelte_check(name = "svelte_check", srcs = []): def svelte_check(name = "svelte_check", srcs = []):

View file

@ -8,7 +8,8 @@
"paths": { "paths": {
"anki/*": ["../bazel-bin/ts/lib/*"], "anki/*": ["../bazel-bin/ts/lib/*"],
"sveltelib/*": ["../bazel-bin/ts/sveltelib/*"], "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", "importsNotUsedAsValues": "error",
"outDir": "dist", "outDir": "dist",