// Licensed to the Software Freedom Conservancy (SFC) under one1// or more contributor license agreements. See the NOTICE file2// distributed with this work for additional information3// regarding copyright ownership. The SFC licenses this file4// to you under the Apache License, Version 2.0 (the5// "License"); you may not use this file except in compliance6// with the License. You may obtain a copy of the License at7//8// http://www.apache.org/licenses/LICENSE-2.09//10// Unless required by applicable law or agreed to in writing,11// software distributed under the License is distributed on an12// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY13// KIND, either express or implied. See the License for the14// specific language governing permissions and limitations15// under the License.1617/**18* @fileoverview Atoms for simulating user actions against the DOM.19* The bot.action namespace is required since these atoms would otherwise form a20* circular dependency between bot.dom and bot.events.21*22*/2324goog.provide('bot.action');2526goog.require('bot');27goog.require('bot.Device');28goog.require('bot.Error');29goog.require('bot.ErrorCode');30goog.require('bot.Keyboard');31goog.require('bot.Mouse');32goog.require('bot.Touchscreen');33goog.require('bot.dom');34goog.require('bot.events');35goog.require('bot.events.EventType');36goog.require('goog.array');37goog.require('goog.dom.TagName');38goog.require('goog.math.Coordinate');39goog.require('goog.math.Vec2');40goog.require('goog.style');414243/**44* Throws an exception if an element is not shown to the user, ignoring its45* opacity.4647*48* @param {!Element} element The element to check.49* @see bot.dom.isShown.50* @private51*/52bot.action.checkShown_ = function (element) {53if (!bot.dom.isShown(element, /*ignoreOpacity=*/true)) {54throw new bot.Error(bot.ErrorCode.ELEMENT_NOT_VISIBLE,55'Element is not currently visible and may not be manipulated');56}57};585960/**61* Throws an exception if the given element cannot be interacted with.62*63* @param {!Element} element The element to check.64* @throws {bot.Error} If the element cannot be interacted with.65* @see bot.dom.isInteractable.66* @private67*/68bot.action.checkInteractable_ = function (element) {69if (!bot.dom.isInteractable(element)) {70throw new bot.Error(bot.ErrorCode.INVALID_ELEMENT_STATE,71'Element is not currently interactable and may not be manipulated');7273}74};757677/**78* Clears the given `element` if it is a editable text field.79*80* @param {!Element} element The element to clear.81* @throws {bot.Error} If the element is not an editable text field.82*/83bot.action.clear = function (element) {84bot.action.checkInteractable_(element);85if (!bot.dom.isEditable(element)) {86throw new bot.Error(bot.ErrorCode.INVALID_ELEMENT_STATE,87'Element must be user-editable in order to clear it.');88}8990if (element.value) {91bot.action.LegacyDevice_.focusOnElement(element);92if (goog.userAgent.IE && bot.dom.isInputType(element, 'range')) {93var min = element.min ? element.min : 0;94var max = element.max ? element.max : 100;95element.value = (max < min) ? min : min + (max - min) / 2;96} else {97element.value = '';98}99bot.events.fire(element, bot.events.EventType.CHANGE);100if (goog.userAgent.IE) {101bot.events.fire(element, bot.events.EventType.BLUR);102}103var body = bot.getDocument().body;104if (body) {105bot.action.LegacyDevice_.focusOnElement(body);106} else {107throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,108'Cannot unfocus element after clearing.');109}110} else if (bot.dom.isElement(element, goog.dom.TagName.INPUT) &&111(element.getAttribute('type') && element.getAttribute('type').toLowerCase() == "number")) {112// number input fields that have invalid inputs113// report their value as empty string with no way to tell if there is a114// current value or not115bot.action.LegacyDevice_.focusOnElement(element);116element.value = '';117} else if (bot.dom.isContentEditable(element)) {118// A single space is required, if you put empty string here you'll not be119// able to interact with this element anymore in Firefox.120bot.action.LegacyDevice_.focusOnElement(element);121if (goog.userAgent.GECKO) {122element.innerHTML = ' ';123} else {124element.textContent = '';125}126var body = bot.getDocument().body;127if (body) {128bot.action.LegacyDevice_.focusOnElement(body);129} else {130throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,131'Cannot unfocus element after clearing.');132}133// contentEditable does not generate onchange event.134}135};136137138/**139* Focuses on the given element if it is not already the active element.140*141* @param {!Element} element The element to focus on.142*/143bot.action.focusOnElement = function (element) {144bot.action.checkInteractable_(element);145bot.action.LegacyDevice_.focusOnElement(element);146};147148149/**150* Types keys on the given `element` with a virtual keyboard.151*152* <p>Callers can pass in a string, a key in bot.Keyboard.Key, or an array153* of strings or keys. If a modifier key is provided, it is pressed but not154* released, until it is either is listed again or the function ends.155*156* <p>Example:157* bot.keys.type(element, ['ab', bot.Keyboard.Key.LEFT,158* bot.Keyboard.Key.SHIFT, 'cd']);159*160* @param {!Element} element The element receiving the event.161* @param {(string|!bot.Keyboard.Key|!Array.<(string|!bot.Keyboard.Key)>)}162* values Value or values to type on the element.163* @param {bot.Keyboard=} opt_keyboard Keyboard to use; if not provided,164* constructs one.165* @param {boolean=} opt_persistModifiers Whether modifier keys should remain166* pressed when this function ends.167* @throws {bot.Error} If the element cannot be interacted with.168*/169bot.action.type = function (170element, values, opt_keyboard, opt_persistModifiers) {171// If the element has already been brought into focus somehow, typing is172// always allowed to proceed. Otherwise, we require the element be in an173// "interactable" state. For example, an element that is hidden by overflow174// can be typed on, so long as the user first tabs to it or the app calls175// focus() on the element first.176if (element != bot.dom.getActiveElement(element)) {177bot.action.checkInteractable_(element);178bot.action.scrollIntoView(element);179}180181var keyboard = opt_keyboard || new bot.Keyboard();182keyboard.moveCursor(element);183184function typeValue(value) {185if (goog.isString(value)) {186goog.array.forEach(value.split(''), function (ch) {187var keyShiftPair = bot.Keyboard.Key.fromChar(ch);188var shiftIsPressed = keyboard.isPressed(bot.Keyboard.Keys.SHIFT);189if (keyShiftPair.shift && !shiftIsPressed) {190keyboard.pressKey(bot.Keyboard.Keys.SHIFT);191}192keyboard.pressKey(keyShiftPair.key);193keyboard.releaseKey(keyShiftPair.key);194if (keyShiftPair.shift && !shiftIsPressed) {195keyboard.releaseKey(bot.Keyboard.Keys.SHIFT);196}197});198} else if (goog.array.contains(bot.Keyboard.MODIFIERS, value)) {199if (keyboard.isPressed(/** @type {!bot.Keyboard.Key} */(value))) {200keyboard.releaseKey(value);201} else {202keyboard.pressKey(value);203}204} else {205keyboard.pressKey(value);206keyboard.releaseKey(value);207}208}209210// mobile safari (iPhone / iPad). one cannot 'type' in a date field211// chrome implements this, but desktop Safari doesn't, what's webkit again?212if ((!(goog.userAgent.product.SAFARI && !goog.userAgent.MOBILE)) &&213goog.userAgent.WEBKIT && element.type == 'date') {214var val = goog.isArray(values) ? values = values.join("") : values;215var datePattern = /\d{4}-\d{2}-\d{2}/;216if (val.match(datePattern)) {217// The following events get fired on iOS first218if (goog.userAgent.MOBILE && goog.userAgent.product.SAFARI) {219bot.events.fire(element, bot.events.EventType.TOUCHSTART);220bot.events.fire(element, bot.events.EventType.TOUCHEND);221}222bot.events.fire(element, bot.events.EventType.FOCUS);223element.value = val.match(datePattern)[0];224bot.events.fire(element, bot.events.EventType.CHANGE);225bot.events.fire(element, bot.events.EventType.BLUR);226return;227}228}229230if (goog.isArray(values)) {231goog.array.forEach(values, typeValue);232} else {233typeValue(values);234}235236if (!opt_persistModifiers) {237// Release all the modifier keys.238goog.array.forEach(bot.Keyboard.MODIFIERS, function (key) {239if (keyboard.isPressed(key)) {240keyboard.releaseKey(key);241}242});243}244};245246247/**248* Submits the form containing the given `element`.249*250* <p>Note this function submits the form, but does not simulate user input251* (a click or key press).252*253* @param {!Element} element The element to submit.254* @deprecated Click on a submit button or type ENTER in a text box instead.255*/256bot.action.submit = function (element) {257var form = bot.action.LegacyDevice_.findAncestorForm(element);258if (!form) {259throw new bot.Error(bot.ErrorCode.NO_SUCH_ELEMENT,260'Element was not in a form, so could not submit.');261}262bot.action.LegacyDevice_.submitForm(element, form);263};264265266/**267* Moves the mouse over the given `element` with a virtual mouse.268*269* @param {!Element} element The element to click.270* @param {goog.math.Coordinate=} opt_coords Mouse position relative to the271* element.272* @param {bot.Mouse=} opt_mouse Mouse to use; if not provided, constructs one.273* @throws {bot.Error} If the element cannot be interacted with.274*/275bot.action.moveMouse = function (element, opt_coords, opt_mouse) {276var coords = bot.action.prepareToInteractWith_(element, opt_coords);277var mouse = opt_mouse || new bot.Mouse();278mouse.move(element, coords);279};280281282/**283* Clicks on the given `element` with a virtual mouse.284*285* @param {!Element} element The element to click.286* @param {goog.math.Coordinate=} opt_coords Mouse position relative to the287* element.288* @param {bot.Mouse=} opt_mouse Mouse to use; if not provided, constructs one.289* @param {boolean=} opt_force Whether the release event should be fired even if the290* element is not interactable.291* @throws {bot.Error} If the element cannot be interacted with.292*/293bot.action.click = function (element, opt_coords, opt_mouse, opt_force) {294var coords = bot.action.prepareToInteractWith_(element, opt_coords);295var mouse = opt_mouse || new bot.Mouse();296mouse.move(element, coords);297mouse.pressButton(bot.Mouse.Button.LEFT);298mouse.releaseButton(opt_force);299};300301302/**303* Right-clicks on the given `element` with a virtual mouse.304*305* @param {!Element} element The element to click.306* @param {goog.math.Coordinate=} opt_coords Mouse position relative to the307* element.308* @param {bot.Mouse=} opt_mouse Mouse to use; if not provided, constructs one.309* @throws {bot.Error} If the element cannot be interacted with.310*/311bot.action.rightClick = function (element, opt_coords, opt_mouse) {312var coords = bot.action.prepareToInteractWith_(element, opt_coords);313var mouse = opt_mouse || new bot.Mouse();314mouse.move(element, coords);315mouse.pressButton(bot.Mouse.Button.RIGHT);316mouse.releaseButton();317};318319320/**321* Double-clicks on the given `element` with a virtual mouse.322*323* @param {!Element} element The element to click.324* @param {goog.math.Coordinate=} opt_coords Mouse position relative to the325* element.326* @param {bot.Mouse=} opt_mouse Mouse to use; if not provided, constructs one.327* @throws {bot.Error} If the element cannot be interacted with.328*/329bot.action.doubleClick = function (element, opt_coords, opt_mouse) {330var coords = bot.action.prepareToInteractWith_(element, opt_coords);331var mouse = opt_mouse || new bot.Mouse();332mouse.move(element, coords);333mouse.pressButton(bot.Mouse.Button.LEFT);334mouse.releaseButton();335mouse.pressButton(bot.Mouse.Button.LEFT);336mouse.releaseButton();337};338339340/**341* Double-clicks on the given `element` with a virtual mouse.342*343* @param {!Element} element The element to click.344* @param {goog.math.Coordinate=} opt_coords Mouse position relative to the345* element.346* @param {bot.Mouse=} opt_mouse Mouse to use; if not provided, constructs one.347* @throws {bot.Error} If the element cannot be interacted with.348*/349bot.action.doubleClick2 = function (element, opt_coords, opt_mouse) {350var coords = bot.action.prepareToInteractWith_(element, opt_coords);351var mouse = opt_mouse || new bot.Mouse();352mouse.move(element, coords);353mouse.pressButton(bot.Mouse.Button.LEFT, 2);354mouse.releaseButton(true, 2);355};356357358/**359* Scrolls the mouse wheel on the given `element` with a virtual mouse.360*361* @param {!Element} element The element to scroll the mouse wheel on.362* @param {number} ticks Number of ticks to scroll the mouse wheel; a positive363* number scrolls down and a negative scrolls up.364* @param {goog.math.Coordinate=} opt_coords Mouse position relative to the365* element.366* @param {bot.Mouse=} opt_mouse Mouse to use; if not provided, constructs one.367* @throws {bot.Error} If the element cannot be interacted with.368*/369bot.action.scrollMouse = function (element, ticks, opt_coords, opt_mouse) {370var coords = bot.action.prepareToInteractWith_(element, opt_coords);371var mouse = opt_mouse || new bot.Mouse();372mouse.move(element, coords);373mouse.scroll(ticks);374};375376377/**378* Drags the given `element` by (dx, dy) with a virtual mouse.379*380* @param {!Element} element The element to drag.381* @param {number} dx Increment in x coordinate.382* @param {number} dy Increment in y coordinate.383* @param {number=} opt_steps The number of steps that should occur as part of384* the drag, default is 2.385* @param {goog.math.Coordinate=} opt_coords Drag start position relative to the386* element.387* @param {bot.Mouse=} opt_mouse Mouse to use; if not provided, constructs one.388* @throws {bot.Error} If the element cannot be interacted with.389*/390bot.action.drag = function (element, dx, dy, opt_steps, opt_coords, opt_mouse) {391var coords = bot.action.prepareToInteractWith_(element, opt_coords);392var initRect = bot.dom.getClientRect(element);393var mouse = opt_mouse || new bot.Mouse();394mouse.move(element, coords);395mouse.pressButton(bot.Mouse.Button.LEFT);396var steps = goog.isDef(opt_steps) ? opt_steps : 2;397if (steps < 1) {398throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,399'There must be at least one step as part of a drag.');400}401for (var i = 1; i <= steps; i++) {402moveTo(Math.floor(i * dx / steps), Math.floor(i * dy / steps));403}404mouse.releaseButton();405406function moveTo(x, y) {407var currRect = bot.dom.getClientRect(element);408var newPos = new goog.math.Coordinate(409coords.x + initRect.left + x - currRect.left,410coords.y + initRect.top + y - currRect.top);411mouse.move(element, newPos);412}413};414415416/**417* Taps on the given `element` with a virtual touch screen.418*419* @param {!Element} element The element to tap.420* @param {goog.math.Coordinate=} opt_coords Finger position relative to the421* target.422* @param {bot.Touchscreen=} opt_touchscreen Touchscreen to use; if not423* provided, constructs one.424* @throws {bot.Error} If the element cannot be interacted with.425*/426bot.action.tap = function (element, opt_coords, opt_touchscreen) {427var coords = bot.action.prepareToInteractWith_(element, opt_coords);428var touchscreen = opt_touchscreen || new bot.Touchscreen();429touchscreen.move(element, coords);430touchscreen.press();431touchscreen.release();432};433434435/**436* Swipes the given `element` by (dx, dy) with a virtual touch screen.437*438* @param {!Element} element The element to swipe.439* @param {number} dx Increment in x coordinate.440* @param {number} dy Increment in y coordinate.441* @param {number=} opt_steps The number of steps that should occurs as part of442* the swipe, default is 2.443* @param {goog.math.Coordinate=} opt_coords Swipe start position relative to444* the element.445* @param {bot.Touchscreen=} opt_touchscreen Touchscreen to use; if not446* provided, constructs one.447* @throws {bot.Error} If the element cannot be interacted with.448*/449bot.action.swipe = function (element, dx, dy, opt_steps, opt_coords,450opt_touchscreen) {451var coords = bot.action.prepareToInteractWith_(element, opt_coords);452var touchscreen = opt_touchscreen || new bot.Touchscreen();453var initRect = bot.dom.getClientRect(element);454touchscreen.move(element, coords);455touchscreen.press();456var steps = goog.isDef(opt_steps) ? opt_steps : 2;457if (steps < 1) {458throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,459'There must be at least one step as part of a swipe.');460}461for (var i = 1; i <= steps; i++) {462moveTo(Math.floor(i * dx / steps), Math.floor(i * dy / steps));463}464touchscreen.release();465466function moveTo(x, y) {467var currRect = bot.dom.getClientRect(element);468var newPos = new goog.math.Coordinate(469coords.x + initRect.left + x - currRect.left,470coords.y + initRect.top + y - currRect.top);471touchscreen.move(element, newPos);472}473};474475476/**477* Pinches the given `element` by the given distance with a virtual touch478* screen. A positive distance moves two fingers inward toward each and a479* negative distances spreads them outward. The optional coordinate is the point480* the fingers move towards (for positive distances) or away from (for negative481* distances); and if not provided, defaults to the center of the element.482*483* @param {!Element} element The element to pinch.484* @param {number} distance The distance by which to pinch the element.485* @param {goog.math.Coordinate=} opt_coords Position relative to the element486* at the center of the pinch.487* @param {bot.Touchscreen=} opt_touchscreen Touchscreen to use; if not488* provided, constructs one.489* @throws {bot.Error} If the element cannot be interacted with.490*/491bot.action.pinch = function (element, distance, opt_coords, opt_touchscreen) {492if (distance == 0) {493throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,494'Cannot pinch by a distance of zero.');495}496function startSoThatEndsAtMax(offsetVec) {497if (distance < 0) {498var magnitude = offsetVec.magnitude();499offsetVec.scale(magnitude ? (magnitude + distance) / magnitude : 0);500}501}502var halfDistance = distance / 2;503function scaleByHalfDistance(offsetVec) {504var magnitude = offsetVec.magnitude();505offsetVec.scale(magnitude ? (magnitude - halfDistance) / magnitude : 0);506}507bot.action.multiTouchAction_(element,508startSoThatEndsAtMax,509scaleByHalfDistance,510opt_coords,511opt_touchscreen);512};513514515/**516* Rotates the given `element` by the given angle with a virtual touch517* screen. A positive angle moves two fingers clockwise and a negative angle518* moves them counter-clockwise. The optional coordinate is the point to519* rotate around; and if not provided, defaults to the center of the element.520*521* @param {!Element} element The element to rotate.522* @param {number} angle The angle by which to rotate the element.523* @param {goog.math.Coordinate=} opt_coords Position relative to the element524* at the center of the rotation.525* @param {bot.Touchscreen=} opt_touchscreen Touchscreen to use; if not526* provided, constructs one.527* @throws {bot.Error} If the element cannot be interacted with.528*/529bot.action.rotate = function (element, angle, opt_coords, opt_touchscreen) {530if (angle == 0) {531throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,532'Cannot rotate by an angle of zero.');533}534function startHalfwayToMax(offsetVec) {535offsetVec.scale(0.5);536}537var halfRadians = Math.PI * (angle / 180) / 2;538function rotateByHalfAngle(offsetVec) {539offsetVec.rotate(halfRadians);540}541bot.action.multiTouchAction_(element,542startHalfwayToMax,543rotateByHalfAngle,544opt_coords,545opt_touchscreen);546};547548549/**550* Performs a multi-touch action with two fingers on the given element. This551* helper function works by manipulating an "offsetVector", which is the vector552* away from the center of the interaction at which the fingers are positioned.553* It computes the maximum offset vector and passes it to transformStart to554* find the starting position of the fingers; it then passes it to transformHalf555* twice to find the midpoint and final position of the fingers.556*557* @param {!Element} element Element to interact with.558* @param {function(goog.math.Vec2)} transformStart Function to transform the559* maximum offset vector to the starting offset vector.560* @param {function(goog.math.Vec2)} transformHalf Function to transform the561* offset vector halfway to its destination.562* @param {goog.math.Coordinate=} opt_coords Position relative to the element563* at the center of the pinch.564* @param {bot.Touchscreen=} opt_touchscreen Touchscreen to use; if not565* provided, constructs one.566* @private567*/568bot.action.multiTouchAction_ = function (element, transformStart, transformHalf,569opt_coords, opt_touchscreen) {570var center = bot.action.prepareToInteractWith_(element, opt_coords);571var size = bot.action.getInteractableSize(element);572var offsetVec = new goog.math.Vec2(573Math.min(center.x, size.width - center.x),574Math.min(center.y, size.height - center.y));575576var touchScreen = opt_touchscreen || new bot.Touchscreen();577transformStart(offsetVec);578var start1 = goog.math.Vec2.sum(center, offsetVec);579var start2 = goog.math.Vec2.difference(center, offsetVec);580touchScreen.move(element, start1, start2);581touchScreen.press(/*Two Finger Press*/ true);582583var initRect = bot.dom.getClientRect(element);584transformHalf(offsetVec);585var mid1 = goog.math.Vec2.sum(center, offsetVec);586var mid2 = goog.math.Vec2.difference(center, offsetVec);587touchScreen.move(element, mid1, mid2);588589var midRect = bot.dom.getClientRect(element);590var movedVec = goog.math.Vec2.difference(591new goog.math.Vec2(midRect.left, midRect.top),592new goog.math.Vec2(initRect.left, initRect.top));593transformHalf(offsetVec);594var end1 = goog.math.Vec2.sum(center, offsetVec).subtract(movedVec);595var end2 = goog.math.Vec2.difference(center, offsetVec).subtract(movedVec);596touchScreen.move(element, end1, end2);597touchScreen.release();598};599600601/**602* Prepares to interact with the given `element`. It checks if the the603* element is shown, scrolls the element into view, and returns the coordinates604* of the interaction, which if not provided, is the center of the element.605*606* @param {!Element} element The element to be interacted with.607* @param {goog.math.Coordinate=} opt_coords Position relative to the target.608* @return {!goog.math.Vec2} Coordinates at the center of the interaction.609* @throws {bot.Error} If the element cannot be interacted with.610* @private611*/612bot.action.prepareToInteractWith_ = function (element, opt_coords) {613bot.action.checkShown_(element);614bot.action.scrollIntoView(element, opt_coords || undefined);615616// NOTE: Ideally, we would check that any provided coordinates fall617// within the bounds of the element, but this has proven difficult, because:618// (1) Browsers sometimes lie about the true size of elements, e.g. when text619// overflows the bounding box of an element, browsers report the size of the620// box even though the true area that can be interacted with is larger; and621// (2) Elements with children styled as position:absolute will often not have622// a bounding box that surrounds all of their children, but it is useful for623// the user to be able to interact with this parent element as if it does.624if (opt_coords) {625return goog.math.Vec2.fromCoordinate(opt_coords);626} else {627var size = bot.action.getInteractableSize(element);628return new goog.math.Vec2(size.width / 2, size.height / 2);629}630};631632633/**634* Returns the interactable size of an element.635*636* @param {!Element} elem Element.637* @return {!goog.math.Size} size Size of the element.638*/639bot.action.getInteractableSize = function (elem) {640var size = goog.style.getSize(elem);641return ((size.width > 0 && size.height > 0) || !elem.offsetParent) ? size :642bot.action.getInteractableSize(elem.offsetParent);643};644645646647/**648* A Device that is intended to allows access to protected members of the649* Device superclass. A singleton.650*651* @constructor652* @extends {bot.Device}653* @private654*/655bot.action.LegacyDevice_ = function () {656goog.base(this);657};658goog.inherits(bot.action.LegacyDevice_, bot.Device);659goog.addSingletonGetter(bot.action.LegacyDevice_);660661662/**663* Focuses on the given element. See {@link bot.device.focusOnElement}.664* @param {!Element} element The element to focus on.665* @return {boolean} True if element.focus() was called on the element.666*/667bot.action.LegacyDevice_.focusOnElement = function (element) {668var instance = bot.action.LegacyDevice_.getInstance();669instance.setElement(element);670return instance.focusOnElement();671};672673674/**675* Submit the form for the element. See {@link bot.device.submit}.676* @param {!Element} element The element to submit a form on.677* @param {!Element} form The form to submit.678*/679bot.action.LegacyDevice_.submitForm = function (element, form) {680var instance = bot.action.LegacyDevice_.getInstance();681instance.setElement(element);682instance.submitForm(form);683};684685686/**687* Find FORM element that is an ancestor of the passed in element. See688* {@link bot.device.findAncestorForm}.689* @param {!Element} element The element to find an ancestor form.690* @return {Element} form The ancestor form, or null if none.691*/692bot.action.LegacyDevice_.findAncestorForm = function (element) {693return bot.Device.findAncestorForm(element);694};695696697/**698* Scrolls the given `element` in to the current viewport. Aims to do the699* minimum scrolling necessary, but prefers too much scrolling to too little.700*701* If an optional coordinate or rectangle region is provided, scrolls that702* region relative to the element into view. A coordinate is treated as a 1x1703* region whose top-left corner is positioned at that coordinate.704*705* @param {!Element} element The element to scroll in to view.706* @param {!(goog.math.Coordinate|goog.math.Rect)=} opt_region707* Region relative to the top-left corner of the element.708* @return {boolean} Whether the element is in view after scrolling.709*/710bot.action.scrollIntoView = function (element, opt_region) {711// If the element is already in view, return true; if hidden, return false.712var overflow = bot.dom.getOverflowState(element, opt_region);713if (overflow != bot.dom.OverflowState.SCROLL) {714return overflow == bot.dom.OverflowState.NONE;715}716717// Some elements may not have a scrollIntoView function - for example,718// elements under an SVG element. Call those only if they exist.719if (element.scrollIntoView) {720element.scrollIntoView();721if (bot.dom.OverflowState.NONE ==722bot.dom.getOverflowState(element, opt_region)) {723return true;724}725}726727// There may have not been a scrollIntoView function, or the specified728// coordinate may not be in view, so scroll "manually".729var region = bot.dom.getClientRegion(element, opt_region);730for (var container = bot.dom.getParentElement(element);731container;732container = bot.dom.getParentElement(container)) {733scrollClientRegionIntoContainerView(container);734}735return bot.dom.OverflowState.NONE ==736bot.dom.getOverflowState(element, opt_region);737738function scrollClientRegionIntoContainerView(container) {739// Based largely from goog.style.scrollIntoContainerView.740var containerRect = bot.dom.getClientRect(container);741var containerBorder = goog.style.getBorderBox(container);742743// Relative position of the region to the container's content box.744var relX = region.left - containerRect.left - containerBorder.left;745var relY = region.top - containerRect.top - containerBorder.top;746747// How much the region can move in the container. Use the container's748// clientWidth/Height, not containerRect, to account for the scrollbar.749var spaceX = container.clientWidth + region.left - region.right;750var spaceY = container.clientHeight + region.top - region.bottom;751752// Scroll the element into view of the container.753container.scrollLeft += Math.min(relX, Math.max(relX - spaceX, 0));754container.scrollTop += Math.min(relY, Math.max(relY - spaceY, 0));755}756};757758759