关于javascript:反应:如何在编辑contentEditable div时保持插入符号的位置?

React: How to maintain caret position when editing contentEditable div?

当前

  • 我有一个react组件,当用户单击contentEditable 时会存储一个newValue,并在用户键入时更新newValue。注意:以这种方式设置此行为的主要原因有两个:(1)我不想发送要在每次击键时保存的数据,并且(2)我计划使用此div的变体,其中检查每个输入以验证输入是否为数字。
  • 失去焦点时,newValue被发送以保存,然后重置道具的状态。
  • 问题

    onChangeHandler将插入标记在可编辑div中的位置移到左侧。这会导致按键123456显示为654321

    代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    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}
             
          </td>
         </tbody>
         </table>
        )
      }
    }

    export default Input;

    注意事项

  • 我以前是用<textarea>来完成此操作的,但没有这个问题,但是切换到可以更好地控制自动调整div高度的行为(请参阅CSS:删除滚动条,并以可变高度替换textarea中的表

    )
  • 我已经找到了许多相关的答案,但是没有一个特定的答案,例如保持光标在contenteditable div中的位置。我认为因为在每个冲程之后React都会重新加载组件,所以会出现此问题。
  • 我以前没有ChangeHandler onInput,但工作正常,但是我无法记录每个按键并验证字符是否为数字。

  • 我能够在https://stackoverflow.com/a/13950376/1730260

    主要更改:

  • 添加具有2个功能的新组件EditCaretPositioning.js:(1)saveSelection保存插入符的位置,和(2)restoreSelection恢复插入符的位置。
  • 将插入符位置保存为Input组件的状态
  • 每次更改事件后调用saveSelection()
  • restoreSelection()作为设置状态后的回调
  • 中添加了id,因此可以在restoreSelection()函数中引用
  • EditCaretPositioning.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    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组件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    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}
             
          </td>
         </tbody>
         </table>
        )
      }
    }

    export default Input;

    ContentEditable是一个棘手的问题,尤其是在做出反应时,因为您必须考虑许多不同种类的行为。我可以建议您看看Facebook的DraftJS。

    他们采用contentEditable并阻止了所有默认行为,并建立了一个很好的框架来使标签可编辑,他们将其用于富文本编辑器,但是您可以使用相同的框架而不必费吹灰之力即可控制内容可编辑。

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


    在尝试在<td>上可编辑的内容时,我也遇到了这个问题。光标在编辑时移到开始处,我尝试了多种方法,但无济于事!

    后来,我最终使用了这个"内容可编辑的" React包。

    https://github.com/lovasoa/react-contenteditable

    以前,我使用的代码是这样的

    1
    2
    3
    4
    5
    6
    7
    8
    9
       <td
          onInput={(e) =>this.handleInput(e, user,"name", index)}
          contentEditable={user.isEditable}
          className={
            user.isEditable ?"border  border-success" :""
          }
       >
         {user.name}
       </td>

    在包含该软件包之后,我使用以下代码对其进行了更改。现在它可以正常工作了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <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>