Medium Editor 扩展体系完全指南:Extension、Button 与 Form Extension 的接口、生命周期与实战

发布时间:2026/9/21 2:48:22
Medium Editor 扩展体系完全指南:Extension、Button 与 Form Extension 的接口、生命周期与实战 前端UI组件【免费下载链接】medium-editorMedium.com WYSIWYG editor clone. Uses contenteditable API to implement a rich text solution.项目地址https://gitcode.com/gh_mirrors/me/medium-editor点击查看免费下载本篇技术指南以 Medium Editor 官方扩展文档为核心系统讲解其扩展体系的三层抽象通用Extension、与工具栏建立契约的Button Extension、以及负责收集用户输入的Form Extension。你将掌握扩展的注册方式、全部生命周期方法与辅助工具、按钮状态的判定机制并通过两个官方 Walkthrough禁用右键菜单扩展与高亮按钮获得可直接落地的实战代码。全文结合 src/js/extensions/ 目录下的真实源码与 demo/ 示例页面确保每个结论都有实现依据。扩展体系总览三层抽象Medium Editor 的所有自定义能力都建立在同一个入口上通过extensions选项传入扩展实例。官方文档将其划分为三个层次Extension通用扩展任意自定义动作或命令可替换同名内置按钮也可为编辑器增加全新功能Button按钮扩展与工具栏有明确契约的一类扩展负责在工具栏渲染可点击元素、在点击时对选中文本执行动作、并根据用户选区更新自身外观Form Extension表单扩展Button 的子类在工具栏内收集用户输入如锚点 URL、字号继承 Button 的全部生命周期方法并额外提供表单显示/隐藏等公共能力。三者之间是严格的继承关系源码中可见Button MediumEditor.Extension.extend(...)见 src/js/extensions/button.js而FormExtension MediumEditor.extensions.button.extend(...)见 src/js/extensions/form.js。内置扩展扩展体系本身就是编辑器的骨架官方文档特别强调整个 Medium Editor 工具栏本身就是一个扩展。以下内置功能全部以扩展形式实现内置扩展源码文件功能Toolbarsrc/js/extensions/toolbar.js承载所有按钮的整个工具栏Auto-Linksrc/js/extensions/auto-link.js自动检测 URL 并将其转换为a锚点Anchor Previewsrc/js/extensions/anchor-preview.js悬停链接时显示 href 提示气泡File Draggingsrc/js/extensions/file-dragging.js允许将文件拖拽进编辑器旧版 image-dragging 已移至 src/js/extensions/deprecated/image-dragging.jsKeyboard Commandssrc/js/extensions/keyboard-commands.js将键盘快捷键映射到各类命令Placeholdersrc/js/extensions/placeholder.js编辑器为空时显示占位文本Pastesrc/js/extensions/paste.js过滤并处理粘贴进编辑器的内容Anchor表单src/js/extensions/anchor.js通过工具栏表单收集 URL 并创建/解除链接FontSize表单betasrc/js/extensions/fontsize.js通过工具栏表单修改选中文字的字号从源码结构看凡是需要与工具栏交互的内置功能都被设计为扩展这使得核心编辑器保持精简而功能全部可插拔、可替换。什么是 Button与工具栏的契约Button 是特定类型的 Extension它与工具栏之间存在明确的契约。只要扩展实现了getButton()方法且其名称出现在toolbar.buttons选项中工具栏就会将其视为按钮扩展。这一契约赋予自定义按钮三类能力在工具栏中渲染一个元素可点击的按钮/链接点击时对编辑器文本执行动作如 bold、italic、blockquote根据用户选区更新元素外观选区已是粗体时激活否则未激活。内置按钮只是不同配置的 Button 扩展官方文档指出所有内置按钮都是带有不同配置的 Button 扩展。这些配置集中定义在 src/js/defaults/buttons.js共 25 个内置按钮bold、italic、underline、strikethrough、subscript、superscript、image、quote、pre、orderedlist、unorderedlist、indent、outdent、justifyLeft、justifyCenter、justifyRight、justifyFull、h1~h6、removeFormat、html。以bold为例其完整配置如下见 src/js/defaults/buttons.jsbold: { name: bold, action: bold, aria: bold, tagNames: [b, strong], style: { prop: font-weight, value: 700|bold }, useQueryState: true, contentDefault: bB/b, contentFA: i classfa fa-bold/i }这里的name就是工具栏配置中的按钮标识。工具栏在创建时遍历toolbar.buttons数组对每个名称调用base.addBuiltInExtension(buttonName, buttonOpts)获取扩展实例再调用其getButton()方法取回 DOM 元素追加到工具栏见 src/js/extensions/toolbar.js。什么是 Form Extension在工具栏内收集输入Form Extension 是特殊的 Button Extension它从 Button 继承全部生命周期方法同时获得与编辑器交互的附加方法用于在工具栏中渲染表单控件。两个内置表单扩展Anchor Buttonsrc/js/extensions/anchor.js点击后弹出 URL 输入框含可选复选框将选中文本转为链接若选区本身已是链接点击则解除链接执行unlink。其源码handleClick中通过getClosestTag(..., a)判断选区是否已在链接内见 src/js/extensions/anchor.js。FontSize Buttonbetasrc/js/extensions/fontsize.js点击后显示字号滑块range 输入范围 1~7修改选中文本的字号源码中createForm()构建了滑块、保存与关闭按钮见 src/js/extensions/fontsize.js。Form Extension 的公共能力见 src/js/extensions/form.jsformSaveLabel/formCloseLabel表单保存/关闭按钮的默认文本✓与×activeClass表单显示时附加的类名默认medium-editor-toolbar-form-activehasForm设为true时工具栏创建时会调用getForm()并将返回的表单追加到工具栏容器内getForm()返回将被追加到工具栏的表单 DOM 元素默认隐藏isDisplayed()/showForm()/hideForm()表单显隐控制showToolbarDefaultActions()/hideToolbarDefaultActions()隐藏表单时恢复/隐藏工具栏默认按钮组setToolbarPosition()根据工具栏内容与选区位置更新工具栏尺寸和位置。Extension 接口核心生命周期方法以下方法是 Medium Editor 在内部与扩展交互时会调用/使用的契约。它们的默认实现与文档注释均可在 src/js/extension.js 中找到。namestring扩展的唯一标识用于MediumEditor.getExtensionByName(name)获取扩展实例。若未定义Medium Editor 会将其设置为extensions选项中传入时的键名。var MyExtension MediumEditor.Extension.extend({ name: myextension }); var myExt new MyExtension(); var editor new MediumEditor(.editor, { extensions: { myextension: myExt } }); editor.getExtensionByName(myextension) myExt; // trueinit()在 Medium Editor 初始化期间被调用。调用时.base属性当前 MediumEditor 实例引用已被设置所有辅助方法也已就绪。源码中其默认实现为空函数见 src/js/extension.js。checkState(node)若实现该方法每当编辑器与工具栏状态更新后它会被调用一次或多次。状态更新时编辑器执行以下流程找到包含当前选区的父节点对该节点调用每个扩展的checkState(node)取上一个节点的父节点重复步骤 2、3直到移出父级 contenteditable。参数nodeNode——选区变化时位于选区祖先链上、当前被检查的节点。官方示例根据选区是否位于带自定义data-edited属性的元素内为编辑器元素添加/移除 CSS 类var EditedExtension MediumEditor.Extension.extend({ name: edited, checkState: function (node) { // checkState 在一次选区变化中会被多次调用 // 因此只在找到属性时才保存值 if (!this.foundAttribute node.getAttribute(data-edited)) { this.foundAttribute true; } // 向上遍历到容器元素时说明祖先链遍历完毕 // 此时可以添加/移除 css 类 if (MediumEditor.util.isMediumEditorElement(node)) { if (this.foundAttribute) { node.classList.add(edited-text); } else { node.classList.remove(edited-text); } // 确保该属性不会被持久化到下一次选区更新 delete this.foundAttribute; } } }); var editedExt new EditedExtension(); var editor new MediumEditor(.editor, { extensions: { edited: editedExt } });destroy()在 Medium Editor 被销毁调用MediumEditor.destroy()时被调用用于移除创建的 HTML、自定义事件处理器或执行其他清理任务。queryCommandState()在编辑器/工具栏状态更新时对每个扩展调用一次。若返回非null值扩展将不再参与 DOM 祖先链的爬升检查若返回true且扩展定义了setActive()Medium Editor 会调用setActive()。返回boolean或null。内置按钮扩展的默认实现是仅当useQueryState为true时调用document.queryCommandState(action)否则返回null见 src/js/extensions/button.js。getInteractionElements()若扩展渲染了用户可交互的元素应实现此方法并返回根元素或包含所有根元素的数组。Medium Editor 在交互时调用它判断用户点击是否发生在编辑器之外这些元素会被用来检查点击目标是否为扩展元素的子孙节点从而把对扩展元素的交互也视为对编辑器的交互避免误触发blur。工具栏扩展正是通过返回整个工具栏元素来保证点击工具栏不会导致失焦见 src/js/extensions/toolbar.js。isActive()返回按钮是否已被设为激活。若返回true该扩展/按钮将跳过激活状态检查若返回falseisAlreadyApplied()仍会随祖先链爬升被逐一调用。返回boolean。isAlreadyApplied(node)与checkState()类似在状态变化后随 DOM 爬升被反复调用用于判断扩展是否已应用于当前节点。注意若已实现checkState()此方法不会被调用若queryCommandState()已实现且返回非null此方法也不会被调用。返回boolean。setActive()/setInactive()setActive()当 Medium Editor 确认扩展当前已启用时调用目前仅在状态更新且queryCommandState()或isAlreadyApplied(node)返回true时触发setInactive()当扩展未应用于当前选区时调用目前每次编辑器/工具栏状态变化开始时都会调用。之后 Medium Editor 会尝试通过checkState()或queryCommandState()、isAlreadyApplied(node)、isActive()、setActive()的组合来更新扩展状态。工具栏在状态更新时的真实调用链可在 src/js/extensions/toolbar.js 中看到setToolbarButtonStates()先对所有扩展调用setInactive()随后checkActiveButtons()优先使用queryCommandState()无法使用浏览器原生查询的扩展则加入manualStateChecks沿选区父节点逐级向上调用isAlreadyApplied()。Extension Helpers内置辅助工具以下辅助属性/方法由 Medium Editor 在初始化时设置或直接路由到 MediumEditor 实例。Helper类型说明baseMediumEditor当前 MediumEditor 实例引用如this.base.saveSelection()保存选区windowWindow内容窗口引用对应contentWindow选项如this.window.innerWidthdocumentDocument所属文档引用对应ownerDocument选项如this.document.createElement(button)getEditorElements()方法返回本实例监视的元素数组ArrayHTMLElement底层即this.base.elementsgetEditorId()方法返回本 MediumEditor 实例的唯一数字标识底层即this.base.idgetEditorOption(option)方法返回初始化 MediumEditor 时使用的某个选项值底层即this.base.options[option]文档中各 Helper 的源码级实现均可在 src/js/extension.js 中找到。getEditorElements()的官方示例——Placeholder 扩展的destroy()方法为所有编辑元素移除占位属性MediumEditor.extensions.placeholder MediumEditor.Extension.extend({ // ... destroy: function () { this.getEditorElements().forEach(function (el) { if (el.getAttribute(data-placeholder) this.text) { el.removeAttribute(data-placeholder); } }, this); }, // ... });getEditorOption()的官方示例——Anchor 扩展根据buttonLabels选项决定表单保存按钮的显示fontawesome图标或默认文本MediumEditor.extensions.anchor MediumEditor.extensions.form.extend({ // ... getTemplate: function () { var template [ input typetext classmedium-editor-toolbar-input placeholder, this.placeholderText, ]; template.push( a href# classmedium-editor-toolbar-save, this.getEditorOption(buttonLabels) fontawesome ? i classfa fa-check/i : this.formSaveLabel, /a ); // ... }, // ... });Extension Proxy Methods直通 MediumEditor 实例以下方法是对既有 MediumEditor 函数的直接代理调用。它们的实现方式是在Extension.prototype上为每个方法名生成转发函数见 src/js/extension.js[execAction, on, off, subscribe, trigger].forEach(function (helper) { Extension.prototype[helper] function () { return this.base[helper].apply(this.base, arguments); }; });方法对应 MediumEditor 方法典型用途execAction(action, opts)MediumEditor.execAction()执行命令如this.execAction(bold)on(target, event, listener, useCapture)MediumEditor.on()绑定 DOM 事件销毁时自动解绑off(target, event, listener, useCapture)MediumEditor.off()解绑 DOM 事件subscribe(name, listener)MediumEditor.subscribe()订阅自定义事件trigger(name, data, editable)MediumEditor.trigger()触发自定义事件官方示例——Anchor Preview 扩展用on()为链接绑定mouseout见 src/js/extensions/anchor-preview.jshandleEditableMouseover: function (event) { // ... this.instanceHandleAnchorMouseout this.handleAnchorMouseout.bind(this); this.on(this.anchorToPreview, mouseout, this.instanceHandleAnchorMouseout); // ... }官方示例——Keyboard Commands 扩展在init()中订阅editableKeydown自定义事件见 src/js/extensions/keyboard-commands.jsinit: function () { MediumEditor.Extension.prototype.init.apply(this, arguments); this.subscribe(editableKeydown, this.handleKeydown.bind(this)); // ... }官方示例——Toolbar 扩展隐藏工具栏时触发hideToolbar自定义事件hideToolbar: function () { if (this.isDisplayed()) { this.getToolbarElement().classList.remove(medium-editor-toolbar-active); this.trigger(hideToolbar, {}, this.base.getFocusedElement()); } }Button 接口与配置项详解getButton()唯一将扩展定义为Button Extension的方法。只要扩展名出现在toolbar.buttons选项中且实现了getButton()工具栏就会按toolbar.buttons中指定的顺序依次调用各按钮的getButton()并将返回的HTMLElement追加到工具栏见 src/js/extensions/toolbar.js。Button 配置项可复用/可覆写以下属性均来自内置按钮扩展实现MediumEditor.extensions.buttonsrc/js/extensions/button.js自定义按钮可直接继承并覆写属性类型说明actionstring点击时传给MediumEditor.execAction()的动作参数同时写入按钮的data-action属性ariastring同时作为按钮的aria-label与title属性值tagNamesArray表示按钮已应用的元素标签名数组命中则按钮显示激活useQueryState为true时不生效styleObject表示按钮已应用的 CSS 属性与值对prop为属性名value为属性值多值用\|分隔useQueryState为true时不生效useQueryStateboolean是否用document.queryCommandState()判断动作是否已应用如queryCommandState(bold)contentDefaultstring按钮默认 innerHTMLcontentFAstringbuttonLabels选项为fontawesome时使用的 innerHTMLclassListArray要添加到按钮的类名数组attrsObject要添加到按钮的自定义属性键值对handleClick(event)function按钮点击事件监听器默认实现调用this.execAction(action)源码中createButton()的完整构建逻辑见 src/js/extensions/button.js它会添加medium-editor-action与medium-editor-action-{name}类、写入data-action、设置 title/aria-label、合并自定义classList与attrs并根据buttonLabels选择contentFA或contentDefault。各配置项的官方示例——定义名为custom-button-extension的按钮扩展var CustomButtonExtension MediumEditor.extensions.button.extend({ name: custom-button-extension, action: bold, // 点击执行 execAction(bold) aria: bold text, // aria-label 与 title 均为 bold text useQueryState: false, // 不使用浏览器原生状态查询 tagNames: [b, strong], // 选区位于 b/strong 内时按钮激活 style: { // 或 font-weight 为 700/bold 时按钮激活 prop: font-weight, value: 700|bold }, contentDefault: bH/b, // 默认内容 contentFA: i classfa fa-paint-brush/i, // fontawesome 内容 classList: [custom-button, custom-extension], // 追加类名 attrs: { data-is-custom: true }, // 追加自定义属性 handleClick: function (event) { var action prompt(Please enter an action, bold); if (action) { this.execAction(action); } } });实战一构建通用扩展DisableContextMenuExtension官方 Walkthroughsrc/js/extensions/WALKTHROUGH-EXTENSION.md演示了如何构建一个禁用右键菜单的扩展完整示例位于 demo/extension-example.html可在浏览器中通过file://[Medium Editor Source Root]/demo/extension-example.html加载体验。1. 定义扩展调用MediumEditor.Extension.extend()并传入要覆写的方法/属性var DisableContextMenuExtension MediumEditor.Extension.extend({ name: disable-context-menu }); var editor new MediumEditor(.editable, { extensions: { disable-context-menu: new DisableContextMenuExtension() } });2. 绑定 contextmenu 事件实现init()方法每个扩展在 Medium Editor 初始化时都会被调用遍历所有编辑元素绑定contextmenuvar DisableContextMenuExtension MediumEditor.Extension.extend({ name: disable-context-menu, init: function () { this.getEditorElements().forEach(function (element) { this.base.on(element, contextmenu, this.handleContextmenu.bind(this)); }, this); }, handleContextmenu: function (event) { } });这里用到了三个 HelpergetEditorElements()获取编辑器维护的所有元素数组base引用 MediumEditor 实例base.on()保证事件处理器在编辑器销毁时自动解绑。文档还特别提示on()也有直通代理方法可直接写作this.on(element, contextmenu, ...)。3. 添加功能阻止默认行为即可禁用右键菜单handleContextmenu: function (event) { event.preventDefault(); }4. 结合自定义事件实现开关需求升级用户按 ESCAPE 时对特定元素切换禁用/启用。实现要点监听每个元素上的keydown可复用内置的editableKeydown自定义事件利用事件监听器的第二个参数当前活动的编辑元素切换data-allow-context-menu属性contextmenu触发时仅当data-allow-context-menu属性不存在时才阻止菜单。var DisableContextMenuExtension MediumEditor.Extension.extend({ name: disable-context-menu, init: function () { this.getEditorElements().forEach(function (element) { this.on(element, contextmenu, this.handleContextmenu.bind(this)); }, this); this.subscribe(editableKeydown, this.handleKeydown.bind(this)); }, handleContextmenu: function (event) { if (!event.currentTarget.getAttribute(data-allow-context-menu)) { event.preventDefault(); } }, handleKeydown: function (event, editable) { // 用户按 ESCAPE 时切换>var HighlighterButton MediumEditor.Extension.extend({ name: highlighter }); var editor new MediumEditor(.editable, { toolbar: { buttons: [bold, italic, underline, highlighter] }, extensions: { highlighter: new HighlighterButton() } });注意要让工具栏寻找并添加按钮扩展名必须出现在toolbar.buttons数组中。2. 创建并显示按钮实现init()创建按钮元素、getButton()作为按钮访问器。工具栏会在扩展创建完成后遍历toolbar.buttons对每个名称取出扩展并检查是否实现了getButton()若实现则将返回的元素追加到工具栏var HighlighterButton MediumEditor.Extension.extend({ name: highlighter, init: function () { this.button this.document.createElement(button); this.button.classList.add(medium-editor-action); this.button.innerHTML bH/b; }, getButton: function () { return this.button; } });运行后选中文本工具栏会出现 Bold、Italic、Underline 与自定义 Highlighter 四个按钮。3. 美化外观fontawesome 图标 提示var HighlighterButton MediumEditor.Extension.extend({ name: highlighter, init: function () { this.button this.document.createElement(button); this.button.classList.add(medium-editor-action); this.button.innerHTML i classfa fa-paint-brush/i; this.button.title Highlight; }, getButton: function () { return this.button; } }); var editor new MediumEditor(.editable, { toolbar: { buttons: [bold, italic, underline, highlighter] }, buttonLabels: fontawesome, // 为其他按钮启用 font-awesome 图标 extensions: { highlighter: new HighlighterButton() } });4. 处理点击借助 rangy 实现高亮使用开源库 rangy 的 CSS Class Applier 模块包裹选区。init()中创建 Class Applier生成带highlight类的mark元素通过this.on()绑定点击事件点击时调用toggleSelection()rangy.init(); var HighlighterButton MediumEditor.Extension.extend({ name: highlighter, init: function () { this.classApplier rangy.createClassApplier(highlight, { elementTagName: mark, normalize: true }); this.button this.document.createElement(button); this.button.classList.add(medium-editor-action); this.button.innerHTML i classfa fa-paint-brush/i; this.button.title Highlight; this.on(this.button, click, this.handleClick.bind(this)); }, getButton: function () { return this.button; }, handleClick: function (event) { this.classApplier.toggleSelection(); // 通知编辑器 html 可能已变化保证外部依赖如 textarea 同步感知 this.base.checkContentChanged(); } });两个关键点toggleSelection()的便利之处在于它会自动取消包裹——再次点击同一选中区域时mark元素会被移除this.base.checkContentChanged()直接调用核心编辑器通知内容变化。当传入textarea作为编辑元素时编辑器依赖editableInput事件保持 textarea 与生成的div同步。5. 响应选区激活/未激活状态实现 4 个扩展方法让按钮外观随选区变化isAlreadyApplied: function (node) { return node.nodeName.toLowerCase() mark; }, isActive: function () { return this.button.classList.contains(medium-editor-button-active); }, setInactive: function () { this.button.classList.remove(medium-editor-button-active); }, setActive: function () { this.button.classList.add(medium-editor-button-active); }isAlreadyApplied(node)随选区父节点祖先链逐级调用任一节点是mark即返回true选区已高亮isActive()返回按钮当前是否已激活检查medium-editor-button-active类setActive()/setInactive()添加/移除激活类。6. 复用内置按钮代码推荐大量内置按钮逻辑是重复的因此更优做法是继承MediumEditor.extensions.button实现见 src/js/extensions/button.js代码量大幅缩减rangy.init(); var HighlighterButton MediumEditor.extensions.button.extend({ name: highlighter, tagNames: [mark], // isAlreadyApplied() 命中这些 nodeName 时按钮激活 contentDefault: bH/b, // 按钮默认 innerHTML contentFA: i classfa fa-paint-brush/i, // fontawesome 模式下的 innerHTML aria: Highlight, // 同时作为 aria-label 与 title action: highlight, // 用作按钮的>赞分享前端UI组件【免费下载链接】medium-editorMedium.com WYSIWYG editor clone. Uses contenteditable API to implement a rich text solution.项目地址https://gitcode.com/gh_mirrors/me/medium-editor点击查看免费下载相关推荐KubeSphere frontend-forge 扩展全生命周期管理InstallPlan 与 Extension 操作权威指南KubeSphere frontend forge 扩展全生命周期管理InstallPlan 与 Extension 操作权威指南 导读 frontend f云原生容器编排后端微服务多集群DevOps可观测性AI 技能OHIF Viewers 扩展系统完全指南从 Extension 骨架、插件注册到模块与生命周期钩子OHIF Viewers 扩展系统完全指南从 Extension 骨架、插件注册到模块与生命周期钩子 OHIF v3 将扩展系统重新设计为「扩展Extens医疗健康前端音视频Elementor Editor V2 包体系深度指南微前端架构、init() 生命周期与扩展实战Elementor Editor V2 包体系深度指南微前端架构、 init 生命周期与扩展实战 Elementor 的 Editor V2 是一套以 ReaCMS前端后端低代码上一篇猫抓视频嗅探工具三秒破解网页视频下载难题的终极解决方案下一篇OpenIM imctl 命令行控制工具 RFC 提案深度解析设计动机、功能规划与仓库现状创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考