@svgrid/grid 1.1.1 → 1.2.1
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/dist/FlexRender.svelte +96 -96
- package/dist/SvGrid.controller.svelte.js +17 -6
- package/dist/SvGrid.css +2012 -2012
- package/dist/SvGrid.svelte +2571 -2346
- package/dist/SvGridDropdown.svelte +7 -3
- package/dist/build-api.js +2 -2
- package/dist/cdn/svgrid.js +13504 -0
- package/dist/cdn/svgrid.svelte-external.js +11188 -0
- package/dist/cell-values.d.ts +1 -1
- package/dist/cell-values.js +7 -7
- package/dist/column-types.js +1 -1
- package/dist/core.d.ts +1 -1
- package/dist/core.js +2 -2
- package/dist/summaries.js +4 -4
- package/package.json +12 -2
- package/src/FlexRender.svelte +96 -96
- package/src/SvGrid.controller.svelte.ts +2363 -2352
- package/src/SvGrid.css +2012 -2012
- package/src/SvGrid.svelte +2571 -2346
- package/src/SvGrid.types.ts +537 -537
- package/src/SvGridDropdown.svelte +7 -3
- package/src/a11y.contract.test.ts +49 -49
- package/src/a11y.test.ts +59 -59
- package/src/a11y.ts +59 -59
- package/src/build-api.ts +683 -683
- package/src/cell-formatting.ts +169 -169
- package/src/cell-values.ts +4 -4
- package/src/column-types.ts +1 -1
- package/src/core.performance.test.ts +30 -30
- package/src/core.ts +1077 -1077
- package/src/createGrid.svelte.ts +42 -42
- package/src/createGrid.test.ts +10 -10
- package/src/createGridState.svelte.ts +17 -17
- package/src/editing.ts +669 -669
- package/src/flex-render.ts +3 -3
- package/src/index.ts +208 -208
- package/src/keyboard.test.ts +59 -59
- package/src/keyboard.ts +97 -97
- package/src/merge-objects.ts +48 -48
- package/src/render-component.ts +28 -28
- package/src/spreadsheet.test.ts +489 -489
- package/src/spreadsheet.ts +304 -304
- package/src/static-functions.ts +11 -11
- package/src/subscribe.ts +38 -38
- package/src/summaries.ts +4 -4
- package/src/svgrid-wrapper.types.ts +412 -412
- package/src/svgrid.features.test.ts +157 -157
- package/src/svgrid.wrapper.test.ts +40 -40
- package/src/virtualization/column-virtualizer.test.ts +27 -27
- package/src/virtualization/column-virtualizer.ts +30 -30
- package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
- package/src/virtualization/types.ts +30 -30
- package/src/virtualization/virtualizer.test.ts +47 -47
- package/src/virtualization/virtualizer.ts +296 -296
- package/dist/SvGrid.helpers.test.d.ts +0 -1
- package/dist/SvGrid.helpers.test.js +0 -298
- package/dist/SvGridChart.test.d.ts +0 -1
- package/dist/SvGridChart.test.js +0 -62
- package/dist/a11y.contract.test.d.ts +0 -1
- package/dist/a11y.contract.test.js +0 -39
- package/dist/a11y.test.d.ts +0 -1
- package/dist/a11y.test.js +0 -44
- package/dist/aligned-grids.test.d.ts +0 -1
- package/dist/aligned-grids.test.js +0 -75
- package/dist/build-api.coverage.test.d.ts +0 -20
- package/dist/build-api.coverage.test.js +0 -505
- package/dist/cell-formatting.test.d.ts +0 -1
- package/dist/cell-formatting.test.js +0 -234
- package/dist/cell-render.test.d.ts +0 -1
- package/dist/cell-render.test.js +0 -338
- package/dist/chart-export.test.d.ts +0 -1
- package/dist/chart-export.test.js +0 -302
- package/dist/chart.coverage.test.d.ts +0 -1
- package/dist/chart.coverage.test.js +0 -748
- package/dist/chart.test.d.ts +0 -1
- package/dist/chart.test.js +0 -289
- package/dist/clipboard.test.d.ts +0 -1
- package/dist/clipboard.test.js +0 -700
- package/dist/collaboration.coverage.test.d.ts +0 -1
- package/dist/collaboration.coverage.test.js +0 -200
- package/dist/collaboration.test.d.ts +0 -1
- package/dist/collaboration.test.js +0 -65
- package/dist/column-groups.test.d.ts +0 -1
- package/dist/column-groups.test.js +0 -56
- package/dist/column-types.test.d.ts +0 -1
- package/dist/column-types.test.js +0 -62
- package/dist/columns.test.d.ts +0 -1
- package/dist/columns.test.js +0 -625
- package/dist/conditional-formatting.test.d.ts +0 -1
- package/dist/conditional-formatting.test.js +0 -87
- package/dist/core.coverage.test.d.ts +0 -1
- package/dist/core.coverage.test.js +0 -186
- package/dist/core.performance.test.d.ts +0 -1
- package/dist/core.performance.test.js +0 -29
- package/dist/createGrid.test.d.ts +0 -1
- package/dist/createGrid.test.js +0 -9
- package/dist/editing.test.d.ts +0 -1
- package/dist/editing.test.js +0 -732
- package/dist/editors/cell-editors.coverage.test.d.ts +0 -1
- package/dist/editors/cell-editors.coverage.test.js +0 -139
- package/dist/editors/cell-editors.test.d.ts +0 -1
- package/dist/editors/cell-editors.test.js +0 -75
- package/dist/facet-buckets.test.d.ts +0 -1
- package/dist/facet-buckets.test.js +0 -296
- package/dist/fill-patterns.test.d.ts +0 -1
- package/dist/fill-patterns.test.js +0 -81
- package/dist/filter-operators.test.d.ts +0 -1
- package/dist/filter-operators.test.js +0 -135
- package/dist/filtering/excel-filters.test.d.ts +0 -1
- package/dist/filtering/excel-filters.test.js +0 -116
- package/dist/filtering/locale-filter.test.d.ts +0 -13
- package/dist/filtering/locale-filter.test.js +0 -189
- package/dist/flex-render.test.d.ts +0 -1
- package/dist/flex-render.test.js +0 -142
- package/dist/group-aggregate.test.d.ts +0 -1
- package/dist/group-aggregate.test.js +0 -32
- package/dist/hyperformula-adapter.test.d.ts +0 -1
- package/dist/hyperformula-adapter.test.js +0 -205
- package/dist/keyboard-handlers.coverage.test.d.ts +0 -1
- package/dist/keyboard-handlers.coverage.test.js +0 -495
- package/dist/keyboard.test.d.ts +0 -1
- package/dist/keyboard.test.js +0 -27
- package/dist/menus.test.d.ts +0 -1
- package/dist/menus.test.js +0 -560
- package/dist/named-views.coverage.test.d.ts +0 -1
- package/dist/named-views.coverage.test.js +0 -180
- package/dist/named-views.test.d.ts +0 -1
- package/dist/named-views.test.js +0 -57
- package/dist/new-features.test.d.ts +0 -1
- package/dist/new-features.test.js +0 -183
- package/dist/render-component.test.d.ts +0 -1
- package/dist/render-component.test.js +0 -41
- package/dist/row-drag.test.d.ts +0 -1
- package/dist/row-drag.test.js +0 -142
- package/dist/row-resize.test.d.ts +0 -1
- package/dist/row-resize.test.js +0 -329
- package/dist/scroll-sync.test.d.ts +0 -1
- package/dist/scroll-sync.test.js +0 -290
- package/dist/selection.multi-range.test.d.ts +0 -1
- package/dist/selection.multi-range.test.js +0 -55
- package/dist/selection.test.d.ts +0 -1
- package/dist/selection.test.js +0 -647
- package/dist/server-data-source.coverage.test.d.ts +0 -1
- package/dist/server-data-source.coverage.test.js +0 -154
- package/dist/server-data-source.test.d.ts +0 -1
- package/dist/server-data-source.test.js +0 -81
- package/dist/sparkline.test.d.ts +0 -1
- package/dist/sparkline.test.js +0 -52
- package/dist/spreadsheet.test.d.ts +0 -1
- package/dist/spreadsheet.test.js +0 -446
- package/dist/subscribe.test.d.ts +0 -1
- package/dist/subscribe.test.js +0 -93
- package/dist/svgrid.api-extensions.test.d.ts +0 -14
- package/dist/svgrid.api-extensions.test.js +0 -334
- package/dist/svgrid.api.test.d.ts +0 -15
- package/dist/svgrid.api.test.js +0 -169
- package/dist/svgrid.behavior.test.d.ts +0 -15
- package/dist/svgrid.behavior.test.js +0 -625
- package/dist/svgrid.column-reorder.test.d.ts +0 -15
- package/dist/svgrid.column-reorder.test.js +0 -224
- package/dist/svgrid.comments-autocomplete.test.d.ts +0 -1
- package/dist/svgrid.comments-autocomplete.test.js +0 -96
- package/dist/svgrid.context-menu.test.d.ts +0 -1
- package/dist/svgrid.context-menu.test.js +0 -102
- package/dist/svgrid.features.test.d.ts +0 -1
- package/dist/svgrid.features.test.js +0 -129
- package/dist/svgrid.interaction.test.d.ts +0 -9
- package/dist/svgrid.interaction.test.js +0 -349
- package/dist/svgrid.locale-filtering.test.d.ts +0 -15
- package/dist/svgrid.locale-filtering.test.js +0 -261
- package/dist/svgrid.new-features.wrapper.test.d.ts +0 -1
- package/dist/svgrid.new-features.wrapper.test.js +0 -194
- package/dist/svgrid.row-pinning.test.d.ts +0 -14
- package/dist/svgrid.row-pinning.test.js +0 -250
- package/dist/svgrid.set-filter.test.d.ts +0 -17
- package/dist/svgrid.set-filter.test.js +0 -268
- package/dist/svgrid.wrapper.test.d.ts +0 -1
- package/dist/svgrid.wrapper.test.js +0 -59
- package/dist/test-setup.d.ts +0 -1
- package/dist/test-setup.js +0 -35
- package/dist/transaction.test.d.ts +0 -1
- package/dist/transaction.test.js +0 -89
- package/dist/virtualization/column-virtualizer.test.d.ts +0 -1
- package/dist/virtualization/column-virtualizer.test.js +0 -25
- package/dist/virtualization/scroll-scaling.test.d.ts +0 -1
- package/dist/virtualization/scroll-scaling.test.js +0 -127
- package/dist/virtualization/virtualizer.test.d.ts +0 -1
- package/dist/virtualization/virtualizer.test.js +0 -41
|
@@ -1,296 +1,296 @@
|
|
|
1
|
-
import type { VirtualItem, VirtualizerOptions, VirtualizerState } from './types'
|
|
2
|
-
|
|
3
|
-
type VirtualizerListener = () => void
|
|
4
|
-
|
|
5
|
-
function clamp(value: number, min: number, max: number) {
|
|
6
|
-
return Math.min(Math.max(value, min), max)
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function sameOptions(a: VirtualizerOptions, b: VirtualizerOptions) {
|
|
10
|
-
return (
|
|
11
|
-
a.count === b.count &&
|
|
12
|
-
a.estimateSize === b.estimateSize &&
|
|
13
|
-
(a.overscan ?? 6) === (b.overscan ?? 6) &&
|
|
14
|
-
a.viewportHeight === b.viewportHeight &&
|
|
15
|
-
(a.scrollOffset ?? 0) === (b.scrollOffset ?? 0)
|
|
16
|
-
)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function sameState(a: VirtualizerState, b: VirtualizerState) {
|
|
20
|
-
if (
|
|
21
|
-
a.totalSize !== b.totalSize ||
|
|
22
|
-
a.startIndex !== b.startIndex ||
|
|
23
|
-
a.endIndex !== b.endIndex ||
|
|
24
|
-
a.viewportHeight !== b.viewportHeight ||
|
|
25
|
-
a.items.length !== b.items.length
|
|
26
|
-
) {
|
|
27
|
-
return false
|
|
28
|
-
}
|
|
29
|
-
// Per-item start+size check: catches column-resize cases where the total
|
|
30
|
-
// is unchanged (e.g. column A grew, column B shrunk to compensate under
|
|
31
|
-
// fit-to-width). Without this, the virtualizer skips the emit and the
|
|
32
|
-
// grid renders with stale widths.
|
|
33
|
-
for (let i = 0; i < a.items.length; i += 1) {
|
|
34
|
-
const ai = a.items[i]!
|
|
35
|
-
const bi = b.items[i]!
|
|
36
|
-
if (ai.index !== bi.index || ai.start !== bi.start || ai.size !== bi.size) return false
|
|
37
|
-
}
|
|
38
|
-
return true
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Slot-based keying: the key is the POSITION of the item inside the
|
|
42
|
-
// visible window (slot 0 = topmost rendered row, slot 1 = next, etc.)
|
|
43
|
-
// - NOT the data index. As the user scrolls, the window's startIndex
|
|
44
|
-
// changes but the slot keys stay the same (0..N-1), so Svelte recycles
|
|
45
|
-
// the existing <tr> DOM nodes and only updates their data + position.
|
|
46
|
-
//
|
|
47
|
-
// This avoids the mount/unmount churn that previously fired on every
|
|
48
|
-
// scroll tick: with data-index keys, scrolling down 1 row meant the old
|
|
49
|
-
// topmost key disappeared and a new bottommost key appeared, forcing
|
|
50
|
-
// Svelte to unmount the top <tr> and mount a fresh one at the bottom.
|
|
51
|
-
// Slot-based keys keep the same N <tr> nodes alive for the lifetime of
|
|
52
|
-
// the grid; scroll just translates them via the top spacer.
|
|
53
|
-
function buildUniformItems(
|
|
54
|
-
startIndex: number,
|
|
55
|
-
endIndex: number,
|
|
56
|
-
estimateSize: number,
|
|
57
|
-
): Array<VirtualItem> {
|
|
58
|
-
const items: Array<VirtualItem> = []
|
|
59
|
-
for (let index = startIndex; index <= endIndex; index += 1) {
|
|
60
|
-
const start = index * estimateSize
|
|
61
|
-
items.push({
|
|
62
|
-
index,
|
|
63
|
-
start,
|
|
64
|
-
size: estimateSize,
|
|
65
|
-
end: start + estimateSize,
|
|
66
|
-
key: `virtual_slot_${index - startIndex}`,
|
|
67
|
-
})
|
|
68
|
-
}
|
|
69
|
-
return items
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function buildVariableItems(
|
|
73
|
-
startIndex: number,
|
|
74
|
-
endIndex: number,
|
|
75
|
-
offsets: Array<number>,
|
|
76
|
-
): Array<VirtualItem> {
|
|
77
|
-
const items: Array<VirtualItem> = []
|
|
78
|
-
for (let index = startIndex; index <= endIndex; index += 1) {
|
|
79
|
-
const start = offsets[index] ?? 0
|
|
80
|
-
const end = offsets[index + 1] ?? start
|
|
81
|
-
items.push({
|
|
82
|
-
index,
|
|
83
|
-
start,
|
|
84
|
-
size: end - start,
|
|
85
|
-
end,
|
|
86
|
-
key: `virtual_slot_${index - startIndex}`,
|
|
87
|
-
})
|
|
88
|
-
}
|
|
89
|
-
return items
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function createState(
|
|
93
|
-
options: VirtualizerOptions,
|
|
94
|
-
/** Pre-built cumulative offsets for the variable-size path. */
|
|
95
|
-
offsets: Array<number> | null,
|
|
96
|
-
): VirtualizerState {
|
|
97
|
-
const count = Math.max(options.count, 0)
|
|
98
|
-
const overscan = Math.max(options.overscan ?? 6, 0)
|
|
99
|
-
const viewportHeight = Math.max(options.viewportHeight, 0)
|
|
100
|
-
|
|
101
|
-
if (typeof options.estimateSize === 'function' && offsets) {
|
|
102
|
-
const totalSize = offsets[count] ?? 0
|
|
103
|
-
const maxOffset = Math.max(totalSize - viewportHeight, 0)
|
|
104
|
-
const scrollOffset = clamp(options.scrollOffset ?? 0, 0, maxOffset)
|
|
105
|
-
|
|
106
|
-
// First index whose end > scrollOffset.
|
|
107
|
-
let lo = 0
|
|
108
|
-
let hi = count
|
|
109
|
-
while (lo < hi) {
|
|
110
|
-
const mid = (lo + hi) >>> 1
|
|
111
|
-
if ((offsets[mid + 1] ?? 0) <= scrollOffset) lo = mid + 1
|
|
112
|
-
else hi = mid
|
|
113
|
-
}
|
|
114
|
-
const visibleStart = lo
|
|
115
|
-
|
|
116
|
-
// First index whose start >= scrollOffset + viewportHeight.
|
|
117
|
-
const viewportEnd = scrollOffset + viewportHeight
|
|
118
|
-
lo = visibleStart
|
|
119
|
-
hi = count
|
|
120
|
-
while (lo < hi) {
|
|
121
|
-
const mid = (lo + hi) >>> 1
|
|
122
|
-
if ((offsets[mid] ?? 0) < viewportEnd) lo = mid + 1
|
|
123
|
-
else hi = mid
|
|
124
|
-
}
|
|
125
|
-
const visibleEnd = Math.max(lo - 1, visibleStart)
|
|
126
|
-
|
|
127
|
-
const startIndex = count === 0 ? 0 : clamp(visibleStart - overscan, 0, count - 1)
|
|
128
|
-
const endIndex = count === 0 ? -1 : clamp(visibleEnd + overscan, 0, count - 1)
|
|
129
|
-
|
|
130
|
-
return {
|
|
131
|
-
items: endIndex >= startIndex ? buildVariableItems(startIndex, endIndex, offsets) : [],
|
|
132
|
-
totalSize,
|
|
133
|
-
startIndex,
|
|
134
|
-
endIndex,
|
|
135
|
-
scrollOffset,
|
|
136
|
-
viewportHeight,
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Uniform-size fast path (original behavior).
|
|
141
|
-
const estimateSize = Math.max(
|
|
142
|
-
typeof options.estimateSize === 'number' ? options.estimateSize : 1,
|
|
143
|
-
1,
|
|
144
|
-
)
|
|
145
|
-
const totalSize = count * estimateSize
|
|
146
|
-
const maxOffset = Math.max(totalSize - viewportHeight, 0)
|
|
147
|
-
const scrollOffset = clamp(options.scrollOffset ?? 0, 0, maxOffset)
|
|
148
|
-
|
|
149
|
-
const visibleStart = Math.floor(scrollOffset / estimateSize)
|
|
150
|
-
const visibleCount = Math.ceil(viewportHeight / estimateSize)
|
|
151
|
-
const visibleEnd = Math.min(visibleStart + visibleCount, Math.max(count - 1, 0))
|
|
152
|
-
|
|
153
|
-
const startIndex = count === 0 ? 0 : clamp(visibleStart - overscan, 0, count - 1)
|
|
154
|
-
const endIndex = count === 0 ? -1 : clamp(visibleEnd + overscan, 0, count - 1)
|
|
155
|
-
|
|
156
|
-
return {
|
|
157
|
-
items: endIndex >= startIndex ? buildUniformItems(startIndex, endIndex, estimateSize) : [],
|
|
158
|
-
totalSize,
|
|
159
|
-
startIndex,
|
|
160
|
-
endIndex,
|
|
161
|
-
scrollOffset,
|
|
162
|
-
viewportHeight,
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/** Cached cumulative-offset table for the variable-size virtualizer path.
|
|
167
|
-
* Rebuilt only when count or the size function changes; reused across the
|
|
168
|
-
* many `recalc()` calls that happen during scroll. */
|
|
169
|
-
type OffsetCache = {
|
|
170
|
-
fn: (index: number) => number
|
|
171
|
-
count: number
|
|
172
|
-
offsets: Array<number>
|
|
173
|
-
} | null
|
|
174
|
-
|
|
175
|
-
export function createVirtualizer(initial: VirtualizerOptions) {
|
|
176
|
-
let options = initial
|
|
177
|
-
let offsetCache: OffsetCache = null
|
|
178
|
-
|
|
179
|
-
function getOffsets(): Array<number> | null {
|
|
180
|
-
if (typeof options.estimateSize !== 'function') return null
|
|
181
|
-
const count = Math.max(options.count, 0)
|
|
182
|
-
if (
|
|
183
|
-
offsetCache &&
|
|
184
|
-
offsetCache.fn === options.estimateSize &&
|
|
185
|
-
offsetCache.count === count
|
|
186
|
-
) {
|
|
187
|
-
return offsetCache.offsets
|
|
188
|
-
}
|
|
189
|
-
const sizeFn = options.estimateSize
|
|
190
|
-
const offsets = new Array<number>(count + 1)
|
|
191
|
-
offsets[0] = 0
|
|
192
|
-
for (let i = 0; i < count; i += 1) {
|
|
193
|
-
offsets[i + 1] = offsets[i]! + Math.max(sizeFn(i), 1)
|
|
194
|
-
}
|
|
195
|
-
offsetCache = { fn: sizeFn, count, offsets }
|
|
196
|
-
return offsets
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
let state = createState(options, getOffsets())
|
|
200
|
-
const listeners = new Set<VirtualizerListener>()
|
|
201
|
-
|
|
202
|
-
function emit() {
|
|
203
|
-
listeners.forEach((listener) => listener())
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function recalc() {
|
|
207
|
-
const next = createState(options, getOffsets())
|
|
208
|
-
if (sameState(state, next)) return
|
|
209
|
-
state = next
|
|
210
|
-
emit()
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
return {
|
|
214
|
-
setOptions(next: Partial<VirtualizerOptions>) {
|
|
215
|
-
const merged = { ...options, ...next }
|
|
216
|
-
if (sameOptions(options, merged)) return
|
|
217
|
-
options = merged
|
|
218
|
-
recalc()
|
|
219
|
-
},
|
|
220
|
-
setScrollOffset(scrollOffset: number) {
|
|
221
|
-
if ((options.scrollOffset ?? 0) === scrollOffset) return
|
|
222
|
-
options = { ...options, scrollOffset }
|
|
223
|
-
recalc()
|
|
224
|
-
},
|
|
225
|
-
setViewportHeight(viewportHeight: number) {
|
|
226
|
-
if (options.viewportHeight === viewportHeight) return
|
|
227
|
-
options = { ...options, viewportHeight }
|
|
228
|
-
recalc()
|
|
229
|
-
},
|
|
230
|
-
scrollToIndex(index: number) {
|
|
231
|
-
const boundedIndex = clamp(index, 0, Math.max(options.count - 1, 0))
|
|
232
|
-
// Read the current state's totals so the offset is correct under both
|
|
233
|
-
// uniform and per-index sizing.
|
|
234
|
-
const totalSize = state.totalSize
|
|
235
|
-
let targetOffset: number
|
|
236
|
-
if (typeof options.estimateSize === 'function') {
|
|
237
|
-
// Use the cached cumulative offsets if present.
|
|
238
|
-
const offsets = getOffsets()
|
|
239
|
-
if (offsets) {
|
|
240
|
-
targetOffset = offsets[boundedIndex] ?? 0
|
|
241
|
-
} else {
|
|
242
|
-
let acc = 0
|
|
243
|
-
const sizeFn = options.estimateSize
|
|
244
|
-
for (let i = 0; i < boundedIndex; i += 1) acc += Math.max(sizeFn(i), 1)
|
|
245
|
-
targetOffset = acc
|
|
246
|
-
}
|
|
247
|
-
} else {
|
|
248
|
-
targetOffset = boundedIndex * Math.max(options.estimateSize, 1)
|
|
249
|
-
}
|
|
250
|
-
const maxOffset = Math.max(totalSize - options.viewportHeight, 0)
|
|
251
|
-
const nextOffset = clamp(targetOffset, 0, maxOffset)
|
|
252
|
-
if ((options.scrollOffset ?? 0) === nextOffset) return
|
|
253
|
-
options = { ...options, scrollOffset: nextOffset }
|
|
254
|
-
recalc()
|
|
255
|
-
},
|
|
256
|
-
getVirtualItems() {
|
|
257
|
-
return state.items
|
|
258
|
-
},
|
|
259
|
-
getTotalSize() {
|
|
260
|
-
return state.totalSize
|
|
261
|
-
},
|
|
262
|
-
/** Cumulative offset of row `index` from the top in px, regardless
|
|
263
|
-
* of whether `estimateSize` is uniform or per-index. Uses the
|
|
264
|
-
* cached offsets array under function-form sizing so the lookup
|
|
265
|
-
* is O(1) instead of O(index). */
|
|
266
|
-
getOffsetForIndex(index: number): number {
|
|
267
|
-
if (index <= 0) return 0
|
|
268
|
-
const count = Math.max(options.count, 0)
|
|
269
|
-
const bounded = Math.min(index, count)
|
|
270
|
-
if (typeof options.estimateSize === 'function') {
|
|
271
|
-
const offsets = getOffsets()
|
|
272
|
-
if (offsets) return offsets[bounded] ?? 0
|
|
273
|
-
let acc = 0
|
|
274
|
-
const fn = options.estimateSize
|
|
275
|
-
for (let i = 0; i < bounded; i += 1) acc += Math.max(fn(i), 1)
|
|
276
|
-
return acc
|
|
277
|
-
}
|
|
278
|
-
return bounded * Math.max(options.estimateSize, 1)
|
|
279
|
-
},
|
|
280
|
-
/** Height of row `index` in px (whichever estimateSize provides). */
|
|
281
|
-
getSizeForIndex(index: number): number {
|
|
282
|
-
if (index < 0 || index >= options.count) return 0
|
|
283
|
-
if (typeof options.estimateSize === 'function') {
|
|
284
|
-
return Math.max(options.estimateSize(index), 1)
|
|
285
|
-
}
|
|
286
|
-
return Math.max(options.estimateSize, 1)
|
|
287
|
-
},
|
|
288
|
-
getState() {
|
|
289
|
-
return state
|
|
290
|
-
},
|
|
291
|
-
subscribe(listener: VirtualizerListener) {
|
|
292
|
-
listeners.add(listener)
|
|
293
|
-
return () => listeners.delete(listener)
|
|
294
|
-
},
|
|
295
|
-
}
|
|
296
|
-
}
|
|
1
|
+
import type { VirtualItem, VirtualizerOptions, VirtualizerState } from './types'
|
|
2
|
+
|
|
3
|
+
type VirtualizerListener = () => void
|
|
4
|
+
|
|
5
|
+
function clamp(value: number, min: number, max: number) {
|
|
6
|
+
return Math.min(Math.max(value, min), max)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function sameOptions(a: VirtualizerOptions, b: VirtualizerOptions) {
|
|
10
|
+
return (
|
|
11
|
+
a.count === b.count &&
|
|
12
|
+
a.estimateSize === b.estimateSize &&
|
|
13
|
+
(a.overscan ?? 6) === (b.overscan ?? 6) &&
|
|
14
|
+
a.viewportHeight === b.viewportHeight &&
|
|
15
|
+
(a.scrollOffset ?? 0) === (b.scrollOffset ?? 0)
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function sameState(a: VirtualizerState, b: VirtualizerState) {
|
|
20
|
+
if (
|
|
21
|
+
a.totalSize !== b.totalSize ||
|
|
22
|
+
a.startIndex !== b.startIndex ||
|
|
23
|
+
a.endIndex !== b.endIndex ||
|
|
24
|
+
a.viewportHeight !== b.viewportHeight ||
|
|
25
|
+
a.items.length !== b.items.length
|
|
26
|
+
) {
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
// Per-item start+size check: catches column-resize cases where the total
|
|
30
|
+
// is unchanged (e.g. column A grew, column B shrunk to compensate under
|
|
31
|
+
// fit-to-width). Without this, the virtualizer skips the emit and the
|
|
32
|
+
// grid renders with stale widths.
|
|
33
|
+
for (let i = 0; i < a.items.length; i += 1) {
|
|
34
|
+
const ai = a.items[i]!
|
|
35
|
+
const bi = b.items[i]!
|
|
36
|
+
if (ai.index !== bi.index || ai.start !== bi.start || ai.size !== bi.size) return false
|
|
37
|
+
}
|
|
38
|
+
return true
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Slot-based keying: the key is the POSITION of the item inside the
|
|
42
|
+
// visible window (slot 0 = topmost rendered row, slot 1 = next, etc.)
|
|
43
|
+
// - NOT the data index. As the user scrolls, the window's startIndex
|
|
44
|
+
// changes but the slot keys stay the same (0..N-1), so Svelte recycles
|
|
45
|
+
// the existing <tr> DOM nodes and only updates their data + position.
|
|
46
|
+
//
|
|
47
|
+
// This avoids the mount/unmount churn that previously fired on every
|
|
48
|
+
// scroll tick: with data-index keys, scrolling down 1 row meant the old
|
|
49
|
+
// topmost key disappeared and a new bottommost key appeared, forcing
|
|
50
|
+
// Svelte to unmount the top <tr> and mount a fresh one at the bottom.
|
|
51
|
+
// Slot-based keys keep the same N <tr> nodes alive for the lifetime of
|
|
52
|
+
// the grid; scroll just translates them via the top spacer.
|
|
53
|
+
function buildUniformItems(
|
|
54
|
+
startIndex: number,
|
|
55
|
+
endIndex: number,
|
|
56
|
+
estimateSize: number,
|
|
57
|
+
): Array<VirtualItem> {
|
|
58
|
+
const items: Array<VirtualItem> = []
|
|
59
|
+
for (let index = startIndex; index <= endIndex; index += 1) {
|
|
60
|
+
const start = index * estimateSize
|
|
61
|
+
items.push({
|
|
62
|
+
index,
|
|
63
|
+
start,
|
|
64
|
+
size: estimateSize,
|
|
65
|
+
end: start + estimateSize,
|
|
66
|
+
key: `virtual_slot_${index - startIndex}`,
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
return items
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function buildVariableItems(
|
|
73
|
+
startIndex: number,
|
|
74
|
+
endIndex: number,
|
|
75
|
+
offsets: Array<number>,
|
|
76
|
+
): Array<VirtualItem> {
|
|
77
|
+
const items: Array<VirtualItem> = []
|
|
78
|
+
for (let index = startIndex; index <= endIndex; index += 1) {
|
|
79
|
+
const start = offsets[index] ?? 0
|
|
80
|
+
const end = offsets[index + 1] ?? start
|
|
81
|
+
items.push({
|
|
82
|
+
index,
|
|
83
|
+
start,
|
|
84
|
+
size: end - start,
|
|
85
|
+
end,
|
|
86
|
+
key: `virtual_slot_${index - startIndex}`,
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
return items
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function createState(
|
|
93
|
+
options: VirtualizerOptions,
|
|
94
|
+
/** Pre-built cumulative offsets for the variable-size path. */
|
|
95
|
+
offsets: Array<number> | null,
|
|
96
|
+
): VirtualizerState {
|
|
97
|
+
const count = Math.max(options.count, 0)
|
|
98
|
+
const overscan = Math.max(options.overscan ?? 6, 0)
|
|
99
|
+
const viewportHeight = Math.max(options.viewportHeight, 0)
|
|
100
|
+
|
|
101
|
+
if (typeof options.estimateSize === 'function' && offsets) {
|
|
102
|
+
const totalSize = offsets[count] ?? 0
|
|
103
|
+
const maxOffset = Math.max(totalSize - viewportHeight, 0)
|
|
104
|
+
const scrollOffset = clamp(options.scrollOffset ?? 0, 0, maxOffset)
|
|
105
|
+
|
|
106
|
+
// First index whose end > scrollOffset.
|
|
107
|
+
let lo = 0
|
|
108
|
+
let hi = count
|
|
109
|
+
while (lo < hi) {
|
|
110
|
+
const mid = (lo + hi) >>> 1
|
|
111
|
+
if ((offsets[mid + 1] ?? 0) <= scrollOffset) lo = mid + 1
|
|
112
|
+
else hi = mid
|
|
113
|
+
}
|
|
114
|
+
const visibleStart = lo
|
|
115
|
+
|
|
116
|
+
// First index whose start >= scrollOffset + viewportHeight.
|
|
117
|
+
const viewportEnd = scrollOffset + viewportHeight
|
|
118
|
+
lo = visibleStart
|
|
119
|
+
hi = count
|
|
120
|
+
while (lo < hi) {
|
|
121
|
+
const mid = (lo + hi) >>> 1
|
|
122
|
+
if ((offsets[mid] ?? 0) < viewportEnd) lo = mid + 1
|
|
123
|
+
else hi = mid
|
|
124
|
+
}
|
|
125
|
+
const visibleEnd = Math.max(lo - 1, visibleStart)
|
|
126
|
+
|
|
127
|
+
const startIndex = count === 0 ? 0 : clamp(visibleStart - overscan, 0, count - 1)
|
|
128
|
+
const endIndex = count === 0 ? -1 : clamp(visibleEnd + overscan, 0, count - 1)
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
items: endIndex >= startIndex ? buildVariableItems(startIndex, endIndex, offsets) : [],
|
|
132
|
+
totalSize,
|
|
133
|
+
startIndex,
|
|
134
|
+
endIndex,
|
|
135
|
+
scrollOffset,
|
|
136
|
+
viewportHeight,
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Uniform-size fast path (original behavior).
|
|
141
|
+
const estimateSize = Math.max(
|
|
142
|
+
typeof options.estimateSize === 'number' ? options.estimateSize : 1,
|
|
143
|
+
1,
|
|
144
|
+
)
|
|
145
|
+
const totalSize = count * estimateSize
|
|
146
|
+
const maxOffset = Math.max(totalSize - viewportHeight, 0)
|
|
147
|
+
const scrollOffset = clamp(options.scrollOffset ?? 0, 0, maxOffset)
|
|
148
|
+
|
|
149
|
+
const visibleStart = Math.floor(scrollOffset / estimateSize)
|
|
150
|
+
const visibleCount = Math.ceil(viewportHeight / estimateSize)
|
|
151
|
+
const visibleEnd = Math.min(visibleStart + visibleCount, Math.max(count - 1, 0))
|
|
152
|
+
|
|
153
|
+
const startIndex = count === 0 ? 0 : clamp(visibleStart - overscan, 0, count - 1)
|
|
154
|
+
const endIndex = count === 0 ? -1 : clamp(visibleEnd + overscan, 0, count - 1)
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
items: endIndex >= startIndex ? buildUniformItems(startIndex, endIndex, estimateSize) : [],
|
|
158
|
+
totalSize,
|
|
159
|
+
startIndex,
|
|
160
|
+
endIndex,
|
|
161
|
+
scrollOffset,
|
|
162
|
+
viewportHeight,
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Cached cumulative-offset table for the variable-size virtualizer path.
|
|
167
|
+
* Rebuilt only when count or the size function changes; reused across the
|
|
168
|
+
* many `recalc()` calls that happen during scroll. */
|
|
169
|
+
type OffsetCache = {
|
|
170
|
+
fn: (index: number) => number
|
|
171
|
+
count: number
|
|
172
|
+
offsets: Array<number>
|
|
173
|
+
} | null
|
|
174
|
+
|
|
175
|
+
export function createVirtualizer(initial: VirtualizerOptions) {
|
|
176
|
+
let options = initial
|
|
177
|
+
let offsetCache: OffsetCache = null
|
|
178
|
+
|
|
179
|
+
function getOffsets(): Array<number> | null {
|
|
180
|
+
if (typeof options.estimateSize !== 'function') return null
|
|
181
|
+
const count = Math.max(options.count, 0)
|
|
182
|
+
if (
|
|
183
|
+
offsetCache &&
|
|
184
|
+
offsetCache.fn === options.estimateSize &&
|
|
185
|
+
offsetCache.count === count
|
|
186
|
+
) {
|
|
187
|
+
return offsetCache.offsets
|
|
188
|
+
}
|
|
189
|
+
const sizeFn = options.estimateSize
|
|
190
|
+
const offsets = new Array<number>(count + 1)
|
|
191
|
+
offsets[0] = 0
|
|
192
|
+
for (let i = 0; i < count; i += 1) {
|
|
193
|
+
offsets[i + 1] = offsets[i]! + Math.max(sizeFn(i), 1)
|
|
194
|
+
}
|
|
195
|
+
offsetCache = { fn: sizeFn, count, offsets }
|
|
196
|
+
return offsets
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let state = createState(options, getOffsets())
|
|
200
|
+
const listeners = new Set<VirtualizerListener>()
|
|
201
|
+
|
|
202
|
+
function emit() {
|
|
203
|
+
listeners.forEach((listener) => listener())
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function recalc() {
|
|
207
|
+
const next = createState(options, getOffsets())
|
|
208
|
+
if (sameState(state, next)) return
|
|
209
|
+
state = next
|
|
210
|
+
emit()
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
setOptions(next: Partial<VirtualizerOptions>) {
|
|
215
|
+
const merged = { ...options, ...next }
|
|
216
|
+
if (sameOptions(options, merged)) return
|
|
217
|
+
options = merged
|
|
218
|
+
recalc()
|
|
219
|
+
},
|
|
220
|
+
setScrollOffset(scrollOffset: number) {
|
|
221
|
+
if ((options.scrollOffset ?? 0) === scrollOffset) return
|
|
222
|
+
options = { ...options, scrollOffset }
|
|
223
|
+
recalc()
|
|
224
|
+
},
|
|
225
|
+
setViewportHeight(viewportHeight: number) {
|
|
226
|
+
if (options.viewportHeight === viewportHeight) return
|
|
227
|
+
options = { ...options, viewportHeight }
|
|
228
|
+
recalc()
|
|
229
|
+
},
|
|
230
|
+
scrollToIndex(index: number) {
|
|
231
|
+
const boundedIndex = clamp(index, 0, Math.max(options.count - 1, 0))
|
|
232
|
+
// Read the current state's totals so the offset is correct under both
|
|
233
|
+
// uniform and per-index sizing.
|
|
234
|
+
const totalSize = state.totalSize
|
|
235
|
+
let targetOffset: number
|
|
236
|
+
if (typeof options.estimateSize === 'function') {
|
|
237
|
+
// Use the cached cumulative offsets if present.
|
|
238
|
+
const offsets = getOffsets()
|
|
239
|
+
if (offsets) {
|
|
240
|
+
targetOffset = offsets[boundedIndex] ?? 0
|
|
241
|
+
} else {
|
|
242
|
+
let acc = 0
|
|
243
|
+
const sizeFn = options.estimateSize
|
|
244
|
+
for (let i = 0; i < boundedIndex; i += 1) acc += Math.max(sizeFn(i), 1)
|
|
245
|
+
targetOffset = acc
|
|
246
|
+
}
|
|
247
|
+
} else {
|
|
248
|
+
targetOffset = boundedIndex * Math.max(options.estimateSize, 1)
|
|
249
|
+
}
|
|
250
|
+
const maxOffset = Math.max(totalSize - options.viewportHeight, 0)
|
|
251
|
+
const nextOffset = clamp(targetOffset, 0, maxOffset)
|
|
252
|
+
if ((options.scrollOffset ?? 0) === nextOffset) return
|
|
253
|
+
options = { ...options, scrollOffset: nextOffset }
|
|
254
|
+
recalc()
|
|
255
|
+
},
|
|
256
|
+
getVirtualItems() {
|
|
257
|
+
return state.items
|
|
258
|
+
},
|
|
259
|
+
getTotalSize() {
|
|
260
|
+
return state.totalSize
|
|
261
|
+
},
|
|
262
|
+
/** Cumulative offset of row `index` from the top in px, regardless
|
|
263
|
+
* of whether `estimateSize` is uniform or per-index. Uses the
|
|
264
|
+
* cached offsets array under function-form sizing so the lookup
|
|
265
|
+
* is O(1) instead of O(index). */
|
|
266
|
+
getOffsetForIndex(index: number): number {
|
|
267
|
+
if (index <= 0) return 0
|
|
268
|
+
const count = Math.max(options.count, 0)
|
|
269
|
+
const bounded = Math.min(index, count)
|
|
270
|
+
if (typeof options.estimateSize === 'function') {
|
|
271
|
+
const offsets = getOffsets()
|
|
272
|
+
if (offsets) return offsets[bounded] ?? 0
|
|
273
|
+
let acc = 0
|
|
274
|
+
const fn = options.estimateSize
|
|
275
|
+
for (let i = 0; i < bounded; i += 1) acc += Math.max(fn(i), 1)
|
|
276
|
+
return acc
|
|
277
|
+
}
|
|
278
|
+
return bounded * Math.max(options.estimateSize, 1)
|
|
279
|
+
},
|
|
280
|
+
/** Height of row `index` in px (whichever estimateSize provides). */
|
|
281
|
+
getSizeForIndex(index: number): number {
|
|
282
|
+
if (index < 0 || index >= options.count) return 0
|
|
283
|
+
if (typeof options.estimateSize === 'function') {
|
|
284
|
+
return Math.max(options.estimateSize(index), 1)
|
|
285
|
+
}
|
|
286
|
+
return Math.max(options.estimateSize, 1)
|
|
287
|
+
},
|
|
288
|
+
getState() {
|
|
289
|
+
return state
|
|
290
|
+
},
|
|
291
|
+
subscribe(listener: VirtualizerListener) {
|
|
292
|
+
listeners.add(listener)
|
|
293
|
+
return () => listeners.delete(listener)
|
|
294
|
+
},
|
|
295
|
+
}
|
|
296
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|