event-custom.js revision 80d2034f65b9348e5fd36291f03b0819181efb89
8648721e29bb657dd5c5ff20f03e86fe50628ce6Tripp
8648721e29bb657dd5c5ff20f03e86fe50628ce6Tripp/**
8648721e29bb657dd5c5ff20f03e86fe50628ce6Tripp * Custom event engine, DOM event listener abstraction layer, synthetic DOM
8648721e29bb657dd5c5ff20f03e86fe50628ce6Tripp * events.
8648721e29bb657dd5c5ff20f03e86fe50628ce6Tripp * @module event
8648721e29bb657dd5c5ff20f03e86fe50628ce6Tripp */
8648721e29bb657dd5c5ff20f03e86fe50628ce6Tripp
8648721e29bb657dd5c5ff20f03e86fe50628ce6Tripp/**
8648721e29bb657dd5c5ff20f03e86fe50628ce6Tripp * Return value from all subscribe operations
e393eced613f9b4a5fb6bdd461d0e0bf5064d5ecTripp * @class EventHandle
e393eced613f9b4a5fb6bdd461d0e0bf5064d5ecTripp * @constructor
e393eced613f9b4a5fb6bdd461d0e0bf5064d5ecTripp * @param evt {Event.Custom} the custom event
e393eced613f9b4a5fb6bdd461d0e0bf5064d5ecTripp * @param sub {Subscriber} the subscriber
e393eced613f9b4a5fb6bdd461d0e0bf5064d5ecTripp */
e393eced613f9b4a5fb6bdd461d0e0bf5064d5ecTripp
e393eced613f9b4a5fb6bdd461d0e0bf5064d5ecTrippvar onsubscribeType = "_event:onsub",
e393eced613f9b4a5fb6bdd461d0e0bf5064d5ecTripp AFTER = 'after',
8648721e29bb657dd5c5ff20f03e86fe50628ce6Tripp CONFIGS = [
e393eced613f9b4a5fb6bdd461d0e0bf5064d5ecTripp 'broadcast',
8648721e29bb657dd5c5ff20f03e86fe50628ce6Tripp 'bubbles',
8648721e29bb657dd5c5ff20f03e86fe50628ce6Tripp 'context',
8648721e29bb657dd5c5ff20f03e86fe50628ce6Tripp 'configured',
8648721e29bb657dd5c5ff20f03e86fe50628ce6Tripp 'currentTarget',
8648721e29bb657dd5c5ff20f03e86fe50628ce6Tripp 'defaultFn',
e393eced613f9b4a5fb6bdd461d0e0bf5064d5ecTripp 'details',
e393eced613f9b4a5fb6bdd461d0e0bf5064d5ecTripp 'emitFacade',
e393eced613f9b4a5fb6bdd461d0e0bf5064d5ecTripp 'fireOnce',
e393eced613f9b4a5fb6bdd461d0e0bf5064d5ecTripp 'host',
e393eced613f9b4a5fb6bdd461d0e0bf5064d5ecTripp 'preventable',
8648721e29bb657dd5c5ff20f03e86fe50628ce6Tripp 'preventedFn',
e393eced613f9b4a5fb6bdd461d0e0bf5064d5ecTripp 'queuable',
8648721e29bb657dd5c5ff20f03e86fe50628ce6Tripp 'silent',
8648721e29bb657dd5c5ff20f03e86fe50628ce6Tripp 'stoppedFn',
8648721e29bb657dd5c5ff20f03e86fe50628ce6Tripp 'target',
8648721e29bb657dd5c5ff20f03e86fe50628ce6Tripp 'type'
8648721e29bb657dd5c5ff20f03e86fe50628ce6Tripp ],
8648721e29bb657dd5c5ff20f03e86fe50628ce6Tripp
8648721e29bb657dd5c5ff20f03e86fe50628ce6Tripp YUI3_SIGNATURE = 9;
e393eced613f9b4a5fb6bdd461d0e0bf5064d5ecTripp
e393eced613f9b4a5fb6bdd461d0e0bf5064d5ecTrippY.EventHandle = function(evt, sub) {
e393eced613f9b4a5fb6bdd461d0e0bf5064d5ecTripp
e393eced613f9b4a5fb6bdd461d0e0bf5064d5ecTripp /**
e393eced613f9b4a5fb6bdd461d0e0bf5064d5ecTripp * The custom event
e393eced613f9b4a5fb6bdd461d0e0bf5064d5ecTripp * @type Event.Custom
e393eced613f9b4a5fb6bdd461d0e0bf5064d5ecTripp */
e393eced613f9b4a5fb6bdd461d0e0bf5064d5ecTripp this.evt = evt;
e393eced613f9b4a5fb6bdd461d0e0bf5064d5ecTripp
e393eced613f9b4a5fb6bdd461d0e0bf5064d5ecTripp /**
8648721e29bb657dd5c5ff20f03e86fe50628ce6Tripp * The subscriber object
e393eced613f9b4a5fb6bdd461d0e0bf5064d5ecTripp * @type Subscriber
8648721e29bb657dd5c5ff20f03e86fe50628ce6Tripp */
8648721e29bb657dd5c5ff20f03e86fe50628ce6Tripp this.sub = sub;
8648721e29bb657dd5c5ff20f03e86fe50628ce6Tripp};
8648721e29bb657dd5c5ff20f03e86fe50628ce6Tripp
8648721e29bb657dd5c5ff20f03e86fe50628ce6TrippY.EventHandle.prototype = {
8648721e29bb657dd5c5ff20f03e86fe50628ce6Tripp /**
8648721e29bb657dd5c5ff20f03e86fe50628ce6Tripp * Detaches this subscriber
e393eced613f9b4a5fb6bdd461d0e0bf5064d5ecTripp * @method detach
e393eced613f9b4a5fb6bdd461d0e0bf5064d5ecTripp */
e393eced613f9b4a5fb6bdd461d0e0bf5064d5ecTripp detach: function() {
e393eced613f9b4a5fb6bdd461d0e0bf5064d5ecTripp
e393eced613f9b4a5fb6bdd461d0e0bf5064d5ecTripp if (this.evt) {
e393eced613f9b4a5fb6bdd461d0e0bf5064d5ecTripp // Y.log('EventHandle.detach: ' + this.sub, 'info', 'Event');
e393eced613f9b4a5fb6bdd461d0e0bf5064d5ecTripp this.evt._delete(this.sub);
e393eced613f9b4a5fb6bdd461d0e0bf5064d5ecTripp }
e393eced613f9b4a5fb6bdd461d0e0bf5064d5ecTripp }
8648721e29bb657dd5c5ff20f03e86fe50628ce6Tripp};
8648721e29bb657dd5c5ff20f03e86fe50628ce6Tripp
8648721e29bb657dd5c5ff20f03e86fe50628ce6Tripp/**
8648721e29bb657dd5c5ff20f03e86fe50628ce6Tripp * The Event.Custom class lets you define events for your application
8648721e29bb657dd5c5ff20f03e86fe50628ce6Tripp * that can be subscribed to by one or more independent component.
e393eced613f9b4a5fb6bdd461d0e0bf5064d5ecTripp *
e393eced613f9b4a5fb6bdd461d0e0bf5064d5ecTripp * @param {String} type The type of event, which is passed to the callback
e393eced613f9b4a5fb6bdd461d0e0bf5064d5ecTripp * when the event fires
e393eced613f9b4a5fb6bdd461d0e0bf5064d5ecTripp * @param o configuration object
e393eced613f9b4a5fb6bdd461d0e0bf5064d5ecTripp * @class Event.Custom
e393eced613f9b4a5fb6bdd461d0e0bf5064d5ecTripp * @constructor
e393eced613f9b4a5fb6bdd461d0e0bf5064d5ecTripp */
Y.CustomEvent = function(type, o) {
// if (arguments.length > 2) {
// this.log('CustomEvent context and silent are now in the config', 'warn', 'Event');
// }
o = o || {};
this.id = Y.stamp(this);
/**
* The type of event, returned to subscribers when the event fires
* @property type
* @type string
*/
this.type = type;
/**
* The context the the event will fire from by default. Defaults to the YUI
* instance.
* @property context
* @type object
*/
this.context = Y;
this.logSystem = (type == "yui:log");
/**
* If 0, this event does not broadcast. If 1, the YUI instance is notified
* every time this event fires. If 2, the YUI instance and the YUI global
* (if event is enabled on the global) are notified every time this event
* fires.
* @property broadcast
* @type int
*/
this.broadcast = 0;
/**
* By default all custom events are logged in the debug build, set silent
* to true to disable debug outpu for this event.
* @property silent
* @type boolean
*/
this.silent = this.logSystem;
// this.queuable = !(this.logSystem);
this.queuable = false;
/**
* The subscribers to this event
* @property subscribers
* @type Subscriber{}
*/
this.subscribers = {};
/*
* The publisher has configured this event
* @property configured
* @type boolean
* @default true
*/
// this.configured = true;
/**
* 'After' subscribers
* @property afters
* @type Subscriber{}
*/
this.afters = {};
/**
* This event has fired if true
*
* @property fired
* @type boolean
* @default false;
*/
this.fired = false;
/**
* This event should only fire one time if true, and if
* it has fired, any new subscribers should be notified
* immediately.
*
* @property fireOnce
* @type boolean
* @default false;
*/
this.fireOnce = false;
/**
* Flag for stopPropagation that is modified during fire()
* 1 means to stop propagation to bubble targets. 2 means
* to also stop additional subscribers on this target.
* @property stopped
* @type int
*/
this.stopped = 0;
/**
* Flag for preventDefault that is modified during fire().
* if it is not 0, the default behavior for this event
* @property prevented
* @type int
*/
this.prevented = 0;
/**
* Specifies the host for this custom event. This is used
* to enable event bubbling
* @property host
* @type Event.Target
*/
this.host = null;
/**
* The default function to execute after event listeners
* have fire, but only if the default action was not
* prevented.
* @property defaultFn
* @type Function
*/
this.defaultFn = null;
/**
* The function to execute if a subscriber calls
* stopPropagation or stopImmediatePropagation
* @property stoppedFn
* @type Function
*/
this.stoppedFn = null;
/**
* The function to execute if a subscriber calls
* preventDefault
* @property preventedFn
* @type Function
*/
this.preventedFn = null;
/**
* Specifies whether or not this event's default function
* can be cancelled by a subscriber by executing preventDefault()
* on the event facade
* @property preventable
* @type boolean
* @default true
*/
this.preventable = true;
/**
* Specifies whether or not a subscriber can stop the event propagation
* via stopPropagation(), stopImmediatePropagation(), or halt()
* @property bubbles
* @type boolean
* @default true
*/
this.bubbles = true;
/**
* Supports multiple options for listener signatures in order to
* port YUI 2 apps.
* @property signature
* @type int
* @default 9
*/
this.signature = YUI3_SIGNATURE;
/**
* If set to true, the custom event will deliver an EventFacade object
* that is similar to a DOM event object.
* @property emitFacade
* @type boolean
* @default false
*/
this.emitFacade = false;
this.applyConfig(o, true);
this.log("Creating " + this.type);
// Only add subscribe events for events that are not generated by
// Event.Custom
if (type !== onsubscribeType) {
/**
* Custom events provide a custom event that fires whenever there is
* a new subscriber to the event. This provides an opportunity to
* handle the case where there is a non-repeating event that has
* already fired has a new subscriber.
*
* @event subscribeEvent
* @type Event.Custom
* @param {Function} fn The function to execute
* @param {Object} obj An object to be passed along when the event
* fires
* @param {boolean|Object} override If true, the obj passed in becomes
* the execution context of the listener.
* if an object, that object becomes the
* the execution context.
*/
this.subscribeEvent = new Y.CustomEvent(onsubscribeType, {
context: this,
silent: true
});
}
};
Y.CustomEvent.prototype = {
_YUI_EVENT: true,
/**
* Apply configuration properties. Only applies the CONFIG whitelist
* @method applyConfig
* @param o hash of properties to apply
* @param force {boolean} if true, properties that exist on the event
* will be overwritten.
*/
applyConfig: function(o, force) {
if (o) {
Y.mix(this, o, force, CONFIGS);
}
},
_subscribe: function(fn, context, args, when) {
if (!fn) {
Y.error("Invalid callback for CE: " + this.type);
}
var se = this.subscribeEvent, s;
if (se) {
se.fire.apply(se, args);
}
s = new Y.Subscriber(fn, context, args, when);
if (this.fireOnce && this.fired) {
// this._notify(s);
// setTimeout(Y.rbind(this._notify, this, s), 0);
Y.later(0, this, this._notify, s);
}
if (when == AFTER) {
this.afters[s.id] = s;
} else {
this.subscribers[s.id] = s;
}
return new Y.EventHandle(this, s);
},
/**
* Listen for this event
* @method subscribe
* @param {Function} fn The function to execute
* @param {Object} context Specifies the value of the
* 'this' keyword in the listener.
* @param args* 0..n params to provide to the listener
* @return {EventHandle} unsubscribe handle
* @deprecated use on
*/
subscribe: function(fn, context) {
return this._subscribe(fn, context, arguments, true);
},
/**
* Listen for this event
* @method on
* @param {Function} fn The function to execute
* @param {Object} context Specifies the value of the
* 'this' keyword in the listener.
* @param args* 0..n params to provide to the listener
* @return {EventHandle} unsubscribe handle
*/
on: function(fn, context) {
return this._subscribe(fn, context, arguments, true);
},
/**
* Listen for this event after the normal subscribers have been notified and
* the default behavior has been applied. If a normal subscriber prevents the
* default behavior, it also prevents after listeners from firing.
* @method after
* @param {Function} fn The function to execute
* @param {Object} context Specifies the value of the
* 'this' keyword in the listener.
* @param args* 0..n params to provide to the listener
* @return {EventHandle} unsubscribe handle
*/
after: function(fn, context) {
return this._subscribe(fn, context, arguments, AFTER);
},
/**
* Detach listeners.
* @method detach
* @param {Function} fn The subscribed function to remove, if not supplied
* all will be removed
* @param {Object} context The context object passed to subscribe.
* @return {boolean} True if the subscriber was found and detached.
*/
detach: function(fn, context) {
// if arg[0] typeof unsubscribe handle
if (fn && fn.detach) {
return fn.detach();
}
if (!fn) {
return this.unsubscribeAll();
}
var found = false, subs = this.subscribers, i, s;
for (i in subs) {
if (subs.hasOwnProperty(i)) {
s = subs[i];
if (s && s.contains(fn, context)) {
this._delete(s);
found = true;
}
}
}
return found;
},
/**
* Detach listeners.
* @method unsubscribe
* @param {Function} fn The subscribed function to remove, if not supplied
* all will be removed
* @param {Object} context The context object passed to subscribe.
* @return {boolean} True if the subscriber was found and detached.
* @deprecated use detach
*/
unsubscribe: function() {
return this.detach.apply(this, arguments);
},
_getFacade: function(args) {
var ef = this._facade, o;
if (!ef) {
ef = new Y.EventFacade(this, this.currentTarget);
}
// if the first argument is an object literal, apply the
// properties to the event facade
o = args && args[0];
if (Y.Lang.isObject(o, true) && !o._yuifacade) {
Y.mix(ef, o, true);
}
// update the details field with the arguments
ef.details = this.details;
ef.target = this.target;
ef.currentTarget = this.currentTarget;
ef.stopped = 0;
ef.prevented = 0;
this._facade = ef;
return this._facade;
},
/**
* Notify a single subscriber
* @method _notify
* @param s {Subscriber} the subscriber
* @param args {Array} the arguments array to apply to the listener
* @private
*/
_notify: function(s, args, ef) {
this.log(this.type + "->" + ": " + s);
var ret, ct;
// emit an EventFacade if this is that sort of event
// if (this.emitFacade && (!args[0] || !args[0]._yuifacade)) {
if (this.emitFacade) {
// @TODO object literal support to fire makes it possible for
// config info to be passed if we wish.
if (!ef) {
ef = this._getFacade(args);
args[0] = ef;
}
}
// The default context should be the object/element that
// the listener was bound to.
ct = (args && Y.Lang.isObject(args[0]) && args[0].currentTarget);
ret = s.notify(ct || this.context, args, this);
if (false === ret || this.stopped > 1) {
this.log(this.type + " cancelled by subscriber");
return false;
}
return true;
},
/**
* Logger abstraction to centralize the application of the silent flag
* @method log
* @param msg {string} message to log
* @param cat {string} log category
*/
log: function(msg, cat) {
if (!this.silent) {
Y.log(this.id + ': ' + msg, cat || "info", "event");
}
},
/**
* Notifies the subscribers. The callback functions will be executed
* from the context specified when the event was created, and with the
* following parameters:
* <ul>
* <li>The type of event</li>
* <li>All of the arguments fire() was executed with as an array</li>
* <li>The custom object (if any) that was passed into the subscribe()
* method</li>
* </ul>
* @method fire
* @param {Object*} arguments an arbitrary set of parameters to pass to
* the handler.
* @return {boolean} false if one of the subscribers returned false,
* true otherwise
*/
fire: function() {
var es = Y.Env._eventstack,
subs, s, args, i, ef, q, queue, ce, hasSub,
ret = true;
if (es) {
// var b = this.bubbles, h = this.host;
// if (b && h) {
// b = (h._yuievt.targets.length);
// }
// es.silent = (es.silent || this.silent);
// queue this event if the current item in the queue bubbles
// if (b && this.queuable && this.type != es.next.type) {
if (this.queuable && this.type != es.next.type) {
this.log('queue ' + this.type);
es.queue.push([this, arguments]);
return true;
}
} else {
Y.Env._eventstack = {
// id of the first event in the stack
id: this.id,
next: this,
silent: this.silent,
logging: (this.type === 'yui:log'),
stopped: 0,
prevented: 0,
queue: []
};
es = Y.Env._eventstack;
}
if (this.fireOnce && this.fired) {
this.log('fireOnce event: ' + this.type + ' already fired');
} else {
// var subs = this.subscribers.slice(), len=subs.length,
subs = Y.merge(this.subscribers);
args = Y.Array(arguments, 0, true);
this.stopped = 0;
this.prevented = 0;
this.target = this.target || this.host;
this.currentTarget = this.host || this.currentTarget;
this.fired = true;
this.details = args.slice(); // original arguments in the details
// this.log("Firing " + this + ", " + "args: " + args);
this.log("Firing " + this.type);
hasSub = false;
es.lastLogState = es.logging;
ef = null;
if (this.emitFacade) {
// this.fire({
// foo: 1
// bar: 2
// }
// this.fire({
// bar: 2
// } // foo is still 1 unless we create a new facade
this._facade = null;
ef = this._getFacade(args);
args[0] = ef;
}
for (i in subs) {
if (subs.hasOwnProperty(i)) {
if (!hasSub) {
es.logging = (es.logging || (this.type === 'yui:log'));
hasSub = true;
}
// stopImmediatePropagation
if (this.stopped == 2) {
break;
}
s = subs[i];
if (s && s.fn) {
ret = this._notify(s, args, ef);
if (false === ret) {
this.stopped = 2;
}
}
}
}
es.logging = (es.lastLogState);
// bubble if this is hosted in an event target and propagation has not been stopped
// @TODO check if we need to worry about defaultFn order
if (this.bubbles && this.host && !this.stopped) {
es.stopped = 0;
es.prevented = 0;
ret = this.host.bubble(this);
this.stopped = Math.max(this.stopped, es.stopped);
this.prevented = Math.max(this.prevented, es.prevented);
}
// execute the default behavior if not prevented
// @TODO need context
if (this.defaultFn && !this.prevented) {
this.defaultFn.apply(this.host || this, args);
}
// process after listeners. If the default behavior was
// prevented, the after events don't fire.
if (!this.prevented && this.stopped < 2) {
subs = Y.merge(this.afters);
for (i in subs) {
if (subs.hasOwnProperty(i)) {
if (!hasSub) {
es.logging = (es.logging || (this.type === 'yui:log'));
hasSub = true;
}
// stopImmediatePropagation
if (this.stopped == 2) {
break;
}
s = subs[i];
if (s && s.fn) {
ret = this._notify(s, args, ef);
if (false === ret) {
this.stopped = 2;
}
}
}
}
}
}
if (es.id === this.id) {
// console.log('clearing stack: ' + es.id + ', ' + this);
// reset propragation properties while processing the rest of the queue
// process queued events
queue = es.queue;
while (queue.length) {
// q[0] = the event, q[1] = arguments to fire
q = queue.pop();
ce = q[0];
// Y.log('firing queued event ' + ce.type + ', from ' + this);
es.stopped = 0;
es.prevented = 0;
// set up stack to allow the next item to be processed
es.next = ce;
ret = ce.fire.apply(ce, q[1]);
}
Y.Env._eventstack = null;
}
return (ret !== false);
},
/**
* Removes all listeners
* @method unsubscribeAll
* @return {int} The number of listeners unsubscribed
*/
unsubscribeAll: function() {
var subs = this.subscribers, i, l=0;
for (i in subs) {
if (subs.hasOwnProperty(i)) {
this._delete(subs[i]);
l++;
}
}
this.subscribers={};
return l;
},
/**
* @method _delete
* @param subscriber object
* @private
*/
_delete: function(s) {
if (s) {
delete s.fn;
delete s.context;
delete this.subscribers[s.id];
delete this.afters[s.id];
}
},
/**
* @method toString
*/
toString: function() {
// return "{ CE '" + this.type + "' " + "id: " + this.id +
// ", host: " + (this.host && Y.stamp(this.host) + " }");
return this.type;
},
/**
* Stop propagation to bubble targets
* @method stopPropagation
*/
stopPropagation: function() {
this.stopped = 1;
Y.Env._eventstack.stopped = 1;
if (this.stoppedFn) {
this.stoppedFn.call(this.host || this, this);
}
},
/**
* Stops propagation to bubble targets, and prevents any remaining
* subscribers on the current target from executing.
* @method stopImmediatePropagation
*/
stopImmediatePropagation: function() {
this.stopped = 2;
Y.Env._eventstack.stopped = 2;
if (this.stoppedFn) {
this.stoppedFn.call(this.host || this, this);
}
},
/**
* Prevents the execution of this event's defaultFn
* @method preventDefault
*/
preventDefault: function() {
if (this.preventable) {
this.prevented = 1;
Y.Env._eventstack.prevented = 1;
}
if (this.preventedFn) {
this.preventedFn.call(this.host || this, this);
}
}
};
/////////////////////////////////////////////////////////////////////
/**
* Stores the subscriber information to be used when the event fires.
* @param {Function} fn The wrapped function to execute
* @param {Object} context The value of the keyword 'this' in the listener
* @param {Array} args* 0..n additional arguments to supply the listener
*
* @class Subscriber
* @constructor
*/
Y.Subscriber = function(fn, context, args) {
/**
* The callback that will be execute when the event fires
* This is wrapped by Y.rbind if obj was supplied.
* @property fn
* @type Function
*/
this.fn = fn;
/**
* Optional 'this' keyword for the listener
* @property context
* @type Object
*/
this.context = context;
/**
* Unique subscriber id
* @property id
* @type String
*/
this.id = Y.stamp(this);
/**
* Optional additional arguments supplied to subscribe(). If present,
* these will be appended to the arguments supplied to fire()
* @property args
* @type Array
*/
// this.args = args;
/**
* }
* fn bound to obj with additional arguments applied via Y.rbind
* @property wrappedFn
* @type Function
*/
this.wrappedFn = fn;
if (context) {
/*
var a = (args) ? Y.Array(args) : [];
a.unshift(fn, context);
// a.unshift(fn);
m = Y.rbind.apply(Y, a);
*/
this.wrappedFn = Y.rbind.apply(Y, args);
}
};
Y.Subscriber.prototype = {
/**
* Executes the subscriber.
* @method notify
* @param defaultContext The execution context if not overridden
* by the subscriber
* @param args {Array} Arguments array for the subscriber
* @param ce {Event.Custom} The custom event that sent the notification
*/
notify: function(defaultContext, args, ce) {
var c = this.context || defaultContext, ret = true,
f = function() {
switch (ce.signature) {
case 0:
ret = this.fn.call(c, ce.type, args, this.context);
break;
case 1:
ret = this.fn.call(c, args[0] || null, this.context);
break;
default:
ret = this.wrappedFn.apply(c, args || []);
}
};
// Ease debugging by only catching errors if we will not re-throw
// them.
if (Y.config.throwFail) {
f.call(this);
} else {
try {
f.call(this);
} catch(e) {
Y.error(this + ' failed: ' + e.message, e);
}
}
return ret;
},
/**
* Returns true if the fn and obj match this objects properties.
* Used by the unsubscribe method to match the right subscriber.
*
* @method contains
* @param {Function} fn the function to execute
* @param {Object} context optional 'this' keyword for the listener
* @return {boolean} true if the supplied arguments match this
* subscriber's signature.
*/
contains: function(fn, context) {
if (context) {
return ((this.fn == fn) && this.context == context);
} else {
return (this.fn == fn);
}
},
/**
* @method toString
*/
toString: function() {
return "Subscriber " + this.id;
}
};