Full Code of avocode/react-shortcuts for AI

master c02d2bff6a73 cached
25 files
54.1 KB
14.5k tokens
23 symbols
1 requests
Download .txt
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`
- `<Shortcut>` 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 `<shortcuts>` can receive it.

```javascript
class App extends React.Component {
  getChildContext() {
    return { shortcuts: shortcutManager }
  }
}

App.childContextTypes = {
  shortcuts: PropTypes.object.isRequired
}
```

#### 5. Require the <shortcuts> component

You need to require the component in the file you want to use shortcuts in.
For example `<TodoItem>`.

```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 (
      <Shortcuts
        name='TODO_ITEM'
        handler={this._handleShortcuts}
      >
        <div>Make something amazing today</div>
      </Shortcuts>
    )
  }
}
```

> The `<Shortcuts>` component creates a `<shortcuts>` 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 `<Shortcuts>` 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
================================================
<!doctype html>
<meta charset="utf-8">

<div id="app"></div>

<script src="/index.js"></script>


================================================
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 (
      <div
        ref={(node) => {
          this._domNode = node
        }}
        tabIndex={this.props.tabIndex}
        className={this.props.className}
      >
        {this.props.children}
      </div>
    )
  }
}


================================================
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('<html><body></body></html>')
    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('<html><body></body></html>')
    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/,
  },
}
Download .txt
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
Download .txt
SYMBOL INDEX (23 symbols across 4 files)

FILE: example/app.js
  method getInitialState (line 18) | getInitialState() {
  method getChildContext (line 22) | getChildContext() {
  method _handleShortcuts (line 26) | _handleShortcuts(command) {
  method _handleShortcuts2 (line 35) | _handleShortcuts2(command) {
  method _handleRoot (line 44) | _handleRoot(command) {
  method _rebind (line 48) | _rebind() {
  method render (line 56) | render() {

FILE: src/component/shortcuts.js
  method componentDidMount (line 42) | componentDidMount() {
  method componentWillUnmount (line 50) | componentWillUnmount() {
  method render (line 220) | render() {

FILE: src/shortcut-manager.js
  class ShortcutManager (line 14) | class ShortcutManager extends EventEmitter {
    method constructor (line 17) | constructor(keymap = {}) {
    method addUpdateListener (line 22) | addUpdateListener(callback) {
    method removeUpdateListener (line 28) | removeUpdateListener(callback) {
    method setKeymap (line 41) | setKeymap(keymap) {
    method extendKeymap (line 48) | extendKeymap(keymap) {
    method getAllShortcuts (line 55) | getAllShortcuts() {
    method getAllShortcutsForPlatform (line 59) | getAllShortcutsForPlatform(platformName) {
    method getAllShortcutsForCurrentPlatform (line 79) | getAllShortcutsForCurrentPlatform() {
    method getShortcuts (line 83) | getShortcuts(componentName) {
    method _parseShortcutKeyName (line 98) | _parseShortcutKeyName(obj, keyName) {
    method findShortcutName (line 113) | findShortcutName(keyName, componentName) {

FILE: test/utils.js
  function fn (line 10) | function fn() { this.a = 1 }
Condensed preview — 25 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (59K chars).
[
  {
    "path": ".babelrc",
    "chars": 163,
    "preview": "{\n  \"presets\": [\"es2015\", \"stage-0\", \"react\"],\n  \"plugins\": [\"add-module-exports\"],\n  \"env\": {\n    \"production\": {\n     "
  },
  {
    "path": ".eslintrc",
    "chars": 2080,
    "preview": "{\n  \"parser\": \"babel-eslint\",\n  \"plugins\": [\n    \"import\",\n    \"react\"\n  ],\n  \"extends\": [ \"airbnb\" ],\n  \"env\": {\n    \"b"
  },
  {
    "path": ".gitignore",
    "chars": 112,
    "preview": "# OS garbage\n.DS_Store\nThumbs.db\n\n# built sources\ndist/\nlib/\n\n# npm stuff\nnode_modules/\nnpm-debug.log\ncoverage/\n"
  },
  {
    "path": ".npmignore",
    "chars": 83,
    "preview": ".babelrc\nexample\ntest\n*.coffee\n*.sh\n*.md\n*.yml\nwebpack.config.js\ncoverage\ndist\nsrc\n"
  },
  {
    "path": ".travis.yml",
    "chars": 36,
    "preview": "language: node_js\nnode_js:\n  - \"12\"\n"
  },
  {
    "path": "LICENSE.md",
    "chars": 1077,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2015 Petr Brzek\n\nPermission is hereby granted, free of charge, to any person obtain"
  },
  {
    "path": "README.md",
    "chars": 5735,
    "preview": "React Shortcuts\n=========\n\n**Manage keyboard shortcuts from one place.**\n\n[![Build Status](https://travis-ci.org/avocode"
  },
  {
    "path": "example/app.js",
    "chars": 2938,
    "preview": "import React from 'react'\nimport PropTypes from 'prop-types'\nimport createClass from 'create-react-class'\nimport ReactDO"
  },
  {
    "path": "example/index.html",
    "chars": 96,
    "preview": "<!doctype html>\n<meta charset=\"utf-8\">\n\n<div id=\"app\"></div>\n\n<script src=\"/index.js\"></script>\n"
  },
  {
    "path": "example/keymap.js",
    "chars": 215,
    "preview": "export default {\n  App: {\n    MOVE_LEFT: 'left',\n    MOVE_RIGHT: 'right',\n    MOVE_UP: ['up', 'w'],\n    DELETE: {\n      "
  },
  {
    "path": "example/main.js",
    "chars": 421,
    "preview": "import React from 'react'\nimport ReactDOM from 'react-dom'\nimport './main.less'\nimport keymap from './keymap'\nimport App"
  },
  {
    "path": "example/main.less",
    "chars": 308,
    "preview": "html {\n  color: #fff;\n  background: #222;\n  line-height: 1.5;\n}\n\n.root {\n  display: flex;\n  justify-content: center;\n  a"
  },
  {
    "path": "package.json",
    "chars": 2280,
    "preview": "{\n  \"name\": \"react-shortcuts\",\n  \"description\": \"React shortcuts\",\n  \"version\": \"2.1.0\",\n  \"license\": \"MIT\",\n  \"main\": \""
  },
  {
    "path": "src/component/index.js",
    "chars": 40,
    "preview": "module.exports = require('./shortcuts')\n"
  },
  {
    "path": "src/component/shortcuts.js",
    "chars": 5858,
    "preview": "import React from 'react'\nimport invariant from 'invariant'\nimport Combokeys from 'combokeys'\nimport PropTypes from 'pro"
  },
  {
    "path": "src/helpers.js",
    "chars": 704,
    "preview": "import platform from 'platform'\n\nconst getPlatformName = () => {\n  let os = platform.os.family || ''\n  os = os.toLowerCa"
  },
  {
    "path": "src/index.js",
    "chars": 109,
    "preview": "module.exports = {\n  ShortcutManager: require('./shortcut-manager'),\n  Shortcuts: require('./component/'),\n}\n"
  },
  {
    "path": "src/shortcut-manager.js",
    "chars": 3233,
    "preview": "import reduce from 'just-reduce-object'\nimport invariant from 'invariant'\nimport { EventEmitter } from 'events'\nimport h"
  },
  {
    "path": "src/utils.js",
    "chars": 1412,
    "preview": "export const isArray = arr => Array.isArray(arr)\n\nexport const isPlainObject = (obj) => {\n  const isObject = typeof obj "
  },
  {
    "path": "test/keymap.js",
    "chars": 530,
    "preview": "export default {\n  'Test': {\n    MOVE_LEFT: 'left',\n    MOVE_RIGHT: 'right',\n    MOVE_UP: ['up', 'w'],\n    DELETE: {\n   "
  },
  {
    "path": "test/mocha.opts",
    "chars": 47,
    "preview": "--compilers js:babel-core/register\n--recursive\n"
  },
  {
    "path": "test/shortcut-manager.spec.js",
    "chars": 5400,
    "preview": "import jsdom from 'jsdom'\nimport chai from 'chai'\nimport _ from 'lodash'\nimport sinonChai from 'sinon-chai'\nimport sinon"
  },
  {
    "path": "test/shortcuts.spec.js",
    "chars": 18589,
    "preview": "import ReactDOMFactories from 'react-dom-factories'\nimport jsdom from 'jsdom'\nimport chai from 'chai'\nimport sinonChai f"
  },
  {
    "path": "test/utils.js",
    "chars": 3144,
    "preview": "import chai from 'chai'\nimport _ from 'lodash'\nimport { isArray, isPlainObject, findKey, compact, flatten, map } from '."
  },
  {
    "path": "webpack.config.js",
    "chars": 771,
    "preview": "const webpack = require('webpack')\n\nmodule.exports = {\n  entry: [\n    'webpack-dev-server/client?http://localhost:8080',"
  }
]

About this extraction

This page contains the full source code of the avocode/react-shortcuts GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 25 files (54.1 KB), approximately 14.5k tokens, and a symbol index with 23 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!