@rokkit/actions 1.0.0-next.106 → 1.0.0-next.108
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/package.json +1 -1
- package/src/navigator.old.svelte.js +260 -0
- package/src/navigator.svelte.js +78 -70
- package/src/types.js +13 -0
- package/src/utils.js +136 -9
package/package.json
CHANGED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { on } from 'svelte/events'
|
|
2
|
+
import { getClosestAncestorWithAttribute, getEventForKey } from './utils.js'
|
|
3
|
+
import { getPathFromKey } from '@rokkit/core'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Key mappings for different navigation directions
|
|
7
|
+
*/
|
|
8
|
+
const KEY_MAPPINGS = {
|
|
9
|
+
horizontal: {
|
|
10
|
+
prev: ['ArrowLeft'],
|
|
11
|
+
next: ['ArrowRight'],
|
|
12
|
+
collapse: ['ArrowUp'],
|
|
13
|
+
expand: ['ArrowDown'],
|
|
14
|
+
select: ['Enter'],
|
|
15
|
+
toggle: ['Space'],
|
|
16
|
+
delete: ['Delete', 'Backspace']
|
|
17
|
+
},
|
|
18
|
+
vertical: {
|
|
19
|
+
prev: ['ArrowUp'],
|
|
20
|
+
next: ['ArrowDown'],
|
|
21
|
+
collapse: ['ArrowLeft'],
|
|
22
|
+
expand: ['ArrowRight'],
|
|
23
|
+
select: ['Enter'],
|
|
24
|
+
toggle: ['Space'],
|
|
25
|
+
delete: ['Delete', 'Backspace']
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Navigator class to handle keyboard and mouse navigation
|
|
31
|
+
* @class
|
|
32
|
+
*/
|
|
33
|
+
class NavigatorController {
|
|
34
|
+
/**
|
|
35
|
+
* @param {HTMLElement} root - The root element
|
|
36
|
+
* @param {Object} wrapper - The data wrapper object
|
|
37
|
+
* @param {Object} options - Configuration options
|
|
38
|
+
*/
|
|
39
|
+
constructor(root, wrapper, options = {}) {
|
|
40
|
+
this.root = root
|
|
41
|
+
this.wrapper = wrapper
|
|
42
|
+
this.options = options
|
|
43
|
+
this.keyMappings = KEY_MAPPINGS[options?.direction || 'vertical']
|
|
44
|
+
|
|
45
|
+
this.handleKeyUp = this.handleKeyUp.bind(this)
|
|
46
|
+
this.handleClick = this.handleClick.bind(this)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Initialize event listeners
|
|
51
|
+
*/
|
|
52
|
+
init() {
|
|
53
|
+
return [on(this.root, 'keyup', this.handleKeyUp), on(this.root, 'click', this.handleClick)]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Check if modifier keys are pressed
|
|
58
|
+
* @param {Event} event - The event object
|
|
59
|
+
* @returns {boolean} - Whether modifier keys are pressed
|
|
60
|
+
*/
|
|
61
|
+
hasModifierKey(event) {
|
|
62
|
+
return event.ctrlKey || event.metaKey || event.shiftKey
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Handle keyboard events
|
|
67
|
+
* @param {KeyboardEvent} event - The keyboard event
|
|
68
|
+
*/
|
|
69
|
+
handleKeyUp(event) {
|
|
70
|
+
const { key } = event
|
|
71
|
+
|
|
72
|
+
const eventName = getEventForKey(this.keyMappings, key)
|
|
73
|
+
|
|
74
|
+
if (!eventName) return
|
|
75
|
+
|
|
76
|
+
const handled = this.processKeyAction(eventName, event)
|
|
77
|
+
|
|
78
|
+
if (handled) {
|
|
79
|
+
event.stopPropagation()
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Process a key action based on its type
|
|
85
|
+
* @param {string} eventName - The mapped event name
|
|
86
|
+
* @param {KeyboardEvent} event - The original keyboard event
|
|
87
|
+
* @returns {boolean} - Whether the event was handled
|
|
88
|
+
*/
|
|
89
|
+
processKeyAction(eventName, event) {
|
|
90
|
+
const actionHandlers = {
|
|
91
|
+
prev: () => this.handleNavigationKey('prev', this.hasModifierKey(event)),
|
|
92
|
+
next: () => this.handleNavigationKey('next', this.hasModifierKey(event)),
|
|
93
|
+
expand: () => this.executeAction('expand'),
|
|
94
|
+
collapse: () => this.executeAction('collapse'),
|
|
95
|
+
select: () => this.executeAction('select'),
|
|
96
|
+
toggle: () => this.executeAction('extend'),
|
|
97
|
+
delete: () => this.executeAction('delete')
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const handler = actionHandlers[eventName]
|
|
101
|
+
if (handler) {
|
|
102
|
+
return handler() !== false
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return false
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Handle navigation key presses (arrows)
|
|
110
|
+
* @param {string} direction - The direction to move ('prev' or 'next')
|
|
111
|
+
* @param {boolean} hasModifier - Whether modifier keys are pressed
|
|
112
|
+
* @returns {boolean} - Whether the action was handled
|
|
113
|
+
*/
|
|
114
|
+
handleNavigationKey(direction, hasModifier) {
|
|
115
|
+
// First move in the specified direction
|
|
116
|
+
const moved = this.executeAction(direction)
|
|
117
|
+
|
|
118
|
+
if (!moved) return false
|
|
119
|
+
|
|
120
|
+
// If modifier key is pressed and multiSelect is enabled, extend selection
|
|
121
|
+
if (hasModifier && this.wrapper.multiSelect) {
|
|
122
|
+
this.executeAction('extend')
|
|
123
|
+
} else {
|
|
124
|
+
// Otherwise just select the current item
|
|
125
|
+
this.executeAction('select')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Always emit a move event for navigation
|
|
129
|
+
this.emitActionEvent('move')
|
|
130
|
+
return true
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Handle click events
|
|
135
|
+
* @param {MouseEvent} event - The click event
|
|
136
|
+
*/
|
|
137
|
+
handleClick(event) {
|
|
138
|
+
const node = getClosestAncestorWithAttribute(event.target, 'data-path')
|
|
139
|
+
|
|
140
|
+
if (!node) return
|
|
141
|
+
|
|
142
|
+
const path = getPathFromKey(node.getAttribute('data-path'))
|
|
143
|
+
// Check if click was on a toggle icon
|
|
144
|
+
const toggleIconClicked = this.handleToggleIconClick(path, event.target)
|
|
145
|
+
|
|
146
|
+
if (toggleIconClicked) return
|
|
147
|
+
|
|
148
|
+
// Move to the clicked item
|
|
149
|
+
this.wrapper.moveTo(path)
|
|
150
|
+
|
|
151
|
+
// Handle selection based on modifier keys
|
|
152
|
+
if (this.hasModifierKey(event) && this.wrapper.multiSelect) {
|
|
153
|
+
this.executeAction('extend', path)
|
|
154
|
+
} else {
|
|
155
|
+
this.executeAction('select', path)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// this.executeAction('toggle', path)
|
|
159
|
+
event.stopPropagation()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Handle clicks on toggle icons
|
|
164
|
+
* @param {number[]} path - The path of the item to perform the action on
|
|
165
|
+
* @param {HTMLElement} target - The clicked element
|
|
166
|
+
* @returns {boolean} - Whether a toggle icon was clicked and handled
|
|
167
|
+
*/
|
|
168
|
+
handleToggleIconClick(path, target) {
|
|
169
|
+
const isIcon = target.tagName.toLowerCase() === 'rk-icon'
|
|
170
|
+
const state = target.getAttribute('data-state')
|
|
171
|
+
if (!isIcon || !['closed', 'opened'].includes(state)) return false
|
|
172
|
+
|
|
173
|
+
return this.executeAction('toggle', path)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Execute an action if available
|
|
178
|
+
* @param {string} actionName - The name of the action to execute
|
|
179
|
+
* @param {number[]} path - The path of the item to perform the action on
|
|
180
|
+
* @returns {boolean} - Whether the action was executed
|
|
181
|
+
*/
|
|
182
|
+
executeAction(actionName, path) {
|
|
183
|
+
// Get the action function based on action name
|
|
184
|
+
const action = this.getActionFunction(actionName)
|
|
185
|
+
|
|
186
|
+
if (action) {
|
|
187
|
+
const executed = path ? action(path) : action()
|
|
188
|
+
if (executed) this.emitActionEvent(actionName)
|
|
189
|
+
|
|
190
|
+
return executed
|
|
191
|
+
}
|
|
192
|
+
return false
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Get the appropriate action function based on action name and conditions
|
|
197
|
+
* @param {string} actionName - The name of the action
|
|
198
|
+
* @returns {Function|null} - The action function or null if not available
|
|
199
|
+
*/
|
|
200
|
+
getActionFunction(actionName) {
|
|
201
|
+
// Basic navigation and selection actions always available
|
|
202
|
+
const actions = {
|
|
203
|
+
prev: () => this.wrapper.movePrev(),
|
|
204
|
+
next: () => this.wrapper.moveNext(),
|
|
205
|
+
select: (path) => this.wrapper.select(path),
|
|
206
|
+
collapse: (path) => this.wrapper.collapse(path),
|
|
207
|
+
expand: (path) => this.wrapper.expand(path),
|
|
208
|
+
toggle: (path) => this.wrapper.toggleExpansion(path),
|
|
209
|
+
extend: (path) => this.wrapper.extendSelection(path),
|
|
210
|
+
delete: () => this.wrapper.delete()
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return actions[actionName] || null
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Emit an action event
|
|
218
|
+
* @param {string} eventName - The name of the event to emit
|
|
219
|
+
*/
|
|
220
|
+
emitActionEvent(eventName) {
|
|
221
|
+
const data = {
|
|
222
|
+
path: this.wrapper.currentNode?.path,
|
|
223
|
+
value: this.wrapper.currentNode?.value
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// For select events, include selected nodes
|
|
227
|
+
if (['select', 'extend'].includes(eventName)) {
|
|
228
|
+
data.selected = Array.from(this.wrapper.selectedNodes.values()).map((node) => node.value)
|
|
229
|
+
// Normalize selection events to 'select'
|
|
230
|
+
eventName = 'select'
|
|
231
|
+
} else if (['prev', 'next'].includes(eventName)) {
|
|
232
|
+
eventName = 'move'
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
this.root.dispatchEvent(
|
|
236
|
+
new CustomEvent('action', {
|
|
237
|
+
detail: {
|
|
238
|
+
eventName,
|
|
239
|
+
data
|
|
240
|
+
}
|
|
241
|
+
})
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Navigator action for Svelte components
|
|
248
|
+
* @param {HTMLElement} root - The root element
|
|
249
|
+
* @param {Object} params - Parameters including wrapper and options
|
|
250
|
+
*/
|
|
251
|
+
export function navigator(root, { wrapper, options }) {
|
|
252
|
+
const controller = new NavigatorController(root, wrapper, options)
|
|
253
|
+
|
|
254
|
+
$effect(() => {
|
|
255
|
+
const cleanupFunctions = controller.init()
|
|
256
|
+
return () => {
|
|
257
|
+
cleanupFunctions.forEach((cleanup) => cleanup())
|
|
258
|
+
}
|
|
259
|
+
})
|
|
260
|
+
}
|
package/src/navigator.svelte.js
CHANGED
|
@@ -1,98 +1,106 @@
|
|
|
1
1
|
import { on } from 'svelte/events'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { omit } from 'ramda'
|
|
3
|
+
import { getClickAction, getKeyboardAction, getPathFromEvent } from './utils'
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
prev: ['ArrowLeft'],
|
|
7
|
-
next: ['ArrowRight'],
|
|
8
|
-
collapse: ['ArrowUp'],
|
|
9
|
-
expand: ['ArrowDown'],
|
|
10
|
-
select: ['Enter']
|
|
11
|
-
}
|
|
5
|
+
const defaultOptions = { horizontal: false, nested: false, enabled: true }
|
|
12
6
|
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
select: ['
|
|
7
|
+
const EVENT_MAP = {
|
|
8
|
+
first: ['move'],
|
|
9
|
+
last: ['move'],
|
|
10
|
+
previous: ['move'],
|
|
11
|
+
next: ['move'],
|
|
12
|
+
select: ['move', 'select'],
|
|
13
|
+
extend: ['move', 'select'],
|
|
14
|
+
collapse: ['toggle'],
|
|
15
|
+
expand: ['toggle'],
|
|
16
|
+
toggle: ['toggle']
|
|
19
17
|
}
|
|
20
18
|
|
|
21
19
|
/**
|
|
22
|
-
*
|
|
20
|
+
* The last only indicates that if there is an array only the last event is fired.
|
|
21
|
+
* This is crucial because a click event needs to fire both move and select,
|
|
22
|
+
* however the keyboard should only fire the select event because we are already
|
|
23
|
+
* on the current item
|
|
23
24
|
*
|
|
24
|
-
* @param {
|
|
25
|
-
* @param {
|
|
26
|
-
* @
|
|
25
|
+
* @param {HTMLElement} root
|
|
26
|
+
* @param {*} controller
|
|
27
|
+
* @param {*} name
|
|
27
28
|
*/
|
|
28
|
-
function
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
29
|
+
export function emitAction(root, controller, name, lastOnly = false) {
|
|
30
|
+
const events = lastOnly ? EVENT_MAP[name].slice(-1) : EVENT_MAP[name]
|
|
31
|
+
|
|
32
|
+
events.forEach((event) => {
|
|
33
|
+
root.dispatchEvent(
|
|
34
|
+
new CustomEvent('action', {
|
|
35
|
+
detail: {
|
|
36
|
+
name: event,
|
|
37
|
+
data: { value: controller.focused, selected: controller.selected }
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
)
|
|
41
|
+
})
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
/*
|
|
45
|
+
* Generic action handler for keyboard events.
|
|
46
|
+
*
|
|
47
|
+
* @param {Record<string, () => void>} actions
|
|
48
|
+
* @param {KeyboardEvent} event
|
|
49
|
+
*/
|
|
50
|
+
export function handleAction(event, handler, path) {
|
|
51
|
+
if (handler) {
|
|
52
|
+
event.preventDefault()
|
|
53
|
+
event.stopPropagation()
|
|
45
54
|
|
|
46
|
-
|
|
47
|
-
wrapper.expand()
|
|
48
|
-
} else if (state === 'opened') {
|
|
49
|
-
wrapper.collapse()
|
|
55
|
+
return handler(path)
|
|
50
56
|
}
|
|
51
|
-
return
|
|
57
|
+
return false
|
|
52
58
|
}
|
|
53
59
|
|
|
60
|
+
function getHandlers(wrapper) {
|
|
61
|
+
return {
|
|
62
|
+
first: () => wrapper.moveFirst(),
|
|
63
|
+
last: () => wrapper.moveLast(),
|
|
64
|
+
previous: () => wrapper.movePrev(),
|
|
65
|
+
next: () => wrapper.moveNext(),
|
|
66
|
+
collapse: () => wrapper.collapse(),
|
|
67
|
+
expand: () => wrapper.expand(),
|
|
68
|
+
select: (path) => wrapper.select(path),
|
|
69
|
+
extend: (path) => wrapper.extendSelection(path),
|
|
70
|
+
toggle: (path) => wrapper.toggleExpansion(path)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
54
73
|
/**
|
|
55
|
-
*
|
|
74
|
+
* A svelte action function that captures keyboard evvents and emits event for corresponding movements.
|
|
56
75
|
*
|
|
57
|
-
* @param {HTMLElement}
|
|
58
|
-
* @param {import('./types
|
|
76
|
+
* @param {HTMLElement} node
|
|
77
|
+
* @param {import('./types').NavigableOptions} options
|
|
78
|
+
* @returns {import('./types').SvelteActionReturn}
|
|
59
79
|
*/
|
|
60
|
-
export function navigator(
|
|
61
|
-
const
|
|
62
|
-
const
|
|
80
|
+
export function navigator(node, options) {
|
|
81
|
+
const { wrapper } = options
|
|
82
|
+
const config = { ...defaultOptions, ...omit(['wrapper'], options) }
|
|
83
|
+
const handlers = getHandlers(wrapper)
|
|
63
84
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
*/
|
|
69
|
-
const keyup = (event) => {
|
|
70
|
-
const { key } = event
|
|
71
|
-
|
|
72
|
-
const eventName = getEventForKey(keyMappings, key)
|
|
73
|
-
if (eventName) {
|
|
74
|
-
actions[eventName]()
|
|
75
|
-
}
|
|
85
|
+
const handleKeydown = (event) => {
|
|
86
|
+
const action = getKeyboardAction(event, config)
|
|
87
|
+
const handled = handleAction(event, handlers[action])
|
|
88
|
+
if (handled) emitAction(node, options.wrapper, action, true)
|
|
76
89
|
}
|
|
77
90
|
|
|
78
|
-
const
|
|
79
|
-
const
|
|
91
|
+
const handleClick = (event) => {
|
|
92
|
+
const action = getClickAction(event)
|
|
93
|
+
const path = getPathFromEvent(event)
|
|
94
|
+
const handled = handleAction(event, handlers[action], path)
|
|
80
95
|
|
|
81
|
-
if (node)
|
|
82
|
-
const iconClicked = handleIconClick(event.target, wrapper)
|
|
83
|
-
const path = getPathFromKey(node.getAttribute('data-path'))
|
|
84
|
-
wrapper.select(path, event.ctrlKey || event.metaKey)
|
|
85
|
-
root.dispatchEvent(new CustomEvent('activate'))
|
|
86
|
-
if (!iconClicked) wrapper.toggleExpansion()
|
|
87
|
-
}
|
|
96
|
+
if (handled) emitAction(node, options.wrapper, action)
|
|
88
97
|
}
|
|
89
98
|
|
|
90
99
|
$effect(() => {
|
|
91
|
-
const
|
|
92
|
-
|
|
100
|
+
const cleanup = [on(node, 'keyup', handleKeydown), on(node, 'click', handleClick)]
|
|
101
|
+
|
|
93
102
|
return () => {
|
|
94
|
-
|
|
95
|
-
cleanupClickEvent()
|
|
103
|
+
cleanup.forEach((fn) => fn())
|
|
96
104
|
}
|
|
97
105
|
})
|
|
98
106
|
}
|
package/src/types.js
CHANGED
|
@@ -48,3 +48,16 @@
|
|
|
48
48
|
* @property {Navigator} wrapper - Whether the navigator is enabled
|
|
49
49
|
* @property {NavigatorOptions} options - Whether the navigator is vertical or horizontal
|
|
50
50
|
*/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @typedef {Object} Controller
|
|
54
|
+
* @property {Function} moveNext
|
|
55
|
+
* @property {Function} movePrev
|
|
56
|
+
* @property {Function} moveFirst
|
|
57
|
+
* @property {Function} moveLast
|
|
58
|
+
* @property {Function} [expand]
|
|
59
|
+
* @property {Function} [collapse]
|
|
60
|
+
* @property {Function} select
|
|
61
|
+
* @property {Function} extendSelection
|
|
62
|
+
* @property {Function} [toggleExpansion]
|
|
63
|
+
*/
|
package/src/utils.js
CHANGED
|
@@ -49,20 +49,147 @@ export function handleAction(actions, event) {
|
|
|
49
49
|
* @param {import('./types').NavigableHandlers} handlers
|
|
50
50
|
*/
|
|
51
51
|
export function getKeyboardActions(options, handlers) {
|
|
52
|
+
const { horizontal, nested } = options
|
|
52
53
|
if (!options.enabled) return {}
|
|
53
54
|
|
|
54
|
-
const
|
|
55
|
+
const common = {
|
|
56
|
+
Enter: handlers.select,
|
|
57
|
+
' ': handlers.select
|
|
58
|
+
}
|
|
59
|
+
const movement = horizontal
|
|
55
60
|
? { ArrowLeft: handlers.previous, ArrowRight: handlers.next }
|
|
56
61
|
: { ArrowUp: handlers.previous, ArrowDown: handlers.next }
|
|
57
|
-
const change =
|
|
58
|
-
?
|
|
59
|
-
|
|
60
|
-
|
|
62
|
+
const change = horizontal
|
|
63
|
+
? { ArrowUp: handlers.collapse, ArrowDown: handlers.expand }
|
|
64
|
+
: { ArrowLeft: handlers.collapse, ArrowRight: handlers.expand }
|
|
65
|
+
|
|
66
|
+
if (nested) return { ...common, ...movement, ...change }
|
|
67
|
+
return { ...common, ...movement }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Finds and returns an index path based on data-path attribute
|
|
72
|
+
*
|
|
73
|
+
* @param {MouseEvent} event
|
|
74
|
+
* @returns {number[]|null} null or index path array
|
|
75
|
+
*/
|
|
76
|
+
export function getPathFromEvent(event) {
|
|
77
|
+
const node = getClosestAncestorWithAttribute(event.target, 'data-path')
|
|
78
|
+
return node?.getAttribute('data-path')
|
|
79
|
+
// return node ? getPathFromKey(node.getAttribute('data-path')) : null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Creates a keyboard action mapping based on navigation options
|
|
84
|
+
*
|
|
85
|
+
* @param {Object} options - Navigation options
|
|
86
|
+
* @param {boolean} options.horizontal - Whether navigation is horizontal
|
|
87
|
+
* @param {boolean} options.nested - Whether navigation is nested
|
|
88
|
+
* @returns {Object} Mapping of keys to actions
|
|
89
|
+
*/
|
|
90
|
+
function createKeyboardActionMap(options) {
|
|
91
|
+
const { horizontal, nested } = options
|
|
92
|
+
|
|
93
|
+
// Define movement actions based on horizontal option
|
|
94
|
+
const movementActions = horizontal
|
|
95
|
+
? { ArrowLeft: 'previous', ArrowRight: 'next' }
|
|
96
|
+
: { ArrowUp: 'previous', ArrowDown: 'next' }
|
|
97
|
+
|
|
98
|
+
// Define expand/collapse actions for nested option
|
|
99
|
+
const nestedActions = nested
|
|
100
|
+
? horizontal
|
|
101
|
+
? { ArrowUp: 'collapse', ArrowDown: 'expand' }
|
|
102
|
+
: { ArrowLeft: 'collapse', ArrowRight: 'expand' }
|
|
61
103
|
: {}
|
|
104
|
+
|
|
105
|
+
// Common actions regardless of options
|
|
106
|
+
const commonActions = {
|
|
107
|
+
Enter: 'select',
|
|
108
|
+
' ': 'select',
|
|
109
|
+
Home: 'first',
|
|
110
|
+
End: 'last'
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Combine all possible actions
|
|
62
114
|
return {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
...
|
|
66
|
-
...change
|
|
115
|
+
...commonActions,
|
|
116
|
+
...movementActions,
|
|
117
|
+
...nestedActions
|
|
67
118
|
}
|
|
68
119
|
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Creates a keyboard action mapping based on navigation options
|
|
123
|
+
*
|
|
124
|
+
* @param {Object} options - Navigation options
|
|
125
|
+
* @param {boolean} options.horizontal - Whether navigation is horizontal
|
|
126
|
+
* @param {boolean} options.nested - Whether navigation is nested
|
|
127
|
+
* @returns {Object} Mapping of keys to actions
|
|
128
|
+
*/
|
|
129
|
+
function createModifierKeyboardActionMap(options) {
|
|
130
|
+
const { horizontal } = options
|
|
131
|
+
const common = { ' ': 'extend', Home: 'first', End: 'last' }
|
|
132
|
+
const directional = horizontal
|
|
133
|
+
? { ArrowLeft: 'first', ArrowRight: 'last' }
|
|
134
|
+
: { ArrowUp: 'first', ArrowDown: 'last' }
|
|
135
|
+
return { ...common, ...directional }
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Identifies if an element is a collapsible icon
|
|
139
|
+
* @param {HTMLElement} target
|
|
140
|
+
* @returns
|
|
141
|
+
*/
|
|
142
|
+
function isNodeToggle(target) {
|
|
143
|
+
return (
|
|
144
|
+
target &&
|
|
145
|
+
target.getAttribute('data-tag') === 'icon' &&
|
|
146
|
+
['closed', 'opened'].includes(target.getAttribute('data-state'))
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Determines an action based on a keyboard event and navigation options
|
|
151
|
+
*
|
|
152
|
+
* @param {KeyboardEvent} event - The keyboard event
|
|
153
|
+
* @param {Object} options - Navigation options
|
|
154
|
+
* @param {boolean} options.horizontal - Whether navigation is horizontal
|
|
155
|
+
* @param {boolean} options.nested - Whether navigation is nested
|
|
156
|
+
* @returns {string|null} The determined action or null if no action matches
|
|
157
|
+
*/
|
|
158
|
+
export const getKeyboardAction = (event, options) => {
|
|
159
|
+
const { key, ctrlKey, metaKey } = event
|
|
160
|
+
|
|
161
|
+
// Check for modifier keys first (highest priority)
|
|
162
|
+
if (ctrlKey || metaKey) {
|
|
163
|
+
const modifierMap = createModifierKeyboardActionMap(options)
|
|
164
|
+
return modifierMap[key] || null
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Get the action map based on options
|
|
168
|
+
const actionMap = createKeyboardActionMap(options)
|
|
169
|
+
|
|
170
|
+
// Return the action or null if no matching key
|
|
171
|
+
return actionMap[key] || null
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Determines an action based on a click event
|
|
176
|
+
*
|
|
177
|
+
* @param {MouseEvent} event - The click event
|
|
178
|
+
* @returns {string} The determined action
|
|
179
|
+
*/
|
|
180
|
+
export const getClickAction = (event) => {
|
|
181
|
+
const { ctrlKey, metaKey, target } = event
|
|
182
|
+
|
|
183
|
+
// Check for modifier keys first (highest priority)
|
|
184
|
+
if (ctrlKey || metaKey) {
|
|
185
|
+
return 'extend'
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Check if clicked on icon with collapsed/expanded state
|
|
189
|
+
if (isNodeToggle(target) || isNodeToggle(target.parentElement)) {
|
|
190
|
+
return 'toggle'
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Default action
|
|
194
|
+
return 'select'
|
|
195
|
+
}
|