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 的官方插件源碼,看有沒有新的思路


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM