UNPKG

ng-virtual-list

Version:

Maximum performance for extremely large lists.<br/> Animation of elements is supported.

2,056 lines 94.1 kB
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