Make indent outdent only work for list items

+ make paragraph show its active state
This commit is contained in:
Henrik Giesel 2021-04-20 03:24:08 +02:00
parent 9803bb19ca
commit 893028b2df
9 changed files with 96 additions and 71 deletions

View file

@ -44,7 +44,6 @@ ts_library(
deps = [ deps = [
"//ts/lib", "//ts/lib",
"//ts/lib:backend_proto", "//ts/lib:backend_proto",
"//ts:image_module_support",
"//ts/sveltelib", "//ts/sveltelib",
"@npm//@popperjs/core", "@npm//@popperjs/core",
"@npm//@types/bootstrap", "@npm//@types/bootstrap",

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

@ -9,17 +9,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
type ActiveMap = Map<string, boolean>; type ActiveMap = Map<string, boolean>;
const updateMap = new Map() as UpdateMap; 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 { function updateButton(key: string, event: MouseEvent): void {
activeMap.update( activeStore.update(
(map: ActiveMap): ActiveMap => (map: ActiveMap): ActiveMap =>
new Map([...map, [key, updateMap.get(key)(event)]]) new Map([...map, [key, updateMap.get(key)(event)]])
); );
} }
function updateButtons(callback: (key: string) => boolean): void { function updateButtons(callback: (key: string) => boolean): void {
activeMap.update( activeStore.update(
(map: ActiveMap): ActiveMap => { (map: ActiveMap): ActiveMap => {
const newMap = new Map() as 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 icon: string;
export let command: string; export let command: string;
export let onClick = () => { export let onClick = (_event: MouseEvent) => {
document.execCommand(command); document.execCommand(command);
}; };
@ -59,19 +60,17 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
updateButton(command, event); updateButton(command, event);
} }
export let activatable = true;
export let onUpdate = (_event: Event) => document.queryCommandState(command); export let onUpdate = (_event: Event) => document.queryCommandState(command);
updateMap.set(command, onUpdate); updateMap.set(command, onUpdate);
let active = false; let active = false;
if (activatable) { activeStore.subscribe((map: ActiveMap): (() => void) => {
activeMap.subscribe((map: ActiveMap): (() => void) => { active = Boolean(map.get(command));
active = Boolean(map.get(command)); return () => map.delete(command);
return () => map.delete(command); });
}); activeMap.set(command, active);
}
export let disables = true; export let disables = true;
export let dropdownToggle = false; export let dropdownToggle = false;

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

@ -12,9 +12,13 @@ import type { CommandIconButtonProps } from "editor-toolbar/CommandIconButton";
import IconButton from "editor-toolbar/IconButton.svelte"; import IconButton from "editor-toolbar/IconButton.svelte";
import type { IconButtonProps } from "editor-toolbar/IconButton"; import type { IconButtonProps } from "editor-toolbar/IconButton";
import type { EditingArea } from "./editingArea";
import { DynamicSvelteComponent, dynamicComponent } from "sveltelib/dynamicComponent"; import { DynamicSvelteComponent, dynamicComponent } from "sveltelib/dynamicComponent";
import * as tr from "anki/i18n"; import * as tr from "anki/i18n";
import { getListItem, getParagraph } from "./helpers";
import paragraphIcon from "./paragraph.svg"; import paragraphIcon from "./paragraph.svg";
import ulIcon from "./list-ul.svg"; import ulIcon from "./list-ul.svg";
import olIcon from "./list-ol.svg"; import olIcon from "./list-ol.svg";
@ -33,6 +37,8 @@ const commandIconButton = dynamicComponent<
CommandIconButtonProps CommandIconButtonProps
>(CommandIconButton); >(CommandIconButton);
const iconButton = dynamicComponent<typeof IconButton, IconButtonProps>(IconButton);
const buttonGroup = dynamicComponent<typeof ButtonGroup, ButtonGroupProps>(ButtonGroup); const buttonGroup = dynamicComponent<typeof ButtonGroup, ButtonGroupProps>(ButtonGroup);
const buttonDropdown = dynamicComponent<typeof ButtonDropdown, ButtonDropdownProps>( const buttonDropdown = dynamicComponent<typeof ButtonDropdown, ButtonDropdownProps>(
ButtonDropdown ButtonDropdown
@ -43,6 +49,25 @@ const withDropdownMenu = dynamicComponent<
WithDropdownMenuProps WithDropdownMenuProps
>(WithDropdownMenu); >(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<typeof ButtonDropdown> & export function getFormatBlockMenus(): (DynamicSvelteComponent<typeof ButtonDropdown> &
ButtonDropdownProps)[] { ButtonDropdownProps)[] {
const justifyLeftButton = commandIconButton({ const justifyLeftButton = commandIconButton({
@ -79,18 +104,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({
@ -106,8 +129,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 paragraphButton = commandIconButton({ const paragraphButton = commandIconButton({
@ -116,8 +137,8 @@ export function getFormatBlockGroup(): DynamicSvelteComponent<typeof ButtonGroup
onClick: () => { onClick: () => {
document.execCommand("formatBlock", false, "p"); document.execCommand("formatBlock", false, "p");
}, },
onUpdate: checkForParagraph,
tooltip: tr.editingUnorderedList(), tooltip: tr.editingUnorderedList(),
activatable: false,
}); });
const ulButton = commandIconButton({ const ulButton = commandIconButton({

View file

@ -2,6 +2,8 @@
// 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 "editor-toolbar/CommandIconButton.svelte"; import CommandIconButton from "editor-toolbar/CommandIconButton.svelte";
import type { CommandIconButtonProps } from "editor-toolbar/CommandIconButton"; 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 ButtonGroup from "editor-toolbar/ButtonGroup.svelte";
import type { ButtonGroupProps } from "editor-toolbar/ButtonGroup"; import type { ButtonGroupProps } from "editor-toolbar/ButtonGroup";
@ -19,45 +21,47 @@ const commandIconButton = dynamicComponent<
typeof CommandIconButton, typeof CommandIconButton,
CommandIconButtonProps CommandIconButtonProps
>(CommandIconButton); >(CommandIconButton);
const iconButton = dynamicComponent<typeof IconButton, IconButtonProps>(IconButton);
const buttonGroup = dynamicComponent<typeof ButtonGroup, ButtonGroupProps>(ButtonGroup); 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({

View file

@ -77,3 +77,31 @@ 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";
export const getListItem = getAnchorParent(isListItem);
export const getParagraph = getAnchorParent(isParagraph);

View file

@ -3,38 +3,9 @@
import { updateActiveButtons } from "editor-toolbar"; import { updateActiveButtons } from "editor-toolbar";
import { EditingArea } from "./editingArea"; import { EditingArea } from "./editingArea";
import { caretToEnd, nodeIsElement } from "./helpers"; import { caretToEnd, nodeIsElement, getListItem, getParagraph } from "./helpers";
import { triggerChangeTimer } from "./changeTimer"; import { triggerChangeTimer } from "./changeTimer";
const getAnchorParent = <T extends Element>(
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 { 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);
@ -53,8 +24,8 @@ export function onKey(evt: KeyboardEvent): void {
// prefer <br> instead of <div></div> // prefer <br> instead of <div></div>
if ( if (
evt.code === "Enter" && evt.code === "Enter" &&
!getListItem(currentField) && !getListItem(currentField.shadowRoot!) &&
!getParagraph(currentField) !getParagraph(currentField.shadowRoot!)
) { ) {
evt.preventDefault(); evt.preventDefault();
document.execCommand("insertLineBreak"); document.execCommand("insertLineBreak");

View file

@ -15,6 +15,8 @@ import { bridgeCommand } from "anki/bridgecommand";
import { DynamicSvelteComponent, dynamicComponent } from "sveltelib/dynamicComponent"; import { DynamicSvelteComponent, dynamicComponent } from "sveltelib/dynamicComponent";
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";
@ -95,19 +97,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 +115,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",