8
0
Просмотр исходного кода

Merge pull request #1005 from ckeditor/t/1002

Feature: Introduced `Position#getCommonAncestor( position )` and `Range#getCommonAncestor()` methods for the view and model. Closes #1002.
Piotrek Koszuliński 8 лет назад
Родитель
Сommit
b28aab831a

+ 21 - 0
packages/ckeditor5-engine/src/model/documentfragment.js

@@ -171,6 +171,27 @@ export default class DocumentFragment {
 		return [];
 	}
 
+	/**
+	 * Returns a descendant node by its path relative to this element.
+	 *
+	 *		// <this>a<b>c</b></this>
+	 *		this.getNodeByPath( [ 0 ] );     // -> "a"
+	 *		this.getNodeByPath( [ 1 ] );     // -> <b>
+	 *		this.getNodeByPath( [ 1, 0 ] );  // -> "c"
+	 *
+	 * @param {Array.<Number>} relativePath Path of the node to find, relative to this element.
+	 * @returns {module:engine/model/node~Node|module:engine/model/documentfragment~DocumentFragment}
+	 */
+	getNodeByPath( relativePath ) {
+		let node = this; // eslint-disable-line consistent-this
+
+		for ( const index of relativePath ) {
+			node = node.getChild( index );
+		}
+
+		return node;
+	}
+
 	/**
 	 * Converts offset "position" to index "position".
 	 *

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

@@ -318,6 +318,26 @@ export default class Position {
 		return this.path.slice( 0, diffAt );
 	}
 
+	/**
+	 * Returns an {@link module:engine/model/element~Element} or {@link module:engine/model/documentfragment~DocumentFragment}
+	 * which is a common ancestor of 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/element~Element|module:engine/model/documentfragment~DocumentFragment|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 ];
+	}
+
 	/**
 	 * 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).

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

@@ -445,6 +445,16 @@ export default class Range {
 		return ranges;
 	}
 
+	/**
+	 * Returns an {@link module:engine/model/element~Element} or {@link module:engine/model/documentfragment~DocumentFragment}
+	 * which is a common ancestor of the range's both ends (in which the entire range is contained).
+	 *
+	 * @returns {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment|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.
 	 *

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

@@ -175,6 +175,26 @@ export default class Position {
 		}
 	}
 
+	/**
+	 * Returns a {@link module:engine/view/node~Node} or {@link module:engine/view/documentfragment~DocumentFragment}
+	 * which is a common ancestor of both positions.
+	 *
+	 * @param {module:engine/view/position~Position} position
+	 * @returns {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment|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.
 	 *

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

@@ -304,6 +304,16 @@ export default class Range {
 		return new TreeWalker( options );
 	}
 
+	/**
+	 * Returns a {@link module:engine/view/node~Node} or {@link module:engine/view/documentfragment~DocumentFragment}
+	 * which is a common ancestor of range's both ends (in which the entire range is contained).
+	 *
+	 * @returns {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment|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.

+ 21 - 0
packages/ckeditor5-engine/tests/model/documentfragment.js

@@ -300,4 +300,25 @@ describe( 'DocumentFragment', () => {
 			expect( deserialized.getChild( 1 ).parent ).to.equal( deserialized );
 		} );
 	} );
+
+	describe( 'getNodeByPath', () => {
+		it( 'should return the whole document fragment if path is empty', () => {
+			const frag = new DocumentFragment();
+
+			expect( frag.getNodeByPath( [] ) ).to.equal( frag );
+		} );
+
+		it( 'should return a descendant of this node', () => {
+			const image = new Element( 'image' );
+			const element = new Element( 'elem', [], [
+				new Element( 'elem', [], [
+					new Text( 'foo' ),
+					image
+				] )
+			] );
+			const frag = new DocumentFragment( element );
+
+			expect( frag.getNodeByPath( [ 0, 0, 1 ] ) ).to.equal( image );
+		} );
+	} );
 } );

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

@@ -844,4 +844,74 @@ describe( 'Position', () => {
 			).to.throw( CKEditorError, /model-position-fromjson-no-root/ );
 		} );
 	} );
+
+	describe( 'getCommonAncestor()', () => {
+		it( 'returns null when roots of both positions are not the same', () => {
+			const pos1 = new Position( root, [ 0 ] );
+			const pos2 = new Position( otherRoot, [ 0 ] );
+
+			test( pos1, pos2, null );
+		} );
+
+		it( 'for two the same positions returns the parent element #1', () => {
+			const fPosition = new Position( root, [ 1, 0, 0 ] );
+			const otherPosition = new Position( root, [ 1, 0, 0 ] );
+
+			test( fPosition, otherPosition, li1 );
+		} );
+
+		it( 'for two the same positions returns the parent element #2', () => {
+			const doc = new Document();
+			const root = doc.createRoot();
+
+			const p = new Element( 'p', null, 'foobar' );
+
+			root.appendChildren( p );
+
+			const postion = new Position( root, [ 0, 3 ] ); // <p>foo^bar</p>
+
+			test( postion, postion, p );
+		} );
+
+		it( 'for two positions in the same element returns the element', () => {
+			const fPosition = new Position( root, [ 1, 0, 0 ] );
+			const zPosition = new Position( root, [ 1, 0, 2 ] );
+
+			test( fPosition, zPosition, li1 );
+		} );
+
+		it( 'works when one positions is nested deeper than the other', () => {
+			const zPosition = new Position( root, [ 1, 0, 2 ] );
+			const liPosition = new Position( root, [ 1, 1 ] );
+
+			test( liPosition, zPosition, ul );
+		} );
+
+		// Checks if by mistake someone didn't use getCommonPath() + getNodeByPath().
+		it( 'works if position is located before an element', () => {
+			const doc = new Document();
+			const root = doc.createRoot();
+
+			const p = new Element( 'p', null, new Element( 'a' ) );
+
+			root.appendChildren( p );
+
+			const postion = new Position( root, [ 0, 0 ] ); // <p>^<a></a></p>
+
+			test( postion, postion, p );
+		} );
+
+		it( 'works fine with positions located in DocumentFragment', () => {
+			const docFrag = new DocumentFragment( [ p, ul ] );
+			const zPosition = new Position( docFrag, [ 1, 0, 2 ] );
+			const afterLiPosition = new Position( docFrag, [ 1, 2 ] );
+
+			test( zPosition, afterLiPosition, 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;

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

@@ -519,4 +519,84 @@ describe( 'Position', () => {
 			document.destroy();
 		} );
 	} );
+
+	describe( 'getCommonAncestor()', () => {
+		let div, ul, liUl1, liUl2, texts, section, article, ol, liOl1, liOl2, p;
+
+		// |- div
+		//   |- ul
+		//   |  |- li
+		//   |  |  |- foz
+		//   |  |- li
+		//   |     |- bar
+		//   |- section
+		//      |- Sed id libero at libero tristique
+		//      |- article
+		//      |  |- ol
+		//      |  |  |- li
+		//      |  |  |  |- Lorem ipsum dolor sit amet.
+		//      |  |  |- li
+		//      |  |     |- Mauris tincidunt tincidunt leo ac rutrum.
+		//      |  |- p
+		//      |  |  |- Maecenas accumsan tellus.
+
+		beforeEach( () => {
+			texts = {
+				foz: new Text( 'foz' ),
+				bar: new Text( 'bar' ),
+				lorem: new Text( 'Lorem ipsum dolor sit amet.' ),
+				mauris: new Text( 'Mauris tincidunt tincidunt leo ac rutrum.' ),
+				maecenas: new Text( 'Maecenas accumsan tellus.' ),
+				sed: new Text( 'Sed id libero at libero tristique.' )
+			};
+
+			liUl1 = new Element( 'li', null, texts.foz );
+			liUl2 = new Element( 'li', null, texts.bar );
+			ul = new Element( 'ul', null, [ liUl1, liUl2 ] );
+
+			liOl1 = new Element( 'li', null, texts.lorem );
+			liOl2 = new Element( 'li', null, texts.mauris );
+			ol = new Element( 'ol', null, [ liOl1, liOl2 ] );
+
+			p = new Element( 'p', null, texts.maecenas );
+
+			article = new Element( 'article', null, [ ol, p ] );
+			section = new Element( 'section', null, [ texts.sed, article ] );
+
+			div = new Element( 'div', null, [ ul, section ] );
+		} );
+
+		it( 'for two the same positions returns the parent element', () => {
+			const afterLoremPosition = new Position( liOl1, 5 );
+			const otherPosition = Position.createFromPosition( afterLoremPosition );
+
+			test( afterLoremPosition, otherPosition, liOl1 );
+		} );
+
+		it( 'for two positions in the same element returns the element', () => {
+			const startMaecenasPosition = Position.createAt( liOl2 );
+			const beforeTellusPosition = new Position( liOl2, 18 );
+
+			test( startMaecenasPosition, beforeTellusPosition, liOl2 );
+		} );
+
+		it( 'works when one of the positions is nested deeper than the other #1', () => {
+			const firstPosition = new Position( liUl1, 1 );
+			const secondPosition = new Position( p, 3 );
+
+			test( firstPosition, secondPosition, div );
+		} );
+
+		it( 'works when one of the positions is nested deeper than the other #2', () => {
+			const firstPosition = new Position( liOl2, 10 );
+			const secondPosition = new Position( section, 1 );
+
+			test( firstPosition, secondPosition, section );
+		} );
+
+		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 );
+		} );
+	} );
 } );