Explorar o código

Merge pull request #1357 from ckeditor/t/1304

Other: View selection is now split onto Selection and DocumentSelection. Closes #1304 .

BREAKING CHANGE: Introduced view.DocumentSelection. It has protected API and can be modified only by view writer. Observers creating instance of selection (like SelectionObserver, MutationObserver) use view.Selection class now.
Piotr Jasiun %!s(int64=7) %!d(string=hai) anos
pai
achega
a973343b63

+ 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>`).

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

+ 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.

+ 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 ) {
@@ -529,42 +541,17 @@ export default class Selection {
 		this.fire( 'change' );
 	}
 
-	/**
-	 * 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.
@@ -595,6 +582,30 @@ export default class Selection {
 		this.fire( 'change' );
 	}
 
+	/**
+	 * 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
@@ -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
- */

+ 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.
 	 *

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

+ 1124 - 0
packages/ckeditor5-engine/tests/view/documentselection.js

@@ -0,0 +1,1124 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import DocumentSelection from '../../src/view/documentselection';
+import Selection from '../../src/view/selection';
+import Range from '../../src/view/range';
+import Document from '../../src/view/document';
+import Element from '../../src/view/element';
+import Text from '../../src/view/text';
+import Position from '../../src/view/position';
+import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
+import count from '@ckeditor/ckeditor5-utils/src/count';
+import createViewRoot from './_utils/createroot';
+import { parse } from '../../src/dev-utils/view';
+
+describe( 'DocumentSelection', () => {
+	let documentSelection, el, range1, range2, range3;
+
+	beforeEach( () => {
+		const text = new Text( 'xxxxxxxxxxxxxxxxxxxx' );
+		el = new Element( 'p', null, text );
+
+		documentSelection = new DocumentSelection();
+
+		range1 = Range.createFromParentsAndOffsets( text, 5, text, 10 );
+		range2 = Range.createFromParentsAndOffsets( text, 1, text, 2 );
+		range3 = Range.createFromParentsAndOffsets( text, 12, text, 14 );
+	} );
+
+	describe( 'constructor()', () => {
+		it( 'should be able to create an empty selection', () => {
+			const selection = new DocumentSelection();
+
+			expect( Array.from( selection.getRanges() ) ).to.deep.equal( [] );
+		} );
+
+		it( 'should be able to create a selection from the given ranges', () => {
+			const ranges = [ range1, range2, range3 ];
+			const selection = new DocumentSelection( ranges );
+
+			expect( Array.from( selection.getRanges() ) ).to.deep.equal( ranges );
+		} );
+
+		it( 'should be able to create a selection from the given ranges and isLastBackward flag', () => {
+			const ranges = [ range1, range2, range3 ];
+			const selection = new DocumentSelection( ranges, { backward: true } );
+
+			expect( selection.isBackward ).to.be.true;
+		} );
+
+		it( 'should be able to create a selection from the given range and isLastBackward flag', () => {
+			const selection = new DocumentSelection( range1, { backward: true } );
+
+			expect( Array.from( selection.getRanges() ) ).to.deep.equal( [ range1 ] );
+			expect( selection.isBackward ).to.be.true;
+		} );
+
+		it( 'should be able to create a selection from the given iterable of ranges and isLastBackward flag', () => {
+			const ranges = new Set( [ range1, range2, range3 ] );
+			const selection = new DocumentSelection( ranges, { backward: false } );
+
+			expect( Array.from( selection.getRanges() ) ).to.deep.equal( [ range1, range2, range3 ] );
+			expect( selection.isBackward ).to.be.false;
+		} );
+
+		it( 'should be able to create a collapsed selection at the given position', () => {
+			const position = range1.start;
+			const selection = new DocumentSelection( position );
+
+			expect( Array.from( selection.getRanges() ).length ).to.equal( 1 );
+			expect( selection.getFirstRange().start ).to.deep.equal( position );
+			expect( selection.getFirstRange().end ).to.deep.equal( position );
+			expect( selection.isBackward ).to.be.false;
+		} );
+
+		it( 'should be able to create a collapsed selection at the given position', () => {
+			const position = range1.start;
+			const selection = new DocumentSelection( position );
+
+			expect( Array.from( selection.getRanges() ).length ).to.equal( 1 );
+			expect( selection.getFirstRange().start ).to.deep.equal( position );
+			expect( selection.getFirstRange().end ).to.deep.equal( position );
+			expect( selection.isBackward ).to.be.false;
+		} );
+
+		it( 'should be able to create a selection from the other document selection', () => {
+			const otherSelection = new DocumentSelection( [ range2, range3 ], { backward: true } );
+			const selection = new DocumentSelection( otherSelection );
+
+			expect( Array.from( selection.getRanges() ) ).to.deep.equal( [ range2, range3 ] );
+			expect( selection.isBackward ).to.be.true;
+		} );
+
+		it( 'should be able to create a selection from the other selection', () => {
+			const otherSelection = new Selection( [ range2, range3 ], { backward: true } );
+			const selection = new DocumentSelection( 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 DocumentSelection( [ range2, range3 ], { fake: true, label: 'foo bar baz' } );
+			const selection = new DocumentSelection( otherSelection );
+
+			expect( selection.isFake ).to.be.true;
+			expect( selection.fakeSelectionLabel ).to.equal( 'foo bar baz' );
+		} );
+
+		it( 'should throw an error when range is invalid', () => {
+			expect( () => {
+				// eslint-disable-next-line no-new
+				new DocumentSelection( [ { invalid: 'range' } ] );
+			} ).to.throw( CKEditorError, 'view-selection-invalid-range: Invalid Range.' );
+		} );
+
+		it( 'should throw an error when ranges intersects', () => {
+			const text = el.getChild( 0 );
+			const range2 = Range.createFromParentsAndOffsets( text, 7, text, 15 );
+
+			expect( () => {
+				// eslint-disable-next-line no-new
+				new DocumentSelection( [ range1, range2 ] );
+			} ).to.throw( CKEditorError, 'view-selection-range-intersects' );
+		} );
+
+		it( 'should throw an error when trying to set to not selectable', () => {
+			expect( () => {
+				// eslint-disable-next-line no-new
+				new DocumentSelection( {} );
+			} ).to.throw( /view-selection-setTo-not-selectable/ );
+		} );
+	} );
+
+	describe( 'anchor', () => {
+		it( 'should return null if no ranges in selection', () => {
+			expect( documentSelection.anchor ).to.be.null;
+		} );
+
+		it( 'should return start of single range in selection', () => {
+			documentSelection._setTo( range1 );
+			const anchor = documentSelection.anchor;
+
+			expect( anchor.isEqual( range1.start ) ).to.be.true;
+			expect( anchor ).to.not.equal( range1.start );
+		} );
+
+		it( 'should return end of single range in selection when added as backward', () => {
+			documentSelection._setTo( range1, { backward: true } );
+			const anchor = documentSelection.anchor;
+
+			expect( anchor.isEqual( range1.end ) ).to.be.true;
+			expect( anchor ).to.not.equal( range1.end );
+		} );
+
+		it( 'should get anchor from last inserted range', () => {
+			documentSelection._setTo( [ range1, range2 ] );
+
+			expect( documentSelection.anchor.isEqual( range2.start ) ).to.be.true;
+		} );
+	} );
+
+	describe( 'focus', () => {
+		it( 'should return null if no ranges in selection', () => {
+			expect( documentSelection.focus ).to.be.null;
+		} );
+
+		it( 'should return end of single range in selection', () => {
+			documentSelection._setTo( range1 );
+			const focus = documentSelection.focus;
+
+			expect( focus.isEqual( range1.end ) ).to.be.true;
+		} );
+
+		it( 'should return start of single range in selection when added as backward', () => {
+			documentSelection._setTo( range1, { backward: true } );
+			const focus = documentSelection.focus;
+
+			expect( focus.isEqual( range1.start ) ).to.be.true;
+			expect( focus ).to.not.equal( range1.start );
+		} );
+
+		it( 'should get focus from last inserted range', () => {
+			documentSelection._setTo( [ range1, range2 ] );
+
+			expect( documentSelection.focus.isEqual( range2.end ) ).to.be.true;
+		} );
+	} );
+
+	describe( '_setFocus()', () => {
+		it( 'keeps all existing ranges when no modifications needed', () => {
+			documentSelection._setTo( range1 );
+			documentSelection._setFocus( documentSelection.focus );
+
+			expect( count( documentSelection.getRanges() ) ).to.equal( 1 );
+		} );
+
+		it( 'throws if there are no ranges in selection', () => {
+			const endPos = Position.createAt( el, 'end' );
+
+			expect( () => {
+				documentSelection._setFocus( endPos );
+			} ).to.throw( CKEditorError, /view-selection-setFocus-no-ranges/ );
+		} );
+
+		it( 'modifies existing collapsed selection', () => {
+			const startPos = Position.createAt( el, 1 );
+			const endPos = Position.createAt( el, 2 );
+
+			documentSelection._setTo( startPos );
+
+			documentSelection._setFocus( endPos );
+
+			expect( documentSelection.anchor.compareWith( startPos ) ).to.equal( 'same' );
+			expect( documentSelection.focus.compareWith( endPos ) ).to.equal( 'same' );
+		} );
+
+		it( 'makes existing collapsed selection a backward selection', () => {
+			const startPos = Position.createAt( el, 1 );
+			const endPos = Position.createAt( el, 0 );
+
+			documentSelection._setTo( startPos );
+
+			documentSelection._setFocus( endPos );
+
+			expect( documentSelection.anchor.compareWith( startPos ) ).to.equal( 'same' );
+			expect( documentSelection.focus.compareWith( endPos ) ).to.equal( 'same' );
+			expect( documentSelection.isBackward ).to.be.true;
+		} );
+
+		it( 'modifies existing non-collapsed selection', () => {
+			const startPos = Position.createAt( el, 1 );
+			const endPos = Position.createAt( el, 2 );
+			const newEndPos = Position.createAt( el, 3 );
+
+			documentSelection._setTo( new Range( startPos, endPos ) );
+
+			documentSelection._setFocus( newEndPos );
+
+			expect( documentSelection.anchor.compareWith( startPos ) ).to.equal( 'same' );
+			expect( documentSelection.focus.compareWith( newEndPos ) ).to.equal( 'same' );
+		} );
+
+		it( 'makes existing non-collapsed selection a backward selection', () => {
+			const startPos = Position.createAt( el, 1 );
+			const endPos = Position.createAt( el, 2 );
+			const newEndPos = Position.createAt( el, 0 );
+
+			documentSelection._setTo( new Range( startPos, endPos ) );
+
+			documentSelection._setFocus( newEndPos );
+
+			expect( documentSelection.anchor.compareWith( startPos ) ).to.equal( 'same' );
+			expect( documentSelection.focus.compareWith( newEndPos ) ).to.equal( 'same' );
+			expect( documentSelection.isBackward ).to.be.true;
+		} );
+
+		it( 'makes existing backward selection a forward selection', () => {
+			const startPos = Position.createAt( el, 1 );
+			const endPos = Position.createAt( el, 2 );
+			const newEndPos = Position.createAt( el, 3 );
+
+			documentSelection._setTo( new Range( startPos, endPos ), { backward: true } );
+
+			documentSelection._setFocus( newEndPos );
+
+			expect( documentSelection.anchor.compareWith( endPos ) ).to.equal( 'same' );
+			expect( documentSelection.focus.compareWith( newEndPos ) ).to.equal( 'same' );
+			expect( documentSelection.isBackward ).to.be.false;
+		} );
+
+		it( 'modifies existing backward selection', () => {
+			const startPos = Position.createAt( el, 1 );
+			const endPos = Position.createAt( el, 2 );
+			const newEndPos = Position.createAt( el, 0 );
+
+			documentSelection._setTo( new Range( startPos, endPos ), { backward: true } );
+
+			documentSelection._setFocus( newEndPos );
+
+			expect( documentSelection.anchor.compareWith( endPos ) ).to.equal( 'same' );
+			expect( documentSelection.focus.compareWith( newEndPos ) ).to.equal( 'same' );
+			expect( documentSelection.isBackward ).to.be.true;
+		} );
+
+		it( 'modifies only the last range', () => {
+			// Offsets are chosen in this way that the order of adding ranges must count, not their document order.
+			const startPos1 = Position.createAt( el, 4 );
+			const endPos1 = Position.createAt( el, 5 );
+			const startPos2 = Position.createAt( el, 1 );
+			const endPos2 = Position.createAt( el, 2 );
+
+			const newEndPos = Position.createAt( el, 0 );
+
+			documentSelection._setTo( [
+				new Range( startPos1, endPos1 ),
+				new Range( startPos2, endPos2 )
+			] );
+
+			documentSelection._setFocus( newEndPos );
+
+			const ranges = Array.from( documentSelection.getRanges() );
+
+			expect( ranges ).to.have.lengthOf( 2 );
+			expect( ranges[ 0 ].start.compareWith( startPos1 ) ).to.equal( 'same' );
+			expect( ranges[ 0 ].end.compareWith( endPos1 ) ).to.equal( 'same' );
+
+			expect( documentSelection.anchor.compareWith( startPos2 ) ).to.equal( 'same' );
+			expect( documentSelection.focus.compareWith( newEndPos ) ).to.equal( 'same' );
+			expect( documentSelection.isBackward ).to.be.true;
+		} );
+
+		it( 'collapses the selection when extending to the anchor', () => {
+			const startPos = Position.createAt( el, 1 );
+			const endPos = Position.createAt( el, 2 );
+
+			documentSelection._setTo( new Range( startPos, endPos ) );
+
+			documentSelection._setFocus( startPos );
+
+			expect( documentSelection.focus.compareWith( startPos ) ).to.equal( 'same' );
+			expect( documentSelection.isCollapsed ).to.be.true;
+		} );
+
+		it( 'uses Position.createAt', () => {
+			const startPos = Position.createAt( el, 1 );
+			const endPos = Position.createAt( el, 2 );
+			const newEndPos = Position.createAt( el, 4 );
+
+			const spy = sinon.stub( Position, 'createAt' ).returns( newEndPos );
+
+			documentSelection._setTo( new Range( startPos, endPos ) );
+			documentSelection._setFocus( el, 'end' );
+
+			expect( spy.calledOnce ).to.be.true;
+			expect( documentSelection.focus.compareWith( newEndPos ) ).to.equal( 'same' );
+
+			Position.createAt.restore();
+		} );
+	} );
+
+	describe( 'isCollapsed', () => {
+		it( 'should return true when there is single collapsed range', () => {
+			const range = Range.createFromParentsAndOffsets( el, 5, el, 5 );
+			documentSelection._setTo( range );
+
+			expect( documentSelection.isCollapsed ).to.be.true;
+		} );
+
+		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 );
+			documentSelection._setTo( [ range1, range2 ] );
+
+			expect( documentSelection.isCollapsed ).to.be.false;
+		} );
+
+		it( 'should return false when there is not collapsed range', () => {
+			const range = Range.createFromParentsAndOffsets( el, 15, el, 16 );
+			documentSelection._setTo( range );
+
+			expect( documentSelection.isCollapsed ).to.be.false;
+		} );
+	} );
+
+	describe( 'rangeCount', () => {
+		it( 'should return proper range count', () => {
+			expect( documentSelection.rangeCount ).to.equal( 0 );
+
+			documentSelection._setTo( range1 );
+
+			expect( documentSelection.rangeCount ).to.equal( 1 );
+
+			documentSelection._setTo( [ range1, range2 ] );
+
+			expect( documentSelection.rangeCount ).to.equal( 2 );
+		} );
+	} );
+
+	describe( 'isBackward', () => {
+		it( 'is defined by the last added range', () => {
+			const range1 = Range.createFromParentsAndOffsets( el, 5, el, 10 );
+			const range2 = Range.createFromParentsAndOffsets( el, 15, el, 16 );
+
+			documentSelection._setTo( range1, { backward: true } );
+			expect( documentSelection ).to.have.property( 'isBackward', true );
+
+			documentSelection._setTo( [ range1, range2 ] );
+			expect( documentSelection ).to.have.property( 'isBackward', false );
+		} );
+
+		it( 'is false when last range is collapsed', () => {
+			const range = Range.createFromParentsAndOffsets( el, 5, el, 5 );
+
+			documentSelection._setTo( range, { backward: true } );
+
+			expect( documentSelection.isBackward ).to.be.false;
+		} );
+	} );
+
+	describe( 'getRanges', () => {
+		it( 'should return iterator with copies of all ranges', () => {
+			documentSelection._setTo( [ range1, range2 ] );
+
+			const iterable = documentSelection.getRanges();
+			const ranges = Array.from( iterable );
+
+			expect( ranges.length ).to.equal( 2 );
+			expect( ranges[ 0 ].isEqual( range1 ) ).to.be.true;
+			expect( ranges[ 0 ] ).to.not.equal( range1 );
+			expect( ranges[ 1 ].isEqual( range2 ) ).to.be.true;
+			expect( ranges[ 1 ] ).to.not.equal( range2 );
+		} );
+	} );
+
+	describe( 'getFirstRange', () => {
+		it( 'should return copy of range with first position', () => {
+			documentSelection._setTo( [ range1, range2, range3 ] );
+
+			const range = documentSelection.getFirstRange();
+
+			expect( range.isEqual( range2 ) ).to.be.true;
+			expect( range ).to.not.equal( range2 );
+		} );
+
+		it( 'should return null if no ranges are present', () => {
+			expect( documentSelection.getFirstRange() ).to.be.null;
+		} );
+	} );
+
+	describe( 'getLastRange', () => {
+		it( 'should return copy of range with last position', () => {
+			documentSelection._setTo( [ range1, range2, range3 ] );
+
+			const range = documentSelection.getLastRange();
+
+			expect( range.isEqual( range3 ) ).to.be.true;
+			expect( range ).to.not.equal( range3 );
+		} );
+
+		it( 'should return null if no ranges are present', () => {
+			expect( documentSelection.getLastRange() ).to.be.null;
+		} );
+	} );
+
+	describe( 'getFirstPosition', () => {
+		it( 'should return copy of first position', () => {
+			documentSelection._setTo( [ range1, range2, range3 ] );
+
+			const position = documentSelection.getFirstPosition();
+
+			expect( position.isEqual( range2.start ) ).to.be.true;
+			expect( position ).to.not.equal( range2.start );
+		} );
+
+		it( 'should return null if no ranges are present', () => {
+			expect( documentSelection.getFirstPosition() ).to.be.null;
+		} );
+	} );
+
+	describe( 'getLastPosition', () => {
+		it( 'should return copy of range with last position', () => {
+			documentSelection._setTo( [ range1, range2, range3 ] );
+
+			const position = documentSelection.getLastPosition();
+
+			expect( position.isEqual( range3.end ) ).to.be.true;
+			expect( position ).to.not.equal( range3.end );
+		} );
+
+		it( 'should return null if no ranges are present', () => {
+			expect( documentSelection.getLastPosition() ).to.be.null;
+		} );
+	} );
+
+	describe( 'isEqual', () => {
+		it( 'should return true if selections equal', () => {
+			documentSelection._setTo( [ range1, range2 ] );
+
+			const otherSelection = new DocumentSelection();
+			otherSelection._setTo( [ range1, range2 ] );
+
+			expect( documentSelection.isEqual( otherSelection ) ).to.be.true;
+		} );
+
+		it( 'should return true if selections equal - DocumentSelection and Selection', () => {
+			documentSelection._setTo( [ range1, range2 ] );
+
+			const otherSelection = new Selection();
+			otherSelection.setTo( [ range1, range2 ] );
+
+			expect( documentSelection.isEqual( otherSelection ) ).to.be.true;
+		} );
+
+		it( 'should return true if backward selections equal', () => {
+			documentSelection._setTo( range1, { backward: true } );
+
+			const otherSelection = new DocumentSelection( [ range1 ], { backward: true } );
+
+			expect( documentSelection.isEqual( otherSelection ) ).to.be.true;
+		} );
+
+		it( 'should return true if backward selections equal - DocumentSelection and Selection', () => {
+			documentSelection._setTo( range1, { backward: true } );
+
+			const otherSelection = new Selection( [ range1 ], { backward: true } );
+
+			expect( documentSelection.isEqual( otherSelection ) ).to.be.true;
+		} );
+
+		it( 'should return false if ranges count does not equal', () => {
+			documentSelection._setTo( [ range1, range2 ] );
+
+			const otherSelection = new DocumentSelection( [ range1 ] );
+
+			expect( documentSelection.isEqual( otherSelection ) ).to.be.false;
+		} );
+
+		it( 'should return false if ranges count does not equal - DocumentSelection and Selection', () => {
+			documentSelection._setTo( [ range1, range2 ] );
+
+			const otherSelection = new Selection( [ range1 ] );
+
+			expect( documentSelection.isEqual( otherSelection ) ).to.be.false;
+		} );
+
+		it( 'should return false if ranges (other than the last added one) do not equal', () => {
+			documentSelection._setTo( [ range1, range3 ] );
+
+			const otherSelection = new DocumentSelection( [ range2, range3 ] );
+
+			expect( documentSelection.isEqual( otherSelection ) ).to.be.false;
+		} );
+
+		it( 'should return false if ranges (other than the last added one) do not equal - DocumentSelection and Selection', () => {
+			documentSelection._setTo( [ range1, range3 ] );
+
+			const otherSelection = new Selection( [ range2, range3 ] );
+
+			expect( documentSelection.isEqual( otherSelection ) ).to.be.false;
+		} );
+
+		it( 'should return false if directions do not equal', () => {
+			documentSelection._setTo( range1 );
+
+			const otherSelection = new DocumentSelection( [ range1 ], { backward: true } );
+
+			expect( documentSelection.isEqual( otherSelection ) ).to.be.false;
+		} );
+
+		it( 'should return false if directions do not equal - DocumentSelection and Selection', () => {
+			documentSelection._setTo( range1 );
+
+			const otherSelection = new Selection( [ range1 ], { backward: true } );
+
+			expect( documentSelection.isEqual( otherSelection ) ).to.be.false;
+		} );
+
+		it( 'should return false if one selection is fake', () => {
+			const otherSelection = new DocumentSelection( null, { fake: true } );
+
+			expect( documentSelection.isEqual( otherSelection ) ).to.be.false;
+		} );
+
+		it( 'should return false if one selection is fake', () => {
+			const otherSelection = new DocumentSelection( null, { fake: true } );
+
+			expect( documentSelection.isEqual( otherSelection ) ).to.be.false;
+		} );
+
+		it( 'should return true if both selection are fake - DocumentSelection and Selection', () => {
+			const otherSelection = new Selection( range1, { fake: true } );
+			documentSelection._setTo( range1, { fake: true } );
+
+			expect( documentSelection.isEqual( otherSelection ) ).to.be.true;
+		} );
+
+		it( 'should return false if both selection are fake but have different label', () => {
+			const otherSelection = new DocumentSelection( [ range1 ], { fake: true, label: 'foo bar baz' } );
+			documentSelection._setTo( range1, { fake: true, label: 'foo' } );
+
+			expect( documentSelection.isEqual( otherSelection ) ).to.be.false;
+		} );
+
+		it( 'should return false if both selection are fake but have different label - DocumentSelection and Selection', () => {
+			const otherSelection = new Selection( [ range1 ], { fake: true, label: 'foo bar baz' } );
+			documentSelection._setTo( range1, { fake: true, label: 'foo' } );
+
+			expect( documentSelection.isEqual( otherSelection ) ).to.be.false;
+		} );
+
+		it( 'should return true if both selections are empty', () => {
+			const otherSelection = new DocumentSelection();
+
+			expect( documentSelection.isEqual( otherSelection ) ).to.be.true;
+		} );
+
+		it( 'should return true if both selections are empty - DocumentSelection and Selection', () => {
+			const otherSelection = new Selection();
+
+			expect( documentSelection.isEqual( otherSelection ) ).to.be.true;
+		} );
+	} );
+
+	describe( 'isSimilar', () => {
+		it( 'should return true if selections equal', () => {
+			documentSelection._setTo( [ range1, range2 ] );
+
+			const otherSelection = new DocumentSelection( [ range1, range2 ] );
+
+			expect( documentSelection.isSimilar( otherSelection ) ).to.be.true;
+		} );
+
+		it( 'should return true if selections equal - DocumentSelection and Selection', () => {
+			documentSelection._setTo( [ range1, range2 ] );
+
+			const otherSelection = new Selection( [ range1, range2 ] );
+
+			expect( documentSelection.isSimilar( otherSelection ) ).to.be.true;
+		} );
+
+		it( 'should return false if ranges count does not equal', () => {
+			documentSelection._setTo( [ range1, range2 ] );
+
+			const otherSelection = new DocumentSelection( [ range1 ] );
+
+			expect( documentSelection.isSimilar( otherSelection ) ).to.be.false;
+		} );
+
+		it( 'should return false if ranges count does not equal - DocumentSelection and Selection', () => {
+			documentSelection._setTo( [ range1, range2 ] );
+
+			const otherSelection = new Selection( [ range1 ] );
+
+			expect( documentSelection.isSimilar( otherSelection ) ).to.be.false;
+		} );
+
+		it( 'should return false if trimmed ranges (other than the last added one) are not equal', () => {
+			documentSelection._setTo( [ range1, range3 ] );
+
+			const otherSelection = new DocumentSelection( [ range2, range3 ] );
+
+			expect( documentSelection.isSimilar( otherSelection ) ).to.be.false;
+		} );
+
+		it( 'should return false if trimmed ranges (other than the last added one) are not equal - with Selection', () => {
+			documentSelection._setTo( [ range1, range3 ] );
+
+			const otherSelection = new Selection( [ range2, range3 ] );
+
+			expect( documentSelection.isSimilar( otherSelection ) ).to.be.false;
+		} );
+
+		it( 'should return false if directions are not equal', () => {
+			documentSelection._setTo( range1 );
+
+			const otherSelection = new DocumentSelection( [ range1 ], { backward: true } );
+
+			expect( documentSelection.isSimilar( otherSelection ) ).to.be.false;
+		} );
+
+		it( 'should return false if directions are not equal - DocumentSelection and Selection', () => {
+			documentSelection._setTo( range1 );
+
+			const otherSelection = new Selection( [ range1 ], { backward: true } );
+
+			expect( documentSelection.isSimilar( otherSelection ) ).to.be.false;
+		} );
+
+		it( 'should return true if both selections are empty', () => {
+			const otherSelection = new DocumentSelection();
+
+			expect( documentSelection.isSimilar( otherSelection ) ).to.be.true;
+		} );
+
+		it( 'should return true if both selections are empty - DocumentSelection and Selection', () => {
+			const otherSelection = new Selection();
+
+			expect( documentSelection.isSimilar( otherSelection ) ).to.be.true;
+		} );
+
+		it( 'should return true if all ranges trimmed from both selections are equal', () => {
+			const view = parse(
+				'<container:p><attribute:span></attribute:span></container:p>' +
+				'<container:p><attribute:span>xx</attribute:span></container:p>'
+			);
+
+			const p1 = view.getChild( 0 );
+			const p2 = view.getChild( 1 );
+			const span1 = p1.getChild( 0 );
+			const span2 = p2.getChild( 0 );
+
+			// <p>[<span>{]</span>}</p><p>[<span>{xx}</span>]</p>
+			const rangeA1 = Range.createFromParentsAndOffsets( p1, 0, span1, 0 );
+			const rangeB1 = Range.createFromParentsAndOffsets( span1, 0, p1, 1 );
+			const rangeA2 = Range.createFromParentsAndOffsets( p2, 0, p2, 1 );
+			const rangeB2 = Range.createFromParentsAndOffsets( span2, 0, span2, 1 );
+
+			documentSelection._setTo( [ rangeA1, rangeA2 ] );
+
+			const otherSelection = new DocumentSelection( [ rangeB2, rangeB1 ] );
+
+			expect( documentSelection.isSimilar( otherSelection ) ).to.be.true;
+			expect( otherSelection.isSimilar( documentSelection ) ).to.be.true;
+
+			expect( documentSelection.isEqual( otherSelection ) ).to.be.false;
+			expect( otherSelection.isEqual( documentSelection ) ).to.be.false;
+		} );
+
+		it( 'should return true if all ranges trimmed from both selections are equal - DocumentSelection and Selection', () => {
+			const view = parse(
+				'<container:p><attribute:span></attribute:span></container:p>' +
+				'<container:p><attribute:span>xx</attribute:span></container:p>'
+			);
+
+			const p1 = view.getChild( 0 );
+			const p2 = view.getChild( 1 );
+			const span1 = p1.getChild( 0 );
+			const span2 = p2.getChild( 0 );
+
+			// <p>[<span>{]</span>}</p><p>[<span>{xx}</span>]</p>
+			const rangeA1 = Range.createFromParentsAndOffsets( p1, 0, span1, 0 );
+			const rangeB1 = Range.createFromParentsAndOffsets( span1, 0, p1, 1 );
+			const rangeA2 = Range.createFromParentsAndOffsets( p2, 0, p2, 1 );
+			const rangeB2 = Range.createFromParentsAndOffsets( span2, 0, span2, 1 );
+
+			documentSelection._setTo( [ rangeA1, rangeA2 ] );
+
+			const otherSelection = new Selection( [ rangeB2, rangeB1 ] );
+
+			expect( documentSelection.isSimilar( otherSelection ) ).to.be.true;
+			expect( otherSelection.isSimilar( documentSelection ) ).to.be.true;
+
+			expect( documentSelection.isEqual( otherSelection ) ).to.be.false;
+			expect( otherSelection.isEqual( documentSelection ) ).to.be.false;
+		} );
+	} );
+
+	describe( '_setTo()', () => {
+		describe( 'simple scenarios', () => {
+			it( 'should set selection ranges from the given selection', () => {
+				documentSelection._setTo( range1 );
+
+				const otherSelection = new DocumentSelection( [ range2, range3 ], { backward: true } );
+
+				documentSelection._setTo( otherSelection );
+
+				expect( documentSelection.rangeCount ).to.equal( 2 );
+				expect( documentSelection._ranges[ 0 ].isEqual( range2 ) ).to.be.true;
+				expect( documentSelection._ranges[ 0 ] ).is.not.equal( range2 );
+				expect( documentSelection._ranges[ 1 ].isEqual( range3 ) ).to.be.true;
+				expect( documentSelection._ranges[ 1 ] ).is.not.equal( range3 );
+
+				expect( documentSelection.anchor.isEqual( range3.end ) ).to.be.true;
+			} );
+
+			it( 'should set selection on the given Range', () => {
+				documentSelection._setTo( range1 );
+
+				expect( Array.from( documentSelection.getRanges() ) ).to.deep.equal( [ range1 ] );
+				expect( documentSelection.isBackward ).to.be.false;
+			} );
+
+			it( 'should set selection on the given iterable of Ranges', () => {
+				documentSelection._setTo( new Set( [ range1, range2 ] ) );
+
+				expect( Array.from( documentSelection.getRanges() ) ).to.deep.equal( [ range1, range2 ] );
+				expect( documentSelection.isBackward ).to.be.false;
+			} );
+
+			it( 'should set collapsed selection on the given Position', () => {
+				documentSelection._setTo( range1.start );
+
+				expect( Array.from( documentSelection.getRanges() ).length ).to.equal( 1 );
+				expect( Array.from( documentSelection.getRanges() )[ 0 ].start ).to.deep.equal( range1.start );
+				expect( documentSelection.isBackward ).to.be.false;
+				expect( documentSelection.isCollapsed ).to.be.true;
+			} );
+
+			it( 'should fire change event', done => {
+				documentSelection.on( 'change', () => {
+					expect( documentSelection.rangeCount ).to.equal( 1 );
+					expect( documentSelection.getFirstRange().isEqual( range1 ) ).to.be.true;
+					done();
+				} );
+
+				const otherSelection = new DocumentSelection( [ range1 ] );
+
+				documentSelection._setTo( otherSelection );
+			} );
+
+			it( 'should set fake state and label', () => {
+				const label = 'foo bar baz';
+				const otherSelection = new DocumentSelection( null, { fake: true, label } );
+				documentSelection._setTo( otherSelection );
+
+				expect( documentSelection.isFake ).to.be.true;
+				expect( documentSelection.fakeSelectionLabel ).to.equal( label );
+			} );
+
+			it( 'should throw an error when trying to set to not selectable', () => {
+				const otherSelection = new DocumentSelection();
+
+				expect( () => {
+					otherSelection._setTo( {} );
+				} ).to.throw( /view-selection-setTo-not-selectable/ );
+			} );
+
+			it( 'should throw an error when trying to set to not selectable #2', () => {
+				const otherSelection = new DocumentSelection();
+
+				expect( () => {
+					otherSelection._setTo();
+				} ).to.throw( /view-selection-setTo-not-selectable/ );
+			} );
+		} );
+
+		describe( 'setting collapsed selection', () => {
+			beforeEach( () => {
+				documentSelection._setTo( [ range1, range2 ] );
+			} );
+
+			it( 'should collapse selection at position', () => {
+				const position = new Position( el, 4 );
+
+				documentSelection._setTo( position );
+				const range = documentSelection.getFirstRange();
+
+				expect( range.start.parent ).to.equal( el );
+				expect( range.start.offset ).to.equal( 4 );
+				expect( range.start.isEqual( range.end ) ).to.be.true;
+			} );
+
+			it( 'should collapse selection at node and offset', () => {
+				const foo = new Text( 'foo' );
+				const p = new Element( 'p', null, foo );
+
+				documentSelection._setTo( foo, 0 );
+				let range = documentSelection.getFirstRange();
+
+				expect( range.start.parent ).to.equal( foo );
+				expect( range.start.offset ).to.equal( 0 );
+				expect( range.start.isEqual( range.end ) ).to.be.true;
+
+				documentSelection._setTo( p, 1 );
+				range = documentSelection.getFirstRange();
+
+				expect( range.start.parent ).to.equal( p );
+				expect( range.start.offset ).to.equal( 1 );
+				expect( range.start.isEqual( range.end ) ).to.be.true;
+			} );
+
+			it( 'should throw an error when the second parameter is not passed and first is an item', () => {
+				const foo = new Text( 'foo' );
+
+				expect( () => {
+					documentSelection._setTo( foo );
+				} ).to.throw( CKEditorError, /view-selection-setTo-required-second-parameter/ );
+			} );
+
+			it( 'should collapse selection at node and flag', () => {
+				const foo = new Text( 'foo' );
+				const p = new Element( 'p', null, foo );
+
+				documentSelection._setTo( foo, 'end' );
+				let range = documentSelection.getFirstRange();
+
+				expect( range.start.parent ).to.equal( foo );
+				expect( range.start.offset ).to.equal( 3 );
+				expect( range.start.isEqual( range.end ) ).to.be.true;
+
+				documentSelection._setTo( foo, 'before' );
+				range = documentSelection.getFirstRange();
+
+				expect( range.start.parent ).to.equal( p );
+				expect( range.start.offset ).to.equal( 0 );
+				expect( range.start.isEqual( range.end ) ).to.be.true;
+
+				documentSelection._setTo( foo, 'after' );
+				range = documentSelection.getFirstRange();
+
+				expect( range.start.parent ).to.equal( p );
+				expect( range.start.offset ).to.equal( 1 );
+				expect( range.start.isEqual( range.end ) ).to.be.true;
+			} );
+		} );
+
+		describe( 'setting collapsed selection at start', () => {
+			it( 'should collapse to start position and fire change event', done => {
+				documentSelection._setTo( [ range1, range2, range3 ] );
+				documentSelection.once( 'change', () => {
+					expect( documentSelection.rangeCount ).to.equal( 1 );
+					expect( documentSelection.isCollapsed ).to.be.true;
+					expect( documentSelection._ranges[ 0 ].start.isEqual( range2.start ) ).to.be.true;
+					done();
+				} );
+
+				documentSelection._setTo( documentSelection.getFirstPosition() );
+			} );
+		} );
+
+		describe( 'setting collapsed selection to end', () => {
+			it( 'should collapse to end position and fire change event', done => {
+				documentSelection._setTo( [ range1, range2, range3 ] );
+				documentSelection.once( 'change', () => {
+					expect( documentSelection.rangeCount ).to.equal( 1 );
+					expect( documentSelection.isCollapsed ).to.be.true;
+					expect( documentSelection._ranges[ 0 ].end.isEqual( range3.end ) ).to.be.true;
+					done();
+				} );
+
+				documentSelection._setTo( documentSelection.getLastPosition() );
+			} );
+		} );
+
+		describe( 'removing all ranges', () => {
+			it( 'should remove all ranges and fire change event', done => {
+				documentSelection._setTo( [ range1, range2 ] );
+
+				documentSelection.once( 'change', () => {
+					expect( documentSelection.rangeCount ).to.equal( 0 );
+					done();
+				} );
+
+				documentSelection._setTo( null );
+			} );
+		} );
+
+		describe( 'setting fake selection', () => {
+			it( 'should allow to set selection to fake', () => {
+				documentSelection._setTo( range1, { fake: true } );
+
+				expect( documentSelection.isFake ).to.be.true;
+			} );
+
+			it( 'should allow to set fake selection label', () => {
+				const label = 'foo bar baz';
+				documentSelection._setTo( range1, { fake: true, label } );
+
+				expect( documentSelection.fakeSelectionLabel ).to.equal( label );
+			} );
+
+			it( 'should not set label when set to false', () => {
+				const label = 'foo bar baz';
+				documentSelection._setTo( range1, { fake: false, label } );
+
+				expect( documentSelection.fakeSelectionLabel ).to.equal( '' );
+			} );
+
+			it( 'should reset label when set to false', () => {
+				const label = 'foo bar baz';
+				documentSelection._setTo( range1, { fake: true, label } );
+				documentSelection._setTo( range1 );
+
+				expect( documentSelection.fakeSelectionLabel ).to.equal( '' );
+			} );
+
+			it( 'should fire change event', done => {
+				documentSelection.once( 'change', () => {
+					expect( documentSelection.isFake ).to.be.true;
+					expect( documentSelection.fakeSelectionLabel ).to.equal( 'foo bar baz' );
+
+					done();
+				} );
+
+				documentSelection._setTo( range1, { fake: true, label: 'foo bar baz' } );
+			} );
+
+			it( 'should be possible to create an empty fake selection', () => {
+				documentSelection._setTo( null, { fake: true, label: 'foo bar baz' } );
+
+				expect( documentSelection.fakeSelectionLabel ).to.equal( 'foo bar baz' );
+				expect( documentSelection.isFake ).to.be.true;
+			} );
+		} );
+
+		describe( 'setting selection to itself', () => {
+			it( 'should correctly set ranges when setting to the same selection', () => {
+				documentSelection._setTo( [ range1, range2 ] );
+				documentSelection._setTo( documentSelection );
+
+				const ranges = Array.from( documentSelection.getRanges() );
+				expect( ranges.length ).to.equal( 2 );
+
+				expect( ranges[ 0 ].isEqual( range1 ) ).to.be.true;
+				expect( ranges[ 1 ].isEqual( range2 ) ).to.be.true;
+			} );
+
+			it( 'should correctly set ranges when setting to the same selection\'s ranges', () => {
+				documentSelection._setTo( [ range1, range2 ] );
+				documentSelection._setTo( documentSelection.getRanges() );
+
+				const ranges = Array.from( documentSelection.getRanges() );
+				expect( ranges.length ).to.equal( 2 );
+
+				expect( ranges[ 0 ].isEqual( range1 ) ).to.be.true;
+				expect( ranges[ 1 ].isEqual( range2 ) ).to.be.true;
+			} );
+		} );
+
+		describe( 'throwing errors', () => {
+			it( 'should throw an error when range is invalid', () => {
+				expect( () => {
+					documentSelection._setTo( [ { invalid: 'range' } ] );
+				} ).to.throw( CKEditorError, 'view-selection-invalid-range: Invalid Range.' );
+			} );
+
+			it( 'should throw when range is intersecting with already added range', () => {
+				const text = el.getChild( 0 );
+				const range2 = Range.createFromParentsAndOffsets( text, 7, text, 15 );
+
+				expect( () => {
+					documentSelection._setTo( [ range1, range2 ] );
+				} ).to.throw( CKEditorError, 'view-selection-range-intersects' );
+			} );
+		} );
+
+		it( 'should allow setting selection on an item', () => {
+			const textNode1 = new Text( 'foo' );
+			const textNode2 = new Text( 'bar' );
+			const textNode3 = new Text( 'baz' );
+			const element = new Element( 'p', null, [ textNode1, textNode2, textNode3 ] );
+
+			documentSelection._setTo( textNode2, 'on' );
+
+			const ranges = Array.from( documentSelection.getRanges() );
+			expect( ranges.length ).to.equal( 1 );
+			expect( ranges[ 0 ].start.parent ).to.equal( element );
+			expect( ranges[ 0 ].start.offset ).to.deep.equal( 1 );
+			expect( ranges[ 0 ].end.parent ).to.equal( element );
+			expect( ranges[ 0 ].end.offset ).to.deep.equal( 2 );
+		} );
+
+		it( 'should allow setting selection inside an element', () => {
+			const element = new Element( 'p', null, [ new Text( 'foo' ), new Text( 'bar' ) ] );
+
+			documentSelection._setTo( element, 'in' );
+
+			const ranges = Array.from( documentSelection.getRanges() );
+			expect( ranges.length ).to.equal( 1 );
+			expect( ranges[ 0 ].start.parent ).to.equal( element );
+			expect( ranges[ 0 ].start.offset ).to.deep.equal( 0 );
+			expect( ranges[ 0 ].end.parent ).to.equal( element );
+			expect( ranges[ 0 ].end.offset ).to.deep.equal( 2 );
+		} );
+
+		it( 'should allow setting backward selection inside an element', () => {
+			const element = new Element( 'p', null, [ new Text( 'foo' ), new Text( 'bar' ) ] );
+
+			documentSelection._setTo( element, 'in', { backward: true } );
+
+			const ranges = Array.from( documentSelection.getRanges() );
+			expect( ranges.length ).to.equal( 1 );
+			expect( ranges[ 0 ].start.parent ).to.equal( element );
+			expect( ranges[ 0 ].start.offset ).to.deep.equal( 0 );
+			expect( ranges[ 0 ].end.parent ).to.equal( element );
+			expect( ranges[ 0 ].end.offset ).to.deep.equal( 2 );
+			expect( documentSelection.isBackward ).to.be.true;
+		} );
+	} );
+
+	describe( 'getEditableElement()', () => {
+		it( 'should return null if no ranges in selection', () => {
+			expect( documentSelection.editableElement ).to.be.null;
+		} );
+
+		it( 'should return null if selection is placed in container that is not EditableElement', () => {
+			documentSelection._setTo( range1 );
+
+			expect( documentSelection.editableElement ).to.be.null;
+		} );
+
+		it( 'should return EditableElement when selection is placed inside', () => {
+			const viewDocument = new Document();
+			documentSelection._setTo( viewDocument.selection );
+			const root = createViewRoot( viewDocument, 'div', 'main' );
+			const element = new Element( 'p' );
+			root._appendChildren( element );
+
+			documentSelection._setTo( Range.createFromParentsAndOffsets( element, 0, element, 0 ) );
+
+			expect( documentSelection.editableElement ).to.equal( root );
+		} );
+	} );
+
+	describe( 'isFake', () => {
+		it( 'should be false for newly created instance', () => {
+			expect( documentSelection.isFake ).to.be.false;
+		} );
+	} );
+
+	describe( 'getSelectedElement()', () => {
+		it( 'should return selected element', () => {
+			const { selection: documentSelection, view } = parse( 'foo [<b>bar</b>] baz' );
+			const b = view.getChild( 1 );
+
+			expect( documentSelection.getSelectedElement() ).to.equal( b );
+		} );
+
+		it( 'should return null if there is more than one range', () => {
+			const { selection: documentSelection } = parse( 'foo [<b>bar</b>] [<i>baz</i>]' );
+
+			expect( documentSelection.getSelectedElement() ).to.be.null;
+		} );
+
+		it( 'should return null if there is no selection', () => {
+			expect( documentSelection.getSelectedElement() ).to.be.null;
+		} );
+
+		it( 'should return null if selection is not over single element #1', () => {
+			const { selection: documentSelection } = parse( 'foo [<b>bar</b> ba}z' );
+
+			expect( documentSelection.getSelectedElement() ).to.be.null;
+		} );
+
+		it( 'should return null if selection is not over single element #2', () => {
+			const { selection: documentSelection } = parse( 'foo <b>{bar}</b> baz' );
+
+			expect( documentSelection.getSelectedElement() ).to.be.null;
+		} );
+	} );
+} );

+ 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();

+ 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 CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
@@ -29,7 +29,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;
 		} );