import { ChangeDetectionStrategy, Component, HostListener, Input, OnInit } from '@angular/core'; import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling'; import { delay, map, startWith } from 'rxjs/operators'; import { combineLatest, fromEvent, Observable } from 'rxjs'; import { Required, shareDistinctLast } from '../../utils'; const ButtonTypes = { top: 'top', bottom: 'bottom', } as const; type ButtonType = keyof typeof ButtonTypes; @Component({ selector: 'iqser-scroll-button', templateUrl: './scroll-button.component.html', styleUrls: ['./scroll-button.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) export class ScrollButtonComponent implements OnInit { readonly buttonType = ButtonTypes; @Input() @Required() scrollViewport!: CdkVirtualScrollViewport; @Input() @Required() itemSize!: number; showScrollUp$?: Observable; showScrollDown$?: Observable; ngOnInit(): void { const scrollSize = () => this.scrollViewport.getDataLength() * this.itemSize; const scrollIsNeeded = () => this.scrollViewport.getViewportSize() < scrollSize(); const reachedEnd = (type: ButtonType) => this.scrollViewport.measureScrollOffset(type) < 0.5; const showScrollUp = () => scrollIsNeeded() && !reachedEnd(ButtonTypes.top); const showScrollDown = () => scrollIsNeeded() && !reachedEnd(ButtonTypes.bottom); /** Force an initial emit, so combineLatest works */ const scrolled$ = this.scrollViewport.elementScrolled().pipe(startWith(null)); const resized$ = fromEvent(window, 'resize').pipe(startWith(null)); const rangeChange$ = this.scrollViewport.renderedRangeStream.pipe(startWith(null)); /** Delay so that we can wait for items to be rendered in viewport and get correct values */ const scroll$ = combineLatest([scrolled$, resized$, rangeChange$]).pipe(delay(0), shareDistinctLast()); this.showScrollUp$ = scroll$.pipe(map(showScrollUp)); this.showScrollDown$ = scroll$.pipe(map(showScrollDown)); } scroll(type: ButtonType): void { const viewportSize = (this.scrollViewport?.getViewportSize() - this.itemSize) * (type === ButtonTypes.top ? -1 : 1); const scrollOffset = this.scrollViewport?.measureScrollOffset('top'); this.scrollViewport?.scrollToOffset(scrollOffset + viewportSize, 'smooth'); } @HostListener('document:keyup', ['$event']) spaceAndPageDownScroll(event: KeyboardEvent): void { const target = event.target as EventTarget & { tagName: string }; if (['Space', 'PageDown'].includes(event.code) && target.tagName === 'BODY') { this.scroll(ButtonTypes.bottom); } else if (['PageUp'].includes(event.code) && target.tagName === 'BODY') { this.scroll(ButtonTypes.top); } } get helpModeKey (): string { const screen = window.location.href.includes('/dossiers/') ? 'documents' : 'dossiers'; return `${screen}`; } }