UNPKG

@rx-angular/template

Version:

**Fully** Reactive Component Template Rendering in Angular. @rx-angular/template aims to be a reflection of Angular's built in renderings just reactive.

2,833 lines 119 kB
import * as i0 from '@angular/core';
import { Directive, InjectionToken, Injectable, inject, Input, isSignal, IterableDiffers, ChangeDetectorRef, NgZone, Injector, ViewContainerRef, ErrorHandler, ElementRef, Component, ViewEncapsulation, ChangeDetectionStrategy, ViewChild, ContentChild, Output } from '@angular/core';
import { RxDefaultListViewContext } from '@rx-angular/cdk/template';
import { Subject, of, Observable, from, ReplaySubject, combineLatest, merge, isObservable, NEVER, concat, defer } from 'rxjs';
import { coalesceWith } from '@rx-angular/cdk/coalescing';
import { distinctUntilChanged, takeUntil, filter, map, pairwise, tap, finalize, startWith, switchMap, groupBy, mergeMap, exhaustMap, take, takeWhile, shareReplay, switchAll, ignoreElements, catchError } from 'rxjs/operators';
import { getZoneUnPatchedApi } from '@rx-angular/cdk/internals/core';
import { requestAnimationFrame, cancelAnimationFrame, Promise as Promise$1 } from '@rx-angular/cdk/zone-less/browser';
import { toObservable } from '@angular/core/rxjs-interop';
import { coerceObservableWith } from '@rx-angular/cdk/coercing';
import { RxStrategyProvider, strategyHandling, onStrategy } from '@rx-angular/cdk/render-strategies';
import { NgIf, DOCUMENT } from '@angular/common';

/**
 * @Directive RxVirtualScrollStrategy
 *
 * @description
 * Abstract implementation for the actual implementations of the ScrollStrategies
 * being consumed by `*rxVirtualFor` and `rx-virtual-scroll-viewport`.
 *
 * This is one of the core parts for the virtual scrolling implementation. It has
 * to determine the `ListRange` being rendered to the DOM as well as managing
 * the layouting task for the `*rxVirtualFor` directive.
 *
 * @docsCategory RxVirtualFor
 * @docsPage RxVirtualFor
 * @publicApi
 */
