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