3 * Copyright Google Inc. All Rights Reserved.
5 * Use of this source code is governed by an MIT-style license that can be
6 * found in the LICENSE file at https://angular.io/license
8 /* tslint:disable:array-type member-access variable-name typedef
9 only-arrow-functions directive-class-suffix component-class-suffix
11 import {Platform} from '@angular/cdk';
12 import {auditTime} from 'rxjs/operator/auditTime';
13 import {ElementRef, Injectable, NgZone, Optional, SkipSelf} from '@angular/core';
14 import {fromEvent} from 'rxjs/observable/fromEvent';
15 import {merge} from 'rxjs/observable/merge';
16 import {Subject} from 'rxjs/Subject';
17 import {Subscription} from 'rxjs/Subscription';
19 import {Scrollable} from './scrollable';
22 /** Time in ms to throttle the scrolling events by default. */
23 export const DEFAULT_SCROLL_TIME = 20;
26 * Service contained all registered Scrollable references and emits an event
27 * when any one of the Scrollable references emit a scrolled event.
30 export class ScrollDispatcher {
31 /** Subject for notifying that a registered scrollable reference element has
33 _scrolled: Subject<void> = new Subject<void>();
35 /** Keeps track of the global `scroll` and `resize` subscriptions. */
36 _globalSubscription: Subscription|null = null;
38 /** Keeps track of the amount of subscriptions to `scrolled`. Used for
39 * cleaning up afterwards. */
40 private _scrolledCount = 0;
43 * Map of all the scrollable references that are registered with the service
44 * and their scroll event subscriptions.
46 scrollableReferences: Map<Scrollable, Subscription> = new Map();
48 constructor(private _ngZone: NgZone, private _platform: Platform) {}
51 * Registers a Scrollable with the service and listens for its scrolled
52 * events. When the scrollable is scrolled, the service emits the event in its
53 * scrolled observable.
54 * @param scrollable Scrollable instance to be registered.
56 register(scrollable: Scrollable): void {
57 const scrollSubscription =
58 scrollable.elementScrolled().subscribe(() => this._notify());
60 this.scrollableReferences.set(scrollable, scrollSubscription);
64 * Deregisters a Scrollable reference and unsubscribes from its scroll event
66 * @param scrollable Scrollable instance to be deregistered.
68 deregister(scrollable: Scrollable): void {
69 const scrollableReference = this.scrollableReferences.get(scrollable);
71 if (scrollableReference) {
72 scrollableReference.unsubscribe();
73 this.scrollableReferences.delete(scrollable);
78 * Subscribes to an observable that emits an event whenever any of the
79 * registered Scrollable references (or window, document, or body) fire a
80 * scrolled event. Can provide a time in ms to override the default "throttle"
83 scrolled(auditTimeInMs: number = DEFAULT_SCROLL_TIME, callback: () => any):
85 // Scroll events can only happen on the browser, so do nothing if we're not
87 if (!this._platform.isBrowser) {
88 return Subscription.EMPTY;
91 // In the case of a 0ms delay, use an observable without auditTime
92 // since it does add a perceptible delay in processing overhead.
93 const observable = auditTimeInMs > 0 ?
94 auditTime.call(this._scrolled.asObservable(), auditTimeInMs) :
95 this._scrolled.asObservable();
97 this._scrolledCount++;
99 if (!this._globalSubscription) {
100 this._globalSubscription = this._ngZone.runOutsideAngular(() => {
102 fromEvent(window.document, 'scroll'),
103 fromEvent(window, 'resize'))
104 .subscribe(() => this._notify());
108 // Note that we need to do the subscribing from here, in order to be able to
109 // remove the global event listeners once there are no more subscriptions.
110 const subscription = observable.subscribe(callback);
112 subscription.add(() => {
113 this._scrolledCount--;
115 if (this._globalSubscription && !this.scrollableReferences.size &&
116 !this._scrolledCount) {
117 this._globalSubscription.unsubscribe();
118 this._globalSubscription = null;
125 /** Returns all registered Scrollables that contain the provided element. */
126 getScrollContainers(elementRef: ElementRef): Scrollable[] {
127 const scrollingContainers: Scrollable[] = [];
129 this.scrollableReferences.forEach(
130 (_subscription: Subscription, scrollable: Scrollable) => {
131 if (this.scrollableContainsElement(scrollable, elementRef)) {
132 scrollingContainers.push(scrollable);
136 return scrollingContainers;
139 /** Returns true if the element is contained within the provided Scrollable.
141 scrollableContainsElement(scrollable: Scrollable, elementRef: ElementRef):
143 let element = elementRef.nativeElement;
144 const scrollableElement = scrollable.getElementRef().nativeElement;
146 // Traverse through the element parents until we reach null, checking if any
147 // of the elements are the scrollable's element.
149 if (element === scrollableElement) {
152 } while (element = element.parentElement);
157 /** Sends a notification that a scroll event has been fired. */
159 this._scrolled.next();
163 export function SCROLL_DISPATCHER_PROVIDER_FACTORY(
164 parentDispatcher: ScrollDispatcher, ngZone: NgZone, platform: Platform) {
165 return parentDispatcher || new ScrollDispatcher(ngZone, platform);
168 export const SCROLL_DISPATCHER_PROVIDER = {
169 // If there is already a ScrollDispatcher available, use that. Otherwise,
170 // provide a new one.
171 provide: ScrollDispatcher,
172 deps: [[new Optional(), new SkipSelf(), ScrollDispatcher], NgZone, Platform],
173 useFactory: SCROLL_DISPATCHER_PROVIDER_FACTORY