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

Commit

Permalink
Merge pull request #1614 from ckeditor/t/ckeditor5-table/126
Browse files Browse the repository at this point in the history
Feature: Introduce `selection.getTopMostBlocks()` method.
  • Loading branch information
scofalik authored Jan 3, 2019
2 parents f041f28 + 6c8c244 commit a9c41c8
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 4 deletions.
22 changes: 21 additions & 1 deletion src/model/documentselection.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 3 additions & 3 deletions src/model/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Expand All @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
47 changes: 47 additions & 0 deletions src/model/selection.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 changes: 68 additions & 0 deletions tests/model/selection.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down

0 comments on commit a9c41c8

Please sign in to comment.