React带行号纯文本编辑器行高匹配问题求助
解决方案:基于镜像容器的行号高度匹配
针对你遇到的「仅为真实换行显示行号,容器自动换行不显示」的问题,你的镜像容器思路是可行的,核心问题出在镜像样式同步和DOM渲染时机的获取上。以下是具体实现方案:
关键思路
- 让隐藏的镜像容器完全复制编辑器的样式(字体、字号、行高、宽度、内边距等),确保自动换行行为和编辑器一致;
- 将真实换行(
\n)分割的内容,逐个放入镜像容器的div中; - 在DOM渲染完成后,获取镜像每行的高度,同步到对应行号的div上。
完整代码实现
import { useRef, useEffect, useState } from 'react'; const LineNumberEditor = () => { const editorRef = useRef(null); const mirrorContainerRef = useRef(null); const lineNumbersRef = useRef(null); const [content, setContent] = useState(''); // 同步编辑器内容到状态(驱动镜像更新) useEffect(() => { const syncContent = () => { if (!editorRef.current) return; setContent(editorRef.current.innerText); }; const editor = editorRef.current; editor?.addEventListener('input', syncContent); // 初始化同步一次 syncContent(); return () => editor?.removeEventListener('input', syncContent); }, []); // 同步行号高度到镜像行高度 useEffect(() => { if (!mirrorContainerRef.current || !lineNumbersRef.current) return; // 等待DOM渲染完成后再获取高度 requestAnimationFrame(() => { const mirrorLines = mirrorContainerRef.current.querySelectorAll('.mirror-line'); const numberLines = lineNumbersRef.current.querySelectorAll('.line-number'); mirrorLines.forEach((mirrorLine, idx) => { if (numberLines[idx]) { numberLines[idx].style.height = `${mirrorLine.offsetHeight}px`; } }); }); }, [content]); // 监听窗口 resize,同步镜像容器宽度 useEffect(() => { const handleResize = () => { if (editorRef.current && mirrorContainerRef.current) { mirrorContainerRef.current.style.width = `${editorRef.current.offsetWidth}px`; // 触发高度重新计算 setContent(editorRef.current.innerText); } }; window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); const lines = content.split('\n'); return ( <div style={{ display: 'flex', gap: '10px', padding: '10px', position: 'relative' }}> {/* 行号区域 */} <div ref={lineNumbersRef} style={{ textAlign: 'right', paddingRight: '8px', userSelect: 'none', color: '#666', fontFamily: 'monospace', fontSize: '14px', lineHeight: '1.5', paddingTop: '8px' }} > {lines.map((_, idx) => ( <div key={idx} className="line-number"> {idx + 1} </div> ))} </div> {/* 编辑器区域 */} <div ref={editorRef} contentEditable suppressContentEditableWarning={true} style={{ flex: 1, minHeight: '250px', padding: '8px', border: '1px solid #e0e0e0', borderRadius: '4px', fontFamily: 'monospace', fontSize: '14px', lineHeight: '1.5', whiteSpace: 'pre-wrap' }} /> {/* 隐藏的镜像容器 */} <div ref={mirrorContainerRef} style={{ position: 'absolute', top: 0, left: '-9999px', visibility: 'hidden', pointerEvents: 'none', fontFamily: 'monospace', fontSize: '14px', lineHeight: '1.5', padding: '8px', whiteSpace: 'pre-wrap', width: editorRef.current?.offsetWidth || '500px' }} > {lines.map((line, idx) => ( <div key={idx} className="mirror-line"> {line || '\u00A0'} {/* 空行用非换行空格填充,避免高度为0 */} </div> ))} </div> </div> ); }; export default LineNumberEditor;
核心细节说明
- 样式完全同步:镜像容器的字体、字号、行高、宽度、内边距必须和编辑器完全一致,否则自动换行的行数会不匹配;
- 空行处理:用
\u00A0(非换行空格)填充空行的镜像div,避免因为无内容导致高度为0,进而行号无法对齐; - DOM渲染时机:用
requestAnimationFrame确保在镜像容器渲染完成后再获取高度,解决useEffect中直接获取不到最新DOM高度的问题; - 动态宽度适配:监听窗口resize事件,同步镜像容器的宽度,保证窗口变化时自动换行行为和编辑器一致。
这个方案既保留了你已实现的「避免重渲染丢失光标」的特性,又完美解决了仅为真实换行显示行号的需求。
内容的提问来源于stack exchange,提问作者Micrified
相关产品推荐
相关产品推荐

