直接设contenteditable="true”会崩编辑体验,因浏览器默认原样插入剪贴板html,含冗余标签、内联样式及脚本;必须在paste事件首行调用preventdefault并手动清洗。

直接设 contenteditable="true" 就往里粘 Word 或微信内容,崩的不是样式,是整个编辑体验——段落塌成一行、光标卡死、脚本偷偷执行、iOS 粘完直接空白。这不是浏览器 bug,是默认行为:它只管“可编辑”,不管“怎么粘”。必须手动拦截 + 清洗,否则没得救。
为什么 paste 事件里 event.preventDefault() 必须写在最开头
常见错误是先做解析再 preventDefault,结果浏览器已经把原始 HTML 插进去了。Chrome/Edge 甚至会发两次 paste 事件(一次带图、一次带 HTML),只拦第二轮根本没用。
- 监听器必须绑定在
contenteditable="true"的最内层元素上,不是父容器 -
event.preventDefault()要放在 handler 函数第一行,不能包在 if 或 setTimeout 里 - 别用
onpaste属性写法,Shadow DOM 下失效,且无法可靠阻止默认行为
取剪贴板内容时,text/html 和 text/plain 怎么选
Safari 对 event.clipboardData.getData('text/html') 经常返回空字符串,但直接 fallback 到 text/plain 又会丢换行、混入 \u200b(零宽空格)和 \u00ad(软连字符),导致渲染异常或搜索失效。
- 优先尝试
event.clipboardData.getData('text/html'),哪怕只是拿它做 DOM 解析的中间载体 - 用
DOMParser解析后遍历节点:对<p></p>、<div style="display:block">、<code><br>手动补\n;删掉所有style、class、data-属性 - fallback 到
text/plain时,立刻做.replace(/\u200b/g, '').replace(/\u00ad/g, '') - 别信
innerText—— 它丢换行;也别信正则/]*>/g—— 它过不了注释、CDATA、自闭合标签里的危险属性 - 遍历
event.clipboardData.items,对每个item.type.startsWith('image/')立即event.preventDefault() - Safari 旧版本(iOS 16.4 之前)不支持
items,fallback 到event.clipboardData.files.length > 0 - 即使拦住了,Chrome 有时仍会把 base64 图片转成
<img src="data:image/png;base64,...">插入,需在setTimeout(() => { ... }, 0)中扫描并移除新<img>标签 - 检测支持性:
typeof document.execCommand === 'function',不支持就走现代路径 - 用
getSelection().getRangeAt(0)获取当前光标位置,创建DocumentFragment插入清洗后的节点 - 插入纯文本时,用
range.insertNode(document.createTextNode(text)),比innerHTML = text安全,也不触发重排 - 特别注意:不要在
input事件里调用innerHTML读值——若用户粘了带<script></script>的 HTML,会执行脚本
如何安全地只放行文字、彻底拦截图片粘贴
靠 CSS 的 user-select: none 或 pointer-events: none 完全无效;拖拽 dragover 也拦不住粘贴——这是两套机制。
document.execCommand 废了,现在该用什么插文本
Chrome 98+、Edge 104+ 直接报 TypeError: document.execCommand is not a function,而且它无视光标位置——你在中间粘贴,文字却跑到末尾。
真正麻烦的不是怎么写代码,而是每种浏览器对 clipboardData 的实现差异、对 contenteditable 的块级插入策略、以及对零宽字符的处理方式都不一样。一个方案在 Chrome 里跑通,到 Safari 里可能连 getData('text/html') 都拿不到——得留好 fallback 链,且每个环节都要加日志验证实际拿到的是什么。











