|
|
@@ -3,8 +3,6 @@ import AttributeOperation from './attributeoperation';
|
|
|
import RenameOperation from './renameoperation';
|
|
|
import MarkerOperation from './markeroperation';
|
|
|
import MoveOperation from './moveoperation';
|
|
|
-import RemoveOperation from './removeoperation';
|
|
|
-import ReinsertOperation from './reinsertoperation';
|
|
|
import RootAttributeOperation from './rootattributeoperation';
|
|
|
import MergeOperation from './mergeoperation';
|
|
|
import SplitOperation from './splitoperation';
|
|
|
@@ -13,7 +11,9 @@ import UnwrapOperation from './unwrapoperation';
|
|
|
import NoOperation from './nooperation';
|
|
|
import Range from '../range';
|
|
|
import Position from '../position';
|
|
|
+
|
|
|
import compareArrays from '@ckeditor/ckeditor5-utils/src/comparearrays';
|
|
|
+import log from '@ckeditor/ckeditor5-utils/src/log';
|
|
|
|
|
|
const transformations = new Map();
|
|
|
|
|
|
@@ -29,32 +29,10 @@ function setTransformation( OperationA, OperationB, transformationFunction ) {
|
|
|
}
|
|
|
|
|
|
function getTransformation( a, b ) {
|
|
|
- const OperationA = a.constructor;
|
|
|
- const OperationB = b.constructor;
|
|
|
-
|
|
|
- const aGroups = new Set();
|
|
|
- const aGroup = transformations.get( OperationA );
|
|
|
-
|
|
|
- if ( aGroup ) {
|
|
|
- aGroups.add( aGroup );
|
|
|
- }
|
|
|
-
|
|
|
- for ( const Operation of transformations.keys() ) {
|
|
|
- if ( a instanceof Operation ) {
|
|
|
- aGroups.add( transformations.get( Operation ) );
|
|
|
- }
|
|
|
- }
|
|
|
+ const aGroup = transformations.get( a.constructor );
|
|
|
|
|
|
- for ( const group of aGroups ) {
|
|
|
- if ( group.has( OperationB ) ) {
|
|
|
- return group.get( OperationB );
|
|
|
- }
|
|
|
-
|
|
|
- for ( const Operation of group.keys() ) {
|
|
|
- if ( b instanceof Operation ) {
|
|
|
- return group.get( Operation );
|
|
|
- }
|
|
|
- }
|
|
|
+ if ( aGroup && aGroup.has( b.constructor ) ) {
|
|
|
+ return aGroup.get( b.constructor );
|
|
|
}
|
|
|
|
|
|
return noUpdateTransformation;
|
|
|
@@ -72,13 +50,26 @@ function updateBaseVersions( operations, baseVersion ) {
|
|
|
return operations;
|
|
|
}
|
|
|
|
|
|
-function transform( a, b, context = { aIsStrong: false } ) {
|
|
|
+export function transform( a, b, context = {} ) {
|
|
|
const transformationFunction = getTransformation( a, b );
|
|
|
|
|
|
- return transformationFunction( a.clone(), b, context );
|
|
|
+ try {
|
|
|
+ return transformationFunction( a.clone(), b, context );
|
|
|
+ } catch ( e ) {
|
|
|
+ log.error( 'Error during operation transformation!' );
|
|
|
+ log.error( 'Transformed operation', a );
|
|
|
+ log.error( 'Operation transformed by', b );
|
|
|
+ log.error( 'context.aIsStrong', context.aIsStrong );
|
|
|
+ log.error( 'context.aWasUndone', context.aWasUndone );
|
|
|
+ log.error( 'context.bWasUndone', context.bWasUndone );
|
|
|
+ log.error( 'context.abRelation', context.abRelation );
|
|
|
+ log.error( 'context.baRelation', context.baRelation );
|
|
|
+
|
|
|
+ throw e;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-function transformSets( operationsA, operationsB, options ) {
|
|
|
+export function transformSets( operationsA, operationsB, options ) {
|
|
|
operationsA = operationsA.slice();
|
|
|
operationsB = operationsB.slice();
|
|
|
|
|
|
@@ -113,17 +104,34 @@ function transformSets( operationsA, operationsB, options ) {
|
|
|
const opA = opsA[ k ];
|
|
|
const opB = opsB[ l ];
|
|
|
|
|
|
- context.aIsStrong = true;
|
|
|
- const newOpA = transform( opA, opB, context );
|
|
|
-
|
|
|
- context.aIsStrong = false;
|
|
|
- const newOpB = transform( opB, opA, context );
|
|
|
+ if ( options.useContext ) {
|
|
|
+ updateRelations( context, opA, opB );
|
|
|
+ }
|
|
|
|
|
|
- delete context.aIsStrong;
|
|
|
+ const contextAB = {
|
|
|
+ aIsStrong: true,
|
|
|
+ aWasUndone: context.wasUndone( opA ),
|
|
|
+ bWasUndone: context.wasUndone( opB ),
|
|
|
+ abRelation: context.getRelation( opA, opB ),
|
|
|
+ baRelation: context.getRelation( opB, opA )
|
|
|
+ };
|
|
|
+
|
|
|
+ const contextBA = {
|
|
|
+ aIsStrong: false,
|
|
|
+ aWasUndone: context.wasUndone( opB ),
|
|
|
+ bWasUndone: context.wasUndone( opA ),
|
|
|
+ abRelation: context.getRelation( opB, opA ),
|
|
|
+ baRelation: context.getRelation( opA, opB )
|
|
|
+ };
|
|
|
+
|
|
|
+ const newOpA = transform( opA, opB, contextAB );
|
|
|
+ const newOpB = transform( opB, opA, contextBA );
|
|
|
|
|
|
if ( options.useContext ) {
|
|
|
updateOriginalOperation( context, opA, newOpA );
|
|
|
updateOriginalOperation( context, opB, newOpB );
|
|
|
+
|
|
|
+ updateRelations( context, opA, opB );
|
|
|
}
|
|
|
|
|
|
opsA.splice( k, 1, ...newOpA );
|
|
|
@@ -155,11 +163,6 @@ function transformSets( operationsA, operationsB, options ) {
|
|
|
return { operationsA, operationsB };
|
|
|
}
|
|
|
|
|
|
-export default {
|
|
|
- transform,
|
|
|
- transformSets
|
|
|
-};
|
|
|
-
|
|
|
function padWithNoOps( operations, howMany ) {
|
|
|
for ( let i = 0; i < howMany; i++ ) {
|
|
|
operations.push( new NoOperation( 0 ) );
|
|
|
@@ -176,6 +179,7 @@ function initializeContext( opsA, opsB, options ) {
|
|
|
}
|
|
|
|
|
|
context.document = options.document;
|
|
|
+ context.relations = new Map();
|
|
|
|
|
|
context.wasUndone = function( op ) {
|
|
|
if ( !options.useContext ) {
|
|
|
@@ -187,9 +191,152 @@ function initializeContext( opsA, opsB, options ) {
|
|
|
return this.document.history.isUndoneOperation( originalOp );
|
|
|
};
|
|
|
|
|
|
+ context.getRelation = function( opA, opB ) {
|
|
|
+ if ( !options.useContext ) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ const origB = this.originalOperations.get( opB );
|
|
|
+ const undoneB = this.document.history.getUndoneOperation( origB );
|
|
|
+
|
|
|
+ if ( !undoneB ) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ const origA = this.originalOperations.get( opA );
|
|
|
+ const relationsA = this.relations.get( origA );
|
|
|
+
|
|
|
+ if ( relationsA ) {
|
|
|
+ return relationsA.get( undoneB ) || null;
|
|
|
+ }
|
|
|
+
|
|
|
+ return null;
|
|
|
+ };
|
|
|
+
|
|
|
return context;
|
|
|
}
|
|
|
|
|
|
+function updateRelations( context, opA, opB ) {
|
|
|
+ switch ( opA.constructor ) {
|
|
|
+ case MoveOperation: {
|
|
|
+ switch ( opB.constructor ) {
|
|
|
+ case MergeOperation: {
|
|
|
+ if ( opA.targetPosition.isEqual( opB.sourcePosition ) || opB.movedRange.containsPosition( opA.targetPosition ) ) {
|
|
|
+ setRelation( context, opA, opB, 'insertAtSource' );
|
|
|
+ setRelation( context, opB, opA, 'splitBefore' );
|
|
|
+ }
|
|
|
+
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ case MoveOperation: {
|
|
|
+ if ( opA.targetPosition.isEqual( opB.sourcePosition ) || opA.targetPosition.isBefore( opB.sourcePosition ) ) {
|
|
|
+ setRelation( context, opA, opB, 'insertBefore' );
|
|
|
+ setRelation( context, opB, opA, 'insertAfter' );
|
|
|
+ } else {
|
|
|
+ setRelation( context, opA, opB, 'insertAfter' );
|
|
|
+ setRelation( context, opB, opA, 'insertBefore' );
|
|
|
+ }
|
|
|
+
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ case UnwrapOperation: {
|
|
|
+ const isInside = opA.targetPosition.hasSameParentAs( opB.targetPosition );
|
|
|
+
|
|
|
+ if ( isInside ) {
|
|
|
+ setRelation( context, opA, opB, 'insertInside' );
|
|
|
+ }
|
|
|
+
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ case SplitOperation: {
|
|
|
+ switch ( opB.constructor ) {
|
|
|
+ case MergeOperation: {
|
|
|
+ if ( opA.position.isBefore( opB.sourcePosition ) ) {
|
|
|
+ setRelation( context, opA, opB, 'splitBefore' );
|
|
|
+ setRelation( context, opB, opA, 'splitAfter' );
|
|
|
+ }
|
|
|
+
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ case MoveOperation: {
|
|
|
+ if ( opA.position.isEqual( opB.sourcePosition ) || opA.position.isBefore( opB.sourcePosition ) ) {
|
|
|
+ setRelation( context, opA, opB, 'splitBefore' );
|
|
|
+ setRelation( context, opB, opA, 'insertAtSource' );
|
|
|
+ }
|
|
|
+
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ case UnwrapOperation: {
|
|
|
+ const isInside = opA.position.hasSameParentAs( opB.position );
|
|
|
+
|
|
|
+ if ( isInside ) {
|
|
|
+ setRelation( context, opA, opB, 'splitInside' );
|
|
|
+ }
|
|
|
+
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ case InsertOperation: {
|
|
|
+ switch ( opB.constructor ) {
|
|
|
+ case MergeOperation: {
|
|
|
+ if ( opA.position.isEqual( opB.sourcePosition ) || opB.movedRange.containsPosition( opA.position ) ) {
|
|
|
+ setRelation( context, opA, opB, 'insertAtSource' );
|
|
|
+ }
|
|
|
+
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ case MoveOperation: {
|
|
|
+ if ( opA.position.isEqual( opB.sourcePosition ) || opA.position.isBefore( opB.sourcePosition ) ) {
|
|
|
+ setRelation( context, opA, opB, 'insertBefore' );
|
|
|
+ }
|
|
|
+
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ case UnwrapOperation: {
|
|
|
+ const isInside = opA.position.hasSameParentAs( opB.position );
|
|
|
+
|
|
|
+ if ( isInside ) {
|
|
|
+ setRelation( context, opA, opB, 'insertInside' );
|
|
|
+ }
|
|
|
+
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function setRelation( context, opA, opB, relation ) {
|
|
|
+ const origA = context.originalOperations.get( opA );
|
|
|
+ const origB = context.originalOperations.get( opB );
|
|
|
+
|
|
|
+ let relationsA = context.relations.get( origA );
|
|
|
+
|
|
|
+ if ( !relationsA ) {
|
|
|
+ relationsA = new Map();
|
|
|
+ context.relations.set( origA, relationsA );
|
|
|
+ }
|
|
|
+
|
|
|
+ relationsA.set( origB, relation );
|
|
|
+}
|
|
|
+
|
|
|
function updateOriginalOperation( context, oldOp, newOps ) {
|
|
|
const originalOp = context.originalOperations.get( oldOp );
|
|
|
|
|
|
@@ -269,7 +416,7 @@ setTransformation( AttributeOperation, InsertOperation, ( a, b ) => {
|
|
|
// <p>Fo[zb]ar<p>
|
|
|
//
|
|
|
// New text with `highlight="red"` is typed:
|
|
|
- // <p>Fo[z<$text higlight="red">x</$text>a]r</p>
|
|
|
+ // <p>Fo[z<$text highlight="red">x</$text>a]r</p>
|
|
|
//
|
|
|
// In this case three operations are needed: `oldValue=null, newValue="yellow"` for `z`, `oldValue="red",
|
|
|
// newValue="yellow"` for `x` and `oldValue=null, newValue="yellow"` for `a`. It could even happen that
|
|
|
@@ -345,6 +492,8 @@ setTransformation( AttributeOperation, MergeOperation, ( a, b ) => {
|
|
|
|
|
|
// Case 1: Attribute change on the merged element. In this case, the merged element was moved to graveyard.
|
|
|
// An additional attribute operation that will change the (re)moved element needs to be generated.
|
|
|
+ // Do it only, if there is more than one element in attribute range. If there is only one element,
|
|
|
+ // it will be handled by the default algorithm.
|
|
|
//
|
|
|
if ( a.range.start.hasSameParentAs( b.deletionPosition ) ) {
|
|
|
if ( a.range.containsPosition( b.deletionPosition ) || a.range.start.isEqual( b.deletionPosition ) ) {
|
|
|
@@ -352,7 +501,12 @@ setTransformation( AttributeOperation, MergeOperation, ( a, b ) => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- ranges.push( a.range._getTransformedByMergeOperation( b ) );
|
|
|
+ const range = a.range._getTransformedByMergeOperation( b );
|
|
|
+
|
|
|
+ // Do not add empty (collapsed) ranges to the result. `range` may be collapsed if it contained only the merged element.
|
|
|
+ if ( !range.isCollapsed ) {
|
|
|
+ ranges.push( range );
|
|
|
+ }
|
|
|
|
|
|
// Create `AttributeOperation`s out of the ranges.
|
|
|
return ranges.map( range => {
|
|
|
@@ -361,20 +515,10 @@ setTransformation( AttributeOperation, MergeOperation, ( a, b ) => {
|
|
|
} );
|
|
|
|
|
|
setTransformation( AttributeOperation, MoveOperation, ( a, b ) => {
|
|
|
- const movedRange = Range.createFromPositionAndShift( b.sourcePosition, b.howMany );
|
|
|
const ranges = breakRangeByMoveOperation( a.range, b, true );
|
|
|
|
|
|
// Create `AttributeOperation`s out of the ranges.
|
|
|
- return ranges.map( range => {
|
|
|
- if ( movedRange.containsRange( range, true ) ) {
|
|
|
- range = range._getTransformedByMoveOperation( b, false )[ 0 ];
|
|
|
- } else {
|
|
|
- range = range._getTransformedByDeletion( b.sourcePosition, b.howMany );
|
|
|
- range = range._getTransformedByInsertion( b.targetPosition, b.howMany, false )[ 0 ];
|
|
|
- }
|
|
|
-
|
|
|
- return new AttributeOperation( range, a.key, a.oldValue, a.newValue, a.baseVersion );
|
|
|
- } );
|
|
|
+ return ranges.map( range => new AttributeOperation( range, a.key, a.oldValue, a.newValue, a.baseVersion ) );
|
|
|
} );
|
|
|
|
|
|
function breakRangeByMoveOperation( range, moveOp, includeCommon ) {
|
|
|
@@ -382,32 +526,48 @@ function breakRangeByMoveOperation( range, moveOp, includeCommon ) {
|
|
|
|
|
|
const movedRange = Range.createFromPositionAndShift( moveOp.sourcePosition, moveOp.howMany );
|
|
|
|
|
|
- if ( range.start.hasSameParentAs( moveOp.sourcePosition ) ) {
|
|
|
+ if ( movedRange.containsRange( range, true ) ) {
|
|
|
+ ranges = [ ...range._getTransformedByMoveOperation( moveOp ) ];
|
|
|
+ } else if ( range.start.hasSameParentAs( moveOp.sourcePosition ) ) {
|
|
|
ranges = range.getDifference( movedRange );
|
|
|
|
|
|
+ _flatMoveTransform( ranges, moveOp );
|
|
|
+
|
|
|
if ( includeCommon ) {
|
|
|
const common = range.getIntersection( movedRange );
|
|
|
|
|
|
if ( common ) {
|
|
|
- ranges.push( common );
|
|
|
+ ranges.push( ...common._getTransformedByMoveOperation( moveOp ) );
|
|
|
}
|
|
|
}
|
|
|
} else {
|
|
|
ranges = [ range ];
|
|
|
+
|
|
|
+ _flatMoveTransform( ranges, moveOp );
|
|
|
}
|
|
|
|
|
|
+ return ranges;
|
|
|
+}
|
|
|
+
|
|
|
+function _flatMoveTransform( ranges, moveOp ) {
|
|
|
+ const targetPosition = moveOp.getMovedRangeStart();
|
|
|
+
|
|
|
for ( let i = 0; i < ranges.length; i++ ) {
|
|
|
- const range = ranges[ i ];
|
|
|
+ let range = ranges[ i ];
|
|
|
|
|
|
- if ( range.start.hasSameParentAs( moveOp.targetPosition ) && range.containsPosition( moveOp.targetPosition ) ) {
|
|
|
- ranges.splice( i, 1,
|
|
|
- new Range( range.start, moveOp.targetPosition ),
|
|
|
- new Range( moveOp.targetPosition, range.end )
|
|
|
- );
|
|
|
+ if ( range.start.hasSameParentAs( moveOp.sourcePosition ) ) {
|
|
|
+ range = range._getTransformedByDeletion( moveOp.sourcePosition, moveOp.howMany );
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- return ranges;
|
|
|
+ if ( range.start.hasSameParentAs( targetPosition ) ) {
|
|
|
+ const result = range._getTransformedByInsertion( targetPosition, moveOp.howMany, true );
|
|
|
+
|
|
|
+ ranges.splice( i, 1, ...result );
|
|
|
+ i = i - 1 + result.length;
|
|
|
+ } else {
|
|
|
+ ranges[ i ] = range;
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
setTransformation( AttributeOperation, SplitOperation, ( a, b ) => {
|
|
|
@@ -443,10 +603,13 @@ setTransformation( AttributeOperation, SplitOperation, ( a, b ) => {
|
|
|
if ( a.range.start.hasSameParentAs( b.position ) && a.range.containsPosition( b.position ) ) {
|
|
|
const secondPart = a.clone();
|
|
|
|
|
|
- secondPart.range.start = Position.createFromPosition( b.moveTargetPosition );
|
|
|
- secondPart.range.end = a.range.end._getCombined( b.position, b.moveTargetPosition );
|
|
|
+ secondPart.range = new Range(
|
|
|
+ Position.createFromPosition( b.moveTargetPosition ),
|
|
|
+ a.range.end._getCombined( b.position, b.moveTargetPosition )
|
|
|
+ );
|
|
|
|
|
|
a.range.end = Position.createFromPosition( b.position );
|
|
|
+ a.range.end.stickiness = 'toPrevious';
|
|
|
|
|
|
return [ a, secondPart ];
|
|
|
}
|
|
|
@@ -541,13 +704,23 @@ setTransformation( InsertOperation, InsertOperation, ( a, b, context ) => {
|
|
|
return [ a ];
|
|
|
} );
|
|
|
|
|
|
-setTransformation( InsertOperation, MoveOperation, ( a, b ) => {
|
|
|
+setTransformation( InsertOperation, MoveOperation, ( a, b, context ) => {
|
|
|
+ if ( a.position.isEqual( b.targetPosition ) && context.abRelation == 'insertBefore' ) {
|
|
|
+ return [ a ];
|
|
|
+ }
|
|
|
+
|
|
|
a.position = a.position._getTransformedByMoveOperation( b );
|
|
|
|
|
|
return [ a ];
|
|
|
} );
|
|
|
|
|
|
-setTransformation( InsertOperation, SplitOperation, ( a, b ) => {
|
|
|
+setTransformation( InsertOperation, SplitOperation, ( a, b, context ) => {
|
|
|
+ if ( a.position.isEqual( b.position ) && context.abRelation == 'insertAtSource' ) {
|
|
|
+ a.position = b.moveTargetPosition;
|
|
|
+
|
|
|
+ return [ a ];
|
|
|
+ }
|
|
|
+
|
|
|
a.position = a.position._getTransformedBySplitOperation( b );
|
|
|
|
|
|
return [ a ];
|
|
|
@@ -559,7 +732,13 @@ setTransformation( InsertOperation, MergeOperation, ( a, b ) => {
|
|
|
return [ a ];
|
|
|
} );
|
|
|
|
|
|
-setTransformation( InsertOperation, WrapOperation, ( a, b ) => {
|
|
|
+setTransformation( InsertOperation, WrapOperation, ( a, b, context ) => {
|
|
|
+ if ( a.position.isEqual( b.position ) && context.abRelation == 'insertInside' ) {
|
|
|
+ a.position = b.targetPosition;
|
|
|
+
|
|
|
+ return [ a ];
|
|
|
+ }
|
|
|
+
|
|
|
a.position = a.position._getTransformedByWrapOperation( b );
|
|
|
|
|
|
return [ a ];
|
|
|
@@ -707,7 +886,7 @@ setTransformation( MergeOperation, MoveOperation, ( a, b, context ) => {
|
|
|
//
|
|
|
const removedRange = Range.createFromPositionAndShift( b.sourcePosition, b.howMany );
|
|
|
|
|
|
- if ( b instanceof RemoveOperation && !context.wasUndone( b ) ) {
|
|
|
+ if ( b.type == 'remove' && !context.bWasUndone ) {
|
|
|
if ( a.deletionPosition.hasSameParentAs( b.sourcePosition ) && removedRange.containsPosition( a.sourcePosition ) ) {
|
|
|
return getNoOp();
|
|
|
}
|
|
|
@@ -716,7 +895,7 @@ setTransformation( MergeOperation, MoveOperation, ( a, b, context ) => {
|
|
|
a.sourcePosition = a.sourcePosition._getTransformedByMoveOperation( b );
|
|
|
a.targetPosition = a.targetPosition._getTransformedByMoveOperation( b );
|
|
|
|
|
|
- if ( !a.graveyardPosition.isEqual( b.targetPosition ) || !context.aIsStrong ) {
|
|
|
+ if ( !a.graveyardPosition.isEqual( b.targetPosition ) ) {
|
|
|
a.graveyardPosition = a.graveyardPosition._getTransformedByMoveOperation( b );
|
|
|
}
|
|
|
|
|
|
@@ -724,8 +903,6 @@ setTransformation( MergeOperation, MoveOperation, ( a, b, context ) => {
|
|
|
} );
|
|
|
|
|
|
setTransformation( MergeOperation, SplitOperation, ( a, b ) => {
|
|
|
- a.sourcePosition = a.sourcePosition._getTransformedBySplitOperation( b );
|
|
|
-
|
|
|
if ( b.graveyardPosition ) {
|
|
|
a.graveyardPosition = a.graveyardPosition._getTransformedByDeletion( b.graveyardPosition, 1 );
|
|
|
}
|
|
|
@@ -744,13 +921,18 @@ setTransformation( MergeOperation, SplitOperation, ( a, b ) => {
|
|
|
// This means that `targetPosition` needs to be transformed. This is the default case though.
|
|
|
// For example, if the split would be after `F`, `targetPosition` should also be transformed.
|
|
|
//
|
|
|
- // There is one exception though - when the split operation is a result of undo. In those cases, it is needed
|
|
|
- // to keep `targetPosition` intact, so the nodes are returned to the correct element.
|
|
|
+ // There is an exception though. It is when merge operation targets into inside of an element.
|
|
|
+ // Such merge operation can be a result of merge x merge transformation, when merges are identical.
|
|
|
+ // Such merge operation's source position is in graveyard and we will use that to recognize it
|
|
|
+ // (although a more precise method would be more correct).
|
|
|
//
|
|
|
- if ( a.targetPosition.isEqual( b.position ) && b.graveyardPosition ) {
|
|
|
+ if ( a.targetPosition.isEqual( b.position ) && a.sourcePosition.root.rootName == '$graveyard' ) {
|
|
|
+ a.sourcePosition = a.sourcePosition._getTransformedBySplitOperation( b );
|
|
|
+
|
|
|
return [ a ];
|
|
|
}
|
|
|
|
|
|
+ a.sourcePosition = a.sourcePosition._getTransformedBySplitOperation( b );
|
|
|
a.targetPosition = a.targetPosition._getTransformedBySplitOperation( b );
|
|
|
|
|
|
return [ a ];
|
|
|
@@ -774,6 +956,23 @@ setTransformation( MergeOperation, WrapOperation, ( a, b ) => {
|
|
|
return [ a ];
|
|
|
}
|
|
|
|
|
|
+ // Case 2: Merged element is wrapped and this is the last (only) element in the wrap.
|
|
|
+ // Because of how this is resolved in `WrapOperation` x `MergeOperation`, we need to apply special handling here.
|
|
|
+ // If the last element from wrapped range is "removed" from it, the wrap is effectively on empty range.
|
|
|
+ // In that case, the wrapper element is moved to graveyard. This happens in `WrapOperation` x
|
|
|
+ // `MergeOperation` and we need to mirror it here.
|
|
|
+ //
|
|
|
+ if ( b.position.isEqual( a.deletionPosition ) && b.howMany == 1 ) {
|
|
|
+ // We need to change `MergeOperation#graveyardPosition` so the merged node is moved into the wrapper element.
|
|
|
+ // Since `UnwrapOperation` created from reverse has graveyard position at [ 0 ], we can safely set the path here to [ 0, 0 ].
|
|
|
+ a.graveyardPosition = new Position( a.graveyardPosition.root, [ 0, 0 ] );
|
|
|
+
|
|
|
+ return [
|
|
|
+ b.getReversed(),
|
|
|
+ a
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
a.sourcePosition = a.sourcePosition._getTransformedByWrapOperation( b );
|
|
|
a.targetPosition = a.targetPosition._getTransformedByWrapOperation( b );
|
|
|
|
|
|
@@ -814,14 +1013,14 @@ setTransformation( MergeOperation, UnwrapOperation, ( a, b, context ) => {
|
|
|
|
|
|
// -----------------------
|
|
|
|
|
|
-setTransformation( MoveOperation, InsertOperation, ( a, b ) => {
|
|
|
+setTransformation( MoveOperation, InsertOperation, ( a, b, context ) => {
|
|
|
const moveRange = Range.createFromPositionAndShift( a.sourcePosition, a.howMany );
|
|
|
const transformed = moveRange._getTransformedByInsertOperation( b, false )[ 0 ];
|
|
|
|
|
|
a.sourcePosition = transformed.start;
|
|
|
a.howMany = transformed.end.offset - transformed.start.offset;
|
|
|
|
|
|
- if ( !a.targetPosition.isEqual( b.position ) ) {
|
|
|
+ if ( !a.targetPosition.isEqual( b.position ) || context.abRelation == 'insertBefore' ) {
|
|
|
a.targetPosition = a.targetPosition._getTransformedByInsertOperation( b );
|
|
|
}
|
|
|
|
|
|
@@ -840,12 +1039,27 @@ setTransformation( MoveOperation, MoveOperation, ( a, b, context ) => {
|
|
|
// this algorithm and we do not want to override original `context.aIsStrong` that will be used in later transformations.
|
|
|
let aIsStrong = context.aIsStrong;
|
|
|
|
|
|
+ if ( context.abRelation == 'insertBefore' ) {
|
|
|
+ aIsStrong = true;
|
|
|
+ } else if ( context.abRelation == 'insertAfter' ) {
|
|
|
+ aIsStrong = false;
|
|
|
+ }
|
|
|
+
|
|
|
// `a.targetPosition` could be affected by the `b` operation. We will transform it.
|
|
|
- const newTargetPosition = a.targetPosition._getTransformedByMove(
|
|
|
- b.sourcePosition,
|
|
|
- b.targetPosition,
|
|
|
- b.howMany
|
|
|
- );
|
|
|
+ let newTargetPosition;
|
|
|
+
|
|
|
+ if ( a.targetPosition.isEqual( b.targetPosition ) && aIsStrong ) {
|
|
|
+ newTargetPosition = a.targetPosition._getTransformedByDeletion(
|
|
|
+ b.sourcePosition,
|
|
|
+ b.howMany
|
|
|
+ );
|
|
|
+ } else {
|
|
|
+ newTargetPosition = a.targetPosition._getTransformedByMove(
|
|
|
+ b.sourcePosition,
|
|
|
+ b.targetPosition,
|
|
|
+ b.howMany
|
|
|
+ );
|
|
|
+ }
|
|
|
|
|
|
//
|
|
|
// Special case #1 + mirror.
|
|
|
@@ -926,9 +1140,9 @@ setTransformation( MoveOperation, MoveOperation, ( a, b, context ) => {
|
|
|
//
|
|
|
// If only one of operations is a remove operation, we force remove operation to be the "stronger" one
|
|
|
// to provide more expected results.
|
|
|
- if ( a instanceof RemoveOperation && !( b instanceof RemoveOperation ) ) {
|
|
|
+ if ( a.type == 'remove' && b.type != 'remove' ) {
|
|
|
aIsStrong = true;
|
|
|
- } else if ( !( a instanceof RemoveOperation ) && b instanceof RemoveOperation ) {
|
|
|
+ } else if ( a.type != 'remove' && b.type == 'remove' ) {
|
|
|
aIsStrong = false;
|
|
|
}
|
|
|
|
|
|
@@ -993,7 +1207,7 @@ setTransformation( MoveOperation, MoveOperation, ( a, b, context ) => {
|
|
|
return makeMoveOperationsFromRanges( ranges, newTargetPosition );
|
|
|
} );
|
|
|
|
|
|
-setTransformation( MoveOperation, SplitOperation, ( a, b ) => {
|
|
|
+setTransformation( MoveOperation, SplitOperation, ( a, b, context ) => {
|
|
|
const newTargetPosition = a.targetPosition._getTransformedBySplitOperation( b );
|
|
|
|
|
|
// Case 1: Last element in the moved range got split.
|
|
|
@@ -1028,8 +1242,8 @@ setTransformation( MoveOperation, SplitOperation, ( a, b ) => {
|
|
|
rightRange = rightRange._getTransformedBySplitOperation( b );
|
|
|
|
|
|
const ranges = [
|
|
|
- rightRange,
|
|
|
- Range.createFromPositionAndShift( moveRange.start, b.position.offset - moveRange.start.offset )
|
|
|
+ new Range( moveRange.start, b.position ),
|
|
|
+ rightRange
|
|
|
];
|
|
|
|
|
|
return makeMoveOperationsFromRanges( ranges, newTargetPosition );
|
|
|
@@ -1043,6 +1257,10 @@ setTransformation( MoveOperation, SplitOperation, ( a, b ) => {
|
|
|
a.howMany = transformed.end.offset - transformed.start.offset;
|
|
|
a.targetPosition = newTargetPosition;
|
|
|
|
|
|
+ if ( a.targetPosition.isEqual( b.position ) && context.abRelation == 'insertAtSource' ) {
|
|
|
+ a.targetPosition = b.targetPosition;
|
|
|
+ }
|
|
|
+
|
|
|
return [ a ];
|
|
|
} );
|
|
|
|
|
|
@@ -1050,14 +1268,14 @@ setTransformation( MoveOperation, MergeOperation, ( a, b, context ) => {
|
|
|
const movedRange = Range.createFromPositionAndShift( a.sourcePosition, a.howMany );
|
|
|
|
|
|
if ( b.deletionPosition.hasSameParentAs( a.sourcePosition ) && movedRange.containsPosition( b.sourcePosition ) ) {
|
|
|
- if ( a instanceof RemoveOperation ) {
|
|
|
+ if ( a.type == 'remove' ) {
|
|
|
// Case 1: The element to remove got merged.
|
|
|
// Merge operation does support merging elements which are not siblings. So it would not be a problem
|
|
|
// from technical point of view. However, if the element was removed, the intention of the user
|
|
|
// deleting it was to have it all deleted. From user experience point of view, moving back the
|
|
|
// removed nodes might be unexpected. This means that in this scenario we will reverse merging and remove the element.
|
|
|
//
|
|
|
- if ( !context.wasUndone( a ) ) {
|
|
|
+ if ( !context.aWasUndone ) {
|
|
|
return [ b.getReversed(), a ];
|
|
|
}
|
|
|
} else {
|
|
|
@@ -1081,7 +1299,7 @@ setTransformation( MoveOperation, MergeOperation, ( a, b, context ) => {
|
|
|
return [ a ];
|
|
|
} );
|
|
|
|
|
|
-setTransformation( MoveOperation, WrapOperation, ( a, b ) => {
|
|
|
+setTransformation( MoveOperation, WrapOperation, ( a, b, context ) => {
|
|
|
const moveRange = Range.createFromPositionAndShift( a.sourcePosition, a.howMany );
|
|
|
const newTargetPosition = a.targetPosition._getTransformedByWrapOperation( b );
|
|
|
|
|
|
@@ -1133,6 +1351,10 @@ setTransformation( MoveOperation, WrapOperation, ( a, b ) => {
|
|
|
a.howMany = transformed.end.offset - transformed.start.offset;
|
|
|
a.targetPosition = newTargetPosition;
|
|
|
|
|
|
+ if ( a.targetPosition.isEqual( b.position ) && context.abRelation == 'insertInside' ) {
|
|
|
+ a.targetPosition = b.targetPosition;
|
|
|
+ }
|
|
|
+
|
|
|
return [ a ];
|
|
|
} );
|
|
|
|
|
|
@@ -1156,6 +1378,13 @@ setTransformation( RenameOperation, InsertOperation, ( a, b ) => {
|
|
|
} );
|
|
|
|
|
|
setTransformation( RenameOperation, MergeOperation, ( a, b ) => {
|
|
|
+ if ( a.position.isEqual( b.deletionPosition ) ) {
|
|
|
+ a.position = Position.createFromPosition( b.graveyardPosition );
|
|
|
+ a.position.stickiness = 'toNext';
|
|
|
+
|
|
|
+ return [ a ];
|
|
|
+ }
|
|
|
+
|
|
|
a.position = a.position._getTransformedByMergeOperation( b );
|
|
|
|
|
|
return [ a ];
|
|
|
@@ -1235,7 +1464,11 @@ setTransformation( RootAttributeOperation, RootAttributeOperation, ( a, b, conte
|
|
|
|
|
|
// -----------------------
|
|
|
|
|
|
-setTransformation( SplitOperation, InsertOperation, ( a, b ) => {
|
|
|
+setTransformation( SplitOperation, InsertOperation, ( a, b, context ) => {
|
|
|
+ if ( a.position.isEqual( b.position ) && context.baRelation == 'insertAtSource' ) {
|
|
|
+ return [ a ];
|
|
|
+ }
|
|
|
+
|
|
|
a.position = a.position._getTransformedByInsertOperation( b );
|
|
|
|
|
|
return [ a ];
|
|
|
@@ -1251,7 +1484,7 @@ setTransformation( SplitOperation, MergeOperation, ( a, b ) => {
|
|
|
return [ a ];
|
|
|
} );
|
|
|
|
|
|
-setTransformation( SplitOperation, MoveOperation, ( a, b ) => {
|
|
|
+setTransformation( SplitOperation, MoveOperation, ( a, b, context ) => {
|
|
|
if ( a.graveyardPosition ) {
|
|
|
a.graveyardPosition = a.graveyardPosition._getTransformedByMoveOperation( b );
|
|
|
}
|
|
|
@@ -1279,6 +1512,12 @@ setTransformation( SplitOperation, MoveOperation, ( a, b ) => {
|
|
|
return [ a ];
|
|
|
}
|
|
|
|
|
|
+ if ( a.position.isEqual( b.targetPosition ) && context.abRelation == 'splitBefore' ) {
|
|
|
+ a.position = a.position._getTransformedByDeletion( b.sourcePosition, b.howMany );
|
|
|
+
|
|
|
+ return [ a ];
|
|
|
+ }
|
|
|
+
|
|
|
// The default case.
|
|
|
//
|
|
|
a.position = a.position._getTransformedByMoveOperation( b );
|
|
|
@@ -1286,7 +1525,7 @@ setTransformation( SplitOperation, MoveOperation, ( a, b ) => {
|
|
|
return [ a ];
|
|
|
} );
|
|
|
|
|
|
-setTransformation( SplitOperation, SplitOperation, ( a, b ) => {
|
|
|
+setTransformation( SplitOperation, SplitOperation, ( a, b, context ) => {
|
|
|
if ( a.position.isEqual( b.position ) ) {
|
|
|
if ( !a.graveyardPosition && !b.graveyardPosition ) {
|
|
|
return getNoOp();
|
|
|
@@ -1295,6 +1534,8 @@ setTransformation( SplitOperation, SplitOperation, ( a, b ) => {
|
|
|
if ( a.graveyardPosition && b.graveyardPosition && a.graveyardPosition.isEqual( b.graveyardPosition ) ) {
|
|
|
return getNoOp();
|
|
|
}
|
|
|
+ } else if ( a.position.isEqual( b.insertionPosition ) && context.abRelation == 'splitBefore' ) {
|
|
|
+ return [ a ];
|
|
|
} else {
|
|
|
a.position = a.position._getTransformedBySplitOperation( b );
|
|
|
}
|
|
|
@@ -1306,7 +1547,7 @@ setTransformation( SplitOperation, SplitOperation, ( a, b ) => {
|
|
|
return [ a ];
|
|
|
} );
|
|
|
|
|
|
-setTransformation( SplitOperation, WrapOperation, ( a, b ) => {
|
|
|
+setTransformation( SplitOperation, WrapOperation, ( a, b, context ) => {
|
|
|
// Case 1: If split position has been wrapped, reverse the wrapping so that split can be applied as intended.
|
|
|
// This is an edge case scenario where it is difficult to find a correct solution.
|
|
|
// Since it will be a rare (or only theoretical) scenario, the algorithm will perform the easy solution.
|
|
|
@@ -1324,20 +1565,33 @@ setTransformation( SplitOperation, WrapOperation, ( a, b ) => {
|
|
|
return [ reversed, a ];
|
|
|
}
|
|
|
|
|
|
- a.position = a.position._getTransformedByWrapOperation( b );
|
|
|
+ if ( a.position.isEqual( b.position ) && context.abRelation == 'splitInside' ) {
|
|
|
+ a.position = b.targetPosition;
|
|
|
+ } else {
|
|
|
+ a.position = a.position._getTransformedByWrapOperation( b );
|
|
|
+ }
|
|
|
|
|
|
- if ( a.graveyardPosition && b.graveyardPosition ) {
|
|
|
- a.graveyardPosition = a.graveyardPosition._getTransformedByDeletion( b.graveyardPosition, 1 );
|
|
|
+ if ( a.graveyardPosition ) {
|
|
|
+ a.graveyardPosition = a.graveyardPosition._getTransformedByWrapOperation( b );
|
|
|
}
|
|
|
|
|
|
return [ a ];
|
|
|
} );
|
|
|
|
|
|
-setTransformation( SplitOperation, UnwrapOperation, ( a, b ) => {
|
|
|
- a.position = a.position._getTransformedByUnwrapOperation( b );
|
|
|
+setTransformation( SplitOperation, UnwrapOperation, ( a, b, context ) => {
|
|
|
+ const splitInside = a.position.hasSameParentAs( b.position );
|
|
|
+
|
|
|
+ if ( splitInside && !context.bWasUndone ) {
|
|
|
+ const path = b.graveyardPosition.path.slice();
|
|
|
+ path.push( 0 );
|
|
|
+
|
|
|
+ a.position = new Position( b.graveyardPosition.root, path );
|
|
|
+ } else {
|
|
|
+ a.position = a.position._getTransformedByUnwrapOperation( b );
|
|
|
+ }
|
|
|
|
|
|
if ( a.graveyardPosition ) {
|
|
|
- a.graveyardPosition = a.graveyardPosition._getTransformedByInsertion( b.graveyardPosition, 1 );
|
|
|
+ a.graveyardPosition = a.graveyardPosition._getTransformedByUnwrapOperation( b );
|
|
|
}
|
|
|
|
|
|
return [ a ];
|
|
|
@@ -1345,7 +1599,13 @@ setTransformation( SplitOperation, UnwrapOperation, ( a, b ) => {
|
|
|
|
|
|
// -----------------------
|
|
|
|
|
|
-setTransformation( WrapOperation, InsertOperation, ( a, b ) => {
|
|
|
+setTransformation( WrapOperation, InsertOperation, ( a, b, context ) => {
|
|
|
+ if ( a.position.isEqual( b.position ) && context.baRelation == 'insertInside' ) {
|
|
|
+ a.howMany += b.howMany;
|
|
|
+
|
|
|
+ return [ a ];
|
|
|
+ }
|
|
|
+
|
|
|
const transformed = a.wrappedRange._getTransformedByInsertOperation( b, false )[ 0 ];
|
|
|
|
|
|
a.position = transformed.start;
|
|
|
@@ -1355,31 +1615,41 @@ setTransformation( WrapOperation, InsertOperation, ( a, b ) => {
|
|
|
} );
|
|
|
|
|
|
setTransformation( WrapOperation, MergeOperation, ( a, b ) => {
|
|
|
+ if ( a.graveyardPosition ) {
|
|
|
+ a.graveyardPosition = a.graveyardPosition._getTransformedByInsertion( b.graveyardPosition, 1 );
|
|
|
+ }
|
|
|
+
|
|
|
+ // Case 1: The element to wrap got merged.
|
|
|
+ //
|
|
|
+ if ( a.position.isEqual( b.deletionPosition ) ) {
|
|
|
+ a.position = Position.createFromPosition( b.graveyardPosition );
|
|
|
+ a.position.stickiness = 'toNext';
|
|
|
+
|
|
|
+ return [ a ];
|
|
|
+ }
|
|
|
+
|
|
|
const transformed = a.wrappedRange._getTransformedByMergeOperation( b );
|
|
|
|
|
|
a.position = transformed.start;
|
|
|
a.howMany = transformed.end.offset - transformed.start.offset;
|
|
|
|
|
|
- if ( a.graveyardPosition ) {
|
|
|
- a.graveyardPosition = a.graveyardPosition._getTransformedByInsertion( b.graveyardPosition, 1 );
|
|
|
- }
|
|
|
-
|
|
|
return [ a ];
|
|
|
} );
|
|
|
|
|
|
-setTransformation( WrapOperation, MoveOperation, ( a, b ) => {
|
|
|
+setTransformation( WrapOperation, MoveOperation, ( a, b, context ) => {
|
|
|
if ( a.graveyardPosition ) {
|
|
|
a.graveyardPosition = a.graveyardPosition._getTransformedByMoveOperation( b );
|
|
|
}
|
|
|
|
|
|
- const ranges = breakRangeByMoveOperation( a.wrappedRange, b, false );
|
|
|
-
|
|
|
- if ( ranges.length == 0 ) {
|
|
|
- const range = a.wrappedRange._getTransformedByMoveOperation( b )[ 0 ];
|
|
|
+ if ( a.position.isEqual( b.targetPosition ) && context.baRelation == 'insertInside' ) {
|
|
|
+ a.position._getTransformedByDeletion( b.sourcePosition, b.howMany );
|
|
|
+ a.howMany += b.howMany;
|
|
|
|
|
|
- ranges.push( range );
|
|
|
+ return [ a ];
|
|
|
}
|
|
|
|
|
|
+ const ranges = breakRangeByMoveOperation( a.wrappedRange, b, false );
|
|
|
+
|
|
|
return ranges.reverse().map( range => {
|
|
|
const howMany = range.end.offset - range.start.offset;
|
|
|
const elementOrGraveyardPosition = a.graveyardPosition ? a.graveyardPosition : a.element;
|
|
|
@@ -1388,20 +1658,25 @@ setTransformation( WrapOperation, MoveOperation, ( a, b ) => {
|
|
|
} );
|
|
|
} );
|
|
|
|
|
|
-setTransformation( WrapOperation, SplitOperation, ( a, b ) => {
|
|
|
- // Case 1: If range to wrap got split by split operation cancel the wrapping.
|
|
|
+setTransformation( WrapOperation, SplitOperation, ( a, b, context ) => {
|
|
|
+ // Case 1: If range to wrap got split cancel the wrapping.
|
|
|
+ // Do that only if this is not undo mode. If `b` operation was earlier transformed by unwrap operation
|
|
|
+ // and the split position was inside the unwrapped range, then proceed without special case.
|
|
|
//
|
|
|
- if ( a.position.hasSameParentAs( b.position ) && a.wrappedRange.containsPosition( b.position ) ) {
|
|
|
+ const isInside = a.position.hasSameParentAs( b.position ) && a.wrappedRange.containsPosition( b.position );
|
|
|
+
|
|
|
+ if ( isInside && context.baRelation !== 'splitInside' ) {
|
|
|
// We cannot just return no-op in this case, because in the mirror case scenario the wrap is reversed, which
|
|
|
// might introduce a new node in the graveyard (if the wrap didn't have `graveyardPosition`, then the wrap
|
|
|
// created a new element which was put to the graveyard when the wrap was reversed).
|
|
|
//
|
|
|
// Instead, a node in graveyard will be inserted.
|
|
|
+ //
|
|
|
if ( a.element ) {
|
|
|
const graveyard = a.position.root.document.graveyard;
|
|
|
const graveyardPosition = new Position( graveyard, [ 0 ] );
|
|
|
|
|
|
- return new InsertOperation( graveyardPosition, a.element, 0 );
|
|
|
+ return [ new InsertOperation( graveyardPosition, a.element, 0 ) ];
|
|
|
} else {
|
|
|
return getNoOp();
|
|
|
}
|
|
|
@@ -1609,11 +1884,21 @@ setTransformation( UnwrapOperation, MergeOperation, ( a, b, context ) => {
|
|
|
return [ b.getReversed(), a ];
|
|
|
}
|
|
|
|
|
|
- const transformed = a.unwrappedRange._getTransformedByMergeOperation( b );
|
|
|
+ // // Case 2: The element to unwrap was merged-to and has new nodes.
|
|
|
+ // //
|
|
|
+ // if ( a.position.hasSameParentAs( b.targetPosition ) ) {
|
|
|
+ // // Merge operation needs `howMany`!
|
|
|
+ // }
|
|
|
|
|
|
- a.position = transformed.start;
|
|
|
- a.position.stickiness = 'toPrevious';
|
|
|
- a.howMany = transformed.end.offset - transformed.start.offset;
|
|
|
+ if ( a.position.hasSameParentAs( b.graveyardPosition ) ) {
|
|
|
+ a.howMany++;
|
|
|
+ }
|
|
|
+
|
|
|
+ if ( a.position.hasSameParentAs( b.deletionPosition ) ) {
|
|
|
+ a.howMany--;
|
|
|
+ }
|
|
|
+
|
|
|
+ a.position = a.position._getTransformedByMergeOperation( b );
|
|
|
|
|
|
if ( !a.graveyardPosition.isEqual( b.graveyardPosition ) || !context.aIsStrong ) {
|
|
|
a.graveyardPosition = a.graveyardPosition._getTransformedByMergeOperation( b );
|
|
|
@@ -1622,7 +1907,7 @@ setTransformation( UnwrapOperation, MergeOperation, ( a, b, context ) => {
|
|
|
return [ a ];
|
|
|
} );
|
|
|
|
|
|
-setTransformation( UnwrapOperation, MoveOperation, ( a, b, context ) => {
|
|
|
+setTransformation( UnwrapOperation, MoveOperation, ( a, b ) => {
|
|
|
// Case 1: Move operation moves nodes from the unwrapped element.
|
|
|
// This does not have any impact on `UnwrapOperation#position`, but `#howMany` has to be changed.
|
|
|
//
|
|
|
@@ -1640,7 +1925,7 @@ setTransformation( UnwrapOperation, MoveOperation, ( a, b, context ) => {
|
|
|
|
|
|
a.position = a.position._getTransformedByMoveOperation( b );
|
|
|
|
|
|
- if ( !a.graveyardPosition.isEqual( b.targetPosition ) || !context.aIsStrong ) {
|
|
|
+ if ( !a.graveyardPosition.isEqual( b.targetPosition ) ) {
|
|
|
a.graveyardPosition = a.graveyardPosition._getTransformedByMoveOperation( b );
|
|
|
}
|
|
|
|
|
|
@@ -1667,17 +1952,17 @@ setTransformation( UnwrapOperation, SplitOperation, ( a, b ) => {
|
|
|
// Case 2: The split element is the last element in unwrapped element. In this case, we need to manually modify
|
|
|
// `howMany` property because it wouldn't be correctly calculated by `_getTransformedBySplitOperation`.
|
|
|
//
|
|
|
- if ( b.insertionPosition.isEqual( a.position.getShiftedBy( a.howMany ) ) ) {
|
|
|
+ if ( a.position.hasSameParentAs( b.insertionPosition ) ) {
|
|
|
a.howMany++;
|
|
|
|
|
|
return [ a ];
|
|
|
}
|
|
|
|
|
|
- const transformed = a.unwrappedRange._getTransformedBySplitOperation( b );
|
|
|
+ a.position = a.position._getTransformedBySplitOperation( b );
|
|
|
|
|
|
- a.position = transformed.start;
|
|
|
- a.position.stickiness = 'toPrevious';
|
|
|
- a.howMany = transformed.end.offset - transformed.start.offset;
|
|
|
+ if ( b.graveyardPosition && b.graveyardPosition.hasSameParentAs( a.position ) ) {
|
|
|
+ a.howMany--;
|
|
|
+ }
|
|
|
|
|
|
a.graveyardPosition = a.graveyardPosition._getTransformedBySplitOperation( b );
|
|
|
|
|
|
@@ -1691,6 +1976,10 @@ setTransformation( UnwrapOperation, WrapOperation, ( a, b ) => {
|
|
|
a.howMany = a.howMany - b.howMany + 1;
|
|
|
}
|
|
|
|
|
|
+ if ( b.graveyardPosition && compareArrays( a.position.getParentPath(), b.graveyardPosition.path ) == 'same' ) {
|
|
|
+ a.howMany = b.howMany;
|
|
|
+ }
|
|
|
+
|
|
|
// The default case.
|
|
|
//
|
|
|
a.position = a.position._getTransformedByWrapOperation( b );
|
|
|
@@ -1709,6 +1998,8 @@ setTransformation( UnwrapOperation, UnwrapOperation, ( a, b, context ) => {
|
|
|
a.position = new Position( b.graveyardPosition.root, path );
|
|
|
a.howMany = 0;
|
|
|
a.graveyardPosition = Position.createFromPosition( b.graveyardPosition );
|
|
|
+
|
|
|
+ return [ a ];
|
|
|
}
|
|
|
|
|
|
a.position = a.position._getTransformedByUnwrapOperation( b );
|
|
|
@@ -1759,33 +2050,19 @@ function makeMoveOperationsFromRanges( ranges, targetPosition ) {
|
|
|
ranges[ j ] = ranges[ j ]._getTransformedByMove( op.sourcePosition, op.targetPosition, op.howMany )[ 0 ];
|
|
|
}
|
|
|
|
|
|
- // targetPosition.stickiness = 'toPrevious';
|
|
|
- targetPosition = targetPosition._getTransformedByMove( op.sourcePosition, op.targetPosition, op.howMany, true );
|
|
|
+ targetPosition = targetPosition._getTransformedByMove( op.sourcePosition, op.targetPosition, op.howMany );
|
|
|
}
|
|
|
|
|
|
return operations;
|
|
|
}
|
|
|
|
|
|
function makeMoveOperation( range, targetPosition ) {
|
|
|
- // We want to keep correct operation class.
|
|
|
- let OperationClass;
|
|
|
-
|
|
|
- if ( targetPosition.root.rootName == '$graveyard' ) {
|
|
|
- OperationClass = RemoveOperation;
|
|
|
- } else if ( range.start.root.rootName == '$graveyard' ) {
|
|
|
- OperationClass = ReinsertOperation;
|
|
|
- } else {
|
|
|
- OperationClass = MoveOperation;
|
|
|
- }
|
|
|
-
|
|
|
targetPosition.stickiness = 'toNone';
|
|
|
|
|
|
- const result = new OperationClass(
|
|
|
+ return new MoveOperation(
|
|
|
range.start,
|
|
|
range.end.offset - range.start.offset,
|
|
|
targetPosition,
|
|
|
- 0 // Is corrected anyway later.
|
|
|
+ 0
|
|
|
);
|
|
|
-
|
|
|
- return result;
|
|
|
}
|