浏览代码

Changed: Selection updates and stores its attributes on it's own.

Szymon Cofalik 9 年之前
父节点
当前提交
ac39cf89a1

+ 3 - 20
packages/ckeditor5-engine/src/command/attributecommand.js

@@ -8,7 +8,6 @@
 import Command from './command.js';
 import TreeWalker from '../treemodel/treewalker.js';
 import Range from '../treemodel/range.js';
-import Selection from '../treemodel/selection.js';
 
 /**
  * An extension of basic {@link core.command.Command} class, which provides utilities for a command that sets a single
@@ -117,26 +116,10 @@ export default class AttributeCommand extends Command {
 		const value = ( forceValue === undefined ) ? !this.value : forceValue;
 
 		if ( selection.isCollapsed ) {
-			let selectionParent = selection.getFirstPosition().parent;
-
-			if ( selectionParent.getChildCount() === 0 ) {
-				// If selection is collapsed and in empty node, operate on stored selection attributes.
-				const storeKey = Selection.getStoreAttributeKey( this.attributeKey );
-
-				document.enqueueChanges( () => {
-					if ( value ) {
-						document.batch().setAttr( storeKey, value, selectionParent );
-					} else {
-						document.batch().removeAttr( storeKey, selectionParent );
-					}
-				} );
+			if ( value ) {
+				selection.setAttribute( this.attributeKey, true );
 			} else {
-				// If selection is collapsed but not in empty node, change only selection attribute. It won't be saved anywhere.
-				if ( value ) {
-					selection.setAttribute( this.attributeKey, true );
-				} else {
-					selection.removeAttribute( this.attributeKey );
-				}
+				selection.removeAttribute( this.attributeKey );
 			}
 		} else if ( selection.hasAnyRange ) {
 			// If selection is not collapsed and has ranges, we change attribute on those ranges.

+ 19 - 79
packages/ckeditor5-engine/src/treemodel/document.js

@@ -16,8 +16,8 @@ import Selection from './selection.js';
 import EmitterMixin from '../emittermixin.js';
 import CKEditorError from '../ckeditorerror.js';
 import utils from '../utils.js';
-import CharacterProxy from './characterproxy.js';
 import Schema from './schema.js';
+import clone from '../lib/lodash/clone.js';
 
 const graveyardSymbol = Symbol( 'graveyard' );
 
@@ -72,7 +72,7 @@ export default class Document {
 		 * @readonly
 		 * @member {core.treeModel.Selection} core.treeModel.Document#selection
 		 */
-		this.selection = new Selection();
+		this.selection = new Selection( this );
 
 		/**
 		 * Schema for this document.
@@ -83,11 +83,11 @@ export default class Document {
 
 		// Add events that will update selection attributes.
 		this.selection.on( 'update', () => {
-			this._updateSelectionAttributes();
+			this.selection._updateAttributes();
 		} );
 
 		this.on( 'changesDone', () => {
-			this._updateSelectionAttributes();
+			this.selection._updateAttributes();
 		} );
 
 		// Graveyard tree root. Document always have a graveyard root, which stores removed nodes.
@@ -171,6 +171,14 @@ export default class Document {
 		return root;
 	}
 
+	/**
+	 * Removes all events listeners set by document instance.
+	 */
+	detach() {
+		this.selection.detach();
+		this.stopListening();
+	}
+
 	/**
 	 * Enqueue a callback with document changes. Any changes to be done on document (mostly using {@link core.treeModel.Document#batch}
 	 * should be placed in the queued callback. If no other plugin is changing document at the moment, the callback will be
@@ -219,85 +227,17 @@ export default class Document {
 	}
 
 	/**
-	 * Updates this document's {@link core.treeModel.Document#selection selection} attributes. Should be fired
-	 * whenever selection attributes might have changed (i.e. when selection ranges change or document is changed).
+	 * Custom toJSON method to solve child-parent circular dependencies.
 	 *
-	 * @private
+	 * @returns {Object} Clone of this object with the document property changed to string.
 	 */
