delegate.js revision 10d8bafc5c24f3a4285cf6060a1935ba5cfc4b85
3f3aa287185afb5d48d7ef0717054a154c372dc9Adam Moore/**
3f3aa287185afb5d48d7ef0717054a154c372dc9Adam Moore * Adds event delegation support to the library.
3f3aa287185afb5d48d7ef0717054a154c372dc9Adam Moore *
3f3aa287185afb5d48d7ef0717054a154c372dc9Adam Moore * @module event
3f3aa287185afb5d48d7ef0717054a154c372dc9Adam Moore * @submodule event-delegate
9f298ce1aa6fec44a47a40d6c358950d3c26ffd0Adam Moore */
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smithvar toArray = Y.Array,
483011d54907725ef9ea08d4c90cca21ae9fa11cLuke Smith YLang = Y.Lang,
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith isString = YLang.isString,
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith isObject = YLang.isObject,
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith isArray = YLang.isArray,
e59eb7d181274b19657e7f22826237dcf8a95c80Luke Smith selectorTest = Y.Selector.test,
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith detachCategories = Y.Env.evt.handles;
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith/**
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith * <p>Sets up event delegation on a container element. The delegated event
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith * will use a supplied selector or filtering function to test if the event
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith * references at least one node that should trigger the subscription
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith * callback.</p>
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith *
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith * <p>Selector string filters will trigger the callback if the event originated
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith * from a node that matches it or is contained in a node that matches it.
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith * Function filters are called for each Node up the parent axis to the
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith * subscribing container node, and receive at each level the Node and the event
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith * object. The function should return true (or a truthy value) if that Node
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith * should trigger the subscription callback. Note, it is possible for filters
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith * to match multiple Nodes for a single event. In this case, the delegate
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith * callback will be executed for each matching Node.</p>
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith *
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith * <p>For each matching Node, the callback will be executed with its 'this'
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith * object set to the Node matched by the filter (unless a specific context was
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith * provided during subscription), and the provided event's
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith * <code>currentTarget</code> will also be set to the matching Node. The
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith * containing Node from which the subscription was originally made can be
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith * referenced as <code>e.container</code>.
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith *
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith * @method delegate
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith * @param type {String} the event type to delegate
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith * @param fn {Function} the callback function to execute. This function
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith * will be provided the event object for the delegated event.
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith * @param el {String|node} the element that is the delegation container
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith * @param spec {string|Function} a selector that must match the target of the
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith * event or a function to test target and its parents for a match
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith * @param context optional argument that specifies what 'this' refers to.
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith * @param args* 0..n additional arguments to pass on to the callback function.
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith * These arguments will be added after the event object.
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith * @return {EventHandle} the detach handle
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith * @for YUI
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith */
e59eb7d181274b19657e7f22826237dcf8a95c80Luke Smithfunction delegate(type, fn, el, filter) {
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith var args = toArray(arguments, 0, true),
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith query = isString(el) ? el : null,
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith typeBits, synth, container, categories, cat, i, len, handles, handle;
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith // Support Y.delegate({ click: fnA, key: fnB }, context, filter, ...);
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith // and Y.delegate(['click', 'key'], fn, context, filter, ...);
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith if (isObject(type)) {
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith handles = [];
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith if (isArray(type)) {
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith for (i = 0, len = type.length; i < len; ++i) {
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith args[0] = type[i];
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith handles.push(Y.delegate.apply(Y, args));
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith }
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith } else {
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith // Y.delegate({'click', fn}, context, filter) =>
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith // Y.delegate('click', fn, context, filter)
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith args.unshift(null); // one arg becomes two; need to make space
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith for (i in type) {
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith if (type.hasOwnProperty(i)) {
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith args[0] = i;
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith args[1] = type[i];
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith handles.push(Y.delegate.apply(Y, args));
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith }
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith }
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith }
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith return new Y.EventHandle(handles);
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith }
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith typeBits = type.split(/\|/);
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith if (typeBits.length > 1) {
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith cat = typeBits.shift();
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith args[0] = type = typeBits.shift();
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith }
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith synth = Y.Node.DOM_EVENTS[type];
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith if (isObject(synth) && synth.delegate) {
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith handle = synth.delegate.apply(synth, arguments);
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith }
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith if (!handle) {
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith if (!type || !fn || !el || !filter) {
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith Y.log("delegate requires type, callback, parent, & filter", "warn");
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith return;
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith }
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith container = (query) ? Y.Selector.query(query, null, true) : el;
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith if (!container && isString(el)) {
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith handle = Y.on('available', function () {
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith Y.mix(handle, Y.delegate.apply(Y, args), true);
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith }, el);
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith }
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith if (!handle && container) {
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith args.splice(2, 2, container); // remove the filter
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith handle = Y.Event._attach(args, { facade: false });
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith handle.sub.filter = filter;
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith handle.sub._notify = delegate.notifySub;
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith }
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith }
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith if (handle && cat) {
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith categories = detachCategories[cat] || (detachCategories[cat] = {});
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith categories = categories[type] || (categories[type] = []);
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith categories.push(handle);
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith }
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith return handle;
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith}
5af2618af73bd3f008ef0e9b5f982f560c64059bAdam Moore
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith/**
5af2618af73bd3f008ef0e9b5f982f560c64059bAdam MooreOverrides the <code>_notify</code> method on the normal DOM subscription to
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smithinject the filtering logic and only proceed in the case of a match.
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith
b839e41217a63e244d65c3aadf54feec82ddd179Luke SmithThis method is hosted as a private property of the `delegate` method
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith(e.g. `Y.delegate.notifySub`)
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith@method notifySub
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith@param thisObj {Object} default 'this' object for the callback
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith@param args {Array} arguments passed to the event's <code>fire()</code>
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith@param ce {CustomEvent} the custom event managing the DOM subscriptions for
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith the subscribed event on the subscribing node.
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith@return {Boolean} false if the event was stopped
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith@private
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith@static
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith@since 3.2.0
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith**/
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smithdelegate.notifySub = function (thisObj, args, ce) {
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith // Preserve args for other subscribers
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith args = args.slice();
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith if (this.args) {
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith args.push.apply(args, this.args);
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith }
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith
5af2618af73bd3f008ef0e9b5f982f560c64059bAdam Moore // Only notify subs if the event occurred on a targeted element
5af2618af73bd3f008ef0e9b5f982f560c64059bAdam Moore var currentTarget = delegate._applyFilter(this.filter, args, ce),
5af2618af73bd3f008ef0e9b5f982f560c64059bAdam Moore //container = e.currentTarget,
537e74c4e869b8efba925d0e37c0a5636203a23bAdam Moore e, i, len, ret;
537e74c4e869b8efba925d0e37c0a5636203a23bAdam Moore
5af2618af73bd3f008ef0e9b5f982f560c64059bAdam Moore if (currentTarget) {
5af2618af73bd3f008ef0e9b5f982f560c64059bAdam Moore // Support multiple matches up the the container subtree
537e74c4e869b8efba925d0e37c0a5636203a23bAdam Moore currentTarget = toArray(currentTarget);
5af2618af73bd3f008ef0e9b5f982f560c64059bAdam Moore
5af2618af73bd3f008ef0e9b5f982f560c64059bAdam Moore // The second arg is the currentTarget, but we'll be reusing this
b839e41217a63e244d65c3aadf54feec82ddd179Luke Smith // facade, replacing the currentTarget for each use, so it doesn't
// matter what element we seed it with.
e = args[0] = new Y.DOMEventFacade(args[0], ce.el, ce);
e.container = Y.one(ce.el);
for (i = 0, len = currentTarget.length; i < len && !e.stopped; ++i) {
e.currentTarget = Y.one(currentTarget[i]);
ret = this.fn.apply(this.context || e.currentTarget, args);
if (ret === false) { // stop further notifications
break;
}
}
return ret;
}
};
/**
Compiles a selector string into a filter function to identify whether
Nodes along the parent axis of an event's target should trigger event
notification.
This function is memoized, so previously compiled filter functions are
returned if the same selector string is provided.
This function may be useful when defining synthetic events for delegate
handling.
Hosted as a property of the `delegate` method (e.g. `Y.delegate.compileFilter`).
@method compileFilter
@param selector {String} the selector string to base the filtration on
@return {Function}
@since 3.2.0
@static
**/
delegate.compileFilter = Y.cached(function (selector) {
return function (target, e) {
return selectorTest(target._node, selector, e.currentTarget._node);
};
});
/**
Walks up the parent axis of an event's target, and tests each element
against a supplied filter function. If any Nodes, including the container,
satisfy the filter, the delegated callback will be triggered for each.
Hosted as a protected property of the `delegate` method (e.g.
`Y.delegate._applyFilter`).
@method _applyFilter
@param filter {Function} boolean function to test for inclusion in event
notification
@param args {Array} the arguments that would be passed to subscribers
@param ce {CustomEvent} the DOM event wrapper
@return {Node|Node[]|undefined} The Node or Nodes that satisfy the filter
@protected
**/
delegate._applyFilter = function (filter, args, ce) {
var e = args[0],
container = ce.el, // facadeless events in IE, have no e.currentTarget
target = e.target || e.srcElement,
match = [],
isContainer = false;
// Resolve text nodes to their containing element
if (target.nodeType === 3) {
target = target.parentNode;
}
// passing target as the first arg rather than leaving well enough alone
// making 'this' in the filter function refer to the target. This is to
// support bound filter functions.
args.unshift(target);
if (isString(filter)) {
while (target) {
isContainer = (target === container);
if (selectorTest(target, filter, (isContainer ?null: container))) {
match.push(target);
}
if (isContainer) {
break;
}
target = target.parentNode;
}
} else {
// filter functions are implementer code and should receive wrappers
args[0] = Y.one(target);
args[1] = new Y.DOMEventFacade(e, container, ce);
while (target) {
// filter(target, e, extra args...) - this === target
if (filter.apply(args[0], args)) {
match.push(target);
}
if (target === container) {
break;
}
target = target.parentNode;
args[0] = Y.one(target);
}
args[1] = e; // restore the raw DOM event
}
if (match.length <= 1) {
match = match[0]; // single match or undefined
}
// remove the target
args.shift();
return match;
};
/**
* Sets up event delegation on a container element. The delegated event
* will use a supplied filter to test if the callback should be executed.
* This filter can be either a selector string or a function that returns
* a Node to use as the currentTarget for the event.
*
* The event object for the delegated event is supplied to the callback
* function. It is modified slightly in order to support all properties
* that may be needed for event delegation. 'currentTarget' is set to
* the element that matched the selector string filter or the Node returned
* from the filter function. 'container' is set to the element that the
* listener is delegated from (this normally would be the 'currentTarget').
*
* Filter functions will be called with the arguments that would be passed to
* the callback function, including the event object as the first parameter.
* The function should return false (or a falsey value) if the success criteria
* aren't met, and the Node to use as the event's currentTarget and 'this'
* object if they are.
*
* @method delegate
* @param type {string} the event type to delegate
* @param fn {function} the callback function to execute. This function
* will be provided the event object for the delegated event.
* @param el {string|node} the element that is the delegation container
* @param filter {string|function} a selector that must match the target of the
* event or a function that returns a Node or false.
* @param context optional argument that specifies what 'this' refers to.
* @param args* 0..n additional arguments to pass on to the callback function.
* These arguments will be added after the event object.
* @return {EventHandle} the detach handle
* @for YUI
*/
Y.delegate = Y.Event.delegate = delegate;