event-custom.js revision feff5ae3b2148d5fbd8677623d98ba844dea1943
d6fa26d0adaec6c910115be34fe7a5a5f402c14fMark Andrews
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User/**
f0aad5341752aefe5059832f6cf3abc3283c6e16Tinderbox User * Custom event engine, DOM event listener abstraction layer, synthetic DOM
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User * events.
5347c0fcb04eaea19d9f39795646239f487c6207Tinderbox User * @module event-custom
5347c0fcb04eaea19d9f39795646239f487c6207Tinderbox User * @submodule event-custom-base
5347c0fcb04eaea19d9f39795646239f487c6207Tinderbox User */
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User
d6fa26d0adaec6c910115be34fe7a5a5f402c14fMark Andrews/**
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User * Return value from all subscribe operations
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User * @class EventHandle
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User * @constructor
fd2597f75693a2279fdf588bd40dfe2407c42028Tinderbox User * @param evt {CustomEvent} the custom event
14a656f94b1fd0ababd84a772228dfa52276ba15Evan Hunt * @param sub {Subscriber} the subscriber
cd32f419a8a5432fbb139f56ee73cbf68b9350ccTinderbox User */
7e71f05d8643aca84914437c900cb716444507e4Tinderbox User
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User// var onsubscribeType = "_event:onsub",
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox Uservar AFTER = 'after',
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User CONFIGS = [
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User 'broadcast',
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User 'bubbles',
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User 'context',
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User 'contextFn',
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User 'currentTarget',
7e71f05d8643aca84914437c900cb716444507e4Tinderbox User 'defaultFn',
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User 'details',
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User 'emitFacade',
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User 'fireOnce',
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User 'host',
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User 'preventable',
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User 'preventedFn',
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User 'queuable',
fd2597f75693a2279fdf588bd40dfe2407c42028Tinderbox User 'silent',
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User 'stoppedFn',
fd2597f75693a2279fdf588bd40dfe2407c42028Tinderbox User 'target',
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User 'type'
dec590a3deb8e87380a8bd3a77d535dba3729bf6Tinderbox User ],
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User
fd2597f75693a2279fdf588bd40dfe2407c42028Tinderbox User
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User YUI3_SIGNATURE = 9,
fd2597f75693a2279fdf588bd40dfe2407c42028Tinderbox User YUI_LOG = 'yui:log';
fd2597f75693a2279fdf588bd40dfe2407c42028Tinderbox User
fd2597f75693a2279fdf588bd40dfe2407c42028Tinderbox UserY.EventHandle = function(evt, sub) {
fd2597f75693a2279fdf588bd40dfe2407c42028Tinderbox User
7e71f05d8643aca84914437c900cb716444507e4Tinderbox User /**
fd2597f75693a2279fdf588bd40dfe2407c42028Tinderbox User * The custom event
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User * @type CustomEvent
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User */
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User this.evt = evt;
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User
fd2597f75693a2279fdf588bd40dfe2407c42028Tinderbox User /**
fd2597f75693a2279fdf588bd40dfe2407c42028Tinderbox User * The subscriber object
7e71f05d8643aca84914437c900cb716444507e4Tinderbox User * @type Subscriber
fd2597f75693a2279fdf588bd40dfe2407c42028Tinderbox User */
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User this.sub = sub;
fd2597f75693a2279fdf588bd40dfe2407c42028Tinderbox User};
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox UserY.EventHandle.prototype = {
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User
fd2597f75693a2279fdf588bd40dfe2407c42028Tinderbox User /**
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User * Detaches this subscriber
fd2597f75693a2279fdf588bd40dfe2407c42028Tinderbox User * @method detach
9d557856c2a19ec95ee73245f60a92f8675cf5baTinderbox User */
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User detach: function() {
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User if (this.evt) {
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User // Y.log('EventHandle.detach: ' + this.sub, 'info', 'Event');
14a656f94b1fd0ababd84a772228dfa52276ba15Evan Hunt this.evt._delete(this.sub);
fd2597f75693a2279fdf588bd40dfe2407c42028Tinderbox User }
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User }
fd2597f75693a2279fdf588bd40dfe2407c42028Tinderbox User};
14a656f94b1fd0ababd84a772228dfa52276ba15Evan Hunt
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User/**
fd2597f75693a2279fdf588bd40dfe2407c42028Tinderbox User * The CustomEvent class lets you define events for your application
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User * that can be subscribed to by one or more independent component.
fd2597f75693a2279fdf588bd40dfe2407c42028Tinderbox User *
14a656f94b1fd0ababd84a772228dfa52276ba15Evan Hunt * @param {String} type The type of event, which is passed to the callback
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User * when the event fires
fd2597f75693a2279fdf588bd40dfe2407c42028Tinderbox User * @param o configuration object
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User * @class CustomEvent
fd2597f75693a2279fdf588bd40dfe2407c42028Tinderbox User * @constructor
fd2597f75693a2279fdf588bd40dfe2407c42028Tinderbox User */
7e71f05d8643aca84914437c900cb716444507e4Tinderbox UserY.CustomEvent = function(type, o) {
fd2597f75693a2279fdf588bd40dfe2407c42028Tinderbox User
fd2597f75693a2279fdf588bd40dfe2407c42028Tinderbox User // if (arguments.length > 2) {
fd2597f75693a2279fdf588bd40dfe2407c42028Tinderbox User// this.log('CustomEvent context and silent are now in the config', 'warn', 'Event');
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User // }
fd2597f75693a2279fdf588bd40dfe2407c42028Tinderbox User
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User o = o || {};
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User this.id = Y.stamp(this);
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User /**
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User * The type of event, returned to subscribers when the event fires
7e71f05d8643aca84914437c900cb716444507e4Tinderbox User * @property type
cd32f419a8a5432fbb139f56ee73cbf68b9350ccTinderbox User * @type string
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User */
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User this.type = type;
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User /**
7e71f05d8643aca84914437c900cb716444507e4Tinderbox User * The context the the event will fire from by default. Defaults to the YUI
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User * instance.
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User * @property context
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User * @type object
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User */
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User this.context = Y;
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User
1ca759b3f5c0672b2a66bc02288fe010cabbfe37Tinderbox User this.logSystem = (type == YUI_LOG);
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User
1ebb25608fa10737ea27abd4e0481707ccd45581Tinderbox User /**
* 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;
/**
* Specifies whether this event should be queued when the host is actively
* processing an event. This will effect exectution order of the callbacks
* for the various events.
* @property queuable
* @type boolean
* @default false
*/
// this.queuable = false;
/**
* The subscribers to this event
* @property subscribers
* @type Subscriber{}
*/
this.subscribers = {};
/**
* '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 EventTarget
*/
// 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;
// this.hasSubscribers = false;
// this.hasAfters = false;
/**
* 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);
};
Y.CustomEvent.prototype = {
/**
* 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);
}
},
_on: function(fn, context, args, when) {
if (!fn) {
this.log("Invalid callback for CE: " + this.type);
}
var s = new Y.Subscriber(fn, context, args, when);
if (this.fireOnce && this.fired) {
Y.later(0, this, this._notify, s);
}
if (when == AFTER) {
this.afters[s.id] = s;
this.hasAfters = true;
} else {
this.subscribers[s.id] = s;
this.hasSubscribers = true;
}
return new Y.EventHandle(this, s);
},
/**
* Listen for this event
* @method subscribe
* @param {Function} fn The function to execute
* @return {EventHandle|EventTarget} unsubscribe handle or a
* chainable event target depending on the 'chain' config.
* @deprecated use on
*/
subscribe: function(fn, context) {
var a = (arguments.length > 2) ? Y.Array(arguments, 2, true): null;
return this._on(fn, context, a, true);
},
/**
* Listen for this event
* @method on
* @param {Function} fn The function to execute
* @return {EventHandle|EventTarget} unsubscribe handle or a
* chainable event target depending on the 'chain' config.
*/
on: function(fn, context) {
var a = (arguments.length > 2) ? Y.Array(arguments, 2, true): null;
return this._on(fn, context, a, 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
* @return {EventHandle|EventTarget} unsubscribe handle or a
* chainable event target depending on the 'chain' config.
*/
after: function(fn, context) {
var a = (arguments.length > 2) ? Y.Array(arguments, 2, true): null;
return this._on(fn, context, a, 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|EventTarget} returns a chainable event target
* or a boolean for legacy detach support.
*/
detach: function(fn, context) {
// 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 && fn === this.fn) {
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|EventTarget} returns a chainable event target
* or a boolean for legacy detach support.
* @deprecated use detach
*/
unsubscribe: function() {
return this.detach.apply(this, arguments);
},
/**
* 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;
// emit an EventFacade if this is that sort of event
// 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);
// if (Y.Lang.isObject(args[0])) {
// args[0] = ef;
// } else {
// args.unshift(ef);
// }
// }
// }
ret = s.notify(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() {
if (this.fireOnce && this.fired) {
this.log('fireOnce event: ' + this.type + ' already fired');
return true;
} else {
this.fired = true;
var args = Y.Array(arguments, 0, true);
if (this.emitFacade) {
return this.fireComplex(args);
} else {
return this.fireSimple(args);
}
}
},
fireSimple: function(args) {
if (this.hasSubscribers || this.hasAfters) {
this._procSubs(Y.merge(this.subscribers, this.afters), args);
}
this._broadcast(args);
return this.stopped ? false : true;
},
fireComplex: function(args) {
return this.fireSimple(args);
},
_procSubs: function(subs, args, ef) {
var s, i;
for (i in subs) {
if (subs.hasOwnProperty(i)) {
s = subs[i];
if (s && s.fn) {
if (false === this._notify(s, args, ef)) {
this.stopped = 2;
}
if (this.stopped == 2) {
return false;
}
}
}
}
return true;
},
_broadcast: function(args) {
if (!this.stopped && this.broadcast) {
if (this.host !== Y) {
Y.fire.apply(Y, args);
}
if (this.broadcast == 2) {
Y.Global.fire.apply(Y.Global, args);
}
}
},
/**
* Removes all listeners
* @method unsubscribeAll
* @return {int} The number of listeners unsubscribed
* @deprecated use detachAll
*/
unsubscribeAll: function() {
return this.detachAll.apply(this, arguments);
},
/**
* Removes all listeners
* @method detachAll
* @return {int} The number of listeners unsubscribed
*/
detachAll: 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];
}
}
};
/////////////////////////////////////////////////////////////////////
/**
* 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);
/**
* Additional arguments to propagate to the subscriber
* @property args
* @type Array
*/
this.args = args;
/**
* Custom events for a given fire transaction.
* @property events
* @type {EventTarget}
*/
this.events = null;
};
Y.Subscriber.prototype = {
_notify: function(c, args, ce) {
var a = this.args, ret;
switch (ce.signature) {
case 0:
ret = this.fn.call(c, ce.type, args, c);
break;
case 1:
ret = this.fn.call(c, args[0] || null, c);
break;
default:
if (a || args) {
args = args || [];
a = (a) ? args.concat(a) : args;
ret = this.fn.apply(c, a);
} else {
ret = this.fn.call(c);
}
}
return ret;
},
/**
* Executes the subscriber.
* @method notify
* @param args {Array} Arguments array for the subscriber
* @param ce {CustomEvent} The custom event that sent the notification
*/
notify: function(args, ce) {
var c = this.context,
ret = true;
if (!c) {
c = (ce.contextFn) ? ce.contextFn() : ce.context;
}
// only catch errors if we will not re-throw them.
if (Y.config.throwFail) {
ret = this._notify(c, args, ce);
} else {
try {
ret = this._notify(c, args, ce);
} 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);
}
}
};