jpuri / react-draft-wysiwyg

A Wysiwyg editor build on top of ReactJS and DraftJS. https://jpuri.github.io/react-draft-wysiwyg
MIT License
6.42k stars 1.16k forks source link

Cursor resetting position when loading previous contentState #652

Open jonasgroendahl opened 6 years ago

jonasgroendahl commented 6 years ago

Hello, I'm experience some strange behavior when loading in a previous content state from my database. The cursor keeps backtracking randomly. Issue is illustrated here: animation Here is my rather simple code:

class EditForm extends Component {
    state = {
        editorState: EditorState.createEmpty(),
        contentState: null,
     };
     componentDidMount() {
          fetch("api")
                      .then( res => res.json())
                      .then(data => this.setState({ contentState: JSON.parse(data) })   
     }
    onContentStateChange = contentState => {
     this.setState({
       contentState
       });
    }
     render(){
          return(
       <Editor
                editorState={this.state.editorState}
                contentState={this.state.contentState}
                onContentStateChange={this.onContentStateChange}
              />
          );
       }
  };

I couldn't find any examples in the docs regarding this.

jpuri commented 6 years ago

@jonasgroendahl you are using both editorState and contentState props here. They make editor a controlled component. Since you are not updating editor state its messing up.

I would suggest that use only editorState and update it with changes in editor content.

Convert it to contentState only while saving in the DB.

arsenowitch commented 6 years ago

Hello @jpuri, I'm having the same problem I would like to have controlled editor.

class Editor extends Component {
  onEditorStateChange(editorState) {
    const textHtml = draftToHtml(convertToRaw(editorState.getCurrentContent()));

    this.props.onChange(textHtml);
  }

  render() {
    const contentBlock = htmlToDraft(this.props.value);
    const contentState = ContentState.createFromBlockArray(contentBlock.contentBlocks);
    const editorState = EditorState.createWithContent(contentState);

    return (
      <Editor
        editorState={editorState}
        onEditorStateChange={state => this.onEditorStateChange(state)}
      />
    );
  }
}

In example above redux-form is passing html string through this.props.value. Any suggestions? Thank you!

jpuri commented 6 years ago

Using editorState={editorState} makes editor a controlled component, you need to ensure that you update it for each change in editor, check this: https://github.com/jpuri/react-draft-wysiwyg/tree/master/stories/BasicControlled Or rather use: https://github.com/jpuri/react-draft-wysiwyg/blob/master/src/Editor/index.js#L50

jpuri commented 6 years ago

Also, you can try

import {
  EditorState,
} from "draft-js";
editorState = EditorState.moveSelectionToEnd(editorState);

Issue in this case may be that you are creating editor state each time from content state and content state does not saves selection.

marlonmantilla commented 6 years ago

I'm getting the same issue, and when i use EditorState.moveSelectionToEnd the cursor is always moved to the last when editing, so it's not possible to move the cursor back and edit the text it's always being moved to the last position. Any other way to solve this ?

marlonmantilla commented 6 years ago

What i'm trying to achieve is to have a controlled editor but in my case i need the content to be changed on componentWillReceiveProps:

   componentWillReceiveProps (nextProps) {
        const nextValue = nextProps.value
        const { value } = this.props
        if (nextValue !== value) {
            const contentBlock = htmlToDraft(nextValue)
            const contentState = ContentState.createFromBlockArray(contentBlock.contentBlocks)
            const editorState = EditorState.createWithContent(contentState)
            this.setState({
                editorState: editorState
            })
        }
    }

But after adding this code the cursor will move to the first position and start overriding the state. I can't find anything in the examples for an editor that needs to change their editorState when props change.

nilaybrahmbhatt commented 6 years ago

having Same issue Like @marlonmantilla.

KipariS commented 6 years ago

@jpuri have same problem like @marcelometal and @nilaybrahmbhatt

In my case I get value in html-format from props of parent component and should update that value in parents state by 'this.props.onChange' When I restart page, I get default value with help of 'componentWillReceiveProps' and I can put cursor whenever I want, but it's still moving it to the begining after every button click action Here is my full component:

`

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

componentWillReceiveProps( nextProps ) {
    const nextValue = nextProps.value
    const { value } = this.props;
    console.log(nextProps.value)
    if (nextValue !== value) {
        const contentBlock = htmlToDraft(nextValue)
        const contentState = ContentState.createFromBlockArray(contentBlock.contentBlocks)
        const editorState = EditorState.createWithContent(contentState)
        this.setState({
            editorState: editorState
        })
    }
}

onEditorStateChange = (editorState) => {
    let html = draftToHtml( convertToRaw(editorState.getCurrentContent()))

    if ( isFunction(this.props.onChange) ) { this.props.onChange(html) }
    this.setState( { editorState } )
};
render() {
    const { editorState } = this.state;

    return (
        <div>
            <Editor
                editorState={editorState}
                onEditorStateChange={this.onEditorStateChange}
                wrapperClassName="editor-wrapper"
                editorClassName="editor-editor"
                placeholder='Write down your text here ...'
            />
        </div>
    )
}

`

Could you please give us a hand? :)

