Merge "Redesign the keyboard shortcuts system" into stable-2.16
This commit is contained in:
commit
e311accd71
@ -14,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">
|
||||
|
||||
@ -21,10 +103,188 @@ 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',
|
||||
|
||||
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',
|
||||
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.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_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.
|
||||
@ -32,44 +292,307 @@ limitations under the License.
|
||||
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) {
|
||||
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 = [{
|
||||
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 {!Object} */
|
||||
getKeyboardEvent(e) {
|
||||
return getKeyboardEvent(e);
|
||||
},
|
||||
|
||||
getRootTarget(e) {
|
||||
return Polymer.dom(getKeyboardEvent(e)).rootTarget;
|
||||
},
|
||||
},
|
||||
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>
|
||||
|
@ -40,6 +40,8 @@ limitations under the License.
|
||||
|
||||
<script>
|
||||
suite('keyboard-shortcut-behavior tests', () => {
|
||||
const kb = window.Gerrit.KeyboardShortcutBinder;
|
||||
|
||||
let element;
|
||||
let overlay;
|
||||
let sandbox;
|
||||
@ -67,6 +69,228 @@ limitations under the License.
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
suite('ShortcutManager', () => {
|
||||
test('bindings management', () => {
|
||||
const mgr = new kb.ShortcutManager();
|
||||
const {NEXT_FILE} = kb.Shortcut;
|
||||
|
||||
assert.isUndefined(mgr.getBindingsForShortcut(NEXT_FILE));
|
||||
mgr.bindShortcut(NEXT_FILE, ']', '}', 'right');
|
||||
assert.deepEqual(
|
||||
mgr.getBindingsForShortcut(NEXT_FILE),
|
||||
[']', '}', 'right']);
|
||||
});
|
||||
|
||||
suite('binding descriptions', () => {
|
||||
function mapToObject(m) {
|
||||
const o = {};
|
||||
m.forEach((v, k) => o[k] = v);
|
||||
return o;
|
||||
}
|
||||
|
||||
test('single combo description', () => {
|
||||
const mgr = new kb.ShortcutManager();
|
||||
assert.deepEqual(mgr.describeBinding('a'), ['a']);
|
||||
assert.deepEqual(mgr.describeBinding('a:keyup'), ['a']);
|
||||
assert.deepEqual(mgr.describeBinding('ctrl+a'), ['Ctrl', 'a']);
|
||||
assert.deepEqual(
|
||||
mgr.describeBinding('ctrl+shift+up:keyup'),
|
||||
['Ctrl', 'Shift', '↑']);
|
||||
});
|
||||
|
||||
test('combo set description', () => {
|
||||
const {GO_KEY, DOC_ONLY, ShortcutManager} = kb;
|
||||
const {GO_TO_OPENED_CHANGES, NEXT_FILE, PREV_FILE} = kb.Shortcut;
|
||||
|
||||
const mgr = new ShortcutManager();
|
||||
assert.isNull(mgr.describeBindings(NEXT_FILE));
|
||||
|
||||
mgr.bindShortcut(GO_TO_OPENED_CHANGES, GO_KEY, 'o');
|
||||
assert.deepEqual(
|
||||
mgr.describeBindings(GO_TO_OPENED_CHANGES),
|
||||
[['g', 'o']]);
|
||||
|
||||
mgr.bindShortcut(NEXT_FILE, DOC_ONLY, ']', 'ctrl+shift+right:keyup');
|
||||
assert.deepEqual(
|
||||
mgr.describeBindings(NEXT_FILE),
|
||||
[[']'], ['Ctrl', 'Shift', '→']]);
|
||||
|
||||
mgr.bindShortcut(PREV_FILE, '[');
|
||||
assert.deepEqual(mgr.describeBindings(PREV_FILE), [['[']]);
|
||||
});
|
||||
|
||||
test('combo set description width', () => {
|
||||
const mgr = new kb.ShortcutManager();
|
||||
assert.strictEqual(mgr.comboSetDisplayWidth([['u']]), 1);
|
||||
assert.strictEqual(mgr.comboSetDisplayWidth([['g', 'o']]), 2);
|
||||
assert.strictEqual(mgr.comboSetDisplayWidth([['Shift', 'r']]), 6);
|
||||
assert.strictEqual(mgr.comboSetDisplayWidth([['x'], ['y']]), 4);
|
||||
assert.strictEqual(
|
||||
mgr.comboSetDisplayWidth([['x'], ['y'], ['Shift', 'z']]),
|
||||
12);
|
||||
});
|
||||
|
||||
test('distribute shortcut help', () => {
|
||||
const mgr = new kb.ShortcutManager();
|
||||
assert.deepEqual(mgr.distributeBindingDesc([['o']]), [[['o']]]);
|
||||
assert.deepEqual(
|
||||
mgr.distributeBindingDesc([['g', 'o']]),
|
||||
[[['g', 'o']]]);
|
||||
assert.deepEqual(
|
||||
mgr.distributeBindingDesc([['ctrl', 'shift', 'meta', 'enter']]),
|
||||
[[['ctrl', 'shift', 'meta', 'enter']]]);
|
||||
assert.deepEqual(
|
||||
mgr.distributeBindingDesc([
|
||||
['ctrl', 'shift', 'meta', 'enter'],
|
||||
['o'],
|
||||
]),
|
||||
[
|
||||
[['ctrl', 'shift', 'meta', 'enter']],
|
||||
[['o']],
|
||||
]);
|
||||
assert.deepEqual(
|
||||
mgr.distributeBindingDesc([
|
||||
['ctrl', 'enter'],
|
||||
['meta', 'enter'],
|
||||
['ctrl', 's'],
|
||||
['meta', 's'],
|
||||
]),
|
||||
[
|
||||
[['ctrl', 'enter'], ['meta', 'enter']],
|
||||
[['ctrl', 's'], ['meta', 's']],
|
||||
]);
|
||||
});
|
||||
|
||||
test('active shortcuts by section', () => {
|
||||
const {NEXT_FILE, NEXT_LINE, GO_TO_OPENED_CHANGES, SEARCH} =
|
||||
kb.Shortcut;
|
||||
const {DIFFS, EVERYWHERE, NAVIGATION} = kb.ShortcutSection;
|
||||
|
||||
const mgr = new kb.ShortcutManager();
|
||||
mgr.bindShortcut(NEXT_FILE, ']');
|
||||
mgr.bindShortcut(NEXT_LINE, 'j');
|
||||
mgr.bindShortcut(GO_TO_OPENED_CHANGES, 'g+o');
|
||||
mgr.bindShortcut(SEARCH, '/');
|
||||
|
||||
assert.deepEqual(
|
||||
mapToObject(mgr.activeShortcutsBySection()),
|
||||
{});
|
||||
|
||||
mgr.attachHost({
|
||||
keyboardShortcuts() {
|
||||
return {
|
||||
[NEXT_FILE]: null,
|
||||
};
|
||||
},
|
||||
});
|
||||
assert.deepEqual(
|
||||
mapToObject(mgr.activeShortcutsBySection()),
|
||||
{
|
||||
[NAVIGATION]: [
|
||||
{shortcut: NEXT_FILE, text: 'Select next file'},
|
||||
],
|
||||
});
|
||||
|
||||
mgr.attachHost({
|
||||
keyboardShortcuts() {
|
||||
return {
|
||||
[NEXT_LINE]: null,
|
||||
};
|
||||
},
|
||||
});
|
||||
assert.deepEqual(
|
||||
mapToObject(mgr.activeShortcutsBySection()),
|
||||
{
|
||||
[DIFFS]: [
|
||||
{shortcut: NEXT_LINE, text: 'Go to next line'},
|
||||
],
|
||||
[NAVIGATION]: [
|
||||
{shortcut: NEXT_FILE, text: 'Select next file'},
|
||||
],
|
||||
});
|
||||
|
||||
mgr.attachHost({
|
||||
keyboardShortcuts() {
|
||||
return {
|
||||
[SEARCH]: null,
|
||||
[GO_TO_OPENED_CHANGES]: null,
|
||||
};
|
||||
},
|
||||
});
|
||||
assert.deepEqual(
|
||||
mapToObject(mgr.activeShortcutsBySection()),
|
||||
{
|
||||
[DIFFS]: [
|
||||
{shortcut: NEXT_LINE, text: 'Go to next line'},
|
||||
],
|
||||
[EVERYWHERE]: [
|
||||
{shortcut: SEARCH, text: 'Search'},
|
||||
{
|
||||
shortcut: GO_TO_OPENED_CHANGES,
|
||||
text: 'Go to Opened Changes',
|
||||
},
|
||||
],
|
||||
[NAVIGATION]: [
|
||||
{shortcut: NEXT_FILE, text: 'Select next file'},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('directory view', () => {
|
||||
const {
|
||||
NEXT_FILE, NEXT_LINE, GO_TO_OPENED_CHANGES, SEARCH,
|
||||
SAVE_COMMENT,
|
||||
} = kb.Shortcut;
|
||||
const {DIFFS, EVERYWHERE, NAVIGATION} = kb.ShortcutSection;
|
||||
const {GO_KEY, ShortcutManager} = kb;
|
||||
|
||||
const mgr = new ShortcutManager();
|
||||
mgr.bindShortcut(NEXT_FILE, ']');
|
||||
mgr.bindShortcut(NEXT_LINE, 'j');
|
||||
mgr.bindShortcut(GO_TO_OPENED_CHANGES, GO_KEY, 'o');
|
||||
mgr.bindShortcut(SEARCH, '/');
|
||||
mgr.bindShortcut(
|
||||
SAVE_COMMENT, 'ctrl+enter', 'meta+enter', 'ctrl+s', 'meta+s');
|
||||
|
||||
assert.deepEqual(mapToObject(mgr.directoryView()), {});
|
||||
|
||||
mgr.attachHost({
|
||||
keyboardShortcuts() {
|
||||
return {
|
||||
[GO_TO_OPENED_CHANGES]: null,
|
||||
[NEXT_FILE]: null,
|
||||
[NEXT_LINE]: null,
|
||||
[SAVE_COMMENT]: null,
|
||||
[SEARCH]: null,
|
||||
};
|
||||
},
|
||||
});
|
||||
assert.deepEqual(
|
||||
mapToObject(mgr.directoryView()),
|
||||
{
|
||||
[DIFFS]: [
|
||||
{binding: [['j']], text: 'Go to next line'},
|
||||
{
|
||||
binding: [['Ctrl', 'Enter'], ['Meta', 'Enter']],
|
||||
text: 'Save comment',
|
||||
},
|
||||
{
|
||||
binding: [['Ctrl', 's'], ['Meta', 's']],
|
||||
text: 'Save comment',
|
||||
},
|
||||
],
|
||||
[EVERYWHERE]: [
|
||||
{binding: [['/']], text: 'Search'},
|
||||
{binding: [['g', 'o']], text: 'Go to Opened Changes'},
|
||||
],
|
||||
[NAVIGATION]: [
|
||||
{binding: [[']']], text: 'Select next file'},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('doesn’t block kb shortcuts for non-whitelisted els', done => {
|
||||
const divEl = document.createElement('div');
|
||||
element.appendChild(divEl);
|
||||
@ -160,5 +384,56 @@ limitations under the License.
|
||||
MockInteractions.keyDownOn(element, 75, 'alt', 'k');
|
||||
assert.isFalse(spy.lastCall.returnValue);
|
||||
});
|
||||
|
||||
suite('GO_KEY timing', () => {
|
||||
let handlerStub;
|
||||
|
||||
setup(() => {
|
||||
element._shortcut_go_table.set('a', '_handleA');
|
||||
handlerStub = element._handleA = sinon.stub();
|
||||
sandbox.stub(Date, 'now').returns(10000);
|
||||
});
|
||||
|
||||
test('success', () => {
|
||||
const e = {detail: {key: 'a'}, preventDefault: () => {}};
|
||||
sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
|
||||
element._shortcut_go_key_last_pressed = 9000;
|
||||
element._handleGoAction(e);
|
||||
assert.isTrue(handlerStub.calledOnce);
|
||||
assert.strictEqual(handlerStub.lastCall.args[0], e);
|
||||
});
|
||||
|
||||
test('go key not pressed', () => {
|
||||
const e = {detail: {key: 'a'}, preventDefault: () => {}};
|
||||
sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
|
||||
element._shortcut_go_key_last_pressed = null;
|
||||
element._handleGoAction(e);
|
||||
assert.isFalse(handlerStub.called);
|
||||
});
|
||||
|
||||
test('go key pressed too long ago', () => {
|
||||
const e = {detail: {key: 'a'}, preventDefault: () => {}};
|
||||
sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
|
||||
element._shortcut_go_key_last_pressed = 3000;
|
||||
element._handleGoAction(e);
|
||||
assert.isFalse(handlerStub.called);
|
||||
});
|
||||
|
||||
test('should suppress', () => {
|
||||
const e = {detail: {key: 'a'}, preventDefault: () => {}};
|
||||
sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(true);
|
||||
element._shortcut_go_key_last_pressed = 9000;
|
||||
element._handleGoAction(e);
|
||||
assert.isFalse(handlerStub.called);
|
||||
});
|
||||
|
||||
test('unrecognized key', () => {
|
||||
const e = {detail: {key: 'f'}, preventDefault: () => {}};
|
||||
sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
|
||||
element._shortcut_go_key_last_pressed = 9000;
|
||||
element._handleGoAction(e);
|
||||
assert.isFalse(handlerStub.called);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
@ -106,17 +106,6 @@
|
||||
Gerrit.URLEncodingBehavior,
|
||||
],
|
||||
|
||||
keyBindings: {
|
||||
'j': '_handleJKey',
|
||||
'k': '_handleKKey',
|
||||
'n ]': '_handleNKey',
|
||||
'o': '_handleOKey',
|
||||
'p [': '_handlePKey',
|
||||
'r': '_handleRKey',
|
||||
'shift+r': '_handleShiftRKey',
|
||||
's': '_handleSKey',
|
||||
},
|
||||
|
||||
listeners: {
|
||||
keydown: '_scopedKeydownHandler',
|
||||
},
|
||||
@ -126,6 +115,19 @@
|
||||
'_computePreferences(account, preferences)',
|
||||
],
|
||||
|
||||
keyboardShortcuts() {
|
||||
return {
|
||||
[this.Shortcut.CURSOR_NEXT_CHANGE]: '_nextChange',
|
||||
[this.Shortcut.CURSOR_PREV_CHANGE]: '_prevChange',
|
||||
[this.Shortcut.NEXT_PAGE]: '_nextPage',
|
||||
[this.Shortcut.PREV_PAGE]: '_prevPage',
|
||||
[this.Shortcut.OPEN_CHANGE]: '_openChange',
|
||||
[this.Shortcut.TOGGLE_CHANGE_REVIEWED]: '_toggleChangeReviewed',
|
||||
[this.Shortcut.TOGGLE_CHANGE_STAR]: '_toggleChangeStar',
|
||||
[this.Shortcut.REFRESH_CHANGE_LIST]: '_refreshChangeList',
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Iron-a11y-keys-behavior catches keyboard events globally. Some keyboard
|
||||
* events must be scoped to a component level (e.g. `enter`) in order to not
|
||||
@ -136,7 +138,7 @@
|
||||
_scopedKeydownHandler(e) {
|
||||
if (e.keyCode === 13) {
|
||||
// Enter.
|
||||
this._handleOKey(e);
|
||||
this._openChange(e);
|
||||
}
|
||||
},
|
||||
|
||||
@ -238,7 +240,7 @@
|
||||
return account._account_id === change.assignee._account_id;
|
||||
},
|
||||
|
||||
_handleJKey(e) {
|
||||
_nextChange(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e) ||
|
||||
this.modifierPressed(e)) { return; }
|
||||
|
||||
@ -246,7 +248,7 @@
|
||||
this.$.cursor.next();
|
||||
},
|
||||
|
||||
_handleKKey(e) {
|
||||
_prevChange(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e) ||
|
||||
this.modifierPressed(e)) { return; }
|
||||
|
||||
@ -254,7 +256,7 @@
|
||||
this.$.cursor.previous();
|
||||
},
|
||||
|
||||
_handleOKey(e) {
|
||||
_openChange(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e) ||
|
||||
this.modifierPressed(e)) { return; }
|
||||
|
||||
@ -262,7 +264,7 @@
|
||||
Gerrit.Nav.navigateToChange(this._changeForIndex(this.selectedIndex));
|
||||
},
|
||||
|
||||
_handleNKey(e) {
|
||||
_nextPage(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e) ||
|
||||
this.modifierPressed(e) && !this.isModifierPressed(e, 'shiftKey')) {
|
||||
return;
|
||||
@ -272,7 +274,7 @@
|
||||
this.fire('next-page');
|
||||
},
|
||||
|
||||
_handlePKey(e) {
|
||||
_prevPage(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e) ||
|
||||
this.modifierPressed(e) && !this.isModifierPressed(e, 'shiftKey')) {
|
||||
return;
|
||||
@ -282,7 +284,7 @@
|
||||
this.fire('previous-page');
|
||||
},
|
||||
|
||||
_handleRKey(e) {
|
||||
_toggleChangeReviewed(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e) ||
|
||||
this.modifierPressed(e)) { return; }
|
||||
|
||||
@ -300,7 +302,7 @@
|
||||
changeEl.toggleReviewed();
|
||||
},
|
||||
|
||||
_handleShiftRKey(e) {
|
||||
_refreshChangeList(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e)) { return; }
|
||||
|
||||
e.preventDefault();
|
||||
@ -311,7 +313,7 @@
|
||||
window.location.reload();
|
||||
},
|
||||
|
||||
_handleSKey(e) {
|
||||
_toggleChangeStar(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e) ||
|
||||
this.modifierPressed(e)) { return; }
|
||||
|
||||
|
@ -42,6 +42,17 @@ limitations under the License.
|
||||
|
||||
<script>
|
||||
suite('gr-change-list basic tests', () => {
|
||||
// Define keybindings before attaching other fixtures.
|
||||
const kb = window.Gerrit.KeyboardShortcutBinder;
|
||||
kb.bindShortcut(kb.Shortcut.CURSOR_NEXT_CHANGE, 'j');
|
||||
kb.bindShortcut(kb.Shortcut.CURSOR_PREV_CHANGE, 'k');
|
||||
kb.bindShortcut(kb.Shortcut.OPEN_CHANGE, 'o');
|
||||
kb.bindShortcut(kb.Shortcut.REFRESH_CHANGE_LIST, 'shift+r');
|
||||
kb.bindShortcut(kb.Shortcut.TOGGLE_CHANGE_REVIEWED, 'r');
|
||||
kb.bindShortcut(kb.Shortcut.TOGGLE_CHANGE_STAR, 's');
|
||||
kb.bindShortcut(kb.Shortcut.NEXT_PAGE, 'n');
|
||||
kb.bindShortcut(kb.Shortcut.NEXT_PAGE, 'p');
|
||||
|
||||
let element;
|
||||
let sandbox;
|
||||
|
||||
|
@ -268,16 +268,20 @@
|
||||
'_patchNumChanged(_patchRange.patchNum)',
|
||||
],
|
||||
|
||||
keyBindings: {
|
||||
'shift+r': '_handleCapitalRKey',
|
||||
'a': '_handleAKey',
|
||||
'd': '_handleDKey',
|
||||
'm': '_handleMKey',
|
||||
's': '_handleSKey',
|
||||
'u': '_handleUKey',
|
||||
'x': '_handleXKey',
|
||||
'z': '_handleZKey',
|
||||
',': '_handleCommaKey',
|
||||
keyboardShortcuts() {
|
||||
return {
|
||||
[this.Shortcut.SEND_REPLY]: null, // DOC_ONLY binding
|
||||
[this.Shortcut.REFRESH_CHANGE]: '_handleRefreshChange',
|
||||
[this.Shortcut.OPEN_REPLY_DIALOG]: '_handleOpenReplyDialog',
|
||||
[this.Shortcut.OPEN_DOWNLOAD_DIALOG]:
|
||||
'_handleOpenDownloadDialogShortcut',
|
||||
[this.Shortcut.TOGGLE_DIFF_MODE]: '_handleToggleDiffMode',
|
||||
[this.Shortcut.TOGGLE_CHANGE_STAR]: '_handleToggleChangeStar',
|
||||
[this.Shortcut.UP_TO_DASHBOARD]: '_handleUpToDashboard',
|
||||
[this.Shortcut.EXPAND_ALL_MESSAGES]: '_handleExpandAllMessages',
|
||||
[this.Shortcut.COLLAPSE_ALL_MESSAGES]: '_handleCollapseAllMessages',
|
||||
[this.Shortcut.OPEN_DIFF_PREFS]: '_handleOpenDiffPrefsShortcut',
|
||||
};
|
||||
},
|
||||
|
||||
attached() {
|
||||
@ -341,7 +345,7 @@
|
||||
});
|
||||
},
|
||||
|
||||
_handleMKey(e) {
|
||||
_handleToggleDiffMode(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e) ||
|
||||
this.modifierPressed(e)) { return; }
|
||||
|
||||
@ -899,7 +903,7 @@
|
||||
return label;
|
||||
},
|
||||
|
||||
_handleAKey(e) {
|
||||
_handleOpenReplyDialog(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e) ||
|
||||
this.modifierPressed(e)) {
|
||||
return;
|
||||
@ -915,7 +919,7 @@
|
||||
});
|
||||
},
|
||||
|
||||
_handleDKey(e) {
|
||||
_handleOpenDownloadDialogShortcut(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e) ||
|
||||
this.modifierPressed(e)) { return; }
|
||||
|
||||
@ -923,13 +927,13 @@
|
||||
this.$.downloadOverlay.open();
|
||||
},
|
||||
|
||||
_handleCapitalRKey(e) {
|
||||
_handleRefreshChange(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e)) { return; }
|
||||
e.preventDefault();
|
||||
Gerrit.Nav.navigateToChange(this._change);
|
||||
},
|
||||
|
||||
_handleSKey(e) {
|
||||
_handleToggleChangeStar(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e) ||
|
||||
this.modifierPressed(e)) { return; }
|
||||
|
||||
@ -937,7 +941,7 @@
|
||||
this.$.changeStar.toggleStar();
|
||||
},
|
||||
|
||||
_handleUKey(e) {
|
||||
_handleUpToDashboard(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e) ||
|
||||
this.modifierPressed(e)) { return; }
|
||||
|
||||
@ -945,7 +949,7 @@
|
||||
this._determinePageBack();
|
||||
},
|
||||
|
||||
_handleXKey(e) {
|
||||
_handleExpandAllMessages(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e) ||
|
||||
this.modifierPressed(e)) { return; }
|
||||
|
||||
@ -953,7 +957,7 @@
|
||||
this.messagesList.handleExpandCollapse(true);
|
||||
},
|
||||
|
||||
_handleZKey(e) {
|
||||
_handleCollapseAllMessages(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e) ||
|
||||
this.modifierPressed(e)) { return; }
|
||||
|
||||
@ -961,7 +965,7 @@
|
||||
this.messagesList.handleExpandCollapse(false);
|
||||
},
|
||||
|
||||
_handleCommaKey(e) {
|
||||
_handleOpenDiffPrefsShortcut(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e) ||
|
||||
this.modifierPressed(e)) { return; }
|
||||
|
||||
|
@ -43,6 +43,18 @@ limitations under the License.
|
||||
|
||||
<script>
|
||||
suite('gr-change-view tests', () => {
|
||||
const kb = window.Gerrit.KeyboardShortcutBinder;
|
||||
kb.bindShortcut(kb.Shortcut.SEND_REPLY, 'ctrl+enter');
|
||||
kb.bindShortcut(kb.Shortcut.REFRESH_CHANGE, 'shift+r');
|
||||
kb.bindShortcut(kb.Shortcut.OPEN_REPLY_DIALOG, 'a');
|
||||
kb.bindShortcut(kb.Shortcut.OPEN_DOWNLOAD_DIALOG, 'd');
|
||||
kb.bindShortcut(kb.Shortcut.TOGGLE_DIFF_MODE, 'm');
|
||||
kb.bindShortcut(kb.Shortcut.TOGGLE_CHANGE_STAR, 's');
|
||||
kb.bindShortcut(kb.Shortcut.UP_TO_DASHBOARD, 'u');
|
||||
kb.bindShortcut(kb.Shortcut.EXPAND_ALL_MESSAGES, 'x');
|
||||
kb.bindShortcut(kb.Shortcut.COLLAPSE_ALL_MESSAGES, 'z');
|
||||
kb.bindShortcut(kb.Shortcut.OPEN_DIFF_PREFS, ',');
|
||||
|
||||
let element;
|
||||
let sandbox;
|
||||
let navigateToChangeStub;
|
||||
@ -277,11 +289,11 @@ limitations under the License.
|
||||
flushAsynchronousOperations();
|
||||
|
||||
element.viewState.diffMode = 'SIDE_BY_SIDE';
|
||||
element._handleMKey(e);
|
||||
element._handleToggleDiffMode(e);
|
||||
assert.isTrue(setModeStub.calledWith('UNIFIED_DIFF'));
|
||||
|
||||
element.viewState.diffMode = 'UNIFIED_DIFF';
|
||||
element._handleMKey(e);
|
||||
element._handleToggleDiffMode(e);
|
||||
assert.isTrue(setModeStub.calledWith('SIDE_BY_SIDE'));
|
||||
});
|
||||
});
|
||||
|
@ -197,23 +197,33 @@
|
||||
],
|
||||
|
||||
keyBindings: {
|
||||
'shift+left': '_handleShiftLeftKey',
|
||||
'shift+right': '_handleShiftRightKey',
|
||||
'i:keyup': '_handleIKey',
|
||||
'shift+i:keyup': '_handleCapitalIKey',
|
||||
'down j': '_handleDownKey',
|
||||
'up k': '_handleUpKey',
|
||||
'c': '_handleCKey',
|
||||
'[': '_handleLeftBracketKey',
|
||||
']': '_handleRightBracketKey',
|
||||
'o': '_handleOKey',
|
||||
'n': '_handleNKey',
|
||||
'p': '_handlePKey',
|
||||
'r': '_handleRKey',
|
||||
'shift+a': '_handleCapitalAKey',
|
||||
'esc': '_handleEscKey',
|
||||
esc: '_handleEscKey',
|
||||
},
|
||||
|
||||
keyboardShortcuts() {
|
||||
return {
|
||||
[this.Shortcut.LEFT_PANE]: '_handleLeftPane',
|
||||
[this.Shortcut.RIGHT_PANE]: '_handleRightPane',
|
||||
[this.Shortcut.TOGGLE_INLINE_DIFF]: '_handleToggleInlineDiff',
|
||||
[this.Shortcut.TOGGLE_ALL_INLINE_DIFFS]: '_handleToggleAllInlineDiffs',
|
||||
[this.Shortcut.CURSOR_NEXT_FILE]: '_handleCursorNext',
|
||||
[this.Shortcut.CURSOR_PREV_FILE]: '_handleCursorPrev',
|
||||
[this.Shortcut.NEXT_LINE]: '_handleCursorNext',
|
||||
[this.Shortcut.PREV_LINE]: '_handleCursorPrev',
|
||||
[this.Shortcut.NEW_COMMENT]: '_handleNewComment',
|
||||
[this.Shortcut.OPEN_LAST_FILE]: '_handleOpenLastFile',
|
||||
[this.Shortcut.OPEN_FIRST_FILE]: '_handleOpenFirstFile',
|
||||
[this.Shortcut.OPEN_FILE]: '_handleOpenFile',
|
||||
[this.Shortcut.NEXT_CHUNK]: '_handleNextChunk',
|
||||
[this.Shortcut.PREV_CHUNK]: '_handlePrevChunk',
|
||||
[this.Shortcut.TOGGLE_FILE_REVIEWED]: '_handleToggleFileReviewed',
|
||||
[this.Shortcut.TOGGLE_LEFT_PANE]: '_handleToggleLeftPane',
|
||||
|
||||
// Final two are actually handled by gr-diff-comment-thread.
|
||||
[this.Shortcut.EXPAND_ALL_COMMENT_THREADS]: null,
|
||||
[this.Shortcut.COLLAPSE_ALL_COMMENT_THREADS]: null,
|
||||
};
|
||||
},
|
||||
listeners: {
|
||||
keydown: '_scopedKeydownHandler',
|
||||
},
|
||||
@ -232,7 +242,7 @@
|
||||
_scopedKeydownHandler(e) {
|
||||
if (e.keyCode === 13) {
|
||||
// Enter.
|
||||
this._handleOKey(e);
|
||||
this._handleOpenFile(e);
|
||||
}
|
||||
},
|
||||
|
||||
@ -536,7 +546,7 @@
|
||||
this._togglePathExpanded(path);
|
||||
},
|
||||
|
||||
_handleShiftLeftKey(e) {
|
||||
_handleLeftPane(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e) || this._noDiffsExpanded()) {
|
||||
return;
|
||||
}
|
||||
@ -545,7 +555,7 @@
|
||||
this.$.diffCursor.moveLeft();
|
||||
},
|
||||
|
||||
_handleShiftRightKey(e) {
|
||||
_handleRightPane(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e) || this._noDiffsExpanded()) {
|
||||
return;
|
||||
}
|
||||
@ -554,7 +564,7 @@
|
||||
this.$.diffCursor.moveRight();
|
||||
},
|
||||
|
||||
_handleIKey(e) {
|
||||
_handleToggleInlineDiff(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e) ||
|
||||
this.modifierPressed(e) ||
|
||||
this.$.fileCursor.index === -1) { return; }
|
||||
@ -563,14 +573,14 @@
|
||||
this._togglePathExpandedByIndex(this.$.fileCursor.index);
|
||||
},
|
||||
|
||||
_handleCapitalIKey(e) {
|
||||
_handleToggleAllInlineDiffs(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e)) { return; }
|
||||
|
||||
e.preventDefault();
|
||||
this._toggleInlineDiffs();
|
||||
},
|
||||
|
||||
_handleDownKey(e) {
|
||||
_handleCursorNext(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
|
||||
return;
|
||||
}
|
||||
@ -588,7 +598,7 @@
|
||||
}
|
||||
},
|
||||
|
||||
_handleUpKey(e) {
|
||||
_handleCursorPrev(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
|
||||
return;
|
||||
}
|
||||
@ -606,7 +616,7 @@
|
||||
}
|
||||
},
|
||||
|
||||
_handleCKey(e) {
|
||||
_handleNewComment(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e) ||
|
||||
this.modifierPressed(e)) { return; }
|
||||
|
||||
@ -619,7 +629,7 @@
|
||||
}
|
||||
},
|
||||
|
||||
_handleLeftBracketKey(e) {
|
||||
_handleOpenLastFile(e) {
|
||||
// Check for meta key to avoid overriding native chrome shortcut.
|
||||
if (this.shouldSuppressKeyboardShortcut(e) ||
|
||||
this.getKeyboardEvent(e).metaKey) { return; }
|
||||
@ -628,7 +638,7 @@
|
||||
this._openSelectedFile(this._files.length - 1);
|
||||
},
|
||||
|
||||
_handleRightBracketKey(e) {
|
||||
_handleOpenFirstFile(e) {
|
||||
// Check for meta key to avoid overriding native chrome shortcut.
|
||||
if (this.shouldSuppressKeyboardShortcut(e) ||
|
||||
this.getKeyboardEvent(e).metaKey) { return; }
|
||||
@ -637,7 +647,7 @@
|
||||
this._openSelectedFile(0);
|
||||
},
|
||||
|
||||
_handleOKey(e) {
|
||||
_handleOpenFile(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e) ||
|
||||
this.modifierPressed(e)) { return; }
|
||||
e.preventDefault();
|
||||
@ -650,7 +660,7 @@
|
||||
this._openSelectedFile();
|
||||
},
|
||||
|
||||
_handleNKey(e) {
|
||||
_handleNextChunk(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e) ||
|
||||
(this.modifierPressed(e) && !this.isModifierPressed(e, 'shiftKey')) ||
|
||||
this._noDiffsExpanded()) {
|
||||
@ -665,7 +675,7 @@
|
||||
}
|
||||
},
|
||||
|
||||
_handlePKey(e) {
|
||||
_handlePrevChunk(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e) ||
|
||||
(this.modifierPressed(e) && !this.isModifierPressed(e, 'shiftKey')) ||
|
||||
this._noDiffsExpanded()) {
|
||||
@ -680,7 +690,7 @@
|
||||
}
|
||||
},
|
||||
|
||||
_handleRKey(e) {
|
||||
_handleToggleFileReviewed(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
|
||||
return;
|
||||
}
|
||||
@ -690,7 +700,7 @@
|
||||
this._reviewFile(this._files[this.$.fileCursor.index].__path);
|
||||
},
|
||||
|
||||
_handleCapitalAKey(e) {
|
||||
_handleToggleLeftPane(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e)) { return; }
|
||||
|
||||
e.preventDefault();
|
||||
|
@ -49,6 +49,24 @@ limitations under the License.
|
||||
|
||||
<script>
|
||||
suite('gr-file-list tests', () => {
|
||||
const kb = window.Gerrit.KeyboardShortcutBinder;
|
||||
kb.bindShortcut(kb.Shortcut.LEFT_PANE, 'shift+left');
|
||||
kb.bindShortcut(kb.Shortcut.RIGHT_PANE, 'shift+right');
|
||||
kb.bindShortcut(kb.Shortcut.TOGGLE_INLINE_DIFF, 'i:keyup');
|
||||
kb.bindShortcut(kb.Shortcut.TOGGLE_ALL_INLINE_DIFFS, 'shift+i:keyup');
|
||||
kb.bindShortcut(kb.Shortcut.CURSOR_NEXT_FILE, 'j', 'down');
|
||||
kb.bindShortcut(kb.Shortcut.CURSOR_PREV_FILE, 'k', 'up');
|
||||
kb.bindShortcut(kb.Shortcut.NEXT_LINE, 'j', 'down');
|
||||
kb.bindShortcut(kb.Shortcut.PREV_LINE, 'k', 'up');
|
||||
kb.bindShortcut(kb.Shortcut.NEW_COMMENT, 'c');
|
||||
kb.bindShortcut(kb.Shortcut.OPEN_LAST_FILE, '[');
|
||||
kb.bindShortcut(kb.Shortcut.OPEN_FIRST_FILE, ']');
|
||||
kb.bindShortcut(kb.Shortcut.OPEN_FILE, 'o');
|
||||
kb.bindShortcut(kb.Shortcut.NEXT_CHUNK, 'n');
|
||||
kb.bindShortcut(kb.Shortcut.PREV_CHUNK, 'p');
|
||||
kb.bindShortcut(kb.Shortcut.TOGGLE_FILE_REVIEWED, 'r');
|
||||
kb.bindShortcut(kb.Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
|
||||
|
||||
let element;
|
||||
let commentApiWrapper;
|
||||
let sandbox;
|
||||
@ -668,7 +686,7 @@ limitations under the License.
|
||||
assert.equal(getNumReviewed(), 0);
|
||||
});
|
||||
|
||||
suite('_handleOKey', () => {
|
||||
suite('_handleOpenFile', () => {
|
||||
let interact;
|
||||
|
||||
setup(() => {
|
||||
@ -686,7 +704,7 @@ limitations under the License.
|
||||
|
||||
const e = new CustomEvent('fake-keyboard-event', opt_payload);
|
||||
sinon.stub(e, 'preventDefault');
|
||||
element._handleOKey(e);
|
||||
element._handleOpenFile(e);
|
||||
assert.isTrue(e.preventDefault.called);
|
||||
const result = {};
|
||||
if (openCursorStub.called) {
|
||||
@ -1545,7 +1563,7 @@ limitations under the License.
|
||||
|
||||
setup(() => {
|
||||
sandbox.stub(element, '_renderInOrder').returns(Promise.resolve());
|
||||
nKeySpy = sandbox.spy(element, '_handleNKey');
|
||||
nKeySpy = sandbox.spy(element, '_handleNextChunk');
|
||||
nextCommentStub = sandbox.stub(element.$.diffCursor,
|
||||
'moveToNextCommentThread');
|
||||
nextChunkStub = sandbox.stub(element.$.diffCursor,
|
||||
@ -1632,11 +1650,11 @@ limitations under the License.
|
||||
const mockEvent = {preventDefault() {}};
|
||||
|
||||
element._displayLine = false;
|
||||
element._handleDownKey(mockEvent);
|
||||
element._handleCursorNext(mockEvent);
|
||||
assert.isTrue(element._displayLine);
|
||||
|
||||
element._displayLine = false;
|
||||
element._handleUpKey(mockEvent);
|
||||
element._handleCursorPrev(mockEvent);
|
||||
assert.isTrue(element._displayLine);
|
||||
|
||||
element._displayLine = true;
|
||||
|
@ -0,0 +1,48 @@
|
||||
<!--
|
||||
@license
|
||||
Copyright (C) 2018 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.
|
||||
-->
|
||||
|
||||
<link rel="import" href="../../../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../../../styles/shared-styles.html">
|
||||
|
||||
<dom-module id="gr-key-binding-display">
|
||||
<template>
|
||||
<style include="shared-styles">
|
||||
.key {
|
||||
background-color: var(--chip-background-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 3px;
|
||||
display: inline-block;
|
||||
font-weight: var(--font-weight-bold);
|
||||
padding: .1em .5em;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
<template is="dom-repeat" items="[[binding]]">
|
||||
<template is="dom-if" if="[[index]]">
|
||||
or
|
||||
</template>
|
||||
<template
|
||||
is="dom-repeat"
|
||||
items="[[_computeModifiers(item)]]"
|
||||
as="modifier">
|
||||
<span class="key modifier">[[modifier]]</span>
|
||||
</template>
|
||||
<span class="key">[[_computeKey(item)]]</span>
|
||||
</template>
|
||||
</template>
|
||||
<script src="gr-key-binding-display.js"></script>
|
||||
</dom-module>
|
@ -0,0 +1,36 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright (C) 2018 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.
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
Polymer({
|
||||
is: 'gr-key-binding-display',
|
||||
|
||||
properties: {
|
||||
/** @type {Array<string>} */
|
||||
binding: Array,
|
||||
},
|
||||
|
||||
_computeModifiers(binding) {
|
||||
return binding.slice(0, binding.length - 1);
|
||||
},
|
||||
|
||||
_computeKey(binding) {
|
||||
return binding[binding.length - 1];
|
||||
},
|
||||
});
|
||||
})();
|
@ -0,0 +1,66 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
@license
|
||||
Copyright (C) 2018 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.
|
||||
-->
|
||||
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
|
||||
<title>gr-key-binding-display</title>
|
||||
|
||||
<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
|
||||
<script src="../../../bower_components/web-component-tester/browser.js"></script>
|
||||
<link rel="import" href="../../../test/common-test-setup.html"/>
|
||||
<link rel="import" href="gr-key-binding-display.html">
|
||||
|
||||
<script>void(0);</script>
|
||||
|
||||
<test-fixture id="basic">
|
||||
<template>
|
||||
<gr-key-binding-display></gr-key-binding-display>
|
||||
</template>
|
||||
</test-fixture>
|
||||
|
||||
<script>
|
||||
suite('gr-key-binding-display tests', () => {
|
||||
let element;
|
||||
|
||||
setup(() => {
|
||||
element = fixture('basic');
|
||||
});
|
||||
|
||||
suite('_computeKey', () => {
|
||||
test('unmodified key', () => {
|
||||
assert.strictEqual(element._computeKey(['x']), 'x');
|
||||
});
|
||||
|
||||
test('key with modifiers', () => {
|
||||
assert.strictEqual(element._computeKey(['Ctrl', 'x']), 'x');
|
||||
assert.strictEqual(element._computeKey(['Shift', 'Meta', 'x']), 'x');
|
||||
});
|
||||
});
|
||||
|
||||
suite('_computeModifiers', () => {
|
||||
test('single unmodified key', () => {
|
||||
assert.deepEqual(element._computeModifiers(['x']), []);
|
||||
});
|
||||
|
||||
test('key with modifiers', () => {
|
||||
assert.deepEqual(element._computeModifiers(['Ctrl', 'x']), ['Ctrl']);
|
||||
assert.deepEqual(
|
||||
element._computeModifiers(['Shift', 'Meta', 'x']),
|
||||
['Shift', 'Meta']);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
@ -16,7 +16,9 @@ limitations under the License.
|
||||
-->
|
||||
|
||||
<link rel="import" href="../../../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
|
||||
<link rel="import" href="../../shared/gr-button/gr-button.html">
|
||||
<link rel="import" href="../gr-key-binding-display/gr-key-binding-display.html">
|
||||
<link rel="import" href="../../../styles/shared-styles.html">
|
||||
|
||||
<dom-module id="gr-keyboard-shortcuts-dialog">
|
||||
@ -54,15 +56,6 @@ limitations under the License.
|
||||
font-weight: var(--font-weight-bold);
|
||||
padding-top: 1em;
|
||||
}
|
||||
.key {
|
||||
background-color: var(--chip-background-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 3px;
|
||||
display: inline-block;
|
||||
font-weight: var(--font-weight-bold);
|
||||
padding: .1em .5em;
|
||||
text-align: center;
|
||||
}
|
||||
.modifier {
|
||||
font-weight: normal;
|
||||
}
|
||||
@ -74,449 +67,42 @@ limitations under the License.
|
||||
<main>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td></td><td class="header">Everywhere</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="key">/</span></td>
|
||||
<td>Search</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="key">?</span></td>
|
||||
<td>Show this dialog</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="key modifier">g</span>
|
||||
<span class="key">o</span>
|
||||
</td>
|
||||
<td>Go to Opened Changes</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="key modifier">g</span>
|
||||
<span class="key">m</span>
|
||||
</td>
|
||||
<td>Go to Merged Changes</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="key modifier">g</span>
|
||||
<span class="key">a</span>
|
||||
</td>
|
||||
<td>Go to Abandoned Changes</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<!-- Change View -->
|
||||
<tbody hidden$="[[!_computeInView(view, 'change')]]" hidden>
|
||||
<tr>
|
||||
<td></td><td class="header">Navigation</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="key">]</span></td>
|
||||
<td>Show first file</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="key">[</span></td>
|
||||
<td>Show last file</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="key">u</span></td>
|
||||
<td>Up to dashboard</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<!-- Diff View -->
|
||||
<tbody hidden$="[[!_computeInView(view, 'diff')]]" hidden>
|
||||
<tr>
|
||||
<td></td><td class="header">Navigation</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="key">]</span></td>
|
||||
<td>Show next file</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="key">[</span></td>
|
||||
<td>Show previous file</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="key modifier">Shift</span>
|
||||
<span class="key">j</span>
|
||||
</td>
|
||||
<td>Show next file that has comments</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="key modifier">Shift</span>
|
||||
<span class="key">k</span>
|
||||
</td>
|
||||
<td>Show previous file that has comments</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="key">u</span></td>
|
||||
<td>Up to change</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table>
|
||||
<!-- Change List -->
|
||||
<tbody hidden$="[[!_computeInView(view, 'search')]]" hidden>
|
||||
<tr>
|
||||
<td></td><td class="header">Change list</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="key">j</span></td>
|
||||
<td>Select next change</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="key">k</span></td>
|
||||
<td>Show previous change</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="key">n</span> or <span class="key">]</span></td>
|
||||
<td>Go to next page</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="key">p</span> or <span class="key">[</span></td>
|
||||
<td>Go to previous page</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="key">Enter</span> or
|
||||
<span class="key">o</span>
|
||||
</td>
|
||||
<td>Show selected change</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="key modifier">Shift</span>
|
||||
<span class="key">r</span>
|
||||
</td>
|
||||
<td>Refresh list of changes</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="key">s</span></td>
|
||||
<td>Star (or unstar) change</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<!-- Dashboard -->
|
||||
<tbody hidden$="[[!_computeInView(view, 'dashboard')]]" hidden>
|
||||
<tr>
|
||||
<td></td><td class="header">Dashboard</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="key">j</span></td>
|
||||
<td>Select next change</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="key">k</span></td>
|
||||
<td>Show previous change</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="key">Enter</span> or
|
||||
<span class="key">o</span>
|
||||
</td>
|
||||
<td>Show selected change</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="key">r</span></td>
|
||||
<td>Mark (or unmark) change as reviewed</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="key modifier">Shift</span>
|
||||
<span class="key">r</span>
|
||||
</td>
|
||||
<td>Refresh list of changes</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="key">s</span></td>
|
||||
<td>Star (or unstar) change</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<!-- Change View -->
|
||||
<tbody hidden$="[[!_computeInView(view, 'change')]]" hidden>
|
||||
<tr>
|
||||
<td></td><td class="header">Actions</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="key">a</span></td>
|
||||
<td>Open reply dialog to publish comments and add reviewers</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="key">d</span></td>
|
||||
<td>Open download overlay</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="key modifier">Shift</span>
|
||||
<span class="key">r</span>
|
||||
</td>
|
||||
<td>Reload the change at the latest patch</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="key">s</span></td>
|
||||
<td>Star (or unstar) change</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="key">x</span></td>
|
||||
<td>Expand all messages</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="key">z</span></td>
|
||||
<td>Collapse all messages</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td><td class="header">Reply dialog</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="key modifier">Ctrl</span>
|
||||
<span class="key">Enter</span><br/>
|
||||
</td>
|
||||
<td>Send reply</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="key modifier">Meta</span>
|
||||
<span class="key">Enter</span>
|
||||
</td>
|
||||
<td>Send reply</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td><td class="header">File list</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="key">j</span> or <span class="key">↓</span></td>
|
||||
<td>Select next file</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="key">k</span> or <span class="key">↑</span></td>
|
||||
<td>Select previous file</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="key">Enter</span> or
|
||||
<span class="key">o</span>
|
||||
</td>
|
||||
<td>Go to selected file</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="key">r</span></td>
|
||||
<td>Toggle review flag on selected file</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="key modifier">Shift</span>
|
||||
<span class="key">i</span>
|
||||
</td>
|
||||
<td>Show/hide all inline diffs</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="key">i</span></td>
|
||||
<td>Show/hide selected inline diff</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td><td class="header">Diffs</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="key">j</span> or <span class="key">↓</span></td>
|
||||
<td>Go to next line</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="key">k</span> or <span class="key">↑</span></td>
|
||||
<td>Go to previous line</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="key">n</span></td>
|
||||
<td>Go to next diff chunk</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="key">p</span></td>
|
||||
<td>Go to previous diff chunk</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="key modifier">Shift</span>
|
||||
<span class="key">n</span>
|
||||
</td>
|
||||
<td>Go to next comment thread</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="key modifier">Shift</span>
|
||||
<span class="key">p</span>
|
||||
</td>
|
||||
<td>Go to previous comment thread</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="key">e</span></td>
|
||||
<td>Expand all comment threads</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="key modifier">Shift</span>
|
||||
<span class="key">e</span>
|
||||
</td>
|
||||
<td>Collapse all comment threads</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="key modifier">Shift</span>
|
||||
<span class="key">←</span>
|
||||
</td>
|
||||
<td>Select left pane</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="key modifier">Shift</span>
|
||||
<span class="key">→</span>
|
||||
</td>
|
||||
<td>Select right pane</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="key modifier">Shift</span>
|
||||
<span class="key">a</span>
|
||||
</td>
|
||||
<td>Hide/show left diff</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="key">m</span>
|
||||
</td>
|
||||
<td>Toggle unified/side-by-side diff</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="key">c</span>
|
||||
</td>
|
||||
<td>Draft new comment</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<!-- Diff View -->
|
||||
<tbody hidden$="[[!_computeInView(view, 'diff')]]" hidden>
|
||||
<tr>
|
||||
<td></td><td class="header">Actions</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="key">j</span> or <span class="key">↓</span></td>
|
||||
<td>Show next line</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="key">k</span> or <span class="key">↑</span></td>
|
||||
<td>Show previous line</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="key">n</span></td>
|
||||
<td>Show next diff chunk</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="key">p</span></td>
|
||||
<td>Show previous diff chunk</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="key modifier">Shift</span>
|
||||
<span class="key">x</span>
|
||||
</td>
|
||||
<td>Expand all diff context</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="key modifier">Shift</span>
|
||||
<span class="key">n</span>
|
||||
</td>
|
||||
<td>Show next comment thread</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="key modifier">Shift</span>
|
||||
<span class="key">p</span>
|
||||
</td>
|
||||
<td>Show previous comment thread</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="key">e</span></td>
|
||||
<td>Expand all comment threads</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="key modifier">Shift</span>
|
||||
<span class="key">e</span>
|
||||
</td>
|
||||
<td>Collapse all comment threads</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="key modifier">Shift</span>
|
||||
<span class="key">←</span>
|
||||
</td>
|
||||
<td>Select left pane</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="key modifier">Shift</span>
|
||||
<span class="key">→</span>
|
||||
</td>
|
||||
<td>Select right pane</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="key modifier">Shift</span>
|
||||
<span class="key">a</span>
|
||||
</td>
|
||||
<td>Hide/show left diff</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="key">m</span>
|
||||
</td>
|
||||
<td>Toggle unified/side-by-side diff</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="key">c</span>
|
||||
</td>
|
||||
<td>Draft new comment</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="key modifier">Ctrl</span>
|
||||
<span class="key">s</span><br/>
|
||||
</td>
|
||||
<td>Save comment</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="key modifier">Ctrl</span>
|
||||
<span class="key">Enter</span><br/>
|
||||
</td>
|
||||
<td>Save comment</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="key modifier">Meta</span>
|
||||
<span class="key">Enter</span>
|
||||
</td>
|
||||
<td>Save comment</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="key">a</span></td>
|
||||
<td>Open reply dialog to publish comments and add reviewers</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="key">,</span></td>
|
||||
<td>Show diff preferences</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="key">r</span></td>
|
||||
<td>Mark/unmark file as reviewed</td>
|
||||
</tr>
|
||||
<template is="dom-repeat" items="[[_left]]">
|
||||
<tr>
|
||||
<td></td><td class="header">[[item.section]]</td>
|
||||
</tr>
|
||||
<template is="dom-repeat" items="[[item.shortcuts]]" as="shortcut">
|
||||
<tr>
|
||||
<td>
|
||||
<gr-key-binding-display binding="[[shortcut.binding]]">
|
||||
</gr-key-binding-display>
|
||||
</td>
|
||||
<td>[[shortcut.text]]</td>
|
||||
</tr>
|
||||
</template>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
<template is="dom-if" if="[[_right]]">
|
||||
<table>
|
||||
<tbody>
|
||||
<template is="dom-repeat" items="[[_right]]">
|
||||
<tr>
|
||||
<td></td><td class="header">[[item.section]]</td>
|
||||
</tr>
|
||||
<template is="dom-repeat" items="[[item.shortcuts]]" as="shortcut">
|
||||
<tr>
|
||||
<td>
|
||||
<gr-key-binding-display binding="[[shortcut.binding]]">
|
||||
</gr-key-binding-display>
|
||||
</td>
|
||||
<td>[[shortcut.text]]</td>
|
||||
</tr>
|
||||
</template>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
</main>
|
||||
<footer></footer>
|
||||
</template>
|
||||
|
@ -17,6 +17,8 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const {ShortcutSection} = window.Gerrit.KeyboardShortcutBinder;
|
||||
|
||||
Polymer({
|
||||
is: 'gr-keyboard-shortcuts-dialog',
|
||||
|
||||
@ -27,20 +29,97 @@
|
||||
*/
|
||||
|
||||
properties: {
|
||||
view: String,
|
||||
_left: Array,
|
||||
_right: Array,
|
||||
|
||||
_propertyBySection: {
|
||||
type: Object,
|
||||
value() {
|
||||
return {
|
||||
[ShortcutSection.EVERYWHERE]: '_everywhere',
|
||||
[ShortcutSection.NAVIGATION]: '_navigation',
|
||||
[ShortcutSection.DASHBOARD]: '_dashboard',
|
||||
[ShortcutSection.CHANGE_LIST]: '_changeList',
|
||||
[ShortcutSection.ACTIONS]: '_actions',
|
||||
[ShortcutSection.REPLY_DIALOG]: '_replyDialog',
|
||||
[ShortcutSection.FILE_LIST]: '_fileList',
|
||||
[ShortcutSection.DIFFS]: '_diffs',
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
behaviors: [
|
||||
Gerrit.KeyboardShortcutBehavior,
|
||||
],
|
||||
|
||||
hostAttributes: {
|
||||
role: 'dialog',
|
||||
},
|
||||
|
||||
_computeInView(currentView, view) {
|
||||
return view === currentView;
|
||||
attached() {
|
||||
this.addKeyboardShortcutDirectoryListener(
|
||||
this._onDirectoryUpdated.bind(this));
|
||||
},
|
||||
|
||||
detached() {
|
||||
this.removeKeyboardShortcutDirectoryListener(
|
||||
this._onDirectoryUpdated.bind(this));
|
||||
},
|
||||
|
||||
_handleCloseTap(e) {
|
||||
e.preventDefault();
|
||||
this.fire('close', null, {bubbles: false});
|
||||
},
|
||||
|
||||
_onDirectoryUpdated(directory) {
|
||||
const left = [];
|
||||
const right = [];
|
||||
|
||||
if (directory.has(ShortcutSection.EVERYWHERE)) {
|
||||
left.push({
|
||||
section: ShortcutSection.EVERYWHERE,
|
||||
shortcuts: directory.get(ShortcutSection.EVERYWHERE),
|
||||
});
|
||||
}
|
||||
|
||||
if (directory.has(ShortcutSection.NAVIGATION)) {
|
||||
left.push({
|
||||
section: ShortcutSection.NAVIGATION,
|
||||
shortcuts: directory.get(ShortcutSection.NAVIGATION),
|
||||
});
|
||||
}
|
||||
|
||||
if (directory.has(ShortcutSection.ACTIONS)) {
|
||||
right.push({
|
||||
section: ShortcutSection.ACTIONS,
|
||||
shortcuts: directory.get(ShortcutSection.ACTIONS),
|
||||
});
|
||||
}
|
||||
|
||||
if (directory.has(ShortcutSection.REPLY_DIALOG)) {
|
||||
right.push({
|
||||
section: ShortcutSection.REPLY_DIALOG,
|
||||
shortcuts: directory.get(ShortcutSection.REPLY_DIALOG),
|
||||
});
|
||||
}
|
||||
|
||||
if (directory.has(ShortcutSection.FILE_LIST)) {
|
||||
right.push({
|
||||
section: ShortcutSection.FILE_LIST,
|
||||
shortcuts: directory.get(ShortcutSection.FILE_LIST),
|
||||
});
|
||||
}
|
||||
|
||||
if (directory.has(ShortcutSection.DIFFS)) {
|
||||
right.push({
|
||||
section: ShortcutSection.DIFFS,
|
||||
shortcuts: directory.get(ShortcutSection.DIFFS),
|
||||
});
|
||||
}
|
||||
|
||||
this.set('_left', left);
|
||||
this.set('_right', right);
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
@ -0,0 +1,179 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
@license
|
||||
Copyright (C) 2018 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.
|
||||
-->
|
||||
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
|
||||
<title>gr-key-binding-display</title>
|
||||
|
||||
<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
|
||||
<script src="../../../bower_components/web-component-tester/browser.js"></script>
|
||||
<link rel="import" href="../../../test/common-test-setup.html"/>
|
||||
<link rel="import" href="gr-keyboard-shortcuts-dialog.html">
|
||||
|
||||
<script>void(0);</script>
|
||||
|
||||
<test-fixture id="basic">
|
||||
<template>
|
||||
<gr-keyboard-shortcuts-dialog></gr-keyboard-shortcuts-dialog>
|
||||
</template>
|
||||
</test-fixture>
|
||||
|
||||
<script>
|
||||
suite('gr-keyboard-shortcuts-dialog tests', () => {
|
||||
const kb = window.Gerrit.KeyboardShortcutBinder;
|
||||
let element;
|
||||
|
||||
setup(() => {
|
||||
element = fixture('basic');
|
||||
});
|
||||
|
||||
function update(directory) {
|
||||
element._onDirectoryUpdated(directory);
|
||||
flushAsynchronousOperations();
|
||||
}
|
||||
|
||||
suite('_left and _right contents', () => {
|
||||
test('empty dialog', () => {
|
||||
assert.strictEqual(element._left.length, 0);
|
||||
assert.strictEqual(element._right.length, 0);
|
||||
});
|
||||
|
||||
test('everywhere goes on left', () => {
|
||||
update(new Map([
|
||||
[kb.ShortcutSection.EVERYWHERE, ['everywhere shortcuts']],
|
||||
]));
|
||||
assert.deepEqual(
|
||||
element._left,
|
||||
[
|
||||
{
|
||||
section: kb.ShortcutSection.EVERYWHERE,
|
||||
shortcuts: ['everywhere shortcuts'],
|
||||
},
|
||||
]);
|
||||
assert.strictEqual(element._right.length, 0);
|
||||
});
|
||||
|
||||
test('navigation goes on left', () => {
|
||||
update(new Map([
|
||||
[kb.ShortcutSection.NAVIGATION, ['navigation shortcuts']],
|
||||
]));
|
||||
assert.deepEqual(
|
||||
element._left,
|
||||
[
|
||||
{
|
||||
section: kb.ShortcutSection.NAVIGATION,
|
||||
shortcuts: ['navigation shortcuts'],
|
||||
},
|
||||
]);
|
||||
assert.strictEqual(element._right.length, 0);
|
||||
});
|
||||
|
||||
test('actions go on right', () => {
|
||||
update(new Map([
|
||||
[kb.ShortcutSection.ACTIONS, ['actions shortcuts']],
|
||||
]));
|
||||
assert.deepEqual(
|
||||
element._right,
|
||||
[
|
||||
{
|
||||
section: kb.ShortcutSection.ACTIONS,
|
||||
shortcuts: ['actions shortcuts'],
|
||||
},
|
||||
]);
|
||||
assert.strictEqual(element._left.length, 0);
|
||||
});
|
||||
|
||||
test('reply dialog goes on right', () => {
|
||||
update(new Map([
|
||||
[kb.ShortcutSection.REPLY_DIALOG, ['reply dialog shortcuts']],
|
||||
]));
|
||||
assert.deepEqual(
|
||||
element._right,
|
||||
[
|
||||
{
|
||||
section: kb.ShortcutSection.REPLY_DIALOG,
|
||||
shortcuts: ['reply dialog shortcuts'],
|
||||
},
|
||||
]);
|
||||
assert.strictEqual(element._left.length, 0);
|
||||
});
|
||||
|
||||
test('file list goes on right', () => {
|
||||
update(new Map([
|
||||
[kb.ShortcutSection.FILE_LIST, ['file list shortcuts']],
|
||||
]));
|
||||
assert.deepEqual(
|
||||
element._right,
|
||||
[
|
||||
{
|
||||
section: kb.ShortcutSection.FILE_LIST,
|
||||
shortcuts: ['file list shortcuts'],
|
||||
},
|
||||
]);
|
||||
assert.strictEqual(element._left.length, 0);
|
||||
});
|
||||
|
||||
test('diffs go on right', () => {
|
||||
update(new Map([
|
||||
[kb.ShortcutSection.DIFFS, ['diffs shortcuts']],
|
||||
]));
|
||||
assert.deepEqual(
|
||||
element._right,
|
||||
[
|
||||
{
|
||||
section: kb.ShortcutSection.DIFFS,
|
||||
shortcuts: ['diffs shortcuts'],
|
||||
},
|
||||
]);
|
||||
assert.strictEqual(element._left.length, 0);
|
||||
});
|
||||
|
||||
test('multiple sections on each side', () => {
|
||||
update(new Map([
|
||||
[kb.ShortcutSection.ACTIONS, ['actions shortcuts']],
|
||||
[kb.ShortcutSection.DIFFS, ['diffs shortcuts']],
|
||||
[kb.ShortcutSection.EVERYWHERE, ['everywhere shortcuts']],
|
||||
[kb.ShortcutSection.NAVIGATION, ['navigation shortcuts']],
|
||||
]));
|
||||
assert.deepEqual(
|
||||
element._left,
|
||||
[
|
||||
{
|
||||
section: kb.ShortcutSection.EVERYWHERE,
|
||||
shortcuts: ['everywhere shortcuts'],
|
||||
},
|
||||
{
|
||||
section: kb.ShortcutSection.NAVIGATION,
|
||||
shortcuts: ['navigation shortcuts'],
|
||||
},
|
||||
]);
|
||||
assert.deepEqual(
|
||||
element._right,
|
||||
[
|
||||
{
|
||||
section: kb.ShortcutSection.ACTIONS,
|
||||
shortcuts: ['actions shortcuts'],
|
||||
},
|
||||
{
|
||||
section: kb.ShortcutSection.DIFFS,
|
||||
shortcuts: ['diffs shortcuts'],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
@ -110,10 +110,6 @@
|
||||
Gerrit.URLEncodingBehavior,
|
||||
],
|
||||
|
||||
keyBindings: {
|
||||
'/': '_handleForwardSlashKey',
|
||||
},
|
||||
|
||||
properties: {
|
||||
value: {
|
||||
type: String,
|
||||
@ -156,6 +152,12 @@
|
||||
},
|
||||
},
|
||||
|
||||
keyboardShortcuts() {
|
||||
return {
|
||||
[this.Shortcut.SEARCH]: '_handleSearch',
|
||||
};
|
||||
},
|
||||
|
||||
_valueChanged(value) {
|
||||
this._inputVal = value;
|
||||
},
|
||||
@ -274,7 +276,7 @@
|
||||
});
|
||||
},
|
||||
|
||||
_handleForwardSlashKey(e) {
|
||||
_handleSearch(e) {
|
||||
const keyboardEvent = this.getKeyboardEvent(e);
|
||||
if (this.shouldSuppressKeyboardShortcut(e) ||
|
||||
(this.modifierPressed(e) && !keyboardEvent.shiftKey)) { return; }
|
||||
|
@ -37,6 +37,9 @@ limitations under the License.
|
||||
|
||||
<script>
|
||||
suite('gr-search-bar tests', () => {
|
||||
const kb = window.Gerrit.KeyboardShortcutBinder;
|
||||
kb.bindShortcut(kb.Shortcut.SEARCH, '/');
|
||||
|
||||
let element;
|
||||
let sandbox;
|
||||
|
||||
|
@ -177,22 +177,41 @@
|
||||
],
|
||||
|
||||
keyBindings: {
|
||||
'esc': '_handleEscKey',
|
||||
'shift+left': '_handleShiftLeftKey',
|
||||
'shift+right': '_handleShiftRightKey',
|
||||
'up k': '_handleUpKey',
|
||||
'down j': '_handleDownKey',
|
||||
'c': '_handleCKey',
|
||||
'[': '_handleLeftBracketKey',
|
||||
']': '_handleRightBracketKey',
|
||||
'n shift+n': '_handleNKey',
|
||||
'p shift+p': '_handlePKey',
|
||||
'a shift+a': '_handleAKey',
|
||||
'u': '_handleUKey',
|
||||
',': '_handleCommaKey',
|
||||
'm': '_handleMKey',
|
||||
'r': '_handleRKey',
|
||||
'shift+x': '_handleShiftXKey',
|
||||
esc: '_handleEscKey',
|
||||
},
|
||||
|
||||
keyboardShortcuts() {
|
||||
return {
|
||||
[this.Shortcut.LEFT_PANE]: '_handleLeftPane',
|
||||
[this.Shortcut.RIGHT_PANE]: '_handleRightPane',
|
||||
[this.Shortcut.NEXT_LINE]: '_handleNextLineOrFileWithComments',
|
||||
[this.Shortcut.PREV_LINE]: '_handlePrevLineOrFileWithComments',
|
||||
[this.Shortcut.NEXT_FILE_WITH_COMMENTS]:
|
||||
'_handleNextLineOrFileWithComments',
|
||||
[this.Shortcut.PREV_FILE_WITH_COMMENTS]:
|
||||
'_handlePrevLineOrFileWithComments',
|
||||
[this.Shortcut.NEW_COMMENT]: '_handleNewComment',
|
||||
[this.Shortcut.SAVE_COMMENT]: null, // DOC_ONLY binding
|
||||
[this.Shortcut.NEXT_FILE]: '_handleNextFile',
|
||||
[this.Shortcut.PREV_FILE]: '_handlePrevFile',
|
||||
[this.Shortcut.NEXT_CHUNK]: '_handleNextChunkOrCommentThread',
|
||||
[this.Shortcut.NEXT_COMMENT_THREAD]: '_handleNextChunkOrCommentThread',
|
||||
[this.Shortcut.PREV_CHUNK]: '_handlePrevChunkOrCommentThread',
|
||||
[this.Shortcut.PREV_COMMENT_THREAD]: '_handlePrevChunkOrCommentThread',
|
||||
[this.Shortcut.OPEN_REPLY_DIALOG]:
|
||||
'_handleOpenReplyDialogOrToggleLeftPane',
|
||||
[this.Shortcut.TOGGLE_LEFT_PANE]:
|
||||
'_handleOpenReplyDialogOrToggleLeftPane',
|
||||
[this.Shortcut.UP_TO_CHANGE]: '_handleUpToChange',
|
||||
[this.Shortcut.OPEN_DIFF_PREFS]: '_handleCommaKey',
|
||||
[this.Shortcut.TOGGLE_DIFF_MODE]: '_handleToggleDiffMode',
|
||||
[this.Shortcut.TOGGLE_FILE_REVIEWED]: '_handleToggleFileReviewed',
|
||||
[this.Shortcut.EXPAND_ALL_DIFF_CONTEXT]: '_handleExpandAllDiffContext',
|
||||
|
||||
// Final two are actually handled by gr-diff-comment-thread.
|
||||
[this.Shortcut.EXPAND_ALL_COMMENT_THREADS]: null,
|
||||
[this.Shortcut.COLLAPSE_ALL_COMMENT_THREADS]: null,
|
||||
};
|
||||
},
|
||||
|
||||
attached() {
|
||||
@ -263,7 +282,7 @@
|
||||
this._patchRange.patchNum, this._path, reviewed);
|
||||
},
|
||||
|
||||
_handleRKey(e) {
|
||||
_handleToggleFileReviewed(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e) ||
|
||||
this.modifierPressed(e)) { return; }
|
||||
|
||||
@ -279,21 +298,21 @@
|
||||
this.$.diffHost.displayLine = false;
|
||||
},
|
||||
|
||||
_handleShiftLeftKey(e) {
|
||||
_handleLeftPane(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e)) { return; }
|
||||
|
||||
e.preventDefault();
|
||||
this.$.cursor.moveLeft();
|
||||
},
|
||||
|
||||
_handleShiftRightKey(e) {
|
||||
_handleRightPane(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e)) { return; }
|
||||
|
||||
e.preventDefault();
|
||||
this.$.cursor.moveRight();
|
||||
},
|
||||
|
||||
_handleUpKey(e) {
|
||||
_handlePrevLineOrFileWithComments(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e)) { return; }
|
||||
if (e.detail.keyboardEvent.shiftKey &&
|
||||
e.detail.keyboardEvent.keyCode === 75) { // 'K'
|
||||
@ -307,7 +326,7 @@
|
||||
this.$.cursor.moveUp();
|
||||
},
|
||||
|
||||
_handleDownKey(e) {
|
||||
_handleNextLineOrFileWithComments(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e)) { return; }
|
||||
if (e.detail.keyboardEvent.shiftKey &&
|
||||
e.detail.keyboardEvent.keyCode === 74) { // 'J'
|
||||
@ -348,7 +367,7 @@
|
||||
this._patchRange.patchNum, this._patchRange.basePatchNum);
|
||||
},
|
||||
|
||||
_handleCKey(e) {
|
||||
_handleNewComment(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e)) { return; }
|
||||
if (this.$.diffHost.isRangeSelected()) { return; }
|
||||
if (this.modifierPressed(e)) { return; }
|
||||
@ -360,7 +379,7 @@
|
||||
}
|
||||
},
|
||||
|
||||
_handleLeftBracketKey(e) {
|
||||
_handlePrevFile(e) {
|
||||
// Check for meta key to avoid overriding native chrome shortcut.
|
||||
if (this.shouldSuppressKeyboardShortcut(e) ||
|
||||
this.getKeyboardEvent(e).metaKey) { return; }
|
||||
@ -369,7 +388,7 @@
|
||||
this._navToFile(this._path, this._fileList, -1);
|
||||
},
|
||||
|
||||
_handleRightBracketKey(e) {
|
||||
_handleNextFile(e) {
|
||||
// Check for meta key to avoid overriding native chrome shortcut.
|
||||
if (this.shouldSuppressKeyboardShortcut(e) ||
|
||||
this.getKeyboardEvent(e).metaKey) { return; }
|
||||
@ -378,7 +397,7 @@
|
||||
this._navToFile(this._path, this._fileList, 1);
|
||||
},
|
||||
|
||||
_handleNKey(e) {
|
||||
_handleNextChunkOrCommentThread(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e)) { return; }
|
||||
|
||||
e.preventDefault();
|
||||
@ -390,7 +409,7 @@
|
||||
}
|
||||
},
|
||||
|
||||
_handlePKey(e) {
|
||||
_handlePrevChunkOrCommentThread(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e)) { return; }
|
||||
|
||||
e.preventDefault();
|
||||
@ -402,7 +421,7 @@
|
||||
}
|
||||
},
|
||||
|
||||
_handleAKey(e) {
|
||||
_handleOpenReplyDialogOrToggleLeftPane(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e)) { return; }
|
||||
|
||||
if (e.detail.keyboardEvent.shiftKey) { // Hide left diff.
|
||||
@ -420,7 +439,7 @@
|
||||
this._navToChangeView();
|
||||
},
|
||||
|
||||
_handleUKey(e) {
|
||||
_handleUpToChange(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e) ||
|
||||
this.modifierPressed(e)) { return; }
|
||||
|
||||
@ -436,7 +455,7 @@
|
||||
this.$.diffPreferences.open();
|
||||
},
|
||||
|
||||
_handleMKey(e) {
|
||||
_handleToggleDiffMode(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e) ||
|
||||
this.modifierPressed(e)) { return; }
|
||||
|
||||
@ -989,7 +1008,7 @@
|
||||
return '';
|
||||
},
|
||||
|
||||
_handleShiftXKey(e) {
|
||||
_handleExpandAllDiffContext(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e)) { return; }
|
||||
this.$.diffHost.expandAllContext();
|
||||
},
|
||||
|
@ -43,6 +43,31 @@ limitations under the License.
|
||||
|
||||
<script>
|
||||
suite('gr-diff-view tests', () => {
|
||||
const kb = window.Gerrit.KeyboardShortcutBinder;
|
||||
kb.bindShortcut(kb.Shortcut.LEFT_PANE, 'shift+left');
|
||||
kb.bindShortcut(kb.Shortcut.RIGHT_PANE, 'shift+right');
|
||||
kb.bindShortcut(kb.Shortcut.NEXT_LINE, 'j', 'down');
|
||||
kb.bindShortcut(kb.Shortcut.PREV_LINE, 'k', 'up');
|
||||
kb.bindShortcut(kb.Shortcut.NEXT_FILE_WITH_COMMENTS, 'shift+j');
|
||||
kb.bindShortcut(kb.Shortcut.PREV_FILE_WITH_COMMENTS, 'shift+k');
|
||||
kb.bindShortcut(kb.Shortcut.NEW_COMMENT, 'c');
|
||||
kb.bindShortcut(kb.Shortcut.SAVE_COMMENT, 'ctrl+s');
|
||||
kb.bindShortcut(kb.Shortcut.NEXT_FILE, ']');
|
||||
kb.bindShortcut(kb.Shortcut.PREV_FILE, '[');
|
||||
kb.bindShortcut(kb.Shortcut.NEXT_CHUNK, 'n');
|
||||
kb.bindShortcut(kb.Shortcut.NEXT_COMMENT_THREAD, 'shift+n');
|
||||
kb.bindShortcut(kb.Shortcut.PREV_CHUNK, 'p');
|
||||
kb.bindShortcut(kb.Shortcut.PREV_COMMENT_THREAD, 'shift+p');
|
||||
kb.bindShortcut(kb.Shortcut.OPEN_REPLY_DIALOG, 'a');
|
||||
kb.bindShortcut(kb.Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
|
||||
kb.bindShortcut(kb.Shortcut.UP_TO_CHANGE, 'u');
|
||||
kb.bindShortcut(kb.Shortcut.OPEN_DIFF_PREFS, ',');
|
||||
kb.bindShortcut(kb.Shortcut.TOGGLE_DIFF_MODE, 'm');
|
||||
kb.bindShortcut(kb.Shortcut.TOGGLE_FILE_REVIEWED, 'r');
|
||||
kb.bindShortcut(kb.Shortcut.EXPAND_ALL_DIFF_CONTEXT, 'shift+x');
|
||||
kb.bindShortcut(kb.Shortcut.EXPAND_ALL_COMMENT_THREADS, 'e');
|
||||
kb.bindShortcut(kb.Shortcut.COLLAPSE_ALL_COMMENT_THREADS, 'shift+e');
|
||||
|
||||
let element;
|
||||
let sandbox;
|
||||
|
||||
@ -838,16 +863,16 @@ limitations under the License.
|
||||
assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
|
||||
});
|
||||
|
||||
test('_handleMKey', () => {
|
||||
test('_handleToggleDiffMode', () => {
|
||||
sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
|
||||
const e = {preventDefault: () => {}};
|
||||
// Initial state.
|
||||
assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
|
||||
|
||||
element._handleMKey(e);
|
||||
element._handleToggleDiffMode(e);
|
||||
assert.equal(element._getDiffViewMode(), 'UNIFIED_DIFF');
|
||||
|
||||
element._handleMKey(e);
|
||||
element._handleToggleDiffMode(e);
|
||||
assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
|
||||
});
|
||||
|
||||
|
@ -17,10 +17,6 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// 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 G_KEY_TIMEOUT_MS = 1000;
|
||||
|
||||
// Eagerly render Polymer components when backgrounded. (Skips
|
||||
// requestAnimationFrame.)
|
||||
// @see https://github.com/Polymer/polymer/issues/3851
|
||||
@ -112,11 +108,17 @@
|
||||
Gerrit.KeyboardShortcutBehavior,
|
||||
],
|
||||
|
||||
keyBindings: {
|
||||
'?': '_showKeyboardShortcuts',
|
||||
'g:keydown': '_gKeyDown',
|
||||
'g:keyup': '_gKeyUp',
|
||||
'a m o': '_jumpKeyPressed',
|
||||
keyboardShortcuts() {
|
||||
return {
|
||||
[this.Shortcut.OPEN_SHORTCUT_HELP_DIALOG]: '_showKeyboardShortcuts',
|
||||
[this.Shortcut.GO_TO_OPENED_CHANGES]: '_goToOpenedChanges',
|
||||
[this.Shortcut.GO_TO_MERGED_CHANGES]: '_goToMergedChanges',
|
||||
[this.Shortcut.GO_TO_ABANDONED_CHANGES]: '_goToAbandonedChanges',
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
this._bindKeyboardShortcuts();
|
||||
},
|
||||
|
||||
ready() {
|
||||
@ -171,6 +173,118 @@
|
||||
};
|
||||
},
|
||||
|
||||
_bindKeyboardShortcuts() {
|
||||
this.bindShortcut(this.Shortcut.SEND_REPLY,
|
||||
this.DOC_ONLY, 'ctrl+enter', 'meta+enter');
|
||||
|
||||
this.bindShortcut(
|
||||
this.Shortcut.OPEN_SHORTCUT_HELP_DIALOG, '?');
|
||||
this.bindShortcut(
|
||||
this.Shortcut.GO_TO_OPENED_CHANGES, this.GO_KEY, 'o');
|
||||
this.bindShortcut(
|
||||
this.Shortcut.GO_TO_MERGED_CHANGES, this.GO_KEY, 'm');
|
||||
this.bindShortcut(
|
||||
this.Shortcut.GO_TO_ABANDONED_CHANGES, this.GO_KEY, 'a');
|
||||
|
||||
this.bindShortcut(
|
||||
this.Shortcut.CURSOR_NEXT_CHANGE, 'j');
|
||||
this.bindShortcut(
|
||||
this.Shortcut.CURSOR_PREV_CHANGE, 'k');
|
||||
this.bindShortcut(
|
||||
this.Shortcut.OPEN_CHANGE, 'o');
|
||||
this.bindShortcut(
|
||||
this.Shortcut.NEXT_PAGE, 'n', ']');
|
||||
this.bindShortcut(
|
||||
this.Shortcut.PREV_PAGE, 'p', '[');
|
||||
this.bindShortcut(
|
||||
this.Shortcut.TOGGLE_CHANGE_REVIEWED, 'r');
|
||||
this.bindShortcut(
|
||||
this.Shortcut.TOGGLE_CHANGE_STAR, 's');
|
||||
this.bindShortcut(
|
||||
this.Shortcut.REFRESH_CHANGE_LIST, 'shift+r');
|
||||
|
||||
this.bindShortcut(
|
||||
this.Shortcut.OPEN_REPLY_DIALOG, 'a');
|
||||
this.bindShortcut(
|
||||
this.Shortcut.OPEN_DOWNLOAD_DIALOG, 'd');
|
||||
this.bindShortcut(
|
||||
this.Shortcut.EXPAND_ALL_MESSAGES, 'x');
|
||||
this.bindShortcut(
|
||||
this.Shortcut.COLLAPSE_ALL_MESSAGES, 'z');
|
||||
this.bindShortcut(
|
||||
this.Shortcut.REFRESH_CHANGE, 'shift+r');
|
||||
this.bindShortcut(
|
||||
this.Shortcut.UP_TO_DASHBOARD, 'u');
|
||||
this.bindShortcut(
|
||||
this.Shortcut.UP_TO_CHANGE, 'u');
|
||||
this.bindShortcut(
|
||||
this.Shortcut.TOGGLE_DIFF_MODE, 'm');
|
||||
|
||||
this.bindShortcut(
|
||||
this.Shortcut.NEXT_LINE, 'j', 'down');
|
||||
this.bindShortcut(
|
||||
this.Shortcut.PREV_LINE, 'k', 'up');
|
||||
this.bindShortcut(
|
||||
this.Shortcut.NEXT_CHUNK, 'n');
|
||||
this.bindShortcut(
|
||||
this.Shortcut.PREV_CHUNK, 'p');
|
||||
this.bindShortcut(
|
||||
this.Shortcut.EXPAND_ALL_DIFF_CONTEXT, 'shift+x');
|
||||
this.bindShortcut(
|
||||
this.Shortcut.NEXT_COMMENT_THREAD, 'shift+n');
|
||||
this.bindShortcut(
|
||||
this.Shortcut.PREV_COMMENT_THREAD, 'shift+p');
|
||||
this.bindShortcut(
|
||||
this.Shortcut.EXPAND_ALL_COMMENT_THREADS, this.DOC_ONLY, 'e');
|
||||
this.bindShortcut(
|
||||
this.Shortcut.COLLAPSE_ALL_COMMENT_THREADS,
|
||||
this.DOC_ONLY, 'shift+e');
|
||||
this.bindShortcut(
|
||||
this.Shortcut.LEFT_PANE, 'shift+left');
|
||||
this.bindShortcut(
|
||||
this.Shortcut.RIGHT_PANE, 'shift+right');
|
||||
this.bindShortcut(
|
||||
this.Shortcut.TOGGLE_LEFT_PANE, 'shift+a');
|
||||
this.bindShortcut(
|
||||
this.Shortcut.NEW_COMMENT, 'c');
|
||||
this.bindShortcut(
|
||||
this.Shortcut.SAVE_COMMENT,
|
||||
'ctrl+enter', 'meta+enter', 'ctrl+s', 'meta+s');
|
||||
this.bindShortcut(
|
||||
this.Shortcut.OPEN_DIFF_PREFS, ',');
|
||||
this.bindShortcut(
|
||||
this.Shortcut.TOGGLE_DIFF_REVIEWED, 'r');
|
||||
|
||||
this.bindShortcut(
|
||||
this.Shortcut.NEXT_FILE, ']');
|
||||
this.bindShortcut(
|
||||
this.Shortcut.PREV_FILE, '[');
|
||||
this.bindShortcut(
|
||||
this.Shortcut.NEXT_FILE_WITH_COMMENTS, 'shift+j');
|
||||
this.bindShortcut(
|
||||
this.Shortcut.PREV_FILE_WITH_COMMENTS, 'shift+k');
|
||||
this.bindShortcut(
|
||||
this.Shortcut.CURSOR_NEXT_FILE, 'j', 'down');
|
||||
this.bindShortcut(
|
||||
this.Shortcut.CURSOR_PREV_FILE, 'k', 'up');
|
||||
this.bindShortcut(
|
||||
this.Shortcut.OPEN_FILE, 'o', 'enter');
|
||||
this.bindShortcut(
|
||||
this.Shortcut.TOGGLE_FILE_REVIEWED, 'r');
|
||||
this.bindShortcut(
|
||||
this.Shortcut.TOGGLE_ALL_INLINE_DIFFS, 'shift+i:keyup');
|
||||
this.bindShortcut(
|
||||
this.Shortcut.TOGGLE_INLINE_DIFF, 'i:keyup');
|
||||
|
||||
this.bindShortcut(
|
||||
this.Shortcut.OPEN_FIRST_FILE, ']');
|
||||
this.bindShortcut(
|
||||
this.Shortcut.OPEN_LAST_FILE, '[');
|
||||
|
||||
this.bindShortcut(
|
||||
this.Shortcut.SEARCH, '/');
|
||||
},
|
||||
|
||||
_accountChanged(account) {
|
||||
if (!account) { return; }
|
||||
|
||||
@ -293,32 +407,16 @@
|
||||
return isShadowDom ? 'shadow' : '';
|
||||
},
|
||||
|
||||
_gKeyDown(e) {
|
||||
if (this.modifierPressed(e)) { return; }
|
||||
this._lastGKeyPressTimestamp = Date.now();
|
||||
_goToOpenedChanges() {
|
||||
Gerrit.Nav.navigateToStatusSearch('open');
|
||||
},
|
||||
|
||||
_gKeyUp() {
|
||||
this._lastGKeyPressTimestamp = null;
|
||||
_goToMergedChanges() {
|
||||
Gerrit.Nav.navigateToStatusSearch('merged');
|
||||
},
|
||||
|
||||
_jumpKeyPressed(e) {
|
||||
if (!this._lastGKeyPressTimestamp ||
|
||||
(Date.now() - this._lastGKeyPressTimestamp > G_KEY_TIMEOUT_MS) ||
|
||||
this.shouldSuppressKeyboardShortcut(e)) { return; }
|
||||
e.preventDefault();
|
||||
|
||||
let status = null;
|
||||
if (e.detail.key === 'a') {
|
||||
status = 'abandoned';
|
||||
} else if (e.detail.key === 'm') {
|
||||
status = 'merged';
|
||||
} else if (e.detail.key === 'o') {
|
||||
status = 'open';
|
||||
}
|
||||
if (status !== null) {
|
||||
Gerrit.Nav.navigateToStatusSearch(status);
|
||||
}
|
||||
_goToAbandonedChanges() {
|
||||
Gerrit.Nav.navigateToStatusSearch('abandoned');
|
||||
},
|
||||
|
||||
_computePluginScreenName({plugin, screen}) {
|
||||
|
@ -114,55 +114,5 @@ limitations under the License.
|
||||
element._paramsChanged({base: {view: Gerrit.Nav.View.SEARCH}});
|
||||
assert.ok(element._lastSearchPage);
|
||||
});
|
||||
|
||||
suite('_jumpKeyPressed', () => {
|
||||
let navStub;
|
||||
|
||||
setup(() => {
|
||||
navStub = sandbox.stub(Gerrit.Nav, 'navigateToStatusSearch');
|
||||
sandbox.stub(Date, 'now').returns(10000);
|
||||
});
|
||||
|
||||
test('success', () => {
|
||||
const e = {detail: {key: 'a'}, preventDefault: () => {}};
|
||||
sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
|
||||
element._lastGKeyPressTimestamp = 9000;
|
||||
element._jumpKeyPressed(e);
|
||||
assert.isTrue(navStub.calledOnce);
|
||||
assert.equal(navStub.lastCall.args[0], 'abandoned');
|
||||
});
|
||||
|
||||
test('no g key', () => {
|
||||
const e = {detail: {key: 'a'}, preventDefault: () => {}};
|
||||
sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
|
||||
element._lastGKeyPressTimestamp = null;
|
||||
element._jumpKeyPressed(e);
|
||||
assert.isFalse(navStub.called);
|
||||
});
|
||||
|
||||
test('g key too long ago', () => {
|
||||
const e = {detail: {key: 'a'}, preventDefault: () => {}};
|
||||
sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
|
||||
element._lastGKeyPressTimestamp = 3000;
|
||||
element._jumpKeyPressed(e);
|
||||
assert.isFalse(navStub.called);
|
||||
});
|
||||
|
||||
test('should suppress', () => {
|
||||
const e = {detail: {key: 'a'}, preventDefault: () => {}};
|
||||
sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(true);
|
||||
element._lastGKeyPressTimestamp = 9000;
|
||||
element._jumpKeyPressed(e);
|
||||
assert.isFalse(navStub.called);
|
||||
});
|
||||
|
||||
test('unrecognized key', () => {
|
||||
const e = {detail: {key: 'f'}, preventDefault: () => {}};
|
||||
sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
|
||||
element._lastGKeyPressTimestamp = 9000;
|
||||
element._jumpKeyPressed(e);
|
||||
assert.isFalse(navStub.called);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
@ -92,6 +92,8 @@ limitations under the License.
|
||||
'core/gr-account-dropdown/gr-account-dropdown_test.html',
|
||||
'core/gr-error-dialog/gr-error-dialog_test.html',
|
||||
'core/gr-error-manager/gr-error-manager_test.html',
|
||||
'core/gr-key-binding-display/gr-key-binding-display_test.html',
|
||||
'core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.html',
|
||||
'core/gr-main-header/gr-main-header_test.html',
|
||||
'core/gr-navigation/gr-navigation_test.html',
|
||||
'core/gr-reporting/gr-jank-detector_test.html',
|
||||
|
Loading…
x
Reference in New Issue
Block a user