Contribute to this guide

guide定义模型和视图

本指南将向您展示如何为 CKEditor 5 创建一个简单的缩写插件。

我们将创建一个工具栏按钮,允许用户将其文档中的缩写插入到文档中。这些缩写将使用 <abbr> HTML 元素,该元素带有“title”属性,当用户将鼠标悬停在该元素上时,该属性会在工具提示中显示。您可以检查悬停在上一句话中带下划线的“HTML”文本上的机制。

本教程的第一部分将只涵盖基本内容。我们只插入一个可能的缩写:“WYSIWYG”。我们将在本教程系列的下一部分中获取用户输入。

如果您想在深入了解之前查看本教程的最终产品,请查看实时演示

# 让我们开始吧!

设置项目的 easiest 方式是从我们的本教程的 Github 存储库中获取入门文件。我们在那里收集了所有必要的依赖项,包括一些 CKEditor 5 包和其他构建编辑器所需的文件。

编辑器已在 app.js 文件中创建,并包含一些基本插件。您只需克隆存储库,运行 npm install 命令,就可以立即开始编码。

Webpack 也已经配置好了,所以您只需要使用 npm run build 命令来构建您的应用程序。无论何时您想在浏览器中检查任何内容,请保存更改并再次运行构建。然后,刷新浏览器中的页面(记住关闭缓存,以便新的更改立即显示)。

我们的入门文件附带了与编辑器一起使用的CKEditor 5 检查器,因此您可以轻松地调试和观察模型和视图层中发生的事情。它将为您提供有关编辑器状态的大量有用信息,例如内部数据结构、选择、命令等等。

如果您想自己设置项目,则应按照“快速入门”部分中列出的步骤操作。此外,您需要安装@ckeditor/ckeditor5-core 包(其中包含 Plugin 类)和@ckeditor/ckeditor5-ui 包(其中包含 UI 库和框架)。

您对插件的入口点是 app.js

// app.js

import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
import Heading from '@ckeditor/ckeditor5-heading/src/heading';
import List from '@ckeditor/ckeditor5-list/src/list';
import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';

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

现在看一下 index.html。我们添加了 <abbr> 元素 - 它现在还不能用,但我们将在几个步骤中修复它。

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <title>CKEditor 5 Framework – Abbreviation plugin</title>
    </head>
    <body>
        <div id="editor">
 			<h2>Abbreviation plugin</h2>
            <p>CKEditor5 is a modern, feature-rich, world-class <abbr title="What You See Is What You Get">WYSIWYG</abbr> editor.</p>
        </div>

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

# 插件结构

我们的缩写插件分为三个组件 - AbbreviationAbbreviationUIAbbreviationEditing

  • AbbreviationEditing 将在模型中启用缩写属性并引入正确的模型 ←→ 视图转换。
  • AbbreviationUI 将负责 UI - 工具栏按钮。
  • Abbreviation 将是将 UI 和编辑结合在一起的粘合剂。

我们将它们放在 /abbreviation 目录中。我们将在本教程的下一部分中添加更多文件。这是我们的目录结构

├── app.js
├── dist
│   ├── bundle.js
│   └── bundle.js.map
├── index.html
├── node_modules
├── package.json
├── abbreviation
│   ├── abbreviation.js
│   ├── abbreviationediting.js
│   └── abbreviationui.js
└── webpack.config.js

看一下这 3 个组件,它们已经定义并导入到 app.js 中。

AbbreviationUI:

// abbreviation/abbreviationui.js

import Plugin from '@ckeditor/ckeditor5-core/src/plugin';

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

AbbreviationEditing:

// abbreviation/abbreviationediting.js

import Plugin from '@ckeditor/ckeditor5-core/src/plugin';

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

Abbreviation:

// abbreviation/abbreviation.js

import AbbreviationEditing from './abbreviationediting';
import AbbreviationUI from './abbreviationui';
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';

