浏览代码

Merge branch 'master' into t/1125

Piotrek Koszuliński 7 年之前
父节点
当前提交
4b803b4c1e
共有 51 个文件被更改,包括 2345 次插入347 次删除
  1. 1 1
      packages/ckeditor5-engine/README.md
  2. 1 1
      packages/ckeditor5-engine/package.json
  3. 24 0
      packages/ckeditor5-engine/src/conversion/conversion.js
  4. 24 6
      packages/ckeditor5-engine/src/conversion/downcast-converters.js
  5. 6 6
      packages/ckeditor5-engine/src/conversion/downcast-selection-converters.js
  6. 3 3
      packages/ckeditor5-engine/src/conversion/upcast-selection-converters.js
  7. 19 17
      packages/ckeditor5-engine/src/dev-utils/view.js
  8. 24 3
      packages/ckeditor5-engine/src/model/document.js
  9. 1 1
      packages/ckeditor5-engine/src/model/element.js
  10. 14 12
      packages/ckeditor5-engine/src/model/model.js
  11. 62 1
      packages/ckeditor5-engine/src/model/node.js
  12. 1 5
      packages/ckeditor5-engine/src/model/position.js
  13. 10 7
      packages/ckeditor5-engine/src/model/schema.js
  14. 2 1
      packages/ckeditor5-engine/src/model/selection.js
  15. 1 1
      packages/ckeditor5-engine/src/model/text.js
  16. 9 7
      packages/ckeditor5-engine/src/model/writer.js
  17. 4 4
      packages/ckeditor5-engine/src/view/document.js
  18. 374 0
      packages/ckeditor5-engine/src/view/documentselection.js
  19. 8 8
      packages/ckeditor5-engine/src/view/domconverter.js
  20. 1 1
      packages/ckeditor5-engine/src/view/element.js
  21. 104 15
      packages/ckeditor5-engine/src/view/node.js
  22. 2 2
      packages/ckeditor5-engine/src/view/observer/fakeselectionobserver.js
  23. 1 1
      packages/ckeditor5-engine/src/view/observer/mutationobserver.js
  24. 6 4
      packages/ckeditor5-engine/src/view/observer/selectionobserver.js
  25. 14 40
      packages/ckeditor5-engine/src/view/position.js
  26. 2 2
      packages/ckeditor5-engine/src/view/renderer.js
  27. 74 63
      packages/ckeditor5-engine/src/view/selection.js
  28. 1 1
      packages/ckeditor5-engine/src/view/text.js
  29. 4 2
      packages/ckeditor5-engine/src/view/writer.js
  30. 43 0
      packages/ckeditor5-engine/tests/conversion/conversion.js
  31. 48 1
      packages/ckeditor5-engine/tests/conversion/downcast-converters.js
  32. 1 1
      packages/ckeditor5-engine/tests/conversion/upcast-selection-converters.js
  33. 10 10
      packages/ckeditor5-engine/tests/dev-utils/view.js
  34. 2 3
      packages/ckeditor5-engine/tests/manual/tickets/ckeditor5-721/1.js
  35. 1 0
      packages/ckeditor5-engine/tests/model/documentfragment.js
  36. 2 1
      packages/ckeditor5-engine/tests/model/element.js
  37. 82 0
      packages/ckeditor5-engine/tests/model/node.js
  38. 2 1
      packages/ckeditor5-engine/tests/model/text.js
  39. 1 0
      packages/ckeditor5-engine/tests/model/textproxy.js
  40. 2 2
      packages/ckeditor5-engine/tests/view/_utils/createdocumentmock.js
  41. 1 0
      packages/ckeditor5-engine/tests/view/documentfragment.js
  42. 1124 0
      packages/ckeditor5-engine/tests/view/documentselection.js
  43. 3 3
      packages/ckeditor5-engine/tests/view/domconverter/binding.js
  44. 3 3
      packages/ckeditor5-engine/tests/view/domconverter/dom-to-view.js
  45. 2 1
      packages/ckeditor5-engine/tests/view/element.js
  46. 99 0
      packages/ckeditor5-engine/tests/view/node.js
  47. 3 2
      packages/ckeditor5-engine/tests/view/observer/selectionobserver.js
  48. 2 2
      packages/ckeditor5-engine/tests/view/renderer.js
  49. 114 101
      packages/ckeditor5-engine/tests/view/selection.js
  50. 2 1
      packages/ckeditor5-engine/tests/view/text.js
  51. 1 0
      packages/ckeditor5-engine/tests/view/textproxy.js

+ 1 - 1
packages/ckeditor5-engine/README.md

@@ -14,7 +14,7 @@ The CKEditor 5 editing engine implements a flexible MVC-based architecture for c
 
 ## Documentation
 
-See the [`@ckeditor/ckeditor5-engine` package](https://ckeditor5.github.io/docs/nightly/ckeditor5/latest/api/engine.html) page in [CKEditor 5 documentation](https://ckeditor5.github.io/docs/nightly/ckeditor5/latest/).
+See the [`@ckeditor/ckeditor5-engine` package](https://docs.ckeditor.com/ckeditor5/latest/api/engine.html) page in [CKEditor 5 documentation](https://docs.ckeditor.com/ckeditor5/latest/).
 
 ## License
 

+ 1 - 1
packages/ckeditor5-engine/package.json

@@ -33,7 +33,7 @@
   },
   "author": "CKSource (http://cksource.com/)",
   "license": "(GPL-2.0 OR LGPL-2.1 OR MPL-1.1)",
-  "homepage": "https://ckeditor5.github.io",
+  "homepage": "https://ckeditor.com",
   "bugs": "https://github.com/ckeditor/ckeditor5-engine/issues",
   "repository": {
     "type": "git",

+ 24 - 0
packages/ckeditor5-engine/src/conversion/conversion.js

@@ -224,6 +224,30 @@ export default class Conversion {
 	 *			}
 	 *		} );
 	 *
+	 *		// Use `config.model.name` to define conversion only from given node type, `$text` in this case.
+	 *		// The same attribute on different elements may be then handled by a different converter.
+	 *		conversion.attributeToElement( {
+	 *			model: {
+	 *				key: 'textDecoration',
+	 *				values: [ 'underline', 'lineThrough' ],
+	 *				name: '$text'
+	 *			},
+	 *			view: {
+	 *				underline: {
+	 *					name: 'span',
+	 *					style: {
+	 *						'text-decoration': 'underline'
+	 *					}
+	 *				},
+	 *				lineThrough: {
+	 *					name: 'span',
+	 *					style: {
+	 *						'text-decoration': 'line-through'
+	 *					}
+	 *				}
+	 *			}
+	 *		} );
+	 *
 	 *		// Use `upcastAlso` to define other view elements that should be also converted to `bold` attribute.
 	 *		conversion.attributeToElement( {
 	 *			model: 'bold',

+ 24 - 6
packages/ckeditor5-engine/src/conversion/downcast-converters.js

@@ -105,6 +105,16 @@ export function downcastElementToElement( config ) {
  * 			}
  * 		} );
  *
