blob: 2e280036d47cc3abac6716c1482fffb1e259282e [file] [log] [blame] [edit]
// 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));
}
}
};