marlonmantilla commented 6 years ago

@KipariS i've found that issues are caused by componentWillReceiveProps (since it's an anti pattern anyway more info here ) and ended up moving things to the constructor instead and using the key attribute to trigger mounting the component:

  // wysiwyg editor component
    constructor(props) {
        super(props)
        const contentBlock = htmlToDraft(props.value || '')
        const contentState = ContentState.createFromBlockArray(contentBlock.contentBlocks)

        this.state = {
            editorState: EditorState.createWithContent(contentState),
            focused: false
        }
    }

 // parent 
  <WYSIWYGEditor
     key={`editor-${id}`}
     value={value} />

So whenever the id in the key attribute changes React will create a new component instead so you will get things initialized via constructor. Hope that helps!

KipariS commented 6 years ago

@marlonmantilla Yep, it's good idea, but when my component mounting and constructor called, it still doesn't have value-data. So I need something to update it after I will receive value prop.

archansel commented 6 years ago

Hi guys, got the same issue but in my case, it only happens if I type fast enough (faster than normal typing), here is my code

import React from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import { EditorState, convertToRaw, ContentState } from 'draft-js';
import { Editor } from 'react-draft-wysiwyg';
import draftToHtml from 'draftjs-to-html';
import htmlToDraft from 'html-to-draftjs';
import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css';

import Wrapper from './Wrapper';

class TextEditor extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      editorState: this.parseEditorState(props.value),
      focused: false,
    };
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.value && nextProps.value !== this.props.value) {
      this.setState({ editorState: this.parseEditorState(nextProps.value) });
    }
  }

  onFocus = () => this.setState({ focused: true });
  onBlur = () => this.setState({ focused: false });

  onEditorStateChange = (editorState) => {
    this.setState({ editorState });
    if (this.props.onValueChange) {
      const content = draftToHtml(convertToRaw(editorState.getCurrentContent()));
      this.props.onValueChange(content);
    }
  }

  parseEditorState = (value) => {
    const blocksFromHtml = htmlToDraft(value);
    const { contentBlocks, entityMap } = blocksFromHtml;
    const contentState = ContentState.createFromBlockArray(contentBlocks, entityMap);
    return EditorState.createWithContent(contentState);
  }

  render() {
    const { placeholder, value, onValueChange, readonly, ...rest } = this.props;
    const { editorState, focused } = this.state;
    return (
      <Wrapper readonly={readonly} {...rest}>
        <Editor
          editorState={editorState}
          wrapperClassName="___editor__wrapper"
          toolbarClassName={cx('___editor__toolbar', { focused })}
          editorClassName={cx('___editor__content', 'form-control', { focused })}
          onEditorStateChange={this.onEditorStateChange}
          onBlur={this.onBlur}
          onFocus={this.onFocus}
          placeholder={placeholder}
          readOnly={readonly}
          toolbar={{
            options: ['inline'],
            inline: {
              options: ['bold', 'italic', 'underline'],
            },
          }}
        />
      </Wrapper>
    );
  }
}

TextEditor.propTypes = {
  placeholder: PropTypes.node,
  value: PropTypes.string,
  onValueChange: PropTypes.func,
  success: PropTypes.any,
  error: PropTypes.any,
  readonly: PropTypes.bool,
};

TextEditor.defaultProps = {
  value: '',
};

export default TextEditor;

Note: Wrapper component just for styling things

archansel commented 6 years ago

I solved it by changing editorState with defaultEditorState

<Editor
    defaultEditorState={editorState}
    wrapperClassName="___editor__wrapper"
    toolbarClassName={cx('___editor__toolbar', { focused })}
    editorClassName={cx('___editor__content', 'form-control', { focused })}
    onEditorStateChange={this.onEditorStateChange}
    onBlur={this.onBlur}
    onFocus={this.onFocus}
    placeholder={placeholder}
    readOnly={readonly}
    toolbar={{
    options: ['inline'],
    inline: {
        options: ['bold', 'italic', 'underline'],
    },
    }}
