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

Commit d3e7afa

Browse files
authored
Merge pull request #985 from ckeditor/t/984
Other: The `Selection#getSelectedBlocks()` method will not return a block in which selection ends if no content of that block is selected. Closes #984. For example, in the following case only the first two paragraphs will be returned: ```html <paragraph>[Foo</paragraph> <paragraph>Bar</paragraph> <paragraph>]Baz</paragraph> ``` The reasoning behind this change is that the user doesn't consider the last block as selected in such a case (as its selection isn't even visible).
2 parents e6e92e9 + 7f40fe6 commit d3e7afa

File tree

3 files changed

+108
-31
lines changed

3 files changed

+108
-31
lines changed

src/model/selection.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -576,7 +576,7 @@ export default class Selection {
576576
*
577577
* This method's result can be used for example to apply block styling to all blocks covered by this selection.
578578
*
579-
* *Note:* `getSelectedBlocks` always returns the deepest block.
579+
* **Note:** `getSelectedBlocks()` always returns the deepest block.
580580
*
581581
* In this case the function will return exactly all 3 paragraphs:
582582
*
@@ -590,6 +590,13 @@ export default class Selection {
590590
*
591591
* <paragraph>[]a</paragraph>
592592
*
593+
* **Special case**: If a selection ends at the beginning of a block, that block is not returned as from user perspective
594+
* this block wasn't selected. See [#984](https://github.com/ckeditor/ckeditor5-engine/issues/984) for more details.
595+
*
596+
* <paragraph>[a</paragraph>
597+
* <paragraph>b</paragraph>
598+
* <paragraph>]c</paragraph> // this block will not be returned
599+
*
593600
* @returns {Iterator.<module:engine/model/element~Element>}
594601
*/
595602
* getSelectedBlocks() {
@@ -610,7 +617,8 @@ export default class Selection {
610617

611618
const endBlock = getParentBlock( range.end, visited );
612619

613-
if ( endBlock ) {
620+
// #984. Don't return the end block if the range ends right at its beginning.
621+
if ( endBlock && !range.end.isTouching( Position.createAt( endBlock ) ) ) {
614622
yield endBlock;
615623
}
616624
}

tests/model/position.js

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { jsonParseStringify } from '../../tests/model/_utils/utils';
1515

1616
testUtils.createSinonSandbox();
1717

18-
describe( 'position', () => {
18+
describe( 'Position', () => {
1919
let doc, root, otherRoot, p, ul, li1, li2, f, o, z, b, a, r, foz, bar;
2020

2121
// root
@@ -118,7 +118,7 @@ describe( 'position', () => {
118118
} );
119119
} );
120120

121-
describe( 'createFromParentAndOffset', () => {
121+
describe( 'createFromParentAndOffset()', () => {
122122
it( 'should create positions form node and offset', () => {
123123
expect( Position.createFromParentAndOffset( root, 0 ) ).to.have.property( 'path' ).that.deep.equals( [ 0 ] );
124124
expect( Position.createFromParentAndOffset( root, 1 ) ).to.have.property( 'path' ).that.deep.equals( [ 1 ] );
@@ -149,7 +149,7 @@ describe( 'position', () => {
149149
} );
150150
} );
151151

152-
describe( 'createAt', () => {
152+
describe( 'createAt()', () => {
153153
it( 'should create positions from positions', () => {
154154
const spy = testUtils.sinon.spy( Position, 'createFromPosition' );
155155

@@ -177,7 +177,7 @@ describe( 'position', () => {
177177
} );
178178
} );
179179

180-
describe( 'createBefore', () => {
180+
describe( 'createBefore()', () => {
181181
it( 'should create positions before elements', () => {
182182
expect( Position.createBefore( p ) ).to.have.property( 'path' ).that.deep.equals( [ 0 ] );
183183

@@ -203,7 +203,7 @@ describe( 'position', () => {
203203
} );
204204
} );
205205

206-
describe( 'createAfter', () => {
206+
describe( 'createAfter()', () => {
207207
it( 'should create positions after elements', () => {
208208
expect( Position.createAfter( p ) ).to.have.property( 'path' ).that.deep.equals( [ 1 ] );
209209

@@ -229,7 +229,7 @@ describe( 'position', () => {
229229
} );
230230
} );
231231

232-
describe( 'createFromPosition', () => {
232+
describe( 'createFromPosition()', () => {
233233
it( 'should create a copy of given position', () => {
234234
const original = new Position( root, [ 1, 2, 3 ] );
235235
const position = Position.createFromPosition( original );
@@ -361,7 +361,7 @@ describe( 'position', () => {
361361
expect( position.getParentPath() ).to.deep.equal( [ 1, 2 ] );
362362
} );
363363

364-
describe( 'isBefore', () => {
364+
describe( 'isBefore()', () => {
365365
it( 'should return true if given position has same root and is before this position', () => {
366366
const position = new Position( root, [ 1, 1, 2 ] );
367367
const beforePosition = new Position( root, [ 1, 0 ] );
@@ -384,7 +384,7 @@ describe( 'position', () => {
384384
} );
385385
} );
386386

387-
describe( 'isEqual', () => {
387+
describe( 'isEqual()', () => {
388388
it( 'should return true if given position has same path and root', () => {
389389
const position = new Position( root, [ 1, 1, 2 ] );
390390
const samePosition = new Position( root, [ 1, 1, 2 ] );
@@ -407,7 +407,7 @@ describe( 'position', () => {
407407
} );
408408
} );
409409

410-
describe( 'isAfter', () => {
410+
describe( 'isAfter()', () => {
411411
it( 'should return true if given position has same root and is after this position', () => {
412412
const position = new Position( root, [ 1, 1, 2 ] );
413413
const afterPosition = new Position( root, [ 1, 2 ] );
@@ -430,7 +430,7 @@ describe( 'position', () => {
430430
} );
431431
} );
432432

433-
describe( 'isTouching', () => {
433+
describe( 'isTouching()', () => {
434434
it( 'should return true if positions are same', () => {
435435
const position = new Position( root, [ 1, 1, 1 ] );
436436
const result = position.isTouching( new Position( root, [ 1, 1, 1 ] ) );
@@ -470,6 +470,14 @@ describe( 'position', () => {
470470
expect( positionB.isTouching( positionA ) ).to.be.false;
471471
} );
472472

473+
it( 'should return false if there are whole nodes between positions (same depth)', () => {
474+
const positionA = new Position( root, [ 1, 0 ] );
475+
const positionB = new Position( root, [ 1, 1 ] );
476+
477+
expect( positionA.isTouching( positionB ) ).to.be.false;
478+
expect( positionB.isTouching( positionA ) ).to.be.false;
479+
} );
480+
473481
it( 'should return false if there are whole nodes between positions', () => {
474482
const positionA = new Position( root, [ 1, 0, 3 ] );
475483
const positionB = new Position( root, [ 1, 1, 1 ] );
@@ -487,21 +495,21 @@ describe( 'position', () => {
487495
} );
488496
} );
489497

490-
describe( 'isAtStart', () => {
498+
describe( 'isAtStart()', () => {
491499
it( 'should return true if position is at the beginning of its parent', () => {
492500
expect( new Position( root, [ 0 ] ).isAtStart ).to.be.true;
493501
expect( new Position( root, [ 1 ] ).isAtStart ).to.be.false;
494502
} );
495503
} );
496504

497-
describe( 'isAtEnd', () => {
505+
describe( 'isAtEnd()', () => {
498506
it( 'should return true if position is at the end of its parent', () => {
499507
expect( new Position( root, [ root.maxOffset ] ).isAtEnd ).to.be.true;
500508
expect( new Position( root, [ 0 ] ).isAtEnd ).to.be.false;
501509
} );
502510
} );
503511

504-
describe( 'getAncestors', () => {
512+
describe( 'getAncestors()', () => {
505513
it( 'should return position parent element and it\'s ancestors', () => {
506514
expect( new Position( root, [ 1, 1, 1 ] ).getAncestors() ).to.deep.equal( [ root, ul, li2 ] );
507515
} );
@@ -513,7 +521,7 @@ describe( 'position', () => {
513521
} );
514522
} );
515523

516-
describe( 'getCommonPath', () => {
524+
describe( 'getCommonPath()', () => {
517525
it( 'returns the common part', () => {
518526
const pos1 = new Position( root, [ 1, 0, 0 ] );
519527
const pos2 = new Position( root, [ 1, 0, 1 ] );
@@ -548,7 +556,7 @@ describe( 'position', () => {
548556
} );
549557
} );
550558

551-
describe( 'compareWith', () => {
559+
describe( 'compareWith()', () => {
552560
it( 'should return same if positions are same', () => {
553561
const position = new Position( root, [ 1, 2, 3 ] );
554562
const compared = new Position( root, [ 1, 2, 3 ] );
@@ -578,7 +586,7 @@ describe( 'position', () => {
578586
} );
579587
} );
580588

581-
describe( 'getLastMatchingPosition', () => {
589+
describe( 'getLastMatchingPosition()', () => {
582590
it( 'should skip forward', () => {
583591
let position = new Position( root, [ 1, 0, 0 ] );
584592

@@ -596,7 +604,7 @@ describe( 'position', () => {
596604
} );
597605
} );
598606

599-
describe( '_getTransformedByInsertion', () => {
607+
describe( '_getTransformedByInsertion()', () => {
600608
it( 'should return a new Position instance', () => {
601609
const position = new Position( root, [ 0 ] );
602610
const transformed = position._getTransformedByInsertion( new Position( root, [ 2 ] ), 4, false );
@@ -656,7 +664,7 @@ describe( 'position', () => {
656664
} );
657665
} );
658666

659-
describe( '_getTransformedByDeletion', () => {
667+
describe( '_getTransformedByDeletion()', () => {
660668
it( 'should return a new Position instance', () => {
661669
const position = new Position( root, [ 0 ] );
662670
const transformed = position._getTransformedByDeletion( new Position( root, [ 2 ] ), 4 );
@@ -716,7 +724,7 @@ describe( 'position', () => {
716724
} );
717725
} );
718726

719-
describe( '_getTransformedByMove', () => {
727+
describe( '_getTransformedByMove()', () => {
720728
it( 'should increment offset if a range was moved to the same parent and closer offset', () => {
721729
const position = new Position( root, [ 1, 2, 3 ] );
722730
const transformed = position._getTransformedByMove( new Position( root, [ 2 ] ), new Position( root, [ 1, 2, 0 ] ), 3, false );
@@ -758,7 +766,7 @@ describe( 'position', () => {
758766
} );
759767
} );
760768

761-
describe( '_getCombined', () => {
769+
describe( '_getCombined()', () => {
762770
it( 'should return correct combination of this and given positions', () => {
763771
const position = new Position( root, [ 1, 3, 4, 2 ] );
764772
const sourcePosition = new Position( root, [ 1, 1 ] );
@@ -770,7 +778,7 @@ describe( 'position', () => {
770778
} );
771779
} );
772780

773-
describe( 'getShiftedBy', () => {
781+
describe( 'getShiftedBy()', () => {
774782
it( 'should return a new instance of Position with offset changed by shift value', () => {
775783
const position = new Position( root, [ 1, 2, 3 ] );
776784
const shifted = position.getShiftedBy( 2 );
@@ -795,7 +803,7 @@ describe( 'position', () => {
795803
} );
796804
} );
797805

798-
describe( 'toJSON', () => {
806+
describe( 'toJSON()', () => {
799807
it( 'should serialize position', () => {
800808
const position = new Position( root, [ 0 ] );
801809

@@ -813,7 +821,7 @@ describe( 'position', () => {
813821
} );
814822
} );
815823

816-
describe( 'fromJSON', () => {
824+
describe( 'fromJSON()', () => {
817825
it( 'should create object with given document', () => {
818826
const deserialized = Position.fromJSON( { root: 'main', path: [ 0, 1, 2 ] }, doc );
819827

tests/model/selection.js

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -865,6 +865,7 @@ describe( 'Selection', () => {
865865

866866
doc.schema.registerItem( 'image' );
867867
doc.schema.allow( { name: 'image', inside: '$root' } );
868+
doc.schema.allow( { name: 'image', inside: '$block' } );
868869
doc.schema.allow( { name: '$text', inside: 'image' } );
869870

870871
// Special block which can contain another blocks.
@@ -884,6 +885,15 @@ describe( 'Selection', () => {
884885
expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'b' ] );
885886
} );
886887

888+
it( 'returns block for a collapsed selection (empty block)', () => {
889+
setData( doc, '<p>a</p><p>[]</p><p>c</p>' );
890+
891+
const blocks = Array.from( doc.selection.getSelectedBlocks() );
892+
893+
expect( blocks ).to.have.length( 1 );
894+
expect( blocks[ 0 ].childCount ).to.equal( 0 );
895+
} );
896+
887897
it( 'returns block for a non collapsed selection', () => {
888898
setData( doc, '<p>a</p><p>[b]</p><p>c</p>' );
889899

@@ -896,11 +906,8 @@ describe( 'Selection', () => {
896906
expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'b', 'c' ] );
897907
} );
898908

899-
// This isn't a behaviour that we like (https://github.com/ckeditor/ckeditor5-heading/issues/9) but I don't
900-
// want to work on this issue now, when porting this method from the list/heading features.
901-
// It has to be covered separately.
902-
it( 'returns two blocks for a non collapsed selection if only end of selection is touching a block', () => {
903-
setData( doc, '<p>a</p><h>b[</h><p>]c</p><p>d</p>' );
909+
it( 'returns two blocks for a non collapsed selection (starts at block end)', () => {
910+
setData( doc, '<p>a</p><h>b[</h><p>c]</p><p>d</p>' );
904911

905912
expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'b', 'c' ] );
906913
} );
@@ -980,8 +987,62 @@ describe( 'Selection', () => {
980987
expect( toText( doc.selection.getSelectedBlocks() ) ).to.be.empty;
981988
} );
982989

990+
describe( '#984', () => {
991+
it( 'does not return the last block if none of its content is selected', () => {
992+
setData( doc, '<p>[a</p><p>b</p><p>]c</p>' );
993+
994+
expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'a', 'b' ] );
995+
} );
996+
997+
it( 'returns only the first block for a non collapsed selection if only end of selection is touching a block', () => {
998+
setData( doc, '<p>a</p><h>b[</h><p>]c</p><p>d</p>' );
999+
1000+
expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'b' ] );
1001+
} );
1002+
1003+
it( 'does not return the last block if none of its content is selected (nested case)', () => {
1004+
setData( doc, '<p>[a</p><nestedBlock><nestedBlock>]b</nestedBlock></nestedBlock>' );
1005+
1006+
expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'a' ] );
1007+
} );
1008+
1009+
// Like a super edge case, we can live with this behavior as I don't even know what we could expect here
1010+
// since only the innermost block is considerd a block to return (so the <nB>b...</nB> needs to be ignored).
1011+
it( 'does not return the last block if none of its content is selected (nested case, wrapper with a content)', () => {
1012+
setData( doc, '<p>[a</p><nestedBlock>b<nestedBlock>]c</nestedBlock></nestedBlock>' );
1013+
1014+
expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'a' ] );
1015+
} );
1016+
1017+
it( 'returns the last block if at least one of its child nodes is selected', () => {
1018+
setData( doc, '<p>[a</p><p>b</p><p><image></image>]c</p>' );
1019+
1020+
expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'a', 'b', 'c' ] );
1021+
} );
1022+
1023+
// I needed these last 2 cases to justify the use of isTouching() instead of simple `offset == 0` check.
1024+
it( 'returns the last block if at least one of its child nodes is selected (end in an inline element)', () => {
1025+
setData( doc, '<p>[a</p><p>b</p><p><image>x]</image>c</p>' );
1026+
1027+
expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'a', 'b', 'c' ] );
1028+
} );
1029+
1030+
it(
1031+
'does not return the last block if at least one of its child nodes is selected ' +
1032+
'(end in an inline element, no content selected)',
1033+
() => {
1034+
setData( doc, '<p>[a</p><p>b</p><p><image>]x</image>c</p>' );
1035+
1036+
expect( toText( doc.selection.getSelectedBlocks() ) ).to.deep.equal( [ 'a', 'b' ] );
1037+
}
1038+
);
1039+
} );
1040+
1041+
// Map all elements to data of its first child text node.
9831042
function toText( elements ) {
984-
return Array.from( elements ).map( el => el.getChild( 0 ).data );
1043+
return Array.from( elements ).map( el => {
1044+
return Array.from( el.getChildren() ).find( child => child.data ).data;
1045+
} );
9851046
}
9861047
} );
9871048

0 commit comments

Comments
 (0)