| // Licensed to the Software Freedom Conservancy (SFC) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The SFC licenses this file |
| // to you under the Apache License, Version 2.0 (the |
| // "License"); you may not use this file except in compliance |
| // with the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, |
| // software distributed under the License is distributed on an |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| // KIND, either express or implied. See the License for the |
| // specific language governing permissions and limitations |
| // under the License. |
| |
| /** |
| * @fileoverview The file contains an abstraction of a keyboard |
| * for simulating the pressing and releasing of keys. |
| */ |
| |
| goog.provide('bot.Keyboard'); |
| goog.provide('bot.Keyboard.Key'); |
| goog.provide('bot.Keyboard.Keys'); |
| |
| goog.require('bot.Device'); |
| goog.require('bot.Error'); |
| goog.require('bot.ErrorCode'); |
| goog.require('bot.dom'); |
| goog.require('bot.events.EventType'); |
| goog.require('bot.userAgent'); |
| goog.require('goog.array'); |
| goog.require('goog.dom.TagName'); |
| goog.require('goog.dom.selection'); |
| goog.require('goog.structs.Map'); |
| goog.require('goog.structs.Set'); |
| goog.require('goog.userAgent'); |
| |
| |
| |
| /** |
| * A keyboard that provides atomic typing actions. |
| * |
| * @constructor |
| * @param {bot.Keyboard.State=} opt_state Optional keyboard state. |
| * @extends {bot.Device} |
| * @suppress {deprecated} |
| */ |
| bot.Keyboard = function (opt_state) { |
| goog.base(this); |
| |
| /** @private {boolean} */ |
| this.editable_ = bot.dom.isEditable(this.getElement()); |
| |
| /** @private {number} */ |
| this.currentPos_ = 0; |
| |
| /** @private {!goog.structs.Set.<!bot.Keyboard.Key>} */ |
| this.pressed_ = new goog.structs.Set(); |
| |
| if (opt_state) { |
| // If a state is passed, let's assume we were passed an object with |
| // the correct properties. |
| goog.array.forEach(opt_state['pressed'], function (key) { |
| this.setKeyPressed_(/** @type {!bot.Keyboard.Key} */(key), true); |
| }, this); |
| |
| this.currentPos_ = opt_state['currentPos'] || 0; |
| } |
| }; |
| goog.inherits(bot.Keyboard, bot.Device); |
| |
| |
| /** |
| * Describes the current state of a keyboard. |
| * @typedef {{pressed: !Array.<!bot.Keyboard.Key>, |
| * currentPos: number}} |
| */ |
| bot.Keyboard.State; |
| |
| |
| /** |
| * Maps characters to (key,boolean) pairs, where the key generates the |
| * character and the boolean is true when the shift must be pressed. |
| * @private {!Object.<string, {key: !bot.Keyboard.Key, shift: boolean}>} |
| * @const |
| */ |
| bot.Keyboard.CHAR_TO_KEY_ = {}; |
| |
| |
| /** |
| * Constructs a new key and, if it is a character key, adds a mapping from the |
| * character to is in the CHAR_TO_KEY_ map. Using this factory function instead |
| * of the new keyword, also helps reduce the size of the compiled Js fragment. |
| * |
| * @param {null|number| |
| * {gecko: (?number), ieWebkit: (?number)}} code |
| * Either a single keycode or a record of per-browser keycodes. |
| * @param {string=} opt_char Character when shift is not pressed. |
| * @param {string=} opt_shiftChar Character when shift is pressed. |
| * @return {!bot.Keyboard.Key} The new key. |
| * @private |
| */ |
| bot.Keyboard.newKey_ = function (code, opt_char, opt_shiftChar) { |
| if (goog.isObject(code)) { |
| if (goog.userAgent.GECKO) { |
| code = code.gecko; |
| } else { // IE and Webkit |
| code = code.ieWebkit; |
| } |
| } |
| var key = new bot.Keyboard.Key(code, opt_char, opt_shiftChar); |
| |
| // For a character key, potentially map the character to the key in the |
| // CHAR_TO_KEY_ map. Because of numpad, multiple keys may have the same |
| // character. To avoid mapping numpad keys, we overwrite a mapping only if |
| // the key has a distinct shift character. |
| if (opt_char && (!(opt_char in bot.Keyboard.CHAR_TO_KEY_) || opt_shiftChar)) { |
| bot.Keyboard.CHAR_TO_KEY_[opt_char] = { key: key, shift: false }; |
| if (opt_shiftChar) { |
| bot.Keyboard.CHAR_TO_KEY_[opt_shiftChar] = { key: key, shift: true }; |
| } |
| } |
| |
| return key; |
| }; |
| |
| |
| |
| /** |
| * A key on the keyboard. |
| * |
| * @constructor |
| * @param {?number} code Keycode for the key; null for the (rare) case |
| * that pressing the key issues no key events. |
| * @param {string=} opt_char Character when shift is not pressed; null |
| * when the key does not cause a character to be typed. |
| * @param {string=} opt_shiftChar Character when shift is pressed; null |
| * when the key does not cause a character to be typed. |
| */ |
| bot.Keyboard.Key = function (code, opt_char, opt_shiftChar) { |
| /** @type {?number} */ |
| this.code = code; |
| |
| /** @type {?string} */ |
| this.character = opt_char || null; |
| |
| /** @type {?string} */ |
| this.shiftChar = opt_shiftChar || this.character; |
| }; |
| |
| |
| /** |
| * An enumeration of keys known to this module. |
| * |
| * @enum {!bot.Keyboard.Key} |
| */ |
| bot.Keyboard.Keys = { |
| BACKSPACE: bot.Keyboard.newKey_(8), |
| TAB: bot.Keyboard.newKey_(9), |
| ENTER: bot.Keyboard.newKey_(13), |
| SHIFT: bot.Keyboard.newKey_(16), |
| CONTROL: bot.Keyboard.newKey_(17), |
| ALT: bot.Keyboard.newKey_(18), |
| PAUSE: bot.Keyboard.newKey_(19), |
| CAPS_LOCK: bot.Keyboard.newKey_(20), |
| ESC: bot.Keyboard.newKey_(27), |
| SPACE: bot.Keyboard.newKey_(32, ' '), |
| PAGE_UP: bot.Keyboard.newKey_(33), |
| PAGE_DOWN: bot.Keyboard.newKey_(34), |
| END: bot.Keyboard.newKey_(35), |
| HOME: bot.Keyboard.newKey_(36), |
| LEFT: bot.Keyboard.newKey_(37), |
| UP: bot.Keyboard.newKey_(38), |
| RIGHT: bot.Keyboard.newKey_(39), |
| DOWN: bot.Keyboard.newKey_(40), |
| PRINT_SCREEN: bot.Keyboard.newKey_(44), |
| INSERT: bot.Keyboard.newKey_(45), |
| DELETE: bot.Keyboard.newKey_(46), |
| |
| // Number keys |
| ZERO: bot.Keyboard.newKey_(48, '0', ')'), |
| ONE: bot.Keyboard.newKey_(49, '1', '!'), |
| TWO: bot.Keyboard.newKey_(50, '2', '@'), |
| THREE: bot.Keyboard.newKey_(51, '3', '#'), |
| FOUR: bot.Keyboard.newKey_(52, '4', '$'), |
| FIVE: bot.Keyboard.newKey_(53, '5', '%'), |
| SIX: bot.Keyboard.newKey_(54, '6', '^'), |
| SEVEN: bot.Keyboard.newKey_(55, '7', '&'), |
| EIGHT: bot.Keyboard.newKey_(56, '8', '*'), |
| NINE: bot.Keyboard.newKey_(57, '9', '('), |
| |
| // Letter keys |
| A: bot.Keyboard.newKey_(65, 'a', 'A'), |
| B: bot.Keyboard.newKey_(66, 'b', 'B'), |
| C: bot.Keyboard.newKey_(67, 'c', 'C'), |
| D: bot.Keyboard.newKey_(68, 'd', 'D'), |
| E: bot.Keyboard.newKey_(69, 'e', 'E'), |
| F: bot.Keyboard.newKey_(70, 'f', 'F'), |
| G: bot.Keyboard.newKey_(71, 'g', 'G'), |
| H: bot.Keyboard.newKey_(72, 'h', 'H'), |
| I: bot.Keyboard.newKey_(73, 'i', 'I'), |
| J: bot.Keyboard.newKey_(74, 'j', 'J'), |
| K: bot.Keyboard.newKey_(75, 'k', 'K'), |
| L: bot.Keyboard.newKey_(76, 'l', 'L'), |
| M: bot.Keyboard.newKey_(77, 'm', 'M'), |
| N: bot.Keyboard.newKey_(78, 'n', 'N'), |
| O: bot.Keyboard.newKey_(79, 'o', 'O'), |
| P: bot.Keyboard.newKey_(80, 'p', 'P'), |
| Q: bot.Keyboard.newKey_(81, 'q', 'Q'), |
| R: bot.Keyboard.newKey_(82, 'r', 'R'), |
| S: bot.Keyboard.newKey_(83, 's', 'S'), |
| T: bot.Keyboard.newKey_(84, 't', 'T'), |
| U: bot.Keyboard.newKey_(85, 'u', 'U'), |
| V: bot.Keyboard.newKey_(86, 'v', 'V'), |
| W: bot.Keyboard.newKey_(87, 'w', 'W'), |
| X: bot.Keyboard.newKey_(88, 'x', 'X'), |
| Y: bot.Keyboard.newKey_(89, 'y', 'Y'), |
| Z: bot.Keyboard.newKey_(90, 'z', 'Z'), |
| |
| // Branded keys |
| META: bot.Keyboard.newKey_( |
| goog.userAgent.WINDOWS ? { gecko: 91, ieWebkit: 91 } : |
| (goog.userAgent.MAC ? { gecko: 224, ieWebkit: 91 } : |
| { gecko: 0, ieWebkit: 91 })), // Linux |
| META_RIGHT: bot.Keyboard.newKey_( |
| goog.userAgent.WINDOWS ? { gecko: 92, ieWebkit: 92 } : |
| (goog.userAgent.MAC ? { gecko: 224, ieWebkit: 93 } : |
| { gecko: 0, ieWebkit: 92 })), // Linux |
| CONTEXT_MENU: bot.Keyboard.newKey_( |
| goog.userAgent.WINDOWS ? { gecko: 93, ieWebkit: 93 } : |
| (goog.userAgent.MAC ? { gecko: 0, ieWebkit: 0 } : |
| { gecko: 93, ieWebkit: null })), // Linux |
| |
| // Numpad keys |
| NUM_ZERO: bot.Keyboard.newKey_({ gecko: 96, ieWebkit: 96 }, '0'), |
| NUM_ONE: bot.Keyboard.newKey_({ gecko: 97, ieWebkit: 97 }, '1'), |
| NUM_TWO: bot.Keyboard.newKey_({ gecko: 98, ieWebkit: 98 }, '2'), |
| NUM_THREE: bot.Keyboard.newKey_({ gecko: 99, ieWebkit: 99 }, '3'), |
| NUM_FOUR: bot.Keyboard.newKey_({ gecko: 100, ieWebkit: 100 }, '4'), |
| NUM_FIVE: bot.Keyboard.newKey_({ gecko: 101, ieWebkit: 101 }, '5'), |
| NUM_SIX: bot.Keyboard.newKey_({ gecko: 102, ieWebkit: 102 }, '6'), |
| NUM_SEVEN: bot.Keyboard.newKey_({ gecko: 103, ieWebkit: 103 }, '7'), |
| NUM_EIGHT: bot.Keyboard.newKey_({ gecko: 104, ieWebkit: 104 }, '8'), |
| NUM_NINE: bot.Keyboard.newKey_({ gecko: 105, ieWebkit: 105 }, '9'), |
| NUM_MULTIPLY: bot.Keyboard.newKey_( |
| { gecko: 106, ieWebkit: 106 }, '*'), |
| NUM_PLUS: bot.Keyboard.newKey_( |
| { gecko: 107, ieWebkit: 107 }, '+'), |
| NUM_MINUS: bot.Keyboard.newKey_( |
| { gecko: 109, ieWebkit: 109 }, '-'), |
| NUM_PERIOD: bot.Keyboard.newKey_( |
| { gecko: 110, ieWebkit: 110 }, '.'), |
| NUM_DIVISION: bot.Keyboard.newKey_( |
| { gecko: 111, ieWebkit: 111 }, '/'), |
| NUM_LOCK: bot.Keyboard.newKey_(144), |
| |
| // Function keys |
| F1: bot.Keyboard.newKey_(112), |
| F2: bot.Keyboard.newKey_(113), |
| F3: bot.Keyboard.newKey_(114), |
| F4: bot.Keyboard.newKey_(115), |
| F5: bot.Keyboard.newKey_(116), |
| F6: bot.Keyboard.newKey_(117), |
| F7: bot.Keyboard.newKey_(118), |
| F8: bot.Keyboard.newKey_(119), |
| F9: bot.Keyboard.newKey_(120), |
| F10: bot.Keyboard.newKey_(121), |
| F11: bot.Keyboard.newKey_(122), |
| F12: bot.Keyboard.newKey_(123), |
| |
| // Punctuation keys |
| EQUALS: bot.Keyboard.newKey_( |
| { gecko: 107, ieWebkit: 187 }, '=', '+'), |
| SEPARATOR: bot.Keyboard.newKey_(108, ','), |
| HYPHEN: bot.Keyboard.newKey_( |
| { gecko: 109, ieWebkit: 189 }, '-', '_'), |
| COMMA: bot.Keyboard.newKey_(188, ',', '<'), |
| PERIOD: bot.Keyboard.newKey_(190, '.', '>'), |
| SLASH: bot.Keyboard.newKey_(191, '/', '?'), |
| BACKTICK: bot.Keyboard.newKey_(192, '`', '~'), |
| OPEN_BRACKET: bot.Keyboard.newKey_(219, '[', '{'), |
| BACKSLASH: bot.Keyboard.newKey_(220, '\\', '|'), |
| CLOSE_BRACKET: bot.Keyboard.newKey_(221, ']', '}'), |
| SEMICOLON: bot.Keyboard.newKey_( |
| { gecko: 59, ieWebkit: 186 }, ';', ':'), |
| APOSTROPHE: bot.Keyboard.newKey_(222, '\'', '"') |
| }; |
| |
| |
| /** |
| * Given a character, returns a pair of a key and a boolean: the key being one |
| * that types the character and the boolean indicating whether the key must be |
| * shifted to type it. This function will never return a numpad key; that is, |
| * it will always return a symbol key when given a number or math symbol. |
| * |
| * If given a character for which this module does not know the key (the key |
| * is not in the bot.Keyboard.Keys enumeration), returns a key that types the |
| * given character but has a (likely incorrect) keycode of zero. |
| * |
| * @param {string} ch Single character. |
| * @return {{key: !bot.Keyboard.Key, shift: boolean}} A pair of a key and |
| * a boolean indicating whether shift must be pressed for the character. |
| */ |
| bot.Keyboard.Key.fromChar = function (ch) { |
| if (ch.length != 1) { |
| throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR, |
| 'Argument not a single character: ' + ch); |
| } |
| var keyShiftPair = bot.Keyboard.CHAR_TO_KEY_[ch]; |
| if (!keyShiftPair) { |
| // We don't know the true keycode of non-US keyboard characters, but |
| // ch.toUpperCase().charCodeAt(0) should occasionally be right, and |
| // at least yield a positive number. |
| var upperCase = ch.toUpperCase(); |
| var keyCode = upperCase.charCodeAt(0); |
| var key = bot.Keyboard.newKey_(keyCode, ch.toLowerCase(), upperCase); |
| keyShiftPair = { key: key, shift: (ch != key.character) }; |
| } |
| return keyShiftPair; |
| }; |
| |
| |
| /** |
| * Array of modifier keys. |
| * |
| * @type {!Array.<!bot.Keyboard.Key>} |
| * @const |
| */ |
| bot.Keyboard.MODIFIERS = [ |
| bot.Keyboard.Keys.ALT, |
| bot.Keyboard.Keys.CONTROL, |
| bot.Keyboard.Keys.META, |
| bot.Keyboard.Keys.SHIFT |
| ]; |
| |
| |
| /** |
| * Map of modifier to key. |
| * @private {!goog.structs.Map.<!bot.Device.Modifier, !bot.Keyboard.Key>} |
| * @suppress {deprecated} |
| */ |
| bot.Keyboard.MODIFIER_TO_KEY_MAP_ = (function () { |
| var modifiersMap = new goog.structs.Map(); |
| modifiersMap.set(bot.Device.Modifier.SHIFT, |
| bot.Keyboard.Keys.SHIFT); |
| modifiersMap.set(bot.Device.Modifier.CONTROL, |
| bot.Keyboard.Keys.CONTROL); |
| modifiersMap.set(bot.Device.Modifier.ALT, |
| bot.Keyboard.Keys.ALT); |
| modifiersMap.set(bot.Device.Modifier.META, |
| bot.Keyboard.Keys.META); |
| |
| return modifiersMap; |
| })(); |
| |
| |
| /** |
| * The reverse map - key to modifier. |
| * @private {!goog.structs.Map.<number, !bot.Device.Modifier>} |
| * @suppress {deprecated} |
| */ |
| bot.Keyboard.KEY_TO_MODIFIER_ = (function (modifiersMap) { |
| var keyToModifierMap = new goog.structs.Map(); |
| goog.array.forEach(modifiersMap.getKeys(), function (m) { |
| keyToModifierMap.set(modifiersMap.get(m).code, m); |
| }); |
| |
| return keyToModifierMap; |
| })(bot.Keyboard.MODIFIER_TO_KEY_MAP_); |
| |
| |
| /** |
| * Set the modifier state if the provided key is one, otherwise just add |
| * to the list of pressed keys. |
| * @param {!bot.Keyboard.Key} key The key to update. |
| * @param {boolean} isPressed Whether the key is pressed. |
| * @private |
| */ |
| bot.Keyboard.prototype.setKeyPressed_ = function (key, isPressed) { |
| if (goog.array.contains(bot.Keyboard.MODIFIERS, key)) { |
| var modifier = /** @type {bot.Device.Modifier}*/ ( |
| bot.Keyboard.KEY_TO_MODIFIER_.get(key.code)); |
| this.modifiersState.setPressed(modifier, isPressed); |
| } |
| |
| if (isPressed) { |
| this.pressed_.add(key); |
| } else { |
| this.pressed_.remove(key); |
| } |
| }; |
| |
| |
| /** |
| * The value used for newlines in the current browser/OS combination. Although |
| * the line endings look platform dependent, they are browser dependent. |
| * |
| * @private {string} |
| * @const |
| */ |
| bot.Keyboard.NEW_LINE_ = goog.userAgent.IE ? '\r\n' : '\n'; |
| |
| |
| /** |
| * Returns whether the key is currently pressed. |
| * |
| * @param {!bot.Keyboard.Key} key Key. |
| * @return {boolean} Whether the key is pressed. |
| */ |
| bot.Keyboard.prototype.isPressed = function (key) { |
| return this.pressed_.contains(key); |
| }; |
| |
| |
| /** |
| * Presses the given key on the keyboard. Keys that are pressed can be pressed |
| * again before releasing, to simulate repeated keys, except for modifier keys, |
| * which must be released before they can be pressed again. |
| * |
| * @param {!bot.Keyboard.Key} key Key to press. |
| */ |
| bot.Keyboard.prototype.pressKey = function (key) { |
| if (goog.array.contains(bot.Keyboard.MODIFIERS, key) && this.isPressed(key)) { |
| throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR, |
| 'Cannot press a modifier key that is already pressed.'); |
| } |
| |
| // Note that GECKO is special-cased below because of |
| // https://bugzilla.mozilla.org/show_bug.cgi?id=501496. "preventDefault on |
| // keydown does not cancel following keypress" |
| var performDefault = !goog.isNull(key.code) && |
| this.fireKeyEvent_(bot.events.EventType.KEYDOWN, key); |
| |
| // Fires keydown and stops if unsuccessful. |
| if (performDefault || goog.userAgent.GECKO) { |
| // Fires keypress if required and stops if unsuccessful. |
| if (!this.requiresKeyPress_(key) || |
| this.fireKeyEvent_( |
| bot.events.EventType.KEYPRESS, key, !performDefault)) { |
| if (performDefault) { |
| this.maybeSubmitForm_(key); |
| if (this.editable_) { |
| this.maybeEditText_(key); |
| } |
| } |
| } |
| } |
| |
| this.setKeyPressed_(key, true); |
| }; |
| |
| |
| /** |
| * Whether the given key currently requires a keypress. |
| * TODO: Make this dependent on the state of the modifier keys. |
| * |
| * @param {bot.Keyboard.Key} key Key. |
| * @return {boolean} Whether it requires a keypress event. |
| * @private |
| */ |
| bot.Keyboard.prototype.requiresKeyPress_ = function (key) { |
| if (key.character || key == bot.Keyboard.Keys.ENTER) { |
| return true; |
| } else if (goog.userAgent.WEBKIT || goog.userAgent.EDGE) { |
| return false; |
| } else if (goog.userAgent.IE) { |
| return key == bot.Keyboard.Keys.ESC; |
| } else { // Gecko |
| switch (key) { |
| case bot.Keyboard.Keys.SHIFT: |
| case bot.Keyboard.Keys.CONTROL: |
| case bot.Keyboard.Keys.ALT: |
| return false; |
| case bot.Keyboard.Keys.META: |
| case bot.Keyboard.Keys.META_RIGHT: |
| case bot.Keyboard.Keys.CONTEXT_MENU: |
| return goog.userAgent.GECKO; |
| default: |
| return true; |
| } |
| } |
| }; |
| |
| |
| /** |
| * Maybe submit a form if the ENTER key is released. On non-FF browsers, firing |
| * the keyPress and keyRelease events for the ENTER key does not result in a |
| * form being submitted so we have to fire the form submit event as well. |
| * |
| * @param {bot.Keyboard.Key} key Key. |
| * @private |
| */ |
| bot.Keyboard.prototype.maybeSubmitForm_ = function (key) { |
| if (key != bot.Keyboard.Keys.ENTER) { |
| return; |
| } |
| if (goog.userAgent.GECKO || |
| !bot.dom.isElement(this.getElement(), goog.dom.TagName.INPUT)) { |
| return; |
| } |
| |
| var form = bot.Device.findAncestorForm(this.getElement()); |
| if (form) { |
| var inputs = form.getElementsByTagName('input'); |
| var hasSubmit = goog.array.some(inputs, function (e) { |
| return bot.Device.isFormSubmitElement(e); |
| }); |
| // The second part of this if statement will always include forms on Safari |
| // version < 5. |
| if (hasSubmit || inputs.length == 1 || |
| (goog.userAgent.WEBKIT && !bot.userAgent.isEngineVersion(534))) { |
| this.submitForm(form); |
| } |
| } |
| }; |
| |
| |
| /** |
| * Maybe edit text when a key is pressed in an editable form. |
| * |
| * @param {!bot.Keyboard.Key} key Key that was pressed. |
| * @private |
| */ |
| bot.Keyboard.prototype.maybeEditText_ = function (key) { |
| if (key.character) { |
| this.updateOnCharacter_(key); |
| } else { |
| switch (key) { |
| case bot.Keyboard.Keys.ENTER: |
| this.updateOnEnter_(); |
| break; |
| case bot.Keyboard.Keys.BACKSPACE: |
| case bot.Keyboard.Keys.DELETE: |
| this.updateOnBackspaceOrDelete_(key); |
| break; |
| case bot.Keyboard.Keys.LEFT: |
| case bot.Keyboard.Keys.RIGHT: |
| this.updateOnLeftOrRight_(key); |
| break; |
| case bot.Keyboard.Keys.HOME: |
| case bot.Keyboard.Keys.END: |
| this.updateOnHomeOrEnd_(key); |
| break; |
| } |
| } |
| }; |
| |
| |
| /** |
| * Releases the given key on the keyboard. Releasing a key that is not |
| * pressed results in an exception. |
| * |
| * @param {!bot.Keyboard.Key} key Key to release. |
| */ |
| bot.Keyboard.prototype.releaseKey = function (key) { |
| if (!this.isPressed(key)) { |
| throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR, |
| 'Cannot release a key that is not pressed. (' + key.code + ')'); |
| } |
| if (!goog.isNull(key.code)) { |
| this.fireKeyEvent_(bot.events.EventType.KEYUP, key); |
| } |
| |
| this.setKeyPressed_(key, false); |
| }; |
| |
| |
| /** |
| * Given the current state of the SHIFT and CAPS_LOCK key, returns the |
| * character that will be typed is the specified key is pressed. |
| * |
| * @param {!bot.Keyboard.Key} key Key. |
| * @return {string} Character to be typed. |
| * @private |
| */ |
| bot.Keyboard.prototype.getChar_ = function (key) { |
| if (!key.character) { |
| throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR, 'not a character key'); |
| } |
| var shiftPressed = this.isPressed(bot.Keyboard.Keys.SHIFT); |
| return /** @type {string} */ (shiftPressed ? key.shiftChar : key.character); |
| }; |
| |
| |
| /** |
| * Whether firing a keypress event causes text to be edited without any |
| * additional logic to surgically apply the edit. |
| * @private {boolean} |
| * @const |
| */ |
| bot.Keyboard.KEYPRESS_EDITS_TEXT_ = goog.userAgent.GECKO && |
| !bot.userAgent.isEngineVersion(12); |
| |
| |
| /** |
| * @param {!bot.Keyboard.Key} key Key with character to insert. |
| * @private |
| */ |
| bot.Keyboard.prototype.updateOnCharacter_ = function (key) { |
| if (bot.Keyboard.KEYPRESS_EDITS_TEXT_) { |
| return; |
| } |
| |
| var character = this.getChar_(key); |
| var newPos = goog.dom.selection.getStart(this.getElement()) + 1; |
| if (bot.Keyboard.supportsSelection(this.getElement())) { |
| goog.dom.selection.setText(this.getElement(), character); |
| goog.dom.selection.setStart(this.getElement(), newPos); |
| } else { |
| this.getElement().value += character; |
| } |
| if (goog.userAgent.WEBKIT) { |
| this.fireHtmlEvent(bot.events.EventType.TEXTINPUT); |
| } |
| if (!bot.userAgent.IE_DOC_PRE9) { |
| this.fireHtmlEvent(bot.events.EventType.INPUT); |
| } |
| this.updateCurrentPos_(newPos); |
| }; |
| |
| |
| /** @private */ |
| bot.Keyboard.prototype.updateOnEnter_ = function () { |
| if (bot.Keyboard.KEYPRESS_EDITS_TEXT_) { |
| return; |
| } |
| |
| // WebKit fires text input regardless of whether a new line is added, see: |
| // https://bugs.webkit.org/show_bug.cgi?id=54152 |
| if (goog.userAgent.WEBKIT) { |
| this.fireHtmlEvent(bot.events.EventType.TEXTINPUT); |
| } |
| if (bot.dom.isElement(this.getElement(), goog.dom.TagName.TEXTAREA)) { |
| var newPos = goog.dom.selection.getStart(this.getElement()) + |
| bot.Keyboard.NEW_LINE_.length; |
| if (bot.Keyboard.supportsSelection(this.getElement())) { |
| goog.dom.selection.setText(this.getElement(), bot.Keyboard.NEW_LINE_); |
| goog.dom.selection.setStart(this.getElement(), newPos); |
| } else { |
| this.getElement().value += bot.Keyboard.NEW_LINE_; |
| } |
| if (!goog.userAgent.IE) { |
| this.fireHtmlEvent(bot.events.EventType.INPUT); |
| } |
| this.updateCurrentPos_(newPos); |
| } |
| }; |
| |
| |
| /** |
| * @param {!bot.Keyboard.Key} key Backspace or delete key. |
| * @private |
| */ |
| bot.Keyboard.prototype.updateOnBackspaceOrDelete_ = function (key) { |
| if (bot.Keyboard.KEYPRESS_EDITS_TEXT_) { |
| return; |
| } |
| |
| // Determine what should be deleted. If text is already selected, that |
| // text is deleted, else we move left/right from the current cursor. |
| bot.Keyboard.checkCanUpdateSelection_(this.getElement()); |
| var endpoints = goog.dom.selection.getEndPoints(this.getElement()); |
| if (endpoints[0] == endpoints[1]) { |
| if (key == bot.Keyboard.Keys.BACKSPACE) { |
| goog.dom.selection.setStart(this.getElement(), endpoints[1] - 1); |
| // On IE, changing goog.dom.selection.setStart also changes the end. |
| goog.dom.selection.setEnd(this.getElement(), endpoints[1]); |
| } else { |
| goog.dom.selection.setEnd(this.getElement(), endpoints[1] + 1); |
| } |
| } |
| |
| // If the endpoints are equal (e.g., the cursor was at the beginning/end |
| // of the input), the text field won't be changed. |
| endpoints = goog.dom.selection.getEndPoints(this.getElement()); |
| var textChanged = !(endpoints[0] == this.getElement().value.length || |
| endpoints[1] == 0); |
| goog.dom.selection.setText(this.getElement(), ''); |
| |
| // Except for IE and GECKO, we need to fire the input event manually, but |
| // only if the text was actually changed. |
| // Note: Gecko has some strange behavior with the input event. In a |
| // textarea, backspace always sends an input event, while delete only |
| // sends one if you actually change the text. |
| // In a textbox/password box, backspace always sends an input event unless |
| // the box has no text. Delete behaves the same way in Firefox 3.0, but |
| // in later versions it only fires an input event if no text changes. |
| if (!goog.userAgent.IE && textChanged || |
| (goog.userAgent.GECKO && key == bot.Keyboard.Keys.BACKSPACE)) { |
| this.fireHtmlEvent(bot.events.EventType.INPUT); |
| } |
| |
| // Update the cursor position |
| endpoints = goog.dom.selection.getEndPoints(this.getElement()); |
| this.updateCurrentPos_(endpoints[1]); |
| }; |
| |
| |
| /** |
| * @param {!bot.Keyboard.Key} key Special key to press. |
| * @private |
| */ |
| bot.Keyboard.prototype.updateOnLeftOrRight_ = function (key) { |
| bot.Keyboard.checkCanUpdateSelection_(this.getElement()); |
| var element = this.getElement(); |
| var start = goog.dom.selection.getStart(element); |
| var end = goog.dom.selection.getEnd(element); |
| |
| var newPos, startPos = 0, endPos = 0; |
| if (key == bot.Keyboard.Keys.LEFT) { |
| if (this.isPressed(bot.Keyboard.Keys.SHIFT)) { |
| // If the current position of the cursor is at the start of the |
| // selection, pressing left expands the selection one character to the |
| // left; otherwise, pressing left collapses it one character to the |
| // left. |
| if (this.currentPos_ == start) { |
| // Never attempt to move further left than the beginning of the text. |
| startPos = Math.max(start - 1, 0); |
| endPos = end; |
| newPos = startPos; |
| } else { |
| startPos = start; |
| endPos = end - 1; |
| newPos = endPos; |
| } |
| } else { |
| // With no current selection, pressing left moves the cursor one |
| // character to the left; with an existing selection, it collapses the |
| // selection to the beginning of the selection. |
| newPos = start == end ? Math.max(start - 1, 0) : start; |
| } |
| } else { // (key == bot.Keyboard.Keys.RIGHT) |
| if (this.isPressed(bot.Keyboard.Keys.SHIFT)) { |
| // If the current position of the cursor is at the end of the selection, |
| // pressing right expands the selection one character to the right; |
| // otherwise, pressing right collapses it one character to the right. |
| if (this.currentPos_ == end) { |
| startPos = start; |
| // Never attempt to move further right than the end of the text. |
| endPos = Math.min(end + 1, element.value.length); |
| newPos = endPos; |
| } else { |
| startPos = start + 1; |
| endPos = end; |
| newPos = startPos; |
| } |
| } else { |
| // With no current selection, pressing right moves the cursor one |
| // character to the right; with an existing selection, it collapses the |
| // selection to the end of the selection. |
| newPos = start == end ? Math.min(end + 1, element.value.length) : end; |
| } |
| } |
| |
| if (this.isPressed(bot.Keyboard.Keys.SHIFT)) { |
| goog.dom.selection.setStart(element, startPos); |
| // On IE, changing goog.dom.selection.setStart also changes the end. |
| goog.dom.selection.setEnd(element, endPos); |
| } else { |
| goog.dom.selection.setCursorPosition(element, newPos); |
| } |
| this.updateCurrentPos_(newPos); |
| }; |
| |
| |
| /** |
| * @param {!bot.Keyboard.Key} key Special key to press. |
| * @private |
| */ |
| bot.Keyboard.prototype.updateOnHomeOrEnd_ = function (key) { |
| bot.Keyboard.checkCanUpdateSelection_(this.getElement()); |
| var element = this.getElement(); |
| var start = goog.dom.selection.getStart(element); |
| var end = goog.dom.selection.getEnd(element); |
| // TODO: Handle multiline (TEXTAREA) elements. |
| if (key == bot.Keyboard.Keys.HOME) { |
| if (this.isPressed(bot.Keyboard.Keys.SHIFT)) { |
| goog.dom.selection.setStart(element, 0); |
| // If current position is at the end of the selection, typing home |
| // changes the selection to begin at the beginning of the text, running |
| // to the where the current selection begins. |
| var endPos = this.currentPos_ == start ? end : start; |
| // On IE, changing goog.dom.selection.setStart also changes the end. |
| goog.dom.selection.setEnd(element, endPos); |
| } else { |
| goog.dom.selection.setCursorPosition(element, 0); |
| } |
| this.updateCurrentPos_(0); |
| } else { // (key == bot.Keyboard.Keys.END) |
| if (this.isPressed(bot.Keyboard.Keys.SHIFT)) { |
| if (this.currentPos_ == start) { |
| // Current position is at the beginning of the selection. Typing end |
| // changes the selection to begin where the current selection ends, |
| // running to the end of the text. |
| goog.dom.selection.setStart(element, end); |
| } |
| goog.dom.selection.setEnd(element, element.value.length); |
| } else { |
| goog.dom.selection.setCursorPosition(element, element.value.length); |
| } |
| this.updateCurrentPos_(element.value.length); |
| } |
| }; |
| |
| |
| /** |
| * Checks that the cursor position can be updated for the given element. |
| * @param {!Element} element The element to test. |
| * @throws {Error} If the cursor position cannot be updated for the given |
| * element. |
| * @see https://code.google.com/p/chromium/issues/detail?id=330456 |
| * @private |
| * @suppress {uselessCode} |
| */ |
| bot.Keyboard.checkCanUpdateSelection_ = function (element) { |
| try { |
| if (typeof element.selectionStart == 'number') { |
| return; |
| } |
| } catch (ex) { |
| // The native error message is actually pretty informative, just add a |
| // reference to the relevant Chrome bug to provide more context. |
| if (ex.message.indexOf('does not support selection.') != -1) { |
| // message is a readonly property, so need to rethrow. |
| throw Error(ex.message + ' (For more information, see ' + |
| 'https://code.google.com/p/chromium/issues/detail?id=330456)'); |
| } |
| throw ex; |
| } |
| throw Error('Element does not support selection'); |
| }; |
| |
| |
| /** |
| * @param {!Element} element The element to test. |
| * @return {boolean} Whether the given element supports the input element |
| * selection API. |
| * @see https://code.google.com/p/chromium/issues/detail?id=330456 |
| */ |
| bot.Keyboard.supportsSelection = function (element) { |
| try { |
| bot.Keyboard.checkCanUpdateSelection_(element); |
| } catch (ex) { |
| return false; |
| } |
| return true; |
| }; |
| |
| |
| /** |
| * @param {number} pos New position of the cursor. |
| * @private |
| */ |
| bot.Keyboard.prototype.updateCurrentPos_ = function (pos) { |
| this.currentPos_ = pos; |
| }; |
| |
| |
| /** |
| * @param {bot.events.EventType} type Event type. |
| * @param {!bot.Keyboard.Key} key Key. |
| * @param {boolean=} opt_preventDefault Whether the default event should be |
| * prevented. Defaults to false. |
| * @return {boolean} Whether the event fired successfully or was cancelled. |
| * @private |
| */ |
| bot.Keyboard.prototype.fireKeyEvent_ = function (type, key, opt_preventDefault) { |
| if (goog.isNull(key.code)) { |
| throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR, |
| 'Key must have a keycode to be fired.'); |
| } |
| |
| var args = { |
| altKey: this.isPressed(bot.Keyboard.Keys.ALT), |
| ctrlKey: this.isPressed(bot.Keyboard.Keys.CONTROL), |
| metaKey: this.isPressed(bot.Keyboard.Keys.META), |
| shiftKey: this.isPressed(bot.Keyboard.Keys.SHIFT), |
| keyCode: key.code, |
| charCode: (key.character && type == bot.events.EventType.KEYPRESS) ? |
| this.getChar_(key).charCodeAt(0) : 0, |
| preventDefault: !!opt_preventDefault |
| }; |
| |
| return this.fireKeyboardEvent(type, args); |
| }; |
| |
| |
| /** |
| * Sets focus to the element. If the element does not have focus, place cursor |
| * at the end of the text in the element. |
| * |
| * @param {!Element} element Element that is moved to. |
| */ |
| bot.Keyboard.prototype.moveCursor = function (element) { |
| this.setElement(element); |
| this.editable_ = bot.dom.isEditable(element); |
| |
| var focusChanged = this.focusOnElement(); |
| if (this.editable_ && focusChanged) { |
| goog.dom.selection.setCursorPosition(element, element.value.length); |
| this.updateCurrentPos_(element.value.length); |
| } |
| }; |
| |
| |
| /** |
| * Serialize the current state of the keyboard. |
| * |
| * @return {bot.Keyboard.State} The current keyboard state. |
| */ |
| bot.Keyboard.prototype.getState = function () { |
| // Need to use quoted literals here, so the compiler will not rename the |
| // properties of the emitted object. When the object is created via the |
| // "constructor", we will look for these *specific* properties. Everywhere |
| // else internally, we use the dot-notation, so it's okay if the compiler |
| // renames the internal variable name. |
| return { |
| 'pressed': this.pressed_.getValues(), |
| 'currentPos': this.currentPos_ |
| }; |
| }; |
| |
| |
| /** |
| * Returns the state of the modifier keys, to be shared with other input |
| * devices. |
| * |
| * @return {bot.Device.ModifiersState} Modifiers state. |
| */ |
| bot.Keyboard.prototype.getModifiersState = function () { |
| return this.modifiersState; |
| }; |