Source: delite/activationTracker.js

/**
 * Tracks which widgets are currently "active".
 * A widget is considered active if it or a descendant widget has focus,
 * or if a non-focusable node of this widget or a descendant was the most recent node
 * to get a touchstart/mousedown/pointerdown event.
 *
 * Emits non-bubbling `delite-activated` and `delite-deactivated` events on widgets
 * as they become active, or stop being active, as defined above.
 *
 * Call `activationTracker.on("active-widget-stack", callback)` to track the stack of currently active widgets.
 *
 * Call `activationTracker.on("deactivated", func)` or `activationTracker.on("activated", ...)` to monitor when
 * when widgets become active/inactive.
 *
 * @module delite/activationTracker
 * */
define([
	"dcl/advise",
	"dcl/dcl",
	"requirejs-dplugins/jquery!attributes/classes",	// hasClass()
	"decor/Evented",
	"dpointer/events",		// so can just monitor for "pointerdown"
	"requirejs-domready/domReady!"
], function (advise, dcl, $, Evented) {

	// Time of the last touch/mouse and focusin events
	var lastPointerDownTime;
	var lastFocusinTime;

	// Last node that got pointerdown or focusin event, and the time it happened.
	var lastPointerDownOrFocusInNode;
	var lastPointerDownOrFocusInTime;

	var ActivationTracker = dcl(Evented, /** @lends module:delite/activationTracker */ {
		/**
		 * List of currently active widgets (focused widget and its ancestors).
		 * @property {Element[]} activeStack
		 */
		activeStack: [],

		/**
		 * Registers listeners on the specified iframe so that any pointerdown
		 * or focus event on that iframe (or anything in it) is reported
		 * as a focus/pointerdown event on the `<iframe>` itself.
		 *
		 * In dijit this was only used by editor; perhaps it should be removed.
		 *
		 * @param {HTMLIframeElement} iframe
		 * @returns {Object} Handle with `remove()` method to deregister.
		 */
		registerIframe: function (iframe) {
			return this.registerWin(iframe.contentWindow, iframe);
		},

		/**
		 * Registers listeners on the specified window (either the main
		 * window or an iframe's window) to detect when the user has touched / mouse-downed /
		 * focused somewhere.
		 *
		 * Users should call registerIframe() instead of this method.
		 *
		 * @param {Window} [targetWindow] - If specified this is the window associated with the iframe,
		 *       i.e. iframe.contentWindow.
		 * @param {Element} [effectiveNode] - If specified, report any focus events inside targetWindow as
		 *       an event on effectiveNode, rather than on evt.target.
		 * @returns {Object} Handle with `remove()` method to deregister.
		 * @private
		 */
		registerWin: function (targetWindow, effectiveNode) {
			// Listen for blur and focus events on targetWindow's document.
			var _this = this,
				doc = targetWindow.document,
				body = doc && doc.body;

			function pointerDownHandler(evt) {
				// workaround weird IE bug where the click is on an orphaned node
				// (first time clicking a Select/DropDownButton inside a TooltipDialog).
				// actually, strangely this is happening on latest chrome too.
				if (evt && evt.target && evt.target.parentNode == null) {
					return;
				}

				lastPointerDownTime = (new Date()).getTime();

				_this._pointerDownOrFocusHandler(effectiveNode || evt.target, "mouse");
			}

			function focusHandler(evt) {
				// When you refocus the browser window, IE gives an event with an empty srcElement
				if (!evt.target.tagName) {
					return;
				}

				// IE reports that nodes like <body> have gotten focus, even though they don't have a
				// tabindex setting.  Ignore those events.
				var tag = evt.target.tagName.toLowerCase();
				if (tag === "#document" || tag === "body") {
					return;
				}

				_this._focusHandler(effectiveNode || evt.target);
			}

			function blurHandler(evt) {
				_this._blurHandler(effectiveNode || evt.target);
			}

			if (body) {
				// Listen for touches or mousedowns.
				body.addEventListener("pointerdown", pointerDownHandler, true);
				body.addEventListener("focus", focusHandler, true);	// need true since focus doesn't bubble
				body.addEventListener("blur", blurHandler, true);	// need true since blur doesn't bubble

				return {
					remove: function () {
						body.removeEventListener("pointerdown", pointerDownHandler, true);
						body.removeEventListener("focus", focusHandler, true);
						body.removeEventListener("blur", blurHandler, true);
					}
				};
			}
		},

		/**
		 * Called when focus leaves a node.
		 * Usually ignored, _unless_ it *isn't* followed by touching another node,
		 * which indicates that we tabbed off the last field on the page,
		 * in which case every widget is marked inactive.
		 * @param {Element} node
		 * @private
		 */
		_blurHandler: function (node) { // jshint unused: vars
			var now = (new Date()).getTime();

			// IE9+ and chrome have a problem where focusout events come after the corresponding focusin event.
			// For chrome problem see https://bugs.dojotoolkit.org/ticket/17668.
			// IE problem happens when moving focus from the Editor's <iframe> to a normal DOMNode.
			if (now < lastFocusinTime + 100) {
				return;
			}

			// Unset timer to zero-out widget stack; we'll reset it below if appropriate.
			if (this._clearActiveWidgetsTimer) {
				clearTimeout(this._clearActiveWidgetsTimer);
			}

			if (now < lastPointerDownOrFocusInTime + 500) {
				// This blur event is coming late (after the call to _pointerDownOrFocusHandler() rather than before.
				// So let _pointerDownOrFocusHandler() handle setting the widget stack.
				// See https://bugs.dojotoolkit.org/ticket/17668
				return;
			}

			// If the blur event isn't followed (or preceded) by a focus or pointerdown event,
			// mark all widgets as inactive.
			this._clearActiveWidgetsTimer = setTimeout(function () {
				delete this._clearActiveWidgetsTimer;
				this._setStack([]);
			}.bind(this), 0);
		},

		/**
		 * Callback when node is focused or pointerdown'd.
		 * @param {Element} node - The node.
		 * @param {string} by - "mouse" if the focus/pointerdown was caused by a mouse down event.
		 * @private
		 */
		_pointerDownOrFocusHandler: function (node, by) {
			if (this._clearActiveWidgetsTimer) {
				// forget the recent blur event
				clearTimeout(this._clearActiveWidgetsTimer);
				delete this._clearActiveWidgetsTimer;
			}

			// compute stack of active widgets (ex: ComboButton --> Menu --> MenuItem)
			var newStack = [];
			try {
				while (node) {
					if (node._popupParent) {
						node = node._popupParent;
					} else if (node.tagName && node.tagName.toLowerCase() === "body") {
						// is this the root of the document or just the root of an iframe?
						if (node === document.body) {
							// node is the root of the main document
							break;
						}
						// otherwise, find the iframe this node refers to (can't access it via parentNode,
						// need to do this trick instead). window.frameElement is supported in IE/FF/Webkit
						node = node.ownerDocument.defaultView.frameElement;
					} else {
						// if this node is the root node of a widget, then add widget id to stack,
						// except ignore clicks on disabled widgets (actually focusing a disabled widget still works,
						// to support MenuItem)
						if (node.render && !(by === "mouse" && node.disabled)) {
							newStack.unshift(node);
						}
						node = node.parentNode;
					}
				}
			} catch (e) { /* squelch */
			}

			this._setStack(newStack, by);

			// Keep track of most recent focusin or pointerdown event.
			lastPointerDownOrFocusInTime = (new Date()).getTime();
			lastPointerDownOrFocusInNode = node;
		},

		/**
		 * Callback when node is focused.
		 * @param {Element} node
		 * @private
		 */
		_focusHandler: function (node) {
			if (!node) {
				return;
			}

			if (node.nodeType === 9) {
				// Ignore focus events on the document itself.  This is here so that
				// (for example) clicking the up/down arrows of a spinner
				// (which don't get focus) won't cause that widget to blur. (FF issue)
				return;
			}

			// Keep track of time of last focusin event.
			lastFocusinTime = (new Date()).getTime();

			// Also, if clicking a node causes its ancestor to be focused, ignore the focus event.
			// Example in the activationTracker.html functional test on IE, where clicking the spinner buttons
			// focuses the <fieldset> holding the spinner.
			if ((new Date()).getTime() < lastPointerDownTime + 100 &&
					node.contains(lastPointerDownOrFocusInNode.parentNode)) {
				return;
			}

			// There was probably a blur event right before this event, but since we have a new focus,
			// forget about the blur
			if (this._clearFocusTimer) {
				clearTimeout(this._clearFocusTimer);
				delete this._clearFocusTimer;
			}

			this._pointerDownOrFocusHandler(node);
		},

		/**
		 * The stack of active widgets has changed.  Send out appropriate events and records new stack.
		 * @param {module:delite/Widget[]} newStack - Array of widgets, starting from the top (outermost) widget.
		 * @param {string} by - "mouse" if the focus/pointerdown was caused by a mouse down event.
		 * @private
		 */
		_setStack: function (newStack, by) {
			var oldStack = this.activeStack, lastOldIdx = oldStack.length - 1, lastNewIdx = newStack.length - 1;

			if (newStack[lastNewIdx] === oldStack[lastOldIdx]) {
				// no changes, return now to avoid spurious notifications about changes to activeStack
				return;
			}

			this.activeStack = newStack;
			this.emit("active-widget-stack", newStack);

			var widget, i;

			// for all elements that have gone out of focus, set focused=false
			for (i = lastOldIdx; i >= 0 && oldStack[i] !== newStack[i]; i--) {
				widget = oldStack[i];
				if (widget) {
					widget.emit("delite-deactivated", {bubbles: false, by: by});
					this.emit("deactivated", widget, by);
				}
			}

			// for all element that have come into focus, set focused=true
			for (i++; i <= lastNewIdx; i++) {
				widget = newStack[i];
				if (widget) {
					widget.emit("delite-activated", {bubbles: false, by: by});
					this.emit("activated", widget, by);
				}
			}
		}
	});

	// Create singleton for top window
	var singleton = new ActivationTracker();
	singleton.registerWin(window);

	return singleton;
});