浏览代码

Revert "Merge pull request #1175 from ckeditor/t/897"

This reverts commit 836dfd89a3b142fc212760f4fa5a80afbc7bb78d, reversing
changes made to fc7da8035cdc9533f66ad5de66711e25cd2b385e.
Piotrek Koszuliński 8 年之前
父节点
当前提交
2debd0e781
共有 28 个文件被更改,包括 594 次插入854 次删除
  1. 2 3
      packages/ckeditor5-engine/src/conversion/viewconversiondispatcher.js
  2. 3 2
      packages/ckeditor5-engine/src/dev-utils/view.js
  3. 15 25
      packages/ckeditor5-engine/src/model/delta/basic-transformations.js
  4. 3 1
      packages/ckeditor5-engine/src/model/documentselection.js
  5. 2 2
      packages/ckeditor5-engine/src/model/liveposition.js
  6. 2 2
      packages/ckeditor5-engine/src/model/liverange.js
  7. 1 1
      packages/ckeditor5-engine/src/model/operation/insertoperation.js
  8. 2 2
      packages/ckeditor5-engine/src/model/operation/renameoperation.js
  9. 23 38
      packages/ckeditor5-engine/src/model/operation/transform.js
  10. 69 117
      packages/ckeditor5-engine/src/model/position.js
  11. 24 56
      packages/ckeditor5-engine/src/model/range.js
  12. 12 10
      packages/ckeditor5-engine/src/model/selection.js
  13. 39 33
      packages/ckeditor5-engine/src/model/treewalker.js
  14. 19 24
      packages/ckeditor5-engine/src/view/position.js
  15. 15 26
      packages/ckeditor5-engine/src/view/range.js
  16. 15 11
      packages/ckeditor5-engine/src/view/selection.js
  17. 58 41
      packages/ckeditor5-engine/src/view/treewalker.js
  18. 24 29
      packages/ckeditor5-engine/src/view/writer.js
  19. 16 23
      packages/ckeditor5-engine/tests/model/delta/transform/_utils/utils.js
  20. 2 2
      packages/ckeditor5-engine/tests/model/delta/transform/movedelta.js
  21. 8 17
      packages/ckeditor5-engine/tests/model/delta/transform/splitdelta.js
  22. 5 15
      packages/ckeditor5-engine/tests/model/liverange.js
  23. 166 305
      packages/ckeditor5-engine/tests/model/operation/transform.js
  24. 24 0
      packages/ckeditor5-engine/tests/model/position.js
  25. 25 50
      packages/ckeditor5-engine/tests/model/range.js
  26. 2 2
      packages/ckeditor5-engine/tests/view/range.js
  27. 5 5
      packages/ckeditor5-engine/tests/view/selection.js
  28. 13 12
      packages/ckeditor5-engine/tests/view/writer/remove.js

+ 2 - 3
packages/ckeditor5-engine/src/conversion/viewconversiondispatcher.js

