UNPKG

@angular/cdk

Version:

Angular Material Component Development Kit

1,394 lines (1,381 loc) 48.3 kB
import * as i0 from '@angular/core'; import { InjectionToken, forwardRef, Directive, Input, inject, NgZone, RendererFactory2, Injectable, ElementRef, Renderer2, DOCUMENT, ChangeDetectorRef, signal, Injector, effect, ApplicationRef, DestroyRef, untracked, afterNextRender, booleanAttribute, Component, ViewEncapsulation, ChangeDetectionStrategy, Output, ViewChild, ViewContainerRef, TemplateRef, IterableDiffers, NgModule } from '@angular/core'; import { Subject, of, Observable, Subscription, animationFrameScheduler, asapScheduler, isObservable } from 'rxjs'; import { distinctUntilChanged, auditTime, filter, startWith, takeUntil, pairwise, switchMap, shareReplay } from 'rxjs/operators'; import { coerceNumberProperty, coerceElement } from './_element-chunk.mjs'; import { Platform } from './_platform-chunk.mjs'; import { Directionality } from './_directionality-chunk.mjs'; import { getRtlScrollAxisType, RtlScrollAxisType, supportsScrollBehavior } from './_scrolling-chunk.mjs'; import { BidiModule } from './bidi.mjs'; export { Dir as ɵɵDir } from './bidi.mjs'; import { _VIEW_REPEATER_STRATEGY, ArrayDataSource, _RecycleViewRepeaterStrategy } from './_recycle-view-repeater-strategy-chunk.mjs'; import { isDataSource } from './_data-source-chunk.mjs'; import '@angular/common'; const VIRTUAL_SCROLL_STRATEGY = new InjectionToken('VIRTUAL_SCROLL_STRATEGY'); class FixedSizeVirtualScrollStrategy { _scrolledIndexChange = new Subject(); scrolledIndexChange = this._scrolledIndexChange.pipe(distinctUntilChanged()); _viewport = null; _itemSize; _minBufferPx; _maxBufferPx; constructor(itemSize, minBufferPx, maxBufferPx) { this._itemSize = itemSize; this._minBufferPx = minBufferPx; this._maxBufferPx = maxBufferPx; } attach(viewport) { this._viewport = viewport; this._updateTotalContentSize(); this._updateRenderedRange(); } detach() { this._scrolledIndexChange.complete(); this._viewport = null; } updateItemAndBufferSize(itemSize, minBufferPx, maxBufferPx) { if (maxBufferPx < minBufferPx && (typeof ngDevMode === 'undefined' || ngDevMode)) { throw Error('CDK virtual scroll: maxBufferPx must be greater than or equal to minBufferPx'); } this._itemSize = itemSize; this._minBufferPx = minBufferPx; this._maxBufferPx = maxBufferPx; this._updateTotalContentSize(); this._updateRenderedRange(); } onContentScrolled() { this._updateRenderedRange(); } onDataLengthChanged() { this._updateTotalContentSize(); this._updateRenderedRange(); } onContentRendered() {} onRenderedOffsetChanged() {} scrollToIndex(index, behavior) { if (this._viewport) { this._viewport.scrollToOffset(index * this._itemSize, behavior); } } _updateTotalContentSize() { if (!this._viewport) { return; } this._viewport.setTotalContentSize(this._viewport.getDataLength() * this._itemSize); } _updateRenderedRange() { if (!this._viewport) { return; } const renderedRange = this._viewport.getRenderedRange(); const newRange = { start: renderedRange.start, end: renderedRange.end }; const viewportSize = this._viewport.getViewportSize(); const dataLength = this._viewport.getDataLength(); let scrollOffset = this._viewport.measureScrollOffset(); let firstVisibleIndex = this._itemSize > 0 ? scrollOffset / this._itemSize : 0; if (newRange.end > dataLength) { const maxVisibleItems = Math.ceil(viewportSize / this._itemSize); const newVisibleIndex = Math.max(0, Math.min(firstVisibleIndex, dataLength - maxVisibleItems)); if (firstVisibleIndex != newVisibleIndex) { firstVisibleIndex = newVisibleIndex; scrollOffset = newVisibleIndex * this._itemSize; newRange.start = Math.floor(firstVisibleIndex); } newRange.end = Math.max(0, Math.min(dataLength, newRange.start + maxVisibleItems)); } const startBuffer = scrollOffset - newRange.start * this._itemSize; if (startBuffer < this._minBufferPx && newRange.start != 0) { const expandStart = Math.ceil((this._maxBufferPx - startBuffer) / this._itemSize); newRange.start = Math.max(0, newRange.start - expandStart); newRange.end = Math.min(dataLength, Math.ceil(firstVisibleIndex + (viewportSize + this._minBufferPx) / this._itemSize)); } else { const endBuffer = newRange.end * this._itemSize - (scrollOffset + viewportSize); if (endBuffer < this._minBufferPx && newRange.end != dataLength) { const expandEnd = Math.ceil((this._maxBufferPx - endBuffer) / this._itemSize); if (expandEnd > 0) { newRange.end = Math.min(dataLength, newRange.end + expandEnd); newRange.start = Math.max(0, Math.floor(firstVisibleIndex - this._minBufferPx / this._itemSize)); } } } this._viewport.setRenderedRange(newRange); this._viewport.setRenderedContentOffset(Math.round(this._itemSize * newRange.start)); this._scrolledIndexChange.next(Math.floor(firstVisibleIndex)); } } function _fixedSizeVirtualScrollStrategyFactory(fixedSizeDir) { return fixedSizeDir._scrollStrategy; } class CdkFixedSizeVirtualScroll { get itemSize() { return this._itemSize; } set itemSize(value) { this._itemSize = coerceNumberProperty(value); } _itemSize = 20; get minBufferPx() { return this._minBufferPx; } set minBufferPx(value) { this._minBufferPx = coerceNumberProperty(value); } _minBufferPx = 100; get maxBufferPx() { return this._maxBufferPx; } set maxBufferPx(value) { this._maxBufferPx = coerceNumberProperty(value); } _maxBufferPx = 200; _scrollStrategy = new FixedSizeVirtualScrollStrategy(this.itemSize, this.minBufferPx, this.maxBufferPx); ngOnChanges() { this._scrollStrategy.updateItemAndBufferSize(this.itemSize, this.minBufferPx, this.maxBufferPx); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: CdkFixedSizeVirtualScroll, deps: [], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.0", type: CdkFixedSizeVirtualScroll, isStandalone: true, selector: "cdk-virtual-scroll-viewport[itemSize]", inputs: { itemSize: "itemSize", minBufferPx: "minBufferPx", maxBufferPx: "maxBufferPx" }, providers: [{ provide: VIRTUAL_SCROLL_STRATEGY, useFactory: _fixedSizeVirtualScrollStrategyFactory, deps: [forwardRef(() => CdkFixedSizeVirtualScroll)] }], usesOnChanges: true, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: CdkFixedSizeVirtualScroll, decorators: [{ type: Directive, args: [{ selector: 'cdk-virtual-scroll-viewport[itemSize]', providers: [{ provide: VIRTUAL_SCROLL_STRATEGY, useFactory: _fixedSizeVirtualScrollStrategyFactory, deps: [forwardRef(() => CdkFixedSizeVirtualScroll)] }] }] }], propDecorators: { itemSize: [{ type: Input }], minBufferPx: [{ type: Input }], maxBufferPx: [{ type: Input }] } }); const DEFAULT_SCROLL_TIME = 20; class ScrollDispatcher { _ngZone = inject(NgZone); _platform = inject(Platform); _renderer = inject(RendererFactory2).createRenderer(null, null); _cleanupGlobalListener; constructor() {} _scrolled = new Subject(); _scrolledCount = 0; scrollContainers = new Map(); register(scrollable) { if (!this.scrollContainers.has(scrollable)) { this.scrollContainers.set(scrollable, scrollable.elementScrolled().subscribe(() => this._scrolled.next(scrollable))); } } deregister(scrollable) { const scrollableReference = this.scrollContainers.get(scrollable); if (scrollableReference) { scrollableReference.unsubscribe(); this.scrollContainers.delete(scrollable); } } scrolled(auditTimeInMs = DEFAULT_SCROLL_TIME) { if (!this._platform.isBrowser) { return of(); } return new Observable(observer => { if (!this._cleanupGlobalListener) { this._cleanupGlobalListener = this._ngZone.runOutsideAngular(() => this._renderer.listen('document', 'scroll', () => this._scrolled.next())); } const subscription = auditTimeInMs > 0 ? this._scrolled.pipe(auditTime(auditTimeInMs)).subscribe(observer) : this._scrolled.subscribe(observer); this._scrolledCount++; return () => { subscription.unsubscribe(); this._scrolledCount--; if (!this._scrolledCount) { this._cleanupGlobalListener?.(); this._cleanupGlobalListener = undefined; } }; }); } ngOnDestroy() { this._cleanupGlobalListener?.(); this._cleanupGlobalListener = undefined; this.scrollContainers.forEach((_, container) => this.deregister(container)); this._scrolled.complete(); } ancestorScrolled(elementOrElementRef, auditTimeInMs) { const ancestors = this.getAncestorScrollContainers(elementOrElementRef); return this.scrolled(auditTimeInMs).pipe(filter(target => !target || ancestors.indexOf(target) > -1)); } getAncestorScrollContainers(elementOrElementRef) { const scrollingContainers = []; this.scrollContainers.forEach((_subscription, scrollable) => { if (this._scrollableContainsElement(scrollable, elementOrElementRef)) { scrollingContainers.push(scrollable); } }); return scrollingContainers; } _scrollableContainsElement(scrollable, elementOrElementRef) { let element = coerceElement(elementOrElementRef); let scrollableElement = scrollable.getElementRef().nativeElement; do { if (element == scrollableElement) { return true; } } while (element = element.parentElement); return false; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: ScrollDispatcher, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: ScrollDispatcher, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: ScrollDispatcher, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [] }); class CdkScrollable { elementRef = inject(ElementRef); scrollDispatcher = inject(ScrollDispatcher); ngZone = inject(NgZone); dir = inject(Directionality, { optional: true }); _scrollElement = this.elementRef.nativeElement; _destroyed = new Subject(); _renderer = inject(Renderer2); _cleanupScroll; _elementScrolled = new Subject(); constructor() {} ngOnInit() { this._cleanupScroll = this.ngZone.runOutsideAngular(() => this._renderer.listen(this._scrollElement, 'scroll', event => this._elementScrolled.next(event))); this.scrollDispatcher.register(this); } ngOnDestroy() { this._cleanupScroll?.(); this._elementScrolled.complete(); this.scrollDispatcher.deregister(this); this._destroyed.next(); this._destroyed.complete(); } elementScrolled() { return this._elementScrolled; } getElementRef() { return this.elementRef; } scrollTo(options) { const el = this.elementRef.nativeElement; const isRtl = this.dir && this.dir.value == 'rtl'; if (options.left == null) { options.left = isRtl ? options.end : options.start; } if (options.right == null) { options.right = isRtl ? options.start : options.end; } if (options.bottom != null) { options.top = el.scrollHeight - el.clientHeight - options.bottom; } if (isRtl && getRtlScrollAxisType() != RtlScrollAxisType.NORMAL) { if (options.left != null) { options.right = el.scrollWidth - el.clientWidth - options.left; } if (getRtlScrollAxisType() == RtlScrollAxisType.INVERTED) { options.left = options.right; } else if (getRtlScrollAxisType() == RtlScrollAxisType.NEGATED) { options.left = options.right ? -options.right : options.right; } } else { if (options.right != null) { options.left = el.scrollWidth - el.clientWidth - options.right; } } this._applyScrollToOptions(options); } _applyScrollToOptions(options) { const el = this.elementRef.nativeElement; if (supportsScrollBehavior()) { el.scrollTo(options); } else { if (options.top != null) { el.scrollTop = options.top; } if (options.left != null) { el.scrollLeft = options.left; } } } measureScrollOffset(from) { const LEFT = 'left'; const RIGHT = 'right'; const el = this.elementRef.nativeElement; if (from == 'top') { return el.scrollTop; } if (from == 'bottom') { return el.scrollHeight - el.clientHeight - el.scrollTop; } const isRtl = this.dir && this.dir.value == 'rtl'; if (from == 'start') { from = isRtl ? RIGHT : LEFT; } else if (from == 'end') { from = isRtl ? LEFT : RIGHT; } if (isRtl && getRtlScrollAxisType() == RtlScrollAxisType.INVERTED) { if (from == LEFT) { return el.scrollWidth - el.clientWidth - el.scrollLeft; } else { return el.scrollLeft; } } else if (isRtl && getRtlScrollAxisType() == RtlScrollAxisType.NEGATED) { if (from == LEFT) { return el.scrollLeft + el.scrollWidth - el.clientWidth; } else { return -el.scrollLeft; } } else { if (from == LEFT) { return el.scrollLeft; } else { return el.scrollWidth - el.clientWidth - el.scrollLeft; } } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: CdkScrollable, deps: [], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.0", type: CdkScrollable, isStandalone: true, selector: "[cdk-scrollable], [cdkScrollable]", ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: CdkScrollable, decorators: [{ type: Directive, args: [{ selector: '[cdk-scrollable], [cdkScrollable]' }] }], ctorParameters: () => [] }); const DEFAULT_RESIZE_TIME = 20; class ViewportRuler { _platform = inject(Platform); _listeners; _viewportSize; _change = new Subject(); _document = inject(DOCUMENT); constructor() { const ngZone = inject(NgZone); const renderer = inject(RendererFactory2).createRenderer(null, null); ngZone.runOutsideAngular(() => { if (this._platform.isBrowser) { const changeListener = event => this._change.next(event); this._listeners = [renderer.listen('window', 'resize', changeListener), renderer.listen('window', 'orientationchange', changeListener)]; } this.change().subscribe(() => this._viewportSize = null); }); } ngOnDestroy() { this._listeners?.forEach(cleanup => cleanup()); this._change.complete(); } getViewportSize() { if (!this._viewportSize) { this._updateViewportSize(); } const output = { width: this._viewportSize.width, height: this._viewportSize.height }; if (!this._platform.isBrowser) { this._viewportSize = null; } return output; } getViewportRect() { const scrollPosition = this.getViewportScrollPosition(); const { width, height } = this.getViewportSize(); return { top: scrollPosition.top, left: scrollPosition.left, bottom: scrollPosition.top + height, right: scrollPosition.left + width, height, width }; } getViewportScrollPosition() { if (!this._platform.isBrowser) { return { top: 0, left: 0 }; } const document = this._document; const window = this._getWindow(); const documentElement = document.documentElement; const documentRect = documentElement.getBoundingClientRect(); const top = -documentRect.top || document.body.scrollTop || window.scrollY || documentElement.scrollTop || 0; const left = -documentRect.left || document.body.scrollLeft || window.scrollX || documentElement.scrollLeft || 0; return { top, left }; } change(throttleTime = DEFAULT_RESIZE_TIME) { return throttleTime > 0 ? this._change.pipe(auditTime(throttleTime)) : this._change; } _getWindow() { return this._document.defaultView || window; } _updateViewportSize() { const window = this._getWindow(); this._viewportSize = this._platform.isBrowser ? { width: window.innerWidth, height: window.innerHeight } : { width: 0, height: 0 }; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: ViewportRuler, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: ViewportRuler, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: ViewportRuler, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [] }); const VIRTUAL_SCROLLABLE = new InjectionToken('VIRTUAL_SCROLLABLE'); class CdkVirtualScrollable extends CdkScrollable { constructor() { super(); } measureViewportSize(orientation) { const viewportEl = this.elementRef.nativeElement; return orientation === 'horizontal' ? viewportEl.clientWidth : viewportEl.clientHeight; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: CdkVirtualScrollable, deps: [], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.0", type: CdkVirtualScrollable, isStandalone: true, usesInheritance: true, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: CdkVirtualScrollable, decorators: [{ type: Directive }], ctorParameters: () => [] }); function rangesEqual(r1, r2) { return r1.start == r2.start && r1.end == r2.end; } const SCROLL_SCHEDULER = typeof requestAnimationFrame !== 'undefined' ? animationFrameScheduler : asapScheduler; class CdkVirtualScrollViewport extends CdkVirtualScrollable { elementRef = inject(ElementRef); _changeDetectorRef = inject(ChangeDetectorRef); _scrollStrategy = inject(VIRTUAL_SCROLL_STRATEGY, { optional: true }); scrollable = inject(VIRTUAL_SCROLLABLE, { optional: true }); _platform = inject(Platform); _detachedSubject = new Subject(); _renderedRangeSubject = new Subject(); get orientation() { return this._orientation; } set orientation(orientation) { if (this._orientation !== orientation) { this._orientation = orientation; this._calculateSpacerSize(); } } _orientation = 'vertical'; appendOnly = false; scrolledIndexChange = new Observable(observer => this._scrollStrategy.scrolledIndexChange.subscribe(index => Promise.resolve().then(() => this.ngZone.run(() => observer.next(index))))); _contentWrapper; renderedRangeStream = this._renderedRangeSubject; _totalContentSize = 0; _totalContentWidth = signal('', ...(ngDevMode ? [{ debugName: "_totalContentWidth" }] : [])); _totalContentHeight = signal('', ...(ngDevMode ? [{ debugName: "_totalContentHeight" }] : [])); _renderedContentTransform; _renderedRange = { start: 0, end: 0 }; _dataLength = 0; _viewportSize = 0; _forOf; _renderedContentOffset = 0; _renderedContentOffsetNeedsRewrite = false; _changeDetectionNeeded = signal(false, ...(ngDevMode ? [{ debugName: "_changeDetectionNeeded" }] : [])); _runAfterChangeDetection = []; _viewportChanges = Subscription.EMPTY; _injector = inject(Injector); _isDestroyed = false; constructor() { super(); const viewportRuler = inject(ViewportRuler); if (!this._scrollStrategy && (typeof ngDevMode === 'undefined' || ngDevMode)) { throw Error('Error: cdk-virtual-scroll-viewport requires the "itemSize" property to be set.'); } this._viewportChanges = viewportRuler.change().subscribe(() => { this.checkViewportSize(); }); if (!this.scrollable) { this.elementRef.nativeElement.classList.add('cdk-virtual-scrollable'); this.scrollable = this; } const ref = effect(() => { if (this._changeDetectionNeeded()) { this._doChangeDetection(); } }, ...(ngDevMode ? [{ debugName: "ref", injector: inject(ApplicationRef).injector }] : [{ injector: inject(ApplicationRef).injector }])); inject(DestroyRef).onDestroy(() => void ref.destroy()); } ngOnInit() { if (!this._platform.isBrowser) { return; } if (this.scrollable === this) { super.ngOnInit(); } this.ngZone.runOutsideAngular(() => Promise.resolve().then(() => { this._measureViewportSize(); this._scrollStrategy.attach(this); this.scrollable.elementScrolled().pipe(startWith(null), auditTime(0, SCROLL_SCHEDULER), takeUntil(this._destroyed)).subscribe(() => this._scrollStrategy.onContentScrolled()); this._markChangeDetectionNeeded(); })); } ngOnDestroy() { this.detach(); this._scrollStrategy.detach(); this._renderedRangeSubject.complete(); this._detachedSubject.complete(); this._viewportChanges.unsubscribe(); this._isDestroyed = true; super.ngOnDestroy(); } attach(forOf) { if (this._forOf && (typeof ngDevMode === 'undefined' || ngDevMode)) { throw Error('CdkVirtualScrollViewport is already attached.'); } this.ngZone.runOutsideAngular(() => { this._forOf = forOf; this._forOf.dataStream.pipe(takeUntil(this._detachedSubject)).subscribe(data => { const newLength = data.length; if (newLength !== this._dataLength) { this._dataLength = newLength; this._scrollStrategy.onDataLengthChanged(); } this._doChangeDetection(); }); }); } detach() { this._forOf = null; this._detachedSubject.next(); } getDataLength() { return this._dataLength; } getViewportSize() { return this._viewportSize; } getRenderedRange() { return this._renderedRange; } measureBoundingClientRectWithScrollOffset(from) { return this.getElementRef().nativeElement.getBoundingClientRect()[from]; } setTotalContentSize(size) { if (this._totalContentSize !== size) { this._totalContentSize = size; this._calculateSpacerSize(); this._markChangeDetectionNeeded(); } } setRenderedRange(range) { if (!rangesEqual(this._renderedRange, range)) { if (this.appendOnly) { range = { start: 0, end: Math.max(this._renderedRange.end, range.end) }; } this._renderedRangeSubject.next(this._renderedRange = range); this._markChangeDetectionNeeded(() => this._scrollStrategy.onContentRendered()); } } getOffsetToRenderedContentStart() { return this._renderedContentOffsetNeedsRewrite ? null : this._renderedContentOffset; } setRenderedContentOffset(offset, to = 'to-start') { offset = this.appendOnly && to === 'to-start' ? 0 : offset; const isRtl = this.dir && this.dir.value == 'rtl'; const isHorizontal = this.orientation == 'horizontal'; const axis = isHorizontal ? 'X' : 'Y'; const axisDirection = isHorizontal && isRtl ? -1 : 1; let transform = `translate${axis}(${Number(axisDirection * offset)}px)`; this._renderedContentOffset = offset; if (to === 'to-end') { transform += ` translate${axis}(-100%)`; this._renderedContentOffsetNeedsRewrite = true; } if (this._renderedContentTransform != transform) { this._renderedContentTransform = transform; this._markChangeDetectionNeeded(() => { if (this._renderedContentOffsetNeedsRewrite) { this._renderedContentOffset -= this.measureRenderedContentSize(); this._renderedContentOffsetNeedsRewrite = false; this.setRenderedContentOffset(this._renderedContentOffset); } else { this._scrollStrategy.onRenderedOffsetChanged(); } }); } } scrollToOffset(offset, behavior = 'auto') { const options = { behavior }; if (this.orientation === 'horizontal') { options.start = offset; } else { options.top = offset; } this.scrollable.scrollTo(options); } scrollToIndex(index, behavior = 'auto') { this._scrollStrategy.scrollToIndex(index, behavior); } measureScrollOffset(from) { let measureScrollOffset; if (this.scrollable == this) { measureScrollOffset = _from => super.measureScrollOffset(_from); } else { measureScrollOffset = _from => this.scrollable.measureScrollOffset(_from); } return Math.max(0, measureScrollOffset(from ?? (this.orientation === 'horizontal' ? 'start' : 'top')) - this.measureViewportOffset()); } measureViewportOffset(from) { let fromRect; const LEFT = 'left'; const RIGHT = 'right'; const isRtl = this.dir?.value == 'rtl'; if (from == 'start') { fromRect = isRtl ? RIGHT : LEFT; } else if (from == 'end') { fromRect = isRtl ? LEFT : RIGHT; } else if (from) { fromRect = from; } else { fromRect = this.orientation === 'horizontal' ? 'left' : 'top'; } const scrollerClientRect = this.scrollable.measureBoundingClientRectWithScrollOffset(fromRect); const viewportClientRect = this.elementRef.nativeElement.getBoundingClientRect()[fromRect]; return viewportClientRect - scrollerClientRect; } measureRenderedContentSize() { const contentEl = this._contentWrapper.nativeElement; return this.orientation === 'horizontal' ? contentEl.offsetWidth : contentEl.offsetHeight; } measureRangeSize(range) { if (!this._forOf) { return 0; } return this._forOf.measureRangeSize(range, this.orientation); } checkViewportSize() { this._measureViewportSize(); this._scrollStrategy.onDataLengthChanged(); } _measureViewportSize() { this._viewportSize = this.scrollable.measureViewportSize(this.orientation); } _markChangeDetectionNeeded(runAfter) { if (runAfter) { this._runAfterChangeDetection.push(runAfter); } if (untracked(this._changeDetectionNeeded)) { return; } this.ngZone.runOutsideAngular(() => { Promise.resolve().then(() => { this.ngZone.run(() => { this._changeDetectionNeeded.set(true); }); }); }); } _doChangeDetection() { if (this._isDestroyed) { return; } this.ngZone.run(() => { this._changeDetectorRef.markForCheck(); this._contentWrapper.nativeElement.style.transform = this._renderedContentTransform; afterNextRender(() => { this._changeDetectionNeeded.set(false); const runAfterChangeDetection = this._runAfterChangeDetection; this._runAfterChangeDetection = []; for (const fn of runAfterChangeDetection) { fn(); } }, { injector: this._injector }); }); } _calculateSpacerSize() { this._totalContentHeight.set(this.orientation === 'horizontal' ? '' : `${this._totalContentSize}px`); this._totalContentWidth.set(this.orientation === 'horizontal' ? `${this._totalContentSize}px` : ''); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: CdkVirtualScrollViewport, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "16.1.0", version: "21.0.0", type: CdkVirtualScrollViewport, isStandalone: true, selector: "cdk-virtual-scroll-viewport", inputs: { orientation: "orientation", appendOnly: ["appendOnly", "appendOnly", booleanAttribute] }, outputs: { scrolledIndexChange: "scrolledIndexChange" }, host: { properties: { "class.cdk-virtual-scroll-orientation-horizontal": "orientation === \"horizontal\"", "class.cdk-virtual-scroll-orientation-vertical": "orientation !== \"horizontal\"" }, classAttribute: "cdk-virtual-scroll-viewport" }, providers: [{ provide: CdkScrollable, useFactory: () => inject(VIRTUAL_SCROLLABLE, { optional: true }) || inject(CdkVirtualScrollViewport) }], viewQueries: [{ propertyName: "_contentWrapper", first: true, predicate: ["contentWrapper"], descendants: true, static: true }], usesInheritance: true, ngImport: i0, template: "<!--\n Wrap the rendered content in an element that will be used to offset it based on the scroll\n position.\n-->\n<div #contentWrapper class=\"cdk-virtual-scroll-content-wrapper\">\n <ng-content></ng-content>\n</div>\n<!--\n Spacer used to force the scrolling container to the correct size for the *total* number of items\n so that the scrollbar captures the size of the entire data set.\n-->\n<div class=\"cdk-virtual-scroll-spacer\"\n [style.width]=\"_totalContentWidth()\" [style.height]=\"_totalContentHeight()\"></div>\n", styles: ["cdk-virtual-scroll-viewport{display:block;position:relative;transform:translateZ(0)}.cdk-virtual-scrollable{overflow:auto;will-change:scroll-position;contain:strict}.cdk-virtual-scroll-content-wrapper{position:absolute;top:0;left:0;contain:content}[dir=rtl] .cdk-virtual-scroll-content-wrapper{right:0;left:auto}.cdk-virtual-scroll-orientation-horizontal .cdk-virtual-scroll-content-wrapper{min-height:100%}.cdk-virtual-scroll-orientation-horizontal .cdk-virtual-scroll-content-wrapper>dl:not([cdkVirtualFor]),.cdk-virtual-scroll-orientation-horizontal .cdk-virtual-scroll-content-wrapper>ol:not([cdkVirtualFor]),.cdk-virtual-scroll-orientation-horizontal .cdk-virtual-scroll-content-wrapper>table:not([cdkVirtualFor]),.cdk-virtual-scroll-orientation-horizontal .cdk-virtual-scroll-content-wrapper>ul:not([cdkVirtualFor]){padding-left:0;padding-right:0;margin-left:0;margin-right:0;border-left-width:0;border-right-width:0;outline:none}.cdk-virtual-scroll-orientation-vertical .cdk-virtual-scroll-content-wrapper{min-width:100%}.cdk-virtual-scroll-orientation-vertical .cdk-virtual-scroll-content-wrapper>dl:not([cdkVirtualFor]),.cdk-virtual-scroll-orientation-vertical .cdk-virtual-scroll-content-wrapper>ol:not([cdkVirtualFor]),.cdk-virtual-scroll-orientation-vertical .cdk-virtual-scroll-content-wrapper>table:not([cdkVirtualFor]),.cdk-virtual-scroll-orientation-vertical .cdk-virtual-scroll-content-wrapper>ul:not([cdkVirtualFor]){padding-top:0;padding-bottom:0;margin-top:0;margin-bottom:0;border-top-width:0;border-bottom-width:0;outline:none}.cdk-virtual-scroll-spacer{height:1px;transform-origin:0 0;flex:0 0 auto}[dir=rtl] .cdk-virtual-scroll-spacer{transform-origin:100% 0}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: CdkVirtualScrollViewport, decorators: [{ type: Component, args: [{ selector: 'cdk-virtual-scroll-viewport', host: { 'class': 'cdk-virtual-scroll-viewport', '[class.cdk-virtual-scroll-orientation-horizontal]': 'orientation === "horizontal"', '[class.cdk-virtual-scroll-orientation-vertical]': 'orientation !== "horizontal"' }, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, providers: [{ provide: CdkScrollable, useFactory: () => inject(VIRTUAL_SCROLLABLE, { optional: true }) || inject(CdkVirtualScrollViewport) }], template: "<!--\n Wrap the rendered content in an element that will be used to offset it based on the scroll\n position.\n-->\n<div #contentWrapper class=\"cdk-virtual-scroll-content-wrapper\">\n <ng-content></ng-content>\n</div>\n<!--\n Spacer used to force the scrolling container to the correct size for the *total* number of items\n so that the scrollbar captures the size of the entire data set.\n-->\n<div class=\"cdk-virtual-scroll-spacer\"\n [style.width]=\"_totalContentWidth()\" [style.height]=\"_totalContentHeight()\"></div>\n", styles: ["cdk-virtual-scroll-viewport{display:block;position:relative;transform:translateZ(0)}.cdk-virtual-scrollable{overflow:auto;will-change:scroll-position;contain:strict}.cdk-virtual-scroll-content-wrapper{position:absolute;top:0;left:0;contain:content}[dir=rtl] .cdk-virtual-scroll-content-wrapper{right:0;left:auto}.cdk-virtual-scroll-orientation-horizontal .cdk-virtual-scroll-content-wrapper{min-height:100%}.cdk-virtual-scroll-orientation-horizontal .cdk-virtual-scroll-content-wrapper>dl:not([cdkVirtualFor]),.cdk-virtual-scroll-orientation-horizontal .cdk-virtual-scroll-content-wrapper>ol:not([cdkVirtualFor]),.cdk-virtual-scroll-orientation-horizontal .cdk-virtual-scroll-content-wrapper>table:not([cdkVirtualFor]),.cdk-virtual-scroll-orientation-horizontal .cdk-virtual-scroll-content-wrapper>ul:not([cdkVirtualFor]){padding-left:0;padding-right:0;margin-left:0;margin-right:0;border-left-width:0;border-right-width:0;outline:none}.cdk-virtual-scroll-orientation-vertical .cdk-virtual-scroll-content-wrapper{min-width:100%}.cdk-virtual-scroll-orientation-vertical .cdk-virtual-scroll-content-wrapper>dl:not([cdkVirtualFor]),.cdk-virtual-scroll-orientation-vertical .cdk-virtual-scroll-content-wrapper>ol:not([cdkVirtualFor]),.cdk-virtual-scroll-orientation-vertical .cdk-virtual-scroll-content-wrapper>table:not([cdkVirtualFor]),.cdk-virtual-scroll-orientation-vertical .cdk-virtual-scroll-content-wrapper>ul:not([cdkVirtualFor]){padding-top:0;padding-bottom:0;margin-top:0;margin-bottom:0;border-top-width:0;border-bottom-width:0;outline:none}.cdk-virtual-scroll-spacer{height:1px;transform-origin:0 0;flex:0 0 auto}[dir=rtl] .cdk-virtual-scroll-spacer{transform-origin:100% 0}\n"] }] }], ctorParameters: () => [], propDecorators: { orientation: [{ type: Input }], appendOnly: [{ type: Input, args: [{ transform: booleanAttribute }] }], scrolledIndexChange: [{ type: Output }], _contentWrapper: [{ type: ViewChild, args: ['contentWrapper', { static: true }] }] } }); function getOffset(orientation, direction, node) { const el = node; if (!el.getBoundingClientRect) { return 0; } const rect = el.getBoundingClientRect(); if (orientation === 'horizontal') { return direction === 'start' ? rect.left : rect.right; } return direction === 'start' ? rect.top : rect.bottom; } class CdkVirtualForOf { _viewContainerRef = inject(ViewContainerRef); _template = inject(TemplateRef); _differs = inject(IterableDiffers); _viewRepeater = inject(_VIEW_REPEATER_STRATEGY); _viewport = inject(CdkVirtualScrollViewport, { skipSelf: true }); viewChange = new Subject(); _dataSourceChanges = new Subject(); get cdkVirtualForOf() { return this._cdkVirtualForOf; } set cdkVirtualForOf(value) { this._cdkVirtualForOf = value; if (isDataSource(value)) { this._dataSourceChanges.next(value); } else { this._dataSourceChanges.next(new ArrayDataSource(isObservable(value) ? value : Array.from(value || []))); } } _cdkVirtualForOf; get cdkVirtualForTrackBy() { return this._cdkVirtualForTrackBy; } set cdkVirtualForTrackBy(fn) { this._needsUpdate = true; this._cdkVirtualForTrackBy = fn ? (index, item) => fn(index + (this._renderedRange ? this._renderedRange.start : 0), item) : undefined; } _cdkVirtualForTrackBy; set cdkVirtualForTemplate(value) { if (value) { this._needsUpdate = true; this._template = value; } } get cdkVirtualForTemplateCacheSize() { return this._viewRepeater.viewCacheSize; } set cdkVirtualForTemplateCacheSize(size) { this._viewRepeater.viewCacheSize = coerceNumberProperty(size); } dataStream = this._dataSourceChanges.pipe(startWith(null), pairwise(), switchMap(([prev, cur]) => this._changeDataSource(prev, cur)), shareReplay(1)); _differ = null; _data; _renderedItems; _renderedRange; _needsUpdate = false; _destroyed = new Subject(); constructor() { const ngZone = inject(NgZone); this.dataStream.subscribe(data => { this._data = data; this._onRenderedDataChange(); }); this._viewport.renderedRangeStream.pipe(takeUntil(this._destroyed)).subscribe(range => { this._renderedRange = range; if (this.viewChange.observers.length) { ngZone.run(() => this.viewChange.next(this._renderedRange)); } this._onRenderedDataChange(); }); this._viewport.attach(this); } measureRangeSize(range, orientation) { if (range.start >= range.end) { return 0; } if ((range.start < this._renderedRange.start || range.end > this._renderedRange.end) && (typeof ngDevMode === 'undefined' || ngDevMode)) { throw Error(`Error: attempted to measure an item that isn't rendered.`); } const renderedStartIndex = range.start - this._renderedRange.start; const rangeLen = range.end - range.start; let firstNode; let lastNode; for (let i = 0; i < rangeLen; i++) { const view = this._viewContainerRef.get(i + renderedStartIndex); if (view && view.rootNodes.length) { firstNode = lastNode = view.rootNodes[0]; break; } } for (let i = rangeLen - 1; i > -1; i--) { const view = this._viewContainerRef.get(i + renderedStartIndex); if (view && view.rootNodes.length) { lastNode = view.rootNodes[view.rootNodes.length - 1]; break; } } return firstNode && lastNode ? getOffset(orientation, 'end', lastNode) - getOffset(orientation, 'start', firstNode) : 0; } ngDoCheck() { if (this._differ && this._needsUpdate) { const changes = this._differ.diff(this._renderedItems); if (!changes) { this._updateContext(); } else { this._applyChanges(changes); } this._needsUpdate = false; } } ngOnDestroy() { this._viewport.detach(); this._dataSourceChanges.next(undefined); this._dataSourceChanges.complete(); this.viewChange.complete(); this._destroyed.next(); this._destroyed.complete(); this._viewRepeater.detach(); } _onRenderedDataChange() { if (!this._renderedRange) { return; } this._renderedItems = this._data.slice(this._renderedRange.start, this._renderedRange.end); if (!this._differ) { this._differ = this._differs.find(this._renderedItems).create((index, item) => { return this.cdkVirtualForTrackBy ? this.cdkVirtualForTrackBy(index, item) : item; }); } this._needsUpdate = true; } _changeDataSource(oldDs, newDs) { if (oldDs) { oldDs.disconnect(this); } this._needsUpdate = true; return newDs ? newDs.connect(this) : of(); } _updateContext() { const count = this._data.length; let i = this._viewContainerRef.length; while (i--) { const view = this._viewContainerRef.get(i); view.context.index = this._renderedRange.start + i; view.context.count = count; this._updateComputedContextProperties(view.context); view.detectChanges(); } } _applyChanges(changes) { this._viewRepeater.applyChanges(changes, this._viewContainerRef, (record, _adjustedPreviousIndex, currentIndex) => this._getEmbeddedViewArgs(record, currentIndex), record => record.item); changes.forEachIdentityChange(record => { const view = this._viewContainerRef.get(record.currentIndex); view.context.$implicit = record.item; }); const count = this._data.length; let i = this._viewContainerRef.length; while (i--) { const view = this._viewContainerRef.get(i); view.context.index = this._renderedRange.start + i; view.context.count = count; this._updateComputedContextProperties(view.context); } } _updateComputedContextProperties(context) { context.first = context.index === 0; context.last = context.index === context.count - 1; context.even = context.index % 2 === 0; context.odd = !context.even; } _getEmbeddedViewArgs(record, index) { return { templateRef: this._template, context: { $implicit: record.item, cdkVirtualForOf: this._cdkVirtualForOf, index: -1, count: -1, first: false, last: false, odd: false, even: false }, index }; } static ngTemplateContextGuard(directive, context) { return true; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: CdkVirtualForOf, deps: [], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.0", type: CdkVirtualForOf, isStandalone: true, selector: "[cdkVirtualFor][cdkVirtualForOf]", inputs: { cdkVirtualForOf: "cdkVirtualForOf", cdkVirtualForTrackBy: "cdkVirtualForTrackBy", cdkVirtualForTemplate: "cdkVirtualForTemplate", cdkVirtualForTemplateCacheSize: "cdkVirtualForTemplateCacheSize" }, providers: [{ provide: _VIEW_REPEATER_STRATEGY, useClass: _RecycleViewRepeaterStrategy }], ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: CdkVirtualForOf, decorators: [{ type: Directive, args: [{ selector: '[cdkVirtualFor][cdkVirtualForOf]', providers: [{ provide: _VIEW_REPEATER_STRATEGY, useClass: _RecycleViewRepeaterStrategy }] }] }], ctorParameters: () => [], propDecorators: { cdkVirtualForOf: [{ type: Input }], cdkVirtualForTrackBy: [{ type: Input }], cdkVirtualForTemplate: [{ type: Input }], cdkVirtualForTemplateCacheSize: [{ type: Input }] } }); class CdkVirtualScrollableElement extends CdkVirtualScrollable { constructor() { super(); } measureBoundingClientRectWithScrollOffset(from) { return this.getElementRef().nativeElement.getBoundingClientRect()[from] - this.measureScrollOffset(from); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: CdkVirtualScrollableElement, deps: [], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.0", type: CdkVirtualScrollableElement, isStandalone: true, selector: "[cdkVirtualScrollingElement]", host: { classAttribute: "cdk-virtual-scrollable" }, providers: [{ provide: VIRTUAL_SCROLLABLE, useExisting: CdkVirtualScrollableElement }], usesInheritance: true, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: CdkVirtualScrollableElement, decorators: [{ type: Directive, args: [{ selector: '[cdkVirtualScrollingElement]', providers: [{ provide: VIRTUAL_SCROLLABLE, useExisting: CdkVirtualScrollableElement }], host: { 'class': 'cdk-virtual-scrollable' } }] }], ctorParameters: () => [] }); class CdkVirtualScrollableWindow extends CdkVirtualScrollable { constructor() { super(); const document = inject(DOCUMENT); this.elementRef = new ElementRef(document.documentElement); this._scrollElement = document; } measureBoundingClientRectWithScrollOffset(from) { return this.getElementRef().nativeElement.getBoundingClientRect()[from]; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: CdkVirtualScrollableWindow, deps: [], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.0", type: CdkVirtualScrollableWindow, isStandalone: true, selector: "cdk-virtual-scroll-viewport[scrollWindow]", providers: [{ provide: VIRTUAL_SCROLLABLE, useExisting: CdkVirtualScrollableWindow }], usesInheritance: true, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: CdkVirtualScrollableWindow, decorators: [{ type: Directive, args: [{ selector: 'cdk-virtual-scroll-viewport[scrollWindow]', providers: [{ provide: VIRTUAL_SCROLLABLE, useExisting: CdkVirtualScrollableWindow }] }] }], ctorParameters: () => [] }); class CdkScrollableModule { static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: CdkScrollableModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.0.0", ngImport: i0, type: CdkScrollableModule, imports: [CdkScrollable], exports: [CdkScrollable] }); static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: CdkScrollableModule }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: CdkScrollableModule, decorators: [{ type: NgModule, args: [{ exports: [CdkScrollable], imports: [CdkScrollable] }] }] }); class ScrollingModule { static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: ScrollingModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.0.0", ngImport: i0, type: ScrollingModule, imports: [BidiModule, CdkScrollableModule, CdkVirtualScrollViewport, CdkFixedSizeVirtualScroll, CdkVirtualForOf, CdkVirtualScrollableWindow, CdkVirtualScrollableElement], exports: [BidiModule, CdkScrollableModule, CdkFixedSizeVirtualScroll, CdkVirtualForOf, CdkVirtualScrollViewport, CdkVirtualScrollableWindow, CdkVirtualScrollableElement] }); static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: ScrollingModule, imports: [BidiModule, CdkScrollableModule, BidiModule, CdkScrollableModule] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: ScrollingModule, decorators: [{ type: NgModule, args: [{ imports: [BidiModule, CdkScrollableModule, CdkVirtualScrollViewport, CdkFixedSizeVirtualScroll, CdkVirtualForOf, CdkVirtualScrollableWindow, CdkVirtualScrollableElement], exports: [BidiModule, CdkScrollableModule, CdkFixedSizeVirtualScroll, CdkVirtualForOf, CdkVirtualScrollViewport, CdkVirtualScrollableWindow, CdkVirtualScrollableElement] }] }] }); export { CdkFixedSizeVirtualScroll, CdkScrollable, CdkScrollableModule, CdkVirtualForOf, CdkVirtualScrollViewport, CdkVirtualScrollable, CdkVirtualScrollableElement, CdkVirtualScrollableWindow, DEFAULT_RESIZE_TIME, DEFAULT_SCROLL_TIME, FixedSizeVirtualScrollStrategy, ScrollDispatcher, ScrollingModule, VIRTUAL_SCROLLABLE, VIRTUAL_SCROLL_STRATEGY, ViewportRuler, _fixedSizeVirtualScrollStrategyFactory }; //# sourceMappingURL=scrolling.mjs.map