ng-virtual-list
Version:
Maximum performance for extremely large lists.<br/> Animation of elements is supported.
2,056 lines • 94.1 kB
JavaScript
import * as i1 from '@angular/common';
import { CommonModule } from '@angular/common';
import * as i0 from '@angular/core';
import { signal, inject, ElementRef, viewChild, ChangeDetectionStrategy, Component, output, input, ViewContainerRef, ViewChild, ViewEncapsulation } from '@angular/core';
import { toObservable, takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { tap, filter, map, combineLatest, distinctUntilChanged, switchMap, of } from 'rxjs';
/**
* Axis of the arrangement of virtual list elements.
* @link https://github.com/DjonnyX/ng-virtual-list/blob/20.x/projects/ng-virtual-list/src/lib/enums/directions.ts
* @author Evgenii Grebennikov
* @email djonnyx@gmail.com
*/
var Directions;
(function (Directions) {
/**
* Horizontal axis.
*/
Directions["HORIZONTAL"] = "horizontal";
/**
* Vertical axis.
*/
Directions["VERTICAL"] = "vertical";
})(Directions || (Directions = {}));
/**
* Snapping method.
* @link https://github.com/DjonnyX/ng-virtual-list/blob/20.x/projects/ng-virtual-list/src/lib/enums/snapping-method.ts
* @author Evgenii Grebennikov
* @email djonnyx@gmail.com
*/
var SnappingMethods;
(function (SnappingMethods) {
/**
* Normal group rendering.
*/
SnappingMethods["NORMAL"] = "normal";
/**
* The group is rendered on a transparent background. List items below the group are not rendered.
*/
SnappingMethods["ADVANCED"] = "advanced";
})(SnappingMethods || (SnappingMethods = {}));
const DEFAULT_ITEM_SIZE = 24;
const DEFAULT_BUFFER_SIZE = 2;
const DEFAULT_MAX_BUFFER_SIZE = 100;
const DEFAULT_LIST_SIZE = 400;
const DEFAULT_SNAP = false;
const DEFAULT_ENABLED_BUFFER_OPTIMIZATION = false;
const DEFAULT_DYNAMIC_SIZE = false;
const TRACK_BY_PROPERTY_NAME = 'id';
const DEFAULT_DIRECTION = Directions.VERTICAL;
const DISPLAY_OBJECTS_LENGTH_MESUREMENT_ERROR = 1;
const MAX_SCROLL_TO_ITERATIONS = 5;
const DEFAULT_SNAPPING_METHOD = SnappingMethods.NORMAL;
// presets
const BEHAVIOR_AUTO = 'auto';
const BEHAVIOR_INSTANT = 'instant';
const BEHAVIOR_SMOOTH = 'smooth';
const DISPLAY_BLOCK = 'block';
const DISPLAY_NONE = 'none';
const OPACITY_0 = '0';
const OPACITY_100 = '100';
const VISIBILITY_VISIBLE = 'visible';
const VISIBILITY_HIDDEN = 'hidden';
const SIZE_100_PERSENT = '100%';
const SIZE_AUTO = 'auto';
const POSITION_ABSOLUTE = 'absolute';
const POSITION_STICKY = 'sticky';
const TRANSLATE_3D = 'translate3d';
const ZEROS_TRANSLATE_3D = `${TRANSLATE_3D}(0,0,0)`;
const HIDDEN_ZINDEX = '-1';
const DEFAULT_ZINDEX = '0';
const TOP_PROP_NAME = 'top';
const LEFT_PROP_NAME = 'left';
const X_PROP_NAME = 'x';
const Y_PROP_NAME = 'y';
const WIDTH_PROP_NAME = 'width';
const HEIGHT_PROP_NAME = 'height';
const PX = 'px';
const SCROLL = 'scroll';
const SCROLL_END = 'scrollend';
const CLASS_LIST_VERTICAL = 'vertical';
const CLASS_LIST_HORIZONTAL = 'horizontal';
/**
* Virtual List Item Interface
* @link https://github.com/DjonnyX/ng-virtual-list/blob/19.x/projects/ng-virtual-list/src/lib/models/base-virtual-list-item-component.ts
* @author Evgenii Grebennikov
* @email djonnyx@gmail.com
*/
class BaseVirtualListItemComponent {
}
/**
* Virtual list item component
* @link https://github.com/DjonnyX/ng-virtual-list/blob/20.x/projects/ng-virtual-list/src/lib/components/ng-virtual-list-item.component.ts
* @author Evgenii Grebennikov
* @email djonnyx@gmail.com
*/
class NgVirtualListItemComponent extends BaseVirtualListItemComponent {
static __nextId = 0;
_id;
get id() {
return this._id;
}
regular = false;
data = signal(undefined);
_data = undefined;
set item(v) {
if (this._data === v) {
return;
}
const data = this._data = v;
this.update();
this.data.set(v);
}
_regularLength = SIZE_100_PERSENT;
set regularLength(v) {
if (this._regularLength === v) {
return;
}
this._regularLength = v;
this.update();
}
get item() {
return this._data;
}
get itemId() {
return this._data?.id;
}
itemRenderer = signal(undefined);
set renderer(v) {
this.itemRenderer.set(v);
}
_elementRef = inject((ElementRef));
get element() {
return this._elementRef.nativeElement;
}
_listItemRef = viewChild('listItem');
constructor() {
super();
this._id = NgVirtualListItemComponent.__nextId = NgVirtualListItemComponent.__nextId === Number.MAX_SAFE_INTEGER
? 0 : NgVirtualListItemComponent.__nextId + 1;
}
update() {
const data = this._data, regular = this.regular, length = this._regularLength;
if (data) {
const styles = this._elementRef.nativeElement.style;
styles.zIndex = data.config.zIndex;
if (data.config.snapped) {
styles.transform = data.config.sticky === 1 ? ZEROS_TRANSLATE_3D : `${TRANSLATE_3D}(${data.config.isVertical ? 0 : data.measures.x}${PX}, ${data.config.isVertical ? data.measures.y : 0}${PX} , 0)`;
;
if (!data.config.isSnappingMethodAdvanced) {
styles.position = POSITION_STICKY;
}
}
else {
styles.position = POSITION_ABSOLUTE;
if (regular) {
styles.transform = `${TRANSLATE_3D}(${data.config.isVertical ? 0 : data.measures.delta}${PX}, ${data.config.isVertical ? data.measures.delta : 0}${PX} , 0)`;
}
else {
styles.transform = `${TRANSLATE_3D}(${data.config.isVertical ? 0 : data.measures.x}${PX}, ${data.config.isVertical ? data.measures.y : 0}${PX} , 0)`;
}
}
styles.height = data.config.isVertical ? data.config.dynamic ? SIZE_AUTO : `${data.measures.height}${PX}` : regular ? length : SIZE_100_PERSENT;
styles.width = data.config.isVertical ? regular ? length : SIZE_100_PERSENT : data.config.dynamic ? SIZE_AUTO : `${data.measures.width}${PX}`;
}
}
getBounds() {
const el = this._elementRef.nativeElement, { width, height } = el.getBoundingClientRect();
return { width, height };
}
show() {
const styles = this._elementRef.nativeElement.style;
if (this.regular) {
if (styles.display === DISPLAY_BLOCK) {
return;
}
styles.display = DISPLAY_BLOCK;
}
else {
if (styles.visibility === VISIBILITY_VISIBLE) {
return;
}
styles.visibility = VISIBILITY_VISIBLE;
}
styles.zIndex = this._data?.config?.zIndex ?? DEFAULT_ZINDEX;
}
hide() {
const styles = this._elementRef.nativeElement.style;
if (this.regular) {
if (styles.display === DISPLAY_NONE) {
return;
}
styles.display = DISPLAY_NONE;
}
else {
if (styles.visibility === VISIBILITY_HIDDEN) {
return;
}
styles.visibility = VISIBILITY_HIDDEN;
}
styles.position = POSITION_ABSOLUTE;
styles.transform = ZEROS_TRANSLATE_3D;
styles.zIndex = HIDDEN_ZINDEX;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: NgVirtualListItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.4", type: NgVirtualListItemComponent, isStandalone: true, selector: "ng-virtual-list-item", host: { classAttribute: "ngvl__item" }, viewQueries: [{ propertyName: "_listItemRef", first: true, predicate: ["listItem"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "@let item = data();\r\n@let renderer = itemRenderer();\r\n\r\n@if (item) {\r\n <li #listItem part=\"item\" class=\"ngvl-item__container\" [ngClass]=\"{'snapped': item.config.snapped,\r\n 'snapped-out': item.config.snappedOut}\">\r\n @if (renderer) {\r\n <ng-container [ngTemplateOutlet]=\"renderer\"\r\n [ngTemplateOutletContext]=\"{data: item.data || {}, config: item.config}\" />\r\n }\r\n </li>\r\n}", styles: [":host{display:block;position:absolute;left:0;top:0;box-sizing:border-box;overflow:hidden}.ngvl-item__container{margin:0;padding:0;overflow:hidden;background-color:#fff;width:inherit;height:inherit}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: NgVirtualListItemComponent, decorators: [{
type: Component,
args: [{ selector: 'ng-virtual-list-item', imports: [CommonModule], host: {
'class': 'ngvl__item',
}, changeDetection: ChangeDetectionStrategy.OnPush, template: "@let item = data();\r\n@let renderer = itemRenderer();\r\n\r\n@if (item) {\r\n <li #listItem part=\"item\" class=\"ngvl-item__container\" [ngClass]=\"{'snapped': item.config.snapped,\r\n 'snapped-out': item.config.snappedOut}\">\r\n @if (renderer) {\r\n <ng-container [ngTemplateOutlet]=\"renderer\"\r\n [ngTemplateOutletContext]=\"{data: item.data || {}, config: item.config}\" />\r\n }\r\n </li>\r\n}", styles: [":host{display:block;position:absolute;left:0;top:0;box-sizing:border-box;overflow:hidden}.ngvl-item__container{margin:0;padding:0;overflow:hidden;background-color:#fff;width:inherit;height:inherit}\n"] }]
}], ctorParameters: () => [] });
/**
* Simple debounce function.
* @link https://github.com/DjonnyX/ng-virtual-list/blob/20.x/projects/ng-virtual-list/src/lib/utils/debounce.ts
* @author Evgenii Grebennikov
* @email djonnyx@gmail.com
*/
const debounce = (cb, debounceTime = 0) => {
let timeout;
const dispose = () => {
if (timeout !== undefined) {
clearTimeout(timeout);
}
};
const execute = (...args) => {
dispose();
timeout = setTimeout(() => {
cb(...args);
}, debounceTime);
};
return {
/**
* Call handling method
*/
execute,
/**
* Method of destroying handlers
*/
dispose,
};
};
/**
* Switch css classes
* @link https://github.com/DjonnyX/ng-virtual-list/blob/20.x/projects/ng-virtual-list/src/lib/utils/toggleClassName.ts
* @author Evgenii Grebennikov
* @email djonnyx@gmail.com
*/
const toggleClassName = (el, className, removeClassName) => {
if (!el.classList.contains(className)) {
el.classList.add(className);
}
if (removeClassName) {
el.classList.remove(removeClassName);
}
};
/**
* Scroll event.
* @link https://github.com/DjonnyX/ng-virtual-list/blob/20.x/projects/ng-virtual-list/src/lib/utils/scrollEvent.ts
* @author Evgenii Grebennikov
* @email djonnyx@gmail.com
*/
class ScrollEvent {
_direction = 1;
get direction() { return this._direction; }
_scrollSize = 0;
get scrollSize() { return this._scrollSize; }
_scrollWeight = 0;
get scrollWeight() { return this._scrollWeight; }
_isVertical = true;
get isVertical() { return this._isVertical; }
_listSize = 0;
get listSize() { return this._listSize; }
_size = 0;
get size() { return this._size; }
_isStart = true;
get isStart() { return this._isStart; }
_isEnd = false;
get isEnd() { return this._isEnd; }
_delta = 0;
get delta() { return this._delta; }
_scrollDelta = 0;
get scrollDelta() { return this._scrollDelta; }
constructor(params) {
const { direction, isVertical, container, list, delta, scrollDelta } = params;
this._direction = direction;
this._isVertical = isVertical;
this._scrollSize = isVertical ? container.scrollTop : container.scrollLeft;
this._scrollWeight = isVertical ? container.scrollHeight : container.scrollWidth;
this._listSize = isVertical ? list.offsetHeight : list.offsetWidth;
this._size = isVertical ? container.offsetHeight : container.offsetWidth;
this._isEnd = (this._scrollSize + this._size) === this._scrollWeight;
this._delta = delta;
this._scrollDelta = scrollDelta;
this._isStart = this._scrollSize === 0;
}
}
/**
* Simple event emitter
* @link https://github.com/DjonnyX/ng-virtual-list/blob/20.x/projects/ng-virtual-list/src/lib/utils/eventEmitter.ts
* @author Evgenii Grebennikov
* @email djonnyx@gmail.com
*/
class EventEmitter {
_listeners = {};
_disposed = false;
constructor() { }
/**
* Emits the event
*/
dispatch(event, ...args) {
const ctx = this;
const listeners = this._listeners[event];
if (Array.isArray(listeners)) {
for (let i = 0, l = listeners.length; i < l; i++) {
const listener = listeners[i];
if (listener) {
listener.apply(ctx, args);
}
}
}
}
/**
* Emits the event async
*/
dispatchAsync(event, ...args) {
queueMicrotask(() => {
if (this._disposed) {
return;
}
this.dispatch(event, ...args);
});
}
/**
* Returns true if the event listener is already subscribed.
*/
hasEventListener(eventName, handler) {
const event = eventName;
if (this._listeners.hasOwnProperty(event)) {
const listeners = this._listeners[event];
const index = listeners.findIndex(v => v === handler);
if (index > -1) {
return true;
}
}
return false;
}
/**
* Add event listener
*/
addEventListener(eventName, handler) {
const event = eventName;
if (!this._listeners.hasOwnProperty(event)) {
this._listeners[event] = [];
}
this._listeners[event].push(handler);
}
/**
* Remove event listener
*/
removeEventListener(eventName, handler) {
const event = eventName;
if (!this._listeners.hasOwnProperty(event)) {
return;
}
const listeners = this._listeners[event], index = listeners.findIndex(v => v === handler);
if (index > -1) {
listeners.splice(index, 1);
if (listeners.length === 0) {
delete this._listeners[event];
}
}
}
/**
* Remove all listeners
*/
removeAllListeners() {
const events = Object.keys(this._listeners);
while (events.length > 0) {
const event = events.pop();
if (event) {
const listeners = this._listeners[event];
if (Array.isArray(listeners)) {
while (listeners.length > 0) {
const listener = listeners.pop();
if (listener) {
this.removeEventListener(event, listener);
}
}
}
}
}
}
/**
* Method of destroying handlers
*/
dispose() {
this._disposed = true;
this.removeAllListeners();
}
}
class CMap {
_dict = {};
constructor(dict) {
if (dict) {
this._dict = { ...dict._dict };
}
}
get(key) {
const k = String(key);
return this._dict[k];
}
set(key, value) {
const k = String(key);
this._dict[k] = value;
return this;
}
has(key) {
return this._dict.hasOwnProperty(String(key));
}
delete(key) {
const k = String(key);
delete this._dict[k];
}
clear() {
this._dict = {};
}
}
const CACHE_BOX_CHANGE_EVENT_NAME = 'change';
const MAX_SCROLL_DIRECTION_POOL = 50, CLEAR_SCROLL_DIRECTION_TO = 10, DIR_BACK = '-1', DIR_NONE = '0', DIR_FORWARD = '1';
/**
* Cache map.
* Emits a change event on each mutation.
* @link https://github.com/DjonnyX/ng-virtual-list/blob/20.x/projects/ng-virtual-list/src/lib/utils/cacheMap.ts
* @author Evgenii Grebennikov
* @email djonnyx@gmail.com
*/
class CacheMap extends EventEmitter {
_map = new CMap();
_snapshot = new CMap();
_version = 0;
_previousVersion = this._version;
_lifeCircleTimeout;
_delta = 0;
get delta() {
return this._delta;
}
_deltaDirection = 0;
set deltaDirection(v) {
this._deltaDirection = v;
this._scrollDirection = this.calcScrollDirection(v);
}
get deltaDirection() {
return this._deltaDirection;
}
_scrollDirectionCache = [];
_scrollDirection = 0;
get scrollDirection() {
return this._scrollDirection;
}
get version() {
return this._version;
}
_clearScrollDirectionDebounce = debounce(() => {
while (this._scrollDirectionCache.length > CLEAR_SCROLL_DIRECTION_TO) {
this._scrollDirectionCache.shift();
}
}, 10);
constructor() {
super();
this.lifeCircle();
}
changesDetected() {
return this._version !== this._previousVersion;
}
stopLifeCircle() {
clearTimeout(this._lifeCircleTimeout);
}
nextTick(cb) {
if (this._disposed) {
return;
}
this._lifeCircleTimeout = setTimeout(() => {
cb();
});
return this._lifeCircleTimeout;
}
lifeCircle() {
this.fireChangeIfNeed();
this.lifeCircleDo();
}
lifeCircleDo() {
this._previousVersion = this._version;
this.nextTick(() => {
this.lifeCircle();
});
}
clearScrollDirectionCache() {
this._clearScrollDirectionDebounce.execute();
}
calcScrollDirection(v) {
while (this._scrollDirectionCache.length >= MAX_SCROLL_DIRECTION_POOL) {
this._scrollDirectionCache.shift();
}
this._scrollDirectionCache.push(v);
const dict = { [DIR_BACK]: 0, [DIR_NONE]: 0, [DIR_FORWARD]: 0 };
for (let i = 0, l = this._scrollDirectionCache.length, li = l - 1; i < l; i++) {
const dir = String(this._scrollDirectionCache[i]);
dict[dir] += 1;
if (i === li) {
for (let d in dict) {
if (d === String(v)) {
continue;
}
dict[d] -= 1;
}
}
}
if (dict[DIR_BACK] > dict[DIR_NONE] && dict[DIR_BACK] > dict[DIR_FORWARD]) {
return -1;
}
else if (dict[DIR_FORWARD] > dict[DIR_BACK] && dict[DIR_FORWARD] > dict[DIR_NONE]) {
return 1;
}
return 0;
}
bumpVersion() {
if (this.changesDetected()) {
return;
}
const v = this._version === Number.MAX_SAFE_INTEGER ? 0 : this._version + 1;
this._version = v;
}
fireChangeIfNeed() {
if (this.changesDetected()) {
this.dispatch(CACHE_BOX_CHANGE_EVENT_NAME, this.version);
}
}
set(id, bounds) {
if (this._map.has(id)) {
const b = this._map.get(id), bb = bounds;
if (b.width === bb.width && b.height === bb.height) {
return this._map;
}
return this._map;
}
const v = this._map.set(id, bounds);
this.bumpVersion();
return v;
}
has(id) {
return this._map.has(id);
}
get(id) {
return this._map.get(id);
}
snapshot() {
this._snapshot = new CMap(this._map);
}
dispose() {
super.dispose();
this.stopLifeCircle();
this._snapshot.clear();
this._map.clear();
}
}
/**
* Tracks display items by property
* @link https://github.com/DjonnyX/ng-virtual-list/blob/20.x/projects/ng-virtual-list/src/lib/utils/tracker.ts
* @author Evgenii Grebennikov
* @email djonnyx@gmail.com
*/
class Tracker {
/**
* display objects dictionary of indexes by id
*/
_displayObjectIndexMapById = {};
set displayObjectIndexMapById(v) {
if (this._displayObjectIndexMapById === v) {
return;
}
this._displayObjectIndexMapById = v;
}
get displayObjectIndexMapById() {
return this._displayObjectIndexMapById;
}
/**
* Dictionary displayItems propertyNameId by items propertyNameId
*/
_trackMap = {};
get trackMap() {
return this._trackMap;
}
_trackingPropertyName;
set trackingPropertyName(v) {
this._trackingPropertyName = v;
}
constructor(trackingPropertyName) {
this._trackingPropertyName = trackingPropertyName;
}
/**
* tracking by propName
*/
track(items, components, snapedComponent, direction) {
if (!items) {
return;
}
const idPropName = this._trackingPropertyName, untrackedItems = [...components], isDown = direction === 0 || direction === 1;
let isRegularSnapped = false;
for (let i = isDown ? 0 : items.length - 1, l = isDown ? items.length : 0; isDown ? i < l : i >= l; isDown ? i++ : i--) {
const item = items[i], itemTrackingProperty = item[idPropName];
if (this._trackMap) {
if (this._trackMap.hasOwnProperty(itemTrackingProperty)) {
const diId = this._trackMap[itemTrackingProperty], compIndex = this._displayObjectIndexMapById[diId], comp = components[compIndex];
const compId = comp?.instance?.id;
if (comp !== undefined && compId === diId) {
const indexByUntrackedItems = untrackedItems.findIndex(v => {
return v.instance.id === compId;
});
if (indexByUntrackedItems > -1) {
if (snapedComponent) {
if (item['config']['snapped'] || item['config']['snappedOut']) {
isRegularSnapped = true;
snapedComponent.instance.item = item;
snapedComponent.instance.show();
}
}
comp.instance.item = item;
if (snapedComponent) {
if (item['config']['snapped'] || item['config']['snappedOut']) {
comp.instance.hide();
}
else {
comp.instance.show();
}
}
else {
comp.instance.show();
}
untrackedItems.splice(indexByUntrackedItems, 1);
continue;
}
}
delete this._trackMap[itemTrackingProperty];
}
}
if (untrackedItems.length > 0) {
const comp = untrackedItems.shift(), item = items[i];
if (comp) {
if (snapedComponent) {
if (item['config']['snapped'] || item['config']['snappedOut']) {
isRegularSnapped = true;
snapedComponent.instance.item = item;
snapedComponent.instance.show();
}
}
comp.instance.item = item;
if (snapedComponent) {
if (item['config']['snapped'] || item['config']['snappedOut']) {
comp.instance.hide();
}
else {
comp.instance.show();
}
}
else {
comp.instance.show();
}
if (this._trackMap) {
this._trackMap[itemTrackingProperty] = comp.instance.id;
}
}
}
}
if (untrackedItems.length) {
for (let i = 0, l = untrackedItems.length; i < l; i++) {
const comp = untrackedItems[i];
comp.instance.hide();
}
}
if (!isRegularSnapped) {
if (snapedComponent) {
snapedComponent.instance.item = null;
snapedComponent.instance.hide();
}
}
}
untrackComponentByIdProperty(component) {
if (!component) {
return;
}
const propertyIdName = this._trackingPropertyName;
if (this._trackMap && component[propertyIdName] !== undefined) {
delete this._trackMap[propertyIdName];
}
}
dispose() {
this._trackMap = null;
}
}
const DEFAULT_EXTRA = {
extremumThreshold: 2,
bufferSize: 10,
};
const bufferInterpolation = (currentBufferValue, array, value, extra) => {
const { extremumThreshold = DEFAULT_EXTRA.extremumThreshold, bufferSize = DEFAULT_EXTRA.bufferSize, } = extra ?? DEFAULT_EXTRA;
if (currentBufferValue < value) {
let i = 0;
while (i < extremumThreshold) {
array.push(value);
i++;
}
}
else {
array.push(value);
}
while (array.length >= bufferSize) {
array.shift();
}
const l = array.length;
let buffer = 0;
for (let i = 0; i < l; i++) {
buffer += array[i];
}
return Math.ceil(buffer / l);
};
const TRACK_BOX_CHANGE_EVENT_NAME = 'change';
var ItemDisplayMethods;
(function (ItemDisplayMethods) {
ItemDisplayMethods[ItemDisplayMethods["CREATE"] = 0] = "CREATE";
ItemDisplayMethods[ItemDisplayMethods["UPDATE"] = 1] = "UPDATE";
ItemDisplayMethods[ItemDisplayMethods["DELETE"] = 2] = "DELETE";
ItemDisplayMethods[ItemDisplayMethods["NOT_CHANGED"] = 3] = "NOT_CHANGED";
})(ItemDisplayMethods || (ItemDisplayMethods = {}));
const DEFAULT_BUFFER_EXTREMUM_THRESHOLD = 15, DEFAULT_MAX_BUFFER_SEQUENCE_LENGTH = 30, DEFAULT_RESET_BUFFER_SIZE_TIMEOUT = 10000;
/**
* An object that performs tracking, calculations and caching.
* @link https://github.com/DjonnyX/ng-virtual-list/blob/20.x/projects/ng-virtual-list/src/lib/utils/trackBox.ts
* @author Evgenii Grebennikov
* @email djonnyx@gmail.com
*/
class TrackBox extends CacheMap {
_tracker;
_items;
set items(v) {
if (this._items === v) {
return;
}
this._items = v;
}
_displayComponents;
set displayComponents(v) {
if (this._displayComponents === v) {
return;
}
this._displayComponents = v;
}
_snapedDisplayComponent;
set snapedDisplayComponent(v) {
if (this._snapedDisplayComponent === v) {
return;
}
this._snapedDisplayComponent = v;
}
_isSnappingMethodAdvanced = false;
set isSnappingMethodAdvanced(v) {
if (this._isSnappingMethodAdvanced === v) {
return;
}
this._isSnappingMethodAdvanced = v;
}
/**
* Set the trackBy property
*/
set trackingPropertyName(v) {
this._trackingPropertyName = this._tracker.trackingPropertyName = v;
}
_trackingPropertyName = TRACK_BY_PROPERTY_NAME;
constructor(trackingPropertyName) {
super();
this._trackingPropertyName = trackingPropertyName;
this.initialize();
}
initialize() {
this._tracker = new Tracker(this._trackingPropertyName);
}
set(id, bounds) {
if (this._map.has(id)) {
const b = this._map.get(id);
if (b?.width === bounds.width && b.height === bounds.height) {
return this._map;
}
}
const v = this._map.set(id, bounds);
this.bumpVersion();
return v;
}
_previousCollection;
_deletedItemsMap = {};
_crudDetected = false;
get crudDetected() { return this._crudDetected; }
fireChangeIfNeed() {
if (this.changesDetected()) {
this.dispatch(TRACK_BOX_CHANGE_EVENT_NAME, this._version);
}
}
_previousTotalSize = 0;
_scrollDelta = 0;
get scrollDelta() { return this._scrollDelta; }
isAdaptiveBuffer = true;
_bufferSequenceExtraThreshold = DEFAULT_BUFFER_EXTREMUM_THRESHOLD;
_maxBufferSequenceLength = DEFAULT_MAX_BUFFER_SEQUENCE_LENGTH;
_bufferSizeSequence = [];
_bufferSize = 0;
get bufferSize() { return this._bufferSize; }
_defaultBufferSize = 0;
_maxBufferSize = this._defaultBufferSize;
_resetBufferSizeTimeout = DEFAULT_RESET_BUFFER_SIZE_TIMEOUT;
_resetBufferSizeTimer;
lifeCircle() {
this.fireChangeIfNeed();
this.lifeCircleDo();
}
/**
* Scans the collection for deleted items and flushes the deleted item cache.
*/
resetCollection(currentCollection, itemSize) {
if (currentCollection !== undefined && currentCollection !== null && currentCollection === this._previousCollection) {
console.warn('Attention! The collection must be immutable.');
return;
}
this.updateCache(this._previousCollection, currentCollection, itemSize);
this._previousCollection = currentCollection;
}
/**
* Update the cache of items from the list
*/
updateCache(previousCollection, currentCollection, itemSize) {
let crudDetected = false;
if (!currentCollection || currentCollection.length === 0) {
if (previousCollection) {
// deleted
for (let i = 0, l = previousCollection.length; i < l; i++) {
const item = previousCollection[i], id = item.id;
crudDetected = true;
if (this._map.has(id)) {
this._map.delete(id);
}
}
}
return;
}
if (!previousCollection || previousCollection.length === 0) {
if (currentCollection) {
// added
for (let i = 0, l = currentCollection.length; i < l; i++) {
crudDetected = true;
const item = currentCollection[i], id = item.id;
this._map.set(id, { width: itemSize, height: itemSize, method: ItemDisplayMethods.CREATE });
}
}
return;
}
const collectionDict = {};
for (let i = 0, l = currentCollection.length; i < l; i++) {
const item = currentCollection[i];
if (item) {
collectionDict[item.id] = item;
}
}
const notChangedMap = {}, deletedMap = {}, deletedItemsMap = {}, updatedMap = {};
for (let i = 0, l = previousCollection.length; i < l; i++) {
const item = previousCollection[i], id = item.id;
if (item) {
if (collectionDict.hasOwnProperty(id)) {
if (item === collectionDict[id]) {
// not changed
notChangedMap[item.id] = item;
this._map.set(id, { ...(this._map.get(id) || { width: itemSize, height: itemSize }), method: ItemDisplayMethods.NOT_CHANGED });
continue;
}
else {
// updated
crudDetected = true;
updatedMap[item.id] = item;
this._map.set(id, { ...(this._map.get(id) || { width: itemSize, height: itemSize }), method: ItemDisplayMethods.UPDATE });
continue;
}
}
// deleted
crudDetected = true;
deletedMap[item.id] = item;
deletedItemsMap[i] = this._map.get(item.id);
this._map.delete(id);
}
}
for (let i = 0, l = currentCollection.length; i < l; i++) {
const item = currentCollection[i], id = item.id;
if (item && !deletedMap.hasOwnProperty(id) && !updatedMap.hasOwnProperty(id) && !notChangedMap.hasOwnProperty(id)) {
// added
crudDetected = true;
this._map.set(id, { width: itemSize, height: itemSize, method: ItemDisplayMethods.CREATE });
}
}
this._crudDetected = crudDetected;
this._deletedItemsMap = deletedItemsMap;
}
/**
* Finds the position of a collection element by the given Id
*/
getItemPosition(id, stickyMap, options) {
const opt = { fromItemId: id, stickyMap, ...options };
this._defaultBufferSize = opt.bufferSize;
this._maxBufferSize = opt.maxBufferSize;
const { scrollSize, isFromItemIdFound } = this.recalculateMetrics({
...opt,
dynamicSize: this._crudDetected || opt.dynamicSize,
previousTotalSize: this._previousTotalSize,
crudDetected: this._crudDetected,
deletedItemsMap: this._deletedItemsMap,
});
return isFromItemIdFound ? scrollSize : -1;
}
/**
* Updates the collection of display objects
*/
updateCollection(items, stickyMap, options) {
const opt = { stickyMap, ...options }, crudDetected = this._crudDetected, deletedItemsMap = this._deletedItemsMap;
if (opt.dynamicSize) {
this.cacheElements();
}
this._defaultBufferSize = opt.bufferSize;
this._maxBufferSize = opt.maxBufferSize;
const metrics = this.recalculateMetrics({
...opt,
collection: items,
previousTotalSize: this._previousTotalSize,
crudDetected: this._crudDetected,
deletedItemsMap,
});
this._delta += metrics.delta;
this.updateAdaptiveBufferParams(metrics, items.length);
this._previousTotalSize = metrics.totalSize;
this._deletedItemsMap = {};
this._crudDetected = false;
if (opt.dynamicSize) {
this.snapshot();
}
const displayItems = this.generateDisplayCollection(items, stickyMap, { ...metrics, });
return { displayItems, totalSize: metrics.totalSize, delta: metrics.delta, crudDetected };
}
/**
* Finds the closest element in the collection by scrollSize
*/
getNearestItem(scrollSize, items, itemSize, isVertical) {
return this.getElementFromStart(scrollSize, items, this._map, itemSize, isVertical);
}
_previousScrollSize = 0;
updateAdaptiveBufferParams(metrics, totalItemsLength) {
this.disposeClearBufferSizeTimer();
const scrollSize = metrics.scrollSize + this._delta, delta = Math.abs(this._previousScrollSize - scrollSize);
this._previousScrollSize = scrollSize;
const bufferRawSize = Math.min(Math.floor(delta / metrics.typicalItemSize) * 5, totalItemsLength), minBufferSize = bufferRawSize < this._defaultBufferSize ? this._defaultBufferSize : bufferRawSize, bufferValue = minBufferSize > this._maxBufferSize ? this._maxBufferSize : minBufferSize;
this._bufferSize = bufferInterpolation(this._bufferSize, this._bufferSizeSequence, bufferValue, {
extremumThreshold: this._bufferSequenceExtraThreshold,
bufferSize: this._maxBufferSequenceLength,
});
this.startResetBufferSizeTimer();
}
startResetBufferSizeTimer() {
this._resetBufferSizeTimer = setTimeout(() => {
this._bufferSize = this._defaultBufferSize;
this._bufferSizeSequence = [];
}, this._resetBufferSizeTimeout);
}
disposeClearBufferSizeTimer() {
clearTimeout(this._resetBufferSizeTimer);
}
/**
* Calculates the position of an element based on the given scrollSize
*/
getElementFromStart(scrollSize, collection, map, typicalItemSize, isVertical) {
const sizeProperty = isVertical ? HEIGHT_PROP_NAME : WIDTH_PROP_NAME;
let offset = 0;
for (let i = 0, l = collection.length; i < l; i++) {
const item = collection[i];
let itemSize = 0;
if (map.has(item.id)) {
const bounds = map.get(item.id);
itemSize = bounds ? bounds[sizeProperty] : typicalItemSize;
}
else {
itemSize = typicalItemSize;
}
if (offset > scrollSize) {
return item;
}
offset += itemSize;
}
return undefined;
}
/**
* Calculates the entry into the overscroll area and returns the number of overscroll elements
*/
getElementNumToEnd(i, collection, map, typicalItemSize, size, isVertical, indexOffset = 0) {
const sizeProperty = isVertical ? HEIGHT_PROP_NAME : WIDTH_PROP_NAME;
let offset = 0, num = 0;
for (let j = collection.length - indexOffset - 1; j >= i; j--) {
const item = collection[j];
let itemSize = 0;
if (map.has(item.id)) {
const bounds = map.get(item.id);
itemSize = bounds ? bounds[sizeProperty] : typicalItemSize;
}
else {
itemSize = typicalItemSize;
}
offset += itemSize;
num++;
if (offset > size) {
return { num: 0, offset };
}
}
return { num, offset };
}
/**
* Calculates list metrics
*/
recalculateMetrics(options) {
const { fromItemId, bounds, collection, dynamicSize, isVertical, itemSize, bufferSize: minBufferSize, scrollSize, snap, stickyMap, enabledBufferOptimization, previousTotalSize, crudDetected, deletedItemsMap } = options;
const bufferSize = Math.max(minBufferSize, this._bufferSize), { 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, snapshot = this._snapshot, checkOverscrollItemsLimit = Math.ceil(size / typicalItemSize), snippedPos = Math.floor(scrollSize), leftItemsWeights = [], isFromId = fromItemId !== undefined && (typeof fromItemId === 'number' && fromItemId > -1)
|| (typeof fromItemId === 'string' && fromItemId > '-1');
let leftItemsOffset = 0, rightItemsOffset = 0;
if (enabledBufferOptimization) {
switch (this.scrollDirection) {
case 1: {
leftItemsOffset = 0;
rightItemsOffset = bufferSize;
break;
}
case -1: {
leftItemsOffset = bufferSize;
rightItemsOffset = 0;
break;
}
case 0:
default: {
leftItemsOffset = rightItemsOffset = bufferSize;
}
}
}
else {
leftItemsOffset = rightItemsOffset = bufferSize;
}
let itemsFromStartToScrollEnd = -1, itemsFromDisplayEndToOffsetEnd = 0, itemsFromStartToDisplayEnd = -1, leftItemLength = 0, rightItemLength = 0, leftItemsWeight = 0, rightItemsWeight = 0, leftHiddenItemsWeight = 0, totalItemsToDisplayEndWeight = 0, leftSizeOfAddedItems = 0, leftSizeOfUpdatedItems = 0, leftSizeOfDeletedItems = 0, itemById = undefined, itemByIdPos = 0, targetDisplayItemIndex = -1, isTargetInOverscroll = false, actualScrollSize = itemByIdPos, totalSize = 0, startIndex, isFromItemIdFound = false;
// If the list is dynamic or there are new elements in the collection, then it switches to the long algorithm.
if (dynamicSize) {
let y = 0, stickyCollectionItem = undefined, stickyComponentSize = 0;
for (let i = 0, l = collection.length; i < l; i++) {
const ii = i + 1, collectionItem = collection[i], id = collectionItem.id;
let componentSize = 0, componentSizeDelta = 0, itemDisplayMethod = ItemDisplayMethods.NOT_CHANGED;
if (map.has(id)) {
const bounds = map.get(id) || { width: typicalItemSize, height: typicalItemSize };
componentSize = bounds[sizeProperty];
itemDisplayMethod = bounds?.method ?? ItemDisplayMethods.UPDATE;
switch (itemDisplayMethod) {
case ItemDisplayMethods.UPDATE: {
const snapshotBounds = snapshot.get(id);
const componentSnapshotSize = componentSize - (snapshotBounds ? snapshotBounds[sizeProperty] : typicalItemSize);
componentSizeDelta = componentSnapshotSize;
map.set(id, { ...bounds, method: ItemDisplayMethods.NOT_CHANGED });
break;
}
case ItemDisplayMethods.CREATE: {
componentSizeDelta = typicalItemSize;
map.set(id, { ...bounds, method: ItemDisplayMethods.NOT_CHANGED });
break;
}
}
}
if (deletedItemsMap.hasOwnProperty(i)) {
const bounds = deletedItemsMap[i], size = bounds?.[sizeProperty] ?? typicalItemSize;
if (y < scrollSize - size) {
leftSizeOfDeletedItems += size;
}
}
totalSize += componentSize;
if (isFromId) {
if (itemById === undefined) {
if (id !== fromItemId && stickyMap && stickyMap[id] === 1) {
stickyComponentSize = componentSize;
stickyCollectionItem = collectionItem;
}
if (id === fromItemId) {
isFromItemIdFound = true;
targetDisplayItemIndex = i;
if (stickyCollectionItem && stickyMap) {
const { num } = this.getElementNumToEnd(i, collection, map, typicalItemSize, size, isVertical);
if (num > 0) {
isTargetInOverscroll = true;
y -= size - componentSize;
}
else {
if (stickyMap && !stickyMap[collectionItem.id] && y >= scrollSize && y < scrollSize + stickyComponentSize) {
const snappedY = scrollSize - stickyComponentSize;
leftHiddenItemsWeight -= (snappedY - y);
y = snappedY;
}
else {
y -= stickyComponentSize;
leftHiddenItemsWeight -= stickyComponentSize;
}
}
}
itemById = collectionItem;
itemByIdPos = y;
}
else {
leftItemsWeights.push(componentSize);
leftHiddenItemsWeight += componentSize;
itemsFromStartToScrollEnd = ii;
}
}
}
else if (y <= scrollSize - componentSize) {
leftItemsWeights.push(componentSize);
leftHiddenItemsWeight += componentSize;
itemsFromStartToScrollEnd = ii;
}
if (isFromId) {
if (itemById === undefined || y < itemByIdPos + size + componentSize) {
itemsFromStartToDisplayEnd = ii;
totalItemsToDisplayEndWeight += componentSize;
itemsFromDisplayEndToOffsetEnd = itemsFromStartToDisplayEnd + rightItemsOffset;
}
}
else if (y <= scrollSize + size + componentSize) {
itemsFromStartToDisplayEnd = ii;
totalItemsToDisplayEndWeight += componentSize;
itemsFromDisplayEndToOffsetEnd = itemsFromStartToDisplayEnd + rightItemsOffset;
if (y <= scrollSize - componentSize) {
switch (itemDisplayMethod) {
case ItemDisplayMethods.CREATE: {
leftSizeOfAddedItems += componentSizeDelta;
break;
}
case ItemDisplayMethods.UPDATE: {
leftSizeOfUpdatedItems += componentSizeDelta;
break;
}
case ItemDisplayMethods.DELETE: {
leftSizeOfDeletedItems += componentSizeDelta;
break;
}
}
}
}
else {
if (i < itemsFromDisplayEndToOffsetEnd) {
rightItemsWeight += componentSize;
}
}
y += componentSize;
}
if (isTargetInOverscroll) {
const { num } = this.getElementNumToEnd(collection.length - (checkOverscrollItemsLimit < 0 ? 0 : collection.length - checkOverscrollItemsLimit), collection, map, typicalItemSize, size, isVertical, collection.length - (collection.length - (targetDisplayItemIndex + 1)));
if (num > 0) {
itemsFromStartToScrollEnd -= num;
}
}
if (itemsFromStartToScrollEnd <= -1) {
itemsFromStartToScrollEnd = 0;
}
if (itemsFromStartToDisplayEnd <= -1) {
itemsFromStartToDisplayEnd = 0;
}
actualScrollSize = isFromId ? itemByIdPos : scrollSize;
leftItemsWeights.splice(0, leftItemsWeights.length - leftItemsOffset);
leftItemsWeights.forEach(v => {
leftItemsWeight += v;
});
leftItemLength = Math.min(itemsFromStartToScrollEnd, leftItemsOffset);
rightItemLength = itemsFromStartToDisplayEnd + rightItemsOffset > totalLength
? totalLength - itemsFromStartToDisplayEnd : rightItemsOffset;
}
else
// Buffer optimization does not work on fast linear algorithm
{
if (crudDetected) {
let y = 0;
for (let i = 0, l = collection.length; i < l; i++) {
const collectionItem = collection[i], id = collectionItem.id;
let componentSize = typicalItemSize, itemDisplayMethod = ItemDisplayMethods.NOT_CHANGED;
if (map.has(id)) {
const bounds = map.get(id);
itemDisplayMethod = bounds?.method ?? ItemDisplayMethods.UPDATE;
if (itemDisplayMethod === ItemDisplayMethods.CREATE) {
map.set(id, { ...bounds, method: ItemDisplayMethods.NOT_CHANGED });
}
}
if (deletedItemsMap.hasOwnProperty(i)) {
const bounds = deletedItemsMap[i], size = bounds?.[sizeProperty] ?? typicalItemSize;
if (y < scrollSize - size) {
leftSizeOfDeletedItems += size;
}
}
if (y < scrollSize - componentSize) {
switch (itemDisplayMethod) {
case ItemDisplayMethods.CREATE: {
leftSizeOfUpdatedItems += componentSize;
break;
}
case ItemDisplayMethods.UPDATE: {
leftSizeOfUpdatedItems += componentSize;
break;
}
case ItemDisplayMethods.DELETE: {
leftSizeOfDeletedItems += componentSize;
break;
}
}
}
y += componentSize;
}
}
itemsFromStartToScrollEnd = Math.floor(scrollSize / typicalItemSize);
itemsFromStartToDisplayEnd = Math.ceil((scrollSize + size) / typicalItemSize);
leftItemLength = Math.min(itemsFromStartToScrollEnd, bufferSize);
rightItemLength = itemsFromStartToDisplayEnd + bufferSize > totalLength
? totalLength - itemsFromStartToDisplayEnd : bufferSize;
leftItemsWeight = leftItemLength * typicalItemSize;
rightItemsWeight = rightItemLength * typicalItemSize;
leftHiddenItemsWeight = itemsFromStartToScrollEnd * typicalItemSize;
totalItemsToDisplayEndWeight = itemsFromStartToDisplayEnd * typicalItemSize;
totalSize = totalLength * typicalItemSize;
const k = totalSize !== 0 ? previousTotalSize / totalSize : 0;
actualScrollSize = scrollSize * k;
}
startIndex = Math.min(itemsFromStartToScrollEnd - leftItemLength, totalLength > 0 ? totalLength - 1 : 0);
const itemsOnDisplayWeight = totalItemsToDisplayEndWeight - leftItemsWeight, itemsOnDisplayLength = itemsFromStartToDisplayEnd - itemsFromStartToScrollEnd, startPosition = leftHiddenItemsWeight - leftItemsWeight, renderItems = itemsOnDisplayLength + leftItemLength + rightItemLength, delta = leftSizeOfUpdatedItems + leftSizeOfAddedItems - leftSizeOfDeletedItems;
const metrics = {
delta,
normalizedItemWidth: w,
normalizedItemHeight: h,
width,
height,
dynamicSize,
itemSize,
itemsFromStartToScrollEnd,
itemsFromStartToDisplayEnd,
itemsOnDisplayWeight,
itemsOnDisplayLength,
isVertical,
leftHiddenItemsWeight,
leftItemLength,
leftItemsWeight,
renderItems,
rightItemLength,
rightItemsWeight,
scrollSize: actualScrollSize,
leftSizeOfAddedItems,
sizeProperty,
snap,
snippedPos,
startIndex,
startPosition,
totalItemsToDisplayEndWeight,
totalLength,
totalSize,
typicalItemSize,
isFromItemIdFound,
};
return metrics;
}
clearDeltaDirection() {
this.clearScrollDirectionCache();
}
clearDelta(clearDirectionDetector = false) {
this._delta = 0;
if (clearDirectionDetector) {
this.clearScrollDirectionCache();
}
}
changes() {
this.bumpVersion();
}
generateDisplayCollection(items, stickyMap, metrics) {
const { width, height, normalizedItemWidth, normalizedItemHeight, dynamicSize, itemsOnDisplayLength, itemsFromStartToScrollEnd, isVertical, renderItems: renderItemsLength, scrollSize, sizeProperty, snap, snippedPos, startPosition, totalLength, startIndex, typicalItemSize, } = metrics, displayItems = [];
if (items.length) {
const actualSnippedPosition = snippedPos, isSnappingMethodAdvanced = this.isSnappingMethodAdvanced, boundsSize = isVertical ? height : width, actualEndSnippedPosition = boundsSize;
let pos = startPosition, renderItems = renderItemsLength, stickyItem, nextSticky, stickyItemIndex = -1, stickyItemSize = 0, endStickyItem, nextEndSticky, endStickyItemIndex = -1, endStickyItemSize = 0;
if (snap) {
for (let i = Math.min(itemsFromStartToScrollEnd > 0 ? itemsFromStartToScrollEnd : 0, totalLength - 1); i >= 0; i--) {
if (!items[i]) {
continue;
}
const id = items[i].id, sticky = stickyMap[id], size = dynamicSize ? this.get(id)?.[sizeProperty] || typicalItemSize : typicalItemSize;
if (sticky === 1) {
const measures = {
x: isVertical ? 0 : actualSnippedPosition,
y: isVertical ? actualSnippedPosition : 0,
width: isVertical ? normalizedItemWidth : size,
height: isVertical ? size : normalizedItemHeight,
delta: 0,
}, config = {
isVertical,
sticky,
snap,
snapped: true,
snappedOut: false,
dynamic: dynamicSize,
isSnappingMethodAdvanced,
zIndex: '1',
};
const itemData = items[i];
stickyItem = { id, measures, data: itemData, config };
stickyItemIndex = i;
stickyItemSize = size;
displayItems.push(stickyItem);
break;
}
}
}
if (snap) {
const startIndex = itemsFromStartToScrollEnd + itemsOnDisplayLength - 1;
for (let i = Math.min(startIndex, totalLength > 0 ? totalLength - 1 : 0), l = totalLength; i < l; i++) {
if (!items[i]) {
continue;
}
const id = items[i].id, sticky = stickyMap[id], size = dynamicSize
? this.get(id)?.[sizeProperty] || typicalItemSize
: typicalItemSize;
if (sticky === 2) {
const w = isVertical ? normalizedItemWidth : size, h = isVertical ? size : normalizedItemHeight, measures = {
x: isVertical ? 0 : actualEndSnippedPosition - w,
y: isVertical ? actualEndSnippedPosition - h : 0,
width: w,
height: h,
delta: 0,
}, config = {
isVertical,
sticky,
snap,
snapped: true,
snappedOut: false,
dynamic: dynamicSize,
isSnappingMethodAdvanced,
zIndex: '1',
};
const itemData = items[i];
endStickyItem = { id, measures, data: itemData, config };
endStickyItemIndex = i;
endStickyItemSize = size;
displayItems.push(endStickyItem);
break;
}
}
}
let i = startIndex;
while (renderItems > 0) {
if (i >= totalLength) {
break;
}
if (!items[i]) {
continue;
}
const id = items[i].id, size = dynamicSize ? this.get(id)?.[sizeProperty] || typicalItemSize : typicalItemSize;
if (id !== stickyItem?.id && id !== endStickyItem?.id) {
const snapped = snap && (stickyMap[id] === 1 && pos <= scrollSize || stickyMap[id] === 2 && pos >= scrollSize + boundsSize - size), measures = {
x: isVertical ? stickyMap[id] === 1 ? 0 : boundsSize - size : pos,
y: isVertical ? pos : stickyMap[id] === 2 ? boundsSize - size : 0,
width: isVertical ? normalizedItemWidth : size,
height: isVertical ? size : normalizedItemHeight,
delta: 0,
}, config = {
isVertical,
sticky: stickyMap[id],
snap,
snapped: false,
snappedOut: false,
dynamic: dynamicSize,
isSnappingMethodAdvanced,
zIndex: '0',
};
if (snapped) {
config.zIndex = '2';
}
const itemData = items[i];
const item = { id, measures, data: itemData, config };
if (!nextSticky && stickyItemIndex < i && stickyMap[id] === 1 && (pos <= scrollSize + size + stickyItemSize)) {
item.measures.x = isVertical ? 0 : snapped ? actualSnippedPosition : pos;
item.measures.y = isVertical ? snapped ? actualSnippedPosition : pos : 0;
nextSticky = item;
nextSticky.config.snapped = snapped;
nextSticky.measures.delta = isVertical ? (item.measures.y - scrollSize) : (item.measures.x - scrollSize);
nextSticky.config.zIndex = '3';
}
else if (!nextEndSticky && endStickyItemIndex > i && stickyMap[id] === 2 && (pos >= scrollSize + boundsSize - size - endStickyItemSize)) {
item.measures.x = isVertical ? 0 : snapped ? actualEndSnippedPosition - size : pos;
item.measures.y = isVertical ? snapped ? actualEndSnippedPosition - size : pos : 0;
nextEndSticky = item;
nextEndSticky.config.zIndex = '3';
nextEndSticky.config.snapped = snapped;
nextEndSticky.measures.delta = isVertical ? (item.measures.y - scrollSize) : (item.measures.x - scrollSize);
}
displayItems.push(item);
}
renderItems -= 1;
pos += size;
i++;
}
const axis = isVertical ? Y_PROP_NAME : X_PROP_NAME;
if (nextSticky && stickyItem && nextSticky.measures[axis] <= scrollSize + stickyItemSize) {
if (nextSticky.measures[axis] > scrollSize) {
stickyItem.measures[axis] = nextSticky.measures[axis] - stickyItemSize;
stickyItem.config.snapped = nextSticky.config.snapped = false;
stickyItem.config.snappedOut = true;
stickyItem.config.sticky = 1;
stickyItem.measures.delta = isVertical ? stickyItem.measures.y - scrollSize : stickyItem.measures.x - scrollSize;
}
else {
nextSticky.config.snapped = true;
nextSticky.measures.delta = isVertical ? nextSticky.measures.y - scrollSize : nextSticky.measures.x - scrollSize;
}
}
if (nextEndSticky && endStickyItem && nextEndSticky.measures[axis] >= scrollSize + boundsSize - endStickyItemSize - nextEndSticky.measures[sizeProperty]) {
if (nextEndSticky.measures[axis] < scrollSize + boundsSize - endStickyItemSize) {
endStickyItem.measures[axis] = nextEndSticky.measures[axis] + nextEndSticky.measures[sizeProperty];
endStickyItem.config.snapped = nextEndSticky.config.snapped = false;
endStickyItem.config.snappedOut = true;
endStickyItem.config.sticky = 2;
endStickyItem.measures.delta = isVertical ? endStickyItem.measures.y - scrollSize : endStickyItem.measures.x - scrollSize;
}
else {
nextEndSticky.config.snapped = true;
nextEndSticky.measures.delta = isVertical ? nextEndSticky.measures.y - scrollSize : nextEndSticky.measures.x - scrollSize;
}
}
}
return displayItems;
}
/**
* tracking by propName
*/
track() {
if (!this._items || !this._displayComponents) {
return;
}
this._tracker.track(this._items, this._displayComponents, this._snapedDisplayComponent, this.scrollDirection);
}
setDisplayObjectIndexMapById(v) {
this._tracker.displayObjectIndexMapById = v;
}
untrackComponentByIdProperty(component) {
this._tracker.untrackComponentByIdProperty(component);
}
getItemBounds(id) {
if (this.has(id)) {
return this.get(id);
}
return undefined;
}
cacheElements() {
if (!this._displayComponents) {
return;
}
for (let i = 0, l = this._displayComponents.length; i < l; i++) {
const component = this._displayComponents[i], itemId = component.instance.itemId;
if (itemId === undefined) {
continue;
}
const bounds = component.instance.getBounds();
this.set(itemId, bounds);
}
}
dispose() {
super.dispose();
this.disposeClearBufferSizeTimer();
if (this._tracker) {
this._tracker.dispose();
}
}
}
const ADVANCED_PATTERNS = [SnappingMethods.ADVANCED, 'advanced'], DEFAULT_PATTERN = [SnappingMethods.NORMAL, 'normal'];
const isSnappingMethodAdvenced = (method) => {
return ADVANCED_PATTERNS.includes(method);
};
const isSnappingMethodDefault = (method) => {
return DEFAULT_PATTERN.includes(method);
};
const IS_FIREFOX = navigator.userAgent.toLowerCase().includes('firefox');
const FIREFOX_SCROLLBAR_OVERLAP_SIZE = 12;
const HORIZONTAL_ALIASES = [Directions.HORIZONTAL, 'horizontal'], VERTICAL_ALIASES = [Directions.VERTICAL, 'vertical'];
/**
* Determines the axis membership of a virtual list
* @link https://github.com/DjonnyX/ng-virtual-list/blob/20.x/projects/ng-virtual-list/src/lib/utils/isDirection.ts
* @author Evgenii Grebennikov
* @email djonnyx@gmail.com
*/
const isDirection = (src, expected) => {
if (HORIZONTAL_ALIASES.includes(expected)) {
return HORIZONTAL_ALIASES.includes(src);
}
return VERTICAL_ALIASES.includes(src);
};
/**
* Virtual list component.
* Maximum performance for extremely large lists.
* It is based on algorithms for virtualization of screen objects.
* @link https://github.com/DjonnyX/ng-virtual-list/blob/20.x/projects/ng-virtual-list/src/lib/ng-virtual-list.component.ts
* @author Evgenii Grebennikov
* @email djonnyx@gmail.com
*/
class NgVirtualListComponent {
static __nextId = 0;
_id = NgVirtualListComponent.__nextId;
/**
* Readonly. Returns the unique identifier of the component.
*/
get id() { return this._id; }
_listContainerRef;
_snapContainerRef;
_snappedContainer = viewChild('snapped');
_container = viewChild('container');
_list = viewChild('list');
/**
* Fires when the list has been scrolled.
*/
onScroll = output();
/**
* Fires when the list has completed scrolling.
*/
onScrollEnd = output();
_itemsOptions = {
transform: (v) => {
this._trackBox.resetCollection(v, this.itemSize());
return v;
},
};
/**
* Collection of list items.
*/
items = input.required({
...this._itemsOptions,
});
/**
* Determines whether elements will snap. Default value is "true".
*/
snap = input(DEFAULT_SNAP);
/**
* Experimental!
* Enables buffer optimization.
* 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.
* Works only if the property dynamic = true
*/
enabledBufferOptimization = input(DEFAULT_ENABLED_BUFFER_OPTIMIZATION);
/**
* Rendering element template.
*/
itemRenderer = input.required();
_itemRenderer = signal(undefined);
/**
* Dictionary zIndex by id of the list element. If the value is not set or equal to 0,
* then a simple element is displayed, if the value is greater than 0, then the sticky position mode is enabled for the element.
*/
stickyMap = input({});
_itemSizeOptions = {
transform: (v) => {
if (v === undefined) {
return DEFAULT_ITEM_SIZE;
}
const val = Number(v);
return Number.isNaN(val) || val <= 0 ? DEFAULT_ITEM_SIZE : val;
},
};
/**
* If direction = 'vertical', then the height of a typical element. If direction = 'horizontal', then the width of a typical element.
* Ignored if the dynamicSize property is true.
*/
itemSize = input(DEFAULT_ITEM_SIZE, { ...this._itemSizeOptions });
/**
* 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.
*/
dynamicSize = input(DEFAULT_DYNAMIC_SIZE);
/**
* Determines the direction in which elements are placed. Default value is "vertical".
*/
direction = input(DEFAULT_DIRECTION);
_itemOffsetTransform = {
transform: (v) => {
throw Error('"itemOffset" parameter is deprecated. Use "bufferSize" and "maxBufferSize".');
}
};
/**
* Number of elements outside the scope of visibility. Default value is 2.
* @deprecated "itemOffset" parameter is deprecated. Use "bufferSize" and "maxBufferSize".
*/
itemsOffset = input(DEFAULT_BUFFER_SIZE, { ...this._itemOffsetTransform });
/**
* Number of elements outside the scope of visibility. Default value is 2.
*/
bufferSize = input(DEFAULT_BUFFER_SIZE);
_maxBufferSizeTransform = {
transform: (v) => {
const bufferSize = this.bufferSize();
if (v === undefined || v <= bufferSize) {
return bufferSize;
}
return v;
}
};
/**
* Maximum number of elements outside the scope of visibility. Default value is 100.
* If maxBufferSize is set to be greater than bufferSize, then adaptive buffer mode is enabled.
* The greater the scroll size, the more elements are allocated for rendering.
*/
maxBufferSize = input(DEFAULT_MAX_BUFFER_SIZE, { ...this._maxBufferSizeTransform });
/**
* Snapping method.
* 'default' - Normal group rendering.
* 'advanced' - The group is rendered on a transparent background. List items below the group are not rendered.
*/
snappingMethod = input(DEFAULT_SNAPPING_METHOD);
_isSnappingMethodAdvanced = this.getIsSnappingMethodAdvanced();
get isSnappingMethodAdvanced() { return this._isSnappingMethodAdvanced; }
_isVertical = this.getIsVertical();
_displayComponents = [];
_snapedDisplayComponent;
_bounds = signal(null);
_scrollSize = signal(0);
_resizeObserver = null;
_resizeSnappedComponentHandler = () => {
const list = this._list(), container = this._container(), snappedComponent = this._snapedDisplayComponent?.instance;
if (list && container && snappedComponent) {
const isVertical = this._isVertical, listBounds = list.nativeElement.getBoundingClientRect(), listElement = list?.nativeElement, { width: lWidth, height: lHeight } = listElement?.getBoundingClientRect() ?? { width: 0, height: 0 }, { width, height } = this._bounds() ?? { width: 0, height: 0 }, isScrollable = isVertical ? container.nativeElement.scrollHeight > 0 : container.nativeElement.scrollWidth > 0;
let scrollBarSize = isVertical ? width - lWidth : height - lHeight, isScrollBarOverlap = true, overlapScrollBarSize = 0;
if (scrollBarSize === 0 && isScrollable) {
isScrollBarOverlap = true;
}
if (isScrollBarOverlap && IS_FIREFOX) {
scrollBarSize = overlapScrollBarSize = FIREFOX_SCROLLBAR_OVERLAP_SIZE;
}
snappedComponent.element.style.clipPath = `path("M 0 0 L 0 ${snappedComponent.element.offsetHeight} L ${snappedComponent.element.offsetWidth - overlapScrollBarSize} ${snappedComponent.element.offsetHeight} L ${snappedComponent.element.offsetWidth - overlapScrollBarSize} 0 Z")`;
snappedComponent.regularLength = `${isVertical ? listBounds.width : listBounds.height}${PX}`;
const { width: sWidth, height: sHeight } = snappedComponent.getBounds() ?? { width: 0, height: 0 }, containerElement = container.nativeElement, delta = snappedComponent.item?.measures.delta ?? 0;
let left, right, top, bottom;
if (isVertical) {
left = 0;
right = width - scrollBarSize;
top = sHeight;
bottom = height;
containerElement.style.clipPath = `path("M 0 ${top + delta} L 0 ${height} L ${width} ${height} L ${width} 0 L ${right} 0 L ${right} ${top + delta} Z")`;
}
else {
left = sWidth;
right = width;
top = 0;
bottom = height - scrollBarSize;
containerElement.style.clipPath = `path("M ${left + delta} 0 L ${left + delta} ${bottom} L 0 ${bottom} L 0 ${height} L ${width} ${height} L ${width} 0 Z")`;
}
}
};
_resizeSnappedObserver = null;
_componentsResizeObserver = new ResizeObserver(() => {
this._trackBox.changes();
});
_onResizeHandler = () => {
const bounds = this._container()?.nativeElement?.getBoundingClientRect();
if (bounds) {
this._bounds.set({ width: bounds.width, height: bounds.height });
}
else {
this._bounds.set({ width: DEFAULT_LIST_SIZE, height: DEFAULT_LIST_SIZE });
}
if (this._isSnappingMethodAdvanced) {
this.updateRegularRenderer();
}
};
_onScrollHandler = (e) => {
this.clearScrollToRepeatExecutionTimeout();
const container = this._container()?.nativeElement;
if (container) {
const scrollSize = (this._isVertical ? container.scrollTop : container.scrollLeft), actualScrollSize = scrollSize;
this._scrollSize.set(actualScrollSize);
}
};
_elementRef = inject((ElementRef));
_initialized;
$initialized;
/**
* The name of the property by which tracking is performed
*/
trackBy = input(TRACK_BY_PROPERTY_NAME);
/**
* Base class of the element component
*/
_itemComponentClass = NgVirtualListItemComponent;
/**
* Base class trackBox
*/
_trackBoxClass = TrackBox;
/**
* Dictionary of element sizes by their id
*/
_trackBox = new this._trackBoxClass(this.trackBy());
_onTrackBoxChangeHandler = (v) => {
this._cacheVersion.set(v);
};
_cacheVersion = signal(-1);
constructor() {
NgVirtualListComponent.__nextId = NgVirtualListComponent.__nextId + 1 === Number.MAX_SAFE_INTEGER
? 0 : NgVirtualListComponent.__nextId + 1;
this._id = NgVirtualListComponent.__nextId;
this._initialized = signal(false);
this.$initialized = toObservable(this._initialized);
this._trackBox.displayComponents = this._displayComponents;
const $trackBy = toObservable(this.trackBy);
$trackBy.pipe(takeUntilDestroyed(), tap(v => {
this._trackBox.trackingPropertyName = v;
})).subscribe();
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)), $bufferSize = toObservable(this.bufferSize).pipe(map(v => v < 0 ? DEFAULT_BUFFER_SIZE : v)), $maxBufferSize = toObservable(this.maxBufferSize).pipe(map(v => v < 0 ? DEFAULT_BUFFER_SIZE : 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), $enabledBufferOptimization = toObservable(this.enabledBufferOptimization), $snappingMethod = toObservable(this.snappingMethod).pipe(map(v => this.getIsSnappingMethodAdvanced(v || DEFAULT_SNAPPING_METHOD))), $cacheVersion = toObservable(this._cacheVersion);
$isVertical.pipe(takeUntilDestroyed(), tap(v => {
this._isVertical = v;
const el = this._elementRef.nativeElement;
toggleClassName(el, v ? CLASS_LIST_VERTICAL : CLASS_LIST_HORIZONTAL, v ? CLASS_LIST_HORIZONTAL : CLASS_LIST_VERTICAL);
})).subscribe();
$snappingMethod.pipe(takeUntilDestroyed(), tap(v => {
this._isSnappingMethodAdvanced = this._trackBox.isSnappingMethodAdvanced = v;
})).subscribe();
$dynamicSize.pipe(takeUntilDestroyed(), tap(dynamicSize => {
this.listenCacheChangesIfNeed(dynamicSize);
})).subscribe();
combineLatest([this.$initialized, $bounds, $items, $stickyMap, $scrollSize, $itemSize,
$bufferSize, $maxBufferSize, $snap, $isVertical, $dynamicSize, $enabledBufferOptimization, $cacheVersion,
]).pipe(takeUntilDestroyed(), distinctUntilChanged(), filter(([initialized]) => !!initialized), switchMap(([, bounds, items, stickyMap, scrollSize, itemSize, bufferSize, maxBufferSize, snap, isVertical, dynamicSize, enabledBufferOptimization, cacheVersion,]) => {
let actualScrollSize = (this._isVertical ? this._container()?.nativeElement.scrollTop ?? 0 : this._container()?.nativeElement.scrollLeft) ?? 0;
const { width, height } = bounds, opts = {
bounds: { width, height }, dynamicSize, isVertical, itemSize,
bufferSize, maxBufferSize, scrollSize: actualScrollSize, snap, enabledBufferOptimization,
}, { displayItems, totalSize } = this._trackBox.updateCollection(items, stickyMap, opts);
this.resetBoundsSize(isVertical, totalSize);
this.createDisplayComponentsIfNeed(displayItems);
this.tracking();
if (this._isSnappingMethodAdvanced) {
this.updateRegularRenderer();
}
const container = this._container();
if (container) {
const delta = this._trackBox.delta;
actualScrollSize = actualScrollSize + delta;
this._trackBox.clearDelta();
if (scrollSize !== actualScrollSize) {
const params = {
[this._isVertical ? TOP_PROP_NAME : LEFT_PROP_NAME]: actualScrollSize,
behavior: BEHAVIOR_INSTANT
};
container.nativeElement.scrollTo(params);
}
}
return of(displayItems);
})).subscribe();
const $itemRenderer = toObservable(this.itemRenderer);
$itemRenderer.pipe(takeUntilDestroyed(), distinctUntilChanged(), filter(v => !!v), tap(v => {
this._itemRenderer.set(v);
})).subscribe();
}
/** @internal */
ngOnInit() {
this.onInit();
}
onInit() {
this._initialized.set(true);
}
listenCacheChangesIfNeed(value) {
if (value) {
if (!this._trackBox.hasEventListener(TRACK_BOX_CHANGE_EVENT_NAME, this._onTrackBoxChangeHandler)) {
this._trackBox.addEventListener(TRACK_BOX_CHANGE_EVENT_NAME, this._onTrackBoxChangeHandler);
}
}
else {
if (this._trackBox.hasEventListener(TRACK_BOX_CHANGE_EVENT_NAME, this._onTrackBoxChangeHandler)) {
this._trackBox.removeEventListener(TRACK_BOX_CHANGE_EVENT_NAME, this._onTrackBoxChangeHandler);
}
}
}
getIsSnappingMethodAdvanced(m) {
const method = m || this.snappingMethod();
return isSnappingMethodAdvenced(method);
}
getIsVertical(d) {
const dir = d || this.direction();
return isDirection(dir, Directions.VERTICAL);
}
createDisplayComponentsIfNeed(displayItems) {
if (!displayItems || !this._listContainerRef) {
this._trackBox.setDisplayObjectIndexMapById({});
return;
}
if (this._isSnappingMethodAdvanced && this.snap()) {
if (!this._snapedDisplayComponent && this._snapContainerRef) {
const comp = this._snapContainerRef.createComponent(this._itemComponentClass);
comp.instance.regular = true;
this._snapedDisplayComponent = comp;
this._trackBox.snapedDisplayComponent = this._snapedDisplayComponent;
this._resizeSnappedObserver = new ResizeObserver(this._resizeSnappedComponentHandler);
this._resizeSnappedObserver.observe(comp.instance.element);
}
}
this._trackBox.items = displayItems;
const _listContainerRef = this._listContainerRef;
const maxLength = displayItems.length, components = this._displayComponents;
while (components.length < maxLength) {
if (_listContainerRef) {
const comp = _listContainerRef.createComponent(this._itemComponentClass);
components.push(comp);
this._componentsResizeObserver.observe(comp.instance.element);
}
}
this.resetRenderers();
}
updateRegularRenderer() {
this._resizeSnappedComponentHandler();
}
resetRenderers(itemRenderer) {
const doMap = {}, components = this._displayComponents;
for (let i = 0, l = components.length; i < l; i++) {
const item = components[i];
if (item) {
const id = item.instance.id;
item.instance.renderer = itemRenderer || this._itemRenderer();
doMap[id] = i;
}
}
if (this._isSnappingMethodAdvanced && this.snap() && this._snapedDisplayComponent && this._snapContainerRef) {
const comp = this._snapedDisplayComponent;
comp.instance.renderer = itemRenderer || this._itemRenderer();
}
this._trackBox.setDisplayObjectIndexMapById(doMap);
}
/**
* Tracking by id
*/
tracking() {
this._trackBox.track();
}
resetBoundsSize(isVertical, totalSize) {
const l = this._list();
if (l) {
l.nativeElement.style[isVertical ? HEIGHT_PROP_NAME : WIDTH_PROP_NAME] = `${totalSize}${PX}`;
}
}
/**
* Returns the bounds of an element with a given id
*/
getItemBounds(id) {
return this._trackBox.getItemBounds(id);
}
/**
* The method scrolls the list to the element with the given id and returns the value of the scrolled area.
* Behavior accepts the values "auto", "instant" and "smooth".
*/
scrollTo(id, behavior = BEHAVIOR_AUTO) {
this.scrollToExecutor(id, behavior);
}
_scrollToRepeatExecutionTimeout;
clearScrollToRepeatExecutionTimeout() {
clearTimeout(this._scrollToRepeatExecutionTimeout);
}
scrollToExecutor(id, behavior, iteration = 0, isLastIteration = false) {
const items = this.items();
if (!items || !items.length) {
return;
}
const dynamicSize = this.dynamicSize(), container = this._container(), itemSize = this.itemSize();
if (container) {
this.clearScrollToRepeatExecutionTimeout();
if (dynamicSize) {
if (container) {
container.nativeElement.removeEventListener(SCROLL, this._onScrollHandler);
}
const { width, height } = this._bounds() || { width: DEFAULT_LIST_SIZE, height: DEFAULT_LIST_SIZE }, stickyMap = this.stickyMap(), items = this.items(), isVertical = this._isVertical, delta = this._trackBox.delta, opts = {
bounds: { width, height }, collection: items, dynamicSize, isVertical: this._isVertical, itemSize,
bufferSize: this.bufferSize(), maxBufferSize: this.maxBufferSize(),
scrollSize: (isVertical ? container.nativeElement.scrollTop : container.nativeElement.scrollLeft) + delta,
snap: this.snap(), fromItemId: id, enabledBufferOptimization: this.enabledBufferOptimization(),
}, scrollSize = this._trackBox.getItemPosition(id, stickyMap, opts), params = { [isVertical ? TOP_PROP_NAME : LEFT_PROP_NAME]: scrollSize, behavior };
if (scrollSize === -1) {
container.nativeElement.addEventListener(SCROLL, this._onScrollHandler);
return;
}
this._trackBox.clearDelta();
if (container) {
const { displayItems, totalSize } = this._trackBox.updateCollection(items, stickyMap, {
...opts, scrollSize, fromItemId: isLastIteration ? undefined : id,
}), delta = this._trackBox.delta;
this._trackBox.clearDelta();
let actualScrollSize = scrollSize + delta;
this.resetBoundsSize(isVertical, totalSize);
this.createDisplayComponentsIfNeed(displayItems);
this.tracking();
const _scrollSize = this._trackBox.getItemPosition(id, stickyMap, { ...opts, scrollSize: actualScrollSize, fromItemId: id });
if (_scrollSize === -1) {
container.nativeElement.addEventListener(SCROLL, this._onScrollHandler);
return;
}
const notChanged = actualScrollSize === _scrollSize;
if (!notChanged || iteration < MAX_SCROLL_TO_ITERATIONS) {
this.clearScrollToRepeatExecutionTimeout();
this._scrollToRepeatExecutionTimeout = setTimeout(() => {
this.scrollToExecutor(id, BEHAVIOR_INSTANT, iteration + 1, notChanged);
});
}
else {
this._scrollSize.set(actualScrollSize);
container.nativeElement.addEventListener(SCROLL, this._onScrollHandler);
}
}
container.nativeElement.scrollTo(params);
this._scrollSize.set(scrollSize);
}
else {
const index = items.findIndex(item => item.id === id);
if (index > -1) {
const scrollSize = index * this.itemSize();
const params = { [this._isVertical ? TOP_PROP_NAME : LEFT_PROP_NAME]: scrollSize, behavior };
container.nativeElement.scrollTo(params);
}
}
}
}
/**
* Scrolls the scroll area to the desired element with the specified ID.
*/
scrollToEnd(behavior = BEHAVIOR_INSTANT) {
const items = this.items(), latItem = items[items.length > 0 ? items.length - 1 : 0];
this.scrollTo(latItem.id, behavior);
}
_onContainerScrollHandler = (e) => {
const containerEl = this._container();
if (containerEl) {
const scrollSize = (this._isVertical ? containerEl.nativeElement.scrollTop : containerEl.nativeElement.scrollLeft);
this._trackBox.deltaDirection = this._scrollSize() > scrollSize ? -1 : this._scrollSize() < scrollSize ? 1 : 0;
const event = new ScrollEvent({
direction: this._trackBox.scrollDirection, container: containerEl.nativeElement,
list: this._list().nativeElement, delta: this._trackBox.delta,
scrollDelta: this._trackBox.scrollDelta, isVertical: this._isVertical,
});
this.onScroll.emit(event);
}
};
_onContainerScrollEndHandler = (e) => {
const containerEl = this._container();
if (containerEl) {
const scrollSize = (this._isVertical ? containerEl.nativeElement.scrollTop : containerEl.nativeElement.scrollLeft);
this._trackBox.deltaDirection = this._scrollSize() > scrollSize ? -1 : 0;
const event = new ScrollEvent({
direction: this._trackBox.scrollDirection, container: containerEl.nativeElement,
list: this._list().nativeElement, delta: this._trackBox.delta,
scrollDelta: this._trackBox.scrollDelta, isVertical: this._isVertical,
});
this.onScrollEnd.emit(event);
}
};
/** @internal */
ngAfterViewInit() {
this.afterViewInit();
}
afterViewInit() {
const containerEl = this._container();
if (containerEl) {
// for direction calculation
containerEl.nativeElement.addEventListener(SCROLL, this._onContainerScrollHandler);
containerEl.nativeElement.addEventListener(SCROLL_END, this._onContainerScrollEndHandler);
containerEl.nativeElement.addEventListener(SCROLL, this._onScrollHandler);
this._resizeObserver = new ResizeObserver(this._onResizeHandler);
this._resizeObserver.observe(containerEl.nativeElement);
this._onResizeHandler();
}
}
/** @internal */
ngOnDestroy() {
this.dispose();
}
dispose() {
this.clearScrollToRepeatExecutionTimeout();
if (this._trackBox) {
this._trackBox.dispose();
}
if (this._componentsResizeObserver) {
this._componentsResizeObserver.disconnect();
}
if (this._resizeSnappedObserver) {
this._resizeSnappedObserver.disconnect();
}
if (this._resizeObserver) {
this._resizeObserver.disconnect();
}
const containerEl = this._container();
if (containerEl) {
containerEl.nativeElement.removeEventListener(SCROLL, this._onScrollHandler);
containerEl.nativeElement.removeEventListener(SCROLL, this._onContainerScrollHandler);
containerEl.nativeElement.removeEventListener(SCROLL_END, this._onContainerScrollEndHandler);
}
if (this._snapedDisplayComponent) {
this._snapedDisplayComponent.destroy();
}
if (this._displayComponents) {
while (this._displayComponents.length > 0) {
const comp = this._displayComponents.pop();
comp?.destroy();
}
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: NgVirtualListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.4", 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 }, 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 }, bufferSize: { classPropertyName: "bufferSize", publicName: "bufferSize", isSignal: true, isRequired: false, transformFunction: null }, maxBufferSize: { classPropertyName: "maxBufferSize", publicName: "maxBufferSize", isSignal: true, isRequired: false, transformFunction: null }, snappingMethod: { classPropertyName: "snappingMethod", publicName: "snappingMethod", isSignal: true, isRequired: false, transformFunction: null }, trackBy: { classPropertyName: "trackBy", publicName: "trackBy", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onScroll: "onScroll", onScrollEnd: "onScrollEnd" }, viewQueries: [{ propertyName: "_snappedContainer", first: true, predicate: ["snapped"], descendants: true, isSignal: true }, { 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 }, { propertyName: "_snapContainerRef", first: true, predicate: ["snapRendererContainer"], descendants: true, read: ViewContainerRef }], ngImport: i0, template: "@if (snap()) {\r\n<div #snapped part=\"snapped-item\" class=\"ngvl__list-snapper\">\r\n <ng-container #snapRendererContainer></ng-container>\r\n</div>\r\n}\r\n<div #container part=\"scroller\" class=\"ngvl__scroller\">\r\n <ul #list part=\"list\" class=\"ngvl__list\">\r\n <ng-container #renderersContainer></ng-container>\r\n </ul>\r\n</div>", styles: [":host{position:relative;display:block;width:400px;overflow:hidden}:host(.horizontal){height:48px}:host(.horizontal) .ngvl__list{display:inline-flex}:host(.horizontal) .ngvl__scroller{overflow:auto hidden}:host(.vertical) .ngvl__scroller{overflow:hidden auto}:host(.vertical){height:320px}.ngvl__scroller{overflow:auto;width:100%;height:100%}.ngvl__list-snapper{pointer-events:none;position:absolute;list-style:none;left:0;top:0;z-index:1}.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 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: NgVirtualListComponent, decorators: [{
type: Component,
args: [{ selector: 'ng-virtual-list', imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.ShadowDom, template: "@if (snap()) {\r\n<div #snapped part=\"snapped-item\" class=\"ngvl__list-snapper\">\r\n <ng-container #snapRendererContainer></ng-container>\r\n</div>\r\n}\r\n<div #container part=\"scroller\" class=\"ngvl__scroller\">\r\n <ul #list part=\"list\" class=\"ngvl__list\">\r\n <ng-container #renderersContainer></ng-container>\r\n </ul>\r\n</div>", styles: [":host{position:relative;display:block;width:400px;overflow:hidden}:host(.horizontal){height:48px}:host(.horizontal) .ngvl__list{display:inline-flex}:host(.horizontal) .ngvl__scroller{overflow:auto hidden}:host(.vertical) .ngvl__scroller{overflow:hidden auto}:host(.vertical){height:320px}.ngvl__scroller{overflow:auto;width:100%;height:100%}.ngvl__list-snapper{pointer-events:none;position:absolute;list-style:none;left:0;top:0;z-index:1}.ngvl__list{position:relative;list-style:none;padding:0;margin:0;width:100%;height:100%}\n"] }]
}], ctorParameters: () => [], propDecorators: { _listContainerRef: [{
type: ViewChild,
args: ['renderersContainer', { read: ViewContainerRef }]
}], _snapContainerRef: [{
type: ViewChild,
args: ['snapRendererContainer', { read: ViewContainerRef }]
}] } });
/*
* Public API Surface of ng-virtual-list
*/
/**
* Generated bundle index. Do not edit.
*/
export { Directions, NgVirtualListComponent, NgVirtualListItemComponent, ScrollEvent, SnappingMethods, debounce, toggleClassName };
//# sourceMappingURL=ng-virtual-list.mjs.map