ckeditor5/engine:model tree的几个概念

ckeditor5/engine封装了model tree的几个概念。

数据类型

以下列出了十几个主要数据类型,其中NodeNodeListElementRootElementTextTextProxyDocumentFragment具有类似的结构:

  • 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. NodeList is used internally in classes like Element or DocumentFragment.

  • Element:继承Node,Model element. Type of node that has a name and child nodes.其offsetSize继承NodeoffsetSize为1.

  • RootElement:Type of Element that is a root of a model tree.

  • Text:继承Node,Model text node. Type of node that contains text data.其offsetSize继承NodeoffsetSize为1.其offsetSize为文本字符长度.

  • TextProxyTextProxy represents 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. TextProxy solves 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 ainsert function.

  • Range:Represents a range in the model tree. A range is defined by its start and end positions. Range构造函数以startend两个position为参数,提供了很多跟这两个postion或其他range相关的操作,如共同的item,共同的祖先,是否交叉等等,也提供了迭代方法* [ Symbol.iterator ] = () => yield* new TreeWalker( { boundaries: this, ignoreElementEnd: true } );,利用TreeWalker遍历postion

  • LiveRangeLiveRange is 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对象有很多相似的概念。

  • LiveSelectionLiveSelection is used internally by DocumentSelection and shouldn't be used directly.LiveSelection is automatically updated upon changes in the document to always contain valid ranges. Its attributes are inherited from the text unless set explicitly.
    Differences between Selection and LiveSelection are:

    1. there is always a range in LiveSelection - even if no ranges were added there is a "default range"
      present in the selection,
    2. ranges added to this selection updates automatically when the document changes,
    3. attributes of LiveSelection are updated automatically according to selection ranges.
  • DocumentSelectionDocumentSelection is a special selection which is used as the document's selection. There can be only one instance of DocumentSelection per document.
    Document selection can only be changed by using the Writer instance inside the change() block, as it provides a secure way to modify model.
    DocumentSelection is automatically updated upon changes in the document to always contain valid ranges. Its attributes are inherited from the text unless set explicitly.
    Differences between Selection and DocumentSelection are:

    1. there is always a range in DocumentSelection - even if no ranges were added there is a "default range" present in the selection,
    2. ranges added to this selection updates automatically when the document changes,
    3. attributes of DocumentSelection are updated automatically according to selection ranges.

    Since DocumentSelection uses 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.

  • Position:Represents a position in the model tree.A position is represented by its root and a path in that root.You can create position instances via its constructor or the createPosition*() factory methods of Model and Writer.
    注意:
    Position is based on offsets, not indexes. This means that a position between two text nodes foo and bar has offset 3, not 1. See path for 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, parent and 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
  • LivePositionLivePosition is a type of Position that updates itself as document is changed through operations. It may be used as a bookmark.Contrary to Position, LivePosition works only in roots that are RootElement. If DocumentFragment is 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:修改elementname
  • AttributeOperation:增加、修改或删除nodeattributes
  • SplitOperation:在给定拆分位置将element拆分为两个element,两个元素都包含元素原始内容的一部分
  • MergeOperation:用于合并两个element,合并后的element是入参sourcePositionparent,并会被合并到入参targetPositionparent。来自合并后的element的节点将移动到targetPosition位置,合并后的element也将移动到入参graveyardPositiongraveyard
  • 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,在modelmodel.documention中通过observablemixinemittermixin(详见:ObservableMixinemittermixin)使得其可以将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}的结构。

  • Documentmodel与document

  • Modelmodel与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.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容