2015-04-04 09:53:59 -07:00
|
|
|
// Licensed to the Software Freedom Conservancy (SFC) under one
|
|
|
|
|
// or more contributor license agreements. See the NOTICE file
|
|
|
|
|
// distributed with this work for additional information
|
|
|
|
|
// regarding copyright ownership. The SFC licenses this file
|
|
|
|
|
// to you under the Apache License, Version 2.0 (the
|
|
|
|
|
// "License"); you may not use this file except in compliance
|
|
|
|
|
// with the License. You may obtain a copy of the License at
|
2014-11-12 19:33:37 +00:00
|
|
|
//
|
2015-04-04 09:53:59 -07:00
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
2014-11-12 19:33:37 +00:00
|
|
|
//
|
2015-04-04 09:53:59 -07:00
|
|
|
// Unless required by applicable law or agreed to in writing,
|
|
|
|
|
// software distributed under the License is distributed on an
|
|
|
|
|
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
|
|
|
// KIND, either express or implied. See the License for the
|
|
|
|
|
// specific language governing permissions and limitations
|
|
|
|
|
// under the License.
|
2011-12-06 11:44:44 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @fileoverview The file contains the base class for input devices such as
|
|
|
|
|
* the keyboard, mouse, and touchscreen.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
goog.provide('bot.Device');
|
2013-05-28 01:26:33 +04:00
|
|
|
goog.provide('bot.Device.EventEmitter');
|
2011-12-06 11:44:44 +00:00
|
|
|
|
|
|
|
|
goog.require('bot');
|
|
|
|
|
goog.require('bot.dom');
|
2018-11-25 13:21:40 +00:00
|
|
|
goog.require('bot.events');
|
2012-09-13 10:52:38 +00:00
|
|
|
goog.require('bot.locators');
|
2011-12-06 11:44:44 +00:00
|
|
|
goog.require('bot.userAgent');
|
2012-09-13 10:52:38 +00:00
|
|
|
goog.require('goog.array');
|
|
|
|
|
goog.require('goog.dom');
|
|
|
|
|
goog.require('goog.dom.TagName');
|
2012-04-26 17:55:18 +00:00
|
|
|
goog.require('goog.userAgent');
|
|
|
|
|
goog.require('goog.userAgent.product');
|
2011-12-06 11:44:44 +00:00
|
|
|
|
|
|
|
|
|
2012-11-08 16:22:32 +00:00
|
|
|
|
2011-12-06 11:44:44 +00:00
|
|
|
/**
|
|
|
|
|
* A Device class that provides common functionality for input devices.
|
2012-08-03 12:36:57 +00:00
|
|
|
* @param {bot.Device.ModifiersState=} opt_modifiersState state of modifier
|
|
|
|
|
* keys. The state is shared, not copied from this parameter.
|
2013-08-16 11:53:17 -07:00
|
|
|
* @param {bot.Device.EventEmitter=} opt_eventEmitter An object that should be
|
|
|
|
|
* used to fire events.
|
2011-12-06 11:44:44 +00:00
|
|
|
* @constructor
|
|
|
|
|
*/
|
2020-11-27 15:46:30 +00:00
|
|
|
bot.Device = function (opt_modifiersState, opt_eventEmitter) {
|
2011-12-06 11:44:44 +00:00
|
|
|
/**
|
|
|
|
|
* Element being interacted with.
|
2013-03-10 13:56:37 -07:00
|
|
|
* @private {!Element}
|
2011-12-06 11:44:44 +00:00
|
|
|
*/
|
|
|
|
|
this.element_ = bot.getDocument().documentElement;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* If the element is an option, this is its parent select element.
|
2013-03-10 13:56:37 -07:00
|
|
|
* @private {Element}
|
2011-12-06 11:44:44 +00:00
|
|
|
*/
|
|
|
|
|
this.select_ = null;
|
|
|
|
|
|
|
|
|
|
// If there is an active element, make that the current element instead.
|
|
|
|
|
var activeElement = bot.dom.getActiveElement(this.element_);
|
|
|
|
|
if (activeElement) {
|
|
|
|
|
this.setElement(activeElement);
|
|
|
|
|
}
|
2012-08-03 12:36:57 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* State of modifier keys for this device.
|
2013-06-15 16:19:51 -07:00
|
|
|
* @protected {bot.Device.ModifiersState}
|
2012-08-03 12:36:57 +00:00
|
|
|
*/
|
|
|
|
|
this.modifiersState = opt_modifiersState || new bot.Device.ModifiersState();
|
2013-05-28 01:26:33 +04:00
|
|
|
|
2013-06-15 16:19:51 -07:00
|
|
|
/** @protected {!bot.Device.EventEmitter} */
|
2013-05-28 01:26:33 +04:00
|
|
|
this.eventEmitter = opt_eventEmitter || new bot.Device.EventEmitter();
|
2011-12-06 11:44:44 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns the element with which the device is interacting.
|
|
|
|
|
*
|
|
|
|
|
* @return {!Element} Element being interacted with.
|
|
|
|
|
* @protected
|
|
|
|
|
*/
|
2020-11-27 15:46:30 +00:00
|
|
|
bot.Device.prototype.getElement = function () {
|
2011-12-06 11:44:44 +00:00
|
|
|
return this.element_;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Sets the element with which the device is interacting.
|
|
|
|
|
*
|
|
|
|
|
* @param {!Element} element Element being interacted with.
|
|
|
|
|
* @protected
|
|
|
|
|
*/
|
2020-11-27 15:46:30 +00:00
|
|
|
bot.Device.prototype.setElement = function (element) {
|
2011-12-06 11:44:44 +00:00
|
|
|
this.element_ = element;
|
|
|
|
|
if (bot.dom.isElement(element, goog.dom.TagName.OPTION)) {
|
|
|
|
|
this.select_ = /** @type {Element} */ (goog.dom.getAncestor(element,
|
2020-11-27 15:46:30 +00:00
|
|
|
function (node) {
|
|
|
|
|
return bot.dom.isElement(node, goog.dom.TagName.SELECT);
|
|
|
|
|
}));
|
2011-12-06 11:44:44 +00:00
|
|
|
} else {
|
|
|
|
|
this.select_ = null;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2012-09-13 10:52:38 +00:00
|
|
|
|
2011-12-06 11:44:44 +00:00
|
|
|
/**
|
|
|
|
|
* Fires an HTML event given the state of the device.
|
|
|
|
|
*
|
|
|
|
|
* @param {bot.events.EventType} type HTML Event type.
|
|
|
|
|
* @return {boolean} Whether the event fired successfully; false if cancelled.
|
|
|
|
|
* @protected
|
|
|
|
|
*/
|
2020-11-27 15:46:30 +00:00
|
|
|
bot.Device.prototype.fireHtmlEvent = function (type) {
|
2013-05-28 01:26:33 +04:00
|
|
|
return this.eventEmitter.fireHtmlEvent(this.element_, type);
|
2011-12-06 11:44:44 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Fires a keyboard event given the state of the device and the given arguments.
|
2013-08-16 11:53:17 -07:00
|
|
|
* TODO: Populate the modifier keys in this method.
|
2011-12-06 11:44:44 +00:00
|
|
|
*
|
|
|
|
|
* @param {bot.events.EventType} type Keyboard event type.
|
|
|
|
|
* @param {bot.events.KeyboardArgs} args Keyboard event arguments.
|
|
|
|
|
* @return {boolean} Whether the event fired successfully; false if cancelled.
|
|
|
|
|
* @protected
|
|
|
|
|
*/
|
2020-11-27 15:46:30 +00:00
|
|
|
bot.Device.prototype.fireKeyboardEvent = function (type, args) {
|
2013-05-28 01:26:33 +04:00
|
|
|
return this.eventEmitter.fireKeyboardEvent(this.element_, type, args);
|
2011-12-06 11:44:44 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Fires a mouse event given the state of the device and the given arguments.
|
2013-08-16 11:53:17 -07:00
|
|
|
* TODO: Populate the modifier keys in this method.
|
2011-12-06 11:44:44 +00:00
|
|
|
*
|
|
|
|
|
* @param {bot.events.EventType} type Mouse event type.
|
|
|
|
|
* @param {!goog.math.Coordinate} coord The coordinate where event will fire.
|
|
|
|
|
* @param {number} button The mouse button value for the event.
|
|
|
|
|
* @param {Element=} opt_related The related element of this event.
|
2012-11-08 16:22:32 +00:00
|
|
|
* @param {?number=} opt_wheelDelta The wheel delta value for the event.
|
|
|
|
|
* @param {boolean=} opt_force Whether the event should be fired even if the
|
|
|
|
|
* element is not interactable, such as the case of a mousemove or
|
|
|
|
|
* mouseover event that immediately follows a mouseout.
|
2013-06-15 16:19:51 -07:00
|
|
|
* @param {?number=} opt_pointerId The pointerId associated with the event.
|
2015-09-19 23:07:49 +03:00
|
|
|
* @param {?number=} opt_count Number of clicks that have been performed.
|
2011-12-06 11:44:44 +00:00
|
|
|
* @return {boolean} Whether the event fired successfully; false if cancelled.
|
|
|
|
|
* @protected
|
|
|
|
|
*/
|
2020-11-27 15:46:30 +00:00
|
|
|
bot.Device.prototype.fireMouseEvent = function (type, coord, button,
|
|
|
|
|
opt_related, opt_wheelDelta, opt_force, opt_pointerId, opt_count) {
|
2012-11-08 16:22:32 +00:00
|
|
|
if (!opt_force && !bot.dom.isInteractable(this.element_)) {
|
2011-12-06 11:44:44 +00:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (opt_related &&
|
2020-11-27 15:46:30 +00:00
|
|
|
!(bot.events.EventType.MOUSEOVER == type ||
|
|
|
|
|
bot.events.EventType.MOUSEOUT == type)) {
|
2011-12-06 11:44:44 +00:00
|
|
|
throw new bot.Error(bot.ErrorCode.INVALID_ELEMENT_STATE,
|
2020-11-27 15:46:30 +00:00
|
|
|
'Event type does not allow related target: ' + type);
|
2011-12-06 11:44:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var args = {
|
|
|
|
|
clientX: coord.x,
|
|
|
|
|
clientY: coord.y,
|
|
|
|
|
button: button,
|
2012-08-03 12:36:57 +00:00
|
|
|
altKey: this.modifiersState.isAltPressed(),
|
|
|
|
|
ctrlKey: this.modifiersState.isControlPressed(),
|
|
|
|
|
shiftKey: this.modifiersState.isShiftPressed(),
|
|
|
|
|
metaKey: this.modifiersState.isMetaPressed(),
|
2012-03-05 14:06:20 +00:00
|
|
|
wheelDelta: opt_wheelDelta || 0,
|
2015-09-19 23:07:49 +03:00
|
|
|
relatedTarget: opt_related || null,
|
|
|
|
|
count: opt_count || 1
|
2011-12-06 11:44:44 +00:00
|
|
|
};
|
|
|
|
|
|
2013-06-15 16:19:51 -07:00
|
|
|
var pointerId = opt_pointerId || bot.Device.MOUSE_MS_POINTER_ID;
|
|
|
|
|
|
|
|
|
|
var target = this.element_;
|
|
|
|
|
// On click and mousedown events, captured pointers are ignored and the
|
|
|
|
|
// event always fires on the original element.
|
|
|
|
|
if (type != bot.events.EventType.CLICK &&
|
2020-11-27 15:46:30 +00:00
|
|
|
type != bot.events.EventType.MOUSEDOWN &&
|
|
|
|
|
pointerId in bot.Device.pointerElementMap_) {
|
2013-06-15 16:19:51 -07:00
|
|
|
target = bot.Device.pointerElementMap_[pointerId];
|
|
|
|
|
} else if (this.select_) {
|
|
|
|
|
target = this.getTargetOfOptionMouseEvent_(type);
|
|
|
|
|
}
|
2013-05-28 01:26:33 +04:00
|
|
|
return target ? this.eventEmitter.fireMouseEvent(target, type, args) : true;
|
2011-12-06 11:44:44 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
2012-02-19 20:32:56 +00:00
|
|
|
/**
|
2020-08-16 00:03:35 +05:30
|
|
|
* Fires a touch event given the state of the device and the given arguments.
|
2012-02-19 20:32:56 +00:00
|
|
|
*
|
|
|
|
|
* @param {bot.events.EventType} type Event type.
|
|
|
|
|
* @param {number} id The touch identifier.
|
|
|
|
|
* @param {!goog.math.Coordinate} coord The coordinate where event will fire.
|
|
|
|
|
* @param {number=} opt_id2 The touch identifier of the second finger.
|
|
|
|
|
* @param {!goog.math.Coordinate=} opt_coord2 The coordinate of the second
|
|
|
|
|
* finger, if any.
|
|
|
|
|
* @return {boolean} Whether the event fired successfully or was cancelled.
|
|
|
|
|
* @protected
|
|
|
|
|
*/
|
2020-11-27 15:46:30 +00:00
|
|
|
bot.Device.prototype.fireTouchEvent = function (type, id, coord, opt_id2,
|
|
|
|
|
opt_coord2) {
|
2012-02-19 20:32:56 +00:00
|
|
|
var args = {
|
|
|
|
|
touches: [],
|
|
|
|
|
targetTouches: [],
|
|
|
|
|
changedTouches: [],
|
2012-08-03 12:36:57 +00:00
|
|
|
altKey: this.modifiersState.isAltPressed(),
|
|
|
|
|
ctrlKey: this.modifiersState.isControlPressed(),
|
|
|
|
|
shiftKey: this.modifiersState.isShiftPressed(),
|
|
|
|
|
metaKey: this.modifiersState.isMetaPressed(),
|
2012-02-19 20:32:56 +00:00
|
|
|
relatedTarget: null,
|
|
|
|
|
scale: 0,
|
|
|
|
|
rotation: 0
|
|
|
|
|
};
|
2015-11-17 15:56:23 -08:00
|
|
|
var pageOffset = goog.dom.getDomHelper(this.element_).getDocumentScroll();
|
2012-02-19 20:32:56 +00:00
|
|
|
|
|
|
|
|
function addTouch(identifier, coords) {
|
|
|
|
|
// Android devices leave identifier to zero.
|
|
|
|
|
var touch = {
|
|
|
|
|
identifier: identifier,
|
|
|
|
|
screenX: coords.x,
|
|
|
|
|
screenY: coords.y,
|
|
|
|
|
clientX: coords.x,
|
|
|
|
|
clientY: coords.y,
|
2015-11-17 15:56:23 -08:00
|
|
|
pageX: coords.x + pageOffset.x,
|
|
|
|
|
pageY: coords.y + pageOffset.y
|
2012-02-19 20:32:56 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
args.changedTouches.push(touch);
|
|
|
|
|
if (type == bot.events.EventType.TOUCHSTART ||
|
2020-11-27 15:46:30 +00:00
|
|
|
type == bot.events.EventType.TOUCHMOVE) {
|
2012-02-19 20:32:56 +00:00
|
|
|
args.touches.push(touch);
|
|
|
|
|
args.targetTouches.push(touch);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
addTouch(id, coord);
|
|
|
|
|
if (goog.isDef(opt_id2)) {
|
|
|
|
|
addTouch(opt_id2, opt_coord2);
|
|
|
|
|
}
|
|
|
|
|
|
2013-05-28 01:26:33 +04:00
|
|
|
return this.eventEmitter.fireTouchEvent(this.element_, type, args);
|
2012-02-19 20:32:56 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
2012-11-08 16:22:32 +00:00
|
|
|
/**
|
|
|
|
|
* Fires a MSPointer event given the state of the device and the given
|
|
|
|
|
* arguments.
|
|
|
|
|
*
|
|
|
|
|
* @param {bot.events.EventType} type MSPointer event type.
|
|
|
|
|
* @param {!goog.math.Coordinate} coord The coordinate where event will fire.
|
|
|
|
|
* @param {number} button The mouse button value for the event.
|
|
|
|
|
* @param {number} pointerId The pointer id for this event.
|
|
|
|
|
* @param {number} device The device type used for this event.
|
|
|
|
|
* @param {boolean} isPrimary Whether the pointer represents the primary point
|
|
|
|
|
* of contact.
|
|
|
|
|
* @param {Element=} opt_related The related element of this event.
|
|
|
|
|
* @param {boolean=} opt_force Whether the event should be fired even if the
|
|
|
|
|
* element is not interactable, such as the case of a mousemove or
|
|
|
|
|
* mouseover event that immediately follows a mouseout.
|
|
|
|
|
* @return {boolean} Whether the event fired successfully; false if cancelled.
|
|
|
|
|
* @protected
|
|
|
|
|
*/
|
2020-11-27 15:46:30 +00:00
|
|
|
bot.Device.prototype.fireMSPointerEvent = function (type, coord, button,
|
|
|
|
|
pointerId, device, isPrimary, opt_related, opt_force) {
|
2012-11-08 16:22:32 +00:00
|
|
|
if (!opt_force && !bot.dom.isInteractable(this.element_)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (opt_related &&
|
2020-11-27 15:46:30 +00:00
|
|
|
!(bot.events.EventType.MSPOINTEROVER == type ||
|
|
|
|
|
bot.events.EventType.MSPOINTEROUT == type)) {
|
2012-11-08 16:22:32 +00:00
|
|
|
throw new bot.Error(bot.ErrorCode.INVALID_ELEMENT_STATE,
|
2020-11-27 15:46:30 +00:00
|
|
|
'Event type does not allow related target: ' + type);
|
2012-11-08 16:22:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var args = {
|
|
|
|
|
clientX: coord.x,
|
|
|
|
|
clientY: coord.y,
|
|
|
|
|
button: button,
|
|
|
|
|
altKey: false,
|
|
|
|
|
ctrlKey: false,
|
|
|
|
|
shiftKey: false,
|
|
|
|
|
metaKey: false,
|
|
|
|
|
relatedTarget: opt_related || null,
|
|
|
|
|
width: 0,
|
|
|
|
|
height: 0,
|
|
|
|
|
pressure: 0, // Pressure is only given when a stylus is used.
|
|
|
|
|
rotation: 0,
|
|
|
|
|
pointerId: pointerId,
|
|
|
|
|
tiltX: 0,
|
|
|
|
|
tiltY: 0,
|
|
|
|
|
pointerType: device,
|
|
|
|
|
isPrimary: isPrimary
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var target = this.select_ ?
|
2020-11-27 15:46:30 +00:00
|
|
|
this.getTargetOfOptionMouseEvent_(type) : this.element_;
|
2013-06-15 16:19:51 -07:00
|
|
|
if (bot.Device.pointerElementMap_[pointerId]) {
|
|
|
|
|
target = bot.Device.pointerElementMap_[pointerId];
|
|
|
|
|
}
|
|
|
|
|
var owner = goog.dom.getWindow(goog.dom.getOwnerDocument(this.element_));
|
|
|
|
|
var originalMsSetPointerCapture;
|
|
|
|
|
if (owner && type == bot.events.EventType.MSPOINTERDOWN) {
|
|
|
|
|
// Overwrite msSetPointerCapture on the Element's msSetPointerCapture
|
|
|
|
|
// because synthetic pointer events cause an access denied exception.
|
|
|
|
|
// The prototype is modified because the pointer event will bubble up and
|
|
|
|
|
// we do not know which element will handle the pointer event.
|
|
|
|
|
originalMsSetPointerCapture =
|
2020-11-27 15:46:30 +00:00
|
|
|
owner['Element'].prototype.msSetPointerCapture;
|
|
|
|
|
owner['Element'].prototype.msSetPointerCapture = function (id) {
|
2013-06-15 16:19:51 -07:00
|
|
|
bot.Device.pointerElementMap_[id] = this;
|
|
|
|
|
};
|
|
|
|
|
}
|
2013-08-16 11:53:17 -07:00
|
|
|
var result =
|
2020-11-27 15:46:30 +00:00
|
|
|
target ? this.eventEmitter.fireMSPointerEvent(target, type, args) : true;
|
2013-06-15 16:19:51 -07:00
|
|
|
if (originalMsSetPointerCapture) {
|
|
|
|
|
owner['Element'].prototype.msSetPointerCapture =
|
2020-11-27 15:46:30 +00:00
|
|
|
originalMsSetPointerCapture;
|
2013-06-15 16:19:51 -07:00
|
|
|
}
|
|
|
|
|
return result;
|
2012-11-08 16:22:32 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
2011-12-06 11:44:44 +00:00
|
|
|
/**
|
2013-10-20 20:57:48 -07:00
|
|
|
* A mouse event fired "on" an option element, doesn't always fire on the
|
|
|
|
|
* option element itself. Sometimes it fires on the parent select element
|
2011-12-06 11:44:44 +00:00
|
|
|
* and sometimes not at all, depending on the browser and event type. This
|
|
|
|
|
* returns the true target element of the event, or null if none is fired.
|
|
|
|
|
*
|
|
|
|
|
* @param {bot.events.EventType} type Type of event.
|
|
|
|
|
* @return {Element} Element the event should be fired on, null if none.
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
2020-11-27 15:46:30 +00:00
|
|
|
bot.Device.prototype.getTargetOfOptionMouseEvent_ = function (type) {
|
2011-12-06 11:44:44 +00:00
|
|
|
// IE either fires the event on the parent select or not at all.
|
|
|
|
|
if (goog.userAgent.IE) {
|
|
|
|
|
switch (type) {
|
|
|
|
|
case bot.events.EventType.MOUSEOVER:
|
2012-11-08 16:22:32 +00:00
|
|
|
case bot.events.EventType.MSPOINTEROVER:
|
2011-12-06 11:44:44 +00:00
|
|
|
return null;
|
|
|
|
|
case bot.events.EventType.CONTEXTMENU:
|
|
|
|
|
case bot.events.EventType.MOUSEMOVE:
|
2012-11-08 16:22:32 +00:00
|
|
|
case bot.events.EventType.MSPOINTERMOVE:
|
2011-12-06 11:44:44 +00:00
|
|
|
return this.select_.multiple ? this.select_ : null;
|
|
|
|
|
default:
|
|
|
|
|
return this.select_;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// WebKit always fires on the option element of multi-selects.
|
|
|
|
|
// On single-selects, it either fires on the parent or not at all.
|
|
|
|
|
if (goog.userAgent.WEBKIT) {
|
|
|
|
|
switch (type) {
|
|
|
|
|
case bot.events.EventType.CLICK:
|
|
|
|
|
case bot.events.EventType.MOUSEUP:
|
|
|
|
|
return this.select_.multiple ? this.element_ : this.select_;
|
|
|
|
|
default:
|
|
|
|
|
return this.select_.multiple ? this.element_ : null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Firefox fires every event or the option element.
|
|
|
|
|
return this.element_;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A helper function to fire click events. This method is shared between
|
|
|
|
|
* the mouse and touchscreen devices.
|
|
|
|
|
*
|
|
|
|
|
* @param {!goog.math.Coordinate} coord The coordinate where event will fire.
|
|
|
|
|
* @param {number} button The mouse button value for the event.
|
2015-02-24 23:55:39 -08:00
|
|
|
* @param {boolean=} opt_force Whether the click should occur even if the
|
|
|
|
|
* element is not interactable, such as when an element is hidden by a
|
|
|
|
|
* mouseup handler.
|
2013-06-15 16:19:51 -07:00
|
|
|
* @param {?number=} opt_pointerId The pointer id associated with the click.
|
2011-12-06 11:44:44 +00:00
|
|
|
* @protected
|
|
|
|
|
*/
|
2020-11-27 15:46:30 +00:00
|
|
|
bot.Device.prototype.clickElement = function (coord, button, opt_force,
|
|
|
|
|
opt_pointerId) {
|
2015-02-24 23:55:39 -08:00
|
|
|
if (!opt_force && !bot.dom.isInteractable(this.element_)) {
|
2011-12-06 11:44:44 +00:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// bot.events.fire(element, 'click') can trigger all onclick events, but may
|
|
|
|
|
// not follow links (FORM.action or A.href).
|
2015-02-28 21:21:52 +01:00
|
|
|
// TAG IE GECKO WebKit
|
|
|
|
|
// A(href) No No Yes
|
|
|
|
|
// FORM(action) No Yes Yes
|
2011-12-06 11:44:44 +00:00
|
|
|
var targetLink = null;
|
|
|
|
|
var targetButton = null;
|
2012-05-04 12:18:31 +00:00
|
|
|
if (!bot.Device.ALWAYS_FOLLOWS_LINKS_ON_CLICK_) {
|
2011-12-06 11:44:44 +00:00
|
|
|
for (var e = this.element_; e; e = e.parentNode) {
|
|
|
|
|
if (bot.dom.isElement(e, goog.dom.TagName.A)) {
|
|
|
|
|
targetLink = /**@type {!Element}*/ (e);
|
|
|
|
|
break;
|
2012-02-19 20:40:52 +00:00
|
|
|
} else if (bot.Device.isFormSubmitElement(e)) {
|
2011-12-06 11:44:44 +00:00
|
|
|
targetButton = e;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// When an element is toggled as the result of a click, the toggling and the
|
2013-06-15 16:19:51 -07:00
|
|
|
// change event happens before the click event on some browsers. However, on
|
|
|
|
|
// radio buttons and checkboxes, the click handler can prevent the toggle from
|
|
|
|
|
// happening, so we must fire the click first to see if it is cancelled.
|
|
|
|
|
var isRadioOrCheckbox = !this.select_ && bot.dom.isSelectable(this.element_);
|
|
|
|
|
var wasChecked = isRadioOrCheckbox && bot.dom.isSelected(this.element_);
|
2011-12-06 11:44:44 +00:00
|
|
|
|
2013-08-16 11:53:17 -07:00
|
|
|
// NOTE: Clicking on a form submit button is a little broken:
|
2011-12-06 11:44:44 +00:00
|
|
|
// (1) When clicking a form submit button in IE, firing a click event or
|
|
|
|
|
// calling Form.submit() will not by itself submit the form, so we call
|
|
|
|
|
// Element.click() explicitly, but as a result, the coordinates of the click
|
|
|
|
|
// event are not provided. Also, when clicking on an <input type=image>, the
|
|
|
|
|
// coordinates click that are submitted with the form are always (0, 0).
|
|
|
|
|
// (2) When clicking a form submit button in GECKO, while the coordinates of
|
|
|
|
|
// the click event are correct, those submitted with the form are always (0,0)
|
|
|
|
|
// .
|
2013-08-16 11:53:17 -07:00
|
|
|
// TODO: See if either of these can be resolved, perhaps by adding
|
2011-12-06 11:44:44 +00:00
|
|
|
// hidden form elements with the coordinates before the form is submitted.
|
|
|
|
|
if (goog.userAgent.IE && targetButton) {
|
|
|
|
|
targetButton.click();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var performDefault = this.fireMouseEvent(
|
2020-11-27 15:46:30 +00:00
|
|
|
bot.events.EventType.CLICK, coord, button, null, 0, opt_force,
|
|
|
|
|
opt_pointerId);
|
2011-12-06 11:44:44 +00:00
|
|
|
if (!performDefault) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (targetLink && bot.Device.shouldFollowHref_(targetLink)) {
|
|
|
|
|
bot.Device.followHref_(targetLink);
|
2013-06-15 16:19:51 -07:00
|
|
|
} else if (isRadioOrCheckbox) {
|
|
|
|
|
this.toggleRadioButtonOrCheckbox_(wasChecked);
|
2011-12-06 11:44:44 +00:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Focuses on the given element and returns true if it supports being focused
|
|
|
|
|
* and does not already have focus; otherwise, returns false. If another element
|
|
|
|
|
* has focus, that element will be blurred before focusing on the given element.
|
|
|
|
|
*
|
|
|
|
|
* @return {boolean} Whether the element was given focus.
|
|
|
|
|
* @protected
|
|
|
|
|
*/
|
2020-11-27 15:46:30 +00:00
|
|
|
bot.Device.prototype.focusOnElement = function () {
|
2016-06-23 13:58:54 -07:00
|
|
|
var elementToFocus = goog.dom.getAncestor(
|
2020-11-27 15:46:30 +00:00
|
|
|
this.element_,
|
|
|
|
|
function (node) {
|
|
|
|
|
return !!node && bot.dom.isElement(node) &&
|
|
|
|
|
bot.dom.isFocusable(/** @type {!Element} */(node));
|
|
|
|
|
},
|
|
|
|
|
true /* Return this.element_ if it is focusable. */);
|
2016-06-23 13:58:54 -07:00
|
|
|
elementToFocus = elementToFocus || this.element_;
|
2011-12-06 11:44:44 +00:00
|
|
|
|
|
|
|
|
var activeElement = bot.dom.getActiveElement(elementToFocus);
|
|
|
|
|
if (elementToFocus == activeElement) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If there is a currently active element, try to blur it.
|
|
|
|
|
if (activeElement && (goog.isFunction(activeElement.blur) ||
|
2020-11-27 15:46:30 +00:00
|
|
|
// IE reports native functions as being objects.
|
|
|
|
|
goog.userAgent.IE && goog.isObject(activeElement.blur))) {
|
2011-12-06 11:44:44 +00:00
|
|
|
// In IE, the focus() and blur() functions fire their respective events
|
|
|
|
|
// asynchronously, and as the result, the focus/blur events fired by the
|
|
|
|
|
// the atoms actions will often be in the wrong order on IE. Firing a blur
|
|
|
|
|
// out of order sometimes causes IE to throw an "Unspecified error", so we
|
2013-01-11 22:18:32 +01:00
|
|
|
// wrap it in a try-catch and catch and ignore the error in this case.
|
2013-06-15 16:19:51 -07:00
|
|
|
if (!bot.dom.isElement(activeElement, goog.dom.TagName.BODY)) {
|
|
|
|
|
try {
|
2012-11-05 20:40:54 +00:00
|
|
|
activeElement.blur();
|
2013-06-15 16:19:51 -07:00
|
|
|
} catch (e) {
|
|
|
|
|
if (!(goog.userAgent.IE && e.message == 'Unspecified error.')) {
|
|
|
|
|
throw e;
|
|
|
|
|
}
|
2011-12-06 11:44:44 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sometimes IE6 and IE7 will not fire an onblur event after blur()
|
|
|
|
|
// is called, unless window.focus() is called immediately afterward.
|
|
|
|
|
// Note that IE8 will hit this branch unless the page is forced into
|
|
|
|
|
// IE8-strict mode. This shouldn't hurt anything, we just use the
|
|
|
|
|
// useragent sniff so we can compile this out for proper browsers.
|
2012-01-13 17:10:50 +00:00
|
|
|
if (goog.userAgent.IE && !bot.userAgent.isEngineVersion(8)) {
|
2011-12-06 11:44:44 +00:00
|
|
|
goog.dom.getWindow(goog.dom.getOwnerDocument(elementToFocus)).focus();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Try to focus on the element.
|
|
|
|
|
if (goog.isFunction(elementToFocus.focus) ||
|
2020-11-27 15:46:30 +00:00
|
|
|
goog.userAgent.IE && goog.isObject(elementToFocus.focus)) {
|
2015-02-28 21:21:52 +01:00
|
|
|
elementToFocus.focus();
|
2011-12-06 11:44:44 +00:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2012-05-04 12:18:31 +00:00
|
|
|
* Whether links must be manually followed when clicking (because firing click
|
|
|
|
|
* events doesn't follow them).
|
2013-03-10 13:56:37 -07:00
|
|
|
* @private {boolean}
|
2012-04-26 17:55:18 +00:00
|
|
|
* @const
|
|
|
|
|
*/
|
2021-02-19 16:01:03 +00:00
|
|
|
bot.Device.ALWAYS_FOLLOWS_LINKS_ON_CLICK_ = goog.userAgent.WEBKIT;
|
2011-12-06 11:44:44 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {Node} element The element to check.
|
|
|
|
|
* @return {boolean} Whether the element is a submit element in form.
|
2012-02-19 20:40:52 +00:00
|
|
|
* @protected
|
2011-12-06 11:44:44 +00:00
|
|
|
*/
|
2020-11-27 15:46:30 +00:00
|
|
|
bot.Device.isFormSubmitElement = function (element) {
|
2011-12-06 11:44:44 +00:00
|
|
|
if (bot.dom.isElement(element, goog.dom.TagName.INPUT)) {
|
|
|
|
|
var type = element.type.toLowerCase();
|
|
|
|
|
if (type == 'submit' || type == 'image') {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (bot.dom.isElement(element, goog.dom.TagName.BUTTON)) {
|
|
|
|
|
var type = element.type.toLowerCase();
|
|
|
|
|
if (type == 'submit') {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Indicates whether we should manually follow the href of the element we're
|
|
|
|
|
* clicking.
|
|
|
|
|
*
|
|
|
|
|
* Versions of firefox from 4+ will handle links properly when this is used in
|
|
|
|
|
* an extension. Versions of Firefox prior to this may or may not do the right
|
|
|
|
|
* thing depending on whether a target window is opened and whether the click
|
|
|
|
|
* has caused a change in just the hash part of the URL.
|
|
|
|
|
*
|
|
|
|
|
* @param {!Element} element The element to consider.
|
|
|
|
|
* @return {boolean} Whether following an href should be skipped.
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
2020-11-27 15:46:30 +00:00
|
|
|
bot.Device.shouldFollowHref_ = function (element) {
|
2012-05-04 12:18:31 +00:00
|
|
|
if (bot.Device.ALWAYS_FOLLOWS_LINKS_ON_CLICK_ || !element.href) {
|
2011-12-06 11:44:44 +00:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-19 16:01:03 +00:00
|
|
|
if (!(bot.userAgent.WEBEXTENSION)) {
|
2011-12-06 11:44:44 +00:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (element.target || element.href.toLowerCase().indexOf('javascript') == 0) {
|
2012-05-04 12:18:31 +00:00
|
|
|
return false;
|
2011-12-06 11:44:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var owner = goog.dom.getWindow(goog.dom.getOwnerDocument(element));
|
|
|
|
|
var sourceUrl = owner.location.href;
|
|
|
|
|
var destinationUrl = bot.Device.resolveUrl_(owner.location, element.href);
|
|
|
|
|
var isOnlyHashChange =
|
2020-11-27 15:46:30 +00:00
|
|
|
sourceUrl.split('#')[0] === destinationUrl.split('#')[0];
|
2011-12-06 11:44:44 +00:00
|
|
|
|
|
|
|
|
return !isOnlyHashChange;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Explicitly follows the href of an anchor.
|
|
|
|
|
*
|
|
|
|
|
* @param {!Element} anchorElement An anchor element.
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
2020-11-27 15:46:30 +00:00
|
|
|
bot.Device.followHref_ = function (anchorElement) {
|
2011-12-06 11:44:44 +00:00
|
|
|
var targetHref = anchorElement.href;
|
|
|
|
|
var owner = goog.dom.getWindow(goog.dom.getOwnerDocument(anchorElement));
|
|
|
|
|
|
|
|
|
|
// IE7 and earlier incorrect resolve a relative href against the top window
|
|
|
|
|
// location instead of the window to which the href is assigned. As a result,
|
|
|
|
|
// we have to resolve the relative URL ourselves. We do not use Closure's
|
|
|
|
|
// goog.Uri to resolve, because it incorrectly fails to support empty but
|
|
|
|
|
// undefined query and fragment components and re-encodes the given url.
|
2012-01-13 17:10:50 +00:00
|
|
|
if (goog.userAgent.IE && !bot.userAgent.isEngineVersion(8)) {
|
2011-12-06 11:44:44 +00:00
|
|
|
targetHref = bot.Device.resolveUrl_(owner.location, targetHref);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (anchorElement.target) {
|
|
|
|
|
owner.open(targetHref, anchorElement.target);
|
|
|
|
|
} else {
|
|
|
|
|
owner.location.href = targetHref;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2013-06-15 16:19:51 -07:00
|
|
|
* Toggles the selected state of the current element if it is an option. This
|
|
|
|
|
* is a noop if the element is not an option, or if it is selected and belongs
|
|
|
|
|
* to a single-select, because it can't be toggled off.
|
2011-12-06 11:44:44 +00:00
|
|
|
*
|
2013-06-15 16:19:51 -07:00
|
|
|
* @protected
|
2011-12-06 11:44:44 +00:00
|
|
|
*/
|
2020-11-27 15:46:30 +00:00
|
|
|
bot.Device.prototype.maybeToggleOption = function () {
|
2013-06-15 16:19:51 -07:00
|
|
|
// If this is not an <option> or not interactable, exit.
|
|
|
|
|
if (!this.select_ || !bot.dom.isInteractable(this.element_)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2011-12-06 11:44:44 +00:00
|
|
|
var select = /** @type {!Element} */ (this.select_);
|
2013-06-15 16:19:51 -07:00
|
|
|
var wasSelected = bot.dom.isSelected(this.element_);
|
2011-12-06 11:44:44 +00:00
|
|
|
// Cannot toggle off options in single-selects.
|
|
|
|
|
if (wasSelected && !select.multiple) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2013-08-16 11:53:17 -07:00
|
|
|
|
2015-04-03 18:04:10 -07:00
|
|
|
// TODO: In a multiselect, clicking an option without the ctrl key down
|
|
|
|
|
// should deselect all other selected options. Right now multiselect click
|
|
|
|
|
// works as ctrl+click should (and unit tests written so that they pass).
|
2013-08-16 11:53:17 -07:00
|
|
|
|
2011-12-06 11:44:44 +00:00
|
|
|
this.element_.selected = !wasSelected;
|
2012-04-26 17:55:18 +00:00
|
|
|
// Only WebKit fires the change event itself and only for multi-selects,
|
2013-08-16 11:53:17 -07:00
|
|
|
// except for Android versions >= 4.0 and Chrome >= 28.
|
2012-04-26 17:55:18 +00:00
|
|
|
if (!(goog.userAgent.WEBKIT && select.multiple) ||
|
2020-11-27 15:46:30 +00:00
|
|
|
(goog.userAgent.product.CHROME && bot.userAgent.isProductVersion(28)) ||
|
|
|
|
|
(goog.userAgent.product.ANDROID && bot.userAgent.isProductVersion(4))) {
|
2011-12-06 11:44:44 +00:00
|
|
|
bot.events.fire(select, bot.events.EventType.CHANGE);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2013-06-15 16:19:51 -07:00
|
|
|
* Toggles the checked state of a radio button or checkbox. This is a noop if
|
|
|
|
|
* it is a radio button that is checked, because it can't be toggled off.
|
2011-12-06 11:44:44 +00:00
|
|
|
*
|
2013-06-15 16:19:51 -07:00
|
|
|
* @param {boolean} wasChecked Whether the element was originally checked.
|
2011-12-06 11:44:44 +00:00
|
|
|
* @private
|
|
|
|
|
*/
|
2020-11-27 15:46:30 +00:00
|
|
|
bot.Device.prototype.toggleRadioButtonOrCheckbox_ = function (wasChecked) {
|
2011-12-06 11:44:44 +00:00
|
|
|
// Gecko and WebKit toggle the element as a result of a click.
|
|
|
|
|
if (goog.userAgent.GECKO || goog.userAgent.WEBKIT) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// Cannot toggle off radio buttons.
|
2013-06-15 16:19:51 -07:00
|
|
|
if (wasChecked && this.element_.type.toLowerCase() == 'radio') {
|
2011-12-06 11:44:44 +00:00
|
|
|
return;
|
|
|
|
|
}
|
2013-06-15 16:19:51 -07:00
|
|
|
this.element_.checked = !wasChecked;
|
2011-12-06 11:44:44 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
2012-02-19 20:40:52 +00:00
|
|
|
/**
|
|
|
|
|
* Find FORM element that is an ancestor of the passed in element.
|
|
|
|
|
* @param {Node} node The node to find a FORM for.
|
|
|
|
|
* @return {Element} The ancestor FORM element if it exists.
|
|
|
|
|
* @protected
|
|
|
|
|
*/
|
2020-11-27 15:46:30 +00:00
|
|
|
bot.Device.findAncestorForm = function (node) {
|
2013-06-13 17:16:23 -07:00
|
|
|
return /** @type {Element} */ (goog.dom.getAncestor(
|
2020-11-27 15:46:30 +00:00
|
|
|
node, bot.Device.isForm_, /*includeNode=*/true));
|
2012-02-19 20:40:52 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {Node} node The node to test.
|
|
|
|
|
* @return {boolean} Whether the node is a FORM element.
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
2020-11-27 15:46:30 +00:00
|
|
|
bot.Device.isForm_ = function (node) {
|
2012-02-19 20:40:52 +00:00
|
|
|
return bot.dom.isElement(node, goog.dom.TagName.FORM);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Submits the specified form. Unlike the public function, it expects to be
|
2013-10-20 20:57:48 -07:00
|
|
|
* given a form element and fails if it is not.
|
2012-02-19 20:40:52 +00:00
|
|
|
* @param {!Element} form The form to submit.
|
|
|
|
|
* @protected
|
|
|
|
|
*/
|
2020-11-27 15:46:30 +00:00
|
|
|
bot.Device.prototype.submitForm = function (form) {
|
2012-02-19 20:40:52 +00:00
|
|
|
if (!bot.Device.isForm_(form)) {
|
|
|
|
|
throw new bot.Error(bot.ErrorCode.INVALID_ELEMENT_STATE,
|
2020-11-27 15:46:30 +00:00
|
|
|
'Element is not a form, so could not submit.');
|
2012-02-19 20:40:52 +00:00
|
|
|
}
|
|
|
|
|
if (bot.events.fire(form, bot.events.EventType.SUBMIT)) {
|
|
|
|
|
// When a form has an element with an id or name exactly equal to "submit"
|
|
|
|
|
// (not uncommon) it masks the form.submit function. We can avoid this by
|
|
|
|
|
// calling the prototype's submit function, except in IE < 8, where DOM id
|
|
|
|
|
// elements don't let you reference their prototypes. For IE < 8, can change
|
|
|
|
|
// the id and names of the elements and revert them back, but they must be
|
|
|
|
|
// reverted before the submit call, because the onsubmit handler might rely
|
|
|
|
|
// on their being correct, and the HTTP request might otherwise be left with
|
|
|
|
|
// incorrect value names. Fortunately, saving the submit function and
|
|
|
|
|
// calling it after reverting the ids and names works! Oh, and goog.typeOf
|
|
|
|
|
// (and thus goog.isFunction) doesn't work for form.submit in IE < 8.
|
|
|
|
|
if (!bot.dom.isElement(form.submit)) {
|
|
|
|
|
form.submit();
|
|
|
|
|
} else if (!goog.userAgent.IE || bot.userAgent.isEngineVersion(8)) {
|
2013-06-15 16:19:51 -07:00
|
|
|
/** @type {Function} */ (form.constructor.prototype['submit']).call(form);
|
2012-02-19 20:40:52 +00:00
|
|
|
} else {
|
2020-11-27 15:46:30 +00:00
|
|
|
var idMasks = bot.locators.findElements({ 'id': 'submit' }, form);
|
|
|
|
|
var nameMasks = bot.locators.findElements({ 'name': 'submit' }, form);
|
|
|
|
|
goog.array.forEach(idMasks, function (m) {
|
2012-02-19 20:40:52 +00:00
|
|
|
m.removeAttribute('id');
|
|
|
|
|
});
|
2020-11-27 15:46:30 +00:00
|
|
|
goog.array.forEach(nameMasks, function (m) {
|
2012-02-19 20:40:52 +00:00
|
|
|
m.removeAttribute('name');
|
|
|
|
|
});
|
|
|
|
|
var submitFunction = form.submit;
|
2020-11-27 15:46:30 +00:00
|
|
|
goog.array.forEach(idMasks, function (m) {
|
2012-02-19 20:40:52 +00:00
|
|
|
m.setAttribute('id', 'submit');
|
|
|
|
|
});
|
2020-11-27 15:46:30 +00:00
|
|
|
goog.array.forEach(nameMasks, function (m) {
|
2012-02-19 20:40:52 +00:00
|
|
|
m.setAttribute('name', 'submit');
|
|
|
|
|
});
|
|
|
|
|
submitFunction();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2012-09-13 10:52:38 +00:00
|
|
|
|
2011-12-06 11:44:44 +00:00
|
|
|
/**
|
|
|
|
|
* Regular expression for splitting up a URL into components.
|
2013-03-10 13:56:37 -07:00
|
|
|
* @private {!RegExp}
|
2011-12-06 11:44:44 +00:00
|
|
|
* @const
|
|
|
|
|
*/
|
|
|
|
|
bot.Device.URL_REGEXP_ = new RegExp(
|
2020-11-27 15:46:30 +00:00
|
|
|
'^' +
|
|
|
|
|
'([^:/?#.]+:)?' + // protocol
|
|
|
|
|
'(?://([^/]*))?' + // host
|
|
|
|
|
'([^?#]+)?' + // pathname
|
|
|
|
|
'(\\?[^#]*)?' + // search
|
|
|
|
|
'(#.*)?' + // hash
|
|
|
|
|
'$');
|
2011-12-06 11:44:44 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Resolves a potentially relative URL against a base location.
|
|
|
|
|
* @param {!Location} base Base location against which to resolve.
|
|
|
|
|
* @param {string} rel Url to resolve against the location.
|
|
|
|
|
* @return {string} Resolution of url against base location.
|
|
|
|
|
* @private
|
|
|
|
|
*/
|
2020-11-27 15:46:30 +00:00
|
|
|
bot.Device.resolveUrl_ = function (base, rel) {
|
2011-12-06 11:44:44 +00:00
|
|
|
var m = rel.match(bot.Device.URL_REGEXP_);
|
|
|
|
|
if (!m) {
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
var target = {
|
|
|
|
|
protocol: m[1] || '',
|
|
|
|
|
host: m[2] || '',
|
|
|
|
|
pathname: m[3] || '',
|
|
|
|
|
search: m[4] || '',
|
|
|
|
|
hash: m[5] || ''
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (!target.protocol) {
|
|
|
|
|
target.protocol = base.protocol;
|
|
|
|
|
if (!target.host) {
|
|
|
|
|
target.host = base.host;
|
|
|
|
|
if (!target.pathname) {
|
|
|
|
|
target.pathname = base.pathname;
|
|
|
|
|
target.search = target.search || base.search;
|
|
|
|
|
} else if (target.pathname.charAt(0) != '/') {
|
|
|
|
|
var lastSlashIndex = base.pathname.lastIndexOf('/');
|
|
|
|
|
if (lastSlashIndex != -1) {
|
|
|
|
|
var directory = base.pathname.substr(0, lastSlashIndex + 1);
|
|
|
|
|
target.pathname = directory + target.pathname;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return target.protocol + '//' + target.host + target.pathname +
|
2020-11-27 15:46:30 +00:00
|
|
|
target.search + target.hash;
|
2011-12-06 11:44:44 +00:00
|
|
|
};
|
2012-08-03 12:36:57 +00:00
|
|
|
|
2012-10-05 10:57:29 +00:00
|
|
|
|
2012-11-08 16:22:32 +00:00
|
|
|
|
2012-08-03 12:36:57 +00:00
|
|
|
/**
|
|
|
|
|
* Stores the state of modifier keys
|
|
|
|
|
*
|
|
|
|
|
* @constructor
|
|
|
|
|
*/
|
2020-11-27 15:46:30 +00:00
|
|
|
bot.Device.ModifiersState = function () {
|
2012-08-03 12:36:57 +00:00
|
|
|
/**
|
|
|
|
|
* State of the modifier keys.
|
2013-03-10 13:56:37 -07:00
|
|
|
* @private {number}
|
2012-08-03 12:36:57 +00:00
|
|
|
*/
|
|
|
|
|
this.pressedModifiers_ = 0;
|
|
|
|
|
};
|
|
|
|
|
|
2012-10-05 10:57:29 +00:00
|
|
|
|
2012-08-03 12:36:57 +00:00
|
|
|
/**
|
|
|
|
|
* An enum for the various modifier keys (keycode-independent).
|
|
|
|
|
* @enum {number}
|
|
|
|
|
*/
|
|
|
|
|
bot.Device.Modifier = {
|
|
|
|
|
SHIFT: 0x1,
|
|
|
|
|
CONTROL: 0x2,
|
|
|
|
|
ALT: 0x4,
|
|
|
|
|
META: 0x8
|
|
|
|
|
};
|
|
|
|
|
|
2012-10-05 10:57:29 +00:00
|
|
|
|
2012-08-03 12:36:57 +00:00
|
|
|
/**
|
|
|
|
|
* Checks whether a specific modifier is pressed
|
|
|
|
|
* @param {!bot.Device.Modifier} modifier The modifier to check.
|
|
|
|
|
* @return {boolean} Whether the modifier is pressed.
|
|
|
|
|
*/
|
2020-11-27 15:46:30 +00:00
|
|
|
bot.Device.ModifiersState.prototype.isPressed = function (modifier) {
|
2012-08-03 12:36:57 +00:00
|
|
|
return (this.pressedModifiers_ & modifier) != 0;
|
|
|
|
|
};
|
|
|
|
|
|
2012-10-05 10:57:29 +00:00
|
|
|
|
2012-08-03 12:36:57 +00:00
|
|
|
/**
|
|
|
|
|
* Sets the state of a given modifier.
|
|
|
|
|
* @param {!bot.Device.Modifier} modifier The modifier to set.
|
|
|
|
|
* @param {boolean} isPressed whether the modifier is set or released.
|
|
|
|
|
*/
|
2020-11-27 15:46:30 +00:00
|
|
|
bot.Device.ModifiersState.prototype.setPressed = function (
|
|
|
|
|
modifier, isPressed) {
|
2012-08-03 12:36:57 +00:00
|
|
|
if (isPressed) {
|
|
|
|
|
this.pressedModifiers_ = this.pressedModifiers_ | modifier;
|
|
|
|
|
} else {
|
|
|
|
|
this.pressedModifiers_ = this.pressedModifiers_ & (~modifier);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2012-09-13 10:52:38 +00:00
|
|
|
|
2012-08-03 12:36:57 +00:00
|
|
|
/**
|
|
|
|
|
* @return {boolean} State of the Shift key.
|
|
|
|
|
*/
|
2020-11-27 15:46:30 +00:00
|
|
|
bot.Device.ModifiersState.prototype.isShiftPressed = function () {
|
2012-08-03 12:36:57 +00:00
|
|
|
return this.isPressed(bot.Device.Modifier.SHIFT);
|
|
|
|
|
};
|
|
|
|
|
|
2012-09-13 10:52:38 +00:00
|
|
|
|
2012-08-03 12:36:57 +00:00
|
|
|
/**
|
|
|
|
|
* @return {boolean} State of the Control key.
|
|
|
|
|
*/
|
2020-11-27 15:46:30 +00:00
|
|
|
bot.Device.ModifiersState.prototype.isControlPressed = function () {
|
2012-08-03 12:36:57 +00:00
|
|
|
return this.isPressed(bot.Device.Modifier.CONTROL);
|
|
|
|
|
};
|
|
|
|
|
|
2012-09-13 10:52:38 +00:00
|
|
|
|
2012-08-03 12:36:57 +00:00
|
|
|
/**
|
|
|
|
|
* @return {boolean} State of the Alt key.
|
|
|
|
|
*/
|
2020-11-27 15:46:30 +00:00
|
|
|
bot.Device.ModifiersState.prototype.isAltPressed = function () {
|
2012-08-03 12:36:57 +00:00
|
|
|
return this.isPressed(bot.Device.Modifier.ALT);
|
|
|
|
|
};
|
|
|
|
|
|
2012-09-13 10:52:38 +00:00
|
|
|
|
2012-08-03 12:36:57 +00:00
|
|
|
/**
|
|
|
|
|
* @return {boolean} State of the Meta key.
|
|
|
|
|
*/
|
2020-11-27 15:46:30 +00:00
|
|
|
bot.Device.ModifiersState.prototype.isMetaPressed = function () {
|
2012-08-03 12:36:57 +00:00
|
|
|
return this.isPressed(bot.Device.Modifier.META);
|
|
|
|
|
};
|
2013-05-28 01:26:33 +04:00
|
|
|
|
|
|
|
|
|
2013-06-15 16:19:51 -07:00
|
|
|
/**
|
|
|
|
|
* The pointer id used for MSPointer events initiated through a mouse device.
|
|
|
|
|
* @type {number}
|
|
|
|
|
* @const
|
|
|
|
|
*/
|
|
|
|
|
bot.Device.MOUSE_MS_POINTER_ID = 1;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A map of pointer id to Elements.
|
|
|
|
|
* @private {!Object.<number, !Element>}
|
|
|
|
|
*/
|
|
|
|
|
bot.Device.pointerElementMap_ = {};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Gets the element associated with a pointer id.
|
|
|
|
|
* @param {number} pointerId The pointer Id.
|
|
|
|
|
* @return {?Element} The element associated with the pointer id.
|
|
|
|
|
* @protected
|
|
|
|
|
*/
|
2020-11-27 15:46:30 +00:00
|
|
|
bot.Device.getPointerElement = function (pointerId) {
|
2013-06-15 16:19:51 -07:00
|
|
|
return bot.Device.pointerElementMap_[pointerId];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Clear the pointer map.
|
|
|
|
|
* @protected
|
|
|
|
|
*/
|
2020-11-27 15:46:30 +00:00
|
|
|
bot.Device.clearPointerMap = function () {
|
2013-06-15 16:19:51 -07:00
|
|
|
bot.Device.pointerElementMap_ = {};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
2013-05-28 01:26:33 +04:00
|
|
|
/**
|
|
|
|
|
* Fires events, a driver can replace it with a custom implementation
|
|
|
|
|
*
|
|
|
|
|
* @constructor
|
|
|
|
|
*/
|
2020-11-27 15:46:30 +00:00
|
|
|
bot.Device.EventEmitter = function () {
|
2013-05-28 01:26:33 +04:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Fires an HTML event given the state of the device.
|
|
|
|
|
*
|
|
|
|
|
* @param {!Element} target The element on which to fire the event.
|
|
|
|
|
* @param {bot.events.EventType} type HTML Event type.
|
|
|
|
|
* @return {boolean} Whether the event fired successfully; false if cancelled.
|
|
|
|
|
* @protected
|
|
|
|
|
*/
|
2020-11-27 15:46:30 +00:00
|
|
|
bot.Device.EventEmitter.prototype.fireHtmlEvent = function (target, type) {
|
2013-05-28 01:26:33 +04:00
|
|
|
return bot.events.fire(target, type);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Fires a keyboard event given the state of the device and the given arguments.
|
|
|
|
|
*
|
|
|
|
|
* @param {!Element} target The element on which to fire the event.
|
|
|
|
|
* @param {bot.events.EventType} type Keyboard event type.
|
|
|
|
|
* @param {bot.events.KeyboardArgs} args Keyboard event arguments.
|
|
|
|
|
* @return {boolean} Whether the event fired successfully; false if cancelled.
|
|
|
|
|
* @protected
|
|
|
|
|
*/
|
2020-11-27 15:46:30 +00:00
|
|
|
bot.Device.EventEmitter.prototype.fireKeyboardEvent = function (
|
|
|
|
|
target, type, args) {
|
2013-05-28 01:26:33 +04:00
|
|
|
return bot.events.fire(target, type, args);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Fires a mouse event given the state of the device and the given arguments.
|
|
|
|
|
*
|
|
|
|
|
* @param {!Element} target The element on which to fire the event.
|
|
|
|
|
* @param {bot.events.EventType} type Mouse event type.
|
|
|
|
|
* @param {bot.events.MouseArgs} args Mouse event arguments.
|
|
|
|
|
* @return {boolean} Whether the event fired successfully; false if cancelled.
|
|
|
|
|
* @protected
|
|
|
|
|
*/
|
2020-11-27 15:46:30 +00:00
|
|
|
bot.Device.EventEmitter.prototype.fireMouseEvent = function (
|
|
|
|
|
target, type, args) {
|
2013-05-28 01:26:33 +04:00
|
|
|
return bot.events.fire(target, type, args);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Fires a mouse event given the state of the device and the given arguments.
|
|
|
|
|
*
|
|
|
|
|
* @param {!Element} target The element on which to fire the event.
|
|
|
|
|
* @param {bot.events.EventType} type Touch event type.
|
|
|
|
|
* @param {bot.events.TouchArgs} args Touch event arguments.
|
|
|
|
|
* @return {boolean} Whether the event fired successfully; false if cancelled.
|
|
|
|
|
* @protected
|
|
|
|
|
*/
|
2020-11-27 15:46:30 +00:00
|
|
|
bot.Device.EventEmitter.prototype.fireTouchEvent = function (
|
|
|
|
|
target, type, args) {
|
2013-05-28 01:26:33 +04:00
|
|
|
return bot.events.fire(target, type, args);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2013-08-16 11:53:17 -07:00
|
|
|
* Fires an MSPointer event given the state of the device and the given
|
|
|
|
|
* arguments.
|
2013-05-28 01:26:33 +04:00
|
|
|
*
|
|
|
|
|
* @param {!Element} target The element on which to fire the event.
|
|
|
|
|
* @param {bot.events.EventType} type MSPointer event type.
|
|
|
|
|
* @param {bot.events.MSPointerArgs} args MSPointer event arguments.
|
|
|
|
|
* @return {boolean} Whether the event fired successfully; false if cancelled.
|
|
|
|
|
* @protected
|
|
|
|
|
*/
|
2020-11-27 15:46:30 +00:00
|
|
|
bot.Device.EventEmitter.prototype.fireMSPointerEvent = function (
|
|
|
|
|
target, type, args) {
|
2013-05-28 01:26:33 +04:00
|
|
|
return bot.events.fire(target, type, args);
|
|
|
|
|
};
|