@rokkit/actions 1.0.0-next.100 → 1.0.0-next.103

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/src/hierarchy.js DELETED
@@ -1,156 +0,0 @@
1
- import { isExpanded } from '@rokkit/core'
2
-
3
- /**
4
- * Navigate to last visible child in the hirarchy starting with the provided path
5
- *
6
- * @param {Array<import('./types').PathFragment>} path - path to a node in the hierarchy
7
- * @returns
8
- */
9
- export function navigateToLastVisibleChild(path) {
10
- let current = path[path.length - 1]
11
-
12
- while (isExpanded(current.items[current.index], current.fields)) {
13
- const items = current.items[current.index][current.fields.children]
14
- const level = {
15
- items,
16
- index: items.length - 1,
17
- fields: current.fields.fields ?? current.fields
18
- }
19
- path.push(level)
20
- current = level
21
- }
22
-
23
- return path
24
- }
25
-
26
- /**
27
- * Navigate to the next item
28
- *
29
- * @param {Array<import('./types').PathFragment>} path - path to a node in the hierarchy
30
- * @param {Array<*>} items - array of items
31
- * @param {import('@rokkit/core').FieldMapping} fields - field mapping
32
- * @returns
33
- */
34
- export function moveNext(path, items, fields) {
35
- if (path.length === 0) {
36
- return [{ index: 0, items, fields }]
37
- }
38
-
39
- const current = path[path.length - 1]
40
- if (isExpanded(current.items[current.index], current.fields)) {
41
- path.push({
42
- items: current.items[current.index][current.fields.children],
43
- index: 0,
44
- fields: current.fields.fields || current.fields
45
- })
46
- } else if (current.index < current.items.length - 1) {
47
- current.index++
48
- } else {
49
- path = navigateToNextLevel(path)
50
- }
51
- return path
52
- }
53
-
54
- /**
55
- * Navigate to the next level
56
- * @param {Array<import('./types').PathFragment>} path
57
- * @returns {Array<import('./types').PathFragment>}
58
- */
59
- function navigateToNextLevel(path) {
60
- let level = path.length - 2
61
- while (level >= 0) {
62
- const parent = path[level]
63
- if (parent.index < parent.items.length - 1) {
64
- parent.index++
65
- path = path.slice(0, level + 1)
66
- break
67
- }
68
- level--
69
- }
70
- return path
71
- }
72
- /**
73
- * Navigate to the previous item
74
- *
75
- * @param {Array<import('./types').PathFragment>} path - path to a node in the hierarchy
76
- * @returns
77
- */
78
- export function movePrevious(path) {
79
- if (path.length === 0) return []
80
-
81
- const current = path[path.length - 1]
82
-
83
- if (current.index === 0) {
84
- if (path.length > 1) path.pop()
85
- return path
86
- }
87
-
88
- current.index--
89
- if (isExpanded(current.items[current.index], current.fields)) {
90
- return navigateToLastVisibleChild(path)
91
- }
92
- return path
93
- }
94
-
95
- /**
96
- *
97
- * @param {Array<integer>} indices
98
- * @param {Array<*>} items
99
- * @param {import('@rokkit/core').FieldMapping} fields
100
- * @returns
101
- */
102
- export function pathFromIndices(indices, items, fields) {
103
- const path = []
104
- let fragment = {}
105
- indices.forEach((index, level) => {
106
- if (level === 0) {
107
- fragment = { index, items, fields }
108
- } else {
109
- fragment = {
110
- index,
111
- items: fragment.items[fragment.index][fragment.fields.children],
112
- fields: fragment.fields.fields ?? fragment.fields
113
- }
114
- }
115
- path.push(fragment)
116
- })
117
- return path
118
- }
119
-
120
- /**
121
- * Get the indices from the path
122
- * @param {Array<import('./types').PathFragment>} path
123
- * @returns {Array<integer>}
124
- */
125
- export function indicesFromPath(path) {
126
- return path.map(({ index }) => index)
127
- }
128
-
129
- /**
130
- * Get the current node from the path
131
- * @param {Array<import('./types').PathFragment>} path
132
- * @returns {*}
133
- */
134
- export function getCurrentNode(path) {
135
- if (path.length === 0) return null
136
- const lastIndex = path.length - 1
137
- return path[lastIndex].items[path[lastIndex].index]
138
- }
139
-
140
- /**
141
- * Find the item in the hierarchy using the indices
142
- *
143
- * @param {Array<*>} items
144
- * @param {Array<integer>} indices
145
- * @param {import('@rokkit/core').FieldMapping} fields
146
- * @returns {*}
147
- */
148
- export function findItem(items, indices, fields) {
149
- let item = items[indices[0]]
150
- let levelFields = fields
151
- for (let level = 1; level < indices.length; level++) {
152
- item = item[levelFields.children][indices[level]]
153
- levelFields = levelFields.fields ?? levelFields
154
- }
155
- return item
156
- }
@@ -1,35 +0,0 @@
1
- export const dimensionAttributes = {
2
- vertical: {
3
- scroll: 'scrollTop',
4
- offset: 'offsetHeight',
5
- paddingStart: 'paddingTop',
6
- paddingEnd: 'paddingBottom',
7
- size: 'height'
8
- },
9
- horizontal: {
10
- scroll: 'scrollLeft',
11
- offset: 'offsetWidth',
12
- paddingStart: 'paddingLeft',
13
- paddingEnd: 'paddingRight',
14
- size: 'width'
15
- }
16
- }
17
-
18
- export const defaultResizerOptions = {
19
- horizontal: false,
20
- minSize: 40,
21
- minVisible: 1,
22
- maxVisible: null,
23
- availableSize: 200,
24
- start: 0
25
- }
26
-
27
- export const defaultVirtualListOptions = {
28
- itemSelector: 'virtual-list-item',
29
- contentSelector: 'virtual-list-content',
30
- enabled: true,
31
- horizontal: false,
32
- start: 0,
33
- limit: null,
34
- index: null
35
- }
@@ -1,50 +0,0 @@
1
- /**
2
- * EventManager class to manage event listeners on an element.
3
- *
4
- * @param {HTMLElement} element - The element to listen for events on.
5
- * @param {Object} handlers - An object with event names as keys and event handlers as values.
6
- * @returns {Object} - An object with methods to activate, reset, and update the event listeners.
7
- */
8
- export function EventManager(element, handlers = {}) {
9
- let listening = false
10
-
11
- /**
12
- * Activate the event listeners.
13
- */
14
- function activate() {
15
- if (!listening) {
16
- Object.entries(handlers).forEach(([event, handler]) =>
17
- element.addEventListener(event, handler)
18
- )
19
- listening = true
20
- }
21
- }
22
-
23
- /**
24
- * Reset the event listeners.
25
- */
26
- function reset() {
27
- if (listening) {
28
- Object.entries(handlers).forEach(([event, handler]) =>
29
- element.removeEventListener(event, handler)
30
- )
31
- listening = false
32
- }
33
- }
34
-
35
- /**
36
- * Update the event listeners.
37
- *
38
- * @param {Object} newHandlers - An object with event names as keys and event handlers as values.
39
- * @param {boolean} enabled - Whether to enable or disable the event listeners.
40
- */
41
- function update(newHandlers = handlers, enabled = true) {
42
- if (listening !== enabled || handlers !== newHandlers) {
43
- reset()
44
- handlers = newHandlers
45
- if (enabled) activate()
46
- }
47
- }
48
-
49
- return { activate, reset, update }
50
- }
package/src/lib/index.js DELETED
@@ -1,5 +0,0 @@
1
- export { dimensionAttributes, defaultResizerOptions, defaultVirtualListOptions } from './constants'
2
- // skipcq: JS-E1004 - Needed for exposing all functions
3
- export * from './internal'
4
- export { EventManager } from './event-manager'
5
- export { virtualListViewport } from './viewport'
@@ -1,185 +0,0 @@
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
- /**
57
- * Sets up event handlers based on the given options.
58
- * Returns whether or not the event handlers are listening.
59
- *
60
- * @param {HTMLElement} element
61
- * @param {import('../types').EventHandlers} listeners
62
- * @param {import('../types').TraversableOptions} options
63
- * @returns {void}
64
- */
65
- export function setupListeners(element, listeners, options) {
66
- const { enabled } = { enabled: true, ...options }
67
- if (enabled) {
68
- Object.entries(listeners).forEach(([event, listener]) =>
69
- element.addEventListener(event, listener)
70
- )
71
- }
72
- }
73
-
74
- /**
75
- * Removes event handlers based on the given options.
76
- * Returns whether or not the event handlers are listening.
77
- *
78
- * @param {HTMLElement} element
79
- * @param {import('../types').EventHandlers} listeners
80
- * @returns {void}
81
- */
82
- export function removeListeners(element, listeners) {
83
- if (listeners) {
84
- Object.entries(listeners).forEach(([event, listener]) => {
85
- element.removeEventListener(event, listener)
86
- })
87
- }
88
- }
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
- }
@@ -1,123 +0,0 @@
1
- import { writable, get } from 'svelte/store'
2
- import { pick } from 'ramda'
3
- import {
4
- updateSizes,
5
- calculateSum,
6
- fixViewportForVisibileCount,
7
- fitIndexInViewport
8
- } from './internal'
9
-
10
- export function virtualListViewport(options) {
11
- const { gap = 0 } = options
12
- let { minSize = 40, maxVisible = 0, visibleSize } = options
13
- let current = { lower: 0, upper: 0 }
14
- const bounds = writable({ lower: 0, upper: 0 })
15
- const space = writable({
16
- before: 0,
17
- after: 0
18
- })
19
- let items = null
20
- let averageSize = minSize
21
- let visibleCount = maxVisible
22
- let value = null
23
- let cache = []
24
- let index = -1
25
-
26
- const updateBounds = ({ lower, upper }) => {
27
- const previous = get(bounds)
28
- if (maxVisible > 0) {
29
- const visible = calculateSum(cache, lower, upper, averageSize, gap)
30
- space.update((state) => (state = { ...state, visible }))
31
- }
32
- if (previous.lower !== lower) {
33
- const before = calculateSum(cache, 0, lower, averageSize)
34
- space.update((state) => (state = { ...state, before }))
35
- }
36
- if (previous.upper !== upper) {
37
- const after = calculateSum(cache, upper, cache.length, averageSize)
38
- space.update((state) => (state = { ...state, after }))
39
- }
40
- if (previous.lower !== lower || previous.upper !== upper) {
41
- bounds.set({ lower, upper })
42
- }
43
- }
44
-
45
- const update = (data) => {
46
- // const previous = get(bounds)
47
-
48
- data = {
49
- start: current.lower,
50
- end: current.upper,
51
- value,
52
- ...data
53
- }
54
- items = data.items ?? items
55
- minSize = data.minSize ?? minSize
56
- maxVisible = data.maxVisible ?? maxVisible
57
- visibleSize = data.visibleSize ?? visibleSize
58
-
59
- if (items.length !== cache.length) {
60
- cache = Array.from({ length: items.length }).fill(null)
61
- if (items.length === 0) index = -1
62
- }
63
- current = { lower: data.start, upper: data.end }
64
-
65
- cache = updateSizes(cache, data.sizes ?? [], current.lower)
66
- averageSize =
67
- cache.length === 0
68
- ? minSize
69
- : calculateSum(cache, 0, cache.length, averageSize) / cache.length
70
-
71
- let visible = calculateSum(cache, current.lower, current.upper, averageSize, gap)
72
-
73
- if (maxVisible > 0) {
74
- visibleCount = maxVisible
75
- } else {
76
- while (visible < visibleSize) visible += averageSize
77
- while (visible - averageSize > visibleSize) visible -= averageSize
78
- visibleCount = Math.ceil(visible / averageSize)
79
- }
80
- current = fixViewportForVisibileCount(current, cache.length, visibleCount)
81
-
82
- // recalculate the lower, upper bounds based on current index
83
- if (items.length > 0 && data.value && data.value !== value) {
84
- index = items.findIndex((item) => item === data.value)
85
- if (index > -1) {
86
- value = data.value
87
- current = fitIndexInViewport(index, current, visibleCount)
88
- }
89
- }
90
- updateBounds(current)
91
- }
92
- const moveByOffset = (offset) => {
93
- if (cache.length > 0) {
94
- index = Math.max(0, Math.min(index + offset, cache.length - 1))
95
- current = fitIndexInViewport(index, current, visibleCount)
96
- updateBounds(current)
97
- }
98
- }
99
-
100
- const scrollTo = (position) => {
101
- const start = Math.round(position / averageSize)
102
- if (start !== current.lower) update({ start })
103
- }
104
-
105
- update(options)
106
-
107
- return {
108
- bounds: pick(['subscribe'], bounds),
109
- space: pick(['subscribe'], space),
110
- get index() {
111
- return index
112
- },
113
- update,
114
- scrollTo,
115
- moveByOffset,
116
- next: () => moveByOffset(1),
117
- previous: () => moveByOffset(-1),
118
- nextPage: () => moveByOffset(visibleCount),
119
- previousPage: () => moveByOffset(-visibleCount),
120
- first: () => moveByOffset(-cache.length),
121
- last: () => moveByOffset(cache.length + 1)
122
- }
123
- }
package/src/navigable.js DELETED
@@ -1,46 +0,0 @@
1
- import { handleAction, getKeyboardActions } from './utils'
2
-
3
- const defaultOptions = { horizontal: true, nested: false, enabled: true }
4
- /**
5
- * A svelte action function that captures keyboard evvents and emits event for corresponding movements.
6
- *
7
- * @param {HTMLElement} node
8
- * @param {import('./types').NavigableOptions} options
9
- * @returns {import('./types').SvelteActionReturn}
10
- */
11
- export function navigable(node, options) {
12
- options = { ...defaultOptions, ...options }
13
-
14
- let listening = false
15
- const handlers = {
16
- previous: () => node.dispatchEvent(new CustomEvent('previous')),
17
- next: () => node.dispatchEvent(new CustomEvent('next')),
18
- collapse: () => node.dispatchEvent(new CustomEvent('collapse')),
19
- expand: () => node.dispatchEvent(new CustomEvent('expand')),
20
- select: () => node.dispatchEvent(new CustomEvent('select'))
21
- }
22
-
23
- let actions = {} //getKeyboardActions(node, { horizontal, nested })
24
- const handleKeydown = (event) => handleAction(actions, event)
25
-
26
- /**
27
- * Update the listeners based on the input configuration.
28
- * @param {import('./types').NavigableOptions} input
29
- */
30
- function updateListeners(input) {
31
- options = { ...options, ...input }
32
- if (listening) node.removeEventListener('keydown', handleKeydown)
33
-
34
- actions = getKeyboardActions(node, input, handlers)
35
- if (input.enabled) node.addEventListener('keydown', handleKeydown)
36
-
37
- listening = input.enabled
38
- }
39
-
40
- updateListeners(options)
41
-
42
- return {
43
- update: (config) => updateListeners(config),
44
- destroy: () => updateListeners({ enabled: false })
45
- }
46
- }