Open jonasgroendahl opened 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.
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!
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
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.
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 ?
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.
having Same issue Like @marlonmantilla.
@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? :)
@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!
@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.
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
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
@archansel yes man, with added content from parent of component, it's not working.
I end up using key
as @marlonmantilla suggested and it solved my problem
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)
}
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
/**
This is used by
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 (
); } }
USAGE <Field component={LabelAndTextArea} label="Motivo" name="motivo" validate={[textAreaRequired]} />
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 ,
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
I solved it by changing
editorState
withdefaultEditorState
<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
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 });
}
Upload link is not working with this
when create new
EditorState
from content, theSelectionState
is re-created, and the cursor position information (anchoroffset
,focusOffset
) will not kept, so need to update from old editorState.for the
EditorState.forceSelection
andEditor.acceptSelection
can refer to the linkstatic 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.
when create new
EditorState
from content, theSelectionState
is re-created, and the cursor position information (anchoroffset
,focusOffset
) will not kept, so need to update from old editorState. for theEditorState.forceSelection
andEditor.acceptSelection
can refer to the linkstatic 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?
I was having the same issue and this is how I solved it https://github.com/amitrke/rke-nextjs/commit/85b83cd14f1cd8730b8458fd1168317dc42fcb5b
Hi from 2024!
The problem here is we are storing the content of the editor but not the cursor/selection state.
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.
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.
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: Here is my rather simple code:
I couldn't find any examples in the docs regarding this.