@pdanpdan/virtual-scroll 0.4.0 → 0.5.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 +246 -297
- package/dist/index.cjs +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +873 -257
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2209 -1109
- package/dist/index.mjs.map +1 -1
- package/dist/virtual-scroll.css +1 -2
- package/package.json +5 -1
- package/src/components/VirtualScroll.test.ts +1886 -326
- package/src/components/VirtualScroll.vue +813 -340
- 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 +789 -373
- package/src/composables/useVirtualScrollbar.test.ts +526 -0
- package/src/composables/useVirtualScrollbar.ts +239 -0
- package/src/index.ts +2 -0
- package/src/types.ts +333 -52
- package/src/utils/fenwick-tree.test.ts +39 -39
- package/src/utils/scroll.test.ts +133 -107
- package/src/utils/scroll.ts +12 -5
- package/src/utils/virtual-scroll-logic.test.ts +653 -320
- package/src/utils/virtual-scroll-logic.ts +685 -389
|
@@ -12,43 +12,422 @@ 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
|
+
// --- Exported Functions ---
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Determines if an item is visible within the usable viewport.
|
|
300
|
+
*
|
|
301
|
+
* @param itemPos - Virtual start position of the item (VU).
|
|
302
|
+
* @param itemSize - Virtual size of the item (VU).
|
|
303
|
+
* @param scrollPos - Virtual scroll position (VU).
|
|
304
|
+
* @param viewSize - Full size of the viewport (VU).
|
|
305
|
+
* @param stickyOffsetStart - Dynamic offset from sticky items at start (VU).
|
|
306
|
+
* @param stickyOffsetEnd - Offset from sticky items at end (VU).
|
|
307
|
+
* @returns True if visible.
|
|
308
|
+
*/
|
|
309
|
+
export function isItemVisible(
|
|
310
|
+
itemPos: number,
|
|
311
|
+
itemSize: number,
|
|
312
|
+
scrollPos: number,
|
|
313
|
+
viewSize: number,
|
|
314
|
+
stickyOffsetStart: number = 0,
|
|
315
|
+
stickyOffsetEnd: number = 0,
|
|
316
|
+
): boolean {
|
|
317
|
+
const usableStart = scrollPos + stickyOffsetStart;
|
|
318
|
+
const usableEnd = scrollPos + viewSize - stickyOffsetEnd;
|
|
319
|
+
const usableSize = viewSize - stickyOffsetStart - stickyOffsetEnd;
|
|
320
|
+
|
|
321
|
+
if (itemSize <= usableSize) {
|
|
322
|
+
return itemPos >= usableStart - 0.5 && (itemPos + itemSize) <= usableEnd + 0.5;
|
|
323
|
+
}
|
|
324
|
+
return itemPos <= usableStart + 0.5 && (itemPos + itemSize) >= usableEnd - 0.5;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Maps a display scroll position to a virtual content position.
|
|
329
|
+
*
|
|
330
|
+
* @param displayPos - Display pixel position (DU).
|
|
331
|
+
* @param hostOffset - Offset of the host element in display pixels (DU).
|
|
332
|
+
* @param scale - Coordinate scaling factor (VU/DU).
|
|
333
|
+
* @returns Virtual content position (VU).
|
|
334
|
+
*/
|
|
335
|
+
export function displayToVirtual(displayPos: number, hostOffset: number, scale: number): number {
|
|
336
|
+
return (displayPos - hostOffset) * scale;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Maps a virtual content position to a display scroll position.
|
|
341
|
+
*
|
|
342
|
+
* @param virtualPos - Virtual content position (VU).
|
|
343
|
+
* @param hostOffset - Offset of the host element in display pixels (DU).
|
|
344
|
+
* @param scale - Coordinate scaling factor (VU/DU).
|
|
345
|
+
* @returns Display pixel position (DU).
|
|
346
|
+
*/
|
|
347
|
+
export function virtualToDisplay(virtualPos: number, hostOffset: number, scale: number): number {
|
|
348
|
+
return virtualPos / scale + hostOffset;
|
|
349
|
+
}
|
|
16
350
|
|
|
17
351
|
/**
|
|
18
352
|
* Calculates the target scroll position (relative to content) for a given row/column index and alignment.
|
|
19
353
|
*
|
|
20
|
-
* @param params -
|
|
21
|
-
* @
|
|
354
|
+
* @param params - Scroll target parameters.
|
|
355
|
+
* @param params.rowIndex - Row index to target.
|
|
356
|
+
* @param params.colIndex - Column index to target.
|
|
357
|
+
* @param params.options - Scroll options including alignment.
|
|
358
|
+
* @param params.direction - Current scroll direction.
|
|
359
|
+
* @param params.viewportWidth - Full viewport width (DU).
|
|
360
|
+
* @param params.viewportHeight - Full viewport height (DU).
|
|
361
|
+
* @param params.totalWidth - Total estimated width (VU).
|
|
362
|
+
* @param params.totalHeight - Total estimated height (VU).
|
|
363
|
+
* @param params.gap - Item gap (VU).
|
|
364
|
+
* @param params.columnGap - Column gap (VU).
|
|
365
|
+
* @param params.fixedSize - Fixed item size (VU).
|
|
366
|
+
* @param params.fixedWidth - Fixed column width (VU).
|
|
367
|
+
* @param params.relativeScrollX - Current relative X scroll (VU).
|
|
368
|
+
* @param params.relativeScrollY - Current relative Y scroll (VU).
|
|
369
|
+
* @param params.getItemSizeY - Resolver for item height (VU).
|
|
370
|
+
* @param params.getItemSizeX - Resolver for item width (VU).
|
|
371
|
+
* @param params.getItemQueryY - Prefix sum resolver for item height (VU).
|
|
372
|
+
* @param params.getItemQueryX - Prefix sum resolver for item width (VU).
|
|
373
|
+
* @param params.getColumnSize - Resolver for column size (VU).
|
|
374
|
+
* @param params.getColumnQuery - Prefix sum resolver for column width (VU).
|
|
375
|
+
* @param params.scaleX - Coordinate scaling factor for X axis.
|
|
376
|
+
* @param params.scaleY - Coordinate scaling factor for Y axis.
|
|
377
|
+
* @param params.hostOffsetX - Display pixels offset of items wrapper on X axis (DU).
|
|
378
|
+
* @param params.hostOffsetY - Display pixels offset of items wrapper on Y axis (DU).
|
|
379
|
+
* @param params.flowPaddingStartX - Display pixels padding at flow start on X axis (DU).
|
|
380
|
+
* @param params.flowPaddingStartY - Display pixels padding at flow start on Y axis (DU).
|
|
381
|
+
* @param params.paddingStartX - Display pixels padding at scroll start on X axis (DU).
|
|
382
|
+
* @param params.paddingStartY - Display pixels padding at scroll start on Y axis (DU).
|
|
383
|
+
* @param params.paddingEndX - Display pixels padding at scroll end on X axis (DU).
|
|
384
|
+
* @param params.paddingEndY - Display pixels padding at scroll end on Y axis (DU).
|
|
385
|
+
* @param params.stickyIndices - List of sticky indices.
|
|
386
|
+
* @param params.stickyStartX - Sticky start offset on X axis (DU).
|
|
387
|
+
* @param params.stickyStartY - Sticky start offset on Y axis (DU).
|
|
388
|
+
* @param params.stickyEndX - Sticky end offset on X axis (DU).
|
|
389
|
+
* @param params.stickyEndY - Sticky end offset on Y axis (DU).
|
|
390
|
+
* @returns The target X and Y positions (VU) and item dimensions (VU).
|
|
22
391
|
* @see ScrollTargetParams
|
|
23
392
|
* @see ScrollTargetResult
|
|
24
393
|
*/
|
|
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
|
-
|
|
394
|
+
export function calculateScrollTarget({
|
|
395
|
+
rowIndex,
|
|
396
|
+
colIndex,
|
|
397
|
+
options,
|
|
398
|
+
direction,
|
|
399
|
+
viewportWidth,
|
|
400
|
+
viewportHeight,
|
|
401
|
+
totalWidth,
|
|
402
|
+
totalHeight,
|
|
403
|
+
gap,
|
|
404
|
+
columnGap,
|
|
405
|
+
fixedSize,
|
|
406
|
+
fixedWidth,
|
|
407
|
+
relativeScrollX,
|
|
408
|
+
relativeScrollY,
|
|
409
|
+
getItemSizeY,
|
|
410
|
+
getItemSizeX,
|
|
411
|
+
getItemQueryY,
|
|
412
|
+
getItemQueryX,
|
|
413
|
+
getColumnSize,
|
|
414
|
+
getColumnQuery,
|
|
415
|
+
scaleX,
|
|
416
|
+
scaleY,
|
|
417
|
+
hostOffsetX,
|
|
418
|
+
hostOffsetY,
|
|
419
|
+
stickyIndices,
|
|
420
|
+
stickyStartX = 0,
|
|
421
|
+
stickyStartY = 0,
|
|
422
|
+
stickyEndX = 0,
|
|
423
|
+
stickyEndY = 0,
|
|
424
|
+
flowPaddingStartX = 0,
|
|
425
|
+
flowPaddingStartY = 0,
|
|
426
|
+
paddingStartX = 0,
|
|
427
|
+
paddingStartY = 0,
|
|
428
|
+
paddingEndX = 0,
|
|
429
|
+
paddingEndY = 0,
|
|
430
|
+
}: ScrollTargetParams): ScrollTargetResult {
|
|
52
431
|
let align: ScrollAlignment | ScrollAlignmentOptions | ScrollToIndexOptions | undefined;
|
|
53
432
|
|
|
54
433
|
if (isScrollToIndexOptions(options)) {
|
|
@@ -57,167 +436,74 @@ export function calculateScrollTarget(params: ScrollTargetParams): ScrollTargetR
|
|
|
57
436
|
align = options as ScrollAlignment | ScrollAlignmentOptions;
|
|
58
437
|
}
|
|
59
438
|
|
|
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';
|
|
439
|
+
const alignX = (align && typeof align === 'object' ? align.x : align) || 'auto';
|
|
440
|
+
const alignY = (align && typeof align === 'object' ? align.y : align) || 'auto';
|
|
65
441
|
|
|
66
442
|
let targetX = relativeScrollX;
|
|
67
443
|
let targetY = relativeScrollY;
|
|
68
444
|
let itemWidth = 0;
|
|
69
445
|
let itemHeight = 0;
|
|
70
|
-
let effectiveAlignX: ScrollAlignment =
|
|
71
|
-
let effectiveAlignY: ScrollAlignment =
|
|
446
|
+
let effectiveAlignX: ScrollAlignment = 'auto';
|
|
447
|
+
let effectiveAlignY: ScrollAlignment = 'auto';
|
|
72
448
|
|
|
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
|
-
}
|
|
449
|
+
// Clamp to valid range
|
|
450
|
+
const rWidth = scaleX === 1 ? totalWidth : BROWSER_MAX_SIZE;
|
|
451
|
+
const rHeight = scaleY === 1 ? totalHeight : BROWSER_MAX_SIZE;
|
|
89
452
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
}
|
|
453
|
+
const maxDisplayX = Math.max(0, hostOffsetX + rWidth - viewportWidth);
|
|
454
|
+
const maxDisplayY = Math.max(0, hostOffsetY + rHeight - viewportHeight);
|
|
94
455
|
|
|
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
|
-
}
|
|
456
|
+
// maxTarget should be in virtual internalScroll coordinates
|
|
457
|
+
const maxTargetX = (maxDisplayX - hostOffsetX) * scaleX;
|
|
458
|
+
const maxTargetY = (maxDisplayY - hostOffsetY) * scaleY;
|
|
103
459
|
|
|
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
|
-
}
|
|
460
|
+
const itemsStartVirtualX = flowPaddingStartX + stickyStartX + paddingStartX;
|
|
461
|
+
const itemsStartVirtualY = flowPaddingStartY + stickyStartY + paddingStartY;
|
|
462
|
+
|
|
463
|
+
// Y calculation
|
|
464
|
+
if (rowIndex != null) {
|
|
465
|
+
const res = calculateAxisTarget({
|
|
466
|
+
index: rowIndex,
|
|
467
|
+
align: alignY as ScrollAlignment,
|
|
468
|
+
viewSize: viewportHeight,
|
|
469
|
+
scrollPos: relativeScrollY,
|
|
470
|
+
fixedSize,
|
|
471
|
+
gap,
|
|
472
|
+
query: getItemQueryY,
|
|
473
|
+
getSize: getItemSizeY,
|
|
474
|
+
stickyIndices,
|
|
475
|
+
stickyStart: stickyStartY + paddingStartY,
|
|
476
|
+
stickyEnd: stickyEndY + paddingEndY,
|
|
477
|
+
});
|
|
478
|
+
targetY = res.target + itemsStartVirtualY;
|
|
479
|
+
itemHeight = res.itemSize;
|
|
480
|
+
effectiveAlignY = res.effectiveAlign;
|
|
141
481
|
}
|
|
142
482
|
|
|
143
483
|
// X calculation
|
|
144
484
|
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
|
-
}
|
|
485
|
+
const isGrid = direction === 'both';
|
|
486
|
+
const isHorizontal = direction === 'horizontal';
|
|
487
|
+
const res = calculateAxisTarget({
|
|
488
|
+
index: colIndex,
|
|
489
|
+
align: alignX as ScrollAlignment,
|
|
490
|
+
viewSize: viewportWidth,
|
|
491
|
+
scrollPos: relativeScrollX,
|
|
492
|
+
fixedSize: isGrid ? fixedWidth : fixedSize,
|
|
493
|
+
gap: (isGrid || isHorizontal) ? columnGap : gap,
|
|
494
|
+
query: isGrid ? getColumnQuery : getItemQueryX,
|
|
495
|
+
getSize: isGrid ? getColumnSize : getItemSizeX,
|
|
496
|
+
stickyIndices,
|
|
497
|
+
stickyStart: stickyStartX + paddingStartX,
|
|
498
|
+
stickyEnd: stickyEndX + paddingEndX,
|
|
499
|
+
});
|
|
500
|
+
targetX = res.target + itemsStartVirtualX;
|
|
501
|
+
itemWidth = res.itemSize;
|
|
502
|
+
effectiveAlignX = res.effectiveAlign;
|
|
216
503
|
}
|
|
217
504
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
targetY = Math.max(0, Math.min(targetY, Math.max(0, totalHeight - usableHeight)));
|
|
505
|
+
targetX = Math.max(0, Math.min(targetX, maxTargetX));
|
|
506
|
+
targetY = Math.max(0, Math.min(targetY, maxTargetY));
|
|
221
507
|
|
|
222
508
|
return { targetX, targetY, itemWidth, itemHeight, effectiveAlignX, effectiveAlignY };
|
|
223
509
|
}
|
|
@@ -225,153 +511,160 @@ export function calculateScrollTarget(params: ScrollTargetParams): ScrollTargetR
|
|
|
225
511
|
/**
|
|
226
512
|
* Calculates the range of items to render based on scroll position and viewport size.
|
|
227
513
|
*
|
|
228
|
-
* @param params -
|
|
514
|
+
* @param params - Range parameters.
|
|
515
|
+
* @param params.direction - Scroll direction.
|
|
516
|
+
* @param params.relativeScrollX - Virtual horizontal position (VU).
|
|
517
|
+
* @param params.relativeScrollY - Virtual vertical position (VU).
|
|
518
|
+
* @param params.usableWidth - Usable viewport width (VU).
|
|
519
|
+
* @param params.usableHeight - Usable viewport height (VU).
|
|
520
|
+
* @param params.itemsLength - Total item count.
|
|
521
|
+
* @param params.bufferBefore - Buffer items before.
|
|
522
|
+
* @param params.bufferAfter - Buffer items after.
|
|
523
|
+
* @param params.gap - Item gap (VU).
|
|
524
|
+
* @param params.columnGap - Column gap (VU).
|
|
525
|
+
* @param params.fixedSize - Fixed item size (VU).
|
|
526
|
+
* @param params.findLowerBoundY - Resolver for vertical index.
|
|
527
|
+
* @param params.findLowerBoundX - Resolver for horizontal index.
|
|
528
|
+
* @param params.queryY - Resolver for vertical offset (VU).
|
|
529
|
+
* @param params.queryX - Resolver for horizontal offset (VU).
|
|
229
530
|
* @returns The start and end indices of the items to render.
|
|
230
531
|
* @see RangeParams
|
|
231
532
|
*/
|
|
232
|
-
export function calculateRange(
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
533
|
+
export function calculateRange({
|
|
534
|
+
direction,
|
|
535
|
+
relativeScrollX,
|
|
536
|
+
relativeScrollY,
|
|
537
|
+
usableWidth,
|
|
538
|
+
usableHeight,
|
|
539
|
+
itemsLength,
|
|
540
|
+
bufferBefore,
|
|
541
|
+
bufferAfter,
|
|
542
|
+
gap,
|
|
543
|
+
columnGap,
|
|
544
|
+
fixedSize,
|
|
545
|
+
findLowerBoundY,
|
|
546
|
+
findLowerBoundX,
|
|
547
|
+
queryY,
|
|
548
|
+
queryX,
|
|
549
|
+
}: RangeParams) {
|
|
550
|
+
const isVertical = direction === 'vertical' || direction === 'both';
|
|
551
|
+
|
|
552
|
+
return calculateGenericRange({
|
|
553
|
+
scrollPos: isVertical ? relativeScrollY : relativeScrollX,
|
|
554
|
+
containerSize: isVertical ? usableHeight : usableWidth,
|
|
555
|
+
count: itemsLength,
|
|
240
556
|
bufferBefore,
|
|
241
557
|
bufferAfter,
|
|
242
|
-
gap,
|
|
243
|
-
columnGap,
|
|
558
|
+
gap: isVertical ? gap : columnGap,
|
|
244
559
|
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
|
-
};
|
|
560
|
+
findLowerBound: isVertical ? findLowerBoundY : findLowerBoundX,
|
|
561
|
+
query: isVertical ? queryY : queryX,
|
|
562
|
+
});
|
|
286
563
|
}
|
|
287
564
|
|
|
288
565
|
/**
|
|
289
566
|
* Calculates the range of columns to render for bidirectional scroll.
|
|
290
567
|
*
|
|
291
|
-
* @param params -
|
|
292
|
-
* @
|
|
568
|
+
* @param params - Column range parameters.
|
|
569
|
+
* @param params.columnCount - Total column count.
|
|
570
|
+
* @param params.relativeScrollX - Virtual horizontal position (VU).
|
|
571
|
+
* @param params.usableWidth - Usable viewport width (VU).
|
|
572
|
+
* @param params.colBuffer - Column buffer size.
|
|
573
|
+
* @param params.fixedWidth - Fixed column width (VU).
|
|
574
|
+
* @param params.columnGap - Column gap (VU).
|
|
575
|
+
* @param params.findLowerBound - Resolver for column index.
|
|
576
|
+
* @param params.query - Resolver for column offset (VU).
|
|
577
|
+
* @param params.totalColsQuery - Resolver for total width (VU).
|
|
578
|
+
* @returns The start and end indices and paddings for columns (VU).
|
|
293
579
|
* @see ColumnRangeParams
|
|
294
580
|
* @see ColumnRange
|
|
295
581
|
*/
|
|
296
|
-
export function calculateColumnRange(
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
} = params;
|
|
308
|
-
|
|
582
|
+
export function calculateColumnRange({
|
|
583
|
+
columnCount,
|
|
584
|
+
relativeScrollX,
|
|
585
|
+
usableWidth,
|
|
586
|
+
colBuffer,
|
|
587
|
+
fixedWidth,
|
|
588
|
+
columnGap,
|
|
589
|
+
findLowerBound,
|
|
590
|
+
query,
|
|
591
|
+
totalColsQuery,
|
|
592
|
+
}: ColumnRangeParams) {
|
|
309
593
|
if (!columnCount) {
|
|
310
594
|
return { start: 0, end: 0, padStart: 0, padEnd: 0 };
|
|
311
595
|
}
|
|
312
596
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
currentX = query(++i);
|
|
325
|
-
}
|
|
326
|
-
end = i;
|
|
327
|
-
}
|
|
597
|
+
const { start, end } = calculateGenericRange({
|
|
598
|
+
scrollPos: relativeScrollX,
|
|
599
|
+
containerSize: usableWidth,
|
|
600
|
+
count: columnCount,
|
|
601
|
+
bufferBefore: colBuffer,
|
|
602
|
+
bufferAfter: colBuffer,
|
|
603
|
+
gap: columnGap,
|
|
604
|
+
fixedSize: fixedWidth,
|
|
605
|
+
findLowerBound,
|
|
606
|
+
query,
|
|
607
|
+
});
|
|
328
608
|
|
|
329
|
-
|
|
330
|
-
const
|
|
331
|
-
const safeEnd = Math.min(columnCount, end + colBuffer);
|
|
609
|
+
const safeStart = start;
|
|
610
|
+
const safeEnd = end;
|
|
332
611
|
|
|
333
612
|
const padStart = fixedWidth !== null ? safeStart * (fixedWidth + columnGap) : query(safeStart);
|
|
334
613
|
const totalWidth = fixedWidth !== null ? columnCount * (fixedWidth + columnGap) - columnGap : Math.max(0, totalColsQuery() - columnGap);
|
|
335
614
|
|
|
336
|
-
const
|
|
337
|
-
? (safeEnd * (fixedWidth + columnGap) - (safeEnd
|
|
338
|
-
: (query(safeEnd) - (safeEnd
|
|
615
|
+
const contentEnd = fixedWidth !== null
|
|
616
|
+
? (safeEnd * (fixedWidth + columnGap) - (safeEnd > 0 ? columnGap : 0))
|
|
617
|
+
: (query(safeEnd) - (safeEnd > 0 ? columnGap : 0));
|
|
339
618
|
|
|
340
619
|
return {
|
|
341
620
|
start: safeStart,
|
|
342
621
|
end: safeEnd,
|
|
343
622
|
padStart,
|
|
344
|
-
padEnd: Math.max(0, totalWidth -
|
|
623
|
+
padEnd: Math.max(0, totalWidth - contentEnd),
|
|
345
624
|
};
|
|
346
625
|
}
|
|
347
626
|
|
|
348
627
|
/**
|
|
349
628
|
* Calculates the sticky state and offset for a single item.
|
|
350
629
|
*
|
|
351
|
-
* @param params -
|
|
352
|
-
* @
|
|
630
|
+
* @param params - Sticky item parameters.
|
|
631
|
+
* @param params.index - Item index.
|
|
632
|
+
* @param params.isSticky - If configured as sticky.
|
|
633
|
+
* @param params.direction - Scroll direction.
|
|
634
|
+
* @param params.relativeScrollX - Virtual horizontal position (VU).
|
|
635
|
+
* @param params.relativeScrollY - Virtual vertical position (VU).
|
|
636
|
+
* @param params.originalX - Virtual original X position (VU).
|
|
637
|
+
* @param params.originalY - Virtual original Y position (VU).
|
|
638
|
+
* @param params.width - Virtual item width (VU).
|
|
639
|
+
* @param params.height - Virtual item height (VU).
|
|
640
|
+
* @param params.stickyIndices - All sticky indices.
|
|
641
|
+
* @param params.fixedSize - Fixed item size (VU).
|
|
642
|
+
* @param params.fixedWidth - Fixed column width (VU).
|
|
643
|
+
* @param params.gap - Item gap (VU).
|
|
644
|
+
* @param params.columnGap - Column gap (VU).
|
|
645
|
+
* @param params.getItemQueryY - Resolver for vertical offset (VU).
|
|
646
|
+
* @param params.getItemQueryX - Resolver for horizontal offset (VU).
|
|
647
|
+
* @returns Sticky state and offset (VU).
|
|
353
648
|
* @see StickyParams
|
|
354
649
|
*/
|
|
355
|
-
export function calculateStickyItem(
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
} = params;
|
|
374
|
-
|
|
650
|
+
export function calculateStickyItem({
|
|
651
|
+
index,
|
|
652
|
+
isSticky,
|
|
653
|
+
direction,
|
|
654
|
+
relativeScrollX,
|
|
655
|
+
relativeScrollY,
|
|
656
|
+
originalX,
|
|
657
|
+
originalY,
|
|
658
|
+
width,
|
|
659
|
+
height,
|
|
660
|
+
stickyIndices,
|
|
661
|
+
fixedSize,
|
|
662
|
+
fixedWidth,
|
|
663
|
+
gap,
|
|
664
|
+
columnGap,
|
|
665
|
+
getItemQueryY,
|
|
666
|
+
getItemQueryX,
|
|
667
|
+
}: StickyParams) {
|
|
375
668
|
let isStickyActive = false;
|
|
376
669
|
const stickyOffset = { x: 0, y: 0 };
|
|
377
670
|
|
|
@@ -379,21 +672,10 @@ export function calculateStickyItem(params: StickyParams) {
|
|
|
379
672
|
return { isStickyActive, stickyOffset };
|
|
380
673
|
}
|
|
381
674
|
|
|
675
|
+
// Y Axis (Sticky Rows)
|
|
382
676
|
if (direction === 'vertical' || direction === 'both') {
|
|
383
677
|
if (relativeScrollY > originalY) {
|
|
384
|
-
|
|
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
|
-
}
|
|
678
|
+
const nextStickyIdx = findNextStickyIndex(stickyIndices, index);
|
|
397
679
|
|
|
398
680
|
if (nextStickyIdx !== undefined) {
|
|
399
681
|
const nextStickyY = fixedSize !== null ? nextStickyIdx * (fixedSize + gap) : getItemQueryY(nextStickyIdx);
|
|
@@ -409,20 +691,10 @@ export function calculateStickyItem(params: StickyParams) {
|
|
|
409
691
|
}
|
|
410
692
|
}
|
|
411
693
|
|
|
694
|
+
// X Axis (Sticky Columns / Items)
|
|
412
695
|
if (direction === 'horizontal' || (direction === 'both' && !isStickyActive)) {
|
|
413
696
|
if (relativeScrollX > originalX) {
|
|
414
|
-
|
|
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
|
-
}
|
|
697
|
+
const nextStickyIdx = findNextStickyIndex(stickyIndices, index);
|
|
426
698
|
|
|
427
699
|
if (nextStickyIdx !== undefined) {
|
|
428
700
|
const nextStickyX = direction === 'horizontal'
|
|
@@ -447,26 +719,38 @@ export function calculateStickyItem(params: StickyParams) {
|
|
|
447
719
|
/**
|
|
448
720
|
* Calculates the position and size of a single item.
|
|
449
721
|
*
|
|
450
|
-
* @param params -
|
|
451
|
-
* @
|
|
722
|
+
* @param params - Item position parameters.
|
|
723
|
+
* @param params.index - Item index.
|
|
724
|
+
* @param params.direction - Scroll direction.
|
|
725
|
+
* @param params.fixedSize - Fixed item size (VU).
|
|
726
|
+
* @param params.gap - Item gap (VU).
|
|
727
|
+
* @param params.columnGap - Column gap (VU).
|
|
728
|
+
* @param params.usableWidth - Usable viewport width (VU).
|
|
729
|
+
* @param params.usableHeight - Usable viewport height (VU).
|
|
730
|
+
* @param params.totalWidth - Total estimated width (VU).
|
|
731
|
+
* @param params.queryY - Resolver for vertical offset (VU).
|
|
732
|
+
* @param params.queryX - Resolver for horizontal offset (VU).
|
|
733
|
+
* @param params.getSizeY - Resolver for height (VU).
|
|
734
|
+
* @param params.getSizeX - Resolver for width (VU).
|
|
735
|
+
* @param params.columnRange - Current column range (for grid mode).
|
|
736
|
+
* @returns Item position and size (VU).
|
|
452
737
|
* @see ItemPositionParams
|
|
453
738
|
*/
|
|
454
|
-
export function calculateItemPosition(
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
739
|
+
export function calculateItemPosition({
|
|
740
|
+
index,
|
|
741
|
+
direction,
|
|
742
|
+
fixedSize,
|
|
743
|
+
gap,
|
|
744
|
+
columnGap,
|
|
745
|
+
usableWidth,
|
|
746
|
+
usableHeight,
|
|
747
|
+
totalWidth,
|
|
748
|
+
queryY,
|
|
749
|
+
queryX,
|
|
750
|
+
getSizeY,
|
|
751
|
+
getSizeX,
|
|
752
|
+
columnRange,
|
|
753
|
+
}: ItemPositionParams) {
|
|
470
754
|
let x = 0;
|
|
471
755
|
let y = 0;
|
|
472
756
|
let width = 0;
|
|
@@ -476,9 +760,13 @@ export function calculateItemPosition(params: ItemPositionParams) {
|
|
|
476
760
|
x = fixedSize !== null ? index * (fixedSize + columnGap) : queryX(index);
|
|
477
761
|
width = fixedSize !== null ? fixedSize : getSizeX(index) - columnGap;
|
|
478
762
|
height = usableHeight;
|
|
763
|
+
} else if (direction === 'both' && columnRange) {
|
|
764
|
+
y = fixedSize !== null ? index * (fixedSize + gap) : queryY(index);
|
|
765
|
+
height = fixedSize !== null ? fixedSize : getSizeY(index) - gap;
|
|
766
|
+
x = columnRange.padStart;
|
|
767
|
+
width = Math.max(0, totalWidth - columnRange.padStart - columnRange.padEnd);
|
|
479
768
|
} else {
|
|
480
|
-
|
|
481
|
-
y = (direction === 'vertical' || direction === 'both') && fixedSize !== null ? index * (fixedSize + gap) : queryY(index);
|
|
769
|
+
y = fixedSize !== null ? index * (fixedSize + gap) : queryY(index);
|
|
482
770
|
height = fixedSize !== null ? fixedSize : getSizeY(index) - gap;
|
|
483
771
|
width = direction === 'both' ? totalWidth : usableWidth;
|
|
484
772
|
}
|
|
@@ -489,21 +777,28 @@ export function calculateItemPosition(params: ItemPositionParams) {
|
|
|
489
777
|
/**
|
|
490
778
|
* Calculates the style object for a rendered item.
|
|
491
779
|
*
|
|
492
|
-
* @param params -
|
|
780
|
+
* @param params - Item style parameters.
|
|
781
|
+
* @param params.item - Rendered item state.
|
|
782
|
+
* @param params.direction - Scroll direction.
|
|
783
|
+
* @param params.itemSize - Virtual item size (VU).
|
|
784
|
+
* @param params.containerTag - Container HTML tag.
|
|
785
|
+
* @param params.paddingStartX - Horizontal virtual padding (DU).
|
|
786
|
+
* @param params.paddingStartY - Vertical virtual padding (DU).
|
|
787
|
+
* @param params.isHydrated - If mounted and hydrated.
|
|
788
|
+
* @param params.isRtl - If in RTL mode.
|
|
493
789
|
* @returns Style object.
|
|
494
790
|
* @see ItemStyleParams
|
|
495
791
|
*/
|
|
496
|
-
export function calculateItemStyle<T = unknown>(
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
792
|
+
export function calculateItemStyle<T = unknown>({
|
|
793
|
+
item,
|
|
794
|
+
direction,
|
|
795
|
+
itemSize,
|
|
796
|
+
containerTag,
|
|
797
|
+
paddingStartX,
|
|
798
|
+
paddingStartY,
|
|
799
|
+
isHydrated,
|
|
800
|
+
isRtl,
|
|
801
|
+
}: ItemStyleParams<T>) {
|
|
507
802
|
const isVertical = direction === 'vertical';
|
|
508
803
|
const isHorizontal = direction === 'horizontal';
|
|
509
804
|
const isBoth = direction === 'both';
|
|
@@ -529,18 +824,20 @@ export function calculateItemStyle<T = unknown>(params: ItemStyleParams<T>) {
|
|
|
529
824
|
}
|
|
530
825
|
|
|
531
826
|
if (isHydrated) {
|
|
827
|
+
const tx = isRtl
|
|
828
|
+
? -(item.isStickyActive ? item.stickyOffset.x : item.offset.x)
|
|
829
|
+
: (item.isStickyActive ? item.stickyOffset.x : item.offset.x);
|
|
830
|
+
|
|
532
831
|
if (item.isStickyActive) {
|
|
533
832
|
if (isVertical || isBoth) {
|
|
534
833
|
style.insetBlockStart = `${ paddingStartY }px`;
|
|
535
834
|
}
|
|
536
|
-
|
|
537
835
|
if (isHorizontal || isBoth) {
|
|
538
836
|
style.insetInlineStart = `${ paddingStartX }px`;
|
|
539
837
|
}
|
|
540
|
-
|
|
541
|
-
style.transform = `translate(${ item.stickyOffset.x }px, ${ item.stickyOffset.y }px)`;
|
|
838
|
+
style.transform = `translate(${ tx }px, ${ item.stickyOffset.y }px)`;
|
|
542
839
|
} else {
|
|
543
|
-
style.transform = `translate(${
|
|
840
|
+
style.transform = `translate(${ tx }px, ${ item.offset.y }px)`;
|
|
544
841
|
}
|
|
545
842
|
}
|
|
546
843
|
|
|
@@ -550,56 +847,55 @@ export function calculateItemStyle<T = unknown>(params: ItemStyleParams<T>) {
|
|
|
550
847
|
/**
|
|
551
848
|
* Calculates the total width and height of the virtualized content.
|
|
552
849
|
*
|
|
553
|
-
* @param params -
|
|
554
|
-
* @
|
|
850
|
+
* @param params - Total size parameters.
|
|
851
|
+
* @param params.direction - Scroll direction.
|
|
852
|
+
* @param params.itemsLength - Total item count.
|
|
853
|
+
* @param params.columnCount - Column count.
|
|
854
|
+
* @param params.fixedSize - Fixed item size (VU).
|
|
855
|
+
* @param params.fixedWidth - Fixed column width (VU).
|
|
856
|
+
* @param params.gap - Item gap (VU).
|
|
857
|
+
* @param params.columnGap - Column gap (VU).
|
|
858
|
+
* @param params.usableWidth - Usable viewport width (VU).
|
|
859
|
+
* @param params.usableHeight - Usable viewport height (VU).
|
|
860
|
+
* @param params.queryY - Resolver for vertical offset (VU).
|
|
861
|
+
* @param params.queryX - Resolver for horizontal offset (VU).
|
|
862
|
+
* @param params.queryColumn - Resolver for column offset (VU).
|
|
863
|
+
* @returns Total width and height (VU).
|
|
555
864
|
* @see TotalSizeParams
|
|
556
865
|
*/
|
|
557
|
-
export function calculateTotalSize(
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
866
|
+
export function calculateTotalSize({
|
|
867
|
+
direction,
|
|
868
|
+
itemsLength,
|
|
869
|
+
columnCount,
|
|
870
|
+
fixedSize,
|
|
871
|
+
fixedWidth,
|
|
872
|
+
gap,
|
|
873
|
+
columnGap,
|
|
874
|
+
usableWidth,
|
|
875
|
+
usableHeight,
|
|
876
|
+
queryY,
|
|
877
|
+
queryX,
|
|
878
|
+
queryColumn,
|
|
879
|
+
}: TotalSizeParams) {
|
|
880
|
+
const isBoth = direction === 'both';
|
|
881
|
+
const isHorizontal = direction === 'horizontal';
|
|
572
882
|
|
|
573
883
|
let width = 0;
|
|
574
884
|
let height = 0;
|
|
575
885
|
|
|
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
|
-
}
|
|
886
|
+
if (isBoth) {
|
|
887
|
+
width = calculateAxisSize(columnCount, fixedWidth, columnGap, queryColumn);
|
|
888
|
+
height = calculateAxisSize(itemsLength, fixedSize, gap, queryY);
|
|
889
|
+
} else if (isHorizontal) {
|
|
890
|
+
width = calculateAxisSize(itemsLength, fixedSize, columnGap, queryX);
|
|
593
891
|
height = usableHeight;
|
|
594
892
|
} else {
|
|
595
|
-
// vertical
|
|
596
893
|
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
|
-
}
|
|
894
|
+
height = calculateAxisSize(itemsLength, fixedSize, gap, queryY);
|
|
602
895
|
}
|
|
603
896
|
|
|
604
|
-
return {
|
|
897
|
+
return {
|
|
898
|
+
width: isBoth ? Math.max(width, usableWidth) : width,
|
|
899
|
+
height: isBoth ? Math.max(height, usableHeight) : height,
|
|
900
|
+
};
|
|
605
901
|
}
|