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

Commit c208ce1

Browse files
authored
Merge pull request #1834 from ckeditor/i/6501
Fix: Intersecting ranges resulting when fixing graveyard selection no longer break the editor. Closes ckeditor/ckeditor5#6501. Closes ckeditor/ckeditor5#6382.
2 parents 08e8294 + c0364a2 commit c208ce1

File tree

3 files changed

+77
-7
lines changed

3 files changed

+77
-7
lines changed

src/model/documentselection.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1137,14 +1137,16 @@ class LiveSelection extends Selection {
11371137
liveRange.detach();
11381138

11391139
// If nearest valid selection range has been found - add it in the place of old range.
1140-
if ( selectionRange ) {
1140+
// If range is equal to any other selection ranges then it is probably due to contents
1141+
// of a multi-range selection being removed. See ckeditor/ckeditor5#6501.
1142+
if ( selectionRange && !isRangeCollidingWithSelection( selectionRange, this ) ) {
11411143
// Check the range, convert it to live range, bind events, etc.
11421144
const newRange = this._prepareRange( selectionRange );
11431145

11441146
// Add new range in the place of old range.
11451147
this._ranges.splice( index, 0, newRange );
11461148
}
1147-
// If nearest valid selection range cannot be found - just removing the old range is fine.
1149+
// If nearest valid selection range cannot be found or is intersecting with other selection ranges removing the old range is fine.
11481150
}
11491151
}
11501152

@@ -1164,7 +1166,6 @@ function getAttrsIfCharacter( node ) {
11641166

11651167
// Removes selection attributes from element which is not empty anymore.
11661168
//
1167-
// @private
11681169
// @param {module:engine/model/model~Model} model
11691170
// @param {module:engine/model/batch~Batch} batch
11701171
function clearAttributesStoredInElement( model, batch ) {
@@ -1190,3 +1191,8 @@ function clearAttributesStoredInElement( model, batch ) {
11901191
}
11911192
}
11921193
}
1194+
1195+
// Checks if range collides with any of selection ranges.
1196+
function isRangeCollidingWithSelection( range, selection ) {
1197+
return !selection._ranges.every( selectionRange => !range.isEqual( selectionRange ) );
1198+
}

tests/model/documentselection.js

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1710,7 +1710,7 @@ describe( 'DocumentSelection', () => {
17101710
} );
17111711

