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...

 


免責聲明!

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



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