anki/ts/sveltelib/input-handler.ts
Henrik Giesel 8f8f3bd465
Insert symbols overlay (#2051)
* Add flag for enabling insert symbols feature

* Add symbols overlay directory

* Detect if :xy is inserted into editable

* Allow naive updating of overlay, and special handling of ':'

* First step towards better Virtual Element support

* Update floating to reference range on insert text

* Position SymbolsOverlay always on top or bottom

* Add a data-provider to emulate API

* Show correct suggestions in symbols overlay

* Rename to replacementLength

* Allow replacing via clicking in menu

* Optionally remove inline padding of Popover

* Hide Symbols overlay on blur of content editable

* Add specialKey to inputHandler and generalize how arrow movement is detected

- This way macOS users can use Ctrl-N to mean down, etc.

* Detect special key from within SymbolsOverlay

* Implement full backwards search while typing

* Allow navigating symbol menu and accepting with enter

* Add some entries to data-provider

* Satisfy eslint

* Generate symbolsTable from sources

* Use other github source, allow multiple names

In return, symbol must be unique

* Automatically scroll in symbols dropdown

* Use from npm packages rather than downloading from URL

* Remove console.log

* Remove print

* Add pointerDown event to input-handler

- so that SymbolsOverlay can reset on field click

* Make tab do the same as enter

* Make font a bit smaller but increase relative icon size

* Satisfy type requirement of handlerlist

* Revert changing default size of DropdownItems

* Remove some now unused code for bootstrap dropdowns
2022-09-10 18:46:59 +10:00

131 lines
4.0 KiB
TypeScript

// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { getRange, getSelection } from "../lib/cross-browser";
import { on } from "../lib/events";
import { isArrowDown, isArrowLeft, isArrowRight, isArrowUp } from "../lib/keys";
import { singleCallback } from "../lib/typing";
import { HandlerList } from "./handler-list";
const nbsp = "\xa0";
export type SetupInputHandlerAction = (element: HTMLElement) => { destroy(): void };
export interface InputEventParams {
event: InputEvent;
}
export interface InsertTextParams {
event: InputEvent;
text: Text;
}
type SpecialKeyAction =
| "caretUp"
| "caretDown"
| "caretLeft"
| "caretRight"
| "enter"
| "tab";
export interface SpecialKeyParams {
event: KeyboardEvent;
action: SpecialKeyAction;
}
export interface InputHandlerAPI {
readonly beforeInput: HandlerList<InputEventParams>;
readonly insertText: HandlerList<InsertTextParams>;
readonly pointerDown: HandlerList<{ event: PointerEvent }>;
readonly specialKey: HandlerList<SpecialKeyParams>;
}
/**
* An interface that allows Svelte components to attach event listeners via triggers.
* They will be attached to the component(s) that install the manager.
* Prevents that too many event listeners are attached and allows for some
* coordination between them.
*/
function useInputHandler(): [InputHandlerAPI, SetupInputHandlerAction] {
const beforeInput = new HandlerList<InputEventParams>();
const insertText = new HandlerList<InsertTextParams>();
async function onBeforeInput(this: Element, event: InputEvent): Promise<void> {
const selection = getSelection(this)!;
const range = getRange(selection);
await beforeInput.dispatch({ event });
if (!range || event.inputType !== "insertText" || insertText.length === 0) {
return;
}
event.preventDefault();
const content = !event.data || event.data === " " ? nbsp : event.data;
const text = new Text(content);
range.deleteContents();
range.insertNode(text);
range.selectNode(text);
range.collapse(false);
await insertText.dispatch({ event, text });
range.commonAncestorContainer.normalize();
}
const pointerDown = new HandlerList<{ event: PointerEvent }>();
function clearInsertText(): void {
insertText.clear();
}
function onPointerDown(event: PointerEvent): void {
pointerDown.dispatch({ event });
clearInsertText();
}
const specialKey = new HandlerList<SpecialKeyParams>();
async function onKeyDown(this: Element, event: KeyboardEvent): Promise<void> {
if (isArrowDown(event)) {
specialKey.dispatch({ event, action: "caretDown" });
} else if (isArrowUp(event)) {
specialKey.dispatch({ event, action: "caretUp" });
} else if (isArrowRight(event)) {
specialKey.dispatch({ event, action: "caretRight" });
} else if (isArrowLeft(event)) {
specialKey.dispatch({ event, action: "caretLeft" });
} else if (event.code === "Enter" || event.code === "NumpadEnter") {
specialKey.dispatch({ event, action: "enter" });
} else if (event.code === "Tab") {
specialKey.dispatch({ event, action: "tab" });
}
}
function setupHandler(element: HTMLElement): { destroy(): void } {
const destroy = singleCallback(
on(element, "beforeinput", onBeforeInput),
on(element, "blur", clearInsertText),
on(element, "pointerdown", onPointerDown),
on(element, "keydown", onKeyDown),
on(document, "selectionchange", clearInsertText),
);
return { destroy };
}
return [
{
beforeInput,
insertText,
specialKey,
pointerDown,
},
setupHandler,
];
}
export default useInputHandler;