1 import {$getSelection, COMMAND_PRIORITY_HIGH, FORMAT_TEXT_COMMAND, KEY_ENTER_COMMAND, LexicalEditor} from "lexical";
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";
16 function headerHandler(editor: LexicalEditor, tag: HeadingTagType): boolean {
17 toggleSelectionAsHeading(editor, tag);
21 function wrapFormatAction(formatAction: (editor: LexicalEditor) => any): ShortcutAction {
22 return (editor: LexicalEditor) => {
28 function toggleInlineCode(editor: LexicalEditor): boolean {
29 editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'code');
33 type ShortcutAction = (editor: LexicalEditor, context: EditorUiContext) => boolean;
36 * List of action functions by their shortcut combo.
37 * We use "meta" as an abstraction for ctrl/cmd depending on platform.
39 const actionsByKeys: Record<string, ShortcutAction> = {
41 window.$events.emit('editor-save-draft');
45 window.$events.emit('editor-save-page');
48 'meta+1': (editor) => headerHandler(editor, 'h1'),
49 'meta+2': (editor) => headerHandler(editor, 'h2'),
50 'meta+3': (editor) => headerHandler(editor, 'h3'),
51 'meta+4': (editor) => headerHandler(editor, 'h4'),
52 'meta+5': wrapFormatAction(toggleSelectionAsParagraph),
53 'meta+d': wrapFormatAction(toggleSelectionAsParagraph),
54 'meta+6': wrapFormatAction(toggleSelectionAsBlockquote),
55 'meta+q': wrapFormatAction(toggleSelectionAsBlockquote),
56 'meta+7': wrapFormatAction(formatCodeBlock),
57 'meta+e': wrapFormatAction(formatCodeBlock),
58 'meta+8': toggleInlineCode,
59 'meta+shift+e': toggleInlineCode,
60 'meta+9': wrapFormatAction(cycleSelectionCalloutFormats),
62 'meta+o': wrapFormatAction((e) => toggleSelectionAsList(e, 'number')),
63 'meta+p': wrapFormatAction((e) => toggleSelectionAsList(e, 'bullet')),
64 'meta+k': (editor, context) => {
65 editor.getEditorState().read(() => {
66 const selectedLink = $getNodeFromSelection($getSelection(), $isLinkNode) as LinkNode | null;
67 $showLinkForm(selectedLink, context);
71 'meta+shift+k': (editor, context) => {
72 showLinkSelector(entity => {
73 insertOrUpdateLink(editor, {
84 function createKeyDownListener(context: EditorUiContext): (e: KeyboardEvent) => void {
85 return (event: KeyboardEvent) => {
86 const combo = keyboardEventToKeyComboString(event);
87 // console.log(`pressed: ${combo}`);
88 if (actionsByKeys[combo]) {
89 const handled = actionsByKeys[combo](context.editor, context);
91 event.stopPropagation();
92 event.preventDefault();
98 function keyboardEventToKeyComboString(event: KeyboardEvent): string {
99 const metaKeyPressed = isMac() ? event.metaKey : event.ctrlKey;
102 metaKeyPressed ? 'meta' : '',
103 event.shiftKey ? 'shift' : '',
107 return parts.filter(Boolean).join('+').toLowerCase();
110 function isMac(): boolean {
111 return window.navigator.userAgent.includes('Mac OS X');
114 function overrideDefaultCommands(editor: LexicalEditor) {
115 // Prevent default ctrl+enter command
116 editor.registerCommand(KEY_ENTER_COMMAND, (event) => {
118 return event?.metaKey || false;
120 return event?.ctrlKey || false;
121 }, COMMAND_PRIORITY_HIGH);
124 export function registerShortcuts(context: EditorUiContext) {
125 const listener = createKeyDownListener(context);
126 overrideDefaultCommands(context.editor);
128 return context.editor.registerRootListener((rootElement: null | HTMLElement, prevRootElement: null | HTMLElement) => {
129 // add the listener to the current root element
130 rootElement?.addEventListener('keydown', listener);
131 // remove the listener from the old root element
132 prevRootElement?.removeEventListener('keydown', listener);