@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.
Files changed (89) hide show
  1. package/AGENTS.md +54 -0
  2. package/Changelog.md +14 -0
  3. package/README.md +935 -426
  4. package/dist/plugins/click/_listenDOM.d.ts +2 -2
  5. package/dist/plugins/form/_listenDOM.d.ts +4 -4
  6. package/dist/plugins/hover/_listenDOM.d.ts +2 -2
  7. package/dist/plugins/key/_listenDOM.d.ts +2 -2
  8. package/dist/plugins/scroll/_listenDOM.d.ts +2 -2
  9. package/dist/shortcuts.cjs +1 -1
  10. package/dist/shortcuts.esm.mjs +1 -1
  11. package/dist/shortcuts.umd.js +1 -1
  12. package/graphify-out/.graphify_benchmark.json +35 -0
  13. package/graphify-out/.graphify_python +1 -0
  14. package/graphify-out/GRAPH_REPORT.md +121 -0
  15. package/graphify-out/cache/024633ee076562ce0dbc0bf3b5315906f4e5d48c57bc96856b98cd5d42a69009.json +1 -0
  16. package/graphify-out/cache/037d2ba5552034c06beca677b1914ac7a053f696dfeaf12ca0de06eb42659f18.json +1 -0
  17. package/graphify-out/cache/055bfc2a3bfb8747f7ee0b7335704d2dff1c071d25a9bdd4f954e4cc2d630772.json +1 -0
  18. package/graphify-out/cache/0b2f626892ed04f158e39593dbf5f266c2a5982a21b2dddb144edc79af81e2d1.json +1 -0
  19. package/graphify-out/cache/0c17fe866170c4ab4023ac0680d1e05c5dc232a17ac0f08dfa1e76c2eaf75c44.json +1 -0
  20. package/graphify-out/cache/1392dca26291b5396829f1a996aac2c1d34a03d134a5fcf54e4f15824ee74e2b.json +1 -0
  21. package/graphify-out/cache/18113cd08ebffb11ed91ffab9c6d34795e22bfb5993941db07a52eed6eba45b8.json +1 -0
  22. package/graphify-out/cache/1a7a71b157cadd117435818e1a6561157c2930c4066d3a207fe04e318f76b296.json +1 -0
  23. package/graphify-out/cache/2448db4b822a94d6f3512ce8788077f35dfb567aef8628a846fad841b40575e7.json +1 -0
  24. package/graphify-out/cache/2592868b7b9d2de3f2cb575b7bd68ca2f252d34f71c12f2e8721d789cfbfbf88.json +1 -0
  25. package/graphify-out/cache/2aaf58292523f67421e6f728fd97740c5bf07dd903cf267cf557a4383759ba5e.json +1 -0
  26. package/graphify-out/cache/349a418954e66e5ef45370dca740ebff559a72d11f5810f6d40c0af14ef768e0.json +1 -0
  27. package/graphify-out/cache/361eacb4abb14862b75257bdf673a353826bf5764fb187ccde94ae21454bcf99.json +1 -0
  28. package/graphify-out/cache/40b4c82a11a2a31b279563c143d91d7894eb3f3d0c386f8323cb8062bcdfadd5.json +1 -0
  29. package/graphify-out/cache/40db98ff036ad694953cb13628795fabc0c5892ff093dfdcc18a309ac0d10846.json +1 -0
  30. package/graphify-out/cache/43a590ac22f05be183ea1a4655922185595c969f79dd3df3d44d1c7e49355785.json +1 -0
  31. package/graphify-out/cache/4993a6e98dcf33bb6fa78341b4eeac4776e2322f10522eecdc5195aa7969f35a.json +1 -0
  32. package/graphify-out/cache/5095c6e52a24f4ecec9acf63835761ad508df88d56b1799faec47672fbd4e348.json +1 -0
  33. package/graphify-out/cache/5cba1d38ffea01d8c62e5a0b0d8b164cf9b115ff1b6f1acc606f78877712d5de.json +1 -0
  34. package/graphify-out/cache/67aadf0b8f90224cb725e903e6fedbf6828f203467a633f98031b0740930cb21.json +1 -0
  35. package/graphify-out/cache/68dffbffbd811942d85ef2600ca31e423d3c3c343de98cbba4c954c94dd11470.json +1 -0
  36. package/graphify-out/cache/69c684e506b9ce22e95aff044c1da4b0f88ef72c510da02a739edbe551372a8e.json +1 -0
  37. package/graphify-out/cache/6e336587fcfe2b298f23326e881f048d37f1bb062d00806c338da58a8fd281ca.json +1 -0
  38. package/graphify-out/cache/78d88737e6db913f091c4c48e96953df065f6808e2b60dd828fdee64067dac91.json +1 -0
  39. package/graphify-out/cache/7e5340b989299a0b7217d1fd0d2b919cba65142f3e468b0aca5f4ffd5c0594d8.json +1 -0
  40. package/graphify-out/cache/813d6bac52066ed8733781c35710ecb7995e6cabbe0d9abb9854e3c67610b974.json +1 -0
  41. package/graphify-out/cache/8180f34832546e874bc5f1931eee545d97300be49faac5c9b6d515653a763324.json +1 -0
  42. package/graphify-out/cache/885894ca90af6a3724182762bc4fc7ff7d22727a931d46fe7593d1eea10c0c71.json +1 -0
  43. package/graphify-out/cache/8a304ae8f6bf02bfa40923cdbea99e4bea943db52c185f22caa43ba7c34f94d3.json +1 -0
  44. package/graphify-out/cache/903a7dea28112a27dcc1b9ece66514f4d5dd6ca264f5ee70835aca069a8df2ad.json +1 -0
  45. package/graphify-out/cache/9831a7833c5bfeb9a0611e416f7038bd37884b42a9a720f9b4c0a01f860a4f54.json +1 -0
  46. package/graphify-out/cache/98dbdcdd1b19bc942850f50b1ebdeb1865c72ba724990217464efd28a3732b32.json +1 -0
  47. package/graphify-out/cache/a2459f621d588f0166ae6a4204bb6b89f9d669b3ad0c54a88afac6c7abb134b4.json +1 -0
  48. package/graphify-out/cache/a25d47ecf087fa6888d641f89f08cefd35c68b5823c8c55b3baa0243ab110110.json +1 -0
  49. package/graphify-out/cache/a3bd22d8493943a3195c3ef1254a7240624a962edf2baa2c30eb0ae60564fbe7.json +1 -0
  50. package/graphify-out/cache/a4d4fb674183a3b348f542b1b9fb9c0d7b176c43636afb2554af088a9613a1c0.json +1 -0
  51. package/graphify-out/cache/a87a705106773b14c5a25697d30c743cdab01df551cdd9892d6ec46f98ad1659.json +1 -0
  52. package/graphify-out/cache/a9416d0397b5fb994b8c3847aea2599a9d33940e6f0652accc5ba1de478349ee.json +1 -0
  53. package/graphify-out/cache/ab03b9df0e9b8a74db3782c96fee833d800d93838fc0c056306ac2ef9a3e0c09.json +1 -0
  54. package/graphify-out/cache/ad3a99182567225cc19374c28d33097f146547bd945967c723b66d1065134ce9.json +1 -0
  55. package/graphify-out/cache/aece91cfc3a5181bbb77a1758921dfb6a323ab04cc402ce42f2832446d04f420.json +1 -0
  56. package/graphify-out/cache/bd65fd515423e8964058f6aa997c05e3e0fb9e6d39209d4a1d76a079c6af46e8.json +1 -0
  57. package/graphify-out/cache/c2a85071784f9516ab2dea976eeb3a514a53b15701bbf60b4d8be6cd3385cd6c.json +1 -0
  58. package/graphify-out/cache/c9a8c9342926031f308af0eb0a8d60cf0b443e84bae839da42998956465e47e6.json +1 -0
  59. package/graphify-out/cache/d05c0aa647a624e0c696f53c027d066c35d0893695e9a23fd820235ee86b4a70.json +1 -0
  60. package/graphify-out/cache/d3d9832015ab51f52ae88375cea2cbeabecd4a000578e28e899ce23e74245733.json +1 -0
  61. package/graphify-out/cache/d449ad503a40840d41cbf24ed57f408bf5fdf891f830990f836cf52da5c605eb.json +1 -0
  62. package/graphify-out/cache/d92b22194973f3c39ac53d85a29f5d4837d07b0f9f0d375e3ddce8da158777fb.json +1 -0
  63. package/graphify-out/cache/dda8f89f688d8a4db8b7279031ad26a0d8d4accc0aa049abda5fa19eac4bd5ef.json +1 -0
  64. package/graphify-out/cache/e1d80dbca10b7e2ba65339eff0649699c6091d30b836a1e9d5d094bb95aacc48.json +1 -0
  65. package/graphify-out/cache/e207108277cbe1af0501688b0268fea879d0414424386fbaa93a5861f306bdba.json +1 -0
  66. package/graphify-out/cache/e6032dad287da859a517d6b59105595db90e81833dbd850b37653bbd0f3acef7.json +1 -0
  67. package/graphify-out/cache/f49d7295a833de68579e0e265832bc78d21e901764e31705423e621a703124dc.json +1 -0
  68. package/graphify-out/cache/fcf90a1251a332948a773c6aaaad4ce7f6de8d2f2333687cb2fe94e0d860a6c9.json +1 -0
  69. package/graphify-out/cache/fe06fcb623d36858b89c8741696482530a084f599d48bea88de7943fae0f9bea.json +1 -0
  70. package/graphify-out/cache/ffa0f819e023809d17aac1af75cf0f6fbf08500615aee27341b658f24357105a.json +1 -0
  71. package/graphify-out/cost.json +12 -0
  72. package/graphify-out/graph.html +266 -0
  73. package/graphify-out/graph.json +634 -0
  74. package/graphify-out/manifest.json +124 -0
  75. package/how-to-create-a-plugin.md +573 -0
  76. package/package.json +23 -21
  77. package/src/main.js +5 -5
  78. package/src/plugins/click/_listenDOM.js +3 -3
  79. package/src/plugins/form/_listenDOM.js +7 -6
  80. package/src/plugins/hover/_listenDOM.js +2 -2
  81. package/src/plugins/key/_listenDOM.js +4 -4
  82. package/src/plugins/scroll/_listenDOM.js +1 -1
  83. package/test/01-general.test.js +1 -1
  84. package/test/02-key.test.js +45 -1
  85. package/test/03-click.test.js +51 -2
  86. package/test/04-form.test.js +26 -1
  87. package/test/05-hover.test.js +50 -2
  88. package/test/06-scroll.test.js +21 -0
  89. package/How.to.create.plugins.md +0 -929
