| // 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 = goog.isDef(opt_numPages) ? 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 (!goog.isDef(win.orientation)) { |
| // 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 || |
| !goog.isDef(newOrientationDegrees)) { |
| 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)); |
| } |
| } |
| }; |