@mezzy/signals
Version:
A luxurious user experience framework, developed by your friends at Mezzanine.
259 lines (194 loc) • 9.34 kB
text/typescript
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;