@pdanpdan/virtual-scroll 0.3.0 → 0.4.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/README.md +160 -116
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +834 -145
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +639 -416
- package/dist/index.mjs.map +1 -1
- package/dist/virtual-scroll.css +1 -1
- package/package.json +1 -1
- package/src/components/VirtualScroll.test.ts +523 -731
- package/src/components/VirtualScroll.vue +343 -214
- package/src/composables/useVirtualScroll.test.ts +240 -1879
- package/src/composables/useVirtualScroll.ts +482 -554
- package/src/index.ts +2 -0
- package/src/types.ts +535 -0
- package/src/utils/fenwick-tree.ts +38 -18
- package/src/utils/scroll.test.ts +148 -0
- package/src/utils/scroll.ts +40 -10
- package/src/utils/virtual-scroll-logic.test.ts +2517 -0
- package/src/utils/virtual-scroll-logic.ts +605 -0
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ColumnRangeParams,
|
|
3
|
+
ItemPositionParams,
|
|
4
|
+
ItemStyleParams,
|
|
5
|
+
RangeParams,
|
|
6
|
+
ScrollAlignment,
|
|
7
|
+
ScrollAlignmentOptions,
|
|
8
|
+
ScrollTargetParams,
|
|
9
|
+
ScrollTargetResult,
|
|
10
|
+
ScrollToIndexOptions,
|
|
11
|
+
StickyParams,
|
|
12
|
+
TotalSizeParams,
|
|
13
|
+
} from '../types';
|
|
14
|
+
|
|
15
|
+
import { isScrollToIndexOptions } from './scroll';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Calculates the target scroll position (relative to content) for a given row/column index and alignment.
|
|
19
|
+
*
|
|
20
|
+
* @param params - The parameters for calculation.
|
|
21
|
+
* @returns The target X and Y positions and item dimensions.
|
|
22
|
+
* @see ScrollTargetParams
|
|
23
|
+
* @see ScrollTargetResult
|
|
24
|
+
*/
|
|
25
|
+
export function calculateScrollTarget(params: ScrollTargetParams): ScrollTargetResult {
|
|
26
|
+
const {
|
|
27
|
+
rowIndex,
|
|
28
|
+
colIndex,
|
|
29
|
+
options,
|
|
30
|
+
itemsLength,
|
|
31
|
+
columnCount,
|
|
32
|
+
direction,
|
|
33
|
+
usableWidth,
|
|
34
|
+
usableHeight,
|
|
35
|
+
totalWidth,
|
|
36
|
+
totalHeight,
|
|
37
|
+
gap,
|
|
38
|
+
columnGap,
|
|
39
|
+
fixedSize,
|
|
40
|
+
fixedWidth,
|
|
41
|
+
relativeScrollX,
|
|
42
|
+
relativeScrollY,
|
|
43
|
+
getItemSizeY,
|
|
44
|
+
getItemSizeX,
|
|
45
|
+
getItemQueryY,
|
|
46
|
+
getItemQueryX,
|
|
47
|
+
getColumnSize,
|
|
48
|
+
getColumnQuery,
|
|
49
|
+
stickyIndices,
|
|
50
|
+
} = params;
|
|
51
|
+
|
|
52
|
+
let align: ScrollAlignment | ScrollAlignmentOptions | ScrollToIndexOptions | undefined;
|
|
53
|
+
|
|
54
|
+
if (isScrollToIndexOptions(options)) {
|
|
55
|
+
align = options.align;
|
|
56
|
+
} else {
|
|
57
|
+
align = options as ScrollAlignment | ScrollAlignmentOptions;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const alignX = (typeof align === 'object' ? align.x : align) || 'auto';
|
|
61
|
+
const alignY = (typeof align === 'object' ? align.y : align) || 'auto';
|
|
62
|
+
|
|
63
|
+
const isVertical = direction === 'vertical' || direction === 'both';
|
|
64
|
+
const isHorizontal = direction === 'horizontal' || direction === 'both';
|
|
65
|
+
|
|
66
|
+
let targetX = relativeScrollX;
|
|
67
|
+
let targetY = relativeScrollY;
|
|
68
|
+
let itemWidth = 0;
|
|
69
|
+
let itemHeight = 0;
|
|
70
|
+
let effectiveAlignX: ScrollAlignment = alignX === 'auto' ? 'auto' : alignX;
|
|
71
|
+
let effectiveAlignY: ScrollAlignment = alignY === 'auto' ? 'auto' : alignY;
|
|
72
|
+
|
|
73
|
+
// Y calculation
|
|
74
|
+
if (rowIndex != null) {
|
|
75
|
+
let stickyOffsetY = 0;
|
|
76
|
+
if (isVertical && stickyIndices && stickyIndices.length > 0) {
|
|
77
|
+
let activeStickyIdx: number | undefined;
|
|
78
|
+
let low = 0;
|
|
79
|
+
let high = stickyIndices.length - 1;
|
|
80
|
+
while (low <= high) {
|
|
81
|
+
const mid = (low + high) >>> 1;
|
|
82
|
+
if (stickyIndices[ mid ]! < rowIndex) {
|
|
83
|
+
activeStickyIdx = stickyIndices[ mid ];
|
|
84
|
+
low = mid + 1;
|
|
85
|
+
} else {
|
|
86
|
+
high = mid - 1;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (activeStickyIdx !== undefined) {
|
|
91
|
+
stickyOffsetY = fixedSize !== null ? fixedSize : getItemSizeY(activeStickyIdx) - gap;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let itemY = 0;
|
|
96
|
+
if (rowIndex >= itemsLength) {
|
|
97
|
+
itemY = totalHeight;
|
|
98
|
+
itemHeight = 0;
|
|
99
|
+
} else {
|
|
100
|
+
itemY = fixedSize !== null ? rowIndex * (fixedSize + gap) : getItemQueryY(rowIndex);
|
|
101
|
+
itemHeight = fixedSize !== null ? fixedSize : getItemSizeY(rowIndex) - gap;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Apply Y Alignment
|
|
105
|
+
if (alignY === 'start') {
|
|
106
|
+
targetY = itemY - stickyOffsetY;
|
|
107
|
+
} else if (alignY === 'center') {
|
|
108
|
+
targetY = itemY - (usableHeight - itemHeight) / 2;
|
|
109
|
+
} else if (alignY === 'end') {
|
|
110
|
+
targetY = itemY - (usableHeight - itemHeight);
|
|
111
|
+
} else {
|
|
112
|
+
// Auto alignment: stay if visible, otherwise align to nearest edge (minimal movement)
|
|
113
|
+
const isVisibleY = itemHeight <= (usableHeight - stickyOffsetY)
|
|
114
|
+
? (itemY >= relativeScrollY + stickyOffsetY - 0.5 && (itemY + itemHeight) <= (relativeScrollY + usableHeight + 0.5))
|
|
115
|
+
: (itemY <= relativeScrollY + stickyOffsetY + 0.5 && (itemY + itemHeight) >= (relativeScrollY + usableHeight - 0.5));
|
|
116
|
+
|
|
117
|
+
if (!isVisibleY) {
|
|
118
|
+
const targetStart = itemY - stickyOffsetY;
|
|
119
|
+
const targetEnd = itemY - (usableHeight - itemHeight);
|
|
120
|
+
|
|
121
|
+
if (itemHeight <= usableHeight - stickyOffsetY) {
|
|
122
|
+
if (itemY < relativeScrollY + stickyOffsetY) {
|
|
123
|
+
targetY = targetStart;
|
|
124
|
+
effectiveAlignY = 'start';
|
|
125
|
+
} else {
|
|
126
|
+
targetY = targetEnd;
|
|
127
|
+
effectiveAlignY = 'end';
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
// Large item: minimal movement
|
|
131
|
+
if (Math.abs(targetStart - relativeScrollY) < Math.abs(targetEnd - relativeScrollY)) {
|
|
132
|
+
targetY = targetStart;
|
|
133
|
+
effectiveAlignY = 'start';
|
|
134
|
+
} else {
|
|
135
|
+
targetY = targetEnd;
|
|
136
|
+
effectiveAlignY = 'end';
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// X calculation
|
|
144
|
+
if (colIndex != null) {
|
|
145
|
+
let stickyOffsetX = 0;
|
|
146
|
+
if (isHorizontal && stickyIndices && stickyIndices.length > 0 && (direction === 'horizontal' || direction === 'both')) {
|
|
147
|
+
let activeStickyIdx: number | undefined;
|
|
148
|
+
let low = 0;
|
|
149
|
+
let high = stickyIndices.length - 1;
|
|
150
|
+
while (low <= high) {
|
|
151
|
+
const mid = (low + high) >>> 1;
|
|
152
|
+
if (stickyIndices[ mid ]! < colIndex) {
|
|
153
|
+
activeStickyIdx = stickyIndices[ mid ];
|
|
154
|
+
low = mid + 1;
|
|
155
|
+
} else {
|
|
156
|
+
high = mid - 1;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (activeStickyIdx !== undefined) {
|
|
161
|
+
stickyOffsetX = direction === 'horizontal'
|
|
162
|
+
? (fixedSize !== null ? fixedSize : getItemSizeX(activeStickyIdx) - columnGap)
|
|
163
|
+
: (fixedWidth !== null ? fixedWidth : getColumnSize(activeStickyIdx) - columnGap);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let itemX = 0;
|
|
168
|
+
if (colIndex >= columnCount && columnCount > 0) {
|
|
169
|
+
itemX = totalWidth;
|
|
170
|
+
itemWidth = 0;
|
|
171
|
+
} else if (direction === 'horizontal') {
|
|
172
|
+
itemX = fixedSize !== null ? colIndex * (fixedSize + columnGap) : getItemQueryX(colIndex);
|
|
173
|
+
itemWidth = fixedSize !== null ? fixedSize : getItemSizeX(colIndex) - columnGap;
|
|
174
|
+
} else {
|
|
175
|
+
itemX = getColumnQuery(colIndex);
|
|
176
|
+
itemWidth = getColumnSize(colIndex) - columnGap;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Apply X Alignment
|
|
180
|
+
if (alignX === 'start') {
|
|
181
|
+
targetX = itemX - stickyOffsetX;
|
|
182
|
+
} else if (alignX === 'center') {
|
|
183
|
+
targetX = itemX - (usableWidth - itemWidth) / 2;
|
|
184
|
+
} else if (alignX === 'end') {
|
|
185
|
+
targetX = itemX - (usableWidth - itemWidth);
|
|
186
|
+
} else {
|
|
187
|
+
// Auto alignment: stay if visible, otherwise align to nearest edge (minimal movement)
|
|
188
|
+
const isVisibleX = itemWidth <= (usableWidth - stickyOffsetX)
|
|
189
|
+
? (itemX >= relativeScrollX + stickyOffsetX - 0.5 && (itemX + itemWidth) <= (relativeScrollX + usableWidth + 0.5))
|
|
190
|
+
: (itemX <= relativeScrollX + stickyOffsetX + 0.5 && (itemX + itemWidth) >= (relativeScrollX + usableWidth - 0.5));
|
|
191
|
+
|
|
192
|
+
if (!isVisibleX) {
|
|
193
|
+
const targetStart = itemX - stickyOffsetX;
|
|
194
|
+
const targetEnd = itemX - (usableWidth - itemWidth);
|
|
195
|
+
|
|
196
|
+
if (itemWidth <= usableWidth - stickyOffsetX) {
|
|
197
|
+
if (itemX < relativeScrollX + stickyOffsetX) {
|
|
198
|
+
targetX = targetStart;
|
|
199
|
+
effectiveAlignX = 'start';
|
|
200
|
+
} else {
|
|
201
|
+
targetX = targetEnd;
|
|
202
|
+
effectiveAlignX = 'end';
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
// Large item: minimal movement
|
|
206
|
+
if (Math.abs(targetStart - relativeScrollX) < Math.abs(targetEnd - relativeScrollX)) {
|
|
207
|
+
targetX = targetStart;
|
|
208
|
+
effectiveAlignX = 'start';
|
|
209
|
+
} else {
|
|
210
|
+
targetX = targetEnd;
|
|
211
|
+
effectiveAlignX = 'end';
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Clamp to valid range
|
|
219
|
+
targetX = Math.max(0, Math.min(targetX, Math.max(0, totalWidth - usableWidth)));
|
|
220
|
+
targetY = Math.max(0, Math.min(targetY, Math.max(0, totalHeight - usableHeight)));
|
|
221
|
+
|
|
222
|
+
return { targetX, targetY, itemWidth, itemHeight, effectiveAlignX, effectiveAlignY };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Calculates the range of items to render based on scroll position and viewport size.
|
|
227
|
+
*
|
|
228
|
+
* @param params - The parameters for calculation.
|
|
229
|
+
* @returns The start and end indices of the items to render.
|
|
230
|
+
* @see RangeParams
|
|
231
|
+
*/
|
|
232
|
+
export function calculateRange(params: RangeParams) {
|
|
233
|
+
const {
|
|
234
|
+
direction,
|
|
235
|
+
relativeScrollX,
|
|
236
|
+
relativeScrollY,
|
|
237
|
+
usableWidth,
|
|
238
|
+
usableHeight,
|
|
239
|
+
itemsLength,
|
|
240
|
+
bufferBefore,
|
|
241
|
+
bufferAfter,
|
|
242
|
+
gap,
|
|
243
|
+
columnGap,
|
|
244
|
+
fixedSize,
|
|
245
|
+
findLowerBoundY,
|
|
246
|
+
findLowerBoundX,
|
|
247
|
+
queryY,
|
|
248
|
+
queryX,
|
|
249
|
+
} = params;
|
|
250
|
+
|
|
251
|
+
const isVertical = direction === 'vertical' || direction === 'both';
|
|
252
|
+
|
|
253
|
+
let start = 0;
|
|
254
|
+
let end = itemsLength;
|
|
255
|
+
|
|
256
|
+
if (isVertical) {
|
|
257
|
+
if (fixedSize !== null) {
|
|
258
|
+
start = Math.floor(relativeScrollY / (fixedSize + gap));
|
|
259
|
+
end = Math.ceil((relativeScrollY + usableHeight) / (fixedSize + gap));
|
|
260
|
+
} else {
|
|
261
|
+
start = findLowerBoundY(relativeScrollY);
|
|
262
|
+
const targetY = relativeScrollY + usableHeight;
|
|
263
|
+
end = findLowerBoundY(targetY);
|
|
264
|
+
if (end < itemsLength && queryY(end) < targetY) {
|
|
265
|
+
end++;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
} else {
|
|
269
|
+
if (fixedSize !== null) {
|
|
270
|
+
start = Math.floor(relativeScrollX / (fixedSize + columnGap));
|
|
271
|
+
end = Math.ceil((relativeScrollX + usableWidth) / (fixedSize + columnGap));
|
|
272
|
+
} else {
|
|
273
|
+
start = findLowerBoundX(relativeScrollX);
|
|
274
|
+
const targetX = relativeScrollX + usableWidth;
|
|
275
|
+
end = findLowerBoundX(targetX);
|
|
276
|
+
if (end < itemsLength && queryX(end) < targetX) {
|
|
277
|
+
end++;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
start: Math.max(0, start - bufferBefore),
|
|
284
|
+
end: Math.min(itemsLength, end + bufferAfter),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Calculates the range of columns to render for bidirectional scroll.
|
|
290
|
+
*
|
|
291
|
+
* @param params - The parameters for calculation.
|
|
292
|
+
* @returns The start and end indices and paddings for columns.
|
|
293
|
+
* @see ColumnRangeParams
|
|
294
|
+
* @see ColumnRange
|
|
295
|
+
*/
|
|
296
|
+
export function calculateColumnRange(params: ColumnRangeParams) {
|
|
297
|
+
const {
|
|
298
|
+
columnCount,
|
|
299
|
+
relativeScrollX,
|
|
300
|
+
usableWidth,
|
|
301
|
+
colBuffer,
|
|
302
|
+
fixedWidth,
|
|
303
|
+
columnGap,
|
|
304
|
+
findLowerBound,
|
|
305
|
+
query,
|
|
306
|
+
totalColsQuery,
|
|
307
|
+
} = params;
|
|
308
|
+
|
|
309
|
+
if (!columnCount) {
|
|
310
|
+
return { start: 0, end: 0, padStart: 0, padEnd: 0 };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
let start = 0;
|
|
314
|
+
let end = columnCount;
|
|
315
|
+
|
|
316
|
+
if (fixedWidth !== null) {
|
|
317
|
+
start = Math.floor(relativeScrollX / (fixedWidth + columnGap));
|
|
318
|
+
end = Math.ceil((relativeScrollX + usableWidth) / (fixedWidth + columnGap));
|
|
319
|
+
} else {
|
|
320
|
+
start = findLowerBound(relativeScrollX);
|
|
321
|
+
let currentX = query(start);
|
|
322
|
+
let i = start;
|
|
323
|
+
while (i < columnCount && currentX < relativeScrollX + usableWidth) {
|
|
324
|
+
currentX = query(++i);
|
|
325
|
+
}
|
|
326
|
+
end = i;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Add buffer of columns
|
|
330
|
+
const safeStart = Math.max(0, start - colBuffer);
|
|
331
|
+
const safeEnd = Math.min(columnCount, end + colBuffer);
|
|
332
|
+
|
|
333
|
+
const padStart = fixedWidth !== null ? safeStart * (fixedWidth + columnGap) : query(safeStart);
|
|
334
|
+
const totalWidth = fixedWidth !== null ? columnCount * (fixedWidth + columnGap) - columnGap : Math.max(0, totalColsQuery() - columnGap);
|
|
335
|
+
|
|
336
|
+
const renderedEnd = fixedWidth !== null
|
|
337
|
+
? (safeEnd * (fixedWidth + columnGap) - (safeEnd >= columnCount ? columnGap : 0))
|
|
338
|
+
: (query(safeEnd) - (safeEnd >= columnCount ? columnGap : 0));
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
start: safeStart,
|
|
342
|
+
end: safeEnd,
|
|
343
|
+
padStart,
|
|
344
|
+
padEnd: Math.max(0, totalWidth - renderedEnd),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Calculates the sticky state and offset for a single item.
|
|
350
|
+
*
|
|
351
|
+
* @param params - The parameters for calculation.
|
|
352
|
+
* @returns Sticky state and offset.
|
|
353
|
+
* @see StickyParams
|
|
354
|
+
*/
|
|
355
|
+
export function calculateStickyItem(params: StickyParams) {
|
|
356
|
+
const {
|
|
357
|
+
index,
|
|
358
|
+
isSticky,
|
|
359
|
+
direction,
|
|
360
|
+
relativeScrollX,
|
|
361
|
+
relativeScrollY,
|
|
362
|
+
originalX,
|
|
363
|
+
originalY,
|
|
364
|
+
width,
|
|
365
|
+
height,
|
|
366
|
+
stickyIndices,
|
|
367
|
+
fixedSize,
|
|
368
|
+
fixedWidth,
|
|
369
|
+
gap,
|
|
370
|
+
columnGap,
|
|
371
|
+
getItemQueryY,
|
|
372
|
+
getItemQueryX,
|
|
373
|
+
} = params;
|
|
374
|
+
|
|
375
|
+
let isStickyActive = false;
|
|
376
|
+
const stickyOffset = { x: 0, y: 0 };
|
|
377
|
+
|
|
378
|
+
if (!isSticky) {
|
|
379
|
+
return { isStickyActive, stickyOffset };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (direction === 'vertical' || direction === 'both') {
|
|
383
|
+
if (relativeScrollY > originalY) {
|
|
384
|
+
// Check if next sticky item pushes this one
|
|
385
|
+
let nextStickyIdx: number | undefined;
|
|
386
|
+
let low = 0;
|
|
387
|
+
let high = stickyIndices.length - 1;
|
|
388
|
+
while (low <= high) {
|
|
389
|
+
const mid = (low + high) >>> 1;
|
|
390
|
+
if (stickyIndices[ mid ]! > index) {
|
|
391
|
+
nextStickyIdx = stickyIndices[ mid ];
|
|
392
|
+
high = mid - 1;
|
|
393
|
+
} else {
|
|
394
|
+
low = mid + 1;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (nextStickyIdx !== undefined) {
|
|
399
|
+
const nextStickyY = fixedSize !== null ? nextStickyIdx * (fixedSize + gap) : getItemQueryY(nextStickyIdx);
|
|
400
|
+
if (relativeScrollY >= nextStickyY) {
|
|
401
|
+
isStickyActive = false;
|
|
402
|
+
} else {
|
|
403
|
+
isStickyActive = true;
|
|
404
|
+
stickyOffset.y = Math.max(0, Math.min(height, nextStickyY - relativeScrollY)) - height;
|
|
405
|
+
}
|
|
406
|
+
} else {
|
|
407
|
+
isStickyActive = true;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (direction === 'horizontal' || (direction === 'both' && !isStickyActive)) {
|
|
413
|
+
if (relativeScrollX > originalX) {
|
|
414
|
+
let nextStickyIdx: number | undefined;
|
|
415
|
+
let low = 0;
|
|
416
|
+
let high = stickyIndices.length - 1;
|
|
417
|
+
while (low <= high) {
|
|
418
|
+
const mid = (low + high) >>> 1;
|
|
419
|
+
if (stickyIndices[ mid ]! > index) {
|
|
420
|
+
nextStickyIdx = stickyIndices[ mid ];
|
|
421
|
+
high = mid - 1;
|
|
422
|
+
} else {
|
|
423
|
+
low = mid + 1;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (nextStickyIdx !== undefined) {
|
|
428
|
+
const nextStickyX = direction === 'horizontal'
|
|
429
|
+
? (fixedSize !== null ? nextStickyIdx * (fixedSize + columnGap) : getItemQueryX(nextStickyIdx))
|
|
430
|
+
: (fixedWidth !== null ? nextStickyIdx * (fixedWidth + columnGap) : getItemQueryX(nextStickyIdx));
|
|
431
|
+
|
|
432
|
+
if (relativeScrollX >= nextStickyX) {
|
|
433
|
+
isStickyActive = false;
|
|
434
|
+
} else {
|
|
435
|
+
isStickyActive = true;
|
|
436
|
+
stickyOffset.x = Math.max(0, Math.min(width, nextStickyX - relativeScrollX)) - width;
|
|
437
|
+
}
|
|
438
|
+
} else {
|
|
439
|
+
isStickyActive = true;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return { isStickyActive, stickyOffset };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Calculates the position and size of a single item.
|
|
449
|
+
*
|
|
450
|
+
* @param params - The parameters for calculation.
|
|
451
|
+
* @returns Item position and size.
|
|
452
|
+
* @see ItemPositionParams
|
|
453
|
+
*/
|
|
454
|
+
export function calculateItemPosition(params: ItemPositionParams) {
|
|
455
|
+
const {
|
|
456
|
+
index,
|
|
457
|
+
direction,
|
|
458
|
+
fixedSize,
|
|
459
|
+
gap,
|
|
460
|
+
columnGap,
|
|
461
|
+
usableWidth,
|
|
462
|
+
usableHeight,
|
|
463
|
+
totalWidth,
|
|
464
|
+
queryY,
|
|
465
|
+
queryX,
|
|
466
|
+
getSizeY,
|
|
467
|
+
getSizeX,
|
|
468
|
+
} = params;
|
|
469
|
+
|
|
470
|
+
let x = 0;
|
|
471
|
+
let y = 0;
|
|
472
|
+
let width = 0;
|
|
473
|
+
let height = 0;
|
|
474
|
+
|
|
475
|
+
if (direction === 'horizontal') {
|
|
476
|
+
x = fixedSize !== null ? index * (fixedSize + columnGap) : queryX(index);
|
|
477
|
+
width = fixedSize !== null ? fixedSize : getSizeX(index) - columnGap;
|
|
478
|
+
height = usableHeight;
|
|
479
|
+
} else {
|
|
480
|
+
// vertical or both
|
|
481
|
+
y = (direction === 'vertical' || direction === 'both') && fixedSize !== null ? index * (fixedSize + gap) : queryY(index);
|
|
482
|
+
height = fixedSize !== null ? fixedSize : getSizeY(index) - gap;
|
|
483
|
+
width = direction === 'both' ? totalWidth : usableWidth;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return { height, width, x, y };
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Calculates the style object for a rendered item.
|
|
491
|
+
*
|
|
492
|
+
* @param params - The parameters for calculation.
|
|
493
|
+
* @returns Style object.
|
|
494
|
+
* @see ItemStyleParams
|
|
495
|
+
*/
|
|
496
|
+
export function calculateItemStyle<T = unknown>(params: ItemStyleParams<T>) {
|
|
497
|
+
const {
|
|
498
|
+
item,
|
|
499
|
+
direction,
|
|
500
|
+
itemSize,
|
|
501
|
+
containerTag,
|
|
502
|
+
paddingStartX,
|
|
503
|
+
paddingStartY,
|
|
504
|
+
isHydrated,
|
|
505
|
+
} = params;
|
|
506
|
+
|
|
507
|
+
const isVertical = direction === 'vertical';
|
|
508
|
+
const isHorizontal = direction === 'horizontal';
|
|
509
|
+
const isBoth = direction === 'both';
|
|
510
|
+
const isDynamic = itemSize === undefined || itemSize === null || itemSize === 0;
|
|
511
|
+
|
|
512
|
+
const style: Record<string, string | number | undefined> = {
|
|
513
|
+
blockSize: isHorizontal ? '100%' : (!isDynamic ? `${ item.size.height }px` : 'auto'),
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
if (isVertical && containerTag === 'table') {
|
|
517
|
+
style.minInlineSize = '100%';
|
|
518
|
+
} else {
|
|
519
|
+
style.inlineSize = isVertical ? '100%' : (!isDynamic ? `${ item.size.width }px` : 'auto');
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (isDynamic) {
|
|
523
|
+
if (!isVertical) {
|
|
524
|
+
style.minInlineSize = '1px';
|
|
525
|
+
}
|
|
526
|
+
if (!isHorizontal) {
|
|
527
|
+
style.minBlockSize = '1px';
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (isHydrated) {
|
|
532
|
+
if (item.isStickyActive) {
|
|
533
|
+
if (isVertical || isBoth) {
|
|
534
|
+
style.insetBlockStart = `${ paddingStartY }px`;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (isHorizontal || isBoth) {
|
|
538
|
+
style.insetInlineStart = `${ paddingStartX }px`;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
style.transform = `translate(${ item.stickyOffset.x }px, ${ item.stickyOffset.y }px)`;
|
|
542
|
+
} else {
|
|
543
|
+
style.transform = `translate(${ item.offset.x }px, ${ item.offset.y }px)`;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return style;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Calculates the total width and height of the virtualized content.
|
|
552
|
+
*
|
|
553
|
+
* @param params - The parameters for calculation.
|
|
554
|
+
* @returns Total width and height.
|
|
555
|
+
* @see TotalSizeParams
|
|
556
|
+
*/
|
|
557
|
+
export function calculateTotalSize(params: TotalSizeParams) {
|
|
558
|
+
const {
|
|
559
|
+
direction,
|
|
560
|
+
itemsLength,
|
|
561
|
+
columnCount,
|
|
562
|
+
fixedSize,
|
|
563
|
+
fixedWidth,
|
|
564
|
+
gap,
|
|
565
|
+
columnGap,
|
|
566
|
+
usableWidth,
|
|
567
|
+
usableHeight,
|
|
568
|
+
queryY,
|
|
569
|
+
queryX,
|
|
570
|
+
queryColumn,
|
|
571
|
+
} = params;
|
|
572
|
+
|
|
573
|
+
let width = 0;
|
|
574
|
+
let height = 0;
|
|
575
|
+
|
|
576
|
+
if (direction === 'both') {
|
|
577
|
+
if (columnCount > 0) {
|
|
578
|
+
width = fixedWidth !== null ? columnCount * (fixedWidth + columnGap) - columnGap : Math.max(0, queryColumn(columnCount) - columnGap);
|
|
579
|
+
}
|
|
580
|
+
if (fixedSize !== null) {
|
|
581
|
+
height = Math.max(0, itemsLength * (fixedSize + gap) - (itemsLength > 0 ? gap : 0));
|
|
582
|
+
} else {
|
|
583
|
+
height = Math.max(0, queryY(itemsLength) - (itemsLength > 0 ? gap : 0));
|
|
584
|
+
}
|
|
585
|
+
width = Math.max(width, usableWidth);
|
|
586
|
+
height = Math.max(height, usableHeight);
|
|
587
|
+
} else if (direction === 'horizontal') {
|
|
588
|
+
if (fixedSize !== null) {
|
|
589
|
+
width = Math.max(0, itemsLength * (fixedSize + columnGap) - (itemsLength > 0 ? columnGap : 0));
|
|
590
|
+
} else {
|
|
591
|
+
width = Math.max(0, queryX(itemsLength) - (itemsLength > 0 ? columnGap : 0));
|
|
592
|
+
}
|
|
593
|
+
height = usableHeight;
|
|
594
|
+
} else {
|
|
595
|
+
// vertical
|
|
596
|
+
width = usableWidth;
|
|
597
|
+
if (fixedSize !== null) {
|
|
598
|
+
height = Math.max(0, itemsLength * (fixedSize + gap) - (itemsLength > 0 ? gap : 0));
|
|
599
|
+
} else {
|
|
600
|
+
height = Math.max(0, queryY(itemsLength) - (itemsLength > 0 ? gap : 0));
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return { width, height };
|
|
605
|
+
}
|