瀏覽代碼

Refactor the selection.getSelectedBlocks().

Maciej Gołaszewski 6 年之前
父節點
當前提交
c7961ae057
共有 2 個文件被更改,包括 68 次插入107 次删除
  1. 21 17
      packages/ckeditor5-engine/src/model/selection.js
  2. 47 90
      packages/ckeditor5-engine/tests/model/selection.js

+ 21 - 17
packages/ckeditor5-engine/src/model/selection.js

@@ -667,19 +667,23 @@ export default class Selection {
 	 *
 	 * @returns {Iterable.<module:engine/model/element~Element>}
 	 */
-	* getBlocks() {
+	* getSelectedBlocks() {
 		const visited = new WeakSet();
 
 		for ( const range of this.getRanges() ) {
 			const startBlock = getParentBlock( range.start, visited );
 
 			if ( startBlock ) {
-				yield startBlock;
+				if ( checkBlock( startBlock, range ) ) {
+					yield startBlock;
+				}
 			}
 
 			for ( const value of range.getWalker() ) {
 				if ( value.type == 'elementEnd' && isUnvisitedBlockContainer( value.item, visited ) ) {
-					yield value.item;
+					if ( checkBlock( value.item, range ) ) {
+						yield value.item;
+					}
 				}
 			}
 
@@ -687,7 +691,9 @@ export default class Selection {
 
 			// #984. Don't return the end block if the range ends right at its beginning.
 			if ( endBlock && !range.end.isTouching( Position._createAt( endBlock, 0 ) ) ) {
-				yield endBlock;
+				if ( checkBlock( endBlock, range ) ) {
+					yield endBlock;
+				}
 			}
 		}
 	}
@@ -708,19 +714,6 @@ export default class Selection {
 	 *
 	 * @returns {Iterable.<module:engine/model/element~Element>}
 	 */
-	* getSelectedBlocks( config = { deep: false } ) {
-		const selected = Array.from( this.getBlocks() );
-		const { deep } = config;
-
-		for ( const block of selected ) {
-			const parentBlock = findAncestorBlock( block );
-
-			// Filter out blocks that are nested in other selected blocks (like paragraphs in tables).
-			if ( deep || ( !parentBlock || !selected.includes( parentBlock ) ) ) {
-				yield block;
-			}
-		}
-	}
 
 	/**
 	 * Checks whether the selection contains the entire content of the given element. This means that selection must start
@@ -902,3 +895,14 @@ function findAncestorBlock( node ) {
  *     null
  * } module:engine/model/selection~Selectable
  */
+
+function checkBlock( block, range ) {
+	const parent = findAncestorBlock( block );
+
+	if ( parent === block || !parent ) {
+		return true;
+	}
+
+	// Check if element is nested in other block from a range.
+	return !range.containsRange( Range._createOn( parent ) );
+}

+ 47 - 90
packages/ckeditor5-engine/tests/model/selection.js

@@ -950,6 +950,12 @@ describe( 'Selection', () => {
 			// Special block which can contain another blocks.
 			model.schema.register( 'nestedBlock', { inheritAllFrom: '$block' } );
 			model.schema.extend( 'nestedBlock', { allowIn: '$block' } );
+
+			model.schema.register( 'table', { isBlock: true, isLimit: true, isObject: true, allowIn: '$root' } );
+			model.schema.register( 'tableRow', { allowIn: 'table', isLimit: true } );
+			model.schema.register( 'tableCell', { allowIn: 'tableRow', isObject: true } );
+
+			model.schema.extend( 'p', { allowIn: 'tableCell' } );
 		} );
 
 		it( 'returns an iterator', () => {
@@ -961,7 +967,7 @@ describe( 'Selection', () => {
 		it( 'returns block for a collapsed selection', () => {
 			setData( model, '<p>a</p><p>[]b</p><p>c</p>' );
 
-			expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'b' ] );
+			expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'p#b' ] );
 		} );
 
 		it( 'returns block for a collapsed selection (empty block)', () => {
@@ -976,86 +982,89 @@ describe( 'Selection', () => {
 		it( 'returns block for a non collapsed selection', () => {
 			setData( model, '<p>a</p><p>[b]</p><p>c</p>' );
 
-			expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'b' ] );
+			expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'p#b' ] );
 		} );
 
 		it( 'returns two blocks for a non collapsed selection', () => {
 			setData( model, '<p>a</p><h>[b</h><p>c]</p><p>d</p>' );
 
-			expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'b', 'c' ] );
+			expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'h#b', 'p#c' ] );
 		} );
 
 		it( 'returns two blocks for a non collapsed selection (starts at block end)', () => {
 			setData( model, '<p>a</p><h>b[</h><p>c]</p><p>d</p>' );
 
-			expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'b', 'c' ] );
+			expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'h#b', 'p#c' ] );
 		} );
 
 		it( 'returns proper block for a multi-range selection', () => {
 			setData( model, '<p>a</p><h>[b</h><p>c]</p><p>d</p><p>[e]</p>' );
 
-			expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'b', 'c', 'e' ] );
+			expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'h#b', 'p#c', 'p#e' ] );
 		} );
 
 		it( 'does not return a block twice if two ranges are anchored in it', () => {
 			setData( model, '<p>[a]b[c]</p>' );
 
-			expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'abc' ] );
+			expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'p#abc' ] );
 		} );
 
 		it( 'returns only blocks', () => {
 			setData( model, '<p>[a</p><image>b</image><p>c]</p>' );
 
-			expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'a', 'c' ] );
+			expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'p#a', 'p#c' ] );
 		} );
 
 		it( 'gets deeper into the tree', () => {
 			setData( model, '<p>[a</p><blockquote><p>b</p><p>c</p></blockquote><p>d]</p>' );
 
-			expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'a', 'b', 'c', 'd' ] );
+			expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) )
+				.to.deep.equal( [ 'p#a', 'p#b', 'p#c', 'p#d' ] );
 		} );
 
 		it( 'gets deeper into the tree (end deeper)', () => {
 			setData( model, '<p>[a</p><blockquote><p>b]</p><p>c</p></blockquote><p>d</p>' );
 
-			expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'a', 'b' ] );
+			expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) )
+				.to.deep.equal( [ 'p#a', 'p#b' ] );
 		} );
 
 		it( 'gets deeper into the tree (start deeper)', () => {
 			setData( model, '<p>a</p><blockquote><p>b</p><p>[c</p></blockquote><p>d]</p>' );
 
-			expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'c', 'd' ] );
+			expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) )
+				.to.deep.equal( [ 'p#c', 'p#d' ] );
 		} );
 
 		it( 'returns an empty array if none of the selected elements is a block', () => {
 			setData( model, '<p>a</p><image>[a</image><image>b]</image><p>b</p>' );
 
-			expect( toText( doc.selection.getSelectedBlocks() ) ).to.be.empty;
+			expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.be.empty;
 		} );
 
 		it( 'returns an empty array if the selected element is not a block', () => {
 			setData( model, '<p>a</p><image>[]a</image><p>b</p>' );
 
-			expect( toText( doc.selection.getSelectedBlocks() ) ).to.be.empty;
+			expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.be.empty;
 		} );
 
 		// Super edge case – should not happen (blocks should never be nested),
 		// but since the code handles it already it's worth testing.
