Source: delite/Viewport.js

/**
 * Utility singleton to watch for viewport resizes, avoiding duplicate notifications
 * which can lead to infinite loops.
 *
 * Usage:
 * ```
 * Viewport.on("resize", myCallback)
 * Viewport.on("scroll", myOtherCallback)
 * ```
 * 
 * myCallback() is called without arguments in case it's Widget.resize(),
 * which would interpret the argument as the size to make the widget.
 *
 * @module delite/Viewport
 */
define([
	"decor/Evented",
	"decor/sniff",	// has("ios")
	"requirejs-domready/domReady!"
], function (Evented, has) {
	var Viewport = new Evented();

	// Get the size of the viewport without size adjustment needed for iOS soft keyboard.
	// On android though, this returns the size of the visible area not including the keyboard.
	function getBox() {
		if (has("ios") < 8) {
			// Workaround iOS < 8 problem where window.innerHeight is too low when the document is scrolled so
			// much that the document ends before the bottom of the keyboard.  Workaround not needed and doesn't work,
			// on iOS 8.
			var bcr = document.body.getBoundingClientRect();
			return {
				w: Math.max(bcr.width, window.innerWidth),
				h: Math.max(bcr.height, window.innerHeight),
				t: window.pageYOffset,
				l: window.pageXOffset
			};
		} else {
			return {
				w: window.innerWidth,
				h: window.innerHeight,
				t: window.pageYOffset,
				l: window.pageXOffset
			};
		}
	}

	/**
	 * Get the size of the viewport, or on mobile devices, the part of the viewport not obscured by the
	 * virtual keyboard.
	 * @function module:delite/Viewport.getEffectiveBox
	 */
	Viewport.getEffectiveBox = function () {
		/* jshint maxcomplexity:12 */

		var box = getBox();

		// Account for iOS virtual keyboard, if it's being shown.  Unfortunately no direct way to check or measure.
		var focusedNode = document.activeElement,
			tag = focusedNode && focusedNode.tagName && focusedNode.tagName.toLowerCase();
		if (has("ios") && focusedNode && !focusedNode.readOnly && (tag === "textarea" || (tag === "input" &&
			/^(color|email|number|password|search|tel|text|url)$/.test(focusedNode.type)))) {

			// Box represents the size of the viewport.  Some of the viewport is likely covered by the keyboard.
			// Estimate height of visible viewport assuming viewport goes to bottom of screen,
			// but is covered by keyboard.
			
			// By my measurements the effective viewport is the following size (compared to full viewport:
			// Portrait / landscape / window.screen.height:
			// iPhone 6 / iOS 8: 54% / 26% / 667
			// iPhone 5s / iOS 8: 53% / 27% / 568
			// iPhone 5s / iOS 7: 53% / 19% / 568
			// iPhone 5 / iOS 8: 52% / 27% / 568
			// iPhone 4s / iOS 7: 41% / 19% / 480
			// iPad 2 / iOS 7.1: 66% / 41%
			// iPhone 3s / iOS6: 43% / 29% (but w/hidden address bar because it hides all the time)

			if (has("ipad")) {
				// Numbers for iPad 2, hopefully it works for other iPads (including iPad mini) too.
				box.h *= (window.orientation === 0 || window.orientation === 180 ? 0.65 : 0.38);
			} else {
				// iPhone varies a lot by model, this should estimate the available space conservatively
				if (window.orientation === 0 || window.orientation === 180) {
					// portrait
					box.h *= (window.screen.height > 500 ? 0.54 : 0.42);
				} else {
					// landscape
					box.h *= (window.screen.height > 500 && has("ios") >= 8 ? 0.26 : 0.19);
				}
			}

			// Account for space taken by auto-completion suggestions.
			if (has("ios") >= 8 &&
				(!focusedNode.hasAttribute("autocorrect") || focusedNode.getAttribute("autocorrect") === "on") &&
				/^(color|number|search|tel|text)$/.test(focusedNode.type)) {
				box.h -= 40;
			}
		}

		return box;
	};

	var oldEffectiveSize = Viewport.getEffectiveBox(),
		oldEffectiveScroll = oldEffectiveSize;

	function checkForResize() {
		var newBox = Viewport.getEffectiveBox();
		if (newBox.h !== oldEffectiveSize.h || newBox.w !== oldEffectiveSize.w) {
			oldEffectiveSize = newBox;
			Viewport.emit("resize", newBox);
			return true;
		} else {
			return false;
		}
	}
	function checkForScroll() {
		var newBox = Viewport.getEffectiveBox();
		if (newBox.t !== oldEffectiveScroll.t || newBox.l !== oldEffectiveScroll.l) {
			oldEffectiveScroll = newBox;
			Viewport.emit("scroll", newBox);
			return true;
		} else {
			return false;
		}
	}

	// Poll for viewport resizes due to rotation, browser window size change, or the virtual keyboard
	// popping up/down.
	function poll() {
		var resized = checkForResize(),
			scrolled = checkForScroll();
		setTimeout(poll, resized || scrolled ? 10 : 50);
	}
	poll();

	return Viewport;
});