Skip to content
This repository was archived by the owner on Jun 26, 2020. It is now read-only.

Commit a9c41c8

Browse files
authored
Merge pull request #1614 from ckeditor/t/ckeditor5-table/126
Feature: Introduce `selection.getTopMostBlocks()` method.
2 parents f041f28 + 6c8c244 commit a9c41c8

File tree

4 files changed

+139
-4
lines changed

4 files changed

+139
-4
lines changed

src/model/documentselection.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,12 +247,32 @@ export default class DocumentSelection {
247247
* <paragraph>b</paragraph>
248248
* <paragraph>]c</paragraph> // this block will not be returned
249249
*
250-
* @returns {Iterator.<module:engine/model/element~Element>}
250+
* @returns {Iterable.<module:engine/model/element~Element>}
251251
*/
252252
getSelectedBlocks() {
253253
return this._selection.getSelectedBlocks();
254254
}
255255

256+
/**
257+
* Returns blocks that aren't nested in other selected blocks.
258+
*
259+
* In this case the method will return blocks A, B and E because C & D are children of block B:
260+
*
261+
* [<blockA></blockA>
262+
* <blockB>
263+
* <blockC></blockC>
264+
* <blockD></blockD>
265+
* </blockB>
266+
* <blockE></blockE>]
267+
*
268+
* **Note:** To get all selected blocks use {@link #getSelectedBlocks `getSelectedBlocks()`}.
269+
*
270+
* @returns {Iterable.<module:engine/model/element~Element>}
271+
*/
272+
getTopMostBlocks() {
273+
return this._selection.getTopMostBlocks();
274+
}
275+
256276
/**
257277
* Returns the selected element. {@link module:engine/model/element~Element Element} is considered as selected if there is only
258278
* one range in the selection, and that range contains exactly one element.

src/model/schema.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -520,7 +520,7 @@ export default class Schema {
520520
*
521521
* @param {Array.<module:engine/model/range~Range>} ranges Ranges to be validated.
522522
* @param {String} attribute The name of the attribute to check.
523-
* @returns {Iterator.<module:engine/model/range~Range>} Ranges in which the attribute is allowed.
523+
* @returns {Iterable.<module:engine/model/range~Range>} Ranges in which the attribute is allowed.
524524
*/
525525
* getValidRanges( ranges, attribute ) {
526526
ranges = convertToMinimalFlatRanges( ranges );
@@ -539,7 +539,7 @@ export default class Schema {
539539
* @private
540540
* @param {module:engine/model/range~Range} range Range to process.
541541
* @param {String} attribute The name of the attribute to check.
542-
* @returns {Iterator.<module:engine/model/range~Range>} Ranges in which the attribute is allowed.
542+
* @returns {Iterable.<module:engine/model/range~Range>} Ranges in which the attribute is allowed.
543543
*/
544544
* _getValidRangesForRange( range, attribute ) {
545545
let start = range.start;
@@ -1459,7 +1459,7 @@ function* combineWalkers( backward, forward ) {
14591459
// all those minimal flat ranges.
14601460
//
14611461
// @param {Array.<module:engine/model/range~Range>} ranges Ranges to process.
1462-
// @returns {Iterator.<module:engine/model/range~Range>} Minimal flat ranges of given `ranges`.
1462+
// @returns {Iterable.<module:engine/model/range~Range>} Minimal flat ranges of given `ranges`.
14631463
function* convertToMinimalFlatRanges( ranges ) {
14641464
for ( const range of ranges ) {
14651465
yield* range.getMinimalFlatRanges();

src/model/selection.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -672,6 +672,35 @@ export default class Selection {
672672
}
673673
}
674674

675+
/**
676+
* Returns blocks that aren't nested in other selected blocks.
677+
*
678+
* In this case the method will return blocks A, B and E because C & D are children of block B:
679+
*
680+
* [<blockA></blockA>
681+
* <blockB>
682+
* <blockC></blockC>
683+
* <blockD></blockD>
684+
* </blockB>
685+
* <blockE></blockE>]
686+
*
687+
* **Note:** To get all selected blocks use {@link #getSelectedBlocks `getSelectedBlocks()`}.
688+
*
689+
* @returns {Iterable.<module:engine/model/element~Element>}
690+
*/
691+
* getTopMostBlocks() {
692+
const selected = Array.from( this.getSelectedBlocks() );
693+
694+
for ( const block of selected ) {
695+
const parentBlock = findAncestorBlock( block );
696+
697+
// Filter out blocks that are nested in other selected blocks (like paragraphs in tables).
698+
if ( !parentBlock || !selected.includes( parentBlock ) ) {
699+
yield block;
700+
}
701+
}
702+
}
703+
675704
/**
676705
* Checks whether the selection contains the entire content of the given element. This means that selection must start
677706
* 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 ) {
802831

803832
return block;
804833
}
834+
835+
// Returns first ancestor block of a node.
836+
//
837+
// @param {module:engine/model/node~Node} node
838+
// @returns {module:engine/model/node~Node|undefined}
839+
function findAncestorBlock( node ) {
840+
const schema = node.document.model.schema;
841+
842+
let parent = node.parent;
843+
844+
while ( parent ) {
845+
if ( schema.isBlock( parent ) ) {
846+
return parent;
847+
}
848+
849+
parent = parent.parent;
850+
}
851+
}

tests/model/selection.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,6 +1110,74 @@ describe( 'Selection', () => {
11101110
}
11111111
} );
11121112

1113+
describe( 'getTopMostBlocks()', () => {
1114+
beforeEach( () => {
1115+
model.schema.register( 'p', { inheritAllFrom: '$block' } );
1116+
model.schema.register( 'lvl0', { isBlock: true, isLimit: true, isObject: true, allowIn: '$root' } );
1117+
model.schema.register( 'lvl1', { allowIn: 'lvl0', isLimit: true } );
1118+
model.schema.register( 'lvl2', { allowIn: 'lvl1', isObject: true } );
1119+
1120+
model.schema.extend( 'p', { allowIn: 'lvl2' } );
1121+
} );
1122+
1123+
it( 'returns an iterator', () => {
1124+
setData( model, '<p>a</p><p>[]b</p><p>c</p>' );
1125+
1126+
expect( doc.selection.getTopMostBlocks().next ).to.be.a( 'function' );
1127+
} );
1128+
1129+
it( 'returns block for a collapsed selection', () => {
1130+
setData( model, '<p>a</p><p>[]b</p><p>c</p>' );
1131+
1132+
expect( stringifyBlocks( doc.selection.getTopMostBlocks() ) ).to.deep.equal( [ 'p#b' ] );
1133+
} );
1134+
1135+
it( 'returns block for a collapsed selection (empty block)', () => {
1136+
setData( model, '<p>a</p><p>[]</p><p>c</p>' );
1137+
1138+
const blocks = Array.from( doc.selection.getTopMostBlocks() );
1139+
1140+
expect( blocks ).to.have.length( 1 );
1141+
expect( blocks[ 0 ].childCount ).to.equal( 0 );
1142+
} );
1143+
1144+
it( 'returns block for a non collapsed selection', () => {
1145+
setData( model, '<p>a</p><p>[b]</p><p>c</p>' );
1146+
1147+
expect( stringifyBlocks( doc.selection.getTopMostBlocks() ) ).to.deep.equal( [ 'p#b' ] );
1148+
} );
1149+
1150+
it( 'returns two blocks for a non collapsed selection', () => {
1151+
setData( model, '<p>a</p><p>[b</p><p>c]</p><p>d</p>' );
1152+
1153+
expect( stringifyBlocks( doc.selection.getTopMostBlocks() ) ).to.deep.equal( [ 'p#b', 'p#c' ] );
1154+
} );
1155+
1156+
it( 'returns only top most blocks', () => {
1157+
setData( model, '[<p>foo</p><lvl0><lvl1><lvl2><p>bar</p></lvl2></lvl1></lvl0><p>baz</p>]' );
1158+
1159+
expect( stringifyBlocks( doc.selection.getTopMostBlocks() ) ).to.deep.equal( [ 'p#foo', 'lvl0', 'p#baz' ] );
1160+
} );
1161+
1162+
it( 'returns only selected blocks even if nested in other blocks', () => {
1163+
setData( model, '<p>foo</p><lvl0><lvl1><lvl2><p>[b]ar</p></lvl2></lvl1></lvl0><p>baz</p>' );
1164+
1165+
expect( stringifyBlocks( doc.selection.getTopMostBlocks() ) ).to.deep.equal( [ 'p#bar' ] );
1166+
} );
1167+
1168+
// Map all elements to names. If element contains child text node it will be appended to name with '#'.
1169+
function stringifyBlocks( elements ) {
1170+
return Array.from( elements ).map( el => {
1171+
const name = el.name;
1172+
1173+
const firstChild = el.getChild( 0 );
1174+
const hasText = firstChild && firstChild.data;
1175+
1176+
return hasText ? `${ name }#${ firstChild.data }` : name;
1177+
} );
1178+
}
1179+
} );
1180+
11131181
describe( 'attributes interface', () => {
11141182
let rangeInFullP;
11151183

0 commit comments

Comments
 (0)