/>

The cursor problem is gone, but new problem arise, the component cannot recieved initial value even though it was re-rendered

namnh06 commented 6 years ago

@archansel yes man, with added content from parent of component, it's not working.

archansel commented 6 years ago

I end up using key as @marlonmantilla suggested and it solved my problem

joinfunny commented 6 years ago

I solved it like this:

on editorState changed:

onEditorStateChange(editorState) {
    this.setState({editorState})
    const content = draftToHtml(convertToRaw(editorState.getCurrentContent())).trim().replace('<p></p>', '')
    this.content = content
    this.props.onContentChange && this.props.onContentChange(content)
}

on componentWillReceiveProps:

componentWillReceiveProps(nextProps) {
    const newState = {}
    if (nextProps.content && nextProps.content !== this.content) {
        this.content = nextProps.content
        newState.editorState = this.createEditorStateFromContent(nextProps.content)
    }
    Object.keys(newState).length > 0 && this.setState(newState)
}

createEditorStateFromContent(content) {
    const contentBlock = htmlToDraft(content)
    const contentState = ContentState.createFromBlockArray(contentBlock)
    return EditorState.createWithContent(contentState)
}
marcelomaia commented 5 years ago

After a lot of effort, i could integrate redux-form with react-draft-wysiwyg loading previus state..

thanks community.

MY COMPONENT

export class LabelAndTextArea extends Component { constructor(props) { super(props); const editorState = EditorState.createEmpty(); this.state = { editorState }; this.changeValue(editorState); }

static defaultProps = { placeholder: "Seu texto aqui." };

/**

USAGE <Field component={LabelAndTextArea} label="Motivo" name="motivo" validate={[textAreaRequired]} />

wangxingxing123654 commented 5 years ago

I solved it like this:

on editorState changed:

onEditorStateChange(editorState) {
    this.setState({editorState})
    const content = draftToHtml(convertToRaw(editorState.getCurrentContent())).trim().replace('<p></p>', '')
    this.content = content
    this.props.onContentChange && this.props.onContentChange(content)
}

on componentWillReceiveProps:

componentWillReceiveProps(nextProps) {
    const newState = {}
    if (nextProps.content && nextProps.content !== this.content) {
        this.content = nextProps.content
        newState.editorState = this.createEditorStateFromContent(nextProps.content)
    }
    Object.keys(newState).length > 0 && this.setState(newState)
}

createEditorStateFromContent(content) {
    const contentBlock = htmlToDraft(content)
    const contentState = ContentState.createFromBlockArray(contentBlock)
    return EditorState.createWithContent(contentState)
}

there also has problem ,when i type fast ,

wangxingxing123654 commented 5 years ago

After a lot of effort, i could integrate redux-form with react-draft-wysiwyg loading previus state..

thanks community.

MY COMPONENT

export class LabelAndTextArea extends Component { constructor(props) { super(props); const editorState = EditorState.createEmpty(); this.state = { editorState }; this.changeValue(editorState); }

static defaultProps = { placeholder: "Seu texto aqui." };

/**

  • Initialising the value for */ initEditorState() { const html = ""; const contentBlock = htmlToDraft(html); const contentState = ContentState.createFromBlockArray( contentBlock.contentBlocks ); return EditorState.createWithContent(contentState); }

/**

  • This is used by to handle change */ handleChange(editorState) { this.setState({ editorState }); this.changeValue(editorState); }

componentWillReceiveProps(nextProps) { // this loads data from previus state. const { input } = nextProps; if ( input.value && input.value !== this.props.value && input.value !== "

\n" ) { const contentBlock = htmlToDraft(input.value); const contentState = ContentState.createFromBlockArray( contentBlock.contentBlocks ); const editorState = EditorState.moveFocusToEnd( EditorState.createWithContent(contentState) ); this.setState({ editorState }); } } onBlur(event) { const value = draftToHtml( convertToRaw(this.state.editorState.getCurrentContent()) ); this.props.input.onBlur(value); }

/**

  • This updates the redux-form wrapper */ changeValue(editorState) { const value = draftToHtml(convertToRaw(editorState.getCurrentContent())); this.props.input.onChange(value); }

render() { const { editorState } = this.state; const { cols, name, label, placeholder, meta } = this.props; return (

<label htmlFor={name} className={"col-form-label"}> {label}

<Editor editorState={editorState} name={name} wrapperClassName="border rounded" editorClassName="ml-2" className="form-control" placeholder={placeholder} onBlur={event => this.onBlur(event)} onEditorStateChange={editorState => this.handleChange(editorState)} // how to config: https://jpuri.github.io/react-draft-wysiwyg/#/docs

      toolbar={{
        options: ["inline", "list"],
        inline: {
          inDropdown: false,
          options: ["bold", "italic", "underline", "strikethrough"]
        },
        list: { inDropdown: false, options: ["unordered", "ordered"] }
      }}
    />
    <ErrorAlert meta={meta} />
  </Grid>
);

} }

USAGE

do you have any other input except rich editor,eg input ????when i use EditorState.moveFocusToEnd, it always stay in the rich editor end ,no longer focus in the upper input box image

ayoubjamouhi commented 5 years ago

I solved it by changing editorState with defaultEditorState

<Editor
    defaultEditorState={editorState}
    wrapperClassName="___editor__wrapper"
    toolbarClassName={cx('___editor__toolbar', { focused })}
    editorClassName={cx('___editor__content', 'form-control', { focused })}
    onEditorStateChange={this.onEditorStateChange}
    onBlur={this.onBlur}
    onFocus={this.onFocus}
    placeholder={placeholder}
    readOnly={readonly}
    toolbar={{
    options: ['inline'],
    inline: {
        options: ['bold', 'italic', 'underline'],
    },
    }}
/>

The cursor problem is gone, but new problem arise, the component cannot recieved initial value even though it was re-rendered

Tnx it's working

htobenothing commented 5 years ago

when create new EditorState from content, the SelectionState is re-created, and the cursor position information (anchoroffset, focusOffset) will not kept, so need to update from old editorState.

for the EditorState.forceSelection and Editor.acceptSelection can refer to the link

