Răsfoiți Sursa

Merge branch 'master' into t/331

Piotrek Koszuliński 9 ani în urmă
părinte
comite
9722f85495
36 a modificat fișierele cu 2354 adăugiri și 176 ștergeri
  1. 65 29
      packages/ckeditor5-engine/src/controller/datacontroller.js
  2. 20 18
      packages/ckeditor5-engine/src/controller/editingcontroller.js
  3. 415 0
      packages/ckeditor5-engine/src/controller/insert.js
  4. 8 0
      packages/ckeditor5-engine/src/conversion/model-selection-to-view-converters.js
  5. 24 23
      packages/ckeditor5-engine/src/conversion/viewconversiondispatcher.js
  6. 3 1
      packages/ckeditor5-engine/src/dev-utils/model.js
  7. 17 0
      packages/ckeditor5-engine/src/model/composer/deletecontents.js
  8. 16 0
      packages/ckeditor5-engine/src/model/schema.js
  9. 4 1
      packages/ckeditor5-engine/src/view/document.js
  10. 47 1
      packages/ckeditor5-engine/src/view/domconverter.js
  11. 1 1
      packages/ckeditor5-engine/src/view/element.js
  12. 2 2
      packages/ckeditor5-engine/src/view/observer/domeventobserver.js
  13. 94 0
      packages/ckeditor5-engine/src/view/observer/fakeselectionobserver.js
  14. 0 1
      packages/ckeditor5-engine/src/view/observer/selectionobserver.js
  15. 85 6
      packages/ckeditor5-engine/src/view/renderer.js
  16. 67 0
      packages/ckeditor5-engine/src/view/selection.js
  17. 36 1
      packages/ckeditor5-engine/tests/controller/datacontroller.js
  18. 14 4
      packages/ckeditor5-engine/tests/controller/editingcontroller.js
  19. 555 0
      packages/ckeditor5-engine/tests/controller/insert.js
  20. 13 1
      packages/ckeditor5-engine/tests/conversion/model-selection-to-view-converters.js
  21. 10 0
      packages/ckeditor5-engine/tests/dev-utils/model.js
  22. 229 79
      packages/ckeditor5-engine/tests/model/composer/deletecontents.js
  23. 18 0
      packages/ckeditor5-engine/tests/model/schema/schema.js
  24. 5 1
      packages/ckeditor5-engine/tests/tickets/401/1.js
  25. 12 4
      packages/ckeditor5-engine/tests/tickets/462/1.js
  26. 6 1
      packages/ckeditor5-engine/tests/tickets/475/1.js
  27. 7 1
      packages/ckeditor5-engine/tests/tickets/603/1.js
  28. 3 1
      packages/ckeditor5-engine/tests/view/document/document.js
  29. 32 0
      packages/ckeditor5-engine/tests/view/domconverter/binding.js
  30. 42 0
      packages/ckeditor5-engine/tests/view/domconverter/dom-to-view.js
  31. 28 0
      packages/ckeditor5-engine/tests/view/manual/fakeselection.html
  32. 81 0
      packages/ckeditor5-engine/tests/view/manual/fakeselection.js
  33. 23 0
      packages/ckeditor5-engine/tests/view/manual/fakeselection.md
  34. 136 0
      packages/ckeditor5-engine/tests/view/observer/fakeselectionobserver.js
  35. 152 0
      packages/ckeditor5-engine/tests/view/renderer.js
  36. 84 0
      packages/ckeditor5-engine/tests/view/selection.js

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

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

+ 20 - 18
packages/ckeditor5-engine/src/editingcontroller.js → packages/ckeditor5-engine/src/controller/editingcontroller.js

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

+ 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;
+	}
+}

+ 8 - 0
packages/ckeditor5-engine/src/conversion/model-selection-to-view-converters.js

@@ -209,3 +209,11 @@ export function clearAttributes() {
 		conversionApi.viewSelection.removeAllRanges();
 	};
 }
+
+/**
+ * Function factory, creates a converter that clears fake selection marking after the previous
+ * {@link engine.model.Selection model selection} conversion.
+ */
+export function clearFakeSelection() {
+	return ( evt, data, consumable, conversionApi ) => conversionApi.viewSelection.setFake( false );
+}

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

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

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

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

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

@@ -5,6 +5,7 @@
 
 import LivePosition from '../liveposition.js';
 import Position from '../position.js';
+import Element from '../element.js';
 import compareArrays from '../../../utils/comparearrays.js';
 
 /**
@@ -59,5 +60,21 @@ export default function deleteContents( batch, selection, options = {} ) {
 	selection.collapse( startPos );
 	selection.refreshAttributes();
 
+	// 3. Autoparagraphing.
+	// Check if a text is allowed in the new container. If not, try to create a new paragraph (if it's allowed here).
+	if ( shouldAutoparagraph( batch.document, startPos ) ) {
+		const paragraph = new Element( 'paragraph' );
+		batch.insert( startPos, paragraph );
+
+		selection.collapse( paragraph );
+	}
+
 	endPos.detach();
 }
+
+function shouldAutoparagraph( doc, position ) {
+	const isTextAllowed = doc.schema.check( { name: '$text', inside: position } );
+	const isParagraphAllowed = doc.schema.check( { name: 'paragraph', inside: position } );
+
+	return !isTextAllowed && isParagraphAllowed;
+}

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

@@ -250,6 +250,15 @@ export default class Schema {
 	 * Creates Schema instance.
 	 */
 	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.
 		 *
@@ -274,6 +283,13 @@ export default class Schema {
 
 		this.allow( { name: '$block', inside: '$root' } );
 		this.allow( { name: '$inline', inside: '$block' } );
+
+		// TMP!
+		// Create an "all allowed" context in the schema for processing the pasted content.
+		// Read: https://github.com/ckeditor/ckeditor5-engine/issues/638#issuecomment-255086588
+
+		this.registerItem( '$clipboardHolder', '$root' );
+		this.allow( { name: '$inline', inside: '$clipboardHolder' } );
 	}
 
 	/**

+ 4 - 1
packages/ckeditor5-engine/src/view/document.js

@@ -13,6 +13,7 @@ import MutationObserver from './observer/mutationobserver.js';
 import SelectionObserver from './observer/selectionobserver.js';
 import FocusObserver from './observer/focusobserver.js';
 import KeyObserver from './observer/keyobserver.js';
+import FakeSelectionObserver from './observer/fakeselectionobserver.js';
 import mix from '../../utils/mix.js';
 import ObservableMixin from '../../utils/observablemixin.js';
 
@@ -30,7 +31,8 @@ import ObservableMixin from '../../utils/observablemixin.js';
  * * {@link view.observer.MutationObserver},
  * * {@link view.observer.SelectionObserver},
  * * {@link view.observer.FocusObserver},
- * * {@link view.observer.KeyObserver}.
+ * * {@link view.observer.KeyObserver},
+ * * {@link view.observer.FakeSelectionObserver}.
  *
  * @memberOf engine.view
  * @mixes utils.EmitterMixin
@@ -107,6 +109,7 @@ export default class Document {
 		this.addObserver( SelectionObserver );
 		this.addObserver( FocusObserver );
 		this.addObserver( KeyObserver );
+		this.addObserver( FakeSelectionObserver );
 
 		injectQuirksHandling( this );
 

+ 47 - 1
packages/ckeditor5-engine/src/view/domconverter.js

@@ -87,6 +87,36 @@ export default class DomConverter {
 		 * @member {WeakMap} engine.view.DomConverter#_viewToDomMapping
 		 */
 		this._viewToDomMapping = new WeakMap();
