anki/ts/sveltelib/input-manager.ts
Henrik Giesel 97b28398ea
Fix some issues with new surround buttons (#1505)
* Add a store to indicate whether input trigger is active

Button state is then indicated by: caretIsInBold XOR boldTriggerActive

* Fix surrounding where normalization is tripped up by empty text nodes

* Add failing test for unsurrounding

* Fix failing test

* prohibitOverlapse does not need to be active, if aboveEnd is null

* Reinsert Italic and Underline button

* Refactor find-adjacent to use sum types

* Simplify return value of normalizeAdjacent
2021-11-24 10:33:14 +10:00

127 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { writable } from "svelte/store";
import type { Writable } from "svelte/store";
import { on } from "../lib/events";
import { nodeIsText } from "../lib/dom";
import { getSelection } from "../lib/cross-browser";
export type OnInsertCallback = ({ node }: { node: Node }) => Promise<void>;
export interface OnNextInsertTrigger {
add: (callback: OnInsertCallback) => void;
remove: () => void;
active: Writable<boolean>;
}
interface InputManager {
manager(element: HTMLElement): { destroy(): void };
getTriggerOnNextInsert(): OnNextInsertTrigger;
}
function getInputManager(): InputManager {
const onInsertText: { callback: OnInsertCallback; remove: () => void }[] = [];
function cancelInsertText(): void {
onInsertText.length = 0;
}
function cancelIfInsertText(event: KeyboardEvent): void {
if (event.key.length !== 1) {
cancelInsertText();
}
}
async function onBeforeInput(event: InputEvent): Promise<void> {
if (event.inputType === "insertText" && onInsertText.length > 0) {
const nbsp = " ";
const textContent = event.data === " " ? nbsp : event.data ?? nbsp;
const node = new Text(textContent);
const selection = getSelection(event.target! as Node)!;
const range = selection.getRangeAt(0);
range.deleteContents();
if (nodeIsText(range.startContainer) && range.startOffset === 0) {
const parent = range.startContainer.parentNode!;
parent.insertBefore(node, range.startContainer);
} else if (
nodeIsText(range.endContainer) &&
range.endOffset === range.endContainer.length
) {
const parent = range.endContainer.parentNode!;
parent.insertBefore(node, range.endContainer.nextSibling!);
} else {
range.insertNode(node);
}
range.selectNode(node);
range.collapse(false);
for (const { callback, remove } of onInsertText) {
await callback({ node });
remove();
}
event.preventDefault();
}
cancelInsertText();
}
function manager(element: HTMLElement): { destroy(): void } {
const removeBeforeInput = on(element, "beforeinput", onBeforeInput);
const removePointerDown = on(element, "pointerdown", cancelInsertText);
const removeBlur = on(element, "blur", cancelInsertText);
const removeKeyDown = on(
element,
"keydown",
cancelIfInsertText as EventListener,
);
return {
destroy() {
removeBeforeInput();
removePointerDown();
removeBlur();
removeKeyDown();
},
};
}
function getTriggerOnNextInsert(): OnNextInsertTrigger {
const active = writable(false);
let index = NaN;
function remove() {
if (!Number.isNaN(index)) {
delete onInsertText[index];
active.set(false);
index = NaN;
}
}
function add(callback: OnInsertCallback): void {
if (Number.isNaN(index)) {
index = onInsertText.push({ callback, remove });
active.set(true);
}
}
return {
add,
remove,
active,
};
}
return {
manager,
getTriggerOnNextInsert,
};
}
export default getInputManager;