应监听focusin事件(而非focus)在回调中加载并渲染工具栏,用requestAnimationFrame延迟聚焦首个按钮,并加防重逻辑确保只执行一次。

focus 事件本身不触发工具栏加载,得靠它当“开关”
focus 事件只是个信号,不是加载指令。你不能指望给编辑器容器绑个 focus 就自动拉取工具栏 HTML —— 它既不带网络能力,也不管 DOM 插入逻辑。真正要做的,是监听 focusin(冒泡版),在回调里发起请求、渲染工具栏,并确保只执行一次。
为什么必须用 focusin 而不是 focus
focus 和 blur 不冒泡,编辑器里一堆 div[contenteditable] 或嵌套子控件时,你在父容器上监听不到子元素聚焦。而 focusin 支持冒泡,且兼容现代浏览器(IE11 也认),是唯一能做委托的可靠选择。
- 错误写法:
editorContainer.addEventListener('focus', loadToolbar)→ 永远不会触发 - 正确写法:
editorContainer.addEventListener('focusin', handleFocusIn),再在handleFocusIn里判断e.target.matches('[contenteditable], input, textarea') - 记得加防重逻辑:加载完成后调用
observer.unobserve()或设标记位,避免每次聚焦都重复请求
动态插入工具栏后,怎么安全聚焦到第一个按钮
工具栏 DOM 插入后立刻 .focus() 很可能失败,尤其用 innerHTML 或框架渲染时。浏览器还没完成布局或焦点管理未就绪,调用会静默忽略。
- 必须用
requestAnimationFrame延迟一帧:requestAnimationFrame(() => toolbarBtn.focus({preventScroll: true})) - 别用
setTimeout(() => ..., 0)—— 它不保证渲染完成,比requestAnimationFrame更不可靠 - 确保按钮本身可聚焦:有
tabindex="0",没被display: none或pointer-events: none阻断 - 移动端 Safari 对程序聚焦更敏感,
preventScroll: true能避免意外滚动,但无法阻止用户输入后浏览器自动滚动(那是另一回事)
容易被忽略的边界情况
工具栏延迟加载看似简单,但真实场景里几个点常被跳过:
立即学习“前端免费学习笔记(深入)”;
- 编辑器在 Shadow DOM 里?得先确认
shadowRoot可访问,再查子节点并聚焦,否则querySelector找不到 - 用户快速点击多个编辑区域,
focusin可能连续触发 —— 必须在加载前检查是否已存在工具栏,或用AbortController中断前一个请求 - 服务端返回的工具栏 HTML 含
<script></script>标签?原生插入不会执行,得手动eval或创建script元素 append,但有 CSP 风险,更稳妥是把逻辑抽成模块,加载后调用初始化函数 - 键盘导航用户 tab 进来时,工具栏还没加载完,焦点会落到编辑器内容区而非工具栏 —— 这不是 bug,是预期行为;若强求,得监听
keydown+Tab并提前触发加载,但会牺牲首次交互速度



