  static getDerivedStateFromProps(nextProps, state) {

    if ('value' in nextProps) {

      // get old editor state and old editor selection
      let oldEditorState = state.editorState
      const oldSelectionState = oldEditorState.getSelection();

      // create new editorState from content
      let content = nextProps.value 
      let contentBlock = htmlToDraft(content || "")
      const contentState = ContentState.createFromBlockArray(contentBlock.contentBlocks);
      const newEditorState = EditorState.createWithContent(contentState);

      // update cursor position base on old selection state
      let updateSelection = newEditorState.getSelection().merge({
        anchorOffset: oldSelectionState.getAnchorOffset(),
        focusOffset: oldSelectionState.getFocusOffset(),
        isBackward: false,
      })

      // check whether editor on focus, if onforcus use forceSelection to manually update the posiition
      // if not on focus, use acceptSelection
      let newEditorStateWithSelection;
      if (oldSelectionState.getHasFocus()) {
        newEditorStateWithSelection = EditorState.forceSelection(newEditorState, newEditorState.getSelection().merge(updateSelection))
      } else {
        newEditorStateWithSelection = EditorState.acceptSelection(newEditorState, newEditorState.getSelection().merge(updateSelection))
      }

      return {
        editorState: newEditorStateWithSelection
      };
    }
  }

  onEditorStateChange = editorState => {
    let value = draftToHtml(convertToRaw(editorState.getCurrentContent()))
    this.setState({ editorState });
  }
AbhishekSatyam96 commented 2 years ago

Upload link is not working with this

trahuynhkms commented 1 year ago

when create new EditorState from content, the SelectionState is re-created, and the cursor position information (anchoroffset, focusOffset) will not kept, so need to update from old editorState.

for the EditorState.forceSelection and Editor.acceptSelection can refer to the link

  static getDerivedStateFromProps(nextProps, state) {

    if ('value' in nextProps) {

      // get old editor state and old editor selection
      let oldEditorState = state.editorState
      const oldSelectionState = oldEditorState.getSelection();

      // create new editorState from content
      let content = nextProps.value 
      let contentBlock = htmlToDraft(content || "")
      const contentState = ContentState.createFromBlockArray(contentBlock.contentBlocks);
      const newEditorState = EditorState.createWithContent(contentState);

      // update cursor position base on old selection state
      let updateSelection = newEditorState.getSelection().merge({
        anchorOffset: oldSelectionState.getAnchorOffset(),
        focusOffset: oldSelectionState.getFocusOffset(),
        isBackward: false,
      })

      // check whether editor on focus, if onforcus use forceSelection to manually update the posiition
      // if not on focus, use acceptSelection
      let newEditorStateWithSelection;
      if (oldSelectionState.getHasFocus()) {
        newEditorStateWithSelection = EditorState.forceSelection(newEditorState, newEditorState.getSelection().merge(updateSelection))
      } else {
        newEditorStateWithSelection = EditorState.acceptSelection(newEditorState, newEditorState.getSelection().merge(updateSelection))
      }

      return {
        editorState: newEditorStateWithSelection
      };
    }
  }

