Contribute to this guide

指南实现块状小部件

在本教程中,您将学习如何实现更复杂的 CKEditor 5 插件。

您将构建一个“简单框”功能,允许用户将带有标题和正文字段的自定义框插入文档中。您将使用小部件实用程序并使用模型-视图转换来正确设置此功能的行为。稍后,您将创建一个 UI,允许使用工具栏按钮将新的简单框插入文档中。

如果您想在深入研究之前查看本教程的最终产品,请查看演示

# 开始之前

本教程将参考您在学习过程中遇到的CKEditor 5 架构部分的各个部分。虽然阅读这些部分不是完成本教程的必要条件,但建议您在某些时候阅读这些指南,以更好地理解本教程中使用的机制。

如果您想为小部件触发的事件使用自己的事件处理程序,您必须将其包装在具有data-cke-ignore-events属性的容器中,以将其从编辑器的默认处理程序中排除。有关更多详细信息,请参阅从默认处理程序中排除 DOM 事件

# 让我们开始

最简单的入门方法是使用以下命令获取入门项目。

npx -y degit ckeditor/ckeditor5-tutorials-examples/block-widget/starter-files block-widget
cd block-widget

npm install
npm run dev

这将在名为block-widget的新目录中创建必要的文件。npm install命令将安装所有依赖项,npm run dev将启动开发服务器。

带有基本插件的编辑器是在main.js文件中创建的。

打开终端中显示的 URL。如果一切正常,您应该在浏览器中看到一个像这样的 CKEditor 5 实例

Screenshot of a classic editor initialized from source.

# 插件结构

编辑器启动并运行后,您可以开始实现插件。您可以将整个插件代码保存在单个文件中,但是建议将它的“编辑”和“UI”层拆分,并创建一个加载两者的主插件。这样,您可以确保更好地分离关注点,并允许重新组合功能(例如,选择现有功能的编辑部分,但编写自己的 UI)。所有官方 CKEditor 5 插件都遵循这种模式。

此外,您将命令、按钮和其他“自包含”组件的代码拆分到单独的文件中。为了避免将这些文件与项目的main.js文件混淆,请创建以下目录结构

├── main.js
├── index.html
├── node_modules
├── package.json
├── simplebox
│   ├── simplebox.js
│   ├── simpleboxediting.js
│   └── simpleboxui.js
└─ ...

现在定义 3 个插件。

首先是主(粘合)插件。它的作用只是加载“编辑”和“UI”部分。

// simplebox/simplebox.js

import SimpleBoxEditing from './simpleboxediting';
import SimpleBoxUI from './simpleboxui';
import { Plugin } from 'ckeditor5';

export default class SimpleBox extends Plugin {
    static get requires() {
        return [ SimpleBoxEditing, SimpleBoxUI ];
    }
}

现在,剩下的两个插件

// simplebox/simpleboxui.js

import { Plugin } from 'ckeditor5';

export default class SimpleBoxUI extends Plugin {
    init() {
        console.log( 'SimpleBoxUI#init() got called' );
    }
}
// simplebox/simpleboxediting.js

import { Plugin } from 'ckeditor5';

export default class SimpleBoxEditing extends Plugin {
    init() {
        console.log( 'SimpleBoxEditing#init() got called' );
    }
}

最后,您需要在main.js文件中加载SimpleBox插件

// main.js

import SimpleBox from './simplebox/simplebox';                                 // ADDED

ClassicEditor
    .create( document.querySelector( '#editor' ), {
        plugins: [
            Essentials, Paragraph, Heading, List, Bold, Italic,
            SimpleBox                                                          // ADDED
        ],
        toolbar: [ 'heading', 'bold', 'italic', 'numberedList', 'bulletedList' ]
    } );

您的页面将刷新,您应该看到SimpleBoxEditingSmpleBoxUI插件已加载

Screenshot of a classic editor initialized from source with the “SimpleBoxEditing#init() got called” and “SimpleBoxUI#init() got called” messages on the console.

# 模型和视图层

CKEditor 5 实现了一个 MVC 架构,它的自定义数据模型虽然仍然是树结构,但并没有与 DOM 一对一映射。您可以将模型视为编辑器内容的语义表示,而 DOM 是其可能的表示之一。

了解有关编辑引擎架构的更多信息。

由于您的简单框功能旨在成为一个带有标题和描述字段的框,因此请像这样定义它的模型表示

