draft-js icon indicating copy to clipboard operation
draft-js copied to clipboard

How to stop DraftJS cursor jumping to beginning of text?

Open Sandeep3005 opened this issue 8 years ago • 43 comments

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

Sandeep3005 avatar May 09 '17 19:05 Sandeep3005

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 avatar Aug 11 '17 15:08 SevenZark

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

marlonmleite avatar Sep 27 '17 13:09 marlonmleite

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

againksy avatar Nov 01 '17 19:11 againksy

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 { 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;

PayscaleNateW avatar Nov 15 '17 02:11 PayscaleNateW

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.

tomspeak avatar May 08 '18 17:05 tomspeak

same issue

koutsenko avatar May 28 '18 06:05 koutsenko

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.

zebulonj avatar Jul 18 '18 16:07 zebulonj

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} />

cmedinasoriano avatar Dec 13 '18 07:12 cmedinasoriano

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

This worked for me...

sandeepreddy19 avatar Jan 24 '19 23:01 sandeepreddy19

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.

stargazing-dino avatar Apr 24 '19 16:04 stargazing-dino

  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

wangxingxing123654 avatar May 13 '19 09:05 wangxingxing123654

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.

crowicked avatar Sep 26 '19 19:09 crowicked

  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;

crowicked avatar Oct 01 '19 17:10 crowicked

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.

drock512 avatar Dec 17 '19 20:12 drock512

  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...

BertieGo avatar Mar 20 '20 10:03 BertieGo

  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

din-chin avatar May 07 '20 14:05 din-chin

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.

kaustubh-karkare avatar Jun 26 '20 06:06 kaustubh-karkare

fixCursorBug

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

cpeele00 avatar Jun 30 '20 21:06 cpeele00

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});
}

kaustubh-karkare avatar Jun 30 '20 21:06 kaustubh-karkare

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);

JPNZ4 avatar Jul 08 '20 02:07 JPNZ4

Any updates on a working solution?

ofirassif avatar Aug 26 '20 15:08 ofirassif

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.

Shaun-Sheppard avatar Sep 17 '20 09:09 Shaun-Sheppard

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);

memasdeligeorgakis avatar Sep 27 '20 11:09 memasdeligeorgakis

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.

wdfinch avatar Oct 15 '20 03:10 wdfinch

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'
    );

dwjohnston avatar Oct 21 '20 06:10 dwjohnston

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.

bnchdrff avatar Jan 20 '21 20:01 bnchdrff

  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?

jenildesai25 avatar Jan 28 '21 17:01 jenildesai25

If you are finding that the selection moves somewhere unexpected after modifying the EditorState somehow, preserve the SelectionState before you modify your editorState and the forceSelection at the end of your logic.

selectionState = editorState.getSelection();

// perform some logic....

editorState = EditorState.forceSelection(editorState, selectionState)

vTrip avatar Aug 17 '21 01:08 vTrip

  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

What worked for me is checking if the current selection has focus before calling moveFocusToEnd So

   this.setState({
         editorState: !editorState.getSelection().getHasFocus() 
            ? EditorState.moveFocusToEnd(newState) 
            : newState
    })

neddinn avatar Aug 19 '21 11:08 neddinn

All the solution here doesn't work for me, and I noticed this happening after I load an already saved html, by converting to draft format as mentioned in the docs.

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

But it kept on automatically focusing it at the end no matter where I put the cursor on. Please does anybody have a fix for this

SirPm avatar Oct 14 '21 09:10 SirPm

Is this problem still not resolved? Disappointed

jiahao-si avatar Oct 15 '21 09:10 jiahao-si

This may or may not help someone. I was able to get past this issue by not updating the component level state until onBlur.

<Editor
    editorState={rte}
    onEditorStateChange={onValChange}
    ref={rteRef}
    onBlur={() => {
      setPageContent({
        ...pageContent,
        newPageContent,
      });
    }}
  />

I set my state as createEmpty on load:

  const [rte, setRte] = useState(EditorState.createEmpty());

Set the state once the component mounts:

 useEffect(() => {
    const blocks = convertFromHTML(copy.body || '');
    const state = customContentStateConverter( // converts for Hooks
      ContentState.createFromBlockArray(blocks.contentBlocks, blocks.entityMap)
    );
    setRte(EditorState.createWithContent(state));
  }, [copy.body]);

Update the state for only component updates. This is done so my useEffect does not always reset the location of the cursor:

  const onValChange = useCallback((e) => {
    setCopyHtml(draftToHtml(convertToRaw(e.getCurrentContent())));
    setRte(e);
  }, []);

