@rokkit/actions 1.0.0-next.44 → 1.0.0-next.48
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 +9 -9
- package/src/delegate.js +34 -0
- package/src/index.js +4 -0
- package/src/lib/constants.js +35 -0
- package/src/lib/event-manager.js +11 -10
- package/src/lib/index.js +2 -0
- package/src/lib/internal.js +76 -0
- package/src/lib/viewport.js +127 -0
- package/src/navigator.js +8 -4
- package/src/pannable.js +0 -1
- package/src/traversable.js +8 -6
- package/src/types.js +12 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rokkit/actions",
|
|
3
|
-
"version": "1.0.0-next.
|
|
3
|
+
"version": "1.0.0-next.48",
|
|
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",
|
|
@@ -13,18 +13,18 @@
|
|
|
13
13
|
"access": "public"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
|
-
"@sveltejs/vite-plugin-svelte": "^2.4.
|
|
16
|
+
"@sveltejs/vite-plugin-svelte": "^2.4.5",
|
|
17
17
|
"@testing-library/svelte": "^4.0.3",
|
|
18
18
|
"@types/ramda": "^0.29.3",
|
|
19
|
-
"@vitest/coverage-v8": "^0.
|
|
20
|
-
"@vitest/ui": "~0.
|
|
19
|
+
"@vitest/coverage-v8": "^0.34.4",
|
|
20
|
+
"@vitest/ui": "~0.34.4",
|
|
21
21
|
"jsdom": "^22.1.0",
|
|
22
|
-
"svelte": "^4.
|
|
23
|
-
"typescript": "^5.
|
|
22
|
+
"svelte": "^4.2.0",
|
|
23
|
+
"typescript": "^5.2.2",
|
|
24
24
|
"validators": "latest",
|
|
25
|
-
"vite": "^4.4.
|
|
26
|
-
"vitest": "~0.
|
|
27
|
-
"shared-config": "1.0.0-next.
|
|
25
|
+
"vite": "^4.4.9",
|
|
26
|
+
"vitest": "~0.34.4",
|
|
27
|
+
"shared-config": "1.0.0-next.48"
|
|
28
28
|
},
|
|
29
29
|
"files": [
|
|
30
30
|
"src/**/*.js",
|
package/src/delegate.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { EventManager } from './lib'
|
|
2
|
+
/**
|
|
3
|
+
* Svelte action function for forwarding keyboard events from a parent element to a child.
|
|
4
|
+
* The child is selected using a CSS selector passed in the options object.
|
|
5
|
+
* Optionally, you can specify which keyboard events you want to forward: "keydown", "keyup", and/or "keypress".
|
|
6
|
+
* By default, all three events are forwarded.
|
|
7
|
+
* The action returns an object with a destroy method.
|
|
8
|
+
* The destroy method removes all event listeners from the parent.
|
|
9
|
+
*
|
|
10
|
+
* @param {HTMLElement} element - The parent element from which keyboard events will be forwarded.
|
|
11
|
+
* @param {import('./types').PushDownOptions} options - The options object.
|
|
12
|
+
* @returns {{destroy: Function}}
|
|
13
|
+
*/
|
|
14
|
+
export function delegateKeyboardEvents(
|
|
15
|
+
element,
|
|
16
|
+
{ selector, events = ['keydown', 'keyup', 'keypress'] }
|
|
17
|
+
) {
|
|
18
|
+
const child = element.querySelector(selector)
|
|
19
|
+
const handlers = {}
|
|
20
|
+
const manager = EventManager(element)
|
|
21
|
+
|
|
22
|
+
function forwardEvent(event) {
|
|
23
|
+
child.dispatchEvent(new KeyboardEvent(event.type, event))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (child) {
|
|
27
|
+
events.forEach((event) => (handlers[event] = forwardEvent))
|
|
28
|
+
manager.update(handlers)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
destroy: () => manager.reset()
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import './types'
|
|
2
|
+
export * from './lib/constants'
|
|
3
|
+
export * from './lib'
|
|
2
4
|
export { fillable } from './fillable'
|
|
3
5
|
export { pannable } from './pannable'
|
|
4
6
|
export { navigable } from './navigable'
|
|
@@ -6,3 +8,5 @@ export { navigator } from './navigator'
|
|
|
6
8
|
export { dismissable } from './dismissable'
|
|
7
9
|
export { themable } from './themeable'
|
|
8
10
|
export { swipeable } from './swipeable'
|
|
11
|
+
export { delegateKeyboardEvents } from './delegate'
|
|
12
|
+
export { virtualListViewport } from './lib/viewport'
|
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
}
|
package/src/lib/event-manager.js
CHANGED
|
@@ -6,27 +6,28 @@ export function EventManager(element, handlers = {}) {
|
|
|
6
6
|
|
|
7
7
|
function activate() {
|
|
8
8
|
if (!listening) {
|
|
9
|
-
|
|
10
|
-
element.addEventListener(event,
|
|
11
|
-
|
|
9
|
+
Object.entries(handlers).forEach(([event, handler]) =>
|
|
10
|
+
element.addEventListener(event, handler)
|
|
11
|
+
)
|
|
12
12
|
listening = true
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
-
function
|
|
15
|
+
function reset() {
|
|
16
16
|
if (listening) {
|
|
17
|
-
|
|
18
|
-
element.removeEventListener(event,
|
|
19
|
-
|
|
17
|
+
Object.entries(handlers).forEach(([event, handler]) =>
|
|
18
|
+
element.removeEventListener(event, handler)
|
|
19
|
+
)
|
|
20
20
|
listening = false
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
-
function update(
|
|
23
|
+
function update(newHandlers = handlers, enabled = true) {
|
|
24
24
|
if (listening !== enabled || handlers !== newHandlers) {
|
|
25
|
-
|
|
25
|
+
reset()
|
|
26
26
|
handlers = newHandlers
|
|
27
|
+
// console.log(listening, enabled, handlers)
|
|
27
28
|
if (enabled) activate()
|
|
28
29
|
}
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
return { activate,
|
|
32
|
+
return { activate, reset, update }
|
|
32
33
|
}
|
package/src/lib/index.js
CHANGED
package/src/lib/internal.js
CHANGED
|
@@ -111,3 +111,79 @@ export function handleItemClick(element, current) {
|
|
|
111
111
|
}
|
|
112
112
|
return current
|
|
113
113
|
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Caclulates sum of array values between the given bounds.
|
|
117
|
+
* If a value is null, the default size is used.
|
|
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
|
+
let result = [
|
|
143
|
+
...sizes.slice(0, offset),
|
|
144
|
+
...values,
|
|
145
|
+
...sizes.slice(offset + values.length)
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
return result
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Adjusts the viewport to ensure that the bounds contain the given number of items.
|
|
153
|
+
*
|
|
154
|
+
* @param {import('../types').Bounds} current
|
|
155
|
+
* @param {number} count
|
|
156
|
+
* @param {number} visibleCount
|
|
157
|
+
* @returns {import('../types').Bounds}
|
|
158
|
+
*/
|
|
159
|
+
export function fixViewportForVisibileCount(current, count, visibleCount) {
|
|
160
|
+
let { lower, upper } = current
|
|
161
|
+
if (lower < 0) lower = 0
|
|
162
|
+
if (lower + visibleCount > count) {
|
|
163
|
+
upper = count
|
|
164
|
+
lower = Math.max(0, upper - visibleCount)
|
|
165
|
+
} else if (lower + visibleCount !== upper) {
|
|
166
|
+
upper = lower + visibleCount
|
|
167
|
+
}
|
|
168
|
+
return { lower, upper }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Adjusts the viewport to ensure the given index is visible.
|
|
173
|
+
*
|
|
174
|
+
* @param {number} index
|
|
175
|
+
* @param {import('../types').Bounds} current
|
|
176
|
+
* @param {number} visibleCount
|
|
177
|
+
* @returns {import('../types').Bounds}
|
|
178
|
+
*/
|
|
179
|
+
export function fitIndexInViewport(index, current, visibleCount) {
|
|
180
|
+
let { lower, upper } = current
|
|
181
|
+
if (index >= upper) {
|
|
182
|
+
upper = index + 1
|
|
183
|
+
lower = upper - visibleCount
|
|
184
|
+
} else if (index < lower) {
|
|
185
|
+
lower = index
|
|
186
|
+
upper = lower + visibleCount
|
|
187
|
+
}
|
|
188
|
+
return { lower, upper }
|
|
189
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
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
|
+
let { minSize = 40, maxVisible = 0, visibleSize, gap = 0 } = options
|
|
12
|
+
let current = { lower: 0, upper: 0 }
|
|
13
|
+
const bounds = writable({ lower: 0, upper: 0 })
|
|
14
|
+
const space = writable({
|
|
15
|
+
before: 0,
|
|
16
|
+
after: 0
|
|
17
|
+
})
|
|
18
|
+
let items
|
|
19
|
+
let averageSize = minSize
|
|
20
|
+
let visibleCount = maxVisible
|
|
21
|
+
let value = null
|
|
22
|
+
let cache = []
|
|
23
|
+
let index = -1
|
|
24
|
+
|
|
25
|
+
const update = (data) => {
|
|
26
|
+
// const previous = get(bounds)
|
|
27
|
+
|
|
28
|
+
data = {
|
|
29
|
+
start: current.lower,
|
|
30
|
+
end: current.upper,
|
|
31
|
+
value,
|
|
32
|
+
...data
|
|
33
|
+
}
|
|
34
|
+
items = data.items ?? items
|
|
35
|
+
minSize = data.minSize ?? minSize
|
|
36
|
+
maxVisible = data.maxVisible ?? maxVisible
|
|
37
|
+
visibleSize = data.visibleSize ?? visibleSize
|
|
38
|
+
|
|
39
|
+
if (items.length !== cache.length) {
|
|
40
|
+
cache = Array.from({ length: items.length }).fill(null)
|
|
41
|
+
if (items.length == 0) index = -1
|
|
42
|
+
}
|
|
43
|
+
current = { lower: data.start, upper: data.end }
|
|
44
|
+
|
|
45
|
+
cache = updateSizes(cache, data.sizes ?? [], current.lower)
|
|
46
|
+
averageSize =
|
|
47
|
+
cache.length == 0
|
|
48
|
+
? minSize
|
|
49
|
+
: calculateSum(cache, 0, cache.length, averageSize) / cache.length
|
|
50
|
+
|
|
51
|
+
let visible = calculateSum(
|
|
52
|
+
cache,
|
|
53
|
+
current.lower,
|
|
54
|
+
current.upper,
|
|
55
|
+
averageSize,
|
|
56
|
+
gap
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if (maxVisible > 0) {
|
|
60
|
+
visibleCount = maxVisible
|
|
61
|
+
} else {
|
|
62
|
+
while (visible < visibleSize) visible += averageSize
|
|
63
|
+
while (visible - averageSize > visibleSize) visible -= averageSize
|
|
64
|
+
visibleCount = Math.ceil(visible / averageSize)
|
|
65
|
+
}
|
|
66
|
+
current = fixViewportForVisibileCount(current, cache.length, visibleCount)
|
|
67
|
+
|
|
68
|
+
// recalculate the lower, upper bounds based on current index
|
|
69
|
+
if (items.length > 0 && data.value && data.value !== value) {
|
|
70
|
+
index = items.findIndex((item) => item === data.value)
|
|
71
|
+
if (index > -1) {
|
|
72
|
+
value = data.value
|
|
73
|
+
current = fitIndexInViewport(index, current, visibleCount)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
updateBounds(current)
|
|
77
|
+
}
|
|
78
|
+
const moveByOffset = (offset) => {
|
|
79
|
+
if (cache.length > 0) {
|
|
80
|
+
index = Math.max(0, Math.min(index + offset, cache.length - 1))
|
|
81
|
+
current = fitIndexInViewport(index, current, visibleCount)
|
|
82
|
+
updateBounds(current)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const updateBounds = ({ lower, upper }) => {
|
|
86
|
+
const previous = get(bounds)
|
|
87
|
+
if (maxVisible > 0) {
|
|
88
|
+
let visible = calculateSum(cache, lower, upper, averageSize, gap)
|
|
89
|
+
space.update((value) => (value = { ...value, visible }))
|
|
90
|
+
}
|
|
91
|
+
if (previous.lower !== lower) {
|
|
92
|
+
let before = calculateSum(cache, 0, lower, averageSize)
|
|
93
|
+
space.update((value) => (value = { ...value, before }))
|
|
94
|
+
}
|
|
95
|
+
if (previous.upper !== upper) {
|
|
96
|
+
let after = calculateSum(cache, upper, cache.length, averageSize)
|
|
97
|
+
space.update((value) => (value = { ...value, after }))
|
|
98
|
+
}
|
|
99
|
+
if (previous.lower !== lower || previous.upper !== upper) {
|
|
100
|
+
bounds.set({ lower, upper })
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const scrollTo = (position) => {
|
|
105
|
+
const start = Math.round(position / averageSize)
|
|
106
|
+
if (start !== current.lower) update({ start })
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
update(options)
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
bounds: pick(['subscribe'], bounds),
|
|
113
|
+
space: pick(['subscribe'], space),
|
|
114
|
+
get index() {
|
|
115
|
+
return index
|
|
116
|
+
},
|
|
117
|
+
update,
|
|
118
|
+
scrollTo,
|
|
119
|
+
moveByOffset,
|
|
120
|
+
next: () => moveByOffset(1),
|
|
121
|
+
previous: () => moveByOffset(-1),
|
|
122
|
+
nextPage: () => moveByOffset(visibleCount),
|
|
123
|
+
previousPage: () => moveByOffset(-visibleCount),
|
|
124
|
+
first: () => moveByOffset(-cache.length),
|
|
125
|
+
last: () => moveByOffset(cache.length + 1)
|
|
126
|
+
}
|
|
127
|
+
}
|
package/src/navigator.js
CHANGED
|
@@ -63,8 +63,7 @@ export function navigator(element, options) {
|
|
|
63
63
|
if (currentNode) {
|
|
64
64
|
const collapse = isExpanded(currentNode, path[path.length - 1].fields)
|
|
65
65
|
if (collapse) {
|
|
66
|
-
|
|
67
|
-
emit('collapse', element, indicesFromPath(path), currentNode)
|
|
66
|
+
toggle()
|
|
68
67
|
} else if (path.length > 0) {
|
|
69
68
|
path = path.slice(0, -1)
|
|
70
69
|
currentNode = getCurrentNode(path)
|
|
@@ -74,10 +73,15 @@ export function navigator(element, options) {
|
|
|
74
73
|
}
|
|
75
74
|
const expand = () => {
|
|
76
75
|
if (currentNode && hasChildren(currentNode, path[path.length - 1].fields)) {
|
|
77
|
-
|
|
78
|
-
emit('expand', element, indicesFromPath(path), currentNode)
|
|
76
|
+
toggle()
|
|
79
77
|
}
|
|
80
78
|
}
|
|
79
|
+
function toggle() {
|
|
80
|
+
const expanded = isExpanded(currentNode, path[path.length - 1].fields)
|
|
81
|
+
const event = expanded ? 'collapse' : 'expand'
|
|
82
|
+
currentNode[path[path.length - 1].fields.isOpen] = !expanded
|
|
83
|
+
emit(event, element, indicesFromPath(path), currentNode)
|
|
84
|
+
}
|
|
81
85
|
const handlers = { next, previous, select, collapse, expand }
|
|
82
86
|
|
|
83
87
|
update(options)
|
package/src/pannable.js
CHANGED
package/src/traversable.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { mappedList, isNested } from '@rokkit/core'
|
|
2
2
|
import { pick } from 'ramda'
|
|
3
3
|
import {
|
|
4
|
-
getClosestAncestorWithAttribute,
|
|
4
|
+
// getClosestAncestorWithAttribute,
|
|
5
5
|
mapKeyboardEventsToActions,
|
|
6
6
|
emit,
|
|
7
|
-
handleItemClick,
|
|
7
|
+
// handleItemClick,
|
|
8
8
|
EventManager
|
|
9
9
|
} from './lib'
|
|
10
10
|
|
|
@@ -32,7 +32,6 @@ export function traversable(element, options) {
|
|
|
32
32
|
const result = content[direction](current.position)
|
|
33
33
|
if (result) {
|
|
34
34
|
current = result
|
|
35
|
-
|
|
36
35
|
checkAndEmit('move')
|
|
37
36
|
}
|
|
38
37
|
}
|
|
@@ -70,11 +69,14 @@ export function traversable(element, options) {
|
|
|
70
69
|
}
|
|
71
70
|
|
|
72
71
|
const update = (data) => {
|
|
73
|
-
options = { ...defaultOptions, ...options, ...data }
|
|
72
|
+
options = { ...defaultOptions, ...options, value: current.item, ...data }
|
|
74
73
|
options.nested = isNested(options.items, options.fields)
|
|
75
74
|
content.update(options.items, options.fields)
|
|
76
75
|
handlers = mapKeyboardEventsToActions(actions, options)
|
|
77
|
-
manager.update(options.enabled
|
|
76
|
+
manager.update(listeners, options.enabled)
|
|
77
|
+
if (options.value !== null) {
|
|
78
|
+
current = { ...current, ...content.findByValue(options.value) }
|
|
79
|
+
}
|
|
78
80
|
// current = handleValueChange(element, data, content, current)
|
|
79
81
|
}
|
|
80
82
|
|
|
@@ -82,7 +84,7 @@ export function traversable(element, options) {
|
|
|
82
84
|
|
|
83
85
|
return {
|
|
84
86
|
update,
|
|
85
|
-
destroy: () => manager.
|
|
87
|
+
destroy: () => manager.reset()
|
|
86
88
|
}
|
|
87
89
|
}
|
|
88
90
|
|
package/src/types.js
CHANGED
|
@@ -116,3 +116,15 @@
|
|
|
116
116
|
* @property {number} startY - The start Y position of the touch.
|
|
117
117
|
* @property {number} startTime - The start time of the touch.
|
|
118
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
|
+
*/
|