TOAST UI Editor 工具栏定制完全指南:从默认选项到自定义按钮与状态联动
【免费下载链接】tui.editor🍞📝 Markdown WYSIWYG Editor. GFM Standard + Chart & UML Extensible.项目地址: https://gitcode.com/gh_mirrors/tu/tui.editor
TOAST UI Editor(以下简称“编辑器”)以 Markdown 与 WYSIWYG 双模式为核心,而工具栏正是这两类编辑体验的交互枢纽:在 Markdown 语法不直观的所见即所得模式中,绝大多数操作都依赖工具栏完成。本文将围绕官方文档《Toolbar》展开,完整讲解toolbarItems二维数组配置、内建按钮定制、popup弹层选项、el自定义元素、state状态联动与onUpdated回调,并深入仓库源码验证其底层实现,帮助你掌握从“组合默认按钮”到“编写完整自定义工具栏插件”的全套能力。
工具栏选项(Toolbar Option)
编辑器内置了 bold、italic、strike 等总计 16 种工具栏元素。在不指定任何选项时,默认工具栏配置如下:
const options = { // ... toolbarItems: [ ['heading', 'bold', 'italic', 'strike'], ['hr', 'quote'], ['ul', 'ol', 'task', 'indent', 'outdent'], ['table', 'image', 'link'], ['code', 'codeblock'], ['scrollSync'], ], }从示例代码可以看到,编辑器的工具栏选项以二维数组的形式定义:第一层数组的每个元素是一个工具栏分组(group),分组内的工具栏元素则作为内层数组的元素。各元素按照定义顺序在分组内渲染,而工具栏分组之间以|分隔线符号隔开渲染。
16 种内建工具栏元素速查
结合 toolbarItemFactory.ts 中的createDefaultToolbarItemInfo实现,默认支持的 16 种元素及其默认绑定的命令、tooltip 与 state 如下表所示:
| 元素名 | 默认命令(command) | 默认 tooltip(en) | 状态(state) |
|---|---|---|---|
heading | (弹层,插入标题) | Headings | heading |
bold | bold | Bold | strong |
italic | italic | Italic | emph |
strike | strike | Strike | strike |
hr | hr | Line | thematicBreak |
quote | blockQuote | Blockquote | blockQuote |
ul | bulletList | Unordered list | bulletList |
ol | orderedList | Ordered list | orderedList |
task | taskList | Task | taskList |
table | (弹层,插入表格) | Insert table | table |
image | (弹层,插入图片) | Insert image | — |
link | (弹层,插入链接) | Insert link | — |
code | code | Code | code |
codeblock | codeBlock | Insert CodeBlock | codeBlock |
indent | indent | Indent | indent |
outdent | outdent | Outdent | outdent |
scrollSync | toggleScrollSync | — | — |
注:
scrollSync是一个特殊的切换式元素,其内部渲染为复选框 + 开关(switch)而非按钮,相关实现见 toolbarItemFactory.ts;同时它并非总是显示——当编辑器处于 WYSIWYG 模式或预览样式为tab时,scrollSync会被隐藏(见 toolbar.ts)。此外,超过一行显示宽度的元素会自动收进 “More(更多)” 下拉工具栏中(见 toolbar.ts)。
重新组合默认工具栏
如果想修改默认工具栏的构成,只需在创建编辑器时传入toolbarItems选项:
const editor = new Editor({ el: document.querySelector('#editor'), toolbarItems: [ ['heading', 'bold'], ['ul', 'ol', 'task'], ['code', 'codeblock'], ], });执行上面的示例代码后,工具栏会按新配置渲染为三个分组。从源码上看,toolbarItems的类型为(string | ToolbarItemOptions)[][](见 editor.d.ts),即每个元素既可以是内建按钮的字符串名称,也可以是一个自定义选项对象——这正是接下来要讲的定制入口。
工具栏按钮定制(Toolbar Button Customizing)
单纯组合默认元素只能满足基础需求。当需要自己创建并添加工具栏按钮时,编辑器提供两大类型的定制选项:一是复用内建按钮 UI,仅重定义图标、tooltip 或弹层行为;二是完全自定义 DOM 元素。
内建按钮元素定制(Button Element Customizing)
这种方式仍然使用编辑器内建的按钮渲染逻辑,只覆盖按钮的图标、tooltip 或 popup 行为。对应的选项接口如下:
| 名称 | 类型 | 说明 |
|---|---|---|
name | string | 工具栏元素的唯一名称,必填。 |
tooltip | string | 可选。鼠标悬停在工具栏元素上时显示的提示文字。 |
text | string | 可选。需要在工具栏按钮元素上显示的文本。 |
className | string | 可选。应用到工具栏元素上的 class。 |
style | Object | 可选。应用到工具栏元素上的行内样式。 |
command | string | 可选。点击工具栏按钮时要执行的命令。与popup选项互斥。 |
popup | PopupOptions | 可选。点击工具栏按钮时希望弹出的弹层。与command选项互斥。 |
const editor = new Editor({ el: document.querySelector('#editor'), toolbarItems: [ [{ name: 'myItem', tooltip: 'myItem', command: 'bold', text: '@', className: 'toastui-editor-toolbar-icons', style: { backgroundImage: 'none', color: 'red' } }] ], // ... });运行上面的示例后,会生成一个同时应用了className与style的工具栏按钮:按钮包含@文本节点,点击时执行bold命令。
从实现层面看,这类定制走的是 toolbarButton.ts 中的ToolbarButtonComp:按钮渲染为<button>元素,text作为按钮文本,className与style直接拼入 class 与行内样式;点击时如果存在command就调用execCommand(command),否则走弹层分支createPopupInfo(见 toolbarButton.ts)。需要特别注意的是,text只影响按钮文本而不会替换图标字体,示例中通过backgroundImage: 'none'去掉图标字体背景、再用@文本代替图标,是一种常见的定制手法。
popup 选项:自定义弹层
如果希望点击按钮时不是执行命令,而是弹出自己定义的弹层,则使用popup选项。其接口如下:
| 名称 | 类型 | 说明 |
|---|---|---|
body | HTMLElement | 要渲染的弹层 DOM 节点,必填。 |
className | string | 可选。应用到弹层元素上的 class。 |
style | Object | 可选。应用到弹层元素上的样式。 |
配置好的弹层节点会在点击工具栏时自动显示在屏幕上,点击其他区域时自动消失。
以编辑器的 color-syntax(颜色选择)插件代码为参考(对应仓库实现见 color-syntax/src/index.ts 与 index.ts):
const container = document.createElement('div'); // ... const button = createApplyButton(i18n.get('OK')); button.addEventListener('click', () => { // ... eventEmitter.emit('command', 'color', { selectedColor }); eventEmitter.emit('closePopup'); }); container.appendChild(button); const colorPickerToolber = { name: 'color', tooltip: 'Text color', className: 'some class', popup: { className: 'some class', body: container, style: { width: 'auto' }, }, };示例代码将弹层要展示的元素放入变量container。该元素包含一个按钮,点击时执行color命令并关闭弹层。这里展示了自定义弹层与编辑器通信的两条关键途径:
- 需要执行命令时,通过
eventEmitter.emit('command', 'color', { selectedColor })发出command事件; - 需要关闭弹层时,发出
closePopup事件。
从底层实现看,自定义弹层最终会渲染为CustomPopupBody,由createPopupInfo('customPopupBody', ...)处理(见 toolbarItemFactory.ts),而弹层的打开/关闭状态由 Toolbar 组件的showPopup状态统一管理,点击弹层外部区域会触发hidePopup(见 toolbar.ts)。color-syntax 插件的完整示例可对照 color-syntax 插件源码 学习。
工具栏元素定制(Toolbar Item Customizing)
如果不使用内建按钮 UI,而是希望完全自己控制渲染元素,则需要指定el选项:
const myCustomEl = document.createElement('span'); myCustomEl.textContent = '😎'; myCustomEl.style = 'cursor: pointer; background: red;' myCustomEl.addEventListener('click', () => { editor.exec('bold'); }); const editor = new Editor({ el: document.querySelector('#editor'), toolbarItems: [ [{ name: 'myItem', tooltip: 'myItem', el: myCustomEl, }] ], // ... });由于el选项接收的是一个完整的 DOM 元素,因此点击行为、style、class 等都必须由开发者自行设置。运行示例后,工具栏会渲染出自定义的😎元素,点击执行bold命令。
在实现上,带el的工具栏项会渲染为CustomToolbarItem,它把自定义元素appendChild进一个带toolbar-item-wrapperclass 的容器中,并自动为其挂上 tooltip 与 popup 事件监听(见 customToolbarItem.ts)。分组渲染时,ToolbarGroup会依据“是否存在el”来区分渲染CustomToolbarItem还是ToolbarButton(见 toolbarGroup.ts)。
除el外,ToolbarCustomOptions还额外支持hidden、onMounted两个仅在自定义元素下可用的选项(见 ui.d.ts),其中onMounted(execCommand)会在元素挂载完成后被调用,可用来绑定需要执行命令的复杂交互(如 scrollSync 元素的自定义复选框逻辑,见 toolbarItemFactory.ts)。
工具栏状态变更(Change Toolbar Item State)
编辑器可以根据当前光标所在位置对应的节点类型,通过改变工具栏元素样式来“点亮”对应按钮。例如,当光标位于加粗文本strong节点上时,bold工具栏元素会被激活(追加active类)。
若希望自定义的工具栏元素也能随光标位置切换状态,则需要配置state选项:
const editor = new Editor({ el: document.querySelector('#editor'), toolbarItems: [ [{ name: 'myItem', tooltip: 'myItem', command: 'bold', text: '@', className: 'toastui-editor-toolbar-icons', style: { backgroundImage: 'none', color: 'red' }, // 光标位于 `strong` 节点时,该工具栏元素会被追加 'active' 类 state: 'strong', }] ], // ... });当工具栏按钮依据state被激活时,编辑器会为其追加activeCSS 类,之后即可基于该类编写自定义激活样式。按钮激活类拼接发生在 toolbarButton.ts:${item.className || ''}${active ? ' active' : ''}。
state 列表
只有使用下列 state 值,才能改变工具栏元素的激活状态:
heading:标题strong:加粗emph:斜体strike:删除线thematicBreak:水平分隔线blockQuote:引用块bulletList:无序列表orderedList:有序列表taskList:任务列表table:表格code:行内代码codeBlock:代码块
从类型定义看,state的类型是ToolbarStateKeys,即上述键的联合类型,同时底层状态映射ToolbarStateMap还额外包含indent、outdent两项(见 ui.d.ts),与默认元素indent/outdent一一对应。
状态联动的底层原理
工具栏状态不是凭空产生的,而是由 WYSIWYG 编辑器内的 ProseMirror 插件实时计算并广播的。在 toolbarState.ts 中,toolbarStateHighlight插件在每次文档选择(selection)更新时调用getToolbarState(selection, doc, schema),遍历光标范围内的节点:
- 位于
listItem时,根据node.attrs.task判定为taskList或父级列表类型; - 节点类型包含
table时归一化为table; - 对
strong、strike、emph、code四种 mark 类型,检查选区两侧是否存在对应 mark; - 其余节点类型直接作为 state 键,标记为
{ active: true }。
计算结果通过eventEmitter.emit('changeToolbarState', { toolbarState })广播出去。而在 UI 侧,buttonHoc.ts 中每个带state的按钮都会listen('changeToolbarState'),从toolbarState[item.state]中取出{ active, disabled }并更新自身状态——这就是光标移动时按钮实时点亮的完整调用链。
onUpdated() 选项:自定义元素的状态回调
使用el选项创建工具栏元素时,由于编辑器无法直接操纵你传入的 DOM,必须借助onUpdated回调来响应状态变化:
const myCustomEl = document.createElement('span'); myCustomEl.textContent = '😎'; myCustomEl.style = 'cursor: pointer; background: red;' myCustomEl.addEventListener('click', () => { editor.exec('bold'); }); const editor = new Editor({ el: document.querySelector('#editor'), toolbarItems: [ [{ name: 'myItem', tooltip: 'myItem', el: myCustomEl, state: 'strong', onUpdated({ active, disabled }) { if (active) { myCustomEl.style.background = 'green'; } else { myCustomEl.style.background = ''; } } }] ], // ... });onUpdated()接收一个包含active、disabled状态的对象作为参数,可据此为元素追加样式或定义任意行为。其触发时机在 customToolbarItem.ts:当active或disabled与前一次渲染不同时,调用item.onUpdated?.({ active, disabled })。注意disabled与active可能同时出现(例如列表内indent/outdent的禁用态),状态对象类型见 ui.d.ts。
动态增删工具栏项:insertToolbarItem 与 removeToolbarItem
除初始化配置外,编辑器还提供了运行时 API:
editor.insertToolbarItem({ groupIndex, itemIndex }, item):向指定分组的指定位置插入一个工具栏项(字符串或选项对象);editor.removeToolbarItem(itemName):按name移除工具栏项。
两个 API 的类型声明见 editor.d.ts,实现位于 toolbar.ts:insertToolbarItem会先通过createToolbarItemInfo把字符串规范化为选项对象,再在对应分组splice插入;若分组索引越界则新建分组。结合 example15-customizing-toolbar-buttons.html 可以看到完整用法——该示例在初始化工具栏后,又通过insertToolbarItem在第一个分组的最前面动态插入了一个自定义的@按钮(并配合firstclass 实现红色样式)。
完整实战示例
官方提供了可运行的示例 example15-customizing-toolbar-buttons.html,其中综合展示了三种定制手段:
function createLastButton() { const button = document.createElement('button'); button.className = 'toastui-editor-toolbar-icons last'; button.style.backgroundImage = 'none'; button.style.margin = '0'; button.innerHTML = `<i>B</i>`; button.addEventListener('click', () => { editor.exec('bold'); }); return button; } const editor = new toastui.Editor({ el: document.querySelector('#editor'), previewStyle: 'vertical', height: '500px', initialValue: 'The first and last buttons are customized.', toolbarItems: [ ['heading', 'bold', 'italic', 'strike'], ['hr', 'quote'], ['ul', 'ol', 'task', 'indent', 'outdent'], ['table', 'image', 'link'], ['code', 'codeblock'], // 用 el 选项定制“最后一个”按钮 [{ el: createLastButton(), command: 'bold', tooltip: 'Custom Bold' }] ] }); editor.insertToolbarItem({ groupIndex: 0, itemIndex: 0 }, { name: 'myItem', tooltip: 'Custom Button', command: 'bold', text: '@', className: 'toastui-editor-toolbar-icons first', style: { backgroundImage: 'none' } });该示例在编辑器初始化后动态插入“第一个”按钮(使用insertToolbarItem),并在toolbarItems中配置了“最后一个”按钮(使用el自定义元素),配合样式表中.toastui-editor-defaultUI button.first { color: red; }与.last { color: orange; }的定制样式,直观演示了本节所有 API 的组合用法。
小结
工具栏是 TOAST UI Editor 中交互密度最高的组件。通过toolbarItems二维数组可以自由重组 16 种内建元素;通过command/popup选项可以复用内建按钮 UI 定制点击行为;通过el选项可以完全接管渲染元素;通过state+onUpdated可以实现随光标位置实时点亮的动态状态;再配合insertToolbarItem/removeToolbarItem两个运行时 API,即可打造出高度贴合业务场景的定制工具栏。
【免费下载链接】tui.editor🍞📝 Markdown WYSIWYG Editor. GFM Standard + Chart & UML Extensible.项目地址: https://gitcode.com/gh_mirrors/tu/tui.editor
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考