Currently
- I have a react component that stores a
newValue
when the user clicks on a contentEditable<div>
, and updatesnewValue
as the user types. Note: there are 2 main reasons why I have set up this behavior this way: (1) I do not want to send the data to be saved on every key stroke, and (2) I plan to use a variation of this div where each input is checked to verify whether the input is a number. - The
newValue
is sent to be saved when the<div>
loses focus, and then the state of the prop is reset.
Problem
The onChangeHandler
is moving the position of the caret within the editable div to the left hand side. This results in the key strokes 123456
appearing as 654321
Code:
class Input extends Component {
constructor(props) {
super(props);
this.state = {
//newValue input by user
newValue : undefined
}
}
//handler during key press / input
onChangeHandler = event => {
let targetValue = event.currentTarget.textContent;
this.setState({"newValue": targetValue})
}
//handler when user opens input form
onBlurHandler = event => {
//some code that sends the "newValue" to be saved, and resets state
}
render() {
//determine which value to show in the div
let showValue;
//if there is a new value being input by user, show this value
if (this.state.newValue !== undefined) {
showValue = this.state.newValue;
} else {
//if prop has no value e.g. null or undefined, use "" placeholder
if (this.props.value) {
showValue = this.props.value;
} else {
showValue = "";
}
}
return (
<table>
<tbody>
<td>
<div
contentEditable="true"
suppressContentEditableWarning="true"
onInput={this.onChangeHandler.bind(this)}
onBlur={this.onBlurHandler}
>{showValue}
</div>
</td>
</tbody>
</table>
)
}
}
export default Input;
Notes
- I previously was doing this with a
<textarea>
which did not have this issue, but switched to<div>
for more control over the auto-adjusting div height behavior (ref CSS: Remove scroll bar and replace with variable height for textarea in a table <td>) - I have been able to find numerous related answers, but none that are specific to react e.g. Maintain cursor position in contenteditable div. I assume because react is reloading the component after each stroke, this issue is occurring.
- I previously had no ChangeHandler onInput, and this was working fine, but I was unable to log each key press and validate whether the character was a number.
Currently
- I have a react component that stores a
newValue
when the user clicks on a contentEditable<div>
, and updatesnewValue
as the user types. Note: there are 2 main reasons why I have set up this behavior this way: (1) I do not want to send the data to be saved on every key stroke, and (2) I plan to use a variation of this div where each input is checked to verify whether the input is a number. - The
newValue
is sent to be saved when the<div>
loses focus, and then the state of the prop is reset.
Problem
The onChangeHandler
is moving the position of the caret within the editable div to the left hand side. This results in the key strokes 123456
appearing as 654321
Code:
class Input extends Component {
constructor(props) {
super(props);
this.state = {
//newValue input by user
newValue : undefined
}
}
//handler during key press / input
onChangeHandler = event => {
let targetValue = event.currentTarget.textContent;
this.setState({"newValue": targetValue})
}
//handler when user opens input form
onBlurHandler = event => {
//some code that sends the "newValue" to be saved, and resets state
}
render() {
//determine which value to show in the div
let showValue;
//if there is a new value being input by user, show this value
if (this.state.newValue !== undefined) {
showValue = this.state.newValue;
} else {
//if prop has no value e.g. null or undefined, use "" placeholder
if (this.props.value) {
showValue = this.props.value;
} else {
showValue = "";
}
}
return (
<table>
<tbody>
<td>
<div
contentEditable="true"
suppressContentEditableWarning="true"
onInput={this.onChangeHandler.bind(this)}
onBlur={this.onBlurHandler}
>{showValue}
</div>
</td>
</tbody>
</table>
)
}
}
export default Input;
Notes
- I previously was doing this with a
<textarea>
which did not have this issue, but switched to<div>
for more control over the auto-adjusting div height behavior (ref CSS: Remove scroll bar and replace with variable height for textarea in a table <td>) - I have been able to find numerous related answers, but none that are specific to react e.g. Maintain cursor position in contenteditable div. I assume because react is reloading the component after each stroke, this issue is occurring.
- I previously had no ChangeHandler onInput, and this was working fine, but I was unable to log each key press and validate whether the character was a number.
- Make a FiddleJS of your component and I'll help you debug – Tekill Commented Apr 27, 2019 at 14:29
- I actually tried to make a Fiddlejs at first, but failed to get it working. This is as far as I got: jsfiddle.net/e4utgwc1. I am not sure how to handle the react and react-dom modules. – Wronski Commented Apr 27, 2019 at 23:06
3 Answers
Reset to default 9I was able to get this working following solution in https://stackoverflow.com/a/13950376/1730260
Key changes:
- Add new component
EditCaretPositioning.js
with 2 functions: (1) saveSelection to save caret position, and (2) restoreSelection to restore caret position. - Save the caret position in the state of
Input
component - Call
saveSelection()
after every Change event restoreSelection()
as a callback after setting the state- Added
id
to<div>
so can reference inrestoreSelection()
function
EditCaretPositioning.js
const EditCaretPositioning = {}
export default EditCaretPositioning;
if (window.getSelection && document.createRange) {
//saves caret position(s)
EditCaretPositioning.saveSelection = function(containerEl) {
var range = window.getSelection().getRangeAt(0);
var preSelectionRange = range.cloneRange();
preSelectionRange.selectNodeContents(containerEl);
preSelectionRange.setEnd(range.startContainer, range.startOffset);
var start = preSelectionRange.toString().length;
return {
start: start,
end: start + range.toString().length
}
};
//restores caret position(s)
EditCaretPositioning.restoreSelection = function(containerEl, savedSel) {
var charIndex = 0, range = document.createRange();
range.setStart(containerEl, 0);
range.collapse(true);
var nodeStack = [containerEl], node, foundStart = false, stop = false;
while (!stop && (node = nodeStack.pop())) {
if (node.nodeType === 3) {
var nextCharIndex = charIndex + node.length;
if (!foundStart && savedSel.start >= charIndex && savedSel.start <= nextCharIndex) {
range.setStart(node, savedSel.start - charIndex);
foundStart = true;
}
if (foundStart && savedSel.end >= charIndex && savedSel.end <= nextCharIndex) {
range.setEnd(node, savedSel.end - charIndex);
stop = true;
}
charIndex = nextCharIndex;
} else {
var i = node.childNodes.length;
while (i--) {
nodeStack.push(node.childNodes[i]);
}
}
}
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
} else if (document.selection && document.body.createTextRange) {
//saves caret position(s)
EditCaretPositioning.saveSelection = function(containerEl) {
var selectedTextRange = document.selection.createRange();
var preSelectionTextRange = document.body.createTextRange();
preSelectionTextRange.moveToElementText(containerEl);
preSelectionTextRange.setEndPoint("EndToStart", selectedTextRange);
var start = preSelectionTextRange.text.length;
return {
start: start,
end: start + selectedTextRange.text.length
}
};
//restores caret position(s)
EditCaretPositioning.restoreSelection = function(containerEl, savedSel) {
var textRange = document.body.createTextRange();
textRange.moveToElementText(containerEl);
textRange.collapse(true);
textRange.moveEnd("character", savedSel.end);
textRange.moveStart("character", savedSel.start);
textRange.select();
};
}
Updated contentEditable div component:
import CaretPositioning from 'EditCaretPositioning'
class Input extends Component {
constructor(props) {
super(props);
this.state = {
//newValue input by user
newValue : undefined,
//stores positions(s) of caret to handle reload after onChange end
caretPosition : {
start : 0,
end : 0
}
}
}
//handler during key press / input
onChangeHandler = event => {
let targetValue = event.currentTarget.textContent;
//save caret position(s), so can restore when component reloads
let savedCaretPosition = CaretPositioning.saveSelection(event.currentTarget);
this.setState({
"newValue": targetValue,
"caretPosition" : savedCaretPosition
}, () => {
//restore caret position(s)
CaretPositioning.restoreSelection(document.getElementById("editable"), this.state.caretPosition);
})
}
//handler when user opens input form
onBlurHandler = event => {
//some code that sends the "newValue" to be saved, and resets state
}
render() {
//determine which value to show in the div
let showValue;
//if there is a new value being input by user, show this value
if (this.state.newValue !== undefined) {
showValue = this.state.newValue;
} else {
//if prop has no value e.g. null or undefined, use "" placeholder
if (this.props.value) {
showValue = this.props.value;
} else {
showValue = "";
}
}
return (
<table>
<tbody>
<td>
<div
id="editable"
contentEditable="true"
suppressContentEditableWarning="true"
onInput={this.onChangeHandler.bind(this)}
onBlur={this.onBlurHandler}
>{showValue}
</div>
</td>
</tbody>
</table>
)
}
}
export default Input;
ContentEditable is a tricky especially with react as you have to account for a lot of different kind of behaviors. May I suggest you take a look at DraftJS by Facebook.
They took contentEditable and prevented all default behavior and built a nice framework to make a tag editable, they use it for a rich text editor but you can use the same framework without all the bells and whistle to just take control of the content editable.
https://draftjs.org/docs/getting-started
I too have faced this issue, while trying content editable on a <td>
. The cursor goes to the start while editing, I tried multiple methods but nothing helps!
Later, I ended up using this "contenteditable" React package.
https://github.com/lovasoa/react-contenteditable
Previously, My code used was like this
<td
onInput={(e) =>this.handleInput(e, user, "name", index)}
contentEditable={user.isEditable}
className={
user.isEditable ? "border border-success" : ""
}
>
{user.name}
</td>
After including the package, I changed that with the below code. And now it works without any issues.
<td className={user.isEditable ? "border border-success" : ""}>
<ContentEditable
html={user.name}
disabled={!user.isEditable}
onChange={(e) =>
this.handleInput(e, user, "name", index)
}
style={{ "text-decoration": "none" }}
/>
</td>