ng-virtual-list 19.1.20 → 19.1.22

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,8 @@ 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. |
455
+ | likeAChat | boolean? = false | If true, optimization for lists that start from the end is enabled. |
454
456
 
455
457
  <br/>
456
458
 
@@ -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,8 @@ 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;
31
+ const DEFAULT_OPTIMIZE_FOR_END = false;
30
32
  const DEFAULT_SNAP_TO_ITEM = false;
31
33
  const DEFAULT_DYNAMIC_SIZE = false;
32
34
  const TRACK_BY_PROPERTY_NAME = 'id';
@@ -406,7 +408,7 @@ class EventEmitter {
406
408
  }
407
409
  }
408
410
 
409
- const MAX_SCROLL_DIRECTION_POOL = 10, CLEAR_SCROLL_DIRECTION_TO = 0;
411
+ const MAX_SCROLL_DIRECTION_POOL = 8, CLEAR_SCROLL_DIRECTION_TO = 0;
410
412
  /**
411
413
  * Cache map.
412
414
  * Emits a change event on each mutation.
@@ -417,7 +419,7 @@ const MAX_SCROLL_DIRECTION_POOL = 10, CLEAR_SCROLL_DIRECTION_TO = 0;
417
419
  class CacheMap extends EventEmitter {
418
420
  _map = new Map();
419
421
  _version = 0;
420
- _previouseFullHeigh = 0;
422
+ _previouseFullSize = 0;
421
423
  _delta = 0;
422
424
  get delta() {
423
425
  return this._delta;
@@ -430,6 +432,17 @@ class CacheMap extends EventEmitter {
430
432
  get deltaDirection() {
431
433
  return this._deltaDirection;
432
434
  }
435
+ _likeAChat = false;
436
+ set likeAChat(v) {
437
+ if (this._likeAChat === v) {
438
+ return;
439
+ }
440
+ if (v) {
441
+ this._scrollDirection = -1;
442
+ }
443
+ this._scrollDirectionCache = [];
444
+ this._likeAChat = v;
445
+ }
433
446
  _scrollDirectionCache = [];
434
447
  _scrollDirection = 1;
435
448
  get scrollDirection() {
@@ -499,11 +512,17 @@ class CacheMap extends EventEmitter {
499
512
 
500
513
  /**
501
514
  * Returns the removed or updated elements of a collection.
515
+ * @link https://github.com/DjonnyX/ng-virtual-list/blob/19.x/projects/ng-virtual-list/src/lib/utils/collection.ts
516
+ * @author Evgenii Grebennikov
517
+ * @email djonnyx@gmail.com
502
518
  */
