import { Directive, ElementRef, OnDestroy, Output, EventEmitter, HostListener, Input, NgZone, OnChanges, AfterViewInit, SimpleChanges, AfterContentChecked } from '@angular/core'; import * as ps from 'perfect-scrollbar'; import {Observable} from 'rxjs/Observable'; import 'rxjs/add/observable/fromEvent'; import 'rxjs/add/operator/debounceTime'; import {Subscription} from 'rxjs'; @Directive({ selector: '[gp-scroll]' }) export class ScrollDirective implements OnDestroy, AfterViewInit, OnChanges, AfterContentChecked { @Input() debounceTime = 100; @Output() scrolled = new EventEmitter(); @Output() scrolledUp = new EventEmitter(); private width: number; private height: number; private contentWidth: number; private contentHeight: number; private childCount: number; private subscriptions: Subscription[] = []; constructor(private el: ElementRef, private zone: NgZone) { } ngAfterViewInit() { if (this.el.nativeElement && ps) { this.zone.runOutsideAngular(() => { ps.initialize(this.el.nativeElement, { handlers: ['click-rail', 'drag-scrollbar', 'wheel'] }); this.enableEvents(); }); } } ngOnChanges(changes: SimpleChanges): void { this.update(); } ngAfterContentChecked(): void { this.check(); } ngOnDestroy() { this.disableEvents(); if (this.el.nativeElement && ps) { this.zone.runOutsideAngular(() => { ps.destroy(this.el.nativeElement); }); } } public update(): void { if (this.el.nativeElement && ps) { this.zone.runOutsideAngular(() => { this.disableEvents(); ps.update(this.el.nativeElement); this.enableEvents(); }); } } private isChanged(): boolean { let doChange = false; if (this.el.nativeElement && ps) { const nEl = this.el.nativeElement, childCount = nEl.getElementsByTagName('*').length; doChange = (this.childCount !== childCount); if (!doChange) { const width = nEl.offsetWidth, height = nEl.offsetHeight; doChange = (width !== this.width || height !== this.height); if (!doChange) { const child = nEl.children[0]; let contentWidth, contentHeight; if (childCount > 0) { contentWidth = child.offsetWidth; contentHeight = child.offsetHeight; } else { contentWidth = this.contentWidth; contentHeight = this.contentHeight; } doChange = (contentWidth !== this.contentWidth || contentHeight !== this.contentHeight); } } } return doChange; } private check() { this.zone.runOutsideAngular(() => { if (this.isChanged()) { this.width = this.el.nativeElement.offsetWidth; this.height = this.el.nativeElement.offsetHeight; this.childCount = this.el.nativeElement.getElementsByTagName('*').length; if (this.childCount > 0) { this.contentWidth = this.el.nativeElement.children[0].offsetWidth; this.contentHeight = this.el.nativeElement.children[0].offsetHeight; } this.update(); } }); } private enableEvents() { const observableReachEnd = Observable.fromEvent(this.el.nativeElement, 'ps-y-reach-end'); const observableReachStart = Observable.fromEvent(this.el.nativeElement, 'ps-y-reach-start'); this.subscriptions.push( observableReachEnd.debounceTime(this.debounceTime).distinct().subscribe((event) => { this.scrolled.emit(event); }) ); this.subscriptions.push( observableReachStart.debounceTime(this.debounceTime).distinct().subscribe((event) => { this.scrolledUp.emit(event); }) ); } private disableEvents() { this.subscriptions.forEach((sub: Subscription) => { sub.unsubscribe(); }); } @HostListener('window:resize') onWindowResize() { this.update(); } }