// Licensed to the Software Freedom Conservancy (SFC) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The SFC licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. /** * @fileoverview Atoms for simulating user actions against the browser window. */ goog.provide('bot.window'); goog.require('bot'); goog.require('bot.Error'); goog.require('bot.ErrorCode'); goog.require('bot.events'); goog.require('bot.userAgent'); goog.require('goog.dom'); goog.require('goog.dom.DomHelper'); goog.require('goog.math.Coordinate'); goog.require('goog.math.Size'); goog.require('goog.style'); goog.require('goog.userAgent'); goog.require('goog.userAgent.product'); /** * Whether the value of history.length includes a newly loaded page. If not, * after a new page load history.length is the number of pages that have loaded, * minus 1, but becomes the total number of pages on a subsequent back() call. * @private {boolean} * @const */ bot.window.HISTORY_LENGTH_INCLUDES_NEW_PAGE_ = !goog.userAgent.IE; /** * Whether value of history.length includes the pages ahead of the current one * in the history. If not, history.length equals the number of prior pages. * Here is the WebKit bug for this behavior that was fixed by version 533: * https://bugs.webkit.org/show_bug.cgi?id=24472 * @private {boolean} * @const */ bot.window.HISTORY_LENGTH_INCLUDES_FORWARD_PAGES_ = !goog.userAgent.WEBKIT || bot.userAgent.isEngineVersion('533'); /** * Screen orientation values. From the draft W3C spec at: * http://www.w3.org/TR/2012/WD-screen-orientation-20120522 * * @enum {string} */ bot.window.Orientation = { PORTRAIT: 'portrait-primary', PORTRAIT_SECONDARY: 'portrait-secondary', LANDSCAPE: 'landscape-primary', LANDSCAPE_SECONDARY: 'landscape-secondary' }; /** * Returns the degrees corresponding to the orientation input. * * @param {!bot.window.Orientation} orientation The orientation. * @return {number} The orientation degrees. * @private */ bot.window.getOrientationDegrees_ = (function () { var orientationMap; return function (orientation) { if (!orientationMap) { orientationMap = {}; if (goog.userAgent.MOBILE) { // The iPhone and Android phones do not change orientation event when // held upside down. Hence, PORTRAIT_SECONDARY is not set. orientationMap[bot.window.Orientation.PORTRAIT] = 0; orientationMap[bot.window.Orientation.LANDSCAPE] = 90; orientationMap[bot.window.Orientation.LANDSCAPE_SECONDARY] = -90; if (goog.userAgent.product.IPAD) { orientationMap[bot.window.Orientation.PORTRAIT_SECONDARY] = 180; } } else if (goog.userAgent.product.ANDROID) { // Unlike the iPad, Android tablets treat landscape orientation as the // default, i.e., having window.orientation = 0. orientationMap[bot.window.Orientation.PORTRAIT] = -90; orientationMap[bot.window.Orientation.LANDSCAPE] = 0; orientationMap[bot.window.Orientation.PORTRAIT_SECONDARY] = 90; orientationMap[bot.window.Orientation.LANDSCAPE_SECONDARY] = 180; } } return orientationMap[orientation]; }; })(); /** * Go back in the browser history. The number of pages to go back can * optionally be specified and defaults to 1. * * @param {number=} opt_numPages Number of pages to go back. */ bot.window.back = function (opt_numPages) { // Relax the upper bound by one for browsers that do not count // newly loaded pages towards the value of window.history.length. var maxPages = bot.window.HISTORY_LENGTH_INCLUDES_NEW_PAGE_ ? bot.getWindow().history.length - 1 : bot.getWindow().history.length; var numPages = bot.window.checkNumPages_(maxPages, opt_numPages); bot.getWindow().history.go(-numPages); }; /** * Go forward in the browser history. The number of pages to go forward can * optionally be specified and defaults to 1. * * @param {number=} opt_numPages Number of pages to go forward. */ bot.window.forward = function (opt_numPages) { // Do not check the upper bound (use null for infinity) for browsers that // do not count forward pages towards the value of window.history.length. var maxPages = bot.window.HISTORY_LENGTH_INCLUDES_FORWARD_PAGES_ ? bot.getWindow().history.length - 1 : null; var numPages = bot.window.checkNumPages_(maxPages, opt_numPages); bot.getWindow().history.go(numPages); }; /** * @param {?number} maxPages Upper bound on number of pages; null for infinity. * @param {number=} opt_numPages Number of pages to move in history. * @return {number} Correct number of pages to move in history. * @private */ bot.window.checkNumPages_ = function (maxPages, opt_numPages) { var numPages = opt_numPages !== undefined ? opt_numPages : 1; if (numPages <= 0) { throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR, 'number of pages must be positive'); } if (maxPages !== null && numPages > maxPages) { throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR, 'number of pages must be less than the length of the browser history'); } return numPages; }; /** * Determine the size of the window that a user could interact with. This will * be the greatest of document.body.(width|scrollWidth), the same for * document.documentElement or the size of the viewport. * * @param {!Window=} opt_win Window to determine the size of. Defaults to * bot.getWindow(). * @return {!goog.math.Size} The calculated size. */ bot.window.getInteractableSize = function (opt_win) { var win = opt_win || bot.getWindow(); var doc = win.document; var elem = doc.documentElement; var body = doc.body; if (!body) { throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR, 'No BODY element present'); } var widths = [ elem.clientWidth, elem.scrollWidth, elem.offsetWidth, body.scrollWidth, body.offsetWidth ]; var heights = [ elem.clientHeight, elem.scrollHeight, elem.offsetHeight, body.scrollHeight, body.offsetHeight ]; var width = Math.max.apply(null, widths); var height = Math.max.apply(null, heights); return new goog.math.Size(width, height); }; /** * Gets the frame element. * * @param {!Window} win Window of the frame. Defaults to bot.getWindow(). * @return {Element} The frame element if it exists, null otherwise. * @private */ bot.window.getFrame_ = function (win) { try { // On IE, accessing the frameElement of a popup window results in a "No // Such interface" exception. return win.frameElement; } catch (e) { return null; } }; /** * Determine the outer size of the window. * * @param {!Window=} opt_win Window to determine the size of. Defaults to * bot.getWindow(). * @return {!goog.math.Size} The calculated size. */ bot.window.getSize = function (opt_win) { var win = opt_win || bot.getWindow(); var frame = bot.window.getFrame_(win); if (bot.userAgent.ANDROID_PRE_ICECREAMSANDWICH) { if (frame) { // Early Android browsers do not account for border width. var box = goog.style.getBorderBox(frame); return new goog.math.Size(frame.clientWidth - box.left - box.right, frame.clientHeight); } else { // A fixed popup size. return new goog.math.Size(320, 240); } } else if (frame) { return new goog.math.Size(frame.clientWidth, frame.clientHeight); } else { var docElem = win.document.documentElement; var body = win.document.body; var width = win.outerWidth || (docElem && docElem.clientWidth) || (body && body.clientWidth) || 0; var height = win.outerHeight || (docElem && docElem.clientHeight) || (body && body.clientHeight) || 0; return new goog.math.Size(width, height); } }; /** * Set the outer size of the window. * * @param {!goog.math.Size} size The new window size. * @param {!Window=} opt_win Window to determine the size of. Defaults to * bot.getWindow(). */ bot.window.setSize = function (size, opt_win) { var win = opt_win || bot.getWindow(); var frame = bot.window.getFrame_(win); if (frame) { // minHeight and minWidth are altered because many browsers will not change // height or width if it is less than a specified minHeight or minWidth. frame.style.minHeight = '0px'; frame.style.minWidth = '0px'; frame.width = size.width + 'px'; frame.style.width = size.width + 'px'; frame.height = size.height + 'px'; frame.style.height = size.height + 'px'; } else { win.resizeTo(size.width, size.height); } }; /** * Determine the scroll position of the window. * * @param {!Window=} opt_win Window to determine the scroll position of. * Defaults to bot.getWindow(). * @return {!goog.math.Coordinate} The scroll position. */ bot.window.getScroll = function (opt_win) { var win = opt_win || bot.getWindow(); return new goog.dom.DomHelper(win.document).getDocumentScroll(); }; /** * Set the scroll position of the window. * * @param {!goog.math.Coordinate} position The new scroll position. * @param {!Window=} opt_win Window to apply position to. Defaults to * bot.getWindow(). */ bot.window.setScroll = function (position, opt_win) { var win = opt_win || bot.getWindow(); win.scrollTo(position.x, position.y); }; /** * Get the position of the window. * * @param {!Window=} opt_win Window to determine the position of. Defaults to * bot.getWindow(). * @return {!goog.math.Coordinate} The position of the window. */ bot.window.getPosition = function (opt_win) { var win = opt_win || bot.getWindow(); var x, y; if (goog.userAgent.IE) { x = win.screenLeft; y = win.screenTop; } else { x = win.screenX; y = win.screenY; } return new goog.math.Coordinate(x, y); }; /** * Set the position of the window. * * @param {!goog.math.Coordinate} position The target position. * @param {!Window=} opt_win Window to set the position of. Defaults to * bot.getWindow(). */ bot.window.setPosition = function (position, opt_win) { var win = opt_win || bot.getWindow(); win.moveTo(position.x, position.y); }; /** * Scrolls the given position into the viewport, using the minimal amount of * scrolling necessary to being the coordinate into view. * * @param {!goog.math.Coordinate} position The position to scroll into view. * @param {!Window=} opt_win Window to apply position to. Defaults to * bot.getWindow(). */ bot.window.scrollIntoView = function (position, opt_win) { var win = opt_win || bot.getWindow(); var viewport = goog.dom.getViewportSize(win); var scroll = bot.window.getScroll(win); // Scroll the minimal amount to bring the position into view. var targetScroll = new goog.math.Coordinate( newScrollDim(position.x, scroll.x, viewport.width), newScrollDim(position.y, scroll.y, viewport.height)); if (!goog.math.Coordinate.equals(targetScroll, scroll)) { bot.window.setScroll(targetScroll, win); } // It is difficult to determine the size of the web page in some browsers. // We check if the scrolling we intended to do really happened. If not we // assume that the target location is not on the web page. if (!goog.math.Coordinate.equals(targetScroll, bot.window.getScroll(win))) { throw new bot.Error(bot.ErrorCode.MOVE_TARGET_OUT_OF_BOUNDS, 'The target scroll location ' + targetScroll + ' is not on the page.'); } function newScrollDim(positionDim, scrollDim, viewportDim) { if (positionDim < scrollDim) { return positionDim; } else if (positionDim >= scrollDim + viewportDim) { return positionDim - viewportDim + 1; } else { return scrollDim; } } }; /** * @return {number} The current window orientation degrees. * window. * @private */ bot.window.getCurrentOrientationDegrees_ = function () { var win = bot.getWindow(); if (win.orientation === undefined) { // If window.orientation is not defined, assume a default orientation of 0. // A value of 0 indicates a portrait orientation except for android tablets // where 0 indicates a landscape orientation. win.orientation = 0; } return win.orientation; }; /** * Changes window orientation. * * @param {!bot.window.Orientation} orientation The new orientation of the * window. */ bot.window.changeOrientation = function (orientation) { var win = bot.getWindow(); var currentOrientationDegrees = bot.window.getCurrentOrientationDegrees_(); var newOrientationDegrees = bot.window.getOrientationDegrees_(orientation); if (currentOrientationDegrees == newOrientationDegrees || newOrientationDegrees === undefined) { return; } // If possible, try to override the window's orientation value. // On some older version of Android, it's not possible to change // the window's orientation value. if (Object.getOwnPropertyDescriptor && Object.defineProperty) { var descriptor = Object.getOwnPropertyDescriptor(win, 'orientation'); if (descriptor && descriptor.configurable) { Object.defineProperty(win, 'orientation', { configurable: true, get: function () { return newOrientationDegrees; } }); } } bot.events.fire(win, bot.events.EventType.ORIENTATIONCHANGE); // Change the window size to reflect the new orientation. if (Math.abs(currentOrientationDegrees - newOrientationDegrees) % 180 != 0) { var size = bot.window.getSize(); var shorter = size.getShortest(); var longer = size.getLongest(); if (orientation == bot.window.Orientation.PORTRAIT || orientation == bot.window.Orientation.PORTRAIT_SECONDARY) { bot.window.setSize(new goog.math.Size(shorter, longer)); } else { bot.window.setSize(new goog.math.Size(longer, shorter)); } } };