kenju's blog

About Programming, Mathematics, Security and Blockchain

Draft.jsにおけるKeyboard Shortcutの実装メモ

Draft.jsでKeyboard Shortcutを実装するには、Key Bindings関連のAPIを扱うことになる。

draftjs.org

やりたいことに対して結構複雑なロジックを書かないといけない印象。

仕様

以下のそれぞれの場合にある特定のイベントを発火:

  • command + s
  • command + enter

commandキーとかいたが、OSごとに修飾キーは異なる。 今回は、以下のように実装した:

  • macOSの場合、修飾キーはのみ(CTRL/SHIFT/OPTIONを省く)
  • macOS以外の場合、修飾キーはCTRLのみ(SHIFT/OPTIONを省く)

ポイント

  • React.jsでKeyboard Eventを扱う際は SyntheticEvent - React を参考に
  • 複数の修飾キーが押された場合の条件を省く点に注意

実装

import React from 'react';
import { getDefaultKeyBinding, KeyBindingUtil } from 'draft-js';
import KeyCode from 'keycode-js';
import UserAgent from 'fbjs/lib/UserAgent';

// fbjsが内部で利用している、UA判別メソッド
const IS_OSX = UserAgent.isPlatform('Mac OS X');

const EVENT_PUBLISH_DRAFT = "publishDraft";
const EVENT_SAVE_AS_DRAFT = "saveAsDraft";

const EVENT_SET = new Set([
  EVENT_PUBLISH_DRAFT,
  EVENT_SAVE_AS_DRAFT
]);

class EditorCore extends React.Component {
  ...

  // 「⌘」キーが押されたかどうかを判別する(つまりmacOSの場合のみ)
  isCommandKeyModifier(keyboardEvent) {
    return IS_OSX && !!keyboardEvent.metaKey && !keyboardEvent.altKey;
  }

  // 「Enter」キーがcommandキーと同時に押されたかどうかを判定する
  // 複数の修飾キーが同時に押された場合を除外するため、条件式が複雑になっている
  // 例:「command + enter」にだけ反応するが「command + shift + enter」は除外したい、など
  isPublishDraftKeyEvent(keyboardEvent) {
    return (keyboardEvent.keyCode === KeyCode.KEY_RETURN || keyboardEvent.keyCode === KeyCode.KEY_ENTER)
    && !keyboardEvent.shiftKey
    && !KeyBindingUtil.isOptionKeyCommand(keyboardEvent)
    && (IS_OSX ?
      !KeyBindingUtil.isCtrlKeyCommand(keyboardEvent) && this.isCommandKeyModifier(keyboardEvent)
      :KeyBindingUtil.isCtrlKeyCommand(keyboardEvent));
  }

  // 「S」キーがcommandキーと同時に押されたかどうかを判定する
  isSaveAsDraftKeyEvent(keyboardEvent) {
    return keyboardEvent.keyCode === KeyCode.KEY_S
      && !keyboardEvent.shiftKey
      && !KeyBindingUtil.isOptionKeyCommand(keyboardEvent)
      && (IS_OSX ?
        !KeyBindingUtil.isCtrlKeyCommand(keyboardEvent) && this.isCommandKeyModifier(keyboardEvent)
        :KeyBindingUtil.isCtrlKeyCommand(keyboardEvent));
  }

  // `handleKeyCommand`が呼ばれたときに実行するメソッド、特定のイベントをマッピングする
  callKeyBindingFn(keyboardEvent) {
    // カスタマイズしたKeyboard Eventだった場合は、用意した特定のイベントを返す
    if (this.isPublishDraftKeyEvent(keyboardEvent)) {
      return EVENT_PUBLISH_DRAFT;
    }
    if (this.isSaveAsDraftKeyEvent(keyboardEvent)) {
      return EVENT_SAVE_AS_DRAFT;
    }
    // デフォルトのKeyboard Eventのマッピングを返す
    return getDefaultKeyBinding(keyboardEvent);
  }

  // カスタマイズした特定のイベントをキャッチした場合は、何かしらの処理をする
  keyCommandHandler(command) {
    if (EVENT_SET.has(command)) {
      // 今回は親Componentがイベント管理をしているので、dispatchしている
      return this.props.dispatch(command);
    }
  }

  render() {
    return (
          <DraftJsEditor 
              ...
              handleKeyCommand={this.keyCommandHandler.bind(this)}
              keyBindingFn={this.callKeyBindingFn.bind(this)} />
    );
  }
}

export default Timeouts(EditorCore);