ng-virtual-list 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@ import { signal, inject, ElementRef, ChangeDetectionStrategy, Component, viewChi
3
3
  import * as i1 from '@angular/common';
4
4
  import { CommonModule } from '@angular/common';
5
5
  import { toObservable, takeUntilDestroyed } from '@angular/core/rxjs-interop';
6
- import { filter, tap, map, combineLatest, distinctUntilChanged, switchMap, of } from 'rxjs';
6
+ import { BehaviorSubject, filter, map, tap, combineLatest, distinctUntilChanged, switchMap, of } from 'rxjs';
7
7
 
8
8
  /**
9
9
  * Virtual list item component
@@ -23,18 +23,8 @@ class NgVirtualListItemComponent {
23
23
  if (this._data === v) {
24
24
  return;
25
25
  }
26
- this._data = v;
27
- this.data.set(v);
28
- }
29
- itemRenderer = signal(undefined);
30
- set renderer(v) {
31
- this.itemRenderer.set(v);
32
- }
33
- _elementRef = inject((ElementRef));
34
- constructor() {
35
- this._id = NgVirtualListItemComponent.__nextId = NgVirtualListItemComponent.__nextId === Number.MAX_SAFE_INTEGER
36
- ? 0 : NgVirtualListItemComponent.__nextId + 1;
37
- toObservable(this.data).pipe(takeUntilDestroyed(), filter(data => !!data), tap(data => {
26
+ const data = this._data = v;
27
+ if (data) {
38
28
  const styles = this._elementRef.nativeElement.style;
39
29
  styles.zIndex = data.config.sticky;
40
30
  if (data.config.snapped) {
@@ -45,9 +35,23 @@ class NgVirtualListItemComponent {
45
35
  styles.position = 'absolute';
46
36
  styles.transform = `translate3d(${data.config.isVertical ? 0 : data.measures.x}px, ${data.config.isVertical ? data.measures.y : 0}px , 0)`;
47
37
  }
48
- styles.height = data.config.isVertical ? `${data.measures.height}px` : '100%';
49
- styles.width = data.config.isVertical ? '100%' : `${data.measures.width}px`;
50
- })).subscribe();
38
+ styles.height = data.config.isVertical ? data.config.dynamic ? 'auto' : `${data.measures.height}px` : '100%';
39
+ styles.width = data.config.isVertical ? '100%' : data.config.dynamic ? 'auto' : `${data.measures.width}px`;
40
+ }
41
+ this.data.set(v);
42
+ }
43
+ itemRenderer = signal(undefined);
44
+ set renderer(v) {
45
+ this.itemRenderer.set(v);
46
+ }
47
+ _elementRef = inject((ElementRef));
48
+ constructor() {
49
+ this._id = NgVirtualListItemComponent.__nextId = NgVirtualListItemComponent.__nextId === Number.MAX_SAFE_INTEGER
50
+ ? 0 : NgVirtualListItemComponent.__nextId + 1;
51
+ }
52
+ getBounds() {
53
+ const el = this._elementRef.nativeElement, { width, height, left, top } = el.getBoundingClientRect();
54
+ return { width, height, x: left, y: top };
51
55
  }
52
56
  showIfNeed() {
53
57
  const styles = this._elementRef.nativeElement.style;
@@ -84,6 +88,8 @@ const DEFAULT_ITEMS_OFFSET = 2;
84
88
  const DEFAULT_LIST_SIZE = 400;
85
89
  const DEFAULT_SNAP = false;
86
90
  const DEFAULT_SNAP_TO_ITEM = false;
91
+ const DEFAULT_DYNAMIC_SIZE = false;
92
+ const TRACK_BY_PROPERTY_NAME = 'id';
87
93
  const DEFAULT_DIRECTION = Directions.VERTICAL;
88
94
  const DISPLAY_OBJECTS_LENGTH_MESUREMENT_ERROR = 1;
89
95
 
@@ -105,7 +111,450 @@ const toggleClassName = (el, className, remove = false) => {
105
111
  };
106
112
 
107
113
  /**
108
- * Virtual list component
114
+ * Tracks display items by property
115
+ * @homepage https://github.com/DjonnyX/ng-virtual-list/tree/main/projects/ng-virtual-list
116
+ * @author Evgenii Grebennikov
117
+ * @email djonnyx@gmail.com
118
+ */
119
+ class Tracker {
120
+ /**
121
+ * display objects dictionary of indexes by id
122
+ */
123
+ _displayObjectIndexMapById = {};
124
+ set displayObjectIndexMapById(v) {
125
+ if (this._displayObjectIndexMapById === v) {
126
+ return;
127
+ }
128
+ this._displayObjectIndexMapById = v;
129
+ }
130
+ get displayObjectIndexMapById() {
131
+ return this._displayObjectIndexMapById;
132
+ }
133
+ /**
134
+ * Dictionary displayItems propertyNameId by items propertyNameId
135
+ */
136
+ _trackMap = {};
137
+ get trackMap() {
138
+ return this._trackMap;
139
+ }
140
+ _trackingPropertyName;
141
+ constructor(trackingPropertyName) {
142
+ this._trackingPropertyName = trackingPropertyName;
143
+ }
144
+ /**
145
+ * tracking by propName
146
+ */
147
+ track(items, components, afterComponentSetup) {
148
+ if (!items) {
149
+ return;
150
+ }
151
+ const idPropName = this._trackingPropertyName, untrackedItems = [...components];
152
+ for (let i = 0, l = items.length; i < l; i++) {
153
+ const item = items[i], itemTrackingProperty = item[idPropName];
154
+ if (this._trackMap) {
155
+ const diId = this._trackMap[itemTrackingProperty];
156
+ if (this._trackMap.hasOwnProperty(itemTrackingProperty)) {
157
+ const lastIndex = this._displayObjectIndexMapById[diId], el = components[lastIndex];
158
+ this._checkComponentProperty(el?.instance);
159
+ const elId = el?.instance?.[itemTrackingProperty];
160
+ if (el && elId === diId) {
161
+ const indexByUntrackedItems = untrackedItems.findIndex(v => {
162
+ this._checkComponentProperty(v.instance);
163
+ return v.instance[itemTrackingProperty] === elId;
164
+ });
165
+ if (indexByUntrackedItems > -1) {
166
+ el.instance.item = item;
167
+ if (afterComponentSetup !== undefined) {
168
+ afterComponentSetup(el.instance, item);
169
+ }
170
+ untrackedItems.splice(indexByUntrackedItems, 1);
171
+ continue;
172
+ }
173
+ }
174
+ delete this._trackMap[itemTrackingProperty];
175
+ }
176
+ }
177
+ if (untrackedItems.length > 0) {
178
+ const el = untrackedItems.shift(), item = items[i];
179
+ if (el) {
180
+ el.instance.item = item;
181
+ if (this._trackMap) {
182
+ this._checkComponentProperty(el.instance);
183
+ this._trackMap[itemTrackingProperty] = el.instance[itemTrackingProperty];
184
+ }
185
+ if (afterComponentSetup !== undefined) {
186
+ afterComponentSetup(el.instance, item);
187
+ }
188
+ }
189
+ }
190
+ }
191
+ if (untrackedItems.length) {
192
+ throw Error('Tracking by id caused an error.');
193
+ }
194
+ }
195
+ untrackComponentByIdProperty(component) {
196
+ if (!component) {
197
+ return;
198
+ }
199
+ const propertyIdName = this._trackingPropertyName;
200
+ this._checkComponentProperty(component);
201
+ if (this._trackMap && component[propertyIdName] !== undefined) {
202
+ delete this._trackMap[propertyIdName];
203
+ }
204
+ }
205
+ _checkComponentProperty(component) {
206
+ if (!component) {
207
+ return;
208
+ }
209
+ const propertyIdName = this._trackingPropertyName;
210
+ try {
211
+ component[propertyIdName];
212
+ }
213
+ catch (err) {
214
+ throw Error(`Property ${propertyIdName} does not exist.`);
215
+ }
216
+ }
217
+ dispose() {
218
+ this._trackMap = null;
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Simple event emitter
224
+ * @homepage https://github.com/DjonnyX/ng-virtual-list/tree/main/projects/ng-virtual-list
225
+ * @author Evgenii Grebennikov
226
+ * @email djonnyx@gmail.com
227
+ */
228
+ class EventEmitter {
229
+ _listeners = {};
230
+ _disposed = false;
231
+ constructor() { }
232
+ /**
233
+ * Emits the event
234
+ */
235
+ dispatch(event, ...args) {
236
+ const ctx = this;
237
+ const listeners = this._listeners[event];
238
+ if (Array.isArray(listeners)) {
239
+ for (let i = 0, l = listeners.length; i < l; i++) {
240
+ const listener = listeners[i];
241
+ if (listener) {
242
+ listener.apply(ctx, args);
243
+ }
244
+ }
245
+ }
246
+ }
247
+ /**
248
+ * Emits the event async
249
+ */
250
+ dispatchAsync(event, ...args) {
251
+ queueMicrotask(() => {
252
+ if (this._disposed) {
253
+ return;
254
+ }
255
+ this.dispatch(event, ...args);
256
+ });
257
+ }
258
+ /**
259
+ * Returns true if the event listener is already subscribed.
260
+ */
261
+ hasEventListener(eventName, handler) {
262
+ const event = eventName;
263
+ if (this._listeners.hasOwnProperty(event)) {
264
+ const listeners = this._listeners[event];
265
+ const index = listeners.findIndex(v => v === handler);
266
+ if (index > -1) {
267
+ return true;
268
+ }
269
+ }
270
+ return false;
271
+ }
272
+ /**
273
+ * Add event listener
274
+ */
275
+ addEventListener(eventName, handler) {
276
+ const event = eventName;
277
+ if (!this._listeners.hasOwnProperty(event)) {
278
+ this._listeners[event] = [];
279
+ }
280
+ this._listeners[event].push(handler);
281
+ }
282
+ /**
283
+ * Remove event listener
284
+ */
285
+ removeEventListener(eventName, handler) {
286
+ const event = eventName;
287
+ if (!this._listeners.hasOwnProperty(event)) {
288
+ return;
289
+ }
290
+ const listeners = this._listeners[event], index = listeners.findIndex(v => v === handler);
291
+ if (index > -1) {
292
+ listeners.splice(index, 1);
293
+ if (listeners.length === 0) {
294
+ delete this._listeners[event];
295
+ }
296
+ }
297
+ }
298
+ /**
299
+ * Remove all listeners
300
+ */
301
+ removeAllListeners() {
302
+ const events = Object.keys(this._listeners);
303
+ while (events.length > 0) {
304
+ const event = events.pop();
305
+ if (event) {
306
+ const listeners = this._listeners[event];
307
+ if (Array.isArray(listeners)) {
308
+ while (listeners.length > 0) {
309
+ const listener = listeners.pop();
310
+ if (listener) {
311
+ this.removeEventListener(event, listener);
312
+ }
313
+ }
314
+ }
315
+ }
316
+ }
317
+ }
318
+ dispose() {
319
+ this._disposed = true;
320
+ this.removeAllListeners();
321
+ }
322
+ }
323
+
324
+ /**
325
+ * Cache map.
326
+ * Emits a change event on each mutation.
327
+ * @homepage https://github.com/DjonnyX/ng-virtual-list/tree/main/projects/ng-virtual-list
328
+ * @author Evgenii Grebennikov
329
+ * @email djonnyx@gmail.com
330
+ */
331
+ class CacheMap extends EventEmitter {
332
+ _map = new Map();
333
+ _version = 0;
334
+ get version() {
335
+ return this._version;
336
+ }
337
+ constructor() {
338
+ super();
339
+ }
340
+ bumpVersion() {
341
+ this._version = this._version === Number.MAX_SAFE_INTEGER ? 0 : this._version + 1;
342
+ }
343
+ fireChange() {
344
+ this.dispatch('change', this.version);
345
+ }
346
+ set(id, bounds) {
347
+ if (this._map.has(id) && JSON.stringify(this._map.get(id)) === JSON.stringify(bounds)) {
348
+ return this._map;
349
+ }
350
+ const v = this._map.set(id, bounds);
351
+ this.bumpVersion();
352
+ this.fireChange();
353
+ return v;
354
+ }
355
+ has(id) {
356
+ return this._map.has(id);
357
+ }
358
+ get(id) {
359
+ return this._map.get(id);
360
+ }
361
+ forEach(callbackfn, thisArg) {
362
+ return this._map.forEach(callbackfn, thisArg);
363
+ }
364
+ dispose() {
365
+ super.dispose();
366
+ this._map.clear();
367
+ }
368
+ }
369
+
370
+ /**
371
+ * An object that performs tracking, calculations and caching.
372
+ * @homepage https://github.com/DjonnyX/ng-virtual-list/tree/main/projects/ng-virtual-list
373
+ * @author Evgenii Grebennikov
374
+ * @email djonnyx@gmail.com
375
+ */
376
+ class TrackBox extends CacheMap {
377
+ _tracker;
378
+ _items;
379
+ set items(v) {
380
+ if (this._items === v) {
381
+ return;
382
+ }
383
+ this._items = v;
384
+ }
385
+ _displayComponents;
386
+ set displayComponents(v) {
387
+ if (this._displayComponents === v) {
388
+ return;
389
+ }
390
+ this._displayComponents = v;
391
+ }
392
+ constructor(trackingPropertyName) {
393
+ super();
394
+ this._tracker = new Tracker(trackingPropertyName);
395
+ }
396
+ set(id, bounds) {
397
+ if (this._map.has(id) && JSON.stringify(this._map.get(id)) === JSON.stringify(bounds)) {
398
+ return this._map;
399
+ }
400
+ const v = this._map.set(id, bounds);
401
+ this.bumpVersion();
402
+ this.fireChange();
403
+ return v;
404
+ }
405
+ _fireChangeTimeouts = [];
406
+ fireChange() {
407
+ this.clearchangesTimeouts();
408
+ this._fireChangeTimeouts.push(setTimeout(() => { this.dispatchAsync('change', this._version); }));
409
+ }
410
+ clearchangesTimeouts() {
411
+ while (this._fireChangeTimeouts.length > 0) {
412
+ const timeout = this._fireChangeTimeouts.pop();
413
+ clearTimeout(timeout);
414
+ }
415
+ }
416
+ recalculateMetrics(options) {
417
+ const { bounds, collection, dynamicSize, isVertical, itemSize, itemsOffset, scrollSize, snap, } = options;
418
+ const { width, height } = bounds, sizeProperty = isVertical ? 'height' : 'width', size = isVertical ? height : width, weightToDisplayEnd = scrollSize + height, totalLength = collection.length, typicalItemSize = dynamicSize ? this.getTypicalItemSize(isVertical, itemSize) || itemSize : itemSize, totalSize = dynamicSize ? this.getBoundsFromCache(collection, typicalItemSize, isVertical) : totalLength * typicalItemSize, snippedPos = Math.floor(scrollSize);
419
+ let itemsFromStartToScrollEnd = -1, itemsFromDisplayEndToOffsetEnd = 0, itemsFromStartToDisplayEnd = -1, leftItemLength = 0, rightItemLength = 0, leftItemsWeight = 0, rightItemsWeight = 0, startIndex;
420
+ if (dynamicSize) {
421
+ let y = 0;
422
+ for (let i = 0, l = collection.length; i < l; i++) {
423
+ const collectionItem = collection[i], map = this._map;
424
+ let itemSize = 0;
425
+ if (map.has(collectionItem.id)) {
426
+ const bounds = map.get(collectionItem.id);
427
+ itemSize = bounds ? bounds[sizeProperty] : typicalItemSize;
428
+ }
429
+ else {
430
+ itemSize = typicalItemSize;
431
+ }
432
+ if (itemsFromStartToScrollEnd === -1 && y >= scrollSize && y <= scrollSize + itemSize) {
433
+ leftItemsWeight += itemSize;
434
+ itemsFromStartToScrollEnd = i;
435
+ }
436
+ if (itemsFromStartToDisplayEnd === -1) {
437
+ if (y >= weightToDisplayEnd && y <= weightToDisplayEnd + itemSize) {
438
+ itemsFromStartToDisplayEnd = i;
439
+ itemsFromDisplayEndToOffsetEnd = itemsFromStartToDisplayEnd + itemsOffset;
440
+ }
441
+ }
442
+ else {
443
+ if (i <= itemsFromDisplayEndToOffsetEnd) {
444
+ rightItemsWeight += itemSize;
445
+ }
446
+ }
447
+ y += itemSize;
448
+ }
449
+ leftItemLength = Math.min(itemsFromStartToScrollEnd, itemsOffset);
450
+ rightItemLength = itemsFromStartToDisplayEnd + itemsOffset > totalLength
451
+ ? totalLength - itemsFromStartToDisplayEnd : itemsOffset;
452
+ startIndex = itemsFromStartToScrollEnd - leftItemLength;
453
+ }
454
+ else {
455
+ itemsFromStartToScrollEnd = Math.ceil(scrollSize / typicalItemSize);
456
+ itemsFromStartToDisplayEnd = Math.ceil((scrollSize + size) / typicalItemSize);
457
+ leftItemLength = Math.min(itemsFromStartToScrollEnd, itemsOffset);
458
+ rightItemLength = itemsFromStartToDisplayEnd + itemsOffset > totalLength
459
+ ? totalLength - itemsFromStartToDisplayEnd : itemsOffset;
460
+ startIndex = itemsFromStartToScrollEnd - leftItemLength;
461
+ leftItemsWeight = leftItemLength * typicalItemSize;
462
+ rightItemsWeight = rightItemLength * typicalItemSize;
463
+ }
464
+ const leftHiddenItemsWeight = itemsFromStartToScrollEnd * typicalItemSize, totalItemsToDisplayEndWeight = itemsFromStartToDisplayEnd * typicalItemSize, itemsOnDisplay = totalItemsToDisplayEndWeight - leftHiddenItemsWeight;
465
+ const metrics = {
466
+ itemsFromStartToScrollEnd,
467
+ itemsFromStartToDisplayEnd,
468
+ itemsOnDisplay,
469
+ leftHiddenItemsWeight,
470
+ leftItemLength,
471
+ leftItemsWeight,
472
+ rightItemLength,
473
+ rightItemsWeight,
474
+ snippedPos,
475
+ totalItemsToDisplayEndWeight,
476
+ totalSize,
477
+ typicalItemSize,
478
+ };
479
+ return metrics;
480
+ }
481
+ /**
482
+ * Calculates and returns the maximum size of a repeating item
483
+ */
484
+ getTypicalItemSize(isVertical, defaultItemSize, fromIndex = 0, to = -1) {
485
+ const sizeProperty = isVertical ? 'height' : 'width', sizes = {};
486
+ let maxRepeatingSize = defaultItemSize, count = 0;
487
+ this._map.forEach(bound => {
488
+ const size = bound[sizeProperty];
489
+ if (sizes.hasOwnProperty(size)) {
490
+ sizes[size] += 1;
491
+ }
492
+ else {
493
+ sizes[size] = 1;
494
+ }
495
+ if (sizes[size] > count) {
496
+ count = sizes[size];
497
+ maxRepeatingSize = size;
498
+ }
499
+ });
500
+ return maxRepeatingSize;
501
+ }
502
+ /**
503
+ * tracking by propName
504
+ */
505
+ track(dynamicSize = false) {
506
+ if (!this._items || !this._displayComponents) {
507
+ return;
508
+ }
509
+ this._tracker.track(this._items, this._displayComponents, dynamicSize ? (component, item) => {
510
+ this.cacheElementBounds(component, item);
511
+ } : undefined);
512
+ }
513
+ setDisplayObjectIndexMapById(v) {
514
+ this._tracker.displayObjectIndexMapById = v;
515
+ }
516
+ untrackComponentByIdProperty(component) {
517
+ this._tracker.untrackComponentByIdProperty(component);
518
+ }
519
+ /**
520
+ * Stores the element bounds in _sizeCacheMap
521
+ */
522
+ cacheElementBounds(component, item) {
523
+ component.item = item;
524
+ const bounds = component.getBounds();
525
+ this.set(item.id, bounds);
526
+ }
527
+ /**
528
+ * Returns calculated bounds from cache
529
+ */
530
+ getBoundsFromCache(items, typicalItemSize, isVertical) {
531
+ const sizeProperty = isVertical ? 'height' : 'width', map = this._map;
532
+ let size = 0;
533
+ for (let i = 0, l = items.length; i < l; i++) {
534
+ const item = items[i];
535
+ if (map.has(item.id)) {
536
+ const bounds = map.get(item.id);
537
+ size += bounds ? bounds[sizeProperty] : typicalItemSize;
538
+ }
539
+ else {
540
+ size += typicalItemSize;
541
+ }
542
+ }
543
+ return size;
544
+ }
545
+ dispose() {
546
+ super.dispose();
547
+ this.clearchangesTimeouts();
548
+ if (this._tracker) {
549
+ this._tracker.dispose();
550
+ }
551
+ }
552
+ }
553
+
554
+ /**
555
+ * Virtual list component.
556
+ * Maximum performance for extremely large lists.
557
+ * It is based on algorithms for virtualization of screen objects.
109
558
  * @homepage https://github.com/DjonnyX/ng-virtual-list/tree/main/projects/ng-virtual-list
110
559
  * @author Evgenii Grebennikov
111
560
  * @email djonnyx@gmail.com
@@ -151,8 +600,14 @@ class NgVirtualListComponent {
151
600
  stickyMap = input({});
152
601
  /**
153
602
  * If direction = 'vertical', then the height of a typical element. If direction = 'horizontal', then the width of a typical element.
603
+ * Ignored if the dynamicSize property is true.
154
604
  */
155
605
  itemSize = input(DEFAULT_ITEM_SIZE);
606
+ /**
607
+ * If true then the items in the list can have different sizes and the itemSize property is ignored.
608
+ * If false then the items in the list have a fixed size specified by the itemSize property. The default value is false.
609
+ */
610
+ dynamicSize = input(DEFAULT_DYNAMIC_SIZE);
156
611
  /**
157
612
  * Determines the direction in which elements are placed. Default value is "vertical".
158
613
  */
@@ -186,40 +641,64 @@ class NgVirtualListComponent {
186
641
  _elementRef = inject((ElementRef));
187
642
  _initialized = signal(false);
188
643
  /**
189
- * Dictionary displayItems id by IRenderVirtualListItem.id
190
- */
191
- _trackMap = {};
192
- /**
193
- * displayItems dictionary of indexes by id
644
+ * Dictionary of element sizes by their id
194
645
  */
195
- _disMap = {};
196
- // for dynamic item size
197
- // private _sizeCacheMap = new Map<Id, IRect>();
646
+ _trackBox = new TrackBox(TRACK_BY_PROPERTY_NAME);
647
+ _onTrackBoxChangeHandler = (v) => {
648
+ this._$cacheVersion.next(v);
649
+ };
650
+ _$cacheVersion = new BehaviorSubject(-1);
651
+ get $cacheVersion() { return this._$cacheVersion.asObservable(); }
198
652
  constructor() {
199
653
  NgVirtualListComponent.__nextId = NgVirtualListComponent.__nextId + 1 === Number.MAX_SAFE_INTEGER
200
654
  ? 0 : NgVirtualListComponent.__nextId + 1;
201
655
  this._id = NgVirtualListComponent.__nextId;
202
- const $bounds = toObservable(this._bounds).pipe(filter(b => !!b)), $items = toObservable(this.items).pipe(map(i => !i ? [] : i)), $scrollSize = toObservable(this._scrollSize), $itemSize = toObservable(this.itemSize).pipe(map(v => v <= 0 ? DEFAULT_ITEM_SIZE : v)), $itemsOffset = toObservable(this.itemsOffset).pipe(map(v => v < 0 ? DEFAULT_ITEMS_OFFSET : v)), $stickyMap = toObservable(this.stickyMap).pipe(map(v => !v ? {} : v)), $snap = toObservable(this.snap), $isVertical = toObservable(this.direction).pipe(map(v => this.getIsVertical(v || DEFAULT_DIRECTION)), tap(v => {
656
+ this._trackBox.displayComponents = this._displayComponents;
657
+ const $bounds = toObservable(this._bounds).pipe(filter(b => !!b)), $items = toObservable(this.items).pipe(map(i => !i ? [] : i)), $scrollSize = toObservable(this._scrollSize), $itemSize = toObservable(this.itemSize).pipe(map(v => v <= 0 ? DEFAULT_ITEM_SIZE : v)), $itemsOffset = toObservable(this.itemsOffset).pipe(map(v => v < 0 ? DEFAULT_ITEMS_OFFSET : v)), $stickyMap = toObservable(this.stickyMap).pipe(map(v => !v ? {} : v)), $snap = toObservable(this.snap), $isVertical = toObservable(this.direction).pipe(map(v => this.getIsVertical(v || DEFAULT_DIRECTION))), $dynamicSize = toObservable(this.dynamicSize), $cacheVersion = this.$cacheVersion, $displayItems = toObservable(this._displayItems), $initialized = toObservable(this._initialized);
658
+ $isVertical.pipe(takeUntilDestroyed(), tap(v => {
203
659
  this._isVertical = v;
204
660
  const el = this._elementRef.nativeElement;
205
661
  toggleClassName(el, v ? 'vertical' : 'horizontal', true);
206
- })), $initialized = toObservable(this._initialized);
207
- combineLatest([$initialized, $bounds, $items, $stickyMap, $scrollSize, $itemSize, $itemsOffset, $snap, $isVertical]).pipe(takeUntilDestroyed(), distinctUntilChanged(), filter(([initialized]) => !!initialized), switchMap(([, bounds, items, stickyMap, scrollSize, itemSize, itemsOffset, snap, isVertical]) => {
208
- const { width, height } = bounds, size = isVertical ? height : width;
209
- const itemsFromStartToScrollEnd = Math.ceil(scrollSize / itemSize), itemsFromStartToDisplayEnd = Math.ceil((scrollSize + size) / itemSize), leftHiddenItemsWeight = itemsFromStartToScrollEnd * itemSize, totalItemsToDisplayEndWeight = itemsFromStartToDisplayEnd * itemSize, totalItems = items.length, totalSize = totalItems * itemSize, itemsOnDisplay = totalItemsToDisplayEndWeight - leftHiddenItemsWeight;
662
+ })).subscribe();
663
+ $dynamicSize.pipe(takeUntilDestroyed(), tap(dynamicSize => {
664
+ if (dynamicSize) {
665
+ if (!this._trackBox.hasEventListener('change', this._onTrackBoxChangeHandler)) {
666
+ this._trackBox.addEventListener('change', this._onTrackBoxChangeHandler);
667
+ }
668
+ }
669
+ else {
670
+ if (this._trackBox.hasEventListener('change', this._onTrackBoxChangeHandler)) {
671
+ this._trackBox.removeEventListener('change', this._onTrackBoxChangeHandler);
672
+ }
673
+ }
674
+ })).subscribe();
675
+ $displayItems.pipe(takeUntilDestroyed(), tap((displayItems) => {
676
+ this._trackBox.items = displayItems;
677
+ })).subscribe();
678
+ combineLatest([$initialized, $bounds, $items, $stickyMap, $scrollSize, $itemSize,
679
+ $itemsOffset, $snap, $isVertical, $dynamicSize, $cacheVersion,
680
+ ]).pipe(takeUntilDestroyed(), distinctUntilChanged(), filter(([initialized]) => !!initialized), switchMap(([, bounds, items, stickyMap, scrollSize, itemSize, itemsOffset, snap, isVertical, dynamicSize, cacheVersion,]) => {
681
+ const { width, height } = bounds, { itemsFromStartToScrollEnd, itemsOnDisplay, leftHiddenItemsWeight, leftItemLength, leftItemsWeight, rightItemsWeight, snippedPos, totalSize, typicalItemSize, } = this._trackBox.recalculateMetrics({
682
+ bounds: { width, height }, collection: items,
683
+ dynamicSize, isVertical, itemSize, itemsOffset, scrollSize, snap,
684
+ });
685
+ // Нужно сперва сделать корректные расчеты для dynamicSize. Сейчас количества элементов в облостях высчитывается не корректно!
686
+ // Необходима кореляция startDisplayObjectY с помощью дельты от высоты предыдущей и текущей размеченной области по версии кэша.
687
+ // TrackBox может расчитать дельту!
210
688
  return of({
211
- items, stickyMap, itemsOffset, width, height, isVertical, scrollSize, itemsFromStartToScrollEnd,
212
- itemsFromStartToDisplayEnd, itemsOnDisplay, leftHiddenItemsWeight, itemSize, totalSize, snap,
689
+ items, stickyMap, width, height, isVertical, scrollSize, itemsFromStartToScrollEnd,
690
+ itemsOnDisplay, leftHiddenItemsWeight, itemSize: typicalItemSize, totalSize, snap,
691
+ leftItemLength, leftItemsWeight, rightItemsWeight, snippedPos, dynamicSize,
213
692
  });
214
- }), tap(({ items, stickyMap, itemsOffset, width, height, isVertical, scrollSize, itemsFromStartToScrollEnd, itemsFromStartToDisplayEnd, itemsOnDisplay, leftHiddenItemsWeight, itemSize, totalSize, snap, }) => {
693
+ }), tap(({ items, stickyMap, width, height, isVertical, scrollSize, itemsFromStartToScrollEnd, itemsOnDisplay, leftHiddenItemsWeight, leftItemLength, leftItemsWeight, rightItemsWeight, snippedPos, itemSize, totalSize, snap, dynamicSize: dynamic, }) => {
215
694
  const displayItems = [];
216
695
  if (items.length) {
217
- const w = isVertical ? width : itemSize, h = isVertical ? itemSize : height, totalItems = items.length, leftItemLength = Math.min(itemsFromStartToScrollEnd, itemsOffset), rightItemLength = itemsFromStartToDisplayEnd + itemsOffset > totalItems
218
- ? totalItems - itemsFromStartToDisplayEnd : itemsOffset, leftItemsWeight = leftItemLength * itemSize, rightItemsWeight = rightItemLength * itemSize, startIndex = itemsFromStartToScrollEnd - leftItemLength, snippedPos = Math.floor(scrollSize);
219
- let pos = leftHiddenItemsWeight - leftItemsWeight, renderWeight = itemsOnDisplay + leftItemsWeight + rightItemsWeight, stickyItem, nextSticky, stickyItemIndex = -1;
696
+ const sizeProperty = isVertical ? 'height' : 'width', w = isVertical ? width : itemSize, h = isVertical ? itemSize : height, totalItems = items.length, startIndex = itemsFromStartToScrollEnd - leftItemLength;
697
+ let pos = leftHiddenItemsWeight - leftItemsWeight, renderWeight = itemsOnDisplay + leftItemsWeight + rightItemsWeight, stickyItem, nextSticky, stickyItemIndex = -1, stickyItemSize = 0;
220
698
  if (snap) {
221
699
  for (let i = itemsFromStartToScrollEnd - 1; i >= 0; i--) {
222
700
  const id = items[i].id, sticky = stickyMap[id];
701
+ stickyItemSize = dynamic ? this._trackBox.get(id)?.[sizeProperty] || itemSize : itemSize;
223
702
  if (sticky > 0) {
224
703
  const measures = {
225
704
  x: isVertical ? 0 : snippedPos,
@@ -231,6 +710,7 @@ class NgVirtualListComponent {
231
710
  sticky,
232
711
  snap,
233
712
  snapped: true,
713
+ dynamic,
234
714
  };
235
715
  const itemData = items[i];
236
716
  stickyItem = { id, measures, data: itemData, config };
@@ -245,7 +725,7 @@ class NgVirtualListComponent {
245
725
  if (i >= totalItems) {
246
726
  break;
247
727
  }
248
- const id = items[i].id;
728
+ const id = items[i].id, size = dynamic ? this._trackBox.get(id)?.[sizeProperty] || itemSize : itemSize;
249
729
  if (id !== stickyItem?.id) {
250
730
  const snaped = snap && stickyMap[id] > 0 && pos <= scrollSize, measures = {
251
731
  x: isVertical ? 0 : pos,
@@ -257,26 +737,25 @@ class NgVirtualListComponent {
257
737
  sticky: stickyMap[id],
258
738
  snap,
259
739
  snapped: false,
740
+ dynamic,
260
741
  };
261
742
  const itemData = items[i];
262
743
  const item = { id, measures, data: itemData, config };
263
- if (!nextSticky && stickyItemIndex < i && snap && stickyMap[id] > 0 && pos <= scrollSize + itemSize) {
744
+ if (!nextSticky && stickyItemIndex < i && snap && stickyMap[id] > 0 && pos <= scrollSize + size) {
264
745
  item.measures.x = isVertical ? 0 : snaped ? snippedPos : pos;
265
746
  item.measures.y = isVertical ? snaped ? snippedPos : pos : 0;
266
747
  nextSticky = item;
267
748
  }
268
749
  displayItems.push(item);
269
- // for dynamic item size
270
- // this._sizeCacheMap.set(id, measures);
271
750
  }
272
- renderWeight -= itemSize;
273
- pos += itemSize;
751
+ renderWeight -= size;
752
+ pos += size;
274
753
  i++;
275
754
  }
276
755
  const axis = isVertical ? 'y' : 'x';
277
- if (nextSticky && stickyItem && nextSticky.measures[axis] <= scrollSize + itemSize) {
756
+ if (nextSticky && stickyItem && nextSticky.measures[axis] <= scrollSize + stickyItemSize) {
278
757
  if (nextSticky.measures[axis] > scrollSize) {
279
- stickyItem.measures[axis] = nextSticky.measures[axis] - itemSize;
758
+ stickyItem.measures[axis] = nextSticky.measures[axis] - stickyItemSize;
280
759
  stickyItem.config.snapped = nextSticky.config.snapped = false;
281
760
  stickyItem.config.sticky = 1;
282
761
  }
@@ -286,17 +765,14 @@ class NgVirtualListComponent {
286
765
  }
287
766
  }
288
767
  this._displayItems.set(displayItems);
289
- const l = this._list();
290
- if (l) {
291
- l.nativeElement.style[isVertical ? 'height' : 'width'] = `${totalSize}px`;
292
- }
768
+ this.resetBoundsSize(isVertical, totalSize);
293
769
  })).subscribe();
294
770
  combineLatest([$initialized, toObservable(this.itemRenderer)]).pipe(takeUntilDestroyed(), distinctUntilChanged(), filter(([initialized]) => !!initialized), tap(([, itemRenderer]) => {
295
771
  this.resetRenderers(itemRenderer);
296
772
  }));
297
- combineLatest([$initialized, toObservable(this._displayItems)]).pipe(takeUntilDestroyed(), distinctUntilChanged(), filter(([initialized]) => !!initialized), tap(([, displayItems]) => {
773
+ combineLatest([$initialized, $displayItems]).pipe(takeUntilDestroyed(), distinctUntilChanged(), filter(([initialized]) => !!initialized), tap(([, displayItems]) => {
298
774
  this.createDisplayComponentsIfNeed(displayItems);
299
- this.tracking(displayItems);
775
+ this.tracking();
300
776
  })).subscribe();
301
777
  }
302
778
  ngOnInit() {
@@ -308,7 +784,7 @@ class NgVirtualListComponent {
308
784
  }
309
785
  createDisplayComponentsIfNeed(displayItems) {
310
786
  if (!displayItems || !this._listContainerRef) {
311
- this._disMap = {};
787
+ this._trackBox.setDisplayObjectIndexMapById({});
312
788
  return;
313
789
  }
314
790
  const _listContainerRef = this._listContainerRef;
@@ -324,7 +800,7 @@ class NgVirtualListComponent {
324
800
  comp?.destroy();
325
801
  const id = comp?.instance.item?.id;
326
802
  if (id !== undefined) {
327
- delete this._trackMap[id];
803
+ this._trackBox.untrackComponentByIdProperty(comp?.instance);
328
804
  }
329
805
  }
330
806
  this.resetRenderers();
@@ -336,40 +812,18 @@ class NgVirtualListComponent {
336
812
  item.instance.renderer = itemRenderer || this.itemRenderer();
337
813
  doMap[item.instance.id] = i;
338
814
  }
339
- this._disMap = doMap;
815
+ this._trackBox.setDisplayObjectIndexMapById(doMap);
340
816
  }
341
817
  /**
342
818
  * tracking by id
343
819
  */
344
- tracking(displayItems) {
345
- if (!displayItems) {
346
- return;
347
- }
348
- const untrackedItems = [...this._displayComponents];
349
- for (let i = 0, l = displayItems.length; i < l; i++) {
350
- const item = displayItems[i], diId = this._trackMap[item.id];
351
- if (this._trackMap.hasOwnProperty(item.id)) {
352
- const lastIndex = this._disMap[diId], el = this._displayComponents[lastIndex], elId = el?.instance.id;
353
- if (el && elId === diId) {
354
- const indexByUntrackedItems = untrackedItems.findIndex(v => v.instance.id === elId);
355
- if (indexByUntrackedItems > -1) {
356
- el.instance.item = item;
357
- untrackedItems.splice(indexByUntrackedItems, 1);
358
- continue;
359
- }
360
- }
361
- delete this._trackMap[item.id];
362
- }
363
- if (untrackedItems.length > 0) {
364
- const el = untrackedItems.shift(), item = displayItems[i];
365
- if (el) {
366
- el.instance.item = item;
367
- this._trackMap[item.id] = el.instance.id;
368
- }
369
- }
370
- }
371
- if (untrackedItems.length) {
372
- throw Error('tracking by id caused an error');
820
+ tracking() {
821
+ this._trackBox.track(this.dynamicSize());
822
+ }
823
+ resetBoundsSize(isVertical, totalSize) {
824
+ const l = this._list();
825
+ if (l) {
826
+ l.nativeElement.style[isVertical ? 'height' : 'width'] = `${totalSize}px`;
373
827
  }
374
828
  }
375
829
  /**
@@ -398,6 +852,9 @@ class NgVirtualListComponent {
398
852
  }
399
853
  }
400
854
  ngOnDestroy() {
855
+ if (this._trackBox) {
856
+ this._trackBox.dispose();
857
+ }
401
858
  const containerEl = this._container();
402
859
  if (containerEl) {
403
860
  containerEl.nativeElement.removeEventListener('scroll', this._onScrollHandler);
@@ -414,7 +871,7 @@ class NgVirtualListComponent {
414
871
  }
415
872
  }
416
873
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NgVirtualListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
417
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "19.2.14", type: NgVirtualListComponent, isStandalone: true, selector: "ng-virtual-list", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: true, transformFunction: null }, snap: { classPropertyName: "snap", publicName: "snap", isSignal: true, isRequired: false, transformFunction: null }, snapToItem: { classPropertyName: "snapToItem", publicName: "snapToItem", isSignal: true, isRequired: false, transformFunction: null }, itemRenderer: { classPropertyName: "itemRenderer", publicName: "itemRenderer", isSignal: true, isRequired: true, transformFunction: null }, stickyMap: { classPropertyName: "stickyMap", publicName: "stickyMap", isSignal: true, isRequired: false, transformFunction: null }, itemSize: { classPropertyName: "itemSize", publicName: "itemSize", isSignal: true, isRequired: false, transformFunction: null }, direction: { classPropertyName: "direction", publicName: "direction", isSignal: true, isRequired: false, transformFunction: null }, itemsOffset: { classPropertyName: "itemsOffset", publicName: "itemsOffset", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onScroll: "onScroll", onScrollEnd: "onScrollEnd" }, viewQueries: [{ propertyName: "_container", first: true, predicate: ["container"], descendants: true, isSignal: true }, { propertyName: "_list", first: true, predicate: ["list"], descendants: true, isSignal: true }, { propertyName: "_listContainerRef", first: true, predicate: ["renderersContainer"], descendants: true, read: ViewContainerRef }], ngImport: i0, template: "<div #container part=\"scroller\" class=\"ngvl__container\">\r\n <ul #list part=\"list\" class=\"ngvl__list\">\r\n <ng-container #renderersContainer></ng-container>\r\n </ul>\r\n</div>", styles: [":host{display:block;width:400px;overflow:hidden}:host(.horizontal){height:48px}:host(.vertical){height:320px}.ngvl__container{overflow:auto;width:100%;height:100%}.ngvl__list{position:relative;list-style:none;padding:0;margin:0;width:100%;height:100%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.ShadowDom });
874
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "19.2.14", type: NgVirtualListComponent, isStandalone: true, selector: "ng-virtual-list", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: true, transformFunction: null }, snap: { classPropertyName: "snap", publicName: "snap", isSignal: true, isRequired: false, transformFunction: null }, snapToItem: { classPropertyName: "snapToItem", publicName: "snapToItem", isSignal: true, isRequired: false, transformFunction: null }, itemRenderer: { classPropertyName: "itemRenderer", publicName: "itemRenderer", isSignal: true, isRequired: true, transformFunction: null }, stickyMap: { classPropertyName: "stickyMap", publicName: "stickyMap", isSignal: true, isRequired: false, transformFunction: null }, itemSize: { classPropertyName: "itemSize", publicName: "itemSize", isSignal: true, isRequired: false, transformFunction: null }, dynamicSize: { classPropertyName: "dynamicSize", publicName: "dynamicSize", isSignal: true, isRequired: false, transformFunction: null }, direction: { classPropertyName: "direction", publicName: "direction", isSignal: true, isRequired: false, transformFunction: null }, itemsOffset: { classPropertyName: "itemsOffset", publicName: "itemsOffset", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onScroll: "onScroll", onScrollEnd: "onScrollEnd" }, viewQueries: [{ propertyName: "_container", first: true, predicate: ["container"], descendants: true, isSignal: true }, { propertyName: "_list", first: true, predicate: ["list"], descendants: true, isSignal: true }, { propertyName: "_listContainerRef", first: true, predicate: ["renderersContainer"], descendants: true, read: ViewContainerRef }], ngImport: i0, template: "<div #container part=\"scroller\" class=\"ngvl__container\">\r\n <ul #list part=\"list\" class=\"ngvl__list\">\r\n <ng-container #renderersContainer></ng-container>\r\n </ul>\r\n</div>", styles: [":host{display:block;width:400px;overflow:hidden}:host(.horizontal){height:48px}:host(.vertical){height:320px}.ngvl__container{overflow:auto;width:100%;height:100%}.ngvl__list{position:relative;list-style:none;padding:0;margin:0;width:100%;height:100%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.ShadowDom });
418
875
  }
419
876
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NgVirtualListComponent, decorators: [{
420
877
  type: Component,