+
+		/**
+		 * Holds mapping between fake selection containers and corresponding view selections.
+		 *
+		 * @private
+		 * @member {WeakMap} engine.view.DomConverter#_fakeSelectionMapping
+		 */
+		this._fakeSelectionMapping = new WeakMap();
+	}
+
+	/**
+	 * Binds given DOM element that represents fake selection to {@link engine.view.Selection view selection}.
+	 * View selection copy is stored and can be retrieved by {@link engine.view.DomConverter#fakeSelectionToView} method.
+	 *
+	 * @param {HTMLElement} domElement
+	 * @param {engine.view.Selection} viewSelection
+	 */
+	bindFakeSelection( domElement, viewSelection ) {
+		this._fakeSelectionMapping.set( domElement, ViewSelection.createFromSelection( viewSelection ) );
+	}
+
+	/**
+	 * Returns {@link engine.view.Selection view selection} instance corresponding to given DOM element that represents fake
+	 * selection. Returns `undefined` if binding to given DOM element does not exists.
+	 *
+	 * @param {HTMLElement} domElement
+	 * @returns {engine.view.Selection|undefined}
+	 */
+	fakeSelectionToView( domElement ) {
+		return this._fakeSelectionMapping.get( domElement );
 	}
 
 	/**
@@ -358,8 +388,24 @@ export default class DomConverter {
 	 * @returns {engine.view.Selection} View selection.
 	 */
 	domSelectionToView( domSelection ) {
-		const viewSelection = new ViewSelection();
+		// DOM selection might be placed in fake selection container.
+		// If container contains fake selection - return corresponding view selection.
+		if ( domSelection.rangeCount === 1 ) {
+			let container = domSelection.getRangeAt( 0 ).startContainer;
+
+			// The DOM selection might be moved to the text node inside the fake selection container.
+			if ( this.isText( container ) ) {
+				container = container.parentNode;
+			}
 
+			const viewSelection = this.fakeSelectionToView( container );
+
+			if ( viewSelection ) {
+				return viewSelection;
+			}
+		}
+
+		const viewSelection = new ViewSelection();
 		const isBackward = this.isDomSelectionBackward( domSelection );
 
 		for ( let i = 0; i < domSelection.rangeCount; i++ ) {

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

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

+ 2 - 2
packages/ckeditor5-engine/src/view/observer/domeventobserver.js

@@ -23,8 +23,8 @@ import DomEventData from './domeventdata.js';
  *				return 'click';
  *			}
  *
- *			onDomEvent( domEvt ) {
- *				this.fire( 'click' );
+ *			onDomEvent( domEvent ) {
+ *				this.fire( 'click', domEvent );
  *			}
  *		}
  *

+ 94 - 0
packages/ckeditor5-engine/src/view/observer/fakeselectionobserver.js

@@ -0,0 +1,94 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import Observer from './observer.js';
+import ViewSelection from '../selection.js';
+import { keyCodes } from '../../../utils/keyboard.js';
+
+/**
+ * Fake selection observer class. If view selection is fake it is placed in dummy DOM container. This observer listens
+ * on {@link engine.view.Document#keydown keydown} events and handles moving fake view selection to the correct place
+ * if arrow keys are pressed.
+ * Fires {@link engine.view.Document#selectionChage selectionChange event} simulating natural behaviour of
+ * {@link engine.view.observer.SelectionObserver SelectionObserver}.
+ *
+ * @memberOf engine.view.observer
+ * @extends engine.view.observer.Observer
+ */
+export default class FakeSelectionObserver extends Observer {
+	/**
+	 * Creates new FakeSelectionObserver instance.
+	 *
+	 * @param {engine.view.Document} document
+	 */
+	constructor( document ) {
+		super( document );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	observe() {
+		const document = this.document;
+
+		document.on( 'keydown', ( eventInfo, data ) => {
+			const selection = document.selection;
+
+			if ( selection.isFake && _isArrowKeyCode( data.keyCode ) && this.isEnabled ) {
+				// Prevents default key down handling - no selection change will occur.
+				data.preventDefault();
+
+				this._handleSelectionMove( data.keyCode );
+			}
+		}, { priority: 'lowest' } );
+	}
+
+	/**
+	 * Handles collapsing view selection according to given key code. If left or up key is provided - new selection will be
+	 * collapsed to left. If right or down key is pressed - new selection will be collapsed to right.
+	 *
+	 * This method fires {@link engine.view.Document#selectionChange} event imitating behaviour of
+	 * {@link engine.view.observer.SelectionObserver}.
+	 *
+	 * @private
+	 * @param {Number} keyCode
+	 * @fires engine.view.Document#selectionChage
+	 */
+	_handleSelectionMove( keyCode ) {
+		const selection = this.document.selection;
+		const newSelection = ViewSelection.createFromSelection( selection );
+		newSelection.setFake( false );
+
+		// Left or up arrow pressed - move selection to start.
+		if ( keyCode == keyCodes.arrowleft || keyCode == keyCodes.arrowup ) {
+			newSelection.collapseToStart();
+		}
+
+		// Right or down arrow pressed - move selection to end.
+		if ( keyCode == keyCodes.arrowright || keyCode == keyCodes.arrowdown ) {
+			newSelection.collapseToEnd();
+		}
+
+		// Fire dummy selection change event.
+		this.document.fire( 'selectionChange', {
+			oldSelection: selection,
+			newSelection: newSelection,
+			domSelection: null
+		} );
+	}
+}
+
+// Checks if one of the arrow keys is pressed.
+//
+// @private
+// @param {Number} keyCode
+// @returns {Boolean}
+function _isArrowKeyCode( keyCode ) {
+	return keyCode == keyCodes.arrowright ||
+		keyCode == keyCodes.arrowleft ||
+		keyCode == keyCodes.arrowup ||
+		keyCode == keyCodes.arrowdown;
+}
+

+ 0 - 1
packages/ckeditor5-engine/src/view/observer/selectionobserver.js

@@ -124,7 +124,6 @@ export default class SelectionObserver extends Observer {
 		// If there were mutations then the view will be re-rendered by the mutation observer and selection
 		// will be updated, so selections will equal and event will not be fired, as expected.
 		const domSelection = domDocument.defaultView.getSelection();
-
 		const newViewSelection = this.domConverter.domSelectionToView( domSelection );
 
 		if ( this.selection.isEqual( newViewSelection ) ) {

+ 85 - 6
packages/ckeditor5-engine/src/view/renderer.js

@@ -15,6 +15,8 @@ import remove from '../../utils/dom/remove.js';
 import ObservableMixin from '../../utils/observablemixin.js';
 import CKEditorError from '../../utils/ckeditorerror.js';
 
+/* global Range */
+
 /**
  * Renderer updates DOM structure and selection, to make them a reflection of the view structure and selection.
  *
@@ -101,6 +103,14 @@ export default class Renderer {
 		 * @member {Boolean} engine.view.Renderer#isFocused
 		 */
 		this.isFocused = false;
+
+		/**
+		 * DOM element containing fake selection.
+		 *
+		 * @private
+		 * @type {null|HTMLElement}
+		 */
+		this._fakeSelectionContainer = null;
 	}
 
 	/**
@@ -418,24 +428,75 @@ export default class Renderer {
 	 * @private
 	 */
 	_updateSelection() {
-		// If there is no selection - remove it from DOM elements that belongs to the editor.
+		// If there is no selection - remove DOM and fake selections.
 		if ( this.selection.rangeCount === 0 ) {
 			this._removeDomSelection();
+			this._removeFakeSelection();
 
 			return;
 		}
 
-		if ( !this.isFocused ) {
+		const domRoot = this.domConverter.getCorrespondingDomElement( this.selection.editableElement );
+
+		// Do nothing if there is no focus, or there is no DOM element corresponding to selection's editable element.
+		if ( !this.isFocused || !domRoot ) {
 			return;
 		}
 
-		const selectedEditable = this.selection.editableElement;
-		const domRoot = this.domConverter.getCorrespondingDomElement( selectedEditable );
+		// Render selection.
+		if ( this.selection.isFake ) {
+			this._updateFakeSelection( domRoot );
+		} else {
+			this._removeFakeSelection();
+			this._updateDomSelection( domRoot );
+		}
+	}
 
-		if ( !domRoot ) {
-			return;
+	/**
+	 * Updates fake selection.
+	 *
+	 * @private
+	 * @param {HTMLElement} domRoot Valid DOM root where fake selection container should be added.
+	 */
+	_updateFakeSelection( domRoot ) {
+		const domDocument = domRoot.ownerDocument;
+
+		// Create fake selection container if one does not exist.
+		if ( !this._fakeSelectionContainer ) {
+			this._fakeSelectionContainer = domDocument.createElement( 'div' );
+			this._fakeSelectionContainer.style.position = 'fixed';
+			this._fakeSelectionContainer.style.top = 0;
+			this._fakeSelectionContainer.style.left = '-9999px';
+			this._fakeSelectionContainer.appendChild( domDocument.createTextNode( '\u00A0' ) );
 		}
 
+		// Add fake container if not already added.
+		if ( !this._fakeSelectionContainer.parentElement ) {
+			domRoot.appendChild( this._fakeSelectionContainer );
+		}
+
+		// Update contents.
+		const content = this.selection.fakeSelectionLabel || '\u00A0';
+		this._fakeSelectionContainer.firstChild.data = content;
+
+		// Update selection.
+		const domSelection = domDocument.getSelection();
+		domSelection.removeAllRanges();
+		const domRange = new Range();
+		domRange.selectNodeContents( this._fakeSelectionContainer );
+		domSelection.addRange( domRange );
+
+		// Bind fake selection container with current selection.
+		this.domConverter.bindFakeSelection( this._fakeSelectionContainer, this.selection );
+	}
+
+	/**
+	 * Updates DOM selection.
+	 *
+	 * @private
+	 * @param {HTMLElement} domRoot Valid DOM root where DOM selection should be rendered.
+	 */
+	_updateDomSelection( domRoot ) {
 		const domSelection = domRoot.ownerDocument.defaultView.getSelection();
 		const oldViewSelection = domSelection && this.domConverter.domSelectionToView( domSelection );
 
@@ -455,6 +516,11 @@ export default class Renderer {
 		domSelection.extend( focus.parent, focus.offset );
 	}
 
+	/**
+	 * Removes DOM selection.
+	 *
+	 * @private
+	 */
 	_removeDomSelection() {
 		for ( let doc of this.domDocuments ) {
 			const domSelection = doc.getSelection();
@@ -470,6 +536,19 @@ export default class Renderer {
 		}
 	}
 
+	/**
+	 * Removes fake selection.
+	 *
+	 * @private
+	 */
+	_removeFakeSelection() {
+		const container = this._fakeSelectionContainer;
+
+		if ( container ) {
+			container.remove();
+		}
+	}
+
 	/**
 	 * Checks if focus needs to be updated and possibly updates it.
 	 *

+ 67 - 0
packages/ckeditor5-engine/src/view/selection.js

@@ -45,6 +45,62 @@ export default class Selection {
 		 * @member {Boolean} engine.view.Selection#_lastRangeBackward
 		 */
 		this._lastRangeBackward = false;
+
+		/**
+		 * Specifies whether selection instance is fake.
+		 *
+		 * @private
+		 * @member {Boolean} engine.view.Selection#_isFake
+		 */
+		this._isFake = false;
+
+		/**
+		 * Fake selection's label.
+		 *
+		 * @private
+		 * @member {String} engine.view.Selection#_fakeSelectionLabel
+		 */
+		this._fakeSelectionLabel = '';
+	}
+
+	/**
+	 * Sets this selection instance to be marked as `fake`. A fake selection does not render as browser native selection
+	 * over selected elements and is hidden to the user. This way, no native selection UI artifacts are displayed to
+	 * the user and selection over elements can be represented in other way, for example by applying proper CSS class.
+	 *
+	 * Additionally fake's selection label can be provided. It will be used to describe fake selection in DOM (and be
+	 * properly handled by screen readers).
+	 *
+	 * @fires engine.view.Selection#change
+	 * @param {Boolean} [value=true] If set to true selection will be marked as `fake`.
+	 * @param {Object} [options] Additional options.
+	 * @param {String} [options.label=''] Fake selection label.
+	 */
+	setFake( value = true, options = {} ) {
+		this._isFake = value;
+		this._fakeSelectionLabel = value ? options.label || '' : '';
+
+		this.fire( 'change' );
+	}
+
+	/**
+	 * Returns true if selection instance is marked as `fake`.
+	 *
+	 * @see {@link engine.view.Selection#setFake}
+	 * @returns {Boolean}
+	 */
+	get isFake() {
+		return this._isFake;
+	}
+
+	/**
+	 * Returns fake selection label.
+	 *
+	 * @see {@link engine.view.Selection#setFake}
+	 * @returns {String}
+	 */
+	get fakeSelectionLabel() {
+		return this._fakeSelectionLabel;
 	}
 
 	/**
@@ -239,6 +295,14 @@ export default class Selection {
 			return false;
 		}
 
+		if ( this.isFake != otherSelection.isFake ) {
+			return false;
+		}
+
+		if ( this.isFake && this.fakeSelectionLabel != otherSelection.fakeSelectionLabel ) {
+			return false;
+		}
+
 		for ( let i = 0; i < this.rangeCount; i++ ) {
 			if ( !this._ranges[ i ].isEqual( otherSelection._ranges[ i ] ) ) {
 				return false;
@@ -292,6 +356,9 @@ export default class Selection {
 	 * @param {engine.view.Selection} otherSelection
 	 */
 	setTo( otherSelection ) {
+		this._isFake = otherSelection._isFake;
+		this._fakeSelectionLabel = otherSelection._fakeSelectionLabel;
+
 		this.setRanges( otherSelection.getRanges(), otherSelection.isBackward );
 	}
 

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

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

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

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

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

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

+ 13 - 1
packages/ckeditor5-engine/tests/conversion/model-selection-to-view-converters.js

@@ -21,7 +21,8 @@ import {
 	convertRangeSelection,
 	convertCollapsedSelection,
 	convertSelectionAttribute,
-	clearAttributes
+	clearAttributes,
+	clearFakeSelection
 } from '/ckeditor5/engine/conversion/model-selection-to-view-converters.js';
 
 import {
@@ -309,6 +310,17 @@ describe( 'clean-up', () => {
 			expect( viewString ).to.equal( '<div>f{}oobar</div>' );
 		} );
 	} );
+
+	describe( 'clearFakeSelection', () => {
+		it( 'should clear fake selection', () => {
+			dispatcher.on( 'selection', clearFakeSelection() );
+			viewSelection.setFake( true );
+
+			dispatcher.convertSelection( modelSelection );
+
+			expect( viewSelection.isFake ).to.be.false;
+		} );
+	} );
 } );
 
 describe( 'using element creator for attributes conversion', () => {

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

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

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

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

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

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

+ 5 - 1
packages/ckeditor5-engine/tests/tickets/401/1.js

@@ -6,9 +6,13 @@
 /* globals console, document, window */
 
 import ClassicEditor from '/ckeditor5/editor-classic/classic.js';
+import Enter from '/ckeditor5/enter/enter.js';
+import Typing from '/ckeditor5/typing/typing.js';
+import Paragraph from '/ckeditor5/paragraph/paragraph.js';
+import Bold from '/ckeditor5/basic-styles/bold.js';
 
 ClassicEditor.create( document.getElementById( 'editor' ), {
-	features: [ 'enter', 'typing', 'paragraph', 'basic-styles/bold' ],
+	features: [ Enter, Typing, Paragraph, Bold ],
 	toolbar: [ 'bold' ]
 } )
 .then( editor => {

+ 12 - 4
packages/ckeditor5-engine/tests/tickets/462/1.js

@@ -6,9 +6,14 @@
 /* globals console, window, document, setInterval */
 
 import ClassicEditor from '/ckeditor5/editor-classic/classic.js';
+import Enter from '/ckeditor5/enter/enter.js';
+import Typing from '/ckeditor5/typing/typing.js';
+import Paragraph from '/ckeditor5/paragraph/paragraph.js';
+import Bold from '/ckeditor5/basic-styles/bold.js';
+import Italic from '/ckeditor5/basic-styles/italic.js';
 
 ClassicEditor.create( document.querySelector( '#editor' ), {
-	features: [ 'enter', 'typing', 'paragraph', 'basic-styles/bold', 'basic-styles/italic' ],
+	features: [ Enter, Typing, Paragraph, Bold, Italic ],
 	toolbar: [ 'bold', 'italic' ]
 } )
 .then( editor => {
@@ -17,10 +22,13 @@ ClassicEditor.create( document.querySelector( '#editor' ), {
 	setInterval( () => {
 		console.clear();
 
+		const domSelection = document.getSelection();
+		const selectionExists = domSelection && domSelection.anchorNode;
+
 		console.log( editor.editing.view.getDomRoot().innerHTML.replace( /\u200b/g, '@' ) );
-		console.log( 'selection.hasAttribute( italic )', editor.document.selection.hasAttribute( 'italic' ) );
-		console.log( 'selection.hasAttribute( bold )', editor.document.selection.hasAttribute( 'bold' ) );
-		console.log( 'selection anchor\'s parentNode', document.getSelection().anchorNode.parentNode );
+		console.log( 'selection.hasAttribute( italic ):', editor.document.selection.hasAttribute( 'italic' ) );
+		console.log( 'selection.hasAttribute( bold ):', editor.document.selection.hasAttribute( 'bold' ) );
+		console.log( 'selection anchor\'s parentNode:', selectionExists ? domSelection.anchorNode.parentNode : 'no DOM selection' );
 	}, 2000 );
 } )
 .catch( err => {

+ 6 - 1
packages/ckeditor5-engine/tests/tickets/475/1.js

@@ -19,6 +19,11 @@ import buildViewConverter from '/ckeditor5/engine/conversion/buildviewconverter.
 
 import AttributeElement from '/ckeditor5/engine/view/attributeelement.js';
 
+import Enter from '/ckeditor5/enter/enter.js';
+import Typing from '/ckeditor5/typing/typing.js';
+import Paragraph from '/ckeditor5/paragraph/paragraph.js';
+import Undo from '/ckeditor5/undo/undo.js';
+
 class Link extends Feature {
 	init() {
 		const editor = this.editor;
@@ -87,6 +92,6 @@ function _getLastPathPart( path ) {
 }
 
 ClassicEditor.create( document.querySelector( '#editor' ), {
-	features: [ 'enter', 'typing', 'paragraph', 'undo', Link, AutoLinker ],
+	features: [ Enter, Typing, Paragraph, Undo, Link, AutoLinker ],
 	toolbar: [ 'undo', 'redo' ]
 } );

+ 7 - 1
packages/ckeditor5-engine/tests/tickets/603/1.js

@@ -6,9 +6,15 @@
 /* globals console, window, document */
 
 import ClassicEditor from '/ckeditor5/editor-classic/classic.js';
+import Enter from '/ckeditor5/enter/enter.js';
+import Typing from '/ckeditor5/typing/typing.js';
+import Heading from '/ckeditor5/heading/heading.js';
+import Paragraph from '/ckeditor5/paragraph/paragraph.js';
+import Bold from '/ckeditor5/basic-styles/bold.js';
+import Italic from '/ckeditor5/basic-styles/italic.js';
 
 ClassicEditor.create( document.querySelector( '#editor' ), {
-	features: [ 'enter', 'typing', 'paragraph', 'heading', 'basic-styles/bold', 'basic-styles/italic' ],
+	features: [ Enter, Typing, Paragraph, Heading, Bold, Italic ],
 	toolbar: [ 'headings', 'bold', 'italic' ]
 } )
 .then( editor => {

+ 3 - 1
packages/ckeditor5-engine/tests/view/document/document.js

@@ -12,6 +12,7 @@ import MutationObserver from '/ckeditor5/engine/view/observer/mutationobserver.j
 import SelectionObserver from '/ckeditor5/engine/view/observer/selectionobserver.js';
 import FocusObserver from '/ckeditor5/engine/view/observer/focusobserver.js';
 import KeyObserver from '/ckeditor5/engine/view/observer/keyobserver.js';
+import FakeSelectionObserver from '/ckeditor5/engine/view/observer/fakeselectionobserver.js';
 import Renderer from '/ckeditor5/engine/view/renderer.js';
 import ViewRange from '/ckeditor5/engine/view/range.js';
 import DomConverter from '/ckeditor5/engine/view/domconverter.js';
@@ -22,7 +23,7 @@ import log from '/ckeditor5/utils/log.js';
 testUtils.createSinonSandbox();
 
 describe( 'Document', () => {
-	const DEFAULT_OBSERVERS_COUNT = 4;
+	const DEFAULT_OBSERVERS_COUNT = 5;
 	let ObserverMock, ObserverMockGlobalCount, instantiated, enabled;
 
 	beforeEach( () => {
@@ -72,6 +73,7 @@ describe( 'Document', () => {
 			expect( viewDocument.getObserver( SelectionObserver ) ).to.be.instanceof( SelectionObserver );
 			expect( viewDocument.getObserver( FocusObserver ) ).to.be.instanceof( FocusObserver );
 			expect( viewDocument.getObserver( KeyObserver ) ).to.be.instanceof( KeyObserver );
+			expect( viewDocument.getObserver( FakeSelectionObserver ) ).to.be.instanceof( FakeSelectionObserver );
 		} );
 	} );
 

+ 32 - 0
packages/ckeditor5-engine/tests/view/domconverter/binding.js

@@ -7,6 +7,8 @@
 /* bender-tags: view, domconverter, browser-only */
 
 import ViewElement from '/ckeditor5/engine/view/element.js';
+import ViewSelection from '/ckeditor5/engine/view/selection.js';
+import ViewRange from '/ckeditor5/engine/view/range.js';
 import DomConverter from '/ckeditor5/engine/view/domconverter.js';
 import ViewDocumentFragment from '/ckeditor5/engine/view/documentfragment.js';
 import { INLINE_FILLER } from '/ckeditor5/engine/view/filler.js';
@@ -334,4 +336,34 @@ describe( 'DomConverter', () => {
 			expect( converter.getCorrespondingDomText( viewText ) ).to.be.null;
 		} );
 	} );
+
+	describe( 'bindFakeSelection', () => {
+		let domEl, selection, viewElement;
+
+		beforeEach( () => {
+			viewElement = new ViewElement();
+			domEl = document.createElement( 'div' );
+			selection = new ViewSelection();
+			selection.addRange( ViewRange.createIn( viewElement ) );
+			converter.bindFakeSelection( domEl, selection );
+		} );
+
+		it( 'should bind DOM element to selection', () => {
+			const bindSelection = converter.fakeSelectionToView( domEl );
+			expect( bindSelection ).to.be.defined;
+			expect( bindSelection.isEqual( selection ) ).to.be.true;
+		} );
+
+		it( 'should keep a copy of selection', () => {
+			const selectionCopy = ViewSelection.createFromSelection( selection );
+
+			selection.addRange( ViewRange.createIn( new ViewElement() ), true );
+			const bindSelection = converter.fakeSelectionToView( domEl );
+
+			expect( bindSelection ).to.be.defined;
+			expect( bindSelection ).to.not.equal( selection );
+			expect( bindSelection.isEqual( selection ) ).to.be.false;
+			expect( bindSelection.isEqual( selectionCopy ) ).to.be.true;
+		} );
+	} );
 } );

+ 42 - 0
packages/ckeditor5-engine/tests/view/domconverter/dom-to-view.js

@@ -7,6 +7,8 @@
 /* bender-tags: view, domconverter, browser-only */
 
 import ViewElement from '/ckeditor5/engine/view/element.js';
+import ViewRange from '/ckeditor5/engine/view/range.js';
+import ViewSelection from '/ckeditor5/engine/view/selection.js';
 import DomConverter from '/ckeditor5/engine/view/domconverter.js';
 import ViewDocumentFragment from '/ckeditor5/engine/view/documentfragment.js';
 import { INLINE_FILLER, INLINE_FILLER_LENGTH, NBSP_FILLER } from '/ckeditor5/engine/view/filler.js';
@@ -653,5 +655,45 @@ describe( 'DomConverter', () => {
 
 			expect( viewSelection.rangeCount ).to.equal( 0 );
 		} );
+
+		it( 'should return fake selection', () => {
+			const domContainer = document.createElement( 'div' );
+			const domSelection = document.getSelection();
+			domContainer.innerHTML = 'fake selection container';
+			document.body.appendChild( domContainer );
+
+			const viewSelection = new ViewSelection();
+			viewSelection.addRange( ViewRange.createIn( new ViewElement() ) );
+			converter.bindFakeSelection( domContainer, viewSelection );
+
+			const domRange = new Range();
+			domRange.selectNodeContents( domContainer );
+			domSelection.removeAllRanges();
+			domSelection.addRange( domRange );
+
+			const bindViewSelection = converter.domSelectionToView( domSelection );
+
+			expect( bindViewSelection.isEqual( viewSelection ) ).to.be.true;
+		} );
+
+		it( 'should return fake selection if selection is placed inside text node', () => {
+			const domContainer = document.createElement( 'div' );
+			const domSelection = document.getSelection();
+			domContainer.innerHTML = 'fake selection container';
+			document.body.appendChild( domContainer );
+
+			const viewSelection = new ViewSelection();
+			viewSelection.addRange( ViewRange.createIn( new ViewElement() ) );
+			converter.bindFakeSelection( domContainer, viewSelection );
+
+			const domRange = new Range();
+			domRange.selectNodeContents( domContainer.firstChild );
+			domSelection.removeAllRanges();
+			domSelection.addRange( domRange );
+
+			const bindViewSelection = converter.domSelectionToView( domSelection );
+
+			expect( bindViewSelection.isEqual( viewSelection ) ).to.be.true;
+		} );
 	} );
 } );

+ 28 - 0
packages/ckeditor5-engine/tests/view/manual/fakeselection.html

@@ -0,0 +1,28 @@
+<style>
+	#editor {
+		padding: 5px;
+		border: solid 1px #000;
+	}
+
+	#editor strong {
+		cursor: pointer;
+	}
+
+	#editor strong.selected {
+		background-color: #dadada;
+	}
+
+	#editor strong.selected.focused {
+		background-color: yellow;
+	}
+</style>
+
+<p>Scroll down.</p>
+
+<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>
+<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>
+
+<div contenteditable="true" id="editor"></div>
+
+<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>
+<br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br>

+ 81 - 0
packages/ckeditor5-engine/tests/view/manual/fakeselection.js

@@ -0,0 +1,81 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals document, console */
+
+import ViewDocument from '/ckeditor5/engine/view/document.js';
+import DomEventObserver from '/ckeditor5/engine/view/observer/domeventobserver.js';
+import ViewRange from '/ckeditor5/engine/view/range.js';
+import { setData } from '/ckeditor5/engine/dev-utils/view.js';
+
+const viewDocument = new ViewDocument();
+const domEditable = document.getElementById( 'editor' );
+const viewRoot = viewDocument.createRoot( domEditable );
+let viewStrong;
+
+// Add mouseup oberver.
+viewDocument.addObserver( class extends DomEventObserver {
+	get domEventType() {
+		return [ 'mousedown', 'mouseup' ];
+	}
+
+	onDomEvent( domEvent ) {
+		this.fire( domEvent.type, domEvent );
+	}
+} );
+
+viewDocument.on( 'selectionChange', ( evt, data ) => {
+	viewDocument.selection.setTo( data.newSelection );
+	viewDocument.render();
+} );
+
+viewDocument.on( 'mouseup', ( evt, data ) => {
+	if ( data.target == viewStrong ) {
+		console.log( 'Making selection around the <strong>.' );
+
+		const range = ViewRange.createOn( viewStrong );
+		viewDocument.selection.setRanges( [ range ] );
+		viewDocument.selection.setFake( true, { label: 'fake selection over bar' } );
+
+		viewDocument.render();
+
+		data.preventDefault();
+	}
+} );
+
+viewDocument.selection.on( 'change', () => {
+	if ( !viewStrong ) {
+		return;
+	}
+
+	const firstPos = viewDocument.selection.getFirstPosition();
+	const lastPos = viewDocument.selection.getLastPosition();
+
+	if ( firstPos && lastPos && firstPos.nodeAfter == viewStrong && lastPos.nodeBefore == viewStrong ) {
+		viewStrong.addClass( 'selected' );
+	} else {
+		viewStrong.removeClass( 'selected' );
+	}
+} );
+
+viewDocument.on( 'focus', () => {
+	viewStrong.addClass( 'focused' );
+	viewDocument.render();
+
+	console.log( 'The document was focused.' );
+} );
+
+viewDocument.on( 'blur', () => {
+	viewStrong.removeClass( 'focused' );
+	viewDocument.render();
+
+	console.log( 'The document was blurred.' );
+} );
+
+setData( viewDocument, '<container:p>{}foo<strong contenteditable="false">bar</strong>baz</container:p>' );
+const viewP = viewRoot.getChild( 0 );
+viewStrong = viewP.getChild( 1 );
+
+viewDocument.focus();

+ 23 - 0
packages/ckeditor5-engine/tests/view/manual/fakeselection.md

@@ -0,0 +1,23 @@
+@bender-ui: collapsed
+@bender-tags: view
+
+## Fake selection
+
+Click on bold `bar` to create fake selection over it before each of following steps:
+   * Press left/up arrow key - collapsed selection should appear before `bar`
+   * Press right/down arrow key - collapsed selection should appear after `bar`
+
+Notes:
+
+- Focus shouldn't disappear from the editable element.
+- No #blur event should be logged on the console.
+- The viewport shouldn't scroll when selecting the `bar`.
+
+-----
+
+Open console and check if `<div style="position: fixed; top: 0px; left: -9999px;">fake selection over bar</div>` is added to editable when fake selection is present. It should be removed when fake selection is not present.
+
+-----
+
+Click on bold `bar` to create fake selection over it. Click outside editable and check if yellow fake selection turns to gray one.
+

+ 136 - 0
packages/ckeditor5-engine/tests/view/observer/fakeselectionobserver.js

@@ -0,0 +1,136 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals document */
+
+import FakeSelectionObserver from '/ckeditor5/engine/view/observer/fakeselectionobserver.js';
+import ViewDocument from '/ckeditor5/engine/view/document.js';
+import DomEventData from '/ckeditor5/engine/view/observer/domeventdata.js';
+import { keyCodes } from '/ckeditor5/utils/keyboard.js';
+import { setData, stringify } from '/ckeditor5/engine/dev-utils/view.js';
+
+describe( 'FakeSelectionObserver', () => {
+	let observer;
+	let viewDocument;
+	let root;
+
+	beforeEach( () => {
+		viewDocument = new ViewDocument();
+		root = viewDocument.createRoot( document.getElementById( 'main' ) );
+		observer = viewDocument.getObserver( FakeSelectionObserver );
+		viewDocument.selection.setFake();
+	} );
+
+	it( 'should do nothing if selection is not fake', () => {
+		viewDocument.selection.setFake( false );
+
+		return checkEventPrevention( keyCodes.arrowleft, false );
+	} );
+
+	it( 'should do nothing if is disabled', () => {
+		observer.disable();
+
+		return checkEventPrevention( keyCodes.arrowleft, false );
+	} );
+
+	it( 'should prevent default for left arrow key', ( ) => {
+		return checkEventPrevention( keyCodes.arrowleft );
+	} );
+
+	it( 'should prevent default for right arrow key', ( ) => {
+		return checkEventPrevention( keyCodes.arrowright );
+	} );
+
+	it( 'should prevent default for up arrow key', ( ) => {
+		return checkEventPrevention( keyCodes.arrowup );
+	} );
+
+	it( 'should prevent default for down arrow key', ( ) => {
+		return checkEventPrevention( keyCodes.arrowdown );
+	} );
+
+	it( 'should fire selectionChange event with new selection when left arrow key is pressed', () => {
+		return checkSelectionChange(
+			'<container:p>foo[<strong>bar</strong>]baz</container:p>',
+			keyCodes.arrowleft,
+			'<container:p>foo[]<strong>bar</strong>baz</container:p>'
+		);
+	} );
+
+	it( 'should fire selectionChange event with new selection when right arrow key is pressed', () => {
+		return checkSelectionChange(
+			'<container:p>foo[<strong>bar</strong>]baz</container:p>',
+			keyCodes.arrowright,
+			'<container:p>foo<strong>bar</strong>[]baz</container:p>'
+		);
+	} );
+
+	it( 'should fire selectionChange event with new selection when up arrow key is pressed', () => {
+		return checkSelectionChange(
+			'<container:p>foo[<strong>bar</strong>]baz</container:p>',
+			keyCodes.arrowup,
+			'<container:p>foo[]<strong>bar</strong>baz</container:p>'
+		);
+	} );
+
+	it( 'should fire selectionChange event with new selection when down arrow key is pressed', () => {
+		return checkSelectionChange(
+			'<container:p>foo[<strong>bar</strong>]baz</container:p>',
+			keyCodes.arrowdown,
+			'<container:p>foo<strong>bar</strong>[]baz</container:p>'
+		);
+	} );
+
+	// Checks if preventDefault method was called by FakeSelectionObserver for specified key code.
+	//
+	// @param {Number} keyCode
+	// @param {Boolean} shouldPrevent If set to true method checks if event was prevented.
+	// @returns {Promise}
+	function checkEventPrevention( keyCode, shouldPrevent = true ) {
+		return new Promise( resolve => {
+			const data = {
+				keyCode,
+				preventDefault: sinon.spy(),
+			};
+
+			viewDocument.once( 'keydown', () => {
+				if ( shouldPrevent ) {
+					sinon.assert.calledOnce( data.preventDefault );
+				} else {
+					sinon.assert.notCalled( data.preventDefault );
+				}
+
+				resolve();
+			}, { priority: 'lowest' } );
+
+			viewDocument.fire( 'keydown', new DomEventData( viewDocument, { target: document.body }, data ) );
+		} );
+	}
+
+	// Checks if proper selectionChange event is fired by FakeSelectionObserver for specified key.
+	//
+	// @param {String} initialData
+	// @param {Number} keyCode
+	// @param {String} output
+	// @returns {Promise}
+	function checkSelectionChange( initialData, keyCode, output ) {
+		return new Promise( resolve => {
+			viewDocument.once( 'selectionChange', ( eventInfo, data ) => {
+				expect( stringify( root.getChild( 0 ), data.newSelection, { showType: true } ) ).to.equal( output );
+				resolve();
+			} );
+
+			setData( viewDocument, initialData );
+			viewDocument.selection.setFake();
+
+			const data = {
+				keyCode,
+				preventDefault: sinon.spy(),
+			};
+
+			viewDocument.fire( 'keydown', new DomEventData( viewDocument, { target: document.body }, data ) );
+		} );
+	}
+} );

+ 152 - 0
packages/ckeditor5-engine/tests/view/renderer.js

@@ -117,6 +117,7 @@ describe( 'Renderer', () => {
 			renderer.markedChildren.clear();
 
 			selection.removeAllRanges();
+			selection.setFake( false );
 
 			selectionEditable = viewRoot;
 
@@ -988,6 +989,157 @@ describe( 'Renderer', () => {
 
 			expect( domFocusSpy.called ).to.be.false;
 		} );
+
+		describe( 'fake selection', () => {
+			beforeEach( () => {
+				const { view: viewP, selection: newSelection } = parse(
+					'<container:p>[foo bar]</container:p>'
+				);
+				viewRoot.appendChildren( viewP );
+				selection.setTo( newSelection );
+				renderer.markToSync( 'children', viewRoot );
+				renderer.render();
+			} );
+
+			it( 'should render fake selection', () => {
+				const label = 'fake selection label';
+				selection.setFake( true, { label } );
+				renderer.render();
+
+				expect( domRoot.childNodes.length ).to.equal( 2 );
+				const container = domRoot.childNodes[ 1 ];
+				expect( domConverter.getCorrespondingViewElement( container ) ).to.be.undefined;
+				expect( container.childNodes.length ).to.equal( 1 );
+				const textNode = container.childNodes[ 0 ];
+				expect( textNode.textContent ).to.equal( label );
+				const domSelection = domRoot.ownerDocument.getSelection();
+				expect( domSelection.anchorNode ).to.equal( textNode );
+				expect( domSelection.anchorOffset ).to.equal( 0 );
+				expect( domSelection.focusNode ).to.equal( textNode );
+				expect( domSelection.focusOffset ).to.equal( label.length );
+			} );
+
+			it( 'should render &nbsp; if no selection label is provided', () => {
+				selection.setFake( true );
+				renderer.render();
+
+				expect( domRoot.childNodes.length ).to.equal( 2 );
+				const container = domRoot.childNodes[ 1 ];
+				expect( container.childNodes.length ).to.equal( 1 );
+				const textNode = container.childNodes[ 0 ];
+				expect( textNode.textContent ).to.equal( '\u00A0' );
+				const domSelection = domRoot.ownerDocument.getSelection();
+				expect( domSelection.anchorNode ).to.equal( textNode );
+				expect( domSelection.anchorOffset ).to.equal( 0 );
+				expect( domSelection.focusNode ).to.equal( textNode );
+				expect( domSelection.focusOffset ).to.equal( 1 );
+			} );
+
+			it( 'should remove fake selection container when selection is no longer fake', () => {
+				selection.setFake( true );
+				renderer.render();
+
+				selection.setFake( false );
+				renderer.render();
+
+				expect( domRoot.childNodes.length ).to.equal( 1 );
+				const domParagraph = domRoot.childNodes[ 0 ];
+				expect( domParagraph.childNodes.length ).to.equal( 1 );
+				const textNode = domParagraph.childNodes[ 0 ];
+				expect( domParagraph.tagName.toLowerCase() ).to.equal( 'p' );
+				const domSelection = domRoot.ownerDocument.getSelection();
+
+				expect( domSelection.anchorNode ).to.equal( textNode );
+				expect( domSelection.anchorOffset ).to.equal( 0 );
+				expect( domSelection.focusNode ).to.equal( textNode );
+				expect( domSelection.focusOffset ).to.equal( 7 );
+			} );
+
+			it( 'should reuse fake selection container #1', () => {
+				const label = 'fake selection label';
+
+				selection.setFake( true, { label } );
+				renderer.render();
+
+				expect( domRoot.childNodes.length ).to.equal( 2 );
+				const container = domRoot.childNodes[ 1 ];
+
+				selection.setFake( true, { label } );
+				renderer.render();
+
+				expect( domRoot.childNodes.length ).to.equal( 2 );
+				const newContainer = domRoot.childNodes[ 1 ];
+				expect( newContainer ).equals( container );
+				expect( newContainer.childNodes.length ).to.equal( 1 );
+				const textNode = newContainer.childNodes[ 0 ];
+				expect( textNode.textContent ).to.equal( label );
+			} );
+
+			it( 'should reuse fake selection container #2', () => {
+				selection.setFake( true, { label: 'label 1' } );
+				renderer.render();
+
+				expect( domRoot.childNodes.length ).to.equal( 2 );
+				const container = domRoot.childNodes[ 1 ];
+
+				selection.setFake( false );
+				renderer.render();
+
+				expect( domRoot.childNodes.length ).to.equal( 1 );
+
+				selection.setFake( true, { label: 'label 2' } );
+				renderer.render();
+
+				expect( domRoot.childNodes.length ).to.equal( 2 );
+				const newContainer = domRoot.childNodes[ 1 ];
+				expect( newContainer ).equals( container );
+				expect( newContainer.childNodes.length ).to.equal( 1 );
+				const textNode = newContainer.childNodes[ 0 ];
+				expect( textNode.textContent ).to.equal( 'label 2' );
+			} );
+
+			it( 'should reuse fake selection container #3', () => {
+				selection.setFake( true, { label: 'label 1' } );
+				renderer.render();
+
+				expect( domRoot.childNodes.length ).to.equal( 2 );
+				const container = domRoot.childNodes[ 1 ];
+
+				selection.setFake( true, { label: 'label 2' } );
+				renderer.render();
+
+				expect( domRoot.childNodes.length ).to.equal( 2 );
+				const newContainer = domRoot.childNodes[ 1 ];
+				expect( newContainer ).equals( container );
+				expect( newContainer.childNodes.length ).to.equal( 1 );
+				const textNode = newContainer.childNodes[ 0 ];
+				expect( textNode.textContent ).to.equal( 'label 2' );
+			} );
+
+			it( 'should style fake selection container properly', () => {
+				selection.setFake( true, { label: 'fake selection' } );
+				renderer.render();
+
+				expect( domRoot.childNodes.length ).to.equal( 2 );
+				const container = domRoot.childNodes[ 1 ];
+
+				expect( container.style.position ).to.equal( 'fixed' );
+				expect( container.style.top ).to.equal( '0px' );
+				expect( container.style.left ).to.equal( '-9999px' );
+			} );
+
+			it( 'should bind fake selection container to view selection', () => {
+				selection.setFake( true, { label: 'fake selection' } );
+				renderer.render();
+
+				expect( domRoot.childNodes.length ).to.equal( 2 );
+				const container = domRoot.childNodes[ 1 ];
+
+				const bindSelection = renderer.domConverter.fakeSelectionToView( container );
+				expect( bindSelection ).to.be.defined;
+				expect( bindSelection.isEqual( selection ) ).to.be.true;
+			} );
+		} );
 	} );
 } );
 

+ 84 - 0
packages/ckeditor5-engine/tests/view/selection.js

@@ -460,6 +460,33 @@ describe( 'Selection', () => {
 
 			expect( selection.isEqual( otherSelection ) ).to.be.false;
 		} );
+
+		it( 'should return false if one selection is fake', () => {
+			const otherSelection = new Selection();
+			otherSelection.setFake( true );
+
+			expect( selection.isEqual( otherSelection ) ).to.be.false;
+		} );
+
+		it( 'should return true if both selection are fake', () => {
+			const otherSelection = new Selection();
+			otherSelection.addRange( range1 );
+			otherSelection.setFake( true );
+			selection.setFake( true );
+			selection.addRange( range1 );
+
+			expect( selection.isEqual( otherSelection ) ).to.be.true;
+		} );
+
+		it( 'should return false if both selection are fake but have different label', () => {
+			const otherSelection = new Selection();
+			otherSelection.addRange( range1 );
+			otherSelection.setFake( true , { label: 'foo bar baz' } );
+			selection.setFake( true );
+			selection.addRange( range1 );
+
+			expect( selection.isEqual( otherSelection ) ).to.be.false;
+		} );
 	} );
 
 	describe( 'removeAllRanges', () => {
@@ -538,6 +565,16 @@ describe( 'Selection', () => {
 
 			selection.setTo( otherSelection );
 		} );
+
+		it( 'should set fake state and label', () => {
+			const otherSelection = new Selection();
+			const label = 'foo bar baz';
+			otherSelection.setFake( true, { label } );
+			selection.setTo( otherSelection );
+
+			expect( selection.isFake ).to.be.true;
+			expect( selection.fakeSelectionLabel ).to.equal( label );
+		} );
 	} );
 
 	describe( 'collapse', () => {
@@ -690,4 +727,51 @@ describe( 'Selection', () => {
 			}
 		} );
 	} );