-	_updateSelectionAttributes() {
-		if ( !this.selection.hasAnyRange ) {
-			this.selection.clearAttributes();
-		} else {
-			const position = this.selection.getFirstPosition();
-			const positionParent = position.parent;
-			let attrs = null;
-
-			if ( this.selection.isCollapsed === false ) {
-				// 1. If selection is a range...
-				const range = this.selection.getFirstRange();
-
-				// ...look for a first character node in that range and take attributes from it.
-				for ( let item of range ) {
-					if ( item.type == 'TEXT' ) {
-						attrs = item.item.getAttributes();
-						break;
-					}
-				}
-			}
-
-			// 2. If the selection is a caret or the range does not contain a character node...
-			if ( !attrs && this.selection.isCollapsed === true ) {
-				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 = Selection.filterStoreAttributes( positionParent.getAttributes() );
-				}
-			}
-
-			if ( attrs ) {
-				this.selection.setAttributesTo( attrs );
-			} else {
-				this.selection.clearAttributes();
-			}
-		}
+	toJSON() {
+		const json = clone( this );
 
-		function getAttrsIfCharacter( node ) {
-			if ( node instanceof CharacterProxy ) {
-				return node.getAttributes();
-			}
+		// Due to circular references we need to remove parent reference.
+		json.selection = '[core.treeModel.Selection]';
 
-			return null;
-		}
+		return {};
 	}
 
 	/**

+ 14 - 14
packages/ckeditor5-engine/src/treemodel/node.js

@@ -148,20 +148,6 @@ export default class Node {
 		return path;
 	}
 
-	/**
-	 * Custom toJSON method to solve child-parent circular dependencies.
-	 *
-	 * @returns {Object} Clone of this object with the parent property replaced with its name.
-	 */
-	toJSON() {
-		const json = clone( this );
-
-		// Due to circular references we need to remove parent reference.
-		json.parent = this.parent ? this.parent.name : null;
-
-		return json;
-	}
-
 	/**
 	 * Checks if the node has an attribute for given key.
 	 *
@@ -190,4 +176,18 @@ export default class Node {
 	getAttributes() {
 		return this._attrs[ Symbol.iterator ]();
 	}
+
+	/**
+	 * Custom toJSON method to solve child-parent circular dependencies.
+	 *
+	 * @returns {Object} Clone of this object with the parent property replaced with its name.
+	 */
+	toJSON() {
+		const json = clone( this );
+
+		// Due to circular references we need to remove parent reference.
+		json.parent = this.parent ? this.parent.name : null;
+
+		return json;
+	}
 }

+ 185 - 20
packages/ckeditor5-engine/src/treemodel/selection.js

@@ -9,14 +9,15 @@ import Position from './position.js';
 import Range from './range.js';
 import LiveRange from './liverange.js';
 import EmitterMixin from '../emittermixin.js';
+import CharacterProxy from './characterproxy.js';
 import CKEditorError from '../ckeditorerror.js';
 import utils from '../utils.js';
 
-const storePrefix = 'selection_store:';
+const storePrefix = 'selection:';
 
 /**
  * Represents a selection that is made on nodes in {@link core.treeModel.Document}. Selection instance is
- * created by {@link core.treeModel.Document}. In most scenarios you should not need to create an instance of Selection.
+ * created by {@link core.treeModel.Document}. You should not need to create an instance of Selection.
  *
  * @memberOf core.treeModel
  */