-		it( 'returns only the lowest block if blocks are nested', () => {
+		it( 'returns only the lowest block if blocks are nested (case #1)', () => {
 			setData( model, '<nestedBlock>a<nestedBlock>[]b</nestedBlock></nestedBlock>' );
 
-			expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'b' ] );
+			expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'nestedBlock#b' ] );
 		} );
 
 		// Like above but trickier.
-		it( 'returns only the lowest block if blocks are nested', () => {
+		it( 'returns only the lowest block if blocks are nested (case #2)', () => {
 			setData(
 				model,
 				'<nestedBlock>a<nestedBlock>[b</nestedBlock></nestedBlock>' +
 				'<nestedBlock>c<nestedBlock>d]</nestedBlock></nestedBlock>'
 			);
 
-			expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'b', 'd' ] );
+			expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'nestedBlock#b', 'nestedBlock#d' ] );
 		} );
 
 		it( 'returns nothing if directly in a root', () => {
@@ -1063,26 +1072,26 @@ describe( 'Selection', () => {
 
 			setData( model, 'a[b]c', { rootName: 'inlineOnlyRoot' } );
 
-			expect( toText( doc.selection.getSelectedBlocks() ) ).to.be.empty;
+			expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.be.empty;
 		} );
 
 		describe( '#984', () => {
 			it( 'does not return the last block if none of its content is selected', () => {
 				setData( model, '<p>[a</p><p>b</p><p>]c</p>' );
 
-				expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'a', 'b' ] );
+				expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'p#a', 'p#b' ] );
 			} );
 
 			it( 'returns only the first block for a non collapsed selection if only end of selection is touching a block', () => {
 				setData( model, '<p>a</p><h>b[</h><p>]c</p><p>d</p>' );
 
-				expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'b' ] );
+				expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'h#b' ] );
 			} );
 
 			it( 'does not return the last block if none of its content is selected (nested case)', () => {
 				setData( model, '<p>[a</p><nestedBlock><nestedBlock>]b</nestedBlock></nestedBlock>' );
 
-				expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'a' ] );
+				expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'p#a' ] );
 			} );
 
 			// Like a super edge case, we can live with this behavior as I don't even know what we could expect here