  onEditorStateChange = editorState => {
    let value = draftToHtml(convertToRaw(editorState.getCurrentContent()))
    this.setState({ editorState });
  }

I have to try this but I got an issue that is when I hit enter the cursor will be reset to the beginning.

icelic commented 1 year ago

when create new EditorState from content, the SelectionState is re-created, and the cursor position information (anchoroffset, focusOffset) will not kept, so need to update from old editorState. for the EditorState.forceSelection and Editor.acceptSelection can refer to the link

  static getDerivedStateFromProps(nextProps, state) {

    if ('value' in nextProps) {

      // get old editor state and old editor selection
      let oldEditorState = state.editorState
      const oldSelectionState = oldEditorState.getSelection();

      // create new editorState from content
      let content = nextProps.value 
      let contentBlock = htmlToDraft(content || "")
      const contentState = ContentState.createFromBlockArray(contentBlock.contentBlocks);
      const newEditorState = EditorState.createWithContent(contentState);

      // update cursor position base on old selection state
      let updateSelection = newEditorState.getSelection().merge({
        anchorOffset: oldSelectionState.getAnchorOffset(),
        focusOffset: oldSelectionState.getFocusOffset(),
        isBackward: false,
      })

      // check whether editor on focus, if onforcus use forceSelection to manually update the posiition
      // if not on focus, use acceptSelection
      let newEditorStateWithSelection;
      if (oldSelectionState.getHasFocus()) {
        newEditorStateWithSelection = EditorState.forceSelection(newEditorState, newEditorState.getSelection().merge(updateSelection))
      } else {
        newEditorStateWithSelection = EditorState.acceptSelection(newEditorState, newEditorState.getSelection().merge(updateSelection))
      }

      return {
        editorState: newEditorStateWithSelection
      };
    }
  }

  onEditorStateChange = editorState => {
    let value = draftToHtml(convertToRaw(editorState.getCurrentContent()))
    this.setState({ editorState });
  }

I have to try this but I got an issue that is when I hit enter the cursor will be reset to the beginning.

Did you manage to resolve this issue?

amitrke commented 1 year ago

I was having the same issue and this is how I solved it https://github.com/amitrke/rke-nextjs/commit/85b83cd14f1cd8730b8458fd1168317dc42fcb5b

bahriddin commented 7 months ago

Hi from 2024!

TLDR

The problem here is we are storing the content of the editor but not the cursor/selection state.

Case

Imagine we have a WYSIWYGEditor function component that takes its value content prop from outside world (cache, backend) and onChange prop to store it:

import { EditorState, convertToRaw, convertFromRaw } from 'draft-js';
import { Editor } from 'react-draft-wysiwyg';

const WYSIWYGEditor = ({ value, onChange }) => {
  const editorState = EditorState.createWithContent(convertFromRaw(JSON.parse(value)));

  function onEditorStateChange(_editorState) {
    return onChange(JSON.stringify(convertToRaw(_editorState.getCurrentContent())));
  }

  return (
      <Editor
        editorState={editorState}
        onEditorStateChange={onEditorStateChange}
      />
  );
}

Whenever you edit it updates content but cursor goes to the starting position. This doesn't allow you delete, type properly (but reverse) and all sorts of weirdness.

Solution

There is nothing to do with Editor. All you need to do is to store SelectionState:

import { EditorState, SelectionState, convertToRaw, convertFromRaw } from 'draft-js';
import { Editor } from 'react-draft-wysiwyg';

class WYSIWYGEditorState {
    static stringify(editorState) {
        const state = {
            content: convertToRaw(editorState.getCurrentContent()),
            selection: editorState.getSelection()
        };

        return JSON.stringify(state);
    }

    static parse(serializedObj) {
        const state = JSON.parse(serializedObj);
        let editorState = EditorState.createWithContent(convertFromRaw(state.content));

        let selection = SelectionState.createEmpty();
        selection = selection.merge(state.selection);

        editorState = EditorState.acceptSelection(editorState, selection);

        return editorState;
    }
}

const WYSIWYGEditor = ({ value, onChange }) => {
    const editorState = WYSIWYGEditorState.parse(value);

    function onEditorStateChange(_editorState) {
        return onChange(WYSIWYGEditorState.stringify(_editorState));
    }

    return (
        <Editor
            editorState={editorState}
            onEditorStateChange={onEditorStateChange}
        />
    );
}

And this should fix it.