Browse Source

Merge branch 'master' into t/627

Piotrek Koszuliński 9 years ago
parent
commit
783b933620

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

@@ -3,30 +3,36 @@
  * For licensing, see LICENSE.md.
  * 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
  * 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:
  * using given:
  *
  *
  * * {@link engine.dataProcessor.DataProcessor data processor},
  * * {@link engine.dataProcessor.DataProcessor data processor},
  * * {@link engine.conversion.ModelConversionDispatcher model to view} and
  * * {@link engine.conversion.ModelConversionDispatcher model to view} and
  * * {@link engine.conversion.ViewConversionDispatcher view to model} converters.
  * * {@link engine.conversion.ViewConversionDispatcher view to model} converters.
  *
  *
- * @memberOf engine
+ * @memberOf engine.controller
+ * @mixes utils.EmitterMixin
  */
  */
 export default class DataController {
 export default class DataController {
 	/**
 	/**
@@ -40,7 +46,7 @@ export default class DataController {
 		 * Document model.
 		 * Document model.
 		 *
 		 *
 		 * @readonly
 		 * @readonly
-		 * @member {engine.model.document} engine.DataController#model
+		 * @member {engine.model.document} engine.controller.DataController#model
 		 */
 		 */
 		this.model = model;
 		this.model = model;
 
 
@@ -48,7 +54,7 @@ export default class DataController {
 		 * Data processor used during the conversion.
 		 * Data processor used during the conversion.
 		 *
 		 *
 		 * @readonly
 		 * @readonly
-		 * @member {engine.dataProcessor.DataProcessor} engine.DataController#processor
+		 * @member {engine.dataProcessor.DataProcessor} engine.controller.DataController#processor
 		 */
 		 */
 		this.processor = dataProcessor;
 		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
 		 * 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.
 		 * 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();
 		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:
 		 * To attach model to view converter to the data pipeline you need to add lister to this property:
 		 *
 		 *
 		 *		data.modelToView( 'insert:$element', customInsertConverter );
 		 *		data.modelToView( 'insert:$element', customInsertConverter );
@@ -72,7 +78,7 @@ export default class DataController {
 		 *		buildModelConverter().for( data.modelToView ).fromAttribute( 'bold' ).toElement( 'b' );
 		 *		buildModelConverter().for( data.modelToView ).fromAttribute( 'bold' ).toElement( 'b' );
 		 *
 		 *
 		 * @readonly
 		 * @readonly
-		 * @member {engine.conversion.ModelConversionDispatcher} engine.DataController#modelToView
+		 * @member {engine.conversion.ModelConversionDispatcher} engine.controller.DataController#modelToView
 		 */
 		 */
 		this.modelToView = new ModelConversionDispatcher( {
 		this.modelToView = new ModelConversionDispatcher( {
 			mapper: this.mapper
 			mapper: this.mapper
@@ -80,7 +86,7 @@ export default class DataController {
 		this.modelToView.on( 'insert:$text', insertText(), { priority: 'lowest' } );
 		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:
 		 * To attach view to model converter to the data pipeline you need to add lister to this property:
 		 *
 		 *
 		 *		data.viewToModel( 'element', customElementConverter );
 		 *		data.viewToModel( 'element', customElementConverter );
@@ -90,7 +96,7 @@ export default class DataController {
 		 *		buildViewConverter().for( data.viewToModel ).fromElement( 'b' ).toAttribute( 'bold', 'true' );
 		 *		buildViewConverter().for( data.viewToModel ).fromElement( 'b' ).toAttribute( 'bold', 'true' );
 		 *
 		 *
 		 * @readonly
 		 * @readonly
-		 * @member {engine.conversion.ViewConversionDispatcher} engine.DataController#viewToModel
+		 * @member {engine.conversion.ViewConversionDispatcher} engine.controller.DataController#viewToModel
 		 */
 		 */
 		this.viewToModel = new ViewConversionDispatcher( {
 		this.viewToModel = new ViewConversionDispatcher( {
 			schema: model.schema
 			schema: model.schema
@@ -104,11 +110,13 @@ export default class DataController {
 		this.viewToModel.on( 'text', convertText(), { priority: 'lowest' } );
 		this.viewToModel.on( 'text', convertText(), { priority: 'lowest' } );
 		this.viewToModel.on( 'element', convertToModelFragment(), { priority: 'lowest' } );
 		this.viewToModel.on( 'element', convertToModelFragment(), { priority: 'lowest' } );
 		this.viewToModel.on( 'documentFragment', 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.
 	 * @param {String} [rootName='main'] Root name.
 	 * @returns {String} Output data.
 	 * @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
 	 * 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.
 	 * @param {engine.model.Element} modelElement Element which content will be stringified.
 	 * @returns {String} Output data.
 	 * @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
 	 * 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}.
 	 * {@link engine.view.DocumentFragment view DocumentFragment}.
 	 *
 	 *
 	 * @param {engine.model.Element} modelElement Element which content will be stringified.
 	 * @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
 	 * 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} data Input data.
 	 * @param {String} [rootName='main'] Root name.
 	 * @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} data Data to parse.
 	 * @param {String} [context='$root'] Base context in which view will be converted to the model. See:
 	 * @param {String} [context='$root'] Base context in which view will be converted to the model. See:
 	 * {@link engine.conversion.ViewConversionDispatcher#convert}.
 	 * {@link engine.conversion.ViewConversionDispatcher#convert}.
@@ -204,4 +212,32 @@ export default class DataController {
 	 * Removes all event listeners set by the DataController.
 	 * Removes all event listeners set by the DataController.
 	 */
 	 */
 	destroy() {}
 	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 → packages/ckeditor5-engine/src/controller/editingcontroller.js

@@ -3,26 +3,26 @@
  * For licensing, see LICENSE.md.
  * 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 {
 import {
 	convertRangeSelection,
 	convertRangeSelection,
 	convertCollapsedSelection,
 	convertCollapsedSelection,
 	clearAttributes,
 	clearAttributes,
 	clearFakeSelection
 	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.
  * browser-independent virtualization over the DOM elements. Editing controller also attach default converters.
  *
  *
- * @memberOf engine
+ * @memberOf engine.controller
  */
  */
 export default class EditingController {
 export default class EditingController {
 	/**
 	/**
@@ -35,7 +35,7 @@ export default class EditingController {
 		 * Document model.
 		 * Document model.
 		 *
 		 *
 		 * @readonly
 		 * @readonly
-		 * @member {engine.model.document} engine.EditingController#model
+		 * @member {engine.model.document} engine.controller.EditingController#model
 		 */
 		 */
 		this.model = model;
 		this.model = model;
 
 
@@ -43,7 +43,7 @@ export default class EditingController {
 		 * View document.
 		 * View document.
 		 *
 		 *
 		 * @readonly
 		 * @readonly
-		 * @member {engine.view.document} engine.EditingController#view
+		 * @member {engine.view.document} engine.controller.EditingController#view
 		 */
 		 */
 		this.view = new ViewDocument();
 		this.view = new ViewDocument();
 
 
@@ -51,13 +51,13 @@ export default class EditingController {
 		 * Mapper which describes model-view binding.
 		 * Mapper which describes model-view binding.
 		 *
 		 *
 		 * @readonly
 		 * @readonly
-		 * @member {engine.conversion.Mapper} engine.EditingController#mapper
+		 * @member {engine.conversion.Mapper} engine.controller.EditingController#mapper
 		 */
 		 */
 		this.mapper = new Mapper();
 		this.mapper = new Mapper();
 
 
 		/**
 		/**
 		 * Model to view conversion dispatcher, which converts changes from the model to
 		 * 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:
 		 * 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' );
 		 *		buildModelConverter().for( editing.modelToView ).fromAttribute( 'bold' ).toElement( 'b' );
 		 *
 		 *
 		 * @readonly
 		 * @readonly
-		 * @member {engine.conversion.ModelConversionDispatcher} engine.EditingController#modelToView
+		 * @member {engine.conversion.ModelConversionDispatcher} engine.controller.EditingController#modelToView
 		 */
 		 */
 		this.modelToView = new ModelConversionDispatcher( {
 		this.modelToView = new ModelConversionDispatcher( {
 			mapper: this.mapper,
 			mapper: this.mapper,
@@ -77,10 +77,10 @@ export default class EditingController {
 
 
 		/**
 		/**
 		 * Property keeping all listenters attached by controller on other objects, so it can
 		 * 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
 		 * @private
-		 * @member {utils.EmitterMixin} engine.EditingController#_listenter
+		 * @member {utils.EmitterMixin} engine.controller.EditingController#_listenter
 		 */
 		 */
 		this._listenter = Object.create( EmitterMixin );
 		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`.
  *		// is going to be appended directly to a '$root' element, use that in `context`.
  *		viewDispatcher.convert( viewDocumentFragment, { context: [ '$root' ] } );
  *		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.
  * event which can be used to prepare tree view for conversion.
  *
  *
  * @mixes utils.EmitterMixin
  * @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
  * @memberOf engine.conversion
  */
  */
@@ -119,12 +119,12 @@ export default class ViewConversionDispatcher {
 	/**
 	/**
 	 * Starts the conversion process. The entry point for the conversion.
 	 * 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 {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`
 	 * @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.
 	 * @returns {engine.model.DocumentFragment} Model document fragment that is a result of the conversion process.
 	 */
 	 */
 	convert( viewItem, additionalData = {} ) {
 	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.
 	 * 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.
 	 * @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
 	 * `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.
 	 * 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
 	 * @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
 	 * 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,
 	 * 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 {engine.view.Element} data.input Converted element.
 	 * @param {*} data.output The current state of conversion result. Every change to converted element should
 	 * @param {*} data.output The current state of conversion result. Every change to converted element should
 	 * be reflected by setting or modifying this property.
 	 * 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 {engine.conversion.ViewConsumable} consumable Values to consume.
 	 * @param {Object} conversionApi Conversion interface to be used by callback, passed in `ViewConversionDispatcher` constructor.
 	 * @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
 	 * 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.
 	 * 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.
 	 * 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
  * @memberOf engine.conversion.ViewConversionApi
  * @function covertItem
  * @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.view.DocumentFragment|engine.view.Element|engine.view.Text} input Item to convert.
  * @param {engine.conversion.ViewConsumable} consumable Values to consume.
  * @param {engine.conversion.ViewConsumable} consumable Values to consume.
  * @param {Object} [additionalData] Additional data to be passed in `data` argument when firing `ViewConversionDispatcher`
  * @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.
  * @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
  * @memberOf engine.conversion.ViewConversionApi
  * @function convertChildren
  * @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.view.DocumentFragment|engine.view.Element} input Item which children will be converted.
  * @param {engine.conversion.ViewConsumable} consumable Values to consume.
  * @param {engine.conversion.ViewConsumable} consumable Values to consume.
  * @param {Object} [additionalData] Additional data to be passed in `data` argument when firing `ViewConversionDispatcher`
  * @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.
  * @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 {Object} options Additional configuration.
  * @param {Array<Object>} [options.selectionAttributes] List of attributes which will be passed to the selection.
  * @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 {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
  * @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.
  * 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() );
 	viewToModel.on( 'text', convertToModelText() );
 
 
 	// Convert view to model.
 	// 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 root DocumentFragment contains only one element - return that element.
 	if ( model instanceof ModelDocumentFragment && model.childCount == 1 ) {
 	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 LivePosition from '../liveposition.js';
 import Position from '../position.js';
 import Position from '../position.js';
+import Element from '../element.js';
 import compareArrays from '../../../utils/comparearrays.js';
 import compareArrays from '../../../utils/comparearrays.js';
 
 
 /**
 /**
@@ -58,5 +59,21 @@ export default function deleteContents( batch, selection, options = {} ) {
 
 
 	selection.collapse( startPos );
 	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();
 	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

@@ -250,6 +250,15 @@ export default class Schema {
 	 * Creates Schema instance.
 	 * Creates Schema instance.
 	 */
 	 */
 	constructor() {
 	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.
 		 * Schema items registered in the schema.
 		 *
 		 *
@@ -274,6 +283,13 @@ export default class Schema {
 
 
 		this.allow( { name: '$block', inside: '$root' } );
 		this.allow( { name: '$block', inside: '$root' } );
 		this.allow( { name: '$inline', inside: '$block' } );
 		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}.
  * class or {@link engine.view.AttributeElement}.
  *
  *
  * Note that for view elements which are not created from model, like elements from mutations, paste or
  * 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}.
  * these will be instances of the {@link engine.view.Element}.
  *
  *
  * @memberOf engine.view
  * @memberOf engine.view

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

@@ -6,12 +6,15 @@
 /* bender-tags: view */
 /* bender-tags: view */
 
 
 import ModelDocument from '/ckeditor5/engine/model/document.js';
 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 HtmlDataProcessor from '/ckeditor5/engine/dataprocessor/htmldataprocessor.js';
 
 
 import buildViewConverter  from '/ckeditor5/engine/conversion/buildviewconverter.js';
 import buildViewConverter  from '/ckeditor5/engine/conversion/buildviewconverter.js';
 import buildModelConverter  from '/ckeditor5/engine/conversion/buildmodelconverter.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 { getData, setData, stringify, parse } from '/ckeditor5/engine/dev-utils/model.js';
 
 
 import count from '/ckeditor5/utils/count.js';
 import count from '/ckeditor5/utils/count.js';
@@ -37,6 +40,20 @@ describe( 'DataController', () => {
 
 
 			expect( data.processor ).to.be.undefined;
 			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', () => {
 	describe( 'parse', () => {
@@ -252,4 +269,22 @@ describe( 'DataController', () => {
 			expect( data ).to.respondTo( 'destroy' );
 			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 → packages/ckeditor5-engine/tests/controller/editingcontroller.js

@@ -6,7 +6,9 @@
 /* globals setTimeout, Range, document */
 /* globals setTimeout, Range, document */
 /* bender-tags: view */
 /* 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';
 import ViewDocument from '/ckeditor5/engine/view/document.js';
 
 
@@ -103,15 +105,18 @@ describe( 'EditingController', () => {
 	} );
 	} );
 
 
 	describe( 'conversion', () => {
 	describe( 'conversion', () => {
-		let model, modelRoot, viewRoot, domRoot, editing;
+		let model, modelRoot, viewRoot, domRoot, editing, listener;
 
 
 		before( () => {
 		before( () => {
+			listener = Object.create( EmitterMixin );
+
 			model = new ModelDocument();
 			model = new ModelDocument();
 			modelRoot = model.createRoot();
 			modelRoot = model.createRoot();
 
 
 			editing = new EditingController( model );
 			editing = new EditingController( model );
 
 
-			domRoot = document.getElementById( 'editor' );
+			domRoot = document.createElement( 'div' );
+			document.body.appendChild( domRoot );
 			viewRoot = editing.createRoot( domRoot );
 			viewRoot = editing.createRoot( domRoot );
 
 
 			model.schema.registerItem( 'paragraph', '$block' );
 			model.schema.registerItem( 'paragraph', '$block' );
@@ -120,6 +125,11 @@ describe( 'EditingController', () => {
 			buildModelConverter().for( editing.modelToView ).fromElement( 'div' ).toElement( 'div' );
 			buildModelConverter().for( editing.modelToView ).fromElement( 'div' ).toElement( 'div' );
 		} );
 		} );
 
 
+		after( () => {
+			document.body.removeChild( domRoot );
+			listener.stopListening();
+		} );
+
 		beforeEach( () => {
 		beforeEach( () => {
 			// Note: The below code is highly overcomplicated due to #455.
 			// Note: The below code is highly overcomplicated due to #455.
 			model.selection.removeAllRanges();
 			model.selection.removeAllRanges();
@@ -182,7 +192,7 @@ describe( 'EditingController', () => {
 		} );
 		} );
 
 
 		it( 'should convert selection from view to model', ( done ) => {
 		it( 'should convert selection from view to model', ( done ) => {
-			editing.view.on( 'selectionChange', () => {
+			listener.listenTo( editing.view, 'selectionChange', () => {
 				setTimeout( () => {
 				setTimeout( () => {
 					expect( getModelData( model ) ).to.equal(
 					expect( getModelData( model ) ).to.equal(
 						'<paragraph>foo</paragraph>' +
 						'<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.` );
 			} ).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', () => {
 		describe( 'selection', () => {
 			test( 'sets collapsed selection in an element', {
 			test( 'sets collapsed selection in an element', {
 				data: '<a>[]</a>',
 				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';
 import { setData, getData } from '/ckeditor5/engine/dev-utils/model.js';
 
 
 describe( 'Delete utils', () => {
 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( 'deleteContents', () => {
 		describe( 'in simple scenarios', () => {
 		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(
 			test(
 				'does nothing on collapsed selection',
 				'does nothing on collapsed selection',
 				'f[]oo',
 				'f[]oo',
@@ -47,11 +39,11 @@ describe( 'Delete utils', () => {
 			);
 			);
 
 
 			it( 'deletes single character (backward selection)' , () => {
 			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(
 			test(
@@ -62,73 +54,92 @@ describe( 'Delete utils', () => {
 
 
 			test(
 			test(
 				'deletes whole text between nodes',
 				'deletes whole text between nodes',
-				'<img></img>[foo]<img></img>',
-				'<img></img>[]<img></img>'
+				'<image></image>[foo]<image></image>',
+				'<image></image>[]<image></image>'
 			);
 			);
 
 
 			test(
 			test(
 				'deletes an element',
 				'deletes an element',
-				'x[<img></img>]y',
+				'x[<image></image>]y',
 				'x[]y'
 				'x[]y'
 			);
 			);
 
 
 			test(
 			test(
 				'deletes a bunch of nodes',
 				'deletes a bunch of nodes',
-				'w[x<img></img>y]z',
+				'w[x<image></image>y]z',
 				'w[]z'
 				'w[]z'
 			);
 			);
 
 
 			test(
 			test(
 				'does not break things when option.merge passed',
 				'does not break things when option.merge passed',
-				'w[x<img></img>y]z',
+				'w[x<image></image>y]z',
 				'w[]z',
 				'w[]z',
 				{ merge: true }
 				{ merge: true }
 			);
 			);
 		} );
 		} );
 
 
 		describe( 'with text attributes', () => {
 		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)', () => {
 			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
 					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)', () => {
 			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
 					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', () => {
 			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', () => {
 			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
 					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>.
 		// 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.
 		// Those case should, again, be handled by their specific implementations.
 		describe( 'in multi-element scenarios', () => {
 		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(
 			test(
 				'do not merge when no need to',
 				'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 }
 				{ merge: true }
 			);
 			);
 
 
 			test(
 			test(
 				'merges second element into the first one (same name)',
 				'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 }
 				{ merge: true }
 			);
 			);
 
 
 			test(
 			test(
 				'does not merge second element into the first one (same name, !option.merge)',
 				'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(
 			test(
 				'merges second element into the first one (same name)',
 				'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 }
 				{ merge: true }
 			);
 			);
 
 
 			test(
 			test(
 				'merges second element into the first one (different name)',
 				'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 }
 				{ merge: true }
 			);
 			);
 
 
 			it( 'merges second element into the first one (different name, backward selection)', () => {
 			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(
 			test(
 				'merges second element into the first one (different attrs)',
 				'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 }
 				{ merge: true }
 			);
 			);
 
 
 			test(
 			test(
 				'merges second element to an empty first element',
 				'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 }
 				{ merge: true }
 			);
 			);
 
 
 			test(
 			test(
 				'merges elements when deep nested',
 				'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 }
 				{ merge: true }
 			);
 			);
 
 
 			// For code coverage reasons.
 			// For code coverage reasons.
 			test(
 			test(
 				'merges element when selection is in two consecutive nodes even when it is empty',
 				'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 }
 				{ merge: true }
 			);
 			);
 
 
 			// If you disagree with this case please read the notes before this section.
 			// If you disagree with this case please read the notes before this section.
 			test(
 			test(
 				'merges elements when left end deep nested',
 				'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 }
 				{ merge: true }
 			);
 			);
 
 
 			// If you disagree with this case please read the notes before this section.
 			// If you disagree with this case please read the notes before this section.
 			test(
 			test(
 				'merges elements when right end deep nested',
 				'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 }
 				{ merge: true }
 			);
 			);
 
 
 			test(
 			test(
 				'merges elements when more content in the right branch',
 				'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 }
 				{ merge: true }
 			);
 			);
 
 
 			test(
 			test(
 				'leaves just one element when all selected',
 				'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 }
 				{ 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 ) {
 		function test( title, input, output, options ) {
 			it( title, () => {
 			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', () => {
 	it( 'should allow inline in block', () => {
 		expect( schema.check( { name: '$inline', inside: [ '$block' ] } ) ).to.be.true;
 		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', () => {
 describe( 'registerItem', () => {