@@ -1090,20 +1099,20 @@ describe( 'Selection', () => {
 			it( 'does not return the last block if none of its content is selected (nested case, wrapper with a content)', () => {
 				setData( model, '<p>[a</p><nestedBlock>b<nestedBlock>]c</nestedBlock></nestedBlock>' );
 
-				expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'a' ] );
+				expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'p#a' ] );
 			} );
 
 			it( 'returns the last block if at least one of its child nodes is selected', () => {
 				setData( model, '<p>[a</p><p>b</p><p><image></image>]c</p>' );
 
-				expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'a', 'b', 'c' ] );
+				expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'p#a', 'p#b', 'p#c' ] );
 			} );
 
 			// I needed these last 2 cases to justify the use of isTouching() instead of simple `offset == 0` check.
 			it( 'returns the last block if at least one of its child nodes is selected (end in an inline element)', () => {
 				setData( model, '<p>[a</p><p>b</p><p><image>x]</image>c</p>' );
 
-				expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'a', 'b', 'c' ] );
+				expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'p#a', 'p#b', 'p#c' ] );
 			} );
 
 			it(
@@ -1112,86 +1121,29 @@ describe( 'Selection', () => {
 				() => {
 					setData( model, '<p>[a</p><p>b</p><p><image>]x</image>c</p>' );
 
-					expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'a', 'b' ] );
+					expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'p#a', 'p#b' ] );
 				}
 			);
 		} );
 
-		it( 'does not go beyond limit elements', () => {
-			model.schema.register( 'table', { isBlock: true, isLimit: true, isObject: true, allowIn: '$root' } );
-			model.schema.register( 'tableRow', { allowIn: 'table', isLimit: true } );
-			model.schema.register( 'tableCell', { allowIn: 'tableRow', isObject: true } );
-
+		it( 'does not go cross limit elements', () => {
 			model.schema.register( 'blk', { allowIn: [ '$root', 'tableCell' ], isObject: true, isBlock: true } );
 
-			model.schema.extend( 'p', { allowIn: 'tableCell' } );
-
 			setData( model, '<table><tableRow><tableCell><p>foo</p>[<blk></blk><p>bar]</p></tableCell></tableRow></table>' );
 
-			expect( stringifyBlocks( doc.selection.getTopMostBlocks() ) ).to.deep.equal( [ 'blk', 'p#bar' ] );
-		} );
-
-		// Map all elements to data of its first child text node.
-		function toText( elements ) {
-			return Array.from( elements ).map( el => {
-				return Array.from( el.getChildren() ).find( child => child.data ).data;
-			} );
-		}
-	} );
-
-	describe( 'getTopMostBlocks()', () => {
-		beforeEach( () => {
-			model.schema.register( 'p', { inheritAllFrom: '$block' } );
-			model.schema.register( 'table', { isBlock: true, isLimit: true, isObject: true, allowIn: '$root' } );
-			model.schema.register( 'tableRow', { allowIn: 'table', isLimit: true } );
-			model.schema.register( 'tableCell', { allowIn: 'tableRow', isObject: true } );
-
-			model.schema.extend( 'p', { allowIn: 'tableCell' } );
-		} );
-
-		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' ] );
+			expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'blk', 'p#bar' ] );
 		} );
 
 		it( 'returns only top most blocks', () => {
-			setData( model, '[<p>foo</p><table><tableRow><tableCell><p>bar</p></tableCell></tableRow></table><p>baz</p>]' );
+			setData( model, '<p>[foo</p><table><tableRow><tableCell><p>bar</p></tableCell></tableRow></table><p>baz]</p>' );
 
-			expect( stringifyBlocks( doc.selection.getTopMostBlocks() ) ).to.deep.equal( [ 'p#foo', 'table', 'p#baz' ] );
+			expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'p#foo', 'table', 'p#baz' ] );
 		} );
 
 		it( 'returns only selected blocks even if nested in other blocks', () => {
 			setData( model, '<p>foo</p><table><tableRow><tableCell><p>[b]ar</p></tableCell></tableRow></table><p>baz</p>' );
 
-			expect( stringifyBlocks( doc.selection.getTopMostBlocks() ) ).to.deep.equal( [ 'p#bar' ] );
+			expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'p#bar' ] );
 		} );
 
 		it( 'returns only selected blocks even if nested in other blocks (selection on the block)', () => {
@@ -1199,7 +1151,7 @@ describe( 'Selection', () => {
 
 			setData( model, '<table><tableRow><tableCell><p>foo</p>[<blk></blk><p>bar]</p></tableCell></tableRow></table>' );
 
-			expect( stringifyBlocks( doc.selection.getTopMostBlocks() ) ).to.deep.equal( [ 'blk', 'p#bar' ] );
+			expect( stringifyBlocks( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'blk', 'p#bar' ] );
 		} );
 	} );
 
@@ -1382,10 +1334,15 @@ describe( 'Selection', () => {
 		return Array.from( elements ).map( el => {
 			const name = el.name;
 
-			const firstChild = el.getChild( 0 );
-			const hasText = firstChild && firstChild.data;
+			let innerText = '';
+
+			for ( const child of el.getChildren() ) {
+				if ( child.is( 'text' ) ) {
+					innerText += child.data;
+				}
+			}
 
-			return hasText ? `${ name }#${ firstChild.data }` : name;
+			return innerText.length ? `${ name }#${ innerText }` : name;
 		} );
 	}
 } );