503
519
  const getCollectionRemovedOrUpdatedItems = (previousCollection, currentCollection) => {
504
- const result = new Array();
505
- if (!currentCollection || currentCollection.length === 0 || !previousCollection || previousCollection.length === 0) {
506
- return (previousCollection ? [...previousCollection] : []);
520
+ const result = { deletedOrUpdated: new Array(), added: new Array(), notChanged: new Array() };
521
+ if (!currentCollection || currentCollection.length === 0) {
522
+ return { deletedOrUpdated: (previousCollection ? [...previousCollection] : []), added: [], notChanged: [] };
523
+ }
524
+ if (!previousCollection || previousCollection.length === 0) {
525
+ return { deletedOrUpdated: [], added: (currentCollection ? [...currentCollection] : []), notChanged: [] };
507
526
  }
508
527
  const collectionDict = {};
509
528
  for (let i = 0, l = currentCollection.length; i < l; i++) {
@@ -512,15 +531,25 @@ const getCollectionRemovedOrUpdatedItems = (previousCollection, currentCollectio
512
531
  collectionDict[item.id] = item;
513
532
  }
514
533
  }
534
+ const notChangedMap = {}, deletedOrUpdatedMap = {};
515
535
  for (let i = 0, l = previousCollection.length; i < l; i++) {
516
536
  const item = previousCollection[i], id = item.id;
517
537
  if (item) {
518
538
  if (collectionDict.hasOwnProperty(id)) {
519
539
  if (item === collectionDict[id]) {
540
+ result.notChanged.push(item);
541
+ notChangedMap[item.id] = item;
520
542
  continue;
521
543
  }
522
544
  }
523
- result.push(item);
545
+ result.deletedOrUpdated.push(item);
546
+ deletedOrUpdatedMap[item.id] = item;
547
+ }
548
+ }
549
+ for (let i = 0, l = currentCollection.length; i < l; i++) {
550
+ const item = currentCollection[i];
551
+ if (item && !deletedOrUpdatedMap.hasOwnProperty(item.id) && !notChangedMap.hasOwnProperty(item.id)) {
552
+ result.added.push(item);
524
553
  }
525
554
  }
526
555
  return result;
@@ -549,6 +578,7 @@ class TrackBox extends CacheMap {
549
578
  }
550
579
  this._displayComponents = v;
551
580
  }
581
+ enabledBufferOptimization = false;
552
582
  constructor(trackingPropertyName) {
553
583
  super();
554
584
  this._tracker = new Tracker(trackingPropertyName);
@@ -565,6 +595,8 @@ class TrackBox extends CacheMap {
565
595
  _fireChanges = (version) => {
566
596
  this.dispatch(TRACK_BOX_CHANGE_EVENT_NAME, version);
567
597
  };
598
+ _isExistAddedItems = false;
599
+ _addedItemsMap = {};
568
600
  _previousCollection;
569
601
  _debounceChanges = debounce(this._fireChanges, 0);
570
602
  fireChange() {
@@ -578,10 +610,20 @@ class TrackBox extends CacheMap {
578
610
  console.warn('Attention! The collection must be immutable.');
579
611
  return;
580
612
  }
581
- const removedOrUpdatedItems = getCollectionRemovedOrUpdatedItems(this._previousCollection, currentCollection);
582
- this.clearCache(removedOrUpdatedItems);
613
+ const { deletedOrUpdated, added } = getCollectionRemovedOrUpdatedItems(this._previousCollection, currentCollection);
614
+ this.clearCache(deletedOrUpdated);
615
+ this.startScrollDeltaCalculationIfNeed(added);
583
616
  this._previousCollection = currentCollection;
584
617
  }
618
+ startScrollDeltaCalculationIfNeed(added) {
619
+ if (added.length > 0) {
620
+ this._isExistAddedItems = true;
621
+ }
622
+ for (let i = 0, l = added.length; i < l; i++) {
623
+ const item = added[i];
624
+ this._addedItemsMap[item.id] = item;
625
+ }
626
+ }
585
627
  /**
586
628
  * Clears the cache of items from the list
587
629
  */
@@ -675,10 +717,11 @@ class TrackBox extends CacheMap {
675
717
  */
676
718
  recalculateMetrics(options) {
677
719
  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)
720
+ 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
721
  || (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) {
722
+ 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;
723
+ // If the list is dynamic or there are new elements in the collection, then it switches to the long algorithm.
724
+ if (dynamicSize || this._isExistAddedItems) {
682
725
  let y = 0, stickyCollectionItem = undefined, stickyComponentSize = 0;
683
726
  for (let i = 0, l = collection.length; i < l; i++) {
684
727
  const ii = i + 1, collectionItem = collection[i];
@@ -729,16 +772,26 @@ class TrackBox extends CacheMap {
729
772
  if (itemById === undefined || y < itemByIdPos + size + componentSize) {
730
773
  itemsFromStartToDisplayEnd = ii;
731
774
  totalItemsToDisplayEndWeight += componentSize;
732
- itemsFromDisplayEndToOffsetEnd = itemsFromStartToDisplayEnd + itemsOffset;
775
+ itemsFromDisplayEndToOffsetEnd = itemsFromStartToDisplayEnd + rightItemsOffset;
776
+ }
777
+ if (y > itemByIdPos + size + componentSize) {
778
+ if (this._addedItemsMap.hasOwnProperty(collectionItem.id)) {
779
+ sizeOfAddedItems += componentSize;
780
+ }
733
781
  }
734
782
  }
735
783
  else if (y < scrollSize + size + componentSize) {
736
784
  itemsFromStartToDisplayEnd = ii;
737
785
  totalItemsToDisplayEndWeight += componentSize;
738
- itemsFromDisplayEndToOffsetEnd = itemsFromStartToDisplayEnd + itemsOffset;
786
+ itemsFromDisplayEndToOffsetEnd = itemsFromStartToDisplayEnd + rightItemsOffset;
739
787
  }
740
- else if (i < itemsFromDisplayEndToOffsetEnd) {
741
- rightItemsWeight += componentSize;
788
+ else {
789
+ if (i < itemsFromDisplayEndToOffsetEnd) {
790
+ rightItemsWeight += componentSize;
791
+ }
792
+ if (this._addedItemsMap.hasOwnProperty(collectionItem.id)) {
793
+ sizeOfAddedItems += componentSize;
794
+ }
742
795
  }
743
796
  y += componentSize;
744
797
  }
@@ -755,15 +808,17 @@ class TrackBox extends CacheMap {
755
808
  itemsFromStartToDisplayEnd = 0;
756
809
  }
757
810
  actualScrollSize = isFromId ? itemByIdPos : scrollSize;
758
- leftItemsWeights.splice(0, leftItemsWeights.length - itemsOffset);
811
+ leftItemsWeights.splice(0, leftItemsWeights.length - leftItemsOffset);
759
812
  leftItemsWeights.forEach(v => {
760
813
  leftItemsWeight += v;
761
814
  });
762
- leftItemLength = Math.min(itemsFromStartToScrollEnd, itemsOffset);
763
- rightItemLength = itemsFromStartToDisplayEnd + itemsOffset > totalLength
764
- ? totalLength - itemsFromStartToDisplayEnd : itemsOffset;
815
+ leftItemLength = Math.min(itemsFromStartToScrollEnd, leftItemsOffset);
816
+ rightItemLength = itemsFromStartToDisplayEnd + rightItemsOffset > totalLength
817
+ ? totalLength - itemsFromStartToDisplayEnd : rightItemsOffset;
765
818
  }
766
- else {
819
+ else
820
+ // Buffer optimization does not work on fast linear algorithm
821
+ {
767
822
  itemsFromStartToScrollEnd = Math.floor(scrollSize / typicalItemSize);
768
823
  itemsFromStartToDisplayEnd = Math.ceil((scrollSize + size) / typicalItemSize);
769
824
  leftItemLength = Math.min(itemsFromStartToScrollEnd, itemsOffset);
@@ -777,9 +832,12 @@ class TrackBox extends CacheMap {
777
832
  totalSize = totalLength * typicalItemSize;
778
833
  }
779
834
  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;
835
+ const itemsOnDisplay = totalItemsToDisplayEndWeight - leftHiddenItemsWeight, itemsOnDisplayLength = itemsFromStartToDisplayEnd - itemsFromStartToScrollEnd, startPosition = leftHiddenItemsWeight - leftItemsWeight, renderItems = itemsOnDisplayLength + leftItemLength + rightItemLength, delta = totalSize - this._previouseFullSize, scrollDelta = sizeOfAddedItems;
781
836
  if (this.scrollDirection === -1) {
782
- this._delta += delta;
837
+ if (this._isExistAddedItems && sizeOfAddedItems > 0) {
838
+ this.stopScrollDeltaCalculation();
839
+ }
840
+ this._delta += delta - scrollDelta;
783
841
  }
784
842
  const metrics = {
785
843
  delta: this._delta,
@@ -801,6 +859,7 @@ class TrackBox extends CacheMap {
801
859
  rightItemLength,
802
860
  rightItemsWeight,
803
861
  scrollSize: actualScrollSize,
862
+ sizeOfAddedItems,
804
863
  sizeProperty,
805
864
  snap,
806
865
  snippedPos,
@@ -811,18 +870,25 @@ class TrackBox extends CacheMap {
811
870
  totalSize,
812
871
  typicalItemSize,
813
872
  };
814
- this._previouseFullHeigh = totalSize;
873
+ this._previouseFullSize = totalSize;
815
874
  return metrics;
816
875
  }
876
+ _scrollDelta = 0;
877
+ get scrollDelta() { return this._scrollDelta; }
817
878
  clearDeltaDirection() {
818
879
  this.clearScrollDirectionCache();
819
880
  }
820
881
  clearDelta(clearDirectionDetector = false) {
821
882
  this._delta = 0;
883
+ this.stopScrollDeltaCalculation();
822
884
  if (clearDirectionDetector) {
823
885
  this.clearScrollDirectionCache();
824
886
  }
825
887
  }
888
+ stopScrollDeltaCalculation() {
889
+ this._isExistAddedItems = false;
890
+ this._addedItemsMap = {};
891
+ }
826
892
  generateDisplayCollection(items, stickyMap, metrics) {
827
893
  const {
828
894
  // delta,
@@ -940,6 +1006,12 @@ class TrackBox extends CacheMap {
940
1006
  untrackComponentByIdProperty(component) {
941
1007
  this._tracker.untrackComponentByIdProperty(component);
942
1008
  }
1009
+ getItemBounds(id) {
1010
+ if (this.has(id)) {
1011
+ return this.get(id);
1012
+ }
1013
+ return undefined;
1014
+ }
943
1015
  cacheElements() {
944
1016
  if (!this._displayComponents) {
945
1017
  return;
@@ -982,6 +1054,12 @@ class TrackBox extends CacheMap {
982
1054
  }
983
1055
  }
984
1056
 
1057
+ /**
1058
+ * Scroll event.
1059
+ * @link https://github.com/DjonnyX/ng-virtual-list/blob/19.x/projects/ng-virtual-list/src/lib/utils/scrollEvent.ts
1060
+ * @author Evgenii Grebennikov
1061
+ * @email djonnyx@gmail.com
1062
+ */
985
1063
  class ScrollEvent {
986
1064
  _direction = 1;
987
1065
  get direction() { return this._direction; }
@@ -1001,7 +1079,10 @@ class ScrollEvent {
1001
1079
  get isEnd() { return this._isEnd; }
1002
1080
  _delta = 0;
1003
1081
  get delta() { return this._delta; }
1004
- constructor(direction, container, list, delta, isVertical) {
1082
+ _scrollDelta = 0;
1083
+ get scrollDelta() { return this._scrollDelta; }
1084
+ constructor(params) {
1085
+ const { direction, isVertical, container, list, delta, scrollDelta } = params;
1005
1086
  this._direction = direction;
1006
1087
  this._isVertical = isVertical;
1007
1088
  this._scrollSize = isVertical ? container.scrollTop : container.scrollLeft;
@@ -1010,6 +1091,7 @@ class ScrollEvent {
1010
1091
  this._size = isVertical ? container.offsetHeight : container.offsetWidth;
1011
1092
  this._isEnd = (this._scrollSize + this._size) === this._scrollWeight;
1012
1093
  this._delta = delta;
1094
+ this._scrollDelta = scrollDelta;
1013
1095
  this._isStart = this._scrollSize === 0;
1014
1096
  }
1015
1097
  }
@@ -1060,6 +1142,16 @@ class NgVirtualListComponent {
1060
1142
  * Determines whether scroll positions will be snapped to the element. Default value is "false".
1061
1143
  */
1062
1144
  snapToItem = input(DEFAULT_SNAP_TO_ITEM);
1145
+ /**
1146
+ * Enables buffer optimization.
1147
+ * 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.
1148
+ * Works only if the property dynamic = true
1149
+ */
1150
+ enabledBufferOptimization = input(DEFAULT_ENABLED_BUFFER_OPTIMIZATION);
1151
+ /**
1152
+ * If true, optimization for lists that start from the end is enabled (chat mode enabled).
1153
+ */
1154
+ likeAChat = input(DEFAULT_OPTIMIZE_FOR_END);
1063
1155
  /**
1064
1156
  * Rendering element template.
1065
1157
  */
@@ -1108,9 +1200,8 @@ class NgVirtualListComponent {
1108
1200
  this.clearScrollToRepeatExecutionTimeout();
1109
1201
  const container = this._container()?.nativeElement;
1110
1202
  if (container) {
1111
- const dynamicSize = this.dynamicSize(), delta = this._trackBox.delta, scrollSize = (this._isVertical ? container.scrollTop : container.scrollLeft), previouseScrollSize = this._scrollSize();
1203
+ const dynamicSize = this.dynamicSize(), delta = this._trackBox.delta, scrollSize = (this._isVertical ? container.scrollTop : container.scrollLeft);
1112
1204
  let actualScrollSize = scrollSize, isImmediateScroll = false;
1113
- this._trackBox.deltaDirection = previouseScrollSize > scrollSize ? -1 : 1;
1114
1205
  if (dynamicSize && delta !== 0) {
1115
1206
  actualScrollSize = scrollSize + delta;
1116
1207
  const params = {
@@ -1120,18 +1211,11 @@ class NgVirtualListComponent {
1120
1211
  const container = this._container();
1121
1212
  if (container) {
1122
1213
  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
- });
1214
+ this.scrollImmediately(container, params);
1127
1215
  this._trackBox.clearDelta();
1128
1216
  }
1129
1217
  }
1130
1218
  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
1219
  }
1136
1220
  };
1137
1221
  scrollImmediately(container, params, cb) {
@@ -1168,7 +1252,6 @@ class NgVirtualListComponent {
1168
1252
  this._trackBox.clearDeltaDirection();
1169
1253
  const itemSize = this.itemSize(), snapToItem = this.snapToItem(), dynamicSize = this.dynamicSize(), delta = this._trackBox.delta, scrollSize = (this._isVertical ? container.nativeElement.scrollTop : container.nativeElement.scrollLeft);
1170
1254
  let actualScrollSize = scrollSize;
1171
- const event = new ScrollEvent(this._trackBox.scrollDirection, container.nativeElement, this._list().nativeElement, delta, this._isVertical);
1172
1255
  if (dynamicSize) {
1173
1256
  actualScrollSize = scrollSize + delta;
1174
1257
  if (snapToItem) {
@@ -1199,7 +1282,6 @@ class NgVirtualListComponent {
1199
1282
  }
1200
1283
  }
1201
1284
  this._scrollSize.set(actualScrollSize);
1202
- this.onScrollEnd.emit(event);
1203
1285
  }
1204
1286
  };
1205
1287
  _elementRef = inject((ElementRef));
@@ -1221,6 +1303,14 @@ class NgVirtualListComponent {
1221
1303
  this._initialized = signal(false);
1222
1304
  this.$initialized = toObservable(this._initialized);
1223
1305
  this._trackBox.displayComponents = this._displayComponents;
1306
+ const $enabledBufferOptimization = toObservable(this.enabledBufferOptimization);
1307
+ $enabledBufferOptimization.pipe(takeUntilDestroyed(), tap(v => {
1308
+ this._trackBox.enabledBufferOptimization = v;
1309
+ })).subscribe();
1310
+ const $likeAChat = toObservable(this.likeAChat);
1311
+ $likeAChat.pipe(takeUntilDestroyed(), tap(v => {
1312
+ this._trackBox.likeAChat = v;
1313
+ })).subscribe();
1224
1314
  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
1315
  $isVertical.pipe(takeUntilDestroyed(), tap(v => {
1226
1316
  this._isVertical = v;
@@ -1315,6 +1405,12 @@ class NgVirtualListComponent {
1315
1405
  l.nativeElement.style[isVertical ? HEIGHT_PROP_NAME : WIDTH_PROP_NAME] = `${totalSize}${PX}`;
1316
1406
  }
1317
1407
  }
1408
+ /**
1409
+ * Returns the bounds of an element with a given id
1410
+ */
1411
+ getItemBounds(id) {
1412
+ return this._trackBox.getItemBounds(id);
1413
+ }
1318
1414
  /**
1319
1415
  * The method scrolls the list to the element with the given id and returns the value of the scrolled area.
1320
1416
  * Behavior accepts the values ​​"auto", "instant" and "smooth".
@@ -1364,8 +1460,6 @@ class NgVirtualListComponent {
1364
1460
  }
1365
1461
  else {
1366
1462
  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
1463
  container.nativeElement.addEventListener(SCROLL, this._onScrollHandler);
1370
1464
  container.nativeElement.addEventListener(SCROLL_END, this._onScrollEndHandler);
1371
1465
  }
@@ -1386,9 +1480,37 @@ class NgVirtualListComponent {
1386
1480
  const items = this.items(), latItem = items[items.length > 0 ? items.length - 1 : 0];
1387
1481
  this.scrollTo(latItem.id, behavior);
1388
1482
  }
1483
+ _onContainerScrollHandler = (e) => {
1484
+ const containerEl = this._container();
1485
+ if (containerEl) {
1486
+ 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);
1487
+ this._trackBox.deltaDirection = this._scrollSize() >= scrollSize ? -1 : this.likeAChat() && (scrollSize + offsetSize) >= listSize ? -1 : 1;
1488
+ const event = new ScrollEvent({
1489
+ direction: this._trackBox.scrollDirection, container: containerEl.nativeElement,
1490
+ list: this._list().nativeElement, delta: this._trackBox.delta,
1491
+ scrollDelta: this._trackBox.scrollDelta, isVertical: this._isVertical,
1492
+ });
1493
+ this.onScroll.emit(event);
1494
+ }
1495
+ };
1496
+ _onContainerScrollEndHandler = (e) => {
1497
+ this._trackBox.deltaDirection = this.likeAChat() ? -1 : 1;
1498
+ const containerEl = this._container();
1499
+ if (containerEl) {
1500
+ const event = new ScrollEvent({
1501
+ direction: this._trackBox.scrollDirection, container: containerEl.nativeElement,
1502
+ list: this._list().nativeElement, delta: this._trackBox.delta,
1503
+ scrollDelta: this._trackBox.scrollDelta, isVertical: this._isVertical,
1504
+ });
1505
+ this.onScrollEnd.emit(event);
1506
+ }
1507
+ };
1389
1508
  ngAfterViewInit() {
1390
1509
  const containerEl = this._container();
1391
1510
  if (containerEl) {
1511
+ // for direction calculation
1512
+ containerEl.nativeElement.addEventListener(SCROLL, this._onContainerScrollHandler);
1513
+ containerEl.nativeElement.addEventListener(SCROLL_END, this._onContainerScrollEndHandler);
1392
1514
  containerEl.nativeElement.addEventListener(SCROLL, this._onScrollHandler);
1393
1515
  containerEl.nativeElement.addEventListener(SCROLL_END, this._onScrollEndHandler);
1394
1516
  this._resizeObserver = new ResizeObserver(this._onResizeHandler);
@@ -1405,6 +1527,8 @@ class NgVirtualListComponent {
1405
1527
  if (containerEl) {
1406
1528
  containerEl.nativeElement.removeEventListener(SCROLL, this._onScrollHandler);
1407
1529
  containerEl.nativeElement.removeEventListener(SCROLL_END, this._onScrollEndHandler);
1530
+ containerEl.nativeElement.removeEventListener(SCROLL, this._onContainerScrollHandler);
1531
+ containerEl.nativeElement.removeEventListener(SCROLL_END, this._onContainerScrollEndHandler);
1408
1532
  if (this._resizeObserver) {
1409
1533
  this._resizeObserver.unobserve(containerEl.nativeElement);
1410
1534
  }
@@ -1417,7 +1541,7 @@ class NgVirtualListComponent {
1417
1541
  }
1418
1542
  }
1419
1543
  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 });
1544
+ 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 }, likeAChat: { classPropertyName: "likeAChat", publicName: "likeAChat", 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
1545
  }
1422
1546
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NgVirtualListComponent, decorators: [{
1423
1547
  type: Component,