@rokkit/actions 1.0.0-next.106 → 1.0.0-next.107
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.svelte.js +228 -67
package/package.json
CHANGED
package/src/navigator.svelte.js
CHANGED
|
@@ -2,97 +2,258 @@ import { on } from 'svelte/events'
|
|
|
2
2
|
import { getClosestAncestorWithAttribute, getEventForKey } from './utils.js'
|
|
3
3
|
import { getPathFromKey } from '@rokkit/core'
|
|
4
4
|
|
|
5
|
-
const Horizontal = {
|
|
6
|
-
prev: ['ArrowLeft'],
|
|
7
|
-
next: ['ArrowRight'],
|
|
8
|
-
collapse: ['ArrowUp'],
|
|
9
|
-
expand: ['ArrowDown'],
|
|
10
|
-
select: ['Enter']
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const Vertical = {
|
|
14
|
-
prev: ['ArrowUp'],
|
|
15
|
-
next: ['ArrowDown'],
|
|
16
|
-
collapse: ['ArrowLeft'],
|
|
17
|
-
expand: ['ArrowRight'],
|
|
18
|
-
select: ['Enter']
|
|
19
|
-
}
|
|
20
|
-
|
|
21
5
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* @param {import('./types.js').DataWrapper} wrapper - The navigator wrapper
|
|
25
|
-
* @param {HTMLElement} root - The root element
|
|
26
|
-
* @returns {import('./types.js').NavigatorActions} - The navigator actions
|
|
6
|
+
* Key mappings for different navigation directions
|
|
27
7
|
*/
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
prev:
|
|
31
|
-
next:
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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']
|
|
38
26
|
}
|
|
39
|
-
return actions
|
|
40
27
|
}
|
|
41
28
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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']
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
} else if (state === 'opened') {
|
|
49
|
-
wrapper.collapse()
|
|
45
|
+
this.handleKeyUp = this.handleKeyUp.bind(this)
|
|
46
|
+
this.handleClick = this.handleClick.bind(this)
|
|
50
47
|
}
|
|
51
|
-
return ['closed', 'opened'].includes(state)
|
|
52
|
-
}
|
|
53
48
|
|
|
54
|
-
/**
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
+
}
|
|
63
64
|
|
|
64
65
|
/**
|
|
65
66
|
* Handle keyboard events
|
|
66
|
-
*
|
|
67
|
-
* @param {KeyboardEvent} event
|
|
67
|
+
* @param {KeyboardEvent} event - The keyboard event
|
|
68
68
|
*/
|
|
69
|
-
|
|
69
|
+
handleKeyUp(event) {
|
|
70
70
|
const { key } = event
|
|
71
71
|
|
|
72
|
-
const eventName = getEventForKey(keyMappings, key)
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
75
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
|
|
76
131
|
}
|
|
77
132
|
|
|
78
|
-
|
|
133
|
+
/**
|
|
134
|
+
* Handle click events
|
|
135
|
+
* @param {MouseEvent} event - The click event
|
|
136
|
+
*/
|
|
137
|
+
handleClick(event) {
|
|
79
138
|
const node = getClosestAncestorWithAttribute(event.target, 'data-path')
|
|
80
139
|
|
|
81
|
-
if (node)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
+
this.executeAction('toggle', path)
|
|
158
|
+
event.stopPropagation()
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Handle clicks on toggle icons
|
|
163
|
+
* @param {number[]} path - The path of the item to perform the action on
|
|
164
|
+
* @param {HTMLElement} target - The clicked element
|
|
165
|
+
* @returns {boolean} - Whether a toggle icon was clicked and handled
|
|
166
|
+
*/
|
|
167
|
+
handleToggleIconClick(path, target) {
|
|
168
|
+
const isIcon = target.tagName.toLowerCase() === 'rk-icon'
|
|
169
|
+
const state = target.getAttribute('data-state')
|
|
170
|
+
if (!isIcon || !['closed', 'opened'].includes(state)) return false
|
|
171
|
+
|
|
172
|
+
return this.executeAction('toggle', path)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Execute an action if available
|
|
177
|
+
* @param {string} actionName - The name of the action to execute
|
|
178
|
+
* @param {number[]} path - The path of the item to perform the action on
|
|
179
|
+
* @returns {boolean} - Whether the action was executed
|
|
180
|
+
*/
|
|
181
|
+
executeAction(actionName, path) {
|
|
182
|
+
// Get the action function based on action name
|
|
183
|
+
const action = this.getActionFunction(actionName)
|
|
184
|
+
|
|
185
|
+
if (action) {
|
|
186
|
+
const executed = path ? action(path) : action()
|
|
187
|
+
if (executed) this.emitActionEvent(actionName)
|
|
188
|
+
|
|
189
|
+
return executed
|
|
190
|
+
}
|
|
191
|
+
return false
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Get the appropriate action function based on action name and conditions
|
|
196
|
+
* @param {string} actionName - The name of the action
|
|
197
|
+
* @returns {Function|null} - The action function or null if not available
|
|
198
|
+
*/
|
|
199
|
+
getActionFunction(actionName) {
|
|
200
|
+
// Basic navigation and selection actions always available
|
|
201
|
+
const actions = {
|
|
202
|
+
prev: () => this.wrapper.movePrev(),
|
|
203
|
+
next: () => this.wrapper.moveNext(),
|
|
204
|
+
select: (path) => this.wrapper.select(path),
|
|
205
|
+
collapse: (path) => this.wrapper.collapse(path),
|
|
206
|
+
expand: (path) => this.wrapper.expand(path),
|
|
207
|
+
toggle: (path) => this.wrapper.toggleExpansion(path),
|
|
208
|
+
extend: (path) => this.wrapper.extendSelection(path),
|
|
209
|
+
delete: () => this.wrapper.delete()
|
|
87
210
|
}
|
|
211
|
+
|
|
212
|
+
return actions[actionName] || null
|
|
88
213
|
}
|
|
89
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Emit an action event
|
|
217
|
+
* @param {string} eventName - The name of the event to emit
|
|
218
|
+
*/
|
|
219
|
+
emitActionEvent(eventName) {
|
|
220
|
+
const data = {
|
|
221
|
+
path: this.wrapper.currentNode?.path,
|
|
222
|
+
value: this.wrapper.currentNode?.value
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// For select events, include selected nodes
|
|
226
|
+
if (['select', 'extend'].includes(eventName)) {
|
|
227
|
+
data.selected = Array.from(this.wrapper.selectedNodes.values()).map((node) => node.value)
|
|
228
|
+
// Normalize selection events to 'select'
|
|
229
|
+
eventName = 'select'
|
|
230
|
+
} else if (['prev', 'next'].includes(eventName)) {
|
|
231
|
+
eventName = 'move'
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
this.root.dispatchEvent(
|
|
235
|
+
new CustomEvent('action', {
|
|
236
|
+
detail: {
|
|
237
|
+
eventName,
|
|
238
|
+
data
|
|
239
|
+
}
|
|
240
|
+
})
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Navigator action for Svelte components
|
|
247
|
+
* @param {HTMLElement} root - The root element
|
|
248
|
+
* @param {Object} params - Parameters including wrapper and options
|
|
249
|
+
*/
|
|
250
|
+
export function navigator(root, { wrapper, options }) {
|
|
251
|
+
const controller = new NavigatorController(root, wrapper, options)
|
|
252
|
+
|
|
90
253
|
$effect(() => {
|
|
91
|
-
const
|
|
92
|
-
const cleanupClickEvent = on(root, 'click', click)
|
|
254
|
+
const cleanupFunctions = controller.init()
|
|
93
255
|
return () => {
|
|
94
|
-
|
|
95
|
-
cleanupClickEvent()
|
|
256
|
+
cleanupFunctions.forEach((cleanup) => cleanup())
|
|
96
257
|
}
|
|
97
258
|
})
|
|
98
259
|
}
|