• CKEditor 5 摸爬滚打(三)—— 自定义一个简单的加粗插件(下)


    上一篇文章将加粗插件的架子给搭好了,现在就来完善具体的逻辑,主要的难点在于 model 和转换器 conversion

     

    一、创建一个 Schema

    在 CKEditor 5 中,编辑器实现了自己的一套运行时的编辑内容,即 model,可以打开调试器 CKEditorInspector 查看

    然后编辑器引擎通过转换器 conversion 将 model 渲染成我们熟悉的 HTML

    就拿最基础的段落插件 Paragraph 来说,它最终渲染出来的是一个 <p> 标签,但在 model 中的体现是 <paragraph>,而这 <paragraph> 就是一个 Schema

    CKEditor 5 有三种基本的通用 Schema:$root$block $text,分别指代根节点、块元素、普通文本。

    对于加粗插件,我们也需要先在 editing.js 中注册一个 Schema:

    // 注册 schema
    _defineSchema() {
      const schema = this.editor.model.schema;
    
      schema.register(SCHEMA_NAME__BOLD, {
        isInline: true, // 是否为行内元素
        isObject: true, // 是否为一个整体
        allowWhere: "$text", // 允许在哪个 schema 插入
        allowAttributes: ["value"], // 允许携带哪些属性
      });
    }

    这里的 schema.register() 方法接收的第一个参数是模型名称,类型为字符串,也放到 constant.js 中单独维护

    // constant.js
    export const SCHEMA_NAME__BOLD = 'bold';

    第二个参数是具体的配置项,完整的配置项可以参考官网 SchemaItemDefinition,常用的属性有:

    1. allowIn: String | Array<String> 可以作为哪些 schema 的子节点;

    2. allowWhere: String | Array<String> 从其他 schema 继承 allowIn;

    3. allowAttributes: String | Array<String> 允许携带哪些属性;

    4. isLimit: 设置为 true 时,元素内的所有操作只会修改内容,不会影响元素本身。也就是说该元素无法使用回车键拆分,无法在元素内部使用删除键删除该元素(如果把整个 Molde 理解为一张网页,Limit Element 就相当于 iframe);

    5. isObject: 是否为一个完整对象,通常结合 Widget 使用(完整对象会被整体选中,无法使用回车拆分,无法直接编辑文本);

    6. isBlock: 是否为块元素,类似 HTML 中的块元素;

    7. isInline: 是否为行内元素。但对于 <a> <strong> 这些需要即时编辑的行内标签,在编辑器中以文本属性来区分,所以 isInline 只用于独立的元素,即 isObject 应设置为 true;

    这里为了介绍 Schema,我使用了 isInline 来开发加粗插件,最终的呈现的效果会和平时使用的加粗功能有所区别,但不影响最终提交的数据

     

     

    二、定义转换器 Conversion

    对于上面定义的 Schema,我期望的 Model 是这样的:

    <paragraph>
      hello
      <bold value="world"></bold>
    </paragraph>

    然后通过转换器渲染为:

    <p>hello <strong>world</strong></p>

    转换器分为单向转换器和双向转换器,常用的单向转换器,具体分为两类:

    1. Upcast: 将 HTML 转换为 Model

    2. Downcast: 将 Model 转换为 HTML,可细分为编辑时的转换 editingDowncast 和导出数据时的转换 dataDowncast

    // 定义转换器
    const conversion = this.editor.conversion;
    conversion.for("editingDowncast").elementToElement();
    conversion.for("dataDowncast").elementToElement();
    conversion.for("upcast").elementToElement();

    通过 this.editor.conversion.for() 来定义对应类型的转换器,详情参考官网 Conversion

    不同类型的转换器,可配置的转换规则并不相同


    首先是 downcast,它的可用转换方法有: elementToElement()、attributeToElement()、attributeToAttribute()、markerToElement()、markerToHighlight()

    这些转换方法被称为 Helper,除了这些自带的 Helper 之外,还可以使用 add() 自定义 Helper,详情查看 DowncastDispatcher

    就目前来说,先掌握基本的 elementToElement 就行,这个 Helper 需要接收一个对象参数,用来配置具体的转换规则,主要是 model 和 view:

    conversion.for("dataDowncast").elementToElement({
      model: SCHEMA_NAME__BOLD,
      view: (modelElement, conversionApi) =>
        createDowncastElement(modelElement, conversionApi),
    });

    downcast 的功能就是将 model -> view(HTML),所以这里的 model 配置为上面定义的 Schema 的名称

    而 view 可以接收一个 function,最终返回一个由 CKEditor 定义的 DOM 元素

    这个 function 提供两个参数,第一个是 modelElement,也就是被转换的 model,第二个是工具方法集合 DowncastConversionApi

    在 DowncastConversionApi 中有一个最常用的工具 writer这个工具非常重要!非常重要!非常重要!

    整个 CKEditor 中有很多 writer,它们之间有很多同名甚至功能相同的 API,但也有些区别,在使用的时候一定要清楚当前使用的是哪个 writer

    而 DowncastConversionApi 提供的是 DowncastWriter,我们可以通过这个工具开发需要渲染的 DOM 结构

    function createDowncastElement(modelElement, writer) {
      const element = writer.createContainerElement("strong");
      const value = modelElement.getAttribute("value");
      const innerText = writer.createText(value);
      writer.insert(writer.createPositionAt(element, 0), innerText);
    
      return element;
    }

    这里使用了 DowncastWriter.createContainerElement() 创建 <strong> 标签,然后通过 createText 创建普通文本,最后通过 writer.insert 将文本节点插入到 <strong> 中

    上面是 dataDowncast 的转换,但第一节的内容有提到,isInline 需要和 isObject 结合,也就是在编辑时 bold 会作为一个整体,所以在 editingDowncast 中需要用到 Widget

    conversion.for("editingDowncast").elementToElement({
      model: SCHEMA_NAME__BOLD,
      view: (modelElement, { writer }) => {
        const element = createDowncastElement(modelElement, writer);
        return toWidget(element, writer);
      },
    });

    downcast 的基本用法就是这样,上面的代码可以作为参考,后面会贴出完整的 eidting.js 代码


    对于 upcast,可用的转换方法 Helper 有:elementToElement()、attributeToElement()、attributeToAttribute()、elementToMarker()

    它的功能是 view -> model,而为了防止“一个 view 对应多个 model 的情况”出现,view 通常会是一个对象

    conversion.for("upcast").elementToElement({
      view: {
        name: "strong",
      },
      model: (view, { writer }) => {
        return writer.createElement(SCHEMA_NAME__BOLD, { value: "wise" });
      },
    });

    对于 view 除了标签名 name 以外,还可以配置 classes、attributes、styles

    upcast 的 model 也是一个 function,第二个参数是 UpcastConversionApi,提供了 UpcastWriter 用来创建 model

    这里只需要使用 createElement 创建对应的 Schema,并传入相应的属性即可


    最终的 editing.js 如下:

    // editing.js
    
    import Plugin from "@ckeditor/ckeditor5-core/src/plugin";
    import { toWidget } from "@ckeditor/ckeditor5-widget/src/utils";
    import Widget from "@ckeditor/ckeditor5-widget/src/widget";
    import BoldCommand from "./command";
    import { COMMAND_NAME__BOLD, SCHEMA_NAME__BOLD } from "./constant";
    
    export default class BoldEditing extends Plugin {
      static get requires() {
        return [Widget];
      }
      static get pluginName() {
        return "BoldEditing";
      }
      init() {
        const editor = this.editor;
    
        this._defineSchema();
        this._defineConverters();
    
        // 注册一个 BoldCommand 命令
        editor.commands.add(COMMAND_NAME__BOLD, new BoldCommand(editor));
      }
    
      // 注册 schema
      _defineSchema() {
        const schema = this.editor.model.schema;
    
        schema.register(SCHEMA_NAME__BOLD, {
          isInline: true,
          isObject: true,
          allowWhere: "$text",
          allowAttributes: ["value"],
        });
      }
    
      // 定义转换器
      _defineConverters() {
        const conversion = this.editor.conversion;
    
        // 将 model 渲染为 HTML
        conversion.for("editingDowncast").elementToElement({
          model: SCHEMA_NAME__BOLD,
          view: (modelElement, { writer }) => {
            const element = createDowncastElement(modelElement, writer);
            return toWidget(element, writer);
          },
        });
        conversion.for("dataDowncast").elementToElement({
          model: SCHEMA_NAME__BOLD,
          view: (modelElement, { writer }) =>
            createDowncastElement(modelElement, writer),
        });
    
        // 将 HTML 渲染为 model
        conversion.for("upcast").elementToElement({
          view: {
            name: "strong",
          },
          model: (view, { writer }) => {
            return writer.createElement(SCHEMA_NAME__BOLD, { value: "wise" });
          },
        });
      }
    }
    
    function createDowncastElement(modelElement, writer) {
      const element = writer.createContainerElement("strong");
      const value = modelElement.getAttribute("value");
      const innerText = writer.createText(value);
      writer.insert(writer.createPositionAt(element, 0), innerText);
    
      return element;
    }

     

     

    三、触发命令 Command

    插件的转换逻辑已经写好了,接下来回到 command.js,完善触发命令的 execute 逻辑

    其实有了 conversion 之后,只要在触发命令的时候,创建对应的 Schema 即可:

    execute() {
      const model = this.editor.model;
      model.change((writer) => {
        const element = writer.createElement(SCHEMA_NAME__BOLD, {
          value: this._getSelectionText(),
        });
        model.insertContent(element);
        writer.setSelection(element, "on");
      });
    }

    model.change() 是调整 model 的主要途径,插件对内容的修改几乎都要使用这个方法

    change 提供的参数是 ModelWriter,希望不要和上面的 UpcastWriter 和 DowncastWriter 搞混淆了

     

    对于 command.js 来说,除了 execute() 之外,还需要在 refresh() 定义规则来即时调整 isEnabled 和 value,这里暂时略过

    // command.js
    
    import Command from "@ckeditor/ckeditor5-core/src/command";
    import { SCHEMA_NAME__BOLD } from "./constant";
    
    export default class BoldCommand extends Command {
      refresh() {
        this.isEnabled = true;
      }
    
      execute() {
        const model = this.editor.model;
        model.change((writer) => {
          const element = writer.createElement(SCHEMA_NAME__BOLD, {
            value: this._getSelectionText(),
          });
          model.insertContent(element);
          writer.setSelection(element, "on");
        });
      }
    
      _getSelectionText() {
        const model = this.editor.model;
        const selection = model.document.selection;
    
        let str = "";
    
        for (const range of selection.getRanges()) {
          for (const item of range.getItems()) {
            str += item.data;
          }
        }
    
        return str;
      }
    }

    到这里插件的功能就已经完成了,接下来回到项目的 example 目录加以验证

     

     

    四、编辑器取值与设置初始值

    在编辑器 packages/my-editor/src/index.js 中,通过 ClassicEditor.create 创建编辑器之后,可以在 then() 中接收到编辑器实例 editor

    editor 提供了 getData() 方法来获取编辑器数据。可以在 example/index.js 中添加一个提交按钮,调用 getData() 来查看结果

    function _bind($editor) {
      const submitBtn = document.getElementById("submit");
      submitBtn.onclick = function () {
        const val = $editor.editor && $editor.editor.getData();
        console.log("editorGetValue", val);
      };
    };

     

    在使用 ClassicEditor.create 创建编辑器的时候,可以传入富文本 initialData 作为编辑器的初始值

    将带有 <strong> 标签的富文本作为初始值,如果能正常渲染,则说明 upcast 也能正常工作

     

     

    五、真正的加粗插件

    上面的加粗插件为了介绍基本的 Model,不得已采用了 isInlie + isObject 的方式,将加粗插件复杂化

    在 CKEditor 5 中最好通过文本属性的方式来开发加粗插件,所以对于 Schema 需要从 $text 继承:

    editor.model.schema.extend( '$text', { allowAttributes:'bold' } );

    这样就能在 $text 上添加 bold 属性,然后设置转换器,将带有 bold 的 $text 转换为 <strong>

    转换逻辑很简单,就不需要分别使用 downcast 和 upcast 了

    conversion.attributeToElement({
      model: SCHEMA_NAME__BOLD,
      view: "strong",
      upcastAlso: [ "b" ],
    });

    这种没有指定 downcast 和 upcast 的转换器就是双向转换器

    这里使用的是 attributeToElement,所以这里的 model 并不是完整的 Schema,而是 Schema 上携带的属性

    upcastAlso 是对双向转换器的扩展,其配置的视图元素会被转换为 model

    最终完整的 eiditing.js 如下:
    // editing.js
    
    import Plugin from "@ckeditor/ckeditor5-core/src/plugin";
    import BoldCommand from "./command";
    import { COMMAND_NAME__BOLD, SCHEMA_NAME__BOLD } from "./constant";
    
    export default class BoldEditing extends Plugin {
      static get pluginName() {
        return "BoldEditing";
      }
      init() {
        const editor = this.editor;
    
        this._defineSchema();
        this._defineConverters();
    
        // 注册一个 BoldCommand 命令
        editor.commands.add(COMMAND_NAME__BOLD, new BoldCommand(editor));
      }
    
      // 注册 schema
      _defineSchema() {
        const schema = this.editor.model.schema;
        schema.extend("$text", { allowAttributes: SCHEMA_NAME__BOLD });
      }
    
      // 定义转换器
      _defineConverters() {
        const conversion = this.editor.conversion;
    
        conversion.attributeToElement({
          model: SCHEMA_NAME__BOLD,
          view: "strong",
          upcastAlso: ["b"],
        });
      }
    }

     

    然后 command.js 也不需要创建 Schema,而是对选中的 $text 添加 bold 属性

    writer.setSelectionAttribute('bold', true);
    writer.setAttribute('bold', true, range);

    另外还可以完善一下 command 的 value 和 isEnabled,以控制加粗按钮在工具栏上的高亮/禁用状态

    refresh() {
      const model = this.editor.model;
      const selection = model.document.selection;
      this.value = selection.hasAttribute('bold');
      this.isEnabled = model.schema.checkAttributeInSelection(selection, 'bold');
    }

    最终完整的 command.js 如下:

    // command.js
    
    import Command from "@ckeditor/ckeditor5-core/src/command";
    import { SCHEMA_NAME__BOLD } from "./constant";
    
    export default class BoldCommand extends Command {
      constructor(editor) {
        super(editor);
        this.attributeKey = SCHEMA_NAME__BOLD;
      }
    
      refresh() {
        const model = this.editor.model;
        const selection = model.document.selection;
    
        // 如果选中的文本含有 bold 属性,设置 value 为 true,
        // 由于已在 toolbar-ui 中关联,当 value 为 true 时会高亮工具栏按钮
        this.value = this._getValueFromFirstAllowedNode();
    
        // 校验选中的 Schema 是否允许 bold 属性,若不允许则禁用按钮
        this.isEnabled = model.schema.checkAttributeInSelection(
          selection,
          this.attributeKey
        );
      }
    
      execute() {
        const model = this.editor.model;
        const selection = model.document.selection;
    
        const value = !this.value;
    
        // 对选中文本设置 bold 属性
        model.change((writer) => {
          if (selection.isCollapsed) {
            if (value) {
              writer.setSelectionAttribute(this.attributeKey, true);
            } else {
              writer.removeSelectionAttribute(this.attributeKey);
            }
          } else {
            const ranges = model.schema.getValidRanges(
              selection.getRanges(),
              this.attributeKey
            );
    
            for (const range of ranges) {
              if (value) {
                writer.setAttribute(this.attributeKey, value, range);
              } else {
                writer.removeAttribute(this.attributeKey, range);
              }
            }
          }
        });
      }
    
      _getValueFromFirstAllowedNode() {
        const model = this.editor.model;
        const schema = model.schema;
        const selection = model.document.selection;
    
        // 选区的锚点和焦点是否位于同一位置
        if (selection.isCollapsed) {
          return selection.hasAttribute(this.attributeKey);
        }
    
        for (const range of selection.getRanges()) {
          for (const item of range.getItems()) {
            if (schema.checkAttribute(item, this.attributeKey)) {
              return item.hasAttribute(this.attributeKey);
            }
          }
        }
    
        return false;
      }
    }

     

    了解了加粗插件的写法,就熟悉了 CKEditor 5 的基本玩法,但如果想开发一个完全自定义的插件,仍然需要努力

    比如插入超链接,选中文本后点需要通过一个表单来输入连接,这个表单应该如何开发?

    又比如插入图片,如果需要在插入之前做一些编辑(比如裁剪图片、添加图片描述),甚至在插入图片后还支持编辑,这就更加复杂

    后面会先用超链接的例子来演示如何开发表单,to be continue...

  • 相关阅读:
    对Warning: Attempt to present XXX on XXX whose view is not in the window hierarchy!的解决方案
    iOS开发路线简述(转)
    Xcode6为什么干掉pch(Precompile Prefix Header)&如何添加pch文件
    ToolBar中的TextField为第一响应者时不弹出键盘
    xcode6 iOS SDK 8.1隐藏系统状态栏
    UE4 材质切换(带动画效果)
    UE4 VR 模式画面扭曲 解决方法
    UE4 去除不正确的水面倒影以及不完整镜头轮廓
    UE4 VR 模式全屏 4.13
    UE4 Windows平台部署游戏到IOS 第二部分
  • 原文地址:https://www.cnblogs.com/wisewrong/p/14436367.html
Copyright © 2020-2023  润新知