@peter.naydenov/shortcuts 3.5.2 → 4.0.1

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 (135) 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 +32 -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/dist/src/main.d.ts +172 -0
  44. package/dist/src/methods/_normalizeWithPlugins.d.ts +64 -0
  45. package/dist/src/methods/_readShortcutWithPlugins.d.ts +9 -0
  46. package/dist/src/methods/_setupPlugin.d.ts +9 -0
  47. package/dist/src/methods/_systemAction.d.ts +9 -0
  48. package/dist/src/methods/changeContext.d.ts +9 -0
  49. package/dist/src/methods/index.d.ts +19 -0
  50. package/dist/src/methods/listShortcuts.d.ts +2 -0
  51. package/dist/src/methods/load.d.ts +9 -0
  52. package/dist/src/methods/unload.d.ts +9 -0
  53. package/dist/src/plugins/click/_findTarget.d.ts +10 -0
  54. package/dist/src/plugins/click/_listenDOM.d.ts +78 -0
  55. package/dist/src/plugins/click/_normalizeShortcutName.d.ts +8 -0
  56. package/dist/src/plugins/click/_readClickEvent.d.ts +2 -0
  57. package/dist/src/plugins/click/_registerShortcutEvents.d.ts +28 -0
  58. package/dist/src/plugins/click/index.d.ts +16 -0
  59. package/dist/src/plugins/form/_defaults.d.ts +17 -0
  60. package/dist/src/plugins/form/_listenDOM.d.ts +68 -0
  61. package/dist/src/plugins/form/_normalizeShortcutName.d.ts +2 -0
  62. package/dist/src/plugins/form/_registerShortcutEvents.d.ts +96 -0
  63. package/dist/src/plugins/form/index.d.ts +9 -0
  64. package/dist/src/plugins/hover/_findTarget.d.ts +10 -0
  65. package/dist/src/plugins/hover/_listenDOM.d.ts +68 -0
  66. package/dist/src/plugins/hover/_normalizeShortcutName.d.ts +2 -0
  67. package/dist/src/plugins/hover/_registerShortcutEvents.d.ts +28 -0
  68. package/dist/src/plugins/hover/index.d.ts +14 -0
  69. package/dist/src/plugins/key/_listenDOM.d.ts +63 -0
  70. package/dist/src/plugins/key/_normalizeShortcutName.d.ts +2 -0
  71. package/dist/src/plugins/key/_readKeyEvent.d.ts +2 -0
  72. package/dist/src/plugins/key/_registerShortcutEvents.d.ts +28 -0
  73. package/dist/src/plugins/key/_specialChars.d.ts +7 -0
  74. package/dist/src/plugins/key/index.d.ts +14 -0
  75. package/dist/src/plugins/scroll/_listenDOM.d.ts +58 -0
  76. package/dist/src/plugins/scroll/_normalizeShortcutName.d.ts +2 -0
  77. package/dist/src/plugins/scroll/_registerShortcutEvents.d.ts +28 -0
  78. package/dist/src/plugins/scroll/index.d.ts +16 -0
  79. package/eslint.config.js +80 -0
  80. package/html/assets/index-COTh6lXR.css +1 -0
  81. package/html/assets/index-DOkKC3NI.js +53 -0
  82. package/html/bg.png +0 -0
  83. package/html/favicon.ico +0 -0
  84. package/html/favicon.svg +5 -0
  85. package/html/html.meta.json.gz +0 -0
  86. package/html/index.html +32 -0
  87. package/package.json +24 -19
  88. package/shortcuts.png +0 -0
  89. package/src/main.js +52 -22
  90. package/src/methods/_normalizeWithPlugins.js +26 -2
  91. package/src/methods/_readShortcutWithPlugins.js +9 -2
  92. package/src/methods/_setupPlugin.js +93 -0
  93. package/src/methods/_systemAction.js +12 -4
  94. package/src/methods/changeContext.js +11 -3
  95. package/src/methods/index.js +2 -0
  96. package/src/methods/listShortcuts.js +5 -12
  97. package/src/methods/load.js +11 -4
  98. package/src/methods/unload.js +8 -1
  99. package/src/plugins/click/_findTarget.js +11 -5
  100. package/src/plugins/click/_listenDOM.js +58 -20
  101. package/src/plugins/click/_normalizeShortcutName.js +11 -4
  102. package/src/plugins/click/_readClickEvent.js +1 -1
  103. package/src/plugins/click/_registerShortcutEvents.js +33 -5
  104. package/src/plugins/click/index.js +33 -60
  105. package/src/plugins/form/_defaults.js +13 -3
  106. package/src/plugins/form/_listenDOM.js +46 -9
  107. package/src/plugins/form/_normalizeShortcutName.js +2 -2
  108. package/src/plugins/form/_registerShortcutEvents.js +93 -17
  109. package/src/plugins/form/index.js +25 -56
  110. package/src/plugins/hover/_findTarget.js +26 -0
  111. package/src/plugins/hover/_listenDOM.js +154 -0
  112. package/src/plugins/hover/_normalizeShortcutName.js +21 -0
  113. package/src/plugins/hover/_registerShortcutEvents.js +51 -0
  114. package/src/plugins/hover/index.js +71 -0
  115. package/src/plugins/key/_listenDOM.js +67 -33
  116. package/src/plugins/key/_normalizeShortcutName.js +4 -3
  117. package/src/plugins/key/_readKeyEvent.js +1 -1
  118. package/src/plugins/key/_registerShortcutEvents.js +34 -5
  119. package/src/plugins/key/_specialChars.js +5 -0
  120. package/src/plugins/key/index.js +34 -59
  121. package/src/plugins/scroll/_listenDOM.js +141 -0
  122. package/src/plugins/scroll/_normalizeShortcutName.js +21 -0
  123. package/src/plugins/scroll/_registerShortcutEvents.js +50 -0
  124. package/src/plugins/scroll/index.js +61 -0
  125. package/test/01-general.test.js +92 -23
  126. package/test/02-key.test.js +241 -40
  127. package/test/03-click.test.js +291 -47
  128. package/test/04-form.test.js +241 -47
  129. package/test/05-hover.test.js +463 -0
  130. package/test/06-scroll.test.js +374 -0
  131. package/test-helpers/Block.jsx +3 -2
  132. package/test-helpers/style.css +6 -1
  133. package/tsconfig.json +2 -1
  134. package/vitest.config.js +13 -11
  135. 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,14 +1,6 @@
