ajo-ui 0.1.0
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/LICENSE +15 -0
- package/README.md +126 -0
- package/dist/accordion.js +130 -0
- package/dist/avatar.js +54 -0
- package/dist/calendar.js +2 -0
- package/dist/carousel.js +239 -0
- package/dist/chart.js +790 -0
- package/dist/checkbox-group.js +70 -0
- package/dist/checkbox.js +77 -0
- package/dist/chunks/bar-CVafh6C1.js +99 -0
- package/dist/chunks/calendar-q8jZ8Cxr.js +1923 -0
- package/dist/chunks/collection-DtRB63U4.js +111 -0
- package/dist/chunks/data-table-DpkWv4y4.js +1039 -0
- package/dist/chunks/menu-B45IyHHC.js +704 -0
- package/dist/chunks/native-BJdhd9XJ.js +20 -0
- package/dist/chunks/popup-C8Bb3l_g.js +459 -0
- package/dist/chunks/popup-surface-BDCgtVU0.js +18 -0
- package/dist/chunks/position-D6_i_SRn.js +434 -0
- package/dist/chunks/virtual-list-B7hjGkjk.js +413 -0
- package/dist/collapsible.js +106 -0
- package/dist/command.js +263 -0
- package/dist/context-menu.js +112 -0
- package/dist/data-table.js +5 -0
- package/dist/dialog.js +207 -0
- package/dist/direction.js +22 -0
- package/dist/drawer.js +139 -0
- package/dist/field.js +26 -0
- package/dist/index.js +38 -0
- package/dist/input-date.js +994 -0
- package/dist/input-group.js +52 -0
- package/dist/input-otp.js +179 -0
- package/dist/menu.js +2 -0
- package/dist/menubar.js +236 -0
- package/dist/message-scroller.js +446 -0
- package/dist/navigation-menu.js +330 -0
- package/dist/popover.js +307 -0
- package/dist/progress.js +39 -0
- package/dist/radio-group.js +107 -0
- package/dist/resizable.js +172 -0
- package/dist/select.js +961 -0
- package/dist/sidebar.js +343 -0
- package/dist/slider.js +259 -0
- package/dist/switch.js +53 -0
- package/dist/tabs.js +182 -0
- package/dist/toast.js +492 -0
- package/dist/toggle-group.js +111 -0
- package/dist/toggle.js +52 -0
- package/dist/toolbar.js +127 -0
- package/dist/tooltip.js +196 -0
- package/dist/utils.js +104 -0
- package/dist/virtual-list.js +2 -0
- package/package.json +250 -0
- package/src/accordion.tsx +261 -0
- package/src/availability.ts +261 -0
- package/src/avatar.tsx +99 -0
- package/src/bar.ts +156 -0
- package/src/calendar.tsx +1441 -0
- package/src/carousel.tsx +424 -0
- package/src/chart.tsx +1194 -0
- package/src/checkbox-group.tsx +132 -0
- package/src/checkbox.tsx +130 -0
- package/src/collapsible.tsx +188 -0
- package/src/collection.ts +154 -0
- package/src/command.tsx +511 -0
- package/src/context-menu.tsx +233 -0
- package/src/data-table-contract.ts +143 -0
- package/src/data-table-model.ts +760 -0
- package/src/data-table.tsx +475 -0
- package/src/dialog.tsx +393 -0
- package/src/direction.tsx +45 -0
- package/src/drawer.tsx +251 -0
- package/src/field.tsx +61 -0
- package/src/index.ts +37 -0
- package/src/input-date.tsx +1539 -0
- package/src/input-group.tsx +142 -0
- package/src/input-otp.tsx +324 -0
- package/src/menu-cluster.ts +124 -0
- package/src/menu.tsx +1095 -0
- package/src/menubar.tsx +459 -0
- package/src/message-scroller.tsx +732 -0
- package/src/native.ts +26 -0
- package/src/navigation-menu.tsx +578 -0
- package/src/popover.tsx +519 -0
- package/src/popup-surface.tsx +31 -0
- package/src/popup.ts +569 -0
- package/src/position.ts +523 -0
- package/src/progress.tsx +70 -0
- package/src/radio-group.tsx +186 -0
- package/src/resizable.tsx +310 -0
- package/src/segments.ts +922 -0
- package/src/select.tsx +1501 -0
- package/src/sidebar.tsx +683 -0
- package/src/slider.tsx +424 -0
- package/src/switch.tsx +104 -0
- package/src/tabs.tsx +314 -0
- package/src/toast.tsx +923 -0
- package/src/toggle-group.tsx +249 -0
- package/src/toggle.tsx +91 -0
- package/src/toolbar.tsx +212 -0
- package/src/tooltip.tsx +359 -0
- package/src/utils.ts +204 -0
- package/src/virtual-list.tsx +205 -0
- package/src/virtual.ts +385 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import type { Children, IntrinsicElements, Stateful } from 'ajo'
|
|
2
|
+
import { statefulRootAttrs as rootAttrs } from 'ajo-cloves'
|
|
3
|
+
import type { FixedArgs, OmitArg } from './utils'
|
|
4
|
+
import { stlx } from './utils'
|
|
5
|
+
import { virtual } from './virtual'
|
|
6
|
+
|
|
7
|
+
/** Identity accepted by a VirtualList item. */
|
|
8
|
+
export type VirtualListKey = number | string
|
|
9
|
+
|
|
10
|
+
/** Alignment used when bringing an item into view. */
|
|
11
|
+
export type VirtualListScrollOptions = {
|
|
12
|
+
align?: 'center' | 'end' | 'nearest' | 'start'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Current item addressed by exactly one logical coordinate. */
|
|
16
|
+
export type VirtualListTarget<Key extends VirtualListKey = VirtualListKey> =
|
|
17
|
+
| { index: number; key?: never }
|
|
18
|
+
| { index?: never; key: Key }
|
|
19
|
+
|
|
20
|
+
/** Small imperative controller for a mounted VirtualList. */
|
|
21
|
+
export type VirtualListApi<Key extends VirtualListKey = VirtualListKey> = {
|
|
22
|
+
/** Brings one current source item into view; false means unavailable. */
|
|
23
|
+
scrollTo(
|
|
24
|
+
target: VirtualListTarget<Key>,
|
|
25
|
+
options?: VirtualListScrollOptions,
|
|
26
|
+
): boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Arguments for one vertical, data-driven virtual list. */
|
|
30
|
+
export type VirtualListArgs<
|
|
31
|
+
T = unknown,
|
|
32
|
+
Key extends VirtualListKey = VirtualListKey,
|
|
33
|
+
> = OmitArg<IntrinsicElements['ul'], 'children' | 'role'>
|
|
34
|
+
& FixedArgs<'children' | 'role'>
|
|
35
|
+
& {
|
|
36
|
+
/** Immutable ordered logical collection. */
|
|
37
|
+
items: readonly T[]
|
|
38
|
+
/** Stable unique identity across insert, delete, reorder, and refresh. */
|
|
39
|
+
getItemKey: (item: T, index: number) => Key
|
|
40
|
+
/** Positive initial border-box block size until an item is measured. */
|
|
41
|
+
estimateSize: number | ((item: T, index: number) => number)
|
|
42
|
+
/** Renders content inside the internally owned keyed list item. */
|
|
43
|
+
renderItem: (item: T, index: number) => Children
|
|
44
|
+
/** Extra items before and after the visible range. */
|
|
45
|
+
overscan?: number
|
|
46
|
+
/** Initial items emitted by SSR and the matching first client pass. */
|
|
47
|
+
prerender?: number
|
|
48
|
+
/** Receives one stable mounted-list controller. */
|
|
49
|
+
setApi?: (api: VirtualListApi<Key>) => void
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type VirtualListRootArgs<T, Key extends VirtualListKey> = Pick<
|
|
53
|
+
VirtualListArgs<T, Key>,
|
|
54
|
+
'estimateSize' | 'getItemKey' | 'items' | 'overscan' | 'prerender' | 'renderItem' | 'setApi'
|
|
55
|
+
>
|
|
56
|
+
|
|
57
|
+
const ROOT_STYLE = 'position:relative;overflow-x:hidden;overflow-y:auto;overflow-anchor:none;margin:0;padding:0;list-style:none'
|
|
58
|
+
const ITEM_STYLE = 'box-sizing:border-box;left:0;position:absolute;width:100%;top:'
|
|
59
|
+
const SIZER_KEY = '\0virtual-list-sizer'
|
|
60
|
+
const SNAPSHOT_ERROR = 'VirtualList mutated items'
|
|
61
|
+
const unavailableScroll: VirtualListApi['scrollTo'] = () => false
|
|
62
|
+
|
|
63
|
+
const vnodeKey = (key: VirtualListKey) => `\0virtual-list-item:${typeof key}:${key}`
|
|
64
|
+
|
|
65
|
+
const estimate = <T,>(value: VirtualListArgs<T>['estimateSize'], item: T, index: number) =>
|
|
66
|
+
typeof value === 'function' ? value(item, index) : value
|
|
67
|
+
|
|
68
|
+
const assertCountOption = (name: 'overscan' | 'prerender', value: number) => {
|
|
69
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
70
|
+
throw new RangeError(`VirtualList invalid ${name}`)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const VirtualListRoot: Stateful<VirtualListRootArgs<any, VirtualListKey>, 'ul'> = function* () {
|
|
75
|
+
const geometry = virtual<VirtualListKey>(this)
|
|
76
|
+
let source: readonly unknown[] | undefined
|
|
77
|
+
let sourceLength = 0
|
|
78
|
+
let keys: readonly VirtualListKey[] = []
|
|
79
|
+
let apiReceiver: VirtualListRootArgs<unknown, VirtualListKey>['setApi']
|
|
80
|
+
let estimateInput: unknown
|
|
81
|
+
let estimateItems: readonly unknown[] | undefined
|
|
82
|
+
let estimateToken: object | undefined
|
|
83
|
+
|
|
84
|
+
const snapshotKeys = (
|
|
85
|
+
items: readonly unknown[],
|
|
86
|
+
getItemKey: (item: unknown, index: number) => VirtualListKey,
|
|
87
|
+
) => {
|
|
88
|
+
if (items !== source) {
|
|
89
|
+
source = items
|
|
90
|
+
sourceLength = items.length
|
|
91
|
+
const next = items.map(getItemKey)
|
|
92
|
+
if (next.length === keys.length && next.every((key, index) => key === keys[index])) return keys
|
|
93
|
+
keys = next
|
|
94
|
+
return keys
|
|
95
|
+
}
|
|
96
|
+
if (items.length !== sourceLength) {
|
|
97
|
+
throw new TypeError(SNAPSHOT_ERROR)
|
|
98
|
+
}
|
|
99
|
+
if (items.length > 0) {
|
|
100
|
+
const last = items.length - 1
|
|
101
|
+
if (getItemKey(items[0], 0) !== keys[0] || getItemKey(items[last], last) !== keys[last]) {
|
|
102
|
+
throw new TypeError(SNAPSHOT_ERROR)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return keys
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const api: VirtualListApi<VirtualListKey> = {
|
|
109
|
+
scrollTo: (target, options) => geometry.scrollTo(
|
|
110
|
+
target,
|
|
111
|
+
options?.align === 'nearest' ? 'auto' : options?.align,
|
|
112
|
+
),
|
|
113
|
+
}
|
|
114
|
+
this.signal.addEventListener('abort', () => {
|
|
115
|
+
api.scrollTo = unavailableScroll
|
|
116
|
+
source = undefined
|
|
117
|
+
keys = []
|
|
118
|
+
apiReceiver = undefined
|
|
119
|
+
}, { once: true })
|
|
120
|
+
|
|
121
|
+
for (const {
|
|
122
|
+
estimateSize,
|
|
123
|
+
getItemKey,
|
|
124
|
+
items,
|
|
125
|
+
overscan = 4,
|
|
126
|
+
prerender = 20,
|
|
127
|
+
renderItem,
|
|
128
|
+
setApi,
|
|
129
|
+
} of this) {
|
|
130
|
+
assertCountOption('overscan', overscan)
|
|
131
|
+
assertCountOption('prerender', prerender)
|
|
132
|
+
if (estimateSize !== estimateInput || (typeof estimateSize === 'function' && items !== estimateItems)) {
|
|
133
|
+
estimateInput = estimateSize
|
|
134
|
+
estimateItems = items
|
|
135
|
+
estimateToken = {}
|
|
136
|
+
}
|
|
137
|
+
geometry.sync({
|
|
138
|
+
estimate: index => estimate(estimateSize, items[index], index),
|
|
139
|
+
estimateToken,
|
|
140
|
+
keys: snapshotKeys(items, getItemKey),
|
|
141
|
+
overscan,
|
|
142
|
+
prerender,
|
|
143
|
+
})
|
|
144
|
+
if (geometry.connected && setApi && setApi !== apiReceiver) {
|
|
145
|
+
apiReceiver = setApi
|
|
146
|
+
setApi(api)
|
|
147
|
+
}
|
|
148
|
+
yield (
|
|
149
|
+
<>
|
|
150
|
+
{geometry.rows.map(({ index, key, start }) => (
|
|
151
|
+
<li
|
|
152
|
+
aria-posinset={index + 1}
|
|
153
|
+
aria-setsize={items.length}
|
|
154
|
+
data-slot="virtual-list-item"
|
|
155
|
+
key={vnodeKey(key)}
|
|
156
|
+
ref={element => geometry.measure(key, element)}
|
|
157
|
+
style={`${ITEM_STYLE}${start}px`}
|
|
158
|
+
>
|
|
159
|
+
{renderItem(items[index], index)}
|
|
160
|
+
</li>
|
|
161
|
+
))}
|
|
162
|
+
<li
|
|
163
|
+
aria-hidden="true"
|
|
164
|
+
data-slot="virtual-list-sizer"
|
|
165
|
+
key={SIZER_KEY}
|
|
166
|
+
role="none"
|
|
167
|
+
style={`height:${geometry.total}px;pointer-events:none;visibility:hidden`}
|
|
168
|
+
/>
|
|
169
|
+
</>
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
VirtualListRoot.is = 'ul'
|
|
175
|
+
|
|
176
|
+
/** Virtualized native list with stable identity and bounded DOM work. */
|
|
177
|
+
const VirtualList = <T, Key extends VirtualListKey = VirtualListKey>({
|
|
178
|
+
estimateSize,
|
|
179
|
+
getItemKey,
|
|
180
|
+
items,
|
|
181
|
+
overscan,
|
|
182
|
+
prerender,
|
|
183
|
+
renderItem,
|
|
184
|
+
setApi,
|
|
185
|
+
style,
|
|
186
|
+
tabindex = 0,
|
|
187
|
+
...attrs
|
|
188
|
+
}: VirtualListArgs<T, Key>) => (
|
|
189
|
+
<VirtualListRoot
|
|
190
|
+
{...rootAttrs(attrs as Record<string, unknown>)}
|
|
191
|
+
estimateSize={estimateSize}
|
|
192
|
+
getItemKey={getItemKey}
|
|
193
|
+
items={items}
|
|
194
|
+
overscan={overscan}
|
|
195
|
+
prerender={prerender}
|
|
196
|
+
renderItem={renderItem}
|
|
197
|
+
setApi={setApi}
|
|
198
|
+
attr:data-slot="virtual-list"
|
|
199
|
+
attr:style={stlx(style, ROOT_STYLE)}
|
|
200
|
+
attr:tabindex={tabindex}
|
|
201
|
+
/>
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
export { VirtualList }
|
|
205
|
+
export default VirtualList
|
package/src/virtual.ts
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defaultRangeExtractor,
|
|
3
|
+
elementScroll,
|
|
4
|
+
measureElement,
|
|
5
|
+
observeElementOffset,
|
|
6
|
+
observeElementRect,
|
|
7
|
+
Virtualizer,
|
|
8
|
+
type Range,
|
|
9
|
+
type VirtualItem,
|
|
10
|
+
} from '@tanstack/virtual-core'
|
|
11
|
+
import { dom, frame, type Host } from 'ajo-cloves'
|
|
12
|
+
|
|
13
|
+
export type VirtualKey = number | string
|
|
14
|
+
|
|
15
|
+
export type VirtualInput<Key extends VirtualKey> = {
|
|
16
|
+
estimate: (index: number) => number
|
|
17
|
+
estimateToken: unknown
|
|
18
|
+
keys: readonly Key[]
|
|
19
|
+
overscan: number
|
|
20
|
+
prerender: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type VirtualRow<Key extends VirtualKey> = Readonly<{
|
|
24
|
+
index: number
|
|
25
|
+
key: Key
|
|
26
|
+
start: number
|
|
27
|
+
}>
|
|
28
|
+
|
|
29
|
+
export type VirtualTarget<Key extends VirtualKey> =
|
|
30
|
+
| { index: number; key?: never }
|
|
31
|
+
| { index?: never; key: Key }
|
|
32
|
+
|
|
33
|
+
export type VirtualView<Key extends VirtualKey> = {
|
|
34
|
+
readonly connected: boolean
|
|
35
|
+
readonly rows: readonly VirtualRow<Key>[]
|
|
36
|
+
readonly total: number
|
|
37
|
+
measure: (key: Key, node: HTMLElement | null) => void
|
|
38
|
+
scrollTo: (target: VirtualTarget<Key>, align?: 'auto' | 'center' | 'end' | 'start') => boolean
|
|
39
|
+
sync: (input: VirtualInput<Key>) => void
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type Geometry<Key extends VirtualKey> = {
|
|
43
|
+
rows: ReadonlyArray<VirtualRow<Key> & Pick<VirtualItem, 'size'>>
|
|
44
|
+
total: number
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const INDEX_ATTRIBUTE = 'data-ajo-virtual-index'
|
|
48
|
+
const TARGET_SHAPE_ERROR = 'VirtualList target needs key or index'
|
|
49
|
+
|
|
50
|
+
const sameGeometry = <Key extends VirtualKey>(left: Geometry<Key>, right: Geometry<Key>) => {
|
|
51
|
+
if (left.total !== right.total || left.rows.length !== right.rows.length) return false
|
|
52
|
+
for (let index = 0; index < left.rows.length; index++) {
|
|
53
|
+
const a = left.rows[index]!
|
|
54
|
+
const b = right.rows[index]!
|
|
55
|
+
if (a.key !== b.key || a.index !== b.index || a.start !== b.start || a.size !== b.size) return false
|
|
56
|
+
}
|
|
57
|
+
return true
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const validateKeys = <Key extends VirtualKey>(keys: readonly Key[]) => {
|
|
61
|
+
const indexes = new Map<Key, number>()
|
|
62
|
+
for (let index = 0; index < keys.length; index++) {
|
|
63
|
+
const key = keys[index]!
|
|
64
|
+
if (typeof key !== 'string' && (typeof key !== 'number' || !Number.isFinite(key))) {
|
|
65
|
+
throw new TypeError(`VirtualList invalid key at ${index}`)
|
|
66
|
+
}
|
|
67
|
+
const previous = indexes.get(key)
|
|
68
|
+
if (previous !== undefined) {
|
|
69
|
+
throw new TypeError(`VirtualList duplicate key ${JSON.stringify(key)} ${previous}/${index}`)
|
|
70
|
+
}
|
|
71
|
+
indexes.set(key, index)
|
|
72
|
+
}
|
|
73
|
+
return indexes
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const virtual = <Key extends VirtualKey>(host: Host): VirtualView<Key> => {
|
|
77
|
+
let input: VirtualInput<Key> | undefined
|
|
78
|
+
let instance: Virtualizer<HTMLElement, HTMLElement> | undefined
|
|
79
|
+
let keyIndexes = new Map<Key, number>()
|
|
80
|
+
let validatedKeys: readonly Key[] | undefined
|
|
81
|
+
let mounted = false
|
|
82
|
+
let live = false
|
|
83
|
+
let mountCleanup: (() => void) | undefined
|
|
84
|
+
let postCommitQueued = false
|
|
85
|
+
let invalidateQueued = false
|
|
86
|
+
let syncing = false
|
|
87
|
+
const positiveSizes = new Map<Key, number>()
|
|
88
|
+
let focusedKey: Key | undefined
|
|
89
|
+
let liveRangeExtractor = defaultRangeExtractor
|
|
90
|
+
const elementsByKey = new Map<Key, HTMLElement>()
|
|
91
|
+
let geometry: Geometry<Key> = { rows: [], total: 0 }
|
|
92
|
+
let rendered = geometry
|
|
93
|
+
|
|
94
|
+
const estimate = (index: number) => {
|
|
95
|
+
const size = input!.estimate(index)
|
|
96
|
+
if (!Number.isFinite(size) || size <= 0) {
|
|
97
|
+
throw new RangeError(`VirtualList invalid estimate at ${index}`)
|
|
98
|
+
}
|
|
99
|
+
return size
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let getItemKey = (index: number): Key | number => index
|
|
103
|
+
|
|
104
|
+
const collect = (): Geometry<Key> => {
|
|
105
|
+
if (!instance) return { rows: [], total: 0 }
|
|
106
|
+
return {
|
|
107
|
+
rows: instance.getVirtualItems().map(item => ({
|
|
108
|
+
index: item.index,
|
|
109
|
+
key: item.key as Key,
|
|
110
|
+
size: item.size,
|
|
111
|
+
start: item.start,
|
|
112
|
+
})),
|
|
113
|
+
total: instance.getTotalSize(),
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const throwAsync = (error: unknown) => {
|
|
118
|
+
if (!host.signal.aborted) host.throw(error)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const renderNext = () => {
|
|
122
|
+
if (host.signal.aborted) return
|
|
123
|
+
try {
|
|
124
|
+
host.next()
|
|
125
|
+
} catch (error) {
|
|
126
|
+
throwAsync(error)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const schedule = frame(renderNext)
|
|
131
|
+
const invalidate = (immediate = false) => {
|
|
132
|
+
if (!immediate) return schedule()
|
|
133
|
+
schedule.cancel()
|
|
134
|
+
if (invalidateQueued || host.signal.aborted) return
|
|
135
|
+
invalidateQueued = true
|
|
136
|
+
queueMicrotask(() => {
|
|
137
|
+
invalidateQueued = false
|
|
138
|
+
renderNext()
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const onChange = () => {
|
|
143
|
+
if (syncing || host.signal.aborted) return
|
|
144
|
+
try {
|
|
145
|
+
const next = collect()
|
|
146
|
+
geometry = next
|
|
147
|
+
if (sameGeometry(next, rendered)) {
|
|
148
|
+
schedule.cancel()
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
invalidate()
|
|
152
|
+
} catch (error) {
|
|
153
|
+
throwAsync(error)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const ssrRangeExtractor = (_range: Range) => {
|
|
158
|
+
const count = Math.min(input!.prerender, input!.keys.length)
|
|
159
|
+
return Array.from({ length: count }, (_, index) => index)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const refreshLiveRangeExtractor = () => {
|
|
163
|
+
const focusedIndex = focusedKey === undefined ? undefined : keyIndexes.get(focusedKey)
|
|
164
|
+
liveRangeExtractor = (range: Range) => {
|
|
165
|
+
const indexes = defaultRangeExtractor(range)
|
|
166
|
+
if (focusedIndex === undefined || indexes.includes(focusedIndex)) return indexes
|
|
167
|
+
indexes.push(focusedIndex)
|
|
168
|
+
indexes.sort((left, right) => left - right)
|
|
169
|
+
return indexes
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const focusedRowKey = () => {
|
|
174
|
+
if (!dom(host)) return undefined
|
|
175
|
+
let node = host.ownerDocument.activeElement as HTMLElement | null
|
|
176
|
+
while (node && node !== host) {
|
|
177
|
+
const index = node.getAttribute(INDEX_ATTRIBUTE)
|
|
178
|
+
if (index !== null) return input?.keys[Number(index)]
|
|
179
|
+
node = node.parentElement
|
|
180
|
+
}
|
|
181
|
+
return undefined
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const updateFocus = () => {
|
|
185
|
+
const next = focusedRowKey()
|
|
186
|
+
if (next === focusedKey) return
|
|
187
|
+
focusedKey = next
|
|
188
|
+
refreshLiveRangeExtractor()
|
|
189
|
+
if (!live || !instance || host.signal.aborted) return
|
|
190
|
+
try {
|
|
191
|
+
syncing = true
|
|
192
|
+
applyOptions()
|
|
193
|
+
geometry = collect()
|
|
194
|
+
} catch (error) {
|
|
195
|
+
throwAsync(error)
|
|
196
|
+
} finally {
|
|
197
|
+
syncing = false
|
|
198
|
+
}
|
|
199
|
+
if (!sameGeometry(geometry, rendered)) invalidate()
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const measure = (
|
|
203
|
+
node: HTMLElement,
|
|
204
|
+
entry: ResizeObserverEntry | undefined,
|
|
205
|
+
current: Virtualizer<HTMLElement, HTMLElement>,
|
|
206
|
+
) => {
|
|
207
|
+
const index = current.indexFromElement(node)
|
|
208
|
+
const key = input?.keys[index]
|
|
209
|
+
const cached = key === undefined ? undefined : positiveSizes.get(key)
|
|
210
|
+
if (!entry && cached !== undefined) return cached
|
|
211
|
+
const size = measureElement(node, entry, current)
|
|
212
|
+
if (Number.isFinite(size) && size > 0) {
|
|
213
|
+
if (key !== undefined) positiveSizes.set(key, size)
|
|
214
|
+
return size
|
|
215
|
+
}
|
|
216
|
+
return key === undefined ? estimate(index) : positiveSizes.get(key) ?? estimate(index)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const coreOptions = () => {
|
|
220
|
+
const prerender = Math.min(input!.prerender, input!.keys.length)
|
|
221
|
+
let initialHeight = 0
|
|
222
|
+
for (let index = 0; index < prerender; index++) initialHeight += estimate(index)
|
|
223
|
+
return {
|
|
224
|
+
count: input!.keys.length,
|
|
225
|
+
estimateSize: estimate,
|
|
226
|
+
getItemKey,
|
|
227
|
+
getScrollElement: () => dom(host) ? host as HTMLElement : null,
|
|
228
|
+
indexAttribute: INDEX_ATTRIBUTE,
|
|
229
|
+
initialRect: { height: initialHeight, width: 0 },
|
|
230
|
+
measureElement: measure,
|
|
231
|
+
observeElementOffset,
|
|
232
|
+
observeElementRect,
|
|
233
|
+
onChange,
|
|
234
|
+
overscan: live ? input!.overscan : 0,
|
|
235
|
+
rangeExtractor: live ? liveRangeExtractor : ssrRangeExtractor,
|
|
236
|
+
scrollToFn: elementScroll,
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const applyOptions = () => {
|
|
241
|
+
if (!instance) instance = new Virtualizer<HTMLElement, HTMLElement>(coreOptions())
|
|
242
|
+
else instance.setOptions(coreOptions())
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const postCommit = () => {
|
|
246
|
+
if (postCommitQueued || host.signal.aborted) return
|
|
247
|
+
postCommitQueued = true
|
|
248
|
+
queueMicrotask(() => {
|
|
249
|
+
postCommitQueued = false
|
|
250
|
+
if (host.signal.aborted || !dom(host) || !host.isConnected || !instance) return
|
|
251
|
+
try {
|
|
252
|
+
syncing = true
|
|
253
|
+
const firstMount = !mounted
|
|
254
|
+
if (firstMount) {
|
|
255
|
+
mountCleanup = instance._didMount()
|
|
256
|
+
mounted = true
|
|
257
|
+
focusedKey = focusedRowKey()
|
|
258
|
+
live = true
|
|
259
|
+
refreshLiveRangeExtractor()
|
|
260
|
+
host.addEventListener('focusin', updateFocus, { signal: host.signal })
|
|
261
|
+
host.addEventListener('focusout', () => queueMicrotask(updateFocus), { signal: host.signal })
|
|
262
|
+
}
|
|
263
|
+
applyOptions()
|
|
264
|
+
instance._willUpdate()
|
|
265
|
+
const next = collect()
|
|
266
|
+
const changed = !sameGeometry(next, rendered)
|
|
267
|
+
if (changed) geometry = next
|
|
268
|
+
if (firstMount || changed) invalidate(firstMount)
|
|
269
|
+
} catch (error) {
|
|
270
|
+
throwAsync(error)
|
|
271
|
+
} finally {
|
|
272
|
+
syncing = false
|
|
273
|
+
}
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
host.signal.addEventListener('abort', () => {
|
|
278
|
+
schedule.cancel()
|
|
279
|
+
mountCleanup?.()
|
|
280
|
+
mountCleanup = undefined
|
|
281
|
+
mounted = false
|
|
282
|
+
live = false
|
|
283
|
+
focusedKey = undefined
|
|
284
|
+
positiveSizes.clear()
|
|
285
|
+
elementsByKey.clear()
|
|
286
|
+
keyIndexes.clear()
|
|
287
|
+
validatedKeys = undefined
|
|
288
|
+
input = undefined
|
|
289
|
+
instance = undefined
|
|
290
|
+
geometry = rendered = { rows: [], total: 0 }
|
|
291
|
+
}, { once: true })
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
get connected() {
|
|
295
|
+
return mounted && dom(host) && host.isConnected
|
|
296
|
+
},
|
|
297
|
+
get rows() {
|
|
298
|
+
return geometry.rows
|
|
299
|
+
},
|
|
300
|
+
get total() {
|
|
301
|
+
return geometry.total
|
|
302
|
+
},
|
|
303
|
+
measure(key, node) {
|
|
304
|
+
if (!instance) return
|
|
305
|
+
if (!node) {
|
|
306
|
+
instance.measureElement(null)
|
|
307
|
+
const previous = elementsByKey.get(key)
|
|
308
|
+
if (previous) queueMicrotask(() => {
|
|
309
|
+
if (host.signal.aborted || previous.isConnected || elementsByKey.get(key) !== previous) return
|
|
310
|
+
elementsByKey.delete(key)
|
|
311
|
+
})
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
const index = keyIndexes.get(key)
|
|
315
|
+
if (index === undefined || host.signal.aborted) return
|
|
316
|
+
elementsByKey.set(key, node)
|
|
317
|
+
node.setAttribute(INDEX_ATTRIBUTE, String(index))
|
|
318
|
+
instance.measureElement(node)
|
|
319
|
+
},
|
|
320
|
+
scrollTo(target, align = 'auto') {
|
|
321
|
+
if (!target || typeof target !== 'object') {
|
|
322
|
+
throw new TypeError(TARGET_SHAPE_ERROR)
|
|
323
|
+
}
|
|
324
|
+
const record = target as { index?: unknown; key?: unknown }
|
|
325
|
+
const hasIndex = Object.hasOwn(record, 'index')
|
|
326
|
+
const hasKey = Object.hasOwn(record, 'key')
|
|
327
|
+
if (hasIndex === hasKey) {
|
|
328
|
+
throw new TypeError(TARGET_SHAPE_ERROR)
|
|
329
|
+
}
|
|
330
|
+
let index: number | undefined
|
|
331
|
+
if (hasKey) {
|
|
332
|
+
if (typeof record.key !== 'string' && (typeof record.key !== 'number' || !Number.isFinite(record.key))) {
|
|
333
|
+
throw new TypeError('VirtualList invalid scroll key')
|
|
334
|
+
}
|
|
335
|
+
index = keyIndexes.get(record.key as Key)
|
|
336
|
+
} else {
|
|
337
|
+
if (typeof record.index !== 'number' || !Number.isInteger(record.index)) {
|
|
338
|
+
throw new TypeError('VirtualList invalid scroll index')
|
|
339
|
+
}
|
|
340
|
+
index = record.index
|
|
341
|
+
}
|
|
342
|
+
if (index === undefined || index < 0 || index >= (input?.keys.length ?? 0)) return false
|
|
343
|
+
if (!instance || !mounted || host.signal.aborted) return false
|
|
344
|
+
instance.scrollToIndex(index, { align, behavior: 'auto' })
|
|
345
|
+
return true
|
|
346
|
+
},
|
|
347
|
+
sync(next) {
|
|
348
|
+
const keysChanged = validatedKeys !== next.keys
|
|
349
|
+
if (keysChanged) {
|
|
350
|
+
const nextIndexes = validateKeys(next.keys)
|
|
351
|
+
if (focusedKey !== undefined && !nextIndexes.has(focusedKey)) {
|
|
352
|
+
const row = elementsByKey.get(focusedKey)
|
|
353
|
+
const active = dom(host) ? host.ownerDocument.activeElement : null
|
|
354
|
+
if (row && active && row.contains(active)) {
|
|
355
|
+
if (dom(host)) host.focus({ preventScroll: true })
|
|
356
|
+
}
|
|
357
|
+
focusedKey = undefined
|
|
358
|
+
}
|
|
359
|
+
for (const key of positiveSizes.keys()) {
|
|
360
|
+
if (!nextIndexes.has(key)) positiveSizes.delete(key)
|
|
361
|
+
}
|
|
362
|
+
if (instance) for (const key of instance.itemSizeCache.keys()) {
|
|
363
|
+
if (!nextIndexes.has(key as Key)) instance.itemSizeCache.delete(key)
|
|
364
|
+
}
|
|
365
|
+
keyIndexes = nextIndexes
|
|
366
|
+
validatedKeys = next.keys
|
|
367
|
+
refreshLiveRangeExtractor()
|
|
368
|
+
}
|
|
369
|
+
if (keysChanged || input?.estimateToken !== next.estimateToken) {
|
|
370
|
+
const currentKeys = next.keys
|
|
371
|
+
getItemKey = index => currentKeys[index] ?? index
|
|
372
|
+
}
|
|
373
|
+
input = next
|
|
374
|
+
syncing = true
|
|
375
|
+
try {
|
|
376
|
+
applyOptions()
|
|
377
|
+
geometry = collect()
|
|
378
|
+
rendered = geometry
|
|
379
|
+
} finally {
|
|
380
|
+
syncing = false
|
|
381
|
+
}
|
|
382
|
+
postCommit()
|
|
383
|
+
},
|
|
384
|
+
}
|
|
385
|
+
}
|