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 = [
"//ts/lib",
"//ts/lib:backend_proto",
"//ts:image_module_support",
"//ts/sveltelib",
"@npm//@popperjs/core",
"@npm//@types/bootstrap",

View File

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

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>;
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;

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 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;
</script>
<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 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<typeof IconButton, IconButtonProps>(IconButton);
const buttonGroup = dynamicComponent<typeof ButtonGroup, ButtonGroupProps>(ButtonGroup);
const buttonDropdown = dynamicComponent<typeof ButtonDropdown, ButtonDropdownProps>(
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<typeof ButtonDropdown> &
ButtonDropdownProps)[] {
const justifyLeftButton = commandIconButton({
@ -79,18 +104,16 @@ export function getFormatBlockMenus(): (DynamicSvelteComponent<typeof ButtonDrop
],
});
const outdentButton = commandIconButton({
const outdentButton = iconButton({
icon: outdentIcon,
command: "outdent",
onClick: outdentListItem,
tooltip: tr.editingOutdent(),
activatable: false,
});
const indentButton = commandIconButton({
const indentButton = iconButton({
icon: indentIcon,
command: "indent",
onClick: indentListItem,
tooltip: tr.editingIndent(),
activatable: false,
});
const indentationGroup = buttonGroup({
@ -106,8 +129,6 @@ export function getFormatBlockMenus(): (DynamicSvelteComponent<typeof ButtonDrop
return [formattingOptions];
}
const iconButton = dynamicComponent<typeof IconButton, IconButtonProps>(IconButton);
export function getFormatBlockGroup(): DynamicSvelteComponent<typeof ButtonGroup> &
ButtonGroupProps {
const paragraphButton = commandIconButton({
@ -116,8 +137,8 @@ export function getFormatBlockGroup(): DynamicSvelteComponent<typeof ButtonGroup
onClick: () => {
document.execCommand("formatBlock", false, "p");
},
onUpdate: checkForParagraph,
tooltip: tr.editingUnorderedList(),
activatable: false,
});
const ulButton = commandIconButton({

View File

@ -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<typeof IconButton, IconButtonProps>(IconButton);
const buttonGroup = dynamicComponent<typeof ButtonGroup, ButtonGroupProps>(ButtonGroup);
export function getFormatInlineGroup(): DynamicSvelteComponent<typeof ButtonGroup> &
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({

View File

@ -77,3 +77,31 @@ export function caretToEnd(currentField: EditingArea): void {
selection.removeAllRanges();
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 { EditingArea } from "./editingArea";
import { caretToEnd, nodeIsElement } from "./helpers";
import { caretToEnd, nodeIsElement, getListItem, getParagraph } from "./helpers";
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 {
// make sure IME changes get saved
triggerChangeTimer(event.currentTarget as EditingArea);
@ -53,8 +24,8 @@ export function onKey(evt: KeyboardEvent): void {
// prefer <br> instead of <div></div>
if (
evt.code === "Enter" &&
!getListItem(currentField) &&
!getParagraph(currentField)
!getListItem(currentField.shadowRoot!) &&
!getParagraph(currentField.shadowRoot!)
) {
evt.preventDefault();
document.execCommand("insertLineBreak");

View File

@ -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<typeof DropdownMenu>
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<typeof DropdownMenu>
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",