Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
seleniumhq
GitHub Repository: seleniumhq/selenium
Path: blob/trunk/javascript/atoms/keyboard.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 an abstraction of a keyboard
20
* for simulating the pressing and releasing of keys.
21
*/
22
23
goog.provide('bot.Keyboard');
24
goog.provide('bot.Keyboard.Key');
25
goog.provide('bot.Keyboard.Keys');
26
27
goog.require('bot.Device');
28
goog.require('bot.Error');
29
goog.require('bot.ErrorCode');
30
goog.require('bot.dom');
31
goog.require('bot.events.EventType');
32
goog.require('bot.userAgent');
33
goog.require('goog.array');
34
goog.require('goog.dom.TagName');
35
goog.require('goog.dom.selection');
36
goog.require('goog.structs.Map');
37
goog.require('goog.structs.Set');
38
goog.require('goog.userAgent');
39
40
41
42
/**
43
* A keyboard that provides atomic typing actions.
44
*
45
* @constructor
46
* @param {bot.Keyboard.State=} opt_state Optional keyboard state.
47
* @extends {bot.Device}
48
* @suppress {deprecated}
49
*/
50
bot.Keyboard = function (opt_state) {
51
goog.base(this);
52
53
/** @private {boolean} */
54
this.editable_ = bot.dom.isEditable(this.getElement());
55
56
/** @private {number} */
57
this.currentPos_ = 0;
58
59
/** @private {!goog.structs.Set.<!bot.Keyboard.Key>} */
60
this.pressed_ = new goog.structs.Set();
61
62
if (opt_state) {
63
// If a state is passed, let's assume we were passed an object with
64
// the correct properties.
65
goog.array.forEach(opt_state['pressed'], function (key) {
66
this.setKeyPressed_(/** @type {!bot.Keyboard.Key} */(key), true);
67
}, this);
68
69
this.currentPos_ = opt_state['currentPos'] || 0;
70
}
71
};
72
goog.inherits(bot.Keyboard, bot.Device);
73
74
75
/**
76
* Describes the current state of a keyboard.
77
* @typedef {{pressed: !Array.<!bot.Keyboard.Key>,
78
* currentPos: number}}
79
*/
80
bot.Keyboard.State;
81
82
83
/**
84
* Maps characters to (key,boolean) pairs, where the key generates the
85
* character and the boolean is true when the shift must be pressed.
86
* @private {!Object.<string, {key: !bot.Keyboard.Key, shift: boolean}>}
87
* @const
88
*/
89
bot.Keyboard.CHAR_TO_KEY_ = {};
90
91
92
/**
93
* Constructs a new key and, if it is a character key, adds a mapping from the
94
* character to is in the CHAR_TO_KEY_ map. Using this factory function instead
95
* of the new keyword, also helps reduce the size of the compiled Js fragment.
96
*
97
* @param {null|number|
98
* {gecko: (?number), ieWebkit: (?number)}} code
99
* Either a single keycode or a record of per-browser keycodes.
100
* @param {string=} opt_char Character when shift is not pressed.
101
* @param {string=} opt_shiftChar Character when shift is pressed.
102
* @return {!bot.Keyboard.Key} The new key.
103
* @private
104
*/
105
bot.Keyboard.newKey_ = function (code, opt_char, opt_shiftChar) {
106
if (goog.isObject(code)) {
107
if (goog.userAgent.GECKO) {
108
code = code.gecko;
109
} else { // IE and Webkit
110
code = code.ieWebkit;
111
}
112
}
113
var key = new bot.Keyboard.Key(code, opt_char, opt_shiftChar);
114
115
// For a character key, potentially map the character to the key in the
116
// CHAR_TO_KEY_ map. Because of numpad, multiple keys may have the same
117
// character. To avoid mapping numpad keys, we overwrite a mapping only if
118
// the key has a distinct shift character.
119
if (opt_char && (!(opt_char in bot.Keyboard.CHAR_TO_KEY_) || opt_shiftChar)) {
120
bot.Keyboard.CHAR_TO_KEY_[opt_char] = { key: key, shift: false };
121
if (opt_shiftChar) {
122
bot.Keyboard.CHAR_TO_KEY_[opt_shiftChar] = { key: key, shift: true };
123
}
124
}
125
126
return key;
127
};
128
129
130
131
/**
132
* A key on the keyboard.
133
*
134
* @constructor
135
* @param {?number} code Keycode for the key; null for the (rare) case
136
* that pressing the key issues no key events.
137
* @param {string=} opt_char Character when shift is not pressed; null
138
* when the key does not cause a character to be typed.
139
* @param {string=} opt_shiftChar Character when shift is pressed; null
140
* when the key does not cause a character to be typed.
141
*/
142
bot.Keyboard.Key = function (code, opt_char, opt_shiftChar) {
143
/** @type {?number} */
144
this.code = code;
145
146
/** @type {?string} */
147
this.character = opt_char || null;
148
149
/** @type {?string} */
150
this.shiftChar = opt_shiftChar || this.character;
151
};
152
153
154
/**
155
* An enumeration of keys known to this module.
156
*
157
* @enum {!bot.Keyboard.Key}
158
*/
159
bot.Keyboard.Keys = {
160
BACKSPACE: bot.Keyboard.newKey_(8),
161
TAB: bot.Keyboard.newKey_(9),
162
ENTER: bot.Keyboard.newKey_(13),
163
SHIFT: bot.Keyboard.newKey_(16),
164
CONTROL: bot.Keyboard.newKey_(17),
165
ALT: bot.Keyboard.newKey_(18),
166
PAUSE: bot.Keyboard.newKey_(19),
167
CAPS_LOCK: bot.Keyboard.newKey_(20),
168
ESC: bot.Keyboard.newKey_(27),
169
SPACE: bot.Keyboard.newKey_(32, ' '),
170
PAGE_UP: bot.Keyboard.newKey_(33),
171
PAGE_DOWN: bot.Keyboard.newKey_(34),
172
END: bot.Keyboard.newKey_(35),
173
HOME: bot.Keyboard.newKey_(36),
174
LEFT: bot.Keyboard.newKey_(37),
175
UP: bot.Keyboard.newKey_(38),
176
RIGHT: bot.Keyboard.newKey_(39),
177
DOWN: bot.Keyboard.newKey_(40),
178
PRINT_SCREEN: bot.Keyboard.newKey_(44),
179
INSERT: bot.Keyboard.newKey_(45),
180
DELETE: bot.Keyboard.newKey_(46),
181
182
// Number keys
183
ZERO: bot.Keyboard.newKey_(48, '0', ')'),
184
ONE: bot.Keyboard.newKey_(49, '1', '!'),
185
TWO: bot.Keyboard.newKey_(50, '2', '@'),
186
THREE: bot.Keyboard.newKey_(51, '3', '#'),
187
FOUR: bot.Keyboard.newKey_(52, '4', '$'),
188
FIVE: bot.Keyboard.newKey_(53, '5', '%'),
189
SIX: bot.Keyboard.newKey_(54, '6', '^'),
190
SEVEN: bot.Keyboard.newKey_(55, '7', '&'),
191
EIGHT: bot.Keyboard.newKey_(56, '8', '*'),
192
NINE: bot.Keyboard.newKey_(57, '9', '('),
193
194
// Letter keys
195
A: bot.Keyboard.newKey_(65, 'a', 'A'),
196
B: bot.Keyboard.newKey_(66, 'b', 'B'),
197
C: bot.Keyboard.newKey_(67, 'c', 'C'),
198
D: bot.Keyboard.newKey_(68, 'd', 'D'),
199
E: bot.Keyboard.newKey_(69, 'e', 'E'),
200
F: bot.Keyboard.newKey_(70, 'f', 'F'),
201
G: bot.Keyboard.newKey_(71, 'g', 'G'),
202
H: bot.Keyboard.newKey_(72, 'h', 'H'),
203
I: bot.Keyboard.newKey_(73, 'i', 'I'),
204
J: bot.Keyboard.newKey_(74, 'j', 'J'),
205
K: bot.Keyboard.newKey_(75, 'k', 'K'),
206
L: bot.Keyboard.newKey_(76, 'l', 'L'),
207
M: bot.Keyboard.newKey_(77, 'm', 'M'),
208
N: bot.Keyboard.newKey_(78, 'n', 'N'),
209
O: bot.Keyboard.newKey_(79, 'o', 'O'),
210
P: bot.Keyboard.newKey_(80, 'p', 'P'),
211
Q: bot.Keyboard.newKey_(81, 'q', 'Q'),
212
R: bot.Keyboard.newKey_(82, 'r', 'R'),
213
S: bot.Keyboard.newKey_(83, 's', 'S'),
214
T: bot.Keyboard.newKey_(84, 't', 'T'),
215
U: bot.Keyboard.newKey_(85, 'u', 'U'),
216
V: bot.Keyboard.newKey_(86, 'v', 'V'),
217
W: bot.Keyboard.newKey_(87, 'w', 'W'),
218
X: bot.Keyboard.newKey_(88, 'x', 'X'),
219
Y: bot.Keyboard.newKey_(89, 'y', 'Y'),
220
Z: bot.Keyboard.newKey_(90, 'z', 'Z'),
221
222
// Branded keys
223
META: bot.Keyboard.newKey_(
224
goog.userAgent.WINDOWS ? { gecko: 91, ieWebkit: 91 } :
225
(goog.userAgent.MAC ? { gecko: 224, ieWebkit: 91 } :
226
{ gecko: 0, ieWebkit: 91 })), // Linux
227
META_RIGHT: bot.Keyboard.newKey_(
228
goog.userAgent.WINDOWS ? { gecko: 92, ieWebkit: 92 } :
229
(goog.userAgent.MAC ? { gecko: 224, ieWebkit: 93 } :
230
{ gecko: 0, ieWebkit: 92 })), // Linux
231
CONTEXT_MENU: bot.Keyboard.newKey_(
232
goog.userAgent.WINDOWS ? { gecko: 93, ieWebkit: 93 } :
233
(goog.userAgent.MAC ? { gecko: 0, ieWebkit: 0 } :
234
{ gecko: 93, ieWebkit: null })), // Linux
235
236
// Numpad keys
237
NUM_ZERO: bot.Keyboard.newKey_({ gecko: 96, ieWebkit: 96 }, '0'),
238
NUM_ONE: bot.Keyboard.newKey_({ gecko: 97, ieWebkit: 97 }, '1'),
239
NUM_TWO: bot.Keyboard.newKey_({ gecko: 98, ieWebkit: 98 }, '2'),
240
NUM_THREE: bot.Keyboard.newKey_({ gecko: 99, ieWebkit: 99 }, '3'),
241
NUM_FOUR: bot.Keyboard.newKey_({ gecko: 100, ieWebkit: 100 }, '4'),
242
NUM_FIVE: bot.Keyboard.newKey_({ gecko: 101, ieWebkit: 101 }, '5'),
243
NUM_SIX: bot.Keyboard.newKey_({ gecko: 102, ieWebkit: 102 }, '6'),
244
NUM_SEVEN: bot.Keyboard.newKey_({ gecko: 103, ieWebkit: 103 }, '7'),
245
NUM_EIGHT: bot.Keyboard.newKey_({ gecko: 104, ieWebkit: 104 }, '8'),
246
NUM_NINE: bot.Keyboard.newKey_({ gecko: 105, ieWebkit: 105 }, '9'),
247
NUM_MULTIPLY: bot.Keyboard.newKey_(
248
{ gecko: 106, ieWebkit: 106 }, '*'),
249
NUM_PLUS: bot.Keyboard.newKey_(
250
{ gecko: 107, ieWebkit: 107 }, '+'),
251
NUM_MINUS: bot.Keyboard.newKey_(
252
{ gecko: 109, ieWebkit: 109 }, '-'),
253
NUM_PERIOD: bot.Keyboard.newKey_(
254
{ gecko: 110, ieWebkit: 110 }, '.'),
255
NUM_DIVISION: bot.Keyboard.newKey_(
256
{ gecko: 111, ieWebkit: 111 }, '/'),
257
NUM_LOCK: bot.Keyboard.newKey_(144),
258
259
// Function keys
260
F1: bot.Keyboard.newKey_(112),
261
F2: bot.Keyboard.newKey_(113),
262
F3: bot.Keyboard.newKey_(114),
263
F4: bot.Keyboard.newKey_(115),
264
F5: bot.Keyboard.newKey_(116),
265
F6: bot.Keyboard.newKey_(117),
266
F7: bot.Keyboard.newKey_(118),
267
F8: bot.Keyboard.newKey_(119),
268
F9: bot.Keyboard.newKey_(120),
269
F10: bot.Keyboard.newKey_(121),
270
F11: bot.Keyboard.newKey_(122),
271
F12: bot.Keyboard.newKey_(123),
272
273
// Punctuation keys
274
EQUALS: bot.Keyboard.newKey_(
275
{ gecko: 107, ieWebkit: 187 }, '=', '+'),
276
SEPARATOR: bot.Keyboard.newKey_(108, ','),
277
HYPHEN: bot.Keyboard.newKey_(
278
{ gecko: 109, ieWebkit: 189 }, '-', '_'),
279
COMMA: bot.Keyboard.newKey_(188, ',', '<'),
280
PERIOD: bot.Keyboard.newKey_(190, '.', '>'),
281
SLASH: bot.Keyboard.newKey_(191, '/', '?'),
282
BACKTICK: bot.Keyboard.newKey_(192, '`', '~'),
283
OPEN_BRACKET: bot.Keyboard.newKey_(219, '[', '{'),
284
BACKSLASH: bot.Keyboard.newKey_(220, '\\', '|'),
285
CLOSE_BRACKET: bot.Keyboard.newKey_(221, ']', '}'),
286
SEMICOLON: bot.Keyboard.newKey_(
287
{ gecko: 59, ieWebkit: 186 }, ';', ':'),
288
APOSTROPHE: bot.Keyboard.newKey_(222, '\'', '"')
289
};
290
291
292
/**
293
* Given a character, returns a pair of a key and a boolean: the key being one
294
* that types the character and the boolean indicating whether the key must be
295
* shifted to type it. This function will never return a numpad key; that is,
296
* it will always return a symbol key when given a number or math symbol.
297
*
298
* If given a character for which this module does not know the key (the key
299
* is not in the bot.Keyboard.Keys enumeration), returns a key that types the
300
* given character but has a (likely incorrect) keycode of zero.
301
*
302
* @param {string} ch Single character.
303
* @return {{key: !bot.Keyboard.Key, shift: boolean}} A pair of a key and
304
* a boolean indicating whether shift must be pressed for the character.
305
*/
306
bot.Keyboard.Key.fromChar = function (ch) {
307
if (ch.length != 1) {
308
throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,
309
'Argument not a single character: ' + ch);
310
}
311
var keyShiftPair = bot.Keyboard.CHAR_TO_KEY_[ch];
312
if (!keyShiftPair) {
313
// We don't know the true keycode of non-US keyboard characters, but
314
// ch.toUpperCase().charCodeAt(0) should occasionally be right, and
315
// at least yield a positive number.
316
var upperCase = ch.toUpperCase();
317
var keyCode = upperCase.charCodeAt(0);
318
var key = bot.Keyboard.newKey_(keyCode, ch.toLowerCase(), upperCase);
319
keyShiftPair = { key: key, shift: (ch != key.character) };
320
}
321
return keyShiftPair;
322
};
323
324
325
/**
326
* Array of modifier keys.
327
*
328
* @type {!Array.<!bot.Keyboard.Key>}
329
* @const
330
*/
331
bot.Keyboard.MODIFIERS = [
332
bot.Keyboard.Keys.ALT,
333
bot.Keyboard.Keys.CONTROL,
334
bot.Keyboard.Keys.META,
335
bot.Keyboard.Keys.SHIFT
336
];
337
338
339
/**
340
* Map of modifier to key.
341
* @private {!goog.structs.Map.<!bot.Device.Modifier, !bot.Keyboard.Key>}
342
* @suppress {deprecated}
343
*/
344
bot.Keyboard.MODIFIER_TO_KEY_MAP_ = (function () {
345
var modifiersMap = new goog.structs.Map();
346
modifiersMap.set(bot.Device.Modifier.SHIFT,
347
bot.Keyboard.Keys.SHIFT);
348
modifiersMap.set(bot.Device.Modifier.CONTROL,
349
bot.Keyboard.Keys.CONTROL);
350
modifiersMap.set(bot.Device.Modifier.ALT,
351
bot.Keyboard.Keys.ALT);
352
modifiersMap.set(bot.Device.Modifier.META,
353
bot.Keyboard.Keys.META);
354
355
return modifiersMap;
356
})();
357
358
359
/**
360
* The reverse map - key to modifier.
361
* @private {!goog.structs.Map.<number, !bot.Device.Modifier>}
362
* @suppress {deprecated}
363
*/
364
bot.Keyboard.KEY_TO_MODIFIER_ = (function (modifiersMap) {
365
var keyToModifierMap = new goog.structs.Map();
366
goog.array.forEach(modifiersMap.getKeys(), function (m) {
367
keyToModifierMap.set(modifiersMap.get(m).code, m);
368
});
369
370
return keyToModifierMap;
371
})(bot.Keyboard.MODIFIER_TO_KEY_MAP_);
372
373
374
/**
375
* Set the modifier state if the provided key is one, otherwise just add
376
* to the list of pressed keys.
377
* @param {!bot.Keyboard.Key} key The key to update.
378
* @param {boolean} isPressed Whether the key is pressed.
379
* @private
380
*/
381
bot.Keyboard.prototype.setKeyPressed_ = function (key, isPressed) {
382
if (goog.array.contains(bot.Keyboard.MODIFIERS, key)) {
383
var modifier = /** @type {bot.Device.Modifier}*/ (
384
bot.Keyboard.KEY_TO_MODIFIER_.get(key.code));
385
this.modifiersState.setPressed(modifier, isPressed);
386
}
387
388
if (isPressed) {
389
this.pressed_.add(key);
390
} else {
391
this.pressed_.remove(key);
392
}
393
};
394
395
396
/**
397
* The value used for newlines in the current browser/OS combination. Although
398
* the line endings look platform dependent, they are browser dependent.
399
*
400
* @private {string}
401
* @const
402
*/
403
bot.Keyboard.NEW_LINE_ = goog.userAgent.IE ? '\r\n' : '\n';
404
405
406
/**
407
* Returns whether the key is currently pressed.
408
*
409
* @param {!bot.Keyboard.Key} key Key.
410
* @return {boolean} Whether the key is pressed.
411
*/
412
bot.Keyboard.prototype.isPressed = function (key) {
413
return this.pressed_.contains(key);
414
};
415
416
417
/**
418
* Presses the given key on the keyboard. Keys that are pressed can be pressed
419
* again before releasing, to simulate repeated keys, except for modifier keys,
420
* which must be released before they can be pressed again.
421
*
422
* @param {!bot.Keyboard.Key} key Key to press.
423
*/
424
bot.Keyboard.prototype.pressKey = function (key) {
425
if (goog.array.contains(bot.Keyboard.MODIFIERS, key) && this.isPressed(key)) {
426
throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,
427
'Cannot press a modifier key that is already pressed.');
428
}
429
430
// Note that GECKO is special-cased below because of
431
// https://bugzilla.mozilla.org/show_bug.cgi?id=501496. "preventDefault on
432
// keydown does not cancel following keypress"
433
var performDefault = !goog.isNull(key.code) &&
434
this.fireKeyEvent_(bot.events.EventType.KEYDOWN, key);
435
436
// Fires keydown and stops if unsuccessful.
437
if (performDefault || goog.userAgent.GECKO) {
438
// Fires keypress if required and stops if unsuccessful.
439
if (!this.requiresKeyPress_(key) ||
440
this.fireKeyEvent_(
441
bot.events.EventType.KEYPRESS, key, !performDefault)) {
442
if (performDefault) {
443
this.maybeSubmitForm_(key);
444
if (this.editable_) {
445
this.maybeEditText_(key);
446
}
447
}
448
}
449
}
450
451
this.setKeyPressed_(key, true);
452
};
453
454
455
/**
456
* Whether the given key currently requires a keypress.
457
* TODO: Make this dependent on the state of the modifier keys.
458
*
459
* @param {bot.Keyboard.Key} key Key.
460
* @return {boolean} Whether it requires a keypress event.
461
* @private
462
*/
463
bot.Keyboard.prototype.requiresKeyPress_ = function (key) {
464
if (key.character || key == bot.Keyboard.Keys.ENTER) {
465
return true;
466
} else if (goog.userAgent.WEBKIT || goog.userAgent.EDGE) {
467
return false;
468
} else if (goog.userAgent.IE) {
469
return key == bot.Keyboard.Keys.ESC;
470
} else { // Gecko
471
switch (key) {
472
case bot.Keyboard.Keys.SHIFT:
473
case bot.Keyboard.Keys.CONTROL:
474
case bot.Keyboard.Keys.ALT:
475
return false;
476
case bot.Keyboard.Keys.META:
477
case bot.Keyboard.Keys.META_RIGHT:
478
case bot.Keyboard.Keys.CONTEXT_MENU:
479
return goog.userAgent.GECKO;
480
default:
481
return true;
482
}
483
}
484
};
485
486
487
/**
488
* Maybe submit a form if the ENTER key is released. On non-FF browsers, firing
489
* the keyPress and keyRelease events for the ENTER key does not result in a
490
* form being submitted so we have to fire the form submit event as well.
491
*
492
* @param {bot.Keyboard.Key} key Key.
493
* @private
494
*/
495
bot.Keyboard.prototype.maybeSubmitForm_ = function (key) {
496
if (key != bot.Keyboard.Keys.ENTER) {
497
return;
498
}
499
if (goog.userAgent.GECKO ||
500
!bot.dom.isElement(this.getElement(), goog.dom.TagName.INPUT)) {
501
return;
502
}
503
504
var form = bot.Device.findAncestorForm(this.getElement());
505
if (form) {
506
var inputs = form.getElementsByTagName('input');
507
var hasSubmit = goog.array.some(inputs, function (e) {
508
return bot.Device.isFormSubmitElement(e);
509
});
510
// The second part of this if statement will always include forms on Safari
511
// version < 5.
512
if (hasSubmit || inputs.length == 1 ||
513
(goog.userAgent.WEBKIT && !bot.userAgent.isEngineVersion(534))) {
514
this.submitForm(form);
515
}
516
}
517
};
518
519
520
/**
521
* Maybe edit text when a key is pressed in an editable form.
522
*
523
* @param {!bot.Keyboard.Key} key Key that was pressed.
524
* @private
525
*/
526
bot.Keyboard.prototype.maybeEditText_ = function (key) {
527
if (key.character) {
528
this.updateOnCharacter_(key);
529
} else {
530
switch (key) {
531
case bot.Keyboard.Keys.ENTER:
532
this.updateOnEnter_();
533
break;
534
case bot.Keyboard.Keys.BACKSPACE:
535
case bot.Keyboard.Keys.DELETE:
536
this.updateOnBackspaceOrDelete_(key);
537
break;
538
case bot.Keyboard.Keys.LEFT:
539
case bot.Keyboard.Keys.RIGHT:
540
this.updateOnLeftOrRight_(key);
541
break;
542
case bot.Keyboard.Keys.HOME:
543
case bot.Keyboard.Keys.END:
544
this.updateOnHomeOrEnd_(key);
545
break;
546
}
547
}
548
};
549
550
551
/**
552
* Releases the given key on the keyboard. Releasing a key that is not
553
* pressed results in an exception.
554
*
555
* @param {!bot.Keyboard.Key} key Key to release.
556
*/
557
bot.Keyboard.prototype.releaseKey = function (key) {
558
if (!this.isPressed(key)) {
559
throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,
560
'Cannot release a key that is not pressed. (' + key.code + ')');
561
}
562
if (!goog.isNull(key.code)) {
563
this.fireKeyEvent_(bot.events.EventType.KEYUP, key);
564
}
565
566
this.setKeyPressed_(key, false);
567
};
568
569
570
/**
571
* Given the current state of the SHIFT and CAPS_LOCK key, returns the
572
* character that will be typed is the specified key is pressed.
573
*
574
* @param {!bot.Keyboard.Key} key Key.
575
* @return {string} Character to be typed.
576
* @private
577
*/
578
bot.Keyboard.prototype.getChar_ = function (key) {
579
if (!key.character) {
580
throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR, 'not a character key');
581
}
582
var shiftPressed = this.isPressed(bot.Keyboard.Keys.SHIFT);
583
return /** @type {string} */ (shiftPressed ? key.shiftChar : key.character);
584
};
585
586
587
/**
588
* Whether firing a keypress event causes text to be edited without any
589
* additional logic to surgically apply the edit.
590
* @private {boolean}
591
* @const
592
*/
593
bot.Keyboard.KEYPRESS_EDITS_TEXT_ = goog.userAgent.GECKO &&
594
!bot.userAgent.isEngineVersion(12);
595
596
597
/**
598
* @param {!bot.Keyboard.Key} key Key with character to insert.
599
* @private
600
*/
601
bot.Keyboard.prototype.updateOnCharacter_ = function (key) {
602
if (bot.Keyboard.KEYPRESS_EDITS_TEXT_) {
603
return;
604
}
605
606
var character = this.getChar_(key);
607
var newPos = goog.dom.selection.getStart(this.getElement()) + 1;
608
if (bot.Keyboard.supportsSelection(this.getElement())) {
609
goog.dom.selection.setText(this.getElement(), character);
610
goog.dom.selection.setStart(this.getElement(), newPos);
611
} else {
612
this.getElement().value += character;
613
}
614
if (goog.userAgent.WEBKIT) {
615
this.fireHtmlEvent(bot.events.EventType.TEXTINPUT);
616
}
617
if (!bot.userAgent.IE_DOC_PRE9) {
618
this.fireHtmlEvent(bot.events.EventType.INPUT);
619
}
620
this.updateCurrentPos_(newPos);
621
};
622
623
624
/** @private */
625
bot.Keyboard.prototype.updateOnEnter_ = function () {
626
if (bot.Keyboard.KEYPRESS_EDITS_TEXT_) {
627
return;
628
}
629
630
// WebKit fires text input regardless of whether a new line is added, see:
631
// https://bugs.webkit.org/show_bug.cgi?id=54152
632
if (goog.userAgent.WEBKIT) {
633
this.fireHtmlEvent(bot.events.EventType.TEXTINPUT);
634
}
635
if (bot.dom.isElement(this.getElement(), goog.dom.TagName.TEXTAREA)) {
636
var newPos = goog.dom.selection.getStart(this.getElement()) +
637
bot.Keyboard.NEW_LINE_.length;
638
if (bot.Keyboard.supportsSelection(this.getElement())) {
639
goog.dom.selection.setText(this.getElement(), bot.Keyboard.NEW_LINE_);
640
goog.dom.selection.setStart(this.getElement(), newPos);
641
} else {
642
this.getElement().value += bot.Keyboard.NEW_LINE_;
643
}
644
if (!goog.userAgent.IE) {
645
this.fireHtmlEvent(bot.events.EventType.INPUT);
646
}
647
this.updateCurrentPos_(newPos);
648
}
649
};
650
651
652
/**
653
* @param {!bot.Keyboard.Key} key Backspace or delete key.
654
* @private
655
*/
656
bot.Keyboard.prototype.updateOnBackspaceOrDelete_ = function (key) {
657
if (bot.Keyboard.KEYPRESS_EDITS_TEXT_) {
658
return;
659
}
660
661
// Determine what should be deleted. If text is already selected, that
662
// text is deleted, else we move left/right from the current cursor.
663
bot.Keyboard.checkCanUpdateSelection_(this.getElement());
664
var endpoints = goog.dom.selection.getEndPoints(this.getElement());
665
if (endpoints[0] == endpoints[1]) {
666
if (key == bot.Keyboard.Keys.BACKSPACE) {
667
goog.dom.selection.setStart(this.getElement(), endpoints[1] - 1);
668
// On IE, changing goog.dom.selection.setStart also changes the end.
669
goog.dom.selection.setEnd(this.getElement(), endpoints[1]);
670
} else {
671
goog.dom.selection.setEnd(this.getElement(), endpoints[1] + 1);
672
}
673
}
674
675
// If the endpoints are equal (e.g., the cursor was at the beginning/end
676
// of the input), the text field won't be changed.
677
endpoints = goog.dom.selection.getEndPoints(this.getElement());
678
var textChanged = !(endpoints[0] == this.getElement().value.length ||
679
endpoints[1] == 0);
680
goog.dom.selection.setText(this.getElement(), '');
681
682
// Except for IE and GECKO, we need to fire the input event manually, but
683
// only if the text was actually changed.
684
// Note: Gecko has some strange behavior with the input event. In a
685
// textarea, backspace always sends an input event, while delete only
686
// sends one if you actually change the text.
687
// In a textbox/password box, backspace always sends an input event unless
688
// the box has no text. Delete behaves the same way in Firefox 3.0, but
689
// in later versions it only fires an input event if no text changes.
690
if (!goog.userAgent.IE && textChanged ||
691
(goog.userAgent.GECKO && key == bot.Keyboard.Keys.BACKSPACE)) {
692
this.fireHtmlEvent(bot.events.EventType.INPUT);
693
}
694
695
// Update the cursor position
696
endpoints = goog.dom.selection.getEndPoints(this.getElement());
697
this.updateCurrentPos_(endpoints[1]);
698
};
699
700
701
/**
702
* @param {!bot.Keyboard.Key} key Special key to press.
703
* @private
704
*/
705
bot.Keyboard.prototype.updateOnLeftOrRight_ = function (key) {
706
bot.Keyboard.checkCanUpdateSelection_(this.getElement());
707
var element = this.getElement();
708
var start = goog.dom.selection.getStart(element);
709
var end = goog.dom.selection.getEnd(element);
710
711
var newPos, startPos = 0, endPos = 0;
712
if (key == bot.Keyboard.Keys.LEFT) {
713
if (this.isPressed(bot.Keyboard.Keys.SHIFT)) {
714
// If the current position of the cursor is at the start of the
715
// selection, pressing left expands the selection one character to the
716
// left; otherwise, pressing left collapses it one character to the
717
// left.
718
if (this.currentPos_ == start) {
719
// Never attempt to move further left than the beginning of the text.
720
startPos = Math.max(start - 1, 0);
721
endPos = end;
722
newPos = startPos;
723
} else {
724
startPos = start;
725
endPos = end - 1;
726
newPos = endPos;
727
}
728
} else {
729
// With no current selection, pressing left moves the cursor one
730
// character to the left; with an existing selection, it collapses the
731
// selection to the beginning of the selection.
732
newPos = start == end ? Math.max(start - 1, 0) : start;
733
}
734
} else { // (key == bot.Keyboard.Keys.RIGHT)
735
if (this.isPressed(bot.Keyboard.Keys.SHIFT)) {
736
// If the current position of the cursor is at the end of the selection,
737
// pressing right expands the selection one character to the right;
738
// otherwise, pressing right collapses it one character to the right.
739
if (this.currentPos_ == end) {
740
startPos = start;
741
// Never attempt to move further right than the end of the text.
742
endPos = Math.min(end + 1, element.value.length);
743
newPos = endPos;
744
} else {
745
startPos = start + 1;
746
endPos = end;
747
newPos = startPos;
748
}
749
} else {
750
// With no current selection, pressing right moves the cursor one
751
// character to the right; with an existing selection, it collapses the
752
// selection to the end of the selection.
753
newPos = start == end ? Math.min(end + 1, element.value.length) : end;
754
}
755
}
756
757
if (this.isPressed(bot.Keyboard.Keys.SHIFT)) {
758
goog.dom.selection.setStart(element, startPos);
759
// On IE, changing goog.dom.selection.setStart also changes the end.
760
goog.dom.selection.setEnd(element, endPos);
761
} else {
762
goog.dom.selection.setCursorPosition(element, newPos);
763
}
764
this.updateCurrentPos_(newPos);
765
};
766
767
768
/**
769
* @param {!bot.Keyboard.Key} key Special key to press.
770
* @private
771
*/
772
bot.Keyboard.prototype.updateOnHomeOrEnd_ = function (key) {
773
bot.Keyboard.checkCanUpdateSelection_(this.getElement());
774
var element = this.getElement();
775
var start = goog.dom.selection.getStart(element);
776
var end = goog.dom.selection.getEnd(element);
777
// TODO: Handle multiline (TEXTAREA) elements.
778
if (key == bot.Keyboard.Keys.HOME) {
779
if (this.isPressed(bot.Keyboard.Keys.SHIFT)) {
780
goog.dom.selection.setStart(element, 0);
781
// If current position is at the end of the selection, typing home
782
// changes the selection to begin at the beginning of the text, running
783
// to the where the current selection begins.
784
var endPos = this.currentPos_ == start ? end : start;
785
// On IE, changing goog.dom.selection.setStart also changes the end.
786
goog.dom.selection.setEnd(element, endPos);
787
} else {
788
goog.dom.selection.setCursorPosition(element, 0);
789
}
790
this.updateCurrentPos_(0);
791
} else { // (key == bot.Keyboard.Keys.END)
792
if (this.isPressed(bot.Keyboard.Keys.SHIFT)) {
793
if (this.currentPos_ == start) {
794
// Current position is at the beginning of the selection. Typing end
795
// changes the selection to begin where the current selection ends,
796
// running to the end of the text.
797
goog.dom.selection.setStart(element, end);
798
}
799
goog.dom.selection.setEnd(element, element.value.length);
800
} else {
801
goog.dom.selection.setCursorPosition(element, element.value.length);
802
}
803
this.updateCurrentPos_(element.value.length);
804
}
805
};
806
807
808
/**
809
* Checks that the cursor position can be updated for the given element.
810
* @param {!Element} element The element to test.
811
* @throws {Error} If the cursor position cannot be updated for the given
812
* element.
813
* @see https://code.google.com/p/chromium/issues/detail?id=330456
814
* @private
815
* @suppress {uselessCode}
816
*/
817
bot.Keyboard.checkCanUpdateSelection_ = function (element) {
818
try {
819
if (typeof element.selectionStart == 'number') {
820
return;
821
}
822
} catch (ex) {
823
// The native error message is actually pretty informative, just add a
824
// reference to the relevant Chrome bug to provide more context.
825
if (ex.message.indexOf('does not support selection.') != -1) {
826
// message is a readonly property, so need to rethrow.
827
throw Error(ex.message + ' (For more information, see ' +
828
'https://code.google.com/p/chromium/issues/detail?id=330456)');
829
}
830
throw ex;
831
}
832
throw Error('Element does not support selection');
833
};
834
835
836
/**
837
* @param {!Element} element The element to test.
838
* @return {boolean} Whether the given element supports the input element
839
* selection API.
840
* @see https://code.google.com/p/chromium/issues/detail?id=330456
841
*/
842
bot.Keyboard.supportsSelection = function (element) {
843
try {
844
bot.Keyboard.checkCanUpdateSelection_(element);
845
} catch (ex) {
846
return false;
847
}
848
return true;
849
};
850
851
852
/**
853
* @param {number} pos New position of the cursor.
854
* @private
855
*/
856
bot.Keyboard.prototype.updateCurrentPos_ = function (pos) {
857
this.currentPos_ = pos;
858
};
859
860
861
/**
862
* @param {bot.events.EventType} type Event type.
863
* @param {!bot.Keyboard.Key} key Key.
864
* @param {boolean=} opt_preventDefault Whether the default event should be
865
* prevented. Defaults to false.
866
* @return {boolean} Whether the event fired successfully or was cancelled.
867
* @private
868
*/
869
bot.Keyboard.prototype.fireKeyEvent_ = function (type, key, opt_preventDefault) {
870
if (goog.isNull(key.code)) {
871
throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,
872
'Key must have a keycode to be fired.');
873
}
874
875
var args = {
876
altKey: this.isPressed(bot.Keyboard.Keys.ALT),
877
ctrlKey: this.isPressed(bot.Keyboard.Keys.CONTROL),
878
metaKey: this.isPressed(bot.Keyboard.Keys.META),
879
shiftKey: this.isPressed(bot.Keyboard.Keys.SHIFT),
880
keyCode: key.code,
881
charCode: (key.character && type == bot.events.EventType.KEYPRESS) ?
882
this.getChar_(key).charCodeAt(0) : 0,
883
preventDefault: !!opt_preventDefault
884
};
885
886
return this.fireKeyboardEvent(type, args);
887
};
888
889
890
/**
891
* Sets focus to the element. If the element does not have focus, place cursor
892
* at the end of the text in the element.
893
*
894
* @param {!Element} element Element that is moved to.
895
*/
896
bot.Keyboard.prototype.moveCursor = function (element) {
897
this.setElement(element);
898
this.editable_ = bot.dom.isEditable(element);
899
900
var focusChanged = this.focusOnElement();
901
if (this.editable_ && focusChanged) {
902
goog.dom.selection.setCursorPosition(element, element.value.length);
903
this.updateCurrentPos_(element.value.length);
904
}
905
};
906
907
908
/**
909
* Serialize the current state of the keyboard.
910
*
911
* @return {bot.Keyboard.State} The current keyboard state.
912
*/
913
bot.Keyboard.prototype.getState = function () {
914
// Need to use quoted literals here, so the compiler will not rename the
915
// properties of the emitted object. When the object is created via the
916
// "constructor", we will look for these *specific* properties. Everywhere
917
// else internally, we use the dot-notation, so it's okay if the compiler
918
// renames the internal variable name.
919
return {
920
'pressed': this.pressed_.getValues(),
921
'currentPos': this.currentPos_
922
};
923
};
924
925
926
/**
927
* Returns the state of the modifier keys, to be shared with other input
928
* devices.
929
*
930
* @return {bot.Device.ModifiersState} Modifiers state.
931
*/
932
bot.Keyboard.prototype.getModifiersState = function () {
933
return this.modifiersState;
934
};
935
936