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/**/*"] }