UNPKG

@mezzy/signals

Version:

A luxurious user experience framework, developed by your friends at Mezzanine.

259 lines (194 loc) 9.34 kB
import Identifier from '@mezzy/ids'; import ISignal from './iSignal'; import ISignalBinding from './iSignalBinding'; import SignalBinding from './signalBinding'; /** * @desc A TypeScript conversion of JS Signals by Miller Medeiros * Released under the MIT license * http://millermedeiros.github.com/js-signals/ * * @version 1.0 - 7th March 2013 * * @author Richard Davey, TypeScript conversion * @author Miller Medeiros, JS Signals * @author Robert Penner, AS Signals * * @url http://www.photonstorm.com */ export class Signal<T> implements ISignal<T> { constructor(isMemorize:boolean = false) { this._key = `signal_${Identifier.getSessionUniqueInteger()}`; this._bindings = []; this._prevParam = null; this._isMemorize = isMemorize; this._isDispatched = false; this._isShouldPropagate = true; this.isActive = true; } get key():string { return this._key; } private readonly _key:string; private readonly _bindings:Array<ISignalBinding<T>>; private _prevParam:T; /** * @return {number} Number of listeners attached to the Signal. */ get listenerCount():number { return !!this._bindings ? this._bindings.length : 0; } /** * If Signal should keep record of previously dispatched parameters and * automatically execute listener during `listen()`/`listenOnce()` if Signal was * already dispatched before. */ get isMemorize():boolean { return this._isMemorize; }; private readonly _isMemorize:boolean; /** * This will be set to true after the Signal has been dispatched at least once, * unless forget() has been called on the Signal. */ private _isDispatched:boolean; private _isShouldPropagate:boolean; /** * If Signal is active and should broadcast events. * <p><strong>IMPORTANT:</strong> Setting this property during a dispatch will only affect the next dispatch, * if you want to stop the propagation of a signal use `halt()` instead.</p> */ isActive:boolean; validateListener(listener:any, fnName:any):void { if (typeof listener !== 'function') { throw new Error('listener is a required param of {fn}() and should be a Function.'.replace('{fn}', fnName)); } } private _registerListener(listener:(value:T) => void, isOnce:boolean, listenerContext:any, priority:number):ISignalBinding<T> { let prevIndex:number = this._indexOfListener(listener, listenerContext); let binding:ISignalBinding<T>; if (prevIndex !== -1) { binding = this._bindings[prevIndex]; if (binding.isOnce() !== isOnce) { throw new Error('You cannot listen' + (isOnce ? '' : 'Once') + '() then add' + (!isOnce ? '' : 'Once') + '() the same listener without removing the relationship first.'); } } else { binding = new SignalBinding<T>(this, listener, isOnce, listenerContext, priority); this._addBinding(binding); } if (this.isMemorize && this._isDispatched) { binding.execute(this._prevParam); } return binding; } private _addBinding(binding:ISignalBinding<T>):void { /// Simplified insertion sort let n:number = this._bindings.length; do { --n; } while (this._bindings[n] && binding.priority <= this._bindings[n].priority); this._bindings.splice(n + 1, 0, binding); } private _indexOfListener(listener:(value:T) => void, context:any):number { let indexCurrent:number = this._bindings.length; let bindingCurrent:ISignalBinding<T>; while (indexCurrent--) { bindingCurrent = this._bindings[indexCurrent]; if (bindingCurrent.listener === listener && bindingCurrent.context === context) return indexCurrent; } return -1; } /** * Check if listener was attached to Signal. * @return {boolean} if Signal has the specified listener. */ has(listener:(value:T) => void, context:any = null):boolean { return this._indexOfListener(listener, context) !== -1; } /** * Add a listener to the signal. * @param {(value:T) => void} listener Signal handler function. * @param {Object} [listenerContext] Context on which listener will be executed * (object that should represent the `this` variable inside listener function). * @param {Number} [priority] The priority level of the event listener. * Listeners with higher priority will be executed before listeners with lower priority. * Listeners with same priority level will be executed at the same order as they were added. (default = 0) * @return {SignalBinding<T>} An Object representing the binding between the Signal and listener. */ listen(listener:(value:T) => void, listenerContext:any = null, priority:number = 0):ISignalBinding<T> { this.validateListener(listener, 'listen'); return this._registerListener(listener, false, listenerContext, priority); } /** * Add listener to the signal that should be deleted after first execution (will be executed only once). * @param {(value:T) => void} listener Signal handler function. * @param {Object} [listenerContext] Context on which listener will be executed * (object that should represent the `this` variable inside listener function). * @param {Number} [priority] The priority level of the event listener. * Listeners with higher priority will be executed before listeners with lower priority. * Listeners with same priority level will be executed at the same order as they were added. (default = 0) * @return {SignalBinding<T>} An Object representing the binding between the Signal and listener. */ listenOnce(listener:(value:T) => void, listenerContext:any = null, priority:number = 0):ISignalBinding<T> { this.validateListener(listener, 'listenOnce'); return this._registerListener(listener, true, listenerContext, priority); } /** * Remove a single listener from the dispatch queue. * @param {(value:T) => void} listener Handler function that should be deleted. * @param {Object} [context] Execution context (since you can add the same handler multiple times if executing in a different context). * @return {(value:T) => void} Listener handler function. */ delete(listener:(value:T) => void, context:any = null):(value:T) => void { this.validateListener(listener, 'remove'); let i:number = this._indexOfListener(listener, context); if (i !== -1) { this._bindings[i].destroy(); // No reason for a SignalBinding to exist if it isn't attached to a signal this._bindings.splice(i, 1); } return listener; } /** * Remove all listeners from the Signal. */ deleteAll():void { let n:number = this._bindings.length; while (n--) { this._bindings[n].destroy(); } this._bindings.length = 0; } /** * Stop propagation of the event, blocking the dispatch to next listeners on the queue. * <p><strong>IMPORTANT:</strong> should be called only during signal dispatch, calling it before/after dispatch won't affect signal broadcast.</p> * @see Signal.prototype.disable */ halt():void { this._isShouldPropagate = false; } /** * Dispatch/Broadcast Signal to all listeners added to the queue. * @param {...*} [params] Parameters that should be passed to each handler. */ dispatch(param:T):void { if (!this.isActive) return; let n:number = this._bindings.length; let bindings:ISignalBinding<T>[]; if (this.isMemorize) { this._prevParam = param; this._isDispatched = true; } if (!n) { return; } // Should come after isMemorize bindings = this._bindings.slice(0); // Clone array in case add/remove items during dispatch. this._isShouldPropagate = true; // In case `halt` was called before dispatch or during the previous dispatch. /// Execute all callbacks until end of the list or until a callback returns `false` or stops propagation. /// Reverse loop since listeners with higher priority will be added at the end of the list. do { n--; } while (bindings[n] && this._isShouldPropagate && bindings[n].execute(param) !== false); } /** * Forget memorized arguments. * @see Signal.isMemorize */ forget():void { this._prevParam = null; this._isDispatched = false; } /** * Remove all bindings from signal and destroy any reference to external objects (destroy Signal object). * <p><strong>IMPORTANT:</strong> calling any method on the signal instance after calling dispose will throw errors.</p> */ dispose():void { this.deleteAll(); this._bindings.splice(0); delete this._prevParam; } /** * @return {string} String representation of the object. */ toString():string { return '[Signal isActive:' + this.isActive + ' numListeners:' + this.listenerCount + ']'; } } // End class export default Signal;