Преглед изворни кода

Changed: split engine.model.Selection to Selection and DocumentSelection.

Szymon Cofalik пре 9 година
родитељ
комит
6ba7325f60

+ 3 - 3
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 RootElement from './rootelement.js';
 import Batch from './batch.js';
 import Batch from './batch.js';
 import History from './history.js';
 import History from './history.js';
-import Selection from './selection.js';
+import DocumentSelection from './documentselection.js';
 import EmitterMixin from '../../utils/emittermixin.js';
 import EmitterMixin from '../../utils/emittermixin.js';
 import CKEditorError from '../../utils/ckeditorerror.js';
 import CKEditorError from '../../utils/ckeditorerror.js';
 import mix from '../../utils/mix.js';
 import mix from '../../utils/mix.js';
@@ -55,9 +55,9 @@ export default class Document {
 		 * Selection done on this document.
 		 * Selection done on this document.
 		 *
 		 *
 		 * @readonly
 		 * @readonly
-		 * @member {engine.model.Selection} engine.model.Document#selection
+		 * @member {engine.model.DocumentSelection} engine.model.Document#selection
 		 */
 		 */
-		this.selection = new Selection( this );
+		this.selection = new DocumentSelection( this );
 
 
 		/**
 		/**
 		 * Schema for this document.
 		 * Schema for this document.

+ 345 - 0
packages/ckeditor5-engine/src/model/documentselection.js

@@ -0,0 +1,345 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import LiveRange from './liverange.js';
+import CharacterProxy from './characterproxy.js';
+import toMap from '../../utils/tomap.js';
+
+import Selection from './selection.js';
+
+const storePrefix = 'selection:';
+
+/**
+ * Represents a main {@link engine.model.Selection selection} of a {@link engine.model.Document}. This is the selection
+ * that user interacts with. `DocumentSelection` instance is created by {@link engine.model.Document}. You should not
+ * create an instance of `DocumentSelection`.
+ *
+ * Differences between {@link engine.model.Selection} and `DocumentSelection` are two:
+ * * ranges added to this selection updates automatically when the document changes,
+ * * document selection may have attributes.
+ *
+ * @memberOf engine.model
+ */
+export default class DocumentSelection extends Selection {
+	/**
+	 * @inheritDoc
+	 */
+	constructor( document ) {
+		super( document );
+
+		/**
+		 * List of attributes set on current selection.
+		 *
+		 * @protected
+		 * @member {Map} engine.model.DocumentSelection#_attrs
+		 */
+		this._attrs = new Map();
+	}
+
+	/**
+	 * Unbinds all events previously bound by document selection.
+	 */
+	destroy() {
+		for ( let i = 0; i < this._ranges.length; i++ ) {
+			this._ranges[ i ].detach();
+		}
+	}
+
+	/**
+	 * @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.DocumentSelection#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.DocumentSelection#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.DocumentSelection#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.DocumentSelection#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' );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	_popRange() {
+		this._ranges.pop().detach();
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	_pushRange( range ) {
+		this._checkRange( range );
+		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 = DocumentSelection._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 = DocumentSelection._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 = DocumentSelection._getStoreAttributeKey( attr[ 0 ] );
+
+					batch.removeAttr( storeKey, selectionParent );
+				}
+
+				for ( let attr of attrs ) {
+					const storeKey = DocumentSelection._getStoreAttributeKey( attr[ 0 ] );
+
+					batch.setAttr( storeKey, attr[ 1 ], selectionParent );
+				}
+			} );
+		}
+	}
+
+	/**
+	 * Updates this selection attributes according to it's ranges and the document.
+	 *
+	 * @fires engine.model.DocumentSelection#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.DocumentSelection#change:attribute
+ */

+ 28 - 301
packages/ckeditor5-engine/src/model/selection.js

@@ -7,21 +7,13 @@
 
 
 import Position from './position.js';
 import Position from './position.js';
 import Range from './range.js';
 import Range from './range.js';
-import LiveRange from './liverange.js';
 import EmitterMixin from '../../utils/emittermixin.js';
 import EmitterMixin from '../../utils/emittermixin.js';
-import CharacterProxy from './characterproxy.js';
 import CKEditorError from '../../utils/ckeditorerror.js';
 import CKEditorError from '../../utils/ckeditorerror.js';
-import toMap from '../../utils/tomap.js';
 import mix from '../../utils/mix.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
  * @memberOf engine.model
  */
  */
@@ -32,14 +24,6 @@ export default class Selection {
 	 * @param {engine.model.Document} document Document which owns this selection.
 	 * @param {engine.model.Document} document Document which owns this selection.
 	 */
 	 */
 	constructor( document ) {
 	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.
 		 * Document which owns this selection.
 		 *
 		 *
@@ -60,7 +44,7 @@ export default class Selection {
 		 * Stores all ranges that are selected.
 		 * Stores all ranges that are selected.
 		 *
 		 *
 		 * @private
 		 * @private
-		 * @member {Array.<engine.model.LiveRange>} engine.model.Selection#_ranges
+		 * @member {Array.<engine.model.Range>} engine.model.Selection#_ranges
 		 */
 		 */
 		this._ranges = [];
 		this._ranges = [];
 	}
 	}
@@ -69,10 +53,9 @@ export default class Selection {
 	 * Selection anchor. Anchor may be described as a position where the selection starts. Together with
 	 * 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
 	 * {@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.
 	 * 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 engine.model.Selection#focus
 	 * @see engine.model.Selection#focus
-	 * @type {engine.model.LivePosition}
+	 * @type {engine.model.Position}
 	 */
 	 */
 	get anchor() {
 	get anchor() {
 		let range = this._ranges.length ? this._ranges[ this._ranges.length - 1 ] : this._getDefaultRange();
 		let range = this._ranges.length ? this._ranges[ this._ranges.length - 1 ] : this._getDefaultRange();
@@ -84,7 +67,7 @@ export default class Selection {
 	 * Selection focus. Focus is a position where the selection ends.
 	 * Selection focus. Focus is a position where the selection ends.
 	 *
 	 *
 	 * @see engine.model.Selection#anchor
 	 * @see engine.model.Selection#anchor
-	 * @type {engine.model.LivePosition}
+	 * @type {engine.model.Position}
 	 */
 	 */
 	get focus() {
 	get focus() {
 		let range = this._ranges.length ? this._ranges[ this._ranges.length - 1 ] : this._getDefaultRange();
 		let range = this._ranges.length ? this._ranges[ this._ranges.length - 1 ] : this._getDefaultRange();
@@ -130,12 +113,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
 	 * 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.
 	 * {@link engine.model.Selection#focus} properties.
 	 *
 	 *
 	 * @fires engine.model.Selection#change:range
 	 * @fires engine.model.Selection#change:range
@@ -151,16 +134,7 @@ 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>}
 	 * @returns {Iterator.<engine.model.Range>}
 	 */
 	 */
@@ -211,7 +185,6 @@ export default class Selection {
 	 * @fires engine.model.Selection#change:range
 	 * @fires engine.model.Selection#change:range
 	 */
 	 */
 	removeAllRanges() {
 	removeAllRanges() {
-		this.destroy();
 		this._ranges = [];
 		this._ranges = [];
 
 
 		this.fire( 'change:range' );
 		this.fire( 'change:range' );
@@ -228,7 +201,6 @@ export default class Selection {
 	 * or backward - from end to start (`true`). Defaults to `false`.
 	 * or backward - from end to start (`true`). Defaults to `false`.
 	 */
 	 */
 	setRanges( newRanges, isLastBackward ) {
 	setRanges( newRanges, isLastBackward ) {
-		this.destroy();
 		this._ranges = [];
 		this._ranges = [];
 
 
 		for ( let i = 0; i < newRanges.length; i++ ) {
 		for ( let i = 0; i < newRanges.length; i++ ) {
@@ -277,8 +249,7 @@ export default class Selection {
 		const anchor = this.anchor;
 		const anchor = this.anchor;
 
 
 		if ( this._ranges.length ) {
 		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' ) {
 		if ( newFocus.compareWith( anchor ) == 'BEFORE' ) {
@@ -289,95 +260,14 @@ 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.
-	 *
-	 * @fires engine.model.Selection#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.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.
+	 * 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.
 	 *
 	 *
-	 * @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 ] ) ) {
 			if ( range.isIntersecting( this._ranges[ i ] ) ) {
 				/**
 				/**
 				 * Trying to add a range that intersects with another range from selection.
 				 * Trying to add a range that intersects with another range from selection.
@@ -392,173 +282,27 @@ 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.
+	 * Removes most recently added range from the selection.
 	 *
 	 *
-	 * @param {Iterable|Object} attrs Iterable object containing attributes to be set.
-	 * @private
+	 * @protected
 	 */
 	 */
-	_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 );
-				}
-			} );
-		}
+	_popRange() {
+		this._ranges.pop();
 	}
 	}
 
 
 	/**
 	/**
-	 * Updates this selection attributes based on it's position in the model.
+	 * 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.
 	 *
 	 *
 	 * @protected
 	 * @protected
+	 * @param {engine.model.Range} range Range to add.
 	 */
 	 */
-	_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' );
+	_pushRange( range ) {
+		this._checkRange( range );
+		this._ranges.push( Range.createFromRange( range ) );
 	}
 	}
 
 
 	/**
 	/**
@@ -584,29 +328,12 @@ export default class Selection {
 
 
 		return new Range( position, position );
 		return new Range( position, position );
 	}
 	}
-
-	/**
-	 * 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;
-	}
 }
 }
 
 
 mix( Selection, EmitterMixin );
 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
  * @event engine.model.Selection#change:range
  */
  */
-
-/**
- * Fired whenever selection attributes are changed.
- *
- * @event engine.model.Selection#change:attribute
- */

+ 571 - 0
packages/ckeditor5-engine/tests/model/documentselection.js

@@ -0,0 +1,571 @@
+/**
+ * @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 DocumentSelection from '/ckeditor5/engine/model/documentselection.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';
+
+testUtils.createSinonSandbox();
+
+describe( 'DocumentSelection', () => {
+	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( '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( '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 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;
+		} );
+	} );
+
+	// 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(
+					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(
+					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(
+					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(
+					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(
+					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(
+					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(
+					new InsertOperation(
+						new Position( root, [ 2 ] ),
+						new Element( 'p' ),
+						doc.version
+					)
+				);
+
+				doc.applyOperation(
+					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( DocumentSelection._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( DocumentSelection._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( DocumentSelection._getStoreAttributeKey( 'foo' ) ) ).to.be.false;
+				expect( fullP.hasAttribute( DocumentSelection._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( DocumentSelection._getStoreAttributeKey( 'foo' ) ) ).to.equal( 'bar' );
+				expect( emptyP.hasAttribute( DocumentSelection._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( DocumentSelection._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( DocumentSelection._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( DocumentSelection._getStoreAttributeKey( 'foo' ) ) ).to.be.false;
+				expect( fullP.hasAttribute( DocumentSelection._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( DocumentSelection._getStoreAttributeKey( 'foo' ) ) ).to.be.false;
+				expect( emptyP.hasAttribute( DocumentSelection._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( [] );
+		} );
+	} );
+} );

+ 1 - 483
packages/ckeditor5-engine/tests/model/selection.js

@@ -9,13 +9,10 @@
 
 
 import Document from '/ckeditor5/engine/model/document.js';
 import Document from '/ckeditor5/engine/model/document.js';
 import Element from '/ckeditor5/engine/model/element.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 Range from '/ckeditor5/engine/model/range.js';
 import Position from '/ckeditor5/engine/model/position.js';
 import Position from '/ckeditor5/engine/model/position.js';
 import LiveRange from '/ckeditor5/engine/model/liverange.js';
 import LiveRange from '/ckeditor5/engine/model/liverange.js';
 import Selection from '/ckeditor5/engine/model/selection.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 CKEditorError from '/ckeditor5/utils/ckeditorerror.js';
 import testUtils from '/tests/ckeditor5/_utils/utils.js';
 import testUtils from '/tests/ckeditor5/_utils/utils.js';
 import count from '/ckeditor5/utils/count.js';
 import count from '/ckeditor5/utils/count.js';
@@ -23,12 +20,6 @@ import count from '/ckeditor5/utils/count.js';
 testUtils.createSinonSandbox();
 testUtils.createSinonSandbox();
 
 
 describe( 'Selection', () => {
 describe( 'Selection', () => {
-	let attrFooBar;
-
-	before( () => {
-		attrFooBar = { foo: 'bar' };
-	} );
-
 	let doc, root, selection, liveRange, range;
 	let doc, root, selection, liveRange, range;
 
 
 	beforeEach( () => {
 	beforeEach( () => {
@@ -43,7 +34,7 @@ describe( 'Selection', () => {
 			new Element( 'p' ),
 			new Element( 'p' ),
 			new Element( 'p', [], 'foobar' )
 			new Element( 'p', [], 'foobar' )
 		] );
 		] );
-		selection = doc.selection;
+		selection = new Selection( doc );
 		doc.schema.registerItem( 'p', '$block' );
 		doc.schema.registerItem( 'p', '$block' );
 
 
 		liveRange = new LiveRange( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) );
 		liveRange = new LiveRange( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) );
@@ -63,8 +54,6 @@ describe( 'Selection', () => {
 			expect( selection.anchor.isEqual( new Position( root, [ 0, 0 ] ) ) ).to.be.true;
 			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.focus.isEqual( new Position( root, [ 0, 0 ] ) ) ).to.be.true;
 			expect( selection ).to.have.property( 'isBackward', false );
 			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', () => {
 		it( 'should be set to the beginning of the doc if there is no editable element', () => {
@@ -211,14 +200,6 @@ describe( 'Selection', () => {
 			expect( ranges[ 0 ].isEqual( liveRange ) ).to.be.true;
 			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', () => {
 		it( 'should fire change:range event when adding a range', () => {
 			let spy = sinon.spy();
 			let spy = sinon.spy();
 			selection.on( 'change:range', spy );
 			selection.on( 'change:range', spy );
@@ -228,24 +209,6 @@ describe( 'Selection', () => {
 			expect( spy.called ).to.be.true;
 			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', () => {
 		it( 'should throw an error if added range intersects with already stored range', () => {
 			selection.addRange( liveRange );
 			selection.addRange( liveRange );
 
 
@@ -261,16 +224,6 @@ describe( 'Selection', () => {
 	} );
 	} );
 
 
 	describe( 'collapse', () => {
 	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', () => {
 		it( 'fires change:range', () => {
 			const spy = sinon.spy();
 			const spy = sinon.spy();
 
 
@@ -515,19 +468,6 @@ describe( 'Selection', () => {
 			expect( spy.calledOnce ).to.be.true;
 			expect( spy.calledOnce ).to.be.true;
 			expect( selection.focus.compareWith( newEndPos ) ).to.equal( 'SAME' );
 			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', () => {
 	describe( 'removeAllRanges', () => {
@@ -542,17 +482,9 @@ describe( 'Selection', () => {
 
 
 			ranges = selection._ranges;
 			ranges = selection._ranges;
 
 
-			sinon.spy( ranges[ 0 ], 'detach' );
-			sinon.spy( ranges[ 1 ], 'detach' );
-
 			selection.removeAllRanges();
 			selection.removeAllRanges();
 		} );
 		} );
 
 
-		afterEach( () => {
-			ranges[ 0 ].detach.restore();
-			ranges[ 1 ].detach.restore();
-		} );
-
 		it( 'should remove all stored ranges (and reset to default range)', () => {
 		it( 'should remove all stored ranges (and reset to default range)', () => {
 			expect( Array.from( selection.getRanges() ).length ).to.equal( 1 );
 			expect( Array.from( selection.getRanges() ).length ).to.equal( 1 );
 			expect( selection.anchor.isEqual( new Position( root, [ 0, 0 ] ) ) ).to.be.true;
 			expect( selection.anchor.isEqual( new Position( root, [ 0, 0 ] ) ) ).to.be.true;
@@ -562,11 +494,6 @@ describe( 'Selection', () => {
 		it( 'should fire exactly one update event', () => {
 		it( 'should fire exactly one update event', () => {
 			expect( spy.calledOnce ).to.be.true;
 			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', () => {
 	describe( 'setRanges', () => {
@@ -587,14 +514,6 @@ describe( 'Selection', () => {
 			selection.on( 'change:range', spy );
 			selection.on( 'change:range', spy );
 
 
 			oldRanges = selection._ranges;
 			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', () => {
 		it( 'should remove all ranges and add given ranges', () => {
@@ -623,12 +542,6 @@ describe( 'Selection', () => {
 			selection.setRanges( newRanges );
 			selection.setRanges( newRanges );
 			expect( spy.calledOnce ).to.be.true;
 			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', () => {
 	describe( 'getFirstRange', () => {
@@ -665,399 +578,4 @@ describe( 'Selection', () => {
 			expect( position.path ).to.deep.equal( [ 1 ] );
 			expect( position.path ).to.deep.equal( [ 1 ] );
 		} );
 		} );
 	} );
 	} );
-
-	// 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(
-					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(
-					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(
-					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(
-					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(
-					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(
-					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(
-					new InsertOperation(
-						new Position( root, [ 2 ] ),
-						new Element( 'p' ),
-						doc.version
-					)
-				);
-
-				doc.applyOperation(
-					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( [] );
-		} );
-
-		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( [] );
-		} );
-	} );
 } );
 } );