/** * Define new DOM events that can be subscribed to from Nodes. * * @module event * @submodule event-synthetic */ var CustomEvent = Y.CustomEvent, DOMMap = Y.Env.evt.dom_map, toArray = Y.Array, YLang = Y.Lang, isObject = YLang.isObject, isString = YLang.isString, isArray = YLang.isArray, query = Y.Selector.query, noop = function () {}; /** * <p>The triggering mechanism used by SyntheticEvents.</p> * * <p>Implementers should not instantiate these directly. Use the Notifier * provided to the event's implemented <code>on(node, sub, notifier)</code> or * <code>delegate(node, sub, notifier, filter)</code> methods.</p> * * @class SyntheticEvent.Notifier * @constructor * @param handle {EventHandle} the detach handle for the subscription to an * internal custom event used to execute the callback passed to * on(..) or delegate(..) * @param emitFacade {Boolean} take steps to ensure the first arg received by * the subscription callback is an event facade * @private * @since 3.2.0 */ function Notifier(handle, emitFacade) { this.handle = handle; this.emitFacade = emitFacade; } /** * <p>Executes the subscription callback, passing the firing arguments as the * first parameters to that callback. For events that are configured with * emitFacade=true, it is common practice to pass the triggering DOMEventFacade * as the first parameter. Barring a proper DOMEventFacade or EventFacade * (from a CustomEvent), a new EventFacade will be generated. In that case, if * fire() is called with a simple object, it will be mixed into the facade. * Otherwise, the facade will be prepended to the callback parameters.</p> * * <p>For notifiers provided to delegate logic, the first argument should be an * object with a "currentTarget" property to identify what object to * default as 'this' in the callback. Typically this is gleaned from the * DOMEventFacade or EventFacade, but if configured with emitFacade=false, an * object must be provided. In that case, the object will be removed from the * callback parameters.</p> * * <p>Additional arguments passed during event subscription will be * automatically added after those passed to fire().</p> * * @method fire * @param {EventFacade|DOMEventFacade|any} e (see description) * @param {any[]} [arg*] additional arguments received by all subscriptions * @private */ Notifier.prototype.fire = function (e) { // first arg to delegate notifier should be an object with currentTarget var args = toArray(arguments, 0, true), handle = this.handle, ce = handle.evt, sub = handle.sub, thisObj = sub.context, delegate = sub.filter, event = e || {}, ret; if (this.emitFacade) { if (!e || !e.preventDefault) { event = ce._getFacade(); if (isObject(e) && !e.preventDefault) { Y.mix(event, e, true); args[0] = event; } else { args.unshift(event); } } event.type = ce.type; event.details = args.slice(); if (delegate) { event.container = ce.host; } } else if (delegate && isObject(e) && e.currentTarget) { args.shift(); } sub.context = thisObj || event.currentTarget || ce.host; ret = ce.fire.apply(ce, args); // have to handle preventedFn and stoppedFn manually because // Notifier CustomEvents are forced to emitFacade=false if (e.prevented && ce.preventedFn) { ce.preventedFn.apply(ce, args); } if (e.stopped && ce.stoppedFn) { ce.stoppedFn.apply(ce, args); } sub.context = thisObj; // reset for future firing // to capture callbacks that return false to stopPropagation. // Useful for delegate implementations return ret; }; /** * Manager object for synthetic event subscriptions to aggregate multiple synths on the * same node without colliding with actual DOM subscription entries in the global map of * DOM subscriptions. Also facilitates proper cleanup on page unload. * * @class SynthRegistry * @constructor * @param el {HTMLElement} the DOM element * @param yuid {String} the yuid stamp for the element * @param key {String} the generated id token used to identify an event type + * element in the global DOM subscription map. * @private */ function SynthRegistry(el, yuid, key) { this.handles = []; this.el = el; this.key = key; this.domkey = yuid; } SynthRegistry.prototype = { constructor: SynthRegistry, // A few object properties to fake the CustomEvent interface for page // unload cleanup. DON'T TOUCH! type : '_synth', fn : noop, capture : false, /** * Adds a subscription from the Notifier registry. * * @method register * @param handle {EventHandle} the subscription * @since 3.4.0 */ register: function (handle) { handle.evt.registry = this; this.handles.push(handle); }, /** * Removes the subscription from the Notifier registry. * * @method _unregisterSub * @param sub {Subscription} the subscription * @since 3.4.0 */ unregister: function (sub) { var handles = this.handles, events = DOMMap[this.domkey], i; for (i = handles.length - 1; i >= 0; --i) { if (handles[i].sub === sub) { handles.splice(i, 1); break; } } // Clean up left over objects when there are no more subscribers. if (!handles.length) { delete events[this.key]; if (!Y.Object.size(events)) { delete DOMMap[this.domkey]; } } }, /** * Used by the event system's unload cleanup process. When navigating * away from the page, the event system iterates the global map of element * subscriptions and detaches everything using detachAll(). Normally, * the map is populated with custom events, so this object needs to * at least support the detachAll method to duck type its way to * cleanliness. * * @method detachAll * @private * @since 3.4.0 */ detachAll : function () { var handles = this.handles, i = handles.length; while (--i >= 0) { handles[i].detach(); } } }; /** * <p>Wrapper class for the integration of new events into the YUI event * infrastructure. Don't instantiate this object directly, use * <code>Y.Event.define(type, config)</code>. See that method for details.</p> * * <p>Properties that MAY or SHOULD be specified in the configuration are noted * below and in the description of <code>Y.Event.define</code>.</p> * * @class SyntheticEvent * @constructor * @param cfg {Object} Implementation pieces and configuration * @since 3.1.0 * @in event-synthetic */ function SyntheticEvent() { this._init.apply(this, arguments); } Y.mix(SyntheticEvent, { Notifier: Notifier, SynthRegistry: SynthRegistry, /** * Returns the array of subscription handles for a node for the given event * type. Passing true as the third argument will create a registry entry * in the event system's DOM map to host the array if one doesn't yet exist. * * @method getRegistry * @param node {Node} the node * @param type {String} the event * @param create {Boolean} create a registration entry to host a new array * if one doesn't exist. * @return {Array} * @static * @protected * @since 3.2.0 */ getRegistry: function (node, type, create) { var el = node._node, yuid = Y.stamp(el), key = 'event:' + yuid + type + '_synth', events = DOMMap[yuid]; if (create) { if (!events) { events = DOMMap[yuid] = {}; } if (!events[key]) { events[key] = new SynthRegistry(el, yuid, key); } } return (events && events[key]) || null; }, /** * Alternate <code>_delete()</code> method for the CustomEvent object * created to manage SyntheticEvent subscriptions. * * @method _deleteSub * @param sub {Subscription} the subscription to clean up * @private * @since 3.2.0 */ _deleteSub: function (sub) { if (sub && sub.fn) { var synth = this.eventDef, method = (sub.filter) ? 'detachDelegate' : 'detach'; this._subscribers = []; if (CustomEvent.keepDeprecatedSubs) { this.subscribers = {}; } synth[method](sub.node, sub, this.notifier, sub.filter); this.registry.unregister(sub); delete sub.fn; delete sub.node; delete sub.context; } }, prototype: { constructor: SyntheticEvent, /** * Construction logic for the event. * * @method _init * @protected */ _init: function () { var config = this.publishConfig || (this.publishConfig = {}); // The notification mechanism handles facade creation this.emitFacade = ('emitFacade' in config) ? config.emitFacade : true; config.emitFacade = false; }, /** * <p>Implementers MAY provide this method definition.</p> * * <p>Implement this function if the event supports a different * subscription signature. This function is used by both * <code>on()</code> and <code>delegate()</code>. The second parameter * indicates that the event is being subscribed via * <code>delegate()</code>.</p> * * <p>Implementations must remove extra arguments from the args list * before returning. The required args for <code>on()</code> * subscriptions are</p> * <pre><code>[type, callback, target, context, argN...]</code></pre> * * <p>The required args for <code>delegate()</code> * subscriptions are</p> * * <pre><code>[type, callback, target, filter, context, argN...]</code></pre> * * <p>The return value from this function will be stored on the * subscription in the '_extra' property for reference elsewhere.</p> * * @method processArgs * @param args {Array} parmeters passed to Y.on(..) or Y.delegate(..) * @param delegate {Boolean} true if the subscription is from Y.delegate * @return {any} */ processArgs: noop, /** * <p>Implementers MAY override this property.</p> * * <p>Whether to prevent multiple subscriptions to this event that are * classified as being the same. By default, this means the subscribed * callback is the same function. See the <code>subMatch</code> * method. Setting this to true will impact performance for high volume * events.</p> * * @property preventDups * @type {Boolean} * @default false */ //preventDups : false, /** * <p>Implementers SHOULD provide this method definition.</p> * * Implementation logic for subscriptions done via <code>node.on(type, * fn)</code> or <code>Y.on(type, fn, target)</code>. This * function should set up the monitor(s) that will eventually fire the * event. Typically this involves subscribing to at least one DOM * event. It is recommended to store detach handles from any DOM * subscriptions to make for easy cleanup in the <code>detach</code> * method. Typically these handles are added to the <code>sub</code> * object. Also for SyntheticEvents that leverage a single DOM * subscription under the hood, it is recommended to pass the DOM event * object to <code>notifier.fire(e)</code>. (The event name on the * object will be updated). * * @method on * @param node {Node} the node the subscription is being applied to * @param sub {Subscription} the object to track this subscription * @param notifier {SyntheticEvent.Notifier} call notifier.fire(..) to * trigger the execution of the subscribers */ on: noop, /** * <p>Implementers SHOULD provide this method definition.</p> * * <p>Implementation logic for detaching subscriptions done via * <code>node.on(type, fn)</code>. This function should clean up any * subscriptions made in the <code>on()</code> phase.</p> * * @method detach * @param node {Node} the node the subscription was applied to * @param sub {Subscription} the object tracking this subscription * @param notifier {SyntheticEvent.Notifier} the Notifier used to * trigger the execution of the subscribers */ detach: noop, /** * <p>Implementers SHOULD provide this method definition.</p> * * <p>Implementation logic for subscriptions done via * <code>node.delegate(type, fn, filter)</code> or * <code>Y.delegate(type, fn, container, filter)</code>. Like with * <code>on()</code> above, this function should monitor the environment * for the event being fired, and trigger subscription execution by * calling <code>notifier.fire(e)</code>.</p> * * <p>This function receives a fourth argument, which is the filter * used to identify which Node's are of interest to the subscription. * The filter will be either a boolean function that accepts a target * Node for each hierarchy level as the event bubbles, or a selector * string. To translate selector strings into filter functions, use * <code>Y.delegate.compileFilter(filter)</code>.</p> * * @method delegate * @param node {Node} the node the subscription is being applied to * @param sub {Subscription} the object to track this subscription * @param notifier {SyntheticEvent.Notifier} call notifier.fire(..) to * trigger the execution of the subscribers * @param filter {String|Function} Selector string or function that * accepts an event object and returns null, a Node, or an * array of Nodes matching the criteria for processing. * @since 3.2.0 */ delegate : noop, /** * <p>Implementers SHOULD provide this method definition.</p> * * <p>Implementation logic for detaching subscriptions done via * <code>node.delegate(type, fn, filter)</code> or * <code>Y.delegate(type, fn, container, filter)</code>. This function * should clean up any subscriptions made in the * <code>delegate()</code> phase.</p> * * @method detachDelegate * @param node {Node} the node the subscription was applied to * @param sub {Subscription} the object tracking this subscription * @param notifier {SyntheticEvent.Notifier} the Notifier used to * trigger the execution of the subscribers * @param filter {String|Function} Selector string or function that * accepts an event object and returns null, a Node, or an * array of Nodes matching the criteria for processing. * @since 3.2.0 */ detachDelegate : noop, /** * Sets up the boilerplate for detaching the event and facilitating the * execution of subscriber callbacks. * * @method _on * @param args {Array} array of arguments passed to * <code>Y.on(...)</code> or <code>Y.delegate(...)</code> * @param delegate {Boolean} true if called from * <code>Y.delegate(...)</code> * @return {EventHandle} the detach handle for this subscription * @private * since 3.2.0 */ _on: function (args, delegate) { var handles = [], originalArgs = args.slice(), extra = this.processArgs(args, delegate), selector = args[2], method = delegate ? 'delegate' : 'on', nodes, handle; // Can't just use Y.all because it doesn't support window (yet?) nodes = (isString(selector)) ? query(selector) : toArray(selector || Y.one(Y.config.win)); if (!nodes.length && isString(selector)) { handle = Y.on('available', function () { Y.mix(handle, Y[method].apply(Y, originalArgs), true); }, selector); return handle; } Y.Array.each(nodes, function (node) { var subArgs = args.slice(), filter; node = Y.one(node); if (node) { if (delegate) { filter = subArgs.splice(3, 1)[0]; } // (type, fn, el, thisObj, ...) => (fn, thisObj, ...) subArgs.splice(0, 4, subArgs[1], subArgs[3]); if (!this.preventDups || !this.getSubs(node, args, null, true)) { handles.push(this._subscribe(node, method, subArgs, extra, filter)); } } }, this); return (handles.length === 1) ? handles[0] : new Y.EventHandle(handles); }, /** * Creates a new Notifier object for use by this event's * <code>on(...)</code> or <code>delegate(...)</code> implementation * and register the custom event proxy in the DOM system for cleanup. * * @method _subscribe * @param node {Node} the Node hosting the event * @param method {String} "on" or "delegate" * @param args {Array} the subscription arguments passed to either * <code>Y.on(...)</code> or <code>Y.delegate(...)</code> * after running through <code>processArgs(args)</code> to * normalize the argument signature * @param extra {any} Extra data parsed from * <code>processArgs(args)</code> * @param filter {String|Function} the selector string or function * filter passed to <code>Y.delegate(...)</code> (not * present when called from <code>Y.on(...)</code>) * @return {EventHandle} * @private * @since 3.2.0 */ _subscribe: function (node, method, args, extra, filter) { var dispatcher = new Y.CustomEvent(this.type, this.publishConfig), handle = dispatcher.on.apply(dispatcher, args), notifier = new Notifier(handle, this.emitFacade), registry = SyntheticEvent.getRegistry(node, this.type, true), sub = handle.sub; sub.node = node; sub.filter = filter; if (extra) { this.applyArgExtras(extra, sub); } Y.mix(dispatcher, { eventDef : this, notifier : notifier, host : node, // I forget what this is for currentTarget: node, // for generating facades target : node, // for generating facades el : node._node, // For category detach _delete : SyntheticEvent._deleteSub }, true); handle.notifier = notifier; registry.register(handle); // Call the implementation's "on" or "delegate" method this[method](node, sub, notifier, filter); return handle; }, /** * <p>Implementers MAY provide this method definition.</p> * * <p>Implement this function if you want extra data extracted during * processArgs to be propagated to subscriptions on a per-node basis. * That is to say, if you call <code>Y.on('xyz', fn, xtra, 'div')</code> * the data returned from processArgs will be shared * across the subscription objects for all the divs. If you want each * subscription to receive unique information, do that processing * here.</p> * * <p>The default implementation adds the data extracted by processArgs * to the subscription object as <code>sub._extra</code>.</p> * * @method applyArgExtras * @param extra {any} Any extra data extracted from processArgs * @param sub {Subscription} the individual subscription */ applyArgExtras: function (extra, sub) { sub._extra = extra; }, /** * Removes the subscription(s) from the internal subscription dispatch * mechanism. See <code>SyntheticEvent._deleteSub</code>. * * @method _detach * @param args {Array} The arguments passed to * <code>node.detach(...)</code> * @private * @since 3.2.0 */ _detach: function (args) { // Can't use Y.all because it doesn't support window (yet?) // TODO: Does Y.all support window now? var target = args[2], els = (isString(target)) ? query(target) : toArray(target), node, i, len, handles, j; // (type, fn, el, context, filter?) => (type, fn, context, filter?) args.splice(2, 1); for (i = 0, len = els.length; i < len; ++i) { node = Y.one(els[i]); if (node) { handles = this.getSubs(node, args); if (handles) { for (j = handles.length - 1; j >= 0; --j) { handles[j].detach(); } } } } }, /** * Returns the detach handles of subscriptions on a node that satisfy a * search/filter function. By default, the filter used is the * <code>subMatch</code> method. * * @method getSubs * @param node {Node} the node hosting the event * @param args {Array} the array of original subscription args passed * to <code>Y.on(...)</code> (before * <code>processArgs</code> * @param filter {Function} function used to identify a subscription * for inclusion in the returned array * @param first {Boolean} stop after the first match (used to check for * duplicate subscriptions) * @return {EventHandle[]} detach handles for the matching subscriptions */ getSubs: function (node, args, filter, first) { var registry = SyntheticEvent.getRegistry(node, this.type), handles = [], allHandles, i, len, handle; if (registry) { allHandles = registry.handles; if (!filter) { filter = this.subMatch; } for (i = 0, len = allHandles.length; i < len; ++i) { handle = allHandles[i]; if (filter.call(this, handle.sub, args)) { if (first) { return handle; } else { handles.push(allHandles[i]); } } } } return handles.length && handles; }, /** * <p>Implementers MAY override this to define what constitutes a * "same" subscription. Override implementations should * consider the lack of a comparator as a match, so calling * <code>getSubs()</code> with no arguments will return all subs.</p> * * <p>Compares a set of subscription arguments against a Subscription * object to determine if they match. The default implementation * compares the callback function against the second argument passed to * <code>Y.on(...)</code> or <code>node.detach(...)</code> etc.</p> * * @method subMatch * @param sub {Subscription} the existing subscription * @param args {Array} the calling arguments passed to * <code>Y.on(...)</code> etc. * @return {Boolean} true if the sub can be described by the args * present * @since 3.2.0 */ subMatch: function (sub, args) { // Default detach cares only about the callback matching return !args[1] || sub.fn === args[1]; } } }, true); Y.SyntheticEvent = SyntheticEvent; /** * <p>Defines a new event in the DOM event system. Implementers are * responsible for monitoring for a scenario whereby the event is fired. A * notifier object is provided to the functions identified below. When the * criteria defining the event are met, call notifier.fire( [args] ); to * execute event subscribers.</p> * * <p>The first parameter is the name of the event. The second parameter is a * configuration object which define the behavior of the event system when the * new event is subscribed to or detached from. The methods that should be * defined in this configuration object are <code>on</code>, * <code>detach</code>, <code>delegate</code>, and <code>detachDelegate</code>. * You are free to define any other methods or properties needed to define your * event. Be aware, however, that since the object is used to subclass * SyntheticEvent, you should avoid method names used by SyntheticEvent unless * your intention is to override the default behavior.</p> * * <p>This is a list of properties and methods that you can or should specify * in the configuration object:</p> * * <dl> * <dt><code>on</code></dt> * <dd><code>function (node, subscription, notifier)</code> The * implementation logic for subscription. Any special setup you need to * do to create the environment for the event being fired--E.g. native * DOM event subscriptions. Store subscription related objects and * state on the <code>subscription</code> object. When the * criteria have been met to fire the synthetic event, call * <code>notifier.fire(e)</code>. See Notifier's <code>fire()</code> * method for details about what to pass as parameters.</dd> * * <dt><code>detach</code></dt> * <dd><code>function (node, subscription, notifier)</code> The * implementation logic for cleaning up a detached subscription. E.g. * detach any DOM subscriptions added in <code>on</code>.</dd> * * <dt><code>delegate</code></dt> * <dd><code>function (node, subscription, notifier, filter)</code> The * implementation logic for subscription via <code>Y.delegate</code> or * <code>node.delegate</code>. The filter is typically either a selector * string or a function. You can use * <code>Y.delegate.compileFilter(selectorString)</code> to create a * filter function from a selector string if needed. The filter function * expects an event object as input and should output either null, a * matching Node, or an array of matching Nodes. Otherwise, this acts * like <code>on</code> DOM event subscriptions. Store subscription * related objects and information on the <code>subscription</code> * object. When the criteria have been met to fire the synthetic event, * call <code>notifier.fire(e)</code> as noted above.</dd> * * <dt><code>detachDelegate</code></dt> * <dd><code>function (node, subscription, notifier)</code> The * implementation logic for cleaning up a detached delegate subscription. * E.g. detach any DOM delegate subscriptions added in * <code>delegate</code>.</dd> * * <dt><code>publishConfig</code></dt> * <dd>(Object) The configuration object that will be used to instantiate * the underlying CustomEvent. See Notifier's <code>fire</code> method * for details.</dd> * * <dt><code>processArgs</code></dt * <dd> * <p><code>function (argArray, fromDelegate)</code> Optional method * to extract any additional arguments from the subscription * signature. Using this allows <code>on</code> or * <code>delegate</code> signatures like * <code>node.on("hover", overCallback, * outCallback)</code>.</p> * <p>When processing an atypical argument signature, make sure the * args array is returned to the normal signature before returning * from the function. For example, in the "hover" example * above, the <code>outCallback</code> needs to be <code>splice</code>d * out of the array. The expected signature of the args array for * <code>on()</code> subscriptions is:</p> * <pre> * <code>[type, callback, target, contextOverride, argN...]</code> * </pre> * <p>And for <code>delegate()</code>:</p> * <pre> * <code>[type, callback, target, filter, contextOverride, argN...]</code> * </pre> * <p>where <code>target</code> is the node the event is being * subscribed for. You can see these signatures documented for * <code>Y.on()</code> and <code>Y.delegate()</code> respectively.</p> * <p>Whatever gets returned from the function will be stored on the * <code>subscription</code> object under * <code>subscription._extra</code>.</p></dd> * <dt><code>subMatch</code></dt> * <dd> * <p><code>function (sub, args)</code> Compares a set of * subscription arguments against a Subscription object to determine * if they match. The default implementation compares the callback * function against the second argument passed to * <code>Y.on(...)</code> or <code>node.detach(...)</code> etc.</p> * </dd> * </dl> * * @method define * @param type {String} the name of the event * @param config {Object} the prototype definition for the new event (see above) * @param force {Boolean} override an existing event (use with caution) * @return {SyntheticEvent} the subclass implementation instance created to * handle event subscriptions of this type * @static * @for Event * @since 3.1.0 * @in event-synthetic */ Y.Event.define = function (type, config, force) { var eventDef, Impl, synth; if (type && type.type) { eventDef = type; force = config; } else if (config) { eventDef = Y.merge({ type: type }, config); } if (eventDef) { if (force || !Y.Node.DOM_EVENTS[eventDef.type]) { Impl = function () { SyntheticEvent.apply(this, arguments); }; Y.extend(Impl, SyntheticEvent, eventDef); synth = new Impl(); type = synth.type; Y.Node.DOM_EVENTS[type] = Y.Env.evt.plugins[type] = { eventDef: synth, on: function () { return synth._on(toArray(arguments)); }, delegate: function () { return synth._on(toArray(arguments), true); }, detach: function () { return synth._detach(toArray(arguments)); } }; } } else if (isString(type) || isArray(type)) { Y.Array.each(toArray(type), function (t) { Y.Node.DOM_EVENTS[t] = 1; }); } return synth; };