现在很多编辑器都有斜杠命令。用户按下 /,弹出一个列表,然后插入标题、列表、图片、按钮、卡片。
表面上看,Odoo 里的 Powerbox 好像也只是这样:
- 打开一个菜单;
- 上下选择;
- 回车执行。
但如果你去读 powerbox_plugin.js、search_powerbox_plugin.js、user_command_plugin.js,就会发现官方真正设计的不是“菜单组件”,而是一套编辑器命令层。
它至少拆成了四件事:
- 先有
user_commands这种抽象命令; - 再把命令映射成
powerbox_items; - 用分类、可用性和关键字组织展示层;
- 最后才由 overlay 里的 Powerbox 组件负责交互呈现。
也就是说,Powerbox 的关键不是“弹窗长什么样”,而是:
编辑器怎样把可执行能力变成可搜索、可过滤、可上下文感知的命令系统。
一、UserCommandPlugin 说明 Odoo 先定义“命令”,再考虑 UI
user_command_plugin.js 非常短,但非常关键。
它把 user_commands 收集成一个只读命令字典,每个命令至少有:
idrun- 可选的
title descriptioniconisAvailable
这意味着 Odoo 的第一层抽象不是菜单项,而是用户命令。
这是个很成熟的设计,因为命令本身应该先独立于展示形式存在。否则你很快就会把执行逻辑死绑在某个按钮或菜单上。
一旦命令被抽象出来,它就可以被:
- toolbar 调用;
- slash 命令调用;
- 搜索型命令调用;
- 未来别的入口复用。
二、PowerboxPlugin 做的不是注册命令,而是“把命令翻译成可展示可执行的候选项”
PowerboxPlugin 的资源里有三类内容特别关键:
user_commandspowerbox_categoriespowerbox_items
这里的设计非常漂亮。
user_commands 是能力定义层
决定命令能做什么。
powerbox_items 是展示映射层
它通过 commandId 引用某个命令,再补上:
categoryId- 可能覆盖的
title descriptioniconkeywordscommandParams- 额外
isAvailable
powerbox_categories 是信息架构层
它决定命令如何分组、如何排序。
这说明 Powerbox 不是把命令和 UI 强耦合,而是插了一个很关键的中间层:
同一个命令,可以按当前场景被组织成不同的 Powerbox 展示项。
三、makePowerboxCommands() 体现了“继承 + 覆盖”的命令组装思路
makePowerboxCommands() 会:
- 取出所有
powerbox_items - 根据
commandId找到原始 user command - 继承
title/description/icon - 再允许 item 覆盖部分属性
- 用 category 词典补上
categoryName - 把
command.run(item.commandParams, context)封成最终执行器 - 把
command.isAvailable和item.isAvailable叠加成联合判断
这套设计特别适合编辑器生态。
因为很多时候你真正想复用的不是“某个菜单项”,而是:
- 同一条基础命令;
- 在不同类目里不同命名;
- 在不同上下文里带不同默认参数;
- 甚至只在部分选区里可用。
Powerbox 正好提供了这个翻译层。
四、为什么 isAvailable 特别重要
Powerbox 不是一个静态功能目录。
getAvailablePowerboxCommands() 会读取当前编辑选区,再结合:
- blacklist selector
- 命令本身的
isAvailable - item 额外的
isAvailable
最后过滤出当前真能执行的命令。
这点很关键,因为编辑器命令天然是上下文敏感的:
- 某些命令只在正文块里可用;
- 某些命令在特定节点里禁止;
- 某些命令需要当前选区满足结构条件。
所以 Powerbox 的本质不是“把所有功能列出来”,而是:
只在当前上下文里暴露真正成立的命令空间。
五、overlay 在这里不是视觉容器,而是命令交互壳
PowerboxPlugin 通过 overlay.createOverlay(Powerbox) 创建展示壳,并把响应式 state 传进去:
commandscurrentIndexshowCategories
随后 openPowerbox() / updatePowerbox() / closePowerbox() 控制这层壳体。
也就是说,Powerbox 组件自身主要负责:
- 渲染命令列表;
- 高亮当前项;
- 滚动对齐;
- 把点击 / 鼠标悬停转成激活。
真正重要的命令组装、筛选和执行策略,都在插件层。
这正是好架构该有的样子:
- UI 管显示;
- 插件管命令逻辑。
六、SearchPowerboxPlugin 说明“/”不是打开菜单,而是进入命令搜索模式
很多人会把 slash command 误解成:
- 输入
/ - 打开一个菜单
- 完。
但 search_powerbox_plugin.js 显示 Odoo 做得更细。
它在:
beforeinput_handlersinput_handlersdelete_handlerspost_undo_handlerspost_redo_handlers
这些事件上都挂了逻辑。
当用户输入 / 时,它会:
- 记录历史 savepoint;
- 打开 Powerbox;
- 记下 slash 所在 offset;
- 后续持续判断当前 selection 是否还处于“搜索态”;
- 从
/到当前光标之间截出searchTerm; - 用
fuzzyLookup在可用命令中实时筛选; - 命令执行时把 searchTerm 写进 context,并恢复 savepoint。
这说明 slash command 的本质不是“菜单快捷键”,而是:
在正文输入流里临时开启一段命令检索会话。
七、为什么要有 savepoint restore
这部分特别体现编辑器的精细程度。
用户输入 /hea 去搜 heading,本质上只是为了选命令,不是为了把 /hea 这串字符永久留在文档里。
所以 onBeforeInput 遇到 / 时会做 history.makeSavePoint(),后面命令真正应用时再通过 historySavePointRestore?.() 把这段搜索输入回滚掉。
这一步非常关键。
否则 slash 搜索就会污染正文历史,导致:
- 文档里残留搜索词;
- undo 栈混进一堆临时字符;
- 命令执行前后内容边界变脏。
也就是说,Powerbox 不只是可搜索,还把搜索过程和最终内容变更分得很清楚。
八、模糊搜索不是锦上添花,而是命令层的可发现性保障
filterCommands(searchTerm) 里用的是 fuzzyLookup,而且匹配维度不只标题,还包括:
titlecategoryNamedescriptionkeywords
这非常重要。
因为命令系统如果只靠精确名称,扩展一多就会变得很难找。Powerbox 把搜索维度做宽,等于在提升:
- 命令可发现性;
- 新手可用性;
- 插件命令的暴露机会。
所以它不是装饰功能,而是命令平台能不能规模化的关键。
九、二开里最容易犯的几个误区
误区 1:把 slash command 当成一个菜单组件改皮肤
这样会只看到 UI,忽略底下真正的命令注册与可用性体系。
误区 2:直接在 Powerbox 组件里塞业务逻辑
命令能力应该优先落在 user_commands / powerbox_items,而不是硬写在展示层。
误区 3:忽略 isAvailable
结果命令在不该出现的上下文里也能弹出来,用户一用就报错。
误区 4:把搜索输入当正式内容
没有 savepoint restore,slash 搜索体验很快就会变得很脏。
十、结论
Odoo 编辑器里的 Powerbox,核心从来不是一个“漂亮的斜杠菜单”。
从源码设计看,它真正做的是:
- 用
UserCommandPlugin抽象命令; - 用
powerbox_items把命令映射成当前入口的展示候选; - 用
isAvailable把命令空间收束到当前上下文; - 用 overlay 承载交互壳;
- 用
SearchPowerboxPlugin把/输入流变成临时命令搜索会话; - 用 savepoint 把搜索过程与最终内容改动分开。
所以更准确的理解应该是:
Powerbox 不是菜单组件,而是 Odoo HTML 编辑器里的命令发现与执行系统。
理解了这一点,你在做编辑器命令扩展时,就不会只想着“加个条目”,而会更自然地去设计:
- 命令抽象层;
- 上下文可用性;
- 搜索关键字;
- 以及执行前后对正文与历史的影响。
DISCUSSION
评论区