Ver código fonte

Merge pull request #1614 from ckeditor/t/ckeditor5-table/126

Feature: Introduce `selection.getTopMostBlocks()` method.
Szymon Cofalik 7 anos atrás
pai
commit
25f168e0f3

+ 21 - 1
packages/ckeditor5-engine/src/model/documentselection.js

@@ -247,12 +247,32 @@ export default class DocumentSelection {
 	 *		<paragraph>b</paragraph>
 	 *		<paragraph>]c</paragraph> // this block will not be returned
 	 *
-	 * @returns {Iterator.<module:engine/model/element~Element>}
+	 * @returns {Iterable.<module:engine/model/element~Element>}
 	 */
 	getSelectedBlocks() {
 		return this._selection.getSelectedBlocks();
 	}
 
+	/**
+	 * Returns blocks that aren't nested in other selected blocks.
+	 *
+	 * In this case the method will return blocks A, B and E because C & D are children of block B:
+	 *
+	 *		[<blockA></blockA>
+	 *		<blockB>
+	 *			<blockC></blockC>
+	 *			<blockD></blockD>
+	 *		</blockB>
+	 *		<blockE></blockE>]
+	 *
+	 * **Note:** To get all selected blocks use {@link #getSelectedBlocks `getSelectedBlocks()`}.
+	 *
+	 * @returns {Iterable.<module:engine/model/element~Element>}
+	 */
+	getTopMostBlocks() {
+		return this._selection.getTopMostBlocks();
+	}
+
 	/**
 	 * Returns the selected element. {@link module:engine/model/element~Element Element} is considered as selected if there is only
 	 * one range in the selection, and that range contains exactly one element.

+ 3 - 3
packages/ckeditor5-engine/src/model/schema.js

@@ -520,7 +520,7 @@ export default class Schema {
 	 *
 	 * @param {Array.<module:engine/model/range~Range>} ranges Ranges to be validated.
 	 * @param {String} attribute The name of the attribute to check.
-	 * @returns {Iterator.<module:engine/model/range~Range>} Ranges in which the attribute is allowed.
+	 * @returns {Iterable.<module:engine/model/range~Range>} Ranges in which the attribute is allowed.
 	 */
 	* getValidRanges( ranges, attribute ) {
 		ranges = convertToMinimalFlatRanges( ranges );
@@ -539,7 +539,7 @@ export default class Schema {
 	 * @private
 	 * @param {module:engine/model/range~Range} range Range to process.
 	 * @param {String} attribute The name of the attribute to check.
-	 * @returns {Iterator.<module:engine/model/range~Range>} Ranges in which the attribute is allowed.
+	 * @returns {Iterable.<module:engine/model/range~Range>} Ranges in which the attribute is allowed.
 	 */
 	* _getValidRangesForRange( range, attribute ) {
 		let start = range.start;
@@ -1459,7 +1459,7 @@ function* combineWalkers( backward, forward ) {
 // all those minimal flat ranges.
 //
 // @param {Array.<module:engine/model/range~Range>} ranges Ranges to process.
-// @returns {Iterator.<module:engine/model/range~Range>} Minimal flat ranges of given `ranges`.
+// @returns {Iterable.<module:engine/model/range~Range>} Minimal flat ranges of given `ranges`.
 function* convertToMinimalFlatRanges( ranges ) {
 	for ( const range of ranges ) {
 		yield* range.getMinimalFlatRanges();

+ 47 - 0
packages/ckeditor5-engine/src/model/selection.js

@@ -672,6 +672,35 @@ export default class Selection {
 		}
 	}
 
+	/**
+	 * Returns blocks that aren't nested in other selected blocks.
+	 *
+	 * In this case the method will return blocks A, B and E because C & D are children of block B:
+	 *
+	 *		[<blockA></blockA>
+	 *		<blockB>
+	 *			<blockC></blockC>
+	 *			<blockD></blockD>
+	 *		</blockB>
+	 *		<blockE></blockE>]
+	 *
+	 * **Note:** To get all selected blocks use {@link #getSelectedBlocks `getSelectedBlocks()`}.
+	 *
+	 * @returns {Iterable.<module:engine/model/element~Element>}
+	 */
+	* getTopMostBlocks() {
+		const selected = Array.from( this.getSelectedBlocks() );
+
+		for ( const block of selected ) {
+			const parentBlock = findAncestorBlock( block );
+
+			// Filter out blocks that are nested in other selected blocks (like paragraphs in tables).
+			if ( !parentBlock || !selected.includes( parentBlock ) ) {
+				yield block;
+			}
+		}
+	}
+
 	/**
 	 * Checks whether the selection contains the entire content of the given element. This means that selection must start
 	 * at a position {@link module:engine/model/position~Position#isTouching touching} the element's start and ends at position
@@ -802,3 +831,21 @@ function getParentBlock( position, visited ) {
 
 	return block;
 }
+
+// Returns first ancestor block of a node.
+//
+// @param {module:engine/model/node~Node} node
+// @returns {module:engine/model/node~Node|undefined}
+function findAncestorBlock( node ) {
+	const schema = node.document.model.schema;
+
+	let parent = node.parent;
+
+	while ( parent ) {
+		if ( schema.isBlock( parent ) ) {
+			return parent;
+		}
+
+		parent = parent.parent;
+	}
+}

+ 68 - 0
packages/ckeditor5-engine/tests/model/selection.js

@@ -1110,6 +1110,74 @@ describe( 'Selection', () => {
 		}
 	} );
 
+	describe( 'getTopMostBlocks()', () => {
+		beforeEach( () => {
+			model.schema.register( 'p', { inheritAllFrom: '$block' } );
+			model.schema.register( 'lvl0', { isBlock: true, isLimit: true, isObject: true, allowIn: '$root' } );
+			model.schema.register( 'lvl1', { allowIn: 'lvl0', isLimit: true } );
+			model.schema.register( 'lvl2', { allowIn: 'lvl1', isObject: true } );
+
+			model.schema.extend( 'p', { allowIn: 'lvl2' } );
+		} );
+
+		it( 'returns an iterator', () => {
+			setData( model, '<p>a</p><p>[]b</p><p>c</p>' );
+
+			expect( doc.selection.getTopMostBlocks().next ).to.be.a( 'function' );
+		} );
+
+		it( 'returns block for a collapsed selection', () => {
+			setData( model, '<p>a</p><p>[]b</p><p>c</p>' );
+
+			expect( stringifyBlocks( doc.selection.getTopMostBlocks() ) ).to.deep.equal( [ 'p#b' ] );
+		} );
+
+		it( 'returns block for a collapsed selection (empty block)', () => {
+			setData( model, '<p>a</p><p>[]</p><p>c</p>' );
+
+			const blocks = Array.from( doc.selection.getTopMostBlocks() );
+
+			expect( blocks ).to.have.length( 1 );
+			expect( blocks[ 0 ].childCount ).to.equal( 0 );
+		} );
+
+		it( 'returns block for a non collapsed selection', () => {
+			setData( model, '<p>a</p><p>[b]</p><p>c</p>' );
+
+			expect( stringifyBlocks( doc.selection.getTopMostBlocks() ) ).to.deep.equal( [ 'p#b' ] );
+		} );
+
+		it( 'returns two blocks for a non collapsed selection', () => {
+			setData( model, '<p>a</p><p>[b</p><p>c]</p><p>d</p>' );
+
+			expect( stringifyBlocks( doc.selection.getTopMostBlocks() ) ).to.deep.equal( [ 'p#b', 'p#c' ] );
+		} );
+
+		it( 'returns only top most blocks', () => {
+			setData( model, '[<p>foo</p><lvl0><lvl1><lvl2><p>bar</p></lvl2></lvl1></lvl0><p>baz</p>]' );
+
+			expect( stringifyBlocks( doc.selection.getTopMostBlocks() ) ).to.deep.equal( [ 'p#foo', 'lvl0', 'p#baz' ] );
+		} );
+
+		it( 'returns only selected blocks even if nested in other blocks', () => {
+			setData( model, '<p>foo</p><lvl0><lvl1><lvl2><p>[b]ar</p></lvl2></lvl1></lvl0><p>baz</p>' );
+
+			expect( stringifyBlocks( doc.selection.getTopMostBlocks() ) ).to.deep.equal( [ 'p#bar' ] );
+		} );
+
+		// Map all elements to names. If element contains child text node it will be appended to name with '#'.
+		function stringifyBlocks( elements ) {
+			return Array.from( elements ).map( el => {
+				const name = el.name;
+
+				const firstChild = el.getChild( 0 );
+				const hasText = firstChild && firstChild.data;
+
+				return hasText ? `${ name }#${ firstChild.data }` : name;
+			} );
+		}
+	} );
+
 	describe( 'attributes interface', () => {
 		let rangeInFullP;