Search code examples
reactjsdraftjs

handleKeyCommand - default fallback function for "space" is not working


Wanted scenario: typing word followed by parses word and automatically replaces if matches list of pre-defined abbrevations.

Implemented how: custom keyBindingFn property where space creates custom command "cc-space". handleKeyCommand event recognizes "cc-space", evalutes last typed word and does function above.

Error: when no match handleKeyCommand returns "not-handled" as recommended in docs to allow "default command", this seems to be ignored as editor never inserts space on let-through while everything else works as expected.

import React from "react"
import { Editor, EditorState, SelectionState, Modifier, getDefaultKeyBinding, KeyBindingUtil } from "draft-js"

const myKeyBindingFn = event => {
  const { hasCommandModifier } = KeyBindingUtil
  if (event.keyCode === 32 && !hasCommandModifier(event)) {
    return "cc-space"
  }
  return getDefaultKeyBinding(event)
}

export default class Note extends React.Component {
  constructor(props) {
    super(props)
    this.state = { editorState: EditorState.createEmpty() }
    this.onChange = this.onChange.bind(this)
    this.handleBeforeInput = this.handleBeforeInput.bind(this)
    this.handleKeyCommand = this.handleKeyCommand.bind(this)
  }

  onChange(editorState) {
    this.setState({ editorState })
  }

  handleBeforeInput(chars, editorState) {
    this.setState({ editorState })
  }

  handleKeyCommand(command, editorState) {
    console.log("command", command)
    if (command === "cc-space") {
      const selectionState = editorState.getSelection()
      const anchorKey = selectionState.getAnchorKey()
      const contentState = editorState.getCurrentContent()
      const currentContentBlock = contentState.getBlockForKey(anchorKey)
      const lastWordEntered = currentContentBlock.getText()

      if (lastWordEntered === "hml") {
        const selectionWord = new SelectionState({
          anchorKey: currentContentBlock.getKey(),
          anchorOffset: 0,
          focusKey: currentContentBlock.getKey(),
          focusOffset: lastWordEntered.length
        })
        const newContentState = Modifier.replaceText(contentState, selectionWord, "heimilislæknir ")
        const nextEditorState = EditorState.push(editorState, newContentState, "insert-characters") // editorState.getLastChangeType()
        this.setState({ editorState: nextEditorState }, this.focus)
        return "handled"
      }
    }
    return "not-handled"
  }

  render() {
    return (
      <Editor
        editorState={this.state.editorState}
        onChange={this.onChange}
        handleBeforeInput={this.handleBeforeInput}
        handleKeyCommand={this.handleKeyCommand}
        keyBindingFn={myKeyBindingFn}
        spellCheck={false}
        autocorrect="off"
      />
    )
  }
}

