Repository: avocode/react-shortcuts Branch: master Commit: c02d2bff6a73 Files: 25 Total size: 54.1 KB Directory structure: gitextract_gjvnrzkr/ ├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── example/ │ ├── app.js │ ├── index.html │ ├── keymap.js │ ├── main.js │ └── main.less ├── package.json ├── src/ │ ├── component/ │ │ ├── index.js │ │ └── shortcuts.js │ ├── helpers.js │ ├── index.js │ ├── shortcut-manager.js │ └── utils.js ├── test/ │ ├── keymap.js │ ├── mocha.opts │ ├── shortcut-manager.spec.js │ ├── shortcuts.spec.js │ └── utils.js └── webpack.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "presets": ["es2015", "stage-0", "react"], "plugins": ["add-module-exports"], "env": { "production": { "presets": ["react-optimize"] } } } ================================================ FILE: .eslintrc ================================================ { "parser": "babel-eslint", "plugins": [ "import", "react" ], "extends": [ "airbnb" ], "env": { "browser": true, "mocha": true, "node": true }, "rules": { "array-bracket-spacing": 0, "arrow-body-style": 0, "comma-dangle": [ 2, "always-multiline" ], "consistent-return": 0, "default-case": 0, "dot-notation": 0, "func-names": 0, "global-require": 0, "import/default": 2, "import/export": 2, "import/imports-first": 0, "import/named": 2, "import/namespace": 2, "import/no-extraneous-dependencies": [ "warn", { "devDependencies": true } ], "import/no-unresolved": [ "error", { "commonjs": true, "amd": true } ], "import/prefer-default-export": 1, "jsx-quotes": 0, "new-cap": 0, "max-len": 0, "no-console": 0, "no-fallthrough": 1, "no-global-assign": 0, "no-irregular-whitespace": [ "error", { "skipStrings": true, "skipTemplates": true, "skipRegExps": true } ], "no-lonely-if": 0, "no-param-reassign": 0, "no-shadow": 1, "no-underscore-dangle": 0, "no-unsafe-negation": 0, "no-unused-expressions": 0, "no-unused-vars": [ "warn", { "vars": "all", "args": "none" } ], "no-use-before-define": [ "warn", { "functions": false, "classes": true } ], "quote-props": 0, "react/no-find-dom-node": 1, "react/prop-types": 1, "react/no-did-mount-set-state": 1, "react/no-did-update-set-state": 1, "react/prefer-stateless-function": 0, "react/jsx-curly-spacing": 0, "react/jsx-no-bind": 0, "react/jsx-filename-extension": 0, "semi": [ 2, "never" ], }, "globals": { "require": false, "ga": false }, "settings": { "import/ignore": [ "node_modules", "\\.json$" ], "import/resolver": { "webpack": { "config": "webpack-js.config.js" } }, "import/parser": "babel-eslint", } } ================================================ FILE: .gitignore ================================================ # OS garbage .DS_Store Thumbs.db # built sources dist/ lib/ # npm stuff node_modules/ npm-debug.log coverage/ ================================================ FILE: .npmignore ================================================ .babelrc example test *.coffee *.sh *.md *.yml webpack.config.js coverage dist src ================================================ FILE: .travis.yml ================================================ language: node_js node_js: - "12" ================================================ FILE: LICENSE.md ================================================ The MIT License (MIT) Copyright (c) 2015 Petr Brzek Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ React Shortcuts ========= **Manage keyboard shortcuts from one place.** [![Build Status](https://travis-ci.org/avocode/react-shortcuts.svg)][travis] Intro ------ Managing keyboard shortcuts can sometimes get messy. Or always, if not implemented the right way. Real problems: - You can't easily tell which shortcut is bound to which component - You have to write a lot of boilerplate code (`addEventListeners`, `removeEventListeners`, ...) - Memory leaks are a real problem if components don’t remove their listeners properly - Platform specific shortcuts is another headache - It's more difficult to implement feature like user-defined shortcuts - You can't easily get allthe application shortcuts and display it (e.g. in settings) **React shortcuts to the rescue!** ----------- With `react-shortcuts` you can declaratively manage shortcuts for each one of your React components. **Important parts of React Shortcuts:** - Your `keymap` definition - `ShortcutManager` which handles `keymap` - `` component for handling shortcuts Try online demo ------- [![Edit l40jjo48nl](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/l40jjo48nl) Quick tour ---------- #### 1. `npm install react-shortcuts` #### 2. **Define application shortcuts** Create a new JS, Coffee, JSON or CSON file wherever you want (which probably is your project root). And define the shortcuts for your React component. **Keymap definition** ```json { "Namespace": { "Action": "Shortcut", "Action_2": ["Shortcut", "Shortcut"], "Action_3": { "osx": "Shortcut", "windows": ["Shortcut", "Shortcut"], "linux": "Shortcut", "other": "Shortcut" } } } ``` - `Namespace` should ideally be the component’s `displayName`. - `Action` describes what will be happening. For example `MODAL_CLOSE`. - `Keyboard shortcut` can be a string, array of strings or an object which specifies platform differences (Windows, OSX, Linux, other). The shortcut may be composed of single keys (`a`, `6`,…), combinations (`command+shift+k`) or sequences (`up up down down left right left right B A`). > **Combokeys** is used under the hood for handling the shortcuts. [Read more][mousetrap] about how you can specify keys. ##### Example `keymap` definition: ```javascript export default { TODO_ITEM: { MOVE_LEFT: 'left', MOVE_RIGHT: 'right', MOVE_UP: ['up', 'w'], DELETE: { osx: ['command+backspace', 'k'], windows: 'delete', linux: 'delete', }, }, } ``` Save this file as `keymap.[js|coffee|json|cson]` and require it into your main file. ```javascript import keymap from './keymap' ``` #### 3. Rise of the ShortcutsManager Define your keymap in whichever supported format but in the end it must be an object. `ShortcutsManager` can’t parse JSON and will certainly not be happy about the situation. ```javascript import keymap from './keymap' import { ShortcutManager } from 'react-shortcuts' const shortcutManager = new ShortcutManager(keymap) // Or like this const shortcutManager = new ShortcutManager() shortcutManager.setKeymap(keymap) ``` #### 4. Include `shortcutManager` into getChildContext of some parent component. So that `` can receive it. ```javascript class App extends React.Component { getChildContext() { return { shortcuts: shortcutManager } } } App.childContextTypes = { shortcuts: PropTypes.object.isRequired } ``` #### 5. Require the component You need to require the component in the file you want to use shortcuts in. For example ``. ```javascript import { Shortcuts } from `react-shortcuts` class TodoItem extends React.Component { _handleShortcuts = (action, event) => { switch (action) { case 'MOVE_LEFT': console.log('moving left') break case 'MOVE_RIGHT': console.log('moving right') break case 'MOVE_UP': console.log('moving up') break case 'COPY': console.log('copying stuff') break } } render() { return (
Make something amazing today
) } } ``` > The `` component creates a `` element in HTML, binds listeners and adds tabIndex to the element so that it’s focusable. `_handleShortcuts` is invoked when some of the defined shortcuts fire. ## Custom props for `` component - `handler`: func - callback function that will fire when a shortcut occurs - `name`: string - The name of the namespace specified in keymap file - `tabIndex`: number - Default is `-1` - `className`: string - `eventType`: string - Just for gourmets (keyup, keydown, keypress) - `stopPropagation`: bool - `preventDefault`: bool - `targetNodeSelector`: DOM Node Selector like `body` or `.my-class` - Use this one with caution. It binds listeners to the provided string instead of the component. - `global`: bool - Use this when you have some global app wide shortcuts like `CMD+Q`. - `isolate`: bool - Use this when a child component has React's key handler (onKeyUp, onKeyPress, onKeyDown). Otherwise, React Shortcuts stops propagation of that event due to nature of event delegation that React uses internally. - `alwaysFireHandler`: bool - Use this when you want events keep firing on the focused input elements. ## Thanks, Atom This library is inspired by [Atom Keymap]. [Atom Keymap]: https://github.com/atom/atom-keymap/ [travis]: https://travis-ci.org/avocode/react-shortcuts [mousetrap]: https://craig.is/killing/mice [keymaps]: https://github.com/atom/atom-keymap/ ================================================ FILE: example/app.js ================================================ import React from 'react' import PropTypes from 'prop-types' import createClass from 'create-react-class' import ReactDOMFactories from 'react-dom-factories' let { Shortcuts } = require('../src') Shortcuts = React.createFactory(Shortcuts) const { button, div, h1, p } = ReactDOMFactories export default createClass({ displayName: 'App', childContextTypes: { shortcuts: PropTypes.object.isRequired, }, getInitialState() { return { show: true, who: 'Nobody' } }, getChildContext() { return { shortcuts: this.props.shortcuts } }, _handleShortcuts(command) { switch (command) { case 'MOVE_LEFT': return this.setState({ who: 'Hemingway - left' }) case 'DELETE': return this.setState({ who: 'Hemingway - delete' }) case 'MOVE_RIGHT': return this.setState({ who: 'Hemingway - right' }) case 'MOVE_UP': return this.setState({ who: 'Hemingway - top' }) } }, _handleShortcuts2(command) { switch (command) { case 'MOVE_LEFT': return this.setState({ who: 'Franz Kafka - left' }) case 'DELETE': return this.setState({ who: 'Franz Kafka - delete' }) case 'MOVE_RIGHT': return this.setState({ who: 'Franz Kafka - right' }) case 'MOVE_UP': return this.setState({ who: 'Franz Kafka - top' }) } }, _handleRoot(command) { this.setState({ who: 'Root shortcuts component' }) }, _rebind() { this.setState({ show: false }) setTimeout(() => { this.setState({ show: true }) }, 100) }, render() { if (!this.state.show) { return null } return ( div({ className: 'root' }, h1({ className: 'who' }, this.state.who), button({ className: 'rebind', onClick: this._rebind }, 'Rebind listeners'), Shortcuts({ name: this.constructor.displayName, handler: this._handleShortcuts, targetNodeSelector: '#app', className: 'content', }, div(null, h1(null, 'Hemingway'), p(null, 'Far far away, behind the word mountains, far from the countries Vokalia and Consonantia, there live the blind texts. Separated they live in Bookmarksgrove right at the coast of the Semantics, a large language ocean. A small river named Duden flows by their place and supplies it with the necessary regelialia.') ) ), Shortcuts({ name: this.constructor.displayName, handler: this._handleShortcuts2, stopPropagation: true, className: 'content', }, div(null, h1(null, 'Franz Kafka'), p(null, 'One morning, when Gregor Samsa woke from troubled dreams, he found himself transformed in his bed into a horrible vermin. He lay on his armour-like back, and if he lifted his head a little he could see his brown belly, slightly domed and divided by arches into stiff sections.') ) ) ) ) }, }) ================================================ FILE: example/index.html ================================================
================================================ FILE: example/keymap.js ================================================ export default { App: { MOVE_LEFT: 'left', MOVE_RIGHT: 'right', MOVE_UP: ['up', 'w'], DELETE: { osx: ['command+backspace', 'k'], windows: 'delete', linux: 'delete', }, }, } ================================================ FILE: example/main.js ================================================ import React from 'react' import ReactDOM from 'react-dom' import './main.less' import keymap from './keymap' import App from './app' import { ShortcutManager } from '../src' const shortcutManager = new ShortcutManager(keymap) // Just for testing window.shortcutManager = shortcutManager const element = React.createElement(App, { shortcuts: shortcutManager }) ReactDOM.render(element, document.getElementById('app')) ================================================ FILE: example/main.less ================================================ html { color: #fff; background: #222; line-height: 1.5; } .root { display: flex; justify-content: center; align-items: center; flex-direction: column; } .who { text-align: center; } .content { width: 400px; margin: 15px auto; background: #535394; padding: 20px; display: flex; } ================================================ FILE: package.json ================================================ { "name": "react-shortcuts", "description": "React shortcuts", "version": "2.1.0", "license": "MIT", "main": "./lib/", "maintainers": [ { "name": "Petr Brzek", "email": "petr@avocode.com" } ], "keywords": [ "react", "react-component", "keyboard", "shortcuts", "mousetrap" ], "scripts": { "prepublish": "babel src/ -d lib/", "start": "webpack-dev-server --hot --progress --colors", "test": "mocha" }, "dependencies": { "combokeys": "^3.0.1", "events": "^1.0.2", "invariant": "^2.1.0", "just-reduce-object": "^1.0.3", "platform": "^1.3.0", "prop-types": "^15.5.8" }, "peerDependencies": { "react": "^0.14.8 || ^15 || ^16", "react-dom": "^0.14.8 || ^15 || ^16" }, "repository": { "type": "git", "url": "git://github.com/avocode/react-shortcuts.git", "web": "http://github.com/avocode/react-shortcuts" }, "bugs": { "url": "http://github.com/avocode/react-shortcuts/issues" }, "devDependencies": { "babel-cli": "^6.14.0", "babel-core": "^6.14.0", "babel-eslint": "^7.1.1", "babel-loader": "^6.2.5", "babel-plugin-add-module-exports": "^0.2.1", "babel-polyfill": "^6.13.0", "babel-preset-es2015": "^6.14.0", "babel-preset-react": "^6.11.1", "babel-preset-react-optimize": "^1.0.1", "babel-preset-stage-0": "^6.5.0", "chai": "^3.5.0", "chai-enzyme": "^1.0.0-beta.1", "cheerio": "^0.20.0", "create-react-class": "^15.6.3", "css-loader": "^0.15.6", "enzyme": "^3.0.0", "enzyme-adapter-react-16": "^1.15.1", "eslint": "^3.10.2", "eslint-config-airbnb": "^13.0.0", "eslint-import-resolver-webpack": "^0.7.0", "eslint-plugin-import": "^2.2.0", "eslint-plugin-jsx-a11y": "^2.2.3", "eslint-plugin-react": "^6.7.1", "eslint-plugin-standard": "^2.0.1", "istanbul": "^0.3.18", "jsdom": "^8.0.4", "less": "^2.5.1", "less-loader": "^2.2.0", "lodash": "^4.15.0", "mocha": "^2.2.5", "react": "^16", "react-dom": "^16", "react-dom-factories": "^1.0.2", "simulant": "^0.2.2", "sinon": "^1.17.5", "sinon-chai": "^2.8.0", "style-loader": "^0.12.3", "webpack": "^1.11.0", "webpack-dev-server": "^1.10.1" } } ================================================ FILE: src/component/index.js ================================================ module.exports = require('./shortcuts') ================================================ FILE: src/component/shortcuts.js ================================================ import React from 'react' import invariant from 'invariant' import Combokeys from 'combokeys' import PropTypes from 'prop-types' import helpers from '../helpers' export default class extends React.Component { static displayName = 'Shortcuts'; static contextTypes = { shortcuts: PropTypes.object.isRequired, }; static propTypes = { children: PropTypes.node, handler: PropTypes.func, name: PropTypes.string, tabIndex: PropTypes.number, className: PropTypes.string, eventType: PropTypes.string, stopPropagation: PropTypes.bool, preventDefault: PropTypes.bool, targetNodeSelector: PropTypes.string, global: PropTypes.bool, isolate: PropTypes.bool, alwaysFireHandler: PropTypes.bool, }; static defaultProps = { tabIndex: -1, className: null, eventType: null, stopPropagation: true, preventDefault: false, targetNodeSelector: null, global: false, isolate: false, alwaysFireHandler: false, }; componentDidMount() { this._onUpdate() if (this.props.name) { this.context.shortcuts.addUpdateListener(this._onUpdate) } } componentWillUnmount() { this._unbindShortcuts() if (this.props.name) { this.context.shortcuts.removeUpdateListener(this._onUpdate) } if (this.props.global) { const element = this._getElementToBind() element.removeEventListener( 'shortcuts:global', this._customGlobalHandler ) } } // NOTE: combokeys must be instance per component _combokeys = null; _lastEvent = null; _bindShortcuts = (shortcutsArr) => { const element = this._getElementToBind() element.setAttribute('tabindex', this.props.tabIndex) this._combokeys = new Combokeys(element, { storeInstancesGlobally: false }) this._decorateCombokeys() this._combokeys.bind( shortcutsArr, this._handleShortcuts, this.props.eventType ) if (this.props.global) { element.addEventListener('shortcuts:global', this._customGlobalHandler) } }; _customGlobalHandler = (e) => { const { character, modifiers, event } = e.detail let targetNode = null if (this.props.targetNodeSelector) { targetNode = document.querySelector(this.props.targetNodeSelector) } if (e.target !== this._domNode && e.target !== targetNode) { this._combokeys.handleKey(character, modifiers, event, true) } }; _decorateCombokeys = () => { const element = this._getElementToBind() const originalHandleKey = this._combokeys.handleKey.bind(this._combokeys) // NOTE: stopCallback is a method that is called to see // if the keyboard event should fire this._combokeys.stopCallback = (event, domElement, combo) => { const isInputLikeElement = domElement.tagName === 'INPUT' || domElement.tagName === 'SELECT' || domElement.tagName === 'TEXTAREA' || (domElement.contentEditable && domElement.contentEditable === 'true') let isReturnString if (event.key) { isReturnString = event.key.length === 1 } else { isReturnString = Boolean(helpers.getCharacter(event)) } if ( isInputLikeElement && isReturnString && !this.props.alwaysFireHandler ) { return true } return false } this._combokeys.handleKey = ( character, modifiers, event, isGlobalHandler ) => { if ( this._lastEvent && event.timeStamp === this._lastEvent.timeStamp && event.type === this._lastEvent.type ) { return } this._lastEvent = event let isolateOwner = false if (this.props.isolate && !event.__isolateShortcuts) { event.__isolateShortcuts = true isolateOwner = true } if (!isGlobalHandler) { element.dispatchEvent( new CustomEvent('shortcuts:global', { detail: { character, modifiers, event }, bubbles: true, cancelable: true, }) ) } // NOTE: works normally if it's not an isolated event if (!event.__isolateShortcuts) { if (this.props.preventDefault) { event.preventDefault() } if (this.props.stopPropagation && !isGlobalHandler) { event.stopPropagation() } originalHandleKey(character, modifiers, event) return } // NOTE: global shortcuts should work even for an isolated event if (this.props.global || isolateOwner) { originalHandleKey(character, modifiers, event) } } }; _getElementToBind = () => { let element = null if (this.props.targetNodeSelector) { element = document.querySelector(this.props.targetNodeSelector) invariant( element, `Node selector '${this.props.targetNodeSelector}' was not found.` ) } else { element = this._domNode } return element }; _unbindShortcuts = () => { if (this._combokeys) { this._combokeys.detach() this._combokeys.reset() } }; _onUpdate = () => { const shortcutsArr = this.props.name && this.context.shortcuts.getShortcuts(this.props.name) this._unbindShortcuts() this._bindShortcuts(shortcutsArr || []) }; _handleShortcuts = (event, keyName) => { if (this.props.name) { const shortcutName = this.context.shortcuts.findShortcutName( keyName, this.props.name ) if (this.props.handler) { this.props.handler(shortcutName, event) } } }; render() { return (
{ this._domNode = node }} tabIndex={this.props.tabIndex} className={this.props.className} > {this.props.children}
) } } ================================================ FILE: src/helpers.js ================================================ import platform from 'platform' const getPlatformName = () => { let os = platform.os.family || '' os = os.toLowerCase().replace(/ /g, '') if (/\bwin/.test(os)) { os = 'windows' } else if (/darwin|osx/.test(os)) { os = 'osx' } else if (/linux|freebsd|sunos|ubuntu|debian|fedora|redhat|suse/.test(os)) { os = 'linux' } else { os = 'other' } return os } const getCharacter = (event) => { if (event.which == null) { // NOTE: IE return String.fromCharCode(event.keyCode) } else if (event.which !== 0 && event.charCode !== 0) { // NOTE: the rest return String.fromCharCode(event.which) } return null } export default { getPlatformName, getCharacter } ================================================ FILE: src/index.js ================================================ module.exports = { ShortcutManager: require('./shortcut-manager'), Shortcuts: require('./component/'), } ================================================ FILE: src/shortcut-manager.js ================================================ import reduce from 'just-reduce-object' import invariant from 'invariant' import { EventEmitter } from 'events' import helpers from './helpers' import { isPlainObject, findKey, isArray, map, compact, flatten } from './utils' const warning = (text) => { if (process && process.env.NODE_ENV !== 'production') { console.warn(text) } } class ShortcutManager extends EventEmitter { static CHANGE_EVENT = 'shortcuts:update' constructor(keymap = {}) { super() this._keymap = keymap } addUpdateListener(callback) { invariant(callback, 'addUpdateListener: callback argument is not defined or falsy') this.on(ShortcutManager.CHANGE_EVENT, callback) } removeUpdateListener(callback) { this.removeListener(ShortcutManager.CHANGE_EVENT, callback) } _platformName = helpers.getPlatformName() _parseShortcutDescriptor = (item) => { if (isPlainObject(item)) { return item[this._platformName] } return item } setKeymap(keymap) { invariant(keymap, 'setKeymap: keymap argument is not defined or falsy.') this._keymap = keymap this.emit(ShortcutManager.CHANGE_EVENT) } extendKeymap(keymap) { invariant(keymap, 'extendKeymap: keymap argument is not defined or falsy.') this._keymap = Object.assign({}, this._keymap, keymap) this.emit(ShortcutManager.CHANGE_EVENT) } getAllShortcuts() { return this._keymap } getAllShortcutsForPlatform(platformName) { const _transformShortcuts = (shortcuts) => { return reduce(shortcuts, (result, keyName, keyValue) => { if (isPlainObject(keyValue)) { if (keyValue[platformName]) { keyValue = keyValue[platformName] } else { result[keyName] = _transformShortcuts(keyValue) return result } } result[keyName] = keyValue return result }, {}) } return _transformShortcuts(this._keymap) } getAllShortcutsForCurrentPlatform() { return this.getAllShortcutsForPlatform(this._platformName) } getShortcuts(componentName) { invariant(componentName, 'getShortcuts: name argument is not defined or falsy.') const cursor = this._keymap[componentName] if (!cursor) { warning(`getShortcuts: There are no shortcuts with name ${componentName}.`) return } const shortcuts = compact(flatten(map(cursor, this._parseShortcutDescriptor))) return shortcuts } _parseShortcutKeyName(obj, keyName) { const result = findKey(obj, (item) => { if (isPlainObject(item)) { item = item[this._platformName] } if (isArray(item)) { const index = item.indexOf(keyName) if (index >= 0) { item = item[index] } } return item === keyName }) return result } findShortcutName(keyName, componentName) { invariant(keyName, 'findShortcutName: keyName argument is not defined or falsy.') invariant(componentName, 'findShortcutName: componentName argument is not defined or falsy.') const cursor = this._keymap[componentName] const result = this._parseShortcutKeyName(cursor, keyName) return result } } export default ShortcutManager ================================================ FILE: src/utils.js ================================================ export const isArray = arr => Array.isArray(arr) export const isPlainObject = (obj) => { const isObject = typeof obj === 'object' && obj !== null && !isArray(obj) if (!isObject || (obj.toString && obj.toString() !== '[object Object]')) return false const proto = Object.getPrototypeOf(obj) if (proto === null) { return true } const Ctor = Object.prototype.hasOwnProperty.call(proto, 'constructor') && proto.constructor return typeof Ctor === 'function' && Ctor instanceof Ctor && Function.prototype.toString.call(Ctor) === Function.prototype.toString.call(Object) } export const findKey = (obj, fn) => { if (!isPlainObject(obj) && !isArray(obj)) return const keys = Object.keys(obj) return keys.find(key => fn(obj[key])) } export const compact = arr => arr.filter(Boolean) const flattenOnce = (arr, recurse = true) => { return arr.reduce((acc, val) => { if (isArray(val) && recurse) return acc.concat(flattenOnce(val, false)) acc.push(val) return acc }, []) } export const flatten = (arr) => { if (!isArray(arr)) throw new Error('flatten expects an array') return flattenOnce(arr) } export const map = (itr, fn) => { if (isArray(itr)) return itr.map(fn) const results = [] const keys = Object.keys(itr) const len = keys.length for (let i = 0; i < len; i += 1) { const key = keys[i] results.push(fn(itr[key], key)) } return results } ================================================ FILE: test/keymap.js ================================================ export default { 'Test': { MOVE_LEFT: 'left', MOVE_RIGHT: 'right', MOVE_UP: ['up', 'w'], DELETE: { osx: 'alt+backspace', windows: 'delete', linux: 'alt+backspace', other: 'alt+backspace', }, }, 'Next': { OPEN: 'alt+o', ABORT: ['d', 'c'], CLOSE: { osx: ['esc', 'enter'], windows: ['esc', 'enter'], linux: ['esc', 'enter'], other: ['esc', 'enter'], }, }, 'TESTING': { 'OPEN': 'enter', 'CLOSE': 'esc', }, 'NON-EXISTING': {}, } ================================================ FILE: test/mocha.opts ================================================ --compilers js:babel-core/register --recursive ================================================ FILE: test/shortcut-manager.spec.js ================================================ import jsdom from 'jsdom' import chai from 'chai' import _ from 'lodash' import sinonChai from 'sinon-chai' import sinon from 'sinon' import keymap from './keymap' chai.use(sinonChai) const { expect } = chai describe('Shortcut manager', () => { let ShortcutManager = null before(() => { global.document = jsdom.jsdom('') global.window = document.defaultView global.Image = window.Image global.navigator = window.navigator global.CustomEvent = window.CustomEvent ShortcutManager = require('../src').ShortcutManager }) it('should return empty object when calling empty constructor', () => { const manager = new ShortcutManager() expect(manager.getAllShortcuts()).to.be.empty }) it('should return all shortcuts', () => { const manager = new ShortcutManager(keymap) expect(manager.getAllShortcuts()).to.not.be.empty expect(manager.getAllShortcuts()).to.be.equal(keymap) manager.setKeymap({}) expect(manager.getAllShortcuts()).to.be.empty manager.setKeymap(keymap) expect(manager.getAllShortcuts()).to.be.equal(keymap) }) it('should return all shortcuts for the Windows platform', () => { const manager = new ShortcutManager(keymap) const keyMapResult = { 'Test': { MOVE_LEFT: 'left', MOVE_RIGHT: 'right', MOVE_UP: ['up', 'w'], DELETE: 'delete', }, 'Next': { OPEN: 'alt+o', ABORT: ['d', 'c'], CLOSE: ['esc', 'enter'], }, 'TESTING': { 'OPEN': 'enter', 'CLOSE': 'esc', }, 'NON-EXISTING': {}, } expect(manager.getAllShortcutsForPlatform('windows')).to.eql(keyMapResult) }) it('should return all shortcuts for the macOs platform', () => { const manager = new ShortcutManager(keymap) const keyMapResult = { 'Test': { MOVE_LEFT: 'left', MOVE_RIGHT: 'right', MOVE_UP: ['up', 'w'], DELETE: 'alt+backspace', }, 'Next': { OPEN: 'alt+o', ABORT: ['d', 'c'], CLOSE: ['esc', 'enter'], }, 'TESTING': { 'OPEN': 'enter', 'CLOSE': 'esc', }, 'NON-EXISTING': {}, } expect(manager.getAllShortcutsForPlatform('osx')).to.eql(keyMapResult) }) it('should expose the change event type as a static constant', () => expect(ShortcutManager.CHANGE_EVENT).to.exist ) it('should have static CHANGE_EVENT', () => expect(ShortcutManager.CHANGE_EVENT).to.be.equal('shortcuts:update') ) it('should call onUpdate', () => { const manager = new ShortcutManager() const spy = sinon.spy() manager.addUpdateListener(spy) manager.setKeymap({}) expect(spy).to.have.beenCalled }) it('should throw an error when setKeymap is called without arg', () => { const manager = new ShortcutManager(keymap) const error = /setKeymap: keymap argument is not defined or falsy./ expect(manager.setKeymap).to.throw(error) }) it('should extend the keymap', () => { const manager = new ShortcutManager() const newKeymap = { 'TESTING-NAMESPACE': {} } const extendedKeymap = Object.assign({}, keymap, newKeymap) manager.setKeymap(keymap) manager.extendKeymap(newKeymap) expect(manager.getAllShortcuts()).to.eql(extendedKeymap) }) it('should return array of shortcuts', () => { const manager = new ShortcutManager(keymap) let shortcuts = manager.getShortcuts('Test') expect(shortcuts).to.be.an.array let shouldContainStrings = _.every(shortcuts, _.isString) expect(shouldContainStrings).to.be.equal(true) expect(shortcuts.length).to.be.equal(5) shortcuts = manager.getShortcuts('Next') expect(shortcuts).to.be.an.array shouldContainStrings = _.every(shortcuts, _.isString) expect(shouldContainStrings).to.be.equal(true) expect(shortcuts.length).to.be.equal(5) }) it('should not throw an error when getting not existing key from keymap', () => { const manager = new ShortcutManager(keymap) const notExist = () => manager.getShortcuts('NotExist') expect(notExist).to.not.throw() }) it('should return correct key label', () => { const manager = new ShortcutManager() manager.setKeymap(keymap) // Test expect(manager.findShortcutName('alt+backspace', 'Test')).to.be.equal('DELETE') expect(manager.findShortcutName('w', 'Test')).to.be.equal('MOVE_UP') expect(manager.findShortcutName('up', 'Test')).to.be.equal('MOVE_UP') expect(manager.findShortcutName('left', 'Test')).to.be.equal('MOVE_LEFT') expect(manager.findShortcutName('right', 'Test')).to.be.equal('MOVE_RIGHT') // Next expect(manager.findShortcutName('alt+o', 'Next')).to.be.equal('OPEN') expect(manager.findShortcutName('d', 'Next')).to.be.equal('ABORT') expect(manager.findShortcutName('c', 'Next')).to.be.equal('ABORT') expect(manager.findShortcutName('esc', 'Next')).to.be.equal('CLOSE') expect(manager.findShortcutName('enter', 'Next')).to.be.equal('CLOSE') }) it('should throw an error', () => { const manager = new ShortcutManager() const fn = () => manager.findShortcutName('left') expect(manager.findShortcutName).to.throw(/findShortcutName: keyName argument is not defined or falsy./) expect(fn).to.throw(/findShortcutName: componentName argument is not defined or falsy./) }) }) ================================================ FILE: test/shortcuts.spec.js ================================================ import ReactDOMFactories from 'react-dom-factories' import jsdom from 'jsdom' import chai from 'chai' import sinonChai from 'sinon-chai' import sinon from 'sinon' import _ from 'lodash' import enzyme from 'enzyme' import Adapter from 'enzyme-adapter-react-16' import keymap from './keymap' enzyme.configure({ adapter: new Adapter() }) describe('Shortcuts component', () => { let baseProps = null let baseContext = null let simulant = null let ShortcutManager = null let Shortcuts = null let ReactDOM = null let React = null chai.use(sinonChai) const { expect } = chai beforeEach(() => { global.document = jsdom.jsdom('') global.window = document.defaultView global.Image = window.Image global.navigator = window.navigator global.CustomEvent = window.CustomEvent simulant = require('simulant') ReactDOM = require('react-dom') React = require('react') const chaiEnzyme = require('chai-enzyme') chai.use(chaiEnzyme()) ShortcutManager = require('../src').ShortcutManager const shortcutsManager = new ShortcutManager(keymap) Shortcuts = require('../src/').Shortcuts baseProps = { handler: sinon.spy(), name: 'TESTING', className: null, } baseContext = { shortcuts: shortcutsManager } }) it('should render component', () => { const shortcutComponent = React.createElement(Shortcuts, baseProps) const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) expect(wrapper.find('div')).to.have.length(1) }) it('should have a tabIndex of -1 by default', () => { let shortcutComponent = React.createElement(Shortcuts, baseProps) let wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) expect(wrapper.props().tabIndex).to.be.equal(-1) const props = _.assign({}, baseProps, { tabIndex: 42 }) shortcutComponent = React.createElement(Shortcuts, props) wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) expect(wrapper.props().tabIndex).to.be.equal(props.tabIndex) let realTabIndex = ReactDOM.findDOMNode(wrapper.instance()).getAttribute('tabindex') expect(realTabIndex).to.have.equal(String(props.tabIndex)) props.tabIndex = 0 shortcutComponent = React.createElement(Shortcuts, props) wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) expect(wrapper.props().tabIndex).to.be.equal(props.tabIndex) realTabIndex = ReactDOM.findDOMNode(wrapper.instance()).getAttribute('tabindex') expect(realTabIndex).to.have.equal(String(props.tabIndex)) }) it('should not have className by default', () => { const shortcutComponent = React.createElement(Shortcuts, baseProps) const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) expect(wrapper.props().className).to.be.equal(null) }) it('should have className', () => { const props = _.assign({}, baseProps, { className: 'testing' }) const shortcutComponent = React.createElement(Shortcuts, props) const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) expect(wrapper.props().className).to.be.equal('testing') expect(wrapper).to.have.className('testing') }) it('should have isolate prop set to false by default', () => { const shortcutComponent = React.createElement(Shortcuts, baseProps) const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) expect(wrapper.props().isolate).to.be.equal(false) }) it('should NOT store combokeys instances on Combokeys constructor', () => { const shortcutComponent = React.createElement(Shortcuts, baseProps) const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) expect(wrapper.find('Shortcuts').instance()._combokeys.constructor.instances).to.be.empty }) it('should have isolate prop', () => { const props = _.assign({}, baseProps, { isolate: true }) const shortcutComponent = React.createElement(Shortcuts, props) const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) expect(wrapper.props().isolate).to.be.equal(true) }) it('should not have children by default', () => { const shortcutComponent = React.createElement(Shortcuts, baseProps) const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) expect(wrapper.props().children).to.be.equal(undefined) }) it('should have children', () => { const props = _.assign({}, baseProps, { children: ReactDOMFactories.div() }) const shortcutComponent = React.createElement(Shortcuts, props) const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) expect(wrapper).to.contain(ReactDOMFactories.div()) }) it('should have handler prop', () => { const shortcutComponent = React.createElement(Shortcuts, baseProps) const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) expect(wrapper.props().handler).to.be.function }) it('should have name prop', () => { const props = _.assign({}, baseProps, { name: 'TESTING' }) const shortcutComponent = React.createElement(Shortcuts, props) const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) expect(wrapper.props().name).to.be.equal('TESTING') }) it('should not have eventType prop by default', () => { const shortcutComponent = React.createElement(Shortcuts, baseProps) const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) expect(wrapper.props().eventType).to.be.equal(null) }) it('should have eventType prop', () => { const props = _.assign({}, baseProps, { eventType: 'keyUp' }) const shortcutComponent = React.createElement(Shortcuts, props) const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) expect(wrapper.props().eventType).to.be.equal('keyUp') }) it('should have stopPropagation prop by default', () => { const shortcutComponent = React.createElement(Shortcuts, baseProps) const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) expect(wrapper.props().stopPropagation).to.be.equal(true) }) it('should have stopPropagation prop set to false', () => { const props = _.assign({}, baseProps, { stopPropagation: false }) const shortcutComponent = React.createElement(Shortcuts, props) const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) expect(wrapper.props().stopPropagation).to.be.equal(false) }) it('should have preventDefault prop set to false by default', () => { const shortcutComponent = React.createElement(Shortcuts, baseProps) const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) expect(wrapper.props().preventDefault).to.be.equal(false) }) it('should have preventDefault prop set to true', () => { const props = _.assign({}, baseProps, { preventDefault: true }) const shortcutComponent = React.createElement(Shortcuts, props) const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) expect(wrapper.props().preventDefault).to.be.equal(true) }) it('should not have targetNodeSelector prop by default', () => { const shortcutComponent = React.createElement(Shortcuts, baseProps) const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) expect(wrapper.props().targetNodeSelector).to.be.equal(null) }) it('should have targetNode prop', () => { const props = _.assign({}, baseProps, { targetNodeSelector: 'body' }) const shortcutComponent = React.createElement(Shortcuts, props) const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) expect(wrapper.props().targetNodeSelector).to.be.equal('body') }) it('should have global prop set to false by default', () => { const shortcutComponent = React.createElement(Shortcuts, baseProps) const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) expect(wrapper.props().global).to.be.equal(false) }) it('should have global prop set to true', () => { const props = _.assign({}, baseProps, { global: true }) const shortcutComponent = React.createElement(Shortcuts, props) const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) expect(wrapper.props().global).to.be.equal(true) }) it('should fire the handler prop with the correct argument', () => { const shortcutComponent = React.createElement(Shortcuts, baseProps) const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) const node = ReactDOM.findDOMNode(wrapper.instance()) node.focus() const enter = 13 simulant.fire(node, 'keydown', { keyCode: enter }) expect(wrapper.props().handler).to.have.been.calledWith('OPEN') const esc = 27 simulant.fire(node, 'keydown', { keyCode: esc }) expect(wrapper.props().handler).to.have.been.calledWith('CLOSE') }) it('should not fire the handler', () => { const props = _.assign({}, baseProps, { name: 'NON-EXISTING' }) const shortcutComponent = React.createElement(Shortcuts, props) const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) const node = ReactDOM.findDOMNode(wrapper.instance()) node.focus() const enter = 13 simulant.fire(node, 'keydown', { keyCode: enter }) expect(wrapper.props().handler).to.not.have.been.called }) it('should not fire twice when global prop is truthy', () => { const props = _.assign({}, baseProps, { global: true }) const shortcutComponent = React.createElement(Shortcuts, props) const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) const node = ReactDOM.findDOMNode(wrapper.instance()) node.focus() const enter = 13 simulant.fire(node, 'keydown', { keyCode: enter }) expect(wrapper.props().handler).to.have.been.calledOnce }) it('should not fire when the component has been unmounted', () => { const handler = sinon.spy() const shortcutComponent = React.createElement(Shortcuts, { ...baseProps, handler }) const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) const node = ReactDOM.findDOMNode(wrapper.instance()) node.focus() wrapper.unmount() const enter = 13 simulant.fire(node, 'keydown', { keyCode: enter }) expect(handler).to.not.have.been.called }) it.skip('should update the shortcuts and fire the handler', () => { const shortcutComponent = React.createElement(Shortcuts, baseProps) const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) const node = ReactDOM.findDOMNode(wrapper.instance()) node.focus() const space = 32 simulant.fire(node, 'keydown', { keyCode: space }) expect(wrapper.props().handler).to.not.have.been.called const editedKeymap = _.assign({}, keymap, { 'TESTING': { 'SPACE': 'space', }, } ) baseContext.shortcuts.setKeymap(editedKeymap) simulant.fire(node, 'keydown', { keyCode: space }) expect(baseProps.handler).to.have.been.called // NOTE: rollback the previous keymap baseContext.shortcuts.setKeymap(keymap) }) it('should fire the handler from a child input', () => { const props = _.assign({}, baseProps, { children: ReactDOMFactories.input({ type: 'text', className: 'input' }), }) const shortcutComponent = React.createElement(Shortcuts, props) const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) const parentNode = ReactDOM.findDOMNode(wrapper.instance()) const node = parentNode.querySelector('.input') node.focus() const enter = 13 simulant.fire(node, 'keydown', { keyCode: enter, key: 'Enter' }) expect(wrapper.props().handler).to.have.been.called }) it('should fire the handler when using targetNodeSelector', () => { const props = _.assign({}, baseProps, { targetNodeSelector: 'body' }) const shortcutComponent = React.createElement(Shortcuts, props) const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) const enter = 13 simulant.fire(document.body, 'keydown', { keyCode: enter, key: 'Enter' }) expect(wrapper.props().handler).to.have.been.called }) it('should throw and error if targetNodeSelector is not found', () => { const props = _.assign({}, baseProps, { targetNodeSelector: 'non-existing' }) const shortcutComponent = React.createElement(Shortcuts, props) try { enzyme.mount(shortcutComponent, { context: baseContext }) } catch (err) { expect(err).to.match(/Node selector 'non-existing' {2}was not found/) } }) it('should fire the handler from focused input', () => { const props = _.assign({}, baseProps, { alwaysFireHandler: true, children: ReactDOMFactories.input({ type: 'text', className: 'input' }), }) const shortcutComponent = React.createElement(Shortcuts, props) const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) const parentNode = ReactDOM.findDOMNode(wrapper.instance()) const node = parentNode.querySelector('.input') node.focus() const enter = 13 simulant.fire(node, 'keydown', { keyCode: enter }) expect(wrapper.props().handler).to.have.been.called }) describe('Shortcuts component inside Shortcuts component:', () => { it('should not fire parent handler when child handler is fired', () => { const props = _.assign({}, baseProps, { children: React.createElement(Shortcuts, _.assign({}, baseProps, { className: 'test' })), }) const shortcutComponent = React.createElement(Shortcuts, props) const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) const parentNode = ReactDOM.findDOMNode(wrapper.instance()) const node = parentNode.querySelector('.test') node.focus() const enter = 13 simulant.fire(node, 'keydown', { keyCode: enter }) expect(baseProps.handler).to.have.been.calledOnce }) it('should fire parent handler when child handler is fired', () => { const props = _.assign({}, baseProps, { children: React.createElement(Shortcuts, _.assign({}, baseProps, { className: 'test', stopPropagation: false })), }) const shortcutComponent = React.createElement(Shortcuts, props) const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) const parentNode = ReactDOM.findDOMNode(wrapper.instance()) const node = parentNode.querySelector('.test') node.focus() const enter = 13 simulant.fire(node, 'keydown', { keyCode: enter }) expect(baseProps.handler).to.have.been.calledTwice }) it('should fire parent handler when parent handler has global prop', () => { const props = _.assign({}, baseProps, { children: React.createElement(Shortcuts, _.assign({}, baseProps, { className: 'test' })), global: true, }) const shortcutComponent = React.createElement(Shortcuts, props) const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) const parentNode = ReactDOM.findDOMNode(wrapper.instance()) const node = parentNode.querySelector('.test') node.focus() const enter = 13 simulant.fire(node, 'keydown', { keyCode: enter }) expect(baseProps.handler).to.have.been.calledTwice }) it('should fire parent handler but not the child handler', () => { const props = _.assign({}, baseProps, { children: React.createElement(Shortcuts, _.assign({}, baseProps, { name: 'NON-EXISTING', className: 'test' })), global: true, }) const shortcutComponent = React.createElement(Shortcuts, props) const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) const parentNode = ReactDOM.findDOMNode(wrapper.instance()) const node = parentNode.querySelector('.test') node.focus() const enter = 13 simulant.fire(node, 'keydown', { keyCode: enter }) expect(baseProps.handler).to.have.been.calledOnce }) it('should fire for all global components', () => { const props = _.assign({}, baseProps, { children: React.createElement(Shortcuts, _.assign({}, baseProps, { global: true, children: React.createElement(Shortcuts, _.assign({}, baseProps, { name: 'NON-EXISTING', className: 'test' })), })), global: true, }) const shortcutComponent = React.createElement(Shortcuts, props) const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) const parentNode = ReactDOM.findDOMNode(wrapper.instance()) const node = parentNode.querySelector('.test') node.focus() const enter = 13 simulant.fire(node, 'keydown', { keyCode: enter }) expect(baseProps.handler).to.have.been.calledTwice }) it('should not fire parent handler when a child has isolate prop set to true', () => { const childHandlerSpy = sinon.spy() const props = _.assign({}, baseProps, { children: React.createElement(Shortcuts, _.assign({}, baseProps, { className: 'test', isolate: true, handler: childHandlerSpy, })), }) const shortcutComponent = React.createElement(Shortcuts, props) const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) const parentNode = ReactDOM.findDOMNode(wrapper.instance()) const node = parentNode.querySelector('.test') node.focus() const enter = 13 simulant.fire(node, 'keydown', { keyCode: enter }) expect(childHandlerSpy).to.have.been.called expect(baseProps.handler).to.not.have.been.called }) it('should fire parent handler when is global and a child has isolate prop set to true', () => { const props = _.assign({}, baseProps, { global: true, children: React.createElement(Shortcuts, _.assign({}, baseProps, { className: 'test', isolate: true })), }) const shortcutComponent = React.createElement(Shortcuts, props) const wrapper = enzyme.mount(shortcutComponent, { context: baseContext }) const parentNode = ReactDOM.findDOMNode(wrapper.instance()) const node = parentNode.querySelector('.test') node.focus() const enter = 13 simulant.fire(node, 'keydown', { keyCode: enter }) expect(baseProps.handler).to.have.been.called }) }) }) ================================================ FILE: test/utils.js ================================================ import chai from 'chai' import _ from 'lodash' import { isArray, isPlainObject, findKey, compact, flatten, map } from '../src/utils' describe('utils', () => { const { expect } = chai let primitives beforeEach(() => { function fn() { this.a = 1 } primitives = [ ['array'], { object: true }, Object.create(null), 'string', null, undefined, NaN, new Map([[ 1, 'one' ], [ 2, 'two' ]]), new fn(), true, 42, ] }) describe('isArray', () => { it('should be true for arrays', () => { primitives.forEach((val, idx) => { if (idx === 0) { expect(isArray(val)).to.be.true expect(_.isArray(val)).to.be.true } else { expect(isArray(val)).to.be.false expect(_.isArray(val)).to.be.false } }) }) }) describe('isPlainObject', () => { it('should be true for plain objects', () => { primitives.forEach((val, idx) => { if (idx === 1 || idx === 2) { expect(isPlainObject(val)).to.be.true expect(_.isPlainObject(val)).to.be.true } else { expect(isPlainObject(val)).to.be.false expect(_.isPlainObject(val)).to.be.false } }) }) }) describe('findKey', () => { it('should return the matching key', () => { const obj = { simple: 1, obj: { val: 4, }, } const checkOne = val => val === 1 const checkTwo = val => typeof val === 'object' expect(findKey(obj, checkOne)).to.deep.equal(_.findKey(obj, checkOne)) expect(findKey(obj, checkTwo)).to.deep.equal(_.findKey(obj, checkTwo)) }) }) describe('compact', () => { it('removes falsy values', () => { const values = [ true, false, 10, 0, null, undefined, NaN, '', 'false, null, 0, "", undefined, and NaN are falsy', ] expect(compact(values)).to.deep.equal(_.compact(values)) }) }) describe('flatten', () => { it('flattens an array 1 level', () => { const value = [1, [2, [3, [4]], 5, [[[6], 7], 8], 9]] expect(flatten(value)).to.deep.equal(_.flatten(value)) }) }) describe('map', () => { it('should map an array', () => { const values = [1, 2, 3, 4] const mapFn = val => val * 10 expect(map(values, mapFn)).to.deep.equal(_.map(values, mapFn)) expect(map(values, mapFn)).to.deep.equal([10, 20, 30, 40]) // ensure that values array is not mutated expect(values).to.deep.equal([1, 2, 3, 4]) }) it('should map an object', () => { const obj = { one: 1, two: 2, three: 3, } const mapFn = (val, key) => `${key} - ${val * 10}` expect(map(obj, mapFn)).to.deep.equal(_.map(obj, mapFn)) expect(map(obj, mapFn)).to.deep.equal([ 'one - 10', 'two - 20', 'three - 30', ]) // ensure the object was not mutated expect(obj).to.deep.equal({ one: 1, two: 2, three: 3, }) }) }) }) ================================================ FILE: webpack.config.js ================================================ const webpack = require('webpack') module.exports = { entry: [ 'webpack-dev-server/client?http://localhost:8080', 'webpack/hot/dev-server', `${__dirname}/example/main.js`, ], devtool: 'inline-source-map', debug: true, output: { path: `${__dirname}/dist`, filename: 'index.js', }, resolve: { extensions: ['', '.js'], }, resolveLoader: { modulesDirectories: ['node_modules'], }, plugins: [ new webpack.HotModuleReplacementPlugin(), ], module: { loaders: [ { test: /\.less$/, loader: 'style-loader!css-loader!less-loader', }, { test: /\.js$/, exclude: /(node_modules|bower_components)/, loader: 'babel', }, ], noParse: /\.min\.js/, }, }