@pdanpdan/virtual-scroll 0.7.0 → 0.9.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 +99 -6
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +295 -31
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +954 -878
- package/dist/index.mjs.map +1 -1
- package/dist/virtual-scroll.css +1 -1
- package/package.json +7 -2
- package/src/components/VirtualScroll.vue +301 -209
- package/src/components/VirtualScrollbar.vue +8 -8
- package/src/composables/useVirtualScroll.ts +261 -521
- package/src/composables/useVirtualScrollSizes.ts +448 -0
- package/src/composables/useVirtualScrollbar.ts +30 -35
- package/src/index.ts +1 -0
- package/src/types.ts +25 -2
- package/src/utils/scroll.ts +14 -14
- package/src/utils/virtual-scroll-logic.ts +305 -1
package/src/utils/scroll.ts
CHANGED
|
@@ -18,50 +18,50 @@ export const BROWSER_MAX_SIZE = 10000000;
|
|
|
18
18
|
/**
|
|
19
19
|
* Checks if the container is the window object.
|
|
20
20
|
*
|
|
21
|
-
* @param container - The container element or window to check.
|
|
21
|
+
* @param container - The container element or window to check. Optional.
|
|
22
22
|
* @returns `true` if the container is the global window object.
|
|
23
23
|
*/
|
|
24
|
-
export function isWindow(container
|
|
24
|
+
export function isWindow(container?: HTMLElement | Window | null): container is Window {
|
|
25
25
|
return container === null || container === document.documentElement || (typeof window !== 'undefined' && container === window);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* Checks if the container is the document body element.
|
|
30
30
|
*
|
|
31
|
-
* @param container - The container element or window to check.
|
|
31
|
+
* @param container - The container element or window to check. Optional.
|
|
32
32
|
* @returns `true` if the container is the `<body>` element.
|
|
33
33
|
*/
|
|
34
|
-
export function isBody(container
|
|
34
|
+
export function isBody(container?: HTMLElement | Window | null): container is HTMLElement {
|
|
35
35
|
return container != null && typeof container === 'object' && 'tagName' in container && container.tagName === 'BODY';
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
39
|
* Checks if the container is window-like (global window or document body).
|
|
40
40
|
*
|
|
41
|
-
* @param container - The container element or window to check.
|
|
41
|
+
* @param container - The container element or window to check. Optional.
|
|
42
42
|
* @returns `true` if the container is window or body.
|
|
43
43
|
*/
|
|
44
|
-
export function isWindowLike(container
|
|
44
|
+
export function isWindowLike(container?: HTMLElement | Window | null): boolean {
|
|
45
45
|
return isWindow(container) || isBody(container);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
49
|
* Checks if the container is a valid HTML Element with bounding rect support.
|
|
50
50
|
*
|
|
51
|
-
* @param container - The container to check.
|
|
51
|
+
* @param container - The container to check. Optional.
|
|
52
52
|
* @returns `true` if the container is an `HTMLElement`.
|
|
53
53
|
*/
|
|
54
|
-
export function isElement(container
|
|
54
|
+
export function isElement(container?: HTMLElement | Window | null): container is HTMLElement {
|
|
55
55
|
return container != null && 'getBoundingClientRect' in container;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
59
|
* Checks if the target is an element that supports scrolling.
|
|
60
60
|
*
|
|
61
|
-
* @param target - The event target to check.
|
|
61
|
+
* @param target - The event target to check. Optional.
|
|
62
62
|
* @returns `true` if the target is an `HTMLElement` with scroll properties.
|
|
63
63
|
*/
|
|
64
|
-
export function isScrollableElement(target
|
|
64
|
+
export function isScrollableElement(target?: EventTarget | null): target is HTMLElement {
|
|
65
65
|
return target != null && 'scrollLeft' in target;
|
|
66
66
|
}
|
|
67
67
|
|
|
@@ -101,11 +101,11 @@ export function isScrollToIndexOptions(options: unknown): options is ScrollToInd
|
|
|
101
101
|
/**
|
|
102
102
|
* Extracts the horizontal padding from a padding configuration.
|
|
103
103
|
*
|
|
104
|
-
* @param p - The padding value (number or object with x/y).
|
|
104
|
+
* @param p - The padding value (number or object with x/y). Optional.
|
|
105
105
|
* @param direction - The current scroll direction.
|
|
106
106
|
* @returns The horizontal padding in pixels.
|
|
107
107
|
*/
|
|
108
|
-
export function getPaddingX(p
|
|
108
|
+
export function getPaddingX(p?: number | { x?: number; y?: number; } | null, direction?: ScrollDirection) {
|
|
109
109
|
if (typeof p === 'object' && p !== null) {
|
|
110
110
|
return p.x || 0;
|
|
111
111
|
}
|
|
@@ -115,11 +115,11 @@ export function getPaddingX(p: number | { x?: number; y?: number; } | undefined,
|
|
|
115
115
|
/**
|
|
116
116
|
* Extracts the vertical padding from a padding configuration.
|
|
117
117
|
*
|
|
118
|
-
* @param p - The padding value (number or object with x/y).
|
|
118
|
+
* @param p - The padding value (number or object with x/y). Optional.
|
|
119
119
|
* @param direction - The current scroll direction.
|
|
120
120
|
* @returns The vertical padding in pixels.
|
|
121
121
|
*/
|
|
122
|
-
export function getPaddingY(p
|
|
122
|
+
export function getPaddingY(p?: number | { x?: number; y?: number; } | null, direction?: ScrollDirection) {
|
|
123
123
|
if (typeof p === 'object' && p !== null) {
|
|
124
124
|
return p.y || 0;
|
|
125
125
|
}
|
|
@@ -5,9 +5,12 @@ import type {
|
|
|
5
5
|
RangeParams,
|
|
6
6
|
ScrollAlignment,
|
|
7
7
|
ScrollAlignmentOptions,
|
|
8
|
+
ScrollDirection,
|
|
8
9
|
ScrollTargetParams,
|
|
9
10
|
ScrollTargetResult,
|
|
10
11
|
ScrollToIndexOptions,
|
|
12
|
+
SnapMode,
|
|
13
|
+
SSRRange,
|
|
11
14
|
StickyParams,
|
|
12
15
|
TotalSizeParams,
|
|
13
16
|
} from '../types';
|
|
@@ -38,6 +41,14 @@ interface AxisAlignmentParams {
|
|
|
38
41
|
stickyOffsetEnd: number;
|
|
39
42
|
}
|
|
40
43
|
|
|
44
|
+
/** Snap result. */
|
|
45
|
+
export interface SnapResult {
|
|
46
|
+
/** Target index. */
|
|
47
|
+
index: number;
|
|
48
|
+
/** Alignment. */
|
|
49
|
+
align: 'start' | 'center' | 'end';
|
|
50
|
+
}
|
|
51
|
+
|
|
41
52
|
// --- Internal Helpers ---
|
|
42
53
|
|
|
43
54
|
/**
|
|
@@ -212,7 +223,7 @@ function calculateAxisAlignment({
|
|
|
212
223
|
* @param query - Prefix sum resolver.
|
|
213
224
|
* @returns Total size.
|
|
214
225
|
*/
|
|
215
|
-
function calculateAxisSize(
|
|
226
|
+
export function calculateAxisSize(
|
|
216
227
|
count: number,
|
|
217
228
|
fixedSize: number | null,
|
|
218
229
|
gap: number,
|
|
@@ -227,6 +238,54 @@ function calculateAxisSize(
|
|
|
227
238
|
return Math.max(0, query(count) - gap);
|
|
228
239
|
}
|
|
229
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Helper to calculate size for a range on a single axis.
|
|
243
|
+
*
|
|
244
|
+
* @param start - Start index.
|
|
245
|
+
* @param end - End index.
|
|
246
|
+
* @param fixedSize - Fixed size if any.
|
|
247
|
+
* @param gap - Gap size.
|
|
248
|
+
* @param query - Prefix sum resolver.
|
|
249
|
+
* @returns Range size.
|
|
250
|
+
*/
|
|
251
|
+
export function calculateRangeSize(
|
|
252
|
+
start: number,
|
|
253
|
+
end: number,
|
|
254
|
+
fixedSize: number | null,
|
|
255
|
+
gap: number,
|
|
256
|
+
query: (index: number) => number,
|
|
257
|
+
): number {
|
|
258
|
+
const count = end - start;
|
|
259
|
+
if (count <= 0) {
|
|
260
|
+
return 0;
|
|
261
|
+
}
|
|
262
|
+
if (fixedSize !== null) {
|
|
263
|
+
return Math.max(0, count * (fixedSize + gap) - gap);
|
|
264
|
+
}
|
|
265
|
+
return Math.max(0, query(end) - query(start) - gap);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Helper to calculate offset at a specific index.
|
|
270
|
+
*
|
|
271
|
+
* @param index - Item index.
|
|
272
|
+
* @param fixedSize - Fixed size if any.
|
|
273
|
+
* @param gap - Gap size.
|
|
274
|
+
* @param query - Prefix sum resolver.
|
|
275
|
+
* @returns Offset at index.
|
|
276
|
+
*/
|
|
277
|
+
export function calculateOffsetAt(
|
|
278
|
+
index: number,
|
|
279
|
+
fixedSize: number | null,
|
|
280
|
+
gap: number,
|
|
281
|
+
query: (index: number) => number,
|
|
282
|
+
): number {
|
|
283
|
+
if (fixedSize !== null) {
|
|
284
|
+
return index * (fixedSize + gap);
|
|
285
|
+
}
|
|
286
|
+
return query(index);
|
|
287
|
+
}
|
|
288
|
+
|
|
230
289
|
/**
|
|
231
290
|
* Helper to calculate target scroll position for a single axis.
|
|
232
291
|
*
|
|
@@ -363,6 +422,35 @@ export function isItemVisible(
|
|
|
363
422
|
return itemPos <= usableStart + 0.5 && (itemPos + itemSize) >= usableEnd - 0.5;
|
|
364
423
|
}
|
|
365
424
|
|
|
425
|
+
/**
|
|
426
|
+
* Calculates the coordinate scaling factor between virtual and display units.
|
|
427
|
+
*
|
|
428
|
+
* @param isWindow - If the container is the window.
|
|
429
|
+
* @param totalSize - Total virtual size (VU).
|
|
430
|
+
* @param viewportSize - Viewport size (DU).
|
|
431
|
+
* @returns Scaling factor (VU/DU).
|
|
432
|
+
*/
|
|
433
|
+
export function calculateScale(isWindow: boolean, totalSize: number, viewportSize: number): number {
|
|
434
|
+
if (isWindow || totalSize <= BROWSER_MAX_SIZE) {
|
|
435
|
+
return 1;
|
|
436
|
+
}
|
|
437
|
+
const displaySize = Math.min(totalSize, BROWSER_MAX_SIZE);
|
|
438
|
+
const realRange = totalSize - viewportSize;
|
|
439
|
+
const displayRange = displaySize - viewportSize;
|
|
440
|
+
return displayRange > 0 ? realRange / displayRange : 1;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Calculates the physical size to be rendered in the DOM.
|
|
445
|
+
*
|
|
446
|
+
* @param isWindow - If the container is the window.
|
|
447
|
+
* @param totalSize - Total virtual size (VU).
|
|
448
|
+
* @returns Physical size (DU).
|
|
449
|
+
*/
|
|
450
|
+
export function calculateRenderedSize(isWindow: boolean, totalSize: number): number {
|
|
451
|
+
return isWindow ? totalSize : Math.min(totalSize, BROWSER_MAX_SIZE);
|
|
452
|
+
}
|
|
453
|
+
|
|
366
454
|
/**
|
|
367
455
|
* Maps a display scroll position to a virtual content position.
|
|
368
456
|
*
|
|
@@ -877,6 +965,72 @@ export function calculateItemStyle<T = unknown>({
|
|
|
877
965
|
return style;
|
|
878
966
|
}
|
|
879
967
|
|
|
968
|
+
/**
|
|
969
|
+
* Calculates the SSR offsets for items based on the configured SSR range and direction.
|
|
970
|
+
*
|
|
971
|
+
* @param direction - Scroll direction.
|
|
972
|
+
* @param ssrRange - SSR configuration.
|
|
973
|
+
* @param fixedItemSize - Fixed item size (VU).
|
|
974
|
+
* @param fixedColumnWidth - Fixed column width (VU).
|
|
975
|
+
* @param gap - Item gap (VU).
|
|
976
|
+
* @param columnGap - Column gap (VU).
|
|
977
|
+
* @param queryY - Resolver for vertical offset (VU).
|
|
978
|
+
* @param queryX - Resolver for horizontal offset (VU).
|
|
979
|
+
* @param queryColumn - Resolver for column offset (VU).
|
|
980
|
+
* @returns Offset X and Y (VU).
|
|
981
|
+
*/
|
|
982
|
+
export function calculateSSROffsets(
|
|
983
|
+
direction: ScrollDirection,
|
|
984
|
+
ssrRange: SSRRange,
|
|
985
|
+
fixedItemSize: number | null,
|
|
986
|
+
fixedColumnWidth: number | null,
|
|
987
|
+
gap: number,
|
|
988
|
+
columnGap: number,
|
|
989
|
+
queryY: (index: number) => number,
|
|
990
|
+
queryX: (index: number) => number,
|
|
991
|
+
queryColumn: (index: number) => number,
|
|
992
|
+
): { x: number; y: number; } {
|
|
993
|
+
const ssrStartRow = ssrRange.start || 0;
|
|
994
|
+
const ssrStartCol = ssrRange.colStart || 0;
|
|
995
|
+
|
|
996
|
+
let x = 0;
|
|
997
|
+
const y = (direction !== 'horizontal')
|
|
998
|
+
? calculateOffsetAt(ssrStartRow, fixedItemSize, gap, queryY)
|
|
999
|
+
: 0;
|
|
1000
|
+
|
|
1001
|
+
if (direction === 'horizontal') {
|
|
1002
|
+
x = calculateOffsetAt(ssrStartCol, fixedItemSize, columnGap, queryX);
|
|
1003
|
+
} else if (direction === 'both') {
|
|
1004
|
+
x = calculateOffsetAt(ssrStartCol, fixedColumnWidth, columnGap, queryColumn);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
return { x, y };
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/**
|
|
1011
|
+
* Detects how many items were prepended to the list based on item identity.
|
|
1012
|
+
*
|
|
1013
|
+
* @param oldItems - Previous items list.
|
|
1014
|
+
* @param newItems - Current items list.
|
|
1015
|
+
* @returns Number of prepended items.
|
|
1016
|
+
*/
|
|
1017
|
+
export function calculatePrependCount<T>(oldItems: T[], newItems: T[]): number {
|
|
1018
|
+
if (oldItems.length === 0 || newItems.length <= oldItems.length) {
|
|
1019
|
+
return 0;
|
|
1020
|
+
}
|
|
1021
|
+
const oldFirstItem = oldItems[ 0 ];
|
|
1022
|
+
if (oldFirstItem === undefined) {
|
|
1023
|
+
return 0;
|
|
1024
|
+
}
|
|
1025
|
+
const limit = newItems.length - oldItems.length;
|
|
1026
|
+
for (let i = 1; i <= limit; i++) {
|
|
1027
|
+
if (newItems[ i ] === oldFirstItem) {
|
|
1028
|
+
return i;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
return 0;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
880
1034
|
/**
|
|
881
1035
|
* Calculates the total width and height of the virtualized content.
|
|
882
1036
|
*
|
|
@@ -932,3 +1086,153 @@ export function calculateTotalSize({
|
|
|
932
1086
|
height: isBoth ? Math.max(height, usableHeight) : height,
|
|
933
1087
|
};
|
|
934
1088
|
}
|
|
1089
|
+
|
|
1090
|
+
/**
|
|
1091
|
+
* Calculates the next step in an inertia animation.
|
|
1092
|
+
*
|
|
1093
|
+
* @param velocity - Current velocity.
|
|
1094
|
+
* @param velocity.x - Horizontal velocity.
|
|
1095
|
+
* @param velocity.y - Vertical velocity.
|
|
1096
|
+
* @param friction - Friction coefficient (0-1).
|
|
1097
|
+
* @param frameTime - Time elapsed since last frame in ms (default 16ms).
|
|
1098
|
+
* @returns Next velocity and distance to move.
|
|
1099
|
+
*/
|
|
1100
|
+
export function calculateInertiaStep(
|
|
1101
|
+
velocity: { x: number; y: number; },
|
|
1102
|
+
friction: number,
|
|
1103
|
+
frameTime: number = 16,
|
|
1104
|
+
) {
|
|
1105
|
+
const nextVelocity = {
|
|
1106
|
+
x: velocity.x * friction,
|
|
1107
|
+
y: velocity.y * friction,
|
|
1108
|
+
};
|
|
1109
|
+
|
|
1110
|
+
return {
|
|
1111
|
+
nextVelocity,
|
|
1112
|
+
delta: {
|
|
1113
|
+
x: nextVelocity.x * frameTime,
|
|
1114
|
+
y: nextVelocity.y * frameTime,
|
|
1115
|
+
},
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/**
|
|
1120
|
+
* Calculates instantaneous velocity from position change.
|
|
1121
|
+
*
|
|
1122
|
+
* @param lastPos - Previous position.
|
|
1123
|
+
* @param lastPos.x - Horizontal position.
|
|
1124
|
+
* @param lastPos.y - Vertical position.
|
|
1125
|
+
* @param currentPos - Current position.
|
|
1126
|
+
* @param currentPos.x - Horizontal position.
|
|
1127
|
+
* @param currentPos.y - Vertical position.
|
|
1128
|
+
* @param dt - Time delta in ms.
|
|
1129
|
+
* @returns Calculated velocity {x, y}.
|
|
1130
|
+
*/
|
|
1131
|
+
export function calculateInstantaneousVelocity(
|
|
1132
|
+
lastPos: { x: number; y: number; },
|
|
1133
|
+
currentPos: { x: number; y: number; },
|
|
1134
|
+
dt: number,
|
|
1135
|
+
) {
|
|
1136
|
+
if (dt <= 0) {
|
|
1137
|
+
return { x: 0, y: 0 };
|
|
1138
|
+
}
|
|
1139
|
+
return {
|
|
1140
|
+
x: (lastPos.x - currentPos.x) / dt,
|
|
1141
|
+
y: (lastPos.y - currentPos.y) / dt,
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
/**
|
|
1146
|
+
* Resolves the target index and alignment for snapping on a single axis.
|
|
1147
|
+
*
|
|
1148
|
+
* @param mode - The snap mode ('start', 'center', 'end', 'auto').
|
|
1149
|
+
* @param dir - The scroll direction on this axis.
|
|
1150
|
+
* @param currentIdx - The current first visible index.
|
|
1151
|
+
* @param currentEndIdx - The current last visible index.
|
|
1152
|
+
* @param relScroll - The current virtual scroll offset.
|
|
1153
|
+
* @param viewSize - The viewport dimension.
|
|
1154
|
+
* @param count - Total number of items/columns.
|
|
1155
|
+
* @param getSize - Resolver for item size.
|
|
1156
|
+
* @param getQuery - Resolver for item prefix sum.
|
|
1157
|
+
* @param getIndexAt - Resolver for index at a virtual offset.
|
|
1158
|
+
* @returns Snap result or null.
|
|
1159
|
+
*/
|
|
1160
|
+
export function resolveSnap(
|
|
1161
|
+
mode: SnapMode,
|
|
1162
|
+
dir: 'start' | 'end' | null,
|
|
1163
|
+
currentIdx: number,
|
|
1164
|
+
currentEndIdx: number,
|
|
1165
|
+
relScroll: number,
|
|
1166
|
+
viewSize: number,
|
|
1167
|
+
count: number,
|
|
1168
|
+
getSize: (i: number) => number,
|
|
1169
|
+
getQuery: (i: number) => number,
|
|
1170
|
+
getIndexAt: (o: number) => number,
|
|
1171
|
+
): SnapResult | null {
|
|
1172
|
+
let effectiveMode = mode;
|
|
1173
|
+
if (mode === 'auto') {
|
|
1174
|
+
if (dir === 'start') {
|
|
1175
|
+
effectiveMode = 'end';
|
|
1176
|
+
} else if (dir === 'end') {
|
|
1177
|
+
effectiveMode = 'start';
|
|
1178
|
+
} else {
|
|
1179
|
+
return null;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
if (effectiveMode === 'start') {
|
|
1184
|
+
const size = getSize(currentIdx);
|
|
1185
|
+
// Ignore items larger than viewport to prevent jarring jumps
|
|
1186
|
+
if (size > viewSize) {
|
|
1187
|
+
return null;
|
|
1188
|
+
}
|
|
1189
|
+
const visibleAmount = getQuery(currentIdx) + size - relScroll;
|
|
1190
|
+
return {
|
|
1191
|
+
index: Math.min(count - 1, visibleAmount / size >= 0.5 ? currentIdx : currentIdx + 1),
|
|
1192
|
+
align: 'start' as const,
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
if (effectiveMode === 'end') {
|
|
1196
|
+
const size = getSize(currentEndIdx);
|
|
1197
|
+
if (size > viewSize) {
|
|
1198
|
+
return null;
|
|
1199
|
+
}
|
|
1200
|
+
const visibleAmount = relScroll + viewSize - getQuery(currentEndIdx);
|
|
1201
|
+
return {
|
|
1202
|
+
index: Math.max(0, visibleAmount / size >= 0.5 ? currentEndIdx : currentEndIdx - 1),
|
|
1203
|
+
align: 'end' as const,
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
if (effectiveMode === 'center') {
|
|
1207
|
+
const center = relScroll + viewSize / 2;
|
|
1208
|
+
const idx = Math.max(0, Math.min(count - 1, getIndexAt(center)));
|
|
1209
|
+
const size = getSize(idx);
|
|
1210
|
+
if (size > viewSize) {
|
|
1211
|
+
return null;
|
|
1212
|
+
}
|
|
1213
|
+
return { index: idx, align: 'center' as const };
|
|
1214
|
+
}
|
|
1215
|
+
return null;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
/**
|
|
1219
|
+
* Helper to get the index at a virtual offset.
|
|
1220
|
+
*
|
|
1221
|
+
* @param offset - Virtual offset.
|
|
1222
|
+
* @param fixedSize - Fixed size if any.
|
|
1223
|
+
* @param gap - Gap size.
|
|
1224
|
+
* @param findLowerBound - Binary search resolver.
|
|
1225
|
+
* @returns Index at offset.
|
|
1226
|
+
*/
|
|
1227
|
+
export function calculateIndexAt(
|
|
1228
|
+
offset: number,
|
|
1229
|
+
fixedSize: number | null,
|
|
1230
|
+
gap: number,
|
|
1231
|
+
findLowerBound: (offset: number) => number,
|
|
1232
|
+
): number {
|
|
1233
|
+
const step = (fixedSize || 0) + gap;
|
|
1234
|
+
if (fixedSize !== null && step > 0) {
|
|
1235
|
+
return Math.floor(offset / step);
|
|
1236
|
+
}
|
|
1237
|
+
return findLowerBound(offset);
|
|
1238
|
+
}
|