• CKEditor 5 摸爬滚打(五)—— 图片的插入与编辑


    这篇文章将以插入图片为例,介绍如何在 CKEditor5 中插入块级元素,以及在块级元素上添加工具栏

    最终的效果如下:

     

     

     

    一、定义 Schema 和 Conversion

    和之前的加粗插件、超链接插件不同,图片在编辑器中是以块级元素呈现的

    所以在定义 Schema 的时候需要设置 isObject 以及 isBlock,从而得到这样的 Schema:

    _defineSchema() {
      const schema = this.editor.model.schema;
    
      // SCHEMA_NAME__IMAGE --> "image"
      schema.register(SCHEMA_NAME__IMAGE, {
        isObject: true,
        isBlock: true,
        allowWhere: "$block",
        allowAttributes: ["src", "title"],
      });
    }

    然后在定义转换器 Conversion 的时候,需要使用 toWidget 将图片元素包装起来,所以得区分 editingDowncast 与 dataDowncast:

    _defineConverters() {
      const conversion = this.editor.conversion;
    
      // SCHEMA_NAME__IMAGE --> "image"
      conversion.for("editingDowncast").elementToElement({
        model: SCHEMA_NAME__IMAGE,
        view: (element, { writer }) => {
          const widgetElement = createImageViewElement(element, writer);
          // 添加自定义属性,以判断是否为 Image Model
          // CUSTOM_PROPERTY__IMAGE --> "is-image"
          writer.setCustomProperty(CUSTOM_PROPERTY__IMAGE, true, widgetElement);
          return toWidget(widgetElement, writer);
        },
      });
    
      conversion.for("dataDowncast").elementToElement({
        model: SCHEMA_NAME__IMAGE,
        view: (element, { writer }) =>
          createImageViewElement(element, writer),
      });
    
      conversion.for("upcast").elementToElement({
        view: {
          name: "figure",
          classes: IMAGE_CLASS,
        },
        model: createImageModel,
      });
    }

    // 先忽略创建 Model 和 View 的具体方法 createImageModel、createImageViewElement

    这里的 editingDowncast 使用了 toWidget,给编辑器里的图片元素添加了一个不可编辑的父元素,这样保证了整个图片元素被视为一个整体

    而在 dataDowncast 里面没有使用 toWidget,最终导出的结果就不会有额外的元素

     

    从目前的设计来看,最终 View 和 Model 的转换结果是这样的:

    <!-- Model -->
    <image src="$url" title="$title"></image>
    
    <!-- View -->
    <figure>
      <img src="$url" title="$title">
    </figure>

     
    需要注意的是,由于使用了 toWidget,所以需要在 requires 中添加  Widget

    // editing.js
    
    import Plugin from "@ckeditor/ckeditor5-core/src/plugin";
    import Widget from '@ckeditor/ckeditor5-widget/src/widget';
    
    export default class ImageEditing extends Plugin {
      static get requires() {
        return [Widget];
      }
    
      static get pluginName() {
        return "ImageEditing";
      }
    
      // ...
        
    }

     

     

    二、创建 Model 和 View

    上面的 Conversion 只是列举了上行和下行的转换逻辑,接下来完善具体创建 Model 和 View 的方法

    首先是根据 Model 创建图片 View:

    // 根据 Model 创建图片 View
    export function createImageViewElement(element, writer) {
    // 使用 createContainerElement 创建容器元素 const figure = writer.createContainerElement("figure", { class: IMAGE_CLASS, }); // 使用 createEmptyElement 创建 img 标签,并设置属性 const imageElement = writer.createEmptyElement("img"); ["src", "title"].map((k) => { writer.setAttribute(k, element.getAttribute(k), imageElement); }); // 将 img 作为子节点插入到 figure writer.insert(writer.createPositionAt(figure, 0), imageElement); return figure; }

     

    然后是根据 View 创建图片 Model,通过 upcast 转换器能够获取到这样的 View:

    <!-- View -->
    <figure>
      <img src="$url" title="$title">
    </figure>

    然后通过操作 DOM 的方法获取到 <img> 上的 src 和 title,并作为属性传给创建的 Schema:

    // 根据 View 创建图片 Model
    export function createImageModel(view, { writer }) {
      const params = {};
      const imageInner = view.getChild(0);
    
      ["src", "title"].map((k) => {
        params[k] = imageInner.getAttribute(k);
      });
    
      return writer.createElement(SCHEMA_NAME__IMAGE, params);
    }

     

     

    三、添加自定义配置

    对于图片元素,在实际应用场景中很可能需要添加一些自定义配置,比如自定义 class

    CKEditor 5 提供了 EditorConfig 用来添加用户的自定义配置

     

    首先在 editing.js 的构造函数 constructor 中声明一个默认值,并通过 get 方法获取:

    constructor(editor) {
      super(editor);
    
      // 配置 IMAGE_CONFIG 的缺省值
      // IMAGE_CONFIG --> "IMAGE_CONFIG"
      editor.config.define(IMAGE_CONFIG, {});
    
      // 通过 get 方法获取实际传入的配置
      this.imageConfig = editor.config.get(IMAGE_CONFIG);
    }

    然后在使用 create 创建 editor 的时候,传入对应的配置项,就能在 this.imageConfig 中获取到用户的配置信息了

     

     

    四、插入图片

    上一篇文章《CKEditor 5 摸爬滚打(四)—— 开发带有弹窗表单的超链接插件》已经介绍了弹窗表单的开发

    这里就不再细讲 toolbar-ui.js、image-form.js 的详细代码,只提一下 command.js 中关于图片的插入

    // command.js
    
    import Command from "@ckeditor/ckeditor5-core/src/command";
    import { insertImage } from "./util";
    
    export default class LinkCommand extends Command {
      refresh() {
        const model = this.editor.model;
        const selectedContent = model.getSelectedContent(model.document.selection);
        this.isEnabled = selectedContent.isEmpty;
      }
    
      execute(data) {
        const model = this.editor.model;
        insertImage(model, data);
      }
    }

    触发命令的时候会将图片元素的参数 { src, title } 传过来,然后通过 insertImage 方法插入图片

    export function insertImage(model, attributes = {}) {
      if (!attributes || !attributes.src) {
        return;
      }
    
      model.change((writer) => {
        const imageElement = writer.createElement(SCHEMA_NAME__IMAGE, attributes);
        // 使用 findOptimalInsertionPosition 方法来获取最佳位置
        // 如果某个选择位于段落的中间,则将返回该段落之前的位置,不拆分当前段落
        // 如果选择位于段落的末尾,则将返回该段落之后的位置
        const insertAtSelection = findOptimalInsertionPosition(
          model.document.selection,
          model
        );
        model.insertContent(imageElement, insertAtSelection);
      });
    }

    和之前介绍的插件的区别在于,对于编辑器中的块级元素,如果直接使用 model.insertContent 插入元素,会截断当前行的内容

    而 CK5 提供的工具方法 findOptimalInsertionPosition 可以返回一个合适的位置,用于插入块级元素

     

     

    五、编辑图片

    CKEditor 5 为 Widget 提供了悬浮工具栏的构造函数 WidgetToolbarRepository

    通过这个组件可以在 Widget 上创建一个悬浮工具栏,但工具栏上的工具按钮需要另外定义

    // ./widget-toolbar/toolbar.js
    
    import Plugin from "@ckeditor/ckeditor5-core/src/plugin";
    import WidgetToolbarRepository from "@ckeditor/ckeditor5-widget/src/widgettoolbarrepository";
    import { getSelectedImageWidget } from '../util';
    import ImageEdit from "./edit/main";
    import {
      WIDGET_TOOLBAR_NAME__IMAGE,
    } from "../constant";
    
    export default class ImageWidgetToolbar extends Plugin {
      static get requires() {
        return [WidgetToolbarRepository, ImageEdit];
      }
    
      static get pluginName() {
        return "ImageToolbar";
      }
    
      afterInit() {
        const editor = this.editor;
        const widgetToolbarRepository = editor.plugins.get(WidgetToolbarRepository);
    
        // WIDGET_TOOLBAR_NAME__IMAGE --> "ck-image-toolbar"
        widgetToolbarRepository.register(WIDGET_TOOLBAR_NAME__IMAGE, {
          ariaLabel: "图片工具栏",
          items: [ImageEdit.pluginName],
          getRelatedElement: getSelectedImageWidget,
        });
      }
    }

    这是工具栏的入口文件 toolbar.js,需要在 plugin-image 组件的入口文件 main.js 中作为 requires 引入

    在通过 register 注册工具栏的时候,第二个参数是工具栏配置项,其中的 items 是一个由工具名组成的数组,类似于创建编辑器时的 toolbar 配置项

    需要注意的是 getRelatedElement,用来判断是否选中的对应的 widget 元素,换句话说就是判断是否需要显示工具栏

    export function getSelectedImageWidget(selection) {
      const viewElement = selection.getSelectedElement();
    
      if (viewElement && isImageWidget(viewElement)) {
        return viewElement;
      }
    
      return null;
    }
    
    export function isImageWidget(viewElement) {
      return (
        !!viewElement && viewElement.getCustomProperty(CUSTOM_PROPERTY__IMAGE) &&
        isWidget(viewElement)
      );
    }

     

    另外 toolbar.js 中引入了一个 ImageEdit 工具,也就是“编辑图片”的功能主体

    这个 ImageEdit 和普通的插件并无二致,也需要 editing.js、command.js、toolbar-ui.js

    也就是说,图片编辑这个功能其实本身也是一个插件,只是这个插件的按钮图标没有放到编辑器的工具栏,而是在图片元素的悬浮工具栏上展示

    这里的 toolbar-ui.js 就不再介绍,和其他插件的 toolbar-ui.js 一样,只是定义了工具栏按钮的样式

    先说一下 command.js 的基本逻辑

    // ./widget-toolbar/edit/command.js
    
    import Command from "@ckeditor/ckeditor5-core/src/command";
    import { COMMAND_NAME__IMAGE } from "../../constant";
    import ImageForm from "../../form/image-form";
    
    export default class ImageEditCommand extends Command {
      constructor(editor) {
        super(editor);
      }
    
      refresh() {
        const element = this.editor.model.document.selection.getSelectedElement();
        this.isEnabled = !!element && element.is("element", COMMAND_NAME__IMAGE);
      }
    
      execute() {
        const model = this.editor.model;
        const viewElement = model.document.selection.getSelectedElement();
        const attributes = viewElement.getAttributes();
    
        // 获取当前图片的参数
        const initialValue = [...attributes].reduce(
          (obj, [key, value]) => ((obj[key] = value), obj),
          {}
        );
    
        // 打开弹窗,编辑图片信息
        this.$form = new ImageForm({
          initialValue,
          onSubmit: this._handleEditImage.bind(this),
        });
      }
    }

    然后对于修改图片这个核心功能 _handleEditImage 有两种思路:

    1. 删除原有图片,在原位置重新插入一个新的图片

    2. 监听属性的修改,在属性改变后更新视图

     

    这两种思路的区别在于:

    方案一(删除后插入)比较暴力,相对来说性能较差,但很实用,也不容易出错

    方案二(修改属性)需要对每一个有可能更改的属性进行监听,如果可修改的属性较多,反而不如方案一

     

    在此基础上,接下来就介绍这两种方案的具体实现:

     

    方案一、删除后插入新图片

    Model 的 writer 提供了 remove 方法,可以删除一个 ModelElement 或者 Rang

    上面“插入图片”小节中封装了一个 insertImage 方法,可以直接调用

    所以最终的 _handleEditImage 就很简单:

    _handleEditImage(data) {
      const model = this.editor.model;
      const imageElement = model.document.selection.getSelectedElement();
    
      model.change((writer) => {
        writer.remove(imageElement);
        insertImage(model, data)
      });
    }

    最后只需要完善 ./widget-toolbar/edit/editing.js,编辑功能就完成了

    // editing.js
    
    import Plugin from "@ckeditor/ckeditor5-core/src/plugin";
    import ImageEditCommand from "./command";
    import { COMMAND_NAME__IMAGE_EDIT } from "../../constant";
    
    export default class ImageEditEditing extends Plugin {
      init() {
        const editor = this.editor;
        const command = new ImageEditCommand(editor);
        editor.commands.add(COMMAND_NAME__IMAGE_EDIT, command);
      }
    }

     

    方案二、属性修改后更新视图

    这种方案的 _handleEditImage 只需要修改对应的属性:

    _handleEditImage(data) {
      const model = this.editor.model;
      const imageElement = model.document.selection.getSelectedElement();
    
      model.change((writer) => {
        ["src", "title"].forEach(key => {
          writer.setAttribute(key, data[key], imageElement);
        })
      });
    }

    但在 editing.js 中需要通过 downcastDispatcher 监听对应的属性

    // editing.js
    
    import Plugin from "@ckeditor/ckeditor5-core/src/plugin";
    import ImageEditCommand from "./command";
    import { COMMAND_NAME__IMAGE_EDIT } from "../../constant";
    
    export default class ImageEditEditing extends Plugin {
      init() {
        const editor = this.editor;
        const data = editor.data;
        const editing = editor.editing;
    
        // 监听 src 和 title 属性的变更,需要从 editing 和 data 中获取 downcastDispatcher
        editing.downcastDispatcher.on(
          "attribute:src:image",
          modelToViewConverter("src")
        );
        data.downcastDispatcher.on(
          "attribute:src:image",
          modelToViewConverter("src")
        );
        editing.downcastDispatcher.on(
          "attribute:title:image",
          modelToViewConverter("title")
        );
        data.downcastDispatcher.on(
          "attribute:title:image",
          modelToViewConverter("title")
        );
    
        const command = new ImageEditCommand(editor);
        editor.commands.add(COMMAND_NAME__IMAGE_EDIT, command);
      }
    }
    
    function modelToViewConverter(attr) {
      return (evt, data, conversionApi) => {
    
        // CK5 会将属性的更改状态保存为 consumable,用于校验该变化是否已经完成
        if (!conversionApi.consumable.consume(data.item, evt.name)) {
          return;
        }
    
        const viewElement = conversionApi.mapper.toViewElement(data.item);
        const viewWriter = conversionApi.writer;
        const imageInner = viewElement.getChild(0);
    
        // 修改视图中对应的属性
        viewWriter.setAttribute(
          attr,
          data.attributeNewValue,
          imageInner
        );
    
        // 阻止事件冒泡
        evt.stop();
      };
    }

     

     


    到此为止的五篇《CKEditor 5 摸爬滚打》 介绍了 CK5 中常见的开发方式,已经能开发大部分的编辑器组件

    但整个 CK5 的架构太过繁琐,还有很多工具函数和细节没有涉及到

    如果在开发的过程中仍然存在问题,建议多挖一挖官方文档,或者结合 CKEditor 5 的官方插件源码,看有没有新的思路

  • 相关阅读:
    linux,windows kettle安装方法
    等待事件分类
    分析函数详细例子
    v$session中不同连接方式module,program的区别
    charles Glist发布设置
    charles 发布Glist
    charles 工具菜单总结
    charles 高级批量请求
    charles 批量重复请求/重复发包工具
    charles 重写工具/rewrite Srttings
  • 原文地址:https://www.cnblogs.com/wisewrong/p/14554110.html
Copyright © 2020-2023  润新知