summaryrefslogtreecommitdiffstats
path: root/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html
diff options
context:
space:
mode:
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.html588
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>