Parcourir la source

Added isBefore, isAfter and compareWith methods to treeView.Position.

Szymon Kupś il y a 9 ans
Parent
commit
aeee5d1777

+ 104 - 2
packages/ckeditor5-engine/src/treeview/position.js

@@ -5,6 +5,8 @@
 
 'use strict';
 
+import utils from '../../utils/utils.js';
+
 /**
  * Position in the tree. Position is always located before or after a node.
  *
@@ -19,9 +21,9 @@ export default class Position {
 	 */
 	constructor( parent, offset ) {
 		/**
-		 * Position parent element.
+		 * Position parent node.
 		 *
-		 * @member {engine.treeView.Element} engine.treeView.Position#parent
+		 * @member {engine.treeView.Node} engine.treeView.Position#parent
 		 */
 		this.parent = parent;
 
@@ -58,6 +60,99 @@ export default class Position {
 		return this == otherPosition || ( this.parent == otherPosition.parent && this.offset == otherPosition.offset );
 	}
 
+	/**
+	 * Checks whether this position is located before given position. When method returns `false` it does not mean that
+	 * this position is after give one. Two positions may be located inside separate roots and in that situation this
+	 * method will still return `false`.
+	 *
+	 * @see engine.treeView.Position#isAfter
+	 * @see engine.treeView.Position#compareWith
+	 * @param {engine.treeView.Position} otherPosition Position to compare with.
+	 * @returns {Boolean} Returns `true` if this position is before given position.
+	 */
+	isBefore( otherPosition ) {
+		return this.compareWith( otherPosition ) == 'BEFORE';
+	}
+
+	/**
+	 * Checks whether this position is located after given position. When method returns `false` it does not mean that
+	 * this position is before give one. Two positions may be located inside separate roots and in that situation this
+	 * method will still return `false`.
+	 *
+	 * @see engine.treeView.Position#isBefore
+	 * @see engine.treeView.Position#compareWith
+	 * @param {engine.treeView.Position} otherPosition Position to compare with.
+	 * @returns {Boolean} Returns `true` if this position is after given position.
+	 */
+	isAfter( otherPosition ) {
+		return this.compareWith( otherPosition ) == 'AFTER';
+	}
+
+	/**
+	 * Checks whether this position is before, after or in same position that other position. Two positions may be also
+	 * different when they are located in separate roots.
+	 *
+	 * @param {engine.treeView.Position} otherPosition Position to compare with.
+	 * @returns {engine.treeView.PositionRelation}
+	 */
+	compareWith( otherPosition ) {
+		if ( this.isEqual( otherPosition ) ) {
+			return 'SAME';
+		}
+
+		// If positions have same parent.
+		if ( this.parent === otherPosition.parent ) {
+			return this.offset - otherPosition.offset < 0 ? 'BEFORE' : 'AFTER';
+		}
+
+		// Get path from root to position's parent element.
+		const path = this.parent.getAncestors( true );
+		const otherPath = otherPosition.parent.getAncestors( true );
+
+		// Compare both path arrays to find common ancestor.
+		const result = utils.compareArrays( path, otherPath );
+
+		let commonAncestorIndex;
+
+		switch ( result ) {
+			case 0:
+				// No common ancestors found.
+				return 'DIFFERENT';
+
+			case 'PREFIX':
+				commonAncestorIndex = path.length - 1;
+				break;
+
+			case 'EXTENSION':
+				commonAncestorIndex = otherPath.length - 1;
+				break;
+
+			default:
+				commonAncestorIndex = result - 1;
+		}
+
+		// 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 - this.parent.getChildIndex( nextAncestor2 );
+
+			return index <= 0 ? 'BEFORE' : 'AFTER';
+		} else if ( commonAncestor === otherPosition.parent ) {
+			const index = otherPosition.parent.getChildIndex( nextAncestor1 ) - otherPosition.offset;
+
+			return index < 0 ? 'BEFORE' : 'AFTER';
+		}
+
+		const index = commonAncestor.getChildIndex( nextAncestor1 ) - commonAncestor.getChildIndex( nextAncestor2 );
+
+		// Compare indexes of next ancestors inside common one.
+		return index < 0 ? 'BEFORE' : 'AFTER';
+	}
+
 	/**
 	 * Creates and returns a new instance of Position, which is equal to passed position.
 	 *
@@ -68,3 +163,10 @@ export default class Position {
 		return new this( position.parent, position.offset );
 	}
 }
+
+/**
+ * A flag indicating whether this position is `'BEFORE'` or `'AFTER'` or `'SAME'` as given position.
+ * If positions are in different roots `'DIFFERENT'` flag is returned.
+ *
+ * @typedef {String} engine.treeView.PositionRelation
+ */

