anki/ts/editor/surround.ts
Henrik Giesel f534dbb8e5
Separate input components into their own directories / Remove WithShortcut (#1613)
* Put PlainTextInput into its own directory

* Create a directory for RichTextInput

* Create editor-toolbar directory

* Move PreviewButton into editor-toolbar

* The time to refactor this is not quite yet here

* Create tag-editor directory

* Remove some of the uses of WithShortcut

* Remove all uses of WithShortcut from editor package

* Remove last uses of WithShortcut

* Fix typo
2022-01-24 11:43:09 +10:00

114 lines
3.4 KiB
TypeScript

// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { get } from "svelte/store";
import { getSelection, getRange } from "../lib/cross-browser";
import { surroundNoSplitting, unsurround, findClosest } from "../domlib/surround";
import type { ElementMatcher, ElementClearer } from "../domlib/surround";
import type { RichTextInputAPI } from "./rich-text-input";
function isSurroundedInner(
range: AbstractRange,
base: HTMLElement,
matcher: ElementMatcher,
): boolean {
return Boolean(
findClosest(range.startContainer, base, matcher) ||
findClosest(range.endContainer, base, matcher),
);
}
function surroundAndSelect(
matches: boolean,
range: Range,
selection: Selection,
surroundElement: Element,
base: HTMLElement,
matcher: ElementMatcher,
clearer: ElementClearer,
): void {
const { surroundedRange } = matches
? unsurround(range, surroundElement, base, matcher, clearer)
: surroundNoSplitting(range, surroundElement, base, matcher, clearer);
selection.removeAllRanges();
selection.addRange(surroundedRange);
}
export interface GetSurrounderResult {
surroundCommand(
surroundElement: Element,
matcher: ElementMatcher,
clearer?: ElementClearer,
): Promise<void>;
isSurrounded(matcher: ElementMatcher): Promise<boolean>;
}
export function getSurrounder(richTextInput: RichTextInputAPI): GetSurrounderResult {
const { add, remove, active } = richTextInput.getTriggerOnNextInsert();
async function isSurrounded(matcher: ElementMatcher): Promise<boolean> {
const base = await richTextInput.element;
const selection = getSelection(base)!;
const range = getRange(selection);
if (!range) {
return false;
}
const isSurrounded = isSurroundedInner(range, base, matcher);
return get(active) ? !isSurrounded : isSurrounded;
}
async function surroundCommand(
surroundElement: Element,
matcher: ElementMatcher,
clearer: ElementClearer = () => false,
): Promise<void> {
const base = await richTextInput.element;
const selection = getSelection(base)!;
const range = getRange(selection);
if (!range) {
return;
} else if (range.collapsed) {
if (get(active)) {
remove();
} else {
add(async ({ node }: { node: Node }) => {
range.selectNode(node);
const matches = Boolean(findClosest(node, base, matcher));
surroundAndSelect(
matches,
range,
selection,
surroundElement,
base,
matcher,
clearer,
);
selection.collapseToEnd();
});
}
} else {
const matches = isSurroundedInner(range, base, matcher);
surroundAndSelect(
matches,
range,
selection,
surroundElement,
base,
matcher,
clearer,
);
}
}
return {
surroundCommand,
isSurrounded,
};
}