resources/js/wysiwyg/services/shortcuts.ts
import {$getSelection, COMMAND_PRIORITY_HIGH, FORMAT_TEXT_COMMAND, KEY_ENTER_COMMAND, LexicalEditor} from "lexical";
import {
cycleSelectionCalloutFormats,
formatCodeBlock, insertOrUpdateLink,
toggleSelectionAsBlockquote,
toggleSelectionAsHeading, toggleSelectionAsList,
toggleSelectionAsParagraph
} from "../utils/formats";
import {HeadingTagType} from "@lexical/rich-text";
import {EditorUiContext} from "../ui/framework/core";
import {$getNodeFromSelection} from "../utils/selection";
import {$isLinkNode, LinkNode} from "@lexical/link";
import {$showLinkForm} from "../ui/defaults/forms/objects";
import {showLinkSelector} from "../utils/links";
function headerHandler(editor: LexicalEditor, tag: HeadingTagType): boolean {
toggleSelectionAsHeading(editor, tag);
return true;
}
function wrapFormatAction(formatAction: (editor: LexicalEditor) => any): ShortcutAction {
return (editor: LexicalEditor) => {
formatAction(editor);
return true;
};
}
function toggleInlineCode(editor: LexicalEditor): boolean {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'code');
return true;
}
type ShortcutAction = (editor: LexicalEditor, context: EditorUiContext) => boolean;
/**
* List of action functions by their shortcut combo.
* We use "meta" as an abstraction for ctrl/cmd depending on platform.
*/
const actionsByKeys: Record<string, ShortcutAction> = {
'meta+s': () => {
window.$events.emit('editor-save-draft');
return true;
},
'meta+enter': () => {
window.$events.emit('editor-save-page');
return true;
},
'meta+1': (editor) => headerHandler(editor, 'h1'),
'meta+2': (editor) => headerHandler(editor, 'h2'),
'meta+3': (editor) => headerHandler(editor, 'h3'),
'meta+4': (editor) => headerHandler(editor, 'h4'),
'meta+5': wrapFormatAction(toggleSelectionAsParagraph),
'meta+d': wrapFormatAction(toggleSelectionAsParagraph),
'meta+6': wrapFormatAction(toggleSelectionAsBlockquote),
'meta+q': wrapFormatAction(toggleSelectionAsBlockquote),
'meta+7': wrapFormatAction(formatCodeBlock),
'meta+e': wrapFormatAction(formatCodeBlock),
'meta+8': toggleInlineCode,
'meta+shift+e': toggleInlineCode,
'meta+9': wrapFormatAction(cycleSelectionCalloutFormats),
'meta+o': wrapFormatAction((e) => toggleSelectionAsList(e, 'number')),
'meta+p': wrapFormatAction((e) => toggleSelectionAsList(e, 'bullet')),
'meta+k': (editor, context) => {
editor.getEditorState().read(() => {
const selectedLink = $getNodeFromSelection($getSelection(), $isLinkNode) as LinkNode | null;
$showLinkForm(selectedLink, context);
});
return true;
},
'meta+shift+k': (editor, context) => {
showLinkSelector(entity => {
insertOrUpdateLink(editor, {
text: entity.name,
title: entity.link,
target: '',
url: entity.link,
});
});
return true;
},
};
function createKeyDownListener(context: EditorUiContext): (e: KeyboardEvent) => void {
return (event: KeyboardEvent) => {
const combo = keyboardEventToKeyComboString(event);
// console.log(`pressed: ${combo}`);
if (actionsByKeys[combo]) {
const handled = actionsByKeys[combo](context.editor, context);
if (handled) {
event.stopPropagation();
event.preventDefault();
}
}
};
}
function keyboardEventToKeyComboString(event: KeyboardEvent): string {
const metaKeyPressed = isMac() ? event.metaKey : event.ctrlKey;
const parts = [
metaKeyPressed ? 'meta' : '',
event.shiftKey ? 'shift' : '',
event.key,
];
return parts.filter(Boolean).join('+').toLowerCase();
}
function isMac(): boolean {
return window.navigator.userAgent.includes('Mac OS X');
}
function overrideDefaultCommands(editor: LexicalEditor) {
// Prevent default ctrl+enter command
editor.registerCommand(KEY_ENTER_COMMAND, (event) => {
if (isMac()) {
return event?.metaKey || false;
}
return event?.ctrlKey || false;
}, COMMAND_PRIORITY_HIGH);
}
export function registerShortcuts(context: EditorUiContext) {
const listener = createKeyDownListener(context);
overrideDefaultCommands(context.editor);
return context.editor.registerRootListener((rootElement: null | HTMLElement, prevRootElement: null | HTMLElement) => {
// add the listener to the current root element
rootElement?.addEventListener('keydown', listener);
// remove the listener from the old root element
prevRootElement?.removeEventListener('keydown', listener);
});
}