@purestack/ts-components 1.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 +21 -0
- package/README.md +10 -0
- package/dist/ts-components.d.mts +1662 -0
- package/dist/ts-components.d.mts.map +1 -0
- package/dist/ts-components.mjs +9200 -0
- package/dist/ts-components.mjs.map +1 -0
- package/package.json +130 -0
- package/src/defineComponents.ts +87 -0
- package/src/index.ts +270 -0
- package/src/registerStyles.ts +63 -0
- package/src/standard/alertBox/alertBox.ts +64 -0
- package/src/standard/autoId.ts +25 -0
- package/src/standard/badge/badge.ts +41 -0
- package/src/standard/badge/badgeStyle.ts +21 -0
- package/src/standard/barChart/barChart.ts +338 -0
- package/src/standard/barChart/barChartStyle.ts +81 -0
- package/src/standard/btn/btn.ts +191 -0
- package/src/standard/btn/btnStyle.ts +59 -0
- package/src/standard/btnGroup/btnGroup.ts +208 -0
- package/src/standard/btnGroup/btnGroupStyle.ts +85 -0
- package/src/standard/chart/cartesianChart.ts +99 -0
- package/src/standard/chart/chartDefaults.ts +25 -0
- package/src/standard/chart/chartUtils.ts +84 -0
- package/src/standard/classicLogo/classicLogo.ts +429 -0
- package/src/standard/classicLogo/classicLogoStyle.ts +292 -0
- package/src/standard/componentVariant.ts +153 -0
- package/src/standard/composer/composer.ts +1035 -0
- package/src/standard/composer/composerCanvasInitialStyle.ts +225 -0
- package/src/standard/composer/composerCanvasRevertStyle.ts +14 -0
- package/src/standard/composer/composerCanvasSharedStyle.ts +60 -0
- package/src/standard/composer/composerCanvasStyleTypes.ts +8 -0
- package/src/standard/composer/composerCanvasThemeStyle.ts +53 -0
- package/src/standard/composer/composerStyle.ts +156 -0
- package/src/standard/composer/emailHtmlPolicy.ts +474 -0
- package/src/standard/consent/consent.ts +173 -0
- package/src/standard/consent/consentStyle.ts +44 -0
- package/src/standard/contactForm/contactForm.ts +135 -0
- package/src/standard/contactForm/contactFormStyle.ts +116 -0
- package/src/standard/doughnutChart/doughnutChart.ts +527 -0
- package/src/standard/doughnutChart/doughnutChartStyle.ts +64 -0
- package/src/standard/dropFiles/dropFiles.ts +480 -0
- package/src/standard/dropFiles/dropFilesStyle.ts +171 -0
- package/src/standard/expandablePanel/expandablePanel.ts +106 -0
- package/src/standard/expandablePanel/expandablePanelStyle.ts +56 -0
- package/src/standard/flex/flex.ts +269 -0
- package/src/standard/flex/flexStyle.ts +556 -0
- package/src/standard/footer/footer.ts +93 -0
- package/src/standard/footer/footerStyle.ts +39 -0
- package/src/standard/form/autoCompleteInput.ts +567 -0
- package/src/standard/form/form.ts +205 -0
- package/src/standard/form/formCheck.ts +96 -0
- package/src/standard/form/formInputField.ts +184 -0
- package/src/standard/form/formSelectField.ts +152 -0
- package/src/standard/form/formStyle.ts +585 -0
- package/src/standard/form/multiAutoCompleteInput.ts +880 -0
- package/src/standard/grid/grid.ts +177 -0
- package/src/standard/grid/gridStyle.ts +56 -0
- package/src/standard/icon/icon.ts +96 -0
- package/src/standard/icon/iconStyle.ts +50 -0
- package/src/standard/landing/codeShowcase.ts +54 -0
- package/src/standard/landing/comparisonColumn.ts +30 -0
- package/src/standard/landing/comparisonFeature.ts +22 -0
- package/src/standard/landing/comparisonTable.ts +38 -0
- package/src/standard/landing/ctaSection.ts +59 -0
- package/src/standard/landing/featureCard.ts +41 -0
- package/src/standard/landing/landing.ts +52 -0
- package/src/standard/landing/landingBand.ts +217 -0
- package/src/standard/landing/landingSection.ts +43 -0
- package/src/standard/landing/landingTypes.ts +141 -0
- package/src/standard/landing/metricItem.ts +25 -0
- package/src/standard/landing/metricStrip.ts +20 -0
- package/src/standard/lineChart/lineChart.ts +462 -0
- package/src/standard/lineChart/lineChartStyle.ts +91 -0
- package/src/standard/logo/logo.ts +192 -0
- package/src/standard/logo/logoStyle.ts +164 -0
- package/src/standard/modal/modal.ts +171 -0
- package/src/standard/modal/modalStore.ts +154 -0
- package/src/standard/modal/modalStyle.ts +190 -0
- package/src/standard/navMenu/navMenu.ts +278 -0
- package/src/standard/navMenu/navMenuStyle.ts +323 -0
- package/src/standard/pageLinks/pageLinks.ts +90 -0
- package/src/standard/pageScript/pageScript.ts +155 -0
- package/src/standard/pageToc/pageToc.ts +154 -0
- package/src/standard/pageToc/pageTocStyle.ts +372 -0
- package/src/standard/panel/panel.ts +55 -0
- package/src/standard/panel/panelStyle.ts +15 -0
- package/src/standard/pricing/pricing.ts +146 -0
- package/src/standard/searchBox/searchBox.ts +54 -0
- package/src/standard/searchBox/searchBoxStyle.ts +180 -0
- package/src/standard/sectionHeader/sectionHeader.ts +99 -0
- package/src/standard/signIn/signIn.ts +167 -0
- package/src/standard/signIn/signInStyle.ts +70 -0
- package/src/standard/tabs/tabs.ts +193 -0
- package/src/standard/tabs/tabsStyle.ts +231 -0
- package/src/standard/themeToggle/themeToggle.ts +33 -0
- package/src/standard/themeToggle/themeToggleStyle.ts +88 -0
- package/src/standard/toast/toastHost.ts +42 -0
- package/src/standard/toast/toastStore.ts +50 -0
- package/src/standard/topBar/topBar.ts +101 -0
- package/src/standard/topBar/topBarStyle.ts +169 -0
- package/src/standard/virtualList/variableVirtualTable.ts +371 -0
- package/src/standard/virtualList/virtualList.ts +423 -0
- package/src/standard/virtualList/virtualListStyle.ts +34 -0
- package/src/standard/virtualList/virtualTable.ts +179 -0
- package/src/standard/virtualList/virtualTableStyle.ts +24 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ComponentHead,
|
|
3
|
+
type ComputedRef,
|
|
4
|
+
computed,
|
|
5
|
+
defineComponent,
|
|
6
|
+
html,
|
|
7
|
+
observe,
|
|
8
|
+
type RefOrValue,
|
|
9
|
+
ref,
|
|
10
|
+
sref,
|
|
11
|
+
unref,
|
|
12
|
+
} from 'regor'
|
|
13
|
+
|
|
14
|
+
export interface VirtualListRow {
|
|
15
|
+
index: number
|
|
16
|
+
item: unknown
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface VirtualList {
|
|
20
|
+
items?: RefOrValue<unknown[]>
|
|
21
|
+
height?: RefOrValue<number | string>
|
|
22
|
+
itemHeight?: RefOrValue<number | string>
|
|
23
|
+
overscan?: RefOrValue<number | string>
|
|
24
|
+
rowComponent?: RefOrValue<string>
|
|
25
|
+
scrollTop?: ReturnType<typeof ref<number>>
|
|
26
|
+
visibleRows?: ComputedRef<VirtualListRow[]>
|
|
27
|
+
viewportStyle?: ComputedRef<Record<string, string>>
|
|
28
|
+
spacerStyle?: ComputedRef<Record<string, string>>
|
|
29
|
+
windowStyle?: ComputedRef<Record<string, string>>
|
|
30
|
+
itemStyle?: ComputedRef<Record<string, string>>
|
|
31
|
+
handleScroll?: (event: Event) => void
|
|
32
|
+
resolvedRowComponent?: ComputedRef<string>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface VariableVirtualList extends VirtualList {
|
|
36
|
+
estimateHeight?: RefOrValue<number | string>
|
|
37
|
+
rowElementRefs?: Array<ReturnType<typeof sref<HTMLElement | null>>>
|
|
38
|
+
viewportElement?: ReturnType<typeof sref<HTMLElement | null>>
|
|
39
|
+
unmounted?: () => void
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const virtualListTemplate = html`<div class="virtual-list" :style="viewportStyle" @scroll="handleScroll">
|
|
43
|
+
<div class="virtual-list__spacer" :style="spacerStyle">
|
|
44
|
+
<div class="virtual-list__window" :style="windowStyle">
|
|
45
|
+
<div
|
|
46
|
+
class="virtual-list__item"
|
|
47
|
+
:style="itemStyle"
|
|
48
|
+
r-for="row in visibleRows"
|
|
49
|
+
>
|
|
50
|
+
<div
|
|
51
|
+
:is="resolvedRowComponent"
|
|
52
|
+
:item="row.item"
|
|
53
|
+
:index="row.index"
|
|
54
|
+
></div>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>`
|
|
59
|
+
|
|
60
|
+
function defineVirtualListComponent() {
|
|
61
|
+
return defineComponent<VirtualList>(virtualListTemplate, {
|
|
62
|
+
props: ['items', 'height', 'itemHeight', 'overscan', 'rowComponent'],
|
|
63
|
+
context: (head) => resolveVirtualList(head),
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const variableVirtualListTemplate = html`<div
|
|
68
|
+
class="virtual-list variable-virtual-list"
|
|
69
|
+
:style="viewportStyle"
|
|
70
|
+
:ref="viewportElement"
|
|
71
|
+
@scroll="handleScroll"
|
|
72
|
+
>
|
|
73
|
+
<div class="virtual-list__spacer" :style="spacerStyle">
|
|
74
|
+
<div class="virtual-list__window" :style="windowStyle">
|
|
75
|
+
<div
|
|
76
|
+
class="virtual-list__item"
|
|
77
|
+
:ref="rowElementRefs[row.index]"
|
|
78
|
+
r-for="row in visibleRows"
|
|
79
|
+
>
|
|
80
|
+
<div
|
|
81
|
+
:is="resolvedRowComponent"
|
|
82
|
+
:item="row.item"
|
|
83
|
+
:index="row.index"
|
|
84
|
+
></div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>`
|
|
89
|
+
|
|
90
|
+
function defineVariableVirtualListComponent() {
|
|
91
|
+
return defineComponent<VariableVirtualList>(variableVirtualListTemplate, {
|
|
92
|
+
props: ['items', 'height', 'estimateHeight', 'overscan', 'rowComponent'],
|
|
93
|
+
context: (head) => resolveVariableVirtualList(head),
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function defineVirtualListComponents() {
|
|
98
|
+
return {
|
|
99
|
+
virtualList: defineVirtualListComponent(),
|
|
100
|
+
variableVirtualList: defineVariableVirtualListComponent(),
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function resolveVirtualList(head: ComponentHead<VirtualList>) {
|
|
105
|
+
const props = head.props
|
|
106
|
+
const scrollTop = ref(0)
|
|
107
|
+
const itemHeight = computed(() => resolvePositiveNumber(props.itemHeight, 44))
|
|
108
|
+
const height = computed(() => resolvePositiveNumber(props.height, 560))
|
|
109
|
+
const overscan = computed(() =>
|
|
110
|
+
Math.trunc(resolvePositiveNumber(props.overscan, 6)),
|
|
111
|
+
)
|
|
112
|
+
const items = computed(() => {
|
|
113
|
+
const value = unref(props.items)
|
|
114
|
+
return Array.isArray(value) ? value : []
|
|
115
|
+
})
|
|
116
|
+
const startIndex = computed(() =>
|
|
117
|
+
Math.max(0, Math.floor(scrollTop() / itemHeight()) - overscan()),
|
|
118
|
+
)
|
|
119
|
+
const visibleCount = computed(
|
|
120
|
+
() => Math.ceil(height() / itemHeight()) + overscan() * 2,
|
|
121
|
+
)
|
|
122
|
+
const visibleRows = computed(() => {
|
|
123
|
+
const source = items()
|
|
124
|
+
const start = startIndex()
|
|
125
|
+
const end = Math.min(source.length, start + visibleCount())
|
|
126
|
+
const rows: VirtualListRow[] = []
|
|
127
|
+
for (let index = start; index < end; index += 1)
|
|
128
|
+
rows.push({ index, item: source[index] })
|
|
129
|
+
|
|
130
|
+
return rows
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
...props,
|
|
135
|
+
scrollTop,
|
|
136
|
+
visibleRows,
|
|
137
|
+
viewportStyle: computed<Record<string, string>>(() => ({
|
|
138
|
+
height: `${height()}px`,
|
|
139
|
+
})),
|
|
140
|
+
spacerStyle: computed<Record<string, string>>(() => ({
|
|
141
|
+
height: `${items().length * itemHeight()}px`,
|
|
142
|
+
})),
|
|
143
|
+
windowStyle: computed<Record<string, string>>(() => ({
|
|
144
|
+
transform: `translateY(${startIndex() * itemHeight()}px)`,
|
|
145
|
+
})),
|
|
146
|
+
itemStyle: computed<Record<string, string>>(() => ({
|
|
147
|
+
height: `${itemHeight()}px`,
|
|
148
|
+
})),
|
|
149
|
+
resolvedRowComponent: computed(() => {
|
|
150
|
+
const component = unref(props.rowComponent)
|
|
151
|
+
return typeof component === 'string' && component.trim()
|
|
152
|
+
? component
|
|
153
|
+
: 'div'
|
|
154
|
+
}),
|
|
155
|
+
handleScroll: (event: Event) => {
|
|
156
|
+
const target = event.currentTarget
|
|
157
|
+
if (target instanceof HTMLElement) scrollTop(target.scrollTop)
|
|
158
|
+
},
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function resolvePositiveNumber(
|
|
163
|
+
value: RefOrValue<number | string> | undefined,
|
|
164
|
+
fallback: number,
|
|
165
|
+
) {
|
|
166
|
+
const raw = unref(value)
|
|
167
|
+
const parsed = typeof raw === 'number' ? raw : Number.parseFloat(String(raw))
|
|
168
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
class VariableVirtualListContext {
|
|
172
|
+
readonly scrollTop = ref(0)
|
|
173
|
+
readonly viewportElement = sref<HTMLElement | null>(null)
|
|
174
|
+
readonly rowElementRefs: Array<ReturnType<typeof sref<HTMLElement | null>>> =
|
|
175
|
+
[]
|
|
176
|
+
readonly visibleRows: ComputedRef<VirtualListRow[]>
|
|
177
|
+
readonly viewportStyle: ComputedRef<Record<string, string>>
|
|
178
|
+
readonly spacerStyle: ComputedRef<Record<string, string>>
|
|
179
|
+
readonly windowStyle: ComputedRef<Record<string, string>>
|
|
180
|
+
readonly resolvedRowComponent: ComputedRef<string>
|
|
181
|
+
private readonly rowHeights: number[] = []
|
|
182
|
+
private rowWindow = new Map<number, VirtualListRow>()
|
|
183
|
+
private readonly rowObservers = new Map<number, ResizeObserver>()
|
|
184
|
+
private readonly measurementVersion = ref(0)
|
|
185
|
+
private measuredHeightTotal = 0
|
|
186
|
+
private measuredRowCount = 0
|
|
187
|
+
private readonly resolvedItems: ComputedRef<unknown[]>
|
|
188
|
+
private readonly resolvedHeight: ComputedRef<number>
|
|
189
|
+
private readonly resolvedEstimateHeight: ComputedRef<number>
|
|
190
|
+
private readonly resolvedOverscan: ComputedRef<number>
|
|
191
|
+
private readonly offsets: ComputedRef<number[]>
|
|
192
|
+
private readonly startIndex: ComputedRef<number>
|
|
193
|
+
private readonly firstVisibleIndex: ComputedRef<number>
|
|
194
|
+
private readonly normalizedScrollTop: ComputedRef<number>
|
|
195
|
+
private pendingIndexes = new Set<number>()
|
|
196
|
+
private pendingFrame = 0
|
|
197
|
+
private scrollEndTimer: ReturnType<typeof setTimeout> | undefined
|
|
198
|
+
private isScrolling = false
|
|
199
|
+
private hasDeferredMeasurements = false
|
|
200
|
+
|
|
201
|
+
constructor(props: VariableVirtualList) {
|
|
202
|
+
this.resolvedItems = computed(() => {
|
|
203
|
+
const value = unref(props.items)
|
|
204
|
+
return Array.isArray(value) ? value : []
|
|
205
|
+
})
|
|
206
|
+
this.resolvedHeight = computed(() =>
|
|
207
|
+
resolvePositiveNumber(props.height, 560),
|
|
208
|
+
)
|
|
209
|
+
this.resolvedEstimateHeight = computed(() =>
|
|
210
|
+
resolvePositiveNumber(props.estimateHeight ?? props.itemHeight, 56),
|
|
211
|
+
)
|
|
212
|
+
this.resolvedOverscan = computed(() =>
|
|
213
|
+
Math.trunc(resolvePositiveNumber(props.overscan, 6)),
|
|
214
|
+
)
|
|
215
|
+
this.offsets = computed(() => {
|
|
216
|
+
this.measurementVersion()
|
|
217
|
+
const estimate = this.currentEstimateHeight()
|
|
218
|
+
const source = this.resolvedItems()
|
|
219
|
+
const offsets = new Array<number>(source.length + 1)
|
|
220
|
+
offsets[0] = 0
|
|
221
|
+
for (let index = 0; index < source.length; index += 1)
|
|
222
|
+
offsets[index + 1] =
|
|
223
|
+
(offsets[index] ?? 0) + (this.rowHeights[index] ?? estimate)
|
|
224
|
+
|
|
225
|
+
return offsets
|
|
226
|
+
})
|
|
227
|
+
this.normalizedScrollTop = computed(() => {
|
|
228
|
+
const value = this.scrollTop()
|
|
229
|
+
return value <= 0.5 ? 0 : value
|
|
230
|
+
})
|
|
231
|
+
this.firstVisibleIndex = computed(() =>
|
|
232
|
+
findOffsetIndex(this.offsets(), this.normalizedScrollTop()),
|
|
233
|
+
)
|
|
234
|
+
this.startIndex = computed(() =>
|
|
235
|
+
Math.max(0, this.firstVisibleIndex() - this.resolvedOverscan()),
|
|
236
|
+
)
|
|
237
|
+
this.visibleRows = computed(() => this.resolveVisibleRows())
|
|
238
|
+
this.viewportStyle = computed<Record<string, string>>(() => ({
|
|
239
|
+
height: `${this.resolvedHeight()}px`,
|
|
240
|
+
}))
|
|
241
|
+
this.spacerStyle = computed<Record<string, string>>(() => {
|
|
242
|
+
const offsets = this.offsets()
|
|
243
|
+
return {
|
|
244
|
+
height: `${offsets[offsets.length - 1] ?? 0}px`,
|
|
245
|
+
}
|
|
246
|
+
})
|
|
247
|
+
this.windowStyle = computed<Record<string, string>>(() => ({
|
|
248
|
+
transform: `translateY(${this.offsets()[this.startIndex()] ?? 0}px)`,
|
|
249
|
+
}))
|
|
250
|
+
this.resolvedRowComponent = computed(() => {
|
|
251
|
+
const component = unref(props.rowComponent)
|
|
252
|
+
return typeof component === 'string' && component.trim()
|
|
253
|
+
? component
|
|
254
|
+
: 'div'
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
handleScroll = () => {
|
|
259
|
+
this.syncScrollTopFromViewport()
|
|
260
|
+
|
|
261
|
+
this.isScrolling = true
|
|
262
|
+
if (this.scrollEndTimer !== undefined) clearTimeout(this.scrollEndTimer)
|
|
263
|
+
this.scrollEndTimer = setTimeout(() => {
|
|
264
|
+
this.scrollEndTimer = undefined
|
|
265
|
+
this.isScrolling = false
|
|
266
|
+
this.syncScrollTopFromViewport()
|
|
267
|
+
if (!this.hasDeferredMeasurements) return
|
|
268
|
+
|
|
269
|
+
this.hasDeferredMeasurements = false
|
|
270
|
+
this.measurementVersion(this.measurementVersion() + 1)
|
|
271
|
+
}, 120)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
unmounted = () => {
|
|
275
|
+
if (this.pendingFrame) cancelAnimationFrame(this.pendingFrame)
|
|
276
|
+
if (this.scrollEndTimer !== undefined) clearTimeout(this.scrollEndTimer)
|
|
277
|
+
for (const observer of this.rowObservers.values()) observer.disconnect()
|
|
278
|
+
this.rowObservers.clear()
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private resolveVisibleRows() {
|
|
282
|
+
const source = this.resolvedItems()
|
|
283
|
+
const offsets = this.offsets()
|
|
284
|
+
const scrollTop = this.normalizedScrollTop()
|
|
285
|
+
const overscan = this.resolvedOverscan()
|
|
286
|
+
// Resolve both boundaries from the same scroll position. A separate
|
|
287
|
+
// computed start can still hold its previous value during notification.
|
|
288
|
+
const start = Math.max(0, findOffsetIndex(offsets, scrollTop) - overscan)
|
|
289
|
+
const viewportBottom = scrollTop + this.resolvedHeight()
|
|
290
|
+
let end = findOffsetIndex(offsets, viewportBottom) + overscan + 1
|
|
291
|
+
end = Math.min(source.length, Math.max(start, end))
|
|
292
|
+
|
|
293
|
+
const rows: VirtualListRow[] = []
|
|
294
|
+
const nextWindow = new Map<number, VirtualListRow>()
|
|
295
|
+
for (let index = start; index < end; index += 1) {
|
|
296
|
+
this.ensureRowRef(index)
|
|
297
|
+
const previous = this.rowWindow.get(index)
|
|
298
|
+
const row =
|
|
299
|
+
previous && previous.item === source[index]
|
|
300
|
+
? previous
|
|
301
|
+
: { index, item: source[index] }
|
|
302
|
+
rows.push(row)
|
|
303
|
+
nextWindow.set(index, row)
|
|
304
|
+
}
|
|
305
|
+
this.rowWindow = nextWindow
|
|
306
|
+
|
|
307
|
+
return rows
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private ensureRowRef(index: number) {
|
|
311
|
+
if (this.rowElementRefs[index]) return
|
|
312
|
+
|
|
313
|
+
const elementRef = sref<HTMLElement | null>(null)
|
|
314
|
+
this.rowElementRefs[index] = elementRef
|
|
315
|
+
observe(elementRef, (element) => {
|
|
316
|
+
this.observeRowElement(index, element)
|
|
317
|
+
})
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private observeRowElement(index: number, element: HTMLElement | null) {
|
|
321
|
+
const existing = this.rowObservers.get(index)
|
|
322
|
+
if (existing) {
|
|
323
|
+
existing.disconnect()
|
|
324
|
+
this.rowObservers.delete(index)
|
|
325
|
+
}
|
|
326
|
+
if (!element) return
|
|
327
|
+
|
|
328
|
+
const observer = new ResizeObserver(() => {
|
|
329
|
+
this.queueMeasurement(index)
|
|
330
|
+
})
|
|
331
|
+
observer.observe(element)
|
|
332
|
+
this.rowObservers.set(index, observer)
|
|
333
|
+
this.queueMeasurement(index)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private queueMeasurement(index: number) {
|
|
337
|
+
this.pendingIndexes.add(index)
|
|
338
|
+
if (this.pendingFrame) return
|
|
339
|
+
|
|
340
|
+
this.pendingFrame = requestAnimationFrame(() => {
|
|
341
|
+
this.pendingFrame = 0
|
|
342
|
+
this.flushMeasurements()
|
|
343
|
+
})
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private flushMeasurements() {
|
|
347
|
+
this.syncScrollTopFromViewport()
|
|
348
|
+
let changed = false
|
|
349
|
+
|
|
350
|
+
for (const index of this.pendingIndexes) {
|
|
351
|
+
const element = this.rowElementRefs[index]?.()
|
|
352
|
+
if (!element) continue
|
|
353
|
+
|
|
354
|
+
const previous = this.rowHeights[index] ?? this.currentEstimateHeight()
|
|
355
|
+
const next = element.getBoundingClientRect().height
|
|
356
|
+
if (!Number.isFinite(next) || next <= 0) continue
|
|
357
|
+
if (Math.abs(previous - next) < 0.5) continue
|
|
358
|
+
|
|
359
|
+
this.updateMeasuredAverage(index, next)
|
|
360
|
+
this.rowHeights[index] = next
|
|
361
|
+
changed = true
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
this.pendingIndexes.clear()
|
|
365
|
+
if (!changed) return
|
|
366
|
+
|
|
367
|
+
if (this.isScrolling) {
|
|
368
|
+
this.hasDeferredMeasurements = true
|
|
369
|
+
return
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
this.measurementVersion(this.measurementVersion() + 1)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
private currentEstimateHeight() {
|
|
376
|
+
if (this.measuredRowCount === 0) return this.resolvedEstimateHeight()
|
|
377
|
+
return this.measuredHeightTotal / this.measuredRowCount
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private updateMeasuredAverage(index: number, next: number) {
|
|
381
|
+
const previous = this.rowHeights[index]
|
|
382
|
+
if (previous === undefined) {
|
|
383
|
+
this.measuredRowCount += 1
|
|
384
|
+
this.measuredHeightTotal += next
|
|
385
|
+
return
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
this.measuredHeightTotal += next - previous
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
private syncScrollTopFromViewport() {
|
|
392
|
+
const viewport = this.viewportElement()
|
|
393
|
+
if (!viewport) return
|
|
394
|
+
|
|
395
|
+
this.setScrollTop(viewport.scrollTop)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private setScrollTop(value: number) {
|
|
399
|
+
const next = Number.isFinite(value) && value > 0 ? value : 0
|
|
400
|
+
if (this.scrollTop() === next) return
|
|
401
|
+
|
|
402
|
+
this.scrollTop(next)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function resolveVariableVirtualList(head: ComponentHead<VariableVirtualList>) {
|
|
407
|
+
head.autoProps = false
|
|
408
|
+
return new VariableVirtualListContext(head.props) as VariableVirtualList
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function findOffsetIndex(offsets: number[], scrollTop: number) {
|
|
412
|
+
if (offsets.length <= 1) return 0
|
|
413
|
+
|
|
414
|
+
let low = 0
|
|
415
|
+
let high = offsets.length - 1
|
|
416
|
+
while (low < high) {
|
|
417
|
+
const middle = Math.floor((low + high) / 2)
|
|
418
|
+
if ((offsets[middle] ?? 0) <= scrollTop) low = middle + 1
|
|
419
|
+
else high = middle
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return Math.max(0, low - 1)
|
|
423
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { styleBuilder, themes } from '@purestack/ts-style'
|
|
2
|
+
|
|
3
|
+
export function registerVirtualListStyles() {
|
|
4
|
+
themes.forEach((theme) => {
|
|
5
|
+
styleBuilder
|
|
6
|
+
.select('.virtual-list', theme)
|
|
7
|
+
.overflowY('auto')
|
|
8
|
+
.overflowX('auto')
|
|
9
|
+
.minWidth('0')
|
|
10
|
+
|
|
11
|
+
styleBuilder
|
|
12
|
+
.select('.virtual-list__spacer', theme)
|
|
13
|
+
.position('relative')
|
|
14
|
+
.minWidth('0')
|
|
15
|
+
|
|
16
|
+
styleBuilder
|
|
17
|
+
.select('.virtual-list__window', theme)
|
|
18
|
+
.position('absolute')
|
|
19
|
+
.top('0')
|
|
20
|
+
.right('0')
|
|
21
|
+
.left('0')
|
|
22
|
+
.willChange('transform')
|
|
23
|
+
|
|
24
|
+
styleBuilder
|
|
25
|
+
.select('.virtual-list__item', theme)
|
|
26
|
+
.overflow('visible')
|
|
27
|
+
.minWidth('0')
|
|
28
|
+
|
|
29
|
+
styleBuilder
|
|
30
|
+
.select('.variable-virtual-list .virtual-list__item', theme)
|
|
31
|
+
.overflow('visible')
|
|
32
|
+
.minWidth('0')
|
|
33
|
+
})
|
|
34
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ComponentHead,
|
|
3
|
+
type ComputedRef,
|
|
4
|
+
computed,
|
|
5
|
+
defineComponent,
|
|
6
|
+
html,
|
|
7
|
+
type RefOrValue,
|
|
8
|
+
ref,
|
|
9
|
+
unref,
|
|
10
|
+
} from 'regor'
|
|
11
|
+
import type { VirtualListRow } from './virtualList'
|
|
12
|
+
|
|
13
|
+
export interface VirtualTable {
|
|
14
|
+
items?: RefOrValue<unknown[]>
|
|
15
|
+
height?: RefOrValue<number | string>
|
|
16
|
+
itemHeight?: RefOrValue<number | string>
|
|
17
|
+
overscan?: RefOrValue<number | string>
|
|
18
|
+
rowComponent?: RefOrValue<string>
|
|
19
|
+
colGroupComponent?: RefOrValue<string>
|
|
20
|
+
headerComponent?: RefOrValue<string>
|
|
21
|
+
footerComponent?: RefOrValue<string>
|
|
22
|
+
tableLayout?: RefOrValue<'auto' | 'fixed'>
|
|
23
|
+
scrollTop?: ReturnType<typeof ref<number>>
|
|
24
|
+
visibleRows?: ComputedRef<VirtualListRow[]>
|
|
25
|
+
viewportStyle?: ComputedRef<Record<string, string>>
|
|
26
|
+
tableStyle?: ComputedRef<Record<string, string>>
|
|
27
|
+
topSpacerStyle?: ComputedRef<Record<string, string>>
|
|
28
|
+
bottomSpacerStyle?: ComputedRef<Record<string, string>>
|
|
29
|
+
handleScroll?: (event: Event) => void
|
|
30
|
+
hasColGroupComponent?: ComputedRef<boolean>
|
|
31
|
+
hasHeaderComponent?: ComputedRef<boolean>
|
|
32
|
+
hasFooterComponent?: ComputedRef<boolean>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const virtualTableTemplate = html`<div class="virtual-table" :style="viewportStyle" @scroll="handleScroll">
|
|
36
|
+
<table class="virtual-table__table" :style="tableStyle">
|
|
37
|
+
<colgroup :is="colGroupComponent" r-if="hasColGroupComponent"></colgroup>
|
|
38
|
+
<thead :is="headerComponent" r-if="hasHeaderComponent"></thead>
|
|
39
|
+
<tbody>
|
|
40
|
+
<tr class="virtual-table__spacer-row">
|
|
41
|
+
<td :style="topSpacerStyle"></td>
|
|
42
|
+
</tr>
|
|
43
|
+
<tr
|
|
44
|
+
:is="rowComponent"
|
|
45
|
+
:item="row.item"
|
|
46
|
+
:index="row.index"
|
|
47
|
+
style="height: var(--virtual-table-item-height) !important;"
|
|
48
|
+
r-for="row in visibleRows"
|
|
49
|
+
></tr>
|
|
50
|
+
<tr class="virtual-table__spacer-row">
|
|
51
|
+
<td :style="bottomSpacerStyle"></td>
|
|
52
|
+
</tr>
|
|
53
|
+
</tbody>
|
|
54
|
+
<tfoot :is="footerComponent" r-if="hasFooterComponent"></tfoot>
|
|
55
|
+
</table>
|
|
56
|
+
</div>`
|
|
57
|
+
|
|
58
|
+
function defineVirtualTableComponent() {
|
|
59
|
+
return defineComponent<VirtualTable>(virtualTableTemplate, {
|
|
60
|
+
props: [
|
|
61
|
+
'items',
|
|
62
|
+
'height',
|
|
63
|
+
'itemHeight',
|
|
64
|
+
'overscan',
|
|
65
|
+
'rowComponent',
|
|
66
|
+
'colGroupComponent',
|
|
67
|
+
'headerComponent',
|
|
68
|
+
'footerComponent',
|
|
69
|
+
'tableLayout',
|
|
70
|
+
],
|
|
71
|
+
context: (head) => resolveVirtualTable(head),
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function defineVirtualTableComponents() {
|
|
76
|
+
return {
|
|
77
|
+
virtualTable: defineVirtualTableComponent(),
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function resolveVirtualTable(head: ComponentHead<VirtualTable>) {
|
|
82
|
+
const props = head.props
|
|
83
|
+
const window = resolveFixedVirtualWindow(props)
|
|
84
|
+
const endIndex = computed(() => {
|
|
85
|
+
const rows = window.visibleRows()
|
|
86
|
+
const last = rows[rows.length - 1]
|
|
87
|
+
return last ? last.index + 1 : window.startIndex()
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
...props,
|
|
92
|
+
scrollTop: window.scrollTop,
|
|
93
|
+
visibleRows: window.visibleRows,
|
|
94
|
+
viewportStyle: computed<Record<string, string>>(() => ({
|
|
95
|
+
height: `${window.height()}px`,
|
|
96
|
+
overflow: 'auto',
|
|
97
|
+
width: '100%',
|
|
98
|
+
maxWidth: '100%',
|
|
99
|
+
boxSizing: 'border-box',
|
|
100
|
+
minWidth: '0',
|
|
101
|
+
})),
|
|
102
|
+
tableStyle: computed<Record<string, string>>(() => {
|
|
103
|
+
const fixed = unref(props.tableLayout) === 'fixed'
|
|
104
|
+
return {
|
|
105
|
+
'--virtual-table-item-height': `${window.itemHeight()}px`,
|
|
106
|
+
width: fixed ? '100%' : 'max-content',
|
|
107
|
+
minWidth: '100%',
|
|
108
|
+
borderCollapse: 'separate',
|
|
109
|
+
borderSpacing: '0',
|
|
110
|
+
tableLayout: fixed ? 'fixed' : 'auto',
|
|
111
|
+
}
|
|
112
|
+
}),
|
|
113
|
+
topSpacerStyle: computed<Record<string, string>>(() => ({
|
|
114
|
+
height: `${window.startIndex() * window.itemHeight()}px`,
|
|
115
|
+
padding: '0',
|
|
116
|
+
border: '0',
|
|
117
|
+
})),
|
|
118
|
+
bottomSpacerStyle: computed<Record<string, string>>(() => ({
|
|
119
|
+
height: `${Math.max(0, window.items().length - endIndex()) * window.itemHeight()}px`,
|
|
120
|
+
padding: '0',
|
|
121
|
+
border: '0',
|
|
122
|
+
})),
|
|
123
|
+
hasColGroupComponent: computed(() => !!unref(props.colGroupComponent)),
|
|
124
|
+
hasHeaderComponent: computed(() => !!unref(props.headerComponent)),
|
|
125
|
+
hasFooterComponent: computed(() => !!unref(props.footerComponent)),
|
|
126
|
+
handleScroll: window.handleScroll,
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function resolveFixedVirtualWindow(props: VirtualTable) {
|
|
131
|
+
const scrollTop = ref(0)
|
|
132
|
+
const itemHeight = computed(() => resolvePositiveNumber(props.itemHeight, 44))
|
|
133
|
+
const height = computed(() => resolvePositiveNumber(props.height, 560))
|
|
134
|
+
const overscan = computed(() =>
|
|
135
|
+
Math.trunc(resolvePositiveNumber(props.overscan, 6)),
|
|
136
|
+
)
|
|
137
|
+
const items = computed(() => {
|
|
138
|
+
const value = unref(props.items)
|
|
139
|
+
return Array.isArray(value) ? value : []
|
|
140
|
+
})
|
|
141
|
+
const startIndex = computed(() =>
|
|
142
|
+
Math.max(0, Math.floor(scrollTop() / itemHeight()) - overscan()),
|
|
143
|
+
)
|
|
144
|
+
const visibleCount = computed(
|
|
145
|
+
() => Math.ceil(height() / itemHeight()) + overscan() * 2,
|
|
146
|
+
)
|
|
147
|
+
const visibleRows = computed(() => {
|
|
148
|
+
const source = items()
|
|
149
|
+
const start = startIndex()
|
|
150
|
+
const end = Math.min(source.length, start + visibleCount())
|
|
151
|
+
const rows: VirtualListRow[] = []
|
|
152
|
+
for (let index = start; index < end; index += 1)
|
|
153
|
+
rows.push({ index, item: source[index] })
|
|
154
|
+
|
|
155
|
+
return rows
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
scrollTop,
|
|
160
|
+
itemHeight,
|
|
161
|
+
height,
|
|
162
|
+
items,
|
|
163
|
+
startIndex,
|
|
164
|
+
visibleRows,
|
|
165
|
+
handleScroll: (event: Event) => {
|
|
166
|
+
const target = event.currentTarget
|
|
167
|
+
if (target instanceof HTMLElement) scrollTop(target.scrollTop)
|
|
168
|
+
},
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function resolvePositiveNumber(
|
|
173
|
+
value: RefOrValue<number | string> | undefined,
|
|
174
|
+
fallback: number,
|
|
175
|
+
) {
|
|
176
|
+
const raw = unref(value)
|
|
177
|
+
const parsed = typeof raw === 'number' ? raw : Number.parseFloat(String(raw))
|
|
178
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback
|
|
179
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { styleBuilder, themes } from '@purestack/ts-style'
|
|
2
|
+
|
|
3
|
+
export function registerVirtualTableStyles() {
|
|
4
|
+
themes.forEach((theme) => {
|
|
5
|
+
styleBuilder
|
|
6
|
+
.select('.virtual-table__table th, .virtual-table__table td', theme)
|
|
7
|
+
.verticalAlign('middle')
|
|
8
|
+
|
|
9
|
+
styleBuilder
|
|
10
|
+
.select('.virtual-table__table thead', theme)
|
|
11
|
+
.position('sticky')
|
|
12
|
+
.top('0')
|
|
13
|
+
.zIndex(2)
|
|
14
|
+
|
|
15
|
+
styleBuilder
|
|
16
|
+
.select(
|
|
17
|
+
'.virtual-table__table tfoot th, .virtual-table__table tfoot td',
|
|
18
|
+
theme,
|
|
19
|
+
)
|
|
20
|
+
.position('sticky')
|
|
21
|
+
.bottom('0')
|
|
22
|
+
.zIndex(1)
|
|
23
|
+
})
|
|
24
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "esnext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"isolatedModules": true,
|
|
9
|
+
"allowSyntheticDefaultImports": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"noImplicitOverride": true,
|
|
12
|
+
"noImplicitAny": true,
|
|
13
|
+
"forceConsistentCasingInFileNames": true,
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
"types": [],
|
|
16
|
+
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
|
17
|
+
"paths": {
|
|
18
|
+
"@purestack/*": ["../../packages/*/src"]
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"include": ["src/**/*.ts"],
|
|
22
|
+
"exclude": ["src/test/**", "src/**/*.test.ts", "src/**/*.spec.ts"]
|
|
23
|
+
}
|