├── .husky ├── .gitignore └── pre-commit ├── website ├── assets │ └── bg.jpg ├── components │ ├── Footer.module.css │ └── Footer.tsx ├── index.tsx ├── vite-env.d.ts ├── vite.config.ts ├── styles │ ├── reset.css │ └── index.module.css ├── index.html ├── tsconfig.json └── App.tsx ├── .github ├── FUNDING.yml └── workflows │ ├── pr.yml │ └── ci.yml ├── .gitignore ├── .editorconfig ├── .npmignore ├── test ├── index.html └── run.test.js ├── tsconfig.json ├── LICENSE ├── vite.config.ts ├── src ├── var.ts ├── utils.ts ├── types.ts └── index.ts ├── package.json ├── eslint.config.js ├── dist ├── test-formats.html ├── hotkeys-js.min.js ├── hotkeys-js.umd.cjs ├── index.d.ts ├── hotkeys-js.js └── hotkeys-js.umd.cjs.map ├── README-zh.md └── README.md /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /website/assets/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaywcjlove/hotkeys-js/HEAD/website/assets/bg.jpg -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: jaywcjlove 2 | buy_me_a_coffee: jaywcjlove 3 | custom: ["https://www.paypal.me/kennyiseeyou", "https://jaywcjlove.github.io/#/sponsor"] 4 | -------------------------------------------------------------------------------- /website/components/Footer.module.css: -------------------------------------------------------------------------------- 1 | .footer { 2 | text-align: center; 3 | padding: 15px 0 100px 0; 4 | font-size: 12px; 5 | line-height: 20px; 6 | } 7 | 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | yarn.lock 3 | node_modules 4 | build 5 | doc 6 | coverage 7 | 8 | .DS_Store 9 | .cache 10 | .vscode 11 | .idea 12 | 13 | *.bak 14 | *.tem 15 | *.temp 16 | #.swp 17 | *.*~ 18 | ~*.* 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /website/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client'; 2 | import App from './App'; 3 | import './styles/reset.css'; 4 | 5 | const container = document.getElementById('root'); 6 | if (!container) { 7 | throw new Error('Failed to find the root element'); 8 | } 9 | const root = createRoot(container); 10 | root.render(); 11 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Exclude test files 2 | dist/test-formats.html 3 | 4 | # Exclude development files 5 | src/ 6 | test/ 7 | website/ 8 | coverage/ 9 | 10 | # Exclude configuration files 11 | *.config.ts 12 | *.config.js 13 | eslint.config.js 14 | tsconfig.json 15 | 16 | # Exclude other unnecessary files 17 | README-zh.md 18 | DISTRIBUTION-GUIDE.md 19 | DISTRIBUTION-GUIDE-zh.md 20 | .github/ 21 | .gitignore 22 | .husky/ -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | hotkeys.js 6 | 7 | 8 | 9 | 10 | 11 |
hotkeys
12 | 13 | 14 | -------------------------------------------------------------------------------- /website/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.module.css' { 4 | const classes: { readonly [key: string]: string }; 5 | export default classes; 6 | } 7 | 8 | declare module '*.css' { 9 | const content: string; 10 | export default content; 11 | } 12 | 13 | import 'react'; 14 | 15 | declare module 'react' { 16 | namespace JSX { 17 | interface IntrinsicElements { 18 | 'dark-mode': React.DetailedHTMLProps< 19 | React.HTMLAttributes & { 20 | permanent?: boolean; 21 | }, 22 | HTMLElement 23 | >; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /website/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | import { resolve } from 'path'; 4 | 5 | export default defineConfig({ 6 | base: './', 7 | plugins: [ 8 | react({ 9 | jsxRuntime: 'automatic', 10 | }), 11 | ], 12 | assetsInclude: ['**/*.md'], 13 | root: resolve(__dirname), 14 | publicDir: resolve(__dirname, '../public'), 15 | build: { 16 | outDir: resolve(__dirname, '../doc'), 17 | emptyOutDir: true, 18 | }, 19 | resolve: { 20 | alias: { 21 | '@': resolve(__dirname), 22 | }, 23 | }, 24 | server: { 25 | port: 3000, 26 | open: true, 27 | }, 28 | }); 29 | -------------------------------------------------------------------------------- /website/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import style from './Footer.module.css'; 3 | 4 | interface FooterProps { 5 | name: string; 6 | href: string; 7 | year: string; 8 | children?: React.ReactNode; 9 | } 10 | 11 | export default function Footer({ name, href, year, children }: FooterProps) { 12 | return ( 13 |
14 | {children} 15 |
16 | Licensed under MIT. (Yes it´s free and 17 | open-sourced 18 | ) 19 |
20 |
21 | © 22 | {name} 23 | {year} 24 |
25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /website/styles/reset.css: -------------------------------------------------------------------------------- 1 | [data-color-mode*='dark'], [data-color-mode*='dark'] body { 2 | --color-header-bg: var(--color-theme-bg); 3 | } 4 | [data-color-mode*='light'], [data-color-mode*='light'] body { 5 | --color-header-bg: #333; 6 | background: #f8f8f8 url(../assets/bg.jpg) repeat top left; 7 | } 8 | 9 | *, *:before, *:after { 10 | box-sizing: inherit !important; 11 | } 12 | 13 | body { 14 | font-family: "PingHei","Lucida Grande", "Lucida Sans Unicode", "STHeitiSC-Light", "Helvetica","Arial","Verdana","sans-serif"; 15 | transition: all 0.3s; 16 | margin: 0; 17 | } 18 | 19 | a { 20 | text-decoration: none; 21 | } 22 | 23 | .wmde-markdown { 24 | background-color: transparent !important; 25 | } 26 | .wmde-markdown img { 27 | background-color: transparent !important; 28 | } 29 | 30 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: CI-PR 2 | on: 3 | pull_request: 4 | 5 | env: 6 | SKIP_PREFLIGHT_CHECK: true 7 | 8 | jobs: 9 | build-deploy: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | id-token: write 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-node@v4 17 | with: 18 | node-version: 20 19 | registry-url: 'https://registry.npmjs.org' 20 | 21 | - name: Look Changelog 22 | uses: jaywcjlove/changelog-generator@main 23 | with: 24 | token: ${{ secrets.GITHUB_TOKEN }} 25 | filter-author: (jaywcjlove|小弟调调™|dependabot\[bot\]|Renovate Bot) 26 | filter: (^[\s]+?[R|r]elease)|(^[R|r]elease) 27 | 28 | - run: npm install 29 | - run: npm run build 30 | - run: npm run test 31 | -------------------------------------------------------------------------------- /website/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | hotkeys.js - A robust Javascript library for capturing keyboard input. 7 | 8 | 12 | 16 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | "moduleResolution": "bundler", 9 | "allowImportingTsExtensions": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "noEmit": true, 13 | "jsx": "react-jsx", 14 | "strict": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "esModuleInterop": true, 19 | "allowSyntheticDefaultImports": true, 20 | "forceConsistentCasingInFileNames": true, 21 | "types": ["vite/client"] 22 | }, 23 | "include": ["./**/*", "./css-modules.d.ts", "../src/**/*"], 24 | "exclude": ["node_modules", "../dist", "../test"] 25 | } 26 | 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2015", 4 | "module": "ESNext", 5 | "lib": ["ES2015", "DOM"], 6 | "declaration": false, 7 | "outDir": "./dist", 8 | "rootDir": "./src", 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "allowSyntheticDefaultImports": true, 16 | "noUnusedLocals": false, 17 | "noUnusedParameters": false, 18 | "noImplicitAny": true, 19 | "strictNullChecks": true, 20 | "strictFunctionTypes": true, 21 | "strictBindCallApply": true, 22 | "strictPropertyInitialization": true, 23 | "noImplicitThis": true, 24 | "alwaysStrict": true 25 | }, 26 | "include": ["src/**/*.ts"], 27 | "exclude": ["node_modules", "test", "website"] 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2015-present, Kenny Wong. 4 | 5 | Copyright (c) 2011-2013 Thomas Fuchs (https://github.com/madrobby/keymaster) 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { resolve } from "path"; 3 | import dts from "vite-plugin-dts"; 4 | import { readFileSync } from "fs"; 5 | 6 | const pkg = JSON.parse(readFileSync("./package.json", "utf-8")); 7 | 8 | // Generate banner 9 | const banner = `/*! 10 | * ${pkg.name} v${pkg.version} 11 | * ${pkg.description} 12 | * 13 | * @author ${pkg.author} 14 | * @license ${pkg.license} 15 | * @homepage ${pkg.homepage} 16 | */`; 17 | 18 | export default defineConfig({ 19 | publicDir: false, 20 | plugins: [dts({ rollupTypes: true, tsconfigPath: "./tsconfig.json" })], 21 | build: { 22 | lib: { 23 | entry: resolve(__dirname, "src/index.ts"), 24 | name: "hotkeys", 25 | formats: ["es", "umd", "iife"], 26 | fileName: (format) => { 27 | if (format === "es") return "hotkeys-js.js"; 28 | if (format === "umd") return "hotkeys-js.umd.cjs"; 29 | if (format === "iife") return "hotkeys-js.min.js"; 30 | return `hotkeys-js.${format}.js`; 31 | }, 32 | }, 33 | rollupOptions: { 34 | output: [ 35 | { 36 | format: "es", 37 | banner, 38 | entryFileNames: "hotkeys-js.js", 39 | }, 40 | { 41 | format: "umd", 42 | banner, 43 | entryFileNames: "hotkeys-js.umd.cjs", 44 | name: "hotkeys", 45 | footer: `if (typeof module === "object" && module.exports) { module.exports.default = module.exports; }`, 46 | }, 47 | { 48 | format: "iife", 49 | banner, 50 | entryFileNames: "hotkeys-js.min.js", 51 | name: "hotkeys", 52 | compact: true, // UMD 格式进行压缩 53 | }, 54 | ], 55 | }, 56 | minify: true, 57 | sourcemap: true, 58 | target: "es2015", 59 | emptyOutDir: false, // Don't empty the output directory 60 | }, 61 | }); 62 | -------------------------------------------------------------------------------- /src/var.ts: -------------------------------------------------------------------------------- 1 | import { HotkeysEvent } from './types'; 2 | import { isff } from './utils'; 3 | 4 | // Special Keys 5 | const _keyMap: Record = { 6 | backspace: 8, 7 | '⌫': 8, 8 | tab: 9, 9 | clear: 12, 10 | enter: 13, 11 | '↩': 13, 12 | return: 13, 13 | esc: 27, 14 | escape: 27, 15 | space: 32, 16 | left: 37, 17 | up: 38, 18 | right: 39, 19 | down: 40, 20 | /// https://w3c.github.io/uievents/#events-keyboard-key-location 21 | arrowup: 38, 22 | arrowdown: 40, 23 | arrowleft: 37, 24 | arrowright: 39, 25 | del: 46, 26 | delete: 46, 27 | ins: 45, 28 | insert: 45, 29 | home: 36, 30 | end: 35, 31 | pageup: 33, 32 | pagedown: 34, 33 | capslock: 20, 34 | num_0: 96, 35 | num_1: 97, 36 | num_2: 98, 37 | num_3: 99, 38 | num_4: 100, 39 | num_5: 101, 40 | num_6: 102, 41 | num_7: 103, 42 | num_8: 104, 43 | num_9: 105, 44 | num_multiply: 106, 45 | num_add: 107, 46 | num_enter: 108, 47 | num_subtract: 109, 48 | num_decimal: 110, 49 | num_divide: 111, 50 | '⇪': 20, 51 | ',': 188, 52 | '.': 190, 53 | '/': 191, 54 | '`': 192, 55 | '-': isff ? 173 : 189, 56 | '=': isff ? 61 : 187, 57 | ';': isff ? 59 : 186, 58 | '\'': 222, 59 | '{': 219, 60 | '}': 221, 61 | '[': 219, 62 | ']': 221, 63 | '\\': 220, 64 | }; 65 | 66 | // Modifier Keys 67 | const _modifier: Record = { 68 | // shiftKey 69 | '⇧': 16, 70 | shift: 16, 71 | // altKey 72 | '⌥': 18, 73 | alt: 18, 74 | option: 18, 75 | // ctrlKey 76 | '⌃': 17, 77 | ctrl: 17, 78 | control: 17, 79 | // metaKey 80 | '⌘': 91, 81 | cmd: 91, 82 | meta: 91, 83 | command: 91, 84 | }; 85 | 86 | const modifierMap: Record = { 87 | 16: 'shiftKey', 88 | 18: 'altKey', 89 | 17: 'ctrlKey', 90 | 91: 'metaKey', 91 | 92 | shiftKey: 16, 93 | ctrlKey: 17, 94 | altKey: 18, 95 | metaKey: 91, 96 | }; 97 | 98 | const _mods: Record = { 99 | 16: false, 100 | 18: false, 101 | 17: false, 102 | 91: false, 103 | }; 104 | 105 | const _handlers: Record = {}; 106 | 107 | // F1~F12 special key 108 | for (let k = 1; k < 20; k++) { 109 | _keyMap[`f${k}`] = 111 + k; 110 | } 111 | 112 | export { _keyMap, _modifier, modifierMap, _mods, _handlers }; 113 | -------------------------------------------------------------------------------- /website/styles/index.module.css: -------------------------------------------------------------------------------- 1 | .tools { 2 | position: absolute; 3 | margin: 15px 0 0 15px; 4 | } 5 | 6 | .version { 7 | appearance: none; 8 | cursor: pointer; 9 | padding: 3px 6px; 10 | margin-right: 10px; 11 | vertical-align: middle; 12 | box-sizing: border-box; 13 | border: none; 14 | border-radius: 3px; 15 | } 16 | 17 | .keyCodeInfo { 18 | position: fixed; 19 | bottom: 10px; 20 | left: 10px; 21 | z-index: 9999; 22 | } 23 | .keyCodeInfo span + span { 24 | margin-left: 10px; 25 | } 26 | .keyCodeInfo span { 27 | display: inline-block; 28 | background: #eff0f2; 29 | border-radius: 3px; 30 | padding: 5px 10px; 31 | border-top: 1px solid #f5f5f5; 32 | box-shadow: inset 0 0 25px #e8e8e8, 0 1px 0 #c3c3c3, 0 2px 0 #c9c9c9, 0 2px 3px #333; 33 | text-shadow: 0px 1px 0px #f5f5f5; 34 | } 35 | 36 | .header { 37 | background-color: var(--color-header-bg); 38 | transition: all 0.3s; 39 | padding: 74px 0 60px 0; 40 | text-align: center; 41 | } 42 | .header .title { 43 | text-align: center; 44 | font-size: 53px; 45 | font-weight: bold; 46 | color: #fff; 47 | 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; 48 | transition: all 0.3s; 49 | } 50 | .header .title:hover { 51 | 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; 52 | } 53 | .header .lang { 54 | text-align: center; 55 | padding-top: 20px; 56 | } 57 | .header .lang a { 58 | color: #fff; 59 | margin: 0 5px; 60 | } 61 | .header .info { 62 | padding: 25px 0 27px 0; 63 | text-align: center; 64 | font-size: 23px; 65 | line-height: 29px; 66 | color: #878787; 67 | max-width: 702px; 68 | margin: 0 auto; 69 | } 70 | .header .github { 71 | text-align: center; 72 | padding: 60px 0 22px 0; 73 | } 74 | .header .github button { 75 | position: relative; 76 | display: inline-block; 77 | border: 1px solid #ddd; 78 | border-bottom-color: #bbb; 79 | padding: 0 15px; 80 | font-family: Helvetica, arial, freesans, clean, sans-serif; 81 | font-size: 12px; 82 | font-weight: bold; 83 | line-height: 23px; 84 | color: #666; 85 | text-shadow: 0 1px rgba(255, 255, 255, 0.9); 86 | cursor: pointer; 87 | border-radius: 3px; 88 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 89 | background: whitesmoke; 90 | background-image: linear-gradient(-90deg, whitesmoke 0%, #e5e5e5 100%); 91 | } 92 | .header .github button:hover { 93 | color: #337797; 94 | border: 1px solid #CBE3EE; 95 | border-bottom-color: #97C7DD; 96 | background: #f0f7fa; 97 | background-image: -webkit-linear-gradient(-90deg, #f0f7fa 0%, #d8eaf2 100%); 98 | } 99 | 100 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hotkeys-js", 3 | "description": "A simple micro-library for defining and dispatching keyboard shortcuts. It has no dependencies.", 4 | "version": "4.0.0-beta.7", 5 | "type": "module", 6 | "main": "dist/hotkeys-js.umd.cjs", 7 | "module": "dist/hotkeys-js.js", 8 | "browser": "dist/hotkeys-js.min.js", 9 | "types": "dist/index.d.ts", 10 | "exports": { 11 | ".": { 12 | "import": "./dist/hotkeys-js.js", 13 | "require": "./dist/hotkeys-js.umd.cjs", 14 | "browser": "./dist/hotkeys-js.min.js", 15 | "types": "./dist/index.d.ts" 16 | } 17 | }, 18 | "scripts": { 19 | "prepare": "npm run build:lib && husky install", 20 | "lint": "eslint src website", 21 | "type-check": "tsc --noEmit", 22 | "watch": "vite build --watch", 23 | "build:lib": "vite build", 24 | "build": "npm run type-check && npm run build:lib && npm run doc && npm run lint", 25 | "pretest": "npm run build", 26 | "test": "jest --coverage --detectOpenHandles", 27 | "test:watch": "jest --watch", 28 | "doc": "vite build --config website/vite.config.ts", 29 | "start": "vite --config website/vite.config.ts" 30 | }, 31 | "files": [ 32 | "dist", 33 | "doc" 34 | ], 35 | "keywords": [ 36 | "hotkey", 37 | "hotkeys", 38 | "hotkeys-js", 39 | "hotkeysjs", 40 | "key", 41 | "keys", 42 | "keyboard", 43 | "shortcuts", 44 | "keypress" 45 | ], 46 | "author": "kenny wong ", 47 | "license": "MIT", 48 | "homepage": "https://jaywcjlove.github.io/hotkeys-js", 49 | "funding": "https://jaywcjlove.github.io/#/sponsor", 50 | "repository": { 51 | "type": "git", 52 | "url": "https://github.com/jaywcjlove/hotkeys-js.git" 53 | }, 54 | "jest": { 55 | "testEnvironmentOptions": { 56 | "url": "http://localhost/" 57 | } 58 | }, 59 | "devDependencies": { 60 | "@eslint/js": "^9.39.1", 61 | "@types/node": "^24.10.1", 62 | "@types/react": "^19.2.4", 63 | "@types/react-dom": "^19.2.3", 64 | "@uiw/react-github-corners": "^1.5.15", 65 | "@uiw/react-mac-keyboard": "^1.1.5", 66 | "@uiw/react-markdown-preview": "^5.0.3", 67 | "@uiw/react-shields": "^2.0.1", 68 | "@vitejs/plugin-react": "^5.1.1", 69 | "@wcj/dark-mode": "~1.0.15", 70 | "eslint": "^9.39.1", 71 | "eslint-plugin-import": "^2.32.0", 72 | "eslint-plugin-jsx-a11y": "^6.10.2", 73 | "eslint-plugin-react": "^7.37.5", 74 | "eslint-plugin-react-hooks": "^5.1.0", 75 | "globals": "^16.5.0", 76 | "husky": "^8.0.3", 77 | "jest": "^29.7.0", 78 | "jest-environment-jsdom": "^29.7.0", 79 | "lint-staged": "^15.2.0", 80 | "puppeteer": "~13.5.2", 81 | "react": "^19.2.0", 82 | "react-dom": "^19.2.0", 83 | "typescript": "^5.8.2", 84 | "typescript-eslint": "^8.46.4", 85 | "vite": "^5.1.0", 86 | "vite-plugin-dts": "^4.5.4" 87 | }, 88 | "browserslist": { 89 | "production": [ 90 | ">0.2%", 91 | "not dead", 92 | "not op_mini all" 93 | ], 94 | "development": [ 95 | "last 1 chrome version", 96 | "last 1 firefox version", 97 | "last 1 safari version" 98 | ] 99 | }, 100 | "lint-staged": { 101 | "src/**/*.{js,ts}": "eslint", 102 | "website/**/*.{js,jsx,ts,tsx}": "eslint" 103 | }, 104 | "overrides": { 105 | "react": "^19.2.0", 106 | "react-dom": "^19.2.0" 107 | } 108 | } -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | const isff: boolean = 2 | typeof navigator !== 'undefined' 3 | ? navigator.userAgent.toLowerCase().indexOf('firefox') > 0 4 | : false; 5 | 6 | /** Bind event */ 7 | function addEvent( 8 | object: HTMLElement | Document | Window, 9 | event: string, 10 | method: EventListenerOrEventListenerObject, 11 | useCapture?: boolean 12 | ): void { 13 | if (object.addEventListener) { 14 | object.addEventListener(event, method, useCapture); 15 | // @ts-expect-error - attachEvent is only available on IE 16 | } else if (object.attachEvent) { 17 | // @ts-expect-error - attachEvent is only available on IE 18 | object.attachEvent(`on${event}`, method); 19 | } 20 | } 21 | 22 | function removeEvent( 23 | object: HTMLElement | Document | Window | null, 24 | event: string, 25 | method: EventListenerOrEventListenerObject, 26 | useCapture?: boolean 27 | ): void { 28 | if (!object) return; 29 | if (object.removeEventListener) { 30 | object.removeEventListener(event, method, useCapture); 31 | // @ts-expect-error - removeEvent is only available on IE 32 | } else if (object.detachEvent) { 33 | // @ts-expect-error - detachEvent is only available on IE 34 | object.detachEvent(`on${event}`, method); 35 | } 36 | } 37 | 38 | /** Convert modifier keys to their corresponding key codes */ 39 | function getMods(modifier: Record, key: string[]): number[] { 40 | const modsKeys = key.slice(0, key.length - 1); 41 | const modsCodes: number[] = []; 42 | for (let i = 0; i < modsKeys.length; i++) { 43 | modsCodes.push(modifier[modsKeys[i].toLowerCase()]); 44 | } 45 | return modsCodes; 46 | } 47 | 48 | /** Process the input key string and convert it to an array */ 49 | function getKeys(key: string | undefined): string[] { 50 | if (typeof key !== 'string') key = ''; 51 | key = key.replace(/\s/g, ''); // Match any whitespace character, including spaces, tabs, form feeds, etc. 52 | const keys = key.split(','); // Allow multiple shortcuts separated by ',' 53 | let index = keys.lastIndexOf(''); 54 | 55 | // Shortcut may include ',' — special handling needed 56 | for (; index >= 0; ) { 57 | keys[index - 1] += ','; 58 | keys.splice(index, 1); 59 | index = keys.lastIndexOf(''); 60 | } 61 | 62 | return keys; 63 | } 64 | 65 | /** Compare arrays of modifier keys */ 66 | function compareArray(a1: number[], a2: number[]): boolean { 67 | const arr1 = a1.length >= a2.length ? a1 : a2; 68 | const arr2 = a1.length >= a2.length ? a2 : a1; 69 | let isIndex = true; 70 | 71 | for (let i = 0; i < arr1.length; i++) { 72 | if (arr2.indexOf(arr1[i]) === -1) isIndex = false; 73 | } 74 | return isIndex; 75 | } 76 | 77 | /** 78 | * Get layout-independent key code from keyboard event 79 | * This makes hotkeys work regardless of keyboard layout (Russian, German, etc.) 80 | * Uses event.code (physical key) instead of keyCode (character) for letter keys 81 | */ 82 | function getLayoutIndependentKeyCode(event: KeyboardEvent): number { 83 | let key = event.keyCode || event.which || event.charCode; 84 | 85 | // Convert physical key code (KeyA-KeyZ) to corresponding ASCII code (65-90) 86 | // This makes 'ctrl+a' work on any keyboard layout 87 | if (event.code && /^Key[A-Z]$/.test(event.code)) { 88 | key = event.code.charCodeAt(3); // "KeyA"[3] = "A" -> 65 89 | } 90 | 91 | return key; 92 | } 93 | 94 | export { 95 | isff, 96 | getMods, 97 | getKeys, 98 | addEvent, 99 | removeEvent, 100 | compareArray, 101 | getLayoutIndependentKeyCode, 102 | }; 103 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { defineConfig } from 'eslint/config'; 3 | import eslint from '@eslint/js'; 4 | import tseslint from 'typescript-eslint'; 5 | import reactPlugin from 'eslint-plugin-react'; 6 | import reactHooksPlugin from 'eslint-plugin-react-hooks'; 7 | import importPlugin from 'eslint-plugin-import'; 8 | import jsxA11yPlugin from 'eslint-plugin-jsx-a11y'; 9 | import globals from 'globals'; 10 | 11 | export default defineConfig([ 12 | eslint.configs.recommended, 13 | ...tseslint.configs.recommended, 14 | { 15 | files: ['**/*.{js,jsx,ts,tsx}'], 16 | languageOptions: { 17 | ecmaVersion: 2020, 18 | sourceType: 'module', 19 | parserOptions: { 20 | ecmaFeatures: { 21 | jsx: true, 22 | }, 23 | }, 24 | globals: { 25 | ...globals.browser, 26 | ...globals.node, 27 | ...globals.es2020, 28 | ...globals.jest, 29 | }, 30 | }, 31 | plugins: { 32 | react: reactPlugin, 33 | 'react-hooks': reactHooksPlugin, 34 | import: importPlugin, 35 | 'jsx-a11y': jsxA11yPlugin, 36 | }, 37 | settings: { 38 | react: { 39 | version: 'detect', 40 | }, 41 | }, 42 | rules: { 43 | // TypeScript rules 44 | '@typescript-eslint/no-explicit-any': 'off', 45 | '@typescript-eslint/explicit-module-boundary-types': 'off', 46 | '@typescript-eslint/no-inferrable-types': 'off', 47 | '@typescript-eslint/no-non-null-assertion': 'off', 48 | '@typescript-eslint/no-unused-expressions': 'error', 49 | 50 | // General rules 51 | 'no-console': ['error', { allow: ['log'] }], 52 | 'no-underscore-dangle': 'off', 53 | 'no-plusplus': 'off', 54 | 'no-param-reassign': 'off', 55 | 'no-restricted-syntax': 'off', 56 | 'no-use-before-define': 'off', 57 | 'max-len': 'off', 58 | 'comma-dangle': 'off', 59 | 'no-multiple-empty-lines': ['error', { max: 1, maxEOF: 0 }], 60 | 'quotes': ['error', 'single', { avoidEscape: true, allowTemplateLiterals: true }], 61 | 'indent': ['error', 2, { SwitchCase: 1 }], 62 | 'linebreak-style': ['error', 'unix'], 63 | 'no-trailing-spaces': 'error', 64 | 'eol-last': ['error', 'always'], 65 | 'object-curly-newline': 'off', 66 | 'arrow-body-style': 'off', 67 | 'consistent-return': 'off', 68 | 'generator-star-spacing': 'off', 69 | 'global-require': 'warn', 70 | 'no-bitwise': 'off', 71 | 'no-cond-assign': 'off', 72 | 'no-else-return': 'off', 73 | 'no-nested-ternary': 'off', 74 | 'require-yield': 'warn', 75 | 'class-methods-use-this': 'off', 76 | 'no-confusing-arrow': 'off', 77 | 'no-unused-expressions': 'off', 78 | 79 | // React rules 80 | 'react/jsx-filename-extension': [ 81 | 'warn', 82 | { 83 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 84 | }, 85 | ], 86 | 'react/jsx-no-bind': 'off', 87 | 'react/prop-types': 'off', 88 | 'react/no-array-index-key': 'off', 89 | 'react/forbid-prop-types': 'off', 90 | 'react/prefer-stateless-function': 'off', 91 | 'react/sort-comp': 'off', 92 | 'react/no-did-mount-set-state': 'off', 93 | 'react-hooks/rules-of-hooks': 'error', 94 | 'react-hooks/exhaustive-deps': 'warn', 95 | 96 | // Import rules 97 | 'import/extensions': 'off', 98 | 'import/no-unresolved': 'off', 99 | 'import/no-extraneous-dependencies': 'off', 100 | 'import/prefer-default-export': 'off', 101 | 102 | // JSX A11y rules 103 | 'jsx-a11y/no-noninteractive-element-interactions': 'off', 104 | 'jsx-a11y/no-static-element-interactions': 'off', 105 | }, 106 | }, 107 | { 108 | files: ['**/*.ts', '**/*.tsx'], 109 | languageOptions: { 110 | parserOptions: { 111 | projectService: true, 112 | tsconfigRootDir: import.meta.dirname, 113 | }, 114 | }, 115 | }, 116 | { 117 | ignores: ['dist/**', 'node_modules/**', 'coverage/**', 'build/**', 'doc/**'], 118 | }, 119 | ]); 120 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | # tags: 7 | # - v* 8 | 9 | env: 10 | SKIP_PREFLIGHT_CHECK: true 11 | 12 | jobs: 13 | build-deploy: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: write 17 | id-token: write 18 | steps: 19 | - uses: actions/checkout@v6 20 | - uses: actions/setup-node@v6 21 | with: 22 | node-version: 22 23 | registry-url: 'https://registry.npmjs.org' 24 | 25 | - name: Look Changelog 26 | uses: jaywcjlove/changelog-generator@main 27 | with: 28 | token: ${{ secrets.GITHUB_TOKEN }} 29 | filter-author: (jaywcjlove|小弟调调™|dependabot\[bot\]|Renovate Bot) 30 | filter: (^[\s]+?[R|r]elease)|(^[R|r]elease) 31 | 32 | - run: npm install 33 | - run: npm run build 34 | - run: npm run test 35 | - run: cp -rp dist doc 36 | 37 | - name: Generate Contributors Images 38 | uses: jaywcjlove/github-action-contributors@main 39 | with: 40 | filter-author: (renovate\[bot\]|renovate-bot|dependabot\[bot\]) 41 | output: doc/CONTRIBUTORS.svg 42 | avatarSize: 42 43 | 44 | - name: Create Tag 45 | id: create_tag 46 | uses: jaywcjlove/create-tag-action@main 47 | with: 48 | package-path: ./package.json 49 | 50 | - name: get tag version 51 | id: tag_version 52 | uses: jaywcjlove/changelog-generator@main 53 | 54 | - name: Build and Deploy 55 | uses: peaceiris/actions-gh-pages@v4 56 | with: 57 | commit_message: ${{steps.tag_version.outputs.tag}} ${{ github.event.head_commit.message }} 58 | github_token: ${{ secrets.GITHUB_TOKEN }} 59 | publish_dir: ./doc 60 | 61 | - name: Generate Changelog 62 | id: changelog 63 | uses: jaywcjlove/changelog-generator@main 64 | with: 65 | token: ${{ secrets.GITHUB_TOKEN }} 66 | filter-author: (jaywcjlove|小弟调调™|dependabot\[bot\]|Renovate Bot) 67 | filter: (^[\s]+?[R|r]elease)|(^[R|r]elease) 68 | 69 | - run: | 70 | echo "tag: ${{ steps.changelog.outputs.tag }}" 71 | echo "version: ${{ steps.changelog.outputs.version }}" 72 | echo "ref: ${{ github.ref }}" 73 | 74 | - name: Create Release 75 | uses: ncipollo/release-action@v1 76 | if: steps.create_tag.outputs.successful 77 | with: 78 | allowUpdates: true 79 | token: ${{ secrets.GITHUB_TOKEN }} 80 | name: ${{ steps.create_tag.outputs.version }} 81 | tag: ${{ steps.create_tag.outputs.version }} 82 | body: | 83 | [![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) 84 | 85 | Documentation ${{ steps.changelog.outputs.tag }}: https://raw.githack.com/jaywcjlove/hotkeys/${{ steps.changelog.outputs.gh-pages-short-hash }}/index.html 86 | Comparing Changes: ${{ steps.changelog.outputs.compareurl }} 87 | 88 | ```bash 89 | npm i hotkeys-js@${{steps.changelog.outputs.version}} 90 | ``` 91 | 92 | ${{ steps.changelog.outputs.changelog }} 93 | 94 | - name: package.json info 95 | uses: jaywcjlove/github-action-package@main 96 | with: 97 | unset: browserslist,lint-staged,devDependencies,jest,scripts 98 | 99 | # npm@v11.5.0+ is required for OIDC support 100 | - name: Upgrade npm for OIDC support 101 | run: npm install -g npm@latest 102 | # node@v22.0.0+ 103 | # https://github.com/orgs/community/discussions/176761 104 | # https://github.com/actions/setup-node/issues/1440#issuecomment-3571890875 105 | - run: NODE_AUTH_TOKEN="" npm publish --access public --provenance 106 | name: 📦 hotkeys-js to NPM 107 | continue-on-error: true -------------------------------------------------------------------------------- /dist/test-formats.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | HotKeys.js Test 7 | 33 | 34 | 35 |

HotKeys.js Format Test

36 |

Test different distribution formats of hotkeys-js library:

37 | 38 |
39 |

1. IIFE Format Test (hotkeys-js.min.js)

40 |

Press F1 to test IIFE version

41 |
Loading...
42 |
43 | 44 |
45 |

2. UMD Format Test (hotkeys-js.umd.cjs)

46 |

Press F2 to test UMD version

47 |
Loading...
48 |
49 | 50 |
51 |

3. ES Module Format Test (hotkeys-js.js)

52 |

Press F3 to test ES Module version

53 |
Loading...
54 |

Note: This test requires a server environment to work properly due to module loading restrictions in browsers.

55 |
56 | 57 | 58 | 59 | 74 | 75 | 76 | 80 | 81 | 96 | 97 | 98 | 110 | 111 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface KeyCodeInfo { 2 | scope: string; 3 | shortcut: string; 4 | mods: number[]; 5 | keys: number[]; 6 | } 7 | 8 | export interface UnbindInfo { 9 | key: string; 10 | scope?: string; 11 | method?: KeyHandler; 12 | splitKey?: string; 13 | } 14 | 15 | export interface HotkeysEvent { 16 | keyup: boolean; 17 | keydown: boolean; 18 | scope: string; 19 | mods: number[]; 20 | shortcut: string; 21 | method: KeyHandler; 22 | key: string; 23 | splitKey: string; 24 | element: HTMLElement | Document; 25 | keys?: number[]; 26 | } 27 | 28 | export interface KeyHandler { 29 | (keyboardEvent: KeyboardEvent, hotkeysEvent: HotkeysEvent): void | boolean; 30 | } 31 | 32 | export type SetScope = (scope: string) => void; 33 | 34 | export type GetScope = () => string; 35 | 36 | export type DeleteScope = (scope?: string, newScope?: string) => void; 37 | 38 | export type GetPressedKeyCodes = () => number[]; 39 | 40 | export type GetPressedKeyString = () => string[]; 41 | 42 | export type GetAllKeyCodes = () => KeyCodeInfo[]; 43 | 44 | export interface IsPressed { 45 | /** For example, `hotkeys.isPressed(77)` is true if the `M` key is currently pressed. */ 46 | (keyCode: number): boolean; 47 | /** For example, `hotkeys.isPressed('m')` is true if the `M` key is currently pressed. */ 48 | (keyCode: string): boolean; 49 | } 50 | 51 | export type Filter = (event: KeyboardEvent) => boolean; 52 | 53 | export type Trigger = (shortcut: string, scope?: string) => void; 54 | 55 | export interface Unbind { 56 | (key?: string): void; 57 | (keysInfo: UnbindInfo): void; 58 | (keysInfo: UnbindInfo[]): void; 59 | (key: string, scopeName: string): void; 60 | (key: string, scopeName: string, method: KeyHandler): void; 61 | (key: string, method: KeyHandler): void; 62 | } 63 | 64 | export type NoConflict = (deep?: boolean) => HotkeysInterface; 65 | 66 | export interface HotkeysOptions { 67 | scope?: string; 68 | element?: HTMLElement | Document; 69 | keyup?: boolean; 70 | keydown?: boolean; 71 | capture?: boolean; 72 | splitKey?: string; 73 | single?: boolean; 74 | } 75 | 76 | export interface HotkeysAPI { 77 | /** 78 | * Use the `hotkeys.setScope` method to set scope. There can only be one active scope besides 'all'. By default 'all' is always active. 79 | * 80 | * ```js 81 | * // Define shortcuts with a scope 82 | * hotkeys('ctrl+o, ctrl+alt+enter', 'issues', function() { 83 | * console.log('do something'); 84 | * }); 85 | * hotkeys('o, enter', 'files', function() { 86 | * console.log('do something else'); 87 | * }); 88 | * 89 | * // Set the scope (only 'all' and 'issues' shortcuts will be honored) 90 | * hotkeys.setScope('issues'); // default scope is 'all' 91 | * ``` 92 | */ 93 | setScope: SetScope; 94 | /** 95 | * Use the `hotkeys.getScope` method to get scope. 96 | * 97 | * ```js 98 | * hotkeys.getScope(); 99 | * ``` 100 | */ 101 | getScope: GetScope; 102 | /** 103 | * Use the `hotkeys.deleteScope` method to delete a scope. This will also remove all associated hotkeys with it. 104 | * 105 | * ```js 106 | * hotkeys.deleteScope('issues'); 107 | * ``` 108 | * You can use second argument, if need set new scope after deleting. 109 | * 110 | * ```js 111 | * hotkeys.deleteScope('issues', 'newScopeName'); 112 | * ``` 113 | */ 114 | deleteScope: DeleteScope; 115 | /** 116 | * Returns an array of key codes currently pressed. 117 | * 118 | * ```js 119 | * hotkeys('command+ctrl+shift+a,f', function() { 120 | * console.log(hotkeys.getPressedKeyCodes()); //=> [17, 65] or [70] 121 | * }) 122 | * ``` 123 | */ 124 | getPressedKeyCodes: GetPressedKeyCodes; 125 | /** 126 | * Returns an array of key codes currently pressed. 127 | * 128 | * ```js 129 | * hotkeys('command+ctrl+shift+a,f', function() { 130 | * console.log(hotkeys.getPressedKeyString()); //=> ['⌘', '⌃', '⇧', 'A', 'F'] 131 | * }) 132 | * ``` 133 | */ 134 | getPressedKeyString: GetPressedKeyString; 135 | /** 136 | * Get a list of all registration codes. 137 | * 138 | * ```js 139 | * hotkeys('command+ctrl+shift+a,f', function() { 140 | * console.log(hotkeys.getAllKeyCodes()); 141 | * // [ 142 | * // { scope: 'all', shortcut: 'command+ctrl+shift+a', mods: [91, 17, 16], keys: [91, 17, 16, 65] }, 143 | * // { scope: 'all', shortcut: 'f', mods: [], keys: [42] } 144 | * // ] 145 | * }) 146 | * ``` 147 | * 148 | */ 149 | getAllKeyCodes: GetAllKeyCodes; 150 | isPressed: IsPressed; 151 | /** 152 | * By default hotkeys are not enabled for `INPUT` `SELECT` `TEXTAREA` elements. 153 | * `Hotkeys.filter` to return to the `true` shortcut keys set to play a role, 154 | * `false` shortcut keys set up failure. 155 | * 156 | * ```js 157 | * hotkeys.filter = function(event){ 158 | * return true; 159 | * } 160 | * //How to add the filter to edit labels.
161 | * //"contentEditable" Older browsers that do not support drops 162 | * hotkeys.filter = function(event) { 163 | * var target = event.target || event.srcElement; 164 | * var tagName = target.tagName; 165 | * return !(target.isContentEditable || tagName == 'INPUT' || tagName == 'SELECT' || tagName == 'TEXTAREA'); 166 | * } 167 | * 168 | * hotkeys.filter = function(event){ 169 | * var tagName = (event.target || event.srcElement).tagName; 170 | * hotkeys.setScope(/^(INPUT|TEXTAREA|SELECT)$/.test(tagName) ? 'input' : 'other'); 171 | * return true; 172 | * } 173 | * ``` 174 | */ 175 | filter: Filter; 176 | /** 177 | * trigger shortcut key event 178 | * 179 | * ```js 180 | * hotkeys.trigger('ctrl+o'); 181 | * hotkeys.trigger('ctrl+o', 'scope2'); 182 | * ``` 183 | */ 184 | trigger: Trigger; 185 | /** 186 | * Unbinds a shortcut key event. 187 | * 188 | * ```js 189 | * hotkeys.unbind('ctrl+o'); 190 | * hotkeys.unbind('ctrl+o', 'scope1'); 191 | * hotkeys.unbind('ctrl+o', 'scope1', method); 192 | * hotkeys.unbind('ctrl+o', method); 193 | * ``` 194 | */ 195 | unbind: Unbind; 196 | /** 197 | * Relinquish HotKeys’s control of the `hotkeys` variable. 198 | * 199 | * ```js 200 | * var k = hotkeys.noConflict(); 201 | * k('a', function() { 202 | * console.log("do something") 203 | * }); 204 | * 205 | * hotkeys() 206 | * // -->Uncaught TypeError: hotkeys is not a function(anonymous function) 207 | * // @ VM2170:2InjectedScript._evaluateOn 208 | * // @ VM2165:883InjectedScript._evaluateAndWrap 209 | * // @ VM2165:816InjectedScript.evaluate @ VM2165:682 210 | * ``` 211 | */ 212 | noConflict: NoConflict; 213 | 214 | keyMap: Record; 215 | modifier: Record; 216 | modifierMap: Record; 217 | } 218 | 219 | export interface HotkeysInterface extends HotkeysAPI { 220 | (key: string, method: KeyHandler): void; 221 | (key: string, scope: string, method: KeyHandler): void; 222 | (key: string, option: HotkeysOptions, method: KeyHandler): void; 223 | 224 | shift?: boolean; 225 | ctrl?: boolean; 226 | alt?: boolean; 227 | option?: boolean; 228 | control?: boolean; 229 | cmd?: boolean; 230 | command?: boolean; 231 | } 232 | -------------------------------------------------------------------------------- /dist/hotkeys-js.min.js: -------------------------------------------------------------------------------- 1 | var hotkeys=function(){"use strict";/*! 2 | * hotkeys-js v4.0.0-beta.7 3 | * A simple micro-library for defining and dispatching keyboard shortcuts. It has no dependencies. 4 | * 5 | * @author kenny wong 6 | * @license MIT 7 | * @homepage https://jaywcjlove.github.io/hotkeys-js 8 | */const j=typeof navigator!="undefined"?navigator.userAgent.toLowerCase().indexOf("firefox")>0:!1;function P(e,t,n,o){e.addEventListener?e.addEventListener(t,n,o):e.attachEvent&&e.attachEvent(`on${t}`,n)}function b(e,t,n,o){e&&(e.removeEventListener?e.removeEventListener(t,n,o):e.detachEvent&&e.detachEvent(`on${t}`,n))}function U(e,t){const n=t.slice(0,t.length-1),o=[];for(let s=0;s=0;)t[n-1]+=",",t.splice(n,1),n=t.lastIndexOf("");return t}function R(e,t){const n=e.length>=t.length?e:t,o=e.length>=t.length?t:e;let s=!0;for(let r=0;r_[e.toLowerCase()]||h[e.toLowerCase()]||e.toUpperCase().charCodeAt(0),V=e=>Object.keys(_).find(t=>_[t]===e),X=e=>Object.keys(h).find(t=>h[t]===e),D=e=>{B=e||"all"},x=()=>B||"all",Z=()=>c.slice(0),q=()=>c.map(e=>V(e)||X(e)||String.fromCharCode(e)),J=()=>{const e=[];return Object.keys(l).forEach(t=>{l[t].forEach(({key:n,scope:o,mods:s,shortcut:r})=>{e.push({scope:o,shortcut:r,mods:s,keys:n.split("+").map(a=>E(a))})})}),e},I=e=>{const t=e.target||e.srcElement,{tagName:n}=t;let o=!0;const s=n==="INPUT"&&!["checkbox","radio","range","button","file","reset","submit","color"].includes(t.type);return(t.isContentEditable||(s||n==="TEXTAREA"||n==="SELECT")&&!t.readOnly)&&(o=!1),o},Q=e=>(typeof e=="string"&&(e=E(e)),c.indexOf(e)!==-1),W=(e,t)=>{let n,o;e||(e=x());for(const s in l)if(Object.prototype.hasOwnProperty.call(l,s))for(n=l[s],o=0;oS(a)):o++;x()===e&&D(t||"all")};function Y(e){let t=F(e);e.key&&e.key.toLowerCase()==="capslock"&&(t=E(e.key));const n=c.indexOf(t);if(n>=0&&c.splice(n,1),e.key&&e.key.toLowerCase()==="meta"&&c.splice(0,c.length),(t===93||t===224)&&(t=91),t in u){u[t]=!1;for(const o in h)h[o]===t&&(k[o]=!1)}}const z=(e,...t)=>{if(typeof e=="undefined")Object.keys(l).forEach(n=>{Array.isArray(l[n])&&l[n].forEach(o=>M(o)),delete l[n]}),S(null);else if(Array.isArray(e))e.forEach(n=>{n.key&&M(n)});else if(typeof e=="object")e.key&&M(e);else if(typeof e=="string"){let[n,o]=t;typeof n=="function"&&(o=n,n=""),M({key:e,scope:n,method:o,splitKey:"+"})}},M=({key:e,scope:t,method:n,splitKey:o="+"})=>{$(e).forEach(r=>{const a=r.split(o),y=a.length,i=a[y-1],d=i==="*"?"*":E(i);if(!l[d])return;t||(t=x());const m=y>1?U(h,a):[],w=[];l[d]=l[d].filter(p=>{const f=(n?p.method===n:!0)&&p.scope===t&&R(p.mods,m);return f&&w.push(p.element),!f}),w.forEach(p=>S(p))})};function G(e,t,n,o){if(t.element!==o)return;let s;if(t.scope===n||t.scope==="all"){s=t.mods.length>0;for(const r in u)Object.prototype.hasOwnProperty.call(u,r)&&(!u[r]&&t.mods.indexOf(+r)>-1||u[r]&&t.mods.indexOf(+r)===-1)&&(s=!1);(t.mods.length===0&&!u[16]&&!u[18]&&!u[17]&&!u[91]||s||t.shortcut==="*")&&(t.keys=[],t.keys=t.keys.concat(c),t.method(e,t)===!1&&(e.preventDefault?e.preventDefault():e.returnValue=!1,e.stopPropagation&&e.stopPropagation(),e.cancelBubble&&(e.cancelBubble=!0)))}}function H(e,t){const n=l["*"];let o=F(e);if(e.key&&e.key.toLowerCase()==="capslock"||!(k.filter||I).call(this,e))return;if((o===93||o===224)&&(o=91),c.indexOf(o)===-1&&o!==229&&c.push(o),["metaKey","ctrlKey","altKey","shiftKey"].forEach(i=>{const d=L[i];e[i]&&c.indexOf(d)===-1?c.push(d):!e[i]&&c.indexOf(d)>-1?c.splice(c.indexOf(d),1):i==="metaKey"&&e[i]&&(c=c.filter(m=>m in L||m===o))}),o in u){u[o]=!0;for(const i in h)if(Object.prototype.hasOwnProperty.call(h,i)){const d=L[h[i]];k[i]=e[d]}if(!n)return}for(const i in u)Object.prototype.hasOwnProperty.call(u,i)&&(u[i]=e[L[i]]);e.getModifierState&&!(e.altKey&&!e.ctrlKey)&&e.getModifierState("AltGraph")&&(c.indexOf(17)===-1&&c.push(17),c.indexOf(18)===-1&&c.push(18),u[17]=!0,u[18]=!0);const r=x();if(n)for(let i=0;i1&&(r=U(h,f));let K=f[f.length-1];K=K==="*"?"*":E(K),K in l||(l[K]=[]),l[K].push({keyup:d,keydown:m,scope:a,mods:r,shortcut:s[i],method:o,key:s[i],splitKey:w,element:y})}if(typeof y!="undefined"&&typeof window!="undefined"){if(!g.has(y)){const f=(A=window.event)=>H(A,y),K=(A=window.event)=>{H(A,y),Y(A)};g.set(y,{keydownListener:f,keyupListenr:K,capture:p}),P(y,"keydown",f,p),P(y,"keyup",K,p)}if(!C){const f=()=>{c=[]};C={listener:f,capture:p},P(window,"focus",f,p)}}};function N(e,t="all"){Object.keys(l).forEach(n=>{l[n].filter(s=>s.scope===t&&s.shortcut===e).forEach(s=>{s&&s.method&&s.method({},s)})})}function S(e){const t=Object.values(l).flat();if(t.findIndex(({element:o})=>o===e)<0&&e){const{keydownListener:o,keyupListenr:s,capture:r}=g.get(e)||{};o&&s&&(b(e,"keyup",s,r),b(e,"keydown",o,r),g.delete(e))}if((t.length<=0||g.size<=0)&&(Array.from(g.keys()).forEach(s=>{const{keydownListener:r,keyupListenr:a,capture:y}=g.get(s)||{};r&&a&&(b(s,"keyup",a,y),b(s,"keydown",r,y),g.delete(s))}),g.clear(),Object.keys(l).forEach(s=>delete l[s]),C)){const{listener:s,capture:r}=C;b(window,"focus",s,r),C=null}}const T={getPressedKeyString:q,setScope:D,getScope:x,deleteScope:W,getPressedKeyCodes:Z,getAllKeyCodes:J,isPressed:Q,filter:I,trigger:N,unbind:z,keyMap:_,modifier:h,modifierMap:L};for(const e in T){const t=e;Object.prototype.hasOwnProperty.call(T,t)&&(k[t]=T[t])}if(typeof window!="undefined"){const e=window.hotkeys;k.noConflict=t=>(t&&window.hotkeys===k&&(window.hotkeys=e),k),window.hotkeys=k}return k}(); 9 | //# sourceMappingURL=hotkeys-js.min.js.map 10 | -------------------------------------------------------------------------------- /dist/hotkeys-js.umd.cjs: -------------------------------------------------------------------------------- 1 | (function(O,E){typeof exports=="object"&&typeof module!="undefined"?module.exports=E():typeof define=="function"&&define.amd?define(E):(O=typeof globalThis!="undefined"?globalThis:O||self,O.hotkeys=E())})(this,function(){"use strict";/*! 2 | * hotkeys-js v4.0.0-beta.7 3 | * A simple micro-library for defining and dispatching keyboard shortcuts. It has no dependencies. 4 | * 5 | * @author kenny wong 6 | * @license MIT 7 | * @homepage https://jaywcjlove.github.io/hotkeys-js 8 | */const O=typeof navigator!="undefined"?navigator.userAgent.toLowerCase().indexOf("firefox")>0:!1;function E(e,t,n,o){e.addEventListener?e.addEventListener(t,n,o):e.attachEvent&&e.attachEvent(`on${t}`,n)}function x(e,t,n,o){e&&(e.removeEventListener?e.removeEventListener(t,n,o):e.detachEvent&&e.detachEvent(`on${t}`,n))}function U(e,t){const n=t.slice(0,t.length-1),o=[];for(let s=0;s=0;)t[n-1]+=",",t.splice(n,1),n=t.lastIndexOf("");return t}function R(e,t){const n=e.length>=t.length?e:t,o=e.length>=t.length?t:e;let s=!0;for(let r=0;rL[e.toLowerCase()]||h[e.toLowerCase()]||e.toUpperCase().charCodeAt(0),V=e=>Object.keys(L).find(t=>L[t]===e),X=e=>Object.keys(h).find(t=>h[t]===e),D=e=>{B=e||"all"},A=()=>B||"all",Z=()=>c.slice(0),q=()=>c.map(e=>V(e)||X(e)||String.fromCharCode(e)),J=()=>{const e=[];return Object.keys(l).forEach(t=>{l[t].forEach(({key:n,scope:o,mods:s,shortcut:r})=>{e.push({scope:o,shortcut:r,mods:s,keys:n.split("+").map(a=>_(a))})})}),e},I=e=>{const t=e.target||e.srcElement,{tagName:n}=t;let o=!0;const s=n==="INPUT"&&!["checkbox","radio","range","button","file","reset","submit","color"].includes(t.type);return(t.isContentEditable||(s||n==="TEXTAREA"||n==="SELECT")&&!t.readOnly)&&(o=!1),o},Q=e=>(typeof e=="string"&&(e=_(e)),c.indexOf(e)!==-1),W=(e,t)=>{let n,o;e||(e=A());for(const s in l)if(Object.prototype.hasOwnProperty.call(l,s))for(n=l[s],o=0;oS(a)):o++;A()===e&&D(t||"all")};function Y(e){let t=F(e);e.key&&e.key.toLowerCase()==="capslock"&&(t=_(e.key));const n=c.indexOf(t);if(n>=0&&c.splice(n,1),e.key&&e.key.toLowerCase()==="meta"&&c.splice(0,c.length),(t===93||t===224)&&(t=91),t in u){u[t]=!1;for(const o in h)h[o]===t&&(g[o]=!1)}}const z=(e,...t)=>{if(typeof e=="undefined")Object.keys(l).forEach(n=>{Array.isArray(l[n])&&l[n].forEach(o=>j(o)),delete l[n]}),S(null);else if(Array.isArray(e))e.forEach(n=>{n.key&&j(n)});else if(typeof e=="object")e.key&&j(e);else if(typeof e=="string"){let[n,o]=t;typeof n=="function"&&(o=n,n=""),j({key:e,scope:n,method:o,splitKey:"+"})}},j=({key:e,scope:t,method:n,splitKey:o="+"})=>{$(e).forEach(r=>{const a=r.split(o),d=a.length,i=a[d-1],y=i==="*"?"*":_(i);if(!l[y])return;t||(t=A());const k=d>1?U(h,a):[],w=[];l[y]=l[y].filter(p=>{const f=(n?p.method===n:!0)&&p.scope===t&&R(p.mods,k);return f&&w.push(p.element),!f}),w.forEach(p=>S(p))})};function G(e,t,n,o){if(t.element!==o)return;let s;if(t.scope===n||t.scope==="all"){s=t.mods.length>0;for(const r in u)Object.prototype.hasOwnProperty.call(u,r)&&(!u[r]&&t.mods.indexOf(+r)>-1||u[r]&&t.mods.indexOf(+r)===-1)&&(s=!1);(t.mods.length===0&&!u[16]&&!u[18]&&!u[17]&&!u[91]||s||t.shortcut==="*")&&(t.keys=[],t.keys=t.keys.concat(c),t.method(e,t)===!1&&(e.preventDefault?e.preventDefault():e.returnValue=!1,e.stopPropagation&&e.stopPropagation(),e.cancelBubble&&(e.cancelBubble=!0)))}}function H(e,t){const n=l["*"];let o=F(e);if(e.key&&e.key.toLowerCase()==="capslock"||!(g.filter||I).call(this,e))return;if((o===93||o===224)&&(o=91),c.indexOf(o)===-1&&o!==229&&c.push(o),["metaKey","ctrlKey","altKey","shiftKey"].forEach(i=>{const y=C[i];e[i]&&c.indexOf(y)===-1?c.push(y):!e[i]&&c.indexOf(y)>-1?c.splice(c.indexOf(y),1):i==="metaKey"&&e[i]&&(c=c.filter(k=>k in C||k===o))}),o in u){u[o]=!0;for(const i in h)if(Object.prototype.hasOwnProperty.call(h,i)){const y=C[h[i]];g[i]=e[y]}if(!n)return}for(const i in u)Object.prototype.hasOwnProperty.call(u,i)&&(u[i]=e[C[i]]);e.getModifierState&&!(e.altKey&&!e.ctrlKey)&&e.getModifierState("AltGraph")&&(c.indexOf(17)===-1&&c.push(17),c.indexOf(18)===-1&&c.push(18),u[17]=!0,u[18]=!0);const r=A();if(n)for(let i=0;i1&&(r=U(h,f));let K=f[f.length-1];K=K==="*"?"*":_(K),K in l||(l[K]=[]),l[K].push({keyup:y,keydown:k,scope:a,mods:r,shortcut:s[i],method:o,key:s[i],splitKey:w,element:d})}if(typeof d!="undefined"&&typeof window!="undefined"){if(!m.has(d)){const f=(P=window.event)=>H(P,d),K=(P=window.event)=>{H(P,d),Y(P)};m.set(d,{keydownListener:f,keyupListenr:K,capture:p}),E(d,"keydown",f,p),E(d,"keyup",K,p)}if(!M){const f=()=>{c=[]};M={listener:f,capture:p},E(window,"focus",f,p)}}};function N(e,t="all"){Object.keys(l).forEach(n=>{l[n].filter(s=>s.scope===t&&s.shortcut===e).forEach(s=>{s&&s.method&&s.method({},s)})})}function S(e){const t=Object.values(l).flat();if(t.findIndex(({element:o})=>o===e)<0&&e){const{keydownListener:o,keyupListenr:s,capture:r}=m.get(e)||{};o&&s&&(x(e,"keyup",s,r),x(e,"keydown",o,r),m.delete(e))}if((t.length<=0||m.size<=0)&&(Array.from(m.keys()).forEach(s=>{const{keydownListener:r,keyupListenr:a,capture:d}=m.get(s)||{};r&&a&&(x(s,"keyup",a,d),x(s,"keydown",r,d),m.delete(s))}),m.clear(),Object.keys(l).forEach(s=>delete l[s]),M)){const{listener:s,capture:r}=M;x(window,"focus",s,r),M=null}}const T={getPressedKeyString:q,setScope:D,getScope:A,deleteScope:W,getPressedKeyCodes:Z,getAllKeyCodes:J,isPressed:Q,filter:I,trigger:N,unbind:z,keyMap:L,modifier:h,modifierMap:C};for(const e in T){const t=e;Object.prototype.hasOwnProperty.call(T,t)&&(g[t]=T[t])}if(typeof window!="undefined"){const e=window.hotkeys;g.noConflict=t=>(t&&window.hotkeys===g&&(window.hotkeys=e),g),window.hotkeys=g}return g});typeof module=="object"&&module.exports&&(module.exports.default=module.exports); 9 | //# sourceMappingURL=hotkeys-js.umd.cjs.map 10 | -------------------------------------------------------------------------------- /website/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import GithubCorner from '@uiw/react-github-corners'; 3 | import { Github } from '@uiw/react-shields'; 4 | import MarkdownPreview from '@uiw/react-markdown-preview'; 5 | import KeyBoard from '@uiw/react-mac-keyboard'; 6 | import '@wcj/dark-mode'; 7 | import Footer from './components/Footer'; 8 | import styles from './styles/index.module.css'; 9 | import DocumentStr from '../README.md?raw'; 10 | import DocumentChinese from '../README-zh.md?raw'; 11 | import hotkeys from '../src'; 12 | import pkg from '../package.json'; 13 | 14 | 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.`; 15 | const chineseDescription = `一个强大的Javascript库,用于捕获键盘输入和键组合。它没有依赖。尝试按下你的键盘,下面的按钮将会高亮显示。`; 16 | 17 | export default function AppRoot() { 18 | const [keyCode, setKeyCode] = useState([]); 19 | const [keyStr, setKeyStr] = useState<(string | number)[]>([]); 20 | // 检查URL参数来决定初始语言 21 | const getInitialLanguage = () => { 22 | const urlParams = new URLSearchParams(window.location.search); 23 | const lang = urlParams.get('lang'); 24 | return lang === 'zh' || lang === 'zh-CN'; 25 | }; 26 | const [isChineseDoc, setIsChineseDoc] = useState(getInitialLanguage()); 27 | const [documentContent, setDocumentContent] = useState( 28 | getInitialLanguage() ? DocumentChinese : DocumentStr 29 | ); 30 | 31 | useEffect(() => { 32 | document.addEventListener('keyup', onKeyUpEvent); 33 | 34 | function pkeys(keys: number[], key: number): number[] { 35 | if (keys.indexOf(key) === -1) keys.push(key); 36 | return keys; 37 | } 38 | function pkeysStr( 39 | keysStr: (string | number)[], 40 | key: string | number 41 | ): (string | number)[] { 42 | if (keysStr.indexOf(key) === -1) keysStr.push(key); 43 | return keysStr; 44 | } 45 | 46 | hotkeys('*', (evn) => { 47 | evn.preventDefault(); 48 | const keys: number[] = []; 49 | const kStr: (string | number)[] = []; 50 | if (hotkeys.shift) { 51 | pkeys(keys, 16); 52 | pkeysStr(kStr, 'shift'); 53 | } 54 | if (hotkeys.ctrl) { 55 | pkeys(keys, 17); 56 | pkeysStr(kStr, 'ctrl'); 57 | } 58 | if (hotkeys.alt) { 59 | pkeys(keys, 18); 60 | pkeysStr(kStr, 'alt'); 61 | } 62 | if (hotkeys.control) { 63 | pkeys(keys, 17); 64 | pkeysStr(kStr, 'control'); 65 | } 66 | if (hotkeys.command) { 67 | pkeys(keys, 91); 68 | pkeysStr(kStr, 'command'); 69 | } 70 | kStr.push(evn.keyCode); 71 | if (keys.indexOf(evn.keyCode) === -1) keys.push(evn.keyCode); 72 | 73 | setKeyCode(keys); 74 | setKeyStr(kStr); 75 | }); 76 | 77 | return () => { 78 | document.removeEventListener('keyup', onKeyUpEvent); 79 | }; 80 | }, []); 81 | 82 | const openVersionWebsite = (e: React.ChangeEvent) => { 83 | if (e.target && e.target.value) { 84 | window.location.href = e.target.value; 85 | } 86 | }; 87 | 88 | const onKeyUpEvent = () => { 89 | setKeyCode([]); 90 | setKeyStr([]); 91 | }; 92 | 93 | const toggleLanguage = () => { 94 | const newIsChineseDoc = !isChineseDoc; 95 | setIsChineseDoc(newIsChineseDoc); 96 | setDocumentContent(newIsChineseDoc ? DocumentChinese : DocumentStr); 97 | 98 | // 更新URL参数 99 | const url = new URL(window.location.href); 100 | if (newIsChineseDoc) { 101 | url.searchParams.set('lang', 'zh'); 102 | } else { 103 | url.searchParams.delete('lang'); 104 | } 105 | window.history.pushState({}, '', url.toString()); 106 | }; 107 | 108 | const onKeyBoardMouseDown = (item: { keycode: number }) => { 109 | if (item.keycode > -1) { 110 | setKeyStr([item.keycode]); 111 | } 112 | }; 113 | 114 | const onKeyBoardMouseUp = () => setKeyStr([]); 115 | 116 | return ( 117 |
118 |
119 | 136 | 137 |
138 | {keyStr.length > -1 && ( 139 |
140 | {keyStr.map((item) => ( 141 | {item} 142 | ))} 143 |
144 | )} 145 | 149 |
150 | HotKeys.js 151 |
152 | 153 | 154 |   155 | 156 | 157 |   158 | 159 | 160 |   161 | 162 | 163 |   164 | 170 |
171 |
172 | {isChineseDoc ? chineseDescription : englishDescription} 173 |
174 |
175 | onKeyBoardMouseDown(item)} 178 | onMouseUp={onKeyBoardMouseUp} 179 | keyCode={keyCode} 180 | /> 181 | 185 |
190 | 191 | 195 | 199 | 203 | 207 | 208 |
209 |
210 | ); 211 | } 212 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | declare const _default: HotkeysInterface; 2 | export default _default; 3 | 4 | declare type DeleteScope = (scope?: string, newScope?: string) => void; 5 | 6 | declare type Filter = (event: KeyboardEvent) => boolean; 7 | 8 | declare type GetAllKeyCodes = () => KeyCodeInfo[]; 9 | 10 | declare type GetPressedKeyCodes = () => number[]; 11 | 12 | declare type GetPressedKeyString = () => string[]; 13 | 14 | declare type GetScope = () => string; 15 | 16 | declare interface HotkeysAPI { 17 | /** 18 | * Use the `hotkeys.setScope` method to set scope. There can only be one active scope besides 'all'. By default 'all' is always active. 19 | * 20 | * ```js 21 | * // Define shortcuts with a scope 22 | * hotkeys('ctrl+o, ctrl+alt+enter', 'issues', function() { 23 | * console.log('do something'); 24 | * }); 25 | * hotkeys('o, enter', 'files', function() { 26 | * console.log('do something else'); 27 | * }); 28 | * 29 | * // Set the scope (only 'all' and 'issues' shortcuts will be honored) 30 | * hotkeys.setScope('issues'); // default scope is 'all' 31 | * ``` 32 | */ 33 | setScope: SetScope; 34 | /** 35 | * Use the `hotkeys.getScope` method to get scope. 36 | * 37 | * ```js 38 | * hotkeys.getScope(); 39 | * ``` 40 | */ 41 | getScope: GetScope; 42 | /** 43 | * Use the `hotkeys.deleteScope` method to delete a scope. This will also remove all associated hotkeys with it. 44 | * 45 | * ```js 46 | * hotkeys.deleteScope('issues'); 47 | * ``` 48 | * You can use second argument, if need set new scope after deleting. 49 | * 50 | * ```js 51 | * hotkeys.deleteScope('issues', 'newScopeName'); 52 | * ``` 53 | */ 54 | deleteScope: DeleteScope; 55 | /** 56 | * Returns an array of key codes currently pressed. 57 | * 58 | * ```js 59 | * hotkeys('command+ctrl+shift+a,f', function() { 60 | * console.log(hotkeys.getPressedKeyCodes()); //=> [17, 65] or [70] 61 | * }) 62 | * ``` 63 | */ 64 | getPressedKeyCodes: GetPressedKeyCodes; 65 | /** 66 | * Returns an array of key codes currently pressed. 67 | * 68 | * ```js 69 | * hotkeys('command+ctrl+shift+a,f', function() { 70 | * console.log(hotkeys.getPressedKeyString()); //=> ['⌘', '⌃', '⇧', 'A', 'F'] 71 | * }) 72 | * ``` 73 | */ 74 | getPressedKeyString: GetPressedKeyString; 75 | /** 76 | * Get a list of all registration codes. 77 | * 78 | * ```js 79 | * hotkeys('command+ctrl+shift+a,f', function() { 80 | * console.log(hotkeys.getAllKeyCodes()); 81 | * // [ 82 | * // { scope: 'all', shortcut: 'command+ctrl+shift+a', mods: [91, 17, 16], keys: [91, 17, 16, 65] }, 83 | * // { scope: 'all', shortcut: 'f', mods: [], keys: [42] } 84 | * // ] 85 | * }) 86 | * ``` 87 | * 88 | */ 89 | getAllKeyCodes: GetAllKeyCodes; 90 | isPressed: IsPressed; 91 | /** 92 | * By default hotkeys are not enabled for `INPUT` `SELECT` `TEXTAREA` elements. 93 | * `Hotkeys.filter` to return to the `true` shortcut keys set to play a role, 94 | * `false` shortcut keys set up failure. 95 | * 96 | * ```js 97 | * hotkeys.filter = function(event){ 98 | * return true; 99 | * } 100 | * //How to add the filter to edit labels.
101 | * //"contentEditable" Older browsers that do not support drops 102 | * hotkeys.filter = function(event) { 103 | * var target = event.target || event.srcElement; 104 | * var tagName = target.tagName; 105 | * return !(target.isContentEditable || tagName == 'INPUT' || tagName == 'SELECT' || tagName == 'TEXTAREA'); 106 | * } 107 | * 108 | * hotkeys.filter = function(event){ 109 | * var tagName = (event.target || event.srcElement).tagName; 110 | * hotkeys.setScope(/^(INPUT|TEXTAREA|SELECT)$/.test(tagName) ? 'input' : 'other'); 111 | * return true; 112 | * } 113 | * ``` 114 | */ 115 | filter: Filter; 116 | /** 117 | * trigger shortcut key event 118 | * 119 | * ```js 120 | * hotkeys.trigger('ctrl+o'); 121 | * hotkeys.trigger('ctrl+o', 'scope2'); 122 | * ``` 123 | */ 124 | trigger: Trigger; 125 | /** 126 | * Unbinds a shortcut key event. 127 | * 128 | * ```js 129 | * hotkeys.unbind('ctrl+o'); 130 | * hotkeys.unbind('ctrl+o', 'scope1'); 131 | * hotkeys.unbind('ctrl+o', 'scope1', method); 132 | * hotkeys.unbind('ctrl+o', method); 133 | * ``` 134 | */ 135 | unbind: Unbind; 136 | /** 137 | * Relinquish HotKeys’s control of the `hotkeys` variable. 138 | * 139 | * ```js 140 | * var k = hotkeys.noConflict(); 141 | * k('a', function() { 142 | * console.log("do something") 143 | * }); 144 | * 145 | * hotkeys() 146 | * // -->Uncaught TypeError: hotkeys is not a function(anonymous function) 147 | * // @ VM2170:2InjectedScript._evaluateOn 148 | * // @ VM2165:883InjectedScript._evaluateAndWrap 149 | * // @ VM2165:816InjectedScript.evaluate @ VM2165:682 150 | * ``` 151 | */ 152 | noConflict: NoConflict; 153 | keyMap: Record; 154 | modifier: Record; 155 | modifierMap: Record; 156 | } 157 | 158 | export declare interface HotkeysEvent { 159 | keyup: boolean; 160 | keydown: boolean; 161 | scope: string; 162 | mods: number[]; 163 | shortcut: string; 164 | method: KeyHandler; 165 | key: string; 166 | splitKey: string; 167 | element: HTMLElement | Document; 168 | keys?: number[]; 169 | } 170 | 171 | declare interface HotkeysInterface extends HotkeysAPI { 172 | (key: string, method: KeyHandler): void; 173 | (key: string, scope: string, method: KeyHandler): void; 174 | (key: string, option: HotkeysOptions, method: KeyHandler): void; 175 | shift?: boolean; 176 | ctrl?: boolean; 177 | alt?: boolean; 178 | option?: boolean; 179 | control?: boolean; 180 | cmd?: boolean; 181 | command?: boolean; 182 | } 183 | 184 | declare interface HotkeysOptions { 185 | scope?: string; 186 | element?: HTMLElement | Document; 187 | keyup?: boolean; 188 | keydown?: boolean; 189 | capture?: boolean; 190 | splitKey?: string; 191 | single?: boolean; 192 | } 193 | 194 | declare interface IsPressed { 195 | /** For example, `hotkeys.isPressed(77)` is true if the `M` key is currently pressed. */ 196 | (keyCode: number): boolean; 197 | /** For example, `hotkeys.isPressed('m')` is true if the `M` key is currently pressed. */ 198 | (keyCode: string): boolean; 199 | } 200 | 201 | declare interface KeyCodeInfo { 202 | scope: string; 203 | shortcut: string; 204 | mods: number[]; 205 | keys: number[]; 206 | } 207 | 208 | export declare interface KeyHandler { 209 | (keyboardEvent: KeyboardEvent, hotkeysEvent: HotkeysEvent): void | boolean; 210 | } 211 | 212 | declare type NoConflict = (deep?: boolean) => HotkeysInterface; 213 | 214 | declare type SetScope = (scope: string) => void; 215 | 216 | declare type Trigger = (shortcut: string, scope?: string) => void; 217 | 218 | declare interface Unbind { 219 | (key?: string): void; 220 | (keysInfo: UnbindInfo): void; 221 | (keysInfo: UnbindInfo[]): void; 222 | (key: string, scopeName: string): void; 223 | (key: string, scopeName: string, method: KeyHandler): void; 224 | (key: string, method: KeyHandler): void; 225 | } 226 | 227 | declare interface UnbindInfo { 228 | key: string; 229 | scope?: string; 230 | method?: KeyHandler; 231 | splitKey?: string; 232 | } 233 | 234 | export { } 235 | -------------------------------------------------------------------------------- /dist/hotkeys-js.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * hotkeys-js v4.0.0-beta.7 3 | * A simple micro-library for defining and dispatching keyboard shortcuts. It has no dependencies. 4 | * 5 | * @author kenny wong 6 | * @license MIT 7 | * @homepage https://jaywcjlove.github.io/hotkeys-js 8 | */ 9 | const j = typeof navigator != "undefined" ? navigator.userAgent.toLowerCase().indexOf("firefox") > 0 : !1; 10 | function P(e, t, n, o) { 11 | e.addEventListener ? e.addEventListener(t, n, o) : e.attachEvent && e.attachEvent(`on${t}`, n); 12 | } 13 | function b(e, t, n, o) { 14 | e && (e.removeEventListener ? e.removeEventListener(t, n, o) : e.detachEvent && e.detachEvent(`on${t}`, n)); 15 | } 16 | function F(e, t) { 17 | const n = t.slice(0, t.length - 1), o = []; 18 | for (let s = 0; s < n.length; s++) 19 | o.push(e[n[s].toLowerCase()]); 20 | return o; 21 | } 22 | function B(e) { 23 | typeof e != "string" && (e = ""), e = e.replace(/\s/g, ""); 24 | const t = e.split(","); 25 | let n = t.lastIndexOf(""); 26 | for (; n >= 0; ) 27 | t[n - 1] += ",", t.splice(n, 1), n = t.lastIndexOf(""); 28 | return t; 29 | } 30 | function R(e, t) { 31 | const n = e.length >= t.length ? e : t, o = e.length >= t.length ? t : e; 32 | let s = !0; 33 | for (let r = 0; r < n.length; r++) 34 | o.indexOf(n[r]) === -1 && (s = !1); 35 | return s; 36 | } 37 | function D(e) { 38 | let t = e.keyCode || e.which || e.charCode; 39 | return e.code && /^Key[A-Z]$/.test(e.code) && (t = e.code.charCodeAt(3)), t; 40 | } 41 | const x = { 42 | backspace: 8, 43 | "⌫": 8, 44 | tab: 9, 45 | clear: 12, 46 | enter: 13, 47 | "↩": 13, 48 | return: 13, 49 | esc: 27, 50 | escape: 27, 51 | space: 32, 52 | left: 37, 53 | up: 38, 54 | right: 39, 55 | down: 40, 56 | /// https://w3c.github.io/uievents/#events-keyboard-key-location 57 | arrowup: 38, 58 | arrowdown: 40, 59 | arrowleft: 37, 60 | arrowright: 39, 61 | del: 46, 62 | delete: 46, 63 | ins: 45, 64 | insert: 45, 65 | home: 36, 66 | end: 35, 67 | pageup: 33, 68 | pagedown: 34, 69 | capslock: 20, 70 | num_0: 96, 71 | num_1: 97, 72 | num_2: 98, 73 | num_3: 99, 74 | num_4: 100, 75 | num_5: 101, 76 | num_6: 102, 77 | num_7: 103, 78 | num_8: 104, 79 | num_9: 105, 80 | num_multiply: 106, 81 | num_add: 107, 82 | num_enter: 108, 83 | num_subtract: 109, 84 | num_decimal: 110, 85 | num_divide: 111, 86 | "⇪": 20, 87 | ",": 188, 88 | ".": 190, 89 | "/": 191, 90 | "`": 192, 91 | "-": j ? 173 : 189, 92 | "=": j ? 61 : 187, 93 | ";": j ? 59 : 186, 94 | "'": 222, 95 | "{": 219, 96 | "}": 221, 97 | "[": 219, 98 | "]": 221, 99 | "\\": 220 100 | }, h = { 101 | // shiftKey 102 | "⇧": 16, 103 | shift: 16, 104 | // altKey 105 | "⌥": 18, 106 | alt: 18, 107 | option: 18, 108 | // ctrlKey 109 | "⌃": 17, 110 | ctrl: 17, 111 | control: 17, 112 | // metaKey 113 | "⌘": 91, 114 | cmd: 91, 115 | meta: 91, 116 | command: 91 117 | }, _ = { 118 | 16: "shiftKey", 119 | 18: "altKey", 120 | 17: "ctrlKey", 121 | 91: "metaKey", 122 | shiftKey: 16, 123 | ctrlKey: 17, 124 | altKey: 18, 125 | metaKey: 91 126 | }, y = { 127 | 16: !1, 128 | 18: !1, 129 | 17: !1, 130 | 91: !1 131 | }, l = {}; 132 | for (let e = 1; e < 20; e++) 133 | x[`f${e}`] = 111 + e; 134 | let c = [], L = null, I = "all"; 135 | const w = /* @__PURE__ */ new Map(), E = (e) => x[e.toLowerCase()] || h[e.toLowerCase()] || e.toUpperCase().charCodeAt(0), V = (e) => Object.keys(x).find((t) => x[t] === e), X = (e) => Object.keys(h).find((t) => h[t] === e), z = (e) => { 136 | I = e || "all"; 137 | }, C = () => I || "all", Z = () => c.slice(0), q = () => c.map( 138 | (e) => V(e) || X(e) || String.fromCharCode(e) 139 | ), J = () => { 140 | const e = []; 141 | return Object.keys(l).forEach((t) => { 142 | l[t].forEach(({ key: n, scope: o, mods: s, shortcut: r }) => { 143 | e.push({ 144 | scope: o, 145 | shortcut: r, 146 | mods: s, 147 | keys: n.split("+").map((a) => E(a)) 148 | }); 149 | }); 150 | }), e; 151 | }, G = (e) => { 152 | const t = e.target || e.srcElement, { tagName: n } = t; 153 | let o = !0; 154 | const s = n === "INPUT" && ![ 155 | "checkbox", 156 | "radio", 157 | "range", 158 | "button", 159 | "file", 160 | "reset", 161 | "submit", 162 | "color" 163 | ].includes(t.type); 164 | return (t.isContentEditable || (s || n === "TEXTAREA" || n === "SELECT") && !t.readOnly) && (o = !1), o; 165 | }, Q = (e) => (typeof e == "string" && (e = E(e)), c.indexOf(e) !== -1), W = (e, t) => { 166 | let n, o; 167 | e || (e = C()); 168 | for (const s in l) 169 | if (Object.prototype.hasOwnProperty.call(l, s)) 170 | for (n = l[s], o = 0; o < n.length; ) 171 | n[o].scope === e ? n.splice(o, 1).forEach(({ element: a }) => T(a)) : o++; 172 | C() === e && z(t || "all"); 173 | }; 174 | function Y(e) { 175 | let t = D(e); 176 | e.key && e.key.toLowerCase() === "capslock" && (t = E(e.key)); 177 | const n = c.indexOf(t); 178 | if (n >= 0 && c.splice(n, 1), e.key && e.key.toLowerCase() === "meta" && c.splice(0, c.length), (t === 93 || t === 224) && (t = 91), t in y) { 179 | y[t] = !1; 180 | for (const o in h) 181 | h[o] === t && (K[o] = !1); 182 | } 183 | } 184 | const H = (e, ...t) => { 185 | if (typeof e == "undefined") 186 | Object.keys(l).forEach((n) => { 187 | Array.isArray(l[n]) && l[n].forEach((o) => A(o)), delete l[n]; 188 | }), T(null); 189 | else if (Array.isArray(e)) 190 | e.forEach((n) => { 191 | n.key && A(n); 192 | }); 193 | else if (typeof e == "object") 194 | e.key && A(e); 195 | else if (typeof e == "string") { 196 | let [n, o] = t; 197 | typeof n == "function" && (o = n, n = ""), A({ 198 | key: e, 199 | scope: n, 200 | method: o, 201 | splitKey: "+" 202 | }); 203 | } 204 | }, A = ({ 205 | key: e, 206 | scope: t, 207 | method: n, 208 | splitKey: o = "+" 209 | }) => { 210 | B(e).forEach((r) => { 211 | const a = r.split(o), u = a.length, i = a[u - 1], d = i === "*" ? "*" : E(i); 212 | if (!l[d]) return; 213 | t || (t = C()); 214 | const g = u > 1 ? F(h, a) : [], k = []; 215 | l[d] = l[d].filter((p) => { 216 | const f = (n ? p.method === n : !0) && p.scope === t && R(p.mods, g); 217 | return f && k.push(p.element), !f; 218 | }), k.forEach((p) => T(p)); 219 | }); 220 | }; 221 | function U(e, t, n, o) { 222 | if (t.element !== o) 223 | return; 224 | let s; 225 | if (t.scope === n || t.scope === "all") { 226 | s = t.mods.length > 0; 227 | for (const r in y) 228 | Object.prototype.hasOwnProperty.call(y, r) && (!y[r] && t.mods.indexOf(+r) > -1 || y[r] && t.mods.indexOf(+r) === -1) && (s = !1); 229 | (t.mods.length === 0 && !y[16] && !y[18] && !y[17] && !y[91] || s || t.shortcut === "*") && (t.keys = [], t.keys = t.keys.concat(c), t.method(e, t) === !1 && (e.preventDefault ? e.preventDefault() : e.returnValue = !1, e.stopPropagation && e.stopPropagation(), e.cancelBubble && (e.cancelBubble = !0))); 230 | } 231 | } 232 | function $(e, t) { 233 | const n = l["*"]; 234 | let o = D(e); 235 | if (e.key && e.key.toLowerCase() === "capslock" || !(K.filter || G).call(this, e)) return; 236 | if ((o === 93 || o === 224) && (o = 91), c.indexOf(o) === -1 && o !== 229 && c.push(o), ["metaKey", "ctrlKey", "altKey", "shiftKey"].forEach((i) => { 237 | const d = _[i]; 238 | e[i] && c.indexOf(d) === -1 ? c.push(d) : !e[i] && c.indexOf(d) > -1 ? c.splice(c.indexOf(d), 1) : i === "metaKey" && e[i] && (c = c.filter((g) => g in _ || g === o)); 239 | }), o in y) { 240 | y[o] = !0; 241 | for (const i in h) 242 | if (Object.prototype.hasOwnProperty.call(h, i)) { 243 | const d = _[h[i]]; 244 | K[i] = e[d]; 245 | } 246 | if (!n) return; 247 | } 248 | for (const i in y) 249 | Object.prototype.hasOwnProperty.call(y, i) && (y[i] = e[_[i]]); 250 | e.getModifierState && !(e.altKey && !e.ctrlKey) && e.getModifierState("AltGraph") && (c.indexOf(17) === -1 && c.push(17), c.indexOf(18) === -1 && c.push(18), y[17] = !0, y[18] = !0); 251 | const r = C(); 252 | if (n) 253 | for (let i = 0; i < n.length; i++) 254 | n[i].scope === r && (e.type === "keydown" && n[i].keydown || e.type === "keyup" && n[i].keyup) && U(e, n[i], r, t); 255 | if (!(o in l)) return; 256 | const a = l[o], u = a.length; 257 | for (let i = 0; i < u; i++) 258 | if ((e.type === "keydown" && a[i].keydown || e.type === "keyup" && a[i].keyup) && a[i].key) { 259 | const d = a[i], { splitKey: g } = d, k = d.key.split(g), p = []; 260 | for (let O = 0; O < k.length; O++) 261 | p.push(E(k[O])); 262 | p.sort().join("") === c.sort().join("") && U(e, d, r, t); 263 | } 264 | } 265 | const K = function(t, n, o) { 266 | c = []; 267 | const s = B(t); 268 | let r = [], a = "all", u = document, i = 0, d = !1, g = !0, k = "+", p = !1, O = !1; 269 | if (o === void 0 && typeof n == "function" && (o = n), Object.prototype.toString.call(n) === "[object Object]") { 270 | const f = n; 271 | f.scope && (a = f.scope), f.element && (u = f.element), f.keyup && (d = f.keyup), f.keydown !== void 0 && (g = f.keydown), f.capture !== void 0 && (p = f.capture), typeof f.splitKey == "string" && (k = f.splitKey), f.single === !0 && (O = !0); 272 | } 273 | for (typeof n == "string" && (a = n), O && H(t, a); i < s.length; i++) { 274 | const f = s[i].split(k); 275 | r = [], f.length > 1 && (r = F(h, f)); 276 | let m = f[f.length - 1]; 277 | m = m === "*" ? "*" : E(m), m in l || (l[m] = []), l[m].push({ 278 | keyup: d, 279 | keydown: g, 280 | scope: a, 281 | mods: r, 282 | shortcut: s[i], 283 | method: o, 284 | key: s[i], 285 | splitKey: k, 286 | element: u 287 | }); 288 | } 289 | if (typeof u != "undefined" && typeof window != "undefined") { 290 | if (!w.has(u)) { 291 | const f = (M = window.event) => $(M, u), m = (M = window.event) => { 292 | $(M, u), Y(M); 293 | }; 294 | w.set(u, { keydownListener: f, keyupListenr: m, capture: p }), P(u, "keydown", f, p), P(u, "keyup", m, p); 295 | } 296 | if (!L) { 297 | const f = () => { 298 | c = []; 299 | }; 300 | L = { listener: f, capture: p }, P(window, "focus", f, p); 301 | } 302 | } 303 | }; 304 | function N(e, t = "all") { 305 | Object.keys(l).forEach((n) => { 306 | l[n].filter( 307 | (s) => s.scope === t && s.shortcut === e 308 | ).forEach((s) => { 309 | s && s.method && s.method({}, s); 310 | }); 311 | }); 312 | } 313 | function T(e) { 314 | const t = Object.values(l).flat(); 315 | if (t.findIndex(({ element: o }) => o === e) < 0 && e) { 316 | const { keydownListener: o, keyupListenr: s, capture: r } = w.get(e) || {}; 317 | o && s && (b(e, "keyup", s, r), b(e, "keydown", o, r), w.delete(e)); 318 | } 319 | if ((t.length <= 0 || w.size <= 0) && (Array.from(w.keys()).forEach((s) => { 320 | const { keydownListener: r, keyupListenr: a, capture: u } = w.get(s) || {}; 321 | r && a && (b(s, "keyup", a, u), b(s, "keydown", r, u), w.delete(s)); 322 | }), w.clear(), Object.keys(l).forEach((s) => delete l[s]), L)) { 323 | const { listener: s, capture: r } = L; 324 | b(window, "focus", s, r), L = null; 325 | } 326 | } 327 | const S = { 328 | getPressedKeyString: q, 329 | setScope: z, 330 | getScope: C, 331 | deleteScope: W, 332 | getPressedKeyCodes: Z, 333 | getAllKeyCodes: J, 334 | isPressed: Q, 335 | filter: G, 336 | trigger: N, 337 | unbind: H, 338 | keyMap: x, 339 | modifier: h, 340 | modifierMap: _ 341 | }; 342 | for (const e in S) { 343 | const t = e; 344 | Object.prototype.hasOwnProperty.call(S, t) && (K[t] = S[t]); 345 | } 346 | if (typeof window != "undefined") { 347 | const e = window.hotkeys; 348 | K.noConflict = (t) => (t && window.hotkeys === K && (window.hotkeys = e), K), window.hotkeys = K; 349 | } 350 | export { 351 | K as default 352 | }; 353 | //# sourceMappingURL=hotkeys-js.js.map 354 | -------------------------------------------------------------------------------- /test/run.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-require-imports */ 2 | const puppeteer = require('puppeteer'); 3 | const path = require('path'); 4 | 5 | let browser; 6 | let page; 7 | 8 | beforeAll(async () => { 9 | browser = await puppeteer.launch({ args: ['--no-sandbox'] }); 10 | page = await browser.newPage(); 11 | 12 | // Load the test HTML file 13 | const htmlPath = path.resolve(__dirname, './index.html'); 14 | await page.goto(`file://${htmlPath}`, { waitUntil: 'networkidle2' }); 15 | }, 1000 * 120); 16 | 17 | describe('\n Hotkeys.js Test Case\n', () => { 18 | test('HTML loader', async () => { 19 | const title = await page.title(); 20 | expect(title).toBe('hotkeys.js'); 21 | }, 10000); 22 | 23 | test('Test HTML load', async () => { 24 | const text = await page.$eval('#root', (el) => el.textContent); 25 | expect(text).toBe('hotkeys'); 26 | 27 | const hasHotkeys = await page.evaluate(() => { 28 | return typeof window.hotkeys !== 'undefined'; 29 | }); 30 | expect(hasHotkeys).toBeTruthy(); 31 | }); 32 | 33 | test('HotKeys getPressedKeyCodes Test Case', async () => { 34 | const result = await page.evaluate(async () => { 35 | return new Promise((resolve) => { 36 | let isExecuteFunction = false; 37 | window.hotkeys('command+ctrl+shift+a', (e) => { 38 | isExecuteFunction = true; 39 | const pressedKeys = window.hotkeys.getPressedKeyCodes(); 40 | resolve({ 41 | isExecuteFunction, 42 | metaKey: e.metaKey, 43 | ctrlKey: e.ctrlKey, 44 | shiftKey: e.shiftKey, 45 | pressedKeys, 46 | }); 47 | }); 48 | 49 | // Trigger the event 50 | const event = new KeyboardEvent('keydown', { 51 | keyCode: 65, 52 | which: 65, 53 | metaKey: true, 54 | ctrlKey: true, 55 | shiftKey: true, 56 | bubbles: true, 57 | cancelable: true, 58 | }); 59 | document.body.dispatchEvent(event); 60 | 61 | setTimeout(() => { 62 | window.hotkeys.unbind('command+ctrl+shift+a'); 63 | resolve({ isExecuteFunction, pressedKeys: [] }); 64 | }, 100); 65 | }); 66 | }); 67 | 68 | expect(result.isExecuteFunction).toBeTruthy(); 69 | expect(result.metaKey).toBeTruthy(); 70 | expect(result.ctrlKey).toBeTruthy(); 71 | expect(result.shiftKey).toBeTruthy(); 72 | expect(result.pressedKeys).toEqual( 73 | expect.arrayContaining([16, 17, 65, 91]) 74 | ); 75 | }); 76 | 77 | test('HotKeys getPressedKeyString Test Case', async () => { 78 | const result = await page.evaluate(async () => { 79 | return new Promise((resolve) => { 80 | let isExecuteFunction = false; 81 | window.hotkeys('command+ctrl+shift+a', (e) => { 82 | isExecuteFunction = true; 83 | const pressedKeys = window.hotkeys.getPressedKeyString(); 84 | resolve({ 85 | isExecuteFunction, 86 | metaKey: e.metaKey, 87 | ctrlKey: e.ctrlKey, 88 | shiftKey: e.shiftKey, 89 | pressedKeys, 90 | }); 91 | }); 92 | 93 | const event = new KeyboardEvent('keydown', { 94 | keyCode: 65, 95 | which: 65, 96 | metaKey: true, 97 | ctrlKey: true, 98 | shiftKey: true, 99 | bubbles: true, 100 | cancelable: true, 101 | }); 102 | document.body.dispatchEvent(event); 103 | 104 | setTimeout(() => { 105 | window.hotkeys.unbind('command+ctrl+shift+a'); 106 | resolve({ isExecuteFunction: false, pressedKeys: [] }); 107 | }, 100); 108 | }); 109 | }); 110 | 111 | expect(result.isExecuteFunction).toBeTruthy(); 112 | expect(result.pressedKeys).toEqual( 113 | expect.arrayContaining(['⇧', '⌃', 'A', '⌘']) 114 | ); 115 | }); 116 | 117 | test('HotKeys unbind Test Case', async () => { 118 | const result = await page.evaluate(async () => { 119 | return new Promise((resolve) => { 120 | let isExecuteFunction = false; 121 | window.hotkeys('enter', () => { 122 | isExecuteFunction = true; 123 | }); 124 | 125 | const event = new KeyboardEvent('keydown', { 126 | keyCode: 13, 127 | which: 13, 128 | bubbles: true, 129 | cancelable: true, 130 | }); 131 | document.body.dispatchEvent(event); 132 | 133 | setTimeout(() => { 134 | const unbindResult = window.hotkeys.unbind('enter'); 135 | resolve({ 136 | isExecuteFunction, 137 | unbindResult, 138 | keyCode: 13, 139 | }); 140 | }, 100); 141 | }); 142 | }); 143 | 144 | expect(result.isExecuteFunction).toBeTruthy(); 145 | expect(result.unbindResult).toBeUndefined(); 146 | }); 147 | 148 | test('getAllKeyCodes Test Case', async () => { 149 | const result = await page.evaluate(() => { 150 | return window.hotkeys.getAllKeyCodes(); 151 | }); 152 | 153 | expect(Array.isArray(result)).toBeTruthy(); 154 | }); 155 | 156 | test('HotKeys Special keys Test Case', async () => { 157 | const result = await page.evaluate(async () => { 158 | const results = {}; 159 | 160 | // Test enter 161 | await new Promise((resolve) => { 162 | window.hotkeys('enter', (e) => { 163 | results.enter = e.keyCode === 13; 164 | }); 165 | 166 | const event = new KeyboardEvent('keydown', { 167 | keyCode: 13, 168 | which: 13, 169 | bubbles: true, 170 | cancelable: true, 171 | }); 172 | document.body.dispatchEvent(event); 173 | 174 | setTimeout(() => { 175 | window.hotkeys.unbind('enter'); 176 | resolve(); 177 | }, 50); 178 | }); 179 | 180 | // Test space 181 | await new Promise((resolve) => { 182 | window.hotkeys('space', (e) => { 183 | results.space = e.keyCode === 32; 184 | }); 185 | 186 | const event = new KeyboardEvent('keydown', { 187 | keyCode: 32, 188 | which: 32, 189 | bubbles: true, 190 | cancelable: true, 191 | }); 192 | document.body.dispatchEvent(event); 193 | 194 | setTimeout(() => { 195 | window.hotkeys.unbind('space'); 196 | resolve(); 197 | }, 50); 198 | }); 199 | 200 | return results; 201 | }); 202 | 203 | expect(result.enter).toBeTruthy(); 204 | expect(result.space).toBeTruthy(); 205 | }); 206 | 207 | test('HotKeys Test Case', async () => { 208 | const result = await page.evaluate(async () => { 209 | const results = {}; 210 | 211 | // Test 'w' 212 | await new Promise((resolve) => { 213 | window.hotkeys('w', (e) => { 214 | results.w = e.keyCode === 87; 215 | }); 216 | 217 | const event = new KeyboardEvent('keydown', { 218 | keyCode: 87, 219 | which: 87, 220 | bubbles: true, 221 | cancelable: true, 222 | }); 223 | document.body.dispatchEvent(event); 224 | 225 | setTimeout(() => { 226 | window.hotkeys.unbind('w'); 227 | resolve(); 228 | }, 50); 229 | }); 230 | 231 | // Test 'a' with isPressed 232 | await new Promise((resolve) => { 233 | window.hotkeys('a', () => { 234 | results.isPressedA = window.hotkeys.isPressed('a'); 235 | results.isPressedAUpper = window.hotkeys.isPressed('A'); 236 | results.isPressed65 = window.hotkeys.isPressed(65); 237 | }); 238 | 239 | const event = new KeyboardEvent('keydown', { 240 | keyCode: 65, 241 | which: 65, 242 | bubbles: true, 243 | cancelable: true, 244 | }); 245 | document.body.dispatchEvent(event); 246 | 247 | setTimeout(() => { 248 | window.hotkeys.unbind('a'); 249 | resolve(); 250 | }, 50); 251 | }); 252 | 253 | return results; 254 | }); 255 | 256 | expect(result.w).toBeTruthy(); 257 | expect(result.isPressedA).toBeTruthy(); 258 | expect(result.isPressedAUpper).toBeTruthy(); 259 | expect(result.isPressed65).toBeTruthy(); 260 | }); 261 | 262 | test('HotKeys Key combination Test Case', async () => { 263 | const result = await page.evaluate(async () => { 264 | const results = {}; 265 | 266 | // Test ⌘+d 267 | await new Promise((resolve) => { 268 | window.hotkeys('⌘+d', (e) => { 269 | results.cmdD = e.keyCode === 68 && e.metaKey; 270 | }); 271 | 272 | const event = new KeyboardEvent('keydown', { 273 | keyCode: 68, 274 | which: 68, 275 | metaKey: true, 276 | bubbles: true, 277 | cancelable: true, 278 | }); 279 | document.body.dispatchEvent(event); 280 | 281 | setTimeout(() => { 282 | window.hotkeys.unbind('⌘+d'); 283 | resolve(); 284 | }, 50); 285 | }); 286 | 287 | // Test shift+a 288 | await new Promise((resolve) => { 289 | window.hotkeys('shift+a', (e) => { 290 | results.shiftA = e.keyCode === 65 && e.shiftKey; 291 | }); 292 | 293 | const event = new KeyboardEvent('keydown', { 294 | keyCode: 65, 295 | which: 65, 296 | shiftKey: true, 297 | bubbles: true, 298 | cancelable: true, 299 | }); 300 | document.body.dispatchEvent(event); 301 | 302 | setTimeout(() => { 303 | window.hotkeys.unbind('shift+a'); 304 | resolve(); 305 | }, 50); 306 | }); 307 | 308 | return results; 309 | }); 310 | 311 | expect(result.cmdD).toBeTruthy(); 312 | expect(result.shiftA).toBeTruthy(); 313 | }); 314 | 315 | test('Hotkey trigger with shortcut', async () => { 316 | const result = await page.evaluate(() => { 317 | let count = 0; 318 | window.hotkeys('a', () => { 319 | count++; 320 | }); 321 | 322 | window.hotkeys.trigger('a'); 323 | 324 | window.hotkeys.unbind('a'); 325 | 326 | return count; 327 | }); 328 | 329 | expect(result).toBe(1); 330 | }); 331 | 332 | test('Hotkey trigger with multi shortcut', async () => { 333 | const result = await page.evaluate(() => { 334 | let count = 0; 335 | for (let i = 0; i < 3; i++) { 336 | window.hotkeys('a', () => { 337 | count++; 338 | }); 339 | } 340 | 341 | window.hotkeys.trigger('a'); 342 | 343 | window.hotkeys.unbind('a'); 344 | 345 | return count; 346 | }); 347 | 348 | expect(result).toBe(3); 349 | }); 350 | 351 | test('Layout-independent hotkeys (Cyrillic/Russian keyboard)', async () => { 352 | const result = await page.evaluate(async () => { 353 | const results = { 354 | altMTriggered: false, 355 | altVTriggered: false, 356 | downKeysAfterAltM: [], 357 | downKeysAfterAltV: [], 358 | downKeysAfterRelease: [], 359 | }; 360 | 361 | // Register hotkeys for alt+m and alt+v 362 | window.hotkeys('alt+m', () => { 363 | results.altMTriggered = true; 364 | results.downKeysAfterAltM = window.hotkeys.getPressedKeyCodes(); 365 | }); 366 | 367 | window.hotkeys('alt+v', () => { 368 | results.altVTriggered = true; 369 | results.downKeysAfterAltV = window.hotkeys.getPressedKeyCodes(); 370 | }); 371 | 372 | // Simulate Alt+M on Russian keyboard layout 373 | // On Russian layout, M key produces "Ь" character (code 1068 or ~126) 374 | // But the physical key is still KeyM 375 | await new Promise((resolve) => { 376 | // Press Alt 377 | const altDown = new KeyboardEvent('keydown', { 378 | keyCode: 18, 379 | which: 18, 380 | code: 'AltLeft', 381 | altKey: true, 382 | bubbles: true, 383 | cancelable: true, 384 | }); 385 | document.body.dispatchEvent(altDown); 386 | 387 | setTimeout(() => { 388 | // Press M (with wrong keyCode but correct code) 389 | const mDown = new KeyboardEvent('keydown', { 390 | keyCode: 126, // Wrong keyCode (~ character on Cyrillic layout) 391 | which: 126, 392 | code: 'KeyM', // Correct physical key code 393 | altKey: true, 394 | bubbles: true, 395 | cancelable: true, 396 | }); 397 | document.body.dispatchEvent(mDown); 398 | 399 | setTimeout(() => { 400 | // Release M 401 | const mUp = new KeyboardEvent('keyup', { 402 | keyCode: 126, 403 | which: 126, 404 | code: 'KeyM', 405 | altKey: true, 406 | bubbles: true, 407 | cancelable: true, 408 | }); 409 | document.body.dispatchEvent(mUp); 410 | 411 | setTimeout(() => { 412 | // Release Alt 413 | const altUp = new KeyboardEvent('keyup', { 414 | keyCode: 18, 415 | which: 18, 416 | code: 'AltLeft', 417 | altKey: false, 418 | bubbles: true, 419 | cancelable: true, 420 | }); 421 | document.body.dispatchEvent(altUp); 422 | 423 | resolve(); 424 | }, 20); 425 | }, 20); 426 | }, 20); 427 | }); 428 | 429 | // Small delay between combinations 430 | await new Promise((resolve) => setTimeout(resolve, 50)); 431 | 432 | // Simulate Alt+V on Russian keyboard layout 433 | // On Russian layout, V key produces "М" character 434 | await new Promise((resolve) => { 435 | // Press Alt 436 | const altDown = new KeyboardEvent('keydown', { 437 | keyCode: 18, 438 | which: 18, 439 | code: 'AltLeft', 440 | altKey: true, 441 | bubbles: true, 442 | cancelable: true, 443 | }); 444 | document.body.dispatchEvent(altDown); 445 | 446 | setTimeout(() => { 447 | // Press V (with wrong keyCode but correct code) 448 | const vDown = new KeyboardEvent('keydown', { 449 | keyCode: 1052, // Wrong keyCode (М character on Cyrillic layout) 450 | which: 1052, 451 | code: 'KeyV', // Correct physical key code 452 | altKey: true, 453 | bubbles: true, 454 | cancelable: true, 455 | }); 456 | document.body.dispatchEvent(vDown); 457 | 458 | setTimeout(() => { 459 | // Release V 460 | const vUp = new KeyboardEvent('keyup', { 461 | keyCode: 1052, 462 | which: 1052, 463 | code: 'KeyV', 464 | altKey: true, 465 | bubbles: true, 466 | cancelable: true, 467 | }); 468 | document.body.dispatchEvent(vUp); 469 | 470 | setTimeout(() => { 471 | // Release Alt 472 | const altUp = new KeyboardEvent('keyup', { 473 | keyCode: 18, 474 | which: 18, 475 | code: 'AltLeft', 476 | altKey: false, 477 | bubbles: true, 478 | cancelable: true, 479 | }); 480 | document.body.dispatchEvent(altUp); 481 | 482 | // Check downKeys after all keys released 483 | setTimeout(() => { 484 | results.downKeysAfterRelease = 485 | window.hotkeys.getPressedKeyCodes(); 486 | resolve(); 487 | }, 20); 488 | }, 20); 489 | }, 20); 490 | }, 20); 491 | }); 492 | 493 | // Cleanup 494 | window.hotkeys.unbind('alt+m'); 495 | window.hotkeys.unbind('alt+v'); 496 | 497 | return results; 498 | }); 499 | 500 | // Both hotkeys should be triggered 501 | expect(result.altMTriggered).toBeTruthy(); 502 | expect(result.altVTriggered).toBeTruthy(); 503 | 504 | // Alt (18) + M (77) should be in downKeys during Alt+M 505 | expect(result.downKeysAfterAltM).toEqual(expect.arrayContaining([18, 77])); 506 | 507 | // Alt (18) + V (86) should be in downKeys during Alt+V 508 | expect(result.downKeysAfterAltV).toEqual(expect.arrayContaining([18, 86])); 509 | 510 | // After all keys released, downKeys should be empty (this was the bug) 511 | expect(result.downKeysAfterRelease).toEqual([]); 512 | }); 513 | 514 | afterAll(async () => { 515 | await browser.close(); 516 | }); 517 | }); 518 | 519 | jest.setTimeout(30000); 520 | -------------------------------------------------------------------------------- /README-zh.md: -------------------------------------------------------------------------------- 1 |
2 | 使用我的应用也是一种支持我的方式: 3 |
4 | Deskmark 5 | Keyzer 6 | Vidwall Hub 7 | VidCrop 8 | Vidwall 9 | Mousio Hint 10 | Mousio 11 | Musicer 12 | Audioer 13 | FileSentinel 14 | FocusCursor 15 | Videoer 16 | KeyClicker 17 | DayBar 18 | Iconed 19 | Mousio 20 | Quick RSS 21 | Quick RSS 22 | Web Serve 23 | Copybook Generator 24 | DevTutor for SwiftUI 25 | RegexMate 26 | Time Passage 27 | Iconize Folder 28 | Textsound Saver 29 | Create Custom Symbols 30 | DevHub 31 | Resume Revise 32 | Palette Genius 33 | Symbol Scribe 34 |
35 |
36 | 37 | # Hotkeys 38 | 39 | 40 | 41 | [![Buy me a coffee](https://img.shields.io/badge/Buy%20me%20a%20coffee-048754?logo=buymeacoffee)](https://jaywcjlove.github.io/#/sponsor) 42 | [![](https://img.shields.io/npm/dm/hotkeys-js?logo=npm)](https://www.npmjs.com/package/hotkeys-js) 43 | [![](https://img.shields.io/github/stars/jaywcjlove/hotkeys-js.svg)](https://github.com/jaywcjlove/hotkeys/stargazers) 44 | ![no dependencies](http://jaywcjlove.github.io/sb/status/no-dependencies.svg) 45 | [![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) 46 | [![Coverage Status](https://coveralls.io/repos/github/jaywcjlove/hotkeys/badge.svg?branch=master)](https://coveralls.io/github/jaywcjlove/hotkeys?branch=master) 47 | [![English](https://jaywcjlove.github.io/sb/lang/english.svg)](https://jaywcjlove.github.io/hotkeys-js/) 48 | [![jaywcjlove/hotkeys-js](https://jaywcjlove.github.io/sb/ico/gitee.svg)](https://gitee.com/jaywcjlove/hotkeys) 49 | 50 | HotKeys.js 是一个具有一些非常特殊功能的输入捕获库,它易于上手和使用,占用空间合理([~6kB](https://bundlephobia.com/result?p=hotkeys-js))(压缩后:**`2.8kB`**),无依赖。它不应该干扰任何 JavaScript 库或框架。官方文档 [演示预览](https://jaywcjlove.github.io/hotkeys-js)。[更多示例](https://github.com/jaywcjlove/hotkeys-js/issues?q=label%3ADemo+)。 51 | 52 | ```bash 53 | ╭┈┈╮ ╭┈┈╮ ╭┈┈╮ 54 | ┆ ├┈┈..┈┈┈┈┈.┆ └┈╮┆ ├┈┈..┈┈┈┈┈..┈┈.┈┈..┈┈┈┈┈. 55 | ┆ ┆┆ □ ┆┆ ┈┤┆ < ┆ -__┘┆ ┆ ┆┆__ ┈┈┤ 56 | ╰┈┈┴┈┈╯╰┈┈┈┈┈╯╰┈┈┈┈╯╰┈┈┴┈┈╯╰┈┈┈┈┈╯╰┈┈┈ ┆╰┈┈┈┈┈╯ 57 | ╰┈┈┈┈┈╯ 58 | ``` 59 | 60 | ## 使用 61 | 62 | 您的系统需要安装 `Node.js`。 63 | 64 | ```bash 65 | npm install hotkeys-js --save 66 | ``` 67 | 68 | ```js 69 | import hotkeys from 'hotkeys-js'; 70 | 71 | hotkeys('f5', function(event, handler){ 72 | // 阻止 WINDOWS 系统下的默认刷新事件 73 | event.preventDefault() 74 | alert('你按下了 F5!') 75 | }); 76 | ``` 77 | 78 | ### 浏览器使用 79 | 80 | 或者手动下载并在 HTML 中链接 **hotkeys.js**。该库提供了不同格式以满足不同的使用需求: 81 | 82 | **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/) 83 | 84 | **可用格式:** 85 | 86 | **IIFE(立即调用函数表达式)- 推荐用于直接浏览器使用:** 87 | 88 | ```html 89 | 91 | 106 | ``` 107 | 108 | **UMD(通用模块定义)- 用于 CommonJS/AMD 环境:** 109 | 110 | ```html 111 | 113 | ``` 114 | 115 | **ES 模块 - 用于支持模块的现代浏览器:** 116 | 117 | ```html 118 | 124 | ``` 125 | 126 | ### 在 React 中使用 127 | 128 | [react-hotkeys](https://github.com/jaywcjlove/react-hotkeys) 是监听 keydown 和 keyup 键盘事件的 React 组件,定义和调度键盘快捷键。详细的使用方法请查看其文档 [react-hotkeys](https://github.com/jaywcjlove/react-hotkeys)。 129 | [react-hotkeys-hook](https://github.com/JohannesKlauss/react-hotkeys-hook) - 在组件中使用键盘快捷键的 React hook。请确保您至少安装了 react 和 react-dom 的 16.8 版本,否则 hooks 将不会为您工作。 130 | 131 | ## 浏览器支持 132 | 133 | Hotkeys.js 已经过测试,应该在以下浏览器中工作。 134 | 135 | ```shell 136 | Internet Explorer 6+ 137 | Safari 138 | Firefox 139 | Chrome 140 | ``` 141 | 142 | ## 支持的按键 143 | 144 | HotKeys 理解以下修饰符:`⇧`、`shift`、`option`、`⌥`、`alt`、`ctrl`、`control`、`command` 和 `⌘`。 145 | 146 | 以下特殊按键可用于快捷键: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。 147 | 148 | `⌘` Command() 149 | `⌃` Control 150 | `⌥` Option(alt) 151 | `⇧` Shift 152 | `⇪` Caps Lock(Capital) 153 | ~~`fn` 不支持 fn~~ 154 | `↩︎` return/Enter space 155 | 156 | ## 定义快捷键 157 | 158 | 暴露了一个全局方法,当直接调用时定义快捷键。 159 | 160 | ```js 161 | declare interface HotkeysInterface extends HotkeysAPI { 162 | (key: string, method: KeyHandler): void; 163 | (key: string, scope: string, method: KeyHandler): void; 164 | (key: string, option: HotkeysOptions, method: KeyHandler): void; 165 | shift?: boolean; 166 | ctrl?: boolean; 167 | alt?: boolean; 168 | option?: boolean; 169 | control?: boolean; 170 | cmd?: boolean; 171 | command?: boolean; 172 | } 173 | ``` 174 | 175 | ```js 176 | hotkeys('f5', function(event, handler) { 177 | // 阻止 WINDOWS 系统下的默认刷新事件 178 | event.preventDefault(); 179 | alert('你按下了 F5!'); 180 | }); 181 | 182 | // 返回 false 停止事件并阻止默认浏览器事件 183 | // Mac OS 系统将 `command + r` 定义为刷新快捷键 184 | hotkeys('ctrl+r, command+r', function() { 185 | alert('停止刷新!'); 186 | return false; 187 | }); 188 | 189 | // 单个按键 190 | hotkeys('a', function(event,handler){ 191 | //event.srcElement: input 192 | //event.target: input 193 | if(event.target === "input"){ 194 | alert('你按下了 a!') 195 | } 196 | alert('你按下了 a!') 197 | }); 198 | 199 | // 组合键 200 | hotkeys('ctrl+a,ctrl+b,r,f', function (event, handler){ 201 | switch (handler.key) { 202 | case 'ctrl+a': alert('你按下了 ctrl+a!'); 203 | break; 204 | case 'ctrl+b': alert('你按下了 ctrl+b!'); 205 | break; 206 | case 'r': alert('你按下了 r!'); 207 | break; 208 | case 'f': alert('你按下了 f!'); 209 | break; 210 | default: alert(event); 211 | } 212 | }); 213 | 214 | hotkeys('ctrl+a+s', function() { 215 | alert('你按下了 ctrl+a+s!'); 216 | }); 217 | 218 | // 使用作用域 219 | hotkeys('*','wcj', function(event){ 220 | console.log('做一些事情', event); 221 | }); 222 | ``` 223 | 224 | #### option 选项 225 | 226 | - `scope`:设置快捷键生效的作用域 227 | - `element`:指定要绑定事件的 DOM 元素 228 | - `keyup`:是否在按键释放时触发快捷键 229 | - `keydown`:是否在按键按下时触发快捷键 230 | - `splitKey`:组合键的分隔符(默认为 `+`) 231 | - `capture`:是否在捕获阶段触发监听器(在事件冒泡之前) 232 | - `single`:只允许一个回调函数(自动解绑之前的) 233 | 234 | ```js 235 | hotkeys('o, enter', { 236 | scope: 'wcj', 237 | element: document.getElementById('wrapper'), 238 | }, function() { 239 | console.log('做其他事情'); 240 | }); 241 | 242 | hotkeys('ctrl-+', { splitKey: '-' }, function(e) { 243 | console.log('你按下了 ctrl 和 +'); 244 | }); 245 | 246 | hotkeys('+', { splitKey: '-' }, function(e){ 247 | console.log('你按下了 +'); 248 | }) 249 | ``` 250 | 251 | **keyup** 252 | 253 | **按键按下** 和 **按键释放** 都执行回调事件。 254 | 255 | ```js 256 | hotkeys('ctrl+a,alt+a+s', {keyup: true}, function(event, handler) { 257 | if (event.type === 'keydown') { 258 | console.log('keydown:', event.type, handler, handler.key); 259 | } 260 | 261 | if (event.type === 'keyup') { 262 | console.log('keyup:', event.type, handler, handler.key); 263 | } 264 | }); 265 | ``` 266 | 267 | ## API 参考 268 | 269 | 星号 "*" 270 | 271 | 修饰键判断 272 | 273 | ```js 274 | hotkeys('*', function() { 275 | if (hotkeys.shift) { 276 | console.log('按下了 shift!'); 277 | } 278 | 279 | if (hotkeys.ctrl) { 280 | console.log('按下了 ctrl!'); 281 | } 282 | 283 | if (hotkeys.alt) { 284 | console.log('按下了 alt!'); 285 | } 286 | 287 | if (hotkeys.option) { 288 | console.log('按下了 option!'); 289 | } 290 | 291 | if (hotkeys.control) { 292 | console.log('按下了 control!'); 293 | } 294 | 295 | if (hotkeys.cmd) { 296 | console.log('按下了 cmd!'); 297 | } 298 | 299 | if (hotkeys.command) { 300 | console.log('按下了 command!'); 301 | } 302 | }); 303 | ``` 304 | 305 | ### setScope 306 | 307 | 使用 `hotkeys.setScope` 方法来设置作用域。除了 'all' 之外,只能有一个活动作用域。默认情况下 'all' 总是活动的。 308 | 309 | ```js 310 | // 定义带有作用域的快捷键 311 | hotkeys('ctrl+o, ctrl+alt+enter', 'issues', function() { 312 | console.log('做一些事情'); 313 | }); 314 | hotkeys('o, enter', 'files', function() { 315 | console.log('做其他事情'); 316 | }); 317 | 318 | // 设置作用域(只有 'all' 和 'issues' 快捷键会被处理) 319 | hotkeys.setScope('issues'); // 默认作用域是 'all' 320 | ``` 321 | 322 | ### getScope 323 | 324 | 使用 `hotkeys.getScope` 方法来获取作用域。 325 | 326 | ```js 327 | hotkeys.getScope(); 328 | ``` 329 | 330 | ### deleteScope 331 | 332 | 使用 `hotkeys.deleteScope` 方法来删除作用域。这也会移除与之关联的所有热键。 333 | 334 | ```js 335 | hotkeys.deleteScope('issues'); 336 | ``` 337 | 如果需要在删除后设置新的作用域,可以使用第二个参数。 338 | 339 | ```js 340 | hotkeys.deleteScope('issues', 'newScopeName'); 341 | ``` 342 | 343 | ### unbind 344 | 345 | 与定义快捷键类似,它们可以使用 `hotkeys.unbind` 来解绑。 346 | 347 | ```js 348 | // 解绑 'a' 处理器 349 | hotkeys.unbind('a'); 350 | 351 | // 只为单个作用域解绑热键 352 | // 如果没有指定作用域,默认为当前作用域 353 | // (hotkeys.getScope()) 354 | hotkeys.unbind('o, enter', 'issues'); 355 | hotkeys.unbind('o, enter', 'files'); 356 | ``` 357 | 358 | 通过函数解绑事件。 359 | 360 | ```js 361 | function example() { 362 | hotkeys('a', example); 363 | hotkeys.unbind('a', example); 364 | 365 | hotkeys('a', 'issues', example); 366 | hotkeys.unbind('a', 'issues', example); 367 | } 368 | ``` 369 | 370 | 解绑所有。 371 | 372 | ```js 373 | hotkeys.unbind(); 374 | ``` 375 | 376 | ### isPressed 377 | 378 | 例如,如果当前按下了 `M` 键,`hotkeys.isPressed(77)` 返回 true。 379 | 380 | ```js 381 | hotkeys('a', function() { 382 | console.log(hotkeys.isPressed('a')); //=> true 383 | console.log(hotkeys.isPressed('A')); //=> true 384 | console.log(hotkeys.isPressed(65)); //=> true 385 | }); 386 | ``` 387 | 388 | ### trigger 389 | 390 | 触发快捷键事件 391 | 392 | ```js 393 | hotkeys.trigger('ctrl+o'); 394 | hotkeys.trigger('ctrl+o', 'scope2'); 395 | ``` 396 | 397 | ### getPressedKeyCodes 398 | 399 | 返回当前按下的键码数组。 400 | 401 | ```js 402 | hotkeys('command+ctrl+shift+a,f', function() { 403 | console.log(hotkeys.getPressedKeyCodes()); //=> [17, 65] 或 [70] 404 | }) 405 | ``` 406 | 407 | ### getPressedKeyString 408 | 409 | 返回当前按下的键字符串数组。 410 | 411 | ```js 412 | hotkeys('command+ctrl+shift+a,f', function() { 413 | console.log(hotkeys.getPressedKeyString()); 414 | //=> ['⌘', '⌃', '⇧', 'A', 'F'] 415 | }) 416 | ``` 417 | 418 | ### getAllKeyCodes 419 | 420 | 获取所有注册码的列表。 421 | 422 | ```js 423 | hotkeys('command+ctrl+shift+a,f', function() { 424 | console.log(hotkeys.getAllKeyCodes()); 425 | // [ 426 | // { 427 | // scope: 'all', 428 | // shortcut: 'command+ctrl+shift+a', 429 | // mods: [91, 17, 16], 430 | // keys: [91, 17, 16, 65] 431 | // }, 432 | // { scope: 'all', shortcut: 'f', mods: [], keys: [42] } 433 | // ] 434 | }) 435 | ``` 436 | 437 | ### filter 438 | 439 | 默认情况下,`INPUT` `SELECT` `TEXTAREA` 元素不启用热键。`Hotkeys.filter` 返回 `true` 快捷键设置发挥作用,`false` 快捷键设置失败。 440 | 441 | ```js 442 | hotkeys.filter = function(event){ 443 | return true; 444 | } 445 | // 如何为编辑标签添加过滤器。 446 | //
447 | // "contentEditable" 不支持的较旧浏览器会被丢弃 448 | hotkeys.filter = function(event) { 449 | var target = event.target || event.srcElement; 450 | var tagName = target.tagName; 451 | return !( 452 | target.isContentEditable || 453 | tagName == 'INPUT' || 454 | tagName == 'SELECT' || 455 | tagName == 'TEXTAREA' 456 | ); 457 | } 458 | 459 | hotkeys.filter = function(event){ 460 | var tagName = (event.target || event.srcElement).tagName; 461 | hotkeys.setScope( 462 | /^(INPUT|TEXTAREA|SELECT)$/.test(tagName) ? 'input' : 'other' 463 | ); 464 | return true; 465 | } 466 | ``` 467 | 468 | ### noConflict 469 | 470 | 放弃 HotKeys 对 `hotkeys` 变量的控制。 471 | 472 | ```js 473 | var k = hotkeys.noConflict(); 474 | k('a', function() { 475 | console.log("做一些事情") 476 | }); 477 | 478 | hotkeys() 479 | // -->Uncaught TypeError: hotkeys is not a function(anonymous function) 480 | // @ VM2170:2InjectedScript._evaluateOn 481 | // @ VM2165:883InjectedScript._evaluateAndWrap 482 | // @ VM2165:816InjectedScript.evaluate @ VM2165:682 483 | ``` 484 | 485 | ## 开发 486 | 487 | 要开发,需要安装依赖,获取代码: 488 | 489 | ```shell 490 | $ git https://github.com/jaywcjlove/hotkeys.git 491 | $ cd hotkeys # 进入目录 492 | $ npm install # 或者 yarn install 493 | ``` 494 | 495 | 要开发,运行自重载构建: 496 | 497 | ```shell 498 | $ npm run watch 499 | ``` 500 | 501 | 运行文档网站环境。 502 | 503 | ```shell 504 | $ npm run doc # 生成文档网页 505 | # 实时生成文档网页 506 | $ npm run start 507 | ``` 508 | 509 | 要贡献,请 fork Hotkeys.js,添加您的补丁和测试(在 `test/` 文件夹中)并提交拉取请求。 510 | 511 | ```shell 512 | $ npm run test 513 | $ npm run test:watch # 开发模式 514 | ``` 515 | 516 | ## 贡献者 517 | 518 | 一如既往,感谢我们出色的贡献者! 519 | 520 | 521 | 522 | 523 | 524 | 使用 [action-contributors](https://github.com/jaywcjlove/github-action-contributors) 制作。 525 | 526 | 特别感谢 [@dimensi](https://github.com/dimensi) 对版本 [4.0](https://github.com/jaywcjlove/hotkeys-js/issues/313) 的重构。 527 | 528 | ## 许可证 529 | 530 | [MIT © Kenny Wong](./LICENSE) 531 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | Using my app is also a way to support me: 3 |
4 | Deskmark 5 | Keyzer 6 | Vidwall Hub 7 | VidCrop 8 | Vidwall 9 | Mousio Hint 10 | Mousio 11 | Musicer 12 | Audioer 13 | FileSentinel 14 | FocusCursor 15 | Videoer 16 | KeyClicker 17 | DayBar 18 | Iconed 19 | Mousio 20 | Quick RSS 21 | Quick RSS 22 | Web Serve 23 | Copybook Generator 24 | DevTutor for SwiftUI 25 | RegexMate 26 | Time Passage 27 | Iconize Folder 28 | Textsound Saver 29 | Create Custom Symbols 30 | DevHub 31 | Resume Revise 32 | Palette Genius 33 | Symbol Scribe 34 |
35 |
36 | 37 | # Hotkeys 38 | 39 | [![Buy me a coffee](https://img.shields.io/badge/Buy%20me%20a%20coffee-048754?logo=buymeacoffee)](https://jaywcjlove.github.io/#/sponsor) 40 | [![](https://img.shields.io/npm/dm/hotkeys-js?logo=npm)](https://www.npmjs.com/package/hotkeys-js) 41 | [![](https://img.shields.io/github/stars/jaywcjlove/hotkeys-js.svg)](https://github.com/jaywcjlove/hotkeys/stargazers) 42 | ![no dependencies](http://jaywcjlove.github.io/sb/status/no-dependencies.svg) 43 | [![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) 44 | [![Coverage Status](https://coveralls.io/repos/github/jaywcjlove/hotkeys/badge.svg?branch=master)](https://coveralls.io/github/jaywcjlove/hotkeys?branch=master) 45 | [![Chinese](https://jaywcjlove.github.io/sb/lang/chinese.svg)](https://wangchujiang.com/hotkeys-js/?lang=zh) 46 | [![jaywcjlove/hotkeys-js](https://jaywcjlove.github.io/sb/ico/gitee.svg)](https://gitee.com/jaywcjlove/hotkeys) 47 | 48 | HotKeys.js is an input capture library with some very special features, it is easy to pick up and use, has a reasonable footprint ([~6kB](https://bundlephobia.com/result?p=hotkeys-js)) (gzipped: **`2.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). [More examples](https://github.com/jaywcjlove/hotkeys-js/issues?q=label%3ADemo+). 49 | 50 | ```bash 51 | ╭┈┈╮ ╭┈┈╮ ╭┈┈╮ 52 | ┆ ├┈┈..┈┈┈┈┈.┆ └┈╮┆ ├┈┈..┈┈┈┈┈..┈┈.┈┈..┈┈┈┈┈. 53 | ┆ ┆┆ □ ┆┆ ┈┤┆ < ┆ -__┘┆ ┆ ┆┆__ ┈┈┤ 54 | ╰┈┈┴┈┈╯╰┈┈┈┈┈╯╰┈┈┈┈╯╰┈┈┴┈┈╯╰┈┈┈┈┈╯╰┈┈┈ ┆╰┈┈┈┈┈╯ 55 | ╰┈┈┈┈┈╯ 56 | ``` 57 | 58 | ## Usage 59 | 60 | You will need `Node.js` installed on your system. 61 | 62 | ```bash 63 | npm install hotkeys-js --save 64 | ``` 65 | 66 | ```js 67 | import hotkeys from 'hotkeys-js'; 68 | 69 | hotkeys('f5', function(event, handler){ 70 | // Prevent the default refresh event under WINDOWS system 71 | event.preventDefault() 72 | alert('you pressed F5!') 73 | }); 74 | ``` 75 | 76 | ### Browser Usage 77 | 78 | Or manually download and link **hotkeys.js** in your HTML. The library provides different formats for different use cases: 79 | 80 | **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/) 81 | 82 | **Available Formats:** 83 | 84 | **IIFE (Immediately Invoked Function Expression) - Recommended for direct browser usage:** 85 | 86 | ```html 87 | 88 | 103 | ``` 104 | 105 | **UMD (Universal Module Definition) - For CommonJS/AMD environments:** 106 | 107 | ```html 108 | 109 | ``` 110 | 111 | **ES Module - For modern browsers with module support:** 112 | 113 | ```html 114 | 120 | ``` 121 | 122 | ### Used in React 123 | 124 | [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). 125 | 126 | [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. 127 | 128 | ## Browser Support 129 | 130 | Hotkeys.js has been tested and should work in. 131 | 132 | ```shell 133 | Internet Explorer 6+ 134 | Safari 135 | Firefox 136 | Chrome 137 | ``` 138 | 139 | ## Supported Keys 140 | 141 | HotKeys understands the following modifiers: `⇧`, `shift`, `option`, `⌥`, `alt`, `ctrl`, `control`, `command`, and `⌘`. 142 | 143 | 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. 144 | 145 | `⌘` Command() 146 | `⌃` Control 147 | `⌥` Option(alt) 148 | `⇧` Shift 149 | `⇪` Caps Lock(Capital) 150 | ~~`fn` Does not support fn~~ 151 | `↩︎` return/Enter space 152 | 153 | ## Defining Shortcuts 154 | 155 | One global method is exposed, key which defines shortcuts when called directly. 156 | 157 | ```js 158 | declare interface HotkeysInterface extends HotkeysAPI { 159 | (key: string, method: KeyHandler): void; 160 | (key: string, scope: string, method: KeyHandler): void; 161 | (key: string, option: HotkeysOptions, method: KeyHandler): void; 162 | shift?: boolean; 163 | ctrl?: boolean; 164 | alt?: boolean; 165 | option?: boolean; 166 | control?: boolean; 167 | cmd?: boolean; 168 | command?: boolean; 169 | } 170 | ``` 171 | 172 | 173 | ```js 174 | hotkeys('f5', function(event, handler) { 175 | // Prevent the default refresh event under WINDOWS system 176 | event.preventDefault(); 177 | alert('you pressed F5!'); 178 | }); 179 | 180 | // Returning false stops the event and prevents default browser events 181 | // Mac OS system defines `command + r` as a refresh shortcut 182 | hotkeys('ctrl+r, command+r', function() { 183 | alert('stopped reload!'); 184 | return false; 185 | }); 186 | 187 | // Single key 188 | hotkeys('a', function(event,handler){ 189 | //event.srcElement: input 190 | //event.target: input 191 | if(event.target === "input"){ 192 | alert('you pressed a!') 193 | } 194 | alert('you pressed a!') 195 | }); 196 | 197 | // Key Combination 198 | hotkeys('ctrl+a,ctrl+b,r,f', function (event, handler){ 199 | switch (handler.key) { 200 | case 'ctrl+a': alert('you pressed ctrl+a!'); 201 | break; 202 | case 'ctrl+b': alert('you pressed ctrl+b!'); 203 | break; 204 | case 'r': alert('you pressed r!'); 205 | break; 206 | case 'f': alert('you pressed f!'); 207 | break; 208 | default: alert(event); 209 | } 210 | }); 211 | 212 | hotkeys('ctrl+a+s', function() { 213 | alert('you pressed ctrl+a+s!'); 214 | }); 215 | 216 | // Using a scope 217 | hotkeys('*','wcj', function(event){ 218 | console.log('do something', event); 219 | }); 220 | ``` 221 | 222 | #### option 223 | 224 | - `scope`: Sets the scope in which the shortcut key is active 225 | - `element`: Specifies the DOM element to bind the event to 226 | - `keyup`: Whether to trigger the shortcut on key release 227 | - `keydown`: Whether to trigger the shortcut on key press 228 | - `splitKey`: Delimiter for key combinations (default is `+`) 229 | - `capture`: Whether to trigger the listener during the capture phase (before the event bubbles down) 230 | - `single`: Allows only one callback function (automatically unbinds previous one) 231 | 232 | ```js 233 | hotkeys('o, enter', { 234 | scope: 'wcj', 235 | element: document.getElementById('wrapper'), 236 | }, function() { 237 | console.log('do something else'); 238 | }); 239 | 240 | hotkeys('ctrl-+', { splitKey: '-' }, function(e) { 241 | console.log('you pressed ctrl and +'); 242 | }); 243 | 244 | hotkeys('+', { splitKey: '-' }, function(e){ 245 | console.log('you pressed +'); 246 | }) 247 | ``` 248 | 249 | **keyup** 250 | 251 | **key down** and **key up** both perform callback events. 252 | 253 | ```js 254 | hotkeys('ctrl+a,alt+a+s', {keyup: true}, function(event, handler) { 255 | if (event.type === 'keydown') { 256 | console.log('keydown:', event.type, handler, handler.key); 257 | } 258 | 259 | if (event.type === 'keyup') { 260 | console.log('keyup:', event.type, handler, handler.key); 261 | } 262 | }); 263 | ``` 264 | 265 | ## API REFERENCE 266 | 267 | Asterisk "*" 268 | 269 | Modifier key judgments 270 | 271 | ```js 272 | hotkeys('*', function() { 273 | if (hotkeys.shift) { 274 | console.log('shift is pressed!'); 275 | } 276 | 277 | if (hotkeys.ctrl) { 278 | console.log('ctrl is pressed!'); 279 | } 280 | 281 | if (hotkeys.alt) { 282 | console.log('alt is pressed!'); 283 | } 284 | 285 | if (hotkeys.option) { 286 | console.log('option is pressed!'); 287 | } 288 | 289 | if (hotkeys.control) { 290 | console.log('control is pressed!'); 291 | } 292 | 293 | if (hotkeys.cmd) { 294 | console.log('cmd is pressed!'); 295 | } 296 | 297 | if (hotkeys.command) { 298 | console.log('command is pressed!'); 299 | } 300 | }); 301 | ``` 302 | 303 | ### setScope 304 | 305 | Use the `hotkeys.setScope` method to set scope. There can only be one active scope besides 'all'. By default 'all' is always active. 306 | 307 | ```js 308 | // Define shortcuts with a scope 309 | hotkeys('ctrl+o, ctrl+alt+enter', 'issues', function() { 310 | console.log('do something'); 311 | }); 312 | hotkeys('o, enter', 'files', function() { 313 | console.log('do something else'); 314 | }); 315 | 316 | // Set the scope (only 'all' and 'issues' shortcuts will be honored) 317 | hotkeys.setScope('issues'); // default scope is 'all' 318 | ``` 319 | 320 | ### getScope 321 | 322 | Use the `hotkeys.getScope` method to get scope. 323 | 324 | ```js 325 | hotkeys.getScope(); 326 | ``` 327 | 328 | ### deleteScope 329 | 330 | Use the `hotkeys.deleteScope` method to delete a scope. This will also remove all associated hotkeys with it. 331 | 332 | ```js 333 | hotkeys.deleteScope('issues'); 334 | ``` 335 | You can use second argument, if need set new scope after deleting. 336 | 337 | ```js 338 | hotkeys.deleteScope('issues', 'newScopeName'); 339 | ``` 340 | 341 | ### unbind 342 | 343 | Similar to defining shortcuts, they can be unbound using `hotkeys.unbind`. 344 | 345 | ```js 346 | // unbind 'a' handler 347 | hotkeys.unbind('a'); 348 | 349 | // Unbind a hotkeys only for a single scope 350 | // If no scope is specified it defaults to the current 351 | // scope (hotkeys.getScope()) 352 | hotkeys.unbind('o, enter', 'issues'); 353 | hotkeys.unbind('o, enter', 'files'); 354 | ``` 355 | 356 | Unbind events through functions. 357 | 358 | ```js 359 | function example() { 360 | hotkeys('a', example); 361 | hotkeys.unbind('a', example); 362 | 363 | hotkeys('a', 'issues', example); 364 | hotkeys.unbind('a', 'issues', example); 365 | } 366 | ``` 367 | 368 | To unbind everything. 369 | 370 | ```js 371 | hotkeys.unbind(); 372 | ``` 373 | 374 | ### isPressed 375 | 376 | For example, `hotkeys.isPressed(77)` is true if the `M` key is currently pressed. 377 | 378 | ```js 379 | hotkeys('a', function() { 380 | console.log(hotkeys.isPressed('a')); //=> true 381 | console.log(hotkeys.isPressed('A')); //=> true 382 | console.log(hotkeys.isPressed(65)); //=> true 383 | }); 384 | ``` 385 | 386 | ### trigger 387 | 388 | trigger shortcut key event 389 | 390 | ```js 391 | hotkeys.trigger('ctrl+o'); 392 | hotkeys.trigger('ctrl+o', 'scope2'); 393 | ``` 394 | 395 | ### getPressedKeyCodes 396 | 397 | Returns an array of key codes currently pressed. 398 | 399 | ```js 400 | hotkeys('command+ctrl+shift+a,f', function() { 401 | console.log(hotkeys.getPressedKeyCodes()); //=> [17, 65] or [70] 402 | }) 403 | ``` 404 | 405 | ### getPressedKeyString 406 | 407 | Returns an array of key codes currently pressed. 408 | 409 | ```js 410 | hotkeys('command+ctrl+shift+a,f', function() { 411 | console.log(hotkeys.getPressedKeyString()); 412 | //=> ['⌘', '⌃', '⇧', 'A', 'F'] 413 | }) 414 | ``` 415 | 416 | ### getAllKeyCodes 417 | 418 | Get a list of all registration codes. 419 | 420 | ```js 421 | hotkeys('command+ctrl+shift+a,f', function() { 422 | console.log(hotkeys.getAllKeyCodes()); 423 | // [ 424 | // { 425 | // scope: 'all', 426 | // shortcut: 'command+ctrl+shift+a', 427 | // mods: [91, 17, 16], 428 | // keys: [91, 17, 16, 65] 429 | // }, 430 | // { scope: 'all', shortcut: 'f', mods: [], keys: [42] } 431 | // ] 432 | }) 433 | ``` 434 | 435 | ### filter 436 | 437 | 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. 438 | 439 | ```js 440 | hotkeys.filter = function(event){ 441 | return true; 442 | } 443 | // How to add the filter to edit labels. 444 | //
445 | // "contentEditable" Older browsers that do not support drops 446 | hotkeys.filter = function(event) { 447 | var target = event.target || event.srcElement; 448 | var tagName = target.tagName; 449 | return !( 450 | target.isContentEditable || 451 | tagName == 'INPUT' || 452 | tagName == 'SELECT' || 453 | tagName == 'TEXTAREA' 454 | ); 455 | } 456 | 457 | hotkeys.filter = function(event){ 458 | var tagName = (event.target || event.srcElement).tagName; 459 | hotkeys.setScope( 460 | /^(INPUT|TEXTAREA|SELECT)$/.test(tagName) ? 'input' : 'other' 461 | ); 462 | return true; 463 | } 464 | ``` 465 | 466 | ### noConflict 467 | 468 | Relinquish HotKeys’s control of the `hotkeys` variable. 469 | 470 | ```js 471 | var k = hotkeys.noConflict(); 472 | k('a', function() { 473 | console.log("do something") 474 | }); 475 | 476 | hotkeys() 477 | // -->Uncaught TypeError: hotkeys is not a function(anonymous function) 478 | // @ VM2170:2InjectedScript._evaluateOn 479 | // @ VM2165:883InjectedScript._evaluateAndWrap 480 | // @ VM2165:816InjectedScript.evaluate @ VM2165:682 481 | ``` 482 | 483 | ## Development 484 | 485 | To develop, Install dependencies, Get the code: 486 | 487 | ```shell 488 | $ git https://github.com/jaywcjlove/hotkeys.git 489 | $ cd hotkeys # Into the directory 490 | $ npm install # or yarn install 491 | ``` 492 | 493 | To develop, run the self-reloading build: 494 | 495 | ```shell 496 | $ npm run watch 497 | ``` 498 | 499 | Run Document Website Environment. 500 | 501 | ```shell 502 | # Generate documentation website 503 | $ npm run doc 504 | # Live-generate documentation website 505 | $ npm run start 506 | ``` 507 | 508 | To contribute, please fork Hotkeys.js, add your patch and tests for it (in the `test/` folder) and submit a pull request. 509 | 510 | ```shell 511 | $ npm run test 512 | $ npm run test:watch # Development model 513 | ``` 514 | 515 | ## Contributors 516 | 517 | As always, thanks to our amazing contributors! 518 | 519 | 520 | 521 | 522 | 523 | Made with [action-contributors](https://github.com/jaywcjlove/github-action-contributors). 524 | 525 | Special thanks to [@dimensi](https://github.com/dimensi) for the refactoring of version [4.0](https://github.com/jaywcjlove/hotkeys-js/issues/313). 526 | 527 | ## License 528 | 529 | [MIT © Kenny Wong](./LICENSE) 530 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | KeyCodeInfo, 3 | HotkeysEvent, 4 | UnbindInfo, 5 | HotkeysOptions, 6 | KeyHandler, 7 | HotkeysInterface, 8 | SetScope, 9 | GetScope, 10 | GetPressedKeyCodes, 11 | GetPressedKeyString, 12 | GetAllKeyCodes, 13 | Filter, 14 | IsPressed, 15 | DeleteScope, 16 | Unbind, 17 | HotkeysAPI, 18 | } from './types'; 19 | import { addEvent, removeEvent, getMods, getKeys, compareArray, getLayoutIndependentKeyCode } from './utils'; 20 | import { _keyMap, _modifier, modifierMap, _mods, _handlers } from './var'; 21 | 22 | /** Record the pressed keys */ 23 | let _downKeys: number[] = []; 24 | /** Whether the window has already listened to the focus event */ 25 | let winListendFocus: { listener: EventListener; capture: boolean } | null = 26 | null; 27 | /** Default hotkey scope */ 28 | let _scope: string = 'all'; 29 | /** Map to record elements with bound events */ 30 | const elementEventMap = new Map< 31 | HTMLElement | Document, 32 | { 33 | keydownListener: EventListener; 34 | keyupListenr: EventListener; 35 | capture: boolean; 36 | } 37 | >(); 38 | 39 | /** Return key code */ 40 | const code = (x: string): number => 41 | _keyMap[x.toLowerCase()] || 42 | _modifier[x.toLowerCase()] || 43 | x.toUpperCase().charCodeAt(0); 44 | 45 | const getKey = (x: number): string | undefined => 46 | Object.keys(_keyMap).find((k) => _keyMap[k] === x); 47 | const getModifier = (x: number): string | undefined => 48 | Object.keys(_modifier).find((k) => _modifier[k] === x); 49 | 50 | /** Set or get the current scope (defaults to 'all') */ 51 | const setScope: SetScope = (scope) => { 52 | _scope = scope || 'all'; 53 | }; 54 | /** Get the current scope */ 55 | const getScope: GetScope = () => { 56 | return _scope || 'all'; 57 | }; 58 | /** Get the key codes of the currently pressed keys */ 59 | const getPressedKeyCodes: GetPressedKeyCodes = () => { 60 | return _downKeys.slice(0); 61 | }; 62 | 63 | const getPressedKeyString: GetPressedKeyString = () => { 64 | return _downKeys.map( 65 | (c) => getKey(c) || getModifier(c) || String.fromCharCode(c) 66 | ); 67 | }; 68 | 69 | const getAllKeyCodes: GetAllKeyCodes = () => { 70 | const result: KeyCodeInfo[] = []; 71 | Object.keys(_handlers).forEach((k) => { 72 | _handlers[k].forEach(({ key, scope, mods, shortcut }) => { 73 | result.push({ 74 | scope, 75 | shortcut, 76 | mods, 77 | keys: key.split('+').map((v) => code(v)), 78 | }); 79 | }); 80 | }); 81 | return result; 82 | }; 83 | 84 | /** hotkey is effective only when filter return true */ 85 | const filter: Filter = (event) => { 86 | const target = (event.target || event.srcElement) as HTMLElement; 87 | const { tagName } = target; 88 | let flag = true; 89 | const isInput = 90 | tagName === 'INPUT' && 91 | ![ 92 | 'checkbox', 93 | 'radio', 94 | 'range', 95 | 'button', 96 | 'file', 97 | 'reset', 98 | 'submit', 99 | 'color', 100 | ].includes((target as HTMLInputElement).type); 101 | // ignore: isContentEditable === 'true', and