能否创建仅含单个容器的CKEditor 5 Widget?
CKEditor5 单容器结构转Widget解决方案
问题背景
需要将以下单容器HTML转成CKEditor5 Widget,要求保留Widget自带的「前后添加空行」按钮和黄色边框:
<div class="widget-tip"> <p> Tip:<br> lorem ipsum </p> </div>
目前已知只有外层widget-tip容器调用toWidget(),同时内层新增容器调用toWidgetEditable()才能实现需求,但现有系统数据库里已有大量无内层容器的旧模板,不想修改数据库结构,尝试自动添加内层容器失败,现有插件代码如下:
import { Plugin } from 'ckeditor5/src/core'; import { Widget, toWidget, toWidgetEditable } from 'ckeditor5/src/widget'; export default class WidgetTip extends Plugin { static get requires() { return [Widget]; } init() { this._defineSchema(); this._defineConverters(); } _defineSchema() { const schema = this.editor.model.schema; schema.register('widgetTip', { inheritAllFrom: '$blockObject' }); schema.register('widgetTipInner', { isLimit: true, allowIn: 'widgetTip', allowContentOf: '$root' }); } _defineConverters() { const conversion = this.editor.conversion; // <widgetTip> converters conversion.for('upcast').elementToElement({ model: 'widgetTip', view: { name: 'div', classes: 'widget-tip' } }); conversion.for('dataDowncast').elementToElement({ model: 'widgetTip', view: { name: 'div', classes: 'widget-tip' } }); conversion.for('editingDowncast').elementToElement({ model: 'widgetTip', view: (modelElement, { writer: viewWriter }) => { const div = viewWriter.createContainerElement('div', { class: 'widget-tip' }); return toWidget(div, viewWriter, { label: 'simple widgetTip widget' }); } }); // <widgetTipInner> converters conversion.for('upcast').elementToElement({ model: 'widgetTipInner', view: { name: 'div', classes: 'widget-tip__inner' } }); conversion.for('dataDowncast').elementToElement({ model: 'widgetTipInner', view: { name: 'div', classes: 'widget-tip__inner' } }); conversion.for('editingDowncast').elementToElement({ model: 'widgetTipInner', view: (modelElement, { writer: viewWriter }) => { const div = viewWriter.createEditableElement('div', { class: 'widget-tip__inner' }); return toWidgetEditable(div, viewWriter); } }); } }
解决方案
核心思路是:编辑器内部使用双层结构(外层Widget+内层Editable)以保留交互功能,但数据上下转换时自动处理单/双层结构的映射,无需修改数据库。具体修改如下:
1. 调整Schema定义
确保widgetTip只能包含widgetTipInner,避免结构混乱:
_defineSchema() { const schema = this.editor.model.schema; schema.register('widgetTip', { inheritAllFrom: '$blockObject', // 限制只能包含widgetTipInner allowChildren: 'widgetTipInner' }); schema.register('widgetTipInner', { isLimit: true, allowIn: 'widgetTip', allowContentOf: '$root' }); }
2. 修改转换逻辑
重点处理**upcast(HTML转Model)和data downcast(Model转存储用HTML)**的逻辑:
- Upcast时,自动将单容器内的所有内容包裹到
widgetTipInner节点中 - Data downcast时,将Model的双层结构还原为单容器结构,保证存储的HTML和旧数据一致
- Editing downcast保持双层结构,确保编辑器内的Widget交互正常
修改后的完整_defineConverters方法:
_defineConverters() { const conversion = this.editor.conversion; // --- Upcast: 单容器HTML转Model双层结构 --- conversion.for('upcast').elementToStructure({ view: { name: 'div', classes: 'widget-tip' }, model: (viewElement, { writer: modelWriter }) => { // 创建外层widgetTip节点 const widgetTip = modelWriter.createElement('widgetTip'); // 创建内层widgetTipInner节点 const inner = modelWriter.createElement('widgetTipInner'); modelWriter.append(inner, widgetTip); // 将原单容器内的所有子节点迁移到inner中 for (const child of viewElement.getChildren()) { const modelChild = conversion.convertItem(child); if (modelChild) { modelWriter.append(modelChild, inner); } } return widgetTip; } }); // --- Data Downcast: Model双层结构转单容器HTML(保持存储结构不变) --- conversion.for('dataDowncast').elementToElement({ model: 'widgetTip', view: (modelElement, { writer: viewWriter, conversion }) => { const outerDiv = viewWriter.createContainerElement('div', { class: 'widget-tip' }); // 获取内层inner节点 const innerModel = modelElement.getChild(0); // 将inner内的内容直接转换到外层div中 conversion.convertChildren(innerModel, outerDiv); return outerDiv; } }); // --- Editing Downcast: Model双层结构转编辑器内的Widget结构 --- conversion.for('editingDowncast').elementToElement({ model: 'widgetTip', view: (modelElement, { writer: viewWriter }) => { const outerDiv = viewWriter.createContainerElement('div', { class: 'widget-tip' }); return toWidget(outerDiv, viewWriter, { label: '提示框Widget' }); } }); // --- widgetTipInner的转换 --- // Upcast不需要单独处理(因为单容器的upcast已经创建了inner) // Data Downcast不需要生成inner节点(直接合并到外层) conversion.for('editingDowncast').elementToElement({ model: 'widgetTipInner', view: (modelElement, { writer: viewWriter }) => { const innerDiv = viewWriter.createEditableElement('div', { class: 'widget-tip__inner' }); return toWidgetEditable(innerDiv, viewWriter); } }); }
关键说明
- Upcast阶段:通过
elementToStructure手动处理节点,把原单容器的内容全部转移到widgetTipInner里,自动补全Model的双层结构 - Data Downcast阶段:跳过
widgetTipInner节点,直接把内层内容输出到外层widget-tip容器中,确保存储的HTML和旧数据完全一致 - Editing Downcast阶段:保持双层结构,外层是Widget(带边框和添加空行按钮),内层是可编辑区域,保证编辑器内的交互体验
内容的提问来源于stack exchange,提问作者Hannes Kirsman
相关产品推荐
相关产品推荐

