]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/services/shortcuts.ts
Lexical: Started adding editor shortcuts
[bookstack] / resources / js / wysiwyg / services / shortcuts.ts
1 import {COMMAND_PRIORITY_HIGH, FORMAT_TEXT_COMMAND, KEY_ENTER_COMMAND, LexicalEditor} from "lexical";
2 import {
3     cycleSelectionCalloutFormats,
4     formatCodeBlock,
5     toggleSelectionAsBlockquote,
6     toggleSelectionAsHeading,
7     toggleSelectionAsParagraph
8 } from "../utils/formats";
9 import {HeadingTagType} from "@lexical/rich-text";
10
11 function headerHandler(editor: LexicalEditor, tag: HeadingTagType): boolean {
12     toggleSelectionAsHeading(editor, tag);
13     return true;
14 }
15
16 function wrapFormatAction(formatAction: (editor: LexicalEditor) => any): ShortcutAction {
17     return (editor: LexicalEditor) => {
18         formatAction(editor);
19         return true;
20     };
21 }
22
23 function toggleInlineCode(editor: LexicalEditor): boolean {
24     editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'code');
25     return true;
26 }
27
28 type ShortcutAction = (editor: LexicalEditor) => boolean;
29
30 const actionsByKeys: Record<string, ShortcutAction> = {
31     // Save draft
32     'ctrl+s': () => {
33         window.$events.emit('editor-save-draft');
34         return true;
35     },
36     'ctrl+enter': () => {
37         window.$events.emit('editor-save-page');
38         return true;
39     },
40     'ctrl+1': (editor) => headerHandler(editor, 'h1'),
41     'ctrl+2': (editor) => headerHandler(editor, 'h2'),
42     'ctrl+3': (editor) => headerHandler(editor, 'h3'),
43     'ctrl+4': (editor) => headerHandler(editor, 'h4'),
44     'ctrl+5': wrapFormatAction(toggleSelectionAsParagraph),
45     'ctrl+d': wrapFormatAction(toggleSelectionAsParagraph),
46     'ctrl+6': wrapFormatAction(toggleSelectionAsBlockquote),
47     'ctrl+q': wrapFormatAction(toggleSelectionAsBlockquote),
48     'ctrl+7': wrapFormatAction(formatCodeBlock),
49     'ctrl+e': wrapFormatAction(formatCodeBlock),
50     'ctrl+8': toggleInlineCode,
51     'ctrl+shift+e': toggleInlineCode,
52     'ctrl+9': wrapFormatAction(cycleSelectionCalloutFormats),
53
54     // TODO Lists
55     // TODO Links
56     // TODO Link selector
57 };
58
59 function createKeyDownListener(editor: LexicalEditor): (e: KeyboardEvent) => void {
60     return (event: KeyboardEvent) => {
61         // TODO - Mac Cmd support
62         const combo = `${event.ctrlKey ? 'ctrl+' : ''}${event.shiftKey ? 'shift+' : ''}${event.key}`.toLowerCase();
63         console.log(`pressed: ${combo}`);
64         if (actionsByKeys[combo]) {
65             const handled = actionsByKeys[combo](editor);
66             if (handled) {
67                 event.stopPropagation();
68                 event.preventDefault();
69             }
70         }
71     };
72 }
73
74 function overrideDefaultCommands(editor: LexicalEditor) {
75     // Prevent default ctrl+enter command
76     editor.registerCommand(KEY_ENTER_COMMAND, (event) => {
77         return event?.ctrlKey ? true : false
78     }, COMMAND_PRIORITY_HIGH);
79 }
80
81 export function registerShortcuts(editor: LexicalEditor) {
82     const listener = createKeyDownListener(editor);
83     overrideDefaultCommands(editor);
84
85     return editor.registerRootListener((rootElement: null | HTMLElement, prevRootElement: null | HTMLElement) => {
86         // add the listener to the current root element
87         rootElement?.addEventListener('keydown', listener);
88         // remove the listener from the old root element
89         prevRootElement?.removeEventListener('keydown', listener);
90     });
91 }