Draft-js: How to stop DraftJS cursor jumping to beginning of text?

Created on 9 May 2017  路  27Comments  路  Source: facebook/draft-js

Code involved using DraftJS and Meteor Js application
Task - Make a live preview where text from DraftJS will get saved to DB and from DB it get displayed on another component.

But problem is once data comes from DB and I try to edit DraftJS cursor moved to the beginning.

Code is

import {Editor, EditorState, ContentState} from 'draft-js';
import React, { Component } from 'react';
import { TestDB } from '../api/yaml-component.js';
import { createContainer } from 'meteor/react-meteor-data';
import PropTypes from 'prop-types';

class EditorComponent extends Component {
  constructor(props) {
    super(props);
    this.state = {
        editorState : EditorState.createEmpty(),
    };
  }

  componentWillReceiveProps(nextProps) {
    console.log('Receiving Props');
    if (!nextProps) return;
    console.log(nextProps);
    let j = nextProps.testDB[0];
    let c = ContentState.createFromText(j.text);
    this.setState({
      editorState: EditorState.createWithContent(c),
    })
  }

  insertToDB(finalComponentStructure) {
    if (!finalComponentStructure) return;
    finalComponentStructure.author = 'Sandeep3005';
    Meteor.call('testDB.insert', finalComponentStructure);
  }


  _handleChange(editorState) {
    console.log('Inside handle change');
    let contentState = editorState.getCurrentContent();
    this.insertToDB({text: contentState.getPlainText()});
    this.setState({editorState});
  }

  render() {
    return (
      <div>
        <Editor
          placeholder="Insert YAML Here"
          editorState={this.state.editorState}
          onChange={this._handleChange.bind(this)}
        />
      </div>
    );
  }
}


    EditorComponent.propTypes = {
     staff: PropTypes.array.isRequired,
    };

    export default createContainer(() => {
      return {
        staff: Staff.find({}).fetch(),
      };
    }, EditorComponent);

Any helpful comment in right direction will be useful

Also find out that issue occurs if we right below code in handleChange method

