@peter.naydenov/shortcuts 3.5.1 → 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 +30 -1
  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 -5
  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 -3
  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 -3
  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 +34 -51
  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 +26 -47
  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 +35 -50
  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
@@ -2,28 +2,45 @@
2
2
 
3
3
 
4
4
 
5
+ /**
6
+ * @function _listenDOM
7
+ * @description Set up DOM event listeners for keyboard events
8
+ * @param {Object} dependencies - Dependencies object containing ev, _specialChars, _readKeyEvent, extra, resetState
9
+ * @param {Object} state - Plugin state containing listenOptions and currentContext
10
+ * @returns {Object} - Object containing start and stop methods
11
+ *
12
+ * @typedef {Object} KeyEventData
13
+ * @property {Function} wait - Function to wait for keys (disables key sequence)
14
+ * @property {Function} end - Function to end waiting for keys (enables key sequence)
15
+ * @property {Function} ignore - Function to ignore the last key in sequence
16
+ * @property {Function} isWaiting - Function to check if currently waiting for keys
17
+ * @property {string|null} note - Current context note
18
+ * @property {string} context - Current context name
19
+ * @property {Object} dependencies - Extra dependencies object
20
+ * @property {Object} options - Plugin state listenOptions (reference to pluginState.listenOptions)
21
+ * @property {Object} viewport - Viewport information with X, Y, width, height
22
+ * @property {string} type - Event type ('key')
23
+ */
5
24
  function _listenDOM ( dependencies, state ) {
6
- // Listen for input signals and generate event titles
25
+ // Listen for input signals and generate event titles
7
26
  const {
8
- ev
27
+ ev
9
28
  , _specialChars
10
29
  , _readKeyEvent
11
- , mainDependencies
30
+ , extra
31
+ , resetState
12
32
  } = dependencies
13
33
  , {
14
34
  currentContext
15
35
  , streamKeys
16
36
  , listenOptions
17
37
  } = state
18
- , {
19
- keyWait
20
- } = listenOptions
21
38
  ;
22
39
 
23
40
  let
24
41
  r = []
25
42
  , keyTimer = null // Timer for key sequence or null
26
- , sequence = true
43
+ , sequence = true // Is false only when time limitter is off - waitKeys ()
27
44
  , ignore = false // Use to trigger a single callback without adding the key to the sequence.
28
45
  ;
29
46
 
@@ -39,7 +56,8 @@ function _listenDOM ( dependencies, state ) {
39
56
 
40
57
 
41
58
  function keySequenceEnd () { // Execute when key sequence ends
42
- let res = r.map ( x => ([x.join('+')]) )
59
+ const res = r.map ( x => ([x.join('+')]) )
60
+
43
61
  const data = {
44
62
  wait: waitKeys
45
63
  , end:endKeys
@@ -47,23 +65,38 @@ function _listenDOM ( dependencies, state ) {
47
65
  , isWaiting:waitingKeys
48
66
  , note: currentContext.note
49
67
  , context: currentContext.name
50
- , dependencies : mainDependencies.extra
68
+ , dependencies : extra
69
+ , options : state.listenOptions
70
+ , viewport : {
71
+ X : window.scrollX
72
+ , Y : window.scrollY
73
+ , width:window.innerWidth
74
+ , height:window.innerHeight
75
+ }
51
76
  , type : 'key'
52
77
  };
78
+
53
79
  if ( !sequence ) {
54
- let signal = res.at(-1);
55
- ev.emit ( signal, data )
56
- if ( ignore ) {
57
- res = res.slice ( 0, -1 )
58
- ignore = false
59
- }
80
+ const signal = `KEY:${res.at(-1).join('+')}`;
81
+ ev.emit ( signal, data )
82
+ if ( ignore ) {
83
+ r = r.slice ( 0, -1 )
84
+ ignore = false
85
+ }
60
86
  }
61
87
 
62
88
  if ( sequence ) {
63
89
  const signal = `KEY:${res.join(',')}`
64
90
  ev.emit ( signal, data )
91
+ if ( ignore ) {
92
+ r = r.slice ( 0, -1 )
93
+ ignore = false
94
+ }
65
95
  // Reset:
66
96
  r = []
97
+ clearTimeout ( state.keyIgnore )
98
+ state.keyIgnore = null
99
+ clearTimeout ( keyTimer )
67
100
  keyTimer = null
68
101
  }
69
102
  } // keySequeceEnd func.
@@ -72,42 +105,43 @@ function _listenDOM ( dependencies, state ) {
72
105
 
73
106
  function listenForSpecialKeys ( event ) { // Listen for special keyboard keys
74
107
  clearTimeout ( keyTimer )
75
- let _sp = _specialChars ()
108
+ const _sp = _specialChars ()
76
109
  if ( _sp.hasOwnProperty(event.code) ) r.push ( _readKeyEvent ( event, _specialChars ))
77
- else return
110
+ else return
78
111
  if ( streamKeys ) streamKeys ({ key:event.key, context:currentContext.name, note:currentContext.note, dependencies:dependencies.extra })
79
- if ( listenOptions.keyIgnore ) {
80
- clearTimeout ( listenOptions.keyIgnore )
81
- listenOptions.keyIgnore = setTimeout ( () => listenOptions.keyIgnore=null, keyWait )
112
+ if ( state.keyIgnore ) {
113
+ clearTimeout ( state.keyIgnore )
114
+ state.keyIgnore = setTimeout ( () => state.keyIgnore=null, listenOptions.keyWait )
115
+ r.pop ()
82
116
  return
83
117
  }
84
- if ( sequence && r.length === listenOptions.maxSequence ) {
118
+ if ( sequence && r.length === state.maxSequence ) {
85
119
  keySequenceEnd ()
86
- listenOptions.keyIgnore = setTimeout ( () => listenOptions.keyIgnore=null, keyWait )
120
+ state.keyIgnore = setTimeout ( () => state.keyIgnore=null, listenOptions.keyWait )
87
121
  return
88
122
  }
89
- if ( sequence ) keyTimer = setTimeout ( keySequenceEnd, keyWait )
123
+ if ( sequence ) keyTimer = setTimeout ( keySequenceEnd, listenOptions.keyWait )
90
124
  else keySequenceEnd ()
91
125
  } // listenForSpecialKeys func.
92
126
 
93
127
 
94
128
 
95
129
  function listenForRegularKeys ( event ) { // Listen for regular keyboard keys
96
- if ( _specialChars().hasOwnProperty(event.code) ) return
130
+ if ( _specialChars().hasOwnProperty ( event.code )) return
97
131
  clearTimeout ( keyTimer )
98
132
  if ( streamKeys ) streamKeys ({ key:event.key, context:currentContext.name, note:currentContext.note, dependencies:dependencies.extra })
99
- if ( listenOptions.keyIgnore ) {
100
- clearTimeout ( listenOptions.keyIgnore )
101
- listenOptions.keyIgnore = setTimeout ( () => listenOptions.keyIgnore=null, keyWait )
133
+ if ( state.keyIgnore ) {
134
+ clearTimeout ( state.keyIgnore )
135
+ state.keyIgnore = setTimeout ( () => state.keyIgnore=null, listenOptions.keyWait )
102
136
  return
103
137
  }
104
138
  r.push ( _readKeyEvent ( event, _specialChars ))
105
- if ( sequence && r.length === listenOptions.maxSequence ) {
139
+ if ( sequence && r.length === state.maxSequence ) {
106
140
  keySequenceEnd ()
107
- listenOptions.keyIgnore = setTimeout ( () => listenOptions.keyIgnore=null, keyWait )
141
+ state.keyIgnore = setTimeout ( () => state.keyIgnore=null, listenOptions.keyWait )
108
142
  return
109
143
  }
110
- if ( sequence ) keyTimer = setTimeout ( keySequenceEnd, keyWait )
144
+ if ( sequence ) keyTimer = setTimeout ( keySequenceEnd, listenOptions.keyWait )
111
145
  else keySequenceEnd ()
112
146
  } // listenForRegularKeys func.
113
147
 
@@ -131,9 +165,9 @@ function _listenDOM ( dependencies, state ) {
131
165
  clearTimeout ( keyTimer )
132
166
  keyTimer = null
133
167
  }
134
- if ( listenOptions.keyIgnore ) {
135
- clearTimeout ( listenOptions.keyIgnore )
136
- listenOptions.keyIgnore = null
168
+ if ( state.keyIgnore ) {
169
+ clearTimeout ( state.keyIgnore )
170
+ state.keyIgnore = null
137
171
  }
138
172
  // Reset all state variables to prevent interference between tests
139
173
  r = []
@@ -7,9 +7,10 @@ function _normalizeShortcutName ( name ) {
7
7
  , isKeyboardShortcut = regex.test ( upperCase )
8
8
  , sliceIndex = upperCase.indexOf ( ':' )
9
9
  ;
10
-
11
- if ( !isKeyboardShortcut ) return name
12
- let shortcut = upperCase
10
+
11
+ if ( !isKeyboardShortcut ) return name
12
+ if ( upperCase.includes ( 'SETUP')) return 'KEY:SETUP'
13
+ const shortcut = upperCase
13
14
  .slice(sliceIndex+1)
14
15
  .split(',')
15
16
  .map ( key => key.trim() )
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  function _readKeyEvent ( event, _specialChars ) {
4
- let
4
+ const
5
5
  { shiftKey, altKey, ctrlKey } = event
6
6
  , falseKeys = [ 'ControlLeft','ControlRight', 'ShiftLeft', 'ShiftRight', 'AltLeft', 'AltRight', 'Meta' ]
7
7
  , _sp = _specialChars ()
@@ -1,23 +1,52 @@
1
1
  'use strict'
2
2
 
3
+ /**
4
+ * @function _registerShortcutEvents
5
+ * @description Register keyboard shortcut events and handle setup
6
+ * @param {Object} dependencies - Dependencies object containing regex
7
+ * @param {Object} pluginState - Plugin state containing currentContext, shortcuts
8
+ * @returns {number} - Number of registered shortcuts
9
+ *
10
+ * @typedef {Object} KeySetupData
11
+ * @property {Object} dependencies - Extra dependencies object
12
+ * @property {Object} defaults - Default options (clone of pluginState.defaultOptions)
13
+ * @property {Object} options - Plugin state listenOptions (reference to pluginState.listenOptions)
14
+ */
3
15
  function _registerShortcutEvents ( dependencies, pluginState ) {
4
16
  let count = 0;
17
+ let hasSetup = false
18
+ const df = pluginState.defaultOptions;
5
19
  const
6
20
  { regex } = dependencies
7
21
  , {
8
- listenOptions
9
- , currentContext : { name: contextName }
22
+ currentContext : { name: contextName }
10
23
  , shortcuts
11
24
  } = pluginState
12
25
  ;
26
+
13
27
  if ( contextName == null ) return 0
14
28
  Object.entries ( shortcuts[contextName] ).forEach ( ([shortcutName, list ]) => { // Enable new context shortcuts and set a listenOptions 'maxSequence'
15
- let isKeyboardEv = regex.test ( shortcutName );
29
+ const isKeyboardEv = regex.test ( shortcutName );
16
30
  if ( !isKeyboardEv ) return
31
+ if ( shortcutName === 'KEY:SETUP' ) {
32
+ hasSetup = true
33
+ const updateOptions = list.reduce ( ( res, fn ) => {
34
+ const r = fn ({
35
+ dependencies : dependencies.extra,
36
+ defaults : structuredClone(pluginState.defaultOptions),
37
+ options : pluginState.listenOptions
38
+ })
39
+ return Object.assign ( res, r )
40
+ }, df )
41
+ Object.assign ( pluginState.listenOptions, updateOptions )
42
+ return
43
+ }
17
44
  count++
18
- let sequenceArraySize = shortcutName.slice(4).split(',').length;
19
- if ( listenOptions.maxSequence < sequenceArraySize ) listenOptions.maxSequence = sequenceArraySize
45
+ const sequenceArraySize = shortcutName.slice(4).split(',').length;
46
+ if ( pluginState.maxSequence < sequenceArraySize ) pluginState.maxSequence = sequenceArraySize
20
47
  })
48
+
49
+ if ( !hasSetup ) Object.assign ( pluginState.listenOptions, df )
21
50
  return count
22
51
  } // _registerShortcutEvents func.
23
52
 
@@ -1,3 +1,8 @@
1
+ /**
2
+ * @function _specialChars
3
+ * @description Get mapping of special keyboard characters to their normalized names
4
+ * @returns {Object} - Object mapping keyboard event keys to normalized names
5
+ */
1
6
  function _specialChars () {
2
7
  return {
3
8
  'ArrowLeft' : 'LEFT'
@@ -1,5 +1,7 @@
1
1
  'use strict'
2
2
 
3
+
4
+
3
5
  // import all plugin files here
4
6
  import _listenDOM from './_listenDOM.js'
5
7
  import _normalizeShortcutName from './_normalizeShortcutName.js'
@@ -12,67 +14,50 @@ import _specialChars from './_specialChars.js'
12
14
  /**
13
15
  * @function pluginKey
14
16
  * @description Plugin for keyboard shortcuts
15
- * @param {Object} dependencies - Internal dependencies
16
- * @param {Object} state - Library state
17
+ * @param {function} setupPlugin - Plugin setup function from the library
17
18
  * @param {Object} [options={}] - Plugin options
18
19
  * @param {number} [options.keyWait=480] - Time to wait for key sequence in ms
19
20
  * @param {function} [options.streamKeys] - Function to stream key presses
20
21
  * @returns {PluginAPI} Plugin API
21
22
  */
22
- function pluginKey ( dependencies, state, options={} ) {
23
- let
24
- { currentContext, shortcuts, exposeShortcut } = state
25
- , { inAPI } = dependencies
26
- , deps = {
27
- ev: dependencies.ev
28
- , _specialChars
23
+ function pluginKey ( setupPlugin, options = {} ) {
24
+ const
25
+ deps = {
26
+ _specialChars
29
27
  , _readKeyEvent
30
- , mainDependencies : dependencies
31
28
  , regex : /KEY\s*\:/i
32
29
  }
33
30
  , pluginState = {
34
- currentContext
35
- , shortcuts
36
- , active : false
31
+ active : false
32
+ , maxSequence : 1 // How many keys can be pressed in a sequence. Controlled automatically by 'changeContext' function.
33
+ , keyIgnore : null // Timer for ignoring key presses after max sequence or null. Not a public option.
34
+ , defaultOptions : {
35
+ keyWait : 480 // 480 ms
36
+ }
37
37
  , listenOptions : {
38
- keyWait : options.keyWait ? options.keyWait : 480 // 480 ms
39
- , maxSequence : 1 // How many keys can be pressed in a sequence. Controlled automatically by 'changeContext' function.
40
- , keyIgnore : null // Timer for ignoring key presses after max sequence or null. Not a public option.
41
- }
38
+ // Filled from 'key: setup' event in the context
39
+ // or getting from the plugin the defaults
40
+ keyWait : 480 // 480 ms // TODO: WHY is need initialization? Register function should fullfill it
41
+ }
42
42
  , streamKeys : (options.streamKeys && ( typeof options.streamKeys === 'function')) ? options.streamKeys : false // Keyboard stream function
43
- , exposeShortcut
44
- }; // state
45
-
46
- // Read shortcuts names from all context entities and normalize entries related to the plugin
47
- inAPI._normalizeWithPlugins ( _normalizeShortcutName )
48
-
49
- let
50
- keysListener = _listenDOM ( deps, pluginState )
51
- , countShortcuts = _registerShortcutEvents ( deps, pluginState )
52
- ;
53
-
54
- if ( countShortcuts > 0 ) keysListener.start ()
55
-
56
- let pluginAPI = {
57
- getPrefix : () => 'key'
58
- , shortcutName : key => { // Format a key string according plugin needs
59
- return _normalizeShortcutName ( key )
60
- }
61
- , contextChange : contextName => {
62
- countShortcuts = _registerShortcutEvents ( deps, pluginState )
63
- if ( countShortcuts < 1 ) { // Remove DOM listener if there are no shortcuts in the current context
64
- keysListener.stop ()
65
- }
66
- if ( countShortcuts > 0 ) { // Add DOM listener if there are shortcuts in the current context
67
- keysListener.start ()
68
- }
69
- }
70
- , mute : () => keysListener.stop ()
71
- , unmute : () => keysListener.start ()
72
- , destroy : () => keysListener.stop ()
73
- };
74
- Object.freeze ( pluginAPI )
75
- return pluginAPI
43
+ } // state
44
+ ;
45
+
46
+ function resetState () {
47
+ pluginState.active = false
48
+ pluginState.keyIgnore = null
49
+ pluginState.maxSequence = 1
50
+ } // resetState func.
51
+ deps.resetState = resetState
52
+
53
+ return setupPlugin ( {
54
+ prefix : 'key'
55
+ , _normalizeShortcutName
56
+ , _registerShortcutEvents
57
+ , _listenDOM
58
+ , pluginState
59
+ , deps
60
+ })
76
61
  } // pluginKey func.
77
62
 
78
63
 
@@ -0,0 +1,141 @@
1
+
2
+
3
+ /**
4
+ * @function _listenDOM
5
+ * @description Set up DOM event listeners for scroll events
6
+ * @param {Object} dependencies - Dependencies object containing ev, resetState, extra
7
+ * @param {Object} state - Plugin state containing listenOptions and currentContext
8
+ * @returns {Object} - Object containing start and stop methods
9
+ *
10
+ * @typedef {Object} ScrollEventData
11
+ * @property {number} x - Current scroll X position
12
+ * @property {number} y - Current scroll Y position
13
+ * @property {string} direction - Scroll direction ('up', 'down', 'left', 'right')
14
+ * @property {string} context - Current context name
15
+ * @property {string|null} note - Current context note
16
+ * @property {Object} dependencies - Extra dependencies object
17
+ * @property {Object} options - Plugin state listenOptions (reference to pluginState.listenOptions)
18
+ * @property {Object} viewport - Viewport information with X, Y, width, height
19
+ * @property {string} type - Event type ('scroll')
20
+ */
21
+ function _listenDOM ( dependencies, state ) {
22
+ const {
23
+ ev
24
+ , resetState
25
+ , extra
26
+ } = dependencies;
27
+ let
28
+ waitForScroll = null // Timeout for reducing scroll events
29
+ , waitForEndScroll = null // Timeout for setting scroll end event
30
+ ;
31
+
32
+ function listenForScroll ( event ) {
33
+ const
34
+ x = event.clientX
35
+ , y = event.clientY
36
+ , {
37
+ lastPosition
38
+ , lastDirection
39
+ , listenOptions
40
+ , currentContext
41
+ } = state
42
+ , {
43
+ scrollWait
44
+ , endScrollWait
45
+ , minSpace
46
+ } = listenOptions
47
+ ;
48
+
49
+
50
+
51
+ if ( !lastPosition ) return; // No previous position to compare
52
+
53
+ let direction = null;
54
+ const
55
+ currentX = window.scrollX
56
+ , currentY = window.scrollY
57
+ , verticalChange = Math.abs ( currentY - lastPosition.y )
58
+ , horizontalChange = Math.abs ( currentX - lastPosition.x )
59
+ ;
60
+
61
+ // Reduce scroll events by space
62
+ if ( verticalChange < minSpace && horizontalChange < minSpace ) return
63
+
64
+ const directions = [];
65
+
66
+ // Check vertical scroll
67
+ if ( verticalChange >= minSpace ) {
68
+ if ( currentY > lastPosition.y ) directions.push('down')
69
+ else directions.push('up')
70
+ }
71
+
72
+ // Check horizontal scroll
73
+ if ( horizontalChange >= minSpace ) {
74
+ if ( currentX > lastPosition.x ) directions.push('right')
75
+ else directions.push('left')
76
+ }
77
+
78
+ // Use first direction for single direction compatibility
79
+ direction = directions[0] || null;
80
+
81
+ const getData = (dir) => ({
82
+ x: currentX
83
+ , y: currentY
84
+ , direction: dir
85
+ , context: currentContext.name
86
+ , note: currentContext.note
87
+ , dependencies: extra
88
+ , options: state.listenOptions
89
+ , viewport : { // Viewport scroll positions and sizes
90
+ X: currentX
91
+ , Y: currentY
92
+ , width: window.innerWidth
93
+ , height: window.innerHeight
94
+ }
95
+ , type: 'scroll'
96
+ })
97
+
98
+ // Emit events for each direction detected
99
+ directions.forEach(dir => {
100
+ const signal = `SCROLL:${dir.toUpperCase()}`
101
+ ev.emit ( signal, getData(dir) )
102
+ })
103
+
104
+ // Set up end scroll timeout (only once)
105
+ clearTimeout ( waitForScroll )
106
+ clearTimeout ( waitForEndScroll )
107
+ const finalDirection = directions[directions.length - 1] || null
108
+ waitForScroll = setTimeout ( () => {}, scrollWait ) // Keep for compatibility
109
+ waitForEndScroll = setTimeout ( () => ev.emit ( 'SCROLL:END', getData(finalDirection) ), endScrollWait )
110
+
111
+ // Update last position
112
+ state.lastPosition = { x: currentX, y: currentY }
113
+ state.lastDirection = direction
114
+
115
+ } // listenForScroll func.
116
+
117
+
118
+ function start () {
119
+ if ( state.active ) return
120
+ // Initialize last position
121
+ state.lastPosition = { x: window.scrollX, y: window.scrollY }
122
+ window.addEventListener ( 'scroll' , listenForScroll )
123
+ state.active = true
124
+ }
125
+
126
+
127
+ function stop () {
128
+ if ( !state.active ) return
129
+ window.removeEventListener ( 'scroll' , listenForScroll )
130
+ resetState ()
131
+ }
132
+
133
+ return { start, stop }
134
+
135
+ } // _listenDOM func.
136
+
137
+
138
+
139
+ export default _listenDOM
140
+
141
+
@@ -0,0 +1,21 @@
1
+ 'use strict'
2
+
3
+ function _normalizeShortcutName ( name ) {
4
+ const
5
+ upperCase = name.toUpperCase ()
6
+ , regex = /SCROLL\s*\:/i
7
+ , isHoverShortcut = regex.test ( upperCase )
8
+ ;
9
+
10
+ const sliceIndex = upperCase.indexOf ( ':' );
11
+
12
+ if ( !isHoverShortcut ) return name
13
+ const shortcut = upperCase.slice(sliceIndex+1).trim ()
14
+ return `SCROLL:${shortcut}`
15
+ } // _normalizeShortcutName func.
16
+
17
+
18
+
19
+ export default _normalizeShortcutName
20
+
21
+
@@ -0,0 +1,50 @@
1
+ /**
2
+ * @function _registerShortcutEvents
3
+ * @description Register scroll shortcut events and handle setup
4
+ * @param {Object} dependencies - Dependencies object containing regex
5
+ * @param {Object} pluginState - Plugin state containing currentContext, shortcuts
6
+ * @returns {number} - Number of registered shortcuts
7
+ *
8
+ * @typedef {Object} ScrollSetupData
9
+ * @property {Object} dependencies - Extra dependencies object
10
+ * @property {Object} defaults - Default options (clone of pluginState.defaultOptions)
11
+ * @property {Object} options - Plugin state listenOptions (reference to pluginState.listenOptions)
12
+ */
13
+ function _registerShortcutEvents ( dependencies, pluginState ) {
14
+ let count = 0;
15
+ let hasSetup = false
16
+ const df = pluginState.defaultOptions;
17
+ const
18
+ { regex } = dependencies
19
+ , {
20
+ currentContext : { name: contextName }
21
+ , shortcuts
22
+ } = pluginState
23
+ ;
24
+ if ( contextName == null ) return count // No context
25
+ Object.entries ( shortcuts[contextName] ).forEach ( ([shortcutName, list ]) => {
26
+ const isScrollEv = regex.test ( shortcutName );
27
+ if ( !isScrollEv ) return
28
+ if ( shortcutName === 'SCROLL:SETUP' ) {
29
+ hasSetup = true
30
+ const updateOptions = list.reduce ( ( res, fn ) => {
31
+ const r = fn ({
32
+ dependencies : dependencies.extra,
33
+ defaults : structuredClone(pluginState.defaultOptions),
34
+ options : pluginState.listenOptions
35
+ })
36
+ return Object.assign ( res, r )
37
+ }, df )
38
+ Object.assign ( pluginState.listenOptions, updateOptions )
39
+ return
40
+ }
41
+ count++
42
+ })
43
+ if ( !hasSetup ) Object.assign ( pluginState.listenOptions, df )
44
+ return count
45
+ } // _registerShortcutEvents func.
46
+
47
+
48
+ export default _registerShortcutEvents
49
+
50
+
@@ -0,0 +1,61 @@
1
+ 'use strict'
2
+
3
+ import _listenDOM from "./_listenDOM"
4
+ import _normalizeShortcutName from "./_normalizeShortcutName"
5
+ import _registerShortcutEvents from "./_registerShortcutEvents"
6
+
7
+
8
+
9
+ /**
10
+ * @function pluginScroll
11
+ * @description Plugin for scroll event shortcuts
12
+ * @param {function} setupPlugin - Plugin setup function from the library
13
+ * @param {Object} [options={}] - Plugin options
14
+ * @param {number} [options.scrollWait=50] - Delay between scroll events in ms
15
+ * @param {number} [options.endScrollWait=400] - Delay when scroll was stopped in ms
16
+ * @param {number} [options.minSpace=40] - Minimum distance between scroll events in px
17
+ * @returns {PluginAPI} Plugin API
18
+ */
19
+ function pluginScroll ( setupPlugin, options={} ) {
20
+ // up, down, left, right
21
+ const
22
+ deps = {
23
+ regex : /SCROLL\s*\:/i
24
+ }
25
+ , pluginState = {
26
+ active : false // Is plugin active?
27
+ , lastPosition : null // Last scroll position
28
+ , lastDirection : null // Last scroll direction
29
+ , defaultOptions : {
30
+ scrollWait : 50 // 50 ms. Delay between scroll events
31
+ , endScrollWait : 400 // 400 ms. When scroll was stopped.
32
+ , minSpace : 40 // 40 px. Minimum distance between scroll events
33
+ }
34
+ , listenOptions : {
35
+ scrollWait : 50 // 50 ms. Delay between scroll events
36
+ , endScrollWait : 400 // 400 ms. When scroll was stopped.
37
+ , minSpace : 40 // 40 px. Minimum distance between scroll events
38
+ }
39
+ } // pluginState
40
+ ;
41
+
42
+ function resetState () {
43
+ pluginState.active = false
44
+ } // resetState func.
45
+ deps.resetState = resetState
46
+
47
+ return setupPlugin ( {
48
+ prefix : 'scroll'
49
+ , _normalizeShortcutName
50
+ , _registerShortcutEvents
51
+ , _listenDOM
52
+ , pluginState
53
+ , deps
54
+ })
55
+ } // pluginScroll func.
56
+
57
+
58
+
59
+ export default pluginScroll
60
+
61
+