Browse Source

Add treeModel.NodeList#setAttribute.

Szymon Cofalik 10 years ago
parent
commit
2bd882e758

+ 52 - 0
packages/ckeditor5-engine/src/treemodel/nodelist.js

@@ -9,6 +9,7 @@ import CharacterProxy from './characterproxy.js';
 import Text from './text.js';
 import Text from './text.js';
 import utils from '../utils.js';
 import utils from '../utils.js';
 import langUtils from '../lib/lodash/lang.js';
 import langUtils from '../lib/lodash/lang.js';
+import CKEditorError from '../ckeditorerror.js';
 
 
 /**
 /**
  * Value that is convertible to an item kept in {@link treeModel.NodeList} or an iterable collection of such items.
  * Value that is convertible to an item kept in {@link treeModel.NodeList} or an iterable collection of such items.
@@ -309,6 +310,57 @@ export default class NodeList {
 		return new NodeList( removed );
 		return new NodeList( removed );
 	}
 	}
 
 
+	/**
+	 * Sets or removes given attribute on a range of nodes in the node list.
+	 *
+	 * @param {Number} index Position of the first node to change.
+	 * @param {Number} number Number of nodes to change.
+	 * @param {String} key Attribute key to change.
+	 * @param {treeModel.Attribute} [attribute] New attribute or null if attribute with given key should be removed.
+	 */
+	setAttribute( index, number, key, attribute ) {
+		if ( index < 0 || index + number > this.length ) {
+			/**
+			 * Trying to set attribute on non-existing node list items.
+			 *
+			 * @error nodelist-setattribute-out-of-bounds
+			 * @param root
+			 */
+			throw new CKEditorError( 'nodelist-setattribute-out-of-bounds: Trying to set attribute on non-existing node list items.' );
+		}
+
+		// "Range" of nodes to remove attributes may start in NodeListText or end in NodeListText. Some splitting may be needed.
+		this._splitNodeAt( index );
+		this._splitNodeAt( index + number );
+
+		let i = index;
+
+		while ( i < index + number ) {
+			let node = this._nodes[ this._indexMap[ i ] ];
+
+			if ( node instanceof NodeListText ) {
+				if ( attribute ) {
+					node._attrs.set( attribute );
+				} else {
+					node._attrs.delete( key );
+				}
+
+				this._mergeNodeAt( i );
+				i += node.text.length;
+			} else {
+				if ( attribute ) {
+					node.setAttribute( attribute );
+				} else {
+					node.removeAttribute( key );
+				}
+
+				i++;
+			}
+		}
+
+		this._mergeNodeAt( index + number );
+	}
+
 	/**
 	/**
 	 * Checks whether given index is inside a text and if so, splits that text node. This method should be used
 	 * Checks whether given index is inside a text and if so, splits that text node. This method should be used
 	 * to split text objects whenever there are some changes made on a part of text object (i.e. removing part of text,
 	 * to split text objects whenever there are some changes made on a part of text object (i.e. removing part of text,

+ 36 - 44
packages/ckeditor5-engine/src/treemodel/operation/attributeoperation.js

@@ -8,6 +8,7 @@
 import Operation from './operation.js';
 import Operation from './operation.js';
 import Range from '../range.js';
 import Range from '../range.js';
 import CKEditorError from '../../ckeditorerror.js';
 import CKEditorError from '../../ckeditorerror.js';
+import TextFragment from '../textfragment.js';
 
 
 /**
 /**
  * Operation to change nodes' attribute. Using this class you can add, remove or change value of the attribute.
  * Operation to change nodes' attribute. Using this class you can add, remove or change value of the attribute.
@@ -91,57 +92,48 @@ export default class AttributeOperation extends Operation {
 			);
 			);
 		}
 		}
 
 
-		// Split text nodes if needed, then get the nodes in the range and convert it to node list. It will be easier to operate.
-		this.range.start.parent._children._splitNodeAt( this.range.start.offset );
-		this.range.end.parent._children._splitNodeAt( this.range.end.offset );
-
-		// Remove or change.
-		if ( oldAttr !== null ) {
-			for ( let node of this.range.getAllNodes( true ) ) {
-				if ( !node.hasAttribute( oldAttr ) ) {
-					/**
-					 * The attribute which should be removed does not exists for the given node.
-					 *
-					 * @error operation-attribute-no-attr-to-remove
-					 * @param {treeModel.Node} node
-					 * @param {treeModel.Attribute} attr
-					 */
-					throw new CKEditorError(
-						'operation-attribute-no-attr-to-remove: The attribute which should be removed does not exists for given node.',
-						{ node: node, attr: oldAttr }
-					);
-				}
+		for ( let item of this.range.getAllNodes( true ) ) {
+			if ( oldAttr !== null && !item.hasAttribute( oldAttr ) ) {
+				/**
+				 * The attribute which should be removed does not exists for the given node.
+				 *
+				 * @error operation-attribute-no-attr-to-remove
+				 * @param {treeModel.Node} node
+				 * @param {treeModel.Attribute} attr
+				 */
+				throw new CKEditorError(
+					'operation-attribute-no-attr-to-remove: The attribute which should be removed does not exists for given node.',
+					{ node: item, attr: oldAttr }
+				);
+			}
 
 
-				if ( newAttr === null ) {
-					node.removeAttribute( oldAttr.key );
-				}
+			if ( oldAttr === null && newAttr !== null && item.hasAttribute( newAttr.key ) ) {
+				/**
+				 * The attribute with given key already exists for the given node.
+				 *
+				 * @error operation-attribute-attr-exists
+				 * @param {treeModel.Node} node
+				 * @param {treeModel.Attribute} attr
+				 */
+				throw new CKEditorError(
+					'operation-attribute-attr-exists: The attribute with given key already exists.',
+					{ node: item, attr: newAttr }
+				);
 			}
 			}
