@html-next/vertical-collection 5.0.0 → 5.0.2

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.
@@ -0,0 +1,1676 @@
1
+ import { DEBUG } from '@glimmer/env';
2
+ import { assert } from '@ember/debug';
3
+ import { A } from '@ember/array';
4
+ import { get, set } from '@ember/object';
5
+ import { begin, end, run } from '@ember/runloop';
6
+ import { scheduler, Token } from 'ember-raf-scheduler';
7
+ import { guidFor } from '@ember/object/internals';
8
+
9
+ function identity(item) {
10
+ let key;
11
+ const type = typeof item;
12
+ if (type === 'string' || type === 'number') {
13
+ key = item;
14
+ } else {
15
+ key = guidFor(item);
16
+ }
17
+ return key;
18
+ }
19
+
20
+ function keyForItem(item, keyPath, index) {
21
+ let key;
22
+ assert(`keyPath must be a string, received: ${keyPath}`, typeof keyPath === 'string');
23
+ switch (keyPath) {
24
+ case '@index':
25
+ assert(`A numerical index must be supplied for keyForItem when keyPath is @index, received: ${index}`, typeof index === 'number');
26
+ key = index;
27
+ break;
28
+ case '@identity':
29
+ key = identity(item);
30
+ break;
31
+ default:
32
+ key = get(item, keyPath);
33
+ }
34
+ if (typeof key === 'number') {
35
+ key = String(key);
36
+ }
37
+ return key;
38
+ }
39
+
40
+ const VENDOR_MATCH_FNS = ['matches', 'webkitMatchesSelector', 'mozMatchesSelector', 'msMatchesSelector', 'oMatchesSelector'];
41
+ let ELEMENT_MATCH_FN;
42
+ function setElementMatchFn(el) {
43
+ VENDOR_MATCH_FNS.forEach(fn => {
44
+ if (ELEMENT_MATCH_FN === undefined && typeof el[fn] === 'function') {
45
+ ELEMENT_MATCH_FN = fn;
46
+ }
47
+ });
48
+ }
49
+ function closest(el, selector) {
50
+ if (ELEMENT_MATCH_FN === undefined) {
51
+ setElementMatchFn(el);
52
+ }
53
+ while (el) {
54
+ // TODO add explicit test
55
+ if (el[ELEMENT_MATCH_FN](selector)) {
56
+ return el;
57
+ }
58
+ el = el.parentElement;
59
+ }
60
+ return null;
61
+ }
62
+
63
+ var document$1 = window ? window.document : undefined;
64
+
65
+ let VC_IDENTITY = 0;
66
+ class VirtualComponent {
67
+ constructor(content = null, index = null) {
68
+ this.id = `VC-${VC_IDENTITY++}`;
69
+ this.content = content;
70
+ this.index = index;
71
+
72
+ // We check to see if the document exists in Fastboot. Since RAF won't run in
73
+ // Fastboot, we'll never have to use these text nodes for measurements, so they
74
+ // can be empty
75
+ this.upperBound = document$1 !== undefined ? document$1.createTextNode('') : null;
76
+ this.lowerBound = document$1 !== undefined ? document$1.createTextNode('') : null;
77
+ this.rendered = false;
78
+ if (DEBUG) {
79
+ Object.preventExtensions(this);
80
+ }
81
+ }
82
+ get realUpperBound() {
83
+ return this.upperBound;
84
+ }
85
+ get realLowerBound() {
86
+ return this.lowerBound;
87
+ }
88
+ getBoundingClientRect() {
89
+ let {
90
+ upperBound,
91
+ lowerBound
92
+ } = this;
93
+ let top = Infinity;
94
+ let bottom = -Infinity;
95
+ while (upperBound !== lowerBound) {
96
+ upperBound = upperBound.nextSibling;
97
+ if (upperBound instanceof Element) {
98
+ top = Math.min(top, upperBound.getBoundingClientRect().top);
99
+ bottom = Math.max(bottom, upperBound.getBoundingClientRect().bottom);
100
+ }
101
+ if (DEBUG) {
102
+ if (upperBound instanceof Element) {
103
+ continue;
104
+ }
105
+ const text = upperBound.textContent;
106
+ assert(`All content inside of vertical-collection must be wrapped in an element. Detected a text node with content: ${text}`, text === '' || text.match(/^\s+$/));
107
+ }
108
+ }
109
+ assert('Items in a vertical collection require atleast one element in them', top !== Infinity && bottom !== -Infinity);
110
+ const height = bottom - top;
111
+ return {
112
+ top,
113
+ bottom,
114
+ height
115
+ };
116
+ }
117
+ recycle(newContent, newIndex) {
118
+ assert(`You cannot set an item's content to undefined`, newContent);
119
+ if (this.index !== newIndex) {
120
+ set(this, 'index', newIndex);
121
+ }
122
+ if (this.content !== newContent) {
123
+ set(this, 'content', newContent);
124
+ }
125
+ }
126
+ destroy() {
127
+ set(this, 'upperBound', null);
128
+ set(this, 'lowerBound', null);
129
+ set(this, 'content', null);
130
+ set(this, 'index', null);
131
+ }
132
+ }
133
+
134
+ let OC_IDENTITY = 0;
135
+ class OccludedContent {
136
+ constructor(tagName) {
137
+ this.id = `OC-${OC_IDENTITY++}`;
138
+ this.isOccludedContent = true;
139
+
140
+ // We check to see if the document exists in Fastboot. Since RAF won't run in
141
+ // Fastboot, we'll never have to use these text nodes for measurements, so they
142
+ // can be empty
143
+ if (document$1 !== undefined) {
144
+ this.element = document$1.createElement(tagName);
145
+ this.element.className += 'occluded-content';
146
+ this.upperBound = document$1.createTextNode('');
147
+ this.lowerBound = document$1.createTextNode('');
148
+ } else {
149
+ this.element = null;
150
+ }
151
+ this.isOccludedContent = true;
152
+ this.rendered = false;
153
+ if (DEBUG) {
154
+ Object.preventExtensions(this);
155
+ }
156
+ }
157
+ getBoundingClientRect() {
158
+ if (this.element !== null) {
159
+ return this.element.getBoundingClientRect();
160
+ }
161
+ }
162
+ addEventListener(event, listener) {
163
+ if (this.element !== null) {
164
+ this.element.addEventListener(event, listener);
165
+ }
166
+ }
167
+ removeEventListener(event, listener) {
168
+ if (this.element !== null) {
169
+ this.element.removeEventListener(event, listener);
170
+ }
171
+ }
172
+ get realUpperBound() {
173
+ return this.upperBound;
174
+ }
175
+ get realLowerBound() {
176
+ return this.lowerBound;
177
+ }
178
+ get parentNode() {
179
+ return this.element !== null ? this.element.parentNode : null;
180
+ }
181
+ get style() {
182
+ return this.element !== null ? this.element.style : {};
183
+ }
184
+ set innerHTML(value) {
185
+ if (this.element !== null) {
186
+ this.element.innerHTML = value;
187
+ }
188
+ }
189
+ destroy() {
190
+ set(this, 'element', null);
191
+ }
192
+ }
193
+
194
+ function insertRangeBefore(parent, element, firstNode, lastNode) {
195
+ let nextNode;
196
+ while (firstNode) {
197
+ nextNode = firstNode.nextSibling;
198
+ parent.insertBefore(firstNode, element);
199
+ if (firstNode === lastNode) {
200
+ break;
201
+ }
202
+ firstNode = nextNode;
203
+ }
204
+ }
205
+
206
+ function objectAt(arr, index) {
207
+ assert('arr must be an instance of a Javascript Array or implement `objectAt`', Array.isArray(arr) || typeof arr.objectAt === 'function');
208
+ return arr.objectAt ? arr.objectAt(index) : arr[index];
209
+ }
210
+
211
+ function roundTo(number, decimal = 2) {
212
+ const exp = Math.pow(10, decimal);
213
+ return Math.round(number * exp) / exp;
214
+ }
215
+
216
+ function isPrepend(lenDiff, newItems, key, oldFirstKey, oldLastKey) {
217
+ const newItemsLength = get(newItems, 'length');
218
+ if (lenDiff <= 0 || lenDiff >= newItemsLength || newItemsLength === 0) {
219
+ return false;
220
+ }
221
+ const newFirstKey = keyForItem(objectAt(newItems, lenDiff), key, lenDiff);
222
+ const newLastKey = keyForItem(objectAt(newItems, newItemsLength - 1), key, newItemsLength - 1);
223
+ return oldFirstKey === newFirstKey && oldLastKey === newLastKey;
224
+ }
225
+ function isAppend(lenDiff, newItems, key, oldFirstKey, oldLastKey) {
226
+ const newItemsLength = get(newItems, 'length');
227
+ if (lenDiff <= 0 || lenDiff >= newItemsLength || newItemsLength === 0) {
228
+ return false;
229
+ }
230
+ const newFirstKey = keyForItem(objectAt(newItems, 0), key, 0);
231
+ const newLastKey = keyForItem(objectAt(newItems, newItemsLength - lenDiff - 1), key, newItemsLength - lenDiff - 1);
232
+ return oldFirstKey === newFirstKey && oldLastKey === newLastKey;
233
+ }
234
+
235
+ let supportsPassive = false;
236
+ try {
237
+ let opts = Object.defineProperty({}, 'passive', {
238
+ get() {
239
+ supportsPassive = true;
240
+ return supportsPassive;
241
+ }
242
+ });
243
+ window.addEventListener('test', null, opts);
244
+ } catch {
245
+ // do nothing
246
+ }
247
+ var SUPPORTS_PASSIVE = supportsPassive;
248
+
249
+ const DEFAULT_ARRAY_SIZE = 10;
250
+ const UNDEFINED_VALUE = Object.create(null);
251
+ class ScrollHandler {
252
+ constructor() {
253
+ this.elements = new Array(DEFAULT_ARRAY_SIZE);
254
+ this.maxLength = DEFAULT_ARRAY_SIZE;
255
+ this.length = 0;
256
+ this.handlers = new Array(DEFAULT_ARRAY_SIZE);
257
+ this.isPolling = false;
258
+ this.isUsingPassive = SUPPORTS_PASSIVE;
259
+ }
260
+ addScrollHandler(element, handler) {
261
+ let index = this.elements.indexOf(element);
262
+ let handlers, cache;
263
+ if (index === -1) {
264
+ index = this.length++;
265
+ if (index === this.maxLength) {
266
+ this.maxLength *= 2;
267
+ this.elements.length = this.maxLength;
268
+ this.handlers.length = this.maxLength;
269
+ }
270
+ handlers = [handler];
271
+ this.elements[index] = element;
272
+ cache = this.handlers[index] = {
273
+ top: element.scrollTop,
274
+ left: element.scrollLeft,
275
+ handlers
276
+ };
277
+ // TODO add explicit test
278
+ if (SUPPORTS_PASSIVE) {
279
+ cache.passiveHandler = function () {
280
+ ScrollHandler.triggerElementHandlers(element, cache);
281
+ };
282
+ } else {
283
+ cache.passiveHandler = UNDEFINED_VALUE;
284
+ }
285
+ } else {
286
+ cache = this.handlers[index];
287
+ handlers = cache.handlers;
288
+ handlers.push(handler);
289
+ }
290
+
291
+ // TODO add explicit test
292
+ if (this.isUsingPassive) {
293
+ // Only add the event listener once, if more handlers are present.
294
+ if (handlers.length === 1) {
295
+ element.addEventListener('scroll', cache.passiveHandler, {
296
+ capture: true,
297
+ passive: true
298
+ });
299
+ }
300
+
301
+ // TODO add explicit test
302
+ } else if (!this.isPolling) {
303
+ this.poll();
304
+ }
305
+ }
306
+ removeScrollHandler(element, handler) {
307
+ let index = this.elements.indexOf(element);
308
+ let elementCache = this.handlers[index];
309
+ // TODO add explicit test
310
+ if (elementCache && elementCache.handlers) {
311
+ let index = elementCache.handlers.indexOf(handler);
312
+ if (index === -1) {
313
+ throw new Error('Attempted to remove an unknown handler');
314
+ }
315
+ elementCache.handlers.splice(index, 1);
316
+
317
+ // cleanup element entirely if needed
318
+ // TODO add explicit test
319
+ if (!elementCache.handlers.length) {
320
+ index = this.elements.indexOf(element);
321
+ this.handlers.splice(index, 1);
322
+ this.elements.splice(index, 1);
323
+ this.length--;
324
+ this.maxLength--;
325
+ if (this.length === 0) {
326
+ this.isPolling = false;
327
+ }
328
+
329
+ // TODO add explicit test
330
+ if (this.isUsingPassive) {
331
+ element.removeEventListener('scroll', elementCache.passiveHandler, {
332
+ capture: true,
333
+ passive: true
334
+ });
335
+ }
336
+ }
337
+ } else {
338
+ throw new Error('Attempted to remove a handler from an unknown element or an element with no handlers');
339
+ }
340
+ }
341
+ static triggerElementHandlers(element, meta) {
342
+ let cachedTop = element.scrollTop;
343
+ let cachedLeft = element.scrollLeft;
344
+ let topChanged = cachedTop !== meta.top;
345
+ let leftChanged = cachedLeft !== meta.left;
346
+ meta.top = cachedTop;
347
+ meta.left = cachedLeft;
348
+ let event = {
349
+ top: cachedTop,
350
+ left: cachedLeft
351
+ };
352
+
353
+ // TODO add explicit test
354
+ if (topChanged || leftChanged) {
355
+ begin();
356
+ for (let j = 0; j < meta.handlers.length; j++) {
357
+ meta.handlers[j](event);
358
+ }
359
+ end();
360
+ }
361
+ }
362
+ poll() {
363
+ this.isPolling = true;
364
+ scheduler.schedule('sync', () => {
365
+ // TODO add explicit test
366
+ if (!this.isPolling) {
367
+ return;
368
+ }
369
+ for (let i = 0; i < this.length; i++) {
370
+ let element = this.elements[i];
371
+ let info = this.handlers[i];
372
+ ScrollHandler.triggerElementHandlers(element, info);
373
+ }
374
+ this.isPolling = this.length > 0;
375
+ // TODO add explicit test
376
+ if (this.isPolling) {
377
+ this.poll();
378
+ }
379
+ });
380
+ }
381
+ }
382
+ const instance = new ScrollHandler();
383
+ function addScrollHandler(element, handler) {
384
+ instance.addScrollHandler(element, handler);
385
+ }
386
+ function removeScrollHandler(element, handler) {
387
+ instance.removeScrollHandler(element, handler);
388
+ }
389
+
390
+ /*
391
+ * There are significant differences between browsers
392
+ * in how they implement "scroll" on document.body
393
+ *
394
+ * The only cross-browser listener for scroll on body
395
+ * is to listen on window with capture.
396
+ *
397
+ * They also implement different standards for how to
398
+ * access the scroll position.
399
+ *
400
+ * This singleton class provides a cross-browser way
401
+ * to access and set the scrollTop and scrollLeft properties.
402
+ *
403
+ */
404
+ function ViewportContainer() {
405
+ // A bug occurs in Chrome when we reload the browser at a lower
406
+ // scrollTop, window.scrollY becomes stuck on a single value.
407
+ Object.defineProperty(this, 'scrollTop', {
408
+ get() {
409
+ return document.body.scrollTop || document.documentElement.scrollTop;
410
+ },
411
+ set(v) {
412
+ document.body.scrollTop = document.documentElement.scrollTop = v;
413
+ }
414
+ });
415
+ Object.defineProperty(this, 'scrollLeft', {
416
+ get() {
417
+ return window.scrollX || window.pageXOffset || document.body.scrollLeft || document.documentElement.scrollLeft;
418
+ },
419
+ set(v) {
420
+ window.scrollX = window.pageXOffset = document.body.scrollLeft = document.documentElement.scrollLeft = v;
421
+ }
422
+ });
423
+ Object.defineProperty(this, 'offsetHeight', {
424
+ get() {
425
+ return window.innerHeight;
426
+ }
427
+ });
428
+ }
429
+ ViewportContainer.prototype.addEventListener = function addEventListener(event, handler, options) {
430
+ return window.addEventListener(event, handler, options);
431
+ };
432
+ ViewportContainer.prototype.removeEventListener = function addEventListener(event, handler, options) {
433
+ return window.removeEventListener(event, handler, options);
434
+ };
435
+ ViewportContainer.prototype.getBoundingClientRect = function getBoundingClientRect() {
436
+ return {
437
+ height: window.innerHeight,
438
+ width: window.innerWidth,
439
+ top: 0,
440
+ left: 0,
441
+ right: window.innerWidth,
442
+ bottom: window.innerHeight
443
+ };
444
+ };
445
+ var ViewportContainer$1 = new ViewportContainer();
446
+
447
+ function estimateElementHeight(element, fallbackHeight) {
448
+ assert(`You called estimateElement height without a fallbackHeight`, fallbackHeight);
449
+ assert(`You called estimateElementHeight without an element`, element);
450
+ if (fallbackHeight.indexOf('%') !== -1) {
451
+ return getPercentageHeight(element, fallbackHeight);
452
+ }
453
+ if (fallbackHeight.indexOf('em') !== -1) {
454
+ return getEmHeight(element, fallbackHeight);
455
+ }
456
+ return parseInt(fallbackHeight, 10);
457
+ }
458
+ function getPercentageHeight(element, fallbackHeight) {
459
+ // We use offsetHeight here to get the element's true height, rather than the
460
+ // bounding rect which may be scaled with transforms
461
+ let parentHeight = element.offsetHeight;
462
+ let percent = parseFloat(fallbackHeight);
463
+ return percent * parentHeight / 100.0;
464
+ }
465
+ function getEmHeight(element, fallbackHeight) {
466
+ const fontSizeElement = fallbackHeight.indexOf('rem') !== -1 ? document.documentElement : element;
467
+ const fontSize = window.getComputedStyle(fontSizeElement).getPropertyValue('font-size');
468
+ return parseFloat(fallbackHeight) * parseFloat(fontSize);
469
+ }
470
+
471
+ function getScaledClientRect(element, scale) {
472
+ const rect = element.getBoundingClientRect();
473
+ if (scale === 1) {
474
+ return rect;
475
+ }
476
+ const scaled = {};
477
+ for (let key in rect) {
478
+ scaled[key] = rect[key] * scale;
479
+ }
480
+ return scaled;
481
+ }
482
+
483
+ class Radar {
484
+ constructor(parentToken, {
485
+ bufferSize,
486
+ containerSelector,
487
+ estimateHeight,
488
+ initialRenderCount,
489
+ items,
490
+ key,
491
+ renderAll,
492
+ renderFromLast,
493
+ shouldRecycle,
494
+ startingIndex,
495
+ occlusionTagName
496
+ }) {
497
+ this.token = new Token(parentToken);
498
+
499
+ // Public API
500
+ this.bufferSize = bufferSize;
501
+ this.containerSelector = containerSelector;
502
+ this.estimateHeight = estimateHeight;
503
+ this.initialRenderCount = initialRenderCount;
504
+ this.items = items;
505
+ this.key = key;
506
+ this.renderAll = renderAll;
507
+ this.renderFromLast = renderFromLast;
508
+ this.shouldRecycle = shouldRecycle;
509
+ this.startingIndex = startingIndex;
510
+
511
+ // defaults to a no-op intentionally, actions will only be sent if they
512
+ // are passed into the component
513
+ this.sendAction = () => {};
514
+
515
+ // Calculated constants
516
+ this._itemContainer = null;
517
+ this._scrollContainer = null;
518
+ this._prependOffset = 0;
519
+ this._calculatedEstimateHeight = 0;
520
+ this._collectionOffset = 0;
521
+ this._calculatedScrollContainerHeight = 0;
522
+ this._transformScale = 1;
523
+
524
+ // Event handler
525
+ this._scrollHandler = ({
526
+ top
527
+ }) => {
528
+ // debounce scheduling updates by checking to make sure we've moved a minimum amount
529
+ if (this._didEarthquake(Math.abs(this._scrollTop - top))) {
530
+ this.scheduleUpdate();
531
+ }
532
+ };
533
+ this._resizeHandler = this.scheduleUpdate.bind(this);
534
+
535
+ // Run state
536
+ this._nextUpdate = null;
537
+ this._nextLayout = null;
538
+ this._started = false;
539
+ this._didReset = true;
540
+ this._didUpdateItems = false;
541
+
542
+ // Cache state
543
+ this._scrollTop = 0;
544
+
545
+ // Setting these values to infinity starts us in a guaranteed good state for the radar,
546
+ // so it knows that it needs to run certain measurements, etc.
547
+ this._prevFirstItemIndex = Infinity;
548
+ this._prevLastItemIndex = -Infinity;
549
+ this._prevFirstVisibleIndex = 0;
550
+ this._prevLastVisibleIndex = 0;
551
+ this._firstReached = false;
552
+ this._lastReached = false;
553
+ this._prevTotalItems = 0;
554
+ this._prevFirstKey = 0;
555
+ this._prevLastKey = 0;
556
+ this._componentPool = [];
557
+ this._prependComponentPool = [];
558
+ this._appendComponentPool = []; // https://github.com/html-next/vertical-collection/issues/296
559
+
560
+ // Boundaries
561
+ this._occludedContentBefore = new OccludedContent(occlusionTagName);
562
+ this._occludedContentAfter = new OccludedContent(occlusionTagName);
563
+ this._pageUpHandler = this.pageUp.bind(this);
564
+ this._occludedContentBefore.addEventListener('click', this._pageUpHandler);
565
+ this._pageDownHandler = this.pageDown.bind(this);
566
+ this._occludedContentAfter.addEventListener('click', this._pageDownHandler);
567
+
568
+ // Element to hold pooled component DOM when not in use
569
+ if (document$1) {
570
+ this._domPool = document$1.createDocumentFragment();
571
+ }
572
+
573
+ // Initialize virtual components
574
+ this.virtualComponents = A([this._occludedContentBefore, this._occludedContentAfter]);
575
+ this.orderedComponents = [];
576
+ this._updateVirtualComponents();
577
+
578
+ // In older versions of Ember/IE, binding anything on an object in the template
579
+ // adds observers which creates __ember_meta__
580
+ this.__ember_meta__ = null;
581
+ if (DEBUG) {
582
+ this._debugDidUpdate = null;
583
+ }
584
+ }
585
+ destroy() {
586
+ this.token.cancel();
587
+ for (let i = 0; i < this.orderedComponents.length; i++) {
588
+ this.orderedComponents[i].destroy();
589
+ }
590
+
591
+ // Boundaries
592
+ this._occludedContentBefore.removeEventListener('click', this._pageUpHandler);
593
+ this._occludedContentAfter.removeEventListener('click', this._pageDownHandler);
594
+ this._occludedContentBefore.destroy();
595
+ this._occludedContentAfter.destroy();
596
+ this.orderedComponents = null;
597
+ set(this, 'virtualComponents', null);
598
+ if (this._started) {
599
+ removeScrollHandler(this._scrollContainer, this._scrollHandler);
600
+ ViewportContainer$1.removeEventListener('resize', this._resizeHandler);
601
+ }
602
+ }
603
+ schedule(queueName, job) {
604
+ return scheduler.schedule(queueName, job, this.token);
605
+ }
606
+
607
+ /**
608
+ * Start the Radar. Does initial measurements, adds event handlers,
609
+ * sets up initial scroll state, and
610
+ */
611
+ start() {
612
+ const {
613
+ startingIndex,
614
+ containerSelector,
615
+ _occludedContentBefore
616
+ } = this;
617
+
618
+ // Use the occluded content element, which has been inserted into the DOM,
619
+ // to find the item container and the scroll container
620
+ this._itemContainer = _occludedContentBefore.element.parentNode;
621
+ this._scrollContainer = containerSelector === 'body' ? ViewportContainer$1 : closest(this._itemContainer, containerSelector);
622
+ this._updateConstants();
623
+
624
+ // Setup initial scroll state
625
+ if (startingIndex !== 0) {
626
+ const {
627
+ renderFromLast,
628
+ _calculatedEstimateHeight,
629
+ _collectionOffset,
630
+ _calculatedScrollContainerHeight
631
+ } = this;
632
+ let startingScrollTop = startingIndex * _calculatedEstimateHeight;
633
+ if (renderFromLast) {
634
+ startingScrollTop -= _calculatedScrollContainerHeight - _calculatedEstimateHeight;
635
+ }
636
+
637
+ // initialize the scrollTop value, which will be applied to the
638
+ // scrollContainer after the collection has been initialized
639
+ this._scrollTop = startingScrollTop + _collectionOffset;
640
+ this._prevFirstVisibleIndex = startingIndex;
641
+ } else {
642
+ this._scrollTop = this._scrollContainer.scrollTop;
643
+ }
644
+ this._started = true;
645
+ this.update();
646
+
647
+ // Setup event handlers
648
+ addScrollHandler(this._scrollContainer, this._scrollHandler);
649
+ ViewportContainer$1.addEventListener('resize', this._resizeHandler);
650
+ }
651
+
652
+ /*
653
+ * Schedules an update for the next RAF
654
+ *
655
+ * This will first run _updateVirtualComponents in the sync phase, which figures out what
656
+ * components need to be rerendered and updates the appropriate VCs and moves their associated
657
+ * DOM. At the end of the `sync` phase the runloop is flushed and Glimmer renders the changes.
658
+ *
659
+ * By the `affect` phase the Radar should have had time to measure, meaning it has all of the
660
+ * current info and we can send actions for any changes.
661
+ *
662
+ * @private
663
+ */
664
+ scheduleUpdate(didUpdateItems, promiseResolve) {
665
+ if (didUpdateItems === true) {
666
+ // Set the update items flag first, in case scheduleUpdate has already been called
667
+ // but the RAF hasn't yet run
668
+ this._didUpdateItems = true;
669
+ }
670
+ if (this._nextUpdate !== null || this._started === false) {
671
+ return;
672
+ }
673
+ this._nextUpdate = this.schedule('sync', () => {
674
+ this._nextUpdate = null;
675
+ this._scrollTop = this._scrollContainer.scrollTop;
676
+ this.update(promiseResolve);
677
+ });
678
+ }
679
+ update(promiseResolve) {
680
+ if (this._didUpdateItems === true) {
681
+ this._determineUpdateType();
682
+ this._didUpdateItems = false;
683
+ }
684
+ this._updateConstants();
685
+ this._updateIndexes();
686
+ this._updateVirtualComponents();
687
+ this.schedule('measure', () => {
688
+ if (promiseResolve) {
689
+ promiseResolve();
690
+ }
691
+ this.afterUpdate();
692
+ });
693
+ }
694
+ afterUpdate() {
695
+ const {
696
+ _prevTotalItems: totalItems
697
+ } = this;
698
+ const scrollDiff = this._calculateScrollDiff();
699
+ if (scrollDiff !== 0) {
700
+ this._scrollContainer.scrollTop += scrollDiff;
701
+ }
702
+
703
+ // Re-sync scrollTop, since Chrome may have intervened
704
+ this._scrollTop = this._scrollContainer.scrollTop;
705
+
706
+ // Unset prepend offset, we're done with any prepend changes at this point
707
+ this._prependOffset = 0;
708
+ if (totalItems !== 0) {
709
+ this._sendActions();
710
+ }
711
+
712
+ // Cache previous values
713
+ this._prevFirstItemIndex = this.firstItemIndex;
714
+ this._prevLastItemIndex = this.lastItemIndex;
715
+ this._prevFirstVisibleIndex = this.firstVisibleIndex;
716
+ this._prevLastVisibleIndex = this.lastVisibleIndex;
717
+
718
+ // Clear the reset flag
719
+ this._didReset = false;
720
+ if (DEBUG && this._debugDidUpdate !== null) {
721
+ // Hook to update the visual debugger
722
+ this._debugDidUpdate(this);
723
+ }
724
+ }
725
+
726
+ /*
727
+ * The scroll diff is the difference between where we want the container's scrollTop to be,
728
+ * and where it actually is right now. By default it accounts for the `_prependOffset`, which
729
+ * is set when items are added to the front of the collection, as well as any discrepancies
730
+ * that may have arisen between the cached `_scrollTop` value and the actually container's
731
+ * scrollTop. The container's scrollTop may be modified by the browser when we manipulate DOM
732
+ * (Chrome specifically does this a lot), so `_scrollTop` should be considered the canonical
733
+ * scroll top.
734
+ *
735
+ * Subclasses should override this method to provide any difference between expected item size
736
+ * pre-render and actual item size post-render.
737
+ */
738
+ _calculateScrollDiff() {
739
+ return this._prependOffset + this._scrollTop - this._scrollContainer.scrollTop;
740
+ }
741
+ _determineUpdateType() {
742
+ const {
743
+ items,
744
+ key,
745
+ totalItems,
746
+ _prevTotalItems,
747
+ _prevFirstKey,
748
+ _prevLastKey
749
+ } = this;
750
+ const lenDiff = totalItems - _prevTotalItems;
751
+ if (isPrepend(lenDiff, items, key, _prevFirstKey, _prevLastKey) === true) {
752
+ this.prepend(lenDiff);
753
+ } else if (isAppend(lenDiff, items, key, _prevFirstKey, _prevLastKey) === true) {
754
+ this.append(lenDiff);
755
+ } else {
756
+ this.reset();
757
+ }
758
+ const firstItem = objectAt(this.items, 0);
759
+ const lastItem = objectAt(this.items, this.totalItems - 1);
760
+ this._prevTotalItems = totalItems;
761
+ this._prevFirstKey = totalItems > 0 ? keyForItem(firstItem, key, 0) : 0;
762
+ this._prevLastKey = totalItems > 0 ? keyForItem(lastItem, key, totalItems - 1) : 0;
763
+ }
764
+ _updateConstants() {
765
+ const {
766
+ estimateHeight,
767
+ _occludedContentBefore,
768
+ _itemContainer,
769
+ _scrollContainer
770
+ } = this;
771
+ assert('Must provide a `estimateHeight` value to vertical-collection', estimateHeight !== null);
772
+ assert('itemContainer must be set on Radar before scheduling an update', _itemContainer !== null);
773
+ assert('scrollContainer must be set on Radar before scheduling an update', _scrollContainer !== null);
774
+
775
+ // The scroll container's offsetHeight will reflect the actual height of the element, while
776
+ // it's measured height via bounding client rect will reflect the height with any transformations
777
+ // applied. We use this to find out the scale of the items so we can store measurements at the
778
+ // correct heights.
779
+ const scrollContainerOffsetHeight = _scrollContainer.offsetHeight;
780
+ const {
781
+ height: scrollContainerRenderedHeight
782
+ } = _scrollContainer.getBoundingClientRect();
783
+ let transformScale;
784
+
785
+ // transformScale represents the opposite of the scale, if any, applied to the collection. Check for equality
786
+ // to guard against floating point errors, and check to make sure we're not dividing by zero (default to scale 1 if so)
787
+ if (scrollContainerOffsetHeight === scrollContainerRenderedHeight || scrollContainerRenderedHeight === 0) {
788
+ transformScale = 1;
789
+ } else {
790
+ transformScale = scrollContainerOffsetHeight / scrollContainerRenderedHeight;
791
+ }
792
+ const {
793
+ top: scrollContentTop
794
+ } = getScaledClientRect(_occludedContentBefore, transformScale);
795
+ const {
796
+ top: scrollContainerTop
797
+ } = getScaledClientRect(_scrollContainer, transformScale);
798
+ let scrollContainerMaxHeight = 0;
799
+ if (_scrollContainer instanceof Element) {
800
+ const maxHeightStyle = window.getComputedStyle(_scrollContainer).maxHeight;
801
+ if (maxHeightStyle && maxHeightStyle !== 'none') {
802
+ scrollContainerMaxHeight = estimateElementHeight(_scrollContainer.parentElement, maxHeightStyle);
803
+ }
804
+ }
805
+ const calculatedEstimateHeight = typeof estimateHeight === 'string' && estimateHeight ? estimateElementHeight(_itemContainer, estimateHeight) : estimateHeight;
806
+ assert(`calculatedEstimateHeight must be greater than 0, instead was "${calculatedEstimateHeight}" based on estimateHeight: ${estimateHeight}`, calculatedEstimateHeight > 0);
807
+ this._transformScale = transformScale;
808
+ this._calculatedEstimateHeight = calculatedEstimateHeight;
809
+ this._calculatedScrollContainerHeight = roundTo(Math.max(scrollContainerOffsetHeight, scrollContainerMaxHeight));
810
+
811
+ // The offset between the top of the collection and the top of the scroll container. Determined by finding
812
+ // the distance from the collection is from the top of the scroll container's content (scrollTop + actual position)
813
+ // and subtracting the scroll containers actual top.
814
+ this._collectionOffset = roundTo(_scrollContainer.scrollTop + scrollContentTop - scrollContainerTop);
815
+ }
816
+
817
+ /*
818
+ * Updates virtualComponents, which is meant to be a static pool of components that we render to.
819
+ * In order to decrease the time spent rendering and diffing, we pull the {{each}} out of the DOM
820
+ * and only replace the content of _virtualComponents which are removed/added.
821
+ *
822
+ * For instance, if we start with the following and scroll down, items 2 and 3 do not need to be
823
+ * rerendered, only item 1 needs to be removed and only item 4 needs to be added. So we replace
824
+ * item 1 with item 4, and then manually move the DOM:
825
+ *
826
+ * 1 4 2
827
+ * 2 -> replace 1 with 4 -> 2 -> manually move DOM -> 3
828
+ * 3 3 4
829
+ *
830
+ * However, _virtualComponents is still out of order. Rather than keep track of the state of
831
+ * things in _virtualComponents, we track the visually ordered components in the
832
+ * _orderedComponents array. This is possible because all of our operations are relatively simple,
833
+ * popping some number of components off one end and pushing them onto the other.
834
+ *
835
+ * @private
836
+ */
837
+ _updateVirtualComponents() {
838
+ const {
839
+ items,
840
+ orderedComponents,
841
+ virtualComponents,
842
+ _componentPool,
843
+ shouldRecycle,
844
+ renderAll,
845
+ _started,
846
+ _didReset,
847
+ _occludedContentBefore,
848
+ _occludedContentAfter,
849
+ totalItems
850
+ } = this;
851
+ let renderedFirstItemIndex, renderedLastItemIndex, renderedTotalBefore, renderedTotalAfter;
852
+ if (renderAll === true) {
853
+ // All items should be rendered, set indexes based on total item count
854
+ renderedFirstItemIndex = 0;
855
+ renderedLastItemIndex = totalItems - 1;
856
+ renderedTotalBefore = 0;
857
+ renderedTotalAfter = 0;
858
+ } else if (_started === false) {
859
+ // The Radar hasn't been started yet, render the initialRenderCount if it exists
860
+ renderedFirstItemIndex = this.startingIndex;
861
+ renderedLastItemIndex = this.startingIndex + this.initialRenderCount - 1;
862
+ renderedTotalBefore = 0;
863
+ renderedTotalAfter = 0;
864
+ } else {
865
+ renderedFirstItemIndex = this.firstItemIndex;
866
+ renderedLastItemIndex = this.lastItemIndex;
867
+ renderedTotalBefore = this.totalBefore;
868
+ renderedTotalAfter = this.totalAfter;
869
+ }
870
+
871
+ // If there are less items available than rendered, we drop the last rendered item index
872
+ renderedLastItemIndex = Math.min(renderedLastItemIndex, totalItems - 1);
873
+
874
+ // Add components to be recycled to the pool
875
+ while (orderedComponents.length > 0 && orderedComponents[0].index < renderedFirstItemIndex) {
876
+ _componentPool.push(orderedComponents.shift());
877
+ }
878
+ while (orderedComponents.length > 0 && orderedComponents[orderedComponents.length - 1].index > renderedLastItemIndex) {
879
+ _componentPool.unshift(orderedComponents.pop());
880
+ }
881
+ if (_didReset) {
882
+ if (shouldRecycle === true) {
883
+ for (let i = 0; i < orderedComponents.length; i++) {
884
+ // If the underlying array has changed, the indexes could be the same but
885
+ // the content may have changed, so recycle the remaining components
886
+ const component = orderedComponents[i];
887
+ component.recycle(objectAt(items, component.index), component.index);
888
+ }
889
+ } else {
890
+ while (orderedComponents.length > 0) {
891
+ // If recycling is disabled we need to delete all components and clear the array
892
+ _componentPool.push(orderedComponents.shift());
893
+ }
894
+ }
895
+ }
896
+ let firstIndexInList = orderedComponents.length > 0 ? orderedComponents[0].index : renderedFirstItemIndex;
897
+ let lastIndexInList = orderedComponents.length > 0 ? orderedComponents[orderedComponents.length - 1].index : renderedFirstItemIndex - 1;
898
+
899
+ // Append as many items as needed to the rendered components
900
+ while (lastIndexInList < renderedLastItemIndex) {
901
+ let component;
902
+ if (shouldRecycle === true) {
903
+ component = _componentPool.pop() || new VirtualComponent();
904
+ } else {
905
+ component = new VirtualComponent();
906
+ }
907
+ const itemIndex = ++lastIndexInList;
908
+ component.recycle(objectAt(items, itemIndex), itemIndex);
909
+ this._appendComponent(component);
910
+ orderedComponents.push(component);
911
+ }
912
+
913
+ // Prepend as many items as needed to the rendered components
914
+ while (firstIndexInList > renderedFirstItemIndex) {
915
+ let component;
916
+ if (shouldRecycle === true) {
917
+ component = _componentPool.pop() || new VirtualComponent();
918
+ } else {
919
+ component = new VirtualComponent();
920
+ }
921
+ const itemIndex = --firstIndexInList;
922
+ component.recycle(objectAt(items, itemIndex), itemIndex);
923
+ this._prependComponent(component);
924
+ orderedComponents.unshift(component);
925
+ }
926
+
927
+ // If there are any items remaining in the pool, remove them
928
+ if (_componentPool.length > 0) {
929
+ // Grab the DOM of the remaining components and move it to temporary node disconnected from
930
+ // the body if the item can be reused later otherwise delete the component to avoid virtual re-rendering of the
931
+ // deleted item. If we end up using these components again, we'll grab their DOM and put it back
932
+ for (let i = _componentPool.length - 1; i >= 0; i--) {
933
+ const component = _componentPool[i];
934
+ const item = objectAt(items, component.index);
935
+ if (shouldRecycle === true && item) {
936
+ insertRangeBefore(this._domPool, null, component.realUpperBound, component.realLowerBound);
937
+ } else {
938
+ // Insert the virtual component bound back to make sure Glimmer is
939
+ // not confused about the state of the DOM.
940
+ insertRangeBefore(this._itemContainer, null, component.realUpperBound, component.realLowerBound);
941
+ run(() => {
942
+ virtualComponents.removeObject(component);
943
+ });
944
+ _componentPool.splice(i, 1);
945
+ }
946
+ }
947
+ }
948
+ const totalItemsBefore = renderedFirstItemIndex;
949
+ const totalItemsAfter = totalItems - renderedLastItemIndex - 1;
950
+ const beforeItemsText = totalItemsBefore === 1 ? 'item' : 'items';
951
+ const afterItemsText = totalItemsAfter === 1 ? 'item' : 'items';
952
+
953
+ // Set padding element heights.
954
+ _occludedContentBefore.style.height = `${Math.max(renderedTotalBefore, 0)}px`;
955
+ _occludedContentBefore.innerHTML = totalItemsBefore > 0 ? `And ${totalItemsBefore} ${beforeItemsText} before` : '';
956
+ _occludedContentAfter.style.height = `${Math.max(renderedTotalAfter, 0)}px`;
957
+ _occludedContentAfter.innerHTML = totalItemsAfter > 0 ? `And ${totalItemsAfter} ${afterItemsText} after` : '';
958
+ }
959
+ _appendComponent(component) {
960
+ const {
961
+ virtualComponents,
962
+ _occludedContentAfter,
963
+ _appendComponentPool,
964
+ shouldRecycle,
965
+ _itemContainer
966
+ } = this;
967
+ const relativeNode = _occludedContentAfter.realUpperBound;
968
+ if (component.rendered === true) {
969
+ insertRangeBefore(_itemContainer, relativeNode, component.realUpperBound, component.realLowerBound);
970
+ } else {
971
+ virtualComponents.insertAt(virtualComponents.length - 1, component);
972
+ component.rendered = true;
973
+
974
+ // shouldRecycle=false breaks UI when scrolling the elements fast.
975
+ // Reference https://github.com/html-next/vertical-collection/issues/296
976
+ // Components that are both new and appended still need to be rendered at the end because Glimmer.
977
+ // We have to move them _after_ they render, so we schedule that if they exist
978
+ if (!shouldRecycle) {
979
+ _appendComponentPool.unshift(component);
980
+ if (this._nextLayout === null) {
981
+ this._nextLayout = this.schedule('layout', () => {
982
+ this._nextLayout = null;
983
+ while (_appendComponentPool.length > 0) {
984
+ const component = _appendComponentPool.pop();
985
+
986
+ // Changes with each inserted component
987
+ const relativeNode = _occludedContentAfter.realUpperBound;
988
+ insertRangeBefore(this._itemContainer, relativeNode, component.realUpperBound, component.realLowerBound);
989
+ }
990
+ });
991
+ }
992
+ }
993
+ }
994
+ }
995
+ _prependComponent(component) {
996
+ const {
997
+ virtualComponents,
998
+ _occludedContentBefore,
999
+ _prependComponentPool,
1000
+ _itemContainer
1001
+ } = this;
1002
+ const relativeNode = _occludedContentBefore.realLowerBound.nextSibling;
1003
+ if (component.rendered === true) {
1004
+ insertRangeBefore(_itemContainer, relativeNode, component.realUpperBound, component.realLowerBound);
1005
+ } else {
1006
+ virtualComponents.insertAt(virtualComponents.length - 1, component);
1007
+ component.rendered = true;
1008
+
1009
+ // Components that are both new and prepended still need to be rendered at the end because Glimmer.
1010
+ // We have to move them _after_ they render, so we schedule that if they exist
1011
+ _prependComponentPool.unshift(component);
1012
+ if (this._nextLayout === null) {
1013
+ this._nextLayout = this.schedule('layout', () => {
1014
+ this._nextLayout = null;
1015
+ while (_prependComponentPool.length > 0) {
1016
+ const component = _prependComponentPool.pop();
1017
+
1018
+ // Changes with each inserted component
1019
+ const relativeNode = _occludedContentBefore.realLowerBound.nextSibling;
1020
+ insertRangeBefore(_itemContainer, relativeNode, component.realUpperBound, component.realLowerBound);
1021
+ }
1022
+ });
1023
+ }
1024
+ }
1025
+ }
1026
+ _sendActions() {
1027
+ const {
1028
+ firstItemIndex,
1029
+ lastItemIndex,
1030
+ firstVisibleIndex,
1031
+ lastVisibleIndex,
1032
+ _prevFirstVisibleIndex,
1033
+ _prevLastVisibleIndex,
1034
+ totalItems,
1035
+ _firstReached,
1036
+ _lastReached,
1037
+ _didReset
1038
+ } = this;
1039
+ if (_didReset || firstVisibleIndex !== _prevFirstVisibleIndex) {
1040
+ this.sendAction('firstVisibleChanged', firstVisibleIndex);
1041
+ }
1042
+ if (_didReset || lastVisibleIndex !== _prevLastVisibleIndex) {
1043
+ this.sendAction('lastVisibleChanged', lastVisibleIndex);
1044
+ }
1045
+ if (_firstReached === false && firstItemIndex === 0) {
1046
+ this.sendAction('firstReached', firstItemIndex);
1047
+ this._firstReached = true;
1048
+ }
1049
+ if (_lastReached === false && lastItemIndex === totalItems - 1) {
1050
+ this.sendAction('lastReached', lastItemIndex);
1051
+ this._lastReached = true;
1052
+ }
1053
+ }
1054
+ prepend(numPrepended) {
1055
+ this._prevFirstItemIndex += numPrepended;
1056
+ this._prevLastItemIndex += numPrepended;
1057
+ this.orderedComponents.forEach(c => set(c, 'index', get(c, 'index') + numPrepended));
1058
+ this._firstReached = false;
1059
+ this._prependOffset = numPrepended * this._calculatedEstimateHeight;
1060
+ }
1061
+ append() {
1062
+ this._lastReached = false;
1063
+ }
1064
+ reset() {
1065
+ this._firstReached = false;
1066
+ this._lastReached = false;
1067
+ this._didReset = true;
1068
+ }
1069
+ pageUp() {
1070
+ if (this.renderAll) {
1071
+ return; // All items rendered, no need to page up
1072
+ }
1073
+ const {
1074
+ bufferSize,
1075
+ firstItemIndex,
1076
+ totalComponents
1077
+ } = this;
1078
+ if (firstItemIndex !== 0) {
1079
+ const newFirstItemIndex = Math.max(firstItemIndex - totalComponents + bufferSize, 0);
1080
+ const offset = this.getOffsetForIndex(newFirstItemIndex);
1081
+ this._scrollContainer.scrollTop = offset + this._collectionOffset;
1082
+ this.scheduleUpdate();
1083
+ }
1084
+ }
1085
+ pageDown() {
1086
+ if (this.renderAll) {
1087
+ return; // All items rendered, no need to page down
1088
+ }
1089
+ const {
1090
+ bufferSize,
1091
+ lastItemIndex,
1092
+ totalComponents,
1093
+ totalItems
1094
+ } = this;
1095
+ if (lastItemIndex !== totalItems - 1) {
1096
+ const newFirstItemIndex = Math.min(lastItemIndex + bufferSize + 1, totalItems - totalComponents);
1097
+ const offset = this.getOffsetForIndex(newFirstItemIndex);
1098
+ this._scrollContainer.scrollTop = offset + this._collectionOffset;
1099
+ this.scheduleUpdate();
1100
+ }
1101
+ }
1102
+ get totalComponents() {
1103
+ return Math.min(this.totalItems, this.lastItemIndex - this.firstItemIndex + 1);
1104
+ }
1105
+
1106
+ /*
1107
+ * `prependOffset` exists because there are times when we need to do the following in this exact
1108
+ * order:
1109
+ *
1110
+ * 1. Prepend, which means we need to adjust the scroll position by `estimateHeight * numPrepended`
1111
+ * 2. Calculate the items that will be displayed after the prepend, and move VCs around as
1112
+ * necessary (`scheduleUpdate`).
1113
+ * 3. Actually add the amount prepended to `scrollContainer.scrollTop`
1114
+ *
1115
+ * This is due to some strange behavior in Chrome where it will modify `scrollTop` on it's own
1116
+ * when prepending item elements. We seem to avoid this behavior by doing these things in a RAF
1117
+ * in this exact order.
1118
+ */
1119
+ get visibleTop() {
1120
+ return Math.max(this._scrollTop - this._collectionOffset + this._prependOffset, 0);
1121
+ }
1122
+ get visibleMiddle() {
1123
+ return this.visibleTop + this._calculatedScrollContainerHeight / 2;
1124
+ }
1125
+ get visibleBottom() {
1126
+ // There is a case where the container of this vertical collection could have height 0 at
1127
+ // initial render step but will be updated later. We want to return visibleBottom to be 0 rather
1128
+ // than -1.
1129
+ return Math.max(this.visibleTop + this._calculatedScrollContainerHeight - 1, 0);
1130
+ }
1131
+ get totalItems() {
1132
+ return this.items ? get(this.items, 'length') : 0;
1133
+ }
1134
+ }
1135
+
1136
+ /*
1137
+ * `SkipList` is a data structure designed with two main uses in mind:
1138
+ *
1139
+ * - Given a target value, find the index i in the list such that
1140
+ * `sum(list[0]..list[i]) <= value < sum(list[0]..list[i + 1])`
1141
+ *
1142
+ * - Given the index i (the fulcrum point) from above, get `sum(list[0]..list[i])`
1143
+ * and `sum(list[i + 1]..list[-1])`
1144
+ *
1145
+ * The idea is that given a list of arbitrary heights or widths in pixels, we want to find
1146
+ * the index of the item such that when all of the items before it are added together, it will
1147
+ * be as close to the target (scrollTop of our container) as possible.
1148
+ *
1149
+ * This data structure acts somewhat like a Binary Search Tree. Given a list of size n, the
1150
+ * retreival time for the index is O(log n) and the update time should any values change is
1151
+ * O(log n). The space complexity is O(n log n) in bytes (using Float32Arrays helps a lot
1152
+ * here), and the initialization time is O(n log n).
1153
+ *
1154
+ * It works by constructing layer arrays, each of which is setup such that
1155
+ * `layer[i] = prevLayer[i * 2] + prevLayer[(i * 2) + 1]`. This allows us to traverse the layers
1156
+ * downward using a binary search to arrive at the index we want. We also add the values up as we
1157
+ * traverse to get the total value before and after the final index.
1158
+ */
1159
+
1160
+ function fill(array, value, start = 0, end = array.length) {
1161
+ if (typeof array.fill === 'function') {
1162
+ array.fill(value, start, end);
1163
+ } else {
1164
+ for (; start < end; start++) {
1165
+ array[start] = value;
1166
+ }
1167
+ return array;
1168
+ }
1169
+ }
1170
+ function subarray(array, start, end) {
1171
+ if (typeof array.subarray === 'function') {
1172
+ return array.subarray(start, end);
1173
+ } else {
1174
+ return array.slice(start, end);
1175
+ }
1176
+ }
1177
+ class SkipList {
1178
+ constructor(length, defaultValue) {
1179
+ const values = new Float32Array(new ArrayBuffer(length * 4));
1180
+ fill(values, defaultValue);
1181
+ this.length = length;
1182
+ this.defaultValue = defaultValue;
1183
+ this._initializeLayers(values, defaultValue);
1184
+ if (DEBUG) {
1185
+ Object.preventExtensions(this);
1186
+ }
1187
+ }
1188
+ _initializeLayers(values, defaultValue) {
1189
+ const layers = [values];
1190
+ let i, length, layer, prevLayer, left, right;
1191
+ prevLayer = layer = values;
1192
+ length = values.length;
1193
+ while (length > 2) {
1194
+ length = Math.ceil(length / 2);
1195
+ layer = new Float32Array(new ArrayBuffer(length * 4));
1196
+ if (defaultValue !== undefined) {
1197
+ // If given a default value we assume that we can fill each
1198
+ // layer of the skip list with the previous layer's value * 2.
1199
+ // This allows us to use the `fill` method on Typed arrays, which
1200
+ // an order of magnitude faster than manually calculating each value.
1201
+ defaultValue = defaultValue * 2;
1202
+ fill(layer, defaultValue);
1203
+ left = prevLayer[(length - 1) * 2] || 0;
1204
+ right = prevLayer[(length - 1) * 2 + 1] || 0;
1205
+
1206
+ // Layers are not powers of 2, and sometimes they may by odd sizes.
1207
+ // Only the last value of a layer will be different, so we calculate
1208
+ // its value manually.
1209
+ layer[length - 1] = left + right;
1210
+ } else {
1211
+ for (i = 0; i < length; i++) {
1212
+ left = prevLayer[i * 2];
1213
+ right = prevLayer[i * 2 + 1];
1214
+ layer[i] = right ? left + right : left;
1215
+ }
1216
+ }
1217
+ layers.unshift(layer);
1218
+ prevLayer = layer;
1219
+ }
1220
+ this.total = layer.length > 0 ? layer.length > 1 ? layer[0] + layer[1] : layer[0] : 0;
1221
+ assert('total must be a number', typeof this.total === 'number');
1222
+ this.layers = layers;
1223
+ this.values = values;
1224
+ }
1225
+ find(targetValue) {
1226
+ const {
1227
+ layers,
1228
+ total,
1229
+ length,
1230
+ values
1231
+ } = this;
1232
+ const numLayers = layers.length;
1233
+ if (length === 0) {
1234
+ return {
1235
+ index: 0,
1236
+ totalBefore: 0,
1237
+ totalAfter: 0
1238
+ };
1239
+ }
1240
+ let i, layer, left, leftIndex, rightIndex;
1241
+ let index = 0;
1242
+ let totalBefore = 0;
1243
+ let totalAfter = 0;
1244
+ targetValue = Math.min(total - 1, targetValue);
1245
+ assert('targetValue must be a number', typeof targetValue === 'number');
1246
+ assert('targetValue must be greater than or equal to 0', targetValue >= 0);
1247
+ assert('targetValue must be no more than total', targetValue < total);
1248
+ for (i = 0; i < numLayers; i++) {
1249
+ layer = layers[i];
1250
+ leftIndex = index;
1251
+ rightIndex = index + 1;
1252
+ left = layer[leftIndex];
1253
+ if (targetValue >= totalBefore + left) {
1254
+ totalBefore = totalBefore + left;
1255
+ index = rightIndex * 2;
1256
+ } else {
1257
+ index = leftIndex * 2;
1258
+ }
1259
+ }
1260
+ index = index / 2;
1261
+ assert('index must be a number', typeof index === 'number');
1262
+ assert('index must be within bounds', index >= 0 && index < this.values.length);
1263
+ totalAfter = total - (totalBefore + values[index]);
1264
+ return {
1265
+ index,
1266
+ totalBefore,
1267
+ totalAfter
1268
+ };
1269
+ }
1270
+ getOffset(targetIndex) {
1271
+ const {
1272
+ layers,
1273
+ length,
1274
+ values
1275
+ } = this;
1276
+ const numLayers = layers.length;
1277
+ if (length === 0) {
1278
+ return 0;
1279
+ }
1280
+ let index = 0;
1281
+ let offset = 0;
1282
+ for (let i = 0; i < numLayers - 1; i++) {
1283
+ const layer = layers[i];
1284
+ const leftIndex = index;
1285
+ const rightIndex = index + 1;
1286
+ if (targetIndex >= rightIndex * Math.pow(2, numLayers - i - 1)) {
1287
+ offset = offset + layer[leftIndex];
1288
+ index = rightIndex * 2;
1289
+ } else {
1290
+ index = leftIndex * 2;
1291
+ }
1292
+ }
1293
+ if (index + 1 === targetIndex) {
1294
+ offset += values[index];
1295
+ }
1296
+ return offset;
1297
+ }
1298
+ set(index, value) {
1299
+ assert('value must be a number', typeof value === 'number');
1300
+ assert('value must non-negative', value >= 0);
1301
+ assert('index must be a number', typeof index === 'number');
1302
+ assert('index must be within bounds', index >= 0 && index < this.values.length);
1303
+ const {
1304
+ layers
1305
+ } = this;
1306
+ const oldValue = layers[layers.length - 1][index];
1307
+ const delta = roundTo(value - oldValue);
1308
+ if (delta === 0) {
1309
+ return delta;
1310
+ }
1311
+ let i, layer;
1312
+ for (i = layers.length - 1; i >= 0; i--) {
1313
+ layer = layers[i];
1314
+ layer[index] += delta;
1315
+ index = Math.floor(index / 2);
1316
+ }
1317
+ this.total += delta;
1318
+ return delta;
1319
+ }
1320
+ prepend(numPrepended) {
1321
+ const {
1322
+ values: oldValues,
1323
+ length: oldLength,
1324
+ defaultValue
1325
+ } = this;
1326
+ const newLength = numPrepended + oldLength;
1327
+ const newValues = new Float32Array(new ArrayBuffer(newLength * 4));
1328
+ newValues.set(oldValues, numPrepended);
1329
+ fill(newValues, defaultValue, 0, numPrepended);
1330
+ this.length = newLength;
1331
+ this._initializeLayers(newValues);
1332
+ }
1333
+ append(numAppended) {
1334
+ const {
1335
+ values: oldValues,
1336
+ length: oldLength,
1337
+ defaultValue
1338
+ } = this;
1339
+ const newLength = numAppended + oldLength;
1340
+ const newValues = new Float32Array(new ArrayBuffer(newLength * 4));
1341
+ newValues.set(oldValues);
1342
+ fill(newValues, defaultValue, oldLength);
1343
+ this.length = newLength;
1344
+ this._initializeLayers(newValues);
1345
+ }
1346
+ reset(newLength) {
1347
+ const {
1348
+ values: oldValues,
1349
+ length: oldLength,
1350
+ defaultValue
1351
+ } = this;
1352
+ if (oldLength === newLength) {
1353
+ return;
1354
+ }
1355
+ const newValues = new Float32Array(new ArrayBuffer(newLength * 4));
1356
+ if (oldLength < newLength) {
1357
+ newValues.set(oldValues);
1358
+ fill(newValues, defaultValue, oldLength);
1359
+ } else {
1360
+ newValues.set(subarray(oldValues, 0, newLength));
1361
+ }
1362
+ this.length = newLength;
1363
+ if (oldLength === 0) {
1364
+ this._initializeLayers(newValues, defaultValue);
1365
+ } else {
1366
+ this._initializeLayers(newValues);
1367
+ }
1368
+ }
1369
+ }
1370
+
1371
+ class DynamicRadar extends Radar {
1372
+ constructor(parentToken, options) {
1373
+ super(parentToken, options);
1374
+ this._firstItemIndex = 0;
1375
+ this._lastItemIndex = 0;
1376
+ this._totalBefore = 0;
1377
+ this._totalAfter = 0;
1378
+ this._minHeight = Infinity;
1379
+ this._nextIncrementalRender = null;
1380
+ this.skipList = null;
1381
+ if (DEBUG) {
1382
+ Object.preventExtensions(this);
1383
+ }
1384
+ }
1385
+ willDestroy() {
1386
+ super.willDestroy();
1387
+ this.skipList = null;
1388
+ }
1389
+ scheduleUpdate(didUpdateItems, promiseResolve) {
1390
+ // Cancel incremental render check, since we'll be remeasuring anyways
1391
+ if (this._nextIncrementalRender !== null) {
1392
+ this._nextIncrementalRender.cancel();
1393
+ this._nextIncrementalRender = null;
1394
+ }
1395
+ super.scheduleUpdate(didUpdateItems, promiseResolve);
1396
+ }
1397
+ afterUpdate() {
1398
+ // Schedule a check to see if we should rerender
1399
+ if (this._nextIncrementalRender === null && this._nextUpdate === null) {
1400
+ this._nextIncrementalRender = this.schedule('sync', () => {
1401
+ this._nextIncrementalRender = null;
1402
+ if (this._shouldScheduleRerender()) {
1403
+ this.update();
1404
+ }
1405
+ });
1406
+ }
1407
+ super.afterUpdate();
1408
+ }
1409
+ _updateConstants() {
1410
+ super._updateConstants();
1411
+ if (this._calculatedEstimateHeight < this._minHeight) {
1412
+ this._minHeight = this._calculatedEstimateHeight;
1413
+ }
1414
+
1415
+ // Create the SkipList only after the estimateHeight has been calculated the first time
1416
+ if (this.skipList === null) {
1417
+ this.skipList = new SkipList(this.totalItems, this._calculatedEstimateHeight);
1418
+ } else {
1419
+ this.skipList.defaultValue = this._calculatedEstimateHeight;
1420
+ }
1421
+ }
1422
+ _updateIndexes() {
1423
+ const {
1424
+ bufferSize,
1425
+ skipList,
1426
+ visibleTop,
1427
+ visibleBottom,
1428
+ totalItems,
1429
+ _didReset
1430
+ } = this;
1431
+ if (totalItems === 0) {
1432
+ this._firstItemIndex = 0;
1433
+ this._lastItemIndex = -1;
1434
+ this._totalBefore = 0;
1435
+ this._totalAfter = 0;
1436
+ return;
1437
+ }
1438
+
1439
+ // Don't measure if the radar has just been instantiated or reset, as we are rendering with a
1440
+ // completely new set of items and won't get an accurate measurement until after they render the
1441
+ // first time.
1442
+ if (_didReset === false) {
1443
+ this._measure();
1444
+ }
1445
+ const {
1446
+ values
1447
+ } = skipList;
1448
+ let {
1449
+ totalBefore,
1450
+ index: firstVisibleIndex
1451
+ } = this.skipList.find(visibleTop);
1452
+ let {
1453
+ totalAfter,
1454
+ index: lastVisibleIndex
1455
+ } = this.skipList.find(visibleBottom);
1456
+ const maxIndex = totalItems - 1;
1457
+ let firstItemIndex = firstVisibleIndex;
1458
+ let lastItemIndex = lastVisibleIndex;
1459
+
1460
+ // Add buffers
1461
+ for (let i = bufferSize; i > 0 && firstItemIndex > 0; i--) {
1462
+ firstItemIndex--;
1463
+ totalBefore -= values[firstItemIndex];
1464
+ }
1465
+ for (let i = bufferSize; i > 0 && lastItemIndex < maxIndex; i--) {
1466
+ lastItemIndex++;
1467
+ totalAfter -= values[lastItemIndex];
1468
+ }
1469
+ this._firstItemIndex = firstItemIndex;
1470
+ this._lastItemIndex = lastItemIndex;
1471
+ this._totalBefore = totalBefore;
1472
+ this._totalAfter = totalAfter;
1473
+ }
1474
+ _calculateScrollDiff() {
1475
+ const {
1476
+ firstItemIndex,
1477
+ _prevFirstVisibleIndex,
1478
+ _prevFirstItemIndex
1479
+ } = this;
1480
+ let beforeVisibleDiff = 0;
1481
+ if (firstItemIndex < _prevFirstItemIndex) {
1482
+ // Measurement only items that could affect scrollTop. This will necesarilly be the
1483
+ // minimum of the either the total number of items that are rendered up to the first
1484
+ // visible item, OR the number of items that changed before the first visible item
1485
+ // (the delta). We want to measure the delta of exactly this number of items, because
1486
+ // items that are after the first visible item should not affect the scroll position,
1487
+ // and neither should items already rendered before the first visible item.
1488
+ const measureLimit = Math.min(Math.abs(firstItemIndex - _prevFirstItemIndex), _prevFirstVisibleIndex - firstItemIndex);
1489
+ beforeVisibleDiff = Math.round(this._measure(measureLimit));
1490
+ }
1491
+ return beforeVisibleDiff + super._calculateScrollDiff();
1492
+ }
1493
+ _shouldScheduleRerender() {
1494
+ const {
1495
+ firstItemIndex,
1496
+ lastItemIndex
1497
+ } = this;
1498
+ this._updateConstants();
1499
+ this._measure();
1500
+
1501
+ // These indexes could change after the measurement, and in the incremental render
1502
+ // case we want to check them _after_ the change.
1503
+ const {
1504
+ firstVisibleIndex,
1505
+ lastVisibleIndex
1506
+ } = this;
1507
+ return firstVisibleIndex < firstItemIndex || lastVisibleIndex > lastItemIndex;
1508
+ }
1509
+ _measure(measureLimit = null) {
1510
+ const {
1511
+ orderedComponents,
1512
+ skipList,
1513
+ _occludedContentBefore,
1514
+ _transformScale
1515
+ } = this;
1516
+ const numToMeasure = measureLimit !== null ? Math.min(measureLimit, orderedComponents.length) : orderedComponents.length;
1517
+ let totalDelta = 0;
1518
+ for (let i = 0; i < numToMeasure; i++) {
1519
+ const currentItem = orderedComponents[i];
1520
+ const previousItem = orderedComponents[i - 1];
1521
+ const itemIndex = currentItem.index;
1522
+ const {
1523
+ top: currentItemTop,
1524
+ height: currentItemHeight
1525
+ } = getScaledClientRect(currentItem, _transformScale);
1526
+ let margin;
1527
+ if (previousItem !== undefined) {
1528
+ margin = currentItemTop - getScaledClientRect(previousItem, _transformScale).bottom;
1529
+ } else {
1530
+ margin = currentItemTop - getScaledClientRect(_occludedContentBefore, _transformScale).bottom;
1531
+ }
1532
+ const newHeight = roundTo(currentItemHeight + margin);
1533
+ const itemDelta = skipList.set(itemIndex, newHeight);
1534
+ if (newHeight < this._minHeight) {
1535
+ this._minHeight = newHeight;
1536
+ }
1537
+ if (itemDelta !== 0) {
1538
+ totalDelta += itemDelta;
1539
+ }
1540
+ }
1541
+ return totalDelta;
1542
+ }
1543
+ _didEarthquake(scrollDiff) {
1544
+ return scrollDiff > this._minHeight / 2;
1545
+ }
1546
+ get total() {
1547
+ return this.skipList.total;
1548
+ }
1549
+ get totalBefore() {
1550
+ return this._totalBefore;
1551
+ }
1552
+ get totalAfter() {
1553
+ return this._totalAfter;
1554
+ }
1555
+ get firstItemIndex() {
1556
+ return this._firstItemIndex;
1557
+ }
1558
+ get lastItemIndex() {
1559
+ return this._lastItemIndex;
1560
+ }
1561
+ get firstVisibleIndex() {
1562
+ const {
1563
+ visibleTop
1564
+ } = this;
1565
+ const {
1566
+ index
1567
+ } = this.skipList.find(visibleTop);
1568
+ return index;
1569
+ }
1570
+ get lastVisibleIndex() {
1571
+ const {
1572
+ visibleBottom,
1573
+ totalItems
1574
+ } = this;
1575
+ const {
1576
+ index
1577
+ } = this.skipList.find(visibleBottom);
1578
+ return Math.min(index, totalItems - 1);
1579
+ }
1580
+ prepend(numPrepended) {
1581
+ super.prepend(numPrepended);
1582
+ this.skipList.prepend(numPrepended);
1583
+ }
1584
+ append(numAppended) {
1585
+ super.append(numAppended);
1586
+ this.skipList.append(numAppended);
1587
+ }
1588
+ reset() {
1589
+ super.reset();
1590
+ this.skipList.reset(this.totalItems);
1591
+ }
1592
+
1593
+ /*
1594
+ * Public API to query the skiplist for the offset of an item
1595
+ */
1596
+ getOffsetForIndex(index) {
1597
+ this._measure();
1598
+ return this.skipList.getOffset(index);
1599
+ }
1600
+ }
1601
+
1602
+ class StaticRadar extends Radar {
1603
+ constructor(parentToken, options) {
1604
+ super(parentToken, options);
1605
+ this._firstItemIndex = 0;
1606
+ this._lastItemIndex = 0;
1607
+ if (DEBUG) {
1608
+ Object.preventExtensions(this);
1609
+ }
1610
+ }
1611
+ _updateIndexes() {
1612
+ const {
1613
+ bufferSize,
1614
+ totalItems,
1615
+ visibleMiddle,
1616
+ _calculatedEstimateHeight,
1617
+ _calculatedScrollContainerHeight
1618
+ } = this;
1619
+ if (totalItems === 0) {
1620
+ this._firstItemIndex = 0;
1621
+ this._lastItemIndex = -1;
1622
+ return;
1623
+ }
1624
+ const maxIndex = totalItems - 1;
1625
+ const middleItemIndex = Math.floor(visibleMiddle / _calculatedEstimateHeight);
1626
+ const shouldRenderCount = Math.min(Math.ceil(_calculatedScrollContainerHeight / _calculatedEstimateHeight), totalItems);
1627
+ let firstItemIndex = middleItemIndex - Math.floor(shouldRenderCount / 2);
1628
+ let lastItemIndex = middleItemIndex + Math.ceil(shouldRenderCount / 2) - 1;
1629
+ if (firstItemIndex < 0) {
1630
+ firstItemIndex = 0;
1631
+ lastItemIndex = shouldRenderCount - 1;
1632
+ }
1633
+ if (lastItemIndex > maxIndex) {
1634
+ lastItemIndex = maxIndex;
1635
+ firstItemIndex = maxIndex - (shouldRenderCount - 1);
1636
+ }
1637
+ firstItemIndex = Math.max(firstItemIndex - bufferSize, 0);
1638
+ lastItemIndex = Math.min(lastItemIndex + bufferSize, maxIndex);
1639
+ this._firstItemIndex = firstItemIndex;
1640
+ this._lastItemIndex = lastItemIndex;
1641
+ }
1642
+ _didEarthquake(scrollDiff) {
1643
+ return scrollDiff > this._calculatedEstimateHeight / 2;
1644
+ }
1645
+ get total() {
1646
+ return this.totalItems * this._calculatedEstimateHeight;
1647
+ }
1648
+ get totalBefore() {
1649
+ return this.firstItemIndex * this._calculatedEstimateHeight;
1650
+ }
1651
+ get totalAfter() {
1652
+ return this.total - (this.lastItemIndex + 1) * this._calculatedEstimateHeight;
1653
+ }
1654
+ get firstItemIndex() {
1655
+ return this._firstItemIndex;
1656
+ }
1657
+ get lastItemIndex() {
1658
+ return this._lastItemIndex;
1659
+ }
1660
+ get firstVisibleIndex() {
1661
+ return Math.ceil(this.visibleTop / this._calculatedEstimateHeight);
1662
+ }
1663
+ get lastVisibleIndex() {
1664
+ return Math.min(Math.ceil(this.visibleBottom / this._calculatedEstimateHeight), this.totalItems) - 1;
1665
+ }
1666
+
1667
+ /*
1668
+ * Public API to query for the offset of an item
1669
+ */
1670
+ getOffsetForIndex(index) {
1671
+ return index * this._calculatedEstimateHeight + 1;
1672
+ }
1673
+ }
1674
+
1675
+ export { DynamicRadar as D, ScrollHandler as S, ViewportContainer$1 as V, StaticRadar as a, addScrollHandler as b, closest as c, keyForItem as k, objectAt as o, removeScrollHandler as r };
1676
+ //# sourceMappingURL=static-radar-D0EvnYLd.js.map