/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import {coerceNumberProperty} from '@angular/cdk/coercion'; import {Directive, forwardRef, Input, OnChanges} from '@angular/core'; import {Observable, Subject} from 'rxjs'; import {distinctUntilChanged} from 'rxjs/operators'; import {VIRTUAL_SCROLL_STRATEGY, VirtualScrollStrategy} from './virtual-scroll-strategy'; import {CdkVirtualScrollViewport} from './virtual-scroll-viewport'; /** Virtual scrolling strategy for lists with items of known fixed size. */ export class FixedSizeVirtualScrollStrategy implements VirtualScrollStrategy { private _scrolledIndexChange = new Subject(); /** @docs-private Implemented as part of VirtualScrollStrategy. */ scrolledIndexChange: Observable = this._scrolledIndexChange.pipe(distinctUntilChanged()); /** The attached viewport. */ private _viewport: CdkVirtualScrollViewport | null = null; /** The size of the items in the virtually scrolling list. */ private _itemSize: number; /** The minimum amount of buffer rendered beyond the viewport (in pixels). */ private _minBufferPx: number; /** The number of buffer items to render beyond the edge of the viewport (in pixels). */ private _maxBufferPx: number; /** * @param itemSize The size of the items in the virtually scrolling list. * @param minBufferPx The minimum amount of buffer (in pixels) before needing to render more * @param maxBufferPx The amount of buffer (in pixels) to render when rendering more. */ constructor(itemSize: number, minBufferPx: number, maxBufferPx: number) { this._itemSize = itemSize; this._minBufferPx = minBufferPx; this._maxBufferPx = maxBufferPx; } /** * Attaches this scroll strategy to a viewport. * @param viewport The viewport to attach this strategy to. */ attach(viewport: CdkVirtualScrollViewport) { this._viewport = viewport; this._updateTotalContentSize(); this._updateRenderedRange(); } /** Detaches this scroll strategy from the currently attached viewport. */ detach() { this._scrolledIndexChange.complete(); this._viewport = null; } /** * Update the item size and buffer size. * @param itemSize The size of the items in the virtually scrolling list. * @param minBufferPx The minimum amount of buffer (in pixels) before needing to render more * @param maxBufferPx The amount of buffer (in pixels) to render when rendering more. */ updateItemAndBufferSize(itemSize: number, minBufferPx: number, maxBufferPx: number) { if (maxBufferPx < minBufferPx) { 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(); } /** @docs-private Implemented as part of VirtualScrollStrategy. */ onContentScrolled() { this._updateRenderedRange(); } /** @docs-private Implemented as part of VirtualScrollStrategy. */ onDataLengthChanged() { this._updateTotalContentSize(); this._updateRenderedRange(); } /** @docs-private Implemented as part of VirtualScrollStrategy. */ onContentRendered() { /* no-op */ } /** @docs-private Implemented as part of VirtualScrollStrategy. */ onRenderedOffsetChanged() { /* no-op */ } /** * Scroll to the offset for the given index. * @param index The index of the element to scroll to. * @param behavior The ScrollBehavior to use when scrolling. */ scrollToIndex(index: number, behavior: ScrollBehavior): void { if (this._viewport) { this._viewport.scrollToOffset(index * this._itemSize, behavior); } } /** Update the viewport's total content size. */ private _updateTotalContentSize() { if (!this._viewport) { return; } this._viewport.setTotalContentSize(this._viewport.getDataLength() * this._itemSize); } /** Update the viewport's rendered range. */ private _updateRenderedRange() { if (!this._viewport) { return; } const scrollOffset = this._viewport.measureScrollOffset(); const firstVisibleIndex = scrollOffset / this._itemSize; const renderedRange = this._viewport.getRenderedRange(); const newRange = {start: renderedRange.start, end: renderedRange.end}; const viewportSize = this._viewport.getViewportSize(); const dataLength = this._viewport.getDataLength(); 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(this._itemSize * newRange.start); this._scrolledIndexChange.next(Math.floor(firstVisibleIndex)); } } /** * Provider factory for `FixedSizeVirtualScrollStrategy` that simply extracts the already created * `FixedSizeVirtualScrollStrategy` from the given directive. * @param fixedSizeDir The instance of `CdkFixedSizeVirtualScroll` to extract the * `FixedSizeVirtualScrollStrategy` from. */ export function _fixedSizeVirtualScrollStrategyFactory(fixedSizeDir: CdkFixedSizeVirtualScroll) { return fixedSizeDir._scrollStrategy; } /** A virtual scroll strategy that supports fixed-size items. */ @Directive({ selector: 'cdk-virtual-scroll-viewport[itemSize]', providers: [{ provide: VIRTUAL_SCROLL_STRATEGY, useFactory: _fixedSizeVirtualScrollStrategyFactory, deps: [forwardRef(() => CdkFixedSizeVirtualScroll)], }], }) export class CdkFixedSizeVirtualScroll implements OnChanges { /** The size of the items in the list (in pixels). */ @Input() get itemSize(): number { return this._itemSize; } set itemSize(value: number) { this._itemSize = coerceNumberProperty(value); } _itemSize = 20; /** * The minimum amount of buffer rendered beyond the viewport (in pixels). * If the amount of buffer dips below this number, more items will be rendered. Defaults to 100px. */ @Input() get minBufferPx(): number { return this._minBufferPx; } set minBufferPx(value: number) { this._minBufferPx = coerceNumberProperty(value); } _minBufferPx = 100; /** * The number of pixels worth of buffer to render for when rendering new items. Defaults to 200px. */ @Input() get maxBufferPx(): number { return this._maxBufferPx; } set maxBufferPx(value: number) { this._maxBufferPx = coerceNumberProperty(value); } _maxBufferPx = 200; /** The scroll strategy used by this directive. */ _scrollStrategy = new FixedSizeVirtualScrollStrategy(this.itemSize, this.minBufferPx, this.maxBufferPx); ngOnChanges() { this._scrollStrategy.updateItemAndBufferSize(this.itemSize, this.minBufferPx, this.maxBufferPx); } }