all files / src/ EventEmitter.js

100% Statements 101/101
100% Branches 58/58
100% Functions 13/13
100% Lines 99/99
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357                            182×                                       332×   240×   236×       92×                         236× 56×   180×     236×                                   236×     224×           224×                   12×     12×           12×                         92×       84×   82×         76× 12×       76× 76× 76× 104× 78×         76× 68×   76×                                     416× 272×         144× 144× 156×   156×   42×   114×   156×     156× 156×                       144×                                             14×           12× 12× 22×   22×     20×   22×     22× 22×                           12×   10×                         254× 168×     168× 134× 134×   34×     168×   250×                                   86×   26× 26× 26× 20×   26×       60× 42× 42× 42×     68× 42×           84×        
/* global hasOwn */
 
( function () {
 
	/**
	 * @class
	 */
	OO.EventEmitter = function OoEventEmitter() {
		// Properties
 
		/**
		 * Storage of bound event handlers by event name.
		 *
		 * @private
		 * @property {Object} bindings
		 */
		this.bindings = {};
	};
 
	OO.initClass( OO.EventEmitter );
 
	/* Private helper functions */
 
	/**
	 * Validate a function or method call in a context
	 *
	 * For a method name, check that it names a function in the context object
	 *
	 * @private
	 * @param {Function|string} method Function or method name
	 * @param {any} context The context of the call
	 * @throws {Error} A method name is given but there is no context
	 * @throws {Error} In the context object, no property exists with the given name
	 * @throws {Error} In the context object, the named property is not a function
	 */
	function validateMethod( method, context ) {
		// Validate method and context
		if ( typeof method === 'string' ) {
			// Validate method
			if ( context === undefined || context === null ) {
				throw new Error( 'Method name "' + method + '" has no context.' );
			}
			if ( typeof context[ method ] !== 'function' ) {
				// Technically the property could be replaced by a function before
				// call time. But this probably signals a typo.
				throw new Error( 'Property "' + method + '" is not a function' );
			}
		} else if ( typeof method !== 'function' ) {
			throw new Error( 'Invalid callback. Function or method name expected.' );
		}
	}
 
	/**
	 * @private
	 * @param {OO.EventEmitter} eventEmitter Event emitter
	 * @param {string} event Event name
	 * @param {Object} binding
	 */
	function addBinding( eventEmitter, event, binding ) {
		let bindings;
		// Auto-initialize bindings list
		if ( hasOwn.call( eventEmitter.bindings, event ) ) {
			bindings = eventEmitter.bindings[ event ];
		} else {
			bindings = eventEmitter.bindings[ event ] = [];
		}
		// Add binding
		bindings.push( binding );
	}
 
	/* Methods */
 
	/**
	 * Add a listener to events of a specific event.
	 *
	 * The listener can be a function or the string name of a method; if the latter, then the
	 * name lookup happens at the time the listener is called.
	 *
	 * @param {string} event Type of event to listen to
	 * @param {Function|string} method Function or method name to call when event occurs
	 * @param {Array} [args] Arguments to pass to listener, will be prepended to emitted arguments
	 * @param {Object} [context=null] Context object for function or method call
	 * @return {OO.EventEmitter}
	 * @throws {Error} Listener argument is not a function or a valid method name
	 */
	OO.EventEmitter.prototype.on = function ( event, method, args, context ) {
		validateMethod( method, context );
 
		// Ensure consistent object shape (optimisation)
		addBinding( this, event, {
			method: method,
			args: args,
			context: ( arguments.length < 4 ) ? null : context,
			once: false
		} );
		return this;
	};
 
	/**
	 * Add a one-time listener to a specific event.
	 *
	 * @param {string} event Type of event to listen to
	 * @param {Function} listener Listener to call when event occurs
	 * @return {OO.EventEmitter}
	 */
	OO.EventEmitter.prototype.once = function ( event, listener ) {
		validateMethod( listener );
 
		// Ensure consistent object shape (optimisation)
		addBinding( this, event, {
			method: listener,
			args: undefined,
			context: null,
			once: true
		} );
		return this;
	};
 
	/**
	 * Remove a specific listener from a specific event.
	 *
	 * @param {string} event Type of event to remove listener from
	 * @param {Function|string} [method] Listener to remove. Must be in the same form as was passed
	 * to "on". Omit to remove all listeners.
	 * @param {Object} [context=null] Context object function or method call
	 * @return {OO.EventEmitter}
	 * @throws {Error} Listener argument is not a function or a valid method name
	 */
	OO.EventEmitter.prototype.off = function ( event, method, context ) {
		if ( arguments.length === 1 ) {
			// Remove all bindings for event
			delete this.bindings[ event ];
			return this;
		}
 
		validateMethod( method, context );
 
		if ( !hasOwn.call( this.bindings, event ) || !this.bindings[ event ].length ) {
			// No matching bindings
			return this;
		}
 
		// Default to null context
		if ( arguments.length < 3 ) {
			context = null;
		}
 
		// Remove matching handlers
		const bindings = this.bindings[ event ];
		let i = bindings.length;
		while ( i-- ) {
			if ( bindings[ i ].method === method && bindings[ i ].context === context ) {
				bindings.splice( i, 1 );
			}
		}
 
		// Cleanup if now empty
		if ( bindings.length === 0 ) {
			delete this.bindings[ event ];
		}
		return this;
	};
 
	/**
	 * Emit an event.
	 *
	 * All listeners for the event will be called synchronously, in an
	 * unspecified order. If any listeners throw an exception, this won't
	 * disrupt the calls to the remaining listeners; however, the exception
	 * won't be thrown until the next tick.
	 *
	 * Listeners should avoid mutating the emitting object, as this is
	 * something of an anti-pattern which can easily result in
	 * hard-to-understand code with hidden side-effects and dependencies.
	 *
	 * @param {string} event Type of event
	 * @param {...any} [args] Arguments passed to the event handler
	 * @return {boolean} Whether the event was handled by at least one listener
	 */
	OO.EventEmitter.prototype.emit = function ( event, ...args ) {
		if ( !hasOwn.call( this.bindings, event ) ) {
			return false;
		}
 
		// Slicing ensures that we don't get tripped up by event
		// handlers that add/remove bindings
		const bindings = this.bindings[ event ].slice();
		for ( let i = 0; i < bindings.length; i++ ) {
			const binding = bindings[ i ];
			let method;
			if ( typeof binding.method === 'string' ) {
				// Lookup method by name (late binding)
				method = binding.context[ binding.method ];
			} else {
				method = binding.method;
			}
			if ( binding.once ) {
				// Unbind before calling, to avoid any nested triggers.
				this.off( event, method );
			}
			try {
				method.apply(
					binding.context,
					binding.args ? binding.args.concat( args ) : args
				);
			} catch ( e ) {
				// If one listener has an unhandled error, don't have it
				// take down the emitter. But rethrow asynchronously so
				// debuggers can break with a full async stack trace.
				setTimeout( ( ( error ) => {
					throw error;
				} ).bind( null, e ) );
			}
 
		}
		return true;
	};
 
	/**
	 * Emit an event, propagating the first exception some listener throws
	 *
	 * All listeners for the event will be called synchronously, in an
	 * unspecified order. If any listener throws an exception, this won't
	 * disrupt the calls to the remaining listeners. The first exception
	 * thrown will be propagated back to the caller; any others won't be
	 * thrown until the next tick.
	 *
	 * Listeners should avoid mutating the emitting object, as this is
	 * something of an anti-pattern which can easily result in
	 * hard-to-understand code with hidden side-effects and dependencies.
	 *
	 * @param {string} event Type of event
	 * @param {...any} [args] Arguments passed to the event handler
	 * @return {boolean} Whether the event was handled by at least one listener
	 */
	OO.EventEmitter.prototype.emitThrow = function ( event, ...args ) {
		// We tolerate code duplication with #emit, because the
		// alternative is an extra level of indirection which will
		// appear in very many stack traces.
		if ( !hasOwn.call( this.bindings, event ) ) {
			return false;
		}
 
		let firstError;
		// Slicing ensures that we don't get tripped up by event
		// handlers that add/remove bindings
		const bindings = this.bindings[ event ].slice();
		for ( let i = 0; i < bindings.length; i++ ) {
			const binding = bindings[ i ];
			let method;
			if ( typeof binding.method === 'string' ) {
				// Lookup method by name (late binding)
				method = binding.context[ binding.method ];
			} else {
				method = binding.method;
			}
			if ( binding.once ) {
				// Unbind before calling, to avoid any nested triggers.
				this.off( event, method );
			}
			try {
				method.apply(
					binding.context,
					binding.args ? binding.args.concat( args ) : args
				);
			} catch ( e ) {
				if ( firstError === undefined ) {
					firstError = e;
				} else {
					// If one listener has an unhandled error, don't have it
					// take down the emitter. But rethrow asynchronously so
					// debuggers can break with a full async stack trace.
					setTimeout( ( ( error ) => {
						throw error;
					} ).bind( null, e ) );
				}
			}
 
		}
		if ( firstError !== undefined ) {
			throw firstError;
		}
		return true;
	};
 
	/**
	 * Connect event handlers to an object.
	 *
	 * @param {Object} context Object to call methods on when events occur
	 * @param {Object.<string,string>|Object.<string,Function>|Object.<string,Array>} methods
	 *  List of event bindings keyed by event name containing either method names, functions or
	 *  arrays containing method name or function followed by a list of arguments to be passed to
	 *  callback before emitted arguments.
	 * @return {OO.EventEmitter}
	 */
	OO.EventEmitter.prototype.connect = function ( context, methods ) {
		for ( const event in methods ) {
			let method = methods[ event ];
			let args;
			// Allow providing additional args
			if ( Array.isArray( method ) ) {
				args = method.slice( 1 );
				method = method[ 0 ];
			} else {
				args = [];
			}
			// Add binding
			this.on( event, method, args, context );
		}
		return this;
	};
 
	/**
	 * Disconnect event handlers from an object.
	 *
	 * @param {Object} context Object to disconnect methods from
	 * @param {Object.<string,string>|Object.<string,Function>|Object.<string,Array>} [methods]
	 *  List of event bindings keyed by event name. Values can be either method names, functions or
	 *  arrays containing a method name.
	 *  NOTE: To allow matching call sites with {@link OO.EventEmitter#connect|connect()}, array
	 *  values are allowed to contain the parameters as well, but only the method name is used to
	 *  find bindings. It is discouraged to have multiple bindings for the same event to the same
	 *  listener, but if used (and only the parameters vary), disconnecting one variation of
	 *  (event name, event listener, parameters) will disconnect other variations as well.
	 * @return {OO.EventEmitter}
	 */
	OO.EventEmitter.prototype.disconnect = function ( context, methods ) {
		let event;
		if ( methods ) {
			// Remove specific connections to the context
			for ( event in methods ) {
				let method = methods[ event ];
				if ( Array.isArray( method ) ) {
					method = method[ 0 ];
				}
				this.off( event, method, context );
			}
		} else {
			// Remove all connections to the context
			for ( event in this.bindings ) {
				const bindings = this.bindings[ event ];
				let i = bindings.length;
				while ( i-- ) {
					// bindings[i] may have been removed by the previous step's
					// this.off so check it still exists
					if ( bindings[ i ] && bindings[ i ].context === context ) {
						this.off( event, bindings[ i ].method, context );
					}
				}
			}
		}
 
		return this;
	};
 
}() );