ng-virtual-list 0.5.1 → 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;
@@ -64,13 +68,13 @@ class NgVirtualListItemComponent {
64
68
  styles.visibility = 'hidden';
65
69
  }
66
70
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NgVirtualListItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
67
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: NgVirtualListItemComponent, isStandalone: true, selector: "ng-virtual-list-item", host: { classAttribute: "ngvl__item" }, ngImport: i0, template: "@let item = data();\r\n@let renderer = itemRenderer();\r\n\r\n@if (item) {\r\n<li #listItem part=\"item\" class=\"ngvl-item__container\">\r\n @if (renderer) {\r\n <ng-container [ngTemplateOutlet]=\"renderer\" [ngTemplateOutletContext]=\"{data: item.data || {}}\" />\r\n }\r\n</li>\r\n}", styles: [":host{position:absolute;left:0;top:0;width:100%;height:100%;box-sizing:border-box;overflow:hidden}.ngvl-item__container{margin:0;padding:0;overflow:hidden;background-color:#fff;width:inherit;height:inherit}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
71
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: NgVirtualListItemComponent, isStandalone: true, selector: "ng-virtual-list-item", host: { classAttribute: "ngvl__item" }, ngImport: i0, template: "@let item = data();\r\n@let renderer = itemRenderer();\r\n\r\n@if (item) {\r\n<li #listItem part=\"item\" class=\"ngvl-item__container\">\r\n @if (renderer) {\r\n <ng-container [ngTemplateOutlet]=\"renderer\" [ngTemplateOutletContext]=\"{data: item.data || {}}\" />\r\n }\r\n</li>\r\n}", styles: [":host{display:block;position:absolute;left:0;top:0;box-sizing:border-box;overflow:hidden}.ngvl-item__container{margin:0;padding:0;overflow:hidden;background-color:#fff;width:inherit;height:inherit}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
68
72
  }
69
73
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NgVirtualListItemComponent, decorators: [{
70
74
  type: Component,
71
75
  args: [{ selector: 'ng-virtual-list-item', imports: [CommonModule], host: {
72
76
  'class': 'ngvl__item',
73
- }, changeDetection: ChangeDetectionStrategy.OnPush, template: "@let item = data();\r\n@let renderer = itemRenderer();\r\n\r\n@if (item) {\r\n<li #listItem part=\"item\" class=\"ngvl-item__container\">\r\n @if (renderer) {\r\n <ng-container [ngTemplateOutlet]=\"renderer\" [ngTemplateOutletContext]=\"{data: item.data || {}}\" />\r\n }\r\n</li>\r\n}", styles: [":host{position:absolute;left:0;top:0;width:100%;height:100%;box-sizing:border-box;overflow:hidden}.ngvl-item__container{margin:0;padding:0;overflow:hidden;background-color:#fff;width:inherit;height:inherit}\n"] }]
77
+ }, changeDetection: ChangeDetectionStrategy.OnPush, template: "@let item = data();\r\n@let renderer = itemRenderer();\r\n\r\n@if (item) {\r\n<li #listItem part=\"item\" class=\"ngvl-item__container\">\r\n @if (renderer) {\r\n <ng-container [ngTemplateOutlet]=\"renderer\" [ngTemplateOutletContext]=\"{data: item.data || {}}\" />\r\n }\r\n</li>\r\n}", styles: [":host{display:block;position:absolute;left:0;top:0;box-sizing:border-box;overflow:hidden}.ngvl-item__container{margin:0;padding:0;overflow:hidden;background-color:#fff;width:inherit;height:inherit}\n"] }]
74
78
  }], ctorParameters: () => [] });
75
79
 
76
80
  var Directions;
@@ -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
  */
@@ -184,33 +639,66 @@ class NgVirtualListComponent {
184
639
  this.onScrollEnd.emit(e);
185
640
  };
186
641
  _elementRef = inject((ElementRef));
