Browse Source

Introduce treeModel.Document#_setSelectionAttributes.

Szymon Cofalik 9 years ago
parent
commit
a6ae92c685

+ 82 - 0
packages/ckeditor5-engine/src/treemodel/document.js

@@ -16,6 +16,7 @@ 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';
 
 const graveyardSymbol = Symbol( 'graveyard' );
 
@@ -74,6 +75,14 @@ export default class Document {
 		 * @member {core.treeModel.Selection} core.treeModel.Document#selection
 		 */
 		this.selection = new Selection();
+
+		this.selection.on( 'update', () => {
+			this._setSelectionAttributes();
+		} );
+
+		this.on( 'changesDone', () => {
+			this._setSelectionAttributes();
+		} );
 	}
 
 	/**
@@ -198,6 +207,79 @@ export default class Document {
 		return this.roots.get( name );
 	}
 
+	_setSelectionAttributes() {
+		if ( !this.selection.hasAnyRange ) {
+			this.selection.clearAttributes();
+		} else {
+			let position = this.selection.getFirstPosition();
+			let positionParent = position.parent;
+			let attrs = null;
+
+			if ( this.selection.isCollapsed === false ) {
+				// 1. If selection is a range...
+				let 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 == 'CHARACTER' ) {
+						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 ) {
+				let nodeBefore = positionParent.getChild( position.offset - 1 );
+				let 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 !== null && !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 !== null && !attrs ) {
+						node = node.nextSibling;
+						attrs = getAttrsIfCharacter( node );
+					}
+				}
+
+				// 6. If not found, selection won't get any attributes.
+			}
+
+			if ( attrs ) {
+				this.selection.setAttributesTo( attrs );
+			} else {
+				this.selection.clearAttributes();
+			}
+		}
+
+		function getAttrsIfCharacter( node ) {
+			if ( node instanceof CharacterProxy ) {
+				return node.getAttributes();
+			}
+
+			return null;
+		}
+	}
+
 	/**
 	 * Fired when document changes by applying an operation.
 	 *

+ 75 - 0
packages/ckeditor5-engine/tests/treemodel/document/document.js

@@ -11,6 +11,10 @@ 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;
@@ -170,4 +174,75 @@ describe( 'Document', () => {
 			expect( order[ 6 ] ).to.equal( 'done' );
 		} );
 	} );
+
+	describe( '_setSelectionAttributes', () => {
+		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, '_setSelectionAttributes' );
+
+			doc.selection.fire( 'update' );
+
+			expect( doc._setSelectionAttributes.called ).to.be.true;
+		} );
+
+		it( 'should be fired whenever changes to Tree Model are applied', () => {
+			sinon.spy( doc, '_setSelectionAttributes' );
+
+			doc.fire( 'changesDone' );
+
+			expect( doc._setSelectionAttributes.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 ] ] );
+
+			// Step into elements when looking for first character:
+			doc.selection.setRanges( [ new Range( new Position( root, [ 5 ] ), new Position( root, [ 7 ] ) ) ] );
+
+			expect( Array.from( doc.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.
+			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( [] );
+		} );
+	} );
 } );