<simpleBox>
    <simpleBoxTitle></simpleBoxTitle>
    <simpleBoxDescription></simpleBoxDescription>
</simpleBox>

# 定义模式

您需要从定义模型的模式开始。您需要定义 3 个元素及其类型,以及允许的父/子元素。

了解有关模式的更多信息。

使用此定义更新SimpleBoxEditing插件。

// simplebox/simpleboxediting.js

import { Plugin } from 'ckeditor5';

export default class SimpleBoxEditing extends Plugin {
    init() {
        console.log( 'SimpleBoxEditing#init() got called' );

        this._defineSchema();                                                  // ADDED
    }

    _defineSchema() {                                                          // ADDED
        const schema = this.editor.model.schema;

        schema.register( 'simpleBox', {
            // Behaves like a self-contained block object (e.g. a block image)
            // allowed in places where other blocks are allowed (e.g. directly in the root).
            inheritAllFrom: '$blockObject'
        } );

        schema.register( 'simpleBoxTitle', {
            // Cannot be split or left by the caret.
            isLimit: true,

            allowIn: 'simpleBox',

            // Allow content which is allowed in blocks (i.e. text with attributes).
            allowContentOf: '$block'
        } );

        schema.register( 'simpleBoxDescription', {
            // Cannot be split or left by the caret.
            isLimit: true,

            allowIn: 'simpleBox',

            // Allow content which is allowed in the root (e.g. paragraphs).
            allowContentOf: '$root'
        } );
    }
}

定义模式目前不会对编辑器有任何影响。这是一个信息,插件和编辑器引擎可以使用它来了解如何处理按下Enter键、单击元素、键入文本、插入图像等操作的行为。

为了使简单框插件开始执行任何操作,您需要定义模型-视图转换器。现在就做吧!

# 定义转换器

转换器告诉编辑器如何将视图转换为模型(例如,加载数据到编辑器或处理粘贴内容)以及如何将模型渲染到视图(用于编辑目的,或者检索编辑器数据时)。

了解有关编辑器中的转换的更多信息。

现在您需要考虑如何将<simpleBox>元素及其子元素渲染到 DOM(用户将看到的内容)和数据中。CKEditor 5 允许将模型转换为不同的结构以供编辑,并转换为不同的结构以存储为“数据”或在复制粘贴内容时与其他应用程序交换。但是,为了简单起见,现在在两个管道中都使用相同的表示。

您要实现的视图中的结构

<section class="simple-box">
    <h1 class="simple-box-title"></h1>
    <div class="simple-box-description"></div>
</section>

使用conversion.elementToElement()方法定义所有转换器。

您可以使用此高级双向转换器定义,因为您为数据编辑管道定义了相同的转换器。

稍后,您将切换到更细粒度的转换器,以获得对转换的更多控制。

您需要为 3 个模型元素定义转换器。使用此代码更新SimpleBoxEditing插件

// simplebox/simpleboxediting.js

import { Plugin } from 'ckeditor5';

export default class SimpleBoxEditing extends Plugin {
    init() {
        console.log( 'SimpleBoxEditing#init() got called' );

        this._defineSchema();
        this._defineConverters();                                              // ADDED
    }

    _defineSchema() {
        // Previously registered schema.
        // ...
    }

    _defineConverters() {                                                      // ADDED
        const conversion = this.editor.conversion;

        conversion.elementToElement( {
            model: 'simpleBox',
            view: {
                name: 'section',
                classes: 'simple-box'
            }
        } );

        conversion.elementToElement( {
            model: 'simpleBoxTitle',
            view: {
                name: 'h1',
                classes: 'simple-box-title'
            }
        } );

        conversion.elementToElement( {
            model: 'simpleBoxDescription',
            view: {
                name: 'div',
                classes: 'simple-box-description'
            }
        } );
    }
}

有了转换器后,您可以尝试查看简单框的实际效果。您还没有定义将新简单框插入文档的方法,因此请通过编辑器数据加载它。为此,您需要修改index.html文件

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>CKEditor 5 Framework – Implementing a simple widget</title>

        <style>
            .simple-box {
                padding: 10px;
                margin: 1em 0;

                background: rgba( 0, 0, 0, 0.1 );
                border: solid 1px hsl(0, 0%, 77%);
                border-radius: 2px;
            }

            .simple-box-title, .simple-box-description {
                padding: 10px;
                margin: 0;

                background: #FFF;
                border: solid 1px hsl(0, 0%, 77%);
            }

            .simple-box-title {
                margin-bottom: 10px;
            }
        </style>
    </head>
    <body>
        <div id="editor">
            <p>This is a simple box:</p>

            <section class="simple-box">
                <h1 class="simple-box-title">Box title</h1>
                <div class="simple-box-description">
                    <p>The description goes here.</p>
                    <ul>
                        <li>It can contain lists,</li>
                        <li>and other block elements like headings.</li>
                    </ul>
                </div>
            </section>
        </div>

        <script src="dist/bundle.js"></script>
    </body>
