我正在尝试在React中设置一个受控的contentEditable
。每次我在div中编写一些内容时,组件都会重新呈现,并且光标/插入符号会跳回到开头。我试图通过将光标保存在onInput
回调中来处理此问题:
import { useState, useEffect, useRef, useLayoutEffect } from 'react'
function App() {
const [HTML, setHTML] = useState()
const [selectionRange, setSelectionRange] = useState()
console.log('on rerender:', selectionRange)
useLayoutEffect(() => {
console.log('in layout effect', selectionRange)
const selection = document.getSelection()
if (selectionRange !== undefined) {
selection.removeAllRanges()
selection.addRange(selectionRange)
}
})
function inputHandler(ev) {
console.log('on input', document.getSelection().getRangeAt(0))
setSelectionRange(document.getSelection().getRangeAt(0).cloneRange())
setHTML(ev.target.innerHTML)
}
return (
<>
<div
contentEditable
suppressContentEditableWarning
onInput={inputHandler}
dangerouslySetInnerHTML={{ __html: HTML }}
>
</div>
<div>html:{HTML}</div>
</>
)
}
export default App
这不起作用,光标仍然停留在开头。如果我在contentEditable
目录中输入一个字符,就会得到输出:
on input
Range { commonAncestorContainer: #text, startContainer: #text, startOffset: 1, endContainer: #text
, endOffset: 1, collapsed: true }
on rerender:
Range { commonAncestorContainer: #text, startContainer: #text, startOffset: 1, endContainer: #text
, endOffset: 1, collapsed: true }
in layout effect
Range { commonAncestorContainer: div, startContainer: div, startOffset: 0, endContainer: div, endOffset: 0, collapsed: true }
为什么selectionRange
的值在重新渲染开始时是正确的,但在useLayoutEffect
回调中却发生了变化?
发布于 2021-09-19 16:36:54
当contentEditable
div被重新渲染时,它将消失。Range
对象包含对该div的子级(startNode
、endNode
属性)的引用,当该div消失时,Range
对象会跟踪该div,并将其自身重置为其父目录,且偏移量为零。
下面的代码演示了如何处理这个问题,如果您现在知道contentEditable
div只有一个子级。它修复了光标在开头卡住的问题。我们要做的是将偏移量保存在文本中,在恢复时,我们创建一个新的Range
对象,新呈现的文本节点为startNode
,保存的偏移量为startOffset
。
import { useState, useEffect, useRef, useLayoutEffect } from 'react'
function App() {
const [HTML, setHTML] = useState()
const [offset, setOffset] = useState()
const textRef = useRef()
useLayoutEffect(() => {
if (offset !== undefined) {
const newRange = document.createRange()
newRange.setStart(textRef.current.childNodes[0], offset)
const selection = document.getSelection()
selection.removeAllRanges()
selection.addRange(newRange)
}
})
function inputHandler(ev) {
const range = document.getSelection().getRangeAt(0)
setOffset(range.startOffset)
setHTML(ev.target.innerHTML)
}
return (
<>
<div
contentEditable
suppressContentEditableWarning
onInput={inputHandler}
dangerouslySetInnerHTML={{ __html: HTML }}
ref={textRef}
>
</div>
<div>html:{HTML}</div>
</>
)
}
export default App
发布于 2021-09-19 13:12:45
好吧,我不熟悉范围操作,但在我看来,问题在于状态的改变。
你可以使用useRef
或useState
来解决这个问题,现在让我在useState
中使用一个对象。
function App() {
const [HTML, setHTML] = useState()
const [selectionRange, setSelectionRange] = useState({ range: null })
useLayoutEffect(() => {
const selection = document.getSelection()
if (selectionRange !== undefined) {
selection.removeAllRanges()
if (selectionRange.range)
selection.addRange(selectionRange.range)
}
})
function inputHandler(ev) {
selectionRange.range = document.getSelection().getRangeAt(0).cloneRange())
setSelectionRange({ ...selectionRange })
setHTML(ev.target.innerHTML)
}
你可以很容易地用useRef
替换这个版本,关键是要确保在通过setState
之前立即赋值,这需要时间来将你的状态更新到最新的值。
https://stackoverflow.com/questions/69243509
复制相似问题