@yorozu/virtual-list 1.0.25
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/LICENSE +21 -0
- package/README.md +57 -0
- package/edge-scroll.d.ts +37 -0
- package/index.d.ts +7 -0
- package/index.js +551 -0
- package/list.d.ts +47 -0
- package/package.json +21 -0
- package/range.d.ts +15 -0
- package/slice.d.ts +44 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ivan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# @yorozu/virtual-list
|
|
2
|
+
|
|
3
|
+
Framework-free id-window engine for absolute-row lists. You own the DOM: paint `top` from `rowTop`, size the sizer from `totalSize`, and iterate `viewportIds`. This package does not render, does not run FLIP, and does not own sortable.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @yorozu/virtual-list
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Slice + factory
|
|
12
|
+
|
|
13
|
+
Use `createVirtualList` from a scroll handler. Number `itemSize` is O(1). Pass a function for two-height rows; the engine rebuilds a prefix on `sync` and lower-bounds it on scroll.
|
|
14
|
+
|
|
15
|
+
`onVisibleRange` is the mounted window. `onPaintedRange` is the rows intersecting the viewport (use this for presence / warm). Edge growth is math-only. Idle trim recenters to ~2× `listSlice` after `DEFAULT_IDLE_TRIM_MS` (150). `listSliceForViewport` is applied on idle, not mid-fling.
|
|
16
|
+
|
|
17
|
+
Pass `getFromTranslateY` through to `@yorozu/animations` `createListReorder`. Snap `itemSize` during a density tween; do not feed fractional height into `rowTop` until settle.
|
|
18
|
+
|
|
19
|
+
`slotIndex` is a stable integer for a mounted id so a host can rebind a row without remounting. The engine does not pool components.
|
|
20
|
+
|
|
21
|
+
History / variable-height bubbles are out of scope. Do not translate the window (`offsetY`). Absolute `top` is the layout.
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { createVirtualList } from "@yorozu/virtual-list"
|
|
25
|
+
|
|
26
|
+
let items = Array.from({ length: 1000 }, (_, i) => `row-${i}`)
|
|
27
|
+
let scroller = document.getElementById("scroller")!
|
|
28
|
+
let sizer = document.getElementById("sizer")!
|
|
29
|
+
let rowNodes = new Map<string, HTMLElement>()
|
|
30
|
+
|
|
31
|
+
let list = createVirtualList({
|
|
32
|
+
getItems: () => items,
|
|
33
|
+
itemSize: 48,
|
|
34
|
+
onChange: () => paint(),
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
list.sync()
|
|
38
|
+
|
|
39
|
+
scroller.addEventListener("scroll", () => {
|
|
40
|
+
list.onScroll({
|
|
41
|
+
scrollTop: scroller.scrollTop,
|
|
42
|
+
viewportHeight: scroller.clientHeight,
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
function paint(): void {
|
|
47
|
+
sizer.style.height = `${list.totalSize()}px`
|
|
48
|
+
let from = list.fromOffset()
|
|
49
|
+
let ids = list.viewportIds() ?? []
|
|
50
|
+
for (let i = 0; i < ids.length; i++) {
|
|
51
|
+
let id = ids[i]!
|
|
52
|
+
let node = rowNodes.get(id)
|
|
53
|
+
if (node === undefined) continue
|
|
54
|
+
node.style.top = `${list.rowTop(from + i)}px`
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
package/edge-scroll.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { LoadDirection } from './slice';
|
|
2
|
+
export declare const DEFAULT_SENSITIVE_AREA_PX: number;
|
|
3
|
+
export declare const DEFAULT_EDGE_DEBOUNCE_MS: number;
|
|
4
|
+
export declare const DEFAULT_IDLE_TRIM_MS: number;
|
|
5
|
+
export type EdgeScrollState = {
|
|
6
|
+
isScrollTopJustUpdated?: boolean;
|
|
7
|
+
lastScrollTop?: number;
|
|
8
|
+
};
|
|
9
|
+
export declare function leadingDebounce<A extends unknown[]>(fn: (...args: A) => void, ms: number): (...args: A) => void;
|
|
10
|
+
export declare function createEdgeDebouncedLoaders(onLoadMore: (args: {
|
|
11
|
+
direction: LoadDirection;
|
|
12
|
+
noScroll?: boolean;
|
|
13
|
+
}) => void, debounceMs?: number): {
|
|
14
|
+
loadMoreForwards: () => void;
|
|
15
|
+
loadMoreBackwards: (noScroll?: boolean) => void;
|
|
16
|
+
};
|
|
17
|
+
export declare function handleEdgeScroll(args: {
|
|
18
|
+
scrollTop: number;
|
|
19
|
+
viewportHeight: number;
|
|
20
|
+
itemHeight: number;
|
|
21
|
+
fromOffset: number;
|
|
22
|
+
mountedCount: number;
|
|
23
|
+
sensitiveArea: number;
|
|
24
|
+
state: EdgeScrollState;
|
|
25
|
+
loadMoreForwards: () => void;
|
|
26
|
+
loadMoreBackwards: (noScroll?: boolean) => void;
|
|
27
|
+
mountedTopPx?: number;
|
|
28
|
+
mountedBottomPx?: number;
|
|
29
|
+
}): boolean;
|
|
30
|
+
export declare function maybePreloadBackwards(args: {
|
|
31
|
+
itemCount: number;
|
|
32
|
+
preloadBackwards: number;
|
|
33
|
+
loadMoreBackwards: (noScroll?: boolean) => void;
|
|
34
|
+
itemHeight?: number;
|
|
35
|
+
viewportHeight?: number;
|
|
36
|
+
scrollContainer?: HTMLElement | null;
|
|
37
|
+
}): boolean;
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type { LoadDirection, ViewportSliceResult, ViewportIdSliceControllerOptions } from './slice';
|
|
2
|
+
export { DEFAULT_LIST_SLICE, DEFAULT_MAX_MOUNTED_FACTOR, areIdArraysEqual, reuseIfEqual, getViewportSlice, ViewportIdSliceController, } from './slice';
|
|
3
|
+
export type { EdgeScrollState } from './edge-scroll';
|
|
4
|
+
export { DEFAULT_SENSITIVE_AREA_PX, DEFAULT_EDGE_DEBOUNCE_MS, DEFAULT_IDLE_TRIM_MS, leadingDebounce, createEdgeDebouncedLoaders, handleEdgeScroll, maybePreloadBackwards, } from './edge-scroll';
|
|
5
|
+
export { LIST_SLICE_MIN, LIST_SLICE_MAX, LIST_SLICE_OVERSCAN_ROWS, heightPrefix, firstVisibleIndexFromPrefix, rowTopFromPrefix, paintedIndexRange, listSliceForViewport, } from './range';
|
|
6
|
+
export type { VirtualListRange, VirtualListOptions, VirtualList } from './list';
|
|
7
|
+
export { createVirtualList } from './list';
|
package/index.js
ADDED
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
//#region src/slice.ts
|
|
2
|
+
var DEFAULT_LIST_SLICE = 30;
|
|
3
|
+
var DEFAULT_MAX_MOUNTED_FACTOR = 3;
|
|
4
|
+
function areIdArraysEqual(a, b) {
|
|
5
|
+
if (a === b) return true;
|
|
6
|
+
if (a === void 0 || b === void 0) return false;
|
|
7
|
+
if (a.length !== b.length) return false;
|
|
8
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
function reuseIfEqual(prev, next) {
|
|
12
|
+
return areIdArraysEqual(prev, next) && prev !== void 0 ? prev : next;
|
|
13
|
+
}
|
|
14
|
+
function getViewportSlice(sourceIds, direction, listSlice, offsetId) {
|
|
15
|
+
let length = sourceIds.length;
|
|
16
|
+
let index = offsetId !== void 0 ? sourceIds.indexOf(offsetId) : 0;
|
|
17
|
+
let indexForDirection = direction === "forwards" ? index : index + 1 || length;
|
|
18
|
+
let from = Math.max(0, indexForDirection - listSlice);
|
|
19
|
+
let to = indexForDirection + listSlice - 1;
|
|
20
|
+
let viewportIds = sourceIds.slice(from, to + 1);
|
|
21
|
+
let isOnTop = viewportIds[0] === sourceIds[0];
|
|
22
|
+
let areSomeLocal;
|
|
23
|
+
let areAllLocal;
|
|
24
|
+
if (direction === "forwards") {
|
|
25
|
+
areSomeLocal = indexForDirection >= 0;
|
|
26
|
+
areAllLocal = from >= 0;
|
|
27
|
+
} else {
|
|
28
|
+
areSomeLocal = indexForDirection < length;
|
|
29
|
+
areAllLocal = to <= length - 1;
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
viewportIds,
|
|
33
|
+
fromOffset: from,
|
|
34
|
+
isOnTop,
|
|
35
|
+
areSomeLocal,
|
|
36
|
+
areAllLocal
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function pickStillPresentOffset(sourceIds, viewportIds) {
|
|
40
|
+
if (viewportIds === void 0 || viewportIds.length === 0) return sourceIds[0];
|
|
41
|
+
let mid = viewportIds[Math.round(viewportIds.length / 2)];
|
|
42
|
+
if (mid !== void 0 && sourceIds.includes(mid)) return mid;
|
|
43
|
+
for (let id of viewportIds) if (sourceIds.includes(id)) return id;
|
|
44
|
+
return sourceIds[0];
|
|
45
|
+
}
|
|
46
|
+
var ViewportIdSliceController = class {
|
|
47
|
+
_listSlice;
|
|
48
|
+
_loadMoreBackwards;
|
|
49
|
+
_viewportIds;
|
|
50
|
+
_fromOffset = 0;
|
|
51
|
+
_isOnTop = true;
|
|
52
|
+
_isDisabled = false;
|
|
53
|
+
_sourceIds;
|
|
54
|
+
constructor(options) {
|
|
55
|
+
this._listSlice = options?.listSlice ?? 30;
|
|
56
|
+
this._loadMoreBackwards = options?.loadMoreBackwards;
|
|
57
|
+
}
|
|
58
|
+
get viewportIds() {
|
|
59
|
+
return this._viewportIds;
|
|
60
|
+
}
|
|
61
|
+
get fromOffset() {
|
|
62
|
+
return this._fromOffset;
|
|
63
|
+
}
|
|
64
|
+
get isOnTop() {
|
|
65
|
+
return this._isOnTop;
|
|
66
|
+
}
|
|
67
|
+
setListSlice(n) {
|
|
68
|
+
if (n <= 0) return;
|
|
69
|
+
this._listSlice = n;
|
|
70
|
+
}
|
|
71
|
+
sync(sourceIds, isDisabled = false) {
|
|
72
|
+
if (sourceIds === void 0) {
|
|
73
|
+
let changed = this._viewportIds !== void 0 || this._fromOffset !== 0 || this._isOnTop !== true;
|
|
74
|
+
this._viewportIds = void 0;
|
|
75
|
+
this._fromOffset = 0;
|
|
76
|
+
this._isOnTop = true;
|
|
77
|
+
this._isDisabled = false;
|
|
78
|
+
this._sourceIds = void 0;
|
|
79
|
+
return changed;
|
|
80
|
+
}
|
|
81
|
+
if (areIdArraysEqual(this._sourceIds, sourceIds) && this._isDisabled === isDisabled && this._viewportIds !== void 0) return false;
|
|
82
|
+
this._sourceIds = sourceIds;
|
|
83
|
+
this._isDisabled = isDisabled;
|
|
84
|
+
if (isDisabled) return this._commitWindow(sourceIds, 0, true);
|
|
85
|
+
let offsetId;
|
|
86
|
+
if (this._viewportIds === void 0) offsetId = sourceIds[0];
|
|
87
|
+
else offsetId = pickStillPresentOffset(sourceIds, this._viewportIds);
|
|
88
|
+
let result = getViewportSlice(sourceIds, "forwards", this._listSlice, offsetId);
|
|
89
|
+
return this._commitWindow(result.viewportIds, result.fromOffset, result.isOnTop);
|
|
90
|
+
}
|
|
91
|
+
getMore(sourceIds, args) {
|
|
92
|
+
if (sourceIds === void 0 || sourceIds.length === 0) {
|
|
93
|
+
this._loadMoreBackwards?.({ offsetId: void 0 });
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
this._sourceIds = sourceIds;
|
|
97
|
+
let from = this._fromOffset;
|
|
98
|
+
let end = from + (this._viewportIds?.length ?? 0);
|
|
99
|
+
let maxMounted = this._listSlice * 3;
|
|
100
|
+
let nextFrom = from;
|
|
101
|
+
let nextEnd = end;
|
|
102
|
+
let direction = args.direction;
|
|
103
|
+
if (direction === "backwards") {
|
|
104
|
+
nextEnd = Math.min(sourceIds.length, end + this._listSlice);
|
|
105
|
+
if (nextEnd - nextFrom > maxMounted) nextFrom = nextEnd - maxMounted;
|
|
106
|
+
} else {
|
|
107
|
+
nextFrom = Math.max(0, from - this._listSlice);
|
|
108
|
+
if (nextEnd - nextFrom > maxMounted) nextEnd = nextFrom + maxMounted;
|
|
109
|
+
}
|
|
110
|
+
nextFrom = Math.max(0, nextFrom);
|
|
111
|
+
nextEnd = Math.min(sourceIds.length, Math.max(nextFrom, nextEnd));
|
|
112
|
+
let nextIds = sourceIds.slice(nextFrom, nextEnd);
|
|
113
|
+
if (direction === "backwards" && end + this._listSlice > sourceIds.length) {
|
|
114
|
+
let offsetId = sourceIds[sourceIds.length - 1];
|
|
115
|
+
this._loadMoreBackwards?.({ offsetId });
|
|
116
|
+
}
|
|
117
|
+
return this._commitWindow(nextIds, nextFrom, nextIds[0] === sourceIds[0]);
|
|
118
|
+
}
|
|
119
|
+
reanchorAtIndex(sourceIds, index) {
|
|
120
|
+
let offsetId = sourceIds[Math.max(0, Math.min(index, sourceIds.length - 1))];
|
|
121
|
+
let result = getViewportSlice(sourceIds, "forwards", this._listSlice, offsetId);
|
|
122
|
+
this._sourceIds = sourceIds;
|
|
123
|
+
return this._commitWindow(result.viewportIds, result.fromOffset, result.isOnTop);
|
|
124
|
+
}
|
|
125
|
+
trimToSlice(sourceIds, firstVisibleIndex) {
|
|
126
|
+
return this.reanchorAtIndex(sourceIds, firstVisibleIndex);
|
|
127
|
+
}
|
|
128
|
+
resetToTop(sourceIds) {
|
|
129
|
+
let result = getViewportSlice(sourceIds, "forwards", this._listSlice, sourceIds[0]);
|
|
130
|
+
this._sourceIds = sourceIds;
|
|
131
|
+
this._commitWindow(result.viewportIds, result.fromOffset, result.isOnTop);
|
|
132
|
+
}
|
|
133
|
+
_commitWindow(ids, from, isOnTop) {
|
|
134
|
+
let prevIds = this._viewportIds;
|
|
135
|
+
let prevFrom = this._fromOffset;
|
|
136
|
+
let prevOnTop = this._isOnTop;
|
|
137
|
+
this._viewportIds = reuseIfEqual(prevIds, ids);
|
|
138
|
+
this._fromOffset = from;
|
|
139
|
+
this._isOnTop = isOnTop;
|
|
140
|
+
return !areIdArraysEqual(prevIds, this._viewportIds) || prevFrom !== from || prevOnTop !== isOnTop;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
//#endregion
|
|
144
|
+
//#region src/edge-scroll.ts
|
|
145
|
+
var DEFAULT_SENSITIVE_AREA_PX = 800;
|
|
146
|
+
var DEFAULT_EDGE_DEBOUNCE_MS = 150;
|
|
147
|
+
var DEFAULT_IDLE_TRIM_MS = 150;
|
|
148
|
+
function leadingDebounce(fn, ms) {
|
|
149
|
+
let lastCall;
|
|
150
|
+
return (...args) => {
|
|
151
|
+
let now = Date.now();
|
|
152
|
+
if (lastCall !== void 0 && now - lastCall < ms) return;
|
|
153
|
+
lastCall = now;
|
|
154
|
+
fn(...args);
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function createEdgeDebouncedLoaders(onLoadMore, debounceMs = 150) {
|
|
158
|
+
return {
|
|
159
|
+
loadMoreForwards: leadingDebounce(() => {
|
|
160
|
+
onLoadMore({ direction: "forwards" });
|
|
161
|
+
}, debounceMs),
|
|
162
|
+
loadMoreBackwards: leadingDebounce((noScroll = false) => {
|
|
163
|
+
onLoadMore({
|
|
164
|
+
direction: "backwards",
|
|
165
|
+
noScroll
|
|
166
|
+
});
|
|
167
|
+
}, debounceMs)
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function handleEdgeScroll(args) {
|
|
171
|
+
let { scrollTop, viewportHeight, itemHeight, fromOffset, mountedCount, sensitiveArea, state, loadMoreForwards, loadMoreBackwards, mountedTopPx, mountedBottomPx } = args;
|
|
172
|
+
if (state.isScrollTopJustUpdated) {
|
|
173
|
+
state.isScrollTopJustUpdated = false;
|
|
174
|
+
state.lastScrollTop = scrollTop;
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
if (mountedCount <= 0 || viewportHeight <= 0 || !(mountedTopPx !== void 0 && mountedBottomPx !== void 0) && itemHeight <= 0) {
|
|
178
|
+
state.lastScrollTop = scrollTop;
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
let lastScrollTop = state.lastScrollTop;
|
|
182
|
+
if (lastScrollTop === void 0) {
|
|
183
|
+
state.lastScrollTop = scrollTop;
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
let topPx = mountedTopPx ?? fromOffset * itemHeight;
|
|
187
|
+
let bottomPx = mountedBottomPx ?? (fromOffset + mountedCount) * itemHeight;
|
|
188
|
+
let nearTop = scrollTop <= topPx + sensitiveArea;
|
|
189
|
+
let nearBottom = bottomPx - (scrollTop + viewportHeight) <= sensitiveArea;
|
|
190
|
+
let movingUp = scrollTop < lastScrollTop;
|
|
191
|
+
let movingDown = scrollTop > lastScrollTop;
|
|
192
|
+
let fired = false;
|
|
193
|
+
if (movingUp && nearTop) {
|
|
194
|
+
loadMoreForwards();
|
|
195
|
+
fired = true;
|
|
196
|
+
} else if (movingDown && nearBottom) {
|
|
197
|
+
loadMoreBackwards();
|
|
198
|
+
fired = true;
|
|
199
|
+
}
|
|
200
|
+
state.lastScrollTop = scrollTop;
|
|
201
|
+
return fired;
|
|
202
|
+
}
|
|
203
|
+
function maybePreloadBackwards(args) {
|
|
204
|
+
let { itemCount, preloadBackwards, loadMoreBackwards, itemHeight, viewportHeight, scrollContainer } = args;
|
|
205
|
+
if (preloadBackwards > 0 && itemCount < preloadBackwards) {
|
|
206
|
+
loadMoreBackwards(true);
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
if (itemHeight !== void 0 && viewportHeight !== void 0 && itemHeight > 0 && viewportHeight > 0 && itemCount * itemHeight < viewportHeight) {
|
|
210
|
+
loadMoreBackwards();
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
if (scrollContainer != null) {
|
|
214
|
+
let { scrollHeight, clientHeight } = scrollContainer;
|
|
215
|
+
if (scrollHeight > 0 && clientHeight > 0 && scrollHeight < clientHeight) {
|
|
216
|
+
loadMoreBackwards();
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
//#endregion
|
|
223
|
+
//#region src/range.ts
|
|
224
|
+
var LIST_SLICE_MIN = 16;
|
|
225
|
+
var LIST_SLICE_MAX = 48;
|
|
226
|
+
var LIST_SLICE_OVERSCAN_ROWS = 4;
|
|
227
|
+
function heightPrefix(count, heightAt) {
|
|
228
|
+
let prefix = new Array(count + 1);
|
|
229
|
+
prefix[0] = 0;
|
|
230
|
+
for (let i = 0; i < count; i++) prefix[i + 1] = prefix[i] + heightAt(i);
|
|
231
|
+
return prefix;
|
|
232
|
+
}
|
|
233
|
+
function firstVisibleIndexFromPrefix(prefix, scrollTop) {
|
|
234
|
+
let count = prefix.length - 1;
|
|
235
|
+
if (count <= 0 || scrollTop <= 0) return 0;
|
|
236
|
+
let lo = 0;
|
|
237
|
+
let hi = count - 1;
|
|
238
|
+
while (lo < hi) {
|
|
239
|
+
let mid = lo + hi >>> 1;
|
|
240
|
+
if (prefix[mid + 1] > scrollTop) hi = mid;
|
|
241
|
+
else lo = mid + 1;
|
|
242
|
+
}
|
|
243
|
+
return lo;
|
|
244
|
+
}
|
|
245
|
+
function rowTopFromPrefix(prefix, index) {
|
|
246
|
+
if (prefix.length === 0) return 0;
|
|
247
|
+
return prefix[Math.max(0, Math.min(index, prefix.length - 1))];
|
|
248
|
+
}
|
|
249
|
+
function paintedIndexRange(scrollTop, viewportHeight, itemHeight, count) {
|
|
250
|
+
if (itemHeight <= 0 || viewportHeight <= 0 || count <= 0) return {
|
|
251
|
+
start: 0,
|
|
252
|
+
end: 0
|
|
253
|
+
};
|
|
254
|
+
let start = Math.max(0, Math.floor(scrollTop / itemHeight));
|
|
255
|
+
let end = Math.min(count, Math.ceil((scrollTop + viewportHeight) / itemHeight));
|
|
256
|
+
if (start > count) start = count;
|
|
257
|
+
if (end < start) end = start;
|
|
258
|
+
return {
|
|
259
|
+
start,
|
|
260
|
+
end
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
function listSliceForViewport(viewportHeight, itemHeight, opts) {
|
|
264
|
+
let min = opts?.min ?? 16;
|
|
265
|
+
let max = opts?.max ?? 48;
|
|
266
|
+
let overscanRows = opts?.overscanRows ?? 4;
|
|
267
|
+
if (viewportHeight <= 0 || itemHeight <= 0) return min;
|
|
268
|
+
let raw = Math.ceil(viewportHeight / itemHeight) + overscanRows;
|
|
269
|
+
return Math.max(min, Math.min(max, raw));
|
|
270
|
+
}
|
|
271
|
+
//#endregion
|
|
272
|
+
//#region src/list.ts
|
|
273
|
+
var DEFAULT_NEAR_END_THRESHOLD = 10;
|
|
274
|
+
function defaultScheduleIdle(fn, timeoutMs) {
|
|
275
|
+
let timer = setTimeout(fn, timeoutMs);
|
|
276
|
+
return { cancel() {
|
|
277
|
+
clearTimeout(timer);
|
|
278
|
+
} };
|
|
279
|
+
}
|
|
280
|
+
function exclusiveEndFromPrefix(prefix, viewportBottom) {
|
|
281
|
+
let count = prefix.length - 1;
|
|
282
|
+
if (count <= 0 || viewportBottom <= 0) return 0;
|
|
283
|
+
let lo = 0;
|
|
284
|
+
let hi = count;
|
|
285
|
+
while (lo < hi) {
|
|
286
|
+
let mid = lo + hi >>> 1;
|
|
287
|
+
if (prefix[mid] >= viewportBottom) hi = mid;
|
|
288
|
+
else lo = mid + 1;
|
|
289
|
+
}
|
|
290
|
+
return lo;
|
|
291
|
+
}
|
|
292
|
+
function createVirtualList(options) {
|
|
293
|
+
let listSlice = options.listSlice ?? 30;
|
|
294
|
+
let sensitiveArea = options.sensitiveArea ?? 800;
|
|
295
|
+
let idleTrimMs = options.idleTrimMs ?? 150;
|
|
296
|
+
let nearEndThreshold = options.nearEndThreshold ?? DEFAULT_NEAR_END_THRESHOLD;
|
|
297
|
+
let scheduleIdle = options.scheduleIdle ?? defaultScheduleIdle;
|
|
298
|
+
let controller = new ViewportIdSliceController({
|
|
299
|
+
listSlice: options.listSlice,
|
|
300
|
+
loadMoreBackwards: options.loadMoreBackwards
|
|
301
|
+
});
|
|
302
|
+
let slots = /* @__PURE__ */ new Map();
|
|
303
|
+
let freeSlots = [];
|
|
304
|
+
let nextSlot = 0;
|
|
305
|
+
let prefix;
|
|
306
|
+
let lastScrollTop = 0;
|
|
307
|
+
let lastViewportHeight;
|
|
308
|
+
let lastPainted;
|
|
309
|
+
let lastNearEndLength;
|
|
310
|
+
let idleHandle;
|
|
311
|
+
let edgeState = {};
|
|
312
|
+
let { loadMoreForwards, loadMoreBackwards } = createEdgeDebouncedLoaders((args) => {
|
|
313
|
+
getMore(args.direction, args.noScroll);
|
|
314
|
+
}, options.edgeDebounceMs ?? 150);
|
|
315
|
+
function sourceIds() {
|
|
316
|
+
return options.getItems().slice();
|
|
317
|
+
}
|
|
318
|
+
function viewportIds() {
|
|
319
|
+
return controller.viewportIds;
|
|
320
|
+
}
|
|
321
|
+
function fromOffset() {
|
|
322
|
+
return controller.fromOffset;
|
|
323
|
+
}
|
|
324
|
+
function isOnTop() {
|
|
325
|
+
return controller.isOnTop;
|
|
326
|
+
}
|
|
327
|
+
function totalSize() {
|
|
328
|
+
let itemSize = options.itemSize;
|
|
329
|
+
if (typeof itemSize === "number") return options.getItems().length * itemSize;
|
|
330
|
+
if (prefix === void 0 || prefix.length === 0) return 0;
|
|
331
|
+
return prefix[prefix.length - 1];
|
|
332
|
+
}
|
|
333
|
+
function rowTop(index) {
|
|
334
|
+
let itemSize = options.itemSize;
|
|
335
|
+
if (typeof itemSize === "number") return index * itemSize;
|
|
336
|
+
if (prefix === void 0) return 0;
|
|
337
|
+
return rowTopFromPrefix(prefix, index);
|
|
338
|
+
}
|
|
339
|
+
function rowHeight(index) {
|
|
340
|
+
let itemSize = options.itemSize;
|
|
341
|
+
if (typeof itemSize === "number") return itemSize;
|
|
342
|
+
if (prefix === void 0) return 0;
|
|
343
|
+
let top = prefix[index];
|
|
344
|
+
let next = prefix[index + 1];
|
|
345
|
+
if (top === void 0 || next === void 0) return 0;
|
|
346
|
+
return next - top;
|
|
347
|
+
}
|
|
348
|
+
function slotIndex(id) {
|
|
349
|
+
return slots.get(id);
|
|
350
|
+
}
|
|
351
|
+
function fromTranslateY(key) {
|
|
352
|
+
return options.getFromTranslateY?.(key);
|
|
353
|
+
}
|
|
354
|
+
function firstVisibleIndex() {
|
|
355
|
+
let count = options.getItems().length;
|
|
356
|
+
if (count <= 0) return 0;
|
|
357
|
+
let itemSize = options.itemSize;
|
|
358
|
+
if (typeof itemSize === "number") {
|
|
359
|
+
if (itemSize <= 0) return 0;
|
|
360
|
+
return Math.max(0, Math.min(count - 1, Math.floor(lastScrollTop / itemSize)));
|
|
361
|
+
}
|
|
362
|
+
if (prefix === void 0) return 0;
|
|
363
|
+
return firstVisibleIndexFromPrefix(prefix, lastScrollTop);
|
|
364
|
+
}
|
|
365
|
+
function reassignSlots() {
|
|
366
|
+
let ids = controller.viewportIds;
|
|
367
|
+
if (ids === void 0) {
|
|
368
|
+
slots.clear();
|
|
369
|
+
freeSlots = [];
|
|
370
|
+
nextSlot = 0;
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
let mounted = new Set(ids);
|
|
374
|
+
for (let [id, slot] of [...slots.entries()]) {
|
|
375
|
+
if (mounted.has(id)) continue;
|
|
376
|
+
slots.delete(id);
|
|
377
|
+
freeSlots.push(slot);
|
|
378
|
+
}
|
|
379
|
+
if (freeSlots.length > 1) freeSlots.sort((a, b) => a - b);
|
|
380
|
+
for (let id of ids) {
|
|
381
|
+
if (slots.has(id)) continue;
|
|
382
|
+
let slot = freeSlots.length > 0 ? freeSlots.shift() : nextSlot++;
|
|
383
|
+
slots.set(id, slot);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
function notify() {
|
|
387
|
+
reassignSlots();
|
|
388
|
+
let mountedCount = controller.viewportIds?.length ?? 0;
|
|
389
|
+
options.onVisibleRange?.({
|
|
390
|
+
start: controller.fromOffset,
|
|
391
|
+
end: controller.fromOffset + mountedCount
|
|
392
|
+
});
|
|
393
|
+
options.onChange?.();
|
|
394
|
+
}
|
|
395
|
+
function notifyIfChanged(changed) {
|
|
396
|
+
if (changed) notify();
|
|
397
|
+
return changed;
|
|
398
|
+
}
|
|
399
|
+
function preloadIfNeeded() {
|
|
400
|
+
if (lastViewportHeight === void 0 || lastViewportHeight <= 0) return;
|
|
401
|
+
let itemSize = options.itemSize;
|
|
402
|
+
let itemHeight = typeof itemSize === "number" ? itemSize : rowHeight(0);
|
|
403
|
+
maybePreloadBackwards({
|
|
404
|
+
itemCount: controller.viewportIds?.length ?? 0,
|
|
405
|
+
preloadBackwards: listSlice,
|
|
406
|
+
loadMoreBackwards,
|
|
407
|
+
itemHeight,
|
|
408
|
+
viewportHeight: lastViewportHeight
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
function cancelIdle() {
|
|
412
|
+
idleHandle?.cancel();
|
|
413
|
+
idleHandle = void 0;
|
|
414
|
+
}
|
|
415
|
+
function applyIdleTrim() {
|
|
416
|
+
let itemSize = options.itemSize;
|
|
417
|
+
if (typeof itemSize === "number" && lastViewportHeight !== void 0 && lastViewportHeight > 0) {
|
|
418
|
+
let nextSlice = listSliceForViewport(lastViewportHeight, itemSize);
|
|
419
|
+
listSlice = nextSlice;
|
|
420
|
+
controller.setListSlice(nextSlice);
|
|
421
|
+
}
|
|
422
|
+
notifyIfChanged(controller.trimToSlice(sourceIds(), firstVisibleIndex()));
|
|
423
|
+
}
|
|
424
|
+
function scheduleIdleTrim() {
|
|
425
|
+
cancelIdle();
|
|
426
|
+
idleHandle = scheduleIdle(() => {
|
|
427
|
+
idleHandle = void 0;
|
|
428
|
+
applyIdleTrim();
|
|
429
|
+
}, idleTrimMs);
|
|
430
|
+
}
|
|
431
|
+
function firePaintedRange(scrollTop, viewportHeight) {
|
|
432
|
+
let onPaintedRange = options.onPaintedRange;
|
|
433
|
+
if (onPaintedRange === void 0) return;
|
|
434
|
+
let count = options.getItems().length;
|
|
435
|
+
let itemSize = options.itemSize;
|
|
436
|
+
let range;
|
|
437
|
+
if (typeof itemSize === "number") range = paintedIndexRange(scrollTop, viewportHeight, itemSize, count);
|
|
438
|
+
else if (prefix === void 0 || viewportHeight <= 0 || count <= 0) range = {
|
|
439
|
+
start: 0,
|
|
440
|
+
end: 0
|
|
441
|
+
};
|
|
442
|
+
else range = {
|
|
443
|
+
start: firstVisibleIndexFromPrefix(prefix, scrollTop),
|
|
444
|
+
end: exclusiveEndFromPrefix(prefix, scrollTop + viewportHeight)
|
|
445
|
+
};
|
|
446
|
+
if (lastPainted !== void 0 && lastPainted.start === range.start && lastPainted.end === range.end) return;
|
|
447
|
+
lastPainted = range;
|
|
448
|
+
onPaintedRange(range);
|
|
449
|
+
}
|
|
450
|
+
function maybeFireNearEnd(count) {
|
|
451
|
+
let onNearEnd = options.onNearEnd;
|
|
452
|
+
if (onNearEnd === void 0) return;
|
|
453
|
+
if (controller.fromOffset + (controller.viewportIds?.length ?? 0) >= count - nearEndThreshold) {
|
|
454
|
+
if (lastNearEndLength !== count) {
|
|
455
|
+
lastNearEndLength = count;
|
|
456
|
+
onNearEnd();
|
|
457
|
+
}
|
|
458
|
+
} else lastNearEndLength = void 0;
|
|
459
|
+
}
|
|
460
|
+
function sync(isDisabled) {
|
|
461
|
+
let source = sourceIds();
|
|
462
|
+
let itemSize = options.itemSize;
|
|
463
|
+
if (typeof itemSize === "function") prefix = heightPrefix(source.length, itemSize);
|
|
464
|
+
let changed = controller.sync(source, isDisabled);
|
|
465
|
+
notifyIfChanged(changed);
|
|
466
|
+
preloadIfNeeded();
|
|
467
|
+
return changed;
|
|
468
|
+
}
|
|
469
|
+
function onScroll(metrics) {
|
|
470
|
+
lastScrollTop = metrics.scrollTop;
|
|
471
|
+
lastViewportHeight = metrics.viewportHeight;
|
|
472
|
+
firePaintedRange(metrics.scrollTop, metrics.viewportHeight);
|
|
473
|
+
let count = options.getItems().length;
|
|
474
|
+
let from = controller.fromOffset;
|
|
475
|
+
let mountedCount = controller.viewportIds?.length ?? 0;
|
|
476
|
+
let first = firstVisibleIndex();
|
|
477
|
+
if (first < from - 2 || first >= from + mountedCount + 2) {
|
|
478
|
+
let changed = controller.reanchorAtIndex(sourceIds(), first);
|
|
479
|
+
edgeState.lastScrollTop = void 0;
|
|
480
|
+
scheduleIdleTrim();
|
|
481
|
+
notifyIfChanged(changed);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
let windowEnd = from + mountedCount;
|
|
485
|
+
let itemSize = options.itemSize;
|
|
486
|
+
handleEdgeScroll({
|
|
487
|
+
scrollTop: metrics.scrollTop,
|
|
488
|
+
viewportHeight: metrics.viewportHeight,
|
|
489
|
+
itemHeight: typeof itemSize === "number" ? itemSize : rowHeight(0),
|
|
490
|
+
fromOffset: from,
|
|
491
|
+
mountedCount,
|
|
492
|
+
sensitiveArea,
|
|
493
|
+
state: edgeState,
|
|
494
|
+
loadMoreForwards,
|
|
495
|
+
loadMoreBackwards,
|
|
496
|
+
mountedTopPx: rowTop(from),
|
|
497
|
+
mountedBottomPx: windowEnd >= count ? totalSize() : rowTop(windowEnd)
|
|
498
|
+
});
|
|
499
|
+
scheduleIdleTrim();
|
|
500
|
+
maybeFireNearEnd(count);
|
|
501
|
+
}
|
|
502
|
+
function reanchor(index) {
|
|
503
|
+
notifyIfChanged(controller.reanchorAtIndex(sourceIds(), index));
|
|
504
|
+
}
|
|
505
|
+
function resetToTop() {
|
|
506
|
+
let prevIds = controller.viewportIds;
|
|
507
|
+
let prevFrom = controller.fromOffset;
|
|
508
|
+
let prevOnTop = controller.isOnTop;
|
|
509
|
+
controller.resetToTop(sourceIds());
|
|
510
|
+
edgeState.isScrollTopJustUpdated = true;
|
|
511
|
+
edgeState.lastScrollTop = void 0;
|
|
512
|
+
notifyIfChanged(!areIdArraysEqual(prevIds, controller.viewportIds) || prevFrom !== controller.fromOffset || prevOnTop !== controller.isOnTop);
|
|
513
|
+
}
|
|
514
|
+
function getMore(direction, noScroll) {
|
|
515
|
+
return notifyIfChanged(controller.getMore(sourceIds(), {
|
|
516
|
+
direction,
|
|
517
|
+
noScroll
|
|
518
|
+
}));
|
|
519
|
+
}
|
|
520
|
+
function trimIdle(firstVisible) {
|
|
521
|
+
return notifyIfChanged(controller.trimToSlice(sourceIds(), firstVisible));
|
|
522
|
+
}
|
|
523
|
+
function setListSlice(n) {
|
|
524
|
+
if (n <= 0) return;
|
|
525
|
+
listSlice = n;
|
|
526
|
+
controller.setListSlice(n);
|
|
527
|
+
}
|
|
528
|
+
function destroy() {
|
|
529
|
+
cancelIdle();
|
|
530
|
+
}
|
|
531
|
+
return {
|
|
532
|
+
viewportIds,
|
|
533
|
+
fromOffset,
|
|
534
|
+
isOnTop,
|
|
535
|
+
totalSize,
|
|
536
|
+
rowTop,
|
|
537
|
+
rowHeight,
|
|
538
|
+
slotIndex,
|
|
539
|
+
fromTranslateY,
|
|
540
|
+
sync,
|
|
541
|
+
onScroll,
|
|
542
|
+
reanchor,
|
|
543
|
+
resetToTop,
|
|
544
|
+
getMore,
|
|
545
|
+
trimIdle,
|
|
546
|
+
setListSlice,
|
|
547
|
+
destroy
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
//#endregion
|
|
551
|
+
export { DEFAULT_EDGE_DEBOUNCE_MS, DEFAULT_IDLE_TRIM_MS, DEFAULT_LIST_SLICE, DEFAULT_MAX_MOUNTED_FACTOR, DEFAULT_SENSITIVE_AREA_PX, LIST_SLICE_MAX, LIST_SLICE_MIN, LIST_SLICE_OVERSCAN_ROWS, ViewportIdSliceController, areIdArraysEqual, createEdgeDebouncedLoaders, createVirtualList, firstVisibleIndexFromPrefix, getViewportSlice, handleEdgeScroll, heightPrefix, leadingDebounce, listSliceForViewport, maybePreloadBackwards, paintedIndexRange, reuseIfEqual, rowTopFromPrefix };
|
package/list.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { LoadDirection } from './slice';
|
|
2
|
+
export type VirtualListRange = {
|
|
3
|
+
start: number;
|
|
4
|
+
end: number;
|
|
5
|
+
};
|
|
6
|
+
export type VirtualListOptions<Id extends string | number> = {
|
|
7
|
+
getItems: () => readonly Id[];
|
|
8
|
+
itemSize: number | ((index: number) => number);
|
|
9
|
+
listSlice?: number;
|
|
10
|
+
sensitiveArea?: number;
|
|
11
|
+
edgeDebounceMs?: number;
|
|
12
|
+
idleTrimMs?: number;
|
|
13
|
+
nearEndThreshold?: number;
|
|
14
|
+
loadMoreBackwards?: (args: {
|
|
15
|
+
offsetId?: Id;
|
|
16
|
+
}) => void;
|
|
17
|
+
onNearEnd?: () => void;
|
|
18
|
+
onVisibleRange?: (range: VirtualListRange) => void;
|
|
19
|
+
onPaintedRange?: (range: VirtualListRange) => void;
|
|
20
|
+
onChange?: () => void;
|
|
21
|
+
getFromTranslateY?: (key: string | number) => number | undefined;
|
|
22
|
+
scheduleIdle?: (fn: () => void, timeoutMs: number) => {
|
|
23
|
+
cancel(): void;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
export type VirtualList<Id extends string | number> = {
|
|
27
|
+
viewportIds: () => Id[] | undefined;
|
|
28
|
+
fromOffset: () => number;
|
|
29
|
+
isOnTop: () => boolean;
|
|
30
|
+
totalSize: () => number;
|
|
31
|
+
rowTop: (index: number) => number;
|
|
32
|
+
rowHeight: (index: number) => number;
|
|
33
|
+
slotIndex: (id: Id) => number | undefined;
|
|
34
|
+
fromTranslateY: (key: string | number) => number | undefined;
|
|
35
|
+
sync: (isDisabled?: boolean) => boolean;
|
|
36
|
+
onScroll: (metrics: {
|
|
37
|
+
scrollTop: number;
|
|
38
|
+
viewportHeight: number;
|
|
39
|
+
}) => void;
|
|
40
|
+
reanchor: (index: number) => void;
|
|
41
|
+
resetToTop: () => void;
|
|
42
|
+
getMore: (direction: LoadDirection, noScroll?: boolean) => boolean;
|
|
43
|
+
trimIdle: (firstVisibleIndex: number) => boolean;
|
|
44
|
+
setListSlice: (n: number) => void;
|
|
45
|
+
destroy: () => void;
|
|
46
|
+
};
|
|
47
|
+
export declare function createVirtualList<Id extends string | number>(options: VirtualListOptions<Id>): VirtualList<Id>;
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yorozu/virtual-list",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "1.0.25",
|
|
5
|
+
"description": "framework-agnostic absolute-row virtual list engine",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": {
|
|
10
|
+
"types": "./index.d.ts",
|
|
11
|
+
"default": "./index.js"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"author": "inshinrei",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/inshinrei/yorozu.git"
|
|
20
|
+
}
|
|
21
|
+
}
|
package/range.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const LIST_SLICE_MIN: number;
|
|
2
|
+
export declare const LIST_SLICE_MAX: number;
|
|
3
|
+
export declare const LIST_SLICE_OVERSCAN_ROWS: number;
|
|
4
|
+
export declare function heightPrefix(count: number, heightAt: (i: number) => number): number[];
|
|
5
|
+
export declare function firstVisibleIndexFromPrefix(prefix: number[], scrollTop: number): number;
|
|
6
|
+
export declare function rowTopFromPrefix(prefix: number[], index: number): number;
|
|
7
|
+
export declare function paintedIndexRange(scrollTop: number, viewportHeight: number, itemHeight: number, count: number): {
|
|
8
|
+
start: number;
|
|
9
|
+
end: number;
|
|
10
|
+
};
|
|
11
|
+
export declare function listSliceForViewport(viewportHeight: number, itemHeight: number, opts?: {
|
|
12
|
+
min?: number;
|
|
13
|
+
max?: number;
|
|
14
|
+
overscanRows?: number;
|
|
15
|
+
}): number;
|
package/slice.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type LoadDirection = "forwards" | "backwards";
|
|
2
|
+
export declare const DEFAULT_LIST_SLICE: number;
|
|
3
|
+
export declare const DEFAULT_MAX_MOUNTED_FACTOR: number;
|
|
4
|
+
export type ViewportSliceResult<Id extends string | number> = {
|
|
5
|
+
viewportIds: Id[];
|
|
6
|
+
fromOffset: number;
|
|
7
|
+
isOnTop: boolean;
|
|
8
|
+
areSomeLocal: boolean;
|
|
9
|
+
areAllLocal: boolean;
|
|
10
|
+
};
|
|
11
|
+
export declare function areIdArraysEqual<Id extends string | number>(a: Id[] | undefined, b: Id[] | undefined): boolean;
|
|
12
|
+
export declare function reuseIfEqual<Id extends string | number>(prev: Id[] | undefined, next: Id[]): Id[];
|
|
13
|
+
export declare function getViewportSlice<Id extends string | number>(sourceIds: Id[], direction: LoadDirection, listSlice: number, offsetId?: Id): ViewportSliceResult<Id>;
|
|
14
|
+
export type ViewportIdSliceControllerOptions<Id extends string | number> = {
|
|
15
|
+
listSlice?: number;
|
|
16
|
+
loadMoreBackwards?: (args: {
|
|
17
|
+
offsetId?: Id;
|
|
18
|
+
}) => void;
|
|
19
|
+
};
|
|
20
|
+
export declare class ViewportIdSliceController<Id extends string | number> {
|
|
21
|
+
protected _listSlice: number;
|
|
22
|
+
protected _loadMoreBackwards: ((args: {
|
|
23
|
+
offsetId?: Id;
|
|
24
|
+
}) => void) | undefined;
|
|
25
|
+
protected _viewportIds: Id[] | undefined;
|
|
26
|
+
protected _fromOffset: number;
|
|
27
|
+
protected _isOnTop: boolean;
|
|
28
|
+
protected _isDisabled: boolean;
|
|
29
|
+
protected _sourceIds: Id[] | undefined;
|
|
30
|
+
constructor(options?: ViewportIdSliceControllerOptions<Id>);
|
|
31
|
+
get viewportIds(): Id[] | undefined;
|
|
32
|
+
get fromOffset(): number;
|
|
33
|
+
get isOnTop(): boolean;
|
|
34
|
+
setListSlice(n: number): void;
|
|
35
|
+
sync(sourceIds: Id[] | undefined, isDisabled?: boolean): boolean;
|
|
36
|
+
getMore(sourceIds: Id[] | undefined, args: {
|
|
37
|
+
direction: LoadDirection;
|
|
38
|
+
noScroll?: boolean;
|
|
39
|
+
}): boolean;
|
|
40
|
+
reanchorAtIndex(sourceIds: Id[], index: number): boolean;
|
|
41
|
+
trimToSlice(sourceIds: Id[], firstVisibleIndex: number): boolean;
|
|
42
|
+
resetToTop(sourceIds: Id[]): void;
|
|
43
|
+
protected _commitWindow(ids: Id[], from: number, isOnTop: boolean): boolean;
|
|
44
|
+
}
|