</html>

瞧!这是您的第一个简单框实例

Screenshot of a classic editor with an instance of a simple box inside.

# 模型中有什么?

您添加到index.html文件中的 HTML 是您的编辑器数据。这是editor.getData()将返回的内容。此外,目前,这也是 CKEditor 5 引擎在可编辑区域中渲染的 DOM 结构

Screenshot of a DOM structure of the simple box instance – it looks exactly like the data loaded into the editor.

但是,模型中有什么?

要了解这一点,请使用官方的CKEditor 5 检查器。在安装后,您需要在main.js文件中加载它

// main.js

import SimpleBox from './simplebox/simplebox';

import CKEditorInspector from '@ckeditor/ckeditor5-inspector';                 // ADDED

ClassicEditor
    .create( document.querySelector( '#editor' ), {
        plugins: [
            Essentials, Paragraph, Heading, List, Bold, Italic,
            SimpleBox
        ],
        toolbar: [ 'heading', 'bold', 'italic', 'numberedList', 'bulletedList' ]
    } )
    .then( editor => {
        console.log( 'Editor was initialized', editor );

        CKEditorInspector.attach( { 'editor': editor } );

        window.editor = editor;
    } );

刷新页面后,您将看到检查器

Screenshot of a the simple box widget structure displayed by CKEditor 5 inspector.

您将看到以下 HTML 类字符串

<paragraph>[]This is a simple box:</paragraph>
<simpleBox>
    <simpleBoxTitle>Box title</simpleBoxTitle>
    <simpleBoxDescription>
        <paragraph>The description goes here.</paragraph>
        <listItem listIndent="0" listType="bulleted">It can contain lists,</listItem>
        <listItem listIndent="0" listType="bulleted">and other block elements like headings.</listItem>
    </simpleBoxDescription>
</simpleBox>

如您所见,此结构与 HTML 输入/输出完全不同。如果您仔细观察,您还会注意到第一段中的[]字符 - 这是选择位置。

使用编辑器功能(粗体、斜体、标题、列表、选择)进行一些操作,以查看模型结构如何变化。

您还可以使用一些有用的助手,如getData()setData()来了解有关编辑器模型状态的更多信息,或在测试中编写断言。

# 将简单框转换为小部件之前的行为

现在该检查简单框是否按您期望的方式运行了。您可以观察到以下情况

  • 您可以在标题中键入文本。按下Enter不会拆分它,Backspace不会完全删除它。这是因为它在模式中被标记为isLimit元素。
  • 您无法在标题中应用列表,也无法将其转换为标题(除了<h1 class="simple-box-title">,它已经是这样了)。这是因为它只允许其他块元素(如段落)中允许的内容。但是,您可以在标题中应用斜体(因为斜体在其他块中是允许的)。
  • 描述的行为与标题类似,但它允许更多内容在其中 - 列表和其他标题。
  • 如果您尝试选择整个简单框实例并按下Delete,它将作为一个整体被删除。复制粘贴它时也是如此。这是因为它在模式中被标记为isObject元素。
  • 您无法通过单击轻松地选择整个简单框实例。此外,当您将鼠标悬停在它上面时,光标指针不会改变。换句话说,它看起来有点“死”了。这是因为您还没有定义视图行为。

到目前为止,非常棒,对吧?只需少量代码,您就可以定义简单框插件的行为,该插件可以维护这些元素的完整性。引擎确保用户不会破坏这些实例。

看看您还能改进什么。

# 将简单框转换为小部件

在 CKEditor 5 中,小部件系统主要由引擎处理。其中一些包含在 (@ckeditor/ckeditor5-widget) 包中,而其他一些则必须由 CKEditor 5 框架提供的其他实用程序处理。

因此,CKEditor 5 的实现对扩展和重新组合是开放的。您可以选择您想要的行为(就像您在本教程中通过定义模式所做的那样),并跳过其他行为或自己实现它们。