-		}
 
 
-		// Insert or change.
-		if ( newAttr !== null ) {
-			for ( let node of this.range.getAllNodes( true ) ) {
-				if ( oldAttr === null && node.hasAttribute( newAttr.key ) ) {
-					/**
-					 * The attribute with given key already exists for the given node.
-					 *
-					 * @error operation-attribute-attr-exists
-					 * @param {treeModel.Node} node
-					 * @param {treeModel.Attribute} attr
-					 */
-					throw new CKEditorError(
-						'operation-attribute-attr-exists: The attribute with given key already exists.',
-						{ node: node, attr: newAttr }
-					);
+			if ( item instanceof TextFragment ) {
+				let key = newAttr ? newAttr.key : oldAttr.key;
+				item.commonParent._children.setAttribute( item.first.getIndex(), item.text.length, key, newAttr );
+			}
+			else {
+				if ( newAttr ) {
+					item.setAttribute( newAttr );
+				} else {
+					item.removeAttribute( oldAttr.key );
 				}
 				}
-
-				node.setAttribute( newAttr );
 			}
 			}
 		}
 		}
 
 
-		this.range.start.parent._children._mergeNodeAt( this.range.start.offset );
-		this.range.end.parent._children._mergeNodeAt( this.range.end.offset );
-
 		return { range: this.range, oldAttr: oldAttr, newAttr: newAttr };
 		return { range: this.range, oldAttr: oldAttr, newAttr: newAttr };
 	}
 	}
 }
 }

+ 9 - 0
packages/ckeditor5-engine/src/treemodel/textfragment.js

@@ -54,6 +54,15 @@ export default class TextFragment {
 		this.last = this.getCharAt( this.text.length - 1 );
 		this.last = this.getCharAt( this.text.length - 1 );
 	}
 	}
 
 
