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

Commit f88c918

Browse files
author
Piotr Jasiun
authored
Merge pull request #1581 from ckeditor/t/1580
Fix: `MoveOperation` x `SplitOperation` transformation for a case when graveyard element is moved. Closes #1580.
2 parents b1e8975 + 87fdbd6 commit f88c918

File tree

4 files changed

+117
-26
lines changed

4 files changed

+117
-26
lines changed

src/model/differ.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,7 @@ export default class Differ {
423423
// If change happens at the same position...
424424
if ( a.position.isEqual( b.position ) ) {
425425
// Keep chronological order of operations.
426-
return a.changeCount < b.changeCount ? -1 : 1;
426+
return a.changeCount - b.changeCount;
427427
}
428428

429429
// If positions differ, position "on the left" should be earlier in the result.

src/model/operation/transform.js

Lines changed: 55 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1669,12 +1669,21 @@ setTransformation( MoveOperation, SplitOperation, ( a, b, context ) => {
16691669
// The default case.
16701670
//
16711671
const transformed = moveRange._getTransformedBySplitOperation( b );
1672+
const ranges = [ transformed ];
16721673

1673-
a.sourcePosition = transformed.start;
1674-
a.howMany = transformed.end.offset - transformed.start.offset;
1675-
a.targetPosition = newTargetPosition;
1674+
// Case 5:
1675+
//
1676+
// Moved range contains graveyard element used by split operation. Add extra move operation to the result.
1677+
//
1678+
if ( b.graveyardPosition ) {
1679+
const movesGraveyardElement = moveRange.start.isEqual( b.graveyardPosition ) || moveRange.containsPosition( b.graveyardPosition );
16761680

1677-
return [ a ];
1681+
if ( a.howMany > 1 && movesGraveyardElement ) {
1682+
ranges.push( Range.createFromPositionAndShift( b.insertionPosition, 1 ) );
1683+
}
1684+
}
1685+
1686+
return _makeMoveOperationsFromRanges( ranges, newTargetPosition );
16781687
} );
16791688

16801689
setTransformation( MoveOperation, MergeOperation, ( a, b, context ) => {
@@ -1692,16 +1701,30 @@ setTransformation( MoveOperation, MergeOperation, ( a, b, context ) => {
16921701
// removed nodes might be unexpected. This means that in this scenario we will reverse merging and remove the element.
16931702
//
16941703
if ( !context.aWasUndone ) {
1695-
const gyMoveTarget = Position.createFromPosition( b.graveyardPosition );
1696-
const gyMove = new MoveOperation( b.graveyardPosition, 1, gyMoveTarget, 0 );
1704+
const results = [];
16971705

1698-
const targetPositionPath = b.graveyardPosition.path.slice();
1706+
let gyMoveSource = Position.createFromPosition( b.graveyardPosition );
1707+
let splitNodesMoveSource = Position.createFromPosition( b.targetPosition );
1708+
1709+
if ( a.howMany > 1 ) {
1710+
results.push( new MoveOperation( a.sourcePosition, a.howMany - 1, a.targetPosition, 0 ) );
1711+
gyMoveSource = gyMoveSource._getTransformedByInsertion( a.targetPosition, a.howMany - 1 );
1712+
splitNodesMoveSource = splitNodesMoveSource._getTransformedByMove( a.sourcePosition, a.targetPosition, a.howMany - 1 );
1713+
}
1714+
1715+
const gyMoveTarget = b.deletionPosition._getCombined( a.sourcePosition, a.targetPosition );
1716+
const gyMove = new MoveOperation( gyMoveSource, 1, gyMoveTarget, 0 );
1717+
1718+
const targetPositionPath = gyMove.getMovedRangeStart().path.slice();
16991719
targetPositionPath.push( 0 );
17001720

1701-
return [
1702-
gyMove,
1703-
new MoveOperation( b.targetPosition, b.howMany, new Position( a.targetPosition.root, targetPositionPath ), 0 )
1704-
];
1721+
const splitNodesMoveTarget = new Position( gyMove.targetPosition.root, targetPositionPath );
1722+
const splitNodesMove = new MoveOperation( splitNodesMoveSource, b.howMany, splitNodesMoveTarget, 0 );
1723+
1724+
results.push( gyMove );
1725+
results.push( splitNodesMove );
1726+
1727+
return results;
17051728
}
17061729
} else {
17071730
// Case 2:
@@ -1934,14 +1957,21 @@ setTransformation( SplitOperation, MoveOperation, ( a, b, context ) => {
19341957
if ( a.graveyardPosition ) {
19351958
// Case 1:
19361959
//
1937-
// Split operation graveyard node was moved. In this case move operation is stronger and the split insertion position
1938-
// should be corrected.
1960+
// Split operation graveyard node was moved. In this case move operation is stronger. Since graveyard element
1961+
// is already moved to the correct position, we need to only move the nodes after the split position.
1962+
// This will be done by `MoveOperation` instead of `SplitOperation`.
19391963
//
1940-
if ( rangeToMove.containsPosition( a.graveyardPosition ) || rangeToMove.start.isEqual( a.graveyardPosition ) ) {
1941-
a.insertionPosition = Position.createFromPosition( b.targetPosition );
1942-
a.splitPosition = a.splitPosition._getTransformedByMoveOperation( b );
1964+
if ( rangeToMove.start.isEqual( a.graveyardPosition ) || rangeToMove.containsPosition( a.graveyardPosition ) ) {
1965+
const sourcePosition = a.splitPosition._getTransformedByMoveOperation( b );
19431966

1944-
return [ a ];
1967+
const newParentPosition = a.graveyardPosition._getTransformedByMoveOperation( b );
1968+
const newTargetPath = newParentPosition.path.slice();
1969+
newTargetPath.push( 0 );
1970+
1971+
const newTargetPosition = new Position( newParentPosition.root, newTargetPath );
1972+
const moveOp = new MoveOperation( sourcePosition, a.howMany, newTargetPosition, 0 );
1973+
1974+
return [ moveOp ];
19451975
}
19461976

19471977
a.graveyardPosition = a.graveyardPosition._getTransformedByMoveOperation( b );
@@ -2172,7 +2202,14 @@ function _makeMoveOperationsFromRanges( ranges, targetPosition ) {
21722202
for ( let i = 0; i < ranges.length; i++ ) {
21732203
// Create new operation out of a range and target position.
21742204
const range = ranges[ i ];
2175-
const op = new MoveOperation( range.start, range.end.offset - range.start.offset, targetPosition, 0 );
2205+
const op = new MoveOperation(
2206+
range.start,
2207+
range.end.offset - range.start.offset,
2208+
// If the target is the end of the move range this operation doesn't really move anything.
2209+
// In this case, it is better for OT to use range start instead of range end.
2210+
targetPosition.isEqual( range.end ) ? range.start : targetPosition,
2211+
0
2212+
);
21762213

21772214
operations.push( op );
21782215

src/model/range.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -517,13 +517,17 @@ export default class Range {
517517
*/
518518
_getTransformedBySplitOperation( operation ) {
519519
const start = this.start._getTransformedBySplitOperation( operation );
520-
521-
let end;
520+
let end = this.end._getTransformedBySplitOperation( operation );
522521

523522
if ( this.end.isEqual( operation.insertionPosition ) ) {
524523
end = this.end.getShiftedBy( 1 );
525-
} else {
526-
end = this.end._getTransformedBySplitOperation( operation );
524+
}
525+
526+
// Below may happen when range contains graveyard element used by split operation.
527+
if ( start.root != end.root ) {
528+
// End position was next to the moved graveyard element and was moved with it.
529+
// Fix it by using old `end` which has proper `root`.
530+
end = this.end.getShiftedBy( -1 );
527531
}
528532

529533
return new Range( start, end );

tests/model/operation/transform/merge.js

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,9 +174,7 @@ describe( 'transform', () => {
174174
kate.undo();
175175
syncClients();
176176

177-
expectClients(
178-
'<paragraph>Foo</paragraph>'
179-
);
177+
expectClients( '<paragraph>Foo</paragraph>' );
180178
} );
181179
} );
182180

@@ -222,6 +220,40 @@ describe( 'transform', () => {
222220

223221
expectClients( '<paragraph>FooBar</paragraph>' );
224222
} );
223+
224+
it( 'remove merged element then undo #3', () => {
225+
john.setData( '[<paragraph>A</paragraph><paragraph>B</paragraph>]<paragraph>C</paragraph>' );
226+
kate.setData( '<paragraph>A</paragraph>[]<paragraph>B</paragraph><paragraph>C</paragraph>' );
227+
228+
kate.merge();
229+
john.remove();
230+
231+
syncClients();
232+
expectClients( '<paragraph>C</paragraph>' );
233+
234+
john.undo();
235+
kate.undo();
236+
237+
syncClients();
238+
expectClients( '<paragraph>A</paragraph><paragraph>B</paragraph><paragraph>C</paragraph>' );
239+
} );
240+
241+
it( 'remove merged element then undo #4', () => {
242+
john.setData( '<paragraph>A</paragraph>[<paragraph>B</paragraph><paragraph>C</paragraph>]' );
243+
kate.setData( '<paragraph>A</paragraph>[]<paragraph>B</paragraph><paragraph>C</paragraph>' );
244+
245+
kate.merge();
246+
john.remove();
247+
248+
syncClients();
249+
expectClients( '<paragraph>A</paragraph>' );
250+
251+
john.undo();
252+
kate.undo();
253+
254+
syncClients();
255+
expectClients( '<paragraph>A</paragraph><paragraph>B</paragraph><paragraph>C</paragraph>' );
256+
} );
225257
} );
226258

227259
describe( 'by delete', () => {
@@ -409,5 +441,23 @@ describe( 'transform', () => {
409441
);
410442
} );
411443
} );
444+
445+
describe( 'by split', () => {
446+
it( 'merge element which got split (the element is in blockquote) and undo', () => {
447+
john.setData( '<paragraph>Foo</paragraph><blockQuote><paragraph>[]Bar</paragraph></blockQuote>' );
448+
kate.setData( '<paragraph>Foo</paragraph><blockQuote><paragraph>B[]ar</paragraph></blockQuote>' );
449+
450+
john._processExecute( 'delete' );
451+
kate.split();
452+
453+
syncClients();
454+
expectClients( '<paragraph>FooB</paragraph><paragraph>ar</paragraph>' );
455+
456+
john.undo();
457+
458+
syncClients();
459+
expectClients( '<paragraph>Foo</paragraph><blockQuote><paragraph>B</paragraph><paragraph>ar</paragraph></blockQuote>' );
460+
} );
461+
} );
412462
} );
413463
} );

0 commit comments

Comments
 (0)