contenteditable + 虚拟滚动必然翻车,因浏览器 selection 和 focus 机制无法适配动态卸载/重挂 dom:节点销毁会导致 domexception;必须用固定行高、position: absolute + top 定位、节点池复用、tabindex="0"、settimeout 绕过 ios 焦点抑制,并禁用 user-select: none 和 pointer-events: none。

contenteditable + 虚拟滚动为什么必然翻车
不是配置没调对,是浏览器 selection 和 focus 机制根本无法适配“动态卸载/重挂”的 DOM 模式。普通虚拟列表靠 transform: translateY 或清空 innerHTML 再重写内容,但 contenteditable 要求每个可编辑节点在生命周期内必须:保持可聚焦、光标坐标系稳定、getSelection().getRangeAt(0) 返回的 commonAncestorContainer 不能指向已销毁节点。一旦滚动后节点被 replaceChildren 或 innerHTML = '' 清掉,再点进去就抛 DOMException: The object is in an invalid state.。
固定行高 + position: absolute 是唯一稳态路径
动态行高在编辑器里基本不可行——getBoundingClientRect() 触发 layout thrashing;预估高度又扛不住 font-family 切换、zoom 变化、图片加载完成等真实场景。必须强制统一行高:
-
line-height: 1.5+min-height: 24px(建议取整数像素,避免 subpixel 渲染抖动) - 外层容器设
position: relative+overflow-y: auto+ 固定高度 - 占位
<div class="spacer"> 设 <code>height: ${totalItems * itemHeight}px,不参与编辑 - 真实内容区用
position: absolute+top: ${startIndex * itemHeight}px,不用transform - 预先创建 20–30 个
<div contenteditable="true" tabindex="0"> 节点,存入池子<li>滚动时只调用 <code>contentEl.replaceChildren(nodePool.slice(start, end)),避免事件监听器残留 - 每个节点必须带
tabindex="0",否则el.focus()失效、execCommand静默 - 禁止在
focus事件里执行DOMPurify.sanitize(),软键盘弹出前会卡住 - 监听
touchstart后立即el.focus(),别等click(iOS 的click有 300ms 延迟) - 显式调用
setTimeout(() => el.focus(), 100),绕过焦点抑制机制 - 禁用
user-select: none和pointer-events: none——这两个 CSS 属性会让contenteditable在移动端彻底失活 - 纯文本场景放弃
innerHTML,改用textContent读写,零成本防 XSS,也避免 layout 触发
注意:top 定位比 transform 更可控——它不改变元素在文档流中的几何位置,selection API 计算坐标时不会偏移。
如果你了解HTML,CSS和JavaScript,您已经拥有所需的工具开发Android应用程序。本动手本书展示了如何使用这些开源web标准设计和建造,可适应任何Android设备的应用程序 - 无需使用Java。您将学习如何创建一个在您选择的平台的Android友好的网络应用程序,然后转换与自由PhoneGap框架到一个原生的Android应用程序。了解为什么设备无关的移动应用是未来的潮流,并开始构建应用程序,提供更
节点池复用比渲染逻辑更关键
每次滚动不是创建新节点,而是复用已有节点并更新 textContent 和 data-index:
移动端焦点必须 setTimeout 绕过抑制
iOS Safari 在软键盘弹出时主动降级 focus 调度优先级,导致点击无响应、光标卡在顶部、连续输入后失焦。这不是 bug,是策略限制:
真正难的不是写出来,是让每一行 DOM 在整个滚动生命周期里都“活着”且可寻址——删一个节点、漏一个 tabindex、错用一次 transform,光标就消失。










