@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rokkit/actions",
3
- "version": "1.0.0-next.106",
3
+ "version": "1.0.0-next.107",
4
4
  "description": "Contains generic actions that can be used in various components.",
5
5
  "author": "Jerry Thomas <me@jerrythomas.name>",
6
6
  "license": "MIT",
@@ -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
- * Get actions for the navigator
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
- function getActions(wrapper, root) {
29
- const actions = {
30
- prev: () => wrapper.movePrev(),
31
- next: () => wrapper.moveNext(),
32
- select: () => {
33
- wrapper.select()
34
- root.dispatchEvent(new CustomEvent('activate'))
35
- },
36
- collapse: () => wrapper.collapse?.(),
37
- expand: () => wrapper.expand?.()
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
- function handleIconClick(target, wrapper) {
43
- const isIcon = target.tagName.toLowerCase() === 'rk-icon'
44
- const state = isIcon ? target.getAttribute('data-state') : null
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
- if (state === 'closed') {
47
- wrapper.expand()
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
- * Handle keyboard events
56
- *
57
- * @param {HTMLElement} root
58
- * @param {import('./types.js').NavigatorConfig} options - Custom key mappings
59
- */
60
- export function navigator(root, { wrapper, options }) {
61
- const keyMappings = options?.direction === 'horizontal' ? Horizontal : Vertical
62
- const actions = getActions(wrapper, root)
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
- const keyup = (event) => {
69
+ handleKeyUp(event) {
70
70
  const { key } = event
71
71
 
72
- const eventName = getEventForKey(keyMappings, key)
73
- if (eventName) {
74
- actions[eventName]()
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
- const click = (event) => {
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
- 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()
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 cleanupKeyupEvent = on(root, 'keyup', keyup)
92
- const cleanupClickEvent = on(root, 'click', click)
254
+ const cleanupFunctions = controller.init()
93
255
  return () => {
94
- cleanupKeyupEvent()
95
- cleanupClickEvent()
256
+ cleanupFunctions.forEach((cleanup) => cleanup())
96
257
  }
97
258
  })
98
259
  }