@peter.naydenov/shortcuts 4.0.1 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +54 -0
- package/Changelog.md +14 -0
- package/README.md +935 -426
- package/dist/plugins/click/_listenDOM.d.ts +2 -2
- package/dist/plugins/form/_listenDOM.d.ts +4 -4
- package/dist/plugins/hover/_listenDOM.d.ts +2 -2
- package/dist/plugins/key/_listenDOM.d.ts +2 -2
- package/dist/plugins/scroll/_listenDOM.d.ts +2 -2
- package/dist/shortcuts.cjs +1 -1
- package/dist/shortcuts.esm.mjs +1 -1
- package/dist/shortcuts.umd.js +1 -1
- package/graphify-out/.graphify_benchmark.json +35 -0
- package/graphify-out/.graphify_python +1 -0
- package/graphify-out/GRAPH_REPORT.md +121 -0
- package/graphify-out/cache/024633ee076562ce0dbc0bf3b5315906f4e5d48c57bc96856b98cd5d42a69009.json +1 -0
- package/graphify-out/cache/037d2ba5552034c06beca677b1914ac7a053f696dfeaf12ca0de06eb42659f18.json +1 -0
- package/graphify-out/cache/055bfc2a3bfb8747f7ee0b7335704d2dff1c071d25a9bdd4f954e4cc2d630772.json +1 -0
- package/graphify-out/cache/0b2f626892ed04f158e39593dbf5f266c2a5982a21b2dddb144edc79af81e2d1.json +1 -0
- package/graphify-out/cache/0c17fe866170c4ab4023ac0680d1e05c5dc232a17ac0f08dfa1e76c2eaf75c44.json +1 -0
- package/graphify-out/cache/1392dca26291b5396829f1a996aac2c1d34a03d134a5fcf54e4f15824ee74e2b.json +1 -0
- package/graphify-out/cache/18113cd08ebffb11ed91ffab9c6d34795e22bfb5993941db07a52eed6eba45b8.json +1 -0
- package/graphify-out/cache/1a7a71b157cadd117435818e1a6561157c2930c4066d3a207fe04e318f76b296.json +1 -0
- package/graphify-out/cache/2448db4b822a94d6f3512ce8788077f35dfb567aef8628a846fad841b40575e7.json +1 -0
- package/graphify-out/cache/2592868b7b9d2de3f2cb575b7bd68ca2f252d34f71c12f2e8721d789cfbfbf88.json +1 -0
- package/graphify-out/cache/2aaf58292523f67421e6f728fd97740c5bf07dd903cf267cf557a4383759ba5e.json +1 -0
- package/graphify-out/cache/349a418954e66e5ef45370dca740ebff559a72d11f5810f6d40c0af14ef768e0.json +1 -0
- package/graphify-out/cache/361eacb4abb14862b75257bdf673a353826bf5764fb187ccde94ae21454bcf99.json +1 -0
- package/graphify-out/cache/40b4c82a11a2a31b279563c143d91d7894eb3f3d0c386f8323cb8062bcdfadd5.json +1 -0
- package/graphify-out/cache/40db98ff036ad694953cb13628795fabc0c5892ff093dfdcc18a309ac0d10846.json +1 -0
- package/graphify-out/cache/43a590ac22f05be183ea1a4655922185595c969f79dd3df3d44d1c7e49355785.json +1 -0
- package/graphify-out/cache/4993a6e98dcf33bb6fa78341b4eeac4776e2322f10522eecdc5195aa7969f35a.json +1 -0
- package/graphify-out/cache/5095c6e52a24f4ecec9acf63835761ad508df88d56b1799faec47672fbd4e348.json +1 -0
- package/graphify-out/cache/5cba1d38ffea01d8c62e5a0b0d8b164cf9b115ff1b6f1acc606f78877712d5de.json +1 -0
- package/graphify-out/cache/67aadf0b8f90224cb725e903e6fedbf6828f203467a633f98031b0740930cb21.json +1 -0
- package/graphify-out/cache/68dffbffbd811942d85ef2600ca31e423d3c3c343de98cbba4c954c94dd11470.json +1 -0
- package/graphify-out/cache/69c684e506b9ce22e95aff044c1da4b0f88ef72c510da02a739edbe551372a8e.json +1 -0
- package/graphify-out/cache/6e336587fcfe2b298f23326e881f048d37f1bb062d00806c338da58a8fd281ca.json +1 -0
- package/graphify-out/cache/78d88737e6db913f091c4c48e96953df065f6808e2b60dd828fdee64067dac91.json +1 -0
- package/graphify-out/cache/7e5340b989299a0b7217d1fd0d2b919cba65142f3e468b0aca5f4ffd5c0594d8.json +1 -0
- package/graphify-out/cache/813d6bac52066ed8733781c35710ecb7995e6cabbe0d9abb9854e3c67610b974.json +1 -0
- package/graphify-out/cache/8180f34832546e874bc5f1931eee545d97300be49faac5c9b6d515653a763324.json +1 -0
- package/graphify-out/cache/885894ca90af6a3724182762bc4fc7ff7d22727a931d46fe7593d1eea10c0c71.json +1 -0
- package/graphify-out/cache/8a304ae8f6bf02bfa40923cdbea99e4bea943db52c185f22caa43ba7c34f94d3.json +1 -0
- package/graphify-out/cache/903a7dea28112a27dcc1b9ece66514f4d5dd6ca264f5ee70835aca069a8df2ad.json +1 -0
- package/graphify-out/cache/9831a7833c5bfeb9a0611e416f7038bd37884b42a9a720f9b4c0a01f860a4f54.json +1 -0
- package/graphify-out/cache/98dbdcdd1b19bc942850f50b1ebdeb1865c72ba724990217464efd28a3732b32.json +1 -0
- package/graphify-out/cache/a2459f621d588f0166ae6a4204bb6b89f9d669b3ad0c54a88afac6c7abb134b4.json +1 -0
- package/graphify-out/cache/a25d47ecf087fa6888d641f89f08cefd35c68b5823c8c55b3baa0243ab110110.json +1 -0
- package/graphify-out/cache/a3bd22d8493943a3195c3ef1254a7240624a962edf2baa2c30eb0ae60564fbe7.json +1 -0
- package/graphify-out/cache/a4d4fb674183a3b348f542b1b9fb9c0d7b176c43636afb2554af088a9613a1c0.json +1 -0
- package/graphify-out/cache/a87a705106773b14c5a25697d30c743cdab01df551cdd9892d6ec46f98ad1659.json +1 -0
- package/graphify-out/cache/a9416d0397b5fb994b8c3847aea2599a9d33940e6f0652accc5ba1de478349ee.json +1 -0
- package/graphify-out/cache/ab03b9df0e9b8a74db3782c96fee833d800d93838fc0c056306ac2ef9a3e0c09.json +1 -0
- package/graphify-out/cache/ad3a99182567225cc19374c28d33097f146547bd945967c723b66d1065134ce9.json +1 -0
- package/graphify-out/cache/aece91cfc3a5181bbb77a1758921dfb6a323ab04cc402ce42f2832446d04f420.json +1 -0
- package/graphify-out/cache/bd65fd515423e8964058f6aa997c05e3e0fb9e6d39209d4a1d76a079c6af46e8.json +1 -0
- package/graphify-out/cache/c2a85071784f9516ab2dea976eeb3a514a53b15701bbf60b4d8be6cd3385cd6c.json +1 -0
- package/graphify-out/cache/c9a8c9342926031f308af0eb0a8d60cf0b443e84bae839da42998956465e47e6.json +1 -0
- package/graphify-out/cache/d05c0aa647a624e0c696f53c027d066c35d0893695e9a23fd820235ee86b4a70.json +1 -0
- package/graphify-out/cache/d3d9832015ab51f52ae88375cea2cbeabecd4a000578e28e899ce23e74245733.json +1 -0
- package/graphify-out/cache/d449ad503a40840d41cbf24ed57f408bf5fdf891f830990f836cf52da5c605eb.json +1 -0
- package/graphify-out/cache/d92b22194973f3c39ac53d85a29f5d4837d07b0f9f0d375e3ddce8da158777fb.json +1 -0
- package/graphify-out/cache/dda8f89f688d8a4db8b7279031ad26a0d8d4accc0aa049abda5fa19eac4bd5ef.json +1 -0
- package/graphify-out/cache/e1d80dbca10b7e2ba65339eff0649699c6091d30b836a1e9d5d094bb95aacc48.json +1 -0
- package/graphify-out/cache/e207108277cbe1af0501688b0268fea879d0414424386fbaa93a5861f306bdba.json +1 -0
- package/graphify-out/cache/e6032dad287da859a517d6b59105595db90e81833dbd850b37653bbd0f3acef7.json +1 -0
- package/graphify-out/cache/f49d7295a833de68579e0e265832bc78d21e901764e31705423e621a703124dc.json +1 -0
- package/graphify-out/cache/fcf90a1251a332948a773c6aaaad4ce7f6de8d2f2333687cb2fe94e0d860a6c9.json +1 -0
- package/graphify-out/cache/fe06fcb623d36858b89c8741696482530a084f599d48bea88de7943fae0f9bea.json +1 -0
- package/graphify-out/cache/ffa0f819e023809d17aac1af75cf0f6fbf08500615aee27341b658f24357105a.json +1 -0
- package/graphify-out/cost.json +12 -0
- package/graphify-out/graph.html +266 -0
- package/graphify-out/graph.json +634 -0
- package/graphify-out/manifest.json +124 -0
- package/how-to-create-a-plugin.md +573 -0
- package/package.json +23 -21
- package/src/main.js +5 -5
- package/src/plugins/click/_listenDOM.js +3 -3
- package/src/plugins/form/_listenDOM.js +7 -6
- package/src/plugins/hover/_listenDOM.js +2 -2
- package/src/plugins/key/_listenDOM.js +4 -4
- package/src/plugins/scroll/_listenDOM.js +1 -1
- package/test/01-general.test.js +1 -1
- package/test/02-key.test.js +45 -1
- package/test/03-click.test.js +51 -2
- package/test/04-form.test.js +26 -1
- package/test/05-hover.test.js +50 -2
- package/test/06-scroll.test.js +21 -0
- package/How.to.create.plugins.md +0 -929
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
# How to Create a Plugin for @peter.naydenov/shortcuts
|
|
2
|
+
|
|
3
|
+
> Audience: developers building a custom input source for the shortcuts library (touch gestures, gamepad, MIDI, custom DOM events, etc.).
|
|
4
|
+
|
|
5
|
+
This guide walks through the structure of a plugin. It assumes you are already familiar with [README.md](./README.md) — the public API, context/note model, action functions, and `data.dependencies.emit`.
|
|
6
|
+
|
|
7
|
+
The source code in `src/plugins/` is the canonical reference. The five shipped plugins (`key`, `click`, `hover`, `scroll`, `form`) follow the exact same contract. If you follow that contract, your plugin drops in alongside them with no changes to the core library.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## Table of Contents
|
|
12
|
+
1. [Mental Model](#mental-model)
|
|
13
|
+
2. [The Plugin Contract in 60 Seconds](#the-plugin-contract-in-60-seconds)
|
|
14
|
+
3. [The Plugin File Layout](#the-plugin-file-layout)
|
|
15
|
+
4. [Step-by-Step: A Minimal `pluginGamepad`](#step-by-step-a-minimal-plugingamepad)
|
|
16
|
+
5. [What `setupPlugin` injects into your pluginState](#what-setupplugin-injects-into-your-pluginstate)
|
|
17
|
+
6. [What `setupPlugin` injects into your `dependencies`](#what-setupplugin-injects-into-your-dependencies)
|
|
18
|
+
7. [The `data` object: what to put in it](#the-data-object-what-to-put-in-it)
|
|
19
|
+
8. [The `PREFIX:SETUP` per-context event](#the-prefixsetup-per-context-event)
|
|
20
|
+
9. [Naming shortcuts and the normalization rules](#naming-shortcuts-and-the-normalization-rules)
|
|
21
|
+
10. [Listening to the DOM safely](#listening-to-the-dom-safely)
|
|
22
|
+
11. [Reset, mute, unmute, destroy](#reset-mute-unmute-destroy)
|
|
23
|
+
12. [Distributing your plugin](#distributing-your-plugin)
|
|
24
|
+
13. [Full checklist](#full-checklist)
|
|
25
|
+
14. [Appendix: the shipped plugins at a glance](#appendix-the-shipped-plugins-at-a-glance)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
## Mental Model
|
|
30
|
+
|
|
31
|
+
A plugin is the layer that turns a **physical input** (keystroke, mouse click, scroll, hover, form change, …) into a **shortcut event** in the library's internal event emitter. Everything else — contexts, notes, action functions, `data.dependencies.emit`, pause/resume, mute/unmute — is the same for every plugin.
|
|
32
|
+
|
|
33
|
+
Your plugin has four jobs:
|
|
34
|
+
|
|
35
|
+
1. **Match** a shortcut name to your input family. (`'key:s+alt'` belongs to the `key` plugin, `'gamepad:a+start'` would belong to a `gamepad` plugin.)
|
|
36
|
+
2. **Normalize** the name the user wrote so that `'key:alt+s'` and `'key:s+alt'` are the same.
|
|
37
|
+
3. **Register** the user's shortcuts in the current context, including their `PREFIX:setup` options.
|
|
38
|
+
4. **Listen** to the DOM (or the device API) and emit a normalized event with a `data` payload.
|
|
39
|
+
|
|
40
|
+
The library takes care of the rest: switching contexts, muting the whole plugin, pausing individual shortcuts, error reporting, and exposing your events to `shortcuts.emit()`.
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
## The Plugin Contract in 60 Seconds
|
|
45
|
+
|
|
46
|
+
A plugin is **one function** that returns a frozen API. The function receives `setupPlugin` (provided by the core library) plus the user's `options`. You call `setupPlugin(...)` with six things and you get back a `pluginAPI` you can attach to the instance.
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
// src/plugins/myplugin/index.js
|
|
50
|
+
function pluginMy ( setupPlugin, options = {} ) {
|
|
51
|
+
const
|
|
52
|
+
deps = { resetState, regex: /MY\s*\:/i /*, helpers… */ }
|
|
53
|
+
, pluginState = { defaultOptions: {…}, listenOptions: {…}, /* state vars… */ }
|
|
54
|
+
;
|
|
55
|
+
|
|
56
|
+
return setupPlugin ({
|
|
57
|
+
prefix : 'my' // (1) one-word tag for shortcut names
|
|
58
|
+
, _normalizeShortcutName // (2) parse user-written names into canonical form
|
|
59
|
+
, _registerShortcutEvents // (3) walk the active context, apply PREFIX:setup, count shortcuts
|
|
60
|
+
, _listenDOM // (4) attach/detach DOM listeners; returns { start, stop }
|
|
61
|
+
, pluginState // (5) mutable per-plugin state; core injects 4 fields into it
|
|
62
|
+
, deps // (6) helpers + resetState; spread into the dependencies your listeners receive
|
|
63
|
+
});
|
|
64
|
+
} // pluginMy
|
|
65
|
+
|
|
66
|
+
export default pluginMy
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
That's the whole thing. The shipped plugins are 60-70 lines each.
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
## The Plugin File Layout
|
|
74
|
+
|
|
75
|
+
Every shipped plugin uses the same directory layout. You can use one file, but splitting them makes maintenance easier and matches the rest of the codebase.
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
src/plugins/my/
|
|
79
|
+
├── index.js // plugin factory (this is the only file required for import)
|
|
80
|
+
├── _normalizeShortcutName.js // (2) name normalization
|
|
81
|
+
├── _registerShortcutEvents.js // (3) per-context setup
|
|
82
|
+
├── _listenDOM.js // (4) DOM listeners
|
|
83
|
+
└── _optional helpers… // anything else you want in deps
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Conventions:
|
|
87
|
+
- Files starting with `_` are private to the plugin.
|
|
88
|
+
- The factory in `index.js` is the only thing you `export default`. It will be passed to `short.enablePlugin(pluginMy)`.
|
|
89
|
+
- Use `'use strict'` and ES module `import` syntax (the library ships ESM + CJS via a build).
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
## Step-by-Step: A Minimal `pluginGamepad`
|
|
94
|
+
|
|
95
|
+
Below is a complete, working example for a hypothetical gamepad plugin. It illustrates every part of the contract without the historical noise of the shipped plugins. After this section, the rest of the guide documents the contract in detail.
|
|
96
|
+
|
|
97
|
+
### 1. The factory
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
// src/plugins/gamepad/index.js
|
|
101
|
+
'use strict'
|
|
102
|
+
|
|
103
|
+
import _normalizeShortcutName from './_normalizeShortcutName.js'
|
|
104
|
+
import _registerShortcutEvents from './_registerShortcutEvents.js'
|
|
105
|
+
import _listenDOM from './_listenDOM.js'
|
|
106
|
+
|
|
107
|
+
function pluginGamepad ( setupPlugin, options = {} ) {
|
|
108
|
+
const
|
|
109
|
+
pluginState = {
|
|
110
|
+
active : false
|
|
111
|
+
, buffer : [] // accumulating button presses in a sequence
|
|
112
|
+
, defaultOptions: {
|
|
113
|
+
gamepadWait : 600 // ms between buttons in a sequence
|
|
114
|
+
}
|
|
115
|
+
, listenOptions : {
|
|
116
|
+
gamepadWait : 600
|
|
117
|
+
}
|
|
118
|
+
, ...(options || {}) // user overrides go straight into state
|
|
119
|
+
}
|
|
120
|
+
, deps = {
|
|
121
|
+
regex : /GAMEPAD\s*\:/i
|
|
122
|
+
, resetState: function () {
|
|
123
|
+
pluginState.active = false
|
|
124
|
+
pluginState.buffer = []
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
;
|
|
128
|
+
|
|
129
|
+
return setupPlugin ({
|
|
130
|
+
prefix : 'gamepad'
|
|
131
|
+
, _normalizeShortcutName
|
|
132
|
+
, _registerShortcutEvents
|
|
133
|
+
, _listenDOM
|
|
134
|
+
, pluginState
|
|
135
|
+
, deps
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export default pluginGamepad
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### 2. Normalize a shortcut name
|
|
143
|
+
|
|
144
|
+
`_normalizeShortcutName(name)` is called for **every** shortcut name across **every** context, by every plugin. Each plugin is responsible for recognizing its own prefix and returning the canonical form (or returning `name` unchanged if it doesn't belong to you).
|
|
145
|
+
|
|
146
|
+
```js
|
|
147
|
+
// src/plugins/gamepad/_normalizeShortcutName.js
|
|
148
|
+
'use strict'
|
|
149
|
+
|
|
150
|
+
function _normalizeShortcutName ( name ) {
|
|
151
|
+
const upper = name.toUpperCase()
|
|
152
|
+
if ( !/GAMEPAD\s*\:/i.test(upper) ) return name // not ours — pass through
|
|
153
|
+
if ( upper.includes('SETUP') ) return 'GAMEPAD:SETUP'
|
|
154
|
+
|
|
155
|
+
// Format: GAMEPAD:A+B,LEFT (commas separate sequence steps, + joins modifiers)
|
|
156
|
+
const body = upper.slice(upper.indexOf(':') + 1)
|
|
157
|
+
const steps = body.split(',').map( step =>
|
|
158
|
+
step.split('+').map( s => s.trim() ).sort().join('+')
|
|
159
|
+
)
|
|
160
|
+
return `GAMEPAD:${steps.join(',')}`
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export default _normalizeShortcutName
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**Rules of thumb:**
|
|
167
|
+
- The returned name **must** start with `PREFIX:` (uppercase prefix).
|
|
168
|
+
- Modifiers should be alphabetized (`shift+ctrl` not `ctrl+shift`) so the same shortcut always normalizes the same way.
|
|
169
|
+
- Return `'PREFIX:SETUP'` (uppercase) when the user wrote a setup event.
|
|
170
|
+
- Return `name` unchanged when it doesn't match your prefix.
|
|
171
|
+
|
|
172
|
+
### 3. Register shortcuts in the active context
|
|
173
|
+
|
|
174
|
+
`_registerShortcutEvents(deps, pluginState)` is called by the core every time the context changes. It walks the current context's shortcuts, applies the user's `PREFIX:setup` (if any), and returns the count of shortcuts that match this plugin. If the count is zero, the core will not call `listener.start()` — you don't have to guard against empty state yourself.
|
|
175
|
+
|
|
176
|
+
```js
|
|
177
|
+
// src/plugins/gamepad/_registerShortcutEvents.js
|
|
178
|
+
'use strict'
|
|
179
|
+
|
|
180
|
+
function _registerShortcutEvents ( deps, pluginState ) {
|
|
181
|
+
const
|
|
182
|
+
{ regex } = deps
|
|
183
|
+
, { currentContext, shortcuts
|
|
184
|
+
, defaultOptions, listenOptions
|
|
185
|
+
} = pluginState
|
|
186
|
+
;
|
|
187
|
+
|
|
188
|
+
if ( currentContext.name == null ) return 0
|
|
189
|
+
|
|
190
|
+
let count = 0
|
|
191
|
+
let hasSetup = false
|
|
192
|
+
|
|
193
|
+
Object.entries( shortcuts[currentContext.name] ).forEach( ([name, list]) => {
|
|
194
|
+
if ( !regex.test(name) ) return // not ours
|
|
195
|
+
|
|
196
|
+
if ( name === 'GAMEPAD:SETUP' ) {
|
|
197
|
+
hasSetup = true
|
|
198
|
+
// Each user's setup fn receives { dependencies, defaults, options }
|
|
199
|
+
// and returns a partial options object we merge into listenOptions.
|
|
200
|
+
const update = list.reduce( (res, fn) => {
|
|
201
|
+
const r = fn({
|
|
202
|
+
dependencies : deps.extra
|
|
203
|
+
, defaults : structuredClone(defaultOptions)
|
|
204
|
+
, options : listenOptions
|
|
205
|
+
})
|
|
206
|
+
return Object.assign(res, r)
|
|
207
|
+
}, defaultOptions)
|
|
208
|
+
Object.assign( listenOptions, update )
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
count++
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
if ( !hasSetup ) Object.assign( listenOptions, defaultOptions )
|
|
216
|
+
return count
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export default _registerShortcutEvents
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**Contract summary:**
|
|
223
|
+
- Walk `pluginState.shortcuts[ pluginState.currentContext.name ]`.
|
|
224
|
+
- Skip entries that don't match your `deps.regex`.
|
|
225
|
+
- If you see `'PREFIX:SETUP'`, call each handler with `{ dependencies, defaults, options }` and merge the returned object into `pluginState.listenOptions`.
|
|
226
|
+
- If the context has **no** setup event, copy `defaultOptions` into `listenOptions` so they're always populated.
|
|
227
|
+
- Return the count of registered shortcuts.
|
|
228
|
+
|
|
229
|
+
### 4. Listen to the device
|
|
230
|
+
|
|
231
|
+
`_listenDOM(deps, pluginState)` is called once at enable time and once on every context change (after the register step). It must return `{ start, stop }`. The core will call `start()` to begin listening and `stop()` to pause.
|
|
232
|
+
|
|
233
|
+
```js
|
|
234
|
+
// src/plugins/gamepad/_listenDOM.js
|
|
235
|
+
'use strict'
|
|
236
|
+
|
|
237
|
+
function _listenDOM ( deps, pluginState ) {
|
|
238
|
+
const { ev, extra } = deps
|
|
239
|
+
const { listenOptions, currentContext } = pluginState
|
|
240
|
+
|
|
241
|
+
function onGamepadEvent ( e ) {
|
|
242
|
+
// Build a canonical "GAMEPAD:A+B,LEFT" string from the device
|
|
243
|
+
const name = `GAMEPAD:${e.buttons.sort().join('+')}`
|
|
244
|
+
|
|
245
|
+
const data = {
|
|
246
|
+
type : 'gamepad'
|
|
247
|
+
, context : currentContext.name
|
|
248
|
+
, note : currentContext.note
|
|
249
|
+
, dependencies : extra
|
|
250
|
+
, event : e
|
|
251
|
+
, options : listenOptions
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
ev.emit( name, data ) // core + user listeners receive the event
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function start () {
|
|
258
|
+
if ( pluginState.active ) return
|
|
259
|
+
window.addEventListener( 'gamepadbuttondown', onGamepadEvent )
|
|
260
|
+
pluginState.active = true
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function stop () {
|
|
264
|
+
if ( !pluginState.active ) return
|
|
265
|
+
window.removeEventListener( 'gamepadbuttondown', onGamepadEvent )
|
|
266
|
+
pluginState.active = false
|
|
267
|
+
pluginState.buffer = [] // clear any in-flight state
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return { start, stop }
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export default _listenDOM
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
**Contract summary:**
|
|
277
|
+
- Return `{ start, stop }`.
|
|
278
|
+
- `start()` adds your event listeners, sets `pluginState.active = true`. Must be idempotent (early return if already active).
|
|
279
|
+
- `stop()` removes listeners, sets `pluginState.active = false`, clears any pending timers. Must be idempotent.
|
|
280
|
+
- Inside your listeners, build a `data` object (see [next section](#the-data-object-what-to-put-in-it)) and call `ev.emit('PREFIX:EVENT_NAME', data)`.
|
|
281
|
+
- The `PREFIX` part of the emitted name must be uppercase.
|
|
282
|
+
|
|
283
|
+
### 5. Wire it up
|
|
284
|
+
|
|
285
|
+
```js
|
|
286
|
+
import { shortcuts, pluginGamepad } from '@my-scope/shortcut-plugin-gamepad'
|
|
287
|
+
|
|
288
|
+
const short = shortcuts()
|
|
289
|
+
short.enablePlugin( pluginGamepad )
|
|
290
|
+
|
|
291
|
+
short.load({
|
|
292
|
+
game: {
|
|
293
|
+
'gamepad:setup': ({ defaults }) => ({ gamepadWait: 300 }),
|
|
294
|
+
'gamepad: a, b, start': () => console.log('Combo A → B → Start')
|
|
295
|
+
}
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
short.changeContext('game')
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
That's the full plugin. Drop the four files into your package, add it to your app, and the rest of the library (`pause`/`resume`, `mute`/`unmute`, `reset`, `getDependencies`, …) works for your input source with no extra code.
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
## What `setupPlugin` injects into your pluginState
|
|
306
|
+
|
|
307
|
+
`setupPlugin` (provided by the core) **augments your `pluginState` object** with four references from the library before calling your `_registerShortcutEvents` and `_listenDOM`. Treat them as read-only unless you know what you're doing.
|
|
308
|
+
|
|
309
|
+
| Field | Type | What it is |
|
|
310
|
+
|---|---|---|
|
|
311
|
+
| `currentContext` | `{ name: string \| null, note: string \| null }` | The live context descriptor. `name` is `null` when no context is active. |
|
|
312
|
+
| `shortcuts` | `object` | The full shortcuts registry: `{ contextName: { shortcutName: [fn, …] } }`. Reading from it is fine. Mutating it can desync the library. |
|
|
313
|
+
| `exposeShortcut` | `function \| false` | The user's `onShortcut` constructor option, or `false` if they didn't set one. The core wires this up to fire on every emitted event. |
|
|
314
|
+
| `ERROR_EVENT_NAME` | `string` | The configured error event name (default `'@shortcuts-error'`). Emit here with `ev.emit(ERROR_EVENT_NAME, 'message')`. |
|
|
315
|
+
|
|
316
|
+
The core **also overrides** these four on your state when the context changes:
|
|
317
|
+
|
|
318
|
+
```js
|
|
319
|
+
pluginState.currentContext // updated to the new context descriptor
|
|
320
|
+
pluginState.shortcuts // same reference, content may have changed via load/unload
|
|
321
|
+
pluginState.exposeShortcut // only set once at enable
|
|
322
|
+
pluginState.ERROR_EVENT_NAME // only set once at enable
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
You don't need to read or write any of them — but you can use `currentContext.name` and `currentContext.note` in your `data` payload (the library's other plugins do).
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
## What `setupPlugin` injects into your `dependencies`
|
|
330
|
+
|
|
331
|
+
`setupPlugin` spreads your `deps` and adds two library-owned fields. Your `_registerShortcutEvents` and `_listenDOM` receive the merged object as their first argument.
|
|
332
|
+
|
|
333
|
+
| Field | Type | What it is |
|
|
334
|
+
|---|---|---|
|
|
335
|
+
| `ev` | `object` | The library's event emitter ([`@peter.naydenov/notice`](https://github.com/PeterNaydenov/notice)). Has `.emit(name, …args)`, `.on(name, fn)`, `.off(name, fn)`, `.reset()`, `.start(name)`, `.stop(name)`. |
|
|
336
|
+
| `extra` | `object` | The bag the user registered via `shortcuts.setDependencies({…})`. Pass this through into your `data.dependencies` so user action functions can read it. |
|
|
337
|
+
| `…your deps` | | Everything you put in your `deps` object — `resetState`, `regex`, helper functions, etc. |
|
|
338
|
+
|
|
339
|
+
The `regex` field is conventional but not enforced. The library uses it to identify your shortcuts in `_registerShortcutEvents`. Pick a regex that matches your `PREFIX:` exactly (case-insensitive with whitespace tolerance) and nothing else.
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
## The `data` object: what to put in it
|
|
344
|
+
|
|
345
|
+
The second argument to `ev.emit('PREFIX:NAME', data)` is the object every user action function receives. The contract is not enforced, but staying close to what the shipped plugins do makes your plugin feel native to users.
|
|
346
|
+
|
|
347
|
+
**Always include:**
|
|
348
|
+
- `type` — your plugin name as a string (e.g. `'key'`, `'click'`, `'gamepad'`). Used internally to tag the event.
|
|
349
|
+
- `context` — `pluginState.currentContext.name` at emit time.
|
|
350
|
+
- `note` — `pluginState.currentContext.note` (or `null`).
|
|
351
|
+
- `dependencies` — `deps.extra`. The library bakes `emit: ev.emit` into `extra` by default, so users get `data.dependencies.emit(eventName, ...args)` for free. Anything the host app adds via `setDependencies({...})` is merged into the same bag, so users can also call `data.dependencies.api.foo()` from their action.
|
|
352
|
+
- `options` — `pluginState.listenOptions` (the live options for the active context).
|
|
353
|
+
|
|
354
|
+
**Include when relevant:**
|
|
355
|
+
- `target` — the DOM element the event came from.
|
|
356
|
+
- `event` — the raw DOM/device event.
|
|
357
|
+
- `x` / `y` / `viewport` — coordinate data, mirroring the shipped `click`/`hover` plugins.
|
|
358
|
+
- `wait` / `end` / `ignore` / `isWaiting` — only meaningful for sequence-style plugins like `key`.
|
|
359
|
+
|
|
360
|
+
**Avoid putting:**
|
|
361
|
+
- Plugin-internal state (timers, buffers, etc.). Users will see it and rely on it, locking you in.
|
|
362
|
+
- The full `ev` object. The library already exposes `ev.emit` to user actions via `data.dependencies.emit` (it bakes `emit: ev.emit` into `extra` for you). Do not add a top-level `emit` field of your own — duplicating it will only confuse readers and break if the library's wrapping changes.
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
## The `PREFIX:SETUP` per-context event
|
|
367
|
+
|
|
368
|
+
A `PREFIX:setup` event inside a context is the user's way to override your plugin's defaults **for that context only**. It's the preferred customization mechanism — see [README → Per-Context Setup vs `enablePlugin` Options](./README.md#per-context-setup-vs-enableplugin-options).
|
|
369
|
+
|
|
370
|
+
Your plugin must support it in `_registerShortcutEvents`. The pattern is:
|
|
371
|
+
|
|
372
|
+
```js
|
|
373
|
+
if ( shortcutName === 'PREFIX:SETUP' ) {
|
|
374
|
+
const update = list.reduce( (res, fn) => {
|
|
375
|
+
const r = fn({
|
|
376
|
+
dependencies : deps.extra // user-set deps
|
|
377
|
+
, defaults : structuredClone(pluginState.defaultOptions) // clone — never mutate
|
|
378
|
+
, options : pluginState.listenOptions // live ref to per-context options
|
|
379
|
+
})
|
|
380
|
+
return Object.assign(res, r) // shallow merge into res
|
|
381
|
+
}, pluginState.defaultOptions) // initial value = defaults
|
|
382
|
+
|
|
383
|
+
Object.assign(pluginState.listenOptions, update) // final write to listenOptions
|
|
384
|
+
return
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
**Key invariants:**
|
|
389
|
+
- The user gets `defaults` (a fresh `structuredClone` of your `defaultOptions`) and `options` (the live `listenOptions`). They may return a partial object with only the keys they want to override.
|
|
390
|
+
- The returned object is shallow-merged first across all setup functions (so order matters if the user defines multiple `PREFIX:setup` handlers), then into `listenOptions`.
|
|
391
|
+
- If the context does **not** define a `PREFIX:setup`, copy `defaultOptions` into `listenOptions` so they're always populated. Otherwise downstream code may read `undefined` options.
|
|
392
|
+
|
|
393
|
+
> The `form` plugin handles this slightly differently because it does not have per-action options — it returns the `defaults` object unchanged when no setup is provided. The `key`/`click`/`hover`/`scroll` plugins follow the pattern above.
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
## Naming shortcuts and the normalization rules
|
|
398
|
+
|
|
399
|
+
A user writes shortcut names like:
|
|
400
|
+
|
|
401
|
+
```js
|
|
402
|
+
'key: ctrl+s' // 'key' is the prefix; 'ctrl+s' is the body
|
|
403
|
+
'click: left-2-ctrl' // 'click' prefix; 'left-2-ctrl' body
|
|
404
|
+
'form:action' // 'form' prefix; one of the form lifecycle events
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
After your `_normalizeShortcutName` runs, the names look like:
|
|
408
|
+
|
|
409
|
+
```js
|
|
410
|
+
'KEY:CTRL+S'
|
|
411
|
+
'CLICK:CTRL-LEFT-2' // modifiers alphabetized, button+count first
|
|
412
|
+
'FORM:ACTION'
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
**Rules:**
|
|
416
|
+
- Normalization runs at `load()` time on **every** shortcut name in **every** context — not just the active one. This is so that `shortcuts.emit('KEY:CTRL+S')` (with any spelling) matches.
|
|
417
|
+
- Each plugin receives every name. Yours must return the input unchanged when it doesn't match your prefix.
|
|
418
|
+
- Modifiers should be sorted alphabetically (`ctrl+shift+s`, not `shift+ctrl+s`).
|
|
419
|
+
- Always uppercase your prefix and the parts that should be uppercase. Lowercase parts (e.g. raw key names) are kept as the user wrote them.
|
|
420
|
+
- Return `'PREFIX:SETUP'` (uppercase) for the setup event, no matter how the user wrote it.
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
## Listening to the DOM safely
|
|
425
|
+
|
|
426
|
+
A few rules the shipped plugins follow — copy them.
|
|
427
|
+
|
|
428
|
+
**Idempotency.** `start()` and `stop()` can be called multiple times. Use `pluginState.active` as a guard:
|
|
429
|
+
|
|
430
|
+
```js
|
|
431
|
+
function start () {
|
|
432
|
+
if ( pluginState.active ) return
|
|
433
|
+
document.addEventListener( '…', handler )
|
|
434
|
+
pluginState.active = true
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function stop () {
|
|
438
|
+
if ( !pluginState.active ) return
|
|
439
|
+
document.removeEventListener( '…', handler )
|
|
440
|
+
pluginState.active = false
|
|
441
|
+
clearTimeout( timer ) // clear any pending timers
|
|
442
|
+
timer = null
|
|
443
|
+
}
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
**Clear timers in `stop`.** Any `setTimeout` / `setInterval` you start inside your listeners must be cleared in `stop()`, otherwise a muted plugin will still fire late. The shipped plugins use a `pluginState.wait[ type ]` for per-type throttles — a plain object on state is fine.
|
|
447
|
+
|
|
448
|
+
**Don't read from `window` at module load.** Reading `window.innerWidth` at import time breaks SSR and tests. Read inside the listener / inside `start()`.
|
|
449
|
+
|
|
450
|
+
**Don't attach global listeners that fire when the user is typing in an input.** The shipped plugins don't filter for this, but it's good practice to add an `isContentEditable` check or look at `event.target` if your plugin would conflict with text input. The `form` plugin only attaches to focus and input events, so it doesn't have this concern.
|
|
451
|
+
|
|
452
|
+
**Cleanup in `resetState`.** Add a `resetState` function to your `deps` that resets everything in `pluginState` to its initial values. The core calls it on every `contextChange` (see [below](#reset-mute-unmute-destroy)).
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
## Reset, mute, unmute, destroy
|
|
457
|
+
|
|
458
|
+
The core calls these four methods on your returned `pluginAPI` (which is what `setupPlugin` constructs for you). You don't write them — `setupPlugin` writes them based on your `deps.resetState` and your `_listenDOM`'s `{start, stop}`.
|
|
459
|
+
|
|
460
|
+
| Method | When the core calls it | What it does |
|
|
461
|
+
|---|---|---|
|
|
462
|
+
| `resetState()` (your `deps`) | On every `contextChange`, on `mute`/`unmute` cycles if you choose, and on `destroy` | Resets your `pluginState` to its initial shape. **You write this.** It typically zeroes counters, clears buffers, and may also re-run defaults into `listenOptions`. |
|
|
463
|
+
| `pluginAPI.mute()` | `short.mutePlugin('prefix')` | Calls your `listener.stop()`. Your `resetState` is **not** called. |
|
|
464
|
+
| `pluginAPI.unmute()` | `short.unmutePlugin('prefix')` | Calls your `listener.start()`. |
|
|
465
|
+
| `pluginAPI.destroy()` | `short.disablePlugin('prefix')` or `short.reset()` | Calls your `listener.stop()` **and** your `resetState()`. After this, the plugin is gone. To bring it back, call `enablePlugin` again. |
|
|
466
|
+
| `pluginAPI.contextChange()` | Every `short.changeContext(name)` | Calls your `resetState()`, then re-runs `_registerShortcutEvents`, then either `listener.stop()` or `listener.start()` depending on the count. **Your listeners are re-attached** so any new `PREFIX:setup` options in the new context take effect. |
|
|
467
|
+
| `pluginAPI.shortcutName(name)` | `shortcuts.emit(name, …)` | Calls your `_normalizeShortcutName` so the user can pass any spelling. |
|
|
468
|
+
| `pluginAPI.getPrefix()` | `short.listPlugins()` | Returns your `prefix` string. |
|
|
469
|
+
|
|
470
|
+
**You write `resetState`.** Keep it focused: clear timers, clear buffers, reset any counters. Don't reset `defaultOptions` / `listenOptions` — those are managed by the core.
|
|
471
|
+
|
|
472
|
+
```js
|
|
473
|
+
// inside pluginMy / index.js
|
|
474
|
+
function resetState () {
|
|
475
|
+
pluginState.active = false
|
|
476
|
+
pluginState.buffer = []
|
|
477
|
+
clearTimeout( pluginState.timer )
|
|
478
|
+
pluginState.timer = null
|
|
479
|
+
}
|
|
480
|
+
deps.resetState = resetState
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
## Distributing your plugin
|
|
486
|
+
|
|
487
|
+
A plugin is just a function. You can publish it as a normal npm package and the consumer does:
|
|
488
|
+
|
|
489
|
+
```js
|
|
490
|
+
import { shortcuts } from '@peter.naydenov/shortcuts'
|
|
491
|
+
import { pluginGamepad } from '@my-scope/shortcut-plugin-gamepad'
|
|
492
|
+
|
|
493
|
+
const short = shortcuts()
|
|
494
|
+
short.enablePlugin( pluginGamepad )
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
Recommended package layout:
|
|
498
|
+
|
|
499
|
+
```
|
|
500
|
+
shortcut-plugin-gamepad/
|
|
501
|
+
├── package.json
|
|
502
|
+
│ ├── name: '@my-scope/shortcut-plugin-gamepad'
|
|
503
|
+
│ ├── main: 'dist/index.cjs'
|
|
504
|
+
│ ├── module: 'dist/index.js'
|
|
505
|
+
│ ├── types: 'dist/index.d.ts'
|
|
506
|
+
│ └── peerDependencies: { '@peter.naydenov/shortcuts': '^4.1.0' }
|
|
507
|
+
├── src/
|
|
508
|
+
│ └── plugins/gamepad/ // the files from this guide
|
|
509
|
+
├── dist/ // build output
|
|
510
|
+
└── README.md // document your prefix, options, action-function `data` shape
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
**Document for your users:**
|
|
514
|
+
1. The prefix (`gamepad`).
|
|
515
|
+
2. The `data` shape they receive in action functions (a TypeScript `type` is best).
|
|
516
|
+
3. The `PREFIX:setup` options and their defaults.
|
|
517
|
+
4. The shortcut name syntax (how to combine buttons, modifiers, sequences).
|
|
518
|
+
5. Any browser/device support caveats.
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
## Full checklist
|
|
523
|
+
|
|
524
|
+
Before publishing your plugin, verify each item.
|
|
525
|
+
|
|
526
|
+
**Contract**
|
|
527
|
+
- [ ] Factory is `(setupPlugin, options = {})` and returns the result of `setupPlugin({…})`.
|
|
528
|
+
- [ ] All six fields are passed to `setupPlugin`: `prefix`, `_normalizeShortcutName`, `_registerShortcutEvents`, `_listenDOM`, `pluginState`, `deps`.
|
|
529
|
+
- [ ] `pluginState` has `defaultOptions` and `listenOptions` (initialized from the same object).
|
|
530
|
+
- [ ] `deps.resetState` is defined and resets only mutable runtime state.
|
|
531
|
+
- [ ] `deps.regex` matches your prefix case-insensitively with optional whitespace.
|
|
532
|
+
|
|
533
|
+
**Naming**
|
|
534
|
+
- [ ] `_normalizeShortcutName` returns input unchanged when the prefix doesn't match.
|
|
535
|
+
- [ ] `_normalizeShortcutName` returns `'PREFIX:SETUP'` for any setup spelling.
|
|
536
|
+
- [ ] Normalized names are uppercase, modifier-sorted, sequence-aware.
|
|
537
|
+
|
|
538
|
+
**Registration**
|
|
539
|
+
- [ ] `_registerShortcutEvents` handles `PREFIX:SETUP` correctly: passes `{dependencies, defaults, options}` to each handler, merges all returned objects, applies to `listenOptions`.
|
|
540
|
+
- [ ] When no setup is defined, `defaultOptions` are copied into `listenOptions`.
|
|
541
|
+
- [ ] Returns the count of registered shortcuts for the active context.
|
|
542
|
+
|
|
543
|
+
**Listening**
|
|
544
|
+
- [ ] `_listenDOM` returns `{ start, stop }`.
|
|
545
|
+
- [ ] `start()` and `stop()` are idempotent.
|
|
546
|
+
- [ ] All timers/intervals are cleared in `stop()`.
|
|
547
|
+
- [ ] `pluginState.active` is set in `start()` and cleared in `stop()`.
|
|
548
|
+
- [ ] Emitted events have a `data` object that includes `type`, `context`, `note`, `dependencies`, `options`. (`emit` is already provided by the library via `data.dependencies.emit` — no need to add it yourself.)
|
|
549
|
+
|
|
550
|
+
**Behavior**
|
|
551
|
+
- [ ] `mutePlugin` stops the listener; `unmutePlugin` restarts it.
|
|
552
|
+
- [ ] `disablePlugin` / `reset` triggers `resetState`.
|
|
553
|
+
- [ ] `changeContext` re-registers and re-attaches the listener.
|
|
554
|
+
|
|
555
|
+
**Distribution**
|
|
556
|
+
- [ ] Package peer-depends on `@peter.naydenov/shortcuts`.
|
|
557
|
+
- [ ] README documents the `data` shape, options, and shortcut syntax.
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
## Appendix: the shipped plugins at a glance
|
|
562
|
+
|
|
563
|
+
| Plugin | Prefix | Regex | Default options | Sequence? | Per-action `data` extras |
|
|
564
|
+
|---|---|---|---|---|---|
|
|
565
|
+
| `key` | `key` | `/KEY\s*:/i` | `keyWait: 480` | yes (`,` separator) | `wait`/`end`/`ignore`/`isWaiting`, `viewport` |
|
|
566
|
+
| `click` | `click` | `/CLICK\s*:/i` | `mouseWait: 320`, `clickTarget: ['data-click','href']` | no | `target`, `x`, `y`, `event` |
|
|
567
|
+
| `hover` | `hover` | `/HOVER\s*:/i` | `wait: 320`, `hoverTarget: ['data-hover']` | no | `target`, `x`, `y`, `event` |
|
|
568
|
+
| `scroll` | `scroll` | `/SCROLL\s*:/i` | `scrollWait: 50`, `endScrollWait: 400`, `minSpace: 40` | no | `event` |
|
|
569
|
+
| `form` | `form` | `/FORM\s*:/i` | (none) | no | `target`, `type`/`timing`/`wait`, `viewport`, `position`, `sizes`, `pagePosition` |
|
|
570
|
+
|
|
571
|
+
**The `form` plugin is the most different.** It does not have per-action `defaultOptions` (the wait is per-type, configured by the user's `form:action` return). Its `_registerShortcutEvents` reads `form:watch`, `form:define`, and `form:action` from the context and wires them up internally. If your plugin needs a multi-handler lifecycle, study `src/plugins/form/` for the closest pattern.
|
|
572
|
+
|
|
573
|
+
**The `key` plugin is the closest template for any "sequence of inputs"** — e.g. chord detection on a gamepad, multi-step voice commands, gesture sequences. Its `wait`/`end`/`ignore` helpers are documented in README under [Action Functions → Keyboard Action Functions](./README.md#keyboard-action-functions).
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peter.naydenov/shortcuts",
|
|
3
3
|
"description": "Context control of shortcuts based on keyboard and mouse events",
|
|
4
|
-
"version": "4.0
|
|
4
|
+
"version": "4.1.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Peter Naydenov",
|
|
7
7
|
"main": "./src/main.js",
|
|
@@ -32,31 +32,29 @@
|
|
|
32
32
|
"url": "git+https://github.com/PeterNaydenov/shortcuts"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@peter.naydenov/notice": "^2.4.
|
|
35
|
+
"@peter.naydenov/notice": "^2.4.2"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@peter.naydenov/visual-controller-for-react": "^3.
|
|
39
|
-
"@rollup/plugin-commonjs": "^29.0.
|
|
38
|
+
"@peter.naydenov/visual-controller-for-react": "^3.1.2",
|
|
39
|
+
"@rollup/plugin-commonjs": "^29.0.3",
|
|
40
40
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
41
41
|
"@rollup/plugin-terser": "^1.0.0",
|
|
42
42
|
"@testing-library/dom": "^10.4.1",
|
|
43
43
|
"@testing-library/user-event": "^14.6.1",
|
|
44
|
-
"@vitejs/plugin-react": "^6.0.
|
|
45
|
-
"@vitest/browser": "^4.
|
|
46
|
-
"@vitest/browser-playwright": "^4.
|
|
47
|
-
"@vitest/coverage-v8": "^4.
|
|
48
|
-
"@vitest/ui": "^4.
|
|
49
|
-
"ask-for-promise": "^3.1.
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"vite": "^8.0.2",
|
|
59
|
-
"vitest": "^4.0.18"
|
|
44
|
+
"@vitejs/plugin-react": "^6.0.2",
|
|
45
|
+
"@vitest/browser": "^4.1.8",
|
|
46
|
+
"@vitest/browser-playwright": "^4.1.8",
|
|
47
|
+
"@vitest/coverage-v8": "^4.1.8",
|
|
48
|
+
"@vitest/ui": "^4.1.8",
|
|
49
|
+
"ask-for-promise": "^3.1.1",
|
|
50
|
+
"eslint": "^10.4.1",
|
|
51
|
+
"playwright": "^1.60.0",
|
|
52
|
+
"react": "^19.2.6",
|
|
53
|
+
"react-dom": "^19.2.7",
|
|
54
|
+
"rollup": "^4.61.1",
|
|
55
|
+
"typescript": "^6.0.3",
|
|
56
|
+
"vite": "^8.0.16",
|
|
57
|
+
"vitest": "^4.1.8"
|
|
60
58
|
},
|
|
61
59
|
"keywords": [
|
|
62
60
|
"shortcut",
|
|
@@ -64,5 +62,9 @@
|
|
|
64
62
|
"keyboard",
|
|
65
63
|
"mouse",
|
|
66
64
|
"click"
|
|
67
|
-
]
|
|
65
|
+
],
|
|
66
|
+
"allowScripts": {
|
|
67
|
+
"fsevents@2.3.2": true,
|
|
68
|
+
"fsevents@2.3.3": true
|
|
69
|
+
}
|
|
68
70
|
}
|