BookStack/resources/js/wysiwyg/services/keyboard-handling.ts
Dan Brown 662110c269
Lexical: Custom list nesting support
Added list nesting support to allow li > ul style nesting which lexical
didn't do by default.
Adds tab handling for inset/outset controls.
Will be a range of edge-case bugs to squash during testing.
2024-09-13 15:50:42 +01:00

97 lines
3.3 KiB
TypeScript

import {EditorUiContext} from "../ui/framework/core";
import {
$getSelection,
$isDecoratorNode,
COMMAND_PRIORITY_LOW,
KEY_BACKSPACE_COMMAND,
KEY_DELETE_COMMAND,
KEY_ENTER_COMMAND, KEY_TAB_COMMAND,
LexicalEditor,
LexicalNode
} from "lexical";
import {$isImageNode} from "../nodes/image";
import {$isMediaNode} from "../nodes/media";
import {getLastSelection} from "../utils/selection";
import {$getNearestNodeBlockParent} from "../utils/nodes";
import {$createCustomParagraphNode} from "../nodes/custom-paragraph";
import {$isCustomListItemNode} from "../nodes/custom-list-item";
import {$setInsetForSelection} from "../utils/lists";
function isSingleSelectedNode(nodes: LexicalNode[]): boolean {
if (nodes.length === 1) {
const node = nodes[0];
if ($isDecoratorNode(node) || $isImageNode(node) || $isMediaNode(node)) {
return true;
}
}
return false;
}
function deleteSingleSelectedNode(editor: LexicalEditor) {
const selectionNodes = getLastSelection(editor)?.getNodes() || [];
if (isSingleSelectedNode(selectionNodes)) {
editor.update(() => {
selectionNodes[0].remove();
});
}
}
function insertAfterSingleSelectedNode(editor: LexicalEditor, event: KeyboardEvent|null): boolean {
const selectionNodes = getLastSelection(editor)?.getNodes() || [];
if (isSingleSelectedNode(selectionNodes)) {
const node = selectionNodes[0];
const nearestBlock = $getNearestNodeBlockParent(node) || node;
if (nearestBlock) {
requestAnimationFrame(() => {
editor.update(() => {
const newParagraph = $createCustomParagraphNode();
nearestBlock.insertAfter(newParagraph);
newParagraph.select();
});
});
event?.preventDefault();
return true;
}
}
return false;
}
function handleInsetOnTab(editor: LexicalEditor, event: KeyboardEvent|null) {
const change = event?.shiftKey ? -40 : 40;
editor.update(() => {
const selection = $getSelection();
const nodes = selection?.getNodes() || [];
if (nodes.length > 1 || (nodes.length === 1 && $isCustomListItemNode(nodes[0].getParent()))) {
$setInsetForSelection(editor, change);
}
});
}
export function registerKeyboardHandling(context: EditorUiContext): () => void {
const unregisterBackspace = context.editor.registerCommand(KEY_BACKSPACE_COMMAND, (): boolean => {
deleteSingleSelectedNode(context.editor);
return false;
}, COMMAND_PRIORITY_LOW);
const unregisterDelete = context.editor.registerCommand(KEY_DELETE_COMMAND, (): boolean => {
deleteSingleSelectedNode(context.editor);
return false;
}, COMMAND_PRIORITY_LOW);
const unregisterEnter = context.editor.registerCommand(KEY_ENTER_COMMAND, (event): boolean => {
return insertAfterSingleSelectedNode(context.editor, event);
}, COMMAND_PRIORITY_LOW);
const unregisterTab = context.editor.registerCommand(KEY_TAB_COMMAND, (event): boolean => {
return handleInsetOnTab(context.editor, event);
}, COMMAND_PRIORITY_LOW);
return () => {
unregisterBackspace();
unregisterDelete();
unregisterEnter();
unregisterTab();
};
}