class RxVirtualScrollStrategy {
    constructor() {
        /**
         * @description
         *
         * Emits whenever an update to a single view was rendered
         */
        this.viewRenderCallback = new Subject();
    }
    /** @internal */
    get isStable() {
        return of(true);
    }
    /** @internal */
    getElement(view) {
        if (this.nodeIndex !== undefined) {
            return view.rootNodes[this.nodeIndex];
        }
        const rootNode = view.rootNodes[0];
        this.nodeIndex = rootNode instanceof HTMLElement ? 0 : 1;
        return view.rootNodes[this.nodeIndex];
    }
    /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: RxVirtualScrollStrategy, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
    /** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.0.0", type: RxVirtualScrollStrategy, isStandalone: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: RxVirtualScrollStrategy, decorators: [{
            type: Directive
        }] });
/** @internal */
class RxVirtualScrollViewport {
    /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: RxVirtualScrollViewport, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
    /** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.0.0", type: RxVirtualScrollViewport, isStandalone: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: RxVirtualScrollViewport, decorators: [{
            type: Directive
        }] });
/** @internal */
class RxVirtualViewRepeater {
    /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: RxVirtualViewRepeater, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
    /** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.0.0", type: RxVirtualViewRepeater, isStandalone: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: RxVirtualViewRepeater, decorators: [{
            type: Directive
        }] });
/** @internal */
class RxVirtualForViewContext extends RxDefaultListViewContext {
    constructor(item, rxVirtualForOf, customProps) {
        super(item, customProps);
        this.rxVirtualForOf = rxVirtualForOf;
    }
}
class RxVirtualScrollElement {
    /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: RxVirtualScrollElement, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
    /** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.0.0", type: RxVirtualScrollElement, isStandalone: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: RxVirtualScrollElement, decorators: [{
            type: Directive
        }] });

function toBoolean(input) {
    return input != null && `${input}` !== 'false';
}
function unpatchedAnimationFrameTick() {
    return new Observable((observer) => {
        const tick = requestAnimationFrame(() => {
            observer.next();
            observer.complete();
        });
        return () => {
            cancelAnimationFrame(tick);
        };
    });
}
function unpatchedMicroTask() {
    return from(Promise$1.resolve());
}
function unpatchedScroll(el) {
    return new Observable((observer) => {
        const listener = () => observer.next();
        getZoneUnPatchedApi(el, 'addEventListener').call(el, 'scroll', listener, {
            passive: true,
        });
        return () => {
            getZoneUnPatchedApi(el, 'removeEventListener').call(el, 'scroll', listener, { passive: true });
        };
    });
}
/**
 * @description
 *
 * calculates the correct scrollTop value in which the rx-virtual-scroll-viewport
 * is actually visible
 */
function parseScrollTopBoundaries(scrollTop, offset, contentSize, containerSize) {
    const scrollTopWithOutOffset = scrollTop - offset;
    const maxSize = Math.max(contentSize - containerSize, containerSize);
    const maxScrollTop = Math.max(contentSize, containerSize);
    const adjustedScrollTop = Math.max(0, scrollTopWithOutOffset);
    const scrollTopAfterOffset = adjustedScrollTop - maxSize;
    return {
        scrollTopWithOutOffset,
        scrollTopAfterOffset,
        scrollTop: Math.min(adjustedScrollTop, maxScrollTop),
    };
}
/**
 * @description
 *
 * Calculates the visible size of the rx-virtual-scroll-viewport container. It
 * accounts for the fact that the viewport can partially or fully be out of viewport because
 * static contents that are living between the boundaries of rx-virtual-scroll-viewport
 * and its scrollable element.
 */
function calculateVisibleContainerSize(containerSize, scrollTopWithOutOffset, scrollTopAfterOffset) {
    let clamped = containerSize;
    if (scrollTopWithOutOffset < 0) {
        clamped = Math.max(0, containerSize + scrollTopWithOutOffset);
    }
    else if (scrollTopAfterOffset > 0) {
        clamped = Math.max(0, containerSize - scrollTopAfterOffset);
    }
    return clamped;
}

/** Injection token to be used to override the default options. */
const RX_VIRTUAL_SCROLL_DEFAULT_OPTIONS = new InjectionToken('rx-virtual-scrolling-default-options', {
    providedIn: 'root',
    factory: RX_VIRTUAL_SCROLL_DEFAULT_OPTIONS_FACTORY,
});
/** @internal */
function RX_VIRTUAL_SCROLL_DEFAULT_OPTIONS_FACTORY() {
    return {
        runwayItems: DEFAULT_RUNWAY_ITEMS,
        runwayItemsOpposite: DEFAULT_RUNWAY_ITEMS_OPPOSITE,
        templateCacheSize: DEFAULT_TEMPLATE_CACHE_SIZE,
        itemSize: DEFAULT_ITEM_SIZE,
    };
}
/** @internal */
const DEFAULT_TEMPLATE_CACHE_SIZE = 20;
/** @internal */
const DEFAULT_ITEM_SIZE = 50;
/** @internal */
const DEFAULT_RUNWAY_ITEMS = 10;
/** @internal */
const DEFAULT_RUNWAY_ITEMS_OPPOSITE = 2;

class RxaResizeObserver {
    constructor() {
        this.resizeObserver = new ResizeObserver((events) => {
            this.viewsResized$.next(events);
        });
        /** @internal */
        this.viewsResized$ = new Subject();
    }
    observeElement(element, options) {
        this.resizeObserver.observe(element, options);
        return new Observable((observer) => {
            const inner = this.viewsResized$.subscribe((events) => {
                const event = events.find((event) => event.target === element);
                if (event) {
                    observer.next(event);
                }
            });
            return () => {
                this.resizeObserver.unobserve(element);
                inner.unsubscribe();
            };
        });
    }
    destroy() {
        this.resizeObserver.disconnect();
    }
    /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: RxaResizeObserver, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
    /** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: RxaResizeObserver }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: RxaResizeObserver, decorators: [{
            type: Injectable
        }] });

const defaultSizeExtract = (entry) => entry.borderBoxSize[0].blockSize;
/**
 * @Directive AutosizeVirtualScrollStrategy
 *
 * @description
 *
 * The `AutosizeVirtualScrollStrategy` provides a twitter-like virtual-scrolling
 * experience. It is able to render and position items based on their individual
 * size. It is comparable to \@angular/cdk/experimental `AutosizeVirtualScrollStrategy`, but
 * with a high performant layouting technique and more features.
 *
 * On top of this the `AutosizeVirtualScrollStrategy` is leveraging the native
 * `ResizeObserver` in order to detect size changes for each individual view
 * rendered to the DOM and properly re-position accordingly.
 *
 * In order to provide top-notch runtime performance the `AutosizeVirtualScrollStrategy`
 * builds up caches that prevent DOM interactions whenever possible. Once a view
 * was visited, its properties will be stored instead of re-read from the DOM again as
 * this can potentially lead to unwanted forced reflows.
 *
 * @docsCategory RxVirtualFor
 * @docsPage RxVirtualFor
 * @publicApi
 */
class AutoSizeVirtualScrollStrategy extends RxVirtualScrollStrategy {
    constructor() {
        super(...arguments);
        this.defaults = inject(RX_VIRTUAL_SCROLL_DEFAULT_OPTIONS, {
            optional: true,
        });
        /**
         * @description
         * The amount of items to render upfront in scroll direction
         */
        this.runwayItems = this.defaults?.runwayItems ?? 10;
        /**
         * @description
         * The amount of items to render upfront in reverse scroll direction
         */
        this.runwayItemsOpposite = this.defaults?.runwayItemsOpposite ?? 2;
        /**
         * @description
         * The default size of the items being rendered. The autosized strategy will assume
         * this size for items it doesn't know yet. For the smoothest experience,
         * you provide the mean size of all items being rendered - if possible of course.
         *
         * As soon as rxVirtualFor is able to also render actual tombstone items, this
         * will be the size of a tombstone item being rendered before the actual item
         * is inserted into its position.
         */
        this.tombstoneSize = this.defaults?.itemSize ?? 50;
        /**
         * @description
         * When enabled, the autosized scroll strategy attaches a `ResizeObserver`
         * to every view within the given renderedRange. If your views receive
         * dimension changes that are not caused by list updates, this is a way to
         * still track height changes. This also applies to resize events of the whole
         * document.
         */
        this.withResizeObserver = true;
        /**
         * @description
         * When enabled, the scroll strategy stops removing views from the viewport,
         * instead it only adds views. This setting can be changed on the fly. Views will be added in both directions
         * according to the user interactions.
         */
        this.appendOnly = false;
        /**
         * @description
         * When enabled, the autosized scroll strategy removes css styles that
         * prevent the scrollbar from being in sync with the input device.
         * Use with caution, as this can lead to extremely weird scroll behavior
         * on chromium based browsers when the rendered views differ
         * in dimensions too much or change dimensions heavily.
         */
        this.withSyncScrollbar = false;
        /**
         * @description
         * If this flag is true, the virtual scroll strategy maintains the scrolled item when new data
         * is prepended to the list. This is very useful when implementing a reversed infinite scroller, that prepends
         * data instead of appending it
         */
        this.keepScrolledIndexOnPrepend = false;
        /** @internal */
        this.viewport = null;
        /** @internal */
        this.viewRepeater = null;
        /** @internal */
        this._contentSize$ = new ReplaySubject(1);
        /** @internal */
        this.contentSize$ = this._contentSize$.asObservable();
        /** @internal */
        this._contentSize = 0;
        /** @internal */
        this._renderedRange$ = new Subject();
        /** @internal */
        this.renderedRange$ = this._renderedRange$.asObservable();
        /** @internal */
        this._renderedRange = { start: 0, end: 0 };
        /** @internal */
        this.positionedRange = { start: 0, end: 0 };
        /** @internal */
        this._scrolledIndex$ = new ReplaySubject(1);
        /** @internal */
        this.scrolledIndex$ = this._scrolledIndex$.pipe(distinctUntilChanged());
        /**
         * @internal
         * The action used to kick off the scroll process
         */
        this.scrollToTrigger$ = new Subject();
        /** @internal */
        this._scrolledIndex = 0;
        /**
         * is set, when scrollToIndex is called
         * @internal
         * */
        this._scrollToIndex = null;
        /** @internal */
        this.containerSize = 0;
        /** @internal */
        this.contentLength = 0;
        /** @internal */
        this._virtualItems = [];
        /** @internal */
        this.scrollTop = 0;
        /** @internal */
        this.scrollTopWithOutOffset = 0;
        /** @internal */
        this.scrollTopAfterOffset = 0;
        /** @internal */
        this.viewportOffset = 0;
        /** @internal */
        this.direction = 'down';
        /** @internal */
        this.anchorScrollTop = 0;
        /** @internal */
        this.anchorItem = {
            index: 0,
            offset: 0,
        };
        /** @internal */
        this.lastScreenItem = {
            index: 0,
            offset: 0,
        };
        /** @internal */
        this.waitForScroll = false;
        /** @internal */
        this.isStable$ = new ReplaySubject(1);
        /** @internal */
        this.detached$ = new Subject();
        /** @internal */
        this.resizeObserver = inject(RxaResizeObserver, { self: true });
        /** @internal */
        this.recalculateRange$ = new Subject();
    }
    /** @internal */
    set contentSize(size) {
        this._contentSize = size;
        this._contentSize$.next(size);
    }
    get contentSize() {
        return this._contentSize;
    }
    /** @internal */
    set renderedRange(range) {
        this._renderedRange = range;
        this._renderedRange$.next(range);
    }
    /** @internal */
    get renderedRange() {
        return this._renderedRange;
    }
    /** @internal */
    get scrolledIndex() {
        return this._scrolledIndex;
    }
    /** @internal */
    set scrolledIndex(index) {
        this._scrolledIndex = index;
        this._scrolledIndex$.next(index);
    }
    /** @internal */
    until$() {
        return (o$) => o$.pipe(takeUntil(this.detached$));
    }
    /** @internal */
    get extractSize() {
        return this.resizeObserverConfig?.extractSize ?? defaultSizeExtract;
    }
    /** @internal */
    get isStable() {
        return this.isStable$.pipe(filter((w) => w));
    }
    /** @internal */
    ngOnChanges(changes) {
        if ((changes['runwayItemsOpposite'] &&
            !changes['runwayItemsOpposite'].firstChange) ||
            (changes['runwayItems'] && !changes['runwayItems'].firstChange)) {
            this.recalculateRange$.next();
        }
        if (changes['withSyncScrollbar']) {
            this.updateScrollElementClass();
        }
    }
    /** @internal */
    ngOnDestroy() {
        this.detach();
    }
    /** @internal */
    attach(viewport, viewRepeater) {
        this.viewport = viewport;
        this.viewRepeater = viewRepeater;
        this.updateScrollElementClass();
        this.maintainVirtualItems();
        this.calcRenderedRange();
        this.positionElements();
        this.listenToScrollTrigger();
    }
    /** @internal */
    detach() {
        this.updateScrollElementClass(false);
        this.viewport = null;
        this.viewRepeater = null;
        this._virtualItems = [];
        this.resizeObserver.destroy();
        this.detached$.next();
    }
    scrollToIndex(index, behavior) {
        const _index = Math.min(Math.max(index, 0), Math.max(0, this.contentLength - 1));
        if (_index !== this.scrolledIndex) {
            const scrollTop = this.calcInitialPosition(_index);
            this._scrollToIndex = _index;
            this.scrollToTrigger$.next({ scrollTop, behavior });
        }
    }
    scrollTo(scrollTo, behavior) {
        this.waitForScroll =
            scrollTo !== this.scrollTop && this.contentSize > this.containerSize;
        if (this.waitForScroll) {
            this.isStable$.next(false);
        }
        this.viewport.scrollTo(this.viewportOffset + scrollTo, behavior);
    }
    /**
     * starts the subscriptions that maintain the virtualItems array on changes
     * to the underlying dataset
     * @internal
     */
    maintainVirtualItems() {
        // reset virtual viewport when opposite orientation to the scroll direction
        // changes, as we have to expect dimension changes for all items when this
        // happens. This could also be configurable as it maybe costs performance
        this.viewport.containerRect$.pipe(map(({ width }) => width), distinctUntilChanged(), filter(() => this.renderedRange.end > 0 && this._virtualItems.length > 0), this.until$()).subscribe(() => {
            // reset because we have no idea how items will behave
            let i = 0;
            while (i < this.renderedRange.start) {
                this._virtualItems[i].cached = false;
                i++;
            }
            i = this.renderedRange.end;
            while (i < this.contentLength - 1) {
                this._virtualItems[i].cached = false;
                i++;
            }
        });
        // synchronises the values with the virtual viewport we've built up
        // it might get costy when having > 100k elements, it's still faster than
        // the IterableDiffer approach, especially on move operations
        const itemCache = new Map();
        const trackBy = this.viewRepeater._trackBy ?? ((i, item) => item);
        this.renderedRange$
            .pipe(pairwise(), this.until$())
            .subscribe(([oldRange, newRange]) => {
            let i = oldRange.start;
            if (i < this._virtualItems.length) {
                for (i; i < Math.min(this._virtualItems.length, oldRange.end); i++) {
                    if (i < newRange.start || i >= newRange.end) {
                        this._virtualItems[i].position = undefined;
                    }
                }
            }
        });
        this.viewRepeater.values$.pipe(this.until$(), tap((values) => {
            const dataArr = Array.isArray(values)
                ? values
                : values
                    ? Array.from(values)
                    : [];
            const existingIds = new Set();
            let size = 0;
            const dataLength = dataArr.length;
            const virtualItems = new Array(dataLength);
            let anchorItemIndex = this.anchorItem.index;
            const keepScrolledIndexOnPrepend = this.keepScrolledIndexOnPrepend &&
                dataArr.length > 0 &&
                itemCache.size > 0;
            for (let i = 0; i < dataLength; i++) {
                const item = dataArr[i];
                const id = trackBy(i, item);
                const cachedItem = itemCache.get(id);
                if (cachedItem === undefined) {
                    // add
                    virtualItems[i] = { size: 0 };
                    itemCache.set(id, { item: dataArr[i], index: i });
                    if (i <= anchorItemIndex) {
                        anchorItemIndex++;
                    }
                }
                else if (cachedItem.index !== i) {
                    // move
                    virtualItems[i] = this._virtualItems[cachedItem.index];
                    virtualItems[i].position = undefined;
                    itemCache.set(id, { item: dataArr[i], index: i });
                }
                else {
                    // update
                    // todo: properly determine update (Object.is?)
                    virtualItems[i] = this._virtualItems[i];
                    // if index is not part of rendered range, remove cache
                    if (!this.withResizeObserver ||
                        i < this.renderedRange.start ||
                        i >= this.renderedRange.end) {
                        virtualItems[i].cached = false;
                    }
                    itemCache.set(id, { item: dataArr[i], index: i });
                }
                existingIds.add(id);
                size += virtualItems[i].size || this.tombstoneSize;
            }
            this._virtualItems = virtualItems;
            // sync delete operations
            if (itemCache.size > dataLength) {
                itemCache.forEach((v, k) => {
                    if (!existingIds.has(k)) {
                        itemCache.delete(k);
                    }
                });
            }
            existingIds.clear();
            this.contentLength = dataLength;
            if (keepScrolledIndexOnPrepend &&
                this.anchorItem.index !== anchorItemIndex) {
                this.scrollToIndex(anchorItemIndex);
            }
            else if (dataLength === 0) {
                this.anchorItem = {
                    index: 0,
                    offset: 0,
                };
                this._renderedRange = {
                    start: 0,
                    end: 0,
                };
                this.scrollTo(0);
                this.scrollTop = this.anchorScrollTop = 0;
            }
            else if (dataLength < this._renderedRange.end) {
                this.anchorItem = this.calculateAnchoredItem({
                    index: dataLength,
                    offset: 0,
                }, Math.max(-size, -calculateVisibleContainerSize(this.containerSize, this.scrollTopWithOutOffset, this.scrollTopAfterOffset)));
                this.calcAnchorScrollTop();
                this._renderedRange = {
                    start: Math.max(0, this.anchorItem.index - this.runwayItems),
                    end: dataLength,
                };
                this.scrollTo(size);
                this.scrollTop = this.anchorScrollTop;
            }
            this.contentSize = size;
        }), finalize(() => itemCache.clear())).subscribe();
    }
    /**
     * listen to triggers that should change the renderedRange
     * @internal
     */
    calcRenderedRange() {
        let removeScrollAnchorOnNextScroll = false;
        const onlyTriggerWhenStable = () => (o$) => o$.pipe(filter(() => this.renderedRange.end === 0 ||
            (this.scrollTop === this.anchorScrollTop &&
                this._scrollToIndex === null)));
        combineLatest([
            this.viewport.containerRect$.pipe(map(({ height }) => {
                this.containerSize = height;
                return height;
            }), distinctUntilChanged(), onlyTriggerWhenStable()),
            this.viewport.elementScrolled$.pipe(startWith(void 0), tap(() => {
                this.viewportOffset = this.viewport.measureOffset();
                const { scrollTop, scrollTopWithOutOffset, scrollTopAfterOffset } = parseScrollTopBoundaries(this.viewport.getScrollTop(), this.viewportOffset, this._contentSize, this.containerSize);
                this.direction =
                    scrollTopWithOutOffset > this.scrollTopWithOutOffset
                        ? 'down'
                        : 'up';
                this.scrollTopWithOutOffset = scrollTopWithOutOffset;
                this.scrollTopAfterOffset = scrollTopAfterOffset;
                this.scrollTop = scrollTop;
                if (removeScrollAnchorOnNextScroll) {
                    this._scrollToIndex = null;
                    removeScrollAnchorOnNextScroll = false;
                }
                else {
                    removeScrollAnchorOnNextScroll = this._scrollToIndex !== null;
                }
                this.waitForScroll = false;
            })),
            this._contentSize$.pipe(distinctUntilChanged(), onlyTriggerWhenStable()),
            this.recalculateRange$.pipe(onlyTriggerWhenStable(), startWith(void 0)),
        ])
            .pipe(
        // make sure to not over calculate things by coalescing all triggers to the next microtask
        coalesceWith(unpatchedMicroTask()), map(() => {
            const range = { start: 0, end: 0 };
            const delta = this.scrollTop - this.anchorScrollTop;
            if (this.scrollTop === 0) {
                this.anchorItem = { index: 0, offset: 0 };
            }
            else {
                this.anchorItem = this.calculateAnchoredItem(this.anchorItem, delta);
            }
            this.anchorScrollTop = this.scrollTop;
            this.scrolledIndex = this.anchorItem.index;
            this.lastScreenItem = this.calculateAnchoredItem(this.anchorItem, calculateVisibleContainerSize(this.containerSize, this.scrollTopWithOutOffset, this.scrollTopAfterOffset));
            if (this.direction === 'up') {
                range.start = Math.max(0, this.anchorItem.index - this.runwayItems);
                range.end = Math.min(this.contentLength, this.lastScreenItem.index + this.runwayItemsOpposite);
            }
            else {
                range.start = Math.max(0, this.anchorItem.index - this.runwayItemsOpposite);
                range.end = Math.min(this.contentLength, this.lastScreenItem.index + this.runwayItems);
            }
            if (this.appendOnly) {
                range.start = Math.min(this._renderedRange.start, range.start);
                range.end = Math.max(this._renderedRange.end, range.end);
            }
            return range;
        }))
            .pipe(this.until$())
            .subscribe((range) => {
            this.renderedRange = range;
            this.isStable$.next(!this.waitForScroll);
        });
    }
    /**
     * position elements after they are created/updated/moved or their dimensions
     * change from other sources
     * @internal
     */
    positionElements() {
        const viewsToObserve$ = new Subject();
        const positionByIterableChange$ = this.viewRepeater.renderingStart$.pipe(switchMap((batchedUpdates) => {
            // initialIndex tells us what will be the first index to be change detected
            // if it's not the first one, we maybe have to adjust the position
            // of all items in the viewport before this index
            const initialIndex = batchedUpdates.size
                ? batchedUpdates.values().next().value + this.renderedRange.start
                : this.renderedRange.start;
            let position = 0;
            let scrollToAnchorPosition = null;
            return this.viewRepeater.viewRendered$.pipe(tap(({ view, index: viewIndex, item }) => {
                const itemIndex = view.context.index;
                // this most of the time causes a forced reflow per rendered view.
                // it doesn't sound good, but it's still way more stable than
                // having one large reflow in a microtask after the actual
                // scheduler tick.
                // Right here, we can insert work into the task which is currently
                // executed as part of the concurrent scheduler tick.
                // causing the forced reflow here, also makes it count for the
                // schedulers frame budget. This way we will always run with the
                // configured FPS. The only case where this is not true is when rendering 1 single view
                // already explodes the budget
                const [, sizeDiff] = this.updateElementSize(view, itemIndex);
                const virtualItem = this._virtualItems[itemIndex];
                // before positioning the first view of this batch, calculate the
                // anchorScrollTop & initial position of the view
                if (itemIndex === initialIndex) {
                    this.calcAnchorScrollTop();
                    position = this.calcInitialPosition(itemIndex);
                    // if we receive a partial update and the current views position is
                    // new, we can safely assume that all positions from views before the current
                    // index are also off. We need to adjust them
                    if (initialIndex > this.renderedRange.start &&
                        virtualItem.position !== position) {
                        let beforePosition = position;
                        let i = initialIndex - 1;
                        while (i >= this.renderedRange.start) {
                            const view = this.getViewRef(i - this.renderedRange.start);
                            const virtualItem = this._virtualItems[i];
                            const element = this.getElement(view);
                            beforePosition -= virtualItem.size;
                            virtualItem.position = beforePosition;
                            this.positionElement(element, beforePosition);
                            i--;
                        }
                    }
                }
                else if (itemIndex < this.anchorItem.index && sizeDiff) {
                    this.anchorScrollTop += sizeDiff;
                }
                const size = virtualItem.size;
                // position current element if we need to
                if (virtualItem.position !== position) {
                    const element = this.getElement(view);
                    this.positionElement(element, position);
                    virtualItem.position = position;
                }
                if (this._scrollToIndex === itemIndex) {
                    scrollToAnchorPosition = position;
                }
                position += size;
                // immediately activate the ResizeObserver after initial positioning
                viewsToObserve$.next(view);
                this.viewRenderCallback.next({
                    index: itemIndex,
                    view,
                    item,
                });
                // after positioning the actual view, we also need to position all
                // views from the current index on until either the renderedRange.end
                // is hit or we hit an index that will anyway receive an update.
                // we can derive that information from the batchedUpdates index Set
                const { lastPositionedIndex: lastIndex, position: newPosition } = this.positionUnchangedViews({
                    viewIndex,
                    itemIndex,
                    batchedUpdates,
                    position,
                });
                position = newPosition;
                this.positionedRange.start = this.renderedRange.start;
                this.positionedRange.end = lastIndex + 1;
            }), coalesceWith(unpatchedMicroTask()), tap(() => {
                this.adjustContentSize(position);
                if (this._scrollToIndex === null) {
                    this.maybeAdjustScrollPosition();
                }
                else if (scrollToAnchorPosition != null) {
                    if (scrollToAnchorPosition !== this.anchorScrollTop) {
                        if (scrollToAnchorPosition >
                            this.contentSize - this.containerSize) {
                            // if the anchorItemPosition is larger than the maximum scrollPos,
                            // we want to scroll until the bottom.
                            // of course, we need to be sure all the items until the end are positioned
                            // until we are sure that we need to scroll to the bottom
                            if (this.renderedRange.end === this.positionedRange.end) {
                                this._scrollToIndex = null;
                                this.scrollTo(this.contentSize);
                            }
                        }
                        else {
                            this._scrollToIndex = null;
                            this.scrollTo(scrollToAnchorPosition);
                        }
                    }
                    else {
                        this._scrollToIndex = null;
                        this.maybeAdjustScrollPosition();
                    }
                }
            }));
        }));
        const positionByResizeObserver$ = viewsToObserve$.pipe(filter(() => this.withResizeObserver), groupBy((viewRef) => viewRef), mergeMap((o$) => o$.pipe(exhaustMap((viewRef) => this.observeViewSize$(viewRef)), tap(([index, viewIndex]) => {
            this.calcAnchorScrollTop();
            let position = this.calcInitialPosition(index);
            let viewIdx = viewIndex;
            if (this._virtualItems[index].position !== position) {
                // we want to reposition the whole viewport, when the current position has changed
                while (viewIdx > 0) {
                    viewIdx--;
                    position -=
                        this._virtualItems[this.getViewRef(viewIdx).context.index]
                            .size;
                }
            }
            else {
                // we only need to reposition everything from the next viewIndex on
                viewIdx++;
                position += this._virtualItems[index].size;
            }
            // position all views from the specified viewIndex
            while (viewIdx < this.viewRepeater.viewContainer.length) {
                const view = this.getViewRef(viewIdx);
                const itemIndex = view.context.index;
                const virtualItem = this._virtualItems[itemIndex];
                const element = this.getElement(view);
                this.updateElementSize(view, itemIndex);
                virtualItem.position = position;
                this.positionElement(element, position);
                position += virtualItem.size;
                viewIdx++;
            }
            this.maybeAdjustScrollPosition();
        }))));
        merge(positionByIterableChange$, positionByResizeObserver$)
            .pipe(this.until$())
            .subscribe();
    }
    /** listen to API initiated scroll triggers (e.g. initialScrollIndex) */
    listenToScrollTrigger() {
        this.scrollToTrigger$
            .pipe(switchMap((scrollTo) => 
        // wait until containerRect at least emitted once
        this.containerSize === 0
            ? this.viewport.containerRect$.pipe(map(() => scrollTo), take(1))
            : of(scrollTo)), this.until$())
            .subscribe(({ scrollTop, behavior }) => {
            this.scrollTo(scrollTop, behavior);
        });
    }
    /** @internal */
    adjustContentSize(position) {
        let newContentSize = position;
        for (let i = this.positionedRange.end; i < this._virtualItems.length; i++) {
            newContentSize += this.getItemSize(i);
        }
        this.contentSize = newContentSize;
    }
    /** @internal */
    observeViewSize$(viewRef) {
        const element = this.getElement(viewRef);
        return this.resizeObserver
            .observeElement(element, this.resizeObserverConfig?.options)
            .pipe(takeWhile((event) => event.target.isConnected &&
            !!this._virtualItems[viewRef.context.index]), map((event) => {
            const index = viewRef.context.index;
            const size = Math.round(this.extractSize(event));
            const diff = size - this._virtualItems[index].size;
            if (diff !== 0) {
                this._virtualItems[index].size = size;
                this._virtualItems[index].cached = true;
                this.contentSize += diff;
                return [index, this.viewRepeater.viewContainer.indexOf(viewRef)];
            }
            return null;
        }), filter((diff) => diff !== null &&
            diff[0] >= this.positionedRange.start &&
            diff[0] < this.positionedRange.end), takeUntil(merge(this.viewRepeater.viewRendered$, this.viewRepeater.renderingStart$).pipe(tap(() => {
            // we need to clean up the position property for views
            // that fall out of the renderedRange.
            const index = viewRef.context.index;
            if (this._virtualItems[index] &&
                (index < this.renderedRange.start ||
                    index >= this.renderedRange.end)) {
                this._virtualItems[index].position = undefined;
            }
        }), filter(() => this.viewRepeater.viewContainer.indexOf(viewRef) === -1))));
    }
    /**
     * @internal
     * heavily inspired by
     *   https://github.com/GoogleChromeLabs/ui-element-samples/blob/gh-pages/infinite-scroller/scripts/infinite-scroll.js
     */
    calculateAnchoredItem(initialAnchor, delta) {
        if (delta === 0)
            return initialAnchor;
        delta += initialAnchor.offset;
        let i = initialAnchor.index;
        const items = this._virtualItems;
        if (delta < 0) {
            while (delta < 0 && i > 0) {
                delta += this.getItemSize(i - 1);
                i--;
            }
        }
        else {
            while (delta > 0 && i < items.length && this.getItemSize(i) <= delta) {
                delta -= this.getItemSize(i);
                i++;
            }
        }
        return {
            index: Math.min(i, items.length),
            offset: delta,
        };
    }
    /** @internal */
    positionUnchangedViews({ viewIndex, itemIndex, batchedUpdates, position, }) {
        let _viewIndex = viewIndex + 1;
        let index = itemIndex + 1;
        let lastPositionedIndex = itemIndex;
        while (!batchedUpdates.has(_viewIndex) && index < this.renderedRange.end) {
            const virtualItem = this._virtualItems[index];
            if (position !== virtualItem.position) {
                const view = this.getViewRef(_viewIndex);
                const element = this.getElement(view);
                this.positionElement(element, position);
                virtualItem.position = position;
            }
            position += virtualItem.size;
            lastPositionedIndex = index;
            index++;
            _viewIndex++;
        }
        return { position, lastPositionedIndex };
    }
    /**
     * Adjust the scroll position when the anchorScrollTop differs from
     * the actual scrollTop.
     * Trigger a range recalculation if there is empty space
     *
     * @internal
     */
    maybeAdjustScrollPosition() {
        if (this.anchorScrollTop !== this.scrollTop) {
            this.scrollTo(this.anchorScrollTop);
        }
    }
    /** @internal */
    calcAnchorScrollTop() {
        this.anchorScrollTop = 0;
        for (let i = 0; i < this.anchorItem.index; i++) {
            this.anchorScrollTop += this.getItemSize(i);
        }
        this.anchorScrollTop += this.anchorItem.offset;
    }
    /** @internal */
    calcInitialPosition(start) {
        // Calculate position of starting node
        let pos = this.anchorScrollTop - this.anchorItem.offset;
        let i = this.anchorItem.index;
        while (i > start) {
            const itemSize = this.getItemSize(i - 1);
            pos -= itemSize;
            i--;
        }
        while (i < start) {
            const itemSize = this.getItemSize(i);
            pos += itemSize;
            i++;
        }
        return pos;
    }
    /** @internal */
    getViewRef(index) {
        return (this.viewRepeater.viewContainer.get(index));
    }
    /** @internal */
    updateElementSize(view, index) {
        const oldSize = this.getItemSize(index);
        const isCached = this._virtualItems[index].cached;
        const size = isCached
            ? oldSize
            : this.getElementSize(this.getElement(view));
        this._virtualItems[index].size = size;
        this._virtualItems[index].cached = true;
        return [size, size - oldSize];
    }
    /** @internal */
    getItemSize(index) {
        return this._virtualItems[index].size || this.tombstoneSize;
    }
    /** @internal */
    getElementSize(element) {
        return element.offsetHeight;
    }
    /** @internal */
    positionElement(element, scrollTop) {
        element.style.position = 'absolute';
        element.style.transform = `translateY(${scrollTop}px)`;
    }
    /** @internal */
    updateScrollElementClass(force = this.withSyncScrollbar) {
        const scrollElement = this.viewport?.getScrollElement?.();
        if (!!scrollElement &&
            scrollElement.classList.contains('rx-virtual-scroll-element')) {
            scrollElement.classList.toggle('rx-virtual-scroll-element--withSyncScrollbar', force);
        }
    }
    /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: AutoSizeVirtualScrollStrategy, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
    /** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "16.1.0", version: "19.0.0", type: AutoSizeVirtualScrollStrategy, isStandalone: true, selector: "rx-virtual-scroll-viewport[autosize]", inputs: { runwayItems: "runwayItems", runwayItemsOpposite: "runwayItemsOpposite", tombstoneSize: "tombstoneSize", resizeObserverConfig: "resizeObserverConfig", withResizeObserver: ["withResizeObserver", "withResizeObserver", toBoolean], appendOnly: ["appendOnly", "appendOnly", toBoolean], withSyncScrollbar: ["withSyncScrollbar", "withSyncScrollbar", toBoolean], keepScrolledIndexOnPrepend: ["keepScrolledIndexOnPrepend", "keepScrolledIndexOnPrepend", toBoolean] }, providers: [
            {
                provide: RxVirtualScrollStrategy,
                useExisting: AutoSizeVirtualScrollStrategy,
            },
            RxaResizeObserver,
        ], usesInheritance: true, usesOnChanges: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: AutoSizeVirtualScrollStrategy, decorators: [{
            type: Directive,
            args: [{
                    selector: 'rx-virtual-scroll-viewport[autosize]',
                    providers: [
                        {
                            provide: RxVirtualScrollStrategy,
                            useExisting: AutoSizeVirtualScrollStrategy,
                        },
                        RxaResizeObserver,
                    ],
                    standalone: true,
                }]
        }], propDecorators: { runwayItems: [{
                type: Input
            }], runwayItemsOpposite: [{
                type: Input
            }], tombstoneSize: [{
                type: Input
            }], resizeObserverConfig: [{
                type: Input
            }], withResizeObserver: [{
                type: Input,
                args: [{ transform: toBoolean }]
            }], appendOnly: [{
                type: Input,
                args: [{ transform: toBoolean }]
            }], withSyncScrollbar: [{
                type: Input,
                args: [{ transform: toBoolean }]
            }], keepScrolledIndexOnPrepend: [{
                type: Input,
                args: [{ transform: toBoolean }]
            }] } });

const defaultItemSize = () => DEFAULT_ITEM_SIZE;
/**
 * @Directive DynamicSizeVirtualScrollStrategy
 *
 * @description
 *
 * The `DynamicSizeVirtualScrollStrategy` is very similar to the `AutosizeVirtualScrollStrategy`.
 * It positions items based on a function determining its size.
 *
 * @docsCategory RxVirtualFor
 * @docsPage RxVirtualFor
 * @publicApi
 */
class DynamicSizeVirtualScrollStrategy extends RxVirtualScrollStrategy {
    constructor() {
        super(...arguments);
        this.defaults = inject(RX_VIRTUAL_SCROLL_DEFAULT_OPTIONS, {
            optional: true,
        });
        /**
         * @description
         * The amount of items to render upfront in scroll direction
         */
        this.runwayItems = this.defaults?.runwayItems ?? DEFAULT_RUNWAY_ITEMS;
        /**
         * @description
         * The amount of items to render upfront in reverse scroll direction
         */
        this.runwayItemsOpposite = this.defaults?.runwayItemsOpposite ?? DEFAULT_RUNWAY_ITEMS_OPPOSITE;
        /**
         * @description
         * When enabled, the scroll strategy stops removing views from the viewport,
         * instead it only adds views. This setting can be changed on the fly. Views will be added in both directions
         * according to the user interactions.
         */
        this.appendOnly = false;
        /**
         * @description
         * If this flag is true, the virtual scroll strategy maintains the scrolled item when new data
         * is prepended to the list. This is very useful when implementing a reversed infinite scroller, that prepends
         * data instead of appending it
         */
        this.keepScrolledIndexOnPrepend = false;
        this._itemSizeFn = defaultItemSize;
        /** @internal */
        this.waitForScroll = false;
        /** @internal */
        this.isStable$ = new ReplaySubject(1);
        /** @internal */
        this.viewport = null;
        /** @internal */
        this.viewRepeater = null;
        /** @internal */
        this._contentSize$ = new ReplaySubject(1);
        /** @internal */
        this.contentSize$ = this._contentSize$.asObservable();
        /** @internal */
        this._contentSize = 0;
        /** @internal */
        this._renderedRange$ = new ReplaySubject(1);
        /** @internal */
        this.renderedRange$ = this._renderedRange$.asObservable();
        /** @internal */
        this._renderedRange = { start: 0, end: 0 };
        /** @internal */
        this._scrolledIndex$ = new ReplaySubject(1);
        /** @internal */
        this.scrolledIndex$ = this._scrolledIndex$.pipe(distinctUntilChanged());
        this._scrolledIndex = 0;
        /** @internal */
        this.containerSize = 0;
        /** @internal */
        this._virtualItems = [];
        /** @internal */
        this.scrollTop = 0;
        /** @internal */
        this.scrollTopWithOutOffset = 0;
        /** @internal */
        this.scrollTopAfterOffset = 0;
        /** @internal */
        this.viewportOffset = 0;
        /** @internal */
        this.direction = 'down';
        /** @internal */
        this.anchorScrollTop = 0;
        /** @internal */
        this.anchorItem = {
            index: 0,
            offset: 0,
        };
        /** @internal */
        this.lastScreenItem = {
            index: 0,
            offset: 0,
        };
        /** @internal */
        this.detached$ = new Subject();
        /** @internal */
        this.recalculateRange$ = new Subject();
    }
    /**
     * @description
     * Function returning the size of an item
     */
    set itemSize(fn) {
        if (fn) {
            this._itemSizeFn = fn;
        }
    }
    get itemSize() {
        return this._itemSizeFn;
    }
    /** @internal */
    get isStable() {
        return this.isStable$.pipe(filter((w) => w));
    }
    /** @internal */
    set contentSize(size) {
        this._contentSize = size;
        this._contentSize$.next(size);
    }
    get contentSize() {
        return this._contentSize;
    }
    // range of items where size is known and doesn't need to be re-calculated
    /** @internal */
    set renderedRange(range) {
        this._renderedRange = range;
        this._renderedRange$.next(range);
    }
    /** @internal */
    get renderedRange() {
        return this._renderedRange;
    }
    /** @internal */
    set scrolledIndex(index) {
        this._scrolledIndex = index;
        this._scrolledIndex$.next(index);
    }
    get scrolledIndex() {
        return this._scrolledIndex;
    }
    /** @internal */
    get contentLength() {
        return this._virtualItems.length;
    }
    /** @internal */
    until$() {
        return (o$) => o$.pipe(takeUntil(this.detached$));
    }
    /** @internal */
    ngOnChanges(changes) {
        if ((changes['runwayItemsOpposite'] &&
            !changes['runwayItemsOpposite'].firstChange) ||
            (changes['runwayItems'] && !changes['runwayItems'].firstChange)) {
            this.recalculateRange$.next();
        }
    }
    /** @internal */
    ngOnDestroy() {
        this.detach();
    }
    /** @internal */
    attach(viewport, viewRepeater) {
        this.viewport = viewport;
        this.viewRepeater = viewRepeater;
        this.maintainVirtualItems();
        this.calcRenderedRange();
        this.positionElements();
    }
    /** @internal */
    detach() {
        this.viewport = null;
        this.viewRepeater = null;
        this._virtualItems = [];
        this.detached$.next();
    }
    scrollToIndex(index, behavior) {
        const _index = Math.min(Math.max(index, 0), this.contentLength - 1);
        let scrollTo = 0;
        for (let i = 0; i < _index; i++) {
            scrollTo += this._virtualItems[i].size;
        }
        this.scrollTo(scrollTo, behavior);
    }
    scrollTo(scrollTo, behavior) {
        this.waitForScroll =
            scrollTo !== this.scrollTop && this.contentSize > this.containerSize;
        if (this.waitForScroll) {
            this.isStable$.next(false);
        }
        this.viewport.scrollTo(this.viewportOffset + scrollTo, behavior);
    }
    /** @internal */
    maintainVirtualItems() {
        const valueArray$ = this.viewRepeater.values$.pipe(map((values) => Array.isArray(values)
            ? values
            : values != null
                ? Array.from(values)
                : []), shareReplay({ bufferSize: 1, refCount: true }));
        valueArray$.pipe(this.until$()).subscribe((dataArr) => {
            const dataLength = dataArr.length;
            if (!dataLength) {
                this._virtualItems = [];
                this.contentSize = 0;
                this._renderedRange = {
                    start: 0,
                    end: 0,
                };
                this.anchorItem = {
                    index: 0,
                    offset: 0,
                };
                this.recalculateRange$.next();
            }
            else {
                let shouldRecalculateRange = false;
                let contentSize = 0;
                for (let i = 0; i < dataArr.length; i++) {
                    const oldSize = this._virtualItems[i]?.size;
                    const newSize = this.itemSize(dataArr[i]);
                    contentSize += newSize;
                    if (oldSize === undefined || oldSize !== newSize) {
                        this._virtualItems[i] = { size: newSize };
                        if (!shouldRecalculateRange &&
                            (!this.contentSize ||
                                (i >= this.renderedRange.start && i < this.renderedRange.end))) {
                            shouldRecalculateRange = true;
                        }
                    }
                }
                if (dataLength < this._renderedRange.end) {
                    this.anchorItem = this.calculateAnchoredItem({
                        index: dataLength,
                        offset: 0,
                    }, -calculateVisibleContainerSize(this.containerSize, this.scrollTopWithOutOffset, this.scrollTopAfterOffset));
                    this._renderedRange.start = Math.max(0, this.anchorItem.index - this.runwayItems);
                    this._renderedRange.end = dataLength;
                    this.calcAnchorScrollTop();
                    this.scrollTo(contentSize);
                    this.scrollTop = this.anchorScrollTop;
                }
                this.contentSize = contentSize;
                if (shouldRecalculateRange) {
                    this.recalculateRange$.next();
                }
            }
        });
        let valueCache = {};
        /*
         * when keepScrolledIndexOnPrepend is active, we need to listen to data changes and figure out what was appended
         * before the last scrolledToItem
         */
        valueArray$
            .pipe(
        // TODO: this might cause issues when turning on/off at runtime
        filter(() => this.keepScrolledIndexOnPrepend), this.until$())
            .subscribe((valueArray) => {
            const trackBy = this.viewRepeater._trackBy;
            let scrollTo = this.scrolledIndex;
            const dataLength = valueArray.length;
            const oldDataLength = Object.keys(valueCache).length;
            if (oldDataLength > 0) {
                let i = 0;
                // check for each item from the last known scrolledIndex if it's an insert
                for (i; i <= scrollTo && i < dataLength; i++) {
                    // item is not in the valueCache, so it was added
                    if (!valueCache[trackBy(i, valueArray[i])]) {
                        scrollTo++;
                    }
                }
            }
            valueCache = {};
            valueArray.forEach((v, i) => (valueCache[trackBy(i, v)] = v));
            if (scrollTo !== this.scrolledIndex) {
                this.scrollToIndex(scrollTo);
            }
        });
    }
    /** @internal */
    calcRenderedRange() {
        combineLatest([
            this.viewport.containerRect$.pipe(map(({ height }) => {
                this.containerSize = height;
                return height;
            }), distinctUntilChanged()),
            this.viewport.elementScrolled$.pipe(startWith(void 0), tap(() => {
                this.viewportOffset = this.viewport.measureOffset();
                const { scrollTop, scrollTopWithOutOffset, scrollTopAfterOffset } = parseScrollTopBoundaries(this.viewport.getScrollTop(), this.viewportOffset, this._contentSize, this.containerSize);
                this.direction =
                    scrollTopWithOutOffset > this.scrollTopWithOutOffset
                        ? 'down'
                        : 'up';
                this.scrollTopWithOutOffset = scrollTopWithOutOffset;
                this.scrollTopAfterOffset = scrollTopAfterOffset;
                this.scrollTop = scrollTop;
                this.waitForScroll = false;
            })),
            this._contentSize$.pipe(distinctUntilChanged()),
            this.recalculateRange$.pipe(startWith(void 0)),
        ])
            .pipe(
        // make sure to not over calculate things by coalescing all triggers to the next microtask
        coalesceWith(unpatchedMicroTask()), map(() => {
            const range = { start: 0, end: 0 };
            const length = this.contentLength;
            const delta = this.scrollTop - this.anchorScrollTop;
            if (this.scrollTop == 0) {
                this.anchorItem = { index: 0, offset: 0 };
            }
            else {
                this.anchorItem = this.calculateAnchoredItem(this.anchorItem, delta);
            }
            this.scrolledIndex = this.anchorItem.index;
            this.anchorScrollTop = this.scrollTop;
            this.lastScreenItem = this.calculateAnchoredItem(this.anchorItem, calculateVisibleContainerSize(this.containerSize, this.scrollTopWithOutOffset, this.scrollTopAfterOffset));
            if (this.direction === 'up') {
                range.start = Math.max(0, this.anchorItem.index - this.runwayItems);
                range.end = Math.min(length, this.lastScreenItem.index + this.runwayItemsOpposite);
            }
            else {
                range.start = Math.max(0, this.anchorItem.index - this.runwayItemsOpposite);
                range.end = Math.min(length, this.lastScreenItem.index + this.runwayItems);
            }
            if (this.appendOnly) {
                range.start = Math.min(this._renderedRange.start, range.start);
                range.end = Math.max(this._renderedRange.end, range.end);
            }
            return range;
        }))
            .pipe(this.until$())
            .subscribe((range) => {
            this.renderedRange = range;
            this.isStable$.next(!this.waitForScroll);
        });
    }
    /** @internal */
    positionElements() {
        this.viewRepeater.renderingStart$.pipe(switchMap((batchedUpdates) => {
            const renderedRange = this.renderedRange;
            const adjustIndexWith = renderedRange.start;
            const initialIndex = batchedUpdates.size
                ? batchedUpdates.values().next().value + this.renderedRange.start
                : this.renderedRange.start;
            let position = this.calcInitialPosition(initialIndex);
            return this.viewRepeater.viewRendered$.pipe(tap(({ view, index: viewIndex, item }) => {
                const index = viewIndex + adjustIndexWith;
                const size = this.getItemSize(index);
                this.positionElement(this.getElement(view), position);
                position += size;
                this.viewRenderCallback.next({
                    index,
                    view,
                    item,
                });
            }));
        }), this.until$()).subscribe();
    }
    /**
     * @internal
     * heavily inspired by
     *   https://github.com/GoogleChromeLabs/ui-element-samples/blob/gh-pages/infinite-scroller/scripts/infinite-scroll.js
     */
    calculateAnchoredItem(initialAnchor, delta) {
        if (delta == 0)
            return initialAnchor;
        delta += initialAnchor.offset;
        let i = initialAnchor.index;
        const items = this._virtualItems;
        if (delta < 0) {
            while (delta < 0 && i > 0) {
                delta += items[i - 1].size;
                i--;
            }
        }
        else {
            while (delta > 0 && i < items.length && items[i].size <= delta) {
                delta -= items[i].size;
                i++;
            }
        }
        return {
            index: Math.min(i, items.length),
            offset: delta,
        };
    }
    /** @internal */
    calcInitialPosition(start) {
        // Calculate position of starting node
        let pos = this.anchorScrollTop - this.anchorItem.offset;
        let i = this.anchorItem.index;
        while (i > start) {
            const itemSize = this.getItemSize(i - 1);
            pos -= itemSize;
            i--;
        }
        while (i < start) {
            const itemSize = this.getItemSize(i);
            pos += itemSize;
            i++;
        }
        return pos;
    }
    /** @internal */
    calcAnchorScrollTop() {
        this.anchorScrollTop = 0;
        for (let i = 0; i < this.anchorItem.index; i++) {
            this.anchorScrollTop += this.getItemSize(i);
        }
        this.anchorScrollTop += this.anchorItem.offset;
    }
    /** @internal */
    getItemSize(index) {
        return this._virtualItems[index].size;
    }
    /** @internal */
    positionElement(element, scrollTop) {
        element.style.position = 'absolute';
        element.style.transform = `translateY(${scrollTop}px)`;
    }
    /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: DynamicSizeVirtualScrollStrategy, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
    /** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "16.1.0", version: "19.0.0", type: DynamicSizeVirtualScrollStrategy, isStandalone: true, selector: "rx-virtual-scroll-viewport[dynamic]", inputs: { runwayItems: "runwayItems", runwayItemsOpposite: "runwayItemsOpposite", appendOnly: ["appendOnly", "appendOnly", toBoolean], keepScrolledIndexOnPrepend: ["keepScrolledIndexOnPrepend", "keepScrolledIndexOnPrepend", toBoolean], itemSize: ["dynamic", "itemSize"] }, providers: [
            {
                provide: RxVirtualScrollStrategy,
                useExisting: DynamicSizeVirtualScrollStrategy,
            },
        ], usesInheritance: true, usesOnChanges: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: DynamicSizeVirtualScrollStrategy, decorators: [{
            type: Directive,
            args: [{
                    selector: 'rx-virtual-scroll-viewport[dynamic]',
                    providers: [
                        {
                            provide: RxVirtualScrollStrategy,
                            useExisting: DynamicSizeVirtualScrollStrategy,
                        },
                    ],
                    standalone: true,
                }]
        }], propDecorators: { runwayItems: [{
                type: Input
            }], runwayItemsOpposite: [{
                type: Input
            }], appendOnly: [{
                type: Input,
                args: [{ transform: toBoolean }]
            }], keepScrolledIndexOnPrepend: [{
                type: Input,
                args: [{ transform: toBoolean }]
            }], itemSize: [{
                type: Input,
                args: ['dynamic']
            }] } });

/**
 * @Directive FixedSizeVirtualScrollStrategy
 *
 * @description
 *
 * The `FixedSizeVirtualScrollStrategy` provides a very performant way of rendering
 * items of a given size. It is comparable to \@angular/cdk `FixedSizeVirtualScrollStrategy`, but
 * with a high performant layouting technique.
 *
 * @docsCategory RxVirtualFor
 * @docsPage RxVirtualFor
 * @publicApi
 */
class FixedSizeVirtualScrollStrategy extends RxVirtualScrollStrategy {
    constructor() {
        super(...arguments);
        this.defaults = inject(RX_VIRTUAL_SCROLL_DEFAULT_OPTIONS, {
            optional: true,
        });
        this._itemSize = DEFAULT_ITEM_SIZE;
        /**
         * @description
         * When enabled, the scroll strategy stops removing views from the viewport,
         * instead it only adds views. This setting can be changed on the fly. Views will be added in both directions
         * according to the user interactions.
         */
        this.appendOnly = false;
        /**
         * @description
         * The amount of items to render upfront in scroll direction
         */
        this.runwayItems = this.defaults?.runwayItems ?? DEFAULT_RUNWAY_ITEMS;
        /**
         * @description
         * The amount of items to render upfront in reverse scroll direction
         */
        this.runwayItemsOpposite = this.defaults?.runwayItemsOpposite ?? DEFAULT_RUNWAY_ITEMS_OPPOSITE;
        /**
         * @description
         * If this flag is true, the virtual scroll strategy maintains the scrolled item when new data
         * is prepended to the list. This is very useful when implementing a reversed infinite scroller, that prepends
         * data instead of appending it
         */
        this.keepScrolledIndexOnPrepend = false;
        /** @internal */
        this.runwayStateChanged$ = new Subject();
        this.viewport = null;
        this.viewRepeater = null;
        this._scrolledIndex$ = new ReplaySubject(1);
        this.scrolledIndex$ = this._scrolledIndex$.pipe(distinctUntilChanged());
        this._scrolledIndex = 0;
        this._contentSize$ = new ReplaySubject(1);
        this.contentSize$ = this._contentSize$.asObservable();
        this._contentSize = 0;
        this._renderedRange$ = new ReplaySubject(1);
        this.renderedRange$ = this._renderedRange$.asObservable();
        this._renderedRange = { start: 0, end: 0 };
        this.scrollTop = 0;
        /** @internal */
        this.scrollTopWithOutOffset = 0;
        /** @internal */
        this.scrollTopAfterOffset = 0;
        /** @internal */
        this.viewportOffset = 0;
        /** @internal */
        this.containerSize = 0;
        this.direction = 'down';
        this.detached$ = new Subject();
    }
    /**
     * @description
     * The size of the items in the virtually scrolled list
     */
    set itemSize(itemSize) {
        if (typeof itemSize === 'number') {
            this._itemSize = itemSize;
        }
    }
    get itemSize() {
        return this._itemSize;
    }
    set scrolledIndex(index) {
        this._scrolledIndex = index;
        this._scrolledIndex$.next(index);
    }
    get scrolledIndex() {
        return this._scrolledIndex;
    }
    set contentSize(size) {
        this._contentSize = size;
        this._contentSize$.next(size);
    }
    set renderedRange(range) {
        this._renderedRange = range;
        this._renderedRange$.next(range);
    }
    get renderedRange() {
        return this._renderedRange;
    }
    /** @internal */
    ngOnChanges(changes) {
        if ((changes['runwayItemsOpposite'] &&
            !changes['runwayItemsOpposite'].firstChange) ||
            (changes['runwayItems'] && !changes['runwayItems'].firstChange)) {
            this.runwayStateChanged$.next();
        }
    }
    ngOnDestroy() {
        this.detach();
    }
    attach(viewport, viewRepeater) {
        this.viewport = viewport;
        this.viewRepeater = viewRepeater;
        this.calcRenderedRange();
        this.positionElements();
    }
    detach() {
        this.viewport = null;
        this.viewRepeater = null;
        this.detached$.next();
    }
    positionElements() {
        this.viewRepeater.renderingStart$.pipe(switchMap(() => {
            const start = this.renderedRange.start;
            return this.viewRepeater.viewRendered$.pipe(tap(({ view, index, item }) => {
                this._setViewPosition(view, (index + start) * this.itemSize);
                this.viewRenderCallback.next({
                    view,
                    item,
                    index,
                });
            }));
        }), this.untilDetached$()).subscribe();
    }
    calcRenderedRange() {
        const valueArray$ = this.viewRepeater.values$.pipe(map((values) => Array.isArray(values)
            ? values
            : values != null
                ? Array.from(values)
                : []), shareReplay({ bufferSize: 1, refCount: true }));
        /*
         * when keepScrolledIndexOnPrepend is active, we need to listen to data changes and figure out what was appended
         * before the last scrolledToItem
         */
        let valueCache = {};
        valueArray$
            .pipe(
        // TODO: this might cause issues when turning on/off
        filter(() => this.keepScrolledIndexOnPrepend), this.untilDetached$())
            .subscribe((valueArray) => {
            const trackBy = this.viewRepeater._trackBy;
            let scrollTo = this.scrolledIndex;
            const dataLength = valueArray.length;
            const oldDataLength = Object.keys(valueCache).length;
            if (oldDataLength > 0) {
                let i = 0;
                // check for each item from the last known scrolledIndex if it's an insert
                for (i; i <= scrollTo && i < dataLength; i++) {
                    // item is not in the valueCache, so it was added
                    if (!valueCache[trackBy(i, valueArray[i])]) {
                        scrollTo++;
                    }
                }
            }
            valueCache = {};
            valueArray.forEach((v, i) => (valueCache[trackBy(i, v)] = v));
            if (scrollTo !== this.scrolledIndex) {
                this.scrollToIndex(scrollTo);
            }
        });
        const dataLengthChanged$ = valueArray$.pipe(map((values) => values.length), distinctUntilChanged(), tap((dataLength) => (this.contentSize = dataLength * this.itemSize)));
        const onScroll$ = this.viewport.elementScrolled$.pipe(coalesceWith(unpatchedAnimationFrameTick()), startWith(void 0), tap(() => {
            this.viewportOffset = this.viewport.measureOffset();
            const { scrollTop, scrollTopWithOutOffset, scrollTopAfterOffset } = parseScrollTopBoundaries(this.viewport.getScrollTop(), this.viewportOffset, this._contentSize, this.containerSize);
            this.direction =
                scrollTopWithOutOffset > this.scrollTopWithOutOffset ? 'down' : 'up';
            this.scrollTopWithOutOffset = scrollTopWithOutOffset;
            this.scrollTopAfterOffset = scrollTopAfterOffset;
            this.scrollTop = scrollTop;
        }));
        combineLatest([
            dataLengthChanged$,
            this.viewport.containerRect$.pipe(map(({ height }) => {
                this.containerSize = height;
                return height;
            }), distinctUntilChanged()),
            onScroll$,
            this.runwayStateChanged$.pipe(startWith(void 0)),
        ])
            .pipe(map(([length]) => {
            const containerSize = calculateVisibleContainerSize(this.containerSize, this.scrollTopWithOutOffset, this.scrollTopAfterOffset);
            const range = { start: 0, end: 0 };
            if (this.direction === 'up') {
                range.start = Math.floor(Math.max(0, this.scrollTop - this.runwayItems * this.itemSize) /
                    this.itemSize);
                range.end = Math.min(length, Math.ceil((this.scrollTop +
                    containerSize +
                    this.runwayItemsOpposite * this.itemSize) /
                    this.itemSize));
            }
            else {
                range.start = Math.floor(Math.max(0, this.scrollTop - this.runwayItemsOpposite * this.itemSize) / this.itemSize);
                range.end = Math.min(length, Math.ceil((this.scrollTop +
                    containerSize +
                    this.runwayItems * this.itemSize) /
                    this.itemSize));
            }
            if (this.appendOnly) {
                range.start = Math.min(this._renderedRange.start, range.start);
                range.end = Math.max(this._renderedRange.end, range.end);
            }
            this.scrolledIndex = Math.floor(this.scrollTop / this.itemSize);
            return range;
        }), distinctUntilChanged(({ start: prevStart, end: prevEnd }, { start, end }) => prevStart === start && prevEnd === end), this.untilDetached$())
            .subscribe((range) => (this.renderedRange = range));
    }
    scrollToIndex(index, behavior) {
        const scrollTop = this.itemSize * index;
        this.viewport.scrollTo(this.viewportOffset + scrollTop, behavior);
    }
    untilDetached$() {
        return (o$) => o$.pipe(takeUntil(this.detached$));
    }
    _setViewPosition(view, scrollTop) {
        const element = this.getElement(view);
        element.style.position = 'absolute';
        element.style.transform = `translateY(${scrollTop}px)`;
    }
    /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: FixedSizeVirtualScrollStrategy, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
    /** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "16.1.0", version: "19.0.0", type: FixedSizeVirtualScrollStrategy, isStandalone: true, selector: "rx-virtual-scroll-viewport[itemSize]", inputs: { itemSize: "itemSize", appendOnly: ["appendOnly", "appendOnly", toBoolean], runwayItems: "runwayItems", runwayItemsOpposite: "runwayItemsOpposite", keepScrolledIndexOnPrepend: ["keepScrolledIndexOnPrepend", "keepScrolledIndexOnPrepend", toBoolean] }, providers: [
            {
                provide: RxVirtualScrollStrategy,
                useExisting: FixedSizeVirtualScrollStrategy,
            },
        ], usesInheritance: true, usesOnChanges: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: FixedSizeVirtualScrollStrategy, decorators: [{
            type: Directive,
            args: [{
                    selector: 'rx-virtual-scroll-viewport[itemSize]',
                    providers: [
                        {
                            provide: RxVirtualScrollStrategy,
                            useExisting: FixedSizeVirtualScrollStrategy,
                        },
                    ],
                    standalone: true,
                }]
        }], propDecorators: { itemSize: [{
                type: Input
            }], appendOnly: [{
                type: Input,
                args: [{ transform: toBoolean }]
            }], runwayItems: [{
                type: Input
            }], runwayItemsOpposite: [{
                type: Input
            }], keepScrolledIndexOnPrepend: [{
                type: Input,
                args: [{ transform: toBoolean }]
            }] } });

/**
 * @internal
 *
 * Factory that returns a `ListTemplateManager` for the passed params.
 *
 * @param templateSettings
 */
function createVirtualListTemplateManager({ viewContainerRef, templateRef, createViewContext, updateViewContext, templateCacheSize, }) {
    let _viewCache = [];
    let itemCount = 0;
    return {
        getListChanges,
        setItemCount: (count) => (itemCount = count),
        detach: () => {
            for (let i = 0; i < _viewCache.length; i++) {
                _viewCache[i].destroy();
            }
            _viewCache = [];
        },
    };
    function _updateView(item, index, count, contextIndex) {
        const view = viewContainerRef.get(index);
        updateViewContext(item, view, {
            count,
            index: contextIndex,
        });
        view.detectChanges();
        return view;
    }
    /**
     * Inserts a view for a new item, either from the cache or by creating a new
     * one.
     */
    function _insertView(value, count, adjustIndexWith, currentIndex) {
        currentIndex = currentIndex ?? viewContainerRef.length;
        const contextIndex = currentIndex + adjustIndexWith;
        const cachedView = _insertViewFromCache(currentIndex);
        if (cachedView) {
            updateViewContext(value, cachedView, {
                count,
                index: contextIndex,
            });
            cachedView.detectChanges();
            return [currentIndex, cachedView];
        }
        const context = createViewContext(value, {
            count,
            index: contextIndex,
        });
        const view = viewContainerRef.createEmbeddedView(templateRef, context, currentIndex);
        view.detectChanges();
        return [currentIndex, view];
    }
    /** Detaches the view at the given index and inserts into the view cache. */
    function _detachAndCacheView(index) {
        const detachedView = viewContainerRef.detach(index);
        _maybeCacheView(detachedView);
        detachedView.detectChanges();
    }
    /** Moves view at the previous index to the current index. */
    function _moveView(value, adjustedPreviousIndex, currentIndex, count, contextIndex) {
        const oldView = viewContainerRef.get(adjustedPreviousIndex);
        const view = (viewContainerRef.move(oldView, currentIndex));
        updateViewContext(value, view, {
            count,
            index: contextIndex,
        });
        view.detectChanges();
        return view;
    }
    /**
     * Cache the given detached view. If the cache is full, the view will be
     * destroyed.
     */
    function _maybeCacheView(view) {
        if (_viewCache.length < templateCacheSize) {
            _viewCache.push(view);
            return true;
        }
        else {
            const index = viewContainerRef.indexOf(view);
            // The host component could remove views from the container outside of
            // the view repeater. It's unlikely this will occur, but just in case,
            // destroy the view on its own, otherwise destroy it through the
            // container to ensure that all the references are removed.
            if (index === -1) {
                view.destroy();
            }
            else {
                viewContainerRef.remove(index);
            }
            return false;
        }
    }
    /** Inserts a recycled view from the cache at the given index. */
    function _insertViewFromCache(index) {
        const cachedView = _viewCache.pop();
        if (cachedView) {
            return viewContainerRef.insert(cachedView, index);
        }
        return null;
    }
    /**
     * @internal
     */
    function getListChanges(changes, items, count, adjustIndexWith) {
        const changedIdxs = new Set();
        const listChanges = [];
        let notifyParent = false;
        let appendedAtEnd = 0;
        const otherMovedIds = [];
        changes.forEachOperation(({ item, previousIndex }, adjustedPreviousIndex, currentIndex) => {
            if (previousIndex == null) {
                // insert
                const index = currentIndex === null ? undefined : currentIndex;
                listChanges.push([
                    index ?? items.length + appendedAtEnd,
                    () => {
                        const [insertedIndex, view] = _insertView(item, count, adjustIndexWith, index);
                        return {
                            view,
                            index: insertedIndex,
                            item,
                        };
                    },
                ]);
                if (index === undefined) {
                    appendedAtEnd++;
                }
                changedIdxs.add(item);
                notifyParent = true;
            }
            else if (currentIndex == null) {
                // remove
                listChanges.push([
                    adjustedPreviousIndex,
                    () => {
                        _detachAndCacheView(adjustedPreviousIndex ?? undefined);
                        return { item };
                    },
                    true,
                ]);
                notifyParent = true;
            }
            else if (adjustedPreviousIndex !== null) {
                // move
                listChanges.push([
                    currentIndex,
                    () => {
                        const view = _moveView(item, adjustedPreviousIndex, currentIndex, count, currentIndex + adjustIndexWith);
                        return {
                            view,
                            index: currentIndex,
                            item,
                        };
                    },
                ]);
                otherMovedIds.push(adjustedPreviousIndex);
                changedIdxs.add(item);
                notifyParent = true;
            }
        });
        changes.forEachIdentityChange(({ item, currentIndex }) => {
            if (currentIndex != null && !changedIdxs.has(item)) {
                listChanges.push([
                    currentIndex,
                    () => {
                        const view = _updateView(item, currentIndex, count, currentIndex + adjustIndexWith);
                        return {
                            view,
                            index: currentIndex,
                            item,
                        };
                    },
                ]);
                changedIdxs.add(item);
            }
        });
        for (let i = 0; i < otherMovedIds.length; i++) {
            const itemIndex = otherMovedIds[i];
            const item = items[itemIndex];
            if (item && !changedIdxs.has(item)) {
                changedIdxs.add(item);
                listChanges.push([
                    itemIndex,
                    () => maybeUpdateView(itemIndex, count, itemIndex + adjustIndexWith, item),
                ]);
            }
        }
        if (changedIdxs.size < items.length) {
            for (let i = 0; i < items.length; i++) {
                const item = items[i];
                if (!changedIdxs.has(item)) {
                    listChanges.push([
                        i,
                        () => maybeUpdateView(i, count, i + adjustIndexWith, item),
                    ]);
                }
            }
        }
        return [listChanges, notifyParent];
    }
    function maybeUpdateView(viewIndex, count, itemIndex, item) {
        const view = viewContainerRef.get(viewIndex);
        if (view.context.count !== count || view.context.index !== itemIndex) {
            return {
                view: _updateView(item, viewIndex, count, itemIndex),
                index: viewIndex,
                item,
            };
        }
        return {
            index: viewIndex,
            view,
            item,
        };
    }
}

const NG_DEV_MODE$1 = typeof ngDevMode === 'undefined' || !!ngDevMode;
/**
 * @Directive RxVirtualFor
 *
 * @description
 *
 * The `*rxVirtualFor` structural directive provides a convenient and performant
 * way for rendering huge lists of items. It brings all the benefits `rxFor` does,
 * and implements virtual rendering.
 *
 * Instead of rendering every item provided, rxVirtualFor only renders what is
 * currently visible to the user, thus providing excellent runtime performance
 * for huge sets of data.
 *
 * The technique to render items is comparable to the on used by twitter and
 * explained in very much detail by @DasSurma in his blog post about the [complexities
 * of infinite scrollers](https://developer.chrome.com/blog/infinite-scroller/).
 *
 * "Each recycling of a DOM element would normally relayout the entire runway which
 * would bring us well below our target of 60 frames per second.
 * To avoid this, we are taking the burden of layout onto ourselves and use
 * absolutely positioned elements with transforms." (@DasSurma)
 *
 * ## API
 * The API is a combination of \@rx-angular/template/for &
 *  \@angular/cdk `*cdkVirtualFor`.
 * * trackBy: `(index: number, item: T) => any` | `keyof T`
 * * strategy: `string` | `Observable<string>`
 * * parent: `boolean`;
 * * renderCallback: `Subject<T[]>`
 * * viewCache: `number`
 * * (Injected) scrollStrategy: `RxVirtualScrollStrategy<T, U>`
 * * provides itself as RxVirtualViewRepeater for RxVirtualViewPortComponent to operate
 *
 * ## Features
 * * Push based architecture
 * * Comprehensive set of context variables
 * * Opt-out of `NgZone` with `patchZone`
 * * Notify when rendering of child templates is finished (`renderCallback`)
 * * Super efficient layouting with css transformations
 * * Define a viewCache in order to re-use views instead of re-creating them
 * * Configurable RxVirtualScrollStrategy<T, U> providing the core logic to calculate the viewRange and position DOM
 * Nodes
 *
 * ### Context Variables
 *
 * The following context variables are available for each template:
 *
 * - $implicit: `T` // the default variable accessed by `let val`
 * - item$: `Observable<T>` // the same value as $implicit, but as `Observable`
 * - index: `number` // current index of the item
 * - count: `number` // count of all items in the list
 * - first: `boolean` // true if the item is the first in the list
 * - last: `boolean` // true if the item is the last in the list
 * - even: `boolean` // true if the item has on even index (index % 2 === 0)
 * - odd: `boolean` // the opposite of even
 * - index$: `Observable<number>` // index as `Observable`
 * - count$: `Observable<number>` // count as `Observable`
 * - first$: `Observable<boolean>` // first as `Observable`
 * - last$: `Observable<boolean>` // last as `Observable`
 * - even$: `Observable<boolean>` // even as `Observable`
 * - odd$: `Observable<boolean>` // odd as `Observable`
 * - select: `(keys: (keyof T)[], distinctByMap) => Observable<Partial<T>>`
 * // returns a selection function which
 * // accepts an array of properties to pluck out of every list item. The function returns the selected properties of
 * // the current list item as distinct `Observable` key-value-pair. See the example below:
 *
 * This example showcases the `select` view-context function used for deeply nested lists.
 *
 *  ```html
 * <rx-virtual-scroll-viewport>
 *   <div
 *    autosized
 *    *rxVirtualFor="let hero of heroes$; trackBy: trackItem; let select = select;">
 *     <div>
 *       <strong>{{ hero.name }}</strong></br>
 *       Defeated enemies:
 *     </div>
 *      <span *rxFor="let enemy of select(['defeatedEnemies']); trackBy: trackEnemy;">
 *        {{ enemy.name }}
 *      </span>
 *   </div>
 * </rx-virtual-scroll-viewport>
 *  ```
 *
 * ### Using the context variables
 *
 * ```html
 * <rx-virtual-scroll-viewport>
 *  <div
 *     *rxVirtualFor="
 *       let item of observableItems$;
 *       let count = count;
 *       let index = index;
 *       let first = first;
 *       let last = last;
 *       let even = even;
 *       let odd = odd;
 *       trackBy: trackItem;
 *     "
 *   >
 *     <div>{{ count }}</div>
 *     <div>{{ index }}</div>
 *     <div>{{ item }}</div>
 *     <div>{{ first }}</div>
 *     <div>{{ last }}</div>
 *     <div>{{ even }}</div>
 *     <div>{{ odd }}</div>
 *   </div>
 * </rx-virtual-scroll-viewport>
 * ```
 *
 * @docsCategory RxVirtualFor
 * @docsPage RxVirtualFor
 * @publicApi
 */
class RxVirtualFor {
    /**
     * @description
     * The iterable input
     *
     * @example
     * <rx-virtual-scroll-viewport>
     *   <app-hero *rxVirtualFor="heroes$; let hero"
     *     [hero]="hero"></app-hero>
     * </rx-virtual-scroll-viewport>
     *
     * @param potentialSignalOrObservable
     */
    set rxVirtualForOf(potentialSignalOrObservable) {
        if (isSignal(potentialSignalOrObservable)) {
            this.staticValue = undefined;
            this.renderStatic = false;
            this.observables$.next(toObservable(potentialSignalOrObservable, { injector: this.injector }));
        }
        else if (!isObservable(potentialSignalOrObservable)) {
            this.staticValue = potentialSignalOrObservable;
            this.renderStatic = true;
        }
        else {
            this.staticValue = undefined;
            this.renderStatic = false;
            this.observables$.next(potentialSignalOrObservable);
        }
    }
    set rxVirtualForTemplate(value) {
        this._template = value;
    }
    /**
     * @description
     * The rendering strategy to be used to render updates to the DOM.
     * Use it to dynamically manage your rendering strategy. You can switch the strategy
     * imperatively (with a string) or by binding an Observable.
     * The default strategy is `'normal'` if not configured otherwise.
     *
     * @example
     * \@Component({
     *   selector: 'app-root',
     *   template: `
     *     <rx-virtual-scroll-viewport>
     *       <app-hero
     *        autosized
     *        *rxVirtualFor="let hero of heroes$; strategy: strategy"
     *        [hero]="hero"></app-hero>
     *     </rx-virtual-scroll-viewport>
     *   `
     * })
     * export class AppComponent {
     *   strategy = 'low';
     * }
     *
     * // OR
     *
     * \@Component({
     *   selector: 'app-root',
     *   template: `
     *     <rx-virtual-scroll-viewport>
     *       <app-hero
     *        autosized
     *        *rxVirtualFor="let hero of heroes$; strategy: strategy$"
     *        [hero]="hero"></app-hero>
     *     </rx-virtual-scroll-viewport>
     *   `
     * })
     * export class AppComponent {
     *   strategy$ = new BehaviorSubject('immediate');
     * }
     *
     * @param strategyName
     * @see {@link strategies}
     */
    set strategy(strategyName) {
        this.strategyHandler.next(strategyName);
    }
    /*@Input('rxVirtualForTombstone') tombstone: TemplateRef<
     RxVirtualForViewContext<T>
     > | null = null;*/
    /**
     * @description
     * A function or key that defines how to track changes for items in the provided
     * iterable data.
     *
     * When items are added, moved, or removed in the iterable,
     * the directive must re-render the appropriate DOM nodes.
     * To minimize operations on the DOM, only nodes that have changed
     * are re-rendered.
     *
     * By default, `rxVirtualFor` assumes that the object instance identifies
     * the node in the iterable (equality check `===`).
     * When a function or key is supplied, `rxVirtualFor` uses the result to identify the item node.
     *
     * @example
     * \@Component({
     *   selector: 'app-root',
     *   template: `
     *    <rx-virtual-scroll-viewport>
     *      <app-list-item
     *        *rxVirtualFor="
     *          let item of items$;
     *          trackBy: 'id';
     *        "
     *        autosized
     *        [item]="item"
     *      >
     *      </app-list-item>
     *    </rx-virtual-scroll-viewport>
     *   `
     * })
     * export class AppComponent {
     *   items$ = itemService.getItems();
     * }
     *
     * // OR
     *
     * \@Component({
     *   selector: 'app-root',
     *   template: `
     *   <rx-virtual-scroll-viewport>
     *      <app-list-item
     *        *rxVirtualFor="
     *          let item of items$;
     *          trackBy: trackItem;
     *        "
     *        autosized
     *        [item]="item"
     *      >
     *      </app-list-item>
     *    </rx-virtual-scroll-viewport>
     *   `
     * })
     * export class AppComponent {
     *   items$ = itemService.getItems();
     *   trackItem = (idx, item) => item.id;
     * }
     *
     * @param trackByFnOrKey
     */
    set trackBy(trackByFnOrKey) {
        if (NG_DEV_MODE$1 &&
            trackByFnOrKey != null &&
            typeof trackByFnOrKey !== 'string' &&
            typeof trackByFnOrKey !== 'symbol' &&
            typeof trackByFnOrKey !== 'function') {
            throw new Error(`trackBy must be typeof function or keyof T, but received ${JSON.stringify(trackByFnOrKey)}.`);
        }
        if (trackByFnOrKey == null) {
            this._trackBy = null;
        }
        else {
            this._trackBy =
                typeof trackByFnOrKey !== 'function'
                    ? (i, a) => a[trackByFnOrKey]
                    : trackByFnOrKey;
        }
    }
    /**
     * @description
     * A `Subject` which emits whenever `*rxVirtualFor` finished rendering a
     * set of changes to the view.
     * This enables developers to perform actions exactly at the timing when the
     * updates passed are rendered to the DOM.
     * The `renderCallback` is useful in situations where you rely on specific DOM
     * properties like the `height` of a table after all items got rendered.
     * It is also possible to use the renderCallback in order to determine if a
     * view should be visible or not. This way developers can hide a list as
     * long as it has not finished rendering.
     *
     * The result of the `renderCallback` will contain the currently rendered set
     * of items in the iterable.
     *
     * @example
     * \@Component({
     *   selector: 'app-root',
     *   template: `
     *    <rx-virtual-scroll-viewport>
     *      <app-list-item
     *        *rxVirtualFor="
     *          let item of items$;
     *          trackBy: trackItem;
     *          renderCallback: itemsRendered;
     *        "
     *        autosized
     *        [item]="item"
     *      >
     *      </app-list-item>
     *    </rx-virtual-scroll-viewport>
     *   `
     * })
     * export class AppComponent {
     *   items$: Observable<Item[]> = itemService.getItems();
     *   trackItem = (idx, item) => item.id;
     *   // this emits whenever rxVirtualFor finished rendering changes
     *   itemsRendered = new Subject<Item[]>();
     * }
     *
     * @param renderCallback
     */
    set renderCallback(renderCallback) {
        this._renderCallback = renderCallback;
    }
    /** @internal */
    get template() {
        return this._template || this.templateRef;
    }
    /** @internal */
    static ngTemplateContextGuard(dir, ctx) {
        return true;
    }
    constructor(templateRef) {
        this.templateRef = templateRef;
        this.scrollStrategy = inject((RxVirtualScrollStrategy));
        this.iterableDiffers = inject(IterableDiffers);
        this.cdRef = inject(ChangeDetectorRef);
        this.ngZone = inject(NgZone);
        /** @internal */
        this.injector = inject(Injector);
        this.viewContainer = inject(ViewContainerRef);
        this.strategyProvider = inject(RxStrategyProvider);
        this.errorHandler = inject(ErrorHandler);
        this.defaults = inject(RX_VIRTUAL_SCROLL_DEFAULT_OPTIONS, {
            optional: true,
        });
        /** @internal */
        this.partiallyFinished = false;
        /** @internal */
        this.renderStatic = false;
        /** @internal */
        this.strategyHandler = strategyHandling(this.strategyProvider.primaryStrategy, this.strategyProvider.strategies);
        /**
         * @description
         * Controls the amount if views held in cache for later re-use when a user is
         * scrolling the list. If this is set to 0, `rxVirtualFor` won't cache any view,
         * thus destroying & re-creating very often on scroll events.
         */
        this.templateCacheSize = this.defaults?.templateCacheSize || DEFAULT_TEMPLATE_CACHE_SIZE;
        /**
         * @description
         *  If `parent` is set to `true` (default to `false`), `*rxVirtualFor` will
         *  automatically run change-detection for its parent component when its scheduled
         *  tasks are done in order to update any pending `@ContentChild` or `@ViewChild`
         *  relation to be updated according to the updated ViewContainer.
         *
         * @example
         * \@Component({
         *   selector: 'app-root',
         *   template: `
         *   <rx-virtual-scroll-viewport>
         *      <app-list-item
         *        *rxVirtualFor="
         *          let item of items$;
         *          trackBy: trackItem;
         *          parent: true;
         *        "
         *        [item]="item"
         *        autosized
         *      ></app-list-item>
         *    </rx-virtual-scroll-viewport>
         *   `
         * })
         * export class AppComponent {
         *   // those queries won't be in sync with what `rxVirtualFor` is rendering
         *   // when parent is set to false.
         *   \@ViewChildren(AppListItem) listItems: QueryList<AppListItem>;
         *
         *   items$ = itemService.getItems();
         * }
         *
         * @param renderParent
         *
         * @deprecated this flag will be dropped soon, as it is no longer required when using signal based view & content queries
         */
        this.renderParent = false;
        /**
         * @description
         * A flag to control whether `*rxVirtualFor` rendering happens within
         * `NgZone` or not. The default value is set to `true` if not configured otherwise.
         * If `patchZone` is set to `false` `*rxVirtualFor` will operate completely outside of `NgZone`.
         *
         * @example
         * \@Component({
         *   selector: 'app-root',
         *   template: `
         *    <rx-virtual-scroll-viewport>
         *      <app-list-item
         *        *rxVirtualFor="
         *          let item of items$;
         *          trackBy: trackItem;
         *          patchZone: false;
         *        "
         *        [item]="item"
         *        autosized
         *      ></app-list-item>
         *    </rx-virtual-scroll-viewport>
         *   `
         * })
         * export class AppComponent {
         *   items$ = itemService.getItems();
         * }
         *
         * @param patchZone
         */
        this.patchZone = this.strategyProvider.config.patchZone;
        /** @internal */
        this.viewsRendered$ = new Subject();
        /** @internal */
        this.viewRendered$ = new Subject();
        /** @internal */
        this.renderingStart$ = new Subject();
        /** @internal */
        this.observables$ = new ReplaySubject(1);
        /** @internal */
        this.values$ = this.observables$.pipe(coerceObservableWith(), switchAll(), shareReplay({ bufferSize: 1, refCount: true }));
        /** @internal */
        this._destroy$ = new Subject();
        /** @internal */
        this._trackBy = null;
    }
    /** @internal */
    ngOnInit() {
        this.values$.pipe(takeUntil(this._destroy$)).subscribe((values) => {
            this.values = values;
        });
        this.templateManager = createVirtualListTemplateManager({
            viewContainerRef: this.viewContainer,
            templateRef: this.template,
            createViewContext: this.createViewContext.bind(this),
            updateViewContext: this.updateViewContext.bind(this),
            templateCacheSize: this.templateCacheSize,
        });
        // let the scroll strategy initialize before
        Promise$1.resolve().then(() => {
            this.render()
                .pipe(takeUntil(this._destroy$))
                .subscribe((v) => {
                this._renderCallback?.next(v);
            });
        });
    }
    /** @internal */
    ngDoCheck() {
        if (this.renderStatic) {
            this.observables$.next(this.staticValue);
        }
    }
    /** @internal */
    ngOnDestroy() {
        this._destroy$.next();
        this.templateManager.detach();
    }
    render() {
        return combineLatest([
            this.values$.pipe(map((values) => Array.isArray(values)
                ? values
                : values != null
                    ? Array.from(values)
                    : [])),
            this.scrollStrategy.renderedRange$.pipe(distinctUntilChanged((oldRange, newRange) => oldRange.start === newRange.start && oldRange.end === newRange.end)),
            this.strategyHandler.strategy$.pipe(distinctUntilChanged()),
        ]).pipe(switchMap(([items, range, strategy]) => 
        // wait for scrollStrategy to be stable until computing new state
        this.scrollStrategy.isStable.pipe(take(1), 
        // map iterable to latest diff
        switchMap(() => {
            const iterable = items.slice(range.start, range.end);
            const differ = this.getDiffer(iterable);
            let changes = null;
            if (differ) {
                if (this.partiallyFinished) {
                    const currentIterable = [];
                    for (let i = 0, ilen = this.viewContainer.length; i < ilen; i++) {
                        const viewRef = (this.viewContainer.get(i));
                        currentIterable[i] = viewRef.context.$implicit;
                    }
                    differ.diff(currentIterable);
                }
                changes = differ.diff(iterable);
            }
            if (!changes) {
                return NEVER;
            }
            const listChanges = this.templateManager.getListChanges(changes, iterable, items.length, range.start);
            const updates = listChanges[0].sort((a, b) => a[0] - b[0]);
            const indicesToPosition = new Set();
            const insertedOrRemoved = listChanges[1];
            const work$ = updates.map(([index, work, removed]) => {
                if (!removed) {
                    indicesToPosition.add(index);
                }
                return onStrategy(null, strategy, () => {
                    const update = work();
                    if (update.view) {
                        this.viewRendered$.next(update);
                    }
                }, { ngZone: this.patchZone ? this.ngZone : undefined });
            });
            this.partiallyFinished = true;
            const notifyParent = insertedOrRemoved && this.renderParent;
            this.renderingStart$.next(indicesToPosition);
            return combineLatest(
            // emit after all changes are rendered
            work$.length > 0 ? work$ : [of(iterable)]).pipe(tap(() => {
                this.templateManager.setItemCount(items.length);
                this.partiallyFinished = false;
                const viewsRendered = [];
                const end = this.viewContainer.length;
                let i = 0;
                for (i; i < end; i++) {
                    viewsRendered.push(this.viewContainer.get(i));
                }
                this.viewsRendered$.next(viewsRendered);
            }), notifyParent
                ? switchMap((v) => concat(of(v), onStrategy(null, strategy, (_, work, options) => {
                    work(this.cdRef, options.scope);
                }, {
                    ngZone: this.patchZone ? this.ngZone : undefined,
                    scope: this.cdRef.context || this.cdRef,
                }).pipe(ignoreElements())))
                : (o$) => o$, this.handleError(), map(() => iterable));
        }))), this.handleError());
    }
    handleError() {
        return (o$) => o$.pipe(catchError((err) => {
            this.partiallyFinished = false;
            this.errorHandler.handleError(err);
            return of(null);
        }));
    }
    getDiffer(values) {
        if (this._differ) {
            return this._differ;
        }
        return values
            ? (this._differ = this.iterableDiffers.find(values).create(this._trackBy))
            : null;
    }
    /** @internal */
    createViewContext(item, computedContext) {
        return new RxVirtualForViewContext(item, this.values, computedContext);
    }
    /** @internal */
    updateViewContext(item, view, computedContext) {
        view.context.updateContext(computedContext);
        view.context.$implicit = item;
        view.context.rxVirtualForOf = this.values;
    }
    /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: RxVirtualFor, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive }); }
    /** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.0.0", type: RxVirtualFor, isStandalone: true, selector: "[rxVirtualFor][rxVirtualForOf]", inputs: { rxVirtualForOf: "rxVirtualForOf", rxVirtualForTemplate: "rxVirtualForTemplate", strategy: ["rxVirtualForStrategy", "strategy"], templateCacheSize: ["rxVirtualForTemplateCacheSize", "templateCacheSize"], renderParent: ["rxVirtualForParent", "renderParent"], patchZone: ["rxVirtualForPatchZone", "patchZone"], trackBy: ["rxVirtualForTrackBy", "trackBy"], renderCallback: ["rxVirtualForRenderCallback", "renderCallback"] }, providers: [{ provide: RxVirtualViewRepeater, useExisting: RxVirtualFor }], ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: RxVirtualFor, decorators: [{
            type: Directive,
            args: [{
                    selector: '[rxVirtualFor][rxVirtualForOf]',
                    providers: [{ provide: RxVirtualViewRepeater, useExisting: RxVirtualFor }],
                    standalone: true,
                }]
        }], ctorParameters: () => [{ type: i0.TemplateRef }], propDecorators: { rxVirtualForOf: [{
                type: Input
            }], rxVirtualForTemplate: [{
                type: Input
            }], strategy: [{
                type: Input,
                args: ['rxVirtualForStrategy']
            }], templateCacheSize: [{
                type: Input,
                args: ['rxVirtualForTemplateCacheSize']
            }], renderParent: [{
                type: Input,
                args: ['rxVirtualForParent']
            }], patchZone: [{
                type: Input,
                args: ['rxVirtualForPatchZone']
            }], trackBy: [{
                type: Input,
                args: ['rxVirtualForTrackBy']
            }], renderCallback: [{
                type: Input,
                args: ['rxVirtualForRenderCallback']
            }] } });

class RxVirtualScrollElementDirective {
    constructor() {
        this.elementRef = inject(ElementRef);
        this.elementScrolled$ = unpatchedScroll(this.elementRef.nativeElement);
    }
    getElementRef() {
        return this.elementRef;
    }
    measureOffset() {
        return this.elementRef.nativeElement.getBoundingClientRect().top;
    }
    /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: RxVirtualScrollElementDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
    /** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.0.0", type: RxVirtualScrollElementDirective, isStandalone: true, selector: "[rxVirtualScrollElement]", host: { classAttribute: "rx-virtual-scroll-element" }, providers: [
            {
                provide: RxVirtualScrollElement,
                useExisting: RxVirtualScrollElementDirective,
            },
        ], ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: RxVirtualScrollElementDirective, decorators: [{
            type: Directive,
            args: [{
                    selector: '[rxVirtualScrollElement]',
                    providers: [
                        {
                            provide: RxVirtualScrollElement,
                            useExisting: RxVirtualScrollElementDirective,
                        },
                    ],
                    // eslint-disable-next-line @angular-eslint/no-host-metadata-property
                    host: {
                        class: 'rx-virtual-scroll-element',
                    },
                    standalone: true,
                }]
        }] });

function observeElementSize(element, config) {
    const extractProp = config?.extract ?? ((entries) => entries[0].contentRect);
    return new Observable((subscriber) => {
        const observer = new ResizeObserver((entries) => {
            subscriber.next(extractProp(entries));
        });
        observer.observe(element, config?.options);
        return () => observer.disconnect();
    });
}

const NG_DEV_MODE = typeof ngDevMode === 'undefined' || !!ngDevMode;
/**
 * @Component RxVirtualScrollViewport
 *
 * @description
 * Container component comparable to CdkVirtualScrollViewport acting as viewport
 * for `*rxVirtualFor` to operate on.
 *
 * Its main purpose is to implement the `RxVirtualScrollViewport` interface
 * as well as maintaining the scroll runways' height in order to give
 * the provided `RxVirtualScrollStrategy` room to position items.
 *
 * Furthermore, it will gather and forward all events to the consumer of `rxVirtualFor`.
 *
 * @docsCategory RxVirtualFor
 * @docsPage RxVirtualFor
 * @publicApi
 */
class RxVirtualScrollViewportComponent {
    /** @internal */
    constructor() {
        this.elementRef = inject((ElementRef));
        this.scrollStrategy = inject((RxVirtualScrollStrategy), {
            optional: true,
        });
        this.scrollElement = inject(RxVirtualScrollElement, { optional: true });
        /**
         * @description
         *
         * Sets the first view to be visible to the user.
         * The viewport waits for the data to arrive and scrolls to the given index immediately.
         *
         * */
        this.initialScrollIndex = 0;
        this.elementScrolled$ = this.scrollElement?.elementScrolled$ ??
            defer(() => unpatchedScroll(this.runway.nativeElement));
        /** @internal */
        this._containerRect$ = new ReplaySubject(1);
        this.containerRect$ = this._containerRect$.asObservable();
        /**
         * @description
         *
         * The range to be rendered by `*rxVirtualFor`. This value is determined by the
         * provided `RxVirtualScrollStrategy`. It gives the user information about the
         * range of items being actually rendered to the DOM.
         * Note this value updates before the `renderCallback` kicks in, thus it is only
         * in sync with the DOM when the next `renderCallback` emitted an event.
         */
        this.viewRange = this.scrollStrategy.renderedRange$;
        /**
         * @description
         *
         * The index of the currently scrolled item. The scrolled item is the topmost
         * item actually being visible to the user.
         */
        this.scrolledIndexChange = this.scrollStrategy.scrolledIndex$;
        /** @internal */
        this.destroy$ = new Subject();
        if (NG_DEV_MODE && !this.scrollStrategy) {
            throw Error('Error: rx-virtual-scroll-viewport requires an `RxVirtualScrollStrategy` to be set.');
        }
        observeElementSize(this.scrollElement?.getElementRef()?.nativeElement ??
            this.elementRef.nativeElement, {
            extract: (entries) => ({
                height: Math.round(entries[0].contentRect.height),
                width: Math.round(entries[0].contentRect.width),
            }),
        })
            .pipe(distinctUntilChanged(({ height: prevHeight, width: prevWidth }, { height, width }) => prevHeight === height && prevWidth === width), takeUntil(this.destroy$))
            .subscribe(this._containerRect$);
    }
    ngAfterViewInit() {
        this.scrollStrategy.contentSize$
            .pipe(distinctUntilChanged(), takeUntil(this.destroy$))
            .subscribe((size) => {
            this.updateContentSize(size);
        });
        if (this.initialScrollIndex != null && this.initialScrollIndex > 0) {
            this.scrollStrategy.contentSize$
                .pipe(filter((size) => size > 0), take(1), takeUntil(this.destroy$))
                .subscribe(() => {
                this.scrollToIndex(this.initialScrollIndex);
            });
        }
    }
    /** @internal */
    ngAfterContentInit() {
        if (ngDevMode && !this.viewRepeater) {
            throw Error('Error: rx-virtual-scroll-viewport requires a `RxVirtualViewRepeater` to be provided.');
        }
        this.scrollStrategy.attach(this, this.viewRepeater);
    }
    /** @internal */
    ngOnDestroy() {
        this.destroy$.next();
        this.scrollStrategy.detach();
    }
    getScrollElement() {
        return (this.scrollElement?.getElementRef()?.nativeElement ??
            this.runway.nativeElement);
    }
    getScrollTop() {
        return this.getScrollElement().scrollTop;
    }
    scrollTo(position, behavior) {
        // TODO: implement more complex scroll scenarios
        this.getScrollElement().scrollTo({ top: position, behavior: behavior });
    }
    scrollToIndex(index, behavior) {
        this.scrollStrategy.scrollToIndex(index, behavior);
    }
    measureOffset() {
        if (this.scrollElement) {
            const scrollableOffset = this.scrollElement.measureOffset();
            const rect = this.elementRef.nativeElement.getBoundingClientRect();
            return this.getScrollTop() + (rect.top - scrollableOffset);
        }
        else {
            return 0;
        }
    }
    updateContentSize(size) {
        if (this.scrollElement) {
            this.elementRef.nativeElement.style.height = `${size}px`;
        }
        else {
            this.scrollSentinel.nativeElement.style.transform = `translate(0, ${size - 1}px)`;
        }
    }
    /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: RxVirtualScrollViewportComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
    /** @nocollapse */ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.0.0", type: RxVirtualScrollViewportComponent, isStandalone: true, selector: "rx-virtual-scroll-viewport", inputs: { initialScrollIndex: "initialScrollIndex" }, outputs: { viewRange: "viewRange", scrolledIndexChange: "scrolledIndexChange" }, host: { classAttribute: "rx-virtual-scroll-viewport" }, providers: [
            {
                provide: RxVirtualScrollViewport,
                useExisting: RxVirtualScrollViewportComponent,
            },
        ], queries: [{ propertyName: "viewRepeater", first: true, predicate: RxVirtualViewRepeater, descendants: true }], viewQueries: [{ propertyName: "scrollSentinel", first: true, predicate: ["sentinel"], descendants: true }, { propertyName: "runway", first: true, predicate: ["runway"], descendants: true, static: true }], ngImport: i0, template: `
    <div
      #runway
      class="rx-virtual-scroll__runway"
      [class.rx-virtual-scroll-element]="!scrollElement"
    >
      <div
        #sentinel
        class="rx-virtual-scroll__sentinel"
        *ngIf="!this.scrollElement"
      ></div>
      <ng-content></ng-content>
    </div>
  `, isInline: true, styles: [".rx-virtual-scroll-viewport{display:block;width:100%;height:100%;box-sizing:border-box;contain:strict}.rx-virtual-scroll-viewport .rx-virtual-scroll__runway{contain:strict;width:100%;position:absolute;top:0;bottom:0}.rx-virtual-scroll-viewport .rx-virtual-scroll__runway>*{position:absolute}.rx-virtual-scroll-viewport .rx-virtual-scroll__sentinel{width:1px;height:1px;contain:strict;position:absolute;will-change:transform}.rx-virtual-scroll-element{contain:strict;overflow:auto;-webkit-overflow-scrolling:touch}.rx-virtual-scroll-element:not(.rx-virtual-scroll-element:is(.rx-virtual-scroll-element--withSyncScrollbar)){transform:translateZ(0);will-change:scroll-position}\n"], dependencies: [{ kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: RxVirtualScrollViewportComponent, decorators: [{
            type: Component,
            args: [{ selector: 'rx-virtual-scroll-viewport', template: `
    <div
      #runway
      class="rx-virtual-scroll__runway"
      [class.rx-virtual-scroll-element]="!scrollElement"
    >
      <div
        #sentinel
        class="rx-virtual-scroll__sentinel"
        *ngIf="!this.scrollElement"
      ></div>
      <ng-content></ng-content>
    </div>
  `, providers: [
                        {
                            provide: RxVirtualScrollViewport,
                            useExisting: RxVirtualScrollViewportComponent,
                        },
                    ], encapsulation: ViewEncapsulation.None, host: {
                        class: 'rx-virtual-scroll-viewport',
                    }, changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgIf], styles: [".rx-virtual-scroll-viewport{display:block;width:100%;height:100%;box-sizing:border-box;contain:strict}.rx-virtual-scroll-viewport .rx-virtual-scroll__runway{contain:strict;width:100%;position:absolute;top:0;bottom:0}.rx-virtual-scroll-viewport .rx-virtual-scroll__runway>*{position:absolute}.rx-virtual-scroll-viewport .rx-virtual-scroll__sentinel{width:1px;height:1px;contain:strict;position:absolute;will-change:transform}.rx-virtual-scroll-element{contain:strict;overflow:auto;-webkit-overflow-scrolling:touch}.rx-virtual-scroll-element:not(.rx-virtual-scroll-element:is(.rx-virtual-scroll-element--withSyncScrollbar)){transform:translateZ(0);will-change:scroll-position}\n"] }]
        }], ctorParameters: () => [], propDecorators: { initialScrollIndex: [{
                type: Input
            }], scrollSentinel: [{
                type: ViewChild,
                args: ['sentinel']
            }], runway: [{
                type: ViewChild,
                args: ['runway', { static: true }]
            }], viewRepeater: [{
                type: ContentChild,
                args: [RxVirtualViewRepeater]
            }], viewRange: [{
                type: Output
            }], scrolledIndexChange: [{
                type: Output
            }] } });

class RxVirtualScrollWindowDirective {
    constructor() {
        this.document = inject(DOCUMENT);
        this.elementRef = new ElementRef(this.document.documentElement);
        this.elementScrolled$ = unpatchedScroll(this.document);
    }
    getElementRef() {
        return this.elementRef;
    }
    measureOffset() {
        return 0;
    }
    /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: RxVirtualScrollWindowDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
    /** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.0.0", type: RxVirtualScrollWindowDirective, isStandalone: true, selector: "rx-virtual-scroll-viewport[scrollWindow]", providers: [
            {
                provide: RxVirtualScrollElement,
                useExisting: RxVirtualScrollWindowDirective,
            },
        ], ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.0", ngImport: i0, type: RxVirtualScrollWindowDirective, decorators: [{
            type: Directive,
            args: [{
                    selector: 'rx-virtual-scroll-viewport[scrollWindow]',
                    providers: [
                        {
                            provide: RxVirtualScrollElement,
                            useExisting: RxVirtualScrollWindowDirective,
                        },
                    ],
                    standalone: true,
                }]
        }] });

/**
 * Generated bundle index. Do not edit.
 */

export { AutoSizeVirtualScrollStrategy, DynamicSizeVirtualScrollStrategy, FixedSizeVirtualScrollStrategy, RX_VIRTUAL_SCROLL_DEFAULT_OPTIONS, RX_VIRTUAL_SCROLL_DEFAULT_OPTIONS_FACTORY, RxVirtualFor, RxVirtualForViewContext, RxVirtualScrollElement, RxVirtualScrollElementDirective, RxVirtualScrollStrategy, RxVirtualScrollViewport, RxVirtualScrollViewportComponent, RxVirtualScrollWindowDirective, RxVirtualViewRepeater };
//# sourceMappingURL=template-experimental-virtual-scrolling.mjs.map