From 893028b2df3040ab5d5d94c0a80d97094c02a290 Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Tue, 20 Apr 2021 03:24:08 +0200 Subject: [PATCH] Make indent outdent only work for list items + make paragraph show its active state --- ts/editor-toolbar/BUILD.bazel | 1 - ts/editor-toolbar/CommandIconButton.d.ts | 8 ++++- ts/editor-toolbar/CommandIconButton.svelte | 21 ++++++------ ts/editor-toolbar/IconButton.svelte | 7 ++-- ts/editor/formatBlock.ts | 39 +++++++++++++++++----- ts/editor/formatInline.ts | 20 ++++++----- ts/editor/helpers.ts | 28 ++++++++++++++++ ts/editor/inputHandlers.ts | 35 ++----------------- ts/editor/template.ts | 8 ++--- 9 files changed, 96 insertions(+), 71 deletions(-) diff --git a/ts/editor-toolbar/BUILD.bazel b/ts/editor-toolbar/BUILD.bazel index 92419a273..4aea9880c 100644 --- a/ts/editor-toolbar/BUILD.bazel +++ b/ts/editor-toolbar/BUILD.bazel @@ -44,7 +44,6 @@ ts_library( deps = [ "//ts/lib", "//ts/lib:backend_proto", - "//ts:image_module_support", "//ts/sveltelib", "@npm//@popperjs/core", "@npm//@types/bootstrap", diff --git a/ts/editor-toolbar/CommandIconButton.d.ts b/ts/editor-toolbar/CommandIconButton.d.ts index 2fe43d032..a64bfd4e6 100644 --- a/ts/editor-toolbar/CommandIconButton.d.ts +++ b/ts/editor-toolbar/CommandIconButton.d.ts @@ -5,6 +5,12 @@ export interface CommandIconButtonProps { className?: string; tooltip: string; icon: string; + command: string; - activatable?: boolean; + onClick: (event: MouseEvent) => void; + + onUpdate: (event: Event) => boolean; + + disables?: boolean; + dropdownToggle?: boolean; } diff --git a/ts/editor-toolbar/CommandIconButton.svelte b/ts/editor-toolbar/CommandIconButton.svelte index 96a5885b1..b78025274 100644 --- a/ts/editor-toolbar/CommandIconButton.svelte +++ b/ts/editor-toolbar/CommandIconButton.svelte @@ -9,17 +9,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html type ActiveMap = Map; const updateMap = new Map() as UpdateMap; - const activeMap = writable(new Map() as ActiveMap); + const activeMap = new Map() as ActiveMap; + const activeStore = writable(activeMap); function updateButton(key: string, event: MouseEvent): void { - activeMap.update( + activeStore.update( (map: ActiveMap): ActiveMap => new Map([...map, [key, updateMap.get(key)(event)]]) ); } function updateButtons(callback: (key: string) => boolean): void { - activeMap.update( + activeStore.update( (map: ActiveMap): ActiveMap => { const newMap = new Map() as ActiveMap; @@ -50,7 +51,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html export let icon: string; export let command: string; - export let onClick = () => { + export let onClick = (_event: MouseEvent) => { document.execCommand(command); }; @@ -59,19 +60,17 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html updateButton(command, event); } - export let activatable = true; export let onUpdate = (_event: Event) => document.queryCommandState(command); updateMap.set(command, onUpdate); let active = false; - if (activatable) { - activeMap.subscribe((map: ActiveMap): (() => void) => { - active = Boolean(map.get(command)); - return () => map.delete(command); - }); - } + activeStore.subscribe((map: ActiveMap): (() => void) => { + active = Boolean(map.get(command)); + return () => map.delete(command); + }); + activeMap.set(command, active); export let disables = true; export let dropdownToggle = false; diff --git a/ts/editor-toolbar/IconButton.svelte b/ts/editor-toolbar/IconButton.svelte index ff9e0a0d4..849b33575 100644 --- a/ts/editor-toolbar/IconButton.svelte +++ b/ts/editor-toolbar/IconButton.svelte @@ -8,11 +8,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html export let id: string; export let className = ""; export let tooltip: string; + export let icon: string; + + export let onClick: (event: MouseEvent) => void; + export let disables = true; export let dropdownToggle = false; - - export let icon = ""; - export let onClick: (event: MouseEvent) => void; diff --git a/ts/editor/formatBlock.ts b/ts/editor/formatBlock.ts index 0de457c20..1d2b4b753 100644 --- a/ts/editor/formatBlock.ts +++ b/ts/editor/formatBlock.ts @@ -12,9 +12,13 @@ import type { CommandIconButtonProps } from "editor-toolbar/CommandIconButton"; import IconButton from "editor-toolbar/IconButton.svelte"; import type { IconButtonProps } from "editor-toolbar/IconButton"; +import type { EditingArea } from "./editingArea"; + import { DynamicSvelteComponent, dynamicComponent } from "sveltelib/dynamicComponent"; import * as tr from "anki/i18n"; +import { getListItem, getParagraph } from "./helpers"; + import paragraphIcon from "./paragraph.svg"; import ulIcon from "./list-ul.svg"; import olIcon from "./list-ol.svg"; @@ -33,6 +37,8 @@ const commandIconButton = dynamicComponent< CommandIconButtonProps >(CommandIconButton); +const iconButton = dynamicComponent(IconButton); + const buttonGroup = dynamicComponent(ButtonGroup); const buttonDropdown = dynamicComponent( ButtonDropdown @@ -43,6 +49,25 @@ const withDropdownMenu = dynamicComponent< WithDropdownMenuProps >(WithDropdownMenu); +const outdentListItem = () => { + const currentField = document.activeElement as EditingArea; + if (getListItem(currentField.shadowRoot!)) { + document.execCommand("outdent"); + } +}; + +const indentListItem = () => { + const currentField = document.activeElement as EditingArea; + if (getListItem(currentField.shadowRoot!)) { + document.execCommand("indent"); + } +}; + +const checkForParagraph = (): boolean => { + const currentField = document.activeElement as EditingArea; + return Boolean(getParagraph(currentField.shadowRoot!)); +}; + export function getFormatBlockMenus(): (DynamicSvelteComponent & ButtonDropdownProps)[] { const justifyLeftButton = commandIconButton({ @@ -79,18 +104,16 @@ export function getFormatBlockMenus(): (DynamicSvelteComponent(IconButton); - export function getFormatBlockGroup(): DynamicSvelteComponent & ButtonGroupProps { const paragraphButton = commandIconButton({ @@ -116,8 +137,8 @@ export function getFormatBlockGroup(): DynamicSvelteComponent { document.execCommand("formatBlock", false, "p"); }, + onUpdate: checkForParagraph, tooltip: tr.editingUnorderedList(), - activatable: false, }); const ulButton = commandIconButton({ diff --git a/ts/editor/formatInline.ts b/ts/editor/formatInline.ts index 4ae1bddf3..ffa0bea66 100644 --- a/ts/editor/formatInline.ts +++ b/ts/editor/formatInline.ts @@ -2,6 +2,8 @@ // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import CommandIconButton from "editor-toolbar/CommandIconButton.svelte"; import type { CommandIconButtonProps } from "editor-toolbar/CommandIconButton"; +import IconButton from "editor-toolbar/IconButton.svelte"; +import type { IconButtonProps } from "editor-toolbar/IconButton"; import ButtonGroup from "editor-toolbar/ButtonGroup.svelte"; import type { ButtonGroupProps } from "editor-toolbar/ButtonGroup"; @@ -19,45 +21,47 @@ const commandIconButton = dynamicComponent< typeof CommandIconButton, CommandIconButtonProps >(CommandIconButton); +const iconButton = dynamicComponent(IconButton); const buttonGroup = dynamicComponent(ButtonGroup); export function getFormatInlineGroup(): DynamicSvelteComponent & ButtonGroupProps { const boldButton = commandIconButton({ icon: boldIcon, - command: "bold", tooltip: tr.editingBoldTextCtrlandb(), + command: "bold", }); const italicButton = commandIconButton({ icon: italicIcon, - command: "italic", tooltip: tr.editingItalicTextCtrlandi(), + command: "italic", }); const underlineButton = commandIconButton({ icon: underlineIcon, - command: "underline", tooltip: tr.editingUnderlineTextCtrlandu(), + command: "underline", }); const superscriptButton = commandIconButton({ icon: superscriptIcon, - command: "superscript", tooltip: tr.editingSuperscriptCtrlandand(), + command: "superscript", }); const subscriptButton = commandIconButton({ icon: subscriptIcon, - command: "subscript", tooltip: tr.editingSubscriptCtrland(), + command: "subscript", }); - const removeFormatButton = commandIconButton({ + const removeFormatButton = iconButton({ icon: eraserIcon, - command: "removeFormat", - activatable: false, tooltip: tr.editingRemoveFormattingCtrlandr(), + onClick: () => { + document.execCommand("removeFormat"); + }, }); return buttonGroup({ diff --git a/ts/editor/helpers.ts b/ts/editor/helpers.ts index bfed2ed7a..5f5ac5abf 100644 --- a/ts/editor/helpers.ts +++ b/ts/editor/helpers.ts @@ -77,3 +77,31 @@ export function caretToEnd(currentField: EditingArea): void { selection.removeAllRanges(); selection.addRange(range); } + +const getAnchorParent = ( + predicate: (element: Element) => element is T +) => (currentField: DocumentOrShadowRoot): T | null => { + const anchor = currentField.getSelection()?.anchorNode; + + if (!anchor) { + return null; + } + + let anchorParent: T | null = null; + let element = nodeIsElement(anchor) ? anchor : anchor.parentElement; + + while (element) { + anchorParent = anchorParent || (predicate(element) ? element : null); + element = element.parentElement; + } + + return anchorParent; +}; + +const isListItem = (element: Element): element is HTMLLIElement => + window.getComputedStyle(element).display === "list-item"; +const isParagraph = (element: Element): element is HTMLParamElement => + element.tagName === "P"; + +export const getListItem = getAnchorParent(isListItem); +export const getParagraph = getAnchorParent(isParagraph); diff --git a/ts/editor/inputHandlers.ts b/ts/editor/inputHandlers.ts index d9a5eb857..613233969 100644 --- a/ts/editor/inputHandlers.ts +++ b/ts/editor/inputHandlers.ts @@ -3,38 +3,9 @@ import { updateActiveButtons } from "editor-toolbar"; import { EditingArea } from "./editingArea"; -import { caretToEnd, nodeIsElement } from "./helpers"; +import { caretToEnd, nodeIsElement, getListItem, getParagraph } from "./helpers"; import { triggerChangeTimer } from "./changeTimer"; -const getAnchorParent = ( - predicate: (element: Element) => element is T -) => (currentField: EditingArea): 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 getListItem = getAnchorParent( - (element: Element): element is HTMLLIElement => - window.getComputedStyle(element).display === "list-item" -); - -const getParagraph = getAnchorParent( - (element: Element): element is HTMLParamElement => element.tagName === "P" -); - export function onInput(event: Event): void { // make sure IME changes get saved triggerChangeTimer(event.currentTarget as EditingArea); @@ -53,8 +24,8 @@ export function onKey(evt: KeyboardEvent): void { // prefer
instead of
if ( evt.code === "Enter" && - !getListItem(currentField) && - !getParagraph(currentField) + !getListItem(currentField.shadowRoot!) && + !getParagraph(currentField.shadowRoot!) ) { evt.preventDefault(); document.execCommand("insertLineBreak"); diff --git a/ts/editor/template.ts b/ts/editor/template.ts index 956dc5e04..99b503fcd 100644 --- a/ts/editor/template.ts +++ b/ts/editor/template.ts @@ -15,6 +15,8 @@ import { bridgeCommand } from "anki/bridgecommand"; import { DynamicSvelteComponent, dynamicComponent } from "sveltelib/dynamicComponent"; import * as tr from "anki/i18n"; +import { wrap } from "./wrap"; + import paperclipIcon from "./paperclip.svg"; import micIcon from "./mic.svg"; import functionIcon from "./function-variant.svg"; @@ -95,19 +97,16 @@ export function getTemplateMenus(): (DynamicSvelteComponent DropdownMenuProps)[] { const mathjaxMenuItems = [ dropdownItem({ - // @ts-expect-error onClick: () => wrap("\\(", "\\)"), label: tr.editingMathjaxInline(), endLabel: "Ctrl+M, M", }), dropdownItem({ - // @ts-expect-error onClick: () => wrap("\\[", "\\]"), label: tr.editingMathjaxBlock(), endLabel: "Ctrl+M, E", }), dropdownItem({ - // @ts-expect-error onClick: () => wrap("\\(\\ce{", "}\\)"), label: tr.editingMathjaxChemistry(), endLabel: "Ctrl+M, C", @@ -116,19 +115,16 @@ export function getTemplateMenus(): (DynamicSvelteComponent const latexMenuItems = [ dropdownItem({ - // @ts-expect-error onClick: () => wrap("[latex]", "[/latex]"), label: tr.editingLatex(), endLabel: "Ctrl+T, T", }), dropdownItem({ - // @ts-expect-error onClick: () => wrap("[$]", "[/$]"), label: tr.editingLatexEquation(), endLabel: "Ctrl+T, E", }), dropdownItem({ - // @ts-expect-error onClick: () => wrap("[$$]", "[/$$]"), label: tr.editingLatexMathEnv(), endLabel: "Ctrl+T, M",