ng-virtual-list 15.0.21 → 15.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +1 -1
  3. package/ng-package.json +7 -0
  4. package/package.json +29 -48
  5. package/src/lib/components/ng-virtual-list-item.component.html +9 -0
  6. package/src/lib/components/ng-virtual-list-item.component.scss +17 -0
  7. package/src/lib/components/ng-virtual-list-item.component.spec.ts +23 -0
  8. package/src/lib/components/ng-virtual-list-item.component.ts +111 -0
  9. package/src/lib/const/index.ts +67 -0
  10. package/{lib/enums/direction.d.ts → src/lib/enums/direction.ts} +1 -0
  11. package/{lib/enums/directions.d.ts → src/lib/enums/directions.ts} +4 -4
  12. package/{lib/enums/index.d.ts → src/lib/enums/index.ts} +4 -1
  13. package/{lib/models/collection.model.d.ts → src/lib/models/collection.model.ts} +2 -2
  14. package/{lib/models/index.d.ts → src/lib/models/index.ts} +8 -1
  15. package/{lib/models/item.model.d.ts → src/lib/models/item.model.ts} +2 -1
  16. package/{lib/models/render-collection.model.d.ts → src/lib/models/render-collection.model.ts} +2 -2
  17. package/{lib/models/render-item-config.model.d.ts → src/lib/models/render-item-config.model.ts} +2 -2
  18. package/{lib/models/render-item.model.d.ts → src/lib/models/render-item.model.ts} +2 -1
  19. package/{lib/models/scroll-event.model.d.ts → src/lib/models/scroll-event.model.ts} +1 -0
  20. package/{lib/models/sticky-map.model.d.ts → src/lib/models/sticky-map.model.ts} +1 -1
  21. package/src/lib/ng-virtual-list.component.html +5 -0
  22. package/src/lib/ng-virtual-list.component.scss +28 -0
  23. package/src/lib/ng-virtual-list.component.spec.ts +23 -0
  24. package/src/lib/ng-virtual-list.component.ts +671 -0
  25. package/src/lib/ng-virtual-list.module.ts +12 -0
  26. package/{lib/types/id.d.ts → src/lib/types/id.ts} +1 -1
  27. package/{lib/types/index.d.ts → src/lib/types/index.ts} +6 -1
  28. package/{lib/types/rect.d.ts → src/lib/types/rect.ts} +2 -1
  29. package/{lib/types/size.d.ts → src/lib/types/size.ts} +1 -1
  30. package/src/lib/utils/cacheMap.ts +224 -0
  31. package/src/lib/utils/debounce.ts +31 -0
  32. package/src/lib/utils/disposableComponent.ts +22 -0
  33. package/src/lib/utils/eventEmitter.ts +119 -0
  34. package/{lib/utils/index.d.ts → src/lib/utils/index.ts} +9 -1
  35. package/src/lib/utils/isDirection.ts +17 -0
  36. package/src/lib/utils/scrollEvent.ts +62 -0
  37. package/src/lib/utils/toggleClassName.ts +14 -0
  38. package/src/lib/utils/trackBox.ts +839 -0
  39. package/src/lib/utils/tracker.ts +126 -0
  40. package/{public-api.d.ts → src/public-api.ts} +4 -0
  41. package/tsconfig.lib.json +15 -0
  42. package/tsconfig.lib.prod.json +11 -0
  43. package/tsconfig.spec.json +15 -0
  44. package/esm2020/lib/components/ng-virtual-list-item.component.mjs +0 -87
  45. package/esm2020/lib/const/index.mjs +0 -35
  46. package/esm2020/lib/enums/direction.mjs +0 -2
  47. package/esm2020/lib/enums/directions.mjs +0 -18
  48. package/esm2020/lib/enums/index.mjs +0 -3
  49. package/esm2020/lib/models/collection.model.mjs +0 -3
  50. package/esm2020/lib/models/index.mjs +0 -2
  51. package/esm2020/lib/models/item.model.mjs +0 -3
  52. package/esm2020/lib/models/render-collection.model.mjs +0 -3
  53. package/esm2020/lib/models/render-item-config.model.mjs +0 -2
  54. package/esm2020/lib/models/render-item.model.mjs +0 -3
  55. package/esm2020/lib/models/scroll-direction.model.mjs +0 -2
  56. package/esm2020/lib/models/scroll-event.model.mjs +0 -2
  57. package/esm2020/lib/models/sticky-map.model.mjs +0 -2
  58. package/esm2020/lib/ng-virtual-list.component.mjs +0 -508
  59. package/esm2020/lib/ng-virtual-list.module.mjs +0 -20
  60. package/esm2020/lib/types/id.mjs +0 -2
  61. package/esm2020/lib/types/index.mjs +0 -2
  62. package/esm2020/lib/types/rect.mjs +0 -2
  63. package/esm2020/lib/types/size.mjs +0 -2
  64. package/esm2020/lib/utils/cacheMap.mjs +0 -167
  65. package/esm2020/lib/utils/debounce.mjs +0 -31
  66. package/esm2020/lib/utils/disposableComponent.mjs +0 -29
  67. package/esm2020/lib/utils/eventEmitter.mjs +0 -106
  68. package/esm2020/lib/utils/index.mjs +0 -8
  69. package/esm2020/lib/utils/isDirection.mjs +0 -15
  70. package/esm2020/lib/utils/scrollEvent.mjs +0 -42
  71. package/esm2020/lib/utils/toggleClassName.mjs +0 -15
  72. package/esm2020/lib/utils/trackBox.mjs +0 -623
  73. package/esm2020/lib/utils/tracker.mjs +0 -92
  74. package/esm2020/ng-virtual-list.mjs +0 -5
  75. package/esm2020/public-api.mjs +0 -8
  76. package/fesm2015/ng-virtual-list.mjs +0 -1767
  77. package/fesm2015/ng-virtual-list.mjs.map +0 -1
  78. package/fesm2020/ng-virtual-list.mjs +0 -1776
  79. package/fesm2020/ng-virtual-list.mjs.map +0 -1
  80. package/index.d.ts +0 -5
  81. package/lib/components/ng-virtual-list-item.component.d.ts +0 -30
  82. package/lib/const/index.d.ts +0 -33
  83. package/lib/ng-virtual-list.component.d.ts +0 -165
  84. package/lib/ng-virtual-list.module.d.ts +0 -9
  85. package/lib/utils/cacheMap.d.ts +0 -60
  86. package/lib/utils/debounce.d.ts +0 -16
  87. package/lib/utils/disposableComponent.d.ts +0 -15
  88. package/lib/utils/eventEmitter.d.ts +0 -40
  89. package/lib/utils/isDirection.d.ts +0 -8
  90. package/lib/utils/scrollEvent.d.ts +0 -39
  91. package/lib/utils/toggleClassName.d.ts +0 -7
  92. package/lib/utils/trackBox.d.ts +0 -176
  93. package/lib/utils/tracker.d.ts +0 -44
  94. /package/{lib/models/scroll-direction.model.d.ts → src/lib/models/scroll-direction.model.ts} +0 -0