1
1
  'use strict'
2
2
 
3
- /**
4
- * @typedef {Object} PluginAPI
5
- * @property {function(): string} getPrefix - Get plugin prefix
6
- * @property {function(string): string} shortcutName - Format shortcut name
7
- * @property {function(string): void} contextChange - Handle context change
8
- * @property {function(): void} mute - Mute the plugin
9
- * @property {function(): void} unmute - Unmute the plugin
10
- * @property {function(): void} destroy - Destroy the plugin
11
- */
3
+
12
4
 
13
5
  // import all plugin files here
14
6
  import _listenDOM from './_listenDOM.js'
@@ -22,67 +14,50 @@ import _specialChars from './_specialChars.js'
22
14
  /**
23
15
  * @function pluginKey
24
16
  * @description Plugin for keyboard shortcuts
25
- * @param {Object} dependencies - Internal dependencies
26
- * @param {Object} state - Library state
17
+ * @param {function} setupPlugin - Plugin setup function from the library
27
18
  * @param {Object} [options={}] - Plugin options
28
19
  * @param {number} [options.keyWait=480] - Time to wait for key sequence in ms
29
20
  * @param {function} [options.streamKeys] - Function to stream key presses
30
21
  * @returns {PluginAPI} Plugin API
31
22
  */
32
- function pluginKey ( dependencies, state, options={} ) {
33
- let
34
- { currentContext, shortcuts, exposeShortcut } = state
35
- , { inAPI } = dependencies
36
- , deps = {
37
- ev: dependencies.ev
38
- , _specialChars
23
+ function pluginKey ( setupPlugin, options = {} ) {
24
+ const
25
+ deps = {
26
+ _specialChars
39
27
  , _readKeyEvent
40
- , mainDependencies : dependencies
41
28
  , regex : /KEY\s*\:/i
42
29
  }
43
30
  , pluginState = {
44
- currentContext
45
- , shortcuts
46
- , 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
+ }
47
37
  , listenOptions : {
48
- keyWait : options.keyWait ? options.keyWait : 480 // 480 ms
49
- , maxSequence : 1 // How many keys can be pressed in a sequence. Controlled automatically by 'changeContext' function.
50
- , keyIgnore : null // Timer for ignoring key presses after max sequence or null. Not a public option.
51
- }
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
+ }
52
42
  , streamKeys : (options.streamKeys && ( typeof options.streamKeys === 'function')) ? options.streamKeys : false // Keyboard stream function
53
- , exposeShortcut
54
- }; // state
55
-
56
- // Read shortcuts names from all context entities and normalize entries related to the plugin
57
- inAPI._normalizeWithPlugins ( _normalizeShortcutName )
58
-
59
- let
60
- keysListener = _listenDOM ( deps, pluginState )
61
- , countShortcuts = _registerShortcutEvents ( deps, pluginState )
62
- ;
63
-
64
- if ( countShortcuts > 0 ) keysListener.start ()
65
-
66
- let pluginAPI = {
67
- getPrefix : () => 'key'
68
- , shortcutName : key => { // Format a key string according plugin needs
69
- return _normalizeShortcutName ( key )
70
- }
71
- , contextChange : contextName => {
72
- countShortcuts = _registerShortcutEvents ( deps, pluginState )
73
- if ( countShortcuts < 1 ) { // Remove DOM listener if there are no shortcuts in the current context
74
- keysListener.stop ()
75
- }
76
- if ( countShortcuts > 0 ) { // Add DOM listener if there are shortcuts in the current context
77
- keysListener.start ()
78
- }
79
- }
80
- , mute : () => keysListener.stop ()
81
- , unmute : () => keysListener.start ()
82
- , destroy : () => keysListener.stop ()
83
- };
84
- Object.freeze ( pluginAPI )
85
- 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
+ })
86
61
  } // pluginKey func.
87
62
 
88
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
+