2c145af555464f556918c2fd21472e4ca80f49e9
[sdc/sdc-workflow-designer.git] /
1 /**
2  * @license
3  * Copyright Google Inc. All Rights Reserved.
4  *
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
7  */
8 /* tslint:disable:array-type member-access variable-name typedef
9  only-arrow-functions directive-class-suffix component-class-suffix
10  component-selector*/
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';
18
19 import {Scrollable} from './scrollable';
20
21
22 /** Time in ms to throttle the scrolling events by default. */
23 export const DEFAULT_SCROLL_TIME = 20;
24
25 /**
26  * Service contained all registered Scrollable references and emits an event
27  * when any one of the Scrollable references emit a scrolled event.
28  */
29 @Injectable()
30 export class ScrollDispatcher {
31   /** Subject for notifying that a registered scrollable reference element has
32    * been scrolled. */
33   _scrolled: Subject<void> = new Subject<void>();
34
35   /** Keeps track of the global `scroll` and `resize` subscriptions. */
36   _globalSubscription: Subscription|null = null;
37
38   /** Keeps track of the amount of subscriptions to `scrolled`. Used for
39    * cleaning up afterwards. */
40   private _scrolledCount = 0;
41
42   /**
43    * Map of all the scrollable references that are registered with the service
44    * and their scroll event subscriptions.
45    */
46   scrollableReferences: Map<Scrollable, Subscription> = new Map();
47
48   constructor(private _ngZone: NgZone, private _platform: Platform) {}
49
50   /**
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.
55    */
56   register(scrollable: Scrollable): void {
57         const scrollSubscription =
58                 scrollable.elementScrolled().subscribe(() => this._notify());
59
60         this.scrollableReferences.set(scrollable, scrollSubscription);
61   }
62
63   /**
64    * Deregisters a Scrollable reference and unsubscribes from its scroll event
65    * observable.
66    * @param scrollable Scrollable instance to be deregistered.
67    */
68   deregister(scrollable: Scrollable): void {
69         const scrollableReference = this.scrollableReferences.get(scrollable);
70
71         if (scrollableReference) {
72                 scrollableReference.unsubscribe();
73                 this.scrollableReferences.delete(scrollable);
74         }
75   }
76
77   /**
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"
81    * time.
82    */
83   scrolled(auditTimeInMs: number = DEFAULT_SCROLL_TIME, callback: () => any):
84                 Subscription {
85     // Scroll events can only happen on the browser, so do nothing if we're not
86     // on the browser.
87         if (!this._platform.isBrowser) {
88                 return Subscription.EMPTY;
89         }
90
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();
96
97         this._scrolledCount++;
98
99         if (!this._globalSubscription) {
100                 this._globalSubscription = this._ngZone.runOutsideAngular(() => {
101                 return merge(
102                                         fromEvent(window.document, 'scroll'),
103                                         fromEvent(window, 'resize'))
104                         .subscribe(() => this._notify());
105                 });
106         }
107
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);
111
112         subscription.add(() => {
113                 this._scrolledCount--;
114
115                 if (this._globalSubscription && !this.scrollableReferences.size &&
116                         !this._scrolledCount) {
117                 this._globalSubscription.unsubscribe();
118                 this._globalSubscription = null;
119                 }
120         });
121
122         return subscription;
123   }
124
125   /** Returns all registered Scrollables that contain the provided element. */
126   getScrollContainers(elementRef: ElementRef): Scrollable[] {
127         const scrollingContainers: Scrollable[] = [];
128
129         this.scrollableReferences.forEach(
130                 (_subscription: Subscription, scrollable: Scrollable) => {
131                         if (this.scrollableContainsElement(scrollable, elementRef)) {
132                         scrollingContainers.push(scrollable);
133                         }
134                 });
135
136         return scrollingContainers;
137   }
138
139   /** Returns true if the element is contained within the provided Scrollable.
140    */
141   scrollableContainsElement(scrollable: Scrollable, elementRef: ElementRef):
142                 boolean {
143         let element = elementRef.nativeElement;
144         const scrollableElement = scrollable.getElementRef().nativeElement;
145
146     // Traverse through the element parents until we reach null, checking if any
147     // of the elements are the scrollable's element.
148         do {
149                 if (element === scrollableElement) {
150                 return true;
151                 }
152         } while (element = element.parentElement);
153
154         return false;
155   }
156
157   /** Sends a notification that a scroll event has been fired. */
158   _notify() {
159         this._scrolled.next();
160   }
161 }
162
163 export function SCROLL_DISPATCHER_PROVIDER_FACTORY(
164         parentDispatcher: ScrollDispatcher, ngZone: NgZone, platform: Platform) {
165   return parentDispatcher || new ScrollDispatcher(ngZone, platform);
166 }
167
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
174 };