您定义的转换器将模型<simpleBox*> 元素转换为视图中的普通 ContainerElement(以及在向上转换期间返回)。

您想稍微更改此行为,以便在编辑视图中创建的结构使用 toWidget()toWidgetEditable() 实用程序进行增强。但是,您不想影响数据视图。因此,您需要分别为编辑和数据向下转换定义转换器。

如果您发现向下转换和向上转换的概念令人困惑,请阅读 转换简介

现在是时候重新审视您之前定义的 _defineConverters() 方法了。您将使用 elementToElement() 向上转换助手elementToElement() 向下转换助手,而不是双向 elementToElement() 转换器助手。

此外,您需要确保 Widget 插件已加载。如果您省略它,视图中的元素将具有所有类(如 ck-widget),但不会加载任何“行为”(例如,单击小部件不会选择它)。

// simplebox/simpleboxediting.js

// ADDED 2 imports.
import { Plugin, Widget, toWidget, toWidgetEditable } from 'ckeditor5';

export default class SimpleBoxEditing extends Plugin {
    static get requires() {                                                    // ADDED
        return [ Widget ];
    }

    init() {
        console.log( 'SimpleBoxEditing#init() got called' );

        this._defineSchema();
        this._defineConverters();
    }

    _defineSchema() {
        // Previously registered schema.
        // ...
    }

    _defineConverters() {                                                      // MODIFIED
        const conversion = this.editor.conversion;

        // <simpleBox> converters.
        conversion.for( 'upcast' ).elementToElement( {
            model: 'simpleBox',
            view: {
                name: 'section',
                classes: 'simple-box'
            }
        } );
        conversion.for( 'dataDowncast' ).elementToElement( {
            model: 'simpleBox',
            view: {
                name: 'section',
                classes: 'simple-box'
            }
        } );
        conversion.for( 'editingDowncast' ).elementToElement( {
            model: 'simpleBox',
            view: ( modelElement, { writer: viewWriter } ) => {
                const section = viewWriter.createContainerElement( 'section', { class: 'simple-box' } );

                return toWidget( section, viewWriter, { label: 'simple box widget' } );
            }
        } );

        // <simpleBoxTitle> converters.
        conversion.for( 'upcast' ).elementToElement( {
            model: 'simpleBoxTitle',
            view: {
                name: 'h1',
                classes: 'simple-box-title'
            }
        } );
        conversion.for( 'dataDowncast' ).elementToElement( {
            model: 'simpleBoxTitle',
            view: {
                name: 'h1',
                classes: 'simple-box-title'
            }
        } );
        conversion.for( 'editingDowncast' ).elementToElement( {
            model: 'simpleBoxTitle',
            view: ( modelElement, { writer: viewWriter } ) => {
                // Note: You use a more specialized createEditableElement() method here.
                const h1 = viewWriter.createEditableElement( 'h1', { class: 'simple-box-title' } );

                return toWidgetEditable( h1, viewWriter );
            }
        } );

        // <simpleBoxDescription> converters.
        conversion.for( 'upcast' ).elementToElement( {
            model: 'simpleBoxDescription',
            view: {
                name: 'div',
                classes: 'simple-box-description'
            }
        } );
        conversion.for( 'dataDowncast' ).elementToElement( {
            model: 'simpleBoxDescription',
            view: {
                name: 'div',
                classes: 'simple-box-description'
            }
        } );
        conversion.for( 'editingDowncast' ).elementToElement( {
            model: 'simpleBoxDescription',
            view: ( modelElement, { writer: viewWriter } ) => {
                // Note: You use a more specialized createEditableElement() method here.
                const div = viewWriter.createEditableElement( 'div', { class: 'simple-box-description' } );

                return toWidgetEditable( div, viewWriter );
            }
        } );
    }
}

如您所见,代码变得更加冗长且更长。这是因为您使用了较低级别的转换器。我们计划在将来提供更多方便的小部件转换实用程序。阅读更多信息(和 👍) 此票证

# 将简单框转换为小部件后的行为

现在,您应该看到您的简单框插件发生了什么变化。

Screenshot of the widget focus outline.

您应该观察到

  • <section><h1><div> 元素具有 contentEditable 属性(以及一些类)。此属性告诉浏览器元素是否被视为可编辑的。将元素通过 toWidget() 传递将使其内容不可编辑。相反,通过 toWidgetEditable() 传递它将使其内容再次可编辑。
  • 您现在可以单击小部件(灰色区域)来选择它。一旦选中,它就更容易进行复制粘贴。
  • 小部件及其嵌套的可编辑区域对悬停、选择和焦点(轮廓)做出反应。

