diff --git a/src/model/element.js b/src/model/element.js index 205e1678e..df1506a57 100644 --- a/src/model/element.js +++ b/src/model/element.js @@ -109,7 +109,7 @@ export default class Element extends Node { */ is( type, name = null ) { if ( !name ) { - return type == 'element' || type == this.name; + return type == 'element' || type == this.name || super.is( type ); } else { return type == 'element' && name == this.name; } diff --git a/src/model/node.js b/src/model/node.js index 2f419cbfc..b748ab377 100644 --- a/src/model/node.js +++ b/src/model/node.js @@ -9,6 +9,7 @@ import toMap from '@ckeditor/ckeditor5-utils/src/tomap'; import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; +import compareArrays from '@ckeditor/ckeditor5-utils/src/comparearrays'; /** * Model node. Most basic structure of model tree. @@ -273,6 +274,63 @@ export default class Node { return i === 0 ? null : ancestorsA[ i - 1 ]; } + /** + * Returns whether this node is before given node. `false` is returned if nodes are in different trees (for example, + * in different {@link module:engine/model/documentfragment~DocumentFragment}s). + * + * @param {module:engine/model/node~Node} node Node to compare with. + * @returns {Boolean} + */ + isBefore( node ) { + // Given node is not before this node if they are same. + if ( this == node ) { + return false; + } + + // Return `false` if it is impossible to compare nodes. + if ( this.root !== node.root ) { + return false; + } + + const thisPath = this.getPath(); + const nodePath = node.getPath(); + + const result = compareArrays( thisPath, nodePath ); + + switch ( result ) { + case 'prefix': + return true; + + case 'extension': + return false; + + default: + return thisPath[ result ] < nodePath[ result ]; + } + } + + /** + * Returns whether this node is after given node. `false` is returned if nodes are in different trees (for example, + * in different {@link module:engine/model/documentfragment~DocumentFragment}s). + * + * @param {module:engine/model/node~Node} node Node to compare with. + * @returns {Boolean} + */ + isAfter( node ) { + // Given node is not before this node if they are same. + if ( this == node ) { + return false; + } + + // Return `false` if it is impossible to compare nodes. + if ( this.root !== node.root ) { + return false; + } + + // In other cases, just check if the `node` is before, and return the opposite. + return !this.isBefore( node ); + } + /** * Checks if the node has an attribute with given key. * @@ -401,7 +459,7 @@ export default class Node { * may return {@link module:engine/model/documentfragment~DocumentFragment} or {@link module:engine/model/node~Node} * that can be either text node or element. This method can be used to check what kind of object is returned. * - * obj.is( 'node' ); // true for any node, false for document fragment + * obj.is( 'node' ); // true for any node, false for document fragment and text fragment * obj.is( 'documentFragment' ); // true for document fragment, false for any node * obj.is( 'element' ); // true for any element, false for text node or document fragment * obj.is( 'element', 'paragraph' ); // true only for element which name is 'paragraph' @@ -413,6 +471,9 @@ export default class Node { * @param {'element'|'rootElement'|'text'|'textProxy'|'documentFragment'} type * @returns {Boolean} */ + is( type ) { + return type == 'node'; + } } /** diff --git a/src/model/position.js b/src/model/position.js index d0c0b6674..47b41a285 100644 --- a/src/model/position.js +++ b/src/model/position.js @@ -238,11 +238,7 @@ export default class Position { return 'after'; default: - if ( this.path[ result ] < otherPosition.path[ result ] ) { - return 'before'; - } else { - return 'after'; - } + return this.path[ result ] < otherPosition.path[ result ] ? 'before' : 'after'; } } diff --git a/src/model/text.js b/src/model/text.js index 0041b860f..4b4a6261e 100644 --- a/src/model/text.js +++ b/src/model/text.js @@ -65,7 +65,7 @@ export default class Text extends Node { * @inheritDoc */ is( type ) { - return type == 'text'; + return type == 'text' || super.is( type ); } /** diff --git a/src/view/element.js b/src/view/element.js index eec64ab39..52eecb997 100644 --- a/src/view/element.js +++ b/src/view/element.js @@ -152,7 +152,7 @@ export default class Element extends Node { */ is( type, name = null ) { if ( !name ) { - return type == 'element' || type == this.name; + return type == 'element' || type == this.name || super.is( type ); } else { return type == 'element' && name == this.name; } diff --git a/src/view/node.js b/src/view/node.js index b7f57eeaf..6a63931c6 100644 --- a/src/view/node.js +++ b/src/view/node.js @@ -11,6 +11,7 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin'; import mix from '@ckeditor/ckeditor5-utils/src/mix'; import clone from '@ckeditor/ckeditor5-utils/src/lib/lodash/clone'; +import compareArrays from '@ckeditor/ckeditor5-utils/src/comparearrays'; /** * Abstract tree view node class. @@ -118,6 +119,33 @@ export default class Node { } } + /** + * Gets a path to the node. The path is an array containing indices of consecutive ancestors of this node, + * beginning from {@link module:engine/view/node~Node#root root}, down to this node's index. + * + * 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 ] ); + * 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 []. + * + * @returns {Array.} The path. + */ + getPath() { + const path = []; + let node = this; // eslint-disable-line consistent-this + + while ( node.parent ) { + path.unshift( node.index ); + node = node.parent; + } + + return path; + } + /** * Returns ancestors array of this node. * @@ -162,6 +190,63 @@ export default class Node { return i === 0 ? null : ancestorsA[ i - 1 ]; } + /** + * Returns whether this node is before given node. `false` is returned if nodes are in different trees (for example, + * in different {@link module:engine/view/documentfragment~DocumentFragment}s). + * + * @param {module:engine/view/node~Node} node Node to compare with. + * @returns {Boolean} + */ + isBefore( node ) { + // Given node is not before this node if they are same. + if ( this == node ) { + return false; + } + + // Return `false` if it is impossible to compare nodes. + if ( this.root !== node.root ) { + return false; + } + + const thisPath = this.getPath(); + const nodePath = node.getPath(); + + const result = compareArrays( thisPath, nodePath ); + + switch ( result ) { + case 'prefix': + return true; + + case 'extension': + return false; + + default: + return thisPath[ result ] < nodePath[ result ]; + } + } + + /** + * Returns whether this node is after given node. `false` is returned if nodes are in different trees (for example, + * in different {@link module:engine/view/documentfragment~DocumentFragment}s). + * + * @param {module:engine/view/node~Node} node Node to compare with. + * @returns {Boolean} + */ + isAfter( node ) { + // Given node is not before this node if they are same. + if ( this == node ) { + return false; + } + + // Return `false` if it is impossible to compare nodes. + if ( this.root !== node.root ) { + return false; + } + + // In other cases, just check if the `node` is before, and return the opposite. + return !this.isBefore( node ); + } + /** * Removes node from parent. * @@ -198,20 +283,6 @@ export default class Node { return json; } - /** - * Clones this node. - * - * @method #clone - * @returns {module:engine/view/node~Node} Clone of this node. - */ - - /** - * Checks if provided node is similar to this node. - * - * @method #isSimilar - * @returns {Boolean} True if nodes are similar. - */ - /** * Checks whether given view tree object is of given type. * @@ -219,7 +290,7 @@ export default class Node { * may return {@link module:engine/view/documentfragment~DocumentFragment} or {@link module:engine/view/node~Node} * that can be either text node or element. This method can be used to check what kind of object is returned. * - * obj.is( 'node' ); // true for any node, false for document fragment + * obj.is( 'node' ); // true for any node, false for document fragment and text fragment * obj.is( 'documentFragment' ); // true for document fragment, false for any node * obj.is( 'element' ); // true for any element, false for text node or document fragment * obj.is( 'element', 'p' ); // true only for element which name is 'p' @@ -231,6 +302,24 @@ export default class Node { * 'rootElement'|'documentFragment'|'text'|'textProxy'} type * @returns {Boolean} */ + is( type ) { + return type == 'node'; + } + + /** + * Clones this node. + * + * @protected + * @method #_clone + * @returns {module:engine/view/node~Node} Clone of this node. + */ + + /** + * Checks if provided node is similar to this node. + * + * @method #isSimilar + * @returns {Boolean} True if nodes are similar. + */ } /** diff --git a/src/view/position.js b/src/view/position.js index 6338df03a..1ccdd16db 100644 --- a/src/view/position.js +++ b/src/view/position.js @@ -243,61 +243,35 @@ export default class Position { * @returns {module:engine/view/position~PositionRelation} */ compareWith( otherPosition ) { - if ( this.isEqual( otherPosition ) ) { - return 'same'; + if ( this.root !== otherPosition.root ) { + return 'different'; } - // If positions have same parent. - if ( this.parent === otherPosition.parent ) { - return this.offset - otherPosition.offset < 0 ? 'before' : 'after'; + if ( this.isEqual( otherPosition ) ) { + return 'same'; } // Get path from root to position's parent element. - const path = this.getAncestors(); - const otherPath = otherPosition.getAncestors(); + const thisPath = this.parent.is( 'node' ) ? this.parent.getPath() : []; + const otherPath = otherPosition.parent.is( 'node' ) ? otherPosition.parent.getPath() : []; - // Compare both path arrays to find common ancestor. - const result = compareArrays( path, otherPath ); + // Add the positions' offsets to the parents offsets. + thisPath.push( this.offset ); + otherPath.push( otherPosition.offset ); - let commonAncestorIndex; + // Compare both path arrays to find common ancestor. + const result = compareArrays( thisPath, otherPath ); switch ( result ) { - case 0: - // No common ancestors found. - return 'different'; - case 'prefix': - commonAncestorIndex = path.length - 1; - break; + return 'before'; case 'extension': - commonAncestorIndex = otherPath.length - 1; - break; + return 'after'; default: - commonAncestorIndex = result - 1; + return thisPath[ result ] < otherPath[ result ] ? 'before' : 'after'; } - - // Common ancestor of two positions. - const commonAncestor = path[ commonAncestorIndex ]; - const nextAncestor1 = path[ commonAncestorIndex + 1 ]; - const nextAncestor2 = otherPath[ commonAncestorIndex + 1 ]; - - // Check if common ancestor is not one of the parents. - if ( commonAncestor === this.parent ) { - const index = this.offset - nextAncestor2.index; - - return index <= 0 ? 'before' : 'after'; - } else if ( commonAncestor === otherPosition.parent ) { - const index = nextAncestor1.index - otherPosition.offset; - - return index < 0 ? 'before' : 'after'; - } - - const index = nextAncestor1.index - nextAncestor2.index; - - // Compare indexes of next ancestors inside common one. - return index < 0 ? 'before' : 'after'; } /** diff --git a/src/view/text.js b/src/view/text.js index 1d1a92f3f..aaad624fe 100644 --- a/src/view/text.js +++ b/src/view/text.js @@ -42,7 +42,7 @@ export default class Text extends Node { * @inheritDoc */ is( type ) { - return type == 'text'; + return type == 'text' || super.is( type ); } /** diff --git a/tests/model/documentfragment.js b/tests/model/documentfragment.js index 1a24afff2..2292d28ab 100644 --- a/tests/model/documentfragment.js +++ b/tests/model/documentfragment.js @@ -74,6 +74,7 @@ describe( 'DocumentFragment', () => { } ); it( 'should return false for other accept values', () => { + expect( frag.is( 'node' ) ).to.be.false; expect( frag.is( 'text' ) ).to.be.false; expect( frag.is( 'textProxy' ) ).to.be.false; expect( frag.is( 'element' ) ).to.be.false; diff --git a/tests/model/element.js b/tests/model/element.js index b3568e279..e56b81a2d 100644 --- a/tests/model/element.js +++ b/tests/model/element.js @@ -46,7 +46,8 @@ describe( 'Element', () => { element = new Element( 'paragraph' ); } ); - it( 'should return true for element, element with same name and element name', () => { + it( 'should return true for node, element, element with same name and element name', () => { + expect( element.is( 'node' ) ).to.be.true; expect( element.is( 'element' ) ).to.be.true; expect( element.is( 'element', 'paragraph' ) ).to.be.true; expect( element.is( 'paragraph' ) ).to.be.true; diff --git a/tests/model/node.js b/tests/model/node.js index 9648ce915..02d048b50 100644 --- a/tests/model/node.js +++ b/tests/model/node.js @@ -164,6 +164,12 @@ describe( 'Node', () => { } ); } ); + describe( 'is()', () => { + it( 'should return true for node', () => { + expect( node.is( 'node' ) ).to.be.true; + } ); + } ); + describe( 'startOffset', () => { it( 'should return null if the parent is null', () => { expect( root.startOffset ).to.be.null; @@ -319,6 +325,82 @@ describe( 'Node', () => { } ); } ); + describe( 'isBefore()', () => { + // Model is: bar + it( 'should return true if the element is before given element', () => { + expect( one.isBefore( two ) ).to.be.true; + expect( one.isBefore( img ) ).to.be.true; + + expect( two.isBefore( textBA ) ).to.be.true; + expect( two.isBefore( textR ) ).to.be.true; + expect( two.isBefore( three ) ).to.be.true; + + expect( root.isBefore( one ) ).to.be.true; + } ); + + it( 'should return false if the element is after given element', () => { + expect( two.isBefore( one ) ).to.be.false; + expect( img.isBefore( one ) ).to.be.false; + + expect( textBA.isBefore( two ) ).to.be.false; + expect( textR.isBefore( two ) ).to.be.false; + expect( three.isBefore( two ) ).to.be.false; + + expect( one.isBefore( root ) ).to.be.false; + } ); + + it( 'should return false if the same element is given', () => { + expect( one.isBefore( one ) ).to.be.false; + } ); + + it( 'should return false if elements are in different roots', () => { + const otherRoot = new Element( 'root' ); + const otherElement = new Element( 'element' ); + + otherRoot._appendChildren( otherElement ); + + expect( otherElement.isBefore( three ) ).to.be.false; + } ); + } ); + + describe( 'isAfter()', () => { + // Model is: bar + it( 'should return true if the element is after given element', () => { + expect( two.isAfter( one ) ).to.be.true; + expect( img.isAfter( one ) ).to.be.true; + + expect( textBA.isAfter( two ) ).to.be.true; + expect( textR.isAfter( two ) ).to.be.true; + expect( three.isAfter( two ) ).to.be.true; + + expect( one.isAfter( root ) ).to.be.true; + } ); + + it( 'should return false if the element is before given element', () => { + expect( one.isAfter( two ) ).to.be.false; + expect( one.isAfter( img ) ).to.be.false; + + expect( two.isAfter( textBA ) ).to.be.false; + expect( two.isAfter( textR ) ).to.be.false; + expect( two.isAfter( three ) ).to.be.false; + + expect( root.isAfter( one ) ).to.be.false; + } ); + + it( 'should return false if the same element is given', () => { + expect( one.isAfter( one ) ).to.be.false; + } ); + + it( 'should return false if elements are in different roots', () => { + const otherRoot = new Element( 'root' ); + const otherElement = new Element( 'element' ); + + otherRoot._appendChildren( otherElement ); + + expect( three.isAfter( otherElement ) ).to.be.false; + } ); + } ); + describe( 'attributes interface', () => { const node = new Node( { foo: 'bar' } ); diff --git a/tests/model/text.js b/tests/model/text.js index b3ab4da94..9d8a97c95 100644 --- a/tests/model/text.js +++ b/tests/model/text.js @@ -40,7 +40,8 @@ describe( 'Text', () => { text = new Text( 'bar' ); } ); - it( 'should return true for text', () => { + it( 'should return true for node, text', () => { + expect( text.is( 'node' ) ).to.be.true; expect( text.is( 'text' ) ).to.be.true; } ); diff --git a/tests/model/textproxy.js b/tests/model/textproxy.js index ed86b888c..be75a9371 100644 --- a/tests/model/textproxy.js +++ b/tests/model/textproxy.js @@ -107,6 +107,7 @@ describe( 'TextProxy', () => { } ); it( 'should return false for other accept values', () => { + expect( textProxy.is( 'node' ) ).to.be.false; expect( textProxy.is( 'text' ) ).to.be.false; expect( textProxy.is( 'element' ) ).to.be.false; expect( textProxy.is( 'documentFragment' ) ).to.be.false; diff --git a/tests/view/documentfragment.js b/tests/view/documentfragment.js index 49c7a253d..ed6c2ebeb 100644 --- a/tests/view/documentfragment.js +++ b/tests/view/documentfragment.js @@ -83,6 +83,7 @@ describe( 'DocumentFragment', () => { } ); it( 'should return false for other accept values', () => { + expect( frag.is( 'node' ) ).to.be.false; expect( frag.is( 'text' ) ).to.be.false; expect( frag.is( 'textProxy' ) ).to.be.false; expect( frag.is( 'element' ) ).to.be.false; diff --git a/tests/view/element.js b/tests/view/element.js index ec86c05cc..f04027740 100644 --- a/tests/view/element.js +++ b/tests/view/element.js @@ -89,7 +89,8 @@ describe( 'Element', () => { el = new Element( 'p' ); } ); - it( 'should return true for element, element with correct name and element name', () => { + it( 'should return true for node, element, element with correct name and element name', () => { + expect( el.is( 'node' ) ).to.be.true; expect( el.is( 'element' ) ).to.be.true; expect( el.is( 'element', 'p' ) ).to.be.true; expect( el.is( 'p' ) ).to.be.true; diff --git a/tests/view/node.js b/tests/view/node.js index b8c94d0b8..777606ecc 100644 --- a/tests/view/node.js +++ b/tests/view/node.js @@ -5,6 +5,7 @@ import Element from '../../src/view/element'; import Text from '../../src/view/text'; +import Node from '../../src/view/node'; import DocumentFragment from '../../src/view/documentfragment'; import RootEditableElement from '../../src/view/rooteditableelement'; import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; @@ -29,6 +30,14 @@ describe( 'Node', () => { root = new Element( null, null, [ one, two, three ] ); } ); + describe( 'is()', () => { + it( 'should return true for node', () => { + const node = new Node(); + + expect( node.is( 'node' ) ).to.be.true; + } ); + } ); + describe( 'getNextSibling/getPreviousSibling()', () => { it( 'should return next sibling', () => { expect( root.nextSibling ).to.be.null; @@ -214,6 +223,20 @@ describe( 'Node', () => { } ); } ); + describe( 'getPath()', () => { + it( 'should return empty array is the element is the root', () => { + expect( root.getPath() ).to.deep.equal( [] ); + } ); + + it( 'should return array with indices of given element and its ancestors starting from top-most one', () => { + expect( one.getPath() ).to.deep.equal( [ 0 ] ); + expect( two.getPath() ).to.deep.equal( [ 1 ] ); + expect( img.getPath() ).to.deep.equal( [ 1, 2 ] ); + expect( charR.getPath() ).to.deep.equal( [ 1, 3 ] ); + expect( three.getPath() ).to.deep.equal( [ 2 ] ); + } ); + } ); + describe( 'getDocument()', () => { it( 'should return null if any parent has not set Document', () => { expect( charA.document ).to.be.null; @@ -258,6 +281,82 @@ describe( 'Node', () => { } ); } ); + describe( 'isBefore()', () => { + // Model is: bar + it( 'should return true if the element is before given element', () => { + expect( one.isBefore( two ) ).to.be.true; + expect( one.isBefore( img ) ).to.be.true; + + expect( two.isBefore( charB ) ).to.be.true; + expect( two.isBefore( charR ) ).to.be.true; + expect( two.isBefore( three ) ).to.be.true; + + expect( root.isBefore( one ) ).to.be.true; + } ); + + it( 'should return false if the element is after given element', () => { + expect( two.isBefore( one ) ).to.be.false; + expect( img.isBefore( one ) ).to.be.false; + + expect( charB.isBefore( two ) ).to.be.false; + expect( charR.isBefore( two ) ).to.be.false; + expect( three.isBefore( two ) ).to.be.false; + + expect( one.isBefore( root ) ).to.be.false; + } ); + + it( 'should return false if the same element is given', () => { + expect( one.isBefore( one ) ).to.be.false; + } ); + + it( 'should return false if elements are in different roots', () => { + const otherRoot = new Element( 'root' ); + const otherElement = new Element( 'element' ); + + otherRoot._appendChildren( otherElement ); + + expect( otherElement.isBefore( three ) ).to.be.false; + } ); + } ); + + describe( 'isAfter()', () => { + // Model is: bar + it( 'should return true if the element is after given element', () => { + expect( two.isAfter( one ) ).to.be.true; + expect( img.isAfter( one ) ).to.be.true; + + expect( charB.isAfter( two ) ).to.be.true; + expect( charR.isAfter( two ) ).to.be.true; + expect( three.isAfter( two ) ).to.be.true; + + expect( one.isAfter( root ) ).to.be.true; + } ); + + it( 'should return false if the element is before given element', () => { + expect( one.isAfter( two ) ).to.be.false; + expect( one.isAfter( img ) ).to.be.false; + + expect( two.isAfter( charB ) ).to.be.false; + expect( two.isAfter( charR ) ).to.be.false; + expect( two.isAfter( three ) ).to.be.false; + + expect( root.isAfter( one ) ).to.be.false; + } ); + + it( 'should return false if the same element is given', () => { + expect( one.isAfter( one ) ).to.be.false; + } ); + + it( 'should return false if elements are in different roots', () => { + const otherRoot = new Element( 'root' ); + const otherElement = new Element( 'element' ); + + otherRoot._appendChildren( otherElement ); + + expect( three.isAfter( otherElement ) ).to.be.false; + } ); + } ); + describe( '_remove()', () => { it( 'should remove node from its parent', () => { const char = new Text( 'a' ); diff --git a/tests/view/text.js b/tests/view/text.js index 3acd9fc81..691544458 100644 --- a/tests/view/text.js +++ b/tests/view/text.js @@ -24,7 +24,8 @@ describe( 'Text', () => { text = new Text( 'foo' ); } ); - it( 'should return true for text', () => { + it( 'should return true for node, text', () => { + expect( text.is( 'node' ) ).to.be.true; expect( text.is( 'text' ) ).to.be.true; } ); diff --git a/tests/view/textproxy.js b/tests/view/textproxy.js index a638f74f7..c82a7d979 100644 --- a/tests/view/textproxy.js +++ b/tests/view/textproxy.js @@ -67,6 +67,7 @@ describe( 'TextProxy', () => { } ); it( 'should return false for other accept values', () => { + expect( textProxy.is( 'node' ) ).to.be.false; expect( textProxy.is( 'text' ) ).to.be.false; expect( textProxy.is( 'element' ) ).to.be.false; expect( textProxy.is( 'containerElement' ) ).to.be.false;