]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/services/shortcuts.ts
Search: Prevented negated terms filling in UI inputs
[bookstack] / resources / js / wysiwyg / services / shortcuts.ts
1 import {$getSelection, COMMAND_PRIORITY_HIGH, FORMAT_TEXT_COMMAND, KEY_ENTER_COMMAND, LexicalEditor} from "lexical";
2 import {
3     cycleSelectionCalloutFormats,
4     formatCodeBlock, insertOrUpdateLink,
5     toggleSelectionAsBlockquote,
6     toggleSelectionAsHeading, toggleSelectionAsList,
7     toggleSelectionAsParagraph
8 } from "../utils/formats";
9 import {HeadingTagType} from "@lexical/rich-text";
10 import {EditorUiContext} from "../ui/framework/core";
11 import {$getNodeFromSelection} from "../utils/selection";
12 import {$isLinkNode, LinkNode} from "@lexical/link";
13 import {$showLinkForm} from "../ui/defaults/forms/objects";
14 import {showLinkSelector} from "../utils/links";
15
16 function headerHandler(editor: LexicalEditor, tag: HeadingTagType): boolean {
17     toggleSelectionAsHeading(editor, tag);
18     return true;
19 }
20
21 function wrapFormatAction(formatAction: (editor: LexicalEditor) => any): ShortcutAction {
22     return (editor: LexicalEditor) => {
23         formatAction(editor);
24         return true;
25     };
26 }
27
28 function toggleInlineCode(editor: LexicalEditor): boolean {
29     editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'code');
30     return true;
31 }
32
33 type ShortcutAction = (editor: LexicalEditor, context: EditorUiContext) => boolean;
34
35 const actionsByKeys: Record<string, ShortcutAction> = {
36     'ctrl+s': () => {
37         window.$events.emit('editor-save-draft');
38         return true;
39     },
40     'ctrl+enter': () => {
41         window.$events.emit('editor-save-page');
42         return true;
43     },
44     'ctrl+1': (editor) => headerHandler(editor, 'h1'),
45     'ctrl+2': (editor) => headerHandler(editor, 'h2'),
46     'ctrl+3': (editor) => headerHandler(editor, 'h3'),
47     'ctrl+4': (editor) => headerHandler(editor, 'h4'),
48     'ctrl+5': wrapFormatAction(toggleSelectionAsParagraph),
49     'ctrl+d': wrapFormatAction(toggleSelectionAsParagraph),
50     'ctrl+6': wrapFormatAction(toggleSelectionAsBlockquote),
51     'ctrl+q': wrapFormatAction(toggleSelectionAsBlockquote),
52     'ctrl+7': wrapFormatAction(formatCodeBlock),
53     'ctrl+e': wrapFormatAction(formatCodeBlock),
54     'ctrl+8': toggleInlineCode,
55     'ctrl+shift+e': toggleInlineCode,
56     'ctrl+9': wrapFormatAction(cycleSelectionCalloutFormats),
57
58     'ctrl+o': wrapFormatAction((e) => toggleSelectionAsList(e, 'number')),
59     'ctrl+p': wrapFormatAction((e) => toggleSelectionAsList(e, 'bullet')),
60     'ctrl+k': (editor, context) => {
61         editor.getEditorState().read(() => {
62             const selectedLink = $getNodeFromSelection($getSelection(), $isLinkNode) as LinkNode | null;
63             $showLinkForm(selectedLink, context);
64         });
65         return true;
66     },
67     'ctrl+shift+k': (editor, context) => {
68         showLinkSelector(entity => {
69             insertOrUpdateLink(editor, {
70                 text: entity.name,
71                 title: entity.link,
72                 target: '',
73                 url: entity.link,
74             });
75         });
76         return true;
77     },
78 };
79
80 function createKeyDownListener(context: EditorUiContext): (e: KeyboardEvent) => void {
81     return (event: KeyboardEvent) => {
82         // TODO - Mac Cmd support
83         const combo = `${event.ctrlKey ? 'ctrl+' : ''}${event.shiftKey ? 'shift+' : ''}${event.key}`.toLowerCase();
84         // console.log(`pressed: ${combo}`);
85         if (actionsByKeys[combo]) {
86             const handled = actionsByKeys[combo](context.editor, context);
87             if (handled) {
88                 event.stopPropagation();
89                 event.preventDefault();
90             }
91         }
92     };
93 }
94
95 function overrideDefaultCommands(editor: LexicalEditor) {
96     // Prevent default ctrl+enter command
97     editor.registerCommand(KEY_ENTER_COMMAND, (event) => {
98         return event?.ctrlKey ? true : false
99     }, COMMAND_PRIORITY_HIGH);
100 }
101
102 export function registerShortcuts(context: EditorUiContext) {
103     const listener = createKeyDownListener(context);
104     overrideDefaultCommands(context.editor);
105
106     return context.editor.registerRootListener((rootElement: null | HTMLElement, prevRootElement: null | HTMLElement) => {
107         // add the listener to the current root element
108         rootElement?.addEventListener('keydown', listener);
109         // remove the listener from the old root element
110         prevRootElement?.removeEventListener('keydown', listener);
111     });
112 }