ng-virtual-list 19.1.19 → 19.1.21

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.
package/README.md CHANGED
@@ -451,6 +451,7 @@ Inputs
451
451
  | snapToItem | boolean? = false | Determines whether scroll positions will be snapped to the element. Default value is "false". |
452
452
  | direction | [Direction? = 'vertical'](https://github.com/DjonnyX/ng-virtual-list/blob/19.x/projects/ng-virtual-list/src/lib/enums/direction.ts) | Determines the direction in which elements are placed. Default value is "vertical". |
453
453
  | dynamicSize | boolean? = false | If true then the items in the list can have different sizes and the itemSize property is ignored. If false then the items in the list have a fixed size specified by the itemSize property. The default value is false. |
454
+ | enabledBufferOptimization | boolean? = true | Enables buffer optimization. Can only be used if items in the collection are not added or updated. |
454
455
 
455
456
  <br/>
456
457
 
@@ -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 { BehaviorSubject, filter, map, tap, combineLatest, distinctUntilChanged, switchMap, of } from 'rxjs';
6
+ import { BehaviorSubject, tap, filter, map, combineLatest, distinctUntilChanged, switchMap, of } from 'rxjs';
7
7
 
8
8
  /**
9
9
  * Axis of the arrangement of virtual list elements.
@@ -27,6 +27,7 @@ const DEFAULT_ITEM_SIZE = 24;
27
27
  const DEFAULT_ITEMS_OFFSET = 2;
28
28
  const DEFAULT_LIST_SIZE = 400;
29
29
  const DEFAULT_SNAP = false;
30
+ const DEFAULT_ENABLED_BUFFER_OPTIMIZATION = true;
30
31
  const DEFAULT_SNAP_TO_ITEM = false;
31
32
  const DEFAULT_DYNAMIC_SIZE = false;
32
33
  const TRACK_BY_PROPERTY_NAME = 'id';
@@ -406,7 +407,7 @@ class EventEmitter {
406
407
  }
407
408
  }
408
409
 
409
- const MAX_SCROLL_DIRECTION_POOL = 10, CLEAR_SCROLL_DIRECTION_TO = 0;
410
+ const MAX_SCROLL_DIRECTION_POOL = 8, CLEAR_SCROLL_DIRECTION_TO = 0;
410
411
  /**
411
412
  * Cache map.
412
413
  * Emits a change event on each mutation.
@@ -417,7 +418,7 @@ const MAX_SCROLL_DIRECTION_POOL = 10, CLEAR_SCROLL_DIRECTION_TO = 0;
417
418
  class CacheMap extends EventEmitter {
418
419
  _map = new Map();
419
420
  _version = 0;
420
- _previouseFullHeigh = 0;
421
+ _previouseFullSize = 0;
421
422
  _delta = 0;
422
423
  get delta() {
423
424
  return this._delta;
@@ -431,7 +432,7 @@ class CacheMap extends EventEmitter {
431
432
  return this._deltaDirection;
432
433
  }
433
434
  _scrollDirectionCache = [];
434
- _scrollDirection = 1;
435
+ _scrollDirection = -1;
435
436
  get scrollDirection() {
436
437
  return this._scrollDirection;
437
438
  }
@@ -499,11 +500,17 @@ class CacheMap extends EventEmitter {
499
500
 
500
501
  /**
501
502
  * Returns the removed or updated elements of a collection.
503
+ * @link https://github.com/DjonnyX/ng-virtual-list/blob/19.x/projects/ng-virtual-list/src/lib/utils/collection.ts
504
+ * @author Evgenii Grebennikov
505
+ * @email djonnyx@gmail.com
502
506
  */
503
507
  const getCollectionRemovedOrUpdatedItems = (previousCollection, currentCollection) => {
504
- const result = new Array;
505
- if (!currentCollection || currentCollection.length === 0 || !previousCollection || previousCollection.length === 0) {
506
- return (previousCollection ? [...previousCollection] : []);
508
+ const result = { deletedOrUpdated: new Array(), added: new Array(), notChanged: new Array() };
509
+ if (!currentCollection || currentCollection.length === 0) {
510
+ return { deletedOrUpdated: (previousCollection ? [...previousCollection] : []), added: [], notChanged: [] };
511
+ }
512
+ if (!previousCollection || previousCollection.length === 0) {
513
+ return { deletedOrUpdated: [], added: (currentCollection ? [...currentCollection] : []), notChanged: [] };
507
514
  }
508
515
  const collectionDict = {};
509
516
  for (let i = 0, l = currentCollection.length; i < l; i++) {
@@ -512,15 +519,25 @@ const getCollectionRemovedOrUpdatedItems = (previousCollection, currentCollectio
512
519
  collectionDict[item.id] = item;
513
520
  }
514
521
  }
522
+ const notChangedMap = {}, deletedOrUpdatedMap = {};
515
523
  for (let i = 0, l = previousCollection.length; i < l; i++) {
516
524
  const item = previousCollection[i], id = item.id;
517
525
  if (item) {
518
526
  if (collectionDict.hasOwnProperty(id)) {
519
527
  if (item === collectionDict[id]) {
528
+ result.notChanged.push(item);
529
+ notChangedMap[item.id] = item;
520
530
  continue;
521
531
  }
522
532
  }
523
- result.push(item);
533
+ result.deletedOrUpdated.push(item);
534
+ deletedOrUpdatedMap[item.id] = item;
535
+ }
536
+ }
537
+ for (let i = 0, l = currentCollection.length; i < l; i++) {
538
+ const item = currentCollection[i];
539
+ if (item && !deletedOrUpdatedMap.hasOwnProperty(item.id) && !notChangedMap.hasOwnProperty(item.id)) {
540
+ result.added.push(item);
524
541
  }
525
542
  }
526
543
  return result;
@@ -549,6 +566,7 @@ class TrackBox extends CacheMap {
549
566
  }
550
567
  this._displayComponents = v;
551
568
  }
569
+ enabledBufferOptimization = false;
552
570
  constructor(trackingPropertyName) {
553
571
  super();
554
572
  this._tracker = new Tracker(trackingPropertyName);
@@ -565,6 +583,8 @@ class TrackBox extends CacheMap {
565
583
  _fireChanges = (version) => {
566
584
  this.dispatch(TRACK_BOX_CHANGE_EVENT_NAME, version);
567
585
  };
586
+ _isExistAddedItems = false;
587
+ _addedItemsMap = {};
568
588
  _previousCollection;
569
589
  _debounceChanges = debounce(this._fireChanges, 0);
570
590
  fireChange() {
@@ -578,10 +598,20 @@ class TrackBox extends CacheMap {
578
598
  console.warn('Attention! The collection must be immutable.');
579
599
  return;
580
600
  }
581
- const removedOrUpdatedItems = getCollectionRemovedOrUpdatedItems(this._previousCollection, currentCollection);
582
- this.clearCache(removedOrUpdatedItems);
601
+ const { deletedOrUpdated, added } = getCollectionRemovedOrUpdatedItems(this._previousCollection, currentCollection);
602
+ this.clearCache(deletedOrUpdated);
603
+ this.startScrollDeltaCalculationIfNeed(added);
583
604
  this._previousCollection = currentCollection;
584
605
  }
606
+ startScrollDeltaCalculationIfNeed(added) {
607
+ if (added.length > 0) {
608
+ this._isExistAddedItems = true;
609
+ }
610
+ for (let i = 0, l = added.length; i < l; i++) {
611
+ const item = added[i];
612
+ this._addedItemsMap[item.id] = item;
613
+ }
614
+ }
585
615
  /**
586
616
  * Clears the cache of items from the list
587
617
  */
@@ -675,10 +705,11 @@ class TrackBox extends CacheMap {
675
705
  */
676
706
  recalculateMetrics(options) {
677
707
  const { fromItemId, bounds, collection, dynamicSize, isVertical, itemSize, itemsOffset, scrollSize, snap, stickyMap } = options;
678
- const { width, height } = bounds, sizeProperty = isVertical ? HEIGHT_PROP_NAME : WIDTH_PROP_NAME, size = isVertical ? height : width, totalLength = collection.length, typicalItemSize = itemSize, w = isVertical ? width : typicalItemSize, h = isVertical ? typicalItemSize : height, map = this._map, checkOverscrollItemsLimit = Math.ceil(size / typicalItemSize), snippedPos = Math.floor(scrollSize), leftItemsWeights = [], isFromId = fromItemId !== undefined && (typeof fromItemId === 'number' && fromItemId > -1)
708
+ const { width, height } = bounds, sizeProperty = isVertical ? HEIGHT_PROP_NAME : WIDTH_PROP_NAME, size = isVertical ? height : width, totalLength = collection.length, typicalItemSize = itemSize, w = isVertical ? width : typicalItemSize, h = isVertical ? typicalItemSize : height, map = this._map, leftItemsOffset = this.enabledBufferOptimization ? this.deltaDirection === 1 ? DEFAULT_ITEMS_OFFSET : itemsOffset : itemsOffset, rightItemsOffset = this.enabledBufferOptimization ? this.deltaDirection === -1 ? DEFAULT_ITEMS_OFFSET : itemsOffset : itemsOffset, checkOverscrollItemsLimit = Math.ceil(size / typicalItemSize), snippedPos = Math.floor(scrollSize), leftItemsWeights = [], isFromId = fromItemId !== undefined && (typeof fromItemId === 'number' && fromItemId > -1)
679
709
  || (typeof fromItemId === 'string' && fromItemId > '-1');
680
- let itemsFromStartToScrollEnd = -1, itemsFromDisplayEndToOffsetEnd = 0, itemsFromStartToDisplayEnd = -1, leftItemLength = 0, rightItemLength = 0, leftItemsWeight = 0, rightItemsWeight = 0, leftHiddenItemsWeight = 0, totalItemsToDisplayEndWeight = 0, itemById = undefined, itemByIdPos = 0, targetDisplayItemIndex = -1, isTargetInOverscroll = false, actualScrollSize = itemByIdPos, totalSize = 0, startIndex;
681
- if (dynamicSize) {
710
+ let itemsFromStartToScrollEnd = -1, itemsFromDisplayEndToOffsetEnd = 0, itemsFromStartToDisplayEnd = -1, leftItemLength = 0, rightItemLength = 0, leftItemsWeight = 0, rightItemsWeight = 0, leftHiddenItemsWeight = 0, totalItemsToDisplayEndWeight = 0, sizeOfAddedItems = 0, itemById = undefined, itemByIdPos = 0, targetDisplayItemIndex = -1, isTargetInOverscroll = false, actualScrollSize = itemByIdPos, totalSize = 0, startIndex;
711
+ // If the list is dynamic or there are new elements in the collection, then it switches to the long algorithm.
712
+ if (dynamicSize || this._isExistAddedItems) {
682
713
  let y = 0, stickyCollectionItem = undefined, stickyComponentSize = 0;
683
714
  for (let i = 0, l = collection.length; i < l; i++) {
684
715
  const ii = i + 1, collectionItem = collection[i];
@@ -729,16 +760,26 @@ class TrackBox extends CacheMap {
729
760
  if (itemById === undefined || y < itemByIdPos + size + componentSize) {
730
761
  itemsFromStartToDisplayEnd = ii;
731
762
  totalItemsToDisplayEndWeight += componentSize;
732
- itemsFromDisplayEndToOffsetEnd = itemsFromStartToDisplayEnd + itemsOffset;
763
+ itemsFromDisplayEndToOffsetEnd = itemsFromStartToDisplayEnd + rightItemsOffset;
764
+ }
765
+ if (y > itemByIdPos + size + componentSize) {
766
+ if (this._addedItemsMap.hasOwnProperty(collectionItem.id)) {
767
+ sizeOfAddedItems += componentSize;
768
+ }
733
769
  }
734
770
  }
735
771
  else if (y < scrollSize + size + componentSize) {
736
772
  itemsFromStartToDisplayEnd = ii;
737
773
  totalItemsToDisplayEndWeight += componentSize;
738
- itemsFromDisplayEndToOffsetEnd = itemsFromStartToDisplayEnd + itemsOffset;
774
+ itemsFromDisplayEndToOffsetEnd = itemsFromStartToDisplayEnd + rightItemsOffset;
739
775
  }
740
- else if (i < itemsFromDisplayEndToOffsetEnd) {
741
- rightItemsWeight += componentSize;
776
+ else {
777
+ if (i < itemsFromDisplayEndToOffsetEnd) {
778
+ rightItemsWeight += componentSize;
779
+ }
780
+ if (this._addedItemsMap.hasOwnProperty(collectionItem.id)) {
781
+ sizeOfAddedItems += componentSize;
782
+ }
742
783
  }
743
784
  y += componentSize;
744
785
  }
@@ -755,15 +796,17 @@ class TrackBox extends CacheMap {
755
796
  itemsFromStartToDisplayEnd = 0;
756
797
  }
757
798
  actualScrollSize = isFromId ? itemByIdPos : scrollSize;
758
- leftItemsWeights.splice(0, leftItemsWeights.length - itemsOffset);
799
+ leftItemsWeights.splice(0, leftItemsWeights.length - leftItemsOffset);
759
800
  leftItemsWeights.forEach(v => {
760
801
  leftItemsWeight += v;
761
802
  });
762
- leftItemLength = Math.min(itemsFromStartToScrollEnd, itemsOffset);
763
- rightItemLength = itemsFromStartToDisplayEnd + itemsOffset > totalLength
764
- ? totalLength - itemsFromStartToDisplayEnd : itemsOffset;
803
+ leftItemLength = Math.min(itemsFromStartToScrollEnd, leftItemsOffset);
804
+ rightItemLength = itemsFromStartToDisplayEnd + rightItemsOffset > totalLength
805
+ ? totalLength - itemsFromStartToDisplayEnd : rightItemsOffset;
765
806
  }
766
- else {
807
+ else
808
+ // Buffer optimization does not work on fast linear algorithm
809
+ {
767
810
  itemsFromStartToScrollEnd = Math.floor(scrollSize / typicalItemSize);
768
811
  itemsFromStartToDisplayEnd = Math.ceil((scrollSize + size) / typicalItemSize);
769
812
  leftItemLength = Math.min(itemsFromStartToScrollEnd, itemsOffset);
@@ -777,9 +820,12 @@ class TrackBox extends CacheMap {
777
820
  totalSize = totalLength * typicalItemSize;
778
821
  }
779
822
  startIndex = Math.min(itemsFromStartToScrollEnd - leftItemLength, totalLength > 0 ? totalLength - 1 : 0);
780
- const itemsOnDisplay = totalItemsToDisplayEndWeight - leftHiddenItemsWeight, itemsOnDisplayLength = itemsFromStartToDisplayEnd - itemsFromStartToScrollEnd, startPosition = leftHiddenItemsWeight - leftItemsWeight, renderItems = itemsOnDisplayLength + leftItemLength + rightItemLength, delta = totalSize - this._previouseFullHeigh;
823
+ const itemsOnDisplay = totalItemsToDisplayEndWeight - leftHiddenItemsWeight, itemsOnDisplayLength = itemsFromStartToDisplayEnd - itemsFromStartToScrollEnd, startPosition = leftHiddenItemsWeight - leftItemsWeight, renderItems = itemsOnDisplayLength + leftItemLength + rightItemLength, delta = totalSize - this._previouseFullSize, scrollDelta = sizeOfAddedItems;
781
824
  if (this.scrollDirection === -1) {
782
- this._delta += delta;
825
+ if (this._isExistAddedItems && sizeOfAddedItems > 0) {
826
+ this.stopScrollDeltaCalculation();
827
+ }
828
+ this._delta += delta - scrollDelta;
783
829
  }
784
830
  const metrics = {
785
831
  delta: this._delta,
@@ -801,6 +847,7 @@ class TrackBox extends CacheMap {
801
847
  rightItemLength,
802
848
  rightItemsWeight,
803
849
  scrollSize: actualScrollSize,
850
+ sizeOfAddedItems,
804
851
  sizeProperty,
805
852
  snap,
806
853
  snippedPos,
@@ -811,18 +858,25 @@ class TrackBox extends CacheMap {
811
858
  totalSize,
812
859
  typicalItemSize,
813
860
  };
814
- this._previouseFullHeigh = totalSize;
861
+ this._previouseFullSize = totalSize;
815
862
  return metrics;
816
863
  }
864
+ _scrollDelta = 0;
865
+ get scrollDelta() { return this._scrollDelta; }
817
866
  clearDeltaDirection() {
818
867
  this.clearScrollDirectionCache();
819
868
  }
820
869
  clearDelta(clearDirectionDetector = false) {
821
870
  this._delta = 0;
871
+ this.stopScrollDeltaCalculation();
822
872
  if (clearDirectionDetector) {
823
873
  this.clearScrollDirectionCache();
824
874
  }
825
875
  }
876
+ stopScrollDeltaCalculation() {
877
+ this._isExistAddedItems = false;
878
+ this._addedItemsMap = {};
879
+ }
826
880
  generateDisplayCollection(items, stickyMap, metrics) {
827
881
  const {
828
882
  // delta,
@@ -940,6 +994,12 @@ class TrackBox extends CacheMap {
940
994
  untrackComponentByIdProperty(component) {
941
995
  this._tracker.untrackComponentByIdProperty(component);
942
996
  }
997
+ getItemBounds(id) {
998
+ if (this.has(id)) {
999
+ return this.get(id);
1000
+ }
1001
+ return undefined;
1002
+ }
943
1003
  cacheElements() {
944
1004
  if (!this._displayComponents) {
945
1005
  return;
@@ -982,6 +1042,12 @@ class TrackBox extends CacheMap {
982
1042
  }
983
1043
  }
984
1044
 
1045
+ /**
1046
+ * Scroll event.
1047
+ * @link https://github.com/DjonnyX/ng-virtual-list/blob/19.x/projects/ng-virtual-list/src/lib/utils/scrollEvent.ts
1048
+ * @author Evgenii Grebennikov
1049
+ * @email djonnyx@gmail.com
1050
+ */
985
1051
  class ScrollEvent {
986
1052
  _direction = 1;
987
1053
  get direction() { return this._direction; }
@@ -1001,7 +1067,10 @@ class ScrollEvent {
1001
1067
  get isEnd() { return this._isEnd; }
1002
1068
  _delta = 0;
1003
1069
  get delta() { return this._delta; }
1004
- constructor(direction, container, list, delta, isVertical) {
1070
+ _scrollDelta = 0;
1071
+ get scrollDelta() { return this._scrollDelta; }
1072
+ constructor(params) {
1073
+ const { direction, isVertical, container, list, delta, scrollDelta } = params;
1005
1074
  this._direction = direction;
1006
1075
  this._isVertical = isVertical;
1007
1076
  this._scrollSize = isVertical ? container.scrollTop : container.scrollLeft;
@@ -1010,6 +1079,7 @@ class ScrollEvent {
1010
1079
  this._size = isVertical ? container.offsetHeight : container.offsetWidth;
1011
1080
  this._isEnd = (this._scrollSize + this._size) === this._scrollWeight;
1012
1081
  this._delta = delta;
1082
+ this._scrollDelta = scrollDelta;
1013
1083
  this._isStart = this._scrollSize === 0;
1014
1084
  }
1015
1085
  }
@@ -1060,6 +1130,12 @@ class NgVirtualListComponent {
1060
1130
  * Determines whether scroll positions will be snapped to the element. Default value is "false".
1061
1131
  */
1062
1132
  snapToItem = input(DEFAULT_SNAP_TO_ITEM);
1133
+ /**
1134
+ * Enables buffer optimization.
1135
+ * Can only be used if items in the collection are not added or updated. Otherwise, artifacts in the form of twitching of the scroll area are possible.
1136
+ * Works only if the property dynamic = true
1137
+ */
1138
+ enabledBufferOptimization = input(DEFAULT_ENABLED_BUFFER_OPTIMIZATION);
1063
1139
  /**
1064
1140
  * Rendering element template.
1065
1141
  */
@@ -1108,9 +1184,8 @@ class NgVirtualListComponent {
1108
1184
  this.clearScrollToRepeatExecutionTimeout();
1109
1185
  const container = this._container()?.nativeElement;
1110
1186
  if (container) {
1111
- const dynamicSize = this.dynamicSize(), delta = this._trackBox.delta, scrollSize = (this._isVertical ? container.scrollTop : container.scrollLeft), previouseScrollSize = this._scrollSize();
1187
+ const dynamicSize = this.dynamicSize(), delta = this._trackBox.delta, scrollSize = (this._isVertical ? container.scrollTop : container.scrollLeft);
1112
1188
  let actualScrollSize = scrollSize, isImmediateScroll = false;
1113
- this._trackBox.deltaDirection = previouseScrollSize > scrollSize ? -1 : 1;
1114
1189
  if (dynamicSize && delta !== 0) {
1115
1190
  actualScrollSize = scrollSize + delta;
1116
1191
  const params = {
@@ -1120,18 +1195,11 @@ class NgVirtualListComponent {
1120
1195
  const container = this._container();
1121
1196
  if (container) {
1122
1197
  isImmediateScroll = true;
1123
- this.scrollImmediately(container, params, () => {
1124
- const event = new ScrollEvent(this._trackBox.scrollDirection, container.nativeElement, this._list().nativeElement, delta, this._isVertical);
1125
- this.onScroll.emit(event);
1126
- });
1198
+ this.scrollImmediately(container, params);
1127
1199
  this._trackBox.clearDelta();
1128
1200
  }
1129
1201
  }
1130
1202
  this._scrollSize.set(actualScrollSize);
1131
- if (!isImmediateScroll) {
1132
- const event = new ScrollEvent(this._trackBox.scrollDirection, container, this._list().nativeElement, delta, this._isVertical);
1133
- this.onScroll.emit(event);
1134
- }
1135
1203
  }
1136
1204
  };
1137
1205
  scrollImmediately(container, params, cb) {
@@ -1168,7 +1236,6 @@ class NgVirtualListComponent {
1168
1236
  this._trackBox.clearDeltaDirection();
1169
1237
  const itemSize = this.itemSize(), snapToItem = this.snapToItem(), dynamicSize = this.dynamicSize(), delta = this._trackBox.delta, scrollSize = (this._isVertical ? container.nativeElement.scrollTop : container.nativeElement.scrollLeft);
1170
1238
  let actualScrollSize = scrollSize;
1171
- const event = new ScrollEvent(this._trackBox.scrollDirection, container.nativeElement, this._list().nativeElement, delta, this._isVertical);
1172
1239
  if (dynamicSize) {
1173
1240
  actualScrollSize = scrollSize + delta;
1174
1241
  if (snapToItem) {
@@ -1199,7 +1266,6 @@ class NgVirtualListComponent {
1199
1266
  }
1200
1267
  }
1201
1268
  this._scrollSize.set(actualScrollSize);
1202
- this.onScrollEnd.emit(event);
1203
1269
  }
1204
1270
  };
1205
1271
  _elementRef = inject((ElementRef));
@@ -1221,6 +1287,10 @@ class NgVirtualListComponent {
1221
1287
  this._initialized = signal(false);
1222
1288
  this.$initialized = toObservable(this._initialized);
1223
1289
  this._trackBox.displayComponents = this._displayComponents;
1290
+ const $enabledBufferOptimization = toObservable(this.enabledBufferOptimization);
1291
+ $enabledBufferOptimization.pipe(tap(v => {
1292
+ this._trackBox.enabledBufferOptimization = v;
1293
+ })).subscribe();
1224
1294
  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;
1225
1295
  $isVertical.pipe(takeUntilDestroyed(), tap(v => {
1226
1296
  this._isVertical = v;
@@ -1315,6 +1385,12 @@ class NgVirtualListComponent {
1315
1385
  l.nativeElement.style[isVertical ? HEIGHT_PROP_NAME : WIDTH_PROP_NAME] = `${totalSize}${PX}`;
1316
1386
  }
1317
1387
  }
1388
+ /**
1389
+ * Returns the bounds of an element with a given id
1390
+ */
1391
+ getItemBounds(id) {
1392
+ return this._trackBox.getItemBounds(id);
1393
+ }
1318
1394
  /**
1319
1395
  * The method scrolls the list to the element with the given id and returns the value of the scrolled area.
1320
1396
  * Behavior accepts the values ​​"auto", "instant" and "smooth".
@@ -1364,8 +1440,6 @@ class NgVirtualListComponent {
1364
1440
  }
1365
1441
  else {
1366
1442
  this._scrollSize.set(scrollSize);
1367
- const event = new ScrollEvent(this._trackBox.scrollDirection, container.nativeElement, this._list().nativeElement, this._trackBox.delta, this._isVertical);
1368
- this.onScroll.emit(event);
1369
1443
  container.nativeElement.addEventListener(SCROLL, this._onScrollHandler);
1370
1444
  container.nativeElement.addEventListener(SCROLL_END, this._onScrollEndHandler);
1371
1445
  }
@@ -1386,9 +1460,37 @@ class NgVirtualListComponent {
1386
1460
  const items = this.items(), latItem = items[items.length > 0 ? items.length - 1 : 0];
1387
1461
  this.scrollTo(latItem.id, behavior);
1388
1462
  }
1463
+ _onContainerScrollHandler = (e) => {
1464
+ const containerEl = this._container();
1465
+ if (containerEl) {
1466
+ const scrollSize = (this._isVertical ? containerEl.nativeElement.scrollTop : containerEl.nativeElement.scrollLeft), offsetSize = (this._isVertical ? containerEl.nativeElement.offsetHeight : containerEl.nativeElement.offsetWidth), listSize = (this._isVertical ? this._list()?.nativeElement.offsetHeight ?? 0 : this._list()?.nativeElement.offsetLeft ?? 0);
1467
+ this._trackBox.deltaDirection = this._scrollSize() >= scrollSize || (scrollSize + offsetSize) >= listSize ? -1 : 1;
1468
+ const event = new ScrollEvent({
1469
+ direction: this._trackBox.scrollDirection, container: containerEl.nativeElement,
1470
+ list: this._list().nativeElement, delta: this._trackBox.delta,
1471
+ scrollDelta: this._trackBox.scrollDelta, isVertical: this._isVertical,
1472
+ });
1473
+ this.onScroll.emit(event);
1474
+ }
1475
+ };
1476
+ _onContainerScrollEndHandler = (e) => {
1477
+ this._trackBox.deltaDirection = -1;
1478
+ const containerEl = this._container();
1479
+ if (containerEl) {
1480
+ const event = new ScrollEvent({
1481
+ direction: this._trackBox.scrollDirection, container: containerEl.nativeElement,
1482
+ list: this._list().nativeElement, delta: this._trackBox.delta,
1483
+ scrollDelta: this._trackBox.scrollDelta, isVertical: this._isVertical,
1484
+ });
1485
+ this.onScrollEnd.emit(event);
1486
+ }
1487
+ };
1389
1488
  ngAfterViewInit() {
1390
1489
  const containerEl = this._container();
1391
1490
  if (containerEl) {
1491
+ // for direction calculation
1492
+ containerEl.nativeElement.addEventListener(SCROLL, this._onContainerScrollHandler);
1493
+ containerEl.nativeElement.addEventListener(SCROLL_END, this._onContainerScrollEndHandler);
1392
1494
  containerEl.nativeElement.addEventListener(SCROLL, this._onScrollHandler);
1393
1495
  containerEl.nativeElement.addEventListener(SCROLL_END, this._onScrollEndHandler);
1394
1496
  this._resizeObserver = new ResizeObserver(this._onResizeHandler);
@@ -1405,6 +1507,8 @@ class NgVirtualListComponent {
1405
1507
  if (containerEl) {
1406
1508
  containerEl.nativeElement.removeEventListener(SCROLL, this._onScrollHandler);
1407
1509
  containerEl.nativeElement.removeEventListener(SCROLL_END, this._onScrollEndHandler);
1510
+ containerEl.nativeElement.removeEventListener(SCROLL, this._onContainerScrollHandler);
1511
+ containerEl.nativeElement.removeEventListener(SCROLL_END, this._onContainerScrollEndHandler);
1408
1512
  if (this._resizeObserver) {
1409
1513
  this._resizeObserver.unobserve(containerEl.nativeElement);
1410
1514
  }
@@ -1417,7 +1521,7 @@ class NgVirtualListComponent {
1417
1521
  }
1418
1522
  }
1419
1523
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NgVirtualListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1420
- 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 });
1524
+ 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 }, enabledBufferOptimization: { classPropertyName: "enabledBufferOptimization", publicName: "enabledBufferOptimization", 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 });
1421
1525
  }
1422
1526
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NgVirtualListComponent, decorators: [{
1423
1527
  type: Component,