export default class Abbreviation extends Plugin {
    static get requires() {
        return [ AbbreviationEditing, AbbreviationUI ];
    }
}

现在我们需要在 app.js 文件中加载 Abbreviation 插件。编辑器将自行加载 AbbreviationUIAbbreviationEditing 组件,因为它们是我们“粘合剂”插件所需要的。

// app.js

import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
import Heading from '@ckeditor/ckeditor5-heading/src/heading';
import List from '@ckeditor/ckeditor5-list/src/list';
import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';

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

import Abbreviation from './abbreviation/abbreviation';					// ADDED

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

现在重建项目,刷新浏览器,您应该看到 AbbreviationEditingAbbreviationUI 插件已加载。

# 模型和视图层

CKEditor 5 实现了自己的自定义数据模型,该模型不会与 DOM 一一对应。模型文档被转换为视图,视图表示用户正在编辑的内容。

在继续之前,了解编辑器架构很重要。阅读有关模型视图的更多信息,以便熟悉基本概念。

在视图层中,我们将拥有 <abbr> HTML 元素,并带有一个 title 属性。看看它在检查器中的样子。
Screenshot of a the inspector showing the view layer.

在模型中,像 <abbr> 这样的内联元素表示为属性,而不是单独的元素。为了使我们的插件正常工作,我们需要确保我们可以将缩写属性添加到文本节点。
Screenshot of a the inspector showing the model layer.

# 定义模式

我们可以通过定义模型的模式来做到这一点。通过几行代码,我们将允许所有文本节点接收模型缩写属性。

模式定义了在模型中允许的结构、属性和其他特征。这些信息随后被功能和引擎用来决定如何处理模型,因此您的自定义插件必须具有定义良好的模式。阅读有关编辑引擎架构介绍的更多信息。

我们将使用 schema.extend() 方法扩展文本节点的模式以接受我们的缩写属性。

使用此定义更新 AbbreviationEditing 插件

// abbreviation/abbreviationediting.js

import Plugin from '@ckeditor/ckeditor5-core/src/plugin';

export default class AbbreviationEditing extends Plugin {
    init() {
        this._defineSchema();									// ADDED
    }

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

        // Extend the text node's schema to accept the abbreviation attribute.
        schema.extend( '$text', {
            allowAttributes: [ 'abbreviation' ]
        } );
    }
}

# 定义转换器

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

转换是我们编辑引擎架构中更复杂的话题之一。在继续之前,绝对值得阅读更多有关编辑器中的转换的信息。

我们需要将模型缩写属性转换为视图元素(向下转换)反之亦然(向上转换)。我们可以通过使用我们的转换助手并定义模型和视图在两种转换中应该是什么样子来实现这一点。

转换缩写的完整标题有点棘手,因为我们需要确保其值在模型和视图之间同步。

# 向下转换

在向下转换中,我们将使用我们的转换助手之一 - attributeToElement() - 将模型缩写属性转换为视图 <abbr> 元素。

我们需要使用回调函数,以便获取存储为模型属性值的标题并将其转换为视图元素的 title 值。这里,视图回调的第二个参数是 DowncastConversionApi 对象。我们将使用它的 writer 属性,它将允许我们在向下转换期间操作数据,因为它包含 DowncastWriter 的一个实例。

// abbreviation/abbreviationediting.js

import Plugin from '@ckeditor/ckeditor5-core/src/plugin';

export default class AbbreviationEditing extends Plugin {
    init() {
        this._defineSchema();
        this._defineConverters();							// ADDED
    }

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

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

        // Conversion from a model attribute to a view element.
        conversion.for( 'downcast' ).attributeToElement( {
            model: 'abbreviation',
            // Callback function provides access to the model attribute value
            // and the DowncastWriter.
            view: ( modelAttributeValue, conversionApi ) => {
                const { writer } = conversionApi;

                return writer.createAttributeElement( 'abbr', {
                    title: modelAttributeValue
                } );
            }
        } );

    }
}

