| <!-- |
| @license |
| Copyright (C) 2016 The Android Open Source Project |
| |
| 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. |
| --> |
| |
| <!-- |
| |
| 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-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"> |
| |
| <script> |
| (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_USER_DASHBOARD: 'GO_TO_USER_DASHBOARD', |
| GO_TO_OPENED_CHANGES: 'GO_TO_OPENED_CHANGES', |
| GO_TO_MERGED_CHANGES: 'GO_TO_MERGED_CHANGES', |
| GO_TO_ABANDONED_CHANGES: 'GO_TO_ABANDONED_CHANGES', |
| GO_TO_WATCHED_CHANGES: 'GO_TO_WATCHED_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', |
| EMOJI_DROPDOWN: 'EMOJI_DROPDOWN', |
| }; |
| |
| 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_USER_DASHBOARD, ShortcutSection.EVERYWHERE, |
| 'Go to User Dashboard'); |
| _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.GO_TO_WATCHED_CHANGES, ShortcutSection.EVERYWHERE, |
| 'Go to Watched 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'); |
| _describe(Shortcut.EMOJI_DROPDOWN, ShortcutSection.REPLY_DIALOG, |
| 'Emoji dropdown'); |
| |
| // Must be declared outside behavior implementation to be accessed inside |
| // behavior functions. |
| |
| /** @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. |
| if (e.event) { e = e.event; } |
| return e; |
| }; |
| |
| class ShortcutManager { |
| constructor() { |
| this.activeHosts = new Map(); |
| this.bindings = new Map(); |
| this.listeners = new Set(); |
| } |
| |
| bindShortcut(shortcut, ...bindings) { |
| this.bindings.set(shortcut, bindings); |
| } |
| |
| getBindingsForShortcut(shortcut) { |
| return this.bindings.get(shortcut); |
| } |
| |
| 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; |
| } |
| return false; |
| } |
| |
| addListener(listener) { |
| this.listeners.add(listener); |
| listener(this.directoryView()); |
| } |
| |
| 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) { |
| if (binding.length === 1) { |
| return [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 Gerrit.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} */ |
| 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> |