facebookarchive / draft-js

A React framework for building text editors.
https://draftjs.org/
MIT License
22.58k stars 2.64k forks source link

Keeping selection after editorState changes [help] #1786

Closed desktp closed 6 years ago

desktp commented 6 years ago

What is the current behavior? I'm having trouble trying to maintain the selection after the editorState being updated through componentWillReceiveProps. I'm following the repo's example on adding links but when clicking the button to prompt for the link, or even trying to add a link, after removing one, but before the state changes, the editor loses its selection and adding a new link doesn't work as it relies on that.

Here's a snippet of the methods I've tried, all of these inside componentWillReceiveProps:

const blocksFromHTML = convertFromHTML(nextProps.value);

if (blocksFromHTML.contentBlocks) {
  const newState = ContentState.createFromBlockArray(
    blocksFromHTML.contentBlocks,
    blocksFromHTML.entityMap,
);

const { editorState } = this.state;
const oldSelectionState = editorState.getSelection();

newEditorState = EditorState.createWithContent(newState, this.decorator);
const newEditorStateWithSelection = EditorState.forceSelection(newEditorState, oldSelectionState);

this.setState({ editorState: newEditorStateWithSelection });
const blocksFromHTML = convertFromHTML(nextProps.value);

if (blocksFromHTML.contentBlocks) {
  const newState = ContentState.createFromBlockArray(
    blocksFromHTML.contentBlocks,
    blocksFromHTML.entityMap,
);

const { editorState } = this.state;
const oldSelectionState = editorState.getSelection();

const updateSelection = new SelectionState({
   anchorKey: oldSelectionState.getAnchorKey(),
   anchorOffset: oldSelectionState.getAnchorOffset(),
   focusKey: oldSelectionState.getAnchorKey(),
   focusOffset: oldSelectionState.getFocusOffset(),
   isBackward: false,
 });

const newEditorState = EditorState.createWithContent(newState, this.decorator);
newEditorState = EditorState.acceptSelection(newEditorState, updateSelection);
const newEditorStateWithSelection = EditorState.forceSelection(newEditorState, newEditorState.getSelection().merge(updateSelection));

this.setState({ editorState: newEditorStateWithSelection });
const blocksFromHTML = convertFromHTML(nextProps.value);

if (blocksFromHTML.contentBlocks) {
  const newState = ContentState.createFromBlockArray(
    blocksFromHTML.contentBlocks,
    blocksFromHTML.entityMap,
);

const { editorState } = this.state;
const oldSelectionState = editorState.getSelection();

let newEditorState = EditorState.push(editorState, newState);

newEditorState = EditorState.acceptSelection(newEditorState, oldSelectionState);

this.setState({ editorState: newEditorStateWithSelection });

With all of these methods above, when I later try to editorState.getCurrentInlineStyles from a formatting bar I implemented, I get

Uncaught TypeError: Cannot read property 'getLength' of undefined
    at getInlineStyleForNonCollapsedSelection (EditorState.js:536)
    at EditorState.getCurrentInlineStyle (EditorState.js:200)
    at InlineStyleControls.render (StyleControls.js:88)
    at InlineStyleControls.render (createPrototypeProxy.js:44)

I can't provide a proper reproduction jsfiddle as the inputs in my application are all wrapped around redux-form, and I couldn't reproduce on a "clean" draftjs Editor, as I get new props from their handlers, which doesn't happen in a default environment.

What also happens, is that in the delay of removing a link with

  removeLink = () => {
    const { editorState } = this.state;
    const selection = editorState.getSelection();
    if (!selection.isCollapsed()) {
      this.setState({
        editorState: RichUtils.toggleLink(editorState, selection, null),
      });
    }
  }

and adding another one, if the setState finishes after I've selected another bit of text and I'm typing the new link, the selection is also lost:

Which versions of Draft.js, and which browser / OS are affected by this issue? Did this work in previous versions of Draft.js? "draft-js": "^0.10.5", "react": "^15.5.4",

colinjeanne commented 6 years ago

The selection references block keys and those keys are part of the content state. In all of these examples you are creating a brand new content state and so will end up with a brand new set of block keys. The old selection will not be meaningful as a result.

desktp commented 6 years ago

In one of the examples I'm using the push method. In any case, which is the correct way of doing this? I'm honestly stumped, and can't figure it out from the docs alone. Creating a new SelectionState with just the offsets would work?

colinjeanne commented 6 years ago

Yes, you're using the push method but you're pushing a brand new content state and so it won't inherit the block keys of the previous content state. You probably want to use Modifier.insertFragment to update the existing content state but that's difficult to tell from your examples since in each you're completely replacing the text rather than adding new text.

desktp commented 6 years ago

I see. I've managed to make it (kinda) work by iterating the new state's block map to get the new keys, and merging them with the old offsets. I'll read up on that method to see if it fits my needs. Thanks for the help

desktp commented 6 years ago

I (kinda) solved with this:

// This gets the selection the editor had before, so when updating, it's 
// preserved so we can add links to the text that was selected
const oldSelectionState = editorState.getSelection();
// Creating a new editor state based on the new incoming content
const newEditorState = EditorState.createWithContent(newState, this.decorator);

// Here, we need to get the new block keys for the content, as they're necessary to maintain the selection
// but are created anew when we created the newEditorState above
let newBlockKey;
const newContentState = newEditorState.getCurrentContent();
const blockMap = newContentState.getBlockMap();

blockMap.forEach((contentBlock) => {
  newBlockKey = contentBlock.getKey();
});

// This sets a new SelectionState with the old selection's offsets
// and the new editor state block keys
const updateSelection = new SelectionState({
  anchorKey: newBlockKey,
  anchorOffset: oldSelectionState.getAnchorOffset(),
  focusKey: newBlockKey,
  focusOffset: oldSelectionState.getFocusOffset(),
  isBackward: false,
});
// And finally, we set the selection state to the new editor state so we can properly add links to it
const newEditorStateWithSelection = EditorState.acceptSelection(newEditorState, newEditorState.getSelection().merge(updateSelection));

But later I just completely reimplemented to fix recreating the editorState on every update, so keeping the selection is easier. Thanks for the help, again.