换句话说,简单框实例变得更具响应性。

此外,如果您调用 editor.getData(),您将获得与将简单框转换为小部件之前相同的 HTML。这是由于仅在 editingDowncast 管道中使用 toWidget()toNestedEditable()

现在,您只需要从模型和视图层中获得这些。就可编辑性和数据输入/输出而言,它完全可以工作。现在找到一种方法将新的简单框插入文档!

# 创建命令

一个 命令 是一个动作和一个状态的组合。您可以通过它们公开的命令与大多数编辑器功能进行交互。这不仅允许执行这些功能(例如,使文本片段变为粗体),还可以检查此操作是否可以在选择的当前位置执行,以及观察其他状态属性(例如,当前选定的文本是否已变为粗体)。

对于简单框,情况很简单

  • 您需要一个“插入新简单框”动作。
  • 您需要一个“您可以在此处(在当前选择位置)插入新的简单框”检查。

simplebox/ 目录中创建一个名为 insertsimpleboxcommand.js 的新文件。您将使用 model.insertObject() 方法,该方法将能够(例如,如果您尝试在段落的中间插入一个简单框,则拆分段落(这在模式中不允许)。

// simplebox/insertsimpleboxcommand.js

import { Command } from 'ckeditor5';

export default class InsertSimpleBoxCommand extends Command {
    execute() {
        this.editor.model.change( writer => {
            // Insert <simpleBox>*</simpleBox> at the current selection position
            // in a way that will result in creating a valid model structure.
            this.editor.model.insertObject( createSimpleBox( writer ) );
        } );
    }

    refresh() {
        const model = this.editor.model;
        const selection = model.document.selection;
        const allowedIn = model.schema.findAllowedParent( selection.getFirstPosition(), 'simpleBox' );

        this.isEnabled = allowedIn !== null;
    }
}

function createSimpleBox( writer ) {
    const simpleBox = writer.createElement( 'simpleBox' );
    const simpleBoxTitle = writer.createElement( 'simpleBoxTitle' );
    const simpleBoxDescription = writer.createElement( 'simpleBoxDescription' );

    writer.append( simpleBoxTitle, simpleBox );
    writer.append( simpleBoxDescription, simpleBox );

    // There must be at least one paragraph for the description to be editable.
    // See https://github.com/ckeditor/ckeditor5/issues/1464.
    writer.appendElement( 'paragraph', simpleBoxDescription );

    return simpleBox;
}

导入命令并在 SimpleBoxEditing 插件中注册它

// simplebox/simpleboxediting.js

import { Plugin, Widget, toWidget, toWidgetEditable } from 'ckeditor5';

import InsertSimpleBoxCommand from './insertsimpleboxcommand';                 // ADDED

export default class SimpleBoxEditing extends Plugin {
    static get requires() {
        return [ Widget ];
    }

    init() {
        console.log( 'SimpleBoxEditing#init() got called' );

        this._defineSchema();
        this._defineConverters();

        // ADDED
        this.editor.commands.add( 'insertSimpleBox', new InsertSimpleBoxCommand( this.editor ) );
    }

    _defineSchema() {
        // Previously registered schema.
        // ...
    }

    _defineConverters() {
        // Previously defined converters.
        // ...
    }
}

您现在可以执行此命令以插入一个新的简单框。调用

editor.execute( 'insertSimpleBox' );

它应该导致

Screenshot of a simple box instance inserted at the beginning of the editor content.

您也可以尝试检查 isEnabled 属性值(或只是在 CKEditor 5 检查器中检查它)

console.log( editor.commands.get( 'insertSimpleBox' ).isEnabled );

它始终为 true,除非选择在一个地方 - 在另一个简单框的标题中。您还可以观察到,当选择在该位置时执行命令不会产生任何影响。

在您继续前进之前,再更改一件事 - 也不允许 simpleBoxsimpleBoxDescription 中。这可以通过 定义自定义子级检查 来完成

// simplebox/simpleboxediting.js

// Previously imported packages.
// ...

export default class SimpleBoxEditing extends Plugin {
    static get requires() {
        return [ Widget ];
    }

    init() {
        console.log( 'SimpleBoxEditing#init() got called' );

        this._defineSchema();
        this._defineConverters();

        this.editor.commands.add( 'insertSimpleBox', new InsertSimpleBoxCommand( this.editor ) );
    }

    _defineSchema() {
        const schema = this.editor.model.schema;

        schema.register( 'simpleBox', {
            // Behaves like a self-contained block object (e.g. a block image)
            // allowed in places where other blocks are allowed (e.g. directly in the root).
            inheritAllFrom: '$blockObject'
        } );

        schema.register( 'simpleBoxTitle', {
            // Cannot be split or left by the caret.
            isLimit: true,

            allowIn: 'simpleBox',

            // Allow content which is allowed in blocks (i.e. text with attributes).
            allowContentOf: '$block'
        } );

        schema.register( 'simpleBoxDescription', {
            // Cannot be split or left by the caret.
            isLimit: true,

            allowIn: 'simpleBox',

            // Allow content which is allowed in the root (e.g. paragraphs).
            allowContentOf: '$root'
        } );

        // ADDED
        schema.addChildCheck( ( context, childDefinition ) => {
            if ( context.endsWith( 'simpleBoxDescription' ) && childDefinition.name == 'simpleBox' ) {
                return false;
            }
        } );
    }

    _defineConverters() {
        // Previously defined converters.
        // ...
    }
}

现在,当选择在另一个简单框实例的描述中时,命令也应该被禁用。

# 创建按钮

现在该允许编辑器用户将小部件插入内容中了。最好的方法是通过工具栏中的 UI 按钮。您可以使用 ButtonView 类(由 CKEditor 5 的 UI 框架 提供)快速创建一个。

按钮应该在单击时执行 命令,如果小部件无法插入选择的某个特定位置(如模式中所定义),则按钮将变为非活动状态。

看看它在实践中的样子,并扩展之前 创建的 SimpleBoxUI 插件

// simplebox/simpleboxui.js

import { ButtonView, Plugin } from 'ckeditor5';

export default class SimpleBoxUI extends Plugin {
    init() {
        console.log( 'SimpleBoxUI#init() got called' );

        const editor = this.editor;
        const t = editor.t;

        // The "simpleBox" button must be registered among the UI components of the editor
        // to be displayed in the toolbar.
        editor.ui.componentFactory.add( 'simpleBox', locale => {
            // The state of the button will be bound to the widget command.
            const command = editor.commands.get( 'insertSimpleBox' );

            // The button will be an instance of ButtonView.
            const buttonView = new ButtonView( locale );

            buttonView.set( {
                // The t() function helps localize the editor. All strings enclosed in t() can be
                // translated and change when the language of the editor changes.
                label: t( 'Simple Box' ),
                withText: true,
                tooltip: true
            } );

            // Bind the state of the button to the command.
            buttonView.bind( 'isOn', 'isEnabled' ).to( command, 'value', 'isEnabled' );

            // Execute the command when the button is clicked (executed).
            this.listenTo( buttonView, 'execute', () => editor.execute( 'insertSimpleBox' ) );

            return buttonView;
        } );
    }
}

您需要做的最后一件事是告诉编辑器在工具栏中显示按钮。为此,您需要稍微修改运行编辑器实例的代码,并将按钮包含在 工具栏配置

ClassicEditor
    .create( document.querySelector( '#editor' ), {
        plugins: [ Essentials, Paragraph, Heading, List, Bold, Italic, SimpleBox ],
        // Insert the "simpleBox" button into the editor toolbar.
        toolbar: [ 'heading', 'bold', 'italic', 'numberedList', 'bulletedList', 'simpleBox' ]
    } )
    .then( editor => {
        // This code runs after the editor initialization.
        // ...
    } )
    .catch( error => {
        // Error handling if something goes wrong during initialization.
        // ...
    } );

刷新网页并自己尝试一下

Screenshot of the simple box widget being inserted using the toolbar button.

# 演示

您可以在下面的编辑器中看到块小部件的实现。如果您想开发自己的块小部件,您还可以查看本教程的完整 源代码

这是一个简单框

框标题

描述在这里。

  • 它可以包含列表,
  • 以及其他块级元素,如标题。

# 最终解决方案

如果您在教程中的任何地方迷路了,或者想直接获得解决方案,则有一个包含 最终项目 的存储库。

npx -y degit ckeditor/ckeditor5-tutorials-examples/block-widget/final-project final-project
cd final-project

npm install
npm run dev