+
+	describe( 'isFake', () => {
+		it( 'should be false for newly created instance', () => {
+			expect( selection.isFake ).to.be.false;
+		} );
+	} );
+
+	describe( 'setFake', () => {
+		it( 'should allow to set selection to fake', () => {
+			selection.setFake( true );
+
+			expect( selection.isFake ).to.be.true;
+		} );
+
+		it( 'should allow to set fake selection label', () => {
+			const label = 'foo bar baz';
+			selection.setFake( true, { label } );
+
+			expect( selection.fakeSelectionLabel ).to.equal( label );
+		} );
+
+		it( 'should not set label when set to false', () => {
+			const label = 'foo bar baz';
+			selection.setFake( false, { label } );
+
+			expect( selection.fakeSelectionLabel ).to.equal( '' );
+		} );
+
+		it( 'should reset label when set to false', () => {
+			const label = 'foo bar baz';
+			selection.setFake( true, { label } );
+			selection.setFake( false );
+
+			expect( selection.fakeSelectionLabel ).to.equal( '' );
+		} );
+
+		it( 'should fire change event', ( done ) => {
+			selection.once( 'change', () => {
+				expect( selection.isFake ).to.be.true;
+				expect( selection.fakeSelectionLabel ).to.equal( 'foo bar baz' );
+
+				done();
+			} );
+
+			selection.setFake( true, { label: 'foo bar baz' } );
+		} );
+	} );
 } );