delegate.js revision 63d012ee193ba8c768b2a2aade99081422759213
3f3aa287185afb5d48d7ef0717054a154c372dc9Adam Moore/**
3f3aa287185afb5d48d7ef0717054a154c372dc9Adam Moore * Adds event delegation support to the library.
3f3aa287185afb5d48d7ef0717054a154c372dc9Adam Moore *
3f3aa287185afb5d48d7ef0717054a154c372dc9Adam Moore * @module event
3f3aa287185afb5d48d7ef0717054a154c372dc9Adam Moore * @submodule event-delegate
3f3aa287185afb5d48d7ef0717054a154c372dc9Adam Moore */
d408aa66c7199d6b6a133c20c2116414dc70fa0aAdam Moore
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smithvar toArray = Y.Array,
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith YLang = Y.Lang,
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith isString = YLang.isString,
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith selectorTest = Y.Selector.test,
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith detachCategories = Y.Env.evt.handles;
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke Smith
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith/**
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * <p>Sets up event delegation on a container element. The delegated event
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * will use a supplied selector or filtering function to test if the event
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * references at least one node that should trigger the subscription callback.
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * Selector string filters will trigger the callback if the event originated
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * from a node that matches it or is contained in a node that matches it.
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * Function filters accept the unfiltered event object and should return null
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * if it does not qualify for firing the callbacks, a Node if the callback
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * should be executed for one Node, or an array of Nodes if there were multiple
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * nodes in touched by the event that meet the filtering criteria.</p>
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith *
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * <p>The callback function will be executed only for those cases where the
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * filter returns at least one match. For each matching Node, the callback
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * will be executed with its 'this' object set to the Node matched by the
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * filter (unless a specific context was provided during subscription), and the
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * provided event's <code>currentTarget</code> will also be set to the matching
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * Node. The containing Node from which the subscription was originally made
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * can be referenced as <code>e.container</code>.
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith *
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * @method delegate
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * @param type {String} the event type to delegate
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * @param fn {Function} the callback function to execute. This function
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * will be provided the event object for the delegated event.
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * @param el {String|node} the element that is the delegation container
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * @param spec {string|Function} a selector that must match the target of the
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * event.
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * @param context optional argument that specifies what 'this' refers to.
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * @param args* 0..n additional arguments to pass on to the callback function.
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * These arguments will be added after the event object.
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * @return {EventHandle} the detach handle
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * @for YUI
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith */
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke Smithfunction delegate(type, fn, el, filter) {
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith var args = toArray(arguments, 0, true),
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith query = isString(el) ? el : null,
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith typeBits = type.split(/\|/),
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith synth, container, categories, cat, handle;
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith if (typeBits.length > 1) {
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith cat = typeBits.shift();
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith type = typeBits.shift();
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke Smith }
eea24ae7b751e818f5a88c631ddfa3799e963cd4Adam Moore
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith synth = Y.Node.DOM_EVENTS[type];
43c15c82a8f3e7361f944d6daa02f1885c88ebf0Adam Moore
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke Smith if (YLang.isObject(synth) && synth.delegate) {
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith handle = synth.delegate.apply(synth, arguments);
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke Smith }
edac37f62b5675c375833896993369855211cbfdTodd Kloots
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith if (!handle) {
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith if (!type || !fn || !el || !filter) {
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith Y.log("delegate requires type, callback, parent, & filter", "warn");
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith return;
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith }
eea24ae7b751e818f5a88c631ddfa3799e963cd4Adam Moore
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith container = (query) ? Y.Selector.query(query, null, true) : el;
eea24ae7b751e818f5a88c631ddfa3799e963cd4Adam Moore
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith if (!container && isString(el)) {
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith handle = Y.on('available', function () {
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith Y.mix(handle, Y.delegate.apply(Y, args), true);
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith }, el);
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith }
d408aa66c7199d6b6a133c20c2116414dc70fa0aAdam Moore
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith if (!handle && container) {
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith args.splice(2, 2, container); // remove the filter
7a925457f765fe5ea2507b4629476fa3e100e4eeTodd Kloots
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith if (isString(filter)) {
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith filter = Y.delegate.compileFilter(filter);
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith }
7a925457f765fe5ea2507b4629476fa3e100e4eeTodd Kloots
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith handle = Y.on.apply(Y, args);
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith handle.sub.getCurrentTarget = filter;
63d012ee193ba8c768b2a2aade99081422759213Luke Smith handle.sub._notify = delegate.notifySub;
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith }
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith }
7a925457f765fe5ea2507b4629476fa3e100e4eeTodd Kloots
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith if (handle && cat) {
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith categories = detachCategories[cat] || (detachCategories[cat] = {});
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith categories = categories[type] || (categories[type] = []);
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith categories.push(handle);
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke Smith }
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith return handle;
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke Smith}
7a925457f765fe5ea2507b4629476fa3e100e4eeTodd Kloots
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith/**
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * Overrides the <code>_notify</code> method on the normal DOM subscription to inject the filtering logic and only proceed in the case of a match.
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith *
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * @method delegate.notifySub
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * @param thisObj {Object} default 'this' object for the callback
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * @param args {Array} arguments passed to the event's <code>fire()</code>
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * @param ce {CustomEvent} the custom event managing the DOM subscriptions for
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * the subscribed event on the subscribing node.
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * @return {Boolean} false if the event was stopped
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * @private
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * @static
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * @since 3.2.0
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith */
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke Smithdelegate.notifySub = function (thisObj, args, ce) {
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke Smith // Preserve args for other subscribers
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke Smith args = args.slice();
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke Smith if (this.args) {
426d8b6487a18aa68071eecf43ed7bc4f58f36d4Luke Smith args.push.apply(args, this.args);
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke Smith }
7a925457f765fe5ea2507b4629476fa3e100e4eeTodd Kloots
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke Smith // Only notify subs if the event occurred on a targeted element
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke Smith var currentTarget = this.getCurrentTarget.apply(this, args),
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke Smith originalEvent = args[0],
14017741ef334485c65013d129608513161db7c2Luke Smith container = originalEvent.currentTarget,
14017741ef334485c65013d129608513161db7c2Luke Smith i, ret, target;
7a925457f765fe5ea2507b4629476fa3e100e4eeTodd Kloots
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke Smith if (currentTarget) {
14017741ef334485c65013d129608513161db7c2Luke Smith // Support multiple matches up the the container subtree
14017741ef334485c65013d129608513161db7c2Luke Smith currentTarget = toArray(currentTarget);
7a925457f765fe5ea2507b4629476fa3e100e4eeTodd Kloots
14017741ef334485c65013d129608513161db7c2Luke Smith for (i = currentTarget.length - 1; i >= 0; --i) {
14017741ef334485c65013d129608513161db7c2Luke Smith target = currentTarget[i];
7a925457f765fe5ea2507b4629476fa3e100e4eeTodd Kloots
14017741ef334485c65013d129608513161db7c2Luke Smith // New facade to avoid corrupting facade sent to direct subs
14017741ef334485c65013d129608513161db7c2Luke Smith args[0] = new Y.DOMEventFacade(originalEvent, target, ce);
7a925457f765fe5ea2507b4629476fa3e100e4eeTodd Kloots
14017741ef334485c65013d129608513161db7c2Luke Smith args[0].container = container;
14017741ef334485c65013d129608513161db7c2Luke Smith
14017741ef334485c65013d129608513161db7c2Luke Smith thisObj = this.context || target;
eea24ae7b751e818f5a88c631ddfa3799e963cd4Adam Moore
14017741ef334485c65013d129608513161db7c2Luke Smith ret = this.fn.apply(thisObj, args);
7a925457f765fe5ea2507b4629476fa3e100e4eeTodd Kloots
0c4bb3413c8172f27fcebdf7242f5798d026064bLuke Smith if (ret === false) { // stop further notifications
0c4bb3413c8172f27fcebdf7242f5798d026064bLuke Smith break;
14017741ef334485c65013d129608513161db7c2Luke Smith }
d408aa66c7199d6b6a133c20c2116414dc70fa0aAdam Moore }
7a925457f765fe5ea2507b4629476fa3e100e4eeTodd Kloots
14017741ef334485c65013d129608513161db7c2Luke Smith return ret;
14017741ef334485c65013d129608513161db7c2Luke Smith }
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke Smith};
67d8986c26b16b1d9ea6e5b62e753c55af9a4f52Todd Kloots
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith/**
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * <p>Compiles a selector string into a filter function that returns an array of Nodes matching that selector starging from the event's target up the parent axis to the Node from which the subscription occurred. If only one Node matches, it is returned (absent the array), and if none, undefined is returned.</p>
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith *
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * <p>Previously compiled filter functions are returned if the same selector string is provided.</p>
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith *
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * <p>This function may be useful when defining synthetic events for delegate handling.</p>
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith *
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * @method delegate.compileFilter
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * @param selector {String} the selector string to base the filtration on
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * @return {Function}
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * @since 3.2.0
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith * @static
680f13616a493c7bf3a794982e07d10abd9763b3Luke Smith */
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke Smithdelegate.compileFilter = Y.cached(function (selector) {
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke Smith return function (e) {
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke Smith var container = e.currentTarget._node,
8177882b1cec452d6822b48b9aa69007dcdf3a28Luke Smith target = e.target._node,
8177882b1cec452d6822b48b9aa69007dcdf3a28Luke Smith match = [];
67d8986c26b16b1d9ea6e5b62e753c55af9a4f52Todd Kloots
8177882b1cec452d6822b48b9aa69007dcdf3a28Luke Smith while (target !== container) {
8177882b1cec452d6822b48b9aa69007dcdf3a28Luke Smith if (selectorTest(target, selector, container)) {
8177882b1cec452d6822b48b9aa69007dcdf3a28Luke Smith match.push(Y.one(target));
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke Smith }
8177882b1cec452d6822b48b9aa69007dcdf3a28Luke Smith target = target.parentNode;
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke Smith }
a5f89af11a2a0cd671fb5d37a001dfc929eba3b1Todd Kloots
8177882b1cec452d6822b48b9aa69007dcdf3a28Luke Smith if (match.length <= 1) {
8177882b1cec452d6822b48b9aa69007dcdf3a28Luke Smith match = match[0]; // single match or undefined
14017741ef334485c65013d129608513161db7c2Luke Smith }
14017741ef334485c65013d129608513161db7c2Luke Smith
8177882b1cec452d6822b48b9aa69007dcdf3a28Luke Smith return match;
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke Smith };
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke Smith});
aadc0b0e666b9b335884a2437510798ae8949343Adam Moore
67d8986c26b16b1d9ea6e5b62e753c55af9a4f52Todd Kloots/**
67d8986c26b16b1d9ea6e5b62e753c55af9a4f52Todd Kloots * Sets up event delegation on a container element. The delegated event
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke Smith * will use a supplied filter to test if the callback should be executed.
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke Smith * This filter can be either a selector string or a function that returns
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke Smith * a Node to use as the currentTarget for the event.
67d8986c26b16b1d9ea6e5b62e753c55af9a4f52Todd Kloots *
67d8986c26b16b1d9ea6e5b62e753c55af9a4f52Todd Kloots * The event object for the delegated event is supplied to the callback
67d8986c26b16b1d9ea6e5b62e753c55af9a4f52Todd Kloots * function. It is modified slightly in order to support all properties
67d8986c26b16b1d9ea6e5b62e753c55af9a4f52Todd Kloots * that may be needed for event delegation. 'currentTarget' is set to
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke Smith * the element that matched the selector string filter or the Node returned
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke Smith * from the filter function. 'container' is set to the element that the
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke Smith * listener is delegated from (this normally would be the 'currentTarget').
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke Smith *
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke Smith * Filter functions will be called with the arguments that would be passed to
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke Smith * the callback function, including the event object as the first parameter.
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke Smith * The function should return false (or a falsey value) if the success criteria
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke Smith * aren't met, and the Node to use as the event's currentTarget and 'this'
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke Smith * object if they are.
67d8986c26b16b1d9ea6e5b62e753c55af9a4f52Todd Kloots *
67d8986c26b16b1d9ea6e5b62e753c55af9a4f52Todd Kloots * @method delegate
67d8986c26b16b1d9ea6e5b62e753c55af9a4f52Todd Kloots * @param type {string} the event type to delegate
67d8986c26b16b1d9ea6e5b62e753c55af9a4f52Todd Kloots * @param fn {function} the callback function to execute. This function
67d8986c26b16b1d9ea6e5b62e753c55af9a4f52Todd Kloots * will be provided the event object for the delegated event.
67d8986c26b16b1d9ea6e5b62e753c55af9a4f52Todd Kloots * @param el {string|node} the element that is the delegation container
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke Smith * @param filter {string|function} a selector that must match the target of the
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke Smith * event or a function that returns a Node or false.
67d8986c26b16b1d9ea6e5b62e753c55af9a4f52Todd Kloots * @param context optional argument that specifies what 'this' refers to.
67d8986c26b16b1d9ea6e5b62e753c55af9a4f52Todd Kloots * @param args* 0..n additional arguments to pass on to the callback function.
67d8986c26b16b1d9ea6e5b62e753c55af9a4f52Todd Kloots * These arguments will be added after the event object.
67d8986c26b16b1d9ea6e5b62e753c55af9a4f52Todd Kloots * @return {EventHandle} the detach handle
67d8986c26b16b1d9ea6e5b62e753c55af9a4f52Todd Kloots * @for YUI
67d8986c26b16b1d9ea6e5b62e753c55af9a4f52Todd Kloots */
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke SmithY.delegate = Y.Event.delegate = delegate;