
前端UI组件【免费下载链接】medium-editorMedium.com WYSIWYG editor clone. Uses contenteditable API to implement a rich text solution.项目地址https://gitcode.com/gh_mirrors/me/medium-editor点击查看免费下载导读本文围绕 MediumEditor一个基于contenteditableAPI 实现的 WYSIWYG 富文本编辑器的扩展机制展开以官方 Walkthrough 文档src/js/extensions/WALKTHROUGH-EXTENSION.md为核心骨架完整演示如何通过MediumEditor.Extension.extend()从零构建一个禁用右键上下文菜单的自定义扩展并逐步引入init()生命周期、编辑器元素遍历、DOM 事件绑定与自定义事件订阅等关键能力。读完本文你将掌握 MediumEditor 扩展的定义、注册、初始化、事件处理与清理的完整流程并能独立编写属于自己的编辑器扩展。一、扩展机制速览为什么需要扩展MediumEditor 的所有高级功能几乎都以扩展Extension的形式实现。查看 src/js/extensions/README.md 可以看到工具栏toolbar、自动链接检测auto-link、链接预览气泡anchor-preview、图片拖拽image-dragging、键盘快捷键keyboard-commands、占位符placeholder、粘贴过滤paste等内置能力全部是扩展。扩展可以通过extensions选项传入编辑器也可以与内置扩展同名来替换默认实现。这意味着想要为编辑器添加自定义行为正确的姿势不是去改核心源码而是写一个扩展挂载进去。本文要构建的DisableContextMenuExtension就是最小而完整的范例。二、准备可运行的 Demo 与前置条件Walkthrough 文档对应的完整示例代码位于仓库的 demo/extension-example.html。该页面加载了dist/css/medium-editor.css、dist/css/themes/default.css与dist/js/medium-editor.js并在页面内直接定义了DisableContextMenuExtension扩展。仓库是只读的你只需在浏览器中打开该文件即可交互验证效果。文档给出的方式为file://[Medium Editor Source Root]/demo/extension-example.html打开页面后在.editable区域内右键会发现浏览器默认的上下文菜单不再弹出按 ESC 键可以切换某个编辑元素上是否允许弹出右键菜单详见下文第四步。这份示例中最终形态的扩展代码如下与官方文档最终版一致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) { // If the user hits escape, toggle the>var DisableContextMenuExtension MediumEditor.Extension.extend({ name: disable-context-menu });这里的name是扩展的唯一标识用于后续通过MediumEditor.getExtensionByName(name)获取扩展实例见 API.md。如果不显式设置nameMediumEditor 会用你传入extensions选项时使用的 key 作为扩展名——这一点在 src/js/extension.js 的原型注释中有明确说明也在 spec/extension.spec.js 的测试中得到验证未指定name的扩展会被自动命名为传入时的 key如one、two。extend() 的实现原理extend()的实现位于 src/js/extension.js其思路借鉴了 Backbone / Google Closure 的继承工具如果你的protoProps中定义了constructor新子类就用它作为构造函数否则默认构造函数会调用父类构造函数并把arguments透传过去extension.js第 40-46 行。通过MediumEditor.util.extend(child, parent)把静态方法包括extend本身复制到子类上因此子类还能继续被extend()支持多级继承。使用一个 Surrogate 中间函数建立原型链Surrogate.prototype parent.prototype避免直接调用父构造函数同时把protoProps浅拷贝到子类原型上第 53-61 行。换句话说extend()是一个浅拷贝 原型链的继承封装传入的属性会覆盖原型上的同名属性传入的方法会成为子类实例的方法。把扩展注册进编辑器定义好扩展类后实例化并通过extensions选项注册var editor new MediumEditor(.editable, { extensions: { disable-context-menu: new DisableContextMenuExtension() } });注册的幕后流程在 src/js/core.js 的initExtensions()中编辑器初始化时遍历options.extensions对每个扩展调用initExtension()core.js 第 311-331 行。initExtension()会先注入三个默认属性window来自options.contentWindowdocument来自options.ownerDocumentbase指向当前 MediumEditor 实例随后调用扩展的init()方法最后在扩展没有name时补上注册 key。这解释了为什么在扩展的init()里可以直接使用this.base、this.window、this.document——它们是由编辑器在初始化时注入的。源码证据上述注入逻辑见 src/js/core.js测试见 spec/extension.spec.js校验base为 MediumEditor 实例与 spec/extension.spec.js校验window/document来自contentWindow/ownerDocument选项。四、第二步接入contextmenu事件 —— 理解init()与助手方法定义好的扩展目前只是个空壳要让它在编辑器初始化时真正干点事需要实现init()方法。MediumEditor 在 setup 阶段会对每一个扩展调用init()因此它是扩展初始化的标准入口var 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) { } });这段代码用到了扩展的三个核心助手助手说明this.getEditorElements()返回当前编辑器实例维护的所有elementscontenteditable 元素数组。其实现就是一行return this.base.elements;见 src/js/extension.js。this.base当前 MediumEditor 实例的引用。用它可以在扩展内调用编辑器公开方法例如this.base.saveSelection()、this.base.execAction()等。this.base.on()MediumEditor 提供的 DOM 事件绑定方法见 API.md#ontarget-event-listener-usecapture。与原生addEventListener的区别在于通过它注册的事件处理器会在 MediumEditor 被destroy()时自动解除避免内存泄漏。为什么优先用this.on()而不是this.base.on()Walkthrough 文档特别强调了一个便利点MediumEditor 为扩展提供了一批代理方法proxy methods它们直接转发到base实例上的同名方法。其中就包括on()。也就是说this.on(...)与this.base.on(...)完全等价见 src/js/extension.js 中on、off、subscribe、trigger、execAction的批量代理定义。因此后续步骤中的代码都统一使用更简洁的this.on(element, contextmenu, ...)写法。init()被调用时发生了什么从 src/js/core.js 可以看出init()被调用之前base、window、document等属性已经注入完成所有代理助手方法也已就位所以init()里可以放心使用它们。这保证了扩展的初始化代码拥有与编辑器实例一致的运行环境例如 iframe 场景下ownerDocument/contentWindow指向的是 iframe 内的 document 与 window。五、第三步添加实际功能 —— 阻止默认行为前面两步只建立了事件绑定框架handleContextmenu还是空函数。现在补上核心逻辑当contextmenu事件触发时调用preventDefault()阻止浏览器默认上下文菜单出现var 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) { event.preventDefault(); } });到这里一个禁用右键菜单的扩展已经可以工作了编辑器初始化后对每个 contenteditable 元素绑定contextmenu监听事件发生时调用preventDefault()浏览器不再弹出右键菜单。需要注意的是这里使用event.preventDefault()而非event.stopPropagation()——前者阻止默认行为但不阻断事件冒泡不会影响页面上其他监听器后者则会中断事件传播。在富文本编辑器场景中避免无谓地截断事件流是良好的实践。六、第四步利用自定义事件实现按 ESC 切换开关光能禁用右键菜单还不够有代表性。Walkthrough 文档的第四个步骤把扩展升级为一个可按需开关的功能当用户按下 ESCAPE 时为当前编辑元素切换一个data-allow-context-menu属性contextmenu触发时只有在该属性不存在的情况下才阻止默认菜单。要实现这个交互需要两块拼图监听keydown事件借助内置自定义事件editableKeydown见 CUSTOM-EVENTS.md#editablekeydown。它是原生keydown事件在编辑元素上的代理封装回调签名为listener(data, editable)其中editable是当前触发事件的 contenteditable 元素——这正是我们需要的当前编辑元素。按属性条件阻止在handleContextmenu中检查event.currentTarget是否带有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) { // If the user hits escape, toggle the>destroy: function () { this.getEditorElements().forEach(function (element) { element.removeAttribute(data-allow-context-menu); }, this); }destroy()被调用的行为在 spec/extension.spec.js 有测试覆盖编辑器destroy()后扩展的destroy()必然被调用。八、扩展的完整工具箱助手方法与代理方法Walkthrough 只用了getEditorElements()、base、on()、subscribe()但掌握完整工具箱能让你写出更强大的扩展。以下是 src/js/extension.js 与 src/js/extensions/README.md 中定义的完整清单助手属性初始化时由编辑器注入属性含义对应选项baseMediumEditor 实例引用自动注入window内容窗口引用contentWindow选项OPTIONS.mddocument宿主文档引用ownerDocument选项OPTIONS.md助手方法方法返回用途getEditorElements()ArrayHTMLElement编辑器维护的所有 contenteditable 元素getEditorId()Number当前编辑器实例的唯一 IDgetEditorOption(option)任意读取初始化时传入的某个选项值代理方法直接转发到 base 实例方法转发目标execAction(action, opts)MediumEditor.execAction()API.md#execactionaction-optson(target, event, listener, useCapture)MediumEditor.on()off(target, event, listener, useCapture)MediumEditor.off()API.md#offtarget-event-listener-usecapturesubscribe(name, listener)MediumEditor.subscribe()API.md#subscribename-listenertrigger(name, data, editable)MediumEditor.trigger()API.md#triggername-data-editable代理的实现方式非常直接在 src/js/extension.jsMediumEditor 将execAction、on、off、subscribe、trigger这五个方法名逐一挂到Extension.prototype上每个都是return this.base[helper].apply(this.base, arguments)的转发。这也是在扩展内使用this.on(...)等价于this.base.on(...)的代码级根源。可选状态接口面向工具栏交互除上述工具外扩展接口还定义了一组可选的生命周期钩子详见 src/js/extensions/README.mdcheckState(node)选区状态变化时从选区节点沿祖先链逐级向上调用queryCommandState()/isAlreadyApplied(node)/isActive()/setActive()/setInactive()用于让扩展尤其是按钮类扩展感知并反映当前选区是否已应用某格式getInteractionElements()返回扩展渲染的可交互元素使编辑器把这些元素内的点击视为编辑器内交互而不触发blurspec/extension.spec.js 有专门测试验证该行为。如果你的扩展只是像本文示例一样做全局行为控制拦截事件、切换属性实现init()和可选的destroy()就足够了若要往工具栏里加按钮则需进一步参考 src/js/extensions/WALKTHROUGH-BUTTON.md。九、测试与验证仓库为扩展机制提供了完整的单元测试见 spec/extension.spec.js其中与本例直接相关的验证点包括扩展的base属性被设置为 MediumEditor 实例第 27-37 行未显式指定name的扩展被自动命名为注册 key第 54-71 行扩展被注入window/document对应contentWindow/ownerDocument选项第 73-110 行destroy()被编辑器destroy()调用第 112-125 行助手方法on/off/subscribe/execAction/trigger均正确转发到 base 实例第 227-251 行getEditorId()/getEditorElements()/getEditorOption()返回正确的编辑器状态第 253-286 行。验证方式是运行npm install后执行测试套件项目使用 Karma Jasmine配置文件见 karma.conf.js 与 karma.dev.conf.js构建任务见 Gruntfile.js。十、小结与进一步探索通过DisableContextMenuExtension这个由浅入深的例子我们完整走通了 MediumEditor 自定义扩展的四个关键阶段定义MediumEditor.Extension.extend({ name: ... })创建扩展类extensions选项注册实例初始化实现init()借助注入的base/window/document与助手方法getEditorElements()遍历编辑器元素功能通过this.on()绑定原生 DOM 事件contextmenu并利用event.preventDefault()拦截默认行为联动通过this.subscribe(editableKeydown, ...)订阅自定义事件结合MediumEditor.util.isKey()与keyCode.ESCAPE实现按键驱动的开关逻辑。这套模式可以推广到几乎所有编辑器自定义需求拦截粘贴editablePaste、监听内容变化editableInput、感知焦点focus/blur、响应外部点击externalInteraction等自定义事件均已在 CUSTOM-EVENTS.md 中列出可供扩展内部直接订阅。若要深入了解扩展接口的完整定义推荐继续阅读 src/js/extensions/README.md、src/js/extensions/WALKTHROUGH-BUTTON.md 以及扩展基类源码 src/js/extension.js。赞分享前端UI组件【免费下载链接】medium-editorMedium.com WYSIWYG editor clone. Uses contenteditable API to implement a rich text solution.项目地址https://gitcode.com/gh_mirrors/me/medium-editor点击查看免费下载相关推荐JupyterLab扩展开发实战从零构建自定义插件JupyterLab扩展开发实战从零构建自定义插件 本文深入探讨JupyterLab扩展开发的完整流程从架构设计理念到实际开发部署。首先解析JupyterL前端后端数据科学开发工具Umi插件开发实战从零构建自定义功能扩展Umi插件开发实战从零构建自定义功能扩展 本文全面介绍了Umi插件开发的全过程从基础环境配置、插件API与生命周期钩子解析到preset umi预设插件集前端Web框架CLI构建工具Faker扩展开发实战从零开始创建一个自定义数据生成器Faker扩展开发实战从零开始创建一个自定义数据生成器 Faker是一款强大的PHP库专为生成逼真的假数据而设计。本指南将带你从零开始创建一个自定义的Fa上一篇FastAPI-Utils 会话管理指南使用 FastAPISessionMaker 优化 SQLAlchemy 集成下一篇重构效率革命Python-Rope核心功能全解析与实战指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考