+	/**
+	 * A common parent of all character nodes contained in {@link treeModel.TextFragment}.
+	 *
+	 * @property {treeModel.Element} commonParent
+	 */
+	get commonParent() {
+		return this.first.parent;
+	}
+
 	/**
 	/**
 	 * Gets a character at given index and creates a {@link treeModel.CharacterProxy} out of it.
 	 * Gets a character at given index and creates a {@link treeModel.CharacterProxy} out of it.
 	 *
 	 *

+ 58 - 0
packages/ckeditor5-engine/tests/treemodel/nodelist.js

@@ -11,6 +11,7 @@ import NodeList from '/ckeditor5/core/treemodel/nodelist.js';
 import Element from '/ckeditor5/core/treemodel/element.js';
 import Element from '/ckeditor5/core/treemodel/element.js';
 import Text from '/ckeditor5/core/treemodel/text.js';
 import Text from '/ckeditor5/core/treemodel/text.js';
 import Attribute from '/ckeditor5/core/treemodel/attribute.js';
 import Attribute from '/ckeditor5/core/treemodel/attribute.js';
+import CKEditorError from '/ckeditor5/core/ckeditorerror.js';
 
 
 describe( 'NodeList', () => {
 describe( 'NodeList', () => {
 	describe( 'constructor', () => {
 	describe( 'constructor', () => {
@@ -220,6 +221,63 @@ describe( 'NodeList', () => {
 		} );
 		} );
 	} );
 	} );
 
 
+	describe( 'setAttribute', () => {
+		it( 'should change attribute for multiple items in node list but not for their children', () => {
+			let attr = new Attribute( 'a', true );
+			let p = new Element( 'p', [], 'x' );
+			let div = new Element( 'div' );
+
+			let nodeList = new NodeList( [ p, 'foo', div, 'bar' ] );
+
+			nodeList.setAttribute( 0, 6, attr.key, attr );
+
+			// Attribute set.
+			expect( p.hasAttribute( 'a' ) ).to.be.true;
+			expect( nodeList.get( 1 ).hasAttribute( 'a' ) ).to.be.true;
+			expect( nodeList.get( 2 ).hasAttribute( 'a' ) ).to.be.true;
+			expect( nodeList.get( 3 ).hasAttribute( 'a' ) ).to.be.true;
+			expect( div.hasAttribute( 'a' ) ).to.be.true;
+			expect( nodeList.get( 5 ).hasAttribute( 'a' ) ).to.be.true;
+			expect( nodeList.get( 6 ).hasAttribute( 'a' ) ).to.be.false;
+			expect( nodeList.get( 7 ).hasAttribute( 'a' ) ).to.be.false;
+
+			// Attribute not set for children.
+			expect( p.getChild( 0 ).hasAttribute( 'a' ) ).to.be.false;
+		} );
+
+		it( 'should remove attribute if no new attribute has been passed', () => {
+			let attr = new Attribute( 'a', true );
+			let p = new Element( 'p', [ attr ] );
+			let text = new Text( 'foobar', [ attr ] );
+			let nodeList = new NodeList( [ p, text ] );
+
+			nodeList.setAttribute( 0, 4, attr.key, null );
+
+			expect( p.hasAttribute( 'a' ) ).to.be.false;
+			expect( nodeList.get( 1 ).hasAttribute( 'a' ) ).to.be.false;
+			expect( nodeList.get( 2 ).hasAttribute( 'a' ) ).to.be.false;
+			expect( nodeList.get( 3 ).hasAttribute( 'a' ) ).to.be.false;
+			expect( nodeList.get( 4 ).hasAttribute( 'a' ) ).to.be.true;
+			expect( nodeList.get( 5 ).hasAttribute( 'a' ) ).to.be.true;
+
+			expect( nodeList._nodes.length ).to.equal( 3 );
+		} );
+
+		it( 'should throw if wrong index or number is passed', () => {
+			let attr = new Attribute( 'a', true );
+			let text = new Text( 'foo', [ attr ] );
+			let nodeList = new NodeList( text );
+
+			expect( () => {
+				nodeList.setAttribute( -1, 2, attr.key, null );
+			} ).to.throw( CKEditorError, /nodelist-setattribute-out-of-bounds/ );
+
+			expect( () => {
+				nodeList.setAttribute( 2, 2, attr.key, null );
+			} ).to.throw( CKEditorError, /nodelist-setattribute-out-of-bounds/ );
+		} );
+	} );
+
 	describe( '_splitNodeAt', () => {
 	describe( '_splitNodeAt', () => {
 		it( 'should split text object into two text objects', () => {
 		it( 'should split text object into two text objects', () => {
 			let nodeList = new NodeList( 'abcd' );
 			let nodeList = new NodeList( 'abcd' );