Jelajahi Sumber

Introduced `Position#getCommonAncestor( otherPosition )` and `Range#getCommonAncestor()` methods for `view` and `model`.

Kamil Piechaczek 8 tahun lalu
induk
melakukan
78d2eb60f0

+ 24 - 0
packages/ckeditor5-engine/src/model/position.js

@@ -318,6 +318,30 @@ export default class Position {
 		return this.path.slice( 0, diffAt );
 	}
 
+	/**
+	 * Returns a {@link module:engine/model/node~Node} which is a common ancestor for both positions. The {@link #root roots}
+	 * of these two positions must be identical.
+	 *
+	 * @param {module:engine/model/position~Position} position The second position.
+	 * @returns {module:engine/model/node~Node|null} Node that contains both positions.
+	 */
+	getCommonAncestor( position ) {
+		if ( this.root !== position.root ) {
+			return null;
+		}
+
+		const ancestorsA = this.getAncestors();
+		const ancestorsB = position.getAncestors();
+
+		let i = 0;
+
+		while ( ancestorsA[ i ] == ancestorsB[ i ] && ancestorsA[ i ] ) {
+			i++;
+		}
+
+		return i === 0 ? null : ancestorsA[ i - 1 ];
+	}
+
 	/**
 	 * Returns a new instance of `Position`, that has same {@link #parent parent} but it's offset
 	 * is shifted by `shift` value (can be a negative value).

+ 9 - 0
packages/ckeditor5-engine/src/model/range.js

@@ -445,6 +445,15 @@ export default class Range {
 		return ranges;
 	}
 
+	/**
+	 * Returns a {@link module:engine/model/node~Node} which is a common ancestor for both positions.
+	 *
+	 * @returns {module:engine/model/node~Node|null}
+	 */
+	getCommonAncestor() {
+		return this.start.getCommonAncestor( this.end );
+	}
+
 	/**
 	 * Returns a range that is a result of transforming this range by a change in the model document.
 	 *

+ 19 - 0
packages/ckeditor5-engine/src/view/position.js

@@ -175,6 +175,25 @@ export default class Position {
 		}
 	}
 
+	/**
+	 * Returns a {@link module:engine/view/node~Node} which is a common ancestor for both positions.
+	 *
+	 * @param {module:engine/view/position~Position} position
+	 * @returns {module:engine/view/node~Node|null}
+	 */
+	getCommonAncestor( position ) {
+		const ancestorsA = this.getAncestors();
+		const ancestorsB = position.getAncestors();
+
+		let i = 0;
+
+		while ( ancestorsA[ i ] == ancestorsB[ i ] && ancestorsA[ i ] ) {
+			i++;
+		}
+
+		return i === 0 ? null : ancestorsA[ i - 1 ];
+	}
+
 	/**
 	 * Checks whether this position equals given position.
 	 *

+ 9 - 0
packages/ckeditor5-engine/src/view/range.js

@@ -304,6 +304,15 @@ export default class Range {
 		return new TreeWalker( options );
 	}
 
+	/**
+	 * Returns a {@link module:engine/view/node~Node} which is a common ancestor for both positions.
+	 *
+	 * @returns {module:engine/view/node~Node|null}
+	 */
+	getCommonAncestor() {
+		return this.start.getCommonAncestor( this.end );
+	}
+
 	/**
 	 * Returns an iterator that iterates over all {@link module:engine/view/item~Item view items} that are in this range and returns
 	 * them.

+ 42 - 0
packages/ckeditor5-engine/tests/model/position.js

@@ -844,4 +844,46 @@ describe( 'Position', () => {
 			).to.throw( CKEditorError, /model-position-fromjson-no-root/ );
 		} );
 	} );
+
+	describe( 'getCommonAncestor()', () => {
+		it( 'returns null when roots of the position are not the same', () => {
+			const pos1 = new Position( root, [ 1, 1 ] );
+			const pos2 = new Position( otherRoot, [ 1, 1 ] );
+
+			test( pos1, pos2, null );
+		} );
+
+		it( 'for two the same positions returns the parent element', () => {
+			const fPosition = new Position( root, [ 1, 0, 0 ] );
+
+			test( fPosition, fPosition, li1 );
+		} );
+
+		it( 'for two positions in the same parent returns the parent element', () => {
+			const fPosition = new Position( root, [ 1, 0, 0 ] );
+			const zPosition = new Position( root, [ 1, 0, 2 ] );
+
+			test( fPosition, zPosition, li1 );
+		} );
+
+		it( 'for two different positions returns first element which contains both positions', () => {
+			const zPosition = new Position( root, [ 1, 0, 2 ] );
+			const rPosition = new Position( root, [ 1, 1, 2 ] );
+
+			test( rPosition, zPosition, ul );
+		} );
+
+		it( 'works fine with positions hooked in `DocumentFragment`', () => {
+			const docFrag = new DocumentFragment( [ p, ul ] );
+			const zPosition = new Position( docFrag, [ 1, 0, 2 ] );
+			const rPosition = new Position( docFrag, [ 1, 1, 2 ] );
+
+			test( zPosition, rPosition, ul );
+		} );
+
+		function test( positionA, positionB, lca ) {
+			expect( positionA.getCommonAncestor( positionB ) ).to.equal( lca );
+			expect( positionB.getCommonAncestor( positionA ) ).to.equal( lca );
+		}
+	} );
 } );

+ 6 - 0
packages/ckeditor5-engine/tests/model/range.js

@@ -1223,6 +1223,12 @@ describe( 'Range', () => {
 		} );
 	} );
 
+	describe( 'getCommonAncestor()', () => {
+		it( 'should return common ancestor for positions from Range', () => {
+			expect( range.getCommonAncestor() ).to.equal( root );
+		} );
+	} );
+
 	function mapNodesToNames( nodes ) {
 		return nodes.map( node => {
 			return ( node instanceof Element ) ? 'E:' + node.name : 'T:' + node.data;

+ 39 - 0
packages/ckeditor5-engine/tests/view/position.js

@@ -519,4 +519,43 @@ describe( 'Position', () => {
 			document.destroy();
 		} );
 	} );
+
+	describe( 'getCommonAncestor()', () => {
+		let ul, li1, li2, foz, bar;
+
+		beforeEach( () => {
+			foz = new Text( 'foz' );
+			bar = new Text( 'bar' );
+
+			li1 = new Element( 'li', null, foz );
+			li2 = new Element( 'li', null, bar );
+
+			ul = new Element( 'ul', null, [ li1, li2 ] );
+		} );
+
+		it( 'for two the same positions returns the parent element', () => {
+			const fPosition = new Position( li1, 0 );
+
+			test( fPosition, fPosition, li1 );
+		} );
+
+		it( 'for two positions in the same parent returns the parent element', () => {
+			const fPosition = new Position( li1, 0 );
+			const zPosition = new Position( li1, 2 );
+
+			test( fPosition, zPosition, li1 );
+		} );
+
+		it( 'for two different positions returns first element which contains both positions', () => {
+			const zPosition = new Position( li1, 2 );
+			const bPosition = new Position( li2, 0 );
+
+			test( bPosition, zPosition, ul );
+		} );
+
+		function test( positionA, positionB, lca ) {
+			expect( positionA.getCommonAncestor( positionB ) ).to.equal( lca );
+			expect( positionB.getCommonAncestor( positionA ) ).to.equal( lca );
+		}
+	} );
 } );

+ 16 - 0
packages/ckeditor5-engine/tests/view/range.js

@@ -692,4 +692,20 @@ describe( 'Range', () => {
 			} );
 		} );
 	} );
+
+	describe( 'getCommonAncestor()', () => {
+		it( 'should return common ancestor for positions from Range', () => {
+			const foz = new Text( 'foz' );
+			const bar = new Text( 'bar' );
+
+			const li1 = new Element( 'li', null, foz );
+			const li2 = new Element( 'li', null, bar );
+
+			const ul = new Element( 'ul', null, [ li1, li2 ] );
+
+			const range = new Range( new Position( li1, 0 ), new Position( li2, 2 ) );
+
+			expect( range.getCommonAncestor() ).to.equal( ul );
+		} );
+	} );
 } );