Procházet zdrojové kódy

Merge pull request #1574 from ckeditor/t/1554

Feature: The `model.insertContent()` method will accept offset parameter. Made `Position.createAt()` require the `offset` when the first parameter is an model/view item. See breaking changes. Closes #1554.

BREAKING CHANGE: The `offset` parameter of the following methods does not default to `0` and hence is no longer optional when `itemOrPosition` is a model/view item:

* `model.Position.createAt()`
* `model.Range.createCollapsedAt()`
* `model.Selection#setFocus()`
* `model.Writer#insert()`
* `model.Writer#insertText()`
* `model.Writer#insertElement()`
* `model.Writer#move()`
* `model.Writer#setSelectionFocus()`
* `view.Writer#setSelectionFocus()`
* `view.Position.createAt()`
* `view.Range.createCollapsedAt()`
* `view.Selection#setFocus()`
Piotrek Koszuliński před 7 roky
rodič
revize
b29296ad47
31 změnil soubory, kde provedl 181 přidání a 115 odebrání
  1. 2 2
      packages/ckeditor5-engine/src/controller/datacontroller.js
  2. 2 2
      packages/ckeditor5-engine/src/conversion/upcast-converters.js
  3. 2 2
      packages/ckeditor5-engine/src/conversion/upcastdispatcher.js
  4. 1 1
      packages/ckeditor5-engine/src/dev-utils/model.js
  5. 8 3
      packages/ckeditor5-engine/src/model/model.js
  6. 11 3
      packages/ckeditor5-engine/src/model/position.js
  7. 1 1
      packages/ckeditor5-engine/src/model/range.js
  8. 1 1
      packages/ckeditor5-engine/src/model/schema.js
  9. 3 3
      packages/ckeditor5-engine/src/model/selection.js
  10. 1 1
      packages/ckeditor5-engine/src/model/utils/deletecontent.js
  11. 1 1
      packages/ckeditor5-engine/src/model/utils/getselectedcontent.js
  12. 3 2
      packages/ckeditor5-engine/src/model/utils/insertcontent.js
  13. 5 2
      packages/ckeditor5-engine/src/model/utils/selection-post-fixer.js
  14. 7 7
      packages/ckeditor5-engine/src/model/writer.js
  15. 1 1
      packages/ckeditor5-engine/src/view/documentselection.js
  16. 2 2
      packages/ckeditor5-engine/src/view/downcastwriter.js
  17. 12 4
      packages/ckeditor5-engine/src/view/position.js
  18. 1 1
      packages/ckeditor5-engine/src/view/range.js
  19. 1 1
      packages/ckeditor5-engine/src/view/selection.js
  20. 2 2
      packages/ckeditor5-engine/tests/conversion/upcast-converters.js
  21. 4 4
      packages/ckeditor5-engine/tests/conversion/upcastdispatcher.js
  22. 7 3
      packages/ckeditor5-engine/tests/model/position.js
  23. 1 1
      packages/ckeditor5-engine/tests/model/range.js
  24. 14 14
      packages/ckeditor5-engine/tests/model/schema.js
  25. 68 37
      packages/ckeditor5-engine/tests/model/utils/insertcontent.js
  26. 2 2
      packages/ckeditor5-engine/tests/model/writer.js
  27. 2 2
      packages/ckeditor5-engine/tests/tickets/1281.js
  28. 3 3
      packages/ckeditor5-engine/tests/view/downcastwriter/writer.js
  29. 10 4
      packages/ckeditor5-engine/tests/view/position.js
  30. 1 1
      packages/ckeditor5-engine/tests/view/range.js
  31. 2 2
      packages/ckeditor5-engine/tests/view/view/view.js

+ 2 - 2
packages/ckeditor5-engine/src/controller/datacontroller.js

@@ -201,7 +201,7 @@ export default class DataController {
 		const modelRoot = this.model.document.getRoot( rootName );
 
 		this.model.enqueueChange( 'transparent', writer => {
-			writer.insert( this.parse( data, modelRoot ), modelRoot );
+			writer.insert( this.parse( data, modelRoot ), modelRoot, 0 );
 		} );
 
 		return Promise.resolve();
@@ -228,7 +228,7 @@ export default class DataController {
 			writer.removeSelectionAttribute( this.model.document.selection.getAttributeKeys() );
 
 			writer.remove( ModelRange.createIn( modelRoot ) );
-			writer.insert( this.parse( data, modelRoot ), modelRoot );
+			writer.insert( this.parse( data, modelRoot ), modelRoot, 0 );
 		} );
 	}
 

+ 2 - 2
packages/ckeditor5-engine/src/conversion/upcast-converters.js

