Home About Contact
lexical , React , Note Taking

簡単なエディタをつくる試み lexical を調べる(その7)

今回は emacs keybindings などを実現するための下準備として キーボードショートカットへの対応方法を調べます。

たとえば、 Ctrl + S したら検索(または保存処理)を実行、などという 機能を実現するために、そのキーイベントに反応させるには どうしたらいいかを調べます。

参考になった情報

そして https://github.com/facebook/lexical からソースをダウンロードして次のコードを読む。

Commands

これ(https://lexical.dev/docs/concepts/commands)を理解すべき。

コマンドを作成して( createCommand )、 editor.dispatchCommand() したり、 dispatchCommand されたコマンドを editor.registerCommand() に登録した関数で受け取るしくみが lexical にはあらかじめ備わっている。

LexicalEvents.ts の onKeyDown

このコード。

function onKeyDown(event: KeyboardEvent, editor: LexicalEditor): void {
  lastKeyDownTimeStamp = event.timeStamp;
  lastKeyCode = event.keyCode;
  if (editor.isComposing()) {
    return;
  }

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

  if (dispatchCommand(editor, KEY_DOWN_COMMAND, event)) {
    return;
  }

  // 以下、デフォルト(というかビルトイン済み)のショートカットの処理が記述されている。

onKeyDown 関数の先頭部分 if (dispatchCommand(editor, KEY_DOWN_COMMAND, event)) { return ; } で、 KEY_DOWN_COMMANDdispatchCommand() されて、 もし dispatchCommnd() の戻り値が true ならば、 onKeyDown 関数はそこで終了する。

したがって キーイベントが起きたときに、何かする処理を入れたい場合、 この KEY_DOWN_COMMAND に反応する処理を editor.registerCommand() で 登録しておけばよいことになる。

MyKeyEventsPlugin

それでは、この KEY_DOWN_COMMAND が dispatch されてきたら、そのイベントを受け取り、処理を追加するプラグイン MyKeyEventsPlugin を書きます。

src/Editor.js

import { KEY_DOWN_COMMAND } from 'lexical';

...

function MyKeyEventsPlugin() {
  const [editor] = useLexicalComposerContext();

  useEffect(() => {
    return editor.registerCommand(
      KEY_DOWN_COMMAND,
      (keyboardEvent)=>{
        console.log(keyboardEvent);
        keyboardEvent.preventDefault();
        return true;
      },
      COMMAND_PRIORITY_EDITOR,
    );
  }, [editor]);
}

...

  return (
    <LexicalComposer initialConfig={initialConfig}>
      <PlainTextPlugin ...  />
      <MyKeyEventsPlugin />
      ...
    </LexicalComposer>
  );

MyKeyEventsPlugin を作成して、 editor.registerCommand() を記述します。

KEY_DOWN_COMMAND を受け取るように指定した上で、処理内容を次のように記述しています。

      (keyboardEvent)=>{
        console.log(keyboardEvent);
        keyboardEvent.preventDefault();
        return true;
      },

preventDefault() してデフォルトの作動を抑止した上で true を返しているだけです。 これで、 Editor で何を入力(どんなキー入力)しても、無反応になります。 これで lexical のエディタに対するキー入力を乗っ取ることができました。

もちろん、エディタで何もキーが受け付けされなかったら無意味なので、 ここで本当に実装したいことは、 自分が対処したいキー入力に対してのみ処理を追加して true を返す、 それ以外は全部 false を返すことです。

たとえば、Ctrl + S だけ乗っ取りたければ、次のように実装します。

function MyKeyEventsPlugin() {
  const [editor] = useLexicalComposerContext();
  useEffect(() => {
    return editor.registerCommand(
      KEY_DOWN_COMMAND,
      (keyboardEvent)=>{
        console.log(keyboardEvent);
        if( keyboardEvent.ctrlKey && keyboardEvent.keyCode === 83 ){
          // Ctrl + S:
          keyboardEvent.preventDefault();
          // TODO: do something.
          return true;
        } else {
          return false;
        }
      },
      COMMAND_PRIORITY_EDITOR,
    );
  }, [editor]);
}

keyboardEvent.keyCode でキー入力を取得していますが、keyboardEvent.which でとるべきとかその辺は本題からはずれるので、省略。 ここでは keyboardEvent.keyCode だけで処理しています。

lexical 神か。 ただし、これは数時間調べて把握したことなので、何か勘違いなどがあるかもしれない。

GlobalEventsPlugin (悪手)

これでも一部のキーイベントをとることはできたのだが、 Ctrl + A など あらかじめ lexical のエディタにビルトインされているショートカットキーを乗っ取ることはできない。

何かの役に立つかも知れないのでメモしておきます。

src/Editor.js

function GlobalEventsPlugin() {
  const [editor] = useLexicalComposerContext();
  useEffect(() => {
    const onKeyDown = (keyboardEvent)=> {
      console.log(keyboardEvent.keyCode);
      if( keyboardEvent.ctrlKey && keyboardEvent.which === 83 ){
        // Ctrl + S:
        keyboardEvent.preventDefault();
        // TODO: do something.
      }
    };

    const eventType = "keydown";

    return editor.registerRootListener((rootElement, prevRootElement) => {
      if (rootElement) {
        rootElement.addEventListener(eventType, onKeyDown);
      }
      if (prevRootElement) {
        prevRootElement.removeEventListener(eventType, onKeyDown);
      }
    });
  }, [editor]);
}

この GlobalEventsPlugin をセットすれば Ctrl+S については意図通作動した。

以上です。