/**
* Custom event engine, DOM event listener abstraction layer, synthetic DOM
* events.
* @module event-custom
* @submodule event-custom-base
*/
// var onsubscribeType = "_event:onsub",
var YArray = Y.Array,
AFTER = 'after',
CONFIGS = [
'broadcast',
'monitored',
'bubbles',
'context',
'contextFn',
'currentTarget',
'defaultFn',
'defaultTargetOnly',
'details',
'emitFacade',
'fireOnce',
'async',
'host',
'preventable',
'preventedFn',
'queuable',
'silent',
'stoppedFn',
'target',
'type'
],
CONFIGS_HASH = YArray.hash(CONFIGS),
nativeSlice = Array.prototype.slice,
YUI3_SIGNATURE = 9,
YUI_LOG = 'yui:log',
mixConfigs = function(r, s, ov) {
var p;
for (p in s) {
if (CONFIGS_HASH[p] && (ov || !(p in r))) {
r[p] = s[p];
}
}
return r;
};
/**
* The CustomEvent class lets you define events for your application
* that can be subscribed to by one or more independent component.
*
* @param {String} type The type of event, which is passed to the callback
* when the event fires.
* @param {object} defaults configuration object.
* @class CustomEvent
* @constructor
*/
/**
* The type of event, returned to subscribers when the event fires
* @property type
* @type string
*/
/**
* 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
*/
Y.CustomEvent = function(type, defaults) {
this._kds = Y.CustomEvent.keepDeprecatedSubs;
this.id = Y.guid();
this.type = type;
this.silent = this.logSystem = (type === YUI_LOG);
if (this._kds) {
/**
* The subscribers to this event
* @property subscribers
* @type Subscriber {}
* @deprecated
*/
/**
* 'After' subscribers
* @property afters
* @type Subscriber {}
* @deprecated
*/
this.subscribers = {};
this.afters = {};
}
if (defaults) {
mixConfigs(this, defaults, true);
}
};
/**
* Static flag to enable population of the <a href="#property_subscribers">`subscribers`</a>
* and <a href="#property_subscribers">`afters`</a> properties held on a `CustomEvent` instance.
*
* These properties were changed to private properties (`_subscribers` and `_afters`), and
* converted from objects to arrays for performance reasons.
*
* Setting this property to true will populate the deprecated `subscribers` and `afters`
* properties for people who may be using them (which is expected to be rare). There will
* be a performance hit, compared to the new array based implementation.
*
* If you are using these deprecated properties for a use case which the public API
* does not support, please file an enhancement request, and we can provide an alternate
* public implementation which doesn't have the performance cost required to maintiain the
* properties as objects.
*
* @property keepDeprecatedSubs
* @static
* @for CustomEvent
* @type boolean
* @default false
* @deprecated
*/
Y.CustomEvent.keepDeprecatedSubs = false;
Y.CustomEvent.mixConfigs = mixConfigs;
Y.CustomEvent.prototype = {
constructor: Y.CustomEvent,
/**
* Monitor when an event is attached or detached.
*
* @property monitored
* @type boolean
*/
/**
* 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
*/
/**
* 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 event has fired if true
*
* @property fired
* @type boolean
* @default false;
*/
/**
* An array containing the arguments the custom event
* was last fired with.
* @property firedWith
* @type Array
*/
/**
* 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;
*/
/**
* fireOnce listeners will fire syncronously unless async
* is set to true
* @property async
* @type boolean
* @default 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
*/
/**
* Flag for preventDefault that is modified during fire().
* if it is not 0, the default behavior for this event
* @property prevented
* @type int
*/
/**
* Specifies the host for this custom event. This is used
* to enable event bubbling
* @property host
* @type EventTarget
*/
/**
* The default function to execute after event listeners
* have fire, but only if the default action was not
* prevented.
* @property defaultFn
* @type Function
*/
/**
* Flag for the default function to execute only if the
* firing event is the current target. This happens only
* when using custom event delegation and setting the
* flag to `true` mimics the behavior of event delegation
* in the DOM.
*
* @property defaultTargetOnly
* @type Boolean
* @default false
*/
/**
* The function to execute if a subscriber calls
* stopPropagation or stopImmediatePropagation
* @property stoppedFn
* @type Function
*/
/**
* The function to execute if a subscriber calls
* preventDefault
* @property preventedFn
* @type Function
*/
/**
* The subscribers to this event
* @property _subscribers
* @type Subscriber []
* @private
*/
/**
* 'After' subscribers
* @property _afters
* @type Subscriber []
* @private
*/
/**
* 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
*/
/**
* Supports multiple options for listener signatures in order to
* port YUI 2 apps.
* @property signature
* @type int
* @default 9
*/
signature : YUI3_SIGNATURE,
/**
* The context the the event will fire from by default. Defaults to the YUI
* instance.
* @property context
* @type object
*/
context : Y,
/**
* 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
*/
preventable : true,
/**
* Specifies whether or not a subscriber can stop the event propagation
* via stopPropagation(), stopImmediatePropagation(), or halt()
*
* Events can only bubble if emitFacade is true.
*
* @property bubbles
* @type boolean
* @default true
*/
bubbles : true,
/**
* Returns the number of subscribers for this event as the sum of the on()
* subscribers and after() subscribers.
*
* @method hasSubs
* @return Number
*/
hasSubs: function(when) {
var s = 0,
a = 0,
subs = this._subscribers,
afters = this._afters,
sib = this.sibling;
if (subs) {
s = subs.length;
}
if (afters) {
a = afters.length;
}
if (sib) {
subs = sib._subscribers;
afters = sib._afters;
if (subs) {
s += subs.length;
}
if (afters) {
a += afters.length;
}
}
if (when) {
return (when === 'after') ? a : s;
}
return (s + a);
},
/**
* Monitor the event state for the subscribed event. The first parameter
* is what should be monitored, the rest are the normal parameters when
* subscribing to an event.
* @method monitor
* @param what {string} what to monitor ('detach', 'attach', 'publish').
* @return {EventHandle} return value from the monitor event subscription.
*/
monitor: function(what) {
this.monitored = true;
var type = this.id + '|' + this.type + '_' + what,
args = nativeSlice.call(arguments, 0);
args[0] = type;
return this.host.on.apply(this.host, args);
},
/**
* Get all of the subscribers to this event and any sibling event
* @method getSubs
* @return {Array} first item is the on subscribers, second the after.
*/
getSubs: function() {
var sibling = this.sibling,
subs = this._subscribers,
afters = this._afters,
siblingSubs,
siblingAfters;
if (sibling) {
siblingSubs = sibling._subscribers;
siblingAfters = sibling._afters;
}
if (siblingSubs) {
if (subs) {
subs = subs.concat(siblingSubs);
} else {
subs = siblingSubs.concat();
}
} else {
if (subs) {
subs = subs.concat();
} else {
subs = [];
}
}
if (siblingAfters) {
if (afters) {
afters = afters.concat(siblingAfters);
} else {
afters = siblingAfters.concat();
}
} else {
if (afters) {
afters = afters.concat();
} else {
afters = [];
}
}
return [subs, afters];
},
/**
* 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) {
mixConfigs(this, o, force);
},
/**
* Create the Subscription for subscribing function, context, and bound
* arguments. If this is a fireOnce event, the subscriber is immediately
* notified.
*
* @method _on
* @param fn {Function} Subscription callback
* @param [context] {Object} Override `this` in the callback
* @param [args] {Array} bound arguments that will be passed to the callback after the arguments generated by fire()
* @param [when] {String} "after" to slot into after subscribers
* @return {EventHandle}
* @protected
*/
_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),
firedWith;
if (this.fireOnce && this.fired) {
firedWith = this.firedWith;
// It's a little ugly for this to know about facades,
// but given the current breakup, not much choice without
// moving a whole lot of stuff around.
if (this.emitFacade && this._addFacadeToArgs) {
this._addFacadeToArgs(firedWith);
}
if (this.async) {
setTimeout(Y.bind(this._notify, this, s, firedWith), 0);
} else {
this._notify(s, firedWith);
}
}
if (when === AFTER) {
if (!this._afters) {
this._afters = [];
}
this._afters.push(s);
} else {
if (!this._subscribers) {
this._subscribers = [];
}
this._subscribers.push(s);
}
if (this._kds) {
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.
* @return {EventHandle} Unsubscribe handle.
* @deprecated use on.
*/
subscribe: function(fn, context) {
Y.log('ce.subscribe deprecated, use "on"', 'warn', 'deprecated');
var a = (arguments.length > 2) ? nativeSlice.call(arguments, 2) : null;
return this._on(fn, context, a, true);
},
/**
* Listen for this event
* @method on
* @param {Function} fn The function to execute.
* @param {object} context optional execution context.
* @param {mixed} arg* 0..n additional arguments to supply to the subscriber
* when the event fires.
* @return {EventHandle} An object with a detach method to detch the handler(s).
*/
on: function(fn, context) {
var a = (arguments.length > 2) ? nativeSlice.call(arguments, 2) : null;
if (this.monitored && this.host) {
this.host._monitor('attach', this, {
args: arguments
});
}
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.
* @param {object} context optional execution context.
* @param {mixed} arg* 0..n additional arguments to supply to the subscriber
* when the event fires.
* @return {EventHandle} handle Unsubscribe handle.
*/
after: function(fn, context) {
var a = (arguments.length > 2) ? nativeSlice.call(arguments, 2) : 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 {Number} returns the number of subscribers unsubscribed.
*/
detach: function(fn, context) {
// unsubscribe handle
if (fn && fn.detach) {
return fn.detach();
}
var i, s,
found = 0,
subs = this._subscribers,
afters = this._afters;
if (subs) {
for (i = subs.length; i >= 0; i--) {
s = subs[i];
if (s && (!fn || fn === s.fn)) {
this._delete(s, subs, i);
found++;
}
}
}
if (afters) {
for (i = afters.length; i >= 0; i--) {
s = afters[i];
if (s && (!fn || fn === s.fn)) {
this._delete(s, afters, i);
found++;
}
}
}
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 {int|undefined} returns the number of subscribers unsubscribed.
* @deprecated use detach.
*/
unsubscribe: function() {
return this.detach.apply(this, arguments);
},
/**
* Notify a single subscriber
* @method _notify
* @param {Subscriber} s the subscriber.
* @param {Array} args the arguments array to apply to the listener.
* @protected
*/
_notify: function(s, args, ef) {
this.log(this.type + '->' + 'sub: ' + s.id);
var ret;
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 {string} msg message to log.
* @param {string} cat 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() {
// push is the fastest way to go from arguments to arrays
// for most browsers currently
// http://jsperf.com/push-vs-concat-vs-slice/2
var args = [];
args.push.apply(args, arguments);
return this._fire(args);
},
/**
* Private internal implementation for `fire`, which is can be used directly by
* `EventTarget` and other event module classes which have already converted from
* an `arguments` list to an array, to avoid the repeated overhead.
*
* @method _fire
* @private
* @param {Array} args The array of arguments passed to be passed to handlers.
* @return {boolean} false if one of the subscribers returned false, true otherwise.
*/
_fire: function(args) {
if (this.fireOnce && this.fired) {
this.log('fireOnce event: ' + this.type + ' already fired');
return true;
} else {
// this doesn't happen if the event isn't published
// this.host._monitor('fire', this.type, args);
this.fired = true;
if (this.fireOnce) {
this.firedWith = args;
}
if (this.emitFacade) {
return this.fireComplex(args);
} else {
return this.fireSimple(args);
}
}
},
/**
* Set up for notifying subscribers of non-emitFacade events.
*
* @method fireSimple
* @param args {Array} Arguments passed to fire()
* @return Boolean false if a subscriber returned false
* @protected
*/
fireSimple: function(args) {
this.stopped = 0;
this.prevented = 0;
if (this.hasSubs()) {
var subs = this.getSubs();
this._procSubs(subs[0], args);
this._procSubs(subs[1], args);
}
if (this.broadcast) {
this._broadcast(args);
}
return this.stopped ? false : true;
},
// Requires the event-custom-complex module for full funcitonality.
fireComplex: function(args) {
this.log('Missing event-custom-complex needed to emit a facade for: ' + this.type);
args[0] = args[0] || {};
return this.fireSimple(args);
},
/**
* Notifies a list of subscribers.
*
* @method _procSubs
* @param subs {Array} List of subscribers
* @param args {Array} Arguments passed to fire()
* @param ef {}
* @return Boolean false if a subscriber returns false or stops the event
* propagation via e.stopPropagation(),
* e.stopImmediatePropagation(), or e.halt()
* @private
*/
_procSubs: function(subs, args, ef) {
var s, i, l;
for (i = 0, l = subs.length; i < l; 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;
},
/**
* Notifies the YUI instance if the event is configured with broadcast = 1,
* and both the YUI instance and Y.Global if configured with broadcast = 2.
*
* @method _broadcast
* @param args {Array} Arguments sent to fire()
* @private
*/
_broadcast: function(args) {
if (!this.stopped && this.broadcast) {
var a = args.concat();
a.unshift(this.type);
if (this.host !== Y) {
Y.fire.apply(Y, a);
}
if (this.broadcast === 2) {
Y.Global.fire.apply(Y.Global, a);
}
}
},
/**
* Removes all listeners
* @method unsubscribeAll
* @return {Number} The number of listeners unsubscribed.
* @deprecated use detachAll.
*/
unsubscribeAll: function() {
return this.detachAll.apply(this, arguments);
},
/**
* Removes all listeners
* @method detachAll
* @return {Number} The number of listeners unsubscribed.
*/
detachAll: function() {
return this.detach();
},
/**
* Deletes the subscriber from the internal store of on() and after()
* subscribers.
*
* @method _delete
* @param s subscriber object.
* @param subs (optional) on or after subscriber array
* @param index (optional) The index found.
* @private
*/
_delete: function(s, subs, i) {
var when = s._when;
if (!subs) {
subs = (when === AFTER) ? this._afters : this._subscribers;
}
if (subs) {
i = YArray.indexOf(subs, s, 0);
if (s && subs[i] === s) {
subs.splice(i, 1);
}
}
if (this._kds) {
if (when === AFTER) {
delete this.afters[s.id];
} else {
delete this.subscribers[s.id];
}
}
if (this.monitored && this.host) {
this.host._monitor('detach', this, {
ce: this,
sub: s
});
}
if (s) {
s.deleted = true;
}
}
};