mirror of
https://github.com/google/blockly.git
synced 2026-05-23 04:20:07 +02:00
feat: Display keyboard shortcuts in context menus (#9785)
* feat: Display keyboard shortcuts in context menus * refactor: Add mapping to keyboard shortcut to context menu item interfaces * chore: Add comment * refactor: Don't match shortcuts and menus based on ID
This commit is contained in:
@@ -11,6 +11,7 @@ import type {BlockSvg} from './block_svg.js';
|
||||
import * as browserEvents from './browser_events.js';
|
||||
import {config} from './config.js';
|
||||
import type {
|
||||
ActionContextMenuOption,
|
||||
ContextMenuOption,
|
||||
LegacyContextMenuOption,
|
||||
} from './contextmenu_registry.js';
|
||||
@@ -25,6 +26,7 @@ import * as aria from './utils/aria.js';
|
||||
import {Coordinate} from './utils/coordinate.js';
|
||||
import * as dom from './utils/dom.js';
|
||||
import {Rect} from './utils/rect.js';
|
||||
import {getShortcutKeysShort} from './utils/shortcut_formatting.js';
|
||||
import * as svgMath from './utils/svg_math.js';
|
||||
import * as WidgetDiv from './widgetdiv.js';
|
||||
import type {WorkspaceSvg} from './workspace_svg.js';
|
||||
@@ -134,7 +136,7 @@ function populate_(
|
||||
continue;
|
||||
}
|
||||
|
||||
const menuItem = new MenuItem(option.text);
|
||||
const menuItem = new MenuItem(makeMenuitem(option));
|
||||
menuItem.setRightToLeft(rtl);
|
||||
menuItem.setRole(aria.Role.MENUITEM);
|
||||
menu.addChild(menuItem);
|
||||
@@ -302,3 +304,48 @@ export function callbackFactory(
|
||||
export function getMenu(): Menu | null {
|
||||
return menu_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a menu item to represent the given context menu option.
|
||||
* For text-based menu options, this wraps the text in a container with its
|
||||
* corresponding keyboard shortcut, if any. HTML-based menu options are displayed
|
||||
* as-is.
|
||||
*
|
||||
* @param option The context menu option to generate a menu item for.
|
||||
* @returns A `MenuItem` representing the given context menu option.
|
||||
*/
|
||||
function makeMenuitem(
|
||||
option: ActionContextMenuOption | LegacyContextMenuOption,
|
||||
) {
|
||||
const text = option.text;
|
||||
if (text && !(text instanceof HTMLElement)) {
|
||||
const container = document.createElement('div');
|
||||
container.className = 'blocklyShortcutContainer';
|
||||
const label = document.createElement('span');
|
||||
label.textContent = text;
|
||||
const shortcut = document.createElement('span');
|
||||
shortcut.className = 'blocklyShortcut';
|
||||
shortcut.textContent = ` ${getKeyboardShortcut(option)}`;
|
||||
container.appendChild(label);
|
||||
container.appendChild(shortcut);
|
||||
return container;
|
||||
}
|
||||
|
||||
return option.text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a textual representation of the keyboard shortcut for the given
|
||||
* context menu item, if any.
|
||||
*
|
||||
* @param option The context menu item to retrieve a keyboard shortcut for.
|
||||
* @returns A textual representation of the keyboard shortcut registered under
|
||||
* the name stored in the menu option's `associatedKeyboardShortcut` field,
|
||||
* if any.
|
||||
*/
|
||||
function getKeyboardShortcut(
|
||||
option: ContextMenuOption | LegacyContextMenuOption,
|
||||
): string {
|
||||
if (!('id' in option) || !option.associatedKeyboardShortcut) return '';
|
||||
return getShortcutKeysShort(option.associatedKeyboardShortcut);
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ export function registerUndo() {
|
||||
scopeType: ContextMenuRegistry.ScopeType.WORKSPACE,
|
||||
id: 'undoWorkspace',
|
||||
weight: 1,
|
||||
associatedKeyboardShortcut: 'undo',
|
||||
};
|
||||
ContextMenuRegistry.registry.register(undoOption);
|
||||
}
|
||||
@@ -76,6 +77,7 @@ export function registerRedo() {
|
||||
scopeType: ContextMenuRegistry.ScopeType.WORKSPACE,
|
||||
id: 'redoWorkspace',
|
||||
weight: 2,
|
||||
associatedKeyboardShortcut: 'redo',
|
||||
};
|
||||
ContextMenuRegistry.registry.register(redoOption);
|
||||
}
|
||||
@@ -103,6 +105,7 @@ export function registerCleanup() {
|
||||
scopeType: ContextMenuRegistry.ScopeType.WORKSPACE,
|
||||
id: 'cleanWorkspace',
|
||||
weight: 3,
|
||||
associatedKeyboardShortcut: 'cleanup',
|
||||
};
|
||||
ContextMenuRegistry.registry.register(cleanOption);
|
||||
}
|
||||
@@ -349,6 +352,7 @@ export function registerDuplicate() {
|
||||
scopeType: ContextMenuRegistry.ScopeType.BLOCK,
|
||||
id: 'blockDuplicate',
|
||||
weight: 1,
|
||||
associatedKeyboardShortcut: 'duplicate',
|
||||
};
|
||||
ContextMenuRegistry.registry.register(duplicateOption);
|
||||
}
|
||||
@@ -547,6 +551,7 @@ export function registerDelete() {
|
||||
scopeType: ContextMenuRegistry.ScopeType.BLOCK,
|
||||
id: 'blockDelete',
|
||||
weight: 6,
|
||||
associatedKeyboardShortcut: 'delete',
|
||||
};
|
||||
ContextMenuRegistry.registry.register(deleteOption);
|
||||
}
|
||||
@@ -596,6 +601,7 @@ export function registerCommentDelete() {
|
||||
scopeType: ContextMenuRegistry.ScopeType.COMMENT,
|
||||
id: 'commentDelete',
|
||||
weight: 6,
|
||||
associatedKeyboardShortcut: 'delete',
|
||||
};
|
||||
ContextMenuRegistry.registry.register(deleteOption);
|
||||
}
|
||||
@@ -616,6 +622,7 @@ export function registerCommentDuplicate() {
|
||||
scopeType: ContextMenuRegistry.ScopeType.COMMENT,
|
||||
id: 'commentDuplicate',
|
||||
weight: 1,
|
||||
associatedKeyboardShortcut: 'duplicate',
|
||||
};
|
||||
ContextMenuRegistry.registry.register(duplicateOption);
|
||||
}
|
||||
|
||||
@@ -100,6 +100,7 @@ export class ContextMenuRegistry {
|
||||
| ContextMenuRegistry.SeparatorContextMenuOption
|
||||
| ContextMenuRegistry.ActionContextMenuOption;
|
||||
menuOption = {
|
||||
id: item.id,
|
||||
scope,
|
||||
weight: item.weight,
|
||||
};
|
||||
@@ -122,6 +123,7 @@ export class ContextMenuRegistry {
|
||||
text: displayText,
|
||||
callback: item.callback,
|
||||
enabled: precondition === 'enabled',
|
||||
associatedKeyboardShortcut: item.associatedKeyboardShortcut,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -188,6 +190,13 @@ export namespace ContextMenuRegistry {
|
||||
displayText: ((p1: Scope) => string | HTMLElement) | string | HTMLElement;
|
||||
separator?: never;
|
||||
preconditionFn: (p1: Scope, menuOpenEvent: Event) => string;
|
||||
/**
|
||||
* Identifier used to associate this context menu item with a keyboard
|
||||
* shortcut which will be displayed in the menu as a hint. Should
|
||||
* correspond to the name under which a keyboard shortcut that performs the
|
||||
* same action as this menu item is registered.
|
||||
*/
|
||||
associatedKeyboardShortcut?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -208,8 +217,10 @@ export namespace ContextMenuRegistry {
|
||||
* Fields common to all context menu items as used by contextmenu.ts.
|
||||
*/
|
||||
export interface CoreContextMenuOption {
|
||||
id: string;
|
||||
scope: Scope;
|
||||
weight: number;
|
||||
associatedKeyboardShortcut?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -276,3 +287,5 @@ export type RegistryItem = ContextMenuRegistry.RegistryItem;
|
||||
export type ContextMenuOption = ContextMenuRegistry.ContextMenuOption;
|
||||
export type LegacyContextMenuOption =
|
||||
ContextMenuRegistry.LegacyContextMenuOption;
|
||||
export type ActionContextMenuOption =
|
||||
ContextMenuRegistry.ActionContextMenuOption;
|
||||
|
||||
@@ -484,6 +484,19 @@ input[type=number] {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.blocklyRTL .blocklyMenuItemContent .blocklyShortcutContainer {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
.blocklyMenuItemContent .blocklyShortcutContainer {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
.blocklyMenuItemContent .blocklyShortcutContainer .blocklyShortcut {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.blocklyBlockDragSurface, .blocklyAnimationLayer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
||||
@@ -48,97 +48,6 @@ const shortModifierNames: Record<string, string> = {
|
||||
'Alt': userAgent.APPLE ? '⌥' : Msg['ALT_KEY'],
|
||||
};
|
||||
|
||||
/**
|
||||
* Key names for common characters. These should be used with keyup/keydown
|
||||
* events, since the .keyCode property on those is meant to indicate the
|
||||
* _physical key_ the user held down on the keyboard. Hence the mapping uses
|
||||
* only the unshifted version of each key (e.g. no '#', since that's shift+3).
|
||||
* Keypress events on the other hand generate (mostly) ASCII codes since they
|
||||
* correspond to *characters* the user typed.
|
||||
*
|
||||
* For further reference: http://unixpapa.com/js/key.html
|
||||
*
|
||||
* This list is not localized and therefore some of the key codes are not
|
||||
* correct for non-US keyboard layouts.
|
||||
*
|
||||
* Partially copied from goog.events.keynames and modified to use translatable
|
||||
* strings or symbols for keys.
|
||||
*/
|
||||
const keyNames: Record<number, string> = {
|
||||
8: Msg['BACKSPACE_KEY'],
|
||||
9: Msg['TAB_KEY'],
|
||||
13: Msg['ENTER_KEY'],
|
||||
16: Msg['SHIFT_KEY'],
|
||||
17: Msg['CTRL_KEY'],
|
||||
18: Msg['ALT_KEY'],
|
||||
19: Msg['PAUSE_KEY'],
|
||||
20: Msg['CAPS_LOCK_KEY'],
|
||||
27: Msg['ESCAPE_KEY'],
|
||||
32: Msg['SPACE_KEY'],
|
||||
33: Msg['PAGE_UP_KEY'],
|
||||
34: Msg['PAGE_DOWN_KEY'],
|
||||
35: Msg['END_KEY'],
|
||||
36: Msg['HOME_KEY'],
|
||||
37: '←',
|
||||
38: '↑',
|
||||
39: '→',
|
||||
40: '↓',
|
||||
45: Msg['INSERT_KEY'],
|
||||
46: Msg['DELETE_KEY'],
|
||||
48: '0',
|
||||
49: '1',
|
||||
50: '2',
|
||||
51: '3',
|
||||
52: '4',
|
||||
53: '5',
|
||||
54: '6',
|
||||
55: '7',
|
||||
56: '8',
|
||||
57: '9',
|
||||
59: ';',
|
||||
61: '=',
|
||||
93: Msg['CONTEXT_MENU_KEY'],
|
||||
96: '0',
|
||||
97: '1',
|
||||
98: '2',
|
||||
99: '3',
|
||||
100: '4',
|
||||
101: '5',
|
||||
102: '6',
|
||||
103: '7',
|
||||
104: '8',
|
||||
105: '9',
|
||||
106: '×',
|
||||
107: '+',
|
||||
109: '−',
|
||||
110: '.',
|
||||
111: '÷',
|
||||
112: 'F1',
|
||||
113: 'F2',
|
||||
114: 'F3',
|
||||
115: 'F4',
|
||||
116: 'F5',
|
||||
117: 'F6',
|
||||
118: 'F7',
|
||||
119: 'F8',
|
||||
120: 'F9',
|
||||
121: 'F10',
|
||||
122: 'F11',
|
||||
123: 'F12',
|
||||
186: ';',
|
||||
187: '=',
|
||||
189: '-',
|
||||
188: ',',
|
||||
190: '.',
|
||||
191: '/',
|
||||
192: '`',
|
||||
219: '[',
|
||||
220: '\\',
|
||||
221: ']',
|
||||
222: "'",
|
||||
224: '⌘',
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets a user-facing name for a keycode.
|
||||
*
|
||||
@@ -150,6 +59,98 @@ function getKeyName(keyCode: number): string {
|
||||
// letters a-z
|
||||
return String.fromCharCode(keyCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Key names for common characters. These should be used with keyup/keydown
|
||||
* events, since the .keyCode property on those is meant to indicate the
|
||||
* _physical key_ the user held down on the keyboard. Hence the mapping uses
|
||||
* only the unshifted version of each key (e.g. no '#', since that's shift+3).
|
||||
* Keypress events on the other hand generate (mostly) ASCII codes since they
|
||||
* correspond to *characters* the user typed.
|
||||
*
|
||||
* For further reference: http://unixpapa.com/js/key.html
|
||||
*
|
||||
* This list is not localized and therefore some of the key codes are not
|
||||
* correct for non-US keyboard layouts.
|
||||
*
|
||||
* Partially copied from goog.events.keynames and modified to use translatable
|
||||
* strings or symbols for keys.
|
||||
*/
|
||||
const keyNames: Record<number, string> = {
|
||||
8: Msg['BACKSPACE_KEY'],
|
||||
9: Msg['TAB_KEY'],
|
||||
13: Msg['ENTER_KEY'],
|
||||
16: Msg['SHIFT_KEY'],
|
||||
17: Msg['CTRL_KEY'],
|
||||
18: Msg['ALT_KEY'],
|
||||
19: Msg['PAUSE_KEY'],
|
||||
20: Msg['CAPS_LOCK_KEY'],
|
||||
27: Msg['ESCAPE_KEY'],
|
||||
32: Msg['SPACE_KEY'],
|
||||
33: Msg['PAGE_UP_KEY'],
|
||||
34: Msg['PAGE_DOWN_KEY'],
|
||||
35: Msg['END_KEY'],
|
||||
36: Msg['HOME_KEY'],
|
||||
37: '←',
|
||||
38: '↑',
|
||||
39: '→',
|
||||
40: '↓',
|
||||
45: Msg['INSERT_KEY'],
|
||||
46: Msg['DELETE_KEY'],
|
||||
48: '0',
|
||||
49: '1',
|
||||
50: '2',
|
||||
51: '3',
|
||||
52: '4',
|
||||
53: '5',
|
||||
54: '6',
|
||||
55: '7',
|
||||
56: '8',
|
||||
57: '9',
|
||||
59: ';',
|
||||
61: '=',
|
||||
93: Msg['CONTEXT_MENU_KEY'],
|
||||
96: '0',
|
||||
97: '1',
|
||||
98: '2',
|
||||
99: '3',
|
||||
100: '4',
|
||||
101: '5',
|
||||
102: '6',
|
||||
103: '7',
|
||||
104: '8',
|
||||
105: '9',
|
||||
106: '×',
|
||||
107: '+',
|
||||
109: '−',
|
||||
110: '.',
|
||||
111: '÷',
|
||||
112: 'F1',
|
||||
113: 'F2',
|
||||
114: 'F3',
|
||||
115: 'F4',
|
||||
116: 'F5',
|
||||
117: 'F6',
|
||||
118: 'F7',
|
||||
119: 'F8',
|
||||
120: 'F9',
|
||||
121: 'F10',
|
||||
122: 'F11',
|
||||
123: 'F12',
|
||||
186: ';',
|
||||
187: '=',
|
||||
189: '-',
|
||||
188: ',',
|
||||
190: '.',
|
||||
191: '/',
|
||||
192: '`',
|
||||
219: '[',
|
||||
220: '\\',
|
||||
221: ']',
|
||||
222: "'",
|
||||
224: '⌘',
|
||||
};
|
||||
|
||||
const keyName = keyNames[keyCode];
|
||||
if (keyName) return keyName;
|
||||
console.warn('Unknown key code: ' + keyCode);
|
||||
|
||||
Reference in New Issue
Block a user