anki/ts/editable/content-editable.ts
2022-03-29 09:28:44 +10:00

143 lines
3.9 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 }>;
}
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 }>();
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): void {
prepareFocusHandling(this, saveSelection(this));
}
function setupFocusHandler(editable: HTMLElement): { destroy(): void } {
prepareFocusHandling(editable);
const off = on(editable, "blur", onBlur);
return {
destroy() {
off();
offFocus?.();
offPointerDown?.();
},
};
}
return [
{
flushCaret,
focus,
},
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;
}