Estoy tratando de configurar un contenido controlado contentEditable en React. Cada vez que escribo algo en el div, el componente vuelve a renderizarse y el cursor/salta de intercalación al principio. Estoy tratando de lidiar con esto guardando el cursor en una devolución de llamada 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 Esto no funciona, el cursor todavía está atascado al principio. Si ingreso un carácter en el div contentEditable , obtengo el resultado:
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 } ¿Por qué cambia el valor de selectionRange en la devolución de llamada useLayoutEffect , cuando era correcto al comienzo de la nueva representación?
Ok, no estoy familiarizado con la operación de rango, pero me parece que el problema radica en el cambio de estado.
Puedes usar useRef o useState para arreglar esto, déjame usar un objeto con useState por ahora.
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) } Puede reemplazar fácilmente esta versión con un useRef , el punto es asegurarse de que el valor se asigne de inmediato antes de pasar por setState , lo que lleva tiempo para que su estado se actualice al valor más reciente.
Cuando el div contentEditable se vuelve a renderizar, desaparece. El objeto Range contiene referencias a los hijos de este div (propiedades startNode , endNode ), y cuando el div desaparece, el objeto Range rastrea esto y se restablece a su padre, con compensación cero.
El siguiente código demuestra cómo lidiar con esto si sabe que el div contentEditable solo tendrá un hijo. Soluciona el problema donde el cursor se atasca al principio. Lo que hacemos es guardar el desplazamiento en el texto, y al restaurar creamos un nuevo objeto Range , con el nodo de texto recién renderizado como startNode y nuestro desplazamiento guardado como 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