@@ -358,7 +358,7 @@ function _prepareToElementConverter( config ) {
 		conversionApi.writer.insert( modelElement, splitResult.position );
 
 		// Convert children and insert to element.
-		const childrenResult = conversionApi.convertChildren( data.viewItem, ModelPosition.createAt( modelElement ) );
+		const childrenResult = conversionApi.convertChildren( data.viewItem, ModelPosition.createAt( modelElement, 0 ) );
 
 		// Consume appropriate value from consumable values list.
 		conversionApi.consumable.consume( data.viewItem, match.match );
@@ -380,7 +380,7 @@ function _prepareToElementConverter( config ) {
 		// before: <allowed><notAllowed>[]</notAllowed></allowed>
 		// after:  <allowed><notAllowed></notAllowed><converted></converted><notAllowed>[]</notAllowed></allowed>
 		if ( splitResult.cursorParent ) {
-			data.modelCursor = ModelPosition.createAt( splitResult.cursorParent );
+			data.modelCursor = ModelPosition.createAt( splitResult.cursorParent, 0 );
 
 			// Otherwise just continue after inserted element.
 		} else {

+ 2 - 2
packages/ckeditor5-engine/src/conversion/upcastdispatcher.js

@@ -71,7 +71,7 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix';
  *  	 				conversionApi.writer.insert( paragraph, splitResult.position );
  *
  *  	 				// Convert children to paragraph.
- *  	 				const { modelRange } = conversionApi.convertChildren( data.viewItem, Position.createAt( paragraph ) );
+ *  	 				const { modelRange } = conversionApi.convertChildren( data.viewItem, Position.createAt( paragraph, 0 ) );
  *
  * 						// Set as conversion result, attribute converters may use this property.
  *  	 				data.modelRange = new Range( Position.createBefore( paragraph ), modelRange.end );
@@ -419,7 +419,7 @@ function createContextTree( contextDefinition, writer ) {
 			writer.append( current, position );
 		}
 
-		position = ModelPosition.createAt( current );
+		position = ModelPosition.createAt( current, 0 );
 	}
 
 	return position;

+ 1 - 1
packages/ckeditor5-engine/src/dev-utils/model.js

@@ -391,7 +391,7 @@ function convertToModelElement() {
 
 		conversionApi.mapper.bindElements( element, data.viewItem );
 
-		conversionApi.convertChildren( data.viewItem, ModelPosition.createAt( element ) );
+		conversionApi.convertChildren( data.viewItem, ModelPosition.createAt( element, 0 ) );
 
 		data.modelRange = ModelRange.createOn( element );
 		data.modelCursor = data.modelRange.end;

+ 8 - 3
packages/ckeditor5-engine/src/model/model.js

@@ -307,6 +307,9 @@ export default class Model {
 	 *
 	 *		// Insert text at given position - document selection will not be modified.
 	 *		editor.model.change( writer => {
+	 *			editor.model.insertContent( writer.createText( 'x' ), doc.getRoot(), 2 );
+	 *
+	 *			// Which is a shorthand for:
 	 *			editor.model.insertContent( writer.createText( 'x' ), Position.createAt( doc.getRoot(), 2 ) );
 	 *		} );
 	 *
@@ -327,12 +330,14 @@ export default class Model {
 	 * @fires insertContent
 	 * @param {module:engine/model/documentfragment~DocumentFragment|module:engine/model/item~Item} content The content to insert.
 	 * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection|
-	 * module:engine/model/position~Position|module:engine/model/element~Element|
+	 * module:engine/model/position~Position|module:engine/model/item~Item|
 	 * Iterable.<module:engine/model/range~Range>|module:engine/model/range~Range|null} [selectable=model.document.selection]
 	 * 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.
 	 */
-	insertContent( content, selectable ) {
-		insertContent( this, content, selectable );
+	insertContent( content, selectable, placeOrOffset ) {
+		insertContent( this, content, selectable, placeOrOffset );
 	}
 
 	/**

+ 11 - 3
packages/ckeditor5-engine/src/model/position.js

@@ -816,7 +816,7 @@ export default class Position {
 	 * * {@link module:engine/model/position~Position.createFromPosition}.
 	 *
 	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
-	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
+	 * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
 	 * first parameter is a {@link module:engine/model/item~Item model item}.
 	 */
 	static createAt( itemOrPosition, offset ) {
@@ -831,8 +831,16 @@ export default class Position {
 				return this.createBefore( node );
 			} else if ( offset == 'after' ) {
 				return this.createAfter( node );
-			} else if ( !offset ) {
-				offset = 0;
+			} else if ( offset !== 0 && !offset ) {
+				/**
+				 * {@link module:engine/model/position~Position.createAt `Position.createAt()`}
+				 * requires the offset to be specified when the first parameter is a model item.
+				 *
+				 * @error model-position-createAt-offset-required
+				 */
+				throw new CKEditorError(
+					'model-position-createAt-offset-required: ' +
+					'Position.createAt() requires the offset when the first parameter is a model item.' );
 			}
 
 			return this.createFromParentAndOffset( node, offset );

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

@@ -835,7 +835,7 @@ export default class Range {
 	 * or on the given {@link module:engine/model/item~Item item}.
 	 *
 	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
-	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
+	 * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
 	 * first parameter is a {@link module:engine/model/item~Item model item}.
 	 */
 	static createCollapsedAt( itemOrPosition, offset ) {

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

@@ -1162,7 +1162,7 @@ export class SchemaContext {
  *		schema.checkChild( contextDefinition, childToCheck );
  *
  *		// Also check in [ rootElement, blockQuoteElement, paragraphElement ].
- *		schema.checkChild( Position.createAt( paragraphElement ), 'foo' );
+ *		schema.checkChild( Position.createAt( paragraphElement, 0 ), 'foo' );
  *
  *		// Check in [ rootElement, paragraphElement ].
  *		schema.checkChild( [ rootElement, paragraphElement ], 'foo' );

+ 3 - 3
packages/ckeditor5-engine/src/model/selection.js

@@ -485,7 +485,7 @@ export default class Selection {
 	 *
 	 * @fires change:range
 	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
-	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
+	 * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
 	 * first parameter is a {@link module:engine/model/item~Item model item}.
 	 */
 	setFocus( itemOrPosition, offset ) {
@@ -665,7 +665,7 @@ export default class Selection {
 			const endBlock = getParentBlock( range.end, visited );
 
 			// #984. Don't return the end block if the range ends right at its beginning.
-			if ( endBlock && !range.end.isTouching( Position.createAt( endBlock ) ) ) {
+			if ( endBlock && !range.end.isTouching( Position.createAt( endBlock, 0 ) ) ) {
 				yield endBlock;
 			}
 		}
@@ -683,7 +683,7 @@ export default class Selection {
 	 * @returns {Boolean}
 	 */
 	containsEntireContent( element = this.anchor.root ) {
-		const limitStartPosition = Position.createAt( element );
+		const limitStartPosition = Position.createAt( element, 0 );
 		const limitEndPosition = Position.createAt( element, 'end' );
 
 		return limitStartPosition.isTouching( this.getFirstPosition() ) &&

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

@@ -208,7 +208,7 @@ function replaceEntireContentWithParagraph( writer, selection ) {
 	const limitElement = writer.model.schema.getLimitElement( selection );
 
 	writer.remove( Range.createIn( limitElement ) );
-	insertParagraph( writer, Position.createAt( limitElement ), selection );
+	insertParagraph( writer, Position.createAt( limitElement, 0 ), selection );
 }
 
 // We want to replace the entire content with a paragraph when:

+ 1 - 1
packages/ckeditor5-engine/src/model/utils/getselectedcontent.js

@@ -99,7 +99,7 @@ export default function getSelectedContent( model, selection ) {
 			// Find the position of the original range in the cloned fragment.
 			const newRange = range._getTransformedByMove( flatSubtreeRange.start, Position.createAt( frag, 0 ), howMany )[ 0 ];
 
-			const leftExcessRange = new Range( Position.createAt( frag ), newRange.start );
+			const leftExcessRange = new Range( Position.createAt( frag, 0 ), newRange.start );
 			const rightExcessRange = new Range( newRange.end, Position.createAt( frag, 'end' ) );
 
 			removeRangeContent( rightExcessRange, writer );

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

@@ -33,8 +33,9 @@ import Selection from '../selection';
  * module:engine/model/position~Position|module:engine/model/element~Element|
  * Iterable.<module:engine/model/range~Range>|module:engine/model/range~Range|null} [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.
  */
-export default function insertContent( model, content, selectable ) {
+export default function insertContent( model, content, selectable, placeOrOffset ) {
 	model.change( writer => {
 		let selection;
 
@@ -43,7 +44,7 @@ export default function insertContent( model, content, selectable ) {
 		} else if ( selectable instanceof Selection || selectable instanceof DocumentSelection ) {
 			selection = selectable;
 		} else {
-			selection = new Selection( selectable );
+			selection = new Selection( selectable, placeOrOffset );
 		}
 
 		if ( !selection.isCollapsed ) {

+ 5 - 2
packages/ckeditor5-engine/src/model/utils/selection-post-fixer.js

@@ -199,8 +199,11 @@ function tryFixingNonCollapsedRage( range, schema ) {
 	if ( isStartInLimit || isEndInLimit ) {
 		// Although we've already found limit element on start/end positions we must find the outer-most limit element.
 		// as limit elements might be nested directly inside (ie table > tableRow > tableCell).
-		const fixedStart = isStartInLimit ? expandSelectionOnIsLimitNode( Position.createAt( startLimitElement ), schema, 'start' ) : start;
-		const fixedEnd = isEndInLimit ? expandSelectionOnIsLimitNode( Position.createAt( endLimitElement ), schema, 'end' ) : end;
+		const startPosition = Position.createAt( startLimitElement, 0 );
+		const endPosition = Position.createAt( endLimitElement, 0 );
+
+		const fixedStart = isStartInLimit ? expandSelectionOnIsLimitNode( startPosition, schema, 'start' ) : start;
+		const fixedEnd = isEndInLimit ? expandSelectionOnIsLimitNode( endPosition, schema, 'end' ) : end;
 
 		return new Range( fixedStart, fixedEnd );
 	}

+ 7 - 7
packages/ckeditor5-engine/src/model/writer.js

@@ -153,10 +153,10 @@ export default class Writer {
 	 * @param {module:engine/model/item~Item|module:engine/model/documentfragment~DocumentFragment} item Item or document
 	 * fragment to insert.
 	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
-	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
+	 * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
 	 * second parameter is a {@link module:engine/model/item~Item model item}.
 	 */
-	insert( item, itemOrPosition, offset ) {
+	insert( item, itemOrPosition, offset = 0 ) {
 		this._assertWriterUsedCorrectly();
 
 		const position = Position.createAt( itemOrPosition, offset );
@@ -198,7 +198,7 @@ export default class Writer {
 		if ( item instanceof DocumentFragment ) {
 			for ( const [ markerName, markerRange ] of item.markers ) {
 				// We need to migrate marker range from DocumentFragment to Document.
-				const rangeRootPosition = Position.createAt( markerRange.root );
+				const rangeRootPosition = Position.createAt( markerRange.root, 0 );
 				const range = new Range(
 					markerRange.start._getCombined( rangeRootPosition, position ),
 					markerRange.end._getCombined( rangeRootPosition, position )
@@ -230,7 +230,7 @@ export default class Writer {
 	 * @param {String} data Text data.
 	 * @param {Object} [attributes] Text attributes.
 	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
-	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
+	 * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
 	 * third parameter is a {@link module:engine/model/item~Item model item}.
 	 */
 	insertText( text, attributes, itemOrPosition, offset ) {
@@ -262,7 +262,7 @@ export default class Writer {
 	 * @param {String} name Name of the element.
 	 * @param {Object} [attributes] Elements attributes.
 	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
-	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
+	 * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
 	 * third parameter is a {@link module:engine/model/item~Item model item}.
 	 */
 	insertElement( name, attributes, itemOrPosition, offset ) {
@@ -440,7 +440,7 @@ export default class Writer {
 	 *
 	 * @param {module:engine/model/range~Range} range Source range.
 	 * @param {module:engine/model/item~Item|module:engine/model/position~Position} itemOrPosition
-	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
+	 * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
 	 * second parameter is a {@link module:engine/model/item~Item model item}.
 	 */
 	move( range, itemOrPosition, offset ) {
@@ -668,7 +668,7 @@ export default class Writer {
 
 		return {
 			position,
-			range: new Range( Position.createAt( firstSplitElement, 'end' ), Position.createAt( firstCopyElement ) )
+			range: new Range( Position.createAt( firstSplitElement, 'end' ), Position.createAt( firstCopyElement, 0 ) )
 		};
 	}
 

+ 1 - 1
packages/ckeditor5-engine/src/view/documentselection.js

@@ -357,7 +357,7 @@ export default class DocumentSelection {
 	 * @protected
 	 * @fires change
 	 * @param {module:engine/view/item~Item|module:engine/view/position~Position} itemOrPosition
-	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
+	 * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
 	 * first parameter is a {@link module:engine/view/item~Item view item}.
 	 */
 	_setFocus( itemOrPosition, offset ) {

+ 2 - 2
packages/ckeditor5-engine/src/view/downcastwriter.js

@@ -123,7 +123,7 @@ export default class DowncastWriter {
 	 * The location can be specified in the same form as {@link module:engine/view/position~Position.createAt} parameters.
 	 *
 	 * @param {module:engine/view/item~Item|module:engine/view/position~Position} itemOrPosition
-	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
+	 * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
 	 * first parameter is a {@link module:engine/view/item~Item view item}.
 	 */
 	setSelectionFocus( itemOrPosition, offset ) {
@@ -915,7 +915,7 @@ export default class DowncastWriter {
 		const newElement = new ContainerElement( newName, viewElement.getAttributes() );
 
 		this.insert( Position.createAfter( viewElement ), newElement );
-		this.move( Range.createIn( viewElement ), Position.createAt( newElement ) );
+		this.move( Range.createIn( viewElement ), Position.createAt( newElement, 0 ) );
 		this.remove( Range.createOn( viewElement ) );
 
 		return newElement;

+ 12 - 4
packages/ckeditor5-engine/src/view/position.js

@@ -288,8 +288,8 @@ export default class Position {
 	 * * {@link module:engine/view/position~Position.createAfter},
 	 * * {@link module:engine/view/position~Position.createFromPosition}.
 	 *
-	 * @param {module:engine/view/item~Item|module:engine/model/position~Position} itemOrPosition
-	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
+	 * @param {module:engine/view/item~Item|module:engine/view/position~Position} itemOrPosition
+	 * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
 	 * first parameter is a {@link module:engine/view/item~Item view item}.
 	 */
 	static createAt( itemOrPosition, offset ) {
@@ -304,8 +304,16 @@ export default class Position {
 				return this.createBefore( node );
 			} else if ( offset == 'after' ) {
 				return this.createAfter( node );
-			} else if ( !offset ) {
-				offset = 0;
+			} else if ( offset !== 0 && !offset ) {
+				/**
+				 * {@link module:engine/view/position~Position.createAt `Position.createAt()`}
+				 * requires the offset to be specified when the first parameter is a view item.
+				 *
+				 * @error view-position-createAt-offset-required
+				 */
+				throw new CKEditorError(
+					'view-position-createAt-offset-required: ' +
+					'Position.createAt() requires the offset when the first parameter is a view item.' );
 			}
 
 			return new Position( node, offset );

+ 1 - 1
packages/ckeditor5-engine/src/view/range.js

@@ -454,7 +454,7 @@ export default class Range {
 	 * or on the given {@link module:engine/view/item~Item item}.
 	 *
 	 * @param {module:engine/view/item~Item|module:engine/view/position~Position} itemOrPosition
-	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
+	 * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
 	 * first parameter is a {@link module:engine/view/item~Item view item}.
 	 */
 	static createCollapsedAt( itemOrPosition, offset ) {

+ 1 - 1
packages/ckeditor5-engine/src/view/selection.js

@@ -548,7 +548,7 @@ export default class Selection {
 	 *
 	 * @fires change
 	 * @param {module:engine/view/item~Item|module:engine/view/position~Position} itemOrPosition
-	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
+	 * @param {Number|'end'|'before'|'after'} [offset] Offset or one of the flags. Used only when
 	 * first parameter is a {@link module:engine/view/item~Item view item}.
 	 */
 	setFocus( itemOrPosition, offset ) {

+ 2 - 2
packages/ckeditor5-engine/tests/conversion/upcast-converters.js

@@ -841,7 +841,7 @@ describe( 'upcast-converters', () => {
 					const paragraph = conversionApi.writer.createElement( 'paragraph' );
 
 					conversionApi.writer.insert( paragraph, data.modelCursor );
-					conversionApi.convertChildren( data.viewItem, ModelPosition.createAt( paragraph ) );
+					conversionApi.convertChildren( data.viewItem, ModelPosition.createAt( paragraph, 0 ) );
 
 					data.modelRange = ModelRange.createOn( paragraph );
 					data.modelCursor = data.modelRange.end;
@@ -863,7 +863,7 @@ describe( 'upcast-converters', () => {
 				new ViewContainerElement( 'div', null, [ new ViewText( 'abc' ), new ViewContainerElement( 'foo' ) ] ),
 				new ViewContainerElement( 'bar' )
 			] );
-			const position = ModelPosition.createAt( new ModelElement( 'element' ) );
+			const position = ModelPosition.createAt( new ModelElement( 'element' ), 0 );
 
 			dispatcher.on( 'documentFragment', convertToModelFragment() );
 			dispatcher.on( 'element', convertToModelFragment(), { priority: 'lowest' } );

+ 4 - 4
packages/ckeditor5-engine/tests/conversion/upcastdispatcher.js

@@ -501,7 +501,7 @@ describe( 'UpcastDispatcher', () => {
 				dispatcher.on( 'documentFragment', ( evt, data, conversionApi ) => {
 					spy();
 
-					const result = conversionApi.convertChildren( data.viewItem, ModelPosition.createAt( rootMock ) );
+					const result = conversionApi.convertChildren( data.viewItem, ModelPosition.createAt( rootMock, 0 ) );
 
 					expect( result.modelRange ).to.be.instanceof( ModelRange );
 					expect( result.modelRange.start.path ).to.deep.equal( [ 0 ] );
@@ -540,7 +540,7 @@ describe( 'UpcastDispatcher', () => {
 				dispatcher.on( 'documentFragment', ( evt, data, conversionApi ) => {
 					const paragraph = conversionApi.writer.createElement( 'paragraph' );
 					const span = conversionApi.writer.createElement( 'span' );
-					const position = ModelPosition.createAt( paragraph );
+					const position = ModelPosition.createAt( paragraph, 0 );
 
 					const result = conversionApi.splitToAllowedParent( span, position );
 
@@ -572,7 +572,7 @@ describe( 'UpcastDispatcher', () => {
 					conversionApi.writer.insert( paragraph, section );
 					conversionApi.writer.insert( span, paragraph );
 
-					const position = ModelPosition.createAt( span );
+					const position = ModelPosition.createAt( span, 0 );
 
 					const paragraph2 = conversionApi.writer.createElement( 'paragraph' );
 					const result = conversionApi.splitToAllowedParent( paragraph2, position );
@@ -597,7 +597,7 @@ describe( 'UpcastDispatcher', () => {
 				dispatcher.on( 'documentFragment', ( evt, data, conversionApi ) => {
 					const paragraph = conversionApi.writer.createElement( 'paragraph' );
 					const span = conversionApi.writer.createElement( 'span' );
-					const position = ModelPosition.createAt( paragraph );
+					const position = ModelPosition.createAt( paragraph, 0 );
 
 					const result = conversionApi.splitToAllowedParent( span, position );
 

+ 7 - 3
packages/ckeditor5-engine/tests/model/position.js

@@ -158,17 +158,21 @@ describe( 'Position', () => {
 	} );
 
 	describe( 'createAt()', () => {
+		it( 'should throw if no offset is passed', () => {
+			expect( () => Position.createAt( ul ) ).to.throw( CKEditorError, /model-position-createAt-offset-required/ );
+		} );
+
 		it( 'should create positions from positions', () => {
 			const spy = testUtils.sinon.spy( Position, 'createFromPosition' );
 
-			expect( Position.createAt( Position.createAt( ul ) ) ).to.have.property( 'path' ).that.deep.equals( [ 1, 0 ] );
+			expect( Position.createAt( Position.createAt( ul, 0 ) ) ).to.have.property( 'path' ).that.deep.equals( [ 1, 0 ] );
 
 			expect( spy.calledOnce ).to.be.true;
 		} );
 
 		it( 'should create positions from node and offset', () => {
-			expect( Position.createAt( ul ) ).to.have.property( 'path' ).that.deep.equals( [ 1, 0 ] );
-			expect( Position.createAt( li1 ) ).to.have.property( 'path' ).that.deep.equals( [ 1, 0, 0 ] );
+			expect( Position.createAt( ul, 0 ) ).to.have.property( 'path' ).that.deep.equals( [ 1, 0 ] );
+			expect( Position.createAt( li1, 0 ) ).to.have.property( 'path' ).that.deep.equals( [ 1, 0, 0 ] );
 			expect( Position.createAt( ul, 1 ) ).to.have.property( 'path' ).that.deep.equals( [ 1, 1 ] );
 		} );
 

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

@@ -176,7 +176,7 @@ describe( 'Range', () => {
 		describe( 'createCollapsedAt()', () => {
 			it( 'should return new collapsed range at the given item position', () => {
 				const item = new Element( 'p', null, new Text( 'foo' ) );
-				const range = Range.createCollapsedAt( item );
+				const range = Range.createCollapsedAt( item, 0 );
 
 				expect( range.start.parent ).to.equal( item );
 				expect( range.start.offset ).to.equal( 0 );

+ 14 - 14
packages/ckeditor5-engine/tests/model/schema.js

@@ -394,8 +394,8 @@ describe( 'Schema', () => {
 		} );
 
 		it( 'accepts a schemaContext instance as a context', () => {
-			const rootContext = new SchemaContext( Position.createAt( root1 ) );
-			const paragraphContext = new SchemaContext( Position.createAt( r1p1 ) );
+			const rootContext = new SchemaContext( Position.createAt( root1, 0 ) );
+			const paragraphContext = new SchemaContext( Position.createAt( r1p1, 0 ) );
 
 			expect( schema.checkChild( rootContext, 'paragraph' ) ).to.be.true;
 			expect( schema.checkChild( rootContext, '$text' ) ).to.be.false;
@@ -405,8 +405,8 @@ describe( 'Schema', () => {
 		} );
 
 		it( 'accepts a position as a context', () => {
-			const posInRoot = Position.createAt( root1 );
-			const posInParagraph = Position.createAt( r1p1 );
+			const posInRoot = Position.createAt( root1, 0 );
+			const posInParagraph = Position.createAt( r1p1, 0 );
 
 			expect( schema.checkChild( posInRoot, 'paragraph' ) ).to.be.true;
 			expect( schema.checkChild( posInRoot, '$text' ) ).to.be.false;
@@ -486,16 +486,16 @@ describe( 'Schema', () => {
 		} );
 
 		it( 'accepts a position as a context', () => {
-			const posInRoot = Position.createAt( root1 );
-			const posInParagraph = Position.createAt( r1p1 );
+			const posInRoot = Position.createAt( root1, 0 );
+			const posInParagraph = Position.createAt( r1p1, 0 );
 
 			expect( schema.checkAttribute( posInRoot, 'align' ) ).to.be.false;
 			expect( schema.checkAttribute( posInParagraph, 'align' ) ).to.be.true;
 		} );
 
 		it( 'accepts a schemaContext instance as a context', () => {
-			const rootContext = new SchemaContext( Position.createAt( root1 ) );
-			const paragraphContext = new SchemaContext( Position.createAt( r1p1 ) );
+			const rootContext = new SchemaContext( Position.createAt( root1, 0 ) );
+			const paragraphContext = new SchemaContext( Position.createAt( r1p1, 0 ) );
 
 			expect( schema.checkAttribute( rootContext, 'align' ) ).to.be.false;
 			expect( schema.checkAttribute( paragraphContext, 'align' ) ).to.be.true;
@@ -1575,7 +1575,7 @@ describe( 'Schema', () => {
 		it( 'should return position ancestor that allows to insert given node to it', () => {
 			const node = new Element( 'paragraph' );
 
-			const allowedParent = schema.findAllowedParent( node, Position.createAt( r1bQp ) );
+			const allowedParent = schema.findAllowedParent( node, Position.createAt( r1bQp, 0 ) );
 
 			expect( allowedParent ).to.equal( r1bQ );
 		} );
@@ -1583,7 +1583,7 @@ describe( 'Schema', () => {
 		it( 'should return position ancestor that allows to insert given node to it when position is already i such an element', () => {
 			const node = new Text( 'text' );
 
-			const parent = schema.findAllowedParent( node, Position.createAt( r1bQp ) );
+			const parent = schema.findAllowedParent( node, Position.createAt( r1bQp, 0 ) );
 
 			expect( parent ).to.equal( r1bQp );
 		} );
@@ -1597,7 +1597,7 @@ describe( 'Schema', () => {
 			} );
 			const node = new Element( 'div' );
 
-			const parent = schema.findAllowedParent( node, Position.createAt( r1bQp ) );
+			const parent = schema.findAllowedParent( node, Position.createAt( r1bQp, 0 ) );
 
 			expect( parent ).to.null;
 		} );
@@ -1611,7 +1611,7 @@ describe( 'Schema', () => {
 			} );
 			const node = new Element( 'div' );
 
-			const parent = schema.findAllowedParent( node, Position.createAt( r1bQp ) );
+			const parent = schema.findAllowedParent( node, Position.createAt( r1bQp, 0 ) );
 
 			expect( parent ).to.null;
 		} );
@@ -1619,7 +1619,7 @@ describe( 'Schema', () => {
 		it( 'should return null when there is no allowed ancestor for given position', () => {
 			const node = new Element( 'section' );
 
-			const parent = schema.findAllowedParent( node, Position.createAt( r1bQp ) );
+			const parent = schema.findAllowedParent( node, Position.createAt( r1bQp, 0 ) );
 
 			expect( parent ).to.null;
 		} );
@@ -2866,7 +2866,7 @@ describe( 'SchemaContext', () => {
 		} );
 
 		it( 'creates context based on a position', () => {
-			const pos = Position.createAt( root.getChild( 0 ).getChild( 0 ) );
+			const pos = Position.createAt( root.getChild( 0 ).getChild( 0 ), 0 );
 			const ctx = new SchemaContext( pos );
 
 			expect( ctx.length ).to.equal( 3 );

+ 68 - 37
packages/ckeditor5-engine/tests/model/utils/insertcontent.js

@@ -18,11 +18,13 @@ describe( 'DataController utils', () => {
 	let model, doc;
 
 	describe( 'insertContent', () => {
-		it( 'should use parent batch', () => {
+		beforeEach( () => {
 			model = new Model();
 			doc = model.document;
 			doc.createRoot();
+		} );
 
+		it( 'should use parent batch', () => {
 			model.schema.extend( '$text', { allowIn: '$root' } );
 			setData( model, 'x[]x' );
 
@@ -33,10 +35,6 @@ describe( 'DataController utils', () => {
 		} );
 
 		it( 'should be able to insert content at custom selection', () => {
-			model = new Model();
-			doc = model.document;
-			doc.createRoot();
-
 			model.schema.extend( '$text', { allowIn: '$root' } );
 			setData( model, 'a[]bc' );
 
@@ -49,10 +47,6 @@ describe( 'DataController utils', () => {
 		} );
 
 		it( 'should modify passed selection instance', () => {
-			model = new Model();
-			doc = model.document;
-			doc.createRoot();
-
 			model.schema.extend( '$text', { allowIn: '$root' } );
 			setData( model, 'a[]bc' );
 
@@ -72,10 +66,6 @@ describe( 'DataController utils', () => {
 		} );
 
 		it( 'should be able to insert content at custom position', () => {
-			model = new Model();
-			doc = model.document;
-			doc.createRoot();
-
 			model.schema.extend( '$text', { allowIn: '$root' } );
 			setData( model, 'a[]bc' );
 
@@ -88,10 +78,6 @@ describe( 'DataController utils', () => {
 		} );
 
 		it( 'should be able to insert content at custom range', () => {
-			model = new Model();
-			doc = model.document;
-			doc.createRoot();
-
 			model.schema.extend( '$text', { allowIn: '$root' } );
 			setData( model, 'a[]bc' );
 
@@ -104,10 +90,6 @@ describe( 'DataController utils', () => {
 		} );
 
 		it( 'should be able to insert content at model selection if document selection is passed', () => {
-			model = new Model();
-			doc = model.document;
-			doc.createRoot();
-
 			model.schema.extend( '$text', { allowIn: '$root' } );
 			setData( model, 'a[]bc' );
 
@@ -118,10 +100,6 @@ describe( 'DataController utils', () => {
 		} );
 
 		it( 'should be able to insert content at model selection if none passed', () => {
-			model = new Model();
-			doc = model.document;
-			doc.createRoot();
-
 			model.schema.extend( '$text', { allowIn: '$root' } );
 			setData( model, 'a[]bc' );
 
@@ -131,11 +109,72 @@ describe( 'DataController utils', () => {
 			} );
 		} );
 
-		it( 'accepts DocumentFragment', () => {
-			model = new Model();
-			doc = model.document;
-			doc.createRoot();
+		it( 'should be able to insert content at model element (numeric offset)', () => {
+			model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+
+			setData( model, '<paragraph>foo[]</paragraph><paragraph>bar</paragraph>' );
+
+			const element = doc.getRoot().getNodeByPath( [ 1 ] );
+
+			model.change( writer => {
+				const text = writer.createText( 'x' );
+
+				insertContent( model, text, element, 2 );
+
+				expect( getData( model ) ).to.equal( '<paragraph>foo[]</paragraph><paragraph>baxr</paragraph>' );
+			} );
+		} );
+
+		it( 'should be able to insert content at model element (offset="in")', () => {
+			model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+
+			setData( model, '<paragraph>foo[]</paragraph><paragraph>bar</paragraph>' );
+
+			const element = doc.getRoot().getNodeByPath( [ 1 ] );
+
+			model.change( writer => {
+				const text = writer.createText( 'x' );
+
+				insertContent( model, text, element, 'in' );
+
+				expect( getData( model ) ).to.equal( '<paragraph>foo[]</paragraph><paragraph>x</paragraph>' );
+			} );
+		} );
+
+		it( 'should be able to insert content at model element (offset="on")', () => {
+			model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+			model.schema.register( 'foo', { inheritAllFrom: '$block' } );
+
+			setData( model, '<paragraph>foo[]</paragraph><paragraph>bar</paragraph>' );
+
+			const element = doc.getRoot().getNodeByPath( [ 1 ] );
+
+			model.change( writer => {
+				const insertElement = writer.createElement( 'foo' );
+
+				insertContent( model, insertElement, element, 'on' );
+
+				expect( getData( model ) ).to.equal( '<paragraph>foo[]</paragraph><foo></foo>' );
+			} );
+		} );
+
+		it( 'should be able to insert content at model element (offset="end")', () => {
+			model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );
+
+			setData( model, '<paragraph>foo[]</paragraph><paragraph>bar</paragraph>' );
+
+			const element = doc.getRoot().getNodeByPath( [ 1 ] );
+
+			model.change( writer => {
+				const text = writer.createText( 'x' );
+
+				insertContent( model, text, element, 'end' );
+
+				expect( getData( model ) ).to.equal( '<paragraph>foo[]</paragraph><paragraph>barx</paragraph>' );
+			} );
+		} );
 
+		it( 'accepts DocumentFragment', () => {
 			model.schema.extend( '$text', { allowIn: '$root' } );
 
 			setData( model, 'x[]x' );
@@ -146,10 +185,6 @@ describe( 'DataController utils', () => {
 		} );
 
 		it( 'accepts Text', () => {
-			model = new Model();
-			doc = model.document;
-			doc.createRoot();
-
 			model.schema.extend( '$text', { allowIn: '$root' } );
 
 			setData( model, 'x[]x' );
@@ -160,10 +195,6 @@ describe( 'DataController utils', () => {
 		} );
 
 		it( 'should save the reference to the original object', () => {
-			model = new Model();
-			doc = model.document;
-			doc.createRoot();
-
 			const content = new Element( 'image' );
 
 			model.schema.register( 'paragraph', { inheritAllFrom: '$block' } );

+ 2 - 2
packages/ckeditor5-engine/tests/model/writer.js

@@ -1476,7 +1476,7 @@ describe( 'Writer', () => {
 			const docFrag = createDocumentFragment();
 
 			expect( () => {
-				move( range, docFrag );
+				move( range, docFrag, 0 );
 			} ).to.throw( CKEditorError, /^writer-move-different-document/ );
 		} );
 
@@ -2385,7 +2385,7 @@ describe( 'Writer', () => {
 			const firstParagraph = root.getNodeByPath( [ 1 ] );
 
 			const setFocusSpy = sinon.spy( DocumentSelection.prototype, '_setFocus' );
-			setSelectionFocus( firstParagraph );
+			setSelectionFocus( firstParagraph, 0 );
 
 			expect( setFocusSpy.calledOnce ).to.be.true;
 			setFocusSpy.restore();

+ 2 - 2
packages/ckeditor5-engine/tests/tickets/1281.js

@@ -47,10 +47,10 @@ describe( 'Bug ckeditor5-engine#1281', () => {
 
 		expect( selRanges.length ).to.equal( 2 );
 
-		assertPositions( Position.createAt( thirdParagraph ), selRanges[ 0 ].start );
+		assertPositions( Position.createAt( thirdParagraph, 0 ), selRanges[ 0 ].start );
 		assertPositions( Position.createAt( thirdParagraph, 'end' ), selRanges[ 0 ].end );
 
-		assertPositions( Position.createAt( fourthParagraph ), selRanges[ 1 ].start );
+		assertPositions( Position.createAt( fourthParagraph, 0 ), selRanges[ 1 ].start );
 		assertPositions( Position.createAt( fourthParagraph, 'end' ), selRanges[ 1 ].end );
 	} );
 

+ 3 - 3
packages/ckeditor5-engine/tests/view/downcastwriter/writer.js

@@ -22,7 +22,7 @@ describe( 'DowncastWriter', () => {
 
 	describe( 'setSelection()', () => {
 		it( 'should set document view selection', () => {
-			const position = ViewPosition.createAt( root );
+			const position = ViewPosition.createAt( root, 0 );
 			writer.setSelection( position );
 
 			const ranges = Array.from( doc.selection.getRanges() );
@@ -33,7 +33,7 @@ describe( 'DowncastWriter', () => {
 		} );
 
 		it( 'should be able to set fake selection', () => {
-			const position = ViewPosition.createAt( root );
+			const position = ViewPosition.createAt( root, 0 );
 			writer.setSelection( position, { fake: true, label: 'foo' } );
 
 			expect( doc.selection.isFake ).to.be.true;
@@ -43,7 +43,7 @@ describe( 'DowncastWriter', () => {
 
 	describe( 'setSelectionFocus()', () => {
 		it( 'should use selection._setFocus method internally', () => {
-			const position = ViewPosition.createAt( root );
+			const position = ViewPosition.createAt( root, 0 );
 			writer.setSelection( position );
 
 			const spy = sinon.spy( writer.document.selection, '_setFocus' );

+ 10 - 4
packages/ckeditor5-engine/tests/view/position.js

@@ -152,12 +152,18 @@ describe( 'Position', () => {
 	} );
 
 	describe( 'createAt', () => {
+		it( 'should throw if no offset is passed', () => {
+			const element = new Element( 'p' );
+
+			expect( () => Position.createAt( element ) ).to.throw( CKEditorError, /view-position-createAt-offset-required/ );
+		} );
+
 		it( 'should create positions from positions', () => {
 			const spy = sinon.spy( Position, 'createFromPosition' );
 
 			const p = new Element( 'p' );
 			const position = new Position( p, 0 );
-			const created = Position.createAt( position );
+			const created = Position.createAt( position, 0 );
 
 			expect( created.isEqual( position ) ).to.be.true;
 			expect( spy.calledOnce ).to.be.true;
@@ -167,8 +173,8 @@ describe( 'Position', () => {
 			const foo = new Text( 'foo' );
 			const p = new Element( 'p', null, foo );
 
-			expect( Position.createAt( foo ).parent ).to.equal( foo );
-			expect( Position.createAt( foo ).offset ).to.equal( 0 );
+			expect( Position.createAt( foo, 0 ).parent ).to.equal( foo );
+			expect( Position.createAt( foo, 0 ).offset ).to.equal( 0 );
 
 			expect( Position.createAt( foo, 2 ).parent ).to.equal( foo );
 			expect( Position.createAt( foo, 2 ).offset ).to.equal( 2 );
@@ -572,7 +578,7 @@ describe( 'Position', () => {
 		} );
 
 		it( 'for two positions in the same element returns the element', () => {
-			const startMaecenasPosition = Position.createAt( liOl2 );
+			const startMaecenasPosition = Position.createAt( liOl2, 0 );
 			const beforeTellusPosition = new Position( liOl2, 18 );
 
 			test( startMaecenasPosition, beforeTellusPosition, liOl2 );

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

@@ -690,7 +690,7 @@ describe( 'Range', () => {
 		describe( 'createCollapsedAt()', () => {
 			it( 'should return new collapsed range at the given item position', () => {
 				const item = new Element( 'p', null, new Text( 'foo' ) );
-				const range = Range.createCollapsedAt( item );
+				const range = Range.createCollapsedAt( item, 0 );
 
 				expect( range.start.parent ).to.equal( item );
 				expect( range.start.offset ).to.equal( 0 );

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

@@ -478,8 +478,8 @@ describe( 'view', () => {
 
 					return element;
 				} );
-				writer.insert( ViewPosition.createAt( p ), ui );
-				writer.insert( ViewPosition.createAt( viewRoot ), p );
+				writer.insert( ViewPosition.createAt( p, 0 ), ui );
+				writer.insert( ViewPosition.createAt( viewRoot, 0 ), p );
 			} );
 
 			expect( renderingCalled ).to.be.true;