@@ -303,11 +303,10 @@ function extractMarkersFromModelFragment( modelItem ) {
 
 		// When marker of given name is not stored it means that we have found the beginning of the range.
 		if ( !markers.has( markerName ) ) {
-			markers.set( markerName, new ModelRange( currentPosition ) );
+			markers.set( markerName, new ModelRange( ModelPosition.createFromPosition( currentPosition ) ) );
 		// Otherwise is means that we have found end of the marker range.
 		} else {
-			const oldMarker = markers.get( markerName );
-			markers.set( markerName, new ModelRange( oldMarker.start, currentPosition ) );
+			markers.get( markerName ).end = ModelPosition.createFromPosition( currentPosition );
 		}
 
 		// Remove marker element from DocumentFragment.

+ 3 - 2
packages/ckeditor5-engine/src/dev-utils/view.js

@@ -541,7 +541,8 @@ class RangeParser {
 				throw new Error( `Parse error - end of range was found '${ item.bracket }' but range was not started before.` );
 			}
 
-			// When second start of range is found when one is already opened - selection does not allow intersecting ranges.
+			// When second start of range is found when one is already opened - selection does not allow intersecting
+			// ranges.
 			if ( range && ( item.bracket == ELEMENT_RANGE_START_TOKEN || item.bracket == TEXT_RANGE_START_TOKEN ) ) {
 				throw new Error( `Parse error - start of range was found '${ item.bracket }' but one range is already started.` );
 			}
@@ -549,7 +550,7 @@ class RangeParser {
 			if ( item.bracket == ELEMENT_RANGE_START_TOKEN || item.bracket == TEXT_RANGE_START_TOKEN ) {
 				range = new Range( item.position, item.position );
 			} else {
-				range = new Range( range.start, item.position );
+				range.end = item.position;
 				ranges.push( range );
 				range = null;
 			}

+ 15 - 25
packages/ckeditor5-engine/src/model/delta/basic-transformations.js

@@ -73,10 +73,8 @@ addTransformationCase( AttributeDelta, SplitDelta, ( a, b, context ) => {
 			const additionalAttributeDelta = new AttributeDelta();
 
 			const rangeStart = splitPosition.getShiftedBy( 1 );
-
-			const rangeEndPath = rangeStart.path.slice();
-			rangeEndPath.push( 0 );
-			const rangeEnd = new Position( rangeStart.root, rangeEndPath );
+			const rangeEnd = Position.createFromPosition( rangeStart );
+			rangeEnd.path.push( 0 );
 
 			const oldValue = b._cloneOperation.nodes.getNode( 0 ).getAttribute( operation.key );
 
@@ -238,7 +236,7 @@ addTransformationCase( SplitDelta, SplitDelta, ( a, b, context ) => {
 				a._cloneOperation instanceof ReinsertOperation && b._cloneOperation instanceof ReinsertOperation &&
 				a._cloneOperation.sourcePosition.offset > b._cloneOperation.sourcePosition.offset
 			) {
-				a._cloneOperation.sourcePosition = a._cloneOperation.sourcePosition.getShiftedBy( -1 );
+				a._cloneOperation.sourcePosition.offset--;
 			}
 
 			// `a` splits closer or at same offset.
@@ -319,33 +317,29 @@ addTransformationCase( SplitDelta, WrapDelta, ( a, b, context ) => {
 		// 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.
-		const splitPath = b.range.start.path.slice();
-		splitPath.push( b.howMany - 1 );
-
-		const splitNodePos = new Position( b.range.start.root, splitPath );
+		splitNodePos.path.push( b.howMany - 1 );
 
 		// SplitDelta insert operation position should be right after the node we split.
-		delta._cloneOperation.position = splitNodePos.getShiftedBy( 1 );
+		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.
-		const sourcePath = splitNodePos.path.slice();
-		sourcePath.push( a.position.offset );
-
-		delta._moveOperation.sourcePosition = new Position( splitNodePos.root, sourcePath );
+		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 targetPath = splitNodePos.getShiftedBy( 1 ).path.slice();
-		targetPath.push( 0 );
-
-		delta._moveOperation.targetPosition = new Position( splitNodePos.root, targetPath );
+		const targetPos = Position.createFromPosition( insertPos );
+		targetPos.path.push( 0 );
+		delta._moveOperation.targetPosition = targetPos;
 
 		return [ delta ];
 	}
@@ -440,17 +434,13 @@ addTransformationCase( WrapDelta, SplitDelta, ( a, b, context ) => {
 		const delta = a.clone();
 
 		// Move wrapping element insert position one node further so it is after the split node insertion.
-		delta._insertOperation.position = delta._insertOperation.position.getShiftedBy( 1 );
+		delta._insertOperation.position.offset++;
 
 		// Include the split node copy.
 		delta._moveOperation.howMany++;
 
 		// Change the path to wrapping element in move operation.
-		const index = delta._moveOperation.targetPosition.path.length - 2;
-
-		const path = delta._moveOperation.targetPosition.path.slice();
-		path[ index ] += 1;
-		delta._moveOperation.targetPosition = new Position( delta._moveOperation.targetPosition.root, path );
+		delta._moveOperation.targetPosition.path[ delta._moveOperation.targetPosition.path.length - 2 ]++;
 
 		return [ delta ];
 	}

+ 3 - 1
packages/ckeditor5-engine/src/model/documentselection.js

@@ -7,6 +7,7 @@
  * @module engine/model/documentselection
  */
 
+import Position from './position';
 import Range from './range';
 import LiveRange from './liverange';
 import Text from './text';
@@ -665,9 +666,10 @@ export default class DocumentSelection extends Selection {
 	_fixGraveyardSelection( liveRange, removedRangeStart ) {
 		// The start of the removed range is the closest position to the `liveRange` - the original selection range.
 		// This is a good candidate for a fixed selection range.
+		const positionCandidate = Position.createFromPosition( removedRangeStart );
 
 		// Find a range that is a correct selection range and is closest to the start of removed range.
-		const selectionRange = this._document.getNearestSelectionRange( removedRangeStart );
+		const selectionRange = this._document.getNearestSelectionRange( positionCandidate );
 
 		// Remove the old selection range before preparing and adding new selection range. This order is important,
 		// because new range, in some cases, may intersect with old range (it depends on `getNearestSelectionRange()` result).

+ 2 - 2
packages/ckeditor5-engine/src/model/liveposition.js

@@ -194,8 +194,8 @@ function transform( type, range, position ) {
 	if ( !this.isEqual( transformed ) ) {
 		const oldPosition = Position.createFromPosition( this );
 
-		this._path = transformed.path;
-		this._root = transformed.root;
+		this.path = transformed.path;
+		this.root = transformed.root;
 
 		this.fire( 'change', oldPosition );
 	}

+ 2 - 2
packages/ckeditor5-engine/src/model/liverange.js

@@ -178,8 +178,8 @@ function transform( changeType, deltaType, batch, targetRange, sourcePosition )
 		// If range boundaries have changed, fire `change:range` event.
 		const oldRange = Range.createFromRange( this );
 
-		this._start = updated.start;
-		this._end = updated.end;
+		this.start = updated.start;
+		this.end = updated.end;
 
 		this.fire( 'change:range', oldRange, {
 			type: changeType,

+ 1 - 1
packages/ckeditor5-engine/src/model/operation/insertoperation.js

@@ -37,7 +37,7 @@ export default class InsertOperation extends Operation {
 		 * @readonly
 		 * @member {module:engine/model/position~Position} module:engine/model/operation/insertoperation~InsertOperation#position
 		 */
-		this.position = position;
+		this.position = Position.createFromPosition( position );
 
 		/**
 		 * List of nodes to insert.

+ 2 - 2
packages/ckeditor5-engine/src/model/operation/renameoperation.js

@@ -66,7 +66,7 @@ export default class RenameOperation extends Operation {
 	 * @returns {module:engine/model/operation/renameoperation~RenameOperation} Clone of this operation.
 	 */
 	clone() {
-		return new RenameOperation( this.position, this.oldName, this.newName, this.baseVersion );
+		return new RenameOperation( Position.createFromPosition( this.position ), this.oldName, this.newName, this.baseVersion );
 	}
 
 	/**
@@ -75,7 +75,7 @@ export default class RenameOperation extends Operation {
 	 * @returns {module:engine/model/operation/renameoperation~RenameOperation}
 	 */
 	getReversed() {
-		return new RenameOperation( this.position, this.newName, this.oldName, this.baseVersion + 1 );
+		return new RenameOperation( Position.createFromPosition( this.position ), this.newName, this.oldName, this.baseVersion + 1 );
 	}
 
 	/**

+ 23 - 38
packages/ckeditor5-engine/src/model/operation/transform.js

@@ -179,29 +179,25 @@ const ot = {
 				// Take the start and the end of the range and transform them by deletion of moved nodes.
 				// Note that if rangeB was inside AttributeOperation range, only difference.end will be transformed.
 				// This nicely covers the joining simplification we did in the previous step.
-				const differenceTransformed = new Range(
-					difference.start._getTransformedByDeletion( b.sourcePosition, b.howMany ),
-					difference.end._getTransformedByDeletion( b.sourcePosition, b.howMany )
-				);
+				difference.start = difference.start._getTransformedByDeletion( b.sourcePosition, b.howMany );
+				difference.end = difference.end._getTransformedByDeletion( b.sourcePosition, b.howMany );
 
 				// MoveOperation pastes nodes into target position. We acknowledge this by proper transformation.
 				// Note that since we operate on transformed difference range, we should transform by
 				// previously transformed target position.
 				// Note that we do not use Position._getTransformedByMove on range boundaries because we need to
 				// transform by insertion a range as a whole, since newTargetPosition might be inside that range.
-				ranges = differenceTransformed._getTransformedByInsertion( b.getMovedRangeStart(), b.howMany, true, false ).reverse();
+				ranges = difference._getTransformedByInsertion( b.getMovedRangeStart(), b.howMany, true, false ).reverse();
 			}
 
 			if ( common !== null ) {
 				// Here we do not need to worry that newTargetPosition is inside moved range, because that
 				// would mean that the MoveOperation targets into itself, and that is incorrect operation.
 				// Instead, we calculate the new position of that part of original range.
-				const commonTransformed = new Range(
-					common.start._getCombined( b.sourcePosition, b.getMovedRangeStart() ),
-					common.end._getCombined( b.sourcePosition, b.getMovedRangeStart() )
-				);
+				common.start = common.start._getCombined( b.sourcePosition, b.getMovedRangeStart() );
+				common.end = common.end._getCombined( b.sourcePosition, b.getMovedRangeStart() );
 
-				ranges.push( commonTransformed );
+				ranges.push( common );
 			}
 
 			// Map transformed range(s) to operations and return them.
@@ -380,7 +376,7 @@ const ot = {
 			// Setting and evaluating some variables that will be used in special cases and default algorithm.
 			//
 			// Create ranges from `MoveOperations` properties.
-			let rangeA = Range.createFromPositionAndShift( a.sourcePosition, a.howMany );
+			const rangeA = Range.createFromPositionAndShift( a.sourcePosition, a.howMany );
 			const rangeB = Range.createFromPositionAndShift( b.sourcePosition, b.howMany );
 
 			// Assign `context.isStrong` to a different variable, because the value may change during execution of
@@ -432,10 +428,8 @@ const ot = {
 			if ( bTargetsToA && rangeA.containsRange( rangeB, true ) ) {
 				// There is a mini-special case here, where `rangeB` is on other level than `rangeA`. That's why
 				// we need to transform `a` operation anyway.
-				rangeA = new Range(
-					rangeA.start._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, !includeB ),
-					rangeA.end._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, includeB )
-				);
+				rangeA.start = rangeA.start._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, !includeB );
+				rangeA.end = rangeA.end._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, includeB );
 
 				return makeMoveOperationsFromRanges( [ rangeA ], newTargetPosition, a );
 			}
@@ -450,10 +444,8 @@ const ot = {
 			if ( aTargetsToB && rangeB.containsRange( rangeA, true ) ) {
 				// `a` operation is "moved together" with `b` operation.
 				// Here, just move `rangeA` "inside" `rangeB`.
-				rangeA = new Range(
-					rangeA.start._getCombined( b.sourcePosition, b.getMovedRangeStart() ),
-					rangeA.end._getCombined( b.sourcePosition, b.getMovedRangeStart() )
-				);
+				rangeA.start = rangeA.start._getCombined( b.sourcePosition, b.getMovedRangeStart() );
+				rangeA.end = rangeA.end._getCombined( b.sourcePosition, b.getMovedRangeStart() );
 
 				return makeMoveOperationsFromRanges( [ rangeA ], newTargetPosition, a );
 			}
@@ -474,10 +466,8 @@ const ot = {
 				// Transform `rangeA` by `b` operation and make operation out of it, and that's all.
 				// Note that this is a simplified version of default case, but here we treat the common part (whole `rangeA`)
 				// like a one difference part.
-				rangeA = new Range(
-					rangeA.start._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, !includeB ),
-					rangeA.end._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, includeB )
-				);
+				rangeA.start = rangeA.start._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, !includeB );
+				rangeA.end = rangeA.end._getTransformedByMove( b.sourcePosition, b.targetPosition, b.howMany, includeB );
 
 				return makeMoveOperationsFromRanges( [ rangeA ], newTargetPosition, a );
 			}
@@ -510,12 +500,10 @@ const ot = {
 			// This is an array with one or two ranges. Two ranges if `rangeB` is inside `rangeA`.
 			const difference = rangeA.getDifference( rangeB );
 
-			for ( const rangeInDiff of difference ) {
+			for ( const range of difference ) {
 				// Transform those ranges by `b` operation. For example if `b` moved range from before those ranges, fix those ranges.
-				const range = new Range(
-					rangeInDiff.start._getTransformedByDeletion( b.sourcePosition, b.howMany ),
-					rangeInDiff.end._getTransformedByDeletion( b.sourcePosition, b.howMany )
-				);
+				range.start = range.start._getTransformedByDeletion( b.sourcePosition, b.howMany );
+				range.end = range.end._getTransformedByDeletion( b.sourcePosition, b.howMany );
 
 				// If `b` operation targets into `rangeA` on the same level, spread `rangeA` into two ranges.
 				const shouldSpread = compareArrays( range.start.getParentPath(), b.getMovedRangeStart().getParentPath() ) == 'same';
@@ -525,14 +513,12 @@ const ot = {
 			}
 
 			// Then, we have to manage the "common part" of both move ranges.
-			const intersectionRange = rangeA.getIntersection( rangeB );
+			const common = rangeA.getIntersection( rangeB );
 
-			if ( intersectionRange !== null && isStrong && !bTargetsToA ) {
+			if ( common !== null && isStrong && !bTargetsToA ) {
 				// Calculate the new position of that part of original range.
-				const common = new Range(
-					intersectionRange.start._getCombined( b.sourcePosition, b.getMovedRangeStart() ),
-					intersectionRange.end._getCombined( b.sourcePosition, b.getMovedRangeStart() )
-				);
+				common.start = common.start._getCombined( b.sourcePosition, b.getMovedRangeStart() );
+				common.end = common.end._getCombined( b.sourcePosition, b.getMovedRangeStart() );
 
 				// Take care of proper range order.
 				//
@@ -641,10 +627,9 @@ function joinRanges( ranges ) {
 	} else if ( ranges.length == 1 ) {
 		return ranges[ 0 ];
 	} else {
-		return new Range(
-			ranges[ 0 ].start,
-			ranges[ ranges.length - 1 ].end
-		);
+		ranges[ 0 ].end = ranges[ ranges.length - 1 ].end;
+
+		return ranges[ 0 ];
 	}
 }
 

+ 69 - 117
packages/ckeditor5-engine/src/model/position.js

@@ -66,70 +66,47 @@ export default class Position {
 
 		// Normalize the root and path (if element was passed).
 		path = root.getPath().concat( path );
-
-		// Make path immutable
-		Object.freeze( path );
+		root = root.root;
 
 		/**
 		 * Root of the position path.
 		 *
-		 * @protected
+		 * @readonly
 		 * @member {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment}
-		 * module:engine/model/position~Position#_root
+		 * module:engine/model/position~Position#root
 		 */
-		this._root = root.root;
+		this.root = root;
 
 		/**
-		 * Position of the node in the tree.
+		 * Position of the node in the tree. **Path contains offsets, not indexes.**
+		 *
+		 * Position can be placed before, after or in a {@link module:engine/model/node~Node node} if that node has
+		 * {@link module:engine/model/node~Node#offsetSize} greater than `1`. Items in position path are
+		 * {@link module:engine/model/node~Node#startOffset starting offsets} of position ancestors, starting from direct root children,
+		 * down to the position offset in it's parent.
+		 *
+		 *		 ROOT
+		 *		  |- P            before: [ 0 ]         after: [ 1 ]
+		 *		  |- UL           before: [ 1 ]         after: [ 2 ]
+		 *		     |- LI        before: [ 1, 0 ]      after: [ 1, 1 ]
+		 *		     |  |- foo    before: [ 1, 0, 0 ]   after: [ 1, 0, 3 ]
+		 *		     |- LI        before: [ 1, 1 ]      after: [ 1, 2 ]
+		 *		        |- bar    before: [ 1, 1, 0 ]   after: [ 1, 1, 3 ]
 		 *
-		 * @protected
-		 * @member {Array.<Number>} module:engine/model/position~Position#_path
+		 * `foo` and `bar` are representing {@link module:engine/model/text~Text text nodes}. Since text nodes has offset size
+		 * greater than `1` you can place position offset between their start and end:
+		 *
+		 *		 ROOT
+		 *		  |- P
+		 *		  |- UL
+		 *		     |- LI
+		 *		     |  |- f^o|o  ^ has path: [ 1, 0, 1 ]   | has path: [ 1, 0, 2 ]
+		 *		     |- LI
+		 *		        |- b^a|r  ^ has path: [ 1, 1, 1 ]   | has path: [ 1, 1, 2 ]
+		 *
+		 * @member {Array.<Number>} module:engine/model/position~Position#path
 		 */
-		this._path = path;
-	}
-
-	/**
-	 * Position of the node in the tree. **Path contains offsets, not indexes.**
-	 *
-	 * Position can be placed before, after or in a {@link module:engine/model/node~Node node} if that node has
-	 * {@link module:engine/model/node~Node#offsetSize} greater than `1`. Items in position path are
-	 * {@link module:engine/model/node~Node#startOffset starting offsets} of position ancestors, starting from direct root children,
-	 * down to the position offset in it's parent.
-	 *
-	 *		 ROOT
-	 *		  |- P            before: [ 0 ]         after: [ 1 ]
-	 *		  |- UL           before: [ 1 ]         after: [ 2 ]
-	 *		     |- LI        before: [ 1, 0 ]      after: [ 1, 1 ]
-	 *		     |  |- foo    before: [ 1, 0, 0 ]   after: [ 1, 0, 3 ]
-	 *		     |- LI        before: [ 1, 1 ]      after: [ 1, 2 ]
-	 *		        |- bar    before: [ 1, 1, 0 ]   after: [ 1, 1, 3 ]
-	 *
-	 * `foo` and `bar` are representing {@link module:engine/model/text~Text text nodes}. Since text nodes has offset size
-	 * greater than `1` you can place position offset between their start and end:
-	 *
-	 *		 ROOT
-	 *		  |- P
-	 *		  |- UL
-	 *		     |- LI
-	 *		     |  |- f^o|o  ^ has path: [ 1, 0, 1 ]   | has path: [ 1, 0, 2 ]
-	 *		     |- LI
-	 *		        |- b^a|r  ^ has path: [ 1, 1, 1 ]   | has path: [ 1, 1, 2 ]
-	 *
-	 * @member {Array.<Number>} module:engine/model/position~Position#path
-	 */
-	get path() {
-		return this._path;
-	}
-
-	/**
-	 * Root of the position path.
-	 *
-	 * @readonly
-	 * @member {module:engine/model/element~Element|module:engine/model/documentfragment~DocumentFragment}
-	 * module:engine/model/position~Position#root
-	 */
-	get root() {
-		return this._root;
+		this.path = path;
 	}
 
 	/**
@@ -143,6 +120,13 @@ export default class Position {
 	}
 
 	/**
+	 * @param {Number} newOffset
+	 */
+	set offset( newOffset ) {
+		this.path[ this.path.length - 1 ] = newOffset;
+	}
+
+	/**
 	 * Parent element of this position.
 	 *
 	 * Keep in mind that `parent` value is calculated when the property is accessed.
@@ -358,29 +342,18 @@ export default class Position {
 
 	/**
 	 * Returns a new instance of `Position`, that has same {@link #parent parent} but it's offset
-	 * is set to `offset` value.
-	 *
-	 * @param {Number} offset Position offset. See {@link module:engine/model/position~Position#offset}.
-	 * @returns {module:engine/model/position~Position} Moved position.
-	 */
-	getShiftedTo( offset ) {
-		const path = this.path.slice();
-		path[ path.length - 1 ] = offset;
-
-		return new Position( this.root, path );
-	}
-
-	/**
-	 * Returns a new instance of `Position`, that has same {@link #parent parent} but it's offset
 	 * is shifted by `shift` value (can be a negative value).
 	 *
 	 * @param {Number} shift Offset shift. Can be a negative value.
 	 * @returns {module:engine/model/position~Position} Shifted position.
 	 */
 	getShiftedBy( shift ) {
-		const newOffset = this.offset + shift;
+		const shifted = Position.createFromPosition( this );
+
+		const offset = shifted.offset + shift;
+		shifted.offset = offset < 0 ? 0 : offset;
 
-		return this.getShiftedTo( newOffset < 0 ? 0 : newOffset );
+		return shifted;
 	}
 
 	/**
@@ -460,13 +433,13 @@ export default class Position {
 				return true;
 
 			case 'before':
-				left = this; // eslint-disable-line consistent-this
-				right = otherPosition;
+				left = Position.createFromPosition( this );
+				right = Position.createFromPosition( otherPosition );
 				break;
 
 			case 'after':
-				left = otherPosition;
-				right = this; // eslint-disable-line consistent-this
+				left = Position.createFromPosition( otherPosition );
+				right = Position.createFromPosition( this );
 				break;
 
 			default:
@@ -486,34 +459,20 @@ export default class Position {
 					return false;
 				}
 
-				const path = left.getParentPath();
-				path[ path.length - 1 ]++;
-				left = new Position( left.root, path );
-
+				left.path = left.path.slice( 0, -1 );
 				leftParent = leftParent.parent;
+				left.offset++;
 			} else {
 				if ( right.offset !== 0 ) {
 					return false;
 				}
 
-				right = new Position( right.root, right.getParentPath() );
+				right.path = right.path.slice( 0, -1 );
 			}
 		}
 	}
 
 	/**
-	 * Converts `Position` to plain object and returns it.
-	 *
-	 * @returns {Object} `Position` converted to plain object.
-	 */
-	toJSON() {
-		return {
-			root: this.root.toJSON(),
-			path: this.path
-		};
-	}
-
-	/**
 	 * Returns a copy of this position that is 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.
 	 *
@@ -523,14 +482,14 @@ export default class Position {
 	 * @returns {module:engine/model/position~Position|null} Transformed position or `null`.
 	 */
 	_getTransformedByDeletion( deletePosition, howMany ) {
+		const transformed = Position.createFromPosition( this );
+
 		// This position can't be affected if deletion was in a different root.
 		if ( this.root != deletePosition.root ) {
-			return Position.createFromPosition( this );
+			return transformed;
 		}
 
-		const comparisonResult = compareArrays( deletePosition.getParentPath(), this.getParentPath() );
-
-		if ( comparisonResult == 'same' ) {
+		if ( compareArrays( deletePosition.getParentPath(), this.getParentPath() ) == '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...
@@ -538,10 +497,11 @@ export default class Position {
 					// Position is in removed range, it's no longer in the tree.
 					return null;
 				} else {
-					return this.getShiftedBy( -howMany );
+					// Decrement the offset accordingly.
+					transformed.offset -= howMany;
 				}
 			}
-		} else if ( comparisonResult == 'prefix' ) {
+		} else if ( compareArrays( deletePosition.getParentPath(), this.getParentPath() ) == 'prefix' ) {
 			// If nodes are removed from a node that is on a path to this position...
 			const i = deletePosition.path.length - 1;
 
@@ -553,16 +513,12 @@ export default class Position {
 					return null;
 				} else {
 					// Otherwise, decrement index on that path.
-					const path = this.path.slice();
-
-					path[ i ] -= howMany;
-
-					return new Position( this.root, path );
+					transformed.path[ i ] -= howMany;
 				}
 			}
 		}
 
-		return Position.createFromPosition( this );
+		return transformed;
 	}
 
 	/**
@@ -577,9 +533,11 @@ export default class Position {
 	 * @returns {module:engine/model/position~Position} Transformed position.
 	 */
 	_getTransformedByInsertion( insertPosition, howMany, insertBefore ) {
+		const transformed = Position.createFromPosition( this );
+
 		// This position can't be affected if insertion was in a different root.
 		if ( this.root != insertPosition.root ) {
-			return Position.createFromPosition( this );
+			return transformed;
 		}
 
 		if ( compareArrays( insertPosition.getParentPath(), this.getParentPath() ) == 'same' ) {
@@ -587,7 +545,7 @@ export default class Position {
 			if ( insertPosition.offset < this.offset || ( insertPosition.offset == this.offset && insertBefore ) ) {
 				// And are inserted before an offset of that position...
 				// "Push" this positions offset.
-				return this.getShiftedBy( howMany );
+				transformed.offset += howMany;
 			}
 		} else if ( compareArrays( insertPosition.getParentPath(), this.getParentPath() ) == 'prefix' ) {
 			// If nodes are inserted in a node that is on a path to this position...
@@ -596,15 +554,11 @@ export default class Position {
 			if ( insertPosition.offset <= this.path[ i ] ) {
 				// And are inserted before next node of that path...
 				// "Push" the index on that path.
-				const path = this.path.slice();
-
-				path[ i ] += howMany;
-
-				return new Position( this.root, path );
+				transformed.path[ i ] += howMany;
 			}
 		}
 
-		return Position.createFromPosition( this );
+		return transformed;
 	}
 
 	/**
@@ -672,20 +626,18 @@ export default class Position {
 		const i = source.path.length - 1;
 
 		// The first part of a path to combined position is a path to the place where nodes were moved.
-		let combinedPath = target.path.slice();
+		const combined = Position.createFromPosition( target );
 
 		// 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.
-		const oldOffset = last( combinedPath );
-		const newOffset = oldOffset + this.path[ i ] - source.offset;
-		combinedPath[ combinedPath.length - 1 ] = newOffset;
+		combined.offset = combined.offset + this.path[ i ] - source.offset;
 
 		// Then, add the rest of the path.
 		// If this position is at the same level as `from` position nothing will get added.
-		combinedPath = combinedPath.concat( this.path.slice( i + 1 ) );
+		combined.path = combined.path.concat( this.path.slice( i + 1 ) );
 
-		return new Position( target.root, combinedPath );
+		return combined;
 	}
 
 	/**

+ 24 - 56
packages/ckeditor5-engine/src/model/range.js

@@ -27,18 +27,18 @@ export default class Range {
 		/**
 		 * Start position.
 		 *
-		 * @protected
+		 * @readonly
 		 * @member {module:engine/model/position~Position}
 		 */
-		this._start = start;
+		this.start = Position.createFromPosition( start );
 
 		/**
 		 * End position.
 		 *
-		 * @protected
+		 * @readonly
 		 * @member {module:engine/model/position~Position}
 		 */
-		this._end = end ? end : start;
+		this.end = end ? Position.createFromPosition( end ) : Position.createFromPosition( start );
 	}
 
 	/**
@@ -58,26 +58,6 @@ export default class Range {
 	}
 
 	/**
-	 * Start position.
-	 *
-	 * @readonly
-	 * @member {module:engine/model/position~Position}
-	 */
-	get start() {
-		return this._start;
-	}
-
-	/**
-	 * End position.
-	 *
-	 * @readonly
-	 * @member {module:engine/model/position~Position}
-	 */
-	get end() {
-		return this._end;
-	}
-
-	/**
 	 * Returns whether the range is collapsed, that is if {@link #start} and
 	 * {@link #end} positions are equal.
 	 *
@@ -300,7 +280,7 @@ export default class Range {
 		const ranges = [];
 		const diffAt = this.start.getCommonPath( this.end ).length;
 
-		let pos = this.start;
+		const pos = Position.createFromPosition( this.start );
 		let posParent = pos.parent;
 
 		// Go up.
@@ -311,7 +291,8 @@ export default class Range {
 				ranges.push( new Range( pos, pos.getShiftedBy( howMany ) ) );
 			}
 
-			pos = Position.createAfter( posParent );
+			pos.path = pos.path.slice( 0, -1 );
+			pos.offset++;
 			posParent = posParent.parent;
 		}
 
@@ -324,11 +305,8 @@ export default class Range {
 				ranges.push( new Range( pos, pos.getShiftedBy( howMany ) ) );
 			}
 
-			const path = pos.getParentPath();
-			path.push( offset );
-			path.push( 0 );
-
-			pos = new Position( pos.root, path );
+			pos.offset = offset;
+			pos.path.push( 0 );
 		}
 
 		return ranges;
@@ -489,18 +467,6 @@ export default class Range {
 	}
 
 	/**
-	 * Converts `Range` to plain object and returns it.
-	 *
-	 * @returns {Object} `Range` converted to plain object.
-	 */
-	toJSON() {
-		return {
-			start: this.start.toJSON(),
-			end: this.end.toJSON()
-		};
-	}
-
-	/**
 	 * Returns a range that is a result of transforming this range by a change in the model document.
 	 *
 	 * @protected
@@ -647,13 +613,15 @@ export default class Range {
 				)
 			];
 		} else {
+			const range = Range.createFromRange( this );
+
 			const insertBeforeStart = !isSticky;
-			const insertBeforeEnd = this.isCollapsed ? true : isSticky;
+			const insertBeforeEnd = range.isCollapsed ? true : isSticky;
 
-			const start = this.start._getTransformedByInsertion( insertPosition, howMany, insertBeforeStart );
-			const end = this.end._getTransformedByInsertion( insertPosition, howMany, insertBeforeEnd );
+			range.start = range.start._getTransformedByInsertion( insertPosition, howMany, insertBeforeStart );
+			range.end = range.end._getTransformedByInsertion( insertPosition, howMany, insertBeforeEnd );
 
-			return [ new Range( start, end ) ];
+			return [ range ];
 		}
 	}
 
@@ -787,8 +755,9 @@ export default class Range {
 	 */
 	static createCollapsedAt( itemOrPosition, offset ) {
 		const start = Position.createAt( itemOrPosition, offset );
+		const end = Position.createFromPosition( start );
 
-		return new Range( start, start );
+		return new Range( start, end );
 	}
 
 	/**
@@ -835,14 +804,13 @@ export default class Range {
 		// 4. At this moment we don't need the original range.
 		// We are going to modify the result and we need to return a new instance of Range.
 		// We have to create a copy of the reference range.
-		let start = ref.start;
-		let end = ref.end;
+		const result = new this( ref.start, ref.end );
 
 		// 5. Ranges should be checked and glued starting from the range that is closest to the reference range.
 		// Since ranges are sorted, start with the range with index that is closest to reference range index.
-		for ( let i = refIndex - 1; i >= 0; i-- ) {
-			if ( ranges[ i ].end.isEqual( start ) ) {
-				start = ranges[ i ].start;
+		for ( let i = refIndex - 1; i >= 0; i++ ) {
+			if ( ranges[ i ].end.isEqual( result.start ) ) {
+				result.start = Position.createFromPosition( ranges[ i ].start );
 			} else {
 				// If ranges are not starting/ending at the same position there is no point in looking further.
 				break;
@@ -852,15 +820,15 @@ export default class Range {
 		// 6. Ranges should be checked and glued starting from the range that is closest to the reference range.
 		// Since ranges are sorted, start with the range with index that is closest to reference range index.
 		for ( let i = refIndex + 1; i < ranges.length; i++ ) {
-			if ( ranges[ i ].start.isEqual( end ) ) {
-				end = ranges[ i ].end;
+			if ( ranges[ i ].start.isEqual( result.end ) ) {
+				result.end = Position.createFromPosition( ranges[ i ].end );
 			} else {
 				// If ranges are not starting/ending at the same position there is no point in looking further.
 				break;
 			}
 		}
 
-		return new this( start, end );
+		return result;
 	}
 
 	/**

+ 12 - 10
packages/ckeditor5-engine/src/model/selection.js

@@ -174,16 +174,18 @@ export default class Selection {
 	}
 
 	/**
-	 * Returns an iterator that iterates over selection ranges.
+	 * Returns an iterator that iterates over copies of selection ranges.
 	 *
 	 * @returns {Iterator.<module:engine/model/range~Range>}
 	 */
-	getRanges() {
-		return this._ranges[ Symbol.iterator ]();
+	* getRanges() {
+		for ( const range of this._ranges ) {
+			yield Range.createFromRange( range );
+		}
 	}
 
 	/**
-	 * Returns first range in the selection.
+	 * Returns a copy of the first range in the selection.
 	 * First range is the one which {@link module:engine/model/range~Range#start start} position
 	 * {@link module:engine/model/position~Position#isBefore is before} start position of all other ranges
 	 * (not to confuse with the first range added to the selection).
@@ -201,11 +203,11 @@ export default class Selection {
 			}
 		}
 
-		return first;
+		return first ? Range.createFromRange( first ) : null;
 	}
 
 	/**
-	 * Returns last range in the selection.
+	 * Returns a copy of the last range in the selection.
 	 * Last range is the one which {@link module:engine/model/range~Range#end end} position
 	 * {@link module:engine/model/position~Position#isAfter is after} end position of all other ranges (not to confuse with the range most
 	 * recently added to the selection).
@@ -223,7 +225,7 @@ export default class Selection {
 			}
 		}
 
-		return last;
+		return last ? Range.createFromRange( last ) : null;
 	}
 
 	/**
@@ -236,9 +238,9 @@ export default class Selection {
 	 * @returns {module:engine/model/position~Position|null}
 	 */
 	getFirstPosition() {
-		const firstRange = this.getFirstRange();
+		const first = this.getFirstRange();
 
-		return firstRange ? firstRange.start : null;
+		return first ? Position.createFromPosition( first.start ) : null;
 	}
 
 	/**
@@ -253,7 +255,7 @@ export default class Selection {
 	getLastPosition() {
 		const lastRange = this.getLastRange();
 
-		return lastRange ? lastRange.end : null;
+		return lastRange ? Position.createFromPosition( lastRange.end ) : null;
 	}
 
 	/**

+ 39 - 33
packages/ckeditor5-engine/src/model/treewalker.js

@@ -85,9 +85,9 @@ export default class TreeWalker {
 		 * @member {module:engine/model/position~Position} module:engine/model/treewalker~TreeWalker#position
 		 */
 		if ( options.startPosition ) {
-			this.position = options.startPosition;
+			this.position = Position.createFromPosition( options.startPosition );
 		} else {
-			this.position = this.boundaries[ this.direction == 'backward' ? 'end' : 'start' ];
+			this.position = Position.createFromPosition( this.boundaries[ this.direction == 'backward' ? 'end' : 'start' ] );
 		}
 
 		/**
@@ -204,33 +204,33 @@ export default class TreeWalker {
 	 */
 	_next() {
 		const previousPosition = this.position;
+		const position = Position.createFromPosition( this.position );
 		const parent = this._visitedParent;
 
 		// We are at the end of the root.
-		if ( parent.parent === null && this.position.offset === parent.maxOffset ) {
+		if ( parent.parent === null && position.offset === parent.maxOffset ) {
 			return { done: true };
 		}
 
 		// We reached the walker boundary.
-		if ( parent === this._boundaryEndParent && this.position.offset == this.boundaries.end.offset ) {
+		if ( parent === this._boundaryEndParent && position.offset == this.boundaries.end.offset ) {
 			return { done: true };
 		}
 
-		const node = this.position.textNode ? this.position.textNode : this.position.nodeAfter;
+		const node = position.textNode ? position.textNode : position.nodeAfter;
 
 		if ( node instanceof Element ) {
 			if ( !this.shallow ) {
 				// Manual operations on path internals for optimization purposes. Here and in the rest of the method.
-				const path = this.position.path.slice();
-				path.push( 0 );
-				this.position = new Position( this.position.root, path );
-
+				position.path.push( 0 );
 				this._visitedParent = node;
 			} else {
-				this.position = this.position.getShiftedBy( 1 );
+				position.offset++;
 			}
 
-			return formatReturnValue( 'elementStart', node, previousPosition, this.position, 1 );
+			this.position = position;
+
+			return formatReturnValue( 'elementStart', node, previousPosition, position, 1 );
 		} else if ( node instanceof Text ) {
 			let charactersCount;
 
@@ -243,24 +243,27 @@ export default class TreeWalker {
 					offset = this.boundaries.end.offset;
 				}
 
-				charactersCount = offset - this.position.offset;
+				charactersCount = offset - position.offset;
 			}
 
-			const offsetInTextNode = this.position.offset - node.startOffset;
+			const offsetInTextNode = position.offset - node.startOffset;
 			const item = new TextProxy( node, offsetInTextNode, charactersCount );
 
-			this.position = this.position.getShiftedBy( charactersCount );
+			position.offset += charactersCount;
+			this.position = position;
 
-			return formatReturnValue( 'text', item, previousPosition, this.position, charactersCount );
+			return formatReturnValue( 'text', item, previousPosition, position, charactersCount );
 		} else {
 			// `node` is not set, we reached the end of current `parent`.
-			this.position = Position.createAfter( parent );
+			position.path.pop();
+			position.offset++;
+			this.position = position;
 			this._visitedParent = parent.parent;
 
 			if ( this.ignoreElementEnd ) {
 				return this._next();
 			} else {
-				return formatReturnValue( 'elementEnd', parent, previousPosition, this.position );
+				return formatReturnValue( 'elementEnd', parent, previousPosition, position );
 			}
 		}
 	}
@@ -275,38 +278,39 @@ export default class TreeWalker {
 	 */
 	_previous() {
 		const previousPosition = this.position;
+		const position = Position.createFromPosition( this.position );
 		const parent = this._visitedParent;
 
 		// We are at the beginning of the root.
-		if ( parent.parent === null && this.position.offset === 0 ) {
+		if ( parent.parent === null && position.offset === 0 ) {
 			return { done: true };
 		}
 
 		// We reached the walker boundary.
-		if ( parent == this._boundaryStartParent && this.position.offset == this.boundaries.start.offset ) {
+		if ( parent == this._boundaryStartParent && position.offset == this.boundaries.start.offset ) {
 			return { done: true };
 		}
 
 		// Get node just before current position
-		const node = this.position.textNode ? this.position.textNode : this.position.nodeBefore;
+		const node = position.textNode ? position.textNode : position.nodeBefore;
 
 		if ( node instanceof Element ) {
-			this.position = this.position.getShiftedBy( -1 );
+			position.offset--;
 
 			if ( !this.shallow ) {
-				const path = this.position.path.slice();
-				path.push( node.maxOffset );
-
-				this.position = new Position( this.position.root, path );
+				position.path.push( node.maxOffset );
+				this.position = position;
 				this._visitedParent = node;
 
 				if ( this.ignoreElementEnd ) {
 					return this._previous();
 				} else {
-					return formatReturnValue( 'elementEnd', node, previousPosition, this.position );
+					return formatReturnValue( 'elementEnd', node, previousPosition, position );
 				}
 			} else {
-				return formatReturnValue( 'elementStart', node, previousPosition, this.position, 1 );
+				this.position = position;
+
+				return formatReturnValue( 'elementStart', node, previousPosition, position, 1 );
 			}
 		} else if ( node instanceof Text ) {
 			let charactersCount;
@@ -320,21 +324,23 @@ export default class TreeWalker {
 					offset = this.boundaries.start.offset;
 				}
 
-				charactersCount = this.position.offset - offset;
+				charactersCount = position.offset - offset;
 			}
 
-			const offsetInTextNode = this.position.offset - node.startOffset;
+			const offsetInTextNode = position.offset - node.startOffset;
 			const item = new TextProxy( node, offsetInTextNode - charactersCount, charactersCount );
 
-			this.position = this.position.getShiftedBy( -charactersCount );
+			position.offset -= charactersCount;
+			this.position = position;
 
-			return formatReturnValue( 'text', item, previousPosition, this.position, charactersCount );
+			return formatReturnValue( 'text', item, previousPosition, position, charactersCount );
 		} else {
 			// `node` is not set, we reached the beginning of current `parent`.
-			this.position = Position.createBefore( parent );
+			position.path.pop();
+			this.position = position;
 			this._visitedParent = parent.parent;
 
-			return formatReturnValue( 'elementStart', parent, previousPosition, this.position, 1 );
+			return formatReturnValue( 'elementStart', parent, previousPosition, position, 1 );
 		}
 	}
 }

+ 19 - 24
packages/ckeditor5-engine/src/view/position.js

@@ -24,28 +24,20 @@ export default class Position {
 	 * @param {Number} offset Position offset.
 	 */
 	constructor( parent, offset ) {
-		this._parent = parent;
-		this._offset = offset;
-	}
-
-	/**
-	 * Position parent.
-	 *
-	 * @readonly
-	 * @type {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment}
-	 */
-	get parent() {
-		return this._parent;
-	}
-
-	/**
-	 * Position offset.
-	 *
-	 * @readonly
-	 * @type {Number}
-	 */
-	get offset() {
-		return this._offset;
+		/**
+		 * Position parent.
+		 *
+		 * @member {module:engine/view/node~Node|module:engine/view/documentfragment~DocumentFragment}
+		 * module:engine/view/position~Position#parent
+		 */
+		this.parent = parent;
+
+		/**
+		 * Position offset.
+		 *
+		 * @member {Number} module:engine/view/position~Position#offset
+		 */
+		this.offset = offset;
 	}
 
 	/**
@@ -137,9 +129,12 @@ export default class Position {
 	 * @returns {module:engine/view/position~Position} Shifted position.
 	 */
 	getShiftedBy( shift ) {
-		const offset = this.offset + shift;
+		const shifted = Position.createFromPosition( this );
+
+		const offset = shifted.offset + shift;
+		shifted.offset = offset < 0 ? 0 : offset;
 
-		return new Position( this.parent, offset < 0 ? 0 : offset );
+		return shifted;
 	}
 
 	/**

+ 15 - 26
packages/ckeditor5-engine/src/view/range.js

@@ -23,8 +23,19 @@ export default class Range {
 	 * @param {module:engine/view/position~Position} [end] End position. If not set, range will be collapsed at `start` position.
 	 */
 	constructor( start, end = null ) {
-		this._start = start;
-		this._end = end ? end : start;
+		/**
+		 * Start position.
+		 *
+		 * @member {module:engine/view/position~Position}
+		 */
+		this.start = Position.createFromPosition( start );
+
+		/**
+		 * End position.
+		 *
+		 * @member {module:engine/view/position~Position}
+		 */
+		this.end = end ? Position.createFromPosition( end ) : Position.createFromPosition( start );
 	}
 
 	/**
@@ -43,29 +54,8 @@ export default class Range {
 	}
 
 	/**
-	 * Start position.
-	 *
-	 * @readonly
-	 * @type {module:engine/view/position~Position}
-	 */
-	get start() {
-		return this._start;
-	}
-
-	/**
-	 * End position.
-	 *
-	 * @readonly
-	 * @type {module:engine/view/position~Position}
-	 */
-	get end() {
-		return this._end;
-	}
-
-	/**
 	 * Returns whether the range is collapsed, that is it start and end positions are equal.
 	 *
-	 * @readonly
 	 * @type {Boolean}
 	 */
 	get isCollapsed() {
@@ -76,7 +66,6 @@ export default class Range {
 	 * Returns whether this range is flat, that is if {@link module:engine/view/range~Range#start start} position and
 	 * {@link module:engine/view/range~Range#end end} position are in the same {@link module:engine/view/position~Position#parent parent}.
 	 *
-	 * @readonly
 	 * @type {Boolean}
 	 */
 	get isFlat() {
@@ -86,7 +75,6 @@ export default class Range {
 	/**
 	 * Range root element.
 	 *
-	 * @readonly
 	 * @type {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment}
 	 */
 	get root() {
@@ -463,8 +451,9 @@ export default class Range {
 	 */
 	static createCollapsedAt( itemOrPosition, offset ) {
 		const start = Position.createAt( itemOrPosition, offset );
+		const end = Position.createFromPosition( start );
 
-		return new Range( start, start );
+		return new Range( start, end );
 	}
 }
 

+ 15 - 11
packages/ckeditor5-engine/src/view/selection.js

@@ -132,8 +132,9 @@ export default class Selection {
 			return null;
 		}
 		const range = this._ranges[ this._ranges.length - 1 ];
+		const anchor = this._lastRangeBackward ? range.end : range.start;
 
-		return this._lastRangeBackward ? range.end : range.start;
+		return Position.createFromPosition( anchor );
 	}
 
 	/**
@@ -147,8 +148,9 @@ export default class Selection {
 			return null;
 		}
 		const range = this._ranges[ this._ranges.length - 1 ];
+		const focus = this._lastRangeBackward ? range.start : range.end;
 
-		return this._lastRangeBackward ? range.start : range.end;
+		return Position.createFromPosition( focus );
 	}
 
 	/**
@@ -220,16 +222,18 @@ export default class Selection {
 	}
 
 	/**
-	 * Returns an iterator that contains all ranges added to the selection.
+	 * Returns an iterator that contains copies of all ranges added to the selection.
 	 *
 	 * @returns {Iterator.<module:engine/view/range~Range>}
 	 */
-	getRanges() {
-		return this._ranges[ Symbol.iterator ]();
+	* getRanges() {
+		for ( const range of this._ranges ) {
+			yield Range.createFromRange( range );
+		}
 	}
 
 	/**
-	 * Returns first range in the selection. First range is the one which
+	 * Returns copy of the first range in the selection. First range is the one which
 	 * {@link module:engine/view/range~Range#start start} position {@link module:engine/view/position~Position#isBefore is before} start
 	 * position of all other ranges (not to confuse with the first range added to the selection).
 	 * Returns `null` if no ranges are added to selection.
@@ -245,11 +249,11 @@ export default class Selection {
 			}
 		}
 
-		return first;
+		return first ? Range.createFromRange( first ) : null;
 	}
 
 	/**
-	 * Returns last range in the selection. Last range is the one which {@link module:engine/view/range~Range#end end}
+	 * Returns copy of the last range in the selection. Last range is the one which {@link module:engine/view/range~Range#end end}
 	 * position {@link module:engine/view/position~Position#isAfter is after} end position of all other ranges (not to confuse
 	 * with the last range added to the selection). Returns `null` if no ranges are added to selection.
 	 *
@@ -264,7 +268,7 @@ export default class Selection {
 			}
 		}
 
-		return last;
+		return last ? Range.createFromRange( last ) : null;
 	}
 
 	/**
@@ -277,7 +281,7 @@ export default class Selection {
 	getFirstPosition() {
 		const firstRange = this.getFirstRange();
 
-		return firstRange ? firstRange.start : null;
+		return firstRange ? Position.createFromPosition( firstRange.start ) : null;
 	}
 
 	/**
@@ -290,7 +294,7 @@ export default class Selection {
 	getLastPosition() {
 		const lastRange = this.getLastRange();
 
-		return lastRange ? lastRange.end : null;
+		return lastRange ? Position.createFromPosition( lastRange.end ) : null;
 	}
 
 	/**

+ 58 - 41
packages/ckeditor5-engine/src/view/treewalker.js

@@ -73,9 +73,9 @@ export default class TreeWalker {
 		 * @member {module:engine/view/position~Position} module:engine/view/treewalker~TreeWalker#position
 		 */
 		if ( options.startPosition ) {
-			this.position = options.startPosition;
+			this.position = Position.createFromPosition( options.startPosition );
 		} else {
-			this.position = options.boundaries[ options.direction == 'backward' ? 'end' : 'start' ];
+			this.position = Position.createFromPosition( options.boundaries[ options.direction == 'backward' ? 'end' : 'start' ] );
 		}
 
 		/**
@@ -187,16 +187,17 @@ export default class TreeWalker {
 	 * @returns {module:engine/view/treewalker~TreeWalkerValue} return.value Information about taken step.
 	 */
 	_next() {
+		let position = Position.createFromPosition( this.position );
 		const previousPosition = this.position;
-		const parent = this.position.parent;
+		const parent = position.parent;
 
 		// We are at the end of the root.
-		if ( parent.parent === null && this.position.offset === parent.childCount ) {
+		if ( parent.parent === null && position.offset === parent.childCount ) {
 			return { done: true };
 		}
 
 		// We reached the walker boundary.
-		if ( parent === this._boundaryEndParent && this.position.offset == this.boundaries.end.offset ) {
+		if ( parent === this._boundaryEndParent && position.offset == this.boundaries.end.offset ) {
 			return { done: true };
 		}
 
@@ -205,29 +206,32 @@ export default class TreeWalker {
 
 		// Text is a specific parent because it contains string instead of child nodes.
 		if ( parent instanceof Text ) {
-			if ( this.position.isAtEnd ) {
+			if ( position.isAtEnd ) {
 				// Prevent returning "elementEnd" for Text node. Skip that value and return the next walker step.
 				this.position = Position.createAfter( parent );
 
 				return this._next();
 			}
 
-			node = parent.data[ this.position.offset ];
+			node = parent.data[ position.offset ];
 		} else {
-			node = parent.getChild( this.position.offset );
+			node = parent.getChild( position.offset );
 		}
 
 		if ( node instanceof Element ) {
 			if ( !this.shallow ) {
-				this.position = new Position( node, 0 );
+				position = new Position( node, 0 );
 			} else {
-				this.position = this.position.getShiftedBy( 1 );
+				position.offset++;
 			}
 
-			return this._formatReturnValue( 'elementStart', node, previousPosition, this.position, 1 );
+			this.position = position;
+
+			return this._formatReturnValue( 'elementStart', node, previousPosition, position, 1 );
 		} else if ( node instanceof Text ) {
 			if ( this.singleCharacters ) {
-				this.position = new Position( node, 0 );
+				position = new Position( node, 0 );
+				this.position = position;
 
 				return this._next();
 			} else {
@@ -238,13 +242,15 @@ export default class TreeWalker {
 				if ( node == this._boundaryEndParent ) {
 					charactersCount = this.boundaries.end.offset;
 					item = new TextProxy( node, 0, charactersCount );
-					this.position = Position.createAfter( item );
+					position = Position.createAfter( item );
 				} else {
 					// If not just keep moving forward.
-					this.position = this.position.getShiftedBy( 1 );
+					position.offset++;
 				}
 
-				return this._formatReturnValue( 'text', item, previousPosition, this.position, charactersCount );
+				this.position = position;
+
+				return this._formatReturnValue( 'text', item, previousPosition, position, charactersCount );
 			}
 		} else if ( typeof node == 'string' ) {
 			let textLength;
@@ -255,22 +261,24 @@ export default class TreeWalker {
 				// Check if text stick out of walker range.
 				const endOffset = parent === this._boundaryEndParent ? this.boundaries.end.offset : parent.data.length;
 
-				textLength = endOffset - this.position.offset;
+				textLength = endOffset - position.offset;
 			}
 
-			const textProxy = new TextProxy( parent, this.position.offset, textLength );
+			const textProxy = new TextProxy( parent, position.offset, textLength );
 
-			this.position = this.position.getShiftedBy( textLength );
+			position.offset += textLength;
+			this.position = position;
 
-			return this._formatReturnValue( 'text', textProxy, previousPosition, this.position, textLength );
+			return this._formatReturnValue( 'text', textProxy, previousPosition, position, textLength );
 		} else {
 			// `node` is not set, we reached the end of current `parent`.
-			this.position = Position.createAfter( parent );
+			position = Position.createAfter( parent );
+			this.position = position;
 
 			if ( this.ignoreElementEnd ) {
 				return this._next();
 			} else {
-				return this._formatReturnValue( 'elementEnd', parent, previousPosition, this.position );
+				return this._formatReturnValue( 'elementEnd', parent, previousPosition, position );
 			}
 		}
 	}
@@ -284,16 +292,17 @@ export default class TreeWalker {
 	 * @returns {module:engine/view/treewalker~TreeWalkerValue} return.value Information about taken step.
 	 */
 	_previous() {
+		let position = Position.createFromPosition( this.position );
 		const previousPosition = this.position;
-		const parent = this.position.parent;
+		const parent = position.parent;
 
 		// We are at the beginning of the root.
-		if ( parent.parent === null && this.position.offset === 0 ) {
+		if ( parent.parent === null && position.offset === 0 ) {
 			return { done: true };
 		}
 
 		// We reached the walker boundary.
-		if ( parent == this._boundaryStartParent && this.position.offset == this.boundaries.start.offset ) {
+		if ( parent == this._boundaryStartParent && position.offset == this.boundaries.start.offset ) {
 			return { done: true };
 		}
 
@@ -302,35 +311,38 @@ export default class TreeWalker {
 
 		// Text {@link module:engine/view/text~Text} element is a specific parent because contains string instead of child nodes.
 		if ( parent instanceof Text ) {
-			if ( this.position.isAtStart ) {
+			if ( position.isAtStart ) {
 				// Prevent returning "elementStart" for Text node. Skip that value and return the next walker step.
 				this.position = Position.createBefore( parent );
 
 				return this._previous();
 			}
 
-			node = parent.data[ this.position.offset - 1 ];
+			node = parent.data[ position.offset - 1 ];
 		} else {
-			node = parent.getChild( this.position.offset - 1 );
+			node = parent.getChild( position.offset - 1 );
 		}
 
 		if ( node instanceof Element ) {
 			if ( !this.shallow ) {
-				this.position = new Position( node, node.childCount );
+				position = new Position( node, node.childCount );
+				this.position = position;
 
 				if ( this.ignoreElementEnd ) {
 					return this._previous();
 				} else {
-					return this._formatReturnValue( 'elementEnd', node, previousPosition, this.position );
+					return this._formatReturnValue( 'elementEnd', node, previousPosition, position );
 				}
 			} else {
-				this.position = this.position.getShiftedBy( -1 );
+				position.offset--;
+				this.position = position;
 
-				return this._formatReturnValue( 'elementStart', node, previousPosition, this.position, 1 );
+				return this._formatReturnValue( 'elementStart', node, previousPosition, position, 1 );
 			}
 		} else if ( node instanceof Text ) {
 			if ( this.singleCharacters ) {
-				this.position = new Position( node, node.data.length );
+				position = new Position( node, node.data.length );
+				this.position = position;
 
 				return this._previous();
 			} else {
@@ -343,13 +355,15 @@ export default class TreeWalker {
 
 					item = new TextProxy( node, offset, node.data.length - offset );
 					charactersCount = item.data.length;
-					this.position = Position.createBefore( item );
+					position = Position.createBefore( item );
 				} else {
 					// If not just keep moving backward.
-					this.position = this.position.getShiftedBy( -1 );
+					position.offset--;
 				}
 
-				return this._formatReturnValue( 'text', item, previousPosition, this.position, charactersCount );
+				this.position = position;
+
+				return this._formatReturnValue( 'text', item, previousPosition, position, charactersCount );
 			}
 		} else if ( typeof node == 'string' ) {
 			let textLength;
@@ -358,21 +372,24 @@ export default class TreeWalker {
 				// Check if text stick out of walker range.
 				const startOffset = parent === this._boundaryStartParent ? this.boundaries.start.offset : 0;
 
-				textLength = this.position.offset - startOffset;
+				textLength = position.offset - startOffset;
 			} else {
 				textLength = 1;
 			}
 
-			this.position = this.position.getShiftedBy( -textLength );
+			position.offset -= textLength;
+
+			const textProxy = new TextProxy( parent, position.offset, textLength );
 
-			const textProxy = new TextProxy( parent, this.position.offset, textLength );
+			this.position = position;
 
-			return this._formatReturnValue( 'text', textProxy, previousPosition, this.position, textLength );
+			return this._formatReturnValue( 'text', textProxy, previousPosition, position, textLength );
 		} else {
 			// `node` is not set, we reached the beginning of current `parent`.
-			this.position = Position.createBefore( parent );
+			position = Position.createBefore( parent );
+			this.position = position;
 
-			return this._formatReturnValue( 'elementStart', parent, previousPosition, this.position, 1 );
+			return this._formatReturnValue( 'elementStart', parent, previousPosition, position, 1 );
 		}
 	}
 

+ 24 - 29
packages/ckeditor5-engine/src/view/writer.js

@@ -305,7 +305,7 @@ export function insert( position, nodes ) {
 	const insertionPosition = _breakAttributes( position, true );
 
 	const length = container.insertChildren( insertionPosition.offset, nodes );
-	let endPosition = insertionPosition.getShiftedBy( length );
+	const endPosition = insertionPosition.getShiftedBy( length );
 	const start = mergeAttributes( insertionPosition );
 
 	// When no nodes were inserted - return collapsed range.
@@ -314,7 +314,7 @@ export function insert( position, nodes ) {
 	} else {
 		// If start position was merged - move end position.
 		if ( !start.isEqual( insertionPosition ) ) {
-			endPosition = endPosition.getShiftedBy( -1 );
+			endPosition.offset--;
 		}
 
 		const end = mergeAttributes( endPosition );
@@ -331,7 +331,8 @@ export function insert( position, nodes ) {
  * same parent container.
  *
  * @function module:engine/view/writer~writer.remove
- * @param {module:engine/view/range~Range} range Range to remove from container.
+ * @param {module:engine/view/range~Range} range Range to remove from container. After removing, it will be updated
+ * to a collapsed range showing the new position.
  * @returns {module:engine/view/documentfragment~DocumentFragment} Document fragment containing removed nodes.
  */
 export function remove( range ) {
@@ -352,7 +353,9 @@ export function remove( range ) {
 	const removed = parentContainer.removeChildren( breakStart.offset, count );
 
 	// Merge after removing.
-	mergeAttributes( breakStart );
+	const mergePosition = mergeAttributes( breakStart );
+	range.start = mergePosition;
+	range.end = Position.createFromPosition( mergePosition );
 
 	// Return removed nodes.
 	return new DocumentFragment( removed );
@@ -403,20 +406,17 @@ export function clear( range, element ) {
 
 		// If we have found element to remove.
 		if ( rangeToRemove ) {
-			let rangeEnd = rangeToRemove.end;
-			let rangeStart = rangeToRemove.start;
-
 			// We need to check if element range stick out of the given range and truncate if it is.
 			if ( rangeToRemove.end.isAfter( range.end ) ) {
-				rangeEnd = range.end;
+				rangeToRemove.end = range.end;
 			}
 
 			if ( rangeToRemove.start.isBefore( range.start ) ) {
-				rangeStart = range.start;
+				rangeToRemove.start = range.start;
 			}
 
 			// At the end we remove range with found element.
-			remove( new Range( rangeStart, rangeEnd ) );
+			remove( rangeToRemove );
 		}
 	}
 }
@@ -447,7 +447,7 @@ export function move( sourceRange, targetPosition ) {
 
 		nodes = remove( sourceRange );
 
-		targetPosition = targetPosition.getShiftedBy( parent.childCount - countBefore );
+		targetPosition.offset += ( parent.childCount - countBefore );
 	} else {
 		nodes = remove( sourceRange );
 	}
@@ -511,13 +511,10 @@ export function wrap( range, attribute ) {
 	const start = mergeAttributes( newRange.start );
 
 	// If start position was merged - move end position back.
-	let rangeEnd = newRange.end;
-
 	if ( !start.isEqual( newRange.start ) ) {
-		rangeEnd = rangeEnd.getShiftedBy( -1 );
+		newRange.end.offset--;
 	}
-
-	const end = mergeAttributes( rangeEnd );
+	const end = mergeAttributes( newRange.end );
 
 	return new Range( start, end );
 }
@@ -540,7 +537,7 @@ export function wrapPosition( position, attribute ) {
 
 	// Return same position when trying to wrap with attribute similar to position parent.
 	if ( attribute.isSimilar( position.parent ) ) {
-		return movePositionToTextNode( position );
+		return movePositionToTextNode( Position.createFromPosition( position ) );
 	}
 
 	// When position is inside text node - break it and place new position between two text nodes.
@@ -628,12 +625,10 @@ export function unwrap( range, attribute ) {
 	const start = mergeAttributes( newRange.start );
 
 	// If start position was merged - move end position back.
-	let rangeEnd = newRange.end;
-
 	if ( !start.isEqual( newRange.start ) ) {
-		rangeEnd = rangeEnd.getShiftedBy( -1 );
+		newRange.end.offset--;
 	}
-	const end = mergeAttributes( rangeEnd );
+	const end = mergeAttributes( newRange.end );
 
 	return new Range( start, end );
 }
@@ -707,12 +702,12 @@ function _breakAttributesRange( range, forceSplitText = false ) {
 		return new Range( position, position );
 	}
 
-	let breakEnd = _breakAttributes( rangeEnd, forceSplitText );
+	const breakEnd = _breakAttributes( rangeEnd, forceSplitText );
 	const count = breakEnd.parent.childCount;
 	const breakStart = _breakAttributes( rangeStart, forceSplitText );
 
 	// Calculate new break end offset.
-	breakEnd = breakEnd.getShiftedBy( breakEnd.parent.childCount - count );
+	breakEnd.offset += breakEnd.parent.childCount - count;
 
 	return new Range( breakStart, breakEnd );
 }
@@ -757,12 +752,12 @@ function _breakAttributes( position, forceSplitText = false ) {
 
 	// There are no attributes to break and text nodes breaking is not forced.
 	if ( !forceSplitText && positionParent.is( 'text' ) && isContainerOrFragment( positionParent.parent ) ) {
-		return position;
+		return Position.createFromPosition( position );
 	}
 
 	// Position's parent is container, so no attributes to break.
 	if ( isContainerOrFragment( positionParent ) ) {
-		return position;
+		return Position.createFromPosition( position );
 	}
 
 	// Break text and start again in new position.
@@ -862,8 +857,8 @@ function unwrapChildren( parent, startOffset, endOffset, attribute ) {
 	// Merge at each unwrap.
 	let offsetChange = 0;
 
-	for ( let position of unwrapPositions ) {
-		position = position.getShiftedBy( -offsetChange );
+	for ( const position of unwrapPositions ) {
+		position.offset -= offsetChange;
 
 		// Do not merge with elements outside selected children.
 		if ( position.offset == startOffset || position.offset == endOffset ) {
@@ -923,8 +918,8 @@ function wrapChildren( parent, startOffset, endOffset, attribute ) {
 	// Merge at each wrap.
 	let offsetChange = 0;
 
-	for ( let position of wrapPositions ) {
-		position = position.getShiftedBy( -offsetChange );
+	for ( const position of wrapPositions ) {
+		position.offset -= offsetChange;
 
 		// Do not merge with elements outside selected children.
 		if ( position.offset == startOffset ) {

+ 16 - 23
packages/ckeditor5-engine/tests/model/delta/transform/_utils/utils.js

@@ -68,14 +68,12 @@ export function getMarkerDelta( name, oldRange, newRange, version ) {
 export function getMergeDelta( position, howManyInPrev, howManyInNext, version ) {
 	const delta = new MergeDelta();
 
-	const sourcePath = position.path.slice();
-	sourcePath.push( 0 );
-	const sourcePosition = new Position( position.root, sourcePath );
+	const sourcePosition = Position.createFromPosition( position );
+	sourcePosition.path.push( 0 );
 
-	const targetPath = position.getShiftedBy( -1 ).path.slice();
-	targetPath.push( howManyInPrev );
-
-	const targetPosition = new Position( position.root, targetPath );
+	const targetPosition = Position.createFromPosition( position );
+	targetPosition.offset--;
+	targetPosition.path.push( howManyInPrev );
 
 	const move = new MoveOperation( sourcePosition, howManyInNext, targetPosition, version );
 	move.isSticky = true;
@@ -131,15 +129,12 @@ export function getRenameDelta( position, oldName, newName, baseVersion ) {
 export function getSplitDelta( position, nodeCopy, howManyMove, version ) {
 	const delta = new SplitDelta();
 
-	const insertPath = position.getParentPath();
-	insertPath[ insertPath.length - 1 ]++;
-
-	const insertPosition = new Position( position.root, insertPath );
+	const insertPosition = Position.createFromPosition( position );
+	insertPosition.path = insertPosition.getParentPath();
+	insertPosition.offset++;
 
-	const targetPath = insertPosition.path.slice();
-	targetPath.push( 0 );
-
-	const targetPosition = new Position( insertPosition.root, targetPath );
+	const targetPosition = Position.createFromPosition( insertPosition );
+	targetPosition.path.push( 0 );
 
 	delta.addOperation( new InsertOperation( insertPosition, [ nodeCopy ], version ) );
 
@@ -158,10 +153,8 @@ export function getWrapDelta( range, element, version ) {
 
 	const insert = new InsertOperation( range.end, element, version );
 
-	const targetPath = range.end.path.slice();
-	targetPath.push( 0 );
-	const targetPosition = new Position( range.end.root, targetPath );
-
+	const targetPosition = Position.createFromPosition( range.end );
+	targetPosition.path.push( 0 );
 	const move = new MoveOperation( range.start, range.end.offset - range.start.offset, targetPosition, version + 1 );
 
 	delta.addOperation( insert );
@@ -175,14 +168,14 @@ export function getWrapDelta( range, element, version ) {
 export function getUnwrapDelta( positionBefore, howManyChildren, version ) {
 	const delta = new UnwrapDelta();
 
-	const sourcePath = positionBefore.path.slice();
-	sourcePath.push( 0 );
-	const sourcePosition = new Position( positionBefore.root, sourcePath );
+	const sourcePosition = Position.createFromPosition( positionBefore );
+	sourcePosition.path.push( 0 );
 
 	const move = new MoveOperation( sourcePosition, howManyChildren, positionBefore, version );
 	move.isSticky = true;
 
-	const removePosition = positionBefore.getShiftedBy( howManyChildren );
+	const removePosition = Position.createFromPosition( positionBefore );
+	removePosition.offset += howManyChildren;
 
 	const gy = sourcePosition.root.document.graveyard;
 	const gyPos = Position.createAt( gy, 0 );

+ 2 - 2
packages/ckeditor5-engine/tests/model/delta/transform/movedelta.js

@@ -135,8 +135,8 @@ describe( 'transform', () => {
 			} );
 
 			it( 'move range in merged node #2', () => {
-				moveDelta._moveOperation.sourcePosition = new Position( root, [ 3, 3, 1 ] );
-				moveDelta._moveOperation.targetPosition = new Position( root, [ 3, 3, 4 ] );
+				moveDelta._moveOperation.sourcePosition.path = [ 3, 3, 1 ];
+				moveDelta._moveOperation.targetPosition.path = [ 3, 3, 4 ];
 
 				const mergePosition = new Position( root, [ 3, 3 ] );
 				const mergeDelta = getMergeDelta( mergePosition, 1, 4, baseVersion );

+ 8 - 17
packages/ckeditor5-engine/tests/model/delta/transform/splitdelta.js

@@ -6,7 +6,6 @@
 import transformations from '../../../../src/model/delta/basic-transformations'; // eslint-disable-line no-unused-vars
 
 import deltaTransform from '../../../../src/model/delta/transform';
-
 const transform = deltaTransform.transform;
 
 import Element from '../../../../src/model/element';
@@ -968,15 +967,10 @@ describe( 'transform', () => {
 				baseVersion = removeDelta.operations.length;
 
 				const newInsertPosition = removeOperation.targetPosition.getShiftedBy( 2 );
-
-				const newSourcePath = removeOperation.targetPosition.getShiftedBy( 1 ).path.slice();
-				newSourcePath.push( 2 );
-				const newMoveSourcePosition = new Position( removeOperation.targetPosition.root, newSourcePath );
-
-				const newMoveTargetPath = newInsertPosition.path.slice();
-				newMoveTargetPath.push( 0 );
-
-				const newMoveTargetPosition = new Position( newInsertPosition.root, newMoveTargetPath );
+				const newMoveSourcePosition = removeOperation.targetPosition.getShiftedBy( 1 );
+				newMoveSourcePosition.path.push( 2 );
+				const newMoveTargetPosition = Position.createAt( newInsertPosition );
+				newMoveTargetPosition.path.push( 0 );
 
 				expectDelta( transformed[ 0 ], {
 					type: SplitDelta,
@@ -1009,13 +1003,10 @@ describe( 'transform', () => {
 				baseVersion = removeDelta.operations.length;
 
 				const newInsertPosition = removeOperation.targetPosition.getShiftedBy( 2 );
-				const newMoveSourcePath = removeOperation.targetPosition.getShiftedBy( 1 ).path.slice();
-				newMoveSourcePath.push( 3 );
-				const newMoveSourcePosition = new Position( removeOperation.targetPosition.root, newMoveSourcePath );
-
-				const newMoveTargetPath = newInsertPosition.path.slice();
-				newMoveTargetPath.push( 0 );
-				const newMoveTargetPosition = new Position( newInsertPosition.root, newMoveTargetPath );
+				const newMoveSourcePosition = removeOperation.targetPosition.getShiftedBy( 1 );
+				newMoveSourcePosition.path.push( 3 );
+				const newMoveTargetPosition = Position.createAt( newInsertPosition );
+				newMoveTargetPosition.path.push( 0 );
 
 				expectDelta( transformed[ 0 ], {
 					type: SplitDelta,

+ 5 - 15
packages/ckeditor5-engine/tests/model/liverange.js

@@ -208,9 +208,7 @@ describe( 'LiveRange', () => {
 			} );
 
 			it( 'is at the live range start position and live range is collapsed', () => {
-				live = new LiveRange( live.start, new Position( live.end.root, [ 0, 1, 4 ] ) );
-				spy = sinon.spy();
-				live.on( 'change:range', spy );
+				live.end.path = [ 0, 1, 4 ];
 
 				const insertRange = new Range( new Position( root, [ 0, 1, 4 ] ), new Position( root, [ 0, 1, 8 ] ) );
 
@@ -374,9 +372,7 @@ describe( 'LiveRange', () => {
 			} );
 
 			it( 'is equal to live range', () => {
-				live = new LiveRange( live.start, new Position( live.end.root, [ 0, 1, 7 ] ) );
-				spy = sinon.spy();
-				live.on( 'change:range', spy );
+				live.end.path = [ 0, 1, 7 ];
 
 				const moveSource = new Position( root, [ 0, 1, 4 ] );
 				const moveRange = new Range( new Position( root, [ 0, 3, 0 ] ), new Position( root, [ 0, 3, 3 ] ) );
@@ -393,9 +389,7 @@ describe( 'LiveRange', () => {
 			} );
 
 			it( 'contains live range', () => {
-				live = new LiveRange( live.start, new Position( live.end.root, [ 0, 1, 7 ] ) );
-				spy = sinon.spy();
-				live.on( 'change:range', spy );
+				live.end.path = [ 0, 1, 7 ];
 
 				const moveSource = new Position( root, [ 0, 1, 3 ] );
 				const moveRange = new Range( new Position( root, [ 0, 3, 0 ] ), new Position( root, [ 0, 3, 9 ] ) );
@@ -412,9 +406,7 @@ describe( 'LiveRange', () => {
 			} );
 
 			it( 'is intersecting with live range and points to live range', () => {
-				live = new LiveRange( live.start, new Position( live.end.root, [ 0, 1, 12 ] ) );
-				spy = sinon.spy();
-				live.on( 'change:range', spy );
+				live.end.path = [ 0, 1, 12 ];
 
 				const moveSource = new Position( root, [ 0, 1, 2 ] );
 				const moveRange = new Range( new Position( root, [ 0, 1, 7 ] ), new Position( root, [ 0, 1, 10 ] ) );
@@ -664,9 +656,7 @@ describe( 'LiveRange', () => {
 			} );
 
 			it( 'from the range to the range', () => {
-				live = new LiveRange( live.start, new Position( live.end.root, [ 0, 1, 12 ] ) );
-				spy = sinon.spy();
-				live.on( 'change:content', spy );
+				live.end.path = [ 0, 1, 12 ];
 
 				const moveSource = new Position( root, [ 0, 1, 6 ] );
 				const moveRange = new Range( new Position( root, [ 0, 1, 8 ] ), new Position( root, [ 0, 1, 10 ] ) );

文件差异内容过多而无法显示
+ 166 - 305
packages/ckeditor5-engine/tests/model/operation/transform.js


+ 24 - 0
packages/ckeditor5-engine/tests/model/position.js

@@ -291,6 +291,14 @@ describe( 'Position', () => {
 		expect( new Position( root, [ 1, 0, 3 ] ) ).to.have.property( 'index' ).that.equals( 1 );
 	} );
 
+	it( 'should be able to set offset', () => {
+		const position = new Position( root, [ 1, 0, 2 ] );
+		position.offset = 4;
+
+		expect( position.offset ).to.equal( 4 );
+		expect( position.path ).to.deep.equal( [ 1, 0, 4 ] );
+	} );
+
 	it( 'should have nodeBefore if it is not inside a text node', () => {
 		expect( new Position( root, [ 0 ] ).nodeBefore ).to.be.null;
 		expect( new Position( root, [ 1 ] ).nodeBefore ).to.equal( p );
@@ -597,6 +605,14 @@ describe( 'Position', () => {
 	} );
 
 	describe( '_getTransformedByInsertion()', () => {
+		it( 'should return a new Position instance', () => {
+			const position = new Position( root, [ 0 ] );
+			const transformed = position._getTransformedByInsertion( new Position( root, [ 2 ] ), 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( root, [ 1, 2, 3 ] );
 			const transformed = position._getTransformedByInsertion( new Position( root, [ 1, 2, 2 ] ), 2, false );
@@ -649,6 +665,14 @@ describe( 'Position', () => {
 	} );
 
 	describe( '_getTransformedByDeletion()', () => {
+		it( 'should return a new Position instance', () => {
+			const position = new Position( root, [ 0 ] );
+			const transformed = position._getTransformedByDeletion( new Position( root, [ 2 ] ), 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( root, [ 1, 2 ] );
 			const transformed = position._getTransformedByDeletion( new Position( root, [ 0 ] ), 2 );

+ 25 - 50
packages/ckeditor5-engine/tests/model/range.js

@@ -855,8 +855,7 @@ describe( 'Range', () => {
 			} );
 
 			it( 'move inside the range', () => {
-				range = new Range( range.start, range.end.getShiftedTo( 6 ) );
-
+				range.end.offset = 6;
 				const start = new Position( root, [ 3 ] );
 				const target = new Position( root, [ 5 ] );
 				const delta = getMoveDelta( start, 1, target, 1 );
@@ -978,10 +977,8 @@ describe( 'Range', () => {
 
 		describe( 'by SplitDelta', () => {
 			it( 'split inside range', () => {
-				range = new Range(
-					new Position( root, [ 0, 2 ] ),
-					new Position( root, [ 0, 4 ] )
-				);
+				range.start = new Position( root, [ 0, 2 ] );
+				range.end = new Position( root, [ 0, 4 ] );
 
 				const delta = getSplitDelta( new Position( root, [ 0, 3 ] ), new Element( 'p' ), 3, 1 );
 
@@ -993,10 +990,8 @@ describe( 'Range', () => {
 			} );
 
 			it( 'split at the beginning of multi-element range', () => {
-				range = new Range(
-					new Position( root, [ 0, 4 ] ),
-					new Position( root, [ 1, 2 ] )
-				);
+				range.start = new Position( root, [ 0, 4 ] );
+				range.end = new Position( root, [ 1, 2 ] );
 
 				const delta = getSplitDelta( new Position( root, [ 0, 4 ] ), new Element( 'p' ), 3, 1 );
 
@@ -1008,10 +1003,8 @@ 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 ] )
-				);
+				range.start = new Position( root, [ 0, 0 ] );
+				range.end = new Position( root, [ 0, 4 ] );
 
 				const delta = getSplitDelta( new Position( root, [ 0, 3 ] ), new Element( 'p' ), 3, 1 );
 
@@ -1023,10 +1016,8 @@ 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 ] )
-				);
+				range.start = new Position( root, [ 0, 3 ] );
+				range.end = new Position( root, [ 0, 6 ] );
 
 				const delta = getSplitDelta( new Position( root, [ 0, 4 ] ), new Element( 'p' ), 2, 1 );
 
@@ -1038,10 +1029,8 @@ 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 ] )
-				);
+				range.start = new Position( root, [ 0, 6 ] );
+				range.end = new Position( root, [ 0, 6 ] );
 
 				const delta = getSplitDelta( new Position( root, [ 0, 3 ] ), new Element( 'p' ), 3, 1 );
 
@@ -1055,10 +1044,8 @@ describe( 'Range', () => {
 
 		describe( 'by MergeDelta', () => {
 			it( 'merge element with collapsed range', () => {
-				range = new Range(
-					new Position( root, [ 1, 0 ] ),
-					new Position( root, [ 1, 0 ] )
-				);
+				range.start = new Position( root, [ 1, 0 ] );
+				range.end = new Position( root, [ 1, 0 ] );
 
 				const delta = getMergeDelta( new Position( root, [ 1 ] ), 3, 3, 1 );
 
@@ -1119,10 +1106,8 @@ describe( 'Range', () => {
 		describe( 'by WrapDelta', () => {
 			it( 'maintans start position when wrapping element in which the range starts and ends', () => {
 				// <p>f[o]o</p><p>bar</p>
-				range = new Range(
-					new Position( root, [ 0, 1 ] ),
-					new Position( root, [ 0, 2 ] )
-				);
+				range.start = new Position( root, [ 0, 1 ] );
+				range.end = new Position( root, [ 0, 2 ] );
 
 				const wrapRange = new Range( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) );
 				const wrapElement = new Element( 'w' );
@@ -1138,10 +1123,8 @@ describe( 'Range', () => {
 
 			it( 'maintans start position when wrapping element in which the range starts but not ends', () => {
 				// <p>f[oo</p><p>b]ar</p>
-				range = new Range(
-					new Position( root, [ 0, 1 ] ),
-					new Position( root, [ 1, 1 ] )
-				);
+				range.start = new Position( root, [ 0, 1 ] );
+				range.end = new Position( root, [ 1, 1 ] );
 
 				const wrapRange = new Range( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) );
 				const wrapElement = new Element( 'w' );
@@ -1157,10 +1140,8 @@ describe( 'Range', () => {
 
 			it( 'maintans end position when wrapping element in which the range ends but not starts', () => {
 				// <p>f[oo</p><p>b]ar</p>
-				range = new Range(
-					new Position( root, [ 0, 1 ] ),
-					new Position( root, [ 1, 1 ] )
-				);
+				range.start = new Position( root, [ 0, 1 ] );
+				range.end = new Position( root, [ 1, 1 ] );
 
 				const wrapRange = new Range( new Position( root, [ 1 ] ), new Position( root, [ 2 ] ) );
 				const wrapElement = new Element( 'w' );
@@ -1178,10 +1159,8 @@ describe( 'Range', () => {
 		describe( 'by UnwrapDelta', () => {
 			it( 'maintans start position when wrapping element in which the range starts and ends', () => {
 				// <w><p>f[o]o</p></w><p>bar</p>
-				range = new Range(
-					new Position( root, [ 0, 0, 1 ] ),
-					new Position( root, [ 0, 0, 2 ] )
-				);
+				range.start = new Position( root, [ 0, 0, 1 ] );
+				range.end = new Position( root, [ 0, 0, 2 ] );
 
 				const unwrapPosition = new Position( root, [ 0 ] );
 				const delta = getUnwrapDelta( unwrapPosition, 1, 1 );
@@ -1196,10 +1175,8 @@ describe( 'Range', () => {
 
 			it( 'maintans start position when wrapping element in which the range starts but not ends', () => {
 				// <w><p>f[oo</p></w><p>b]ar</p>
-				range = new Range(
-					new Position( root, [ 0, 0, 1 ] ),
-					new Position( root, [ 1, 1 ] )
-				);
+				range.start = new Position( root, [ 0, 0, 1 ] );
+				range.end = new Position( root, [ 1, 1 ] );
 
 				const unwrapPosition = new Position( root, [ 0 ] );
 				const delta = getUnwrapDelta( unwrapPosition, 1, 1 );
@@ -1217,10 +1194,8 @@ describe( 'Range', () => {
 
 			it( 'maintans end position when wrapping element in which the range ends but not starts', () => {
 				// <p>f[oo</p><w><p>b]ar</p></w>
-				range = new Range(
-					new Position( root, [ 0, 1 ] ),
-					new Position( root, [ 1, 0, 1 ] )
-				);
+				range.start = new Position( root, [ 0, 1 ] );
+				range.end = new Position( root, [ 1, 0, 1 ] );
 
 				const unwrapPosition = new Position( root, [ 1 ] );
 				const delta = getUnwrapDelta( unwrapPosition, 1, 1 );

+ 2 - 2
packages/ckeditor5-engine/tests/view/range.js

@@ -25,8 +25,8 @@ describe( 'Range', () => {
 			const range = new Range( start, end );
 
 			expect( range ).to.be.an.instanceof( Range );
-			expect( range ).to.have.property( 'start' ).that.equals( start );
-			expect( range ).to.have.property( 'end' ).that.equals( end );
+			expect( range ).to.have.property( 'start' ).that.not.equals( start );
+			expect( range ).to.have.property( 'end' ).that.not.equals( end );
 			expect( range.start.parent ).to.equal( start.parent );
 			expect( range.end.parent ).to.equal( end.parent );
 			expect( range.start.offset ).to.equal( start.offset );

+ 5 - 5
packages/ckeditor5-engine/tests/view/selection.js

@@ -59,7 +59,7 @@ describe( 'Selection', () => {
 			const anchor = selection.anchor;
 
 			expect( anchor.isEqual( range1.start ) ).to.be.true;
-			expect( anchor ).to.equal( range1.start );
+			expect( anchor ).to.not.equal( range1.start );
 		} );
 
 		it( 'should return end of single range in selection when added as backward', () => {
@@ -67,7 +67,7 @@ describe( 'Selection', () => {
 			const anchor = selection.anchor;
 
 			expect( anchor.isEqual( range1.end ) ).to.be.true;
-			expect( anchor ).to.equal( range1.end );
+			expect( anchor ).to.not.equal( range1.end );
 		} );
 
 		it( 'should get anchor from last inserted range', () => {
@@ -95,7 +95,7 @@ describe( 'Selection', () => {
 			const focus = selection.focus;
 
 			expect( focus.isEqual( range1.start ) ).to.be.true;
-			expect( focus ).to.equal( range1.start );
+			expect( focus ).to.not.equal( range1.start );
 		} );
 
 		it( 'should get focus from last inserted range', () => {
@@ -410,7 +410,7 @@ describe( 'Selection', () => {
 			const position = selection.getFirstPosition();
 
 			expect( position.isEqual( range2.start ) ).to.be.true;
-			expect( position ).to.equal( range2.start );
+			expect( position ).to.not.equal( range2.start );
 		} );
 
 		it( 'should return null if no ranges are present', () => {
@@ -427,7 +427,7 @@ describe( 'Selection', () => {
 			const position = selection.getLastPosition();
 
 			expect( position.isEqual( range3.end ) ).to.be.true;
-			expect( position ).to.equal( range3.end );
+			expect( position ).to.not.equal( range3.end );
 		} );
 
 		it( 'should return null if no ranges are present', () => {

+ 13 - 12
packages/ckeditor5-engine/tests/view/writer/remove.js

@@ -15,7 +15,8 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 
 describe( 'writer', () => {
 	/**
-	 * Executes test using `parse` and `stringify` utils functions. Uses range delimiters `[]{}` to create ranges.
+	 * Executes test using `parse` and `stringify` utils functions. Uses range delimiters `[]{}` to create and
+	 * test ranges.
 	 *
 	 * @param {String} input
 	 * @param {String} expectedResult
@@ -26,7 +27,7 @@ describe( 'writer', () => {
 
 		const range = selection.getFirstRange();
 		const removed = remove( range );
-		expect( stringify( view, null, { showType: true, showPriority: true } ) ).to.equal( expectedResult );
+		expect( stringify( view, range, { showType: true, showPriority: true } ) ).to.equal( expectedResult );
 		expect( stringify( removed, null, { showType: true, showPriority: true } ) ).to.equal( expectedRemoved );
 	}
 
@@ -59,21 +60,21 @@ describe( 'writer', () => {
 		} );
 
 		it( 'should remove single text node', () => {
-			test( '<container:p>[foobar]</container:p>', '<container:p></container:p>', 'foobar' );
+			test( '<container:p>[foobar]</container:p>', '<container:p>[]</container:p>', 'foobar' );
 		} );
 
 		it( 'should not leave empty text nodes', () => {
-			test( '<container:p>{foobar}</container:p>', '<container:p></container:p>', 'foobar' );
+			test( '<container:p>{foobar}</container:p>', '<container:p>[]</container:p>', 'foobar' );
 		} );
 
 		it( 'should remove part of the text node', () => {
-			test( '<container:p>f{oob}ar</container:p>', '<container:p>far</container:p>', 'oob' );
+			test( '<container:p>f{oob}ar</container:p>', '<container:p>f{}ar</container:p>', 'oob' );
 		} );
 
 		it( 'should remove parts of nodes #1', () => {
 			test(
 				'<container:p>f{oo<attribute:b view-priority="10">ba}r</attribute:b></container:p>',
-				'<container:p>f<attribute:b view-priority="10">r</attribute:b></container:p>',
+				'<container:p>f[]<attribute:b view-priority="10">r</attribute:b></container:p>',
 				'oo<attribute:b view-priority="10">ba</attribute:b>'
 			);
 		} );
@@ -81,7 +82,7 @@ describe( 'writer', () => {
 		it( 'should support unicode', () => {
 			test(
 				'<container:p>நி{லை<attribute:b view-priority="10">க்}கு</attribute:b></container:p>',
-				'<container:p>நி<attribute:b view-priority="10">கு</attribute:b></container:p>',
+				'<container:p>நி[]<attribute:b view-priority="10">கு</attribute:b></container:p>',
 				'லை<attribute:b view-priority="10">க்</attribute:b>'
 			);
 		} );
@@ -91,7 +92,7 @@ describe( 'writer', () => {
 				'<container:p>' +
 					'<attribute:b view-priority="1">foo</attribute:b>[bar]<attribute:b view-priority="1">bazqux</attribute:b>' +
 				'</container:p>',
-				'<container:p><attribute:b view-priority="1">foobazqux</attribute:b></container:p>',
+				'<container:p><attribute:b view-priority="1">foo{}bazqux</attribute:b></container:p>',
 				'bar'
 			);
 		} );
@@ -101,19 +102,19 @@ describe( 'writer', () => {
 				'<container:p>' +
 					'<attribute:b view-priority="1">fo{o</attribute:b>bar<attribute:b view-priority="1">ba}zqux</attribute:b>' +
 				'</container:p>',
-				'<container:p><attribute:b view-priority="1">fozqux</attribute:b></container:p>',
+				'<container:p><attribute:b view-priority="1">fo{}zqux</attribute:b></container:p>',
 				'<attribute:b view-priority="1">o</attribute:b>bar<attribute:b view-priority="1">ba</attribute:b>'
 			);
 		} );
 
 		it( 'should remove part of the text node in document fragment', () => {
-			test( 'fo{ob}ar', 'foar', 'ob' );
+			test( 'fo{ob}ar', 'fo{}ar', 'ob' );
 		} );
 
 		it( 'should remove EmptyElement', () => {
 			test(
 				'<container:p>foo[<empty:img></empty:img>]bar</container:p>',
-				'<container:p>foobar</container:p>',
+				'<container:p>foo{}bar</container:p>',
 				'<empty:img></empty:img>'
 			);
 		} );
@@ -132,7 +133,7 @@ describe( 'writer', () => {
 		it( 'should remove UIElement', () => {
 			test(
 				'<container:p>foo[<ui:span></ui:span>]bar</container:p>',
-				'<container:p>foobar</container:p>',
+				'<container:p>foo{}bar</container:p>',
 				'<ui:span></ui:span>'
 			);
 		} );