I can use all the functionality of the Editor without weird cursor jumping. Then when I click outside of the Editor onBlur updates the Parent component.

DeadDuck83 avatar Dec 03 '21 00:12 DeadDuck83

OMG this library is a complete mess. So simple features and a huge complexity behind. And I can see was made by facebook engineers. I saw a guy in conferences introducing this library LOL He has a rock face XD. OMG I thought this thinks only happened in low level companies but as I can see Facebook enjoy complexity but not for analise algorithms XD I think good complexity is required in code interviews only at facebooks. XD

Im passing of this mess of lib. Good luck for those are using this mess.

fabricioAburto avatar Dec 24 '21 18:12 fabricioAburto

I was facing this same issue, and ended up getting it working for my use case with a much simpler approach than many of the above examples.

TL;DR

Using moveFocusToEnd, then toggling Editor's readOnly prop fixed the issue for me.

Details

My use case

I have a component named RichEditor, which is my wrapper for Draft.js' Editor. RichEditor is used by many components in my app. Most of the instances of said components use RichEditor persistently; they sit there in read-only mode until you double-click them, at which point you can edit them. Read-only mode was implemented by having a readOnly prop in my RichEditor component, which is passed directly down to Editor's readOnly prop.

I also have "plain" (not using Draft.js) label elements which I want users to be able to edit by double-clicking on them. The double-click causes a popover to appear (which also contains RichEditor).

My issue was that for both the persistent components and the popover, the cursor always went to the beginning of the text after double-clicking them.

What worked for me

First, I was able to use one of the suggestions above with partial success:

setEditorState(editorState => EditorState.moveFocusToEnd(editorState));

This worked in getting the cursor to the end of the text upon the <Editor> component receiving focus. However, this introduced some maddening behaviors in my popover:

  • The first time you hit the Backspace key after getting focus, it would delete the character immediately to the left of the cursor (as expected), but the cursor would then immediately shoot over to the beginning of all the text! Only once you've changed the cursor location after this (whether mouse-clicking or using arrow keys) would Backspace work fully as expected.
  • Similar behavior when selecting some text via double-clicking a word or just plain click-dragging: the selection of text would work as expected, and the first character you typed would overwrite the selection with that character... but then immediately shoot the cursor over to the beginning of the text.
  • When hitting Shift+Enter to create a newline (I've got my handleKeyCommand configured that way), the newline would be appended after the cursor, instead of inserted before it. This would keep happening until you used the arrow keys (specifically) to change cursor location.

After spending an inordinate amount of time chasing this down (and trying some of the above suggestions with no success), I'd noticed that none of my persistent components were having these same new issues as the popover. Again, they're all using the same RichEditor component, so why the different behavior?

Well, it finally dawned on me that the difference between them is that the persistent components were having the readOnly value changed upon double-clicking (to invoke "edit" mode) - but I hadn't bothered with doing that for the popover since it would only ever be visible when meant to accept changes. With this thought, I started playing around with toggling readOnly in my popover, and voila - the weirdness described above all went away!

The tricky part of getting it sorted was to ensure that my popover (and ultimately, Editor) was fully rendered before changing readOnly from true to false. The way I implemented the fix was to add const [readOnly, setReadOnly] = useState(true); to my popover component, then in a useEffect() block I'd fire setReadOnly(false). The useEffect() block had a single variable in its dependency array, which was in my case a reliable way to determine whether it was time to fire setReadOnly.

I'm not sure why this works, but I suspect it's probably just a matter of triggering a re-render of Editor after moveFocusToEnd() was invoked. I didn't try any of the suggestions above involving EditorState.createEmpty(), as all instances of RichEditor have existing content, plus decorators and such. Maybe there's a simpler approach than this, but it's what worked for me.

ggunnigle avatar Jan 19 '22 08:01 ggunnigle

@ggunnigle thanks for that write up! Totally fixed the jankyness of Draftjs for us. The local readOnly state value works super well.

aroc avatar Jan 28 '22 18:01 aroc

@drock512 thank you, saved my day.

shuiRong avatar Feb 24 '22 16:02 shuiRong

thank you all! i love you :) you saved me!

digitalgopnik avatar Apr 12 '22 16:04 digitalgopnik

I want to let a comment here cause I had the same problem in production and the problem was I update the state with a custom handler:

const handleContentChange = (state) => { setEditorState(state) ....other process.... } But If you read the doc at draftjs.org, you can see that they use setEditorState to directly update the state, I figured out with this:

carbon

Hope it'll help

AntoineGGG avatar Apr 27 '22 06:04 AntoineGGG