@rokkit/actions 1.0.0-next.99 → 1.0.2
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/README.md +189 -1
- package/package.json +20 -31
- package/src/{delegate.js → delegate.svelte.js} +10 -10
- package/src/{dismissable.js → dismissable.svelte.js} +11 -11
- package/src/{fillable.js → fillable.svelte.js} +54 -45
- package/src/hover-lift.svelte.js +64 -0
- package/src/index.js +19 -12
- package/src/kbd.js +191 -0
- package/src/keyboard.svelte.js +59 -0
- package/src/keymap.js +89 -0
- package/src/lib/event-manager.js +35 -18
- package/src/lib/index.js +0 -2
- package/src/lib/internal.js +0 -152
- package/src/magnetic.svelte.js +63 -0
- package/src/nav-constants.js +61 -0
- package/src/navigable.svelte.js +40 -0
- package/src/navigator.js +241 -151
- package/src/navigator.svelte.js +235 -0
- package/src/{pannable.js → pannable.svelte.js} +38 -39
- package/src/reveal.svelte.js +147 -0
- package/src/ripple.svelte.js +92 -0
- package/src/skinnable.svelte.js +12 -0
- package/src/{swipeable.js → swipeable.svelte.js} +79 -89
- package/src/themable.svelte.js +46 -0
- package/src/tooltip.svelte.js +149 -0
- package/src/trigger.js +126 -0
- package/src/types.js +39 -108
- package/src/utils.js +137 -15
- package/LICENSE +0 -21
- package/src/hierarchy.js +0 -156
- package/src/lib/constants.js +0 -35
- package/src/lib/viewport.js +0 -123
- package/src/navigable.js +0 -46
- package/src/switchable.js +0 -52
- package/src/themeable.js +0 -42
- package/src/traversable.js +0 -385
package/src/kbd.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gets horizontal movement actions based on text direction
|
|
3
|
+
* @param {Object} handlers - Handler functions
|
|
4
|
+
* @param {string} [dir='ltr'] - Text direction ('ltr' or 'rtl')
|
|
5
|
+
* @returns {Object} Object mapping arrow keys to movement handlers
|
|
6
|
+
*/
|
|
7
|
+
function getHorizontalMovementActions(handlers, dir = 'ltr') {
|
|
8
|
+
return dir === 'rtl'
|
|
9
|
+
? { ArrowRight: handlers.previous, ArrowLeft: handlers.next }
|
|
10
|
+
: { ArrowLeft: handlers.previous, ArrowRight: handlers.next }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Gets vertical movement actions (not affected by direction)
|
|
15
|
+
* @param {Object} handlers - Handler functions
|
|
16
|
+
* @returns {Object} Object mapping arrow keys to movement handlers
|
|
17
|
+
*/
|
|
18
|
+
function getVerticalMovementActions(handlers) {
|
|
19
|
+
return { ArrowUp: handlers.previous, ArrowDown: handlers.next }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Gets horizontal expand/collapse actions (not affected by direction)
|
|
24
|
+
* @param {Object} handlers - Handler functions
|
|
25
|
+
* @returns {Object} Object mapping arrow keys to expand/collapse handlers
|
|
26
|
+
*/
|
|
27
|
+
function getHorizontalExpandActions(handlers) {
|
|
28
|
+
return { ArrowUp: handlers.collapse, ArrowDown: handlers.expand }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Gets vertical expand/collapse actions based on text direction
|
|
33
|
+
* @param {Object} handlers - Handler functions
|
|
34
|
+
* @param {string} [dir='ltr'] - Text direction ('ltr' or 'rtl')
|
|
35
|
+
* @returns {Object} Object mapping arrow keys to expand/collapse handlers
|
|
36
|
+
*/
|
|
37
|
+
function getVerticalExpandActions(handlers, dir = 'ltr') {
|
|
38
|
+
return dir === 'rtl'
|
|
39
|
+
? { ArrowRight: handlers.collapse, ArrowLeft: handlers.expand }
|
|
40
|
+
: { ArrowLeft: handlers.collapse, ArrowRight: handlers.expand }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Gets common selection actions
|
|
45
|
+
* @param {Object} handlers - Handler functions
|
|
46
|
+
* @returns {Object} Object mapping keys to selection handlers
|
|
47
|
+
*/
|
|
48
|
+
function getCommonActions(handlers) {
|
|
49
|
+
return {
|
|
50
|
+
Enter: handlers.select,
|
|
51
|
+
' ': handlers.select
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Default navigation options
|
|
56
|
+
export const defaultNavigationOptions = {
|
|
57
|
+
orientation: 'vertical',
|
|
58
|
+
dir: 'ltr',
|
|
59
|
+
nested: false,
|
|
60
|
+
enabled: true,
|
|
61
|
+
typeahead: false
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Gets keyboard action handlers based on orientation and direction
|
|
66
|
+
* @param {Object} options - Configuration options
|
|
67
|
+
* @param {Object} handlers - Event handler functions
|
|
68
|
+
* @returns {Object} Object mapping key presses to handler functions
|
|
69
|
+
*/
|
|
70
|
+
export function getKeyboardActions(options, handlers) {
|
|
71
|
+
const { orientation, dir, nested, enabled } = { ...defaultNavigationOptions, ...options }
|
|
72
|
+
|
|
73
|
+
if (!enabled) return {}
|
|
74
|
+
|
|
75
|
+
const common = getCommonActions(handlers)
|
|
76
|
+
|
|
77
|
+
// Determine movement actions based on orientation
|
|
78
|
+
const isHorizontal = orientation === 'horizontal'
|
|
79
|
+
const movement = isHorizontal
|
|
80
|
+
? getHorizontalMovementActions(handlers, dir)
|
|
81
|
+
: getVerticalMovementActions(handlers)
|
|
82
|
+
|
|
83
|
+
// If not nested, we don't need expand/collapse actions
|
|
84
|
+
if (!nested) {
|
|
85
|
+
return { ...common, ...movement }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Determine expand/collapse actions based on orientation
|
|
89
|
+
const expandCollapse = isHorizontal
|
|
90
|
+
? getHorizontalExpandActions(handlers)
|
|
91
|
+
: getVerticalExpandActions(handlers, dir)
|
|
92
|
+
|
|
93
|
+
return { ...common, ...movement, ...expandCollapse }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function buildHorizontalMovementMap(dir) {
|
|
97
|
+
return dir === 'rtl'
|
|
98
|
+
? { ArrowRight: 'previous', ArrowLeft: 'next' }
|
|
99
|
+
: { ArrowLeft: 'previous', ArrowRight: 'next' }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function buildVerticalNestedMap(dir) {
|
|
103
|
+
return dir === 'rtl'
|
|
104
|
+
? { ArrowRight: 'collapse', ArrowLeft: 'expand' }
|
|
105
|
+
: { ArrowLeft: 'collapse', ArrowRight: 'expand' }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function buildNestedActions(isHorizontal, dir) {
|
|
109
|
+
if (isHorizontal) return { ArrowUp: 'collapse', ArrowDown: 'expand' }
|
|
110
|
+
return buildVerticalNestedMap(dir)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Creates a keyboard action mapping based on navigation options
|
|
115
|
+
*
|
|
116
|
+
* @param {Object} options - Navigation options
|
|
117
|
+
* @param {string} options.orientation - Whether navigation is horizontal or vertical
|
|
118
|
+
* @param {string} options.dir - Text direction ('ltr' or 'rtl')
|
|
119
|
+
* @param {boolean} options.nested - Whether navigation is nested
|
|
120
|
+
* @returns {Object} Mapping of keys to actions
|
|
121
|
+
*/
|
|
122
|
+
export function createKeyboardActionMap(options) {
|
|
123
|
+
const { orientation, dir, nested } = options
|
|
124
|
+
const isHorizontal = orientation === 'horizontal'
|
|
125
|
+
|
|
126
|
+
const movementActions = isHorizontal
|
|
127
|
+
? buildHorizontalMovementMap(dir)
|
|
128
|
+
: { ArrowUp: 'previous', ArrowDown: 'next' }
|
|
129
|
+
|
|
130
|
+
const nestedActions = nested ? buildNestedActions(isHorizontal, dir) : {}
|
|
131
|
+
|
|
132
|
+
const commonActions = {
|
|
133
|
+
Enter: 'select',
|
|
134
|
+
' ': 'select',
|
|
135
|
+
Home: 'first',
|
|
136
|
+
End: 'last'
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { ...commonActions, ...movementActions, ...nestedActions }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Creates a keyboard action mapping for modifier keys based on navigation options
|
|
144
|
+
*
|
|
145
|
+
* @param {Object} options - Navigation options
|
|
146
|
+
* @param {string} options.orientation - Whether navigation is horizontal or vertical
|
|
147
|
+
* @returns {Object} Mapping of keys to actions
|
|
148
|
+
*/
|
|
149
|
+
export function createModifierKeyboardActionMap(options) {
|
|
150
|
+
const isHorizontal = options.orientation === 'horizontal'
|
|
151
|
+
const common = { ' ': 'extend', Home: 'first', End: 'last' }
|
|
152
|
+
const directional = isHorizontal
|
|
153
|
+
? { ArrowLeft: 'first', ArrowRight: 'last' }
|
|
154
|
+
: { ArrowUp: 'first', ArrowDown: 'last' }
|
|
155
|
+
return { ...common, ...directional }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Creates a keyboard action mapping for shift key combinations
|
|
160
|
+
*
|
|
161
|
+
* @returns {Object} Mapping of keys to actions
|
|
162
|
+
*/
|
|
163
|
+
export function createShiftKeyboardActionMap() {
|
|
164
|
+
return { ' ': 'range' }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const KEY_LAYER_RESOLVERS = {
|
|
168
|
+
shift: (key, _opts) => createShiftKeyboardActionMap()[key] || null,
|
|
169
|
+
modifier: (key, opts) => createModifierKeyboardActionMap(opts)[key] || null,
|
|
170
|
+
plain: (key, opts) => createKeyboardActionMap(opts)[key] || null
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function getKeyLayer(ctrlKey, metaKey, shiftKey) {
|
|
174
|
+
if (ctrlKey) return 'modifier'
|
|
175
|
+
if (metaKey) return 'modifier'
|
|
176
|
+
if (shiftKey) return 'shift'
|
|
177
|
+
return 'plain'
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Gets the keyboard action for a key event
|
|
182
|
+
* @param {KeyboardEvent} event - The keyboard event
|
|
183
|
+
* @param {Object} options - Configuration options
|
|
184
|
+
* @returns {string|null} The action to perform, or null if no action is defined
|
|
185
|
+
*/
|
|
186
|
+
export function getKeyboardAction(event, options = {}) {
|
|
187
|
+
const { key, ctrlKey, metaKey, shiftKey } = event
|
|
188
|
+
const mergedOptions = { ...defaultNavigationOptions, ...options }
|
|
189
|
+
const layer = getKeyLayer(ctrlKey, metaKey, shiftKey)
|
|
190
|
+
return KEY_LAYER_RESOLVERS[layer](key, mergedOptions)
|
|
191
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { on } from 'svelte/events'
|
|
2
|
+
import { getClosestAncestorWithAttribute, getEventForKey } from './utils.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Default key mappings
|
|
6
|
+
* @type {import('./types.js').KeyboardConfig}
|
|
7
|
+
*/
|
|
8
|
+
const defaultKeyMappings = {
|
|
9
|
+
remove: ['Backspace', 'Delete'],
|
|
10
|
+
submit: ['Enter'],
|
|
11
|
+
add: /^[a-zA-Z]$/
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Handle keyboard events
|
|
16
|
+
*
|
|
17
|
+
* @param {HTMLElement} root
|
|
18
|
+
* @param {import('./types.js').KeyboardConfig} options - Custom key mappings
|
|
19
|
+
*/
|
|
20
|
+
export function keyboard(root, options = null) {
|
|
21
|
+
const keyMappings = options ?? defaultKeyMappings
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Handle keyboard events
|
|
25
|
+
*
|
|
26
|
+
* @param {KeyboardEvent} event
|
|
27
|
+
*/
|
|
28
|
+
const keyup = (event) => {
|
|
29
|
+
const { key } = event
|
|
30
|
+
|
|
31
|
+
const eventName = getEventForKey(keyMappings, key)
|
|
32
|
+
// verify that the target is a child of the root element?
|
|
33
|
+
if (eventName) {
|
|
34
|
+
root.dispatchEvent(new CustomEvent(eventName, { detail: key }))
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const click = (event) => {
|
|
39
|
+
const node = getClosestAncestorWithAttribute(event.target, 'data-key')
|
|
40
|
+
|
|
41
|
+
if (node) {
|
|
42
|
+
const key = node.getAttribute('data-key')
|
|
43
|
+
const eventName = getEventForKey(keyMappings, key)
|
|
44
|
+
|
|
45
|
+
if (eventName) {
|
|
46
|
+
root.dispatchEvent(new CustomEvent(eventName, { detail: key }))
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
$effect(() => {
|
|
52
|
+
const cleanupKeyupEvent = on(root, 'keyup', keyup)
|
|
53
|
+
const cleanupClickEvent = on(root, 'click', click)
|
|
54
|
+
return () => {
|
|
55
|
+
cleanupKeyupEvent()
|
|
56
|
+
cleanupClickEvent()
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
}
|
package/src/keymap.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List Keymap
|
|
3
|
+
*
|
|
4
|
+
* Maps keyboard inputs to semantic actions.
|
|
5
|
+
*
|
|
6
|
+
* Design principle: orientation is just a rotation of arrow key assignments.
|
|
7
|
+
*
|
|
8
|
+
* vertical ltr up/down = prev/next left/right = collapse/expand (when collapsible)
|
|
9
|
+
* vertical rtl up/down = prev/next right/left = collapse/expand (expand/collapse reversed)
|
|
10
|
+
* horizontal left/right = prev/next up/down = collapse/expand (dir ignored — use CSS flex-reverse for RTL)
|
|
11
|
+
*
|
|
12
|
+
* ─── Actions ────────────────────────────────────────────────────────────────
|
|
13
|
+
*
|
|
14
|
+
* next focus next visible item, skip disabled
|
|
15
|
+
* prev focus previous visible item, skip disabled
|
|
16
|
+
* first jump to first visible item
|
|
17
|
+
* last jump to last visible item
|
|
18
|
+
* expand when collapsible: expand collapsed group
|
|
19
|
+
* if already expanded: move focus to first child
|
|
20
|
+
* on leaf: no-op
|
|
21
|
+
* collapse when collapsible: collapse expanded group
|
|
22
|
+
* if already collapsed or leaf: move focus to parent
|
|
23
|
+
* at root level: no-op
|
|
24
|
+
* select activate the focused item
|
|
25
|
+
* extend toggle individual selection (multiselect ctrl/cmd + space)
|
|
26
|
+
* range select contiguous range (multiselect shift + space)
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { PLAIN_FIXED, CTRL_FIXED, SHIFT_FIXED, ARROWS } from './nav-constants.js'
|
|
30
|
+
export { ACTIONS } from './nav-constants.js'
|
|
31
|
+
|
|
32
|
+
function getArrows(orientation, dir) {
|
|
33
|
+
if (orientation === 'horizontal') return ARROWS.horizontal
|
|
34
|
+
return ARROWS[`vertical-${dir}`]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function buildPlainLayer(arrows, collapsible) {
|
|
38
|
+
return {
|
|
39
|
+
...PLAIN_FIXED,
|
|
40
|
+
...arrows.move,
|
|
41
|
+
...(collapsible ? arrows.nested : {})
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ─── buildKeymap ──────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Build a complete keymap for the given options.
|
|
49
|
+
*
|
|
50
|
+
* Returns three layers — plain, shift, ctrl — each mapping key name → action name.
|
|
51
|
+
* Call resolveAction(event, keymap) to look up the action for a keyboard event.
|
|
52
|
+
*
|
|
53
|
+
* @param {Object} [options]
|
|
54
|
+
* @param {'vertical'|'horizontal'} [options.orientation='vertical']
|
|
55
|
+
* @param {'ltr'|'rtl'} [options.dir='ltr']
|
|
56
|
+
* @param {boolean} [options.collapsible=false]
|
|
57
|
+
* @returns {{ plain: Record<string, string>, shift: Record<string, string>, ctrl: Record<string, string> }}
|
|
58
|
+
*/
|
|
59
|
+
export function buildKeymap({ orientation = 'vertical', dir = 'ltr', collapsible = false } = {}) {
|
|
60
|
+
const arrows = getArrows(orientation, dir)
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
plain: buildPlainLayer(arrows, collapsible),
|
|
64
|
+
shift: { ...SHIFT_FIXED },
|
|
65
|
+
ctrl: { ...CTRL_FIXED }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ─── resolveAction ────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
function pickLayer(shiftKey, ctrlKey, metaKey) {
|
|
72
|
+
if (ctrlKey) return 'ctrl'
|
|
73
|
+
if (metaKey) return 'ctrl'
|
|
74
|
+
if (shiftKey) return 'shift'
|
|
75
|
+
return 'plain'
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Resolve the action for a keyboard event given a pre-built keymap.
|
|
80
|
+
* Returns null if the key has no binding.
|
|
81
|
+
*
|
|
82
|
+
* @param {KeyboardEvent} event
|
|
83
|
+
* @param {{ plain: Record<string, string>, shift: Record<string, string>, ctrl: Record<string, string> }} keymap
|
|
84
|
+
* @returns {string|null}
|
|
85
|
+
*/
|
|
86
|
+
export function resolveAction(event, keymap) {
|
|
87
|
+
const { key, ctrlKey, metaKey, shiftKey } = event
|
|
88
|
+
return keymap[pickLayer(shiftKey, ctrlKey, metaKey)][key] ?? null
|
|
89
|
+
}
|
package/src/lib/event-manager.js
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
import { on } from 'svelte/events'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reset an event listener.
|
|
5
|
+
* @param {string} event - The event name.
|
|
6
|
+
* @param {Object} registry - The object containing the event listeners.
|
|
7
|
+
*/
|
|
8
|
+
function resetEvent(event, registry) {
|
|
9
|
+
if (typeof registry[event] === 'function') {
|
|
10
|
+
registry[event]()
|
|
11
|
+
delete registry[event]
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
1
15
|
/**
|
|
2
16
|
* EventManager class to manage event listeners on an element.
|
|
3
17
|
*
|
|
@@ -5,18 +19,19 @@
|
|
|
5
19
|
* @param {Object} handlers - An object with event names as keys and event handlers as values.
|
|
6
20
|
* @returns {Object} - An object with methods to activate, reset, and update the event listeners.
|
|
7
21
|
*/
|
|
8
|
-
export function EventManager(element, handlers = {}) {
|
|
9
|
-
|
|
22
|
+
export function EventManager(element, handlers = {}, enabled = true) {
|
|
23
|
+
const registeredEvents = {}
|
|
24
|
+
const options = { handlers, enabled }
|
|
10
25
|
|
|
11
26
|
/**
|
|
12
27
|
* Activate the event listeners.
|
|
13
28
|
*/
|
|
14
29
|
function activate() {
|
|
15
|
-
if (
|
|
16
|
-
Object.entries(handlers).forEach(([event, handler]) =>
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
30
|
+
if (options.enabled) {
|
|
31
|
+
Object.entries(options.handlers).forEach(([event, handler]) => {
|
|
32
|
+
resetEvent(event, registeredEvents)
|
|
33
|
+
registeredEvents[event] = on(element, event, handler)
|
|
34
|
+
})
|
|
20
35
|
}
|
|
21
36
|
}
|
|
22
37
|
|
|
@@ -24,12 +39,9 @@ export function EventManager(element, handlers = {}) {
|
|
|
24
39
|
* Reset the event listeners.
|
|
25
40
|
*/
|
|
26
41
|
function reset() {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
)
|
|
31
|
-
listening = false
|
|
32
|
-
}
|
|
42
|
+
Object.keys(registeredEvents).forEach((event) => {
|
|
43
|
+
resetEvent(event, registeredEvents)
|
|
44
|
+
})
|
|
33
45
|
}
|
|
34
46
|
|
|
35
47
|
/**
|
|
@@ -39,12 +51,17 @@ export function EventManager(element, handlers = {}) {
|
|
|
39
51
|
* @param {boolean} enabled - Whether to enable or disable the event listeners.
|
|
40
52
|
*/
|
|
41
53
|
function update(newHandlers = handlers, enabled = true) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
54
|
+
const hasChanged = handlers !== newHandlers
|
|
55
|
+
|
|
56
|
+
if (!enabled) reset()
|
|
57
|
+
|
|
58
|
+
if (hasChanged) {
|
|
59
|
+
options.handlers = newHandlers
|
|
60
|
+
options.enabled = enabled
|
|
61
|
+
activate()
|
|
46
62
|
}
|
|
47
63
|
}
|
|
48
64
|
|
|
49
|
-
|
|
65
|
+
activate()
|
|
66
|
+
return { reset, update }
|
|
50
67
|
}
|
package/src/lib/index.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
export { dimensionAttributes, defaultResizerOptions, defaultVirtualListOptions } from './constants'
|
|
2
1
|
// skipcq: JS-E1004 - Needed for exposing all functions
|
|
3
2
|
export * from './internal'
|
|
4
3
|
export { EventManager } from './event-manager'
|
|
5
|
-
export { virtualListViewport } from './viewport'
|
package/src/lib/internal.js
CHANGED
|
@@ -1,58 +1,3 @@
|
|
|
1
|
-
import { compact, hasChildren, isExpanded } from '@rokkit/core'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Emits a custom event with the given data.
|
|
5
|
-
*
|
|
6
|
-
* @param {HTMLElement} element
|
|
7
|
-
* @param {string} event
|
|
8
|
-
* @param {*} data
|
|
9
|
-
* @returns {void}
|
|
10
|
-
*/
|
|
11
|
-
export function emit(element, event, data) {
|
|
12
|
-
element.dispatchEvent(new CustomEvent(event, { detail: data }))
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Maps keyboard events to actions based on the given handlers and options.
|
|
17
|
-
*
|
|
18
|
-
* @param {import('../types').ActionHandlers} handlers
|
|
19
|
-
* @param {import('../types').NavigationOptions} options
|
|
20
|
-
* @returns {import('../types').KeyboardActions}
|
|
21
|
-
*/
|
|
22
|
-
export function mapKeyboardEventsToActions(handlers, options) {
|
|
23
|
-
const { next, previous, select, escape } = handlers
|
|
24
|
-
const { horizontal, nested } = {
|
|
25
|
-
horizontal: false,
|
|
26
|
-
nested: false,
|
|
27
|
-
...options
|
|
28
|
-
}
|
|
29
|
-
const expand = nested ? handlers.expand : null
|
|
30
|
-
const collapse = nested ? handlers.collapse : null
|
|
31
|
-
|
|
32
|
-
return compact({
|
|
33
|
-
ArrowDown: horizontal ? expand : next,
|
|
34
|
-
ArrowUp: horizontal ? collapse : previous,
|
|
35
|
-
ArrowRight: horizontal ? next : expand,
|
|
36
|
-
ArrowLeft: horizontal ? previous : collapse,
|
|
37
|
-
Enter: select,
|
|
38
|
-
Escape: escape,
|
|
39
|
-
' ': select
|
|
40
|
-
})
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Finds the closest ancestor of the given element that has the given attribute.
|
|
45
|
-
*
|
|
46
|
-
* @param {HTMLElement} element
|
|
47
|
-
* @param {string} attribute
|
|
48
|
-
* @returns {HTMLElement|null}
|
|
49
|
-
*/
|
|
50
|
-
export function getClosestAncestorWithAttribute(element, attribute) {
|
|
51
|
-
if (!element) return null
|
|
52
|
-
if (element.getAttribute(attribute)) return element
|
|
53
|
-
return getClosestAncestorWithAttribute(element.parentElement, attribute)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
1
|
/**
|
|
57
2
|
* Sets up event handlers based on the given options.
|
|
58
3
|
* Returns whether or not the event handlers are listening.
|
|
@@ -86,100 +31,3 @@ export function removeListeners(element, listeners) {
|
|
|
86
31
|
})
|
|
87
32
|
}
|
|
88
33
|
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Handles the click event.
|
|
92
|
-
* @param {HTMLElement} element - The root element.
|
|
93
|
-
* @param {CurrentItem} current - A reference to the current Item
|
|
94
|
-
* @returns {CurrentItem} The updated current item.
|
|
95
|
-
*/
|
|
96
|
-
export function handleItemClick(element, current) {
|
|
97
|
-
const { item, fields, position } = current
|
|
98
|
-
const detail = { item, position }
|
|
99
|
-
|
|
100
|
-
if (hasChildren(item, fields)) {
|
|
101
|
-
if (isExpanded(item, fields)) {
|
|
102
|
-
item[fields.isOpen] = false
|
|
103
|
-
emit(element, 'collapse', detail)
|
|
104
|
-
} else {
|
|
105
|
-
item[fields.isOpen] = true
|
|
106
|
-
emit(element, 'expand', detail)
|
|
107
|
-
}
|
|
108
|
-
} else {
|
|
109
|
-
emit(element, 'select', detail)
|
|
110
|
-
}
|
|
111
|
-
return current
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Caclulates sum of array values between the given bounds.
|
|
116
|
-
* If a value is null, the default size is used.
|
|
117
|
-
*
|
|
118
|
-
* @param {Array<number|null>} sizes
|
|
119
|
-
* @param {number} lower
|
|
120
|
-
* @param {number} upper
|
|
121
|
-
* @param {number} [defaultSize]
|
|
122
|
-
* @returns {number}
|
|
123
|
-
*/
|
|
124
|
-
export function calculateSum(sizes, lower, upper, defaultSize = 40, gap = 0) {
|
|
125
|
-
return (
|
|
126
|
-
sizes
|
|
127
|
-
.slice(lower, upper)
|
|
128
|
-
.map((size) => size ?? defaultSize)
|
|
129
|
-
.reduce((acc, size) => acc + size + gap, 0) - gap
|
|
130
|
-
)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Updates the sizes array with the given values.
|
|
135
|
-
*
|
|
136
|
-
* @param {Array<number|null>} sizes
|
|
137
|
-
* @param {Array<number>} values
|
|
138
|
-
* @param {number} [offset]
|
|
139
|
-
* @returns {Array<number|null>}
|
|
140
|
-
*/
|
|
141
|
-
export function updateSizes(sizes, values, offset = 0) {
|
|
142
|
-
const result = [...sizes.slice(0, offset), ...values, ...sizes.slice(offset + values.length)]
|
|
143
|
-
|
|
144
|
-
return result
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Adjusts the viewport to ensure that the bounds contain the given number of items.
|
|
149
|
-
*
|
|
150
|
-
* @param {import('../types').Bounds} current
|
|
151
|
-
* @param {number} count
|
|
152
|
-
* @param {number} visibleCount
|
|
153
|
-
* @returns {import('../types').Bounds}
|
|
154
|
-
*/
|
|
155
|
-
export function fixViewportForVisibileCount(current, count, visibleCount) {
|
|
156
|
-
let { lower, upper } = current
|
|
157
|
-
if (lower < 0) lower = 0
|
|
158
|
-
if (lower + visibleCount > count) {
|
|
159
|
-
upper = count
|
|
160
|
-
lower = Math.max(0, upper - visibleCount)
|
|
161
|
-
} else if (lower + visibleCount !== upper) {
|
|
162
|
-
upper = lower + visibleCount
|
|
163
|
-
}
|
|
164
|
-
return { lower, upper }
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Adjusts the viewport to ensure the given index is visible.
|
|
169
|
-
*
|
|
170
|
-
* @param {number} index
|
|
171
|
-
* @param {import('../types').Bounds} current
|
|
172
|
-
* @param {number} visibleCount
|
|
173
|
-
* @returns {import('../types').Bounds}
|
|
174
|
-
*/
|
|
175
|
-
export function fitIndexInViewport(index, current, visibleCount) {
|
|
176
|
-
let { lower, upper } = current
|
|
177
|
-
if (index >= upper) {
|
|
178
|
-
upper = index + 1
|
|
179
|
-
lower = upper - visibleCount
|
|
180
|
-
} else if (index < lower) {
|
|
181
|
-
lower = index
|
|
182
|
-
upper = lower + visibleCount
|
|
183
|
-
}
|
|
184
|
-
return { lower, upper }
|
|
185
|
-
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Magnetic action — element shifts subtly toward the cursor on hover.
|
|
3
|
+
* Calculates cursor offset from element center and translates proportionally.
|
|
4
|
+
*
|
|
5
|
+
* @param {HTMLElement} node
|
|
6
|
+
* @param {MagneticOptions} [options]
|
|
7
|
+
*
|
|
8
|
+
* @typedef {Object} MagneticOptions
|
|
9
|
+
* @property {number} [strength=0.3] Maximum displacement as fraction of element size (0–1)
|
|
10
|
+
* @property {number} [duration=300] Transition duration for return to center (ms)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
function resolveMagneticOpts(options) {
|
|
14
|
+
return { strength: 0.3, duration: 300, ...options }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isReducedMotion() {
|
|
18
|
+
return (
|
|
19
|
+
typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function applyMagnetic(node, opts) {
|
|
24
|
+
const originalTransform = node.style.transform
|
|
25
|
+
const originalTransition = node.style.transition
|
|
26
|
+
|
|
27
|
+
node.style.transition = `transform ${opts.duration}ms ease`
|
|
28
|
+
|
|
29
|
+
function onMove(e) {
|
|
30
|
+
const rect = node.getBoundingClientRect()
|
|
31
|
+
const centerX = rect.left + rect.width / 2
|
|
32
|
+
const centerY = rect.top + rect.height / 2
|
|
33
|
+
|
|
34
|
+
const offsetX = (e.clientX - centerX) * opts.strength
|
|
35
|
+
const offsetY = (e.clientY - centerY) * opts.strength
|
|
36
|
+
|
|
37
|
+
node.style.transition = 'none'
|
|
38
|
+
node.style.transform = `translate(${offsetX}px, ${offsetY}px)`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function onLeave() {
|
|
42
|
+
node.style.transition = `transform ${opts.duration}ms ease`
|
|
43
|
+
node.style.transform = originalTransform
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
node.addEventListener('mousemove', onMove)
|
|
47
|
+
node.addEventListener('mouseleave', onLeave)
|
|
48
|
+
|
|
49
|
+
return () => {
|
|
50
|
+
node.removeEventListener('mousemove', onMove)
|
|
51
|
+
node.removeEventListener('mouseleave', onLeave)
|
|
52
|
+
node.style.transform = originalTransform
|
|
53
|
+
node.style.transition = originalTransition
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function magnetic(node, options = {}) {
|
|
58
|
+
$effect(() => {
|
|
59
|
+
const opts = resolveMagneticOpts(options)
|
|
60
|
+
if (isReducedMotion()) return
|
|
61
|
+
return applyMagnetic(node, opts)
|
|
62
|
+
})
|
|
63
|
+
}
|