From e794ce03c65337fbc0a15716fd1976e53a0fee2c Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske <510760+vidartf@users.noreply.github.com> Date: Sun, 2 Mar 2025 16:04:15 +0000 Subject: [PATCH 01/15] Refactor keyboard package In preparation for changes. --- packages/keyboard/src/core.ts | 197 +++++++++++++++ packages/keyboard/src/index.ts | 320 +------------------------ packages/keyboard/src/layouts/en-US.ts | 128 ++++++++++ packages/keyboard/src/layouts/index.ts | 4 + 4 files changed, 334 insertions(+), 315 deletions(-) create mode 100644 packages/keyboard/src/core.ts create mode 100644 packages/keyboard/src/layouts/en-US.ts create mode 100644 packages/keyboard/src/layouts/index.ts diff --git a/packages/keyboard/src/core.ts b/packages/keyboard/src/core.ts new file mode 100644 index 000000000..d315abf50 --- /dev/null +++ b/packages/keyboard/src/core.ts @@ -0,0 +1,197 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. +/*----------------------------------------------------------------------------- +| Copyright (c) 2014-2017, PhosphorJS Contributors +| +| Distributed under the terms of the BSD 3-Clause License. +| +| The full license is in the file LICENSE, distributed with this software. +|----------------------------------------------------------------------------*/ + +/** + * An object which represents an abstract keyboard layout. + */ +export interface IKeyboardLayout { + /** + * The human readable name of the layout. + * + * This value is used primarily for display and debugging purposes. + */ + readonly name: string; + + /** + * Get an array of all key values supported by the layout. + * + * @returns A new array of the supported key values. + * + * #### Notes + * This can be useful for authoring tools and debugging, when it's + * necessary to know which keys are available for shortcut use. + */ + keys(): string[]; + + /** + * Test whether the given key is a valid value for the layout. + * + * @param key - The user provided key to test for validity. + * + * @returns `true` if the key is valid, `false` otherwise. + */ + isValidKey(key: string): boolean; + + /** + * Test whether the given key is a modifier key. + * + * @param key - The user provided key. + * + * @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. + */ + isModifierKey(key: string): boolean; + + /** + * Get the key for a `'keydown'` event. + * + * @param event - The event object for a `'keydown'` event. + * + * @returns The associated key value, or an empty string if the event + * does not represent a valid primary key. + */ + keyForKeydownEvent(event: KeyboardEvent): string; +} + +/** + * A concrete implementation of {@link IKeyboardLayout} based on keycodes. + * + * The `keyCode` property of a `'keydown'` event is a browser and OS + * specific representation of the physical key (not character) which + * was pressed on a keyboard. While not the most convenient API, it + * is currently the only one which works reliably on all browsers. + * + * This class accepts a user-defined mapping of keycode to key, which + * allows for reliable shortcuts tailored to the user's system. + */ +export class KeycodeLayout implements IKeyboardLayout { + /** + * Construct a new keycode layout. + * + * @param name - The human readable name for the layout. + * + * @param codes - A mapping of keycode to key value. + * + * @param modifierKeys - Array of modifier key names + */ + constructor( + name: string, + codes: KeycodeLayout.CodeMap, + modifierKeys: string[] = [] + ) { + this.name = name; + this._codes = codes; + this._keys = KeycodeLayout.extractKeys(codes); + this._modifierKeys = KeycodeLayout.convertToKeySet(modifierKeys); + } + + /** + * The human readable name of the layout. + */ + readonly name: string; + + /** + * Get an array of the key values supported by the layout. + * + * @returns A new array of the supported key values. + */ + keys(): string[] { + return Object.keys(this._keys); + } + + /** + * Test whether the given key is a valid value for the layout. + * + * @param key - The user provided key to test for validity. + * + * @returns `true` if the key is valid, `false` otherwise. + */ + isValidKey(key: string): boolean { + return key in this._keys; + } + + /** + * Test whether the given key is a modifier key. + * + * @param key - The user provided key. + * + * @returns `true` if the key is a modifier key, `false` otherwise. + */ + isModifierKey(key: string): boolean { + return key in this._modifierKeys; + } + + /** + * Get the key for a `'keydown'` event. + * + * @param event - The event object for a `'keydown'` event. + * + * @returns The associated key value, or an empty string if + * the event does not represent a valid primary key. + */ + keyForKeydownEvent(event: KeyboardEvent): string { + return this._codes[event.keyCode] || ''; + } + + private _keys: KeycodeLayout.KeySet; + private _codes: KeycodeLayout.CodeMap; + private _modifierKeys: KeycodeLayout.KeySet; +} + +/** + * The namespace for the `KeycodeLayout` class statics. + */ +export namespace KeycodeLayout { + /** + * A type alias for a keycode map. + */ + export type CodeMap = { readonly [code: number]: string }; + + /** + * A type alias for a key set. + */ + export type KeySet = { readonly [key: string]: boolean }; + + /** + * Extract the set of keys from a code map. + * + * @param codes - The code map of interest. + * + * @returns A set of the keys in the code map. + */ + export function extractKeys(codes: CodeMap): KeySet { + let keys: any = Object.create(null); + for (let c in codes) { + keys[codes[c]] = true; + } + return keys as KeySet; + } + + /** + * Convert array of keys to a key set. + * + * @param keys - The array that needs to be converted + * + * @returns A set of the keys in the array. + */ + export function convertToKeySet(keys: string[]): KeySet { + let keySet = Object(null); + for (let i = 0, n = keys.length; i < n; ++i) { + keySet[keys[i]] = true; + } + return keySet; + } +} diff --git a/packages/keyboard/src/index.ts b/packages/keyboard/src/index.ts index 7676e77d1..7b0a0c4c7 100644 --- a/packages/keyboard/src/index.ts +++ b/packages/keyboard/src/index.ts @@ -12,63 +12,11 @@ * @module keyboard */ -/** - * An object which represents an abstract keyboard layout. - */ -export interface IKeyboardLayout { - /** - * The human readable name of the layout. - * - * This value is used primarily for display and debugging purposes. - */ - readonly name: string; - - /** - * Get an array of all key values supported by the layout. - * - * @returns A new array of the supported key values. - * - * #### Notes - * This can be useful for authoring tools and debugging, when it's - * necessary to know which keys are available for shortcut use. - */ - keys(): string[]; - - /** - * Test whether the given key is a valid value for the layout. - * - * @param key - The user provided key to test for validity. - * - * @returns `true` if the key is valid, `false` otherwise. - */ - isValidKey(key: string): boolean; - - /** - * Test whether the given key is a modifier key. - * - * @param key - The user provided key. - * - * @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. - */ - isModifierKey(key: string): boolean; +import { IKeyboardLayout } from './core'; +export { IKeyboardLayout, KeycodeLayout } from './core'; - /** - * Get the key for a `'keydown'` event. - * - * @param event - The event object for a `'keydown'` event. - * - * @returns The associated key value, or an empty string if the event - * does not represent a valid primary key. - */ - keyForKeydownEvent(event: KeyboardEvent): string; -} +import { EN_US } from './layouts'; +export * from './layouts'; /** * Get the global application keyboard layout instance. @@ -81,11 +29,10 @@ export interface IKeyboardLayout { export function getKeyboardLayout(): IKeyboardLayout { return Private.keyboardLayout; } - /** * Set the global application keyboard layout instance. * - * @param layout The keyboard layout for use by the application. + * @param - The keyboard layout for use by the application. * * #### Notes * The keyboard layout should typically be set on application startup @@ -95,263 +42,6 @@ export function setKeyboardLayout(layout: IKeyboardLayout): void { Private.keyboardLayout = layout; } -/** - * A concrete implementation of {@link IKeyboardLayout} based on keycodes. - * - * The `keyCode` property of a `'keydown'` event is a browser and OS - * specific representation of the physical key (not character) which - * was pressed on a keyboard. While not the most convenient API, it - * is currently the only one which works reliably on all browsers. - * - * This class accepts a user-defined mapping of keycode to key, which - * allows for reliable shortcuts tailored to the user's system. - */ -export class KeycodeLayout implements IKeyboardLayout { - /** - * Construct a new keycode layout. - * - * @param name - The human readable name for the layout. - * - * @param codes - A mapping of keycode to key value. - * - * @param modifierKeys - Array of modifier key names - */ - constructor( - name: string, - codes: KeycodeLayout.CodeMap, - modifierKeys: string[] = [] - ) { - this.name = name; - this._codes = codes; - this._keys = KeycodeLayout.extractKeys(codes); - this._modifierKeys = KeycodeLayout.convertToKeySet(modifierKeys); - } - - /** - * The human readable name of the layout. - */ - readonly name: string; - - /** - * Get an array of the key values supported by the layout. - * - * @returns A new array of the supported key values. - */ - keys(): string[] { - return Object.keys(this._keys); - } - - /** - * Test whether the given key is a valid value for the layout. - * - * @param key - The user provided key to test for validity. - * - * @returns `true` if the key is valid, `false` otherwise. - */ - isValidKey(key: string): boolean { - return key in this._keys; - } - - /** - * Test whether the given key is a modifier key. - * - * @param key - The user provided key. - * - * @returns `true` if the key is a modifier key, `false` otherwise. - */ - isModifierKey(key: string): boolean { - return key in this._modifierKeys; - } - - /** - * Get the key for a `'keydown'` event. - * - * @param event - The event object for a `'keydown'` event. - * - * @returns The associated key value, or an empty string if - * the event does not represent a valid primary key. - */ - keyForKeydownEvent(event: KeyboardEvent): string { - return this._codes[event.keyCode] || ''; - } - - private _keys: KeycodeLayout.KeySet; - private _codes: KeycodeLayout.CodeMap; - private _modifierKeys: KeycodeLayout.KeySet; -} - -/** - * The namespace for the `KeycodeLayout` class statics. - */ -export namespace KeycodeLayout { - /** - * A type alias for a keycode map. - */ - export type CodeMap = { readonly [code: number]: string }; - - /** - * A type alias for a key set. - */ - export type KeySet = { readonly [key: string]: boolean }; - - /** - * Extract the set of keys from a code map. - * - * @param codes - The code map of interest. - * - * @returns A set of the keys in the code map. - */ - export function extractKeys(codes: CodeMap): KeySet { - let keys: any = Object.create(null); - for (let c in codes) { - keys[codes[c]] = true; - } - return keys as KeySet; - } - - /** - * Convert array of keys to a key set. - * - * @param keys - The array that needs to be converted - * - * @returns A set of the keys in the array. - */ - export function convertToKeySet(keys: string[]): KeySet { - let keySet = Object(null); - for (let i = 0, n = keys.length; i < n; ++i) { - keySet[keys[i]] = true; - } - return keySet; - } -} - -/** - * A keycode-based keyboard layout for US English keyboards. - * - * This layout is valid for the following OS/Browser combinations. - * - * - Windows - * - Chrome - * - Firefox - * - IE - * - * - OSX - * - Chrome - * - Firefox - * - Safari - * - * - Linux - * - Chrome - * - Firefox - * - * Other combinations may also work, but are untested. - */ -export const EN_US: IKeyboardLayout = new KeycodeLayout( - 'en-us', - { - 8: 'Backspace', - 9: 'Tab', - 13: 'Enter', - 16: 'Shift', - 17: 'Ctrl', - 18: 'Alt', - 19: 'Pause', - 27: 'Escape', - 32: 'Space', - 33: 'PageUp', - 34: 'PageDown', - 35: 'End', - 36: 'Home', - 37: 'ArrowLeft', - 38: 'ArrowUp', - 39: 'ArrowRight', - 40: 'ArrowDown', - 45: 'Insert', - 46: 'Delete', - 48: '0', - 49: '1', - 50: '2', - 51: '3', - 52: '4', - 53: '5', - 54: '6', - 55: '7', - 56: '8', - 57: '9', - 59: ';', // firefox - 61: '=', // firefox - 65: 'A', - 66: 'B', - 67: 'C', - 68: 'D', - 69: 'E', - 70: 'F', - 71: 'G', - 72: 'H', - 73: 'I', - 74: 'J', - 75: 'K', - 76: 'L', - 77: 'M', - 78: 'N', - 79: 'O', - 80: 'P', - 81: 'Q', - 82: 'R', - 83: 'S', - 84: 'T', - 85: 'U', - 86: 'V', - 87: 'W', - 88: 'X', - 89: 'Y', - 90: 'Z', - 91: 'Meta', // non-firefox - 93: 'ContextMenu', - 96: '0', // numpad - 97: '1', // numpad - 98: '2', // numpad - 99: '3', // numpad - 100: '4', // numpad - 101: '5', // numpad - 102: '6', // numpad - 103: '7', // numpad - 104: '8', // numpad - 105: '9', // numpad - 106: '*', // numpad - 107: '+', // numpad - 109: '-', // numpad - 110: '.', // numpad - 111: '/', // numpad - 112: 'F1', - 113: 'F2', - 114: 'F3', - 115: 'F4', - 116: 'F5', - 117: 'F6', - 118: 'F7', - 119: 'F8', - 120: 'F9', - 121: 'F10', - 122: 'F11', - 123: 'F12', - 173: '-', // firefox - 186: ';', // non-firefox - 187: '=', // non-firefox - 188: ',', - 189: '-', // non-firefox - 190: '.', - 191: '/', - 192: '`', - 219: '[', - 220: '\\', - 221: ']', - 222: "'", - 224: 'Meta' // firefox - }, - ['Shift', 'Ctrl', 'Alt', 'Meta'] // modifier keys -); - /** * The namespace for the module implementation details. */ diff --git a/packages/keyboard/src/layouts/en-US.ts b/packages/keyboard/src/layouts/en-US.ts new file mode 100644 index 000000000..20cf49f52 --- /dev/null +++ b/packages/keyboard/src/layouts/en-US.ts @@ -0,0 +1,128 @@ +import { type IKeyboardLayout, KeycodeLayout } from '../core'; + +/** + * A keycode-based keyboard layout for US English keyboards. + * + * This layout is valid for the following OS/Browser combinations. + * + * - Windows + * - Chrome + * - Firefox + * - IE + * + * - OSX + * - Chrome + * - Firefox + * - Safari + * + * - Linux + * - Chrome + * - Firefox + * + * Other combinations may also work, but are untested. + */ +export const EN_US: IKeyboardLayout = new KeycodeLayout( + 'en-us', + { + 8: 'Backspace', + 9: 'Tab', + 13: 'Enter', + 16: 'Shift', + 17: 'Ctrl', + 18: 'Alt', + 19: 'Pause', + 27: 'Escape', + 32: 'Space', + 33: 'PageUp', + 34: 'PageDown', + 35: 'End', + 36: 'Home', + 37: 'ArrowLeft', + 38: 'ArrowUp', + 39: 'ArrowRight', + 40: 'ArrowDown', + 45: 'Insert', + 46: 'Delete', + 48: '0', + 49: '1', + 50: '2', + 51: '3', + 52: '4', + 53: '5', + 54: '6', + 55: '7', + 56: '8', + 57: '9', + 59: ';', // firefox + 61: '=', // firefox + 65: 'A', + 66: 'B', + 67: 'C', + 68: 'D', + 69: 'E', + 70: 'F', + 71: 'G', + 72: 'H', + 73: 'I', + 74: 'J', + 75: 'K', + 76: 'L', + 77: 'M', + 78: 'N', + 79: 'O', + 80: 'P', + 81: 'Q', + 82: 'R', + 83: 'S', + 84: 'T', + 85: 'U', + 86: 'V', + 87: 'W', + 88: 'X', + 89: 'Y', + 90: 'Z', + 91: 'Meta', // non-firefox + 93: 'ContextMenu', + 96: '0', // numpad + 97: '1', // numpad + 98: '2', // numpad + 99: '3', // numpad + 100: '4', // numpad + 101: '5', // numpad + 102: '6', // numpad + 103: '7', // numpad + 104: '8', // numpad + 105: '9', // numpad + 106: '*', // numpad + 107: '+', // numpad + 109: '-', // numpad + 110: '.', // numpad + 111: '/', // numpad + 112: 'F1', + 113: 'F2', + 114: 'F3', + 115: 'F4', + 116: 'F5', + 117: 'F6', + 118: 'F7', + 119: 'F8', + 120: 'F9', + 121: 'F10', + 122: 'F11', + 123: 'F12', + 173: '-', // firefox + 186: ';', // non-firefox + 187: '=', // non-firefox + 188: ',', + 189: '-', // non-firefox + 190: '.', + 191: '/', + 192: '`', + 219: '[', + 220: '\\', + 221: ']', + 222: "'", + 224: 'Meta' // firefox + }, + ['Shift', 'Ctrl', 'Alt', 'Meta'] // modifier keys +); diff --git a/packages/keyboard/src/layouts/index.ts b/packages/keyboard/src/layouts/index.ts new file mode 100644 index 000000000..4cfddb8e6 --- /dev/null +++ b/packages/keyboard/src/layouts/index.ts @@ -0,0 +1,4 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +export { EN_US } from './en-US'; From 81b62faf586ff7a88973c39cb44683a183729b80 Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Wed, 2 Feb 2022 23:54:21 +0000 Subject: [PATCH 02/15] Add capability for KeyboardEvent.code + capture This: - Adds the capability for KeycodeLayout to also use the `code` field from KeyboardEvents. - Adds a map for `code` fields for EN_US. - Adds a package with a widget for capturing keyboard layouts. - Adds an example app for capturing keyboard layouts. TODOs: - Add tests for capture widget / example. - Figure out a pattern for adding other layouts. Should they be added to keyboard package, or to one or more separate language packs? --- examples/example-keyboard-capture/index.html | 14 + .../example-keyboard-capture/package.json | 22 ++ .../rollup.config.mjs | 8 + .../example-keyboard-capture/src/index.ts | 134 +++++++ .../example-keyboard-capture/style/index.css | 54 +++ .../example-keyboard-capture/tsconfig.json | 17 + packages/keyboard-capture/api-extractor.json | 20 ++ packages/keyboard-capture/package.json | 67 ++++ packages/keyboard-capture/rollup.config.mjs | 13 + packages/keyboard-capture/src/capture.ts | 161 +++++++++ packages/keyboard-capture/src/index.ts | 11 + .../keyboard-capture/tests/rollup.config.mjs | 8 + .../keyboard-capture/tests/src/index.spec.ts | 145 ++++++++ packages/keyboard-capture/tests/tsconfig.json | 10 + packages/keyboard-capture/tsconfig.json | 21 ++ packages/keyboard-capture/typedoc.json | 6 + packages/keyboard/src/core.ts | 60 +++- packages/keyboard/src/index.ts | 2 +- packages/keyboard/src/layouts/en-US.ts | 121 ++++++- packages/keyboard/src/special-keys.ts | 331 ++++++++++++++++++ packages/keyboard/tests/src/index.spec.ts | 62 +++- packages/keyboard/tsconfig.json | 2 +- yarn.lock | 43 +++ 23 files changed, 1313 insertions(+), 19 deletions(-) create mode 100644 examples/example-keyboard-capture/index.html create mode 100644 examples/example-keyboard-capture/package.json create mode 100644 examples/example-keyboard-capture/rollup.config.mjs create mode 100644 examples/example-keyboard-capture/src/index.ts create mode 100644 examples/example-keyboard-capture/style/index.css create mode 100644 examples/example-keyboard-capture/tsconfig.json create mode 100644 packages/keyboard-capture/api-extractor.json create mode 100644 packages/keyboard-capture/package.json create mode 100644 packages/keyboard-capture/rollup.config.mjs create mode 100644 packages/keyboard-capture/src/capture.ts create mode 100644 packages/keyboard-capture/src/index.ts create mode 100644 packages/keyboard-capture/tests/rollup.config.mjs create mode 100644 packages/keyboard-capture/tests/src/index.spec.ts create mode 100644 packages/keyboard-capture/tests/tsconfig.json create mode 100644 packages/keyboard-capture/tsconfig.json create mode 100644 packages/keyboard-capture/typedoc.json create mode 100644 packages/keyboard/src/special-keys.ts diff --git a/examples/example-keyboard-capture/index.html b/examples/example-keyboard-capture/index.html new file mode 100644 index 000000000..0e7d44f2f --- /dev/null +++ b/examples/example-keyboard-capture/index.html @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/examples/example-keyboard-capture/package.json b/examples/example-keyboard-capture/package.json new file mode 100644 index 000000000..ba52b7958 --- /dev/null +++ b/examples/example-keyboard-capture/package.json @@ -0,0 +1,22 @@ +{ + "name": "@lumino/example-keyboard-capture", + "version": "2.0.0", + "private": true, + "scripts": { + "build": "tsc && rollup -c", + "clean": "rimraf build" + }, + "dependencies": { + "@lumino/keyboard-capture": "^2.0.0", + "@lumino/signaling": "^2.1.3", + "@lumino/widgets": "^2.6.0" + }, + "devDependencies": { + "@lumino/messaging": "^2.0.2", + "@rollup/plugin-node-resolve": "^15.0.1", + "rimraf": "^5.0.1", + "rollup": "^3.25.1", + "rollup-plugin-styles": "^4.0.0", + "typescript": "~5.1.3" + } +} diff --git a/examples/example-keyboard-capture/rollup.config.mjs b/examples/example-keyboard-capture/rollup.config.mjs new file mode 100644 index 000000000..1e910abb8 --- /dev/null +++ b/examples/example-keyboard-capture/rollup.config.mjs @@ -0,0 +1,8 @@ +/* + * Copyright (c) Jupyter Development Team. + * Distributed under the terms of the Modified BSD License. + */ + +import { createRollupExampleConfig } from '@lumino/buildutils'; +const rollupConfig = createRollupExampleConfig(); +export default rollupConfig; diff --git a/examples/example-keyboard-capture/src/index.ts b/examples/example-keyboard-capture/src/index.ts new file mode 100644 index 000000000..83b0dadd1 --- /dev/null +++ b/examples/example-keyboard-capture/src/index.ts @@ -0,0 +1,134 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. +/*----------------------------------------------------------------------------- +| Copyright (c) 2019, PhosphorJS Contributors +| +| Distributed under the terms of the BSD 3-Clause License. +| +| The full license is in the file LICENSE, distributed with this software. +|----------------------------------------------------------------------------*/ + +import { CaptureWidget } from '@lumino/keyboard-capture'; +import { Message } from '@lumino/messaging'; +import { ISignal, Signal } from '@lumino/signaling'; +import { Panel, Widget } from '@lumino/widgets'; + +import '../style/index.css'; + +export class OutputWidget extends Widget { + /** + * + */ + constructor(options?: Widget.IOptions) { + super(options); + this._output = document.createElement('div'); + this._exportButton = document.createElement('button'); + this._exportButton.innerText = 'Show'; + this._copyButton = document.createElement('button'); + this._copyButton.innerText = 'Copy'; + this._clearButton = document.createElement('button'); + this._clearButton.innerText = 'Clear'; + this.node.appendChild(this._exportButton); + this.node.appendChild(this._copyButton); + this.node.appendChild(this._clearButton); + this.node.appendChild(this._output); + this.addClass('lm-keyboardCaptureOutputArea'); + } + + set value(content: string) { + this._output.innerHTML = content; + } + + get action(): ISignal { + return this._action; + } + + /** + * Handle the DOM events for the widget. + * + * @param event - The DOM event sent to the element. + */ + handleEvent(event: Event): void { + switch (event.type) { + case 'click': + if (event.target === this._exportButton) { + event.stopPropagation(); + this._action.emit('display'); + } else if (event.target === this._copyButton) { + event.stopPropagation(); + this._action.emit('clipboard'); + } else if (event.target === this._clearButton) { + event.stopPropagation(); + this._action.emit('clear'); + } + break; + } + } + + /** + * A message handler invoked on a `'before-attach'` message. + */ + protected onBeforeAttach(msg: Message): void { + this._exportButton.addEventListener('click', this); + this._copyButton.addEventListener('click', this); + this._clearButton.addEventListener('click', this); + super.onBeforeAttach(msg); + } + + /** + * A message handler invoked on an `'after-detach'` message. + */ + protected onAfterDetach(msg: Message): void { + super.onAfterDetach(msg); + this._exportButton.removeEventListener('click', this); + this._copyButton.removeEventListener('click', this); + this._clearButton.removeEventListener('click', this); + } + + private _output: HTMLElement; + private _exportButton: HTMLButtonElement; + private _copyButton: HTMLButtonElement; + private _clearButton: HTMLButtonElement; + private _action = new Signal(this); +} + +/** + * Initialize the applicaiton. + */ +async function init(): Promise { + // Add the text editors to a dock panel. + let capture = new CaptureWidget(); + let output = new OutputWidget(); + + capture.node.textContent = + 'Focus me and hit each key on your keyboard without any modifiers'; + + // Add the dock panel to the document. + let box = new Panel(); + box.id = 'main'; + box.addWidget(capture); + box.addWidget(output); + + capture.dataAdded.connect((sender, entry) => { + output.value = `Added ${entry.type}: ${ + entry.code ? `${entry.code} →` : '' + } ${entry.key}`; + }); + output.action.connect((sender, action) => { + if (action === 'clipboard') { + navigator.clipboard.writeText(capture.formatMap()); + } else if (action === 'clear') { + capture.clear(); + output.value = ' '; + } else { + output.value = `
${capture.formatMap()}
`; + } + }); + + window.onresize = () => { + box.update(); + }; + Widget.attach(box, document.body); +} + +window.onload = init; diff --git a/examples/example-keyboard-capture/style/index.css b/examples/example-keyboard-capture/style/index.css new file mode 100644 index 000000000..bbe0d6847 --- /dev/null +++ b/examples/example-keyboard-capture/style/index.css @@ -0,0 +1,54 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) Jupyter Development Team. +| Copyright (c) 2014-2017, PhosphorJS Contributors +| +| Distributed under the terms of the BSD 3-Clause License. +| +| The full license is in the file LICENSE, distributed with this software. +|----------------------------------------------------------------------------*/ +@import '~@lumino/widgets/style/index.css'; + +body { + display: flex; + flex-direction: column; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin: 0; + padding: 0; +} + +#main { + flex: 1 1 auto; + overflow: auto; + padding: 10px; +} + +.lm-keyboardCaptureArea { + border-radius: 5px; + border: 3px dashed #88a; + padding: 6px; + margin: 6px; +} + +.lm-keyboardCaptureOutputArea kbd { + background-color: #eee; + border-radius: 5px; + border: 3px solid #b4b4b4; + box-shadow: + 0 1px 1px rgba(0, 0, 0, 0.2), + 0 2px 0 0 rgba(255, 255, 255, 0.7) inset; + color: #333; + display: inline-block; + font-size: 0.85em; + font-weight: 700; + line-height: 1; + padding: 2px 4px; + white-space: nowrap; +} + +.lm-keyboardCaptureOutputArea button { + margin: 4px; +} diff --git a/examples/example-keyboard-capture/tsconfig.json b/examples/example-keyboard-capture/tsconfig.json new file mode 100644 index 000000000..b14ef2b28 --- /dev/null +++ b/examples/example-keyboard-capture/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "declaration": false, + "noImplicitAny": true, + "noEmitOnError": true, + "noUnusedLocals": true, + "strictNullChecks": true, + "sourceMap": true, + "module": "ES6", + "moduleResolution": "node", + "target": "ES2018", + "outDir": "./build", + "lib": ["DOM", "ES2018"], + "types": [] + }, + "include": ["src/*"] +} diff --git a/packages/keyboard-capture/api-extractor.json b/packages/keyboard-capture/api-extractor.json new file mode 100644 index 000000000..052dd8d2f --- /dev/null +++ b/packages/keyboard-capture/api-extractor.json @@ -0,0 +1,20 @@ +/** + * Config file for API Extractor. For more info, please visit: https://api-extractor.com + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + + /** + * Optionally specifies another JSON config file that this file extends from. This provides a way for + * standard settings to be shared across multiple projects. + * + * If the path starts with "./" or "../", the path is resolved relative to the folder of the file that contains + * the "extends" field. Otherwise, the first path segment is interpreted as an NPM package name, and will be + * resolved using NodeJS require(). + * + * SUPPORTED TOKENS: none + * DEFAULT VALUE: "" + */ + "extends": "../../api-extractor-base.json" + // "extends": "my-package/include/api-extractor-base.json" +} diff --git a/packages/keyboard-capture/package.json b/packages/keyboard-capture/package.json new file mode 100644 index 000000000..0159077be --- /dev/null +++ b/packages/keyboard-capture/package.json @@ -0,0 +1,67 @@ +{ + "name": "@lumino/keyboard-capture", + "version": "2.0.0", + "description": "Lumino Keyboard Capture widget", + "homepage": "https://github.com/jupyterlab/lumino", + "bugs": { + "url": "https://github.com/jupyterlab/lumino/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/jupyterlab/lumino.git" + }, + "license": "BSD-3-Clause", + "author": "Project Jupyter", + "main": "dist/index.js", + "jsdelivr": "dist/index.min.js", + "unpkg": "dist/index.min.js", + "module": "dist/index.es6", + "types": "types/index.d.ts", + "files": [ + "dist/*", + "src/*", + "types/*" + ], + "scripts": { + "api": "api-extractor run --local --verbose", + "build": "yarn build:src && rollup -c", + "build:src": "tsc --build", + "build:test": "yarn clean:test && tsc --build tests && cd tests && rollup -c", + "clean": "rimraf ./lib *.tsbuildinfo ./types ./dist", + "clean:test": "rimraf tests/lib tests/tsconfig.tsbuildinfo", + "minimize": "terser dist/index.js -c -m --source-map \"content='dist/index.js.map',url='index.min.js.map'\" -o dist/index.min.js", + "test": "yarn build:test && web-test-runner tests/lib/bundle.test.js --node-resolve --playwright", + "test:chromium": "yarn test -- --browsers chromium", + "test:debug": "yarn test -- --manual --open", + "test:firefox": "yarn test -- --browsers firefox", + "test:webkit": "yarn test -- --browsers webkit", + "watch": "tsc --build --watch" + }, + "dependencies": { + "@lumino/keyboard": "^2.0.2", + "@lumino/signaling": "^2.1.3", + "@lumino/widgets": "^2.6.0" + }, + "devDependencies": { + "@lumino/messaging": "^2.0.2", + "@microsoft/api-extractor": "^7.36.0", + "@rollup/plugin-commonjs": "^24.0.0", + "@rollup/plugin-node-resolve": "^15.0.1", + "@types/chai": "^3.4.35", + "@types/mocha": "^2.2.39", + "@web/test-runner": "^0.18.2", + "@web/test-runner-playwright": "^0.11.0", + "chai": "^4.3.4", + "mocha": "^9.0.3", + "rimraf": "^5.0.1", + "rollup": "^3.25.1", + "rollup-plugin-postcss": "^4.0.2", + "rollup-plugin-sourcemaps": "^0.6.3", + "terser": "^5.18.1", + "tslib": "^2.3.0", + "typescript": "~5.1.3" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/keyboard-capture/rollup.config.mjs b/packages/keyboard-capture/rollup.config.mjs new file mode 100644 index 000000000..b5224b39b --- /dev/null +++ b/packages/keyboard-capture/rollup.config.mjs @@ -0,0 +1,13 @@ +/* + * Copyright (c) Jupyter Development Team. + * Distributed under the terms of the Modified BSD License. + */ + +import { createRollupConfig } from '@lumino/buildutils'; +import * as fs from 'fs'; + +const config = createRollupConfig({ + pkg: JSON.parse(fs.readFileSync('./package.json', 'utf-8')) +}); + +export default config; diff --git a/packages/keyboard-capture/src/capture.ts b/packages/keyboard-capture/src/capture.ts new file mode 100644 index 000000000..c00fbbca4 --- /dev/null +++ b/packages/keyboard-capture/src/capture.ts @@ -0,0 +1,161 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. +/*----------------------------------------------------------------------------- +| Copyright (c) 2019, PhosphorJS Contributors +| +| Distributed under the terms of the BSD 3-Clause License. +| +| The full license is in the file LICENSE, distributed with this software. +|----------------------------------------------------------------------------*/ + +import { KeycodeLayout } from '@lumino/keyboard'; +import { Message } from '@lumino/messaging'; +import { ISignal, Signal } from '@lumino/signaling'; +import { Widget } from '@lumino/widgets'; + +/** + * A widget for capturing a keyboard layout. + */ +export class CaptureWidget extends Widget { + /** + * + */ + constructor(options?: Widget.IOptions) { + super(options); + this.addClass('lm-keyboardCaptureArea'); + if (!options || !options.node) { + this.node.tabIndex = 0; + } + } + + extractLayout(name: string): KeycodeLayout { + return new KeycodeLayout( + name, + this._keyCodeMap, + Array.from(this._modifierKeys), + this._codeMap + ); + } + + formatMap(): string { + return `codes: ${Private.formatCodeMap( + this._codeMap + )}\n\nmodifiers: [${Array.from(this._modifierKeys) + .map(k => `"${k}"`) + .sort() + .join(', ')}]${ + Private.isCodeMapEmpty(this._keyCodeMap) + ? '' + : `\n\nkeyCodes${Private.formatCodeMap(this._keyCodeMap)}` + }`; + } + + clear(): void { + this._codeMap = {}; + this._keyCodeMap = {}; + this._modifierKeys.clear(); + } + + node: HTMLInputElement; + + get dataAdded(): ISignal { + return this._dataAdded; + } + + /** + * Handle the DOM events for the widget. + * + * @param event - The DOM event sent to the element. + */ + handleEvent(event: Event): void { + switch (event.type) { + case 'keydown': + this._onKeyDown(event as KeyboardEvent); + break; + case 'keyup': + this._onKeyUp(event as KeyboardEvent); + break; + } + } + + /** + * A message handler invoked on a `'before-attach'` message. + */ + protected onBeforeAttach(msg: Message): void { + this.node.addEventListener('keydown', this); + this.node.addEventListener('keyup', this); + super.onBeforeAttach(msg); + } + + /** + * A message handler invoked on an `'after-detach'` message. + */ + protected onAfterDetach(msg: Message): void { + super.onAfterDetach(msg); + this.node.removeEventListener('keydown', this); + this.node.removeEventListener('keyup', this); + } + + private _onKeyDown(event: KeyboardEvent): void { + event.stopPropagation(); + event.preventDefault(); + if (event.getModifierState(event.key)) { + this._modifierKeys.add(event.key); + this._dataAdded.emit({ key: event.key, type: 'modifier' }); + } + } + + private _onKeyUp(event: KeyboardEvent): void { + event.stopPropagation(); + event.preventDefault(); + if (event.getModifierState(event.key)) { + this._modifierKeys.add(event.key); + this._dataAdded.emit({ key: event.key, type: 'modifier' }); + return; + } + let { key, code } = event; + if (key === 'Dead') { + console.log('Dead key', event); + return; + } + if ((!code || code === 'Unidentified') && event.keyCode) { + console.log('Unidentified code', event); + this._keyCodeMap[event.keyCode] = key; + this._dataAdded.emit({ key, code: event.keyCode, type: 'keyCode' }); + } else { + this._codeMap[code] = key; + this._dataAdded.emit({ key, code, type: 'code' }); + } + } + + private _codeMap: { [key: string]: string } = {}; + private _keyCodeMap: { [key: number]: string } = {}; + private _modifierKeys: Set = new Set(); + private _dataAdded = new Signal(this); +} + +namespace CaptureWidget { + export type Entry = { type: string; code?: string | number; key: string }; +} + +namespace Private { + export function isCodeMapEmpty( + codemap: { [key: string]: string } | { [key: number]: string } + ): boolean { + return !Object.keys(codemap).length; + } + export function formatCodeMap( + codemap: { [key: string]: string } | { [key: number]: string } + ): string { + return `{\n${Object.keys(codemap) + .sort() + .map( + k => + ` "${k}": "${ + (codemap as any)[k] && + (codemap as any)[k][0].toUpperCase() + (codemap as any)[k].slice(1) + }"` + ) + .join(',\n')}\n}`; + } +} diff --git a/packages/keyboard-capture/src/index.ts b/packages/keyboard-capture/src/index.ts new file mode 100644 index 000000000..26315feec --- /dev/null +++ b/packages/keyboard-capture/src/index.ts @@ -0,0 +1,11 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. +/*----------------------------------------------------------------------------- +| Copyright (c) 2014-2017, PhosphorJS Contributors +| +| Distributed under the terms of the BSD 3-Clause License. +| +| The full license is in the file LICENSE, distributed with this software. +|----------------------------------------------------------------------------*/ + +export { CaptureWidget } from './capture'; diff --git a/packages/keyboard-capture/tests/rollup.config.mjs b/packages/keyboard-capture/tests/rollup.config.mjs new file mode 100644 index 000000000..10d911526 --- /dev/null +++ b/packages/keyboard-capture/tests/rollup.config.mjs @@ -0,0 +1,8 @@ +/* + * Copyright (c) Jupyter Development Team. + * Distributed under the terms of the Modified BSD License. + */ + +import { createRollupTestConfig } from '@lumino/buildutils'; +const rollupConfig = createRollupTestConfig(); +export default rollupConfig; diff --git a/packages/keyboard-capture/tests/src/index.spec.ts b/packages/keyboard-capture/tests/src/index.spec.ts new file mode 100644 index 000000000..ca2f97f7c --- /dev/null +++ b/packages/keyboard-capture/tests/src/index.spec.ts @@ -0,0 +1,145 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. +/*----------------------------------------------------------------------------- +| Copyright (c) 2014-2017, PhosphorJS Contributors +| +| Distributed under the terms of the BSD 3-Clause License. +| +| The full license is in the file LICENSE, distributed with this software. +|----------------------------------------------------------------------------*/ +import { expect } from 'chai'; + +import { + EN_US, + getKeyboardLayout, + KeycodeLayout, + setKeyboardLayout +} from '@lumino/keyboard'; + +describe('@lumino/keyboard', () => { + describe('getKeyboardLayout()', () => { + it('should return the global keyboard layout', () => { + expect(getKeyboardLayout()).to.equal(EN_US); + }); + }); + + describe('setKeyboardLayout()', () => { + it('should set the global keyboard layout', () => { + let layout = new KeycodeLayout('ab-cd', {}); + setKeyboardLayout(layout); + expect(getKeyboardLayout()).to.equal(layout); + setKeyboardLayout(EN_US); + expect(getKeyboardLayout()).to.equal(EN_US); + }); + }); + + describe('KeycodeLayout', () => { + describe('#constructor()', () => { + it('should construct a new keycode layout', () => { + let layout = new KeycodeLayout('ab-cd', {}); + expect(layout).to.be.an.instanceof(KeycodeLayout); + }); + }); + + describe('#name', () => { + it('should be a human readable name of the layout', () => { + let layout = new KeycodeLayout('ab-cd', {}); + expect(layout.name).to.equal('ab-cd'); + }); + }); + + describe('#keys()', () => { + it('should get an array of all key values supported by the layout', () => { + let layout = new KeycodeLayout('ab-cd', { 100: 'F' }); + let keys = layout.keys(); + expect(keys.length).to.equal(1); + expect(keys[0]).to.equal('F'); + }); + }); + + describe('#isValidKey()', () => { + it('should test whether the key is valid for the layout', () => { + let layout = new KeycodeLayout('foo', { 100: 'F' }); + expect(layout.isValidKey('F')).to.equal(true); + expect(layout.isValidKey('A')).to.equal(false); + }); + + it('should treat modifier keys as valid', () => { + let layout = new KeycodeLayout('foo', { 100: 'F', 101: 'A' }, ['A']); + expect(layout.isValidKey('A')).to.equal(true); + }); + }); + + describe('#isModifierKey()', () => { + it('should test whether the key is modifier for the layout', () => { + let layout = new KeycodeLayout('foo', { 100: 'F', 101: 'A' }, ['A']); + expect(layout.isModifierKey('F')).to.equal(false); + expect(layout.isModifierKey('A')).to.equal(true); + }); + + it('should return false for keys that are not in the layout', () => { + let layout = new KeycodeLayout('foo', { 100: 'F', 101: 'A' }, ['A']); + expect(layout.isModifierKey('B')).to.equal(false); + }); + }); + + describe('#keyForKeydownEvent()', () => { + it('should get the key for a `keydown` event', () => { + let layout = new KeycodeLayout('foo', { 100: 'F' }); + let event = new KeyboardEvent('keydown', { keyCode: 100 }); + let key = layout.keyForKeydownEvent(event as KeyboardEvent); + expect(key).to.equal('F'); + }); + + it('should return an empty string if the code is not valid', () => { + let layout = new KeycodeLayout('foo', { 100: 'F' }); + let event = new KeyboardEvent('keydown', { keyCode: 101 }); + let key = layout.keyForKeydownEvent(event as KeyboardEvent); + expect(key).to.equal(''); + }); + }); + + describe('.extractKeys()', () => { + it('should extract the keys from a code map', () => { + let keys: KeycodeLayout.CodeMap = { 70: 'F', 71: 'G', 72: 'H' }; + let goal: KeycodeLayout.KeySet = { F: true, G: true, H: true }; + expect(KeycodeLayout.extractKeys(keys)).to.deep.equal(goal); + }); + }); + + describe('.convertToKeySet()', () => { + it('should convert key array to key set', () => { + let keys: string[] = ['F', 'G', 'H']; + let goal: KeycodeLayout.KeySet = { F: true, G: true, H: true }; + expect(KeycodeLayout.convertToKeySet(keys)).to.deep.equal(goal); + }); + }); + }); + + describe('EN_US', () => { + it('should be a keycode layout', () => { + expect(EN_US).to.be.an.instanceof(KeycodeLayout); + }); + + it('should have standardized keys', () => { + expect(EN_US.isValidKey('A')).to.equal(true); + expect(EN_US.isValidKey('Z')).to.equal(true); + expect(EN_US.isValidKey('0')).to.equal(true); + expect(EN_US.isValidKey('a')).to.equal(false); + }); + + it('should have modifier keys', () => { + expect(EN_US.isValidKey('Shift')).to.equal(true); + expect(EN_US.isValidKey('Ctrl')).to.equal(true); + expect(EN_US.isValidKey('Alt')).to.equal(true); + expect(EN_US.isValidKey('Meta')).to.equal(true); + }); + + it('should correctly detect modifier keys', () => { + expect(EN_US.isModifierKey('Shift')).to.equal(true); + expect(EN_US.isModifierKey('Ctrl')).to.equal(true); + expect(EN_US.isModifierKey('Alt')).to.equal(true); + expect(EN_US.isModifierKey('Meta')).to.equal(true); + }); + }); +}); diff --git a/packages/keyboard-capture/tests/tsconfig.json b/packages/keyboard-capture/tests/tsconfig.json new file mode 100644 index 000000000..d2de12617 --- /dev/null +++ b/packages/keyboard-capture/tests/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfigbase", + "compilerOptions": { + "lib": ["DOM", "ES2018"], + "outDir": "lib", + "rootDir": "src", + "types": ["chai", "mocha"] + }, + "include": ["src/*"] +} diff --git a/packages/keyboard-capture/tsconfig.json b/packages/keyboard-capture/tsconfig.json new file mode 100644 index 000000000..e91bdd250 --- /dev/null +++ b/packages/keyboard-capture/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../tsconfigbase", + "compilerOptions": { + "declarationDir": "types", + "lib": ["DOM", "ES2018"], + "outDir": "lib", + "rootDir": "src" + }, + "include": ["src/*"], + "references": [ + { + "path": "../keyboard" + }, + { + "path": "../signaling" + }, + { + "path": "../widgets" + } + ] +} diff --git a/packages/keyboard-capture/typedoc.json b/packages/keyboard-capture/typedoc.json new file mode 100644 index 000000000..3b1a7b153 --- /dev/null +++ b/packages/keyboard-capture/typedoc.json @@ -0,0 +1,6 @@ +{ + "extends": ["../../typedoc.base.json"], + "entryPoints": ["src/index.ts"], + "name": "keyboard-capture", + "readme": "none" +} diff --git a/packages/keyboard/src/core.ts b/packages/keyboard/src/core.ts index d315abf50..fb5dc35dd 100644 --- a/packages/keyboard/src/core.ts +++ b/packages/keyboard/src/core.ts @@ -8,6 +8,8 @@ | The full license is in the file LICENSE, distributed with this software. |----------------------------------------------------------------------------*/ +import { SPECIAL_KEYS } from './special-keys'; + /** * An object which represents an abstract keyboard layout. */ @@ -89,12 +91,14 @@ export class KeycodeLayout implements IKeyboardLayout { */ constructor( name: string, - codes: KeycodeLayout.CodeMap, - modifierKeys: string[] = [] + keyCodes: KeycodeLayout.CodeMap, + modifierKeys: string[] = [], + codes: KeycodeLayout.ModernCodeMap = {} ) { this.name = name; + this._keyCodes = keyCodes; this._codes = codes; - this._keys = KeycodeLayout.extractKeys(codes); + this._keys = KeycodeLayout.extractKeys(keyCodes, codes); this._modifierKeys = KeycodeLayout.convertToKeySet(modifierKeys); } @@ -120,7 +124,7 @@ export class KeycodeLayout implements IKeyboardLayout { * @returns `true` if the key is valid, `false` otherwise. */ isValidKey(key: string): boolean { - return key in this._keys; + return key in this._keys || Private.isSpecialCharacter(key); } /** @@ -143,11 +147,22 @@ export class KeycodeLayout implements IKeyboardLayout { * the event does not represent a valid primary key. */ keyForKeydownEvent(event: KeyboardEvent): string { - return this._codes[event.keyCode] || ''; + if ( + event.code !== '' && + event.code !== 'Unidentified' && + event.code in this._codes + ) { + return this._codes[event.code]; + } + return ( + this._keyCodes[event.keyCode] || + (Private.isSpecialCharacter(event.key) ? event.key : '') + ); } private _keys: KeycodeLayout.KeySet; - private _codes: KeycodeLayout.CodeMap; + private _keyCodes: KeycodeLayout.CodeMap; + private _codes: KeycodeLayout.ModernCodeMap; private _modifierKeys: KeycodeLayout.KeySet; } @@ -158,7 +173,12 @@ export namespace KeycodeLayout { /** * A type alias for a keycode map. */ - export type CodeMap = { readonly [code: number]: string }; + export type CodeMap = { readonly [keyCode: number]: string }; + + /** + * A type alias for a code map. + */ + export type ModernCodeMap = { readonly [code: string]: string }; /** * A type alias for a key set. @@ -168,12 +188,18 @@ export namespace KeycodeLayout { /** * Extract the set of keys from a code map. * - * @param codes - The code map of interest. + * @param code - The code map of interest. * * @returns A set of the keys in the code map. */ - export function extractKeys(codes: CodeMap): KeySet { + export function extractKeys( + keyCodes: CodeMap, + codes: ModernCodeMap = {} + ): KeySet { let keys: any = Object.create(null); + for (let c in keyCodes) { + keys[keyCodes[c]] = true; + } for (let c in codes) { keys[codes[c]] = true; } @@ -195,3 +221,19 @@ export namespace KeycodeLayout { return keySet; } } + +/** + * The namespace for the module implementation details. + */ +namespace Private { + /** + * Whether the key value can be considered a special character. + * + * @param key - The key value that is to be considered + */ + export function isSpecialCharacter(key: string): boolean { + // If the value starts with an uppercase latin character and is followed by one + // or more alphanumeric basic latin characters, it is likely a special key. + return SPECIAL_KEYS.has(key); + } +} diff --git a/packages/keyboard/src/index.ts b/packages/keyboard/src/index.ts index 7b0a0c4c7..f833b57e3 100644 --- a/packages/keyboard/src/index.ts +++ b/packages/keyboard/src/index.ts @@ -32,7 +32,7 @@ export function getKeyboardLayout(): IKeyboardLayout { /** * Set the global application keyboard layout instance. * - * @param - The keyboard layout for use by the application. + * @param layout The keyboard layout for use by the application. * * #### Notes * The keyboard layout should typically be set on application startup diff --git a/packages/keyboard/src/layouts/en-US.ts b/packages/keyboard/src/layouts/en-US.ts index 20cf49f52..e3e5e0436 100644 --- a/packages/keyboard/src/layouts/en-US.ts +++ b/packages/keyboard/src/layouts/en-US.ts @@ -1,5 +1,17 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. +/*----------------------------------------------------------------------------- +| Copyright (c) 2014-2017, PhosphorJS Contributors +| +| Distributed under the terms of the BSD 3-Clause License. +| +| The full license is in the file LICENSE, distributed with this software. +|----------------------------------------------------------------------------*/ + import { type IKeyboardLayout, KeycodeLayout } from '../core'; +import { MODIFIER_KEYS } from '../special-keys'; + /** * A keycode-based keyboard layout for US English keyboards. * @@ -28,7 +40,7 @@ export const EN_US: IKeyboardLayout = new KeycodeLayout( 9: 'Tab', 13: 'Enter', 16: 'Shift', - 17: 'Ctrl', + 17: 'Control', 18: 'Alt', 19: 'Pause', 27: 'Escape', @@ -124,5 +136,110 @@ export const EN_US: IKeyboardLayout = new KeycodeLayout( 222: "'", 224: 'Meta' // firefox }, - ['Shift', 'Ctrl', 'Alt', 'Meta'] // modifier keys + // TODO: Figure out Ctrl vs Control + [...MODIFIER_KEYS, 'Ctrl'], + { + AltLeft: 'Alt', + AltRight: 'Alt', + ArrowDown: 'ArrowDown', + ArrowLeft: 'ArrowLeft', + ArrowRight: 'ArrowRight', + ArrowUp: 'ArrowUp', + Backquote: '`', + Backslash: '\\', + Backspace: 'Backspace', + BracketLeft: '[', + BracketRight: ']', + CapsLock: 'CapsLock', + Comma: ',', + ControlLeft: 'Control', + ControlRight: 'Control', + Delete: 'Delete', + Digit0: '0', + Digit1: '1', + Digit2: '2', + Digit3: '3', + Digit4: '4', + Digit5: '5', + Digit6: '6', + Digit7: '7', + Digit8: '8', + Digit9: '9', + End: 'End', + Equal: '=', + Escape: 'Escape', + F1: 'F1', + F10: 'F10', + F11: 'F11', + F12: 'F12', + F2: 'F2', + F3: 'F3', + F4: 'F4', + F5: 'F5', + F6: 'F6', + F7: 'F7', + F8: 'F8', + F9: 'F9', + Home: 'Home', + Insert: 'Insert', + KeyA: 'A', + KeyB: 'B', + KeyC: 'C', + KeyD: 'D', + KeyE: 'E', + KeyF: 'F', + KeyG: 'G', + KeyH: 'H', + KeyI: 'I', + KeyJ: 'J', + KeyK: 'K', + KeyL: 'L', + KeyM: 'M', + KeyN: 'N', + KeyO: 'O', + KeyP: 'P', + KeyQ: 'Q', + KeyR: 'R', + KeyS: 'S', + KeyT: 'T', + KeyU: 'U', + KeyV: 'V', + KeyW: 'W', + KeyX: 'X', + KeyY: 'Y', + KeyZ: 'Z', + MetaLeft: 'Meta', + MetaRight: 'Meta', + Minus: '-', + NumLock: 'NumLock', + Numpad0: 'Insert', + Numpad1: 'End', + Numpad2: 'ArrowDown', + Numpad3: 'PageDown', + Numpad4: 'ArrowLeft', + Numpad5: 'Clear', + Numpad6: 'ArrowRight', + Numpad7: 'Home', + Numpad8: 'ArrowUp', + Numpad9: 'PageUp', + NumpadAdd: '+', + NumpadDecimal: 'Delete', + NumpadDivide: '/', + NumpadEnter: 'Enter', + NumpadMultiply: '*', + NumpadSubtract: '-', + OSLeft: 'OS', // firefox + OSRight: 'OS', // firefox + PageDown: 'PageDown', + PageUp: 'PageUp', + Pause: 'Pause', + Period: '.', + PrintScreen: 'PrintScreen', + Quote: "'", + Semicolon: ';', + ShiftLeft: 'Shift', + ShiftRight: 'Shift', + Slash: '/', + Tab: 'Tab' + } ); diff --git a/packages/keyboard/src/special-keys.ts b/packages/keyboard/src/special-keys.ts new file mode 100644 index 000000000..8f02f0922 --- /dev/null +++ b/packages/keyboard/src/special-keys.ts @@ -0,0 +1,331 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +export const MODIFIER_KEYS = [ + 'Alt', + 'AltGraph', + 'CapsLock', + 'Control', + 'Fn', + 'FnLock', + 'Meta', + 'NumLock', + 'ScrollLock', + 'Shift', + 'Symbol', + 'SymbolLock' +]; + +/** + * The list of predefined special characters according to W3C. + * + * This list does not include "Unidentified" or "Dead". + * + * Ref. https://www.w3.org/TR/uievents-key/#named-key-attribute-values + */ +export const SPECIAL_KEYS = new Set([ + 'Alt', + 'AltGraph', + 'CapsLock', + 'Control', + 'Fn', + 'FnLock', + 'Meta', + 'NumLock', + 'ScrollLock', + 'Shift', + 'Symbol', + 'SymbolLock', + 'Hyper', + 'Super', + 'Enter', + 'Tab', + 'ArrowDown', + 'ArrowLeft', + 'ArrowRight', + 'ArrowUp', + 'End', + 'Home', + 'PageDown', + 'PageUp', + 'Backspace', + 'Clear', + 'Copy', + 'CrSel', + 'Cut', + 'Delete', + 'EraseEof', + 'ExSel', + 'Insert', + 'Paste', + 'Redo', + 'Undo', + 'Accept', + 'Again', + 'Attn', + 'Cancel', + 'ContextMenu', + 'Escape', + 'Execute', + 'Find', + 'Help', + 'Pause', + 'Play', + 'Props', + 'Select', + 'ZoomIn', + 'ZoomOut', + 'BrightnessDown', + 'BrightnessUp', + 'Eject', + 'LogOff', + 'Power', + 'PowerOff', + 'PrintScreen', + 'Hibernate', + 'Standby', + 'WakeUp', + 'AllCandidates', + 'Alphanumeric', + 'CodeInput', + 'Compose', + 'Convert', + 'FinalMode', + 'GroupFirst', + 'GroupLast', + 'GroupNext', + 'GroupPrevious', + 'ModeChange', + 'NextCandidate', + 'NonConvert', + 'PreviousCandidate', + 'Process', + 'SingleCandidate', + 'HangulMode', + 'HanjaMode', + 'JunjaMode', + 'Eisu', + 'Hankaku', + 'Hiragana', + 'HiraganaKatakana', + 'KanaMode', + 'KanjiMode', + 'Katakana', + 'Romaji', + 'Zenkaku', + 'ZenkakuHankaku', + 'F1', + 'F2', + 'F3', + 'F4', + 'F5', + 'F6', + 'F7', + 'F8', + 'F9', + 'F10', + 'F11', + 'F12', + 'Soft1', + 'Soft2', + 'Soft3', + 'Soft4', + 'ChannelDown', + 'ChannelUp', + 'Close', + 'MailForward', + 'MailReply', + 'MailSend', + 'MediaClose', + 'MediaFastForward', + 'MediaPause', + 'MediaPlay', + 'MediaPlayPause', + 'MediaRecord', + 'MediaRewind', + 'MediaStop', + 'MediaTrackNext', + 'MediaTrackPrevious', + 'New', + 'Open', + 'Print', + 'Save', + 'SpellCheck', + 'Key11', + 'Key12', + 'AudioBalanceLeft', + 'AudioBalanceRight', + 'AudioBassBoostDown', + 'AudioBassBoostToggle', + 'AudioBassBoostUp', + 'AudioFaderFront', + 'AudioFaderRear', + 'AudioSurroundModeNext', + 'AudioTrebleDown', + 'AudioTrebleUp', + 'AudioVolumeDown', + 'AudioVolumeUp', + 'AudioVolumeMute', + 'MicrophoneToggle', + 'MicrophoneVolumeDown', + 'MicrophoneVolumeUp', + 'MicrophoneVolumeMute', + 'SpeechCorrectionList', + 'SpeechInputToggle', + 'LaunchApplication1', + 'LaunchApplication2', + 'LaunchCalendar', + 'LaunchContacts', + 'LaunchMail', + 'LaunchMediaPlayer', + 'LaunchMusicPlayer', + 'LaunchPhone', + 'LaunchScreenSaver', + 'LaunchSpreadsheet', + 'LaunchWebBrowser', + 'LaunchWebCam', + 'LaunchWordProcessor', + 'BrowserBack', + 'BrowserFavorites', + 'BrowserForward', + 'BrowserHome', + 'BrowserRefresh', + 'BrowserSearch', + 'BrowserStop', + 'AppSwitch', + 'Call', + 'Camera', + 'CameraFocus', + 'EndCall', + 'GoBack', + 'GoHome', + 'HeadsetHook', + 'LastNumberRedial', + 'Notification', + 'MannerMode', + 'VoiceDial', + 'TV', + 'TV3DMode', + 'TVAntennaCable', + 'TVAudioDescription', + 'TVAudioDescriptionMixDown', + 'TVAudioDescriptionMixUp', + 'TVContentsMenu', + 'TVDataService', + 'TVInput', + 'TVInputComponent1', + 'TVInputComponent2', + 'TVInputComposite1', + 'TVInputComposite2', + 'TVInputHDMI1', + 'TVInputHDMI2', + 'TVInputHDMI3', + 'TVInputHDMI4', + 'TVInputVGA1', + 'TVMediaContext', + 'TVNetwork', + 'TVNumberEntry', + 'TVPower', + 'TVRadioService', + 'TVSatellite', + 'TVSatelliteBS', + 'TVSatelliteCS', + 'TVSatelliteToggle', + 'TVTerrestrialAnalog', + 'TVTerrestrialDigital', + 'TVTimer', + 'AVRInput', + 'AVRPower', + 'ColorF0Red', + 'ColorF1Green', + 'ColorF2Yellow', + 'ColorF3Blue', + 'ColorF4Grey', + 'ColorF5Brown', + 'ClosedCaptionToggle', + 'Dimmer', + 'DisplaySwap', + 'DVR', + 'Exit', + 'FavoriteClear0', + 'FavoriteClear1', + 'FavoriteClear2', + 'FavoriteClear3', + 'FavoriteRecall0', + 'FavoriteRecall1', + 'FavoriteRecall2', + 'FavoriteRecall3', + 'FavoriteStore0', + 'FavoriteStore1', + 'FavoriteStore2', + 'FavoriteStore3', + 'Guide', + 'GuideNextDay', + 'GuidePreviousDay', + 'Info', + 'InstantReplay', + 'Link', + 'ListProgram', + 'LiveContent', + 'Lock', + 'MediaApps', + 'MediaAudioTrack', + 'MediaLast', + 'MediaSkipBackward', + 'MediaSkipForward', + 'MediaStepBackward', + 'MediaStepForward', + 'MediaTopMenu', + 'NavigateIn', + 'NavigateNext', + 'NavigateOut', + 'NavigatePrevious', + 'NextFavoriteChannel', + 'NextUserProfile', + 'OnDemand', + 'Pairing', + 'PinPDown', + 'PinPMove', + 'PinPToggle', + 'PinPUp', + 'PlaySpeedDown', + 'PlaySpeedReset', + 'PlaySpeedUp', + 'RandomToggle', + 'RcLowBattery', + 'RecordSpeedNext', + 'RfBypass', + 'ScanChannelsToggle', + 'ScreenModeNext', + 'Settings', + 'SplitScreenToggle', + 'STBInput', + 'STBPower', + 'Subtitle', + 'Teletext', + 'VideoModeNext', + 'Wink', + 'ZoomToggle', + 'AudioVolumeDown', + 'AudioVolumeUp', + 'AudioVolumeMute', + 'BrowserBack', + 'BrowserForward', + 'ChannelDown', + 'ChannelUp', + 'ContextMenu', + 'Eject', + 'End', + 'Enter', + 'Home', + 'MediaFastForward', + 'MediaPlay', + 'MediaPlayPause', + 'MediaRecord', + 'MediaRewind', + 'MediaStop', + 'MediaNextTrack', + 'MediaPause', + 'MediaPreviousTrack', + 'Power' +]); diff --git a/packages/keyboard/tests/src/index.spec.ts b/packages/keyboard/tests/src/index.spec.ts index ca2f97f7c..58b40a1c9 100644 --- a/packages/keyboard/tests/src/index.spec.ts +++ b/packages/keyboard/tests/src/index.spec.ts @@ -50,20 +50,26 @@ describe('@lumino/keyboard', () => { describe('#keys()', () => { it('should get an array of all key values supported by the layout', () => { - let layout = new KeycodeLayout('ab-cd', { 100: 'F' }); + let layout = new KeycodeLayout('ab-cd', { 100: 'F' }, [], { F4: 'F4' }); let keys = layout.keys(); - expect(keys.length).to.equal(1); - expect(keys[0]).to.equal('F'); + expect(keys.length).to.equal(2); + expect(keys[0]).to.equal('F', 'F4'); }); }); describe('#isValidKey()', () => { it('should test whether the key is valid for the layout', () => { - let layout = new KeycodeLayout('foo', { 100: 'F' }); + let layout = new KeycodeLayout('foo', { 100: 'F' }, [], { F4: 'F4' }); expect(layout.isValidKey('F')).to.equal(true); + expect(layout.isValidKey('F4')).to.equal(true); expect(layout.isValidKey('A')).to.equal(false); }); + it('should treat unmodified special keys as valid', () => { + let layout = new KeycodeLayout('foo', { 100: 'F' }, [], { F4: 'F4' }); + expect(layout.isValidKey('MediaPlayPause')).to.equal(true); + }); + it('should treat modifier keys as valid', () => { let layout = new KeycodeLayout('foo', { 100: 'F', 101: 'A' }, ['A']); expect(layout.isValidKey('A')).to.equal(true); @@ -97,6 +103,50 @@ describe('@lumino/keyboard', () => { let key = layout.keyForKeydownEvent(event as KeyboardEvent); expect(key).to.equal(''); }); + + it('should get the key from a `code` value', () => { + let layout = new KeycodeLayout('foo', { 100: 'F' }, [], { + Escape: 'Escape' + }); + let event = new KeyboardEvent('keydown', { code: 'Escape' }); + let key = layout.keyForKeydownEvent(event as KeyboardEvent); + expect(key).to.equal('Escape'); + }); + + it('should fall back to keyCode for Unidentified', () => { + let layout = new KeycodeLayout('foo', { 100: 'F' }, [], { + Escape: 'Escape' + }); + let event = new KeyboardEvent('keydown', { + code: 'Unidentified', + keyCode: 100 + }); + let key = layout.keyForKeydownEvent(event as KeyboardEvent); + expect(key).to.equal('F'); + }); + + it('should treat special keys as valid', () => { + let layout = new KeycodeLayout('foo', { 100: 'F' }, [], { F4: 'F4' }); + let event = new KeyboardEvent('keydown', { + code: 'Unidentified', + ctrlKey: true, + key: 'MediaPlayPause', + keyCode: 170 + }); + let key = layout.keyForKeydownEvent(event as KeyboardEvent); + expect(key).to.equal('MediaPlayPause'); + }); + + it('should use keyCode over special key value', () => { + let layout = new KeycodeLayout('foo', { 100: 'F' }, [], { F4: 'F4' }); + let event = new KeyboardEvent('keydown', { + code: 'Unidentified', + key: 'MediaPlayPause', + keyCode: 100 + }); + let key = layout.keyForKeydownEvent(event as KeyboardEvent); + expect(key).to.equal('F'); + }); }); describe('.extractKeys()', () => { @@ -130,14 +180,14 @@ describe('@lumino/keyboard', () => { it('should have modifier keys', () => { expect(EN_US.isValidKey('Shift')).to.equal(true); - expect(EN_US.isValidKey('Ctrl')).to.equal(true); + expect(EN_US.isValidKey('Control')).to.equal(true); expect(EN_US.isValidKey('Alt')).to.equal(true); expect(EN_US.isValidKey('Meta')).to.equal(true); }); it('should correctly detect modifier keys', () => { expect(EN_US.isModifierKey('Shift')).to.equal(true); - expect(EN_US.isModifierKey('Ctrl')).to.equal(true); + expect(EN_US.isModifierKey('Control')).to.equal(true); expect(EN_US.isModifierKey('Alt')).to.equal(true); expect(EN_US.isModifierKey('Meta')).to.equal(true); }); diff --git a/packages/keyboard/tsconfig.json b/packages/keyboard/tsconfig.json index 8d70721ce..8a18e322e 100644 --- a/packages/keyboard/tsconfig.json +++ b/packages/keyboard/tsconfig.json @@ -6,5 +6,5 @@ "outDir": "lib", "rootDir": "src" }, - "include": ["src/*"] + "include": ["src/**/*"] } diff --git a/yarn.lock b/yarn.lock index 48fd6d3c9..cc923de41 100644 --- a/yarn.lock +++ b/yarn.lock @@ -616,6 +616,22 @@ __metadata: languageName: unknown linkType: soft +"@lumino/example-keyboard-capture@workspace:examples/example-keyboard-capture": + version: 0.0.0-use.local + resolution: "@lumino/example-keyboard-capture@workspace:examples/example-keyboard-capture" + dependencies: + "@lumino/keyboard-capture": ^2.0.0 + "@lumino/messaging": ^2.0.2 + "@lumino/signaling": ^2.1.3 + "@lumino/widgets": ^2.6.0 + "@rollup/plugin-node-resolve": ^15.0.1 + rimraf: ^5.0.1 + rollup: ^3.25.1 + rollup-plugin-styles: ^4.0.0 + typescript: ~5.1.3 + languageName: unknown + linkType: soft + "@lumino/example-menubar@workspace:examples/example-menubar": version: 0.0.0-use.local resolution: "@lumino/example-menubar@workspace:examples/example-menubar" @@ -639,6 +655,33 @@ __metadata: languageName: unknown linkType: soft +"@lumino/keyboard-capture@^2.0.0, @lumino/keyboard-capture@workspace:packages/keyboard-capture": + version: 0.0.0-use.local + resolution: "@lumino/keyboard-capture@workspace:packages/keyboard-capture" + dependencies: + "@lumino/keyboard": ^2.0.2 + "@lumino/messaging": ^2.0.2 + "@lumino/signaling": ^2.1.3 + "@lumino/widgets": ^2.6.0 + "@microsoft/api-extractor": ^7.36.0 + "@rollup/plugin-commonjs": ^24.0.0 + "@rollup/plugin-node-resolve": ^15.0.1 + "@types/chai": ^3.4.35 + "@types/mocha": ^2.2.39 + "@web/test-runner": ^0.18.2 + "@web/test-runner-playwright": ^0.11.0 + chai: ^4.3.4 + mocha: ^9.0.3 + rimraf: ^5.0.1 + rollup: ^3.25.1 + rollup-plugin-postcss: ^4.0.2 + rollup-plugin-sourcemaps: ^0.6.3 + terser: ^5.18.1 + tslib: ^2.3.0 + typescript: ~5.1.3 + languageName: unknown + linkType: soft + "@lumino/keyboard@^2.0.2, @lumino/keyboard@workspace:packages/keyboard": version: 0.0.0-use.local resolution: "@lumino/keyboard@workspace:packages/keyboard" From 6dbf4a88e7d9b20e768a0894e23941e8ddd81ada Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske <510760+vidartf@users.noreply.github.com> Date: Sun, 2 Mar 2025 15:46:24 +0000 Subject: [PATCH 03/15] Potential Ctrl compatability Not 100% on if this is really needed or not. --- packages/keyboard/src/core.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/keyboard/src/core.ts b/packages/keyboard/src/core.ts index fb5dc35dd..96f5dc131 100644 --- a/packages/keyboard/src/core.ts +++ b/packages/keyboard/src/core.ts @@ -124,6 +124,7 @@ export class KeycodeLayout implements IKeyboardLayout { * @returns `true` if the key is valid, `false` otherwise. */ isValidKey(key: string): boolean { + key = Private.normalizeCtrl(key); return key in this._keys || Private.isSpecialCharacter(key); } @@ -135,6 +136,7 @@ export class KeycodeLayout implements IKeyboardLayout { * @returns `true` if the key is a modifier key, `false` otherwise. */ isModifierKey(key: string): boolean { + key = Private.normalizeCtrl(key); return key in this._modifierKeys; } @@ -236,4 +238,14 @@ namespace Private { // or more alphanumeric basic latin characters, it is likely a special key. return SPECIAL_KEYS.has(key); } + + /** + * Normalize Ctrl to Control for backwards compatability. + * + * @param key - The key value that is to be normalized + * @returns The normalized key string + */ + export function normalizeCtrl(key: string): string { + return key === 'Ctrl' ? 'Control' : key; + } } From 66dadf351d3bdd44e0b8b7686a5531dfa455bcf6 Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Fri, 4 Feb 2022 18:14:23 +0000 Subject: [PATCH 04/15] Add norwegian keyboard layout --- packages/keyboard/src/layouts/index.ts | 1 + packages/keyboard/src/layouts/nb-NO.ts | 123 +++++++++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 packages/keyboard/src/layouts/nb-NO.ts diff --git a/packages/keyboard/src/layouts/index.ts b/packages/keyboard/src/layouts/index.ts index 4cfddb8e6..c13cdc4e3 100644 --- a/packages/keyboard/src/layouts/index.ts +++ b/packages/keyboard/src/layouts/index.ts @@ -2,3 +2,4 @@ // Distributed under the terms of the Modified BSD License. export { EN_US } from './en-US'; +export { NB_NO } from './nb-NO'; diff --git a/packages/keyboard/src/layouts/nb-NO.ts b/packages/keyboard/src/layouts/nb-NO.ts new file mode 100644 index 000000000..35d8034c1 --- /dev/null +++ b/packages/keyboard/src/layouts/nb-NO.ts @@ -0,0 +1,123 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import { IKeyboardLayout, KeycodeLayout } from '../core'; + +import { MODIFIER_KEYS } from '../special-keys'; + +/** + * A code-based keyboard layout for a common Norwegian keyboard. + * + * Note that this does not include Apple's magic Keyboards, as they map + * the keys next to the Enter key differently (BracketRight and + * Backslash on en-US). + */ +export const NB_NO: IKeyboardLayout = new KeycodeLayout( + 'nb-NO', + {}, + MODIFIER_KEYS, + { + AltLeft: 'Alt', + AltRight: 'AltGraph', + Backquote: '|', + Backslash: "'", + Backspace: 'Backspace', + BracketLeft: 'Å', + CapsLock: 'CapsLock', + Comma: ',', + ContextMenu: 'ContextMenu', + ControlLeft: 'Control', + ControlRight: 'Control', + Delete: 'Delete', + Digit0: '0', + Digit1: '1', + Digit2: '2', + Digit3: '3', + Digit4: '4', + Digit5: '5', + Digit6: '6', + Digit7: '7', + Digit8: '8', + Digit9: '9', + End: 'End', + Enter: 'Enter', + Equal: '\\', + Escape: 'Escape', + F1: 'F1', + F10: 'F10', + F11: 'F11', + F12: 'F12', + F2: 'F2', + F3: 'F3', + F4: 'F4', + F5: 'F5', + F6: 'F6', + F7: 'F7', + F8: 'F8', + F9: 'F9', + Home: 'Home', + Insert: 'Insert', + IntlBackslash: '<', + KeyA: 'A', + KeyB: 'B', + KeyC: 'C', + KeyD: 'D', + KeyE: 'E', + KeyF: 'F', + KeyG: 'G', + KeyH: 'H', + KeyI: 'I', + KeyJ: 'J', + KeyK: 'K', + KeyL: 'L', + KeyM: 'M', + KeyN: 'N', + KeyO: 'O', + KeyP: 'P', + KeyQ: 'Q', + KeyR: 'R', + KeyS: 'S', + KeyT: 'T', + KeyU: 'U', + KeyV: 'V', + KeyW: 'W', + KeyX: 'X', + KeyY: 'Y', + KeyZ: 'Z', + MetaLeft: 'Meta', // chrome + MetaRight: 'Meta', // chrome + Minus: '+', + NumLock: 'NumLock', + Numpad0: 'Insert', + Numpad1: 'End', + Numpad2: 'ArrowDown', + Numpad3: 'PageDown', + Numpad4: 'ArrowLeft', + Numpad5: 'Clear', + Numpad6: 'ArrowRight', + Numpad7: 'Home', + Numpad8: 'ArrowUp', + Numpad9: 'PageUp', + NumpadAdd: '+', + NumpadDecimal: 'Delete', + NumpadDivide: '/', + NumpadEnter: 'Enter', + NumpadMultiply: '*', + NumpadSubtract: '-', + OSLeft: 'OS', // firefox + OSRight: 'OS', // firefox + PageDown: 'PageDown', + PageUp: 'PageUp', + Pause: 'Pause', + Period: '.', + PrintScreen: 'PrintScreen', + Quote: 'Æ', + ScrollLock: 'ScrollLock', + Semicolon: 'Ø', + ShiftLeft: 'Shift', + ShiftRight: 'Shift', + Slash: '-', + Space: ' ', + Tab: 'Tab' + } +); From 6f3d9be95d95fb9b13f6d6efa30c9fbe01910ac3 Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske <510760+vidartf@users.noreply.github.com> Date: Sun, 2 Mar 2025 15:45:46 +0000 Subject: [PATCH 05/15] Add fr-FR keyboard layout --- packages/keyboard/src/layouts/en-US.ts | 3 +- packages/keyboard/src/layouts/fr-FR.ts | 117 ++++++++++++++++++++++ packages/keyboard/src/layouts/index.ts | 1 + packages/keyboard/tests/src/index.spec.ts | 61 +++++++++++ 4 files changed, 180 insertions(+), 2 deletions(-) create mode 100644 packages/keyboard/src/layouts/fr-FR.ts diff --git a/packages/keyboard/src/layouts/en-US.ts b/packages/keyboard/src/layouts/en-US.ts index e3e5e0436..8902fc341 100644 --- a/packages/keyboard/src/layouts/en-US.ts +++ b/packages/keyboard/src/layouts/en-US.ts @@ -136,8 +136,7 @@ export const EN_US: IKeyboardLayout = new KeycodeLayout( 222: "'", 224: 'Meta' // firefox }, - // TODO: Figure out Ctrl vs Control - [...MODIFIER_KEYS, 'Ctrl'], + MODIFIER_KEYS, { AltLeft: 'Alt', AltRight: 'Alt', diff --git a/packages/keyboard/src/layouts/fr-FR.ts b/packages/keyboard/src/layouts/fr-FR.ts new file mode 100644 index 000000000..2f94cef94 --- /dev/null +++ b/packages/keyboard/src/layouts/fr-FR.ts @@ -0,0 +1,117 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import { IKeyboardLayout, KeycodeLayout } from '../core'; + +import { MODIFIER_KEYS } from '../special-keys'; + +/** + * A code-based keyboard layout for a common Norwegian keyboard. + * + * Note that this does not include Apple's magic Keyboards, as they map + * the keys next to the Enter key differently (BracketRight and + * Backslash on en-US). + */ +export const FR_FR: IKeyboardLayout = new KeycodeLayout( + 'fr-FR', + {}, + MODIFIER_KEYS, + { + AltLeft: 'Alt', + AltRight: 'AltGraph', + ArrowDown: 'ArrowDown', + ArrowLeft: 'ArrowLeft', + ArrowRight: 'ArrowRight', + ArrowUp: 'ArrowUp', + Backquote: '²', + Backslash: '*', + Backspace: 'Backspace', + BracketRight: '$', + Comma: ';', + ControlLeft: 'Control', + ControlRight: 'Control', + Delete: 'Delete', + Digit0: 'À', + Digit1: '&', + Digit2: 'É', + Digit3: '"', + Digit4: "'", + Digit5: '(', + Digit6: '-', + Digit7: 'È', + Digit8: '_', + Digit9: 'Ç', + End: 'End', + Enter: 'Enter', + Equal: '=', + Escape: 'Escape', + F1: 'F1', + F10: 'F10', + F11: 'F11', + F12: 'F12', + F2: 'F2', + F3: 'F3', + F4: 'F4', + F5: 'F5', + F6: 'F6', + F7: 'F7', + F8: 'F8', + F9: 'F9', + Home: 'Home', + Insert: 'Insert', + IntlBackslash: '<', + KeyA: 'Q', + KeyB: 'B', + KeyC: 'C', + KeyD: 'D', + KeyE: 'E', + KeyF: 'F', + KeyG: 'G', + KeyH: 'H', + KeyI: 'I', + KeyJ: 'J', + KeyK: 'K', + KeyL: 'L', + KeyM: ',', + KeyN: 'N', + KeyO: 'O', + KeyP: 'P', + KeyQ: 'A', + KeyR: 'R', + KeyS: 'S', + KeyT: 'T', + KeyU: 'U', + KeyV: 'V', + KeyW: 'Z', + KeyX: 'X', + KeyY: 'Y', + KeyZ: 'W', + Minus: ')', + Numpad0: '0', + Numpad1: '1', + Numpad2: '2', + Numpad3: '3', + Numpad4: '4', + Numpad5: '5', + Numpad6: '6', + Numpad7: '7', + Numpad8: '8', + Numpad9: '9', + NumpadAdd: '+', + NumpadDecimal: '.', + NumpadDivide: '/', + NumpadEnter: 'Enter', + NumpadMultiply: '*', + NumpadSubtract: '-', + PageDown: 'PageDown', + PageUp: 'PageUp', + Period: ':', + Quote: 'Ù', + ScrollLock: 'ScrollLock', + Semicolon: 'M', + ShiftLeft: 'Shift', + ShiftRight: 'Shift', + Slash: '!', + Tab: 'Tab' + } +); diff --git a/packages/keyboard/src/layouts/index.ts b/packages/keyboard/src/layouts/index.ts index c13cdc4e3..ff6806b4a 100644 --- a/packages/keyboard/src/layouts/index.ts +++ b/packages/keyboard/src/layouts/index.ts @@ -2,4 +2,5 @@ // Distributed under the terms of the Modified BSD License. export { EN_US } from './en-US'; +export { FR_FR } from './fr-FR'; export { NB_NO } from './nb-NO'; diff --git a/packages/keyboard/tests/src/index.spec.ts b/packages/keyboard/tests/src/index.spec.ts index 58b40a1c9..acaab922d 100644 --- a/packages/keyboard/tests/src/index.spec.ts +++ b/packages/keyboard/tests/src/index.spec.ts @@ -11,8 +11,10 @@ import { expect } from 'chai'; import { EN_US, + FR_FR, getKeyboardLayout, KeycodeLayout, + NB_NO, setKeyboardLayout } from '@lumino/keyboard'; @@ -192,4 +194,63 @@ describe('@lumino/keyboard', () => { expect(EN_US.isModifierKey('Meta')).to.equal(true); }); }); + + describe('FR_FR', () => { + it('should be a keycode layout', () => { + expect(FR_FR).to.be.an.instanceof(KeycodeLayout); + }); + + it('should have standardized keys', () => { + expect(FR_FR.isValidKey('A')).to.equal(true); + expect(FR_FR.isValidKey('Z')).to.equal(true); + expect(FR_FR.isValidKey('0')).to.equal(true); + expect(FR_FR.isValidKey('a')).to.equal(false); + expect(FR_FR.isValidKey('Ù')).to.equal(true); + }); + + it('should have modifier keys', () => { + expect(FR_FR.isValidKey('Shift')).to.equal(true); + expect(FR_FR.isValidKey('Control')).to.equal(true); + expect(FR_FR.isValidKey('Alt')).to.equal(true); + expect(NB_NO.isValidKey('AltGraph')).to.equal(true); + expect(FR_FR.isValidKey('Meta')).to.equal(true); + }); + + it('should correctly detect modifier keys', () => { + expect(FR_FR.isModifierKey('Shift')).to.equal(true); + expect(FR_FR.isModifierKey('Control')).to.equal(true); + expect(FR_FR.isModifierKey('Alt')).to.equal(true); + expect(FR_FR.isModifierKey('Meta')).to.equal(true); + }); + }); + + describe('NB_NO', () => { + it('should be a keycode layout', () => { + expect(NB_NO).to.be.an.instanceof(KeycodeLayout); + }); + + it('should have standardized keys', () => { + expect(NB_NO.isValidKey('A')).to.equal(true); + expect(NB_NO.isValidKey('Z')).to.equal(true); + expect(NB_NO.isValidKey('0')).to.equal(true); + expect(NB_NO.isValidKey('a')).to.equal(false); + expect(NB_NO.isValidKey('Æ')).to.equal(true); + }); + + it('should have modifier keys', () => { + expect(NB_NO.isValidKey('Shift')).to.equal(true); + expect(NB_NO.isValidKey('Control')).to.equal(true); + expect(NB_NO.isValidKey('Alt')).to.equal(true); + expect(NB_NO.isValidKey('AltGraph')).to.equal(true); + expect(NB_NO.isValidKey('Meta')).to.equal(true); + }); + + it('should correctly detect modifier keys', () => { + expect(NB_NO.isModifierKey('Shift')).to.equal(true); + expect(NB_NO.isModifierKey('Control')).to.equal(true); + expect(NB_NO.isModifierKey('Alt')).to.equal(true); + expect(NB_NO.isModifierKey('AltGraph')).to.equal(true); + expect(NB_NO.isModifierKey('Meta')).to.equal(true); + }); + }); }); From ef652c0681eaec21de41a1d567cbde6f9c24e166 Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske <510760+vidartf@users.noreply.github.com> Date: Sun, 9 Mar 2025 14:22:37 +0000 Subject: [PATCH 06/15] Reorg layout exports Only export en_US directly, the rest in a collection --- packages/keyboard/src/index.ts | 5 ++++- packages/keyboard/tests/src/index.spec.ts | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/keyboard/src/index.ts b/packages/keyboard/src/index.ts index f833b57e3..9ff104e5c 100644 --- a/packages/keyboard/src/index.ts +++ b/packages/keyboard/src/index.ts @@ -15,8 +15,11 @@ import { IKeyboardLayout } from './core'; export { IKeyboardLayout, KeycodeLayout } from './core'; +export { EN_US } from './layouts'; import { EN_US } from './layouts'; -export * from './layouts'; +import * as Layouts from './layouts'; + +export const KeyboardLayouts = Object.values(Layouts); /** * Get the global application keyboard layout instance. diff --git a/packages/keyboard/tests/src/index.spec.ts b/packages/keyboard/tests/src/index.spec.ts index acaab922d..e6649dfa1 100644 --- a/packages/keyboard/tests/src/index.spec.ts +++ b/packages/keyboard/tests/src/index.spec.ts @@ -11,13 +11,15 @@ import { expect } from 'chai'; import { EN_US, - FR_FR, getKeyboardLayout, + KeyboardLayouts, KeycodeLayout, - NB_NO, setKeyboardLayout } from '@lumino/keyboard'; +const FR_FR = KeyboardLayouts.find(layout => layout.name === 'fr-FR'); +const NB_NO = KeyboardLayouts.find(layout => layout.name === 'nb-NO'); + describe('@lumino/keyboard', () => { describe('getKeyboardLayout()', () => { it('should return the global keyboard layout', () => { From 2e0293cd609547691037271c8f2d66ce07c8c075 Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske <510760+vidartf@users.noreply.github.com> Date: Sun, 9 Mar 2025 14:25:13 +0000 Subject: [PATCH 07/15] Experimental brower keyboard API --- packages/keyboard/src/index.ts | 91 +++++++++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/packages/keyboard/src/index.ts b/packages/keyboard/src/index.ts index 9ff104e5c..891a82754 100644 --- a/packages/keyboard/src/index.ts +++ b/packages/keyboard/src/index.ts @@ -12,12 +12,13 @@ * @module keyboard */ -import { IKeyboardLayout } from './core'; +import { IKeyboardLayout, KeycodeLayout } from './core'; export { IKeyboardLayout, KeycodeLayout } from './core'; export { EN_US } from './layouts'; import { EN_US } from './layouts'; import * as Layouts from './layouts'; +import { MODIFIER_KEYS } from './special-keys'; export const KeyboardLayouts = Object.values(Layouts); @@ -42,9 +43,40 @@ export function getKeyboardLayout(): IKeyboardLayout { * to a layout which is appropriate for the user's system. */ export function setKeyboardLayout(layout: IKeyboardLayout): void { + try { + Private.unsubscribeBrowserUpdates(); + } catch (e) { + // Ignore exceptions in experimental code + } Private.keyboardLayout = layout; } +/** + * Whether the browser supports inspecting the keyboard layout. + * + * @alpha + */ +export function hasBrowserLayout(): boolean { + return !!(navigator as any)?.keyboard?.getLayoutMap; +} + +/** + * Use the keyboard layout of the browser if it supports it. + * + * @alpha + * @returns Whether the browser supports inspecting the keyboard layout. + */ +export async function useBrowserLayout(): Promise { + const keyboardApi = (navigator as any)?.keyboard; + if (!(await Private.updateBrowserLayout())) { + return false; + } + if (keyboardApi?.addEventListener) { + keyboardApi.addEventListener('layoutchange', Private.updateBrowserLayout); + } + return true; +} + /** * The namespace for the module implementation details. */ @@ -53,4 +85,61 @@ namespace Private { * The global keyboard layout instance. */ export let keyboardLayout = EN_US; + + /** + * Polyfill until Object.fromEntries is available + */ + function fromEntries(entries: Iterable<[string, T]>) { + const ret = {} as { [key: string]: T }; + for (const [key, value] of entries) { + ret[key] = value; + } + return ret; + } + + /** + * Get the current browser keyboard layout, or null if unsupported. + * + * @returns The keyboard layout of the browser at this moment if supported, otherwise null. + */ + export async function getBrowserKeyboardLayout(): Promise< + IKeyboardLayout | undefined + > { + const keyboardApi = (navigator as any)?.keyboard; + if (!keyboardApi) { + return undefined; + } + const browserMap = await keyboardApi.getLayoutMap(); + if (!browserMap) { + return undefined; + } + return new KeycodeLayout( + 'browser', + {}, + MODIFIER_KEYS, + fromEntries(browserMap.entries()) + ); + } + + /** + * Set the active layout to that of the browser at this instant. + */ + export async function updateBrowserLayout(): Promise { + const initial = await getBrowserKeyboardLayout(); + if (!initial) { + return false; + } + keyboardLayout = initial; + return true; + } + + /** + * Unsubscribe any browser updates + */ + export function unsubscribeBrowserUpdates(): void { + const keyboardApi = (navigator as any)?.keyboard; + if (keyboardApi?.removeEventListener) { + keyboardApi.removeEventListener(updateBrowserLayout); + } + } } From 9fef01c7c175b9c726004baeb906124e516c47e8 Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske <510760+vidartf@users.noreply.github.com> Date: Sun, 9 Mar 2025 14:25:29 +0000 Subject: [PATCH 08/15] Rename en-US keymap (case) --- packages/keyboard/src/layouts/en-US.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/keyboard/src/layouts/en-US.ts b/packages/keyboard/src/layouts/en-US.ts index 8902fc341..c1798b8bf 100644 --- a/packages/keyboard/src/layouts/en-US.ts +++ b/packages/keyboard/src/layouts/en-US.ts @@ -34,7 +34,7 @@ import { MODIFIER_KEYS } from '../special-keys'; * Other combinations may also work, but are untested. */ export const EN_US: IKeyboardLayout = new KeycodeLayout( - 'en-us', + 'en-US', { 8: 'Backspace', 9: 'Tab', From 4ed7b70767f4936166bd1c4357a67536875cbea5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 9 Mar 2025 14:50:06 +0000 Subject: [PATCH 09/15] Automatic application of license header --- examples/example-keyboard-capture/style/index.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/examples/example-keyboard-capture/style/index.css b/examples/example-keyboard-capture/style/index.css index bbe0d6847..2a53fa752 100644 --- a/examples/example-keyboard-capture/style/index.css +++ b/examples/example-keyboard-capture/style/index.css @@ -1,3 +1,8 @@ +/* + * Copyright (c) Jupyter Development Team. + * Distributed under the terms of the Modified BSD License. + */ + /*----------------------------------------------------------------------------- | Copyright (c) Jupyter Development Team. | Copyright (c) 2014-2017, PhosphorJS Contributors From 4afa01736d2896eaaa3aa0302276bac7f0a60f02 Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske <510760+vidartf@users.noreply.github.com> Date: Sun, 9 Mar 2025 15:40:36 +0000 Subject: [PATCH 10/15] Make useBrowserLayout have less churn If we already are using a browser based layout, don't create a new one. --- packages/keyboard/src/index.ts | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/keyboard/src/index.ts b/packages/keyboard/src/index.ts index 891a82754..c13c02f37 100644 --- a/packages/keyboard/src/index.ts +++ b/packages/keyboard/src/index.ts @@ -68,8 +68,11 @@ export function hasBrowserLayout(): boolean { */ export async function useBrowserLayout(): Promise { const keyboardApi = (navigator as any)?.keyboard; - if (!(await Private.updateBrowserLayout())) { - return false; + // avoid updating if already set + if (Private.keyboardLayout.name !== Private.INTERNAL_BROWSER_LAYOUT_NAME) { + if (!(await Private.updateBrowserLayout())) { + return false; + } } if (keyboardApi?.addEventListener) { keyboardApi.addEventListener('layoutchange', Private.updateBrowserLayout); @@ -87,7 +90,12 @@ namespace Private { export let keyboardLayout = EN_US; /** - * Polyfill until Object.fromEntries is available + * Internal name for browser-based keyboard layout. + */ + export const INTERNAL_BROWSER_LAYOUT_NAME = '__lumino-internal-browser'; + + /** + * Polyfill until Object.fromEntries is available. */ function fromEntries(entries: Iterable<[string, T]>) { const ret = {} as { [key: string]: T }; @@ -114,10 +122,17 @@ namespace Private { return undefined; } return new KeycodeLayout( - 'browser', + INTERNAL_BROWSER_LAYOUT_NAME, {}, MODIFIER_KEYS, - fromEntries(browserMap.entries()) + fromEntries( + browserMap + .entries() + .map(([k, v]: string[]) => [ + k, + v.charAt(0).toUpperCase() + v.slice(1) + ]) + ) ); } From 989c318fb50d419f27aed656a0e5a59acbccafde Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske <510760+vidartf@users.noreply.github.com> Date: Wed, 12 Mar 2025 16:55:45 +0000 Subject: [PATCH 11/15] Move all capture code to example This avoids a new package, and the code is small enough that it fits well as an example. --- .../example-keyboard-capture/package.json | 1 - .../example-keyboard-capture}/src/capture.ts | 0 .../example-keyboard-capture/src/index.ts | 81 +--------- .../example-keyboard-capture/src/output.ts | 82 ++++++++++ packages/keyboard-capture/api-extractor.json | 20 --- packages/keyboard-capture/package.json | 67 -------- packages/keyboard-capture/rollup.config.mjs | 13 -- packages/keyboard-capture/src/index.ts | 11 -- .../keyboard-capture/tests/rollup.config.mjs | 8 - .../keyboard-capture/tests/src/index.spec.ts | 145 ------------------ packages/keyboard-capture/tests/tsconfig.json | 10 -- packages/keyboard-capture/tsconfig.json | 21 --- packages/keyboard-capture/typedoc.json | 6 - yarn.lock | 28 ---- 14 files changed, 84 insertions(+), 409 deletions(-) rename {packages/keyboard-capture => examples/example-keyboard-capture}/src/capture.ts (100%) create mode 100644 examples/example-keyboard-capture/src/output.ts delete mode 100644 packages/keyboard-capture/api-extractor.json delete mode 100644 packages/keyboard-capture/package.json delete mode 100644 packages/keyboard-capture/rollup.config.mjs delete mode 100644 packages/keyboard-capture/src/index.ts delete mode 100644 packages/keyboard-capture/tests/rollup.config.mjs delete mode 100644 packages/keyboard-capture/tests/src/index.spec.ts delete mode 100644 packages/keyboard-capture/tests/tsconfig.json delete mode 100644 packages/keyboard-capture/tsconfig.json delete mode 100644 packages/keyboard-capture/typedoc.json diff --git a/examples/example-keyboard-capture/package.json b/examples/example-keyboard-capture/package.json index ba52b7958..91a77f5bb 100644 --- a/examples/example-keyboard-capture/package.json +++ b/examples/example-keyboard-capture/package.json @@ -7,7 +7,6 @@ "clean": "rimraf build" }, "dependencies": { - "@lumino/keyboard-capture": "^2.0.0", "@lumino/signaling": "^2.1.3", "@lumino/widgets": "^2.6.0" }, diff --git a/packages/keyboard-capture/src/capture.ts b/examples/example-keyboard-capture/src/capture.ts similarity index 100% rename from packages/keyboard-capture/src/capture.ts rename to examples/example-keyboard-capture/src/capture.ts diff --git a/examples/example-keyboard-capture/src/index.ts b/examples/example-keyboard-capture/src/index.ts index 83b0dadd1..594185a30 100644 --- a/examples/example-keyboard-capture/src/index.ts +++ b/examples/example-keyboard-capture/src/index.ts @@ -8,89 +8,12 @@ | The full license is in the file LICENSE, distributed with this software. |----------------------------------------------------------------------------*/ -import { CaptureWidget } from '@lumino/keyboard-capture'; -import { Message } from '@lumino/messaging'; -import { ISignal, Signal } from '@lumino/signaling'; import { Panel, Widget } from '@lumino/widgets'; +import { CaptureWidget } from './capture'; +import { OutputWidget } from './output'; import '../style/index.css'; -export class OutputWidget extends Widget { - /** - * - */ - constructor(options?: Widget.IOptions) { - super(options); - this._output = document.createElement('div'); - this._exportButton = document.createElement('button'); - this._exportButton.innerText = 'Show'; - this._copyButton = document.createElement('button'); - this._copyButton.innerText = 'Copy'; - this._clearButton = document.createElement('button'); - this._clearButton.innerText = 'Clear'; - this.node.appendChild(this._exportButton); - this.node.appendChild(this._copyButton); - this.node.appendChild(this._clearButton); - this.node.appendChild(this._output); - this.addClass('lm-keyboardCaptureOutputArea'); - } - - set value(content: string) { - this._output.innerHTML = content; - } - - get action(): ISignal { - return this._action; - } - - /** - * Handle the DOM events for the widget. - * - * @param event - The DOM event sent to the element. - */ - handleEvent(event: Event): void { - switch (event.type) { - case 'click': - if (event.target === this._exportButton) { - event.stopPropagation(); - this._action.emit('display'); - } else if (event.target === this._copyButton) { - event.stopPropagation(); - this._action.emit('clipboard'); - } else if (event.target === this._clearButton) { - event.stopPropagation(); - this._action.emit('clear'); - } - break; - } - } - - /** - * A message handler invoked on a `'before-attach'` message. - */ - protected onBeforeAttach(msg: Message): void { - this._exportButton.addEventListener('click', this); - this._copyButton.addEventListener('click', this); - this._clearButton.addEventListener('click', this); - super.onBeforeAttach(msg); - } - - /** - * A message handler invoked on an `'after-detach'` message. - */ - protected onAfterDetach(msg: Message): void { - super.onAfterDetach(msg); - this._exportButton.removeEventListener('click', this); - this._copyButton.removeEventListener('click', this); - this._clearButton.removeEventListener('click', this); - } - - private _output: HTMLElement; - private _exportButton: HTMLButtonElement; - private _copyButton: HTMLButtonElement; - private _clearButton: HTMLButtonElement; - private _action = new Signal(this); -} /** * Initialize the applicaiton. diff --git a/examples/example-keyboard-capture/src/output.ts b/examples/example-keyboard-capture/src/output.ts new file mode 100644 index 000000000..d191ce935 --- /dev/null +++ b/examples/example-keyboard-capture/src/output.ts @@ -0,0 +1,82 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. +import { Message } from '@lumino/messaging'; +import { ISignal, Signal } from '@lumino/signaling'; +import { Widget } from '@lumino/widgets'; + +export class OutputWidget extends Widget { + /** + * + */ + constructor(options?: Widget.IOptions) { + super(options); + this._output = document.createElement('div'); + this._exportButton = document.createElement('button'); + this._exportButton.innerText = 'Show'; + this._copyButton = document.createElement('button'); + this._copyButton.innerText = 'Copy'; + this._clearButton = document.createElement('button'); + this._clearButton.innerText = 'Clear'; + this.node.appendChild(this._exportButton); + this.node.appendChild(this._copyButton); + this.node.appendChild(this._clearButton); + this.node.appendChild(this._output); + this.addClass('lm-keyboardCaptureOutputArea'); + } + + set value(content: string) { + this._output.innerHTML = content; + } + + get action(): ISignal { + return this._action; + } + + /** + * Handle the DOM events for the widget. + * + * @param event - The DOM event sent to the element. + */ + handleEvent(event: Event): void { + switch (event.type) { + case 'click': + if (event.target === this._exportButton) { + event.stopPropagation(); + this._action.emit('display'); + } else if (event.target === this._copyButton) { + event.stopPropagation(); + this._action.emit('clipboard'); + } else if (event.target === this._clearButton) { + event.stopPropagation(); + this._action.emit('clear'); + } + break; + } + } + + /** + * A message handler invoked on a `'before-attach'` message. + */ + protected onBeforeAttach(msg: Message): void { + this._exportButton.addEventListener('click', this); + this._copyButton.addEventListener('click', this); + this._clearButton.addEventListener('click', this); + super.onBeforeAttach(msg); + } + + /** + * A message handler invoked on an `'after-detach'` message. + */ + protected onAfterDetach(msg: Message): void { + super.onAfterDetach(msg); + this._exportButton.removeEventListener('click', this); + this._copyButton.removeEventListener('click', this); + this._clearButton.removeEventListener('click', this); + } + + private _output: HTMLElement; + private _exportButton: HTMLButtonElement; + private _copyButton: HTMLButtonElement; + private _clearButton: HTMLButtonElement; + private _action = new Signal(this); +} \ No newline at end of file diff --git a/packages/keyboard-capture/api-extractor.json b/packages/keyboard-capture/api-extractor.json deleted file mode 100644 index 052dd8d2f..000000000 --- a/packages/keyboard-capture/api-extractor.json +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Config file for API Extractor. For more info, please visit: https://api-extractor.com - */ -{ - "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", - - /** - * Optionally specifies another JSON config file that this file extends from. This provides a way for - * standard settings to be shared across multiple projects. - * - * If the path starts with "./" or "../", the path is resolved relative to the folder of the file that contains - * the "extends" field. Otherwise, the first path segment is interpreted as an NPM package name, and will be - * resolved using NodeJS require(). - * - * SUPPORTED TOKENS: none - * DEFAULT VALUE: "" - */ - "extends": "../../api-extractor-base.json" - // "extends": "my-package/include/api-extractor-base.json" -} diff --git a/packages/keyboard-capture/package.json b/packages/keyboard-capture/package.json deleted file mode 100644 index 0159077be..000000000 --- a/packages/keyboard-capture/package.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "name": "@lumino/keyboard-capture", - "version": "2.0.0", - "description": "Lumino Keyboard Capture widget", - "homepage": "https://github.com/jupyterlab/lumino", - "bugs": { - "url": "https://github.com/jupyterlab/lumino/issues" - }, - "repository": { - "type": "git", - "url": "https://github.com/jupyterlab/lumino.git" - }, - "license": "BSD-3-Clause", - "author": "Project Jupyter", - "main": "dist/index.js", - "jsdelivr": "dist/index.min.js", - "unpkg": "dist/index.min.js", - "module": "dist/index.es6", - "types": "types/index.d.ts", - "files": [ - "dist/*", - "src/*", - "types/*" - ], - "scripts": { - "api": "api-extractor run --local --verbose", - "build": "yarn build:src && rollup -c", - "build:src": "tsc --build", - "build:test": "yarn clean:test && tsc --build tests && cd tests && rollup -c", - "clean": "rimraf ./lib *.tsbuildinfo ./types ./dist", - "clean:test": "rimraf tests/lib tests/tsconfig.tsbuildinfo", - "minimize": "terser dist/index.js -c -m --source-map \"content='dist/index.js.map',url='index.min.js.map'\" -o dist/index.min.js", - "test": "yarn build:test && web-test-runner tests/lib/bundle.test.js --node-resolve --playwright", - "test:chromium": "yarn test -- --browsers chromium", - "test:debug": "yarn test -- --manual --open", - "test:firefox": "yarn test -- --browsers firefox", - "test:webkit": "yarn test -- --browsers webkit", - "watch": "tsc --build --watch" - }, - "dependencies": { - "@lumino/keyboard": "^2.0.2", - "@lumino/signaling": "^2.1.3", - "@lumino/widgets": "^2.6.0" - }, - "devDependencies": { - "@lumino/messaging": "^2.0.2", - "@microsoft/api-extractor": "^7.36.0", - "@rollup/plugin-commonjs": "^24.0.0", - "@rollup/plugin-node-resolve": "^15.0.1", - "@types/chai": "^3.4.35", - "@types/mocha": "^2.2.39", - "@web/test-runner": "^0.18.2", - "@web/test-runner-playwright": "^0.11.0", - "chai": "^4.3.4", - "mocha": "^9.0.3", - "rimraf": "^5.0.1", - "rollup": "^3.25.1", - "rollup-plugin-postcss": "^4.0.2", - "rollup-plugin-sourcemaps": "^0.6.3", - "terser": "^5.18.1", - "tslib": "^2.3.0", - "typescript": "~5.1.3" - }, - "publishConfig": { - "access": "public" - } -} diff --git a/packages/keyboard-capture/rollup.config.mjs b/packages/keyboard-capture/rollup.config.mjs deleted file mode 100644 index b5224b39b..000000000 --- a/packages/keyboard-capture/rollup.config.mjs +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright (c) Jupyter Development Team. - * Distributed under the terms of the Modified BSD License. - */ - -import { createRollupConfig } from '@lumino/buildutils'; -import * as fs from 'fs'; - -const config = createRollupConfig({ - pkg: JSON.parse(fs.readFileSync('./package.json', 'utf-8')) -}); - -export default config; diff --git a/packages/keyboard-capture/src/index.ts b/packages/keyboard-capture/src/index.ts deleted file mode 100644 index 26315feec..000000000 --- a/packages/keyboard-capture/src/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. -/*----------------------------------------------------------------------------- -| Copyright (c) 2014-2017, PhosphorJS Contributors -| -| Distributed under the terms of the BSD 3-Clause License. -| -| The full license is in the file LICENSE, distributed with this software. -|----------------------------------------------------------------------------*/ - -export { CaptureWidget } from './capture'; diff --git a/packages/keyboard-capture/tests/rollup.config.mjs b/packages/keyboard-capture/tests/rollup.config.mjs deleted file mode 100644 index 10d911526..000000000 --- a/packages/keyboard-capture/tests/rollup.config.mjs +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright (c) Jupyter Development Team. - * Distributed under the terms of the Modified BSD License. - */ - -import { createRollupTestConfig } from '@lumino/buildutils'; -const rollupConfig = createRollupTestConfig(); -export default rollupConfig; diff --git a/packages/keyboard-capture/tests/src/index.spec.ts b/packages/keyboard-capture/tests/src/index.spec.ts deleted file mode 100644 index ca2f97f7c..000000000 --- a/packages/keyboard-capture/tests/src/index.spec.ts +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. -/*----------------------------------------------------------------------------- -| Copyright (c) 2014-2017, PhosphorJS Contributors -| -| Distributed under the terms of the BSD 3-Clause License. -| -| The full license is in the file LICENSE, distributed with this software. -|----------------------------------------------------------------------------*/ -import { expect } from 'chai'; - -import { - EN_US, - getKeyboardLayout, - KeycodeLayout, - setKeyboardLayout -} from '@lumino/keyboard'; - -describe('@lumino/keyboard', () => { - describe('getKeyboardLayout()', () => { - it('should return the global keyboard layout', () => { - expect(getKeyboardLayout()).to.equal(EN_US); - }); - }); - - describe('setKeyboardLayout()', () => { - it('should set the global keyboard layout', () => { - let layout = new KeycodeLayout('ab-cd', {}); - setKeyboardLayout(layout); - expect(getKeyboardLayout()).to.equal(layout); - setKeyboardLayout(EN_US); - expect(getKeyboardLayout()).to.equal(EN_US); - }); - }); - - describe('KeycodeLayout', () => { - describe('#constructor()', () => { - it('should construct a new keycode layout', () => { - let layout = new KeycodeLayout('ab-cd', {}); - expect(layout).to.be.an.instanceof(KeycodeLayout); - }); - }); - - describe('#name', () => { - it('should be a human readable name of the layout', () => { - let layout = new KeycodeLayout('ab-cd', {}); - expect(layout.name).to.equal('ab-cd'); - }); - }); - - describe('#keys()', () => { - it('should get an array of all key values supported by the layout', () => { - let layout = new KeycodeLayout('ab-cd', { 100: 'F' }); - let keys = layout.keys(); - expect(keys.length).to.equal(1); - expect(keys[0]).to.equal('F'); - }); - }); - - describe('#isValidKey()', () => { - it('should test whether the key is valid for the layout', () => { - let layout = new KeycodeLayout('foo', { 100: 'F' }); - expect(layout.isValidKey('F')).to.equal(true); - expect(layout.isValidKey('A')).to.equal(false); - }); - - it('should treat modifier keys as valid', () => { - let layout = new KeycodeLayout('foo', { 100: 'F', 101: 'A' }, ['A']); - expect(layout.isValidKey('A')).to.equal(true); - }); - }); - - describe('#isModifierKey()', () => { - it('should test whether the key is modifier for the layout', () => { - let layout = new KeycodeLayout('foo', { 100: 'F', 101: 'A' }, ['A']); - expect(layout.isModifierKey('F')).to.equal(false); - expect(layout.isModifierKey('A')).to.equal(true); - }); - - it('should return false for keys that are not in the layout', () => { - let layout = new KeycodeLayout('foo', { 100: 'F', 101: 'A' }, ['A']); - expect(layout.isModifierKey('B')).to.equal(false); - }); - }); - - describe('#keyForKeydownEvent()', () => { - it('should get the key for a `keydown` event', () => { - let layout = new KeycodeLayout('foo', { 100: 'F' }); - let event = new KeyboardEvent('keydown', { keyCode: 100 }); - let key = layout.keyForKeydownEvent(event as KeyboardEvent); - expect(key).to.equal('F'); - }); - - it('should return an empty string if the code is not valid', () => { - let layout = new KeycodeLayout('foo', { 100: 'F' }); - let event = new KeyboardEvent('keydown', { keyCode: 101 }); - let key = layout.keyForKeydownEvent(event as KeyboardEvent); - expect(key).to.equal(''); - }); - }); - - describe('.extractKeys()', () => { - it('should extract the keys from a code map', () => { - let keys: KeycodeLayout.CodeMap = { 70: 'F', 71: 'G', 72: 'H' }; - let goal: KeycodeLayout.KeySet = { F: true, G: true, H: true }; - expect(KeycodeLayout.extractKeys(keys)).to.deep.equal(goal); - }); - }); - - describe('.convertToKeySet()', () => { - it('should convert key array to key set', () => { - let keys: string[] = ['F', 'G', 'H']; - let goal: KeycodeLayout.KeySet = { F: true, G: true, H: true }; - expect(KeycodeLayout.convertToKeySet(keys)).to.deep.equal(goal); - }); - }); - }); - - describe('EN_US', () => { - it('should be a keycode layout', () => { - expect(EN_US).to.be.an.instanceof(KeycodeLayout); - }); - - it('should have standardized keys', () => { - expect(EN_US.isValidKey('A')).to.equal(true); - expect(EN_US.isValidKey('Z')).to.equal(true); - expect(EN_US.isValidKey('0')).to.equal(true); - expect(EN_US.isValidKey('a')).to.equal(false); - }); - - it('should have modifier keys', () => { - expect(EN_US.isValidKey('Shift')).to.equal(true); - expect(EN_US.isValidKey('Ctrl')).to.equal(true); - expect(EN_US.isValidKey('Alt')).to.equal(true); - expect(EN_US.isValidKey('Meta')).to.equal(true); - }); - - it('should correctly detect modifier keys', () => { - expect(EN_US.isModifierKey('Shift')).to.equal(true); - expect(EN_US.isModifierKey('Ctrl')).to.equal(true); - expect(EN_US.isModifierKey('Alt')).to.equal(true); - expect(EN_US.isModifierKey('Meta')).to.equal(true); - }); - }); -}); diff --git a/packages/keyboard-capture/tests/tsconfig.json b/packages/keyboard-capture/tests/tsconfig.json deleted file mode 100644 index d2de12617..000000000 --- a/packages/keyboard-capture/tests/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../../tsconfigbase", - "compilerOptions": { - "lib": ["DOM", "ES2018"], - "outDir": "lib", - "rootDir": "src", - "types": ["chai", "mocha"] - }, - "include": ["src/*"] -} diff --git a/packages/keyboard-capture/tsconfig.json b/packages/keyboard-capture/tsconfig.json deleted file mode 100644 index e91bdd250..000000000 --- a/packages/keyboard-capture/tsconfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "extends": "../../tsconfigbase", - "compilerOptions": { - "declarationDir": "types", - "lib": ["DOM", "ES2018"], - "outDir": "lib", - "rootDir": "src" - }, - "include": ["src/*"], - "references": [ - { - "path": "../keyboard" - }, - { - "path": "../signaling" - }, - { - "path": "../widgets" - } - ] -} diff --git a/packages/keyboard-capture/typedoc.json b/packages/keyboard-capture/typedoc.json deleted file mode 100644 index 3b1a7b153..000000000 --- a/packages/keyboard-capture/typedoc.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": ["../../typedoc.base.json"], - "entryPoints": ["src/index.ts"], - "name": "keyboard-capture", - "readme": "none" -} diff --git a/yarn.lock b/yarn.lock index cc923de41..c0ba01755 100644 --- a/yarn.lock +++ b/yarn.lock @@ -620,7 +620,6 @@ __metadata: version: 0.0.0-use.local resolution: "@lumino/example-keyboard-capture@workspace:examples/example-keyboard-capture" dependencies: - "@lumino/keyboard-capture": ^2.0.0 "@lumino/messaging": ^2.0.2 "@lumino/signaling": ^2.1.3 "@lumino/widgets": ^2.6.0 @@ -655,33 +654,6 @@ __metadata: languageName: unknown linkType: soft -"@lumino/keyboard-capture@^2.0.0, @lumino/keyboard-capture@workspace:packages/keyboard-capture": - version: 0.0.0-use.local - resolution: "@lumino/keyboard-capture@workspace:packages/keyboard-capture" - dependencies: - "@lumino/keyboard": ^2.0.2 - "@lumino/messaging": ^2.0.2 - "@lumino/signaling": ^2.1.3 - "@lumino/widgets": ^2.6.0 - "@microsoft/api-extractor": ^7.36.0 - "@rollup/plugin-commonjs": ^24.0.0 - "@rollup/plugin-node-resolve": ^15.0.1 - "@types/chai": ^3.4.35 - "@types/mocha": ^2.2.39 - "@web/test-runner": ^0.18.2 - "@web/test-runner-playwright": ^0.11.0 - chai: ^4.3.4 - mocha: ^9.0.3 - rimraf: ^5.0.1 - rollup: ^3.25.1 - rollup-plugin-postcss: ^4.0.2 - rollup-plugin-sourcemaps: ^0.6.3 - terser: ^5.18.1 - tslib: ^2.3.0 - typescript: ~5.1.3 - languageName: unknown - linkType: soft - "@lumino/keyboard@^2.0.2, @lumino/keyboard@workspace:packages/keyboard": version: 0.0.0-use.local resolution: "@lumino/keyboard@workspace:packages/keyboard" From 400cc397dfc9dc5f10122b20f4f0e9cf6044847f Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske <510760+vidartf@users.noreply.github.com> Date: Wed, 12 Mar 2025 16:56:39 +0000 Subject: [PATCH 12/15] auto-format --- examples/example-keyboard-capture/src/index.ts | 1 - examples/example-keyboard-capture/src/output.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/example-keyboard-capture/src/index.ts b/examples/example-keyboard-capture/src/index.ts index 594185a30..d22d75e0b 100644 --- a/examples/example-keyboard-capture/src/index.ts +++ b/examples/example-keyboard-capture/src/index.ts @@ -14,7 +14,6 @@ import { OutputWidget } from './output'; import '../style/index.css'; - /** * Initialize the applicaiton. */ diff --git a/examples/example-keyboard-capture/src/output.ts b/examples/example-keyboard-capture/src/output.ts index d191ce935..371b41fa8 100644 --- a/examples/example-keyboard-capture/src/output.ts +++ b/examples/example-keyboard-capture/src/output.ts @@ -79,4 +79,4 @@ export class OutputWidget extends Widget { private _copyButton: HTMLButtonElement; private _clearButton: HTMLButtonElement; private _action = new Signal(this); -} \ No newline at end of file +} From 103134e20ff5bccfc86d50066771d9225a703494 Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske <510760+vidartf@users.noreply.github.com> Date: Wed, 12 Mar 2025 17:56:57 +0000 Subject: [PATCH 13/15] Remove non-US layouts from keyboard package Also simplifies back the modules for a smaller diff, and adjusts and fixes some smaller points. --- packages/keyboard/src/core.ts | 251 ----------- packages/keyboard/src/index.ts | 503 +++++++++++++++++++++- packages/keyboard/src/layouts/en-US.ts | 244 ----------- packages/keyboard/src/layouts/fr-FR.ts | 117 ----- packages/keyboard/src/layouts/index.ts | 6 - packages/keyboard/src/layouts/nb-NO.ts | 123 ------ packages/keyboard/src/special-keys.ts | 9 +- packages/keyboard/tests/src/index.spec.ts | 231 +++++++++- packages/keyboard/tests/tsconfig.json | 2 +- review/api/keyboard.api.md | 15 +- 10 files changed, 739 insertions(+), 762 deletions(-) delete mode 100644 packages/keyboard/src/core.ts delete mode 100644 packages/keyboard/src/layouts/en-US.ts delete mode 100644 packages/keyboard/src/layouts/fr-FR.ts delete mode 100644 packages/keyboard/src/layouts/index.ts delete mode 100644 packages/keyboard/src/layouts/nb-NO.ts diff --git a/packages/keyboard/src/core.ts b/packages/keyboard/src/core.ts deleted file mode 100644 index 96f5dc131..000000000 --- a/packages/keyboard/src/core.ts +++ /dev/null @@ -1,251 +0,0 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. -/*----------------------------------------------------------------------------- -| Copyright (c) 2014-2017, PhosphorJS Contributors -| -| Distributed under the terms of the BSD 3-Clause License. -| -| The full license is in the file LICENSE, distributed with this software. -|----------------------------------------------------------------------------*/ - -import { SPECIAL_KEYS } from './special-keys'; - -/** - * An object which represents an abstract keyboard layout. - */ -export interface IKeyboardLayout { - /** - * The human readable name of the layout. - * - * This value is used primarily for display and debugging purposes. - */ - readonly name: string; - - /** - * Get an array of all key values supported by the layout. - * - * @returns A new array of the supported key values. - * - * #### Notes - * This can be useful for authoring tools and debugging, when it's - * necessary to know which keys are available for shortcut use. - */ - keys(): string[]; - - /** - * Test whether the given key is a valid value for the layout. - * - * @param key - The user provided key to test for validity. - * - * @returns `true` if the key is valid, `false` otherwise. - */ - isValidKey(key: string): boolean; - - /** - * Test whether the given key is a modifier key. - * - * @param key - The user provided key. - * - * @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. - */ - isModifierKey(key: string): boolean; - - /** - * Get the key for a `'keydown'` event. - * - * @param event - The event object for a `'keydown'` event. - * - * @returns The associated key value, or an empty string if the event - * does not represent a valid primary key. - */ - keyForKeydownEvent(event: KeyboardEvent): string; -} - -/** - * A concrete implementation of {@link IKeyboardLayout} based on keycodes. - * - * The `keyCode` property of a `'keydown'` event is a browser and OS - * specific representation of the physical key (not character) which - * was pressed on a keyboard. While not the most convenient API, it - * is currently the only one which works reliably on all browsers. - * - * This class accepts a user-defined mapping of keycode to key, which - * allows for reliable shortcuts tailored to the user's system. - */ -export class KeycodeLayout implements IKeyboardLayout { - /** - * Construct a new keycode layout. - * - * @param name - The human readable name for the layout. - * - * @param codes - A mapping of keycode to key value. - * - * @param modifierKeys - Array of modifier key names - */ - constructor( - name: string, - keyCodes: KeycodeLayout.CodeMap, - modifierKeys: string[] = [], - codes: KeycodeLayout.ModernCodeMap = {} - ) { - this.name = name; - this._keyCodes = keyCodes; - this._codes = codes; - this._keys = KeycodeLayout.extractKeys(keyCodes, codes); - this._modifierKeys = KeycodeLayout.convertToKeySet(modifierKeys); - } - - /** - * The human readable name of the layout. - */ - readonly name: string; - - /** - * Get an array of the key values supported by the layout. - * - * @returns A new array of the supported key values. - */ - keys(): string[] { - return Object.keys(this._keys); - } - - /** - * Test whether the given key is a valid value for the layout. - * - * @param key - The user provided key to test for validity. - * - * @returns `true` if the key is valid, `false` otherwise. - */ - isValidKey(key: string): boolean { - key = Private.normalizeCtrl(key); - return key in this._keys || Private.isSpecialCharacter(key); - } - - /** - * Test whether the given key is a modifier key. - * - * @param key - The user provided key. - * - * @returns `true` if the key is a modifier key, `false` otherwise. - */ - isModifierKey(key: string): boolean { - key = Private.normalizeCtrl(key); - return key in this._modifierKeys; - } - - /** - * Get the key for a `'keydown'` event. - * - * @param event - The event object for a `'keydown'` event. - * - * @returns The associated key value, or an empty string if - * the event does not represent a valid primary key. - */ - keyForKeydownEvent(event: KeyboardEvent): string { - if ( - event.code !== '' && - event.code !== 'Unidentified' && - event.code in this._codes - ) { - return this._codes[event.code]; - } - return ( - this._keyCodes[event.keyCode] || - (Private.isSpecialCharacter(event.key) ? event.key : '') - ); - } - - private _keys: KeycodeLayout.KeySet; - private _keyCodes: KeycodeLayout.CodeMap; - private _codes: KeycodeLayout.ModernCodeMap; - private _modifierKeys: KeycodeLayout.KeySet; -} - -/** - * The namespace for the `KeycodeLayout` class statics. - */ -export namespace KeycodeLayout { - /** - * A type alias for a keycode map. - */ - export type CodeMap = { readonly [keyCode: number]: string }; - - /** - * A type alias for a code map. - */ - export type ModernCodeMap = { readonly [code: string]: string }; - - /** - * A type alias for a key set. - */ - export type KeySet = { readonly [key: string]: boolean }; - - /** - * Extract the set of keys from a code map. - * - * @param code - The code map of interest. - * - * @returns A set of the keys in the code map. - */ - export function extractKeys( - keyCodes: CodeMap, - codes: ModernCodeMap = {} - ): KeySet { - let keys: any = Object.create(null); - for (let c in keyCodes) { - keys[keyCodes[c]] = true; - } - for (let c in codes) { - keys[codes[c]] = true; - } - return keys as KeySet; - } - - /** - * Convert array of keys to a key set. - * - * @param keys - The array that needs to be converted - * - * @returns A set of the keys in the array. - */ - export function convertToKeySet(keys: string[]): KeySet { - let keySet = Object(null); - for (let i = 0, n = keys.length; i < n; ++i) { - keySet[keys[i]] = true; - } - return keySet; - } -} - -/** - * The namespace for the module implementation details. - */ -namespace Private { - /** - * Whether the key value can be considered a special character. - * - * @param key - The key value that is to be considered - */ - export function isSpecialCharacter(key: string): boolean { - // If the value starts with an uppercase latin character and is followed by one - // or more alphanumeric basic latin characters, it is likely a special key. - return SPECIAL_KEYS.has(key); - } - - /** - * Normalize Ctrl to Control for backwards compatability. - * - * @param key - The key value that is to be normalized - * @returns The normalized key string - */ - export function normalizeCtrl(key: string): string { - return key === 'Ctrl' ? 'Control' : key; - } -} diff --git a/packages/keyboard/src/index.ts b/packages/keyboard/src/index.ts index c13c02f37..d78087490 100644 --- a/packages/keyboard/src/index.ts +++ b/packages/keyboard/src/index.ts @@ -12,15 +12,65 @@ * @module keyboard */ -import { IKeyboardLayout, KeycodeLayout } from './core'; -export { IKeyboardLayout, KeycodeLayout } from './core'; +import { MODIFIER_KEYS, SPECIAL_KEYS } from './special-keys'; -export { EN_US } from './layouts'; -import { EN_US } from './layouts'; -import * as Layouts from './layouts'; -import { MODIFIER_KEYS } from './special-keys'; +/** + * An object which represents an abstract keyboard layout. + */ +export interface IKeyboardLayout { + /** + * The human readable name of the layout. + * + * This value is used primarily for display and debugging purposes. + */ + readonly name: string; + + /** + * Get an array of all key values supported by the layout. + * + * @returns A new array of the supported key values. + * + * #### Notes + * This can be useful for authoring tools and debugging, when it's + * necessary to know which keys are available for shortcut use. + */ + keys(): string[]; -export const KeyboardLayouts = Object.values(Layouts); + /** + * Test whether the given key is a valid value for the layout. + * + * @param key - The user provided key to test for validity. + * + * @returns `true` if the key is valid, `false` otherwise. + */ + isValidKey(key: string): boolean; + + /** + * Test whether the given key is a modifier key. + * + * @param key - The user provided key. + * + * @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. + */ + isModifierKey(key: string): boolean; + + /** + * Get the key for a `'keydown'` event. + * + * @param event - The event object for a `'keydown'` event. + * + * @returns The associated key value, or an empty string if the event + * does not represent a valid primary key. + */ + keyForKeydownEvent(event: KeyboardEvent): string; +} /** * Get the global application keyboard layout instance. @@ -33,6 +83,7 @@ export const KeyboardLayouts = Object.values(Layouts); export function getKeyboardLayout(): IKeyboardLayout { return Private.keyboardLayout; } + /** * Set the global application keyboard layout instance. * @@ -51,6 +102,404 @@ export function setKeyboardLayout(layout: IKeyboardLayout): void { Private.keyboardLayout = layout; } +/** + * A concrete implementation of {@link IKeyboardLayout} based on keycodes. + * + * The `keyCode` property of a `'keydown'` event is a browser and OS + * specific representation of the physical key (not character) which + * was pressed on a keyboard. While not the most convenient API, it + * is currently the only one which works reliably on all browsers. + * + * This class accepts a user-defined mapping of keycode to key, which + * allows for reliable shortcuts tailored to the user's system. + */ +export class KeycodeLayout implements IKeyboardLayout { + /** + * Construct a new keycode layout. + * + * @param name - The human readable name for the layout. + * + * @param keyCodes - A mapping of legacy keycodes to key values. + * + * @param modifierKeys - Array of modifier key names + * + * @param codes - A mapping of modern keycodes to key values. + * + * #### Notes + * The legacy mapping is from KeyboardEvent.keyCode values to key + * strings, while the modern mapping is from KeyboardEvent.code + * values to key strings. While `keyCodes` is required and `codes` + * is optional for API backwards-compatability, it is recommended + * to always pass the modern mapping, and it should then be safe to + * leave the `keyCodes` mapping empty. + */ + constructor( + name: string, + keyCodes: KeycodeLayout.CodeMap, + modifierKeys: string[] = [], + codes: KeycodeLayout.ModernCodeMap = {} + ) { + this.name = name; + this._legacyCodes = keyCodes; + this._modernCodes = codes; + this._keys = KeycodeLayout.extractKeys(keyCodes, codes); + this._modifierKeys = KeycodeLayout.convertToKeySet(modifierKeys); + } + + /** + * The human readable name of the layout. + */ + readonly name: string; + + /** + * Get an array of the key values supported by the layout. + * + * @returns A new array of the supported key values. + */ + keys(): string[] { + return Object.keys(this._keys); + } + + /** + * Test whether the given key is a valid value for the layout. + * + * @param key - The user provided key to test for validity. + * + * @returns `true` if the key is valid, `false` otherwise. + */ + isValidKey(key: string): boolean { + key = Private.normalizeCtrl(key); + return key in this._keys || Private.isSpecialCharacter(key); + } + + /** + * Test whether the given key is a modifier key. + * + * @param key - The user provided key. + * + * @returns `true` if the key is a modifier key, `false` otherwise. + */ + isModifierKey(key: string): boolean { + key = Private.normalizeCtrl(key); + return key in this._modifierKeys; + } + + /** + * Get the key for a `'keydown'` event. + * + * @param event - The event object for a `'keydown'` event. + * + * @returns The associated key value, or an empty string if + * the event does not represent a valid primary key. + */ + keyForKeydownEvent(event: KeyboardEvent): string { + if ( + event.code !== '' && + event.code !== 'Unidentified' && + event.code in this._modernCodes + ) { + return this._modernCodes[event.code]; + } + return ( + this._legacyCodes[event.keyCode] || + (Private.isSpecialCharacter(event.key) ? event.key : '') + ); + } + + private _keys: KeycodeLayout.KeySet; + private _legacyCodes: KeycodeLayout.CodeMap; + private _modernCodes: KeycodeLayout.ModernCodeMap; + private _modifierKeys: KeycodeLayout.KeySet; +} + +/** + * The namespace for the `KeycodeLayout` class statics. + */ +export namespace KeycodeLayout { + /** + * A type alias for a keycode map. + */ + export type CodeMap = { readonly [keyCode: number]: string }; + + /** + * A type alias for a code map. + */ + export type ModernCodeMap = { readonly [code: string]: string }; + + /** + * A type alias for a key set. + */ + export type KeySet = { readonly [key: string]: boolean }; + + /** + * Extract the set of keys from a code map. + * + * @param keyCodes - A legacy code map mapping form event.keyCode to key. + * @param codes - A modern code map mapping from event.code to key. + * + * @returns A set of the keys in the code map. + */ + export function extractKeys( + keyCodes: CodeMap, + codes: ModernCodeMap = {} + ): KeySet { + let keys: any = Object.create(null); + for (let c in keyCodes) { + keys[keyCodes[c]] = true; + } + for (let c in codes) { + keys[codes[c]] = true; + } + return keys as KeySet; + } + + /** + * Convert array of keys to a key set. + * + * @param keys - The array that needs to be converted + * + * @returns A set of the keys in the array. + */ + export function convertToKeySet(keys: string[]): KeySet { + let keySet = Object(null); + for (let i = 0, n = keys.length; i < n; ++i) { + keySet[keys[i]] = true; + } + return keySet; + } +} + +/** + * A keycode-based keyboard layout for US English keyboards. + * + * This layout is valid for the following OS/Browser combinations. + * + * - Windows + * - Chrome + * - Firefox + * - IE + * + * - OSX + * - Chrome + * - Firefox + * - Safari + * + * - Linux + * - Chrome + * - Firefox + * + * Other combinations may also work, but are untested. + */ +export const EN_US: IKeyboardLayout = new KeycodeLayout( + 'en-us', + { + 8: 'Backspace', + 9: 'Tab', + 13: 'Enter', + 16: 'Shift', + 17: 'Control', + 18: 'Alt', + 19: 'Pause', + 27: 'Escape', + 32: 'Space', + 33: 'PageUp', + 34: 'PageDown', + 35: 'End', + 36: 'Home', + 37: 'ArrowLeft', + 38: 'ArrowUp', + 39: 'ArrowRight', + 40: 'ArrowDown', + 45: 'Insert', + 46: 'Delete', + 48: '0', + 49: '1', + 50: '2', + 51: '3', + 52: '4', + 53: '5', + 54: '6', + 55: '7', + 56: '8', + 57: '9', + 59: ';', // firefox + 61: '=', // firefox + 65: 'A', + 66: 'B', + 67: 'C', + 68: 'D', + 69: 'E', + 70: 'F', + 71: 'G', + 72: 'H', + 73: 'I', + 74: 'J', + 75: 'K', + 76: 'L', + 77: 'M', + 78: 'N', + 79: 'O', + 80: 'P', + 81: 'Q', + 82: 'R', + 83: 'S', + 84: 'T', + 85: 'U', + 86: 'V', + 87: 'W', + 88: 'X', + 89: 'Y', + 90: 'Z', + 91: 'Meta', // non-firefox + 93: 'ContextMenu', + 96: '0', // numpad + 97: '1', // numpad + 98: '2', // numpad + 99: '3', // numpad + 100: '4', // numpad + 101: '5', // numpad + 102: '6', // numpad + 103: '7', // numpad + 104: '8', // numpad + 105: '9', // numpad + 106: '*', // numpad + 107: '+', // numpad + 109: '-', // numpad + 110: '.', // numpad + 111: '/', // numpad + 112: 'F1', + 113: 'F2', + 114: 'F3', + 115: 'F4', + 116: 'F5', + 117: 'F6', + 118: 'F7', + 119: 'F8', + 120: 'F9', + 121: 'F10', + 122: 'F11', + 123: 'F12', + 173: '-', // firefox + 186: ';', // non-firefox + 187: '=', // non-firefox + 188: ',', + 189: '-', // non-firefox + 190: '.', + 191: '/', + 192: '`', + 219: '[', + 220: '\\', + 221: ']', + 222: "'", + 224: 'Meta' // firefox + }, + MODIFIER_KEYS, + { + AltLeft: 'Alt', + AltRight: 'Alt', + ArrowDown: 'ArrowDown', + ArrowLeft: 'ArrowLeft', + ArrowRight: 'ArrowRight', + ArrowUp: 'ArrowUp', + Backquote: '`', + Backslash: '\\', + Backspace: 'Backspace', + BracketLeft: '[', + BracketRight: ']', + CapsLock: 'CapsLock', + Comma: ',', + ControlLeft: 'Control', + ControlRight: 'Control', + Delete: 'Delete', + Digit0: '0', + Digit1: '1', + Digit2: '2', + Digit3: '3', + Digit4: '4', + Digit5: '5', + Digit6: '6', + Digit7: '7', + Digit8: '8', + Digit9: '9', + End: 'End', + Equal: '=', + Escape: 'Escape', + F1: 'F1', + F10: 'F10', + F11: 'F11', + F12: 'F12', + F2: 'F2', + F3: 'F3', + F4: 'F4', + F5: 'F5', + F6: 'F6', + F7: 'F7', + F8: 'F8', + F9: 'F9', + Home: 'Home', + Insert: 'Insert', + KeyA: 'A', + KeyB: 'B', + KeyC: 'C', + KeyD: 'D', + KeyE: 'E', + KeyF: 'F', + KeyG: 'G', + KeyH: 'H', + KeyI: 'I', + KeyJ: 'J', + KeyK: 'K', + KeyL: 'L', + KeyM: 'M', + KeyN: 'N', + KeyO: 'O', + KeyP: 'P', + KeyQ: 'Q', + KeyR: 'R', + KeyS: 'S', + KeyT: 'T', + KeyU: 'U', + KeyV: 'V', + KeyW: 'W', + KeyX: 'X', + KeyY: 'Y', + KeyZ: 'Z', + MetaLeft: 'Meta', + MetaRight: 'Meta', + Minus: '-', + NumLock: 'NumLock', + Numpad0: 'Insert', + Numpad1: 'End', + Numpad2: 'ArrowDown', + Numpad3: 'PageDown', + Numpad4: 'ArrowLeft', + Numpad5: 'Clear', + Numpad6: 'ArrowRight', + Numpad7: 'Home', + Numpad8: 'ArrowUp', + Numpad9: 'PageUp', + NumpadAdd: '+', + NumpadDecimal: 'Delete', + NumpadDivide: '/', + NumpadEnter: 'Enter', + NumpadMultiply: '*', + NumpadSubtract: '-', + OSLeft: 'OS', // firefox + OSRight: 'OS', // firefox + PageDown: 'PageDown', + PageUp: 'PageUp', + Pause: 'Pause', + Period: '.', + PrintScreen: 'PrintScreen', + Quote: "'", + Semicolon: ';', + ShiftLeft: 'Shift', + ShiftRight: 'Shift', + Slash: '/', + Tab: 'Tab' + } +); + /** * Whether the browser supports inspecting the keyboard layout. * @@ -67,16 +516,13 @@ export function hasBrowserLayout(): boolean { * @returns Whether the browser supports inspecting the keyboard layout. */ export async function useBrowserLayout(): Promise { - const keyboardApi = (navigator as any)?.keyboard; // avoid updating if already set if (Private.keyboardLayout.name !== Private.INTERNAL_BROWSER_LAYOUT_NAME) { if (!(await Private.updateBrowserLayout())) { return false; } } - if (keyboardApi?.addEventListener) { - keyboardApi.addEventListener('layoutchange', Private.updateBrowserLayout); - } + Private.subscribeBrowserUpdates(); return true; } @@ -94,6 +540,27 @@ namespace Private { */ export const INTERNAL_BROWSER_LAYOUT_NAME = '__lumino-internal-browser'; + /** + * Whether the key value can be considered a special character. + * + * @param key - The key value that is to be considered + */ + export function isSpecialCharacter(key: string): boolean { + // If the value starts with an uppercase latin character and is followed by one + // or more alphanumeric basic latin characters, it is likely a special key. + return SPECIAL_KEYS.has(key); + } + + /** + * Normalize Ctrl to Control for backwards compatability. + * + * @param key - The key value that is to be normalized + * @returns The normalized key string + */ + export function normalizeCtrl(key: string): string { + return key === 'Ctrl' ? 'Control' : key; + } + /** * Polyfill until Object.fromEntries is available. */ @@ -137,7 +604,7 @@ namespace Private { } /** - * Set the active layout to that of the browser at this instant. + * Set the active layout to that of the browser at this moment. */ export async function updateBrowserLayout(): Promise { const initial = await getBrowserKeyboardLayout(); @@ -149,7 +616,17 @@ namespace Private { } /** - * Unsubscribe any browser updates + * Subscribe to any browser updates to keyboard layout + */ + export function subscribeBrowserUpdates(): void { + const keyboardApi = (navigator as any)?.keyboard; + if (keyboardApi?.addEventListener) { + keyboardApi.addEventListener('layoutchange', Private.updateBrowserLayout); + } + } + + /** + * Unsubscribe from any browser updates */ export function unsubscribeBrowserUpdates(): void { const keyboardApi = (navigator as any)?.keyboard; diff --git a/packages/keyboard/src/layouts/en-US.ts b/packages/keyboard/src/layouts/en-US.ts deleted file mode 100644 index c1798b8bf..000000000 --- a/packages/keyboard/src/layouts/en-US.ts +++ /dev/null @@ -1,244 +0,0 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. -/*----------------------------------------------------------------------------- -| Copyright (c) 2014-2017, PhosphorJS Contributors -| -| Distributed under the terms of the BSD 3-Clause License. -| -| The full license is in the file LICENSE, distributed with this software. -|----------------------------------------------------------------------------*/ - -import { type IKeyboardLayout, KeycodeLayout } from '../core'; - -import { MODIFIER_KEYS } from '../special-keys'; - -/** - * A keycode-based keyboard layout for US English keyboards. - * - * This layout is valid for the following OS/Browser combinations. - * - * - Windows - * - Chrome - * - Firefox - * - IE - * - * - OSX - * - Chrome - * - Firefox - * - Safari - * - * - Linux - * - Chrome - * - Firefox - * - * Other combinations may also work, but are untested. - */ -export const EN_US: IKeyboardLayout = new KeycodeLayout( - 'en-US', - { - 8: 'Backspace', - 9: 'Tab', - 13: 'Enter', - 16: 'Shift', - 17: 'Control', - 18: 'Alt', - 19: 'Pause', - 27: 'Escape', - 32: 'Space', - 33: 'PageUp', - 34: 'PageDown', - 35: 'End', - 36: 'Home', - 37: 'ArrowLeft', - 38: 'ArrowUp', - 39: 'ArrowRight', - 40: 'ArrowDown', - 45: 'Insert', - 46: 'Delete', - 48: '0', - 49: '1', - 50: '2', - 51: '3', - 52: '4', - 53: '5', - 54: '6', - 55: '7', - 56: '8', - 57: '9', - 59: ';', // firefox - 61: '=', // firefox - 65: 'A', - 66: 'B', - 67: 'C', - 68: 'D', - 69: 'E', - 70: 'F', - 71: 'G', - 72: 'H', - 73: 'I', - 74: 'J', - 75: 'K', - 76: 'L', - 77: 'M', - 78: 'N', - 79: 'O', - 80: 'P', - 81: 'Q', - 82: 'R', - 83: 'S', - 84: 'T', - 85: 'U', - 86: 'V', - 87: 'W', - 88: 'X', - 89: 'Y', - 90: 'Z', - 91: 'Meta', // non-firefox - 93: 'ContextMenu', - 96: '0', // numpad - 97: '1', // numpad - 98: '2', // numpad - 99: '3', // numpad - 100: '4', // numpad - 101: '5', // numpad - 102: '6', // numpad - 103: '7', // numpad - 104: '8', // numpad - 105: '9', // numpad - 106: '*', // numpad - 107: '+', // numpad - 109: '-', // numpad - 110: '.', // numpad - 111: '/', // numpad - 112: 'F1', - 113: 'F2', - 114: 'F3', - 115: 'F4', - 116: 'F5', - 117: 'F6', - 118: 'F7', - 119: 'F8', - 120: 'F9', - 121: 'F10', - 122: 'F11', - 123: 'F12', - 173: '-', // firefox - 186: ';', // non-firefox - 187: '=', // non-firefox - 188: ',', - 189: '-', // non-firefox - 190: '.', - 191: '/', - 192: '`', - 219: '[', - 220: '\\', - 221: ']', - 222: "'", - 224: 'Meta' // firefox - }, - MODIFIER_KEYS, - { - AltLeft: 'Alt', - AltRight: 'Alt', - ArrowDown: 'ArrowDown', - ArrowLeft: 'ArrowLeft', - ArrowRight: 'ArrowRight', - ArrowUp: 'ArrowUp', - Backquote: '`', - Backslash: '\\', - Backspace: 'Backspace', - BracketLeft: '[', - BracketRight: ']', - CapsLock: 'CapsLock', - Comma: ',', - ControlLeft: 'Control', - ControlRight: 'Control', - Delete: 'Delete', - Digit0: '0', - Digit1: '1', - Digit2: '2', - Digit3: '3', - Digit4: '4', - Digit5: '5', - Digit6: '6', - Digit7: '7', - Digit8: '8', - Digit9: '9', - End: 'End', - Equal: '=', - Escape: 'Escape', - F1: 'F1', - F10: 'F10', - F11: 'F11', - F12: 'F12', - F2: 'F2', - F3: 'F3', - F4: 'F4', - F5: 'F5', - F6: 'F6', - F7: 'F7', - F8: 'F8', - F9: 'F9', - Home: 'Home', - Insert: 'Insert', - KeyA: 'A', - KeyB: 'B', - KeyC: 'C', - KeyD: 'D', - KeyE: 'E', - KeyF: 'F', - KeyG: 'G', - KeyH: 'H', - KeyI: 'I', - KeyJ: 'J', - KeyK: 'K', - KeyL: 'L', - KeyM: 'M', - KeyN: 'N', - KeyO: 'O', - KeyP: 'P', - KeyQ: 'Q', - KeyR: 'R', - KeyS: 'S', - KeyT: 'T', - KeyU: 'U', - KeyV: 'V', - KeyW: 'W', - KeyX: 'X', - KeyY: 'Y', - KeyZ: 'Z', - MetaLeft: 'Meta', - MetaRight: 'Meta', - Minus: '-', - NumLock: 'NumLock', - Numpad0: 'Insert', - Numpad1: 'End', - Numpad2: 'ArrowDown', - Numpad3: 'PageDown', - Numpad4: 'ArrowLeft', - Numpad5: 'Clear', - Numpad6: 'ArrowRight', - Numpad7: 'Home', - Numpad8: 'ArrowUp', - Numpad9: 'PageUp', - NumpadAdd: '+', - NumpadDecimal: 'Delete', - NumpadDivide: '/', - NumpadEnter: 'Enter', - NumpadMultiply: '*', - NumpadSubtract: '-', - OSLeft: 'OS', // firefox - OSRight: 'OS', // firefox - PageDown: 'PageDown', - PageUp: 'PageUp', - Pause: 'Pause', - Period: '.', - PrintScreen: 'PrintScreen', - Quote: "'", - Semicolon: ';', - ShiftLeft: 'Shift', - ShiftRight: 'Shift', - Slash: '/', - Tab: 'Tab' - } -); diff --git a/packages/keyboard/src/layouts/fr-FR.ts b/packages/keyboard/src/layouts/fr-FR.ts deleted file mode 100644 index 2f94cef94..000000000 --- a/packages/keyboard/src/layouts/fr-FR.ts +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -import { IKeyboardLayout, KeycodeLayout } from '../core'; - -import { MODIFIER_KEYS } from '../special-keys'; - -/** - * A code-based keyboard layout for a common Norwegian keyboard. - * - * Note that this does not include Apple's magic Keyboards, as they map - * the keys next to the Enter key differently (BracketRight and - * Backslash on en-US). - */ -export const FR_FR: IKeyboardLayout = new KeycodeLayout( - 'fr-FR', - {}, - MODIFIER_KEYS, - { - AltLeft: 'Alt', - AltRight: 'AltGraph', - ArrowDown: 'ArrowDown', - ArrowLeft: 'ArrowLeft', - ArrowRight: 'ArrowRight', - ArrowUp: 'ArrowUp', - Backquote: '²', - Backslash: '*', - Backspace: 'Backspace', - BracketRight: '$', - Comma: ';', - ControlLeft: 'Control', - ControlRight: 'Control', - Delete: 'Delete', - Digit0: 'À', - Digit1: '&', - Digit2: 'É', - Digit3: '"', - Digit4: "'", - Digit5: '(', - Digit6: '-', - Digit7: 'È', - Digit8: '_', - Digit9: 'Ç', - End: 'End', - Enter: 'Enter', - Equal: '=', - Escape: 'Escape', - F1: 'F1', - F10: 'F10', - F11: 'F11', - F12: 'F12', - F2: 'F2', - F3: 'F3', - F4: 'F4', - F5: 'F5', - F6: 'F6', - F7: 'F7', - F8: 'F8', - F9: 'F9', - Home: 'Home', - Insert: 'Insert', - IntlBackslash: '<', - KeyA: 'Q', - KeyB: 'B', - KeyC: 'C', - KeyD: 'D', - KeyE: 'E', - KeyF: 'F', - KeyG: 'G', - KeyH: 'H', - KeyI: 'I', - KeyJ: 'J', - KeyK: 'K', - KeyL: 'L', - KeyM: ',', - KeyN: 'N', - KeyO: 'O', - KeyP: 'P', - KeyQ: 'A', - KeyR: 'R', - KeyS: 'S', - KeyT: 'T', - KeyU: 'U', - KeyV: 'V', - KeyW: 'Z', - KeyX: 'X', - KeyY: 'Y', - KeyZ: 'W', - Minus: ')', - Numpad0: '0', - Numpad1: '1', - Numpad2: '2', - Numpad3: '3', - Numpad4: '4', - Numpad5: '5', - Numpad6: '6', - Numpad7: '7', - Numpad8: '8', - Numpad9: '9', - NumpadAdd: '+', - NumpadDecimal: '.', - NumpadDivide: '/', - NumpadEnter: 'Enter', - NumpadMultiply: '*', - NumpadSubtract: '-', - PageDown: 'PageDown', - PageUp: 'PageUp', - Period: ':', - Quote: 'Ù', - ScrollLock: 'ScrollLock', - Semicolon: 'M', - ShiftLeft: 'Shift', - ShiftRight: 'Shift', - Slash: '!', - Tab: 'Tab' - } -); diff --git a/packages/keyboard/src/layouts/index.ts b/packages/keyboard/src/layouts/index.ts deleted file mode 100644 index ff6806b4a..000000000 --- a/packages/keyboard/src/layouts/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -export { EN_US } from './en-US'; -export { FR_FR } from './fr-FR'; -export { NB_NO } from './nb-NO'; diff --git a/packages/keyboard/src/layouts/nb-NO.ts b/packages/keyboard/src/layouts/nb-NO.ts deleted file mode 100644 index 35d8034c1..000000000 --- a/packages/keyboard/src/layouts/nb-NO.ts +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -import { IKeyboardLayout, KeycodeLayout } from '../core'; - -import { MODIFIER_KEYS } from '../special-keys'; - -/** - * A code-based keyboard layout for a common Norwegian keyboard. - * - * Note that this does not include Apple's magic Keyboards, as they map - * the keys next to the Enter key differently (BracketRight and - * Backslash on en-US). - */ -export const NB_NO: IKeyboardLayout = new KeycodeLayout( - 'nb-NO', - {}, - MODIFIER_KEYS, - { - AltLeft: 'Alt', - AltRight: 'AltGraph', - Backquote: '|', - Backslash: "'", - Backspace: 'Backspace', - BracketLeft: 'Å', - CapsLock: 'CapsLock', - Comma: ',', - ContextMenu: 'ContextMenu', - ControlLeft: 'Control', - ControlRight: 'Control', - Delete: 'Delete', - Digit0: '0', - Digit1: '1', - Digit2: '2', - Digit3: '3', - Digit4: '4', - Digit5: '5', - Digit6: '6', - Digit7: '7', - Digit8: '8', - Digit9: '9', - End: 'End', - Enter: 'Enter', - Equal: '\\', - Escape: 'Escape', - F1: 'F1', - F10: 'F10', - F11: 'F11', - F12: 'F12', - F2: 'F2', - F3: 'F3', - F4: 'F4', - F5: 'F5', - F6: 'F6', - F7: 'F7', - F8: 'F8', - F9: 'F9', - Home: 'Home', - Insert: 'Insert', - IntlBackslash: '<', - KeyA: 'A', - KeyB: 'B', - KeyC: 'C', - KeyD: 'D', - KeyE: 'E', - KeyF: 'F', - KeyG: 'G', - KeyH: 'H', - KeyI: 'I', - KeyJ: 'J', - KeyK: 'K', - KeyL: 'L', - KeyM: 'M', - KeyN: 'N', - KeyO: 'O', - KeyP: 'P', - KeyQ: 'Q', - KeyR: 'R', - KeyS: 'S', - KeyT: 'T', - KeyU: 'U', - KeyV: 'V', - KeyW: 'W', - KeyX: 'X', - KeyY: 'Y', - KeyZ: 'Z', - MetaLeft: 'Meta', // chrome - MetaRight: 'Meta', // chrome - Minus: '+', - NumLock: 'NumLock', - Numpad0: 'Insert', - Numpad1: 'End', - Numpad2: 'ArrowDown', - Numpad3: 'PageDown', - Numpad4: 'ArrowLeft', - Numpad5: 'Clear', - Numpad6: 'ArrowRight', - Numpad7: 'Home', - Numpad8: 'ArrowUp', - Numpad9: 'PageUp', - NumpadAdd: '+', - NumpadDecimal: 'Delete', - NumpadDivide: '/', - NumpadEnter: 'Enter', - NumpadMultiply: '*', - NumpadSubtract: '-', - OSLeft: 'OS', // firefox - OSRight: 'OS', // firefox - PageDown: 'PageDown', - PageUp: 'PageUp', - Pause: 'Pause', - Period: '.', - PrintScreen: 'PrintScreen', - Quote: 'Æ', - ScrollLock: 'ScrollLock', - Semicolon: 'Ø', - ShiftLeft: 'Shift', - ShiftRight: 'Shift', - Slash: '-', - Space: ' ', - Tab: 'Tab' - } -); diff --git a/packages/keyboard/src/special-keys.ts b/packages/keyboard/src/special-keys.ts index 8f02f0922..30acaf2fc 100644 --- a/packages/keyboard/src/special-keys.ts +++ b/packages/keyboard/src/special-keys.ts @@ -1,6 +1,11 @@ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. +/** + * Known modifier keys. + * + * Ref. https://www.w3.org/TR/uievents-key/#keys-modifier + */ export const MODIFIER_KEYS = [ 'Alt', 'AltGraph', @@ -13,7 +18,9 @@ export const MODIFIER_KEYS = [ 'ScrollLock', 'Shift', 'Symbol', - 'SymbolLock' + 'SymbolLock', + 'Hyper', + 'Super' ]; /** diff --git a/packages/keyboard/tests/src/index.spec.ts b/packages/keyboard/tests/src/index.spec.ts index e6649dfa1..98763c3fa 100644 --- a/packages/keyboard/tests/src/index.spec.ts +++ b/packages/keyboard/tests/src/index.spec.ts @@ -12,13 +12,238 @@ import { expect } from 'chai'; import { EN_US, getKeyboardLayout, - KeyboardLayouts, KeycodeLayout, setKeyboardLayout } from '@lumino/keyboard'; -const FR_FR = KeyboardLayouts.find(layout => layout.name === 'fr-FR'); -const NB_NO = KeyboardLayouts.find(layout => layout.name === 'nb-NO'); +const MODIFIER_KEYS = [ + 'Alt', + 'AltGraph', + 'CapsLock', + 'Control', + 'Fn', + 'FnLock', + 'Meta', + 'NumLock', + 'ScrollLock', + 'Shift', + 'Symbol', + 'SymbolLock' +]; + +/** + * A common Norwegian keyboard layout. + * + * Note that this does not include Apple's magic Keyboards, as they map + * the keys next to the Enter key differently (BracketRight and + * Backslash on en-US). + */ +export const NB_NO = new KeycodeLayout('nb-NO', {}, MODIFIER_KEYS, { + AltLeft: 'Alt', + AltRight: 'AltGraph', + Backquote: '|', + Backslash: "'", + Backspace: 'Backspace', + BracketLeft: 'Å', + CapsLock: 'CapsLock', + Comma: ',', + ContextMenu: 'ContextMenu', + ControlLeft: 'Control', + ControlRight: 'Control', + Delete: 'Delete', + Digit0: '0', + Digit1: '1', + Digit2: '2', + Digit3: '3', + Digit4: '4', + Digit5: '5', + Digit6: '6', + Digit7: '7', + Digit8: '8', + Digit9: '9', + End: 'End', + Enter: 'Enter', + Equal: '\\', + Escape: 'Escape', + F1: 'F1', + F10: 'F10', + F11: 'F11', + F12: 'F12', + F2: 'F2', + F3: 'F3', + F4: 'F4', + F5: 'F5', + F6: 'F6', + F7: 'F7', + F8: 'F8', + F9: 'F9', + Home: 'Home', + Insert: 'Insert', + IntlBackslash: '<', + KeyA: 'A', + KeyB: 'B', + KeyC: 'C', + KeyD: 'D', + KeyE: 'E', + KeyF: 'F', + KeyG: 'G', + KeyH: 'H', + KeyI: 'I', + KeyJ: 'J', + KeyK: 'K', + KeyL: 'L', + KeyM: 'M', + KeyN: 'N', + KeyO: 'O', + KeyP: 'P', + KeyQ: 'Q', + KeyR: 'R', + KeyS: 'S', + KeyT: 'T', + KeyU: 'U', + KeyV: 'V', + KeyW: 'W', + KeyX: 'X', + KeyY: 'Y', + KeyZ: 'Z', + MetaLeft: 'Meta', // chrome + MetaRight: 'Meta', // chrome + Minus: '+', + NumLock: 'NumLock', + Numpad0: 'Insert', + Numpad1: 'End', + Numpad2: 'ArrowDown', + Numpad3: 'PageDown', + Numpad4: 'ArrowLeft', + Numpad5: 'Clear', + Numpad6: 'ArrowRight', + Numpad7: 'Home', + Numpad8: 'ArrowUp', + Numpad9: 'PageUp', + NumpadAdd: '+', + NumpadDecimal: 'Delete', + NumpadDivide: '/', + NumpadEnter: 'Enter', + NumpadMultiply: '*', + NumpadSubtract: '-', + OSLeft: 'OS', // firefox + OSRight: 'OS', // firefox + PageDown: 'PageDown', + PageUp: 'PageUp', + Pause: 'Pause', + Period: '.', + PrintScreen: 'PrintScreen', + Quote: 'Æ', + ScrollLock: 'ScrollLock', + Semicolon: 'Ø', + ShiftLeft: 'Shift', + ShiftRight: 'Shift', + Slash: '-', + Space: ' ', + Tab: 'Tab' +}); + +/** + * A common French keyboard layout + */ +export const FR_FR = new KeycodeLayout('fr-FR', {}, MODIFIER_KEYS, { + AltLeft: 'Alt', + AltRight: 'AltGraph', + ArrowDown: 'ArrowDown', + ArrowLeft: 'ArrowLeft', + ArrowRight: 'ArrowRight', + ArrowUp: 'ArrowUp', + Backquote: '²', + Backslash: '*', + Backspace: 'Backspace', + BracketRight: '$', + Comma: ';', + ControlLeft: 'Control', + ControlRight: 'Control', + Delete: 'Delete', + Digit0: 'À', + Digit1: '&', + Digit2: 'É', + Digit3: '"', + Digit4: "'", + Digit5: '(', + Digit6: '-', + Digit7: 'È', + Digit8: '_', + Digit9: 'Ç', + End: 'End', + Enter: 'Enter', + Equal: '=', + Escape: 'Escape', + F1: 'F1', + F10: 'F10', + F11: 'F11', + F12: 'F12', + F2: 'F2', + F3: 'F3', + F4: 'F4', + F5: 'F5', + F6: 'F6', + F7: 'F7', + F8: 'F8', + F9: 'F9', + Home: 'Home', + Insert: 'Insert', + IntlBackslash: '<', + KeyA: 'Q', + KeyB: 'B', + KeyC: 'C', + KeyD: 'D', + KeyE: 'E', + KeyF: 'F', + KeyG: 'G', + KeyH: 'H', + KeyI: 'I', + KeyJ: 'J', + KeyK: 'K', + KeyL: 'L', + KeyM: ',', + KeyN: 'N', + KeyO: 'O', + KeyP: 'P', + KeyQ: 'A', + KeyR: 'R', + KeyS: 'S', + KeyT: 'T', + KeyU: 'U', + KeyV: 'V', + KeyW: 'Z', + KeyX: 'X', + KeyY: 'Y', + KeyZ: 'W', + Minus: ')', + Numpad0: '0', + Numpad1: '1', + Numpad2: '2', + Numpad3: '3', + Numpad4: '4', + Numpad5: '5', + Numpad6: '6', + Numpad7: '7', + Numpad8: '8', + Numpad9: '9', + NumpadAdd: '+', + NumpadDecimal: '.', + NumpadDivide: '/', + NumpadEnter: 'Enter', + NumpadMultiply: '*', + NumpadSubtract: '-', + PageDown: 'PageDown', + PageUp: 'PageUp', + Period: ':', + Quote: 'Ù', + ScrollLock: 'ScrollLock', + Semicolon: 'M', + ShiftLeft: 'Shift', + ShiftRight: 'Shift', + Slash: '!', + Tab: 'Tab' +}); describe('@lumino/keyboard', () => { describe('getKeyboardLayout()', () => { diff --git a/packages/keyboard/tests/tsconfig.json b/packages/keyboard/tests/tsconfig.json index d2de12617..e72478e93 100644 --- a/packages/keyboard/tests/tsconfig.json +++ b/packages/keyboard/tests/tsconfig.json @@ -6,5 +6,5 @@ "rootDir": "src", "types": ["chai", "mocha"] }, - "include": ["src/*"] + "include": ["src/**/*"] } diff --git a/review/api/keyboard.api.md b/review/api/keyboard.api.md index 0eb5a147c..01d94b09a 100644 --- a/review/api/keyboard.api.md +++ b/review/api/keyboard.api.md @@ -10,6 +10,9 @@ export const EN_US: IKeyboardLayout; // @public export function getKeyboardLayout(): IKeyboardLayout; +// @alpha +export function hasBrowserLayout(): boolean; + // @public export interface IKeyboardLayout { isModifierKey(key: string): boolean; @@ -21,7 +24,7 @@ export interface IKeyboardLayout { // @public export class KeycodeLayout implements IKeyboardLayout { - constructor(name: string, codes: KeycodeLayout.CodeMap, modifierKeys?: string[]); + constructor(name: string, keyCodes: KeycodeLayout.CodeMap, modifierKeys?: string[], codes?: KeycodeLayout.ModernCodeMap); isModifierKey(key: string): boolean; isValidKey(key: string): boolean; keyForKeydownEvent(event: KeyboardEvent): string; @@ -32,16 +35,22 @@ export class KeycodeLayout implements IKeyboardLayout { // @public export namespace KeycodeLayout { export type CodeMap = { - readonly [code: number]: string; + readonly [keyCode: number]: string; }; export function convertToKeySet(keys: string[]): KeySet; - export function extractKeys(codes: CodeMap): KeySet; + export function extractKeys(keyCodes: CodeMap, codes?: ModernCodeMap): KeySet; export type KeySet = { readonly [key: string]: boolean; }; + export type ModernCodeMap = { + readonly [code: string]: string; + }; } // @public export function setKeyboardLayout(layout: IKeyboardLayout): void; +// @alpha +export function useBrowserLayout(): Promise; + ``` From edefffb7bccf8a1828ae8a6e15b1bea0c4bd6159 Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske <510760+vidartf@users.noreply.github.com> Date: Wed, 12 Mar 2025 18:18:38 +0000 Subject: [PATCH 14/15] Re-narrow tsconfig --- packages/keyboard/tests/tsconfig.json | 2 +- packages/keyboard/tsconfig.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/keyboard/tests/tsconfig.json b/packages/keyboard/tests/tsconfig.json index e72478e93..d2de12617 100644 --- a/packages/keyboard/tests/tsconfig.json +++ b/packages/keyboard/tests/tsconfig.json @@ -6,5 +6,5 @@ "rootDir": "src", "types": ["chai", "mocha"] }, - "include": ["src/**/*"] + "include": ["src/*"] } diff --git a/packages/keyboard/tsconfig.json b/packages/keyboard/tsconfig.json index 8a18e322e..8d70721ce 100644 --- a/packages/keyboard/tsconfig.json +++ b/packages/keyboard/tsconfig.json @@ -6,5 +6,5 @@ "outDir": "lib", "rootDir": "src" }, - "include": ["src/**/*"] + "include": ["src/*"] } From 066222ca166f87ec4a5698194f17cf587f8ea0ee Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske <510760+vidartf@users.noreply.github.com> Date: Wed, 26 Mar 2025 13:49:25 +0000 Subject: [PATCH 15/15] Clean up licenses --- examples/example-keyboard-capture/src/capture.ts | 7 ------- examples/example-keyboard-capture/src/index.ts | 7 ------- examples/example-keyboard-capture/src/output.ts | 1 + examples/example-keyboard-capture/style/index.css | 1 - 4 files changed, 1 insertion(+), 15 deletions(-) diff --git a/examples/example-keyboard-capture/src/capture.ts b/examples/example-keyboard-capture/src/capture.ts index c00fbbca4..58dcaad04 100644 --- a/examples/example-keyboard-capture/src/capture.ts +++ b/examples/example-keyboard-capture/src/capture.ts @@ -1,12 +1,5 @@ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. -/*----------------------------------------------------------------------------- -| Copyright (c) 2019, PhosphorJS Contributors -| -| Distributed under the terms of the BSD 3-Clause License. -| -| The full license is in the file LICENSE, distributed with this software. -|----------------------------------------------------------------------------*/ import { KeycodeLayout } from '@lumino/keyboard'; import { Message } from '@lumino/messaging'; diff --git a/examples/example-keyboard-capture/src/index.ts b/examples/example-keyboard-capture/src/index.ts index d22d75e0b..dd960926c 100644 --- a/examples/example-keyboard-capture/src/index.ts +++ b/examples/example-keyboard-capture/src/index.ts @@ -1,12 +1,5 @@ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. -/*----------------------------------------------------------------------------- -| Copyright (c) 2019, PhosphorJS Contributors -| -| Distributed under the terms of the BSD 3-Clause License. -| -| The full license is in the file LICENSE, distributed with this software. -|----------------------------------------------------------------------------*/ import { Panel, Widget } from '@lumino/widgets'; import { CaptureWidget } from './capture'; diff --git a/examples/example-keyboard-capture/src/output.ts b/examples/example-keyboard-capture/src/output.ts index 371b41fa8..f3b0393ec 100644 --- a/examples/example-keyboard-capture/src/output.ts +++ b/examples/example-keyboard-capture/src/output.ts @@ -1,5 +1,6 @@ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. + import { Message } from '@lumino/messaging'; import { ISignal, Signal } from '@lumino/signaling'; import { Widget } from '@lumino/widgets'; diff --git a/examples/example-keyboard-capture/style/index.css b/examples/example-keyboard-capture/style/index.css index 2a53fa752..06c716ffe 100644 --- a/examples/example-keyboard-capture/style/index.css +++ b/examples/example-keyboard-capture/style/index.css @@ -4,7 +4,6 @@ */ /*----------------------------------------------------------------------------- -| Copyright (c) Jupyter Development Team. | Copyright (c) 2014-2017, PhosphorJS Contributors | | Distributed under the terms of the BSD 3-Clause License.