# 向上转换

向上转换告诉编辑器视图 <abbr> 元素在模型中应该是什么样子。我们将使用另一个转换助手 - elementToAttribute() - 将其转换。

我们还需要从内容中获取 title 值并在模型中使用它。我们可以通过回调函数来做到这一点,该函数允许我们访问视图元素。

// abbreviation/abbreviationediting.js

import Plugin from '@ckeditor/ckeditor5-core/src/plugin';

export default class AbbreviationEditing extends Plugin {
    init() {
    // Schema and converters initialization.
    // ...
    }

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

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

        conversion.for( 'downcast' ).attributeToElement(
        // Code responsible for downcast conversion.
        // ...
        );

        // Conversion from a view element to a model attribute.
        conversion.for( 'upcast' ).elementToAttribute( {
            view: {
                name: 'abbr',
                attributes: [ 'title' ]
            },
            model: {
                key: 'abbreviation',
                // Callback function provides access to the view element.
                value: viewElement => {
                    const title = viewElement.getAttribute( 'title' );

                    return title;
                }
            }
        } );
    }
}

由于向上转换,我们现在应该可以在 index.html 中添加的缩写了。重建编辑器并自己检查一下。

Screenshot of the editor showing working abbreviation.

# 创建工具栏按钮

现在我们可以使用 ButtonView 类来创建 Abbreviation 工具栏按钮。

我们需要在编辑器的 UI componentFactory 中注册它,以便它可以在工具栏中显示。

// abbreviation/abbreviationui.js

import Plugin from '@ckeditor/ckeditor5-core/src/plugin';

class AbbreviationUI extends Plugin {
    init() {
        const editor = this.editor;

        editor.ui.componentFactory.add( 'abbreviation', () => {
            const button = new ButtonView();

            button.label = 'Abbreviation';
            button.tooltip = true;
            button.withText = true;

            return button;
        } );
    }
}

我们在 componentFactory.add 中传递了按钮的名称,因此它现在可以在工具栏配置中使用。我们现在只需在 app.js 中将其添加到工具栏即可。

// app.js

import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials';
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
import Heading from '@ckeditor/ckeditor5-heading/src/heading';
import List from '@ckeditor/ckeditor5-list/src/list';
import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic';

import Abbreviation from './abbreviation/abbreviation';

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

我们有了按钮,所以让我们定义用户单击它后应该发生什么。

我们将使用 insertContent() 方法将我们的缩写及其标题属性插入文档。在里面,我们只需要使用 writer.createText() 创建一个新的文本节点。

// abbreviation/abbreviationui.js

import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';

class AbbreviationUI extends Plugin {
    init() {
        const editor = this.editor;

        editor.ui.componentFactory.add( 'abbreviation', () => {
            // Previously initialized button view.
            // ...

            this.listenTo( button, 'execute', () => {
                const selection = editor.model.document.selection;
                const title = 'What You See Is What You Get';
                const abbr = 'WYSIWYG';

                // Change the model to insert the abbreviation.
                editor.model.change( writer => {
                    editor.model.insertContent(
                        // Create a text node with the abbreviation attribute.
                        writer.createText( abbr, { abbreviation: title } )
                    );
                } );
            } );

            return button;
        } );
    }
}

# 演示

缩写插件

CKEditor 5 是一款现代、功能丰富、世界一流的 WYSIWYG 编辑器。

# 最终代码

如果您在任何地方迷路,这是 插件的最终实现。您可以将来自不同文件的代码粘贴到您的项目中,或者克隆并安装整个项目,它将开箱即用。

下一步是什么?

本教程的第一部分到此结束!您的插件现在应该可以工作(至少在最基本的形式中)。继续到 第二部分,您将在其中创建一个带有表单的气球来获取用户的输入,以替换我们硬编码的“WYSIWYG”缩写。