纯前端实现可访问富文本工具栏:选区恢复、命令状态与快捷键

举报
yd_213843956 发表于 2026/11/03 08:37:41 2026/11/03
【摘要】 富文本工具栏难点不在按钮样式,而在点击控件后选区是否还在。这篇讲清楚选区保存、命令抽象、状态同步和键盘可达性。

给 contenteditable 加一排加粗、列表和链接按钮,代码量看起来不大,真正使用时却经常出现“点按钮后选区消失”“按钮状态和正文不一致”“键盘用户无法操作工具栏”等问题。工具栏必须和编辑器选区、命令模型与焦点状态一起设计。

一、点击按钮前先保住选区

浏览器在按钮获得焦点时可能折叠或清除编辑器里的 Range。简单做法是在工具栏按钮的 pointerdown 阶段阻止默认焦点切换,或者关闭前保存当前 Range:

let savedRange = null;

editor.addEventListener('keyup', saveSelection);
editor.addEventListener('mouseup', saveSelection);

function saveSelection() {
  const selection = getSelection();
  if (!selection.rangeCount) return;
  const range = selection.getRangeAt(0);
  if (editor.contains(range.commonAncestorContainer)) {
    savedRange = range.cloneRange();
  }
}

真正执行命令前,把 selection 恢复到 savedRange。恢复后仍要检查节点是否还在文档中,撤销、重新渲染或协同更新都可能让旧 Range 失效。

二、把按钮绑定到命令而不是 DOM 细节

工具栏按钮应该调用统一的命令接口,例如 toggleBold、setBlockStyle 或 insertLink。适配层内部可以调用编辑器的命令系统、文档模型或浏览器的 Range API。这样按钮不需要知道当前选中的是文本节点还是段落。

const commands = {
  bold: () => applyInline('strong'),
  orderedList: () => applyBlock('ol'),
  link: () => openLinkDialog()
};

如果使用 document.execCommand,需要清楚它已被标记为过时。它适合轻量兼容层,不适合作为复杂编辑器的长期核心。生产级产品通常应使用成熟的富文本框架,并复用其事务与历史机制。

三、命令状态来自唯一数据源

加粗按钮的按下状态不能因为用户点了一次就永久保持。状态应跟随当前选区重新计算。简单环境可以查询当前格式;使用编辑器模型时,应从选区所在节点及其祖先节点推导:

function syncToolbarState() {
  const active = readActiveFormats();
  for (const button of toolbar.querySelectorAll('[data-command]')) {
    const command = button.dataset.command;
    button.setAttribute('aria-pressed', String(active.has(command)));
  }
}

selectionchange、输入、撤销和切换到源码模式后都要重新同步。工具栏显示的状态必须和最终文档结构一致。

四、工具栏语义与焦点

一组格式按钮可以放在 role="toolbar" 的容器中,并提供可读名称。按钮不要只显示 B、I 等字符,要有 aria-label,例如“加粗,Ctrl+B”。切换型按钮使用 aria-pressed,菜单型入口使用 aria-haspopup 和 aria-expanded。

完整实现 toolbar 时,方向键应在工具项之间移动,Home 和 End 跳到首尾,Tab 用于离开工具栏。若只使用普通按钮,也要保证 Tab 顺序合理,不能为了视觉整齐把控件全部移出焦点序列。

五、快捷键要有作用域

Ctrl/Cmd+B、I、U 等快捷键只在编辑器或工具栏拥有焦点时处理,不能拦截整个页面的按键。链接弹窗打开后,键盘焦点应进入第一个输入框,Escape 关闭并把焦点还给触发按钮或编辑器。

editor.addEventListener('keydown', (event) => {
  if (!(event.ctrlKey || event.metaKey)) return;
  if (event.key.toLowerCase() === 'b') {
    event.preventDefault();
    restoreSelection();
    commands.bold();
  }
});

六、链接和弹窗的细节

插入链接时保存原选区,选择已有链接时读出 href 与文字。提交前校验协议,避免把 javascript: 当作普通链接写入。取消操作不能改变正文;确认后恢复选区、插入内容并重新同步工具栏。

对于读屏用户,命令执行后通过 aria-live 简短播报“已加粗”或“已插入链接”。不要每次按键都播报,只在命令确实改变文档时反馈。

七、移动端与输入法

移动端浏览器可能收起软键盘并改变 selection,工具栏按钮需要更长的事件处理窗口。输入法组合期间不要立即执行格式命令,至少等 compositionend 后再读取选区,避免把组合中的字符拆开。

八、测试重点

覆盖空选区、跨段选区、点击工具栏后选区是否恢复、命令状态同步、撤销重做、链接取消、Esc 与 Tab 焦点路径以及移动端软键盘变化。把命令适配层和工具栏展示分离后,才能分别测试文档行为与可访问交互。

九、小结

可访问富文本工具栏的核心是保存并恢复选区、用统一命令接口操作文档、从同一状态源同步 aria-pressed、给快捷键限定作用域,并保证弹窗焦点可靠返回。样式只是表层,选区与焦点才是功能稳定的基础。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。