diff --git a/packages/commands/src/index.ts b/packages/commands/src/index.ts index 3db49daed..d0ee91a4a 100644 --- a/packages/commands/src/index.ts +++ b/packages/commands/src/index.ts @@ -1062,26 +1062,6 @@ export namespace CommandRegistry { */ shift: boolean; - /** - * Whether `'ArrowLeft'` appears in the keystroke. - */ - arrowLeft: boolean; - - /** - * Whether `'ArrowUp'` appears in the keystroke. - */ - arrowUp: boolean; - - /** - * Whether `'ArrowRight'` appears in the keystroke. - */ - arrowRight: boolean; - - /** - * Whether `'ArrowDown'` appears in the keystroke. - */ - arrowDown: boolean; - /** * The primary key for the keystroke. */ @@ -1116,10 +1096,6 @@ export namespace CommandRegistry { let cmd = false; let ctrl = false; let shift = false; - let arrowLeft = false; - let arrowUp = false; - let arrowRight = false; - let arrowDown = false; for (let token of keystroke.split(/\s+/)) { if (token === 'Accel') { if (Platform.IS_MAC) { @@ -1135,29 +1111,11 @@ export namespace CommandRegistry { ctrl = true; } else if (token === 'Shift') { shift = true; - } else if (token === 'ArrowLeft') { - arrowLeft = true; - } else if (token === 'ArrowUp') { - arrowUp = true; - } else if (token === 'ArrowRight') { - arrowRight = true; - } else if (token === 'ArrowDown') { - arrowDown = true; } else if (token.length > 0) { key = token; } } - return { - cmd, - ctrl, - alt, - shift, - key, - arrowLeft, - arrowUp, - arrowRight, - arrowDown - }; + return { cmd, ctrl, alt, shift, key }; } /** @@ -1214,45 +1172,25 @@ export namespace CommandRegistry { * Format a keystroke for display on the local system. */ export function formatKeystroke(keystroke: string): string { - let mods = ''; let parts = parseKeystroke(keystroke); - if (Platform.IS_MAC) { - if (parts.ctrl) { - mods += '\u2303 '; - } - if (parts.alt) { - mods += '\u2325 '; - } - if (parts.shift) { - mods += '\u21E7 '; - } - if (parts.cmd) { - mods += '\u2318 '; - } - if (parts.arrowLeft) { - mods += '\u2190 '; - } - if (parts.arrowUp) { - mods += '\u2191 '; - } - if (parts.arrowRight) { - mods += '\u2192 '; - } - if (parts.arrowDown) { - mods += '\u2193 '; - } - } else { - if (parts.ctrl) { - mods += 'Ctrl+'; - } - if (parts.alt) { - mods += 'Alt+'; - } - if (parts.shift) { - mods += 'Shift+'; - } + let layout = getKeyboardLayout(); + let label = []; + let separator = Platform.IS_MAC ? ' ' : '+'; + if (parts.ctrl) { + label.push('Ctrl'); } - return mods + parts.key; + if (parts.alt) { + label.push('Alt'); + } + if (parts.shift) { + label.push('Shift'); + } + if (Platform.IS_MAC && parts.cmd) { + // Keyboard layouts label Command as Meta + label.push('Meta'); + } + label.push(parts.key); + return label.map(k => layout.formatKey(k)).join(separator); } /** @@ -1282,20 +1220,25 @@ export namespace CommandRegistry { if (!key || layout.isModifierKey(key)) { return ''; } - let mods = ''; - if (event.ctrlKey) { - mods += 'Ctrl '; - } - if (event.altKey) { - mods += 'Alt '; - } - if (event.shiftKey) { - mods += 'Shift '; - } - if (event.metaKey && Platform.IS_MAC) { - mods += 'Cmd '; + // Loop through modifier keys in order to test them + let mods = []; + for (let mod of layout.modifierKeys()) { + // Special treatment for Meta (Cmd on macOS) for backwards compatibility + if (mod === 'Meta') { + if (Platform.IS_MAC && event.getModifierState(mod)) { + mods.push('Cmd'); + } + } else if (mod === 'Ctrl') { + // For backwards compatibility, our keyboard layout still uses Ctrl. + if (event.getModifierState('Control')) { + mods.push('Ctrl'); + } + } else if (event.getModifierState(mod)) { + mods.push(mod); + } } - return mods + key; + mods.push(key); + return mods.join(' '); } } @@ -1635,6 +1578,7 @@ namespace Private { clone.shiftKey = event.shiftKey || false; clone.metaKey = event.metaKey || false; clone.view = event.view || window; + clone.getModifierState = (key: string) => event.getModifierState(key); return clone as KeyboardEvent; } } diff --git a/packages/commands/tests/src/index.spec.ts b/packages/commands/tests/src/index.spec.ts index fd8bd8235..4ae79bd66 100644 --- a/packages/commands/tests/src/index.spec.ts +++ b/packages/commands/tests/src/index.spec.ts @@ -26,6 +26,29 @@ const NULL_COMMAND = { } }; +function generateKeydown(options: any) { + let event = generate('keydown', options); + (event as any).getModifierState = (key: string) => { + let state = false; + switch (key) { + case 'Alt': + state = options.altKey || false; + break; + case 'Control': + state = options.ctrlKey || false; + break; + case 'Shift': + state = options.shiftKey || false; + break; + case 'Meta': + state = options.metaKey || false; + break; + } + return state; + }; + return event; +} + describe('@lumino/commands', () => { describe('CommandRegistry', () => { let registry: CommandRegistry = null!; @@ -581,7 +604,7 @@ describe('@lumino/commands', () => { selector: `#${elem.id}`, command: 'test' }); - let event = generate('keydown', { keyCode: 59, ctrlKey: true }); + let event = generateKeydown({ keyCode: 59, ctrlKey: true }); elem.dispatchEvent(event); expect(called).to.equal(true); }); @@ -599,7 +622,7 @@ describe('@lumino/commands', () => { command: 'test' }); binding.dispose(); - let event = generate('keydown', { keyCode: 59, ctrlKey: true }); + let event = generateKeydown({ keyCode: 59, ctrlKey: true }); elem.dispatchEvent(event); expect(called).to.equal(false); }); @@ -641,7 +664,7 @@ describe('@lumino/commands', () => { selector: `#${elem.id}`, command: 'test' }); - let event = generate('keydown', { keyCode: 59, ctrlKey: true }); + let event = generateKeydown({ keyCode: 59, ctrlKey: true }); elem.dispatchEvent(event); expect(called).to.equal(true); }); @@ -659,7 +682,7 @@ describe('@lumino/commands', () => { command: 'test' }); parent.setAttribute('data-lm-suppress-shortcuts', 'true'); - let event = generate('keydown', { keyCode: 59, ctrlKey: true }); + let event = generateKeydown({ keyCode: 59, ctrlKey: true }); elem.dispatchEvent(event); expect(called).to.equal(false); }); @@ -676,7 +699,7 @@ describe('@lumino/commands', () => { selector: `#${elem.id}`, command: 'test' }); - let event = generate('keydown', { keyCode: 45, ctrlKey: true }); + let event = generateKeydown({ keyCode: 45, ctrlKey: true }); elem.dispatchEvent(event); expect(called).to.equal(false); }); @@ -693,8 +716,8 @@ describe('@lumino/commands', () => { selector: `#${elem.id}`, command: 'test' }); - let eventAlt = generate('keydown', { keyCode: 83, altKey: true }); - let eventShift = generate('keydown', { keyCode: 83, shiftKey: true }); + let eventAlt = generateKeydown({ keyCode: 83, altKey: true }); + let eventShift = generateKeydown({ keyCode: 83, shiftKey: true }); elem.dispatchEvent(eventAlt); expect(count).to.equal(0); elem.dispatchEvent(eventShift); @@ -713,17 +736,17 @@ describe('@lumino/commands', () => { selector: `#${elem.id}`, command: 'test' }); - let eventK = generate('keydown', { keyCode: 75, ctrlKey: true }); - let eventL = generate('keydown', { keyCode: 76, ctrlKey: true }); + let eventK = generateKeydown({ keyCode: 75, ctrlKey: true }); + let eventL = generateKeydown({ keyCode: 76, ctrlKey: true }); elem.dispatchEvent(eventK); expect(count).to.equal(0); elem.dispatchEvent(eventL); expect(count).to.equal(1); - elem.dispatchEvent(generate('keydown', eventL)); // Don't reuse; clone. + elem.dispatchEvent(generateKeydown(eventL)); // Don't reuse; clone. expect(count).to.equal(1); - elem.dispatchEvent(generate('keydown', eventK)); // Don't reuse; clone. + elem.dispatchEvent(generateKeydown(eventK)); // Don't reuse; clone. expect(count).to.equal(1); - elem.dispatchEvent(generate('keydown', eventL)); // Don't reuse; clone. + elem.dispatchEvent(generateKeydown(eventL)); // Don't reuse; clone. expect(count).to.equal(2); }); @@ -739,7 +762,7 @@ describe('@lumino/commands', () => { selector: '.inaccessible-scope', command: 'test' }); - let event = generate('keydown', { keyCode: 80, shiftKey: true }); + let event = generateKeydown({ keyCode: 80, shiftKey: true }); expect(count).to.equal(0); elem.dispatchEvent(event); expect(count).to.equal(0); @@ -757,7 +780,7 @@ describe('@lumino/commands', () => { selector: `#${elem.id}`, command: 'test' }); - let event = generate('keydown', { keyCode: 17 }); + let event = generateKeydown({ keyCode: 17 }); expect(count).to.equal(0); elem.dispatchEvent(event); expect(count).to.equal(0); @@ -786,8 +809,8 @@ describe('@lumino/commands', () => { selector: `#${elem.id}`, command: 'test2' }); - let event1 = generate('keydown', { keyCode: 83, ctrlKey: true }); - let event2 = generate('keydown', { keyCode: 68, ctrlKey: true }); + let event1 = generateKeydown({ keyCode: 83, ctrlKey: true }); + let event2 = generateKeydown({ keyCode: 68, ctrlKey: true }); expect(count1).to.equal(0); expect(count2).to.equal(0); elem.dispatchEvent(event1); @@ -821,13 +844,13 @@ describe('@lumino/commands', () => { selector: `#${elem.id}`, command: 'test2' }); - let event1 = generate('keydown', { + let event1 = generateKeydown({ keyCode: 84, ctrlKey: true, altKey: true, shiftKey: true }); - let event2 = generate('keydown', { + let event2 = generateKeydown({ keyCode: 81, ctrlKey: true, altKey: true, @@ -858,9 +881,10 @@ describe('@lumino/commands', () => { selector: `#${elem.id}`, command: 'test' }); - let event1 = generate('keydown', { keyCode: 68 }); - let event2 = generate('keydown', { keyCode: 69 }); + let event1 = generateKeydown({ keyCode: 68 }); + let event2 = generateKeydown({ keyCode: 69 }); elem.dispatchEvent(event1); + console.log(codes); expect(codes.length).to.equal(0); elem.dispatchEvent(event2); expect(called).to.equal(false); @@ -885,7 +909,7 @@ describe('@lumino/commands', () => { selector: `#${elem.id}`, command: 'test' }); - let event = generate('keydown', { keyCode: 68 }); + let event = generateKeydown({ keyCode: 68 }); elem.dispatchEvent(event); expect(codes.length).to.equal(0); setTimeout(() => { @@ -919,7 +943,7 @@ describe('@lumino/commands', () => { selector: `#${elem.id}`, command: 'test2' }); - let event = generate('keydown', { keyCode: 68 }); + let event = generateKeydown({ keyCode: 68 }); elem.dispatchEvent(event); expect(called1).to.equal(false); expect(called2).to.equal(false); @@ -954,7 +978,7 @@ describe('@lumino/commands', () => { selector: `#${elem.id}`, command: 'test2' }); - let event = generate('keydown', { keyCode: 59, ctrlKey: true }); + let event = generateKeydown({ keyCode: 59, ctrlKey: true }); elem.dispatchEvent(event); expect(called1).to.equal(false); expect(called2).to.equal(true); @@ -977,7 +1001,7 @@ describe('@lumino/commands', () => { selector: '#baz', command: 'test' }); - let event = generate('keydown', { keyCode: 68 }); + let event = generateKeydown({ keyCode: 68 }); elem.dispatchEvent(event); expect(codes).to.deep.equal([68]); expect(called).to.equal(false); @@ -1001,7 +1025,7 @@ describe('@lumino/commands', () => { selector: '#baz', command: 'test' }); - let event = generate('keydown', { keyCode: 68 }); + let event = generateKeydown({ keyCode: 68 }); elem.dispatchEvent(event); expect(codes).to.deep.equal([68]); expect(called).to.equal(false); @@ -1020,9 +1044,9 @@ describe('@lumino/commands', () => { selector: `#${elem.id}`, command: 'test' }); - let eventK = generate('keydown', { keyCode: 75, ctrlKey: true }); - let eventCtrl = generate('keydown', { keyCode: 17, ctrlKey: true }); - let eventL = generate('keydown', { keyCode: 76, ctrlKey: true }); + let eventK = generateKeydown({ keyCode: 75, ctrlKey: true }); + let eventCtrl = generateKeydown({ keyCode: 17, ctrlKey: true }); + let eventL = generateKeydown({ keyCode: 76, ctrlKey: true }); elem.dispatchEvent(eventK); expect(count).to.equal(0); elem.dispatchEvent(eventCtrl); // user presses Ctrl again - this should not break the sequence @@ -1043,10 +1067,10 @@ describe('@lumino/commands', () => { selector: `#${elem.id}`, command: 'test' }); - let eventShift = generate('keydown', { keyCode: 16, shiftlKey: true }); - let eventK = generate('keydown', { keyCode: 75, shiftKey: true }); - let eventCtrl = generate('keydown', { keyCode: 17, ctrlKey: true }); - let eventL = generate('keydown', { keyCode: 76, ctrlKey: true }); + let eventShift = generateKeydown({ keyCode: 16, shiftlKey: true }); + let eventK = generateKeydown({ keyCode: 75, shiftKey: true }); + let eventCtrl = generateKeydown({ keyCode: 17, ctrlKey: true }); + let eventL = generateKeydown({ keyCode: 76, ctrlKey: true }); elem.dispatchEvent(eventShift); expect(count).to.equal(0); elem.dispatchEvent(eventK); @@ -1107,7 +1131,7 @@ describe('@lumino/commands', () => { describe('.keystrokeForKeydownEvent()', () => { it('should create a normalized keystroke', () => { - let event = generate('keydown', { ctrlKey: true, keyCode: 83 }); + let event = generateKeydown({ ctrlKey: true, keyCode: 83 }); let keystroke = CommandRegistry.keystrokeForKeydownEvent( event as KeyboardEvent ); @@ -1115,7 +1139,7 @@ describe('@lumino/commands', () => { }); it('should handle multiple modifiers', () => { - let event = generate('keydown', { + let event = generateKeydown({ ctrlKey: true, altKey: true, shiftKey: true, @@ -1128,7 +1152,7 @@ describe('@lumino/commands', () => { }); it('should fail on an invalid shortcut', () => { - let event = generate('keydown', { keyCode: -1 }); + let event = generateKeydown({ keyCode: -1 }); let keystroke = CommandRegistry.keystrokeForKeydownEvent( event as KeyboardEvent ); @@ -1136,7 +1160,7 @@ describe('@lumino/commands', () => { }); it('should return nothing for keys that are marked as modifier in keyboard layout', () => { - let event = generate('keydown', { keyCode: 17, ctrlKey: true }); + let event = generateKeydown({ keyCode: 17, ctrlKey: true }); let keystroke = CommandRegistry.keystrokeForKeydownEvent( event as KeyboardEvent ); diff --git a/packages/keyboard/package.json b/packages/keyboard/package.json index 4f84a2405..16e7cf9dd 100644 --- a/packages/keyboard/package.json +++ b/packages/keyboard/package.json @@ -43,6 +43,9 @@ "test:ie": "cd tests && karma start --browsers=IE", "watch": "tsc --build --watch" }, + "dependencies": { + "@lumino/domutils": "^1.8.0" + }, "devDependencies": { "@microsoft/api-extractor": "^7.6.0", "@types/chai": "^3.4.35", diff --git a/packages/keyboard/src/index.ts b/packages/keyboard/src/index.ts index 3e3cd5d01..4648697cc 100644 --- a/packages/keyboard/src/index.ts +++ b/packages/keyboard/src/index.ts @@ -8,6 +8,8 @@ | The full license is in the file LICENSE, distributed with this software. |----------------------------------------------------------------------------*/ +import { Platform } from '@lumino/domutils'; + /** * An object which represents an abstract keyboard layout. */ @@ -30,6 +32,17 @@ export interface IKeyboardLayout { */ keys(): string[]; + /** + * Get an arrow of all modifier key values supported by the layout. + * + * @returns A new array of the supported modifier key values. + * + * #### Notes + * This can be useful for authoring tools and debugging, when it's + * necessary to know which modifier keys are available for shortcut use. + */ + modifierKeys(): string[]; + /** * Test whether the given key is a valid value for the layout. * @@ -40,18 +53,17 @@ export interface IKeyboardLayout { isValidKey(key: string): boolean; /** - * Test whether the given key is a modifier key. + * Test whether the given key is a valid modifier key for the layout. * - * @param key - The user provided key. + * @param key - The user provided key to test for validity. * * @returns `true` if the key is a modifier key, `false` otherwise. * * #### Notes - * This is necessary so that we don't process modifier keys pressed - * in the middle of the key sequence. - * E.g. "Shift C Ctrl P" is actually 4 keydown events: - * "Shift", "Shift P", "Ctrl", "Ctrl P", - * and events for "Shift" and "Ctrl" should be ignored. + * This may be useful to determine whether we should ignore a keypress + * event in the middle of a key sequence. For example, "Shift C Ctrl P" is + * actually four keydown events: "Shift, "Shift P", "Ctrl", "Ctrl P", but + * the keypress events for the modifier keys "Shift" and "Ctrl" are ignored. */ isModifierKey(key: string): boolean; @@ -64,6 +76,15 @@ export interface IKeyboardLayout { * does not represent a valid primary key. */ keyForKeydownEvent(event: KeyboardEvent): string; + + /** + * Get the formatted string for displaying a key in the user interface. + * + * @param key - The user provided key. + * + * @returns A unicode string representing the key in the interface. + */ + formatKey(key: string): string; } /** @@ -115,12 +136,14 @@ export class KeycodeLayout implements IKeyboardLayout { constructor( name: string, codes: KeycodeLayout.CodeMap, - modifierKeys: string[] = [] + modifierKeys: string[] = [], + format: KeycodeLayout.KeyFormat = x => x ) { this.name = name; this._codes = codes; this._keys = KeycodeLayout.extractKeys(codes); - this._modifierKeys = KeycodeLayout.convertToKeySet(modifierKeys); + this._modifierKeys = new Set(modifierKeys); + this._format = format; } /** @@ -137,6 +160,15 @@ export class KeycodeLayout implements IKeyboardLayout { return Object.keys(this._keys); } + /** + * Get an arrow of all modifier key values supported by the layout. + * + * @returns A new array of the supported modifier key values. + */ + modifierKeys(): string[] { + return Array.from(this._modifierKeys); + } + /** * Test whether the given key is a valid value for the layout. * @@ -156,7 +188,7 @@ export class KeycodeLayout implements IKeyboardLayout { * @returns `true` if the key is a modifier key, `false` otherwise. */ isModifierKey(key: string): boolean { - return key in this._modifierKeys; + return this._modifierKeys.has(key); } /** @@ -171,9 +203,21 @@ export class KeycodeLayout implements IKeyboardLayout { return this._codes[event.keyCode] || ''; } + /** + * Get the formatted string for displaying a key in the user interface. + * + * @param key - The user provided key. + * + * @returns A unicode string representing the key in the interface. + */ + formatKey(key: string): string { + return this._format(key); + } + private _keys: KeycodeLayout.KeySet; private _codes: KeycodeLayout.CodeMap; - private _modifierKeys: KeycodeLayout.KeySet; + private _modifierKeys: Set; + private _format: KeycodeLayout.KeyFormat; } /** @@ -190,6 +234,11 @@ export namespace KeycodeLayout { */ export type KeySet = { readonly [key: string]: boolean }; + /** + * A type alias for a key format function. + */ + export type KeyFormat = (key: string) => string; + /** * Extract the set of keys from a code map. * @@ -221,6 +270,57 @@ export namespace KeycodeLayout { } } +export const MAC_DISPLAY: { [key: string]: string } = { + Backspace: '⌫', + Tab: '⇥', + Enter: '↩', + Shift: '⇧', + Ctrl: '⌃', + Alt: '⌥', + Escape: '⎋', + PageUp: '⇞', + PageDown: '⇟', + End: '↘', + Home: '↖', + ArrowLeft: '←', + ArrowUp: '↑', + ArrowRight: '→', + ArrowDown: '↓', + Delete: '⌦', + Meta: '⌘' +}; + +export const WIN_DISPLAY: { [key: string]: string } = { + // 'Backspace': '⌫', + // 'Tab': '⇥', + // 'Enter': 'Return', + // 'Shift': '⇧', + // 'Ctrl': 'Ctrl', + // 'Alt': '⌥', + // 'Pause': '', + Escape: 'Esc', + // 'Space': '␣', + PageUp: 'Page Up', + PageDown: 'Page Down', + // 'End': '↘', + // 'Home': '↖', + ArrowLeft: 'Left', + ArrowUp: 'Right', + ArrowRight: 'Up', + ArrowDown: 'Down', + // 'Insert': '', + Delete: 'Del' + // 'Meta': '' +}; + +export function formatKey(key: string): string { + if (Platform.IS_MAC) { + return MAC_DISPLAY.hasOwnProperty(key) ? MAC_DISPLAY[key] : key; + } else { + return WIN_DISPLAY.hasOwnProperty(key) ? WIN_DISPLAY[key] : key; + } +} + /** * A keycode-based keyboard layout for US English keyboards. * @@ -345,7 +445,9 @@ export const EN_US: IKeyboardLayout = new KeycodeLayout( 222: "'", 224: 'Meta' // firefox }, - ['Shift', 'Ctrl', 'Alt', 'Meta'] // modifier keys + // The modifier is labeled "Control", but the key value is "Ctrl"? + ['Ctrl', 'Alt', 'Shift', 'Meta'], // modifier keys in display order + formatKey ); /** diff --git a/packages/keyboard/tsconfig.json b/packages/keyboard/tsconfig.json index 17f720517..6f983ed21 100644 --- a/packages/keyboard/tsconfig.json +++ b/packages/keyboard/tsconfig.json @@ -13,7 +13,7 @@ "moduleResolution": "node", "target": "ES5", "outDir": "lib", - "lib": ["ES5", "DOM", "ES2015.Iterable"], + "lib": ["ES5", "DOM", "ES2015.Collection", "ES2015.Iterable"], "importHelpers": true, "types": [], "rootDir": "src"