@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.
Files changed (55) hide show
  1. package/dist/Layout.main.js +2 -41
  2. package/dist/Layout.main.js.map +1 -1
  3. package/dist/Layout.mjs +2 -41
  4. package/dist/Layout.module.js +2 -41
  5. package/dist/Layout.module.js.map +1 -1
  6. package/dist/LayoutInfo.main.js.map +1 -1
  7. package/dist/LayoutInfo.module.js.map +1 -1
  8. package/dist/OverscanManager.main.js +8 -43
  9. package/dist/OverscanManager.main.js.map +1 -1
  10. package/dist/OverscanManager.mjs +8 -43
  11. package/dist/OverscanManager.module.js +8 -43
  12. package/dist/OverscanManager.module.js.map +1 -1
  13. package/dist/ReusableView.main.js +23 -0
  14. package/dist/ReusableView.main.js.map +1 -1
  15. package/dist/ReusableView.mjs +23 -0
  16. package/dist/ReusableView.module.js +23 -0
  17. package/dist/ReusableView.module.js.map +1 -1
  18. package/dist/Virtualizer.main.js +123 -710
  19. package/dist/Virtualizer.main.js.map +1 -1
  20. package/dist/Virtualizer.mjs +124 -711
  21. package/dist/Virtualizer.module.js +124 -711
  22. package/dist/Virtualizer.module.js.map +1 -1
  23. package/dist/types.d.ts +64 -225
  24. package/dist/types.d.ts.map +1 -1
  25. package/dist/useVirtualizerState.main.js +39 -40
  26. package/dist/useVirtualizerState.main.js.map +1 -1
  27. package/dist/useVirtualizerState.mjs +40 -41
  28. package/dist/useVirtualizerState.module.js +40 -41
  29. package/dist/useVirtualizerState.module.js.map +1 -1
  30. package/dist/utils.main.js +1 -27
  31. package/dist/utils.main.js.map +1 -1
  32. package/dist/utils.mjs +2 -26
  33. package/dist/utils.module.js +2 -26
  34. package/dist/utils.module.js.map +1 -1
  35. package/package.json +4 -4
  36. package/src/Layout.ts +10 -55
  37. package/src/LayoutInfo.ts +2 -2
  38. package/src/OverscanManager.ts +10 -47
  39. package/src/ReusableView.ts +36 -7
  40. package/src/Virtualizer.ts +163 -1058
  41. package/src/types.ts +16 -38
  42. package/src/useVirtualizerState.ts +40 -39
  43. package/src/utils.ts +0 -52
  44. package/dist/Transaction.main.js +0 -32
  45. package/dist/Transaction.main.js.map +0 -1
  46. package/dist/Transaction.mjs +0 -27
  47. package/dist/Transaction.module.js +0 -27
  48. package/dist/Transaction.module.js.map +0 -1
  49. package/dist/tween.main.js +0 -67
  50. package/dist/tween.main.js.map +0 -1
  51. package/dist/tween.mjs +0 -61
  52. package/dist/tween.module.js +0 -61
  53. package/dist/tween.module.js.map +0 -1
  54. package/src/Transaction.ts +0 -28
  55. package/src/tween.ts +0 -83
@@ -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 {concatIterators, difference, isSetEqual} from './utils';
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 CollectionView class renders a scrollable collection of data using customizable layouts,
34
- * and manages animated updates to the data over time. It supports very large collections by
35
- * only rendering visible views to the DOM, reusing them as you scroll. Collection views can
36
- * present any type of view, including non-item views such as section headers and footers.
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
- * Collection views get their data from a {@link DataSource} object that you provide. Items are
41
- * grouped into sections by the data source, and the collection view calls its methods to retrieve
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, optionally with an animated transition between the layouts.
33
+ * dynamically at runtime as well.
50
34
  *
