@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,101 @@
|
|
|
1
|
+
import type { SiteLogoConfig, TsSsgContext } from '@purestack/ts-common'
|
|
2
|
+
|
|
3
|
+
import { resolveTsSsgContext } from '@purestack/ts-common'
|
|
4
|
+
import type { SemanticTone } from '@purestack/ts-style'
|
|
5
|
+
import {
|
|
6
|
+
type ComputedRef,
|
|
7
|
+
computed,
|
|
8
|
+
defineComponent,
|
|
9
|
+
html,
|
|
10
|
+
type RefOrValue,
|
|
11
|
+
unref,
|
|
12
|
+
} from 'regor'
|
|
13
|
+
import {
|
|
14
|
+
type ComponentVariant,
|
|
15
|
+
type ComponentVariantMode,
|
|
16
|
+
resolveComponentClasses,
|
|
17
|
+
} from '../componentVariant'
|
|
18
|
+
|
|
19
|
+
export interface TopBar {
|
|
20
|
+
siteLogo: SiteLogoConfig
|
|
21
|
+
logoComponent?: RefOrValue<string>
|
|
22
|
+
resolvedLogoComponent?: ComputedRef<string>
|
|
23
|
+
tone?: RefOrValue<SemanticTone>
|
|
24
|
+
variant?: RefOrValue<ComponentVariant>
|
|
25
|
+
signInAvatarSrc?: RefOrValue<string>
|
|
26
|
+
signInAvatarAlt?: RefOrValue<string>
|
|
27
|
+
classes?: ComputedRef<string>
|
|
28
|
+
searchEnabled?: boolean
|
|
29
|
+
signInEnabled?: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const DEFAULT_TOP_BAR_VARIANT: ComponentVariant = 'surfaceAlt'
|
|
33
|
+
const DEFAULT_TOP_BAR_VARIANT_MODE: ComponentVariantMode = 'stateless'
|
|
34
|
+
|
|
35
|
+
function resolveTopBar(context: TsSsgContext, props: TopBar): TopBar {
|
|
36
|
+
return {
|
|
37
|
+
...props,
|
|
38
|
+
siteLogo: context.site.logo,
|
|
39
|
+
resolvedLogoComponent: computed(
|
|
40
|
+
() =>
|
|
41
|
+
unref(props.logoComponent) ?? context.site.logo.component ?? 'SiteLogo',
|
|
42
|
+
),
|
|
43
|
+
searchEnabled: context.site.pagefind?.enabled === true,
|
|
44
|
+
classes: computed(() =>
|
|
45
|
+
resolveComponentClasses(props, {
|
|
46
|
+
defaultVariant: DEFAULT_TOP_BAR_VARIANT,
|
|
47
|
+
defaultVariantMode: DEFAULT_TOP_BAR_VARIANT_MODE,
|
|
48
|
+
}),
|
|
49
|
+
),
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const topBarTemplate = html`<input
|
|
54
|
+
class="doc-nav-toggle"
|
|
55
|
+
id="doc-nav-toggle"
|
|
56
|
+
type="checkbox"
|
|
57
|
+
autocomplete="off"
|
|
58
|
+
aria-hidden="true"/>
|
|
59
|
+
<header class="topbar" :class="classes" r-inherit>
|
|
60
|
+
<Flex align="center">
|
|
61
|
+
<div :is="resolvedLogoComponent" class="flex-none" :config="siteLogo"></div>
|
|
62
|
+
<SearchBox
|
|
63
|
+
r-if="searchEnabled"
|
|
64
|
+
class="topbar__search flex-auto rounded-md tone-text-surface"
|
|
65
|
+
variant="none"/>
|
|
66
|
+
<Flex class="topbar__controls flex-none" align="center" justify="end">
|
|
67
|
+
<ThemeToggle/>
|
|
68
|
+
<SignIn
|
|
69
|
+
r-if="signInEnabled"
|
|
70
|
+
class="topbar__account"
|
|
71
|
+
:avatarSrc="signInAvatarSrc"
|
|
72
|
+
:avatarAlt="signInAvatarAlt"/>
|
|
73
|
+
<label
|
|
74
|
+
class="topbar__icon topbar__toggle"
|
|
75
|
+
for="doc-nav-toggle"
|
|
76
|
+
role="button"
|
|
77
|
+
aria-label="Toggle navigation"
|
|
78
|
+
></label>
|
|
79
|
+
</Flex>
|
|
80
|
+
</Flex>
|
|
81
|
+
</header>`
|
|
82
|
+
|
|
83
|
+
function defineTopBarComponent() {
|
|
84
|
+
return defineComponent<TopBar>(topBarTemplate, {
|
|
85
|
+
props: [
|
|
86
|
+
'class',
|
|
87
|
+
'logoComponent',
|
|
88
|
+
'tone',
|
|
89
|
+
'variant',
|
|
90
|
+
'signInAvatarSrc',
|
|
91
|
+
'signInAvatarAlt',
|
|
92
|
+
'signInEnabled',
|
|
93
|
+
'searchEnabled',
|
|
94
|
+
],
|
|
95
|
+
context: (head) => resolveTopBar(resolveTsSsgContext(head), head.props),
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function defineTopBarComponents() {
|
|
100
|
+
return { topBar: defineTopBarComponent() }
|
|
101
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { ThemePalette } from '@purestack/ts-style'
|
|
2
|
+
import {
|
|
3
|
+
BREAKPOINTS,
|
|
4
|
+
mediaBelow,
|
|
5
|
+
mediaMax,
|
|
6
|
+
mediaMin,
|
|
7
|
+
styleBuilder,
|
|
8
|
+
type ThemeMode,
|
|
9
|
+
themes,
|
|
10
|
+
} from '@purestack/ts-style'
|
|
11
|
+
|
|
12
|
+
export function registerTopBarStyles() {
|
|
13
|
+
themes.forEach((theme, palette) => {
|
|
14
|
+
registerTopBarShellStyles(theme, palette)
|
|
15
|
+
registerTopBarToggleStyles(theme, palette)
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getTopBarToggleVisibleSelectors(targetSelector: string) {
|
|
20
|
+
return [
|
|
21
|
+
{ selector: `.template-doc--nav-drawer ${targetSelector}` },
|
|
22
|
+
{
|
|
23
|
+
selector: `.template-doc--has-nav ${targetSelector}`,
|
|
24
|
+
media: mediaBelow(BREAKPOINTS.lg),
|
|
25
|
+
},
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getTopBarToggleHiddenSelectors(targetSelector: string) {
|
|
30
|
+
return [
|
|
31
|
+
{
|
|
32
|
+
selector: `body:not(.template-doc--nav-drawer):not(.template-doc--has-nav) ${targetSelector}`,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
selector: `.template-doc--has-nav:not(.template-doc--nav-drawer) ${targetSelector}`,
|
|
36
|
+
media: mediaMin(BREAKPOINTS.lg),
|
|
37
|
+
},
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function registerTopBarShellStyles(
|
|
42
|
+
theme: ThemeMode,
|
|
43
|
+
palette: ThemePalette,
|
|
44
|
+
) {
|
|
45
|
+
styleBuilder
|
|
46
|
+
.select('.topbar', theme)
|
|
47
|
+
.padding('1rem')
|
|
48
|
+
.position('sticky')
|
|
49
|
+
.top('0')
|
|
50
|
+
.overflowX('clip')
|
|
51
|
+
.zIndex(40)
|
|
52
|
+
.backdropFilter('blur(0.625rem)')
|
|
53
|
+
.borderTopWidth('0 !important')
|
|
54
|
+
.borderRightWidth('0 !important')
|
|
55
|
+
.borderBottomWidth('1px !important')
|
|
56
|
+
.borderBottomStyle('solid !important')
|
|
57
|
+
.borderLeftWidth('0 !important')
|
|
58
|
+
.borderRadius('0 !important')
|
|
59
|
+
|
|
60
|
+
styleBuilder.select('.topbar__controls', theme).marginLeft('auto')
|
|
61
|
+
styleBuilder
|
|
62
|
+
.select('.topbar', theme)
|
|
63
|
+
.media(mediaBelow(BREAKPOINTS.sm))
|
|
64
|
+
.padding('0.75rem')
|
|
65
|
+
styleBuilder
|
|
66
|
+
.select('.topbar > .flex, .topbar__controls', theme)
|
|
67
|
+
.media(mediaBelow(BREAKPOINTS.sm))
|
|
68
|
+
.gap('0.5rem')
|
|
69
|
+
registerTopBarAccountVisibilityStyles(theme)
|
|
70
|
+
|
|
71
|
+
styleBuilder
|
|
72
|
+
.select('.topbar__icon', theme)
|
|
73
|
+
.width('2.625rem')
|
|
74
|
+
.height('2.625rem')
|
|
75
|
+
.borderRadius(palette.radii.pill)
|
|
76
|
+
.display('grid')
|
|
77
|
+
.placeItems('center')
|
|
78
|
+
.border('1px solid transparent')
|
|
79
|
+
.background('transparent')
|
|
80
|
+
.cursor('pointer')
|
|
81
|
+
.position('relative')
|
|
82
|
+
.padding('0')
|
|
83
|
+
.color(palette.current.text.subtle)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function registerTopBarAccountVisibilityStyles(theme: ThemeMode) {
|
|
87
|
+
styleBuilder.select('.topbar__account', theme).display('none')
|
|
88
|
+
for (const rule of getTopBarToggleHiddenSelectors('.topbar__account')) {
|
|
89
|
+
let selector = styleBuilder.select(rule.selector, theme)
|
|
90
|
+
if (rule.media) selector = selector.media(rule.media)
|
|
91
|
+
selector.display('inline-block')
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function registerTopBarToggleStyles(
|
|
96
|
+
theme: ThemeMode,
|
|
97
|
+
palette: ThemePalette,
|
|
98
|
+
) {
|
|
99
|
+
registerTopBarToggleVisibilityStyles(theme, palette)
|
|
100
|
+
registerTopBarToggleGlyphStyles(theme)
|
|
101
|
+
registerTopBarToggleCheckedStateStyles(theme)
|
|
102
|
+
registerTopBarResponsiveSearchStyles(theme)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function registerTopBarToggleVisibilityStyles(
|
|
106
|
+
theme: ThemeMode,
|
|
107
|
+
palette: ThemePalette,
|
|
108
|
+
) {
|
|
109
|
+
styleBuilder
|
|
110
|
+
.select('.topbar__toggle', theme)
|
|
111
|
+
.background(palette.current.surface.rest.background)
|
|
112
|
+
.borderColor(palette.current.border.default)
|
|
113
|
+
.color(palette.current.text.subtle)
|
|
114
|
+
.display('none')
|
|
115
|
+
styleBuilder
|
|
116
|
+
.select('.template-doc--nav-drawer .topbar__toggle', theme)
|
|
117
|
+
.display('grid')
|
|
118
|
+
styleBuilder
|
|
119
|
+
.select('.template-doc--has-nav .topbar__toggle', theme)
|
|
120
|
+
.media(mediaBelow(BREAKPOINTS.lg))
|
|
121
|
+
.display('grid')
|
|
122
|
+
styleBuilder
|
|
123
|
+
.select('.topbar__toggle:focus-visible', theme)
|
|
124
|
+
.outline(`2px solid ${palette.current.border.focus}`)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function registerTopBarToggleGlyphStyles(theme: ThemeMode) {
|
|
128
|
+
styleBuilder
|
|
129
|
+
.select('.topbar__toggle::before', theme)
|
|
130
|
+
.content('""')
|
|
131
|
+
.width('1.125rem')
|
|
132
|
+
.height('0.125rem')
|
|
133
|
+
.background('currentColor')
|
|
134
|
+
.position('absolute')
|
|
135
|
+
.top('0.875rem')
|
|
136
|
+
.left('0.75rem')
|
|
137
|
+
.transition('transform 200ms ease, top 200ms ease')
|
|
138
|
+
.boxShadow('0 0.375rem 0 0 currentColor')
|
|
139
|
+
styleBuilder
|
|
140
|
+
.select('.topbar__toggle::after', theme)
|
|
141
|
+
.content('""')
|
|
142
|
+
.width('1.125rem')
|
|
143
|
+
.height('0.125rem')
|
|
144
|
+
.background('currentColor')
|
|
145
|
+
.position('absolute')
|
|
146
|
+
.top('1.625rem')
|
|
147
|
+
.left('0.75rem')
|
|
148
|
+
.transition('transform 200ms ease, top 200ms ease')
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function registerTopBarToggleCheckedStateStyles(theme: ThemeMode) {
|
|
152
|
+
styleBuilder.select('.doc-nav-toggle', theme).display('none')
|
|
153
|
+
styleBuilder
|
|
154
|
+
.select('.doc-nav-toggle:checked ~ .topbar .topbar__toggle::before', theme)
|
|
155
|
+
.top('1.25rem')
|
|
156
|
+
.transform('rotate(45deg)')
|
|
157
|
+
.boxShadow('none')
|
|
158
|
+
styleBuilder
|
|
159
|
+
.select('.doc-nav-toggle:checked ~ .topbar .topbar__toggle::after', theme)
|
|
160
|
+
.top('1.25rem')
|
|
161
|
+
.transform('rotate(-45deg)')
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function registerTopBarResponsiveSearchStyles(theme: ThemeMode) {
|
|
165
|
+
styleBuilder
|
|
166
|
+
.select('.topbar__search', theme)
|
|
167
|
+
.media(mediaMax(BREAKPOINTS.sm))
|
|
168
|
+
.display('none')
|
|
169
|
+
}
|
|
@@ -0,0 +1,371 @@
|
|
|
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
|
+
import type { VirtualListRow } from './virtualList'
|
|
14
|
+
import type { VirtualTable } from './virtualTable'
|
|
15
|
+
// known limitation: vertical scroll bar is not synced perfectly to the mouse pointer during scroll. eiter fix it or delete this component!
|
|
16
|
+
export interface VariableVirtualTable extends VirtualTable {
|
|
17
|
+
estimateHeight?: RefOrValue<number | string>
|
|
18
|
+
viewportElement?: ReturnType<typeof sref<HTMLElement | null>>
|
|
19
|
+
unmounted?: () => void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const variableVirtualTableTemplate = html`<div
|
|
23
|
+
class="virtual-table variable-virtual-table"
|
|
24
|
+
:style="viewportStyle"
|
|
25
|
+
:ref="viewportElement"
|
|
26
|
+
@scroll="handleScroll"
|
|
27
|
+
>
|
|
28
|
+
<table class="virtual-table__table" :style="tableStyle">
|
|
29
|
+
<colgroup :is="colGroupComponent" r-if="hasColGroupComponent"></colgroup>
|
|
30
|
+
<thead :is="headerComponent" r-if="hasHeaderComponent"></thead>
|
|
31
|
+
<tbody>
|
|
32
|
+
<tr class="virtual-table__spacer-row">
|
|
33
|
+
<td :style="topSpacerStyle"></td>
|
|
34
|
+
</tr>
|
|
35
|
+
<tr
|
|
36
|
+
:is="rowComponent"
|
|
37
|
+
:data-virtual-row-index="row.index"
|
|
38
|
+
:item="row.item"
|
|
39
|
+
:index="row.index"
|
|
40
|
+
r-for="row in visibleRows"
|
|
41
|
+
></tr>
|
|
42
|
+
<tr class="virtual-table__spacer-row">
|
|
43
|
+
<td :style="bottomSpacerStyle"></td>
|
|
44
|
+
</tr>
|
|
45
|
+
</tbody>
|
|
46
|
+
<tfoot :is="footerComponent" r-if="hasFooterComponent"></tfoot>
|
|
47
|
+
</table>
|
|
48
|
+
</div>`
|
|
49
|
+
|
|
50
|
+
function defineVariableVirtualTableComponent() {
|
|
51
|
+
return defineComponent<VariableVirtualTable>(variableVirtualTableTemplate, {
|
|
52
|
+
props: [
|
|
53
|
+
'items',
|
|
54
|
+
'height',
|
|
55
|
+
'estimateHeight',
|
|
56
|
+
'overscan',
|
|
57
|
+
'rowComponent',
|
|
58
|
+
'colGroupComponent',
|
|
59
|
+
'headerComponent',
|
|
60
|
+
'footerComponent',
|
|
61
|
+
],
|
|
62
|
+
context: (head) => resolveVariableVirtualTable(head),
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function defineVariableVirtualTableComponents() {
|
|
67
|
+
return {
|
|
68
|
+
variableVirtualTable: defineVariableVirtualTableComponent(),
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
class VariableVirtualTableContext implements VariableVirtualTable {
|
|
73
|
+
readonly items?: RefOrValue<unknown[]>
|
|
74
|
+
readonly height?: RefOrValue<number | string>
|
|
75
|
+
readonly estimateHeight?: RefOrValue<number | string>
|
|
76
|
+
readonly overscan?: RefOrValue<number | string>
|
|
77
|
+
readonly rowComponent?: RefOrValue<string>
|
|
78
|
+
readonly colGroupComponent?: RefOrValue<string>
|
|
79
|
+
readonly headerComponent?: RefOrValue<string>
|
|
80
|
+
readonly footerComponent?: RefOrValue<string>
|
|
81
|
+
readonly scrollTop = ref(0)
|
|
82
|
+
readonly viewportElement = sref<HTMLElement | null>(null)
|
|
83
|
+
readonly visibleRows: ComputedRef<VirtualListRow[]>
|
|
84
|
+
readonly viewportStyle: ComputedRef<Record<string, string>>
|
|
85
|
+
readonly tableStyle: ComputedRef<Record<string, string>>
|
|
86
|
+
readonly topSpacerStyle: ComputedRef<Record<string, string>>
|
|
87
|
+
readonly bottomSpacerStyle: ComputedRef<Record<string, string>>
|
|
88
|
+
readonly hasColGroupComponent: ComputedRef<boolean>
|
|
89
|
+
readonly hasHeaderComponent: ComputedRef<boolean>
|
|
90
|
+
readonly hasFooterComponent: ComputedRef<boolean>
|
|
91
|
+
private readonly rowHeights: number[] = []
|
|
92
|
+
private rowWindow = new Map<number, VirtualListRow>()
|
|
93
|
+
private readonly rowElements = new Map<number, HTMLElement>()
|
|
94
|
+
private rowObserver?: ResizeObserver
|
|
95
|
+
private bodyObserver?: MutationObserver
|
|
96
|
+
private readonly measurementVersion = ref(0)
|
|
97
|
+
private measuredHeightTotal = 0
|
|
98
|
+
private measuredRowCount = 0
|
|
99
|
+
private pendingFrame = 0
|
|
100
|
+
private scrollEndTimer: ReturnType<typeof setTimeout> | undefined
|
|
101
|
+
private isScrolling = false
|
|
102
|
+
private hasDeferredMeasurements = false
|
|
103
|
+
private readonly resolvedItems: ComputedRef<unknown[]>
|
|
104
|
+
private readonly resolvedHeight: ComputedRef<number>
|
|
105
|
+
private readonly resolvedEstimateHeight: ComputedRef<number>
|
|
106
|
+
private readonly resolvedOverscan: ComputedRef<number>
|
|
107
|
+
private readonly offsets: ComputedRef<number[]>
|
|
108
|
+
private readonly startIndex: ComputedRef<number>
|
|
109
|
+
private readonly firstVisibleIndex: ComputedRef<number>
|
|
110
|
+
private readonly endIndex: ComputedRef<number>
|
|
111
|
+
private readonly pendingIndexes = new Set<number>()
|
|
112
|
+
|
|
113
|
+
constructor(props: VariableVirtualTable) {
|
|
114
|
+
this.items = props.items
|
|
115
|
+
this.height = props.height
|
|
116
|
+
this.estimateHeight = props.estimateHeight
|
|
117
|
+
this.overscan = props.overscan
|
|
118
|
+
this.rowComponent = props.rowComponent
|
|
119
|
+
this.colGroupComponent = props.colGroupComponent
|
|
120
|
+
this.headerComponent = props.headerComponent
|
|
121
|
+
this.footerComponent = props.footerComponent
|
|
122
|
+
this.resolvedItems = computed(() => {
|
|
123
|
+
const value = unref(props.items)
|
|
124
|
+
return Array.isArray(value) ? value : []
|
|
125
|
+
})
|
|
126
|
+
this.resolvedHeight = computed(() =>
|
|
127
|
+
resolvePositiveNumber(props.height, 560),
|
|
128
|
+
)
|
|
129
|
+
this.resolvedEstimateHeight = computed(() =>
|
|
130
|
+
resolvePositiveNumber(props.estimateHeight ?? props.itemHeight, 56),
|
|
131
|
+
)
|
|
132
|
+
this.resolvedOverscan = computed(() =>
|
|
133
|
+
Math.trunc(resolvePositiveNumber(props.overscan, 6)),
|
|
134
|
+
)
|
|
135
|
+
this.offsets = computed(() => {
|
|
136
|
+
this.measurementVersion()
|
|
137
|
+
const source = this.resolvedItems()
|
|
138
|
+
const estimate = this.currentEstimateHeight()
|
|
139
|
+
const offsets = new Array<number>(source.length + 1)
|
|
140
|
+
offsets[0] = 0
|
|
141
|
+
for (let index = 0; index < source.length; index += 1)
|
|
142
|
+
offsets[index + 1] =
|
|
143
|
+
(offsets[index] ?? 0) + (this.rowHeights[index] ?? estimate)
|
|
144
|
+
|
|
145
|
+
return offsets
|
|
146
|
+
})
|
|
147
|
+
this.firstVisibleIndex = computed(() =>
|
|
148
|
+
findOffsetIndex(this.offsets(), this.scrollTop()),
|
|
149
|
+
)
|
|
150
|
+
this.startIndex = computed(() =>
|
|
151
|
+
Math.max(0, this.firstVisibleIndex() - this.resolvedOverscan()),
|
|
152
|
+
)
|
|
153
|
+
this.visibleRows = computed(() => this.resolveVisibleRows())
|
|
154
|
+
this.endIndex = computed(() => {
|
|
155
|
+
const rows = this.visibleRows()
|
|
156
|
+
const last = rows[rows.length - 1]
|
|
157
|
+
return last ? last.index + 1 : this.startIndex()
|
|
158
|
+
})
|
|
159
|
+
this.viewportStyle = computed<Record<string, string>>(() => ({
|
|
160
|
+
height: `${this.resolvedHeight()}px`,
|
|
161
|
+
overflow: 'auto',
|
|
162
|
+
width: '100%',
|
|
163
|
+
maxWidth: '100%',
|
|
164
|
+
boxSizing: 'border-box',
|
|
165
|
+
minWidth: '0',
|
|
166
|
+
}))
|
|
167
|
+
this.tableStyle = computed<Record<string, string>>(() => ({
|
|
168
|
+
width: 'max-content',
|
|
169
|
+
minWidth: '100%',
|
|
170
|
+
borderCollapse: 'separate',
|
|
171
|
+
borderSpacing: '0',
|
|
172
|
+
}))
|
|
173
|
+
this.topSpacerStyle = computed<Record<string, string>>(() => ({
|
|
174
|
+
height: `${this.offsets()[this.startIndex()] ?? 0}px`,
|
|
175
|
+
padding: '0',
|
|
176
|
+
border: '0',
|
|
177
|
+
}))
|
|
178
|
+
this.bottomSpacerStyle = computed<Record<string, string>>(() => {
|
|
179
|
+
const offsets = this.offsets()
|
|
180
|
+
const totalHeight = offsets[offsets.length - 1] ?? 0
|
|
181
|
+
const visibleBottom = offsets[this.endIndex()] ?? totalHeight
|
|
182
|
+
return {
|
|
183
|
+
height: `${Math.max(0, totalHeight - visibleBottom)}px`,
|
|
184
|
+
padding: '0',
|
|
185
|
+
border: '0',
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
this.hasColGroupComponent = computed(() => !!unref(props.colGroupComponent))
|
|
189
|
+
this.hasHeaderComponent = computed(() => !!unref(props.headerComponent))
|
|
190
|
+
this.hasFooterComponent = computed(() => !!unref(props.footerComponent))
|
|
191
|
+
observe(this.viewportElement, (element) => this.observeViewport(element))
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
handleScroll = (event: Event) => {
|
|
195
|
+
const target = event.currentTarget
|
|
196
|
+
if (!(target instanceof HTMLElement)) return
|
|
197
|
+
|
|
198
|
+
this.scrollTop(target.scrollTop)
|
|
199
|
+
this.isScrolling = true
|
|
200
|
+
if (this.scrollEndTimer !== undefined) clearTimeout(this.scrollEndTimer)
|
|
201
|
+
this.scrollEndTimer = setTimeout(() => {
|
|
202
|
+
this.scrollEndTimer = undefined
|
|
203
|
+
this.isScrolling = false
|
|
204
|
+
if (!this.hasDeferredMeasurements) return
|
|
205
|
+
|
|
206
|
+
this.hasDeferredMeasurements = false
|
|
207
|
+
this.measurementVersion(this.measurementVersion() + 1)
|
|
208
|
+
}, 120)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
unmounted = () => {
|
|
212
|
+
if (this.pendingFrame) cancelAnimationFrame(this.pendingFrame)
|
|
213
|
+
if (this.scrollEndTimer !== undefined) clearTimeout(this.scrollEndTimer)
|
|
214
|
+
this.rowObserver?.disconnect()
|
|
215
|
+
this.bodyObserver?.disconnect()
|
|
216
|
+
this.rowElements.clear()
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private resolveVisibleRows() {
|
|
220
|
+
const source = this.resolvedItems()
|
|
221
|
+
const offsets = this.offsets()
|
|
222
|
+
const scrollTop = this.scrollTop()
|
|
223
|
+
const overscan = this.resolvedOverscan()
|
|
224
|
+
// Keep the two boundaries on the same scroll-position snapshot.
|
|
225
|
+
const start = Math.max(0, findOffsetIndex(offsets, scrollTop) - overscan)
|
|
226
|
+
const viewportBottom = scrollTop + this.resolvedHeight()
|
|
227
|
+
let end = findOffsetIndex(offsets, viewportBottom) + overscan + 1
|
|
228
|
+
end = Math.min(source.length, Math.max(start, end))
|
|
229
|
+
|
|
230
|
+
const rows: VirtualListRow[] = []
|
|
231
|
+
const nextWindow = new Map<number, VirtualListRow>()
|
|
232
|
+
for (let index = start; index < end; index += 1) {
|
|
233
|
+
const previous = this.rowWindow.get(index)
|
|
234
|
+
const row =
|
|
235
|
+
previous && previous.item === source[index]
|
|
236
|
+
? previous
|
|
237
|
+
: { index, item: source[index] }
|
|
238
|
+
rows.push(row)
|
|
239
|
+
nextWindow.set(index, row)
|
|
240
|
+
}
|
|
241
|
+
this.rowWindow = nextWindow
|
|
242
|
+
|
|
243
|
+
return rows
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private observeViewport(element: HTMLElement | null) {
|
|
247
|
+
this.rowObserver?.disconnect()
|
|
248
|
+
this.bodyObserver?.disconnect()
|
|
249
|
+
this.rowElements.clear()
|
|
250
|
+
if (!element) return
|
|
251
|
+
|
|
252
|
+
this.rowObserver = new ResizeObserver((entries) => {
|
|
253
|
+
for (const entry of entries) {
|
|
254
|
+
const row = entry.target as HTMLElement
|
|
255
|
+
const index = Number(row.getAttribute('data-virtual-row-index'))
|
|
256
|
+
if (this.rowElements.get(index) === row) this.queueMeasurement(index)
|
|
257
|
+
}
|
|
258
|
+
})
|
|
259
|
+
this.bodyObserver = new MutationObserver(() =>
|
|
260
|
+
this.syncMountedRows(element),
|
|
261
|
+
)
|
|
262
|
+
this.bodyObserver.observe(element, { childList: true, subtree: true })
|
|
263
|
+
this.syncMountedRows(element)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private syncMountedRows(viewport: HTMLElement) {
|
|
267
|
+
const next = new Map<number, HTMLElement>()
|
|
268
|
+
for (const row of viewport.querySelectorAll<HTMLElement>(
|
|
269
|
+
':scope > table > tbody > tr[data-virtual-row-index]',
|
|
270
|
+
)) {
|
|
271
|
+
const index = Number(row.getAttribute('data-virtual-row-index'))
|
|
272
|
+
if (Number.isInteger(index) && index >= 0) next.set(index, row)
|
|
273
|
+
}
|
|
274
|
+
for (const [index, element] of this.rowElements) {
|
|
275
|
+
if (next.get(index) !== element) {
|
|
276
|
+
this.rowObserver?.unobserve(element)
|
|
277
|
+
this.rowElements.delete(index)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
for (const [index, element] of next) {
|
|
281
|
+
if (this.rowElements.get(index) === element) continue
|
|
282
|
+
this.rowElements.set(index, element)
|
|
283
|
+
this.rowObserver?.observe(element)
|
|
284
|
+
this.queueMeasurement(index)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private queueMeasurement(index: number) {
|
|
289
|
+
this.pendingIndexes.add(index)
|
|
290
|
+
if (this.pendingFrame) return
|
|
291
|
+
|
|
292
|
+
this.pendingFrame = requestAnimationFrame(() => {
|
|
293
|
+
this.pendingFrame = 0
|
|
294
|
+
this.flushMeasurements()
|
|
295
|
+
})
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private flushMeasurements() {
|
|
299
|
+
let changed = false
|
|
300
|
+
|
|
301
|
+
for (const index of this.pendingIndexes) {
|
|
302
|
+
const element = this.rowElements.get(index)
|
|
303
|
+
if (!element) continue
|
|
304
|
+
|
|
305
|
+
const previous = this.rowHeights[index] ?? this.currentEstimateHeight()
|
|
306
|
+
const next = element.getBoundingClientRect().height
|
|
307
|
+
if (!Number.isFinite(next) || next <= 0) continue
|
|
308
|
+
if (Math.abs(previous - next) < 0.5) continue
|
|
309
|
+
|
|
310
|
+
this.updateMeasuredAverage(index, next)
|
|
311
|
+
this.rowHeights[index] = next
|
|
312
|
+
changed = true
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
this.pendingIndexes.clear()
|
|
316
|
+
if (!changed) return
|
|
317
|
+
|
|
318
|
+
if (this.isScrolling) {
|
|
319
|
+
this.hasDeferredMeasurements = true
|
|
320
|
+
return
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
this.measurementVersion(this.measurementVersion() + 1)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private currentEstimateHeight() {
|
|
327
|
+
if (this.measuredRowCount === 0) return this.resolvedEstimateHeight()
|
|
328
|
+
return this.measuredHeightTotal / this.measuredRowCount
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private updateMeasuredAverage(index: number, next: number) {
|
|
332
|
+
const previous = this.rowHeights[index]
|
|
333
|
+
if (previous === undefined) {
|
|
334
|
+
this.measuredRowCount += 1
|
|
335
|
+
this.measuredHeightTotal += next
|
|
336
|
+
return
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
this.measuredHeightTotal += next - previous
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function resolveVariableVirtualTable(
|
|
344
|
+
head: ComponentHead<VariableVirtualTable>,
|
|
345
|
+
) {
|
|
346
|
+
head.autoProps = false
|
|
347
|
+
return new VariableVirtualTableContext(head.props)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function resolvePositiveNumber(
|
|
351
|
+
value: RefOrValue<number | string> | undefined,
|
|
352
|
+
fallback: number,
|
|
353
|
+
) {
|
|
354
|
+
const raw = unref(value)
|
|
355
|
+
const parsed = typeof raw === 'number' ? raw : Number.parseFloat(String(raw))
|
|
356
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function findOffsetIndex(offsets: number[], scrollTop: number) {
|
|
360
|
+
if (offsets.length <= 1) return 0
|
|
361
|
+
|
|
362
|
+
let low = 0
|
|
363
|
+
let high = offsets.length - 1
|
|
364
|
+
while (low < high) {
|
|
365
|
+
const middle = Math.floor((low + high) / 2)
|
|
366
|
+
if ((offsets[middle] ?? 0) <= scrollTop) low = middle + 1
|
|
367
|
+
else high = middle
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return Math.max(0, low - 1)
|
|
371
|
+
}
|