+ *		downcastAttributeToElement( {
+ *			model: {
+ *				key: 'color',
+ *				name: '$text'
+ *			},
+ *			view: ( modelAttributeValue, viewWriter ) => {
+ *				return viewWriter.createAttributeElement( 'span', { style: 'color:' + modelAttributeValue } );
+ *			}
+ *		} );
+ *
  * See {@link module:engine/conversion/conversion~Conversion#for} to learn how to add converter to conversion process.
  *
  * @param {Object} config Conversion configuration.
@@ -120,6 +130,11 @@ export function downcastAttributeToElement( config ) {
 	config = cloneDeep( config );
 
 	const modelKey = config.model.key ? config.model.key : config.model;
+	let eventName = 'attribute:' + modelKey;
+
+	if ( config.model.name ) {
+		eventName += ':' + config.model.name;
+	}
 
 	if ( config.model.values ) {
 		for ( const modelValue of config.model.values ) {
@@ -132,7 +147,7 @@ export function downcastAttributeToElement( config ) {
 	const elementCreator = _getFromAttributeCreator( config );
 
 	return dispatcher => {
-		dispatcher.on( 'attribute:' + modelKey, wrap( elementCreator ), { priority: config.priority || 'normal' } );
+		dispatcher.on( eventName, wrap( elementCreator ), { priority: config.priority || 'normal' } );
 	};
 }
 
@@ -639,6 +654,13 @@ export function changeAttribute( attributeCreator ) {
 	attributeCreator = attributeCreator || ( ( value, data ) => ( { value, key: data.attributeKey } ) );
 
 	return ( evt, data, conversionApi ) => {
+		const oldAttribute = attributeCreator( data.attributeOldValue, data );
+		const newAttribute = attributeCreator( data.attributeNewValue, data );
+
+		if ( !oldAttribute && !newAttribute ) {
+			return;
+		}
+
 		if ( !conversionApi.consumable.consume( data.item, evt.name ) ) {
 			return;
 		}
@@ -647,8 +669,6 @@ export function changeAttribute( attributeCreator ) {
 		const viewWriter = conversionApi.writer;
 
 		// First remove the old attribute if there was one.
-		const oldAttribute = attributeCreator( data.attributeOldValue, data );
-
 		if ( data.attributeOldValue !== null && oldAttribute ) {
 			if ( oldAttribute.key == 'class' ) {
 				const classes = Array.isArray( oldAttribute.value ) ? oldAttribute.value : [ oldAttribute.value ];
@@ -667,9 +687,7 @@ export function changeAttribute( attributeCreator ) {
 			}
 		}
 
-		// Then, if conversion was successful, set the new attribute.
-		const newAttribute = attributeCreator( data.attributeNewValue, data );
-
+		// Then set the new attribute.
 		if ( data.attributeNewValue !== null && newAttribute ) {
 			if ( newAttribute.key == 'class' ) {
 				const classes = Array.isArray( newAttribute.value ) ? newAttribute.value : [ newAttribute.value ];

+ 6 - 6
packages/ckeditor5-engine/src/conversion/downcast-selection-converters.js

@@ -5,7 +5,7 @@
 
 /**
  * Contains {@link module:engine/model/selection~Selection model selection} to
- * {@link module:engine/view/selection~Selection view selection} converters for
+ * {@link module:engine/view/documentselection~DocumentSelection view selection} converters for
  * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher}.
  *
  * @module engine/conversion/downcast-selection-converters
@@ -13,8 +13,8 @@
 
 /**
  * Function factory, creates a converter that converts non-collapsed {@link module:engine/model/selection~Selection model selection} to
- * {@link module:engine/view/selection~Selection view selection}. The converter consumes appropriate value from `consumable` object
- * and maps model positions from selection to view positions.
+ * {@link module:engine/view/documentselection~DocumentSelection view selection}. The converter consumes appropriate
+ * value from `consumable` object and maps model positions from selection to view positions.
  *
  *		modelDispatcher.on( 'selection', convertRangeSelection() );
  *
@@ -45,9 +45,9 @@ export function convertRangeSelection() {
 
 /**
  * Function factory, creates a converter that converts collapsed {@link module:engine/model/selection~Selection model selection} to
- * {@link module:engine/view/selection~Selection view selection}. The converter consumes appropriate value from `consumable` object,
- * maps model selection position to view position and breaks {@link module:engine/view/attributeelement~AttributeElement attribute elements}
- * at the selection position.
+ * {@link module:engine/view/documentselection~DocumentSelection view selection}. The converter consumes appropriate
+ * value from `consumable` object, maps model selection position to view position and breaks
+ * {@link module:engine/view/attributeelement~AttributeElement attribute elements} at the selection position.
  *
  *		modelDispatcher.on( 'selection', convertCollapsedSelection() );
  *

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

@@ -4,7 +4,7 @@
  */
 
 /**
- * Contains {@link module:engine/view/selection~Selection view selection}
+ * Contains {@link module:engine/view/documentselection~DocumentSelection view selection}
  * to {@link module:engine/model/selection~Selection model selection} conversion helpers.
  *
  * @module engine/conversion/upcast-selection-converters
@@ -13,8 +13,8 @@
 import ModelSelection from '../model/selection';
 
 /**
- * Function factory, creates a callback function which converts a {@link module:engine/view/selection~Selection view selection} taken
- * from the {@link module:engine/view/document~Document#event:selectionChange} event
+ * Function factory, creates a callback function which converts a {@link module:engine/view/selection~Selection
+ * view selection} taken from the {@link module:engine/view/document~Document#event:selectionChange} event
  * and sets in on the {@link module:engine/model/document~Document#selection model}.
  *
  * **Note**: because there is no view selection change dispatcher nor any other advanced view selection to model

+ 19 - 17
packages/ckeditor5-engine/src/dev-utils/view.js

@@ -15,7 +15,7 @@ import View from '../view/view';
 import ViewDocumentFragment from '../view/documentfragment';
 import XmlDataProcessor from '../dataprocessor/xmldataprocessor';
 import ViewElement from '../view/element';
-import Selection from '../view/selection';
+import DocumentSelection from '../view/documentselection';
 import Range from '../view/range';
 import Position from '../view/position';
 import AttributeElement from '../view/attributeelement';
@@ -125,8 +125,8 @@ setData._parse = parse;
  *
  *		stringify( fragment ); // '<p style="color:red;"></p><b name="test">foobar</b>'
  *
- * Additionally, a {@link module:engine/view/selection~Selection selection} instance can be provided. Ranges from the selection
- * will then be included in output data.
+ * Additionally, a {@link module:engine/view/documentselection~DocumentSelection selection} instance can be provided.
+ * Ranges from the selection will then be included in output data.
  * If a range position is placed inside the element node, it will be represented with `[` and `]`:
  *
  *		const text = new Text( 'foobar' );
@@ -163,9 +163,9 @@ setData._parse = parse;
  *		stringify( text, selection ); // '{f}oo{ba}r'
  *
  * A {@link module:engine/view/range~Range range} or {@link module:engine/view/position~Position position} instance can be provided
- * instead of the {@link module:engine/view/selection~Selection selection} instance. If a range instance is provided, it will be
- * converted to a selection containing this range. If a position instance is provided, it will be converted to a selection
- * containing one range collapsed at this position.
+ * instead of the {@link module:engine/view/documentselection~DocumentSelection selection} instance. If a range instance
+ * is provided, it will be converted to a selection containing this range. If a position instance is provided, it will
+ * be converted to a selection containing one range collapsed at this position.
  *
  *		const text = new Text( 'foobar' );
  *		const range = Range.createFromParentsAndOffsets( text, 0, text, 1 );
@@ -206,7 +206,7 @@ setData._parse = parse;
  *
  * @param {module:engine/view/text~Text|module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment}
  * node The node to stringify.
- * @param {module:engine/view/selection~Selection|module:engine/view/position~Position|module:engine/view/range~Range}
+ * @param {module:engine/view/documentselection~DocumentSelection|module:engine/view/position~Position|module:engine/view/range~Range}
  * [selectionOrPositionOrRange = null ]
  * A selection instance whose ranges will be included in the returned string data. If a range instance is provided, it will be
  * converted to a selection containing this range. If a position instance is provided, it will be converted to a selection
@@ -231,7 +231,7 @@ export function stringify( node, selectionOrPositionOrRange = null, options = {}
 		selectionOrPositionOrRange instanceof Position ||
 		selectionOrPositionOrRange instanceof Range
 	) {
-		selection = new Selection( selectionOrPositionOrRange );
+		selection = new DocumentSelection( selectionOrPositionOrRange );
 	} else {
 		selection = selectionOrPositionOrRange;
 	}
@@ -257,7 +257,7 @@ export function stringify( node, selectionOrPositionOrRange = null, options = {}
  *		parse( '<b>foo</b><i>bar</i>' ); // Returns a document fragment with two child elements.
  *
  * The method can parse multiple {@link module:engine/view/range~Range ranges} provided in string data and return a
- * {@link module:engine/view/selection~Selection selection} instance containing these ranges. Ranges placed inside
+ * {@link module:engine/view/documentselection~DocumentSelection selection} instance containing these ranges. Ranges placed inside
  * {@link module:engine/view/text~Text text} nodes should be marked using `{` and `}` brackets:
  *
  *		const { text, selection } = parse( 'f{ooba}r' );
@@ -278,8 +278,9 @@ export function stringify( node, selectionOrPositionOrRange = null, options = {}
  * In the example above, the first range (`{fo}`) will be added to the selection as the second one, the second range (`{ar}`) will be
  * added as the third and the third range (`{ba}`) will be added as the first one.
  *
- * If the selection's last range should be added as a backward one (so the {@link module:engine/view/selection~Selection#anchor selection
- * anchor} is represented by the `end` position and {@link module:engine/view/selection~Selection#focus selection focus} is
+ * If the selection's last range should be added as a backward one
+ * (so the {@link module:engine/view/documentselection~DocumentSelection#anchor selection anchor} is represented
+ * by the `end` position and {@link module:engine/view/documentselection~DocumentSelection#focus selection focus} is
  * represented by the `start` position), use the `lastRangeBackward` flag:
  *
  *		const { root, selection } = parse( `{foo}bar{baz}`, { lastRangeBackward: true } );
@@ -298,11 +299,11 @@ export function stringify( node, selectionOrPositionOrRange = null, options = {}
  * @param {String} data An HTML-like string to be parsed.
  * @param {Object} options
  * @param {Array.<Number>} [options.order] An array with the order of parsed ranges added to the returned
- * {@link module:engine/view/selection~Selection Selection} instance. Each element should represent the desired position of each range in
- * the selection instance. For example: `[2, 3, 1]` means that the first range will be placed as the second, the second as the third and
- * the third as the first.
+ * {@link module:engine/view/documentselection~DocumentSelection Selection} instance. Each element should represent the
+ * desired position of each range in the selection instance. For example: `[2, 3, 1]` means that the first range will be
+ * placed as the second, the second as the third and the third as the first.
  * @param {Boolean} [options.lastRangeBackward=false] If set to `true`, the last range will be added as backward to the returned
- * {@link module:engine/view/selection~Selection selection} instance.
+ * {@link module:engine/view/documentselection~DocumentSelection selection} instance.
  * @param {module:engine/view/element~Element|module:engine/view/documentfragment~DocumentFragment}
  * [options.rootElement=null] The default root to use when parsing elements.
  * When set to `null`, the root element will be created automatically. If set to
@@ -351,7 +352,7 @@ export function parse( data, options = {} ) {
 
 	// When ranges are present - return object containing view, and selection.
 	if ( ranges.length ) {
-		const selection = new Selection( ranges, { backward: !!options.lastRangeBackward } );
+		const selection = new DocumentSelection( ranges, { backward: !!options.lastRangeBackward } );
 
 		return {
 			view,
@@ -594,7 +595,8 @@ class ViewStringify {
 	 * Creates a view stringify instance.
 	 *
 	 * @param root
-	 * @param {module:engine/view/selection~Selection} selection A selection whose ranges should also be converted to a string.
+	 * @param {module:engine/view/documentselection~DocumentSelection} selection A selection whose ranges
+	 * should also be converted to a string.
 	 * @param {Object} options An options object.
 	 * @param {Boolean} [options.showType=false] When set to `true`, the type of elements will be printed (`<container:p>`
 	 * instead of `<p>`, `<attribute:b>` instead of `<b>` and `<empty:img>` instead of `<img>`).

+ 24 - 3
packages/ckeditor5-engine/src/model/document.js

@@ -363,9 +363,30 @@ export default class Document {
 	}
 
 	/**
-	 * Fired after an {@link module:engine/model/model~Model#enqueueChange enqueue change block} or the outermost
-	 * {@link module:engine/model/model~Model#change change block} was executed and the document model tree was changed
-	 * during that block execution.
+	 * Fired after each {@link module:engine/model/model~Model#enqueueChange `enqueueChange()` block} or the outermost
+	 * {@link module:engine/model/model~Model#change `change()` block} was executed and the document was changed
+	 * during that block's execution.
+	 *
+	 * The changes which this event will cover include:
+	 *
+	 * * document structure changes,
+	 * * selection changes,
+	 * * marker changes.
+	 *
+	 * If you want to be notified about all these changes, then simply listen to this event like this:
+	 *
+	 *		model.document.on( 'change', () => {
+	 *			console.log( 'The Document has changed!' );
+	 *		} );
+	 *
+	 * If, however, you only want to be notified about structure changes, then check whether the
+	 * {@link module:engine/model/differ~Differ differ} contains any changes:
+	 *
+	 *		model.document.on( 'change', () => {
+	 *			if ( model.document.differ.getChanges().length > 0 ) {
+	 *				console.log( 'The Document has changed!' );
+	 *			}
+	 *		} );
 	 *
 	 * @event change
 	 * @param {module:engine/model/batch~Batch} batch The batch that was used in the executed changes block.

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

@@ -109,7 +109,7 @@ export default class Element extends Node {
 	 */
 	is( type, name = null ) {
 		if ( !name ) {
-			return type == 'element' || type == this.name;
+			return type == 'element' || type == this.name || super.is( type );
 		} else {
 			return type == 'element' && name == this.name;
 		}

+ 14 - 12
packages/ckeditor5-engine/src/model/model.js

@@ -36,7 +36,7 @@ import getSelectedContent from './utils/getselectedcontent';
 export default class Model {
 	constructor() {
 		/**
-		 * Models markers' collection.
+		 * Model's marker collection.
 		 *
 		 * @readonly
 		 * @member {module:engine/model/markercollection~MarkerCollection}
@@ -44,7 +44,7 @@ export default class Model {
 		this.markers = new MarkerCollection();
 
 		/**
-		 * Editors document model.
+		 * Model's document.
 		 *
 		 * @readonly
 		 * @member {module:engine/model/document~Document}
@@ -52,7 +52,7 @@ export default class Model {
 		this.document = new Document( this );
 
 		/**
-		 * Schema for editors model.
+		 * Model's schema.
 		 *
 		 * @readonly
 		 * @member {module:engine/model/schema~Schema}
@@ -114,15 +114,17 @@ export default class Model {
 	}
 
 	/**
-	 * Change method is the primary way of changing the model. You should use it to modify any node, including detached
-	 * nodes (not added to the {@link module:engine/model/model~Model#document model document}).
+	 * The `change()` method is the primary way of changing the model. You should use it to modify all document nodes
+	 * (including detached nodes – i.e. nodes not added to the {@link module:engine/model/model~Model#document model document}),
+	 * the {@link module:engine/model/document~Document#selection document's selection}, and
+	 * {@link module:engine/model/model~Model#markers model markers}.
 	 *
 	 *		model.change( writer => {
 	 *			writer.insertText( 'foo', paragraph, 'end' );
 	 *		} );
 	 *
-	 * All changes inside the change block use the same {@link module:engine/model/batch~Batch} so they share the same
-	 * undo step.
+	 * All changes inside the change block use the same {@link module:engine/model/batch~Batch} so they are combined
+	 * into a single undo step.
 	 *
 	 *		model.change( writer => {
 	 *			writer.insertText( 'foo', paragraph, 'end' ); // foo.
@@ -134,7 +136,7 @@ export default class Model {
 	 * 			writer.insertText( 'bom', paragraph, 'end' ); // foobarbom.
 	 *		} );
 	 *
-	 * Change block is executed immediately.
+	 * The callback of the `change()` block is executed synchronously.
 	 *
 	 * You can also return a value from the change block.
 	 *
@@ -161,7 +163,7 @@ export default class Model {
 	/**
 	 * The `enqueueChange()` method performs similar task as the {@link #change `change()` method}, with two major differences.
 	 *
-	 * First, the callback of the `enqueueChange` is executed when all other changes are done. It might be executed
+	 * First, the callback of `enqueueChange()` is executed when all other enqueued changes are done. It might be executed
 	 * immediately if it is not nested in any other change block, but if it is nested in another (enqueue)change block,
 	 * it will be delayed and executed after the outermost block.
 	 *
@@ -179,15 +181,15 @@ export default class Model {
 	 * By default, a new batch is created. In the sample above, `change` and `enqueueChange` blocks use a different
 	 * batch (and different {@link module:engine/model/writer~Writer} since each of them operates on the separate batch).
 	 *
-	 * Using `enqueueChange` block you can also add some changes to the batch you used before.
+	 * When using the `enqueueChange()` block you can also add some changes to the batch you used before.
 	 *
 	 *		model.enqueueChange( batch, writer => {
 	 *			writer.insertText( 'foo', paragraph, 'end' );
 	 *		} );
 	 *
-	 * `Batch` instance can be obtained from {@link module:engine/model/writer~Writer#batch the writer}.
+	 * The batch instance can be obtained from {@link module:engine/model/writer~Writer#batch the writer}.
 	 *
-	 * @param {module:engine/model/batch~Batch|String} batchOrType Batch or batch type should be used in the callback.
+	 * @param {module:engine/model/batch~Batch|'transparent'|'default'} batchOrType Batch or batch type should be used in the callback.
 	 * If not defined, a new batch will be created.
 	 * @param {Function} callback Callback function which may modify the model.
 	 */

+ 62 - 1
packages/ckeditor5-engine/src/model/node.js

@@ -9,6 +9,7 @@
 
 import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+import compareArrays from '@ckeditor/ckeditor5-utils/src/comparearrays';
 
 /**
  * Model node. Most basic structure of model tree.
@@ -274,6 +275,63 @@ export default class Node {
 	}
 
 	/**
+	 * Returns whether this node is before given node. `false` is returned if nodes are in different trees (for example,
+	 * in different {@link module:engine/model/documentfragment~DocumentFragment}s).
+	 *
+	 * @param {module:engine/model/node~Node} node Node to compare with.
+	 * @returns {Boolean}
+	 */
+	isBefore( node ) {
+		// Given node is not before this node if they are same.
+		if ( this == node ) {
+			return false;
+		}
+
+		// Return `false` if it is impossible to compare nodes.
+		if ( this.root !== node.root ) {
+			return false;
+		}
+
+		const thisPath = this.getPath();
+		const nodePath = node.getPath();
+
+		const result = compareArrays( thisPath, nodePath );
+
+		switch ( result ) {
+			case 'prefix':
+				return true;
+
+			case 'extension':
+				return false;
+
+			default:
+				return thisPath[ result ] < nodePath[ result ];
+		}
+	}
+
+	/**
+	 * Returns whether this node is after given node. `false` is returned if nodes are in different trees (for example,
+	 * in different {@link module:engine/model/documentfragment~DocumentFragment}s).
+	 *
+	 * @param {module:engine/model/node~Node} node Node to compare with.
+	 * @returns {Boolean}
+	 */
+	isAfter( node ) {
+		// Given node is not before this node if they are same.
+		if ( this == node ) {
+			return false;
+		}
+
+		// Return `false` if it is impossible to compare nodes.
+		if ( this.root !== node.root ) {
+			return false;
+		}
+
+		// In other cases, just check if the `node` is before, and return the opposite.
+		return !this.isBefore( node );
+	}
+
+	/**
 	 * Checks if the node has an attribute with given key.
 	 *
 	 * @param {String} key Key of attribute to check.
@@ -401,7 +459,7 @@ export default class Node {
 	 * may return {@link module:engine/model/documentfragment~DocumentFragment} or {@link module:engine/model/node~Node}
 	 * that can be either text node or element. This method can be used to check what kind of object is returned.
 	 *
-	 *		obj.is( 'node' ); // true for any node, false for document fragment
+	 *		obj.is( 'node' ); // true for any node, false for document fragment and text fragment
 	 *		obj.is( 'documentFragment' ); // true for document fragment, false for any node
 	 *		obj.is( 'element' ); // true for any element, false for text node or document fragment
 	 *		obj.is( 'element', 'paragraph' ); // true only for element which name is 'paragraph'
@@ -413,6 +471,9 @@ export default class Node {
 	 * @param {'element'|'rootElement'|'text'|'textProxy'|'documentFragment'} type
 	 * @returns {Boolean}
 	 */
+	is( type ) {
+		return type == 'node';
+	}
 }
 
 /**

+ 1 - 5
packages/ckeditor5-engine/src/model/position.js

@@ -238,11 +238,7 @@ export default class Position {
 				return 'after';
 
 			default:
-				if ( this.path[ result ] < otherPosition.path[ result ] ) {
-					return 'before';
-				} else {
-					return 'after';
-				}
+				return this.path[ result ] < otherPosition.path[ result ] ? 'before' : 'after';
 		}
 	}
 

+ 10 - 7
packages/ckeditor5-engine/src/model/schema.js

@@ -575,7 +575,8 @@ export default class Schema {
 	 * Returns the lowest {@link module:engine/model/schema~Schema#isLimit limit element} containing the entire
 	 * selection or the root otherwise.
 	 *
-	 * @param {module:engine/model/selection~Selection} selection Selection which returns the common ancestor.
+	 * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
+	 * Selection which returns the common ancestor.
 	 * @returns {module:engine/model/element~Element}
 	 */
 	getLimitElement( selection ) {
@@ -609,7 +610,8 @@ export default class Schema {
 	 * * if the selection is collapsed, then checks if on the selection position there's a text with the
 	 * specified attribute allowed.
 	 *
-	 * @param {module:engine/model/selection~Selection} selection Selection which will be checked.
+	 * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} selection
+	 * Selection which will be checked.
 	 * @param {String} attribute The name of the attribute to check.
 	 * @returns {Boolean}
 	 */
@@ -671,14 +673,15 @@ export default class Schema {
 	}
 
 	/**
-	 * Basing on given `position`, finds and returns a {@link module:engine/model/range~Range Range} instance that is
+	 * Basing on given the `position`, finds and returns a {@link module:engine/model/range~Range range} which is
 	 * nearest to that `position` and is a correct range for selection.
 	 *
-	 * Correct selection range might be collapsed - when it's located in position where text node can be placed.
-	 * Non-collapsed range is returned when selection can be placed around element marked as "object" in
-	 * {@link module:engine/model/schema~Schema schema}.
+	 * The correct selection range might be collapsed when it is located in a position where the text node can be placed.
+	 * Non-collapsed range is returned when selection can be placed around element marked as an "object" in
+	 * the {@link module:engine/model/schema~Schema schema}.
+	 *
+	 * Direction of searching for the nearest correct selection range can be specified as:
 	 *
-	 * Direction of searching for nearest correct selection range can be specified as:
 	 * * `both` - searching will be performed in both ways,
 	 * * `forward` - searching will be performed only forward,
 	 * * `backward` - searching will be performed only backward.

+ 2 - 1
packages/ckeditor5-engine/src/model/selection.js

@@ -193,7 +193,8 @@ export default class Selection {
 	 * Checks whether this selection is equal to given selection. Selections are equal if they have same directions,
 	 * same number of ranges and all ranges from one selection equal to a range from other selection.
 	 *
-	 * @param {module:engine/model/selection~Selection} otherSelection Selection to compare with.
+	 * @param {module:engine/model/selection~Selection|module:engine/model/documentselection~DocumentSelection} otherSelection
+	 * Selection to compare with.
 	 * @returns {Boolean} `true` if selections are equal, `false` otherwise.
 	 */
 	isEqual( otherSelection ) {

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

@@ -65,7 +65,7 @@ export default class Text extends Node {
 	 * @inheritDoc
 	 */
 	is( type ) {
-		return type == 'text';
+		return type == 'text' || super.is( type );
 	}
 
 	/**

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

@@ -43,25 +43,27 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import uid from '@ckeditor/ckeditor5-utils/src/uid';
 
 /**
- * Model writer it the proper way of modifying model. It should be used whenever you wants to create node, modify
- * child nodes, attributes or text. To get writer use {@link module:engine/model/model~Model#change} or
- * {@link module:engine/model/model~Model#enqueueChange}.
+ * The model can only be modified by using the writer. It should be used whenever you want to create a node, modify
+ * child nodes, attributes or text, set the selection's position and its attributes.
+ *
+ * The instance of the writer is only available in the {@link module:engine/model/model~Model#change `change()`} or
+ * {@link module:engine/model/model~Model#enqueueChange `enqueueChange()`}.
  *
  *		model.change( writer => {
  *			writer.insertText( 'foo', paragraph, 'end' );
  *		} );
  *
- * Note that writer can be passed to a nested function but you should never store and use it outside the `change` or
- * `enqueueChange` block.
+ * Note that the should never be stored and used outside of the `change()` or
+ * `enqueueChange()` blocks.
  *
  * @see module:engine/model/model~Model#change
  * @see module:engine/model/model~Model#enqueueChange
  */
 export default class Writer {
 	/**
-	 * Writer class constructor.
+	 * Creates a writer instance.
 	 *
-	 * It is not recommended to use it directly, use {@link module:engine/model/model~Model#change} or
+	 * **Note:** It is not recommended to use it directly. Use {@link module:engine/model/model~Model#change} or
 	 * {@link module:engine/model/model~Model#enqueueChange} instead.
 	 *
 	 * @protected

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

@@ -7,14 +7,14 @@
  * @module engine/view/document
  */
 
-import Selection from './selection';
+import DocumentSelection from './documentselection';
 import Collection from '@ckeditor/ckeditor5-utils/src/collection';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
 import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
 
 /**
  * Document class creates an abstract layer over the content editable area, contains a tree of view elements and
- * {@link module:engine/view/selection~Selection view selection} associated with this document.
+ * {@link module:engine/view/documentselection~DocumentSelection view selection} associated with this document.
  *
  * @mixes module:utils/observablemixin~ObservableMixin
  */
@@ -27,9 +27,9 @@ export default class Document {
 		 * Selection done on this document.
 		 *
 		 * @readonly
-		 * @member {module:engine/view/selection~Selection} module:engine/view/document~Document#selection
+		 * @member {module:engine/view/documentselection~DocumentSelection} module:engine/view/document~Document#selection
 		 */
-		this.selection = new Selection();
+		this.selection = new DocumentSelection();
 
 		/**
 		 * Roots of the view tree. Collection of the {module:engine/view/element~Element view elements}.

+ 374 - 0
packages/ckeditor5-engine/src/view/documentselection.js

@@ -0,0 +1,374 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module engine/view/documentselection
+ */
+
+import Selection from './selection';
+import mix from '@ckeditor/ckeditor5-utils/src/mix';
+import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
+
+/**
+ * Class representing document selection in tree view. It's instance is stored at
+ * {@link module:engine/view/document~Document#selection}. It is similar to {@link module:engine/view/selection~Selection} but
+ * it has read-only API and can be modified only by writer obtained from {@link module:engine/view/view~View#change} method.
+ *
+ * Selection can consist of {@link module:engine/view/range~Range ranges}.
+ * Selection's ranges can be obtained via {@link module:engine/view/documentselection~DocumentSelection#getRanges getRanges},
+ * {@link module:engine/view/documentselection~DocumentSelection#getFirstRange getFirstRange}
+ * and {@link module:engine/view/documentselection~DocumentSelection#getLastRange getLastRange}
+ * methods, which return copies of ranges stored inside selection. Modifications made on these copies will not change
+ * selection's state. Similar situation occurs when getting {@link module:engine/view/documentselection~DocumentSelection#anchor anchor},
+ * {@link module:engine/view/documentselection~DocumentSelection#focus focus},
+ * {@link module:engine/view/documentselection~DocumentSelection#getFirstPosition first} and
+ * {@link module:engine/view/documentselection~DocumentSelection#getLastPosition last} positions - all will return
+ * copies of requested positions.
+ */
+export default class DocumentSelection {
+	/**
+	 * Creates new DocumentSelection instance.
+	 *
+	 * 		// Creates empty selection without ranges.
+	 *		const selection = new DocumentSelection();
+	 *
+	 *		// Creates selection at the given range.
+	 *		const range = new Range( start, end );
+	 *		const selection = new DocumentSelection( range );
+	 *
+	 *		// Creates selection at the given ranges
+	 * 		const ranges = [ new Range( start1, end2 ), new Range( star2, end2 ) ];
+	 *		const selection = new DocumentSelection( ranges );
+	 *
+	 *		// Creates selection from the other selection.
+	 *		const otherSelection = new Selection();
+	 *		const selection = new DocumentSelection( otherSelection );
+	 *
+	 * 		// Creates selection at the given position.
+	 *		const position = new Position( root, path );
+	 *		const selection = new DocumentSelection( position );
+	 *
+	 *		// Creates collapsed selection at the position of given item and offset.
+	 *		const paragraph = writer.createContainerElement( 'paragraph' );
+	 *		const selection = new DocumentSelection( paragraph, offset );
+	 *
+	 *		// Creates a range inside an {@link module:engine/view/element~Element element} which starts before the
+	 *		// first child of that element and ends after the last child of that element.
+	 *		const selection = new DocumentSelection( paragraph, 'in' );
+	 *
+	 *		// Creates a range on an {@link module:engine/view/item~Item item} which starts before the item and ends
+	 *		// just after the item.
+	 *		const selection = new DocumentSelection( paragraph, 'on' );
+	 *
+	 * `Selection`'s constructor allow passing additional options (`backward`, `fake` and `label`) as the last argument.
+	 *
+	 *		// Creates backward selection.
+	 *		const selection = new DocumentSelection( range, { backward: true } );
+	 *
+	 * 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).
+	 *
+	 *		// Creates fake selection with label.
+	 *		const selection = new DocumentSelection( range, { fake: true, label: 'foo' } );
+	 *
+	 * @param {module:engine/view/selection~Selection|module:engine/view/position~Position|
+	 * Iterable.<module:engine/view/range~Range>|module:engine/view/range~Range|
+	 * module:engine/view/item~Item|null} [selectable=null]
+	 * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Offset or place when selectable is an `Item`.
+	 * @param {Object} [options]
+	 * @param {Boolean} [options.backward] Sets this selection instance to be backward.
+	 * @param {Boolean} [options.fake] Sets this selection instance to be marked as `fake`.
+	 * @param {String} [options.label] Label for the fake selection.
+	 */
+	constructor( selectable = null, placeOrOffset, options ) {
+		/**
+		 * Selection is used internally (`DocumentSelection` is a proxy to that selection).
+		 *
+		 * @private
+		 * @member {module:engine/view/selection~Selection}
+		 */
+		this._selection = new Selection();
+
+		// Delegate change event to be fired on DocumentSelection instance.
+		this._selection.delegate( 'change' ).to( this );
+
+		// Set selection data.
+		this._selection.setTo( selectable, placeOrOffset, options );
+	}
+
+	/**
+	 * Returns true if selection instance is marked as `fake`.
+	 *
+	 * @see #_setTo
+	 * @returns {Boolean}
+	 */
+	get isFake() {
+		return this._selection.isFake;
+	}
+
+	/**
+	 * Returns fake selection label.
+	 *
+	 * @see #_setTo
+	 * @returns {String}
+	 */
+	get fakeSelectionLabel() {
+		return this._selection.fakeSelectionLabel;
+	}
+
+	/**
+	 * Selection anchor. Anchor may be described as a position where the selection starts. Together with
+	 * {@link #focus focus} they define the direction of selection, which is important
+	 * when expanding/shrinking selection. Anchor is always the start or end of the most recent added range.
+	 * It may be a bit unintuitive when there are multiple ranges in selection.
+	 *
+	 * @see #focus
+	 * @type {module:engine/view/position~Position}
+	 */
+	get anchor() {
+		return this._selection.anchor;
+	}
+
+	/**
+	 * Selection focus. Focus is a position where the selection ends.
+	 *
+	 * @see #anchor
+	 * @type {module:engine/view/position~Position}
+	 */
+	get focus() {
+		return this._selection.focus;
+	}
+
+	/**
+	 * Returns whether the selection is collapsed. Selection is collapsed when there is exactly one range which is
+	 * collapsed.
+	 *
+	 * @type {Boolean}
+	 */
+	get isCollapsed() {
+		return this._selection.isCollapsed;
+	}
+
+	/**
+	 * Returns number of ranges in selection.
+	 *
+	 * @type {Number}
+	 */
+	get rangeCount() {
+		return this._selection.rangeCount;
+	}
+
+	/**
+	 * Specifies whether the {@link #focus} precedes {@link #anchor}.
+	 *
+	 * @type {Boolean}
+	 */
+	get isBackward() {
+		return this._selection.isBackward;
+	}
+
+	/**
+	 * {@link module:engine/view/editableelement~EditableElement EditableElement} instance that contains this selection, or `null`
+	 * if the selection is not inside an editable element.
+	 *
+	 * @type {module:engine/view/editableelement~EditableElement|null}
+	 */
+	get editableElement() {
+		return this._selection.editableElement;
+	}
+
+	/**
+	 * Used for the compatibility with the {@link module:engine/view/selection~Selection#isEqual} method.
+	 *
+	 * @protected
+	 */
+	get _ranges() {
+		return this._selection._ranges;
+	}
+
+	/**
+	 * Returns an iterable that contains copies of all ranges added to the selection.
+	 *
+	 * @returns {Iterable.<module:engine/view/range~Range>}
+	 */
+	* getRanges() {
+		yield* this._selection.getRanges();
+	}
+
+	/**
+	 * Returns copy of the first range in the selection. First range is the one which
+	 * {@link module:engine/view/range~Range#start start} position {@link module:engine/view/position~Position#isBefore is before} start
+	 * position of all other ranges (not to confuse with the first range added to the selection).
+	 * Returns `null` if no ranges are added to selection.
+	 *
+	 * @returns {module:engine/view/range~Range|null}
+	 */
+	getFirstRange() {
+		return this._selection.getFirstRange();
+	}
+
+	/**
+	 * Returns copy of the last range in the selection. Last range is the one which {@link module:engine/view/range~Range#end end}
+	 * position {@link module:engine/view/position~Position#isAfter is after} end position of all other ranges (not to confuse
+	 * with the last range added to the selection). Returns `null` if no ranges are added to selection.
+	 *
+	 * @returns {module:engine/view/range~Range|null}
+	 */
+	getLastRange() {
+		return this._selection.getLastRange();
+	}
+
+	/**
+	 * Returns copy of the first position in the selection. First position is the position that
+	 * {@link module:engine/view/position~Position#isBefore is before} any other position in the selection ranges.
+	 * Returns `null` if no ranges are added to selection.
+	 *
+	 * @returns {module:engine/view/position~Position|null}
+	 */
+	getFirstPosition() {
+		return this._selection.getFirstPosition();
+	}
+
+	/**
+	 * Returns copy of the last position in the selection. Last position is the position that
+	 * {@link module:engine/view/position~Position#isAfter is after} any other position in the selection ranges.
+	 * Returns `null` if no ranges are added to selection.
+	 *
+	 * @returns {module:engine/view/position~Position|null}
+	 */
+	getLastPosition() {
+		return this._selection.getLastPosition();
+	}
+
+	/**
+	 * Returns the selected element. {@link module:engine/view/element~Element Element} is considered as selected if there is only
+	 * one range in the selection, and that range contains exactly one element.
+	 * Returns `null` if there is no selected element.
+	 *
+	 * @returns {module:engine/view/element~Element|null}
+	 */
+	getSelectedElement() {
+		return this._selection.getSelectedElement();
+	}
+
+	/**
+	 * Checks whether, this selection is equal to given selection. Selections are equal if they have same directions,
+	 * same number of ranges and all ranges from one selection equal to a range from other selection.
+	 *
+	 * @param {module:engine/view/selection~Selection|module:engine/view/documentselection~DocumentSelection} otherSelection
+	 * Selection to compare with.
+	 * @returns {Boolean} `true` if selections are equal, `false` otherwise.
+	 */
+	isEqual( otherSelection ) {
+		return this._selection.isEqual( otherSelection );
+	}
+
+	/**
+	 * Checks whether this selection is similar to given selection. Selections are similar if they have same directions, same
+	 * number of ranges, and all {@link module:engine/view/range~Range#getTrimmed trimmed} ranges from one selection are
+	 * equal to any trimmed range from other selection.
+	 *
+	 * @param {module:engine/view/selection~Selection|module:engine/view/documentselection~DocumentSelection} otherSelection
+	 * Selection to compare with.
+	 * @returns {Boolean} `true` if selections are similar, `false` otherwise.
+	 */
+	isSimilar( otherSelection ) {
+		return this._selection.isSimilar( otherSelection );
+	}
+
+	/**
+	 * Sets this selection's ranges and direction to the specified location based on the given
+	 * {@link module:engine/view/documentselection~DocumentSelection document selection},
+	 * {@link module:engine/view/selection~Selection selection}, {@link module:engine/view/position~Position position},
+	 * {@link module:engine/view/item~Item item}, {@link module:engine/view/range~Range range},
+	 * an iterable of {@link module:engine/view/range~Range ranges} or null.
+	 *
+	 *		// Sets selection to the given range.
+	 *		const range = new Range( start, end );
+	 *		documentSelection._setTo( range );
+	 *
+	 *		// Sets selection to given ranges.
+	 * 		const ranges = [ new Range( start1, end2 ), new Range( star2, end2 ) ];
+	 *		documentSelection._setTo( range );
+	 *
+	 *		// Sets selection to the other selection.
+	 *		const otherSelection = new Selection();
+	 *		documentSelection._setTo( otherSelection );
+	 *
+	 * 		// Sets collapsed selection at the given position.
+	 *		const position = new Position( root, path );
+	 *		documentSelection._setTo( position );
+	 *
+	 * 		// Sets collapsed selection at the position of given item and offset.
+	 *		documentSelection._setTo( paragraph, offset );
+	 *
+	 * Creates a range inside an {@link module:engine/view/element~Element element} which starts before the first child of
+	 * that element and ends after the last child of that element.
+	 *
+	 *		documentSelection._setTo( paragraph, 'in' );
+	 *
+	 * Creates a range on an {@link module:engine/view/item~Item item} which starts before the item and ends just after the item.
+	 *
+	 *		documentSelection._setTo( paragraph, 'on' );
+	 *
+	 * 		// Clears selection. Removes all ranges.
+	 *		documentSelection._setTo( null );
+	 *
+	 * `Selection#_setTo()` method allow passing additional options (`backward`, `fake` and `label`) as the last argument.
+	 *
+	 *		// Sets selection as backward.
+	 *		documentSelection._setTo( range, { backward: true } );
+	 *
+	 * 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 des cribe fake selection in DOM
+	 * (and be  properly handled by screen readers).
+	 *
+	 *		// Creates fake selection with label.
+	 *		documentSelection._setTo( range, { fake: true, label: 'foo' } );
+	 *
+	 * @protected
+	 * @fires change
+	 * @param {module:engine/view/selection~Selection|module:engine/view/position~Position|
+	 * Iterable.<module:engine/view/range~Range>|module:engine/view/range~Range|module:engine/view/item~Item|null} selectable
+	 * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
+	 * @param {Object} [options]
+	 * @param {Boolean} [options.backward] Sets this selection instance to be backward.
+	 * @param {Boolean} [options.fake] Sets this selection instance to be marked as `fake`.
+	 * @param {String} [options.label] Label for the fake selection.
+	 */
+	_setTo( selectable, placeOrOffset, options ) {
+		this._selection.setTo( selectable, placeOrOffset, options );
+	}
+
+	/**
+	 * Moves {@link #focus} to the specified location.
+	 *
+	 * The location can be specified in the same form as {@link module:engine/view/position~Position.createAt} parameters.
+	 *
+	 * @protected
+	 * @fires change
+	 * @param {module:engine/view/item~Item|module:engine/view/position~Position} itemOrPosition
+	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
+	 * first parameter is a {@link module:engine/view/item~Item view item}.
+	 */
+	_setFocus( itemOrPosition, offset ) {
+		this._selection.setFocus( itemOrPosition, offset );
+	}
+
+	/**
+	 * Fired whenever selection ranges are changed through {@link ~DocumentSelection Selection API}.
+	 *
+	 * @event change
+	 */
+}
+
+mix( DocumentSelection, EmitterMixin );

+ 8 - 8
packages/ckeditor5-engine/src/view/domconverter.js

@@ -103,20 +103,20 @@ export default class DomConverter {
 	}
 
 	/**
-	 * Binds given DOM element that represents fake selection to {@link module:engine/view/selection~Selection view selection}.
-	 * View selection copy is stored and can be retrieved by {@link module:engine/view/domconverter~DomConverter#fakeSelectionToView}
-	 * method.
+	 * Binds given DOM element that represents fake selection to {@link module:engine/view/documentselection~DocumentSelection
+	 * document selection}. Document selection copy is stored and can be retrieved by
+	 * {@link module:engine/view/domconverter~DomConverter#fakeSelectionToView} method.
 	 *
 	 * @param {HTMLElement} domElement
-	 * @param {module:engine/view/selection~Selection} viewSelection
+	 * @param {module:engine/view/documentselection~DocumentSelection} viewDocumentSelection
 	 */
-	bindFakeSelection( domElement, viewSelection ) {
-		this._fakeSelectionMapping.set( domElement, new ViewSelection( viewSelection ) );
+	bindFakeSelection( domElement, viewDocumentSelection ) {
+		this._fakeSelectionMapping.set( domElement, new ViewSelection( viewDocumentSelection ) );
 	}
 
 	/**
-	 * Returns {@link module:engine/view/selection~Selection view selection} instance corresponding to given DOM element that represents
-	 * fake selection. Returns `undefined` if binding to given DOM element does not exists.
+	 * Returns {@link module:engine/view/selection~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 {module:engine/view/selection~Selection|undefined}

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

@@ -152,7 +152,7 @@ export default class Element extends Node {
 	 */
 	is( type, name = null ) {
 		if ( !name ) {
-			return type == 'element' || type == this.name;
+			return type == 'element' || type == this.name || super.is( type );
 		} else {
 			return type == 'element' && name == this.name;
 		}

+ 104 - 15
packages/ckeditor5-engine/src/view/node.js

@@ -11,6 +11,7 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
 import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
 import mix from '@ckeditor/ckeditor5-utils/src/mix';
 import clone from '@ckeditor/ckeditor5-utils/src/lib/lodash/clone';
+import compareArrays from '@ckeditor/ckeditor5-utils/src/comparearrays';
 
 /**
  * Abstract tree view node class.
@@ -119,6 +120,33 @@ export default class Node {
 	}
 
 	/**
+	 * Gets a path to the node. The path is an array containing indices of consecutive ancestors of this node,
+	 * beginning from {@link module:engine/view/node~Node#root root}, down to this node's index.
+	 *
+	 *		const abc = new Text( 'abc' );
+	 *		const foo = new Text( 'foo' );
+	 *		const h1 = new Element( 'h1', null, new Text( 'header' ) );
+	 *		const p = new Element( 'p', null, [ abc, foo ] );
+	 *		const div = new Element( 'div', null, [ h1, p ] );
+	 *		foo.getPath(); // Returns [ 1, 3 ]. `foo` is in `p` which is in `div`. `p` starts at offset 1, while `foo` at 3.
+	 *		h1.getPath(); // Returns [ 0 ].
+	 *		div.getPath(); // Returns [].
+	 *
+	 * @returns {Array.<Number>} The path.
+	 */
+	getPath() {
+		const path = [];
+		let node = this; // eslint-disable-line consistent-this
+
+		while ( node.parent ) {
+			path.unshift( node.index );
+			node = node.parent;
+		}
+
+		return path;
+	}
+
+	/**
 	 * Returns ancestors array of this node.
 	 *
 	 * @param {Object} options Options object.
@@ -163,6 +191,63 @@ export default class Node {
 	}
 
 	/**
+	 * Returns whether this node is before given node. `false` is returned if nodes are in different trees (for example,
+	 * in different {@link module:engine/view/documentfragment~DocumentFragment}s).
+	 *
+	 * @param {module:engine/view/node~Node} node Node to compare with.
+	 * @returns {Boolean}
+	 */
+	isBefore( node ) {
+		// Given node is not before this node if they are same.
+		if ( this == node ) {
+			return false;
+		}
+
+		// Return `false` if it is impossible to compare nodes.
+		if ( this.root !== node.root ) {
+			return false;
+		}
+
+		const thisPath = this.getPath();
+		const nodePath = node.getPath();
+
+		const result = compareArrays( thisPath, nodePath );
+
+		switch ( result ) {
+			case 'prefix':
+				return true;
+
+			case 'extension':
+				return false;
+
+			default:
+				return thisPath[ result ] < nodePath[ result ];
+		}
+	}
+
+	/**
+	 * Returns whether this node is after given node. `false` is returned if nodes are in different trees (for example,
+	 * in different {@link module:engine/view/documentfragment~DocumentFragment}s).
+	 *
+	 * @param {module:engine/view/node~Node} node Node to compare with.
+	 * @returns {Boolean}
+	 */
+	isAfter( node ) {
+		// Given node is not before this node if they are same.
+		if ( this == node ) {
+			return false;
+		}
+
+		// Return `false` if it is impossible to compare nodes.
+		if ( this.root !== node.root ) {
+			return false;
+		}
+
+		// In other cases, just check if the `node` is before, and return the opposite.
+		return !this.isBefore( node );
+	}
+
+	/**
 	 * Removes node from parent.
 	 *
 	 * @protected
@@ -199,27 +284,13 @@ export default class Node {
 	}
 
 	/**
-	 * Clones this node.
-	 *
-	 * @method #clone
-	 * @returns {module:engine/view/node~Node} Clone of this node.
-	 */
-
-	/**
-	 * Checks if provided node is similar to this node.
-	 *
-	 * @method #isSimilar
-	 * @returns {Boolean} True if nodes are similar.
-	 */
-
-	/**
 	 * Checks whether given view tree object is of given type.
 	 *
 	 * This method is useful when processing view tree objects that are of unknown type. For example, a function
 	 * may return {@link module:engine/view/documentfragment~DocumentFragment} or {@link module:engine/view/node~Node}
 	 * that can be either text node or element. This method can be used to check what kind of object is returned.
 	 *
-	 *		obj.is( 'node' ); // true for any node, false for document fragment
+	 *		obj.is( 'node' ); // true for any node, false for document fragment and text fragment
 	 *		obj.is( 'documentFragment' ); // true for document fragment, false for any node
 	 *		obj.is( 'element' ); // true for any element, false for text node or document fragment
 	 *		obj.is( 'element', 'p' ); // true only for element which name is 'p'
@@ -231,6 +302,24 @@ export default class Node {
 	 * 'rootElement'|'documentFragment'|'text'|'textProxy'} type
 	 * @returns {Boolean}
 	 */
+	is( type ) {
+		return type == 'node';
+	}
+
+	/**
+	 * Clones this node.
+	 *
+	 * @protected
+	 * @method #_clone
+	 * @returns {module:engine/view/node~Node} Clone of this node.
+	 */
+
+	/**
+	 * Checks if provided node is similar to this node.
+	 *
+	 * @method #isSimilar
+	 * @returns {Boolean} True if nodes are similar.
+	 */
 }
 
 /**

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

@@ -86,12 +86,12 @@ export default class FakeSelectionObserver extends Observer {
 
 		// Left or up arrow pressed - move selection to start.
 		if ( keyCode == keyCodes.arrowleft || keyCode == keyCodes.arrowup ) {
-			newSelection._setTo( newSelection.getFirstPosition() );
+			newSelection.setTo( newSelection.getFirstPosition() );
 		}
 
 		// Right or down arrow pressed - move selection to end.
 		if ( keyCode == keyCodes.arrowright || keyCode == keyCodes.arrowdown ) {
-			newSelection._setTo( newSelection.getLastPosition() );
+			newSelection.setTo( newSelection.getLastPosition() );
 		}
 
 		const data = {

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

@@ -240,7 +240,7 @@ export default class MutationObserver extends Observer {
 			// Anchor and focus has to be properly mapped to view.
 			if ( viewSelectionAnchor && viewSelectionFocus ) {
 				viewSelection = new ViewSelection( viewSelectionAnchor );
-				viewSelection._setFocus( viewSelectionFocus );
+				viewSelection.setFocus( viewSelectionFocus );
 			}
 		}
 

+ 6 - 4
packages/ckeditor5-engine/src/view/observer/selectionobserver.js

@@ -42,10 +42,12 @@ export default class SelectionObserver extends Observer {
 		this.mutationObserver = view.getObserver( MutationObserver );
 
 		/**
-		 * Reference to the view {@link module:engine/view/selection~Selection} object used to compare new selection with it.
+		 * Reference to the view {@link module:engine/view/documentselection~DocumentSelection} object used to compare
+		 * new selection with it.
 		 *
 		 * @readonly
-		 * @member {module:engine/view/selection~Selection} module:engine/view/observer/selectionobserver~SelectionObserver#selection
+		 * @member {module:engine/view/documentselection~DocumentSelection}
+		 * module:engine/view/observer/selectionobserver~SelectionObserver#selection
 		 */
 		this.selection = this.document.selection;
 
@@ -205,7 +207,7 @@ export default class SelectionObserver extends Observer {
  * @see module:engine/view/observer/selectionobserver~SelectionObserver
  * @event module:engine/view/document~Document#event:selectionChange
  * @param {Object} data
- * @param {module:engine/view/selection~Selection} data.oldSelection Old View selection which is
+ * @param {module:engine/view/documentselection~DocumentSelection} data.oldSelection Old View selection which is
  * {@link module:engine/view/document~Document#selection}.
  * @param {module:engine/view/selection~Selection} data.newSelection New View selection which is converted DOM selection.
  * @param {Selection} data.domSelection Native DOM selection.
@@ -222,7 +224,7 @@ export default class SelectionObserver extends Observer {
  * @see module:engine/view/observer/selectionobserver~SelectionObserver
  * @event module:engine/view/document~Document#event:selectionChangeDone
  * @param {Object} data
- * @param {module:engine/view/selection~Selection} data.oldSelection Old View selection which is
+ * @param {module:engine/view/documentselection~DocumentSelection} data.oldSelection Old View selection which is
  * {@link module:engine/view/document~Document#selection}.
  * @param {module:engine/view/selection~Selection} data.newSelection New View selection which is converted DOM selection.
  * @param {Selection} data.domSelection Native DOM selection.

+ 14 - 40
packages/ckeditor5-engine/src/view/position.js

@@ -243,61 +243,35 @@ export default class Position {
 	 * @returns {module:engine/view/position~PositionRelation}
 	 */
 	compareWith( otherPosition ) {
-		if ( this.isEqual( otherPosition ) ) {
-			return 'same';
+		if ( this.root !== otherPosition.root ) {
+			return 'different';
 		}
 
-		// If positions have same parent.
-		if ( this.parent === otherPosition.parent ) {
-			return this.offset - otherPosition.offset < 0 ? 'before' : 'after';
+		if ( this.isEqual( otherPosition ) ) {
+			return 'same';
 		}
 
 		// Get path from root to position's parent element.
-		const path = this.getAncestors();
-		const otherPath = otherPosition.getAncestors();
+		const thisPath = this.parent.is( 'node' ) ? this.parent.getPath() : [];
+		const otherPath = otherPosition.parent.is( 'node' ) ? otherPosition.parent.getPath() : [];
 
-		// Compare both path arrays to find common ancestor.
-		const result = compareArrays( path, otherPath );
+		// Add the positions' offsets to the parents offsets.
+		thisPath.push( this.offset );
+		otherPath.push( otherPosition.offset );
 
-		let commonAncestorIndex;
+		// Compare both path arrays to find common ancestor.
+		const result = compareArrays( thisPath, otherPath );
 
 		switch ( result ) {
-			case 0:
-				// No common ancestors found.
-				return 'different';
-
 			case 'prefix':
-				commonAncestorIndex = path.length - 1;
-				break;
+				return 'before';
 
 			case 'extension':
-				commonAncestorIndex = otherPath.length - 1;
-				break;
+				return 'after';
 
 			default:
-				commonAncestorIndex = result - 1;
+				return thisPath[ result ] < otherPath[ result ] ? 'before' : 'after';
 		}
-
-		// Common ancestor of two positions.
-		const commonAncestor = path[ commonAncestorIndex ];
-		const nextAncestor1 = path[ commonAncestorIndex + 1 ];
-		const nextAncestor2 = otherPath[ commonAncestorIndex + 1 ];
-
-		// Check if common ancestor is not one of the parents.
-		if ( commonAncestor === this.parent ) {
-			const index = this.offset - nextAncestor2.index;
-
-			return index <= 0 ? 'before' : 'after';
-		} else if ( commonAncestor === otherPosition.parent ) {
-			const index = nextAncestor1.index - otherPosition.offset;
-
-			return index < 0 ? 'before' : 'after';
-		}
-
-		const index = nextAncestor1.index - nextAncestor2.index;
-
-		// Compare indexes of next ancestors inside common one.
-		return index < 0 ? 'before' : 'after';
 	}
 
 	/**

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

@@ -37,7 +37,7 @@ export default class Renderer {
 	 * Creates a renderer instance.
 	 *
 	 * @param {module:engine/view/domconverter~DomConverter} domConverter Converter instance.
-	 * @param {module:engine/view/selection~Selection} selection View selection.
+	 * @param {module:engine/view/documentselection~DocumentSelection} selection View selection.
 	 */
 	constructor( domConverter, selection ) {
 		/**
@@ -84,7 +84,7 @@ export default class Renderer {
 		 * View selection. Renderer updates DOM selection based on the view selection.
 		 *
 		 * @readonly
-		 * @member {module:engine/view/selection~Selection}
+		 * @member {module:engine/view/documentselection~DocumentSelection}
 		 */
 		this.selection = selection;
 

+ 74 - 63
packages/ckeditor5-engine/src/view/selection.js

@@ -16,21 +16,23 @@ import Node from './node';
 import Element from './element';
 import count from '@ckeditor/ckeditor5-utils/src/count';
 import isIterable from '@ckeditor/ckeditor5-utils/src/isiterable';
+import DocumentSelection from './documentselection';
 
 /**
  * Class representing selection in tree view.
  *
  * Selection can consist of {@link module:engine/view/range~Range ranges} that can be set using
- * {@link module:engine/view/selection~Selection#_setTo} method.
+ * {@link module:engine/view/selection~Selection#setTo setTo} method.
  * That method create copies of provided ranges and store those copies internally. Further modifications to passed
  * ranges will not change selection's state.
  * Selection's ranges can be obtained via {@link module:engine/view/selection~Selection#getRanges getRanges},
- * {@link module:engine/view/selection~Selection#getFirstRange getFirstRange}
- * and {@link module:engine/view/selection~Selection#getLastRange getLastRange}
- * methods, which return copies of ranges stored inside selection. Modifications made on these copies will not change
- * selection's state. Similar situation occurs when getting {@link module:engine/view/selection~Selection#anchor anchor},
- * {@link module:engine/view/selection~Selection#focus focus}, {@link module:engine/view/selection~Selection#getFirstPosition first} and
- * {@link module:engine/view/selection~Selection#getLastPosition last} positions - all will return copies of requested positions.
+ * {@link module:engine/view/selection~Selection#getFirstRange getFirstRange} and
+ * {@link module:engine/view/selection~Selection#getLastRange getLastRange} methods, which return copies of ranges
+ * stored inside selection. Modifications made on these copies will not change selection's state. Similar situation
+ * occurs when getting {@link module:engine/view/selection~Selection#anchor anchor},
+ * {@link module:engine/view/selection~Selection#focus focus}, {@link module:engine/view/selection~Selection#getFirstPosition first}
+ * and {@link module:engine/view/selection~Selection#getLastPosition last} positions - all will return
+ * copies of requested positions.
  */
 export default class Selection {
 	/**
@@ -51,12 +53,15 @@ export default class Selection {
 	 *		const otherSelection = new Selection();
 	 *		const selection = new Selection( otherSelection );
 	 *
+	 *		// Creates selection from the document selection.
+	 *		const selection = new Selection( editor.editing.view.document.selection );
+	 *
 	 * 		// Creates selection at the given position.
 	 *		const position = new Position( root, path );
 	 *		const selection = new Selection( position );
 	 *
 	 *		// Creates collapsed selection at the position of given item and offset.
-	 *		const paragraph = writer.createElement( 'paragraph' );
+	 *		const paragraph = writer.createContainerElement( 'paragraph' );
 	 *		const selection = new Selection( paragraph, offset );
 	 *
 	 *		// Creates a range inside an {@link module:engine/view/element~Element element} which starts before the
@@ -82,8 +87,9 @@ export default class Selection {
 	 *		// Creates fake selection with label.
 	 *		const selection = new Selection( range, { fake: true, label: 'foo' } );
 	 *
-	 * @param {module:engine/view/selection~Selection|module:engine/view/position~Position|
-	 * Iterable.<module:engine/view/range~Range>|module:engine/view/range~Range|module:engine/view/item~Item|null} [selectable=null]
+	 * @param {module:engine/view/selection~Selection|module:engine/view/documentselection~DocumentSelection|
+	 * module:engine/view/position~Position|Iterable.<module:engine/view/range~Range>|module:engine/view/range~Range|
+	 * module:engine/view/item~Item|null} [selectable=null]
 	 * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Offset or place when selectable is an `Item`.
 	 * @param {Object} [options]
 	 * @param {Boolean} [options.backward] Sets this selection instance to be backward.
@@ -123,13 +129,13 @@ export default class Selection {
 		 */
 		this._fakeSelectionLabel = '';
 
-		this._setTo( selectable, placeOrOffset, options );
+		this.setTo( selectable, placeOrOffset, options );
 	}
 
 	/**
 	 * Returns true if selection instance is marked as `fake`.
 	 *
-	 * @see #_setTo
+	 * @see #setTo
 	 * @returns {Boolean}
 	 */
 	get isFake() {
@@ -139,7 +145,7 @@ export default class Selection {
 	/**
 	 * Returns fake selection label.
 	 *
-	 * @see #_setTo
+	 * @see #setTo
 	 * @returns {String}
 	 */
 	get fakeSelectionLabel() {
@@ -303,7 +309,8 @@ export default class Selection {
 	 * Checks whether, this selection is equal to given selection. Selections are equal if they have same directions,
 	 * same number of ranges and all ranges from one selection equal to a range from other selection.
 	 *
-	 * @param {module:engine/view/selection~Selection} otherSelection Selection to compare with.
+	 * @param {module:engine/view/selection~Selection|module:engine/view/documentselection~DocumentSelection} otherSelection
+	 * Selection to compare with.
 	 * @returns {Boolean} `true` if selections are equal, `false` otherwise.
 	 */
 	isEqual( otherSelection ) {
@@ -348,7 +355,8 @@ export default class Selection {
 	 * number of ranges, and all {@link module:engine/view/range~Range#getTrimmed trimmed} ranges from one selection are
 	 * equal to any trimmed range from other selection.
 	 *
-	 * @param {module:engine/view/selection~Selection} otherSelection Selection to compare with.
+	 * @param {module:engine/view/selection~Selection|module:engine/view/documentselection~DocumentSelection} otherSelection
+	 * Selection to compare with.
 	 * @returns {Boolean} `true` if selections are similar, `false` otherwise.
 	 */
 	isSimilar( otherSelection ) {
@@ -415,45 +423,49 @@ export default class Selection {
 
 	/**
 	 * Sets this selection's ranges and direction to the specified location based on the given
+	 * {@link module:engine/view/documentselection~DocumentSelection document selection},
 	 * {@link module:engine/view/selection~Selection selection}, {@link module:engine/view/position~Position position},
 	 * {@link module:engine/view/item~Item item}, {@link module:engine/view/range~Range range},
 	 * an iterable of {@link module:engine/view/range~Range ranges} or null.
 	 *
 	 *		// Sets selection to the given range.
 	 *		const range = new Range( start, end );
-	 *		selection._setTo( range );
+	 *		selection.setTo( range );
 	 *
 	 *		// Sets selection to given ranges.
 	 * 		const ranges = [ new Range( start1, end2 ), new Range( star2, end2 ) ];
-	 *		selection._setTo( range );
+	 *		selection.setTo( range );
 	 *
 	 *		// Sets selection to the other selection.
 	 *		const otherSelection = new Selection();
-	 *		selection._setTo( otherSelection );
+	 *		selection.setTo( otherSelection );
+	 *
+	 *	 	// Sets selection to contents of DocumentSelection.
+	 *		selection.setTo( editor.editing.view.document.selection );
 	 *
 	 * 		// Sets collapsed selection at the given position.
 	 *		const position = new Position( root, path );
-	 *		selection._setTo( position );
+	 *		selection.setTo( position );
 	 *
 	 * 		// Sets collapsed selection at the position of given item and offset.
-	 *		selection._setTo( paragraph, offset );
+	 *		selection.setTo( paragraph, offset );
 	 *
 	 * Creates a range inside an {@link module:engine/view/element~Element element} which starts before the first child of
 	 * that element and ends after the last child of that element.
 	 *
-	 *		selection._setTo( paragraph, 'in' );
+	 *		selection.setTo( paragraph, 'in' );
 	 *
 	 * Creates a range on an {@link module:engine/view/item~Item item} which starts before the item and ends just after the item.
 	 *
-	 *		selection._setTo( paragraph, 'on' );
+	 *		selection.setTo( paragraph, 'on' );
 	 *
 	 * 		// Clears selection. Removes all ranges.
-	 *		selection._setTo( null );
+	 *		selection.setTo( null );
 	 *
-	 * `Selection#_setTo()` method allow passing additional options (`backward`, `fake` and `label`) as the last argument.
+	 * `Selection#setTo()` method allow passing additional options (`backward`, `fake` and `label`) as the last argument.
 	 *
 	 *		// Sets selection as backward.
-	 *		selection._setTo( range, { backward: true } );
+	 *		selection.setTo( range, { backward: true } );
 	 *
 	 * 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
@@ -463,23 +475,23 @@ export default class Selection {
 	 * (and be  properly handled by screen readers).
 	 *
 	 *		// Creates fake selection with label.
-	 *		selection._setTo( range, { fake: true, label: 'foo' } );
+	 *		selection.setTo( range, { fake: true, label: 'foo' } );
 	 *
-	 * @protected
 	 * @fires change
-	 * @param {module:engine/view/selection~Selection|module:engine/view/position~Position|
-	 * Iterable.<module:engine/view/range~Range>|module:engine/view/range~Range|module:engine/view/item~Item|null} selectable
+	 * @param {module:engine/view/selection~Selection|module:engine/view/documentselection~DocumentSelection|
+	 * module:engine/view/position~Position|Iterable.<module:engine/view/range~Range>|module:engine/view/range~Range|
+	 * module:engine/view/item~Item|null} selectable
 	 * @param {Number|'before'|'end'|'after'|'on'|'in'} [placeOrOffset] Sets place or offset of the selection.
 	 * @param {Object} [options]
 	 * @param {Boolean} [options.backward] Sets this selection instance to be backward.
 	 * @param {Boolean} [options.fake] Sets this selection instance to be marked as `fake`.
 	 * @param {String} [options.label] Label for the fake selection.
 	 */
-	_setTo( selectable, placeOrOffset, options ) {
+	setTo( selectable, placeOrOffset, options ) {
 		if ( selectable === null ) {
 			this._setRanges( [] );
 			this._setFakeOptions( placeOrOffset );
-		} else if ( selectable instanceof Selection ) {
+		} else if ( selectable instanceof Selection || selectable instanceof DocumentSelection ) {
 			this._setRanges( selectable.getRanges(), selectable.isBackward );
 			this._setFakeOptions( { fake: selectable.isFake, label: selectable.fakeSelectionLabel } );
 		} else if ( selectable instanceof Range ) {
@@ -530,41 +542,16 @@ export default class Selection {
 	}
 
 	/**
-	 * Replaces all ranges that were added to the selection with given array of ranges. Last range of the array
-	 * is treated like the last added range and is used to set {@link #anchor anchor} and {@link #focus focus}.
-	 * Accepts a flag describing in which way the selection is made.
-	 *
-	 * @private
-	 * @param {Iterable.<module:engine/view/range~Range>} newRanges Iterable object of ranges to set.
-	 * @param {Boolean} [isLastBackward=false] Flag describing if last added range was selected forward - from start to end
-	 * (`false`) or backward - from end to start (`true`). Defaults to `false`.
-	 */
-	_setRanges( newRanges, isLastBackward = false ) {
-		// New ranges should be copied to prevent removing them by setting them to `[]` first.
-		// Only applies to situations when selection is set to the same selection or same selection's ranges.
-		newRanges = Array.from( newRanges );
-
-		this._ranges = [];
-
-		for ( const range of newRanges ) {
-			this._addRange( range );
-		}
-
-		this._lastRangeBackward = !!isLastBackward;
-	}
-
-	/**
 	 * Moves {@link #focus} to the specified location.
 	 *
 	 * The location can be specified in the same form as {@link module:engine/view/position~Position.createAt} parameters.
 	 *
-	 * @protected
 	 * @fires change
 	 * @param {module:engine/view/item~Item|module:engine/view/position~Position} itemOrPosition
 	 * @param {Number|'end'|'before'|'after'} [offset=0] Offset or one of the flags. Used only when
 	 * first parameter is a {@link module:engine/view/item~Item view item}.
 	 */
-	_setFocus( itemOrPosition, offset ) {
+	setFocus( itemOrPosition, offset ) {
 		if ( this.anchor === null ) {
 			/**
 			 * Cannot set selection focus if there are no ranges in selection.
@@ -596,6 +583,30 @@ export default class Selection {
 	}
 
 	/**
+	 * Replaces all ranges that were added to the selection with given array of ranges. Last range of the array
+	 * is treated like the last added range and is used to set {@link #anchor anchor} and {@link #focus focus}.
+	 * Accepts a flag describing in which way the selection is made.
+	 *
+	 * @private
+	 * @param {Iterable.<module:engine/view/range~Range>} newRanges Iterable object of ranges to set.
+	 * @param {Boolean} [isLastBackward=false] Flag describing if last added range was selected forward - from start to end
+	 * (`false`) or backward - from end to start (`true`). Defaults to `false`.
+	 */
+	_setRanges( newRanges, isLastBackward = false ) {
+		// New ranges should be copied to prevent removing them by setting them to `[]` first.
+		// Only applies to situations when selection is set to the same selection or same selection's ranges.
+		newRanges = Array.from( newRanges );
+
+		this._ranges = [];
+
+		for ( const range of newRanges ) {
+			this._addRange( range );
+		}
+
+		this._lastRangeBackward = !!isLastBackward;
+	}
+
+	/**
 	 * 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.
@@ -667,12 +678,12 @@ export default class Selection {
 
 		this._ranges.push( Range.createFromRange( range ) );
 	}
+
+	/**
+	 * Fired whenever selection ranges are changed through {@link ~Selection Selection API}.
+	 *
+	 * @event change
+	 */
 }
 
 mix( Selection, EmitterMixin );
-
-/**
- * Fired whenever selection ranges are changed through {@link ~Selection Selection API}.
- *
- * @event change
- */

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

@@ -42,7 +42,7 @@ export default class Text extends Node {
 	 * @inheritDoc
 	 */
 	is( type ) {
-		return type == 'text';
+		return type == 'text' || super.is( type );
 	}
 
 	/**

+ 4 - 2
packages/ckeditor5-engine/src/view/writer.js

@@ -44,7 +44,8 @@ export default class Writer {
 	}
 
 	/**
-	 * Sets {@link module:engine/view/selection~Selection selection's} ranges and direction to the specified location based on the given
+	 * Sets {@link module:engine/view/documentselection~DocumentSelection selection's} ranges and direction to the
+	 * specified location based on the given {@link module:engine/view/documentselection~DocumentSelection document selection},
 	 * {@link module:engine/view/selection~Selection selection}, {@link module:engine/view/position~Position position},
 	 * {@link module:engine/view/item~Item item}, {@link module:engine/view/range~Range range},
 	 * an iterable of {@link module:engine/view/range~Range ranges} or null.
@@ -72,6 +73,7 @@ export default class Writer {
 	 *		writer.setSelection( position );
 	 *
 	 * 		// Sets collapsed selection at the position of given item and offset.
+	 *		const paragraph = writer.createContainerElement( 'paragraph' );
 	 *		writer.setSelection( paragraph, offset );
 	 *
 	 * Creates a range inside an {@link module:engine/view/element~Element element} which starts before the first child of
@@ -114,7 +116,7 @@ export default class Writer {
 	}
 
 	/**
-	 * Moves {@link module:engine/view/selection~Selection#focus selection's focus} to the specified location.
+	 * Moves {@link module:engine/view/documentselection~DocumentSelection#focus selection's focus} to the specified location.
 	 *
 	 * The location can be specified in the same form as {@link module:engine/view/position~Position.createAt} parameters.
 	 *

+ 43 - 0
packages/ckeditor5-engine/tests/conversion/conversion.js

@@ -407,6 +407,49 @@ describe( 'Conversion', () => {
 					'<p>Foo bar</p>'
 				);
 			} );
+
+			it( 'config.model.name is given', () => {
+				schema.extend( '$text', {
+					allowAttributes: [ 'textDecoration' ]
+				} );
+
+				conversion.attributeToElement( {
+					model: {
+						key: 'textDecoration',
+						values: [ 'underline', 'lineThrough' ],
+						name: '$text'
+					},
+					view: {
+						underline: {
+							name: 'span',
+							style: {
+								'text-decoration': 'underline'
+							}
+						},
+						lineThrough: {
+							name: 'span',
+							style: {
+								'text-decoration': 'line-through'
+							}
+						}
+					}
+				} );
+
+				test(
+					'<p><span style="text-decoration:underline">Foo</span></p>',
+					'<paragraph><$text textDecoration="underline">Foo</$text></paragraph>'
+				);
+
+				test(
+					'<p><span style="text-decoration:line-through">Foo</span></p>',
+					'<paragraph><$text textDecoration="lineThrough">Foo</$text></paragraph>'
+				);
+
+				test(
+					'<p><span style="text-decoration:underline">Foo</span></p>',
+					'<paragraph><$text textDecoration="underline">Foo</$text></paragraph>'
+				);
+			} );
 		} );
 
 		describe( 'attributeToAttribute', () => {

+ 48 - 1
packages/ckeditor5-engine/tests/conversion/downcast-converters.js

@@ -209,6 +209,37 @@ describe( 'downcast-helpers', () => {
 
 			expectResult( '<span style="font-weight:500">foo</span>' );
 		} );
+
+		it( 'config.model.name is given', () => {
+			const helper = downcastAttributeToElement( {
+				model: {
+					key: 'color',
+					name: '$text'
+				},
+				view: ( modelAttributeValue, viewWriter ) => {
+					return viewWriter.createAttributeElement( 'span', { style: 'color:' + modelAttributeValue } );
+				}
+			} );
+
+			conversion.for( 'downcast' )
+				.add( helper )
+				.add( downcastElementToElement( {
+					model: 'smiley',
+					view: ( modelElement, viewWriter ) => {
+						return viewWriter.createEmptyElement( 'img', {
+							src: 'smile.jpg',
+							class: 'smiley'
+						} );
+					}
+				} ) );
+
+			model.change( writer => {
+				writer.insertText( 'foo', { color: '#FF0000' }, modelRoot, 0 );
+				writer.insertElement( 'smiley', { color: '#FF0000' }, modelRoot, 3 );
+			} );
+
+			expectResult( '<span style="color:#FF0000">foo</span><img class="smiley" src="smile.jpg"></img>' );
+		} );
 	} );
 
 	describe( 'downcastAttributeToAttribute', () => {
@@ -615,7 +646,7 @@ describe( 'downcast-converters', () => {
 		} );
 	} );
 
-	describe( 'setAttribute', () => {
+	describe( 'changeAttribute', () => {
 		it( 'should convert attribute insert/change/remove on a model node', () => {
 			const modelElement = new ModelElement( 'paragraph', { class: 'foo' }, new ModelText( 'foobar' ) );
 
@@ -686,6 +717,22 @@ describe( 'downcast-converters', () => {
 			// No attribute set.
 			expect( viewToString( viewRoot ) ).to.equal( '<div><p>foobar</p></div>' );
 		} );
+
+		it( 'should not convert or consume if element creator returned null', () => {
+			const callback = sinon.stub().returns( null );
+
+			dispatcher.on( 'attribute:class', changeAttribute( callback ) );
+
+			const modelElement = new ModelElement( 'paragraph', { class: 'foo' }, new ModelText( 'foobar' ) );
+
+			model.change( writer => {
+				writer.insert( modelElement, modelRootStart );
+			} );
+
+			expect( viewToString( viewRoot ) ).to.equal( '<div><p class="foo">foobar</p></div>' );
+
+			sinon.assert.called( callback );
+		} );
 	} );
 
 	describe( 'wrap', () => {

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

@@ -46,7 +46,7 @@ describe( 'convertSelectionChange', () => {
 
 	it( 'should convert collapsed selection', () => {
 		const viewSelection = new ViewSelection();
-		viewSelection._setTo( ViewRange.createFromParentsAndOffsets(
+		viewSelection.setTo( ViewRange.createFromParentsAndOffsets(
 			viewRoot.getChild( 0 ).getChild( 0 ), 1, viewRoot.getChild( 0 ).getChild( 0 ), 1 ) );
 
 		convertSelection( null, { newSelection: viewSelection } );

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

@@ -14,7 +14,7 @@ import ContainerElement from '../../src/view/containerelement';
 import EmptyElement from '../../src/view/emptyelement';
 import UIElement from '../../src/view/uielement';
 import Text from '../../src/view/text';
-import Selection from '../../src/view/selection';
+import DocumentSelection from '../../src/view/documentselection';
 import Range from '../../src/view/range';
 import View from '../../src/view/view';
 import XmlDataProcessor from '../../src/dataprocessor/xmldataprocessor';
@@ -167,7 +167,7 @@ describe( 'view test utils', () => {
 			const b2 = new Element( 'b', null, text2 );
 			const p = new Element( 'p', null, [ b1, b2 ] );
 			const range = Range.createFromParentsAndOffsets( p, 1, p, 2 );
-			const selection = new Selection( [ range ] );
+			const selection = new DocumentSelection( [ range ] );
 			expect( stringify( p, selection ) ).to.equal( '<p><b>foobar</b>[<b>bazqux</b>]</p>' );
 		} );
 
@@ -176,7 +176,7 @@ describe( 'view test utils', () => {
 			const b = new Element( 'b', null, text );
 			const p = new Element( 'p', null, b );
 			const range = Range.createFromParentsAndOffsets( p, 0, text, 4 );
-			const selection = new Selection( [ range ] );
+			const selection = new DocumentSelection( [ range ] );
 
 			expect( stringify( p, selection ) ).to.equal( '<p>[<b>நிலை}க்கு</b></p>' );
 		} );
@@ -185,7 +185,7 @@ describe( 'view test utils', () => {
 			const text = new Text( 'foobar' );
 			const p = new Element( 'p', null, text );
 			const range = Range.createFromParentsAndOffsets( p, 0, p, 0 );
-			const selection = new Selection( [ range ] );
+			const selection = new DocumentSelection( [ range ] );
 			expect( stringify( p, selection ) ).to.equal( '<p>[]foobar</p>' );
 		} );
 
@@ -196,7 +196,7 @@ describe( 'view test utils', () => {
 			const b2 = new Element( 'b', null, text2 );
 			const p = new Element( 'p', null, [ b1, b2 ] );
 			const range = Range.createFromParentsAndOffsets( text1, 1, text1, 5 );
-			const selection = new Selection( [ range ] );
+			const selection = new DocumentSelection( [ range ] );
 			expect( stringify( p, selection ) ).to.equal( '<p><b>f{ooba}r</b><b>bazqux</b></p>' );
 		} );
 
@@ -207,7 +207,7 @@ describe( 'view test utils', () => {
 			const b2 = new Element( 'b', null, text2 );
 			const p = new Element( 'p', null, [ b1, b2 ] );
 			const range = Range.createFromParentsAndOffsets( text1, 1, text1, 5 );
-			const selection = new Selection( [ range ] );
+			const selection = new DocumentSelection( [ range ] );
 			expect( stringify( p, selection, { sameSelectionCharacters: true } ) )
 				.to.equal( '<p><b>f[ooba]r</b><b>bazqux</b></p>' );
 		} );
@@ -216,7 +216,7 @@ describe( 'view test utils', () => {
 			const text = new Text( 'foobar' );
 			const p = new Element( 'p', null, text );
 			const range = Range.createFromParentsAndOffsets( text, 0, text, 0 );
-			const selection = new Selection( [ range ] );
+			const selection = new DocumentSelection( [ range ] );
 			expect( stringify( p, selection ) ).to.equal( '<p>{}foobar</p>' );
 		} );
 
@@ -227,7 +227,7 @@ describe( 'view test utils', () => {
 			const b2 = new Element( 'b', null, text2 );
 			const p = new Element( 'p', null, [ b1, b2 ] );
 			const range = Range.createFromParentsAndOffsets( p, 0, text2, 5 );
-			const selection = new Selection( [ range ] );
+			const selection = new DocumentSelection( [ range ] );
 			expect( stringify( p, selection ) ).to.equal( '<p>[<b>foobar</b><b>bazqu}x</b></p>' );
 		} );
 
@@ -317,7 +317,7 @@ describe( 'view test utils', () => {
 			const p = new Element( 'p', null, [ b1, b2 ] );
 			const range1 = Range.createFromParentsAndOffsets( p, 0, p, 1 );
 			const range2 = Range.createFromParentsAndOffsets( p, 1, p, 1 );
-			const selection = new Selection( [ range2, range1 ] );
+			const selection = new DocumentSelection( [ range2, range1 ] );
 
 			expect( stringify( p, selection ) ).to.equal( '<p>[<b>foobar</b>][]<b>bazqux</b></p>' );
 		} );
@@ -331,7 +331,7 @@ describe( 'view test utils', () => {
 			const range2 = Range.createFromParentsAndOffsets( text2, 0, text2, 3 );
 			const range3 = Range.createFromParentsAndOffsets( text2, 3, text2, 4 );
 			const range4 = Range.createFromParentsAndOffsets( p, 1, p, 1 );
-			const selection = new Selection( [ range1, range2, range3, range4 ] );
+			const selection = new DocumentSelection( [ range1, range2, range3, range4 ] );
 
 			expect( stringify( p, selection ) ).to.equal( '<p>[<b>foobar</b>][]{baz}{q}ux</p>' );
 		} );

+ 2 - 3
packages/ckeditor5-engine/tests/manual/tickets/ckeditor5-721/1.js

@@ -43,8 +43,7 @@ ClassicEditor
 		editor.conversion.for( 'downcast' )
 			.add( downcastElementToElement( {
 				model: 'widget',
-				view: ( modelItem, conversionApi ) => {
-					const writer = conversionApi.writer;
+				view: ( modelItem, writer ) => {
 					const b = writer.createAttributeElement( 'b' );
 					const div = writer.createContainerElement( 'div' );
 
@@ -55,7 +54,7 @@ ClassicEditor
 			} ) )
 			.add( downcastElementToElement( {
 				model: 'nested',
-				view: ( item, api ) => api.writer.createEditableElement( 'figcaption', { contenteditable: true } )
+				view: ( item, writer ) => writer.createEditableElement( 'figcaption', { contenteditable: true } )
 			} ) );
 
 		setData( editor.model,

+ 1 - 0
packages/ckeditor5-engine/tests/model/documentfragment.js

@@ -74,6 +74,7 @@ describe( 'DocumentFragment', () => {
 		} );
 
 		it( 'should return false for other accept values', () => {
+			expect( frag.is( 'node' ) ).to.be.false;
 			expect( frag.is( 'text' ) ).to.be.false;
 			expect( frag.is( 'textProxy' ) ).to.be.false;
 			expect( frag.is( 'element' ) ).to.be.false;

+ 2 - 1
packages/ckeditor5-engine/tests/model/element.js

@@ -46,7 +46,8 @@ describe( 'Element', () => {
 			element = new Element( 'paragraph' );
 		} );
 
-		it( 'should return true for element, element with same name and element name', () => {
+		it( 'should return true for node, element, element with same name and element name', () => {
+			expect( element.is( 'node' ) ).to.be.true;
 			expect( element.is( 'element' ) ).to.be.true;
 			expect( element.is( 'element', 'paragraph' ) ).to.be.true;
 			expect( element.is( 'paragraph' ) ).to.be.true;

+ 82 - 0
packages/ckeditor5-engine/tests/model/node.js

@@ -164,6 +164,12 @@ describe( 'Node', () => {
 		} );
 	} );
 
+	describe( 'is()', () => {
+		it( 'should return true for node', () => {
+			expect( node.is( 'node' ) ).to.be.true;
+		} );
+	} );
+
 	describe( 'startOffset', () => {
 		it( 'should return null if the parent is null', () => {
 			expect( root.startOffset ).to.be.null;
@@ -319,6 +325,82 @@ describe( 'Node', () => {
 		} );
 	} );
 
+	describe( 'isBefore()', () => {
+		// Model is: <root><one></one><two>ba<img></img>r</two><three></three>
+		it( 'should return true if the element is before given element', () => {
+			expect( one.isBefore( two ) ).to.be.true;
+			expect( one.isBefore( img ) ).to.be.true;
+
+			expect( two.isBefore( textBA ) ).to.be.true;
+			expect( two.isBefore( textR ) ).to.be.true;
+			expect( two.isBefore( three ) ).to.be.true;
+
+			expect( root.isBefore( one ) ).to.be.true;
+		} );
+
+		it( 'should return false if the element is after given element', () => {
+			expect( two.isBefore( one ) ).to.be.false;
+			expect( img.isBefore( one ) ).to.be.false;
+
+			expect( textBA.isBefore( two ) ).to.be.false;
+			expect( textR.isBefore( two ) ).to.be.false;
+			expect( three.isBefore( two ) ).to.be.false;
+
+			expect( one.isBefore( root ) ).to.be.false;
+		} );
+
+		it( 'should return false if the same element is given', () => {
+			expect( one.isBefore( one ) ).to.be.false;
+		} );
+
+		it( 'should return false if elements are in different roots', () => {
+			const otherRoot = new Element( 'root' );
+			const otherElement = new Element( 'element' );
+
+			otherRoot._appendChildren( otherElement );
+
+			expect( otherElement.isBefore( three ) ).to.be.false;
+		} );
+	} );
+
+	describe( 'isAfter()', () => {
+		// Model is: <root><one></one><two>ba<img></img>r</two><three></three>
+		it( 'should return true if the element is after given element', () => {
+			expect( two.isAfter( one ) ).to.be.true;
+			expect( img.isAfter( one ) ).to.be.true;
+
+			expect( textBA.isAfter( two ) ).to.be.true;
+			expect( textR.isAfter( two ) ).to.be.true;
+			expect( three.isAfter( two ) ).to.be.true;
+
+			expect( one.isAfter( root ) ).to.be.true;
+		} );
+
+		it( 'should return false if the element is before given element', () => {
+			expect( one.isAfter( two ) ).to.be.false;
+			expect( one.isAfter( img ) ).to.be.false;
+
+			expect( two.isAfter( textBA ) ).to.be.false;
+			expect( two.isAfter( textR ) ).to.be.false;
+			expect( two.isAfter( three ) ).to.be.false;
+
+			expect( root.isAfter( one ) ).to.be.false;
+		} );
+
+		it( 'should return false if the same element is given', () => {
+			expect( one.isAfter( one ) ).to.be.false;
+		} );
+
+		it( 'should return false if elements are in different roots', () => {
+			const otherRoot = new Element( 'root' );
+			const otherElement = new Element( 'element' );
+
+			otherRoot._appendChildren( otherElement );
+
+			expect( three.isAfter( otherElement ) ).to.be.false;
+		} );
+	} );
+
 	describe( 'attributes interface', () => {
 		const node = new Node( { foo: 'bar' } );
 

+ 2 - 1
packages/ckeditor5-engine/tests/model/text.js

@@ -40,7 +40,8 @@ describe( 'Text', () => {
 			text = new Text( 'bar' );
 		} );
 
-		it( 'should return true for text', () => {
+		it( 'should return true for node, text', () => {
+			expect( text.is( 'node' ) ).to.be.true;
 			expect( text.is( 'text' ) ).to.be.true;
 		} );
 

+ 1 - 0
packages/ckeditor5-engine/tests/model/textproxy.js

@@ -107,6 +107,7 @@ describe( 'TextProxy', () => {
 		} );
 
 		it( 'should return false for other accept values', () => {
+			expect( textProxy.is( 'node' ) ).to.be.false;
 			expect( textProxy.is( 'text' ) ).to.be.false;
 			expect( textProxy.is( 'element' ) ).to.be.false;
 			expect( textProxy.is( 'documentFragment' ) ).to.be.false;

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

@@ -4,7 +4,7 @@
  */
 
 import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin';
-import Selection from '../../../src/view/selection';
+import DocumentSelection from '../../../src/view/documentselection';
 
 /**
  * Creates {@link module:engine/view/document~Document view Document} mock.
@@ -15,7 +15,7 @@ export default function createDocumentMock() {
 	const doc = Object.create( ObservableMixin );
 	doc.set( 'isFocused', false );
 	doc.set( 'isReadOnly', false );
-	doc.selection = new Selection();
+	doc.selection = new DocumentSelection();
 
 	return doc;
 }

+ 1 - 0
packages/ckeditor5-engine/tests/view/documentfragment.js

@@ -83,6 +83,7 @@ describe( 'DocumentFragment', () => {
 		} );
 
 		it( 'should return false for other accept values', () => {
+			expect( frag.is( 'node' ) ).to.be.false;
 			expect( frag.is( 'text' ) ).to.be.false;
 			expect( frag.is( 'textProxy' ) ).to.be.false;
 			expect( frag.is( 'element' ) ).to.be.false;

文件差异内容过多而无法显示
+ 1124 - 0
packages/ckeditor5-engine/tests/view/documentselection.js


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

@@ -6,7 +6,7 @@
 /* globals document */
 
 import ViewElement from '../../../src/view/element';
-import ViewSelection from '../../../src/view/selection';
+import ViewDocumentSelection from '../../../src/view/documentselection';
 import DomConverter from '../../../src/view/domconverter';
 import ViewDocumentFragment from '../../../src/view/documentfragment';
 import { INLINE_FILLER } from '../../../src/view/filler';
@@ -269,7 +269,7 @@ describe( 'DomConverter', () => {
 		beforeEach( () => {
 			viewElement = new ViewElement();
 			domEl = document.createElement( 'div' );
-			selection = new ViewSelection( viewElement, 'in' );
+			selection = new ViewDocumentSelection( viewElement, 'in' );
 			converter.bindFakeSelection( domEl, selection );
 		} );
 
@@ -280,7 +280,7 @@ describe( 'DomConverter', () => {
 		} );
 
 		it( 'should keep a copy of selection', () => {
-			const selectionCopy = new ViewSelection( selection );
+			const selectionCopy = new ViewDocumentSelection( selection );
 
 			selection._setTo( new ViewElement(), 'in', { backward: true } );
 			const bindSelection = converter.fakeSelectionToView( domEl );

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

@@ -6,7 +6,7 @@
 /* globals document */
 
 import ViewElement from '../../../src/view/element';
-import ViewSelection from '../../../src/view/selection';
+import ViewDocumentSelection from '../../../src/view/documentselection';
 import DomConverter from '../../../src/view/domconverter';
 import ViewDocumentFragment from '../../../src/view/documentfragment';
 import { INLINE_FILLER, INLINE_FILLER_LENGTH, NBSP_FILLER } from '../../../src/view/filler';
@@ -882,7 +882,7 @@ describe( 'DomConverter', () => {
 			domContainer.innerHTML = 'fake selection container';
 			document.body.appendChild( domContainer );
 
-			const viewSelection = new ViewSelection( new ViewElement(), 'in' );
+			const viewSelection = new ViewDocumentSelection( new ViewElement(), 'in' );
 			converter.bindFakeSelection( domContainer, viewSelection );
 
 			const domRange = document.createRange();
@@ -903,7 +903,7 @@ describe( 'DomConverter', () => {
 			domContainer.innerHTML = 'fake selection container';
 			document.body.appendChild( domContainer );
 
-			const viewSelection = new ViewSelection( new ViewElement(), 'in' );
+			const viewSelection = new ViewDocumentSelection( new ViewElement(), 'in' );
 			converter.bindFakeSelection( domContainer, viewSelection );
 
 			const domRange = document.createRange();

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

@@ -89,7 +89,8 @@ describe( 'Element', () => {
 			el = new Element( 'p' );
 		} );
 
-		it( 'should return true for element, element with correct name and element name', () => {
+		it( 'should return true for node, element, element with correct name and element name', () => {
+			expect( el.is( 'node' ) ).to.be.true;
 			expect( el.is( 'element' ) ).to.be.true;
 			expect( el.is( 'element', 'p' ) ).to.be.true;
 			expect( el.is( 'p' ) ).to.be.true;

+ 99 - 0
packages/ckeditor5-engine/tests/view/node.js

@@ -5,6 +5,7 @@
 
 import Element from '../../src/view/element';
 import Text from '../../src/view/text';
+import Node from '../../src/view/node';
 import DocumentFragment from '../../src/view/documentfragment';
 import RootEditableElement from '../../src/view/rooteditableelement';
 import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
@@ -29,6 +30,14 @@ describe( 'Node', () => {
 		root = new Element( null, null, [ one, two, three ] );
 	} );
 
+	describe( 'is()', () => {
+		it( 'should return true for node', () => {
+			const node = new Node();
+
+			expect( node.is( 'node' ) ).to.be.true;
+		} );
+	} );
+
 	describe( 'getNextSibling/getPreviousSibling()', () => {
 		it( 'should return next sibling', () => {
 			expect( root.nextSibling ).to.be.null;
@@ -214,6 +223,20 @@ describe( 'Node', () => {
 		} );
 	} );
 
+	describe( 'getPath()', () => {
+		it( 'should return empty array is the element is the root', () => {
+			expect( root.getPath() ).to.deep.equal( [] );
+		} );
+
+		it( 'should return array with indices of given element and its ancestors starting from top-most one', () => {
+			expect( one.getPath() ).to.deep.equal( [ 0 ] );
+			expect( two.getPath() ).to.deep.equal( [ 1 ] );
+			expect( img.getPath() ).to.deep.equal( [ 1, 2 ] );
+			expect( charR.getPath() ).to.deep.equal( [ 1, 3 ] );
+			expect( three.getPath() ).to.deep.equal( [ 2 ] );
+		} );
+	} );
+
 	describe( 'getDocument()', () => {
 		it( 'should return null if any parent has not set Document', () => {
 			expect( charA.document ).to.be.null;
@@ -258,6 +281,82 @@ describe( 'Node', () => {
 		} );
 	} );
 
+	describe( 'isBefore()', () => {
+		// Model is: <root><one></one><two>ba<img></img>r</two><three></three>
+		it( 'should return true if the element is before given element', () => {
+			expect( one.isBefore( two ) ).to.be.true;
+			expect( one.isBefore( img ) ).to.be.true;
+
+			expect( two.isBefore( charB ) ).to.be.true;
+			expect( two.isBefore( charR ) ).to.be.true;
+			expect( two.isBefore( three ) ).to.be.true;
+
+			expect( root.isBefore( one ) ).to.be.true;
+		} );
+
+		it( 'should return false if the element is after given element', () => {
+			expect( two.isBefore( one ) ).to.be.false;
+			expect( img.isBefore( one ) ).to.be.false;
+
+			expect( charB.isBefore( two ) ).to.be.false;
+			expect( charR.isBefore( two ) ).to.be.false;
+			expect( three.isBefore( two ) ).to.be.false;
+
+			expect( one.isBefore( root ) ).to.be.false;
+		} );
+
+		it( 'should return false if the same element is given', () => {
+			expect( one.isBefore( one ) ).to.be.false;
+		} );
+
+		it( 'should return false if elements are in different roots', () => {
+			const otherRoot = new Element( 'root' );
+			const otherElement = new Element( 'element' );
+
+			otherRoot._appendChildren( otherElement );
+
+			expect( otherElement.isBefore( three ) ).to.be.false;
+		} );
+	} );
+
+	describe( 'isAfter()', () => {
+		// Model is: <root><one></one><two>ba<img></img>r</two><three></three>
+		it( 'should return true if the element is after given element', () => {
+			expect( two.isAfter( one ) ).to.be.true;
+			expect( img.isAfter( one ) ).to.be.true;
+
+			expect( charB.isAfter( two ) ).to.be.true;
+			expect( charR.isAfter( two ) ).to.be.true;
+			expect( three.isAfter( two ) ).to.be.true;
+
+			expect( one.isAfter( root ) ).to.be.true;
+		} );
+
+		it( 'should return false if the element is before given element', () => {
+			expect( one.isAfter( two ) ).to.be.false;
+			expect( one.isAfter( img ) ).to.be.false;
+
+			expect( two.isAfter( charB ) ).to.be.false;
+			expect( two.isAfter( charR ) ).to.be.false;
+			expect( two.isAfter( three ) ).to.be.false;
+
+			expect( root.isAfter( one ) ).to.be.false;
+		} );
+
+		it( 'should return false if the same element is given', () => {
+			expect( one.isAfter( one ) ).to.be.false;
+		} );
+
+		it( 'should return false if elements are in different roots', () => {
+			const otherRoot = new Element( 'root' );
+			const otherElement = new Element( 'element' );
+
+			otherRoot._appendChildren( otherElement );
+
+			expect( three.isAfter( otherElement ) ).to.be.false;
+		} );
+	} );
+
 	describe( '_remove()', () => {
 		it( 'should remove node from its parent', () => {
 			const char = new Text( 'a' );

+ 3 - 2
packages/ckeditor5-engine/tests/view/observer/selectionobserver.js

@@ -7,6 +7,7 @@
 
 import ViewRange from '../../../src/view/range';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import DocumentSelection from '../../../src/view/documentselection';
 import ViewSelection from '../../../src/view/selection';
 import View from '../../../src/view/view';
 import SelectionObserver from '../../../src/view/observer/selectionobserver';
@@ -64,7 +65,7 @@ describe( 'SelectionObserver', () => {
 		viewDocument.on( 'selectionChange', ( evt, data ) => {
 			expect( data ).to.have.property( 'domSelection' ).that.equals( domDocument.getSelection() );
 
-			expect( data ).to.have.property( 'oldSelection' ).that.is.instanceof( ViewSelection );
+			expect( data ).to.have.property( 'oldSelection' ).that.is.instanceof( DocumentSelection );
 			expect( data.oldSelection.rangeCount ).to.equal( 0 );
 
 			expect( data ).to.have.property( 'newSelection' ).that.is.instanceof( ViewSelection );
@@ -265,7 +266,7 @@ describe( 'SelectionObserver', () => {
 				expect( spy.calledOnce ).to.true;
 				expect( data ).to.have.property( 'domSelection' ).to.equal( domDocument.getSelection() );
 
-				expect( data ).to.have.property( 'oldSelection' ).to.instanceof( ViewSelection );
+				expect( data ).to.have.property( 'oldSelection' ).to.instanceof( DocumentSelection );
 				expect( data.oldSelection.rangeCount ).to.equal( 0 );
 
 				expect( data ).to.have.property( 'newSelection' ).to.instanceof( ViewSelection );

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

@@ -12,7 +12,7 @@ import ViewAttributeElement from '../../src/view/attributeelement';
 import ViewText from '../../src/view/text';
 import ViewRange from '../../src/view/range';
 import ViewPosition from '../../src/view/position';
-import Selection from '../../src/view/selection';
+import DocumentSelection from '../../src/view/documentselection';
 import DomConverter from '../../src/view/domconverter';
 import Renderer from '../../src/view/renderer';
 import DocumentFragment from '../../src/view/documentfragment';
@@ -30,7 +30,7 @@ describe( 'Renderer', () => {
 	let selection, domConverter, renderer;
 
 	beforeEach( () => {
-		selection = new Selection();
+		selection = new DocumentSelection();
 		domConverter = new DomConverter();
 		renderer = new Renderer( domConverter, selection );
 		renderer.domDocuments.add( document );

+ 114 - 101
packages/ckeditor5-engine/tests/view/selection.js

@@ -4,6 +4,7 @@
  */
 
 import Selection from '../../src/view/selection';
+import DocumentSelection from '../../src/view/documentselection';
 import Range from '../../src/view/range';
 import Document from '../../src/view/document';
 import Element from '../../src/view/element';
@@ -92,6 +93,14 @@ describe( 'Selection', () => {
 			expect( selection.isBackward ).to.be.true;
 		} );
 
+		it( 'should be able to create a selection from the other document selection', () => {
+			const otherSelection = new DocumentSelection( [ range2, range3 ], { backward: true } );
+			const selection = new Selection( otherSelection );
+
+			expect( Array.from( selection.getRanges() ) ).to.deep.equal( [ range2, range3 ] );
+			expect( selection.isBackward ).to.be.true;
+		} );
+
 		it( 'should be able to create a fake selection from the other fake selection', () => {
 			const otherSelection = new Selection( [ range2, range3 ], { fake: true, label: 'foo bar baz' } );
 			const selection = new Selection( otherSelection );
@@ -131,7 +140,7 @@ describe( 'Selection', () => {
 		} );
 
 		it( 'should return start of single range in selection', () => {
-			selection._setTo( range1 );
+			selection.setTo( range1 );
 			const anchor = selection.anchor;
 
 			expect( anchor.isEqual( range1.start ) ).to.be.true;
@@ -139,7 +148,7 @@ describe( 'Selection', () => {
 		} );
 
 		it( 'should return end of single range in selection when added as backward', () => {
-			selection._setTo( range1, { backward: true } );
+			selection.setTo( range1, { backward: true } );
 			const anchor = selection.anchor;
 
 			expect( anchor.isEqual( range1.end ) ).to.be.true;
@@ -147,7 +156,7 @@ describe( 'Selection', () => {
 		} );
 
 		it( 'should get anchor from last inserted range', () => {
-			selection._setTo( [ range1, range2 ] );
+			selection.setTo( [ range1, range2 ] );
 
 			expect( selection.anchor.isEqual( range2.start ) ).to.be.true;
 		} );
@@ -159,14 +168,14 @@ describe( 'Selection', () => {
 		} );
 
 		it( 'should return end of single range in selection', () => {
-			selection._setTo( range1 );
+			selection.setTo( range1 );
 			const focus = selection.focus;
 
 			expect( focus.isEqual( range1.end ) ).to.be.true;
 		} );
 
 		it( 'should return start of single range in selection when added as backward', () => {
-			selection._setTo( range1, { backward: true } );
+			selection.setTo( range1, { backward: true } );
 			const focus = selection.focus;
 
 			expect( focus.isEqual( range1.start ) ).to.be.true;
@@ -174,16 +183,16 @@ describe( 'Selection', () => {
 		} );
 
 		it( 'should get focus from last inserted range', () => {
-			selection._setTo( [ range1, range2 ] );
+			selection.setTo( [ range1, range2 ] );
 
 			expect( selection.focus.isEqual( range2.end ) ).to.be.true;
 		} );
 	} );
 
-	describe( '_setFocus()', () => {
+	describe( 'setFocus()', () => {
 		it( 'keeps all existing ranges when no modifications needed', () => {
-			selection._setTo( range1 );
-			selection._setFocus( selection.focus );
+			selection.setTo( range1 );
+			selection.setFocus( selection.focus );
 
 			expect( count( selection.getRanges() ) ).to.equal( 1 );
 		} );
@@ -192,7 +201,7 @@ describe( 'Selection', () => {
 			const endPos = Position.createAt( el, 'end' );
 
 			expect( () => {
-				selection._setFocus( endPos );
+				selection.setFocus( endPos );
 			} ).to.throw( CKEditorError, /view-selection-setFocus-no-ranges/ );
 		} );
 
@@ -200,9 +209,9 @@ describe( 'Selection', () => {
 			const startPos = Position.createAt( el, 1 );
 			const endPos = Position.createAt( el, 2 );
 
-			selection._setTo( startPos );
+			selection.setTo( startPos );
 
-			selection._setFocus( endPos );
+			selection.setFocus( endPos );
 
 			expect( selection.anchor.compareWith( startPos ) ).to.equal( 'same' );
 			expect( selection.focus.compareWith( endPos ) ).to.equal( 'same' );
@@ -212,9 +221,9 @@ describe( 'Selection', () => {
 			const startPos = Position.createAt( el, 1 );
 			const endPos = Position.createAt( el, 0 );
 
-			selection._setTo( startPos );
+			selection.setTo( startPos );
 
-			selection._setFocus( endPos );
+			selection.setFocus( endPos );
 
 			expect( selection.anchor.compareWith( startPos ) ).to.equal( 'same' );
 			expect( selection.focus.compareWith( endPos ) ).to.equal( 'same' );
@@ -226,9 +235,9 @@ describe( 'Selection', () => {
 			const endPos = Position.createAt( el, 2 );
 			const newEndPos = Position.createAt( el, 3 );
 
-			selection._setTo( new Range( startPos, endPos ) );
+			selection.setTo( new Range( startPos, endPos ) );
 
-			selection._setFocus( newEndPos );
+			selection.setFocus( newEndPos );
 
 			expect( selection.anchor.compareWith( startPos ) ).to.equal( 'same' );
 			expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'same' );
@@ -239,9 +248,9 @@ describe( 'Selection', () => {
 			const endPos = Position.createAt( el, 2 );
 			const newEndPos = Position.createAt( el, 0 );
 
-			selection._setTo( new Range( startPos, endPos ) );
+			selection.setTo( new Range( startPos, endPos ) );
 
-			selection._setFocus( newEndPos );
+			selection.setFocus( newEndPos );
 
 			expect( selection.anchor.compareWith( startPos ) ).to.equal( 'same' );
 			expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'same' );
@@ -253,9 +262,9 @@ describe( 'Selection', () => {
 			const endPos = Position.createAt( el, 2 );
 			const newEndPos = Position.createAt( el, 3 );
 
-			selection._setTo( new Range( startPos, endPos ), { backward: true } );
+			selection.setTo( new Range( startPos, endPos ), { backward: true } );
 
-			selection._setFocus( newEndPos );
+			selection.setFocus( newEndPos );
 
 			expect( selection.anchor.compareWith( endPos ) ).to.equal( 'same' );
 			expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'same' );
@@ -267,9 +276,9 @@ describe( 'Selection', () => {
 			const endPos = Position.createAt( el, 2 );
 			const newEndPos = Position.createAt( el, 0 );
 
-			selection._setTo( new Range( startPos, endPos ), { backward: true } );
+			selection.setTo( new Range( startPos, endPos ), { backward: true } );
 
-			selection._setFocus( newEndPos );
+			selection.setFocus( newEndPos );
 
 			expect( selection.anchor.compareWith( endPos ) ).to.equal( 'same' );
 			expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'same' );
@@ -285,12 +294,12 @@ describe( 'Selection', () => {
 
 			const newEndPos = Position.createAt( el, 0 );
 
-			selection._setTo( [
+			selection.setTo( [
 				new Range( startPos1, endPos1 ),
 				new Range( startPos2, endPos2 )
 			] );
 
-			selection._setFocus( newEndPos );
+			selection.setFocus( newEndPos );
 
 			const ranges = Array.from( selection.getRanges() );
 
@@ -307,9 +316,9 @@ describe( 'Selection', () => {
 			const startPos = Position.createAt( el, 1 );
 			const endPos = Position.createAt( el, 2 );
 
-			selection._setTo( new Range( startPos, endPos ) );
+			selection.setTo( new Range( startPos, endPos ) );
 
-			selection._setFocus( startPos );
+			selection.setFocus( startPos );
 
 			expect( selection.focus.compareWith( startPos ) ).to.equal( 'same' );
 			expect( selection.isCollapsed ).to.be.true;
@@ -322,8 +331,8 @@ describe( 'Selection', () => {
 
 			const spy = sinon.stub( Position, 'createAt' ).returns( newEndPos );
 
-			selection._setTo( new Range( startPos, endPos ) );
-			selection._setFocus( el, 'end' );
+			selection.setTo( new Range( startPos, endPos ) );
+			selection.setFocus( el, 'end' );
 
 			expect( spy.calledOnce ).to.be.true;
 			expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'same' );
@@ -335,7 +344,7 @@ describe( 'Selection', () => {
 	describe( 'isCollapsed', () => {
 		it( 'should return true when there is single collapsed range', () => {
 			const range = Range.createFromParentsAndOffsets( el, 5, el, 5 );
-			selection._setTo( range );
+			selection.setTo( range );
 
 			expect( selection.isCollapsed ).to.be.true;
 		} );
@@ -343,14 +352,14 @@ describe( 'Selection', () => {
 		it( 'should return false when there are multiple ranges', () => {
 			const range1 = Range.createFromParentsAndOffsets( el, 5, el, 5 );
 			const range2 = Range.createFromParentsAndOffsets( el, 15, el, 15 );
-			selection._setTo( [ range1, range2 ] );
+			selection.setTo( [ range1, range2 ] );
 
 			expect( selection.isCollapsed ).to.be.false;
 		} );
 
 		it( 'should return false when there is not collapsed range', () => {
 			const range = Range.createFromParentsAndOffsets( el, 15, el, 16 );
-			selection._setTo( range );
+			selection.setTo( range );
 
 			expect( selection.isCollapsed ).to.be.false;
 		} );
@@ -360,11 +369,11 @@ describe( 'Selection', () => {
 		it( 'should return proper range count', () => {
 			expect( selection.rangeCount ).to.equal( 0 );
 
-			selection._setTo( range1 );
+			selection.setTo( range1 );
 
 			expect( selection.rangeCount ).to.equal( 1 );
 
-			selection._setTo( [ range1, range2 ] );
+			selection.setTo( [ range1, range2 ] );
 
 			expect( selection.rangeCount ).to.equal( 2 );
 		} );
@@ -375,17 +384,17 @@ describe( 'Selection', () => {
 			const range1 = Range.createFromParentsAndOffsets( el, 5, el, 10 );
 			const range2 = Range.createFromParentsAndOffsets( el, 15, el, 16 );
 
-			selection._setTo( range1, { backward: true } );
+			selection.setTo( range1, { backward: true } );
 			expect( selection ).to.have.property( 'isBackward', true );
 
-			selection._setTo( [ range1, range2 ] );
+			selection.setTo( [ range1, range2 ] );
 			expect( selection ).to.have.property( 'isBackward', false );
 		} );
 
 		it( 'is false when last range is collapsed', () => {
 			const range = Range.createFromParentsAndOffsets( el, 5, el, 5 );
 
-			selection._setTo( range, { backward: true } );
+			selection.setTo( range, { backward: true } );
 
 			expect( selection.isBackward ).to.be.false;
 		} );
@@ -393,7 +402,7 @@ describe( 'Selection', () => {
 
 	describe( 'getRanges', () => {
 		it( 'should return iterator with copies of all ranges', () => {
-			selection._setTo( [ range1, range2 ] );
+			selection.setTo( [ range1, range2 ] );
 
 			const iterable = selection.getRanges();
 			const ranges = Array.from( iterable );
@@ -408,7 +417,7 @@ describe( 'Selection', () => {
 
 	describe( 'getFirstRange', () => {
 		it( 'should return copy of range with first position', () => {
-			selection._setTo( [ range1, range2, range3 ] );
+			selection.setTo( [ range1, range2, range3 ] );
 
 			const range = selection.getFirstRange();
 
@@ -423,7 +432,7 @@ describe( 'Selection', () => {
 
 	describe( 'getLastRange', () => {
 		it( 'should return copy of range with last position', () => {
-			selection._setTo( [ range1, range2, range3 ] );
+			selection.setTo( [ range1, range2, range3 ] );
 
 			const range = selection.getLastRange();
 
@@ -438,7 +447,7 @@ describe( 'Selection', () => {
 
 	describe( 'getFirstPosition', () => {
 		it( 'should return copy of first position', () => {
-			selection._setTo( [ range1, range2, range3 ] );
+			selection.setTo( [ range1, range2, range3 ] );
 
 			const position = selection.getFirstPosition();
 
@@ -453,7 +462,7 @@ describe( 'Selection', () => {
 
 	describe( 'getLastPosition', () => {
 		it( 'should return copy of range with last position', () => {
-			selection._setTo( [ range1, range2, range3 ] );
+			selection.setTo( [ range1, range2, range3 ] );
 
 			const position = selection.getLastPosition();
 
@@ -468,16 +477,16 @@ describe( 'Selection', () => {
 
 	describe( 'isEqual', () => {
 		it( 'should return true if selections equal', () => {
-			selection._setTo( [ range1, range2 ] );
+			selection.setTo( [ range1, range2 ] );
 
 			const otherSelection = new Selection();
-			otherSelection._setTo( [ range1, range2 ] );
+			otherSelection.setTo( [ range1, range2 ] );
 
 			expect( selection.isEqual( otherSelection ) ).to.be.true;
 		} );
 
 		it( 'should return true if backward selections equal', () => {
-			selection._setTo( range1, { backward: true } );
+			selection.setTo( range1, { backward: true } );
 
 			const otherSelection = new Selection( [ range1 ], { backward: true } );
 
@@ -485,7 +494,7 @@ describe( 'Selection', () => {
 		} );
 
 		it( 'should return false if ranges count does not equal', () => {
-			selection._setTo( [ range1, range2 ] );
+			selection.setTo( [ range1, range2 ] );
 
 			const otherSelection = new Selection( [ range1 ] );
 
@@ -493,7 +502,7 @@ describe( 'Selection', () => {
 		} );
 
 		it( 'should return false if ranges (other than the last added one) do not equal', () => {
-			selection._setTo( [ range1, range3 ] );
+			selection.setTo( [ range1, range3 ] );
 
 			const otherSelection = new Selection( [ range2, range3 ] );
 
@@ -501,7 +510,7 @@ describe( 'Selection', () => {
 		} );
 
 		it( 'should return false if directions do not equal', () => {
-			selection._setTo( range1 );
+			selection.setTo( range1 );
 
 			const otherSelection = new Selection( [ range1 ], { backward: true } );
 
@@ -516,14 +525,14 @@ describe( 'Selection', () => {
 
 		it( 'should return true if both selection are fake', () => {
 			const otherSelection = new Selection( range1, { fake: true } );
-			selection._setTo( range1, { fake: true } );
+			selection.setTo( range1, { fake: true } );
 
 			expect( selection.isEqual( otherSelection ) ).to.be.true;
 		} );
 
 		it( 'should return false if both selection are fake but have different label', () => {
 			const otherSelection = new Selection( [ range1 ], { fake: true, label: 'foo bar baz' } );
-			selection._setTo( range1, { fake: true, label: 'foo' } );
+			selection.setTo( range1, { fake: true, label: 'foo' } );
 
 			expect( selection.isEqual( otherSelection ) ).to.be.false;
 		} );
@@ -537,7 +546,7 @@ describe( 'Selection', () => {
 
 	describe( 'isSimilar', () => {
 		it( 'should return true if selections equal', () => {
-			selection._setTo( [ range1, range2 ] );
+			selection.setTo( [ range1, range2 ] );
 
 			const otherSelection = new Selection( [ range1, range2 ] );
 
@@ -545,7 +554,7 @@ describe( 'Selection', () => {
 		} );
 
 		it( 'should return false if ranges count does not equal', () => {
-			selection._setTo( [ range1, range2 ] );
+			selection.setTo( [ range1, range2 ] );
 
 			const otherSelection = new Selection( [ range1 ] );
 
@@ -553,7 +562,7 @@ describe( 'Selection', () => {
 		} );
 
 		it( 'should return false if trimmed ranges (other than the last added one) are not equal', () => {
-			selection._setTo( [ range1, range3 ] );
+			selection.setTo( [ range1, range3 ] );
 
 			const otherSelection = new Selection( [ range2, range3 ] );
 
@@ -561,7 +570,7 @@ describe( 'Selection', () => {
 		} );
 
 		it( 'should return false if directions are not equal', () => {
-			selection._setTo( range1 );
+			selection.setTo( range1 );
 
 			const otherSelection = new Selection( [ range1 ], { backward: true } );
 
@@ -591,7 +600,7 @@ describe( 'Selection', () => {
 			const rangeA2 = Range.createFromParentsAndOffsets( p2, 0, p2, 1 );
 			const rangeB2 = Range.createFromParentsAndOffsets( span2, 0, span2, 1 );
 
-			selection._setTo( [ rangeA1, rangeA2 ] );
+			selection.setTo( [ rangeA1, rangeA2 ] );
 
 			const otherSelection = new Selection( [ rangeB2, rangeB1 ] );
 
@@ -603,14 +612,14 @@ describe( 'Selection', () => {
 		} );
 	} );
 
-	describe( '_setTo()', () => {
+	describe( 'setTo()', () => {
 		describe( 'simple scenarios', () => {
 			it( 'should set selection ranges from the given selection', () => {
-				selection._setTo( range1 );
+				selection.setTo( range1 );
 
 				const otherSelection = new Selection( [ range2, range3 ], { backward: true } );
 
-				selection._setTo( otherSelection );
+				selection.setTo( otherSelection );
 
 				expect( selection.rangeCount ).to.equal( 2 );
 				expect( selection._ranges[ 0 ].isEqual( range2 ) ).to.be.true;
@@ -622,21 +631,21 @@ describe( 'Selection', () => {
 			} );
 
 			it( 'should set selection on the given Range', () => {
-				selection._setTo( range1 );
+				selection.setTo( range1 );
 
 				expect( Array.from( selection.getRanges() ) ).to.deep.equal( [ range1 ] );
 				expect( selection.isBackward ).to.be.false;
 			} );
 
 			it( 'should set selection on the given iterable of Ranges', () => {
-				selection._setTo( new Set( [ range1, range2 ] ) );
+				selection.setTo( new Set( [ range1, range2 ] ) );
 
 				expect( Array.from( selection.getRanges() ) ).to.deep.equal( [ range1, range2 ] );
 				expect( selection.isBackward ).to.be.false;
 			} );
 
 			it( 'should set collapsed selection on the given Position', () => {
-				selection._setTo( range1.start );
+				selection.setTo( range1.start );
 
 				expect( Array.from( selection.getRanges() ).length ).to.equal( 1 );
 				expect( Array.from( selection.getRanges() )[ 0 ].start ).to.deep.equal( range1.start );
@@ -653,13 +662,13 @@ describe( 'Selection', () => {
 
 				const otherSelection = new Selection( [ range1 ] );
 
-				selection._setTo( otherSelection );
+				selection.setTo( otherSelection );
 			} );
 
 			it( 'should set fake state and label', () => {
 				const label = 'foo bar baz';
 				const otherSelection = new Selection( null, { fake: true, label } );
-				selection._setTo( otherSelection );
+				selection.setTo( otherSelection );
 
 				expect( selection.isFake ).to.be.true;
 				expect( selection.fakeSelectionLabel ).to.equal( label );
@@ -669,7 +678,7 @@ describe( 'Selection', () => {
 				const otherSelection = new Selection();
 
 				expect( () => {
-					otherSelection._setTo( {} );
+					otherSelection.setTo( {} );
 				} ).to.throw( /view-selection-setTo-not-selectable/ );
 			} );
 
@@ -677,20 +686,20 @@ describe( 'Selection', () => {
 				const otherSelection = new Selection();
 
 				expect( () => {
-					otherSelection._setTo();
+					otherSelection.setTo();
 				} ).to.throw( /view-selection-setTo-not-selectable/ );
 			} );
 		} );
 
 		describe( 'setting collapsed selection', () => {
 			beforeEach( () => {
-				selection._setTo( [ range1, range2 ] );
+				selection.setTo( [ range1, range2 ] );
 			} );
 
 			it( 'should collapse selection at position', () => {
 				const position = new Position( el, 4 );
 
-				selection._setTo( position );
+				selection.setTo( position );
 				const range = selection.getFirstRange();
 
 				expect( range.start.parent ).to.equal( el );
@@ -702,14 +711,14 @@ describe( 'Selection', () => {
 				const foo = new Text( 'foo' );
 				const p = new Element( 'p', null, foo );
 
-				selection._setTo( foo, 0 );
+				selection.setTo( foo, 0 );
 				let range = selection.getFirstRange();
 
 				expect( range.start.parent ).to.equal( foo );
 				expect( range.start.offset ).to.equal( 0 );
 				expect( range.start.isEqual( range.end ) ).to.be.true;
 
-				selection._setTo( p, 1 );
+				selection.setTo( p, 1 );
 				range = selection.getFirstRange();
 
 				expect( range.start.parent ).to.equal( p );
@@ -721,7 +730,7 @@ describe( 'Selection', () => {
 				const foo = new Text( 'foo' );
 
 				expect( () => {
-					selection._setTo( foo );
+					selection.setTo( foo );
 				} ).to.throw( CKEditorError, /view-selection-setTo-required-second-parameter/ );
 			} );
 
@@ -729,21 +738,21 @@ describe( 'Selection', () => {
 				const foo = new Text( 'foo' );
 				const p = new Element( 'p', null, foo );
 
-				selection._setTo( foo, 'end' );
+				selection.setTo( foo, 'end' );
 				let range = selection.getFirstRange();
 
 				expect( range.start.parent ).to.equal( foo );
 				expect( range.start.offset ).to.equal( 3 );
 				expect( range.start.isEqual( range.end ) ).to.be.true;
 
-				selection._setTo( foo, 'before' );
+				selection.setTo( foo, 'before' );
 				range = selection.getFirstRange();
 
 				expect( range.start.parent ).to.equal( p );
 				expect( range.start.offset ).to.equal( 0 );
 				expect( range.start.isEqual( range.end ) ).to.be.true;
 
-				selection._setTo( foo, 'after' );
+				selection.setTo( foo, 'after' );
 				range = selection.getFirstRange();
 
 				expect( range.start.parent ).to.equal( p );
@@ -754,7 +763,7 @@ describe( 'Selection', () => {
 
 		describe( 'setting collapsed selection at start', () => {
 			it( 'should collapse to start position and fire change event', done => {
-				selection._setTo( [ range1, range2, range3 ] );
+				selection.setTo( [ range1, range2, range3 ] );
 				selection.once( 'change', () => {
 					expect( selection.rangeCount ).to.equal( 1 );
 					expect( selection.isCollapsed ).to.be.true;
@@ -762,13 +771,13 @@ describe( 'Selection', () => {
 					done();
 				} );
 
-				selection._setTo( selection.getFirstPosition() );
+				selection.setTo( selection.getFirstPosition() );
 			} );
 		} );
 
 		describe( 'setting collapsed selection to end', () => {
 			it( 'should collapse to end position and fire change event', done => {
-				selection._setTo( [ range1, range2, range3 ] );
+				selection.setTo( [ range1, range2, range3 ] );
 				selection.once( 'change', () => {
 					expect( selection.rangeCount ).to.equal( 1 );
 					expect( selection.isCollapsed ).to.be.true;
@@ -776,48 +785,48 @@ describe( 'Selection', () => {
 					done();
 				} );
 
-				selection._setTo( selection.getLastPosition() );
+				selection.setTo( selection.getLastPosition() );
 			} );
 		} );
 
 		describe( 'removing all ranges', () => {
 			it( 'should remove all ranges and fire change event', done => {
-				selection._setTo( [ range1, range2 ] );
+				selection.setTo( [ range1, range2 ] );
 
 				selection.once( 'change', () => {
 					expect( selection.rangeCount ).to.equal( 0 );
 					done();
 				} );
 
-				selection._setTo( null );
+				selection.setTo( null );
 			} );
 		} );
 
 		describe( 'setting fake selection', () => {
 			it( 'should allow to set selection to fake', () => {
-				selection._setTo( range1, { fake: true } );
+				selection.setTo( range1, { fake: true } );
 
 				expect( selection.isFake ).to.be.true;
 			} );
 
 			it( 'should allow to set fake selection label', () => {
 				const label = 'foo bar baz';
-				selection._setTo( range1, { fake: true, label } );
+				selection.setTo( range1, { fake: true, label } );
 
 				expect( selection.fakeSelectionLabel ).to.equal( label );
 			} );
 
 			it( 'should not set label when set to false', () => {
 				const label = 'foo bar baz';
-				selection._setTo( range1, { fake: false, label } );
+				selection.setTo( range1, { fake: false, label } );
 
 				expect( selection.fakeSelectionLabel ).to.equal( '' );
 			} );
 
 			it( 'should reset label when set to false', () => {
 				const label = 'foo bar baz';
-				selection._setTo( range1, { fake: true, label } );
-				selection._setTo( range1 );
+				selection.setTo( range1, { fake: true, label } );
+				selection.setTo( range1 );
 
 				expect( selection.fakeSelectionLabel ).to.equal( '' );
 			} );
@@ -830,11 +839,11 @@ describe( 'Selection', () => {
 					done();
 				} );
 
-				selection._setTo( range1, { fake: true, label: 'foo bar baz' } );
+				selection.setTo( range1, { fake: true, label: 'foo bar baz' } );
 			} );
 
 			it( 'should be possible to create an empty fake selection', () => {
-				selection._setTo( null, { fake: true, label: 'foo bar baz' } );
+				selection.setTo( null, { fake: true, label: 'foo bar baz' } );
 
 				expect( selection.fakeSelectionLabel ).to.equal( 'foo bar baz' );
 				expect( selection.isFake ).to.be.true;
@@ -843,8 +852,8 @@ describe( 'Selection', () => {
 
 		describe( 'setting selection to itself', () => {
 			it( 'should correctly set ranges when setting to the same selection', () => {
-				selection._setTo( [ range1, range2 ] );
-				selection._setTo( selection );
+				selection.setTo( [ range1, range2 ] );
+				selection.setTo( selection );
 
 				const ranges = Array.from( selection.getRanges() );
 				expect( ranges.length ).to.equal( 2 );
@@ -854,8 +863,8 @@ describe( 'Selection', () => {
 			} );
 
 			it( 'should correctly set ranges when setting to the same selection\'s ranges', () => {
-				selection._setTo( [ range1, range2 ] );
-				selection._setTo( selection.getRanges() );
+				selection.setTo( [ range1, range2 ] );
+				selection.setTo( selection.getRanges() );
 
 				const ranges = Array.from( selection.getRanges() );
 				expect( ranges.length ).to.equal( 2 );
@@ -868,7 +877,7 @@ describe( 'Selection', () => {
 		describe( 'throwing errors', () => {
 			it( 'should throw an error when range is invalid', () => {
 				expect( () => {
-					selection._setTo( [ { invalid: 'range' } ] );
+					selection.setTo( [ { invalid: 'range' } ] );
 				} ).to.throw( CKEditorError, 'view-selection-invalid-range: Invalid Range.' );
 			} );
 
@@ -877,7 +886,7 @@ describe( 'Selection', () => {
 				const range2 = Range.createFromParentsAndOffsets( text, 7, text, 15 );
 
 				expect( () => {
-					selection._setTo( [ range1, range2 ] );
+					selection.setTo( [ range1, range2 ] );
 				} ).to.throw( CKEditorError, 'view-selection-range-intersects' );
 			} );
 		} );
@@ -888,7 +897,7 @@ describe( 'Selection', () => {
 			const textNode3 = new Text( 'baz' );
 			const element = new Element( 'p', null, [ textNode1, textNode2, textNode3 ] );
 
-			selection._setTo( textNode2, 'on' );
+			selection.setTo( textNode2, 'on' );
 
 			const ranges = Array.from( selection.getRanges() );
 			expect( ranges.length ).to.equal( 1 );
@@ -901,7 +910,7 @@ describe( 'Selection', () => {
 		it( 'should allow setting selection inside an element', () => {
 			const element = new Element( 'p', null, [ new Text( 'foo' ), new Text( 'bar' ) ] );
 
-			selection._setTo( element, 'in' );
+			selection.setTo( element, 'in' );
 
 			const ranges = Array.from( selection.getRanges() );
 			expect( ranges.length ).to.equal( 1 );
@@ -914,7 +923,7 @@ describe( 'Selection', () => {
 		it( 'should allow setting backward selection inside an element', () => {
 			const element = new Element( 'p', null, [ new Text( 'foo' ), new Text( 'bar' ) ] );
 
-			selection._setTo( element, 'in', { backward: true } );
+			selection.setTo( element, 'in', { backward: true } );
 
 			const ranges = Array.from( selection.getRanges() );
 			expect( ranges.length ).to.equal( 1 );
@@ -932,19 +941,19 @@ describe( 'Selection', () => {
 		} );
 
 		it( 'should return null if selection is placed in container that is not EditableElement', () => {
-			selection._setTo( range1 );
+			selection.setTo( range1 );
 
 			expect( selection.editableElement ).to.be.null;
 		} );
 
 		it( 'should return EditableElement when selection is placed inside', () => {
 			const viewDocument = new Document();
-			const selection = viewDocument.selection;
+			selection.setTo( viewDocument.selection );
 			const root = createViewRoot( viewDocument, 'div', 'main' );
 			const element = new Element( 'p' );
 			root._appendChildren( element );
 
-			selection._setTo( Range.createFromParentsAndOffsets( element, 0, element, 0 ) );
+			selection.setTo( Range.createFromParentsAndOffsets( element, 0, element, 0 ) );
 
 			expect( selection.editableElement ).to.equal( root );
 		} );
@@ -958,14 +967,16 @@ describe( 'Selection', () => {
 
 	describe( 'getSelectedElement()', () => {
 		it( 'should return selected element', () => {
-			const { selection, view } = parse( 'foo [<b>bar</b>] baz' );
+			const { selection: docSelection, view } = parse( 'foo [<b>bar</b>] baz' );
 			const b = view.getChild( 1 );
+			const selection = new Selection( docSelection );
 
 			expect( selection.getSelectedElement() ).to.equal( b );
 		} );
 
 		it( 'should return null if there is more than one range', () => {
-			const { selection } = parse( 'foo [<b>bar</b>] [<i>baz</i>]' );
+			const { selection: docSelection } = parse( 'foo [<b>bar</b>] [<i>baz</i>]' );
+			const selection = new Selection( docSelection );
 
 			expect( selection.getSelectedElement() ).to.be.null;
 		} );
@@ -975,13 +986,15 @@ describe( 'Selection', () => {
 		} );
 
 		it( 'should return null if selection is not over single element #1', () => {
-			const { selection } = parse( 'foo [<b>bar</b> ba}z' );
+			const { selection: docSelection } = parse( 'foo [<b>bar</b> ba}z' );
+			const selection = new Selection( docSelection );
 
 			expect( selection.getSelectedElement() ).to.be.null;
 		} );
 
 		it( 'should return null if selection is not over single element #2', () => {
-			const { selection } = parse( 'foo <b>{bar}</b> baz' );
+			const { selection: docSelection } = parse( 'foo <b>{bar}</b> baz' );
+			const selection = new Selection( docSelection );
 
 			expect( selection.getSelectedElement() ).to.be.null;
 		} );

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

@@ -24,7 +24,8 @@ describe( 'Text', () => {
 			text = new Text( 'foo' );
 		} );
 
-		it( 'should return true for text', () => {
+		it( 'should return true for node, text', () => {
+			expect( text.is( 'node' ) ).to.be.true;
 			expect( text.is( 'text' ) ).to.be.true;
 		} );
 

+ 1 - 0
packages/ckeditor5-engine/tests/view/textproxy.js

@@ -67,6 +67,7 @@ describe( 'TextProxy', () => {
 		} );
 
 		it( 'should return false for other accept values', () => {
+			expect( textProxy.is( 'node' ) ).to.be.false;
 			expect( textProxy.is( 'text' ) ).to.be.false;
 			expect( textProxy.is( 'element' ) ).to.be.false;
 			expect( textProxy.is( 'containerElement' ) ).to.be.false;