8
0
Просмотр исходного кода

Changed: Introduced `MergeOperation#howMany` and `SplitOperation#howMany`.

Szymon Cofalik 7 лет назад
Родитель
Сommit
1e01bf36d9

+ 14 - 4
packages/ckeditor5-engine/src/model/operation/mergeoperation.js

@@ -30,12 +30,13 @@ export default class MergeOperation extends Operation {
 	 *
 	 * @param {module:engine/model/position~Position} sourcePosition Position inside the merged element. All nodes from that
 	 * element after that position will be moved to {@link ~#targetPosition}.
+	 * @param {Number} howMany
 	 * @param {module:engine/model/position~Position} targetPosition Position which the nodes from the merged elements will be moved to.
 	 * @param {module:engine/model/position~Position} graveyardPosition Position in graveyard to which the merged element will be moved.
 	 * @param {Number|null} baseVersion Document {@link module:engine/model/document~Document#version} on which operation
 	 * can be applied or `null` if the operation operates on detached (non-document) tree.
 	 */
-	constructor( sourcePosition, targetPosition, graveyardPosition, baseVersion ) {
+	constructor( sourcePosition, howMany, targetPosition, graveyardPosition, baseVersion ) {
 		super( baseVersion );
 
 		/**
@@ -56,6 +57,8 @@ export default class MergeOperation extends Operation {
 		// is it? think about reversed split operations, undo, etc.
 
 		this.graveyardPosition = Position.createFromPosition( graveyardPosition );
+
+		this.howMany = howMany;
 	}
 
 	/**
@@ -94,7 +97,7 @@ export default class MergeOperation extends Operation {
 	 * @returns {module:engine/model/operation/mergeoperation~MergeOperation} Clone of this operation.
 	 */
 	clone() {
-		return new this.constructor( this.sourcePosition, this.targetPosition, this.graveyardPosition, this.baseVersion );
+		return new this.constructor( this.sourcePosition, this.howMany, this.targetPosition, this.graveyardPosition, this.baseVersion );
 	}
 
 	/**
@@ -103,7 +106,7 @@ export default class MergeOperation extends Operation {
 	 * @returns {module:engine/model/operation/splitoperation~SplitOperation}
 	 */
 	getReversed() {
-		return new SplitOperation( this.targetPosition, this.graveyardPosition, this.baseVersion + 1 );
+		return new SplitOperation( this.targetPosition, this.howMany, this.graveyardPosition, this.baseVersion + 1 );
 	}
 
 	/**
@@ -128,6 +131,13 @@ export default class MergeOperation extends Operation {
 			 * @error merge-operation-target-position-invalid
 			 */
 			throw new CKEditorError( 'merge-operation-target-position-invalid: Merge target position is invalid.' );
+		} else if ( this.howMany != sourceElement.maxOffset ) {
+			/**
+			 * Merge operation specifies wrong number of nodes to move.
+			 *
+			 * @error merge-operation-how-many-invalid
+			 */
+			throw new CKEditorError( 'merge-operation-how-many-invalid: Merge operation specifies wrong number of nodes to move.' );
 		}
 	}
 
@@ -161,6 +171,6 @@ export default class MergeOperation extends Operation {
 		const targetPosition = Position.fromJSON( json.targetPosition, document );
 		const graveyardPosition = Position.fromJSON( json.graveyardPosition, document );
 
-		return new this( sourcePosition, targetPosition, graveyardPosition, json.baseVersion );
+		return new this( sourcePosition, json.howMany, targetPosition, graveyardPosition, json.baseVersion );
 	}
 }

+ 14 - 4
packages/ckeditor5-engine/src/model/operation/splitoperation.js

@@ -26,12 +26,13 @@ export default class SplitOperation extends Operation {
 	 * Creates a split operation.
 	 *
 	 * @param {module:engine/model/position~Position} position Position at which an element should be split.
+	 * @param {Number}
 	 * @param {module:engine/model/position~Position|null} graveyardPosition Position in graveyard before the element which
 	 * should be used as a parent of the nodes after `position`. If it is not set, a copy of the the `position` parent will be used.
 	 * @param {Number|null} baseVersion Document {@link module:engine/model/document~Document#version} on which operation
 	 * can be applied or `null` if the operation operates on detached (non-document) tree.
 	 */
-	constructor( position, graveyardPosition, baseVersion ) {
+	constructor( position, howMany, graveyardPosition, baseVersion ) {
 		super( baseVersion );
 
 		/**
@@ -47,6 +48,8 @@ export default class SplitOperation extends Operation {
 		if ( this.graveyardPosition ) {
 			this.graveyardPosition.stickiness = 'toNext';
 		}
+
+		this.howMany = howMany;
 	}
 
 	/**
@@ -104,7 +107,7 @@ export default class SplitOperation extends Operation {
 	 * @returns {module:engine/model/operation/splitoperation~SplitOperation} Clone of this operation.
 	 */
 	clone() {
-		return new this.constructor( this.position, this.graveyardPosition, this.baseVersion );
+		return new this.constructor( this.position, this.howMany, this.graveyardPosition, this.baseVersion );
 	}
 
 	/**
@@ -116,7 +119,7 @@ export default class SplitOperation extends Operation {
 		const graveyard = this.position.root.document.graveyard;
 		const graveyardPosition = new Position( graveyard, [ 0 ] );
 
-		return new MergeOperation( this.moveTargetPosition, this.position, graveyardPosition, this.baseVersion + 1 );
+		return new MergeOperation( this.moveTargetPosition, this.howMany, this.position, graveyardPosition, this.baseVersion + 1 );
 	}
 
 	/**
@@ -134,6 +137,13 @@ export default class SplitOperation extends Operation {
 			 * @error split-operation-position-invalid
 			 */
 			throw new CKEditorError( 'split-operation-position-invalid: Split position is invalid.' );
+		} else if ( this.howMany != element.maxOffset - this.position.offset ) {
+			/**
+			 * Split operation specifies wrong number of nodes to move.
+			 *
+			 * @error split-operation-how-many-invalid
+			 */
+			throw new CKEditorError( 'split-operation-how-many-invalid: Split operation specifies wrong number of nodes to move.' );
 		}
 	}
 
@@ -174,6 +184,6 @@ export default class SplitOperation extends Operation {
 		const position = Position.fromJSON( json.position, document );
 		const graveyardPosition = json.graveyardPosition ? Position.fromJSON( json.graveyardPosition, document ) : null;
 
-		return new this( position, graveyardPosition, json.baseVersion );
+		return new this( position, json.howMany, graveyardPosition, json.baseVersion );
 	}
 }

+ 78 - 9
packages/ckeditor5-engine/src/model/operation/transform.js

@@ -839,6 +839,10 @@ setTransformation( MarkerOperation, UnwrapOperation, ( a, b ) => {
 // -----------------------
 
 setTransformation( MergeOperation, InsertOperation, ( a, b ) => {
+	if ( a.sourcePosition.hasSameParentAs( b.position ) ) {
+		a.howMany += b.howMany;
+	}
+
 	a.sourcePosition = a.sourcePosition._getTransformedByInsertOperation( b );
 	a.targetPosition = a.targetPosition._getTransformedByInsertOperation( b );
 
@@ -855,6 +859,7 @@ setTransformation( MergeOperation, MergeOperation, ( a, b, context ) => {
 		path.push( 0 );
 
 		a.sourcePosition = new Position( b.graveyardPosition.root, path );
+		a.howMany = 0;
 
 		return [ a ];
 	}
@@ -864,6 +869,10 @@ setTransformation( MergeOperation, MergeOperation, ( a, b, context ) => {
 
 	// The default case.
 	//
+	if ( a.sourcePosition.hasSameParentAs( b.targetPosition ) ) {
+		a.howMany += b.howMany;
+	}
+
 	a.sourcePosition = a.sourcePosition._getTransformedByMergeOperation( b );
 	a.targetPosition = a.targetPosition._getTransformedByMergeOperation( b );
 
@@ -892,6 +901,14 @@ setTransformation( MergeOperation, MoveOperation, ( a, b, context ) => {
 		}
 	}
 
+	if ( a.sourcePosition.hasSameParentAs( b.targetPosition ) ) {
+		a.howMany += b.howMany;
+	}
+
+	if ( a.sourcePosition.hasSameParentAs( b.sourcePosition ) ) {
+		a.howMany -= b.howMany;
+	}
+
 	a.sourcePosition = a.sourcePosition._getTransformedByMoveOperation( b );
 	a.targetPosition = a.targetPosition._getTransformedByMoveOperation( b );
 
@@ -922,16 +939,17 @@ setTransformation( MergeOperation, SplitOperation, ( a, b ) => {
 	//			For example, if the split would be after `F`, `targetPosition` should also be transformed.
 	//
 	//			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 ) && a.sourcePosition.root.rootName == '$graveyard' ) {
+	if ( a.targetPosition.isEqual( b.position ) && b.howMany != 0 ) {
 		a.sourcePosition = a.sourcePosition._getTransformedBySplitOperation( b );
 
 		return [ a ];
 	}
 
+	if ( a.sourcePosition.hasSameParentAs( b.position ) ) {
+		a.howMany = b.position.offset;
+	}
+
 	a.sourcePosition = a.sourcePosition._getTransformedBySplitOperation( b );
 	a.targetPosition = a.targetPosition._getTransformedBySplitOperation( b );
 
@@ -943,6 +961,10 @@ setTransformation( MergeOperation, WrapOperation, ( a, b ) => {
 		a.graveyardPosition = a.graveyardPosition._getTransformedByDeletion( b.graveyardPosition, 1 );
 	}
 
+	if ( a.sourcePosition.hasSameParentAs( b.position ) ) {
+		a.howMany = a.howMany + 1 - b.howMany;
+	}
+
 	// Case 1:	Wrap is with an element from graveyard, which also is merge operation target. This happens
 	//			in some undo scenarios. In this case, the target position needs to be properly transformed.
 	//
@@ -995,10 +1017,15 @@ setTransformation( MergeOperation, UnwrapOperation, ( a, b, context ) => {
 		path.push( 0 );
 
 		a.sourcePosition = new Position( b.graveyardPosition.root, path );
+		a.howMany = 0;
 
 		return [ a ];
 	}
 
+	if ( a.sourcePosition.hasSameParentAs( b.targetPosition ) ) {
+		a.howMany = a.howMany - 1 + b.howMany;
+	}
+
 	a.sourcePosition = a.sourcePosition._getTransformedByUnwrapOperation( b );
 	a.targetPosition = a.targetPosition._getTransformedByUnwrapOperation( b );
 
@@ -1466,15 +1493,25 @@ setTransformation( RootAttributeOperation, RootAttributeOperation, ( a, b, conte
 
 setTransformation( SplitOperation, InsertOperation, ( a, b, context ) => {
 	if ( a.position.isEqual( b.position ) && context.baRelation == 'insertAtSource' ) {
+		a.howMany += b.howMany;
+
 		return [ a ];
 	}
 
+	if ( a.position.hasSameParentAs( b.position ) && a.position.offset < b.position.offset ) {
+		a.howMany += b.howMany;
+	}
+
 	a.position = a.position._getTransformedByInsertOperation( b );
 
 	return [ a ];
 } );
 
 setTransformation( SplitOperation, MergeOperation, ( a, b ) => {
+	if ( a.position.hasSameParentAs( b.targetPosition ) ) {
+		a.howMany += b.howMany;
+	}
+
 	a.position = a.position._getTransformedByMergeOperation( b );
 
 	if ( a.graveyardPosition ) {
@@ -1507,12 +1544,20 @@ setTransformation( SplitOperation, MoveOperation, ( a, b, context ) => {
 	const rangeToMove = Range.createFromPositionAndShift( b.sourcePosition, b.howMany );
 
 	if ( a.position.hasSameParentAs( b.sourcePosition ) && rangeToMove.containsPosition( a.position ) ) {
+		const howManyRemoved = b.howMany - ( a.position.offset - b.sourcePosition.offset );
+		a.howMany -= howManyRemoved;
+
+		if ( a.position.hasSameParentAs( b.targetPosition ) && a.position.offset < b.targetPosition.offset ) {
+			a.howMany += b.howMany;
+		}
+
 		a.position = Position.createFromPosition( b.sourcePosition );
 
 		return [ a ];
 	}
 
 	if ( a.position.isEqual( b.targetPosition ) && context.abRelation == 'splitBefore' ) {
+		a.howMany += b.howMany;
 		a.position = a.position._getTransformedByDeletion( b.sourcePosition, b.howMany );
 
 		return [ a ];
@@ -1520,6 +1565,14 @@ setTransformation( SplitOperation, MoveOperation, ( a, b, context ) => {
 
 	// The default case.
 	//
+	if ( a.position.hasSameParentAs( b.sourcePosition ) && a.position.offset < b.sourcePosition.offset ) {
+		a.howMany -= b.howMany;
+	}
+
+	if ( a.position.hasSameParentAs( b.targetPosition ) && a.position.offset < b.sourcePosition.offset ) {
+		a.howMany += b.howMany;
+	}
+
 	a.position = a.position._getTransformedByMoveOperation( b );
 
 	return [ a ];
@@ -1534,9 +1587,15 @@ setTransformation( SplitOperation, SplitOperation, ( a, b, context ) => {
 		if ( a.graveyardPosition && b.graveyardPosition && a.graveyardPosition.isEqual( b.graveyardPosition ) ) {
 			return getNoOp();
 		}
+
+		a.howMany = 0;
 	} else if ( a.position.isEqual( b.insertionPosition ) && context.abRelation == 'splitBefore' ) {
 		return [ a ];
 	} else {
+		if ( a.position.hasSameParentAs( b.position ) && a.position.offset < b.position.offset ) {
+			a.howMany -= b.howMany;
+		}
+
 		a.position = a.position._getTransformedBySplitOperation( b );
 	}
 
@@ -1567,7 +1626,12 @@ setTransformation( SplitOperation, WrapOperation, ( a, b, context ) => {
 
 	if ( a.position.isEqual( b.position ) && context.abRelation == 'splitInside' ) {
 		a.position = b.targetPosition;
+		a.howMany = b.howMany;
 	} else {
+		if ( a.position.hasSameParentAs( b.position ) && a.position.offset < b.position.offset ) {
+			a.howMany = a.howMany + 1 - b.howMany;
+		}
+
 		a.position = a.position._getTransformedByWrapOperation( b );
 	}
 
@@ -1586,7 +1650,12 @@ setTransformation( SplitOperation, UnwrapOperation, ( a, b, context ) => {
 		path.push( 0 );
 
 		a.position = new Position( b.graveyardPosition.root, path );
+		a.howMany = 0;
 	} else {
+		if ( a.position.hasSameParentAs( b.targetPosition ) && a.position.offset < b.targetPosition.offset ) {
+			a.howMany = a.howMany - 1 + b.howMany;
+		}
+
 		a.position = a.position._getTransformedByUnwrapOperation( b );
 	}
 
@@ -1884,11 +1953,11 @@ setTransformation( UnwrapOperation, MergeOperation, ( a, b, context ) => {
 		return [ b.getReversed(), a ];
 	}
 
-	// // Case 2:	The element to unwrap was merged-to and has new nodes.
-	// //
-	// if ( a.position.hasSameParentAs( b.targetPosition ) ) {
-	// 	// Merge operation needs `howMany`!
-	// }
+	// Case 2:	The element to unwrap was merged-to and has new nodes.
+	//
+	if ( a.position.hasSameParentAs( b.targetPosition ) ) {
+		a.howMany += b.howMany;
+	}
 
 	if ( a.position.hasSameParentAs( b.graveyardPosition ) ) {
 		a.howMany++;

+ 3 - 2
packages/ckeditor5-engine/src/model/writer.js

@@ -567,7 +567,7 @@ export default class Writer {
 
 		const version = position.root.document.version;
 
-		const merge = new MergeOperation( sourcePosition, targetPosition, graveyardPosition, version );
+		const merge = new MergeOperation( sourcePosition, position.nodeAfter.maxOffset, targetPosition, graveyardPosition, version );
 
 		this.batch.addOperation( merge );
 		this.model.applyOperation( merge );
@@ -644,7 +644,8 @@ export default class Writer {
 
 		do {
 			const version = splitElement.root.document ? splitElement.root.document.version : null;
-			const split = new SplitOperation( position, null, version );
+			const howMany = splitElement.maxOffset - position.offset;
+			const split = new SplitOperation( position, howMany, null, version );
 
 			this.batch.addOperation( split );
 			this.model.applyOperation( split );

+ 1 - 1
packages/ckeditor5-engine/tests/model/documentselection.js

@@ -1028,7 +1028,7 @@ describe( 'DocumentSelection', () => {
 				} );
 
 				const batch = new Batch();
-				const splitOperation = new SplitOperation( new Position( root, [ 1, 2 ] ), null, 0 );
+				const splitOperation = new SplitOperation( new Position( root, [ 1, 2 ] ), 4, null, 0 );
 
 				batch.addOperation( splitOperation );
 				model.applyOperation( splitOperation );

+ 1 - 0
packages/ckeditor5-engine/tests/model/liverange.js

@@ -178,6 +178,7 @@ describe( 'LiveRange', () => {
 
 			const merge = new MergeOperation(
 				new Position( root, [ 0, 0 ] ),
+				10,
 				new Position( gy, [ 0, 0 ] ),
 				new Position( gy, [ 0 ] ),
 				model.document.version + 1

+ 8 - 5
packages/ckeditor5-engine/tests/model/range.js

@@ -944,7 +944,7 @@ describe( 'Range', () => {
 			it( 'split inside range', () => {
 				range = new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 0, 4 ] ) );
 
-				const op = new SplitOperation( new Position( root, [ 0, 3 ] ), gyPos, 1 );
+				const op = new SplitOperation( new Position( root, [ 0, 3 ] ), 6, gyPos, 1 );
 				const transformed = range.getTransformedByOperation( op );
 
 				expect( transformed.length ).to.equal( 1 );
@@ -955,7 +955,7 @@ describe( 'Range', () => {
 			it( 'split at the beginning of multi-element range', () => {
 				range = new Range( new Position( root, [ 0, 4 ] ), new Position( root, [ 1, 2 ] ) );
 
-				const op = new SplitOperation( new Position( root, [ 0, 4 ] ), gyPos, 1 );
+				const op = new SplitOperation( new Position( root, [ 0, 4 ] ), 5, gyPos, 1 );
 				const transformed = range.getTransformedByOperation( op );
 
 				expect( transformed.length ).to.equal( 1 );
@@ -966,7 +966,7 @@ describe( 'Range', () => {
 			it( 'split inside range which starts at the beginning of split element', () => {
 				range = new Range( new Position( root, [ 0, 0 ] ), new Position( root, [ 0, 4 ] ) );
 
-				const op = new SplitOperation( new Position( root, [ 0, 3 ] ), gyPos, 1 );
+				const op = new SplitOperation( new Position( root, [ 0, 3 ] ), 6, gyPos, 1 );
 				const transformed = range.getTransformedByOperation( op );
 
 				expect( transformed.length ).to.equal( 1 );
@@ -977,7 +977,7 @@ describe( 'Range', () => {
 			it( 'split inside range which end is at the end of split element', () => {
 				range = new Range( new Position( root, [ 0, 3 ] ), new Position( root, [ 0, 6 ] ) );
 
-				const op = new SplitOperation( new Position( root, [ 0, 4 ] ), gyPos, 1 );
+				const op = new SplitOperation( new Position( root, [ 0, 4 ] ), 5, gyPos, 1 );
 				const transformed = range.getTransformedByOperation( op );
 
 				expect( transformed.length ).to.equal( 1 );
@@ -988,7 +988,7 @@ describe( 'Range', () => {
 			it( 'split element which has collapsed range at the end', () => {
 				range = new Range( new Position( root, [ 0, 6 ] ), new Position( root, [ 0, 6 ] ) );
 
-				const op = new SplitOperation( new Position( root, [ 0, 3 ] ), gyPos, 1 );
+				const op = new SplitOperation( new Position( root, [ 0, 3 ] ), 6, gyPos, 1 );
 				const transformed = range.getTransformedByOperation( op );
 
 				expect( transformed.length ).to.equal( 1 );
@@ -1004,6 +1004,7 @@ describe( 'Range', () => {
 
 				const op = new MergeOperation(
 					new Position( root, [ 1, 0 ] ),
+					4,
 					new Position( root, [ 0, 3 ] ),
 					gyPos,
 					1
@@ -1022,6 +1023,7 @@ describe( 'Range', () => {
 
 				const op = new MergeOperation(
 					new Position( root, [ 0, 0 ] ),
+					4,
 					new Position( root, [ 1, 3 ] ),
 					gyPos,
 					1
@@ -1045,6 +1047,7 @@ describe( 'Range', () => {
 
 				const op = new MergeOperation(
 					new Position( root, [ 1, 0 ] ),
+					4,
 					new Position( root, [ 0, 1 ] ),
 					gyPos,
 					1