Full Code of jaywcjlove/hotkeys-js for AI

master b274b620e006 cached
35 files
180.1 KB
51.7k tokens
74 symbols
1 requests
Download .txt
Repository: jaywcjlove/hotkeys-js
Branch: master
Commit: b274b620e006
Files: 35
Total size: 180.1 KB

Directory structure:
gitextract_070mo1hj/

├── .editorconfig
├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       ├── ci.yml
│       └── pr.yml
├── .gitignore
├── .husky/
│   ├── .gitignore
│   └── pre-commit
├── .npmignore
├── LICENSE
├── README-zh.md
├── README.md
├── dist/
│   ├── compatibility-test.html
│   ├── hotkeys-js.js
│   ├── hotkeys-js.umd.cjs
│   └── index.d.ts
├── eslint.config.js
├── package.json
├── src/
│   ├── index.ts
│   ├── types.ts
│   ├── utils.ts
│   └── var.ts
├── test/
│   ├── index.html
│   └── run.test.js
├── tsconfig.json
├── vite.config.ts
└── website/
    ├── App.tsx
    ├── components/
    │   ├── Footer.module.css
    │   └── Footer.tsx
    ├── index.html
    ├── index.tsx
    ├── styles/
    │   ├── index.module.css
    │   └── reset.css
    ├── tsconfig.json
    ├── vite-env.d.ts
    └── vite.config.ts

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
# http://editorconfig.org
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false

[Makefile]
indent_style = tab


================================================
FILE: .github/FUNDING.yml
================================================
github: [jaywcjlove]
#ko_fi: jaywcjlove
#buy_me_a_coffee: jaywcjlove
# custom: ["https://wangchujiang.com/#/sponsor"]


================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
  push:
    branches:
      - master
    # tags:
    #   - v*

env:
  SKIP_PREFLIGHT_CHECK: true

