@rokkit/actions 1.0.0-next.100

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.
@@ -0,0 +1,385 @@
1
+ import { has } from 'ramda'
2
+ import { EventManager } from './lib'
3
+
4
+ const defaultConfig = {
5
+ allowDrag: false,
6
+ allowDrop: false,
7
+ pageSize: 10,
8
+ horizontal: false,
9
+ vertical: true,
10
+ multiselect: false
11
+ }
12
+
13
+ /**
14
+ * A svelte action to add keyboard navigation to a list/tree/grid
15
+ *
16
+ * @param {HTMLElement} root - The DOM root node to add the action to
17
+ * @param {Object} config - The configuration object
18
+ * @param {Object} config.store - The store object with navigation methods
19
+ * @param {Object} config.options - The configuration options
20
+ * @param {number} config.options.pageSize - The number of items to move on page up/down
21
+ * @param {boolean} config.options.horizontal - The orientation of the list/tree
22
+ * @param {boolean} config.options.vertical - The orientation of the list/tree
23
+ */
24
+ export function traversable(root, config) {
25
+ const manager = EventManager(root, {})
26
+ const events = config.store.events
27
+
28
+ const unsubscribe = events.subscribe((data) => {
29
+ if (data.length > 0) {
30
+ data.forEach(({ type, detail }) => root.dispatchEvent(new CustomEvent(type, { detail })))
31
+ events.set([])
32
+ }
33
+ })
34
+
35
+ updateEventHandlers(root, manager, config)
36
+
37
+ return {
38
+ destroy: () => {
39
+ // console.log(typeof unsubscribe)
40
+ unsubscribe()
41
+ manager.reset()
42
+ },
43
+ update: (newConfig) => updateEventHandlers(root, manager, newConfig)
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Update the event handlers based on the configuration
49
+ *
50
+ * @param {HTMLElement} root - The DOM root node to add the action to
51
+ * @param {Object} manager - The event manager object
52
+ * @param {Object} config - The configuration object
53
+ */
54
+ function updateEventHandlers(root, manager, config) {
55
+ const store = config.store
56
+ const options = { ...defaultConfig, ...config.options }
57
+
58
+ const listeners = {
59
+ keydown: getKeydownHandler(store, options, root),
60
+ click: getClickHandler(store, options)
61
+ }
62
+ if (options.allowDrag) listeners.dragstart = getDragEventHandler(store, 'dragStart')
63
+ if (options.allowDrop) {
64
+ listeners.dragover = getDragEventHandler(store, 'dragOver')
65
+ listeners.drop = getDragEventHandler(store, 'dropOver')
66
+ }
67
+ manager.update(listeners)
68
+ }
69
+
70
+ /**
71
+ * Get a map of actions for various key combinations
72
+ *
73
+ * @param {Object} store - The store object with navigation methods
74
+ * @param {number} pageSize - The number of items to move on page up/down
75
+ */
76
+ function getKeyHandlers(store, options) {
77
+ const { pageSize, horizontal, vertical } = options
78
+ const isGrid = horizontal && vertical
79
+ const arrowActions = isGrid
80
+ ? getArrowKeyActionsForGrid(store)
81
+ : getArrowKeyActions(store, horizontal)
82
+
83
+ const actions = {
84
+ ...arrowActions,
85
+ PageUp: () => store.moveByOffset(-pageSize),
86
+ PageDown: () => store.moveByOffset(pageSize),
87
+ Home: () => store.moveFirst(),
88
+ End: () => store.moveLast(),
89
+ Enter: () => store.select(),
90
+ Escape: () => store.escape(),
91
+ ' ': () => store.select()
92
+ }
93
+
94
+ const modifierActions = {
95
+ ctrl: getMetaKeyActions(store, horizontal),
96
+ meta: getMetaKeyActions(store, horizontal),
97
+ shift: isGrid ? getShiftKeyActionsForGrid(store) : getShiftKeyActions(store, horizontal)
98
+ }
99
+
100
+ return { actions, modifierActions }
101
+ }
102
+
103
+ /**
104
+ * Get action handlers based on direction
105
+ *
106
+ * @param {Object} store - The store object with navigation methods
107
+ * @param {boolean} horizontal - if the content is navigable horizontally
108
+ */
109
+ function getArrowKeyActions(store, horizontal = false) {
110
+ if (horizontal) {
111
+ return {
112
+ ArrowUp: () => store.collapse(),
113
+ ArrowDown: () => store.expand(),
114
+ ArrowRight: () => store.moveByOffset(1),
115
+ ArrowLeft: () => store.moveByOffset(-1)
116
+ }
117
+ } else {
118
+ return {
119
+ ArrowUp: () => store.moveByOffset(-1),
120
+ ArrowDown: () => store.moveByOffset(1),
121
+ ArrowRight: () => store.expand(),
122
+ ArrowLeft: () => store.collapse()
123
+ }
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Get the handler function for the keydown event
129
+ *
130
+ * @param {Object} store - The store object with navigation methods
131
+ * @param {Object} options - The configuration options
132
+ */
133
+ function getClickHandler(store, options) {
134
+ const { multiselect = false } = options
135
+
136
+ function handleClick(event) {
137
+ const modifiers = identifyModifiers(event)
138
+ const indexPath = getTargetIndex(event)
139
+
140
+ if (!indexPath) return
141
+ event.stopPropagation()
142
+
143
+ if (isToggleStateIcon(event.target)) {
144
+ store.toggleExpansion(indexPath)
145
+ } else {
146
+ if (multiselect) {
147
+ handleMultiSelect(store, indexPath, modifiers)
148
+ } else {
149
+ store.moveTo(indexPath)
150
+ store.select(indexPath)
151
+ }
152
+ }
153
+ // dispatchEvents(event.target, store)
154
+ }
155
+
156
+ return handleClick
157
+ }
158
+
159
+ /**
160
+ * Get the handler function for the drag events
161
+ *
162
+ * @param {Object} store - The store object with navigation methods
163
+ * @param {string} eventName - The name of the event to dispatch
164
+ * @returns {Function} The event handler function
165
+ */
166
+ function getDragEventHandler(store, eventName) {
167
+ function handle(event) {
168
+ const index = getTargetIndex(event)
169
+ if (index) store[eventName](index)
170
+ }
171
+ return handle
172
+ }
173
+
174
+ /**
175
+ * Handle multi-select based on the modifier keys pressed
176
+ *
177
+ * @param {Object} store - The store object with navigation methods
178
+ * @param {number[]} index - The index path of the item to select
179
+ * @param {string[]} modifier - The modifier keys pressed
180
+ */
181
+ function handleMultiSelect(store, index, modifier) {
182
+ if (modifier.includes('shift')) {
183
+ store.selectRange(index)
184
+ } else if (modifier.includes('ctrl') || modifier.includes('meta')) {
185
+ store.toggleSelection(index)
186
+ } else {
187
+ store.select(index)
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Get the keydown event handler
193
+ *
194
+ * @param {Object} store - The store object with navigation methods
195
+ * @param {Object} options - The configuration options
196
+ * @param {HTMLElement} root - The root element to add the event listener to
197
+ */
198
+ function getKeydownHandler(store, options, root) {
199
+ const handlers = getKeyHandlers(store, options)
200
+
201
+ /**
202
+ * Use the keyboard event map to handle the keydown event
203
+ *
204
+ * @param {KeyboardEvent} event - The keyboard event
205
+ */
206
+ function handleKeydown(event) {
207
+ const action = getAction(event, handlers)
208
+ if (action) {
209
+ event.preventDefault()
210
+ action()
211
+ scrollIntoView(root, store)
212
+ // dispatchEvents(root, store)
213
+ }
214
+ }
215
+
216
+ return handleKeydown
217
+ }
218
+
219
+ /**
220
+ * Get the action for the keydown event
221
+ *
222
+ * @param {KeyboardEvent} event - The keyboard event
223
+ * @param {Object} handlers - The key handlers object
224
+ */
225
+ function getAction(event, handlers) {
226
+ const key = event.key.length === 1 ? event.key.toUpperCase() : event.key
227
+ const modifier = identifyModifiers(event).join('-')
228
+ if (modifier.length === 0) return handlers.actions[key]
229
+
230
+ if (has(modifier, handlers.modifierActions)) {
231
+ return handlers.modifierActions[modifier][key]
232
+ }
233
+ return null
234
+ }
235
+
236
+ /**
237
+ * Identify modifier keys pressed in the event
238
+ *
239
+ * @param {KeyboardEvent} event - The keyboard event
240
+ */
241
+ function identifyModifiers(event) {
242
+ const modifiers = []
243
+
244
+ if (event.ctrlKey) modifiers.push('ctrl')
245
+ if (event.shiftKey) modifiers.push('shift')
246
+ if (event.metaKey) modifiers.push('meta')
247
+
248
+ return modifiers
249
+ }
250
+
251
+ /**
252
+ * Get the meta key actions for a list/tree
253
+ *
254
+ * @param {Object} store - The store object with navigation methods
255
+ * @param {boolean} horizontal - The orientation of the list/tree
256
+ */
257
+ function getMetaKeyActions(store, horizontal = false) {
258
+ const actions = {
259
+ X: () => store.cut(),
260
+ C: () => store.copy(),
261
+ V: () => store.paste(),
262
+ A: () => store.selectAll(),
263
+ D: () => store.selectNone(),
264
+ I: () => store.selectInvert(),
265
+ Z: () => store.undo(),
266
+ Y: () => store.redo()
267
+ }
268
+ const horizontalActions = {
269
+ ArrowRight: () => store.moveLast(),
270
+ ArrowLeft: () => store.moveFirst()
271
+ }
272
+ const verticalActions = {
273
+ ArrowUp: () => store.moveFirst(),
274
+ ArrowDown: () => store.moveLast()
275
+ }
276
+ const arrowActions = horizontal ? horizontalActions : verticalActions
277
+
278
+ return { ...actions, ...arrowActions }
279
+ }
280
+
281
+ /**
282
+ * Get the shift key actions for a list
283
+ *
284
+ * @param {Object} store - The store object with navigation methods
285
+ * @param {boolean} horizontal - The orientation of the list/tree
286
+ */
287
+ function getShiftKeyActions(store, horizontal = false) {
288
+ const actions = {
289
+ Home: () => store.selectRange(-Infinity),
290
+ End: () => store.selectRange(Infinity)
291
+ }
292
+ const horizontalActions = {
293
+ ArrowRight: () => store.selectRange(1),
294
+ ArrowLeft: () => store.selectRange(-1)
295
+ }
296
+ const verticalActions = {
297
+ ArrowUp: () => store.selectRange(-1),
298
+ ArrowDown: () => store.selectRange(1)
299
+ }
300
+ const arrowActions = horizontal ? horizontalActions : verticalActions
301
+
302
+ return { ...actions, ...arrowActions }
303
+ }
304
+
305
+ /**
306
+ * Get the arrow key actions for a grid
307
+ *
308
+ * @param {Object} store - The store object with navigation methods
309
+ * @returns {Object} - The map of actions
310
+ */
311
+ function getArrowKeyActionsForGrid(store) {
312
+ return {
313
+ ArrowUp: () => store.moveByOffset(-1),
314
+ ArrowDown: () => store.moveByOffset(1),
315
+ ArrowRight: () => store.moveByOffset(0, 1),
316
+ ArrowLeft: () => store.moveByOffset(0, -1),
317
+ Home: () => store.moveByOffset(-Infinity, -Infinity),
318
+ End: () => store.moveByOffset(Infinity, Infinity)
319
+ }
320
+ }
321
+
322
+ /**
323
+ * Get the shift key actions for a grid
324
+ *
325
+ * @param {Object} store - The store object with navigation methods
326
+ * @returns {Object} - The map of actions
327
+ */
328
+ function getShiftKeyActionsForGrid(store) {
329
+ return {
330
+ ArrowUp: () => store.selectRange(-1),
331
+ ArrowDown: () => store.selectRange(1),
332
+ ArrowRight: () => store.selectRange(0, 1),
333
+ ArrowLeft: () => store.selectRange(0, -1),
334
+ Home: () => store.selectRange(0, -Infinity),
335
+ End: () => store.selectRange(0, Infinity)
336
+ }
337
+ }
338
+
339
+ /**
340
+ * Identify if an html element is a toggle state icon
341
+ * A toggle state icon element tag is ICON and has a data-state attribute value of 'opened' or 'closed'
342
+ *
343
+ * @param {HTMLElement} element - The html element to check
344
+ */
345
+ function isToggleStateIcon(element) {
346
+ return (
347
+ element.tagName === 'ICON' && ['opened', 'closed'].includes(element.getAttribute('data-state'))
348
+ )
349
+ }
350
+
351
+ /**
352
+ * Get the index of the target element
353
+ *
354
+ * @param {MouseEvent} event - The mouse event
355
+ */
356
+ function getTargetIndex(event) {
357
+ const target = event.target.closest('[data-index]')
358
+ if (target) return target.getAttribute('data-index').split('-').map(Number)
359
+
360
+ return null
361
+ }
362
+
363
+ /**
364
+ * Make the current item visible in the view
365
+ *
366
+ * @param {HTMLElement} root - The root element which contains the items
367
+ * @param {Object} store - The item to make visible
368
+ */
369
+ function scrollIntoView(root, store) {
370
+ const item = store.currentItem()
371
+ const dataIndex = item.indexPath.join('-')
372
+ const node = root.querySelector(`[data-index="${dataIndex}"]`)
373
+ if (node) node.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
374
+ }
375
+
376
+ /**
377
+ * Dispatch custom events based on the state changes
378
+ *
379
+ * @param {HTMLElement} root - The root element to dispatch the events from
380
+ * @param {Object} store - The store object with navigation methods
381
+ */
382
+ // function dispatchEvents(root, store) {
383
+ // const events = store.getEvents()
384
+ // events.forEach((event, detail) => root.dispatchEvent(new CustomEvent(event, { detail })))
385
+ // }
package/src/types.js ADDED
@@ -0,0 +1,132 @@
1
+ /**
2
+ * @typedef SvelteActionReturn
3
+ * @property {() => void} destroy
4
+ * @property {() => void} [update]
5
+ */
6
+
7
+ /**
8
+ * @typedef FillableData
9
+ * @property {string} value
10
+ * @property {integer} actualIndex
11
+ * @property {integer} expectedIndex
12
+ */
13
+
14
+ /**
15
+ * @typedef FillOptions
16
+ * @property {Array<FillableData>} options available options to fill
17
+ * @property {integer} current index of option to be filled
18
+ * @property {boolean} check validate filled values
19
+ */
20
+
21
+ /**
22
+ * A part of the path to node in hierarchy
23
+ *
24
+ * @typedef PathFragment
25
+ * @property {integer} index - Index to item in array
26
+ * @property {Array<*>} items - Array of items
27
+ * @property {import('@rokkit/core').FieldMapping} fields - Field mapping for the data
28
+ */
29
+
30
+ /**
31
+ * Options for the Navigable action
32
+ * @typedef NavigableOptions
33
+ * @property {boolean} horizontal - Navigate horizontally
34
+ * @property {boolean} nested - Navigate nested items
35
+ * @property {boolean} enabled - Enable navigation
36
+ */
37
+
38
+ /**
39
+ * @typedef NavigatorOptions
40
+ * @property {Array<*>} items - An array containing the data set to navigate
41
+ * @property {boolean} [vertical=true] - Identifies whether navigation shoud be vertical or horizontal
42
+ * @property {string} [idPrefix='id-'] - id prefix used for identifying individual node
43
+ * @property {import('../constants').FieldMapping} fields - Field mapping to identify attributes to be used for state and identification of children
44
+ */
45
+
46
+ /**
47
+ * @typedef SwipeableOptions
48
+ * @property {boolean} horizontal - Swipe horizontally
49
+ * @property {boolean} vertical - Swipe vertically
50
+ * @property {boolean} enabled - Enable swiping
51
+ * @property {number} threshold - Threshold for swipe
52
+ * @property {number} minSpeed - Minimum speed for swipe
53
+ */
54
+
55
+ /**
56
+ * @typedef TraversableOptions
57
+ * @property {boolean} horizontal - Traverse horizontally
58
+ * @property {boolean} nested - Traverse nested items
59
+ * @property {boolean} enabled - Enable traversal
60
+ * @property {string} value - Value to be used for traversal
61
+ * @property {Array<*>} items - An array containing the data set to traverse
62
+ * @property {Array<integer} [indices] - Indices of the items to be traversed
63
+ */
64
+
65
+ /**
66
+ * @typedef PositionTracker
67
+ * @property {integer} index
68
+ * @property {integer} previousIndex
69
+ */
70
+
71
+ /**
72
+ * @typedef EventHandlers
73
+ * @property {function} [keydown]
74
+ * @property {function} [keyup]
75
+ * @property {function} [click]
76
+ * @property {function} [touchstart]
77
+ * @property {function} [touchmove]
78
+ * @property {function} [touchend]
79
+ * @property {function} [touchcancel]
80
+ * @property {function} [mousedown]
81
+ * @property {function} [mouseup]
82
+ * @property {function} [mousemove]
83
+ */
84
+
85
+ /**
86
+ * @typedef {Object} ActionHandlers
87
+ * @property {Function} [next]
88
+ * @property {Function} [previous]
89
+ * @property {Function} [select]
90
+ * @property {Function} [escape]
91
+ * @property {Function} [collapse]
92
+ * @property {Function} [expand]
93
+ */
94
+
95
+ /**
96
+ * @typedef {Object} NavigationOptions
97
+ * @property {Boolean} [horizontal]
98
+ * @property {Boolean} [nested]
99
+ * @property {Boolean} [enabled]
100
+ */
101
+
102
+ /**
103
+ * @typedef {Object} KeyboardActions
104
+ * @property {Function} [ArrowDown]
105
+ * @property {Function} [ArrowUp]
106
+ * @property {Function} [ArrowRight]
107
+ * @property {Function} [ArrowLeft]
108
+ * @property {Function} [Enter]
109
+ * @property {Function} [Escape]
110
+ * @property {Function} [" "]
111
+ */
112
+
113
+ /**
114
+ * @typedef {Object} TouchTracker
115
+ * @property {number} startX - The start X position of the touch.
116
+ * @property {number} startY - The start Y position of the touch.
117
+ * @property {number} startTime - The start time of the touch.
118
+ */
119
+
120
+ /**
121
+ * @typedef {Object} PushDownOptions
122
+ * @property {string} selector - The CSS selector for the child element to which keyboard events will be forwarded.
123
+ * @property {Array<string>} [events=['keydown', 'keyup', 'keypress']] - The keyboard events to forward.
124
+ */
125
+
126
+ /**
127
+ * @typedef {Object} Bounds
128
+ * @property {number} lower
129
+ * @property {number} upper
130
+ */
131
+
132
+ export default {}
package/src/utils.js ADDED
@@ -0,0 +1,24 @@
1
+ export function handleAction(actions, event) {
2
+ if (event.key in actions) {
3
+ event.preventDefault()
4
+ event.stopPropagation()
5
+ actions[event.key]()
6
+ }
7
+ }
8
+
9
+ export function getKeyboardActions(node, options, handlers) {
10
+ const movement = options.horizontal
11
+ ? { ArrowLeft: handlers.previous, ArrowRight: handlers.next }
12
+ : { ArrowUp: handlers.previous, ArrowDown: handlers.next }
13
+ const change = options.nested
14
+ ? options.horizontal
15
+ ? { ArrowUp: handlers.collapse, ArrowDown: handlers.expand }
16
+ : { ArrowLeft: handlers.collapse, ArrowRight: handlers.expand }
17
+ : {}
18
+ return {
19
+ Enter: handlers.select,
20
+ ' ': handlers.select,
21
+ ...movement,
22
+ ...change
23
+ }
24
+ }