97b28398ea
* 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
127 lines
3.7 KiB
TypeScript
127 lines
3.7 KiB
TypeScript
// 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;
|