// 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('goog.array');36goog.require('goog.dom.TagName');37goog.require('goog.math.Coordinate');38goog.require('goog.math.Vec2');39goog.require('goog.style');40goog.require('goog.userAgent');41goog.require('goog.userAgent.product');42goog.require('goog.utils');434445/**46* Throws an exception if an element is not shown to the user, ignoring its47* opacity.4849*50* @param {!Element} element The element to check.51* @see bot.dom.isShown.52* @private53*/54bot.action.checkShown_ = function (element) {55if (!bot.dom.isShown(element, /*ignoreOpacity=*/true)) {56throw new bot.Error(bot.ErrorCode.ELEMENT_NOT_VISIBLE,57'Element is not currently visible and may not be manipulated');58}59};606162/**63* Throws an exception if the given element cannot be interacted with.64*65* @param {!Element} element The element to check.66* @throws {bot.Error} If the element cannot be interacted with.67* @see bot.dom.isInteractable.68* @private69*/70bot.action.checkInteractable_ = function (element) {71if (!bot.dom.isInteractable(element)) {72throw new bot.Error(bot.ErrorCode.INVALID_ELEMENT_STATE,73'Element is not currently interactable and may not be manipulated');7475}76};777879/**80* Clears the given `element` if it is a editable text field.81*82* @param {!Element} element The element to clear.83* @throws {bot.Error} If the element is not an editable text field.84*/85bot.action.clear = function (element) {86bot.action.checkInteractable_(element);87if (!bot.dom.isEditable(element)) {88throw new bot.Error(bot.ErrorCode.INVALID_ELEMENT_STATE,89'Element must be user-editable in order to clear it.');90}9192if (element.value) {93bot.action.LegacyDevice_.focusOnElement(element);94if (goog.userAgent.IE && bot.dom.isInputType(element, 'range')) {95var min = element.min ? element.min : 0;96var max = element.max ? element.max : 100;97element.value = (max < min) ? min : min + (max - min) / 2;98} else {99element.value = '';100}101bot.events.fire(element, bot.events.EventType.CHANGE);102if (goog.userAgent.IE) {103bot.events.fire(element, bot.events.EventType.BLUR);104}105var body = bot.getDocument().body;106if (body) {107bot.action.LegacyDevice_.focusOnElement(body);108} else {109throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,110'Cannot unfocus element after clearing.');111}112} else if (bot.dom.isElement(element, goog.dom.TagName.INPUT) &&113(element.getAttribute('type') && element.getAttribute('type').toLowerCase() == "number")) {114// number input fields that have invalid inputs115// report their value as empty string with no way to tell if there is a116// current value or not117bot.action.LegacyDevice_.focusOnElement(element);118element.value = '';119} else if (bot.dom.isContentEditable(element)) {120// A single space is required, if you put empty string here you'll not be121// able to interact with this element anymore in Firefox.122bot.action.LegacyDevice_.focusOnElement(element);123if (goog.userAgent.GECKO) {124element.textContent = ' ';125} else {126element.textContent = '';127}128var body = bot.getDocument().body;129if (body) {130bot.action.LegacyDevice_.focusOnElement(body);131} else {132throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,133'Cannot unfocus element after clearing.');134}135// contentEditable does not generate onchange event.136}137};138139140/**141* Focuses on the given element if it is not already the active element.142*143* @param {!Element} element The element to focus on.144*/145bot.action.focusOnElement = function (element) {146bot.action.checkInteractable_(element);147bot.action.LegacyDevice_.focusOnElement(element);148};149150151/**152* Types keys on the given `element` with a virtual keyboard.153*154* <p>Callers can pass in a string, a key in bot.Keyboard.Key, or an array155* of strings or keys. If a modifier key is provided, it is pressed but not156* released, until it is either is listed again or the function ends.157*158* <p>Example:159* bot.keys.type(element, ['ab', bot.Keyboard.Key.LEFT,160* bot.Keyboard.Key.SHIFT, 'cd']);161*162* @param {!Element} element The element receiving the event.163* @param {(string|!bot.Keyboard.Key|!Array.<(string|!bot.Keyboard.Key)>)}164* values Value or values to type on the element.165* @param {bot.Keyboard=} opt_keyboard Keyboard to use; if not provided,166* constructs one.167* @param {boolean=} opt_persistModifiers Whether modifier keys should remain168* pressed when this function ends.169* @throws {bot.Error} If the element cannot be interacted with.170*/171bot.action.type = function (172element, values, opt_keyboard, opt_persistModifiers) {173// If the element has already been brought into focus somehow, typing is174// always allowed to proceed. Otherwise, we require the element be in an175// "interactable" state. For example, an element that is hidden by overflow176// can be typed on, so long as the user first tabs to it or the app calls177// focus() on the element first.178if (element != bot.dom.getActiveElement(element)) {179bot.action.checkInteractable_(element);180bot.action.scrollIntoView(element);181}182183var keyboard = opt_keyboard || new bot.Keyboard();184keyboard.moveCursor(element);185186function typeValue(value) {187if (typeof value === 'string') {188goog.array.forEach(value.split(''), function (ch) {189var keyShiftPair = bot.Keyboard.Key.fromChar(ch);190var shiftIsPressed = keyboard.isPressed(bot.Keyboard.Keys.SHIFT);191if (keyShiftPair.shift && !shiftIsPressed) {192keyboard.pressKey(bot.Keyboard.Keys.SHIFT);193}194keyboard.pressKey(keyShiftPair.key);195keyboard.releaseKey(keyShiftPair.key);196if (keyShiftPair.shift && !shiftIsPressed) {197keyboard.releaseKey(bot.Keyboard.Keys.SHIFT);198}199});200} else if (goog.array.contains(bot.Keyboard.MODIFIERS, value)) {201if (keyboard.isPressed(/** @type {!bot.Keyboard.Key} */(value))) {202keyboard.releaseKey(value);203} else {204keyboard.pressKey(value);205}206} else {207keyboard.pressKey(value);208keyboard.releaseKey(value);209}210}211212// mobile safari (iPhone / iPad). one cannot 'type' in a date field213// chrome implements this, but desktop Safari doesn't, what's webkit again?214if ((!(goog.userAgent.product.SAFARI && !goog.userAgent.MOBILE)) &&215goog.userAgent.WEBKIT && element.type == 'date') {216var val = Array.isArray(values) ? values = values.join("") : values;217var datePattern = /\d{4}-\d{2}-\d{2}/;218if (val.match(datePattern)) {219// The following events get fired on iOS first220if (goog.userAgent.MOBILE && goog.userAgent.product.SAFARI) {221bot.events.fire(element, bot.events.EventType.TOUCHSTART);222bot.events.fire(element, bot.events.EventType.TOUCHEND);223}224bot.events.fire(element, bot.events.EventType.FOCUS);225element.value = val.match(datePattern)[0];226bot.events.fire(element, bot.events.EventType.CHANGE);227bot.events.fire(element, bot.events.EventType.BLUR);228return;229}230}231232if (Array.isArray(values)) {233goog.array.forEach(values, typeValue);234} else {235typeValue(values);236}237238if (!opt_persistModifiers) {239// Release all the modifier keys.240goog.array.forEach(bot.Keyboard.MODIFIERS, function (key) {241if (keyboard.isPressed(key)) {242keyboard.releaseKey(key);243}244});245}246};247248249/**250* Submits the form containing the given `element`.251*252* <p>Note this function submits the form, but does not simulate user input253* (a click or key press).254*255* @param {!Element} element The element to submit.256* @deprecated Click on a submit button or type ENTER in a text box instead.257*/258bot.action.submit = function (element) {259var form = bot.action.LegacyDevice_.findAncestorForm(element);260if (!form) {261throw new bot.Error(bot.ErrorCode.NO_SUCH_ELEMENT,262'Element was not in a form, so could not submit.');263}264bot.action.LegacyDevice_.submitForm(element, form);265};266267268/**269* Moves the mouse over the given `element` with a virtual mouse.270*271* @param {!Element} element The element to click.272* @param {goog.math.Coordinate=} opt_coords Mouse position relative to the273* element.274* @param {bot.Mouse=} opt_mouse Mouse to use; if not provided, constructs one.275* @throws {bot.Error} If the element cannot be interacted with.276*/277bot.action.moveMouse = function (element, opt_coords, opt_mouse) {278var coords = bot.action.prepareToInteractWith_(element, opt_coords);279var mouse = opt_mouse || new bot.Mouse();280mouse.move(element, coords);281};282283284/**285* Clicks on the given `element` with a virtual mouse.286*287* @param {!Element} element The element to click.288* @param {goog.math.Coordinate=} opt_coords Mouse position relative to the289* element.290* @param {bot.Mouse=} opt_mouse Mouse to use; if not provided, constructs one.291* @param {boolean=} opt_force Whether the release event should be fired even if the292* element is not interactable.293* @throws {bot.Error} If the element cannot be interacted with.294*/295bot.action.click = function (element, opt_coords, opt_mouse, opt_force) {296var coords = bot.action.prepareToInteractWith_(element, opt_coords);297var mouse = opt_mouse || new bot.Mouse();298mouse.move(element, coords);299mouse.pressButton(bot.Mouse.Button.LEFT);300mouse.releaseButton(opt_force);301};302303304/**305* Right-clicks on the given `element` with a virtual mouse.306*307* @param {!Element} element The element to click.308* @param {goog.math.Coordinate=} opt_coords Mouse position relative to the309* element.310* @param {bot.Mouse=} opt_mouse Mouse to use; if not provided, constructs one.311* @throws {bot.Error} If the element cannot be interacted with.312*/313bot.action.rightClick = function (element, opt_coords, opt_mouse) {314var coords = bot.action.prepareToInteractWith_(element, opt_coords);315var mouse = opt_mouse || new bot.Mouse();316mouse.move(element, coords);317mouse.pressButton(bot.Mouse.Button.RIGHT);318mouse.releaseButton();319};320321322/**323* Double-clicks on the given `element` with a virtual mouse.324*325* @param {!Element} element The element to click.326* @param {goog.math.Coordinate=} opt_coords Mouse position relative to the327* element.328* @param {bot.Mouse=} opt_mouse Mouse to use; if not provided, constructs one.329* @throws {bot.Error} If the element cannot be interacted with.330*/331bot.action.doubleClick = function (element, opt_coords, opt_mouse) {332var coords = bot.action.prepareToInteractWith_(element, opt_coords);333var mouse = opt_mouse || new bot.Mouse();334mouse.move(element, coords);335mouse.pressButton(bot.Mouse.Button.LEFT);336mouse.releaseButton();337mouse.pressButton(bot.Mouse.Button.LEFT);338mouse.releaseButton();339};340341342/**343* Double-clicks on the given `element` with a virtual mouse.344*345* @param {!Element} element The element to click.346* @param {goog.math.Coordinate=} opt_coords Mouse position relative to the347* element.348* @param {bot.Mouse=} opt_mouse Mouse to use; if not provided, constructs one.349* @throws {bot.Error} If the element cannot be interacted with.350*/351bot.action.doubleClick2 = function (element, opt_coords, opt_mouse) {352var coords = bot.action.prepareToInteractWith_(element, opt_coords);353var mouse = opt_mouse || new bot.Mouse();354mouse.move(element, coords);355mouse.pressButton(bot.Mouse.Button.LEFT, 2);356mouse.releaseButton(true, 2);357};358359360/**361* Scrolls the mouse wheel on the given `element` with a virtual mouse.362*363* @param {!Element} element The element to scroll the mouse wheel on.364* @param {number} ticks Number of ticks to scroll the mouse wheel; a positive365* number scrolls down and a negative scrolls up.366* @param {goog.math.Coordinate=} opt_coords Mouse position relative to the367* element.368* @param {bot.Mouse=} opt_mouse Mouse to use; if not provided, constructs one.369* @throws {bot.Error} If the element cannot be interacted with.370*/371bot.action.scrollMouse = function (element, ticks, opt_coords, opt_mouse) {372var coords = bot.action.prepareToInteractWith_(element, opt_coords);373var mouse = opt_mouse || new bot.Mouse();374mouse.move(element, coords);375mouse.scroll(ticks);376};377378379/**380* Drags the given `element` by (dx, dy) with a virtual mouse.381*382* @param {!Element} element The element to drag.383* @param {number} dx Increment in x coordinate.384* @param {number} dy Increment in y coordinate.385* @param {number=} opt_steps The number of steps that should occur as part of386* the drag, default is 2.387* @param {goog.math.Coordinate=} opt_coords Drag start position relative to the388* element.389* @param {bot.Mouse=} opt_mouse Mouse to use; if not provided, constructs one.390* @throws {bot.Error} If the element cannot be interacted with.391*/392bot.action.drag = function (element, dx, dy, opt_steps, opt_coords, opt_mouse) {393var coords = bot.action.prepareToInteractWith_(element, opt_coords);394var initRect = bot.dom.getClientRect(element);395var mouse = opt_mouse || new bot.Mouse();396mouse.move(element, coords);397mouse.pressButton(bot.Mouse.Button.LEFT);398var steps = opt_steps !== undefined ? opt_steps : 2;399if (steps < 1) {400throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,401'There must be at least one step as part of a drag.');402}403for (var i = 1; i <= steps; i++) {404moveTo(Math.floor(i * dx / steps), Math.floor(i * dy / steps));405}406mouse.releaseButton();407408function moveTo(x, y) {409var currRect = bot.dom.getClientRect(element);410var newPos = new goog.math.Coordinate(411coords.x + initRect.left + x - currRect.left,412coords.y + initRect.top + y - currRect.top);413mouse.move(element, newPos);414}415};416417418/**419* Taps on the given `element` with a virtual touch screen.420*421* @param {!Element} element The element to tap.422* @param {goog.math.Coordinate=} opt_coords Finger position relative to the423* target.424* @param {bot.Touchscreen=} opt_touchscreen Touchscreen to use; if not425* provided, constructs one.426* @throws {bot.Error} If the element cannot be interacted with.427*/428bot.action.tap = function (element, opt_coords, opt_touchscreen) {429var coords = bot.action.prepareToInteractWith_(element, opt_coords);430var touchscreen = opt_touchscreen || new bot.Touchscreen();431touchscreen.move(element, coords);432touchscreen.press();433touchscreen.release();434};435436437/**438* Swipes the given `element` by (dx, dy) with a virtual touch screen.439*440* @param {!Element} element The element to swipe.441* @param {number} dx Increment in x coordinate.442* @param {number} dy Increment in y coordinate.443* @param {number=} opt_steps The number of steps that should occurs as part of444* the swipe, default is 2.445* @param {goog.math.Coordinate=} opt_coords Swipe start position relative to446* the element.447* @param {bot.Touchscreen=} opt_touchscreen Touchscreen to use; if not448* provided, constructs one.449* @throws {bot.Error} If the element cannot be interacted with.450*/451bot.action.swipe = function (element, dx, dy, opt_steps, opt_coords,452opt_touchscreen) {453var coords = bot.action.prepareToInteractWith_(element, opt_coords);454var touchscreen = opt_touchscreen || new bot.Touchscreen();455var initRect = bot.dom.getClientRect(element);456touchscreen.move(element, coords);457touchscreen.press();458var steps = opt_steps !== undefined ? opt_steps : 2;459if (steps < 1) {460throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,461'There must be at least one step as part of a swipe.');462}463for (var i = 1; i <= steps; i++) {464moveTo(Math.floor(i * dx / steps), Math.floor(i * dy / steps));465}466touchscreen.release();467468function moveTo(x, y) {469var currRect = bot.dom.getClientRect(element);470var newPos = new goog.math.Coordinate(471coords.x + initRect.left + x - currRect.left,472coords.y + initRect.top + y - currRect.top);473touchscreen.move(element, newPos);474}475};476477478/**479* Pinches the given `element` by the given distance with a virtual touch480* screen. A positive distance moves two fingers inward toward each and a481* negative distances spreads them outward. The optional coordinate is the point482* the fingers move towards (for positive distances) or away from (for negative483* distances); and if not provided, defaults to the center of the element.484*485* @param {!Element} element The element to pinch.486* @param {number} distance The distance by which to pinch the element.487* @param {goog.math.Coordinate=} opt_coords Position relative to the element488* at the center of the pinch.489* @param {bot.Touchscreen=} opt_touchscreen Touchscreen to use; if not490* provided, constructs one.491* @throws {bot.Error} If the element cannot be interacted with.492*/493bot.action.pinch = function (element, distance, opt_coords, opt_touchscreen) {494if (distance == 0) {495throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,496'Cannot pinch by a distance of zero.');497}498function startSoThatEndsAtMax(offsetVec) {499if (distance < 0) {500var magnitude = offsetVec.magnitude();501offsetVec.scale(magnitude ? (magnitude + distance) / magnitude : 0);502}503}504var halfDistance = distance / 2;505function scaleByHalfDistance(offsetVec) {506var magnitude = offsetVec.magnitude();507offsetVec.scale(magnitude ? (magnitude - halfDistance) / magnitude : 0);508}509bot.action.multiTouchAction_(element,510startSoThatEndsAtMax,511scaleByHalfDistance,512opt_coords,513opt_touchscreen);514};515516517/**518* Rotates the given `element` by the given angle with a virtual touch519* screen. A positive angle moves two fingers clockwise and a negative angle520* moves them counter-clockwise. The optional coordinate is the point to521* rotate around; and if not provided, defaults to the center of the element.522*523* @param {!Element} element The element to rotate.524* @param {number} angle The angle by which to rotate the element.525* @param {goog.math.Coordinate=} opt_coords Position relative to the element526* at the center of the rotation.527* @param {bot.Touchscreen=} opt_touchscreen Touchscreen to use; if not528* provided, constructs one.529* @throws {bot.Error} If the element cannot be interacted with.530*/531bot.action.rotate = function (element, angle, opt_coords, opt_touchscreen) {532if (angle == 0) {533throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,534'Cannot rotate by an angle of zero.');535}536function startHalfwayToMax(offsetVec) {537offsetVec.scale(0.5);538}539var halfRadians = Math.PI * (angle / 180) / 2;540function rotateByHalfAngle(offsetVec) {541offsetVec.rotate(halfRadians);542}543bot.action.multiTouchAction_(element,544startHalfwayToMax,545rotateByHalfAngle,546opt_coords,547opt_touchscreen);548};549550551/**552* Performs a multi-touch action with two fingers on the given element. This553* helper function works by manipulating an "offsetVector", which is the vector554* away from the center of the interaction at which the fingers are positioned.555* It computes the maximum offset vector and passes it to transformStart to556* find the starting position of the fingers; it then passes it to transformHalf557* twice to find the midpoint and final position of the fingers.558*559* @param {!Element} element Element to interact with.560* @param {function(goog.math.Vec2)} transformStart Function to transform the561* maximum offset vector to the starting offset vector.562* @param {function(goog.math.Vec2)} transformHalf Function to transform the563* offset vector halfway to its destination.564* @param {goog.math.Coordinate=} opt_coords Position relative to the element565* at the center of the pinch.566* @param {bot.Touchscreen=} opt_touchscreen Touchscreen to use; if not567* provided, constructs one.568* @private569*/570bot.action.multiTouchAction_ = function (element, transformStart, transformHalf,571opt_coords, opt_touchscreen) {572var center = bot.action.prepareToInteractWith_(element, opt_coords);573var size = bot.action.getInteractableSize(element);574var offsetVec = new goog.math.Vec2(575Math.min(center.x, size.width - center.x),576Math.min(center.y, size.height - center.y));577578var touchScreen = opt_touchscreen || new bot.Touchscreen();579transformStart(offsetVec);580var start1 = goog.math.Vec2.sum(center, offsetVec);581var start2 = goog.math.Vec2.difference(center, offsetVec);582touchScreen.move(element, start1, start2);583touchScreen.press(/*Two Finger Press*/ true);584585var initRect = bot.dom.getClientRect(element);586transformHalf(offsetVec);587var mid1 = goog.math.Vec2.sum(center, offsetVec);588var mid2 = goog.math.Vec2.difference(center, offsetVec);589touchScreen.move(element, mid1, mid2);590591var midRect = bot.dom.getClientRect(element);592var movedVec = goog.math.Vec2.difference(593new goog.math.Vec2(midRect.left, midRect.top),594new goog.math.Vec2(initRect.left, initRect.top));595transformHalf(offsetVec);596var end1 = goog.math.Vec2.sum(center, offsetVec).subtract(movedVec);597var end2 = goog.math.Vec2.difference(center, offsetVec).subtract(movedVec);598touchScreen.move(element, end1, end2);599touchScreen.release();600};601602603/**604* Prepares to interact with the given `element`. It checks if the the605* element is shown, scrolls the element into view, and returns the coordinates606* of the interaction, which if not provided, is the center of the element.607*608* @param {!Element} element The element to be interacted with.609* @param {goog.math.Coordinate=} opt_coords Position relative to the target.610* @return {!goog.math.Vec2} Coordinates at the center of the interaction.611* @throws {bot.Error} If the element cannot be interacted with.612* @private613*/614bot.action.prepareToInteractWith_ = function (element, opt_coords) {615bot.action.checkShown_(element);616bot.action.scrollIntoView(element, opt_coords || undefined);617618// NOTE: Ideally, we would check that any provided coordinates fall619// within the bounds of the element, but this has proven difficult, because:620// (1) Browsers sometimes lie about the true size of elements, e.g. when text621// overflows the bounding box of an element, browsers report the size of the622// box even though the true area that can be interacted with is larger; and623// (2) Elements with children styled as position:absolute will often not have624// a bounding box that surrounds all of their children, but it is useful for625// the user to be able to interact with this parent element as if it does.626if (opt_coords) {627return goog.math.Vec2.fromCoordinate(opt_coords);628} else {629var size = bot.action.getInteractableSize(element);630return new goog.math.Vec2(size.width / 2, size.height / 2);631}632};633634635/**636* Returns the interactable size of an element.637*638* @param {!Element} elem Element.639* @return {!goog.math.Size} size Size of the element.640*/641bot.action.getInteractableSize = function (elem) {642var size = goog.style.getSize(elem);643return ((size.width > 0 && size.height > 0) || !elem.offsetParent) ? size :644bot.action.getInteractableSize(elem.offsetParent);645};646647648649/**650* A Device that is intended to allows access to protected members of the651* Device superclass. A singleton.652*653* @constructor654* @extends {bot.Device}655* @private656*/657bot.action.LegacyDevice_ = function () {658bot.Device.call(this);659};660goog.utils.inherits(bot.action.LegacyDevice_, bot.Device);661goog.utils.addSingletonGetter(bot.action.LegacyDevice_);662663664/**665* Focuses on the given element. See {@link bot.device.focusOnElement}.666* @param {!Element} element The element to focus on.667* @return {boolean} True if element.focus() was called on the element.668*/669bot.action.LegacyDevice_.focusOnElement = function (element) {670var instance = bot.action.LegacyDevice_.getInstance();671instance.setElement(element);672return instance.focusOnElement();673};674675676/**677* Submit the form for the element. See {@link bot.device.submit}.678* @param {!Element} element The element to submit a form on.679* @param {!Element} form The form to submit.680*/681bot.action.LegacyDevice_.submitForm = function (element, form) {682var instance = bot.action.LegacyDevice_.getInstance();683instance.setElement(element);684instance.submitForm(form);685};686687688/**689* Find FORM element that is an ancestor of the passed in element. See690* {@link bot.device.findAncestorForm}.691* @param {!Element} element The element to find an ancestor form.692* @return {Element} form The ancestor form, or null if none.693*/694bot.action.LegacyDevice_.findAncestorForm = function (element) {695return bot.Device.findAncestorForm(element);696};697698699/**700* Scrolls the given `element` in to the current viewport. Aims to do the701* minimum scrolling necessary, but prefers too much scrolling to too little.702*703* If an optional coordinate or rectangle region is provided, scrolls that704* region relative to the element into view. A coordinate is treated as a 1x1705* region whose top-left corner is positioned at that coordinate.706*707* @param {!Element} element The element to scroll in to view.708* @param {!(goog.math.Coordinate|goog.math.Rect)=} opt_region709* Region relative to the top-left corner of the element.710* @return {boolean} Whether the element is in view after scrolling.711*/712bot.action.scrollIntoView = function (element, opt_region) {713// If the element is already in view, return true; if hidden, return false.714var overflow = bot.dom.getOverflowState(element, opt_region);715if (overflow != bot.dom.OverflowState.SCROLL) {716return overflow == bot.dom.OverflowState.NONE;717}718719// Some elements may not have a scrollIntoView function - for example,720// elements under an SVG element. Call those only if they exist.721if (element.scrollIntoView) {722element.scrollIntoView();723if (bot.dom.OverflowState.NONE ==724bot.dom.getOverflowState(element, opt_region)) {725return true;726}727}728729// There may have not been a scrollIntoView function, or the specified730// coordinate may not be in view, so scroll "manually".731var region = bot.dom.getClientRegion(element, opt_region);732for (var container = bot.dom.getParentElement(element);733container;734container = bot.dom.getParentElement(container)) {735scrollClientRegionIntoContainerView(container);736}737return bot.dom.OverflowState.NONE ==738bot.dom.getOverflowState(element, opt_region);739740function scrollClientRegionIntoContainerView(container) {741// Based largely from goog.style.scrollIntoContainerView.742var containerRect = bot.dom.getClientRect(container);743var containerBorder = goog.style.getBorderBox(container);744745// Relative position of the region to the container's content box.746var relX = region.left - containerRect.left - containerBorder.left;747var relY = region.top - containerRect.top - containerBorder.top;748749// How much the region can move in the container. Use the container's750// clientWidth/Height, not containerRect, to account for the scrollbar.751var spaceX = container.clientWidth + region.left - region.right;752var spaceY = container.clientHeight + region.top - region.bottom;753754// Scroll the element into view of the container.755container.scrollLeft += Math.min(relX, Math.max(relX - spaceX, 0));756container.scrollTop += Math.min(relY, Math.max(relY - spaceY, 0));757}758};759760761