I want to automatically replace character sequences while the user is typing, but it seems Draft isn't behaving the way I expected it to.
I adapted the plain text example, and extended my onChange method to do optionally do some transformations on the editor state before applying the new state (in this case, replacing -- by *):
this.onChange = (editorState) => {
const selection = editorState.getSelection();
const block = editorState.getCurrentContent().getBlockForKey(selection.getAnchorKey());
if (block.getText().startsWith("--")) {
// Replace '--' with '*'
const contentWithoutDash = Modifier.replaceText(
editorState.getCurrentContent(),
new SelectionState({
anchorKey: block.getKey(),
anchorOffset: 0,
focusKey: block.getKey(),
focusOffset: 2
}),
"*");
editorState = EditorState.push(
editorState,
contentWithoutDash,
'replace-text'
);
}
this.setState({editorState});
};
I have 2 problems with this approach:
--, at the point of this.setState, the state is as I expect it to be (it has a block with just * in it); however, after the setState, the editor contents is a block with -* (so the last typed - somehow came through after all). I noticed that there are indeed state changes happening after the setState, which doesn't happen if I don't modify the text. The unexpected extra state changes contain a lastEventType=spellcheck-change (spellchecking is off for the editor), not sure if that helps?-- by an empty string instead of *, I get the following error:Invariant Violation: findComponentRoot(..., .0.0.0.$editor0.0.0.$8ms3b.$8ms3b.0:$8ms3b-0-0.0): Unable to find element. This probably means the DOM was unexpectedly mutated (e.g., by the browser), usually due to forgetting a
<tbody>when using tables, nesting tags like<form>, <p>, or <a>, or using non-SVG elements in an<svg>parent. Try inspecting the child nodes of the element with React ID
Any clues how to resolve these issues?
I was able to work around the problem by putting my code in handleBeforeInput.
Yeah, it's currently a bit risky to modify EditorState in onChange if the modification is triggered by text entry. This is largely because regular typing is handled in a way that the native event is allowed to occur as often as possible. If the state change is propagated but followed by the native character insertion, the DOM and state can get out of sync.
For input-triggered changes like this, though, handleBeforeInput is a good solution. :)
I'm still trying to come up with a good way to handle this. I'd like it if onChange can manipulate the EditorState as freely as possible.
I'm using the replaceText method and it works quite well. However the cursor is being put before the replace text. Should I manually set the selection after replacing text?
beforeInput(char) {
var {editorState} = this.state;
console.log('char ', char)
const regex = /\@[\w]+/g;
let content = editorState.getCurrentContent();
let selection = editorState.getSelection();
const block = editorState.getCurrentContent().getBlockForKey(selection.getAnchorKey());
let text = block.getText().slice(0, selection.getEndOffset());
let lastWord = R.compose(R.last, R.split(' '))(text);
console.log('last word ', selection.getAnchorOffset());
if (R.test(regex, lastWord)) {
const entityKey = Entity.create('MENTION', 'IMMUTABLE', {name: lastWord + char});
let replaced = Modifier.replaceText(
content,
new SelectionState({
anchorKey: block.getKey(),
anchorOffset: selection.getEndOffset() - lastWord.length,
focusKey: block.getKey(),
focusOffset: selection.getEndOffset()
}),
lastWord + char,
null,
entityKey
);
editorState = EditorState.push(
editorState,
replaced,
'replace-text'
);
this.setState({editorState});
return true;
}
}
@alexeygolev You shouldn't have to put the selection there yourself -- it should set it for you. I'm not sure why it would fail to do so. Are you using a decorator component for this entity type? If so, be sure to set the rendered children of the decorator to be this.props.children. Without this, the selection logic may not be able to place selection within your decorated element.
You might consider not using an entity here, by the way. Since the text itself dictates the decoration, you could just use a decorator. The tweet.html example demonstrates this.
I also notice is that your EditorChangeType (third parameter in EditorState.push) is not a valid string in the EditorChangeType union. I think the right change type here is probably apply-entity since that's effectively what is being done here. I don't know if this is what is causing the trouble, but it could be related.
I'm running into the same selection behavior. When I replace-text matching all of the text in the current block, the selection jumps to the previous block and I have to reset it manually.
(In my case I'm matching a > input character at the start of a line to create a blockquote by replacing text, then setting the block type. Although either order of operations causes the same selection jump.)
Does anyone know why my components written as classes doesn't update any of the text? I'm using the "rich" example, rewritten using reactClass() instead of React components. Here is a gist to my code: https://gist.github.com/conor909/2c040c1983ef42f988a45a351081429f
I also have the same issue. I managed to have it in a codepen. The example editor is supposed to detect replace any text between 2 stars with '---': it shows the right behavior _but_ the selection/carret is put at the beginning of the block instead of being put after the '---'.
I printed the selection offset to be sure, and it shows the right offset (i.e. after the '---').
I know the EditorChangeType is not one of the union, but 1) I found no one with the right semantic meaning 2) I tried others and still have the same issue.
I'm lost ;)
Does the EditorChangeType replace-text or insert-text exist for EditorState.push? I can't find it listed on https://facebook.github.io/draft-js/docs/api-reference-editor-change-type.html
@ianstormtaylor I came across the same problem, when I replace the whole block text, the cursor jumps before all characters after replacing. Even I merge "selectionAfter" to contentState, still not work:
onMentionSelect = mention => {
const {
editorState
} = this.props;
let contentState = editorState.getCurrentContent();
let contentBlock = utils.getSelectBlock(editorState);
const blockKey = contentBlock.getKey();
const selectionState = editorState.getSelection();
const name = mention.get('name');
contentState = Modifier.replaceText(contentState, new SelectionState({
anchorKey: blockKey,
anchorOffset: 0,
focusKey: blockKey,
focusOffset: contentBlock.getLength()
}), name);
contentBlock = contentState.getBlockForKey(contentBlock.getKey());
this.onChange(EditorState.push(editorState, contentState.merge(Map({
//not work!!!!!
selectionAfter: new SelectionState({
anchorKey: blockKey,
anchorOffset: contentBlock.getLength(),
focusKey: blockKey,
focusOffset: contentBlock.getLength()
})
})), 'insert-characters'));
}
I trace the diff of the editorState. Find there is a function editOnSelect called after replace. It force the selectionState changed to "forceSelection" and make anchorOffset and focusOffset both 0.
@hellendag What should I do to avoid calling this function?
Most helpful comment
@ianstormtaylor I came across the same problem, when I replace the whole block text, the cursor jumps before all characters after replacing. Even I merge "selectionAfter" to contentState, still not work:
I trace the diff of the editorState. Find there is a function editOnSelect called after replace. It force the selectionState changed to "forceSelection" and make anchorOffset and focusOffset both 0.
@hellendag What should I do to avoid calling this function?