@react-stately/virtualizer 3.7.2-nightly.4649 → 3.7.2-nightly.4656
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/dist/Layout.main.js +2 -41
- package/dist/Layout.main.js.map +1 -1
- package/dist/Layout.mjs +2 -41
- package/dist/Layout.module.js +2 -41
- package/dist/Layout.module.js.map +1 -1
- package/dist/LayoutInfo.main.js.map +1 -1
- package/dist/LayoutInfo.module.js.map +1 -1
- package/dist/OverscanManager.main.js +8 -43
- package/dist/OverscanManager.main.js.map +1 -1
- package/dist/OverscanManager.mjs +8 -43
- package/dist/OverscanManager.module.js +8 -43
- package/dist/OverscanManager.module.js.map +1 -1
- package/dist/ReusableView.main.js +23 -0
- package/dist/ReusableView.main.js.map +1 -1
- package/dist/ReusableView.mjs +23 -0
- package/dist/ReusableView.module.js +23 -0
- package/dist/ReusableView.module.js.map +1 -1
- package/dist/Virtualizer.main.js +123 -710
- package/dist/Virtualizer.main.js.map +1 -1
- package/dist/Virtualizer.mjs +124 -711
- package/dist/Virtualizer.module.js +124 -711
- package/dist/Virtualizer.module.js.map +1 -1
- package/dist/types.d.ts +64 -225
- package/dist/types.d.ts.map +1 -1
- package/dist/useVirtualizerState.main.js +39 -40
- package/dist/useVirtualizerState.main.js.map +1 -1
- package/dist/useVirtualizerState.mjs +40 -41
- package/dist/useVirtualizerState.module.js +40 -41
- package/dist/useVirtualizerState.module.js.map +1 -1
- package/dist/utils.main.js +1 -27
- package/dist/utils.main.js.map +1 -1
- package/dist/utils.mjs +2 -26
- package/dist/utils.module.js +2 -26
- package/dist/utils.module.js.map +1 -1
- package/package.json +4 -4
- package/src/Layout.ts +10 -55
- package/src/LayoutInfo.ts +2 -2
- package/src/OverscanManager.ts +10 -47
- package/src/ReusableView.ts +36 -7
- package/src/Virtualizer.ts +163 -1058
- package/src/types.ts +16 -38
- package/src/useVirtualizerState.ts +40 -39
- package/src/utils.ts +0 -52
- package/dist/Transaction.main.js +0 -32
- package/dist/Transaction.main.js.map +0 -1
- package/dist/Transaction.mjs +0 -27
- package/dist/Transaction.module.js +0 -27
- package/dist/Transaction.module.js.map +0 -1
- package/dist/tween.main.js +0 -67
- package/dist/tween.main.js.map +0 -1
- package/dist/tween.mjs +0 -61
- package/dist/tween.module.js +0 -61
- package/dist/tween.module.js.map +0 -1
- package/src/Transaction.ts +0 -28
- package/src/tween.ts +0 -83
package/src/Virtualizer.ts
CHANGED
|
@@ -10,16 +10,9 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {CancelablePromise, easeOut, tween} from './tween';
|
|
14
13
|
import {Collection, Key} from '@react-types/shared';
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
InvalidationContext,
|
|
18
|
-
ScrollAnchor,
|
|
19
|
-
ScrollToItemOptions,
|
|
20
|
-
VirtualizerDelegate,
|
|
21
|
-
VirtualizerOptions
|
|
22
|
-
} from './types';
|
|
14
|
+
import {InvalidationContext, Mutable, VirtualizerDelegate, VirtualizerRenderOptions} from './types';
|
|
15
|
+
import {isSetEqual} from './utils';
|
|
23
16
|
import {Layout} from './Layout';
|
|
24
17
|
import {LayoutInfo} from './LayoutInfo';
|
|
25
18
|
import {OverscanManager} from './OverscanManager';
|
|
@@ -27,237 +20,70 @@ import {Point} from './Point';
|
|
|
27
20
|
import {Rect} from './Rect';
|
|
28
21
|
import {ReusableView} from './ReusableView';
|
|
29
22
|
import {Size} from './Size';
|
|
30
|
-
import {Transaction} from './Transaction';
|
|
31
23
|
|
|
32
24
|
/**
|
|
33
|
-
* The
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* Optionally, the {@link EditableCollectionView} subclass can be used to enable user interaction
|
|
38
|
-
* with the collection, including drag and drop, multiple selection, and keyboard interacton.
|
|
25
|
+
* The Virtualizer class renders a scrollable collection of data using customizable layouts.
|
|
26
|
+
* It supports very large collections by only rendering visible views to the DOM, reusing
|
|
27
|
+
* them as you scroll. Virtualizer can present any type of view, including non-item views
|
|
28
|
+
* such as section headers and footers.
|
|
39
29
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* the data. When data changes, the data source emits change events, and the collection view
|
|
43
|
-
* updates as appropriate, optionally with an animated transition. There is one built-in data source
|
|
44
|
-
* implementation, {@link ArrayDataSource}, which renders content from a 2d array.
|
|
45
|
-
*
|
|
46
|
-
* Collection views use {@link Layout} objects to compute what views should be visible, and how
|
|
47
|
-
* to position and style them. This means that collection views can have their items arranged in
|
|
30
|
+
* Virtualizer uses {@link Layout} objects to compute what views should be visible, and how
|
|
31
|
+
* to position and style them. This means that virtualizer can have its items arranged in
|
|
48
32
|
* a stack, a grid, a circle, or any other layout you can think of. The layout can be changed
|
|
49
|
-
* dynamically at runtime as well
|
|
33
|
+
* dynamically at runtime as well.
|
|
50
34
|
*
|
|
51
|
-
* Layouts produce information on what views should appear in the
|
|
52
|
-
* the views themselves directly. It is the responsibility of the {@link
|
|
53
|
-
* to
|
|
54
|
-
*
|
|
55
|
-
* views as needed by the collection view. Those views are then reused by the collection view as
|
|
56
|
-
* the user scrolls through the content.
|
|
35
|
+
* Layouts produce information on what views should appear in the virtualizer, but do not create
|
|
36
|
+
* the views themselves directly. It is the responsibility of the {@link VirtualizerDelegate} object
|
|
37
|
+
* to render elements for each layout info. The virtualizer manages a set of {@link ReusableView} objects,
|
|
38
|
+
* which are reused as the user scrolls by swapping their content with cached elements returned by the delegate.
|
|
57
39
|
*/
|
|
58
40
|
export class Virtualizer<T extends object, V, W> {
|
|
59
41
|
/**
|
|
60
|
-
* The
|
|
42
|
+
* The virtualizer delegate. The delegate is used by the virtualizer
|
|
61
43
|
* to create and configure views.
|
|
62
44
|
*/
|
|
63
45
|
delegate: VirtualizerDelegate<T, V, W>;
|
|
64
46
|
|
|
65
|
-
/** The
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
47
|
+
/** The current content of the virtualizer. */
|
|
48
|
+
readonly collection: Collection<T>;
|
|
49
|
+
/** The layout object that determines the visible views. */
|
|
50
|
+
readonly layout: Layout<T>;
|
|
51
|
+
/** The size of the scrollable content. */
|
|
52
|
+
readonly contentSize: Size;
|
|
53
|
+
/** The currently visible rectangle. */
|
|
54
|
+
readonly visibleRect: Rect;
|
|
55
|
+
/** The set of persisted keys that are always present in the DOM, even if not currently in view. */
|
|
56
|
+
readonly persistedKeys: Set<Key>;
|
|
73
57
|
|
|
74
|
-
/** Whether to anchor the scroll position when at the top of the content. Default is off. */
|
|
75
|
-
anchorScrollPositionAtTop: boolean;
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Whether to overscan the visible area to pre-render items slightly outside and
|
|
79
|
-
* improve performance. Default is on.
|
|
80
|
-
*/
|
|
81
|
-
shouldOverscan: boolean;
|
|
82
|
-
|
|
83
|
-
private _collection: Collection<T>;
|
|
84
|
-
private _layout: Layout<T>;
|
|
85
|
-
private _contentSize: Size;
|
|
86
|
-
private _visibleRect: Rect;
|
|
87
|
-
private _visibleLayoutInfos: Map<Key, LayoutInfo>;
|
|
88
|
-
private _reusableViews: {[type: string]: ReusableView<T, V>[]};
|
|
89
58
|
private _visibleViews: Map<Key, ReusableView<T, V>>;
|
|
90
59
|
private _renderedContent: WeakMap<T, V>;
|
|
91
|
-
private
|
|
92
|
-
private _viewsByParentKey: Map<Key | null, ReusableView<T, V>[]>;
|
|
93
|
-
private _invalidationContext: InvalidationContext<T, V> | null;
|
|
94
|
-
private _overscanManager: OverscanManager;
|
|
95
|
-
private _persistedKeys: Set<Key>;
|
|
96
|
-
private _relayoutRaf: number | null;
|
|
97
|
-
private _scrollAnimation: CancelablePromise<void> | null;
|
|
60
|
+
private _rootView: ReusableView<T, V>;
|
|
98
61
|
private _isScrolling: boolean;
|
|
99
|
-
private
|
|
100
|
-
private
|
|
101
|
-
private _transaction: Transaction<T, V> | null;
|
|
102
|
-
private _nextTransaction: Transaction<T, V> | null;
|
|
103
|
-
private _transactionQueue: Transaction<T, V>[];
|
|
104
|
-
|
|
105
|
-
constructor(options: VirtualizerOptions<T, V, W> = {}) {
|
|
106
|
-
this._contentSize = new Size;
|
|
107
|
-
this._visibleRect = new Rect;
|
|
62
|
+
private _invalidationContext: InvalidationContext | null;
|
|
63
|
+
private _overscanManager: OverscanManager;
|
|
108
64
|
|
|
109
|
-
|
|
110
|
-
this.
|
|
65
|
+
constructor(delegate: VirtualizerDelegate<T, V, W>) {
|
|
66
|
+
this.delegate = delegate;
|
|
67
|
+
this.contentSize = new Size;
|
|
68
|
+
this.visibleRect = new Rect;
|
|
69
|
+
this.persistedKeys = new Set();
|
|
111
70
|
this._visibleViews = new Map();
|
|
112
71
|
this._renderedContent = new WeakMap();
|
|
113
|
-
this.
|
|
72
|
+
this._rootView = new ReusableView(this);
|
|
73
|
+
this._isScrolling = false;
|
|
114
74
|
this._invalidationContext = null;
|
|
115
75
|
this._overscanManager = new OverscanManager();
|
|
116
|
-
this._persistedKeys = new Set();
|
|
117
|
-
|
|
118
|
-
this._scrollAnimation = null;
|
|
119
|
-
this._isScrolling = false;
|
|
120
|
-
this._sizeUpdateQueue = new Map();
|
|
121
|
-
this._animatedContentOffset = new Point(0, 0);
|
|
122
|
-
|
|
123
|
-
this._transaction = null;
|
|
124
|
-
this._nextTransaction = null;
|
|
125
|
-
this._transactionQueue = [];
|
|
126
|
-
|
|
127
|
-
// Set options from passed object if given
|
|
128
|
-
this.transitionDuration = options.transitionDuration ?? 500;
|
|
129
|
-
this.anchorScrollPosition = options.anchorScrollPosition || false;
|
|
130
|
-
this.anchorScrollPositionAtTop = options.anchorScrollPositionAtTop || false;
|
|
131
|
-
this.shouldOverscan = options.shouldOverscan !== false;
|
|
132
|
-
for (let key of ['delegate', 'size', 'layout', 'collection']) {
|
|
133
|
-
if (options[key]) {
|
|
134
|
-
this[key] = options[key];
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
_setContentSize(size: Size) {
|
|
140
|
-
this._contentSize = size;
|
|
141
|
-
this.delegate.setContentSize(size);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
_setContentOffset(offset: Point) {
|
|
145
|
-
let rect = new Rect(offset.x, offset.y, this._visibleRect.width, this._visibleRect.height);
|
|
146
|
-
this.delegate.setVisibleRect(rect);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Get the size of the scrollable content.
|
|
151
|
-
*/
|
|
152
|
-
get contentSize(): Size {
|
|
153
|
-
return this._contentSize;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Get the collection view's currently visible rectangle.
|
|
158
|
-
*/
|
|
159
|
-
get visibleRect(): Rect {
|
|
160
|
-
return this._visibleRect;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Set the collection view's currently visible rectangle.
|
|
165
|
-
*/
|
|
166
|
-
set visibleRect(rect: Rect) {
|
|
167
|
-
this._setVisibleRect(rect);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
_setVisibleRect(rect: Rect, forceUpdate = false) {
|
|
171
|
-
let current = this._visibleRect;
|
|
172
|
-
|
|
173
|
-
// Ignore if the rects are equal
|
|
174
|
-
if (rect.equals(current)) {
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (this.shouldOverscan) {
|
|
179
|
-
this._overscanManager.setVisibleRect(rect);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
let shouldInvalidate = this.layout && this.layout.shouldInvalidate(rect, this._visibleRect);
|
|
183
|
-
|
|
184
|
-
this._resetAnimatedContentOffset();
|
|
185
|
-
this._visibleRect = rect;
|
|
186
|
-
|
|
187
|
-
if (shouldInvalidate) {
|
|
188
|
-
// We are already in a layout effect when this method is called, so relayoutNow is appropriate.
|
|
189
|
-
this.relayoutNow({
|
|
190
|
-
offsetChanged: !rect.pointEquals(current),
|
|
191
|
-
sizeChanged: !rect.sizeEquals(current)
|
|
192
|
-
});
|
|
193
|
-
} else {
|
|
194
|
-
this.updateSubviews(forceUpdate);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
get collection(): Collection<T> {
|
|
199
|
-
return this._collection;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
set collection(data: Collection<T>) {
|
|
203
|
-
this._setData(data);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
private _setData(data: Collection<T>) {
|
|
207
|
-
if (data === this._collection) {
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (this._collection) {
|
|
212
|
-
this._runTransaction(() => {
|
|
213
|
-
this._collection = data;
|
|
214
|
-
}, this.transitionDuration > 0);
|
|
215
|
-
} else {
|
|
216
|
-
this._collection = data;
|
|
217
|
-
this.reloadData();
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Reloads the data from the data source and relayouts the collection view.
|
|
223
|
-
* Does not animate any changes. Equivalent to re-assigning the same data source
|
|
224
|
-
* to the collection view.
|
|
225
|
-
*/
|
|
226
|
-
reloadData() {
|
|
227
|
-
this.relayout({
|
|
228
|
-
contentChanged: true
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Returns the item with the given key.
|
|
234
|
-
*/
|
|
235
|
-
getItem(key: Key) {
|
|
236
|
-
return this._collection ? this._collection.getItem(key) : null;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/** The set of persisted keys are always present in the DOM, even if not currently in view. */
|
|
240
|
-
get persistedKeys(): Set<Key> {
|
|
241
|
-
return this._persistedKeys;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/** The set of persisted keys are always present in the DOM, even if not currently in view. */
|
|
245
|
-
set persistedKeys(persistedKeys: Set<Key>) {
|
|
246
|
-
if (!isSetEqual(persistedKeys, this._persistedKeys)) {
|
|
247
|
-
this._persistedKeys = persistedKeys;
|
|
248
|
-
this.updateSubviews();
|
|
249
|
-
}
|
|
250
76
|
}
|
|
251
77
|
|
|
252
78
|
/** Returns whether the given key, or an ancestor, is persisted. */
|
|
253
79
|
isPersistedKey(key: Key) {
|
|
254
80
|
// Quick check if the key is directly in the set of persisted keys.
|
|
255
|
-
if (this.
|
|
81
|
+
if (this.persistedKeys.has(key)) {
|
|
256
82
|
return true;
|
|
257
83
|
}
|
|
258
84
|
|
|
259
85
|
// If not, check if the key is an ancestor of any of the persisted keys.
|
|
260
|
-
for (let k of this.
|
|
86
|
+
for (let k of this.persistedKeys) {
|
|
261
87
|
while (k != null) {
|
|
262
88
|
let layoutInfo = this.layout.getLayoutInfo(k);
|
|
263
89
|
if (!layoutInfo) {
|
|
@@ -275,96 +101,17 @@ export class Virtualizer<T extends object, V, W> {
|
|
|
275
101
|
return false;
|
|
276
102
|
}
|
|
277
103
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
get layout(): Layout<T> {
|
|
282
|
-
return this._layout;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* Set the collection view's layout.
|
|
287
|
-
*/
|
|
288
|
-
set layout(layout: Layout<T>) {
|
|
289
|
-
this.setLayout(layout);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* Sets the collection view's layout, optionally with an animated transition
|
|
294
|
-
* from the current layout to the new layout.
|
|
295
|
-
* @param layout The layout to switch to.
|
|
296
|
-
* @param animated Whether to animate the layout change.
|
|
297
|
-
*/
|
|
298
|
-
setLayout(layout: Layout<T>, animated = false) {
|
|
299
|
-
if (layout === this._layout) {
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
let applyLayout = () => {
|
|
304
|
-
if (this._layout) {
|
|
305
|
-
// @ts-ignore
|
|
306
|
-
this._layout.virtualizer = null;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
layout.virtualizer = this;
|
|
310
|
-
this._layout = layout;
|
|
311
|
-
};
|
|
312
|
-
|
|
313
|
-
if (animated) {
|
|
314
|
-
// Animated layout transitions are really simple, thanks to our transaction support.
|
|
315
|
-
// We just set the layout inside a transaction action, which runs after the initial
|
|
316
|
-
// layout infos for the animation are retrieved from the previous layout. Then, the
|
|
317
|
-
// final layout infos are retrieved from the new layout, and animations occur.
|
|
318
|
-
this._runTransaction(applyLayout);
|
|
319
|
-
} else {
|
|
320
|
-
applyLayout();
|
|
321
|
-
this.relayout();
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
private _getReuseType(layoutInfo: LayoutInfo, content: T | null) {
|
|
326
|
-
if (layoutInfo.type === 'item' && content) {
|
|
327
|
-
let type = this.delegate.getType ? this.delegate.getType(content) : 'item';
|
|
328
|
-
let reuseType = type === 'item' ? 'item' : layoutInfo.type + '_' + type;
|
|
329
|
-
return {type, reuseType};
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
return {
|
|
333
|
-
type: layoutInfo.type,
|
|
334
|
-
reuseType: layoutInfo.type
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
getReusableView(layoutInfo: LayoutInfo): ReusableView<T, V> {
|
|
339
|
-
let content = this.getItem(layoutInfo.key);
|
|
340
|
-
let {reuseType} = this._getReuseType(layoutInfo, content);
|
|
341
|
-
|
|
342
|
-
if (!this._reusableViews[reuseType]) {
|
|
343
|
-
this._reusableViews[reuseType] = [];
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
let reusable = this._reusableViews[reuseType];
|
|
347
|
-
let view = reusable.length > 0
|
|
348
|
-
? reusable.pop()
|
|
349
|
-
: new ReusableView<T, V>(this);
|
|
350
|
-
|
|
351
|
-
view.viewType = reuseType;
|
|
352
|
-
|
|
353
|
-
if (!this._animatedContentOffset.isOrigin()) {
|
|
354
|
-
layoutInfo = layoutInfo.copy();
|
|
355
|
-
layoutInfo.rect.x += this._animatedContentOffset.x;
|
|
356
|
-
layoutInfo.rect.y += this._animatedContentOffset.y;
|
|
357
|
-
}
|
|
358
|
-
|
|
104
|
+
private getReusableView(layoutInfo: LayoutInfo): ReusableView<T, V> {
|
|
105
|
+
let parentView = layoutInfo.parentKey != null ? this._visibleViews.get(layoutInfo.parentKey) : this._rootView;
|
|
106
|
+
let view = parentView.getReusableView(layoutInfo.type);
|
|
359
107
|
view.layoutInfo = layoutInfo;
|
|
360
|
-
|
|
361
108
|
this._renderView(view);
|
|
362
109
|
return view;
|
|
363
110
|
}
|
|
364
111
|
|
|
365
112
|
private _renderView(reusableView: ReusableView<T, V>) {
|
|
366
113
|
let {type, key} = reusableView.layoutInfo;
|
|
367
|
-
reusableView.content = this.getItem(key);
|
|
114
|
+
reusableView.content = this.collection.getItem(key);
|
|
368
115
|
reusableView.rendered = this._renderContent(type, reusableView.content);
|
|
369
116
|
}
|
|
370
117
|
|
|
@@ -381,44 +128,6 @@ export class Virtualizer<T extends object, V, W> {
|
|
|
381
128
|
return rendered;
|
|
382
129
|
}
|
|
383
130
|
|
|
384
|
-
/**
|
|
385
|
-
* Returns an array of all currently visible views, including both
|
|
386
|
-
* item views and supplementary views.
|
|
387
|
-
*/
|
|
388
|
-
get visibleViews(): ReusableView<T, V>[] {
|
|
389
|
-
return Array.from(this._visibleViews.values());
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/**
|
|
393
|
-
* Gets the visible view for the given type and key. Returns null if
|
|
394
|
-
* the view is not currently visible.
|
|
395
|
-
*
|
|
396
|
-
* @param key The key of the view to retrieve.
|
|
397
|
-
*/
|
|
398
|
-
getView(key: Key): ReusableView<T, V> | null {
|
|
399
|
-
return this._visibleViews.get(key) || null;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
/**
|
|
403
|
-
* Returns an array of visible views matching the given type.
|
|
404
|
-
* @param type The view type to find.
|
|
405
|
-
*/
|
|
406
|
-
getViewsOfType(type: string): ReusableView<T, V>[] {
|
|
407
|
-
return this.visibleViews.filter(v => v.layoutInfo && v.layoutInfo.type === type);
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
/**
|
|
411
|
-
* Returns the key for the given view. Returns null
|
|
412
|
-
* if the view is not currently visible.
|
|
413
|
-
*/
|
|
414
|
-
keyForView(view: ReusableView<T, V>): Key | null {
|
|
415
|
-
if (view && view.layoutInfo) {
|
|
416
|
-
return view.layoutInfo.key;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
return null;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
131
|
/**
|
|
423
132
|
* Returns the key for the item view currently at the given point.
|
|
424
133
|
*/
|
|
@@ -437,810 +146,206 @@ export class Virtualizer<T extends object, V, W> {
|
|
|
437
146
|
return null;
|
|
438
147
|
}
|
|
439
148
|
|
|
440
|
-
|
|
441
|
-
* Cleanup for when the Virtualizer will be unmounted.
|
|
442
|
-
*/
|
|
443
|
-
willUnmount() {
|
|
444
|
-
cancelAnimationFrame(this._relayoutRaf);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
/**
|
|
448
|
-
* Triggers a layout invalidation, and updates the visible subviews.
|
|
449
|
-
*/
|
|
450
|
-
relayout(context: InvalidationContext<T, V> = {}) {
|
|
451
|
-
// Ignore relayouts while animating the scroll position
|
|
452
|
-
if (this._scrollAnimation || typeof requestAnimationFrame === 'undefined') {
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
// If we already scheduled a relayout, extend the invalidation
|
|
457
|
-
// context so we coalesce multiple relayouts in the same frame.
|
|
458
|
-
if (this._invalidationContext) {
|
|
459
|
-
Object.assign(this._invalidationContext, context);
|
|
460
|
-
return;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
this._invalidationContext = context;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
/**
|
|
467
|
-
* Performs a relayout immediately. Prefer {@link relayout} over this method
|
|
468
|
-
* where possible, since it coalesces multiple layout passes in the same tick.
|
|
469
|
-
*/
|
|
470
|
-
relayoutNow(context: InvalidationContext<T, V> = this._invalidationContext || {}) {
|
|
471
|
-
// Cancel the scheduled relayout, since we're doing it now.
|
|
472
|
-
if (this._relayoutRaf) {
|
|
473
|
-
cancelAnimationFrame(this._relayoutRaf);
|
|
474
|
-
this._relayoutRaf = null;
|
|
475
|
-
// Update the provided context with the current invalidationContext since we are cancelling
|
|
476
|
-
// a scheduled relayoutNow call that has this._invalidationContext set as its default context arg (relayoutNow() in relayout)
|
|
477
|
-
context = {...this._invalidationContext, ...context};
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
// Reset the invalidation context
|
|
481
|
-
this._invalidationContext = null;
|
|
482
|
-
|
|
483
|
-
// Do nothing if we don't have a layout or content, or we are
|
|
484
|
-
// in the middle of an animated scroll transition.
|
|
485
|
-
if (!this.layout || !this._collection || this._scrollAnimation) {
|
|
486
|
-
return;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
let scrollAnchor = this._getScrollAnchor();
|
|
490
|
-
|
|
491
|
-
// Trigger the beforeLayout hook, if provided
|
|
492
|
-
if (typeof context.beforeLayout === 'function') {
|
|
493
|
-
context.beforeLayout();
|
|
494
|
-
}
|
|
495
|
-
|
|
149
|
+
private relayout(context: InvalidationContext = {}) {
|
|
496
150
|
// Validate the layout
|
|
497
151
|
this.layout.validate(context);
|
|
498
|
-
this.
|
|
499
|
-
|
|
500
|
-
// Trigger the afterLayout hook, if provided
|
|
501
|
-
if (typeof context.afterLayout === 'function') {
|
|
502
|
-
context.afterLayout();
|
|
503
|
-
}
|
|
152
|
+
(this as Mutable<this>).contentSize = this.layout.getContentSize();
|
|
504
153
|
|
|
505
|
-
//
|
|
154
|
+
// Constrain scroll position.
|
|
506
155
|
// If the content changed, scroll to the top.
|
|
507
|
-
let visibleRect = this.
|
|
508
|
-
let
|
|
509
|
-
let
|
|
510
|
-
let contentOffsetY = context.contentChanged ? 0 : restoredScrollAnchor.y;
|
|
156
|
+
let visibleRect = this.visibleRect;
|
|
157
|
+
let contentOffsetX = context.contentChanged ? 0 : visibleRect.x;
|
|
158
|
+
let contentOffsetY = context.contentChanged ? 0 : visibleRect.y;
|
|
511
159
|
contentOffsetX = Math.max(0, Math.min(this.contentSize.width - visibleRect.width, contentOffsetX));
|
|
512
160
|
contentOffsetY = Math.max(0, Math.min(this.contentSize.height - visibleRect.height, contentOffsetY));
|
|
513
161
|
|
|
514
|
-
let hasLayoutUpdates = false;
|
|
515
162
|
if (contentOffsetX !== visibleRect.x || contentOffsetY !== visibleRect.y) {
|
|
516
|
-
// If
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
// scroll offset for real. This ensures jitter-free animation since we don't need to sync
|
|
520
|
-
// the scroll animation and the content animation.
|
|
521
|
-
if (context.animated || !this._animatedContentOffset.isOrigin()) {
|
|
522
|
-
this._animatedContentOffset.x += visibleRect.x - contentOffsetX;
|
|
523
|
-
this._animatedContentOffset.y += visibleRect.y - contentOffsetY;
|
|
524
|
-
hasLayoutUpdates = this.updateSubviews(context.contentChanged);
|
|
525
|
-
} else {
|
|
526
|
-
this._setContentOffset(new Point(contentOffsetX, contentOffsetY));
|
|
527
|
-
}
|
|
163
|
+
// If the offset changed, trigger a new re-render.
|
|
164
|
+
let rect = new Rect(contentOffsetX, contentOffsetY, visibleRect.width, visibleRect.height);
|
|
165
|
+
this.delegate.setVisibleRect(rect);
|
|
528
166
|
} else {
|
|
529
|
-
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
// Apply layout infos, unless this is coming from an animated transaction
|
|
533
|
-
if (!(context.transaction && context.animated)) {
|
|
534
|
-
this._applyLayoutInfos();
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
// Wait for animations, and apply the afterAnimation hook, if provided
|
|
538
|
-
if (context.animated && hasLayoutUpdates) {
|
|
539
|
-
this._enableTransitions();
|
|
540
|
-
|
|
541
|
-
let done = () => {
|
|
542
|
-
this._disableTransitions();
|
|
543
|
-
|
|
544
|
-
// Reset scroll position after animations (see above comment).
|
|
545
|
-
if (!this._animatedContentOffset.isOrigin()) {
|
|
546
|
-
// Get the content offset to scroll to, taking _animatedContentOffset into account.
|
|
547
|
-
let {x, y} = this.getVisibleRect();
|
|
548
|
-
this._resetAnimatedContentOffset();
|
|
549
|
-
this._setContentOffset(new Point(x, y));
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
if (typeof context.afterAnimation === 'function') {
|
|
553
|
-
context.afterAnimation();
|
|
554
|
-
}
|
|
555
|
-
};
|
|
556
|
-
|
|
557
|
-
// Sometimes the animation takes slightly longer than expected.
|
|
558
|
-
setTimeout(done, this.transitionDuration + 100);
|
|
559
|
-
return;
|
|
560
|
-
} else if (typeof context.afterAnimation === 'function') {
|
|
561
|
-
context.afterAnimation();
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
/**
|
|
566
|
-
* Corrects DOM order of visible views to match item order of collection.
|
|
567
|
-
*/
|
|
568
|
-
private _correctItemOrder() {
|
|
569
|
-
// Defer until after scrolling and animated transactions are complete
|
|
570
|
-
if (this._isScrolling || this._transaction) {
|
|
571
|
-
return;
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
for (let key of this._visibleLayoutInfos.keys()) {
|
|
575
|
-
let view = this._visibleViews.get(key);
|
|
576
|
-
this._children.delete(view);
|
|
577
|
-
this._children.add(view);
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
private _enableTransitions() {
|
|
582
|
-
this.delegate.beginAnimations();
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
private _disableTransitions() {
|
|
586
|
-
this.delegate.endAnimations();
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
private _getScrollAnchor(): ScrollAnchor | null {
|
|
590
|
-
if (!this.anchorScrollPosition) {
|
|
591
|
-
return null;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
let visibleRect = this.getVisibleRect();
|
|
595
|
-
|
|
596
|
-
// Ask the delegate to provide a scroll anchor, if possible
|
|
597
|
-
if (this.delegate.getScrollAnchor) {
|
|
598
|
-
let key = this.delegate.getScrollAnchor(visibleRect);
|
|
599
|
-
if (key != null) {
|
|
600
|
-
let layoutInfo = this.layout.getLayoutInfo(key);
|
|
601
|
-
let corner = layoutInfo.rect.getCornerInRect(visibleRect);
|
|
602
|
-
if (corner) {
|
|
603
|
-
let key = layoutInfo.key;
|
|
604
|
-
let offset = layoutInfo.rect[corner].y - visibleRect.y;
|
|
605
|
-
return {key, layoutInfo, corner, offset};
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
// No need to anchor the scroll position if it is at the top
|
|
611
|
-
if (visibleRect.y === 0 && !this.anchorScrollPositionAtTop) {
|
|
612
|
-
return null;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
// Find a view with a visible corner that has the smallest distance to the top of the collection view
|
|
616
|
-
let cornerAnchor: ScrollAnchor | null = null;
|
|
617
|
-
|
|
618
|
-
for (let [key, view] of this._visibleViews) {
|
|
619
|
-
let layoutInfo = view.layoutInfo;
|
|
620
|
-
if (layoutInfo && layoutInfo.rect.area > 0) {
|
|
621
|
-
let corner = layoutInfo.rect.getCornerInRect(visibleRect);
|
|
622
|
-
|
|
623
|
-
if (corner) {
|
|
624
|
-
let offset = layoutInfo.rect[corner].y - visibleRect.y;
|
|
625
|
-
if (!cornerAnchor || (offset < cornerAnchor.offset)) {
|
|
626
|
-
cornerAnchor = {key, layoutInfo, corner, offset};
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
return cornerAnchor;
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
private _restoreScrollAnchor(scrollAnchor: ScrollAnchor | null, context: InvalidationContext<T, V>) {
|
|
636
|
-
let contentOffset = this.getVisibleRect();
|
|
637
|
-
|
|
638
|
-
if (scrollAnchor) {
|
|
639
|
-
let finalAnchor = context.transaction?.animated
|
|
640
|
-
? context.transaction.finalMap.get(scrollAnchor.key)
|
|
641
|
-
: this.layout.getLayoutInfo(scrollAnchor.layoutInfo.key);
|
|
642
|
-
|
|
643
|
-
if (finalAnchor) {
|
|
644
|
-
let adjustment = (finalAnchor.rect[scrollAnchor.corner].y - contentOffset.y) - scrollAnchor.offset;
|
|
645
|
-
contentOffset.y += adjustment;
|
|
646
|
-
}
|
|
167
|
+
this.updateSubviews();
|
|
647
168
|
}
|
|
648
|
-
|
|
649
|
-
return contentOffset;
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
getVisibleRect(): Rect {
|
|
653
|
-
let v = this.visibleRect;
|
|
654
|
-
let x = v.x - this._animatedContentOffset.x;
|
|
655
|
-
let y = v.y - this._animatedContentOffset.y;
|
|
656
|
-
return new Rect(x, y, v.width, v.height);
|
|
657
169
|
}
|
|
658
170
|
|
|
659
171
|
getVisibleLayoutInfos() {
|
|
660
172
|
let isTestEnv = process.env.NODE_ENV === 'test' && !process.env.VIRT_ON;
|
|
173
|
+
let isClientWidthMocked = isTestEnv && typeof HTMLElement !== 'undefined' && Object.getOwnPropertyNames(HTMLElement.prototype).includes('clientWidth');
|
|
174
|
+
let isClientHeightMocked = isTestEnv && typeof HTMLElement !== 'undefined' && Object.getOwnPropertyNames(HTMLElement.prototype).includes('clientHeight');
|
|
661
175
|
|
|
662
|
-
let
|
|
663
|
-
let isClientHeightMocked = Object.getOwnPropertyNames(window.HTMLElement.prototype).includes('clientHeight');
|
|
664
|
-
|
|
665
|
-
let rect;
|
|
176
|
+
let rect: Rect;
|
|
666
177
|
if (isTestEnv && !(isClientWidthMocked && isClientHeightMocked)) {
|
|
667
|
-
rect = this.
|
|
178
|
+
rect = new Rect(0, 0, this.contentSize.width, this.contentSize.height);
|
|
668
179
|
} else {
|
|
669
|
-
rect = this.
|
|
180
|
+
rect = this._overscanManager.getOverscannedRect();
|
|
670
181
|
}
|
|
671
182
|
|
|
672
|
-
this._visibleLayoutInfos = this._getLayoutInfoMap(rect);
|
|
673
|
-
return this._visibleLayoutInfos;
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
private _getLayoutInfoMap(rect: Rect, copy = false) {
|
|
677
183
|
let layoutInfos = this.layout.getVisibleLayoutInfos(rect);
|
|
678
184
|
let map = new Map;
|
|
679
|
-
|
|
680
185
|
for (let layoutInfo of layoutInfos) {
|
|
681
|
-
if (copy) {
|
|
682
|
-
layoutInfo = layoutInfo.copy();
|
|
683
|
-
}
|
|
684
|
-
|
|
685
186
|
map.set(layoutInfo.key, layoutInfo);
|
|
686
187
|
}
|
|
687
188
|
|
|
688
189
|
return map;
|
|
689
190
|
}
|
|
690
191
|
|
|
691
|
-
updateSubviews(
|
|
692
|
-
if (!this._collection) {
|
|
693
|
-
return;
|
|
694
|
-
}
|
|
695
|
-
|
|
192
|
+
private updateSubviews() {
|
|
696
193
|
let visibleLayoutInfos = this.getVisibleLayoutInfos();
|
|
697
|
-
let currentlyVisible = this._visibleViews;
|
|
698
|
-
let toAdd, toRemove, toUpdate;
|
|
699
|
-
|
|
700
|
-
// If this is a force update, remove and re-add all views.
|
|
701
|
-
// Otherwise, find and update the diff.
|
|
702
|
-
if (forceUpdate) {
|
|
703
|
-
toAdd = visibleLayoutInfos;
|
|
704
|
-
toRemove = currentlyVisible;
|
|
705
|
-
toUpdate = new Set();
|
|
706
|
-
} else {
|
|
707
|
-
({toAdd, toRemove, toUpdate} = difference(currentlyVisible, visibleLayoutInfos));
|
|
708
|
-
|
|
709
|
-
for (let key of toUpdate) {
|
|
710
|
-
let view = currentlyVisible.get(key);
|
|
711
|
-
if (!view || !view.layoutInfo) {
|
|
712
|
-
continue;
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
let item = this.getItem(visibleLayoutInfos.get(key).key);
|
|
716
|
-
if (view.content === item) {
|
|
717
|
-
toUpdate.delete(key);
|
|
718
|
-
} else {
|
|
719
|
-
// If the view type changes, delete and recreate the view instead of updating
|
|
720
|
-
let {reuseType} = this._getReuseType(view.layoutInfo, item);
|
|
721
|
-
if (view.viewType !== reuseType) {
|
|
722
|
-
toUpdate.delete(key);
|
|
723
|
-
toAdd.add(key);
|
|
724
|
-
toRemove.add(key);
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
// We are done if the sets are equal
|
|
730
|
-
if (toAdd.size === 0 && toRemove.size === 0 && toUpdate.size === 0) {
|
|
731
|
-
if (this._transaction) {
|
|
732
|
-
this._applyLayoutInfos();
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
return;
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
194
|
|
|
739
|
-
// Track views that should be removed. They are not removed from
|
|
740
|
-
// the DOM immediately, since we may reuse and need to re-insert
|
|
741
|
-
// them back into the DOM anyway.
|
|
742
195
|
let removed = new Set<ReusableView<T, V>>();
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
let view = this._visibleViews.get(key);
|
|
746
|
-
if (view) {
|
|
747
|
-
removed.add(view);
|
|
196
|
+
for (let [key, view] of this._visibleViews) {
|
|
197
|
+
if (!visibleLayoutInfos.has(key)) {
|
|
748
198
|
this._visibleViews.delete(key);
|
|
749
|
-
|
|
750
|
-
//
|
|
751
|
-
// of the animations to remove the views from the DOM. Also means
|
|
752
|
-
// we can't reuse those views immediately.
|
|
753
|
-
if (this._transaction) {
|
|
754
|
-
this._transaction.toRemove.set(key, view);
|
|
755
|
-
} else {
|
|
756
|
-
this.reuseView(view);
|
|
757
|
-
}
|
|
199
|
+
view.parent.reuseChild(view);
|
|
200
|
+
removed.add(view); // Defer removing in case we reuse this view.
|
|
758
201
|
}
|
|
759
202
|
}
|
|
760
203
|
|
|
761
|
-
for (let key of
|
|
762
|
-
let
|
|
763
|
-
let view: ReusableView<T, V> | void;
|
|
764
|
-
|
|
765
|
-
// If we're in a transaction, and a layout change happens
|
|
766
|
-
// during the animations such that a view that was going
|
|
767
|
-
// to be removed is now not, we don't create a new view
|
|
768
|
-
// since the old one is still in the DOM, marked as toRemove.
|
|
769
|
-
if (this._transaction) {
|
|
770
|
-
// if transaction, get initial layout attributes for the animation
|
|
771
|
-
if (this._transaction.initialLayoutInfo.has(key)) {
|
|
772
|
-
layoutInfo = this._transaction.initialLayoutInfo.get(key);
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
view = this._transaction.toRemove.get(key);
|
|
776
|
-
if (view) {
|
|
777
|
-
this._transaction.toRemove.delete(key);
|
|
778
|
-
this._applyLayoutInfo(view, layoutInfo);
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
|
|
204
|
+
for (let [key, layoutInfo] of visibleLayoutInfos) {
|
|
205
|
+
let view = this._visibleViews.get(key);
|
|
782
206
|
if (!view) {
|
|
783
|
-
// Create or reuse a view for this row
|
|
784
207
|
view = this.getReusableView(layoutInfo);
|
|
208
|
+
view.parent.children.add(view);
|
|
209
|
+
this._visibleViews.set(key, view);
|
|
210
|
+
removed.delete(view);
|
|
211
|
+
} else {
|
|
212
|
+
view.layoutInfo = layoutInfo;
|
|
785
213
|
|
|
786
|
-
|
|
787
|
-
if (
|
|
788
|
-
this.
|
|
214
|
+
let item = this.collection.getItem(layoutInfo.key);
|
|
215
|
+
if (view.content !== item) {
|
|
216
|
+
this._renderedContent.delete(view.content);
|
|
217
|
+
this._renderView(view);
|
|
789
218
|
}
|
|
790
219
|
}
|
|
791
|
-
|
|
792
|
-
this._visibleViews.set(key, view);
|
|
793
|
-
removed.delete(view);
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
for (let key of toUpdate) {
|
|
797
|
-
let view = currentlyVisible.get(key) as ReusableView<T, V>;
|
|
798
|
-
this._renderedContent.delete(key);
|
|
799
|
-
this._renderView(view);
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
// Remove the remaining rows to delete from the DOM
|
|
803
|
-
if (!this._transaction) {
|
|
804
|
-
this.removeViews(removed);
|
|
805
220
|
}
|
|
806
221
|
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
if (this._transactionQueue.length > 0) {
|
|
826
|
-
this._processTransactionQueue();
|
|
827
|
-
} else if (this._invalidationContext) {
|
|
828
|
-
this.relayoutNow();
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
if (this.shouldOverscan) {
|
|
832
|
-
this._overscanManager.collectMetrics();
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
private _flushVisibleViews() {
|
|
837
|
-
// CollectionVirtualizer deals with a flattened set of LayoutInfos, but they can represent hierarchy
|
|
838
|
-
// by referencing a parentKey. Just before rendering the visible views, we rebuild this hierarchy
|
|
839
|
-
// by creating a mapping of views by parent key and recursively calling the delegate's renderWrapper
|
|
840
|
-
// method to build the final tree.
|
|
841
|
-
this._viewsByParentKey = new Map([[null, []]]);
|
|
842
|
-
for (let view of this._children) {
|
|
843
|
-
if (view.layoutInfo?.parentKey != null && !this._viewsByParentKey.has(view.layoutInfo.parentKey)) {
|
|
844
|
-
this._viewsByParentKey.set(view.layoutInfo.parentKey, []);
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
this._viewsByParentKey.get(view.layoutInfo?.parentKey)?.push(view);
|
|
848
|
-
if (!this._viewsByParentKey.has(view.layoutInfo?.key)) {
|
|
849
|
-
this._viewsByParentKey.set(view.layoutInfo?.key, []);
|
|
222
|
+
// The remaining views in `removed` were not reused to render new items.
|
|
223
|
+
// They should be removed from the DOM. We also clear the reusable view queue
|
|
224
|
+
// here since there's no point holding onto views that have been removed.
|
|
225
|
+
// Doing so hurts performance in the future when reusing elements due to FIFO order.
|
|
226
|
+
for (let view of removed) {
|
|
227
|
+
view.parent.children.delete(view);
|
|
228
|
+
view.parent.reusableViews.clear();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Reordering DOM nodes is costly, so we defer this until scrolling stops.
|
|
232
|
+
// DOM order does not affect visual order (due to absolute positioning),
|
|
233
|
+
// but does matter for assistive technology users.
|
|
234
|
+
if (!this._isScrolling) {
|
|
235
|
+
// Layout infos must be in topological order (parents before children).
|
|
236
|
+
for (let key of visibleLayoutInfos.keys()) {
|
|
237
|
+
let view = this._visibleViews.get(key)!;
|
|
238
|
+
view.parent.children.delete(view);
|
|
239
|
+
view.parent.children.add(view);
|
|
850
240
|
}
|
|
851
241
|
}
|
|
852
|
-
|
|
853
|
-
let children = this.getChildren(null);
|
|
854
|
-
this.delegate.setVisibleViews(children);
|
|
855
242
|
}
|
|
856
243
|
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
);
|
|
866
|
-
});
|
|
867
|
-
|
|
868
|
-
let parent = this._visibleViews.get(key)!;
|
|
869
|
-
return buildTree(parent, this._viewsByParentKey.get(key));
|
|
870
|
-
}
|
|
244
|
+
/** Performs layout and updates visible views as needed. */
|
|
245
|
+
render(opts: VirtualizerRenderOptions<T>): W[] {
|
|
246
|
+
let mutableThis: Mutable<this> = this;
|
|
247
|
+
let needsLayout = false;
|
|
248
|
+
let offsetChanged = false;
|
|
249
|
+
let sizeChanged = false;
|
|
250
|
+
let itemSizeChanged = false;
|
|
251
|
+
let needsUpdate = false;
|
|
871
252
|
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
253
|
+
if (opts.collection !== this.collection) {
|
|
254
|
+
mutableThis.collection = opts.collection;
|
|
255
|
+
needsLayout = true;
|
|
875
256
|
}
|
|
876
257
|
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
private _applyLayoutInfos() {
|
|
882
|
-
let updated = false;
|
|
883
|
-
|
|
884
|
-
// Apply layout infos to visible views
|
|
885
|
-
for (let view of this._visibleViews.values()) {
|
|
886
|
-
let cur = view.layoutInfo;
|
|
887
|
-
if (cur?.key != null) {
|
|
888
|
-
let layoutInfo = this.layout.getLayoutInfo(cur.key);
|
|
889
|
-
if (this._applyLayoutInfo(view, layoutInfo)) {
|
|
890
|
-
updated = true;
|
|
891
|
-
}
|
|
258
|
+
if (opts.layout !== this.layout) {
|
|
259
|
+
if (this.layout) {
|
|
260
|
+
this.layout.virtualizer = null;
|
|
892
261
|
}
|
|
893
|
-
}
|
|
894
262
|
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
let cur = view.layoutInfo;
|
|
899
|
-
if (cur?.key != null) {
|
|
900
|
-
let layoutInfo = this.layout.getLayoutInfo(cur.key);
|
|
901
|
-
if (this._applyLayoutInfo(view, layoutInfo)) {
|
|
902
|
-
updated = true;
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
for (let view of this._transaction.removed.values()) {
|
|
908
|
-
let cur = view.layoutInfo;
|
|
909
|
-
let layoutInfo = this._transaction.finalLayoutInfo.get(cur.key) || cur;
|
|
910
|
-
layoutInfo = this.layout.getFinalLayoutInfo(layoutInfo.copy());
|
|
911
|
-
if (this._applyLayoutInfo(view, layoutInfo)) {
|
|
912
|
-
updated = true;
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
if (updated) {
|
|
918
|
-
this._flushVisibleViews();
|
|
263
|
+
opts.layout.virtualizer = this;
|
|
264
|
+
mutableThis.layout = opts.layout;
|
|
265
|
+
needsLayout = true;
|
|
919
266
|
}
|
|
920
|
-
}
|
|
921
267
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
268
|
+
if (opts.persistedKeys && !isSetEqual(opts.persistedKeys, this.persistedKeys)) {
|
|
269
|
+
mutableThis.persistedKeys = opts.persistedKeys;
|
|
270
|
+
needsUpdate = true;
|
|
925
271
|
}
|
|
926
272
|
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
cur.opacity !== layoutInfo.opacity ||
|
|
938
|
-
cur.transform !== layoutInfo.transform
|
|
939
|
-
) {
|
|
940
|
-
return true;
|
|
273
|
+
if (!this.visibleRect.equals(opts.visibleRect)) {
|
|
274
|
+
this._overscanManager.setVisibleRect(opts.visibleRect);
|
|
275
|
+
let shouldInvalidate = this.layout.shouldInvalidate(opts.visibleRect, this.visibleRect);
|
|
276
|
+
|
|
277
|
+
if (shouldInvalidate) {
|
|
278
|
+
offsetChanged = !opts.visibleRect.pointEquals(this.visibleRect);
|
|
279
|
+
sizeChanged = !opts.visibleRect.sizeEquals(this.visibleRect);
|
|
280
|
+
needsLayout = true;
|
|
281
|
+
} else {
|
|
282
|
+
needsUpdate = true;
|
|
941
283
|
}
|
|
942
|
-
}
|
|
943
284
|
|
|
944
|
-
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
reuseView(view: ReusableView<T, V>) {
|
|
948
|
-
view.prepareForReuse();
|
|
949
|
-
this._reusableViews[view.viewType].push(view);
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
removeViews(toRemove: Set<ReusableView<T, V>>) {
|
|
953
|
-
for (let view of toRemove) {
|
|
954
|
-
this._children.delete(view);
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
updateItemSize(key: Key, size: Size) {
|
|
959
|
-
// TODO: we should be able to invalidate a single index path
|
|
960
|
-
// @ts-ignore
|
|
961
|
-
if (!this.layout.updateItemSize) {
|
|
962
|
-
return;
|
|
285
|
+
mutableThis.visibleRect = opts.visibleRect;
|
|
963
286
|
}
|
|
964
287
|
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
// @ts-ignore
|
|
973
|
-
let changed = this.layout.updateItemSize(key, size);
|
|
974
|
-
if (changed) {
|
|
975
|
-
this.relayout();
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
startScrolling() {
|
|
980
|
-
this._isScrolling = true;
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
endScrolling() {
|
|
984
|
-
this._isScrolling = false;
|
|
985
|
-
this._correctItemOrder();
|
|
986
|
-
this._flushVisibleViews();
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
private _resetAnimatedContentOffset() {
|
|
990
|
-
// Reset the animated content offset of subviews. See comment in relayoutNow for details.
|
|
991
|
-
if (!this._animatedContentOffset.isOrigin()) {
|
|
992
|
-
this._animatedContentOffset = new Point(0, 0);
|
|
993
|
-
this._applyLayoutInfos();
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
/**
|
|
998
|
-
* Scrolls the item with the given key into view, optionally with an animation.
|
|
999
|
-
* @param key The key of the item to scroll into view.
|
|
1000
|
-
* @param duration The duration of the scroll animation.
|
|
1001
|
-
*/
|
|
1002
|
-
scrollToItem(key: Key, options?: ScrollToItemOptions) {
|
|
1003
|
-
// key can be 0, so check if null or undefined
|
|
1004
|
-
if (key == null) {
|
|
1005
|
-
return;
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
let layoutInfo = this.layout.getLayoutInfo(key);
|
|
1009
|
-
if (!layoutInfo) {
|
|
1010
|
-
return;
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
let {
|
|
1014
|
-
duration = 300,
|
|
1015
|
-
shouldScrollX = true,
|
|
1016
|
-
shouldScrollY = true,
|
|
1017
|
-
offsetX = 0,
|
|
1018
|
-
offsetY = 0
|
|
1019
|
-
} = options;
|
|
1020
|
-
|
|
1021
|
-
let x = this.visibleRect.x;
|
|
1022
|
-
let y = this.visibleRect.y;
|
|
1023
|
-
let minX = layoutInfo.rect.x - offsetX;
|
|
1024
|
-
let minY = layoutInfo.rect.y - offsetY;
|
|
1025
|
-
let maxX = x + this.visibleRect.width;
|
|
1026
|
-
let maxY = y + this.visibleRect.height;
|
|
1027
|
-
|
|
1028
|
-
if (shouldScrollX) {
|
|
1029
|
-
if (minX <= x || maxX === 0) {
|
|
1030
|
-
x = minX;
|
|
1031
|
-
} else if (layoutInfo.rect.maxX > maxX) {
|
|
1032
|
-
x += layoutInfo.rect.maxX - maxX;
|
|
288
|
+
if (opts.invalidationContext !== this._invalidationContext) {
|
|
289
|
+
if (opts.invalidationContext) {
|
|
290
|
+
sizeChanged ||= opts.invalidationContext.sizeChanged || false;
|
|
291
|
+
offsetChanged ||= opts.invalidationContext.offsetChanged || false;
|
|
292
|
+
itemSizeChanged ||= opts.invalidationContext.itemSizeChanged || false;
|
|
293
|
+
needsLayout ||= itemSizeChanged || sizeChanged || offsetChanged;
|
|
294
|
+
needsLayout ||= opts.invalidationContext.layoutOptions !== this._invalidationContext.layoutOptions;
|
|
1033
295
|
}
|
|
296
|
+
this._invalidationContext = opts.invalidationContext;
|
|
1034
297
|
}
|
|
1035
298
|
|
|
1036
|
-
if (
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
299
|
+
if (opts.isScrolling !== this._isScrolling) {
|
|
300
|
+
this._isScrolling = opts.isScrolling;
|
|
301
|
+
if (!opts.isScrolling) {
|
|
302
|
+
// Update to fix the DOM order after scrolling.
|
|
303
|
+
needsUpdate = true;
|
|
1041
304
|
}
|
|
1042
305
|
}
|
|
1043
306
|
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
scrollTo(offset: Point, duration: number = 300): Promise<void> {
|
|
1054
|
-
// Cancel the current scroll animation
|
|
1055
|
-
if (this._scrollAnimation) {
|
|
1056
|
-
this._scrollAnimation.cancel();
|
|
1057
|
-
this._scrollAnimation = null;
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
// Set the content offset synchronously if the duration is zero
|
|
1061
|
-
if (duration <= 0 || this.visibleRect.pointEquals(offset)) {
|
|
1062
|
-
this._setContentOffset(offset);
|
|
1063
|
-
return Promise.resolve();
|
|
307
|
+
if (needsLayout) {
|
|
308
|
+
this.relayout({
|
|
309
|
+
offsetChanged,
|
|
310
|
+
sizeChanged,
|
|
311
|
+
itemSizeChanged,
|
|
312
|
+
layoutOptions: this._invalidationContext.layoutOptions
|
|
313
|
+
});
|
|
314
|
+
} else if (needsUpdate) {
|
|
315
|
+
this.updateSubviews();
|
|
1064
316
|
}
|
|
1065
317
|
|
|
1066
|
-
this.
|
|
1067
|
-
|
|
1068
|
-
this._scrollAnimation = tween(this.visibleRect, offset, duration, easeOut, offset => {this._setContentOffset(offset);});
|
|
1069
|
-
this._scrollAnimation.then(() => {
|
|
1070
|
-
this._scrollAnimation = null;
|
|
1071
|
-
|
|
1072
|
-
// Process view size updates that occurred during the animation.
|
|
1073
|
-
// Only views that are still visible will be actually updated.
|
|
1074
|
-
for (let [key, size] of this._sizeUpdateQueue) {
|
|
1075
|
-
this.updateItemSize(key, size);
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
this._sizeUpdateQueue.clear();
|
|
1079
|
-
this.relayout();
|
|
1080
|
-
this._processTransactionQueue();
|
|
1081
|
-
this.endScrolling();
|
|
1082
|
-
});
|
|
1083
|
-
|
|
1084
|
-
return this._scrollAnimation;
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
private _runTransaction(action: () => void, animated?: boolean) {
|
|
1088
|
-
this._startTransaction();
|
|
1089
|
-
if (this._nextTransaction) {
|
|
1090
|
-
this._nextTransaction.actions.push(action);
|
|
1091
|
-
}
|
|
1092
|
-
this._endTransaction(animated);
|
|
318
|
+
return this.getChildren(null);
|
|
1093
319
|
}
|
|
1094
320
|
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
321
|
+
getChildren(key: Key | null): W[] {
|
|
322
|
+
let parent = key == null ? this._rootView : this._visibleViews.get(key);
|
|
323
|
+
let renderChildren = (parent: ReusableView<T, V>, views: ReusableView<T, V>[]) => views.map(view => {
|
|
324
|
+
return this.delegate.renderWrapper(
|
|
325
|
+
parent,
|
|
326
|
+
view,
|
|
327
|
+
view.children ? Array.from(view.children) : [],
|
|
328
|
+
childViews => renderChildren(view, childViews)
|
|
329
|
+
);
|
|
330
|
+
});
|
|
1099
331
|
|
|
1100
|
-
|
|
332
|
+
return renderChildren(parent, Array.from(parent.children));
|
|
1101
333
|
}
|
|
1102
334
|
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
return false;
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
// Save whether the transaction should be animated.
|
|
1109
|
-
if (animated != null) {
|
|
1110
|
-
this._nextTransaction.animated = animated;
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
// If we haven't reached level 0, we are still in a
|
|
1114
|
-
// nested transaction. Wait for the parent to end.
|
|
1115
|
-
if (--this._nextTransaction.level > 0) {
|
|
1116
|
-
return false;
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
// Do nothing for empty transactions
|
|
1120
|
-
if (this._nextTransaction.actions.length === 0) {
|
|
1121
|
-
this._nextTransaction = null;
|
|
1122
|
-
return false;
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
// Default animations to true
|
|
1126
|
-
if (this._nextTransaction.animated == null) {
|
|
1127
|
-
this._nextTransaction.animated = true;
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
// Enqueue the transaction
|
|
1131
|
-
this._transactionQueue.push(this._nextTransaction);
|
|
1132
|
-
this._nextTransaction = null;
|
|
1133
|
-
|
|
1134
|
-
return true;
|
|
335
|
+
invalidate(context: InvalidationContext) {
|
|
336
|
+
this.delegate.invalidate(context);
|
|
1135
337
|
}
|
|
1136
338
|
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
// to process the next transaction.
|
|
1140
|
-
if (this._transaction || this._scrollAnimation) {
|
|
339
|
+
updateItemSize(key: Key, size: Size) {
|
|
340
|
+
if (!this.layout.updateItemSize) {
|
|
1141
341
|
return;
|
|
1142
342
|
}
|
|
1143
343
|
|
|
1144
|
-
let
|
|
1145
|
-
if (
|
|
1146
|
-
this.
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
private _getContentRect(): Rect {
|
|
1151
|
-
return new Rect(0, 0, this.contentSize.width, this.contentSize.height);
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
private _performTransaction(transaction: Transaction<T, V>) {
|
|
1155
|
-
this._transaction = transaction;
|
|
1156
|
-
|
|
1157
|
-
this.relayoutNow({
|
|
1158
|
-
transaction: transaction,
|
|
1159
|
-
animated: transaction.animated,
|
|
1160
|
-
|
|
1161
|
-
beforeLayout: () => {
|
|
1162
|
-
// Get the initial layout infos for all views before the updates
|
|
1163
|
-
// so we can figure out which views to add and remove.
|
|
1164
|
-
if (transaction.animated) {
|
|
1165
|
-
transaction.initialMap = this._getLayoutInfoMap(this._getContentRect(), true);
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
// Apply the actions that occurred during this transaction
|
|
1169
|
-
for (let action of transaction.actions) {
|
|
1170
|
-
action();
|
|
1171
|
-
}
|
|
1172
|
-
},
|
|
1173
|
-
|
|
1174
|
-
afterLayout: () => {
|
|
1175
|
-
// Get the final layout infos after the updates
|
|
1176
|
-
if (transaction.animated) {
|
|
1177
|
-
transaction.finalMap = this._getLayoutInfoMap(this._getContentRect());
|
|
1178
|
-
this._setupTransactionAnimations(transaction);
|
|
1179
|
-
} else {
|
|
1180
|
-
this._transaction = null;
|
|
1181
|
-
}
|
|
1182
|
-
},
|
|
1183
|
-
|
|
1184
|
-
afterAnimation: () => {
|
|
1185
|
-
// Remove and reuse views when animations are done
|
|
1186
|
-
if (transaction.toRemove.size > 0 || transaction.removed.size > 0) {
|
|
1187
|
-
for (let view of concatIterators(transaction.toRemove.values(), transaction.removed.values())) {
|
|
1188
|
-
this._children.delete(view);
|
|
1189
|
-
this.reuseView(view);
|
|
1190
|
-
}
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
this._transaction = null;
|
|
1194
|
-
|
|
1195
|
-
// Ensure DOM order is correct for accessibility after animations are complete
|
|
1196
|
-
this._correctItemOrder();
|
|
1197
|
-
this._flushVisibleViews();
|
|
1198
|
-
|
|
1199
|
-
this._processTransactionQueue();
|
|
1200
|
-
}
|
|
1201
|
-
});
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
private _setupTransactionAnimations(transaction: Transaction<T, V>) {
|
|
1205
|
-
let {initialMap, finalMap} = transaction;
|
|
1206
|
-
|
|
1207
|
-
// Store initial and final layout infos for animations
|
|
1208
|
-
for (let [key, layoutInfo] of initialMap) {
|
|
1209
|
-
if (finalMap.has(key)) {
|
|
1210
|
-
// Store the initial layout info for use during animations.
|
|
1211
|
-
transaction.initialLayoutInfo.set(key, layoutInfo);
|
|
1212
|
-
} else {
|
|
1213
|
-
// This view was removed. Store the layout info for use
|
|
1214
|
-
// in Layout#getFinalLayoutInfo during animations.
|
|
1215
|
-
transaction.finalLayoutInfo.set(layoutInfo.key, layoutInfo);
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
// Get initial layout infos for views that were added
|
|
1220
|
-
for (let [key, layoutInfo] of finalMap) {
|
|
1221
|
-
if (!initialMap.has(key)) {
|
|
1222
|
-
let initialLayoutInfo = this.layout.getInitialLayoutInfo(layoutInfo.copy());
|
|
1223
|
-
transaction.initialLayoutInfo.set(key, initialLayoutInfo);
|
|
1224
|
-
}
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
// Figure out which views were removed.
|
|
1228
|
-
for (let [key, view] of this._visibleViews) {
|
|
1229
|
-
// If an item has a width of 0, there is no need to remove it from the _visibleViews.
|
|
1230
|
-
// Removing an item with width of 0 can cause a loop where the item gets added, removed,
|
|
1231
|
-
// added, removed... etc in a loop.
|
|
1232
|
-
if (!finalMap.has(key) && view.layoutInfo.rect.width > 0) {
|
|
1233
|
-
transaction.removed.set(key, view);
|
|
1234
|
-
this._visibleViews.delete(key);
|
|
1235
|
-
|
|
1236
|
-
// In case something weird happened, where we have a view but no
|
|
1237
|
-
// initial layout info, use the one attached to the view.
|
|
1238
|
-
if (view.layoutInfo) {
|
|
1239
|
-
if (!transaction.finalLayoutInfo.has(view.layoutInfo.key)) {
|
|
1240
|
-
transaction.finalLayoutInfo.set(view.layoutInfo.key, view.layoutInfo);
|
|
1241
|
-
}
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
344
|
+
let changed = this.layout.updateItemSize(key, size);
|
|
345
|
+
if (changed) {
|
|
346
|
+
this.invalidate({
|
|
347
|
+
itemSizeChanged: true
|
|
348
|
+
});
|
|
1244
349
|
}
|
|
1245
350
|
}
|
|
1246
351
|
}
|