contenteditable 仅为可编辑开关,非富文本编辑器;需配合 tabindex、块级容器、手动选区控制及 xss 过滤等才能安全使用。

contenteditable 不是富文本编辑器,它只是个“可编辑开关”。加了就以为能用,结果光标乱跳、回车插 <div>、粘贴带 XSS 风险的 HTML —— 这些都不是 bug,是浏览器默认行为。
<h3>为什么加了 <code>contenteditable="true" 还点不动
最常见原因是元素根本无法获得焦点:
- 原生
<div> 没有 Tab 焦点能力,必须加 <code>tabindex="0" - 父级若设了
contenteditable="false",子元素再写true也无效(“就近 false 优先”) - CSS 中写了
user-select: none或pointer-events: none,会直接禁用交互 - 别用在
<span></span>、<strong></strong>这类内联元素上——光标定位不可靠,块级容器如<div>、<code><section></section>才安全双击后光标总跳到开头?别只调
focus()el.focus()只是让元素聚焦,不控制落点。浏览器按自己的规则选位置:首个文本节点起始、空<p></p>后崩溃、或复用上次 selection 缓存。必须手动构造
Range并注入Selection:- 确保 DOM 已渲染完成(可用
setTimeout(() => {}, 0)或监听transitionend) - 用
window.getSelection()获取当前 selection - 用
document.caretPositionFromPoint(event.clientX, event.clientY)(Chrome/Firefox)或document.caretRangeFromPoint()(旧 Safari)获取点击处的node+offset - 创建
Range,调用range.setStart(node, offset)锚定,再sel.removeAllRanges(); sel.addRange(range)—— 漏掉removeAllRanges在 Safari 下极易失效
怎么安全读取和清洗用户输入
innerHTML直接暴露 XSS 风险,textContent又丢格式,二者不能混用:- 纯文本场景(如摘要、校验):用
el.textContent,它不执行脚本、不渲染标签 - 需保留基础格式(换行、粗体、列表):取
el.innerHTML,但必须过滤——推荐用DOMParser解析后白名单遍历,只留<p></p>、<strong></strong>、<br>、<ul></ul>/<ol></ol>,删掉所有style、class和未知标签 - 绝对别用正则替换 HTML 字符串:
innerHTML.replace(/<script.>/gi, '')</script.>容易漏变体或闭合错误 - 粘贴时若业务允许,直接走
event.clipboardData.getData('text/plain'),跳过 HTML 解析更轻量也更安全
回车、粘贴、中文输入必须全拦截重写
浏览器默认行为几乎都不符合实际需求:
- 回车:Chrome 插
<div>,Firefox 插 <code><p></p>,Shift+Enter行为也不统一 → 监听keydown,对Enter调e.preventDefault(),再按需插入<br>或<p></p> - 粘贴:默认带 Word 样式、嵌套、
data-属性甚至残留<script></script>→ 必须拦截paste,e.preventDefault()后取'text/html'或'text/plain',再用RangeAPI 插入,别依赖已废弃的document.execCommand('insertText') - 中文输入:必须监听
compositionstart和compositionend,否则拼音上屏瞬间内容会丢失;input事件只在 composition 结束后触发,不能替代
真正难的不是让文字变可编辑,而是把光标位置、选区边界、格式嵌套、跨浏览器差异、XSS 过滤这些细节全兜住——哪怕只做一个内嵌备注框,也得先处理好粘贴和回车;要做正式产品,建议直接上 Tiptap 或 Quill。
- 确保 DOM 已渲染完成(可用











