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: |
[](https://jaywcjlove.github.io/#/sponsor) [](https://uiwjs.github.io/npm-unpkg/#/pkg/hotkeys-js@${{steps.changelog.outputs.version}}/file/README.md) [](https://bundlephobia.com/result?p=hotkeys-js@${{steps.changelog.outputs.version}}) [](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
================================================
# Hotkeys
[](https://jaywcjlove.github.io/#/sponsor)
[](https://x.com/jaywcjlove)
[](https://www.npmjs.com/package/hotkeys-js)
[](https://github.com/jaywcjlove/hotkeys/stargazers)
[](https://github.com/jaywcjlove/hotkeys-js/actions/workflows/ci.yml)
[](https://jaywcjlove.github.io/hotkeys-js/lcov-report/index.html)
[](https://jaywcjlove.github.io/hotkeys-js/)
[](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
```
**UMD(通用模块定义)- 用于 CommonJS/AMD 环境:**
```html
```
**ES 模块 - 用于支持模块的现代浏览器:**
```html
```
### 在 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;
modifier: Record;
modifierMap: Record;
}
```
```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`:设置快捷键生效的作用域
- `element`:指定要绑定事件的 DOM 元素
- `keyup`:是否在按键释放时触发快捷键
- `keydown`:是否在按键按下时触发快捷键
- `splitKey`:组合键的分隔符(默认为 `+`)
- `capture`:是否在捕获阶段触发监听器(在事件冒泡之前)
- `single`:只允许一个回调函数(自动解绑之前的)
```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;
}
// 如何为编辑标签添加过滤器。
//
// "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 # 开发模式
```
## 贡献者
一如既往,感谢我们出色的贡献者!
使用 [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
================================================
# Hotkeys
[](https://jaywcjlove.github.io/#/sponsor)
[](https://x.com/jaywcjlove)
[](https://www.npmjs.com/package/hotkeys-js)
[](https://github.com/jaywcjlove/hotkeys/stargazers)
[](https://github.com/jaywcjlove/hotkeys-js/actions/workflows/ci.yml)
[](https://jaywcjlove.github.io/hotkeys-js/lcov-report/index.html)
[](https://wangchujiang.com/hotkeys-js/?lang=zh)
[](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
```
**UMD (Universal Module Definition) - For CommonJS/AMD environments:**
```html
```
**ES Module - For modern browsers with module support:**
```html
```
### 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;
modifier: Record;
modifierMap: Record;
}
```
```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`: Sets the scope in which the shortcut key is active
- `element`: Specifies the DOM element to bind the event to
- `keyup`: Whether to trigger the shortcut on key release
- `keydown`: Whether to trigger the shortcut on key press
- `splitKey`: Delimiter for key combinations (default is `+`)
- `capture`: Whether to trigger the listener during the capture phase (before the event bubbles down)
- `single`: 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.
//
// "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!
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
================================================
Hotkeys-js Comprehensive Compatibility Test
📦 Module Format Compatibility Tests
Test loading and basic functionality of hotkeys-js library in different build formats
🎯 Hotkey Functionality Tests
Test various hotkey combinations and functions
Ctrl + K
Basic hotkey test
Alt + S
Modifier key combination test
Ctrl + Shift + T
Multiple modifier keys test
Esc
Single key test
Space
Space key test
Enter
Enter key test
🔧 API Method Tests
Test various API methods provided by hotkeys-js
📝 Usage Examples
ESM Environment (Recommended)
import hotkeys from 'hotkeys-js';
hotkeys('ctrl+k', function(event, handler) {
event.preventDefault();
console.log('Hotkey pressed!');
});
CommonJS Environment
const hotkeys = require('hotkeys-js');
// or
const hotkeys = require('hotkeys-js').default;
hotkeys('ctrl+k', function(event, handler) {
event.preventDefault();
console.log('Hotkey pressed!');
});
Browser Global Environment
<script src="hotkeys-js/dist/hotkeys-js.min.js"></script>
<script>
hotkeys('ctrl+k', function(event, handler) {
event.preventDefault();
console.log('Hotkey pressed!');
});
</script>
================================================
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
* @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
* @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.
* //"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;
modifier: Record;
modifierMap: Record;
}
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 ",
"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', and