Bladeren bron

Merge branch 'master' into t/627

Piotrek Koszuliński 9 jaren geleden
bovenliggende
commit
783b933620

+ 65 - 29
packages/ckeditor5-engine/src/datacontroller.js

@@ -3,30 +3,36 @@
  * For licensing, see LICENSE.md.
  */
 
-import Mapper from './conversion/mapper.js';
+import mix from '../../utils/mix.js';
+import EmitterMixin from '../../utils/emittermixin.js';
 
-import ModelConversionDispatcher from './conversion/modelconversiondispatcher.js';
-import { insertText } from './conversion/model-to-view-converters.js';
+import Mapper from '../conversion/mapper.js';
 
-import ViewConversionDispatcher from './conversion/viewconversiondispatcher.js';
-import { convertText, convertToModelFragment } from './conversion/view-to-model-converters.js';
+import ModelConversionDispatcher from '../conversion/modelconversiondispatcher.js';
+import { insertText } from '../conversion/model-to-view-converters.js';
 
-import ViewDocumentFragment from './view/documentfragment.js';
+import ViewConversionDispatcher from '../conversion/viewconversiondispatcher.js';
+import { convertText, convertToModelFragment } from '../conversion/view-to-model-converters.js';
 
-import ModelRange from './model/range.js';
-import ModelPosition from './model/position.js';
+import ViewDocumentFragment from '../view/documentfragment.js';
+
+import ModelRange from '../model/range.js';
+import ModelPosition from '../model/position.js';
+
+import insert from './insert.js';
 
 /**
  * Controller for the data pipeline. The data pipeline controls how data is retrieved from the document
- * and set inside it. Hence, the controller features two methods which allow to {@link engine.DataController#get get}
- * and {@link engine.DataController#set set} data of the {@link engine.DataController#model model}
+ * and set inside it. Hence, the controller features two methods which allow to {@link engine.controller.DataController#get get}
+ * and {@link engine.controller.DataController#set set} data of the {@link engine.controller.DataController#model model}
  * using given:
  *
  * * {@link engine.dataProcessor.DataProcessor data processor},
  * * {@link engine.conversion.ModelConversionDispatcher model to view} and
  * * {@link engine.conversion.ViewConversionDispatcher view to model} converters.
  *
- * @memberOf engine
+ * @memberOf engine.controller
+ * @mixes utils.EmitterMixin
  */
 export default class DataController {
 	/**
@@ -40,7 +46,7 @@ export default class DataController {
 		 * Document model.
 		 *
 		 * @readonly
-		 * @member {engine.model.document} engine.DataController#model
+		 * @member {engine.model.document} engine.controller.DataController#model
 		 */
 		this.model = model;
 
@@ -48,7 +54,7 @@ export default class DataController {
 		 * Data processor used during the conversion.
 		 *
 		 * @readonly
-		 * @member {engine.dataProcessor.DataProcessor} engine.DataController#processor
+		 * @member {engine.dataProcessor.DataProcessor} engine.controller.DataController#processor
 		 */
 		this.processor = dataProcessor;
 
@@ -57,12 +63,12 @@ export default class DataController {
 		 * cleared directly after data are converted. However, the mapper is defined as class property, because
 		 * it needs to be passed to the `ModelConversionDispatcher` as a conversion API.
 		 *
-		 * @member {engine.conversion.Mapper} engine.DataController#_mapper
+		 * @member {engine.conversion.Mapper} engine.controller.DataController#_mapper
 		 */
 		this.mapper = new Mapper();
 
 		/**
-		 * Model to view conversion dispatcher used by the {@link engine.DataController#get get method}.
+		 * Model to view conversion dispatcher used by the {@link engine.controller.DataController#get get method}.
 		 * To attach model to view converter to the data pipeline you need to add lister to this property:
 		 *
 		 *		data.modelToView( 'insert:$element', customInsertConverter );
@@ -72,7 +78,7 @@ export default class DataController {
 		 *		buildModelConverter().for( data.modelToView ).fromAttribute( 'bold' ).toElement( 'b' );
 		 *
 		 * @readonly
-		 * @member {engine.conversion.ModelConversionDispatcher} engine.DataController#modelToView
+		 * @member {engine.conversion.ModelConversionDispatcher} engine.controller.DataController#modelToView
 		 */
 		this.modelToView = new ModelConversionDispatcher( {
 			mapper: this.mapper
@@ -80,7 +86,7 @@ export default class DataController {
 		this.modelToView.on( 'insert:$text', insertText(), { priority: 'lowest' } );
 
 		/**
-		 * View to model conversion dispatcher used by the {@link engine.DataController#set set method}.
+		 * View to model conversion dispatcher used by the {@link engine.controller.DataController#set set method}.
 		 * To attach view to model converter to the data pipeline you need to add lister to this property:
 		 *
 		 *		data.viewToModel( 'element', customElementConverter );
@@ -90,7 +96,7 @@ export default class DataController {
 		 *		buildViewConverter().for( data.viewToModel ).fromElement( 'b' ).toAttribute( 'bold', 'true' );
 		 *
 		 * @readonly
-		 * @member {engine.conversion.ViewConversionDispatcher} engine.DataController#viewToModel
+		 * @member {engine.conversion.ViewConversionDispatcher} engine.controller.DataController#viewToModel
 		 */
 		this.viewToModel = new ViewConversionDispatcher( {
 			schema: model.schema
@@ -104,11 +110,13 @@ export default class DataController {
 		this.viewToModel.on( 'text', convertText(), { priority: 'lowest' } );
 		this.viewToModel.on( 'element', convertToModelFragment(), { priority: 'lowest' } );
 		this.viewToModel.on( 'documentFragment', convertToModelFragment(), { priority: 'lowest' } );
+
+		this.on( 'insert', ( evt, data ) => insert( this, data.content, data.selection, data.batch ) );
 	}
 
 	/**
-	 * Returns model's data converted by the {@link engine.DataController#modelToView model to view converters} and
-	 * formatted by the {@link engine.DataController#processor data processor}.
+	 * Returns model's data converted by the {@link engine.controller.DataController#modelToView model to view converters} and
+	 * formatted by the {@link engine.controller.DataController#processor data processor}.
 	 *
 	 * @param {String} [rootName='main'] Root name.
 	 * @returns {String} Output data.
@@ -120,8 +128,8 @@ export default class DataController {
 
 	/**
 	 * Returns the content of the given {@link engine.model.Element model's element} converted by the
-	 * {@link engine.DataController#modelToView model to view converters} and formatted by the
-	 * {@link engine.DataController#processor data processor}.
+	 * {@link engine.controller.DataController#modelToView model to view converters} and formatted by the
+	 * {@link engine.controller.DataController#processor data processor}.
 	 *
 	 * @param {engine.model.Element} modelElement Element which content will be stringified.
 	 * @returns {String} Output data.
@@ -136,7 +144,7 @@ export default class DataController {
 
 	/**
 	 * Returns the content of the given {@link engine.model.Element model's element} converted by the
-	 * {@link engine.DataController#modelToView model to view converters} to the
+	 * {@link engine.controller.DataController#modelToView model to view converters} to the
 	 * {@link engine.view.DocumentFragment view DocumentFragment}.
 	 *
 	 * @param {engine.model.Element} modelElement Element which content will be stringified.
@@ -156,11 +164,11 @@ export default class DataController {
 	}
 
 	/**
-	 * Sets input data parsed by the {@link engine.DataController#processor data processor} and
-	 * converted by the {@link engine.DataController#viewToModel view to model converters}.
+	 * Sets input data parsed by the {@link engine.controller.DataController#processor data processor} and
+	 * converted by the {@link engine.controller.DataController#viewToModel view to model converters}.
 	 *
 	 * This method also creates a batch with all the changes applied. If all you need is to parse data use
-	 * the {@link engine.dataController#parse} method.
+	 * the {@link engine.controller.DataController#parse} method.
 	 *
 	 * @param {String} data Input data.
 	 * @param {String} [rootName='main'] Root name.
@@ -183,10 +191,10 @@ export default class DataController {
 	}
 
 	/**
-	 * Returns data parsed by the {@link engine.DataController#processor data processor} and then
-	 * converted by the {@link engine.DataController#viewToModel view to model converters}.
+	 * Returns data parsed by the {@link engine.controller.DataController#processor data processor} and then
+	 * converted by the {@link engine.controller.DataController#viewToModel view to model converters}.
 	 *
-	 * @see engine.DataController#set
+	 * @see engine.controller.DataController#set
 	 * @param {String} data Data to parse.
 	 * @param {String} [context='$root'] Base context in which view will be converted to the model. See:
 	 * {@link engine.conversion.ViewConversionDispatcher#convert}.
@@ -204,4 +212,32 @@ export default class DataController {
 	 * Removes all event listeners set by the DataController.
 	 */
 	destroy() {}
+
+	/**
+	 * See {@link engine.controller.insert}.
+	 *
+	 * @fires engine.controller.DataController#insert
+	 * @param {engine.view.DocumentFragment} content The content to insert.
+	 * @param {engine.model.Selection} selection Selection into which the content should be inserted.
+	 * @param {engine.model.Batch} [batch] Batch to which deltas will be added. If not specified, then
+	 * changes will be added to a new batch.
+	 */
+	insert( content, selection, batch ) {
+		this.fire( 'insert', { content, selection, batch } );
+	}
 }
+
+mix( DataController, EmitterMixin );
+
+/**
+ * Event fired when {@link engine.controller.DataController#insert} method is called.
+ * The {@link engine.controller.dataController.insert default action of the composer} is implemented as a
+ * listener to that event so it can be fully customized by the features.
+ *
+ * @event engine.controller.DataController#insert
+ * @param {Object} data
+ * @param {engine.view.DocumentFragment} data.content The content to insert.
+ * @param {engine.model.Selection} data.selection Selection into which the content should be inserted.
+ * @param {engine.model.Batch} [data.batch] Batch to which deltas will be added.
+ */
+

+ 17 - 17
packages/ckeditor5-engine/src/editingcontroller.js

@@ -3,26 +3,26 @@
  * For licensing, see LICENSE.md.
  */
 
-import ViewDocument from './view/document.js';
-import Mapper from './conversion/mapper.js';
-import ModelConversionDispatcher from './conversion/modelconversiondispatcher.js';
-import { insertText, remove, move, rename } from './conversion/model-to-view-converters.js';
-import { convertSelectionChange } from './conversion/view-selection-to-model-converters.js';
+import ViewDocument from '../view/document.js';
+import Mapper from '../conversion/mapper.js';
+import ModelConversionDispatcher from '../conversion/modelconversiondispatcher.js';
+import { insertText, remove, move, rename } from '../conversion/model-to-view-converters.js';
+import { convertSelectionChange } from '../conversion/view-selection-to-model-converters.js';
 import {
 	convertRangeSelection,
 	convertCollapsedSelection,
 	clearAttributes,
 	clearFakeSelection
-} from './conversion/model-selection-to-view-converters.js';
+} from '../conversion/model-selection-to-view-converters.js';
 
-import EmitterMixin from '../utils/emittermixin.js';
+import EmitterMixin from '../../utils/emittermixin.js';
 
 /**
- * Controller for the editing pipeline. The editing pipeline controls {@link engine.EditingController#model model} rendering,
- * including selection handling. It also creates {@link engine.EditingController#view view document} which build a
+ * Controller for the editing pipeline. The editing pipeline controls {@link engine.controller.EditingController#model model} rendering,
+ * including selection handling. It also creates {@link engine.controller.EditingController#view view document} which build a
  * browser-independent virtualization over the DOM elements. Editing controller also attach default converters.
  *
- * @memberOf engine
+ * @memberOf engine.controller
  */
 export default class EditingController {
 	/**
@@ -35,7 +35,7 @@ export default class EditingController {
 		 * Document model.
 		 *
 		 * @readonly
-		 * @member {engine.model.document} engine.EditingController#model
+		 * @member {engine.model.document} engine.controller.EditingController#model
 		 */
 		this.model = model;
 
@@ -43,7 +43,7 @@ export default class EditingController {
 		 * View document.
 		 *
 		 * @readonly
-		 * @member {engine.view.document} engine.EditingController#view
+		 * @member {engine.view.document} engine.controller.EditingController#view
 		 */
 		this.view = new ViewDocument();
 
@@ -51,13 +51,13 @@ export default class EditingController {
 		 * Mapper which describes model-view binding.
 		 *
 		 * @readonly
-		 * @member {engine.conversion.Mapper} engine.EditingController#mapper
+		 * @member {engine.conversion.Mapper} engine.controller.EditingController#mapper
 		 */
 		this.mapper = new Mapper();
 
 		/**
 		 * Model to view conversion dispatcher, which converts changes from the model to
-		 * {@link engine.EditingController#view editing view}.
+		 * {@link engine.controller.EditingController#view editing view}.
 		 *
 		 * To attach model to view converter to the editing pipeline you need to add lister to this property:
 		 *
@@ -68,7 +68,7 @@ export default class EditingController {
 		 *		buildModelConverter().for( editing.modelToView ).fromAttribute( 'bold' ).toElement( 'b' );
 		 *
 		 * @readonly
-		 * @member {engine.conversion.ModelConversionDispatcher} engine.EditingController#modelToView
+		 * @member {engine.conversion.ModelConversionDispatcher} engine.controller.EditingController#modelToView
 		 */
 		this.modelToView = new ModelConversionDispatcher( {
 			mapper: this.mapper,
@@ -77,10 +77,10 @@ export default class EditingController {
 
 		/**
 		 * Property keeping all listenters attached by controller on other objects, so it can
-		 * stop listening on {@link engine.EditingController#destroy}.
+		 * stop listening on {@link engine.controller.EditingController#destroy}.
 		 *
 		 * @private
-		 * @member {utils.EmitterMixin} engine.EditingController#_listenter
+		 * @member {utils.EmitterMixin} engine.controller.EditingController#_listenter
 		 */
 		this._listenter = Object.create( EmitterMixin );
 

+ 415 - 0
packages/ckeditor5-engine/src/controller/insert.js

@@ -0,0 +1,415 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import Position from '../model/position.js';
+import LivePosition from '../model/liveposition.js';
+import Text from '../model/text.js';
+import Element from '../model/element.js';
+import Range from '../model/range.js';
+import log from '../../utils/log.js';
+
+// import { stringify as stringifyModel } from '../dev-utils/model.js';
+
+/**
+ * Inserts content into the editor (specified selection) as one would expect the paste
+ * functionality to work.
+ *
+ * **Note:** Use {@link engine.controller.DataController#insert} instead of this function.
+ * This function is only exposed to be reusable in algorithms which change the {@link engine.controller.DataController#insert}
+ * method's behavior.
+ *
+ * @method engine.controller.insert
+ * @param {engine.controller.DataController} dataController The data controller in context of which the insertion
+ * should be performed.
+ * @param {engine.view.DocumentFragment} content The content to insert.
+ * @param {engine.model.Selection} selection Selection into which the content should be inserted.
+ * @param {engine.model.Batch} [batch] Batch to which deltas will be added. If not specified, then
+ * changes will be added to a new batch.
+ */
+export default function insert( dataController, content, selection, batch ) {
+	if ( !batch ) {
+		batch = dataController.model.batch();
+	}
+
+	if ( !selection.isCollapsed ) {
+		dataController.model.composer.deleteContents( batch, selection, {
+			merge: true
+		} );
+	}
+
+	// Convert the pasted content to a model document fragment.
+	// Convertion is contextual, but in this case we need an "all allowed" context and for that
+	// we use the $clipboardHolder item.
+	const modelFragment = dataController.viewToModel.convert( content, {
+		context: [ '$clipboardHolder' ]
+	} );
+
+	// We'll be debugging this dozens of times still.
+	// console.log( stringifyModel( modelFragment ) );
+
+	const insertion = new Insertion( dataController, batch, selection.anchor );
+
+	insertion.handleNodes( modelFragment.getChildren(), {
+		// The set of children being inserted is the only set in this context
+		// so it's the first and last (it's a hack ;)).
+		isFirst: true,
+		isLast: true
+	} );
+
+	selection.setRanges( insertion.getSelectionRanges() );
+}
+
+/**
+ * Utility class for performing content insertion.
+ *
+ * @private
+ * @memberOf engine.dataController.insert
+ */
+class Insertion {
+	constructor( dataController, batch, position ) {
+		/**
+		 * The data controller in context of which the insertion should be performed.
+		 */
+		this.dataController = dataController;
+
+		/**
+		 * Batch to which deltas will be added.
+		 */
+		this.batch = batch;
+
+		/**
+		 * The position at which (or near which) the next node will be inserted.
+		 */
+		this.position = position;
+
+		/**
+		 * Elements with which the inserted elements can be merged.
+		 *
+		 *		<p>x^</p><p>y</p> + <p>z</p> (can merge to <p>x</p>)
+		 *		<p>x</p><p>^y</p> + <p>z</p> (can merge to <p>y</p>)
+		 *		<p>x^y</p> + <p>z</p> (can merge to <p>xy</p> which will be split during the action,
+		 *								so both its pieces will be added to this set)
+		 */
+		this.canMergeWith = new Set( [ this.position.parent ] );
+
+		/**
+		 * Schema of the model.
+		 */
+		this.schema = dataController.model.schema;
+	}
+
+	/**
+	 * Handles insertion of a set of nodes.
+	 *
+	 * @param {Iterable.<engine.model.Node>} nodes Nodes to insert.
+	 * @param {Object} parentContext Context in which parent of these nodes was supposed to be inserted.
+	 * If the parent context is passed it means that the parent element was stripped (was not allowed).
+	 */
+	handleNodes( nodes, parentContext ) {
+		nodes = Array.from( nodes );
+
+		for ( let i = 0; i < nodes.length; i++ ) {
+			const node = nodes[ i ].clone();
+
+			this._handleNode( node, {
+				isFirst: i === 0 && parentContext.isFirst,
+				isLast: ( i === ( nodes.length - 1 ) ) && parentContext.isLast
+			} );
+		}
+	}
+
+	/**
+	 * Returns a range to be selected after insertion.
+	 *
+	 * @returns {engine.model.Range}
+	 */
+	getSelectionRanges() {
+		if ( this.nodeToSelect ) {
+			return [ Range.createOn( this.nodeToSelect ) ];
+		} else {
+			const searchRange = new Range( Position.createAt( this.position.root ), this.position );
+
+			for ( const position of searchRange.getPositions( { direction: 'backward' } ) ) {
+				if ( this.schema.check( { name: '$text', inside: position } ) ) {
+					return [ new Range( position ) ];
+				}
+			}
+
+			// As a last resort, simply return the current position.
+			// See the "should not break when autoparagraphing of text is not possible" test for
+			// a case which triggers this condition.
+			return [ new Range( this.position ) ];
+		}
+	}
+
+	/**
+	 * Handles insertion of a single node.
+	 *
+	 * @param {engine.model.Node} node
+	 * @param {Object} context
+	 * @param {Boolean} context.isFirst Whether the given node is the first one in the content to be inserted.
+	 * @param {Boolean} context.isLast Whether the given node is the last one in the content to be inserted.
+	 */
+	_handleNode( node, context = {} ) {
+		// Let's handle object in a special way.
+		// * They should never be merged with other elements.
+		// * If they are not allowed in any of the selection ancestors, they could be either autoparagraphed or totally removed.
+		if ( this._checkIsObject( node ) ) {
+			this._handleObject( node, context );
+
+			return;
+		}
+
+		// Try to find a place for the given node.
+		// Split the position.parent's branch up to a point where the node can be inserted.
+		// If it isn't allowed in the whole branch, then of course don't split anything.
+		const isAllowed = this._checkAndSplitToAllowedPosition( node, context );
+
+		if ( !isAllowed ) {
+			this._handleDisallowedNode( node, context );
+
+			return;
+		}
+
+		this._insert( node );
+
+		// After the node was inserted we may try to merge it with its siblings.
+		// This should happen only if it was the first and/or last of the nodes (so only with boundary nodes)
+		// and only if the selection was in those elements initially.
+		//
+		// 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>
+		// 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
+		//																	 it's exactly the same case as above)
+		this._mergeSiblingsOf( node, context );
+	}
+
+	/**
+	 * @param {engine.model.Element} node The object element.
+	 * @param {Object} context
+	 */
+	_handleObject( node, context ) {
+		// Try finding it a place in the tree.
+		if ( this._checkAndSplitToAllowedPosition( node ) ) {
+			this._insert( node );
+		}
+		// Try autoparagraphing.
+		else {
+			this._tryAutoparagraphing( node, context );
+		}
+	}
+
+	/**
+	 * @param {engine.model.Node} node The disallowed node which needs to be handled.
+	 * @param {Object} context
+	 */
+	_handleDisallowedNode( node, context ) {
+		// Try inserting its children (strip the parent).
+		if ( node instanceof Element ) {
+			this.handleNodes( node.getChildren(), context );
+		}
+		// Try autoparagraphing.
+		else {
+			this._tryAutoparagraphing( node, context );
+		}
+	}
+
+	/**
+	 * @param {engine.model.Node} node The node to insert.
+	 */
+	_insert( node ) {
+		/* istanbul ignore if */
+		if ( !this._checkIsAllowed( node, [ this.position.parent ] ) ) {
+			// Algorithm's correctness check. We should never end up here but it's good to know that we did.
+			// Note that it would often be a silent issue if we insert node in a place where it's not allowed.
+			log.error(
+				'insertcontent-wrong-position: The node cannot be inserted on the given position.',
+				{ node, position: this.position }
+			);
+
+			return;
+		}
+
+		const livePos = LivePosition.createFromPosition( this.position );
+
+		this.batch.insert( this.position, node );
+
+		this.position = Position.createFromPosition( livePos );
+		livePos.detach();
+
+		// The last inserted object should be selected because we can't put a collapsed selection after it.
+		if ( this._checkIsObject( node ) && !this.schema.check( { name: '$text', inside: [ this.position.parent ] } ) ) {
+			this.nodeToSelect = node;
+		} else {
+			this.nodeToSelect = null;
+		}
+	}
+
+	/**
+	 * @param {engine.model.Node} node The node which could potentially be merged.
+	 * @param {Object} context
+	 */
+	_mergeSiblingsOf( node, context ) {
+		if ( !( node instanceof Element ) ) {
+			return;
+		}
+
+		const mergeLeft = context.isFirst && ( node.previousSibling instanceof Element ) && this.canMergeWith.has( node.previousSibling );
+		const mergeRight = context.isLast && ( node.nextSibling instanceof Element ) && this.canMergeWith.has( node.nextSibling );
+		const mergePosLeft = LivePosition.createBefore( node );
+		const mergePosRight = LivePosition.createAfter( node );
+
+		if ( mergeLeft ) {
+			const position = LivePosition.createFromPosition( this.position );
+
+			this.batch.merge( mergePosLeft );
+
+			this.position = Position.createFromPosition( position );
+			position.detach();
+		}
+
+		if ( mergeRight ) {
+			let position;
+
+			/* istanbul ignore if */
+			if ( !this.position.isEqual( mergePosRight ) ) {
+				// Algorithm's correctness check. We should never end up here but it's good to know that we did.
+				// At this point the insertion position should be after the node we'll merge. If it isn't,
+				// it should need to be secured as in the left merge case.
+				log.error( 'insertcontent-wrong-position-on-merge: The insertion position should equal the merge position' );
+			}
+
+			// Move the position to the previous node, so it isn't moved to the graveyard on merge.
+			// <p>x</p>[]<p>y</p> => <p>x[]</p><p>y</p>
+			this.position = Position.createAt( mergePosRight.nodeBefore, 'end' );
+
+			// 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)
+			position = new LivePosition( this.position.root, this.position.path, 'STICKS_TO_PREVIOUS' );
+
+			this.batch.merge( mergePosRight );
+
+			this.position = Position.createFromPosition( position );
+			position.detach();
+		}
+
+		mergePosLeft.detach();
+		mergePosRight.detach();
+	}
+
+	/**
+	 * Tries wrapping the node in a new paragraph and inserting it this way.
+	 *
+	 * @param {engine.model.Node} node The node which needs to be autoparagraphed.
+	 * @param {Object} context
+	 */
+	_tryAutoparagraphing( node, context ) {
+		const paragraph = new Element( 'paragraph' );
+
+		// Do not autoparagraph if the paragraph won't be allowed there,
+		// cause that would lead to an infinite loop. The paragraph would be rejected in
+		// the next _handleNode() call and we'd be here again.
+		if ( this._getAllowedIn( paragraph, this.position.parent ) && this._checkIsAllowed( node, [ paragraph ] ) ) {
+			paragraph.appendChildren( node );
+
+			this._handleNode( paragraph, context );
+		}
+	}
+
+	/**
+	 * @param {engine.model.Node} node
+	 */
+	_checkAndSplitToAllowedPosition( node ) {
+		const allowedIn = this._getAllowedIn( node, this.position.parent );
+
+		if ( !allowedIn ) {
+			return false;
+		}
+
+		while ( allowedIn != this.position.parent ) {
+			if ( this.position.isAtStart ) {
+				const parent = this.position.parent;
+				this.position = Position.createBefore( parent );
+
+				// Special case – parent is empty (<p>^</p>) so isAtStart == isAtEnd == true.
+				// We can remove the element after moving selection out of it.
+				if ( parent.isEmpty ) {
+					this.batch.remove( parent );
+				}
+			} else if ( this.position.isAtEnd ) {
+				this.position = Position.createAfter( this.position.parent );
+			} else {
+				const tempPos = Position.createAfter( this.position.parent );
+
+				this.batch.split( this.position );
+
+				this.position = tempPos;
+
+				this.canMergeWith.add( this.position.nodeAfter );
+			}
+		}
+
+		return true;
+	}
+
+	/**
+	 * Gets the element in which the given node is allowed. It checks the passed element and all its ancestors.
+	 *
+	 * @param {engine.model.Node} node The node to check.
+	 * @param {engine.model.Element} element The element in which the node's correctness should be checked.
+	 * @returns {engine.model.Element|null}
+	 */
+	_getAllowedIn( node, element ) {
+		if ( this._checkIsAllowed( node, [ element ] ) ) {
+			return element;
+		}
+
+		if ( element.parent ) {
+			return this._getAllowedIn( node, element.parent );
+		}
+
+		return null;
+	}
+
+	/**
+	 * Check whether the given node is allowed in the specified schema path.
+	 *
+	 * @param {engine.model.Node} node
+	 * @param {engine.model.SchemaPath} path
+	 */
+	_checkIsAllowed( node, path ) {
+		return this.schema.check( {
+			name: this._getNodeSchemaName( node ),
+			attributes: Array.from( node.getAttributeKeys() ),
+			inside: path
+		} );
+	}
+
+	/**
+	 * Checks wether according to the schema this is an object type element.
+	 *
+	 * @param {engine.model.Node} node The node to check.
+	 */
+	_checkIsObject( node ) {
+		return this.schema.objects.has( this._getNodeSchemaName( node ) );
+	}
+
+	/**
+	 * Gets a name under which we should check this node in the schema.
+	 *
+	 * @param {engine.model.Node} node The node.
+	 */
+	_getNodeSchemaName( node ) {
+		if ( node instanceof Text ) {
+			return '$text';
+		}
+
+		return node.name;
+	}
+}

+ 24 - 23
packages/ckeditor5-engine/src/conversion/viewconversiondispatcher.js

@@ -83,14 +83,14 @@ import extend from '../../utils/lib/lodash/extend.js';
  *		// is going to be appended directly to a '$root' element, use that in `context`.
  *		viewDispatcher.convert( viewDocumentFragment, { context: [ '$root' ] } );
  *
- * Before each conversion process, `ViewConversionDispatcher` fires {@link engine.conversion.ViewConversionDispatcher.viewCleanup}
+ * Before each conversion process, `ViewConversionDispatcher` fires {@link engine.conversion.ViewConversionDispatcher#viewCleanup}
  * event which can be used to prepare tree view for conversion.
  *
  * @mixes utils.EmitterMixin
- * @fires engine.conversion.ViewConversionDispatcher.viewCleanup
- * @fires engine.conversion.ViewConversionDispatcher.element
- * @fires engine.conversion.ViewConversionDispatcher.text
- * @fires engine.conversion.ViewConversionDispatcher.documentFragment
+ * @fires engine.conversion.ViewConversionDispatcher#viewCleanup
+ * @fires engine.conversion.ViewConversionDispatcher#element
+ * @fires engine.conversion.ViewConversionDispatcher#text
+ * @fires engine.conversion.ViewConversionDispatcher#documentFragment
  *
  * @memberOf engine.conversion
  */
@@ -119,12 +119,12 @@ export default class ViewConversionDispatcher {
 	/**
 	 * Starts the conversion process. The entry point for the conversion.
 	 *
-	 * @fires engine.conversion.ViewConversionDispatcher.element
-	 * @fires engine.conversion.ViewConversionDispatcher.text
-	 * @fires engine.conversion.ViewConversionDispatcher.documentFragment
+	 * @fires engine.conversion.ViewConversionDispatcher#element
+	 * @fires engine.conversion.ViewConversionDispatcher#text
+	 * @fires engine.conversion.ViewConversionDispatcher#documentFragment
 	 * @param {engine.view.DocumentFragment|engine.view.Element} viewItem Part of the view to be converted.
 	 * @param {Object} [additionalData] Additional data to be passed in `data` argument when firing `ViewConversionDispatcher`
-	 * events. See also {@link engine.conversion.ViewConversionDispatcher.element element event}.
+	 * events. See also {@link engine.conversion.ViewConversionDispatcher#element element event}.
 	 * @returns {engine.model.DocumentFragment} Model document fragment that is a result of the conversion process.
 	 */
 	convert( viewItem, additionalData = {} ) {
@@ -171,7 +171,7 @@ export default class ViewConversionDispatcher {
 	/**
 	 * Fired before the first conversion event, at the beginning of view to model conversion process.
 	 *
-	 * @event engine.conversion.ViewConversionDispatcher.viewCleanup
+	 * @event engine.conversion.ViewConversionDispatcher#viewCleanup
 	 * @param {engine.view.DocumentFragment|engine.view.Element} viewItem Part of the view to be converted.
 	 */
 
@@ -182,7 +182,7 @@ export default class ViewConversionDispatcher {
 	 * `element:<elementName>` where `elementName` is the name of converted element. This way listeners may listen to
 	 * all elements conversion or to conversion of specific elements.
 	 *
-	 * @event engine.conversion.ViewConversionDispatcher.element
+	 * @event engine.conversion.ViewConversionDispatcher#element
 	 * @param {Object} data Object containing conversion input and a placeholder for conversion output and possibly other
 	 * values (see {@link engine.conversion.ViewConversionDispatcher#convert}). Keep in mind that this object is shared
 	 * by reference between all callbacks that will be called. This means that callbacks can add their own values if needed,
@@ -190,6 +190,7 @@ export default class ViewConversionDispatcher {
 	 * @param {engine.view.Element} data.input Converted element.
 	 * @param {*} data.output The current state of conversion result. Every change to converted element should
 	 * be reflected by setting or modifying this property.
+	 * @param {engine.model.SchemaPath} data.context The conversion context.
 	 * @param {engine.conversion.ViewConsumable} consumable Values to consume.
 	 * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ViewConversionDispatcher` constructor.
 	 * Besides of properties passed in constructor, it also has `convertItem` and `convertChildren` methods which are references
@@ -201,15 +202,15 @@ export default class ViewConversionDispatcher {
 	/**
 	 * Fired when {@link engine.view.Text} is converted.
 	 *
-	 * @event engine.conversion.ViewConversionDispatcher.text
-	 * @see engine.conversion.ViewConversionDispatcher.element
+	 * @event engine.conversion.ViewConversionDispatcher#text
+	 * @see engine.conversion.ViewConversionDispatcher#element
 	 */
 
 	/**
 	 * Fired when {@link engine.view.DocumentFragment} is converted.
 	 *
-	 * @event engine.conversion.ViewConversionDispatcher.documentFragment
-	 * @see engine.conversion.ViewConversionDispatcher.element
+	 * @event engine.conversion.ViewConversionDispatcher#documentFragment
+	 * @see engine.conversion.ViewConversionDispatcher#element
 	 */
 }
 
@@ -233,13 +234,13 @@ mix( ViewConversionDispatcher, EmitterMixin );
  *
  * @memberOf engine.conversion.ViewConversionApi
  * @function covertItem
- * @fires engine.conversion.ViewConversionDispatcher.element
- * @fires engine.conversion.ViewConversionDispatcher.text
- * @fires engine.conversion.ViewConversionDispatcher.documentFragment
+ * @fires engine.conversion.ViewConversionDispatcher#element
+ * @fires engine.conversion.ViewConversionDispatcher#text
+ * @fires engine.conversion.ViewConversionDispatcher#documentFragment
  * @param {engine.view.DocumentFragment|engine.view.Element|engine.view.Text} input Item to convert.
  * @param {engine.conversion.ViewConsumable} consumable Values to consume.
  * @param {Object} [additionalData] Additional data to be passed in `data` argument when firing `ViewConversionDispatcher`
- * events. See also {@link engine.conversion.ViewConversionDispatcher.element element event}.
+ * events. See also {@link engine.conversion.ViewConversionDispatcher#element element event}.
  * @returns {*} The result of item conversion, created and modified by callbacks attached to fired event.
  */
 
@@ -248,12 +249,12 @@ mix( ViewConversionDispatcher, EmitterMixin );
  *
  * @memberOf engine.conversion.ViewConversionApi
  * @function convertChildren
- * @fires engine.conversion.ViewConversionDispatcher.element
- * @fires engine.conversion.ViewConversionDispatcher.text
- * @fires engine.conversion.ViewConversionDispatcher.documentFragment
+ * @fires engine.conversion.ViewConversionDispatcher#element
+ * @fires engine.conversion.ViewConversionDispatcher#text
+ * @fires engine.conversion.ViewConversionDispatcher#documentFragment
  * @param {engine.view.DocumentFragment|engine.view.Element} input Item which children will be converted.
  * @param {engine.conversion.ViewConsumable} consumable Values to consume.
  * @param {Object} [additionalData] Additional data to be passed in `data` argument when firing `ViewConversionDispatcher`
- * events. See also {@link engine.conversion.ViewConversionDispatcher.element element event}.
+ * events. See also {@link engine.conversion.ViewConversionDispatcher#element element event}.
  * @returns {Array.<*>} Array containing results of conversion of all children of given item.
  */

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

@@ -260,6 +260,8 @@ export function stringify( node, selectionOrPositionOrRange = null ) {
  * @param {Object} options Additional configuration.
  * @param {Array<Object>} [options.selectionAttributes] List of attributes which will be passed to the selection.
  * @param {Boolean} [options.lastRangeBackward=false] If set to true last range will be added as backward.
+ * @param {engine.model.SchemaPath} [options.context=[ '$root' ]] The conversion context.
+ * If not provided default `[ '$root' ]` will be used.
  * @returns {engine.model.Element|engine.model.Text|engine.model.DocumentFragment|Object} Returns parsed model node or
  * object with two fields `model` and `selection` when selection ranges were included in data to parse.
  */
@@ -294,7 +296,7 @@ export function parse( data, schema, options = {} ) {
 	viewToModel.on( 'text', convertToModelText() );
 
 	// Convert view to model.
-	let model = viewToModel.convert( viewDocumentFragment.root, { context: [ '$root' ] } );
+	let model = viewToModel.convert( viewDocumentFragment.root, { context: options.context || [ '$root' ] } );
 
 	// If root DocumentFragment contains only one element - return that element.
 	if ( model instanceof ModelDocumentFragment && model.childCount == 1 ) {

+ 17 - 0
packages/ckeditor5-engine/src/model/composer/deletecontents.js

@@ -5,6 +5,7 @@
 
 import LivePosition from '../liveposition.js';
 import Position from '../position.js';
+import Element from '../element.js';
 import compareArrays from '../../../utils/comparearrays.js';
 
 /**
@@ -58,5 +59,21 @@ export default function deleteContents( batch, selection, options = {} ) {
 
 	selection.collapse( startPos );
 
+	// 3. Autoparagraphing.
+	// 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( batch.document, startPos ) ) {
+		const paragraph = new Element( 'paragraph' );
+		batch.insert( startPos, paragraph );
+
+		selection.collapse( paragraph );
+	}
+
 	endPos.detach();
 }
+
+function shouldAutoparagraph( doc, position ) {
+	const isTextAllowed = doc.schema.check( { name: '$text', inside: position } );
+	const isParagraphAllowed = doc.schema.check( { name: 'paragraph', inside: position } );
+
+	return !isTextAllowed && isParagraphAllowed;
+}

+ 16 - 0
packages/ckeditor5-engine/src/model/schema.js

@@ -251,6 +251,15 @@ export default class Schema {
 	 */
 	constructor() {
 		/**
+		 * Names of elements which have "object" nature. This means that these
+		 * elements should be treated as whole, never merged, can be selected from outside, etc.
+		 * Just like images, placeholder widgets, etc.
+		 *
+		 * @member {Set.<String>} engine.model.Schema#objects
+		 */
+		this.objects = new Set();
+
+		/**
 		 * Schema items registered in the schema.
 		 *
 		 * @private
@@ -274,6 +283,13 @@ export default class Schema {
 
 		this.allow( { name: '$block', inside: '$root' } );
 		this.allow( { name: '$inline', inside: '$block' } );
+
+		// TMP!
+		// Create an "all allowed" context in the schema for processing the pasted content.
+		// Read: https://github.com/ckeditor/ckeditor5-engine/issues/638#issuecomment-255086588
+
+		this.registerItem( '$clipboardHolder', '$root' );
+		this.allow( { name: '$inline', inside: '$clipboardHolder' } );
 	}
 
 	/**

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

@@ -17,7 +17,7 @@ import isPlainObject from '../../utils/lib/lodash/isPlainObject.js';
  * class or {@link engine.view.AttributeElement}.
  *
  * Note that for view elements which are not created from model, like elements from mutations, paste or
- * {@link engine.DataController#set data.set} it is not possible to define the type of the element, so
+ * {@link engine.controller.DataController#set data.set} it is not possible to define the type of the element, so
  * these will be instances of the {@link engine.view.Element}.
  *
  * @memberOf engine.view

+ 36 - 1
packages/ckeditor5-engine/tests/datacontroller.js

@@ -6,12 +6,15 @@
 /* bender-tags: view */
 
 import ModelDocument from '/ckeditor5/engine/model/document.js';
-import DataController from '/ckeditor5/engine/datacontroller.js';
+import DataController from '/ckeditor5/engine/controller/datacontroller.js';
 import HtmlDataProcessor from '/ckeditor5/engine/dataprocessor/htmldataprocessor.js';
 
 import buildViewConverter  from '/ckeditor5/engine/conversion/buildviewconverter.js';
 import buildModelConverter  from '/ckeditor5/engine/conversion/buildmodelconverter.js';
 
+import ViewDocumentFragment from '/ckeditor5/engine/view/documentfragment.js';
+import ViewText from '/ckeditor5/engine/view/text.js';
+
 import { getData, setData, stringify, parse } from '/ckeditor5/engine/dev-utils/model.js';
 
 import count from '/ckeditor5/utils/count.js';
@@ -37,6 +40,20 @@ describe( 'DataController', () => {
 
 			expect( data.processor ).to.be.undefined;
 		} );
+
+		it( 'should add insertContent listener', () => {
+			const batch = modelDocument.batch();
+			const content = new ViewDocumentFragment( [ new ViewText( 'x' ) ] );
+
+			schema.registerItem( 'paragraph', '$block' );
+
+			setData( modelDocument, '<paragraph>a[]b</paragraph>' );
+
+			data.fire( 'insert', { content, selection: modelDocument.selection, batch } );
+
+			expect( getData( modelDocument ) ).to.equal( '<paragraph>ax[]b</paragraph>' );
+			expect( batch.deltas.length ).to.be.above( 0 );
+		} );
 	} );
 
 	describe( 'parse', () => {
@@ -252,4 +269,22 @@ describe( 'DataController', () => {
 			expect( data ).to.respondTo( 'destroy' );
 		} );
 	} );
+
+	describe( 'insert', () => {
+		it( 'should fire the insert event', () => {
+			const spy = sinon.spy();
+			const content = new ViewDocumentFragment( [ new ViewText( 'x' ) ] );
+			const batch = modelDocument.batch();
+
+			data.on( 'insert', spy );
+
+			data.insert( content, modelDocument.selection, batch );
+
+			expect( spy.args[ 0 ][ 1 ] ).to.deep.equal( {
+				batch: batch,
+				selection: modelDocument.selection,
+				content: content
+			} );
+		} );
+	} );
 } );

+ 14 - 4
packages/ckeditor5-engine/tests/editingcontroller.js

@@ -6,7 +6,9 @@
 /* globals setTimeout, Range, document */
 /* bender-tags: view */
 
-import EditingController from '/ckeditor5/engine/editingcontroller.js';
+import EmitterMixin from '/ckeditor5/utils/emittermixin.js';
+
+import EditingController from '/ckeditor5/engine/controller/editingcontroller.js';
 
 import ViewDocument from '/ckeditor5/engine/view/document.js';
 
@@ -103,15 +105,18 @@ describe( 'EditingController', () => {
 	} );
 
 	describe( 'conversion', () => {
-		let model, modelRoot, viewRoot, domRoot, editing;
+		let model, modelRoot, viewRoot, domRoot, editing, listener;
 
 		before( () => {
+			listener = Object.create( EmitterMixin );
+
 			model = new ModelDocument();
 			modelRoot = model.createRoot();
 
 			editing = new EditingController( model );
 
-			domRoot = document.getElementById( 'editor' );
+			domRoot = document.createElement( 'div' );
+			document.body.appendChild( domRoot );
 			viewRoot = editing.createRoot( domRoot );
 
 			model.schema.registerItem( 'paragraph', '$block' );
@@ -120,6 +125,11 @@ describe( 'EditingController', () => {
 			buildModelConverter().for( editing.modelToView ).fromElement( 'div' ).toElement( 'div' );
 		} );
 
+		after( () => {
+			document.body.removeChild( domRoot );
+			listener.stopListening();
+		} );
+
 		beforeEach( () => {
 			// Note: The below code is highly overcomplicated due to #455.
 			model.selection.removeAllRanges();
@@ -182,7 +192,7 @@ describe( 'EditingController', () => {
 		} );
 
 		it( 'should convert selection from view to model', ( done ) => {
-			editing.view.on( 'selectionChange', () => {
+			listener.listenTo( editing.view, 'selectionChange', () => {
 				setTimeout( () => {
 					expect( getModelData( model ) ).to.equal(
 						'<paragraph>foo</paragraph>' +

+ 555 - 0
packages/ckeditor5-engine/tests/controller/insert.js

@@ -0,0 +1,555 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: model */
+
+import Document from '/ckeditor5/engine/model/document.js';
+import DataController from '/ckeditor5/engine/controller/datacontroller.js';
+import insert from '/ckeditor5/engine/controller/insert.js';
+
+import ViewDocumentFragment from '/ckeditor5/engine/view/documentfragment.js';
+import ViewText from '/ckeditor5/engine/view/text.js';
+import ModelDocumentFragment from '/ckeditor5/engine/model/documentfragment.js';
+import Text from '/ckeditor5/engine/model/text.js';
+
+import { setData, getData, parse } from '/ckeditor5/engine/dev-utils/model.js';
+
+describe( 'DataController', () => {
+	let doc, dataController;
+
+	describe( 'insert', () => {
+		it( 'uses the passed batch', () => {
+			doc = new Document();
+			doc.createRoot();
+			doc.schema.allow( { name: '$text', inside: '$root' } );
+
+			dataController = new DataController( doc );
+
+			const batch = doc.batch();
+
+			setData( doc, 'x[]x' );
+
+			insert( dataController, new ViewDocumentFragment( [ new ViewText( 'a' ) ] ), doc.selection, batch );
+
+			expect( batch.deltas.length ).to.be.above( 0 );
+		} );
+
+		describe( 'in simple scenarios', () => {
+			beforeEach( () => {
+				doc = new Document();
+				doc.createRoot();
+
+				dataController = new DataController( doc );
+
+				const schema = doc.schema;
+
+				schema.registerItem( 'image', '$inline' );
+				schema.registerItem( 'disallowedElement' );
+
+				schema.allow( { name: '$text', inside: '$root' } );
+				schema.allow( { name: 'image', inside: '$root' } );
+				// Otherwise it won't be passed to the temporary model fragment used inside insert().
+				schema.allow( { name: 'disallowedElement', inside: '$clipboardHolder' } );
+
+				schema.objects.add( 'image' );
+			} );
+
+			it( 'inserts one text node', () => {
+				setData( doc, 'f[]oo' );
+				insertHelper( 'xyz' );
+				expect( getData( doc ) ).to.equal( 'fxyz[]oo' );
+			} );
+
+			it( 'inserts one text node (at the end)', () => {
+				setData( doc, 'foo[]' );
+				insertHelper( 'xyz' );
+				expect( getData( doc ) ).to.equal( 'fooxyz[]' );
+			} );
+
+			it( 'inserts an element', () => {
+				setData( doc, 'f[]oo' );
+				insertHelper( '<image></image>' );
+				expect( getData( doc ) ).to.equal( 'f<image></image>[]oo' );
+			} );
+
+			it( 'inserts a text and an element', () => {
+				setData( doc, 'f[]oo' );
+				insertHelper( 'xyz<image></image>' );
+				expect( getData( doc ) ).to.equal( 'fxyz<image></image>[]oo' );
+			} );
+
+			it( 'strips a disallowed element', () => {
+				setData( doc, 'f[]oo' );
+				insertHelper( '<disallowedElement>xyz</disallowedElement>' );
+				expect( getData( doc ) ).to.equal( 'fxyz[]oo' );
+			} );
+
+			it( 'deletes selection before inserting the content', () => {
+				setData( doc, 'f[abc]oo' );
+				insertHelper( 'x' );
+				expect( getData( doc ) ).to.equal( 'fx[]oo' );
+			} );
+
+			describe( 'spaces handling', () => {
+				// Note: spaces in the view are not encoded like in the DOM, so subsequent spaces must be
+				// inserted into the model as is. The conversion to nbsps happen on view<=>DOM conversion.
+
+				it( 'inserts one space', () => {
+					setData( doc, 'f[]oo' );
+					insertHelper( new Text( ' ' ) );
+					expect( getData( doc ) ).to.equal( 'f []oo' );
+				} );
+
+				it( 'inserts three spaces', () => {
+					setData( doc, 'f[]oo' );
+					insertHelper( new Text( '   ' ) );
+					expect( getData( doc ) ).to.equal( 'f   []oo' );
+				} );
+
+				it( 'inserts spaces at the end', () => {
+					setData( doc, 'foo[]' );
+					insertHelper( new Text( '   ' ) );
+					expect( getData( doc ) ).to.equal( 'foo   []' );
+				} );
+
+				it( 'inserts one nbsp', () => {
+					setData( doc, 'f[]oo' );
+					insertHelper( new Text( '\u200a' ) );
+					expect( getData( doc ) ).to.equal( 'f\u200a[]oo' );
+				} );
+
+				it( 'inserts word surrounded by spaces', () => {
+					setData( doc, 'f[]oo' );
+					insertHelper( new Text( ' xyz  ' ) );
+					expect( getData( doc ) ).to.equal( 'f xyz  []oo' );
+				} );
+			} );
+		} );
+
+		describe( 'in blocks', () => {
+			beforeEach( () => {
+				doc = new Document();
+				doc.createRoot();
+
+				dataController = new DataController( doc );
+
+				const schema = doc.schema;
+
+				schema.registerItem( 'paragraph', '$block' );
+				schema.registerItem( 'heading1', '$block' );
+				schema.registerItem( 'heading2', '$block' );
+				schema.registerItem( 'blockWidget' );
+				schema.registerItem( 'inlineWidget' );
+				schema.registerItem( 'listItem', '$block' );
+
+				schema.allow( { name: 'blockWidget', inside: '$root' } );
+				schema.allow( { name: 'inlineWidget', inside: '$block' } );
+				schema.allow( { name: 'inlineWidget', inside: '$clipboardHolder' } );
+				schema.allow( {
+					name: 'listItem',
+					inside: '$root',
+					attributes: [ 'type', 'indent' ]
+				} );
+				schema.requireAttributes( 'listItem', [ 'type', 'indent' ] );
+
+				schema.objects.add( 'blockWidget' );
+				schema.objects.add( 'inlineWidget' );
+			} );
+
+			it( 'inserts one text node', () => {
+				setData( doc, '<paragraph>f[]oo</paragraph>' );
+				insertHelper( 'xyz' );
+				expect( getData( doc ) ).to.equal( '<paragraph>fxyz[]oo</paragraph>' );
+			} );
+
+			it( 'inserts one text node to fully selected paragraph', () => {
+				setData( doc, '<paragraph>[foo]</paragraph>' );
+				insertHelper( 'xyz' );
+				expect( getData( doc ) ).to.equal( '<paragraph>xyz[]</paragraph>' );
+			} );
+
+			it( 'inserts one text node to fully selected paragraphs (from outside)', () => {
+				setData( doc, '[<paragraph>foo</paragraph><paragraph>bar</paragraph>]' );
+				insertHelper( 'xyz' );
+				expect( getData( doc ) ).to.equal( '<paragraph>xyz[]</paragraph>' );
+			} );
+
+			it( 'merges two blocks before inserting content (p+p)', () => {
+				setData( doc, '<paragraph>fo[o</paragraph><paragraph>b]ar</paragraph>' );
+				insertHelper( 'xyz' );
+				expect( getData( doc ) ).to.equal( '<paragraph>foxyz[]ar</paragraph>' );
+			} );
+
+			it( 'inserts inline widget and text', () => {
+				setData( doc, '<paragraph>f[]oo</paragraph>' );
+				insertHelper( 'xyz<inlineWidget></inlineWidget>' );
+				expect( getData( doc ) ).to.equal( '<paragraph>fxyz<inlineWidget></inlineWidget>[]oo</paragraph>' );
+			} );
+
+			// Note: In CKEditor 4 the blocks are not merged, but to KISS we're merging here
+			// because that's what deleteContent() does.
+			it( 'merges two blocks before inserting content (h+p)', () => {
+				setData( doc, '<heading1>fo[o</heading1><paragraph>b]ar</paragraph>' );
+				insertHelper( 'xyz' );
+				expect( getData( doc ) ).to.equal( '<heading1>foxyz[]ar</heading1>' );
+			} );
+
+			describe( 'block to block handling', () => {
+				it( 'inserts one paragraph', () => {
+					setData( doc, '<paragraph>f[]oo</paragraph>' );
+					insertHelper( '<paragraph>xyz</paragraph>' );
+					expect( getData( doc ) ).to.equal( '<paragraph>fxyz[]oo</paragraph>' );
+				} );
+
+				it( 'inserts one paragraph (at the end)', () => {
+					setData( doc, '<paragraph>foo[]</paragraph>' );
+					insertHelper( '<paragraph>xyz</paragraph>' );
+					expect( getData( doc ) ).to.equal( '<paragraph>fooxyz[]</paragraph>' );
+				} );
+
+				it( 'inserts one paragraph into an empty paragraph', () => {
+					setData( doc, '<paragraph>[]</paragraph>' );
+					insertHelper( '<paragraph>xyz</paragraph>' );
+					expect( getData( doc ) ).to.equal( '<paragraph>xyz[]</paragraph>' );
+				} );
+
+				it( 'inserts one block into a fully selected content', () => {
+					setData( doc, '<heading1>[foo</heading1><paragraph>bar]</paragraph>' );
+					insertHelper( '<heading2>xyz</heading2>' );
+					expect( getData( doc ) ).to.equal( '<heading2>xyz[]</heading2>' );
+				} );
+
+				it( 'inserts one heading', () => {
+					setData( doc, '<paragraph>f[]oo</paragraph>' );
+					insertHelper( '<heading1>xyz</heading1>' );
+					expect( getData( doc ) ).to.equal( '<paragraph>fxyz[]oo</paragraph>' );
+				} );
+
+				it( 'inserts two headings', () => {
+					setData( doc, '<paragraph>f[]oo</paragraph>' );
+					insertHelper( '<heading1>xxx</heading1><heading1>yyy</heading1>' );
+					expect( getData( doc ) ).to.equal( '<paragraph>fxxx</paragraph><heading1>yyy[]oo</heading1>' );
+				} );
+
+				it( 'inserts one object', () => {
+					setData( doc, '<paragraph>f[]oo</paragraph>' );
+					insertHelper( '<blockWidget></blockWidget>' );
+					expect( getData( doc ) ).to.equal( '<paragraph>f</paragraph>[<blockWidget></blockWidget>]<paragraph>oo</paragraph>' );
+				} );
+
+				it( 'inserts one object (at the end)', () => {
+					setData( doc, '<paragraph>foo[]</paragraph>' );
+					insertHelper( '<blockWidget></blockWidget>' );
+					expect( getData( doc ) ).to.equal( '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]' );
+				} );
+
+				it( 'inserts one object (at the beginning)', () => {
+					setData( doc, '<paragraph>[]bar</paragraph>' );
+					insertHelper( '<blockWidget></blockWidget>' );
+					expect( getData( doc ) ).to.equal( '[<blockWidget></blockWidget>]<paragraph>bar</paragraph>' );
+				} );
+
+				it( 'inserts one list item', () => {
+					setData( doc, '<paragraph>f[]oo</paragraph>' );
+					insertHelper( '<listItem indent="0" type="bulleted">xyz</listItem>' );
+					expect( getData( doc ) ).to.equal( '<paragraph>fxyz[]oo</paragraph>' );
+				} );
+
+				it( 'inserts list item to empty element', () => {
+					setData( doc, '<paragraph>[]</paragraph>' );
+					insertHelper( '<listItem indent="0" type="bulleted">xyz</listItem>' );
+					expect( getData( doc ) ).to.equal( '<listItem indent="0" type="bulleted">xyz[]</listItem>' );
+				} );
+
+				it( 'inserts three list items at the end of paragraph', () => {
+					setData( doc, '<paragraph>foo[]</paragraph>' );
+					insertHelper(
+						'<listItem indent="0" type="bulleted">xxx</listItem>' +
+						'<listItem indent="0" type="bulleted">yyy</listItem>' +
+						'<listItem indent="0" type="bulleted">zzz</listItem>'
+					);
+					expect( getData( doc ) ).to.equal(
+						'<paragraph>fooxxx</paragraph>' +
+						'<listItem indent="0" type="bulleted">yyy</listItem>' +
+						'<listItem indent="0" type="bulleted">zzz[]</listItem>'
+					);
+				} );
+
+				it( 'inserts two list items to an empty paragraph', () => {
+					setData( doc, '<paragraph>a</paragraph><paragraph>[]</paragraph><paragraph>b</paragraph>' );
+					insertHelper(
+						'<listItem indent="0" type="bulleted">xxx</listItem>' +
+						'<listItem indent="0" type="bulleted">yyy</listItem>'
+					);
+					expect( getData( doc ) ).to.equal(
+						'<paragraph>a</paragraph>' +
+						'<listItem indent="0" type="bulleted">xxx</listItem>' +
+						'<listItem indent="0" type="bulleted">yyy[]</listItem>' +
+						'<paragraph>b</paragraph>'
+					);
+				} );
+			} );
+
+			describe( 'mixed content to block', () => {
+				it( 'inserts text + paragraph', () => {
+					setData( doc, '<paragraph>f[]oo</paragraph>' );
+					insertHelper( 'xxx<paragraph>yyy</paragraph>' );
+					expect( getData( doc ) ).to.equal( '<paragraph>fxxx</paragraph><paragraph>yyy[]oo</paragraph>' );
+				} );
+
+				it( 'inserts text + inlineWidget + text + paragraph', () => {
+					setData( doc, '<paragraph>f[]oo</paragraph>' );
+					insertHelper( 'xxx<inlineWidget></inlineWidget>yyy<paragraph>zzz</paragraph>' );
+					expect( getData( doc ) ).to.equal( '<paragraph>fxxx<inlineWidget></inlineWidget>yyy</paragraph><paragraph>zzz[]oo</paragraph>' );
+				} );
+
+				it( 'inserts text + paragraph (at the beginning)', () => {
+					setData( doc, '<paragraph>[]foo</paragraph>' );
+					insertHelper( 'xxx<paragraph>yyy</paragraph>' );
+					expect( getData( doc ) ).to.equal( '<paragraph>xxx</paragraph><paragraph>yyy[]foo</paragraph>' );
+				} );
+
+				it( 'inserts text + paragraph (at the end)', () => {
+					setData( doc, '<paragraph>foo[]</paragraph>' );
+					insertHelper( 'xxx<paragraph>yyy</paragraph>' );
+					expect( getData( doc ) ).to.equal( '<paragraph>fooxxx</paragraph><paragraph>yyy[]</paragraph>' );
+				} );
+
+				it( 'inserts paragraph + text', () => {
+					setData( doc, '<paragraph>f[]oo</paragraph>' );
+					insertHelper( '<paragraph>yyy</paragraph>xxx' );
+					expect( getData( doc ) ).to.equal( '<paragraph>fyyy</paragraph><paragraph>xxx[]oo</paragraph>' );
+				} );
+
+				// This is the expected result, but it was so hard to achieve at this stage that I
+				// decided to go with the what the next test represents.
+				// it( 'inserts paragraph + text + inlineWidget + text', () => {
+				// 	setData( doc, '<paragraph>f[]oo</paragraph>' );
+				// 	insertHelper( '<paragraph>yyy</paragraph>xxx<inlineWidget></inlineWidget>zzz' );
+				// 	expect( getData( doc ) )
+				// 		.to.equal( '<paragraph>fyyy</paragraph><paragraph>xxx<inlineWidget></inlineWidget>zzz[]oo</paragraph>' );
+				// } );
+
+				// See the comment above.
+				it( 'inserts paragraph + text + inlineWidget + text', () => {
+					setData( doc, '<paragraph>f[]oo</paragraph>' );
+					insertHelper( '<paragraph>yyy</paragraph>xxx<inlineWidget></inlineWidget>zzz' );
+					expect( getData( doc ) ).to.equal(
+						'<paragraph>fyyy</paragraph><paragraph>xxx</paragraph>' +
+						'<paragraph><inlineWidget></inlineWidget></paragraph>' +
+						'<paragraph>zzz[]oo</paragraph>'
+					);
+				} );
+
+				it( 'inserts paragraph + text + paragraph', () => {
+					setData( doc, '<paragraph>f[]oo</paragraph>' );
+					insertHelper( '<paragraph>yyy</paragraph>xxx<paragraph>zzz</paragraph>' );
+					expect( getData( doc ) ).to.equal( '<paragraph>fyyy</paragraph><paragraph>xxx</paragraph><paragraph>zzz[]oo</paragraph>' );
+				} );
+
+				it( 'inserts paragraph + text (at the beginning)', () => {
+					setData( doc, '<paragraph>[]foo</paragraph>' );
+					insertHelper( '<paragraph>yyy</paragraph>xxx' );
+					expect( getData( doc ) ).to.equal( '<paragraph>yyy</paragraph><paragraph>xxx[]foo</paragraph>' );
+				} );
+
+				it( 'inserts paragraph + text (at the end)', () => {
+					setData( doc, '<paragraph>foo[]</paragraph>' );
+					insertHelper( '<paragraph>yyy</paragraph>xxx' );
+					expect( getData( doc ) ).to.equal( '<paragraph>fooyyy</paragraph><paragraph>xxx[]</paragraph>' );
+				} );
+
+				it( 'inserts text + heading', () => {
+					setData( doc, '<paragraph>f[]oo</paragraph>' );
+					insertHelper( 'xxx<heading1>yyy</heading1>' );
+					expect( getData( doc ) ).to.equal( '<paragraph>fxxx</paragraph><heading1>yyy[]oo</heading1>' );
+				} );
+
+				it( 'inserts paragraph + object', () => {
+					setData( doc, '<paragraph>f[]oo</paragraph>' );
+					insertHelper( '<paragraph>xxx</paragraph><blockWidget></blockWidget>' );
+					expect( getData( doc ) ).to.equal( '<paragraph>fxxx</paragraph>[<blockWidget></blockWidget>]<paragraph>oo</paragraph>' );
+				} );
+
+				it( 'inserts object + paragraph', () => {
+					setData( doc, '<paragraph>f[]oo</paragraph>' );
+					insertHelper( '<blockWidget></blockWidget><paragraph>xxx</paragraph>' );
+					expect( getData( doc ) ).to.equal( '<paragraph>f</paragraph><blockWidget></blockWidget><paragraph>xxx[]oo</paragraph>' );
+				} );
+			} );
+
+			describe( 'content over a block object', () => {
+				it( 'inserts text', () => {
+					setData( doc, '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]<paragraph>bar</paragraph>' );
+					insertHelper( 'xxx' );
+					expect( getData( doc ) ).to.equal( '<paragraph>foo</paragraph><paragraph>xxx[]</paragraph><paragraph>bar</paragraph>' );
+				} );
+
+				it( 'inserts paragraph', () => {
+					setData( doc, '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]<paragraph>bar</paragraph>' );
+					insertHelper( '<paragraph>xxx</paragraph>' );
+					expect( getData( doc ) ).to.equal( '<paragraph>foo</paragraph><paragraph>xxx[]</paragraph><paragraph>bar</paragraph>' );
+				} );
+
+				it( 'inserts text + paragraph', () => {
+					setData( doc, '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]<paragraph>bar</paragraph>' );
+					insertHelper( 'yyy<paragraph>xxx</paragraph>' );
+					expect( getData( doc ) )
+						.to.equal( '<paragraph>foo</paragraph><paragraph>yyy</paragraph><paragraph>xxx[]</paragraph><paragraph>bar</paragraph>' );
+				} );
+
+				it( 'inserts two blocks', () => {
+					setData( doc, '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]<paragraph>bar</paragraph>' );
+					insertHelper( '<heading1>xxx</heading1><paragraph>yyy</paragraph>' );
+					expect( getData( doc ) )
+						.to.equal( '<paragraph>foo</paragraph><heading1>xxx</heading1><paragraph>yyy[]</paragraph><paragraph>bar</paragraph>' );
+				} );
+
+				it( 'inserts block object', () => {
+					setData( doc, '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]<paragraph>bar</paragraph>' );
+					insertHelper( '<blockWidget></blockWidget>' );
+					// It's enough, don't worry.
+					expect( getData( doc ) ).to.equal( '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]<paragraph>bar</paragraph>' );
+				} );
+
+				it( 'inserts inline object', () => {
+					setData( doc, '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]<paragraph>bar</paragraph>' );
+					insertHelper( '<inlineWidget></inlineWidget>' );
+					expect( getData( doc ) )
+						.to.equal( '<paragraph>foo</paragraph><paragraph><inlineWidget></inlineWidget>[]</paragraph><paragraph>bar</paragraph>' );
+				} );
+			} );
+
+			describe( 'content over an inline object', () => {
+				it( 'inserts text', () => {
+					setData( doc, '<paragraph>foo[<inlineWidget></inlineWidget>]bar</paragraph>' );
+					insertHelper( 'xxx' );
+					expect( getData( doc ) ).to.equal( '<paragraph>fooxxx[]bar</paragraph>' );
+				} );
+
+				it( 'inserts paragraph', () => {
+					setData( doc, '<paragraph>foo[<inlineWidget></inlineWidget>]bar</paragraph>' );
+					insertHelper( '<paragraph>xxx</paragraph>' );
+					expect( getData( doc ) ).to.equal( '<paragraph>fooxxx[]bar</paragraph>' );
+				} );
+
+				it( 'inserts text + paragraph', () => {
+					setData( doc, '<paragraph>foo[<inlineWidget></inlineWidget>]bar</paragraph>' );
+					insertHelper( 'yyy<paragraph>xxx</paragraph>' );
+					expect( getData( doc ) ).to.equal( '<paragraph>fooyyy</paragraph><paragraph>xxx[]bar</paragraph>' );
+				} );
+
+				it( 'inserts two blocks', () => {
+					setData( doc, '<paragraph>foo[<inlineWidget></inlineWidget>]bar</paragraph>' );
+					insertHelper( '<heading1>xxx</heading1><paragraph>yyy</paragraph>' );
+					expect( getData( doc ) ).to.equal( '<paragraph>fooxxx</paragraph><paragraph>yyy[]bar</paragraph>' );
+				} );
+
+				it( 'inserts inline object', () => {
+					setData( doc, '<paragraph>foo[<inlineWidget></inlineWidget>]bar</paragraph>' );
+					insertHelper( '<inlineWidget></inlineWidget>' );
+					expect( getData( doc ) ).to.equal( '<paragraph>foo<inlineWidget></inlineWidget>[]bar</paragraph>' );
+				} );
+
+				it( 'inserts block object', () => {
+					setData( doc, '<paragraph>foo[<inlineWidget></inlineWidget>]bar</paragraph>' );
+					insertHelper( '<blockWidget></blockWidget>' );
+					expect( getData( doc ) ).to.equal( '<paragraph>foo</paragraph>[<blockWidget></blockWidget>]<paragraph>bar</paragraph>' );
+				} );
+			} );
+		} );
+
+		describe( 'filtering out', () => {
+			beforeEach( () => {
+				doc = new Document();
+				doc.createRoot();
+
+				dataController = new DataController( doc );
+
+				const schema = doc.schema;
+
+				schema.registerItem( 'paragraph', '$block' );
+
+				// Let's use table as an example of content which needs to be filtered out.
+				schema.registerItem( 'table' );
+				schema.registerItem( 'td' );
+				schema.registerItem( 'disallowedWidget' );
+
+				schema.allow( { name: 'table', inside: '$clipboardHolder' } );
+				schema.allow( { name: 'td', inside: '$clipboardHolder' } );
+				schema.allow( { name: '$block', inside: 'td' } );
+				schema.allow( { name: '$text', inside: 'td' } );
+
+				schema.allow( { name: 'disallowedWidget', inside: '$clipboardHolder' } );
+				schema.allow( { name: '$text', inside: 'disallowedWidget' } );
+				schema.objects.add( 'disallowedWidget' );
+			} );
+
+			it( 'filters out disallowed elements and leaves out the text', () => {
+				setData( doc, '<paragraph>f[]oo</paragraph>' );
+				insertHelper( '<table><td>xxx</td><td>yyy</td></table>' );
+				expect( getData( doc ) ).to.equal( '<paragraph>fxxxyyy[]oo</paragraph>' );
+			} );
+
+			it( 'filters out disallowed elements and leaves out the paragraphs', () => {
+				setData( doc, '<paragraph>f[]oo</paragraph>' );
+				insertHelper( '<table><td><paragraph>xxx</paragraph><paragraph>yyy</paragraph><paragraph>zzz</paragraph></td></table>' );
+				expect( getData( doc ) ).to.equal( '<paragraph>fxxx</paragraph><paragraph>yyy</paragraph><paragraph>zzz[]oo</paragraph>' );
+			} );
+
+			it( 'filters out disallowed objects', () => {
+				setData( doc, '<paragraph>f[]oo</paragraph>' );
+				insertHelper( '<disallowedWidget>xxx</disallowedWidget>' );
+				expect( getData( doc ) ).to.equal( '<paragraph>f[]oo</paragraph>' );
+			} );
+		} );
+
+		describe( 'special schema configurations', () => {
+			beforeEach( () => {
+				doc = new Document();
+				doc.createRoot();
+
+				dataController = new DataController( doc );
+			} );
+
+			it( 'should not break when autoparagraphing of text is not possible', () => {
+				const schema = doc.schema;
+
+				schema.registerItem( 'noTextAllowed' );
+				schema.registerItem( 'object' );
+
+				schema.allow( { name: 'noTextAllowed', inside: '$root' } );
+				schema.allow( { name: 'object', inside: 'noTextAllowed' } );
+
+				schema.objects.add( 'object' );
+
+				setData( doc, '<noTextAllowed>[<object></object>]</noTextAllowed>' );
+				insertHelper( 'foo' );
+				expect( getData( doc ) ).to.equal( '<noTextAllowed>[]</noTextAllowed>' );
+			} );
+		} );
+	} );
+
+	// @param {engine.model.Item|String} content
+	function insertHelper( content ) {
+		if ( typeof content == 'string' ) {
+			content = parse( content, doc.schema, {
+				context: [ '$clipboardHolder' ]
+			} );
+		}
+
+		if ( !( content instanceof ModelDocumentFragment ) ) {
+			content = new ModelDocumentFragment( [ content ] );
+		}
+
+		// Override the convertion so we get exactly the model that we defined in the content param.
+		// This way we avoid the need to write converters for everything we want to test.
+		dataController.viewToModel.convert = () => {
+			return content;
+		};
+
+		insert( dataController, new ViewDocumentFragment(), doc.selection );
+	}
+} );

+ 10 - 0
packages/ckeditor5-engine/tests/dev-utils/model.js

@@ -491,6 +491,16 @@ describe( 'model test utils', () => {
 			} ).to.throw( Error, `Element '$text' not allowed in context.` );
 		} );
 
+		it( 'converts data in the specified context', () => {
+			const doc = new Document();
+			doc.schema.registerItem( 'foo' );
+			doc.schema.allow( { name: '$text', inside: 'foo' } );
+
+			expect( () => {
+				parse( 'text', doc.schema, { context: [ 'foo' ] } );
+			} ).to.not.throw();
+		} );
+
 		describe( 'selection', () => {
 			test( 'sets collapsed selection in an element', {
 				data: '<a>[]</a>',

+ 229 - 81
packages/ckeditor5-engine/tests/model/composer/deletecontents.js

@@ -10,30 +10,22 @@ import deleteContents from '/ckeditor5/engine/model/composer/deletecontents.js';
 import { setData, getData } from '/ckeditor5/engine/dev-utils/model.js';
 
 describe( 'Delete utils', () => {
-	let document;
-
-	beforeEach( () => {
-		document = new Document();
-		document.createRoot();
-
-		const schema = document.schema;
-
-		// Note: We used short names instead of "image", "paragraph", etc. to make the tests shorter.
-		// We could use any random names in fact, but using HTML tags may explain the tests a bit better.
-		schema.registerItem( 'img', '$inline' );
-		schema.registerItem( 'p', '$block' );
-		schema.registerItem( 'h1', '$block' );
-		schema.registerItem( 'pchild' );
-
-		schema.allow( { name: 'pchild', inside: 'p' } );
-		schema.allow( { name: '$text', inside: '$root' } );
-		schema.allow( { name: 'img', inside: '$root' } );
-		schema.allow( { name: '$text', attributes: [ 'bold', 'italic' ] } );
-		schema.allow( { name: 'p', attributes: [ 'align' ] } );
-	} );
+	let doc;
 
 	describe( 'deleteContents', () => {
 		describe( 'in simple scenarios', () => {
+			beforeEach( () => {
+				doc = new Document();
+				doc.createRoot();
+
+				const schema = doc.schema;
+
+				schema.registerItem( 'image', '$inline' );
+
+				schema.allow( { name: '$text', inside: '$root' } );
+				schema.allow( { name: 'image', inside: '$root' } );
+			} );
+
 			test(
 				'does nothing on collapsed selection',
 				'f[]oo',
@@ -47,11 +39,11 @@ describe( 'Delete utils', () => {
 			);
 
 			it( 'deletes single character (backward selection)' , () => {
-				setData( document, 'f[o]o', { lastRangeBackward: true } );
+				setData( doc, 'f[o]o', { lastRangeBackward: true } );
 
-				deleteContents( document.batch(), document.selection );
+				deleteContents( doc.batch(), doc.selection );
 
-				expect( getData( document ) ).to.equal( 'f[]o' );
+				expect( getData( doc ) ).to.equal( 'f[]o' );
 			} );
 
 			test(
@@ -62,73 +54,92 @@ describe( 'Delete utils', () => {
 
 			test(
 				'deletes whole text between nodes',
-				'<img></img>[foo]<img></img>',
-				'<img></img>[]<img></img>'
+				'<image></image>[foo]<image></image>',
+				'<image></image>[]<image></image>'
 			);
 
 			test(
 				'deletes an element',
-				'x[<img></img>]y',
+				'x[<image></image>]y',
 				'x[]y'
 			);
 
 			test(
 				'deletes a bunch of nodes',
-				'w[x<img></img>y]z',
+				'w[x<image></image>y]z',
 				'w[]z'
 			);
 
 			test(
 				'does not break things when option.merge passed',
-				'w[x<img></img>y]z',
+				'w[x<image></image>y]z',
 				'w[]z',
 				{ merge: true }
 			);
 		} );
 
 		describe( 'with text attributes', () => {
+			beforeEach( () => {
+				doc = new Document();
+				doc.createRoot();
+
+				const schema = doc.schema;
+
+				schema.registerItem( 'image', '$inline' );
+				schema.registerItem( 'paragraph', '$block' );
+
+				schema.allow( { name: '$text', inside: '$root' } );
+				schema.allow( { name: '$text', attributes: [ 'bold', 'italic' ] } );
+			} );
+
 			it( 'deletes characters (first half has attrs)', () => {
-				setData( document, '<$text bold="true">fo[o</$text>b]ar', { selectionAttributes: {
+				setData( doc, '<$text bold="true">fo[o</$text>b]ar', { selectionAttributes: {
 					bold: true
 				} } );
 
-				deleteContents( document.batch(), document.selection );
+				deleteContents( doc.batch(), doc.selection );
 
-				expect( getData( document ) ).to.equal( '<$text bold="true">fo[]</$text>ar' );
-				expect( document.selection.getAttribute( 'bold' ) ).to.equal( true );
+				expect( getData( doc ) ).to.equal( '<$text bold="true">fo[]</$text>ar' );
+				expect( doc.selection.getAttribute( 'bold' ) ).to.equal( true );
 			} );
 
 			it( 'deletes characters (2nd half has attrs)', () => {
-				setData( document, 'fo[o<$text bold="true">b]ar</$text>', { selectionAttributes: {
+				setData( doc, 'fo[o<$text bold="true">b]ar</$text>', { selectionAttributes: {
 					bold: true
 				} } );
 
-				deleteContents( document.batch(), document.selection );
+				deleteContents( doc.batch(), doc.selection );
 
-				expect( getData( document ) ).to.equal( 'fo[]<$text bold="true">ar</$text>' );
-				expect( document.selection.getAttribute( 'bold' ) ).to.undefined;
+				expect( getData( doc ) ).to.equal( 'fo[]<$text bold="true">ar</$text>' );
+				expect( doc.selection.getAttribute( 'bold' ) ).to.undefined;
 			} );
 
 			it( 'clears selection attrs when emptied content', () => {
-				setData( document, '<p>x</p><p>[<$text bold="true">foo</$text>]</p><p>y</p>', { selectionAttributes: {
-					bold: true
-				} } );
-
-				deleteContents( document.batch(), document.selection );
-
-				expect( getData( document ) ).to.equal( '<p>x</p><p>[]</p><p>y</p>' );
-				expect( document.selection.getAttribute( 'bold' ) ).to.undefined;
+				setData(
+					doc,
+					'<paragraph>x</paragraph><paragraph>[<$text bold="true">foo</$text>]</paragraph><paragraph>y</paragraph>',
+					{
+						selectionAttributes: {
+							bold: true
+						}
+					}
+				);
+
+				deleteContents( doc.batch(), doc.selection );
+
+				expect( getData( doc ) ).to.equal( '<paragraph>x</paragraph><paragraph>[]</paragraph><paragraph>y</paragraph>' );
+				expect( doc.selection.getAttribute( 'bold' ) ).to.undefined;
 			} );
 
 			it( 'leaves selection attributes when text contains them', () => {
-				setData( document, '<p>x<$text bold="true">a[foo]b</$text>y</p>', { selectionAttributes: {
+				setData( doc, '<paragraph>x<$text bold="true">a[foo]b</$text>y</paragraph>', { selectionAttributes: {
 					bold: true
 				} } );
 
-				deleteContents( document.batch(), document.selection );
+				deleteContents( doc.batch(), doc.selection );
 
-				expect( getData( document ) ).to.equal( '<p>x<$text bold="true">a[]b</$text>y</p>' );
-				expect( document.selection.getAttribute( 'bold' ) ).to.equal( true );
+				expect( getData( doc ) ).to.equal( '<paragraph>x<$text bold="true">a[]b</$text>y</paragraph>' );
+				expect( doc.selection.getAttribute( 'bold' ) ).to.equal( true );
 			} );
 		} );
 
@@ -142,115 +153,252 @@ describe( 'Delete utils', () => {
 		// like – multiple editing hosts (cE=true/false in use) or block limit elements like <td>.
 		// Those case should, again, be handled by their specific implementations.
 		describe( 'in multi-element scenarios', () => {
+			beforeEach( () => {
+				doc = new Document();
+				doc.createRoot();
+
+				const schema = doc.schema;
+
+				schema.registerItem( 'paragraph', '$block' );
+				schema.registerItem( 'heading1', '$block' );
+				schema.registerItem( 'pchild' );
+				schema.registerItem( 'image', '$inline' );
+
+				schema.allow( { name: 'pchild', inside: 'paragraph' } );
+				schema.allow( { name: 'paragraph', attributes: [ 'align' ] } );
+			} );
+
 			test(
 				'do not merge when no need to',
-				'<p>x</p><p>[foo]</p><p>y</p>',
-				'<p>x</p><p>[]</p><p>y</p>',
+				'<paragraph>x</paragraph><paragraph>[foo]</paragraph><paragraph>y</paragraph>',
+				'<paragraph>x</paragraph><paragraph>[]</paragraph><paragraph>y</paragraph>',
 				{ merge: true }
 			);
 
 			test(
 				'merges second element into the first one (same name)',
-				'<p>x</p><p>fo[o</p><p>b]ar</p><p>y</p>',
-				'<p>x</p><p>fo[]ar</p><p>y</p>',
+				'<paragraph>x</paragraph><paragraph>fo[o</paragraph><paragraph>b]ar</paragraph><paragraph>y</paragraph>',
+				'<paragraph>x</paragraph><paragraph>fo[]ar</paragraph><paragraph>y</paragraph>',
 				{ merge: true }
 			);
 
 			test(
 				'does not merge second element into the first one (same name, !option.merge)',
-				'<p>x</p><p>fo[o</p><p>b]ar</p><p>y</p>',
-				'<p>x</p><p>fo[]</p><p>ar</p><p>y</p>'
+				'<paragraph>x</paragraph><paragraph>fo[o</paragraph><paragraph>b]ar</paragraph><paragraph>y</paragraph>',
+				'<paragraph>x</paragraph><paragraph>fo[]</paragraph><paragraph>ar</paragraph><paragraph>y</paragraph>'
 			);
 
 			test(
 				'merges second element into the first one (same name)',
-				'<p>x</p><p>fo[o</p><p>b]ar</p><p>y</p>',
-				'<p>x</p><p>fo[]ar</p><p>y</p>',
+				'<paragraph>x</paragraph><paragraph>fo[o</paragraph><paragraph>b]ar</paragraph><paragraph>y</paragraph>',
+				'<paragraph>x</paragraph><paragraph>fo[]ar</paragraph><paragraph>y</paragraph>',
 				{ merge: true }
 			);
 
 			test(
 				'merges second element into the first one (different name)',
-				'<p>x</p><h1>fo[o</h1><p>b]ar</p><p>y</p>',
-				'<p>x</p><h1>fo[]ar</h1><p>y</p>',
+				'<paragraph>x</paragraph><heading1>fo[o</heading1><paragraph>b]ar</paragraph><paragraph>y</paragraph>',
+				'<paragraph>x</paragraph><heading1>fo[]ar</heading1><paragraph>y</paragraph>',
 				{ merge: true }
 			);
 
 			it( 'merges second element into the first one (different name, backward selection)', () => {
-				setData( document, '<p>x</p><h1>fo[o</h1><p>b]ar</p><p>y</p>', { lastRangeBackward: true } );
+				setData(
+					doc,
+					'<paragraph>x</paragraph><heading1>fo[o</heading1><paragraph>b]ar</paragraph><paragraph>y</paragraph>',
+					{ lastRangeBackward: true }
+				);
 
-				deleteContents( document.batch(), document.selection, { merge: true } );
+				deleteContents( doc.batch(), doc.selection, { merge: true } );
 
-				expect( getData( document ) ).to.equal( '<p>x</p><h1>fo[]ar</h1><p>y</p>' );
+				expect( getData( doc ) ).to.equal( '<paragraph>x</paragraph><heading1>fo[]ar</heading1><paragraph>y</paragraph>' );
 			} );
 
 			test(
 				'merges second element into the first one (different attrs)',
-				'<p>x</p><p align="l">fo[o</p><p>b]ar</p><p>y</p>',
-				'<p>x</p><p align="l">fo[]ar</p><p>y</p>',
+				'<paragraph>x</paragraph><paragraph align="l">fo[o</paragraph><paragraph>b]ar</paragraph><paragraph>y</paragraph>',
+				'<paragraph>x</paragraph><paragraph align="l">fo[]ar</paragraph><paragraph>y</paragraph>',
 				{ merge: true }
 			);
 
 			test(
 				'merges second element to an empty first element',
-				'<p>x</p><h1>[</h1><p>fo]o</p><p>y</p>',
-				'<p>x</p><h1>[]o</h1><p>y</p>',
+				'<paragraph>x</paragraph><heading1>[</heading1><paragraph>fo]o</paragraph><paragraph>y</paragraph>',
+				'<paragraph>x</paragraph><heading1>[]o</heading1><paragraph>y</paragraph>',
 				{ merge: true }
 			);
 
 			test(
 				'merges elements when deep nested',
-				'<p>x<pchild>fo[o</pchild></p><p><pchild>b]ar</pchild>y</p>',
-				'<p>x<pchild>fo[]ar</pchild>y</p>',
+				'<paragraph>x<pchild>fo[o</pchild></paragraph><paragraph><pchild>b]ar</pchild>y</paragraph>',
+				'<paragraph>x<pchild>fo[]ar</pchild>y</paragraph>',
 				{ merge: true }
 			);
 
 			// For code coverage reasons.
 			test(
 				'merges element when selection is in two consecutive nodes even when it is empty',
-				'<p>foo[</p><p>]bar</p>',
-				'<p>foo[]bar</p>',
+				'<paragraph>foo[</paragraph><paragraph>]bar</paragraph>',
+				'<paragraph>foo[]bar</paragraph>',
 				{ merge: true }
 			);
 
 			// If you disagree with this case please read the notes before this section.
 			test(
 				'merges elements when left end deep nested',
-				'<p>x<pchild>fo[o</pchild></p><p>b]ary</p>',
-				'<p>x<pchild>fo[]</pchild>ary</p>',
+				'<paragraph>x<pchild>fo[o</pchild></paragraph><paragraph>b]ary</paragraph>',
+				'<paragraph>x<pchild>fo[]</pchild>ary</paragraph>',
 				{ merge: true }
 			);
 
 			// If you disagree with this case please read the notes before this section.
 			test(
 				'merges elements when right end deep nested',
-				'<p>xfo[o</p><p><pchild>b]ar</pchild>y<img></img></p>',
-				'<p>xfo[]<pchild>ar</pchild>y<img></img></p>',
+				'<paragraph>xfo[o</paragraph><paragraph><pchild>b]ar</pchild>y<image></image></paragraph>',
+				'<paragraph>xfo[]<pchild>ar</pchild>y<image></image></paragraph>',
 				{ merge: true }
 			);
 
 			test(
 				'merges elements when more content in the right branch',
-				'<p>xfo[o</p><p>b]a<pchild>r</pchild>y</p>',
-				'<p>xfo[]a<pchild>r</pchild>y</p>',
+				'<paragraph>xfo[o</paragraph><paragraph>b]a<pchild>r</pchild>y</paragraph>',
+				'<paragraph>xfo[]a<pchild>r</pchild>y</paragraph>',
 				{ merge: true }
 			);
 
 			test(
 				'leaves just one element when all selected',
-				'<h1>[x</h1><p>foo</p><p>y]</p>',
-				'<h1>[]</h1>',
+				'<heading1>[x</heading1><paragraph>foo</paragraph><paragraph>y]</paragraph>',
+				'<heading1>[]</heading1>',
 				{ merge: true }
 			);
 		} );
 
+		describe( 'in element selections scenarios', () => {
+			beforeEach( () => {
+				doc = new Document();
+				// <p> like root.
+				doc.createRoot( 'paragraph', 'paragraphRoot' );
+				// <body> like root.
+				doc.createRoot( '$root', 'bodyRoot' );
+				// Special root which allows only blockWidgets inside itself.
+				doc.createRoot( 'restrictedRoot', 'restrictedRoot' );
+
+				const schema = doc.schema;
+
+				schema.registerItem( 'image', '$inline' );
+				schema.registerItem( 'paragraph', '$block' );
+				schema.registerItem( 'heading1', '$block' );
+				schema.registerItem( 'blockWidget' );
+				schema.registerItem( 'restrictedRoot' );
+
+				schema.allow( { name: '$block', inside: '$root' } );
+				schema.allow( { name: 'blockWidget', inside: '$root' } );
+
+				schema.allow( { name: 'blockWidget', inside: 'restrictedRoot' } );
+			} );
+
+			// See also "in simple scenarios => deletes an element".
+
+			it( 'deletes two inline elements', () => {
+				setData(
+					doc,
+					'<paragraph>x[<image></image><image></image>]z</paragraph>',
+					{ rootName: 'paragraphRoot' }
+				);
+
+				deleteContents( doc.batch(), doc.selection );
+
+				expect( getData( doc, { rootName: 'paragraphRoot' } ) )
+					.to.equal( '<paragraph>x[]z</paragraph>' );
+			} );
+
+			it( 'creates a paragraph when text is not allowed (paragraph selected)', () => {
+				setData(
+					doc,
+					'<paragraph>x</paragraph>[<paragraph>yyy</paragraph>]<paragraph>z</paragraph>',
+					{ rootName: 'bodyRoot' }
+				);
+
+				deleteContents( doc.batch(), doc.selection );
+
+				expect( getData( doc, { rootName: 'bodyRoot' } ) )
+					.to.equal( '<paragraph>x</paragraph><paragraph>[]</paragraph><paragraph>z</paragraph>' );
+			} );
+
+			it( 'creates a paragraph when text is not allowed (block widget selected)', () => {
+				setData(
+					doc,
+					'<paragraph>x</paragraph>[<blockWidget></blockWidget>]<paragraph>z</paragraph>',
+					{ rootName: 'bodyRoot' }
+				);
+
+				deleteContents( doc.batch(), doc.selection );
+
+				expect( getData( doc, { rootName: 'bodyRoot' } ) )
+					.to.equal( '<paragraph>x</paragraph><paragraph>[]</paragraph><paragraph>z</paragraph>' );
+			} );
+
+			it( 'creates paragraph when text is not allowed (heading selected)', () => {
+				setData(
+					doc,
+					'<paragraph>x</paragraph>[<heading1>yyy</heading1>]<paragraph>z</paragraph>',
+					{ rootName: 'bodyRoot' }
+				);
+
+				deleteContents( doc.batch(), doc.selection );
+
+				expect( getData( doc, { rootName: 'bodyRoot' } ) )
+					.to.equal( '<paragraph>x</paragraph><paragraph>[]</paragraph><paragraph>z</paragraph>' );
+			} );
+
+			it( 'creates paragraph when text is not allowed (two blocks selected)', () => {
+				setData(
+					doc,
+					'<paragraph>x</paragraph>[<heading1>yyy</heading1><paragraph>yyy</paragraph>]<paragraph>z</paragraph>',
+					{ rootName: 'bodyRoot' }
+				);
+
+				deleteContents( doc.batch(), doc.selection );
+
+				expect( getData( doc, { rootName: 'bodyRoot' } ) )
+					.to.equal( '<paragraph>x</paragraph><paragraph>[]</paragraph><paragraph>z</paragraph>' );
+			} );
+
+			it( 'creates paragraph when text is not allowed (all content selected)', () => {
+				setData(
+					doc,
+					'[<heading1>x</heading1><paragraph>z</paragraph>]',
+					{ rootName: 'bodyRoot' }
+				);
+
+				deleteContents( doc.batch(), doc.selection );
+
+				expect( getData( doc, { rootName: 'bodyRoot' } ) )
+					.to.equal( '<paragraph>[]</paragraph>' );
+			} );
+
+			it( 'does not create a paragraph when it is not allowed', () => {
+				setData(
+					doc,
+					'<blockWidget></blockWidget>[<blockWidget></blockWidget>]<blockWidget></blockWidget>',
+					{ rootName: 'restrictedRoot' }
+				);
+
+				deleteContents( doc.batch(), doc.selection );
+
+				expect( getData( doc, { rootName: 'restrictedRoot' } ) )
+					.to.equal( '<blockWidget></blockWidget>[]<blockWidget></blockWidget>' );
+			} );
+		} );
+
 		function test( title, input, output, options ) {
 			it( title, () => {
-				setData( document, input );
+				setData( doc, input );
 
-				deleteContents( document.batch(), document.selection, options );
+				deleteContents( doc.batch(), doc.selection, options );
 
-				expect( getData( document ) ).to.equal( output );
+				expect( getData( doc ) ).to.equal( output );
 			} );
 		}
 	} );

+ 18 - 0
packages/ckeditor5-engine/tests/model/schema/schema.js

@@ -39,6 +39,24 @@ describe( 'constructor', () => {
 	it( 'should allow inline in block', () => {
 		expect( schema.check( { name: '$inline', inside: [ '$block' ] } ) ).to.be.true;
 	} );
+
+	it( 'should create the objects set', () => {
+		expect( schema.objects ).to.be.instanceOf( Set );
+	} );
+
+	describe( '$clipboardHolder', () => {
+		it( 'should allow $block', () => {
+			expect( schema.check( { name: '$block', inside: [ '$clipboardHolder' ] } ) ).to.be.true;
+		} );
+
+		it( 'should allow $inline', () => {
+			expect( schema.check( { name: '$inline', inside: [ '$clipboardHolder' ] } ) ).to.be.true;
+		} );
+
+		it( 'should allow $text', () => {
+			expect( schema.check( { name: '$text', inside: [ '$clipboardHolder' ] } ) ).to.be.true;
+		} );
+	} );
 } );
 
 describe( 'registerItem', () => {