anki/ts/editable/content-editable.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

151 lines
4.2 KiB
TypeScript

// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import type { SelectionLocation } from "../domlib/location";
import { restoreSelection, saveSelection } from "../domlib/location";
import { placeCaretAfterContent } from "../domlib/place-caret";
import { bridgeCommand } from "../lib/bridgecommand";
import { on, preventDefault } from "../lib/events";
import { isApplePlatform } from "../lib/platform";
import { registerShortcut } from "../lib/shortcuts";
import type { Callback } from "../lib/typing";
import { HandlerList } from "../sveltelib/handler-list";
/**
* Workaround: If you try to invoke an IME after calling
* `placeCaretAfterContent` on a cE element, the IME will immediately
* end and the input character will be duplicated
*/
function safePlaceCaretAfterContent(editable: HTMLElement): void {
placeCaretAfterContent(editable);
restoreSelection(editable, saveSelection(editable)!);
}
function restoreCaret(element: HTMLElement, location: SelectionLocation | null): void {
if (!location) {
return safePlaceCaretAfterContent(element);
}
try {
restoreSelection(element, location);
} catch {
safePlaceCaretAfterContent(element);
}
}
type SetupFocusHandlerAction = (element: HTMLElement) => { destroy(): void };
export interface FocusHandlerAPI {
/**
* Prevent the automatic caret restoration, that happens upon field focus
*/
flushCaret(): void;
/**
* Executed upon focus event of editable.
*/
focus: HandlerList<{ event: FocusEvent }>;
/**
* Executed upon blur event of editable.
*/
blur: HandlerList<{ event: FocusEvent }>;
}
export function useFocusHandler(): [FocusHandlerAPI, SetupFocusHandlerAction] {
let latestLocation: SelectionLocation | null = null;
let offFocus: Callback | null;
let offPointerDown: Callback | null;
let flush = false;
function flushCaret(): void {
flush = true;
}
const focus = new HandlerList<{ event: FocusEvent }>();
const blur = new HandlerList<{ event: FocusEvent }>();
function prepareFocusHandling(
editable: HTMLElement,
location: SelectionLocation | null = null,
): void {
latestLocation = location;
offFocus?.();
offFocus = on(
editable,
"focus",
(event: FocusEvent): void => {
if (flush) {
flush = false;
} else {
restoreCaret(event.currentTarget as HTMLElement, latestLocation);
}
focus.dispatch({ event });
},
{ once: true },
);
offPointerDown?.();
offPointerDown = on(
editable,
"pointerdown",
() => {
offFocus?.();
offFocus = null;
},
{ once: true },
);
}
/**
* Must execute before DOMMirror.
*/
function onBlur(this: HTMLElement, event: FocusEvent): void {
prepareFocusHandling(this, saveSelection(this));
blur.dispatch({ event });
}
function setupFocusHandler(editable: HTMLElement): { destroy(): void } {
prepareFocusHandling(editable);
const off = on(editable, "blur", onBlur);
return {
destroy() {
off();
offFocus?.();
offPointerDown?.();
},
};
}
return [
{
flushCaret,
focus,
blur,
},
setupFocusHandler,
];
}
if (isApplePlatform()) {
registerShortcut(() => bridgeCommand("paste"), "Control+Shift+V");
}
export function preventBuiltinShortcuts(editable: HTMLElement): void {
for (const keyCombination of ["Control+B", "Control+U", "Control+I"]) {
registerShortcut(preventDefault, keyCombination, { target: editable });
}
}
/** API */
export interface ContentEditableAPI {
/**
* Can be used to turn off the caret restoring functionality of
* the ContentEditable. Can be used when you want to set the caret
* yourself.
*/
focusHandler: FocusHandlerAPI;
}