| // Copyright 2006 The Closure Library Authors. All Rights Reserved. |
| // |
| // Licensed 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 Generic keyboard shortcut handler. |
| * |
| * @author eae@google.com (Emil A Eklund) |
| * @see ../demos/keyboardshortcuts.html |
| */ |
| |
| goog.provide('goog.ui.KeyboardShortcutEvent'); |
| goog.provide('goog.ui.KeyboardShortcutHandler'); |
| goog.provide('goog.ui.KeyboardShortcutHandler.EventType'); |
| |
| goog.require('goog.Timer'); |
| goog.require('goog.array'); |
| goog.require('goog.asserts'); |
| goog.require('goog.events'); |
| goog.require('goog.events.Event'); |
| goog.require('goog.events.EventTarget'); |
| goog.require('goog.events.EventType'); |
| goog.require('goog.events.KeyCodes'); |
| goog.require('goog.events.KeyNames'); |
| goog.require('goog.object'); |
| goog.require('goog.userAgent'); |
| |
| |
| |
| /** |
| * Component for handling keyboard shortcuts. A shortcut is registered and bound |
| * to a specific identifier. Once the shortcut is triggered an event is fired |
| * with the identifier for the shortcut. This allows keyboard shortcuts to be |
| * customized without modifying the code that listens for them. |
| * |
| * Supports keyboard shortcuts triggered by a single key, a stroke stroke (key |
| * plus at least one modifier) and a sequence of keys or strokes. |
| * |
| * @param {goog.events.EventTarget|EventTarget} keyTarget Event target that the |
| * key event listener is attached to, typically the applications root |
| * container. |
| * @constructor |
| * @extends {goog.events.EventTarget} |
| */ |
| goog.ui.KeyboardShortcutHandler = function(keyTarget) { |
| goog.events.EventTarget.call(this); |
| |
| /** |
| * Registered keyboard shortcuts tree. Stored as a map with the keyCode and |
| * modifier(s) as the key and either a list of further strokes or the shortcut |
| * task identifier as the value. |
| * @type {!goog.ui.KeyboardShortcutHandler.SequenceTree_} |
| * @see #makeStroke_ |
| * @private |
| */ |
| this.shortcuts_ = {}; |
| |
| /** |
| * The currently active shortcut sequence tree, which represents the position |
| * in the complete shortcuts_ tree reached by recent key strokes. |
| * @type {!goog.ui.KeyboardShortcutHandler.SequenceTree_} |
| * @private |
| */ |
| this.currentTree_ = this.shortcuts_; |
| |
| /** |
| * The time (in ms, epoch time) of the last keystroke which made progress in |
| * the shortcut sequence tree (i.e. the time that currentTree_ was last set). |
| * Used for timing out stroke sequences. |
| * @type {number} |
| * @private |
| */ |
| this.lastStrokeTime_ = 0; |
| |
| /** |
| * List of numeric key codes for keys that are safe to always regarded as |
| * shortcuts, even if entered in a textarea or input field. |
| * @type {Object} |
| * @private |
| */ |
| this.globalKeys_ = goog.object.createSet( |
| goog.ui.KeyboardShortcutHandler.DEFAULT_GLOBAL_KEYS_); |
| |
| /** |
| * List of input types that should only accept ENTER as a shortcut. |
| * @type {Object} |
| * @private |
| */ |
| this.textInputs_ = goog.object.createSet( |
| goog.ui.KeyboardShortcutHandler.DEFAULT_TEXT_INPUTS_); |
| |
| /** |
| * Whether to always prevent the default action if a shortcut event is fired. |
| * @type {boolean} |
| * @private |
| */ |
| this.alwaysPreventDefault_ = true; |
| |
| /** |
| * Whether to always stop propagation if a shortcut event is fired. |
| * @type {boolean} |
| * @private |
| */ |
| this.alwaysStopPropagation_ = false; |
| |
| /** |
| * Whether to treat all shortcuts as if they had been passed |
| * to setGlobalKeys(). |
| * @type {boolean} |
| * @private |
| */ |
| this.allShortcutsAreGlobal_ = false; |
| |
| /** |
| * Whether to treat shortcuts with modifiers as if they had been passed |
| * to setGlobalKeys(). Ignored if allShortcutsAreGlobal_ is true. Applies |
| * only to form elements (not content-editable). |
| * @type {boolean} |
| * @private |
| */ |
| this.modifierShortcutsAreGlobal_ = true; |
| |
| /** |
| * Whether to treat space key as a shortcut when the focused element is a |
| * checkbox, radiobutton or button. |
| * @type {boolean} |
| * @private |
| */ |
| this.allowSpaceKeyOnButtons_ = false; |
| |
| /** |
| * Tracks the currently pressed shortcut key, for Firefox. |
| * @type {?number} |
| * @private |
| */ |
| this.activeShortcutKeyForGecko_ = null; |
| |
| this.initializeKeyListener(keyTarget); |
| }; |
| goog.inherits(goog.ui.KeyboardShortcutHandler, goog.events.EventTarget); |
| goog.tagUnsealableClass(goog.ui.KeyboardShortcutHandler); |
| |
| |
| |
| /** |
| * A node in a keyboard shortcut sequence tree. A node is either: |
| * 1. A terminal node with a non-nullable shortcut string which is the |
| * identifier for the shortcut triggered by traversing the tree to that node. |
| * 2. An internal node with a null shortcut string and a |
| * {@code goog.ui.KeyboardShortcutHandler.SequenceTree_} representing the |
| * continued stroke sequences from this node. |
| * For clarity, the static factory methods for creating internal and terminal |
| * nodes below should be used rather than using this constructor directly. |
| * @param {string=} opt_shortcut The shortcut identifier, for terminal nodes. |
| * @constructor |
| * @struct |
| * @private |
| */ |
| goog.ui.KeyboardShortcutHandler.SequenceNode_ = function(opt_shortcut) { |
| /** @const {?string} The shorcut action identifier, for terminal nodes. */ |
| this.shortcut = opt_shortcut || null; |
| |
| /** @const {goog.ui.KeyboardShortcutHandler.SequenceTree_} */ |
| this.next = opt_shortcut ? null : {}; |
| }; |
| |
| |
| /** |
| * Creates a terminal shortcut sequence node for the given shortcut identifier. |
| * @param {string} shortcut The shortcut identifier. |
| * @return {!goog.ui.KeyboardShortcutHandler.SequenceNode_} |
| * @private |
| */ |
| goog.ui.KeyboardShortcutHandler.createTerminalNode_ = function(shortcut) { |
| return new goog.ui.KeyboardShortcutHandler.SequenceNode_(shortcut); |
| }; |
| |
| |
| /** |
| * Creates an internal shortcut sequence node - a non-terminal part of a |
| * keyboard sequence. |
| * @return {!goog.ui.KeyboardShortcutHandler.SequenceNode_} |
| * @private |
| */ |
| goog.ui.KeyboardShortcutHandler.createInternalNode_ = function() { |
| return new goog.ui.KeyboardShortcutHandler.SequenceNode_(); |
| }; |
| |
| |
| /** |
| * A map of strokes (represented as numbers) to the nodes reached by those |
| * strokes. |
| * @typedef {Object<number, goog.ui.KeyboardShortcutHandler.SequenceNode_>} |
| * @private |
| */ |
| goog.ui.KeyboardShortcutHandler.SequenceTree_; |
| |
| |
| /** |
| * Maximum allowed delay, in milliseconds, allowed between the first and second |
| * key in a key sequence. |
| * @type {number} |
| */ |
| goog.ui.KeyboardShortcutHandler.MAX_KEY_SEQUENCE_DELAY = 1500; // 1.5 sec |
| |
| |
| /** |
| * Bit values for modifier keys. |
| * @enum {number} |
| */ |
| goog.ui.KeyboardShortcutHandler.Modifiers = { |
| NONE: 0, |
| SHIFT: 1, |
| CTRL: 2, |
| ALT: 4, |
| META: 8 |
| }; |
| |
| |
| /** |
| * Keys marked as global by default. |
| * @type {Array<goog.events.KeyCodes>} |
| * @private |
| */ |
| goog.ui.KeyboardShortcutHandler.DEFAULT_GLOBAL_KEYS_ = [ |
| goog.events.KeyCodes.ESC, |
| goog.events.KeyCodes.F1, |
| goog.events.KeyCodes.F2, |
| goog.events.KeyCodes.F3, |
| goog.events.KeyCodes.F4, |
| goog.events.KeyCodes.F5, |
| goog.events.KeyCodes.F6, |
| goog.events.KeyCodes.F7, |
| goog.events.KeyCodes.F8, |
| goog.events.KeyCodes.F9, |
| goog.events.KeyCodes.F10, |
| goog.events.KeyCodes.F11, |
| goog.events.KeyCodes.F12, |
| goog.events.KeyCodes.PAUSE |
| ]; |
| |
| |
| /** |
| * Text input types to allow only ENTER shortcuts. |
| * Web Forms 2.0 for HTML5: Section 4.10.7 from 29 May 2012. |
| * @type {Array<string>} |
| * @private |
| */ |
| goog.ui.KeyboardShortcutHandler.DEFAULT_TEXT_INPUTS_ = [ |
| 'color', |
| 'date', |
| 'datetime', |
| 'datetime-local', |
| 'email', |
| 'month', |
| 'number', |
| 'password', |
| 'search', |
| 'tel', |
| 'text', |
| 'time', |
| 'url', |
| 'week' |
| ]; |
| |
| |
| /** |
| * Events. |
| * @enum {string} |
| */ |
| goog.ui.KeyboardShortcutHandler.EventType = { |
| SHORTCUT_TRIGGERED: 'shortcut', |
| SHORTCUT_PREFIX: 'shortcut_' |
| }; |
| |
| |
| /** |
| * Cache for name to key code lookup. |
| * @type {Object.<number>} |
| * @private |
| */ |
| goog.ui.KeyboardShortcutHandler.nameToKeyCodeCache_; |
| |
| |
| /** |
| * Target on which to listen for key events. |
| * @type {goog.events.EventTarget|EventTarget} |
| * @private |
| */ |
| goog.ui.KeyboardShortcutHandler.prototype.keyTarget_; |
| |
| |
| /** |
| * Due to a bug in the way that Gecko on Mac handles cut/copy/paste key events |
| * using the meta key, it is necessary to fake the keyDown for the action key |
| * (C,V,X) by capturing it on keyUp. |
| * Because users will often release the meta key a slight moment before they |
| * release the action key, we need this variable that will store whether the |
| * meta key has been released recently. |
| * It will be cleared after a short delay in the key handling logic. |
| * @type {boolean} |
| * @private |
| */ |
| goog.ui.KeyboardShortcutHandler.prototype.metaKeyRecentlyReleased_; |
| |
| |
| /** |
| * Whether a key event is a printable-key event. Windows uses ctrl+alt |
| * (alt-graph) keys to type characters on European keyboards. For such keys, we |
| * cannot identify whether these keys are used for typing characters when |
| * receiving keydown events. Therefore, we set this flag when we receive their |
| * respective keypress events and fire shortcut events only when we do not |
| * receive them. |
| * @type {boolean} |
| * @private |
| */ |
| goog.ui.KeyboardShortcutHandler.prototype.isPrintableKey_; |
| |
| |
| /** |
| * Static method for getting the key code for a given key. |
| * @param {string} name Name of key. |
| * @return {number} The key code. |
| */ |
| goog.ui.KeyboardShortcutHandler.getKeyCode = function(name) { |
| // Build reverse lookup object the first time this method is called. |
| if (!goog.ui.KeyboardShortcutHandler.nameToKeyCodeCache_) { |
| var map = {}; |
| for (var key in goog.events.KeyNames) { |
| // Explicitly convert the stringified map keys to numbers and normalize. |
| map[goog.events.KeyNames[key]] = |
| goog.events.KeyCodes.normalizeKeyCode(parseInt(key, 10)); |
| } |
| goog.ui.KeyboardShortcutHandler.nameToKeyCodeCache_ = map; |
| } |
| |
| // Check if key is in cache. |
| return goog.ui.KeyboardShortcutHandler.nameToKeyCodeCache_[name]; |
| }; |
| |
| |
| /** |
| * Sets whether to always prevent the default action when a shortcut event is |
| * fired. If false, the default action is prevented only if preventDefault is |
| * called on either of the corresponding SHORTCUT_TRIGGERED or SHORTCUT_PREFIX |
| * events. If true, the default action is prevented whenever a shortcut event |
| * is fired. The default value is true. |
| * @param {boolean} alwaysPreventDefault Whether to always call preventDefault. |
| */ |
| goog.ui.KeyboardShortcutHandler.prototype.setAlwaysPreventDefault = function( |
| alwaysPreventDefault) { |
| this.alwaysPreventDefault_ = alwaysPreventDefault; |
| }; |
| |
| |
| /** |
| * Returns whether the default action will always be prevented when a shortcut |
| * event is fired. The default value is true. |
| * @see #setAlwaysPreventDefault |
| * @return {boolean} Whether preventDefault will always be called. |
| */ |
| goog.ui.KeyboardShortcutHandler.prototype.getAlwaysPreventDefault = function() { |
| return this.alwaysPreventDefault_; |
| }; |
| |
| |
| /** |
| * Sets whether to always stop propagation for the event when fired. If false, |
| * the propagation is stopped only if stopPropagation is called on either of the |
| * corresponding SHORT_CUT_TRIGGERED or SHORTCUT_PREFIX events. If true, the |
| * event is prevented from propagating beyond its target whenever it is fired. |
| * The default value is false. |
| * @param {boolean} alwaysStopPropagation Whether to always call |
| * stopPropagation. |
| */ |
| goog.ui.KeyboardShortcutHandler.prototype.setAlwaysStopPropagation = function( |
| alwaysStopPropagation) { |
| this.alwaysStopPropagation_ = alwaysStopPropagation; |
| }; |
| |
| |
| /** |
| * Returns whether the event will always be stopped from propagating beyond its |
| * target when a shortcut event is fired. The default value is false. |
| * @see #setAlwaysStopPropagation |
| * @return {boolean} Whether stopPropagation will always be called. |
| */ |
| goog.ui.KeyboardShortcutHandler.prototype.getAlwaysStopPropagation = |
| function() { |
| return this.alwaysStopPropagation_; |
| }; |
| |
| |
| /** |
| * Sets whether to treat all shortcuts (including modifier shortcuts) as if the |
| * keys had been passed to the setGlobalKeys function. |
| * @param {boolean} allShortcutsGlobal Whether to treat all shortcuts as global. |
| */ |
| goog.ui.KeyboardShortcutHandler.prototype.setAllShortcutsAreGlobal = function( |
| allShortcutsGlobal) { |
| this.allShortcutsAreGlobal_ = allShortcutsGlobal; |
| }; |
| |
| |
| /** |
| * Returns whether all shortcuts (including modifier shortcuts) are treated as |
| * if the keys had been passed to the setGlobalKeys function. |
| * @see #setAllShortcutsAreGlobal |
| * @return {boolean} Whether all shortcuts are treated as globals. |
| */ |
| goog.ui.KeyboardShortcutHandler.prototype.getAllShortcutsAreGlobal = |
| function() { |
| return this.allShortcutsAreGlobal_; |
| }; |
| |
| |
| /** |
| * Sets whether to treat shortcuts with modifiers as if the keys had been |
| * passed to the setGlobalKeys function. Ignored if you have called |
| * setAllShortcutsAreGlobal(true). Applies only to form elements (not |
| * content-editable). |
| * @param {boolean} modifierShortcutsGlobal Whether to treat shortcuts with |
| * modifiers as global. |
| */ |
| goog.ui.KeyboardShortcutHandler.prototype.setModifierShortcutsAreGlobal = |
| function(modifierShortcutsGlobal) { |
| this.modifierShortcutsAreGlobal_ = modifierShortcutsGlobal; |
| }; |
| |
| |
| /** |
| * Returns whether shortcuts with modifiers are treated as if the keys had been |
| * passed to the setGlobalKeys function. Ignored if you have called |
| * setAllShortcutsAreGlobal(true). Applies only to form elements (not |
| * content-editable). |
| * @see #setModifierShortcutsAreGlobal |
| * @return {boolean} Whether shortcuts with modifiers are treated as globals. |
| */ |
| goog.ui.KeyboardShortcutHandler.prototype.getModifierShortcutsAreGlobal = |
| function() { |
| return this.modifierShortcutsAreGlobal_; |
| }; |
| |
| |
| /** |
| * Sets whether to treat space key as a shortcut when the focused element is a |
| * checkbox, radiobutton or button. |
| * @param {boolean} allowSpaceKeyOnButtons Whether to treat space key as a |
| * shortcut when the focused element is a checkbox, radiobutton or button. |
| */ |
| goog.ui.KeyboardShortcutHandler.prototype.setAllowSpaceKeyOnButtons = function( |
| allowSpaceKeyOnButtons) { |
| this.allowSpaceKeyOnButtons_ = allowSpaceKeyOnButtons; |
| }; |
| |
| |
| /** |
| * Registers a keyboard shortcut. |
| * @param {string} identifier Identifier for the task performed by the keyboard |
| * combination. Multiple shortcuts can be provided for the same |
| * task by specifying the same identifier. |
| * @param {...(number|string|Array<number>)} var_args See below. |
| * |
| * param {number} keyCode Numeric code for key |
| * param {number=} opt_modifiers Bitmap indicating required modifier keys. |
| * goog.ui.KeyboardShortcutHandler.Modifiers.SHIFT, CONTROL, |
| * ALT, or META. |
| * |
| * The last two parameters can be repeated any number of times to create a |
| * shortcut using a sequence of strokes. Instead of varagrs the second parameter |
| * could also be an array where each element would be ragarded as a parameter. |
| * |
| * A string representation of the shortcut can be supplied instead of the last |
| * two parameters. In that case the method only takes two arguments, the |
| * identifier and the string. |
| * |
| * Examples: |
| * g registerShortcut(str, G_KEYCODE) |
| * Ctrl+g registerShortcut(str, G_KEYCODE, CTRL) |
| * Ctrl+Shift+g registerShortcut(str, G_KEYCODE, CTRL | SHIFT) |
| * Ctrl+g a registerShortcut(str, G_KEYCODE, CTRL, A_KEYCODE) |
| * Ctrl+g Shift+a registerShortcut(str, G_KEYCODE, CTRL, A_KEYCODE, SHIFT) |
| * g a registerShortcut(str, G_KEYCODE, NONE, A_KEYCODE) |
| * |
| * Examples using string representation for shortcuts: |
| * g registerShortcut(str, 'g') |
| * Ctrl+g registerShortcut(str, 'ctrl+g') |
| * Ctrl+Shift+g registerShortcut(str, 'ctrl+shift+g') |
| * Ctrl+g a registerShortcut(str, 'ctrl+g a') |
| * Ctrl+g Shift+a registerShortcut(str, 'ctrl+g shift+a') |
| * g a registerShortcut(str, 'g a'). |
| */ |
| goog.ui.KeyboardShortcutHandler.prototype.registerShortcut = function( |
| identifier, var_args) { |
| |
| // Add shortcut to shortcuts_ tree |
| goog.ui.KeyboardShortcutHandler.setShortcut_( |
| this.shortcuts_, this.interpretStrokes_(1, arguments), identifier); |
| }; |
| |
| |
| /** |
| * Unregisters a keyboard shortcut by keyCode and modifiers or string |
| * representation of sequence. |
| * |
| * param {number} keyCode Numeric code for key |
| * param {number=} opt_modifiers Bitmap indicating required modifier keys. |
| * goog.ui.KeyboardShortcutHandler.Modifiers.SHIFT, CONTROL, |
| * ALT, or META. |
| * |
| * The two parameters can be repeated any number of times to create a shortcut |
| * using a sequence of strokes. |
| * |
| * A string representation of the shortcut can be supplied instead see |
| * {@link #registerShortcut} for syntax. In that case the method only takes one |
| * argument. |
| * |
| * @param {...(number|string|Array<number>)} var_args String representation, or |
| * array or list of alternating key codes and modifiers. |
| */ |
| goog.ui.KeyboardShortcutHandler.prototype.unregisterShortcut = function( |
| var_args) { |
| // Remove shortcut from tree. |
| goog.ui.KeyboardShortcutHandler.unsetShortcut_( |
| this.shortcuts_, this.interpretStrokes_(0, arguments)); |
| }; |
| |
| |
| /** |
| * Verifies if a particular keyboard shortcut is registered already. It has |
| * the same interface as the unregistering of shortcuts. |
| * |
| * param {number} keyCode Numeric code for key |
| * param {number=} opt_modifiers Bitmap indicating required modifier keys. |
| * goog.ui.KeyboardShortcutHandler.Modifiers.SHIFT, CONTROL, |
| * ALT, or META. |
| * |
| * The two parameters can be repeated any number of times to create a shortcut |
| * using a sequence of strokes. |
| * |
| * A string representation of the shortcut can be supplied instead see |
| * {@link #registerShortcut} for syntax. In that case the method only takes one |
| * argument. |
| * |
| * @param {...(number|string|Array<number>)} var_args String representation, or |
| * array or list of alternating key codes and modifiers. |
| * @return {boolean} Whether the specified keyboard shortcut is registered. |
| */ |
| goog.ui.KeyboardShortcutHandler.prototype.isShortcutRegistered = function( |
| var_args) { |
| return this.checkShortcut_(this.interpretStrokes_(0, arguments)); |
| }; |
| |
| |
| /** |
| * Parses the variable arguments for registerShortcut and unregisterShortcut. |
| * @param {number} initialIndex The first index of "args" to treat as |
| * variable arguments. |
| * @param {Object} args The "arguments" array passed |
| * to registerShortcut or unregisterShortcut. Please see the comments in |
| * registerShortcut for list of allowed forms. |
| * @return {!Array<number>} The sequence of strokes, represented as numbers. |
| * @private |
| */ |
| goog.ui.KeyboardShortcutHandler.prototype.interpretStrokes_ = function( |
| initialIndex, args) { |
| var strokes; |
| |
| // Build strokes array from string. |
| if (goog.isString(args[initialIndex])) { |
| strokes = goog.array.map( |
| goog.ui.KeyboardShortcutHandler.parseStringShortcut(args[initialIndex]), |
| function(stroke) { |
| goog.asserts.assertNumber( |
| stroke.keyCode, 'A non-modifier key is needed in each stroke.'); |
| return goog.ui.KeyboardShortcutHandler.makeStroke_( |
| stroke.keyCode, stroke.modifiers); |
| }); |
| |
| // Build strokes array from arguments list or from array. |
| } else { |
| var strokesArgs = args, i = initialIndex; |
| if (goog.isArray(args[initialIndex])) { |
| strokesArgs = args[initialIndex]; |
| i = 0; |
| } |
| |
| strokes = []; |
| for (; i < strokesArgs.length; i += 2) { |
| strokes.push(goog.ui.KeyboardShortcutHandler.makeStroke_( |
| strokesArgs[i], strokesArgs[i + 1])); |
| } |
| } |
| |
| return strokes; |
| }; |
| |
| |
| /** |
| * Unregisters all keyboard shortcuts. |
| */ |
| goog.ui.KeyboardShortcutHandler.prototype.unregisterAll = function() { |
| this.shortcuts_ = {}; |
| }; |
| |
| |
| /** |
| * Sets the global keys; keys that are safe to always regarded as shortcuts, |
| * even if entered in a textarea or input field. |
| * @param {Array<number>} keys List of keys. |
| */ |
| goog.ui.KeyboardShortcutHandler.prototype.setGlobalKeys = function(keys) { |
| this.globalKeys_ = goog.object.createSet(keys); |
| }; |
| |
| |
| /** |
| * @return {!Array<string>} The global keys, i.e. keys that are safe to always |
| * regard as shortcuts, even if entered in a textarea or input field. |
| */ |
| goog.ui.KeyboardShortcutHandler.prototype.getGlobalKeys = function() { |
| return goog.object.getKeys(this.globalKeys_); |
| }; |
| |
| |
| /** @override */ |
| goog.ui.KeyboardShortcutHandler.prototype.disposeInternal = function() { |
| goog.ui.KeyboardShortcutHandler.superClass_.disposeInternal.call(this); |
| this.unregisterAll(); |
| this.clearKeyListener(); |
| }; |
| |
| |
| /** |
| * Returns event type for a specific shortcut. |
| * @param {string} identifier Identifier for the shortcut task. |
| * @return {string} Theh event type. |
| */ |
| goog.ui.KeyboardShortcutHandler.prototype.getEventType = |
| function(identifier) { |
| |
| return goog.ui.KeyboardShortcutHandler.EventType.SHORTCUT_PREFIX + identifier; |
| }; |
| |
| |
| /** |
| * Builds stroke array from string representation of shortcut. |
| * @param {string} s String representation of shortcut. |
| * @return {!Array<!{keyCode: ?number, modifiers: number}>} The stroke array. A |
| * null keyCode means no non-modifier key was part of the stroke. |
| */ |
| goog.ui.KeyboardShortcutHandler.parseStringShortcut = function(s) { |
| // Normalize whitespace and force to lower case. |
| s = s.replace(/[ +]*\+[ +]*/g, '+').replace(/[ ]+/g, ' ').toLowerCase(); |
| |
| // Build strokes array from string, space separates strokes, plus separates |
| // individual keys. |
| var groups = s.split(' '); |
| var strokes = []; |
| for (var group, i = 0; group = groups[i]; i++) { |
| var keys = group.split('+'); |
| // Explicitly re-initialize key data (JS does not have block scoping). |
| var keyCode = null; |
| var modifiers = goog.ui.KeyboardShortcutHandler.Modifiers.NONE; |
| for (var key, j = 0; key = keys[j]; j++) { |
| switch (key) { |
| case 'shift': |
| modifiers |= goog.ui.KeyboardShortcutHandler.Modifiers.SHIFT; |
| continue; |
| case 'ctrl': |
| modifiers |= goog.ui.KeyboardShortcutHandler.Modifiers.CTRL; |
| continue; |
| case 'alt': |
| modifiers |= goog.ui.KeyboardShortcutHandler.Modifiers.ALT; |
| continue; |
| case 'meta': |
| modifiers |= goog.ui.KeyboardShortcutHandler.Modifiers.META; |
| continue; |
| } |
| if (!goog.isNull(keyCode)) { |
| goog.asserts.fail('At most one non-modifier key can be in a stroke.'); |
| } |
| keyCode = goog.ui.KeyboardShortcutHandler.getKeyCode(key); |
| goog.asserts.assertNumber( |
| keyCode, 'Key name not found in goog.events.KeyNames: ' + key); |
| break; |
| } |
| strokes.push({keyCode: keyCode, modifiers: modifiers}); |
| } |
| |
| return strokes; |
| }; |
| |
| |
| /** |
| * Adds a key event listener that triggers {@link #handleKeyDown_} when keys |
| * are pressed. |
| * @param {goog.events.EventTarget|EventTarget} keyTarget Event target that the |
| * event listener should be attached to. |
| * @protected |
| */ |
| goog.ui.KeyboardShortcutHandler.prototype.initializeKeyListener = |
| function(keyTarget) { |
| this.keyTarget_ = keyTarget; |
| |
| goog.events.listen(this.keyTarget_, goog.events.EventType.KEYDOWN, |
| this.handleKeyDown_, false, this); |
| |
| if (goog.userAgent.GECKO) { |
| goog.events.listen(this.keyTarget_, goog.events.EventType.KEYUP, |
| this.handleGeckoKeyUp_, false, this); |
| } |
| |
| // Windows uses ctrl+alt keys (a.k.a. alt-graph keys) for typing characters |
| // on European keyboards (e.g. ctrl+alt+e for an an euro sign.) Unfortunately, |
| // Windows browsers except Firefox does not have any methods except listening |
| // keypress and keyup events to identify if ctrl+alt keys are really used for |
| // inputting characters. Therefore, we listen to these events and prevent |
| // firing shortcut-key events if ctrl+alt keys are used for typing characters. |
| if (goog.userAgent.WINDOWS && !goog.userAgent.GECKO) { |
| goog.events.listen(this.keyTarget_, goog.events.EventType.KEYPRESS, |
| this.handleWindowsKeyPress_, false, this); |
| goog.events.listen(this.keyTarget_, goog.events.EventType.KEYUP, |
| this.handleWindowsKeyUp_, false, this); |
| } |
| }; |
| |
| |
| /** |
| * Handler for when a keyup event is fired in Firefox (Gecko). |
| * @param {goog.events.BrowserEvent} e The key event. |
| * @private |
| */ |
| goog.ui.KeyboardShortcutHandler.prototype.handleGeckoKeyUp_ = function(e) { |
| // Due to a bug in the way that Gecko on Mac handles cut/copy/paste key events |
| // using the meta key, it is necessary to fake the keyDown for the action keys |
| // (C,V,X) by capturing it on keyUp. |
| // This is because the keyDown events themselves are not fired by the browser |
| // in this case. |
| // Because users will often release the meta key a slight moment before they |
| // release the action key, we need to store whether the meta key has been |
| // released recently to avoid "flaky" cutting/pasting behavior. |
| if (goog.userAgent.MAC) { |
| if (e.keyCode == goog.events.KeyCodes.MAC_FF_META) { |
| this.metaKeyRecentlyReleased_ = true; |
| goog.Timer.callOnce(function() { |
| this.metaKeyRecentlyReleased_ = false; |
| }, 400, this); |
| return; |
| } |
| |
| var metaKey = e.metaKey || this.metaKeyRecentlyReleased_; |
| if ((e.keyCode == goog.events.KeyCodes.C || |
| e.keyCode == goog.events.KeyCodes.X || |
| e.keyCode == goog.events.KeyCodes.V) && metaKey) { |
| e.metaKey = metaKey; |
| this.handleKeyDown_(e); |
| } |
| } |
| |
| // Firefox triggers buttons on space keyUp instead of keyDown. So if space |
| // keyDown activated a shortcut, do NOT also trigger the focused button. |
| if (goog.events.KeyCodes.SPACE == this.activeShortcutKeyForGecko_ && |
| goog.events.KeyCodes.SPACE == e.keyCode) { |
| e.preventDefault(); |
| } |
| this.activeShortcutKeyForGecko_ = null; |
| }; |
| |
| |
| /** |
| * Returns whether this event is possibly used for typing a printable character. |
| * Windows uses ctrl+alt (a.k.a. alt-graph) keys for typing characters on |
| * European keyboards. Since only Firefox provides a method that can identify |
| * whether ctrl+alt keys are used for typing characters, we need to check |
| * whether Windows sends a keypress event to prevent firing shortcut event if |
| * this event is used for typing characters. |
| * @param {goog.events.BrowserEvent} e The key event. |
| * @return {boolean} Whether this event is a possible printable-key event. |
| * @private |
| */ |
| goog.ui.KeyboardShortcutHandler.prototype.isPossiblePrintableKey_ = |
| function(e) { |
| return goog.userAgent.WINDOWS && !goog.userAgent.GECKO && |
| e.ctrlKey && e.altKey && !e.shiftKey; |
| }; |
| |
| |
| /** |
| * Handler for when a keypress event is fired on Windows. |
| * @param {goog.events.BrowserEvent} e The key event. |
| * @private |
| */ |
| goog.ui.KeyboardShortcutHandler.prototype.handleWindowsKeyPress_ = function(e) { |
| // When this keypress event consists of a printable character, set the flag to |
| // prevent firing shortcut key events when we receive the succeeding keyup |
| // event. We accept all Unicode characters except control ones since this |
| // keyCode may be a non-ASCII character. |
| if (e.keyCode > 0x20 && this.isPossiblePrintableKey_(e)) { |
| this.isPrintableKey_ = true; |
| } |
| }; |
| |
| |
| /** |
| * Handler for when a keyup event is fired on Windows. |
| * @param {goog.events.BrowserEvent} e The key event. |
| * @private |
| */ |
| goog.ui.KeyboardShortcutHandler.prototype.handleWindowsKeyUp_ = function(e) { |
| // For possible printable-key events, try firing a shortcut-key event only |
| // when this event is not used for typing a character. |
| if (!this.isPrintableKey_ && this.isPossiblePrintableKey_(e)) { |
| this.handleKeyDown_(e); |
| } |
| }; |
| |
| |
| /** |
| * Removes the listener that was added by link {@link #initializeKeyListener}. |
| * @protected |
| */ |
| goog.ui.KeyboardShortcutHandler.prototype.clearKeyListener = function() { |
| goog.events.unlisten(this.keyTarget_, goog.events.EventType.KEYDOWN, |
| this.handleKeyDown_, false, this); |
| if (goog.userAgent.GECKO) { |
| goog.events.unlisten(this.keyTarget_, goog.events.EventType.KEYUP, |
| this.handleGeckoKeyUp_, false, this); |
| } |
| if (goog.userAgent.WINDOWS && !goog.userAgent.GECKO) { |
| goog.events.unlisten(this.keyTarget_, goog.events.EventType.KEYPRESS, |
| this.handleWindowsKeyPress_, false, this); |
| goog.events.unlisten(this.keyTarget_, goog.events.EventType.KEYUP, |
| this.handleWindowsKeyUp_, false, this); |
| } |
| this.keyTarget_ = null; |
| }; |
| |
| |
| /** |
| * Adds a shortcut stroke sequence to the given sequence tree. Recursive. |
| * @param {!goog.ui.KeyboardShortcutHandler.SequenceTree_} tree The stroke |
| * sequence tree to add to. |
| * @param {Array<number>} strokes Array of strokes for shortcut. |
| * @param {string} identifier Identifier for the task performed by shortcut. |
| * @private |
| */ |
| goog.ui.KeyboardShortcutHandler.setShortcut_ = function( |
| tree, strokes, identifier) { |
| var stroke = strokes.shift(); |
| var node = tree[stroke]; |
| if (node && (strokes.length == 0 || node.shortcut)) { |
| // This new shortcut would override an existing shortcut or shortcut prefix |
| // (since the new strokes end at an existing node), or an existing shortcut |
| // would be triggered by the prefix to this new shortcut (since there is |
| // already a terminal node on the path we are trying to create). |
| throw Error('Keyboard shortcut conflicts with existing shortcut'); |
| } |
| |
| if (strokes.length) { |
| node = goog.object.setIfUndefined(tree, stroke.toString(), |
| goog.ui.KeyboardShortcutHandler.createInternalNode_()); |
| goog.ui.KeyboardShortcutHandler.setShortcut_( |
| goog.asserts.assert(node.next, 'An internal node must have a next map'), |
| strokes, identifier); |
| } else { |
| // Add a terminal node. |
| tree[stroke] = |
| goog.ui.KeyboardShortcutHandler.createTerminalNode_(identifier); |
| } |
| }; |
| |
| |
| /** |
| * Removes a shortcut stroke sequence from the given sequence tree, pruning any |
| * dead branches of the tree. Recursive. |
| * @param {!goog.ui.KeyboardShortcutHandler.SequenceTree_} tree The stroke |
| * sequence tree to remove from. |
| * @param {Array<number>} strokes Array of strokes for shortcut to remove. |
| * @private |
| */ |
| goog.ui.KeyboardShortcutHandler.unsetShortcut_ = function(tree, strokes) { |
| var stroke = strokes.shift(); |
| var node = tree[stroke]; |
| |
| if (!node) { |
| // The given stroke sequence is not in the tree. |
| return; |
| } |
| if (strokes.length == 0) { |
| // Base case - the end of the stroke sequence. |
| if (!node.shortcut) { |
| // The given stroke sequence does not end at a terminal node. |
| return; |
| } |
| delete tree[stroke]; |
| } else { |
| if (!node.next) { |
| // The given stroke sequence is not in the tree. |
| return; |
| } |
| // Recursively remove the rest of the shortcut sequence from the node.next |
| // subtree. |
| goog.ui.KeyboardShortcutHandler.unsetShortcut_(node.next, strokes); |
| if (goog.object.isEmpty(node.next)) { |
| // The node.next subtree is now empty (the last stroke in it was just |
| // removed), so prune this dead branch of the tree. |
| delete tree[stroke]; |
| } |
| } |
| }; |
| |
| |
| /** |
| * Checks if a particular keyboard shortcut is registered. |
| * @param {Array<number>} strokes Strokes array. |
| * @return {boolean} True iff the keyboard is registred. |
| * @private |
| */ |
| goog.ui.KeyboardShortcutHandler.prototype.checkShortcut_ = function(strokes) { |
| var tree = this.shortcuts_; |
| while (strokes.length > 0 && tree) { |
| var node = tree[strokes.shift()]; |
| if (!node) { |
| return false; |
| } |
| if (strokes.length == 0 && node.shortcut) { |
| return true; |
| } |
| tree = node.next; |
| } |
| return false; |
| }; |
| |
| |
| /** |
| * Constructs key from key code and modifiers. |
| * |
| * The lower 8 bits are used for the key code, the following 3 for modifiers and |
| * the remaining bits are unused. |
| * |
| * @param {number} keyCode Numeric key code. |
| * @param {number} modifiers Required modifiers. |
| * @return {number} The key. |
| * @private |
| */ |
| goog.ui.KeyboardShortcutHandler.makeStroke_ = function(keyCode, modifiers) { |
| // Make sure key code is just 8 bits and OR it with the modifiers left shifted |
| // 8 bits. |
| return (keyCode & 255) | (modifiers << 8); |
| }; |
| |
| |
| /** |
| * Keypress handler. |
| * @param {goog.events.BrowserEvent} event Keypress event. |
| * @private |
| */ |
| goog.ui.KeyboardShortcutHandler.prototype.handleKeyDown_ = function(event) { |
| if (!this.isValidShortcut_(event)) { |
| return; |
| } |
| // For possible printable-key events, we cannot identify whether the events |
| // are used for typing characters until we receive respective keyup events. |
| // Therefore, we handle this event when we receive a succeeding keyup event |
| // to verify this event is not used for typing characters. |
| if (event.type == 'keydown' && this.isPossiblePrintableKey_(event)) { |
| this.isPrintableKey_ = false; |
| return; |
| } |
| |
| var keyCode = goog.events.KeyCodes.normalizeKeyCode(event.keyCode); |
| |
| var modifiers = |
| (event.shiftKey ? goog.ui.KeyboardShortcutHandler.Modifiers.SHIFT : 0) | |
| (event.ctrlKey ? goog.ui.KeyboardShortcutHandler.Modifiers.CTRL : 0) | |
| (event.altKey ? goog.ui.KeyboardShortcutHandler.Modifiers.ALT : 0) | |
| (event.metaKey ? goog.ui.KeyboardShortcutHandler.Modifiers.META : 0); |
| var stroke = goog.ui.KeyboardShortcutHandler.makeStroke_(keyCode, modifiers); |
| |
| if (!this.currentTree_[stroke] || this.hasSequenceTimedOut_()) { |
| // Either this stroke does not continue any active sequence, or the |
| // currently active sequence has timed out. Reset shortcut tree progress. |
| this.setCurrentTree_(this.shortcuts_); |
| } |
| |
| var node = this.currentTree_[stroke]; |
| if (!node) { |
| // This stroke does not correspond to a shortcut or continued sequence. |
| return; |
| } |
| if (node.next) { |
| // This stroke does not trigger a shortcut, but entered stroke(s) are a part |
| // of a sequence. Progress in the sequence tree and record time to allow the |
| // following stroke(s) to trigger the shortcut. |
| this.setCurrentTree_(node.next); |
| // Prevent default action so that the rest of the stroke sequence can be |
| // completed. |
| event.preventDefault(); |
| return; |
| } |
| // This stroke triggers a shortcut. Any active sequence has been completed, so |
| // reset the sequence tree. |
| this.setCurrentTree_(this.shortcuts_); |
| |
| // Dispatch the triggered keyboard shortcut event. In addition to the generic |
| // keyboard shortcut event a more specific fine grained one, specific for the |
| // shortcut identifier, is fired. |
| if (this.alwaysPreventDefault_) { |
| event.preventDefault(); |
| } |
| |
| if (this.alwaysStopPropagation_) { |
| event.stopPropagation(); |
| } |
| |
| var shortcut = goog.asserts.assertString( |
| node.shortcut, 'A terminal node must have a string shortcut identifier.'); |
| // Dispatch SHORTCUT_TRIGGERED event |
| var target = /** @type {Node} */ (event.target); |
| var triggerEvent = new goog.ui.KeyboardShortcutEvent( |
| goog.ui.KeyboardShortcutHandler.EventType.SHORTCUT_TRIGGERED, shortcut, |
| target); |
| var retVal = this.dispatchEvent(triggerEvent); |
| |
| // Dispatch SHORTCUT_PREFIX_<identifier> event |
| var prefixEvent = new goog.ui.KeyboardShortcutEvent( |
| goog.ui.KeyboardShortcutHandler.EventType.SHORTCUT_PREFIX + shortcut, |
| shortcut, target); |
| retVal &= this.dispatchEvent(prefixEvent); |
| |
| // The default action is prevented if 'preventDefault' was |
| // called on either event, or if a listener returned false. |
| if (!retVal) { |
| event.preventDefault(); |
| } |
| |
| // For Firefox, track which shortcut key was pushed. |
| if (goog.userAgent.GECKO) { |
| this.activeShortcutKeyForGecko_ = keyCode; |
| } |
| }; |
| |
| |
| /** |
| * Checks if a given keypress event may be treated as a shortcut. |
| * @param {goog.events.BrowserEvent} event Keypress event. |
| * @return {boolean} Whether to attempt to process the event as a shortcut. |
| * @private |
| */ |
| goog.ui.KeyboardShortcutHandler.prototype.isValidShortcut_ = function(event) { |
| var keyCode = event.keyCode; |
| |
| // Ignore Ctrl, Shift and ALT |
| if (keyCode == goog.events.KeyCodes.SHIFT || |
| keyCode == goog.events.KeyCodes.CTRL || |
| keyCode == goog.events.KeyCodes.ALT) { |
| return false; |
| } |
| var el = /** @type {Element} */ (event.target); |
| var isFormElement = |
| el.tagName == 'TEXTAREA' || el.tagName == 'INPUT' || |
| el.tagName == 'BUTTON' || el.tagName == 'SELECT'; |
| |
| var isContentEditable = !isFormElement && (el.isContentEditable || |
| (el.ownerDocument && el.ownerDocument.designMode == 'on')); |
| |
| if (!isFormElement && !isContentEditable) { |
| return true; |
| } |
| // Always allow keys registered as global to be used (typically Esc, the |
| // F-keys and other keys that are not typically used to manipulate text). |
| if (this.globalKeys_[keyCode] || this.allShortcutsAreGlobal_) { |
| return true; |
| } |
| if (isContentEditable) { |
| // For events originating from an element in editing mode we only let |
| // global key codes through. |
| return false; |
| } |
| // Event target is one of (TEXTAREA, INPUT, BUTTON, SELECT). |
| // Allow modifier shortcuts, unless we shouldn't. |
| if (this.modifierShortcutsAreGlobal_ && ( |
| event.altKey || event.ctrlKey || event.metaKey)) { |
| return true; |
| } |
| // Allow ENTER to be used as shortcut for text inputs. |
| if (el.tagName == 'INPUT' && this.textInputs_[el.type]) { |
| return keyCode == goog.events.KeyCodes.ENTER; |
| } |
| // Checkboxes, radiobuttons and buttons. Allow all but SPACE as shortcut. |
| if (el.tagName == 'INPUT' || el.tagName == 'BUTTON') { |
| // TODO(gboyer): If more flexibility is needed, create protected helper |
| // methods for each case (e.g. button, input, etc). |
| if (this.allowSpaceKeyOnButtons_) { |
| return true; |
| } else { |
| return keyCode != goog.events.KeyCodes.SPACE; |
| } |
| } |
| // Don't allow any additional shortcut keys for textareas or selects. |
| return false; |
| }; |
| |
| |
| /** |
| * @return {boolean} True iff the current stroke sequence has timed out. |
| * @private |
| */ |
| goog.ui.KeyboardShortcutHandler.prototype.hasSequenceTimedOut_ = function() { |
| return goog.now() - this.lastStrokeTime_ >= |
| goog.ui.KeyboardShortcutHandler.MAX_KEY_SEQUENCE_DELAY; |
| }; |
| |
| |
| /** |
| * Sets the current keyboard shortcut sequence tree and updates the last stroke |
| * time. |
| * @param {!goog.ui.KeyboardShortcutHandler.SequenceTree_} tree |
| * @private |
| */ |
| goog.ui.KeyboardShortcutHandler.prototype.setCurrentTree_ = function(tree) { |
| this.currentTree_ = tree; |
| this.lastStrokeTime_ = goog.now(); |
| }; |
| |
| |
| |
| /** |
| * Object representing a keyboard shortcut event. |
| * @param {string} type Event type. |
| * @param {string} identifier Task identifier for the triggered shortcut. |
| * @param {Node|goog.events.EventTarget} target Target the original key press |
| * event originated from. |
| * @extends {goog.events.Event} |
| * @constructor |
| * @final |
| */ |
| goog.ui.KeyboardShortcutEvent = function(type, identifier, target) { |
| goog.events.Event.call(this, type, target); |
| |
| /** |
| * Task identifier for the triggered shortcut |
| * @type {string} |
| */ |
| this.identifier = identifier; |
| }; |
| goog.inherits(goog.ui.KeyboardShortcutEvent, goog.events.Event); |