Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
seleniumhq
GitHub Repository: seleniumhq/selenium
Path: blob/trunk/javascript/atoms/device.js
2884 views
1
// Licensed to the Software Freedom Conservancy (SFC) under one
2
// or more contributor license agreements. See the NOTICE file
3
// distributed with this work for additional information
4
// regarding copyright ownership. The SFC licenses this file
5
// to you under the Apache License, Version 2.0 (the
6
// "License"); you may not use this file except in compliance
7
// with the License. You may obtain a copy of the License at
8
//
9
// http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing,
12
// software distributed under the License is distributed on an
13
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
// KIND, either express or implied. See the License for the
15
// specific language governing permissions and limitations
16
// under the License.
17
18
/**
19
* @fileoverview The file contains the base class for input devices such as
20
* the keyboard, mouse, and touchscreen.
21
*/
22
23
goog.provide('bot.Device');
24
goog.provide('bot.Device.EventEmitter');
25
26
goog.require('bot');
27
goog.require('bot.dom');
28
goog.require('bot.events');
29
goog.require('bot.locators');
30
goog.require('bot.userAgent');
31
goog.require('goog.array');
32
goog.require('goog.dom');
33
goog.require('goog.dom.TagName');
34
goog.require('goog.userAgent');
35
goog.require('goog.userAgent.product');
36
37
38
39
/**
40
* A Device class that provides common functionality for input devices.
41
* @param {bot.Device.ModifiersState=} opt_modifiersState state of modifier
42
* keys. The state is shared, not copied from this parameter.
43
* @param {bot.Device.EventEmitter=} opt_eventEmitter An object that should be
44
* used to fire events.
45
* @constructor
46
*/
47
bot.Device = function (opt_modifiersState, opt_eventEmitter) {
48
/**
49
* Element being interacted with.
50
* @private {!Element}
51
*/
52
this.element_ = bot.getDocument().documentElement;
53
54
/**
55
* If the element is an option, this is its parent select element.
56
* @private {Element}
57
*/
58
this.select_ = null;
59
60
// If there is an active element, make that the current element instead.
61
var activeElement = bot.dom.getActiveElement(this.element_);
62
if (activeElement) {
63
this.setElement(activeElement);
64
}
65
66
/**
67
* State of modifier keys for this device.
68
* @protected {bot.Device.ModifiersState}
69
*/
70
this.modifiersState = opt_modifiersState || new bot.Device.ModifiersState();
71
72
/** @protected {!bot.Device.EventEmitter} */
73
this.eventEmitter = opt_eventEmitter || new bot.Device.EventEmitter();
74
};
75
76
77
/**
78
* Returns the element with which the device is interacting.
79
*
80
* @return {!Element} Element being interacted with.
81
* @protected
82
*/
83
bot.Device.prototype.getElement = function () {
84
return this.element_;
85
};
86
87
88
/**
89
* Sets the element with which the device is interacting.
90
*
91
* @param {!Element} element Element being interacted with.
92
* @protected
93
*/
94
bot.Device.prototype.setElement = function (element) {
95
this.element_ = element;
96
if (bot.dom.isElement(element, goog.dom.TagName.OPTION)) {
97
this.select_ = /** @type {Element} */ (goog.dom.getAncestor(element,
98
function (node) {
99
return bot.dom.isElement(node, goog.dom.TagName.SELECT);
100
}));
101
} else {
102
this.select_ = null;
103
}
104
};
105
106
107
/**
108
* Fires an HTML event given the state of the device.
109
*
110
* @param {bot.events.EventType} type HTML Event type.
111
* @return {boolean} Whether the event fired successfully; false if cancelled.
112
* @protected
113
*/
114
bot.Device.prototype.fireHtmlEvent = function (type) {
115
return this.eventEmitter.fireHtmlEvent(this.element_, type);
116
};
117
118
119
/**
120
* Fires a keyboard event given the state of the device and the given arguments.
121
* TODO: Populate the modifier keys in this method.
122
*
123
* @param {bot.events.EventType} type Keyboard event type.
124
* @param {bot.events.KeyboardArgs} args Keyboard event arguments.
125
* @return {boolean} Whether the event fired successfully; false if cancelled.
126
* @protected
127
*/
128
bot.Device.prototype.fireKeyboardEvent = function (type, args) {
129
return this.eventEmitter.fireKeyboardEvent(this.element_, type, args);
130
};
131
132
133
/**
134
* Fires a mouse event given the state of the device and the given arguments.
135
* TODO: Populate the modifier keys in this method.
136
*
137
* @param {bot.events.EventType} type Mouse event type.
138
* @param {!goog.math.Coordinate} coord The coordinate where event will fire.
139
* @param {number} button The mouse button value for the event.
140
* @param {Element=} opt_related The related element of this event.
141
* @param {?number=} opt_wheelDelta The wheel delta value for the event.
142
* @param {boolean=} opt_force Whether the event should be fired even if the
143
* element is not interactable, such as the case of a mousemove or
144
* mouseover event that immediately follows a mouseout.
145
* @param {?number=} opt_pointerId The pointerId associated with the event.
146
* @param {?number=} opt_count Number of clicks that have been performed.
147
* @return {boolean} Whether the event fired successfully; false if cancelled.
148
* @protected
149
*/
150
bot.Device.prototype.fireMouseEvent = function (type, coord, button,
151
opt_related, opt_wheelDelta, opt_force, opt_pointerId, opt_count) {
152
if (!opt_force && !bot.dom.isInteractable(this.element_)) {
153
return false;
154
}
155
156
if (opt_related &&
157
!(bot.events.EventType.MOUSEOVER == type ||
158
bot.events.EventType.MOUSEOUT == type)) {
159
throw new bot.Error(bot.ErrorCode.INVALID_ELEMENT_STATE,
160
'Event type does not allow related target: ' + type);
161
}
162
163
var args = {
164
clientX: coord.x,
165
clientY: coord.y,
166
button: button,
167
altKey: this.modifiersState.isAltPressed(),
168
ctrlKey: this.modifiersState.isControlPressed(),
169
shiftKey: this.modifiersState.isShiftPressed(),
170
metaKey: this.modifiersState.isMetaPressed(),
171
wheelDelta: opt_wheelDelta || 0,
172
relatedTarget: opt_related || null,
173
count: opt_count || 1
174
};
175
176
var pointerId = opt_pointerId || bot.Device.MOUSE_MS_POINTER_ID;
177
178
var target = this.element_;
179
// On click and mousedown events, captured pointers are ignored and the
180
// event always fires on the original element.
181
if (type != bot.events.EventType.CLICK &&
182
type != bot.events.EventType.MOUSEDOWN &&
183
pointerId in bot.Device.pointerElementMap_) {
184
target = bot.Device.pointerElementMap_[pointerId];
185
} else if (this.select_) {
186
target = this.getTargetOfOptionMouseEvent_(type);
187
}
188
return target ? this.eventEmitter.fireMouseEvent(target, type, args) : true;
189
};
190
191
192
/**
193
* Fires a touch event given the state of the device and the given arguments.
194
*
195
* @param {bot.events.EventType} type Event type.
196
* @param {number} id The touch identifier.
197
* @param {!goog.math.Coordinate} coord The coordinate where event will fire.
198
* @param {number=} opt_id2 The touch identifier of the second finger.
199
* @param {!goog.math.Coordinate=} opt_coord2 The coordinate of the second
200
* finger, if any.
201
* @return {boolean} Whether the event fired successfully or was cancelled.
202
* @protected
203
*/
204
bot.Device.prototype.fireTouchEvent = function (type, id, coord, opt_id2,
205
opt_coord2) {
206
var args = {
207
touches: [],
208
targetTouches: [],
209
changedTouches: [],
210
altKey: this.modifiersState.isAltPressed(),
211
ctrlKey: this.modifiersState.isControlPressed(),
212
shiftKey: this.modifiersState.isShiftPressed(),
213
metaKey: this.modifiersState.isMetaPressed(),
214
relatedTarget: null,
215
scale: 0,
216
rotation: 0
217
};
218
var pageOffset = goog.dom.getDomHelper(this.element_).getDocumentScroll();
219
220
function addTouch(identifier, coords) {
221
// Android devices leave identifier to zero.
222
var touch = {
223
identifier: identifier,
224
screenX: coords.x,
225
screenY: coords.y,
226
clientX: coords.x,
227
clientY: coords.y,
228
pageX: coords.x + pageOffset.x,
229
pageY: coords.y + pageOffset.y
230
};
231
232
args.changedTouches.push(touch);
233
if (type == bot.events.EventType.TOUCHSTART ||
234
type == bot.events.EventType.TOUCHMOVE) {
235
args.touches.push(touch);
236
args.targetTouches.push(touch);
237
}
238
}
239
240
addTouch(id, coord);
241
if (goog.isDef(opt_id2)) {
242
addTouch(opt_id2, opt_coord2);
243
}
244
245
return this.eventEmitter.fireTouchEvent(this.element_, type, args);
246
};
247
248
249
/**
250
* Fires a MSPointer event given the state of the device and the given
251
* arguments.
252
*
253
* @param {bot.events.EventType} type MSPointer event type.
254
* @param {!goog.math.Coordinate} coord The coordinate where event will fire.
255
* @param {number} button The mouse button value for the event.
256
* @param {number} pointerId The pointer id for this event.
257
* @param {number} device The device type used for this event.
258
* @param {boolean} isPrimary Whether the pointer represents the primary point
259
* of contact.
260
* @param {Element=} opt_related The related element of this event.
261
* @param {boolean=} opt_force Whether the event should be fired even if the
262
* element is not interactable, such as the case of a mousemove or
263
* mouseover event that immediately follows a mouseout.
264
* @return {boolean} Whether the event fired successfully; false if cancelled.
265
* @protected
266
*/
267
bot.Device.prototype.fireMSPointerEvent = function (type, coord, button,
268
pointerId, device, isPrimary, opt_related, opt_force) {
269
if (!opt_force && !bot.dom.isInteractable(this.element_)) {
270
return false;
271
}
272
273
if (opt_related &&
274
!(bot.events.EventType.MSPOINTEROVER == type ||
275
bot.events.EventType.MSPOINTEROUT == type)) {
276
throw new bot.Error(bot.ErrorCode.INVALID_ELEMENT_STATE,
277
'Event type does not allow related target: ' + type);
278
}
279
280
var args = {
281
clientX: coord.x,
282
clientY: coord.y,
283
button: button,
284
altKey: false,
285
ctrlKey: false,
286
shiftKey: false,
287
metaKey: false,
288
relatedTarget: opt_related || null,
289
width: 0,
290
height: 0,
291
pressure: 0, // Pressure is only given when a stylus is used.
292
rotation: 0,
293
pointerId: pointerId,
294
tiltX: 0,
295
tiltY: 0,
296
pointerType: device,
297
isPrimary: isPrimary
298
};
299
300
var target = this.select_ ?
301
this.getTargetOfOptionMouseEvent_(type) : this.element_;
302
if (bot.Device.pointerElementMap_[pointerId]) {
303
target = bot.Device.pointerElementMap_[pointerId];
304
}
305
var owner = goog.dom.getWindow(goog.dom.getOwnerDocument(this.element_));
306
var originalMsSetPointerCapture;
307
if (owner && type == bot.events.EventType.MSPOINTERDOWN) {
308
// Overwrite msSetPointerCapture on the Element's msSetPointerCapture
309
// because synthetic pointer events cause an access denied exception.
310
// The prototype is modified because the pointer event will bubble up and
311
// we do not know which element will handle the pointer event.
312
originalMsSetPointerCapture =
313
owner['Element'].prototype.msSetPointerCapture;
314
owner['Element'].prototype.msSetPointerCapture = function (id) {
315
bot.Device.pointerElementMap_[id] = this;
316
};
317
}
318
var result =
319
target ? this.eventEmitter.fireMSPointerEvent(target, type, args) : true;
320
if (originalMsSetPointerCapture) {
321
owner['Element'].prototype.msSetPointerCapture =
322
originalMsSetPointerCapture;
323
}
324
return result;
325
};
326
327
328
/**
329
* A mouse event fired "on" an option element, doesn't always fire on the
330
* option element itself. Sometimes it fires on the parent select element
331
* and sometimes not at all, depending on the browser and event type. This
332
* returns the true target element of the event, or null if none is fired.
333
*
334
* @param {bot.events.EventType} type Type of event.
335
* @return {Element} Element the event should be fired on, null if none.
336
* @private
337
*/
338
bot.Device.prototype.getTargetOfOptionMouseEvent_ = function (type) {
339
// IE either fires the event on the parent select or not at all.
340
if (goog.userAgent.IE) {
341
switch (type) {
342
case bot.events.EventType.MOUSEOVER:
343
case bot.events.EventType.MSPOINTEROVER:
344
return null;
345
case bot.events.EventType.CONTEXTMENU:
346
case bot.events.EventType.MOUSEMOVE:
347
case bot.events.EventType.MSPOINTERMOVE:
348
return this.select_.multiple ? this.select_ : null;
349
default:
350
return this.select_;
351
}
352
}
353
354
// WebKit always fires on the option element of multi-selects.
355
// On single-selects, it either fires on the parent or not at all.
356
if (goog.userAgent.WEBKIT) {
357
switch (type) {
358
case bot.events.EventType.CLICK:
359
case bot.events.EventType.MOUSEUP:
360
return this.select_.multiple ? this.element_ : this.select_;
361
default:
362
return this.select_.multiple ? this.element_ : null;
363
}
364
}
365
366
// Firefox fires every event or the option element.
367
return this.element_;
368
};
369
370
371
/**
372
* A helper function to fire click events. This method is shared between
373
* the mouse and touchscreen devices.
374
*
375
* @param {!goog.math.Coordinate} coord The coordinate where event will fire.
376
* @param {number} button The mouse button value for the event.
377
* @param {boolean=} opt_force Whether the click should occur even if the
378
* element is not interactable, such as when an element is hidden by a
379
* mouseup handler.
380
* @param {?number=} opt_pointerId The pointer id associated with the click.
381
* @protected
382
*/
383
bot.Device.prototype.clickElement = function (coord, button, opt_force,
384
opt_pointerId) {
385
if (!opt_force && !bot.dom.isInteractable(this.element_)) {
386
return;
387
}
388
389
// bot.events.fire(element, 'click') can trigger all onclick events, but may
390
// not follow links (FORM.action or A.href).
391
// TAG IE GECKO WebKit
392
// A(href) No No Yes
393
// FORM(action) No Yes Yes
394
var targetLink = null;
395
var targetButton = null;
396
if (!bot.Device.ALWAYS_FOLLOWS_LINKS_ON_CLICK_) {
397
for (var e = this.element_; e; e = e.parentNode) {
398
if (bot.dom.isElement(e, goog.dom.TagName.A)) {
399
targetLink = /**@type {!Element}*/ (e);
400
break;
401
} else if (bot.Device.isFormSubmitElement(e)) {
402
targetButton = e;
403
break;
404
}
405
}
406
}
407
408
// When an element is toggled as the result of a click, the toggling and the
409
// change event happens before the click event on some browsers. However, on
410
// radio buttons and checkboxes, the click handler can prevent the toggle from
411
// happening, so we must fire the click first to see if it is cancelled.
412
var isRadioOrCheckbox = !this.select_ && bot.dom.isSelectable(this.element_);
413
var wasChecked = isRadioOrCheckbox && bot.dom.isSelected(this.element_);
414
415
// NOTE: Clicking on a form submit button is a little broken:
416
// (1) When clicking a form submit button in IE, firing a click event or
417
// calling Form.submit() will not by itself submit the form, so we call
418
// Element.click() explicitly, but as a result, the coordinates of the click
419
// event are not provided. Also, when clicking on an <input type=image>, the
420
// coordinates click that are submitted with the form are always (0, 0).
421
// (2) When clicking a form submit button in GECKO, while the coordinates of
422
// the click event are correct, those submitted with the form are always (0,0)
423
// .
424
// TODO: See if either of these can be resolved, perhaps by adding
425
// hidden form elements with the coordinates before the form is submitted.
426
if (goog.userAgent.IE && targetButton) {
427
targetButton.click();
428
return;
429
}
430
431
var performDefault = this.fireMouseEvent(
432
bot.events.EventType.CLICK, coord, button, null, 0, opt_force,
433
opt_pointerId);
434
if (!performDefault) {
435
return;
436
}
437
438
if (targetLink && bot.Device.shouldFollowHref_(targetLink)) {
439
bot.Device.followHref_(targetLink);
440
} else if (isRadioOrCheckbox) {
441
this.toggleRadioButtonOrCheckbox_(wasChecked);
442
}
443
};
444
445
446
/**
447
* Focuses on the given element and returns true if it supports being focused
448
* and does not already have focus; otherwise, returns false. If another element
449
* has focus, that element will be blurred before focusing on the given element.
450
*
451
* @return {boolean} Whether the element was given focus.
452
* @protected
453
*/
454
bot.Device.prototype.focusOnElement = function () {
455
var elementToFocus = goog.dom.getAncestor(
456
this.element_,
457
function (node) {
458
return !!node && bot.dom.isElement(node) &&
459
bot.dom.isFocusable(/** @type {!Element} */(node));
460
},
461
true /* Return this.element_ if it is focusable. */);
462
elementToFocus = elementToFocus || this.element_;
463
464
var activeElement = bot.dom.getActiveElement(elementToFocus);
465
if (elementToFocus == activeElement) {
466
return false;
467
}
468
469
// If there is a currently active element, try to blur it.
470
if (activeElement && (goog.isFunction(activeElement.blur) ||
471
// IE reports native functions as being objects.
472
goog.userAgent.IE && goog.isObject(activeElement.blur))) {
473
// In IE, the focus() and blur() functions fire their respective events
474
// asynchronously, and as the result, the focus/blur events fired by the
475
// the atoms actions will often be in the wrong order on IE. Firing a blur
476
// out of order sometimes causes IE to throw an "Unspecified error", so we
477
// wrap it in a try-catch and catch and ignore the error in this case.
478
if (!bot.dom.isElement(activeElement, goog.dom.TagName.BODY)) {
479
try {
480
activeElement.blur();
481
} catch (e) {
482
if (!(goog.userAgent.IE && e.message == 'Unspecified error.')) {
483
throw e;
484
}
485
}
486
}
487
488
// Sometimes IE6 and IE7 will not fire an onblur event after blur()
489
// is called, unless window.focus() is called immediately afterward.
490
// Note that IE8 will hit this branch unless the page is forced into
491
// IE8-strict mode. This shouldn't hurt anything, we just use the
492
// useragent sniff so we can compile this out for proper browsers.
493
if (goog.userAgent.IE && !bot.userAgent.isEngineVersion(8)) {
494
goog.dom.getWindow(goog.dom.getOwnerDocument(elementToFocus)).focus();
495
}
496
}
497
498
// Try to focus on the element.
499
if (goog.isFunction(elementToFocus.focus) ||
500
goog.userAgent.IE && goog.isObject(elementToFocus.focus)) {
501
elementToFocus.focus();
502
return true;
503
}
504
505
return false;
506
};
507
508
509
/**
510
* Whether links must be manually followed when clicking (because firing click
511
* events doesn't follow them).
512
* @private {boolean}
513
* @const
514
*/
515
bot.Device.ALWAYS_FOLLOWS_LINKS_ON_CLICK_ = goog.userAgent.WEBKIT;
516
517
518
/**
519
* @param {Node} element The element to check.
520
* @return {boolean} Whether the element is a submit element in form.
521
* @protected
522
*/
523
bot.Device.isFormSubmitElement = function (element) {
524
if (bot.dom.isElement(element, goog.dom.TagName.INPUT)) {
525
var type = element.type.toLowerCase();
526
if (type == 'submit' || type == 'image') {
527
return true;
528
}
529
}
530
531
if (bot.dom.isElement(element, goog.dom.TagName.BUTTON)) {
532
var type = element.type.toLowerCase();
533
if (type == 'submit') {
534
return true;
535
}
536
}
537
return false;
538
};
539
540
541
/**
542
* Indicates whether we should manually follow the href of the element we're
543
* clicking.
544
*
545
* Versions of firefox from 4+ will handle links properly when this is used in
546
* an extension. Versions of Firefox prior to this may or may not do the right
547
* thing depending on whether a target window is opened and whether the click
548
* has caused a change in just the hash part of the URL.
549
*
550
* @param {!Element} element The element to consider.
551
* @return {boolean} Whether following an href should be skipped.
552
* @private
553
*/
554
bot.Device.shouldFollowHref_ = function (element) {
555
if (bot.Device.ALWAYS_FOLLOWS_LINKS_ON_CLICK_ || !element.href) {
556
return false;
557
}
558
559
if (!(bot.userAgent.WEBEXTENSION)) {
560
return true;
561
}
562
563
if (element.target || element.href.toLowerCase().indexOf('javascript') == 0) {
564
return false;
565
}
566
567
var owner = goog.dom.getWindow(goog.dom.getOwnerDocument(element));
568
var sourceUrl = owner.location.href;
569
var destinationUrl = bot.Device.resolveUrl_(owner.location, element.href);
570
var isOnlyHashChange =
571
sourceUrl.split('#')[0] === destinationUrl.split('#')[0];
572
573
return !isOnlyHashChange;
574
};
575
576
577
/**
578
* Explicitly follows the href of an anchor.
579
*
580
* @param {!Element} anchorElement An anchor element.
581
* @private
582
*/
583
bot.Device.followHref_ = function (anchorElement) {
584
var targetHref = anchorElement.href;
585
var owner = goog.dom.getWindow(goog.dom.getOwnerDocument(anchorElement));
586
587
// IE7 and earlier incorrect resolve a relative href against the top window
588
// location instead of the window to which the href is assigned. As a result,
589
// we have to resolve the relative URL ourselves. We do not use Closure's
590
// goog.Uri to resolve, because it incorrectly fails to support empty but
591
// undefined query and fragment components and re-encodes the given url.
592
if (goog.userAgent.IE && !bot.userAgent.isEngineVersion(8)) {
593
targetHref = bot.Device.resolveUrl_(owner.location, targetHref);
594
}
595
596
if (anchorElement.target) {
597
owner.open(targetHref, anchorElement.target);
598
} else {
599
owner.location.href = targetHref;
600
}
601
};
602
603
604
/**
605
* Toggles the selected state of the current element if it is an option. This
606
* is a noop if the element is not an option, or if it is selected and belongs
607
* to a single-select, because it can't be toggled off.
608
*
609
* @protected
610
*/
611
bot.Device.prototype.maybeToggleOption = function () {
612
// If this is not an <option> or not interactable, exit.
613
if (!this.select_ || !bot.dom.isInteractable(this.element_)) {
614
return;
615
}
616
var select = /** @type {!Element} */ (this.select_);
617
var wasSelected = bot.dom.isSelected(this.element_);
618
// Cannot toggle off options in single-selects.
619
if (wasSelected && !select.multiple) {
620
return;
621
}
622
623
// TODO: In a multiselect, clicking an option without the ctrl key down
624
// should deselect all other selected options. Right now multiselect click
625
// works as ctrl+click should (and unit tests written so that they pass).
626
627
this.element_.selected = !wasSelected;
628
// Only WebKit fires the change event itself and only for multi-selects,
629
// except for Android versions >= 4.0 and Chrome >= 28.
630
if (!(goog.userAgent.WEBKIT && select.multiple) ||
631
(goog.userAgent.product.CHROME && bot.userAgent.isProductVersion(28)) ||
632
(goog.userAgent.product.ANDROID && bot.userAgent.isProductVersion(4))) {
633
bot.events.fire(select, bot.events.EventType.CHANGE);
634
}
635
};
636
637
638
/**
639
* Toggles the checked state of a radio button or checkbox. This is a noop if
640
* it is a radio button that is checked, because it can't be toggled off.
641
*
642
* @param {boolean} wasChecked Whether the element was originally checked.
643
* @private
644
*/
645
bot.Device.prototype.toggleRadioButtonOrCheckbox_ = function (wasChecked) {
646
// Gecko and WebKit toggle the element as a result of a click.
647
if (goog.userAgent.GECKO || goog.userAgent.WEBKIT) {
648
return;
649
}
650
// Cannot toggle off radio buttons.
651
if (wasChecked && this.element_.type.toLowerCase() == 'radio') {
652
return;
653
}
654
this.element_.checked = !wasChecked;
655
};
656
657
658
/**
659
* Find FORM element that is an ancestor of the passed in element.
660
* @param {Node} node The node to find a FORM for.
661
* @return {Element} The ancestor FORM element if it exists.
662
* @protected
663
*/
664
bot.Device.findAncestorForm = function (node) {
665
return /** @type {Element} */ (goog.dom.getAncestor(
666
node, bot.Device.isForm_, /*includeNode=*/true));
667
};
668
669
670
/**
671
* @param {Node} node The node to test.
672
* @return {boolean} Whether the node is a FORM element.
673
* @private
674
*/
675
bot.Device.isForm_ = function (node) {
676
return bot.dom.isElement(node, goog.dom.TagName.FORM);
677
};
678
679
680
/**
681
* Submits the specified form. Unlike the public function, it expects to be
682
* given a form element and fails if it is not.
683
* @param {!Element} form The form to submit.
684
* @protected
685
*/
686
bot.Device.prototype.submitForm = function (form) {
687
if (!bot.Device.isForm_(form)) {
688
throw new bot.Error(bot.ErrorCode.INVALID_ELEMENT_STATE,
689
'Element is not a form, so could not submit.');
690
}
691
if (bot.events.fire(form, bot.events.EventType.SUBMIT)) {
692
// When a form has an element with an id or name exactly equal to "submit"
693
// (not uncommon) it masks the form.submit function. We can avoid this by
694
// calling the prototype's submit function, except in IE < 8, where DOM id
695
// elements don't let you reference their prototypes. For IE < 8, can change
696
// the id and names of the elements and revert them back, but they must be
697
// reverted before the submit call, because the onsubmit handler might rely
698
// on their being correct, and the HTTP request might otherwise be left with
699
// incorrect value names. Fortunately, saving the submit function and
700
// calling it after reverting the ids and names works! Oh, and goog.typeOf
701
// (and thus goog.isFunction) doesn't work for form.submit in IE < 8.
702
if (!bot.dom.isElement(form.submit)) {
703
form.submit();
704
} else if (!goog.userAgent.IE || bot.userAgent.isEngineVersion(8)) {
705
/** @type {Function} */ (form.constructor.prototype['submit']).call(form);
706
} else {
707
var idMasks = bot.locators.findElements({ 'id': 'submit' }, form);
708
var nameMasks = bot.locators.findElements({ 'name': 'submit' }, form);
709
goog.array.forEach(idMasks, function (m) {
710
m.removeAttribute('id');
711
});
712
goog.array.forEach(nameMasks, function (m) {
713
m.removeAttribute('name');
714
});
715
var submitFunction = form.submit;
716
goog.array.forEach(idMasks, function (m) {
717
m.setAttribute('id', 'submit');
718
});
719
goog.array.forEach(nameMasks, function (m) {
720
m.setAttribute('name', 'submit');
721
});
722
submitFunction();
723
}
724
}
725
};
726
727
728
/**
729
* Regular expression for splitting up a URL into components.
730
* @private {!RegExp}
731
* @const
732
*/
733
bot.Device.URL_REGEXP_ = new RegExp(
734
'^' +
735
'([^:/?#.]+:)?' + // protocol
736
'(?://([^/]*))?' + // host
737
'([^?#]+)?' + // pathname
738
'(\\?[^#]*)?' + // search
739
'(#.*)?' + // hash
740
'$');
741
742
743
/**
744
* Resolves a potentially relative URL against a base location.
745
* @param {!Location} base Base location against which to resolve.
746
* @param {string} rel Url to resolve against the location.
747
* @return {string} Resolution of url against base location.
748
* @private
749
*/
750
bot.Device.resolveUrl_ = function (base, rel) {
751
var m = rel.match(bot.Device.URL_REGEXP_);
752
if (!m) {
753
return '';
754
}
755
var target = {
756
protocol: m[1] || '',
757
host: m[2] || '',
758
pathname: m[3] || '',
759
search: m[4] || '',
760
hash: m[5] || ''
761
};
762
763
if (!target.protocol) {
764
target.protocol = base.protocol;
765
if (!target.host) {
766
target.host = base.host;
767
if (!target.pathname) {
768
target.pathname = base.pathname;
769
target.search = target.search || base.search;
770
} else if (target.pathname.charAt(0) != '/') {
771
var lastSlashIndex = base.pathname.lastIndexOf('/');
772
if (lastSlashIndex != -1) {
773
var directory = base.pathname.substr(0, lastSlashIndex + 1);
774
target.pathname = directory + target.pathname;
775
}
776
}
777
}
778
}
779
780
return target.protocol + '//' + target.host + target.pathname +
781
target.search + target.hash;
782
};
783
784
785
786
/**
787
* Stores the state of modifier keys
788
*
789
* @constructor
790
*/
791
bot.Device.ModifiersState = function () {
792
/**
793
* State of the modifier keys.
794
* @private {number}
795
*/
796
this.pressedModifiers_ = 0;
797
};
798
799
800
/**
801
* An enum for the various modifier keys (keycode-independent).
802
* @enum {number}
803
*/
804
bot.Device.Modifier = {
805
SHIFT: 0x1,
806
CONTROL: 0x2,
807
ALT: 0x4,
808
META: 0x8
809
};
810
811
812
/**
813
* Checks whether a specific modifier is pressed
814
* @param {!bot.Device.Modifier} modifier The modifier to check.
815
* @return {boolean} Whether the modifier is pressed.
816
*/
817
bot.Device.ModifiersState.prototype.isPressed = function (modifier) {
818
return (this.pressedModifiers_ & modifier) != 0;
819
};
820
821
822
/**
823
* Sets the state of a given modifier.
824
* @param {!bot.Device.Modifier} modifier The modifier to set.
825
* @param {boolean} isPressed whether the modifier is set or released.
826
*/
827
bot.Device.ModifiersState.prototype.setPressed = function (
828
modifier, isPressed) {
829
if (isPressed) {
830
this.pressedModifiers_ = this.pressedModifiers_ | modifier;
831
} else {
832
this.pressedModifiers_ = this.pressedModifiers_ & (~modifier);
833
}
834
};
835
836
837
/**
838
* @return {boolean} State of the Shift key.
839
*/
840
bot.Device.ModifiersState.prototype.isShiftPressed = function () {
841
return this.isPressed(bot.Device.Modifier.SHIFT);
842
};
843
844
845
/**
846
* @return {boolean} State of the Control key.
847
*/
848
bot.Device.ModifiersState.prototype.isControlPressed = function () {
849
return this.isPressed(bot.Device.Modifier.CONTROL);
850
};
851
852
853
/**
854
* @return {boolean} State of the Alt key.
855
*/
856
bot.Device.ModifiersState.prototype.isAltPressed = function () {
857
return this.isPressed(bot.Device.Modifier.ALT);
858
};
859
860
861
/**
862
* @return {boolean} State of the Meta key.
863
*/
864
bot.Device.ModifiersState.prototype.isMetaPressed = function () {
865
return this.isPressed(bot.Device.Modifier.META);
866
};
867
868
869
/**
870
* The pointer id used for MSPointer events initiated through a mouse device.
871
* @type {number}
872
* @const
873
*/
874
bot.Device.MOUSE_MS_POINTER_ID = 1;
875
876
877
/**
878
* A map of pointer id to Elements.
879
* @private {!Object.<number, !Element>}
880
*/
881
bot.Device.pointerElementMap_ = {};
882
883
884
/**
885
* Gets the element associated with a pointer id.
886
* @param {number} pointerId The pointer Id.
887
* @return {?Element} The element associated with the pointer id.
888
* @protected
889
*/
890
bot.Device.getPointerElement = function (pointerId) {
891
return bot.Device.pointerElementMap_[pointerId];
892
};
893
894
895
/**
896
* Clear the pointer map.
897
* @protected
898
*/
899
bot.Device.clearPointerMap = function () {
900
bot.Device.pointerElementMap_ = {};
901
};
902
903
904
/**
905
* Fires events, a driver can replace it with a custom implementation
906
*
907
* @constructor
908
*/
909
bot.Device.EventEmitter = function () {
910
};
911
912
913
/**
914
* Fires an HTML event given the state of the device.
915
*
916
* @param {!Element} target The element on which to fire the event.
917
* @param {bot.events.EventType} type HTML Event type.
918
* @return {boolean} Whether the event fired successfully; false if cancelled.
919
* @protected
920
*/
921
bot.Device.EventEmitter.prototype.fireHtmlEvent = function (target, type) {
922
return bot.events.fire(target, type);
923
};
924
925
926
/**
927
* Fires a keyboard event given the state of the device and the given arguments.
928
*
929
* @param {!Element} target The element on which to fire the event.
930
* @param {bot.events.EventType} type Keyboard event type.
931
* @param {bot.events.KeyboardArgs} args Keyboard event arguments.
932
* @return {boolean} Whether the event fired successfully; false if cancelled.
933
* @protected
934
*/
935
bot.Device.EventEmitter.prototype.fireKeyboardEvent = function (
936
target, type, args) {
937
return bot.events.fire(target, type, args);
938
};
939
940
941
/**
942
* Fires a mouse event given the state of the device and the given arguments.
943
*
944
* @param {!Element} target The element on which to fire the event.
945
* @param {bot.events.EventType} type Mouse event type.
946
* @param {bot.events.MouseArgs} args Mouse event arguments.
947
* @return {boolean} Whether the event fired successfully; false if cancelled.
948
* @protected
949
*/
950
bot.Device.EventEmitter.prototype.fireMouseEvent = function (
951
target, type, args) {
952
return bot.events.fire(target, type, args);
953
};
954
955
956
/**
957
* Fires a mouse event given the state of the device and the given arguments.
958
*
959
* @param {!Element} target The element on which to fire the event.
960
* @param {bot.events.EventType} type Touch event type.
961
* @param {bot.events.TouchArgs} args Touch event arguments.
962
* @return {boolean} Whether the event fired successfully; false if cancelled.
963
* @protected
964
*/
965
bot.Device.EventEmitter.prototype.fireTouchEvent = function (
966
target, type, args) {
967
return bot.events.fire(target, type, args);
968
};
969
970
971
/**
972
* Fires an MSPointer event given the state of the device and the given
973
* arguments.
974
*
975
* @param {!Element} target The element on which to fire the event.
976
* @param {bot.events.EventType} type MSPointer event type.
977
* @param {bot.events.MSPointerArgs} args MSPointer event arguments.
978
* @return {boolean} Whether the event fired successfully; false if cancelled.
979
* @protected
980
*/
981
bot.Device.EventEmitter.prototype.fireMSPointerEvent = function (
982
target, type, args) {
983
return bot.events.fire(target, type, args);
984
};
985
986