anki/ts/sveltelib/input-manager.ts
Hikaru Y df0ad4be4b
Prevent unwanted <div> from being left behind when clearing field (#1565)
* Prevent unwanted <div> from being left behind when clearing field

* Use event.currentTarget instead of 'this'

* Check if event.data is empty for better performance

* Change order of conditions for better performance
2021-12-24 09:12:04 +10:00

148 lines
4.4 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 onInput(event: Event): void {
if (
!(event instanceof InputEvent) ||
!(event.currentTarget instanceof HTMLElement)
) {
return;
}
// prevent unwanted <div> from being left behind when clearing field contents
if (
(event.data === null || event.data === "") &&
event.currentTarget.children.length === 1 &&
event.currentTarget.children.item(0) instanceof HTMLDivElement &&
/^\n?$/.test(event.currentTarget.innerText)
) {
event.currentTarget.innerHTML = "";
}
}
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,
);
const removeInput = on(element, "input", onInput);
return {
destroy() {
removeBeforeInput();
removePointerDown();
removeBlur();
removeKeyDown();
removeInput();
},
};
}
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;