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

Position.getCombined(), .getTransformedByInsertion() and .getTransformedByDeletion() introduced.

Szymon Cofalik 10 лет назад
Родитель
Сommit
dedf77e561

+ 131 - 0
packages/ckeditor5-engine/src/document/position.js

@@ -234,6 +234,137 @@ CKEDITOR.define( [ 'document/rootelement', 'utils', 'ckeditorerror' ], ( RootEle
 					}
 			}
 		}
+
+		/**
+		 * Returns a new position that is a combination of this position and given positions. The combined
+		 * position is this position transformed by moving a range starting at `from` to `to` position.
+		 * It is expected that `original` position is inside the moved range.
+		 *
+		 * In other words, this method in a smart way "cuts out" `from` path from this position and
+		 * injects `to` path in it's place, while doing necessary fixes in order to get a correct path.
+		 *
+		 * Example:
+		 * 	let original = new Position( [ 2, 3, 1 ], root );
+		 * 	let from = new Position( [ 2, 2 ], root );
+		 * 	let to = new Position( [ 1, 1, 3 ], otherRoot );
+		 * 	let combined = original.getCombined( from, to );
+		 * 	// combined.path is [ 1, 1, 4, 1 ], combined.root is otherRoot
+		 *
+		 * Explanation:
+		 * We have a position `[ 2, 3, 1 ]` and move some nodes from `[ 2, 2 ]` to `[ 1, 1, 3 ]`. The original position
+		 * was inside moved nodes and now should point to the new place. The moved nodes will be after
+		 * positions `[ 1, 1, 3 ]`, `[ 1, 1, 4 ]`, `[ 1, 1, 5 ]`. Since our position was in the second moved node,
+		 * the transformed position will be in a sub-tree of a node at `[ 1, 1, 4 ]`. Looking at original path, we
+		 * took care of `[ 2, 3 ]` part of it. Now we have to add the rest of the original path to the transformed path.
+		 * Finally, the transformed position will point to `[ 1, 1, 4, 1 ]`.
+		 *
+		 * @param {document.Position} from Beginning of the moved range.
+		 * @param {document.Position} to Position where the range is moved.
+		 * @returns {document.Position} Combined position.
+		 */
+		getCombined( from, to ) {
+			const i = from.parentPath.length;
+
+			// The first part of a path to combined position is a path to the place where nodes were moved.
+			let combined = to.clone();
+
+			// Then we have to update the rest of the path.
+
+			// Fix the offset because this position might be after `from` position and we have to reflect that.
+			combined.offset = combined.offset + this.path[ i ] - from.offset;
+
+			// Then, add the rest of the path.
+			// If this position is at the same level as `from` position nothing will get added.
+			combined.path = combined.path.concat( this.path.slice( i + 1 ) );
+
+			return combined;
+		}
+
+		/**
+		 * Returns this position after being updated by inserting `howMany` nodes at `insertPosition`.
+		 *
+		 * @param {document.Position} insertPosition Position where nodes are inserted.
+		 * @param {Number} howMany How many nodes are inserted.
+		 * @param {Boolean} insertBefore Flag indicating whether nodes are inserted before or after `insertPosition`.
+		 * This is important only when `insertPosition` and this position are same. If that is the case and the flag is
+		 * set to true, this position will get transformed. If the flag is set to false, it won't.
+		 * @returns {document.Position} Transformed position.
+		 */
+		getTransformedByInsertion( insertPosition, howMany, insertBefore ) {
+			let transformed = this.clone();
+
+			// This position can't be affected if insertion was in a different root.
+			if ( this.root != insertPosition.root ) {
+				return transformed;
+			}
+
+			if ( utils.compareArrays( insertPosition.parentPath, this.parentPath ) == utils.compareArrays.SAME ) {
+				// If nodes are inserted in the node that is pointed by this position...
+				if ( insertPosition.offset < this.offset || ( insertPosition.offset == this.offset && insertBefore ) ) {
+					// And are inserted before an offset of that position...
+					// "Push" this positions offset.
+					transformed.offset += howMany;
+				}
+			} else if ( utils.compareArrays( insertPosition.parentPath, this.parentPath ) == utils.compareArrays.PREFIX ) {
+				// If nodes are inserted in a node that is on a path to this position...
+				const i = insertPosition.parentPath.length;
+
+				if ( insertPosition.offset <= this.path[ i ] ) {
+					// And are inserted before next node of that path...
+					// "Push" the index on that path.
+					transformed.path[ i ] += howMany;
+				}
+			}
+
+			return transformed;
+		}
+
+		/**
+		 * Returns this position after being updated by removing `howMany` nodes starting from `deletePosition`.
+		 * It may happen that this position is in a removed node. If that is the case, `null` is returned instead.
+		 *
+		 * @param {document.Position} deletePosition Position before the first removed node.
+		 * @param {Number} howMany How many nodes are removed.
+		 * @returns {document.Position|null} Transformed position or null.
+		 */
+		getTransformedByDeletion( deletePosition, howMany ) {
+			let transformed = this.clone();
+
+			// This position can't be affected if deletion was in a different root.
+			if ( this.root != deletePosition.root ) {
+				return transformed;
+			}
+
+			if ( utils.compareArrays( deletePosition.parentPath, this.parentPath ) == utils.compareArrays.SAME ) {
+				// If nodes are removed from the node that is pointed by this position...
+				if ( deletePosition.offset < this.offset ) {
+					// And are removed from before an offset of that position...
+					// Decrement the offset accordingly.
+					if ( deletePosition.offset + howMany > this.offset ) {
+						transformed.offset = deletePosition.offset;
+					} else {
+						transformed.offset -= howMany;
+					}
+				}
+			} else if ( utils.compareArrays( deletePosition.parentPath, this.parentPath ) == utils.compareArrays.PREFIX ) {
+				// If nodes are removed from a node that is on a path to this position...
+				const i = deletePosition.parentPath.length;
+
+				if ( deletePosition.offset < this.path[ i ] ) {
+					// And are removed from before next node of that path...
+					if ( deletePosition.offset + howMany > this.path[ i ] ) {
+						// If the next node of that path is removed return null
+						// because the node containing this position got removed.
+						return null;
+					} else {
+						// Otherwise, decrement index on that path.
+						transformed.path[ i ] -= howMany;
+					}
+				}
+			}
+
+			return transformed;
+		}
 	}
 
 	/**

+ 128 - 0
packages/ckeditor5-engine/tests/document/position.js

@@ -258,6 +258,16 @@ describe( 'position', () => {
 		expect( clone.path ).not.to.be.equal( position.path ); // make sure the paths are not the same array
 	} );
 
+	it( 'should return correct combination of this and given positions', () => {
+		const position = new Position( [ 1, 3, 4, 2 ], root );
+		const prefixPosition = new Position( [ 1, 1 ], root );
+		const targetPosition = new Position( [ 2, 5 ], root );
+
+		const combined = position.getCombined( prefixPosition, targetPosition );
+
+		expect( combined.path ).to.deep.equal( [ 2, 7, 4, 2 ] );
+	} );
+
 	describe( 'compareWith', () => {
 		it( 'should return Position.SAME if positions are same', () => {
 			const position = new Position( [ 1, 2, 3 ], root );
@@ -280,4 +290,122 @@ describe( 'position', () => {
 			expect( position.compareWith( compared ) ).to.equal( Position.AFTER );
 		} );
 	} );
+
+	describe( 'getTransformedByInsertion', () => {
+		it( 'should return a new Position instance', () => {
+			const position = new Position( [ 0 ], root );
+			const transformed = position.getTransformedByInsertion( new Position( [ 2 ], root ), 4, false );
+
+			expect( transformed ).not.to.equal( position );
+			expect( transformed ).to.be.instanceof( Position );
+		} );
+
+		it( 'should increment offset if insertion is in the same parent and closer offset', () => {
+			const position = new Position( [ 1, 2, 3 ], root );
+			const transformed = position.getTransformedByInsertion( new Position( [ 1, 2, 2 ], root ), 2, false );
+
+			expect( transformed.offset ).to.equal( 5 );
+		} );
+
+		it( 'should not increment offset if insertion position is in different root', () => {
+			const position = new Position( [ 1, 2, 3 ], root );
+			const transformed = position.getTransformedByInsertion( new Position( [ 1, 2, 2 ], otherRoot ), 2, false );
+
+			expect( transformed.offset ).to.equal( 3 );
+		} );
+
+		it( 'should not increment offset if insertion is in the same parent and the same offset', () => {
+			const position = new Position( [ 1, 2, 3 ], root );
+			const transformed = position.getTransformedByInsertion( new Position( [ 1, 2, 3 ], root ), 2, false );
+
+			expect( transformed.offset ).to.equal( 3 );
+		} );
+
+		it( 'should increment offset if insertion is in the same parent and the same offset and it is inserted before', () => {
+			const position = new Position( [ 1, 2, 3 ], root );
+			const transformed = position.getTransformedByInsertion( new Position( [ 1, 2, 3 ], root ), 2, true );
+
+			expect( transformed.offset ).to.equal( 5 );
+		} );
+
+		it( 'should not increment offset if insertion is in the same parent and further offset', () => {
+			const position = new Position( [ 1, 2, 3 ], root );
+			const transformed = position.getTransformedByInsertion( new Position( [ 1, 2, 4 ], root ), 2, false );
+
+			expect( transformed.offset ).to.equal( 3 );
+		} );
+
+		it( 'should update path if insertion position parent is a node from that path and offset is before next node on that path', () => {
+			const position = new Position( [ 1, 2, 3 ], root );
+			const transformed = position.getTransformedByInsertion( new Position( [ 1, 2 ], root ), 2, false );
+
+			expect( transformed.path ).to.deep.equal( [ 1, 4, 3 ] );
+		} );
+
+		it( 'should not update path if insertion position parent is a node from that path and offset is after next node on that path', () => {
+			const position = new Position( [ 1, 2, 3 ], root );
+			const transformed = position.getTransformedByInsertion( new Position( [ 1, 3 ], root ), 2, false );
+
+			expect( transformed.path ).to.deep.equal( [ 1, 2, 3 ] );
+		} );
+	} );
+
+	describe( 'getTransformedByDeletion', () => {
+		it( 'should return a new Position instance', () => {
+			const position = new Position( [ 0 ], root );
+			const transformed = position.getTransformedByDeletion( new Position( [ 2 ], root ), 4 );
+
+			expect( transformed ).not.to.equal( position );
+			expect( transformed ).to.be.instanceof( Position );
+		} );
+
+		it( 'should return null if original position is inside one of removed nodes', () => {
+			const position = new Position( [ 1, 2 ], root );
+			const transformed = position.getTransformedByDeletion( new Position( [ 0 ], root ), 2 );
+
+			expect( transformed ).to.be.null;
+		} );
+
+		it( 'should decrement offset if deletion is in the same parent and closer offset', () => {
+			const position = new Position( [ 1, 2, 7 ], root );
+			const transformed = position.getTransformedByDeletion( new Position( [ 1, 2, 2 ], root ), 2, false );
+
+			expect( transformed.offset ).to.equal( 5 );
+		} );
+
+		it( 'should set position offset to deletion offset if position is between removed nodes', () => {
+			const position = new Position( [ 1, 2, 4 ], root );
+			const transformed = position.getTransformedByDeletion( new Position( [ 1, 2, 3 ], root ), 5, false );
+
+			expect( transformed.offset ).to.equal( 3 );
+		} );
+
+		it( 'should not decrement offset if deletion position is in different root', () => {
+			const position = new Position( [ 1, 2, 3 ], root );
+			const transformed = position.getTransformedByDeletion( new Position( [ 1, 2, 1 ], otherRoot ), 2, false );
+
+			expect( transformed.offset ).to.equal( 3 );
+		} );
+
+		it( 'should not decrement offset if deletion is in the same parent and further offset', () => {
+			const position = new Position( [ 1, 2, 3 ], root );
+			const transformed = position.getTransformedByDeletion( new Position( [ 1, 2, 4 ], root ), 2, false );
+
+			expect( transformed.offset ).to.equal( 3 );
+		} );
+
+		it( 'should update path if deletion position parent is a node from that path and offset is before next node on that path', () => {
+			const position = new Position( [ 1, 2, 3 ], root );
+			const transformed = position.getTransformedByDeletion( new Position( [ 1, 0 ], root ), 2, false );
+
+			expect( transformed.path ).to.deep.equal( [ 1, 0, 3 ] );
+		} );
+
+		it( 'should not update path if deletion position parent is a node from that path and offset is after next node on that path', () => {
+			const position = new Position( [ 1, 2, 3 ], root );
+			const transformed = position.getTransformedByDeletion( new Position( [ 1, 3 ], root ), 2, false );
+
+			expect( transformed.path ).to.deep.equal( [ 1, 2, 3 ] );
+		} );
+	} );
 } );