51
- * Layouts produce information on what views should appear in the collection view, but do not create
52
- * the views themselves directly. It is the responsibility of the {@link CollectionViewDelegate} object
53
- * to create instances of {@link ReusableView} subclasses which render the items into DOM nodes.
54
- * The delegate determines what type of view to display for each item, and creates instances of
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 collection view delegate. The delegate is used by the collection view
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 duration of animated layout changes, in milliseconds. Default is 500ms. */
66
- transitionDuration: number;
67
-
68
- /**
69
- * Whether to enable scroll anchoring. This will attempt to restore the scroll position
70
- * after layout changes outside the viewport. Default is off.
71
- */
72
- anchorScrollPosition: boolean;
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 _children: Set<ReusableView<T, V>>;
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 _sizeUpdateQueue: Map<Key, Size>;
100
- private _animatedContentOffset: Point;
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
- this._reusableViews = {};
110
- this._visibleLayoutInfos = new Map();
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._children = new Set();
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._persistedKeys.has(key)) {
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._persistedKeys) {
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
- * Get the collection view's layout.
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._setContentSize(this.layout.getContentSize());
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
- // Adjust scroll position based on scroll anchor, and constrain.
154
+ // Constrain scroll position.
506
155
  // If the content changed, scroll to the top.
507
- let visibleRect = this.getVisibleRect();
508
- let restoredScrollAnchor = this._restoreScrollAnchor(scrollAnchor, context);
509
- let contentOffsetX = context.contentChanged ? 0 : restoredScrollAnchor.x;
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 this is an animated relayout, we do not immediately scroll because it would be jittery.
517
- // Save the difference between the current and new content offsets, and apply it to the
518
- // individual content items instead. At the end of the animation, we'll reset and set the
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
- hasLayoutUpdates = this.updateSubviews(context.contentChanged);
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 isClientWidthMocked = Object.getOwnPropertyNames(window.HTMLElement.prototype).includes('clientWidth');
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._getContentRect();
178
+ rect = new Rect(0, 0, this.contentSize.width, this.contentSize.height);
668
179
  } else {
669
- rect = this.shouldOverscan ? this._overscanManager.getOverscannedRect() : this.getVisibleRect();
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(forceUpdate = false) {
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
- for (let key of toRemove.keys()) {
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
- // If we are in the middle of a transaction, wait until the end
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 toAdd.keys()) {
762
- let layoutInfo = visibleLayoutInfos.get(key);
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
- // Add the view to the DOM if needed
787
- if (!removed.has(view)) {
788
- this._children.add(view);
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
- this._correctItemOrder();
808
- this._flushVisibleViews();
809
-
810
- let hasLayoutUpdates = this._transaction && (toAdd.size > 0 || toRemove.size > 0 || this._hasLayoutUpdates());
811
- if (hasLayoutUpdates) {
812
- requestAnimationFrame(() => {
813
- // If we're in a transaction, apply animations to visible views
814
- // and "to be removed" views, which animate off screen.
815
- if (this._transaction) {
816
- requestAnimationFrame(() => this._applyLayoutInfos());
817
- }
818
- });
819
- }
820
-
821
- return hasLayoutUpdates;
822
- }
823
-
824
- afterRender() {
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
- getChildren(key: Key) {
858
- let buildTree = (parent: ReusableView<T, V>, views: ReusableView<T, V>[]): W[] => views.map(view => {
859
- let children = this._viewsByParentKey.get(view.layoutInfo.key);
860
- return this.delegate.renderWrapper(
861
- parent,
862
- view,
863
- children,
864
- (childViews) => buildTree(view, childViews)
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
- private _applyLayoutInfo(view: ReusableView<T, V>, layoutInfo: LayoutInfo) {
873
- if (view.layoutInfo === layoutInfo) {
874
- return false;
253
+ if (opts.collection !== this.collection) {
254
+ mutableThis.collection = opts.collection;
255
+ needsLayout = true;
875
256
  }
876
257
 
877
- view.layoutInfo = layoutInfo;
878
- return true;
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
- // Apply final layout infos for views that will be removed
896
- if (this._transaction) {
897
- for (let view of this._transaction.toRemove.values()) {
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
- private _hasLayoutUpdates() {
923
- if (!this._transaction) {
924
- return false;
268
+ if (opts.persistedKeys && !isSetEqual(opts.persistedKeys, this.persistedKeys)) {
269
+ mutableThis.persistedKeys = opts.persistedKeys;
270
+ needsUpdate = true;
925
271
  }
926
272
 
927
- for (let view of this._visibleViews.values()) {
928
- let cur = view.layoutInfo;
929
- if (!cur) {
930
- return true;
931
- }
932
-
933
- let layoutInfo = this.layout.getLayoutInfo(cur.key);
934
- if (
935
- // Uses equals rather than pointEquals so that width/height changes are taken into account
936
- !cur.rect.equals(layoutInfo.rect) ||
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
- return false;
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
- // If the scroll position is currently animating, add the update
966
- // to a queue to be processed after the animation is complete.
967
- if (this._scrollAnimation) {
968
- this._sizeUpdateQueue.set(key, size);
969
- return;
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 (shouldScrollY) {
1037
- if (minY <= y || maxY === 0) {
1038
- y = minY;
1039
- } else if (layoutInfo.rect.maxY > maxY) {
1040
- y += layoutInfo.rect.maxY - maxY;
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
- return this.scrollTo(new Point(x, y), duration);
1045
- }
1046
-
1047
- /**
1048
- * Performs an animated scroll to the given offset.
1049
- * @param offset - The offset to scroll to.
1050
- * @param duration The duration of the animation.
1051
- * @returns A promise that resolves when the animation is complete.
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.startScrolling();
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
- private _startTransaction() {
1096
- if (!this._nextTransaction) {
1097
- this._nextTransaction = new Transaction;
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
- this._nextTransaction.level++;
332
+ return renderChildren(parent, Array.from(parent.children));
1101
333
  }
1102
334
 
1103
- private _endTransaction(animated?: boolean) {
1104
- if (!this._nextTransaction) {
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
- private _processTransactionQueue() {
1138
- // If the current transaction is animating, wait until the end
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 next = this._transactionQueue.shift();
1145
- if (next) {
1146
- this._performTransaction(next);
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
  }