let { editorState : olderState} = this.state; if(olderState == editorState ) return; this.setState({editorState
Which versions of Draft.js, and which browser / OS are affected by this issue? Did this work in previous versions of Draft.js?
Using DraftJs v 0.10

Most helpful comment

  const newState = EditorState.createEmpty()
  this.setState({
          editorState: EditorState.moveFocusToEnd(newState)
   })

This worked for me...

All 27 comments

I have the same problem. I load a raw editor state into a draft js Editor component, then click the mouse anywhere in the Editor. I see a blinking cursor at the point where I clicked, but when I start typing the cursor always jumps to the beginning of the content in the Editor. Very frustrating. In a long document users are definitely going to want to use the mouse to quickly move to places they want to edit. Being stuck using only the keyboard and always from the beginning of the content is not an acceptable workaround.

@SevenZark @Sandeep3005 view my solution: https://github.com/facebook/draft-js/issues/989#issuecomment-332522174

Anyone knows how to solve this? THe cursor jumps before last letter, when beginning to enter text

Still unsure exactly why this happened, or why my fix worked, but I managed to get around this with a few steps.

  1. Storing the text of the editor in the state of my component
  2. Checking that text has changed in componentWillReceiveProps before creating the new editor
  3. Pass the old SelectionState to the new editor in componentWIllReceiveProps
  4. Remembering to update the state with the new text whenever editor changes

```import React, { Component } from 'react';
import { CompositeDecorator, ContentState,
Editor, EditorState, Modifier } from 'draft-js';

const decorator = new CompositeDecorator([
... my decorator
]);

class MyEditor extends Component {

constructor(props) {
    super(props);

    const contentState = ContentState.createFromText(props.text);
    const editorState = EditorState.createWithContent(contentState, decorator);
    this.state = {
        ...props,
        editorState,
    };
    this._onEditorChange = this._onEditorChange.bind(this);
}

_onEditorChange(editorState) {
    const content = editorState.getCurrentContent();
    const newText = content.getPlainText();
    this.setState({
        editorState,
        text: newText // update state with new text whenever editor changes
    }, () => this.props.onChange(newText));
}

componentWillReceiveProps(nextProps) {
    const { editorState, text} = this.state;
    const nextText = nextProps.text;
    if (text !== nextText) {   // check that text has changed before updating the editor
        const selectionState = editorState.getSelection();            
        const newContentState = ContentState.createFromText(nextProps.text);            
        const newEditorState = EditorState.create({
                currentContent: newContentState,
                selection: selectionState,  // make sure the new editor has the old editor's selection state
                decorator: decorator
            });           
        this.setState({
            ...nextProps,
            editorState: newEditorState
        });
    }
}

render() {
    const { editorState } = this.state;
    return (
            <div
                style={{
                    border: '1px solid #ccc',
                    cursor: 'text',
                    minHeight: 80,
                    padding: 10                    
                }}>
                    <Editor 
                        editorState={editorState}
                        onChange={this._onEditorChange}
                        placeholder="Enter text" />
            </div>
    );
}

}

export default MyEditor;

```

Has anyone else recently encountered this? I am having the same issue after hydrating the editorState from an API call. After focusing the content, and entering a character, the cursor jumps to the start of the content.

None of the fixes I have seen across several threads seem to do the trick.

same issue

In case this proves helpful to someone in the future. I recently observed the behavior described here (cursor jumping to the beginning of the editor content) when unexpected entity data made its way into the editor state due to a copy-paste from another rich-text source.

For me, the solution was to __always__ update the editorState in the local state. Something along these lines:

handleChange = editorState => {
  if (/* some condition */) {
    // Do whatever you need to
  }

  // Aways update it since handleChange is always called when
  // the selection changes or when the content changes
  this.setState({ editorState });
}

...

<Editor onChange={this.handleChange} {...otherProps} />
  const newState = EditorState.createEmpty()
  this.setState({
          editorState: EditorState.moveFocusToEnd(newState)
   })

This worked for me...

Here's how I ended up solving my problem.

Note I am serializing both content state and selection state so I can restore the entire editor state on subsequent returns. I am also using a decorator that is applying styling to words. This is also in a functional component rather than a class based one -- I say that because I have had different issues using functionless as opposed to class based.

// inside component
const editor = useRef(null);
const [editorSelection, setEditorSelection] = useState(
  new SelectionState(selection)
);

let [editorState, setEditorState] = useState(
  EditorState.set(EditorState.createWithContent(convertFromRaw(content)), {
    decorator: decorator
  })
);

const focus = () => editor.current.focus();

useEffect(() => {
  focus();
  setEditorState(EditorState.forceSelection(editorState, editorSelection));
  /* ... */
}, []);

I have tried it a hundred different ways to create the editor state in one go with content state, selection state and the decorator or applying one after another in different orders (forceSelection, EditorState.set, EditorState.create). E.g.:

EditorState.create({
    contentState: //
    selection: //
    decorator: //
  })

These didn't work as I'd end up losing either the decorator or the selection. Only this seemed to work. You have to call focus before you set your selection.

  const newState = EditorState.createEmpty()
  this.setState({
          editorState: EditorState.moveFocusToEnd(newState)
   })

This worked for me...

but if you have another input ,it cannot focus into other input box

Did a bit of debugging. Seems like the editor internally erroneously updates the block key after typing in the first character, but keeps it constant for subsequent updates.

const handleEditorChange = newEditorState => {
    const rawData = convertToRaw(newEditorState.getCurrentContent());
    const currentContentTextLength = editorState.getCurrentContent().getPlainText().length;
    const newContentTextLength = newEditorState.getCurrentContent().getPlainText().length;

    if (currentContentTextLength === 0 && newContentTextLength === 1) {
      // WORKAROUND: listens to input changes and focuses/moves cursor to back after typing in first character
      setEditorState(EditorState.moveFocusToEnd(newEditorState));
    } else {
      setEditorState(newEditorState);
    }
}

This is a workaround that worked for me - hope it helps others.

  const newState = EditorState.createEmpty()
  this.setState({
          editorState: EditorState.moveFocusToEnd(newState)
   })

This worked for me...

but if you have another input ,it cannot focus into other input box

In that case, use moveSelectionToEnd.

/**

  • Move selection to the end of the editor without forcing focus.
    */
    static moveSelectionToEnd(editorState: EditorState): EditorState;

The root cause of my issue appears to have stemmed from how I was resetting the DraftJs content. I'm using DraftJS for a chat application. The first chat message was fine and didn't exhibit the cursor jumping to the beginning but there was a high probability that any subsequent messages would have the cursor jump.

Before, I would reset the DraftJS content with the following:

const newEditorState = EditorState.createEmpty();
draftJsField = EditorState.moveFocusToEnd(newEditorState);

What fixed it for me was moving to the following:

draftJsField = EditorState.moveFocusToEnd(EditorState.push(editorState, ContentState.createFromText(''), 'remove-range'));

I found through some searching that if you need to clear the DraftJs content, it is recommended to perform a 'createFromText' rather than a 'createEmpty'. The 'createEmpty' should only be used on initialization.

So my initial chat message uses the 'createEmpty' but all subsequent messages use the 'createFromText' to clear the content and reset the focus. Hope this helps.

  const newState = EditorState.createEmpty()
  this.setState({
          editorState: EditorState.moveFocusToEnd(newState)
   })

This worked for me...

This case just work in user edit in the end, if user edit in text of middle, cursor turn to the end, it's not the right way...

  const newState = EditorState.createEmpty()
  this.setState({
          editorState: EditorState.moveFocusToEnd(newState)
   })

This worked for me...

This case just work in user edit in the end, if user edit in text of middle, cursor turn to the end, it's not the right way...

I too faced the same issue. so do we have a solution where content can be edited in the middle also

I was facing the same issue, and none of the above options worked out for me, so this is what I did:

function fixCursorBug(prevEditorState, nextEditorState) {
    const prevSelection = prevEditorState.getSelection();
    const nextSelection = nextEditorState.getSelection();
    if (
        prevSelection.getAnchorKey() === nextSelection.getAnchorKey()
        && prevSelection.getAnchorOffset() === 0
        && nextSelection.getAnchorOffset() === 1
        && prevSelection.getFocusKey() === nextSelection.getFocusKey()
        && prevSelection.getFocusOffset() === 0
        && nextSelection.getFocusOffset() === 1
        && prevSelection.getHasFocus() === false
        && nextSelection.getHasFocus() === false
    ) {
        const fixedSelection = nextSelection.merge({ hasFocus: true });
        return EditorState.forceSelection(nextEditorState, fixedSelection);
    }
    return nextEditorState;
}

This was based on how the SelectionState changed in my experiments, described as follows:

onChange(nextEditorState) {
    console.info(nextEditorState.getSelection().serialize());
    this.props.onChange(nextEditorState);
}

Looking at those console logs, I saw lines like:

Anchor: a05u4:0, Focus: a05u4:0, Is Backward: false, Has Focus: false // when the editor is empty
Anchor: a05u4:1, Focus: a05u4:1, Is Backward: false, Has Focus: false // when I type the first character
Anchor: a05u4:0, Focus: a05u4:0, Is Backward: false, Has Focus: true // when the cursor is reset to position 0

Decided to specifically target this case.

fixCursorBug

@kaustubh-karkare , where did you apply the function "fixCursorBug" at?

I used it in my onChange method given as a prop to the <TextEditor> component:

onChange(nextEditorState) {
    nextEditorState = fixCursorBug(this.props.editorState, nextEditorState);
    this.props.onChange(nextEditorState);
}

or

onChange(nextEditorState) {
    nextEditorState = fixCursorBug(this.state.editorState, nextEditorState);
    this.setState({editorState: nextEditorState});
}

I had the same problem, it works fine just using useState, but once I tried to save my editor into redux and then load, it would always put the cursor to the left.

I have fixed this by using both useState and a redux update. I update useState first and then dispatch an action to update redux and it keeps the cursor position.

Here's a watered down version of my component.

const RichTextEditor = (props) => {
  const { widget, updateWidget } = props;
  const { rawState } = widget.menu;
  const editor = useRef(null);

  // Create new empty state or existing state
  const [editorState, setEditorState] = useState(
    rawState ? EditorState.createWithContent(convertFromRaw(rawState)) : EditorState.createEmpty()
  );

 // This runs separately to useState as it would always set cursor to left without the useState update first
  const updateReduxState = (editorState) => {
    const contentState = editorState.getCurrentContent();
    const editorToJSONFormat = convertToRaw(contentState);
    updateWidget({
      ...widget,
      menu: { ...widget.menu, rawState: editorToJSONFormat },
    });
  };

  const focusEditor = () => {
    editor.current.focus();
  };

  return (
    <>
      <div className="RichEditor-root">
        <div className={className} onClick={focusEditor}>
          <Editor
            editorState={editorState}
            onChange={(editorState) => {
              // ************ This is what fixed it for me, update the components setState and then redux after that ****************
              setEditorState(editorState);
              updateReduxState(editorState);
            }}
            ref={editor}
            spellCheck={true}
          />
        </div>
      </div>
    </>
  );
};

const mapDispatchToProps = (dispatch) => ({
  updateWidget: (widget) => dispatch(updateWidgetAndSaveState(widget)),
});

export default connect(null, mapDispatchToProps)(RichTextEditor);

Any updates on a working solution?

Also experiencing the same issue.

Using draft-js as a chat box, first comment everything is fine, but when you start typing on subsequent comments the cursor jumps to the start of the input after the first couple of characters have been typed. I've tried to apply suggestions above, but none of them work.

My solution is to save SelectionState in addition to RawDraftContentState and then deconstruct them both.

const editorContentWithSelection = {
  rawCurrentContent: convertToRaw(editorState.getCurrentContent()),
  selectionState: JSON.stringify(editorState.getSelection()),
};
// editorContentWithSelection to redux or server

and then deconstruct it like this:

// editorContentWithSelection from redux or server (having valid data)
const emptySelectionState = SelectionState.createEmpty("");
const parsedSelectionState = JSON.parse(editorContentWithSelection.selectionState);
const selectionState = emptySelectionState.merge(parsedSelectionState);
const editorState = EditorState.createWithContent(
  convertFromRaw(editorContentWithSelection.rawCurrentContent)
);
return EditorState.forceSelection(editorState, selectionState);

I didn't have much luck with most of the above. For what's it's worth the easiest solution I found was to simply save the last correct selection state, let the editor move the cursor to the start, and then manually set the correct selection state after the incorrect editor state update.

So something like this after the cursor has been moved to the start.

const selection = editorState.getSelection()
if (selection.isCollapsed()) {
  const selectionWithCorrectOffset = selection.merge({focusOffset: correctOffset, anchorOffset: correctOffset})
  const newEditorState = EditorState.forceSelection(draftEditorState.draftState, updatedSelected)
}

Obviously, there is more to consider than just this but hopefully, this will point someone in the right direction.

Additionally, I maintain my own focus state as there are a number of edge cases that will cause Draft to have the incorrect focus state and therefore insert in the wrong location. In the case that Draft has the wrong focus state, I force it to use the correct one.

My solution is to insert some empty text after entering the library:

    //Enter your new entity as per normal
    const newContent = Modifier.insertText(
      contentState,
      selection,
      " ",    // note in my case it doesn't have any text
      null,
      entityKey
    );


     // enter a couple of spaces after the entity
    const newContent2 = Modifier.insertText(
      newContent,
      newContent.getSelectionAfter(),
      "  ",
      null,
      null   //it's just text
    );


    const nextState = EditorState.push(
      editorState,
      newContent2,
      'insert-characters'
    );

I ran into this issue when using a custom implementation of RichTextUtils.toggleInlineStyle and was able to achieve the desired behavior with:

if (selection.isCollapsed())
  return EditorState.setInlineStyleOverride(
    EditorState.forceSelection(editorState, selection),
    newInlineStyle
  )

Applying the forceSelection after setInlineStyleOverride does not work.

  const newState = EditorState.createEmpty()
  this.setState({
          editorState: EditorState.moveFocusToEnd(newState)
   })

This worked for me...

This case just work in user edit in the end, if user edit in text of middle, cursor turn to the end, it's not the right way...

I too faced the same issue. so do we have a solution where content can be edited in the middle also

I am trying to achieve same behavior where I can edit at middle. Would you help me with that?

Was this page helpful?
0 / 5 - 0 ratings