]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/services/keyboard-handling.ts
5f7f41ef02c12f8bb3a73b601098978d04622fb2
[bookstack] / resources / js / wysiwyg / services / keyboard-handling.ts
1 import {EditorUiContext} from "../ui/framework/core";
2 import {
3     $createParagraphNode,
4     $getSelection,
5     $isDecoratorNode,
6     COMMAND_PRIORITY_LOW,
7     KEY_BACKSPACE_COMMAND,
8     KEY_DELETE_COMMAND,
9     KEY_ENTER_COMMAND, KEY_TAB_COMMAND,
10     LexicalEditor,
11     LexicalNode
12 } from "lexical";
13 import {$isImageNode} from "../nodes/image";
14 import {$isMediaNode} from "../nodes/media";
15 import {getLastSelection} from "../utils/selection";
16 import {$getNearestNodeBlockParent} from "../utils/nodes";
17 import {$setInsetForSelection} from "../utils/lists";
18 import {$isListItemNode} from "@lexical/list";
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 = $createParagraphNode();
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): boolean {
62     const change = event?.shiftKey ? -40 : 40;
63     const selection = $getSelection();
64     const nodes = selection?.getNodes() || [];
65     if (nodes.length > 1 || (nodes.length === 1 && $isListItemNode(nodes[0].getParent()))) {
66         editor.update(() => {
67             $setInsetForSelection(editor, change);
68         });
69         event?.preventDefault();
70         return true;
71     }
72
73     return false;
74 }
75
76 export function registerKeyboardHandling(context: EditorUiContext): () => void {
77     const unregisterBackspace = context.editor.registerCommand(KEY_BACKSPACE_COMMAND, (): boolean => {
78         deleteSingleSelectedNode(context.editor);
79         return false;
80     }, COMMAND_PRIORITY_LOW);
81
82     const unregisterDelete = context.editor.registerCommand(KEY_DELETE_COMMAND, (): boolean => {
83         deleteSingleSelectedNode(context.editor);
84         return false;
85     }, COMMAND_PRIORITY_LOW);
86
87     const unregisterEnter = context.editor.registerCommand(KEY_ENTER_COMMAND, (event): boolean => {
88         return insertAfterSingleSelectedNode(context.editor, event);
89     }, COMMAND_PRIORITY_LOW);
90
91     const unregisterTab = context.editor.registerCommand(KEY_TAB_COMMAND, (event): boolean => {
92         return handleInsetOnTab(context.editor, event);
93     }, COMMAND_PRIORITY_LOW);
94
95     return () => {
96         unregisterBackspace();
97         unregisterDelete();
98         unregisterEnter();
99         unregisterTab();
100     };
101 }