1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225 |
- // 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.provide('goog.ui.KeyboardShortcutHandler.Modifiers');
- goog.require('goog.Timer');
- goog.require('goog.array');
- goog.require('goog.asserts');
- goog.require('goog.dom.TagName');
- 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.events.Keys');
- 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 strings) to the nodes reached by those
- * strokes.
- * @typedef {Object<string, 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, CTRL, 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 varargs the second parameter
- * could also be an array where each element would be regarded 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, CTRL, 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, CTRL, 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.shortcuts_, 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<Array<string>>} The sequence of strokes,
- * represented as arrays of strings.
- * @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.key || '', 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) {
- // keyName == '' because this branch is only run on numbers
- // (corresponding to keyCodes).
- 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} The 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<!{key: ?string, 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 keyName = null;
- 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);
- keyName = key;
- break;
- }
- strokes.push({key: keyName, 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_,
- undefined /* opt_capture */, 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 do not have any methods except listening to 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.events.listen(
- this.keyTarget_, goog.events.EventType.KEYPRESS,
- this.handleWindowsKeyPress_, undefined /* opt_capture */, this);
- }
- goog.events.listen(
- this.keyTarget_, goog.events.EventType.KEYUP, this.handleKeyUp_,
- undefined /* opt_capture */, this);
- };
- /**
- * Handler for when a keyup event is fired. Currently only handled on Windows
- * (all browsers) or Gecko (all platforms).
- * @param {!goog.events.BrowserEvent} e The key event.
- * @private
- */
- goog.ui.KeyboardShortcutHandler.prototype.handleKeyUp_ = function(e) {
- if (goog.userAgent.GECKO) {
- this.handleGeckoKeyUp_(e);
- }
- if (goog.userAgent.WINDOWS) {
- this.handleWindowsKeyUp_(e);
- }
- };
- /**
- * 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 && e.ctrlKey && e.altKey;
- };
- /**
- * 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.WINDOWS) {
- goog.events.unlisten(
- this.keyTarget_, goog.events.EventType.KEYPRESS,
- this.handleWindowsKeyPress_, false, this);
- }
- goog.events.unlisten(
- this.keyTarget_, goog.events.EventType.KEYUP, this.handleKeyUp_, 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<Array<string>>} 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();
- goog.array.forEach(stroke, function(s) {
- var node = tree[s];
- 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) {
- goog.array.forEach(stroke, function(s) {
- var node = goog.object.setIfUndefined(
- tree, s.toString(),
- goog.ui.KeyboardShortcutHandler.createInternalNode_());
- // setShortcut_ modifies strokes
- var strokesCopy = strokes.slice(0);
- goog.ui.KeyboardShortcutHandler.setShortcut_(
- goog.asserts.assert(
- node.next, 'An internal node must have a next map'),
- strokesCopy, identifier);
- });
- } else {
- goog.array.forEach(stroke, function(s) {
- // Add a terminal node.
- tree[s] = 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<Array<string>>} strokes Array of strokes for shortcut to
- * remove.
- * @private
- */
- goog.ui.KeyboardShortcutHandler.unsetShortcut_ = function(tree, strokes) {
- var stroke = strokes.shift();
- goog.array.forEach(stroke, function(s) {
- var node = tree[s];
- 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[s];
- } 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.
- // unsetShortcut_ modifies strokes
- var strokesCopy = strokes.slice(0);
- goog.ui.KeyboardShortcutHandler.unsetShortcut_(node.next, strokesCopy);
- 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[s];
- }
- }
- });
- };
- /**
- * Checks tree for a node matching one of stroke.
- * @param {!goog.ui.KeyboardShortcutHandler.SequenceTree_} tree The
- * stroke sequence tree to find the node in.
- * @param {Array<string>} stroke Stroke to find.
- * @return {goog.ui.KeyboardShortcutHandler.SequenceNode_|undefined} Node matching stroke.
- * @private
- */
- goog.ui.KeyboardShortcutHandler.prototype.getNode_ = function(tree, stroke) {
- for (var i = 0; i < stroke.length; i++) {
- var node = tree[stroke[i]];
- if (!node) {
- continue;
- }
- return node;
- }
- return undefined;
- };
- /**
- * Checks if a particular keyboard shortcut is registered.
- * @param {goog.ui.KeyboardShortcutHandler.SequenceTree_|null} tree The
- * stroke sequence tree to find the keyboard shortcut in.
- * @param {Array<Array<string>>} strokes Strokes array.
- * @return {boolean} True iff the keyboard shortcut is registred.
- * @private
- */
- goog.ui.KeyboardShortcutHandler.prototype.checkShortcut_ = function(
- tree, strokes) {
- while (strokes.length > 0 && tree) {
- var stroke = strokes.shift();
- var node = this.getNode_(tree, stroke);
- if (!node) {
- continue;
- }
- if (strokes.length == 0 && node.shortcut) {
- return true;
- }
- // checkShortcut_ modifies strokes
- var strokesCopy = strokes.slice(0);
- if (this.checkShortcut_(node.next, strokesCopy)) {
- return true;
- }
- }
- return false;
- };
- /**
- * Constructs key identification string from key name, key code and modifiers.
- *
- * @param {string} keyName Key name.
- * @param {number} keyCode Numeric key code.
- * @param {number} modifiers Required modifiers.
- * @return {Array<string>} An array of strings identifying the key/modifier
- * combinations.
- * @private
- */
- goog.ui.KeyboardShortcutHandler.makeStroke_ = function(
- keyName, keyCode, modifiers) {
- var mods = modifiers || 0;
- // entries must be usable as key in a map
- var strokes = ['c_' + keyCode + '_' + mods];
- if (keyName != '') {
- strokes.push('n_' + keyName + '_' + mods);
- }
- return strokes;
- };
- /**
- * Keypress handler.
- * @param {!goog.events.BrowserEvent} event Keypress event.
- * @private
- */
- goog.ui.KeyboardShortcutHandler.prototype.handleKeyDown_ = function(event) {
- if (!this.isValidShortcut_(event)) {
- return;
- }
- var keyCode = goog.events.KeyCodes.normalizeKeyCode(event.keyCode);
- var keyName = event.key;
- 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_(keyName, keyCode, modifiers);
- var node = this.getNode_(this.currentTree_, stroke);
- if (!node || 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_);
- }
- node = this.getNode_(this.currentTree_, stroke);
- if (node && 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);
- }
- // 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. preventDefault is
- // not called on the event to avoid disrupting a character input.
- if (event.type == 'keydown' && this.isPossiblePrintableKey_(event)) {
- this.isPrintableKey_ = false;
- return;
- } else if (!node) {
- // This stroke does not correspond to a shortcut or continued sequence.
- return;
- } else if (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) {
- // Ignore Ctrl, Shift and ALT
- var keyCode = event.keyCode;
- if (event.key != '') {
- var keyName = event.key;
- if (keyName == goog.events.Keys.CTRL || keyName == goog.events.Keys.SHIFT ||
- keyName == goog.events.Keys.ALT ||
- keyName == goog.events.Keys.ALTGRAPH) {
- return false;
- }
- } else {
- 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 == goog.dom.TagName.TEXTAREA ||
- el.tagName == goog.dom.TagName.INPUT ||
- el.tagName == goog.dom.TagName.BUTTON ||
- el.tagName == goog.dom.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 == goog.dom.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 == goog.dom.TagName.INPUT ||
- el.tagName == goog.dom.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);
|