delegate.js revision 0211eb547f9f237c08793afee4008af3e8bd3da9
3f3aa287185afb5d48d7ef0717054a154c372dc9Adam Moore * Adds event delegation support to the library.
3f3aa287185afb5d48d7ef0717054a154c372dc9Adam Moore * @module event
3f3aa287185afb5d48d7ef0717054a154c372dc9Adam Moore * @submodule event-delegate
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smithvar toArray = Y.Array,
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
fb83a09fe023a741781ee955f4e9538d3cbe21a2Luke Smith * references at least one node that should trigger the subscription
fb83a09fe023a741781ee955f4e9538d3cbe21a2Luke Smith * callback.</p>
fb83a09fe023a741781ee955f4e9538d3cbe21a2Luke Smith * <p>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.
fb83a09fe023a741781ee955f4e9538d3cbe21a2Luke Smith * Function filters are called for each Node up the parent axis to the
fb83a09fe023a741781ee955f4e9538d3cbe21a2Luke Smith * subscribing container node, and receive at each level the Node and the event
fb83a09fe023a741781ee955f4e9538d3cbe21a2Luke Smith * object. The function should return true (or a truthy value) if that Node
fb83a09fe023a741781ee955f4e9538d3cbe21a2Luke Smith * should trigger the subscription callback. Note, it is possible for filters
fb83a09fe023a741781ee955f4e9538d3cbe21a2Luke Smith * to match multiple Nodes for a single event. In this case, the delegate
fb83a09fe023a741781ee955f4e9538d3cbe21a2Luke Smith * callback will be executed for each matching Node.</p>
fb83a09fe023a741781ee955f4e9538d3cbe21a2Luke Smith * <p>For each matching Node, the callback will be executed with its 'this'
fb83a09fe023a741781ee955f4e9538d3cbe21a2Luke Smith * object set to the Node matched by the filter (unless a specific context was
fb83a09fe023a741781ee955f4e9538d3cbe21a2Luke Smith * provided during subscription), and the provided event's
fb83a09fe023a741781ee955f4e9538d3cbe21a2Luke Smith * <code>currentTarget</code> will also be set to the matching Node. The
fb83a09fe023a741781ee955f4e9538d3cbe21a2Luke Smith * containing Node from which the subscription was originally made can be
fb83a09fe023a741781ee955f4e9538d3cbe21a2Luke Smith * referenced as <code>e.container</code>.
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
fb83a09fe023a741781ee955f4e9538d3cbe21a2Luke Smith * event or a function to test target and its parents for a match
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
f60d4516da25663a6679c02654d8c2da2701657bLuke Smith * @for Event
030b855bbf1937a46e1e2b88025d61e72a205469Luke Smith typeBits, synth, container, categories, cat, i, len, handles, handle;
d9462785fbdde2766d901cb36d19c9d6810c7954Ryan Grove // Support Y.delegate({ click: fnA, key: fnB }, el, filter, ...);
d9462785fbdde2766d901cb36d19c9d6810c7954Ryan Grove // and Y.delegate(['click', 'key'], fn, el, filter, ...);
d9462785fbdde2766d901cb36d19c9d6810c7954Ryan Grove // Y.delegate({'click', fn}, el, filter) =>
d9462785fbdde2766d901cb36d19c9d6810c7954Ryan Grove // Y.delegate('click', fn, el, filter)
030b855bbf1937a46e1e2b88025d61e72a205469Luke Smith args.unshift(null); // one arg becomes two; need to make space
030b855bbf1937a46e1e2b88025d61e72a205469Luke Smith for (i in type) {
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith Y.log("delegate requires type, callback, parent, & filter", "warn");
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith container = (query) ? Y.Selector.query(query, null, true) : el;
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith args.splice(2, 2, container); // remove the filter
669975fc1822c49f2f84c92ac2b7809df46b1093Luke Smith handle = Y.Event._attach(args, { facade: false });
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith categories = detachCategories[cat] || (detachCategories[cat] = {});
73cf5b20418beae941f34ec39a8d87035ae01711Luke Smith categories = categories[type] || (categories[type] = []);
10d8bafc5c24f3a4285cf6060a1935ba5cfc4b85Luke SmithOverrides the <code>_notify</code> method on the normal DOM subscription to
10d8bafc5c24f3a4285cf6060a1935ba5cfc4b85Luke Smithinject the filtering logic and only proceed in the case of a match.
10d8bafc5c24f3a4285cf6060a1935ba5cfc4b85Luke SmithThis method is hosted as a private property of the `delegate` method
10d8bafc5c24f3a4285cf6060a1935ba5cfc4b85Luke Smith(e.g. `Y.delegate.notifySub`)
10d8bafc5c24f3a4285cf6060a1935ba5cfc4b85Luke Smith@method notifySub
10d8bafc5c24f3a4285cf6060a1935ba5cfc4b85Luke Smith@param thisObj {Object} default 'this' object for the callback
10d8bafc5c24f3a4285cf6060a1935ba5cfc4b85Luke Smith@param args {Array} arguments passed to the event's <code>fire()</code>
10d8bafc5c24f3a4285cf6060a1935ba5cfc4b85Luke Smith@param ce {CustomEvent} the custom event managing the DOM subscriptions for
10d8bafc5c24f3a4285cf6060a1935ba5cfc4b85Luke Smith the subscribed event on the subscribing node.
10d8bafc5c24f3a4285cf6060a1935ba5cfc4b85Luke Smith@return {Boolean} false if the event was stopped
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke Smithdelegate.notifySub = function (thisObj, args, ce) {
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke Smith // Preserve args for other subscribers
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke Smith // Only notify subs if the event occurred on a targeted element
669975fc1822c49f2f84c92ac2b7809df46b1093Luke Smith var currentTarget = delegate._applyFilter(this.filter, args, ce),
669975fc1822c49f2f84c92ac2b7809df46b1093Luke Smith //container = e.currentTarget,
14017741ef334485c65013d129608513161db7c2Luke Smith // Support multiple matches up the the container subtree
669975fc1822c49f2f84c92ac2b7809df46b1093Luke Smith // The second arg is the currentTarget, but we'll be reusing this
669975fc1822c49f2f84c92ac2b7809df46b1093Luke Smith // facade, replacing the currentTarget for each use, so it doesn't
669975fc1822c49f2f84c92ac2b7809df46b1093Luke Smith // matter what element we seed it with.
669975fc1822c49f2f84c92ac2b7809df46b1093Luke Smith e = args[0] = new Y.DOMEventFacade(args[0], ce.el, ce);
851a447adf0cc52bb286ca6e8d71c3f6e088030dLuke Smith for (i = 0, len = currentTarget.length; i < len && !e.stopped; ++i) {
669975fc1822c49f2f84c92ac2b7809df46b1093Luke Smith ret = this.fn.apply(this.context || e.currentTarget, args);
0c4bb3413c8172f27fcebdf7242f5798d026064bLuke Smith if (ret === false) { // stop further notifications
10d8bafc5c24f3a4285cf6060a1935ba5cfc4b85Luke SmithCompiles a selector string into a filter function to identify whether
10d8bafc5c24f3a4285cf6060a1935ba5cfc4b85Luke SmithNodes along the parent axis of an event's target should trigger event
10d8bafc5c24f3a4285cf6060a1935ba5cfc4b85Luke Smithnotification.
10d8bafc5c24f3a4285cf6060a1935ba5cfc4b85Luke SmithThis function is memoized, so previously compiled filter functions are
10d8bafc5c24f3a4285cf6060a1935ba5cfc4b85Luke Smithreturned if the same selector string is provided.
10d8bafc5c24f3a4285cf6060a1935ba5cfc4b85Luke SmithThis function may be useful when defining synthetic events for delegate
10d8bafc5c24f3a4285cf6060a1935ba5cfc4b85Luke SmithHosted as a property of the `delegate` method (e.g. `Y.delegate.compileFilter`).
10d8bafc5c24f3a4285cf6060a1935ba5cfc4b85Luke Smith@method compileFilter
10d8bafc5c24f3a4285cf6060a1935ba5cfc4b85Luke Smith@param selector {String} the selector string to base the filtration on
10d8bafc5c24f3a4285cf6060a1935ba5cfc4b85Luke Smith@return {Function}
7b4d1363155303b5bfb852e5639b851bbd4dc255Luke Smithdelegate.compileFilter = Y.cached(function (selector) {
fb83a09fe023a741781ee955f4e9538d3cbe21a2Luke Smith return function (target, e) {
0211eb547f9f237c08793afee4008af3e8bd3da9Luke Smith (e.currentTarget === e.target) ? null : e.currentTarget._node);
10d8bafc5c24f3a4285cf6060a1935ba5cfc4b85Luke SmithWalks up the parent axis of an event's target, and tests each element
10d8bafc5c24f3a4285cf6060a1935ba5cfc4b85Luke Smithagainst a supplied filter function. If any Nodes, including the container,
10d8bafc5c24f3a4285cf6060a1935ba5cfc4b85Luke Smithsatisfy the filter, the delegated callback will be triggered for each.
10d8bafc5c24f3a4285cf6060a1935ba5cfc4b85Luke SmithHosted as a protected property of the `delegate` method (e.g.
10d8bafc5c24f3a4285cf6060a1935ba5cfc4b85Luke Smith`Y.delegate._applyFilter`).
10d8bafc5c24f3a4285cf6060a1935ba5cfc4b85Luke Smith@method _applyFilter
10d8bafc5c24f3a4285cf6060a1935ba5cfc4b85Luke Smith@param filter {Function} boolean function to test for inclusion in event
10d8bafc5c24f3a4285cf6060a1935ba5cfc4b85Luke Smith notification
10d8bafc5c24f3a4285cf6060a1935ba5cfc4b85Luke Smith@param args {Array} the arguments that would be passed to subscribers
10d8bafc5c24f3a4285cf6060a1935ba5cfc4b85Luke Smith@param ce {CustomEvent} the DOM event wrapper
10d8bafc5c24f3a4285cf6060a1935ba5cfc4b85Luke Smith@return {Node|Node[]|undefined} The Node or Nodes that satisfy the filter
669975fc1822c49f2f84c92ac2b7809df46b1093Luke Smithdelegate._applyFilter = function (filter, args, ce) {
669975fc1822c49f2f84c92ac2b7809df46b1093Luke Smith container = ce.el, // facadeless events in IE, have no e.currentTarget
669975fc1822c49f2f84c92ac2b7809df46b1093Luke Smith // Resolve text nodes to their containing element
fb83a09fe023a741781ee955f4e9538d3cbe21a2Luke Smith // passing target as the first arg rather than leaving well enough alone
fb83a09fe023a741781ee955f4e9538d3cbe21a2Luke Smith // making 'this' in the filter function refer to the target. This is to
fb83a09fe023a741781ee955f4e9538d3cbe21a2Luke Smith // support bound filter functions.
0211eb547f9f237c08793afee4008af3e8bd3da9Luke Smith if (selectorTest(target, filter, (isContainer ? null: container))) {
669975fc1822c49f2f84c92ac2b7809df46b1093Luke Smith // filter functions are implementer code and should receive wrappers
669975fc1822c49f2f84c92ac2b7809df46b1093Luke Smith args[1] = new Y.DOMEventFacade(e, container, ce);
669975fc1822c49f2f84c92ac2b7809df46b1093Luke Smith // filter(target, e, extra args...) - this === target
fb83a09fe023a741781ee955f4e9538d3cbe21a2Luke Smith // remove the target
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 * 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 * 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 * @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