Sfoglia il codice sorgente

Merge pull request #505 from ckeditor/t/329

T/329 Split Selection to Selection and DocumentSelection
Piotr Jasiun 9 anni fa
parent
commit
43ed3843e9

+ 5 - 5
packages/ckeditor5-engine/src/model/document.js

@@ -12,7 +12,7 @@ import transformations from './delta/basic-transformations.js'; // jshint ignore
 import RootElement from './rootelement.js';
 import Batch from './batch.js';
 import History from './history.js';
-import Selection from './selection.js';
+import LiveSelection from './liveselection.js';
 import EmitterMixin from '../../utils/emittermixin.js';
 import CKEditorError from '../../utils/ckeditorerror.js';
 import mix from '../../utils/mix.js';
@@ -55,9 +55,9 @@ export default class Document {
 		 * Selection done on this document.
 		 *
 		 * @readonly
-		 * @member {engine.model.Selection} engine.model.Document#selection
+		 * @member {engine.model.LiveSelection} engine.model.Document#selection
 		 */
-		this.selection = new Selection( this );
+		this.selection = new LiveSelection( this );
 
 		/**
 		 * Schema for this document.
@@ -285,9 +285,9 @@ export default class Document {
 		const json = clone( this );
 
 		// Due to circular references we need to remove parent reference.
-		json.selection = '[engine.model.Selection]';
+		json.selection = '[engine.model.LiveSelection]';
 
-		return {};
+		return json;
 	}
 
 	/**

+ 439 - 0
packages/ckeditor5-engine/src/model/liveselection.js

@@ -0,0 +1,439 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import LiveRange from './liverange.js';
+import Range from './range.js';
+import Position from './position.js';
+import CharacterProxy from './characterproxy.js';
+import toMap from '../../utils/tomap.js';
+
+import Selection from './selection.js';
+
+const storePrefix = 'selection:';
+
+/**
+ * `LiveSelection` is a special type of {@link engine.model.Selection selection} that listens to changes on a
+ * {@link engine.model.Document document} and has it ranges updated accordingly. Internal implementation of this
+ * mechanism bases on {@link engine.model.LiveRange live ranges}.
+ *
+ * Differences between {@link engine.model.Selection} and `LiveSelection` are three:
+ * * there is always a range in `LiveSelection`, even if no ranges were added - in this case, there is a
+ * "default range" in selection which is a collapsed range set at the beginning of the {@link engine.model.Document document},
+ * * ranges added to this selection updates automatically when the document changes,
+ * * live selection may have attributes.
+ *
+ * @memberOf engine.model
+ */
+export default class LiveSelection extends Selection {
+	/**
+	 * Creates an empty document selection for given {@link engine.model.Document}.
+	 *
+	 * @param {engine.model.Document} document Document which owns this selection.
+	 */
+	constructor( document ) {
+		super();
+
+		/**
+		 * Document which owns this selection.
+		 *
+		 * @private
+		 * @member {engine.model.Document} engine.model.Selection#_document
+		 */
+		this._document = document;
+
+		/**
+		 * List of attributes set on current selection.
+		 *
+		 * @protected
+		 * @member {Map} engine.model.LiveSelection#_attrs
+		 */
+		this._attrs = new Map();
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	get isCollapsed() {
+		const length = this._ranges.length;
+
+		return length === 0 ? true : super.isCollapsed;
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	get anchor() {
+		return super.anchor || this._getDefaultRange().start;
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	get focus() {
+		return super.focus || this._getDefaultRange().start;
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	get rangeCount() {
+		return this._ranges.length ? this._ranges.length : 1;
+	}
+
+	/**
+	 * Unbinds all events previously bound by document selection.
+	 */
+	destroy() {
+		for ( let i = 0; i < this._ranges.length; i++ ) {
+			this._ranges[ i ].detach();
+		}
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	*getRanges() {
+		if ( this._ranges.length ) {
+			yield *super.getRanges();
+		} else {
+			yield this._getDefaultRange();
+		}
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	getFirstRange() {
+		return super.getFirstRange() || this._getDefaultRange();
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	removeAllRanges() {
+		this.destroy();
+		super.removeAllRanges();
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	setRanges( newRanges, isLastBackward ) {
+		this.destroy();
+		super.setRanges( newRanges, isLastBackward );
+	}
+
+	/**
+	 * Removes all attributes from the selection.
+	 *
+	 * @fires engine.model.LiveSelection#change:attribute
+	 */
+	clearAttributes() {
+		this._attrs.clear();
+		this._setStoredAttributesTo( new Map() );
+
+		this.fire( 'change:attribute' );
+	}
+
+	/**
+	 * Gets an attribute value for given key or `undefined` if that attribute is not set on the selection.
+	 *
+	 * @param {String} key Key of attribute to look for.
+	 * @returns {*} Attribute value or `undefined`.
+	 */
+	getAttribute( key ) {
+		return this._attrs.get( key );
+	}
+
+	/**
+	 * Returns iterator that iterates over this selection attributes.
+	 *
+	 * @returns {Iterable.<*>}
+	 */
+	getAttributes() {
+		return this._attrs[ Symbol.iterator ]();
+	}
+
+	/**
+	 * Checks if the selection has an attribute for given key.
+	 *
+	 * @param {String} key Key of attribute to check.
+	 * @returns {Boolean} `true` if attribute with given key is set on selection, `false` otherwise.
+	 */
+	hasAttribute( key ) {
+		return this._attrs.has( key );
+	}
+
+	/**
+	 * Removes an attribute with given key from the selection.
+	 *
+	 * @fires engine.model.LiveSelection#change:attribute
+	 * @param {String} key Key of attribute to remove.
+	 */
+	removeAttribute( key ) {
+		this._attrs.delete( key );
+		this._removeStoredAttribute( key );
+
+		this.fire( 'change:attribute' );
+	}
+
+	/**
+	 * Sets attribute on the selection. If attribute with the same key already is set, it overwrites its values.
+	 *
+	 * @fires engine.model.LiveSelection#change:attribute
+	 * @param {String} key Key of attribute to set.
+	 * @param {*} value Attribute value.
+	 */
+	setAttribute( key, value ) {
+		this._attrs.set( key, value );
+		this._storeAttribute( key, value );
+
+		this.fire( 'change:attribute' );
+	}
+
+	/**
+	 * Removes all attributes from the selection and sets given attributes.
+	 *
+	 * @fires engine.model.LiveSelection#change:attribute
+	 * @param {Iterable|Object} attrs Iterable object containing attributes to be set.
+	 */
+	setAttributesTo( attrs ) {
+		this._attrs = toMap( attrs );
+		this._setStoredAttributesTo( this._attrs );
+
+		this.fire( 'change:attribute' );
+	}
+
+	/**
+	 * Creates and returns an instance of {@link engine.model.LiveSelection} that is a clone of given selection,
+	 * meaning that it has same ranges and same direction as it.
+	 *
+	 * @params {engine.model.Selection} otherSelection Selection to be cloned.
+	 * @returns {engine.model.LiveSelection} `LiveSelection` instance that is a clone of given selection.
+	 */
+
+	/**
+	 * @inheritDoc
+	 */
+	_popRange() {
+		this._ranges.pop().detach();
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	_pushRange( range ) {
+		this._checkRange( range );
+		this._ranges.push( LiveRange.createFromRange( range ) );
+	}
+
+	/**
+	 * Returns a default range for this selection. The default range is a collapsed range that starts and ends
+	 * at the beginning of this selection's document {@link engine.model.Document#_getDefaultRoot default root}.
+	 * This "artificial" range is important for algorithms that base on selection, so they won't break or need
+	 * special logic if there are no real ranges in the selection.
+	 *
+	 * @private
+	 * @returns {engine.model.Range}
+	 */
+	_getDefaultRange() {
+		const defaultRoot = this._document._getDefaultRoot();
+
+		// Find the first position where the selection can be put.
+		for ( let position of Range.createFromElement( defaultRoot ).getPositions() ) {
+			if ( this._document.schema.check( { name: '$text', inside: position } ) ) {
+				return new Range( position, position );
+			}
+		}
+
+		const position = new Position( defaultRoot, [ 0 ] );
+
+		return new Range( position, position );
+	}
+
+	/**
+	 * Iterates through all attributes stored in current selection's parent.
+	 *
+	 * @returns {Iterable.<*>}
+	 */
+	*_getStoredAttributes() {
+		const selectionParent = this.getFirstPosition().parent;
+
+		if ( this.isCollapsed && selectionParent.getChildCount() === 0 ) {
+			for ( let attr of selectionParent.getAttributes() ) {
+				if ( attr[ 0 ].indexOf( storePrefix ) === 0 ) {
+					const realKey = attr[ 0 ].substr( storePrefix.length );
+
+					yield [ realKey, attr[ 1 ] ];
+				}
+			}
+		}
+	}
+
+	/**
+	 * Removes attribute with given key from attributes stored in current selection's parent node.
+	 *
+	 * @private
+	 * @param {String} key Key of attribute to remove.
+	 */
+	_removeStoredAttribute( key ) {
+		const selectionParent = this.getFirstPosition().parent;
+
+		if ( this.isCollapsed && selectionParent.getChildCount() === 0 ) {
+			const storeKey = LiveSelection._getStoreAttributeKey( key );
+
+			this._document.enqueueChanges( () => {
+				this._document.batch().removeAttr( storeKey, selectionParent );
+			} );
+		}
+	}
+
+	/**
+	 * Stores given attribute key and value in current selection's parent node if the selection is collapsed and
+	 * the parent node is empty.
+	 *
+	 * @private
+	 * @param {String} key Key of attribute to set.
+	 * @param {*} value Attribute value.
+	 */
+	_storeAttribute( key, value ) {
+		const selectionParent = this.getFirstPosition().parent;
+
+		if ( this.isCollapsed && selectionParent.getChildCount() === 0 ) {
+			const storeKey = LiveSelection._getStoreAttributeKey( key );
+
+			this._document.enqueueChanges( () => {
+				this._document.batch().setAttr( storeKey, value, selectionParent );
+			} );
+		}
+	}
+
+	/**
+	 * Sets selection attributes stored in current selection's parent node to given set of attributes.
+	 *
+	 * @param {Iterable|Object} attrs Iterable object containing attributes to be set.
+	 * @private
+	 */
+	_setStoredAttributesTo( attrs ) {
+		const selectionParent = this.getFirstPosition().parent;
+
+		if ( this.isCollapsed && selectionParent.getChildCount() === 0 ) {
+			this._document.enqueueChanges( () => {
+				const batch = this._document.batch();
+
+				for ( let attr of this._getStoredAttributes() ) {
+					const storeKey = LiveSelection._getStoreAttributeKey( attr[ 0 ] );
+
+					batch.removeAttr( storeKey, selectionParent );
+				}
+
+				for ( let attr of attrs ) {
+					const storeKey = LiveSelection._getStoreAttributeKey( attr[ 0 ] );
+
+					batch.setAttr( storeKey, attr[ 1 ], selectionParent );
+				}
+			} );
+		}
+	}
+
+	/**
+	 * Updates this selection attributes according to it's ranges and the document.
+	 *
+	 * @fires engine.model.LiveSelection#change:attribute
+	 * @protected
+	 */
+	_updateAttributes() {
+		const position = this.getFirstPosition();
+		const positionParent = position.parent;
+
+		let attrs = null;
+
+		if ( !this.isCollapsed ) {
+			// 1. If selection is a range...
+			const range = this.getFirstRange();
+
+			// ...look for a first character node in that range and take attributes from it.
+			for ( let item of range ) {
+				// This is not an optimal solution because of https://github.com/ckeditor/ckeditor5-engine/issues/454.
+				// It can be done better by using `break;` instead of checking `attrs === null`.
+				if ( item.type == 'TEXT' && attrs === null ) {
+					attrs = item.item.getAttributes();
+				}
+			}
+		} else {
+			// 2. If the selection is a caret or the range does not contain a character node...
+
+			const nodeBefore = positionParent.getChild( position.offset - 1 );
+			const nodeAfter = positionParent.getChild( position.offset );
+
+			// ...look at the node before caret and take attributes from it if it is a character node.
+			attrs = getAttrsIfCharacter( nodeBefore );
+
+			// 3. If not, look at the node after caret...
+			if ( !attrs ) {
+				attrs = getAttrsIfCharacter( nodeAfter );
+			}
+
+			// 4. If not, try to find the first character on the left, that is in the same node.
+			if ( !attrs ) {
+				let node = nodeBefore;
+
+				while ( node && !attrs ) {
+					node = node.previousSibling;
+					attrs = getAttrsIfCharacter( node );
+				}
+			}
+
+			// 5. If not found, try to find the first character on the right, that is in the same node.
+			if ( !attrs ) {
+				let node = nodeAfter;
+
+				while ( node && !attrs ) {
+					node = node.nextSibling;
+					attrs = getAttrsIfCharacter( node );
+				}
+			}
+
+			// 6. If not found, selection should retrieve attributes from parent.
+			if ( !attrs ) {
+				attrs = this._getStoredAttributes();
+			}
+		}
+
+		if ( attrs ) {
+			this._attrs = new Map( attrs );
+		} else {
+			this.clearAttributes();
+		}
+
+		function getAttrsIfCharacter( node ) {
+			if ( node instanceof CharacterProxy ) {
+				return node.getAttributes();
+			}
+
+			return null;
+		}
+
+		this.fire( 'change:attribute' );
+	}
+
+	/**
+	 * Generates and returns an attribute key for selection attributes store, basing on original attribute key.
+	 *
+	 * @param {String} key Attribute key to convert.
+	 * @returns {String} Converted attribute key, applicable for selection store.
+	 */
+	static _getStoreAttributeKey( key ) {
+		return storePrefix + key;
+	}
+}
+
+/**
+ * Fired whenever selection attributes are changed.
+ *
+ * @event engine.model.LiveSelection#change:attribute
+ */

+ 81 - 354
packages/ckeditor5-engine/src/model/selection.js

@@ -7,47 +7,21 @@
 
 import Position from './position.js';
 import Range from './range.js';
-import LiveRange from './liverange.js';
 import EmitterMixin from '../../utils/emittermixin.js';
-import CharacterProxy from './characterproxy.js';
 import CKEditorError from '../../utils/ckeditorerror.js';
-import toMap from '../../utils/tomap.js';
 import mix from '../../utils/mix.js';
 
-const storePrefix = 'selection:';
-
 /**
- * Represents a selection that is made on nodes in {@link engine.model.Document}. `Selection` instance is
- * created by {@link engine.model.Document}. You should not need to create an instance of `Selection`.
- *
- * Keep in mind that selection always contains at least one range. If no ranges has been added to selection or all ranges
- * got removed from selection, the selection will be reset to contain {@link engine.model.Selection#_getDefaultRange the default range}.
+ * `Selection` is a group of {@link engine.model.Range ranges} which has a direction specified by
+ * {@link engine.model.Selection#anchor anchor} and {@link engine.model.Selection#focus focus}.
  *
  * @memberOf engine.model
  */
 export default class Selection {
 	/**
 	 * Creates an empty selection.
-	 *
-	 * @param {engine.model.Document} document Document which owns this selection.
 	 */
-	constructor( document ) {
-		/**
-		 * List of attributes set on current selection.
-		 *
-		 * @protected
-		 * @member {Map} engine.model.Selection#_attrs
-		 */
-		this._attrs = new Map();
-
-		/**
-		 * Document which owns this selection.
-		 *
-		 * @private
-		 * @member {engine.model.Document} engine.model.Selection#_document
-		 */
-		this._document = document;
-
+	constructor() {
 		/**
 		 * Specifies whether the last added range was added as a backward or forward range.
 		 *
@@ -59,8 +33,8 @@ export default class Selection {
 		/**
 		 * Stores all ranges that are selected.
 		 *
-		 * @private
-		 * @member {Array.<engine.model.LiveRange>} engine.model.Selection#_ranges
+		 * @protected
+		 * @member {Array.<engine.model.Range>} engine.model.Selection#_ranges
 		 */
 		this._ranges = [];
 	}
@@ -69,27 +43,38 @@ export default class Selection {
 	 * Selection anchor. Anchor may be described as a position where the selection starts. Together with
 	 * {@link engine.model.Selection#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.
+	 *
+	 * Is set to `null` if there are no ranges in selection.
 	 *
 	 * @see engine.model.Selection#focus
-	 * @type {engine.model.LivePosition}
+	 * @type {engine.model.Position|null}
 	 */
 	get anchor() {
-		let range = this._ranges.length ? this._ranges[ this._ranges.length - 1 ] : this._getDefaultRange();
+		if ( this._ranges.length > 0 ) {
+			const range = this._ranges[ this._ranges.length - 1 ];
+
+			return this._lastRangeBackward ? range.end : range.start;
+		}
 
-		return this._lastRangeBackward ? range.end : range.start;
+		return null;
 	}
 
 	/**
 	 * Selection focus. Focus is a position where the selection ends.
 	 *
+	 * Is set to `null` if there are no ranges in selection.
+	 *
 	 * @see engine.model.Selection#anchor
-	 * @type {engine.model.LivePosition}
+	 * @type {engine.model.Position|null}
 	 */
 	get focus() {
-		let range = this._ranges.length ? this._ranges[ this._ranges.length - 1 ] : this._getDefaultRange();
+		if ( this._ranges.length > 0 ) {
+			const range = this._ranges[ this._ranges.length - 1 ];
 
-		return this._lastRangeBackward ? range.start : range.end;
+			return this._lastRangeBackward ? range.start : range.end;
+		}
+
+		return null;
 	}
 
 	/**
@@ -101,10 +86,7 @@ export default class Selection {
 	get isCollapsed() {
 		const length = this._ranges.length;
 
-		if ( length === 0 ) {
-			// Default range is collapsed.
-			return true;
-		} else if ( length === 1 ) {
+		if ( length === 1 ) {
 			return this._ranges[ 0 ].isCollapsed;
 		} else {
 			return false;
@@ -117,7 +99,7 @@ export default class Selection {
 	 * @type {Number}
      */
 	get rangeCount() {
-		return this._ranges.length ? this._ranges.length : 1;
+		return this._ranges.length;
 	}
 
 	/**
@@ -130,12 +112,12 @@ export default class Selection {
 	}
 
 	/**
-	 * Adds a range to the selection. Added range is copied and converted to {@link engine.model.LiveRange}. This means
-	 * that passed range is not saved in the Selection instance and you can safely operate on it.
+	 * Adds a range to the selection. Added range is copied. This means that passed range is not saved in `Selection`
+	 * instance and operating on it will not change `Selection` state.
 	 *
 	 * Accepts a flag describing in which way the selection is made - passed range might be selected from
-	 * {@link engine.model.Range#start} to {@link engine.model.Range#end} or from {@link engine.model.Range#end}
-	 * to {@link engine.model.Range#start}. The flag is used to set {@link engine.model.Selection#anchor} and
+	 * {@link engine.model.Range#start start} to {@link engine.model.Range#end end} or from {@link engine.model.Range#end end}
+	 * to {@link engine.model.Range#start start}. The flag is used to set {@link engine.model.Selection#anchor} and
 	 * {@link engine.model.Selection#focus} properties.
 	 *
 	 * @fires engine.model.Selection#change:range
@@ -151,35 +133,24 @@ export default class Selection {
 	}
 
 	/**
-	 * Unbinds all events previously bound by this selection or objects created by this selection.
-	 */
-	destroy() {
-		for ( let i = 0; i < this._ranges.length; i++ ) {
-			this._ranges[ i ].detach();
-		}
-	}
-
-	/**
-	 * Returns an iterator that contains copies of all ranges added to the selection.
+	 * Returns an iterator that iterates over copies of selection ranges.
 	 *
 	 * @returns {Iterator.<engine.model.Range>}
 	 */
 	*getRanges() {
-		if ( this._ranges.length ) {
-			for ( let range of this._ranges ) {
-				yield Range.createFromRange( range );
-			}
-		} else {
-			yield this._getDefaultRange();
+		for ( let range of this._ranges ) {
+			yield Range.createFromRange( range );
 		}
 	}
 
 	/**
-	 * Returns the first range in the selection. First range is the one which {@link engine.model.Range#start start} position
+	 * Returns a copy of the first range in the selection. First range is the one which {@link engine.model.Range#start start} position
 	 * {@link engine.model.Position#isBefore is before} start position of all other ranges (not to confuse with the first range
 	 * added to the selection).
 	 *
-	 * @returns {engine.model.Range}
+	 * Returns `null` if there are no ranges in selection.
+	 *
+	 * @returns {engine.model.Range|null}
 	 */
 	getFirstRange() {
 		let first = null;
@@ -192,17 +163,21 @@ export default class Selection {
 			}
 		}
 
-		return first ? Range.createFromRange( first ) : this._getDefaultRange();
+		return first ? Range.createFromRange( first ) : null;
 	}
 
 	/**
 	 * Returns the first position in the selection. First position is the position that {@link engine.model.Position#isBefore is before}
 	 * any other position in the selection ranges.
 	 *
-	 * @returns {engine.model.Position}
+	 * Returns `null` if there are no ranges in selection.
+	 *
+	 * @returns {engine.model.Position|null}
 	 */
 	getFirstPosition() {
-		return Position.createFromPosition( this.getFirstRange().start );
+		const first = this.getFirstRange();
+
+		return first ? Position.createFromPosition( first.start ) : null;
 	}
 
 	/**
@@ -211,7 +186,6 @@ export default class Selection {
 	 * @fires engine.model.Selection#change:range
 	 */
 	removeAllRanges() {
-		this.destroy();
 		this._ranges = [];
 
 		this.fire( 'change:range' );
@@ -223,16 +197,15 @@ export default class Selection {
 	 * describing in which way the selection is made (see {@link #addRange}).
 	 *
 	 * @fires engine.model.Selection#change:range
-	 * @param {Array.<engine.model.Range>} newRanges Array of ranges to set.
+	 * @param {Iterable.<engine.model.Range>} newRanges Iterable set of ranges that should be set.
 	 * @param {Boolean} [isLastBackward] 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 ) {
-		this.destroy();
 		this._ranges = [];
 
-		for ( let i = 0; i < newRanges.length; i++ ) {
-			this._pushRange( newRanges[ i ] );
+		for ( let range of newRanges ) {
+			this._pushRange( range );
 		}
 
 		this._lastRangeBackward = !!isLastBackward;
@@ -268,6 +241,15 @@ export default class Selection {
 	 * first parameter is a node.
 	 */
 	setFocus( nodeOrPosition, offset ) {
+		if ( this.anchor === null ) {
+			/**
+			 * Cannot set selection focus if there are no ranges in selection.
+			 *
+			 * @error selection-setFocus-no-ranges
+			 */
+			throw new CKEditorError( 'selection-setFocus-no-ranges: Cannot set selection focus if there are no ranges in selection.' );
+		}
+
 		const newFocus = Position.createAt( nodeOrPosition, offset );
 
 		if ( newFocus.compareWith( this.focus ) == 'SAME' ) {
@@ -277,8 +259,7 @@ export default class Selection {
 		const anchor = this.anchor;
 
 		if ( this._ranges.length ) {
-			// TODO Replace with _popRange, so child classes can override this (needed for #329).
-			this._ranges.pop().detach();
+			this._popRange();
 		}
 
 		if ( newFocus.compareWith( anchor ) == 'BEFORE' ) {
@@ -289,95 +270,28 @@ export default class Selection {
 	}
 
 	/**
-	 * Removes all attributes from the selection.
-	 *
-	 * @fires engine.model.Selection#change:attribute
-	 */
-	clearAttributes() {
-		this._attrs.clear();
-		this._setStoredAttributesTo( new Map() );
-
-		this.fire( 'change:attribute' );
-	}
-
-	/**
-	 * Gets an attribute value for given key or undefined it that attribute is not set on selection.
-	 *
-	 * @param {String} key Key of attribute to look for.
-	 * @returns {*} Attribute value or null.
-	 */
-	getAttribute( key ) {
-		return this._attrs.get( key );
-	}
-
-	/**
-	 * Returns iterator that iterates over this selection attributes.
-	 *
-	 * @returns {Iterable.<*>}
-	 */
-	getAttributes() {
-		return this._attrs[ Symbol.iterator ]();
-	}
-
-	/**
-	 * Checks if the selection has an attribute for given key.
-	 *
-	 * @param {String} key Key of attribute to check.
-	 * @returns {Boolean} `true` if attribute with given key is set on selection, `false` otherwise.
-	 */
-	hasAttribute( key ) {
-		return this._attrs.has( key );
-	}
-
-	/**
-	 * Removes an attribute with given key from the selection.
-	 *
-	 * @fires engine.model.Selection#change:attribute
-	 * @param {String} key Key of attribute to remove.
-	 */
-	removeAttribute( key ) {
-		this._attrs.delete( key );
-		this._removeStoredAttribute( key );
-
-		this.fire( 'change:attribute' );
-	}
-
-	/**
-	 * Sets attribute on the selection. If attribute with the same key already is set, it overwrites its values.
+	 * Creates and returns an instance of {@link engine.model.Selection} that is a clone of given selection,
+	 * meaning that it has same ranges and same direction as it.
 	 *
-	 * @fires engine.model.Selection#change:attribute
-	 * @param {String} key Key of attribute to set.
-	 * @param {*} value Attribute value.
+	 * @params {engine.model.Selection} otherSelection Selection to be cloned.
+	 * @returns {engine.model.Selection} `Selection` instance that is a clone of given selection.
 	 */
-	setAttribute( key, value ) {
-		this._attrs.set( key, value );
-		this._storeAttribute( key, value );
+	static createFromSelection( otherSelection ) {
+		const selection = new this();
+		selection.setRanges( otherSelection.getRanges(), otherSelection.isBackward );
 
-		this.fire( 'change:attribute' );
+		return selection;
 	}
 
 	/**
-	 * Removes all attributes from the selection and sets given attributes.
+	 * Checks if given range intersects with ranges that are already in the selection. Throws an error if it does.
+	 * This method is extracted from {@link engine.model.Selection#_pushRange } so it is easier to override it.
 	 *
-	 * @fires engine.model.Selection#change:attribute
-	 * @param {Iterable|Object} attrs Iterable object containing attributes to be set.
-	 */
-	setAttributesTo( attrs ) {
-		this._attrs = toMap( attrs );
-		this._setStoredAttributesTo( this._attrs );
-
-		this.fire( 'change:attribute' );
-	}
-
-	/**
-	 * Converts given range to {@link engine.model.LiveRange} and adds it to internal ranges array. Throws an error
-	 * if given range is intersecting with any range that is already stored in this selection.
-	 *
-	 * @private
-	 * @param {engine.model.Range} range Range to add.
+	 * @param {engine.model.Range} range Range to check.
+	 * @protected
 	 */
-	_pushRange( range ) {
-		for ( let i = 0; i < this._ranges.length ; i++ ) {
+	_checkRange( range ) {
+		for ( let i = 0; i < this._ranges.length; i++ ) {
 			if ( range.isIntersecting( this._ranges[ i ] ) ) {
 				/**
 				 * Trying to add a range that intersects with another range from selection.
@@ -392,221 +306,34 @@ export default class Selection {
 				);
 			}
 		}
-
-		this._ranges.push( LiveRange.createFromRange( range ) );
 	}
 
 	/**
-	 * Iterates through all attributes stored in current selection's parent.
-	 *
-	 * @returns {Iterable.<*>}
-	 */
-	*_getStoredAttributes() {
-		const selectionParent = this.getFirstPosition().parent;
-
-		if ( this.isCollapsed && selectionParent.getChildCount() === 0 ) {
-			for ( let attr of selectionParent.getAttributes() ) {
-				if ( attr[ 0 ].indexOf( storePrefix ) === 0 ) {
-					const realKey = attr[ 0 ].substr( storePrefix.length );
-
-					yield [ realKey, attr[ 1 ] ];
-				}
-			}
-		}
-	}
-
-	/**
-	 * Removes attribute with given key from attributes stored in current selection's parent node.
-	 *
-	 * @private
-	 * @param {String} key Key of attribute to remove.
-	 */
-	_removeStoredAttribute( key ) {
-		const selectionParent = this.getFirstPosition().parent;
-
-		if ( this.isCollapsed && selectionParent.getChildCount() === 0 ) {
-			const storeKey = Selection._getStoreAttributeKey( key );
-
-			this._document.enqueueChanges( () => {
-				this._document.batch().removeAttr( storeKey, selectionParent );
-			} );
-		}
-	}
-
-	/**
-	 * Stores given attribute key and value in current selection's parent node if the selection is collapsed and
-	 * the parent node is empty.
-	 *
-	 * @private
-	 * @param {String} key Key of attribute to set.
-	 * @param {*} value Attribute value.
-	 */
-	_storeAttribute( key, value ) {
-		const selectionParent = this.getFirstPosition().parent;
-
-		if ( this.isCollapsed && selectionParent.getChildCount() === 0 ) {
-			const storeKey = Selection._getStoreAttributeKey( key );
-
-			this._document.enqueueChanges( () => {
-				this._document.batch().setAttr( storeKey, value, selectionParent );
-			} );
-		}
-	}
-
-	/**
-	 * Sets selection attributes stored in current selection's parent node to given set of attributes.
-	 *
-	 * @param {Iterable|Object} attrs Iterable object containing attributes to be set.
-	 * @private
-	 */
-	_setStoredAttributesTo( attrs ) {
-		const selectionParent = this.getFirstPosition().parent;
-
-		if ( this.isCollapsed && selectionParent.getChildCount() === 0 ) {
-			this._document.enqueueChanges( () => {
-				const batch = this._document.batch();
-
-				for ( let attr of this._getStoredAttributes() ) {
-					const storeKey = Selection._getStoreAttributeKey( attr[ 0 ] );
-
-					batch.removeAttr( storeKey, selectionParent );
-				}
-
-				for ( let attr of attrs ) {
-					const storeKey = Selection._getStoreAttributeKey( attr[ 0 ] );
-
-					batch.setAttr( storeKey, attr[ 1 ], selectionParent );
-				}
-			} );
-		}
-	}
-
-	/**
-	 * Updates this selection attributes based on it's position in the model.
+	 * Removes most recently added range from the selection.
 	 *
 	 * @protected
 	 */
-	_updateAttributes() {
-		const position = this.getFirstPosition();
-		const positionParent = position.parent;
-
-		let attrs = null;
-
-		if ( !this.isCollapsed ) {
-			// 1. If selection is a range...
-			const range = this.getFirstRange();
-
-			// ...look for a first character node in that range and take attributes from it.
-			for ( let item of range ) {
-				// This is not an optimal solution because of https://github.com/ckeditor/ckeditor5-engine/issues/454.
-				// It can be done better by using `break;` instead of checking `attrs === null`.
-				if ( item.type == 'TEXT' && attrs === null ) {
-					attrs = item.item.getAttributes();
-				}
-			}
-		} else {
-			// 2. If the selection is a caret or the range does not contain a character node...
-
-			const nodeBefore = positionParent.getChild( position.offset - 1 );
-			const nodeAfter = positionParent.getChild( position.offset );
-
-			// ...look at the node before caret and take attributes from it if it is a character node.
-			attrs = getAttrsIfCharacter( nodeBefore );
-
-			// 3. If not, look at the node after caret...
-			if ( !attrs ) {
-				attrs = getAttrsIfCharacter( nodeAfter );
-			}
-
-			// 4. If not, try to find the first character on the left, that is in the same node.
-			if ( !attrs ) {
-				let node = nodeBefore;
-
-				while ( node && !attrs ) {
-					node = node.previousSibling;
-					attrs = getAttrsIfCharacter( node );
-				}
-			}
-
-			// 5. If not found, try to find the first character on the right, that is in the same node.
-			if ( !attrs ) {
-				let node = nodeAfter;
-
-				while ( node && !attrs ) {
-					node = node.nextSibling;
-					attrs = getAttrsIfCharacter( node );
-				}
-			}
-
-			// 6. If not found, selection should retrieve attributes from parent.
-			if ( !attrs ) {
-				attrs = this._getStoredAttributes();
-			}
-		}
-
-		if ( attrs ) {
-			this._attrs = new Map( attrs );
-		} else {
-			this.clearAttributes();
-		}
-
-		function getAttrsIfCharacter( node ) {
-			if ( node instanceof CharacterProxy ) {
-				return node.getAttributes();
-			}
-
-			return null;
-		}
-
-		this.fire( 'change:attribute' );
+	_popRange() {
+		this._ranges.pop();
 	}
 
 	/**
-	 * Returns a default range for this selection. The default range is a collapsed range that starts and ends
-	 * at the beginning of this selection's document {@link engine.model.Document#_getDefaultRoot default root}.
-	 * This "artificial" range is important for algorithms that base on selection, so they won't break or need
-	 * special logic if there are no real ranges in the selection.
-	 *
-	 * @private
-	 * @returns {engine.model.Range}
-	 */
-	_getDefaultRange() {
-		const defaultRoot = this._document._getDefaultRoot();
-
-		// Find the first position where the selection can be put.
-		for ( let position of Range.createFromElement( defaultRoot ).getPositions() ) {
-			if ( this._document.schema.check( { name: '$text', inside: position } ) ) {
-				return new Range( position, position );
-			}
-		}
-
-		const position = new Position( defaultRoot, [ 0 ] );
-
-		return new Range( position, position );
-	}
-
-	/**
-	 * Generates and returns an attribute key for selection attributes store, basing on original attribute key.
+	 * Adds given range to internal {@link engine.model.Selection#_ranges ranges array}. Throws an error
+	 * if given range is intersecting with any range that is already stored in this selection.
 	 *
-	 * @param {String} key Attribute key to convert.
-	 * @returns {String} Converted attribute key, applicable for selection store.
+	 * @protected
+	 * @param {engine.model.Range} range Range to add.
 	 */
-	static _getStoreAttributeKey( key ) {
-		return storePrefix + key;
+	_pushRange( range ) {
+		this._checkRange( range );
+		this._ranges.push( Range.createFromRange( range ) );
 	}
 }
 
 mix( Selection, EmitterMixin );
 
 /**
- * Fired whenever selection ranges are changed through {@link engine.model.Selection Selection API}. Not fired when
- * {@link engine.model.LiveRange live ranges} inserted in selection change because of Tree Model changes.
+ * Fired whenever selection ranges are changed through {@link engine.model.Selection Selection API}.
  *
  * @event engine.model.Selection#change:range
  */
-
-/**
- * Fired whenever selection attributes are changed.
- *
- * @event engine.model.Selection#change:attribute
- */

+ 5 - 0
packages/ckeditor5-engine/tests/model/document/document.js

@@ -15,6 +15,7 @@ import Batch from '/ckeditor5/engine/model/batch.js';
 import Delta from '/ckeditor5/engine/model/delta/delta.js';
 import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
 import count from '/ckeditor5/utils/count.js';
+import { jsonParseStringify } from '/tests/engine/model/_utils/utils.js';
 
 describe( 'Document', () => {
 	let doc;
@@ -253,4 +254,8 @@ describe( 'Document', () => {
 			expect( doc._getDefaultRoot() ).to.equal( rootA );
 		} );
 	} );
+
+	it( 'should be correctly converted to json', () => {
+		expect( jsonParseStringify( doc ).selection ).to.equal( '[engine.model.LiveSelection]' );
+	} );
 } );

+ 681 - 0
packages/ckeditor5-engine/tests/model/liveselection.js

@@ -0,0 +1,681 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* bender-tags: model */
+
+'use strict';
+
+import Document from '/ckeditor5/engine/model/document.js';
+import Element from '/ckeditor5/engine/model/element.js';
+import Text from '/ckeditor5/engine/model/text.js';
+import Range from '/ckeditor5/engine/model/range.js';
+import Position from '/ckeditor5/engine/model/position.js';
+import LiveRange from '/ckeditor5/engine/model/liverange.js';
+import LiveSelection from '/ckeditor5/engine/model/liveselection.js';
+import InsertOperation from '/ckeditor5/engine/model/operation/insertoperation.js';
+import MoveOperation from '/ckeditor5/engine/model/operation/moveoperation.js';
+import testUtils from '/tests/ckeditor5/_utils/utils.js';
+import { wrapInDelta } from '/tests/engine/model/_utils/utils.js';
+
+testUtils.createSinonSandbox();
+
+describe( 'LiveSelection', () => {
+	let attrFooBar;
+
+	before( () => {
+		attrFooBar = { foo: 'bar' };
+	} );
+
+	let doc, root, selection, liveRange, range;
+
+	beforeEach( () => {
+		doc = new Document();
+		root = doc.createRoot();
+		root.appendChildren( [
+			new Element( 'p' ),
+			new Element( 'p' ),
+			new Element( 'p', [], 'foobar' ),
+			new Element( 'p' ),
+			new Element( 'p' ),
+			new Element( 'p' ),
+			new Element( 'p', [], 'foobar' )
+		] );
+		selection = doc.selection;
+		doc.schema.registerItem( 'p', '$block' );
+
+		liveRange = new LiveRange( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) );
+		range = new Range( new Position( root, [ 2 ] ), new Position( root, [ 2, 2 ] ) );
+	} );
+
+	afterEach( () => {
+		doc.destroy();
+		liveRange.detach();
+	} );
+
+	describe( 'default range', () => {
+		it( 'should go to the first editable element', () => {
+			const ranges = Array.from( selection.getRanges() );
+
+			expect( ranges.length ).to.equal( 1 );
+			expect( selection.anchor.isEqual( new Position( root, [ 0, 0 ] ) ) ).to.be.true;
+			expect( selection.focus.isEqual( new Position( root, [ 0, 0 ] ) ) ).to.be.true;
+			expect( selection ).to.have.property( 'isBackward', false );
+		} );
+
+		it( 'should be set to the beginning of the doc if there is no editable element', () => {
+			doc = new Document();
+			root = doc.createRoot();
+			root.insertChildren( 0, 'foobar' );
+			selection = doc.selection;
+
+			const ranges = Array.from( selection.getRanges() );
+
+			expect( ranges.length ).to.equal( 1 );
+			expect( selection.anchor.isEqual( new Position( root, [ 0 ] ) ) ).to.be.true;
+			expect( selection.focus.isEqual( new Position( root, [ 0 ] ) ) ).to.be.true;
+			expect( selection ).to.have.property( 'isBackward', false );
+			expect( selection._attrs ).to.be.instanceof( Map );
+			expect( selection._attrs.size ).to.equal( 0 );
+		} );
+
+		it( 'should skip element when you can not put selection', () => {
+			doc = new Document();
+			root = doc.createRoot();
+			root.insertChildren( 0, [
+				new Element( 'img' ),
+				new Element( 'p', [], 'foobar' )
+			] );
+			doc.schema.registerItem( 'img' );
+			doc.schema.registerItem( 'p', '$block' );
+			selection = doc.selection;
+
+			const ranges = Array.from( selection.getRanges() );
+
+			expect( ranges.length ).to.equal( 1 );
+			expect( selection.anchor.isEqual( new Position( root, [ 1, 0 ] ) ) ).to.be.true;
+			expect( selection.focus.isEqual( new Position( root, [ 1, 0 ] ) ) ).to.be.true;
+			expect( selection ).to.have.property( 'isBackward', false );
+			expect( selection._attrs ).to.be.instanceof( Map );
+			expect( selection._attrs.size ).to.equal( 0 );
+		} );
+	} );
+
+	describe( 'isCollapsed', () => {
+		it( 'should return true for default range', () => {
+			expect( selection.isCollapsed ).to.be.true;
+		} );
+	} );
+
+	describe( 'rangeCount', () => {
+		it( 'should return proper range count', () => {
+			expect( selection.rangeCount ).to.equal( 1 );
+
+			selection.addRange( new Range( new Position( root, [ 0 ] ), new Position( root, [ 0 ] ) ) );
+
+			expect( selection.rangeCount ).to.equal( 1 );
+
+			selection.addRange( new Range( new Position( root, [ 2 ] ), new Position( root, [ 2 ] ) ) );
+
+			expect( selection.rangeCount ).to.equal( 2 );
+		} );
+	} );
+
+	describe( 'addRange', () => {
+		it( 'should convert added Range to LiveRange', () => {
+			selection.addRange( range );
+
+			const ranges = selection._ranges;
+
+			expect( ranges[ 0 ] ).to.be.instanceof( LiveRange );
+		} );
+	} );
+
+	describe( 'collapse', () => {
+		it( 'detaches all existing ranges', () => {
+			selection.addRange( range );
+			selection.addRange( liveRange );
+
+			const spy = testUtils.sinon.spy( LiveRange.prototype, 'detach' );
+			selection.collapse( root );
+
+			expect( spy.calledTwice ).to.be.true;
+		} );
+	} );
+
+	describe( 'destroy', () => {
+		it( 'should unbind all events', () => {
+			selection.addRange( liveRange );
+			selection.addRange( range );
+
+			const ranges = selection._ranges;
+
+			sinon.spy( ranges[ 0 ], 'detach' );
+			sinon.spy( ranges[ 1 ], 'detach' );
+
+			selection.destroy();
+
+			expect( ranges[ 0 ].detach.called ).to.be.true;
+			expect( ranges[ 1 ].detach.called ).to.be.true;
+
+			ranges[ 0 ].detach.restore();
+			ranges[ 1 ].detach.restore();
+		} );
+	} );
+
+	describe( 'setFocus', () => {
+		it( 'modifies default range', () => {
+			const startPos = selection.getFirstPosition();
+			const endPos = Position.createAt( root, 'END' );
+
+			selection.setFocus( endPos );
+
+			expect( selection.anchor.compareWith( startPos ) ).to.equal( 'SAME' );
+			expect( selection.focus.compareWith( endPos ) ).to.equal( 'SAME' );
+		} );
+
+		it( 'detaches the range it replaces', () => {
+			const startPos = Position.createAt( root, 1 );
+			const endPos = Position.createAt( root, 2 );
+			const newEndPos = Position.createAt( root, 4 );
+			const spy = testUtils.sinon.spy( LiveRange.prototype, 'detach' );
+
+			selection.addRange( new Range( startPos, endPos ) );
+
+			selection.setFocus( newEndPos );
+
+			expect( spy.calledOnce ).to.be.true;
+		} );
+	} );
+
+	describe( 'removeAllRanges', () => {
+		let spy, ranges;
+
+		beforeEach( () => {
+			selection.addRange( liveRange );
+			selection.addRange( range );
+
+			spy = sinon.spy();
+			selection.on( 'change:range', spy );
+
+			ranges = selection._ranges;
+
+			sinon.spy( ranges[ 0 ], 'detach' );
+			sinon.spy( ranges[ 1 ], 'detach' );
+
+			selection.removeAllRanges();
+		} );
+
+		afterEach( () => {
+			ranges[ 0 ].detach.restore();
+			ranges[ 1 ].detach.restore();
+		} );
+
+		it( 'should remove all stored ranges (and reset to default range)', () => {
+			expect( Array.from( selection.getRanges() ).length ).to.equal( 1 );
+			expect( selection.anchor.isEqual( new Position( root, [ 0, 0 ] ) ) ).to.be.true;
+			expect( selection.focus.isEqual( new Position( root, [ 0, 0 ] ) ) ).to.be.true;
+		} );
+
+		it( 'should detach ranges', () => {
+			expect( ranges[ 0 ].detach.called ).to.be.true;
+			expect( ranges[ 1 ].detach.called ).to.be.true;
+		} );
+	} );
+
+	describe( 'setRanges', () => {
+		let newRanges, spy, oldRanges;
+
+		before( () => {
+			newRanges = [
+				new Range( new Position( root, [ 4 ] ), new Position( root, [ 5 ] ) ),
+				new Range( new Position( root, [ 5, 0 ] ), new Position( root, [ 6, 0 ] ) )
+			];
+		} );
+
+		beforeEach( () => {
+			selection.addRange( liveRange );
+			selection.addRange( range );
+
+			spy = sinon.spy();
+			selection.on( 'change:range', spy );
+
+			oldRanges = selection._ranges;
+
+			sinon.spy( oldRanges[ 0 ], 'detach' );
+			sinon.spy( oldRanges[ 1 ], 'detach' );
+		} );
+
+		afterEach( () => {
+			oldRanges[ 0 ].detach.restore();
+			oldRanges[ 1 ].detach.restore();
+		} );
+
+		it( 'should detach removed ranges', () => {
+			selection.setRanges( newRanges );
+			expect( oldRanges[ 0 ].detach.called ).to.be.true;
+			expect( oldRanges[ 1 ].detach.called ).to.be.true;
+		} );
+	} );
+
+	describe( 'getFirstRange', () => {
+		it( 'should return default range if no ranges were added', () => {
+			const firstRange = selection.getFirstRange();
+
+			expect( firstRange.start.isEqual( new Position( root, [ 0, 0 ] ) ) );
+			expect( firstRange.end.isEqual( new Position( root, [ 0, 0 ] ) ) );
+		} );
+	} );
+
+	describe( 'getFirstPosition', () => {
+		it( 'should return start position of default range if no ranges were added', () => {
+			const firstPosition = selection.getFirstPosition();
+
+			expect( firstPosition.isEqual( new Position( root, [ 0, 0 ] ) ) );
+		} );
+	} );
+
+	describe( 'createFromSelection', () => {
+		it( 'should return a LiveSelection instance', () => {
+			selection.addRange( range, true );
+
+			expect( LiveSelection.createFromSelection( selection ) ).to.be.instanceof( LiveSelection );
+		} );
+	} );
+
+	// LiveSelection uses LiveRanges so here are only simple test to see if integration is
+	// working well, without getting into complicated corner cases.
+	describe( 'after applying an operation should get updated and not fire update event', () => {
+		let spy;
+
+		beforeEach( () => {
+			root.insertChildren( 0, [ new Element( 'ul', [], 'abcdef' ), new Element( 'p', [], 'foobar' ), 'xyz' ] );
+
+			selection.addRange( new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 1, 4 ] ) ) );
+
+			spy = sinon.spy();
+			selection.on( 'change:range', spy );
+		} );
+
+		describe( 'InsertOperation', () => {
+			it( 'before selection', () => {
+				doc.applyOperation( wrapInDelta(
+					new InsertOperation(
+						new Position( root, [ 0, 1 ] ),
+						'xyz',
+						doc.version
+					)
+				) );
+
+				let range = selection._ranges[ 0 ];
+
+				expect( range.start.path ).to.deep.equal( [ 0, 5 ] );
+				expect( range.end.path ).to.deep.equal( [ 1, 4 ] );
+				expect( spy.called ).to.be.false;
+			} );
+
+			it( 'inside selection', () => {
+				doc.applyOperation( wrapInDelta(
+					new InsertOperation(
+						new Position( root, [ 1, 0 ] ),
+						'xyz',
+						doc.version
+					)
+				) );
+
+				let range = selection._ranges[ 0 ];
+
+				expect( range.start.path ).to.deep.equal( [ 0, 2 ] );
+				expect( range.end.path ).to.deep.equal( [ 1, 7 ] );
+				expect( spy.called ).to.be.false;
+			} );
+		} );
+
+		describe( 'MoveOperation', () => {
+			it( 'move range from before a selection', () => {
+				doc.applyOperation( wrapInDelta(
+					new MoveOperation(
+						new Position( root, [ 0, 0 ] ),
+						2,
+						new Position( root, [ 2 ] ),
+						doc.version
+					)
+				) );
+
+				let range = selection._ranges[ 0 ];
+
+				expect( range.start.path ).to.deep.equal( [ 0, 0 ] );
+				expect( range.end.path ).to.deep.equal( [ 1, 4 ] );
+				expect( spy.called ).to.be.false;
+			} );
+
+			it( 'moved into before a selection', () => {
+				doc.applyOperation( wrapInDelta(
+					new MoveOperation(
+						new Position( root, [ 2 ] ),
+						2,
+						new Position( root, [ 0, 0 ] ),
+						doc.version
+					)
+				) );
+
+				let range = selection._ranges[ 0 ];
+
+				expect( range.start.path ).to.deep.equal( [ 0, 4 ] );
+				expect( range.end.path ).to.deep.equal( [ 1, 4 ] );
+				expect( spy.called ).to.be.false;
+			} );
+
+			it( 'move range from inside of selection', () => {
+				doc.applyOperation( wrapInDelta(
+					new MoveOperation(
+						new Position( root, [ 1, 0 ] ),
+						2,
+						new Position( root, [ 2 ] ),
+						doc.version
+					)
+				) );
+
+				let range = selection._ranges[ 0 ];
+
+				expect( range.start.path ).to.deep.equal( [ 0, 2 ] );
+				expect( range.end.path ).to.deep.equal( [ 1, 2 ] );
+				expect( spy.called ).to.be.false;
+			} );
+
+			it( 'moved range intersects with selection', () => {
+				doc.applyOperation( wrapInDelta(
+					new MoveOperation(
+						new Position( root, [ 1, 3 ] ),
+						2,
+						new Position( root, [ 4 ] ),
+						doc.version
+					)
+				) );
+
+				let range = selection._ranges[ 0 ];
+
+				expect( range.start.path ).to.deep.equal( [ 0, 2 ] );
+				expect( range.end.path ).to.deep.equal( [ 1, 3 ] );
+				expect( spy.called ).to.be.false;
+			} );
+
+			it( 'split inside selection (do not break selection)', () => {
+				doc.applyOperation( wrapInDelta(
+					new InsertOperation(
+						new Position( root, [ 2 ] ),
+						new Element( 'p' ),
+						doc.version
+					)
+				) );
+
+				doc.applyOperation( wrapInDelta(
+					new MoveOperation(
+						new Position( root, [ 1, 2 ] ),
+						4,
+						new Position( root, [ 2, 0 ] ),
+						doc.version
+					)
+				) );
+
+				let range = selection._ranges[ 0 ];
+
+				expect( range.start.path ).to.deep.equal( [ 0, 2 ] );
+				expect( range.end.path ).to.deep.equal( [ 2, 2 ] );
+				expect( spy.called ).to.be.false;
+			} );
+		} );
+	} );
+
+	describe( 'attributes interface', () => {
+		let fullP, emptyP, rangeInFullP, rangeInEmptyP;
+
+		beforeEach( () => {
+			root.insertChildren( 0, [
+				new Element( 'p', [], 'foobar' ),
+				new Element( 'p', [], [] )
+			] );
+
+			fullP = root.getChild( 0 );
+			emptyP = root.getChild( 1 );
+
+			rangeInFullP = new Range( new Position( root, [ 0, 4 ] ), new Position( root, [ 0, 4 ] ) );
+			rangeInEmptyP = new Range( new Position( root, [ 1, 0 ] ), new Position( root, [ 1, 0 ] ) );
+		} );
+
+		describe( 'setAttribute', () => {
+			it( 'should set given attribute on the selection', () => {
+				selection.setRanges( [ rangeInFullP ] );
+				selection.setAttribute( 'foo', 'bar' );
+
+				expect( selection.getAttribute( 'foo' ) ).to.equal( 'bar' );
+				expect( fullP.hasAttribute( LiveSelection._getStoreAttributeKey( 'foo' ) ) ).to.be.false;
+			} );
+
+			it( 'should store attribute if the selection is in empty node', () => {
+				selection.setRanges( [ rangeInEmptyP ] );
+				selection.setAttribute( 'foo', 'bar' );
+
+				expect( selection.getAttribute( 'foo' ) ).to.equal( 'bar' );
+
+				expect( emptyP.getAttribute( LiveSelection._getStoreAttributeKey( 'foo' ) ) ).to.equal( 'bar' );
+			} );
+
+			it( 'should fire change:attribute event', () => {
+				let spy = sinon.spy();
+				selection.on( 'change:attribute', spy );
+
+				selection.setAttribute( 'foo', 'bar' );
+
+				expect( spy.called ).to.be.true;
+			} );
+		} );
+
+		describe( 'hasAttribute', () => {
+			it( 'should return true if element contains attribute with given key', () => {
+				selection.setRanges( [ rangeInFullP ] );
+				selection.setAttribute( 'foo', 'bar' );
+
+				expect( selection.hasAttribute( 'foo' ) ).to.be.true;
+			} );
+
+			it( 'should return false if element does not contain attribute with given key', () => {
+				expect( selection.hasAttribute( 'abc' ) ).to.be.false;
+			} );
+		} );
+
+		describe( 'getAttribute', () => {
+			it( 'should return undefined if element does not contain given attribute', () => {
+				expect( selection.getAttribute( 'abc' ) ).to.be.undefined;
+			} );
+		} );
+
+		describe( 'getAttributes', () => {
+			it( 'should return an iterator that iterates over all attributes set on the text fragment', () => {
+				selection.setRanges( [ rangeInFullP ] );
+				selection.setAttribute( 'foo', 'bar' );
+				selection.setAttribute( 'abc', 'xyz' );
+
+				let attrs = Array.from( selection.getAttributes() );
+
+				expect( attrs ).to.deep.equal( [ [ 'foo', 'bar' ], [ 'abc', 'xyz' ] ] );
+			} );
+		} );
+
+		describe( 'setAttributesTo', () => {
+			it( 'should remove all attributes set on element and set the given ones', () => {
+				selection.setRanges( [ rangeInFullP ] );
+				selection.setAttribute( 'abc', 'xyz' );
+				selection.setAttributesTo( { foo: 'bar' } );
+
+				expect( selection.getAttribute( 'foo' ) ).to.equal( 'bar' );
+				expect( selection.getAttribute( 'abc' ) ).to.be.undefined;
+
+				expect( fullP.hasAttribute( LiveSelection._getStoreAttributeKey( 'foo' ) ) ).to.be.false;
+				expect( fullP.hasAttribute( LiveSelection._getStoreAttributeKey( 'abc' ) ) ).to.be.false;
+			} );
+
+			it( 'should remove all stored attributes and store the given ones if the selection is in empty node', () => {
+				selection.setRanges( [ rangeInEmptyP ] );
+				selection.setAttribute( 'abc', 'xyz' );
+				selection.setAttributesTo( { foo: 'bar' } );
+
+				expect( selection.getAttribute( 'foo' ) ).to.equal( 'bar' );
+				expect( selection.getAttribute( 'abc' ) ).to.be.undefined;
+
+				expect( emptyP.getAttribute( LiveSelection._getStoreAttributeKey( 'foo' ) ) ).to.equal( 'bar' );
+				expect( emptyP.hasAttribute( LiveSelection._getStoreAttributeKey( 'abc' ) ) ).to.be.false;
+			} );
+
+			it( 'should fire change:attribute event', () => {
+				let spy = sinon.spy();
+				selection.on( 'change:attribute', spy );
+
+				selection.setAttributesTo( { foo: 'bar' } );
+
+				expect( spy.called ).to.be.true;
+			} );
+		} );
+
+		describe( 'removeAttribute', () => {
+			it( 'should remove attribute set on the text fragment', () => {
+				selection.setRanges( [ rangeInFullP ] );
+				selection.setAttribute( 'foo', 'bar' );
+				selection.removeAttribute( 'foo' );
+
+				expect( selection.getAttribute( 'foo' ) ).to.be.undefined;
+
+				expect( fullP.hasAttribute( LiveSelection._getStoreAttributeKey( 'foo' ) ) ).to.be.false;
+			} );
+
+			it( 'should remove stored attribute if the selection is in empty node', () => {
+				selection.setRanges( [ rangeInEmptyP ] );
+				selection.setAttribute( 'foo', 'bar' );
+				selection.removeAttribute( 'foo' );
+
+				expect( selection.getAttribute( 'foo' ) ).to.be.undefined;
+
+				expect( emptyP.hasAttribute( LiveSelection._getStoreAttributeKey( 'foo' ) ) ).to.be.false;
+			} );
+
+			it( 'should fire change:attribute event', () => {
+				let spy = sinon.spy();
+				selection.on( 'change:attribute', spy );
+
+				selection.removeAttribute( 'foo' );
+
+				expect( spy.called ).to.be.true;
+			} );
+		} );
+
+		describe( 'clearAttributes', () => {
+			it( 'should remove all attributes from the element', () => {
+				selection.setRanges( [ rangeInFullP ] );
+				selection.setAttribute( 'foo', 'bar' );
+				selection.setAttribute( 'abc', 'xyz' );
+
+				selection.clearAttributes();
+
+				expect( selection.getAttribute( 'foo' ) ).to.be.undefined;
+				expect( selection.getAttribute( 'abc' ) ).to.be.undefined;
+
+				expect( fullP.hasAttribute( LiveSelection._getStoreAttributeKey( 'foo' ) ) ).to.be.false;
+				expect( fullP.hasAttribute( LiveSelection._getStoreAttributeKey( 'abc' ) ) ).to.be.false;
+			} );
+
+			it( 'should remove all stored attributes if the selection is in empty node', () => {
+				selection.setRanges( [ rangeInEmptyP ] );
+				selection.setAttribute( 'foo', 'bar' );
+				selection.setAttribute( 'abc', 'xyz' );
+
+				selection.clearAttributes();
+
+				expect( selection.getAttribute( 'foo' ) ).to.be.undefined;
+				expect( selection.getAttribute( 'abc' ) ).to.be.undefined;
+
+				expect( emptyP.hasAttribute( LiveSelection._getStoreAttributeKey( 'foo' ) ) ).to.be.false;
+				expect( emptyP.hasAttribute( LiveSelection._getStoreAttributeKey( 'abc' ) ) ).to.be.false;
+			} );
+
+			it( 'should fire change:attribute event', () => {
+				let spy = sinon.spy();
+				selection.on( 'change:attribute', spy );
+
+				selection.clearAttributes();
+
+				expect( spy.called ).to.be.true;
+			} );
+		} );
+	} );
+
+	describe( '_updateAttributes', () => {
+		beforeEach( () => {
+			root.insertChildren( 0, [
+				new Element( 'p', { p: true } ),
+				new Text( 'a', { a: true } ),
+				new Element( 'p', { p: true } ),
+				new Text( 'b', { b: true } ),
+				new Text( 'c', { c: true } ),
+				new Element( 'p', [], [
+					new Text( 'd', { d: true } )
+				] ),
+				new Element( 'p', { p: true } ),
+				new Text( 'e', { e: true } )
+			] );
+		} );
+
+		it( 'if selection is a range, should find first character in it and copy it\'s attributes', () => {
+			selection.setRanges( [ new Range( new Position( root, [ 2 ] ), new Position( root, [ 5 ] ) ) ] );
+
+			expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'b', true ] ] );
+
+			// Step into elements when looking for first character:
+			selection.setRanges( [ new Range( new Position( root, [ 5 ] ), new Position( root, [ 7 ] ) ) ] );
+
+			expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'd', true ] ] );
+		} );
+
+		it( 'if selection is collapsed it should seek a character to copy that character\'s attributes', () => {
+			// Take styles from character before selection.
+			selection.setRanges( [ new Range( new Position( root, [ 2 ] ), new Position( root, [ 2 ] ) ) ] );
+			expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'a', true ] ] );
+
+			// If there are none,
+			// Take styles from character after selection.
+			selection.setRanges( [ new Range( new Position( root, [ 3 ] ), new Position( root, [ 3 ] ) ) ] );
+			expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'b', true ] ] );
+
+			// If there are none,
+			// Look from the selection position to the beginning of node looking for character to take attributes from.
+			selection.setRanges( [ new Range( new Position( root, [ 6 ] ), new Position( root, [ 6 ] ) ) ] );
+			expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'c', true ] ] );
+
+			// If there are none,
+			// Look from the selection position to the end of node looking for character to take attributes from.
+			selection.setRanges( [ new Range( new Position( root, [ 0 ] ), new Position( root, [ 0 ] ) ) ] );
+			expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'a', true ] ] );
+
+			// If there are no characters to copy attributes from, use stored attributes.
+			selection.setRanges( [ new Range( new Position( root, [ 0, 0 ] ), new Position( root, [ 0, 0 ] ) ) ] );
+			expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [] );
+		} );
+
+		it( 'should fire change:attribute event', () => {
+			let spy = sinon.spy();
+			selection.on( 'change:attribute', spy );
+
+			selection.setRanges( [ new Range( new Position( root, [ 2 ] ), new Position( root, [ 5 ] ) ) ] );
+
+			expect( spy.called ).to.be.true;
+		} );
+	} );
+
+	describe( '_getStoredAttributes', () => {
+		it( 'should return no values if there are no ranges in selection', () => {
+			let values = Array.from( selection._getStoredAttributes() );
+
+			expect( values ).to.deep.equal( [] );
+		} );
+	} );
+} );

+ 30 - 538
packages/ckeditor5-engine/tests/model/selection.js

@@ -9,27 +9,17 @@
 
 import Document from '/ckeditor5/engine/model/document.js';
 import Element from '/ckeditor5/engine/model/element.js';
-import Text from '/ckeditor5/engine/model/text.js';
 import Range from '/ckeditor5/engine/model/range.js';
 import Position from '/ckeditor5/engine/model/position.js';
 import LiveRange from '/ckeditor5/engine/model/liverange.js';
 import Selection from '/ckeditor5/engine/model/selection.js';
-import InsertOperation from '/ckeditor5/engine/model/operation/insertoperation.js';
-import MoveOperation from '/ckeditor5/engine/model/operation/moveoperation.js';
 import CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
 import testUtils from '/tests/ckeditor5/_utils/utils.js';
 import count from '/ckeditor5/utils/count.js';
-import { jsonParseStringify, wrapInDelta } from '/tests/engine/model/_utils/utils.js';
 
 testUtils.createSinonSandbox();
 
 describe( 'Selection', () => {
-	let attrFooBar;
-
-	before( () => {
-		attrFooBar = { foo: 'bar' };
-	} );
-
 	let doc, root, selection, liveRange, range;
 
 	beforeEach( () => {
@@ -44,8 +34,7 @@ describe( 'Selection', () => {
 			new Element( 'p' ),
 			new Element( 'p', [], 'foobar' )
 		] );
-		selection = doc.selection;
-		doc.schema.registerItem( 'p', '$block' );
+		selection = new Selection();
 
 		liveRange = new LiveRange( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) );
 		range = new Range( new Position( root, [ 2 ] ), new Position( root, [ 2, 2 ] ) );
@@ -56,59 +45,9 @@ describe( 'Selection', () => {
 		liveRange.detach();
 	} );
 
-	describe( 'default range', () => {
-		it( 'should go to the first editable element', () => {
-			const ranges = Array.from( selection.getRanges() );
-
-			expect( ranges.length ).to.equal( 1 );
-			expect( selection.anchor.isEqual( new Position( root, [ 0, 0 ] ) ) ).to.be.true;
-			expect( selection.focus.isEqual( new Position( root, [ 0, 0 ] ) ) ).to.be.true;
-			expect( selection ).to.have.property( 'isBackward', false );
-			expect( selection._attrs ).to.be.instanceof( Map );
-			expect( selection._attrs.size ).to.equal( 0 );
-		} );
-
-		it( 'should be set to the beginning of the doc if there is no editable element', () => {
-			doc = new Document();
-			root = doc.createRoot();
-			root.insertChildren( 0, 'foobar' );
-			selection = doc.selection;
-
-			const ranges = Array.from( selection.getRanges() );
-
-			expect( ranges.length ).to.equal( 1 );
-			expect( selection.anchor.isEqual( new Position( root, [ 0 ] ) ) ).to.be.true;
-			expect( selection.focus.isEqual( new Position( root, [ 0 ] ) ) ).to.be.true;
-			expect( selection ).to.have.property( 'isBackward', false );
-			expect( selection._attrs ).to.be.instanceof( Map );
-			expect( selection._attrs.size ).to.equal( 0 );
-		} );
-
-		it( 'should skip element when you can not put selection', () => {
-			doc = new Document();
-			root = doc.createRoot();
-			root.insertChildren( 0, [
-				new Element( 'img' ),
-				new Element( 'p', [], 'foobar' )
-			] );
-			doc.schema.registerItem( 'img' );
-			doc.schema.registerItem( 'p', '$block' );
-			selection = doc.selection;
-
-			const ranges = Array.from( selection.getRanges() );
-
-			expect( ranges.length ).to.equal( 1 );
-			expect( selection.anchor.isEqual( new Position( root, [ 1, 0 ] ) ) ).to.be.true;
-			expect( selection.focus.isEqual( new Position( root, [ 1, 0 ] ) ) ).to.be.true;
-			expect( selection ).to.have.property( 'isBackward', false );
-			expect( selection._attrs ).to.be.instanceof( Map );
-			expect( selection._attrs.size ).to.equal( 0 );
-		} );
-	} );
-
 	describe( 'isCollapsed', () => {
-		it( 'should return true for default range', () => {
-			expect( selection.isCollapsed ).to.be.true;
+		it( 'should return false for empty selection', () => {
+			expect( selection.isCollapsed ).to.be.false;
 		} );
 
 		it( 'should return true when there is single collapsed ranges', () => {
@@ -133,7 +72,7 @@ describe( 'Selection', () => {
 
 	describe( 'rangeCount', () => {
 		it( 'should return proper range count', () => {
-			expect( selection.rangeCount ).to.equal( 1 );
+			expect( selection.rangeCount ).to.equal( 0 );
 
 			selection.addRange( new Range( new Position( root, [ 0 ] ), new Position( root, [ 0 ] ) ) );
 
@@ -212,14 +151,6 @@ describe( 'Selection', () => {
 			expect( ranges[ 0 ].isEqual( liveRange ) ).to.be.true;
 		} );
 
-		it( 'should convert added Range to LiveRange', () => {
-			selection.addRange( range );
-
-			const ranges = selection._ranges;
-
-			expect( ranges[ 0 ] ).to.be.instanceof( LiveRange );
-		} );
-
 		it( 'should fire change:range event when adding a range', () => {
 			let spy = sinon.spy();
 			selection.on( 'change:range', spy );
@@ -229,24 +160,6 @@ describe( 'Selection', () => {
 			expect( spy.called ).to.be.true;
 		} );
 
-		it( 'should unbind all events when destroyed', () => {
-			selection.addRange( liveRange );
-			selection.addRange( range );
-
-			const ranges = selection._ranges;
-
-			sinon.spy( ranges[ 0 ], 'detach' );
-			sinon.spy( ranges[ 1 ], 'detach' );
-
-			selection.destroy();
-
-			expect( ranges[ 0 ].detach.called ).to.be.true;
-			expect( ranges[ 1 ].detach.called ).to.be.true;
-
-			ranges[ 0 ].detach.restore();
-			ranges[ 1 ].detach.restore();
-		} );
-
 		it( 'should throw an error if added range intersects with already stored range', () => {
 			selection.addRange( liveRange );
 
@@ -262,16 +175,6 @@ describe( 'Selection', () => {
 	} );
 
 	describe( 'collapse', () => {
-		it( 'detaches all existing ranges', () => {
-			selection.addRange( range );
-			selection.addRange( liveRange );
-
-			const spy = testUtils.sinon.spy( LiveRange.prototype, 'detach' );
-			selection.collapse( root );
-
-			expect( spy.calledTwice ).to.be.true;
-		} );
-
 		it( 'fires change:range', () => {
 			const spy = sinon.spy();
 
@@ -370,14 +273,12 @@ describe( 'Selection', () => {
 			expect( spy.calledOnce ).to.be.true;
 		} );
 
-		it( 'modifies default range', () => {
-			const startPos = selection.getFirstPosition();
+		it( 'throws if there are no ranges in selection', () => {
 			const endPos = Position.createAt( root, 'END' );
 
-			selection.setFocus( endPos );
-
-			expect( selection.anchor.compareWith( startPos ) ).to.equal( 'SAME' );
-			expect( selection.focus.compareWith( endPos ) ).to.equal( 'SAME' );
+			expect( () => {
+				selection.setFocus( endPos );
+			} ).to.throw( CKEditorError, /selection-setFocus-no-ranges/ );
 		} );
 
 		it( 'modifies existing collapsed selection', () => {
@@ -516,19 +417,6 @@ describe( 'Selection', () => {
 			expect( spy.calledOnce ).to.be.true;
 			expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'SAME' );
 		} );
-
-		it( 'detaches the range it replaces', () => {
-			const startPos = Position.createAt( root, 1 );
-			const endPos = Position.createAt( root, 2 );
-			const newEndPos = Position.createAt( root, 4 );
-			const spy = testUtils.sinon.spy( LiveRange.prototype, 'detach' );
-
-			selection.addRange( new Range( startPos, endPos ) );
-
-			selection.setFocus( newEndPos );
-
-			expect( spy.calledOnce ).to.be.true;
-		} );
 	} );
 
 	describe( 'removeAllRanges', () => {
@@ -543,31 +431,16 @@ describe( 'Selection', () => {
 
 			ranges = selection._ranges;
 
-			sinon.spy( ranges[ 0 ], 'detach' );
-			sinon.spy( ranges[ 1 ], 'detach' );
-
 			selection.removeAllRanges();
 		} );
 
-		afterEach( () => {
-			ranges[ 0 ].detach.restore();
-			ranges[ 1 ].detach.restore();
-		} );
-
-		it( 'should remove all stored ranges (and reset to default range)', () => {
-			expect( Array.from( selection.getRanges() ).length ).to.equal( 1 );
-			expect( selection.anchor.isEqual( new Position( root, [ 0, 0 ] ) ) ).to.be.true;
-			expect( selection.focus.isEqual( new Position( root, [ 0, 0 ] ) ) ).to.be.true;
+		it( 'should remove all stored ranges', () => {
+			expect( Array.from( selection.getRanges() ).length ).to.equal( 0 );
 		} );
 
 		it( 'should fire exactly one update event', () => {
 			expect( spy.calledOnce ).to.be.true;
 		} );
-
-		it( 'should detach ranges', () => {
-			expect( ranges[ 0 ].detach.called ).to.be.true;
-			expect( ranges[ 1 ].detach.called ).to.be.true;
-		} );
 	} );
 
 	describe( 'setRanges', () => {
@@ -588,14 +461,6 @@ describe( 'Selection', () => {
 			selection.on( 'change:range', spy );
 
 			oldRanges = selection._ranges;
-
-			sinon.spy( oldRanges[ 0 ], 'detach' );
-			sinon.spy( oldRanges[ 1 ], 'detach' );
-		} );
-
-		afterEach( () => {
-			oldRanges[ 0 ].detach.restore();
-			oldRanges[ 1 ].detach.restore();
 		} );
 
 		it( 'should remove all ranges and add given ranges', () => {
@@ -624,15 +489,13 @@ describe( 'Selection', () => {
 			selection.setRanges( newRanges );
 			expect( spy.calledOnce ).to.be.true;
 		} );
-
-		it( 'should detach removed LiveRanges', () => {
-			selection.setRanges( newRanges );
-			expect( oldRanges[ 0 ].detach.called ).to.be.true;
-			expect( oldRanges[ 1 ].detach.called ).to.be.true;
-		} );
 	} );
 
 	describe( 'getFirstRange', () => {
+		it( 'should return null if no ranges were added', () => {
+			expect( selection.getFirstRange() ).to.be.null;
+		} );
+
 		it( 'should return a range which start position is before all other ranges\' start positions', () => {
 			// This will not be the first range despite being added as first
 			selection.addRange( new Range( new Position( root, [ 4 ] ), new Position( root, [ 5 ] ) ) );
@@ -651,6 +514,10 @@ describe( 'Selection', () => {
 	} );
 
 	describe( 'getFirstPosition', () => {
+		it( 'should return null if no ranges were added', () => {
+			expect( selection.getFirstPosition() ).to.be.null;
+		} );
+
 		it( 'should return a position that is in selection and is before any other position from the selection', () => {
 			// This will not be a range containing the first position despite being added as first
 			selection.addRange( new Range( new Position( root, [ 4 ] ), new Position( root, [ 5 ] ) ) );
@@ -667,398 +534,23 @@ describe( 'Selection', () => {
 		} );
 	} );
 
-	// Selection uses LiveRanges so here are only simple test to see if integration is
-	// working well, without getting into complicated corner cases.
-	describe( 'after applying an operation should get updated and not fire update event', () => {
-		let spy;
-
-		beforeEach( () => {
-			root.insertChildren( 0, [ new Element( 'ul', [], 'abcdef' ), new Element( 'p', [], 'foobar' ), 'xyz' ] );
-
-			selection.addRange( new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 1, 4 ] ) ) );
-
-			spy = sinon.spy();
-			selection.on( 'change:range', spy );
-		} );
-
-		describe( 'InsertOperation', () => {
-			it( 'before selection', () => {
-				doc.applyOperation( wrapInDelta(
-					new InsertOperation(
-						new Position( root, [ 0, 1 ] ),
-						'xyz',
-						doc.version
-					)
-				) );
-
-				let range = selection._ranges[ 0 ];
-
-				expect( range.start.path ).to.deep.equal( [ 0, 5 ] );
-				expect( range.end.path ).to.deep.equal( [ 1, 4 ] );
-				expect( spy.called ).to.be.false;
-			} );
-
-			it( 'inside selection', () => {
-				doc.applyOperation( wrapInDelta(
-					new InsertOperation(
-						new Position( root, [ 1, 0 ] ),
-						'xyz',
-						doc.version
-					)
-				) );
-
-				let range = selection._ranges[ 0 ];
-
-				expect( range.start.path ).to.deep.equal( [ 0, 2 ] );
-				expect( range.end.path ).to.deep.equal( [ 1, 7 ] );
-				expect( spy.called ).to.be.false;
-			} );
-		} );
-
-		describe( 'MoveOperation', () => {
-			it( 'move range from before a selection', () => {
-				doc.applyOperation( wrapInDelta(
-					new MoveOperation(
-						new Position( root, [ 0, 0 ] ),
-						2,
-						new Position( root, [ 2 ] ),
-						doc.version
-					)
-				) );
-
-				let range = selection._ranges[ 0 ];
-
-				expect( range.start.path ).to.deep.equal( [ 0, 0 ] );
-				expect( range.end.path ).to.deep.equal( [ 1, 4 ] );
-				expect( spy.called ).to.be.false;
-			} );
-
-			it( 'moved into before a selection', () => {
-				doc.applyOperation( wrapInDelta(
-					new MoveOperation(
-						new Position( root, [ 2 ] ),
-						2,
-						new Position( root, [ 0, 0 ] ),
-						doc.version
-					)
-				) );
-
-				let range = selection._ranges[ 0 ];
-
-				expect( range.start.path ).to.deep.equal( [ 0, 4 ] );
-				expect( range.end.path ).to.deep.equal( [ 1, 4 ] );
-				expect( spy.called ).to.be.false;
-			} );
-
-			it( 'move range from inside of selection', () => {
-				doc.applyOperation( wrapInDelta(
-					new MoveOperation(
-						new Position( root, [ 1, 0 ] ),
-						2,
-						new Position( root, [ 2 ] ),
-						doc.version
-					)
-				) );
-
-				let range = selection._ranges[ 0 ];
-
-				expect( range.start.path ).to.deep.equal( [ 0, 2 ] );
-				expect( range.end.path ).to.deep.equal( [ 1, 2 ] );
-				expect( spy.called ).to.be.false;
-			} );
-
-			it( 'moved range intersects with selection', () => {
-				doc.applyOperation( wrapInDelta(
-					new MoveOperation(
-						new Position( root, [ 1, 3 ] ),
-						2,
-						new Position( root, [ 4 ] ),
-						doc.version
-					)
-				) );
-
-				let range = selection._ranges[ 0 ];
-
-				expect( range.start.path ).to.deep.equal( [ 0, 2 ] );
-				expect( range.end.path ).to.deep.equal( [ 1, 3 ] );
-				expect( spy.called ).to.be.false;
-			} );
-
-			it( 'split inside selection (do not break selection)', () => {
-				doc.applyOperation( wrapInDelta(
-					new InsertOperation(
-						new Position( root, [ 2 ] ),
-						new Element( 'p' ),
-						doc.version
-					)
-				) );
-
-				doc.applyOperation( wrapInDelta(
-					new MoveOperation(
-						new Position( root, [ 1, 2 ] ),
-						4,
-						new Position( root, [ 2, 0 ] ),
-						doc.version
-					)
-				) );
-
-				let range = selection._ranges[ 0 ];
-
-				expect( range.start.path ).to.deep.equal( [ 0, 2 ] );
-				expect( range.end.path ).to.deep.equal( [ 2, 2 ] );
-				expect( spy.called ).to.be.false;
-			} );
-		} );
-	} );
-
-	describe( 'attributes interface', () => {
-		let fullP, emptyP, rangeInFullP, rangeInEmptyP;
-
-		beforeEach( () => {
-			root.insertChildren( 0, [
-				new Element( 'p', [], 'foobar' ),
-				new Element( 'p', [], [] )
-			] );
-
-			fullP = root.getChild( 0 );
-			emptyP = root.getChild( 1 );
-
-			rangeInFullP = new Range( new Position( root, [ 0, 4 ] ), new Position( root, [ 0, 4 ] ) );
-			rangeInEmptyP = new Range( new Position( root, [ 1, 0 ] ), new Position( root, [ 1, 0 ] ) );
-		} );
-
-		describe( 'setAttribute', () => {
-			it( 'should set given attribute on the selection', () => {
-				selection.setRanges( [ rangeInFullP ] );
-				selection.setAttribute( 'foo', 'bar' );
-
-				expect( selection.getAttribute( 'foo' ) ).to.equal( 'bar' );
-				expect( fullP.hasAttribute( Selection._getStoreAttributeKey( 'foo' ) ) ).to.be.false;
-			} );
-
-			it( 'should store attribute if the selection is in empty node', () => {
-				selection.setRanges( [ rangeInEmptyP ] );
-				selection.setAttribute( 'foo', 'bar' );
-
-				expect( selection.getAttribute( 'foo' ) ).to.equal( 'bar' );
-
-				expect( emptyP.getAttribute( Selection._getStoreAttributeKey( 'foo' ) ) ).to.equal( 'bar' );
-			} );
-
-			it( 'should fire change:attribute event', () => {
-				let spy = sinon.spy();
-				selection.on( 'change:attribute', spy );
-
-				selection.setAttribute( 'foo', 'bar' );
-
-				expect( spy.called ).to.be.true;
-			} );
-		} );
-
-		describe( 'hasAttribute', () => {
-			it( 'should return true if element contains attribute with given key', () => {
-				selection.setRanges( [ rangeInFullP ] );
-				selection.setAttribute( 'foo', 'bar' );
-
-				expect( selection.hasAttribute( 'foo' ) ).to.be.true;
-			} );
-
-			it( 'should return false if element does not contain attribute with given key', () => {
-				expect( selection.hasAttribute( 'abc' ) ).to.be.false;
-			} );
-		} );
-
-		describe( 'getAttribute', () => {
-			it( 'should return undefined if element does not contain given attribute', () => {
-				expect( selection.getAttribute( 'abc' ) ).to.be.undefined;
-			} );
-		} );
-
-		describe( 'getAttributes', () => {
-			it( 'should return an iterator that iterates over all attributes set on the text fragment', () => {
-				selection.setRanges( [ rangeInFullP ] );
-				selection.setAttribute( 'foo', 'bar' );
-				selection.setAttribute( 'abc', 'xyz' );
-
-				let attrs = Array.from( selection.getAttributes() );
-
-				expect( attrs ).to.deep.equal( [ [ 'foo', 'bar' ], [ 'abc', 'xyz' ] ] );
-			} );
-		} );
-
-		describe( 'setAttributesTo', () => {
-			it( 'should remove all attributes set on element and set the given ones', () => {
-				selection.setRanges( [ rangeInFullP ] );
-				selection.setAttribute( 'abc', 'xyz' );
-				selection.setAttributesTo( { foo: 'bar' } );
-
-				expect( selection.getAttribute( 'foo' ) ).to.equal( 'bar' );
-				expect( selection.getAttribute( 'abc' ) ).to.be.undefined;
-
-				expect( fullP.hasAttribute( Selection._getStoreAttributeKey( 'foo' ) ) ).to.be.false;
-				expect( fullP.hasAttribute( Selection._getStoreAttributeKey( 'abc' ) ) ).to.be.false;
-			} );
-
-			it( 'should remove all stored attributes and store the given ones if the selection is in empty node', () => {
-				selection.setRanges( [ rangeInEmptyP ] );
-				selection.setAttribute( 'abc', 'xyz' );
-				selection.setAttributesTo( { foo: 'bar' } );
-
-				expect( selection.getAttribute( 'foo' ) ).to.equal( 'bar' );
-				expect( selection.getAttribute( 'abc' ) ).to.be.undefined;
-
-				expect( emptyP.getAttribute( Selection._getStoreAttributeKey( 'foo' ) ) ).to.equal( 'bar' );
-				expect( emptyP.hasAttribute( Selection._getStoreAttributeKey( 'abc' ) ) ).to.be.false;
-			} );
-
-			it( 'should fire change:attribute event', () => {
-				let spy = sinon.spy();
-				selection.on( 'change:attribute', spy );
-
-				selection.setAttributesTo( { foo: 'bar' } );
-
-				expect( spy.called ).to.be.true;
-			} );
-		} );
-
-		describe( 'removeAttribute', () => {
-			it( 'should remove attribute set on the text fragment', () => {
-				selection.setRanges( [ rangeInFullP ] );
-				selection.setAttribute( 'foo', 'bar' );
-				selection.removeAttribute( 'foo' );
-
-				expect( selection.getAttribute( 'foo' ) ).to.be.undefined;
-
-				expect( fullP.hasAttribute( Selection._getStoreAttributeKey( 'foo' ) ) ).to.be.false;
-			} );
-
-			it( 'should remove stored attribute if the selection is in empty node', () => {
-				selection.setRanges( [ rangeInEmptyP ] );
-				selection.setAttribute( 'foo', 'bar' );
-				selection.removeAttribute( 'foo' );
-
-				expect( selection.getAttribute( 'foo' ) ).to.be.undefined;
-
-				expect( emptyP.hasAttribute( Selection._getStoreAttributeKey( 'foo' ) ) ).to.be.false;
-			} );
-
-			it( 'should fire change:attribute event', () => {
-				let spy = sinon.spy();
-				selection.on( 'change:attribute', spy );
-
-				selection.removeAttribute( 'foo' );
-
-				expect( spy.called ).to.be.true;
-			} );
-		} );
-
-		describe( 'clearAttributes', () => {
-			it( 'should remove all attributes from the element', () => {
-				selection.setRanges( [ rangeInFullP ] );
-				selection.setAttribute( 'foo', 'bar' );
-				selection.setAttribute( 'abc', 'xyz' );
-
-				selection.clearAttributes();
-
-				expect( selection.getAttribute( 'foo' ) ).to.be.undefined;
-				expect( selection.getAttribute( 'abc' ) ).to.be.undefined;
-
-				expect( fullP.hasAttribute( Selection._getStoreAttributeKey( 'foo' ) ) ).to.be.false;
-				expect( fullP.hasAttribute( Selection._getStoreAttributeKey( 'abc' ) ) ).to.be.false;
-			} );
-
-			it( 'should remove all stored attributes if the selection is in empty node', () => {
-				selection.setRanges( [ rangeInEmptyP ] );
-				selection.setAttribute( 'foo', 'bar' );
-				selection.setAttribute( 'abc', 'xyz' );
-
-				selection.clearAttributes();
-
-				expect( selection.getAttribute( 'foo' ) ).to.be.undefined;
-				expect( selection.getAttribute( 'abc' ) ).to.be.undefined;
-
-				expect( emptyP.hasAttribute( Selection._getStoreAttributeKey( 'foo' ) ) ).to.be.false;
-				expect( emptyP.hasAttribute( Selection._getStoreAttributeKey( 'abc' ) ) ).to.be.false;
-			} );
-
-			it( 'should fire change:attribute event', () => {
-				let spy = sinon.spy();
-				selection.on( 'change:attribute', spy );
-
-				selection.clearAttributes();
-
-				expect( spy.called ).to.be.true;
-			} );
-		} );
-	} );
-
-	describe( '_updateAttributes', () => {
-		beforeEach( () => {
-			root.insertChildren( 0, [
-				new Element( 'p', { p: true } ),
-				new Text( 'a', { a: true } ),
-				new Element( 'p', { p: true } ),
-				new Text( 'b', { b: true } ),
-				new Text( 'c', { c: true } ),
-				new Element( 'p', [], [
-					new Text( 'd', { d: true } )
-				] ),
-				new Element( 'p', { p: true } ),
-				new Text( 'e', { e: true } )
-			] );
-		} );
-
-		it( 'if selection is a range, should find first character in it and copy it\'s attributes', () => {
-			selection.setRanges( [ new Range( new Position( root, [ 2 ] ), new Position( root, [ 5 ] ) ) ] );
-
-			expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'b', true ] ] );
-
-			// Step into elements when looking for first character:
-			selection.setRanges( [ new Range( new Position( root, [ 5 ] ), new Position( root, [ 7 ] ) ) ] );
-
-			expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'd', true ] ] );
-		} );
-
-		it( 'if selection is collapsed it should seek a character to copy that character\'s attributes', () => {
-			// Take styles from character before selection.
-			selection.setRanges( [ new Range( new Position( root, [ 2 ] ), new Position( root, [ 2 ] ) ) ] );
-			expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'a', true ] ] );
-
-			// If there are none,
-			// Take styles from character after selection.
-			selection.setRanges( [ new Range( new Position( root, [ 3 ] ), new Position( root, [ 3 ] ) ) ] );
-			expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'b', true ] ] );
-
-			// If there are none,
-			// Look from the selection position to the beginning of node looking for character to take attributes from.
-			selection.setRanges( [ new Range( new Position( root, [ 6 ] ), new Position( root, [ 6 ] ) ) ] );
-			expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'c', true ] ] );
-
-			// If there are none,
-			// Look from the selection position to the end of node looking for character to take attributes from.
-			selection.setRanges( [ new Range( new Position( root, [ 0 ] ), new Position( root, [ 0 ] ) ) ] );
-			expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [ [ 'a', true ] ] );
-
-			// If there are no characters to copy attributes from, use stored attributes.
-			selection.setRanges( [ new Range( new Position( root, [ 0, 0 ] ), new Position( root, [ 0, 0 ] ) ) ] );
-			expect( Array.from( selection.getAttributes() ) ).to.deep.equal( [] );
-		} );
+	describe( 'createFromSelection', () => {
+		it( 'should return a Selection instance with same ranges and direction as given selection', () => {
+			selection.addRange( liveRange );
+			selection.addRange( range, true );
 
-		it( 'should fire change:attribute event', () => {
-			let spy = sinon.spy();
-			selection.on( 'change:attribute', spy );
+			const snapshot = Selection.createFromSelection( selection );
 
-			selection.setRanges( [ new Range( new Position( root, [ 2 ] ), new Position( root, [ 5 ] ) ) ] );
+			expect( selection.isBackward ).to.equal( snapshot.isBackward );
 
-			expect( spy.called ).to.be.true;
-		} );
-	} );
+			const selectionRanges = Array.from( selection.getRanges() );
+			const snapshotRanges = Array.from( snapshot.getRanges() );
 
-	describe( '_getStoredAttributes', () => {
-		it( 'should return no values if there are no ranges in selection', () => {
-			let values = Array.from( selection._getStoredAttributes() );
+			expect( selectionRanges.length ).to.equal( snapshotRanges.length );
 
-			expect( values ).to.deep.equal( [] );
+			for ( let i = 0; i < selectionRanges.length; i++ ) {
+				expect( selectionRanges[ i ].isEqual( snapshotRanges[ i ] ) ).to.be.true;
+			}
 		} );
 	} );
 } );