17121712
describe( 'MoveOperation to graveyard', () => {
1713-
it( 'fix selection range if it ends up in graveyard #1', () => {
1713+
it( 'fix selection range if it ends up in graveyard - collapsed selection', () => {
17141714
selection._setTo( new Position( root, [ 1, 3 ] ) );
17151715

17161716
model.applyOperation(
@@ -1725,7 +1725,7 @@ describe( 'DocumentSelection', () => {
17251725
expect( selection.getFirstPosition().path ).to.deep.equal( [ 1, 2 ] );
17261726
} );
17271727

1728-
it( 'fix selection range if it ends up in graveyard #2', () => {
1728+
it( 'fix selection range if it ends up in graveyard - text from non-collapsed selection is moved', () => {
17291729
selection._setTo( [ new Range( new Position( root, [ 1, 2 ] ), new Position( root, [ 1, 4 ] ) ) ] );
17301730

17311731
model.applyOperation(
@@ -1740,7 +1740,7 @@ describe( 'DocumentSelection', () => {
17401740
expect( selection.getFirstPosition().path ).to.deep.equal( [ 1, 2 ] );
17411741
} );
17421742

1743-
it( 'fix selection range if it ends up in graveyard #3', () => {
1743+
it( 'fix selection range if it ends up in graveyard - parent of non-collapsed selection is moved', () => {
17441744
selection._setTo( [ new Range( new Position( root, [ 1, 1 ] ), new Position( root, [ 1, 2 ] ) ) ] );
17451745

17461746
model.applyOperation(
@@ -1755,7 +1755,7 @@ describe( 'DocumentSelection', () => {
17551755
expect( selection.getFirstPosition().path ).to.deep.equal( [ 0, 6 ] );
17561756
} );
17571757

1758-
it( 'fix selection range if it ends up in graveyard #4 - whole content removed', () => {
1758+
it( 'fix selection range if it ends up in graveyard - whole content removed', () => {
17591759
model.applyOperation(
17601760
new MoveOperation(
17611761
new Position( root, [ 0 ] ),
@@ -1778,6 +1778,63 @@ describe( 'DocumentSelection', () => {
17781778
// Now it's clear that it's the default range.
17791779
expect( selection.getFirstPosition().path ).to.deep.equal( [ 0, 0 ] );
17801780
} );
1781+
1782+
it( 'handles multi-range selection in a text node by merging it into one range (resulting in collapsed ranges)', () => {
1783+
const ranges = [
1784+
new Range( new Position( root, [ 1, 1 ] ), new Position( root, [ 1, 2 ] ) ),
1785+
new Range( new Position( root, [ 1, 3 ] ), new Position( root, [ 1, 4 ] ) )
1786+
];
1787+
1788+
selection._setTo( ranges );
1789+
1790+
model.applyOperation(
1791+
new MoveOperation(
1792+
new Position( root, [ 1, 1 ] ),
1793+
4,
1794+
new Position( doc.graveyard, [ 0 ] ),
1795+
doc.version
1796+
)
1797+
);
1798+
1799+
expect( selection.rangeCount ).to.equal( 1 );
1800+
expect( selection.getFirstPosition().path ).to.deep.equal( [ 1, 1 ] );
1801+
expect( selection.getLastPosition().path ).to.deep.equal( [ 1, 1 ] );
1802+
} );
1803+
1804+
it( 'handles multi-range selection on object nodes by merging it into one range (resulting in non-collapsed ranges)', () => {
1805+
model.schema.register( 'outer', {
1806+
isObject: true
1807+
} );
1808+
model.schema.register( 'inner', {
1809+
isObject: true,
1810+
allowIn: 'outer'
1811+
} );
1812+
1813+
root._removeChildren( 0, root.childCount );
1814+
root._insertChild( 0, [
1815+
new Element( 'outer', [], [ new Element( 'inner' ), new Element( 'inner' ), new Element( 'inner' ) ] )
1816+
] );
1817+
1818+
const ranges = [
1819+
new Range( new Position( root, [ 0, 0 ] ), new Position( root, [ 0, 1 ] ) ),
1820+
new Range( new Position( root, [ 0, 1 ] ), new Position( root, [ 0, 2 ] ) )
1821+
];
1822+
1823+
selection._setTo( ranges );
1824+
1825+
model.applyOperation(
1826+
new MoveOperation(
1827+
new Position( root, [ 0, 0 ] ),
1828+
2,
1829+
new Position( doc.graveyard, [ 0 ] ),
1830+
doc.version
1831+
)
1832+
);
1833+
1834+
expect( selection.rangeCount ).to.equal( 1 );
1835+
expect( selection.getFirstPosition().path ).to.deep.equal( [ 0, 0 ] );
1836+
expect( selection.getLastPosition().path ).to.deep.equal( [ 0, 1 ] );
1837+
} );
17811838
} );
17821839

17831840
it( '`DocumentSelection#change:range` event should be fire once even if selection contains multi-ranges', () => {

tests/model/range.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,13 @@ describe( 'Range', () => {
156156
const otherRange = new Range( new Position( otherRoot, [ 0 ] ), new Position( otherRoot, [ 1, 4 ] ) );
157157
expect( range.isIntersecting( otherRange ) ).to.be.false;
158158
} );
159+
160+
it( 'should return false if ranges are collapsed and equal', () => {
161+
range = new Range( new Position( root, [ 1, 1 ] ) );
162+
const otherRange = new Range( new Position( otherRoot, [ 1, 1 ] ) );
163+
164+
expect( range.isIntersecting( otherRange ) ).to.be.false;
165+
} );
159166
} );
160167

161168
describe( 'static constructors', () => {

0 commit comments

Comments
 (0)