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

Commit ed1b7e7

Browse files
authored
Merge pull request #1056 from ckeditor/t/1055
Fixed: Selection attributes should be cleared in an `enqueueChanges()` block. Fixed also a bug concerning `AttributeDelta` x `SplitDelta` transformation. Closes #1055.
2 parents ec22a29 + fd4eadc commit ed1b7e7

File tree

8 files changed

+246
-60
lines changed

8 files changed

+246
-60
lines changed

src/model/delta/basic-transformations.js

Lines changed: 51 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,16 @@ addTransformationCase( AttributeDelta, SplitDelta, ( a, b, context ) => {
5555
return defaultTransform( a, b, context );
5656
}
5757

58+
const undoMode = context.aWasUndone || context.bWasUndone;
5859
const splitPosition = new Position( b.position.root, b.position.path.slice( 0, -1 ) );
5960

6061
const deltas = defaultTransform( a, b, context );
6162

63+
// Special case applies only if undo is not a context and only if `SplitDelta` has `InsertOperation` (not `ReinsertOperation`).
64+
if ( undoMode || !( b._cloneOperation instanceof InsertOperation ) ) {
65+
return deltas;
66+
}
67+
6268
for ( const operation of a.operations ) {
6369
// If a node that has been split has it's attribute updated, we should also update attribute of
6470
// the node created during splitting.
@@ -292,21 +298,25 @@ addTransformationCase( SplitDelta, AttributeDelta, ( a, b, context ) => {
292298

293299
a = a.clone();
294300

301+
const undoMode = context.aWasUndone || context.bWasUndone;
295302
const splitPosition = new Position( a.position.root, a.position.path.slice( 0, -1 ) );
296303

297-
if ( a._cloneOperation instanceof InsertOperation ) {
298-
// If element to split had it's attribute changed, we have to reflect this change in an element
299-
// that is in SplitDelta's InsertOperation.
300-
for ( const operation of b.operations ) {
301-
if ( operation.range.containsPosition( splitPosition ) || operation.range.start.isEqual( splitPosition ) ) {
302-
if ( operation.newValue !== null ) {
303-
a._cloneOperation.nodes.getNode( 0 ).setAttribute( operation.key, operation.newValue );
304-
} else {
305-
a._cloneOperation.nodes.getNode( 0 ).removeAttribute( operation.key );
306-
}
307-
308-
break;
304+
// Special case applies only if undo is not a context and only if `SplitDelta` has `InsertOperation` (not `ReinsertOperation`).
305+
if ( undoMode || !( a._cloneOperation instanceof InsertOperation ) ) {
306+
return [ a ];
307+
}
308+
309+
// If element to split had it's attribute changed, we have to reflect this change in an element
310+
// that is in SplitDelta's InsertOperation.
311+
for ( const operation of b.operations ) {
312+
if ( operation.range.containsPosition( splitPosition ) || operation.range.start.isEqual( splitPosition ) ) {
313+
if ( operation.newValue !== null ) {
314+
a._cloneOperation.nodes.getNode( 0 ).setAttribute( operation.key, operation.newValue );
315+
} else {
316+
a._cloneOperation.nodes.getNode( 0 ).removeAttribute( operation.key );
309317
}
318+
319+
break;
310320
}
311321
}
312322

@@ -383,18 +393,16 @@ addTransformationCase( WrapDelta, SplitDelta, ( a, b, context ) => {
383393
// Add special case for RenameDelta x SplitDelta transformation.
384394
addTransformationCase( RenameDelta, SplitDelta, ( a, b, context ) => {
385395
const undoMode = context.aWasUndone || context.bWasUndone;
396+
const deltas = defaultTransform( a, b, context );
386397

387-
// The "clone operation" may be `InsertOperation`, `ReinsertOperation`, `MoveOperation` or `NoOperation`.
388-
// `MoveOperation` has `targetPosition` which we want to use. `NoOperation` has no `position` and we don't use special case then.
389-
let insertPosition = b._cloneOperation.position || b._cloneOperation.targetPosition;
390-
391-
if ( insertPosition ) {
392-
insertPosition = insertPosition.getShiftedBy( -1 );
398+
// Special case applies only if undo is not a context and only if `SplitDelta` has `InsertOperation` (not `ReinsertOperation`).
399+
if ( undoMode || !( b._cloneOperation instanceof InsertOperation ) ) {
400+
return deltas;
393401
}
394402

395-
const deltas = defaultTransform( a, b, context );
403+
const insertPosition = b._cloneOperation.position.getShiftedBy( -1 );
396404

397-
if ( insertPosition && !undoMode && a.operations[ 0 ].position.isEqual( insertPosition ) ) {
405+
if ( insertPosition && a.operations[ 0 ].position.isEqual( insertPosition ) ) {
398406
// If a node that has been split has it's name changed, we should also change name of
399407
// the node created during splitting.
400408
const additionalRenameDelta = a.clone();
@@ -408,31 +416,27 @@ addTransformationCase( RenameDelta, SplitDelta, ( a, b, context ) => {
408416

409417
// Add special case for SplitDelta x RenameDelta transformation.
410418
addTransformationCase( SplitDelta, RenameDelta, ( a, b, context ) => {
411-
const undoMode = context.aWasUndone || context.bWasUndone;
419+
a = a.clone();
412420

413-
// The "clone operation" may be `InsertOperation`, `ReinsertOperation`, `MoveOperation` or `NoOperation`.
414-
// `MoveOperation` has `targetPosition` which we want to use. `NoOperation` has no `position` and we don't use special case then.
415-
let insertPosition = a._cloneOperation.position || a._cloneOperation.targetPosition;
421+
const undoMode = context.aWasUndone || context.bWasUndone;
416422

417-
if ( insertPosition ) {
418-
insertPosition = insertPosition.getShiftedBy( -1 );
423+
// Special case applies only if undo is not a context and only if `SplitDelta` has `InsertOperation` (not `ReinsertOperation`).
424+
if ( undoMode || !( a._cloneOperation instanceof InsertOperation ) ) {
425+
return [ a ];
419426
}
420427

428+
const insertPosition = a._cloneOperation.position.getShiftedBy( -1 );
429+
421430
// If element to split had it's name changed, we have to reflect this by creating additional rename operation.
422431
if ( insertPosition && !undoMode && b.operations[ 0 ].position.isEqual( insertPosition ) ) {
423432
const additionalRenameDelta = b.clone();
424433
additionalRenameDelta.operations[ 0 ].position = insertPosition.getShiftedBy( 1 );
425-
426-
// `nodes` is a property that is available only if `SplitDelta` `a` has `InsertOperation`.
427-
// `SplitDelta` may have `ReinsertOperation` instead of `InsertOperation`.
428-
// However, such delta is only created when `MergeDelta` is reversed.
429-
// So if this is not undo mode, it means that `SplitDelta` has `InsertOperation`.
430434
additionalRenameDelta.operations[ 0 ].oldName = a._cloneOperation.nodes.getNode( 0 ).name;
431435

432-
return [ a.clone(), additionalRenameDelta ];
436+
return [ a, additionalRenameDelta ];
433437
}
434438

435-
return [ a.clone() ];
439+
return [ a ];
436440
} );
437441

438442
// Add special case for RemoveDelta x SplitDelta transformation.
@@ -446,6 +450,13 @@ addTransformationCase( RemoveDelta, SplitDelta, ( a, b, context ) => {
446450
return defaultTransform( a, b, context );
447451
}
448452

453+
const undoMode = context.aWasUndone || context.bWasUndone;
454+
455+
// Special case applies only if undo is not a context.
456+
if ( undoMode ) {
457+
return deltas;
458+
}
459+
449460
// In case if `defaultTransform` returned more than one delta.
450461
for ( const delta of deltas ) {
451462
// "No delta" may be returned in some cases.
@@ -464,6 +475,13 @@ addTransformationCase( RemoveDelta, SplitDelta, ( a, b, context ) => {
464475

465476
// Add special case for SplitDelta x RemoveDelta transformation.
466477
addTransformationCase( SplitDelta, RemoveDelta, ( a, b, context ) => {
478+
const undoMode = context.aWasUndone || context.bWasUndone;
479+
480+
// Special case applies only if undo is not a context.
481+
if ( undoMode ) {
482+
return defaultTransform( a, b, context );
483+
}
484+
467485
// This case is very trickily solved.
468486
// Instead of fixing `a` delta, we change `b` delta for a while and fire default transformation with fixed `b` delta.
469487
// Thanks to that fixing `a` delta will be differently (correctly) transformed.

src/model/documentselection.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export default class DocumentSelection extends Selection {
8585

8686
// Whenever element which had selection's attributes stored in it stops being empty,
8787
// the attributes need to be removed.
88-
clearAttributesStoredInElement( changes, batch );
88+
clearAttributesStoredInElement( changes, batch, this._document );
8989
} );
9090
}
9191

@@ -691,7 +691,7 @@ function getAttrsIfCharacter( node ) {
691691
}
692692

693693
// Removes selection attributes from element which is not empty anymore.
694-
function clearAttributesStoredInElement( changes, batch ) {
694+
function clearAttributesStoredInElement( changes, batch, document ) {
695695
// Batch may not be passed to the document#change event in some tests.
696696
// See https://github.com/ckeditor/ckeditor5-engine/issues/1001#issuecomment-314202352
697697
// Ignore also transparent batches because they are... transparent.
@@ -707,9 +707,11 @@ function clearAttributesStoredInElement( changes, batch ) {
707707
return;
708708
}
709709

710-
const storedAttributes = Array.from( changeParent.getAttributeKeys() ).filter( key => key.startsWith( storePrefix ) );
710+
document.enqueueChanges( () => {
711+
const storedAttributes = Array.from( changeParent.getAttributeKeys() ).filter( key => key.startsWith( storePrefix ) );
711712

712-
for ( const key of storedAttributes ) {
713-
batch.removeAttribute( changeParent, key );
714-
}
713+
for ( const key of storedAttributes ) {
714+
batch.removeAttribute( changeParent, key );
715+
}
716+
} );
715717
}

tests/model/delta/transform/_utils/utils.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -202,14 +202,14 @@ export function expectDelta( delta, expected ) {
202202
export function expectOperation( op, params ) {
203203
for ( const i in params ) {
204204
if ( i == 'type' ) {
205-
expect( op ).to.be.instanceof( params[ i ] );
205+
expect( op, 'operation type' ).to.be.instanceof( params[ i ] );
206206
}
207207
else if ( i == 'nodes' ) {
208-
expect( Array.from( op.nodes ) ).to.deep.equal( params[ i ] );
208+
expect( Array.from( op.nodes ), 'nodes' ).to.deep.equal( params[ i ] );
209209
} else if ( params[ i ] instanceof Position || params[ i ] instanceof Range ) {
210-
expect( op[ i ].isEqual( params[ i ] ) ).to.be.true;
210+
expect( op[ i ].isEqual( params[ i ] ), 'property ' + i ).to.be.true;
211211
} else {
212-
expect( op[ i ] ).to.equal( params[ i ] );
212+
expect( op[ i ], 'property ' + 1 ).to.equal( params[ i ] );
213213
}
214214
}
215215
}

tests/model/delta/transform/attributedelta.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ import Range from '../../../../src/model/range';
1515

1616
import AttributeDelta from '../../../../src/model/delta/attributedelta';
1717
import Delta from '../../../../src/model/delta/delta';
18+
import SplitDelta from '../../../../src/model/delta/splitdelta';
1819
import AttributeOperation from '../../../../src/model/operation/attributeoperation';
20+
import MoveOperation from '../../../../src/model/operation/moveoperation';
21+
import ReinsertOperation from '../../../../src/model/operation/reinsertoperation';
1922
import NoOperation from '../../../../src/model/operation/nooperation';
2023

2124
import {
@@ -257,6 +260,54 @@ describe( 'transform', () => {
257260
} );
258261
} );
259262

263+
it( 'change attribute of split element that reinserts from graveyard', () => {
264+
const gy = doc.graveyard;
265+
const splitDelta = new SplitDelta();
266+
267+
splitDelta.operations[ 0 ] = new ReinsertOperation(
268+
new Position( gy, [ 1 ] ),
269+
1,
270+
new Position( root, [ 2 ] ),
271+
baseVersion
272+
);
273+
274+
splitDelta.operations[ 1 ] = new MoveOperation(
275+
new Position( root, [ 1, 4 ] ),
276+
4,
277+
new Position( root, [ 2, 0 ] ),
278+
baseVersion
279+
);
280+
281+
const attributeDelta = new AttributeDelta();
282+
283+
const range = new Range( new Position( root, [ 1 ] ), new Position( root, [ 1, 0 ] ) );
284+
285+
attributeDelta.addOperation( new AttributeOperation( range, 'key', 'oldValue', 'newValue', baseVersion ) );
286+
287+
// The split delta was undone.
288+
context.bWasUndone = true;
289+
290+
const transformed = transform( attributeDelta, splitDelta, context );
291+
292+
baseVersion = attributeDelta.operations.length;
293+
// We expect only one delta. Split delta should not affect attribute delta transformation. It was undone.
294+
expect( transformed.length ).to.equal( 1 );
295+
296+
expectDelta( transformed[ 0 ], {
297+
type: AttributeDelta,
298+
operations: [
299+
{
300+
type: AttributeOperation,
301+
range,
302+
key: 'key',
303+
oldValue: 'oldValue',
304+
newValue: 'newValue',
305+
baseVersion
306+
}
307+
]
308+
} );
309+
} );
310+
260311
it( 'should use default algorithm and not throw if split delta has NoOperation', () => {
261312
const range = new Range( new Position( root, [ 1 ] ), new Position( root, [ 2, 3 ] ) );
262313
const attrDelta = getAttributeDelta( range, 'foo', null, 'bar', 0 );

tests/model/delta/transform/removedelta.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,35 @@ describe( 'transform', () => {
185185
} );
186186
} );
187187

188+
it( 'removed node has been split - undo context', () => {
189+
const sourcePosition = new Position( root, [ 3, 3, 1 ] );
190+
const removeDelta = getRemoveDelta( sourcePosition, 1, baseVersion );
191+
192+
const splitPosition = new Position( root, [ 3, 3, 1, 2 ] );
193+
const nodeCopy = new Element( 'x' );
194+
const splitDelta = getSplitDelta( splitPosition, nodeCopy, 2, baseVersion );
195+
196+
context.bWasUndone = true;
197+
198+
const transformed = transform( removeDelta, splitDelta, context );
199+
200+
expect( transformed.length ).to.equal( 1 );
201+
202+
baseVersion = splitDelta.operations.length;
203+
204+
expectDelta( transformed[ 0 ], {
205+
type: RemoveDelta,
206+
operations: [
207+
{
208+
type: RemoveOperation,
209+
sourcePosition,
210+
howMany: 1,
211+
baseVersion
212+
}
213+
]
214+
} );
215+
} );
216+
188217
it( 'should not throw if clone operation is NoOperation and use default transformation in that case', () => {
189218
const noOpSplitDelta = new SplitDelta();
190219
noOpSplitDelta.addOperation( new NoOperation( 0 ) );

tests/model/delta/transform/renamedelta.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,39 @@ describe( 'transform', () => {
7878
} );
7979
} );
8080

81+
it( 'split element is renamed but split delta was undone', () => {
82+
const renameDelta = new RenameDelta();
83+
renameDelta.addOperation( new RenameOperation(
84+
new Position( root, [ 3, 3 ] ),
85+
'p',
86+
'li',
87+
baseVersion
88+
) );
89+
90+
const splitPosition = new Position( root, [ 3, 3, 3 ] );
91+
const splitDelta = getSplitDelta( splitPosition, new Element( 'p' ), 9, baseVersion );
92+
93+
context.bWasUndone = true;
94+
95+
const transformed = transform( renameDelta, splitDelta, context );
96+
97+
baseVersion = splitDelta.length;
98+
99+
expect( transformed.length ).to.equal( 1 );
100+
101+
expectDelta( transformed[ 0 ], {
102+
type: RenameDelta,
103+
operations: [
104+
{
105+
type: RenameOperation,
106+
oldName: 'p',
107+
newName: 'li',
108+
position: new Position( root, [ 3, 3 ] )
109+
}
110+
]
111+
} );
112+
} );
113+
81114
it( 'split element is different than renamed element', () => {
82115
const renameDelta = new RenameDelta();
83116
renameDelta.addOperation( new RenameOperation(

tests/model/delta/transform/splitdelta.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -990,6 +990,37 @@ describe( 'transform', () => {
990990
} );
991991
} );
992992

993+
it( 'split node has been removed - undo context', () => {
994+
const removePosition = new Position( root, [ 3, 3, 3 ] );
995+
const removeDelta = getRemoveDelta( removePosition, 1, baseVersion );
996+
997+
context.bWasUndone = true;
998+
999+
const transformed = transform( splitDelta, removeDelta, context );
1000+
1001+
expect( transformed.length ).to.equal( 1 );
1002+
1003+
baseVersion = removeDelta.operations.length;
1004+
1005+
expectDelta( transformed[ 0 ], {
1006+
type: SplitDelta,
1007+
operations: [
1008+
{
1009+
type: InsertOperation,
1010+
position: splitDelta.operations[ 0 ].position.getShiftedBy( -1 ),
1011+
baseVersion
1012+
},
1013+
{
1014+
type: ReinsertOperation,
1015+
sourcePosition: new Position( gy, [ 0, 3 ] ),
1016+
howMany: splitDelta.operations[ 1 ].howMany,
1017+
targetPosition: new Position( root, [ 3, 3, 3, 0 ] ),
1018+
baseVersion: baseVersion + 1
1019+
}
1020+
]
1021+
} );
1022+
} );
1023+
9931024
it( 'should not throw if clone operation is NoOperation and use default transformation in that case', () => {
9941025
const noOpSplitDelta = new SplitDelta();
9951026
noOpSplitDelta.addOperation( new NoOperation( 0 ) );

0 commit comments

Comments
 (0)