浏览代码

Introduced Collection.

fredck 10 年之前
父节点
当前提交
cf8a13ece1

+ 58 - 13
packages/ckeditor5-utils/src/emitter.js

@@ -211,32 +211,77 @@ CKEDITOR.define( [ 'eventinfo', 'utils' ], function( EventInfo, utils ) {
 		 * @param {...*} [args] Additional arguments to be passed to the callbacks.
 		 */
 		fire: function( event, args ) {
-			var callbacks = getCallbacksIfAny( this, event );
+			var eventInfo = event instanceof EventInfo && event;
+			var eventName = eventInfo ? eventInfo.name : event;
+			var parents = this._parentEmitters;
 
-			if ( !callbacks ) {
+			var callbacks = getCallbacksIfAny( this, eventName );
+
+			if ( !callbacks && !parents ) {
 				return;
 			}
 
-			var eventInfo = new EventInfo( this, event );
+			if ( !eventInfo ) {
+				eventInfo = new EventInfo( this, eventName );
+			}
 
 			// Take the list of arguments to pass to the callbacks.
 			args = Array.prototype.slice.call( arguments, 1 );
 			args.unshift( eventInfo );
 
-			for ( var i = 0; i < callbacks.length; i++ ) {
-				callbacks[ i ].callback.apply( callbacks[ i ].ctx, args );
+			if ( callbacks ) {
+				for ( var i = 0; i < callbacks.length; i++ ) {
+					callbacks[ i ].callback.apply( callbacks[ i ].ctx, args );
 
-				if ( eventInfo.stop.called ) {
-					break;
+					if ( eventInfo.stop.called ) {
+						break;
+					}
+
+					if ( eventInfo.off.called ) {
+						// Remove the called mark for the next calls.
+						delete eventInfo.off.called;
+
+						// Remove the callback from the list (fixing the next index).
+						callbacks.splice( i, 1 );
+						i--;
+					}
 				}
+			}
 
-				if ( eventInfo.off.called ) {
-					// Remove the called mark for the next calls.
-					delete eventInfo.off.called;
+			// Fires the same event on all parents.
+			if ( parents ) {
+				for ( var e = 0; e < parents.length; e++ ) {
+					parents[ e ].fire.apply( parents[ e ], args );
+				}
+			}
+		},
 
-					// Remove the callback from the list (fixing the next index).
-					callbacks.splice( i, 1 );
-					i--;
+		/**
+		 * Adds a parent emitter to this object.
+		 *
+		 * A parent emitter fires the same events fired by its children.
+		 *
+		 * @param {Emitter} parentEmitter The parent to be added.
+		 */
+		addParentEmitter: function( parentEmitter ) {
+			if ( !this._parentEmitters ) {
+				this._parentEmitters = [];
+			}
+
+			this._parentEmitters.push( parentEmitter );
+		},
+
+		/**
+		 * Removes a parent emitter from this object.
+		 *
+		 * @param {Emitter} parentEmitter The parent to be removed.
+		 */
+		removeParentEmitter: function( parentEmitter ) {
+			if ( this._parentEmitters ) {
+				this._parentEmitters.splice( this._parentEmitters.indexOf( parentEmitter ), 1 );
+
+				if ( !this._parentEmitters.length ) {
+					delete this._parentEmitters;
 				}
 			}
 		}

+ 109 - 0
packages/ckeditor5-utils/src/mvc/collection.js

@@ -0,0 +1,109 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+/**
+ * Collections are ordered sets of models.
+ *
+ * Any event that is triggered on a model in a collection will also be triggered on the collection directly, for
+ * convenience.
+ *
+ * @class Collection
+ */
+
+CKEDITOR.define( [ 'emitter', 'utils' ], function( EmitterMixin, utils ) {
+	function Collection() {
+		/**
+		 * The internal list of models in the collection.
+		 *
+		 * @property _models
+		 * @private
+		 */
+		Object.defineProperty( this, '_models', {
+			value: []
+		} );
+
+		/**
+		 * The number of items available in the collection.
+		 *
+		 * @property _models
+		 */
+		Object.defineProperty( this, 'length', {
+			get: function() {
+				return this._models.length;
+			}
+		} );
+	}
+
+	/**
+	 * @inheritdoc utils#extend
+	 */
+	Collection.extend = utils.extendMixin;
+
+	utils.extend( Collection.prototype, EmitterMixin, {
+		/**
+		 * Adds an item into the collection.
+		 *
+		 * Note that this os an array-like collection, so the same item can be present more than once. This behavior is
+		 * for performance purposes only and is not guaranteed to be kept in the same way in the future.
+		 *
+		 * @param {Model} model The item to be added.
+		 */
+		add: function( model ) {
+			model.addParentEmitter( this );
+
+			this._models.push( model );
+
+			this.fire( 'add', model );
+		},
+
+		/**
+		 * Gets one item from the collection.
+		 *
+		 * @param {Number} index The index to take the item from.
+		 * @returns {Model} The requested item.
+		 */
+		get: function( index ) {
+			var model = this._models[ index ];
+
+			if ( !model ) {
+				throw 'Index not found';
+			}
+
+			return model;
+		},
+
+		/**
+		 * Removes an item from the collection.
+		 *
+		 * @param {Model|Number} modelOrIndex Either the item itself or its index inside the collection.
+		 * @returns {Model} The removed item.
+		 */
+		remove: function( modelOrIndex ) {
+			// If a model has been passed, convert it to its index.
+			if ( typeof modelOrIndex != 'number' ) {
+				modelOrIndex = this._models.indexOf( modelOrIndex );
+
+				if ( modelOrIndex == -1 ) {
+					throw 'Model not found';
+				}
+			}
+
+			var removedModel = this._models.splice( modelOrIndex, 1 )[ 0 ];
+
+			if ( !removedModel ) {
+				throw 'Index not found';
+			}
+
+			removedModel.removeParentEmitter( this );
+			this.fire( 'remove', removedModel );
+
+			return removedModel;
+		}
+	} );
+
+	return Collection;
+} );

+ 48 - 0
packages/ckeditor5-utils/tests/emitter/emitter.js

@@ -340,6 +340,54 @@ describe( 'stopListening', function() {
 	} );
 } );
 
+describe( 'addParentEmitter', function() {
+	it( 'should propagate events to the parent', function() {
+		var parentEmitter = getEmitterInstance();
+		emitter.addParentEmitter( parentEmitter );
+
+		var parentSpy = sinon.spy();
+
+		parentEmitter.on( 'test', parentSpy );
+
+		emitter.fire( 'test', 'a' );
+		emitter.fire( 'test' );
+
+		sinon.assert.calledTwice( parentSpy );
+		sinon.assert.calledWithExactly( parentSpy, sinon.match.has( 'source', emitter ) , 'a' );
+		sinon.assert.calledWithExactly( parentSpy, sinon.match.has( 'source', emitter ) );
+	} );
+} );
+
+describe( 'removeParentEmitter', function() {
+	it( 'should stop propagating events to the parent', function() {
+		var parentEmitter = getEmitterInstance();
+		emitter.addParentEmitter( parentEmitter );
+
+		var parentSpy = sinon.spy();
+		var childSpy = sinon.spy();
+
+		parentEmitter.on( 'test', parentSpy );
+		emitter.on( 'test', childSpy );
+
+		emitter.fire( 'test' );
+
+		sinon.assert.calledOnce( parentSpy );
+		sinon.assert.calledOnce( childSpy );
+
+		emitter.removeParentEmitter( parentEmitter );
+
+		parentSpy.reset();
+		childSpy.reset();
+
+		emitter.fire( 'test' );
+
+		sinon.assert.notCalled( parentSpy );
+		sinon.assert.calledOnce( childSpy );
+
+		emitter.removeParentEmitter( parentEmitter );
+	} );
+} );
+
 function refreshEmitter() {
 	emitter = getEmitterInstance();
 }

+ 149 - 0
packages/ckeditor5-utils/tests/mvc/collection/collection.js

@@ -0,0 +1,149 @@
+/**
+ * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals describe, it, expect, bender, sinon */
+
+'use strict';
+
+var modules = bender.amd.require( 'mvc/collection', 'mvc/model' );
+
+describe( 'add', function() {
+	it( 'should change length and enable get', function() {
+		var Model = modules[ 'mvc/model' ];
+
+		var box = getCollection();
+
+		expect( box.length ).to.equals( 0 );
+
+		box.add( getItem() );
+
+		expect( box.length ).to.equals( 1 );
+
+		expect( box.get( 0 ) ).to.be.an.instanceof( Model );
+	} );
+
+	it( 'should fire the "add" event', function() {
+		var spy = sinon.spy();
+
+		var box = getCollection();
+		box.on( 'add', spy );
+
+		var item = getItem();
+		box.add( item );
+
+		sinon.assert.calledWithExactly( spy, sinon.match.has( 'source', box ), item );
+	} );
+} );
+
+describe( 'get', function() {
+	it( 'should throw error on invalid index', function() {
+		var box = getCollection();
+		box.add( getItem() );
+
+		expect( function() {
+			box.get( 1 );
+		} ).to.throw( 'Index not found' );
+	} );
+} );
+
+describe( 'remove', function() {
+	it( 'should remove the model by index', function() {
+		var box = getCollection();
+		var item = getItem();
+
+		box.add( item );
+
+		expect( box.length ).to.equals( 1 );
+
+		box.remove( 0 );
+
+		expect( box.length ).to.equals( 0 );
+	} );
+
+	it( 'should remove the model by model', function() {
+		var box = getCollection();
+		var item = getItem();
+
+		box.add( item );
+
+		expect( box.length ).to.equals( 1 );
+
+		box.remove( item );
+
+		expect( box.length ).to.equals( 0 );
+	} );
+
+	it( 'should fire the "remove" event', function() {
+		var box = getCollection();
+		var item1 = getItem();
+		var item2 = getItem();
+
+		box.add( item1 );
+		box.add( item2 );
+
+		var spy = sinon.spy();
+
+		box.on( 'remove', spy );
+
+		box.remove( 1 );		// by index
+		box.remove( item1 );	// by model
+
+		sinon.assert.calledTwice( spy );
+		sinon.assert.calledWithExactly( spy, sinon.match.has( 'source', box ), item1 );
+		sinon.assert.calledWithExactly( spy, sinon.match.has( 'source', box ), item2 );
+	} );
+
+	it( 'should throw error on invalid index', function() {
+		var box = getCollection();
+		box.add( getItem() );
+
+		expect( function() {
+			box.remove( 1 );
+		} ).to.throw( 'Index not found' );
+	} );
+
+	it( 'should throw error on invalid model', function() {
+		var box = getCollection();
+		box.add( getItem() );
+
+		expect( function() {
+			box.remove( getItem() );
+		} ).to.throw( 'Model not found' );
+	} );
+} );
+
+describe( 'on', function() {
+	it( 'should listen to child events', function() {
+		var box = getCollection();
+		var item1 = getItem();
+		var item2 = getItem();
+		var spy = sinon.spy();
+
+		box.add( item1 );
+		box.add( item2 );
+
+		box.on( 'item-event', spy );
+
+		item1.fire( 'item-event' );
+		item2.fire( 'item-event' );
+
+		sinon.assert.calledTwice( spy );
+		sinon.assert.alwaysCalledOn( spy, box );
+		sinon.assert.calledWithExactly( spy, sinon.match.has( 'source', item1 ) );
+		sinon.assert.calledWithExactly( spy, sinon.match.has( 'source', item2 ) );
+	} );
+} );
+
+function getCollection() {
+	var Collection = modules[ 'mvc/collection' ];
+
+	return new Collection();
+}
+
+function getItem() {
+	var Model = modules[ 'mvc/model' ];
+
+	return new Model();
+}