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

Commit e08b019

Browse files
authored
Merge pull request #879 from ckeditor/t/877
Fix: Live ranges, selections and markers no longer lose content when using the move delta. Closes #877. The base algorithm implemented in `Range#_getTransformedByDocumentChange()` will now include all model items between the old and new range boundary. See https://github.com/ckeditor/ckeditor5-engine/issues/877#issuecomment-287740021 for more details.
2 parents 79c2bfe + 3f4ef3e commit e08b019

File tree

5 files changed

+125
-59
lines changed

5 files changed

+125
-59
lines changed

src/model/delta/basic-transformations.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ addTransformationCase( MarkerDelta, SplitDelta, transformMarkerDelta );
111111
addTransformationCase( MarkerDelta, MergeDelta, transformMarkerDelta );
112112
addTransformationCase( MarkerDelta, WrapDelta, transformMarkerDelta );
113113
addTransformationCase( MarkerDelta, UnwrapDelta, transformMarkerDelta );
114+
addTransformationCase( MarkerDelta, MoveDelta, transformMarkerDelta );
115+
addTransformationCase( MarkerDelta, RenameDelta, transformMarkerDelta );
114116

115117
// Add special case for MoveDelta x MergeDelta transformation.
116118
addTransformationCase( MoveDelta, MergeDelta, ( a, b, isStrong ) => {

src/model/range.js

Lines changed: 31 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -447,61 +447,43 @@ export default class Range {
447447
* @returns {Array.<module:engine/model/range~Range>}
448448
*/
449449
_getTransformedByDocumentChange( type, deltaType, targetPosition, howMany, sourcePosition ) {
450-
// IMPORTANT! Every special case added here has to be reflected in MarkerDelta transformations!
451-
// Check /src/model/delta/basic-transformations.js.
452450
if ( type == 'insert' ) {
453451
return this._getTransformedByInsertion( targetPosition, howMany, false, false );
454452
} else {
455-
const ranges = this._getTransformedByMove( sourcePosition, targetPosition, howMany );
456-
457-
// Don't ask. Just debug.
458-
// Like this: https://github.com/ckeditor/ckeditor5-engine/issues/841#issuecomment-282706488.
459-
//
460-
// In following cases, in examples, the last step is the fix step.
461-
// When there are multiple ranges in an example, ranges[] array indices are represented as follows:
462-
// * [] is ranges[ 0 ],
463-
// * {} is ranges[ 1 ],
464-
// * () is ranges[ 2 ].
465-
if ( type == 'move' ) {
466-
const sourceRange = Range.createFromPositionAndShift( sourcePosition, howMany );
467-
468-
if ( deltaType == 'split' && this.containsPosition( sourcePosition ) ) {
469-
// Range contains a position where an element is split.
470-
// <p>f[ooba]r</p> -> <p>f[ooba]r</p><p></p> -> <p>f[oo]</p><p>{ba}r</p> -> <p>f[oo</p><p>ba]r</p>
471-
return [ new Range( ranges[ 0 ].start, ranges[ 1 ].end ) ];
472-
} else if ( deltaType == 'merge' && this.isCollapsed && ranges[ 0 ].start.isEqual( sourcePosition ) ) {
473-
// Collapsed range is in merged element.
474-
// Without fix, the range would end up in the graveyard, together with removed element.
475-
// <p>foo</p><p>[]bar</p> -> <p>foobar</p><p>[]</p> -> <p>foobar</p> -> <p>foo[]bar</p>
476-
return [ new Range( targetPosition.getShiftedBy( this.start.offset ) ) ];
477-
} else if ( deltaType == 'wrap' ) {
478-
// Range intersects (at the start) with wrapped element (<p>ab</p>).
479-
// <p>a[b</p><p>c]d</p> -> <p>a[b</p><w></w><p>c]d</p> -> [<w>]<p>a(b</p>){</w><p>c}d</p> -> <w><p>a[b</p></w><p>c]d</p>
480-
if ( sourceRange.containsPosition( this.start ) && this.containsPosition( sourceRange.end ) ) {
481-
return [ new Range( ranges[ 2 ].start, ranges[ 1 ].end ) ];
482-
}
483-
// Range intersects (at the end) with wrapped element (<p>cd</p>).
484-
// <p>a[b</p><p>c]d</p> -> <p>a[b</p><p>c]d</p><w></w> -> <p>a[b</p>]<w>{<p>c}d</p></w> -> <p>a[b</p><w><p>c]d</p></w>
485-
else if ( sourceRange.containsPosition( this.end ) && this.containsPosition( sourceRange.start ) ) {
486-
return [ new Range( ranges[ 0 ].start, ranges[ 1 ].end ) ];
487-
}
488-
} else if ( deltaType == 'unwrap' ) {
489-
// Range intersects (at the beginning) with unwrapped element (<w></w>).
490-
// <w><p>a[b</p></w><p>c]d</p> -> <p>a{b</p>}<w>[</w><p>c]d</p> -> <p>a[b</p><w></w><p>c]d</p>
491-
// <w></w> is removed in next operation, but the remove does not mess up ranges.
492-
if ( sourceRange.containsPosition( this.start ) && this.containsPosition( sourceRange.end ) ) {
493-
return [ new Range( ranges[ 1 ].start, ranges[ 0 ].end ) ];
494-
}
495-
// Range intersects (at the end) with unwrapped element (<w></w>).
496-
// <p>a[b</p><w><p>c]d</p></w> -> <p>a[b</p>](<p>c)d</p>{<w>}</w> -> <p>a[b</p><p>c]d</p><w></w>
497-
// <w></w> is removed in next operation, but the remove does not mess up ranges.
498-
else if ( sourceRange.containsPosition( this.end ) && this.containsPosition( sourceRange.start ) ) {
499-
return [ new Range( ranges[ 0 ].start, ranges[ 2 ].end ) ];
500-
}
453+
const sourceRange = Range.createFromPositionAndShift( sourcePosition, howMany );
454+
455+
if ( deltaType == 'merge' && this.isCollapsed && ( this.start.isEqual( sourceRange.start ) || this.start.isEqual( sourceRange.end ) ) ) {
456+
// Collapsed range is in merged element.
457+
// Without fix, the range would end up in the graveyard, together with removed element.
458+
// <p>foo</p><p>[]bar</p> -> <p>foobar</p><p>[]</p> -> <p>foobar</p> -> <p>foo[]bar</p>
459+
return [ new Range( targetPosition.getShiftedBy( this.start.offset ) ) ];
460+
} else if ( type == 'move' ) {
461+
// In all examples `[]` is `this` and `{}` is `sourceRange`, while `^` is move target position.
462+
//
463+
// Example:
464+
// <p>xx</p>^<w>{<p>a[b</p>}</w><p>c]d</p> --> <p>xx</p><p>a[b</p><w></w><p>c]d</p>
465+
// ^<p>xx</p><w>{<p>a[b</p>}</w><p>c]d</p> --> <p>a[b</p><p>xx</p><w></w><p>c]d</p> // Note <p>xx</p> inclusion.
466+
// <w>{<p>a[b</p>}</w>^<p>c]d</p> --> <w></w><p>a[b</p><p>c]d</p>
467+
if ( sourceRange.containsPosition( this.start ) && this.containsPosition( sourceRange.end ) && this.end.isAfter( targetPosition ) ) {
468+
let start = this.start._getCombined( sourcePosition, targetPosition._getTransformedByDeletion( sourcePosition, howMany ) );
469+
const end = this.end._getTransformedByMove( sourcePosition, targetPosition, howMany, false, false );
470+
471+
return [ new Range( start, end ) ];
472+
}
473+
474+
// Example:
475+
// <p>c[d</p><w>{<p>a]b</p>}</w>^<p>xx</p> --> <p>c[d</p><w></w><p>a]b</p><p>xx</p>
476+
// <p>c[d</p><w>{<p>a]b</p>}</w><p>xx</p>^ --> <p>c[d</p><w></w><p>xx</p><p>a]b</p> // Note <p>xx</p> inclusion.
477+
// <p>c[d</p>^<w>{<p>a]b</p>}</w> --> <p>c[d</p><p>a]b</p><w></w>
478+
if ( sourceRange.containsPosition( this.end ) && this.containsPosition( sourceRange.start ) && this.start.isBefore( targetPosition ) ) {
479+
const start = this.start._getTransformedByMove( sourcePosition, targetPosition, howMany, true, false );
480+
let end = this.end._getCombined( sourcePosition, targetPosition._getTransformedByDeletion( sourcePosition, howMany ) );
481+
482+
return [ new Range( start, end ) ];
501483
}
502484
}
503485

504-
return ranges;
486+
return this._getTransformedByMove( sourcePosition, targetPosition, howMany );
505487
}
506488
}
507489

tests/model/liverange.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ describe( 'LiveRange', () => {
292292
doc.fire( 'change', 'move', changes, null );
293293

294294
expect( live.start.path ).to.deep.equal( [ 0, 1, 4 ] );
295-
expect( live.end.path ).to.deep.equal( [ 0, 2, 1 ] );
295+
expect( live.end.path ).to.deep.equal( [ 2, 1 ] ); // Included some nodes.
296296
expect( spy.calledOnce ).to.be.true;
297297
} );
298298

@@ -307,7 +307,7 @@ describe( 'LiveRange', () => {
307307
doc.fire( 'change', 'move', changes, null );
308308

309309
expect( live.start.path ).to.deep.equal( [ 0, 1, 4 ] );
310-
expect( live.end.path ).to.deep.equal( [ 0, 2, 6 ] );
310+
expect( live.end.path ).to.deep.equal( [ 0, 2, 1 ] );
311311
expect( spy.calledOnce ).to.be.true;
312312
} );
313313

@@ -357,7 +357,7 @@ describe( 'LiveRange', () => {
357357
};
358358
doc.fire( 'change', 'move', changes, null );
359359

360-
expect( live.start.path ).to.deep.equal( [ 0, 1, 2 ] );
360+
expect( live.start.path ).to.deep.equal( [ 0, 1, 9 ] );
361361
expect( live.end.path ).to.deep.equal( [ 0, 1, 12 ] );
362362
expect( spy.calledOnce ).to.be.true;
363363
} );

tests/model/liveselection.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,7 @@ describe( 'LiveSelection', () => {
453453
let range = selection.getFirstRange();
454454

455455
expect( range.start.path ).to.deep.equal( [ 0, 2 ] );
456-
expect( range.end.path ).to.deep.equal( [ 1, 3 ] );
456+
expect( range.end.path ).to.deep.equal( [ 5 ] );
457457
expect( spyRange.calledOnce ).to.be.true;
458458
} );
459459

tests/model/range.js

Lines changed: 88 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -750,8 +750,8 @@ describe( 'Range', () => {
750750
describe( 'by MoveDelta', () => {
751751
it( 'move before range', () => {
752752
const start = new Position( root, [ 0 ] );
753-
const end = new Position( otherRoot, [ 0 ] );
754-
const delta = getMoveDelta( start, 2, end, 1 );
753+
const target = new Position( otherRoot, [ 0 ] );
754+
const delta = getMoveDelta( start, 2, target, 1 );
755755

756756
const transformed = range.getTransformedByDelta( delta );
757757

@@ -760,8 +760,8 @@ describe( 'Range', () => {
760760

761761
it( 'move intersecting with range (and targeting before range)', () => {
762762
const start = new Position( root, [ 4 ] );
763-
const end = new Position( root, [ 0 ] );
764-
const delta = getMoveDelta( start, 2, end, 1 );
763+
const target = new Position( root, [ 0 ] );
764+
const delta = getMoveDelta( start, 2, target, 1 );
765765

766766
const transformed = range.getTransformedByDelta( delta );
767767

@@ -772,15 +772,80 @@ describe( 'Range', () => {
772772
it( 'move inside the range', () => {
773773
range.end.offset = 6;
774774
const start = new Position( root, [ 3 ] );
775-
const end = new Position( root, [ 5 ] );
776-
const delta = getMoveDelta( start, 1, end, 1 );
775+
const target = new Position( root, [ 5 ] );
776+
const delta = getMoveDelta( start, 1, target, 1 );
777777

778778
const transformed = range.getTransformedByDelta( delta );
779779

780780
expectRange( transformed[ 0 ], 2, 4 );
781781
expectRange( transformed[ 1 ], 5, 6 );
782782
expectRange( transformed[ 2 ], 4, 5 );
783783
} );
784+
785+
// #877.
786+
it( 'moved element contains range start and is moved towards inside of range', () => {
787+
// Initial state:
788+
// <w><p>abc</p><p>x[x</p></w><p>d]ef</p>
789+
// Expected state after moving `<p>` out of `<w>`:
790+
// <w><p>abc</p></w><p>x[x</p><p>d]ef</p>
791+
792+
const range = new Range( new Position( root, [ 0, 1, 1 ] ), new Position( root, [ 1, 1 ] ) );
793+
const delta = getMoveDelta( new Position( root, [ 0, 1 ] ), 1, new Position( root, [ 1 ] ), 1 );
794+
795+
const transformed = range.getTransformedByDelta( delta );
796+
797+
expect( transformed.length ).to.equal( 1 );
798+
expect( transformed[ 0 ].start.path ).to.deep.equal( [ 1, 1 ] );
799+
expect( transformed[ 0 ].end.path ).to.deep.equal( [ 2, 1 ] );
800+
} );
801+
802+
it( 'moved element contains range start and is moved out of range', () => {
803+
// Initial state:
804+
// <p>abc</p><p>x[x</p><p>d]ef</p>
805+
// Expected state after moving:
806+
// <p>x[x</p><p>abc</p><p>d]ef</p>
807+
808+
const range = new Range( new Position( root, [ 1, 1 ] ), new Position( root, [ 2, 1 ] ) );
809+
const delta = getMoveDelta( new Position( root, [ 1 ] ), 1, new Position( root, [ 0 ] ), 1 );
810+
811+
const transformed = range.getTransformedByDelta( delta );
812+
813+
expect( transformed.length ).to.equal( 1 );
814+
expect( transformed[ 0 ].start.path ).to.deep.equal( [ 0, 1 ] );
815+
expect( transformed[ 0 ].end.path ).to.deep.equal( [ 2, 1 ] );
816+
} );
817+
818+
it( 'moved element contains range end and is moved towards range', () => {
819+
// Initial state:
820+
// <p>a[bc</p><p>def</p><p>x]x</p>
821+
// Expected state after moving:
822+
// <p>a[bc</p><p>x]x</p><p>def</p>
823+
824+
const range = new Range( new Position( root, [ 0, 1 ] ), new Position( root, [ 2, 1 ] ) );
825+
const delta = getMoveDelta( new Position( root, [ 2 ] ), 1, new Position( root, [ 1 ] ), 1 );
826+
827+
const transformed = range.getTransformedByDelta( delta );
828+
829+
expect( transformed.length ).to.equal( 1 );
830+
expect( transformed[ 0 ].start.path ).to.deep.equal( [ 0, 1 ] );
831+
expect( transformed[ 0 ].end.path ).to.deep.equal( [ 1, 1 ] );
832+
} );
833+
834+
it( 'moved element contains range end and is moved out of range', () => {
835+
// Initial state:
836+
// <p>a[bc</p><p>x]x</p><p>def</p>
837+
// Expected state after moving:
838+
// <p>a[bc</p><p>def</p><p>x]x</p>
839+
840+
const range = new Range( new Position( root, [ 0, 1 ] ), new Position( root, [ 1, 1 ] ) );
841+
const delta = getMoveDelta( new Position( root, [ 1 ] ), 1, new Position( root, [ 3 ] ), 1 );
842+
843+
const transformed = range.getTransformedByDelta( delta );
844+
845+
expect( transformed.length ).to.equal( 1 );
846+
expect( transformed[ 0 ].start.path ).to.deep.equal( [ 0, 1 ] );
847+
expect( transformed[ 0 ].end.path ).to.deep.equal( [ 2, 1 ] );
848+
} );
784849
} );
785850

786851
describe( 'by RemoveDelta', () => {
@@ -858,6 +923,23 @@ describe( 'Range', () => {
858923
expect( transformed[ 0 ].start.path ).to.deep.equal( [ 0, 3 ] );
859924
expect( transformed[ 0 ].end.path ).to.deep.equal( [ 0, 3 ] );
860925
} );
926+
927+
// #877.
928+
it( 'merge elements that contain elements with range boundaries', () => {
929+
// Initial state:
930+
// <w><p>x[x</p></w><w><p>y]y</p></w>
931+
// Expected state after merge:
932+
// <w><p>x[x</p><p>y]y</p></w>
933+
934+
const range = new Range( new Position( root, [ 0, 0, 1 ] ), new Position( root, [ 1, 0, 1 ] ) );
935+
const delta = getMergeDelta( new Position( root, [ 1 ] ), 1, 1, 1 );
936+
937+
const transformed = range.getTransformedByDelta( delta );
938+
939+
expect( transformed.length ).to.equal( 1 );
940+
expect( transformed[ 0 ].start.path ).to.deep.equal( [ 0, 0, 1 ] );
941+
expect( transformed[ 0 ].end.path ).to.deep.equal( [ 0, 1, 1 ] );
942+
} );
861943
} );
862944

863945
describe( 'by WrapDelta', () => {

0 commit comments

Comments
 (0)