Przeglądaj źródła

Added new special cases for SplitDelta x WrapDelta (and vice versa) transformations.

Szymon Cofalik 9 lat temu
rodzic
commit
671d1883dd

+ 50 - 0
packages/ckeditor5-engine/src/treemodel/delta/basic-transformations.js

@@ -8,6 +8,7 @@
 import { addTransformationCase, defaultTransform } from './transform.js';
 
 import Range from '../range.js';
+import Position from '../position.js';
 
 import AttributeOperation from '../operation/attributeoperation.js';
 
@@ -165,6 +166,42 @@ addTransformationCase( SplitDelta, WrapDelta, ( a, b, isStrong ) => {
 		// This is "no-op" delta, it has no type and no operations, it basically does nothing.
 		// It is used when we don't want to apply changes but still we need to return a delta.
 		return [ new Delta() ];
+	} else if ( utils.compareArrays( a.position.getParentPath(), b.range.end.getShiftedBy( -1 ).path ) === 'SAME' ) {
+		// Split position is directly inside the last node from wrap range.
+		// If that's the case, we manually change split delta so it will "target" inside the wrapping element.
+		// By doing so we will be inserting split node right to the original node which feels natural and is a good UX.
+		const delta = a.clone();
+
+		// 1. Fix insert operation position.
+		// Node to split is the last children of the wrapping element.
+		// Wrapping element is the element inserted by WrapDelta (re)insert operation.
+		// It is inserted after the wrapped range, but the wrapped range will be moved inside it.
+		// Having this in mind, it is correct to use wrapped range start position as the position before wrapping element.
+		const splitNodePos = Position.createFromPosition( b.range.start );
+		// Now, `splitNodePos` points before wrapping element.
+		// To get a position before last children of that element, we expand position's `path` member by proper offset.
+		splitNodePos.path.push( b.howMany - 1 );
+
+		// SplitDelta insert operation position should be right after the node we split.
+		const insertPos = splitNodePos.getShiftedBy( 1 );
+		delta._cloneOperation.position = insertPos;
+
+		// 2. Fix move operation source position.
+		// Nodes moved by SplitDelta will be moved from new position, modified by WrapDelta.
+		// To obtain that new position, `splitNodePos` will be used, as this is the node we are extracting children from.
+		const sourcePos = Position.createFromPosition( splitNodePos );
+		// Nothing changed inside split node so it is correct to use the original split position offset.
+		sourcePos.path.push( a.position.offset );
+		delta._moveOperation.sourcePosition = sourcePos;
+
+		// 3. Fix move operation target position.
+		// SplitDelta move operation target position should be inside the node inserted by operation above.
+		// Since the node is empty, we will insert at offset 0.
+		const targetPos = Position.createFromPosition( insertPos );
+		targetPos.path.push( 0 );
+		delta._moveOperation.targetPosition = targetPos;
+
+		return [ delta ];
 	}
 
 	return defaultTransform( a, b, isStrong );
@@ -210,6 +247,19 @@ addTransformationCase( WrapDelta, SplitDelta, ( a, b, isStrong ) => {
 			b.getReversed(),
 			a.clone()
 		];
+	} else if ( utils.compareArrays( b.position.getParentPath(), a.range.end.getShiftedBy( -1 ).path ) === 'SAME' ) {
+		const delta = a.clone();
+
+		// Move wrapping element insert position one node further so it is after the split node insertion.
+		delta._insertOperation.position.offset++;
+
+		// Include the split node copy.
+		delta._moveOperation.howMany++;
+
+		// Change the path to wrapping element in move operation.
+		delta._moveOperation.targetPosition.path[ delta._moveOperation.targetPosition.path.length - 2 ]++;
+
+		return [ delta ];
 	}
 
 	return defaultTransform( a, b, isStrong );

+ 40 - 1
packages/ckeditor5-engine/tests/treemodel/delta/transform/splitdelta.js

@@ -273,12 +273,51 @@ describe( 'transform', () => {
 				expect( nodesAndText ).to.equal( 'PaEbcfoEobarxyzP' );
 			} );
 
+			it( 'split position is before wrapped nodes', () => {
+				let wrapRange = new Range( new Position( root, [ 3, 3, 3, 5 ] ), new Position( root, [ 3, 3, 3, 7 ] ) );
+				let wrapElement = new Element( 'E' );
+				let wrapDelta = getWrapDelta( wrapRange, wrapElement, baseVersion );
+
+				let transformed = transform( splitDelta, wrapDelta );
+
+				expect( transformed.length ).to.equal( 1 );
+
+				baseVersion = wrapDelta.operations.length;
+
+				expectDelta( transformed[ 0 ], {
+					type: SplitDelta,
+					operations: [
+						{
+							type: InsertOperation,
+							position: new Position( root, [ 3, 3, 4 ] ),
+							baseVersion: baseVersion
+						},
+						{
+							type: MoveOperation,
+							sourcePosition: new Position( root, [ 3, 3, 3, 3 ] ),
+							howMany: 8,
+							targetPosition: new Position( root, [ 3, 3, 4, 0 ] ),
+							baseVersion: baseVersion + 1
+						}
+					]
+				} );
+
+				// Test if deltas do what they should after applying transformed delta.
+				applyDelta( wrapDelta, doc );
+				applyDelta( transformed[ 0 ], doc );
+
+				let nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3, 3, 3 ] ), 2 ) );
+
+				// WrapDelta and SplitDelta are correctly applied.
+				expect( nodesAndText ).to.equal( 'PabcPPfoEobEarxyzP' );
+			} );
+
 			it( 'split position is inside wrapped node', () => {
 				let wrapRange = new Range( new Position( root, [ 3, 3, 2 ] ), new Position( root, [ 3, 3, 4 ] ) );
 				let wrapElement = new Element( 'E' );
 				let wrapDelta = getWrapDelta( wrapRange, wrapElement, baseVersion );
 
-				let transformed = transform( splitDelta, wrapDelta, true );
+				let transformed = transform( splitDelta, wrapDelta );
 
 				expect( transformed.length ).to.equal( 1 );
 

+ 45 - 1
packages/ckeditor5-engine/tests/treemodel/delta/transform/wrapdelta.js

@@ -117,7 +117,7 @@ describe( 'transform', () => {
 				let splitPosition = new Position( root, [ 3, 3, 3, 1 ] );
 				let splitDelta = getSplitDelta( splitPosition, new Element( 'p' ), 11, baseVersion );
 
-				let transformed = transform( wrapDelta, splitDelta, true );
+				let transformed = transform( wrapDelta, splitDelta );
 
 				expect( transformed.length ).to.equal( 1 );
 
@@ -150,6 +150,50 @@ describe( 'transform', () => {
 				// WrapDelta and SplitDelta are correctly applied.
 				expect( nodesAndText ).to.equal( 'PaPPEbcfoEobarxyzP' );
 			} );
+
+			it( 'split position is inside wrapped node', () => {
+				// For this case, we need different WrapDelta so it is overwritten.
+				let wrapRange = new Range( new Position( root, [ 3, 3, 2 ] ), new Position( root, [ 3, 3, 4 ] ) );
+				let wrapElement = new Element( 'E' );
+
+				wrapDelta = getWrapDelta( wrapRange, wrapElement, baseVersion );
+
+				let splitPosition = new Position( root, [ 3, 3, 3, 3 ] );
+				let splitDelta = getSplitDelta( splitPosition, new Element( 'p' ), 9, baseVersion );
+
+				let transformed = transform( wrapDelta, splitDelta );
+
+				expect( transformed.length ).to.equal( 1 );
+
+				baseVersion = wrapDelta.operations.length;
+
+				expectDelta( transformed[ 0 ], {
+					type: WrapDelta,
+					operations: [
+						{
+							type: InsertOperation,
+							position: new Position( root, [ 3, 3, 5 ] ),
+							baseVersion: baseVersion
+						},
+						{
+							type: MoveOperation,
+							sourcePosition: new Position( root, [ 3, 3, 2 ] ),
+							howMany: 3,
+							targetPosition: new Position( root, [ 3, 3, 5, 0 ] ),
+							baseVersion: baseVersion + 1
+						}
+					]
+				} );
+
+				// Test if deltas do what they should after applying transformed delta.
+				applyDelta( splitDelta, doc );
+				applyDelta( transformed[ 0 ], doc );
+
+				let nodesAndText = getNodesAndText( Range.createFromPositionAndShift( new Position( root, [ 3, 3, 2 ] ), 1 ) );
+
+				// WrapDelta and SplitDelta are correctly applied.
+				expect( nodesAndText ).to.equal( 'EXabcdXPabcPPfoobarxyzPE' );
+			} );
 		} );
 	} );
 } );