/** * Adds bubbling and delegation support to DOM events focus and blur. * * @module event * @submodule event-focus */ var Event = Y.Event, YLang = Y.Lang, isString = YLang.isString, arrayIndex = Y.Array.indexOf, useActivate = (function() { // Changing the structure of this test, so that it doesn't use inline JS in HTML, // which throws an exception in Win8 packaged apps, due to additional security restrictions: // http://msdn.microsoft.com/en-us/library/windows/apps/hh465380.aspx#differences var supported = false, doc = Y.config.doc, p; if (doc) { p = doc.createElement("p"); p.setAttribute("onbeforeactivate", ";"); // onbeforeactivate is a function in IE8+. // onbeforeactivate is a string in IE6,7 (unfortunate, otherwise we could have just checked for function below). // onbeforeactivate is a function in IE10, in a Win8 App environment (no exception running the test). // onbeforeactivate is undefined in Webkit/Gecko. // onbeforeactivate is a function in Webkit/Gecko if it's a supported event (e.g. onclick). supported = (p.onbeforeactivate !== undefined); } return supported; }()); function define(type, proxy, directEvent) { var nodeDataKey = '_' + type + 'Notifiers'; Y.Event.define(type, { _useActivate : useActivate, _attach: function (el, notifier, delegate) { if (Y.DOM.isWindow(el)) { return Event._attach([type, function (e) { notifier.fire(e); }, el]); } else { return Event._attach( [proxy, this._proxy, el, this, notifier, delegate], { capture: true }); } }, _proxy: function (e, notifier, delegate) { var target = e.target, currentTarget = e.currentTarget, notifiers = target.getData(nodeDataKey), yuid = Y.stamp(currentTarget._node), defer = (useActivate || target !== currentTarget), directSub; notifier.currentTarget = (delegate) ? target : currentTarget; notifier.container = (delegate) ? currentTarget : null; // Maintain a list to handle subscriptions from nested // containers div#a>div#b>input #a.on(focus..) #b.on(focus..), // use one focus or blur subscription that fires notifiers from // #b then #a to emulate bubble sequence. if (!notifiers) { notifiers = {}; target.setData(nodeDataKey, notifiers); // only subscribe to the element's focus if the target is // not the current target ( if (defer) { directSub = Event._attach( [directEvent, this._notify, target._node]).sub; directSub.once = true; } } else { // In old IE, defer is always true. In capture-phase browsers, // The delegate subscriptions will be encountered first, which // will establish the notifiers data and direct subscription // on the node. If there is also a direct subscription to the // node's focus/blur, it should not call _notify because the // direct subscription from the delegate sub(s) exists, which // will call _notify. So this avoids _notify being called // twice, unnecessarily. defer = true; } if (!notifiers[yuid]) { notifiers[yuid] = []; } notifiers[yuid].push(notifier); if (!defer) { this._notify(e); } }, _notify: function (e, container) { var currentTarget = e.currentTarget, notifierData = currentTarget.getData(nodeDataKey), axisNodes = currentTarget.ancestors(), doc = currentTarget.get('ownerDocument'), delegates = [], // Used to escape loops when there are no more // notifiers to consider count = notifierData ? Y.Object.keys(notifierData).length : 0, target, notifiers, notifier, yuid, match, tmp, i, len, sub, ret; // clear the notifications list (mainly for delegation) currentTarget.clearData(nodeDataKey); // Order the delegate subs by their placement in the parent axis axisNodes.push(currentTarget); // document.get('ownerDocument') returns null // which we'll use to prevent having duplicate Nodes in the list if (doc) { axisNodes.unshift(doc); } // ancestors() returns the Nodes from top to bottom axisNodes._nodes.reverse(); if (count) { // Store the count for step 2 tmp = count; axisNodes.some(function (node) { var yuid = Y.stamp(node), notifiers = notifierData[yuid], i, len; if (notifiers) { count--; for (i = 0, len = notifiers.length; i < len; ++i) { if (notifiers[i].handle.sub.filter) { delegates.push(notifiers[i]); } } } return !count; }); count = tmp; } // Walk up the parent axis, notifying direct subscriptions and // testing delegate filters. while (count && (target = axisNodes.shift())) { yuid = Y.stamp(target); notifiers = notifierData[yuid]; if (notifiers) { for (i = 0, len = notifiers.length; i < len; ++i) { notifier = notifiers[i]; sub = notifier.handle.sub; match = true; e.currentTarget = target; if (sub.filter) { match = sub.filter.apply(target, [target, e].concat(sub.args || [])); // No longer necessary to test against this // delegate subscription for the nodes along // the parent axis. delegates.splice( arrayIndex(delegates, notifier), 1); } if (match) { // undefined for direct subs e.container = notifier.container; ret = notifier.fire(e); } if (ret === false || e.stopped === 2) { break; } } delete notifiers[yuid]; count--; } if (e.stopped !== 2) { // delegates come after subs targeting this specific node // because they would not normally report until they'd // bubbled to the container node. for (i = 0, len = delegates.length; i < len; ++i) { notifier = delegates[i]; sub = notifier.handle.sub; if (sub.filter.apply(target, [target, e].concat(sub.args || []))) { e.container = notifier.container; e.currentTarget = target; ret = notifier.fire(e); } if (ret === false || e.stopped === 2 || // If e.stopPropagation() is called, notify any // delegate subs from the same container, but break // once the container changes. This emulates // delegate() behavior for events like 'click' which // won't notify delegates higher up the parent axis. (e.stopped && delegates[i+1] && delegates[i+1].container !== notifier.container)) { break; } } } if (e.stopped) { break; } } }, on: function (node, sub, notifier) { sub.handle = this._attach(node._node, notifier); }, detach: function (node, sub) { sub.handle.detach(); }, delegate: function (node, sub, notifier, filter) { if (isString(filter)) { sub.filter = function (target) { return Y.Selector.test(target._node, filter, node === target ? null : node._node); }; } sub.handle = this._attach(node._node, notifier, true); }, detachDelegate: function (node, sub) { sub.handle.detach(); } }, true); } // For IE, we need to defer to focusin rather than focus because // `el.focus(); doSomething();` executes el.onbeforeactivate, el.onactivate, // el.onfocusin, doSomething, then el.onfocus. All others support capture // phase focus, which executes before doSomething. To guarantee consistent // behavior for this use case, IE's direct subscriptions are made against // focusin so subscribers will be notified before js following el.focus() is // executed. if (useActivate) { // name capture phase direct subscription define("focus", "beforeactivate", "focusin"); define("blur", "beforedeactivate", "focusout"); } else { define("focus", "focus", "focus"); define("blur", "blur", "blur"); }