核心软件包@ckeditor/ckeditor5-core相对简单,只包含少数几个类。
编辑器 Editor
类 Editor 是编辑器的基础,作为应用程序的入口点,将所有其它组件粘合在一起。它提供了一些需要了解的属性:
config- 配置对象;plugins和commands(插件和指令)- 已加载插件和指令的集合;model- 编辑器数据模型的入口点;data- 数据控制器。它控制如何从文档中获取数据并在其中进行设置;editing- 可编辑元件控制器。它控制如何将模型渲染给用户进行编辑;keystrokes- 按键处理程序。它允许将按键与操作绑定。
create()- 静态create()方法。编辑器自身的构造函数是受保护的,因此应使用此静态方法创建编辑器。此方法允许异步初始化;destroy()- 销毁编辑器;execute()- 执行给定的命令;setData()和getData()- 从编辑器中获取数据,和在编辑器中设置数据的方法。数据格式由数据控制器的数据处理器控制;
类 Editor 是自行定制编辑器的基础。CKEditor 5 框架原生附带了几种编辑器类型,但开发人员仍可以自由实现工作方式和外观完全不同的编辑器。
插件 Plugin
插件是引入编辑器功能的一种方式。在 CKEditor 5 中,就连键入(Typing)功能都是一个插件。更重要的是,键入插件依赖于输入(Input)和删除(Delete)插件,这两个插件分别负责处理插入文本和删除内容的方法。又或者,某些插件需要在某些情况下自定义退格(Backspace)行为并自行处理。
用于实现 CKEditor 5 插件的的另一个重要策略是:将其分为引擎(Editing Engine)和 UI 两大部分。例如:对于加粗功能而言,BoldEditing 子插件负责引入模式定义、渲染<strong>标签的机制,以及应用和移除文本加粗的命令;而 BoldUI 子插件则添加了该功能的用户界面(即按钮)。这种功能分离旨在实现更高的重用性(可以获取引擎部分并为功能实现自己的 UI),以及支持在服务器端运行 CKEditor 5。最后使用一个空壳 Bold 插件来集成它们,即可提供完整的体验。
总结起来就是:
- 每个功能都是通过插件实现或启用的;
- 插件被高度细化;
- 插件构成编辑器的一切;
- 插件之间应尽可能少地耦合。
在冗长的介绍之后(目的是令读者更容易消化已有的内置插件),可以再来解释一下插件 API。
所有插件都需要实现PluginInterface——最简单的方法是从Plugin类继承。插件初始化代码应位于init()方法中(该方法可以返回一个Promise)。如果某些代码需要在其它插件初始化之后执行,可以将其置于afterInit()方法中。插件之间的依赖关系通过静态requires属性来实现。
import MyDependency from 'some/other/plugin';
class MyPlugin extends Plugin {
static get requires() {
return [ MyDependency ];
}
init() {
// 在此处初始化插件
// 负责加载此插件的编辑器实例
this.editor;
}
}
指令 Command
指令是执行动作(Execute)和状态(State)(一组属性)的组合。例如:粗体指令会应用或删除所选文本的粗体属性。如果选中的文本已经加粗,则该指令的值为 true,否则为false。如果粗体指令可以在当前选区中执行,则启用该指令。如果不能(比如因为此处不允许加粗)则会禁用。
建议使用官方提供的 CKEditor 5 检查器(Inspector)进行开发和调试,它会提供大量有关编辑器状态的有用信息,如内部数据结构、选区和指令等。
所有指令都应继承自Command类。指令需要添加到编辑器的指令集中,以便使用Editor::execute()方法执行。
class MyCommand extends Command {
execute( message ) {
console.log( message );
}
}
class MyPlugin extends Plugin {
init() {
const editor = this.editor;
editor.commands.add( 'myCommand', new MyCommand( editor ) );
}
}
执行editor.execute( 'myCommand', 'Foo!' )就会在控制台输出Foo!。
要了解像bold这样的典型指令的状态管理是如何实现的,请看一下作为bold基础的AttributeCommand类的内容。
首先要注意的是refresh()方法:
refresh() {
const doc = this.editor.document;
this.value = doc.selection.hasAttribute( this.attributeKey );
this.isEnabled = doc.schema.checkAttributeInSelection(
doc.selection, this.attributeKey
);
}
当 Model 发生任何变化时,(指令本身)会自动调用此方法。这意味着当编辑器中有任何变化时,指令会自动刷新自己的状态。
指令的重要之处在于,其状态的每次变化以及调用execute()方法都会触发事件。例如当修改#value属性时,会触发#set:value和#change:value事件;执行指令时则会触发#execute事件。通过这些事件,可以从外部控制指令。例如,如果想在某些条件为true时禁用特定的指令(例如根据某些定制的逻辑,这些指令应暂时不可用),而且没有其它更简洁的方法,则可以手动禁用指令:
function disableCommand( cmd ) {
cmd.on( 'set:isEnabled', forceDisable, { priority: 'highest' } );
cmd.isEnabled = false;
// 使再次启用指令成为可能
return () => {
cmd.off( 'set:isEnabled', forceDisable );
cmd.refresh();
};
function forceDisable( evt ) {
evt.return = false;
evt.stop();
}
}
// 用法:
// 禁用指令
const enableBold = disableCommand( editor.commands.get( 'bold' ) );
// 重新启用指令
enableBold();
现在只要不关闭该监听器,无论调用多少次someCommand.refresh(),该指令都会被禁用。默认情况下,当编辑器处于只读模式时,编辑器指令会被禁用。但是,如果指令不会修改编辑器数据,且希望其在只读模式下仍然能保持启用状态,则可以将affectsData标志设置为false:
class MyAlwaysEnabledCommand extends Command {
constructor( editor ) {
super( editor );
// 即使编辑器为只读状态,该命令也将保持启用
this.affectsData = false;
}
}
在限制用户写入权限的其它编辑器模式下,affectsData标志也会影响指令。此外,所有编辑器指令的affectsData标志都默认设置为true,除非指令需要在编辑器只读模式下启用,否则无需对其做出更改。该标志位在编辑器生命周期内不可更改。
Event & Observable
CKEditor 5 采用了基于事件的架构,因此Emitter和Observable被混合在一起。这两种机制都允许代码解耦,且使其具有可扩展性。前面提到的大多数类都是Emitter或Observable(Observable也是Emitter)。Emitter可以触发事件,也可以监听事件。
class MyPlugin extends Plugin {
init() {
// 使 MyPlugin 监听 someCommand#execute
this.listenTo( someCommand, 'execute', () => {
console.log( 'someCommand 被执行。' );
} );
// 让 MyPlugin 监听 someOtherCommand#execute 并阻断它
// 可使用高优先级监听,以便在调用 someOtherCommand 的 execute() 方法之前阻断该事件
this.listenTo( someOtherCommand, 'execute', evt => {
evt.stop();
}, { priority: 'high' } );
}
// 继承自 Plugin:
destroy() {
// 移除所有通过 this.listenTo() 添加的监听器
this.stopListening();
}
}
execute的第二个监听器表征了 CKEditor 5 代码中的一种常见做法。一般地,execute的默认操作(即调用execute()方法)以默认优先级注册为该事件的监听器。因此,通过使用low或high优先级监听事件,便可以在真正调用execute()之前或之后执行某些代码。如果阻断事件,则根本不会调用execute()方法。在本例中,Command::execute()方法使用ObservableMixin::decorate()函数修饰了事件:
import { ObservableMixin, mix } from 'ckeditor5';
class Command {
constructor() {
this.decorate( 'execute' );
}
// 现在将自动触发 #execute 事件
execute() {}
}
// 将 ObservableMixin 混合到指令中
mix( Command, ObservableMixin );
除了用事件修饰方法以外,可观察对象还允许观察其所选属性。例如,Command类可通过调用set()来观察其#value和#isEnabled:
class Command {
constructor() {
this.set( 'value', undefined );
this.set( 'isEnabled', undefined );
}
}
mix( Command, ObservableMixin );
const command = new Command();
command.on( 'change:value', ( evt, propertyName, newValue, oldValue ) => {
console.log(
`属性 ${ propertyName } 已从 ${ oldValue } 修改至 ${ newValue }。`
);
} )
command.value = true; // -> '属性 value 已从 undefined 修改至 true。'
可观察对象还有一个被编辑器广泛使用的功能(尤其是在 UI 库中)——可以将一个对象的属性值与其它属性值绑定(一个或多个对象的属性)。当然也可以通过回调来处理。假设目标和源都是可观察对象,且使用的属性都是可观察属性:
target.bind( 'foo' ).to( source );
source.foo = 1;
target.foo; // -> 1
// 或者:
target.bind( 'foo' ).to( source, 'bar' );
source.bar = 1;
target.foo; // -> 1