/** * @license * Copyright Google Inc. 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 */ /* tslint:disable:array-type member-access variable-name typedef only-arrow-functions directive-class-suffix component-class-suffix component-selector*/ import {Platform} from '@angular/cdk'; import {auditTime} from 'rxjs/operator/auditTime'; import {ElementRef, Injectable, NgZone, Optional, SkipSelf} from '@angular/core'; import {fromEvent} from 'rxjs/observable/fromEvent'; import {merge} from 'rxjs/observable/merge'; import {Subject} from 'rxjs/Subject'; import {Subscription} from 'rxjs/Subscription'; import {Scrollable} from './scrollable'; /** Time in ms to throttle the scrolling events by default. */ export const DEFAULT_SCROLL_TIME = 20; /** * Service contained all registered Scrollable references and emits an event * when any one of the Scrollable references emit a scrolled event. */ @Injectable() export class ScrollDispatcher { /** Subject for notifying that a registered scrollable reference element has * been scrolled. */ _scrolled: Subject = new Subject(); /** Keeps track of the global `scroll` and `resize` subscriptions. */ _globalSubscription: Subscription|null = null; /** Keeps track of the amount of subscriptions to `scrolled`. Used for * cleaning up afterwards. */ private _scrolledCount = 0; /** * Map of all the scrollable references that are registered with the service * and their scroll event subscriptions. */ scrollableReferences: Map = new Map(); constructor(private _ngZone: NgZone, private _platform: Platform) {} /** * Registers a Scrollable with the service and listens for its scrolled * events. When the scrollable is scrolled, the service emits the event in its * scrolled observable. * @param scrollable Scrollable instance to be registered. */ register(scrollable: Scrollable): void { const scrollSubscription = scrollable.elementScrolled().subscribe(() => this._notify()); this.scrollableReferences.set(scrollable, scrollSubscription); } /** * Deregisters a Scrollable reference and unsubscribes from its scroll event * observable. * @param scrollable Scrollable instance to be deregistered. */ deregister(scrollable: Scrollable): void { const scrollableReference = this.scrollableReferences.get(scrollable); if (scrollableReference) { scrollableReference.unsubscribe(); this.scrollableReferences.delete(scrollable); } } /** * Subscribes to an observable that emits an event whenever any of the * registered Scrollable references (or window, document, or body) fire a * scrolled event. Can provide a time in ms to override the default "throttle" * time. */ scrolled(auditTimeInMs: number = DEFAULT_SCROLL_TIME, callback: () => any): Subscription { // Scroll events can only happen on the browser, so do nothing if we're not // on the browser. if (!this._platform.isBrowser) { return Subscription.EMPTY; } // In the case of a 0ms delay, use an observable without auditTime // since it does add a perceptible delay in processing overhead. const observable = auditTimeInMs > 0 ? auditTime.call(this._scrolled.asObservable(), auditTimeInMs) : this._scrolled.asObservable(); this._scrolledCount++; if (!this._globalSubscription) { this._globalSubscription = this._ngZone.runOutsideAngular(() => { return merge( fromEvent(window.document, 'scroll'), fromEvent(window, 'resize')) .subscribe(() => this._notify()); }); } // Note that we need to do the subscribing from here, in order to be able to // remove the global event listeners once there are no more subscriptions. const subscription = observable.subscribe(callback); subscription.add(() => { this._scrolledCount--; if (this._globalSubscription && !this.scrollableReferences.size && !this._scrolledCount) { this._globalSubscription.unsubscribe(); this._globalSubscription = null; } }); return subscription; } /** Returns all registered Scrollables that contain the provided element. */ getScrollContainers(elementRef: ElementRef): Scrollable[] { const scrollingContainers: Scrollable[] = []; this.scrollableReferences.forEach( (_subscription: Subscription, scrollable: Scrollable) => { if (this.scrollableContainsElement(scrollable, elementRef)) { scrollingContainers.push(scrollable); } }); return scrollingContainers; } /** Returns true if the element is contained within the provided Scrollable. */ scrollableContainsElement(scrollable: Scrollable, elementRef: ElementRef): boolean { let element = elementRef.nativeElement; const scrollableElement = scrollable.getElementRef().nativeElement; // Traverse through the element parents until we reach null, checking if any // of the elements are the scrollable's element. do { if (element === scrollableElement) { return true; } } while (element = element.parentElement); return false; } /** Sends a notification that a scroll event has been fired. */ _notify() { this._scrolled.next(); } } export function SCROLL_DISPATCHER_PROVIDER_FACTORY( parentDispatcher: ScrollDispatcher, ngZone: NgZone, platform: Platform) { return parentDispatcher || new ScrollDispatcher(ngZone, platform); } export const SCROLL_DISPATCHER_PROVIDER = { // If there is already a ScrollDispatcher available, use that. Otherwise, // provide a new one. provide: ScrollDispatcher, deps: [[new Optional(), new SkipSelf(), ScrollDispatcher], NgZone, Platform], useFactory: SCROLL_DISPATCHER_PROVIDER_FACTORY };