React:编辑contentEditable div时如何保持插入位置?

问题描述 投票:1回答:2

目前

  1. 我有一个react组件,当用户点击contentEditable newValue时存储<div>,并在用户输入时更新newValue。注意:有两个主要原因可以解释为什么我这样设置这种行为:(1)我不想发送要保存在每个按键上的数据,(2)我打算使用这个div的变体检查每个输入以验证输入是否是数字。
  2. newValue失去焦点时,发送<div>被保存,然后重置道具的状态。

问题

onChangeHandler正在将可编辑div内的插入符号位置移动到左侧。这导致击键123456出现为654321

码:

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;

笔记

  1. 我以前用<textarea>这样做没有这个问题,但切换到<div>更多地控制自动调整div高度行为(ref CSS: Remove scroll bar and replace with variable height for textarea in a table <td>
  2. 我已经能够找到许多相关的答案,但没有一个具体反应,例如Maintain cursor position in contenteditable div。我假设因为反应在每次中风后重新加载组件,这个问题正在发生。
  3. 我以前没有ChangeHandler onInput,这工作正常,但我无法记录每个按键并验证该字符是否为数字。
javascript html reactjs
2个回答
1
投票

ContentEditable是一个棘手的问题,尤其是反应,因为你必须考虑很多不同类型的行为。我建议你看看Facebook的DraftJS。

他们使用contentEditable并阻止了所有默认行为,并构建了一个很好的框架来使标签可编辑,他们将它用于富文本编辑器,但是您可以使用相同的框架,而不需要所有的钟声和哨声来控制可编辑的内容。

https://draftjs.org/docs/getting-started


0
投票

我能够在https://stackoverflow.com/a/13950376/1730260解决这个问题

主要变化:

  1. 添加具有2个函数的新组件EditCaretPositioning.js:(1)saveSelection以保存插入位置,以及(2)restoreSelection以恢复插入位置。
  2. 将插入符号位置保存在Input组件的状态中
  3. 在每次Change事件后调用saveSelection()
  4. 设置状态后restoreSelection()作为回调
  5. id添加到<div>所以可以参考restoreSelection()函数

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();
    };

}

更新的contentEditable div组件:

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;

推荐问答