jobs:
  build-deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      id-token: write
    steps:
    - uses: actions/checkout@v6
    - uses: actions/setup-node@v6
      with:
        node-version: 24
        registry-url: 'https://registry.npmjs.org'

    - name: Look Changelog
      uses: jaywcjlove/changelog-generator@main
      with:
        token: ${{ secrets.GITHUB_TOKEN }}
        filter-author: (jaywcjlove|小弟调调™|dependabot\[bot\]|Renovate Bot)
        filter: (^[\s]+?[R|r]elease)|(^[R|r]elease)

    - run: npm install
    - run: npm run build
    - run: npm run test
    - run: cp -rp dist doc
    - run: cp -rp coverage/lcov-report doc

    - name: Extract coverage percentage
      id: coverage
      run: |
        COVERAGE=$(cat coverage/js-coverage.txt)
        echo "coverage=$COVERAGE" >> $GITHUB_OUTPUT
        echo "Coverage: $COVERAGE%"

    - name: Generate Badges
      uses: jaywcjlove/generated-badges@main
      with:
        label: coverage
        status: ${{ steps.coverage.outputs.coverage }}%
        output: doc/coverage.svg

    - name: Generate Contributors Images
      uses: jaywcjlove/github-action-contributors@main
      with:
        filter-author: (renovate\[bot\]|renovate-bot|dependabot\[bot\])
        output: doc/CONTRIBUTORS.svg
        avatarSize: 42

    - name: Create Tag
      id: create_tag
      uses: jaywcjlove/create-tag-action@main
      with:
        package-path: ./package.json

    - name: get tag version
      id: tag_version
      uses: jaywcjlove/changelog-generator@main

    - name: Build and Deploy
      uses: peaceiris/actions-gh-pages@v4
      with:
        commit_message: ${{steps.tag_version.outputs.tag}} ${{ github.event.head_commit.message }}
        github_token: ${{ secrets.GITHUB_TOKEN }}
        publish_dir: ./doc

    - name: Generate Changelog
      id: changelog
      uses: jaywcjlove/changelog-generator@main
      with:
        token: ${{ secrets.GITHUB_TOKEN }}
        filter-author: (jaywcjlove|小弟调调™|dependabot\[bot\]|Renovate Bot)
        filter: (^[\s]+?[R|r]elease)|(^[R|r]elease)

    - run: |
        echo "tag: ${{ steps.changelog.outputs.tag }}"
        echo "version: ${{ steps.changelog.outputs.version }}"
        echo "ref: ${{ github.ref }}"

    - name: Create Release
      uses: ncipollo/release-action@v1
      if: steps.create_tag.outputs.successful
      with:
        allowUpdates: true
        token: ${{ secrets.GITHUB_TOKEN }}
        name: ${{ steps.create_tag.outputs.version }}
        tag: ${{ steps.create_tag.outputs.version }}
        body: |
          [![Buy me a coffee](https://img.shields.io/badge/Buy%20me%20a%20coffee-048754?logo=buymeacoffee)](https://jaywcjlove.github.io/#/sponsor) [![](https://img.shields.io/badge/Open%20in-unpkg-blue)](https://uiwjs.github.io/npm-unpkg/#/pkg/hotkeys-js@${{steps.changelog.outputs.version}}/file/README.md) [![npm bundle size](https://img.shields.io/bundlephobia/minzip/hotkeys-js)](https://bundlephobia.com/result?p=hotkeys-js@${{steps.changelog.outputs.version}}) [![npm version](https://img.shields.io/npm/v/hotkeys-js.svg)](https://www.npmjs.com/package/hotkeys-js)
          
          Documentation ${{ steps.changelog.outputs.tag }}: https://raw.githack.com/jaywcjlove/hotkeys/${{ steps.changelog.outputs.gh-pages-short-hash }}/index.html  
          Comparing Changes: ${{ steps.changelog.outputs.compareurl }}  

          ```bash
          npm i hotkeys-js@${{steps.changelog.outputs.version}}
          ```

          ${{ steps.changelog.outputs.changelog }}

    - name: package.json info
      uses: jaywcjlove/github-action-package@main
      with:
        unset: browserslist,lint-staged,devDependencies,jest,scripts
    
    # node@v24.0.0+ 
    # https://gist.github.com/jaywcjlove/a178278521a6f72c74525d3f1d9c4bf9
    - run: NODE_AUTH_TOKEN="" npm publish --access public --provenance
      name: 📦 hotkeys-js to NPM
      continue-on-error: true

================================================
FILE: .github/workflows/pr.yml
================================================
name: CI-PR
on:
  pull_request:

env:
  SKIP_PREFLIGHT_CHECK: true

jobs:
  build-deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      id-token: write
    steps:
    - uses: actions/checkout@v6
    - uses: actions/setup-node@v6
      with:
        node-version: 24
        registry-url: 'https://registry.npmjs.org'

    - name: Look Changelog
      uses: jaywcjlove/changelog-generator@main
      with:
        token: ${{ secrets.GITHUB_TOKEN }}
        filter-author: (jaywcjlove|小弟调调™|dependabot\[bot\]|Renovate Bot)
        filter: (^[\s]+?[R|r]elease)|(^[R|r]elease)

    - run: npm install
    - run: npm run build
    - run: npm run test
    

================================================
FILE: .gitignore
================================================
npm-debug.log
yarn.lock
node_modules
build
doc
coverage

.DS_Store
.cache
.vscode
.idea

*.bak
*.tem
*.temp
#.swp
*.*~
~*.*


================================================
FILE: .husky/.gitignore
================================================
_


================================================
FILE: .husky/pre-commit
================================================
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npx lint-staged


================================================
FILE: .npmignore
================================================
# Exclude test files
dist/test-formats.html

# Exclude development files
src/
test/
website/
coverage/

# Exclude configuration files
*.config.ts
*.config.js
eslint.config.js
tsconfig.json

# Exclude other unnecessary files
README-zh.md
DISTRIBUTION-GUIDE.md
DISTRIBUTION-GUIDE-zh.md
.github/
.gitignore
.husky/

================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2015-present, Kenny Wong.

Copyright (c) 2011-2013 Thomas Fuchs (https://github.com/madrobby/keymaster)

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-zh.md
================================================
<div markdown="1">
  <sup>使用<a href="https://wangchujiang.com/#/app" target="_blank">我的应用</a>也是一种<a href="https://wangchujiang.com/#/sponsor" target="_blank">支持</a>我的方式:</sup>
  <br>
  <a target="_blank" href="https://apps.apple.com/app/6758053530" title="Scap: Screenshot & Markup Edit for macOS"><img alt="Scap: Screenshot & Markup Edit" height="52" width="52" src="https://wangchujiang.com/appicon/scap.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6757317079" title="Screen Test for macOS"><img alt="Screen Test" height="52" width="52" src="https://wangchujiang.com/appicon/screen-test.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/Deskmark/6755948110" title="Deskmark for macOS"><img alt="Deskmark" height="52" width="52" src="https://wangchujiang.com/appicon/deskmark.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/Keyzer/6500434773" title="Keyzer for macOS"><img alt="Keyzer" height="52" width="52" src="https://wangchujiang.com/appicon/keyzer.png"></a>
  <a target="_blank" href="https://github.com/jaywcjlove/vidwall-hub" title="Vidwall Hub for macOS"><img alt="Vidwall Hub" height="52" width="52" src="https://wangchujiang.com/appicon/vidwall-hub.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/VidCrop/6752624705" title="VidCrop for macOS"><img alt="VidCrop" height="52" width="52" src="https://wangchujiang.com/appicon/vidcrop.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/Vidwall/6747587746" title="Vidwall for macOS"><img alt="Vidwall" height="52" width="52" src="https://wangchujiang.com/appicon/vidwall.png"></a>
  <a target="_blank" href="https://wangchujiang.com/mousio-hint/" title="Mousio Hint for macOS"><img alt="Mousio Hint" height="52" width="52" src="https://wangchujiang.com/appicon/mousio-hint.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6746747327" title="Mousio for macOS"><img alt="Mousio" height="52" width="52" src="https://wangchujiang.com/appicon/mousio.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6745227444" title="Musicer for macOS"><img alt="Musicer" height="52" width="52" src="https://wangchujiang.com/appicon/musicer.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6743841447" title="Audioer for macOS"><img alt="Audioer" height="52" width="52" src="https://wangchujiang.com/appicon/audioer.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6744690194" title="FileSentinel for macOS"><img alt="FileSentinel" height="52" width="52" src="https://wangchujiang.com/appicon/file-sentinel.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6743495172" title="FocusCursor for macOS"><img alt="FocusCursor" height="52" width="52" src="https://wangchujiang.com/appicon/focus-cursor.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6742680573" title="Videoer for macOS"><img alt="Videoer" height="52" width="52" src="https://wangchujiang.com/appicon/videoer.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6740425504" title="KeyClicker for macOS"><img alt="KeyClicker" height="52" width="52" src="https://wangchujiang.com/appicon/key-clicker.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6739052447" title="DayBar for macOS"><img alt="DayBar" height="52" width="52" src="https://wangchujiang.com/appicon/daybar.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6739444407" title="Iconed for macOS"><img alt="Iconed" height="52" width="52" src="https://wangchujiang.com/appicon/iconed.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6737160756" title="Menuist for macOS"><img alt="Menuist" height="52" width="52" src="https://wangchujiang.com/appicon/rightmenu-master.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6723903021" title="Paste Quick for macOS"><img alt="Quick RSS" height="52" width="52" src="https://wangchujiang.com/appicon/paste-quick.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6670696072" title="Quick RSS for macOS/iOS"><img alt="Quick RSS" height="52" width="52" src="https://wangchujiang.com/appicon/quick-rss.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6670167443" title="Web Serve for macOS"><img alt="Web Serve" height="52" width="52" src="https://wangchujiang.com/appicon/web-serve.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6503953628" title="Copybook Generator for macOS/iOS"><img alt="Copybook Generator" height="52" width="52" src="https://wangchujiang.com/appicon/copybook-generator.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6471227008" title="DevTutor for macOS/iOS"><img alt="DevTutor for SwiftUI" height="52" width="52" src="https://wangchujiang.com/appicon/devtutor.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6479819388" title="RegexMate for macOS/iOS"><img alt="RegexMate" height="52" width="52" src="https://wangchujiang.com/appicon/regex-mate.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6479194014" title="Time Passage for macOS/iOS"><img alt="Time Passage" height="52" width="52" src="https://wangchujiang.com/appicon/time-passage.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6478772538" title="IconizeFolder for macOS"><img alt="Iconize Folder" height="52" width="52" src="https://wangchujiang.com/appicon/iconize-folder.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6478511402" title="Textsound Saver for macOS/iOS"><img alt="Textsound Saver" height="52" width="52" src="https://wangchujiang.com/appicon/textsound-saver.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6476924627" title="Create Custom Symbols for macOS"><img alt="Create Custom Symbols" height="52" width="52" src="https://wangchujiang.com/appicon/create-custom-symbols.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6476452351" title="DevHub for macOS"><img alt="DevHub" height="52" width="52" src="https://wangchujiang.com/appicon/devhub.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6476400184" title="Resume Revise for macOS"><img alt="Resume Revise" height="52" width="52" src="https://wangchujiang.com/appicon/resume-revise.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6472593276" title="Palette Genius for macOS"><img alt="Palette Genius" height="52" width="52" src="https://wangchujiang.com/appicon/palette-genius.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6470879005" title="Symbol Scribe for macOS"><img alt="Symbol Scribe" height="52" width="52" src="https://wangchujiang.com/appicon/symbol-scribe.png"></a>
</div>
<hr>

# Hotkeys

<!--dividing-->

[![Buy me a coffee](https://img.shields.io/badge/Buy_Me_a_Coffee-ffdd00?logo=buy-me-a-coffee&logoColor=black)](https://jaywcjlove.github.io/#/sponsor)
[![Follow On X](https://img.shields.io/badge/Follow%20on%20X-333333?logo=x&logoColor=white)](https://x.com/jaywcjlove)
[![](https://img.shields.io/npm/dm/hotkeys-js?logo=npm)](https://www.npmjs.com/package/hotkeys-js)
[![](https://img.shields.io/github/stars/jaywcjlove/hotkeys-js.svg)](https://github.com/jaywcjlove/hotkeys/stargazers)
[![GitHub Actions CI](https://github.com/jaywcjlove/hotkeys-js/actions/workflows/ci.yml/badge.svg)](https://github.com/jaywcjlove/hotkeys-js/actions/workflows/ci.yml)
[![Coverage Status](https://jaywcjlove.github.io/hotkeys-js/coverage.svg)](https://jaywcjlove.github.io/hotkeys-js/lcov-report/index.html)
[![English](https://jaywcjlove.github.io/sb/lang/english.svg)](https://jaywcjlove.github.io/hotkeys-js/)
[![jaywcjlove/hotkeys-js](https://jaywcjlove.github.io/sb/ico/gitee.svg)](https://gitee.com/jaywcjlove/hotkeys)

HotKeys.js 是一个具有一些非常特殊功能的输入捕获库,它易于上手和使用,占用空间合理([~8kB](https://bundlephobia.com/result?p=hotkeys-js))(压缩后:**`3.8kB`**),无依赖。它不应该干扰任何 JavaScript 库或框架。官方文档 [演示预览](https://jaywcjlove.github.io/hotkeys-js),[兼容性测试](https://jaywcjlove.github.io/hotkeys-js/dist/compatibility-test.html)。[更多示例](https://github.com/jaywcjlove/hotkeys-js/issues?q=label%3ADemo+)。

```bash
╭┈┈╮          ╭┈┈╮  ╭┈┈╮
┆  ├┈┈..┈┈┈┈┈.┆  └┈╮┆  ├┈┈..┈┈┈┈┈..┈┈.┈┈..┈┈┈┈┈.
┆     ┆┆  □  ┆┆   ┈┤┆    < ┆  -__┘┆  ┆  ┆┆__ ┈┈┤
╰┈┈┴┈┈╯╰┈┈┈┈┈╯╰┈┈┈┈╯╰┈┈┴┈┈╯╰┈┈┈┈┈╯╰┈┈┈  ┆╰┈┈┈┈┈╯
                                  ╰┈┈┈┈┈╯
```

## 使用

您的系统需要安装 `Node.js`。

```bash
npm install hotkeys-js --save
```

```js
import hotkeys from 'hotkeys-js';

hotkeys('f5', function(event, handler){
  // 阻止 WINDOWS 系统下的默认刷新事件
  event.preventDefault()
  alert('你按下了 F5!')
});
```

### 浏览器使用

或者手动下载并在 HTML 中链接 **hotkeys.js**。该库提供了不同格式以满足不同的使用需求:

**CDN 链接:** [UNPKG](https://unpkg.com/hotkeys-js/dist/) | [jsDelivr](https://cdn.jsdelivr.net/npm/hotkeys-js/) | [Githack](https://raw.githack.com/jaywcjlove/hotkeys/master/dist/) | [Statically](https://cdn.statically.io/gh/jaywcjlove/hotkeys/master/dist/)

**可用格式:**

**IIFE(立即调用函数表达式)- 推荐用于直接浏览器使用:**

```html
<script src="https://unpkg.com/hotkeys-js/dist/hotkeys-js.min.js">
</script>
<script type="text/javascript">
hotkeys('ctrl+a,ctrl+b,r,f', function (event, handler){
  switch (handler.key) {
    case 'ctrl+a': alert('你按下了 ctrl+a!');
      break;
    case 'ctrl+b': alert('你按下了 ctrl+b!');
      break;
    case 'r': alert('你按下了 r!');
      break;
    case 'f': alert('你按下了 f!');
      break;
    default: alert(event);
  }
});
</script>
```

**UMD(通用模块定义)- 用于 CommonJS/AMD 环境:**

```html
<script src="https://unpkg.com/hotkeys-js/dist/hotkeys-js.umd.cjs">
</script>
```

**ES 模块 - 用于支持模块的现代浏览器:**

```html
<script type="module">
import hotkeys from 'https://unpkg.com/hotkeys-js/dist/hotkeys-js.js';
hotkeys('ctrl+a', function(event, handler){
  alert('你按下了 ctrl+a!');
});
</script>
```

### 在 React 中使用

[react-hotkeys](https://github.com/jaywcjlove/react-hotkeys) 是监听 keydown 和 keyup 键盘事件的 React 组件,定义和调度键盘快捷键。详细的使用方法请查看其文档 [react-hotkeys](https://github.com/jaywcjlove/react-hotkeys)。  
[react-hotkeys-hook](https://github.com/JohannesKlauss/react-hotkeys-hook) - 在组件中使用键盘快捷键的 React hook。请确保您至少安装了 react 和 react-dom 的 16.8 版本,否则 hooks 将不会为您工作。

## 浏览器支持

Hotkeys.js 已经过测试,应该在以下浏览器中工作。

```shell
Internet Explorer 6+
Safari
Firefox
Chrome
```

## 支持的按键

HotKeys 理解以下修饰符:`⇧`、`shift`、`option`、`⌥`、`alt`、`ctrl`、`control`、`command` 和 `⌘`。

以下特殊按键可用于快捷键:backspace、tab、clear、enter、return、esc、escape、space、up、down、left、right、home、end、pageup、pagedown、del、delete、f1 到 f19、num_0 到 num_9、num_multiply、num_add、num_enter、num_subtract、num_decimal、num_divide。

`⌘` Command()  
`⌃` Control  
`⌥` Option(alt)  
`⇧` Shift  
`⇪` Caps Lock(Capital)  
~~`fn` 不支持 fn~~  
`↩︎` return/Enter space  

## 定义快捷键

暴露了一个全局方法,当直接调用时定义快捷键。

```ts
declare interface HotkeysInterface extends HotkeysAPI {
  (key: string, method: KeyHandler): void;
  (key: string, scope: string, method: KeyHandler): void;
  (key: string, option: HotkeysOptions, method: KeyHandler): void;
  shift?: boolean;
  ctrl?: boolean;
  alt?: boolean;
  option?: boolean;
  control?: boolean;
  cmd?: boolean;
  command?: boolean;
}
declare interface HotkeysAPI {
  setScope: SetScope;
  getScope: GetScope;
  deleteScope: DeleteScope;
  getPressedKeyCodes: GetPressedKeyCodes;
  getPressedKeyString: GetPressedKeyString;
  getAllKeyCodes: GetAllKeyCodes;
  isPressed: IsPressed;
  filter: Filter;
  trigger: Trigger;
  unbind: Unbind;
  noConflict: NoConflict;
  keyMap: Record<string, number>;
  modifier: Record<string, number>;
  modifierMap: Record<string | number, number | string>;
}
```

```js
hotkeys('f5', function(event, handler) {
  // 阻止 WINDOWS 系统下的默认刷新事件
  event.preventDefault();
  alert('你按下了 F5!');
});

// 返回 false 停止事件并阻止默认浏览器事件
// Mac OS 系统将 `command + r` 定义为刷新快捷键
hotkeys('ctrl+r, command+r', function() {
  alert('停止刷新!');
  return false;
});

// 单个按键
hotkeys('a', function(event,handler){
  //event.srcElement: input
  //event.target: input
  if(event.target === "input"){
      alert('你按下了 a!')
  }
  alert('你按下了 a!')
});

// 组合键
hotkeys('ctrl+a,ctrl+b,r,f', function (event, handler){
  switch (handler.key) {
    case 'ctrl+a': alert('你按下了 ctrl+a!');
      break;
    case 'ctrl+b': alert('你按下了 ctrl+b!');
      break;
    case 'r': alert('你按下了 r!');
      break;
    case 'f': alert('你按下了 f!');
      break;
    default: alert(event);
  }
});

hotkeys('ctrl+a+s', function() {
    alert('你按下了 ctrl+a+s!');
});

// 使用作用域
hotkeys('*','wcj', function(event){
  console.log('做一些事情', event);
});
```

#### option 选项

- `scope<String>`:设置快捷键生效的作用域
- `element<HTMLElement>`:指定要绑定事件的 DOM 元素
- `keyup<Boolean>`:是否在按键释放时触发快捷键
- `keydown<Boolean>`:是否在按键按下时触发快捷键
- `splitKey<String>`:组合键的分隔符(默认为 `+`)
- `capture<Boolean>`:是否在捕获阶段触发监听器(在事件冒泡之前)
- `single<Boolean>`:只允许一个回调函数(自动解绑之前的)

```js
hotkeys('o, enter', {
  scope: 'wcj',
  element: document.getElementById('wrapper'),
}, function() {
  console.log('做其他事情');
});

hotkeys('ctrl-+', { splitKey: '-' }, function(e) {
  console.log('你按下了 ctrl 和 +');
});

hotkeys('+', { splitKey: '-' }, function(e){
  console.log('你按下了 +');
})
```

**keyup**

**按键按下** 和 **按键释放** 都执行回调事件。

```js
hotkeys('ctrl+a,alt+a+s', {keyup: true}, function(event, handler) {
  if (event.type === 'keydown') {
    console.log('keydown:', event.type, handler, handler.key);
  }

  if (event.type === 'keyup') {
    console.log('keyup:', event.type, handler, handler.key);
  }
});
```

## API 参考

星号 "*"

修饰键判断

```js
hotkeys('*', function() {
  if (hotkeys.shift) {
    console.log('按下了 shift!');
  }

  if (hotkeys.ctrl) {
    console.log('按下了 ctrl!');
  }

  if (hotkeys.alt) {
    console.log('按下了 alt!');
  }

  if (hotkeys.option) {
    console.log('按下了 option!');
  }

  if (hotkeys.control) {
    console.log('按下了 control!');
  }

  if (hotkeys.cmd) {
    console.log('按下了 cmd!');
  }

  if (hotkeys.command) {
    console.log('按下了 command!');
  }
});
```

### setScope

使用 `hotkeys.setScope` 方法来设置作用域。除了 'all' 之外,只能有一个活动作用域。默认情况下 'all' 总是活动的。

```js
// 定义带有作用域的快捷键
hotkeys('ctrl+o, ctrl+alt+enter', 'issues', function() {
  console.log('做一些事情');
});
hotkeys('o, enter', 'files', function() {
  console.log('做其他事情');
});

// 设置作用域(只有 'all' 和 'issues' 快捷键会被处理)
hotkeys.setScope('issues'); // 默认作用域是 'all'
```

### getScope

使用 `hotkeys.getScope` 方法来获取作用域。

```js
hotkeys.getScope();
```

### deleteScope

使用 `hotkeys.deleteScope` 方法来删除作用域。这也会移除与之关联的所有热键。

```js
hotkeys.deleteScope('issues');
```
如果需要在删除后设置新的作用域,可以使用第二个参数。

```js
hotkeys.deleteScope('issues', 'newScopeName');
```

### unbind

与定义快捷键类似,它们可以使用 `hotkeys.unbind` 来解绑。

```js
// 解绑 'a' 处理器
hotkeys.unbind('a');

// 只为单个作用域解绑热键
// 如果没有指定作用域,默认为当前作用域
// (hotkeys.getScope())
hotkeys.unbind('o, enter', 'issues');
hotkeys.unbind('o, enter', 'files');
```

通过函数解绑事件。

```js
function example() {
  hotkeys('a', example);
  hotkeys.unbind('a', example);

  hotkeys('a', 'issues', example);
  hotkeys.unbind('a', 'issues', example);
}
```

解绑所有。

```js
hotkeys.unbind();
```

### isPressed

例如,如果当前按下了 `M` 键,`hotkeys.isPressed(77)` 返回 true。

```js
hotkeys('a', function() {
  console.log(hotkeys.isPressed('a')); //=> true
  console.log(hotkeys.isPressed('A')); //=> true
  console.log(hotkeys.isPressed(65)); //=> true
});
```

### trigger

触发快捷键事件

```js
hotkeys.trigger('ctrl+o');
hotkeys.trigger('ctrl+o', 'scope2');
```

### getPressedKeyCodes

返回当前按下的键码数组。

```js
hotkeys('command+ctrl+shift+a,f', function() {
  console.log(hotkeys.getPressedKeyCodes()); //=> [17, 65] 或 [70]
})
```

### getPressedKeyString

返回当前按下的键字符串数组。

```js
hotkeys('command+ctrl+shift+a,f', function() {
  console.log(hotkeys.getPressedKeyString()); 
  //=> ['⌘', '⌃', '⇧', 'A', 'F']
})
```

### getAllKeyCodes

获取所有注册码的列表。

```js
hotkeys('command+ctrl+shift+a,f', function() {
  console.log(hotkeys.getAllKeyCodes());
  // [
  //   { 
  //      scope: 'all', 
  //      shortcut: 'command+ctrl+shift+a', 
  //      mods: [91, 17, 16], 
  //      keys: [91, 17, 16, 65] 
  //    },
  //   { scope: 'all', shortcut: 'f', mods: [], keys: [42] }
  // ]
})
```

### filter

默认情况下,`INPUT` `SELECT` `TEXTAREA` 元素不启用热键。`Hotkeys.filter` 返回 `true` 快捷键设置发挥作用,`false` 快捷键设置失败。

```js
hotkeys.filter = function(event){
  return true;
}
// 如何为编辑标签添加过滤器。
// <div contentEditable="true"></div>
// "contentEditable" 不支持的较旧浏览器会被丢弃
hotkeys.filter = function(event) {
  var target = event.target || event.srcElement;
  var tagName = target.tagName;
  return !(
    target.isContentEditable ||
    tagName == 'INPUT' ||
    tagName == 'SELECT' ||
    tagName == 'TEXTAREA'
  );
}

hotkeys.filter = function(event){
  var tagName = (event.target || event.srcElement).tagName;
  hotkeys.setScope(
    /^(INPUT|TEXTAREA|SELECT)$/.test(tagName) ? 'input' : 'other'
  );
  return true;
}
```

### noConflict

放弃 HotKeys 对 `hotkeys` 变量的控制。

```js
var k = hotkeys.noConflict();
k('a', function() {
  console.log("做一些事情")
});

hotkeys()
// -->Uncaught TypeError: hotkeys is not a function(anonymous function)
// @ VM2170:2InjectedScript._evaluateOn
// @ VM2165:883InjectedScript._evaluateAndWrap
// @ VM2165:816InjectedScript.evaluate @ VM2165:682
```

## 开发

要开发,需要安装依赖,获取代码:

```shell
$ git https://github.com/jaywcjlove/hotkeys.git
$ cd hotkeys     # 进入目录
$ npm install    # 或者 yarn install
```

要开发,运行自重载构建:

```shell
$ npm run watch
```

运行文档网站环境。

```shell
$ npm run doc # 生成文档网页
# 实时生成文档网页
$ npm run start 
```

要贡献,请 fork Hotkeys.js,添加您的补丁和测试(在 `test/` 文件夹中)并提交拉取请求。

```shell
$ npm run test
$ npm run test:watch # 开发模式
```

## 贡献者

一如既往,感谢我们出色的贡献者!

<a href="https://github.com/jaywcjlove/hotkeys-js/graphs/contributors">
  <img src="https://jaywcjlove.github.io/hotkeys-js/CONTRIBUTORS.svg" />
</a>

使用 [action-contributors](https://github.com/jaywcjlove/github-action-contributors) 制作。

特别感谢 [@dimensi](https://github.com/dimensi) 对版本 [4.0](https://github.com/jaywcjlove/hotkeys-js/issues/313) 的重构。

## 许可证

[MIT © Kenny Wong](./LICENSE)


================================================
FILE: README.md
================================================
<div markdown="1">
  <sup>Using <a href="https://wangchujiang.com/#/app" target="_blank">my app</a> is also a way to <a href="https://wangchujiang.com/#/sponsor" target="_blank">support</a> me:</sup>
  <br>
  <a target="_blank" href="https://apps.apple.com/app/6758053530" title="Scap: Screenshot & Markup Edit for macOS"><img alt="Scap: Screenshot & Markup Edit" height="52" width="52" src="https://wangchujiang.com/appicon/scap.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6757317079" title="Screen Test for macOS"><img alt="Screen Test" height="52" width="52" src="https://wangchujiang.com/appicon/screen-test.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/Deskmark/6755948110" title="Deskmark for macOS"><img alt="Deskmark" height="52" width="52" src="https://wangchujiang.com/appicon/deskmark.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/Keyzer/6500434773" title="Keyzer for macOS"><img alt="Keyzer" height="52" width="52" src="https://wangchujiang.com/appicon/keyzer.png"></a>
  <a target="_blank" href="https://github.com/jaywcjlove/vidwall-hub" title="Vidwall Hub for macOS"><img alt="Vidwall Hub" height="52" width="52" src="https://wangchujiang.com/appicon/vidwall-hub.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/VidCrop/6752624705" title="VidCrop for macOS"><img alt="VidCrop" height="52" width="52" src="https://wangchujiang.com/appicon/vidcrop.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/Vidwall/6747587746" title="Vidwall for macOS"><img alt="Vidwall" height="52" width="52" src="https://wangchujiang.com/appicon/vidwall.png"></a>
  <a target="_blank" href="https://wangchujiang.com/mousio-hint/" title="Mousio Hint for macOS"><img alt="Mousio Hint" height="52" width="52" src="https://wangchujiang.com/appicon/mousio-hint.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6746747327" title="Mousio for macOS"><img alt="Mousio" height="52" width="52" src="https://wangchujiang.com/appicon/mousio.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6745227444" title="Musicer for macOS"><img alt="Musicer" height="52" width="52" src="https://wangchujiang.com/appicon/musicer.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6743841447" title="Audioer for macOS"><img alt="Audioer" height="52" width="52" src="https://wangchujiang.com/appicon/audioer.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6744690194" title="FileSentinel for macOS"><img alt="FileSentinel" height="52" width="52" src="https://wangchujiang.com/appicon/file-sentinel.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6743495172" title="FocusCursor for macOS"><img alt="FocusCursor" height="52" width="52" src="https://wangchujiang.com/appicon/focus-cursor.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6742680573" title="Videoer for macOS"><img alt="Videoer" height="52" width="52" src="https://wangchujiang.com/appicon/videoer.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6740425504" title="KeyClicker for macOS"><img alt="KeyClicker" height="52" width="52" src="https://wangchujiang.com/appicon/key-clicker.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6739052447" title="DayBar for macOS"><img alt="DayBar" height="52" width="52" src="https://wangchujiang.com/appicon/daybar.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6739444407" title="Iconed for macOS"><img alt="Iconed" height="52" width="52" src="https://wangchujiang.com/appicon/iconed.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6737160756" title="Menuist for macOS"><img alt="Menuist" height="52" width="52" src="https://wangchujiang.com/appicon/rightmenu-master.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6723903021" title="Paste Quick for macOS"><img alt="Quick RSS" height="52" width="52" src="https://wangchujiang.com/appicon/paste-quick.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6670696072" title="Quick RSS for macOS/iOS"><img alt="Quick RSS" height="52" width="52" src="https://wangchujiang.com/appicon/quick-rss.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6670167443" title="Web Serve for macOS"><img alt="Web Serve" height="52" width="52" src="https://wangchujiang.com/appicon/web-serve.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6503953628" title="Copybook Generator for macOS/iOS"><img alt="Copybook Generator" height="52" width="52" src="https://wangchujiang.com/appicon/copybook-generator.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6471227008" title="DevTutor for macOS/iOS"><img alt="DevTutor for SwiftUI" height="52" width="52" src="https://wangchujiang.com/appicon/devtutor.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6479819388" title="RegexMate for macOS/iOS"><img alt="RegexMate" height="52" width="52" src="https://wangchujiang.com/appicon/regex-mate.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6479194014" title="Time Passage for macOS/iOS"><img alt="Time Passage" height="52" width="52" src="https://wangchujiang.com/appicon/time-passage.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6478772538" title="IconizeFolder for macOS"><img alt="Iconize Folder" height="52" width="52" src="https://wangchujiang.com/appicon/iconize-folder.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6478511402" title="Textsound Saver for macOS/iOS"><img alt="Textsound Saver" height="52" width="52" src="https://wangchujiang.com/appicon/textsound-saver.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6476924627" title="Create Custom Symbols for macOS"><img alt="Create Custom Symbols" height="52" width="52" src="https://wangchujiang.com/appicon/create-custom-symbols.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6476452351" title="DevHub for macOS"><img alt="DevHub" height="52" width="52" src="https://wangchujiang.com/appicon/devhub.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6476400184" title="Resume Revise for macOS"><img alt="Resume Revise" height="52" width="52" src="https://wangchujiang.com/appicon/resume-revise.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6472593276" title="Palette Genius for macOS"><img alt="Palette Genius" height="52" width="52" src="https://wangchujiang.com/appicon/palette-genius.png"></a>
  <a target="_blank" href="https://apps.apple.com/app/6470879005" title="Symbol Scribe for macOS"><img alt="Symbol Scribe" height="52" width="52" src="https://wangchujiang.com/appicon/symbol-scribe.png"></a>
</div>
<hr>

# Hotkeys

[![Buy me a coffee](https://img.shields.io/badge/Buy_Me_a_Coffee-ffdd00?logo=buy-me-a-coffee&logoColor=black)](https://jaywcjlove.github.io/#/sponsor)
[![Follow On X](https://img.shields.io/badge/Follow%20on%20X-333333?logo=x&logoColor=white)](https://x.com/jaywcjlove)
[![](https://img.shields.io/npm/dm/hotkeys-js?logo=npm&label=)](https://www.npmjs.com/package/hotkeys-js)
[![](https://img.shields.io/github/stars/jaywcjlove/hotkeys-js.svg)](https://github.com/jaywcjlove/hotkeys/stargazers)
[![GitHub Actions CI](https://github.com/jaywcjlove/hotkeys-js/actions/workflows/ci.yml/badge.svg)](https://github.com/jaywcjlove/hotkeys-js/actions/workflows/ci.yml)
[![Coverage Status](https://jaywcjlove.github.io/hotkeys-js/coverage.svg)](https://jaywcjlove.github.io/hotkeys-js/lcov-report/index.html)
[![Chinese](https://jaywcjlove.github.io/sb/lang/chinese.svg)](https://wangchujiang.com/hotkeys-js/?lang=zh)
[![jaywcjlove/hotkeys-js](https://jaywcjlove.github.io/sb/ico/gitee.svg)](https://gitee.com/jaywcjlove/hotkeys)

HotKeys.js is an input capture library with some very special features, it is easy to pick up and use, has a reasonable footprint ([~8kB](https://bundlephobia.com/result?p=hotkeys-js)) (gzipped: **`3.8kB`**), and has no dependencies. It should not interfere with any JavaScript libraries or frameworks. Official document [demo preview](https://jaywcjlove.github.io/hotkeys-js), [compatibility test](https://jaywcjlove.github.io/hotkeys-js/dist/compatibility-test.html). [More examples](https://github.com/jaywcjlove/hotkeys-js/issues?q=label%3ADemo+).

```bash
╭┈┈╮          ╭┈┈╮  ╭┈┈╮
┆  ├┈┈..┈┈┈┈┈.┆  └┈╮┆  ├┈┈..┈┈┈┈┈..┈┈.┈┈..┈┈┈┈┈.
┆     ┆┆  □  ┆┆   ┈┤┆    < ┆  -__┘┆  ┆  ┆┆__ ┈┈┤
╰┈┈┴┈┈╯╰┈┈┈┈┈╯╰┈┈┈┈╯╰┈┈┴┈┈╯╰┈┈┈┈┈╯╰┈┈┈  ┆╰┈┈┈┈┈╯
                                  ╰┈┈┈┈┈╯
```

## Usage

You will need `Node.js` installed on your system.

```bash
npm install hotkeys-js --save
```

```js
import hotkeys from 'hotkeys-js';

hotkeys('f5', function(event, handler){
  // Prevent the default refresh event under WINDOWS system
  event.preventDefault()
  alert('you pressed F5!')
});
```

### Browser Usage

Or manually download and link **hotkeys.js** in your HTML. The library provides different formats for different use cases:

**CDN Links:** [UNPKG](https://unpkg.com/hotkeys-js/dist/) | [jsDelivr](https://cdn.jsdelivr.net/npm/hotkeys-js/) | [Githack](https://raw.githack.com/jaywcjlove/hotkeys/master/dist/) | [Statically](https://cdn.statically.io/gh/jaywcjlove/hotkeys/master/dist/)

**Available Formats:**

**IIFE (Immediately Invoked Function Expression) - Recommended for direct browser usage:**

```html
<script src="https://unpkg.com/hotkeys-js/dist/hotkeys-js.min.js">
</script>
<script type="text/javascript">
hotkeys('ctrl+a,ctrl+b,r,f', function (event, handler){
  switch (handler.key) {
    case 'ctrl+a': alert('you pressed ctrl+a!');
      break;
    case 'ctrl+b': alert('you pressed ctrl+b!');
      break;
    case 'r': alert('you pressed r!');
      break;
    case 'f': alert('you pressed f!');
      break;
    default: alert(event);
  }
});
</script>
```

**UMD (Universal Module Definition) - For CommonJS/AMD environments:**

```html
<script src="https://unpkg.com/hotkeys-js/dist/hotkeys-js.umd.cjs">
</script>
```

**ES Module - For modern browsers with module support:**

```html
<script type="module">
import hotkeys from 'https://unpkg.com/hotkeys-js/dist/hotkeys-js.js';
hotkeys('ctrl+a', function(event, handler){
  alert('you pressed ctrl+a!');
});
</script>
```

### Used in React

[react-hotkeys](https://github.com/jaywcjlove/react-hotkeys) is the React component that listen to keydown and keyup keyboard events, defining and dispatching keyboard shortcuts. Detailed use method please see its documentation [react-hotkeys](https://github.com/jaywcjlove/react-hotkeys).

[react-hotkeys-hook](https://github.com/JohannesKlauss/react-hotkeys-hook) - React hook for using keyboard shortcuts in components. Make sure that you have at least version 16.8 of react and react-dom installed, or otherwise hooks won't work for you.

## Browser Support

Hotkeys.js has been tested and should work in.

```shell
Internet Explorer 6+
Safari
Firefox
Chrome
```

## Supported Keys

HotKeys understands the following modifiers: `⇧`, `shift`, `option`, `⌥`, `alt`, `ctrl`, `control`, `command`, and `⌘`.

The following special keys can be used for shortcuts: backspace, tab, clear, enter, return, esc, escape, space, up, down, left, right, home, end, pageup, pagedown, del, delete, f1 through f19, num_0 through num_9, num_multiply, num_add, num_enter, num_subtract, num_decimal, num_divide.

`⌘` Command()  
`⌃` Control  
`⌥` Option(alt)  
`⇧` Shift  
`⇪` Caps Lock(Capital)  
~~`fn` Does not support fn~~  
`↩︎` return/Enter space  

## Defining Shortcuts

One global method is exposed, key which defines shortcuts when called directly.

```ts
declare interface HotkeysInterface extends HotkeysAPI {
  (key: string, method: KeyHandler): void;
  (key: string, scope: string, method: KeyHandler): void;
  (key: string, option: HotkeysOptions, method: KeyHandler): void;
  shift?: boolean;
  ctrl?: boolean;
  alt?: boolean;
  option?: boolean;
  control?: boolean;
  cmd?: boolean;
  command?: boolean;
}
declare interface HotkeysAPI {
  setScope: SetScope;
  getScope: GetScope;
  deleteScope: DeleteScope;
  getPressedKeyCodes: GetPressedKeyCodes;
  getPressedKeyString: GetPressedKeyString;
  getAllKeyCodes: GetAllKeyCodes;
  isPressed: IsPressed;
  filter: Filter;
  trigger: Trigger;
  unbind: Unbind;
  noConflict: NoConflict;
  keyMap: Record<string, number>;
  modifier: Record<string, number>;
  modifierMap: Record<string | number, number | string>;
}
```


```js
hotkeys('f5', function(event, handler) {
  // Prevent the default refresh event under WINDOWS system
  event.preventDefault();
  alert('you pressed F5!');
});

// Returning false stops the event and prevents default browser events
// Mac OS system defines `command + r` as a refresh shortcut
hotkeys('ctrl+r, command+r', function() {
  alert('stopped reload!');
  return false;
});

// Single key
hotkeys('a', function(event,handler){
  //event.srcElement: input
  //event.target: input
  if(event.target === "input"){
      alert('you pressed a!')
  }
  alert('you pressed a!')
});

// Key Combination
hotkeys('ctrl+a,ctrl+b,r,f', function (event, handler){
  switch (handler.key) {
    case 'ctrl+a': alert('you pressed ctrl+a!');
      break;
    case 'ctrl+b': alert('you pressed ctrl+b!');
      break;
    case 'r': alert('you pressed r!');
      break;
    case 'f': alert('you pressed f!');
      break;
    default: alert(event);
  }
});

hotkeys('ctrl+a+s', function() {
    alert('you pressed ctrl+a+s!');
});

// Using a scope
hotkeys('*','wcj', function(event){
  console.log('do something', event);
});
```

#### option

- `scope<String>`: Sets the scope in which the shortcut key is active
- `element<HTMLElement>`: Specifies the DOM element to bind the event to
- `keyup<Boolean>`: Whether to trigger the shortcut on key release
- `keydown<Boolean>`: Whether to trigger the shortcut on key press
- `splitKey<String>`: Delimiter for key combinations (default is `+`)
- `capture<Boolean>`: Whether to trigger the listener during the capture phase (before the event bubbles down)
- `single<Boolean>`: Allows only one callback function (automatically unbinds previous one)

```js
hotkeys('o, enter', {
  scope: 'wcj',
  element: document.getElementById('wrapper'),
}, function() {
  console.log('do something else');
});

hotkeys('ctrl-+', { splitKey: '-' }, function(e) {
  console.log('you pressed ctrl and +');
});

hotkeys('+', { splitKey: '-' }, function(e){
  console.log('you pressed +');
})
```

**keyup**

**key down** and **key up** both perform callback events.

```js
hotkeys('ctrl+a,alt+a+s', {keyup: true}, function(event, handler) {
  if (event.type === 'keydown') {
    console.log('keydown:', event.type, handler, handler.key);
  }

  if (event.type === 'keyup') {
    console.log('keyup:', event.type, handler, handler.key);
  }
});
```

## API REFERENCE

Asterisk "*"

Modifier key judgments

```js
hotkeys('*', function() {
  if (hotkeys.shift) {
    console.log('shift is pressed!');
  }

  if (hotkeys.ctrl) {
    console.log('ctrl is pressed!');
  }

  if (hotkeys.alt) {
    console.log('alt is pressed!');
  }

  if (hotkeys.option) {
    console.log('option is pressed!');
  }

  if (hotkeys.control) {
    console.log('control is pressed!');
  }

  if (hotkeys.cmd) {
    console.log('cmd is pressed!');
  }

  if (hotkeys.command) {
    console.log('command is pressed!');
  }
});
```

### setScope

Use the `hotkeys.setScope` method to set scope. There can only be one active scope besides 'all'.  By default 'all' is always active.

```js
// Define shortcuts with a scope
hotkeys('ctrl+o, ctrl+alt+enter', 'issues', function() {
  console.log('do something');
});
hotkeys('o, enter', 'files', function() {
  console.log('do something else');
});

// Set the scope (only 'all' and 'issues' shortcuts will be honored)
hotkeys.setScope('issues'); // default scope is 'all'
```

### getScope

Use the `hotkeys.getScope` method to get scope.

```js
hotkeys.getScope();
```

### deleteScope

Use the `hotkeys.deleteScope` method to delete a scope. This will also remove all associated hotkeys with it.

```js
hotkeys.deleteScope('issues');
```
You can use second argument, if need set new scope after deleting.

```js
hotkeys.deleteScope('issues', 'newScopeName');
```

### unbind

Similar to defining shortcuts, they can be unbound using `hotkeys.unbind`.

```js
// unbind 'a' handler
hotkeys.unbind('a');

// Unbind a hotkeys only for a single scope
// If no scope is specified it defaults to the current 
// scope (hotkeys.getScope())
hotkeys.unbind('o, enter', 'issues');
hotkeys.unbind('o, enter', 'files');
```

Unbind events through functions.

```js
function example() {
  hotkeys('a', example);
  hotkeys.unbind('a', example);

  hotkeys('a', 'issues', example);
  hotkeys.unbind('a', 'issues', example);
}
```

To unbind everything.

```js
hotkeys.unbind();
```

### isPressed

For example, `hotkeys.isPressed(77)` is true if the `M` key is currently pressed.

```js
hotkeys('a', function() {
  console.log(hotkeys.isPressed('a')); //=> true
  console.log(hotkeys.isPressed('A')); //=> true
  console.log(hotkeys.isPressed(65)); //=> true
});
```

### trigger

trigger shortcut key event

```js
hotkeys.trigger('ctrl+o');
hotkeys.trigger('ctrl+o', 'scope2');
```

### getPressedKeyCodes

Returns an array of key codes currently pressed.

```js
hotkeys('command+ctrl+shift+a,f', function() {
  console.log(hotkeys.getPressedKeyCodes()); //=> [17, 65] or [70]
})
```

### getPressedKeyString

Returns an array of key codes currently pressed.

```js
hotkeys('command+ctrl+shift+a,f', function() {
  console.log(hotkeys.getPressedKeyString()); 
  //=> ['⌘', '⌃', '⇧', 'A', 'F']
})
```

### getAllKeyCodes

Get a list of all registration codes.

```js
hotkeys('command+ctrl+shift+a,f', function() {
  console.log(hotkeys.getAllKeyCodes());
  // [
  //   { 
  //      scope: 'all', 
  //      shortcut: 'command+ctrl+shift+a', 
  //      mods: [91, 17, 16], 
  //      keys: [91, 17, 16, 65] 
  //    },
  //   { scope: 'all', shortcut: 'f', mods: [], keys: [42] }
  // ]
})
```

### filter

By default hotkeys are not enabled for `INPUT` `SELECT` `TEXTAREA` elements. `Hotkeys.filter` to return to the `true` shortcut keys set to play a role, `false` shortcut keys set up failure.

```js
hotkeys.filter = function(event){
  return true;
}
// How to add the filter to edit labels. 
// <div contentEditable="true"></div>
// "contentEditable" Older browsers that do not support drops
hotkeys.filter = function(event) {
  var target = event.target || event.srcElement;
  var tagName = target.tagName;
  return !(
    target.isContentEditable || 
    tagName == 'INPUT' || 
    tagName == 'SELECT' || 
    tagName == 'TEXTAREA'
  );
}

hotkeys.filter = function(event){
  var tagName = (event.target || event.srcElement).tagName;
  hotkeys.setScope(
    /^(INPUT|TEXTAREA|SELECT)$/.test(tagName) ? 'input' : 'other'
  );
  return true;
}
```

### noConflict

Relinquish HotKeys’s control of the `hotkeys` variable.

```js
var k = hotkeys.noConflict();
k('a', function() {
  console.log("do something")
});

hotkeys()
// -->Uncaught TypeError: hotkeys is not a function(anonymous function)
// @ VM2170:2InjectedScript._evaluateOn
// @ VM2165:883InjectedScript._evaluateAndWrap
// @ VM2165:816InjectedScript.evaluate @ VM2165:682
```

## Development

To develop, Install dependencies, Get the code:

```shell
$ git https://github.com/jaywcjlove/hotkeys.git
$ cd hotkeys     # Into the directory
$ npm install    # or  yarn install
```

To develop, run the self-reloading build:

```shell
$ npm run watch
```

Run Document Website Environment.

```shell
# Generate documentation website
$ npm run doc 
# Live-generate documentation website
$ npm run start 
```

To contribute, please fork Hotkeys.js, add your patch and tests for it (in the `test/` folder) and submit a pull request.

```shell
$ npm run test
$ npm run test:watch # Development model
```

## Contributors

As always, thanks to our amazing contributors!

<a href="https://github.com/jaywcjlove/hotkeys-js/graphs/contributors">
  <img src="https://jaywcjlove.github.io/hotkeys-js/CONTRIBUTORS.svg" />
</a>

Made with [action-contributors](https://github.com/jaywcjlove/github-action-contributors).

Special thanks to [@dimensi](https://github.com/dimensi) for the refactoring of version [4.0](https://github.com/jaywcjlove/hotkeys-js/issues/313).

## License

[MIT © Kenny Wong](./LICENSE)


================================================
FILE: dist/compatibility-test.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Hotkeys-js Comprehensive Compatibility Test</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 1000px;
            margin: 20px auto;
            padding: 20px;
            background: #f5f5f5;
        }
        .header {
            text-align: center;
            background: white;
            padding: 30px;
            border-radius: 10px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            margin-bottom: 30px;
        }
        .test-section {
            background: white;
            margin: 20px 0;
            padding: 25px;
            border-radius: 8px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
        }
        .test-section h2 {
            margin-top: 0;
            color: #333;
            border-bottom: 2px solid #007bff;
            padding-bottom: 10px;
        }
        .test-section h3 {
            color: #555;
            margin-top: 25px;
        }
        .success {
            color: #28a745;
            font-weight: bold;
        }
        .error {
            color: #dc3545;
            font-weight: bold;
        }
        .info {
            color: #17a2b8;
        }
        .warning {
            color: #ffc107;
            font-weight: bold;
        }
        .log-entry {
            padding: 8px 12px;
            margin: 5px 0;
            border-radius: 4px;
            border-left: 4px solid #ddd;
        }
        .log-entry.success {
            background: #d4edda;
            border-left-color: #28a745;
        }
        .log-entry.error {
            background: #f8d7da;
            border-left-color: #dc3545;
        }
        .log-entry.info {
            background: #d1ecf1;
            border-left-color: #17a2b8;
        }
        .log-entry.warning {
            background: #fff3cd;
            border-left-color: #ffc107;
        }
        button {
            background: #007bff;
            color: white;
            border: none;
            padding: 10px 20px;
            margin: 5px;
            border-radius: 5px;
            cursor: pointer;
            font-size: 14px;
        }
        button:hover {
            background: #0056b3;
        }
        kbd {
            background: #f8f9fa;
            border: 1px solid #ddd;
            border-radius: 3px;
            padding: 2px 6px;
            font-family: monospace;
            font-size: 0.9em;
        }
        .format-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
            gap: 20px;
            margin-top: 20px;
        }
        .format-card {
            background: #f8f9fa;
            padding: 20px;
            border-radius: 6px;
            border: 1px solid #e9ecef;
        }
        .format-card h4 {
            margin-top: 0;
            color: #495057;
        }
        pre {
            background: #f8f9fa;
            padding: 15px;
            border-radius: 5px;
            overflow-x: auto;
            font-size: 13px;
        }
        .stats-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
            gap: 15px;
            margin: 20px 0;
        }
        .stat-card {
            background: #f8f9fa;
            padding: 15px;
            border-radius: 6px;
            text-align: center;
        }
        .stat-number {
            font-size: 2em;
            font-weight: bold;
            color: #007bff;
        }
        .hotkey-list {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
            gap: 10px;
            margin: 15px 0;
        }
        .hotkey-item {
            background: #f8f9fa;
            padding: 10px;
            border-radius: 4px;
            border-left: 3px solid #007bff;
        }
    </style>
</head>
<body>
    <div class="header">
        <h1>🔥 Hotkeys-js Comprehensive Compatibility Test Suite</h1>
        <p>Test the compatibility of hotkeys-js library across different module formats and environments</p>
        <div class="stats-grid">
            <div class="stat-card">
                <div class="stat-number" id="total-tests">0</div>
                <div>Total Tests</div>
            </div>
            <div class="stat-card">
                <div class="stat-number success" id="passed-tests">0</div>
                <div>Passed Tests</div>
            </div>
            <div class="stat-card">
                <div class="stat-number error" id="failed-tests">0</div>
                <div>Failed Tests</div>
            </div>
        </div>
    </div>

    <!-- Module Format Tests -->
    <div class="test-section">
        <h2>📦 Module Format Compatibility Tests</h2>
        <p>Test loading and basic functionality of hotkeys-js library in different build formats</p>
        
        <div class="format-grid">
            <div class="format-card">
                <h4>IIFE Format (hotkeys-js.min.js)</h4>
                <p>Immediately Invoked Function Expression, suitable for direct browser use</p>
                <p>Press <kbd>F1</kbd> to test</p>
                <div id="iife-status">Loading...</div>
            </div>
            
            <div class="format-card">
                <h4>UMD Format (hotkeys-js.umd.cjs)</h4>
                <p>Universal Module Definition, compatible with CommonJS, AMD and global variables</p>
                <p>Press <kbd>F2</kbd> to test</p>
                <div id="umd-status">Loading...</div>
            </div>
            
            <div class="format-card">
                <h4>ES Module Format (hotkeys-js.js)</h4>
                <p>Modern ES6 module format, requires server environment</p>
                <p>Press <kbd>F3</kbd> to test</p>
                <div id="esm-status">Loading...</div>
            </div>
        </div>
    </div>

    <!-- Hotkey Functionality Tests -->
    <div class="test-section">
        <h2>🎯 Hotkey Functionality Tests</h2>
        <p>Test various hotkey combinations and functions</p>
        
        <div class="hotkey-list">
            <div class="hotkey-item">
                <kbd>Ctrl + K</kbd><br>
                <small>Basic hotkey test</small>
            </div>
            <div class="hotkey-item">
                <kbd>Alt + S</kbd><br>
                <small>Modifier key combination test</small>
            </div>
            <div class="hotkey-item">
                <kbd>Ctrl + Shift + T</kbd><br>
                <small>Multiple modifier keys test</small>
            </div>
            <div class="hotkey-item">
                <kbd>Esc</kbd><br>
                <small>Single key test</small>
            </div>
            <div class="hotkey-item">
                <kbd>Space</kbd><br>
                <small>Space key test</small>
            </div>
            <div class="hotkey-item">
                <kbd>Enter</kbd><br>
                <small>Enter key test</small>
            </div>
        </div>
        
        <div id="hotkey-output"></div>
    </div>

    <!-- API Tests -->
    <div class="test-section">
        <h2>🔧 API Method Tests</h2>
        <p>Test various API methods provided by hotkeys-js</p>
        
        <div>
            <button onclick="testAPI()">🧪 Run API Tests</button>
            <button onclick="testScopes()">🎯 Test Scope Functions</button>
            <button onclick="testKeyMapping()">🗝️ Test Key Mapping</button>
            <button onclick="clearOutput('api-output')">🧹 Clear Output</button>
        </div>
        
        <div id="api-output"></div>
    </div>

    <!-- Module Loading Details -->
    <div class="test-section">
        <h2>📋 Module Loading Details</h2>
        <div id="module-details"></div>
    </div>

    <!-- Usage Examples -->
    <div class="test-section">
        <h2>📝 Usage Examples</h2>
        
        <h3>ESM Environment (Recommended)</h3>
        <pre><code>import hotkeys from 'hotkeys-js';

hotkeys('ctrl+k', function(event, handler) {
  event.preventDefault();
  console.log('Hotkey pressed!');
});</code></pre>
        
        <h3>CommonJS Environment</h3>
        <pre><code>const hotkeys = require('hotkeys-js');
// or
const hotkeys = require('hotkeys-js').default;

hotkeys('ctrl+k', function(event, handler) {
  event.preventDefault();
  console.log('Hotkey pressed!');
});</code></pre>
        
        <h3>Browser Global Environment</h3>
        <pre><code>&lt;script src="hotkeys-js/dist/hotkeys-js.min.js">&lt;/script>
&lt;script>
  hotkeys('ctrl+k', function(event, handler) {
    event.preventDefault();
    console.log('Hotkey pressed!');
  });
&lt;/script></code></pre>
    </div>

    <!-- Load all formats for testing -->
    
    <!-- 1. Load IIFE format -->
    <script src="./hotkeys-js.min.js"></script>
    
    <!-- 2. Load UMD format (load later to test independence) -->
    
    <!-- 3. ES Module will be loaded via import -->

    <script>
        // Global variables
        let testStats = { total: 0, passed: 0, failed: 0 };
        let hotkeysInstance = null;

        // Utility functions
        function updateStats() {
            document.getElementById('total-tests').textContent = testStats.total;
            document.getElementById('passed-tests').textContent = testStats.passed;
            document.getElementById('failed-tests').textContent = testStats.failed;
        }

        function logTest(elementId, message, type = 'info', description = '') {
            testStats.total++;
            if (type === 'success') testStats.passed++;
            if (type === 'error') testStats.failed++;
            updateStats();

            const element = document.getElementById(elementId);
            if (!element) return;
            
            const div = document.createElement('div');
            div.className = `log-entry ${type}`;
            const timestamp = new Date().toLocaleTimeString();
            const fullMessage = `[${timestamp}] ${message}`;
            if (description) {
                div.innerHTML = `${fullMessage}<br><small>${description}</small>`;
            } else {
                div.textContent = fullMessage;
            }
            element.appendChild(div);
            
            // Auto scroll to latest message
            element.scrollTop = element.scrollHeight;
        }

        function clearOutput(elementId) {
            const element = document.getElementById(elementId);
            if (element) {
                element.innerHTML = '';
            }
        }

        // Test IIFE format
        function testIIFE() {
            try {
                if (typeof window.hotkeys !== 'undefined') {
                    hotkeysInstance = window.hotkeys;
                    
                    // Bind F1 hotkey
                    hotkeys('f1', function(event, handler) {
                        event.preventDefault();
                        logTest('hotkey-output', '✅ IIFE Format: F1 hotkey triggered successfully!', 'success');
                    });
                    
                    logTest('iife-status', '✅ IIFE format loaded successfully', 'success', 'Global window.hotkeys available');
                    logTest('module-details', 'IIFE format: hotkeys type is ' + typeof hotkeys, 'info');
                    
                    return true;
                } else {
                    throw new Error('window.hotkeys is undefined');
                }
            } catch (e) {
                logTest('iife-status', '❌ IIFE format failed: ' + e.message, 'error');
                return false;
            }
        }

        // Test UMD format
        function testUMD() {
            // Create new script tag to load UMD
            const script = document.createElement('script');
            script.src = './hotkeys-js.umd.cjs';
            script.onload = function() {
                try {
                    if (typeof hotkeys !== 'undefined') {
                        // Bind F2 hotkey
                        hotkeys('f2', function(event, handler) {
                            event.preventDefault();
                            logTest('hotkey-output', '✅ UMD Format: F2 hotkey triggered successfully!', 'success');
                        });
                        
                        logTest('umd-status', '✅ UMD format loaded successfully', 'success', 'Supports CommonJS/AMD/Global environments');
                        logTest('module-details', 'UMD format: hotkeys type is ' + typeof hotkeys, 'info');
                    } else {
                        throw new Error('UMD hotkeys is undefined');
                    }
                } catch (e) {
                    logTest('umd-status', '❌ UMD format failed: ' + e.message, 'error');
                }
            };
            script.onerror = function() {
                logTest('umd-status', '❌ UMD format loading failed', 'error', 'Cannot load hotkeys-js.umd.cjs');
            };
            document.head.appendChild(script);
        }

        // Test ES Module format
        async function testESM() {
            try {
                const { default: hotkeysESM } = await import('./hotkeys-js.js');
                
                // Bind F3 hotkey
                hotkeysESM('f3', function(event, handler) {
                    event.preventDefault();
                    logTest('hotkey-output', '✅ ES Module Format: F3 hotkey triggered successfully!', 'success');
                });
                
                // Bind Ctrl+Shift+T (ESM specific test)
                hotkeysESM('ctrl+shift+t', function(event, handler) {
                    event.preventDefault();
                    logTest('hotkey-output', '✅ ESM Format: Ctrl+Shift+T hotkey triggered successfully!', 'success');
                });
                
                logTest('esm-status', '✅ ES Module format loaded successfully', 'success', 'Modern module format with tree-shaking support');
                logTest('module-details', 'ESM format: hotkeys type is ' + typeof hotkeysESM, 'info');
                
            } catch (e) {
                logTest('esm-status', '❌ ES Module format failed: ' + e.message, 'error', 
                       'ES Module requires server environment, does not support file:// protocol');
            }
        }

        // Setup common hotkey tests
        function setupCommonHotkeys() {
            if (!hotkeysInstance) return;
            
            // Ctrl+K
            hotkeysInstance('ctrl+k', function(event, handler) {
                event.preventDefault();
                logTest('hotkey-output', '✅ Ctrl+K hotkey triggered', 'success', 'Basic modifier key combination test');
            });
            
            // Alt+S  
            hotkeysInstance('alt+s', function(event, handler) {
                event.preventDefault();
                logTest('hotkey-output', '✅ Alt+S hotkey triggered', 'success', 'Alt modifier key test');
            });
            
            // Esc
            hotkeysInstance('esc', function(event, handler) {
                event.preventDefault();
                logTest('hotkey-output', '✅ Esc key triggered', 'success', 'Single key test');
            });
            
            // Space
            hotkeysInstance('space', function(event, handler) {
                event.preventDefault();
                logTest('hotkey-output', '✅ Space key triggered', 'success', 'Space key test');
            });
            
            // Enter
            hotkeysInstance('enter', function(event, handler) {
                event.preventDefault();
                logTest('hotkey-output', '✅ Enter key triggered', 'success', 'Enter key test');
            });
            
            logTest('hotkey-output', '🎯 Common hotkeys have been set, please test by pressing keys', 'info');
        }

        // API tests
        function testAPI() {
            clearOutput('api-output');
            
            if (!hotkeysInstance) {
                logTest('api-output', '❌ 没有可用的 hotkeys 实例', 'error');
                return;
            }

            try {
                // Test basic API
                const methods = ['setScope', 'getScope', 'deleteScope', 'unbind', 'trigger', 'isPressed', 
                               'getPressedKeyCodes', 'getPressedKeyString', 'getAllKeyCodes', 'filter'];
                
                logTest('api-output', '🧪 Starting API method tests', 'info');
                
                methods.forEach(method => {
                    if (hotkeysInstance[method]) {
                        logTest('api-output', `✅ API method ${method} exists`, 'success');
                    } else {
                        logTest('api-output', `❌ API method ${method} missing`, 'error');
                    }
                });
                
                // Test scope functionality
                const currentScope = hotkeysInstance.getScope();
                logTest('api-output', `Current scope: ${currentScope}`, 'info');
                
                // Test key status
                const pressedCodes = hotkeysInstance.getPressedKeyCodes();
                logTest('api-output', `Currently pressed key codes: [${pressedCodes.join(', ')}]`, 'info');
                
                // Test key mapping
                const enterCode = hotkeysInstance.keyMap.enter;
                logTest('api-output', `Enter key mapping: ${enterCode}`, 'info');
                
                // 测试修饰键
                const ctrlCode = hotkeysInstance.modifier.ctrl;
                logTest('api-output', `Ctrl modifier key code: ${ctrlCode}`, 'info');
                
                logTest('api-output', '✅ API tests completed', 'success');
                
            } catch (e) {
                logTest('api-output', `❌ API tests failed: ${e.message}`, 'error');
            }
        }

        // Scope tests
        function testScopes() {
            clearOutput('api-output');
            
            if (!hotkeysInstance) {
                logTest('api-output', '❌ 没有可用的 hotkeys 实例', 'error');
                return;
            }

            try {
                logTest('api-output', '🎯 Starting scope tests', 'info');
                
                // Get current scope
                const originalScope = hotkeysInstance.getScope();
                logTest('api-output', `Original scope: ${originalScope}`, 'info');
                
                // Set test scope
                hotkeysInstance.setScope('test-scope');
                const newScope = hotkeysInstance.getScope();
                logTest('api-output', `Set new scope: ${newScope}`, newScope === 'test-scope' ? 'success' : 'error');
                
                // Restore original scope
                hotkeysInstance.setScope(originalScope);
                const restoredScope = hotkeysInstance.getScope();
                logTest('api-output', `Restored scope: ${restoredScope}`, restoredScope === originalScope ? 'success' : 'error');
                
                logTest('api-output', '✅ Scope tests completed', 'success');
                
            } catch (e) {
                logTest('api-output', `❌ Scope tests failed: ${e.message}`, 'error');
            }
        }

        // Key mapping tests
        function testKeyMapping() {
            clearOutput('api-output');
            
            if (!hotkeysInstance) {
                logTest('api-output', '❌ 没有可用的 hotkeys 实例', 'error');
                return;
            }

            try {
                logTest('api-output', '🗝️ Starting key mapping tests', 'info');
                
                // Test common key mappings
                const commonKeys = ['enter', 'esc', 'space', 'tab', 'backspace', 'delete'];
                commonKeys.forEach(key => {
                    const code = hotkeysInstance.keyMap[key];
                    if (code !== undefined) {
                        logTest('api-output', `✅ ${key} -> ${code}`, 'success');
                    } else {
                        logTest('api-output', `❌ ${key} mapping missing`, 'error');
                    }
                });
                
                // Test modifier key mappings
                const modifiers = ['ctrl', 'alt', 'shift', 'meta'];
                modifiers.forEach(mod => {
                    const code = hotkeysInstance.modifier[mod];
                    if (code !== undefined) {
                        logTest('api-output', `✅ ${mod} -> ${code}`, 'success');
                    } else {
                        logTest('api-output', `❌ ${mod} mapping missing`, 'error');
                    }
                });
                
                // Count key mappings
                const keyCount = Object.keys(hotkeysInstance.keyMap).length;
                const modCount = Object.keys(hotkeysInstance.modifier).length;
                logTest('api-output', `Total key mappings: ${keyCount}, modifier keys: ${modCount}`, 'info');
                
                logTest('api-output', '✅ Key mapping tests completed', 'success');
                
            } catch (e) {
                logTest('api-output', `❌ Key mapping tests failed: ${e.message}`, 'error');
            }
        }

        // Run tests after page load
        document.addEventListener('DOMContentLoaded', function() {
            logTest('module-details', '🚀 Starting compatibility tests', 'info', 'Loading and testing various module formats');
            
            // Test IIFE
            testIIFE();
            
            // Delay UMD test (avoid conflicts)
            setTimeout(testUMD, 1000);
            
            // Delay ESM test
            setTimeout(testESM, 1500);
            
            // Setup common hotkeys
            setTimeout(() => {
                setupCommonHotkeys();
                logTest('module-details', '✅ Initialization completed', 'success', 'All tests are ready');
            }, 2000);
        });
    </script>
</body>
</html>

================================================
FILE: dist/hotkeys-js.js
================================================
/*!
 * hotkeys-js v4.0.2
 * A simple micro-library for defining and dispatching keyboard shortcuts. It has no dependencies.
 * 
 * @author kenny wong <wowohoo@qq.com>
 * @license MIT
 * @homepage https://jaywcjlove.github.io/hotkeys-js
 */
const isff = typeof navigator !== "undefined" ? navigator.userAgent.toLowerCase().indexOf("firefox") > 0 : false;
function addEvent(object, event, method, useCapture) {
  if (object.addEventListener) {
    object.addEventListener(event, method, useCapture);
  } else if (object.attachEvent) {
    object.attachEvent(`on${event}`, method);
  }
}
function removeEvent(object, event, method, useCapture) {
  if (!object) return;
  if (object.removeEventListener) {
    object.removeEventListener(event, method, useCapture);
  } else if (object.detachEvent) {
    object.detachEvent(`on${event}`, method);
  }
}
function getMods(modifier, key) {
  const modsKeys = key.slice(0, key.length - 1);
  const modsCodes = [];
  for (let i = 0; i < modsKeys.length; i++) {
    modsCodes.push(modifier[modsKeys[i].toLowerCase()]);
  }
  return modsCodes;
}
function getKeys(key) {
  if (typeof key !== "string") key = "";
  key = key.replace(/\s/g, "");
  const keys = key.split(",");
  let index = keys.lastIndexOf("");
  for (; index >= 0; ) {
    keys[index - 1] += ",";
    keys.splice(index, 1);
    index = keys.lastIndexOf("");
  }
  return keys;
}
function compareArray(a1, a2) {
  const arr1 = a1.length >= a2.length ? a1 : a2;
  const arr2 = a1.length >= a2.length ? a2 : a1;
  let isIndex = true;
  for (let i = 0; i < arr1.length; i++) {
    if (arr2.indexOf(arr1[i]) === -1) isIndex = false;
  }
  return isIndex;
}
function getLayoutIndependentKeyCode(event) {
  let key = event.keyCode || event.which || event.charCode;
  if (event.code && /^Key[A-Z]$/.test(event.code)) {
    key = event.code.charCodeAt(3);
  }
  return key;
}
const _keyMap = {
  backspace: 8,
  "⌫": 8,
  tab: 9,
  clear: 12,
  enter: 13,
  "↩": 13,
  return: 13,
  esc: 27,
  escape: 27,
  space: 32,
  left: 37,
  up: 38,
  right: 39,
  down: 40,
  /// https://w3c.github.io/uievents/#events-keyboard-key-location
  arrowup: 38,
  arrowdown: 40,
  arrowleft: 37,
  arrowright: 39,
  del: 46,
  delete: 46,
  ins: 45,
  insert: 45,
  home: 36,
  end: 35,
  pageup: 33,
  pagedown: 34,
  capslock: 20,
  num_0: 96,
  num_1: 97,
  num_2: 98,
  num_3: 99,
  num_4: 100,
  num_5: 101,
  num_6: 102,
  num_7: 103,
  num_8: 104,
  num_9: 105,
  num_multiply: 106,
  num_add: 107,
  num_enter: 108,
  num_subtract: 109,
  num_decimal: 110,
  num_divide: 111,
  "⇪": 20,
  ",": 188,
  ".": 190,
  "/": 191,
  "`": 192,
  "-": isff ? 173 : 189,
  "=": isff ? 61 : 187,
  ";": isff ? 59 : 186,
  "'": 222,
  "{": 219,
  "}": 221,
  "[": 219,
  "]": 221,
  "\\": 220
};
const _modifier = {
  // shiftKey
  "⇧": 16,
  shift: 16,
  // altKey
  "⌥": 18,
  alt: 18,
  option: 18,
  // ctrlKey
  "⌃": 17,
  ctrl: 17,
  control: 17,
  // metaKey
  "⌘": 91,
  cmd: 91,
  meta: 91,
  command: 91
};
const modifierMap = {
  16: "shiftKey",
  18: "altKey",
  17: "ctrlKey",
  91: "metaKey",
  shiftKey: 16,
  ctrlKey: 17,
  altKey: 18,
  metaKey: 91
};
const _mods = {
  16: false,
  18: false,
  17: false,
  91: false
};
const _handlers = {};
for (let k = 1; k < 20; k++) {
  _keyMap[`f${k}`] = 111 + k;
}
let _downKeys = [];
let winListendFocus = null;
let winListendFullscreen = null;
let _scope = "all";
const elementEventMap = /* @__PURE__ */ new Map();
const code = (x) => _keyMap[x.toLowerCase()] || _modifier[x.toLowerCase()] || x.toUpperCase().charCodeAt(0);
const getKey = (x) => Object.keys(_keyMap).find((k) => _keyMap[k] === x);
const getModifier = (x) => Object.keys(_modifier).find((k) => _modifier[k] === x);
const setScope = (scope) => {
  _scope = scope || "all";
};
const getScope = () => {
  return _scope || "all";
};
const getPressedKeyCodes = () => {
  return _downKeys.slice(0);
};
const getPressedKeyString = () => {
  return _downKeys.map(
    (c) => getKey(c) || getModifier(c) || String.fromCharCode(c)
  );
};
const getAllKeyCodes = () => {
  const result = [];
  Object.keys(_handlers).forEach((k) => {
    _handlers[k].forEach(({ key, scope, mods, shortcut }) => {
      result.push({
        scope,
        shortcut,
        mods,
        keys: key.split("+").map((v) => code(v))
      });
    });
  });
  return result;
};
const filter = (event) => {
  const target = event.target || event.srcElement;
  const { tagName } = target;
  let flag = true;
  const isInput = tagName === "INPUT" && ![
    "checkbox",
    "radio",
    "range",
    "button",
    "file",
    "reset",
    "submit",
    "color"
  ].includes(target.type);
  if (target.isContentEditable || (isInput || tagName === "TEXTAREA" || tagName === "SELECT") && !target.readOnly) {
    flag = false;
  }
  return flag;
};
const isPressed = (keyCode) => {
  if (typeof keyCode === "string") {
    keyCode = code(keyCode);
  }
  return _downKeys.indexOf(keyCode) !== -1;
};
const deleteScope = (scope, newScope) => {
  let handlers;
  let i;
  if (!scope) scope = getScope();
  for (const key in _handlers) {
    if (Object.prototype.hasOwnProperty.call(_handlers, key)) {
      handlers = _handlers[key];
      for (i = 0; i < handlers.length; ) {
        if (handlers[i].scope === scope) {
          const deleteItems = handlers.splice(i, 1);
          deleteItems.forEach(({ element }) => removeKeyEvent(element));
        } else {
          i++;
        }
      }
    }
  }
  if (getScope() === scope) setScope(newScope || "all");
};
function clearModifier(event) {
  let key = getLayoutIndependentKeyCode(event);
  if (event.key && event.key.toLowerCase() === "capslock") {
    key = code(event.key);
  }
  const i = _downKeys.indexOf(key);
  if (i >= 0) {
    _downKeys.splice(i, 1);
  }
  if (event.key && event.key.toLowerCase() === "meta") {
    _downKeys.splice(0, _downKeys.length);
  }
  if (key === 93 || key === 224) key = 91;
  if (key in _mods) {
    _mods[key] = false;
    for (const k in _modifier)
      if (_modifier[k] === key) hotkeys[k] = false;
  }
}
const unbind = (keysInfo, ...args) => {
  if (typeof keysInfo === "undefined") {
    Object.keys(_handlers).forEach((key) => {
      if (Array.isArray(_handlers[key])) {
        _handlers[key].forEach((info) => eachUnbind(info));
      }
      delete _handlers[key];
    });
    removeKeyEvent(null);
  } else if (Array.isArray(keysInfo)) {
    keysInfo.forEach((info) => {
      if (info.key) eachUnbind(info);
    });
  } else if (typeof keysInfo === "object") {
    if (keysInfo.key) eachUnbind(keysInfo);
  } else if (typeof keysInfo === "string") {
    let [scope, method] = args;
    if (typeof scope === "function") {
      method = scope;
      scope = "";
    }
    eachUnbind({
      key: keysInfo,
      scope,
      method,
      splitKey: "+"
    });
  }
};
const eachUnbind = ({
  key,
  scope,
  method,
  splitKey = "+"
}) => {
  const multipleKeys = getKeys(key);
  multipleKeys.forEach((originKey) => {
    const unbindKeys = originKey.split(splitKey);
    const len = unbindKeys.length;
    const lastKey = unbindKeys[len - 1];
    const keyCode = lastKey === "*" ? "*" : code(lastKey);
    if (!_handlers[keyCode]) return;
    if (!scope) scope = getScope();
    const mods = len > 1 ? getMods(_modifier, unbindKeys) : [];
    const unbindElements = [];
    _handlers[keyCode] = _handlers[keyCode].filter((record) => {
      const isMatchingMethod = method ? record.method === method : true;
      const isUnbind = isMatchingMethod && record.scope === scope && compareArray(record.mods, mods);
      if (isUnbind) unbindElements.push(record.element);
      return !isUnbind;
    });
    unbindElements.forEach((element) => removeKeyEvent(element));
  });
};
function eventHandler(event, handler, scope, element) {
  if (handler.element !== element) {
    return;
  }
  let modifiersMatch;
  if (handler.scope === scope || handler.scope === "all") {
    modifiersMatch = handler.mods.length > 0;
    for (const y in _mods) {
      if (Object.prototype.hasOwnProperty.call(_mods, y)) {
        if (!_mods[y] && handler.mods.indexOf(+y) > -1 || _mods[y] && handler.mods.indexOf(+y) === -1) {
          modifiersMatch = false;
        }
      }
    }
    if (handler.mods.length === 0 && !_mods[16] && !_mods[18] && !_mods[17] && !_mods[91] || modifiersMatch || handler.shortcut === "*") {
      handler.keys = [];
      handler.keys = handler.keys.concat(_downKeys);
      if (handler.method(event, handler) === false) {
        if (event.preventDefault) event.preventDefault();
        else event.returnValue = false;
        if (event.stopPropagation) event.stopPropagation();
        if (event.cancelBubble) event.cancelBubble = true;
      }
    }
  }
}
function dispatch(event, element) {
  const asterisk = _handlers["*"];
  let key = getLayoutIndependentKeyCode(event);
  if (event.key && event.key.toLowerCase() === "capslock") {
    return;
  }
  const filterFn = hotkeys.filter || filter;
  if (!filterFn.call(this, event)) return;
  if (key === 93 || key === 224) key = 91;
  if (_downKeys.indexOf(key) === -1 && key !== 229) _downKeys.push(key);
  ["metaKey", "ctrlKey", "altKey", "shiftKey"].forEach((keyName) => {
    const keyNum = modifierMap[keyName];
    if (event[keyName] && _downKeys.indexOf(keyNum) === -1) {
      _downKeys.push(keyNum);
    } else if (!event[keyName] && _downKeys.indexOf(keyNum) > -1) {
      _downKeys.splice(_downKeys.indexOf(keyNum), 1);
    } else if (keyName === "metaKey" && event[keyName]) {
      _downKeys = _downKeys.filter((k) => k in modifierMap || k === key);
    }
  });
  if (key in _mods) {
    _mods[key] = true;
    for (const k in _modifier) {
      if (Object.prototype.hasOwnProperty.call(_modifier, k)) {
        const eventKey = modifierMap[_modifier[k]];
        hotkeys[k] = event[eventKey];
      }
    }
    if (!asterisk) return;
  }
  for (const e in _mods) {
    if (Object.prototype.hasOwnProperty.call(_mods, e)) {
      _mods[e] = event[modifierMap[e]];
    }
  }
  if (event.getModifierState && !(event.altKey && !event.ctrlKey) && event.getModifierState("AltGraph")) {
    if (_downKeys.indexOf(17) === -1) {
      _downKeys.push(17);
    }
    if (_downKeys.indexOf(18) === -1) {
      _downKeys.push(18);
    }
    _mods[17] = true;
    _mods[18] = true;
  }
  const scope = getScope();
  if (asterisk) {
    for (let i = 0; i < asterisk.length; i++) {
      if (asterisk[i].scope === scope && (event.type === "keydown" && asterisk[i].keydown || event.type === "keyup" && asterisk[i].keyup)) {
        eventHandler(event, asterisk[i], scope, element);
      }
    }
  }
  if (!(key in _handlers)) return;
  const handlerKey = _handlers[key];
  const keyLen = handlerKey.length;
  for (let i = 0; i < keyLen; i++) {
    if (event.type === "keydown" && handlerKey[i].keydown || event.type === "keyup" && handlerKey[i].keyup) {
      if (handlerKey[i].key) {
        const record = handlerKey[i];
        const { splitKey } = record;
        const keyShortcut = record.key.split(splitKey);
        const _downKeysCurrent = [];
        for (let a = 0; a < keyShortcut.length; a++) {
          _downKeysCurrent.push(code(keyShortcut[a]));
        }
        if (_downKeysCurrent.sort().join("") === _downKeys.sort().join("")) {
          eventHandler(event, record, scope, element);
        }
      }
    }
  }
}
const hotkeys = function hotkeys2(key, option, method) {
  _downKeys = [];
  const keys = getKeys(key);
  let mods = [];
  let scope = "all";
  let element = document;
  let i = 0;
  let keyup = false;
  let keydown = true;
  let splitKey = "+";
  let capture = false;
  let single = false;
  if (method === void 0 && typeof option === "function") {
    method = option;
  }
  if (Object.prototype.toString.call(option) === "[object Object]") {
    const opts = option;
    if (opts.scope) scope = opts.scope;
    if (opts.element) element = opts.element;
    if (opts.keyup) keyup = opts.keyup;
    if (opts.keydown !== void 0) keydown = opts.keydown;
    if (opts.capture !== void 0) capture = opts.capture;
    if (typeof opts.splitKey === "string") splitKey = opts.splitKey;
    if (opts.single === true) single = true;
  }
  if (typeof option === "string") scope = option;
  if (single) unbind(key, scope);
  for (; i < keys.length; i++) {
    const currentKey = keys[i].split(splitKey);
    mods = [];
    if (currentKey.length > 1) mods = getMods(_modifier, currentKey);
    let finalKey = currentKey[currentKey.length - 1];
    finalKey = finalKey === "*" ? "*" : code(finalKey);
    if (!(finalKey in _handlers)) _handlers[finalKey] = [];
    _handlers[finalKey].push({
      keyup,
      keydown,
      scope,
      mods,
      shortcut: keys[i],
      method,
      key: keys[i],
      splitKey,
      element
    });
  }
  if (typeof element !== "undefined" && typeof window !== "undefined") {
    if (!elementEventMap.has(element)) {
      const keydownListener = (event = window.event) => dispatch(event, element);
      const keyupListenr = (event = window.event) => {
        dispatch(event, element);
        clearModifier(event);
      };
      elementEventMap.set(element, { keydownListener, keyupListenr, capture });
      addEvent(element, "keydown", keydownListener, capture);
      addEvent(element, "keyup", keyupListenr, capture);
    }
    if (!winListendFocus) {
      const listener = () => {
        _downKeys = [];
      };
      winListendFocus = { listener, capture };
      addEvent(window, "focus", listener, capture);
    }
    if (!winListendFullscreen && typeof document !== "undefined") {
      const onFullscreenChange = () => {
        _downKeys = [];
        for (const k in _mods) _mods[k] = false;
        for (const k in _modifier) hotkeys2[k] = false;
      };
      const fullscreenListener = onFullscreenChange;
      const webkitListener = onFullscreenChange;
      document.addEventListener("fullscreenchange", fullscreenListener);
      document.addEventListener("webkitfullscreenchange", webkitListener);
      winListendFullscreen = { fullscreen: fullscreenListener, webkit: webkitListener };
    }
  }
};
function trigger(shortcut, scope = "all") {
  Object.keys(_handlers).forEach((key) => {
    const dataList = _handlers[key].filter(
      (item) => item.scope === scope && item.shortcut === shortcut
    );
    dataList.forEach((data) => {
      if (data && data.method) {
        data.method({}, data);
      }
    });
  });
}
function removeKeyEvent(element) {
  const values = Object.values(_handlers).flat();
  const findindex = values.findIndex(({ element: el }) => el === element);
  if (findindex < 0 && element) {
    const { keydownListener, keyupListenr, capture } = elementEventMap.get(element) || {};
    if (keydownListener && keyupListenr) {
      removeEvent(element, "keyup", keyupListenr, capture);
      removeEvent(element, "keydown", keydownListener, capture);
      elementEventMap.delete(element);
    }
  }
  if (values.length <= 0 || elementEventMap.size <= 0) {
    const eventKeys = Array.from(elementEventMap.keys());
    eventKeys.forEach((el) => {
      const { keydownListener, keyupListenr, capture } = elementEventMap.get(el) || {};
      if (keydownListener && keyupListenr) {
        removeEvent(el, "keyup", keyupListenr, capture);
        removeEvent(el, "keydown", keydownListener, capture);
        elementEventMap.delete(el);
      }
    });
    elementEventMap.clear();
    Object.keys(_handlers).forEach((key) => delete _handlers[key]);
    if (winListendFocus) {
      const { listener, capture } = winListendFocus;
      removeEvent(window, "focus", listener, capture);
      winListendFocus = null;
    }
    if (winListendFullscreen && typeof document !== "undefined") {
      document.removeEventListener("fullscreenchange", winListendFullscreen.fullscreen);
      document.removeEventListener("webkitfullscreenchange", winListendFullscreen.webkit);
      winListendFullscreen = null;
    }
  }
}
const _api = {
  getPressedKeyString,
  setScope,
  getScope,
  deleteScope,
  getPressedKeyCodes,
  getAllKeyCodes,
  isPressed,
  filter,
  trigger,
  unbind,
  keyMap: _keyMap,
  modifier: _modifier,
  modifierMap
};
for (const a in _api) {
  const key = a;
  if (Object.prototype.hasOwnProperty.call(_api, key)) {
    hotkeys[key] = _api[key];
  }
}
if (typeof window !== "undefined") {
  const _hotkeys = window.hotkeys;
  hotkeys.noConflict = (deep) => {
    if (deep && window.hotkeys === hotkeys) {
      window.hotkeys = _hotkeys;
    }
    return hotkeys;
  };
  window.hotkeys = hotkeys;
}
if (typeof module !== "undefined" && module.exports) {
  module.exports = hotkeys;
  module.exports.default = hotkeys;
}
export {
  hotkeys as default
};
//# sourceMappingURL=hotkeys-js.js.map


================================================
FILE: dist/hotkeys-js.umd.cjs
================================================
(function(global, factory) {
  typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory() : typeof define === "function" && define.amd ? define(factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, global.hotkeys = factory());
})(this, function() {
  "use strict";/*!
 * hotkeys-js v4.0.2
 * A simple micro-library for defining and dispatching keyboard shortcuts. It has no dependencies.
 * 
 * @author kenny wong <wowohoo@qq.com>
 * @license MIT
 * @homepage https://jaywcjlove.github.io/hotkeys-js
 */

  const isff = typeof navigator !== "undefined" ? navigator.userAgent.toLowerCase().indexOf("firefox") > 0 : false;
  function addEvent(object, event, method, useCapture) {
    if (object.addEventListener) {
      object.addEventListener(event, method, useCapture);
    } else if (object.attachEvent) {
      object.attachEvent(`on${event}`, method);
    }
  }
  function removeEvent(object, event, method, useCapture) {
    if (!object) return;
    if (object.removeEventListener) {
      object.removeEventListener(event, method, useCapture);
    } else if (object.detachEvent) {
      object.detachEvent(`on${event}`, method);
    }
  }
  function getMods(modifier, key) {
    const modsKeys = key.slice(0, key.length - 1);
    const modsCodes = [];
    for (let i = 0; i < modsKeys.length; i++) {
      modsCodes.push(modifier[modsKeys[i].toLowerCase()]);
    }
    return modsCodes;
  }
  function getKeys(key) {
    if (typeof key !== "string") key = "";
    key = key.replace(/\s/g, "");
    const keys = key.split(",");
    let index = keys.lastIndexOf("");
    for (; index >= 0; ) {
      keys[index - 1] += ",";
      keys.splice(index, 1);
      index = keys.lastIndexOf("");
    }
    return keys;
  }
  function compareArray(a1, a2) {
    const arr1 = a1.length >= a2.length ? a1 : a2;
    const arr2 = a1.length >= a2.length ? a2 : a1;
    let isIndex = true;
    for (let i = 0; i < arr1.length; i++) {
      if (arr2.indexOf(arr1[i]) === -1) isIndex = false;
    }
    return isIndex;
  }
  function getLayoutIndependentKeyCode(event) {
    let key = event.keyCode || event.which || event.charCode;
    if (event.code && /^Key[A-Z]$/.test(event.code)) {
      key = event.code.charCodeAt(3);
    }
    return key;
  }
  const _keyMap = {
    backspace: 8,
    "⌫": 8,
    tab: 9,
    clear: 12,
    enter: 13,
    "↩": 13,
    return: 13,
    esc: 27,
    escape: 27,
    space: 32,
    left: 37,
    up: 38,
    right: 39,
    down: 40,
    /// https://w3c.github.io/uievents/#events-keyboard-key-location
    arrowup: 38,
    arrowdown: 40,
    arrowleft: 37,
    arrowright: 39,
    del: 46,
    delete: 46,
    ins: 45,
    insert: 45,
    home: 36,
    end: 35,
    pageup: 33,
    pagedown: 34,
    capslock: 20,
    num_0: 96,
    num_1: 97,
    num_2: 98,
    num_3: 99,
    num_4: 100,
    num_5: 101,
    num_6: 102,
    num_7: 103,
    num_8: 104,
    num_9: 105,
    num_multiply: 106,
    num_add: 107,
    num_enter: 108,
    num_subtract: 109,
    num_decimal: 110,
    num_divide: 111,
    "⇪": 20,
    ",": 188,
    ".": 190,
    "/": 191,
    "`": 192,
    "-": isff ? 173 : 189,
    "=": isff ? 61 : 187,
    ";": isff ? 59 : 186,
    "'": 222,
    "{": 219,
    "}": 221,
    "[": 219,
    "]": 221,
    "\\": 220
  };
  const _modifier = {
    // shiftKey
    "⇧": 16,
    shift: 16,
    // altKey
    "⌥": 18,
    alt: 18,
    option: 18,
    // ctrlKey
    "⌃": 17,
    ctrl: 17,
    control: 17,
    // metaKey
    "⌘": 91,
    cmd: 91,
    meta: 91,
    command: 91
  };
  const modifierMap = {
    16: "shiftKey",
    18: "altKey",
    17: "ctrlKey",
    91: "metaKey",
    shiftKey: 16,
    ctrlKey: 17,
    altKey: 18,
    metaKey: 91
  };
  const _mods = {
    16: false,
    18: false,
    17: false,
    91: false
  };
  const _handlers = {};
  for (let k = 1; k < 20; k++) {
    _keyMap[`f${k}`] = 111 + k;
  }
  let _downKeys = [];
  let winListendFocus = null;
  let winListendFullscreen = null;
  let _scope = "all";
  const elementEventMap = /* @__PURE__ */ new Map();
  const code = (x) => _keyMap[x.toLowerCase()] || _modifier[x.toLowerCase()] || x.toUpperCase().charCodeAt(0);
  const getKey = (x) => Object.keys(_keyMap).find((k) => _keyMap[k] === x);
  const getModifier = (x) => Object.keys(_modifier).find((k) => _modifier[k] === x);
  const setScope = (scope) => {
    _scope = scope || "all";
  };
  const getScope = () => {
    return _scope || "all";
  };
  const getPressedKeyCodes = () => {
    return _downKeys.slice(0);
  };
  const getPressedKeyString = () => {
    return _downKeys.map(
      (c) => getKey(c) || getModifier(c) || String.fromCharCode(c)
    );
  };
  const getAllKeyCodes = () => {
    const result = [];
    Object.keys(_handlers).forEach((k) => {
      _handlers[k].forEach(({ key, scope, mods, shortcut }) => {
        result.push({
          scope,
          shortcut,
          mods,
          keys: key.split("+").map((v) => code(v))
        });
      });
    });
    return result;
  };
  const filter = (event) => {
    const target = event.target || event.srcElement;
    const { tagName } = target;
    let flag = true;
    const isInput = tagName === "INPUT" && ![
      "checkbox",
      "radio",
      "range",
      "button",
      "file",
      "reset",
      "submit",
      "color"
    ].includes(target.type);
    if (target.isContentEditable || (isInput || tagName === "TEXTAREA" || tagName === "SELECT") && !target.readOnly) {
      flag = false;
    }
    return flag;
  };
  const isPressed = (keyCode) => {
    if (typeof keyCode === "string") {
      keyCode = code(keyCode);
    }
    return _downKeys.indexOf(keyCode) !== -1;
  };
  const deleteScope = (scope, newScope) => {
    let handlers;
    let i;
    if (!scope) scope = getScope();
    for (const key in _handlers) {
      if (Object.prototype.hasOwnProperty.call(_handlers, key)) {
        handlers = _handlers[key];
        for (i = 0; i < handlers.length; ) {
          if (handlers[i].scope === scope) {
            const deleteItems = handlers.splice(i, 1);
            deleteItems.forEach(({ element }) => removeKeyEvent(element));
          } else {
            i++;
          }
        }
      }
    }
    if (getScope() === scope) setScope(newScope || "all");
  };
  function clearModifier(event) {
    let key = getLayoutIndependentKeyCode(event);
    if (event.key && event.key.toLowerCase() === "capslock") {
      key = code(event.key);
    }
    const i = _downKeys.indexOf(key);
    if (i >= 0) {
      _downKeys.splice(i, 1);
    }
    if (event.key && event.key.toLowerCase() === "meta") {
      _downKeys.splice(0, _downKeys.length);
    }
    if (key === 93 || key === 224) key = 91;
    if (key in _mods) {
      _mods[key] = false;
      for (const k in _modifier)
        if (_modifier[k] === key) hotkeys2[k] = false;
    }
  }
  const unbind = (keysInfo, ...args) => {
    if (typeof keysInfo === "undefined") {
      Object.keys(_handlers).forEach((key) => {
        if (Array.isArray(_handlers[key])) {
          _handlers[key].forEach((info) => eachUnbind(info));
        }
        delete _handlers[key];
      });
      removeKeyEvent(null);
    } else if (Array.isArray(keysInfo)) {
      keysInfo.forEach((info) => {
        if (info.key) eachUnbind(info);
      });
    } else if (typeof keysInfo === "object") {
      if (keysInfo.key) eachUnbind(keysInfo);
    } else if (typeof keysInfo === "string") {
      let [scope, method] = args;
      if (typeof scope === "function") {
        method = scope;
        scope = "";
      }
      eachUnbind({
        key: keysInfo,
        scope,
        method,
        splitKey: "+"
      });
    }
  };
  const eachUnbind = ({
    key,
    scope,
    method,
    splitKey = "+"
  }) => {
    const multipleKeys = getKeys(key);
    multipleKeys.forEach((originKey) => {
      const unbindKeys = originKey.split(splitKey);
      const len = unbindKeys.length;
      const lastKey = unbindKeys[len - 1];
      const keyCode = lastKey === "*" ? "*" : code(lastKey);
      if (!_handlers[keyCode]) return;
      if (!scope) scope = getScope();
      const mods = len > 1 ? getMods(_modifier, unbindKeys) : [];
      const unbindElements = [];
      _handlers[keyCode] = _handlers[keyCode].filter((record) => {
        const isMatchingMethod = method ? record.method === method : true;
        const isUnbind = isMatchingMethod && record.scope === scope && compareArray(record.mods, mods);
        if (isUnbind) unbindElements.push(record.element);
        return !isUnbind;
      });
      unbindElements.forEach((element) => removeKeyEvent(element));
    });
  };
  function eventHandler(event, handler, scope, element) {
    if (handler.element !== element) {
      return;
    }
    let modifiersMatch;
    if (handler.scope === scope || handler.scope === "all") {
      modifiersMatch = handler.mods.length > 0;
      for (const y in _mods) {
        if (Object.prototype.hasOwnProperty.call(_mods, y)) {
          if (!_mods[y] && handler.mods.indexOf(+y) > -1 || _mods[y] && handler.mods.indexOf(+y) === -1) {
            modifiersMatch = false;
          }
        }
      }
      if (handler.mods.length === 0 && !_mods[16] && !_mods[18] && !_mods[17] && !_mods[91] || modifiersMatch || handler.shortcut === "*") {
        handler.keys = [];
        handler.keys = handler.keys.concat(_downKeys);
        if (handler.method(event, handler) === false) {
          if (event.preventDefault) event.preventDefault();
          else event.returnValue = false;
          if (event.stopPropagation) event.stopPropagation();
          if (event.cancelBubble) event.cancelBubble = true;
        }
      }
    }
  }
  function dispatch(event, element) {
    const asterisk = _handlers["*"];
    let key = getLayoutIndependentKeyCode(event);
    if (event.key && event.key.toLowerCase() === "capslock") {
      return;
    }
    const filterFn = hotkeys2.filter || filter;
    if (!filterFn.call(this, event)) return;
    if (key === 93 || key === 224) key = 91;
    if (_downKeys.indexOf(key) === -1 && key !== 229) _downKeys.push(key);
    ["metaKey", "ctrlKey", "altKey", "shiftKey"].forEach((keyName) => {
      const keyNum = modifierMap[keyName];
      if (event[keyName] && _downKeys.indexOf(keyNum) === -1) {
        _downKeys.push(keyNum);
      } else if (!event[keyName] && _downKeys.indexOf(keyNum) > -1) {
        _downKeys.splice(_downKeys.indexOf(keyNum), 1);
      } else if (keyName === "metaKey" && event[keyName]) {
        _downKeys = _downKeys.filter((k) => k in modifierMap || k === key);
      }
    });
    if (key in _mods) {
      _mods[key] = true;
      for (const k in _modifier) {
        if (Object.prototype.hasOwnProperty.call(_modifier, k)) {
          const eventKey = modifierMap[_modifier[k]];
          hotkeys2[k] = event[eventKey];
        }
      }
      if (!asterisk) return;
    }
    for (const e in _mods) {
      if (Object.prototype.hasOwnProperty.call(_mods, e)) {
        _mods[e] = event[modifierMap[e]];
      }
    }
    if (event.getModifierState && !(event.altKey && !event.ctrlKey) && event.getModifierState("AltGraph")) {
      if (_downKeys.indexOf(17) === -1) {
        _downKeys.push(17);
      }
      if (_downKeys.indexOf(18) === -1) {
        _downKeys.push(18);
      }
      _mods[17] = true;
      _mods[18] = true;
    }
    const scope = getScope();
    if (asterisk) {
      for (let i = 0; i < asterisk.length; i++) {
        if (asterisk[i].scope === scope && (event.type === "keydown" && asterisk[i].keydown || event.type === "keyup" && asterisk[i].keyup)) {
          eventHandler(event, asterisk[i], scope, element);
        }
      }
    }
    if (!(key in _handlers)) return;
    const handlerKey = _handlers[key];
    const keyLen = handlerKey.length;
    for (let i = 0; i < keyLen; i++) {
      if (event.type === "keydown" && handlerKey[i].keydown || event.type === "keyup" && handlerKey[i].keyup) {
        if (handlerKey[i].key) {
          const record = handlerKey[i];
          const { splitKey } = record;
          const keyShortcut = record.key.split(splitKey);
          const _downKeysCurrent = [];
          for (let a = 0; a < keyShortcut.length; a++) {
            _downKeysCurrent.push(code(keyShortcut[a]));
          }
          if (_downKeysCurrent.sort().join("") === _downKeys.sort().join("")) {
            eventHandler(event, record, scope, element);
          }
        }
      }
    }
  }
  const hotkeys2 = function hotkeys22(key, option, method) {
    _downKeys = [];
    const keys = getKeys(key);
    let mods = [];
    let scope = "all";
    let element = document;
    let i = 0;
    let keyup = false;
    let keydown = true;
    let splitKey = "+";
    let capture = false;
    let single = false;
    if (method === void 0 && typeof option === "function") {
      method = option;
    }
    if (Object.prototype.toString.call(option) === "[object Object]") {
      const opts = option;
      if (opts.scope) scope = opts.scope;
      if (opts.element) element = opts.element;
      if (opts.keyup) keyup = opts.keyup;
      if (opts.keydown !== void 0) keydown = opts.keydown;
      if (opts.capture !== void 0) capture = opts.capture;
      if (typeof opts.splitKey === "string") splitKey = opts.splitKey;
      if (opts.single === true) single = true;
    }
    if (typeof option === "string") scope = option;
    if (single) unbind(key, scope);
    for (; i < keys.length; i++) {
      const currentKey = keys[i].split(splitKey);
      mods = [];
      if (currentKey.length > 1) mods = getMods(_modifier, currentKey);
      let finalKey = currentKey[currentKey.length - 1];
      finalKey = finalKey === "*" ? "*" : code(finalKey);
      if (!(finalKey in _handlers)) _handlers[finalKey] = [];
      _handlers[finalKey].push({
        keyup,
        keydown,
        scope,
        mods,
        shortcut: keys[i],
        method,
        key: keys[i],
        splitKey,
        element
      });
    }
    if (typeof element !== "undefined" && typeof window !== "undefined") {
      if (!elementEventMap.has(element)) {
        const keydownListener = (event = window.event) => dispatch(event, element);
        const keyupListenr = (event = window.event) => {
          dispatch(event, element);
          clearModifier(event);
        };
        elementEventMap.set(element, { keydownListener, keyupListenr, capture });
        addEvent(element, "keydown", keydownListener, capture);
        addEvent(element, "keyup", keyupListenr, capture);
      }
      if (!winListendFocus) {
        const listener = () => {
          _downKeys = [];
        };
        winListendFocus = { listener, capture };
        addEvent(window, "focus", listener, capture);
      }
      if (!winListendFullscreen && typeof document !== "undefined") {
        const onFullscreenChange = () => {
          _downKeys = [];
          for (const k in _mods) _mods[k] = false;
          for (const k in _modifier) hotkeys22[k] = false;
        };
        const fullscreenListener = onFullscreenChange;
        const webkitListener = onFullscreenChange;
        document.addEventListener("fullscreenchange", fullscreenListener);
        document.addEventListener("webkitfullscreenchange", webkitListener);
        winListendFullscreen = { fullscreen: fullscreenListener, webkit: webkitListener };
      }
    }
  };
  function trigger(shortcut, scope = "all") {
    Object.keys(_handlers).forEach((key) => {
      const dataList = _handlers[key].filter(
        (item) => item.scope === scope && item.shortcut === shortcut
      );
      dataList.forEach((data) => {
        if (data && data.method) {
          data.method({}, data);
        }
      });
    });
  }
  function removeKeyEvent(element) {
    const values = Object.values(_handlers).flat();
    const findindex = values.findIndex(({ element: el }) => el === element);
    if (findindex < 0 && element) {
      const { keydownListener, keyupListenr, capture } = elementEventMap.get(element) || {};
      if (keydownListener && keyupListenr) {
        removeEvent(element, "keyup", keyupListenr, capture);
        removeEvent(element, "keydown", keydownListener, capture);
        elementEventMap.delete(element);
      }
    }
    if (values.length <= 0 || elementEventMap.size <= 0) {
      const eventKeys = Array.from(elementEventMap.keys());
      eventKeys.forEach((el) => {
        const { keydownListener, keyupListenr, capture } = elementEventMap.get(el) || {};
        if (keydownListener && keyupListenr) {
          removeEvent(el, "keyup", keyupListenr, capture);
          removeEvent(el, "keydown", keydownListener, capture);
          elementEventMap.delete(el);
        }
      });
      elementEventMap.clear();
      Object.keys(_handlers).forEach((key) => delete _handlers[key]);
      if (winListendFocus) {
        const { listener, capture } = winListendFocus;
        removeEvent(window, "focus", listener, capture);
        winListendFocus = null;
      }
      if (winListendFullscreen && typeof document !== "undefined") {
        document.removeEventListener("fullscreenchange", winListendFullscreen.fullscreen);
        document.removeEventListener("webkitfullscreenchange", winListendFullscreen.webkit);
        winListendFullscreen = null;
      }
    }
  }
  const _api = {
    getPressedKeyString,
    setScope,
    getScope,
    deleteScope,
    getPressedKeyCodes,
    getAllKeyCodes,
    isPressed,
    filter,
    trigger,
    unbind,
    keyMap: _keyMap,
    modifier: _modifier,
    modifierMap
  };
  for (const a in _api) {
    const key = a;
    if (Object.prototype.hasOwnProperty.call(_api, key)) {
      hotkeys2[key] = _api[key];
    }
  }
  if (typeof window !== "undefined") {
    const _hotkeys = window.hotkeys;
    hotkeys2.noConflict = (deep) => {
      if (deep && window.hotkeys === hotkeys2) {
        window.hotkeys = _hotkeys;
      }
      return hotkeys2;
    };
    window.hotkeys = hotkeys2;
  }
  if (typeof module !== "undefined" && module.exports) {
    module.exports = hotkeys2;
    module.exports.default = hotkeys2;
  }
  return hotkeys2;
});
if (typeof module === "object" && module.exports) {
  module.exports.default = module.exports;
}
if (typeof define === "function" && define.amd) {
  define([], function() {
    return hotkeys;
  });
}
//# sourceMappingURL=hotkeys-js.umd.cjs.map


================================================
FILE: dist/index.d.ts
================================================
declare const _default: HotkeysInterface;
export default _default;

declare type DeleteScope = (scope?: string, newScope?: string) => void;

declare type Filter = (event: KeyboardEvent) => boolean;

declare type GetAllKeyCodes = () => KeyCodeInfo[];

declare type GetPressedKeyCodes = () => number[];

declare type GetPressedKeyString = () => string[];

declare type GetScope = () => string;

declare interface HotkeysAPI {
    /**
     * Use the `hotkeys.setScope` method to set scope. There can only be one active scope besides 'all'.  By default 'all' is always active.
     *
     * ```js
     * // Define shortcuts with a scope
     * hotkeys('ctrl+o, ctrl+alt+enter', 'issues', function() {
     *   console.log('do something');
     * });
     * hotkeys('o, enter', 'files', function() {
     *   console.log('do something else');
     * });
     *
     * // Set the scope (only 'all' and 'issues' shortcuts will be honored)
     * hotkeys.setScope('issues'); // default scope is 'all'
     * ```
     */
    setScope: SetScope;
    /**
     * Use the `hotkeys.getScope` method to get scope.
     *
     * ```js
     * hotkeys.getScope();
     * ```
     */
    getScope: GetScope;
    /**
     * Use the `hotkeys.deleteScope` method to delete a scope. This will also remove all associated hotkeys with it.
     *
     * ```js
     * hotkeys.deleteScope('issues');
     * ```
     * You can use second argument, if need set new scope after deleting.
     *
     * ```js
     * hotkeys.deleteScope('issues', 'newScopeName');
     * ```
     */
    deleteScope: DeleteScope;
    /**
     * Returns an array of key codes currently pressed.
     *
     * ```js
     * hotkeys('command+ctrl+shift+a,f', function() {
     *   console.log(hotkeys.getPressedKeyCodes()); //=> [17, 65] or [70]
     * })
     * ```
     */
    getPressedKeyCodes: GetPressedKeyCodes;
    /**
     * Returns an array of key codes currently pressed.
     *
     * ```js
     * hotkeys('command+ctrl+shift+a,f', function() {
     *   console.log(hotkeys.getPressedKeyString()); //=> ['⌘', '⌃', '⇧', 'A', 'F']
     * })
     * ```
     */
    getPressedKeyString: GetPressedKeyString;
    /**
     * Get a list of all registration codes.
     *
     * ```js
     * hotkeys('command+ctrl+shift+a,f', function() {
     *   console.log(hotkeys.getAllKeyCodes());
     *   // [
     *   //   { scope: 'all', shortcut: 'command+ctrl+shift+a', mods: [91, 17, 16], keys: [91, 17, 16, 65] },
     *   //   { scope: 'all', shortcut: 'f', mods: [], keys: [42] }
     *   // ]
     * })
     * ```
     *
     */
    getAllKeyCodes: GetAllKeyCodes;
    isPressed: IsPressed;
    /**
     * By default hotkeys are not enabled for `INPUT` `SELECT` `TEXTAREA` elements.
     * `Hotkeys.filter` to return to the `true` shortcut keys set to play a role,
     * `false` shortcut keys set up failure.
     *
     * ```js
     * hotkeys.filter = function(event){
     *   return true;
     * }
     * //How to add the filter to edit labels. <div contentEditable="true"></div>
     * //"contentEditable" Older browsers that do not support drops
     * hotkeys.filter = function(event) {
     *   var target = event.target || event.srcElement;
     *   var tagName = target.tagName;
     *   return !(target.isContentEditable || tagName == 'INPUT' || tagName == 'SELECT' || tagName == 'TEXTAREA');
     * }
     *
     * hotkeys.filter = function(event){
     *   var tagName = (event.target || event.srcElement).tagName;
     *   hotkeys.setScope(/^(INPUT|TEXTAREA|SELECT)$/.test(tagName) ? 'input' : 'other');
     *   return true;
     * }
     * ```
     */
    filter: Filter;
    /**
     * trigger shortcut key event
     *
     * ```js
     * hotkeys.trigger('ctrl+o');
     * hotkeys.trigger('ctrl+o', 'scope2');
     * ```
     */
    trigger: Trigger;
    /**
     * Unbinds a shortcut key event.
     *
     * ```js
     * hotkeys.unbind('ctrl+o');
     * hotkeys.unbind('ctrl+o', 'scope1');
     * hotkeys.unbind('ctrl+o', 'scope1', method);
     * hotkeys.unbind('ctrl+o', method);
     * ```
     */
    unbind: Unbind;
    /**
     * Relinquish HotKeys’s control of the `hotkeys` variable.
     *
     * ```js
     * var k = hotkeys.noConflict();
     * k('a', function() {
     *   console.log("do something")
     * });
     *
     * hotkeys()
     * // -->Uncaught TypeError: hotkeys is not a function(anonymous function)
     * // @ VM2170:2InjectedScript._evaluateOn
     * // @ VM2165:883InjectedScript._evaluateAndWrap
     * // @ VM2165:816InjectedScript.evaluate @ VM2165:682
     * ```
     */
    noConflict: NoConflict;
    keyMap: Record<string, number>;
    modifier: Record<string, number>;
    modifierMap: Record<string | number, number | string>;
}

export declare interface HotkeysEvent {
    keyup: boolean;
    keydown: boolean;
    scope: string;
    mods: number[];
    shortcut: string;
    method: KeyHandler;
    key: string;
    splitKey: string;
    element: HTMLElement | Document;
    keys?: number[];
}

declare interface HotkeysInterface extends HotkeysAPI {
    (key: string, method: KeyHandler): void;
    (key: string, scope: string, method: KeyHandler): void;
    (key: string, option: HotkeysOptions, method: KeyHandler): void;
    shift?: boolean;
    ctrl?: boolean;
    alt?: boolean;
    option?: boolean;
    control?: boolean;
    cmd?: boolean;
    command?: boolean;
}

declare interface HotkeysOptions {
    scope?: string;
    element?: HTMLElement | Document;
    keyup?: boolean;
    keydown?: boolean;
    capture?: boolean;
    splitKey?: string;
    single?: boolean;
}

declare interface IsPressed {
    /** For example, `hotkeys.isPressed(77)` is true if the `M` key is currently pressed. */
    (keyCode: number): boolean;
    /** For example, `hotkeys.isPressed('m')` is true if the `M` key is currently pressed. */
    (keyCode: string): boolean;
}

declare interface KeyCodeInfo {
    scope: string;
    shortcut: string;
    mods: number[];
    keys: number[];
}

export declare interface KeyHandler {
    (keyboardEvent: KeyboardEvent, hotkeysEvent: HotkeysEvent): void | boolean;
}

declare type NoConflict = (deep?: boolean) => HotkeysInterface;

declare type SetScope = (scope: string) => void;

declare type Trigger = (shortcut: string, scope?: string) => void;

declare interface Unbind {
    (key?: string): void;
    (keysInfo: UnbindInfo): void;
    (keysInfo: UnbindInfo[]): void;
    (key: string, scopeName: string): void;
    (key: string, scopeName: string, method: KeyHandler): void;
    (key: string, method: KeyHandler): void;
}

declare interface UnbindInfo {
    key: string;
    scope?: string;
    method?: KeyHandler;
    splitKey?: string;
}

export { }


================================================
FILE: eslint.config.js
================================================
// @ts-check
import { defineConfig } from 'eslint/config';
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import reactPlugin from 'eslint-plugin-react';
import reactHooksPlugin from 'eslint-plugin-react-hooks';
import importPlugin from 'eslint-plugin-import';
import jsxA11yPlugin from 'eslint-plugin-jsx-a11y';
import globals from 'globals';

export default defineConfig([
  eslint.configs.recommended,
  ...tseslint.configs.recommended,
  {
    files: ['**/*.{js,jsx,ts,tsx}'],
    languageOptions: {
      ecmaVersion: 2020,
      sourceType: 'module',
      parserOptions: {
        ecmaFeatures: {
          jsx: true,
        },
      },
      globals: {
        ...globals.browser,
        ...globals.node,
        ...globals.es2020,
        ...globals.jest,
      },
    },
    plugins: {
      react: reactPlugin,
      'react-hooks': reactHooksPlugin,
      import: importPlugin,
      'jsx-a11y': jsxA11yPlugin,
    },
    settings: {
      react: {
        version: 'detect',
      },
    },
    rules: {
      // TypeScript rules
      '@typescript-eslint/no-explicit-any': 'off',
      '@typescript-eslint/explicit-module-boundary-types': 'off',
      '@typescript-eslint/no-inferrable-types': 'off',
      '@typescript-eslint/no-non-null-assertion': 'off',
      '@typescript-eslint/no-unused-expressions': 'error',

      // General rules
      'no-console': ['error', { allow: ['log'] }],
      'no-underscore-dangle': 'off',
      'no-plusplus': 'off',
      'no-param-reassign': 'off',
      'no-restricted-syntax': 'off',
      'no-use-before-define': 'off',
      'max-len': 'off',
      'comma-dangle': 'off',
      'no-multiple-empty-lines': ['error', { max: 1, maxEOF: 0 }],
      'quotes': ['error', 'single', { avoidEscape: true, allowTemplateLiterals: true }],
      'indent': ['error', 2, { SwitchCase: 1 }],
      'linebreak-style': ['error', 'unix'],
      'no-trailing-spaces': 'error',
      'eol-last': ['error', 'always'],
      'object-curly-newline': 'off',
      'arrow-body-style': 'off',
      'consistent-return': 'off',
      'generator-star-spacing': 'off',
      'global-require': 'warn',
      'no-bitwise': 'off',
      'no-cond-assign': 'off',
      'no-else-return': 'off',
      'no-nested-ternary': 'off',
      'require-yield': 'warn',
      'class-methods-use-this': 'off',
      'no-confusing-arrow': 'off',
      'no-unused-expressions': 'off',

      // React rules
      'react/jsx-filename-extension': [
        'warn',
        {
          extensions: ['.js', '.jsx', '.ts', '.tsx'],
        },
      ],
      'react/jsx-no-bind': 'off',
      'react/prop-types': 'off',
      'react/no-array-index-key': 'off',
      'react/forbid-prop-types': 'off',
      'react/prefer-stateless-function': 'off',
      'react/sort-comp': 'off',
      'react/no-did-mount-set-state': 'off',
      'react-hooks/rules-of-hooks': 'error',
      'react-hooks/exhaustive-deps': 'warn',

      // Import rules
      'import/extensions': 'off',
      'import/no-unresolved': 'off',
      'import/no-extraneous-dependencies': 'off',
      'import/prefer-default-export': 'off',

      // JSX A11y rules
      'jsx-a11y/no-noninteractive-element-interactions': 'off',
      'jsx-a11y/no-static-element-interactions': 'off',
    },
  },
  {
    files: ['**/*.ts', '**/*.tsx'],
    languageOptions: {
      parserOptions: {
        projectService: true,
        tsconfigRootDir: import.meta.dirname,
      },
    },
  },
  {
    ignores: ['dist/**', 'node_modules/**', 'coverage/**', 'build/**', 'doc/**'],
  },
]);


================================================
FILE: package.json
================================================
{
  "name": "hotkeys-js",
  "description": "A simple micro-library for defining and dispatching keyboard shortcuts. It has no dependencies.",
  "version": "4.0.2",
  "type": "module",
  "main": "dist/hotkeys-js.umd.cjs",
  "module": "dist/hotkeys-js.js",
  "browser": "dist/hotkeys-js.min.js",
  "types": "dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/hotkeys-js.js",
      "require": "./dist/hotkeys-js.umd.cjs",
      "browser": "./dist/hotkeys-js.min.js",
      "default": "./dist/hotkeys-js.js"
    },
    "./package.json": "./package.json"
  },
  "scripts": {
    "prepare": "npm run build:lib && husky install",
    "lint": "eslint src website",
    "type-check": "tsc --noEmit",
    "watch": "vite build --watch",
    "build:lib": "vite build",
    "build": "npm run type-check && npm run build:lib && npm run doc && npm run lint",
    "pretest": "npm run build",
    "test": "jest --coverage --detectOpenHandles",
    "test:watch": "jest --watch",
    "doc": "vite build --config website/vite.config.ts",
    "start": "vite --config website/vite.config.ts"
  },
  "files": [
    "dist",
    "doc"
  ],
  "keywords": [
    "hotkey",
    "hotkeys",
    "hotkeys-js",
    "hotkeysjs",
    "key",
    "keys",
    "keyboard",
    "shortcuts",
    "keypress"
  ],
  "author": "kenny wong <wowohoo@qq.com>",
  "license": "MIT",
  "homepage": "https://jaywcjlove.github.io/hotkeys-js",
  "funding": "https://jaywcjlove.github.io/#/sponsor",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/jaywcjlove/hotkeys-js.git"
  },
  "devDependencies": {
    "@eslint/js": "^9.39.1",
    "@rollup/plugin-terser": "^0.4.4",
    "@types/node": "^24.10.1",
    "@types/react": "^19.2.4",
    "@types/react-dom": "^19.2.3",
    "@uiw/react-github-corners": "^1.5.15",
    "@uiw/react-mac-keyboard": "^1.1.5",
    "@uiw/react-markdown-preview": "^5.0.3",
    "@uiw/react-shields": "^2.0.1",
    "@vitejs/plugin-react": "^5.1.1",
    "@wcj/dark-mode": "~1.1.0",
    "eslint": "^9.39.1",
    "eslint-plugin-import": "^2.32.0",
    "eslint-plugin-jsx-a11y": "^6.10.2",
    "eslint-plugin-react": "^7.37.5",
    "eslint-plugin-react-hooks": "^5.1.0",
    "globals": "^16.5.0",
    "husky": "^8.0.3",
    "jest": "^29.7.0",
    "jest-environment-jsdom": "^29.7.0",
    "lint-staged": "^15.2.0",
    "puppeteer": "~13.5.2",
    "react": "^19.2.0",
    "react-dom": "^19.2.0",
    "typescript": "^5.8.2",
    "typescript-eslint": "^8.46.4",
    "vite": "^5.1.0",
    "vite-plugin-dts": "^4.5.4"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "lint-staged": {
    "src/**/*.{js,ts}": "eslint",
    "website/**/*.{js,jsx,ts,tsx}": "eslint"
  },
  "overrides": {
    "react": "^19.2.0",
    "react-dom": "^19.2.0"
  }
}


================================================
FILE: src/index.ts
================================================
import {
  KeyCodeInfo,
  HotkeysEvent,
  UnbindInfo,
  HotkeysOptions,
  KeyHandler,
  HotkeysInterface,
  SetScope,
  GetScope,
  GetPressedKeyCodes,
  GetPressedKeyString,
  GetAllKeyCodes,
  Filter,
  IsPressed,
  DeleteScope,
  Unbind,
  HotkeysAPI,
} from './types';
import { addEvent, removeEvent, getMods, getKeys, compareArray, getLayoutIndependentKeyCode } from './utils';
import { _keyMap, _modifier, modifierMap, _mods, _handlers } from './var';

/** Record the pressed keys */
let _downKeys: number[] = [];
/** Whether the window has already listened to the focus event */
let winListendFocus: { listener: EventListener; capture: boolean } | null =
  null;
/** Whether we already listen to fullscreen change (to clear stuck _downKeys) */
let winListendFullscreen: { fullscreen: EventListener; webkit: EventListener } | null = null;
/** Default hotkey scope */
let _scope: string = 'all';
/** Map to record elements with bound events */
const elementEventMap = new Map<
  HTMLElement | Document,
  {
    keydownListener: EventListener;
    keyupListenr: EventListener;
    capture: boolean;
  }
>();

/** Return key code */
const code = (x: string): number =>
  _keyMap[x.toLowerCase()] ||
  _modifier[x.toLowerCase()] ||
  x.toUpperCase().charCodeAt(0);

const getKey = (x: number): string | undefined =>
  Object.keys(_keyMap).find((k) => _keyMap[k] === x);
const getModifier = (x: number): string | undefined =>
  Object.keys(_modifier).find((k) => _modifier[k] === x);

/** Set or get the current scope (defaults to 'all') */
const setScope: SetScope = (scope) => {
  _scope = scope || 'all';
};
/** Get the current scope */
const getScope: GetScope = () => {
  return _scope || 'all';
};
/** Get the key codes of the currently pressed keys */
const getPressedKeyCodes: GetPressedKeyCodes = () => {
  return _downKeys.slice(0);
};

const getPressedKeyString: GetPressedKeyString = () => {
  return _downKeys.map(
    (c) => getKey(c) || getModifier(c) || String.fromCharCode(c)
  );
};

const getAllKeyCodes: GetAllKeyCodes = () => {
  const result: KeyCodeInfo[] = [];
  Object.keys(_handlers).forEach((k) => {
    _handlers[k].forEach(({ key, scope, mods, shortcut }) => {
      result.push({
        scope,
        shortcut,
        mods,
        keys: key.split('+').map((v) => code(v)),
      });
    });
  });
  return result;
};

/** hotkey is effective only when filter return true */
const filter: Filter = (event) => {
  const target = (event.target || event.srcElement) as HTMLElement;
  const { tagName } = target;
  let flag = true;
  const isInput =
    tagName === 'INPUT' &&
    ![
      'checkbox',
      'radio',
      'range',
      'button',
      'file',
      'reset',
      'submit',
      'color',
    ].includes((target as HTMLInputElement).type);
  // ignore: isContentEditable === 'true', <input> and <textarea> when readOnly state is false, <select>
  if (
    target.isContentEditable ||
    ((isInput || tagName === 'TEXTAREA' || tagName === 'SELECT') &&
      !(target as HTMLInputElement | HTMLTextAreaElement).readOnly)
  ) {
    flag = false;
  }
  return flag;
};

/** Determine whether the pressed key matches a specific key, returns true or false */
const isPressed: IsPressed = (keyCode) => {
  if (typeof keyCode === 'string') {
    keyCode = code(keyCode); // Convert to key code
  }
  return _downKeys.indexOf(keyCode) !== -1;
};

/** Loop through and delete all handlers with the specified scope */
const deleteScope: DeleteScope = (scope, newScope) => {
  let handlers: HotkeysEvent[];
  let i: number;

  // If no scope is specified, get the current scope
  if (!scope) scope = getScope();

  for (const key in _handlers) {
    if (Object.prototype.hasOwnProperty.call(_handlers, key)) {
      handlers = _handlers[key];
      for (i = 0; i < handlers.length; ) {
        if (handlers[i].scope === scope) {
          const deleteItems = handlers.splice(i, 1);
          deleteItems.forEach(({ element }) => removeKeyEvent(element));
        } else {
          i++;
        }
      }
    }
  }

  // If the current scope has been deleted, reset the scope to 'all'
  if (getScope() === scope) setScope(newScope || 'all');
};

/** Clear modifier keys */
function clearModifier(event: KeyboardEvent): void {
  let key = getLayoutIndependentKeyCode(event);

  if (event.key && event.key.toLowerCase() === 'capslock') {
    // Ensure that when capturing keystrokes in modern browsers,
    // uppercase and lowercase letters (such as R and r) return the same key value.
    // https://github.com/jaywcjlove/hotkeys-js/pull/514
    // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key
    key = code(event.key);
  }
  const i = _downKeys.indexOf(key);

  // Remove the pressed key from the list
  if (i >= 0) {
    _downKeys.splice(i, 1);
  }
  // Special handling for the command key: fix the issue where keyup only triggers once for command combos
  if (event.key && event.key.toLowerCase() === 'meta') {
    _downKeys.splice(0, _downKeys.length);
  }

  // Clear modifier keys: shiftKey, altKey, ctrlKey, (command || metaKey)
  if (key === 93 || key === 224) key = 91;
  if (key in _mods) {
    _mods[key] = false;

    // Reset the modifier key status to false
    for (const k in _modifier)
      if (_modifier[k] === key) (hotkeys as any)[k] = false;
  }
}

const unbind: Unbind = (
  keysInfo?: string | UnbindInfo | UnbindInfo[],
  ...args: any[]
): void => {
  // unbind(), unbind all keys
  if (typeof keysInfo === 'undefined') {
    Object.keys(_handlers).forEach((key) => {
      if (Array.isArray(_handlers[key])) {
        _handlers[key].forEach((info) => eachUnbind(info));
      }
      delete _handlers[key];
    });
    removeKeyEvent(null);
  } else if (Array.isArray(keysInfo)) {
    // support like : unbind([{key: 'ctrl+a', scope: 's1'}, {key: 'ctrl-a', scope: 's2', splitKey: '-'}])
    keysInfo.forEach((info) => {
      if (info.key) eachUnbind(info);
    });
  } else if (typeof keysInfo === 'object') {
    // support like unbind({key: 'ctrl+a, ctrl+b', scope:'abc'})
    if (keysInfo.key) eachUnbind(keysInfo);
  } else if (typeof keysInfo === 'string') {
    // support old method
    let [scope, method] = args;
    if (typeof scope === 'function') {
      method = scope;
      scope = '';
    }
    eachUnbind({
      key: keysInfo,
      scope,
      method,
      splitKey: '+',
    });
  }
};

/** Unbind hotkeys for a specific scope */
const eachUnbind = ({
  key,
  scope,
  method,
  splitKey = '+',
}: UnbindInfo): void => {
  const multipleKeys = getKeys(key);
  multipleKeys.forEach((originKey) => {
    const unbindKeys = originKey.split(splitKey);
    const len = unbindKeys.length;
    const lastKey = unbindKeys[len - 1];
    const keyCode = lastKey === '*' ? '*' : code(lastKey);
    if (!_handlers[keyCode]) return;
    // If scope is not provided, get the current scope
    if (!scope) scope = getScope();
    const mods = len > 1 ? getMods(_modifier, unbindKeys) : [];
    const unbindElements: (HTMLElement | Document)[] = [];
    _handlers[keyCode] = _handlers[keyCode].filter((record) => {
      // Check if the method matches; if method is provided, must be equal to unbind
      const isMatchingMethod = method ? record.method === method : true;
      const isUnbind =
        isMatchingMethod &&
        record.scope === scope &&
        compareArray(record.mods, mods);
      if (isUnbind) unbindElements.push(record.element);
      return !isUnbind;
    });
    unbindElements.forEach((element) => removeKeyEvent(element));
  });
};

/** Handle the callback function for the corresponding hotkey */
function eventHandler(
  event: KeyboardEvent,
  handler: HotkeysEvent,
  scope: string,
  element: HTMLElement | Document
): void {
  if (handler.element !== element) {
    return;
  }
  let modifiersMatch: boolean;

  // Check if it is within the current scope
  if (handler.scope === scope || handler.scope === 'all') {
    // Check whether modifier keys match (returns true if they do)
    modifiersMatch = handler.mods.length > 0;

    for (const y in _mods) {
      if (Object.prototype.hasOwnProperty.call(_mods, y)) {
        if (
          (!_mods[y] && handler.mods.indexOf(+y) > -1) ||
          (_mods[y] && handler.mods.indexOf(+y) === -1)
        ) {
          modifiersMatch = false;
        }
      }
    }

    // Call the handler function; ignore if it's only a modifier key
    if (
      (handler.mods.length === 0 &&
        !_mods[16] &&
        !_mods[18] &&
        !_mods[17] &&
        !_mods[91]) ||
      modifiersMatch ||
      handler.shortcut === '*'
    ) {
      handler.keys = [];
      handler.keys = handler.keys.concat(_downKeys);
      if (handler.method(event, handler) === false) {
        if (event.preventDefault) event.preventDefault();
        else event.returnValue = false;
        if (event.stopPropagation) event.stopPropagation();
        if (event.cancelBubble) event.cancelBubble = true;
      }
    }
  }
}

/** Handle the keydown event */
function dispatch(
  this: any,
  event: KeyboardEvent,
  element: HTMLElement | Document
): void {
  const asterisk = _handlers['*'];
  let key = getLayoutIndependentKeyCode(event);

  // Ensure that when capturing keystrokes in modern browsers,
  // uppercase and lowercase letters (such as R and r) return the same key value.
  // https://github.com/jaywcjlove/hotkeys-js/pull/514
  // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key
  // CapsLock key
  // There's an issue where `keydown` and `keyup` events are not triggered after CapsLock is enabled to activate uppercase.
  if (event.key && event.key.toLowerCase() === 'capslock') {
    return;
  }
  // Form control filter: by default, shortcut keys are not triggered in form elements
  const filterFn = (hotkeys as any).filter || filter;
  if (!filterFn.call(this, event)) return;

  // In Gecko (Firefox), the command key code is 224; unify it with WebKit (Chrome)
  // In WebKit, left and right command keys have different codes
  if (key === 93 || key === 224) key = 91;

  /**
   * Collect bound keys
   * If an Input Method Editor is processing key input and the event is keydown, return 229.
   * https://stackoverflow.com/questions/25043934/is-it-ok-to-ignore-keydown-events-with-keycode-229
   * http://lists.w3.org/Archives/Public/www-dom/2010JulSep/att-0182/keyCode-spec.html
   */
  if (_downKeys.indexOf(key) === -1 && key !== 229) _downKeys.push(key);
  /**
   * Jest test cases are required.
   * ===============================
   */
  ['metaKey', 'ctrlKey', 'altKey', 'shiftKey'].forEach((keyName) => {
    const keyNum = (modifierMap as any)[keyName] as number;
    if ((event as any)[keyName] && _downKeys.indexOf(keyNum) === -1) {
      _downKeys.push(keyNum);
    } else if (!(event as any)[keyName] && _downKeys.indexOf(keyNum) > -1) {
      _downKeys.splice(_downKeys.indexOf(keyNum), 1);
    } else if (keyName === 'metaKey' && (event as any)[keyName]) {
      // If the command key is pressed, clear all non-modifier keys except the current event key.
      // This is because keyup for non-modifier keys will NEVER be triggered when command is pressed.
      // This is a known browser limitation.
      _downKeys = _downKeys.filter((k) => k in modifierMap || k === key);
    }
  });
  /**
   * -------------------------------
   */
  if (key in _mods) {
    _mods[key] = true;
    // Register special modifier keys to the `hotkeys` object
    for (const k in _modifier) {
      if (Object.prototype.hasOwnProperty.call(_modifier, k)) {
        const eventKey = (modifierMap as any)[_modifier[k]] as string;
        (hotkeys as any)[k] = (event as any)[eventKey];
      }
    }

    if (!asterisk) return;
  }

  // Bind the modifier keys in modifierMap to the event
  for (const e in _mods) {
    if (Object.prototype.hasOwnProperty.call(_mods, e)) {
      _mods[e] = (event as any)[(modifierMap as any)[e]];
    }
  }
  /**
   * https://github.com/jaywcjlove/hotkeys/pull/129
   * This solves the issue in Firefox on Windows where hotkeys corresponding to special characters would not trigger.
   * An example of this is ctrl+alt+m on a Swedish keyboard which is used to type μ.
   * Browser support: https://caniuse.com/#feat=keyboardevent-getmodifierstate
   */
  if (
    event.getModifierState &&
    !(event.altKey && !event.ctrlKey) &&
    event.getModifierState('AltGraph')
  ) {
    if (_downKeys.indexOf(17) === -1) {
      _downKeys.push(17);
    }

    if (_downKeys.indexOf(18) === -1) {
      _downKeys.push(18);
    }

    _mods[17] = true;
    _mods[18] = true;
  }

  // Get the current scope (defaults to 'all')
  const scope = getScope();
  // Handle any hotkeys registered as '*'
  if (asterisk) {
    for (let i = 0; i < asterisk.length; i++) {
      if (
        asterisk[i].scope === scope &&
        ((event.type === 'keydown' && asterisk[i].keydown) ||
          (event.type === 'keyup' && asterisk[i].keyup))
      ) {
        eventHandler(event, asterisk[i], scope, element);
      }
    }
  }
  // If the key is not registered, return
  if (!(key in _handlers)) return;

  const handlerKey = _handlers[key];
  const keyLen = handlerKey.length;
  for (let i = 0; i < keyLen; i++) {
    if (
      (event.type === 'keydown' && handlerKey[i].keydown) ||
      (event.type === 'keyup' && handlerKey[i].keyup)
    ) {
      if (handlerKey[i].key) {
        const record = handlerKey[i];
        const { splitKey } = record;
        const keyShortcut = record.key.split(splitKey);
        const _downKeysCurrent: number[] = []; // Store the current key codes
        for (let a = 0; a < keyShortcut.length; a++) {
          _downKeysCurrent.push(code(keyShortcut[a]));
        }
        if (_downKeysCurrent.sort().join('') === _downKeys.sort().join('')) {
          // Match found, call the handler
          eventHandler(event, record, scope, element);
        }
      }
    }
  }
}

const hotkeys = function hotkeys(
  key: string,
  option?: string | HotkeysOptions | KeyHandler,
  method?: KeyHandler
): void {
  _downKeys = [];
  /** List of hotkeys to handle */
  const keys = getKeys(key);
  let mods: number[] = [];
  /** Default scope is 'all', meaning effective in all scopes */
  let scope: string = 'all';
  /** Element to which the hotkey events are bound */
  let element: HTMLElement | Document = document;
  let i = 0;
  let keyup = false;
  let keydown = true;
  let splitKey = '+';
  let capture = false;
  /** Allow only a single callback */
  let single = false;

  // Determine if the second argument is a function (no options provided)
  if (method === undefined && typeof option === 'function') {
    method = option;
  }

  // Parse options object
  if (Object.prototype.toString.call(option) === '[object Object]') {
    const opts = option as HotkeysOptions;
    if (opts.scope) scope = opts.scope; // Set scope
    if (opts.element) element = opts.element; // Set binding element
    if (opts.keyup) keyup = opts.keyup;
    if (opts.keydown !== undefined) keydown = opts.keydown;
    if (opts.capture !== undefined) capture = opts.capture;
    if (typeof opts.splitKey === 'string') splitKey = opts.splitKey;
    if (opts.single === true) single = true;
  }

  if (typeof option === 'string') scope = option;

  // If only one callback is allowed, unbind the existing one first
  if (single) unbind(key, scope);

  // Handle each hotkey
  for (; i < keys.length; i++) {
    const currentKey = keys[i].split(splitKey); // Split into individual keys
    mods = [];

    // If it's a combination, extract modifier keys
    if (currentKey.length > 1) mods = getMods(_modifier, currentKey);

    // Convert non-modifier key to key code
    let finalKey: string | number = currentKey[currentKey.length - 1];
    finalKey = finalKey === '*' ? '*' : code(finalKey); // '*' means match all hotkeys

    // Initialize handler array if this key has no handlers yet
    if (!(finalKey in _handlers)) _handlers[finalKey] = [];

    _handlers[finalKey].push({
      keyup,
      keydown,
      scope,
      mods,
      shortcut: keys[i],
      method: method!,
      key: keys[i],
      splitKey,
      element,
    });
  }
  // Register hotkey event listeners on the global document
  if (typeof element !== 'undefined' && typeof window !== 'undefined') {
    if (!elementEventMap.has(element)) {
      // @ts-expect-error - window.event is deprecated IE property
      const keydownListener = (event: Event = window.event) =>
        dispatch(event as KeyboardEvent, element);
      // @ts-expect-error - window.event is deprecated IE property
      const keyupListenr = (event: Event = window.event) => {
        dispatch(event as KeyboardEvent, element);
        clearModifier(event as KeyboardEvent);
      };
      elementEventMap.set(element, { keydownListener, keyupListenr, capture });
      addEvent(element, 'keydown', keydownListener, capture);
      addEvent(element, 'keyup', keyupListenr, capture);
    }
    // Register focus event listener once to clear pressed keys on window focus
    if (!winListendFocus) {
      const listener = () => {
        _downKeys = [];
      };
      winListendFocus = { listener, capture };
      addEvent(window, 'focus', listener, capture);
    }
    // Register fullscreen change once: keyup can be lost when entering fullscreen (e.g. Alt+F), so clear stuck keys
    if (!winListendFullscreen && typeof document !== 'undefined') {
      const onFullscreenChange = () => {
        _downKeys = [];
        for (const k in _mods) _mods[k] = false;
        for (const k in _modifier) (hotkeys as any)[k] = false;
      };
      const fullscreenListener = onFullscreenChange as EventListener;
      const webkitListener = onFullscreenChange as EventListener;
      document.addEventListener('fullscreenchange', fullscreenListener);
      document.addEventListener('webkitfullscreenchange', webkitListener);
      winListendFullscreen = { fullscreen: fullscreenListener, webkit: webkitListener };
    }
  }
} as HotkeysInterface;

function trigger(shortcut: string, scope: string = 'all'): void {
  Object.keys(_handlers).forEach((key) => {
    const dataList = _handlers[key].filter(
      (item) => item.scope === scope && item.shortcut === shortcut
    );
    dataList.forEach((data) => {
      if (data && data.method) {
        data.method({} as KeyboardEvent, data);
      }
    });
  });
}

/** Clean up event listeners. After unbinding, check whether the element still has any hotkeys bound. If not, remove its event listeners. */
function removeKeyEvent(element: HTMLElement | Document | null): void {
  const values = Object.values(_handlers).flat();
  const findindex = values.findIndex(({ element: el }) => el === element);

  if (findindex < 0 && element) {
    const { keydownListener, keyupListenr, capture } =
      elementEventMap.get(element) || {};
    if (keydownListener && keyupListenr) {
      removeEvent(element, 'keyup', keyupListenr, capture);
      removeEvent(element, 'keydown', keydownListener, capture);
      elementEventMap.delete(element);
    }
  }

  if (values.length <= 0 || elementEventMap.size <= 0) {
    // Remove all event listeners from all elements
    const eventKeys = Array.from(elementEventMap.keys());
    eventKeys.forEach((el) => {
      const { keydownListener, keyupListenr, capture } =
        elementEventMap.get(el) || {};
      if (keydownListener && keyupListenr) {
        removeEvent(el, 'keyup', keyupListenr, capture);
        removeEvent(el, 'keydown', keydownListener, capture);
        elementEventMap.delete(el);
      }
    });
    // Clear the elementEventMap
    elementEventMap.clear();
    // Clear all handlers
    Object.keys(_handlers).forEach((key) => delete _handlers[key]);
    // Remove the global window focus event listener
    if (winListendFocus) {
      const { listener, capture } = winListendFocus;
      removeEvent(window, 'focus', listener, capture);
      winListendFocus = null;
    }
    // Remove fullscreen change listeners
    if (winListendFullscreen && typeof document !== 'undefined') {
      document.removeEventListener('fullscreenchange', winListendFullscreen.fullscreen);
      document.removeEventListener('webkitfullscreenchange', winListendFullscreen.webkit);
      winListendFullscreen = null;
    }
  }
}

const _api: Omit<HotkeysAPI, 'noConflict'> = {
  getPressedKeyString,
  setScope,
  getScope,
  deleteScope,
  getPressedKeyCodes,
  getAllKeyCodes,
  isPressed,
  filter,
  trigger,
  unbind,
  keyMap: _keyMap,
  modifier: _modifier,
  modifierMap,
};

for (const a in _api) {
  const key = a as keyof typeof _api;
  if (Object.prototype.hasOwnProperty.call(_api, key)) {
    (hotkeys as any)[key] = _api[key];
  }
}

if (typeof window !== 'undefined') {
  const _hotkeys = (window as any).hotkeys;
  (hotkeys as HotkeysInterface).noConflict = (deep?: boolean) => {
    if (deep && (window as any).hotkeys === hotkeys) {
      (window as any).hotkeys = _hotkeys;
    }
    return hotkeys as HotkeysInterface;
  };
  (window as any).hotkeys = hotkeys;
}

// Export for both ESM and CommonJS
export default hotkeys as HotkeysInterface;
export type { KeyHandler, HotkeysEvent };

// Additional CommonJS compatibility
// This will be handled by the build process for UMD builds
if (typeof module !== 'undefined' && module.exports) {
  module.exports = hotkeys;
  module.exports.default = hotkeys;
}


================================================
FILE: src/types.ts
================================================
export interface KeyCodeInfo {
  scope: string;
  shortcut: string;
  mods: number[];
  keys: number[];
}

export interface UnbindInfo {
  key: string;
  scope?: string;
  method?: KeyHandler;
  splitKey?: string;
}

export interface HotkeysEvent {
  keyup: boolean;
  keydown: boolean;
  scope: string;
  mods: number[];
  shortcut: string;
  method: KeyHandler;
  key: string;
  splitKey: string;
  element: HTMLElement | Document;
  keys?: number[];
}

export interface KeyHandler {
  (keyboardEvent: KeyboardEvent, hotkeysEvent: HotkeysEvent): void | boolean;
}

export type SetScope = (scope: string) => void;

export type GetScope = () => string;

export type DeleteScope = (scope?: string, newScope?: string) => void;

export type GetPressedKeyCodes = () => number[];

export type GetPressedKeyString = () => string[];

export type GetAllKeyCodes = () => KeyCodeInfo[];

export interface IsPressed {
  /** For example, `hotkeys.isPressed(77)` is true if the `M` key is currently pressed. */
  (keyCode: number): boolean;
  /** For example, `hotkeys.isPressed('m')` is true if the `M` key is currently pressed. */
  (keyCode: string): boolean;
}

export type Filter = (event: KeyboardEvent) => boolean;

export type Trigger = (shortcut: string, scope?: string) => void;

export interface Unbind {
  (key?: string): void;
  (keysInfo: UnbindInfo): void;
  (keysInfo: UnbindInfo[]): void;
  (key: string, scopeName: string): void;
  (key: string, scopeName: string, method: KeyHandler): void;
  (key: string, method: KeyHandler): void;
}

export type NoConflict = (deep?: boolean) => HotkeysInterface;

export interface HotkeysOptions {
  scope?: string;
  element?: HTMLElement | Document;
  keyup?: boolean;
  keydown?: boolean;
  capture?: boolean;
  splitKey?: string;
  single?: boolean;
}

export interface HotkeysAPI {
  /**
   * Use the `hotkeys.setScope` method to set scope. There can only be one active scope besides 'all'.  By default 'all' is always active.
   *
   * ```js
   * // Define shortcuts with a scope
   * hotkeys('ctrl+o, ctrl+alt+enter', 'issues', function() {
   *   console.log('do something');
   * });
   * hotkeys('o, enter', 'files', function() {
   *   console.log('do something else');
   * });
   *
   * // Set the scope (only 'all' and 'issues' shortcuts will be honored)
   * hotkeys.setScope('issues'); // default scope is 'all'
   * ```
   */
  setScope: SetScope;
  /**
   * Use the `hotkeys.getScope` method to get scope.
   *
   * ```js
   * hotkeys.getScope();
   * ```
   */
  getScope: GetScope;
  /**
   * Use the `hotkeys.deleteScope` method to delete a scope. This will also remove all associated hotkeys with it.
   *
   * ```js
   * hotkeys.deleteScope('issues');
   * ```
   * You can use second argument, if need set new scope after deleting.
   *
   * ```js
   * hotkeys.deleteScope('issues', 'newScopeName');
   * ```
   */
  deleteScope: DeleteScope;
  /**
   * Returns an array of key codes currently pressed.
   *
   * ```js
   * hotkeys('command+ctrl+shift+a,f', function() {
   *   console.log(hotkeys.getPressedKeyCodes()); //=> [17, 65] or [70]
   * })
   * ```
   */
  getPressedKeyCodes: GetPressedKeyCodes;
  /**
   * Returns an array of key codes currently pressed.
   *
   * ```js
   * hotkeys('command+ctrl+shift+a,f', function() {
   *   console.log(hotkeys.getPressedKeyString()); //=> ['⌘', '⌃', '⇧', 'A', 'F']
   * })
   * ```
   */
  getPressedKeyString: GetPressedKeyString;
  /**
   * Get a list of all registration codes.
   *
   * ```js
   * hotkeys('command+ctrl+shift+a,f', function() {
   *   console.log(hotkeys.getAllKeyCodes());
   *   // [
   *   //   { scope: 'all', shortcut: 'command+ctrl+shift+a', mods: [91, 17, 16], keys: [91, 17, 16, 65] },
   *   //   { scope: 'all', shortcut: 'f', mods: [], keys: [42] }
   *   // ]
   * })
   * ```
   *
   */
  getAllKeyCodes: GetAllKeyCodes;
  isPressed: IsPressed;
  /**
   * By default hotkeys are not enabled for `INPUT` `SELECT` `TEXTAREA` elements.
   * `Hotkeys.filter` to return to the `true` shortcut keys set to play a role,
   * `false` shortcut keys set up failure.
   *
   * ```js
   * hotkeys.filter = function(event){
   *   return true;
   * }
   * //How to add the filter to edit labels. <div contentEditable="true"></div>
   * //"contentEditable" Older browsers that do not support drops
   * hotkeys.filter = function(event) {
   *   var target = event.target || event.srcElement;
   *   var tagName = target.tagName;
   *   return !(target.isContentEditable || tagName == 'INPUT' || tagName == 'SELECT' || tagName == 'TEXTAREA');
   * }
   *
   * hotkeys.filter = function(event){
   *   var tagName = (event.target || event.srcElement).tagName;
   *   hotkeys.setScope(/^(INPUT|TEXTAREA|SELECT)$/.test(tagName) ? 'input' : 'other');
   *   return true;
   * }
   * ```
   */
  filter: Filter;
  /**
   * trigger shortcut key event
   *
   * ```js
   * hotkeys.trigger('ctrl+o');
   * hotkeys.trigger('ctrl+o', 'scope2');
   * ```
   */
  trigger: Trigger;
  /**
   * Unbinds a shortcut key event.
   *
   * ```js
   * hotkeys.unbind('ctrl+o');
   * hotkeys.unbind('ctrl+o', 'scope1');
   * hotkeys.unbind('ctrl+o', 'scope1', method);
   * hotkeys.unbind('ctrl+o', method);
   * ```
   */
  unbind: Unbind;
  /**
   * Relinquish HotKeys’s control of the `hotkeys` variable.
   *
   * ```js
   * var k = hotkeys.noConflict();
   * k('a', function() {
   *   console.log("do something")
   * });
   *
   * hotkeys()
   * // -->Uncaught TypeError: hotkeys is not a function(anonymous function)
   * // @ VM2170:2InjectedScript._evaluateOn
   * // @ VM2165:883InjectedScript._evaluateAndWrap
   * // @ VM2165:816InjectedScript.evaluate @ VM2165:682
   * ```
   */
  noConflict: NoConflict;

  keyMap: Record<string, number>;
  modifier: Record<string, number>;
  modifierMap: Record<string | number, number | string>;
}

export interface HotkeysInterface extends HotkeysAPI {
  (key: string, method: KeyHandler): void;
  (key: string, scope: string, method: KeyHandler): void;
  (key: string, option: HotkeysOptions, method: KeyHandler): void;

  shift?: boolean;
  ctrl?: boolean;
  alt?: boolean;
  option?: boolean;
  control?: boolean;
  cmd?: boolean;
  command?: boolean;
}


================================================
FILE: src/utils.ts
================================================
const isff: boolean =
  typeof navigator !== 'undefined'
    ? navigator.userAgent.toLowerCase().indexOf('firefox') > 0
    : false;

/** Bind event */
function addEvent(
  object: HTMLElement | Document | Window,
  event: string,
  method: EventListenerOrEventListenerObject,
  useCapture?: boolean
): void {
  if (object.addEventListener) {
    object.addEventListener(event, method, useCapture);
    // @ts-expect-error - attachEvent is only available on IE
  } else if (object.attachEvent) {
    // @ts-expect-error - attachEvent is only available on IE
    object.attachEvent(`on${event}`, method);
  }
}

function removeEvent(
  object: HTMLElement | Document | Window | null,
  event: string,
  method: EventListenerOrEventListenerObject,
  useCapture?: boolean
): void {
  if (!object) return;
  if (object.removeEventListener) {
    object.removeEventListener(event, method, useCapture);
    // @ts-expect-error - removeEvent is only available on IE
  } else if (object.detachEvent) {
    // @ts-expect-error - detachEvent is only available on IE
    object.detachEvent(`on${event}`, method);
  }
}

/** Convert modifier keys to their corresponding key codes */
function getMods(modifier: Record<string, number>, key: string[]): number[] {
  const modsKeys = key.slice(0, key.length - 1);
  const modsCodes: number[] = [];
  for (let i = 0; i < modsKeys.length; i++) {
    modsCodes.push(modifier[modsKeys[i].toLowerCase()]);
  }
  return modsCodes;
}

/** Process the input key string and convert it to an array */
function getKeys(key: string | undefined): string[] {
  if (typeof key !== 'string') key = '';
  key = key.replace(/\s/g, ''); // Match any whitespace character, including spaces, tabs, form feeds, etc.
  const keys = key.split(','); // Allow multiple shortcuts separated by ','
  let index = keys.lastIndexOf('');

  // Shortcut may include ',' — special handling needed
  for (; index >= 0; ) {
    keys[index - 1] += ',';
    keys.splice(index, 1);
    index = keys.lastIndexOf('');
  }

  return keys;
}

/** Compare arrays of modifier keys */
function compareArray(a1: number[], a2: number[]): boolean {
  const arr1 = a1.length >= a2.length ? a1 : a2;
  const arr2 = a1.length >= a2.length ? a2 : a1;
  let isIndex = true;

  for (let i = 0; i < arr1.length; i++) {
    if (arr2.indexOf(arr1[i]) === -1) isIndex = false;
  }
  return isIndex;
}

/**
 * Get layout-independent key code from keyboard event
 * This makes hotkeys work regardless of keyboard layout (Russian, German, etc.)
 * Uses event.code (physical key) instead of keyCode (character) for letter keys
 */
function getLayoutIndependentKeyCode(event: KeyboardEvent): number {
  let key = event.keyCode || event.which || event.charCode;

  // Convert physical key code (KeyA-KeyZ) to corresponding ASCII code (65-90)
  // This makes 'ctrl+a' work on any keyboard layout
  if (event.code && /^Key[A-Z]$/.test(event.code)) {
    key = event.code.charCodeAt(3); // "KeyA"[3] = "A" -> 65
  }

  return key;
}

export {
  isff,
  getMods,
  getKeys,
  addEvent,
  removeEvent,
  compareArray,
  getLayoutIndependentKeyCode,
};


================================================
FILE: src/var.ts
================================================
import { HotkeysEvent } from './types';
import { isff } from './utils';

// Special Keys
const _keyMap: Record<string, number> = {
  backspace: 8,
  '⌫': 8,
  tab: 9,
  clear: 12,
  enter: 13,
  '↩': 13,
  return: 13,
  esc: 27,
  escape: 27,
  space: 32,
  left: 37,
  up: 38,
  right: 39,
  down: 40,
  /// https://w3c.github.io/uievents/#events-keyboard-key-location
  arrowup: 38,
  arrowdown: 40,
  arrowleft: 37,
  arrowright: 39,
  del: 46,
  delete: 46,
  ins: 45,
  insert: 45,
  home: 36,
  end: 35,
  pageup: 33,
  pagedown: 34,
  capslock: 20,
  num_0: 96,
  num_1: 97,
  num_2: 98,
  num_3: 99,
  num_4: 100,
  num_5: 101,
  num_6: 102,
  num_7: 103,
  num_8: 104,
  num_9: 105,
  num_multiply: 106,
  num_add: 107,
  num_enter: 108,
  num_subtract: 109,
  num_decimal: 110,
  num_divide: 111,
  '⇪': 20,
  ',': 188,
  '.': 190,
  '/': 191,
  '`': 192,
  '-': isff ? 173 : 189,
  '=': isff ? 61 : 187,
  ';': isff ? 59 : 186,
  '\'': 222,
  '{': 219,
  '}': 221,
  '[': 219,
  ']': 221,
  '\\': 220,
};

// Modifier Keys
const _modifier: Record<string, number> = {
  // shiftKey
  '⇧': 16,
  shift: 16,
  // altKey
  '⌥': 18,
  alt: 18,
  option: 18,
  // ctrlKey
  '⌃': 17,
  ctrl: 17,
  control: 17,
  // metaKey
  '⌘': 91,
  cmd: 91,
  meta: 91,
  command: 91,
};

const modifierMap: Record<string | number, number | string> = {
  16: 'shiftKey',
  18: 'altKey',
  17: 'ctrlKey',
  91: 'metaKey',

  shiftKey: 16,
  ctrlKey: 17,
  altKey: 18,
  metaKey: 91,
};

const _mods: Record<number, boolean> = {
  16: false,
  18: false,
  17: false,
  91: false,
};

const _handlers: Record<string | number, HotkeysEvent[]> = {};

// F1~F12 special key
for (let k = 1; k < 20; k++) {
  _keyMap[`f${k}`] = 111 + k;
}

export { _keyMap, _modifier, modifierMap, _mods, _handlers };


================================================
FILE: test/index.html
================================================
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>hotkeys.js</title>
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
  <meta name="description" content="A robust Javascript library for capturing keyboard input and key combinations entered. It has no dependencies. Try to press your keyboard, The following button will highlight.">
  <script src="../dist/hotkeys-js.umd.cjs"></script>
</head>
<body>
  <div id="root">hotkeys</div>
</body>
</html>


================================================
FILE: test/run.test.js
================================================
/* eslint-disable @typescript-eslint/no-require-imports */
const puppeteer = require('puppeteer');
const path = require('path');

let browser;
let page;

beforeAll(async () => {
  browser = await puppeteer.launch({ args: ['--no-sandbox'] });
  page = await browser.newPage();

  // Enable coverage collection
  await page.coverage.startJSCoverage();

  // Load the test HTML file
  const htmlPath = path.resolve(__dirname, './index.html');
  await page.goto(`file://${htmlPath}`, { waitUntil: 'networkidle2' });
}, 1000 * 120);

describe('\n   Hotkeys.js Test Case\n', () => {
  test('HTML loader', async () => {
    const title = await page.title();
    expect(title).toBe('hotkeys.js');
  }, 10000);

  test('Test HTML load', async () => {
    const text = await page.$eval('#root', (el) => el.textContent);
    expect(text).toBe('hotkeys');

    const hasHotkeys = await page.evaluate(() => {
      return typeof window.hotkeys !== 'undefined';
    });
    expect(hasHotkeys).toBeTruthy();
  });

  test('HotKeys getPressedKeyCodes Test Case', async () => {
    const result = await page.evaluate(async () => {
      return new Promise((resolve) => {
        let isExecuteFunction = false;
        window.hotkeys('command+ctrl+shift+a', (e) => {
          isExecuteFunction = true;
          const pressedKeys = window.hotkeys.getPressedKeyCodes();
          resolve({
            isExecuteFunction,
            metaKey: e.metaKey,
            ctrlKey: e.ctrlKey,
            shiftKey: e.shiftKey,
            pressedKeys,
          });
        });

        // Trigger the event
        const event = new KeyboardEvent('keydown', {
          keyCode: 65,
          which: 65,
          metaKey: true,
          ctrlKey: true,
          shiftKey: true,
          bubbles: true,
          cancelable: true,
        });
        document.body.dispatchEvent(event);

        setTimeout(() => {
          window.hotkeys.unbind('command+ctrl+shift+a');
          resolve({ isExecuteFunction, pressedKeys: [] });
        }, 100);
      });
    });

    expect(result.isExecuteFunction).toBeTruthy();
    expect(result.metaKey).toBeTruthy();
    expect(result.ctrlKey).toBeTruthy();
    expect(result.shiftKey).toBeTruthy();
    expect(result.pressedKeys).toEqual(
      expect.arrayContaining([16, 17, 65, 91])
    );
  });

  test('HotKeys getPressedKeyString Test Case', async () => {
    const result = await page.evaluate(async () => {
      return new Promise((resolve) => {
        let isExecuteFunction = false;
        window.hotkeys('command+ctrl+shift+a', (e) => {
          isExecuteFunction = true;
          const pressedKeys = window.hotkeys.getPressedKeyString();
          resolve({
            isExecuteFunction,
            metaKey: e.metaKey,
            ctrlKey: e.ctrlKey,
            shiftKey: e.shiftKey,
            pressedKeys,
          });
        });

        const event = new KeyboardEvent('keydown', {
          keyCode: 65,
          which: 65,
          metaKey: true,
          ctrlKey: true,
          shiftKey: true,
          bubbles: true,
          cancelable: true,
        });
        document.body.dispatchEvent(event);

        setTimeout(() => {
          window.hotkeys.unbind('command+ctrl+shift+a');
          resolve({ isExecuteFunction: false, pressedKeys: [] });
        }, 100);
      });
    });

    expect(result.isExecuteFunction).toBeTruthy();
    expect(result.pressedKeys).toEqual(
      expect.arrayContaining(['⇧', '⌃', 'A', '⌘'])
    );
  });

  test('HotKeys unbind Test Case', async () => {
    const result = await page.evaluate(async () => {
      return new Promise((resolve) => {
        let isExecuteFunction = false;
        window.hotkeys('enter', () => {
          isExecuteFunction = true;
        });

        const event = new KeyboardEvent('keydown', {
          keyCode: 13,
          which: 13,
          bubbles: true,
          cancelable: true,
        });
        document.body.dispatchEvent(event);

        setTimeout(() => {
          const unbindResult = window.hotkeys.unbind('enter');
          resolve({
            isExecuteFunction,
            unbindResult,
            keyCode: 13,
          });
        }, 100);
      });
    });

    expect(result.isExecuteFunction).toBeTruthy();
    expect(result.unbindResult).toBeUndefined();
  });

  test('getAllKeyCodes Test Case', async () => {
    const result = await page.evaluate(() => {
      return window.hotkeys.getAllKeyCodes();
    });

    expect(Array.isArray(result)).toBeTruthy();
  });

  test('HotKeys Special keys Test Case', async () => {
    const result = await page.evaluate(async () => {
      const results = {};

      // Test enter
      await new Promise((resolve) => {
        window.hotkeys('enter', (e) => {
          results.enter = e.keyCode === 13;
        });

        const event = new KeyboardEvent('keydown', {
          keyCode: 13,
          which: 13,
          bubbles: true,
          cancelable: true,
        });
        document.body.dispatchEvent(event);

        setTimeout(() => {
          window.hotkeys.unbind('enter');
          resolve();
        }, 50);
      });

      // Test space
      await new Promise((resolve) => {
        window.hotkeys('space', (e) => {
          results.space = e.keyCode === 32;
        });

        const event = new KeyboardEvent('keydown', {
          keyCode: 32,
          which: 32,
          bubbles: true,
          cancelable: true,
        });
        document.body.dispatchEvent(event);

        setTimeout(() => {
          window.hotkeys.unbind('space');
          resolve();
        }, 50);
      });

      return results;
    });

    expect(result.enter).toBeTruthy();
    expect(result.space).toBeTruthy();
  });

  test('HotKeys Test Case', async () => {
    const result = await page.evaluate(async () => {
      const results = {};

      // Test 'w'
      await new Promise((resolve) => {
        window.hotkeys('w', (e) => {
          results.w = e.keyCode === 87;
        });

        const event = new KeyboardEvent('keydown', {
          keyCode: 87,
          which: 87,
          bubbles: true,
          cancelable: true,
        });
        document.body.dispatchEvent(event);

        setTimeout(() => {
          window.hotkeys.unbind('w');
          resolve();
        }, 50);
      });

      // Test 'a' with isPressed
      await new Promise((resolve) => {
        window.hotkeys('a', () => {
          results.isPressedA = window.hotkeys.isPressed('a');
          results.isPressedAUpper = window.hotkeys.isPressed('A');
          results.isPressed65 = window.hotkeys.isPressed(65);
        });

        const event = new KeyboardEvent('keydown', {
          keyCode: 65,
          which: 65,
          bubbles: true,
          cancelable: true,
        });
        document.body.dispatchEvent(event);

        setTimeout(() => {
          window.hotkeys.unbind('a');
          resolve();
        }, 50);
      });

      return results;
    });

    expect(result.w).toBeTruthy();
    expect(result.isPressedA).toBeTruthy();
    expect(result.isPressedAUpper).toBeTruthy();
    expect(result.isPressed65).toBeTruthy();
  });

  test('HotKeys Key combination Test Case', async () => {
    const result = await page.evaluate(async () => {
      const results = {};

      // Test ⌘+d
      await new Promise((resolve) => {
        window.hotkeys('⌘+d', (e) => {
          results.cmdD = e.keyCode === 68 && e.metaKey;
        });

        const event = new KeyboardEvent('keydown', {
          keyCode: 68,
          which: 68,
          metaKey: true,
          bubbles: true,
          cancelable: true,
        });
        document.body.dispatchEvent(event);

        setTimeout(() => {
          window.hotkeys.unbind('⌘+d');
          resolve();
        }, 50);
      });

      // Test shift+a
      await new Promise((resolve) => {
        window.hotkeys('shift+a', (e) => {
          results.shiftA = e.keyCode === 65 && e.shiftKey;
        });

        const event = new KeyboardEvent('keydown', {
          keyCode: 65,
          which: 65,
          shiftKey: true,
          bubbles: true,
          cancelable: true,
        });
        document.body.dispatchEvent(event);

        setTimeout(() => {
          window.hotkeys.unbind('shift+a');
          resolve();
        }, 50);
      });

      return results;
    });

    expect(result.cmdD).toBeTruthy();
    expect(result.shiftA).toBeTruthy();
  });

  test('Hotkey trigger with shortcut', async () => {
    const result = await page.evaluate(() => {
      let count = 0;
      window.hotkeys('a', () => {
        count++;
      });

      window.hotkeys.trigger('a');

      window.hotkeys.unbind('a');

      return count;
    });

    expect(result).toBe(1);
  });

  test('Hotkey trigger with multi shortcut', async () => {
    const result = await page.evaluate(() => {
      let count = 0;
      for (let i = 0; i < 3; i++) {
        window.hotkeys('a', () => {
          count++;
        });
      }

      window.hotkeys.trigger('a');

      window.hotkeys.unbind('a');

      return count;
    });

    expect(result).toBe(3);
  });

  test('Layout-independent hotkeys (Cyrillic/Russian keyboard)', async () => {
    const result = await page.evaluate(async () => {
      const results = {
        altMTriggered: false,
        altVTriggered: false,
        downKeysAfterAltM: [],
        downKeysAfterAltV: [],
        downKeysAfterRelease: [],
      };

      // Register hotkeys for alt+m and alt+v
      window.hotkeys('alt+m', () => {
        results.altMTriggered = true;
        results.downKeysAfterAltM = window.hotkeys.getPressedKeyCodes();
      });

      window.hotkeys('alt+v', () => {
        results.altVTriggered = true;
        results.downKeysAfterAltV = window.hotkeys.getPressedKeyCodes();
      });

      // Simulate Alt+M on Russian keyboard layout
      // On Russian layout, M key produces "Ь" character (code 1068 or ~126)
      // But the physical key is still KeyM
      await new Promise((resolve) => {
        // Press Alt
        const altDown = new KeyboardEvent('keydown', {
          keyCode: 18,
          which: 18,
          code: 'AltLeft',
          altKey: true,
          bubbles: true,
          cancelable: true,
        });
        document.body.dispatchEvent(altDown);

        setTimeout(() => {
          // Press M (with wrong keyCode but correct code)
          const mDown = new KeyboardEvent('keydown', {
            keyCode: 126, // Wrong keyCode (~ character on Cyrillic layout)
            which: 126,
            code: 'KeyM', // Correct physical key code
            altKey: true,
            bubbles: true,
            cancelable: true,
          });
          document.body.dispatchEvent(mDown);

          setTimeout(() => {
            // Release M
            const mUp = new KeyboardEvent('keyup', {
              keyCode: 126,
              which: 126,
              code: 'KeyM',
              altKey: true,
              bubbles: true,
              cancelable: true,
            });
            document.body.dispatchEvent(mUp);

            setTimeout(() => {
              // Release Alt
              const altUp = new KeyboardEvent('keyup', {
                keyCode: 18,
                which: 18,
                code: 'AltLeft',
                altKey: false,
                bubbles: true,
                cancelable: true,
              });
              document.body.dispatchEvent(altUp);

              resolve();
            }, 20);
          }, 20);
        }, 20);
      });

      // Small delay between combinations
      await new Promise((resolve) => setTimeout(resolve, 50));

      // Simulate Alt+V on Russian keyboard layout
      // On Russian layout, V key produces "М" character
      await new Promise((resolve) => {
        // Press Alt
        const altDown = new KeyboardEvent('keydown', {
          keyCode: 18,
          which: 18,
          code: 'AltLeft',
          altKey: true,
          bubbles: true,
          cancelable: true,
        });
        document.body.dispatchEvent(altDown);

        setTimeout(() => {
          // Press V (with wrong keyCode but correct code)
          const vDown = new KeyboardEvent('keydown', {
            keyCode: 1052, // Wrong keyCode (М character on Cyrillic layout)
            which: 1052,
            code: 'KeyV', // Correct physical key code
            altKey: true,
            bubbles: true,
            cancelable: true,
          });
          document.body.dispatchEvent(vDown);

          setTimeout(() => {
            // Release V
            const vUp = new KeyboardEvent('keyup', {
              keyCode: 1052,
              which: 1052,
              code: 'KeyV',
              altKey: true,
              bubbles: true,
              cancelable: true,
            });
            document.body.dispatchEvent(vUp);

            setTimeout(() => {
              // Release Alt
              const altUp = new KeyboardEvent('keyup', {
                keyCode: 18,
                which: 18,
                code: 'AltLeft',
                altKey: false,
                bubbles: true,
                cancelable: true,
              });
              document.body.dispatchEvent(altUp);

              // Check downKeys after all keys released
              setTimeout(() => {
                results.downKeysAfterRelease =
                  window.hotkeys.getPressedKeyCodes();
                resolve();
              }, 20);
            }, 20);
          }, 20);
        }, 20);
      });

      // Cleanup
      window.hotkeys.unbind('alt+m');
      window.hotkeys.unbind('alt+v');

      return results;
    });

    // Both hotkeys should be triggered
    expect(result.altMTriggered).toBeTruthy();
    expect(result.altVTriggered).toBeTruthy();

    // Alt (18) + M (77) should be in downKeys during Alt+M
    expect(result.downKeysAfterAltM).toEqual(expect.arrayContaining([18, 77]));

    // Alt (18) + V (86) should be in downKeys during Alt+V
    expect(result.downKeysAfterAltV).toEqual(expect.arrayContaining([18, 86]));

    // After all keys released, downKeys should be empty (this was the bug)
    expect(result.downKeysAfterRelease).toEqual([]);
  });

  afterAll(async () => {
    // Collect coverage data
    const jsCoverage = await page.coverage.stopJSCoverage();
    
    // Convert to Istanbul/Jest compatible coverage format
    global.__coverage__ = global.__coverage__ || {};
    
    for (const entry of jsCoverage) {
      // Only process hotkeys related files
      if (entry.url.includes('hotkeys') && !entry.url.includes('test')) {
        // Handle file path correctly, remove file:// prefix
        const filePath = entry.url.replace(/^file:\/\//, '');
        const sourceMap = createCoverageMap(entry, filePath);
        
        // Add to global coverage object
        global.__coverage__[filePath] = sourceMap;
      }
    }
    
    // Calculate total coverage for logging
    let totalBytes = 0;
    let usedBytes = 0;
    for (const entry of jsCoverage) {
      totalBytes += entry.text.length;
      for (const range of entry.ranges) {
        usedBytes += range.end - range.start;
      }
    }
    const coverage = (usedBytes / totalBytes) * 100;
    console.log(`JS Coverage: ${coverage.toFixed(2)}%`);
    
    // Save coverage metrics to file for CI/Actions usage
    const fs = require('fs');
    const coverageDir = path.join(process.cwd(), 'coverage');
    if (!fs.existsSync(coverageDir)) {
      fs.mkdirSync(coverageDir, { recursive: true });
    }

    // Save coverage as simple text for easy shell script access
    fs.writeFileSync(
      path.join(coverageDir, 'js-coverage.txt'), 
      `${coverage.toFixed(2)}`
    );
    
    console.log(`Coverage metrics saved to: ${path.join(coverageDir, 'js-coverage.txt')}`);
    
    await browser.close();
  });
});

jest.setTimeout(30000);


// Helper function to convert Puppeteer coverage data to Istanbul format
function createCoverageMap(entry, filePath) {
  const fs = require('fs');
  
  // Read source file content to create detailed line mapping
  let sourceText = '';
  try {
    sourceText = fs.readFileSync(filePath, 'utf8');
  } catch (error) {
    sourceText = entry.text; // Fallback to entry.text
  }
  
  const lines = sourceText.split('\n');
  const statements = {};
  const functions = {};
  const branches = {};
  const statementMap = {};
  const functionMap = {};
  const branchMap = {};
  
  let statementId = 1;
  
  // Create statement mapping for each line of code
  lines.forEach((line, lineIndex) => {
    const lineNumber = lineIndex + 1;
    const lineLength = line.length;
    
    if (line.trim().length > 0) { // Only process non-empty lines
      const id = String(statementId++);
      statementMap[id] = {
        start: { line: lineNumber, column: 0 },
        end: { line: lineNumber, column: lineLength }
      };
      
      // Check if this line is covered
      let covered = 0;
      let lineStartOffset = lines.slice(0, lineIndex).reduce((acc, l) => acc + l.length + 1, 0);
      let lineEndOffset = lineStartOffset + lineLength;
      
      for (const range of entry.ranges) {
        if (range.start <= lineEndOffset && range.end >= lineStartOffset) {
          covered = Math.max(covered, range.count || 1);
        }
      }
      
      statements[id] = covered;
    }
  });
  
  // If no statement mappings exist, create default ones
  if (Object.keys(statements).length === 0) {
    statementMap['1'] = {
      start: { line: 1, column: 0 },
      end: { line: 1, column: 100 }
    };
    statements['1'] = entry.ranges.length > 0 ? 1 : 0;
  }
  
  // Add some function mappings (based on simple function detection)
  lines.forEach((line, lineIndex) => {
    if (line.includes('function') || line.includes('=>') || line.includes('class')) {
      const lineNumber = lineIndex + 1;
      const functionId = String(Object.keys(functionMap).length + 1);
      
      functionMap[functionId] = {
        name: `function_${functionId}`,
        decl: { 
          start: { line: lineNumber, column: 0 },
          end: { line: lineNumber, column: line.length }
        },
        loc: { 
          start: { line: lineNumber, column: 0 },
          end: { line: lineNumber, column: line.length }
        },
        line: lineNumber
      };
      
      // Check if function is executed
      let functionCovered = 0;
      let lineStartOffset = lines.slice(0, lineIndex).reduce((acc, l) => acc + l.length + 1, 0);
      let lineEndOffset = lineStartOffset + line.length;
      
      for (const range of entry.ranges) {
        if (range.start <= lineEndOffset && range.end >= lineStartOffset && range.count > 0) {
          functionCovered = range.count;
          break;
        }
      }
      
      functions[functionId] = functionCovered;
    }
  });
  
  return {
    path: filePath,
    statementMap,
    fnMap: functionMap,
    branchMap,
    s: statements,
    f: functions,
    b: branches,
    _coverageSchema: "1a1c01bbd47fc00a2c39e90264f33305004495a9",
    hash: "coverage-" + Date.now()
  };
}

================================================
FILE: tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "ES2015",
    "module": "ESNext",
    "lib": ["ES2015", "DOM"],
    "declaration": false,
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "allowSyntheticDefaultImports": true,
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitThis": true,
    "alwaysStrict": true
  },
  "include": ["src/**/*.ts"],
  "exclude": ["node_modules", "test", "website"]
}


================================================
FILE: vite.config.ts
================================================
import { defineConfig } from "vite";
import { resolve } from "path";
import dts from "vite-plugin-dts";
import { readFileSync } from "fs";
import terser from "@rollup/plugin-terser";

const pkg = JSON.parse(readFileSync("./package.json", "utf-8"));

// Generate banner
const banner = `/*!
 * ${pkg.name} v${pkg.version}
 * ${pkg.description}
 * 
 * @author ${pkg.author}
 * @license ${pkg.license}
 * @homepage ${pkg.homepage}
 */`;

export default defineConfig({
  publicDir: false,
  plugins: [dts({ rollupTypes: true, tsconfigPath: "./tsconfig.json" })],
  build: {
    lib: {
      entry: resolve(__dirname, "src/index.ts"),
      name: "hotkeys",
    },
    rollupOptions: {
      output: [
        {
          format: "es",
          banner,
          entryFileNames: "hotkeys-js.js",
        },
        {
          format: "umd",
          banner,
          entryFileNames: "hotkeys-js.umd.cjs",
          name: "hotkeys",
          footer: `
// CommonJS compatibility
if (typeof module === "object" && module.exports) {
  module.exports.default = module.exports;
}
// AMD compatibility  
if (typeof define === "function" && define.amd) {
  define([], function() { return hotkeys; });
}`,
        },
        {
          format: "iife",
          banner,
          entryFileNames: "hotkeys-js.min.js",
          name: "hotkeys",
          plugins: [terser()], // 仅对 IIFE 格式进行压缩
        },
      ],
    },
    minify: false,
    sourcemap: true,
    target: "es2015",
    emptyOutDir: false, // Don't empty the output directory
  },
});


================================================
FILE: website/App.tsx
================================================
import React, { useEffect, useState } from 'react';
import GithubCorner from '@uiw/react-github-corners';
import { Github } from '@uiw/react-shields';
import MarkdownPreview from '@uiw/react-markdown-preview';
import KeyBoard from '@uiw/react-mac-keyboard';
import '@wcj/dark-mode';
import Footer from './components/Footer';
import styles from './styles/index.module.css';
import DocumentStr from '../README.md?raw';
import DocumentChinese from '../README-zh.md?raw';
import hotkeys from '../src';
import pkg from '../package.json';

const englishDescription = `A robust Javascript library for capturing keyboard input and key combinations entered. It has no dependencies. Try to press your keyboard, The following button will highlight.`;
const chineseDescription = `一个强大的Javascript库,用于捕获键盘输入和键组合。它没有依赖。尝试按下你的键盘,下面的按钮将会高亮显示。`;

export default function AppRoot() {
  const [keyCode, setKeyCode] = useState<number[]>([]);
  const [keyStr, setKeyStr] = useState<(string | number)[]>([]);
  // 检查URL参数来决定初始语言
  const getInitialLanguage = () => {
    const urlParams = new URLSearchParams(window.location.search);
    const lang = urlParams.get('lang');
    return lang === 'zh' || lang === 'zh-CN';
  };
  const [isChineseDoc, setIsChineseDoc] = useState<boolean>(getInitialLanguage());
  const [documentContent, setDocumentContent] = useState<string>(
    getInitialLanguage() ? DocumentChinese : DocumentStr
  );

  useEffect(() => {
    document.addEventListener('keyup', onKeyUpEvent);

    function pkeys(keys: number[], key: number): number[] {
      if (keys.indexOf(key) === -1) keys.push(key);
      return keys;
    }
    function pkeysStr(
      keysStr: (string | number)[],
      key: string | number
    ): (string | number)[] {
      if (keysStr.indexOf(key) === -1) keysStr.push(key);
      return keysStr;
    }

    hotkeys('*', (evn) => {
      evn.preventDefault();
      const keys: number[] = [];
      const kStr: (string | number)[] = [];
      if (hotkeys.shift) {
        pkeys(keys, 16);
        pkeysStr(kStr, 'shift');
      }
      if (hotkeys.ctrl) {
        pkeys(keys, 17);
        pkeysStr(kStr, 'ctrl');
      }
      if (hotkeys.alt) {
        pkeys(keys, 18);
        pkeysStr(kStr, 'alt');
      }
      if (hotkeys.control) {
        pkeys(keys, 17);
        pkeysStr(kStr, 'control');
      }
      if (hotkeys.command) {
        pkeys(keys, 91);
        pkeysStr(kStr, 'command');
      }
      kStr.push(evn.keyCode);
      if (keys.indexOf(evn.keyCode) === -1) keys.push(evn.keyCode);

      setKeyCode(keys);
      setKeyStr(kStr);
    });

    return () => {
      document.removeEventListener('keyup', onKeyUpEvent);
    };
  }, []);

  const openVersionWebsite = (e: React.ChangeEvent<HTMLSelectElement>) => {
    if (e.target && e.target.value) {
      window.location.href = e.target.value;
    }
  };

  const onKeyUpEvent = () => {
    setKeyCode([]);
    setKeyStr([]);
  };

  const toggleLanguage = () => {
    const newIsChineseDoc = !isChineseDoc;
    setIsChineseDoc(newIsChineseDoc);
    setDocumentContent(newIsChineseDoc ? DocumentChinese : DocumentStr);

    // 更新URL参数
    const url = new URL(window.location.href);
    if (newIsChineseDoc) {
      url.searchParams.set('lang', 'zh');
    } else {
      url.searchParams.delete('lang');
    }
    window.history.pushState({}, '', url.toString());
  };

  const onKeyBoardMouseDown = (item: { keycode: number }) => {
    if (item.keycode > -1) {
      setKeyStr([item.keycode]);
    }
  };

  const onKeyBoardMouseUp = () => setKeyStr([]);

  return (
    <div>
      <div className={styles.tools}>
        <select className={styles.version} onChange={openVersionWebsite}>
          <option value="https://jaywcjlove.github.io/hotkeys-js">
            v{pkg.version}
          </option>
          <option value="https://raw.githack.com/jaywcjlove/hotkeys/dbdc32d/index.html">
            v3.13.15
          </option>
          <option value="https://unpkg.com/hotkeys-js@3.7.5/doc/index.html">
            v3.7.5
          </option>
          <option value="https://unpkg.com/hotkeys-js@3.4.2/doc/index.html">
            v3.4.2
          </option>
          <option value="https://unpkg.com/hotkeys-js@2.0.10/doc/index.html">
            v2.0.10
          </option>
        </select>
        <dark-mode permanent />
      </div>
      {keyStr.length > -1 && (
        <div className={styles.keyCodeInfo}>
          {keyStr.map((item) => (
            <span key={`${item}`}>{item}</span>
          ))}
        </div>
      )}
      <GithubCorner
        href="https://github.com/jaywcjlove/hotkeys-js"
        target="__blank"
      />
      <div className={styles.header}>
        <a className={styles.title} href="http://jaywcjlove.github.io">HotKeys.js</a>
        <div className={styles.github}>
          <a href="https://www.npmjs.com/package/hotkeys-js">
            <button type="button">On NPM</button>
          </a>&nbsp;
          <a href="https://github.com/jaywcjlove/hotkeys-js/">
            <button type="button">Fork on Github</button>
          </a>&nbsp;
          <a href="https://github.com/jaywcjlove/hotkeys-js/">
            <button type="button">Doc on Github</button>
          </a>&nbsp;
          <a href="https://gitee.com/jaywcjlove/hotkeys/">
            <button type="button">Doc on Gitee</button>
          </a>&nbsp;
          <button
            onClick={toggleLanguage}
            title={isChineseDoc ? 'Switch to English' : '切换到中文'}
          >
            {isChineseDoc ? 'EN' : '🇨🇳中文'}
          </button>
        </div>
        <div className={styles.info}>
          {isChineseDoc ? chineseDescription : englishDescription}
        </div>
      </div>
      <KeyBoard
        style={{ top: -40 }}
        onMouseDown={(_, item) => onKeyBoardMouseDown(item)}
        onMouseUp={onKeyBoardMouseUp}
        keyCode={keyCode}
      />
      <MarkdownPreview
        style={{ maxWidth: 995, margin: '0 auto' }}
        source={documentContent}
      />
      <Footer
        name="Kenny Wong"
        href="http://jaywcjlove.github.io"
        year="2015-present"
      >
        <Github user="jaywcjlove" repo="hotkeys-js">
          <Github.Social
            href="https://github.com/jaywcjlove/hotkeys-js"
            type="forks"
          />
          <Github.Social
            href="https://github.com/jaywcjlove/hotkeys-js"
            type="stars"
          />
          <Github.Social
            href="https://github.com/jaywcjlove/hotkeys-js"
            type="watchers"
          />
          <Github.Social
            href="https://github.com/jaywcjlove/hotkeys-js"
            type="followers"
          />
        </Github>
      </Footer>
    </div>
  );
}


================================================
FILE: website/components/Footer.module.css
================================================
.footer {
    text-align: center;
    padding: 15px 0 100px 0;
    font-size: 12px;
    line-height: 20px;
}



================================================
FILE: website/components/Footer.tsx
================================================
import React from 'react';
import style from './Footer.module.css';

interface FooterProps {
  name: string;
  href: string;
  year: string;
  children?: React.ReactNode;
}

export default function Footer({ name, href, year, children }: FooterProps) {
  return (
    <div className={style.footer}>
      {children}
      <div>
        Licensed under MIT. (Yes it&acute;s free and
        <a href="https://github.com/jaywcjlove/hotkeys"> open-sourced</a>
        )
      </div>
      <div>
        ©
        <a target="_blank" rel="noopener noreferrer" href={href}>{name}</a>
        {year}
      </div>
    </div>
  );
}


================================================
FILE: website/index.html
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>
      hotkeys.js - A robust Javascript library for capturing keyboard input.
    </title>
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1, maximum-scale=1"
    />
    <meta
      name="keywords"
      content="hotkey,hotkeys,hotkeysjs,key,keys,keyboard,shortcuts,keypress"
    />
    <meta
      name="description"
      content="A robust Javascript library for capturing keyboard input and key combinations entered. It has no dependencies. Try to press your keyboard, The following button will highlight."
    />
  </head>

  <body>
    <div id="root"></div>
    <script type="module" src="/index.tsx"></script>
  </body>
</html>


================================================
FILE: website/index.tsx
================================================
import { createRoot } from 'react-dom/client';
import App from './App';
import './styles/reset.css';

const container = document.getElementById('root');
if (!container) {
  throw new Error('Failed to find the root element');
}
const root = createRoot(container);
root.render(<App />);


================================================
FILE: website/styles/index.module.css
================================================
.tools {
  position: absolute;
  margin: 15px 0 0 15px;
}

.version {
  appearance: none;
  cursor: pointer;
  padding: 3px 6px;
  margin-right: 10px;
  vertical-align: middle;
  box-sizing: border-box;
  border: none;
  border-radius: 3px;
}

.keyCodeInfo {
  position: fixed;
  bottom: 10px;
  left: 10px;
  z-index: 9999;
}
.keyCodeInfo span + span {
  margin-left: 10px;
}
.keyCodeInfo span {
  display: inline-block;
  background: #eff0f2;
  border-radius: 3px;
  padding: 5px 10px;
  border-top: 1px solid #f5f5f5;
  box-shadow: inset 0 0 25px #e8e8e8, 0 1px 0 #c3c3c3, 0 2px 0 #c9c9c9, 0 2px 3px #333;
  text-shadow: 0px 1px 0px #f5f5f5;
}

.header {
  background-color: var(--color-header-bg);
  transition: all  0.3s;
  padding: 74px 0 60px 0;
  text-align: center;
}
.header .title {
  text-align: center;
  font-size: 53px;
  font-weight: bold;
  color: #fff;
  text-shadow: -3px -3px 0 #676767, -3px -3px 0 #676767, -3px -3px 0 #676767, -2px -2px 0 #676767, -2px -2px 0 #676767, -1px -1px 0 #676767;
  transition: all 0.3s;
}
.header .title:hover {
  text-shadow: -3px -3px 0 #363636, -3px -3px 0 #363636, -3px -3px 0 #363636, -2px -2px 0 #363636, -2px -2px 0 #363636, -1px -1px 0 #363636;
}
.header .lang {
  text-align: center;
  padding-top: 20px;
}
.header .lang a {
  color: #fff;
  margin: 0 5px;
}
.header .info {
  padding: 25px 0 27px 0;
  text-align: center;
  font-size: 23px;
  line-height: 29px;
  color: #878787;
  max-width: 702px;
  margin: 0 auto;
}
.header .github {
  text-align: center;
  padding: 60px 0 22px 0;
}
.header .github button {
  position: relative;
  display: inline-block;
  border: 1px solid #ddd;
  border-bottom-color: #bbb;
  padding: 0 15px;
  font-family: Helvetica, arial, freesans, clean, sans-serif;
  font-size: 12px;
  font-weight: bold;
  line-height: 23px;
  color: #666;
  text-shadow: 0 1px rgba(255, 255, 255, 0.9);
  cursor: pointer;
  border-radius: 3px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  background: whitesmoke;
  background-image: linear-gradient(-90deg, whitesmoke 0%, #e5e5e5 100%);
}
.header .github button:hover {
  color: #337797;
  border: 1px solid #CBE3EE;
  border-bottom-color: #97C7DD;
  background: #f0f7fa;
  background-image: -webkit-linear-gradient(-90deg, #f0f7fa 0%, #d8eaf2 100%);
}



================================================
FILE: website/styles/reset.css
================================================
[data-color-mode*='dark'], [data-color-mode*='dark'] body {
  --color-header-bg: var(--color-theme-bg);
}
[data-color-mode*='light'], [data-color-mode*='light'] body {
  --color-header-bg: #333;
  background: #f8f8f8 url(../assets/bg.jpg) repeat top left;
}

*, *:before, *:after {
  box-sizing: inherit !important;
}

body {
  font-family: "PingHei","Lucida Grande", "Lucida Sans Unicode", "STHeitiSC-Light", "Helvetica","Arial","Verdana","sans-serif";
  transition: all  0.3s;
  margin: 0;
}

a {
  text-decoration: none;
}

.wmde-markdown {
  background-color: transparent !important;
}
.wmde-markdown img {
  background-color: transparent !important;
}



================================================
FILE: website/tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "forceConsistentCasingInFileNames": true,
    "types": ["vite/client"]
  },
  "include": ["./**/*", "./css-modules.d.ts", "../src/**/*"],
  "exclude": ["node_modules", "../dist", "../test"]
}



================================================
FILE: website/vite-env.d.ts
================================================
/// <reference types="vite/client" />

declare module '*.module.css' {
  const classes: { readonly [key: string]: string };
  export default classes;
}

declare module '*.css' {
  const content: string;
  export default content;
}

import 'react';

declare module 'react' {
  namespace JSX {
    interface IntrinsicElements {
      'dark-mode': React.DetailedHTMLProps<
        React.HTMLAttributes<HTMLElement> & {
          permanent?: boolean;
        },
        HTMLElement
      >;
    }
  }
}


================================================
FILE: website/vite.config.ts
================================================
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';

export default defineConfig({
  base: './',
  plugins: [
    react({
      jsxRuntime: 'automatic',
    }),
  ],
  assetsInclude: ['**/*.md'],
  root: resolve(__dirname),
  publicDir: resolve(__dirname, '../public'),
  build: {
    outDir: resolve(__dirname, '../doc'),
    emptyOutDir: true,
    chunkSizeWarningLimit: 1000,
    rollupOptions: {
      output: {
        manualChunks: {
          // React 核心库单独分割
          'react-vendor': ['react', 'react-dom'],
          // UI 组件库分割
          'markdown-vendor': ['@uiw/react-markdown-preview'],
          'uiw-components': [
            '@uiw/react-github-corners',
            '@uiw/react-shields',
            '@uiw/react-mac-keyboard'
          ],
          // 主题和样式相关
          'theme-vendor': ['@wcj/dark-mode'],
        },
        // 自定义chunk命名和文件结构
        chunkFileNames: () => {
          return `assets/[name].js`;
        },
        assetFileNames: 'assets/[name]-[hash].[ext]'
      }
    },
    // 启用更好的压缩
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true, // 移除console.log
        drop_debugger: true,
      },
    },
    // 启用source map用于调试
    sourcemap: false, // 生产环境关闭source map减少文件大小
  },
  resolve: {
    alias: {
      '@': resolve(__dirname),
    },
  },
  server: {
    port: 3000,
    open: true,
  },
});
Download .txt
gitextract_070mo1hj/

├── .editorconfig
├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       ├── ci.yml
│       └── pr.yml
├── .gitignore
├── .husky/
│   ├── .gitignore
│   └── pre-commit
├── .npmignore
├── LICENSE
├── README-zh.md
├── README.md
├── dist/
│   ├── compatibility-test.html
│   ├── hotkeys-js.js
│   ├── hotkeys-js.umd.cjs
│   └── index.d.ts
├── eslint.config.js
├── package.json
├── src/
│   ├── index.ts
│   ├── types.ts
│   ├── utils.ts
│   └── var.ts
├── test/
│   ├── index.html
│   └── run.test.js
├── tsconfig.json
├── vite.config.ts
└── website/
    ├── App.tsx
    ├── components/
    │   ├── Footer.module.css
    │   └── Footer.tsx
    ├── index.html
    ├── index.tsx
    ├── styles/
    │   ├── index.module.css
    │   └── reset.css
    ├── tsconfig.json
    ├── vite-env.d.ts
    └── vite.config.ts
Download .txt
SYMBOL INDEX (74 symbols across 10 files)

FILE: dist/hotkeys-js.js
  function addEvent (line 10) | function addEvent(object, event, method, useCapture) {
  function removeEvent (line 17) | function removeEvent(object, event, method, useCapture) {
  function getMods (line 25) | function getMods(modifier, key) {
  function getKeys (line 33) | function getKeys(key) {
  function compareArray (line 45) | function compareArray(a1, a2) {
  function getLayoutIndependentKeyCode (line 54) | function getLayoutIndependentKeyCode(event) {
  function clearModifier (line 239) | function clearModifier(event) {
  function eventHandler (line 312) | function eventHandler(event, handler, scope, element) {
  function dispatch (line 338) | function dispatch(event, element) {
  function trigger (line 489) | function trigger(shortcut, scope = "all") {
  function removeKeyEvent (line 501) | function removeKeyEvent(element) {

FILE: dist/hotkeys-js.umd.cjs
  function addEvent (line 14) | function addEvent(object, event, method, useCapture) {
  function removeEvent (line 21) | function removeEvent(object, event, method, useCapture) {
  function getMods (line 29) | function getMods(modifier, key) {
  function getKeys (line 37) | function getKeys(key) {
  function compareArray (line 49) | function compareArray(a1, a2) {
  function getLayoutIndependentKeyCode (line 58) | function getLayoutIndependentKeyCode(event) {
  function clearModifier (line 243) | function clearModifier(event) {
  function eventHandler (line 316) | function eventHandler(event, handler, scope, element) {
  function dispatch (line 342) | function dispatch(event, element) {
  function trigger (line 493) | function trigger(shortcut, scope = "all") {
  function removeKeyEvent (line 505) | function removeKeyEvent(element) {

FILE: dist/index.d.ts
  type DeleteScope (line 4) | type DeleteScope = (scope?: string, newScope?: string) => void;
  type Filter (line 6) | type Filter = (event: KeyboardEvent) => boolean;
  type GetAllKeyCodes (line 8) | type GetAllKeyCodes = () => KeyCodeInfo[];
  type GetPressedKeyCodes (line 10) | type GetPressedKeyCodes = () => number[];
  type GetPressedKeyString (line 12) | type GetPressedKeyString = () => string[];
  type GetScope (line 14) | type GetScope = () => string;
  type HotkeysAPI (line 16) | interface HotkeysAPI {
  type HotkeysEvent (line 158) | interface HotkeysEvent {
  type HotkeysInterface (line 171) | interface HotkeysInterface extends HotkeysAPI {
  type HotkeysOptions (line 184) | interface HotkeysOptions {
  type IsPressed (line 194) | interface IsPressed {
  type KeyCodeInfo (line 201) | interface KeyCodeInfo {
  type KeyHandler (line 208) | interface KeyHandler {
  type NoConflict (line 212) | type NoConflict = (deep?: boolean) => HotkeysInterface;
  type SetScope (line 214) | type SetScope = (scope: string) => void;
  type Trigger (line 216) | type Trigger = (shortcut: string, scope?: string) => void;
  type Unbind (line 218) | interface Unbind {
  type UnbindInfo (line 227) | interface UnbindInfo {

FILE: src/index.ts
  function clearModifier (line 149) | function clearModifier(event: KeyboardEvent): void {
  function eventHandler (line 251) | function eventHandler(
  function dispatch (line 301) | function dispatch(
  function trigger (line 545) | function trigger(shortcut: string, scope: string = 'all'): void {
  function removeKeyEvent (line 559) | function removeKeyEvent(element: HTMLElement | Document | null): void {

FILE: src/types.ts
  type KeyCodeInfo (line 1) | interface KeyCodeInfo {
  type UnbindInfo (line 8) | interface UnbindInfo {
  type HotkeysEvent (line 15) | interface HotkeysEvent {
  type KeyHandler (line 28) | interface KeyHandler {
  type SetScope (line 32) | type SetScope = (scope: string) => void;
  type GetScope (line 34) | type GetScope = () => string;
  type DeleteScope (line 36) | type DeleteScope = (scope?: string, newScope?: string) => void;
  type GetPressedKeyCodes (line 38) | type GetPressedKeyCodes = () => number[];
  type GetPressedKeyString (line 40) | type GetPressedKeyString = () => string[];
  type GetAllKeyCodes (line 42) | type GetAllKeyCodes = () => KeyCodeInfo[];
  type IsPressed (line 44) | interface IsPressed {
  type Filter (line 51) | type Filter = (event: KeyboardEvent) => boolean;
  type Trigger (line 53) | type Trigger = (shortcut: string, scope?: string) => void;
  type Unbind (line 55) | interface Unbind {
  type NoConflict (line 64) | type NoConflict = (deep?: boolean) => HotkeysInterface;
  type HotkeysOptions (line 66) | interface HotkeysOptions {
  type HotkeysAPI (line 76) | interface HotkeysAPI {
  type HotkeysInterface (line 219) | interface HotkeysInterface extends HotkeysAPI {

FILE: src/utils.ts
  function addEvent (line 7) | function addEvent(
  function removeEvent (line 22) | function removeEvent(
  function getMods (line 39) | function getMods(modifier: Record<string, number>, key: string[]): numbe...
  function getKeys (line 49) | function getKeys(key: string | undefined): string[] {
  function compareArray (line 66) | function compareArray(a1: number[], a2: number[]): boolean {
  function getLayoutIndependentKeyCode (line 82) | function getLayoutIndependentKeyCode(event: KeyboardEvent): number {

FILE: test/run.test.js
  function createCoverageMap (line 571) | function createCoverageMap(entry, filePath) {

FILE: website/App.tsx
  function AppRoot (line 17) | function AppRoot() {

FILE: website/components/Footer.tsx
  type FooterProps (line 4) | interface FooterProps {
  function Footer (line 11) | function Footer({ name, href, year, children }: FooterProps) {

FILE: website/vite-env.d.ts
  type IntrinsicElements (line 17) | interface IntrinsicElements {
Condensed preview — 35 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (194K chars).
[
  {
    "path": ".editorconfig",
    "chars": 245,
    "preview": "# http://editorconfig.org\nroot = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_tr"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 118,
    "preview": "github: [jaywcjlove]\n#ko_fi: jaywcjlove\n#buy_me_a_coffee: jaywcjlove\n# custom: [\"https://wangchujiang.com/#/sponsor\"]\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 4068,
    "preview": "name: CI\non:\n  push:\n    branches:\n      - master\n    # tags:\n    #   - v*\n\nenv:\n  SKIP_PREFLIGHT_CHECK: true\n\njobs:\n  b"
  },
  {
    "path": ".github/workflows/pr.yml",
    "chars": 676,
    "preview": "name: CI-PR\non:\n  pull_request:\n\nenv:\n  SKIP_PREFLIGHT_CHECK: true\n\njobs:\n  build-deploy:\n    runs-on: ubuntu-latest\n   "
  },
  {
    "path": ".gitignore",
    "chars": 124,
    "preview": "npm-debug.log\nyarn.lock\nnode_modules\nbuild\ndoc\ncoverage\n\n.DS_Store\n.cache\n.vscode\n.idea\n\n*.bak\n*.tem\n*.temp\n#.swp\n*.*~\n~"
  },
  {
    "path": ".husky/.gitignore",
    "chars": 2,
    "preview": "_\n"
  },
  {
    "path": ".husky/pre-commit",
    "chars": 58,
    "preview": "#!/bin/sh\n. \"$(dirname \"$0\")/_/husky.sh\"\n\nnpx lint-staged\n"
  },
  {
    "path": ".npmignore",
    "chars": 311,
    "preview": "# Exclude test files\ndist/test-formats.html\n\n# Exclude development files\nsrc/\ntest/\nwebsite/\ncoverage/\n\n# Exclude config"
  },
  {
    "path": "LICENSE",
    "chars": 1155,
    "preview": "MIT License\n\nCopyright (c) 2015-present, Kenny Wong.\n\nCopyright (c) 2011-2013 Thomas Fuchs (https://github.com/madrobby/"
  },
  {
    "path": "README-zh.md",
    "chars": 17763,
    "preview": "<div markdown=\"1\">\n  <sup>使用<a href=\"https://wangchujiang.com/#/app\" target=\"_blank\">我的应用</a>也是一种<a href=\"https://wangch"
  },
  {
    "path": "README.md",
    "chars": 20429,
    "preview": "<div markdown=\"1\">\n  <sup>Using <a href=\"https://wangchujiang.com/#/app\" target=\"_blank\">my app</a> is also a way to <a "
  },
  {
    "path": "dist/compatibility-test.html",
    "chars": 22236,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width"
  },
  {
    "path": "dist/hotkeys-js.js",
    "chars": 16761,
    "preview": "/*!\n * hotkeys-js v4.0.2\n * A simple micro-library for defining and dispatching keyboard shortcuts. It has no dependenci"
  },
  {
    "path": "dist/hotkeys-js.umd.cjs",
    "chars": 18422,
    "preview": "(function(global, factory) {\n  typeof exports === \"object\" && typeof module !== \"undefined\" ? module.exports = factory()"
  },
  {
    "path": "dist/index.d.ts",
    "chars": 6942,
    "preview": "declare const _default: HotkeysInterface;\r\nexport default _default;\r\n\r\ndeclare type DeleteScope = (scope?: string, newSc"
  },
  {
    "path": "eslint.config.js",
    "chars": 3575,
    "preview": "// @ts-check\nimport { defineConfig } from 'eslint/config';\nimport eslint from '@eslint/js';\nimport tseslint from 'typesc"
  },
  {
    "path": "package.json",
    "chars": 2965,
    "preview": "{\n  \"name\": \"hotkeys-js\",\n  \"description\": \"A simple micro-library for defining and dispatching keyboard shortcuts. It h"
  },
  {
    "path": "src/index.ts",
    "chars": 21412,
    "preview": "import {\n  KeyCodeInfo,\n  HotkeysEvent,\n  UnbindInfo,\n  HotkeysOptions,\n  KeyHandler,\n  HotkeysInterface,\n  SetScope,\n  "
  },
  {
    "path": "src/types.ts",
    "chars": 6228,
    "preview": "export interface KeyCodeInfo {\n  scope: string;\n  shortcut: string;\n  mods: number[];\n  keys: number[];\n}\n\nexport interf"
  },
  {
    "path": "src/utils.ts",
    "chars": 3116,
    "preview": "const isff: boolean =\n  typeof navigator !== 'undefined'\n    ? navigator.userAgent.toLowerCase().indexOf('firefox') > 0\n"
  },
  {
    "path": "src/var.ts",
    "chars": 1787,
    "preview": "import { HotkeysEvent } from './types';\nimport { isff } from './utils';\n\n// Special Keys\nconst _keyMap: Record<string, n"
  },
  {
    "path": "test/index.html",
    "chars": 500,
    "preview": "<!DOCTYPE html>\n<html>\n<head>\n  <meta charset=\"UTF-8\">\n  <title>hotkeys.js</title>\n  <meta name=\"viewport\" content=\"widt"
  },
  {
    "path": "test/run.test.js",
    "chars": 19148,
    "preview": "/* eslint-disable @typescript-eslint/no-require-imports */\nconst puppeteer = require('puppeteer');\nconst path = require("
  },
  {
    "path": "tsconfig.json",
    "chars": 769,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2015\",\n    \"module\": \"ESNext\",\n    \"lib\": [\"ES2015\", \"DOM\"],\n    \"declaration\""
  },
  {
    "path": "vite.config.ts",
    "chars": 1542,
    "preview": "import { defineConfig } from \"vite\";\nimport { resolve } from \"path\";\nimport dts from \"vite-plugin-dts\";\nimport { readFil"
  },
  {
    "path": "website/App.tsx",
    "chars": 6722,
    "preview": "import React, { useEffect, useState } from 'react';\nimport GithubCorner from '@uiw/react-github-corners';\nimport { Githu"
  },
  {
    "path": "website/components/Footer.module.css",
    "chars": 110,
    "preview": ".footer {\n    text-align: center;\n    padding: 15px 0 100px 0;\n    font-size: 12px;\n    line-height: 20px;\n}\n\n"
  },
  {
    "path": "website/components/Footer.tsx",
    "chars": 621,
    "preview": "import React from 'react';\nimport style from './Footer.module.css';\n\ninterface FooterProps {\n  name: string;\n  href: str"
  },
  {
    "path": "website/index.html",
    "chars": 749,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>\n      hotkeys.js - A robust Javascrip"
  },
  {
    "path": "website/index.tsx",
    "chars": 285,
    "preview": "import { createRoot } from 'react-dom/client';\nimport App from './App';\nimport './styles/reset.css';\n\nconst container = "
  },
  {
    "path": "website/styles/index.module.css",
    "chars": 2280,
    "preview": ".tools {\n  position: absolute;\n  margin: 15px 0 0 15px;\n}\n\n.version {\n  appearance: none;\n  cursor: pointer;\n  padding: "
  },
  {
    "path": "website/styles/reset.css",
    "chars": 658,
    "preview": "[data-color-mode*='dark'], [data-color-mode*='dark'] body {\n  --color-header-bg: var(--color-theme-bg);\n}\n[data-color-mo"
  },
  {
    "path": "website/tsconfig.json",
    "chars": 749,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM."
  },
  {
    "path": "website/vite-env.d.ts",
    "chars": 499,
    "preview": "/// <reference types=\"vite/client\" />\n\ndeclare module '*.module.css' {\n  const classes: { readonly [key: string]: string"
  },
  {
    "path": "website/vite.config.ts",
    "chars": 1434,
    "preview": "import { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\nimport { resolve } from 'path';\n\nexport d"
  }
]

About this extraction

This page contains the full source code of the jaywcjlove/hotkeys-js GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 35 files (180.1 KB), approximately 51.7k tokens, and a symbol index with 74 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!