// 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 The file contains the base class for input devices such as19* the keyboard, mouse, and touchscreen.20*/2122goog.provide('bot.Device');23goog.provide('bot.Device.EventEmitter');2425goog.require('bot');26goog.require('bot.Error');27goog.require('bot.ErrorCode');28goog.require('bot.dom');29goog.require('bot.events');30goog.require('bot.locators');31goog.require('bot.userAgent');32goog.require('goog.array');33goog.require('goog.dom');34goog.require('goog.dom.TagName');35goog.require('goog.userAgent');36goog.require('goog.userAgent.product');37383940/**41* A Device class that provides common functionality for input devices.42* @param {bot.Device.ModifiersState=} opt_modifiersState state of modifier43* keys. The state is shared, not copied from this parameter.44* @param {bot.Device.EventEmitter=} opt_eventEmitter An object that should be45* used to fire events.46* @constructor47*/48bot.Device = function (opt_modifiersState, opt_eventEmitter) {49/**50* Element being interacted with.51* @private {!Element}52*/53this.element_ = bot.getDocument().documentElement;5455/**56* If the element is an option, this is its parent select element.57* @private {Element}58*/59this.select_ = null;6061// If there is an active element, make that the current element instead.62var activeElement = bot.dom.getActiveElement(this.element_);63if (activeElement) {64this.setElement(activeElement);65}6667/**68* State of modifier keys for this device.69* @protected {bot.Device.ModifiersState}70*/71this.modifiersState = opt_modifiersState || new bot.Device.ModifiersState();7273/** @protected {!bot.Device.EventEmitter} */74this.eventEmitter = opt_eventEmitter || new bot.Device.EventEmitter();75};767778/**79* Returns the element with which the device is interacting.80*81* @return {!Element} Element being interacted with.82* @protected83*/84bot.Device.prototype.getElement = function () {85return this.element_;86};878889/**90* Sets the element with which the device is interacting.91*92* @param {!Element} element Element being interacted with.93* @protected94*/95bot.Device.prototype.setElement = function (element) {96this.element_ = element;97if (bot.dom.isElement(element, goog.dom.TagName.OPTION)) {98this.select_ = /** @type {Element} */ (goog.dom.getAncestor(element,99function (node) {100return bot.dom.isElement(node, goog.dom.TagName.SELECT);101}));102} else {103this.select_ = null;104}105};106107108/**109* Fires an HTML event given the state of the device.110*111* @param {!bot.events.EventFactory_} type HTML Event type.112* @return {boolean} Whether the event fired successfully; false if cancelled.113* @protected114*/115bot.Device.prototype.fireHtmlEvent = function (type) {116return this.eventEmitter.fireHtmlEvent(this.element_, type);117};118119120/**121* Fires a keyboard event given the state of the device and the given arguments.122* TODO: Populate the modifier keys in this method.123*124* @param {!bot.events.EventFactory_} type Keyboard event type.125* @param {bot.events.KeyboardArgs} args Keyboard event arguments.126* @return {boolean} Whether the event fired successfully; false if cancelled.127* @protected128*/129bot.Device.prototype.fireKeyboardEvent = function (type, args) {130return this.eventEmitter.fireKeyboardEvent(this.element_, type, args);131};132133134/**135* Fires a mouse event given the state of the device and the given arguments.136* TODO: Populate the modifier keys in this method.137*138* @param {!bot.events.EventFactory_} type Mouse event type.139* @param {!goog.math.Coordinate} coord The coordinate where event will fire.140* @param {number} button The mouse button value for the event.141* @param {Element=} opt_related The related element of this event.142* @param {?number=} opt_wheelDelta The wheel delta value for the event.143* @param {boolean=} opt_force Whether the event should be fired even if the144* element is not interactable, such as the case of a mousemove or145* mouseover event that immediately follows a mouseout.146* @param {?number=} opt_pointerId The pointerId associated with the event.147* @param {?number=} opt_count Number of clicks that have been performed.148* @return {boolean} Whether the event fired successfully; false if cancelled.149* @protected150*/151bot.Device.prototype.fireMouseEvent = function (type, coord, button,152opt_related, opt_wheelDelta, opt_force, opt_pointerId, opt_count) {153if (!opt_force && !bot.dom.isInteractable(this.element_)) {154return false;155}156157if (opt_related &&158!(bot.events.EventType.MOUSEOVER == type ||159bot.events.EventType.MOUSEOUT == type)) {160throw new bot.Error(bot.ErrorCode.INVALID_ELEMENT_STATE,161'Event type does not allow related target: ' + type);162}163164var args = {165clientX: coord.x,166clientY: coord.y,167button: button,168altKey: this.modifiersState.isAltPressed(),169ctrlKey: this.modifiersState.isControlPressed(),170shiftKey: this.modifiersState.isShiftPressed(),171metaKey: this.modifiersState.isMetaPressed(),172wheelDelta: opt_wheelDelta || 0,173relatedTarget: opt_related || null,174count: opt_count || 1175};176177var pointerId = opt_pointerId || bot.Device.MOUSE_MS_POINTER_ID;178179var target = this.element_;180// On click and mousedown events, captured pointers are ignored and the181// event always fires on the original element.182if (type != bot.events.EventType.CLICK &&183type != bot.events.EventType.MOUSEDOWN &&184pointerId in bot.Device.pointerElementMap_) {185target = bot.Device.pointerElementMap_[pointerId];186} else if (this.select_) {187target = this.getTargetOfOptionMouseEvent_(type);188}189return target ? this.eventEmitter.fireMouseEvent(target, type, args) : true;190};191192193/**194* Fires a touch event given the state of the device and the given arguments.195*196* @param {!bot.events.EventFactory_} type Event type.197* @param {number} id The touch identifier.198* @param {!goog.math.Coordinate} coord The coordinate where event will fire.199* @param {number=} opt_id2 The touch identifier of the second finger.200* @param {!goog.math.Coordinate=} opt_coord2 The coordinate of the second201* finger, if any.202* @return {boolean} Whether the event fired successfully or was cancelled.203* @protected204*/205bot.Device.prototype.fireTouchEvent = function (type, id, coord, opt_id2,206opt_coord2) {207var args = {208touches: [],209targetTouches: [],210changedTouches: [],211altKey: this.modifiersState.isAltPressed(),212ctrlKey: this.modifiersState.isControlPressed(),213shiftKey: this.modifiersState.isShiftPressed(),214metaKey: this.modifiersState.isMetaPressed(),215relatedTarget: null,216scale: 0,217rotation: 0218};219var pageOffset = goog.dom.getDomHelper(this.element_).getDocumentScroll();220221function addTouch(identifier, coords) {222// Android devices leave identifier to zero.223var touch = {224identifier: identifier,225screenX: coords.x,226screenY: coords.y,227clientX: coords.x,228clientY: coords.y,229pageX: coords.x + pageOffset.x,230pageY: coords.y + pageOffset.y231};232233args.changedTouches.push(touch);234if (type == bot.events.EventType.TOUCHSTART ||235type == bot.events.EventType.TOUCHMOVE) {236args.touches.push(touch);237args.targetTouches.push(touch);238}239}240241addTouch(id, coord);242if (opt_id2 !== undefined) {243addTouch(opt_id2, opt_coord2);244}245246return this.eventEmitter.fireTouchEvent(this.element_, type, args);247};248249250/**251* Fires a MSPointer event given the state of the device and the given252* arguments.253*254* @param {!bot.events.EventFactory_} type MSPointer event type.255* @param {!goog.math.Coordinate} coord The coordinate where event will fire.256* @param {number} button The mouse button value for the event.257* @param {number} pointerId The pointer id for this event.258* @param {number} device The device type used for this event.259* @param {boolean} isPrimary Whether the pointer represents the primary point260* of contact.261* @param {Element=} opt_related The related element of this event.262* @param {boolean=} opt_force Whether the event should be fired even if the263* element is not interactable, such as the case of a mousemove or264* mouseover event that immediately follows a mouseout.265* @return {boolean} Whether the event fired successfully; false if cancelled.266* @protected267*/268bot.Device.prototype.fireMSPointerEvent = function (type, coord, button,269pointerId, device, isPrimary, opt_related, opt_force) {270if (!opt_force && !bot.dom.isInteractable(this.element_)) {271return false;272}273274if (opt_related &&275!(bot.events.EventType.MSPOINTEROVER == type ||276bot.events.EventType.MSPOINTEROUT == type)) {277throw new bot.Error(bot.ErrorCode.INVALID_ELEMENT_STATE,278'Event type does not allow related target: ' + type);279}280281var args = {282clientX: coord.x,283clientY: coord.y,284button: button,285altKey: false,286ctrlKey: false,287shiftKey: false,288metaKey: false,289relatedTarget: opt_related || null,290width: 0,291height: 0,292pressure: 0, // Pressure is only given when a stylus is used.293rotation: 0,294pointerId: pointerId,295tiltX: 0,296tiltY: 0,297pointerType: device,298isPrimary: isPrimary299};300301var target = this.select_ ?302this.getTargetOfOptionMouseEvent_(type) : this.element_;303if (bot.Device.pointerElementMap_[pointerId]) {304target = bot.Device.pointerElementMap_[pointerId];305}306var owner = goog.dom.getWindow(goog.dom.getOwnerDocument(this.element_));307var originalMsSetPointerCapture;308if (owner && type == bot.events.EventType.MSPOINTERDOWN) {309// Overwrite msSetPointerCapture on the Element's msSetPointerCapture310// because synthetic pointer events cause an access denied exception.311// The prototype is modified because the pointer event will bubble up and312// we do not know which element will handle the pointer event.313originalMsSetPointerCapture =314owner['Element'].prototype.msSetPointerCapture;315owner['Element'].prototype.msSetPointerCapture = function (id) {316bot.Device.pointerElementMap_[id] = this;317};318}319var result =320target ? this.eventEmitter.fireMSPointerEvent(target, type, args) : true;321if (originalMsSetPointerCapture) {322owner['Element'].prototype.msSetPointerCapture =323originalMsSetPointerCapture;324}325return result;326};327328329/**330* A mouse event fired "on" an option element, doesn't always fire on the331* option element itself. Sometimes it fires on the parent select element332* and sometimes not at all, depending on the browser and event type. This333* returns the true target element of the event, or null if none is fired.334*335* @param {!bot.events.EventFactory_} type Type of event.336* @return {Element} Element the event should be fired on, null if none.337* @private338*/339bot.Device.prototype.getTargetOfOptionMouseEvent_ = function (type) {340// IE either fires the event on the parent select or not at all.341if (goog.userAgent.IE) {342switch (type) {343case bot.events.EventType.MOUSEOVER:344case bot.events.EventType.MSPOINTEROVER:345return null;346case bot.events.EventType.CONTEXTMENU:347case bot.events.EventType.MOUSEMOVE:348case bot.events.EventType.MSPOINTERMOVE:349return this.select_.multiple ? this.select_ : null;350default:351return this.select_;352}353}354355// WebKit always fires on the option element of multi-selects.356// On single-selects, it either fires on the parent or not at all.357if (goog.userAgent.WEBKIT) {358switch (type) {359case bot.events.EventType.CLICK:360case bot.events.EventType.MOUSEUP:361return this.select_.multiple ? this.element_ : this.select_;362default:363return this.select_.multiple ? this.element_ : null;364}365}366367// Firefox fires every event or the option element.368return this.element_;369};370371372/**373* A helper function to fire click events. This method is shared between374* the mouse and touchscreen devices.375*376* @param {!goog.math.Coordinate} coord The coordinate where event will fire.377* @param {number} button The mouse button value for the event.378* @param {boolean=} opt_force Whether the click should occur even if the379* element is not interactable, such as when an element is hidden by a380* mouseup handler.381* @param {?number=} opt_pointerId The pointer id associated with the click.382* @protected383*/384bot.Device.prototype.clickElement = function (coord, button, opt_force,385opt_pointerId) {386if (!opt_force && !bot.dom.isInteractable(this.element_)) {387return;388}389390// bot.events.fire(element, 'click') can trigger all onclick events, but may391// not follow links (FORM.action or A.href).392// TAG IE GECKO WebKit393// A(href) No No Yes394// FORM(action) No Yes Yes395var targetLink = null;396var targetButton = null;397if (!bot.Device.ALWAYS_FOLLOWS_LINKS_ON_CLICK_) {398for (var e = this.element_; e; e = e.parentNode) {399if (bot.dom.isElement(e, goog.dom.TagName.A)) {400targetLink = /**@type {!Element}*/ (e);401break;402} else if (bot.Device.isFormSubmitElement(e)) {403targetButton = e;404break;405}406}407}408409// When an element is toggled as the result of a click, the toggling and the410// change event happens before the click event on some browsers. However, on411// radio buttons and checkboxes, the click handler can prevent the toggle from412// happening, so we must fire the click first to see if it is cancelled.413var isRadioOrCheckbox = !this.select_ && bot.dom.isSelectable(this.element_);414var wasChecked = isRadioOrCheckbox && bot.dom.isSelected(this.element_);415416// NOTE: Clicking on a form submit button is a little broken:417// (1) When clicking a form submit button in IE, firing a click event or418// calling Form.submit() will not by itself submit the form, so we call419// Element.click() explicitly, but as a result, the coordinates of the click420// event are not provided. Also, when clicking on an <input type=image>, the421// coordinates click that are submitted with the form are always (0, 0).422// (2) When clicking a form submit button in GECKO, while the coordinates of423// the click event are correct, those submitted with the form are always (0,0)424// .425// TODO: See if either of these can be resolved, perhaps by adding426// hidden form elements with the coordinates before the form is submitted.427if (goog.userAgent.IE && targetButton) {428targetButton.click();429return;430}431432var performDefault = this.fireMouseEvent(433bot.events.EventType.CLICK, coord, button, null, 0, opt_force,434opt_pointerId);435if (!performDefault) {436return;437}438439if (targetLink && bot.Device.shouldFollowHref_(targetLink)) {440bot.Device.followHref_(targetLink);441} else if (isRadioOrCheckbox) {442this.toggleRadioButtonOrCheckbox_(wasChecked);443}444};445446447/**448* Focuses on the given element and returns true if it supports being focused449* and does not already have focus; otherwise, returns false. If another element450* has focus, that element will be blurred before focusing on the given element.451*452* @return {boolean} Whether the element was given focus.453* @protected454*/455bot.Device.prototype.focusOnElement = function () {456var elementToFocus = goog.dom.getAncestor(457this.element_,458function (node) {459return !!node && bot.dom.isElement(node) &&460bot.dom.isFocusable(/** @type {!Element} */(node));461},462true /* Return this.element_ if it is focusable. */);463elementToFocus = elementToFocus || this.element_;464465var activeElement = bot.dom.getActiveElement(elementToFocus);466if (elementToFocus == activeElement) {467return false;468}469470// If there is a currently active element, try to blur it.471if (activeElement && (typeof activeElement.blur === 'function' ||472// IE reports native functions as being objects.473goog.userAgent.IE && (typeof activeElement.blur === 'object' && activeElement.blur !== null))) {474// In IE, the focus() and blur() functions fire their respective events475// asynchronously, and as the result, the focus/blur events fired by the476// the atoms actions will often be in the wrong order on IE. Firing a blur477// out of order sometimes causes IE to throw an "Unspecified error", so we478// wrap it in a try-catch and catch and ignore the error in this case.479if (!bot.dom.isElement(activeElement, goog.dom.TagName.BODY)) {480try {481activeElement.blur();482} catch (e) {483if (!(goog.userAgent.IE && e.message == 'Unspecified error.')) {484throw e;485}486}487}488489// Sometimes IE6 and IE7 will not fire an onblur event after blur()490// is called, unless window.focus() is called immediately afterward.491// Note that IE8 will hit this branch unless the page is forced into492// IE8-strict mode. This shouldn't hurt anything, we just use the493// useragent sniff so we can compile this out for proper browsers.494if (goog.userAgent.IE && !bot.userAgent.isEngineVersion(8)) {495goog.dom.getWindow(goog.dom.getOwnerDocument(elementToFocus)).focus();496}497}498499// Try to focus on the element.500if (typeof elementToFocus.focus === 'function' ||501goog.userAgent.IE && (typeof elementToFocus.focus === 'object' && elementToFocus.focus !== null)) {502/** @type {function()} */ (elementToFocus.focus).call(elementToFocus);503return true;504}505506return false;507};508509510/**511* Whether links must be manually followed when clicking (because firing click512* events doesn't follow them).513* @private {boolean}514* @const515*/516bot.Device.ALWAYS_FOLLOWS_LINKS_ON_CLICK_ = goog.userAgent.WEBKIT;517518519/**520* @param {Node} element The element to check.521* @return {boolean} Whether the element is a submit element in form.522* @protected523*/524bot.Device.isFormSubmitElement = function (element) {525if (bot.dom.isElement(element, goog.dom.TagName.INPUT)) {526var type = element.type.toLowerCase();527if (type == 'submit' || type == 'image') {528return true;529}530}531532if (bot.dom.isElement(element, goog.dom.TagName.BUTTON)) {533var type = element.type.toLowerCase();534if (type == 'submit') {535return true;536}537}538return false;539};540541542/**543* Indicates whether we should manually follow the href of the element we're544* clicking.545*546* Versions of firefox from 4+ will handle links properly when this is used in547* an extension. Versions of Firefox prior to this may or may not do the right548* thing depending on whether a target window is opened and whether the click549* has caused a change in just the hash part of the URL.550*551* @param {!Element} element The element to consider.552* @return {boolean} Whether following an href should be skipped.553* @private554*/555bot.Device.shouldFollowHref_ = function (element) {556if (bot.Device.ALWAYS_FOLLOWS_LINKS_ON_CLICK_ || !element.href) {557return false;558}559560if (!(bot.userAgent.WEBEXTENSION)) {561return true;562}563564if (element.target || element.href.toLowerCase().indexOf('javascript') == 0) {565return false;566}567568var owner = goog.dom.getWindow(goog.dom.getOwnerDocument(element));569var sourceUrl = owner.location.href;570var destinationUrl = bot.Device.resolveUrl_(owner.location, element.href);571var isOnlyHashChange =572sourceUrl.split('#')[0] === destinationUrl.split('#')[0];573574return !isOnlyHashChange;575};576577578/**579* Explicitly follows the href of an anchor.580*581* @param {!Element} anchorElement An anchor element.582* @private583*/584bot.Device.followHref_ = function (anchorElement) {585var targetHref = anchorElement.href;586var owner = goog.dom.getWindow(goog.dom.getOwnerDocument(anchorElement));587588// IE7 and earlier incorrect resolve a relative href against the top window589// location instead of the window to which the href is assigned. As a result,590// we have to resolve the relative URL ourselves. We do not use Closure's591// goog.Uri to resolve, because it incorrectly fails to support empty but592// undefined query and fragment components and re-encodes the given url.593if (goog.userAgent.IE && !bot.userAgent.isEngineVersion(8)) {594targetHref = bot.Device.resolveUrl_(owner.location, targetHref);595}596597if (anchorElement.target) {598owner.open(targetHref, anchorElement.target);599} else {600owner.location.href = targetHref;601}602};603604605/**606* Toggles the selected state of the current element if it is an option. This607* is a noop if the element is not an option, or if it is selected and belongs608* to a single-select, because it can't be toggled off.609*610* @protected611*/612bot.Device.prototype.maybeToggleOption = function () {613// If this is not an <option> or not interactable, exit.614if (!this.select_ || !bot.dom.isInteractable(this.element_)) {615return;616}617var select = /** @type {!Element} */ (this.select_);618var wasSelected = bot.dom.isSelected(this.element_);619// Cannot toggle off options in single-selects.620if (wasSelected && !select.multiple) {621return;622}623624// TODO: In a multiselect, clicking an option without the ctrl key down625// should deselect all other selected options. Right now multiselect click626// works as ctrl+click should (and unit tests written so that they pass).627628this.element_.selected = !wasSelected;629// Only WebKit fires the change event itself and only for multi-selects,630// except for Android versions >= 4.0 and Chrome >= 28.631if (!(goog.userAgent.WEBKIT && select.multiple) ||632(goog.userAgent.product.CHROME && bot.userAgent.isProductVersion(28)) ||633(goog.userAgent.product.ANDROID && bot.userAgent.isProductVersion(4))) {634bot.events.fire(select, bot.events.EventType.CHANGE);635}636};637638639/**640* Toggles the checked state of a radio button or checkbox. This is a noop if641* it is a radio button that is checked, because it can't be toggled off.642*643* @param {boolean} wasChecked Whether the element was originally checked.644* @private645*/646bot.Device.prototype.toggleRadioButtonOrCheckbox_ = function (wasChecked) {647// Gecko and WebKit toggle the element as a result of a click.648if (goog.userAgent.GECKO || goog.userAgent.WEBKIT) {649return;650}651// Cannot toggle off radio buttons.652if (wasChecked && this.element_.type.toLowerCase() == 'radio') {653return;654}655this.element_.checked = !wasChecked;656};657658659/**660* Find FORM element that is an ancestor of the passed in element.661* @param {Node} node The node to find a FORM for.662* @return {Element} The ancestor FORM element if it exists.663* @protected664*/665bot.Device.findAncestorForm = function (node) {666return /** @type {Element} */ (goog.dom.getAncestor(667node, bot.Device.isForm_, /*includeNode=*/true));668};669670671/**672* @param {Node} node The node to test.673* @return {boolean} Whether the node is a FORM element.674* @private675*/676bot.Device.isForm_ = function (node) {677return bot.dom.isElement(node, goog.dom.TagName.FORM);678};679680681/**682* Submits the specified form. Unlike the public function, it expects to be683* given a form element and fails if it is not.684* @param {!Element} form The form to submit.685* @protected686*/687bot.Device.prototype.submitForm = function (form) {688if (!bot.Device.isForm_(form)) {689throw new bot.Error(bot.ErrorCode.INVALID_ELEMENT_STATE,690'Element is not a form, so could not submit.');691}692if (bot.events.fire(form, bot.events.EventType.SUBMIT)) {693// When a form has an element with an id or name exactly equal to "submit"694// (not uncommon) it masks the form.submit function. We can avoid this by695// calling the prototype's submit function, except in IE < 8, where DOM id696// elements don't let you reference their prototypes. For IE < 8, can change697// the id and names of the elements and revert them back, but they must be698// reverted before the submit call, because the onsubmit handler might rely699// on their being correct, and the HTTP request might otherwise be left with700// incorrect value names. Fortunately, saving the submit function and701// calling it after reverting the ids and names works! Oh, and goog.typeOf702// (and thus goog.isFunction) doesn't work for form.submit in IE < 8.703if (!bot.dom.isElement(form.submit)) {704form.submit();705} else if (!goog.userAgent.IE || bot.userAgent.isEngineVersion(8)) {706/** @type {Function} */ (form.constructor.prototype['submit']).call(form);707} else {708var idMasks = bot.locators.findElements({ 'id': 'submit' }, form);709var nameMasks = bot.locators.findElements({ 'name': 'submit' }, form);710goog.array.forEach(idMasks, function (m) {711m.removeAttribute('id');712});713goog.array.forEach(nameMasks, function (m) {714m.removeAttribute('name');715});716var submitFunction = form.submit;717goog.array.forEach(idMasks, function (m) {718m.setAttribute('id', 'submit');719});720goog.array.forEach(nameMasks, function (m) {721m.setAttribute('name', 'submit');722});723submitFunction();724}725}726};727728729/**730* Regular expression for splitting up a URL into components.731* @private {!RegExp}732* @const733*/734bot.Device.URL_REGEXP_ = new RegExp(735'^' +736'([^:/?#.]+:)?' + // protocol737'(?://([^/]*))?' + // host738'([^?#]+)?' + // pathname739'(\\?[^#]*)?' + // search740'(#.*)?' + // hash741'$');742743744/**745* Resolves a potentially relative URL against a base location.746* @param {!Location} base Base location against which to resolve.747* @param {string} rel Url to resolve against the location.748* @return {string} Resolution of url against base location.749* @private750*/751bot.Device.resolveUrl_ = function (base, rel) {752var m = rel.match(bot.Device.URL_REGEXP_);753if (!m) {754return '';755}756var target = {757protocol: m[1] || '',758host: m[2] || '',759pathname: m[3] || '',760search: m[4] || '',761hash: m[5] || ''762};763764if (!target.protocol) {765target.protocol = base.protocol;766if (!target.host) {767target.host = base.host;768if (!target.pathname) {769target.pathname = base.pathname;770target.search = target.search || base.search;771} else if (target.pathname.charAt(0) != '/') {772var lastSlashIndex = base.pathname.lastIndexOf('/');773if (lastSlashIndex != -1) {774var directory = base.pathname.substr(0, lastSlashIndex + 1);775target.pathname = directory + target.pathname;776}777}778}779}780781return target.protocol + '//' + target.host + target.pathname +782target.search + target.hash;783};784785786787/**788* Stores the state of modifier keys789*790* @constructor791*/792bot.Device.ModifiersState = function () {793/**794* State of the modifier keys.795* @private {number}796*/797this.pressedModifiers_ = 0;798};799800801/**802* An enum for the various modifier keys (keycode-independent).803* @enum {number}804*/805bot.Device.Modifier = {806SHIFT: 0x1,807CONTROL: 0x2,808ALT: 0x4,809META: 0x8810};811812813/**814* Checks whether a specific modifier is pressed815* @param {!bot.Device.Modifier} modifier The modifier to check.816* @return {boolean} Whether the modifier is pressed.817*/818bot.Device.ModifiersState.prototype.isPressed = function (modifier) {819return (this.pressedModifiers_ & modifier) != 0;820};821822823/**824* Sets the state of a given modifier.825* @param {!bot.Device.Modifier} modifier The modifier to set.826* @param {boolean} isPressed whether the modifier is set or released.827*/828bot.Device.ModifiersState.prototype.setPressed = function (829modifier, isPressed) {830if (isPressed) {831this.pressedModifiers_ = this.pressedModifiers_ | modifier;832} else {833this.pressedModifiers_ = this.pressedModifiers_ & (~modifier);834}835};836837838/**839* @return {boolean} State of the Shift key.840*/841bot.Device.ModifiersState.prototype.isShiftPressed = function () {842return this.isPressed(bot.Device.Modifier.SHIFT);843};844845846/**847* @return {boolean} State of the Control key.848*/849bot.Device.ModifiersState.prototype.isControlPressed = function () {850return this.isPressed(bot.Device.Modifier.CONTROL);851};852853854/**855* @return {boolean} State of the Alt key.856*/857bot.Device.ModifiersState.prototype.isAltPressed = function () {858return this.isPressed(bot.Device.Modifier.ALT);859};860861862/**863* @return {boolean} State of the Meta key.864*/865bot.Device.ModifiersState.prototype.isMetaPressed = function () {866return this.isPressed(bot.Device.Modifier.META);867};868869870/**871* The pointer id used for MSPointer events initiated through a mouse device.872* @type {number}873* @const874*/875bot.Device.MOUSE_MS_POINTER_ID = 1;876877878/**879* A map of pointer id to Elements.880* @private {!Object.<number, !Element>}881*/882bot.Device.pointerElementMap_ = {};883884885/**886* Gets the element associated with a pointer id.887* @param {number} pointerId The pointer Id.888* @return {?Element} The element associated with the pointer id.889* @protected890*/891bot.Device.getPointerElement = function (pointerId) {892return bot.Device.pointerElementMap_[pointerId];893};894895896/**897* Clear the pointer map.898* @protected899*/900bot.Device.clearPointerMap = function () {901bot.Device.pointerElementMap_ = {};902};903904905/**906* Fires events, a driver can replace it with a custom implementation907*908* @constructor909*/910bot.Device.EventEmitter = function () {911};912913914/**915* Fires an HTML event given the state of the device.916*917* @param {!Element} target The element on which to fire the event.918* @param {!bot.events.EventFactory_} type HTML Event type.919* @return {boolean} Whether the event fired successfully; false if cancelled.920* @protected921*/922bot.Device.EventEmitter.prototype.fireHtmlEvent = function (target, type) {923return bot.events.fire(target, type);924};925926927/**928* Fires a keyboard event given the state of the device and the given arguments.929*930* @param {!Element} target The element on which to fire the event.931* @param {!bot.events.EventFactory_} type Keyboard event type.932* @param {bot.events.KeyboardArgs} args Keyboard event arguments.933* @return {boolean} Whether the event fired successfully; false if cancelled.934* @protected935*/936bot.Device.EventEmitter.prototype.fireKeyboardEvent = function (937target, type, args) {938return bot.events.fire(target, type, args);939};940941942/**943* Fires a mouse event given the state of the device and the given arguments.944*945* @param {!Element} target The element on which to fire the event.946* @param {!bot.events.EventFactory_} type Mouse event type.947* @param {bot.events.MouseArgs} args Mouse event arguments.948* @return {boolean} Whether the event fired successfully; false if cancelled.949* @protected950*/951bot.Device.EventEmitter.prototype.fireMouseEvent = function (952target, type, args) {953return bot.events.fire(target, type, args);954};955956957/**958* Fires a mouse event given the state of the device and the given arguments.959*960* @param {!Element} target The element on which to fire the event.961* @param {!bot.events.EventFactory_} type Touch event type.962* @param {bot.events.TouchArgs} args Touch event arguments.963* @return {boolean} Whether the event fired successfully; false if cancelled.964* @protected965*/966bot.Device.EventEmitter.prototype.fireTouchEvent = function (967target, type, args) {968return bot.events.fire(target, type, args);969};970971972/**973* Fires an MSPointer event given the state of the device and the given974* arguments.975*976* @param {!Element} target The element on which to fire the event.977* @param {!bot.events.EventFactory_} type MSPointer event type.978* @param {bot.events.MSPointerArgs} args MSPointer event arguments.979* @return {boolean} Whether the event fired successfully; false if cancelled.980* @protected981*/982bot.Device.EventEmitter.prototype.fireMSPointerEvent = function (983target, type, args) {984return bot.events.fire(target, type, args);985};986987988