@peter.naydenov/shortcuts 3.5.2 → 4.0.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/API.md +939 -0
- package/CODE_OF_CONDUCT.md +84 -0
- package/CONTRIBUTING.md +476 -0
- package/Changelog.md +26 -2
- package/How.to.create.plugins.md +929 -0
- package/Migration.guide.md +48 -0
- package/README.md +396 -24
- package/dist/main.d.ts +54 -2
- package/dist/methods/_normalizeWithPlugins.d.ts +63 -1
- package/dist/methods/_readShortcutWithPlugins.d.ts +8 -1
- package/dist/methods/_setupPlugin.d.ts +9 -0
- package/dist/methods/_systemAction.d.ts +8 -1
- package/dist/methods/changeContext.d.ts +8 -1
- package/dist/methods/index.d.ts +2 -0
- package/dist/methods/listShortcuts.d.ts +1 -16
- package/dist/methods/load.d.ts +8 -1
- package/dist/methods/unload.d.ts +8 -1
- package/dist/plugins/click/_findTarget.d.ts +9 -1
- package/dist/plugins/click/_listenDOM.d.ts +76 -3
- package/dist/plugins/click/_normalizeShortcutName.d.ts +7 -1
- package/dist/plugins/click/_registerShortcutEvents.d.ts +26 -0
- package/dist/plugins/click/index.d.ts +6 -31
- package/dist/plugins/form/_defaults.d.ts +13 -1
- package/dist/plugins/form/_listenDOM.d.ts +66 -3
- package/dist/plugins/form/_registerShortcutEvents.d.ts +95 -1
- package/dist/plugins/form/index.d.ts +2 -29
- package/dist/plugins/hover/_findTarget.d.ts +10 -0
- package/dist/plugins/hover/_listenDOM.d.ts +68 -0
- package/dist/plugins/hover/_normalizeShortcutName.d.ts +2 -0
- package/dist/plugins/hover/_registerShortcutEvents.d.ts +28 -0
- package/dist/plugins/hover/index.d.ts +14 -0
- package/dist/plugins/key/_listenDOM.d.ts +61 -3
- package/dist/plugins/key/_registerShortcutEvents.d.ts +26 -0
- package/dist/plugins/key/_specialChars.d.ts +6 -31
- package/dist/plugins/key/index.d.ts +2 -29
- package/dist/plugins/scroll/_listenDOM.d.ts +58 -0
- package/dist/plugins/scroll/_normalizeShortcutName.d.ts +2 -0
- package/dist/plugins/scroll/_registerShortcutEvents.d.ts +28 -0
- package/dist/plugins/scroll/index.d.ts +16 -0
- package/dist/shortcuts.cjs +1 -1
- package/dist/shortcuts.esm.mjs +1 -1
- package/dist/shortcuts.umd.js +1 -1
- package/eslint.config.js +80 -0
- package/html/assets/index-COTh6lXR.css +1 -0
- package/html/assets/index-DOkKC3NI.js +53 -0
- package/html/bg.png +0 -0
- package/html/favicon.ico +0 -0
- package/html/favicon.svg +5 -0
- package/html/html.meta.json.gz +0 -0
- package/html/index.html +32 -0
- package/package.json +16 -12
- package/shortcuts.png +0 -0
- package/src/main.js +52 -22
- package/src/methods/_normalizeWithPlugins.js +26 -2
- package/src/methods/_readShortcutWithPlugins.js +9 -2
- package/src/methods/_setupPlugin.js +93 -0
- package/src/methods/_systemAction.js +12 -4
- package/src/methods/changeContext.js +11 -3
- package/src/methods/index.js +2 -0
- package/src/methods/listShortcuts.js +5 -12
- package/src/methods/load.js +11 -4
- package/src/methods/unload.js +8 -1
- package/src/plugins/click/_findTarget.js +11 -5
- package/src/plugins/click/_listenDOM.js +58 -20
- package/src/plugins/click/_normalizeShortcutName.js +11 -4
- package/src/plugins/click/_readClickEvent.js +1 -1
- package/src/plugins/click/_registerShortcutEvents.js +33 -5
- package/src/plugins/click/index.js +33 -60
- package/src/plugins/form/_defaults.js +13 -3
- package/src/plugins/form/_listenDOM.js +46 -9
- package/src/plugins/form/_normalizeShortcutName.js +2 -2
- package/src/plugins/form/_registerShortcutEvents.js +93 -17
- package/src/plugins/form/index.js +25 -56
- package/src/plugins/hover/_findTarget.js +26 -0
- package/src/plugins/hover/_listenDOM.js +154 -0
- package/src/plugins/hover/_normalizeShortcutName.js +21 -0
- package/src/plugins/hover/_registerShortcutEvents.js +51 -0
- package/src/plugins/hover/index.js +71 -0
- package/src/plugins/key/_listenDOM.js +67 -33
- package/src/plugins/key/_normalizeShortcutName.js +4 -3
- package/src/plugins/key/_readKeyEvent.js +1 -1
- package/src/plugins/key/_registerShortcutEvents.js +34 -5
- package/src/plugins/key/_specialChars.js +5 -0
- package/src/plugins/key/index.js +34 -59
- package/src/plugins/scroll/_listenDOM.js +141 -0
- package/src/plugins/scroll/_normalizeShortcutName.js +21 -0
- package/src/plugins/scroll/_registerShortcutEvents.js +50 -0
- package/src/plugins/scroll/index.js +61 -0
- package/test/01-general.test.js +92 -23
- package/test/02-key.test.js +241 -40
- package/test/03-click.test.js +291 -47
- package/test/04-form.test.js +241 -47
- package/test/05-hover.test.js +463 -0
- package/test/06-scroll.test.js +374 -0
- package/test-helpers/Block.jsx +3 -2
- package/test-helpers/style.css +6 -1
- package/vitest.config.js +13 -11
- package/How..to.make.plugins.md +0 -41
|
@@ -0,0 +1,929 @@
|
|
|
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.
|