dialog 元素必须作为 body 直系子元素并调用 showModal() 才能正确渲染 backdrop、响应 Esc、锁定焦点;需动态监听 input + keydown 事件匹配 @ 关键词,结合光标位置精确定位弹窗,并安全渲染异步数据列表。

dialog 元素是首选,但必须确保它在 body 下且调用 showModal()
直接把 <dialog> 写在编辑器容器内部(比如 <div id="editor"><dialog>...</dialog></div>)会导致 backdrop 不渲染、Esc 失效、焦点无法锁定——因为 showModal() 要求弹窗是 body 的直系子元素,否则视口级模态行为被父容器的 overflow: hidden 或 transform 截断。实操时应在初始化编辑器后,用 document.body.appendChild(dialog) 显式挂载。
- 不要等 DOM 加载完再查
dialog,document.getElementById('mention-dialog')返回null就会报Cannot read property 'showModal' of null - 必须用
dialog.showModal(),不是dialog.show(),后者只是浮层,背景仍可点击、无 backdrop、不锁焦点 - Safari 15.4+ 才支持
showModal(),检测写法:if ('showModal' in HTMLDialogElement.prototype),不支持时需降级
关键词触发逻辑要监听 input + keydown,别只靠 @ 键松开
用户输入 @ 后可能继续打字、删减、粘贴,甚至光标跳转——仅监听 keyup 或 blur 容易漏判或误判。真实场景中,应结合 input 事件捕获内容变化,再用 keydown 拦截 Escape、Enter、ArrowDown 等操作。
- 每次
input触发时,用正则匹配光标前最近的@\w*片段,例如const match = editor.innerText.match(/@(\w+)$/),注意要基于当前光标位置做 substring 截取,不能全量匹配 - 匹配到关键词后,立即计算弹窗定位:
const rect = editor.getBoundingClientRect(),再结合光标坐标(需用getSelection().getRangeAt(0).getBoundingClientRect())微调 top/left - 按
Escape时,先dialog.close(),再清空编辑器内残留的@xxx半截内容
弹窗内容动态注入要用 data-* 或函数参数,别硬编码 HTML 字符串
提及列表通常来自异步接口(如 fetch('/api/users?q=' + keyword)),返回的是用户数组。若把整个 <ul><li>...</li></ul> 拼成字符串再 innerHTML 注入,既难维护又易 XSS。更安全的做法是用模板函数或 document.createElement 构建节点。
- 推荐结构:
<dialog id="mention-dialog"><div class="mention-list"></div></dialog>,每次只清空并重绘.mention-list子节点 - 每个候选条目绑定
data-id和data-name,点击时读取:btn.closest('[data-id]').dataset.id - 避免在
onclick属性里写内联 JS,统一用事件委托:dialog.addEventListener('click', e => { if (e.target.matches('[data-id]')) { insertMention(e.target.dataset); } })
移动端和 Safari 下的滚动穿透与焦点丢失是高频坑
在 iOS Safari 或部分安卓 WebView 中,打开弹窗后背景仍可滚动,或弹窗内列表无法用方向键导航——这不是 bug,而是浏览器对 position: fixed 和焦点管理的宽松策略导致的。
立即学习“前端免费学习笔记(深入)”;
- 防背景滚动别只设
body { overflow: hidden },它会让弹窗内长列表也失焦;正确做法是:打开前记录scrollTop,然后body.style.position = 'fixed'; body.style.top = `-${scrollTop}px`;关闭后恢复 - 弹窗打开后必须手动聚焦第一个可交互元素:
dialog.querySelector('button, [data-id]')?.focus(),否则键盘用户无法用Tab或方向键操作 - 点击遮罩关闭时,严格判断
e.target === dialog,否则点列表项也会触发关闭——这是事件冒泡导致的典型误关,不是样式问题



















