// Licensed to the Software Freedom Conservancy (SFC) under one1// or more contributor license agreements. See the NOTICE file2// distributed with this work for additional information3// regarding copyright ownership. The SFC licenses this file4// to you under the Apache License, Version 2.0 (the5// "License"); you may not use this file except in compliance6// with the License. You may obtain a copy of the License at7//8// http://www.apache.org/licenses/LICENSE-2.09//10// Unless required by applicable law or agreed to in writing,11// software distributed under the License is distributed on an12// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY13// KIND, either express or implied. See the License for the14// specific language governing permissions and limitations15// under the License.1617/**18* @fileoverview The file contains an abstraction of a keyboard19* for simulating the pressing and releasing of keys.20*/2122goog.provide('bot.Keyboard');23goog.provide('bot.Keyboard.Key');24goog.provide('bot.Keyboard.Keys');2526goog.require('bot.Device');27goog.require('bot.Error');28goog.require('bot.ErrorCode');29goog.require('bot.dom');30goog.require('bot.events.EventType');31goog.require('bot.userAgent');32goog.require('goog.array');33goog.require('goog.dom.TagName');34goog.require('goog.dom.selection');35goog.require('goog.structs.Map');36goog.require('goog.structs.Set');37goog.require('goog.userAgent');38goog.require('goog.utils');39404142/**43* A keyboard that provides atomic typing actions.44*45* @constructor46* @param {bot.Keyboard.State=} opt_state Optional keyboard state.47* @extends {bot.Device}48* @suppress {deprecated}49*/50bot.Keyboard = function (opt_state) {51bot.Device.call(this);5253/** @private {boolean} */54this.editable_ = bot.dom.isEditable(this.getElement());5556/** @private {number} */57this.currentPos_ = 0;5859/** @private {!goog.structs.Set.<!bot.Keyboard.Key>} */60this.pressed_ = new goog.structs.Set();6162if (opt_state) {63// If a state is passed, let's assume we were passed an object with64// the correct properties.65goog.array.forEach(opt_state['pressed'], function (key) {66this.setKeyPressed_(/** @type {!bot.Keyboard.Key} */(key), true);67}, this);6869this.currentPos_ = opt_state['currentPos'] || 0;70}71};72goog.utils.inherits(bot.Keyboard, bot.Device);737475/**76* Describes the current state of a keyboard.77* @typedef {{pressed: !Array.<!bot.Keyboard.Key>,78* currentPos: number}}79*/80bot.Keyboard.State;818283/**84* Maps characters to (key,boolean) pairs, where the key generates the85* character and the boolean is true when the shift must be pressed.86* @private {!Object.<string, {key: !bot.Keyboard.Key, shift: boolean}>}87* @const88*/89bot.Keyboard.CHAR_TO_KEY_ = {};909192/**93* Constructs a new key and, if it is a character key, adds a mapping from the94* character to is in the CHAR_TO_KEY_ map. Using this factory function instead95* of the new keyword, also helps reduce the size of the compiled Js fragment.96*97* @param {null|number|98* {gecko: (?number), ieWebkit: (?number)}} code99* 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* @private104*/105bot.Keyboard.newKey_ = function (code, opt_char, opt_shiftChar) {106if (goog.utils.isObject(code)) {107if (goog.userAgent.GECKO) {108code = code.gecko;109} else { // IE and Webkit110code = code.ieWebkit;111}112}113var key = new bot.Keyboard.Key(/** @type {?number} */ (code), opt_char, opt_shiftChar);114115// For a character key, potentially map the character to the key in the116// CHAR_TO_KEY_ map. Because of numpad, multiple keys may have the same117// character. To avoid mapping numpad keys, we overwrite a mapping only if118// the key has a distinct shift character.119if (opt_char && (!(opt_char in bot.Keyboard.CHAR_TO_KEY_) || opt_shiftChar)) {120bot.Keyboard.CHAR_TO_KEY_[opt_char] = { key: key, shift: false };121if (opt_shiftChar) {122bot.Keyboard.CHAR_TO_KEY_[opt_shiftChar] = { key: key, shift: true };123}124}125126return key;127};128129130131/**132* A key on the keyboard.133*134* @constructor135* @param {?number} code Keycode for the key; null for the (rare) case136* that pressing the key issues no key events.137* @param {string=} opt_char Character when shift is not pressed; null138* when the key does not cause a character to be typed.139* @param {string=} opt_shiftChar Character when shift is pressed; null140* when the key does not cause a character to be typed.141*/142bot.Keyboard.Key = function (code, opt_char, opt_shiftChar) {143/** @type {?number} */144this.code = code;145146/** @type {?string} */147this.character = opt_char || null;148149/** @type {?string} */150this.shiftChar = opt_shiftChar || this.character;151};152153154/**155* Type definition for the keyboard keys object.156* @typedef {{157* BACKSPACE: !bot.Keyboard.Key,158* TAB: !bot.Keyboard.Key,159* ENTER: !bot.Keyboard.Key,160* SHIFT: !bot.Keyboard.Key,161* CONTROL: !bot.Keyboard.Key,162* ALT: !bot.Keyboard.Key,163* PAUSE: !bot.Keyboard.Key,164* CAPS_LOCK: !bot.Keyboard.Key,165* ESC: !bot.Keyboard.Key,166* SPACE: !bot.Keyboard.Key,167* PAGE_UP: !bot.Keyboard.Key,168* PAGE_DOWN: !bot.Keyboard.Key,169* END: !bot.Keyboard.Key,170* HOME: !bot.Keyboard.Key,171* LEFT: !bot.Keyboard.Key,172* UP: !bot.Keyboard.Key,173* RIGHT: !bot.Keyboard.Key,174* DOWN: !bot.Keyboard.Key,175* PRINT_SCREEN: !bot.Keyboard.Key,176* INSERT: !bot.Keyboard.Key,177* DELETE: !bot.Keyboard.Key,178* ZERO: !bot.Keyboard.Key,179* ONE: !bot.Keyboard.Key,180* TWO: !bot.Keyboard.Key,181* THREE: !bot.Keyboard.Key,182* FOUR: !bot.Keyboard.Key,183* FIVE: !bot.Keyboard.Key,184* SIX: !bot.Keyboard.Key,185* SEVEN: !bot.Keyboard.Key,186* EIGHT: !bot.Keyboard.Key,187* NINE: !bot.Keyboard.Key,188* A: !bot.Keyboard.Key,189* B: !bot.Keyboard.Key,190* C: !bot.Keyboard.Key,191* D: !bot.Keyboard.Key,192* E: !bot.Keyboard.Key,193* F: !bot.Keyboard.Key,194* G: !bot.Keyboard.Key,195* H: !bot.Keyboard.Key,196* I: !bot.Keyboard.Key,197* J: !bot.Keyboard.Key,198* K: !bot.Keyboard.Key,199* L: !bot.Keyboard.Key,200* M: !bot.Keyboard.Key,201* N: !bot.Keyboard.Key,202* O: !bot.Keyboard.Key,203* P: !bot.Keyboard.Key,204* Q: !bot.Keyboard.Key,205* R: !bot.Keyboard.Key,206* S: !bot.Keyboard.Key,207* T: !bot.Keyboard.Key,208* U: !bot.Keyboard.Key,209* V: !bot.Keyboard.Key,210* W: !bot.Keyboard.Key,211* X: !bot.Keyboard.Key,212* Y: !bot.Keyboard.Key,213* Z: !bot.Keyboard.Key,214* META: !bot.Keyboard.Key,215* META_RIGHT: !bot.Keyboard.Key,216* CONTEXT_MENU: !bot.Keyboard.Key,217* NUM_ZERO: !bot.Keyboard.Key,218* NUM_ONE: !bot.Keyboard.Key,219* NUM_TWO: !bot.Keyboard.Key,220* NUM_THREE: !bot.Keyboard.Key,221* NUM_FOUR: !bot.Keyboard.Key,222* NUM_FIVE: !bot.Keyboard.Key,223* NUM_SIX: !bot.Keyboard.Key,224* NUM_SEVEN: !bot.Keyboard.Key,225* NUM_EIGHT: !bot.Keyboard.Key,226* NUM_NINE: !bot.Keyboard.Key,227* NUM_MULTIPLY: !bot.Keyboard.Key,228* NUM_PLUS: !bot.Keyboard.Key,229* NUM_MINUS: !bot.Keyboard.Key,230* NUM_PERIOD: !bot.Keyboard.Key,231* NUM_DIVISION: !bot.Keyboard.Key,232* NUM_LOCK: !bot.Keyboard.Key,233* F1: !bot.Keyboard.Key,234* F2: !bot.Keyboard.Key,235* F3: !bot.Keyboard.Key,236* F4: !bot.Keyboard.Key,237* F5: !bot.Keyboard.Key,238* F6: !bot.Keyboard.Key,239* F7: !bot.Keyboard.Key,240* F8: !bot.Keyboard.Key,241* F9: !bot.Keyboard.Key,242* F10: !bot.Keyboard.Key,243* F11: !bot.Keyboard.Key,244* F12: !bot.Keyboard.Key,245* EQUALS: !bot.Keyboard.Key,246* SEPARATOR: !bot.Keyboard.Key,247* HYPHEN: !bot.Keyboard.Key,248* COMMA: !bot.Keyboard.Key,249* PERIOD: !bot.Keyboard.Key,250* SLASH: !bot.Keyboard.Key,251* BACKTICK: !bot.Keyboard.Key,252* OPEN_BRACKET: !bot.Keyboard.Key,253* BACKSLASH: !bot.Keyboard.Key,254* CLOSE_BRACKET: !bot.Keyboard.Key,255* SEMICOLON: !bot.Keyboard.Key,256* APOSTROPHE: !bot.Keyboard.Key257* }}258*/259bot.Keyboard.KeysType;260261262/**263* The set of keys known to this module.264*265* @const {!bot.Keyboard.KeysType}266*/267bot.Keyboard.Keys = /** @type {!bot.Keyboard.KeysType} */ ({268BACKSPACE: bot.Keyboard.newKey_(8),269TAB: bot.Keyboard.newKey_(9),270ENTER: bot.Keyboard.newKey_(13),271SHIFT: bot.Keyboard.newKey_(16),272CONTROL: bot.Keyboard.newKey_(17),273ALT: bot.Keyboard.newKey_(18),274PAUSE: bot.Keyboard.newKey_(19),275CAPS_LOCK: bot.Keyboard.newKey_(20),276ESC: bot.Keyboard.newKey_(27),277SPACE: bot.Keyboard.newKey_(32, ' '),278PAGE_UP: bot.Keyboard.newKey_(33),279PAGE_DOWN: bot.Keyboard.newKey_(34),280END: bot.Keyboard.newKey_(35),281HOME: bot.Keyboard.newKey_(36),282LEFT: bot.Keyboard.newKey_(37),283UP: bot.Keyboard.newKey_(38),284RIGHT: bot.Keyboard.newKey_(39),285DOWN: bot.Keyboard.newKey_(40),286PRINT_SCREEN: bot.Keyboard.newKey_(44),287INSERT: bot.Keyboard.newKey_(45),288DELETE: bot.Keyboard.newKey_(46),289290// Number keys291ZERO: bot.Keyboard.newKey_(48, '0', ')'),292ONE: bot.Keyboard.newKey_(49, '1', '!'),293TWO: bot.Keyboard.newKey_(50, '2', '@'),294THREE: bot.Keyboard.newKey_(51, '3', '#'),295FOUR: bot.Keyboard.newKey_(52, '4', '$'),296FIVE: bot.Keyboard.newKey_(53, '5', '%'),297SIX: bot.Keyboard.newKey_(54, '6', '^'),298SEVEN: bot.Keyboard.newKey_(55, '7', '&'),299EIGHT: bot.Keyboard.newKey_(56, '8', '*'),300NINE: bot.Keyboard.newKey_(57, '9', '('),301302// Letter keys303A: bot.Keyboard.newKey_(65, 'a', 'A'),304B: bot.Keyboard.newKey_(66, 'b', 'B'),305C: bot.Keyboard.newKey_(67, 'c', 'C'),306D: bot.Keyboard.newKey_(68, 'd', 'D'),307E: bot.Keyboard.newKey_(69, 'e', 'E'),308F: bot.Keyboard.newKey_(70, 'f', 'F'),309G: bot.Keyboard.newKey_(71, 'g', 'G'),310H: bot.Keyboard.newKey_(72, 'h', 'H'),311I: bot.Keyboard.newKey_(73, 'i', 'I'),312J: bot.Keyboard.newKey_(74, 'j', 'J'),313K: bot.Keyboard.newKey_(75, 'k', 'K'),314L: bot.Keyboard.newKey_(76, 'l', 'L'),315M: bot.Keyboard.newKey_(77, 'm', 'M'),316N: bot.Keyboard.newKey_(78, 'n', 'N'),317O: bot.Keyboard.newKey_(79, 'o', 'O'),318P: bot.Keyboard.newKey_(80, 'p', 'P'),319Q: bot.Keyboard.newKey_(81, 'q', 'Q'),320R: bot.Keyboard.newKey_(82, 'r', 'R'),321S: bot.Keyboard.newKey_(83, 's', 'S'),322T: bot.Keyboard.newKey_(84, 't', 'T'),323U: bot.Keyboard.newKey_(85, 'u', 'U'),324V: bot.Keyboard.newKey_(86, 'v', 'V'),325W: bot.Keyboard.newKey_(87, 'w', 'W'),326X: bot.Keyboard.newKey_(88, 'x', 'X'),327Y: bot.Keyboard.newKey_(89, 'y', 'Y'),328Z: bot.Keyboard.newKey_(90, 'z', 'Z'),329330// Branded keys331META: bot.Keyboard.newKey_(332goog.userAgent.WINDOWS ? { gecko: 91, ieWebkit: 91 } :333(goog.userAgent.MAC ? { gecko: 224, ieWebkit: 91 } :334{ gecko: 0, ieWebkit: 91 })), // Linux335META_RIGHT: bot.Keyboard.newKey_(336goog.userAgent.WINDOWS ? { gecko: 92, ieWebkit: 92 } :337(goog.userAgent.MAC ? { gecko: 224, ieWebkit: 93 } :338{ gecko: 0, ieWebkit: 92 })), // Linux339CONTEXT_MENU: bot.Keyboard.newKey_(340goog.userAgent.WINDOWS ? { gecko: 93, ieWebkit: 93 } :341(goog.userAgent.MAC ? { gecko: 0, ieWebkit: 0 } :342{ gecko: 93, ieWebkit: null })), // Linux343344// Numpad keys345NUM_ZERO: bot.Keyboard.newKey_({ gecko: 96, ieWebkit: 96 }, '0'),346NUM_ONE: bot.Keyboard.newKey_({ gecko: 97, ieWebkit: 97 }, '1'),347NUM_TWO: bot.Keyboard.newKey_({ gecko: 98, ieWebkit: 98 }, '2'),348NUM_THREE: bot.Keyboard.newKey_({ gecko: 99, ieWebkit: 99 }, '3'),349NUM_FOUR: bot.Keyboard.newKey_({ gecko: 100, ieWebkit: 100 }, '4'),350NUM_FIVE: bot.Keyboard.newKey_({ gecko: 101, ieWebkit: 101 }, '5'),351NUM_SIX: bot.Keyboard.newKey_({ gecko: 102, ieWebkit: 102 }, '6'),352NUM_SEVEN: bot.Keyboard.newKey_({ gecko: 103, ieWebkit: 103 }, '7'),353NUM_EIGHT: bot.Keyboard.newKey_({ gecko: 104, ieWebkit: 104 }, '8'),354NUM_NINE: bot.Keyboard.newKey_({ gecko: 105, ieWebkit: 105 }, '9'),355NUM_MULTIPLY: bot.Keyboard.newKey_(356{ gecko: 106, ieWebkit: 106 }, '*'),357NUM_PLUS: bot.Keyboard.newKey_(358{ gecko: 107, ieWebkit: 107 }, '+'),359NUM_MINUS: bot.Keyboard.newKey_(360{ gecko: 109, ieWebkit: 109 }, '-'),361NUM_PERIOD: bot.Keyboard.newKey_(362{ gecko: 110, ieWebkit: 110 }, '.'),363NUM_DIVISION: bot.Keyboard.newKey_(364{ gecko: 111, ieWebkit: 111 }, '/'),365NUM_LOCK: bot.Keyboard.newKey_(144),366367// Function keys368F1: bot.Keyboard.newKey_(112),369F2: bot.Keyboard.newKey_(113),370F3: bot.Keyboard.newKey_(114),371F4: bot.Keyboard.newKey_(115),372F5: bot.Keyboard.newKey_(116),373F6: bot.Keyboard.newKey_(117),374F7: bot.Keyboard.newKey_(118),375F8: bot.Keyboard.newKey_(119),376F9: bot.Keyboard.newKey_(120),377F10: bot.Keyboard.newKey_(121),378F11: bot.Keyboard.newKey_(122),379F12: bot.Keyboard.newKey_(123),380381// Punctuation keys382EQUALS: bot.Keyboard.newKey_(383{ gecko: 107, ieWebkit: 187 }, '=', '+'),384SEPARATOR: bot.Keyboard.newKey_(108, ','),385HYPHEN: bot.Keyboard.newKey_(386{ gecko: 109, ieWebkit: 189 }, '-', '_'),387COMMA: bot.Keyboard.newKey_(188, ',', '<'),388PERIOD: bot.Keyboard.newKey_(190, '.', '>'),389SLASH: bot.Keyboard.newKey_(191, '/', '?'),390BACKTICK: bot.Keyboard.newKey_(192, '`', '~'),391OPEN_BRACKET: bot.Keyboard.newKey_(219, '[', '{'),392BACKSLASH: bot.Keyboard.newKey_(220, '\\', '|'),393CLOSE_BRACKET: bot.Keyboard.newKey_(221, ']', '}'),394SEMICOLON: bot.Keyboard.newKey_(395{ gecko: 59, ieWebkit: 186 }, ';', ':'),396APOSTROPHE: bot.Keyboard.newKey_(222, '\'', '"')397});398399400/**401* Given a character, returns a pair of a key and a boolean: the key being one402* that types the character and the boolean indicating whether the key must be403* shifted to type it. This function will never return a numpad key; that is,404* it will always return a symbol key when given a number or math symbol.405*406* If given a character for which this module does not know the key (the key407* is not in the bot.Keyboard.Keys enumeration), returns a key that types the408* given character but has a (likely incorrect) keycode of zero.409*410* @param {string} ch Single character.411* @return {{key: !bot.Keyboard.Key, shift: boolean}} A pair of a key and412* a boolean indicating whether shift must be pressed for the character.413*/414bot.Keyboard.Key.fromChar = function (ch) {415if (ch.length != 1) {416throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,417'Argument not a single character: ' + ch);418}419var keyShiftPair = bot.Keyboard.CHAR_TO_KEY_[ch];420if (!keyShiftPair) {421// We don't know the true keycode of non-US keyboard characters, but422// ch.toUpperCase().charCodeAt(0) should occasionally be right, and423// at least yield a positive number.424var upperCase = ch.toUpperCase();425var keyCode = upperCase.charCodeAt(0);426var key = bot.Keyboard.newKey_(keyCode, ch.toLowerCase(), upperCase);427keyShiftPair = { key: key, shift: (ch != key.character) };428}429return keyShiftPair;430};431432433/**434* Array of modifier keys.435*436* @type {!Array.<!bot.Keyboard.Key>}437* @const438*/439bot.Keyboard.MODIFIERS = [440bot.Keyboard.Keys.ALT,441bot.Keyboard.Keys.CONTROL,442bot.Keyboard.Keys.META,443bot.Keyboard.Keys.SHIFT444];445446447/**448* Map of modifier to key.449* @private {!goog.structs.Map.<!bot.Device.Modifier, !bot.Keyboard.Key>}450* @suppress {deprecated}451*/452bot.Keyboard.MODIFIER_TO_KEY_MAP_ = (function () {453var modifiersMap = new goog.structs.Map();454modifiersMap.set(bot.Device.Modifier.SHIFT,455bot.Keyboard.Keys.SHIFT);456modifiersMap.set(bot.Device.Modifier.CONTROL,457bot.Keyboard.Keys.CONTROL);458modifiersMap.set(bot.Device.Modifier.ALT,459bot.Keyboard.Keys.ALT);460modifiersMap.set(bot.Device.Modifier.META,461bot.Keyboard.Keys.META);462463return modifiersMap;464})();465466467/**468* The reverse map - key to modifier.469* @private {!goog.structs.Map.<number, !bot.Device.Modifier>}470* @suppress {deprecated}471*/472bot.Keyboard.KEY_TO_MODIFIER_ = (function (modifiersMap) {473var keyToModifierMap = new goog.structs.Map();474goog.array.forEach(modifiersMap.getKeys(), function (m) {475keyToModifierMap.set(modifiersMap.get(m).code, m);476});477478return keyToModifierMap;479})(bot.Keyboard.MODIFIER_TO_KEY_MAP_);480481482/**483* Set the modifier state if the provided key is one, otherwise just add484* to the list of pressed keys.485* @param {!bot.Keyboard.Key} key The key to update.486* @param {boolean} isPressed Whether the key is pressed.487* @private488*/489bot.Keyboard.prototype.setKeyPressed_ = function (key, isPressed) {490if (goog.array.contains(bot.Keyboard.MODIFIERS, key)) {491var modifier = /** @type {bot.Device.Modifier}*/ (492bot.Keyboard.KEY_TO_MODIFIER_.get(key.code));493this.modifiersState.setPressed(modifier, isPressed);494}495496if (isPressed) {497this.pressed_.add(key);498} else {499this.pressed_.remove(key);500}501};502503504/**505* The value used for newlines in the current browser/OS combination. Although506* the line endings look platform dependent, they are browser dependent.507*508* @private {string}509* @const510*/511bot.Keyboard.NEW_LINE_ = goog.userAgent.IE ? '\r\n' : '\n';512513514/**515* Returns whether the key is currently pressed.516*517* @param {!bot.Keyboard.Key} key Key.518* @return {boolean} Whether the key is pressed.519*/520bot.Keyboard.prototype.isPressed = function (key) {521return this.pressed_.contains(key);522};523524525/**526* Presses the given key on the keyboard. Keys that are pressed can be pressed527* again before releasing, to simulate repeated keys, except for modifier keys,528* which must be released before they can be pressed again.529*530* @param {!bot.Keyboard.Key} key Key to press.531*/532bot.Keyboard.prototype.pressKey = function (key) {533if (goog.array.contains(bot.Keyboard.MODIFIERS, key) && this.isPressed(key)) {534throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,535'Cannot press a modifier key that is already pressed.');536}537538// Note that GECKO is special-cased below because of539// https://bugzilla.mozilla.org/show_bug.cgi?id=501496. "preventDefault on540// keydown does not cancel following keypress"541var performDefault = key.code !== null &&542this.fireKeyEvent_(bot.events.EventType.KEYDOWN, key);543544// Fires keydown and stops if unsuccessful.545if (performDefault || goog.userAgent.GECKO) {546// Fires keypress if required and stops if unsuccessful.547if (!this.requiresKeyPress_(key) ||548this.fireKeyEvent_(549bot.events.EventType.KEYPRESS, key, !performDefault)) {550if (performDefault) {551this.maybeSubmitForm_(key);552if (this.editable_) {553this.maybeEditText_(key);554}555}556}557}558559this.setKeyPressed_(key, true);560};561562563/**564* Whether the given key currently requires a keypress.565* TODO: Make this dependent on the state of the modifier keys.566*567* @param {bot.Keyboard.Key} key Key.568* @return {boolean} Whether it requires a keypress event.569* @private570*/571bot.Keyboard.prototype.requiresKeyPress_ = function (key) {572if (key.character || key == bot.Keyboard.Keys.ENTER) {573return true;574} else if (goog.userAgent.WEBKIT || goog.userAgent.EDGE) {575return false;576} else if (goog.userAgent.IE) {577return key == bot.Keyboard.Keys.ESC;578} else { // Gecko579switch (key) {580case bot.Keyboard.Keys.SHIFT:581case bot.Keyboard.Keys.CONTROL:582case bot.Keyboard.Keys.ALT:583return false;584case bot.Keyboard.Keys.META:585case bot.Keyboard.Keys.META_RIGHT:586case bot.Keyboard.Keys.CONTEXT_MENU:587return goog.userAgent.GECKO;588default:589return true;590}591}592};593594595/**596* Maybe submit a form if the ENTER key is released. On non-FF browsers, firing597* the keyPress and keyRelease events for the ENTER key does not result in a598* form being submitted so we have to fire the form submit event as well.599*600* @param {bot.Keyboard.Key} key Key.601* @private602*/603bot.Keyboard.prototype.maybeSubmitForm_ = function (key) {604if (key != bot.Keyboard.Keys.ENTER) {605return;606}607if ((goog.userAgent.GECKO && !bot.userAgent.isEngineVersion(93)) ||608!bot.dom.isElement(this.getElement(), goog.dom.TagName.INPUT)) {609return;610}611612var form = bot.Device.findAncestorForm(this.getElement());613if (form) {614var inputs = form.getElementsByTagName('input');615var hasSubmit = goog.array.some(inputs, function (e) {616return bot.Device.isFormSubmitElement(e);617});618// The second part of this if statement will always include forms on Safari619// version < 5.620if (hasSubmit || inputs.length == 1 ||621(goog.userAgent.WEBKIT && !bot.userAgent.isEngineVersion(534))) {622this.submitForm(form);623}624}625};626627628/**629* Maybe edit text when a key is pressed in an editable form.630*631* @param {!bot.Keyboard.Key} key Key that was pressed.632* @private633*/634bot.Keyboard.prototype.maybeEditText_ = function (key) {635if (key.character) {636this.updateOnCharacter_(key);637} else {638switch (key) {639case bot.Keyboard.Keys.ENTER:640this.updateOnEnter_();641break;642case bot.Keyboard.Keys.BACKSPACE:643case bot.Keyboard.Keys.DELETE:644this.updateOnBackspaceOrDelete_(key);645break;646case bot.Keyboard.Keys.LEFT:647case bot.Keyboard.Keys.RIGHT:648this.updateOnLeftOrRight_(key);649break;650case bot.Keyboard.Keys.HOME:651case bot.Keyboard.Keys.END:652this.updateOnHomeOrEnd_(key);653break;654}655}656};657658659/**660* Releases the given key on the keyboard. Releasing a key that is not661* pressed results in an exception.662*663* @param {!bot.Keyboard.Key} key Key to release.664*/665bot.Keyboard.prototype.releaseKey = function (key) {666if (!this.isPressed(key)) {667throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,668'Cannot release a key that is not pressed. (' + key.code + ')');669}670if (key.code !== null) {671this.fireKeyEvent_(bot.events.EventType.KEYUP, key);672}673674this.setKeyPressed_(key, false);675};676677678/**679* Given the current state of the SHIFT and CAPS_LOCK key, returns the680* character that will be typed is the specified key is pressed.681*682* @param {!bot.Keyboard.Key} key Key.683* @return {string} Character to be typed.684* @private685*/686bot.Keyboard.prototype.getChar_ = function (key) {687if (!key.character) {688throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR, 'not a character key');689}690var shiftPressed = this.isPressed(bot.Keyboard.Keys.SHIFT);691return /** @type {string} */ (shiftPressed ? key.shiftChar : key.character);692};693694695/**696* Whether firing a keypress event causes text to be edited without any697* additional logic to surgically apply the edit.698* @private {boolean}699* @const700*/701bot.Keyboard.KEYPRESS_EDITS_TEXT_ = goog.userAgent.GECKO &&702!bot.userAgent.isEngineVersion(12);703704705/**706* @param {!bot.Keyboard.Key} key Key with character to insert.707* @private708*/709bot.Keyboard.prototype.updateOnCharacter_ = function (key) {710if (bot.Keyboard.KEYPRESS_EDITS_TEXT_) {711return;712}713714var character = this.getChar_(key);715var newPos = goog.dom.selection.getStart(this.getElement()) + 1;716if (bot.Keyboard.supportsSelection(this.getElement())) {717goog.dom.selection.setText(this.getElement(), character);718goog.dom.selection.setStart(this.getElement(), newPos);719} else {720this.getElement().value += character;721}722if (goog.userAgent.WEBKIT) {723this.fireHtmlEvent(bot.events.EventType.TEXTINPUT);724}725if (!bot.userAgent.IE_DOC_PRE9) {726this.fireHtmlEvent(bot.events.EventType.INPUT);727}728this.updateCurrentPos_(newPos);729};730731732/** @private */733bot.Keyboard.prototype.updateOnEnter_ = function () {734if (bot.Keyboard.KEYPRESS_EDITS_TEXT_) {735return;736}737738// WebKit fires text input regardless of whether a new line is added, see:739// https://bugs.webkit.org/show_bug.cgi?id=54152740if (goog.userAgent.WEBKIT) {741this.fireHtmlEvent(bot.events.EventType.TEXTINPUT);742}743if (bot.dom.isElement(this.getElement(), goog.dom.TagName.TEXTAREA)) {744var newPos = goog.dom.selection.getStart(this.getElement()) +745bot.Keyboard.NEW_LINE_.length;746if (bot.Keyboard.supportsSelection(this.getElement())) {747goog.dom.selection.setText(this.getElement(), bot.Keyboard.NEW_LINE_);748goog.dom.selection.setStart(this.getElement(), newPos);749} else {750this.getElement().value += bot.Keyboard.NEW_LINE_;751}752if (!goog.userAgent.IE) {753this.fireHtmlEvent(bot.events.EventType.INPUT);754}755this.updateCurrentPos_(newPos);756}757};758759760/**761* @param {!bot.Keyboard.Key} key Backspace or delete key.762* @private763*/764bot.Keyboard.prototype.updateOnBackspaceOrDelete_ = function (key) {765if (bot.Keyboard.KEYPRESS_EDITS_TEXT_) {766return;767}768769// Determine what should be deleted. If text is already selected, that770// text is deleted, else we move left/right from the current cursor.771bot.Keyboard.checkCanUpdateSelection_(this.getElement());772var endpoints = goog.dom.selection.getEndPoints(this.getElement());773if (endpoints[0] == endpoints[1]) {774if (key == bot.Keyboard.Keys.BACKSPACE) {775goog.dom.selection.setStart(this.getElement(), endpoints[1] - 1);776// On IE, changing goog.dom.selection.setStart also changes the end.777goog.dom.selection.setEnd(this.getElement(), endpoints[1]);778} else {779goog.dom.selection.setEnd(this.getElement(), endpoints[1] + 1);780}781}782783// If the endpoints are equal (e.g., the cursor was at the beginning/end784// of the input), the text field won't be changed.785endpoints = goog.dom.selection.getEndPoints(this.getElement());786var textChanged = !(endpoints[0] == this.getElement().value.length ||787endpoints[1] == 0);788goog.dom.selection.setText(this.getElement(), '');789790// Except for IE and GECKO, we need to fire the input event manually, but791// only if the text was actually changed.792// Note: Gecko has some strange behavior with the input event. In a793// textarea, backspace always sends an input event, while delete only794// sends one if you actually change the text.795// In a textbox/password box, backspace always sends an input event unless796// the box has no text. Delete behaves the same way in Firefox 3.0, but797// in later versions it only fires an input event if no text changes.798if (!goog.userAgent.IE && textChanged ||799(goog.userAgent.GECKO && key == bot.Keyboard.Keys.BACKSPACE)) {800this.fireHtmlEvent(bot.events.EventType.INPUT);801}802803// Update the cursor position804endpoints = goog.dom.selection.getEndPoints(this.getElement());805this.updateCurrentPos_(endpoints[1]);806};807808809/**810* @param {!bot.Keyboard.Key} key Special key to press.811* @private812*/813bot.Keyboard.prototype.updateOnLeftOrRight_ = function (key) {814bot.Keyboard.checkCanUpdateSelection_(this.getElement());815var element = this.getElement();816var start = goog.dom.selection.getStart(element);817var end = goog.dom.selection.getEnd(element);818819var newPos, startPos = 0, endPos = 0;820if (key == bot.Keyboard.Keys.LEFT) {821if (this.isPressed(bot.Keyboard.Keys.SHIFT)) {822// If the current position of the cursor is at the start of the823// selection, pressing left expands the selection one character to the824// left; otherwise, pressing left collapses it one character to the825// left.826if (this.currentPos_ == start) {827// Never attempt to move further left than the beginning of the text.828startPos = Math.max(start - 1, 0);829endPos = end;830newPos = startPos;831} else {832startPos = start;833endPos = end - 1;834newPos = endPos;835}836} else {837// With no current selection, pressing left moves the cursor one838// character to the left; with an existing selection, it collapses the839// selection to the beginning of the selection.840newPos = start == end ? Math.max(start - 1, 0) : start;841}842} else { // (key == bot.Keyboard.Keys.RIGHT)843if (this.isPressed(bot.Keyboard.Keys.SHIFT)) {844// If the current position of the cursor is at the end of the selection,845// pressing right expands the selection one character to the right;846// otherwise, pressing right collapses it one character to the right.847if (this.currentPos_ == end) {848startPos = start;849// Never attempt to move further right than the end of the text.850endPos = Math.min(end + 1, element.value.length);851newPos = endPos;852} else {853startPos = start + 1;854endPos = end;855newPos = startPos;856}857} else {858// With no current selection, pressing right moves the cursor one859// character to the right; with an existing selection, it collapses the860// selection to the end of the selection.861newPos = start == end ? Math.min(end + 1, element.value.length) : end;862}863}864865if (this.isPressed(bot.Keyboard.Keys.SHIFT)) {866goog.dom.selection.setStart(element, startPos);867// On IE, changing goog.dom.selection.setStart also changes the end.868goog.dom.selection.setEnd(element, endPos);869} else {870goog.dom.selection.setCursorPosition(element, newPos);871}872this.updateCurrentPos_(newPos);873};874875876/**877* @param {!bot.Keyboard.Key} key Special key to press.878* @private879*/880bot.Keyboard.prototype.updateOnHomeOrEnd_ = function (key) {881bot.Keyboard.checkCanUpdateSelection_(this.getElement());882var element = this.getElement();883var start = goog.dom.selection.getStart(element);884var end = goog.dom.selection.getEnd(element);885// TODO: Handle multiline (TEXTAREA) elements.886if (key == bot.Keyboard.Keys.HOME) {887if (this.isPressed(bot.Keyboard.Keys.SHIFT)) {888goog.dom.selection.setStart(element, 0);889// If current position is at the end of the selection, typing home890// changes the selection to begin at the beginning of the text, running891// to the where the current selection begins.892var endPos = this.currentPos_ == start ? end : start;893// On IE, changing goog.dom.selection.setStart also changes the end.894goog.dom.selection.setEnd(element, endPos);895} else {896goog.dom.selection.setCursorPosition(element, 0);897}898this.updateCurrentPos_(0);899} else { // (key == bot.Keyboard.Keys.END)900if (this.isPressed(bot.Keyboard.Keys.SHIFT)) {901if (this.currentPos_ == start) {902// Current position is at the beginning of the selection. Typing end903// changes the selection to begin where the current selection ends,904// running to the end of the text.905goog.dom.selection.setStart(element, end);906}907goog.dom.selection.setEnd(element, element.value.length);908} else {909goog.dom.selection.setCursorPosition(element, element.value.length);910}911this.updateCurrentPos_(element.value.length);912}913};914915916/**917* Checks that the cursor position can be updated for the given element.918* @param {!Element} element The element to test.919* @throws {Error} If the cursor position cannot be updated for the given920* element.921* @see https://code.google.com/p/chromium/issues/detail?id=330456922* @private923* @suppress {uselessCode}924*/925bot.Keyboard.checkCanUpdateSelection_ = function (element) {926try {927if (typeof element.selectionStart == 'number') {928return;929}930} catch (ex) {931// The native error message is actually pretty informative, just add a932// reference to the relevant Chrome bug to provide more context.933if (ex.message.indexOf('does not support selection.') != -1) {934// message is a readonly property, so need to rethrow.935throw Error(ex.message + ' (For more information, see ' +936'https://code.google.com/p/chromium/issues/detail?id=330456)');937}938throw ex;939}940throw Error('Element does not support selection');941};942943944/**945* @param {!Element} element The element to test.946* @return {boolean} Whether the given element supports the input element947* selection API.948* @see https://code.google.com/p/chromium/issues/detail?id=330456949*/950bot.Keyboard.supportsSelection = function (element) {951try {952bot.Keyboard.checkCanUpdateSelection_(element);953} catch (ex) {954return false;955}956return true;957};958959960/**961* @param {number} pos New position of the cursor.962* @private963*/964bot.Keyboard.prototype.updateCurrentPos_ = function (pos) {965this.currentPos_ = pos;966};967968969/**970* @param {!bot.events.EventFactory_} type Event type.971* @param {!bot.Keyboard.Key} key Key.972* @param {boolean=} opt_preventDefault Whether the default event should be973* prevented. Defaults to false.974* @return {boolean} Whether the event fired successfully or was cancelled.975* @private976*/977bot.Keyboard.prototype.fireKeyEvent_ = function (type, key, opt_preventDefault) {978if (key.code === null) {979throw new bot.Error(bot.ErrorCode.UNKNOWN_ERROR,980'Key must have a keycode to be fired.');981}982983var args = {984altKey: this.isPressed(bot.Keyboard.Keys.ALT),985ctrlKey: this.isPressed(bot.Keyboard.Keys.CONTROL),986metaKey: this.isPressed(bot.Keyboard.Keys.META),987shiftKey: this.isPressed(bot.Keyboard.Keys.SHIFT),988keyCode: key.code,989charCode: (key.character && type == bot.events.EventType.KEYPRESS) ?990this.getChar_(key).charCodeAt(0) : 0,991preventDefault: !!opt_preventDefault992};993994return this.fireKeyboardEvent(type, args);995};996997998/**999* Sets focus to the element. If the element does not have focus, place cursor1000* at the end of the text in the element.1001*1002* @param {!Element} element Element that is moved to.1003*/1004bot.Keyboard.prototype.moveCursor = function (element) {1005this.setElement(element);1006this.editable_ = bot.dom.isEditable(element);10071008var focusChanged = this.focusOnElement();1009if (this.editable_ && focusChanged) {1010goog.dom.selection.setCursorPosition(element, element.value.length);1011this.updateCurrentPos_(element.value.length);1012}1013};101410151016/**1017* Serialize the current state of the keyboard.1018*1019* @return {bot.Keyboard.State} The current keyboard state.1020*/1021bot.Keyboard.prototype.getState = function () {1022// Need to use quoted literals here, so the compiler will not rename the1023// properties of the emitted object. When the object is created via the1024// "constructor", we will look for these *specific* properties. Everywhere1025// else internally, we use the dot-notation, so it's okay if the compiler1026// renames the internal variable name.1027return {1028'pressed': this.pressed_.getValues(),1029'currentPos': this.currentPos_1030};1031};103210331034/**1035* Returns the state of the modifier keys, to be shared with other input1036* devices.1037*1038* @return {bot.Device.ModifiersState} Modifiers state.1039*/1040bot.Keyboard.prototype.getModifiersState = function () {1041return this.modifiersState;1042};104310441045