Solution

  • When you use custom key binding function myKeyBindingFn, you should provide custom logic to this custom event.

    For example, if you remove return "cc-space" from your myKeyBindingFn space will works correctly.

    So, you can define your own logic when user entered space:

    import React from "react";
    import {
      Editor,
      EditorState,
      SelectionState,
      Modifier,
      getDefaultKeyBinding,
      KeyBindingUtil
    } from "draft-js";
    
    const myKeyBindingFn = event => {
      const { hasCommandModifier } = KeyBindingUtil;
    
      if (event.keyCode === 32 && !hasCommandModifier(event)) {
        return "cc-space";
      }
      return getDefaultKeyBinding(event);
    };
    
    export default class Note extends React.Component {
      constructor(props) {
        super(props);
        this.state = { editorState: EditorState.createEmpty() };
        this.onChange = this.onChange.bind(this);
        this.handleBeforeInput = this.handleBeforeInput.bind(this);
        this.handleKeyCommand = this.handleKeyCommand.bind(this);
      }
    
      onChange(editorState) {
        this.setState({ editorState });
      }
    
      handleBeforeInput(chars, editorState) {
        this.setState({ editorState });
      }
    
      handleKeyCommand(command, editorState) {
        console.log("command", command);
        if (command === "cc-space") {
          const selectionState = editorState.getSelection();
          const anchorKey = selectionState.getAnchorKey();
          const contentState = editorState.getCurrentContent();
          const currentContentBlock = contentState.getBlockForKey(anchorKey);
          const lastWordEntered = currentContentBlock.getText();
    
          if (lastWordEntered === "hml") {
            const selectionWord = new SelectionState({
              anchorKey: currentContentBlock.getKey(),
              anchorOffset: 0,
              focusKey: currentContentBlock.getKey(),
              focusOffset: lastWordEntered.length
            });
            const newContentState = Modifier.replaceText(
              contentState,
              selectionWord,
              "heimilislæknir "
            );
            const nextEditorState = EditorState.push(
              editorState,
              newContentState,
              "insert-characters"
            ); // editorState.getLastChangeType()
            this.setState({ editorState: nextEditorState }, this.focus);
            return "handled";
          } else {
            // There are no any matches.
            // We have to implement custom logic to space:
    
            const newContentState = Modifier.insertText(
              contentState,
              selectionState,
              " "
            );
            const nextEditorState = EditorState.push(
              editorState,
              newContentState,
              "insert-characters"
            );
            this.setState({ editorState: nextEditorState });
          }
        }
        return "not-handled";
      }
    
      render() {
        return (
          <Editor
            editorState={this.state.editorState}
            onChange={this.onChange}
            handleBeforeInput={this.handleBeforeInput}
            handleKeyCommand={this.handleKeyCommand}
            keyBindingFn={myKeyBindingFn}
            spellCheck={false}
            autocorrect="off"
          />
        );
      }
    }

    Also you can return cc-space event only if it's necessary instead of implementing custom logic to insert space character:

    import React from "react";
    import {
      Editor,
      EditorState,
      SelectionState,
      Modifier,
      getDefaultKeyBinding,
      KeyBindingUtil
    } from "draft-js";
    
    export default class Note extends React.Component {
      constructor(props) {
        super(props);
        this.state = { editorState: EditorState.createEmpty() };
        this.onChange = this.onChange.bind(this);
        this.handleBeforeInput = this.handleBeforeInput.bind(this);
        this.handleKeyCommand = this.handleKeyCommand.bind(this);
      }
    
      // we move this method to class, becouse we have to get this.state.editorState to recognize the last word
      myKeyBindingFn = event => {
        const { hasCommandModifier } = KeyBindingUtil;
    
        if (event.keyCode === 32 && !hasCommandModifier(event)) {
          const selectionState = this.state.editorState.getSelection();
          const anchorKey = selectionState.getAnchorKey();
          const contentState = this.state.editorState.getCurrentContent();
          const currentContentBlock = contentState.getBlockForKey(anchorKey);
          const lastWordEntered = currentContentBlock.getText();
    
          if (lastWordEntered === "hml") {
            // return cc-space only if it's necessary
            return "cc-space";
          }
        }
        // in any other cases we return defaultKeyBinding
        return getDefaultKeyBinding(event);
      };
    
      onChange(editorState) {
        this.setState({ editorState });
      }
    
      handleBeforeInput(chars, editorState) {
        this.setState({ editorState });
      }
    
      handleKeyCommand(command, editorState) {
        console.log("command", command);
        if (command === "cc-space") {
          const selectionState = editorState.getSelection();
          const anchorKey = selectionState.getAnchorKey();
          const contentState = editorState.getCurrentContent();
          const currentContentBlock = contentState.getBlockForKey(anchorKey);
          const lastWordEntered = currentContentBlock.getText();
    
          if (lastWordEntered === "hml") {
            const selectionWord = new SelectionState({
              anchorKey: currentContentBlock.getKey(),
              anchorOffset: 0,
              focusKey: currentContentBlock.getKey(),
              focusOffset: lastWordEntered.length
            });
            const newContentState = Modifier.replaceText(
              contentState,
              selectionWord,
              "heimilislæknir "
            );
            const nextEditorState = EditorState.push(
              editorState,
              newContentState,
              "insert-characters"
            ); // editorState.getLastChangeType()
            this.setState({ editorState: nextEditorState }, this.focus);
            return "handled";
          }
        }
        return "not-handled";
      }
    
      render() {
        return (
          <Editor
            editorState={this.state.editorState}
            onChange={this.onChange}
            handleBeforeInput={this.handleBeforeInput}
            handleKeyCommand={this.handleKeyCommand}
            keyBindingFn={this.myKeyBindingFn}
            spellCheck={false}
            autocorrect="off"
          />
        );
      }
    }

    P.S. lastWordEntered has the whole text, not an entered word. Maybe, you want to get only the one word — you can get a solution from here https://github.com/facebook/draft-js/issues/506