最近文章

CKEditor5——Position源码分析(三)

CKEditor5——Position源码分析(三)上一节我们学习了比较两个Position之间的关系,它们可能属于不同的根节点,它们可能完全相同,它们也可能存在前后关系。今天我们继续学习这个类的另一个关键方法:那就是getTransformedByOperation( operation ) → PositiongetTransformedByOperation( operation ) {
标签:

CKEditor5——Position源码分析(二)

CKEditor5——Position源码分析(二)上一节,我们学习了Position的基础知识,包括怎么寻找它的parent属性,它的path是什么意思?今天我们看另一个方法,两个Position之间的关系是什么,也就是这个方法:compareWith( otherPosition ) → PositionRelationcompareWith( otherPosition ) { if (
标签:

CKEditor5——Position源码分析

CKEditor5——Position源码分析前面我们介绍过CK5模型中的一个关键类,那就是Position的使用,今天我们来看看它的源码,梳理一下方便以后理解:export default class Position { /** * Creates a position. * * @param {module:engine/model/element~Element|module
标签:

CKEditor5——模型理解(二:Node)

上一节我们理解了基本的CK5的模型基本信息,今天我们来学习一些模型的API。节点说明首先,需要理解的就是模型的节点。在这一点上,CK5的模型节点和dom的节点有点类似,也有一些不同。我会在文章中一一介绍。节点是模型树的基本结构。它是模型中不同节点类型的一种抽象。这里需要指出的一点是:如果一个节点从模型树中分离出来,你可以使用它的 API 来操作它。但是,非常重要的是,已经附加到模型树的节点只能通过
标签:

CKEditor5——Node,Element,NodeList源码分析

CKEditor5——Node,Element,NodeList源码分析前面大概知道了模型的基本组成,包括一个抽象类Node,两个实现类Text和Element,以及还有一个关键的类就是NodeList。为了容易展开这个话题,我首先提出一个问题,那就是为什么是Node类是一个抽象类?先看看这个类的具体代码:export default class Node { /** * Creates a
标签:

CKEditor5——模型理解(四:模型组成)

'insertContent', 'deleteContent', 'modifySelection''insertContent', 'deleteContent', 'modifySelection', 'getSelectedContent', 'applyOperation'今天我们来深入学习一下CK5的模型。我们先看看model.js的源码:export default class Mo
标签:

CKEditor5——Template理解(三)

CKEditor5——Template理解(三)上一节我们分析了render()方法中关键的_renderText(),我们再来看看另一个方法就是renderElement()_renderElement( data ) { let node = data.node; if ( !node ) { node = data.node = document.createElementNS(
标签:

CKEditor5——Template理解(二)

CKEditor5——Template理解(二)在上一节我们分析了类Template的构造函数,今天我们继续学习其他的方法:我们先看看render()方法render() { const node = this._renderNode( { intoFragment: true } ); this._isRendered = true; return node; } // cons
标签:

CKEditor5——Template理解(一)

CKEditor5——Template理解上一节,我们学习并大致理解了CK5的UI中最重要的一个类,那就是View,可以看出这个类其实有点像一个门面,而很多实际的功能包括render和bind等都是委托给Template这个类来实现的,因此,我们今天来靴子这个类。export default class Template { /** * Creates an instance of the
标签:

CKEditor5——View理解

CKEditor5——View理解今天我们继续学习ck5UI中的一个基础类View,这个类是所有视图的基础类,我们先看看这个类的代码:export default class View { /** * Creates an instance of the {@link module:ui/view~View} class. * * Also see {@link #render}.
标签:

CKEditor5——UI理解

CKEditor5——UI理解今天我们开始认识CK5中的UI,首先我们还是认识一些基础类,今天开始认识的第一个类比较简单,那就是ViewCollection,首先我们看看代码:export default class ViewCollection extends Collection { constructor( initialItems = [] ) { super( initialIt
标签:

CKEditor5——Uitls(DomEmitterMixin类理解)

CKEditor5——Uitls(DomEmitterMixin类理解)今天开始研究在CK5的Utils包中的另一个比较重要的类,这个类就是DomEmitterMixin,我们先看看这个类的代码:const DomEmitterMixin = extend( {}, EmitterMixin, { listenTo( emitter, event, callback, options = {}

CKEditor5——Utils(Collection类理解)

CKEditor5——Utils(Collection类理解)今天我们学习CK5的Utils工具包中比较重要且常用的一个关键类:Collection,先贴出比较关键的代码:export default class Collection { constructor( initialItemsOrOptions = {}, options = {} ) { const hasInitialIt
标签:

CKEditor5——Plugin理解

CKEditor5——Plugin理解今天开始理解学习CK5的插件类,整个类是所有插件的抽象,看看源码:export default class Plugin { /** * @inheritDoc */ constructor( editor ) { this.editor = editor; this.set( 'isEnabled', true );
标签:

CKEditor5事件系统源码分析(三)

CKEditor5事件系统源码分析(三)上一节我们分析了事件系统的关键类EmitterMixin的fire()方法,今天我们看看另一个方法就是delegate()这个方法的代码如下:delegate( ...events ) { return { to: ( emitter, nameOrFunction ) => { if ( !this._delegations ) {
标签:

CKEditor5事件系统源码分析(一)

在学习CK5的时候,在事件系统这一块,有一个特别关键的类,那就是EmitterMixin这个类,这个类在什么地方呢?他其实就在ckeditor5-utils包中。下面我们来看看这个类:  EmitterMixin类const EmitterMixin = { /** * @inheritDoc */ on( event, callback, options = {} ) {
标签:

CKEditor5事件系统源码分析(二)

CKEditor5事件系统源码分析(二)上一节我们分析了CK5的EmitterMixin类的重点方法addEventListener,今天我们再认真看看fire方法。fire( eventOrInfo, ...args ) { try { const eventInfo = eventOrInfo instanceof EventInfo ? eventOrInfo : new EventI
标签:

CKEditor5事件系统(基础使用)

最近在学习CK5,一种最大的感受就是CK5的架构不是很大,但是内容特别多。笔者在学习中,总结出一个浅显的道理,那就是掌握基础知识,对框架宏观把握,学习起来会事半功倍。今天开始初步研究一下CK5的事件系统:在CK5的事件系统中,关键的一个对象被称作Emitter(发射器),Emitter是一个可以发送事件的对象。如何创建一个Emitter,下面的代码创建了一个混合了事件发送的AnyClass类,它实

CKEditor5——Conversion理解

大家知道,在CKEditor5中,Conversion(转化器)是最重要的一个组件之一,为了深入的理解转化器,我们先从大的层面来掌握一下,以后再分别从细节入手。我们从上面的图中不难看出,总的来说有三个converter,那么这三个converter在代码中具体在哪里呢?我们在ckeditor5-engine包下的controller包中有两个类,而这两个类才是真正存放转化控制器的代码的地方:这里我
标签:

CKEditor5——Utils(工具类理解)

如果对CK5的代码有所理解的话,大概知道,CK5有一个非常重要的工具包项目,这个工具包非常重要,提供了CK5最基础的一些功能。比如:集合类Collection、事件类EmitterMixin、观察者类ObservableMixin等。今天我们暂缓学些以上的类,主要理解一个关于dom的类,那就是Position和Rect类,因为这两个类是CK5中弹出balloon工具条的基础类。我会一点点学习它的原
标签:

getClientRects应用举例

上一节我们介绍了getClients的用法,今天我们学习一个简答的应用场景需求:我在一篇文章中选中一段文字,然后在文字的下方弹出一个简单的浮动按钮。思路分析:1、按钮需要浮动,首先我想到的是在文档中使用一个元素来作为按钮,这个元素我采用绝对定位,当我知道选择区域的大概位置的时候,根据位置修改元素的top和left值,就可以啦。2、按钮元素最好首先将top和left的位置放置于文档的视口外部,这个我
标签:

CKeditor5事件系统命名空间分析

在上一节的事件系统源码中,我们留下了一个函数createEventNamespace( this, event )今天我们来分析这个函数:首先,我将这个函数摘取出来,放到一个utils.js文件中:function getEvents( source ) { if ( !source._events ) { Object.defineProperty( source, '_events',
标签:

CKEditor5 Observable——属性绑定

前面我们知道了,在CK5中怎么样将一个对象设置成Observable以及Observable在UI中如何使用?属性绑定今天我们来看看如何进行可观测对象的属性绑定和重命名。首先,我们假定有两个Observable对象,所谓绑定就是将一个对象的可观测状态绑定到另一个可观测对象,如下所示:const button = new Button(); const command = editor.comman

CKEditor5 模板绑定

CK5还可以将模板的属性绑定到可观测对象属性,如下代码所示:import {View} from 'ckeditor5/src/ui'; export default class Button extends View { constructor(){ super(); this.type = 'button'; const bind =

CKEditor5事件系统(视图事件冒泡)

CK5的视图(view.document)不仅是一个Observable和emitter,而且还实现了一个BubblingEmitter,它是由BubblingEmitterMixin实现的。它提供了在虚拟dom机制上的冒泡事件。 它与普通的dom树上的冒泡机制不同。它不会在特定的元素上注册监听器,而是在指定的上下文上注册监听器。这里的上下文,要么是一个元素,要么是虚拟上下文之一,要么是

CKEditor5事件系统(代理事件)

emitter接口提供了事件代理机制。也就是说指定选择的事件能够被其他的emitter触发。 1、代理指定的事件到另一个emitterlet anyClass = new AnyClass(); let anotherClass = new AnyClass(); let oneClass = new AnyClass(); anotherClass.on('bar',(evt,data

CKEditor5 Observables

在CKEditor5中除了事件系统外,还有另一个重要的系统就是可观测对象,俗称Observable对象。此对象的属性是可观测的,一段对象的属性发生改变,将会触发一个事件,监听此事件的代码片段可以做出一些相应的操作。 CK5定义一个可观测对象import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'

CKEditor5——(九:TreeWalker)

前面我们学习了模型的基础知识,今天我们来看看模型另一个特点:模型位置的迭代。也就是TeeeWalker这个类用来在模型的位置之间迭代访问模型的节点。TreeWalker属性boundaries : Range迭代器边界这个属性用于指定迭代器在文档的哪个范围内迭代访问文档的Item。当迭代器在边界的末端“向前”行走或在边界的起点“向后”行走时,返回 { done: true }。direction
标签:

getClientRects(学习)

Element 接口的 getClientRects() 方法返回 DOMRect 对象的集合,这些对象指示客户端中每个 CSS 边框框的边界矩形。大多数元素每个只有一个边框,但多行内联元素(例如多行 <span> 元素,默认情况下)在每一行周围都有一个边框。调用语法:let rectCollection = object.getClientRects();返回值是 DOMRect 对
标签:

getBoundingClientRect(学习)

最近在学习CK5的时候,学习到了一个Rect的类,这个类主要提供盒子元素定位时候用到的一些值,比如top、left、right、bottom、width、height。而它的实现主要用到了两个方法,其中一个就是:Element.getBoundingClientRect()Element.getBoundingClientRect() 方法返回一个 DOMRect 对象,该对象提供有关元素大小及其

CKEditor5——模型理解(一)

我们知道,CK5实现了一个MVC的架构,从今天开始,我们一步一步深入学习模型,视图,以及模型和视图之间的转换。今天我们开始模型的学习。首先,我们看模型的定义:The model is implemented by a DOM-like tree structure of elements and text nodes.模型由两类节点构成,分别是元素节点和文本节点,模型是一种类Dom树结构。我们知道
标签:

CKEditor5——模型理解(八:Operation和Batch)

上一节主要学习并理解CK5中的Selection,今天主要来分析并研究一下CK5中的两个重要的概念:Operation和Batch。同时结合代码来深入理解。Operation属性baseVersion : Number这里我的理解是操作都是用在模型的文档上,而模型文档是有不同的版本的,因此操作也要有一个版本号,如果这个版本号与模型的版本号不一致,可能会导致错误。后面理解模型文档上的时候再来说这个属
标签:

CKEditor5——模型理解(七:Selection)

昨天我们学习了Range的一些API使用,今天我们看看另一个重要的类Selection的API:Selection的作用是记录鼠标在文档上的选择区域,如果是单个用户在编辑一份文档的时候,选择应该就是一个Range,如果是多个用户在编辑一份文档的时候,那么选择的区域就应该是多个range。因此,我大胆的猜测,Selection中应该有Range数组。我们来看看吧。Selection属性anchor
标签:

CKEditor5——模型理解(六:Range)

上一节我们主要介绍了模型中的Position这个关键的类,今天我们开始学习Range这个类。简单来说的话,如果Position表示一个点的话,那么Range是不是可以理解为一条线段呢?这个线段有一个startPostion,endPosition以及线段的长度等属性,我们暂且这么认为,那么我们可以看看Range官方的文档。从文档中看到,Range类有五个属性:Range属性start:Positi
标签:

CKEditor5事件系统(事件优先级)

今天继续学习CK5的事件系统,上一节我们知道了绑定和取消绑定事件的两种方法,知道在一个emitter上一个同名事件可以绑定多个回调函数,自然问题来了,这些函数的执行顺序是怎么样的呢?CK5的事件监听优先级实际上,对于一个同名事件,CK5提供了事件优先级功能,如下代码所示const anyClass = new AnyClass(); anyClass.on( 'eventName', ( even

CKEditor5——模型理解(五:Position, Range, Selection)

今天我们继续学习CK5中模型的一些知识,主要包括:Position, Range, Selection首先,我们需要知道:position表示模型树中的一个位置。模型的位置有两部分组成:root,path。即位置由其根和该根中的路径表示。位置基于偏移量,而不是索引。这意味着两个文本节点 foo 和 bar 之间的位置偏移为 3,而不是 1。由于模型中的位置由位置根和位置路径表示,因此可以创建不存在

CKEditor5——模型理解(三:Element Text)

在上一节,我们学习了CK5中模型节点Node的API,今天我们学习另一个常用的API:Element。元素节点说明element表示模型的元素节点类型,它包含一个拥有名称和子节点的节点类型,继承自Node类。元素属性说明1、name,元素的名称举个例子哈,段落的名称是paragraph,代码块的名称是codeBlock等等。2、childCount, 子元素的数目这里指的是此元素节点包含的子元素的
标签:

formControlName must be used with a parent formGroup directive. You'll want to add a formGroup

异常情况描述Angular模板出现异常情况,如下所示:异常分析大概的意思就是formControlName没有在formGroup里面,所以猜测应该是模板位置不正确。代码查看<form class="signin-container" [formGroup]="signinForm" #formDir="ngForm" (ngSubmit)="onSubmit()"> <
标签:

CKEditor5——视图添加

上一节我们介绍了在CK5中UI组件的一些基本使用,今天我们继续UI部分的学习,如何添加一个UI视图到CK5?CK5视图结构首先,我们贴上代码:EditorUIView ├── "top" collection │ └── ToolbarView │ └── "items" collection │ ├── DropdownView

CKEditor5 Observable——绑定多个对象或属性

上一节我们学习了如何绑定属性,今天我们继续学习绑定多个属性或者多个Observable对象。 绑定多个属性如何绑定多个属性,下面我们用代码来说明:const button = new Button(); const command = editor.commands.get( 'bold' ); button.bind( 'isOn', 'isEnabled' ).to( command

CKEditor5——Position源码分析(三)

发布于 2022.09.20 0分钟阅读 0 评论 5 推荐

    CKEditor5

    作者: 敲碎时间,铸造不朽
  1. CKEditor5事件系统(基础使用) Page 1
    1. CKEditor5事件系统(事件优先级) Page 12
    2. CKEditor5事件系统(代理事件) Page 25
    3. CKEditor5事件系统(视图事件冒泡) Page 32
    4. CKEditor5事件系统源码分析(一) Page 36
    5. CKEditor5事件系统源码分析(二) Page 54
    6. CKEditor5事件系统源码分析(三) Page 67
    7. CKeditor5事件系统命名空间分析 Page 73
  2. CKEditor5 模板绑定 Page 87
    1. CKEditor5 Observables Page 94
    2. CKEditor5 Observable——属性绑定 Page 100
    3. CKEditor5 Observable——绑定多个对象或属性 Page 108
    4. CKEditor5 Observable——装饰方法 Page 115
    5. CKEditor5 UI——UI组件 Page 127
    6. CKEditor5——视图添加 Page 137
  3. CKEditor5——模型理解(一) Page 144
    1. CKEditor5——模型理解(二:Node) Page 151
    2. CKEditor5——模型理解(三:Element Text) Page 160
    3. CKEditor5——Node,Element,NodeList源码分析 Page 163
    4. CKEditor5——模型理解(四:模型组成) Page 176
    5. CKEditor5——模型理解(五:Position, Range, Selection) Page 192
    6. CKEditor5——Position源码分析 Page 205
    7. CKEditor5——Position源码分析(二) Page 224
    8. CKEditor5——Position源码分析(三) Page 235
    9. CKEditor5——模型理解(六:Range) Page 247
    10. CKEditor5——模型理解(七:Selection) Page 261
    11. CKEditor5——模型理解(八:Operation和Batch) Page 272
    12. CKEditor5——(九:TreeWalker) Page 276
  4. CKEditor5——Conversion理解 Page 283
  5. CKEditor5——Utils(工具类理解) Page 300
    1. CKEditor5——Utils(Collection类理解) Page 312
    2. CKEditor5——Uitls(DomEmitterMixin类理解) Page 351
  6. CKEditor5——Plugin理解 Page 374
  7. CKEditor5——UI理解 Page 380
    1. CKEditor5——View理解 Page 393
    2. CKEditor5——Template理解(一) Page 403
    3. CKEditor5——Template理解(二) Page 423
    4. CKEditor5——Template理解(三) Page 460

CKEditor5——Position源码分析(三)

上一节我们学习了比较两个Position之间的关系,它们可能属于不同的根节点,它们可能完全相同,它们也可能存在前后关系。

今天我们继续学习这个类的另一个关键方法:那就是

getTransformedByOperation( operation ) → Position

getTransformedByOperation( operation ) {
	let result;

	switch ( operation.type ) {
		case 'insert':
			result = this._getTransformedByInsertOperation( operation );
			break;
		case 'move':
		case 'remove':
		case 'reinsert':
			result = this._getTransformedByMoveOperation( operation );
			break;
		case 'split':
			result = this._getTransformedBySplitOperation( operation );
			break;
		case 'merge':
			result = this._getTransformedByMergeOperation( operation );
			break;
		default:
			result = Position._createAt( this );
			break;
	}
	return result;
}

首先说明一下这个方法:

返回由给定操作转换的此位置的副本。新位置的参数会根据操作的效果进行相应更新。例如,如果在位置之前插入n个节点,则返回的位置偏移量将增加n。如果该位置在合并元素中,它将相应地移动到新元素等。这种方法可以安全地用于不存在的位置(例如在操作转换期间)。

从以上说明,我们可以看出,这个方法是当前位置应用一个给定的操作后返回一个新的位置,具体的操作有六类:insert,move,remove,reinsert,split,merge,而move,remove,reinsert又是同一种情况,我们先看看insert的情况:this._getTransformedByInsertOperation( operation );

_getTransformedByInsertOperation( operation ) {
	return this._getTransformedByInsertion( operation.position, operation.howMany );
}
_getTransformedByInsertion( insertPosition, howMany ) {
	const transformed = Position._createAt( this );

	// This position can't be affected if insertion was in a different root.
	if ( this.root != insertPosition.root ) {
		return transformed;
	}

	if ( compareArrays( insertPosition.getParentPath(), this.getParentPath() ) == 'same' ) {
		// If nodes are inserted in the node that is pointed by this position...
		if ( insertPosition.offset < this.offset || ( insertPosition.offset == this.offset && this.stickiness != 'toPrevious' ) ) {
			// And are inserted before an offset of that position...
			// "Push" this positions offset.
			transformed.offset += howMany;
		}
	} else if ( compareArrays( insertPosition.getParentPath(), this.getParentPath() ) == 'prefix' ) {
		// If nodes are inserted in a node that is on a path to this position...
		const i = insertPosition.path.length - 1;

		if ( insertPosition.offset <= this.path[ i ] ) {
			// And are inserted before next node of that path...
			// "Push" the index on that path.
			transformed.path[ i ] += howMany;
		}
	}

	return transformed;
}

我们看到insert的情况实际上分为三类,

1、如果当前位置的根节点与操作对应的位置的根节点不一致,那么直接返回当前位置,实际上可能啥都没有做,因为操作对应的位置没有啥效果

2、如果当前位置的parentPath与插入操作的位置的parentPath一致,那么这里分两种情况:

//第一种情况
// should increment offset if insertion is in the same parent and the same offset
const position = new Position( root, [ 1, 2, 3 ] );
position.stickiness = 'toNext';
const transformed = position._getTransformedByInsertion( new Position( root, [ 1, 2, 3 ] ), 2 );

expect( transformed.offset ).to.equal( 5 );
//第二种情况
//should increment offset if insertion is in the same parent and closer offset
const position = new Position( root, [ 1, 2, 3 ] );
const transformed = position._getTransformedByInsertion( new Position( root, [ 1, 2, 2 ] ), 2 );
expect( transformed.offset ).to.equal( 5 );

3、如果插入位置的parentPath在当前位置的parentPath之前,那么这种情况如下:

//should update path if insertion position parent is a node from that path and offset is before next node on that path
const position = new Position( root, [ 1, 2, 3 ] );
const transformed = position._getTransformedByInsertion( new Position( root, [ 1, 2 ] ), 2 );

expect( transformed.path ).to.deep.equal( [ 1, 4, 3 ] );

另外的其他情况的话,返回的位置都不会发生变化,因此,以上就是插入操作的情况下,可能发生新位置偏移的情况。另外move的情况比较复杂,我们以后分析。

 

 

CKEditor5——Position源码分析(二)

发布于 2022.09.20 10分钟阅读 0 评论 5 推荐

    CKEditor5

    作者: 敲碎时间,铸造不朽
  1. CKEditor5事件系统(基础使用) Page 1
    1. CKEditor5事件系统(事件优先级) Page 12
    2. CKEditor5事件系统(代理事件) Page 25
    3. CKEditor5事件系统(视图事件冒泡) Page 32
    4. CKEditor5事件系统源码分析(一) Page 36
    5. CKEditor5事件系统源码分析(二) Page 54
    6. CKEditor5事件系统源码分析(三) Page 67
    7. CKeditor5事件系统命名空间分析 Page 73
  2. CKEditor5 模板绑定 Page 87
    1. CKEditor5 Observables Page 94
    2. CKEditor5 Observable——属性绑定 Page 100
    3. CKEditor5 Observable——绑定多个对象或属性 Page 108
    4. CKEditor5 Observable——装饰方法 Page 115
    5. CKEditor5 UI——UI组件 Page 127
    6. CKEditor5——视图添加 Page 137
  3. CKEditor5——模型理解(一) Page 144
    1. CKEditor5——模型理解(二:Node) Page 151
    2. CKEditor5——模型理解(三:Element Text) Page 160
    3. CKEditor5——Node,Element,NodeList源码分析 Page 163
    4. CKEditor5——模型理解(四:模型组成) Page 176
    5. CKEditor5——模型理解(五:Position, Range, Selection) Page 192
    6. CKEditor5——Position源码分析 Page 205
    7. CKEditor5——Position源码分析(二) Page 224
    8. CKEditor5——Position源码分析(三) Page 235
    9. CKEditor5——模型理解(六:Range) Page 247
    10. CKEditor5——模型理解(七:Selection) Page 261
    11. CKEditor5——模型理解(八:Operation和Batch) Page 272
    12. CKEditor5——(九:TreeWalker) Page 276
  4. CKEditor5——Conversion理解 Page 283
  5. CKEditor5——Utils(工具类理解) Page 300
    1. CKEditor5——Utils(Collection类理解) Page 312
    2. CKEditor5——Uitls(DomEmitterMixin类理解) Page 351
  6. CKEditor5——Plugin理解 Page 374
  7. CKEditor5——UI理解 Page 380
    1. CKEditor5——View理解 Page 393
    2. CKEditor5——Template理解(一) Page 403
    3. CKEditor5——Template理解(二) Page 423
    4. CKEditor5——Template理解(三) Page 460

CKEditor5——Position源码分析(二)

上一节,我们学习了Position的基础知识,包括怎么寻找它的parent属性,它的path是什么意思?

今天我们看另一个方法,两个Position之间的关系是什么,也就是这个方法:

compareWith( otherPosition ) → PositionRelation

compareWith( otherPosition ) {
	if ( this.root != otherPosition.root ) {
		return 'different';
	}

	const result = compareArrays( this.path, otherPosition.path );

	switch ( result ) {
		case 'same':
			return 'same';

		case 'prefix':
			return 'before';

		case 'extension':
			return 'after';

		default:
			return this.path[ result ] < otherPosition.path[ result ] ? 'before' : 'after';
	}
}

从以上的代码可以看出,如果两个Position的根节点不一样,它们一定是不同的。它们之间关系的比较是通过一个工具方法:compareArrays( this.path, otherPosition.path );

下面我们看看这个方法:

export default function compareArrays( a, b ) {
	const minLen = Math.min( a.length, b.length );

	for ( let i = 0; i < minLen; i++ ) {
		if ( a[ i ] != b[ i ] ) {
			// The arrays are different.
			return i;
		}
	}

	// Both arrays were same at all points.
	if ( a.length == b.length ) {
		// If their length is also same, they are the same.
		return 'same';
	} else if ( a.length < b.length ) {
		// Compared array is shorter so it is a prefix of the other array.
		return 'prefix';
	} else {
		// Compared array is longer so it is an extension of the other array.
		return 'extension';
	}
}

首先获取两个数组长度最短的值minLen,然后比较前minLen个值,如果不相同的话没救返回不相同的位置的索引;如果前minLen个值都相同,如果a的长度比b的长度小,那么返回prefix;如果a的长度大于b的长度,那么返回extension;否则长度相同,值也相同,则返回same;

有了这个工具方法,我们可以知道,两个Position的关系可能有四种same,before,after,different

如果他们的path数组的值是一样的,那么这两个Position的关系是same;

如果当前PositionpathotherPositionpath之前,那么认为当前PositionotherPosition之前,否则就是之后。

这里的一个默认值是比较它们path的第一个不相等的值对应的大小,小的在前面,大的在后面。

 

有了以上的比较两个Position关系的方法,我们可以看看其他的几个方法,比如:

isAfter( otherPosition ) → Boolean

isBefore( otherPosition ) → Boolean

isEqual( otherPosition ) → Boolean

isTouching( otherPosition ) → Boolean

都是调用我们以上分析的方法来实现的,这里我们重点看看最后一个:

isTouching( otherPosition ) {
	let left = null;
	let right = null;
	const compare = this.compareWith( otherPosition );

	switch ( compare ) {
		case 'same':
			return true;

		case 'before':
			left = Position._createAt( this );
			right = Position._createAt( otherPosition );
			break;

		case 'after':
			left = Position._createAt( otherPosition );
			right = Position._createAt( this );
			break;

		default:
			return false;
	}

	// Cached for optimization purposes.
	let leftParent = left.parent;

	while ( left.path.length + right.path.length ) {
		if ( left.isEqual( right ) ) {
			return true;
		}

		if ( left.path.length > right.path.length ) {
			if ( left.offset !== leftParent.maxOffset ) {
				return false;
			}

			left.path = left.path.slice( 0, -1 );
			leftParent = leftParent.parent;
			left.offset++;
		} else {
			if ( right.offset !== 0 ) {
				return false;
			}

			right.path = right.path.slice( 0, -1 );
		}
	}
}

这里我们先看看什么是touching:检查此位置是否接触给定位置。当它们之间的范围内没有文本节点或空节点时,位置接触。从技术上讲,这些位置并不相同,但在许多情况下,它们非常相似甚至无法区分。

这里我们思考一下,两个Position接触的情况有哪些:

1、两个Position一模一样。

2、第一个Position在前面,第二个Position在后面,但是它们之间没有空隙

3、第一个Position在后面,第二个Position在前面,它们之间也没有空隙。

以上的方法实现就是针对的这三种情况,感兴趣的可以去仔细分析一下。

CKEditor5——Position源码分析

发布于 2022.08.31 20分钟阅读 0 评论 5 推荐

    CKEditor5

    作者: 敲碎时间,铸造不朽
  1. CKEditor5事件系统(基础使用) Page 1
    1. CKEditor5事件系统(事件优先级) Page 12
    2. CKEditor5事件系统(代理事件) Page 25
    3. CKEditor5事件系统(视图事件冒泡) Page 32
    4. CKEditor5事件系统源码分析(一) Page 36
    5. CKEditor5事件系统源码分析(二) Page 54
    6. CKEditor5事件系统源码分析(三) Page 67
    7. CKeditor5事件系统命名空间分析 Page 73
  2. CKEditor5 模板绑定 Page 87
    1. CKEditor5 Observables Page 94
    2. CKEditor5 Observable——属性绑定 Page 100
    3. CKEditor5 Observable——绑定多个对象或属性 Page 108
    4. CKEditor5 Observable——装饰方法 Page 115
    5. CKEditor5 UI——UI组件 Page 127
    6. CKEditor5——视图添加 Page 137
  3. CKEditor5——模型理解(一) Page 144
    1. CKEditor5——模型理解(二:Node) Page 151
    2. CKEditor5——模型理解(三:Element Text) Page 160
    3. CKEditor5——Node,Element,NodeList源码分析 Page 163
    4. CKEditor5——模型理解(四:模型组成) Page 176
    5. CKEditor5——模型理解(五:Position, Range, Selection) Page 192
    6. CKEditor5——Position源码分析 Page 205
    7. CKEditor5——Position源码分析(二) Page 224
    8. CKEditor5——Position源码分析(三) Page 235
    9. CKEditor5——模型理解(六:Range) Page 247
    10. CKEditor5——模型理解(七:Selection) Page 261
    11. CKEditor5——模型理解(八:Operation和Batch) Page 272
    12. CKEditor5——(九:TreeWalker) Page 276
  4. CKEditor5——Conversion理解 Page 283
  5. CKEditor5——Utils(工具类理解) Page 300
    1. CKEditor5——Utils(Collection类理解) Page 312
    2. CKEditor5——Uitls(DomEmitterMixin类理解) Page 351
  6. CKEditor5——Plugin理解 Page 374
  7. CKEditor5——UI理解 Page 380
    1. CKEditor5——View理解 Page 393
    2. CKEditor5——Template理解(一) Page 403
    3. CKEditor5——Template理解(二) Page 423
    4. CKEditor5——Template理解(三) Page 460

CKEditor5——Position源码分析

前面我们介绍过CK5模型中的一个关键类,那就是Position的使用,今天我们来看看它的源码,梳理一下方便以后理解:

export default class Position {
	/**
	 * Creates a position.
	 *
	 * @param {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment} root Root of the position.
	 * @param {Array.<Number>} path Position path. See {@link module:engine/model/position~Position#path}.
	 * @param {module:engine/model/position~PositionStickiness} [stickiness='toNone'] Position stickiness.
	 * See {@link module:engine/model/position~PositionStickiness}.
	 */
	constructor( root, path, stickiness = 'toNone' ) {
		if ( !root.is( 'element' ) && !root.is( 'documentFragment' ) ) {
			/**
			 * Position root is invalid.
			 *
			 * Positions can only be anchored in elements or document fragments.
			 *
			 * @error model-position-root-invalid
			 */
			throw new CKEditorError(
				'model-position-root-invalid',
				root
			);
		}

		if ( !( path instanceof Array ) || path.length === 0 ) {
			/**
			 * Position path must be an array with at least one item.
			 *
			 * @error model-position-path-incorrect-format
			 * @param path
			 */
			throw new CKEditorError(
				'model-position-path-incorrect-format',
				root,
				{ path }
			);
		}

		// Normalize the root and path when element (not root) is passed.
		if ( root.is( 'rootElement' ) ) {
			path = path.slice();
		} else {
			path = [ ...root.getPath(), ...path ];
			root = root.root;
		}

		/**
		 * Root of the position path.
		 *
		 * @readonly
		 * @member {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment}
		 * module:engine/model/position~Position#root
		 */
		this.root = root;

		/**
		 * Position of the node in the tree. **Path contains offsets, not indexes.**
		 *
		 * Position can be placed before, after or in a {@link module:engine/model/node~Node node} if that node has
		 * {@link module:engine/model/node~Node#offsetSize} greater than `1`. Items in position path are
		 * {@link module:engine/model/node~Node#startOffset starting offsets} of position ancestors, starting from direct root children,
		 * down to the position offset in it's parent.
		 *
		 *		 ROOT
		 *		  |- P            before: [ 0 ]         after: [ 1 ]
		 *		  |- UL           before: [ 1 ]         after: [ 2 ]
		 *		     |- LI        before: [ 1, 0 ]      after: [ 1, 1 ]
		 *		     |  |- foo    before: [ 1, 0, 0 ]   after: [ 1, 0, 3 ]
		 *		     |- LI        before: [ 1, 1 ]      after: [ 1, 2 ]
		 *		        |- bar    before: [ 1, 1, 0 ]   after: [ 1, 1, 3 ]
		 *
		 * `foo` and `bar` are representing {@link module:engine/model/text~Text text nodes}. Since text nodes has offset size
		 * greater than `1` you can place position offset between their start and end:
		 *
		 *		 ROOT
		 *		  |- P
		 *		  |- UL
		 *		     |- LI
		 *		     |  |- f^o|o  ^ has path: [ 1, 0, 1 ]   | has path: [ 1, 0, 2 ]
		 *		     |- LI
		 *		        |- b^a|r  ^ has path: [ 1, 1, 1 ]   | has path: [ 1, 1, 2 ]
		 *
		 * @readonly
		 * @member {Array.<Number>} module:engine/model/position~Position#path
		 */
		this.path = path;

		/**
		 * Position stickiness. See {@link module:engine/model/position~PositionStickiness}.
		 *
		 * @member {module:engine/model/position~PositionStickiness} module:engine/model/position~Position#stickiness
		 */
		this.stickiness = stickiness;
	}
 }

首先,我们分析构造函数,有三个关键的参数,那就是root,path,以及stickness

首先会检查当前的position的root属性如果不是Element类型或者DocumentFragment类型,那么直接会抛出错误,其次path必须是一个数组,且长度必须大于0。

这里有一个逻辑就是如果传递的root参数不是根节点,那么需要处理归一化root和path,当root是根节点的时候,path就直接复制,如果不是根节点,那么需要将根节点的path补上合并为新的数组。

最后就是将关键的三个属性赋值保存。

这里我们需要理解的path属性,它其实是从根节点到当前位置之间所有节点在父节点中的索引,然后将这些索引构成一个数组。这一点我们只需要看对代码属性的注释就不难分析出来。

我们知道Position还有一个关键的属性offset偏移量:

get offset() {
	return this.path[ this.path.length - 1 ];
}

set offset( newOffset ) {
	this.path[ this.path.length - 1 ] = newOffset;
}

在这里,偏移量就是数组path的最后一个值。这样的话,我们就将偏移量与path建立了关系。

Position还有一个parent属性:

get parent() {
	let parent = this.root;

	for ( let i = 0; i < this.path.length - 1; i++ ) {
		parent = parent.getChild( parent.offsetToIndex( this.path[ i ] ) );

		if ( !parent ) {
			/**
			 * The position's path is incorrect. This means that a position does not point to
			 * a correct place in the tree and hence, some of its methods and getters cannot work correctly.
			 *
			 * **Note**: Unlike DOM and view positions, in the model, the
			 * {@link module:engine/model/position~Position#parent position's parent} is always an element or a document fragment.
			 * The last offset in the {@link module:engine/model/position~Position#path position's path} is the point in this element
			 * where this position points.
			 *
			 * Read more about model positions and offsets in
			 * the {@glink framework/guides/architecture/editing-engine#indexes-and-offsets Editing engine architecture guide}.
			 *
			 * @error model-position-path-incorrect
			 * @param {module:engine/model/position~Position} position The incorrect position.
			 */
			throw new CKEditorError( 'model-position-path-incorrect', this, { position: this } );
		}
	}

	if ( parent.is( '$text' ) ) {
		throw new CKEditorError( 'model-position-path-incorrect', this, { position: this } );
	}

	return parent;
}

我们知道path存在的是当前位置的到根节点的所有节点的offset值,而获取当前位置的parent属性,本质上就是从根节点一直根据索引找下去,然后就可以找到当前位置的parent属性,这里在查找的时候用到了我们前几节讲述的Element的偏移量到索引值,根据索引值找节点的方法,一直找下去,如果存在,且不是Text节点,那么就返回找到的值,否则直接抛出错误。

同时Position类还有一个index属性

get index() {
	return this.parent.offsetToIndex( this.offset );
}

从代码可以看出,其实就是调用父节点的一些方法来实现。因此,只要我们理解了如何寻找父节点,那么这些其他方法也可以循序渐进的推导出来的。好了,大概的理解就是这些,后续遇到问题再具体分析,这一节的功能都是在Node那一部分为基础的,如果不理解的话,可以查看那一部分。

欢迎讨论。

 

 

CKEditor5——模型理解(二:Node)

更新于 2022.08.30 9分钟阅读 0 评论 5 推荐

    CKEditor5

    作者: 敲碎时间,铸造不朽
  1. CKEditor5事件系统(基础使用) Page 1
    1. CKEditor5事件系统(事件优先级) Page 12
    2. CKEditor5事件系统(代理事件) Page 25
    3. CKEditor5事件系统(视图事件冒泡) Page 32
    4. CKEditor5事件系统源码分析(一) Page 36
    5. CKEditor5事件系统源码分析(二) Page 54
    6. CKEditor5事件系统源码分析(三) Page 67
    7. CKeditor5事件系统命名空间分析 Page 73
  2. CKEditor5 模板绑定 Page 87
    1. CKEditor5 Observables Page 94
    2. CKEditor5 Observable——属性绑定 Page 100
    3. CKEditor5 Observable——绑定多个对象或属性 Page 108
    4. CKEditor5 Observable——装饰方法 Page 115
    5. CKEditor5 UI——UI组件 Page 127
    6. CKEditor5——视图添加 Page 137
  3. CKEditor5——模型理解(一) Page 144
    1. CKEditor5——模型理解(二:Node) Page 151
    2. CKEditor5——模型理解(三:Element Text) Page 160
    3. CKEditor5——Node,Element,NodeList源码分析 Page 163
    4. CKEditor5——模型理解(四:模型组成) Page 176
    5. CKEditor5——模型理解(五:Position, Range, Selection) Page 192
    6. CKEditor5——Position源码分析 Page 205
    7. CKEditor5——Position源码分析(二) Page 224
    8. CKEditor5——Position源码分析(三) Page 235
    9. CKEditor5——模型理解(六:Range) Page 247
    10. CKEditor5——模型理解(七:Selection) Page 261
    11. CKEditor5——模型理解(八:Operation和Batch) Page 272
    12. CKEditor5——(九:TreeWalker) Page 276
  4. CKEditor5——Conversion理解 Page 283
  5. CKEditor5——Utils(工具类理解) Page 300
    1. CKEditor5——Utils(Collection类理解) Page 312
    2. CKEditor5——Uitls(DomEmitterMixin类理解) Page 351
  6. CKEditor5——Plugin理解 Page 374
  7. CKEditor5——UI理解 Page 380
    1. CKEditor5——View理解 Page 393
    2. CKEditor5——Template理解(一) Page 403
    3. CKEditor5——Template理解(二) Page 423
    4. CKEditor5——Template理解(三) Page 460

上一节我们理解了基本的CK5的模型基本信息,今天我们来学习一些模型的API。

节点说明

首先,需要理解的就是模型的节点。在这一点上,CK5的模型节点和dom的节点有点类似,也有一些不同。我会在文章中一一介绍。

节点是模型树的基本结构。它是模型中不同节点类型的一种抽象。

这里需要指出的一点是:如果一个节点从模型树中分离出来,你可以使用它的 API 来操作它。但是,非常重要的是,已经附加到模型树的节点只能通过 Writer API 进行更改。

如果您修改文档根目录中的节点,使用节点方法(如 _insertChild 或 _setAttribute)完成的更改不会记录到文档的历史操作中,这可能对某些功能造成困扰。

节点属性说明

我们来看看节点有哪些基本属性:

1、index,节点索引

就是这个节点在父元素中的索引。举个例子

<heading>
	<paragraph><$text>abc</$text></paragraph> 	//paragraph-1
	<paragraph><$text>def</$text></paragraph>	//paragraph-2
	<paragraph></paragraph>					    //paragraph-3
</heading>

在上面的模型中,我们在 <heading>中定义了两个paragraph节点paragraph-1paragraph-2 在这种情况下paragraph-1的index值就是0而paragraph-2的index值就是1。还有一点需要注意就是index是相对于父元素,因此我们知道节点应该有另一个属性parent。

2、parent,节点父元素

节点的父元素不难理解哈,需要注意的是节点的父元素有两种类型Element | DocumentFragment

3、root,根元素

节点的根元素就是节点最顶层的祖先元素,如果节点没有在文档树结构上是,此时的root就是一个DocumentFragment

4、previousSibling,节点的前一个元素

这个属性用于快速访问节点的前一个兄弟元素

5、nextSubling,节点的后一个元素

这个属性用于快速访问节点的后一个兄弟元素

6、offsetSize,节点所占据的偏移大小。

这个属性需要仔细说说,还是用上面的例子举例,paragraph-3是元素节点类型,它里面没有文本元素,那么它的offsetSize就是1,也就是占据一个偏移大小,而paragraph-2中的文本节点<$text>def</$text>它就要占据3个偏移大小。

paragraph-2的offsetSize就是它的子元素的offsetSize之和。这里有没有问题?欢迎大家讨论?

7、startOffset,节点的起始偏移值

这个值用于指示节点的开始位置相对于父元素的偏移,它等于它之前所有兄弟的 offsetSize 的总和。

8、endOffset,节点的结束偏移值

这个值用于指示节点的结束位置相对于父元素的偏移,它等于该节点的起始偏移量和节点偏移量大小之和。

为了说明上面的问题,我新建了一个模型如上图所示,我们看看第一个paragraph的startOffset,endOffset以及offsetSize

可以看到对于元素节点类型,它的startOffset和endOffset的值是0和1,而它有一个maxOffset值来记录它的子元素占据的offsetSize。

对于文本节点类型,它的startOffset和endOffset的值是0和4,offsetSize是4,它有一个path,实际上就是节点的起始位置,有了这个位置,我们可以结合其他API来进行相应的创建位置,创建范围,创建选择区等等。这个我们以后再讨论。

节点方法说明

节点有很多判断属性和获取属性的方法,比较简单,我们就不详细说明啦。我们只看看几个比较重要且关键的方法。

1、getAncestors() ,返回祖先元素

这个方法用于返回节点的祖先元素数组

2、getCommonAncestor(node) ,返回共同祖先元素

这个方法用于返回参数节点和调用节点共同祖先元素。

3、getPath() ,获取节点的路径

这是一个最重要的方法,它返回节点的路径。路径是一个数组,其中包含此节点的连续祖先的起始偏移量,从根开始,一直到此节点的起始偏移量。该路径可用于创建 Position 实例。

为了说明问题,我还是举一个例子

const abc = new Text( 'abc' );
const foo = new Text( 'foo' );
const h1 = new Element( 'h1', null, new Text( 'header' ) );
const p = new Element( 'p', null, [ abc, foo ] );
const div = new Element( 'div', null, [ h1, p ] );
//generate dom
<div>
	<h1>header</h1>
	<p>abcfoo</p>
</div>
foo.getPath(); // Returns [ 1, 3 ]. `foo` is in `p` which is in `div`. `p` starts at offset 1, while `foo` at 3.
h1.getPath(); // Returns [ 0 ].
div.getPath(); // Returns [].

通过上面的例子,我想大家应该很容易理解这个方法了吧

4、is( type ) → Boolean

检查节点是否是指定类型。此方法在处理未知类型的模型对象时很有用。例如,一个函数可能返回一个 DocumentFragment 或一个可以是文本节点或元素的节点。此方法可用于检查返回的对象类型。还需要注意的是

这个方法会被它的很多子类覆盖重写

 

总结

今天我们对模型的节点这个类的属性和方法进行了基本的分析和介绍,特别是几个重点属性和方法,如果掌握清楚啦,对未来的CK学习将会有很大的帮助作用。欢迎各位留言讨论。

CKEditor5——Node,Element,NodeList源码分析

发布于 2022.08.30 15分钟阅读 0 评论 5 推荐

    CKEditor5

    作者: 敲碎时间,铸造不朽
  1. CKEditor5事件系统(基础使用) Page 1
    1. CKEditor5事件系统(事件优先级) Page 12
    2. CKEditor5事件系统(代理事件) Page 25
    3. CKEditor5事件系统(视图事件冒泡) Page 32
    4. CKEditor5事件系统源码分析(一) Page 36
    5. CKEditor5事件系统源码分析(二) Page 54
    6. CKEditor5事件系统源码分析(三) Page 67
    7. CKeditor5事件系统命名空间分析 Page 73
  2. CKEditor5 模板绑定 Page 87
    1. CKEditor5 Observables Page 94
    2. CKEditor5 Observable——属性绑定 Page 100
    3. CKEditor5 Observable——绑定多个对象或属性 Page 108
    4. CKEditor5 Observable——装饰方法 Page 115
    5. CKEditor5 UI——UI组件 Page 127
    6. CKEditor5——视图添加 Page 137
  3. CKEditor5——模型理解(一) Page 144
    1. CKEditor5——模型理解(二:Node) Page 151
    2. CKEditor5——模型理解(三:Element Text) Page 160
    3. CKEditor5——Node,Element,NodeList源码分析 Page 163
    4. CKEditor5——模型理解(四:模型组成) Page 176
    5. CKEditor5——模型理解(五:Position, Range, Selection) Page 192
    6. CKEditor5——Position源码分析 Page 205
    7. CKEditor5——Position源码分析(二) Page 224
    8. CKEditor5——Position源码分析(三) Page 235
    9. CKEditor5——模型理解(六:Range) Page 247
    10. CKEditor5——模型理解(七:Selection) Page 261
    11. CKEditor5——模型理解(八:Operation和Batch) Page 272
    12. CKEditor5——(九:TreeWalker) Page 276
  4. CKEditor5——Conversion理解 Page 283
  5. CKEditor5——Utils(工具类理解) Page 300
    1. CKEditor5——Utils(Collection类理解) Page 312
    2. CKEditor5——Uitls(DomEmitterMixin类理解) Page 351
  6. CKEditor5——Plugin理解 Page 374
  7. CKEditor5——UI理解 Page 380
    1. CKEditor5——View理解 Page 393
    2. CKEditor5——Template理解(一) Page 403
    3. CKEditor5——Template理解(二) Page 423
    4. CKEditor5——Template理解(三) Page 460

CKEditor5——Node,Element,NodeList源码分析

前面大概知道了模型的基本组成,包括一个抽象类Node,两个实现类TextElement,以及还有一个关键的类就是NodeList

为了容易展开这个话题,我首先提出一个问题,那就是为什么是Node类是一个抽象类?先看看这个类的具体代码:

export default class Node {
	/**
	 * Creates a model node.
	 *
	 * This is an abstract class, so this constructor should not be used directly.
	 *
	 * @abstract
	 * @param {Object} [attrs] Node's attributes. See {@link module:utils/tomap~toMap} for a list of accepted values.
	 */
	constructor( attrs ) {
		/**
		 * Parent of this node. It could be {@link module:engine/model/element~Element}
		 * or {@link module:engine/model/documentfragment~DocumentFragment}.
		 * Equals to `null` if the node has no parent.
		 *
		 * @readonly
		 * @member {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment|null}
		 */
		this.parent = null;

		/**
		 * Attributes set on this node.
		 *
		 * @private
		 * @member {Map} module:engine/model/node~Node#_attrs
		 */
		this._attrs = toMap( attrs );
	}
}

我们看这个类,首先,这个类应该能够存储属性,因此它有一个Map属性叫做this._attrs,此外还有一个属性叫做this.parent,因此有了这个属性之后,就可以用Node节点构成一个类似树形结构。

有了这个基础知识以后,看看一个具体的方法:

get index() {
	let pos;

	if ( !this.parent ) {
		return null;
	}

	if ( ( pos = this.parent.getChildIndex( this ) ) === null ) {
		throw new CKEditorError( 'model-node-not-found-in-parent', this );
	}

	return pos;
}

注意到没有,这个类的所有实现都是代理到了它的父节点的方法,在this.parent这个属性的类型有三类:Element,DocumentFragment,null,因此,可以大胆猜想,除了null之外,其他的类一定要实现具体方法。

用获取某个节点的索引来举例,实现的逻辑就是,判断当前节点在父节点中的索引位置就是具体的索引值。因此,我们可以看看Element.getChildIndex()这个方法:

Element类

export default class Element extends Node {
	/**
	 * Creates a model element.
	 *
	 * **Note:** Constructor of this class shouldn't be used directly in the code.
	 * Use the {@link module:engine/model/writer~Writer#createElement} method instead.
	 *
	 * @protected
	 * @param {String} name Element's name.
	 * @param {Object} [attrs] Element's attributes. See {@link module:utils/tomap~toMap} for a list of accepted values.
	 * @param {module:engine/model/node~Node|Iterable.<module:engine/model/node~Node>} [children]
	 * One or more nodes to be inserted as children of created element.
	 */
	constructor( name, attrs, children ) {
		super( attrs );

		/**
		 * Element name.
		 *
		 * @readonly
		 * @member {String} module:engine/model/element~Element#name
		 */
		this.name = name;

		/**
		 * List of children nodes.
		 *
		 * @private
		 * @member {module:engine/model/nodelist~NodeList} module:engine/model/element~Element#_children
		 */
		this._children = new NodeList();

		if ( children ) {
			this._insertChild( 0, children );
		}
	}
}

从Element类可以看出,这个类除了能保存属性值之外,还有两个属性就是名称和子节点,而子节点的实现就是NodeList

我们继续看getChildIndex()

getChildIndex( node ) {
	return this._children.getNodeIndex( node );
}

这个方法的具体实现其实是代理给了NodeList这个类。

export default class NodeList {
	/**
	 * Creates an empty node list.
	 *
	 * @protected
	 * @param {Iterable.<module:engine/model/node~Node>} nodes Nodes contained in this node list.
	 */
	constructor( nodes ) {
		/**
		 * Nodes contained in this node list.
		 *
		 * @private
		 * @member {Array.<module:engine/model/node~Node>}
		 */
		this._nodes = [];

		if ( nodes ) {
			this._insertNodes( 0, nodes );
		}
	}
}

NodeList类其实就是Node数组的一个封装。

getNodeIndex()

getNodeIndex( node ) {
	const index = this._nodes.indexOf( node );
	return index == -1 ? null : index;
}

这下逻辑很清晰了吧,获取某个节点在数组中的位置,然后判断,如果不存在的话,返回null,否则返回具体的索引。知道理清楚了Node,NodeList,Element之间的关系,就能明白每一个方法是怎样实现的。

好了,剩余的其它方法都可以按照上面的逻辑来进行梳理和分析。

我们再看看一个属性叫做offsetSize,这个属性表示当前节点占据的偏移量值的大小,普通节点的值是1,而文本节点的大小就是文本的长度。

Text

get offsetSize() {
	return this.data.length;
}

Node

get offsetSize() {
	return 1;
}

另一个属性就是maxOffset

这个属性是只有Element,其实表示的就是当前节点占据的偏移量的大小。

get maxOffset() {
	return this._children.maxOffset;
}

NodeList

get maxOffset() {
	return this._nodes.reduce( ( sum, node ) => sum + node.offsetSize, 0 );
}

这里的逻辑就是将子节点的offsetSize相加即可。注意一点的是,这里的偏移量一定是相对于父节点的。

CKEditor5——模型理解(四:模型组成)

更新于 2023.01.12 16分钟阅读 0 评论 5 推荐

    CKEditor5

    作者: 敲碎时间,铸造不朽
  1. CKEditor5事件系统(基础使用) Page 1
    1. CKEditor5事件系统(事件优先级) Page 12
    2. CKEditor5事件系统(代理事件) Page 25
    3. CKEditor5事件系统(视图事件冒泡) Page 32
    4. CKEditor5事件系统源码分析(一) Page 36
    5. CKEditor5事件系统源码分析(二) Page 54
    6. CKEditor5事件系统源码分析(三) Page 67
    7. CKeditor5事件系统命名空间分析 Page 73
  2. CKEditor5 模板绑定 Page 87
    1. CKEditor5 Observables Page 94
    2. CKEditor5 Observable——属性绑定 Page 100
    3. CKEditor5 Observable——绑定多个对象或属性 Page 108
    4. CKEditor5 Observable——装饰方法 Page 115
    5. CKEditor5 UI——UI组件 Page 127
    6. CKEditor5——视图添加 Page 137
  3. CKEditor5——模型理解(一) Page 144
    1. CKEditor5——模型理解(二:Node) Page 151
    2. CKEditor5——模型理解(三:Element Text) Page 160
    3. CKEditor5——Node,Element,NodeList源码分析 Page 163
    4. CKEditor5——模型理解(四:模型组成) Page 176
    5. CKEditor5——模型理解(五:Position, Range, Selection) Page 192
    6. CKEditor5——Position源码分析 Page 205
    7. CKEditor5——Position源码分析(二) Page 224
    8. CKEditor5——Position源码分析(三) Page 235
    9. CKEditor5——模型理解(六:Range) Page 247
    10. CKEditor5——模型理解(七:Selection) Page 261
    11. CKEditor5——模型理解(八:Operation和Batch) Page 272
    12. CKEditor5——(九:TreeWalker) Page 276
  4. CKEditor5——Conversion理解 Page 283
  5. CKEditor5——Utils(工具类理解) Page 300
    1. CKEditor5——Utils(Collection类理解) Page 312
    2. CKEditor5——Uitls(DomEmitterMixin类理解) Page 351
  6. CKEditor5——Plugin理解 Page 374
  7. CKEditor5——UI理解 Page 380
    1. CKEditor5——View理解 Page 393
    2. CKEditor5——Template理解(一) Page 403
    3. CKEditor5——Template理解(二) Page 423
    4. CKEditor5——Template理解(三) Page 460

'insertContent', 'deleteContent', 'modifySelection''insertContent', 'deleteContent', 'modifySelection', 'getSelectedContent', 'applyOperation'今天我们来深入学习一下CK5的模型。

我们先看看model.js的源码:

export default class Model {
	constructor() {
		
		this.markers = new MarkerCollection();


		this.document = new Document( this );


		this.schema = new Schema();

		this._pendingChanges = [];

		this._currentWriter = null;

		[ 'insertContent', 'deleteContent', 'modifySelection', 'getSelectedContent', 'applyOperation' ]
			.forEach( methodName => this.decorate( methodName ) );

		this.on( 'applyOperation', ( evt, args ) => {
			const operation = args[ 0 ];

			operation._validate();
		}, { priority: 'highest' } );

		// Register some default abstract entities.
		this.schema.register( '$root', {
			isLimit: true
		} );

		this.schema.register( '$block', {
			allowIn: '$root',
			isBlock: true
		} );

		this.schema.register( '$text', {
			allowIn: '$block',
			isInline: true,
			isContent: true
		} );

		this.schema.register( '$clipboardHolder', {
			allowContentOf: '$root',
			allowChildren: '$text',
			isLimit: true
		} );

		this.schema.register( '$documentFragment', {
			allowContentOf: '$root',
			allowChildren: '$text',
			isLimit: true
		} );

		this.schema.register( '$marker' );
		this.schema.addChildCheck( ( context, childDefinition ) => {
			if ( childDefinition.name === '$marker' ) {
				return true;
			}
		} );

		injectSelectionPostFixer( this );


		this.document.registerPostFixer( autoParagraphEmptyRoots );

	}
}

从上面的源码,我们可以看出。模型部分包含的功能比较多,主要的有一下几点:

1、一个存储marker的集合

2、一个模型文档属性。比如<root><paragraph></paragraph></root>。主要是ck的模型数据

3、一个存储schema的属性,并且定义一些基本元素,比如$root,$block,$text,$clipboardHolder,$documentFragment,$marker

4、一个存储模型变化操作的回调函数。

5、一个用于操作修改模型的writer

6、装饰一些可以在外部监听的方法:比如'insertContent', 'deleteContent', 'modifySelection', 'getSelectedContent', 'applyOperation'

7、注册了一个模型后处理器。

8、绑定了一个监听applyOperation事件的监听函数,用于验证操作的合法性。

在这里,我们重点分析一下:model.change(callback)这个方法:

change( callback ) {
	try {
		if ( this._pendingChanges.length === 0 ) {
	
			this._pendingChanges.push( { batch: new Batch(), callback } );
			return this._runPendingChanges()[ 0 ];
		} else {
	
			return callback( this._currentWriter );
		}
	} catch ( err ) {
		CKEditorError.rethrowUnexpectedError( err, this );
	}
}

_runPendingChanges() {
	const ret = [];

	this.fire( '_beforeChanges' );

	while ( this._pendingChanges.length ) {

		const currentBatch = this._pendingChanges[ 0 ].batch;
		this._currentWriter = new Writer( this, currentBatch );
		//当有嵌套调用的时候,这里实际上会形成一个递归调用
		const callbackReturnValue = this._pendingChanges[ 0 ].callback( this._currentWriter );
		ret.push( callbackReturnValue );

		this.document._handleChangeBlock( this._currentWriter );

		this._pendingChanges.shift();
		this._currentWriter = null;
	}

	this.fire( '_afterChanges' );

	return ret;
}
//举个例子
model.change( writer => {
	 writer.insertText( 'foo', paragraph, 'end' ); // foo.
	 
	 model.change( writer => {
	 	writer.insertText( 'bar', paragraph, 'end' ); // foobar.
	 } );
	 
	  writer.insertText( 'bom', paragraph, 'end' ); // foobarbom.
} );

可以看到:在例子中,外层调用的时候,this._pendingChanges为空,这个时候会执行

this._pendingChanges.push( { batch: new Batch(), callback } );
return this._runPendingChanges()[ 0 ];

这时,会创建一个叫做Batch的对象,同时运行一个叫做_runPendingChanges()的函数。

这个函数的逻辑就是创建一个模型writer来处理模型文档块改变的业务逻辑。同时还触发了两个事件_beforeChanges_afterChanges,注意,这个writer的batch属性是最外层调用时候创建的,因此内层的函数调用时候使用的这个writer将共享这个Batch,因此,外层和内层实际上是用一个Batch。当调用结束以后,这个this._pendingChanges会被移除掉。

 

我们再看看另一个方法:enqueueChange([ batchOrType ], callback)

enqueueChange( batchOrType, callback ) {
	try {
		if ( typeof batchOrType === 'string' ) {
			batchOrType = new Batch( batchOrType );
		} else if ( typeof batchOrType == 'function' ) {
			callback = batchOrType;
			batchOrType = new Batch();
		}

		this._pendingChanges.push( { batch: batchOrType, callback } );

		if ( this._pendingChanges.length == 1 ) {
			this._runPendingChanges();
		}
	} catch ( err ) {
		// @if CK_DEBUG // throw err;
		/* istanbul ignore next */
		CKEditorError.rethrowUnexpectedError( err, this );
	}
}

可以看到,这个方法多了一个参数就是batchOrType,这个参数可能是string,或者Batch类型,当属于没有参数,或者参数类型为string时,我用下面的例子说明

model.change( writer => {
	console.log( 1 );

	model.enqueueChange( writer => {
		console.log( 2 );
	} );

	console.log( 3 );
} ); // Will log: 1, 3, 2.

从上面的代码可以看出,只有在this._pendingChanges == 1时,才会执行enqueueChanges()的回调函数,实际上上面的逻辑就是最后一个执行,所以以上代码会最后打印出2。

当这个参数是从最外层的调用而来的时候,此时这个回调函数将共享这个batch,实际上就是起到了自己将定义的操作放到某个Batch的作用。

好了,今天分享了模型的相关属性和修改模型的方法原理,欢迎分享讨论。

CKEditor5——Template理解(三)

发布于 2022.08.26 22分钟阅读 0 评论 5 推荐

    CKEditor5

    作者: 敲碎时间,铸造不朽
  1. CKEditor5事件系统(基础使用) Page 1
    1. CKEditor5事件系统(事件优先级) Page 12
    2. CKEditor5事件系统(代理事件) Page 25
    3. CKEditor5事件系统(视图事件冒泡) Page 32
    4. CKEditor5事件系统源码分析(一) Page 36
    5. CKEditor5事件系统源码分析(二) Page 54
    6. CKEditor5事件系统源码分析(三) Page 67
    7. CKeditor5事件系统命名空间分析 Page 73
  2. CKEditor5 模板绑定 Page 87
    1. CKEditor5 Observables Page 94
    2. CKEditor5 Observable——属性绑定 Page 100
    3. CKEditor5 Observable——绑定多个对象或属性 Page 108
    4. CKEditor5 Observable——装饰方法 Page 115
    5. CKEditor5 UI——UI组件 Page 127
    6. CKEditor5——视图添加 Page 137
  3. CKEditor5——模型理解(一) Page 144
    1. CKEditor5——模型理解(二:Node) Page 151
    2. CKEditor5——模型理解(三:Element Text) Page 160
    3. CKEditor5——Node,Element,NodeList源码分析 Page 163
    4. CKEditor5——模型理解(四:模型组成) Page 176
    5. CKEditor5——模型理解(五:Position, Range, Selection) Page 192
    6. CKEditor5——Position源码分析 Page 205
    7. CKEditor5——Position源码分析(二) Page 224
    8. CKEditor5——Position源码分析(三) Page 235
    9. CKEditor5——模型理解(六:Range) Page 247
    10. CKEditor5——模型理解(七:Selection) Page 261
    11. CKEditor5——模型理解(八:Operation和Batch) Page 272
    12. CKEditor5——(九:TreeWalker) Page 276
  4. CKEditor5——Conversion理解 Page 283
  5. CKEditor5——Utils(工具类理解) Page 300
    1. CKEditor5——Utils(Collection类理解) Page 312
    2. CKEditor5——Uitls(DomEmitterMixin类理解) Page 351
  6. CKEditor5——Plugin理解 Page 374
  7. CKEditor5——UI理解 Page 380
    1. CKEditor5——View理解 Page 393
    2. CKEditor5——Template理解(一) Page 403
    3. CKEditor5——Template理解(二) Page 423
    4. CKEditor5——Template理解(三) Page 460

CKEditor5——Template理解(三)

上一节我们分析了render()方法中关键的_renderText(),我们再来看看另一个方法就是renderElement()

_renderElement( data ) {
	let node = data.node;

	if ( !node ) {
		node = data.node = document.createElementNS( this.ns || xhtmlNs, this.tag );
	}

	this._renderAttributes( data );
	this._renderElementChildren( data );
	this._setUpListeners( data );

	return node;
}

从这里可以看出,渲染元素节点和文本节点有差别,那就是渲染元素节点,分为三步:

1、渲染节点的属性

2、渲染元素的子元素

3、设置当前元素的事件监听器。

在此之前需要判断当前数据是否存在node属性,一般来说,如果是render()方法调用,那么是没有node属性的,需要根据tag来创建一个节点,然后才开始渲染;而apply方法是直接有node属性,可以直接渲染。

我们再来看看this._renderAttributes( data )

_renderAttributes( data ) {
	let attrName, attrValue, domAttrValue, attrNs;

	if ( !this.attributes ) {
		return;
	}

	const node = data.node;
	const revertData = data.revertData;

	for ( attrName in this.attributes ) {
		// Current attribute value in DOM.
		domAttrValue = node.getAttribute( attrName );

		// The value to be set.
		attrValue = this.attributes[ attrName ];

		// Save revert data.
		if ( revertData ) {
			revertData.attributes[ attrName ] = domAttrValue;
		}

		// Detect custom namespace:
		//
		//		class: {
		//			ns: 'abc',
		//			value: Template.bind( ... ).to( ... )
		//		}
		//
		attrNs = ( isObject( attrValue[ 0 ] ) && attrValue[ 0 ].ns ) ? attrValue[ 0 ].ns : null;

		// Activate binding if one is found. Cases:
		//
		//		class: [
		//			Template.bind( ... ).to( ... )
		//		]
		//
		//		class: [
		//			'bar',
		//			Template.bind( ... ).to( ... ),
		//			'baz'
		//		]
		//
		//		class: {
		//			ns: 'abc',
		//			value: Template.bind( ... ).to( ... )
		//		}
		//
		if ( hasTemplateBinding( attrValue ) ) {
			// Normalize attributes with additional data like namespace:
			//
			//		class: {
			//			ns: 'abc',
			//			value: [ ... ]
			//		}
			//
			const valueToBind = attrNs ? attrValue[ 0 ].value : attrValue;

			// Extend the original value of attributes like "style" and "class",
			// don't override them.
			if ( revertData && shouldExtend( attrName ) ) {
				valueToBind.unshift( domAttrValue );
			}

			this._bindToObservable( {
				schema: valueToBind,
				updater: getAttributeUpdater( node, attrName, attrNs ),
				data
			} );
		}

		// Style attribute could be an Object so it needs to be parsed in a specific way.
		//
		//		style: {
		//			width: '100px',
		//			height: Template.bind( ... ).to( ... )
		//		}
		//
		else if ( attrName == 'style' && typeof attrValue[ 0 ] !== 'string' ) {
			this._renderStyleAttribute( attrValue[ 0 ], data );
		}

		// Otherwise simply set the static attribute:
		//
		//		class: [ 'foo' ]
		//
		//		class: [ 'all', 'are', 'static' ]
		//
		//		class: [
		//			{
		//				ns: 'abc',
		//				value: [ 'foo' ]
		//			}
		//		]
		//
		else {
			// Extend the original value of attributes like "style" and "class",
			// don't override them.
			if ( revertData && domAttrValue && shouldExtend( attrName ) ) {
				attrValue.unshift( domAttrValue );
			}

			attrValue = attrValue
				// Retrieve "values" from:
				//
				//		class: [
				//			{
				//				ns: 'abc',
				//				value: [ ... ]
				//			}
				//		]
				//
				.map( val => val ? ( val.value || val ) : val )
				// Flatten the array.
				.reduce( ( prev, next ) => prev.concat( next ), [] )
				// Convert into string.
				.reduce( arrayValueReducer, '' );

			if ( !isFalsy( attrValue ) ) {
				node.setAttributeNS( attrNs, attrName, attrValue );
			}
		}
	}
}

这个方法的逻辑比较多,但是总的来说分为两种情况,对普通属性的处理和对有bind.to()或者bind.if()

我们具体来看看:

1、首先是将当前节点的属性值取出来进行迭代,如果不存在直接返回,同时拿出当前数据的revertData数据用于记录之前的状态。

2、其次在迭代中判断属性是否为TemplateBinding,如果是的话需要设置响应的Observable监听,这里的监听我们在上一节已经分析过,感兴趣的可以去看看。

3、如果属性的名称是style,也就是css的style属性,那么需要调用单独的this._renderStyleAttribute

这个方法的处理和属性处理类似

4、最后一种情况就是普通属性的处理,因为属性的值一般都是数组,因此将这些属性联结起来后设置到节点上即可。

 

我们再看看第二方法this._renderElementChildren( data )

_renderElementChildren( data ) {
	const node = data.node;
	const container = data.intoFragment ? document.createDocumentFragment() : node;
	const isApplying = data.isApplying;
	let childIndex = 0;

	for ( const child of this.children ) {
		if ( isViewCollection( child ) ) {
			if ( !isApplying ) {
				child.setParent( node );

				// Note: ViewCollection renders its children.
				for ( const view of child ) {
					container.appendChild( view.element );
				}
			}
		} else if ( isView( child ) ) {
			if ( !isApplying ) {
				if ( !child.isRendered ) {
					child.render();
				}

				container.appendChild( child.element );
			}
		} else if ( isNode( child ) ) {
			container.appendChild( child );
		} else {
			if ( isApplying ) {
				const revertData = data.revertData;
				const childRevertData = getEmptyRevertData();

				revertData.children.push( childRevertData );

				child._renderNode( {
					node: container.childNodes[ childIndex++ ],
					isApplying: true,
					revertData: childRevertData
				} );
			} else {
				container.appendChild( child.render() );
			}
		}
	}

	if ( data.intoFragment ) {
		node.appendChild( container );
	}
}

这里渲染子节点的的时候也分为两种情况,render调用和apply调用:

1、如果是render()调用,那么此时会data.intoFragment为true,根据逻辑,会创建一个documentFragment

将这个节点作为container,然后分为三种情况处理节点:第一种情况是如果子节点是ViewCollection,此时就直接将节点的element添加到container;第二种情况是如果子节点是View,那么需要渲染后才能添加子节点的element;最后一种情况是节点是一个Node,此时直接添加就可以啦;

2、如果是apply()调用,那么这里实际上就是一种递归调用,然后将渲染后的子节点添加到container

具体的逻辑大家可以参考源代码进行分析,如果有啥问题,欢迎讨论哈。

最后我们看看这个方法this._setUpListeners( data )

_setUpListeners( data ) {
	if ( !this.eventListeners ) {
		return;
	}

	for ( const key in this.eventListeners ) {
		const revertBindings = this.eventListeners[ key ].map( schemaItem => {
			const [ domEvtName, domSelector ] = key.split( '@' );

			return schemaItem.activateDomEventListener( domEvtName, domSelector, data );
		} );

		if ( data.revertData ) {
			data.revertData.bindings.push( revertBindings );
		}
	}
}

如果大家看看上一篇的TemplateBinding的分析,就会对这里有所理解,这个方法的主要作用就是看看当前节点是否存在eventListeners,如果不存在直接返回,如果存在的话,那么迭代此监听器,这个迭代的值

this.eventListeners[key]实际上就是TemplateBinding类型,这里的逻辑就是从key中获取事件名称和dom元素选择器,调用activateDomEventListener绑定dom节点事件,返回值也是一个函数,这个函数的作用就是取消绑定,从这里不难看出revertBindings是一个取消绑定的函数,而这个值会被添加到data.revertData.bindings这个数组中,在revert()方法调用恢复以前的状态的时候会取消dom事件的绑定。

好了,这个渲染dom的逻辑基本上就分析结束了。

这里提一个问题,在渲染子节点的时候,如果child是ViewCollection的时候,为什么子节点不需要渲染呢?

这里我们之前分析的时候其实已经理解过,欢迎大家讨论学习。

 

 

CKEditor5——Template理解(二)

更新于 2022.08.26 38分钟阅读 0 评论 5 推荐

    CKEditor5

    作者: 敲碎时间,铸造不朽
  1. CKEditor5事件系统(基础使用) Page 1
    1. CKEditor5事件系统(事件优先级) Page 12
    2. CKEditor5事件系统(代理事件) Page 25
    3. CKEditor5事件系统(视图事件冒泡) Page 32
    4. CKEditor5事件系统源码分析(一) Page 36
    5. CKEditor5事件系统源码分析(二) Page 54
    6. CKEditor5事件系统源码分析(三) Page 67
    7. CKeditor5事件系统命名空间分析 Page 73
  2. CKEditor5 模板绑定 Page 87
    1. CKEditor5 Observables Page 94
    2. CKEditor5 Observable——属性绑定 Page 100
    3. CKEditor5 Observable——绑定多个对象或属性 Page 108
    4. CKEditor5 Observable——装饰方法 Page 115
    5. CKEditor5 UI——UI组件 Page 127
    6. CKEditor5——视图添加 Page 137
  3. CKEditor5——模型理解(一) Page 144
    1. CKEditor5——模型理解(二:Node) Page 151
    2. CKEditor5——模型理解(三:Element Text) Page 160
    3. CKEditor5——Node,Element,NodeList源码分析 Page 163
    4. CKEditor5——模型理解(四:模型组成) Page 176
    5. CKEditor5——模型理解(五:Position, Range, Selection) Page 192
    6. CKEditor5——Position源码分析 Page 205
    7. CKEditor5——Position源码分析(二) Page 224
    8. CKEditor5——Position源码分析(三) Page 235
    9. CKEditor5——模型理解(六:Range) Page 247
    10. CKEditor5——模型理解(七:Selection) Page 261
    11. CKEditor5——模型理解(八:Operation和Batch) Page 272
    12. CKEditor5——(九:TreeWalker) Page 276
  4. CKEditor5——Conversion理解 Page 283
  5. CKEditor5——Utils(工具类理解) Page 300
    1. CKEditor5——Utils(Collection类理解) Page 312
    2. CKEditor5——Uitls(DomEmitterMixin类理解) Page 351
  6. CKEditor5——Plugin理解 Page 374
  7. CKEditor5——UI理解 Page 380
    1. CKEditor5——View理解 Page 393
    2. CKEditor5——Template理解(一) Page 403
    3. CKEditor5——Template理解(二) Page 423
    4. CKEditor5——Template理解(三) Page 460

CKEditor5——Template理解(二)

在上一节我们分析了类Template的构造函数,今天我们继续学习其他的方法:

我们先看看render()方法

render() {
	const node = this._renderNode( {
		intoFragment: true
	} );

	this._isRendered = true;

	return node;
}
// const domNode = new Template( { ... } ).render(); 然后将此节点添加到某个dom,然后
// 就可以操作此dom节点啦。

这个方法的主要作用就是渲染出来一个dom节点,然后设置当前的模板this._isRendered = true,并且返回当前节点。为了理解这个方法,我们需要理解this._renderNode(data)

_renderNode( data ) {
	let isInvalid;

	if ( data.node ) {
		// When applying, a definition cannot have "tag" and "text" at the same time.
		isInvalid = this.tag && this.text;
	} else {
		// When rendering, a definition must have either "tag" or "text": XOR( this.tag, this.text ).
		isInvalid = this.tag ? this.text : !this.text;
	}

	if ( isInvalid ) {
		/**
		 * Node definition cannot have the "tag" and "text" properties at the same time.
		 * Node definition must have either "tag" or "text" when rendering a new Node.
		 *
		 * @error ui-template-wrong-syntax
		 */
		throw new CKEditorError(
			'ui-template-wrong-syntax',
			this
		);
	}

	if ( this.text ) {
		return this._renderText( data );
	} else {
		return this._renderElement( data );
	}
}

从这个方法,我们可以看出,首先会判断节点的数据是否合法,这里有一个逻辑就是tag和text属性不能同时存在,如果同时存在就认为是不合法的,并且还有一点需要注意的是:这个方法会被render(data)apply(data)调用,它们的判断逻辑是不一样的。剩下的逻辑就是如果当前的节点是text,那么就调用this._renderText( data ),否则调用this._renderElement( data )

下面我们看看this._renderText( data )

_renderText( data ) {
	let node = data.node;

	// Save the original textContent to revert it in #revert().
	if ( node ) {
		data.revertData.text = node.textContent;
	} else {
		node = data.node = document.createTextNode( '' );
	}

	// Check if this Text Node is bound to Observable. Cases:
	//
	//		text: [ Template.bind( ... ).to( ... ) ]
	//
	//		text: [
	//			'foo',
	//			Template.bind( ... ).to( ... ),
	//			...
	//		]
	//
	if ( hasTemplateBinding( this.text ) ) {
		this._bindToObservable( {
			schema: this.text,
			updater: getTextUpdater( node ),
			data
		} );
	}
	// Simply set text. Cases:
	//
	//		text: [ 'all', 'are', 'static' ]
	//
	//		text: [ 'foo' ]
	//
	else {
		node.textContent = this.text.join( '' );
	}

	return node;
}

我们看这个方法,可以知道,首先会将文本节点的数据存在在一个叫做data.revertData.text,这个属性revertData属性的作用我们暂时不用理解,后面介绍applyrevert方法的时候才来学习这个属性。如果node不存在就创建一个文本节点,并且将文本属性这是到node.textContent。这里还有一个逻辑就是检查当前的文本节点是否绑定了Observable,如果有这样的绑定,那么需要执行对文本节点的绑定添加事件,this._bindToObservable( { schema: this.text, updater: getTextUpdater( node ), data } );

我们再看看以上的这个方法,不过呢?我们先看看hasTemplateBinding( )这个方法

function hasTemplateBinding( schema ) {
	if ( !schema ) {
		return false;
	}

	// Normalize attributes with additional data like namespace:
	//
	//		class: {
	//			ns: 'abc',
	//			value: [ ... ]
	//		}
	//
	if ( schema.value ) {
		schema = schema.value;
	}

	if ( Array.isArray( schema ) ) {
		return schema.some( hasTemplateBinding );
	} else if ( schema instanceof TemplateBinding ) {
		return true;
	}

	return false;
}

这里的逻辑比较简单,我拿this.text来举例哈,如果此属性包含类似:

text: [
	//			'foo',
	//			Template.bind( ... ).to( ... ),
	//			...
	//		
]

比如,以上就是一个包含TemplateBinding的数组,因此这样的情况就需要处理绑定方法:

_bindToObservable( { schema, updater, data } ) {
	const revertData = data.revertData;

	// Set initial values.
	syncValueSchemaValue( schema, updater, data );

	const revertBindings = schema
		// Filter "falsy" (false, undefined, null, '') value schema components out.
		.filter( item => !isFalsy( item ) )
		// Filter inactive bindings from schema, like static strings ('foo'), numbers (42), etc.
		.filter( item => item.observable )
		// Once only the actual binding are left, let the emitter listen to observable change:attribute event.
		// TODO: Reduce the number of listeners attached as many bindings may listen
		// to the same observable attribute.
		.map( templateBinding => templateBinding.activateAttributeListener( schema, updater, data ) );

	if ( revertData ) {
		revertData.bindings.push( revertBindings );
	}
}

这个方法看着比较复杂,但是我们分三步来看:

1、首先拿出revertData属性,然后将此属性的bindings添加一个revertBindings,这个是个啥呢,我告诉你,这个实际上就是一个数组,并且数组中包含的值是函数。

2、其次就是syncValueSchemaValue这个方法,作用就是初始化一些值,

3、最后就是对参数的schema进行处理,首先过滤掉一些为false的对象,然后再筛选出包含observable的schema,最后就是剩下的templateBinding执行添加属性监听器。

我们先看看syncValueSchemaValue这个函数

function syncValueSchemaValue( schema, updater, { node } ) {
	let value = getValueSchemaValue( schema, node );

	// Check if schema is a single Template.bind.if, like:
	//
	//		class: Template.bind.if( 'foo' )
	//
	if ( schema.length == 1 && schema[ 0 ] instanceof TemplateIfBinding ) {
		value = value[ 0 ];
	} else {
		value = value.reduce( arrayValueReducer, '' );
	}

	if ( isFalsy( value ) ) {
		updater.remove();
	} else {
		updater.set( value );
	}
}
function getValueSchemaValue( schema, node ) {
	return schema.map( schemaItem => {
		// Process {@link module:ui/template~TemplateBinding} bindings.
		if ( schemaItem instanceof TemplateBinding ) {
			return schemaItem.getValue( node );
		}

		// All static values like strings, numbers, and "falsy" values (false, null, undefined, '', etc.) just pass.
		return schemaItem;
	} );
}
function getTextUpdater( node ) {
	return {
		set( value ) {
			node.textContent = value;
		},

		remove() {
			node.textContent = '';
		}
	};
}

我们需要理解这个参数updater这个有函数getTextUpdater知道这个实际上是一个对象,包含两个方法,设置值和移除值,就是对当前的文本节点设置值和将当前节点的值设置为空字符串。我们根据逻辑知道

getValueSchemaValue这个函数返回的值为false,则将当前节点的文本属性设置为空,否则就将此值修改为新的值。同时我们也知道这个返回值是一个数组,这个数组的值是怎么来的呢?

getValueSchemaValue的具体实现可以看出,如果schemaItem只是普通文本,那么直接返回,如果是TemplateBinding,那么需要获取新的值,然后对这些值进行处理后设置。设置的时候分为数组长度为1或者数组长度大于1的情况,数组长度为1的时候就直接取第一个值,否则就是将这这些值连起来。

比如:

{
    text: [
		'aaa'
    ]
}
//以上就是数组是一个值的情况
{
    text: [
		'aaa',
		'bbb'
    ]
}
//以上就是数组是多个值的情况

为了理解以上情况,我们需要知道什么是TemplateBinding

export class TemplateBinding {
	/**
	 * Creates an instance of the {@link module:ui/template~TemplateBinding} class.
	 *
	 * @param {module:ui/template~TemplateDefinition} def The definition of the binding.
	 */
	constructor( def ) {
		Object.assign( this, def );

		/**
		 * An observable instance of the binding. It either:
		 *
		 * * provides the attribute with the value,
		 * * or passes the event when a corresponding DOM event is fired.
		 *
		 * @member {module:utils/observablemixin~ObservableMixin} module:ui/template~TemplateBinding#observable
		 */

		/**
		 * An {@link module:utils/emittermixin~Emitter} used by the binding to:
		 *
		 * * listen to the attribute change in the {@link module:ui/template~TemplateBinding#observable},
		 * * or listen to the event in the DOM.
		 *
		 * @member {module:utils/emittermixin~EmitterMixin} module:ui/template~TemplateBinding#emitter
		 */

		/**
		 * The name of the {@link module:ui/template~TemplateBinding#observable observed attribute}.
		 *
		 * @member {String} module:ui/template~TemplateBinding#attribute
		 */

		/**
		 * A custom function to process the value of the {@link module:ui/template~TemplateBinding#attribute}.
		 *
		 * @member {Function} [module:ui/template~TemplateBinding#callback]
		 */
	}

	/**
	 * Returns the value of the binding. It is the value of the {@link module:ui/template~TemplateBinding#attribute} in
	 * {@link module:ui/template~TemplateBinding#observable}. The value may be processed by the
	 * {@link module:ui/template~TemplateBinding#callback}, if such has been passed to the binding.
	 *
	 * @param {Node} [node] A native DOM node, passed to the custom {@link module:ui/template~TemplateBinding#callback}.
	 * @returns {*} The value of {@link module:ui/template~TemplateBinding#attribute} in
	 * {@link module:ui/template~TemplateBinding#observable}.
	 */
	getValue( node ) {
		const value = this.observable[ this.attribute ];

		return this.callback ? this.callback( value, node ) : value;
	}

	/**
	 * Activates the listener which waits for changes of the {@link module:ui/template~TemplateBinding#attribute} in
	 * {@link module:ui/template~TemplateBinding#observable}, then updates the DOM with the aggregated
	 * value of {@link module:ui/template~TemplateValueSchema}.
	 *
	 * @param {module:ui/template~TemplateValueSchema} schema A full schema to generate an attribute or text in the DOM.
	 * @param {Function} updater A DOM updater function used to update the native DOM attribute or text.
	 * @param {module:ui/template~RenderData} data Rendering data.
	 * @returns {Function} A function to sever the listener binding.
	 */
	activateAttributeListener( schema, updater, data ) {
		const callback = () => syncValueSchemaValue( schema, updater, data );

		this.emitter.listenTo( this.observable, 'change:' + this.attribute, callback );

		// Allows revert of the listener.
		return () => {
			this.emitter.stopListening( this.observable, 'change:' + this.attribute, callback );
		};
	}
}
export class TemplateToBinding extends TemplateBinding {
	/**
	 * Activates the listener for the native DOM event, which when fired, is propagated by
	 * the {@link module:ui/template~TemplateBinding#emitter}.
	 *
	 * @param {String} domEvtName The name of the native DOM event.
	 * @param {String} domSelector The selector in the DOM to filter delegated events.
	 * @param {module:ui/template~RenderData} data Rendering data.
	 * @returns {Function} A function to sever the listener binding.
	 */
	activateDomEventListener( domEvtName, domSelector, data ) {
		const callback = ( evt, domEvt ) => {
			if ( !domSelector || domEvt.target.matches( domSelector ) ) {
				if ( typeof this.eventNameOrFunction == 'function' ) {
					this.eventNameOrFunction( domEvt );
				} else {
					this.observable.fire( this.eventNameOrFunction, domEvt );
				}
			}
		};

		this.emitter.listenTo( data.node, domEvtName, callback );

		// Allows revert of the listener.
		return () => {
			this.emitter.stopListening( data.node, domEvtName, callback );
		};
	}
}
export class TemplateIfBinding extends TemplateBinding {
	/**
	 * @inheritDoc
	 */
	getValue( node ) {
		const value = super.getValue( node );

		return isFalsy( value ) ? false : ( this.valueIfTrue || true );
	}

	/**
	 * The value of the DOM attribute or text to be set if the {@link module:ui/template~TemplateBinding#attribute} in
	 * {@link module:ui/template~TemplateBinding#observable} is `true`.
	 *
	 * @member {String} [module:ui/template~TemplateIfBinding#valueIfTrue]
	 */
}

以上是TemplateBinding的实现代码,我们可以看出它有两个关键方法activateAttributeListenergetValue方法,同时这个类有两个继承类,TemplateToBinding对应的实际上就是bind.to(),而TemplateIfBinding对应的就是bind.if()

我们这里需要先看看构造函数:

构造函数里实际上包含多个属性:observableemitterattributecallback,在执行activateAttributeListener的时候,实际上就是监听observable的属性的变化,如果属性有变化,那么这个变化就会同步到dom节点上去。并且返回一个函数,这个函数就是为了取消这种监听。前面我们提到了返回的是一个函数数组,在那里的函数实际上就是这里的返回函数,作用就是revert的时候取消这样的监听。

同时这个类有一个getValue方法,这个方法就是首先获得observable的属性值,如果有回调函数,那么就调用回调函数,否则直接返回属性的值。

{
	text: bind.to( 'b', ( value, node ) => value.toUpperCase() )
}
//这里就是有对调函数的情况,获取的值是通过对调函数来实现的。

好了,今天我们讲解了render()方法中的_renderText()的具体实现,并且简单介绍了绑定是怎么实现的。

另外一个就是_renderElement方法的实现,我打算下一节来继续分析。欢迎讨论。

 

 

 

 

CKEditor5——Template理解(一)

更新于 2022.08.24 21分钟阅读 0 评论 5 推荐

    CKEditor5

    作者: 敲碎时间,铸造不朽
  1. CKEditor5事件系统(基础使用) Page 1
    1. CKEditor5事件系统(事件优先级) Page 12
    2. CKEditor5事件系统(代理事件) Page 25
    3. CKEditor5事件系统(视图事件冒泡) Page 32
    4. CKEditor5事件系统源码分析(一) Page 36
    5. CKEditor5事件系统源码分析(二) Page 54
    6. CKEditor5事件系统源码分析(三) Page 67
    7. CKeditor5事件系统命名空间分析 Page 73
  2. CKEditor5 模板绑定 Page 87
    1. CKEditor5 Observables Page 94
    2. CKEditor5 Observable——属性绑定 Page 100
    3. CKEditor5 Observable——绑定多个对象或属性 Page 108
    4. CKEditor5 Observable——装饰方法 Page 115
    5. CKEditor5 UI——UI组件 Page 127
    6. CKEditor5——视图添加 Page 137
  3. CKEditor5——模型理解(一) Page 144
    1. CKEditor5——模型理解(二:Node) Page 151
    2. CKEditor5——模型理解(三:Element Text) Page 160
    3. CKEditor5——Node,Element,NodeList源码分析 Page 163
    4. CKEditor5——模型理解(四:模型组成) Page 176
    5. CKEditor5——模型理解(五:Position, Range, Selection) Page 192
    6. CKEditor5——Position源码分析 Page 205
    7. CKEditor5——Position源码分析(二) Page 224
    8. CKEditor5——Position源码分析(三) Page 235
    9. CKEditor5——模型理解(六:Range) Page 247
    10. CKEditor5——模型理解(七:Selection) Page 261
    11. CKEditor5——模型理解(八:Operation和Batch) Page 272
    12. CKEditor5——(九:TreeWalker) Page 276
  4. CKEditor5——Conversion理解 Page 283
  5. CKEditor5——Utils(工具类理解) Page 300
    1. CKEditor5——Utils(Collection类理解) Page 312
    2. CKEditor5——Uitls(DomEmitterMixin类理解) Page 351
  6. CKEditor5——Plugin理解 Page 374
  7. CKEditor5——UI理解 Page 380
    1. CKEditor5——View理解 Page 393
    2. CKEditor5——Template理解(一) Page 403
    3. CKEditor5——Template理解(二) Page 423
    4. CKEditor5——Template理解(三) Page 460

CKEditor5——Template理解

上一节,我们学习并大致理解了CK5的UI中最重要的一个类,那就是View,可以看出这个类其实有点像一个门面,而很多实际的功能包括renderbind等都是委托给Template这个类来实现的,因此,我们今天来靴子这个类。

export default class Template {
	/**
	 * Creates an instance of the {@link ~Template} class.
	 *
	 * @param {module:ui/template~TemplateDefinition} def The definition of the template.
	 */
	constructor( def ) {
		Object.assign( this, normalize( clone( def ) ) );

		/**
		 * Indicates whether this particular Template instance has been
		 * {@link #render rendered}.
		 *
		 * @readonly
		 * @protected
		 * @member {Boolean}
		 */
		this._isRendered = false;

		/**
		 * The tag (`tagName`) of this template, e.g. `div`. It also indicates that the template
		 * renders to an HTML element.
		 *
		 * @member {String} #tag
		 */

		/**
		 * The text of the template. It also indicates that the template renders to a DOM text node.
		 *
		 * @member {Array.<String|module:ui/template~TemplateValueSchema>} #text
		 */

		/**
		 * The attributes of the template, e.g. `{ id: [ 'ck-id' ] }`, corresponding with
		 * the attributes of an HTML element.
		 *
		 * **Note**: This property only makes sense when {@link #tag} is defined.
		 *
		 * @member {Object} #attributes
		 */

		/**
		 * The children of the template. They can be either:
		 * * independent instances of {@link ~Template} (sub–templates),
		 * * native DOM Nodes.
		 *
		 * **Note**: This property only makes sense when {@link #tag} is defined.
		 *
		 * @member {Array.<module:ui/template~Template|Node>} #children
		 */

		/**
		 * The DOM event listeners of the template.
		 *
		 * @member {Object} #eventListeners
		 */

		/**
		 * The data used by the {@link #revert} method to restore a node to its original state.
		 *
		 * See: {@link #apply}.
		 *
		 * @readonly
		 * @protected
		 * @member {module:ui/template~RenderData}
		 */
		this._revertData = null;
	}
}

我们首先看构造函数,这个函数实际上就是将一些属性复制到当前模板对象,因此,我们看看具体的方法:

我们先看看clone方法

function clone( def ) {
	const clone = cloneDeepWith( def, value => {
		// Don't clone the `Template.bind`* bindings because of the references to Observable
		// and DomEmitterMixin instances inside, which would also be traversed and cloned by greedy
		// cloneDeepWith algorithm. There's no point in cloning Observable/DomEmitterMixins
		// along with the definition.
		//
		// Don't clone Template instances if provided as a child. They're simply #render()ed
		// and nothing should interfere.
		//
		// Also don't clone View instances if provided as a child of the Template. The template
		// instance will be extracted from the View during the normalization and there's no need
		// to clone it.
		if ( value && ( value instanceof TemplateBinding || isTemplate( value ) || isView( value ) || isViewCollection( value ) ) ) {
			return value;
		}
	} );

	return clone;
}

看看这个方法可以知道,并不是所有的属性都需要拷贝,如果遇上Template.bind,View等都直接返回原始的引用,而不用再拷贝一份新的实例。主要有四类属性不用拷贝,至于为什么?大家可以思考一下,我们后文再分析。

拷贝完成之后,我们需要归一化,也就是这个方法:normalize()

function normalize( def ) {
	if ( typeof def == 'string' ) {
		def = normalizePlainTextDefinition( def );
	} else if ( def.text ) {
		normalizeTextDefinition( def );
	}

	if ( def.on ) {
		def.eventListeners = normalizeListeners( def.on );

		// Template mixes EmitterMixin, so delete #on to avoid collision.
		delete def.on;
	}

	if ( !def.text ) {
		if ( def.attributes ) {
			normalizeAttributes( def.attributes );
		}

		const children = [];

		if ( def.children ) {
			if ( isViewCollection( def.children ) ) {
				children.push( def.children );
			} else {
				for ( const child of def.children ) {
					if ( isTemplate( child ) || isView( child ) || isNode( child ) ) {
						children.push( child );
					} else {
						children.push( new Template( child ) );
					}
				}
			}
		}

		def.children = children;
	}

	return def;
}

从这个归一化的方法,可以看出对模板的def处理主要分成几类:

1、如果def是一个字符该怎么处理,实际上就是作为一个文本出行返回

function normalizePlainTextDefinition( def ) {
	return {
		text: [ def ]
	};
}
// "foo" ---> { text: [ 'foo' ] }

 

2、如果def是一个对象,且存在一个text属性,那么处理方法如下

function normalizeTextDefinition( def ) {
	def.text = toArray( def.text );
}
// children: [
//			{ text: 'def' },
//			{ text: {@link module:ui/template~TemplateBinding} }
//]

// become 
children: [
//			{ text: [ 'def' ] },
//			{ text: [ {@link module:ui/template~TemplateBinding} ] }
//		]

实际上就是将def的text属性的值转化为数组

3、如果def是一个对象,且存在一个on属性,那么处理如下:

// Normalizes "on" section of {@link module:ui/template~TemplateDefinition}.
//
//		on: {
//			a: 'bar',
//			b: {@link module:ui/template~TemplateBinding},
//			c: [ {@link module:ui/template~TemplateBinding}, () => { ... } ]
//		}
//
// becomes
//
//		on: {
//			a: [ 'bar' ],
//			b: [ {@link module:ui/template~TemplateBinding} ],
//			c: [ {@link module:ui/template~TemplateBinding}, () => { ... } ]
//		}
function normalizeListeners( listeners ) {
	for ( const l in listeners ) {
		arrayify( listeners, l );
	}

	return listeners;
}
function arrayify( obj, key ) {
	obj[ key ] = toArray( obj[ key ] );
}

可以看出实际上就是将对象listeners的值转化成数组,同时返回这个对象,并添加到def属性上。

4、如果def.text不存在,那么就是处理def的属性和子节点

// Normalizes "attributes" section of {@link module:ui/template~TemplateDefinition}.
//
//		attributes: {
//			a: 'bar',
//			b: {@link module:ui/template~TemplateBinding},
//			c: {
//				value: 'bar'
//			}
//		}
//
// becomes
//
//		attributes: {
//			a: [ 'bar' ],
//			b: [ {@link module:ui/template~TemplateBinding} ],
//			c: {
//				value: [ 'bar' ]
//			}
//		}
function normalizeAttributes( attributes ) {
	for ( const a in attributes ) {
		if ( attributes[ a ].value ) {
			attributes[ a ].value = toArray( attributes[ a ].value );
		}

		arrayify( attributes, a );
	}
}

这里的处理就是将属性的值转化为数组,处理子节点就是将子节点打开放到一个数组当中。

归一化之后,将这些属性复制到this对象,然后设置一些基本的值之后就算完成构造函数。让复制完成之后,我们知道当然的this对象可能有的属性包括,注意,为什么归一化方法没有处理tag属性

tag属性,attributes属性,children属性,text属性,eventListeners属性这些属性会成为我们后文分析的重点,一定要掌握这些属性的作用。另外值得一提的是还有一个this._revertData属性,这个属性暂时没有用到,因此可以略过,后文分析方法的时候会介绍。最后一个就是模板是否渲染的boolean属性,用于判断是否已经模板渲染到dom。

 

CKEditor5——View理解

发布于 2022.08.22 9分钟阅读 0 评论 5 推荐

    CKEditor5

    作者: 敲碎时间,铸造不朽
  1. CKEditor5事件系统(基础使用) Page 1
    1. CKEditor5事件系统(事件优先级) Page 12
    2. CKEditor5事件系统(代理事件) Page 25
    3. CKEditor5事件系统(视图事件冒泡) Page 32
    4. CKEditor5事件系统源码分析(一) Page 36
    5. CKEditor5事件系统源码分析(二) Page 54
    6. CKEditor5事件系统源码分析(三) Page 67
    7. CKeditor5事件系统命名空间分析 Page 73
  2. CKEditor5 模板绑定 Page 87
    1. CKEditor5 Observables Page 94
    2. CKEditor5 Observable——属性绑定 Page 100
    3. CKEditor5 Observable——绑定多个对象或属性 Page 108
    4. CKEditor5 Observable——装饰方法 Page 115
    5. CKEditor5 UI——UI组件 Page 127
    6. CKEditor5——视图添加 Page 137
  3. CKEditor5——模型理解(一) Page 144
    1. CKEditor5——模型理解(二:Node) Page 151
    2. CKEditor5——模型理解(三:Element Text) Page 160
    3. CKEditor5——Node,Element,NodeList源码分析 Page 163
    4. CKEditor5——模型理解(四:模型组成) Page 176
    5. CKEditor5——模型理解(五:Position, Range, Selection) Page 192
    6. CKEditor5——Position源码分析 Page 205
    7. CKEditor5——Position源码分析(二) Page 224
    8. CKEditor5——Position源码分析(三) Page 235
    9. CKEditor5——模型理解(六:Range) Page 247
    10. CKEditor5——模型理解(七:Selection) Page 261
    11. CKEditor5——模型理解(八:Operation和Batch) Page 272
    12. CKEditor5——(九:TreeWalker) Page 276
  4. CKEditor5——Conversion理解 Page 283
  5. CKEditor5——Utils(工具类理解) Page 300
    1. CKEditor5——Utils(Collection类理解) Page 312
    2. CKEditor5——Uitls(DomEmitterMixin类理解) Page 351
  6. CKEditor5——Plugin理解 Page 374
  7. CKEditor5——UI理解 Page 380
    1. CKEditor5——View理解 Page 393
    2. CKEditor5——Template理解(一) Page 403
    3. CKEditor5——Template理解(二) Page 423
    4. CKEditor5——Template理解(三) Page 460

CKEditor5——View理解

今天我们继续学习ck5UI中的一个基础类View,这个类是所有视图的基础类,我们先看看这个类的代码:

export default class View {
	/**
	 * Creates an instance of the {@link module:ui/view~View} class.
	 *
	 * Also see {@link #render}.
	 *
	 * @param {module:utils/locale~Locale} [locale] The localization services instance.
	 */
	constructor( locale ) {
		
		this.element = null;

		this.isRendered = false;

		this.locale = locale;
 
		this.t = locale && locale.t;

		this._viewCollections = new Collection();

		this._unboundChildren = this.createCollection();

		// Pass parent locale to its children.
		this._viewCollections.on( 'add', ( evt, collection ) => {
			collection.locale = locale;
		} );

		/**
		 * Template of this view. It provides the {@link #element} representing
		 * the view in DOM, which is {@link #render rendered}.
		 *
		 * @member {module:ui/template~Template} #template
		 */

		/**
		 * Cached {@link module:ui/template~BindChain bind chain} object created by the
		 * {@link #template}. See {@link #bindTemplate}.
		 *
		 * @private
		 * @member {Object} #_bindTemplate
		 */

		this.decorate( 'render' );
	}
}
 
 
mix( View, DomEmitterMixin );
mix( View, ObservableMixin );

从这个类的构造函数可以看出,这个类是一个dom事件类,也是一个可观察对象。它接收一个locale对象作为参数。视图类包含一个element元素,这个是实际的dom元素,从视图到dom元素节点需要一个渲染的过程,因此有一个判断视图是否渲染的boolean属性;还有一个存储子节点视图的集合元素,这个集合用于给当前的视图节点添加子节点。this._unboundChildren这个属性用于我们注册子节点的时候存储子节点视图。还有一个是装饰了render方法,方便我们在视图渲染的时候添加一些自定义的功能。

这个类有一个关键的属性:this._bindTemplate

get bindTemplate() {
	if ( this._bindTemplate ) {
		return this._bindTemplate;
	}

	return ( this._bindTemplate = Template.bind( this, this ) );
}

这个属性主要用于给视图绑定事件,或者配置一些dom节点的class或者style等属性。

createCollection( views ) {
	const collection = new ViewCollection( views );

	this._viewCollections.add( collection );

	return collection;
}

以上方法是主要用于添加子视图View

另外的一个关键方法就是:

setTemplate( definition ) {
	this.template = new Template( definition );
}
extendTemplate( definition ) {
	Template.extend( this.template, definition );
}

这个方法的作用是配置视图的模板,主要用于渲染的时候使用。另一个就是扩展模板视图,类似于对当前的模板进行装饰作用。

剩下的就是渲染了:

render() {
	if ( this.isRendered ) {
		/**
		 * This View has already been rendered.
		 *
		 * @error ui-view-render-already-rendered
		 */
		throw new CKEditorError( 'ui-view-render-already-rendered', this );
	}

	// Render #element of the view.
	if ( this.template ) {
		this.element = this.template.render();

		// Auto–register view children from #template.
		this.registerChild( this.template.getViews() );
	}

	this.isRendered = true;
}

从这个方法不难看出,这里的渲染主要是调用模板的渲染方法来实现,将渲染过程代理给了Template这个类。同时还将模板的视图注册到了子节点。

总结起来说就是:

1、配置视图的模板,然后进行渲染。

2、给当前视图添加子视图。

3、还有一个就是注册子节点(这里很简单,就是添加到视图的一个集合)。

有一点需要重点注意的是,这里渲染功能和绑定功能都是委托到了Template这个类来完成的,因此,Template会成为我们分析的重点。我们下一节对这个类进行学习。

欢迎讨论

 

CKEditor5——UI理解

发布于 2022.08.20 13分钟阅读 0 评论 5 推荐

    CKEditor5

    作者: 敲碎时间,铸造不朽
  1. CKEditor5事件系统(基础使用) Page 1
    1. CKEditor5事件系统(事件优先级) Page 12
    2. CKEditor5事件系统(代理事件) Page 25
    3. CKEditor5事件系统(视图事件冒泡) Page 32
    4. CKEditor5事件系统源码分析(一) Page 36
    5. CKEditor5事件系统源码分析(二) Page 54
    6. CKEditor5事件系统源码分析(三) Page 67
    7. CKeditor5事件系统命名空间分析 Page 73
  2. CKEditor5 模板绑定 Page 87
    1. CKEditor5 Observables Page 94
    2. CKEditor5 Observable——属性绑定 Page 100
    3. CKEditor5 Observable——绑定多个对象或属性 Page 108
    4. CKEditor5 Observable——装饰方法 Page 115
    5. CKEditor5 UI——UI组件 Page 127
    6. CKEditor5——视图添加 Page 137
  3. CKEditor5——模型理解(一) Page 144
    1. CKEditor5——模型理解(二:Node) Page 151
    2. CKEditor5——模型理解(三:Element Text) Page 160
    3. CKEditor5——Node,Element,NodeList源码分析 Page 163
    4. CKEditor5——模型理解(四:模型组成) Page 176
    5. CKEditor5——模型理解(五:Position, Range, Selection) Page 192
    6. CKEditor5——Position源码分析 Page 205
    7. CKEditor5——Position源码分析(二) Page 224
    8. CKEditor5——Position源码分析(三) Page 235
    9. CKEditor5——模型理解(六:Range) Page 247
    10. CKEditor5——模型理解(七:Selection) Page 261
    11. CKEditor5——模型理解(八:Operation和Batch) Page 272
    12. CKEditor5——(九:TreeWalker) Page 276
  4. CKEditor5——Conversion理解 Page 283
  5. CKEditor5——Utils(工具类理解) Page 300
    1. CKEditor5——Utils(Collection类理解) Page 312
    2. CKEditor5——Uitls(DomEmitterMixin类理解) Page 351
  6. CKEditor5——Plugin理解 Page 374
  7. CKEditor5——UI理解 Page 380
    1. CKEditor5——View理解 Page 393
    2. CKEditor5——Template理解(一) Page 403
    3. CKEditor5——Template理解(二) Page 423
    4. CKEditor5——Template理解(三) Page 460

CKEditor5——UI理解

今天我们开始认识CK5中的UI,首先我们还是认识一些基础类,今天开始认识的第一个类比较简单,那就是

ViewCollection,首先我们看看代码:

export default class ViewCollection extends Collection {
 	constructor( initialItems = [] ) {
		super( initialItems, {
			// An #id Number attribute should be legal and not break the `ViewCollection` instance.
			// https://github.com/ckeditor/ckeditor5-ui/issues/93
			idProperty: 'viewUid'
		} );

		// Handle {@link module:ui/view~View#element} in DOM when a new view is added to the collection.
		this.on( 'add', ( evt, view, index ) => {
			this._renderViewIntoCollectionParent( view, index );
		} );

		// Handle {@link module:ui/view~View#element} in DOM when a view is removed from the collection.
		this.on( 'remove', ( evt, view ) => {
			if ( view.element && this._parentElement ) {
				view.element.remove();
			}
		} );

		/**
		 * A parent element within which child views are rendered and managed in DOM.
		 *
		 * @protected
		 * @member {HTMLElement}
		 */
		this._parentElement = null;
	}
}

我们之前分析过Collection类,我们的ViewCollection类继承自Collection,因此它具有Collection的一切功能,此外就是增添了往集合添加或者删除View元素的时候的逻辑。

首先,我们需要明白的是这个集合是管理View的一个集合,因此这个集合需要有一个所有集合的父元素,可以看见this._parentElement这个属性就是存储集合的父元素,其次,应该不难猜想构造函数initialItems

应该是一个包含View的数组或者可迭代对象。

我们还是重点看看this._renderViewIntoCollectionParent( view, index )

_renderViewIntoCollectionParent( view, index ) {
	if ( !view.isRendered ) {
		view.render();
	}

	if ( view.element && this._parentElement ) {
		this._parentElement.insertBefore( view.element, this._parentElement.children[ index ] );
	}
}

其实这个方法主要就是渲染指定的view元素,并且将此元素添加到父元素中。因此我们可以知道只要往集合中添加元素,它会立即渲染到视图。

我们再来看看另一个代理方法:delegate( ...events )

delegate( ...events ) {
	if ( !events.length || !isStringArray( events ) ) {
		/**
		 * All event names must be strings.
		 *
		 * @error ui-viewcollection-delegate-wrong-events
		 */
		throw new CKEditorError(
			'ui-viewcollection-delegate-wrong-events',
				this
		);
	}

	return {
	  /**
		* Selects destination for {@link module:utils/emittermixin~Emitter#delegate} events.
		*
		* @memberOf module:ui/viewcollection~ViewCollection#delegate
	    * @function module:ui/viewcollection~ViewCollection#delegate.to
	    * @param {module:utils/emittermixin~Emitter} dest An `Emitter` instance which is
		* the destination for delegated events.
		*/
		to: dest => {
			// Activate delegating on existing views in this collection.
			for ( const view of this ) {
				for ( const evtName of events ) {
					view.delegate( evtName ).to( dest );
				}
			}

			// Activate delegating on future views in this collection.
			this.on( 'add', ( evt, view ) => {
				for ( const evtName of events ) {
					view.delegate( evtName ).to( dest );
				}
			} );

			// Deactivate delegating when view is removed from this collection.
			this.on( 'remove', ( evt, view ) => {
				for ( const evtName of events ) {
					view.stopDelegating( evtName, dest );
				}
			} );
		}
	};
}

这个方法的作用就是将集合中的每个View的指定事件代理给其他外部的View元素。同时在添加元素和删除View元素的时候也要代理或者取消代理相关的事件。看看例子

const viewA = new View();
const viewB = new View();
const viewC = new View();

const views = parentView.createCollection();

views.delegate( 'eventX' ).to( viewB );
views.delegate( 'eventX', 'eventY' ).to( viewC );

views.add( viewA );

viewA.fire( 'eventX', customData );

viewA.fire( 'eventY', customData );

这里需要注意的是,事件的触发是由集合中的View触发的,并且被代理的视图需要监听事件才有作用。

 

最后一个有意思方法就是

setParent( elementOrDocFragment ) {
	this._parentElement = elementOrDocFragment;

	// Take care of the initial collection items passed to the constructor.
	for ( const view of this ) {
		this._renderViewIntoCollectionParent( view );
	}
}

这个方法的作用就是修改集合视图的父元素,并且将子节点渲染到新的父元素之下。这个有啥作用呢?我的猜想就是可以提高集合视图的复用效果,比如我写了一个不错的集合视图UI,当我需要把它切换到新的节点位置的时候,我只需要修改父节点就OK。

 

 

 

 

CKEditor5——Uitls(DomEmitterMixin类理解)

发布于 2022.08.18 24分钟阅读 0 评论 5 推荐

    CKEditor5

    作者: 敲碎时间,铸造不朽
  1. CKEditor5事件系统(基础使用) Page 1
    1. CKEditor5事件系统(事件优先级) Page 12
    2. CKEditor5事件系统(代理事件) Page 25
    3. CKEditor5事件系统(视图事件冒泡) Page 32
    4. CKEditor5事件系统源码分析(一) Page 36
    5. CKEditor5事件系统源码分析(二) Page 54
    6. CKEditor5事件系统源码分析(三) Page 67
    7. CKeditor5事件系统命名空间分析 Page 73
  2. CKEditor5 模板绑定 Page 87
    1. CKEditor5 Observables Page 94
    2. CKEditor5 Observable——属性绑定 Page 100
    3. CKEditor5 Observable——绑定多个对象或属性 Page 108
    4. CKEditor5 Observable——装饰方法 Page 115
    5. CKEditor5 UI——UI组件 Page 127
    6. CKEditor5——视图添加 Page 137
  3. CKEditor5——模型理解(一) Page 144
    1. CKEditor5——模型理解(二:Node) Page 151
    2. CKEditor5——模型理解(三:Element Text) Page 160
    3. CKEditor5——Node,Element,NodeList源码分析 Page 163
    4. CKEditor5——模型理解(四:模型组成) Page 176
    5. CKEditor5——模型理解(五:Position, Range, Selection) Page 192
    6. CKEditor5——Position源码分析 Page 205
    7. CKEditor5——Position源码分析(二) Page 224
    8. CKEditor5——Position源码分析(三) Page 235
    9. CKEditor5——模型理解(六:Range) Page 247
    10. CKEditor5——模型理解(七:Selection) Page 261
    11. CKEditor5——模型理解(八:Operation和Batch) Page 272
    12. CKEditor5——(九:TreeWalker) Page 276
  4. CKEditor5——Conversion理解 Page 283
  5. CKEditor5——Utils(工具类理解) Page 300
    1. CKEditor5——Utils(Collection类理解) Page 312
    2. CKEditor5——Uitls(DomEmitterMixin类理解) Page 351
  6. CKEditor5——Plugin理解 Page 374
  7. CKEditor5——UI理解 Page 380
    1. CKEditor5——View理解 Page 393
    2. CKEditor5——Template理解(一) Page 403
    3. CKEditor5——Template理解(二) Page 423
    4. CKEditor5——Template理解(三) Page 460

CKEditor5——Uitls(DomEmitterMixin类理解)

今天开始研究在CK5的Utils包中的另一个比较重要的类,这个类就是DomEmitterMixin,我们先看看这个类的代码:

const DomEmitterMixin = extend( {}, EmitterMixin, {
	listenTo( emitter, event, callback, options = {} ){
	},
	stopListening(){
	},
	_getProxyEmitter() {
	},
	_getAllProxyEmitters(){
	}
}

通过上面的代码,我们不难看出,这个类是继承字基础类EmitterMixin,然后扩展并且重写了基类的两个方法,它们分别是:listenTo和stopListening。思考一下,这在设计模式中是什么模式呢?是不是就是装饰器模式。如果猜测没错的话,这个装饰器增强的功能就是为dom节点绑定事件和删除事件。接下来,我们分析关键的方法listenTo

listenTo( emitter, event, callback, options = {} ) {
	// Check if emitter is an instance of DOM Node. If so, use corresponding ProxyEmitter (or create one if not existing).
	if ( isNode( emitter ) || isWindow( emitter ) ) {
		const proxyOptions = {
			capture: !!options.useCapture,
			passive: !!options.usePassive
		};

		const proxyEmitter = this._getProxyEmitter( emitter, proxyOptions ) || new ProxyEmitter( emitter, proxyOptions );

		this.listenTo( proxyEmitter, event, callback, options );
	} else {
		// Execute parent class method with Emitter (or ProxyEmitter) instance.
		EmitterMixin.listenTo.call( this, emitter, event, callback, options );
	}
}

注意到没,这里开始对emitter进行判断,如果是Window对象或者Node对象,那么就采用增加的业务逻辑,否则调用基类的方法进行绑定,因此,我们之间的猜测是正确的。

这里增强的业务逻辑也比较简单,就是根据emitter和配置对象proxyOptions要么创建一个ProxyEmitter,要么采用已经创建好的proxyEmitter进行事件的绑定。

这里我们重点关注这行代码:

this.listenTo( proxyEmitter, event, callback, options );

我们知道这行代码会调用添加事件监听器的事件,因此,我们猜想ProxyEmitter一定会重写添加事件监听器的方法,否则无法绑定dom事件,也就是说具体的装饰增强实际上是在ProxyEmitter 中实现的;所以我们需要看看ProxyEmitter的具体实现:

class ProxyEmitter {
	/**
	 * @param {Node} node DOM Node that fires events.
	 * @param {Object} [options] Additional options.
	 * @param {Boolean} [options.useCapture=false] Indicates that events of this type will be dispatched to the registered
	 * listener before being dispatched to any EventTarget beneath it in the DOM tree.
	 * @param {Boolean} [options.usePassive=false] Indicates that the function specified by listener will never call preventDefault()
	 * and prevents blocking browser's main thread by this event handler.
	 */
	constructor( node, options ) {
		// Set emitter ID to match DOM Node "expando" property.
		_setEmitterId( this, getProxyEmitterId( node, options ) );

		// Remember the DOM Node this ProxyEmitter is bound to.
		this._domNode = node;

		// And given options.
		this._options = options;
	}
}

extend( ProxyEmitter.prototype, EmitterMixin, {
	/**
	 * Collection of native DOM listeners.
	 *
	 * @private
	 * @member {Object} module:utils/dom/emittermixin~ProxyEmitter#_domListeners
	 */

	/**
	 * Registers a callback function to be executed when an event is fired.
	 *
	 * It attaches a native DOM listener to the DOM Node. When fired,
	 * a corresponding Emitter event will also fire with DOM Event object as an argument.
	 *
	 * **Note**: This is automatically called by the
	 * {@link module:utils/emittermixin~EmitterMixin#listenTo `EmitterMixin#listenTo()`}.
	 *
	 * @method module:utils/dom/emittermixin~ProxyEmitter#attach
	 * @param {String} event The name of the event.
	 */
	attach( event ) {
		// If the DOM Listener for given event already exist it is pointless
		// to attach another one.
		if ( this._domListeners && this._domListeners[ event ] ) {
			return;
		}

		const domListener = this._createDomListener( event );

		// Attach the native DOM listener to DOM Node.
		this._domNode.addEventListener( event, domListener, this._options );

		if ( !this._domListeners ) {
			this._domListeners = {};
		}

		// Store the native DOM listener in this ProxyEmitter. It will be helpful
		// when stopping listening to the event.
		this._domListeners[ event ] = domListener;
	},

	/**
	 * Stops executing the callback on the given event.
	 *
	 * **Note**: This is automatically called by the
	 * {@link module:utils/emittermixin~EmitterMixin#stopListening `EmitterMixin#stopListening()`}.
	 *
	 * @method module:utils/dom/emittermixin~ProxyEmitter#detach
	 * @param {String} event The name of the event.
	 */
	detach( event ) {
		let events;

		// Remove native DOM listeners which are orphans. If no callbacks
		// are awaiting given event, detach native DOM listener from DOM Node.
		// See: {@link attach}.

		if ( this._domListeners[ event ] && ( !( events = this._events[ event ] ) || !events.callbacks.length ) ) {
			this._domListeners[ event ].removeListener();
		}
	},

	/**
	 * Adds callback to emitter for given event.
	 *
	 * @protected
	 * @method module:utils/dom/emittermixin~ProxyEmitter#_addEventListener
	 * @param {String} event The name of the event.
	 * @param {Function} callback The function to be called on event.
	 * @param {Object} [options={}] Additional options.
	 * @param {module:utils/priorities~PriorityString|Number} [options.priority='normal'] The priority of this event callback. The higher
	 * the priority value the sooner the callback will be fired. Events having the same priority are called in the
	 * order they were added.
	 */
	_addEventListener( event, callback, options ) {
		this.attach( event );
		EmitterMixin._addEventListener.call( this, event, callback, options );
	},

	/**
	 * Removes callback from emitter for given event.
	 *
	 * @protected
	 * @method module:utils/dom/emittermixin~ProxyEmitter#_removeEventListener
	 * @param {String} event The name of the event.
	 * @param {Function} callback The function to stop being called.
	 */
	_removeEventListener( event, callback ) {
		EmitterMixin._removeEventListener.call( this, event, callback );
		this.detach( event );
	},

	/**
	 * Creates a native DOM listener callback. When the native DOM event
	 * is fired it will fire corresponding event on this ProxyEmitter.
	 * Note: A native DOM Event is passed as an argument.
	 *
	 * @private
	 * @method module:utils/dom/emittermixin~ProxyEmitter#_createDomListener
	 * @param {String} event The name of the event.
	 * @returns {Function} The DOM listener callback.
	 */
	_createDomListener( event ) {
		const domListener = domEvt => {
			this.fire( event, domEvt );
		};

		// Supply the DOM listener callback with a function that will help
		// detach it from the DOM Node, when it is no longer necessary.
		// See: {@link detach}.
		domListener.removeListener = () => {
			this._domNode.removeEventListener( event, domListener, this._options );
			delete this._domListeners[ event ];
		};

		return domListener;
	}
} );

我们首先看构造函数,首先设置一个id,然后将dom节点和配置参数存储起来。关键的方法是看扩展的这个类的原型,我们看到了一个关键的方法就是_addEventListener首先调用attach()

这个关键的attach就是增强的关键方法,为dom节点添加事件:

首先判断事件是否存在,如果存在就不需要添加了。

其次创建一个domListener回调函数,然后在dom节点上绑定对应的事件,并且在回调函数上添加了一个移除监听器的方法,用于销毁创建的监听器。

将新建的回调函数绑定到this._domListeners[event]

有了以上的三个步骤,就完成了给dom节点添加事件的功能,因此增加后的节点可以认为是一个domEmitter。

同时需要注意的是,除了调用attach()方法之外,其实还调用了基类的方法来绑定事件。

以上基本分析了绑定事件的全部过程,至于如果为节点创建id啥的,这部分对主流程影响不大,有兴趣的可以自行分析,移出事件的过程也是类似的道理。这个类的增强将装饰器设计模式的使用发挥到了淋漓尽致的地步,需要仔细体会才能看到代码的精妙之处。

欢迎讨论。

 

 

CKEditor5——Utils(Collection类理解)

发布于 2022.08.16 41分钟阅读 0 评论 5 推荐

    CKEditor5

    作者: 敲碎时间,铸造不朽
  1. CKEditor5事件系统(基础使用) Page 1
    1. CKEditor5事件系统(事件优先级) Page 12
    2. CKEditor5事件系统(代理事件) Page 25
    3. CKEditor5事件系统(视图事件冒泡) Page 32
    4. CKEditor5事件系统源码分析(一) Page 36
    5. CKEditor5事件系统源码分析(二) Page 54
    6. CKEditor5事件系统源码分析(三) Page 67
    7. CKeditor5事件系统命名空间分析 Page 73
  2. CKEditor5 模板绑定 Page 87
    1. CKEditor5 Observables Page 94
    2. CKEditor5 Observable——属性绑定 Page 100
    3. CKEditor5 Observable——绑定多个对象或属性 Page 108
    4. CKEditor5 Observable——装饰方法 Page 115
    5. CKEditor5 UI——UI组件 Page 127
    6. CKEditor5——视图添加 Page 137
  3. CKEditor5——模型理解(一) Page 144
    1. CKEditor5——模型理解(二:Node) Page 151
    2. CKEditor5——模型理解(三:Element Text) Page 160
    3. CKEditor5——Node,Element,NodeList源码分析 Page 163
    4. CKEditor5——模型理解(四:模型组成) Page 176
    5. CKEditor5——模型理解(五:Position, Range, Selection) Page 192
    6. CKEditor5——Position源码分析 Page 205
    7. CKEditor5——Position源码分析(二) Page 224
    8. CKEditor5——Position源码分析(三) Page 235
    9. CKEditor5——模型理解(六:Range) Page 247
    10. CKEditor5——模型理解(七:Selection) Page 261
    11. CKEditor5——模型理解(八:Operation和Batch) Page 272
    12. CKEditor5——(九:TreeWalker) Page 276
  4. CKEditor5——Conversion理解 Page 283
  5. CKEditor5——Utils(工具类理解) Page 300
    1. CKEditor5——Utils(Collection类理解) Page 312
    2. CKEditor5——Uitls(DomEmitterMixin类理解) Page 351
  6. CKEditor5——Plugin理解 Page 374
  7. CKEditor5——UI理解 Page 380
    1. CKEditor5——View理解 Page 393
    2. CKEditor5——Template理解(一) Page 403
    3. CKEditor5——Template理解(二) Page 423
    4. CKEditor5——Template理解(三) Page 460

CKEditor5——Utils(Collection类理解)

今天我们学习CK5的Utils工具包中比较重要且常用的一个关键类:Collection,先贴出比较关键的代码:

export default class Collection {
 	constructor( initialItemsOrOptions = {}, options = {} ) {
		const hasInitialItems = isIterable( initialItemsOrOptions );

		if ( !hasInitialItems ) {
			options = initialItemsOrOptions;
		}

		/**
		 * The internal list of items in the collection.
		 *
		 * @private
		 * @member {Object[]}
		 */
		this._items = [];

		/**
		 * The internal map of items in the collection.
		 *
		 * @private
		 * @member {Map}
		 */
		this._itemMap = new Map();

		/**
		 * The name of the property which is considered to identify an item.
		 *
		 * @private
		 * @member {String}
		 */
		this._idProperty = options.idProperty || 'id';

		/**
		 * A helper mapping external items of a bound collection ({@link #bindTo})
		 * and actual items of this collection. It provides information
		 * necessary to properly remove items bound to another collection.
		 *
		 * See {@link #_bindToInternalToExternalMap}.
		 *
		 * @protected
		 * @member {WeakMap}
		 */
		this._bindToExternalToInternalMap = new WeakMap();

		/**
		 * A helper mapping items of this collection to external items of a bound collection
		 * ({@link #bindTo}). It provides information necessary to manage the bindings, e.g.
		 * to avoid loops in two–way bindings.
		 *
		 * See {@link #_bindToExternalToInternalMap}.
		 *
		 * @protected
		 * @member {WeakMap}
		 */
		this._bindToInternalToExternalMap = new WeakMap();

		/**
		 * Stores indexes of skipped items from bound external collection.
		 *
		 * @private
		 * @member {Array}
		 */
		this._skippedIndexesFromExternal = [];

		// Set the initial content of the collection (if provided in the constructor).
		if ( hasInitialItems ) {
			for ( const item of initialItemsOrOptions ) {
				this._items.push( item );
				this._itemMap.set( this._getItemIdBeforeAdding( item ), item );
			}
		}

		/**
		 * A collection instance this collection is bound to as a result
		 * of calling {@link #bindTo} method.
		 *
		 * @protected
		 * @member {module:utils/collection~Collection} #_bindToCollection
		 */
	}
}

从上面的代码可以看出有两个关键的字段就是: this._items,this._itemMap一个数组和一个Map,所以可以猜测collection可以方便的插入数据和快速查询。

从构造函数可以看出,它接收两个值,第一个值是一个可以迭代的对象,另一个是配置对象。我们看看它的具体用法:

const collection = new Collection( [ { id: 'John' }, { id: 'Mike' } ] );

console.log( collection.get( 0 ) ); // -> { id: 'John' }
console.log( collection.get( 1 ) ); // -> { id: 'Mike' }
console.log( collection.get( 'Mike' ) ); // -> { id: 'Mike' }
// or add item
const collection = new Collection();

collection.add( { id: 'John' } );
console.log( collection.get( 0 ) ); // -> { id: 'John' }
//这里是往connection添加了数组对象

//这里是通过最后一个参数来配置一些属性比如集合的idProperty属性,这样在查找的时候就可以
//根据这个属性进行查找
const emptyCollection = new Collection( { idProperty: 'name' } );
emptyCollection.add( { name: 'John' } );
console.log( collection.get( 'John' ) ); // -> { name: 'John' }

const nonEmptyCollection = new Collection( [ { name: 'John' } ], { idProperty: 'name' } );
nonEmptyCollection.add( { name: 'George' } );
console.log( collection.get( 'George' ) ); // -> { name: 'George' }
console.log( collection.get( 'John' ) ); // -> { name: 'John' }

 

我们再来看看另外两个关键的属性:this._bindToInternalToExternalMapthis._bindToInternalToExternalMap这是两个有意思的属性,根据字面意思猜测这两个属性是用在bindTo这个方法中的,而且这两个Map构成了一个双向映射。即从内部到外部的映射和从外部到内部的映射。

下面我们看看这个方法:

bindTo( externalCollection ) {
	if ( this._bindToCollection ) {
		/**
		 * The collection cannot be bound more than once.
		 *
		 * @error collection-bind-to-rebind
		 */
		throw new CKEditorError( 'collection-bind-to-rebind', this );
	}

	this._bindToCollection = externalCollection;

	return {
		as: Class => {
			this._setUpBindToBinding( item => new Class( item ) );
		},

		using: callbackOrProperty => {
			if ( typeof callbackOrProperty == 'function' ) {
				this._setUpBindToBinding( item => callbackOrProperty( item ) );
			} else {
				this._setUpBindToBinding( item => item[ callbackOrProperty ] );
			}
		}
	};
}

这个方法的作用是绑定当前集合与外部集合,让它们的数据能够同步,我们看看用法:

class FactoryClass {
	constructor( data ) {
		this.label = data.label;
	}
}

const source = new Collection( { idProperty: 'label' } );
const target = new Collection();

target.bindTo( source ).as( FactoryClass );

source.add( { label: 'foo' } );
source.add( { label: 'bar' } );

console.log( target.length ); // 2
console.log( target.get( 1 ).label ); // 'bar'

source.remove( 0 );
console.log( target.length ); // 1
console.log( target.get( 0 ).label ); // 'bar'

注意到没,我们这里有一个集合source,它的id属性叫做label,另一个集合叫做target,通过调用

target.bindTo( source ).as( FactoryClass );

这样source就将source与target进行了绑定,一旦修改了source的值,比如添加之后,那么target也会跟着同步,这里有一点需要注意的是,尽管source和target都是集合,但是它们里面存储的item是不一样的,source存储的是普通对象,但是target存储的FactoryClass类型的对象,且这些对象有共同的属性就是label。

我们再看bindTo这个方法,其实关键的就是this._setUpBindToBinding( item => new Class( item ) );

/**
 * Finalizes and activates a binding initiated by {#bindTo}.
 *
 * @protected
 * @param {Function} factory A function which produces collection items.
 */
_setUpBindToBinding( factory ) {
	const externalCollection = this._bindToCollection;

	// Adds the item to the collection once a change has been done to the external collection.
	//
	// @private
	const addItem = ( evt, externalItem, index ) => {
		const isExternalBoundToThis = externalCollection._bindToCollection == this;
		const externalItemBound = externalCollection._bindToInternalToExternalMap.get( externalItem );

		// If an external collection is bound to this collection, which makes it a 2–way binding,
		// and the particular external collection item is already bound, don't add it here.
		// The external item has been created **out of this collection's item** and (re)adding it will
		// cause a loop.
		if ( isExternalBoundToThis && externalItemBound ) {
			this._bindToExternalToInternalMap.set( externalItem, externalItemBound );
			this._bindToInternalToExternalMap.set( externalItemBound, externalItem );
		} else {
			const item = factory( externalItem );

			// When there is no item we need to remember skipped index first and then we can skip this item.
			if ( !item ) {
				this._skippedIndexesFromExternal.push( index );

				return;
			}

			// Lets try to put item at the same index as index in external collection
			// but when there are a skipped items in one or both collections we need to recalculate this index.
			let finalIndex = index;

			// When we try to insert item after some skipped items from external collection we need
			// to include this skipped items and decrease index.
			//
			// For the following example:
			// external -> [ 'A', 'B - skipped for internal', 'C - skipped for internal' ]
			// internal -> [ A ]
			//
			// Another item is been added at the end of external collection:
			// external.add( 'D' )
			// external -> [ 'A', 'B - skipped for internal', 'C - skipped for internal', 'D' ]
			//
			// We can't just add 'D' to internal at the same index as index in external because
			// this will produce empty indexes what is invalid:
			// internal -> [ 'A', empty, empty, 'D' ]
			//
			// So we need to include skipped items and decrease index
			// internal -> [ 'A', 'D' ]
			for ( const skipped of this._skippedIndexesFromExternal ) {
				if ( index > skipped ) {
					finalIndex--;
				}
			}

			// We need to take into consideration that external collection could skip some items from
			// internal collection.
			//
			// For the following example:
			// internal -> [ 'A', 'B - skipped for external', 'C - skipped for external' ]
			// external -> [ A ]
			//
			// Another item is been added at the end of external collection:
			// external.add( 'D' )
			// external -> [ 'A', 'D' ]
			//
			// We need to include skipped items and place new item after them:
			// internal -> [ 'A', 'B - skipped for external', 'C - skipped for external', 'D' ]
			for ( const skipped of externalCollection._skippedIndexesFromExternal ) {
				if ( finalIndex >= skipped ) {
					finalIndex++;
				}
			}

			this._bindToExternalToInternalMap.set( externalItem, item );
			this._bindToInternalToExternalMap.set( item, externalItem );
			this.add( item, finalIndex );

			// After adding new element to internal collection we need update indexes
			// of skipped items in external collection.
			for ( let i = 0; i < externalCollection._skippedIndexesFromExternal.length; i++ ) {
				if ( finalIndex <= externalCollection._skippedIndexesFromExternal[ i ] ) {
					externalCollection._skippedIndexesFromExternal[ i ]++;
				}
			}
		}
	};

	// Load the initial content of the collection.
	for ( const externalItem of externalCollection ) {
		addItem( null, externalItem, externalCollection.getIndex( externalItem ) );
	}

	// Synchronize the with collection as new items are added.
	this.listenTo( externalCollection, 'add', addItem );

	// Synchronize the with collection as new items are removed.
	this.listenTo( externalCollection, 'remove', ( evt, externalItem, index ) => {
		const item = this._bindToExternalToInternalMap.get( externalItem );

		if ( item ) {
			this.remove( item );
		}

		// After removing element from external collection we need update/remove indexes
		// of skipped items in internal collection.
		this._skippedIndexesFromExternal = this._skippedIndexesFromExternal.reduce( ( result, skipped ) => {
			if ( index < skipped ) {
				result.push( skipped - 1 );
			}

			if ( index > skipped ) {
				result.push( skipped );
			}

			return result;
		}, [] );
	} );
}

首先我们看到的是有一个关键属性this._bindToCollection,这个属性就是当前集合绑定的需要同步数据的外部集合。然后迭代外部集合,将外部集合的数据添加到当前集合。添加集合的逻辑是一个私有方法addItem,其实就是建立外部集合和内部集合的映射。最后就是绑定外部集合的add和remove事件,也就是一旦外部集合添加或者移出数据,那么这里的回调函数就会触发,从而添加当前集合数据或者删除。

这里具体的逻辑也很容易,就是当绑定的数据存在的时候只是添加双向映射,而绑定的数据不存在的时候,调用工厂函数创建并且添加双向映射。有兴趣的可以详细按照各种情况进行列举分析。

最后,我们要谈到的是两个方法,那就是addremove

add( item, index ) {
	return this.addMany( [ item ], index );
}

/**
 * Adds multiple items into the collection.
 *
 * Any item not containing an id will get an automatically generated one.
 *
 * @chainable
 * @param {Iterable.<Object>} item
 * @param {Number} [index] The position of the insertion. Items will be appended if no `index` is specified.
 * @fires add
 * @fires change
 */
addMany( items, index ) {
	if ( index === undefined ) {
		index = this._items.length;
	} else if ( index > this._items.length || index < 0 ) {
		/**
		 * The `index` passed to {@link module:utils/collection~Collection#addMany `Collection#addMany()`}
		 * is invalid. It must be a number between 0 and the collection's length.
		 *
		 * @error collection-add-item-invalid-index
		 */
		throw new CKEditorError( 'collection-add-item-invalid-index', this );
	}

	for ( let offset = 0; offset < items.length; offset++ ) {
		const item = items[ offset ];
		const itemId = this._getItemIdBeforeAdding( item );
		const currentItemIndex = index + offset;

		this._items.splice( currentItemIndex, 0, item );
		this._itemMap.set( itemId, item );

		this.fire( 'add', item, currentItemIndex );
	}

	this.fire( 'change', {
		added: items,
		removed: [],
		index
	} );

	return this;
}
remove( subject ) {
	const [ item, index ] = this._remove( subject );

	this.fire( 'change', {
		added: [],
		removed: [ item ],
		index
	} );

	return item;
}

这里我们注意到这两个方法的逻辑除了操作this._itemsthis._itemMap这两个内部的基本数据结构外,还触发了add,remove,以及change事件,正式由于这个简单的功能,让collection变成了一个基于事件驱动的集合,因此可以监听集合的这些事件,从而做一些自定义的处理。为什么集合有这样的功能呢?关键的代码在这里:

mix( Collection, EmitterMixin );

好了,做了以上分析以后,大家应该对这个Collection的功能都清晰了吧。

总结如下:

1、Collection是一个基本的数据集合容器,能方便的添加,删除以及查询数据。

2、Collection是一个基于事件驱动的集合。

3、Collection是一个能够方便的在两个集合(source-->target)之间同步数据的容器。

CKEditor5——Plugin理解

发布于 2022.08.01 0分钟阅读 0 评论 5 推荐

    CKEditor5

    作者: 敲碎时间,铸造不朽
  1. CKEditor5事件系统(基础使用) Page 1
    1. CKEditor5事件系统(事件优先级) Page 12
    2. CKEditor5事件系统(代理事件) Page 25
    3. CKEditor5事件系统(视图事件冒泡) Page 32
    4. CKEditor5事件系统源码分析(一) Page 36
    5. CKEditor5事件系统源码分析(二) Page 54
    6. CKEditor5事件系统源码分析(三) Page 67
    7. CKeditor5事件系统命名空间分析 Page 73
  2. CKEditor5 模板绑定 Page 87
    1. CKEditor5 Observables Page 94
    2. CKEditor5 Observable——属性绑定 Page 100
    3. CKEditor5 Observable——绑定多个对象或属性 Page 108
    4. CKEditor5 Observable——装饰方法 Page 115
    5. CKEditor5 UI——UI组件 Page 127
    6. CKEditor5——视图添加 Page 137
  3. CKEditor5——模型理解(一) Page 144
    1. CKEditor5——模型理解(二:Node) Page 151
    2. CKEditor5——模型理解(三:Element Text) Page 160
    3. CKEditor5——Node,Element,NodeList源码分析 Page 163
    4. CKEditor5——模型理解(四:模型组成) Page 176
    5. CKEditor5——模型理解(五:Position, Range, Selection) Page 192
    6. CKEditor5——Position源码分析 Page 205
    7. CKEditor5——Position源码分析(二) Page 224
    8. CKEditor5——Position源码分析(三) Page 235
    9. CKEditor5——模型理解(六:Range) Page 247
    10. CKEditor5——模型理解(七:Selection) Page 261
    11. CKEditor5——模型理解(八:Operation和Batch) Page 272
    12. CKEditor5——(九:TreeWalker) Page 276
  4. CKEditor5——Conversion理解 Page 283
  5. CKEditor5——Utils(工具类理解) Page 300
    1. CKEditor5——Utils(Collection类理解) Page 312
    2. CKEditor5——Uitls(DomEmitterMixin类理解) Page 351
  6. CKEditor5——Plugin理解 Page 374
  7. CKEditor5——UI理解 Page 380
    1. CKEditor5——View理解 Page 393
    2. CKEditor5——Template理解(一) Page 403
    3. CKEditor5——Template理解(二) Page 423
    4. CKEditor5——Template理解(三) Page 460

CKEditor5——Plugin理解

今天开始理解学习CK5的插件类,整个类是所有插件的抽象,看看源码:

export default class Plugin {
	/**
	 * @inheritDoc
	 */
	constructor( editor ) {
		
		this.editor = editor;

		
		this.set( 'isEnabled', true );

		/**
		 * Holds identifiers for {@link #forceDisabled} mechanism.
		 *
		 * @type {Set.<String>}
		 * @private
		 */
		this._disableStack = new Set();
	}
}

从以上代码可以看出,每个插件有包含一个eidtor引用,因此,插件可以调用编辑器实例的任何方法,包括调用命令,判断editor的只读性等等。

这里有一个isEnabled属性,判断当前插件是否可用,以及一个设置插件可用性的Set集合。

插件的启用和禁用

然后再看看另外另个重要方法:

forceDisabled( id ) {
	this._disableStack.add( id );

	if ( this._disableStack.size == 1 ) {
		this.on( 'set:isEnabled', forceDisable, { priority: 'highest' } );
		this.isEnabled = false;
	}
}

/**
 * Clears forced disable previously set through {@link #forceDisabled}. See {@link #forceDisabled}.
 *
 * @param {String} id Unique identifier, equal to the one passed in {@link #forceDisabled} call.
 */
clearForceDisabled( id ) {
	this._disableStack.delete( id );
	if ( this._disableStack.size == 0 ) {
		this.off( 'set:isEnabled', forceDisable );
		this.isEnabled = true;
	}
}

这里如果需要禁用插件,只需要调用forceDisabled方法,这个方法很简单,就是将名字存储起来,然后绑定一个禁用的事件set:isEnabled,然后设置isEnabled为false。

同理,清除禁用就是将名字删除掉,并且全部删除掉,然后去掉绑定的set:isEnabled事件,设置isEnabled为true。

我们看看这个函数forceDisable

function forceDisable( evt ) {
	evt.return = false;
	evt.stop();
}

看到了吧,一旦我们禁用了插件,当我们再设置plugin.isEnabled的时候,就会触发这个回调函数,返回值为false,且停止后续回调函数的执行,因为这个优先级是最高,因此插件的这个属性值永远不会修改,它永远是false,因此这个插件永远不可用。当去掉这个事件监听后,这个属性isEnabled变回true,因此插件又变成可用啦。

大家应该理解这个原理了吧。

CKEditor5事件系统源码分析(三)

发布于 2022.07.31 5分钟阅读 0 评论 5 推荐

    CKEditor5

    作者: 敲碎时间,铸造不朽
  1. CKEditor5事件系统(基础使用) Page 1
    1. CKEditor5事件系统(事件优先级) Page 12
    2. CKEditor5事件系统(代理事件) Page 25
    3. CKEditor5事件系统(视图事件冒泡) Page 32
    4. CKEditor5事件系统源码分析(一) Page 36
    5. CKEditor5事件系统源码分析(二) Page 54
    6. CKEditor5事件系统源码分析(三) Page 67
    7. CKeditor5事件系统命名空间分析 Page 73
  2. CKEditor5 模板绑定 Page 87
    1. CKEditor5 Observables Page 94
    2. CKEditor5 Observable——属性绑定 Page 100
    3. CKEditor5 Observable——绑定多个对象或属性 Page 108
    4. CKEditor5 Observable——装饰方法 Page 115
    5. CKEditor5 UI——UI组件 Page 127
    6. CKEditor5——视图添加 Page 137
  3. CKEditor5——模型理解(一) Page 144
    1. CKEditor5——模型理解(二:Node) Page 151
    2. CKEditor5——模型理解(三:Element Text) Page 160
    3. CKEditor5——Node,Element,NodeList源码分析 Page 163
    4. CKEditor5——模型理解(四:模型组成) Page 176
    5. CKEditor5——模型理解(五:Position, Range, Selection) Page 192
    6. CKEditor5——Position源码分析 Page 205
    7. CKEditor5——Position源码分析(二) Page 224
    8. CKEditor5——Position源码分析(三) Page 235
    9. CKEditor5——模型理解(六:Range) Page 247
    10. CKEditor5——模型理解(七:Selection) Page 261
    11. CKEditor5——模型理解(八:Operation和Batch) Page 272
    12. CKEditor5——(九:TreeWalker) Page 276
  4. CKEditor5——Conversion理解 Page 283
  5. CKEditor5——Utils(工具类理解) Page 300
    1. CKEditor5——Utils(Collection类理解) Page 312
    2. CKEditor5——Uitls(DomEmitterMixin类理解) Page 351
  6. CKEditor5——Plugin理解 Page 374
  7. CKEditor5——UI理解 Page 380
    1. CKEditor5——View理解 Page 393
    2. CKEditor5——Template理解(一) Page 403
    3. CKEditor5——Template理解(二) Page 423
    4. CKEditor5——Template理解(三) Page 460

CKEditor5事件系统源码分析(三)

上一节我们分析了事件系统的关键类EmitterMixin的fire()方法,今天我们看看另一个方法就是delegate()

这个方法的代码如下:

delegate( ...events ) {
	return {
		to: ( emitter, nameOrFunction ) => {
			if ( !this._delegations ) {
				this._delegations = new Map();
			}

		    // Originally there was a for..of loop which unfortunately caused an error in Babel that didn't allow
			// build an application. See: https://github.com/ckeditor/ckeditor5-react/issues/40.
			events.forEach( eventName => {
				const destinations = this._delegations.get( eventName );

				if ( !destinations ) {
					this._delegations.set( eventName, new Map( [ [ emitter, nameOrFunction ] ] ) );
				} else {
					destinations.set( emitter, nameOrFunction );
				}
			} );
		}
	};
}

这个方法就比较简单,首先,我们需要一个代理映射,这个代理映射是一个map,它的key显然是事件名称,而value值又是一个map,这个map的key是emitter,value是事件名称或者一个函数。

实际上在调用这个delegate方法的时候,本质上就是存储一些数据结构,而这个结构是有一个两层的map来构建的。

这样在fire的时候,先取出这些map,然后再调用一个方法就是fireDelegatedEvents

下面我们看看这个方法:

fireDelegatedEvents( destinations, eventInfo, fireArgs ) {
	for ( let [ emitter, name ] of destinations ) {
		if ( !name ) {
			name = eventInfo.name;
		} else if ( typeof name == 'function' ) {
			name = name( eventInfo.name );
		}

		const delegatedInfo = new EventInfo( eventInfo.source, name );

		delegatedInfo.path = [ ...eventInfo.path ];

		emitter.fire( delegatedInfo, ...fireArgs );
	}
}

首先会跌倒这个事件对应的map,实际上就是一个一个的<emitter,name>对,然后构造具体的事件,这个通过代码const delegatedInfo = new EventInfo( eventInfo.source, name );,然后配置path,最后调用fire出发事件,迭代完map实际上就是执行所有的代理功能。

好了,这个方法挺简单的,至于取消代理的方法就不用分析了,本质上就是删除两层map中的一些值。

 

 

 

CKEditor5事件系统源码分析(一)

更新于 2022.07.30 18分钟阅读 0 评论 5 推荐

    CKEditor5

    作者: 敲碎时间,铸造不朽
  1. CKEditor5事件系统(基础使用) Page 1
    1. CKEditor5事件系统(事件优先级) Page 12
    2. CKEditor5事件系统(代理事件) Page 25
    3. CKEditor5事件系统(视图事件冒泡) Page 32
    4. CKEditor5事件系统源码分析(一) Page 36
    5. CKEditor5事件系统源码分析(二) Page 54
    6. CKEditor5事件系统源码分析(三) Page 67
    7. CKeditor5事件系统命名空间分析 Page 73
  2. CKEditor5 模板绑定 Page 87
    1. CKEditor5 Observables Page 94
    2. CKEditor5 Observable——属性绑定 Page 100
    3. CKEditor5 Observable——绑定多个对象或属性 Page 108
    4. CKEditor5 Observable——装饰方法 Page 115
    5. CKEditor5 UI——UI组件 Page 127
    6. CKEditor5——视图添加 Page 137
  3. CKEditor5——模型理解(一) Page 144
    1. CKEditor5——模型理解(二:Node) Page 151
    2. CKEditor5——模型理解(三:Element Text) Page 160
    3. CKEditor5——Node,Element,NodeList源码分析 Page 163
    4. CKEditor5——模型理解(四:模型组成) Page 176
    5. CKEditor5——模型理解(五:Position, Range, Selection) Page 192
    6. CKEditor5——Position源码分析 Page 205
    7. CKEditor5——Position源码分析(二) Page 224
    8. CKEditor5——Position源码分析(三) Page 235
    9. CKEditor5——模型理解(六:Range) Page 247
    10. CKEditor5——模型理解(七:Selection) Page 261
    11. CKEditor5——模型理解(八:Operation和Batch) Page 272
    12. CKEditor5——(九:TreeWalker) Page 276
  4. CKEditor5——Conversion理解 Page 283
  5. CKEditor5——Utils(工具类理解) Page 300
    1. CKEditor5——Utils(Collection类理解) Page 312
    2. CKEditor5——Uitls(DomEmitterMixin类理解) Page 351
  6. CKEditor5——Plugin理解 Page 374
  7. CKEditor5——UI理解 Page 380
    1. CKEditor5——View理解 Page 393
    2. CKEditor5——Template理解(一) Page 403
    3. CKEditor5——Template理解(二) Page 423
    4. CKEditor5——Template理解(三) Page 460

在学习CK5的时候,在事件系统这一块,有一个特别关键的类,那就是EmitterMixin这个类,这个类在什么地方呢?他其实就在ckeditor5-utils包中。下面我们来看看这个类:  

EmitterMixin类

const EmitterMixin = {
	/**
	 * @inheritDoc
	 */
	on( event, callback, options = {} ) {
		this.listenTo( this, event, callback, options );
	},
	/**
	 * @inheritDoc
	 */
	listenTo( emitter, event, callback, options = {} ) {
		let emitterInfo, eventCallbacks;

		// _listeningTo contains a list of emitters that this object is listening to.
		// This list has the following format:
		//
		// _listeningTo: {
		//     emitterId: {
		//         emitter: emitter,
		//         callbacks: {
		//             event1: [ callback1, callback2, ... ]
		//             ....
		//         }
		//     },
		//     ...
		// } 

		if ( !this[ _listeningTo ] ) {
			this[ _listeningTo ] = {};
		}

		const emitters = this[ _listeningTo ];

		if ( !_getEmitterId( emitter ) ) {
			_setEmitterId( emitter );
		}

		const emitterId = _getEmitterId( emitter );

		if ( !( emitterInfo = emitters[ emitterId ] ) ) {
			emitterInfo = emitters[ emitterId ] = {
				emitter,
				callbacks: {}
			};
		}

		if ( !( eventCallbacks = emitterInfo.callbacks[ event ] ) ) {
			eventCallbacks = emitterInfo.callbacks[ event ] = [];
		}

		eventCallbacks.push( callback );

		// Finally register the callback to the event.
		addEventListener( this, emitter, event, callback, options );
	},
	/**
	 * @inheritDoc
	 */
	_addEventListener( event, callback, options ) {
		createEventNamespace( this, event );

		const lists = getCallbacksListsForNamespace( this, event );
		const priority = priorities.get( options.priority );

		const callbackDefinition = {
			callback,
			priority
		};

		// Add the callback to all callbacks list.
		for ( const callbacks of lists ) {
			// Add the callback to the list in the right priority position.
			insertToPriorityArray( callbacks, callbackDefinition );
		}
	},
}
/**
 * Sets emitter's unique id.
 *
 * **Note:** `_emitterId` can be set only once.
 *
 * @protected
 * @param {module:utils/emittermixin~Emitter} emitter An emitter for which id will be set.
 * @param {String} [id] Unique id to set. If not passed, random unique id will be set.
 */
export function _setEmitterId( emitter, id ) {
	if ( !emitter[ _emitterId ] ) {
		emitter[ _emitterId ] = id || uid();
	}
}

/**
 * Returns emitter's unique id.
 *
 * @protected
 * @param {module:utils/emittermixin~Emitter} emitter An emitter which id will be returned.
 */
export function _getEmitterId( emitter ) {
	return emitter[ _emitterId ];
}
// Helper for registering event callback on the emitter.
function addEventListener( listener, emitter, event, callback, options ) {
	if ( emitter._addEventListener ) {
		emitter._addEventListener( event, callback, options );
	} else {
		// Allow listening on objects that do not implement Emitter interface.
		// This is needed in some tests that are using mocks instead of the real objects with EmitterMixin mixed.
		listener._addEventListener.call( emitter, event, callback, options );
	}
}

以上部分,我贴出了这个类最核心的代码,总结如下:

1、on方法实际上是listenTo方法的一个包装,on方法监听的是emitter自身,而listenTo可以监听别的emitter。

2、listenTo有四个参数,分别是需要监听的emitter,监听的事件名称,对应的回调函数,以及可选参数,比如优先级啥的。

3、这个方法调用的结果是会构造一个emitters数组。这个数组的结构如下:

_listeningTo: {
	emitterId: {
	    emitter: emitter,
		callbacks: {
			event1: [ callback1, callback2, ... ]
		    ....
		}
    },
	...
}

结果是会创建多个emitter,每个emitter会有一个唯一的emitterId,这个id对应的属性如下:emitter,一组callbacks,这个callbacks实际上是一个map,key对应的是事件名称,而value对应的是这个事件对应的回调函数数组。

/** _listeningTo这个属性对应的命名空间不存在,创建这个属性 */
if ( !this[ _listeningTo ] ) {
	this[ _listeningTo ] = {};
}

/** 获取_listeningTo属性下的emitters*/
const emitters = this[ _listeningTo ];

/** 如果传递进来的emitters没有一个emitterId,那么设置一个id */
if ( !_getEmitterId( emitter ) ) {
	_setEmitterId( emitter );
}
/** 获取emitterId*/
const emitterId = _getEmitterId( emitter );

/** 构建emitter基本信息,实际上就是设置emitter属性和callbacks属性 */
if ( !( emitterInfo = emitters[ emitterId ] ) ) {
	emitterInfo = emitters[ emitterId ] = {
		emitter,
		callbacks: {}
	};
}
/** 判断事件名称event有没有对应的回调函数数组,没有的话创建一个,有的话将回调函数添加到末尾 */
if ( !( eventCallbacks = emitterInfo.callbacks[ event ] ) ) {
	eventCallbacks = emitterInfo.callbacks[ event ] = [];
}
eventCallbacks.push( callback );

addEventListener

最后一步就是绑定事件addEventListener( this, emitter, event, callback, options );

下面我们介绍绑定事件:

这个方法实际上调用的是emitter类中的_addEventListener( event, callback, options )

createEventNamespace( this, event );

const lists = getCallbacksListsForNamespace( this, event );
const priority = priorities.get( options.priority );

const callbackDefinition = {
	callback,
	priority
};

// Add the callback to all callbacks list.
for ( const callbacks of lists ) {
	// Add the callback to the list in the right priority position.
	insertToPriorityArray( callbacks, callbackDefinition );
}

这个类的作用如下:

1、创建事件的命名空间

2、返回名称为event的事件对应的回调函数数组

3、根据参数options中的优先级对回调函数数组排序,这样我们在执行fire的时候就能够根据优先级先后执行回调函数啦。

 

注意:在ck5中事件有一个命名空间的概念,它的实现其实就是在createEventNamespace( this, event )

这个函数里实现的,如果想理解具体的请参考这个类的具体实现,我在这里不做分析啦。

另一个实现其实就是对回调函数按照优先级进行排序。这里就这两个知识点。

 

有了以上的分析,我们可以得出结论就是事件系统的emitters的基本结构和事件绑定已经构建起来,下一步就是fire出发回调函数的执行,我们以后在分析。欢迎分析和讨论交流。

这个类可以说的整个ck5中最基础,最关键的一个类,后面的Observavle,Plugin,Command,Editor,Model,Document等等都是以这个类为基础。

 

 

 

 

 

CKEditor5事件系统源码分析(二)

更新于 2022.07.30 13分钟阅读 0 评论 5 推荐

    CKEditor5

    作者: 敲碎时间,铸造不朽
  1. CKEditor5事件系统(基础使用) Page 1
    1. CKEditor5事件系统(事件优先级) Page 12
    2. CKEditor5事件系统(代理事件) Page 25
    3. CKEditor5事件系统(视图事件冒泡) Page 32
    4. CKEditor5事件系统源码分析(一) Page 36
    5. CKEditor5事件系统源码分析(二) Page 54
    6. CKEditor5事件系统源码分析(三) Page 67
    7. CKeditor5事件系统命名空间分析 Page 73
  2. CKEditor5 模板绑定 Page 87
    1. CKEditor5 Observables Page 94
    2. CKEditor5 Observable——属性绑定 Page 100
    3. CKEditor5 Observable——绑定多个对象或属性 Page 108
    4. CKEditor5 Observable——装饰方法 Page 115
    5. CKEditor5 UI——UI组件 Page 127
    6. CKEditor5——视图添加 Page 137
  3. CKEditor5——模型理解(一) Page 144
    1. CKEditor5——模型理解(二:Node) Page 151
    2. CKEditor5——模型理解(三:Element Text) Page 160
    3. CKEditor5——Node,Element,NodeList源码分析 Page 163
    4. CKEditor5——模型理解(四:模型组成) Page 176
    5. CKEditor5——模型理解(五:Position, Range, Selection) Page 192
    6. CKEditor5——Position源码分析 Page 205
    7. CKEditor5——Position源码分析(二) Page 224
    8. CKEditor5——Position源码分析(三) Page 235
    9. CKEditor5——模型理解(六:Range) Page 247
    10. CKEditor5——模型理解(七:Selection) Page 261
    11. CKEditor5——模型理解(八:Operation和Batch) Page 272
    12. CKEditor5——(九:TreeWalker) Page 276
  4. CKEditor5——Conversion理解 Page 283
  5. CKEditor5——Utils(工具类理解) Page 300
    1. CKEditor5——Utils(Collection类理解) Page 312
    2. CKEditor5——Uitls(DomEmitterMixin类理解) Page 351
  6. CKEditor5——Plugin理解 Page 374
  7. CKEditor5——UI理解 Page 380
    1. CKEditor5——View理解 Page 393
    2. CKEditor5——Template理解(一) Page 403
    3. CKEditor5——Template理解(二) Page 423
    4. CKEditor5——Template理解(三) Page 460

CKEditor5事件系统源码分析(二)

上一节我们分析了CK5的EmitterMixin类的重点方法addEventListener,今天我们再认真看看fire方法。

fire( eventOrInfo, ...args ) {
	try {
		const eventInfo = eventOrInfo instanceof EventInfo ? eventOrInfo : new EventInfo( this, eventOrInfo );
		const event = eventInfo.name;
		let callbacks = getCallbacksForEvent( this, event );

		// Record that the event passed this emitter on its path.
		eventInfo.path.push( this ); 

		// Handle event listener callbacks first.
		if ( callbacks ) {
			// Arguments passed to each callback.
			const callbackArgs = [ eventInfo, ...args ];

			// Copying callbacks array is the easiest and most secure way of preventing infinite loops, when event callbacks
			// are added while processing other callbacks. Previous solution involved adding counters (unique ids) but
			// failed if callbacks were added to the queue before currently processed callback.
			// If this proves to be too inefficient, another method is to change `.on()` so callbacks are stored if same
			// event is currently processed. Then, `.fire()` at the end, would have to add all stored events.
			callbacks = Array.from( callbacks );

			for ( let i = 0; i < callbacks.length; i++ ) {
				callbacks[ i ].callback.apply( this, callbackArgs );

				// Remove the callback from future requests if off() has been called.
				if ( eventInfo.off.called ) {
					// Remove the called mark for the next calls.
					delete eventInfo.off.called;
					this._removeEventListener( event, callbacks[ i ].callback );
				}

				// Do not execute next callbacks if stop() was called.
				if ( eventInfo.stop.called ) {
					break;
				}
			}
		}

		// Delegate event to other emitters if needed.
		if ( this._delegations ) {
			const destinations = this._delegations.get( event );
			const passAllDestinations = this._delegations.get( '*' );

			if ( destinations ) {
				fireDelegatedEvents( destinations, eventInfo, args );
			}
			if ( passAllDestinations ) {
				fireDelegatedEvents( passAllDestinations, eventInfo, args );
			}
		}

		return eventInfo.return;
	} catch ( err ) {
		// @if CK_DEBUG // throw err;
		/* istanbul ignore next */
		CKEditorError.rethrowUnexpectedError( err, this );
	}
},

重点方法

这里面有一个重点方法:

// Get the list of callbacks for a given event, but only if there any callbacks have been registered.
// If there are no callbacks registered for given event, it checks if this is a specific event and looks
// for callbacks for it's more generic version.
function getCallbacksForEvent( source, eventName ) {
	let event;

	if ( !source._events || !( event = source._events[ eventName ] ) || !event.callbacks.length ) {
		// There are no callbacks registered for specified eventName.
		// But this could be a specific-type event that is in a namespace.
		if ( eventName.indexOf( ':' ) > -1 ) {
			// If the eventName is specific, try to find callback lists for more generic event.
			return getCallbacksForEvent( source, eventName.substr( 0, eventName.lastIndexOf( ':' ) ) );
		} else {
			// If this is a top-level generic event, return null;
			return null;
		}
	}

	return event.callbacks;
}

注意,这里获取指定事件的回调函数列表的时候,举个例子来说吧,如果指定的事件名称是insert:p;它会先寻找这个事件对应的回调函数列表,如果不存在,则会寻找insert事件名称对应的回调函数;因此当我们调用emitters.fire('insert:p', args)的时候,监听insert:p 事件和监听insert事件的回调函数都会执行。

这里有一个疑问?如果这个回去回调函数列表的方法已经注册了insert:p事件,那么它就不会往下寻找更通用的insert事件,这个又是怎么出发insert事件的回调函数呢?这里留一个疑问哈。

当我们获取回调函数列表后,会构建EventInfo对象,然后将当前的emitter放到EventInfo的path路径中,这样我们就方便知道事件的传播路径啦。 

剩下的自然就是迭代回调函数,然后执行。在执行的过程中,我们看到一些逻辑,如果这个事件调用了off方法,那么这个回调函数将取消监听;而如果这个事件调用了stop()方法,那么回调函数的迭代将结束;这就是有的时候我们需要用自定义的回调屏蔽掉一些不必要的回调的时候,我们就调用event.stop()即可。

剩下的部分代码是关于事件代理的,这个我们以后在分析,当这些迭代执行完成后,我们返回eventInfo.return属性,这就是我们可以为回调函数添加返回值的原因。

CKEditor5事件系统(基础使用)

更新于 2022.07.26 10分钟阅读 0 评论 5 推荐

    CKEditor5

    作者: 敲碎时间,铸造不朽
  1. CKEditor5事件系统(基础使用) Page 1
    1. CKEditor5事件系统(事件优先级) Page 12
    2. CKEditor5事件系统(代理事件) Page 25
    3. CKEditor5事件系统(视图事件冒泡) Page 32
    4. CKEditor5事件系统源码分析(一) Page 36
    5. CKEditor5事件系统源码分析(二) Page 54
    6. CKEditor5事件系统源码分析(三) Page 67
    7. CKeditor5事件系统命名空间分析 Page 73
  2. CKEditor5 模板绑定 Page 87
    1. CKEditor5 Observables Page 94
    2. CKEditor5 Observable——属性绑定 Page 100
    3. CKEditor5 Observable——绑定多个对象或属性 Page 108
    4. CKEditor5 Observable——装饰方法 Page 115
    5. CKEditor5 UI——UI组件 Page 127
    6. CKEditor5——视图添加 Page 137
  3. CKEditor5——模型理解(一) Page 144
    1. CKEditor5——模型理解(二:Node) Page 151
    2. CKEditor5——模型理解(三:Element Text) Page 160
    3. CKEditor5——Node,Element,NodeList源码分析 Page 163
    4. CKEditor5——模型理解(四:模型组成) Page 176
    5. CKEditor5——模型理解(五:Position, Range, Selection) Page 192
    6. CKEditor5——Position源码分析 Page 205
    7. CKEditor5——Position源码分析(二) Page 224
    8. CKEditor5——Position源码分析(三) Page 235
    9. CKEditor5——模型理解(六:Range) Page 247
    10. CKEditor5——模型理解(七:Selection) Page 261
    11. CKEditor5——模型理解(八:Operation和Batch) Page 272
    12. CKEditor5——(九:TreeWalker) Page 276
  4. CKEditor5——Conversion理解 Page 283
  5. CKEditor5——Utils(工具类理解) Page 300
    1. CKEditor5——Utils(Collection类理解) Page 312
    2. CKEditor5——Uitls(DomEmitterMixin类理解) Page 351
  6. CKEditor5——Plugin理解 Page 374
  7. CKEditor5——UI理解 Page 380
    1. CKEditor5——View理解 Page 393
    2. CKEditor5——Template理解(一) Page 403
    3. CKEditor5——Template理解(二) Page 423
    4. CKEditor5——Template理解(三) Page 460

最近在学习CK5,一种最大的感受就是CK5的架构不是很大,但是内容特别多。笔者在学习中,总结出一个浅显的道理,那就是

掌握基础知识,对框架宏观把握,学习起来会事半功倍。

今天开始初步研究一下CK5的事件系统:

在CK5的事件系统中,关键的一个对象被称作Emitter(发射器),Emitter是一个可以发送事件的对象。如何创建一个Emitter,下面的代码创建了一个混合了事件发送的AnyClass类,它实例化以后就是一个可以发送事件的Emitter对象

import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
import mix from '@ckeditor/ckeditor5-utils/src/mix';
class AnyClass {
   // ...
}
mix( AnyClass, EmitterMixin );

上面的代码来自CK5的官网,分析以上代码,可以看出核心的类有两个,一个是EmitterMixin,另一个是mix

不难看出:

第一个类EmitterMixin是实现了事件系统的核心类,而mix方法只是将事件系统核心功能复制到具体类。

Emitter的On和Off分析

以上代码中AnyClass就是一个具有监听和发送时间的类,因此可以有以下代码:

Const anyClass = new AnyClass();
anyClass.on( 'eventName', ( eventInfo, ...args ) => { ... } );

这个anyClass对象就可以监听一个名为eventName的事件,一旦触发(anyClass.fire('eventName'))这个事件,那么就会执行后面的回调函数。

再次用另一段代码来说明问题

splitButtonView.on( 'execute', () => {
	editor.execute( 'newCodeBlock', {
		usePreviousLanguageChoice: true
	} );
	editor.editing.view.focus();
} );

看到了吧,这段代码的意思是,一旦在splitButtonView触发execute事件,那么就会执行回调函数。这里需要注意的是:

  1. splitButtonView是一个Emitter对象
  2. 触发事件只能在这个splitButtonView对象上,也就是自己监听自己

如果要移除监听呢?

AnyClass.off( 'eventName', handler );

简单吧。

Emitter的listenTo和stopListening分析

CK5还提供了另一种监听方式:

const anyClass = new AnyClass();
//注意,这里假定AnotherClass也是一个Emitter
const anotherClass = new AnotherClass();
AnyClass.listenTo( anotherClass, 'eventName', ( eventInfo, ...args ) => { ... } );

这里的意思就是emitter:anyClass 监听 emitter:anotherClass上的eventName事件,如果后者(anotherClass.fire('eventName'))触发了这个事件,那么回调函数会立即执行。

this.listenTo( editor.editing.view.document, 'clipboardInput', ( evt, data ) => {
    console.log('clipboardInput');
    let insertionRange = model.createRange( model.document.selection.anchor );
 
    // Use target ranges in case this is a drop.
    if ( data.targetRanges ) {
          insertionRange = editor.editing.mapper.toModelRange( data.targetRanges[ 0 ] );
    }
 
    if ( !insertionRange.start.parent.is( 'element', 'newCodeBlock' ) ) {
          return;
    }
 
    const text = data.dataTransfer.getData( 'text/plain' );
    const writer = new UpcastWriter( editor.editing.view.document );
 
    // Pass the view fragment to the default clipboardInput handler.
    data.content = rawSnippetTextToViewDocumentFragment( writer, text );
} );

上面的代码我是从一个插件类中摘取出来的,this就代表这个插件类,而这个插件类会监听这个对象:

editor.editing.view.document

而监听的事件是剪贴板在视图文档的输入,其实说人话就是当我们复制一些内容到CK5的内容去的时候,这个插件的回调函数就会执行。

类似的原理,如果想移除对某个emitter的监听:

const anyClass = new AnyClass();
//注意,这里假定AnotherClass也是一个Emitter
const anotherClass = new AnotherClass();
// Stop listening to a specific handler.
anyClass.stopListening( anotherClass, 'eventName', handler );
// Stop listening to a specific event.
anyClass.stopListening( anotherClass, 'eventName' );
// Stop listening to all events fired by a specific emitter.
anyClass.stopListening( anotherClass);
// Stop listening to all events fired by all bound emitters.
anyClass.stopListening();

通过以上介绍,我想大家应该明白emitter的on和listenTo方法的区别和如何使用了吧,以及stopListening()方法的使用。

总结一下:

1、emitter的on方法绑定的函数是通过自身来触发的。

2、emitter的listenTo方法是通过其他emitter来触发的。

3、stopListening()解除绑定的函数,是否有参数决定解除绑定的粒度问题。

CKEditor5——Conversion理解

更新于 2022.07.26 1分钟阅读 0 评论 5 推荐

    CKEditor5

    作者: 敲碎时间,铸造不朽
  1. CKEditor5事件系统(基础使用) Page 1
    1. CKEditor5事件系统(事件优先级) Page 12
    2. CKEditor5事件系统(代理事件) Page 25
    3. CKEditor5事件系统(视图事件冒泡) Page 32
    4. CKEditor5事件系统源码分析(一) Page 36
    5. CKEditor5事件系统源码分析(二) Page 54
    6. CKEditor5事件系统源码分析(三) Page 67
    7. CKeditor5事件系统命名空间分析 Page 73
  2. CKEditor5 模板绑定 Page 87
    1. CKEditor5 Observables Page 94
    2. CKEditor5 Observable——属性绑定 Page 100
    3. CKEditor5 Observable——绑定多个对象或属性 Page 108
    4. CKEditor5 Observable——装饰方法 Page 115
    5. CKEditor5 UI——UI组件 Page 127
    6. CKEditor5——视图添加 Page 137
  3. CKEditor5——模型理解(一) Page 144
    1. CKEditor5——模型理解(二:Node) Page 151
    2. CKEditor5——模型理解(三:Element Text) Page 160
    3. CKEditor5——Node,Element,NodeList源码分析 Page 163
    4. CKEditor5——模型理解(四:模型组成) Page 176
    5. CKEditor5——模型理解(五:Position, Range, Selection) Page 192
    6. CKEditor5——Position源码分析 Page 205
    7. CKEditor5——Position源码分析(二) Page 224
    8. CKEditor5——Position源码分析(三) Page 235
    9. CKEditor5——模型理解(六:Range) Page 247
    10. CKEditor5——模型理解(七:Selection) Page 261
    11. CKEditor5——模型理解(八:Operation和Batch) Page 272
    12. CKEditor5——(九:TreeWalker) Page 276
  4. CKEditor5——Conversion理解 Page 283
  5. CKEditor5——Utils(工具类理解) Page 300
    1. CKEditor5——Utils(Collection类理解) Page 312
    2. CKEditor5——Uitls(DomEmitterMixin类理解) Page 351
  6. CKEditor5——Plugin理解 Page 374
  7. CKEditor5——UI理解 Page 380
    1. CKEditor5——View理解 Page 393
    2. CKEditor5——Template理解(一) Page 403
    3. CKEditor5——Template理解(二) Page 423
    4. CKEditor5——Template理解(三) Page 460

大家知道,在CKEditor5中,Conversion(转化器)是最重要的一个组件之一,为了深入的理解转化器,我们先从大的层面来掌握一下,以后再分别从细节入手。

我们从上面的图中不难看出,总的来说有三个converter,那么这三个converter在代码中具体在哪里呢?

我们在ckeditor5-engine包下的controller包中有两个类,而这两个类才是真正存放转化控制器的代码的地方:

这里我贴出代码:

datacontroller.js

this.downcastDispatcher = new DowncastDispatcher( {
	mapper: this.mapper,
	schema: model.schema
} );
this.downcastDispatcher.on( 'insert:$text', insertText(), { priority: 'lowest' } );


this.upcastDispatcher = new UpcastDispatcher( {
	schema: model.schema
} );
this.upcastDispatcher.on( 'text', convertText(), { priority: 'lowest' } );
this.upcastDispatcher.on( 'element', convertToModelFragment(), { priority: 'lowest' } );
this.upcastDispatcher.on( 'documentFragment', convertToModelFragment(), { priority: 'lowest' } );

editingcontroller.js

this.downcastDispatcher = new DowncastDispatcher( {
	mapper: this.mapper,
	schema: model.schema
} );
// Attach default model converters.
this.downcastDispatcher.on( 'insert:$text', insertText(), { priority: 'lowest' } );
this.downcastDispatcher.on( 'remove', remove(), { priority: 'low' } );

// Attach default model selection converters.
this.downcastDispatcher.on( 'selection', clearAttributes(), { priority: 'high' } );
this.downcastDispatcher.on( 'selection', convertRangeSelection(), { priority: 'low' } );
this.downcastDispatcher.on( 'selection', convertCollapsedSelection(), { priority: 'low' } );

大家注意没有,在datacontroller(数据控制器)中又两个转化器,分别对应着向上和向下两个方向。在向下的方向,注册了一个插入文本模型节点的事件,一旦我们向我们的模型树中插入文本节点,那么就会执行insertText(),这个方法用于向数据视图插入数据。其实在editingcontroller(编辑控制器)中也有一个这样的事件,它对文本节点的插入和数据控制器的处理是一致的。

同时,在datacontroller中初始化了一个向上的转化器,它主要处理文本,元素和文档段的转化,将它们转化成模型或者模型的属性。

同理,在editingcontroller(编辑控制器)中还处理其他一些事件,比如移除事件,选择事件等,这些都是在模型节点有移除或者模型节点被选择的时候触发的模型转视图。

记住,我们这里可以暂时不用管具体是怎么处理的,只需要知道有三个转化器,每个转化器处理不同的情况。

 

有了上面的知识,我们对conversion的理解有近了一步,那么这里有个问题,conversion是editor的一个属性,那么这个属性是怎么初始化的呢?我在editor.js中找到如下代码:

this.data = new DataController( this.model, stylesProcessor );
this.editing = new EditingController( this.model, stylesProcessor );
this.conversion = new Conversion( [ this.editing.downcastDispatcher, this.data.downcastDispatcher ], this.data.upcastDispatcher );
this.conversion.addAlias( 'dataDowncast', this.data.downcastDispatcher );
this.conversion.addAlias( 'editingDowncast', this.editing.downcastDispatcher );

大家看到了吧,在editor初始化的时候,将数据控制器的两个转化器和编辑控制器的转化器作为构造参数传递到了conversion这个类中。下面我们看看conversion.js是怎么实现的:

conversion.js

export default class Conversion {
	constructor( downcastDispatchers, upcastDispatchers ) {

		this._helpers = new Map();


		this._downcast = toArray( downcastDispatchers );
		this._createConversionHelpers( { name: 'downcast', dispatchers: this._downcast, isDowncast: true } );

		this._upcast = toArray( upcastDispatchers );
		this._createConversionHelpers( { name: 'upcast', dispatchers: this._upcast, isDowncast: false } );
	}
}

从这里的代码我们可以知道,这三个转化器都被传递到了Conversion这个类中,分别存储在_downcast和_upcast属性中,同时,我们看看这个私有方法:_createConversionHelpers()

_createConversionHelpers( { name, dispatchers, isDowncast } ) {
	if ( this._helpers.has( name ) ) {

		throw new CKEditorError( 'conversion-group-exists', this );
	}

	const helpers = isDowncast ? new DowncastHelpers( dispatchers ) : new UpcastHelpers( dispatchers );

	this._helpers.set( name, helpers );
}

从上面我们可以看出,这个方法实际上就是将转化器包装成不同的helper后进行分组,一个组的名字叫做downcast,另一个叫做upcast。

一般我们在使用的时候调用方法都是:

editor.conversion.for( 'downcast' ).elementToElement( config ) );

我们知道editor.conversion.for( 'downcast' )这个方法调用后返回的是DowncastHelpers的实例,而这个类的elementToElement()是这样的:

elementToElement( config ) {
	return this.add( downcastElementToElement( config ) );
}

这里的add是父类的一个方法:

conversionhelpers.js

export default class ConversionHelpers {
	
	constructor( dispatchers ) {
		this._dispatchers = dispatchers;
	}


	add( conversionHelper ) {
		for ( const dispatcher of this._dispatchers ) {
			conversionHelper( dispatcher );
		}

		return this;
	}
}

这里的add方法的参数是一个函数,且这个函数接收一个dispatcher作为参数,因此我们可以推测:

downcastElementToElement( config )应该返回一个函数,同时这个函数的参数是dispatcher

下面我们看看这个方法是怎么实现的:

function downcastElementToElement( config ) {
	config = cloneDeep( config );

	config.view = normalizeToElementConfig( config.view, 'container' );

	return dispatcher => {
		dispatcher.on( 'insert:' + config.model, insertElement( config.view ), { priority: config.converterPriority || 'normal' } );

		if ( config.triggerBy ) {
			if ( config.triggerBy.attributes ) {
				for ( const attributeKey of config.triggerBy.attributes ) {
					dispatcher._mapReconversionTriggerEvent( config.model, `attribute:${ attributeKey }:${ config.model }` );
				}
			}

			if ( config.triggerBy.children ) {
				for ( const childName of config.triggerBy.children ) {
					dispatcher._mapReconversionTriggerEvent( config.model, `insert:${ childName }` );
					dispatcher._mapReconversionTriggerEvent( config.model, `remove:${ childName }` );
				}
			}
		}
	};
}

不出所料,这个方法的确返回一个函数,而这个函数的执行逻辑就是绑定当某个模型插入的时候,我们应该进行的模型转化视图的逻辑,同时还附加一些其他操作,这里的其他操作我们以后分析。当然,这里还有一个逻辑就是normalizeToElementConfig()这个方法,它是对视图的一些处理。感兴趣的可以具体分析一下。好了,我在这里就大概分析了ck5的conversion的转化逻辑,感兴趣的可以一起讨论。

 

CKEditor5——Utils(工具类理解)

更新于 2022.08.09 11分钟阅读 2 评论 5 推荐

    CKEditor5

    作者: 敲碎时间,铸造不朽
  1. CKEditor5事件系统(基础使用) Page 1
    1. CKEditor5事件系统(事件优先级) Page 12
    2. CKEditor5事件系统(代理事件) Page 25
    3. CKEditor5事件系统(视图事件冒泡) Page 32
    4. CKEditor5事件系统源码分析(一) Page 36
    5. CKEditor5事件系统源码分析(二) Page 54
    6. CKEditor5事件系统源码分析(三) Page 67
    7. CKeditor5事件系统命名空间分析 Page 73
  2. CKEditor5 模板绑定 Page 87
    1. CKEditor5 Observables Page 94
    2. CKEditor5 Observable——属性绑定 Page 100
    3. CKEditor5 Observable——绑定多个对象或属性 Page 108
    4. CKEditor5 Observable——装饰方法 Page 115
    5. CKEditor5 UI——UI组件 Page 127
    6. CKEditor5——视图添加 Page 137
  3. CKEditor5——模型理解(一) Page 144
    1. CKEditor5——模型理解(二:Node) Page 151
    2. CKEditor5——模型理解(三:Element Text) Page 160
    3. CKEditor5——Node,Element,NodeList源码分析 Page 163
    4. CKEditor5——模型理解(四:模型组成) Page 176
    5. CKEditor5——模型理解(五:Position, Range, Selection) Page 192
    6. CKEditor5——Position源码分析 Page 205
    7. CKEditor5——Position源码分析(二) Page 224
    8. CKEditor5——Position源码分析(三) Page 235
    9. CKEditor5——模型理解(六:Range) Page 247
    10. CKEditor5——模型理解(七:Selection) Page 261
    11. CKEditor5——模型理解(八:Operation和Batch) Page 272
    12. CKEditor5——(九:TreeWalker) Page 276
  4. CKEditor5——Conversion理解 Page 283
  5. CKEditor5——Utils(工具类理解) Page 300
    1. CKEditor5——Utils(Collection类理解) Page 312
    2. CKEditor5——Uitls(DomEmitterMixin类理解) Page 351
  6. CKEditor5——Plugin理解 Page 374
  7. CKEditor5——UI理解 Page 380
    1. CKEditor5——View理解 Page 393
    2. CKEditor5——Template理解(一) Page 403
    3. CKEditor5——Template理解(二) Page 423
    4. CKEditor5——Template理解(三) Page 460

如果对CK5的代码有所理解的话,大概知道,CK5有一个非常重要的工具包项目,这个工具包非常重要,提供了CK5最基础的一些功能。比如:集合类Collection、事件类EmitterMixin、观察者类ObservableMixin等。

今天我们暂缓学些以上的类,主要理解一个关于dom的类,那就是Position和Rect类,因为这两个类是CK5中弹出balloon工具条的基础类。我会一点点学习它的原理。

我们首先看看Rect类

Rect属性

这个类的属性很简单,主要有六个:

top、left、bottom、right、width、height。看到了吧,实际上这个类就是一个dom元素在文档中的位置信息和大小信息,只不过这里有一个关系要记住:

bottom = top + height;

right = left + width;

Rect方法

首先我们需要理解的就是构造器

constructor(source) 

这个构造器看似简单,实际上它接受的参数可以是多种类型,比如:

HTMLElement | Range | Window | ClientRect | DOMRect | Rect | Object

它可以接受基本dom元素,nativeRange,window对象,ClientRect对象,DomRect类型,它自身类型,甚至Object都可以。

这里需要注意的是:

默认情况下,HTML 元素的矩形包括其 CSS 边框和滚动条(如果有),窗口的矩形也包括滚动条。

在这种情况下,我们需要获得出去边框和滚动条的Rect,那就用到了下面的方法。

为了说明这个构造器的source的多样性,我还是用官网的代码来说明一下:

// Rect of an HTMLElement. 这里是常用dom元素
const rectA = new Rect( document.body );

// Rect of a DOM Range. 这里是选择区域的第一个Range
const rectB = new Rect( document.getSelection().getRangeAt( 0 ) );

// Rect of a window (web browser viewport). window对象
const rectC = new Rect( window );

// Rect out of an object.  实际上就是Object对象
const rectD = new Rect( { top: 0, right: 10, bottom: 10, left: 0, width: 10, height: 10 } );

// Rect out of another Rect instance. 
const rectE = new Rect( rectD );

// Rect out of a ClientRect. //这里是一个ClientRect对象
const rectF = new Rect( document.body.getClientRects().item( 0 ) );

excludeScrollbarsAndBorders() → Rect

调用这个方法就可以返回一个不包含边框和滚动条的Rect对象。

另外Rect这个类还包含一些比较有用的方法,简单说一下:

clone() : Rect

复制一个对象

contains(anotherRect) : Boolean

当前对象是否包含参数对象

getArea() : Number

返回Rect的面积

getIntersection(anotherRect) : Rect

 返回两个Rect的交集

getIntersectionArea(anotherRect) : Number

返回两个Rect的交集的面积

getVisible() : Rect | null

返回一个新的矩形,原始矩形的一部分,它实际上对用户可见,例如由父元素 rect 裁剪的原始 rect,其在 CSS 中设置了溢出而不是“可见”。如果没有这样的可见矩形,即当矩形被一个或多个祖先限制时,则返回 null。

isEqual(anotherRect) : Boolean

两个Rect是否相等

moveBy(x , y ) : Rect

将Rect移动一定的偏移量

moveTo(x , y) : Rect

移动Rect,使其左上角落在所需的 [ x, y ] 位置。

此外还有两个静态方法

getBoundingRect( rects ) → Rect | null

返回一个包含所有给定矩形的边界矩形

getDomRangeRects( range ) → Array.<Rect>

返回给定原生 DOM 范围的矩形数组。

不难看出,这个类实际上就是对dom中的矩形元素框的一个抽象,所以理解这些方法对于我们后续学习Position是很有帮助的。

 

Position属性

Position的基本属性也不错,主要有三个: name、top、left。这三个属性主要用于命名和定位。另外一个属性config,这个属性暂时不是很重要

 

Position方法

最重要的是构造器方法

constructor( [ positioningFunction ], [ options ] = { options.elementRect, options.targetRect, options.viewportRect, [options.positionedElementAncestor] } )

在构造器中,有两个参数,第一个是定位函数,另一个是可选的配置对象,我们先看看这个配置对象Options

Options属性

这个对象主要有六个属性:

element : Element

实际就是需要定位的元素

target : HTMLElement | Range | Window | ClientRect | DOMRect | Rect | Object | function

这个是定位元素相对的目标元素

positions : Array<positioningFunction>>

一组定位使用的函数

这三个元素值最重要的属性,另外三个属性可以查看官网的说明

这里再介绍一下定位函数

positioningFunction

定位函数有三个重要的参数

第一个是targetRect,第二个是elementRect,第三个是viewportRect

// This simple position will place the element directly under the target, in the middle:
//
//	    [ Target ]
//	+-----------------+
//	|     Element     |
//	+-----------------+
//
const position = ( targetRect, elementRect, [ viewportRect ] ) => ( {
	top: targetRect.bottom,
	left: targetRect.left + targetRect.width / 2 - elementRect.width / 2,
	name: 'bottomMiddle',

	// Note: The config is optional.
	config: {
		zIndex: '999'
	}
} );

这个函数会返回一个对象,对象包含以下属性

top、left、name、config。实际上就是定位需要的属性以及为定位命一个名字。

有了Position元素后,这个类中还有一个关键的方法:

getOptimalPosition()

这个方法会返回一个定位最优的位置来帮助我们的定位元素进行定位。有了这些功能,我们大概就能理解balloon工具条的原理啦。如果希望我分析源码的话,欢迎留言

 

getClientRects应用举例

上一节我们介绍了getClients的用法,今天我们学习一个简答的应用场景

需求:

我在一篇文章中选中一段文字,然后在文字的下方弹出一个简单的浮动按钮。

思路分析:

1、按钮需要浮动,首先我想到的是在文档中使用一个元素来作为按钮,这个元素我采用绝对定位,当我知道选择区域的大概位置的时候,根据位置修改元素的top和left值,就可以啦。

2、按钮元素最好首先将top和left的位置放置于文档的视口外部,这个我使用比较大的负值就可以啦。

3、如果知道我们选中区域的位置是我们本文的重点,也是本文的主题所在。

 

实现步骤代码:

<!doctype html>
<style>
	.comment {
		width: 35px;
		height:35px;
		background:#FFF;
		position:absolute;
		top: -500px;
		left:-500px;
	}
	button {
		width: 100%;
		height:100%;
	}
</style>
<body>
<p id="p">Select me: <i>italic</i> and <b>bold</b></p>

From <input id="from" disabled> – To <input id="to" disabled>
<script>

  document.onselectionchange = function(e) {
    let {anchorNode, anchorOffset, focusNode, focusOffset} = document.getSelection();
	let selection = document.getSelection();
	let range = selection.getRangeAt(0);
	const clientRects = range.getClientRects();
	let divElement = document.getElementById('comment');
	if (clientRects.length ==0) {
		divElement.style.top = '-500px';
	    divElement.style.left = '-500px';
		return;
	}
	divElement.style.top = (clientRects[0].top+20)+'px';
	divElement.style.left = clientRects[0].left+'px';
	
  };
</script>
<div id="comment" class="comment">
   <button>
	   <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
			<path d="M4 1.5h12A3.5 3.5 0 0 1 19.5 5v8l-.005.192a3.501 3.501 0 0 1-2.927 3.262l-.062.008v1.813a1.5 1.5 0 0 1-2.193 1.33l-.371-.193-.38-.212a13.452 13.452 0 0 1-3.271-2.63l-.062-.07H4A3.5 3.5 0 0 1 .5 13V5A3.5 3.5 0 0 1 4 1.5ZM4 3a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6.924a11.916 11.916 0 0 0 3.71 3.081l.372.194v-3.268L14.962 15H16a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H4Zm1.55 5a1.25 1.25 0 1 1 0 2.5 1.25 1.25 0 0 1 0-2.5Zm4.5 0a1.25 1.25 0 1 1 0 2.5 1.25 1.25 0 0 1 0-2.5Zm4.5 0a1.25 1.25 0 1 1 0 2.5 1.25 1.25 0 0 1 0-2.5Z"></path>
		</svg>
	</button>
</div>
</body>

思路分析:

1、在文档中放置了一个div元素,id和class设置为comment,将它们设置为绝对定位,同时将它们的位置放到视口以外。

2、我们监听document.onselectionchange事件,第一步是获取选择的区域,这个可以根据document.getSelection();

3、我们得到选择区域的范围(range),这里需要注意的是,这里的范围有的浏览器有一个值,有的浏览器有多个值,这里我只需要第一个值就可以啦

4、调用range的getClientRects方法,注意,这个方法返回一个数组,我只需要第一个数组的值就足够,因为我只拿第一个DomRect的top和left

5、如果这个range没有值,或者即使有值,但是选中的range的宽度为0 ,这些情况直接返回就可以啦

实现的效果如上图所示。欢迎讨论

 

 

CKeditor5事件系统命名空间分析

发布于 2022.07.25 14分钟阅读 0 评论 5 推荐

    CKEditor5

    作者: 敲碎时间,铸造不朽
  1. CKEditor5事件系统(基础使用) Page 1
    1. CKEditor5事件系统(事件优先级) Page 12
    2. CKEditor5事件系统(代理事件) Page 25
    3. CKEditor5事件系统(视图事件冒泡) Page 32
    4. CKEditor5事件系统源码分析(一) Page 36
    5. CKEditor5事件系统源码分析(二) Page 54
    6. CKEditor5事件系统源码分析(三) Page 67
    7. CKeditor5事件系统命名空间分析 Page 73
  2. CKEditor5 模板绑定 Page 87
    1. CKEditor5 Observables Page 94
    2. CKEditor5 Observable——属性绑定 Page 100
    3. CKEditor5 Observable——绑定多个对象或属性 Page 108
    4. CKEditor5 Observable——装饰方法 Page 115
    5. CKEditor5 UI——UI组件 Page 127
    6. CKEditor5——视图添加 Page 137
  3. CKEditor5——模型理解(一) Page 144
    1. CKEditor5——模型理解(二:Node) Page 151
    2. CKEditor5——模型理解(三:Element Text) Page 160
    3. CKEditor5——Node,Element,NodeList源码分析 Page 163
    4. CKEditor5——模型理解(四:模型组成) Page 176
    5. CKEditor5——模型理解(五:Position, Range, Selection) Page 192
    6. CKEditor5——Position源码分析 Page 205
    7. CKEditor5——Position源码分析(二) Page 224
    8. CKEditor5——Position源码分析(三) Page 235
    9. CKEditor5——模型理解(六:Range) Page 247
    10. CKEditor5——模型理解(七:Selection) Page 261
    11. CKEditor5——模型理解(八:Operation和Batch) Page 272
    12. CKEditor5——(九:TreeWalker) Page 276
  4. CKEditor5——Conversion理解 Page 283
  5. CKEditor5——Utils(工具类理解) Page 300
    1. CKEditor5——Utils(Collection类理解) Page 312
    2. CKEditor5——Uitls(DomEmitterMixin类理解) Page 351
  6. CKEditor5——Plugin理解 Page 374
  7. CKEditor5——UI理解 Page 380
    1. CKEditor5——View理解 Page 393
    2. CKEditor5——Template理解(一) Page 403
    3. CKEditor5——Template理解(二) Page 423
    4. CKEditor5——Template理解(三) Page 460

在上一节的事件系统源码中,我们留下了一个函数createEventNamespace( this, event )

今天我们来分析这个函数:

首先,我将这个函数摘取出来,放到一个utils.js文件中:

function getEvents( source ) {
	if ( !source._events ) {
		Object.defineProperty( source, '_events', {
			value: {}
		} );
	}

	return source._events;
}

function makeEventNode() {
	return {
		callbacks: [],
		childEvents: []
	};
}

function createEventNamespace( source, eventName ) {
	const events = getEvents( source );

	// First, check if the event we want to add to the structure already exists.
	if ( events[ eventName ] ) {
		// If it exists, we don't have to do anything.
		return;
	}

	// In other case, we have to create the structure for the event.
	// Note, that we might need to create intermediate events too.
	// I.e. if foo:bar:abc is being registered and we only have foo in the structure,
	// we need to also register foo:bar.

	// Currently processed event name.
	let name = eventName;
	// Name of the event that is a child event for currently processed event.
	let childEventName = null;

	// Array containing all newly created specific events.
	const newEventNodes = [];

	// While loop can't check for ':' index because we have to handle generic events too.
	// In each loop, we truncate event name, going from the most specific name to the generic one.
	// I.e. foo:bar:abc -> foo:bar -> foo.
	while ( name !== '' ) {
		if ( events[ name ] ) {
			// If the currently processed event name is already registered, we can be sure
			// that it already has all the structure created, so we can break the loop here
			// as no more events need to be registered.
			break;
		}

		// If this event is not yet registered, create a new object for it.
		events[ name ] = makeEventNode();
		// Add it to the array with newly created events.
		newEventNodes.push( events[ name ] );

		// Add previously processed event name as a child of this event.
		if ( childEventName ) {
			events[ name ].childEvents.push( childEventName );
		}

		childEventName = name;
		// If `.lastIndexOf()` returns -1, `.substr()` will return '' which will break the loop.
		name = name.substr( 0, name.lastIndexOf( ':' ) );
	}

	if ( name !== '' ) {
		// If name is not empty, we found an already registered event that was a parent of the
		// event we wanted to register.

		// Copy that event's callbacks to newly registered events.
		for ( const node of newEventNodes ) {
			node.callbacks = events[ name ].callbacks.slice();
		}

		// Add last newly created event to the already registered event.
		events[ name ].childEvents.push( childEventName );
	}
    return events;
}

module.exports = {
    createEventNamespace
}

为了理解这个方法的执行,我写了一个测试类utils.test.js

import {createEventNamespace } from '../src/utils';

test('createEventNamespace', ()=>{
    const events = createEventNamespace({},'foo:bar:des');
    expect(events.foo).toEqual({callbacks: [], childEvents: [ 'foo:bar'] });
    expect(events.foo.childEvents).toEqual([ 'foo:bar'])
    expect(events['foo:bar']).toEqual({ callbacks: [], childEvents: [ 'foo:bar:des' ] });
    expect(events['foo:bar'].childEvents).toEqual([ 'foo:bar:des' ])
    expect(events['foo:bar:des']).toEqual({ callbacks: [], childEvents: [] }); 
    expect(events['foo:bar:des'].childEvents).toEqual([])
});
test('events keys', ()=>{
    const events = createEventNamespace({},'foo:bar:des');
    const keys = Object.keys(events);
    console.log('keys:',keys)
    expect(keys).toEqual(['foo:bar:des','foo:bar','foo']);
});

各位注意了:

1、在createEventNamespace方法中,首先获取当前emitter对应的事件对象,如果在事件对象上已经存在对应的事件,那么直接返回。

2、然后看看事件是否包含命名空间的分隔符 :注意,这里在创建的时候,会创建一个数据结构:

{
	callbacks: [],
	childEvents: []
};

也就是每个事件的值对象是一个包含callbacks和childEvents两个属性的结构。

3、然后没迭代一次,分割符就会少一个,具体情况是这样的,foo:bar:des,第一步创建key为foo:bar:des的结构,然后是foo:bar,最后是foo

4、最后是设置childEvents属性,还是用第三点的例子来说,foo:bar:des这个key对象的childEvents属性为空组数,而foo:barkey对应的childEvents属性就是foo:bar:des,而fookey对象的childEvents对应的属性则是foo:bar

5、最终产生的数据结构

events: {
    'foo:bar:des': { callbacks: [], childEvents: [] },
    'foo:bar': { callbacks: [], childEvents: [ 'foo:bar:des' ] },
     foo: { callbacks: [], childEvents: [ 'foo:bar' ] }
}

好了,大家应该理解了这个函数的作用了吧

 

CKEditor5 Observable——属性绑定

更新于 2022.07.21 8分钟阅读 0 评论 5 推荐

    CKEditor5

    作者: 敲碎时间,铸造不朽
  1. CKEditor5事件系统(基础使用) Page 1
    1. CKEditor5事件系统(事件优先级) Page 12
    2. CKEditor5事件系统(代理事件) Page 25
    3. CKEditor5事件系统(视图事件冒泡) Page 32
    4. CKEditor5事件系统源码分析(一) Page 36
    5. CKEditor5事件系统源码分析(二) Page 54
    6. CKEditor5事件系统源码分析(三) Page 67
    7. CKeditor5事件系统命名空间分析 Page 73
  2. CKEditor5 模板绑定 Page 87
    1. CKEditor5 Observables Page 94
    2. CKEditor5 Observable——属性绑定 Page 100
    3. CKEditor5 Observable——绑定多个对象或属性 Page 108
    4. CKEditor5 Observable——装饰方法 Page 115
    5. CKEditor5 UI——UI组件 Page 127
    6. CKEditor5——视图添加 Page 137
  3. CKEditor5——模型理解(一) Page 144
    1. CKEditor5——模型理解(二:Node) Page 151
    2. CKEditor5——模型理解(三:Element Text) Page 160
    3. CKEditor5——Node,Element,NodeList源码分析 Page 163
    4. CKEditor5——模型理解(四:模型组成) Page 176
    5. CKEditor5——模型理解(五:Position, Range, Selection) Page 192
    6. CKEditor5——Position源码分析 Page 205
    7. CKEditor5——Position源码分析(二) Page 224
    8. CKEditor5——Position源码分析(三) Page 235
    9. CKEditor5——模型理解(六:Range) Page 247
    10. CKEditor5——模型理解(七:Selection) Page 261
    11. CKEditor5——模型理解(八:Operation和Batch) Page 272
    12. CKEditor5——(九:TreeWalker) Page 276
  4. CKEditor5——Conversion理解 Page 283
  5. CKEditor5——Utils(工具类理解) Page 300
    1. CKEditor5——Utils(Collection类理解) Page 312
    2. CKEditor5——Uitls(DomEmitterMixin类理解) Page 351
  6. CKEditor5——Plugin理解 Page 374
  7. CKEditor5——UI理解 Page 380
    1. CKEditor5——View理解 Page 393
    2. CKEditor5——Template理解(一) Page 403
    3. CKEditor5——Template理解(二) Page 423
    4. CKEditor5——Template理解(三) Page 460

前面我们知道了,在CK5中怎么样将一个对象设置成Observable以及Observable在UI中如何使用?

属性绑定

今天我们来看看如何进行可观测对象的属性绑定和重命名。

首先,我们假定有两个Observable对象,所谓绑定就是将一个对象的可观测状态绑定到另一个可观测对象,如下所示:

const button = new Button();
const command = editor.commands.get( 'bold' );//1

button.bind( 'isEnabled' ).to( command );//2

分析以上代码,我们知道:

  1. 1处的command是Observable对象,2处的button也是Observable对象。
     
  2. button将自己的isEnable这个可观测属性绑定到了command上。button.isEnabled===command.isEnabled
     
  3. 当command的isEnabled属性改变时,button的isEnabled属性也会改变。
     
  4. 如果button的class属性也是绑定到isEnabled,这时,button的dom元素也会更新,因为class属性也会改变。

通过以上四个步骤,就达到了通过command刷新button的目的。

小提示:将一个对象的属性设置成可观测属性,set()方法是唯一的方法。

 

重命名

仔细观察上面的代码2处,其实to方法还可以写成如下:

button.bind( 'isEnabled' ).to( command, 'isEnabled' );

所以to方法有第二个参数。它有什么作用呢?请看如下代码:

button.bind( 'isOn' ).to( command, 'value' );

这个时候,command的value属性改变的时候,button的isOn属性同样会反映出这种变化。

注意,这里是to后面的Observable属性改变然后bind方法前面的Observable属性反应这种变化。这里提出一个问题,如果bind前面的Observable属性改变,to后面的Observable属性会发生变化吗?我们试试:

 

定义两个Observable对象

import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
import mix from '@ckeditor/ckeditor5-utils/src/mix';

export default class SomeClass {
  constructor(){
      this.set('value',false);
  }
}

mix( SomeClass, ObservableMixin );

 

import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
import mix from '@ckeditor/ckeditor5-utils/src/mix';

export default class AnyClass {
  constructor(){
      this.set('isOn');
  }
}

mix( AnyClass, ObservableMixin );

SomeClass类定义了一个可观测对象value,而AnyClass类定义了一个可观测对象isOn;

下面是验证这种绑定是单向的还是双向的:

const someClass = new SomeClass();
const anyClass = new AnyClass();

someClass.bind('value').to(anyClass,'isOn');

anyClass.isOn = true;
console.log('someClass.value',someClass.value);

//反过来:
someClass.value = false;
console.log('anyClass.isOn',anyClass.isOn);

anyClass.isOn = false;
console.log('someClass.value',someClass.value);

从打印出来的日志可以看出,这种绑定是单向的,只有to后面的Observable对象的属性变化时,bind前的Observable对象会反应这种变化。而相反的情况bind前的Observable对象发生改变时,to后面的Observable对象不会反应这种变化。

 

处理属性值

某些情况下,还可以处理属性值:

const command = editor.commands.get( 'heading' );
button.bind( 'isOn' ).to( command, 'value', value => value === 'heading1' );

上面的例子意思就是,当command的value是不是heading1时,那么button的isOn属性就是false,按钮的dom也会有相应的反映。

 

下一节我们介绍绑定多个属性以及绑定多个Observable对象。

CKEditor5 模板绑定

更新于 2022.07.21 7分钟阅读 0 评论 5 推荐

    CKEditor5

    作者: 敲碎时间,铸造不朽
  1. CKEditor5事件系统(基础使用) Page 1
    1. CKEditor5事件系统(事件优先级) Page 12
    2. CKEditor5事件系统(代理事件) Page 25
    3. CKEditor5事件系统(视图事件冒泡) Page 32
    4. CKEditor5事件系统源码分析(一) Page 36
    5. CKEditor5事件系统源码分析(二) Page 54
    6. CKEditor5事件系统源码分析(三) Page 67
    7. CKeditor5事件系统命名空间分析 Page 73
  2. CKEditor5 模板绑定 Page 87
    1. CKEditor5 Observables Page 94
    2. CKEditor5 Observable——属性绑定 Page 100
    3. CKEditor5 Observable——绑定多个对象或属性 Page 108
    4. CKEditor5 Observable——装饰方法 Page 115
    5. CKEditor5 UI——UI组件 Page 127
    6. CKEditor5——视图添加 Page 137
  3. CKEditor5——模型理解(一) Page 144
    1. CKEditor5——模型理解(二:Node) Page 151
    2. CKEditor5——模型理解(三:Element Text) Page 160
    3. CKEditor5——Node,Element,NodeList源码分析 Page 163
    4. CKEditor5——模型理解(四:模型组成) Page 176
    5. CKEditor5——模型理解(五:Position, Range, Selection) Page 192
    6. CKEditor5——Position源码分析 Page 205
    7. CKEditor5——Position源码分析(二) Page 224
    8. CKEditor5——Position源码分析(三) Page 235
    9. CKEditor5——模型理解(六:Range) Page 247
    10. CKEditor5——模型理解(七:Selection) Page 261
    11. CKEditor5——模型理解(八:Operation和Batch) Page 272
    12. CKEditor5——(九:TreeWalker) Page 276
  4. CKEditor5——Conversion理解 Page 283
  5. CKEditor5——Utils(工具类理解) Page 300
    1. CKEditor5——Utils(Collection类理解) Page 312
    2. CKEditor5——Uitls(DomEmitterMixin类理解) Page 351
  6. CKEditor5——Plugin理解 Page 374
  7. CKEditor5——UI理解 Page 380
    1. CKEditor5——View理解 Page 393
    2. CKEditor5——Template理解(一) Page 403
    3. CKEditor5——Template理解(二) Page 423
    4. CKEditor5——Template理解(三) Page 460

CK5还可以将模板的属性绑定到可观测对象属性,如下代码所示:

import {View} from 'ckeditor5/src/ui';
export default class Button extends View {
    constructor(){
        super();
        this.type = 'button';

        const bind = this.bindTemplate;

        // this.label is observable but undefined.
        this.set( 'label' );

        // this.isOn is observable and false.
        this.set( 'isOn', false );

        // this.isEnabled is observable and true.
        this.set( 'isEnabled', true );

        this.setTemplate( {
            tag: 'button',
            attributes: {
                class: [
                // The 'ck-on' and 'ck-off' classes toggle according to the #isOn property.
                    bind.to( 'isOn', value => value ? 'ck-on' : 'ck-off' ),

                // The 'ck-enabled' class appears when the #isEnabled property is false.
                    bind.if( 'isEnabled', 'ck-disabled', value => !value )
                ],
                type: this.type
            },
            children: [
                {
                    // The text of the button is bound to the #label property.
                    text: bind.to( 'label' )
                }
            ]
        } );
    }
}

这里的button对象有三个可观测属性,使用bind方法将isOn和isEnabled绑定到class属性;同时将label绑定到text属性。

接下来是使用button的代码

const button = new Button();
// Render the button to create its #element.
button.render();
button.label = 'Bold';     // <button class="ck-off" type="button">Bold</button>
button.isOn = true;        // <button class="ck-on" type="button">Bold</button>
button.label = 'B';        // <button class="ck-on" type="button">B</button>
button.isOff = false;      // <button class="ck-off" type="button">B</button>
button.isEnabled = false;  // <button class="ck-off ck-disabled" type="button">B</button>
document.body.appendChild( button.element );

下面我们看看全部执行会产生一个什么样的button:

从以上生成的按钮和代码可以看出当改变button的属性时,对应的class属性值和text的值也发生了变化。

 

下一节我们学习怎么对绑定的属性进行传播和共享。

CKEditor5事件系统(视图事件冒泡)

更新于 2022.07.19 1分钟阅读 0 评论 5 推荐

    CKEditor5

    作者: 敲碎时间,铸造不朽
  1. CKEditor5事件系统(基础使用) Page 1
    1. CKEditor5事件系统(事件优先级) Page 12
    2. CKEditor5事件系统(代理事件) Page 25
    3. CKEditor5事件系统(视图事件冒泡) Page 32
    4. CKEditor5事件系统源码分析(一) Page 36
    5. CKEditor5事件系统源码分析(二) Page 54
    6. CKEditor5事件系统源码分析(三) Page 67
    7. CKeditor5事件系统命名空间分析 Page 73
  2. CKEditor5 模板绑定 Page 87
    1. CKEditor5 Observables Page 94
    2. CKEditor5 Observable——属性绑定 Page 100
    3. CKEditor5 Observable——绑定多个对象或属性 Page 108
    4. CKEditor5 Observable——装饰方法 Page 115
    5. CKEditor5 UI——UI组件 Page 127
    6. CKEditor5——视图添加 Page 137
  3. CKEditor5——模型理解(一) Page 144
    1. CKEditor5——模型理解(二:Node) Page 151
    2. CKEditor5——模型理解(三:Element Text) Page 160
    3. CKEditor5——Node,Element,NodeList源码分析 Page 163
    4. CKEditor5——模型理解(四:模型组成) Page 176
    5. CKEditor5——模型理解(五:Position, Range, Selection) Page 192
    6. CKEditor5——Position源码分析 Page 205
    7. CKEditor5——Position源码分析(二) Page 224
    8. CKEditor5——Position源码分析(三) Page 235
    9. CKEditor5——模型理解(六:Range) Page 247
    10. CKEditor5——模型理解(七:Selection) Page 261
    11. CKEditor5——模型理解(八:Operation和Batch) Page 272
    12. CKEditor5——(九:TreeWalker) Page 276
  4. CKEditor5——Conversion理解 Page 283
  5. CKEditor5——Utils(工具类理解) Page 300
    1. CKEditor5——Utils(Collection类理解) Page 312
    2. CKEditor5——Uitls(DomEmitterMixin类理解) Page 351
  6. CKEditor5——Plugin理解 Page 374
  7. CKEditor5——UI理解 Page 380
    1. CKEditor5——View理解 Page 393
    2. CKEditor5——Template理解(一) Page 403
    3. CKEditor5——Template理解(二) Page 423
    4. CKEditor5——Template理解(三) Page 460

CK5的视图(view.document)不仅是一个Observable和emitter,而且还实现了一个BubblingEmitter,它是由BubblingEmitterMixin实现的。它提供了在虚拟dom机制上的冒泡事件。

 

它与普通的dom树上的冒泡机制不同。它不会在特定的元素上注册监听器,而是在指定的上下文上注册监听器。

这里的上下文,要么是一个元素,要么是虚拟上下文之一,要么是匹配节点的回调函数。

 

注册在视图元素上的监听器

this.listenTo( view.document, 'enter', ( evt, data ) => {
   // ...
}, { context: 'blockquote' } );
this.listenTo( view.document, 'enter', ( evt, data ) => {
   // ...
}, { context: 'li' } );

这里的监听器注册在blockquote和li元素上。

注册在虚拟上下文中的监听器

this.listenTo( view.document, 'arrowKey', ( evt, data ) => {
   // ...
}, { context: '$text', priority: 'high' } );
this.listenTo( view.document, 'arrowKey', ( evt, data ) => {
   // ...
}, { context: '$root' } );
this.listenTo( view.document, 'arrowKey', ( evt, data ) => {
   // ...
}, { context: '$capture' } );

这里的监听器注册在$text, $root和$capture上。

注册在自定义回调函数的上下文上的监听器

import { isWidget } from '@ckeditor/ckeditor5-widget/src/utils';
this.listenTo( view.document, 'arrowKey', ( evt, data ) => {
   // ...
}, { context: isWidget } );
this.listenTo( view.document, 'arrowKey', ( evt, data ) => {
   // ...
}, { context: isWidget, priority: 'high' } );

这里的监听器注册在isWidget上。

 

这里我们只需要知道有这三种情况就好,以后我们会用例子来说明事件的传播顺序。

 

CKEditor5事件系统(代理事件)

更新于 2022.07.19 6分钟阅读 0 评论 5 推荐

    CKEditor5

    作者: 敲碎时间,铸造不朽
  1. CKEditor5事件系统(基础使用) Page 1
    1. CKEditor5事件系统(事件优先级) Page 12
    2. CKEditor5事件系统(代理事件) Page 25
    3. CKEditor5事件系统(视图事件冒泡) Page 32
    4. CKEditor5事件系统源码分析(一) Page 36
    5. CKEditor5事件系统源码分析(二) Page 54
    6. CKEditor5事件系统源码分析(三) Page 67
    7. CKeditor5事件系统命名空间分析 Page 73
  2. CKEditor5 模板绑定 Page 87
    1. CKEditor5 Observables Page 94
    2. CKEditor5 Observable——属性绑定 Page 100
    3. CKEditor5 Observable——绑定多个对象或属性 Page 108
    4. CKEditor5 Observable——装饰方法 Page 115
    5. CKEditor5 UI——UI组件 Page 127
    6. CKEditor5——视图添加 Page 137
  3. CKEditor5——模型理解(一) Page 144
    1. CKEditor5——模型理解(二:Node) Page 151
    2. CKEditor5——模型理解(三:Element Text) Page 160
    3. CKEditor5——Node,Element,NodeList源码分析 Page 163
    4. CKEditor5——模型理解(四:模型组成) Page 176
    5. CKEditor5——模型理解(五:Position, Range, Selection) Page 192
    6. CKEditor5——Position源码分析 Page 205
    7. CKEditor5——Position源码分析(二) Page 224
    8. CKEditor5——Position源码分析(三) Page 235
    9. CKEditor5——模型理解(六:Range) Page 247
    10. CKEditor5——模型理解(七:Selection) Page 261
    11. CKEditor5——模型理解(八:Operation和Batch) Page 272
    12. CKEditor5——(九:TreeWalker) Page 276
  4. CKEditor5——Conversion理解 Page 283
  5. CKEditor5——Utils(工具类理解) Page 300
    1. CKEditor5——Utils(Collection类理解) Page 312
    2. CKEditor5——Uitls(DomEmitterMixin类理解) Page 351
  6. CKEditor5——Plugin理解 Page 374
  7. CKEditor5——UI理解 Page 380
    1. CKEditor5——View理解 Page 393
    2. CKEditor5——Template理解(一) Page 403
    3. CKEditor5——Template理解(二) Page 423
    4. CKEditor5——Template理解(三) Page 460

emitter接口提供了事件代理机制。也就是说指定选择的事件能够被其他的emitter触发。 

1、代理指定的事件到另一个emitter

let anyClass = new AnyClass();
let anotherClass = new AnyClass();
let oneClass = new AnyClass();
anotherClass.on('bar',(evt,data)=>{
    console.log(evt.source);
    console.log('data',data);
    console.log('bar');
});


oneClass.on('one',(evt,data)=>{
    console.log(evt.source);
    console.log('data',data);
    console.log('one');
});


anyClass.delegate('foo').to(anotherClass,'bar');
anyClass.delegate('foo').to(oneClass,'one');
//anotherClass.fire('bar', 1 );
anyClass.fire('foo', 1 );

以上代码打印出来的日志信息如下:

由此可以看出anyClass将foo事件代理到了anotherClass和oneClass。同时还给被代理的事件名称改了个名字。

这里需要注意的是:delegate A to B 这里的B才是被代理的emitter,可以看看一下代码:

let anyClass = new AnyClass();
let anotherClass = new AnyClass();
           
anyClass.on('foo',(evt,data)=>{
    console.log(evt.source);
    console.log('data',data);
    console.log('bar');
});
anyClass.delegate('foo').to(anotherClass);           
anotherClass.fire('foo', 1 );

这里我们按照字面意思理解,

定义两个emitter anyClass和anotherClass

anyClass 绑定foo事件

anyClass的foo事件代理到anotherClass这个emitter

anotherClass触发foo事件

然后anyClass应该会执行回调函数。实际上回调函数不会执行,因为anotherClass才是被代理的emitter,只有被代理的emitter上绑定了函数,才会被代理给其他emitter,而被代理emitter回调函数执行需要有其他emitter触发。

emitterA.delegate( 'foo' ).to( emitterB );
emitterA.delegate( 'foo', 'bar' ).to( emitterC );

 

2、代理到不同名称的事件

emitterA.delegate( 'foo' ).to( emitterB, 'bar' );
emitterA.delegate( 'foo' ).to( emitterB, name => `delegated:${ name }` );

3、代理所有事件

emitterA.delegate( '*' ).to( emitterB );

4、取消代理事件

// Stop delegating all events.
emitterA.stopDelegating();
// Stop delegating a specific event to all emitters.
emitterA.stopDelegating( 'foo' );
// Stop delegating a specific event to a specific emitter.
emitterA.stopDelegating( 'foo', emitterB );

最后一点知识就是代理事件信息,它可以获得事件的名称,事件的来源以及事件的传播路径等。具体可以查看官网。

 

 

CKEditor5 Observables

更新于 2022.04.15 6分钟阅读 0 评论 5 推荐

    CKEditor5

    作者: 敲碎时间,铸造不朽
  1. CKEditor5事件系统(基础使用) Page 1
    1. CKEditor5事件系统(事件优先级) Page 12
    2. CKEditor5事件系统(代理事件) Page 25
    3. CKEditor5事件系统(视图事件冒泡) Page 32
    4. CKEditor5事件系统源码分析(一) Page 36
    5. CKEditor5事件系统源码分析(二) Page 54
    6. CKEditor5事件系统源码分析(三) Page 67
    7. CKeditor5事件系统命名空间分析 Page 73
  2. CKEditor5 模板绑定 Page 87
    1. CKEditor5 Observables Page 94
    2. CKEditor5 Observable——属性绑定 Page 100
    3. CKEditor5 Observable——绑定多个对象或属性 Page 108
    4. CKEditor5 Observable——装饰方法 Page 115
    5. CKEditor5 UI——UI组件 Page 127
    6. CKEditor5——视图添加 Page 137
  3. CKEditor5——模型理解(一) Page 144
    1. CKEditor5——模型理解(二:Node) Page 151
    2. CKEditor5——模型理解(三:Element Text) Page 160
    3. CKEditor5——Node,Element,NodeList源码分析 Page 163
    4. CKEditor5——模型理解(四:模型组成) Page 176
    5. CKEditor5——模型理解(五:Position, Range, Selection) Page 192
    6. CKEditor5——Position源码分析 Page 205
    7. CKEditor5——Position源码分析(二) Page 224
    8. CKEditor5——Position源码分析(三) Page 235
    9. CKEditor5——模型理解(六:Range) Page 247
    10. CKEditor5——模型理解(七:Selection) Page 261
    11. CKEditor5——模型理解(八:Operation和Batch) Page 272
    12. CKEditor5——(九:TreeWalker) Page 276
  4. CKEditor5——Conversion理解 Page 283
  5. CKEditor5——Utils(工具类理解) Page 300
    1. CKEditor5——Utils(Collection类理解) Page 312
    2. CKEditor5——Uitls(DomEmitterMixin类理解) Page 351
  6. CKEditor5——Plugin理解 Page 374
  7. CKEditor5——UI理解 Page 380
    1. CKEditor5——View理解 Page 393
    2. CKEditor5——Template理解(一) Page 403
    3. CKEditor5——Template理解(二) Page 423
    4. CKEditor5——Template理解(三) Page 460

在CKEditor5中除了事件系统外,还有另一个重要的系统就是可观测对象,俗称Observable对象。此对象的属性是可观测的,一段对象的属性发生改变,将会触发一个事件,监听此事件的代码片段可以做出一些相应的操作。

 

CK5定义一个可观测对象

import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
import mix from '@ckeditor/ckeditor5-utils/src/mix';

export default class SomeClass {
  constructor(){
      this.set('value',false);
  }
}

mix( SomeClass, ObservableMixin );

然后使用这个类的代码如下:

const someClass = new SomeClass();
someClass.on( 'change:value', ( evt, propertyName, newValue, oldValue ) => {
     console.log(
          `#${ propertyName } has changed from "${ oldValue }" to "${ newValue }"`
     );
 } );
 someClass.value = true;

打印出来的日志信息如下:

可以知道SomeClass对象的value属性从false变成了true,因此我们自己创建了一个自定义可观测对象。

 

可观测对象在UI中的使用

我们首先定义一个Button

import {View} from 'ckeditor5/src/ui';
export default class Button extends View {
    constructor(){
        super();
        this.type = 'button';


        const bind = this.bindTemplate;


        // this.label is observable but undefined.
        this.set( 'label' );


        // this.isOn is observable and false.
        this.set( 'isOn', false );


        // this.isEnabled is observable and true.
        this.set( 'isEnabled', true );
    }
}

然后使用这个Button

const view = new Button();

view.on( 'change:label', ( evt, propertyName, newValue, oldValue ) => {
    console.log(
        `#${ propertyName } has changed from "${ oldValue }" to "${ newValue }"`
    );
} )

view.label = 'Hello world!'; // -> #label has changed from "undefined" to "Hello world!"
view.label = 'Bold'; // -> #label has changed from "Hello world!" to "Bold"

view.type = 'submit';

从日志信息可以看出,button的属性label是可观测的,而type属性是不可观测的。

小知识:这里每次调用set方法来设置可观测属性是比较麻烦的,CK5提供了一下方法来简化使用:

this.set( {
   label: undefined,
   isOn: false,
   isEnabled: true
} );

 

CKEditor5——(九:TreeWalker)

更新于 2022.04.12 7分钟阅读 0 评论 5 推荐

    CKEditor5

    作者: 敲碎时间,铸造不朽
  1. CKEditor5事件系统(基础使用) Page 1
    1. CKEditor5事件系统(事件优先级) Page 12
    2. CKEditor5事件系统(代理事件) Page 25
    3. CKEditor5事件系统(视图事件冒泡) Page 32
    4. CKEditor5事件系统源码分析(一) Page 36
    5. CKEditor5事件系统源码分析(二) Page 54
    6. CKEditor5事件系统源码分析(三) Page 67
    7. CKeditor5事件系统命名空间分析 Page 73
  2. CKEditor5 模板绑定 Page 87
    1. CKEditor5 Observables Page 94
    2. CKEditor5 Observable——属性绑定 Page 100
    3. CKEditor5 Observable——绑定多个对象或属性 Page 108
    4. CKEditor5 Observable——装饰方法 Page 115
    5. CKEditor5 UI——UI组件 Page 127
    6. CKEditor5——视图添加 Page 137
  3. CKEditor5——模型理解(一) Page 144
    1. CKEditor5——模型理解(二:Node) Page 151
    2. CKEditor5——模型理解(三:Element Text) Page 160
    3. CKEditor5——Node,Element,NodeList源码分析 Page 163
    4. CKEditor5——模型理解(四:模型组成) Page 176
    5. CKEditor5——模型理解(五:Position, Range, Selection) Page 192
    6. CKEditor5——Position源码分析 Page 205
    7. CKEditor5——Position源码分析(二) Page 224
    8. CKEditor5——Position源码分析(三) Page 235
    9. CKEditor5——模型理解(六:Range) Page 247
    10. CKEditor5——模型理解(七:Selection) Page 261
    11. CKEditor5——模型理解(八:Operation和Batch) Page 272
    12. CKEditor5——(九:TreeWalker) Page 276
  4. CKEditor5——Conversion理解 Page 283
  5. CKEditor5——Utils(工具类理解) Page 300
    1. CKEditor5——Utils(Collection类理解) Page 312
    2. CKEditor5——Uitls(DomEmitterMixin类理解) Page 351
  6. CKEditor5——Plugin理解 Page 374
  7. CKEditor5——UI理解 Page 380
    1. CKEditor5——View理解 Page 393
    2. CKEditor5——Template理解(一) Page 403
    3. CKEditor5——Template理解(二) Page 423
    4. CKEditor5——Template理解(三) Page 460

前面我们学习了模型的基础知识,今天我们来看看模型另一个特点:模型位置的迭代。也就是TeeeWalker这个类用来在模型的位置之间迭代访问模型的节点。

TreeWalker属性

boundaries : Range
迭代器边界

这个属性用于指定迭代器在文档的哪个范围内迭代访问文档的Item。当迭代器在边界的末端“向前”行走或在边界的起点“向后”行走时,返回 { done: true }。

direction : 'backward' | 'forward'

行走方向。默认“前进”。

ignoreElementEnd : Boolean

指示迭代器是否应忽略 elementEnd 标记的标志。如果选项为 true,walker 将不会返回起始位置的父节点。如果此选项为真,则每个元素将返回一次,而如果选项为假,则它们可能会返回两次:对于“elementStart”和“elementEnd”。

position : Position

迭代器位置。这始终是静态位置,即使初始位置是实时位置。如果未定义起始位置,则位置取决于方向。如果方向是“向前”,则位置从开头开始,当方向为“向后”时,位置从结尾开始。

shallow : Boolean

指示迭代器是否为输入元素的标志。如果迭代器是浅子节点,则任何被迭代节点的子节点都不会与 elementEnd 标签一起返回。

singleChracters : Boolean

指示是否应将具有相同属性的所有连续字符作为一个 TextProxy (true) 或一个一个 (false) 返回的标志。

TreeWalker方法

首先是构造器方法,这个方法其实就是将属性通过构造器传入TreeWalker对象

Symbol.iterator() → Iterable.<TreeWalkerValue>

这个方法其实是最奇怪的方法吧,没见过这样写法的。不要着急,参考可迭代协议迭代器协议,我们可以知道,如果一个对象是可迭代的,那么这个对象必须实现@@iterator 方法,这意味着对象(或者它原型链上的某个对象)必须有一个键为 @@iterator 的属性,可通过常量 Symbol.iterator 访问该属性:

这个属性的特点是:

[Symbol.iterator]

一个无参数的函数,其返回值为一个符合迭代器协议的对象。

 

所以我们理解了为啥TreeWalker有这个方法,其实就是实现可迭代协议。这个方法的具体实现如下

Symbol.iterator ]() {
	return this;
}

这个函数返回的是this,即这个对象本身。因为这个对象应该实现迭代器协议

实现迭代器协议就是需要有一个next方法

next() → TreeWalkerValue

所以这个方法不难理解,就是为了实现迭代器协议

skip(skip)

只要回调函数返回 true,就在跳过值的方向上移动位置。其实就是有些位置的值不处理,直接跳过。

我们再来看看TreeWalkerValue

这个对象就是每次迭代的时候处理并返回的对象,它有五个属性

item : Item

TreeWalker 新旧位置之间的项目。

length : Number

项目的长度。对于'elementStart',它是1。对于'text',它是文本的长度。对于“elementEnd”,它是未定义的 

nextPosition : Position 

迭代器的下一个位置。

前向迭代:对于“elementStart”,它是元素内的第一个位置。对于所有其他类型,它是项目之后的位置。

向后迭代:对于'elementEnd',它是元素内的最后一个位置。对于所有其他类型,它是项目之前的位置。

previousPosition : Position

迭代器的先前位置。

前向迭代:对于'elementEnd',它是元素内的最后一个位置。对于所有其他类型,它是项目之前的位置。

向后迭代:对于'elementStart',它是元素内的第一个位置。对于所有其他类型,它是项目之后的位置。

type : TreeWalkerValueType

'elementStart' | 'elementEnd' | 'text'

TreeWalker 执行的步骤的类型。可能的值:'elementStart' 如果 walker 在节点的开头,'elementEnd' 如果 walker 在节点的末尾,或者'text' 如果 walker 遍历文本。

 

好了,有了以上这个类之后,我们就可以遍历模型文档的节点,然后对节点做一些操作,比如属性操作等。

总结:

1、理解并学习了TreeWalker的迭代原理

2、知道如何通过迭代器操作模型文档

3、理解并学习了可迭代协议和迭代器协议

getClientRects(学习)

Element 接口的 getClientRects() 方法返回 DOMRect 对象的集合,这些对象指示客户端中每个 CSS 边框框的边界矩形。

大多数元素每个只有一个边框,但多行内联元素(例如多行 <span> 元素,默认情况下)在每一行周围都有一个边框。

调用语法:

let rectCollection = object.getClientRects();

返回值是 DOMRect 对象的集合,每个与元素关联的 CSS 边框框都有一个。每个 DOMRect 对象都包含只读的 left、top、right 和 bottom 属性,以像素为单位描述边框框,左上角相对于视口的左上角。对于带有标题的表格,即使标题位于表格的边框之外,也会包含标题。 当在除外部 <svg> 之外的 SVG 元素上调用时,生成的矩形相对于“视口”是元素的外部 <svg> 建立的视口(并且要清楚,矩形也由 external-<svg> 的 viewBox 变换,如果有的话)。

 

最初,Microsoft 打算使用此方法为每行文本返回一个 TextRectangle 对象。但是,CSSOM 工作草案指定它为每个边框框返回一个 DOMRect。对于内联元素,这两个定义是相同的。但是对于块元素,Mozilla 将只返回一个矩形。

 

计算矩形时会考虑视口区域(或任何其他可滚动元素)的滚动量。

返回的矩形不包括任何可能发生溢出的子元素的边界。

对于 HTML <area> 元素、自身不呈现任何内容的 SVG 元素、display:none 元素以及通常不直接呈现的任何元素,将返回一个空列表。

即使对于具有空边框框的 CSS 框,也会返回矩形。左、上、右和下坐标仍然有意义。

小数像素偏移是可能的。

这里的例子可以参考mdn官网的例子

这个例子列举了三种情况:

1、p标签里面有span,然后再不同元素上画rect的情况

2、ol里面有li元素,同样在不同元素上画rect的情况

3、table里面有caption,然后在不同元素画rect的情况

getBoundingClientRect(学习)

最近在学习CK5的时候,学习到了一个Rect的类,这个类主要提供盒子元素定位时候用到的一些值,比如top、left、right、bottom、width、height。而它的实现主要用到了两个方法,其中一个就是:Element.getBoundingClientRect()

Element.getBoundingClientRect() 方法返回一个 DOMRect 对象,该对象提供有关元素大小及其相对于视口的位置的信息。

注意:这个方法的返回值是记录元素大小,以及相对于视口的位置信息

调用语法如下:

domRect = element.getBoundingClientRect();

说明:返回值是一个 DOMRect 对象,它是包含整个元素的最小矩形,包括它的填充(padding)边框(border)宽度

left、top、right、bottom、x、y、width 和 height 属性描述了整个矩形的位置和大小(以像素为单位)。宽度和高度以外的属性相对于视口的左上角。该方法返回的 DOMRect 对象的 width 和 height 属性包括 padding 和 border-width,而不仅仅是content的宽度/高度。在标准盒子模型中,这将等于元素的宽度或高度属性 + 填充(padding) + 边框(border)宽度。但是如果为元素设置了 box-sizing:border-box ,这将直接等于它的宽度或高度。

返回值可以被认为是 getClientRects() 为元素返回的矩形的并集,即与元素关联的 CSS 边框。

空边框框被完全忽略。如果所有元素的边框框都是空的,则返回一个宽度和高度为零的矩形,其中顶部和左侧是第一个 CSS 框(按内容顺序)的边框框的左上角元素。

在计算边界矩形时,会考虑视口区域(或任何其他可滚动元素)的滚动量。这意味着每次滚动位置改变时,矩形的边界边缘(上、右、下、左)都会改变它们的值(因为它们的值是相对于视口而不是绝对的)。

如果您需要相对于文档左上角的边界矩形,只需将当前滚动位置添加到 top 和 left 属性(这些可以使用 window.scrollX 和 window.scrollY 获得)以获得边界矩形,即 独立于当前滚动位置。

我用一段代码来演示一下:

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>getBoundingClientRect</title>
    <style>
        div {
		  width: 400px;
		  height: 200px;
		  padding: 20px;
		  margin: 50px auto;
		  background: purple;
		}

    </style>
</head>
<body>
    <div>
	</div>
<script>
	let elem = document.querySelector('div');
	let rect = elem.getBoundingClientRect();
	for (var key in rect) {
	  if(typeof rect[key] !== 'function') {
		let para = document.createElement('p');
		para.textContent  = `${ key } : ${ rect[key] }`;
		document.body.appendChild(para);
	  }
	}
</script>
</body>
</html>

演示的结果显示出了值宽度和高度不会改变,但是如果我改变窗口的大小,那些定位属性的值会发生变化。不过这些值都是相对于视口定位的。并且宽度是加上了padding的大小

 

我在演示一下有滚动的情况:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>getBoundingClientRect</title>
    <style>
        div#example {
		  width: 400px;
		  height: 200px;
		  padding: 20px;
		  margin: 50px auto;
		  background: purple;
		}

		body { padding-bottom: 1000px; }
		p { margin: 0; }


    </style>
</head>
<body>
    <div id="example"></div>
    <div id="controls"></div>
<script>
	function update() {
	  const container = document.getElementById("controls");
	  const elem = document.getElementById("example");
	  const rect = elem.getBoundingClientRect();

	  container.innerHTML = '';
	  for (let key in rect) {
		if(typeof rect[key] !== 'function') {
		  let para = document.createElement('p');
		  para.textContent  = `${ key } : ${ rect[key] }`;
		  container.appendChild(para);
		}
	  }
	}

document.addEventListener('scroll', update);
update();

</script>
</body>
</html>

通过以上的演示代码,可以看到,当我滚动窗口的时候,定位元素会发生变化,这也容易理解,因为是相对于视口定位嘛

CKEditor5——模型理解(一)

更新于 2022.07.29 7分钟阅读 1 评论 5 推荐

    CKEditor5

    作者: 敲碎时间,铸造不朽
  1. CKEditor5事件系统(基础使用) Page 1
    1. CKEditor5事件系统(事件优先级) Page 12
    2. CKEditor5事件系统(代理事件) Page 25
    3. CKEditor5事件系统(视图事件冒泡) Page 32
    4. CKEditor5事件系统源码分析(一) Page 36
    5. CKEditor5事件系统源码分析(二) Page 54
    6. CKEditor5事件系统源码分析(三) Page 67
    7. CKeditor5事件系统命名空间分析 Page 73
  2. CKEditor5 模板绑定 Page 87
    1. CKEditor5 Observables Page 94
    2. CKEditor5 Observable——属性绑定 Page 100
    3. CKEditor5 Observable——绑定多个对象或属性 Page 108
    4. CKEditor5 Observable——装饰方法 Page 115
    5. CKEditor5 UI——UI组件 Page 127
    6. CKEditor5——视图添加 Page 137
  3. CKEditor5——模型理解(一) Page 144
    1. CKEditor5——模型理解(二:Node) Page 151
    2. CKEditor5——模型理解(三:Element Text) Page 160
    3. CKEditor5——Node,Element,NodeList源码分析 Page 163
    4. CKEditor5——模型理解(四:模型组成) Page 176
    5. CKEditor5——模型理解(五:Position, Range, Selection) Page 192
    6. CKEditor5——Position源码分析 Page 205
    7. CKEditor5——Position源码分析(二) Page 224
    8. CKEditor5——Position源码分析(三) Page 235
    9. CKEditor5——模型理解(六:Range) Page 247
    10. CKEditor5——模型理解(七:Selection) Page 261
    11. CKEditor5——模型理解(八:Operation和Batch) Page 272
    12. CKEditor5——(九:TreeWalker) Page 276
  4. CKEditor5——Conversion理解 Page 283
  5. CKEditor5——Utils(工具类理解) Page 300
    1. CKEditor5——Utils(Collection类理解) Page 312
    2. CKEditor5——Uitls(DomEmitterMixin类理解) Page 351
  6. CKEditor5——Plugin理解 Page 374
  7. CKEditor5——UI理解 Page 380
    1. CKEditor5——View理解 Page 393
    2. CKEditor5——Template理解(一) Page 403
    3. CKEditor5——Template理解(二) Page 423
    4. CKEditor5——Template理解(三) Page 460

我们知道,CK5实现了一个MVC的架构,从今天开始,我们一步一步深入学习模型,视图,以及模型和视图之间的转换。今天我们开始模型的学习。

首先,我们看模型的定义:

The model is implemented by a DOM-like tree structure of elements and text nodes.

模型由两类节点构成,分别是元素节点文本节点,模型是一种类Dom树结构。我们知道,在DOM中元素节点可以包含属性,文本节点不能包含属性。但是在CK5中,不仅元素节点可以包含属性,文本节点也可以包含属性。

获取模型的方法

editor.model

有了模型,我们需要知道

模型在什么地方,以及模型如何操作?

模型存在编辑器editor的属性,模型也包含一个模型文档,这个文档包含一个根元素,具体代码如下:

editor.model.document;              // -> The document.
editor.model.document.getRoot();    // -> The document's root.

这里需要解释的一点就是:模型的根元素可能存在多个。

留个悬念,为什么可能存在多个根元素呢?

模型文档还包含一个选择属性,这里的选择就是你选中了模型的哪些节点。同时还包含一个对模型操作的历史记录,这个历史记录放置在模型对应的文档对象上。

editor.model.document.selection;    // -> The document's selection.
editor.model.schema;                // -> The model's schema.
editor.model.document.history;      // -> 模型历史操作记录

注意,模型还有一个schema属性,这个属性我们后续会专门分析,现在只需要知道就可以啦。一般我们要操作模型一定是在某个根元素下进行操作。我们的重点是掌握模型操作的方法。

模型操作有哪些操作类别呢?

这里我们指出四类:

1、文档结构的改变

2、文档选择的改变

3、文档元素的创建,修改,删除

4、文档元素属性的添加,修改,删除

如果读者认为还有其他的话,欢迎添加评论。

操作模型

模型的操作需要使用一个类:model writer,具体用法如下:

editor.model.change( writer => {
    writer.insertText( 'foo', editor.model.document.selection.getFirstPosition() );
} );

在文档选择的位置(这里一般是光标的位置,也有特殊情况)插入一个文本节点。

下面我们尝试一些操作模型的方法

1、创建段落并插入文本节点

//在根节点插入一个paragraph

//获取文档的根元素
const root = this.editor.model.document.getRoot();
//创建一个带属性的paragraph元素
const newParagraph = writer.createElement( 'paragraph', { alignment: 'center' } );
//添加带属性的文本到paragraph元素
writer.appendText('我的测试文档添加', { bold: true } ,newParagraph);
//将段落添加到跟节点
writer.append(newParagraph,root);

如上图所示,最后红框部分就是我创建的模型

注意,在这里我们还可以在光标插入的地方操作节点,代码如下:


const newParagraph = writer.createElement( 'paragraph', { alignment: 'center' } );
writer.appendText('我的测试文档添加', { bold: true } ,newParagraph);
writer.insert(newParagraph, selection.getFirstPosition() )

这里粘贴一下模型操作的相关API文档

https://ckeditor.com/docs/ckeditor5/latest/api/module_engine_model_writer-Writer.html

我们后续的操作都是以这个文档为准,然后参考一些具体的案例来进行学习。

总结

1、模型文档和DOM文档有什么区别和联系?(都是树结构,模型节点都可以有属性,DOM文本节点没有属性)

2、模型的操作有几类,分别是什么?

3、如何操作模型?(一定使用model writer)

CKEditor5——模型理解(八:Operation和Batch)