import { Subject, fromEvent, merge } from 'rxjs'; import { auditTime, takeUntil, distinctUntilChanged, mapTo } from 'rxjs/operators'; import { InjectionToken, EventEmitter, Directive, NgZone, KeyValueDiffers, ElementRef, Inject, PLATFORM_ID, Optional, Input, Output, Component, ViewEncapsulation, ChangeDetectorRef, HostBinding, ViewChild, Renderer2, NgModule } from '@angular/core'; import { isPlatformBrowser, CommonModule } from '@angular/common'; import PerfectScrollbar from 'perfect-scrollbar'; import ResizeObserver from 'resize-observer-polyfill'; const PERFECT_SCROLLBAR_CONFIG = new InjectionToken('PERFECT_SCROLLBAR_CONFIG'); class Geometry { constructor(x, y, w, h) { this.x = x; this.y = y; this.w = w; this.h = h; } } class Position { constructor(x, y) { this.x = x; this.y = y; } } const PerfectScrollbarEvents = [ 'psScrollY', 'psScrollX', 'psScrollUp', 'psScrollDown', 'psScrollLeft', 'psScrollRight', 'psYReachEnd', 'psYReachStart', 'psXReachEnd', 'psXReachStart' ]; class PerfectScrollbarConfig { constructor(config = {}) { this.assign(config); } assign(config = {}) { for (const key in config) { this[key] = config[key]; } } } class PerfectScrollbarDirective { constructor(zone, differs, elementRef, platformId, defaults) { this.zone = zone; this.differs = differs; this.elementRef = elementRef; this.platformId = platformId; this.defaults = defaults; this.instance = null; this.ro = null; this.timeout = null; this.animation = null; this.configDiff = null; this.ngDestroy = new Subject(); this.disabled = false; this.psScrollY = new EventEmitter(); this.psScrollX = new EventEmitter(); this.psScrollUp = new EventEmitter(); this.psScrollDown = new EventEmitter(); this.psScrollLeft = new EventEmitter(); this.psScrollRight = new EventEmitter(); this.psYReachEnd = new EventEmitter(); this.psYReachStart = new EventEmitter(); this.psXReachEnd = new EventEmitter(); this.psXReachStart = new EventEmitter(); } ngOnInit() { if (!this.disabled && isPlatformBrowser(this.platformId)) { const config = new PerfectScrollbarConfig(this.defaults); config.assign(this.config); // Custom configuration this.zone.runOutsideAngular(() => { this.instance = new PerfectScrollbar(this.elementRef.nativeElement, config); }); if (!this.configDiff) { this.configDiff = this.differs.find(this.config || {}).create(); this.configDiff.diff(this.config || {}); } this.zone.runOutsideAngular(() => { this.ro = new ResizeObserver(() => { this.update(); }); if (this.elementRef.nativeElement.children[0]) { this.ro.observe(this.elementRef.nativeElement.children[0]); } this.ro.observe(this.elementRef.nativeElement); }); this.zone.runOutsideAngular(() => { PerfectScrollbarEvents.forEach((eventName) => { const eventType = eventName.replace(/([A-Z])/g, (c) => `-${c.toLowerCase()}`); fromEvent(this.elementRef.nativeElement, eventType) .pipe(auditTime(20), takeUntil(this.ngDestroy)) .subscribe((event) => { this[eventName].emit(event); }); }); }); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { this.ngDestroy.next(); this.ngDestroy.complete(); if (this.ro) { this.ro.disconnect(); } if (this.timeout && typeof window !== 'undefined') { window.clearTimeout(this.timeout); } this.zone.runOutsideAngular(() => { if (this.instance) { this.instance.destroy(); } }); this.instance = null; } } ngDoCheck() { if (!this.disabled && this.configDiff && isPlatformBrowser(this.platformId)) { const changes = this.configDiff.diff(this.config || {}); if (changes) { this.ngOnDestroy(); this.ngOnInit(); } } } ngOnChanges(changes) { if (changes['disabled'] && !changes['disabled'].isFirstChange() && isPlatformBrowser(this.platformId)) { if (changes['disabled'].currentValue !== changes['disabled'].previousValue) { if (changes['disabled'].currentValue === true) { this.ngOnDestroy(); } else if (changes['disabled'].currentValue === false) { this.ngOnInit(); } } } } ps() { return this.instance; } update() { if (typeof window !== 'undefined') { if (this.timeout) { window.clearTimeout(this.timeout); } this.timeout = window.setTimeout(() => { if (!this.disabled && this.configDiff) { try { this.zone.runOutsideAngular(() => { if (this.instance) { this.instance.update(); } }); } catch (error) { // Update can be finished after destroy so catch errors } } }, 0); } } geometry(prefix = 'scroll') { return new Geometry(this.elementRef.nativeElement[prefix + 'Left'], this.elementRef.nativeElement[prefix + 'Top'], this.elementRef.nativeElement[prefix + 'Width'], this.elementRef.nativeElement[prefix + 'Height']); } position(absolute = false) { if (!absolute && this.instance) { return new Position(this.instance.reach.x || 0, this.instance.reach.y || 0); } else { return new Position(this.elementRef.nativeElement.scrollLeft, this.elementRef.nativeElement.scrollTop); } } scrollable(direction = 'any') { const element = this.elementRef.nativeElement; if (direction === 'any') { return element.classList.contains('ps--active-x') || element.classList.contains('ps--active-y'); } else if (direction === 'both') { return element.classList.contains('ps--active-x') && element.classList.contains('ps--active-y'); } else { return element.classList.contains('ps--active-' + direction); } } scrollTo(x, y, speed) { if (!this.disabled) { if (y == null && speed == null) { this.animateScrolling('scrollTop', x, speed); } else { if (x != null) { this.animateScrolling('scrollLeft', x, speed); } if (y != null) { this.animateScrolling('scrollTop', y, speed); } } } } scrollToX(x, speed) { this.animateScrolling('scrollLeft', x, speed); } scrollToY(y, speed) { this.animateScrolling('scrollTop', y, speed); } scrollToTop(offset, speed) { this.animateScrolling('scrollTop', (offset || 0), speed); } scrollToLeft(offset, speed) { this.animateScrolling('scrollLeft', (offset || 0), speed); } scrollToRight(offset, speed) { const left = this.elementRef.nativeElement.scrollWidth - this.elementRef.nativeElement.clientWidth; this.animateScrolling('scrollLeft', left - (offset || 0), speed); } scrollToBottom(offset, speed) { const top = this.elementRef.nativeElement.scrollHeight - this.elementRef.nativeElement.clientHeight; this.animateScrolling('scrollTop', top - (offset || 0), speed); } scrollToElement(element, offset, speed) { if (typeof element === 'string') { element = this.elementRef.nativeElement.querySelector(element); } if (element) { const elementPos = element.getBoundingClientRect(); const scrollerPos = this.elementRef.nativeElement.getBoundingClientRect(); if (this.elementRef.nativeElement.classList.contains('ps--active-x')) { const currentPos = this.elementRef.nativeElement['scrollLeft']; const position = elementPos.left - scrollerPos.left + currentPos; this.animateScrolling('scrollLeft', position + (offset || 0), speed); } if (this.elementRef.nativeElement.classList.contains('ps--active-y')) { const currentPos = this.elementRef.nativeElement['scrollTop']; const position = elementPos.top - scrollerPos.top + currentPos; this.animateScrolling('scrollTop', position + (offset || 0), speed); } } } animateScrolling(target, value, speed) { if (this.animation) { window.cancelAnimationFrame(this.animation); this.animation = null; } if (!speed || typeof window === 'undefined') { this.elementRef.nativeElement[target] = value; } else if (value !== this.elementRef.nativeElement[target]) { let newValue = 0; let scrollCount = 0; let oldTimestamp = performance.now(); let oldValue = this.elementRef.nativeElement[target]; const cosParameter = (oldValue - value) / 2; const step = (newTimestamp) => { scrollCount += Math.PI / (speed / (newTimestamp - oldTimestamp)); newValue = Math.round(value + cosParameter + cosParameter * Math.cos(scrollCount)); // Only continue animation if scroll position has not changed if (this.elementRef.nativeElement[target] === oldValue) { if (scrollCount >= Math.PI) { this.animateScrolling(target, value, 0); } else { this.elementRef.nativeElement[target] = newValue; // On a zoomed out page the resulting offset may differ oldValue = this.elementRef.nativeElement[target]; oldTimestamp = newTimestamp; this.animation = window.requestAnimationFrame(step); } } }; window.requestAnimationFrame(step); } } } PerfectScrollbarDirective.decorators = [ { type: Directive, args: [{ selector: '[perfectScrollbar]', exportAs: 'ngxPerfectScrollbar' },] } ]; PerfectScrollbarDirective.ctorParameters = () => [ { type: NgZone }, { type: KeyValueDiffers }, { type: ElementRef }, { type: Object, decorators: [{ type: Inject, args: [PLATFORM_ID,] }] }, { type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [PERFECT_SCROLLBAR_CONFIG,] }] } ]; PerfectScrollbarDirective.propDecorators = { disabled: [{ type: Input }], config: [{ type: Input, args: ['perfectScrollbar',] }], psScrollY: [{ type: Output }], psScrollX: [{ type: Output }], psScrollUp: [{ type: Output }], psScrollDown: [{ type: Output }], psScrollLeft: [{ type: Output }], psScrollRight: [{ type: Output }], psYReachEnd: [{ type: Output }], psYReachStart: [{ type: Output }], psXReachEnd: [{ type: Output }], psXReachStart: [{ type: Output }] }; class PerfectScrollbarComponent { constructor(zone, cdRef, platformId) { this.zone = zone; this.cdRef = cdRef; this.platformId = platformId; this.states = {}; this.indicatorX = false; this.indicatorY = false; this.interaction = false; this.scrollPositionX = 0; this.scrollPositionY = 0; this.scrollDirectionX = 0; this.scrollDirectionY = 0; this.usePropagationX = false; this.usePropagationY = false; this.allowPropagationX = false; this.allowPropagationY = false; this.stateTimeout = null; this.ngDestroy = new Subject(); this.stateUpdate = new Subject(); this.disabled = false; this.usePSClass = true; this.autoPropagation = false; this.scrollIndicators = false; this.psScrollY = new EventEmitter(); this.psScrollX = new EventEmitter(); this.psScrollUp = new EventEmitter(); this.psScrollDown = new EventEmitter(); this.psScrollLeft = new EventEmitter(); this.psScrollRight = new EventEmitter(); this.psYReachEnd = new EventEmitter(); this.psYReachStart = new EventEmitter(); this.psXReachEnd = new EventEmitter(); this.psXReachStart = new EventEmitter(); } ngOnInit() { if (isPlatformBrowser(this.platformId)) { this.stateUpdate .pipe(takeUntil(this.ngDestroy), distinctUntilChanged((a, b) => (a === b && !this.stateTimeout))) .subscribe((state) => { if (this.stateTimeout && typeof window !== 'undefined') { window.clearTimeout(this.stateTimeout); this.stateTimeout = null; } if (state === 'x' || state === 'y') { this.interaction = false; if (state === 'x') { this.indicatorX = false; this.states.left = false; this.states.right = false; if (this.autoPropagation && this.usePropagationX) { this.allowPropagationX = false; } } else if (state === 'y') { this.indicatorY = false; this.states.top = false; this.states.bottom = false; if (this.autoPropagation && this.usePropagationY) { this.allowPropagationY = false; } } } else { if (state === 'left' || state === 'right') { this.states.left = false; this.states.right = false; this.states[state] = true; if (this.autoPropagation && this.usePropagationX) { this.indicatorX = true; } } else if (state === 'top' || state === 'bottom') { this.states.top = false; this.states.bottom = false; this.states[state] = true; if (this.autoPropagation && this.usePropagationY) { this.indicatorY = true; } } if (this.autoPropagation && typeof window !== 'undefined') { this.stateTimeout = window.setTimeout(() => { this.indicatorX = false; this.indicatorY = false; this.stateTimeout = null; if (this.interaction && (this.states.left || this.states.right)) { this.allowPropagationX = true; } if (this.interaction && (this.states.top || this.states.bottom)) { this.allowPropagationY = true; } this.cdRef.markForCheck(); }, 500); } } this.cdRef.markForCheck(); this.cdRef.detectChanges(); }); this.zone.runOutsideAngular(() => { if (this.directiveRef) { const element = this.directiveRef.elementRef.nativeElement; fromEvent(element, 'wheel') .pipe(takeUntil(this.ngDestroy)) .subscribe((event) => { if (!this.disabled && this.autoPropagation) { const scrollDeltaX = event.deltaX; const scrollDeltaY = event.deltaY; this.checkPropagation(event, scrollDeltaX, scrollDeltaY); } }); fromEvent(element, 'touchmove') .pipe(takeUntil(this.ngDestroy)) .subscribe((event) => { if (!this.disabled && this.autoPropagation) { const scrollPositionX = event.touches[0].clientX; const scrollPositionY = event.touches[0].clientY; const scrollDeltaX = scrollPositionX - this.scrollPositionX; const scrollDeltaY = scrollPositionY - this.scrollPositionY; this.checkPropagation(event, scrollDeltaX, scrollDeltaY); this.scrollPositionX = scrollPositionX; this.scrollPositionY = scrollPositionY; } }); merge(fromEvent(element, 'ps-scroll-x') .pipe(mapTo('x')), fromEvent(element, 'ps-scroll-y') .pipe(mapTo('y')), fromEvent(element, 'ps-x-reach-end') .pipe(mapTo('right')), fromEvent(element, 'ps-y-reach-end') .pipe(mapTo('bottom')), fromEvent(element, 'ps-x-reach-start') .pipe(mapTo('left')), fromEvent(element, 'ps-y-reach-start') .pipe(mapTo('top'))) .pipe(takeUntil(this.ngDestroy)) .subscribe((state) => { if (!this.disabled && (this.autoPropagation || this.scrollIndicators)) { this.stateUpdate.next(state); } }); } }); window.setTimeout(() => { PerfectScrollbarEvents.forEach((eventName) => { if (this.directiveRef) { this.directiveRef[eventName] = this[eventName]; } }); }, 0); } } ngOnDestroy() { if (isPlatformBrowser(this.platformId)) { this.ngDestroy.next(); this.ngDestroy.unsubscribe(); if (this.stateTimeout && typeof window !== 'undefined') { window.clearTimeout(this.stateTimeout); } } } ngDoCheck() { if (isPlatformBrowser(this.platformId)) { if (!this.disabled && this.autoPropagation && this.directiveRef) { const element = this.directiveRef.elementRef.nativeElement; this.usePropagationX = element.classList.contains('ps--active-x'); this.usePropagationY = element.classList.contains('ps--active-y'); } } } checkPropagation(event, deltaX, deltaY) { this.interaction = true; const scrollDirectionX = (deltaX < 0) ? -1 : 1; const scrollDirectionY = (deltaY < 0) ? -1 : 1; if ((this.usePropagationX && this.usePropagationY) || (this.usePropagationX && (!this.allowPropagationX || (this.scrollDirectionX !== scrollDirectionX))) || (this.usePropagationY && (!this.allowPropagationY || (this.scrollDirectionY !== scrollDirectionY)))) { event.preventDefault(); event.stopPropagation(); } if (!!deltaX) { this.scrollDirectionX = scrollDirectionX; } if (!!deltaY) { this.scrollDirectionY = scrollDirectionY; } this.stateUpdate.next('interaction'); this.cdRef.detectChanges(); } } PerfectScrollbarComponent.decorators = [ { type: Component, args: [{ selector: 'perfect-scrollbar', exportAs: 'ngxPerfectScrollbar', template: "