diff options
Diffstat (limited to 'polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html')
-rw-r--r-- | polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html | 588 |
1 files changed, 560 insertions, 28 deletions
diff --git a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html index bd996760bb..af982cfceb 100644 --- a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html +++ b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html @@ -1,4 +1,5 @@ <!-- +@license Copyright (C) 2016 The Android Open Source Project Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,6 +14,88 @@ 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. --> + +<!-- + +How to Add a Keyboard Shortcut +============================== + +A keyboard shortcut is composed of the following parts: + + 1. A semantic identifier (e.g. OPEN_CHANGE, NEXT_PAGE) + 2. Documentation for the keyboard shortcut help dialog + 3. A binding between key combos and the semantic identifier + 4. A binding between the semantic identifier and a listener + +Parts (1) and (2) for all shortcuts are defined in this file. The semantic +identifier is declared in the Shortcut enum near the head of this script: + + const Shortcut = { + // ... + TOGGLE_LEFT_PANE: 'TOGGLE_LEFT_PANE', + // ... + }; + +Immediately following the Shortcut enum definition, there is a _describe +function defined which is then invoked many times to populate the help dialog. +Add a new invocation here to document the shortcut: + + _describe(Shortcut.TOGGLE_LEFT_PANE, ShortcutSection.DIFFS, + 'Hide/show left diff'); + +When an attached view binds one or more key combos to this shortcut, the help +dialog will display this text in the given section (in this case, "Diffs"). See +the ShortcutSection enum immediately below for the list of supported sections. + +Part (3), the actual key bindings, are declared by gr-app. In the future, this +system may be expanded to allow key binding customizations by plugins or user +preferences. Key bindings are defined in the following forms: + + // Ordinary shortcut with a single binding. + this.bindShortcut( + this.Shortcut.TOGGLE_LEFT_PANE, 'shift+a'); + + // Ordinary shortcut with multiple bindings. + this.bindShortcut( + this.Shortcut.CURSOR_NEXT_FILE, 'j', 'down'); + + // A "go-key" keyboard shortcut, which is combined with a previously and + // continuously pressed "go" key (the go-key is hard-coded as 'g'). + this.bindShortcut( + this.Shortcut.GO_TO_OPENED_CHANGES, this.GO_KEY, 'o'); + + // A "doc-only" keyboard shortcut. This declares the key-binding for help + // dialog purposes, but doesn't actually implement the binding. It is up + // to some element to implement this binding using iron-a11y-keys-behavior's + // keyBindings property. + this.bindShortcut( + this.Shortcut.EXPAND_ALL_COMMENT_THREADS, this.DOC_ONLY, 'e'); + +Part (4), the listener definitions, are declared by the view or element that +implements the shortcut behavior. This is done by implementing a method named +keyboardShortcuts() in an element that mixes in this behavior, returning an +object that maps semantic identifiers (as property names) to listener method +names, like this: + + keyboardShortcuts() { + return { + [this.Shortcut.TOGGLE_LEFT_PANE]: '_handleToggleLeftPane', + }; + }, + +You can implement key bindings in an element that is hosted by a view IF that +element is always attached exactly once under that view (e.g. the search bar in +gr-app). When that is not the case, you will have to define a doc-only binding +in gr-app, declare the shortcut in the view that hosts the element, and use +iron-a11y-keys-behavior's keyBindings attribute to implement the binding in the +element. An example of this is in comment threads. A diff view supports actions +on comment threads, but there may be zero or many comment threads attached at +any given point. So the shortcut is declared as doc-only by the diff view and +by gr-app, and actually implemented by gr-diff-comment-thread. + +NOTE: doc-only shortcuts will not be customizable in the same way that other +shortcuts are. +--> <link rel="import" href="../../bower_components/polymer/polymer.html"> <link rel="import" href="../../bower_components/iron-a11y-keys-behavior/iron-a11y-keys-behavior.html"> @@ -20,10 +103,194 @@ limitations under the License. (function(window) { 'use strict'; + const DOC_ONLY = 'DOC_ONLY'; + const GO_KEY = 'GO_KEY'; + + // The maximum age of a keydown event to be used in a jump navigation. This + // is only for cases when the keyup event is lost. + const GO_KEY_TIMEOUT_MS = 1000; + + const ShortcutSection = { + ACTIONS: 'Actions', + DIFFS: 'Diffs', + EVERYWHERE: 'Everywhere', + FILE_LIST: 'File list', + NAVIGATION: 'Navigation', + REPLY_DIALOG: 'Reply dialog', + }; + + const Shortcut = { + OPEN_SHORTCUT_HELP_DIALOG: 'OPEN_SHORTCUT_HELP_DIALOG', + GO_TO_OPENED_CHANGES: 'GO_TO_OPENED_CHANGES', + GO_TO_MERGED_CHANGES: 'GO_TO_MERGED_CHANGES', + GO_TO_ABANDONED_CHANGES: 'GO_TO_ABANDONED_CHANGES', + + CURSOR_NEXT_CHANGE: 'CURSOR_NEXT_CHANGE', + CURSOR_PREV_CHANGE: 'CURSOR_PREV_CHANGE', + OPEN_CHANGE: 'OPEN_CHANGE', + NEXT_PAGE: 'NEXT_PAGE', + PREV_PAGE: 'PREV_PAGE', + TOGGLE_CHANGE_REVIEWED: 'TOGGLE_CHANGE_REVIEWED', + TOGGLE_CHANGE_STAR: 'TOGGLE_CHANGE_STAR', + REFRESH_CHANGE_LIST: 'REFRESH_CHANGE_LIST', + + OPEN_REPLY_DIALOG: 'OPEN_REPLY_DIALOG', + OPEN_DOWNLOAD_DIALOG: 'OPEN_DOWNLOAD_DIALOG', + EXPAND_ALL_MESSAGES: 'EXPAND_ALL_MESSAGES', + COLLAPSE_ALL_MESSAGES: 'COLLAPSE_ALL_MESSAGES', + UP_TO_DASHBOARD: 'UP_TO_DASHBOARD', + UP_TO_CHANGE: 'UP_TO_CHANGE', + TOGGLE_DIFF_MODE: 'TOGGLE_DIFF_MODE', + REFRESH_CHANGE: 'REFRESH_CHANGE', + EDIT_TOPIC: 'EDIT_TOPIC', + + NEXT_LINE: 'NEXT_LINE', + PREV_LINE: 'PREV_LINE', + NEXT_CHUNK: 'NEXT_CHUNK', + PREV_CHUNK: 'PREV_CHUNK', + EXPAND_ALL_DIFF_CONTEXT: 'EXPAND_ALL_DIFF_CONTEXT', + NEXT_COMMENT_THREAD: 'NEXT_COMMENT_THREAD', + PREV_COMMENT_THREAD: 'PREV_COMMENT_THREAD', + EXPAND_ALL_COMMENT_THREADS: 'EXPAND_ALL_COMMENT_THREADS', + COLLAPSE_ALL_COMMENT_THREADS: 'COLLAPSE_ALL_COMMENT_THREADS', + LEFT_PANE: 'LEFT_PANE', + RIGHT_PANE: 'RIGHT_PANE', + TOGGLE_LEFT_PANE: 'TOGGLE_LEFT_PANE', + NEW_COMMENT: 'NEW_COMMENT', + SAVE_COMMENT: 'SAVE_COMMENT', + OPEN_DIFF_PREFS: 'OPEN_DIFF_PREFS', + TOGGLE_DIFF_REVIEWED: 'TOGGLE_DIFF_REVIEWED', + + NEXT_FILE: 'NEXT_FILE', + PREV_FILE: 'PREV_FILE', + NEXT_FILE_WITH_COMMENTS: 'NEXT_FILE_WITH_COMMENTS', + PREV_FILE_WITH_COMMENTS: 'PREV_FILE_WITH_COMMENTS', + NEXT_UNREVIEWED_FILE: 'NEXT_UNREVIEWED_FILE', + CURSOR_NEXT_FILE: 'CURSOR_NEXT_FILE', + CURSOR_PREV_FILE: 'CURSOR_PREV_FILE', + OPEN_FILE: 'OPEN_FILE', + TOGGLE_FILE_REVIEWED: 'TOGGLE_FILE_REVIEWED', + TOGGLE_ALL_INLINE_DIFFS: 'TOGGLE_ALL_INLINE_DIFFS', + TOGGLE_INLINE_DIFF: 'TOGGLE_INLINE_DIFF', + + OPEN_FIRST_FILE: 'OPEN_FIRST_FILE', + OPEN_LAST_FILE: 'OPEN_LAST_FILE', + + SEARCH: 'SEARCH', + SEND_REPLY: 'SEND_REPLY', + }; + + const _help = new Map(); + + function _describe(shortcut, section, text) { + if (!_help.has(section)) { + _help.set(section, []); + } + _help.get(section).push({shortcut, text}); + } + + _describe(Shortcut.SEARCH, ShortcutSection.EVERYWHERE, 'Search'); + _describe(Shortcut.OPEN_SHORTCUT_HELP_DIALOG, ShortcutSection.EVERYWHERE, + 'Show this dialog'); + _describe(Shortcut.GO_TO_OPENED_CHANGES, ShortcutSection.EVERYWHERE, + 'Go to Opened Changes'); + _describe(Shortcut.GO_TO_MERGED_CHANGES, ShortcutSection.EVERYWHERE, + 'Go to Merged Changes'); + _describe(Shortcut.GO_TO_ABANDONED_CHANGES, ShortcutSection.EVERYWHERE, + 'Go to Abandoned Changes'); + + _describe(Shortcut.CURSOR_NEXT_CHANGE, ShortcutSection.ACTIONS, + 'Select next change'); + _describe(Shortcut.CURSOR_PREV_CHANGE, ShortcutSection.ACTIONS, + 'Select previous change'); + _describe(Shortcut.OPEN_CHANGE, ShortcutSection.ACTIONS, + 'Show selected change'); + _describe(Shortcut.NEXT_PAGE, ShortcutSection.ACTIONS, 'Go to next page'); + _describe(Shortcut.PREV_PAGE, ShortcutSection.ACTIONS, 'Go to previous page'); + _describe(Shortcut.OPEN_REPLY_DIALOG, ShortcutSection.ACTIONS, + 'Open reply dialog to publish comments and add reviewers'); + _describe(Shortcut.OPEN_DOWNLOAD_DIALOG, ShortcutSection.ACTIONS, + 'Open download overlay'); + _describe(Shortcut.EXPAND_ALL_MESSAGES, ShortcutSection.ACTIONS, + 'Expand all messages'); + _describe(Shortcut.COLLAPSE_ALL_MESSAGES, ShortcutSection.ACTIONS, + 'Collapse all messages'); + _describe(Shortcut.REFRESH_CHANGE, ShortcutSection.ACTIONS, + 'Reload the change at the latest patch'); + _describe(Shortcut.TOGGLE_CHANGE_REVIEWED, ShortcutSection.ACTIONS, + 'Mark/unmark change as reviewed'); + _describe(Shortcut.TOGGLE_FILE_REVIEWED, ShortcutSection.ACTIONS, + 'Toggle review flag on selected file'); + _describe(Shortcut.REFRESH_CHANGE_LIST, ShortcutSection.ACTIONS, + 'Refresh list of changes'); + _describe(Shortcut.TOGGLE_CHANGE_STAR, ShortcutSection.ACTIONS, + 'Star/unstar change'); + _describe(Shortcut.EDIT_TOPIC, ShortcutSection.ACTIONS, + 'Add a change topic'); + + _describe(Shortcut.NEXT_LINE, ShortcutSection.DIFFS, 'Go to next line'); + _describe(Shortcut.PREV_LINE, ShortcutSection.DIFFS, 'Go to previous line'); + _describe(Shortcut.NEXT_CHUNK, ShortcutSection.DIFFS, + 'Go to next diff chunk'); + _describe(Shortcut.PREV_CHUNK, ShortcutSection.DIFFS, + 'Go to previous diff chunk'); + _describe(Shortcut.EXPAND_ALL_DIFF_CONTEXT, ShortcutSection.DIFFS, + 'Expand all diff context'); + _describe(Shortcut.NEXT_COMMENT_THREAD, ShortcutSection.DIFFS, + 'Go to next comment thread'); + _describe(Shortcut.PREV_COMMENT_THREAD, ShortcutSection.DIFFS, + 'Go to previous comment thread'); + _describe(Shortcut.EXPAND_ALL_COMMENT_THREADS, ShortcutSection.DIFFS, + 'Expand all comment threads'); + _describe(Shortcut.COLLAPSE_ALL_COMMENT_THREADS, ShortcutSection.DIFFS, + 'Collapse all comment threads'); + _describe(Shortcut.LEFT_PANE, ShortcutSection.DIFFS, 'Select left pane'); + _describe(Shortcut.RIGHT_PANE, ShortcutSection.DIFFS, 'Select right pane'); + _describe(Shortcut.TOGGLE_LEFT_PANE, ShortcutSection.DIFFS, + 'Hide/show left diff'); + _describe(Shortcut.NEW_COMMENT, ShortcutSection.DIFFS, 'Draft new comment'); + _describe(Shortcut.SAVE_COMMENT, ShortcutSection.DIFFS, 'Save comment'); + _describe(Shortcut.OPEN_DIFF_PREFS, ShortcutSection.DIFFS, + 'Show diff preferences'); + _describe(Shortcut.TOGGLE_DIFF_REVIEWED, ShortcutSection.DIFFS, + 'Mark/unmark file as reviewed'); + _describe(Shortcut.TOGGLE_DIFF_MODE, ShortcutSection.DIFFS, + 'Toggle unified/side-by-side diff'); + _describe(Shortcut.NEXT_UNREVIEWED_FILE, ShortcutSection.DIFFS, + 'Mark file as reviewed and go to next unreviewed file'); + + _describe(Shortcut.NEXT_FILE, ShortcutSection.NAVIGATION, 'Select next file'); + _describe(Shortcut.PREV_FILE, ShortcutSection.NAVIGATION, + 'Select previous file'); + _describe(Shortcut.NEXT_FILE_WITH_COMMENTS, ShortcutSection.NAVIGATION, + 'Select next file that has comments'); + _describe(Shortcut.PREV_FILE_WITH_COMMENTS, ShortcutSection.NAVIGATION, + 'Select previous file that has comments'); + _describe(Shortcut.OPEN_FIRST_FILE, ShortcutSection.NAVIGATION, + 'Show first file'); + _describe(Shortcut.OPEN_LAST_FILE, ShortcutSection.NAVIGATION, + 'Show last file'); + _describe(Shortcut.UP_TO_DASHBOARD, ShortcutSection.NAVIGATION, + 'Up to dashboard'); + _describe(Shortcut.UP_TO_CHANGE, ShortcutSection.NAVIGATION, 'Up to change'); + + _describe(Shortcut.CURSOR_NEXT_FILE, ShortcutSection.FILE_LIST, + 'Select next file'); + _describe(Shortcut.CURSOR_PREV_FILE, ShortcutSection.FILE_LIST, + 'Select previous file'); + _describe(Shortcut.OPEN_FILE, ShortcutSection.FILE_LIST, + 'Go to selected file'); + _describe(Shortcut.TOGGLE_ALL_INLINE_DIFFS, ShortcutSection.FILE_LIST, + 'Show/hide all inline diffs'); + _describe(Shortcut.TOGGLE_INLINE_DIFF, ShortcutSection.FILE_LIST, + 'Show/hide selected inline diff'); + + _describe(Shortcut.SEND_REPLY, ShortcutSection.REPLY_DIALOG, 'Send reply'); + // Must be declared outside behavior implementation to be accessed inside // behavior functions. - /** @return {!Object} */ + /** @return {!(Event|PolymerDomApi|PolymerEventApi)} */ const getKeyboardEvent = function(e) { e = Polymer.dom(e.detail ? e.detail.keyboardEvent : e); // When e is a keyboardEvent, e.event is not null. @@ -31,42 +298,307 @@ limitations under the License. return e; }; - window.Gerrit = window.Gerrit || {}; + class ShortcutManager { + constructor() { + this.activeHosts = new Map(); + this.bindings = new Map(); + this.listeners = new Set(); + } - /** @polymerBehavior KeyboardShortcutBehavior */ - Gerrit.KeyboardShortcutBehavior = [{ - modifierPressed(e) { - e = getKeyboardEvent(e); - return e.altKey || e.ctrlKey || e.metaKey || e.shiftKey; - }, + bindShortcut(shortcut, ...bindings) { + this.bindings.set(shortcut, bindings); + } - isModifierPressed(e, modifier) { - return getKeyboardEvent(e)[modifier]; - }, + getBindingsForShortcut(shortcut) { + return this.bindings.get(shortcut); + } - shouldSuppressKeyboardShortcut(e) { - e = getKeyboardEvent(e); - const tagName = Polymer.dom(e).rootTarget.tagName; - if (tagName === 'INPUT' || tagName === 'TEXTAREA') { + attachHost(host) { + if (!host.keyboardShortcuts) { return; } + const shortcuts = host.keyboardShortcuts(); + this.activeHosts.set(host, new Map(Object.entries(shortcuts))); + this.notifyListeners(); + return shortcuts; + } + + detachHost(host) { + if (this.activeHosts.delete(host)) { + this.notifyListeners(); return true; } - for (let i = 0; e.path && i < e.path.length; i++) { - if (e.path[i].tagName === 'GR-OVERLAY') { return true; } - } return false; - }, + } - // Alias for getKeyboardEvent. - /** @return {!Object} */ - getKeyboardEvent(e) { - return getKeyboardEvent(e); - }, + addListener(listener) { + this.listeners.add(listener); + listener(this.directoryView()); + } - getRootTarget(e) { - return Polymer.dom(getKeyboardEvent(e)).rootTarget; - }, - }, + removeListener(listener) { + return this.listeners.delete(listener); + } + + activeShortcutsBySection() { + const activeShortcuts = new Set(); + this.activeHosts.forEach(shortcuts => { + shortcuts.forEach((_, shortcut) => activeShortcuts.add(shortcut)); + }); + + const activeShortcutsBySection = new Map(); + _help.forEach((shortcutList, section) => { + shortcutList.forEach(shortcutHelp => { + if (activeShortcuts.has(shortcutHelp.shortcut)) { + if (!activeShortcutsBySection.has(section)) { + activeShortcutsBySection.set(section, []); + } + activeShortcutsBySection.get(section).push(shortcutHelp); + } + }); + }); + return activeShortcutsBySection; + } + + directoryView() { + const view = new Map(); + this.activeShortcutsBySection().forEach((shortcutHelps, section) => { + const sectionView = []; + shortcutHelps.forEach(shortcutHelp => { + const bindingDesc = this.describeBindings(shortcutHelp.shortcut); + if (!bindingDesc) { return; } + this.distributeBindingDesc(bindingDesc).forEach(bindingDesc => { + sectionView.push({ + binding: bindingDesc, + text: shortcutHelp.text, + }); + }); + }); + view.set(section, sectionView); + }); + return view; + } + + distributeBindingDesc(bindingDesc) { + if (bindingDesc.length === 1 || + this.comboSetDisplayWidth(bindingDesc) < 21) { + return [bindingDesc]; + } + // Find the largest prefix of bindings that is under the + // size threshold. + const head = [bindingDesc[0]]; + for (let i = 1; i < bindingDesc.length; i++) { + head.push(bindingDesc[i]); + if (this.comboSetDisplayWidth(head) >= 21) { + head.pop(); + return [head].concat( + this.distributeBindingDesc(bindingDesc.slice(i))); + } + } + } + + comboSetDisplayWidth(bindingDesc) { + const bindingSizer = binding => binding.reduce( + (acc, key) => acc + key.length, 0); + // Width is the sum of strings + (n-1) * 2 to account for the word + // "or" joining them. + return bindingDesc.reduce( + (acc, binding) => acc + bindingSizer(binding), 0) + + 2 * (bindingDesc.length - 1); + } + + describeBindings(shortcut) { + const bindings = this.bindings.get(shortcut); + if (!bindings) { return null; } + if (bindings[0] === GO_KEY) { + return [['g'].concat(bindings.slice(1))]; + } + return bindings + .filter(binding => binding !== DOC_ONLY) + .map(binding => this.describeBinding(binding)); + } + + describeBinding(binding) { + return binding.split(':')[0].split('+').map(part => { + switch (part) { + case 'shift': + return 'Shift'; + case 'meta': + return 'Meta'; + case 'ctrl': + return 'Ctrl'; + case 'enter': + return 'Enter'; + case 'up': + return '↑'; + case 'down': + return '↓'; + case 'left': + return '←'; + case 'right': + return '→'; + default: + return part; + } + }); + } + + notifyListeners() { + const view = this.directoryView(); + this.listeners.forEach(listener => listener(view)); + } + } + + const shortcutManager = new ShortcutManager(); + + window.Gerrit = window.Gerrit || {}; + + /** @polymerBehavior KeyboardShortcutBehavior */ + Gerrit.KeyboardShortcutBehavior = [ Polymer.IronA11yKeysBehavior, + { + // Exports for convenience. Note: Closure compiler crashes when + // object-shorthand syntax is used here. + // eslint-disable-next-line object-shorthand + DOC_ONLY: DOC_ONLY, + // eslint-disable-next-line object-shorthand + GO_KEY: GO_KEY, + // eslint-disable-next-line object-shorthand + Shortcut: Shortcut, + + properties: { + _shortcut_go_key_last_pressed: { + type: Number, + value: null, + }, + + _shortcut_go_table: { + type: Array, + value() { return new Map(); }, + }, + }, + + modifierPressed(e) { + e = getKeyboardEvent(e); + return e.altKey || e.ctrlKey || e.metaKey || e.shiftKey; + }, + + isModifierPressed(e, modifier) { + return getKeyboardEvent(e)[modifier]; + }, + + shouldSuppressKeyboardShortcut(e) { + e = getKeyboardEvent(e); + const tagName = Polymer.dom(e).rootTarget.tagName; + if (tagName === 'INPUT' || tagName === 'TEXTAREA' || + (e.keyCode === 13 && tagName === 'A')) { + // Suppress shortcuts if the key is 'enter' and target is an anchor. + return true; + } + for (let i = 0; e.path && i < e.path.length; i++) { + if (e.path[i].tagName === 'GR-OVERLAY') { return true; } + } + return false; + }, + + // Alias for getKeyboardEvent. + /** @return {!(Event|PolymerDomApi|PolymerEventApi)} */ + getKeyboardEvent(e) { + return getKeyboardEvent(e); + }, + + getRootTarget(e) { + return Polymer.dom(getKeyboardEvent(e)).rootTarget; + }, + + bindShortcut(shortcut, ...bindings) { + shortcutManager.bindShortcut(shortcut, ...bindings); + }, + + _addOwnKeyBindings(shortcut, handler) { + const bindings = shortcutManager.getBindingsForShortcut(shortcut); + if (!bindings) { + return; + } + if (bindings[0] === DOC_ONLY) { + return; + } + if (bindings[0] === GO_KEY) { + this._shortcut_go_table.set(bindings[1], handler); + } else { + this.addOwnKeyBinding(bindings.join(' '), handler); + } + }, + + attached() { + const shortcuts = shortcutManager.attachHost(this); + if (!shortcuts) { return; } + + for (const key of Object.keys(shortcuts)) { + this._addOwnKeyBindings(key, shortcuts[key]); + } + + // If any of the shortcuts utilized GO_KEY, then they are handled + // directly by this behavior. + if (this._shortcut_go_table.size > 0) { + this.addOwnKeyBinding('g:keydown', '_handleGoKeyDown'); + this.addOwnKeyBinding('g:keyup', '_handleGoKeyUp'); + this._shortcut_go_table.forEach((handler, key) => { + this.addOwnKeyBinding(key, '_handleGoAction'); + }); + } + }, + + detached() { + if (shortcutManager.detachHost(this)) { + this.removeOwnKeyBindings(); + } + }, + + keyboardShortcuts() { + return {}; + }, + + addKeyboardShortcutDirectoryListener(listener) { + shortcutManager.addListener(listener); + }, + + removeKeyboardShortcutDirectoryListener(listener) { + shortcutManager.removeListener(listener); + }, + + _handleGoKeyDown(e) { + if (this.modifierPressed(e)) { return; } + this._shortcut_go_key_last_pressed = Date.now(); + }, + + _handleGoKeyUp(e) { + this._shortcut_go_key_last_pressed = null; + }, + + _handleGoAction(e) { + if (!this._shortcut_go_key_last_pressed || + (Date.now() - this._shortcut_go_key_last_pressed > + GO_KEY_TIMEOUT_MS) || + !this._shortcut_go_table.has(e.detail.key) || + this.shouldSuppressKeyboardShortcut(e)) { + return; + } + e.preventDefault(); + const handler = this._shortcut_go_table.get(e.detail.key); + this[handler](e); + }, + }, ]; + + Gerrit.KeyboardShortcutBinder = { + DOC_ONLY, + GO_KEY, + Shortcut, + ShortcutManager, + ShortcutSection, + + bindShortcut(shortcut, ...bindings) { + shortcutManager.bindShortcut(shortcut, ...bindings); + }, + }; })(window); </script> |