@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.
- package/API.md +939 -0
- package/CODE_OF_CONDUCT.md +84 -0
- package/CONTRIBUTING.md +476 -0
- package/Changelog.md +30 -1
- 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 -5
- 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 -3
- 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 -3
- 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 +34 -51
- 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 +26 -47
- 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 +35 -50
- 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
|
@@ -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
|
-
|
|
27
|
+
ev
|
|
9
28
|
, _specialChars
|
|
10
29
|
, _readKeyEvent
|
|
11
|
-
,
|
|
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
|
-
|
|
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 :
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
108
|
+
const _sp = _specialChars ()
|
|
76
109
|
if ( _sp.hasOwnProperty(event.code) ) r.push ( _readKeyEvent ( event, _specialChars ))
|
|
77
|
-
else
|
|
110
|
+
else return
|
|
78
111
|
if ( streamKeys ) streamKeys ({ key:event.key, context:currentContext.name, note:currentContext.note, dependencies:dependencies.extra })
|
|
79
|
-
if (
|
|
80
|
-
clearTimeout (
|
|
81
|
-
|
|
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 ===
|
|
118
|
+
if ( sequence && r.length === state.maxSequence ) {
|
|
85
119
|
keySequenceEnd ()
|
|
86
|
-
|
|
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
|
|
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 (
|
|
100
|
-
clearTimeout (
|
|
101
|
-
|
|
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 ===
|
|
139
|
+
if ( sequence && r.length === state.maxSequence ) {
|
|
106
140
|
keySequenceEnd ()
|
|
107
|
-
|
|
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 (
|
|
135
|
-
clearTimeout (
|
|
136
|
-
|
|
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
|
|
12
|
-
|
|
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,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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
19
|
-
if (
|
|
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
|
|
package/src/plugins/key/index.js
CHANGED
|
@@ -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 {
|
|
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 (
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
35
|
-
,
|
|
36
|
-
,
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
+
|