contenteditable 元素粘贴必须在 paste 事件首行调用 event.preventDefault() 拦截默认行为,否则原始 HTML 已插入导致 XSS 和光标错乱;需优先解析 text/html,fallback 时清洗零宽字符,并用 Range API 安全插入纯文本。

contenteditable 元素默认粘贴会原样插入剪贴板里的 HTML,不是“格式没清干净”,而是根本没清——浏览器压根不给你机会清。必须拦截 paste 事件并立即调用 event.preventDefault(),否则 XSS、段落塌陷、光标错乱已成定局。
paste 事件里 event.preventDefault() 必须写在第一行
常见错误是先解析剪贴板再 preventDefault,结果浏览器早已把原始 HTML 插进 DOM。Chrome/Edge 甚至发两次 paste(一次带图、一次带 HTML),只拦第二轮毫无意义。
- 监听器必须绑定到
contenteditable="true"的具体元素上,不是父容器或document -
event.preventDefault()要放在 handler 函数开头,不能包在if、setTimeout或异步回调里 - 别用
onpaste属性写法,Shadow DOM 下失效,且无法可靠阻止默认行为
取剪贴板内容:优先 text/html,fallback 到 text/plain 并清洗零宽字符
Safari 对 event.clipboardData.getData('text/html') 经常返回空字符串,但直接依赖 text/plain 又会丢换行、混入 \u200b(零宽空格)和 \u00ad(软连字符),导致段落视觉塌陷或搜索失效。
- 先尝试
event.clipboardData.getData('text/html'),哪怕只用来做 DOM 解析的中间载体 - 用
new DOMParser().parseFromString(html, 'text/html')解析后遍历节点:对<p>、<div style="display:block">手动补\n;删掉所有style、class、data-属性 - fallback 到
text/plain时,立刻执行.replace(/\u200b/g, '').replace(/\u00ad/g, '') - 别信
innerText——它丢换行;也别信正则/<[^>]*>/g——它过不了注释、CDATA、自闭合标签里的危险属性
插入清洗后文本:别用 innerHTML = text,也别用废弃的 execCommand
el.innerHTML = text 会销毁所有子节点、丢失光标位置,若 text 含 <script> 字符还可能触发 XSS;document.execCommand('insertText') 在 Chrome 98+ 已报错,且无视光标位置,总插到末尾。
立即学习“前端免费学习笔记(深入)”;
- 现代方案:用
getSelection().getRangeAt(0)获取当前选区 →range.deleteContents()清空 →range.insertNode(document.createTextNode(cleanText)) - IE11 不支持
insertNode(),需降级为range.createContextualFragment()或拼接字符串 - 若目标是
<textarea>,改用setRangeText(),它支持精确光标控制,兼容 IE10+ - 兜底方案:创建临时
document.createElement('div'),设textContent = cleanText,再用el.appendChild(tempDiv.firstChild)
真正难的不是“怎么过滤”,而是“什么时候开始过滤”——一旦让浏览器默认插入发生,DOM 就已污染,XSS 已执行,光标已错位。所有清洗逻辑都必须发生在 preventDefault() 之后、DOM 插入之前,且全程不能依赖任何未校验的剪贴板数据源。



















