]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/services/keyboard-handling.ts
Lexical: Custom list nesting support
[bookstack] / resources / js / wysiwyg / services / keyboard-handling.ts
1 import {EditorUiContext} from "../ui/framework/core";
2 import {
3     $getSelection,
4     $isDecoratorNode,
5     COMMAND_PRIORITY_LOW,
6     KEY_BACKSPACE_COMMAND,
7     KEY_DELETE_COMMAND,
8     KEY_ENTER_COMMAND, KEY_TAB_COMMAND,
9     LexicalEditor,
10     LexicalNode
11 } from "lexical";
12 import {$isImageNode} from "../nodes/image";
13 import {$isMediaNode} from "../nodes/media";
14 import {getLastSelection} from "../utils/selection";
15 import {$getNearestNodeBlockParent} from "../utils/nodes";
16 import {$createCustomParagraphNode} from "../nodes/custom-paragraph";
17 import {$isCustomListItemNode} from "../nodes/custom-list-item";
18 import {$setInsetForSelection} from "../utils/lists";
19
20 function isSingleSelectedNode(nodes: LexicalNode[]): boolean {
21     if (nodes.length === 1) {
22         const node = nodes[0];
23         if ($isDecoratorNode(node) || $isImageNode(node) || $isMediaNode(node)) {
24             return true;
25         }
26     }
27
28     return false;
29 }
30
31 function deleteSingleSelectedNode(editor: LexicalEditor) {
32     const selectionNodes = getLastSelection(editor)?.getNodes() || [];
33     if (isSingleSelectedNode(selectionNodes)) {
34         editor.update(() => {
35             selectionNodes[0].remove();
36         });
37     }
38 }
39
40 function insertAfterSingleSelectedNode(editor: LexicalEditor, event: KeyboardEvent|null): boolean {
41     const selectionNodes = getLastSelection(editor)?.getNodes() || [];
42     if (isSingleSelectedNode(selectionNodes)) {
43         const node = selectionNodes[0];
44         const nearestBlock = $getNearestNodeBlockParent(node) || node;
45         if (nearestBlock) {
46             requestAnimationFrame(() => {
47                 editor.update(() => {
48                     const newParagraph = $createCustomParagraphNode();
49                     nearestBlock.insertAfter(newParagraph);
50                     newParagraph.select();
51                 });
52             });
53             event?.preventDefault();
54             return true;
55         }
56     }
57
58     return false;
59 }
60
61 function handleInsetOnTab(editor: LexicalEditor, event: KeyboardEvent|null) {
62     const change = event?.shiftKey ? -40 : 40;
63     editor.update(() => {
64         const selection = $getSelection();
65         const nodes = selection?.getNodes() || [];
66         if (nodes.length > 1 || (nodes.length === 1 && $isCustomListItemNode(nodes[0].getParent()))) {
67             $setInsetForSelection(editor, change);
68         }
69     });
70 }
71
72 export function registerKeyboardHandling(context: EditorUiContext): () => void {
73     const unregisterBackspace = context.editor.registerCommand(KEY_BACKSPACE_COMMAND, (): boolean => {
74         deleteSingleSelectedNode(context.editor);
75         return false;
76     }, COMMAND_PRIORITY_LOW);
77
78     const unregisterDelete = context.editor.registerCommand(KEY_DELETE_COMMAND, (): boolean => {
79         deleteSingleSelectedNode(context.editor);
80         return false;
81     }, COMMAND_PRIORITY_LOW);
82
83     const unregisterEnter = context.editor.registerCommand(KEY_ENTER_COMMAND, (event): boolean => {
84         return insertAfterSingleSelectedNode(context.editor, event);
85     }, COMMAND_PRIORITY_LOW);
86
87     const unregisterTab = context.editor.registerCommand(KEY_TAB_COMMAND, (event): boolean => {
88         return handleInsetOnTab(context.editor, event);
89     }, COMMAND_PRIORITY_LOW);
90
91     return () => {
92         unregisterBackspace();
93         unregisterDelete();
94         unregisterEnter();
95         unregisterTab();
96     };
97 }