How can I listen to a keystroke combination with lexical.js?
Asked Answered
H

4

6

I'm new to lexical.js and don't fully understand how I can listen to different keystroke combinations. I want to listen to "Ctrl/Cmd+S" and then fire off a callback (to save data). How can I do that?

With a textarea I can do something like:

const onKeyDown = (event) => {
  // "Ctrl" or "Cmd" + "s"
  if ((event.ctrlKey || event.metaKey) && event.which === 83) {
    // save data
  }
}

<textarea onKeyDown={onKeyDown} />

With lexical I've tried to do something like:

const MY_SAVE_COMMAND: LexicalCommand<string> = createCommand('MY_SAVE_COMMAND')

editor.registerCommand<KeyboardEvent>(
  MY_SAVE_COMMAND,
  (event) => {
    console.log('[MY_SAVE_COMMAND] event', event)
    return true
  },
  COMMAND_PRIORITY_HIGH,
)

which does not work. Where do I insert the part where I listen to the keystrokes, or is there a different way of doing this altogether?

Hyades answered 3/2, 2023 at 16:17 Comment(0)
B
6

Custom commands are more like global event types. Lexical provides a lot out of the box you can listen to, but for things like regular event listeners, what you probably want is to attach the event to the rootElement via editor.registerRootListener.

You can use a custom plugin to manage attaching and removing this event listener. In the example below, I don't implement onKeyDown, but you would just like any other normal event handler.

import {useLayoutEffect} from 'react';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';

function CommandSPlugin() {
  const [editor] = useLexicalComposerContext();
  useLayoutEffect(() => {
    const onKeyDown = () => { /* Your handler logic here */ };

    return editor.registerRootListener(
      (
        rootElement: null | HTMLElement,
        prevRootElement: null | HTMLElement,
      ) => {
        if (prevRootElement !== null) {
          prevRootElement.removeEventListener('keydown', onKeyDown);
        }
        if (rootElement !== null) {
          rootElement.addEventListener('keydown', onKeyDown);
        }
      }
    );
  }, [editor]);
}

// Then later...

const initialConfig = { /* ... */ };
function App() {
  return (
    <LexicalComposer initialConfig={initialConfig}>
      <CommandSPlugin />
      <YourEditorHere />
    </LexicalComposer>
  );
}
Brittle answered 3/2, 2023 at 17:25 Comment(0)
H
3

Thank you for the answers they helped me a lot. Below the full solution I ended up using. I created a <GlobalEventsPlugin /> with a SAVE_COMMAND command which can then be consumed by any plugin:

// GlobalEventsPlugin.tsx
import { useLayoutEffect } from 'react'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { LexicalCommand, createCommand } from 'lexical'

export const SAVE_COMMAND: LexicalCommand<KeyboardEvent> = createCommand('SAVE_COMMAND')

const GlobalEventsPlugin = () => {
  const [editor] = useLexicalComposerContext()

  useLayoutEffect(() => {
    const onKeyDown = (event: KeyboardEvent) => {
      // "Ctrl" or "Cmd" + "s"
      if ((event.ctrlKey || event.metaKey) && event.which === 83) {
        editor.dispatchCommand(SAVE_COMMAND, event)
      }
    }

    return editor.registerRootListener((rootElement: HTMLElement | null, prevRootElement: HTMLElement | null) => {
      if (prevRootElement !== null) {
        prevRootElement.removeEventListener('keydown', onKeyDown)
      }
      if (rootElement !== null) {
        rootElement.addEventListener('keydown', onKeyDown)
      }
    })
  }, [editor])

  return null
}

export default GlobalEventsPlugin
// Editor.tsx
const initialConfig = { /* ... */ }
function App() {
  return (
    <LexicalComposer initialConfig={initialConfig}>
      <GlobalEventsPlugin />
      <YourEditorHere />
    </LexicalComposer>
  )
}
// Any other context that has access to `editor`
editor.registerCommand(
  SAVE_COMMAND,
  (event) => {
    // Do something with `event`, e.g. `event.preventDefault() && saveData()`
  },
  COMMAND_PRIORITY_HIGH,
)
Hyades answered 3/2, 2023 at 18:4 Comment(0)
C
0

Custom commands do not have automatic triggers: you must 'dispatch' them.

From the intro in the docs:

Commands are the communication system used to wire everything together in Lexical. Custom commands can be created using createCommand() and dispatched to an editor using editor.dispatchCommand(command, payload). Lexical dispatches commands internally when key presses are triggered and when other important signals occur. Commands can also be handled using editor.registerCommand(handler, priority), and incoming commands are propagated through all handlers by priority until a handler stops the propagation (in a similar way to event propagation in the browser).

The built-in commands are dispatched in the same way. If you look at the LexicalEvents.ts file (referenced frequently in the docs), you can see in the function onKeyDown how their keyboard-triggered commands are implemented:

function onKeyDown(event: KeyboardEvent, editor: LexicalEditor): void {
  ...

  const {keyCode, shiftKey, ctrlKey, metaKey, altKey} = event;

  if (isMoveForward(keyCode, ctrlKey, altKey, metaKey)) {
    dispatchCommand(editor, KEY_ARROW_RIGHT_COMMAND, event);
  } else if (isMoveToEnd(keyCode, ctrlKey, shiftKey, altKey, metaKey)) {
    dispatchCommand(editor, MOVE_TO_END, event);
  } ...

It uses helper functions to check the key and some of the modifier keys to determine if a certain combo was used, and, if so, dispatches the relevant command.

The pattern of including the original event is noted in the docs for dispatchCommand:

The payloads are typed via the createCommand(...) API, but they're usually a DOM event for commands dispatched from an event listener.

Note: if you use Ctrl+S, you'll need to use preventDefault() to prevent the browser trying to download the page.

Consumer answered 3/2, 2023 at 17:7 Comment(1)
I was missing the part where I can actually listen to the events via the registerRootListener. I haven't yet figured out commands but I think that's where I would then fire off the custom command so any other plugin can use it.Hyades
O
0

There're two commands you can use:

  • KEY_DOWN_COMMAND - Triggered on any key down event.
  • MODIFIER_KEY_COMMAND – Triggered on key press if any modifier key (Cmd/Ctrl/Alt) is also pressed, which makes it a perfect match for shortcuts.
Orling answered 11/4, 2023 at 13:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.