@pdanpdan/virtual-scroll 0.4.0 → 0.6.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 +172 -324
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +836 -376
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1334 -741
- package/dist/index.mjs.map +1 -1
- package/dist/virtual-scroll.css +1 -1
- package/package.json +8 -2
- package/src/components/VirtualScroll.test.ts +1921 -325
- package/src/components/VirtualScroll.vue +829 -386
- package/src/components/VirtualScrollbar.test.ts +174 -0
- package/src/components/VirtualScrollbar.vue +102 -0
- package/src/composables/useVirtualScroll.test.ts +1506 -228
- package/src/composables/useVirtualScroll.ts +869 -517
- package/src/composables/useVirtualScrollbar.test.ts +526 -0
- package/src/composables/useVirtualScrollbar.ts +244 -0
- package/src/index.ts +9 -0
- package/src/types.ts +353 -110
- package/src/utils/fenwick-tree.test.ts +39 -39
- package/src/utils/scroll.test.ts +181 -101
- package/src/utils/scroll.ts +43 -5
- package/src/utils/virtual-scroll-logic.test.ts +673 -323
- package/src/utils/virtual-scroll-logic.ts +759 -430
|
@@ -12,43 +12,461 @@ import type {
|
|
|
12
12
|
TotalSizeParams,
|
|
13
13
|
} from '../types';
|
|
14
14
|
|
|
15
|
-
import { isScrollToIndexOptions } from './scroll';
|
|
15
|
+
import { BROWSER_MAX_SIZE, isScrollToIndexOptions } from './scroll';
|
|
16
|
+
|
|
17
|
+
// --- Internal Helper Types ---
|
|
18
|
+
|
|
19
|
+
interface GenericRangeParams {
|
|
20
|
+
scrollPos: number;
|
|
21
|
+
containerSize: number;
|
|
22
|
+
count: number;
|
|
23
|
+
bufferBefore: number;
|
|
24
|
+
bufferAfter: number;
|
|
25
|
+
gap: number;
|
|
26
|
+
fixedSize: number | null;
|
|
27
|
+
findLowerBound: (offset: number) => number;
|
|
28
|
+
query: (index: number) => number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface AxisAlignmentParams {
|
|
32
|
+
align: ScrollAlignment;
|
|
33
|
+
targetPos: number;
|
|
34
|
+
itemSize: number;
|
|
35
|
+
scrollPos: number;
|
|
36
|
+
viewSize: number;
|
|
37
|
+
stickyOffsetStart: number;
|
|
38
|
+
stickyOffsetEnd: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// --- Internal Helpers ---
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Generic range calculation for a single axis (row or column).
|
|
45
|
+
*
|
|
46
|
+
* @param params - Range parameters.
|
|
47
|
+
* @param params.scrollPos - Virtual scroll position.
|
|
48
|
+
* @param params.containerSize - Usable viewport size.
|
|
49
|
+
* @param params.count - Total item count.
|
|
50
|
+
* @param params.bufferBefore - Buffer items before.
|
|
51
|
+
* @param params.bufferAfter - Buffer items after.
|
|
52
|
+
* @param params.gap - Item gap.
|
|
53
|
+
* @param params.fixedSize - Fixed item size.
|
|
54
|
+
* @param params.findLowerBound - Binary search for index.
|
|
55
|
+
* @param params.query - Prefix sum for index.
|
|
56
|
+
* @returns Start and end indices.
|
|
57
|
+
*/
|
|
58
|
+
function calculateGenericRange({
|
|
59
|
+
scrollPos,
|
|
60
|
+
containerSize,
|
|
61
|
+
count,
|
|
62
|
+
bufferBefore,
|
|
63
|
+
bufferAfter,
|
|
64
|
+
gap,
|
|
65
|
+
fixedSize,
|
|
66
|
+
findLowerBound,
|
|
67
|
+
query,
|
|
68
|
+
}: GenericRangeParams) {
|
|
69
|
+
let start = 0;
|
|
70
|
+
let end = count;
|
|
71
|
+
const endOffset = scrollPos + containerSize;
|
|
72
|
+
|
|
73
|
+
if (fixedSize !== null) {
|
|
74
|
+
const step = fixedSize + gap;
|
|
75
|
+
start = Math.floor(scrollPos / step);
|
|
76
|
+
end = Math.ceil(endOffset / step);
|
|
77
|
+
} else {
|
|
78
|
+
start = findLowerBound(scrollPos);
|
|
79
|
+
end = findLowerBound(endOffset);
|
|
80
|
+
if (end < count && query(end) < endOffset) {
|
|
81
|
+
end++;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
start: Math.max(0, start - bufferBefore),
|
|
87
|
+
end: Math.min(count, end + bufferAfter),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Binary search for the next sticky index after the current index.
|
|
93
|
+
*
|
|
94
|
+
* @param stickyIndices - Sorted array of sticky indices.
|
|
95
|
+
* @param index - Current index.
|
|
96
|
+
* @returns Next sticky index or undefined.
|
|
97
|
+
*/
|
|
98
|
+
function findNextStickyIndex(stickyIndices: number[], index: number): number | undefined {
|
|
99
|
+
let low = 0;
|
|
100
|
+
let high = stickyIndices.length - 1;
|
|
101
|
+
let nextStickyIdx: number | undefined;
|
|
102
|
+
|
|
103
|
+
while (low <= high) {
|
|
104
|
+
const mid = (low + high) >>> 1;
|
|
105
|
+
if (stickyIndices[ mid ]! > index) {
|
|
106
|
+
nextStickyIdx = stickyIndices[ mid ];
|
|
107
|
+
high = mid - 1;
|
|
108
|
+
} else {
|
|
109
|
+
low = mid + 1;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return nextStickyIdx;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Binary search for the previous sticky index before the current index.
|
|
117
|
+
*
|
|
118
|
+
* @param stickyIndices - Sorted array of sticky indices.
|
|
119
|
+
* @param index - Current index.
|
|
120
|
+
* @returns Previous sticky index or undefined.
|
|
121
|
+
*/
|
|
122
|
+
export function findPrevStickyIndex(stickyIndices: number[], index: number): number | undefined {
|
|
123
|
+
let low = 0;
|
|
124
|
+
let high = stickyIndices.length - 1;
|
|
125
|
+
let prevStickyIdx: number | undefined;
|
|
126
|
+
|
|
127
|
+
while (low <= high) {
|
|
128
|
+
const mid = (low + high) >>> 1;
|
|
129
|
+
if (stickyIndices[ mid ]! < index) {
|
|
130
|
+
prevStickyIdx = stickyIndices[ mid ];
|
|
131
|
+
low = mid + 1;
|
|
132
|
+
} else {
|
|
133
|
+
high = mid - 1;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return prevStickyIdx;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Generic alignment calculation for a single axis.
|
|
141
|
+
*
|
|
142
|
+
* @param params - Alignment parameters.
|
|
143
|
+
* @param params.align - Desired alignment.
|
|
144
|
+
* @param params.targetPos - Virtual item position.
|
|
145
|
+
* @param params.itemSize - Virtual item size.
|
|
146
|
+
* @param params.scrollPos - Virtual scroll position.
|
|
147
|
+
* @param params.viewSize - Full viewport size.
|
|
148
|
+
* @param params.stickyOffsetStart - Dynamic sticky offset at start.
|
|
149
|
+
* @param params.stickyOffsetEnd - Sticky offset at end.
|
|
150
|
+
* @returns Target scroll position and effective alignment.
|
|
151
|
+
*/
|
|
152
|
+
function calculateAxisAlignment({
|
|
153
|
+
align,
|
|
154
|
+
targetPos,
|
|
155
|
+
itemSize,
|
|
156
|
+
scrollPos,
|
|
157
|
+
viewSize,
|
|
158
|
+
stickyOffsetStart,
|
|
159
|
+
stickyOffsetEnd,
|
|
160
|
+
}: AxisAlignmentParams) {
|
|
161
|
+
const targetStart = targetPos - stickyOffsetStart;
|
|
162
|
+
const targetEnd = targetPos - (viewSize - stickyOffsetEnd - itemSize);
|
|
163
|
+
|
|
164
|
+
if (align === 'start') {
|
|
165
|
+
return { target: targetStart, effectiveAlign: 'start' as const };
|
|
166
|
+
}
|
|
167
|
+
if (align === 'center') {
|
|
168
|
+
return {
|
|
169
|
+
target: targetPos - stickyOffsetStart - (viewSize - stickyOffsetStart - stickyOffsetEnd - itemSize) / 2,
|
|
170
|
+
effectiveAlign: 'center' as const,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
if (align === 'end') {
|
|
174
|
+
return { target: targetEnd, effectiveAlign: 'end' as const };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (isItemVisible(targetPos, itemSize, scrollPos, viewSize, stickyOffsetStart, stickyOffsetEnd)) {
|
|
178
|
+
return { target: scrollPos, effectiveAlign: 'auto' as const };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const usableSize = viewSize - stickyOffsetStart - stickyOffsetEnd;
|
|
182
|
+
|
|
183
|
+
if (itemSize <= usableSize) {
|
|
184
|
+
return targetPos < scrollPos + stickyOffsetStart
|
|
185
|
+
? {
|
|
186
|
+
target: targetStart,
|
|
187
|
+
effectiveAlign: 'start' as const,
|
|
188
|
+
}
|
|
189
|
+
: {
|
|
190
|
+
target: targetEnd,
|
|
191
|
+
effectiveAlign: 'end' as const,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return Math.abs(targetStart - scrollPos) < Math.abs(targetEnd - scrollPos)
|
|
196
|
+
? {
|
|
197
|
+
target: targetStart,
|
|
198
|
+
effectiveAlign: 'start' as const,
|
|
199
|
+
}
|
|
200
|
+
: {
|
|
201
|
+
target: targetEnd,
|
|
202
|
+
effectiveAlign: 'end' as const,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Helper to calculate total size for a single axis.
|
|
208
|
+
*
|
|
209
|
+
* @param count - Item count.
|
|
210
|
+
* @param fixedSize - Fixed size if any.
|
|
211
|
+
* @param gap - Gap size.
|
|
212
|
+
* @param query - Prefix sum resolver.
|
|
213
|
+
* @returns Total size.
|
|
214
|
+
*/
|
|
215
|
+
function calculateAxisSize(
|
|
216
|
+
count: number,
|
|
217
|
+
fixedSize: number | null,
|
|
218
|
+
gap: number,
|
|
219
|
+
query: (index: number) => number,
|
|
220
|
+
): number {
|
|
221
|
+
if (count <= 0) {
|
|
222
|
+
return 0;
|
|
223
|
+
}
|
|
224
|
+
if (fixedSize !== null) {
|
|
225
|
+
return Math.max(0, count * (fixedSize + gap) - gap);
|
|
226
|
+
}
|
|
227
|
+
return Math.max(0, query(count) - gap);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Helper to calculate target scroll position for a single axis.
|
|
232
|
+
*
|
|
233
|
+
* @param params - Axis target parameters.
|
|
234
|
+
* @param params.index - Row/column index.
|
|
235
|
+
* @param params.align - Desired alignment.
|
|
236
|
+
* @param params.viewSize - Full viewport size.
|
|
237
|
+
* @param params.scrollPos - Virtual scroll position.
|
|
238
|
+
* @param params.fixedSize - Fixed item size.
|
|
239
|
+
* @param params.gap - Item gap.
|
|
240
|
+
* @param params.query - Prefix sum resolver.
|
|
241
|
+
* @param params.getSize - Item size resolver.
|
|
242
|
+
* @param params.stickyIndices - Sticky indices.
|
|
243
|
+
* @param params.stickyStart - Sticky start element size.
|
|
244
|
+
* @param params.stickyEnd - Sticky end element size.
|
|
245
|
+
* @returns Target position, item size and effective alignment.
|
|
246
|
+
*/
|
|
247
|
+
function calculateAxisTarget({
|
|
248
|
+
index,
|
|
249
|
+
align,
|
|
250
|
+
viewSize,
|
|
251
|
+
scrollPos,
|
|
252
|
+
fixedSize,
|
|
253
|
+
gap,
|
|
254
|
+
query,
|
|
255
|
+
getSize,
|
|
256
|
+
stickyIndices,
|
|
257
|
+
stickyStart,
|
|
258
|
+
stickyEnd = 0,
|
|
259
|
+
}: {
|
|
260
|
+
index: number;
|
|
261
|
+
align: ScrollAlignment;
|
|
262
|
+
viewSize: number;
|
|
263
|
+
scrollPos: number;
|
|
264
|
+
fixedSize: number | null;
|
|
265
|
+
gap: number;
|
|
266
|
+
query: (idx: number) => number;
|
|
267
|
+
getSize: (idx: number) => number;
|
|
268
|
+
stickyIndices?: number[] | undefined;
|
|
269
|
+
stickyStart: number;
|
|
270
|
+
stickyEnd?: number;
|
|
271
|
+
}) {
|
|
272
|
+
let stickyOffsetStart = stickyStart;
|
|
273
|
+
if (stickyIndices && stickyIndices.length > 0) {
|
|
274
|
+
const activeStickyIdx = findPrevStickyIndex(stickyIndices, index);
|
|
275
|
+
if (activeStickyIdx !== undefined) {
|
|
276
|
+
stickyOffsetStart += calculateAxisSize(1, fixedSize, 0, () => getSize(activeStickyIdx));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const itemPos = (fixedSize !== null ? index * (fixedSize + gap) : query(index));
|
|
281
|
+
const itemSize = fixedSize !== null ? fixedSize : getSize(index) - gap;
|
|
282
|
+
|
|
283
|
+
const { target, effectiveAlign } = calculateAxisAlignment({
|
|
284
|
+
align,
|
|
285
|
+
targetPos: itemPos,
|
|
286
|
+
itemSize,
|
|
287
|
+
scrollPos,
|
|
288
|
+
viewSize,
|
|
289
|
+
stickyOffsetStart,
|
|
290
|
+
stickyOffsetEnd: stickyEnd,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
return { target, itemSize, effectiveAlign };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Helper to calculate sticky state for a single axis.
|
|
298
|
+
*
|
|
299
|
+
* @param scrollPos - Virtual scroll position.
|
|
300
|
+
* @param originalPos - Original virtual item position.
|
|
301
|
+
* @param size - Virtual item size.
|
|
302
|
+
* @param index - Item index.
|
|
303
|
+
* @param stickyIndices - All sticky indices.
|
|
304
|
+
* @param getNextStickyPos - Resolver for the next sticky item's position.
|
|
305
|
+
* @returns Sticky state for this axis.
|
|
306
|
+
*/
|
|
307
|
+
function calculateAxisSticky(
|
|
308
|
+
scrollPos: number,
|
|
309
|
+
originalPos: number,
|
|
310
|
+
size: number,
|
|
311
|
+
index: number,
|
|
312
|
+
stickyIndices: number[],
|
|
313
|
+
getNextStickyPos: (idx: number) => number,
|
|
314
|
+
) {
|
|
315
|
+
if (scrollPos <= originalPos) {
|
|
316
|
+
return { isActive: false, offset: 0 };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const nextStickyIdx = findNextStickyIndex(stickyIndices, index);
|
|
320
|
+
if (nextStickyIdx === undefined) {
|
|
321
|
+
return { isActive: true, offset: 0 };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const nextStickyPos = getNextStickyPos(nextStickyIdx);
|
|
325
|
+
if (scrollPos >= nextStickyPos) {
|
|
326
|
+
return { isActive: false, offset: 0 };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
isActive: true,
|
|
331
|
+
offset: Math.max(0, Math.min(size, nextStickyPos - scrollPos)) - size,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// --- Exported Functions ---
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Determines if an item is visible within the usable viewport.
|
|
339
|
+
*
|
|
340
|
+
* @param itemPos - Virtual start position of the item (VU).
|
|
341
|
+
* @param itemSize - Virtual size of the item (VU).
|
|
342
|
+
* @param scrollPos - Virtual scroll position (VU).
|
|
343
|
+
* @param viewSize - Full size of the viewport (VU).
|
|
344
|
+
* @param stickyOffsetStart - Dynamic offset from sticky items at start (VU).
|
|
345
|
+
* @param stickyOffsetEnd - Offset from sticky items at end (VU).
|
|
346
|
+
* @returns True if visible.
|
|
347
|
+
*/
|
|
348
|
+
export function isItemVisible(
|
|
349
|
+
itemPos: number,
|
|
350
|
+
itemSize: number,
|
|
351
|
+
scrollPos: number,
|
|
352
|
+
viewSize: number,
|
|
353
|
+
stickyOffsetStart: number = 0,
|
|
354
|
+
stickyOffsetEnd: number = 0,
|
|
355
|
+
): boolean {
|
|
356
|
+
const usableStart = scrollPos + stickyOffsetStart;
|
|
357
|
+
const usableEnd = scrollPos + viewSize - stickyOffsetEnd;
|
|
358
|
+
const usableSize = viewSize - stickyOffsetStart - stickyOffsetEnd;
|
|
359
|
+
|
|
360
|
+
if (itemSize <= usableSize) {
|
|
361
|
+
return itemPos >= usableStart - 0.5 && (itemPos + itemSize) <= usableEnd + 0.5;
|
|
362
|
+
}
|
|
363
|
+
return itemPos <= usableStart + 0.5 && (itemPos + itemSize) >= usableEnd - 0.5;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Maps a display scroll position to a virtual content position.
|
|
368
|
+
*
|
|
369
|
+
* @param displayPos - Display pixel position (DU).
|
|
370
|
+
* @param hostOffset - Offset of the host element in display pixels (DU).
|
|
371
|
+
* @param scale - Coordinate scaling factor (VU/DU).
|
|
372
|
+
* @returns Virtual content position (VU).
|
|
373
|
+
*/
|
|
374
|
+
export function displayToVirtual(displayPos: number, hostOffset: number, scale: number): number {
|
|
375
|
+
return (displayPos - hostOffset) * scale;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Maps a virtual content position to a display scroll position.
|
|
380
|
+
*
|
|
381
|
+
* @param virtualPos - Virtual content position (VU).
|
|
382
|
+
* @param hostOffset - Offset of the host element in display pixels (DU).
|
|
383
|
+
* @param scale - Coordinate scaling factor (VU/DU).
|
|
384
|
+
* @returns Display pixel position (DU).
|
|
385
|
+
*/
|
|
386
|
+
export function virtualToDisplay(virtualPos: number, hostOffset: number, scale: number): number {
|
|
387
|
+
return virtualPos / scale + hostOffset;
|
|
388
|
+
}
|
|
16
389
|
|
|
17
390
|
/**
|
|
18
391
|
* Calculates the target scroll position (relative to content) for a given row/column index and alignment.
|
|
19
392
|
*
|
|
20
|
-
* @param params -
|
|
21
|
-
* @
|
|
393
|
+
* @param params - Scroll target parameters.
|
|
394
|
+
* @param params.rowIndex - Row index to target.
|
|
395
|
+
* @param params.colIndex - Column index to target.
|
|
396
|
+
* @param params.options - Scroll options including alignment.
|
|
397
|
+
* @param params.direction - Current scroll direction.
|
|
398
|
+
* @param params.viewportWidth - Full viewport width (DU).
|
|
399
|
+
* @param params.viewportHeight - Full viewport height (DU).
|
|
400
|
+
* @param params.totalWidth - Total estimated width (VU).
|
|
401
|
+
* @param params.totalHeight - Total estimated height (VU).
|
|
402
|
+
* @param params.gap - Item gap (VU).
|
|
403
|
+
* @param params.columnGap - Column gap (VU).
|
|
404
|
+
* @param params.fixedSize - Fixed item size (VU).
|
|
405
|
+
* @param params.fixedWidth - Fixed column width (VU).
|
|
406
|
+
* @param params.relativeScrollX - Current relative X scroll (VU).
|
|
407
|
+
* @param params.relativeScrollY - Current relative Y scroll (VU).
|
|
408
|
+
* @param params.getItemSizeY - Resolver for item height (VU).
|
|
409
|
+
* @param params.getItemSizeX - Resolver for item width (VU).
|
|
410
|
+
* @param params.getItemQueryY - Prefix sum resolver for item height (VU).
|
|
411
|
+
* @param params.getItemQueryX - Prefix sum resolver for item width (VU).
|
|
412
|
+
* @param params.getColumnSize - Resolver for column size (VU).
|
|
413
|
+
* @param params.getColumnQuery - Prefix sum resolver for column width (VU).
|
|
414
|
+
* @param params.scaleX - Coordinate scaling factor for X axis.
|
|
415
|
+
* @param params.scaleY - Coordinate scaling factor for Y axis.
|
|
416
|
+
* @param params.hostOffsetX - Display pixels offset of items wrapper on X axis (DU).
|
|
417
|
+
* @param params.hostOffsetY - Display pixels offset of items wrapper on Y axis (DU).
|
|
418
|
+
* @param params.flowPaddingStartX - Display pixels padding at flow start on X axis (DU).
|
|
419
|
+
* @param params.flowPaddingStartY - Display pixels padding at flow start on Y axis (DU).
|
|
420
|
+
* @param params.paddingStartX - Display pixels padding at scroll start on X axis (DU).
|
|
421
|
+
* @param params.paddingStartY - Display pixels padding at scroll start on Y axis (DU).
|
|
422
|
+
* @param params.paddingEndX - Display pixels padding at scroll end on X axis (DU).
|
|
423
|
+
* @param params.paddingEndY - Display pixels padding at scroll end on Y axis (DU).
|
|
424
|
+
* @param params.stickyIndices - List of sticky indices.
|
|
425
|
+
* @param params.stickyStartX - Sticky start offset on X axis (DU).
|
|
426
|
+
* @param params.stickyStartY - Sticky start offset on Y axis (DU).
|
|
427
|
+
* @param params.stickyEndX - Sticky end offset on X axis (DU).
|
|
428
|
+
* @param params.stickyEndY - Sticky end offset on Y axis (DU).
|
|
429
|
+
* @returns The target X and Y positions (VU) and item dimensions (VU).
|
|
22
430
|
* @see ScrollTargetParams
|
|
23
431
|
* @see ScrollTargetResult
|
|
24
432
|
*/
|
|
25
|
-
export function calculateScrollTarget(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
433
|
+
export function calculateScrollTarget({
|
|
434
|
+
rowIndex,
|
|
435
|
+
colIndex,
|
|
436
|
+
options,
|
|
437
|
+
direction,
|
|
438
|
+
viewportWidth,
|
|
439
|
+
viewportHeight,
|
|
440
|
+
totalWidth,
|
|
441
|
+
totalHeight,
|
|
442
|
+
gap,
|
|
443
|
+
columnGap,
|
|
444
|
+
fixedSize,
|
|
445
|
+
fixedWidth,
|
|
446
|
+
relativeScrollX,
|
|
447
|
+
relativeScrollY,
|
|
448
|
+
getItemSizeY,
|
|
449
|
+
getItemSizeX,
|
|
450
|
+
getItemQueryY,
|
|
451
|
+
getItemQueryX,
|
|
452
|
+
getColumnSize,
|
|
453
|
+
getColumnQuery,
|
|
454
|
+
scaleX,
|
|
455
|
+
scaleY,
|
|
456
|
+
hostOffsetX,
|
|
457
|
+
hostOffsetY,
|
|
458
|
+
stickyIndices,
|
|
459
|
+
stickyStartX = 0,
|
|
460
|
+
stickyStartY = 0,
|
|
461
|
+
stickyEndX = 0,
|
|
462
|
+
stickyEndY = 0,
|
|
463
|
+
flowPaddingStartX = 0,
|
|
464
|
+
flowPaddingStartY = 0,
|
|
465
|
+
paddingStartX = 0,
|
|
466
|
+
paddingStartY = 0,
|
|
467
|
+
paddingEndX = 0,
|
|
468
|
+
paddingEndY = 0,
|
|
469
|
+
}: ScrollTargetParams): ScrollTargetResult {
|
|
52
470
|
let align: ScrollAlignment | ScrollAlignmentOptions | ScrollToIndexOptions | undefined;
|
|
53
471
|
|
|
54
472
|
if (isScrollToIndexOptions(options)) {
|
|
@@ -57,167 +475,74 @@ export function calculateScrollTarget(params: ScrollTargetParams): ScrollTargetR
|
|
|
57
475
|
align = options as ScrollAlignment | ScrollAlignmentOptions;
|
|
58
476
|
}
|
|
59
477
|
|
|
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';
|
|
478
|
+
const alignX = (align && typeof align === 'object' ? align.x : align) || 'auto';
|
|
479
|
+
const alignY = (align && typeof align === 'object' ? align.y : align) || 'auto';
|
|
65
480
|
|
|
66
481
|
let targetX = relativeScrollX;
|
|
67
482
|
let targetY = relativeScrollY;
|
|
68
483
|
let itemWidth = 0;
|
|
69
484
|
let itemHeight = 0;
|
|
70
|
-
let effectiveAlignX: ScrollAlignment =
|
|
71
|
-
let effectiveAlignY: ScrollAlignment =
|
|
485
|
+
let effectiveAlignX: ScrollAlignment = 'auto';
|
|
486
|
+
let effectiveAlignY: ScrollAlignment = 'auto';
|
|
72
487
|
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
}
|
|
488
|
+
// Clamp to valid range
|
|
489
|
+
const rWidth = scaleX === 1 ? totalWidth : BROWSER_MAX_SIZE;
|
|
490
|
+
const rHeight = scaleY === 1 ? totalHeight : BROWSER_MAX_SIZE;
|
|
89
491
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
}
|
|
492
|
+
const maxDisplayX = Math.max(0, hostOffsetX + rWidth - viewportWidth);
|
|
493
|
+
const maxDisplayY = Math.max(0, hostOffsetY + rHeight - viewportHeight);
|
|
94
494
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
itemHeight = 0;
|
|
99
|
-
} else {
|
|
100
|
-
itemY = fixedSize !== null ? rowIndex * (fixedSize + gap) : getItemQueryY(rowIndex);
|
|
101
|
-
itemHeight = fixedSize !== null ? fixedSize : getItemSizeY(rowIndex) - gap;
|
|
102
|
-
}
|
|
495
|
+
// maxTarget should be in virtual internalScroll coordinates
|
|
496
|
+
const maxTargetX = (maxDisplayX - hostOffsetX) * scaleX;
|
|
497
|
+
const maxTargetY = (maxDisplayY - hostOffsetY) * scaleY;
|
|
103
498
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
}
|
|
499
|
+
const itemsStartVirtualX = flowPaddingStartX + stickyStartX + paddingStartX;
|
|
500
|
+
const itemsStartVirtualY = flowPaddingStartY + stickyStartY + paddingStartY;
|
|
501
|
+
|
|
502
|
+
// Y calculation
|
|
503
|
+
if (rowIndex != null) {
|
|
504
|
+
const res = calculateAxisTarget({
|
|
505
|
+
index: rowIndex,
|
|
506
|
+
align: alignY as ScrollAlignment,
|
|
507
|
+
viewSize: viewportHeight,
|
|
508
|
+
scrollPos: relativeScrollY,
|
|
509
|
+
fixedSize,
|
|
510
|
+
gap,
|
|
511
|
+
query: getItemQueryY,
|
|
512
|
+
getSize: getItemSizeY,
|
|
513
|
+
stickyIndices,
|
|
514
|
+
stickyStart: stickyStartY + paddingStartY,
|
|
515
|
+
stickyEnd: stickyEndY + paddingEndY,
|
|
516
|
+
});
|
|
517
|
+
targetY = res.target + itemsStartVirtualY;
|
|
518
|
+
itemHeight = res.itemSize;
|
|
519
|
+
effectiveAlignY = res.effectiveAlign;
|
|
141
520
|
}
|
|
142
521
|
|
|
143
522
|
// X calculation
|
|
144
523
|
if (colIndex != null) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
}
|
|
524
|
+
const isGrid = direction === 'both';
|
|
525
|
+
const isHorizontal = direction === 'horizontal';
|
|
526
|
+
const res = calculateAxisTarget({
|
|
527
|
+
index: colIndex,
|
|
528
|
+
align: alignX as ScrollAlignment,
|
|
529
|
+
viewSize: viewportWidth,
|
|
530
|
+
scrollPos: relativeScrollX,
|
|
531
|
+
fixedSize: isGrid ? fixedWidth : fixedSize,
|
|
532
|
+
gap: (isGrid || isHorizontal) ? columnGap : gap,
|
|
533
|
+
query: isGrid ? getColumnQuery : getItemQueryX,
|
|
534
|
+
getSize: isGrid ? getColumnSize : getItemSizeX,
|
|
535
|
+
stickyIndices,
|
|
536
|
+
stickyStart: stickyStartX + paddingStartX,
|
|
537
|
+
stickyEnd: stickyEndX + paddingEndX,
|
|
538
|
+
});
|
|
539
|
+
targetX = res.target + itemsStartVirtualX;
|
|
540
|
+
itemWidth = res.itemSize;
|
|
541
|
+
effectiveAlignX = res.effectiveAlign;
|
|
216
542
|
}
|
|
217
543
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
targetY = Math.max(0, Math.min(targetY, Math.max(0, totalHeight - usableHeight)));
|
|
544
|
+
targetX = Math.max(0, Math.min(targetX, maxTargetX));
|
|
545
|
+
targetY = Math.max(0, Math.min(targetY, maxTargetY));
|
|
221
546
|
|
|
222
547
|
return { targetX, targetY, itemWidth, itemHeight, effectiveAlignX, effectiveAlignY };
|
|
223
548
|
}
|
|
@@ -225,248 +550,240 @@ export function calculateScrollTarget(params: ScrollTargetParams): ScrollTargetR
|
|
|
225
550
|
/**
|
|
226
551
|
* Calculates the range of items to render based on scroll position and viewport size.
|
|
227
552
|
*
|
|
228
|
-
* @param params -
|
|
553
|
+
* @param params - Range parameters.
|
|
554
|
+
* @param params.direction - Scroll direction.
|
|
555
|
+
* @param params.relativeScrollX - Virtual horizontal position (VU).
|
|
556
|
+
* @param params.relativeScrollY - Virtual vertical position (VU).
|
|
557
|
+
* @param params.usableWidth - Usable viewport width (VU).
|
|
558
|
+
* @param params.usableHeight - Usable viewport height (VU).
|
|
559
|
+
* @param params.itemsLength - Total item count.
|
|
560
|
+
* @param params.bufferBefore - Buffer items before.
|
|
561
|
+
* @param params.bufferAfter - Buffer items after.
|
|
562
|
+
* @param params.gap - Item gap (VU).
|
|
563
|
+
* @param params.columnGap - Column gap (VU).
|
|
564
|
+
* @param params.fixedSize - Fixed item size (VU).
|
|
565
|
+
* @param params.findLowerBoundY - Resolver for vertical index.
|
|
566
|
+
* @param params.findLowerBoundX - Resolver for horizontal index.
|
|
567
|
+
* @param params.queryY - Resolver for vertical offset (VU).
|
|
568
|
+
* @param params.queryX - Resolver for horizontal offset (VU).
|
|
229
569
|
* @returns The start and end indices of the items to render.
|
|
230
570
|
* @see RangeParams
|
|
231
571
|
*/
|
|
232
|
-
export function calculateRange(
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
572
|
+
export function calculateRange({
|
|
573
|
+
direction,
|
|
574
|
+
relativeScrollX,
|
|
575
|
+
relativeScrollY,
|
|
576
|
+
usableWidth,
|
|
577
|
+
usableHeight,
|
|
578
|
+
itemsLength,
|
|
579
|
+
bufferBefore,
|
|
580
|
+
bufferAfter,
|
|
581
|
+
gap,
|
|
582
|
+
columnGap,
|
|
583
|
+
fixedSize,
|
|
584
|
+
findLowerBoundY,
|
|
585
|
+
findLowerBoundX,
|
|
586
|
+
queryY,
|
|
587
|
+
queryX,
|
|
588
|
+
}: RangeParams) {
|
|
589
|
+
const isVertical = direction === 'vertical' || direction === 'both';
|
|
590
|
+
|
|
591
|
+
return calculateGenericRange({
|
|
592
|
+
scrollPos: isVertical ? relativeScrollY : relativeScrollX,
|
|
593
|
+
containerSize: isVertical ? usableHeight : usableWidth,
|
|
594
|
+
count: itemsLength,
|
|
240
595
|
bufferBefore,
|
|
241
596
|
bufferAfter,
|
|
242
|
-
gap,
|
|
243
|
-
columnGap,
|
|
597
|
+
gap: isVertical ? gap : columnGap,
|
|
244
598
|
fixedSize,
|
|
245
|
-
findLowerBoundY,
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
};
|
|
599
|
+
findLowerBound: isVertical ? findLowerBoundY : findLowerBoundX,
|
|
600
|
+
query: isVertical ? queryY : queryX,
|
|
601
|
+
});
|
|
286
602
|
}
|
|
287
603
|
|
|
288
604
|
/**
|
|
289
605
|
* Calculates the range of columns to render for bidirectional scroll.
|
|
290
606
|
*
|
|
291
|
-
* @param params -
|
|
292
|
-
* @
|
|
607
|
+
* @param params - Column range parameters.
|
|
608
|
+
* @param params.columnCount - Total column count.
|
|
609
|
+
* @param params.relativeScrollX - Virtual horizontal position (VU).
|
|
610
|
+
* @param params.usableWidth - Usable viewport width (VU).
|
|
611
|
+
* @param params.colBuffer - Column buffer size.
|
|
612
|
+
* @param params.fixedWidth - Fixed column width (VU).
|
|
613
|
+
* @param params.columnGap - Column gap (VU).
|
|
614
|
+
* @param params.findLowerBound - Resolver for column index.
|
|
615
|
+
* @param params.query - Resolver for column offset (VU).
|
|
616
|
+
* @param params.totalColsQuery - Resolver for total width (VU).
|
|
617
|
+
* @returns The start and end indices and paddings for columns (VU).
|
|
293
618
|
* @see ColumnRangeParams
|
|
294
619
|
* @see ColumnRange
|
|
295
620
|
*/
|
|
296
|
-
export function calculateColumnRange(
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
} = params;
|
|
308
|
-
|
|
621
|
+
export function calculateColumnRange({
|
|
622
|
+
columnCount,
|
|
623
|
+
relativeScrollX,
|
|
624
|
+
usableWidth,
|
|
625
|
+
colBuffer,
|
|
626
|
+
fixedWidth,
|
|
627
|
+
columnGap,
|
|
628
|
+
findLowerBound,
|
|
629
|
+
query,
|
|
630
|
+
totalColsQuery,
|
|
631
|
+
}: ColumnRangeParams) {
|
|
309
632
|
if (!columnCount) {
|
|
310
633
|
return { start: 0, end: 0, padStart: 0, padEnd: 0 };
|
|
311
634
|
}
|
|
312
635
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
currentX = query(++i);
|
|
325
|
-
}
|
|
326
|
-
end = i;
|
|
327
|
-
}
|
|
636
|
+
const { start, end } = calculateGenericRange({
|
|
637
|
+
scrollPos: relativeScrollX,
|
|
638
|
+
containerSize: usableWidth,
|
|
639
|
+
count: columnCount,
|
|
640
|
+
bufferBefore: colBuffer,
|
|
641
|
+
bufferAfter: colBuffer,
|
|
642
|
+
gap: columnGap,
|
|
643
|
+
fixedSize: fixedWidth,
|
|
644
|
+
findLowerBound,
|
|
645
|
+
query,
|
|
646
|
+
});
|
|
328
647
|
|
|
329
|
-
|
|
330
|
-
const
|
|
331
|
-
const safeEnd = Math.min(columnCount, end + colBuffer);
|
|
648
|
+
const safeStart = start;
|
|
649
|
+
const safeEnd = end;
|
|
332
650
|
|
|
333
651
|
const padStart = fixedWidth !== null ? safeStart * (fixedWidth + columnGap) : query(safeStart);
|
|
334
652
|
const totalWidth = fixedWidth !== null ? columnCount * (fixedWidth + columnGap) - columnGap : Math.max(0, totalColsQuery() - columnGap);
|
|
335
653
|
|
|
336
|
-
const
|
|
337
|
-
? (safeEnd * (fixedWidth + columnGap) - (safeEnd
|
|
338
|
-
: (query(safeEnd) - (safeEnd
|
|
654
|
+
const contentEnd = fixedWidth !== null
|
|
655
|
+
? (safeEnd * (fixedWidth + columnGap) - (safeEnd > 0 ? columnGap : 0))
|
|
656
|
+
: (query(safeEnd) - (safeEnd > 0 ? columnGap : 0));
|
|
339
657
|
|
|
340
658
|
return {
|
|
341
659
|
start: safeStart,
|
|
342
660
|
end: safeEnd,
|
|
343
661
|
padStart,
|
|
344
|
-
padEnd: Math.max(0, totalWidth -
|
|
662
|
+
padEnd: Math.max(0, totalWidth - contentEnd),
|
|
345
663
|
};
|
|
346
664
|
}
|
|
347
665
|
|
|
348
666
|
/**
|
|
349
667
|
* Calculates the sticky state and offset for a single item.
|
|
350
668
|
*
|
|
351
|
-
* @param params -
|
|
352
|
-
* @
|
|
669
|
+
* @param params - Sticky item parameters.
|
|
670
|
+
* @param params.index - Item index.
|
|
671
|
+
* @param params.isSticky - If configured as sticky.
|
|
672
|
+
* @param params.direction - Scroll direction.
|
|
673
|
+
* @param params.relativeScrollX - Virtual horizontal position (VU).
|
|
674
|
+
* @param params.relativeScrollY - Virtual vertical position (VU).
|
|
675
|
+
* @param params.originalX - Virtual original X position (VU).
|
|
676
|
+
* @param params.originalY - Virtual original Y position (VU).
|
|
677
|
+
* @param params.width - Virtual item width (VU).
|
|
678
|
+
* @param params.height - Virtual item height (VU).
|
|
679
|
+
* @param params.stickyIndices - All sticky indices.
|
|
680
|
+
* @param params.fixedSize - Fixed item size (VU).
|
|
681
|
+
* @param params.gap - Item gap (VU).
|
|
682
|
+
* @param params.columnGap - Column gap (VU).
|
|
683
|
+
* @param params.getItemQueryY - Resolver for vertical offset (VU).
|
|
684
|
+
* @param params.getItemQueryX - Resolver for horizontal offset (VU).
|
|
685
|
+
* @returns Sticky state and offset (VU).
|
|
353
686
|
* @see StickyParams
|
|
354
687
|
*/
|
|
355
|
-
export function calculateStickyItem(
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
let isStickyActive = false;
|
|
688
|
+
export function calculateStickyItem({
|
|
689
|
+
index,
|
|
690
|
+
isSticky,
|
|
691
|
+
direction,
|
|
692
|
+
relativeScrollX,
|
|
693
|
+
relativeScrollY,
|
|
694
|
+
originalX,
|
|
695
|
+
originalY,
|
|
696
|
+
width,
|
|
697
|
+
height,
|
|
698
|
+
stickyIndices,
|
|
699
|
+
fixedSize,
|
|
700
|
+
gap,
|
|
701
|
+
columnGap,
|
|
702
|
+
getItemQueryY,
|
|
703
|
+
getItemQueryX,
|
|
704
|
+
}: StickyParams) {
|
|
705
|
+
let isStickyActiveX = false;
|
|
706
|
+
let isStickyActiveY = false;
|
|
376
707
|
const stickyOffset = { x: 0, y: 0 };
|
|
377
708
|
|
|
378
709
|
if (!isSticky) {
|
|
379
|
-
return { isStickyActive, stickyOffset };
|
|
710
|
+
return { isStickyActiveX, isStickyActiveY, isStickyActive: false, stickyOffset };
|
|
380
711
|
}
|
|
381
712
|
|
|
713
|
+
// Y Axis (Sticky Rows)
|
|
382
714
|
if (direction === 'vertical' || direction === 'both') {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
-
}
|
|
715
|
+
const res = calculateAxisSticky(
|
|
716
|
+
relativeScrollY,
|
|
717
|
+
originalY,
|
|
718
|
+
height,
|
|
719
|
+
index,
|
|
720
|
+
stickyIndices,
|
|
721
|
+
(nextIdx) => (fixedSize !== null ? nextIdx * (fixedSize + gap) : getItemQueryY(nextIdx)),
|
|
722
|
+
);
|
|
723
|
+
isStickyActiveY = res.isActive;
|
|
724
|
+
stickyOffset.y = res.offset;
|
|
410
725
|
}
|
|
411
726
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
-
}
|
|
727
|
+
// X Axis (Sticky Columns / Items)
|
|
728
|
+
if (direction === 'horizontal') {
|
|
729
|
+
const res = calculateAxisSticky(
|
|
730
|
+
relativeScrollX,
|
|
731
|
+
originalX,
|
|
732
|
+
width,
|
|
733
|
+
index,
|
|
734
|
+
stickyIndices,
|
|
735
|
+
(nextIdx) => (fixedSize !== null ? nextIdx * (fixedSize + columnGap) : getItemQueryX(nextIdx)),
|
|
736
|
+
);
|
|
737
|
+
|
|
738
|
+
if (res.isActive) {
|
|
739
|
+
isStickyActiveX = true;
|
|
740
|
+
stickyOffset.x = res.offset;
|
|
441
741
|
}
|
|
442
742
|
}
|
|
443
743
|
|
|
444
|
-
return {
|
|
744
|
+
return {
|
|
745
|
+
isStickyActiveX,
|
|
746
|
+
isStickyActiveY,
|
|
747
|
+
isStickyActive: isStickyActiveX || isStickyActiveY,
|
|
748
|
+
stickyOffset,
|
|
749
|
+
};
|
|
445
750
|
}
|
|
446
751
|
|
|
447
752
|
/**
|
|
448
753
|
* Calculates the position and size of a single item.
|
|
449
754
|
*
|
|
450
|
-
* @param params -
|
|
451
|
-
* @
|
|
755
|
+
* @param params - Item position parameters.
|
|
756
|
+
* @param params.index - Item index.
|
|
757
|
+
* @param params.direction - Scroll direction.
|
|
758
|
+
* @param params.fixedSize - Fixed item size (VU).
|
|
759
|
+
* @param params.gap - Item gap (VU).
|
|
760
|
+
* @param params.columnGap - Column gap (VU).
|
|
761
|
+
* @param params.usableWidth - Usable viewport width (VU).
|
|
762
|
+
* @param params.usableHeight - Usable viewport height (VU).
|
|
763
|
+
* @param params.totalWidth - Total estimated width (VU).
|
|
764
|
+
* @param params.queryY - Resolver for vertical offset (VU).
|
|
765
|
+
* @param params.queryX - Resolver for horizontal offset (VU).
|
|
766
|
+
* @param params.getSizeY - Resolver for height (VU).
|
|
767
|
+
* @param params.getSizeX - Resolver for width (VU).
|
|
768
|
+
* @param params.columnRange - Current column range (for grid mode).
|
|
769
|
+
* @returns Item position and size (VU).
|
|
452
770
|
* @see ItemPositionParams
|
|
453
771
|
*/
|
|
454
|
-
export function calculateItemPosition(
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
772
|
+
export function calculateItemPosition({
|
|
773
|
+
index,
|
|
774
|
+
direction,
|
|
775
|
+
fixedSize,
|
|
776
|
+
gap,
|
|
777
|
+
columnGap,
|
|
778
|
+
usableWidth,
|
|
779
|
+
usableHeight,
|
|
780
|
+
totalWidth,
|
|
781
|
+
queryY,
|
|
782
|
+
queryX,
|
|
783
|
+
getSizeY,
|
|
784
|
+
getSizeX,
|
|
785
|
+
columnRange,
|
|
786
|
+
}: ItemPositionParams) {
|
|
470
787
|
let x = 0;
|
|
471
788
|
let y = 0;
|
|
472
789
|
let width = 0;
|
|
@@ -476,9 +793,13 @@ export function calculateItemPosition(params: ItemPositionParams) {
|
|
|
476
793
|
x = fixedSize !== null ? index * (fixedSize + columnGap) : queryX(index);
|
|
477
794
|
width = fixedSize !== null ? fixedSize : getSizeX(index) - columnGap;
|
|
478
795
|
height = usableHeight;
|
|
796
|
+
} else if (direction === 'both' && columnRange) {
|
|
797
|
+
y = fixedSize !== null ? index * (fixedSize + gap) : queryY(index);
|
|
798
|
+
height = fixedSize !== null ? fixedSize : getSizeY(index) - gap;
|
|
799
|
+
x = columnRange.padStart;
|
|
800
|
+
width = Math.max(0, totalWidth - columnRange.padStart - columnRange.padEnd);
|
|
479
801
|
} else {
|
|
480
|
-
|
|
481
|
-
y = (direction === 'vertical' || direction === 'both') && fixedSize !== null ? index * (fixedSize + gap) : queryY(index);
|
|
802
|
+
y = fixedSize !== null ? index * (fixedSize + gap) : queryY(index);
|
|
482
803
|
height = fixedSize !== null ? fixedSize : getSizeY(index) - gap;
|
|
483
804
|
width = direction === 'both' ? totalWidth : usableWidth;
|
|
484
805
|
}
|
|
@@ -489,21 +810,28 @@ export function calculateItemPosition(params: ItemPositionParams) {
|
|
|
489
810
|
/**
|
|
490
811
|
* Calculates the style object for a rendered item.
|
|
491
812
|
*
|
|
492
|
-
* @param params -
|
|
813
|
+
* @param params - Item style parameters.
|
|
814
|
+
* @param params.item - Rendered item state.
|
|
815
|
+
* @param params.direction - Scroll direction.
|
|
816
|
+
* @param params.itemSize - Virtual item size (VU).
|
|
817
|
+
* @param params.containerTag - Container HTML tag.
|
|
818
|
+
* @param params.paddingStartX - Horizontal virtual padding (DU).
|
|
819
|
+
* @param params.paddingStartY - Vertical virtual padding (DU).
|
|
820
|
+
* @param params.isHydrated - If mounted and hydrated.
|
|
821
|
+
* @param params.isRtl - If in RTL mode.
|
|
493
822
|
* @returns Style object.
|
|
494
823
|
* @see ItemStyleParams
|
|
495
824
|
*/
|
|
496
|
-
export function calculateItemStyle<T = unknown>(
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
825
|
+
export function calculateItemStyle<T = unknown>({
|
|
826
|
+
item,
|
|
827
|
+
direction,
|
|
828
|
+
itemSize,
|
|
829
|
+
containerTag,
|
|
830
|
+
paddingStartX,
|
|
831
|
+
paddingStartY,
|
|
832
|
+
isHydrated,
|
|
833
|
+
isRtl,
|
|
834
|
+
}: ItemStyleParams<T>) {
|
|
507
835
|
const isVertical = direction === 'vertical';
|
|
508
836
|
const isHorizontal = direction === 'horizontal';
|
|
509
837
|
const isBoth = direction === 'both';
|
|
@@ -529,18 +857,20 @@ export function calculateItemStyle<T = unknown>(params: ItemStyleParams<T>) {
|
|
|
529
857
|
}
|
|
530
858
|
|
|
531
859
|
if (isHydrated) {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
style.
|
|
860
|
+
const isStickingVertically = item.isStickyActiveY ?? (item.isStickyActive && (isVertical || isBoth));
|
|
861
|
+
const isStickingHorizontally = item.isStickyActiveX ?? (item.isStickyActive && isHorizontal);
|
|
862
|
+
|
|
863
|
+
const tx = isRtl
|
|
864
|
+
? -(isStickingHorizontally ? item.stickyOffset.x : item.offset.x)
|
|
865
|
+
: (isStickingHorizontally ? item.stickyOffset.x : item.offset.x);
|
|
866
|
+
const ty = isStickingVertically ? item.stickyOffset.y : item.offset.y;
|
|
867
|
+
|
|
868
|
+
if (item.isStickyActive || item.isStickyActiveX || item.isStickyActiveY) {
|
|
869
|
+
style.insetBlockStart = isStickingVertically ? `${ paddingStartY }px` : 'auto';
|
|
870
|
+
style.insetInlineStart = isStickingHorizontally ? `${ paddingStartX }px` : 'auto';
|
|
871
|
+
style.transform = `translate(${ tx }px, ${ ty }px)`;
|
|
542
872
|
} else {
|
|
543
|
-
style.transform = `translate(${
|
|
873
|
+
style.transform = `translate(${ tx }px, ${ item.offset.y }px)`;
|
|
544
874
|
}
|
|
545
875
|
}
|
|
546
876
|
|
|
@@ -550,56 +880,55 @@ export function calculateItemStyle<T = unknown>(params: ItemStyleParams<T>) {
|
|
|
550
880
|
/**
|
|
551
881
|
* Calculates the total width and height of the virtualized content.
|
|
552
882
|
*
|
|
553
|
-
* @param params -
|
|
554
|
-
* @
|
|
883
|
+
* @param params - Total size parameters.
|
|
884
|
+
* @param params.direction - Scroll direction.
|
|
885
|
+
* @param params.itemsLength - Total item count.
|
|
886
|
+
* @param params.columnCount - Column count.
|
|
887
|
+
* @param params.fixedSize - Fixed item size (VU).
|
|
888
|
+
* @param params.fixedWidth - Fixed column width (VU).
|
|
889
|
+
* @param params.gap - Item gap (VU).
|
|
890
|
+
* @param params.columnGap - Column gap (VU).
|
|
891
|
+
* @param params.usableWidth - Usable viewport width (VU).
|
|
892
|
+
* @param params.usableHeight - Usable viewport height (VU).
|
|
893
|
+
* @param params.queryY - Resolver for vertical offset (VU).
|
|
894
|
+
* @param params.queryX - Resolver for horizontal offset (VU).
|
|
895
|
+
* @param params.queryColumn - Resolver for column offset (VU).
|
|
896
|
+
* @returns Total width and height (VU).
|
|
555
897
|
* @see TotalSizeParams
|
|
556
898
|
*/
|
|
557
|
-
export function calculateTotalSize(
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
899
|
+
export function calculateTotalSize({
|
|
900
|
+
direction,
|
|
901
|
+
itemsLength,
|
|
902
|
+
columnCount,
|
|
903
|
+
fixedSize,
|
|
904
|
+
fixedWidth,
|
|
905
|
+
gap,
|
|
906
|
+
columnGap,
|
|
907
|
+
usableWidth,
|
|
908
|
+
usableHeight,
|
|
909
|
+
queryY,
|
|
910
|
+
queryX,
|
|
911
|
+
queryColumn,
|
|
912
|
+
}: TotalSizeParams) {
|
|
913
|
+
const isBoth = direction === 'both';
|
|
914
|
+
const isHorizontal = direction === 'horizontal';
|
|
572
915
|
|
|
573
916
|
let width = 0;
|
|
574
917
|
let height = 0;
|
|
575
918
|
|
|
576
|
-
if (
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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
|
-
}
|
|
919
|
+
if (isBoth) {
|
|
920
|
+
width = calculateAxisSize(columnCount, fixedWidth, columnGap, queryColumn);
|
|
921
|
+
height = calculateAxisSize(itemsLength, fixedSize, gap, queryY);
|
|
922
|
+
} else if (isHorizontal) {
|
|
923
|
+
width = calculateAxisSize(itemsLength, fixedSize, columnGap, queryX);
|
|
593
924
|
height = usableHeight;
|
|
594
925
|
} else {
|
|
595
|
-
// vertical
|
|
596
926
|
width = usableWidth;
|
|
597
|
-
|
|
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
|
-
}
|
|
927
|
+
height = calculateAxisSize(itemsLength, fixedSize, gap, queryY);
|
|
602
928
|
}
|
|
603
929
|
|
|
604
|
-
return {
|
|
930
|
+
return {
|
|
931
|
+
width: isBoth ? Math.max(width, usableWidth) : width,
|
|
932
|
+
height: isBoth ? Math.max(height, usableHeight) : height,
|
|
933
|
+
};
|
|
605
934
|
}
|