187
- // for dynamic item size
188
- // private _sizeCacheMap = new Map<Id, IRect>();
642
+ _initialized = signal(false);
643
+ /**
644
+ * Dictionary of element sizes by their id
645
+ */
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(); }
189
652
  constructor() {
190
653
  NgVirtualListComponent.__nextId = NgVirtualListComponent.__nextId + 1 === Number.MAX_SAFE_INTEGER
191
654
  ? 0 : NgVirtualListComponent.__nextId + 1;
192
655
  this._id = NgVirtualListComponent.__nextId;
193
- 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 => {
194
659
  this._isVertical = v;
195
660
  const el = this._elementRef.nativeElement;
196
661
  toggleClassName(el, v ? 'vertical' : 'horizontal', true);
197
- }));
198
- combineLatest([$bounds, $items, $stickyMap, $scrollSize, $itemSize, $itemsOffset, $snap, $isVertical]).pipe(takeUntilDestroyed(), distinctUntilChanged(), switchMap(([bounds, items, stickyMap, scrollSize, itemSize, itemsOffset, snap, isVertical]) => {
199
- const { width, height } = bounds, size = isVertical ? height : width;
200
- 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 может расчитать дельту!
201
688
  return of({
202
- items, stickyMap, itemsOffset, width, height, isVertical, scrollSize, itemsFromStartToScrollEnd,
203
- 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,
204
692
  });
205
- }), 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, }) => {
206
694
  const displayItems = [];
207
695
  if (items.length) {
208
- const w = isVertical ? width : itemSize, h = isVertical ? itemSize : height, totalItems = items.length, leftItemLength = Math.min(itemsFromStartToScrollEnd, itemsOffset), rightItemLength = itemsFromStartToDisplayEnd + itemsOffset > totalItems
209
- ? totalItems - itemsFromStartToDisplayEnd : itemsOffset, leftItemsWeight = leftItemLength * itemSize, rightItemsWeight = rightItemLength * itemSize, startIndex = itemsFromStartToScrollEnd - leftItemLength, snippedPos = Math.floor(scrollSize);
210
- 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;
211
698
  if (snap) {
212
699
  for (let i = itemsFromStartToScrollEnd - 1; i >= 0; i--) {
213
700
  const id = items[i].id, sticky = stickyMap[id];
701
+ stickyItemSize = dynamic ? this._trackBox.get(id)?.[sizeProperty] || itemSize : itemSize;
214
702
  if (sticky > 0) {
215
703
  const measures = {
216
704
  x: isVertical ? 0 : snippedPos,
@@ -222,6 +710,7 @@ class NgVirtualListComponent {
222
710
  sticky,
223
711
  snap,
224
712
  snapped: true,
713
+ dynamic,
225
714
  };
226
715
  const itemData = items[i];
227
716
  stickyItem = { id, measures, data: itemData, config };
@@ -236,7 +725,7 @@ class NgVirtualListComponent {
236
725
  if (i >= totalItems) {
237
726
  break;
238
727
  }
239
- const id = items[i].id;
728
+ const id = items[i].id, size = dynamic ? this._trackBox.get(id)?.[sizeProperty] || itemSize : itemSize;
240
729
  if (id !== stickyItem?.id) {
241
730
  const snaped = snap && stickyMap[id] > 0 && pos <= scrollSize, measures = {
242
731
  x: isVertical ? 0 : pos,
@@ -248,26 +737,25 @@ class NgVirtualListComponent {
248
737
  sticky: stickyMap[id],
249
738
  snap,
250
739
  snapped: false,
740
+ dynamic,
251
741
  };
252
742
  const itemData = items[i];
253
743
  const item = { id, measures, data: itemData, config };
254
- if (!nextSticky && stickyItemIndex < i && snap && stickyMap[id] > 0 && pos <= scrollSize + itemSize) {
744
+ if (!nextSticky && stickyItemIndex < i && snap && stickyMap[id] > 0 && pos <= scrollSize + size) {
255
745
  item.measures.x = isVertical ? 0 : snaped ? snippedPos : pos;
256
746
  item.measures.y = isVertical ? snaped ? snippedPos : pos : 0;
257
747
  nextSticky = item;
258
748
  }
259
749
  displayItems.push(item);
260
- // for dynamic item size
261
- // this._sizeCacheMap.set(id, measures);
262
750
  }
263
- renderWeight -= itemSize;
264
- pos += itemSize;
751
+ renderWeight -= size;
752
+ pos += size;
265
753
  i++;
266
754
  }
267
755
  const axis = isVertical ? 'y' : 'x';
268
- if (nextSticky && stickyItem && nextSticky.measures[axis] <= scrollSize + itemSize) {
756
+ if (nextSticky && stickyItem && nextSticky.measures[axis] <= scrollSize + stickyItemSize) {
269
757
  if (nextSticky.measures[axis] > scrollSize) {
270
- stickyItem.measures[axis] = nextSticky.measures[axis] - itemSize;
758
+ stickyItem.measures[axis] = nextSticky.measures[axis] - stickyItemSize;
271
759
  stickyItem.config.snapped = nextSticky.config.snapped = false;
272
760
  stickyItem.config.sticky = 1;
273
761
  }
@@ -277,26 +765,26 @@ class NgVirtualListComponent {
277
765
  }
278
766
  }
279
767
  this._displayItems.set(displayItems);
280
- const l = this._list();
281
- if (l) {
282
- l.nativeElement.style[isVertical ? 'height' : 'width'] = `${totalSize}px`;
283
- }
768
+ this.resetBoundsSize(isVertical, totalSize);
284
769
  })).subscribe();
285
- toObservable(this.itemRenderer).pipe(takeUntilDestroyed(), distinctUntilChanged(), tap(itemRenderer => {
770
+ combineLatest([$initialized, toObservable(this.itemRenderer)]).pipe(takeUntilDestroyed(), distinctUntilChanged(), filter(([initialized]) => !!initialized), tap(([, itemRenderer]) => {
286
771
  this.resetRenderers(itemRenderer);
287
772
  }));
288
- toObservable(this._displayItems).pipe(takeUntilDestroyed(), distinctUntilChanged(), tap(displayItems => {
773
+ combineLatest([$initialized, $displayItems]).pipe(takeUntilDestroyed(), distinctUntilChanged(), filter(([initialized]) => !!initialized), tap(([, displayItems]) => {
289
774
  this.createDisplayComponentsIfNeed(displayItems);
290
- this.tracking(displayItems);
775
+ this.tracking();
291
776
  })).subscribe();
292
777
  }
778
+ ngOnInit() {
779
+ this._initialized.set(true);
780
+ }
293
781
  getIsVertical(d) {
294
782
  const dir = d || this.direction();
295
783
  return isDirection(dir, Directions.VERTICAL);
296
784
  }
297
785
  createDisplayComponentsIfNeed(displayItems) {
298
786
  if (!displayItems || !this._listContainerRef) {
299
- this._doMap = {};
787
+ this._trackBox.setDisplayObjectIndexMapById({});
300
788
  return;
301
789
  }
302
790
  const _listContainerRef = this._listContainerRef;
@@ -312,7 +800,7 @@ class NgVirtualListComponent {
312
800
  comp?.destroy();
313
801
  const id = comp?.instance.item?.id;
314
802
  if (id !== undefined) {
315
- delete this._trackMap[id];
803
+ this._trackBox.untrackComponentByIdProperty(comp?.instance);
316
804
  }
317
805
  }
318
806
  this.resetRenderers();
@@ -324,48 +812,18 @@ class NgVirtualListComponent {
324
812
  item.instance.renderer = itemRenderer || this.itemRenderer();
325
813
  doMap[item.instance.id] = i;
326
814
  }
327
- this._doMap = doMap;
815
+ this._trackBox.setDisplayObjectIndexMapById(doMap);
328
816
  }
329
- /**
330
- * Dictionary displayItems id by IRenderVirtualListItem.id
331
- */
332
- _trackMap = {};
333
- /**
334
- * displayItems dictionary of indexes by id
335
- */
336
- _doMap = {};
337
817
  /**
338
818
  * tracking by id
339
819
  */
340
- tracking(displayItems) {
341
- if (!displayItems) {
342
- return;
343
- }
344
- const untrackedItems = [...this._displayComponents];
345
- for (let i = 0, l = displayItems.length; i < l; i++) {
346
- const item = displayItems[i], doId = this._trackMap[item.id];
347
- if (this._trackMap.hasOwnProperty(item.id)) {
348
- const lastIndex = this._doMap[doId], el = this._displayComponents[lastIndex], elId = el?.instance.id;
349
- if (el && elId === doId) {
350
- const indexByUntrackedItems = untrackedItems.findIndex(v => v.instance.id === elId);
351
- if (indexByUntrackedItems > -1) {
352
- el.instance.item = item;
353
- untrackedItems.splice(indexByUntrackedItems, 1);
354
- continue;
355
- }
356
- }
357
- delete this._trackMap[item.id];
358
- }
359
- if (untrackedItems.length > 0) {
360
- const el = untrackedItems.shift(), item = displayItems[i];
361
- if (el) {
362
- el.instance.item = item;
363
- this._trackMap[item.id] = el.instance.id;
364
- }
365
- }
366
- }
367
- if (untrackedItems.length) {
368
- 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`;
369
827
  }
370
828
  }
371
829
  /**
@@ -394,6 +852,9 @@ class NgVirtualListComponent {
394
852
  }
395
853
  }
396
854
  ngOnDestroy() {
855
+ if (this._trackBox) {
856
+ this._trackBox.dispose();
857
+ }
397
858
  const containerEl = this._container();
398
859
  if (containerEl) {
399
860
  containerEl.nativeElement.removeEventListener('scroll', this._onScrollHandler);
@@ -410,7 +871,7 @@ class NgVirtualListComponent {
410
871
  }
411
872
  }
412
873
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NgVirtualListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
413
- 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 });
414
875
  }
415
876
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NgVirtualListComponent, decorators: [{
416
877
  type: Component,