diff --git a/examples/example-keyboard-capture/index.html b/examples/example-keyboard-capture/index.html
new file mode 100644
index 000000000..45db1d491
--- /dev/null
+++ b/examples/example-keyboard-capture/index.html
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/examples/example-keyboard-capture/package.json b/examples/example-keyboard-capture/package.json
new file mode 100644
index 000000000..6ef6fc53d
--- /dev/null
+++ b/examples/example-keyboard-capture/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "@lumino/example-keyboard-capture",
+ "version": "1.0.0",
+ "private": true,
+ "scripts": {
+ "build": "tsc && webpack",
+ "clean": "rimraf build",
+ "start": "node build/server.js"
+ },
+ "dependencies": {
+ "@lumino/keyboard-capture": "^1.0.0",
+ "@lumino/signaling": "^1.10.1",
+ "@lumino/widgets": "^1.31.1",
+ "es6-promise": "^4.0.5"
+ },
+ "devDependencies": {
+ "@lumino/messaging": "^1.10.1",
+ "@types/node": "^10.12.19",
+ "css-loader": "^3.4.0",
+ "file-loader": "^5.0.2",
+ "rimraf": "^3.0.2",
+ "style-loader": "^1.0.2",
+ "typescript": "~3.6.0",
+ "webpack": "^4.41.3"
+ }
+}
diff --git a/examples/example-keyboard-capture/src/index.ts b/examples/example-keyboard-capture/src/index.ts
new file mode 100644
index 000000000..4d84895ab
--- /dev/null
+++ b/examples/example-keyboard-capture/src/index.ts
@@ -0,0 +1,133 @@
+// 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();
+ } else {
+ output.value = `${capture.formatMap()}
`;
+ }
+ });
+
+ window.onresize = () => {
+ box.update();
+ };
+ Widget.attach(box, document.body);
+}
+
+window.onload = init;
diff --git a/examples/example-keyboard-capture/src/server.ts b/examples/example-keyboard-capture/src/server.ts
new file mode 100644
index 000000000..426256a6e
--- /dev/null
+++ b/examples/example-keyboard-capture/src/server.ts
@@ -0,0 +1,55 @@
+// 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 * as fs from 'fs';
+
+import * as http from 'http';
+
+import * as path from 'path';
+
+/**
+ * Create a HTTP static file server for serving the static
+ * assets to the user.
+ */
+let server = http.createServer((request, response) => {
+ console.log('request starting...');
+
+ let filePath = '.' + request.url;
+ if (filePath == './') {
+ filePath = './index.html';
+ }
+
+ let extname = path.extname(filePath);
+ let contentType = 'text/html';
+ switch (extname) {
+ case '.js':
+ contentType = 'text/javascript';
+ break;
+ case '.css':
+ contentType = 'text/css';
+ break;
+ }
+
+ fs.readFile(filePath, (error, content) => {
+ if (error) {
+ console.error(`Could not find file: ${filePath}`);
+ response.writeHead(404, { 'Content-Type': contentType });
+ response.end();
+ } else {
+ response.writeHead(200, { 'Content-Type': contentType });
+ response.end(content, 'utf-8');
+ }
+ });
+});
+
+// Start the server
+server.listen(8000, () => {
+ console.info(new Date() + ' Page server is listening on port 8000');
+});
diff --git a/examples/example-keyboard-capture/style/index.css b/examples/example-keyboard-capture/style/index.css
new file mode 100644
index 000000000..0571f106c
--- /dev/null
+++ b/examples/example-keyboard-capture/style/index.css
@@ -0,0 +1,53 @@
+/*-----------------------------------------------------------------------------
+| 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..c622114cd
--- /dev/null
+++ b/examples/example-keyboard-capture/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "compilerOptions": {
+ "declaration": false,
+ "noImplicitAny": true,
+ "noEmitOnError": true,
+ "noUnusedLocals": true,
+ "strictNullChecks": true,
+ "inlineSourceMap": true,
+ "module": "commonjs",
+ "moduleResolution": "node",
+ "target": "ES5",
+ "outDir": "./build",
+ "lib": [
+ "ES5",
+ "es2015.collection",
+ "DOM",
+ "ES2015.Promise",
+ "ES2015.Iterable"
+ ]
+ },
+ "include": ["src/*"]
+}
diff --git a/examples/example-keyboard-capture/webpack.config.js b/examples/example-keyboard-capture/webpack.config.js
new file mode 100644
index 000000000..13b21c9e3
--- /dev/null
+++ b/examples/example-keyboard-capture/webpack.config.js
@@ -0,0 +1,18 @@
+var path = require('path');
+
+module.exports = {
+ entry: './build/index.js',
+ mode: 'development',
+ output: {
+ path: __dirname + '/build/',
+ filename: 'bundle.example.js',
+ publicPath: './build/'
+ },
+ module: {
+ rules: [
+ { test: /\.css$/, use: ['style-loader', 'css-loader'] },
+ { test: /\.png$/, use: 'file-loader' }
+ ]
+ },
+ plugins: []
+};
diff --git a/packages/keyboard-capture/package.json b/packages/keyboard-capture/package.json
new file mode 100644
index 000000000..7a661da45
--- /dev/null
+++ b/packages/keyboard-capture/package.json
@@ -0,0 +1,80 @@
+{
+ "name": "@lumino/keyboard-capture",
+ "version": "1.0.0",
+ "description": "Lumino Keyboard",
+ "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": "Vidar T. Fauske ",
+ "contributors": [
+ "Vidar T. Fauske "
+ ],
+ "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": "npm run build:src && rollup -c",
+ "build:src": "tsc --build",
+ "build:test": "tsc --build tests && cd tests && webpack",
+ "capture": "node lib/capture.js",
+ "clean": "rimraf ./lib && rimraf *.tsbuildinfo && rimraf ./types && rimraf ./dist",
+ "clean:test": "rimraf tests/build",
+ "docs": "typedoc --options tdoptions.json src",
+ "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": "npm run test:firefox-headless",
+ "test:chrome": "cd tests && karma start --browsers=Chrome",
+ "test:chrome-headless": "cd tests && karma start --browsers=ChromeHeadless",
+ "test:firefox": "cd tests && karma start --browsers=Firefox",
+ "test:firefox-headless": "cd tests && karma start --browsers=FirefoxHeadless",
+ "test:ie": "cd tests && karma start --browsers=IE",
+ "watch": "tsc --build --watch"
+ },
+ "dependencies": {
+ "@lumino/keyboard": "^1.8.1",
+ "@lumino/signaling": "^1.10.1",
+ "@lumino/widgets": "^1.31.1"
+ },
+ "devDependencies": {
+ "@lumino/messaging": "^1.10.1",
+ "@microsoft/api-extractor": "^7.6.0",
+ "@types/chai": "^3.4.35",
+ "@types/mocha": "^2.2.39",
+ "chai": "^4.3.4",
+ "karma": "^6.3.4",
+ "karma-chrome-launcher": "^3.1.0",
+ "karma-firefox-launcher": "^2.1.1",
+ "karma-ie-launcher": "^1.0.0",
+ "karma-mocha": "^2.0.1",
+ "karma-mocha-reporter": "^2.2.5",
+ "mocha": "^9.0.3",
+ "rimraf": "^3.0.2",
+ "rollup": "^2.56.0",
+ "rollup-plugin-node-resolve": "^5.2.0",
+ "rollup-plugin-postcss": "^4.0.0",
+ "rollup-plugin-sourcemaps": "^0.6.3",
+ "simulate-event": "^1.4.0",
+ "terser": "^5.7.1",
+ "tslib": "^2.3.0",
+ "typedoc": "~0.15.0",
+ "typescript": "~3.6.0",
+ "webpack": "^4.41.3",
+ "webpack-cli": "^3.3.10"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/packages/keyboard-capture/rollup.config.js b/packages/keyboard-capture/rollup.config.js
new file mode 100644
index 000000000..d89401005
--- /dev/null
+++ b/packages/keyboard-capture/rollup.config.js
@@ -0,0 +1,40 @@
+import nodeResolve from 'rollup-plugin-node-resolve';
+import sourcemaps from 'rollup-plugin-sourcemaps';
+import postcss from 'rollup-plugin-postcss';
+
+const pkg = require('./package.json');
+
+const globals = id =>
+ id.indexOf('@lumino/') === 0 ? id.replace('@lumino/', 'lumino_') : id;
+
+export default [
+ {
+ input: 'lib/index',
+ external: id => pkg.dependencies && !!pkg.dependencies[id],
+ output: [
+ {
+ file: pkg.main,
+ globals,
+ format: 'umd',
+ sourcemap: true,
+ name: pkg.name
+ },
+ {
+ file: pkg.module + '.js',
+ format: 'es',
+ sourcemap: true,
+ name: pkg.name
+ }
+ ],
+ plugins: [
+ nodeResolve({
+ preferBuiltins: true
+ }),
+ sourcemaps(),
+ postcss({
+ extensions: ['.css'],
+ minimize: true
+ })
+ ]
+ }
+];
diff --git a/packages/keyboard-capture/src/capture.ts b/packages/keyboard-capture/src/capture.ts
new file mode 100644
index 000000000..5301ec639
--- /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._codeMap,
+ Array.from(this._modifierKeys)
+ );
+ }
+
+ 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') {
+ // TODO: Prompt for a key on Dead
+ console.log('Dead', 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/tdoptions.json b/packages/keyboard-capture/tdoptions.json
new file mode 100644
index 000000000..77a0bf868
--- /dev/null
+++ b/packages/keyboard-capture/tdoptions.json
@@ -0,0 +1,17 @@
+{
+ "excludeNotExported": true,
+ "mode": "file",
+ "target": "es5",
+ "module": "es5",
+ "lib": [
+ "lib.es2015.d.ts",
+ "lib.es2015.collection.d.ts",
+ "lib.es2015.promise.d.ts",
+ "lib.dom.d.ts"
+ ],
+ "out": "../../docs/source/api/keyboard",
+ "baseUrl": ".",
+ "paths": {
+ "@lumino/*": ["node_modules/@lumino/*"]
+ }
+}
diff --git a/packages/keyboard-capture/tests/karma.conf.js b/packages/keyboard-capture/tests/karma.conf.js
new file mode 100644
index 000000000..e1289141e
--- /dev/null
+++ b/packages/keyboard-capture/tests/karma.conf.js
@@ -0,0 +1,14 @@
+module.exports = function (config) {
+ config.set({
+ basePath: '.',
+ frameworks: ['mocha'],
+ reporters: ['mocha'],
+ files: ['build/bundle.test.js'],
+ port: 9876,
+ colors: true,
+ singleRun: true,
+ browserNoActivityTimeout: 30000,
+ failOnEmptyTestSuite: false,
+ logLevel: config.LOG_INFO
+ });
+};
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..46bf88f7e
--- /dev/null
+++ b/packages/keyboard-capture/tests/src/index.spec.ts
@@ -0,0 +1,147 @@
+// 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 { generate } from 'simulate-event';
+
+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 = generate('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 = generate('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..adb3fe733
--- /dev/null
+++ b/packages/keyboard-capture/tests/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "compilerOptions": {
+ "declaration": false,
+ "noImplicitAny": true,
+ "noEmitOnError": true,
+ "noUnusedLocals": true,
+ "strictNullChecks": true,
+ "module": "commonjs",
+ "moduleResolution": "node",
+ "target": "ES5",
+ "outDir": "build",
+ "lib": ["ES5", "DOM"],
+ "types": ["chai", "mocha"],
+ "rootDir": "src"
+ },
+ "include": ["src/*"]
+}
diff --git a/packages/keyboard-capture/tests/webpack.config.js b/packages/keyboard-capture/tests/webpack.config.js
new file mode 100644
index 000000000..f16ebd4a8
--- /dev/null
+++ b/packages/keyboard-capture/tests/webpack.config.js
@@ -0,0 +1,10 @@
+var path = require('path');
+
+module.exports = {
+ entry: './build/index.spec.js',
+ mode: 'development',
+ output: {
+ filename: './build/bundle.test.js',
+ path: path.resolve(__dirname)
+ }
+};
diff --git a/packages/keyboard-capture/tsconfig.json b/packages/keyboard-capture/tsconfig.json
new file mode 100644
index 000000000..379190f55
--- /dev/null
+++ b/packages/keyboard-capture/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "sourceMap": true,
+ "declaration": true,
+ "declarationDir": "./types",
+ "declarationMap": true,
+ "noImplicitAny": true,
+ "noEmitOnError": true,
+ "noUnusedLocals": true,
+ "strictNullChecks": true,
+ "module": "ES6",
+ "moduleResolution": "node",
+ "target": "es5",
+ "outDir": "lib",
+ "lib": ["ES5", "DOM", "ES2015.Iterable", "ES2015.Collection"],
+ "importHelpers": true,
+ "types": [],
+ "rootDir": "src"
+ },
+ "include": ["src/*"]
+}
diff --git a/packages/keyboard/src/core.ts b/packages/keyboard/src/core.ts
new file mode 100644
index 000000000..f3700b0ef
--- /dev/null
+++ b/packages/keyboard/src/core.ts
@@ -0,0 +1,239 @@
+// 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 [[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 {
+ 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 {
+ 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.indexOf(key) !== -1;
+ }
+}
diff --git a/packages/keyboard/src/index.ts b/packages/keyboard/src/index.ts
index 3e3cd5d01..acf9e6f06 100644
--- a/packages/keyboard/src/index.ts
+++ b/packages/keyboard/src/index.ts
@@ -8,63 +8,11 @@
| 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;
+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.
@@ -91,263 +39,6 @@ export function setKeyboardLayout(layout: IKeyboardLayout): void {
Private.keyboardLayout = layout;
}
-/**
- * A concrete implementation of [[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 code - 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..669d32519
--- /dev/null
+++ b/packages/keyboard/src/layouts/en-US.ts
@@ -0,0 +1,245 @@
+// 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 { 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
+ },
+ // 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/layouts/index.ts b/packages/keyboard/src/layouts/index.ts
new file mode 100644
index 000000000..c13cdc4e3
--- /dev/null
+++ b/packages/keyboard/src/layouts/index.ts
@@ -0,0 +1,5 @@
+// Copyright (c) Jupyter Development Team.
+// 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'
+ }
+);
diff --git a/packages/keyboard/src/special-keys.ts b/packages/keyboard/src/special-keys.ts
new file mode 100644
index 000000000..a69a5b83b
--- /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 = [
+ '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 46bf88f7e..7a614472c 100644
--- a/packages/keyboard/tests/src/index.spec.ts
+++ b/packages/keyboard/tests/src/index.spec.ts
@@ -52,20 +52,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);
@@ -99,6 +105,47 @@ 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 = generate('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 = generate('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 = generate('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 = generate('keydown', {
+ code: 'Unidentified',
+ key: 'MediaPlayPause',
+ keyCode: 100
+ });
+ let key = layout.keyForKeydownEvent(event as KeyboardEvent);
+ expect(key).to.equal('F');
+ });
});
describe('.extractKeys()', () => {
@@ -132,14 +179,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 17f720517..5a55a2eda 100644
--- a/packages/keyboard/tsconfig.json
+++ b/packages/keyboard/tsconfig.json
@@ -18,5 +18,5 @@
"types": [],
"rootDir": "src"
},
- "include": ["src/*"]
+ "include": ["src/**/*"]
}