@@ -24,7 +25,7 @@ export default class Selection {
 	/**
 	 * Creates an empty selection.
 	 */
-	constructor() {
+	constructor( document ) {
 		/**
 		 * List of attributes set on current selection.
 		 *
@@ -34,12 +35,12 @@ export default class Selection {
 		this._attrs = new Map();
 
 		/**
-		 * Stores all ranges that are selected.
+		 * Document which owns this selection.
 		 *
 		 * @private
-		 * @member {Array.<core.treeModel.LiveRange>} core.treeModel.Selection#_ranges
+		 * @member {core.treeModel.Document} core.treeModel.Selection#_document
 		 */
-		this._ranges = [];
+		this._document = document;
 
 		/**
 		 * Specifies whether the last added range was added as a backward or forward range.
@@ -48,6 +49,14 @@ export default class Selection {
 		 * @member {Boolean} core.treeModel.Selection#_lastRangeBackward
 		 */
 		this._lastRangeBackward = false;
+
+		/**
+		 * Stores all ranges that are selected.
+		 *
+		 * @private
+		 * @member {Array.<core.treeModel.LiveRange>} core.treeModel.Selection#_ranges
+		 */
+		this._ranges = [];
 	}
 
 	/**
@@ -229,6 +238,7 @@ export default class Selection {
 	 */
 	clearAttributes() {
 		this._attrs.clear();
+		this._setStoredAttributesTo( new Map() );
 	}
 
 	/**
@@ -264,10 +274,10 @@ export default class Selection {
 	 * Removes an attribute with given key from the selection.
 	 *
 	 * @param {String} key Key of attribute to remove.
-	 * @returns {Boolean} `true` if the attribute was set on the selection, `false` otherwise.
 	 */
 	removeAttribute( key ) {
-		return this._attrs.delete( key );
+		this._attrs.delete( key );
+		this._removeStoredAttribute( key );
 	}
 
 	/**
@@ -278,6 +288,7 @@ export default class Selection {
 	 */
 	setAttribute( key, value ) {
 		this._attrs.set( key, value );
+		this._storeAttribute( key, value );
 	}
 
 	/**
@@ -287,6 +298,7 @@ export default class Selection {
 	 */
 	setAttributesTo( attrs ) {
 		this._attrs = utils.toMap( attrs );
+		this._setStoredAttributesTo( this._attrs );
 	}
 
 	/**
@@ -317,24 +329,177 @@ export default class Selection {
 	}
 
 	/**
-	 * Iterates through given set of attributes looking for attributes stored for selection. Keeps all such attributes
-	 * and removes others. Then, converts attributes keys from store key to original key.
+	 * Iterates through all attributes stored in current selection's parent.
+	 *
+	 * @returns {Iterable.<*>}
+	 */
+	*_getStoredAttributes() {
+		if ( this.hasAnyRange ) {
+			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 ) {
+		if ( this.hasAnyRange ) {
+			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 ) {
+		if ( this.hasAnyRange ) {
+			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} attrs Iterable object containing attributes to be filtered. See {@link core.treeModel.Node#getAttributes}.
-	 * @returns {Map} Map containing filtered attributes with keys converted to their original state.
+	 * @param {Iterable|Object} attrs Iterable object containing attributes to be set.
+	 * @private
 	 */
-	static filterStoreAttributes( attrs ) {
-		const filtered = new Map();
+	_setStoredAttributesTo( attrs ) {
+		if ( this.hasAnyRange ) {
+			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 ) {
-			if ( attr[ 0 ].indexOf( storePrefix ) === 0 ) {
-				const realKey = attr[ 0 ].substr( storePrefix.length );
+					for ( let attr of attrs ) {
+						const storeKey = Selection._getStoreAttributeKey( attr[ 0 ] );
 
-				filtered.set( realKey, attr[ 1 ] );
+						batch.setAttr( storeKey, attr[ 1 ], selectionParent );
+					}
+				} );
 			}
 		}
+	}
 
-		return filtered;
+	/**
+	 * Updates this selection attributes basing on it's position in the Tree Model.
+	 *
+	 * @private
+	 */
+	_updateAttributes() {
+		if ( !this.hasAnyRange ) {
+			this.clearAttributes();
+		} else {
+			const position = this.getFirstPosition();
+			const positionParent = position.parent;
+			let attrs = null;
+
+			if ( this.isCollapsed === false ) {
+				// 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 ) {
+					if ( item.type == 'TEXT' ) {
+						attrs = item.item.getAttributes();
+						break;
+					}
+				}
+			}
+
+			// 2. If the selection is a caret or the range does not contain a character node...
+			if ( !attrs && this.isCollapsed === true ) {
+				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;
+		}
 	}
 
 	/**
@@ -343,7 +508,7 @@ export default class Selection {
 	 * @param {String} key Attribute key to convert.
 	 * @returns {String} Converted attribute key, applicable for selection store.
 	 */
-	static getStoreAttributeKey( key ) {
+	static _getStoreAttributeKey( key ) {
 		return storePrefix + key;
 	}
 }

+ 0 - 6
packages/ckeditor5-engine/tests/commands/attributecommand.js

@@ -11,7 +11,6 @@ import Text from '/ckeditor5/core/treemodel/text.js';
 import Range from '/ckeditor5/core/treemodel/range.js';
 import Position from '/ckeditor5/core/treemodel/position.js';
 import Element from '/ckeditor5/core/treemodel/element.js';
-import Selection from '/ckeditor5/core/treemodel/selection.js';
 
 let element, editor, command, modelDoc, root;
 
@@ -152,11 +151,7 @@ describe( '_execute', () => {
 		expect( command.value ).to.be.true;
 		expect( modelDoc.selection.hasAttribute( 'bold' ) ).to.be.true;
 
-		let selectionParent = root.getChild( 1 );
-
 		// Attribute should be stored.
-		expect( selectionParent.hasAttribute( Selection.getStoreAttributeKey( 'bold' ) ) ).to.be.true;
-
 		// Simulate clicking somewhere else in the editor.
 		modelDoc.selection.setRanges( [ new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 0, 2 ] ) ) ] );
 
@@ -172,7 +167,6 @@ describe( '_execute', () => {
 
 		expect( command.value ).to.be.false;
 		expect( modelDoc.selection.hasAttribute( 'bold' ) ).to.be.false;
-		expect( selectionParent.hasAttribute( Selection.getStoreAttributeKey( 'bold' ) ) ).to.be.false;
 	} );
 
 	it( 'should not throw and do nothing if selection has no ranges', () => {

+ 9 - 68
packages/ckeditor5-engine/tests/treemodel/document/document.js

@@ -11,10 +11,6 @@ import Document from '/ckeditor5/core/treemodel/document.js';
 import RootElement from '/ckeditor5/core/treemodel/rootelement.js';
 import Batch from '/ckeditor5/core/treemodel/batch.js';
 import CKEditorError from '/ckeditor5/core/ckeditorerror.js';
-import Text from '/ckeditor5/core/treemodel/text.js';
-import Element from '/ckeditor5/core/treemodel/element.js';
-import Range from '/ckeditor5/core/treemodel/range.js';
-import Position from '/ckeditor5/core/treemodel/position.js';
 
 describe( 'Document', () => {
 	let doc;
@@ -175,74 +171,19 @@ describe( 'Document', () => {
 		} );
 	} );
 
-	describe( '_updateSelectionAttributes', () => {
-		let root;
-		beforeEach( () => {
-			root = doc.createRoot( 'root' );
-			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( 'should be fired whenever selection gets updated', () => {
-			sinon.spy( doc, '_updateSelectionAttributes' );
-
-			doc.selection.fire( 'update' );
-
-			expect( doc._updateSelectionAttributes.called ).to.be.true;
-		} );
-
-		it( 'should be fired whenever changes to Tree Model are applied', () => {
-			sinon.spy( doc, '_updateSelectionAttributes' );
+	it( 'should update selection attributes whenever selection gets updated', () => {
+		sinon.spy( doc.selection, '_updateAttributes' );
 
-			doc.fire( 'changesDone' );
+		doc.selection.fire( 'update' );
 
-			expect( doc._updateSelectionAttributes.called ).to.be.true;
-		} );
-
-		it( 'if selection is a range, should find first character in it and copy it\'s attributes', () => {
-			doc.selection.setRanges( [ new Range( new Position( root, [ 2 ] ), new Position( root, [ 5 ] ) ) ] );
-
-			expect( Array.from( doc.selection.getAttributes() ) ).to.deep.equal( [ [ 'b', true ] ] );
+		expect( doc.selection._updateAttributes.called ).to.be.true;
+	} );
 
-			// Step into elements when looking for first character:
-			doc.selection.setRanges( [ new Range( new Position( root, [ 5 ] ), new Position( root, [ 7 ] ) ) ] );
+	it( 'should update selection attributes whenever changes to the document are applied', () => {
+		sinon.spy( doc.selection, '_updateAttributes' );
 
-			expect( Array.from( doc.selection.getAttributes() ) ).to.deep.equal( [ [ 'd', true ] ] );
-		} );
+		doc.fire( 'changesDone' );
 
-		it( 'if selection is collapsed it should seek a character to copy that character\'s attributes', () => {
-			// Take styles from character before selection.
-			doc.selection.setRanges( [ new Range( new Position( root, [ 2 ] ), new Position( root, [ 2 ] ) ) ] );
-			expect( Array.from( doc.selection.getAttributes() ) ).to.deep.equal( [ [ 'a', true ] ] );
-
-			// If there are none,
-			// Take styles from character after selection.
-			doc.selection.setRanges( [ new Range( new Position( root, [ 3 ] ), new Position( root, [ 3 ] ) ) ] );
-			expect( Array.from( doc.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.
-			doc.selection.setRanges( [ new Range( new Position( root, [ 6 ] ), new Position( root, [ 6 ] ) ) ] );
-			expect( Array.from( doc.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.
-			doc.selection.setRanges( [ new Range( new Position( root, [ 0 ] ), new Position( root, [ 0 ] ) ) ] );
-			expect( Array.from( doc.selection.getAttributes() ) ).to.deep.equal( [ [ 'a', true ] ] );
-
-			// If there are no characters to copy attributes from, clear selection attributes.
-			doc.selection.setRanges( [ new Range( new Position( root, [ 0, 0 ] ), new Position( root, [ 0, 0 ] ) ) ] );
-			expect( Array.from( doc.selection.getAttributes() ) ).to.deep.equal( [] );
-		} );
+		expect( doc.selection._updateAttributes.called ).to.be.true;
 	} );
 } );

+ 147 - 8
packages/ckeditor5-engine/tests/treemodel/selection.js

@@ -9,6 +9,7 @@
 
 import Document from '/ckeditor5/core/treemodel/document.js';
 import Element from '/ckeditor5/core/treemodel/element.js';
+import Text from '/ckeditor5/core/treemodel/text.js';
 import Range from '/ckeditor5/core/treemodel/range.js';
 import Position from '/ckeditor5/core/treemodel/position.js';
 import LiveRange from '/ckeditor5/core/treemodel/liverange.js';
@@ -29,14 +30,23 @@ describe( 'Selection', () => {
 	beforeEach( () => {
 		doc = new Document();
 		root = doc.createRoot( 'root' );
-		selection = new Selection();
+		root.insertChildren( 0, [
+			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;
 
 		liveRange = new LiveRange( new Position( root, [ 0 ] ), new Position( root, [ 1 ] ) );
 		range = new Range( new Position( root, [ 2 ] ), new Position( root, [ 2, 2 ] ) );
 	} );
 
 	afterEach( () => {
-		selection.detach();
+		doc.detach();
 		liveRange.detach();
 	} );
 
@@ -472,16 +482,43 @@ describe( 'Selection', () => {
 	} );
 
 	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' );
 			} );
 		} );
 
 		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;
@@ -500,6 +537,7 @@ describe( 'Selection', () => {
 
 		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' );
 
@@ -511,32 +549,69 @@ describe( 'Selection', () => {
 
 		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;
 			} );
 		} );
 
 		describe( 'removeAttribute', () => {
-			it( 'should remove attribute set on the text fragment and return true', () => {
+			it( 'should remove attribute set on the text fragment', () => {
+				selection.setRanges( [ rangeInFullP ] );
 				selection.setAttribute( 'foo', 'bar' );
-				let result = selection.removeAttribute( 'foo' );
+				selection.removeAttribute( 'foo' );
 
 				expect( selection.getAttribute( 'foo' ) ).to.be.undefined;
-				expect( result ).to.be.true;
+
+				expect( fullP.hasAttribute( Selection._getStoreAttributeKey( 'foo' ) ) ).to.be.false;
 			} );
 
-			it( 'should return false if text fragment does not have given attribute', () => {
-				let result = selection.removeAttribute( 'abc' );
+			it( 'should remove stored attribute if the selection is in empty node', () => {
+				selection.setRanges( [ rangeInEmptyP ] );
+				selection.setAttribute( 'foo', 'bar' );
+				selection.removeAttribute( 'foo' );
 
-				expect( result ).to.be.false;
+				expect( selection.getAttribute( 'foo' ) ).to.be.undefined;
+
+				expect( emptyP.hasAttribute( Selection._getStoreAttributeKey( 'foo' ) ) ).to.be.false;
 			} );
 		} );
 
 		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' );
 
@@ -544,7 +619,71 @@ describe( 'Selection', () => {
 
 				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;
 			} );
 		} );
 	} );
+
+	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( '_getStoredAttributes', () => {
+		it( 'should return no values if there are no ranges in selection', () => {
+			let values = Array.from( selection._getStoredAttributes() );
+
+			expect( values ).to.deep.equal( [] );
+		} );
+	} );
 } );