ckeditor5/engine封装了model tree的几个概念。
数据类型
以下列出了十几个主要数据类型,其中Node、NodeList、Element、RootElement、Text、TextProxy和DocumentFragment具有类似的结构:
Node:Model node. Most basic structure of model tree.This is an abstract class that is a base for other classes representing different nodes in model.其
offsetSize为1.NodeList:Provides an interface to operate on a list of nodes.
NodeListis used internally in classes like Element or DocumentFragment.Element:继承
Node,Model element. Type of node that has a name and child nodes.其offsetSize继承Node的offsetSize为1.RootElement:Type of
Elementthat is a root of a model tree.Text:继承
Node,Model text node. Type of node that contains text data.其offsetSize继承Node的offsetSize为1.其offsetSize为文本字符长度.TextProxy:
TextProxyrepresents a part of text node.Since positions can be placed between characters of a text node, ranges may contain only parts of text nodes. When getting items contained in such range, we need to represent a part of that text node, since returning the whole text node would be incorrect.TextProxysolves this issue. 从构造函数constructor( textNode, offsetInText, length )可以看出,主要是根据一个textNode和截取开始的下标及长度来生成新的data属性,相应偏移量和path相关的也相应做出修改。DocumentFragment:DocumentFragment represents a part of model which does not have a common root but it's top-level nodes can be seen as siblings. In other words, it is a detached part of model tree, without a root.DocumentFragment has own
MarkerCollection. Markers from this collection will be set to the model markers by ainsertfunction.Range:Represents a range in the model tree. A range is defined by its
startandendpositions.Range构造函数以start和end两个position为参数,提供了很多跟这两个postion或其他range相关的操作,如共同的item,共同的祖先,是否交叉等等,也提供了迭代方法* [ Symbol.iterator ] = () => yield* new TreeWalker( { boundaries: this, ignoreElementEnd: true } );,利用TreeWalker遍历postion。LiveRange:
LiveRangeis a type of Range that updates itself as document is changed through operations(能够响应applayOpeartion事件和触发change事件). It may be used as a bookmark.Selection:Selection is a set of ranges. It has a direction specified by its anchor and focus (it can be forward or backward). Additionally, selection may have its own attributes (think – whether text typed in in this selection should have those attributes – e.g. whether you type a bolded text). 这里的
Selection跟js的Selection对象有很多相似的概念。-
LiveSelection:
LiveSelectionis used internally byDocumentSelectionand shouldn't be used directly.LiveSelectionis automatically updated upon changes in thedocumentto always contain valid ranges. Its attributes are inherited from the text unless set explicitly.
Differences betweenSelectionandLiveSelectionare:- there is always a range in
LiveSelection- even if no ranges were added there is a "default range"
present in the selection, - ranges added to this selection updates automatically when the document changes,
- attributes of
LiveSelectionare updated automatically according to selection ranges.
- there is always a range in
-
DocumentSelection:
DocumentSelectionis a special selection which is used as the document's selection. There can be only one instance ofDocumentSelectionper document.
Document selection can only be changed by using theWriterinstance inside thechange()block, as it provides a secure way to modify model.
DocumentSelectionis automatically updated upon changes in the document to always contain valid ranges. Its attributes are inherited from the text unless set explicitly.
Differences betweenSelectionandDocumentSelectionare:- there is always a range in
DocumentSelection- even if no ranges were added there is a "default range" present in the selection, - ranges added to this selection updates automatically when the document changes,
- attributes of
DocumentSelectionare updated automatically according to selection ranges.
Since
DocumentSelectionuses live ranges and is updated when document changes, it cannot be set on nodes that are inside document fragment. If you need to represent a selection in document fragment, use Selection class instead. - there is always a range in
-
Position:Represents a position in the model tree.A position is represented by its
rootand apathin that root.You can create position instances via its constructor or thecreatePosition*()factory methods ofModelandWriter.
注意:
Position is based on offsets, not indexes. This means that a position between two text nodesfooandbarhas offset3, not1. Seepathfor more information.
Since a position in the model is represented by a position root and position path it is possible to create positions placed in non-existing places. This requirement is important for operational transformation algorithms.
Also, operations kept in the document history are storing positions (and ranges) which were correct when those operations were applied, but may not be correct after the document has changed.
When changes are applied to the model, it may also happen that position parent will change even if position path has not changed. Keep in mind, that if a position leads to non-existing element,parentand some other properties and methods will throw errors.
In most cases, position with wrong path is caused by an error in code, but it is sometimes needed, as described above.
参考 ckeditor5/engine:Indexes and offsets
-
LivePosition:
LivePositionis a type of Position that updates itself as document is changed through operations. It may be used as a bookmark.Contrary toPosition,LivePositionworks only in roots that areRootElement. IfDocumentFragmentis passed, error will be thrown.
举例
测试偏移量
const model = new Model();
node = new Node();
one = new Element( 'one' );
two = new Element( 'two', null, [ new Text( 'ba' ), new Element( 'img' ), new Text( 'r' ) ] );
textBA = two.getChild( 0 );
img = two.getChild( 1 );
textR = two.getChild( 2 );
three = new Element( 'three' );
doc = model.document;
root = doc.createRoot();
root._appendChild( [ one, two, three ] );
// 测试property
expect( root ).to.have.property( 'root' ).that.equals( root );
expect( one ).to.have.property( 'nextSibling' ).that.equals( two );
// 测试startOffset
expect( one.startOffset ).to.equal( 0 );
expect( two.startOffset ).to.equal( 1 );
expect( three.startOffset ).to.equal( 2 );
expect( textBA.startOffset ).to.equal( 0 );
expect( img.startOffset ).to.equal( 2 );
expect( textR.startOffset ).to.equal( 3 );
// 测试endOffset
expect( one.endOffset ).to.equal( 1 );
expect( two.endOffset ).to.equal( 2 );
expect( three.endOffset ).to.equal( 3 );
expect( textBA.endOffset ).to.equal( 2 );
expect( img.endOffset ).to.equal( 3 );
expect( textR.endOffset ).to.equal( 4 );
// 测试path
expect( root.getPath() ).to.deep.equal( [] );
expect( one.getPath() ).to.deep.equal( [ 0 ] );
expect( two.getPath() ).to.deep.equal( [ 1 ] );
expect( three.getPath() ).to.deep.equal( [ 2 ] );
expect( textBA.getPath() ).to.deep.equal( [ 1, 0 ] );
expect( img.getPath() ).to.deep.equal( [ 1, 2 ] );
expect( textR.getPath() ).to.deep.equal( [ 1, 3 ] );
测试Position
// root
// |- p Before: [ 0 ] After: [ 1 ]
// |- ul Before: [ 1 ] After: [ 2 ]
// |- li Before: [ 1, 0 ] After: [ 1, 1 ]
// | |- f Before: [ 1, 0, 0 ] After: [ 1, 0, 1 ]
// | |- o Before: [ 1, 0, 1 ] After: [ 1, 0, 2 ]
// | |- z Before: [ 1, 0, 2 ] After: [ 1, 0, 3 ]
// |- li Before: [ 1, 1 ] After: [ 1, 2 ]
// |- b Before: [ 1, 1, 0 ] After: [ 1, 1, 1 ]
// |- a Before: [ 1, 1, 1 ] After: [ 1, 1, 2 ]
// |- r Before: [ 1, 1, 2 ] After: [ 1, 1, 3 ]
model = new Model();
doc = model.document;
root = doc.createRoot();
otherRoot = doc.createRoot( '$root', 'otherRoot' );
foz = new Text( 'foz' );
li1 = new Element( 'li', [], foz );
f = new TextProxy( foz, 0, 1 );
o = new TextProxy( foz, 1, 1 );
z = new TextProxy( foz, 2, 1 );
bar = new Text( 'bar' );
li2 = new Element( 'li', [], bar );
b = new TextProxy( bar, 0, 1 );
a = new TextProxy( bar, 1, 1 );
r = new TextProxy( bar, 2, 1 );
ul = new Element( 'ul', [], [ li1, li2 ] );
p = new Element( 'p' );
root._insertChild( 0, [ p, ul ] );
// 测试stickiness
var position = new Position( root, [ 0 ] );
expect( position ).to.have.property( 'path' ).that.deep.equals( [ 0 ] );
expect( position ).to.have.property( 'root' ).that.equals( root );
// offset
expect( new Position( root, [ 0 ] ) ).to.have.property( 'parent' ).that.equals( root );
expect( new Position( root, [ 1 ] ) ).to.have.property( 'parent' ).that.equals( root );
expect( new Position( root, [ 2 ] ) ).to.have.property( 'parent' ).that.equals( root );
expect( new Position( root, [ 0, 0 ] ) ).to.have.property( 'parent' ).that.equals( p );
expect( new Position( root, [ 1, 0 ] ) ).to.have.property( 'parent' ).that.equals( ul );
expect( new Position( root, [ 1, 1 ] ) ).to.have.property( 'parent' ).that.equals( ul );
expect( new Position( root, [ 1, 2 ] ) ).to.have.property( 'parent' ).that.equals( ul );
expect( new Position( root, [ 1, 0, 0 ] ) ).to.have.property( 'parent' ).that.equals( li1 );
expect( new Position( root, [ 1, 0, 1 ] ) ).to.have.property( 'parent' ).that.equals( li1 );
expect( new Position( root, [ 1, 0, 2 ] ) ).to.have.property( 'parent' ).that.equals( li1 );
expect( new Position( root, [ 1, 0, 3 ] ) ).to.have.property( 'parent' ).that.equals( li1 );
// isEqual
var position = new Position( root, [ 1, 1, 2 ] );
const samePosition = new Position( root, [ 1, 1, 2 ] );
expect( position.isEqual( samePosition ) ).to.be.true;
几个Operation
ckeditor5定义了8个主要的操作(operation):
- InsertOperation:在
model指定位置插入1个或多个node - MoveOperation:将一系列的
model items移动到指定位置 - RenameOperation:修改
element的name - AttributeOperation:增加、修改或删除
node的attributes - SplitOperation:在给定拆分位置将
element拆分为两个element,两个元素都包含元素原始内容的一部分 - MergeOperation:用于合并两个
element,合并后的element是入参sourcePosition的parent,并会被合并到入参targetPosition的parent。来自合并后的element的节点将移动到targetPosition位置,合并后的element也将移动到入参graveyardPosition的graveyard中 - NoOperation:对
tree model没有任何改动 - MarkerOperation
const model = new Model();
const doc = model.document;
const operation = new InsertOperation( position, item, doc.version );
model.applyOperation( operation );
示例:
通过model.applyOperation(opt)的方式应用operation,在model和model.documention中通过observablemixin和emittermixin(详见:ObservableMixin、emittermixin)使得其可以将applyOperation转换成事件名,当model调用applyOperation方法时,触发applyOperation事件的回调:
// model/model.js
[ 'insertContent', 'deleteContent', 'modifySelection', 'getSelectedContent', 'applyOperation' ]
.forEach( methodName => this.decorate( methodName ) );
this.on( 'applyOperation', ( evt, args ) => {
const operation = args[ 0 ];
operation._validate();
}, { priority: 'highest' } );
// model/document.js
this.listenTo( model, 'applyOperation', ( evt, args ) => {
// ...
}, { priority: 'highest' } );
重要概念
TreeWalker:Position iterator class. It allows to iterate forward and backward over the document.
TreeWalker定义了[Symbol.iterator]方法和next方法用于迭代,next方法会根据direction来逐步遍历,返回类似{next: true/false}的结构。Document:model与document
Model:model与document
Differ:Calculates the difference between two model states. Receives operations that are to be applied on the model document. Marks parts of the model document tree which are changed and saves the state of these elements before the change. Then, it compares saved elements with the changed elements, after all changes are applied on the model document. Calculates the diff between saved elements and new ones and returns a change set.
History:keeps the track of all the operations applied to the document.