blob: 92ecb8d1547cbaa2069b53d6f5dc376a733ffad7 [file] [log] [blame]
/**
* @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.
*/
import {IronA11yKeysBehavior} from '@polymer/iron-a11y-keys-behavior/iron-a11y-keys-behavior.js';
import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
const DOC_ONLY = 'DOC_ONLY';
const GO_KEY = 'GO_KEY';
const V_KEY = 'V_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 V_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',
DIFF_AGAINST_BASE: 'DIFF_AGAINST_BASE',
DIFF_AGAINST_LATEST: 'DIFF_AGAINST_LATEST',
DIFF_BASE_AGAINST_LEFT: 'DIFF_BASE_AGAINST_LEFT',
DIFF_RIGHT_AGAINST_LATEST: 'DIFF_RIGHT_AGAINST_LATEST',
DIFF_BASE_AGAINST_LATEST: 'DIFF_BASE_AGAINST_LATEST',
NEXT_LINE: 'NEXT_LINE',
PREV_LINE: 'PREV_LINE',
VISIBLE_LINE: 'VISIBLE_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',
TOGGLE_HIDE_ALL_COMMENT_THREADS: 'TOGGLE_HIDE_ALL_COMMENT_THREADS',
OPEN_FIRST_FILE: 'OPEN_FIRST_FILE',
OPEN_LAST_FILE: 'OPEN_LAST_FILE',
SEARCH: 'SEARCH',
SEND_REPLY: 'SEND_REPLY',
EMOJI_DROPDOWN: 'EMOJI_DROPDOWN',
TOGGLE_BLAME: 'TOGGLE_BLAME',
};
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.DIFF_AGAINST_BASE, ShortcutSection.ACTIONS,
'Diff against base');
_describe(Shortcut.DIFF_AGAINST_LATEST, ShortcutSection.ACTIONS,
'Diff against latest patchset');
_describe(Shortcut.DIFF_BASE_AGAINST_LEFT, ShortcutSection.ACTIONS,
'Diff base against left');
_describe(Shortcut.DIFF_RIGHT_AGAINST_LATEST, ShortcutSection.ACTIONS,
'Diff right against latest');
_describe(Shortcut.DIFF_BASE_AGAINST_LATEST, ShortcutSection.ACTIONS,
'Diff base against latest');
_describe(Shortcut.NEXT_LINE, ShortcutSection.DIFFS, 'Go to next line');
_describe(Shortcut.PREV_LINE, ShortcutSection.DIFFS, 'Go to previous line');
_describe(Shortcut.DIFF_AGAINST_BASE, ShortcutSection.DIFFS,
'Diff against base');
_describe(Shortcut.DIFF_AGAINST_LATEST, ShortcutSection.DIFFS,
'Diff against latest patchset');
_describe(Shortcut.DIFF_BASE_AGAINST_LEFT, ShortcutSection.DIFFS,
'Diff base against left');
_describe(Shortcut.DIFF_RIGHT_AGAINST_LATEST, ShortcutSection.DIFFS,
'Diff right against latest');
_describe(Shortcut.DIFF_BASE_AGAINST_LATEST, ShortcutSection.DIFFS,
'Diff base against latest');
_describe(Shortcut.VISIBLE_LINE, ShortcutSection.DIFFS,
'Move cursor to currently visible code');
_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.TOGGLE_HIDE_ALL_COMMENT_THREADS, ShortcutSection.DIFFS,
'Hide/Display 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.TOGGLE_BLAME, ShortcutSection.DIFFS, 'Toggle blame');
_describe(Shortcut.NEXT_FILE, ShortcutSection.NAVIGATION, 'Go to next file');
_describe(Shortcut.PREV_FILE, ShortcutSection.NAVIGATION,
'Go to previous file');
_describe(Shortcut.NEXT_FILE_WITH_COMMENTS, ShortcutSection.NAVIGATION,
'Go to next file that has comments');
_describe(Shortcut.PREV_FILE_WITH_COMMENTS, ShortcutSection.NAVIGATION,
'Go to previous file that has comments');
_describe(Shortcut.OPEN_FIRST_FILE, ShortcutSection.NAVIGATION,
'Go to first file');
_describe(Shortcut.OPEN_LAST_FILE, ShortcutSection.NAVIGATION,
'Go to 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_HIDE_ALL_COMMENT_THREADS, ShortcutSection.FILE_LIST,
'Hide/Display all comment threads');
_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 = 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);
}
getDescription(section, shortcutName) {
const binding =
_help.get(section).find(binding => binding.shortcut == shortcutName);
return binding ? binding.text : '';
}
getShortcut(shortcutName) {
const binding = this.bindings.get(shortcutName);
return binding ? this.describeBinding(binding) : '';
}
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 bindings.slice(1).map(
binding => this._describeKey(binding)
)
.map(binding => ['g'].concat(binding));
}
if (bindings[0] === V_KEY) {
return bindings.slice(1).map(
binding => this._describeKey(binding)
)
.map(binding => ['v'].concat(binding));
}
return bindings
.filter(binding => binding !== DOC_ONLY)
.map(binding => this.describeBinding(binding));
}
_describeKey(key) {
switch (key) {
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 key;
}
}
describeBinding(binding) {
if (binding.length === 1) {
return [binding];
}
return binding.split(':')[0].split('+').map(part =>
this._describeKey(part)
);
}
notifyListeners() {
const view = this.directoryView();
this.listeners.forEach(listener => listener(view));
}
}
const shortcutManager = new ShortcutManager();
/** @polymerBehavior Gerrit.KeyboardShortcutBehavior*/
export const KeyboardShortcutBehavior = [
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
V_KEY: V_KEY,
// eslint-disable-next-line object-shorthand
Shortcut: Shortcut,
// eslint-disable-next-line object-shorthand
ShortcutSection: ShortcutSection,
properties: {
_shortcut_go_key_last_pressed: {
type: Number,
value: null,
},
_shortcut_go_table: {
type: Array,
value() { return new Map(); },
},
_shortcut_v_table: {
type: Array,
value() { return new Map(); },
},
},
modifierPressed(e) {
e = getKeyboardEvent(e);
return e.altKey || e.ctrlKey || e.metaKey || e.shiftKey ||
!!this._inGoKeyMode() || !!this._inVKeyMode();
},
isModifierPressed(e, modifier) {
return getKeyboardEvent(e)[modifier];
},
shouldSuppressKeyboardShortcut(e) {
e = getKeyboardEvent(e);
const tagName = 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; }
}
this.dispatchEvent(new CustomEvent('shortcut-triggered', {
detail: {
event: e,
goKey: this._inGoKeyMode(),
},
composed: true, bubbles: true,
}));
return false;
},
// Alias for getKeyboardEvent.
/** @return {!Event} */
getKeyboardEvent(e) {
return getKeyboardEvent(e);
},
getRootTarget(e) {
return dom(getKeyboardEvent(e)).rootTarget;
},
bindShortcut(shortcut, ...bindings) {
shortcutManager.bindShortcut(shortcut, ...bindings);
},
createTitle(shortcutName, section) {
const desc = shortcutManager.getDescription(section, shortcutName);
const shortcut = shortcutManager.getShortcut(shortcutName);
return (desc && shortcut) ? `${desc} (shortcut: ${shortcut})` : '';
},
_addOwnKeyBindings(shortcut, handler) {
const bindings = shortcutManager.getBindingsForShortcut(shortcut);
if (!bindings) {
return;
}
if (bindings[0] === DOC_ONLY) {
return;
}
if (bindings[0] === GO_KEY) {
bindings.slice(1).forEach(binding =>
this._shortcut_go_table.set(binding, handler));
} else if (bindings[0] === V_KEY) {
// for each binding added with the go/v key, we set the handler to be
// handleVKeyAction. handleVKeyAction then looks up in th
// shortcut_table to see what the relevant handler should be
bindings.slice(1).forEach(binding =>
this._shortcut_v_table.set(binding, handler));
} else {
this.addOwnKeyBinding(bindings.join(' '), handler);
}
},
/** @override */
attached() {
const shortcuts = shortcutManager.attachHost(this);
if (!shortcuts) { return; }
for (const key of Object.keys(shortcuts)) {
this._addOwnKeyBindings(key, shortcuts[key]);
}
// each component that uses this behaviour must be aware if go key is
// pressed or not, since it needs to check it as a modifier
this.addOwnKeyBinding('g:keydown', '_handleGoKeyDown');
this.addOwnKeyBinding('g:keyup', '_handleGoKeyUp');
// If any of the shortcuts utilized GO_KEY, then they are handled
// directly by this behavior.
if (this._shortcut_go_table.size > 0) {
this._shortcut_go_table.forEach((handler, key) => {
this.addOwnKeyBinding(key, '_handleGoAction');
});
}
this.addOwnKeyBinding('v:keydown', '_handleVKeyDown');
this.addOwnKeyBinding('v:keyup', '_handleVKeyUp');
if (this._shortcut_v_table.size > 0) {
this._shortcut_v_table.forEach((handler, key) => {
this.addOwnKeyBinding(key, '_handleVAction');
});
}
},
/** @override */
detached() {
if (shortcutManager.detachHost(this)) {
this.removeOwnKeyBindings();
}
},
keyboardShortcuts() {
return {};
},
addKeyboardShortcutDirectoryListener(listener) {
shortcutManager.addListener(listener);
},
removeKeyboardShortcutDirectoryListener(listener) {
shortcutManager.removeListener(listener);
},
_handleVKeyDown(e) {
this._shortcut_v_key_last_pressed = Date.now();
},
_handleVKeyUp(e) {
setTimeout(() => {
this._shortcut_v_key_last_pressed = null;
}, V_KEY_TIMEOUT_MS);
},
_inVKeyMode() {
return this._shortcut_v_key_last_pressed &&
(Date.now() - this._shortcut_v_key_last_pressed <=
V_KEY_TIMEOUT_MS);
},
_handleVAction(e) {
if (!this._inVKeyMode() ||
!this._shortcut_v_table.has(e.detail.key) ||
this.shouldSuppressKeyboardShortcut(e)) {
return;
}
e.preventDefault();
const handler = this._shortcut_v_table.get(e.detail.key);
this[handler](e);
},
_handleGoKeyDown(e) {
this._shortcut_go_key_last_pressed = Date.now();
},
_handleGoKeyUp(e) {
// Set go_key_last_pressed to null `GO_KEY_TIMEOUT_MS` after keyup event
// so that users can trigger `g + i` by pressing g and i quickly.
setTimeout(() => {
this._shortcut_go_key_last_pressed = null;
}, GO_KEY_TIMEOUT_MS);
},
_inGoKeyMode() {
return this._shortcut_go_key_last_pressed &&
(Date.now() - this._shortcut_go_key_last_pressed <=
GO_KEY_TIMEOUT_MS);
},
_handleGoAction(e) {
if (!this._inGoKeyMode() ||
!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);
},
},
];
export const KeyboardShortcutBinder = {
DOC_ONLY,
GO_KEY,
V_KEY,
Shortcut,
ShortcutManager,
ShortcutSection,
bindShortcut(shortcut, ...bindings) {
shortcutManager.bindShortcut(shortcut, ...bindings);
},
};
// TODO(dmfilippov) Remove the following lines with assignments
// Plugins can use the behavior because it was accessible with
// the global Gerrit... variable. To avoid breaking changes in plugins
// temporary assign global variables.
window.Gerrit = window.Gerrit || {};
window.Gerrit.KeyboardShortcutBehavior = KeyboardShortcutBehavior;
window.Gerrit.KeyboardShortcutBinder = KeyboardShortcutBinder;