+ 153 - 0
packages/ckeditor5-engine/tests/treeview/position.js

@@ -8,6 +8,9 @@
 'use strict';
 
 import Position from '/ckeditor5/engine/treeview/position.js';
+import Node from '/ckeditor5/engine/treeview/node.js';
+import Element from '/ckeditor5/engine/treeview/element.js';
+import Text from '/ckeditor5/engine/treeview/text.js';
 
 describe( 'Position', () => {
 	const parentMock = {};
@@ -80,4 +83,154 @@ describe( 'Position', () => {
 			expect( position1.isEqual( position2 ) ).to.be.false;
 		} );
 	} );
+
+	describe( 'isBefore', () => {
+		it( 'should return false for same positions', () => {
+			const node = new Node();
+			const position1 = new Position( node, 10 );
+			const position2 = new Position( node, 10 );
+
+			expect( position1.isBefore( position1 ) ).to.be.false;
+			expect( position1.isBefore( position2 ) ).to.be.false;
+			expect( position2.isBefore( position1 ) ).to.be.false;
+		} );
+
+		it( 'should return false if no common ancestor is found', () => {
+			const t1 = new Text( 'foo' );
+			const t2 = new Text( 'bar' );
+			const e1 = new Element( 'p', null, [ t1 ] );
+			const e2 = new Element( 'p', null, [ t2 ] );
+			const position1 = new Position( e1, 0 );
+			const position2 = new Position( e2, 1 );
+
+			expect( position1.isBefore( position2 ) );
+			expect( position2.isBefore( position1 ) );
+		} );
+
+		it( 'should return true if position is before in same node', () => {
+			const node = new Node();
+			const p1 = new Position( node, 10 );
+			const p2 = new Position( node, 5 );
+
+			expect( p2.isBefore( p1 ) ).to.be.true;
+			expect( p1.isBefore( p2 ) ).to.be.false;
+		} );
+
+		it( 'should compare positions that have common parent', () => {
+			const t1 = new Text( 'foo' );
+			const t2 = new Text( 'bar' );
+			const root = new Element( 'p', null, [ t1, t2 ] );
+			const position1 = new Position( t1, 2 );
+			const position2 = new Position( t2, 0 );
+			const position3 = new Position( root, 0 );
+			const position4 = new Position( root, 2 );
+			const position5 = new Position( t1, 0 );
+			const position6 = new Position( root, 1 );
+
+			expect( position1.isBefore( position2 ) ).to.be.true;
+			expect( position2.isBefore( position1 ) ).to.be.false;
+			expect( position3.isBefore( position1  ) ).to.be.true;
+			expect( position3.isBefore( position2 ) ).to.be.true;
+			expect( position1.isBefore( position3 ) ).to.be.false;
+			expect( position2.isBefore( position3 ) ).to.be.false;
+			expect( position4.isBefore( position1 ) ).to.be.false;
+			expect( position4.isBefore( position3 ) ).to.be.false;
+			expect( position3.isBefore( position4 ) ).to.be.true;
+			expect( position3.isBefore( position5 ) ).to.be.true;
+			expect( position6.isBefore( position2 ) ).to.be.true;
+			expect( position1.isBefore( position6 ) ).to.be.true;
+		} );
+	} );
+
+	describe( 'isAfter', () => {
+		it( 'should return false for same positions', () => {
+			const node = new Node();
+			const position1 = new Position( node, 10 );
+			const position2 = new Position( node, 10 );
+
+			expect( position1.isAfter( position1 ) ).to.be.false;
+			expect( position1.isAfter( position2 ) ).to.be.false;
+			expect( position2.isAfter( position1 ) ).to.be.false;
+		} );
+
+		it( 'should return false if no common ancestor is found', () => {
+			const t1 = new Text( 'foo' );
+			const t2 = new Text( 'bar' );
+			const e1 = new Element( 'p', null, [ t1 ] );
+			const e2 = new Element( 'p', null, [ t2 ] );
+			const position1 = new Position( e1, 0 );
+			const position2 = new Position( e2, 1 );
+
+			expect( position1.isAfter( position2 ) );
+			expect( position2.isAfter( position1 ) );
+		} );
+
+		it( 'should return true if position is after in same node', () => {
+			const node = new Node();
+			const p1 = new Position( node, 10 );
+			const p2 = new Position( node, 5 );
+
+			expect( p2.isAfter( p1 ) ).to.be.false;
+			expect( p1.isAfter( p2 ) ).to.be.true;
+		} );
+
+		it( 'should compare positions that have common parent', () => {
+			const t1 = new Text( 'foo' );
+			const t2 = new Text( 'bar' );
+			const root = new Element( 'p', null, [ t1, t2 ] );
+			const position1 = new Position( t1, 2 );
+			const position2 = new Position( t2, 0 );
+			const position3 = new Position( root, 0 );
+			const position4 = new Position( root, 2 );
+			const position5 = new Position( t1, 0 );
+			const position6 = new Position( root, 1 );
+
+			expect( position1.isAfter( position2 ) ).to.be.false;
+			expect( position2.isAfter( position1 ) ).to.be.true;
+			expect( position3.isAfter( position1 ) ).to.be.false;
+			expect( position3.isAfter( position2 ) ).to.be.false;
+			expect( position1.isAfter( position3 ) ).to.be.true;
+			expect( position2.isAfter( position3 ) ).to.be.true;
+			expect( position4.isAfter( position1 ) ).to.be.true;
+			expect( position4.isAfter( position3 ) ).to.be.true;
+			expect( position3.isAfter( position4 ) ).to.be.false;
+			expect( position5.isAfter( position3 ) ).to.be.true;
+			expect( position2.isAfter( position6 ) ).to.be.true;
+		} );
+	} );
+
+	describe( 'compareWith', () => {
+		it( 'should return SAME if positions are same', () => {
+			const root = new Node();
+			const position = new Position( root, 0 );
+			const compared = new Position( root, 0 );
+
+			expect( position.compareWith( compared ) ).to.equal( 'SAME' );
+		} );
+
+		it( 'should return BEFORE if the position is before compared one', () => {
+			const root = new Node();
+			const position = new Position( root, 0 );
+			const compared = new Position( root, 1 );
+
+			expect( position.compareWith( compared ) ).to.equal( 'BEFORE' );
+		} );
+
+		it( 'should return AFTER if the position is after compared one', () => {
+			const root = new Node();
+			const position = new Position( root, 4 );
+			const compared = new Position( root, 1 );
+
+			expect( position.compareWith( compared ) ).to.equal( 'AFTER' );
+		} );
+
+		it( 'should return DIFFERENT if positions are in different roots', () => {
+			const root1 = new Node();
+			const root2 = new Node();
+			const position = new Position( root1, 4 );
+			const compared = new Position( root2, 1 );
+
+			expect( position.compareWith( compared ) ).to.equal( 'DIFFERENT' );
+		} );
+	} );
 } );