anki/ts/editable/content-editable.ts

184 lines
5.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 { getSelection } from "../lib/cross-browser";
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 });
}
}
declare global {
interface Selection {
modify(s: string, t: string, u: string): void;
}
}
// Fix inverted Ctrl+right/left handling in RTL fields
export function fixRTLKeyboardNav(editable: HTMLElement): void {
editable.addEventListener("keydown", (evt: KeyboardEvent) => {
if (window.getComputedStyle(editable).direction === "rtl") {
const selection = getSelection(editable)!;
let granularity = "character";
let alter = "move";
if (evt.ctrlKey) {
granularity = "word";
}
if (evt.shiftKey) {
alter = "extend";
}
if (evt.code === "ArrowRight") {
selection.modify(alter, "right", granularity);
evt.preventDefault();
return;
} else if (evt.code === "ArrowLeft") {
selection.modify(alter, "left", granularity);
evt.preventDefault();
return;
}
}
});
}
/** 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;
}