@@ -1,929 +0,0 @@
1
- # How to Create a Plugin for Shortcuts
2
-
3
- This guide will walk you through creating a new plugin for the shortcuts library. We'll examine the existing plugins to understand the architecture and patterns, then create a step-by-step guide for building your own plugin.
4
-
5
- ## 🚀 Important: Plugins are Independent Projects
6
-
7
- **Plugins are designed to be standalone projects**, not part of the main shortcuts library. This intentional architecture allows:
8
-
9
- - **Independent Distribution**: Publish plugins as separate npm packages
10
- - **Modular Usage**: Users only install the plugins they need
11
- - **Third-Party Development**: Anyone can create and distribute plugins
12
- - **Version Independence**: Plugins can have their own release cycles
13
-
14
- **Usage Pattern:**
15
- ```javascript
16
- import { shortcuts } from '@peter.naydenov/shortcuts'
17
- import pluginTouch from 'shortcuts-touch-plugin' // Separate package
18
- import pluginVoice from 'my-custom-voice-plugin' // Your own plugin
19
-
20
- const short = shortcuts()
21
- short.enablePlugin(pluginTouch)
22
- short.enablePlugin(pluginVoice)
23
- ```
24
-
25
- This guide shows you how to create such independent plugins by examining the existing plugins as reference implementations.
26
-
27
- ## Table of Contents
28
-
29
- 1. [Plugin Architecture Overview](#plugin-architecture-overview)
30
- 2. [Required Files Structure](#required-files-structure)
31
- 3. [Plugin Factory Function](#plugin-factory-function)
32
- 4. [Core Components Explained](#core-components-explained)
33
- 5. [Step-by-Step Plugin Creation](#step-by-step-plugin-creation)
34
- 6. [Plugin API and Integration](#plugin-api-and-integration)
35
- 7. [Best Practices and Patterns](#best-practices-and-patterns)
36
- 8. [Complete Example: Creating a "touch" Plugin](#complete-example-creating-a-touch-plugin)
37
-
38
- ## Plugin Architecture Overview
39
-
40
- Every plugin for the shortcuts library follows a consistent architecture, whether it's bundled with the library or distributed as a standalone package:
41
-
42
- ```
43
- your-plugin-project/
44
- ├── package.json # Your plugin's package configuration
45
- ├── README.md # Plugin documentation
46
- ├── src/ # Source code
47
- │ └── index.js # Main plugin factory
48
- │ ├── _registerShortcutEvents.js # Shortcut registration and setup
49
- │ ├── _listenDOM.js # DOM event listeners
50
- │ ├── _normalizeShortcutName.js # Shortcut name normalization
51
- │ └── [plugin-specific files] # Additional helper files
52
- ├── dist/ # Built distribution files
53
- └── test/ # Plugin tests
54
- ```
55
-
56
- **Key Points:**
57
- - **Standalone Package**: Each plugin is its own npm package
58
- - **No Library Dependency**: Plugins don't need to be inside the shortcuts library
59
- - **Standard Interface**: All plugins use the same API contract
60
- - **Independent Development**: Create, test, and publish plugins separately
61
- src/plugins/[plugin-name]/
62
- ├── index.js # Main plugin factory
63
- ├── _registerShortcutEvents.js # Shortcut registration and setup
64
- ├── _listenDOM.js # DOM event listeners
65
- ├── _normalizeShortcutName.js # Shortcut name normalization
66
- └── [plugin-specific files] # Additional helper files
67
- ```
68
-
69
-
70
-
71
- ### Key Concepts
72
-
73
- - **Plugin Prefix**: Each plugin has a unique prefix (e.g., "click", "key", "form")
74
- - **Shortcut Registration**: Plugins register shortcuts using regex patterns
75
- - **Event Management**: Plugins manage DOM listeners automatically
76
- - **State Management**: Each plugin maintains its own state and options
77
- - **Setup Configuration**: Plugins can be configured via `[PREFIX]:SETUP` shortcuts
78
-
79
-
80
-
81
-
82
-
83
- ## Required Files Structure
84
-
85
- ### 1. `index.js` - Main Plugin Factory
86
-
87
- This is the entry point that creates and configures the plugin.
88
-
89
- ```javascript
90
- import _normalizeShortcutName from './_normalizeShortcutName.js'
91
- import _registerShortcutEvents from './_registerShortcutEvents.js'
92
- import _listenDOM from './_listenDOM.js'
93
-
94
- function plugin[Name](setupPlugin, options = {}) {
95
- // Plugin-specific dependencies
96
- const deps = {
97
- regex: /[PREFIX]:.+/i, // Regex to identify plugin shortcuts
98
- // ... other dependencies
99
- }
100
-
101
- // Plugin state
102
- const pluginState = {
103
- active: false,
104
- defaultOptions: { /* default configuration */ },
105
- listenOptions: { /* runtime configuration */ },
106
- // ... plugin-specific state
107
- }
108
-
109
- // Reset state function
110
- function resetState() {
111
- // Clean up timers, state, etc.
112
- }
113
- deps.resetState = resetState
114
-
115
- // Return configured plugin
116
- return setupPlugin({
117
- prefix: '[PREFIX]',
118
- _normalizeShortcutName,
119
- _registerShortcutEvents,
120
- _listenDOM,
121
- pluginState,
122
- deps
123
- })
124
- }
125
-
126
- export default plugin[Name]
127
- ```
128
-
129
- ### 2. `_registerShortcutEvents.js` - Shortcut Registration
130
-
131
- This file handles shortcut discovery, setup, and registration.
132
-
133
- ```javascript
134
- 'use strict'
135
-
136
- /**
137
- * Register plugin shortcuts and handle setup
138
- * @param {Object} dependencies - Plugin dependencies
139
- * @param {Object} pluginState - Plugin state
140
- * @returns {number|false} - Number of registered shortcuts
141
- */
142
- function _registerShortcutEvents(dependencies, pluginState) {
143
- const { regex, /* other deps */ } = dependencies
144
- const {
145
- currentContext: { name: contextName, note },
146
- shortcuts,
147
- defaultOptions
148
- } = pluginState
149
-
150
- let count = 0;
151
- let setupOptions = [];
152
-
153
- if (!contextName) return count
154
-
155
- // Scan for plugin shortcuts
156
- Object.entries(shortcuts[contextName]).forEach(([shortcutName, list]) => {
157
- if (!regex.test(shortcutName)) return
158
-
159
- // Handle SETUP shortcuts
160
- if ( shortcutName.includes('SETUP') ) {
161
- let updateOptions = list.reduce((res, fn) => {
162
- let r = fn({
163
- dependencies: dependencies.extra,
164
- defaults: structuredClone(pluginState.defaultOptions),
165
- options: pluginState.listenOptions
166
- })
167
- return Object.assign ( res, r )
168
- }, defaultOptions )
169
- Object.assign(pluginState.listenOptions, updateOptions)
170
- return
171
- }
172
-
173
- // Handle other shortcuts (ACTION, WATCH, etc.)
174
- // ... plugin-specific logic
175
- count++
176
- }) // forEach context shortcut entries
177
- return count
178
- }
179
-
180
- export default _registerShortcutEvents
181
- ```
182
-
183
-
184
-
185
- ### 3. `_listenDOM.js` - DOM Event Listeners
186
-
187
- This file sets up and manages DOM event listeners.
188
-
189
- ```javascript
190
- 'use strict'
191
-
192
- /**
193
- * Set up DOM event listeners for the plugin
194
- * @param {Object} dependencies - Plugin dependencies
195
- * @param {Object} pluginState - Plugin state
196
- * @param {Object} ev - Event emitter
197
- * @returns {Object} - Listener control object
198
- */
199
- function _listenDOM ( dependencies, pluginState, ev ) {
200
- let listeners = [];
201
- let timers = [];
202
-
203
- function start () {
204
- if (pluginState.active) return
205
- pluginState.active = true
206
-
207
- // Set up DOM event listeners
208
- const listener = (e) => {
209
- // Prepare event data
210
- const eventData = {
211
- target: e.target,
212
- context: pluginState.currentContext.name,
213
- note: pluginState.currentContext.note,
214
- event: e,
215
- dependencies: dependencies.extra,
216
- options: pluginState.listenOptions,
217
- viewport: {
218
- X: window.scrollX,
219
- Y: window.scrollY,
220
- width: window.innerWidth,
221
- height: window.innerHeight
222
- }
223
- // ... other event data
224
- }
225
-
226
- // Emit plugin-specific events
227
- ev.emit('[PREFIX]:EVENT', eventData)
228
- }
229
-
230
- document.addEventListener('[dom-event]', listener)
231
- listeners.push({ event: '[dom-event]', listener })
232
- }
233
-
234
- function stop () {
235
- pluginState.active = false
236
-
237
- // Remove all listeners
238
- listeners.forEach(({ event, listener }) => {
239
- document.removeEventListener(event, listener)
240
- })
241
- listeners = []
242
-
243
- // Clear all timers
244
- timers.forEach(timer => clearTimeout(timer))
245
- timers = []
246
- }
247
-
248
- return { start, stop }
249
- }
250
-
251
- export default _listenDOM
252
- ```
253
-
254
-
255
-
256
- ### 4. `_normalizeShortcutName.js` - Shortcut Normalization
257
-
258
- This file standardizes shortcut names to UPPERCASE. Removes spaces and eliminates risk of using different letter cases. Using uppercase makes it easy to see which plugins are enabled, as all enabled plugins will normalize their shortcuts to uppercase format.
259
-
260
- ```javascript
261
- 'use strict'
262
-
263
- /**
264
- * Normalize shortcut names to standard format
265
- * @param {string} name - Raw shortcut name
266
- * @returns {string} - Normalized shortcut name (UPPERCASE)
267
- */
268
- function _normalizeShortcutName(name) {
269
- return name
270
- .toUpperCase()
271
- .split(':')
272
- .map(part => part.trim())
273
- .filter(part => part.length > 0)
274
- .join(':')
275
- }
276
-
277
- export default _normalizeShortcutName
278
- ```
279
-
280
-
281
-
282
-
283
-
284
- ## Core Components Explained
285
-
286
- ### Normalization Purpose
287
-
288
- The normalization function converts all shortcut names to **UPPERCASE** for several important reasons:
289
-
290
- 1. **Consistency**: Eliminates variations in letter case and spacing
291
- 2. **Debugging**: Makes it easy to see which plugins are enabled - all active plugins will have their shortcuts displayed in uppercase
292
- 3. **Conflict Prevention**: Ensures different writing styles don't create duplicate shortcuts
293
- 4. **Standardization**: Provides a uniform format across all plugins
294
-
295
- **Example:**
296
- - Input: `'touch: Swipe-Left'` → Output: `'TOUCH:SWIPE-LEFT'`
297
- - Input: `' Click:left-1 '` → Output: `'CLICK:LEFT-1'`
298
-
299
-
300
-
301
- ### Plugin State Structure
302
-
303
- Every plugin must maintain this standard state:
304
-
305
- ```javascript
306
- const pluginState = {
307
- active: false, // Whether plugin is listening
308
- defaultOptions: {}, // Default configuration
309
- listenOptions: {}, // Runtime configuration
310
- currentContext: {}, // Current context info by reference. Uses global context
311
- shortcuts: {}, // Available shortcuts by reference. Uses global shortcuts
312
- callbacks: {}, // Event callbacks
313
- wait: {}, // Wait timers
314
- // ... plugin-specific state
315
- }
316
- ```
317
-
318
-
319
-
320
- ### Dependencies Object
321
-
322
- The dependencies object provides access to shared utilities across the library(available for all plugins):
323
-
324
- ```javascript
325
- const deps = {
326
- regex: /[PREFIX]:.+/i, // Plugin shortcut identifier
327
- resetState: Function, // State cleanup function
328
- // ... plugin-specific helpers
329
- }
330
- ```
331
-
332
-
333
-
334
- ### Event Data Structure
335
-
336
- All plugins emit consistent event data:
337
-
338
- ```javascript
339
- const eventData = {
340
- target: HTMLElement, // Event target (if applicable)
341
- context: string, // Current context name
342
- note: string|null, // Current context note
343
- event: Event, // Original DOM event
344
- dependencies: object, // Extra dependencies
345
- options: object, // Plugin listenOptions
346
- viewport: { // Viewport information
347
- X: number, Y: number,
348
- width: number, height: number
349
- },
350
- // ... plugin-specific data
351
- }
352
- ```
353
-
354
-
355
-
356
-
357
-
358
-
359
- ## Step-by-Step Plugin Creation
360
-
361
- ### Step 1: Define Plugin Purpose and Prefix
362
-
363
- First, determine what your plugin will do and choose a unique prefix:
364
-
365
- ```javascript
366
- // Example: A plugin for touch gestures
367
- const PLUGIN_PREFIX = 'touch'
368
- ```
369
-
370
-
371
-
372
- ### Step 2: Create Your Plugin Project
373
-
374
- Since plugins are standalone projects, create a new package for your plugin:
375
-
376
- ```bash
377
- # Create your plugin project
378
- mkdir shortcuts-touch-plugin
379
- cd shortcuts-touch-plugin
380
-
381
- # Initialize as npm package
382
- npm init -y
383
-
384
- # Create source directory
385
- mkdir src
386
-
387
- # Install shortcuts as peer dependency (optional for development)
388
- npm install @peter.naydenov/shortcuts --save-dev
389
-
390
- # Create plugin files
391
- touch src/index.js
392
- touch src/_registerShortcutEvents.js
393
- touch src/_listenDOM.js
394
- touch src/_normalizeShortcutName.js
395
- ```
396
-
397
- **package.json example:**
398
- ```json
399
- {
400
- "name": "shortcuts-touch-plugin",
401
- "version": "1.0.0",
402
- "description": "Touch gesture plugin for shortcuts library",
403
- "main": "dist/index.js",
404
- "module": "dist/index.esm.js",
405
- "types": "dist/index.d.ts",
406
- "peerDependencies": {
407
- "@peter.naydenov/shortcuts": "^3.0.0"
408
- },
409
- "devDependencies": {
410
- "@peter.naydenov/shortcuts": "^3.5.2",
411
- "rollup": "^2.0.0",
412
- "typescript": "^4.0.0"
413
- }
414
- }
415
- ```
416
-
417
-
418
-
419
- ### Step 3: Implement Core Files
420
-
421
- Create the four required files with your plugin-specific logic.
422
-
423
-
424
-
425
- ### Step 4: Define Plugin Shortcuts
426
-
427
- Determine what shortcuts your plugin will handle:
428
-
429
- **Important Naming Convention:**
430
- - The first colon (`:`) separates the **plugin name** from the **shortcut action**
431
- - **Never use colons within the shortcut action itself**
432
- - Use hyphens (`-`) or commas (`,`) to separate parts within the shortcut action
433
-
434
- **Correct Examples:**
435
- - `click: left-1` ✅ (plugin: action)
436
- - `scroll: up` ✅ (plugin: action)
437
- - `touch: swipe-left` ✅ (plugin: action-detail)
438
-
439
- **Incorrect Examples:**
440
- - `touch:swipe:left` ❌ (colon used within action)
441
- - `click:left:1` ❌ (colon used within action)
442
-
443
- Following this convention ensures proper parsing and avoids conflicts with other plugins.
444
-
445
- ```javascript
446
- // Examples for touch plugin (will be normalized to uppercase):
447
- // 'touch: swipe-left' → 'TOUCH:SWIPE-LEFT' - Swipe left gesture
448
- // 'touch: swipe-right' → 'TOUCH:SWIPE-RIGHT' - Swipe right gesture
449
- // 'touch: pinch-zoom' → 'TOUCH:PINCH-ZOOM' - Pinch to zoom
450
- // 'touch: setup' → 'TOUCH:SETUP' - Configure touch options
451
- ```
452
-
453
-
454
-
455
- ### Step 5: Implement Event Logic
456
-
457
- Write the DOM event handling logic in `_listenDOM.js`.
458
-
459
-
460
-
461
- ### Step 6: Build and Publish Your Plugin
462
-
463
- Since plugins are standalone packages, build and publish them independently:
464
-
465
- ```bash
466
- # Build your plugin (using rollup, webpack, etc.)
467
- npm run build
468
-
469
- # Test your plugin
470
- npm test
471
-
472
- # Publish to npm
473
- npm publish
474
- ```
475
-
476
- **Example build configuration (rollup.config.js):**
477
- ```javascript
478
- import { nodeResolve } from '@rollup/plugin-node-resolve'
479
-
480
- export default {
481
- input: 'src/index.js',
482
- output: [
483
- { file: 'dist/index.js', format: 'cjs' },
484
- { file: 'dist/index.esm.js', format: 'esm' },
485
- { file: 'dist/index.umd.js', format: 'umd', name: 'ShortcutsTouchPlugin' }
486
- ],
487
- plugins: [nodeResolve()]
488
- }
489
- ```
490
-
491
- **Users will then install and use your plugin:**
492
- ```bash
493
- npm install shortcuts-touch-plugin
494
- ```
495
-
496
- ```javascript
497
- import { shortcuts } from '@peter.naydenov/shortcuts'
498
- import pluginTouch from 'shortcuts-touch-plugin'
499
-
500
- const short = shortcuts()
501
- short.enablePlugin(pluginTouch)
502
- ```
503
-
504
-
505
-
506
-
507
-
508
- ## Plugin API and Integration
509
-
510
- ### Standard Plugin API
511
-
512
- All plugins receive this standardized API from `setupPlugin`:
513
-
514
- ```javascript
515
- const pluginAPI = setupPlugin({
516
- prefix: string,
517
- _normalizeShortcutName: Function,
518
- _registerShortcutEvents: Function,
519
- _listenDOM: Function,
520
- pluginState: Object,
521
- deps: Object
522
- })
523
- ```
524
-
525
-
526
-
527
- ### Returned Plugin Methods
528
-
529
- Each plugin returns these standard methods:
530
-
531
- ```javascript
532
- const plugin = {
533
- getPrefix(): string, // Returns plugin prefix
534
- shortcutName(key): string, // Normalizes shortcut names
535
- contextChange(): void, // Handles context changes
536
- mute(): void, // Temporarily disable
537
- unmute(): void, // Re-enable
538
- destroy(): void // Complete cleanup
539
- }
540
- ```
541
-
542
-
543
-
544
-
545
-
546
-
547
- ## Best Practices and Patterns
548
-
549
- ### 1. Plugin Distribution
550
-
551
- - **Standalone Packages**: Always distribute plugins as separate npm packages
552
- - **Naming Convention**: Use `shortcuts-[plugin-name]-plugin` format
553
- - **Peer Dependencies**: Declare shortcuts as peer dependency, not direct dependency
554
- - **Version Compatibility**: Specify compatible shortcuts library versions
555
- - **Documentation**: Include comprehensive README with usage examples
556
-
557
- ### 2. Consistent Naming
558
-
559
- - Use lowercase prefixes: `click`, `key`, `form`, `hover`, `scroll`
560
- - Use consistent shortcut naming: `[PREFIX]:ACTION`
561
- - Private files start with underscore: `_helper.js`
562
-
563
-
564
-
565
- ### 2. Development Workflow
566
-
567
- - **Local Testing**: Test your plugin with local shortcuts installation
568
- - **Multiple Environments**: Test in browsers, Node.js (if applicable)
569
- - **Version Management**: Use semantic versioning for releases
570
- - **CI/CD**: Set up automated testing and publishing
571
- - **Examples**: Provide working examples in documentation
572
-
573
- ### 3. State Management
574
-
575
- - Always provide a `resetState()` function
576
- - Clean up timers and listeners in `stop()` methods
577
- - Isolate state between contexts
578
-
579
-
580
-
581
- ### 3. Error Handling
582
-
583
- - Validate input parameters
584
- - Emit error events for invalid configurations
585
- - Provide meaningful error messages
586
-
587
-
588
-
589
- ### 4. Performance
590
-
591
- - Only listen when shortcuts are active
592
- - Use event delegation where possible
593
- - Clean up resources properly
594
-
595
-
596
-
597
- ### 5. Documentation
598
-
599
- - Document all public functions with JSDoc
600
- - Include type definitions for TypeScript
601
- - Provide usage examples
602
-
603
-
604
-
605
-
606
-
607
-
608
- ## Complete Example: Creating a "touch" Plugin
609
-
610
- Let's create a complete touch gesture plugin:
611
-
612
- ### `src/plugins/touch/index.js`
613
-
614
- ```javascript
615
- import _normalizeShortcutName from './_normalizeShortcutName.js'
616
- import _registerShortcutEvents from './_registerShortcutEvents.js'
617
- import _listenDOM from './_listenDOM.js'
618
-
619
- function pluginTouch(setupPlugin, options = {}) {
620
- const deps = {
621
- regex: /touch:.+/i,
622
- _readTouchEvent: null // Will be implemented
623
- }
624
-
625
- const pluginState = {
626
- active: false,
627
- defaultOptions: {
628
- swipeThreshold: 50,
629
- pinchThreshold: 20,
630
- touchTimeout: 300
631
- },
632
- listenOptions: {},
633
- touches: new Map(), // Track active touches
634
- gestures: [] // Track gesture history
635
- }
636
-
637
- function resetState() {
638
- pluginState.touches.clear()
639
- pluginState.gestures = []
640
- }
641
- deps.resetState = resetState
642
-
643
- return setupPlugin({
644
- prefix: 'touch',
645
- _normalizeShortcutName,
646
- _registerShortcutEvents,
647
- _listenDOM,
648
- pluginState,
649
- deps
650
- })
651
- }
652
-
653
- export default pluginTouch
654
- ```
655
-
656
-
657
-
658
- ### `src/plugins/touch/_normalizeShortcutName.js`
659
-
660
- ```javascript
661
- 'use strict'
662
-
663
- function _normalizeShortcutName(name) {
664
- return name
665
- .toUpperCase()
666
- .split(':')
667
- .map(part => part.trim())
668
- .filter(part => part.length > 0)
669
- .join(':')
670
- }
671
-
672
- export default _normalizeShortcutName
673
- ```
674
-
675
-
676
-
677
- ### `src/plugins/touch/_registerShortcutEvents.js`
678
-
679
- ```javascript
680
- 'use strict'
681
-
682
- function _registerShortcutEvents(dependencies, pluginState) {
683
- const { regex } = dependencies
684
- const {
685
- currentContext: { name: contextName },
686
- shortcuts,
687
- defaultOptions
688
- } = pluginState
689
-
690
- if (!contextName) return false
691
-
692
- let count = 0
693
-
694
- Object.entries(shortcuts[contextName]).forEach(([shortcutName, list]) => {
695
- if (!regex.test(shortcutName)) return
696
-
697
- if (shortcutName.includes('SETUP')) {
698
- let updateOptions = list.reduce((res, fn) => {
699
- let r = fn({
700
- dependencies: dependencies.extra,
701
- defaults: structuredClone(pluginState.defaultOptions),
702
- options: pluginState.listenOptions
703
- })
704
- return Object.assign(res, r)
705
- }, defaultOptions)
706
- Object.assign(pluginState.listenOptions, updateOptions)
707
- return
708
- }
709
-
710
- // Register touch gesture shortcuts (normalized to uppercase)
711
- // Example: 'touch: swipe-left' → 'TOUCH:SWIPE-LEFT', 'touch: pinch-zoom' → 'TOUCH:PINCH-ZOOM'
712
- count++
713
- })
714
-
715
- return count
716
- }
717
-
718
- export default _registerShortcutEvents
719
- ```
720
-
721
-
722
-
723
- ### `src/plugins/touch/_listenDOM.js`
724
-
725
- ```javascript
726
- 'use strict'
727
-
728
- function _listenDOM(dependencies, pluginState, ev) {
729
- let listeners = []
730
- let touchStartTime = 0
731
- let touchStartPos = { x: 0, y: 0 }
732
-
733
- function start() {
734
- if (pluginState.active) return
735
- pluginState.active = true
736
-
737
- // Touch start
738
- const touchStartHandler = (e) => {
739
- touchStartTime = Date.now()
740
- const touch = e.touches[0]
741
- touchStartPos = { x: touch.clientX, y: touch.clientY }
742
- }
743
-
744
- // Touch end
745
- const touchEndHandler = (e) => {
746
- const touchEndTime = Date.now()
747
- const touch = e.changedTouches[0]
748
- const touchEndPos = { x: touch.clientX, y: touch.clientY }
749
-
750
- const deltaX = touchEndPos.x - touchStartPos.x
751
- const deltaY = touchEndPos.y - touchStartPos.y
752
- const deltaTime = touchEndTime - touchStartTime
753
-
754
- // Detect swipe gestures
755
- if (Math.abs(deltaX) > pluginState.listenOptions.swipeThreshold) {
756
- const direction = deltaX > 0 ? 'right' : 'left'
757
- const eventData = {
758
- target: e.target,
759
- context: pluginState.currentContext.name,
760
- note: pluginState.currentContext.note,
761
- event: e,
762
- dependencies: dependencies.extra,
763
- options: pluginState.listenOptions,
764
- gesture: 'swipe',
765
- direction,
766
- velocity: Math.abs(deltaX) / deltaTime,
767
- viewport: {
768
- X: window.scrollX,
769
- Y: window.scrollY,
770
- width: window.innerWidth,
771
- height: window.innerHeight
772
- }
773
- }
774
- ev.emit(`touch: swipe-${direction}`, eventData)
775
- }
776
- }
777
-
778
- document.addEventListener('touchstart', touchStartHandler, { passive: true })
779
- document.addEventListener('touchend', touchEndHandler, { passive: true })
780
-
781
- listeners.push(
782
- { event: 'touchstart', listener: touchStartHandler },
783
- { event: 'touchend', listener: touchEndHandler }
784
- )
785
- }
786
-
787
- function stop() {
788
- pluginState.active = false
789
- listeners.forEach(({ event, listener }) => {
790
- document.removeEventListener(event, listener)
791
- })
792
- listeners = []
793
- }
794
-
795
- return { start, stop }
796
- }
797
-
798
- export default _listenDOM
799
- ```
800
-
801
-
802
-
803
- ### Update Main Export
804
-
805
- Add to `src/main.js`:
806
-
807
- ```javascript
808
- import pluginTouch from './plugins/touch/index.js'
809
-
810
- export {
811
- shortcuts,
812
- pluginClick,
813
- pluginKey,
814
- pluginForm,
815
- pluginHover,
816
- pluginScroll,
817
- pluginTouch // Add this line
818
- }
819
- ```
820
-
821
-
822
-
823
- ### Usage Example
824
-
825
- **For users installing your standalone plugin:**
826
-
827
- ```javascript
828
- import { shortcuts } from '@peter.naydenov/shortcuts'
829
- import pluginTouch from 'shortcuts-touch-plugin' // Your published plugin
830
-
831
- const short = shortcuts()
832
-
833
- // Enable touch plugin
834
- short.enablePlugin(pluginTouch)
835
-
836
- // Define touch shortcuts (will be normalized to uppercase)
837
- const context = {
838
- mobile: {
839
- 'touch: setup': () => ({ // Becomes 'TOUCH:SETUP'
840
- swipeThreshold: 75,
841
- touchTimeout: 250
842
- }),
843
- 'touch: swipe-left': ({ options, gesture, direction, velocity }) => { // Becomes 'TOUCH:SWIPE-LEFT'
844
- console.log(`Swiped ${direction} with velocity ${velocity}`)
845
- // Navigate to previous page
846
- },
847
- 'touch: swipe-right': ({ options, gesture, direction, velocity }) => { // Becomes 'TOUCH:SWIPE-RIGHT'
848
- console.log(`Swiped ${direction} with velocity ${velocity}`)
849
- // Navigate to next page
850
- }
851
- }
852
- }
853
-
854
- short.load(context)
855
- short.changeContext('mobile')
856
- ```
857
-
858
-
859
-
860
-
861
- ## Testing Your Plugin
862
-
863
- Follow the testing patterns from existing plugins:
864
-
865
- 1. Create a test file: `test/07-[plugin-name].test.js`
866
- 2. Test plugin installation and removal
867
- 3. Test shortcut registration
868
- 4. Test event handling
869
- 5. Test setup configuration
870
- 6. Test error conditions
871
-
872
- Example test structure:
873
-
874
- ```javascript
875
- import { beforeEach, afterEach, describe, it, test, expect } from 'vitest'
876
- import { shortcuts, plugin[Name] } from '../src/main.js'
877
-
878
- describe('[Name] plugin', () => {
879
- let short
880
-
881
- beforeEach(() => {
882
- short = shortcuts()
883
- short.enablePlugin(plugin[Name])
884
- })
885
-
886
- afterEach(() => {
887
- short.reset()
888
- short.disablePlugin('[name]')
889
- })
890
-
891
- it('should register shortcuts', () => {
892
- // Test shortcut registration
893
- })
894
-
895
- it('should handle events', () => {
896
- // Test event handling
897
- })
898
-
899
- it('should handle setup configuration', () => {
900
- // Test setup options
901
- })
902
- })
903
- ```
904
-
905
-
906
-
907
- ## Conclusion
908
-
909
- Creating a **standalone plugin** for shortcuts follows a consistent pattern:
910
-
911
- 1. **Independent Project**: Create as separate npm package, not part of library
912
- 2. **Structure**: Use the standard file structure in your own project
913
- 3. **Integration**: Follow the setupPlugin pattern for library compatibility
914
- 4. **State Management**: Maintain clean, isolated state
915
- 5. **Events**: Emit consistent event data
916
- 6. **API**: Provide standard plugin methods
917
- 7. **Distribution**: Publish as independent package with proper dependencies
918
- 8. **Testing**: Test thoroughly following existing patterns
919
-
920
- ## 🎯 Key Takeaway
921
-
922
- **Plugins are designed to be independent, distributable packages** - not part of the core shortcuts library. This modular architecture allows:
923
-
924
- - **Third-party developers** to create and publish plugins
925
- - **Users** to install only the plugins they need
926
- - **Independent versioning** and development cycles
927
- - **Community ecosystem** of specialized input handlers
928
-
929
- By following this guide and studying the existing plugins, you can create robust, standalone plugins that extend the shortcuts library with new input types and capabilities, and share them with the community as independent packages.