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

Commit 6875eff

Browse files
authored
Merge pull request #947 from ckeditor/t/946
Fix: Editor will no longer crash when `ReinsertOperation` is transformed by specific `RemoveOperation`. Closes #946.
2 parents cd6a1af + 7eaef0a commit 6875eff

File tree

2 files changed

+89
-13
lines changed

2 files changed

+89
-13
lines changed

src/model/operation/transform.js

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -384,18 +384,30 @@ const ot = {
384384
return [ b.getReversed() ];
385385
}
386386

387-
// Special case when both operations are RemoveOperations. RemoveOperation not only moves nodes but also
388-
// (usually) creates a "holder" element for them in graveyard. Each RemoveOperation should move nodes to different
389-
// "holder" element. If `a` operation points after `b` operation, we move `a` offset to acknowledge
390-
// "holder" element insertion.
391-
if ( a instanceof RemoveOperation && b instanceof RemoveOperation ) {
392-
const aTarget = a.targetPosition.path[ 0 ];
393-
const bTarget = b.targetPosition.path[ 0 ];
394-
395-
if ( aTarget > bTarget || ( aTarget == bTarget && isStrong ) ) {
396-
// Do not change original operation!
397-
a = a.clone();
398-
a.targetPosition.path[ 0 ]++;
387+
// Special case when operation transformed by is RemoveOperation. RemoveOperation not only moves nodes but also
388+
// (usually) creates a "holder" element for them in graveyard. This has to be taken into consideration when
389+
// transforming operations that operate in graveyard root.
390+
if ( b instanceof RemoveOperation && b._needsHolderElement ) {
391+
a = a.clone();
392+
393+
const aSourceOffset = a.sourcePosition.path[ 0 ];
394+
const aTargetOffset = a.targetPosition.path[ 0 ];
395+
396+
// We can't use Position#_getTransformedByInsertion because it works a bit differently and we would get wrong results.
397+
// That's because `sourcePosition`/`targetPosition` is, for example, `[ 1, 0 ]`, while `insertionPosition` is
398+
// `[ 1 ]`. In this case, `sourcePosition`/`targetPosition` would always be transformed but in fact, only
399+
// "holderElement" offsets should be looked at (so `[ 1 ]` in case of `sourcePosition`/`targetPosition`, not `[ 1, 0 ]` ).
400+
// This is why we are doing it "by hand".
401+
if ( a.sourcePosition.root == b.targetPosition.root ) {
402+
if ( aSourceOffset > b._holderElementOffset || aSourceOffset == b._holderElementOffset ) {
403+
a.sourcePosition.path[ 0 ]++;
404+
}
405+
}
406+
407+
if ( a.targetPosition.root == b.targetPosition.root ) {
408+
if ( aTargetOffset > b._holderElementOffset || ( aTargetOffset == b._holderElementOffset && isStrong ) ) {
409+
a.targetPosition.path[ 0 ]++;
410+
}
399411
}
400412
}
401413

tests/model/operation/transform.js

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import MarkerOperation from '../../../src/model/operation/markeroperation';
1818
import MoveOperation from '../../../src/model/operation/moveoperation';
1919
import RemoveOperation from '../../../src/model/operation/removeoperation';
2020
import RenameOperation from '../../../src/model/operation/renameoperation';
21+
import ReinsertOperation from '../../../src/model/operation/reinsertoperation';
2122
import NoOperation from '../../../src/model/operation/nooperation';
2223

2324
describe( 'transform', () => {
@@ -2771,7 +2772,7 @@ describe( 'transform', () => {
27712772
} );
27722773

27732774
describe( 'by MoveOperation', () => {
2774-
it( 'should create not more than RemoveOperation that needs new graveyard holder', () => {
2775+
it( 'should create not more than one RemoveOperation that needs new graveyard holder', () => {
27752776
let op = new RemoveOperation( new Position( root, [ 1 ] ), 4, baseVersion );
27762777
let transformBy = new MoveOperation( new Position( root, [ 0 ] ), 2, new Position( root, [ 8 ] ), baseVersion );
27772778

@@ -2796,6 +2797,28 @@ describe( 'transform', () => {
27962797
expect( transOp[ 0 ]._needsHolderElement ).to.be.false;
27972798
expect( transOp[ 1 ]._needsHolderElement ).to.be.false;
27982799
} );
2800+
2801+
it( 'should force removing content even if was less important', () => {
2802+
let op = new RemoveOperation( new Position( root, [ 8 ] ), 2, baseVersion );
2803+
2804+
const targetPosition = Position.createFromPosition( op.targetPosition );
2805+
2806+
let transformBy = new MoveOperation( new Position( root, [ 8 ] ), 2, new Position( root, [ 1 ] ), baseVersion );
2807+
2808+
const sourcePosition = Position.createFromPosition( transformBy.targetPosition );
2809+
2810+
let transOp = transform( op, transformBy, false );
2811+
2812+
expect( transOp.length ).to.equal( 1 );
2813+
2814+
expectOperation( transOp[ 0 ], {
2815+
type: RemoveOperation,
2816+
howMany: 2,
2817+
sourcePosition: sourcePosition,
2818+
targetPosition: targetPosition,
2819+
baseVersion: baseVersion + 1
2820+
} );
2821+
} );
27992822
} );
28002823

28012824
describe( 'by RemoveOperation', () => {
@@ -2835,6 +2858,47 @@ describe( 'transform', () => {
28352858
} );
28362859
} );
28372860

2861+
describe( 'ReinsertOperation', () => {
2862+
describe( 'by RemoveOperation', () => {
2863+
it( 'should update sourcePosition if remove operation inserted holder offset before reinsert operation holder', () => {
2864+
let position = new Position( root, [ 2, 1 ] );
2865+
let transformBy = new RemoveOperation( position, 3, baseVersion );
2866+
let op = new ReinsertOperation( new Position( doc.graveyard, [ 0, 0 ] ), 3, position, baseVersion );
2867+
2868+
let transOp = transform( op, transformBy );
2869+
2870+
expect( transOp.length ).to.equal( 1 );
2871+
2872+
expectOperation( transOp[ 0 ], {
2873+
type: ReinsertOperation,
2874+
howMany: 3,
2875+
sourcePosition: new Position( doc.graveyard, [ 1, 0 ] ),
2876+
targetPosition: position,
2877+
baseVersion: baseVersion + 1
2878+
} );
2879+
} );
2880+
2881+
it( 'should not update sourcePosition if remove operation inserted holder offset after reinsert operation source', () => {
2882+
let position = new Position( root, [ 2, 1 ] );
2883+
let transformBy = new RemoveOperation( position, 3, baseVersion );
2884+
transformBy.targetPosition = new Position( doc.graveyard, [ 3, 0 ] );
2885+
let op = new ReinsertOperation( new Position( doc.graveyard, [ 2, 0 ] ), 3, position, baseVersion );
2886+
2887+
let transOp = transform( op, transformBy );
2888+
2889+
expect( transOp.length ).to.equal( 1 );
2890+
2891+
expectOperation( transOp[ 0 ], {
2892+
type: ReinsertOperation,
2893+
howMany: 3,
2894+
sourcePosition: new Position( doc.graveyard, [ 2, 0 ] ),
2895+
targetPosition: position,
2896+
baseVersion: baseVersion + 1
2897+
} );
2898+
} );
2899+
} );
2900+
} );
2901+
28382902
describe( 'NoOperation', () => {
28392903
beforeEach( () => {
28402904
op = new NoOperation( baseVersion );

0 commit comments

Comments
 (0)