Make indent outdent only work for list items
+ make paragraph show its active state
This commit is contained in:
parent
9803bb19ca
commit
893028b2df
@ -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",
|
||||
|
8
ts/editor-toolbar/CommandIconButton.d.ts
vendored
8
ts/editor-toolbar/CommandIconButton.d.ts
vendored
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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({
|
||||
|
@ -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({
|
||||
|
@ -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);
|
||||
|
@ -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");
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user