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

Commit 551ab50

Browse files
authoredFeb 18, 2019
Merge pull request #1678 from ckeditor/t/ckeditor5/1385
Fix: Undo and redo no longer crashes in scenarios featuring pasting content into earlier pasted content. Closes [ckeditor/ckeditor5#1385](ckeditor/ckeditor5#1385).
2 parents 7815ab0 + 639f162 commit 551ab50

File tree

2 files changed

+60
-14
lines changed

2 files changed

+60
-14
lines changed
 

‎src/model/operation/transform.js

+22-14
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ export function transformSets( operationsA, operationsB, options ) {
306306
originalOperationsBCount: operationsB.length
307307
};
308308

309-
const contextFactory = new ContextFactory( options.document, options.useRelations );
309+
const contextFactory = new ContextFactory( options.document, options.useRelations, options.forceWeakRemove );
310310
contextFactory.setOriginalOperations( operationsA );
311311
contextFactory.setOriginalOperations( operationsB );
312312

@@ -386,13 +386,17 @@ class ContextFactory {
386386
// @param {module:engine/model/document~Document} document Document which the operations change.
387387
// @param {Boolean} useRelations Whether during transformation relations should be used (used during undo for
388388
// better conflict resolution).
389-
constructor( document, useRelations ) {
389+
// @param {Boolean} [forceWeakRemove=false] If set to `false`, remove operation will be always stronger than move operation,
390+
// so the removed nodes won't end up back in the document root. When set to `true`, context data will be used.
391+
constructor( document, useRelations, forceWeakRemove = false ) {
390392
// `model.History` instance which information about undone operations will be taken from.
391393
this._history = document.history;
392394

393395
// Whether additional context should be used.
394396
this._useRelations = useRelations;
395397

398+
this._forceWeakRemove = !!forceWeakRemove;
399+
396400
// For each operation that is created during transformation process, we keep a reference to the original operation
397401
// which it comes from. The original operation works as a kind of "identifier". Every contextual information
398402
// gathered during transformation that we want to save for given operation, is actually saved for the original operation.
@@ -583,7 +587,8 @@ class ContextFactory {
583587
aWasUndone: this._wasUndone( opA ),
584588
bWasUndone: this._wasUndone( opB ),
585589
abRelation: this._useRelations ? this._getRelation( opA, opB ) : null,
586-
baRelation: this._useRelations ? this._getRelation( opB, opA ) : null
590+
baRelation: this._useRelations ? this._getRelation( opB, opA ) : null,
591+
forceWeakRemove: this._forceWeakRemove
587592
};
588593
}
589594

@@ -1313,7 +1318,7 @@ setTransformation( MergeOperation, MoveOperation, ( a, b, context ) => {
13131318
//
13141319
const removedRange = Range._createFromPositionAndShift( b.sourcePosition, b.howMany );
13151320

1316-
if ( b.type == 'remove' && !context.bWasUndone ) {
1321+
if ( b.type == 'remove' && !context.bWasUndone && !context.forceWeakRemove ) {
13171322
if ( a.deletionPosition.hasSameParentAs( b.sourcePosition ) && removedRange.containsPosition( a.sourcePosition ) ) {
13181323
return [ new NoOperation( 0 ) ];
13191324
}
@@ -1596,9 +1601,9 @@ setTransformation( MoveOperation, MoveOperation, ( a, b, context ) => {
15961601
//
15971602
// If only one of operations is a remove operation, we force remove operation to be the "stronger" one
15981603
// to provide more expected results.
1599-
if ( a.type == 'remove' && b.type != 'remove' && !context.aWasUndone ) {
1604+
if ( a.type == 'remove' && b.type != 'remove' && !context.aWasUndone && !context.forceWeakRemove ) {
16001605
aIsStrong = true;
1601-
} else if ( a.type != 'remove' && b.type == 'remove' && !context.bWasUndone ) {
1606+
} else if ( a.type != 'remove' && b.type == 'remove' && !context.bWasUndone && !context.forceWeakRemove ) {
16021607
aIsStrong = false;
16031608
}
16041609

@@ -1768,7 +1773,7 @@ setTransformation( MoveOperation, SplitOperation, ( a, b, context ) => {
17681773
if ( b.graveyardPosition ) {
17691774
const movesGraveyardElement = moveRange.start.isEqual( b.graveyardPosition ) || moveRange.containsPosition( b.graveyardPosition );
17701775

1771-
if ( a.howMany > 1 && movesGraveyardElement ) {
1776+
if ( a.howMany > 1 && movesGraveyardElement && !context.aWasUndone ) {
17721777
ranges.push( Range._createFromPositionAndShift( b.insertionPosition, 1 ) );
17731778
}
17741779
}
@@ -1780,7 +1785,7 @@ setTransformation( MoveOperation, MergeOperation, ( a, b, context ) => {
17801785
const movedRange = Range._createFromPositionAndShift( a.sourcePosition, a.howMany );
17811786

17821787
if ( b.deletionPosition.hasSameParentAs( a.sourcePosition ) && movedRange.containsPosition( b.sourcePosition ) ) {
1783-
if ( a.type == 'remove' ) {
1788+
if ( a.type == 'remove' && !context.forceWeakRemove ) {
17841789
// Case 1:
17851790
//
17861791
// The element to remove got merged.
@@ -1794,21 +1799,22 @@ setTransformation( MoveOperation, MergeOperation, ( a, b, context ) => {
17941799
const results = [];
17951800

17961801
let gyMoveSource = b.graveyardPosition.clone();
1797-
let splitNodesMoveSource = b.targetPosition.clone();
1802+
let splitNodesMoveSource = b.targetPosition._getTransformedByMergeOperation( b );
17981803

17991804
if ( a.howMany > 1 ) {
18001805
results.push( new MoveOperation( a.sourcePosition, a.howMany - 1, a.targetPosition, 0 ) );
1801-
gyMoveSource = gyMoveSource._getTransformedByInsertion( a.targetPosition, a.howMany - 1 );
1806+
1807+
gyMoveSource = gyMoveSource._getTransformedByMove( a.sourcePosition, a.targetPosition, a.howMany - 1 );
18021808
splitNodesMoveSource = splitNodesMoveSource._getTransformedByMove( a.sourcePosition, a.targetPosition, a.howMany - 1 );
18031809
}
18041810

18051811
const gyMoveTarget = b.deletionPosition._getCombined( a.sourcePosition, a.targetPosition );
18061812
const gyMove = new MoveOperation( gyMoveSource, 1, gyMoveTarget, 0 );
18071813

1808-
const targetPositionPath = gyMove.getMovedRangeStart().path.slice();
1809-
targetPositionPath.push( 0 );
1814+
const splitNodesMoveTargetPath = gyMove.getMovedRangeStart().path.slice();
1815+
splitNodesMoveTargetPath.push( 0 );
18101816

1811-
const splitNodesMoveTarget = new Position( gyMove.targetPosition.root, targetPositionPath );
1817+
const splitNodesMoveTarget = new Position( gyMove.targetPosition.root, splitNodesMoveTargetPath );
18121818
splitNodesMoveSource = splitNodesMoveSource._getTransformedByMove( gyMoveSource, gyMoveTarget, 1 );
18131819
const splitNodesMove = new MoveOperation( splitNodesMoveSource, b.howMany, splitNodesMoveTarget, 0 );
18141820

@@ -2052,7 +2058,9 @@ setTransformation( SplitOperation, MoveOperation, ( a, b, context ) => {
20522058
// is already moved to the correct position, we need to only move the nodes after the split position.
20532059
// This will be done by `MoveOperation` instead of `SplitOperation`.
20542060
//
2055-
if ( rangeToMove.start.isEqual( a.graveyardPosition ) || rangeToMove.containsPosition( a.graveyardPosition ) ) {
2061+
const gyElementMoved = rangeToMove.start.isEqual( a.graveyardPosition ) || rangeToMove.containsPosition( a.graveyardPosition );
2062+
2063+
if ( !context.bWasUndone && gyElementMoved ) {
20562064
const sourcePosition = a.splitPosition._getTransformedByMoveOperation( b );
20572065

20582066
const newParentPosition = a.graveyardPosition._getTransformedByMoveOperation( b );

‎tests/model/operation/transform/undo.js

+38
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55

66
import { Client, expectClients, clearBuffer } from './utils.js';
77

8+
import Element from '../../../../src/model/element';
9+
import Text from '../../../../src/model/text';
10+
811
describe( 'transform', () => {
912
let john;
1013

@@ -574,4 +577,39 @@ describe( 'transform', () => {
574577

575578
expectClients( '<paragraph>XY</paragraph>' );
576579
} );
580+
581+
// https://github.com/ckeditor/ckeditor5/issues/1385
582+
it( 'paste inside paste + undo, undo + redo, redo', () => {
583+
const model = john.editor.model;
584+
585+
john.setData( '<paragraph>[]</paragraph>' );
586+
587+
model.insertContent( getPastedContent() );
588+
589+
john.setSelection( [ 0, 3 ] );
590+
591+
model.insertContent( getPastedContent() );
592+
593+
expectClients( '<heading1>FooFoobarbar</heading1>' );
594+
595+
john.undo();
596+
597+
expectClients( '<heading1>Foobar</heading1>' );
598+
599+
john.undo();
600+
601+
expectClients( '<paragraph></paragraph>' );
602+
603+
john.redo();
604+
605+
expectClients( '<heading1>Foobar</heading1>' );
606+
607+
john.redo();
608+
609+
expectClients( '<heading1>FooFoobarbar</heading1>' );
610+
611+
function getPastedContent() {
612+
return new Element( 'heading1', null, new Text( 'Foobar' ) );
613+
}
614+
} );
577615
} );

0 commit comments

Comments
 (0)
This repository has been archived.