UNPKG

@angular/cdk

Version:

Angular Material Component Development Kit

441 lines (433 loc) 16.3 kB
/** * @license * Copyright Google LLC 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 */ import { Platform, PlatformModule } from '@angular/cdk/platform'; import { Injectable, NgZone, Optional, SkipSelf, Directive, ElementRef, NgModule, defineInjectable, inject } from '@angular/core'; import { fromEvent, of, Subject, Observable, merge } from 'rxjs'; import { auditTime, filter } from 'rxjs/operators'; /** * @fileoverview added by tsickle * @suppress {checkTypes} checked by tsc */ /** * Time in ms to throttle the scrolling events by default. */ const /** @type {?} */ 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. */ class ScrollDispatcher { /** * @param {?} _ngZone * @param {?} _platform */ constructor(_ngZone, _platform) { this._ngZone = _ngZone; this._platform = _platform; /** * Subject for notifying that a registered scrollable reference element has been scrolled. */ this._scrolled = new Subject(); /** * Keeps track of the global `scroll` and `resize` subscriptions. */ this._globalSubscription = null; /** * Keeps track of the amount of subscriptions to `scrolled`. Used for cleaning up afterwards. */ this._scrolledCount = 0; /** * Map of all the scrollable references that are registered with the service and their * scroll event subscriptions. */ this.scrollContainers = new Map(); } /** * Registers a scrollable instance with the service and listens for its scrolled events. When the * scrollable is scrolled, the service emits the event to its scrolled observable. * @param {?} scrollable Scrollable instance to be registered. * @return {?} */ register(scrollable) { const /** @type {?} */ scrollSubscription = scrollable.elementScrolled() .subscribe(() => this._scrolled.next(scrollable)); this.scrollContainers.set(scrollable, scrollSubscription); } /** * Deregisters a Scrollable reference and unsubscribes from its scroll event observable. * @param {?} scrollable Scrollable instance to be deregistered. * @return {?} */ deregister(scrollable) { const /** @type {?} */ scrollableReference = this.scrollContainers.get(scrollable); if (scrollableReference) { scrollableReference.unsubscribe(); this.scrollContainers.delete(scrollable); } } /** * Returns 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. * * **Note:** in order to avoid hitting change detection for every scroll event, * all of the events emitted from this stream will be run outside the Angular zone. * If you need to update any data bindings as a result of a scroll event, you have * to run the callback using `NgZone.run`. * @param {?=} auditTimeInMs * @return {?} */ scrolled(auditTimeInMs = DEFAULT_SCROLL_TIME) { return this._platform.isBrowser ? Observable.create(observer => { if (!this._globalSubscription) { this._addGlobalListener(); } // In the case of a 0ms delay, use an observable without auditTime // since it does add a perceptible delay in processing overhead. const /** @type {?} */ subscription = auditTimeInMs > 0 ? this._scrolled.pipe(auditTime(auditTimeInMs)).subscribe(observer) : this._scrolled.subscribe(observer); this._scrolledCount++; return () => { subscription.unsubscribe(); this._scrolledCount--; if (!this._scrolledCount) { this._removeGlobalListener(); } }; }) : of(); } /** * @return {?} */ ngOnDestroy() { this._removeGlobalListener(); this.scrollContainers.forEach((_, container) => this.deregister(container)); this._scrolled.complete(); } /** * Returns an observable that emits whenever any of the * scrollable ancestors of an element are scrolled. * @param {?} elementRef Element whose ancestors to listen for. * @param {?=} auditTimeInMs Time to throttle the scroll events. * @return {?} */ ancestorScrolled(elementRef, auditTimeInMs) { const /** @type {?} */ ancestors = this.getAncestorScrollContainers(elementRef); return this.scrolled(auditTimeInMs).pipe(filter(target => { return !target || ancestors.indexOf(target) > -1; })); } /** * Returns all registered Scrollables that contain the provided element. * @param {?} elementRef * @return {?} */ getAncestorScrollContainers(elementRef) { const /** @type {?} */ scrollingContainers = []; this.scrollContainers.forEach((_subscription, scrollable) => { if (this._scrollableContainsElement(scrollable, elementRef)) { scrollingContainers.push(scrollable); } }); return scrollingContainers; } /** * Returns true if the element is contained within the provided Scrollable. * @param {?} scrollable * @param {?} elementRef * @return {?} */ _scrollableContainsElement(scrollable, elementRef) { let /** @type {?} */ element = elementRef.nativeElement; let /** @type {?} */ 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 = /** @type {?} */ ((element)).parentElement); return false; } /** * Sets up the global scroll listeners. * @return {?} */ _addGlobalListener() { this._globalSubscription = this._ngZone.runOutsideAngular(() => { return fromEvent(window.document, 'scroll').subscribe(() => this._scrolled.next()); }); } /** * Cleans up the global scroll listener. * @return {?} */ _removeGlobalListener() { if (this._globalSubscription) { this._globalSubscription.unsubscribe(); this._globalSubscription = null; } } } ScrollDispatcher.decorators = [ { type: Injectable, args: [{ providedIn: 'root' },] }, ]; /** @nocollapse */ ScrollDispatcher.ctorParameters = () => [ { type: NgZone, }, { type: Platform, }, ]; /** @nocollapse */ ScrollDispatcher.ngInjectableDef = defineInjectable({ factory: function ScrollDispatcher_Factory() { return new ScrollDispatcher(inject(NgZone), inject(Platform)); }, token: ScrollDispatcher, providedIn: "root" }); /** * \@docs-private \@deprecated \@breaking-change 7.0.0 * @param {?} parentDispatcher * @param {?} ngZone * @param {?} platform * @return {?} */ function SCROLL_DISPATCHER_PROVIDER_FACTORY(parentDispatcher, ngZone, platform) { return parentDispatcher || new ScrollDispatcher(ngZone, platform); } /** * \@docs-private \@deprecated \@breaking-change 7.0.0 */ const /** @type {?} */ 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 }; /** * @fileoverview added by tsickle * @suppress {checkTypes} checked by tsc */ /** * Sends an event when the directive's element is scrolled. Registers itself with the * ScrollDispatcher service to include itself as part of its collection of scrolling events that it * can be listened to through the service. */ class CdkScrollable { /** * @param {?} _elementRef * @param {?} _scroll * @param {?} _ngZone */ constructor(_elementRef, _scroll, _ngZone) { this._elementRef = _elementRef; this._scroll = _scroll; this._ngZone = _ngZone; this._elementScrolled = new Subject(); this._scrollListener = (event) => this._elementScrolled.next(event); } /** * @return {?} */ ngOnInit() { this._ngZone.runOutsideAngular(() => { this.getElementRef().nativeElement.addEventListener('scroll', this._scrollListener); }); this._scroll.register(this); } /** * @return {?} */ ngOnDestroy() { this._scroll.deregister(this); if (this._scrollListener) { this.getElementRef().nativeElement.removeEventListener('scroll', this._scrollListener); } this._elementScrolled.complete(); } /** * Returns observable that emits when a scroll event is fired on the host element. * @return {?} */ elementScrolled() { return this._elementScrolled.asObservable(); } /** * @return {?} */ getElementRef() { return this._elementRef; } } CdkScrollable.decorators = [ { type: Directive, args: [{ selector: '[cdk-scrollable], [cdkScrollable]' },] }, ]; /** @nocollapse */ CdkScrollable.ctorParameters = () => [ { type: ElementRef, }, { type: ScrollDispatcher, }, { type: NgZone, }, ]; /** * @fileoverview added by tsickle * @suppress {checkTypes} checked by tsc */ /** * Time in ms to throttle the resize events by default. */ const /** @type {?} */ DEFAULT_RESIZE_TIME = 20; /** * Simple utility for getting the bounds of the browser viewport. * \@docs-private */ class ViewportRuler { /** * @param {?} _platform * @param {?} ngZone */ constructor(_platform, ngZone) { this._platform = _platform; this._change = _platform.isBrowser ? ngZone.runOutsideAngular(() => { return merge(fromEvent(window, 'resize'), fromEvent(window, 'orientationchange')); }) : of(); this._invalidateCache = this.change().subscribe(() => this._updateViewportSize()); } /** * @return {?} */ ngOnDestroy() { this._invalidateCache.unsubscribe(); } /** * Returns the viewport's width and height. * @return {?} */ getViewportSize() { if (!this._viewportSize) { this._updateViewportSize(); } const /** @type {?} */ output = { width: this._viewportSize.width, height: this._viewportSize.height }; // If we're not on a browser, don't cache the size since it'll be mocked out anyway. if (!this._platform.isBrowser) { this._viewportSize = /** @type {?} */ ((null)); } return output; } /** * Gets a ClientRect for the viewport's bounds. * @return {?} */ getViewportRect() { // Use the document element's bounding rect rather than the window scroll properties // (e.g. pageYOffset, scrollY) due to in issue in Chrome and IE where window scroll // properties and client coordinates (boundingClientRect, clientX/Y, etc.) are in different // conceptual viewports. Under most circumstances these viewports are equivalent, but they // can disagree when the page is pinch-zoomed (on devices that support touch). // See https://bugs.chromium.org/p/chromium/issues/detail?id=489206#c4 // We use the documentElement instead of the body because, by default (without a css reset) // browsers typically give the document body an 8px margin, which is not included in // getBoundingClientRect(). const /** @type {?} */ scrollPosition = this.getViewportScrollPosition(); const { width, height } = this.getViewportSize(); return { top: scrollPosition.top, left: scrollPosition.left, bottom: scrollPosition.top + height, right: scrollPosition.left + width, height, width, }; } /** * Gets the (top, left) scroll position of the viewport. * @return {?} */ getViewportScrollPosition() { // While we can get a reference to the fake document // during SSR, it doesn't have getBoundingClientRect. if (!this._platform.isBrowser) { return { top: 0, left: 0 }; } // The top-left-corner of the viewport is determined by the scroll position of the document // body, normally just (scrollLeft, scrollTop). However, Chrome and Firefox disagree about // whether `document.body` or `document.documentElement` is the scrolled element, so reading // `scrollTop` and `scrollLeft` is inconsistent. However, using the bounding rect of // `document.documentElement` works consistently, where the `top` and `left` values will // equal negative the scroll position. const /** @type {?} */ documentRect = document.documentElement.getBoundingClientRect(); const /** @type {?} */ top = -documentRect.top || document.body.scrollTop || window.scrollY || document.documentElement.scrollTop || 0; const /** @type {?} */ left = -documentRect.left || document.body.scrollLeft || window.scrollX || document.documentElement.scrollLeft || 0; return { top, left }; } /** * Returns a stream that emits whenever the size of the viewport changes. * @param {?=} throttleTime Time in milliseconds to throttle the stream. * @return {?} */ change(throttleTime = DEFAULT_RESIZE_TIME) { return throttleTime > 0 ? this._change.pipe(auditTime(throttleTime)) : this._change; } /** * Updates the cached viewport size. * @return {?} */ _updateViewportSize() { this._viewportSize = this._platform.isBrowser ? { width: window.innerWidth, height: window.innerHeight } : { width: 0, height: 0 }; } } ViewportRuler.decorators = [ { type: Injectable, args: [{ providedIn: 'root' },] }, ]; /** @nocollapse */ ViewportRuler.ctorParameters = () => [ { type: Platform, }, { type: NgZone, }, ]; /** @nocollapse */ ViewportRuler.ngInjectableDef = defineInjectable({ factory: function ViewportRuler_Factory() { return new ViewportRuler(inject(Platform), inject(NgZone)); }, token: ViewportRuler, providedIn: "root" }); /** * \@docs-private \@deprecated \@breaking-change 7.0.0 * @param {?} parentRuler * @param {?} platform * @param {?} ngZone * @return {?} */ function VIEWPORT_RULER_PROVIDER_FACTORY(parentRuler, platform, ngZone) { return parentRuler || new ViewportRuler(platform, ngZone); } /** * \@docs-private \@deprecated \@breaking-change 7.0.0 */ const /** @type {?} */ VIEWPORT_RULER_PROVIDER = { // If there is already a ViewportRuler available, use that. Otherwise, provide a new one. provide: ViewportRuler, deps: [[new Optional(), new SkipSelf(), ViewportRuler], Platform, NgZone], useFactory: VIEWPORT_RULER_PROVIDER_FACTORY }; /** * @fileoverview added by tsickle * @suppress {checkTypes} checked by tsc */ class ScrollDispatchModule { } ScrollDispatchModule.decorators = [ { type: NgModule, args: [{ imports: [PlatformModule], exports: [CdkScrollable], declarations: [CdkScrollable], },] }, ]; /** * @fileoverview added by tsickle * @suppress {checkTypes} checked by tsc */ /** * @fileoverview added by tsickle * @suppress {checkTypes} checked by tsc */ export { DEFAULT_SCROLL_TIME, ScrollDispatcher, SCROLL_DISPATCHER_PROVIDER_FACTORY, SCROLL_DISPATCHER_PROVIDER, CdkScrollable, DEFAULT_RESIZE_TIME, ViewportRuler, VIEWPORT_RULER_PROVIDER_FACTORY, VIEWPORT_RULER_PROVIDER, ScrollDispatchModule }; //# sourceMappingURL=scrolling.js.map