@@ -0,0 +1,839 @@
1
+ import { ComponentRef } from "@angular/core";
2
+ import { NgVirtualListItemComponent } from "../components/ng-virtual-list-item.component";
3
+ import { IRenderVirtualListCollection } from "../models/render-collection.model";
4
+ import { IRenderVirtualListItem } from "../models/render-item.model";
5
+ import { Id } from "../types/id";
6
+ import { CacheMap, CMap } from "./cacheMap";
7
+ import { Tracker } from "./tracker";
8
+ import { ISize } from "../types";
9
+ import { HEIGHT_PROP_NAME, WIDTH_PROP_NAME, X_PROP_NAME, Y_PROP_NAME } from "../const";
10
+ import { IVirtualListStickyMap } from "../models";
11
+
12
+ export const TRACK_BOX_CHANGE_EVENT_NAME = 'change';
13
+
14
+ export interface IMetrics {
15
+ delta: number;
16
+ normalizedItemWidth: number;
17
+ normalizedItemHeight: number;
18
+ width: number;
19
+ height: number;
20
+ dynamicSize: boolean;
21
+ itemSize: number;
22
+ itemsFromStartToScrollEnd: number;
23
+ itemsFromStartToDisplayEnd: number;
24
+ itemsOnDisplay: number;
25
+ itemsOnDisplayLength: number;
26
+ isVertical: boolean;
27
+ leftHiddenItemsWeight: number;
28
+ leftItemLength: number;
29
+ leftItemsWeight: number;
30
+ renderItems: number;
31
+ rightItemLength: number;
32
+ rightItemsWeight: number;
33
+ scrollSize: number;
34
+ leftSizeOfAddedItems: number;
35
+ sizeProperty: typeof HEIGHT_PROP_NAME | typeof WIDTH_PROP_NAME;
36
+ snap: boolean;
37
+ snippedPos: number;
38
+ startIndex: number;
39
+ startPosition: number;
40
+ totalItemsToDisplayEndWeight: number;
41
+ totalLength: number;
42
+ totalSize: number;
43
+ typicalItemSize: number;
44
+ }
45
+
46
+ export interface IRecalculateMetricsOptions<I extends { id: Id }, C extends Array<I>> {
47
+ bounds: ISize;
48
+ collection: C;
49
+ isVertical: boolean;
50
+ itemSize: number;
51
+ itemsOffset: number;
52
+ dynamicSize: boolean;
53
+ scrollSize: number;
54
+ snap: boolean;
55
+ enabledBufferOptimization: boolean;
56
+ fromItemId?: Id;
57
+ previousTotalSize: number;
58
+ crudDetected: boolean;
59
+ deletedItemsMap: { [index: number]: ISize; };
60
+ }
61
+
62
+ export interface IGetItemPositionOptions<I extends { id: Id }, C extends Array<I>>
63
+ extends Omit<IRecalculateMetricsOptions<I, C>, 'previousTotalSize' | 'crudDetected' | 'deletedItemsMap'> { }
64
+
65
+ export interface IUpdateCollectionOptions<I extends { id: Id }, C extends Array<I>>
66
+ extends Omit<IRecalculateMetricsOptions<I, C>, 'collection' | 'previousTotalSize' | 'crudDetected' | 'deletedItemsMap'> { }
67
+
68
+ type CacheMapEvents = typeof TRACK_BOX_CHANGE_EVENT_NAME;
69
+
70
+ type OnChangeEventListener = (version: number) => void;
71
+
72
+ type CacheMapListeners = OnChangeEventListener;
73
+
74
+ enum ItemDisplayMethods {
75
+ CREATE,
76
+ UPDATE,
77
+ DELETE,
78
+ NOT_CHANGED,
79
+ }
80
+
81
+ interface IUpdateCollectionReturns {
82
+ displayItems: IRenderVirtualListCollection;
83
+ totalSize: number;
84
+ delta: number;
85
+ crudDetected: boolean;
86
+ }
87
+
88
+ /**
89
+ * An object that performs tracking, calculations and caching.
90
+ * @link https://github.com/DjonnyX/ng-virtual-list/blob/15.x/projects/ng-virtual-list/src/lib/utils/trackBox.ts
91
+ * @author Evgenii Grebennikov
92
+ * @email djonnyx@gmail.com
93
+ */
94
+ export class TrackBox extends CacheMap<Id, ISize & { method?: ItemDisplayMethods }, CacheMapEvents, CacheMapListeners> {
95
+ protected _tracker!: Tracker<IRenderVirtualListItem, NgVirtualListItemComponent>;
96
+
97
+ protected _items: IRenderVirtualListCollection | null | undefined;
98
+
99
+ set items(v: IRenderVirtualListCollection | null | undefined) {
100
+ if (this._items === v) {
101
+ return;
102
+ }
103
+
104
+ this._items = v;
105
+ }
106
+
107
+ protected _displayComponents: Array<ComponentRef<NgVirtualListItemComponent>> | null | undefined;
108
+
109
+ set displayComponents(v: Array<ComponentRef<NgVirtualListItemComponent>> | null | undefined) {
110
+ if (this._displayComponents === v) {
111
+ return;
112
+ }
113
+
114
+ this._displayComponents = v;
115
+ }
116
+
117
+ /**
118
+ * Set the trackBy property
119
+ */
120
+ set trackingPropertyName(v: string) {
121
+ this._tracker.trackingPropertyName = v;
122
+ }
123
+
124
+ constructor(trackingPropertyName: string) {
125
+ super();
126
+
127
+ this._tracker = new Tracker(trackingPropertyName);
128
+ }
129
+
130
+ override set(id: Id, bounds: ISize): CMap<Id, ISize> {
131
+ if (this._map.has(id)) {
132
+ const b = this._map.get(id);
133
+ if (b?.width === bounds.width && b.height === bounds.height) {
134
+ return this._map;
135
+ }
136
+ }
137
+
138
+ const v = this._map.set(id, bounds);
139
+
140
+ this.bumpVersion();
141
+ return v;
142
+ }
143
+
144
+ private _previousCollection: Array<{ id: Id; }> | null | undefined;
145
+
146
+ private _deletedItemsMap: { [index: number]: ISize } = {};
147
+
148
+ private _crudDetected = false;
149
+ get crudDetected() { return this._crudDetected; }
150
+
151
+ protected override fireChangeIfNeed() {
152
+ if (this.changesDetected()) {
153
+ this.dispatch(TRACK_BOX_CHANGE_EVENT_NAME, this._version);
154
+ }
155
+ }
156
+
157
+ private _previousTotalSize = 0;
158
+
159
+ protected _scrollDelta: number = 0;
160
+ get scrollDelta() { return this._scrollDelta; }
161
+
162
+ protected override lifeCircle() {
163
+ this.fireChangeIfNeed();
164
+
165
+ this.lifeCircleDo();
166
+ }
167
+
168
+ /**
169
+ * Scans the collection for deleted items and flushes the deleted item cache.
170
+ */
171
+ resetCollection<I extends { id: Id; }, C extends Array<I>>(currentCollection: C | null | undefined, itemSize: number): void {
172
+ if (currentCollection !== undefined && currentCollection !== null && currentCollection === this._previousCollection) {
173
+ console.warn('Attention! The collection must be immutable.');
174
+ return;
175
+ }
176
+
177
+ this.updateCache(this._previousCollection, currentCollection, itemSize);
178
+
179
+ this._previousCollection = currentCollection;
180
+ }
181
+
182
+ /**
183
+ * Update the cache of items from the list
184
+ */
185
+ protected updateCache<I extends { id: Id; }, C extends Array<I>>(previousCollection: C | null | undefined, currentCollection: C | null | undefined,
186
+ itemSize: number): void {
187
+ let crudDetected = false;
188
+
189
+ if (!currentCollection || currentCollection.length === 0) {
190
+ if (previousCollection) {
191
+ // deleted
192
+ for (let i = 0, l = previousCollection.length; i < l; i++) {
193
+ const item = previousCollection[i], id = item.id;
194
+ crudDetected = true;
195
+ if (this._map.has(id)) {
196
+ this._map.delete(id);
197
+ }
198
+ }
199
+ }
200
+ return;
201
+ }
202
+ if (!previousCollection || previousCollection.length === 0) {
203
+ if (currentCollection) {
204
+ // added
205
+ for (let i = 0, l = currentCollection.length; i < l; i++) {
206
+ crudDetected = true;
207
+ const item = currentCollection[i], id = item.id;
208
+ this._map.set(id, { width: itemSize, height: itemSize, method: ItemDisplayMethods.CREATE });
209
+ }
210
+ }
211
+ return;
212
+ }
213
+ const collectionDict: { [id: Id]: I } = {};
214
+ for (let i = 0, l = currentCollection.length; i < l; i++) {
215
+ const item = currentCollection[i];
216
+ if (item) {
217
+ collectionDict[item.id] = item;
218
+ }
219
+ }
220
+ const notChangedMap: { [id: Id]: I } = {}, deletedMap: { [id: Id]: I } = {}, deletedItemsMap: { [index: number]: ISize } = {}, updatedMap: { [id: Id]: I } = {};
221
+ for (let i = 0, l = previousCollection.length; i < l; i++) {
222
+ const item = previousCollection[i], id = item.id;
223
+ if (item) {
224
+ if (collectionDict.hasOwnProperty(id)) {
225
+ if (item === collectionDict[id]) {
226
+ // not changed
227
+ notChangedMap[item.id] = item;
228
+ this._map.set(id, { ...(this._map.get(id) || { width: itemSize, height: itemSize }), method: ItemDisplayMethods.NOT_CHANGED });
229
+ continue;
230
+ } else {
231
+ // updated
232
+ crudDetected = true;
233
+ updatedMap[item.id] = item;
234
+ this._map.set(id, { ...(this._map.get(id) || { width: itemSize, height: itemSize }), method: ItemDisplayMethods.UPDATE });
235
+ continue;
236
+ }
237
+ }
238
+
239
+ // deleted
240
+ crudDetected = true;
241
+ deletedMap[item.id] = item;
242
+ deletedItemsMap[i] = this._map.get(item.id);
243
+ this._map.delete(id);
244
+ }
245
+ }
246
+
247
+ for (let i = 0, l = currentCollection.length; i < l; i++) {
248
+ const item = currentCollection[i], id = item.id;
249
+ if (item && !deletedMap.hasOwnProperty(id) && !updatedMap.hasOwnProperty(id) && !notChangedMap.hasOwnProperty(id)) {
250
+ // added
251
+ crudDetected = true;
252
+ this._map.set(id, { width: itemSize, height: itemSize, method: ItemDisplayMethods.CREATE });
253
+ }
254
+ }
255
+ this._crudDetected = crudDetected;
256
+ this._deletedItemsMap = deletedItemsMap;
257
+ }
258
+
259
+ /**
260
+ * Finds the position of a collection element by the given Id
261
+ */
262
+ getItemPosition<I extends { id: Id }, C extends Array<I>>(id: Id, stickyMap: IVirtualListStickyMap,
263
+ options: IGetItemPositionOptions<I, C>): number {
264
+ const opt = { fromItemId: id, stickyMap, ...options };
265
+ const { scrollSize } = this.recalculateMetrics({
266
+ ...opt,
267
+ dynamicSize: this._crudDetected || opt.dynamicSize,
268
+ previousTotalSize: this._previousTotalSize,
269
+ crudDetected: this._crudDetected,
270
+ deletedItemsMap: this._deletedItemsMap,
271
+ });
272
+ return scrollSize;
273
+ }
274
+
275
+ /**
276
+ * Updates the collection of display objects
277
+ */
278
+ updateCollection<I extends { id: Id }, C extends Array<I>>(items: C, stickyMap: IVirtualListStickyMap,
279
+ options: IUpdateCollectionOptions<I, C>): IUpdateCollectionReturns {
280
+ const opt = { stickyMap, ...options }, crudDetected = this._crudDetected, deletedItemsMap = this._deletedItemsMap;
281
+ if (opt.dynamicSize) {
282
+ this.cacheElements();
283
+ }
284
+
285
+ const metrics = this.recalculateMetrics({
286
+ ...opt,
287
+ collection: items,
288
+ previousTotalSize: this._previousTotalSize,
289
+ crudDetected: this._crudDetected,
290
+ deletedItemsMap,
291
+ });
292
+
293
+ this._delta += metrics.delta;
294
+
295
+ this._previousTotalSize = metrics.totalSize;
296
+
297
+ this._deletedItemsMap = {};
298
+
299
+ this._crudDetected = false;
300
+
301
+ if (opt.dynamicSize) {
302
+ this.snapshot();
303
+ }
304
+
305
+ const displayItems = this.generateDisplayCollection(items, stickyMap, { ...metrics, });
306
+ return { displayItems, totalSize: metrics.totalSize, delta: metrics.delta, crudDetected };
307
+ }
308
+
309
+ /**
310
+ * Finds the closest element in the collection by scrollSize
311
+ */
312
+ getNearestItem<I extends { id: Id }, C extends Array<I>>(scrollSize: number, items: C, itemSize: number, isVertical: boolean): I | undefined {
313
+ return this.getElementFromStart(scrollSize, items, this._map, itemSize, isVertical);
314
+ }
315
+
316
+ /**
317
+ * Calculates the position of an element based on the given scrollSize
318
+ */
319
+ private getElementFromStart<I extends { id: Id }, C extends Array<I>>(scrollSize: number, collection: C, map: CMap<Id, ISize>, typicalItemSize: number,
320
+ isVertical: boolean): I | undefined {
321
+ const sizeProperty = isVertical ? HEIGHT_PROP_NAME : WIDTH_PROP_NAME;
322
+ let offset = 0;
323
+ for (let i = 0, l = collection.length; i < l; i++) {
324
+ const item = collection[i];
325
+ let itemSize = 0;
326
+ if (map.has(item.id)) {
327
+ const bounds = map.get(item.id);
328
+ itemSize = bounds ? bounds[sizeProperty] : typicalItemSize;
329
+ } else {
330
+ itemSize = typicalItemSize;
331
+ }
332
+ if (offset > scrollSize) {
333
+ return item;
334
+ }
335
+ offset += itemSize;
336
+ }
337
+ return undefined;
338
+ }
339
+
340
+ /**
341
+ * Calculates the entry into the overscroll area and returns the number of overscroll elements
342
+ */
343
+ private getElementNumToEnd<I extends { id: Id }, C extends Array<I>>(i: number, collection: C, map: CMap<Id, ISize>, typicalItemSize: number,
344
+ size: number, isVertical: boolean, indexOffset: number = 0): { num: number, offset: number } {
345
+ const sizeProperty = isVertical ? HEIGHT_PROP_NAME : WIDTH_PROP_NAME;
346
+ let offset = 0, num = 0;
347
+ for (let j = collection.length - indexOffset - 1; j >= i; j--) {
348
+ const item = collection[j];
349
+ let itemSize = 0;
350
+ if (map.has(item.id)) {
351
+ const bounds = map.get(item.id);
352
+ itemSize = bounds ? bounds[sizeProperty] : typicalItemSize;
353
+ } else {
354
+ itemSize = typicalItemSize;
355
+ }
356
+ offset += itemSize;
357
+ num++;
358
+ if (offset > size) {
359
+ return { num: 0, offset };
360
+ }
361
+ }
362
+ return { num, offset };
363
+ }
364
+
365
+ /**
366
+ * Calculates list metrics
367
+ */
368
+ protected recalculateMetrics<I extends { id: Id }, C extends Array<I>>(options: IRecalculateMetricsOptions<I, C>): IMetrics {
369
+ const { fromItemId, bounds, collection, dynamicSize, isVertical, itemSize,
370
+ itemsOffset, scrollSize, snap, stickyMap, enabledBufferOptimization,
371
+ previousTotalSize, crudDetected, deletedItemsMap } = options as IRecalculateMetricsOptions<I, C> & {
372
+ stickyMap: IVirtualListStickyMap,
373
+ };
374
+
375
+ const { width, height } = bounds, sizeProperty = isVertical ? HEIGHT_PROP_NAME : WIDTH_PROP_NAME, size = isVertical ? height : width,
376
+ totalLength = collection.length, typicalItemSize = itemSize,
377
+ w = isVertical ? width : typicalItemSize, h = isVertical ? typicalItemSize : height,
378
+ map = this._map, snapshot = this._snapshot,
379
+ checkOverscrollItemsLimit = Math.ceil(size / typicalItemSize),
380
+ snippedPos = Math.floor(scrollSize),
381
+ leftItemsWeights: Array<number> = [],
382
+ isFromId = fromItemId !== undefined && (typeof fromItemId === 'number' && fromItemId > -1)
383
+ || (typeof fromItemId === 'string' && fromItemId > '-1');
384
+
385
+ let leftItemsOffset = 0, rightItemsOffset = 0;
386
+ if (enabledBufferOptimization) {
387
+ switch (this.scrollDirection) {
388
+ case 1: {
389
+ leftItemsOffset = 0;
390
+ rightItemsOffset = itemsOffset;
391
+ break;
392
+ }
393
+ case -1: {
394
+ leftItemsOffset = itemsOffset;
395
+ rightItemsOffset = 0;
396
+ break;
397
+ }
398
+ case 0:
399
+ default: {
400
+ leftItemsOffset = rightItemsOffset = itemsOffset;
401
+ }
402
+ }
403
+ } else {
404
+ leftItemsOffset = rightItemsOffset = itemsOffset;
405
+ }
406
+
407
+ let itemsFromStartToScrollEnd: number = -1, itemsFromDisplayEndToOffsetEnd = 0, itemsFromStartToDisplayEnd = -1,
408
+ leftItemLength = 0, rightItemLength = 0,
409
+ leftItemsWeight = 0, rightItemsWeight = 0,
410
+ leftHiddenItemsWeight = 0,
411
+ totalItemsToDisplayEndWeight = 0,
412
+ leftSizeOfAddedItems = 0,
413
+ leftSizeOfUpdatedItems = 0,
414
+ leftSizeOfDeletedItems = 0,
415
+ itemById: I | undefined = undefined,
416
+ itemByIdPos: number = 0,
417
+ targetDisplayItemIndex: number = -1,
418
+ isTargetInOverscroll: boolean = false,
419
+ actualScrollSize = itemByIdPos,
420
+ totalSize = 0,
421
+ startIndex;
422
+
423
+ // If the list is dynamic or there are new elements in the collection, then it switches to the long algorithm.
424
+ if (dynamicSize) {
425
+ let y = 0, stickyCollectionItem: I | undefined = undefined, stickyComponentSize = 0;
426
+ for (let i = 0, l = collection.length; i < l; i++) {
427
+ const ii = i + 1, collectionItem = collection[i], id = collectionItem.id;
428
+
429
+ let componentSize = 0, componentSizeDelta = 0, itemDisplayMethod: ItemDisplayMethods = ItemDisplayMethods.NOT_CHANGED;
430
+ if (map.has(id)) {
431
+ const bounds = map.get(id) || { width: typicalItemSize, height: typicalItemSize };
432
+ componentSize = bounds[sizeProperty];
433
+ itemDisplayMethod = bounds?.method ?? ItemDisplayMethods.UPDATE;
434
+ switch (itemDisplayMethod) {
435
+ case ItemDisplayMethods.UPDATE: {
436
+ const snapshotBounds = snapshot.get(id);
437
+ const componentSnapshotSize = componentSize - (snapshotBounds ? snapshotBounds[sizeProperty] : typicalItemSize);
438
+ componentSizeDelta = componentSnapshotSize;
439
+ map.set(id, { ...bounds, method: ItemDisplayMethods.NOT_CHANGED });
440
+ break;
441
+ }
442
+ case ItemDisplayMethods.CREATE: {
443
+ componentSizeDelta = typicalItemSize;
444
+ map.set(id, { ...bounds, method: ItemDisplayMethods.NOT_CHANGED });
445
+ break;
446
+ }
447
+ }
448
+ }
449
+
450
+ if (deletedItemsMap.hasOwnProperty(i)) {
451
+ const bounds = deletedItemsMap[i], size = bounds[sizeProperty] ?? typicalItemSize;
452
+ if (y < scrollSize - size) {
453
+ leftSizeOfDeletedItems += size;
454
+ }
455
+ }
456
+
457
+ totalSize += componentSize;
458
+
459
+ if (isFromId) {
460
+ if (itemById === undefined) {
461
+ if (id !== fromItemId && stickyMap && stickyMap[id] > 0) {
462
+ stickyComponentSize = componentSize;
463
+ stickyCollectionItem = collectionItem;
464
+ }
465
+
466
+ if (id === fromItemId) {
467
+ targetDisplayItemIndex = i;
468
+ if (stickyCollectionItem && stickyMap) {
469
+ const { num } = this.getElementNumToEnd(i, collection, map, typicalItemSize, size, isVertical);
470
+ if (num > 0) {
471
+ isTargetInOverscroll = true;
472
+ y -= size - componentSize;
473
+ } else {
474
+ if (stickyMap && !stickyMap[collectionItem.id] && y >= scrollSize && y < scrollSize + stickyComponentSize) {
475
+ const snappedY = scrollSize - stickyComponentSize;
476
+ leftHiddenItemsWeight -= (snappedY - y);
477
+ y = snappedY;
478
+ } else {
479
+ y -= stickyComponentSize;
480
+ leftHiddenItemsWeight -= stickyComponentSize;
481
+ }
482
+ }
483
+ }
484
+ itemById = collectionItem;
485
+ itemByIdPos = y;
486
+ } else {
487
+ leftItemsWeights.push(componentSize);
488
+ leftHiddenItemsWeight += componentSize;
489
+ itemsFromStartToScrollEnd = ii;
490
+ }
491
+ }
492
+ } else if (y <= scrollSize - componentSize) {
493
+ leftItemsWeights.push(componentSize);
494
+ leftHiddenItemsWeight += componentSize;
495
+ itemsFromStartToScrollEnd = ii;
496
+ }
497
+
498
+ if (isFromId) {
499
+ if (itemById === undefined || y < itemByIdPos + size + componentSize) {
500
+ itemsFromStartToDisplayEnd = ii;
501
+ totalItemsToDisplayEndWeight += componentSize;
502
+ itemsFromDisplayEndToOffsetEnd = itemsFromStartToDisplayEnd + rightItemsOffset;
503
+ }
504
+ } else if (y <= scrollSize + size + componentSize) {
505
+ itemsFromStartToDisplayEnd = ii;
506
+ totalItemsToDisplayEndWeight += componentSize;
507
+ itemsFromDisplayEndToOffsetEnd = itemsFromStartToDisplayEnd + rightItemsOffset;
508
+
509
+ if (y <= scrollSize - componentSize) {
510
+ switch (itemDisplayMethod) {
511
+ case ItemDisplayMethods.CREATE: {
512
+ leftSizeOfAddedItems += componentSizeDelta;
513
+ break;
514
+ }
515
+ case ItemDisplayMethods.UPDATE: {
516
+ leftSizeOfUpdatedItems += componentSizeDelta;
517
+ break;
518
+ }
519
+ case ItemDisplayMethods.DELETE: {
520
+ leftSizeOfDeletedItems += componentSizeDelta;
521
+ break;
522
+ }
523
+ }
524
+ }
525
+ } else {
526
+ if (i < itemsFromDisplayEndToOffsetEnd) {
527
+ rightItemsWeight += componentSize;
528
+ }
529
+ }
530
+
531
+ y += componentSize;
532
+ }
533
+
534
+ if (isTargetInOverscroll) {
535
+ const { num } = this.getElementNumToEnd(
536
+ collection.length - (checkOverscrollItemsLimit < 0 ? 0 : collection.length - checkOverscrollItemsLimit),
537
+ collection, map, typicalItemSize, size, isVertical, collection.length - (collection.length - (targetDisplayItemIndex + 1)),
538
+ );
539
+ if (num > 0) {
540
+ itemsFromStartToScrollEnd -= num;
541
+ }
542
+ }
543
+
544
+ if (itemsFromStartToScrollEnd <= -1) {
545
+ itemsFromStartToScrollEnd = 0;
546
+ }
547
+ if (itemsFromStartToDisplayEnd <= -1) {
548
+ itemsFromStartToDisplayEnd = 0;
549
+ }
550
+ actualScrollSize = isFromId ? itemByIdPos : scrollSize;
551
+
552
+ leftItemsWeights.splice(0, leftItemsWeights.length - leftItemsOffset);
553
+ leftItemsWeights.forEach(v => {
554
+ leftItemsWeight += v;
555
+ });
556
+
557
+ leftItemLength = Math.min(itemsFromStartToScrollEnd, leftItemsOffset);
558
+ rightItemLength = itemsFromStartToDisplayEnd + rightItemsOffset > totalLength
559
+ ? totalLength - itemsFromStartToDisplayEnd : rightItemsOffset;
560
+
561
+ } else
562
+ // Buffer optimization does not work on fast linear algorithm
563
+ {
564
+ if (crudDetected) {
565
+ let y = 0;
566
+ for (let i = 0, l = collection.length; i < l; i++) {
567
+ const collectionItem = collection[i], id = collectionItem.id;
568
+ let componentSize = typicalItemSize, itemDisplayMethod: ItemDisplayMethods = ItemDisplayMethods.NOT_CHANGED;
569
+ if (map.has(id)) {
570
+ const bounds = map.get(id)!;
571
+ itemDisplayMethod = bounds?.method ?? ItemDisplayMethods.UPDATE;
572
+ if (itemDisplayMethod === ItemDisplayMethods.CREATE) {
573
+ map.set(id, { ...bounds, method: ItemDisplayMethods.NOT_CHANGED });
574
+ }
575
+ }
576
+
577
+ if (deletedItemsMap.hasOwnProperty(i)) {
578
+ const bounds = deletedItemsMap[i], size = bounds[sizeProperty] ?? typicalItemSize;
579
+ if (y < scrollSize - size) {
580
+ leftSizeOfDeletedItems += size;
581
+ }
582
+ }
583
+
584
+ if (y < scrollSize - componentSize) {
585
+ switch (itemDisplayMethod) {
586
+ case ItemDisplayMethods.CREATE: {
587
+ leftSizeOfUpdatedItems += componentSize;
588
+ break;
589
+ }
590
+ case ItemDisplayMethods.UPDATE: {
591
+ leftSizeOfUpdatedItems += componentSize;
592
+ break;
593
+ }
594
+ case ItemDisplayMethods.DELETE: {
595
+ leftSizeOfDeletedItems += componentSize;
596
+ break;
597
+ }
598
+ }
599
+ }
600
+ y += componentSize;
601
+ }
602
+ }
603
+ itemsFromStartToScrollEnd = Math.floor(scrollSize / typicalItemSize);
604
+ itemsFromStartToDisplayEnd = Math.ceil((scrollSize + size) / typicalItemSize);
605
+ leftItemLength = Math.min(itemsFromStartToScrollEnd, itemsOffset);
606
+ rightItemLength = itemsFromStartToDisplayEnd + itemsOffset > totalLength
607
+ ? totalLength - itemsFromStartToDisplayEnd : itemsOffset;
608
+ leftItemsWeight = leftItemLength * typicalItemSize;
609
+ rightItemsWeight = rightItemLength * typicalItemSize;
610
+ leftHiddenItemsWeight = itemsFromStartToScrollEnd * typicalItemSize;
611
+ totalItemsToDisplayEndWeight = itemsFromStartToDisplayEnd * typicalItemSize;
612
+ totalSize = totalLength * typicalItemSize;
613
+
614
+ const k = totalSize !== 0 ? previousTotalSize / totalSize : 0;
615
+ actualScrollSize = scrollSize * k;
616
+ }
617
+ startIndex = Math.min(itemsFromStartToScrollEnd - leftItemLength, totalLength > 0 ? totalLength - 1 : 0);
618
+
619
+ const itemsOnDisplay = totalItemsToDisplayEndWeight - leftHiddenItemsWeight,
620
+ itemsOnDisplayLength = itemsFromStartToDisplayEnd - itemsFromStartToScrollEnd,
621
+ startPosition = leftHiddenItemsWeight - leftItemsWeight,
622
+ renderItems = itemsOnDisplayLength + leftItemLength + rightItemLength,
623
+ delta = leftSizeOfUpdatedItems + leftSizeOfAddedItems - leftSizeOfDeletedItems;
624
+
625
+ const metrics: IMetrics = {
626
+ delta,
627
+ normalizedItemWidth: w,
628
+ normalizedItemHeight: h,
629
+ width,
630
+ height,
631
+ dynamicSize,
632
+ itemSize,
633
+ itemsFromStartToScrollEnd,
634
+ itemsFromStartToDisplayEnd,
635
+ itemsOnDisplay,
636
+ itemsOnDisplayLength,
637
+ isVertical,
638
+ leftHiddenItemsWeight,
639
+ leftItemLength,
640
+ leftItemsWeight,
641
+ renderItems,
642
+ rightItemLength,
643
+ rightItemsWeight,
644
+ scrollSize: actualScrollSize,
645
+ leftSizeOfAddedItems,
646
+ sizeProperty,
647
+ snap,
648
+ snippedPos,
649
+ startIndex,
650
+ startPosition,
651
+ totalItemsToDisplayEndWeight,
652
+ totalLength,
653
+ totalSize,
654
+ typicalItemSize,
655
+ };
656
+
657
+ return metrics;
658
+ }
659
+
660
+ clearDeltaDirection() {
661
+ this.clearScrollDirectionCache();
662
+ }
663
+
664
+ clearDelta(clearDirectionDetector = false): void {
665
+ this._delta = 0;
666
+
667
+ if (clearDirectionDetector) {
668
+ this.clearScrollDirectionCache();
669
+ }
670
+ }
671
+
672
+ changes(): void {
673
+ this.bumpVersion();
674
+ }
675
+
676
+ protected generateDisplayCollection<I extends { id: Id }, C extends Array<I>>(items: C, stickyMap: IVirtualListStickyMap,
677
+ metrics: IMetrics): IRenderVirtualListCollection {
678
+ const {
679
+ normalizedItemWidth,
680
+ normalizedItemHeight,
681
+ dynamicSize,
682
+ itemsFromStartToScrollEnd,
683
+ isVertical,
684
+ renderItems: renderItemsLength,
685
+ scrollSize,
686
+ sizeProperty,
687
+ snap,
688
+ snippedPos,
689
+ startPosition,
690
+ totalLength,
691
+ startIndex,
692
+ typicalItemSize,
693
+ } = metrics,
694
+ displayItems: IRenderVirtualListCollection = [];
695
+ if (items.length) {
696
+ const actualSnippedPosition = snippedPos;
697
+ let pos = startPosition,
698
+ renderItems = renderItemsLength,
699
+ stickyItem: IRenderVirtualListItem | undefined, nextSticky: IRenderVirtualListItem | undefined, stickyItemIndex = -1,
700
+ stickyItemSize = 0;
701
+
702
+ if (snap) {
703
+ for (let i = Math.min(itemsFromStartToScrollEnd > 0 ? itemsFromStartToScrollEnd : 0, totalLength - 1); i >= 0; i--) {
704
+ const id = items[i].id, sticky = stickyMap[id], size = dynamicSize ? this.get(id)?.[sizeProperty] || typicalItemSize : typicalItemSize;
705
+ if (sticky > 0) {
706
+ const measures = {
707
+ x: isVertical ? 0 : actualSnippedPosition,
708
+ y: isVertical ? actualSnippedPosition : 0,
709
+ width: normalizedItemWidth,
710
+ height: normalizedItemHeight,
711
+ }, config = {
712
+ isVertical,
713
+ sticky,
714
+ snap,
715
+ snapped: true,
716
+ snappedOut: false,
717
+ dynamic: dynamicSize,
718
+ };
719
+
720
+ const itemData: I = items[i];
721
+
722
+ stickyItem = { id, measures, data: itemData, config };
723
+ stickyItemIndex = i;
724
+ stickyItemSize = size;
725
+
726
+ displayItems.push(stickyItem);
727
+ break;
728
+ }
729
+ }
730
+ }
731
+
732
+ let i = startIndex;
733
+
734
+ while (renderItems > 0) {
735
+ if (i >= totalLength) {
736
+ break;
737
+ }
738
+
739
+ const id = items[i].id, size = dynamicSize ? this.get(id)?.[sizeProperty] || typicalItemSize : typicalItemSize;
740
+
741
+ if (id !== stickyItem?.id) {
742
+ const snapped = snap && stickyMap[id] > 0 && pos <= scrollSize,
743
+ measures = {
744
+ x: isVertical ? 0 : pos,
745
+ y: isVertical ? pos : 0,
746
+ width: normalizedItemWidth,
747
+ height: normalizedItemHeight,
748
+ }, config = {
749
+ isVertical,
750
+ sticky: stickyMap[id],
751
+ snap,
752
+ snapped: false,
753
+ snappedOut: false,
754
+ dynamic: dynamicSize,
755
+ };
756
+
757
+ const itemData: I = items[i];
758
+
759
+ const item: IRenderVirtualListItem = { id, measures, data: itemData, config };
760
+ if (!nextSticky && stickyItemIndex < i && stickyMap[id] > 0 && pos <= scrollSize + size + stickyItemSize) {
761
+ item.measures.x = isVertical ? 0 : snapped ? actualSnippedPosition : pos;
762
+ item.measures.y = isVertical ? snapped ? actualSnippedPosition : pos : 0;
763
+ nextSticky = item;
764
+ nextSticky.config.snapped = snapped;
765
+ }
766
+
767
+ displayItems.push(item);
768
+ }
769
+
770
+ renderItems -= 1;
771
+ pos += size;
772
+ i++;
773
+ }
774
+
775
+ const axis = isVertical ? Y_PROP_NAME : X_PROP_NAME;
776
+
777
+ if (nextSticky && stickyItem && nextSticky.measures[axis] <= scrollSize + stickyItemSize) {
778
+ if (nextSticky.measures[axis] > scrollSize) {
779
+ stickyItem.measures[axis] = nextSticky.measures[axis] - stickyItemSize;
780
+ stickyItem.config.snapped = nextSticky.config.snapped = false;
781
+ stickyItem.config.snappedOut = true;
782
+ stickyItem.config.sticky = 1;
783
+ } else {
784
+ nextSticky.config.snapped = true;
785
+ }
786
+ }
787
+ }
788
+ return displayItems;
789
+ }
790
+
791
+ /**
792
+ * tracking by propName
793
+ */
794
+ track(): void {
795
+ if (!this._items || !this._displayComponents) {
796
+ return;
797
+ }
798
+
799
+ this._tracker.track(this._items, this._displayComponents, this.scrollDirection);
800
+ }
801
+
802
+ setDisplayObjectIndexMapById(v: { [id: number]: number }): void {
803
+ this._tracker.displayObjectIndexMapById = v;
804
+ }
805
+
806
+ untrackComponentByIdProperty(component?: NgVirtualListItemComponent | undefined) {
807
+ this._tracker.untrackComponentByIdProperty(component);
808
+ }
809
+
810
+ getItemBounds(id: Id): ISize | undefined {
811
+ if (this.has(id)) {
812
+ return this.get(id);
813
+ }
814
+ return undefined;
815
+ }
816
+
817
+ protected cacheElements(): void {
818
+ if (!this._displayComponents) {
819
+ return;
820
+ }
821
+
822
+ for (let i = 0, l = this._displayComponents.length; i < l; i++) {
823
+ const component = this._displayComponents[i], itemId = component.instance.itemId;
824
+ if (itemId === undefined) {
825
+ continue;
826
+ }
827
+ const bounds = component.instance.getBounds();
828
+ this.set(itemId, bounds);
829
+ }
830
+ }
831
+
832
+ override dispose() {
833
+ super.dispose();
834
+
835
+ if (this._tracker) {
836
+ this._tracker.dispose();
837
+ }
838
+ }
839
+ }