// 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.dom');27goog.require('bot.events');28goog.require('bot.locators');29goog.require('bot.userAgent');30goog.require('goog.array');31goog.require('goog.dom');32goog.require('goog.dom.TagName');33goog.require('goog.userAgent');34goog.require('goog.userAgent.product');35363738/**39* A Device class that provides common functionality for input devices.40* @param {bot.Device.ModifiersState=} opt_modifiersState state of modifier41* keys. The state is shared, not copied from this parameter.42* @param {bot.Device.EventEmitter=} opt_eventEmitter An object that should be43* used to fire events.44* @constructor45*/46bot.Device = function (opt_modifiersState, opt_eventEmitter) {47/**48* Element being interacted with.49* @private {!Element}50*/51this.element_ = bot.getDocument().documentElement;5253/**54* If the element is an option, this is its parent select element.55* @private {Element}56*/57this.select_ = null;5859// If there is an active element, make that the current element instead.60var activeElement = bot.dom.getActiveElement(this.element_);61if (activeElement) {62this.setElement(activeElement);63}6465/**66* State of modifier keys for this device.67* @protected {bot.Device.ModifiersState}68*/69this.modifiersState = opt_modifiersState || new bot.Device.ModifiersState();7071/** @protected {!bot.Device.EventEmitter} */72this.eventEmitter = opt_eventEmitter || new bot.Device.EventEmitter();73};747576/**77* Returns the element with which the device is interacting.78*79* @return {!Element} Element being interacted with.80* @protected81*/82bot.Device.prototype.getElement = function () {83return this.element_;84};858687/**88* Sets the element with which the device is interacting.89*90* @param {!Element} element Element being interacted with.91* @protected92*/93bot.Device.prototype.setElement = function (element) {94this.element_ = element;95if (bot.dom.isElement(element, goog.dom.TagName.OPTION)) {96this.select_ = /** @type {Element} */ (goog.dom.getAncestor(element,97function (node) {98return bot.dom.isElement(node, goog.dom.TagName.SELECT);99}));100} else {101this.select_ = null;102}103};104105106/**107* Fires an HTML event given the state of the device.108*109* @param {bot.events.EventType} type HTML Event type.110* @return {boolean} Whether the event fired successfully; false if cancelled.111* @protected112*/113bot.Device.prototype.fireHtmlEvent = function (type) {114return this.eventEmitter.fireHtmlEvent(this.element_, type);115};116117118/**119* Fires a keyboard event given the state of the device and the given arguments.120* TODO: Populate the modifier keys in this method.121*122* @param {bot.events.EventType} type Keyboard event type.123* @param {bot.events.KeyboardArgs} args Keyboard event arguments.124* @return {boolean} Whether the event fired successfully; false if cancelled.125* @protected126*/127bot.Device.prototype.fireKeyboardEvent = function (type, args) {128return this.eventEmitter.fireKeyboardEvent(this.element_, type, args);129};130131132/**133* Fires a mouse event given the state of the device and the given arguments.134* TODO: Populate the modifier keys in this method.135*136* @param {bot.events.EventType} type Mouse event type.137* @param {!goog.math.Coordinate} coord The coordinate where event will fire.138* @param {number} button The mouse button value for the event.139* @param {Element=} opt_related The related element of this event.140* @param {?number=} opt_wheelDelta The wheel delta value for the event.141* @param {boolean=} opt_force Whether the event should be fired even if the142* element is not interactable, such as the case of a mousemove or143* mouseover event that immediately follows a mouseout.144* @param {?number=} opt_pointerId The pointerId associated with the event.145* @param {?number=} opt_count Number of clicks that have been performed.146* @return {boolean} Whether the event fired successfully; false if cancelled.147* @protected148*/149bot.Device.prototype.fireMouseEvent = function (type, coord, button,150opt_related, opt_wheelDelta, opt_force, opt_pointerId, opt_count) {151if (!opt_force && !bot.dom.isInteractable(this.element_)) {152return false;153}154155if (opt_related &&156!(bot.events.EventType.MOUSEOVER == type ||157bot.events.EventType.MOUSEOUT == type)) {158throw new bot.Error(bot.ErrorCode.INVALID_ELEMENT_STATE,159'Event type does not allow related target: ' + type);160}161162var args = {163clientX: coord.x,164clientY: coord.y,165button: button,166altKey: this.modifiersState.isAltPressed(),167ctrlKey: this.modifiersState.isControlPressed(),168shiftKey: this.modifiersState.isShiftPressed(),169metaKey: this.modifiersState.isMetaPressed(),170wheelDelta: opt_wheelDelta || 0,171relatedTarget: opt_related || null,172count: opt_count || 1173};174175var pointerId = opt_pointerId || bot.Device.MOUSE_MS_POINTER_ID;176177var target = this.element_;178// On click and mousedown events, captured pointers are ignored and the179// event always fires on the original element.180if (type != bot.events.EventType.CLICK &&181type != bot.events.EventType.MOUSEDOWN &&182pointerId in bot.Device.pointerElementMap_) {183target = bot.Device.pointerElementMap_[pointerId];184} else if (this.select_) {185target = this.getTargetOfOptionMouseEvent_(type);186}187return target ? this.eventEmitter.fireMouseEvent(target, type, args) : true;188};189190191/**192* Fires a touch event given the state of the device and the given arguments.193*194* @param {bot.events.EventType} type Event type.195* @param {number} id The touch identifier.196* @param {!goog.math.Coordinate} coord The coordinate where event will fire.197* @param {number=} opt_id2 The touch identifier of the second finger.198* @param {!goog.math.Coordinate=} opt_coord2 The coordinate of the second199* finger, if any.200* @return {boolean} Whether the event fired successfully or was cancelled.201* @protected202*/203bot.Device.prototype.fireTouchEvent = function (type, id, coord, opt_id2,204opt_coord2) {205var args = {206touches: [],207targetTouches: [],208changedTouches: [],209altKey: this.modifiersState.isAltPressed(),210ctrlKey: this.modifiersState.isControlPressed(),211shiftKey: this.modifiersState.isShiftPressed(),212metaKey: this.modifiersState.isMetaPressed(),213relatedTarget: null,214scale: 0,215rotation: 0216};217var pageOffset = goog.dom.getDomHelper(this.element_).getDocumentScroll();218219function addTouch(identifier, coords) {220// Android devices leave identifier to zero.221var touch = {222identifier: identifier,223screenX: coords.x,224screenY: coords.y,225clientX: coords.x,226clientY: coords.y,227pageX: coords.x + pageOffset.x,228pageY: coords.y + pageOffset.y229};230231args.changedTouches.push(touch);232if (type == bot.events.EventType.TOUCHSTART ||233type == bot.events.EventType.TOUCHMOVE) {234args.touches.push(touch);235args.targetTouches.push(touch);236}237}238239addTouch(id, coord);240if (goog.isDef(opt_id2)) {241addTouch(opt_id2, opt_coord2);242}243244return this.eventEmitter.fireTouchEvent(this.element_, type, args);245};246247248/**249* Fires a MSPointer event given the state of the device and the given250* arguments.251*252* @param {bot.events.EventType} type MSPointer event type.253* @param {!goog.math.Coordinate} coord The coordinate where event will fire.254* @param {number} button The mouse button value for the event.255* @param {number} pointerId The pointer id for this event.256* @param {number} device The device type used for this event.257* @param {boolean} isPrimary Whether the pointer represents the primary point258* of contact.259* @param {Element=} opt_related The related element of this event.260* @param {boolean=} opt_force Whether the event should be fired even if the261* element is not interactable, such as the case of a mousemove or262* mouseover event that immediately follows a mouseout.263* @return {boolean} Whether the event fired successfully; false if cancelled.264* @protected265*/266bot.Device.prototype.fireMSPointerEvent = function (type, coord, button,267pointerId, device, isPrimary, opt_related, opt_force) {268if (!opt_force && !bot.dom.isInteractable(this.element_)) {269return false;270}271272if (opt_related &&273!(bot.events.EventType.MSPOINTEROVER == type ||274bot.events.EventType.MSPOINTEROUT == type)) {275throw new bot.Error(bot.ErrorCode.INVALID_ELEMENT_STATE,276'Event type does not allow related target: ' + type);277}278279var args = {280clientX: coord.x,281clientY: coord.y,282button: button,283altKey: false,284ctrlKey: false,285shiftKey: false,286metaKey: false,287relatedTarget: opt_related || null,288width: 0,289height: 0,290pressure: 0, // Pressure is only given when a stylus is used.291rotation: 0,292pointerId: pointerId,293tiltX: 0,294tiltY: 0,295pointerType: device,296isPrimary: isPrimary297};298299var target = this.select_ ?300this.getTargetOfOptionMouseEvent_(type) : this.element_;301if (bot.Device.pointerElementMap_[pointerId]) {302target = bot.Device.pointerElementMap_[pointerId];303}304var owner = goog.dom.getWindow(goog.dom.getOwnerDocument(this.element_));305var originalMsSetPointerCapture;306if (owner && type == bot.events.EventType.MSPOINTERDOWN) {307// Overwrite msSetPointerCapture on the Element's msSetPointerCapture308// because synthetic pointer events cause an access denied exception.309// The prototype is modified because the pointer event will bubble up and310// we do not know which element will handle the pointer event.311originalMsSetPointerCapture =312owner['Element'].prototype.msSetPointerCapture;313owner['Element'].prototype.msSetPointerCapture = function (id) {314bot.Device.pointerElementMap_[id] = this;315};316}317var result =318target ? this.eventEmitter.fireMSPointerEvent(target, type, args) : true;319if (originalMsSetPointerCapture) {320owner['Element'].prototype.msSetPointerCapture =321originalMsSetPointerCapture;322}323return result;324};325326327/**328* A mouse event fired "on" an option element, doesn't always fire on the329* option element itself. Sometimes it fires on the parent select element330* and sometimes not at all, depending on the browser and event type. This331* returns the true target element of the event, or null if none is fired.332*333* @param {bot.events.EventType} type Type of event.334* @return {Element} Element the event should be fired on, null if none.335* @private336*/337bot.Device.prototype.getTargetOfOptionMouseEvent_ = function (type) {338// IE either fires the event on the parent select or not at all.339if (goog.userAgent.IE) {340switch (type) {341case bot.events.EventType.MOUSEOVER:342case bot.events.EventType.MSPOINTEROVER:343return null;344case bot.events.EventType.CONTEXTMENU:345case bot.events.EventType.MOUSEMOVE:346case bot.events.EventType.MSPOINTERMOVE:347return this.select_.multiple ? this.select_ : null;348default:349return this.select_;350}351}352353// WebKit always fires on the option element of multi-selects.354// On single-selects, it either fires on the parent or not at all.355if (goog.userAgent.WEBKIT) {356switch (type) {357case bot.events.EventType.CLICK:358case bot.events.EventType.MOUSEUP:359return this.select_.multiple ? this.element_ : this.select_;360default:361return this.select_.multiple ? this.element_ : null;362}363}364365// Firefox fires every event or the option element.366return this.element_;367};368369370/**371* A helper function to fire click events. This method is shared between372* the mouse and touchscreen devices.373*374* @param {!goog.math.Coordinate} coord The coordinate where event will fire.375* @param {number} button The mouse button value for the event.376* @param {boolean=} opt_force Whether the click should occur even if the377* element is not interactable, such as when an element is hidden by a378* mouseup handler.379* @param {?number=} opt_pointerId The pointer id associated with the click.380* @protected381*/382bot.Device.prototype.clickElement = function (coord, button, opt_force,383opt_pointerId) {384if (!opt_force && !bot.dom.isInteractable(this.element_)) {385return;386}387388// bot.events.fire(element, 'click') can trigger all onclick events, but may389// not follow links (FORM.action or A.href).390// TAG IE GECKO WebKit391// A(href) No No Yes392// FORM(action) No Yes Yes393var targetLink = null;394var targetButton = null;395if (!bot.Device.ALWAYS_FOLLOWS_LINKS_ON_CLICK_) {396for (var e = this.element_; e; e = e.parentNode) {397if (bot.dom.isElement(e, goog.dom.TagName.A)) {398targetLink = /**@type {!Element}*/ (e);399break;400} else if (bot.Device.isFormSubmitElement(e)) {401targetButton = e;402break;403}404}405}406407// When an element is toggled as the result of a click, the toggling and the408// change event happens before the click event on some browsers. However, on409// radio buttons and checkboxes, the click handler can prevent the toggle from410// happening, so we must fire the click first to see if it is cancelled.411var isRadioOrCheckbox = !this.select_ && bot.dom.isSelectable(this.element_);412var wasChecked = isRadioOrCheckbox && bot.dom.isSelected(this.element_);413414// NOTE: Clicking on a form submit button is a little broken:415// (1) When clicking a form submit button in IE, firing a click event or416// calling Form.submit() will not by itself submit the form, so we call417// Element.click() explicitly, but as a result, the coordinates of the click418// event are not provided. Also, when clicking on an <input type=image>, the419// coordinates click that are submitted with the form are always (0, 0).420// (2) When clicking a form submit button in GECKO, while the coordinates of421// the click event are correct, those submitted with the form are always (0,0)422// .423// TODO: See if either of these can be resolved, perhaps by adding424// hidden form elements with the coordinates before the form is submitted.425if (goog.userAgent.IE && targetButton) {426targetButton.click();427return;428}429430var performDefault = this.fireMouseEvent(431bot.events.EventType.CLICK, coord, button, null, 0, opt_force,432opt_pointerId);433if (!performDefault) {434return;435}436437if (targetLink && bot.Device.shouldFollowHref_(targetLink)) {438bot.Device.followHref_(targetLink);439} else if (isRadioOrCheckbox) {440this.toggleRadioButtonOrCheckbox_(wasChecked);441}442};443444445/**446* Focuses on the given element and returns true if it supports being focused447* and does not already have focus; otherwise, returns false. If another element448* has focus, that element will be blurred before focusing on the given element.449*450* @return {boolean} Whether the element was given focus.451* @protected452*/453bot.Device.prototype.focusOnElement = function () {454var elementToFocus = goog.dom.getAncestor(455this.element_,456function (node) {457return !!node && bot.dom.isElement(node) &&458bot.dom.isFocusable(/** @type {!Element} */(node));459},460true /* Return this.element_ if it is focusable. */);461elementToFocus = elementToFocus || this.element_;462463var activeElement = bot.dom.getActiveElement(elementToFocus);464if (elementToFocus == activeElement) {465return false;466}467468// If there is a currently active element, try to blur it.469if (activeElement && (goog.isFunction(activeElement.blur) ||470// IE reports native functions as being objects.471goog.userAgent.IE && goog.isObject(activeElement.blur))) {472// In IE, the focus() and blur() functions fire their respective events473// asynchronously, and as the result, the focus/blur events fired by the474// the atoms actions will often be in the wrong order on IE. Firing a blur475// out of order sometimes causes IE to throw an "Unspecified error", so we476// wrap it in a try-catch and catch and ignore the error in this case.477if (!bot.dom.isElement(activeElement, goog.dom.TagName.BODY)) {478try {479activeElement.blur();480} catch (e) {481if (!(goog.userAgent.IE && e.message == 'Unspecified error.')) {482throw e;483}484}485}486487// Sometimes IE6 and IE7 will not fire an onblur event after blur()488// is called, unless window.focus() is called immediately afterward.489// Note that IE8 will hit this branch unless the page is forced into490// IE8-strict mode. This shouldn't hurt anything, we just use the491// useragent sniff so we can compile this out for proper browsers.492if (goog.userAgent.IE && !bot.userAgent.isEngineVersion(8)) {493goog.dom.getWindow(goog.dom.getOwnerDocument(elementToFocus)).focus();494}495}496497// Try to focus on the element.498if (goog.isFunction(elementToFocus.focus) ||499goog.userAgent.IE && goog.isObject(elementToFocus.focus)) {500elementToFocus.focus();501return true;502}503504return false;505};506507508/**509* Whether links must be manually followed when clicking (because firing click510* events doesn't follow them).511* @private {boolean}512* @const513*/514bot.Device.ALWAYS_FOLLOWS_LINKS_ON_CLICK_ = goog.userAgent.WEBKIT;515516517/**518* @param {Node} element The element to check.519* @return {boolean} Whether the element is a submit element in form.520* @protected521*/522bot.Device.isFormSubmitElement = function (element) {523if (bot.dom.isElement(element, goog.dom.TagName.INPUT)) {524var type = element.type.toLowerCase();525if (type == 'submit' || type == 'image') {526return true;527}528}529530if (bot.dom.isElement(element, goog.dom.TagName.BUTTON)) {531var type = element.type.toLowerCase();532if (type == 'submit') {533return true;534}535}536return false;537};538539540/**541* Indicates whether we should manually follow the href of the element we're542* clicking.543*544* Versions of firefox from 4+ will handle links properly when this is used in545* an extension. Versions of Firefox prior to this may or may not do the right546* thing depending on whether a target window is opened and whether the click547* has caused a change in just the hash part of the URL.548*549* @param {!Element} element The element to consider.550* @return {boolean} Whether following an href should be skipped.551* @private552*/553bot.Device.shouldFollowHref_ = function (element) {554if (bot.Device.ALWAYS_FOLLOWS_LINKS_ON_CLICK_ || !element.href) {555return false;556}557558if (!(bot.userAgent.WEBEXTENSION)) {559return true;560}561562if (element.target || element.href.toLowerCase().indexOf('javascript') == 0) {563return false;564}565566var owner = goog.dom.getWindow(goog.dom.getOwnerDocument(element));567var sourceUrl = owner.location.href;568var destinationUrl = bot.Device.resolveUrl_(owner.location, element.href);569var isOnlyHashChange =570sourceUrl.split('#')[0] === destinationUrl.split('#')[0];571572return !isOnlyHashChange;573};574575576/**577* Explicitly follows the href of an anchor.578*579* @param {!Element} anchorElement An anchor element.580* @private581*/582bot.Device.followHref_ = function (anchorElement) {583var targetHref = anchorElement.href;584var owner = goog.dom.getWindow(goog.dom.getOwnerDocument(anchorElement));585586// IE7 and earlier incorrect resolve a relative href against the top window587// location instead of the window to which the href is assigned. As a result,588// we have to resolve the relative URL ourselves. We do not use Closure's589// goog.Uri to resolve, because it incorrectly fails to support empty but590// undefined query and fragment components and re-encodes the given url.591if (goog.userAgent.IE && !bot.userAgent.isEngineVersion(8)) {592targetHref = bot.Device.resolveUrl_(owner.location, targetHref);593}594595if (anchorElement.target) {596owner.open(targetHref, anchorElement.target);597} else {598owner.location.href = targetHref;599}600};601602603/**604* Toggles the selected state of the current element if it is an option. This605* is a noop if the element is not an option, or if it is selected and belongs606* to a single-select, because it can't be toggled off.607*608* @protected609*/610bot.Device.prototype.maybeToggleOption = function () {611// If this is not an <option> or not interactable, exit.612if (!this.select_ || !bot.dom.isInteractable(this.element_)) {613return;614}615var select = /** @type {!Element} */ (this.select_);616var wasSelected = bot.dom.isSelected(this.element_);617// Cannot toggle off options in single-selects.618if (wasSelected && !select.multiple) {619return;620}621622// TODO: In a multiselect, clicking an option without the ctrl key down623// should deselect all other selected options. Right now multiselect click624// works as ctrl+click should (and unit tests written so that they pass).625626this.element_.selected = !wasSelected;627// Only WebKit fires the change event itself and only for multi-selects,628// except for Android versions >= 4.0 and Chrome >= 28.629if (!(goog.userAgent.WEBKIT && select.multiple) ||630(goog.userAgent.product.CHROME && bot.userAgent.isProductVersion(28)) ||631(goog.userAgent.product.ANDROID && bot.userAgent.isProductVersion(4))) {632bot.events.fire(select, bot.events.EventType.CHANGE);633}634};635636637/**638* Toggles the checked state of a radio button or checkbox. This is a noop if639* it is a radio button that is checked, because it can't be toggled off.640*641* @param {boolean} wasChecked Whether the element was originally checked.642* @private643*/644bot.Device.prototype.toggleRadioButtonOrCheckbox_ = function (wasChecked) {645// Gecko and WebKit toggle the element as a result of a click.646if (goog.userAgent.GECKO || goog.userAgent.WEBKIT) {647return;648}649// Cannot toggle off radio buttons.650if (wasChecked && this.element_.type.toLowerCase() == 'radio') {651return;652}653this.element_.checked = !wasChecked;654};655656657/**658* Find FORM element that is an ancestor of the passed in element.659* @param {Node} node The node to find a FORM for.660* @return {Element} The ancestor FORM element if it exists.661* @protected662*/663bot.Device.findAncestorForm = function (node) {664return /** @type {Element} */ (goog.dom.getAncestor(665node, bot.Device.isForm_, /*includeNode=*/true));666};667668669/**670* @param {Node} node The node to test.671* @return {boolean} Whether the node is a FORM element.672* @private673*/674bot.Device.isForm_ = function (node) {675return bot.dom.isElement(node, goog.dom.TagName.FORM);676};677678679/**680* Submits the specified form. Unlike the public function, it expects to be681* given a form element and fails if it is not.682* @param {!Element} form The form to submit.683* @protected684*/685bot.Device.prototype.submitForm = function (form) {686if (!bot.Device.isForm_(form)) {687throw new bot.Error(bot.ErrorCode.INVALID_ELEMENT_STATE,688'Element is not a form, so could not submit.');689}690if (bot.events.fire(form, bot.events.EventType.SUBMIT)) {691// When a form has an element with an id or name exactly equal to "submit"692// (not uncommon) it masks the form.submit function. We can avoid this by693// calling the prototype's submit function, except in IE < 8, where DOM id694// elements don't let you reference their prototypes. For IE < 8, can change695// the id and names of the elements and revert them back, but they must be696// reverted before the submit call, because the onsubmit handler might rely697// on their being correct, and the HTTP request might otherwise be left with698// incorrect value names. Fortunately, saving the submit function and699// calling it after reverting the ids and names works! Oh, and goog.typeOf700// (and thus goog.isFunction) doesn't work for form.submit in IE < 8.701if (!bot.dom.isElement(form.submit)) {702form.submit();703} else if (!goog.userAgent.IE || bot.userAgent.isEngineVersion(8)) {704/** @type {Function} */ (form.constructor.prototype['submit']).call(form);705} else {706var idMasks = bot.locators.findElements({ 'id': 'submit' }, form);707var nameMasks = bot.locators.findElements({ 'name': 'submit' }, form);708goog.array.forEach(idMasks, function (m) {709m.removeAttribute('id');710});711goog.array.forEach(nameMasks, function (m) {712m.removeAttribute('name');713});714var submitFunction = form.submit;715goog.array.forEach(idMasks, function (m) {716m.setAttribute('id', 'submit');717});718goog.array.forEach(nameMasks, function (m) {719m.setAttribute('name', 'submit');720});721submitFunction();722}723}724};725726727/**728* Regular expression for splitting up a URL into components.729* @private {!RegExp}730* @const731*/732bot.Device.URL_REGEXP_ = new RegExp(733'^' +734'([^:/?#.]+:)?' + // protocol735'(?://([^/]*))?' + // host736'([^?#]+)?' + // pathname737'(\\?[^#]*)?' + // search738'(#.*)?' + // hash739'$');740741742/**743* Resolves a potentially relative URL against a base location.744* @param {!Location} base Base location against which to resolve.745* @param {string} rel Url to resolve against the location.746* @return {string} Resolution of url against base location.747* @private748*/749bot.Device.resolveUrl_ = function (base, rel) {750var m = rel.match(bot.Device.URL_REGEXP_);751if (!m) {752return '';753}754var target = {755protocol: m[1] || '',756host: m[2] || '',757pathname: m[3] || '',758search: m[4] || '',759hash: m[5] || ''760};761762if (!target.protocol) {763target.protocol = base.protocol;764if (!target.host) {765target.host = base.host;766if (!target.pathname) {767target.pathname = base.pathname;768target.search = target.search || base.search;769} else if (target.pathname.charAt(0) != '/') {770var lastSlashIndex = base.pathname.lastIndexOf('/');771if (lastSlashIndex != -1) {772var directory = base.pathname.substr(0, lastSlashIndex + 1);773target.pathname = directory + target.pathname;774}775}776}777}778779return target.protocol + '//' + target.host + target.pathname +780target.search + target.hash;781};782783784785/**786* Stores the state of modifier keys787*788* @constructor789*/790bot.Device.ModifiersState = function () {791/**792* State of the modifier keys.793* @private {number}794*/795this.pressedModifiers_ = 0;796};797798799/**800* An enum for the various modifier keys (keycode-independent).801* @enum {number}802*/803bot.Device.Modifier = {804SHIFT: 0x1,805CONTROL: 0x2,806ALT: 0x4,807META: 0x8808};809810811/**812* Checks whether a specific modifier is pressed813* @param {!bot.Device.Modifier} modifier The modifier to check.814* @return {boolean} Whether the modifier is pressed.815*/816bot.Device.ModifiersState.prototype.isPressed = function (modifier) {817return (this.pressedModifiers_ & modifier) != 0;818};819820821/**822* Sets the state of a given modifier.823* @param {!bot.Device.Modifier} modifier The modifier to set.824* @param {boolean} isPressed whether the modifier is set or released.825*/826bot.Device.ModifiersState.prototype.setPressed = function (827modifier, isPressed) {828if (isPressed) {829this.pressedModifiers_ = this.pressedModifiers_ | modifier;830} else {831this.pressedModifiers_ = this.pressedModifiers_ & (~modifier);832}833};834835836/**837* @return {boolean} State of the Shift key.838*/839bot.Device.ModifiersState.prototype.isShiftPressed = function () {840return this.isPressed(bot.Device.Modifier.SHIFT);841};842843844/**845* @return {boolean} State of the Control key.846*/847bot.Device.ModifiersState.prototype.isControlPressed = function () {848return this.isPressed(bot.Device.Modifier.CONTROL);849};850851852/**853* @return {boolean} State of the Alt key.854*/855bot.Device.ModifiersState.prototype.isAltPressed = function () {856return this.isPressed(bot.Device.Modifier.ALT);857};858859860/**861* @return {boolean} State of the Meta key.862*/863bot.Device.ModifiersState.prototype.isMetaPressed = function () {864return this.isPressed(bot.Device.Modifier.META);865};866867868/**869* The pointer id used for MSPointer events initiated through a mouse device.870* @type {number}871* @const872*/873bot.Device.MOUSE_MS_POINTER_ID = 1;874875876/**877* A map of pointer id to Elements.878* @private {!Object.<number, !Element>}879*/880bot.Device.pointerElementMap_ = {};881882883/**884* Gets the element associated with a pointer id.885* @param {number} pointerId The pointer Id.886* @return {?Element} The element associated with the pointer id.887* @protected888*/889bot.Device.getPointerElement = function (pointerId) {890return bot.Device.pointerElementMap_[pointerId];891};892893894/**895* Clear the pointer map.896* @protected897*/898bot.Device.clearPointerMap = function () {899bot.Device.pointerElementMap_ = {};900};901902903/**904* Fires events, a driver can replace it with a custom implementation905*906* @constructor907*/908bot.Device.EventEmitter = function () {909};910911912/**913* Fires an HTML event given the state of the device.914*915* @param {!Element} target The element on which to fire the event.916* @param {bot.events.EventType} type HTML Event type.917* @return {boolean} Whether the event fired successfully; false if cancelled.918* @protected919*/920bot.Device.EventEmitter.prototype.fireHtmlEvent = function (target, type) {921return bot.events.fire(target, type);922};923924925/**926* Fires a keyboard event given the state of the device and the given arguments.927*928* @param {!Element} target The element on which to fire the event.929* @param {bot.events.EventType} type Keyboard event type.930* @param {bot.events.KeyboardArgs} args Keyboard event arguments.931* @return {boolean} Whether the event fired successfully; false if cancelled.932* @protected933*/934bot.Device.EventEmitter.prototype.fireKeyboardEvent = function (935target, type, args) {936return bot.events.fire(target, type, args);937};938939940/**941* Fires a mouse event given the state of the device and the given arguments.942*943* @param {!Element} target The element on which to fire the event.944* @param {bot.events.EventType} type Mouse event type.945* @param {bot.events.MouseArgs} args Mouse event arguments.946* @return {boolean} Whether the event fired successfully; false if cancelled.947* @protected948*/949bot.Device.EventEmitter.prototype.fireMouseEvent = function (950target, type, args) {951return bot.events.fire(target, type, args);952};953954955/**956* Fires a mouse event given the state of the device and the given arguments.957*958* @param {!Element} target The element on which to fire the event.959* @param {bot.events.EventType} type Touch event type.960* @param {bot.events.TouchArgs} args Touch event arguments.961* @return {boolean} Whether the event fired successfully; false if cancelled.962* @protected963*/964bot.Device.EventEmitter.prototype.fireTouchEvent = function (965target, type, args) {966return bot.events.fire(target, type, args);967};968969970/**971* Fires an MSPointer event given the state of the device and the given972* arguments.973*974* @param {!Element} target The element on which to fire the event.975* @param {bot.events.EventType} type MSPointer event type.976* @param {bot.events.MSPointerArgs} args MSPointer event arguments.977* @return {boolean} Whether the event fired successfully; false if cancelled.978* @protected979*/980bot.Device.EventEmitter.prototype.fireMSPointerEvent = function (981target, type, args) {982return bot.events.fire(target, type, args);983};984985986