Jelajahi Sumber

Merge pull request #1693 from ckeditor/cf/2435

Feature: `Model#insertContent()` will return a range affected by the insertion.
In `Model#deleteContent()`, added `doNotAutoparagraph` flag to `options`.
`Position` and `LivePosition` static creators should handle `stickiness` param.
Closes #1689. Closes #1688.
Piotr Jasiun 6 tahun lalu
induk
melakukan
ef6697ad84

+ 13 - 0
packages/ckeditor5-engine/src/model/liveposition.js

@@ -88,6 +88,7 @@ export default class LivePosition extends Position {
 	 * @method module:engine/model/liveposition~LivePosition._createAfter
 	 * @see module:engine/model/position~Position._createAfter
 	 * @param {module:engine/model/node~Node} node
+	 * @param {module:engine/model/position~PositionStickiness} [stickiness='toNone']
 	 * @returns {module:engine/model/liveposition~LivePosition}
 	 */
 
@@ -97,6 +98,18 @@ export default class LivePosition extends Position {
 	 * @method module:engine/model/liveposition~LivePosition._createBefore
 	 * @see module:engine/model/position~Position._createBefore
 	 * @param {module:engine/model/node~Node} node
+	 * @param {module:engine/model/position~PositionStickiness} [stickiness='toNone']
+	 * @returns {module:engine/model/liveposition~LivePosition}
+	 */
+
+	/**
+	 * @static
+	 * @protected
+	 * @method module:engine/model/liveposition~LivePosition._createAt
+	 * @see module:engine/model/position~Position._createAt
+	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
+	 * @param {Number|'end'|'before'|'after'} [offset]
+	 * @param {module:engine/model/position~PositionStickiness} [stickiness='toNone']
 	 * @returns {module:engine/model/liveposition~LivePosition}
 	 */
 

+ 16 - 1
packages/ckeditor5-engine/src/model/model.js

@@ -336,9 +336,12 @@ export default class Model {
 	 * The selection into which the content should be inserted. If not provided the current model document selection will be used.
 	 * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] To be used when a model item was passed as `selectable`.
 	 * This param defines a position in relation to that item.
+	 * @returns {module:engine/model/range~Range} Range which contains all the performed changes. This is a range that, if removed,
+	 * would return the model to the state before the insertion. If no changes were preformed by `insertContent`, returns a range collapsed
+	 * at the insertion position.
 	 */
 	insertContent( content, selectable, placeOrOffset ) {
-		insertContent( this, content, selectable, placeOrOffset );
+		return insertContent( this, content, selectable, placeOrOffset );
 	}
 
 	/**
@@ -373,6 +376,18 @@ export default class Model {
 	 *
 	 * * `<paragraph>^</paragraph>` with the option disabled (`doNotResetEntireContent == false`)
 	 * * `<heading1>^</heading1>` with enabled (`doNotResetEntireContent == true`)
+	 *
+	 * @param {Boolean} [options.doNotAutoparagraph=false] Whether to create a paragraph if after content deletion selection is moved
+	 * to a place where text cannot be inserted.
+	 *
+	 * For example `<paragraph>x</paragraph>[<image src="foo.jpg"></image>]` will become:
+	 *
+	 * * `<paragraph>x</paragraph><paragraph>[]</paragraph>` with the option disabled (`doNotAutoparagraph == false`)
+	 * * `<paragraph>x[]</paragraph>` with the option enabled (`doNotAutoparagraph == true`).
+	 *
+	 * **Note:** if there is no valid position for the selection, the paragraph will always be created:
+	 *
+	 * `[<image src="foo.jpg"></image>]` -> `<paragraph>[]</paragraph>`.
 	 */
 	deleteContent( selection, options ) {
 		deleteContent( this, selection, options );

+ 14 - 13
packages/ckeditor5-engine/src/model/position.js

@@ -828,11 +828,13 @@ export default class Position {
 	 * * {@link module:engine/model/position~Position._createAfter}.
 	 *
 	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
-	 * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
+	 * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when the
+	 * first parameter is a {@link module:engine/model/item~Item model item}.
+	 * @param {module:engine/model/position~PositionStickiness} [stickiness='toNone'] Position stickiness. Used only when the
 	 * first parameter is a {@link module:engine/model/item~Item model item}.
 	 * @protected
 	 */
-	static _createAt( itemOrPosition, offset ) {
+	static _createAt( itemOrPosition, offset, stickiness = 'toNone' ) {
 		if ( itemOrPosition instanceof Position ) {
 			return new Position( itemOrPosition.root, itemOrPosition.path, itemOrPosition.stickiness );
 		} else {
@@ -841,9 +843,9 @@ export default class Position {
 			if ( offset == 'end' ) {
 				offset = node.maxOffset;
 			} else if ( offset == 'before' ) {
-				return this._createBefore( node );
+				return this._createBefore( node, stickiness );
 			} else if ( offset == 'after' ) {
-				return this._createAfter( node );
+				return this._createAfter( node, stickiness );
 			} else if ( offset !== 0 && !offset ) {
 				/**
 				 * {@link module:engine/model/model~Model#createPositionAt `Model#createPositionAt()`}
@@ -869,7 +871,7 @@ export default class Position {
 
 			path.push( offset );
 
-			return new this( node.root, path );
+			return new this( node.root, path, stickiness );
 		}
 	}
 
@@ -877,10 +879,11 @@ export default class Position {
 	 * Creates a new position, after given {@link module:engine/model/item~Item model item}.
 	 *
 	 * @param {module:engine/model/item~Item} item Item after which the position should be placed.
+	 * @param {module:engine/model/position~PositionStickiness} [stickiness='toNone'] Position stickiness.
 	 * @returns {module:engine/model/position~Position}
 	 * @protected
 	 */
-	static _createAfter( item ) {
+	static _createAfter( item, stickiness ) {
 		if ( !item.parent ) {
 			/**
 			 * You can not make a position after a root element.
@@ -891,17 +894,18 @@ export default class Position {
 			throw new CKEditorError( 'model-position-after-root: You cannot make a position after root.', { root: item } );
 		}
 
-		return this._createAt( item.parent, item.endOffset );
+		return this._createAt( item.parent, item.endOffset, stickiness );
 	}
 
 	/**
 	 * Creates a new position, before the given {@link module:engine/model/item~Item model item}.
 	 *
 	 * @param {module:engine/model/item~Item} item Item before which the position should be placed.
+	 * @param {module:engine/model/position~PositionStickiness} [stickiness='toNone'] Position stickiness.
 	 * @returns {module:engine/model/position~Position}
 	 * @protected
 	 */
-	static _createBefore( item ) {
+	static _createBefore( item, stickiness ) {
 		if ( !item.parent ) {
 			/**
 			 * You can not make a position before a root element.
@@ -912,7 +916,7 @@ export default class Position {
 			throw new CKEditorError( 'model-position-before-root: You cannot make a position before root.', { root: item } );
 		}
 
-		return this._createAt( item.parent, item.startOffset );
+		return this._createAt( item.parent, item.startOffset, stickiness );
 	}
 
 	/**
@@ -943,10 +947,7 @@ export default class Position {
 			);
 		}
 
-		const pos = new Position( doc.getRoot( json.root ), json.path );
-		pos.stickiness = json.stickiness;
-
-		return pos;
+		return new Position( doc.getRoot( json.root ), json.path, json.stickiness );
 	}
 }
 

+ 35 - 13
packages/ckeditor5-engine/src/model/utils/deletecontent.js

@@ -42,6 +42,18 @@ import DocumentSelection from '../documentselection';
  *
  * * `<paragraph>^</paragraph>` with the option disabled (`doNotResetEntireContent == false`)
  * * `<heading>^</heading>` with enabled (`doNotResetEntireContent == true`).
+ *
+ * @param {Boolean} [options.doNotAutoparagraph=false] Whether to create a paragraph if after content deletion selection is moved
+ * to a place where text cannot be inserted.
+ *
+ * For example `<paragraph>x</paragraph>[<image src="foo.jpg"></image>]` will become:
+ *
+ * * `<paragraph>x</paragraph><paragraph>[]</paragraph>` with the option disabled (`doNotAutoparagraph == false`)
+ * * `<paragraph>x[]</paragraph>` with the option enabled (`doNotAutoparagraph == true`).
+ *
+ * **Note:** if there is no valid position for the selection, the paragraph will always be created:
+ *
+ * `[<image src="foo.jpg"></image>]` -> `<paragraph>[]</paragraph>`.
  */
 export default function deleteContent( model, selection, options = {} ) {
 	if ( selection.isCollapsed ) {
@@ -79,7 +91,7 @@ export default function deleteContent( model, selection, options = {} ) {
 		if ( !options.leaveUnmerged ) {
 			mergeBranches( writer, startPos, endPos );
 
-			// TMP this will be replaced with a postifxer.
+			// TMP this will be replaced with a postfixer.
 			// We need to check and strip disallowed attributes in all nested nodes because after merge
 			// some attributes could end up in a path where are disallowed.
 			//
@@ -88,16 +100,20 @@ export default function deleteContent( model, selection, options = {} ) {
 			schema.removeDisallowedAttributes( startPos.parent.getChildren(), writer );
 		}
 
-		if ( selection instanceof DocumentSelection ) {
-			writer.setSelection( startPos );
-		} else {
-			selection.setTo( startPos );
-		}
+		collapseSelectionAt( writer, selection, startPos );
 
-		// 4. Autoparagraphing.
+		// 4. Add a paragraph to set selection in it.
 		// Check if a text is allowed in the new container. If not, try to create a new paragraph (if it's allowed here).
 		if ( shouldAutoparagraph( schema, startPos ) ) {
-			insertParagraph( writer, startPos, selection );
+			// If auto-paragraphing is off, find the closest valid selection range and collapse the selection there.
+			// If there is no valid selection range, create paragraph anyway and set selection there.
+			const validSelectionRange = schema.getNearestSelectionRange( startPos );
+
+			if ( options.doNotAutoparagraph && validSelectionRange ) {
+				collapseSelectionAt( writer, selection, validSelectionRange );
+			} else {
+				insertParagraph( writer, startPos, selection );
+			}
 		}
 
 		endPos.detach();
@@ -195,11 +211,7 @@ function insertParagraph( writer, position, selection ) {
 
 	writer.insert( paragraph, position );
 
-	if ( selection instanceof DocumentSelection ) {
-		writer.setSelection( paragraph, 0 );
-	} else {
-		selection.setTo( paragraph, 0 );
-	}
+	collapseSelectionAt( writer, selection, writer.createPositionAt( paragraph, 0 ) );
 }
 
 function replaceEntireContentWithParagraph( writer, selection ) {
@@ -228,3 +240,13 @@ function shouldEntireContentBeReplacedWithParagraph( schema, selection ) {
 
 	return schema.checkChild( limitElement, 'paragraph' );
 }
+
+// Helper function that sets the selection. Depending whether given `selection` is a document selection or not,
+// uses a different method to set it.
+function collapseSelectionAt( writer, selection, positionOrRange ) {
+	if ( selection instanceof DocumentSelection ) {
+		writer.setSelection( positionOrRange );
+	} else {
+		selection.setTo( positionOrRange );
+	}
+}

+ 145 - 9
packages/ckeditor5-engine/src/model/utils/insertcontent.js

@@ -22,6 +22,8 @@ import Selection from '../selection';
  * If an instance of {@link module:engine/model/selection~Selection} is passed as `selectable` it will be modified
  * to the insertion selection (equal to a range to be selected after insertion).
  *
+ * If `selectable` is not passed, the content will be inserted using the current selection of the model document.
+ *
  * **Note:** Use {@link module:engine/model/model~Model#insertContent} instead of this function.
  * This function is only exposed to be reusable in algorithms which change the {@link module:engine/model/model~Model#insertContent}
  * method's behavior.
@@ -32,9 +34,12 @@ import Selection from '../selection';
  * @param {module:engine/model/selection~Selectable} [selectable=model.document.selection]
  * Selection into which the content should be inserted.
  * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
+ * @returns {module:engine/model/range~Range} Range which contains all the performed changes. This is a range that, if removed,
+ * would return the model to the state before the insertion. If no changes were preformed by `insertContent`, returns a range collapsed
+ * at the insertion position.
  */
 export default function insertContent( model, content, selectable, placeOrOffset ) {
-	model.change( writer => {
+	return model.change( writer => {
 		let selection;
 
 		if ( !selectable ) {
@@ -45,11 +50,13 @@ export default function insertContent( model, content, selectable, placeOrOffset
 			selection = writer.createSelection( selectable, placeOrOffset );
 		}
 
+		const insertionPosition = selection.anchor.clone();
+
 		if ( !selection.isCollapsed ) {
-			model.deleteContent( selection );
+			model.deleteContent( selection, { doNotAutoparagraph: true } );
 		}
 
-		const insertion = new Insertion( model, writer, selection.anchor );
+		const insertion = new Insertion( model, writer, insertionPosition );
 
 		let nodesToInsert;
 
@@ -86,6 +93,12 @@ export default function insertContent( model, content, selectable, placeOrOffset
 			 */
 			log.warn( 'insertcontent-no-range: Cannot determine a proper selection range after insertion.' );
 		}
+
+		const affectedRange = insertion.getAffectedRange() || model.createRange( insertionPosition );
+
+		insertion.destroy();
+
+		return affectedRange;
 	} );
 }
 
@@ -138,6 +151,22 @@ class Insertion {
 		this.schema = model.schema;
 
 		this._filterAttributesOf = [];
+
+		/**
+		 * Beginning of the affected range. See {@link module:engine/model/utils/insertcontent~Insertion#getAffectedRange}.
+		 *
+		 * @private
+		 * @member {module:engine/model/liveposition~LivePosition|null} #_affectedStart
+		 */
+		this._affectedStart = null;
+
+		/**
+		 * End of the affected range. See {@link module:engine/model/utils/insertcontent~Insertion#getAffectedRange}.
+		 *
+		 * @private
+		 * @member {module:engine/model/liveposition~LivePosition|null} #_affectedEnd
+		 */
+		this._affectedEnd = null;
 	}
 
 	/**
@@ -166,7 +195,7 @@ class Insertion {
 
 	/**
 	 * Returns range to be selected after insertion.
-	 * Returns null if there is no valid range to select after insertion.
+	 * Returns `null` if there is no valid range to select after insertion.
 	 *
 	 * @returns {module:engine/model/range~Range|null}
 	 */
@@ -178,6 +207,33 @@ class Insertion {
 		return this.model.schema.getNearestSelectionRange( this.position );
 	}
 
+	/**
+	 * Returns a range which contains all the performed changes. This is a range that, if removed, would return the model to the state
+	 * before the insertion. Returns `null` if no changes were done.
+	 *
+	 * @returns {module:engine/model/range~Range|null}
+	 */
+	getAffectedRange() {
+		if ( !this._affectedStart ) {
+			return null;
+		}
+
+		return new Range( this._affectedStart, this._affectedEnd );
+	}
+
+	/**
+	 * Destroys `Insertion` instance.
+	 */
+	destroy() {
+		if ( this._affectedStart ) {
+			this._affectedStart.detach();
+		}
+
+		if ( this._affectedEnd ) {
+			this._affectedEnd.detach();
+		}
+	}
+
 	/**
 	 * Handles insertion of a single node.
 	 *
@@ -217,10 +273,10 @@ class Insertion {
 		// E.g.:
 		// <p>x^</p> + <p>y</p> => <p>x</p><p>y</p> => <p>xy[]</p>
 		// and:
-		// <p>x^y</p> + <p>z</p> => <p>x</p>^<p>y</p> + <p>z</p> => <p>x</p><p>y</p><p>z</p> => <p>xy[]z</p>
+		// <p>x^y</p> + <p>z</p> => <p>x</p>^<p>y</p> + <p>z</p> => <p>x</p><p>z</p><p>y</p> => <p>xz[]y</p>
 		// but:
 		// <p>x</p><p>^</p><p>z</p> + <p>y</p> => <p>x</p><p>y</p><p>z</p> (no merging)
-		// <p>x</p>[<img>]<p>z</p> + <p>y</p> => <p>x</p><p>y</p><p>z</p> (no merging, note: after running deletetContents
+		// <p>x</p>[<img>]<p>z</p> + <p>y</p> => <p>x</p><p>y</p><p>z</p> (no merging, note: after running deleteContents
 		//																	 it's exactly the same case as above)
 		this._mergeSiblingsOf( node, context );
 	}
@@ -276,6 +332,7 @@ class Insertion {
 
 		const livePos = LivePosition.fromPosition( this.position, 'toNext' );
 
+		this._setAffectedBoundaries( this.position );
 		this.writer.insert( node, this.position );
 
 		this.position = livePos.toPosition();
@@ -291,6 +348,37 @@ class Insertion {
 		this._filterAttributesOf.push( node );
 	}
 
+	/**
+	 * Sets `_affectedStart` and `_affectedEnd` to the given `position`. Should be used before a change is done during insertion process to
+	 * mark the affected range.
+	 *
+	 * This method is used before inserting a node or splitting a parent node. `_affectedStart` and `_affectedEnd` are also changed
+	 * during merging, but the logic there is more complicated so it is left out of this function.
+	 *
+	 * @private
+	 * @param {module:engine/model/position~Position} position
+	 */
+	_setAffectedBoundaries( position ) {
+		// Set affected boundaries stickiness so that those position will "expand" when something is inserted in between them:
+		// <paragraph>Foo][bar</paragraph> -> <paragraph>Foo]xx[bar</paragraph>
+		// This is why it cannot be a range but two separate positions.
+		if ( !this._affectedStart ) {
+			this._affectedStart = LivePosition.fromPosition( position, 'toPrevious' );
+		}
+
+		// If `_affectedEnd` is before the new boundary position, expand `_affectedEnd`. This can happen if first inserted node was
+		// inserted into the parent but the next node is moved-out of that parent:
+		// (1) <paragraph>Foo][</paragraph> -> <paragraph>Foo]xx[</paragraph>
+		// (2) <paragraph>Foo]xx[</paragraph> -> <paragraph>Foo]xx</paragraph><widget></widget>[
+		if ( !this._affectedEnd || this._affectedEnd.isBefore( position ) ) {
+			if ( this._affectedEnd ) {
+				this._affectedEnd.detach();
+			}
+
+			this._affectedEnd = LivePosition.fromPosition( position, 'toNext' );
+		}
+	}
+
 	/**
 	 * @private
 	 * @param {module:engine/model/node~Node} node The node which could potentially be merged.
@@ -312,8 +400,38 @@ class Insertion {
 			const livePosition = LivePosition.fromPosition( this.position );
 			livePosition.stickiness = 'toNext';
 
+			// If `_affectedStart` is sames as merge position, it means that the element "marked" by `_affectedStart` is going to be
+			// removed and its contents will be moved. This won't transform `LivePosition` so `_affectedStart` needs to be moved
+			// by hand to properly reflect affected range. (Due to `_affectedStart` and `_affectedEnd` stickiness, the "range" is
+			// shown as `][`).
+			//
+			// Example - insert `<paragraph>Abc</paragraph><paragraph>Xyz</paragraph>` at the end of `<paragraph>Foo^</paragraph>`:
+			//
+			// <paragraph>Foo</paragraph><paragraph>Bar</paragraph>   -->
+			// <paragraph>Foo</paragraph>]<paragraph>Abc</paragraph><paragraph>Xyz</paragraph>[<paragraph>Bar</paragraph>   -->
+			// <paragraph>Foo]Abc</paragraph><paragraph>Xyz</paragraph>[<paragraph>Bar</paragraph>
+			//
+			// Note, that if we are here then something must have been inserted, so `_affectedStart` and `_affectedEnd` have to be set.
+			if ( this._affectedStart.isEqual( mergePosLeft ) ) {
+				this._affectedStart.detach();
+				this._affectedStart = LivePosition._createAt( mergePosLeft.nodeBefore, 'end', 'toPrevious' );
+			}
+
 			this.writer.merge( mergePosLeft );
 
+			// If only one element (the merged one) is in the "affected range", also move the affected range end appropriately.
+			//
+			// Example - insert `<paragraph>Abc</paragraph>` at the of `<paragraph>Foo^</paragraph>`:
+			//
+			// <paragraph>Foo</paragraph><paragraph>Bar</paragraph>   -->
+			// <paragraph>Foo</paragraph>]<paragraph>Abc</paragraph>[<paragraph>Bar</paragraph>   -->
+			// <paragraph>Foo]Abc</paragraph>[<paragraph>Bar</paragraph>   -->
+			// <paragraph>Foo]Abc[</paragraph><paragraph>Bar</paragraph>
+			if ( mergePosLeft.isEqual( this._affectedEnd ) && context.isLast ) {
+				this._affectedEnd.detach();
+				this._affectedEnd = LivePosition._createAt( mergePosLeft.nodeBefore, 'end', 'toNext' );
+			}
+
 			this.position = livePosition.toPosition();
 			livePosition.detach();
 		}
@@ -333,10 +451,22 @@ class Insertion {
 
 			// OK:  <p>xx[]</p> + <p>yy</p> => <p>xx[]yy</p> (when sticks to previous)
 			// NOK: <p>xx[]</p> + <p>yy</p> => <p>xxyy[]</p> (when sticks to next)
-			const livePosition = new LivePosition( this.position.root, this.position.path, 'toPrevious' );
+			const livePosition = LivePosition.fromPosition( this.position, 'toPrevious' );
+
+			// See comment above on moving `_affectedStart`.
+			if ( this._affectedEnd.isEqual( mergePosRight ) ) {
+				this._affectedEnd.detach();
+				this._affectedEnd = LivePosition._createAt( mergePosRight.nodeBefore, 'end', 'toNext' );
+			}
 
 			this.writer.merge( mergePosRight );
 
+			// See comment above on moving `_affectedStart`.
+			if ( mergePosRight.getShiftedBy( -1 ).isEqual( this._affectedStart ) && context.isFirst ) {
+				this._affectedStart.detach();
+				this._affectedStart = LivePosition._createAt( mergePosRight.nodeBefore, 0, 'toPrevious' );
+			}
+
 			this.position = livePosition.toPosition();
 			livePosition.detach();
 		}
@@ -424,19 +554,25 @@ class Insertion {
 			}
 
 			if ( this.position.isAtStart ) {
+				// If insertion position is at the beginning of the parent, move it out instead of splitting.
+				// <p>^Foo</p> -> ^<p>Foo</p>
 				const parent = this.position.parent;
+
 				this.position = this.writer.createPositionBefore( parent );
 
-				// Special case – parent is empty (<p>^</p>) so isAtStart == isAtEnd == true.
-				// We can remove the element after moving selection out of it.
+				// Special case – parent is empty (<p>^</p>).
+				// We can remove the element after moving insertion position out of it.
 				if ( parent.isEmpty ) {
 					this.writer.remove( parent );
 				}
 			} else if ( this.position.isAtEnd ) {
+				// If insertion position is at the end of the parent, move it out instead of splitting.
+				// <p>Foo^</p> -> <p>Foo</p>^
 				this.position = this.writer.createPositionAfter( this.position.parent );
 			} else {
 				const tempPos = this.writer.createPositionAfter( this.position.parent );
 
+				this._setAffectedBoundaries( this.position );
 				this.writer.split( this.position );
 
 				this.position = tempPos;

+ 11 - 2
packages/ckeditor5-engine/tests/model/liveposition.js

@@ -78,14 +78,23 @@ describe( 'LivePosition', () =>
 	} );
 
 	it( '_createBefore should return LivePosition', () => {
-		const position = LivePosition._createBefore( ul );
+		const position = LivePosition._createBefore( ul, 'toPrevious' );
 		expect( position ).to.be.instanceof( LivePosition );
+		expect( position.stickiness ).to.equal( 'toPrevious' );
 		position.detach();
 	} );
 
 	it( '_createAfter should return LivePosition', () => {
-		const position = LivePosition._createAfter( ul );
+		const position = LivePosition._createAfter( ul, 'toPrevious' );
 		expect( position ).to.be.instanceof( LivePosition );
+		expect( position.stickiness ).to.equal( 'toPrevious' );
+		position.detach();
+	} );
+
+	it( '_createAt should return LivePosition', () => {
+		const position = LivePosition._createAt( ul, 'end', 'toPrevious' );
+		expect( position ).to.be.instanceof( LivePosition );
+		expect( position.stickiness ).to.equal( 'toPrevious' );
 		position.detach();
 	} );
 

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

@@ -182,6 +182,10 @@ describe( 'Position', () => {
 				expect( Position._createAt( ul, 'end' ) ).to.have.property( 'path' ).that.deep.equals( [ 1, 2 ] );
 			} );
 
+			it( 'should set stickiness (if not cloning other position)', () => {
+				expect( Position._createAt( root, 'end', 'toPrevious' ) ).to.have.property( 'stickiness' ).that.equals( 'toPrevious' );
+			} );
+
 			it( 'throws when parent is not an element', () => {
 				expect( () => {
 					Position._createAt( b, 0 );
@@ -214,6 +218,10 @@ describe( 'Position', () => {
 				expect( Position._createBefore( r ) ).to.have.property( 'path' ).that.deep.equals( [ 1, 1, 2 ] );
 			} );
 
+			it( 'should set stickiness', () => {
+				expect( Position._createBefore( p, 'toPrevious' ) ).to.have.property( 'stickiness' ).that.equals( 'toPrevious' );
+			} );
+
 			it( 'should throw error if one try to create positions before root', () => {
 				expect( () => {
 					Position._createBefore( root );
@@ -240,6 +248,10 @@ describe( 'Position', () => {
 				expect( Position._createAfter( r ) ).to.have.property( 'path' ).that.deep.equals( [ 1, 1, 3 ] );
 			} );
 
+			it( 'should set stickiness', () => {
+				expect( Position._createAfter( p, 'toPrevious' ) ).to.have.property( 'stickiness' ).that.equals( 'toPrevious' );
+			} );
+
 			it( 'should throw error if one try to make positions after root', () => {
 				expect( () => {
 					Position._createAfter( root );

+ 27 - 13
packages/ckeditor5-engine/tests/model/utils/deletecontent.js

@@ -592,19 +592,6 @@ describe( 'DataController utils', () => {
 					.to.equal( 'x[]z' );
 			} );
 
-			it( 'creates a paragraph when text is not allowed (paragraph selected)', () => {
-				setData(
-					model,
-					'<paragraph>x</paragraph>[<paragraph>yyy</paragraph>]<paragraph>z</paragraph>',
-					{ rootName: 'bodyRoot' }
-				);
-
-				deleteContent( model, doc.selection );
-
-				expect( getData( model, { rootName: 'bodyRoot' } ) )
-					.to.equal( '<paragraph>x</paragraph><paragraph>[]</paragraph><paragraph>z</paragraph>' );
-			} );
-
 			it( 'creates a paragraph when text is not allowed (custom selection)', () => {
 				setData(
 					model,
@@ -614,6 +601,7 @@ describe( 'DataController utils', () => {
 
 				const root = doc.getRoot( 'bodyRoot' );
 
+				// [<paragraph>yyy</paragraph>]
 				const selection = new Selection( [
 					new Range( new Position( root, [ 1 ] ), new Position( root, [ 2 ] ) )
 				] );
@@ -700,6 +688,32 @@ describe( 'DataController utils', () => {
 				expect( getData( model, { rootName: 'restrictedRoot' } ) )
 					.to.equal( '<blockWidget></blockWidget>[]<blockWidget></blockWidget>' );
 			} );
+
+			it( 'does not create a paragraph when doNotAutoparagraph option is set to true', () => {
+				setData(
+					model,
+					'<paragraph>x</paragraph>[<blockWidget></blockWidget>]<paragraph>z</paragraph>',
+					{ rootName: 'bodyRoot' }
+				);
+
+				deleteContent( model, doc.selection, { doNotAutoparagraph: true } );
+
+				expect( getData( model, { rootName: 'bodyRoot' } ) )
+					.to.equal( '<paragraph>x[]</paragraph><paragraph>z</paragraph>' );
+			} );
+
+			it( 'creates paragraph when after deletion there is no valid selection range', () => {
+				setData(
+					model,
+					'[<blockWidget></blockWidget>]',
+					{ rootName: 'bodyRoot' }
+				);
+
+				deleteContent( model, doc.selection, { doNotAutoparagraph: true } );
+
+				expect( getData( model, { rootName: 'bodyRoot' } ) )
+					.to.equal( '<paragraph>[]</paragraph>' );
+			} );
 		} );
 
 		describe( 'integration with inline limit elements', () => {

File diff ditekan karena terlalu besar
+ 395 - 105
packages/ckeditor5-engine/tests/model/utils/insertcontent.js


Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini