@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.
Files changed (98) hide show
  1. package/API.md +939 -0
  2. package/CODE_OF_CONDUCT.md +84 -0
  3. package/CONTRIBUTING.md +476 -0
  4. package/Changelog.md +26 -2
  5. package/How.to.create.plugins.md +929 -0
  6. package/Migration.guide.md +48 -0
  7. package/README.md +396 -24
  8. package/dist/main.d.ts +54 -2
  9. package/dist/methods/_normalizeWithPlugins.d.ts +63 -1
  10. package/dist/methods/_readShortcutWithPlugins.d.ts +8 -1
  11. package/dist/methods/_setupPlugin.d.ts +9 -0
  12. package/dist/methods/_systemAction.d.ts +8 -1
  13. package/dist/methods/changeContext.d.ts +8 -1
  14. package/dist/methods/index.d.ts +2 -0
  15. package/dist/methods/listShortcuts.d.ts +1 -16
  16. package/dist/methods/load.d.ts +8 -1
  17. package/dist/methods/unload.d.ts +8 -1
  18. package/dist/plugins/click/_findTarget.d.ts +9 -1
  19. package/dist/plugins/click/_listenDOM.d.ts +76 -3
  20. package/dist/plugins/click/_normalizeShortcutName.d.ts +7 -1
  21. package/dist/plugins/click/_registerShortcutEvents.d.ts +26 -0
  22. package/dist/plugins/click/index.d.ts +6 -31
  23. package/dist/plugins/form/_defaults.d.ts +13 -1
  24. package/dist/plugins/form/_listenDOM.d.ts +66 -3
  25. package/dist/plugins/form/_registerShortcutEvents.d.ts +95 -1
  26. package/dist/plugins/form/index.d.ts +2 -29
  27. package/dist/plugins/hover/_findTarget.d.ts +10 -0
  28. package/dist/plugins/hover/_listenDOM.d.ts +68 -0
  29. package/dist/plugins/hover/_normalizeShortcutName.d.ts +2 -0
  30. package/dist/plugins/hover/_registerShortcutEvents.d.ts +28 -0
  31. package/dist/plugins/hover/index.d.ts +14 -0
  32. package/dist/plugins/key/_listenDOM.d.ts +61 -3
  33. package/dist/plugins/key/_registerShortcutEvents.d.ts +26 -0
  34. package/dist/plugins/key/_specialChars.d.ts +6 -31
  35. package/dist/plugins/key/index.d.ts +2 -29
  36. package/dist/plugins/scroll/_listenDOM.d.ts +58 -0
  37. package/dist/plugins/scroll/_normalizeShortcutName.d.ts +2 -0
  38. package/dist/plugins/scroll/_registerShortcutEvents.d.ts +28 -0
  39. package/dist/plugins/scroll/index.d.ts +16 -0
  40. package/dist/shortcuts.cjs +1 -1
  41. package/dist/shortcuts.esm.mjs +1 -1
  42. package/dist/shortcuts.umd.js +1 -1
  43. package/eslint.config.js +80 -0
  44. package/html/assets/index-COTh6lXR.css +1 -0
  45. package/html/assets/index-DOkKC3NI.js +53 -0
  46. package/html/bg.png +0 -0
  47. package/html/favicon.ico +0 -0
  48. package/html/favicon.svg +5 -0
  49. package/html/html.meta.json.gz +0 -0
  50. package/html/index.html +32 -0
  51. package/package.json +16 -12
  52. package/shortcuts.png +0 -0
  53. package/src/main.js +52 -22
  54. package/src/methods/_normalizeWithPlugins.js +26 -2
  55. package/src/methods/_readShortcutWithPlugins.js +9 -2
  56. package/src/methods/_setupPlugin.js +93 -0
  57. package/src/methods/_systemAction.js +12 -4
  58. package/src/methods/changeContext.js +11 -3
  59. package/src/methods/index.js +2 -0
  60. package/src/methods/listShortcuts.js +5 -12
  61. package/src/methods/load.js +11 -4
  62. package/src/methods/unload.js +8 -1
  63. package/src/plugins/click/_findTarget.js +11 -5
  64. package/src/plugins/click/_listenDOM.js +58 -20
  65. package/src/plugins/click/_normalizeShortcutName.js +11 -4
  66. package/src/plugins/click/_readClickEvent.js +1 -1
  67. package/src/plugins/click/_registerShortcutEvents.js +33 -5
  68. package/src/plugins/click/index.js +33 -60
  69. package/src/plugins/form/_defaults.js +13 -3
  70. package/src/plugins/form/_listenDOM.js +46 -9
  71. package/src/plugins/form/_normalizeShortcutName.js +2 -2
  72. package/src/plugins/form/_registerShortcutEvents.js +93 -17
  73. package/src/plugins/form/index.js +25 -56
  74. package/src/plugins/hover/_findTarget.js +26 -0
  75. package/src/plugins/hover/_listenDOM.js +154 -0
  76. package/src/plugins/hover/_normalizeShortcutName.js +21 -0
  77. package/src/plugins/hover/_registerShortcutEvents.js +51 -0
  78. package/src/plugins/hover/index.js +71 -0
  79. package/src/plugins/key/_listenDOM.js +67 -33
  80. package/src/plugins/key/_normalizeShortcutName.js +4 -3
  81. package/src/plugins/key/_readKeyEvent.js +1 -1
  82. package/src/plugins/key/_registerShortcutEvents.js +34 -5
  83. package/src/plugins/key/_specialChars.js +5 -0
  84. package/src/plugins/key/index.js +34 -59
  85. package/src/plugins/scroll/_listenDOM.js +141 -0
  86. package/src/plugins/scroll/_normalizeShortcutName.js +21 -0
  87. package/src/plugins/scroll/_registerShortcutEvents.js +50 -0
  88. package/src/plugins/scroll/index.js +61 -0
  89. package/test/01-general.test.js +92 -23
  90. package/test/02-key.test.js +241 -40
  91. package/test/03-click.test.js +291 -47
  92. package/test/04-form.test.js +241 -47
  93. package/test/05-hover.test.js +463 -0
  94. package/test/06-scroll.test.js +374 -0
  95. package/test-helpers/Block.jsx +3 -2
  96. package/test-helpers/style.css +6 -1
  97. package/vitest.config.js +13 -11
  98. package/How..to.make.plugins.md +0 -41
@@ -1,10 +1,11 @@
1
1
  import { beforeEach, afterEach, describe, it, expect } from 'vitest'
2
- import { userEvent } from '@vitest/browser/context'
2
+ import { userEvent } from 'vitest/browser'
3
3
  import {
4
4
  getByLabelText,
5
5
  getByText,
6
6
  getByTestId,
7
7
  queryByTestId,
8
+ fireEvent,
8
9
  // Tip: all queries are also exposed on an object
9
10
  // called "queries" which you could import here as well
10
11
  waitFor
@@ -27,10 +28,11 @@ import {
27
28
 
28
29
  const html = new VisaulController ();
29
30
  let
30
- a = false
31
- , b = false
32
- , c = null
33
- ;
31
+ a = false
32
+ , b = false
33
+ , c = null
34
+ , d = null
35
+ ;
34
36
 
35
37
  const contextDefinition = {
36
38
  general : {
@@ -51,27 +53,44 @@ const contextDefinition = {
51
53
  b = true
52
54
  c = target.dataset.click
53
55
  },
54
- // Single click with right button
55
- 'click: right-1': () => c = 'right'
56
- }
57
- , extra : {
58
- 'key : p,r,o,b,a': () => b = true
56
+ // Just for have definition for more then 2 clicks
57
+ 'click: left-3': () => {},
58
+ // Single click with right button
59
+ 'click: right-1': () => {
60
+ c = 'right'
61
+ },
62
+ ' click: right-2': () => d = 'clicked',
63
+ // Click with modifier
64
+ 'click: left-1-alt': () => d = 'alt-clicked',
65
+ 'click: left-1-ctrl': () => d = 'ctrl-clicked',
66
+ 'click: left-1-shift': () => d = 'shift-clicked'
59
67
  }
60
- , extend : {
61
- 'form : watch' : () => 'input'
62
- , 'form : define' : () => 'input'
63
- , 'form : action' : () => [
64
- {
65
- fn : (e) => console.log ( e.target )
66
- , type : 'input'
67
- , mode : 'in'
68
- }
69
- ]
70
- }
71
- }
72
-
73
-
74
- let short = shortcuts ();
68
+ , extra : {
69
+ 'key : p,r,o,b,a': () => b = true
70
+ }
71
+ , clickSetup : {
72
+ // Test CLICK:SETUP event to modify plugin options
73
+ 'click:setup': ({ dependencies, defaults }) => {
74
+ return {
75
+ mouseWait: 100 // Reduce wait time for faster test
76
+ }
77
+ }
78
+ }
79
+ , extend : {
80
+ 'form : watch' : () => 'input'
81
+ , 'form : define' : () => 'input'
82
+ , 'form : action' : () => [
83
+ {
84
+ fn : (e) => e.target
85
+ , type : 'input'
86
+ , mode : 'in'
87
+ }
88
+ ]
89
+ }
90
+ }
91
+
92
+
93
+ const short = shortcuts ();
75
94
 
76
95
 
77
96
 
@@ -81,24 +100,33 @@ describe ( 'Click plugin', () => {
81
100
 
82
101
  beforeEach ( async () => {
83
102
  short.load ( contextDefinition )
84
- let container = document.createElement ( 'div' );
103
+ const container = document.createElement ( 'div' );
85
104
  container.id = 'app'
86
105
  document.body.appendChild ( container )
87
106
  await html.publish ( Block, {}, 'app' )
88
- a = false, b = false
107
+ a = false, b = false, c = null, d = null
89
108
  }) // beforeEach
90
109
 
91
110
 
111
+ afterEach ( async () => {
112
+ short.reset ();
113
+ short.disablePlugin ( 'click' )
114
+ html.destroy ();
115
+ a = false, b = false, c = null, d = null;
116
+ document.body.querySelector ( '#app' ).remove ()
117
+ }) // afterEach
118
+
119
+
92
120
 
93
121
  afterEach ( async () => {
94
122
  short.reset ();
95
- a = false, b = false, c = null;
123
+ a = false, b = false, c = null, d = null;
96
124
  }) // afterEach
97
125
 
98
126
 
99
127
 
100
128
  it ( 'No "click" plugin installed', async () => {
101
- let r = short.listShortcuts ('touch');
129
+ const r = short.listShortcuts ('touch');
102
130
  // Shortcuts are untouched if plugin is not installed
103
131
  expect ( r[0]).to.equal ( ' click: left-1' )
104
132
  }) // it no 'click' plugin installed
@@ -107,7 +135,7 @@ describe ( 'Click plugin', () => {
107
135
 
108
136
  it ( 'Click plugin installed', async () => {
109
137
  short.enablePlugin ( pluginClick )
110
- let r = short.listShortcuts ( 'touch' );
138
+ const r = short.listShortcuts ( 'touch' );
111
139
  // Shortcuts are normalized
112
140
  expect ( r[0]).to.equal ( 'CLICK:LEFT-1' )
113
141
  }) // it click plugin installed
@@ -166,11 +194,17 @@ describe ( 'Click plugin', () => {
166
194
  await userEvent.tripleClick ( hitItem )
167
195
  // Default wait mouse timeout is 320 ms, but maxClicks is set to 3,
168
196
  // so we don't need to wait for timeout
169
- await waitFor ( () => {
170
- expect ( a ).to.equal ( true )
171
- expect ( b ).to.equal ( false )
172
- }, { timeout: 1000, interval: 12 })
173
- }) // it triple left click
197
+ await waitFor ( () => {
198
+ expect ( a ).to.equal ( true )
199
+ expect ( b ).to.equal ( false )
200
+ }, { timeout: 1000, interval: 12 })
201
+ // Now click again during the ignore period - should be ignored
202
+ await userEvent.click ( hitItem )
203
+ await wait ( 50 )
204
+ // Should still be a = true, b = false
205
+ expect ( a ).to.equal ( true )
206
+ expect ( b ).to.equal ( false )
207
+ }) // it triple left click
174
208
 
175
209
 
176
210
 
@@ -186,15 +220,136 @@ describe ( 'Click plugin', () => {
186
220
  },{ timeout: 1000, interval: 12 })
187
221
  if ( find ) await userEvent.click ( find , { button:'right' })
188
222
  // Default wait mouse timeout is 320 ms
189
- await wait ( 320 )
223
+ // await wait ( 320 )
190
224
  await waitFor ( () => {
191
225
  expect ( c ).to.equal ( 'right' )
192
226
  }, { timeout: 1000, interval: 12 })
193
- }) // it single right click
194
-
227
+ }) // it single right click
228
+
229
+
230
+
231
+ it ( 'Double right click', async () => {
232
+ short.enablePlugin ( pluginClick )
233
+ short.changeContext ( 'touch' )
234
+ // Load context with double right click
235
+ short.load ({
236
+ 'touch' : {
237
+ 'click: right-2' : () => d = 'double right'
238
+ }
239
+ })
240
+ await wait ( 12 )
241
+ const hitItem = document.querySelector ( '#rspan' );
242
+ expect ( d ).to.equal ( null )
243
+ // Simulate double right click
244
+ await userEvent.dblClick ( hitItem , { button:'right' })
245
+ await waitFor ( () => {
246
+ expect ( d ).to.equal ( 'double right' )
247
+ }, { timeout: 1000, interval: 12 })
248
+ }) // it double right click
249
+
250
+
251
+
252
+ it ( 'Ignore clicks on elements that are not a target', async () => {
253
+ short.enablePlugin ( pluginClick )
254
+ const
255
+ hidden = document.getElementById ( 'hidden' )
256
+ , name = document.getElementById ( 'name' )
257
+ , hitItem = document.querySelector ( '#rspan' )
258
+ ;
259
+ hidden.classList.remove ( 'hide' )
260
+ short.changeContext ( 'touch' )
261
+
262
+ // Click on element without data-click property. Should be ignored
263
+ await userEvent.click ( name , { button:'right' })
264
+ // Click on element with data-click property
265
+ await userEvent.click ( hitItem , { button:'right' })
266
+ await wait ( 330 )
267
+ await waitFor ( () => {
268
+ // Click should be read as single right click
269
+ expect ( c ).to.equal ( 'right' )
270
+ }, { timeout: 1000, interval: 12 })
271
+ }) // it ignore clicks on elements that are not a target
272
+
195
273
 
196
274
 
197
- it ( 'Arguments of click handler', async () => {
275
+ it ( 'Multiple right clicks', async () => {
276
+ short.enablePlugin ( pluginClick )
277
+ // Reload because in testcase 'Double right click' context 'touch' was changed;
278
+ short.load ( contextDefinition )
279
+ const hitItem = document.querySelector ( '#rspan' );
280
+ short.changeContext ( 'touch' )
281
+
282
+ await userEvent.click ( hitItem , { button:'right' })
283
+ await userEvent.click ( hitItem , { button:'right' })
284
+ // This click should be ignored
285
+ await userEvent.click ( hitItem , { button:'right' })
286
+ await wait ( 330 )
287
+ await userEvent.click ( hitItem , { button:'right' })
288
+
289
+ await wait ( 500 )
290
+ await waitFor ( () => {
291
+ // Click should be read as double right click, then single right click
292
+ expect ( c ).to.equal ( 'right' )
293
+ expect ( d ).to.equal ( 'clicked' )
294
+ }, { timeout: 1000, interval: 12 })
295
+ }) // it multiple right clicks
296
+
297
+
298
+
299
+ it ( 'Click with modifiers', async () => {
300
+ short.enablePlugin ( pluginClick )
301
+ short.changeContext ( 'touch' )
302
+ const r = short.listShortcuts ('touch');
303
+ expect ( r ).to.include ( 'CLICK:LEFT-1-ALT' )
304
+ const hitItem = document.querySelector ( '.block' );
305
+ // Click with alt key - should trigger the alt shortcut
306
+ fireEvent.click ( hitItem , { altKey: true } )
307
+ await wait ( 330 )
308
+ // Callback should be triggered
309
+ expect ( d ).to.equal ( 'alt-clicked' )
310
+ }) // it click with modifiers
311
+
312
+
313
+ it ( 'Click on document body - no target found', async () => {
314
+ short.enablePlugin ( pluginClick )
315
+ short.changeContext ( 'touch' )
316
+ fireEvent.click ( document.body )
317
+ await wait ( 330 )
318
+ // No callback should be triggered since no target
319
+ expect ( b ).to.equal ( false )
320
+ }) // it click on document body
321
+
322
+
323
+ it ( 'Click with ctrl modifier', async () => {
324
+ short.enablePlugin ( pluginClick )
325
+ short.changeContext ( 'touch' )
326
+ const r = short.listShortcuts ('touch');
327
+ expect ( r ).to.include ( 'CLICK:LEFT-1-CTRL' )
328
+ const hitItem = document.querySelector ( '.block' );
329
+ // Click with ctrl key - should trigger the ctrl shortcut
330
+ fireEvent.click ( hitItem , { ctrlKey: true } )
331
+ await wait ( 330 )
332
+ // Callback should be triggered
333
+ expect ( d ).to.equal ( 'ctrl-clicked' )
334
+ }) // it click with ctrl modifier
335
+
336
+
337
+ it ( 'Click with shift modifier', async () => {
338
+ short.enablePlugin ( pluginClick )
339
+ short.changeContext ( 'touch' )
340
+ const r = short.listShortcuts ('touch');
341
+ expect ( r ).to.include ( 'CLICK:LEFT-1-SHIFT' )
342
+ const hitItem = document.querySelector ( '.block' );
343
+ // Click with shift key - should trigger the shift shortcut
344
+ fireEvent.click ( hitItem , { shiftKey: true } )
345
+ await wait ( 330 )
346
+ // Callback should be triggered
347
+ expect ( d ).to.equal ( 'shift-clicked' )
348
+ }) // it click with shift modifier
349
+
350
+
351
+
352
+ it ( 'Arguments of click handler', async () => {
198
353
  /**
199
354
  * Need to know arguments for 'click' handler
200
355
  * function myMouseHandler ({
@@ -211,8 +366,8 @@ describe ( 'Click plugin', () => {
211
366
  * }
212
367
  */
213
368
  // Ensure clean state for this test
214
- let megaBtn = document.querySelector ( '[data-click="mega"]' )
215
- let test = [];
369
+ const megaBtn = document.querySelector ( '[data-click="mega"]' )
370
+ const test = [];
216
371
  let i = 0;
217
372
  short.enablePlugin ( pluginClick )
218
373
  short.setDependencies ({ test })
@@ -247,7 +402,7 @@ describe ( 'Click plugin', () => {
247
402
  await wait ( 50 ) // Wait for click processing
248
403
  await waitFor ( () => {
249
404
  expect ( i ).to.be.equal ( 1 )
250
- let result = test[0];
405
+ const result = test[0];
251
406
  expect ( result.target ).to.be.equal ( 'mega' )
252
407
  expect ( result.context ).to.be.equal ( 'local' )
253
408
  }, { timeout: 1000, interval: 12 })
@@ -269,7 +424,7 @@ describe ( 'Click plugin', () => {
269
424
  }
270
425
  })
271
426
  short.changeContext ( 'extra' )
272
- let loc = document.querySelector ( '#anchor' ) || false;
427
+ const loc = document.querySelector ( '#anchor' ) || false;
273
428
  if ( loc ) await userEvent.click ( loc )
274
429
  expect ( result ).to.be.equal ( 'A' )
275
430
  }) // it click on anchor
@@ -289,7 +444,7 @@ describe ( 'Click plugin', () => {
289
444
  short.load ({
290
445
  'local' : {
291
446
  'click: left-1 ' : ({dependencies}) => {
292
- let { result } = dependencies;
447
+ const { result } = dependencies;
293
448
  result.push ( i++ )
294
449
  }
295
450
  }
@@ -330,7 +485,7 @@ describe ( 'Click plugin', () => {
330
485
 
331
486
 
332
487
  it ( 'Pause and resume', async () => {
333
- let target = document.querySelector ( '#rspan' )
488
+ const target = document.querySelector ( '#rspan' )
334
489
  short.enablePlugin ( pluginClick )
335
490
  expect ( b ).to.be.equal ( false )
336
491
  short.changeContext ( 'touch' )
@@ -348,5 +503,94 @@ describe ( 'Click plugin', () => {
348
503
  expect ( c ).to.be.equal ( 'red' )
349
504
  }, { timeout: 1000, interval: 30 })
350
505
  }) // it pause and resume
351
-
352
- }) // describe
506
+
507
+
508
+
509
+ it ( 'Click setup event', async () => {
510
+ // Test that CLICK:SETUP can modify plugin options
511
+ short.enablePlugin ( pluginClick )
512
+ const emit = [];
513
+ short.setDependencies ( { emit } )
514
+
515
+ // Create a context with CLICK:SETUP to modify mouseWait to 100ms
516
+ const setupContext = {
517
+ clickSetup : {
518
+ // This should modify the plugin's mouseWait option from default 320ms to 100ms
519
+ ' click : setup ': ({ dependencies, defaults }) => {
520
+ dependencies.emit.push ( 'setup' )
521
+ return {
522
+ mouseWait: 100 // Reduce wait time for faster test
523
+ }
524
+ }
525
+ // Add a click handler to test the modified timing
526
+ , 'click: left-1': () => {
527
+ b = true
528
+ c = 'setup-worked'
529
+ }
530
+ }
531
+ }
532
+
533
+ short.load ( setupContext )
534
+ short.changeContext ( 'clickSetup' )
535
+ expect ( emit[0] ).to.equal ( 'setup' )
536
+
537
+ const
538
+ target = document.querySelector ( '#rspan' )
539
+ , startTime = performance.now()
540
+ ;
541
+
542
+ // Reset test variables
543
+ b = false
544
+ c = null
545
+
546
+ // Click and measure time
547
+ await userEvent.click ( target )
548
+
549
+ await waitFor ( () => {
550
+ expect ( b ).to.equal ( true )
551
+ expect ( c ).to.equal ( 'setup-worked' )
552
+ // Verify that the setup was applied by checking that we got a response quickly
553
+ // If setup worked, mouseWait should be 100ms, so total time should be much less than default 320ms
554
+ const duration = performance.now() - startTime
555
+ expect ( duration ).to.be.lessThan ( 250 ) // Should be much less than default 320ms + test overhead
556
+ }, { timeout: 1000, interval: 12 })
557
+ }) // it click setup event
558
+
559
+
560
+
561
+ it ( 'Extra parameters to plugin options', async () => {
562
+ short.enablePlugin ( pluginClick )
563
+
564
+ const emit = [];
565
+ const setupContext = {
566
+ clickSetup : {
567
+ // This should modify the plugin's mouseWait and add an extra plugin option
568
+ ' click : setup ': ({ dependencies, defaults, options }) => {
569
+ emit.push ( 'setup' )
570
+ return {
571
+ mouseWait: 100 // Reduce wait time for faster test
572
+ , emit
573
+ }
574
+ }
575
+ // Add a click handler
576
+ , 'click: left-1': ( {options }) => {
577
+ options.emit.push ( 'clicked' )
578
+ }
579
+ }
580
+ }
581
+
582
+ short.load ( setupContext )
583
+ short.changeContext ( 'clickSetup' )
584
+ // Setup event execution is on change context:
585
+ expect ( emit[0] ).to.equal ( 'setup' )
586
+
587
+ const target = document.querySelector ( '[data-hover="blue"]' );
588
+
589
+ // Click and measure time
590
+ await userEvent.click ( target )
591
+ await waitFor ( () => {
592
+ expect ( emit ).to.deep.equal ( [ 'setup', 'clicked' ] )
593
+ }, { timeout: 1000, interval: 12 })
594
+ }) // it extra parameters to plugin options
595
+
596
+ }) // describe