UNPKG

@akaustav/ngx-scroll-to

Version:

A simple Angular 12 plugin enabling you to smooth scroll to any element on your page and enhance scroll-based features in your app. This is a fork from @nicky-lenaers/ngx-scroll-to that adds support for Angular 12.

624 lines (611 loc) 27.7 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@angular/core'), require('@angular/common'), require('rxjs')) : typeof define === 'function' && define.amd ? define('@akaustav/ngx-scroll-to', ['exports', '@angular/core', '@angular/common', 'rxjs'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory((global.akaustav = global.akaustav || {}, global.akaustav["ngx-scroll-to"] = {}), global.ng.core, global.ng.common, global.rxjs)); })(this, (function (exports, i0, common, rxjs) { 'use strict'; function _interopNamespace(e) { if (e && e.__esModule) return e; var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n["default"] = e; return Object.freeze(n); } var i0__namespace = /*#__PURE__*/_interopNamespace(i0); /** Default values for Component Input */ var DEFAULTS = { target: null, action: 'click', duration: 650, easing: 'easeInOutQuad', offset: 0, offsetMap: new Map() }; /** Easing Colleciton */ var EASING = { easeInQuad: function (time) { return time * time; }, easeOutQuad: function (time) { return time * (2 - time); }, easeInOutQuad: function (time) { return time < 0.5 ? 2 * time * time : -1 + (4 - 2 * time) * time; }, easeInCubic: function (time) { return time * time * time; }, easeOutCubic: function (time) { return (--time) * time * time + 1; }, easeInOutCubic: function (time) { return time < 0.5 ? 4 * time * time * time : (time - 1) * (2 * time - 2) * (2 * time - 2) + 1; }, easeInQuart: function (time) { return time * time * time * time; }, easeOutQuart: function (time) { return 1 - (--time) * time * time * time; }, easeInOutQuart: function (time) { return time < 0.5 ? 8 * time * time * time * time : 1 - 8 * (--time) * time * time * time; }, easeInQuint: function (time) { return time * time * time * time * time; }, easeOutQuint: function (time) { return 1 + (--time) * time * time * time * time; }, easeInOutQuint: function (time) { return time < 0.5 ? 16 * time * time * time * time * time : 1 + 16 * (--time) * time * time * time * time; }, easeOutElastic: function (time) { return Math.pow(2, -10 * time) * Math.sin((time - 1 / 4) * (2 * Math.PI) / 1) + 1; } }; /** * Set of allowed events as triggers * for the Animation to start. */ var EVENTS = [ 'click', 'mouseenter', 'mouseover', 'mousedown', 'mouseup', 'dblclick', 'contextmenu', 'wheel', 'mouseleave', 'mouseout' ]; /** * Strip hash (#) from value. * * @param value The given string value * @returns The stripped string value */ function stripHash(value) { return value.substring(0, 1) === '#' ? value.substring(1) : value; } /** * Test if a given value is a string. * * @param value The given value * @returns Whether the given value is a string */ function isString(value) { return typeof value === 'string' || value instanceof String; } /** * Test if a given Element is the Window. * * @param container The given Element * @returns Whether the given Element is Window */ function isWindow(container) { return container === window; } /** * Test if a given value is of type ElementRef. * * @param value The given value * @returns Whether the given value is a number */ function isElementRef(value) { return value instanceof i0.ElementRef; } /** * Whether or not the given value is a Native Element. * * @param value The given value * @returns Whether or not the value is a Native Element */ function isNativeElement(value) { return value instanceof HTMLElement; } /** * Test if a given value is type number. * * @param value The given value * @returns Whether the given value is a number */ function isNumber(value) { return !isNaN(parseFloat(value)) && isFinite(value); } /** Scroll To Animation */ var ScrollToAnimation = /** @class */ (function () { /** * Class Constructor. * * @param container The Container * @param listenerTarget The Element that listens for DOM Events * @param isWindow Whether or not the listener is the Window * @param to Position to scroll to * @param options Additional options for scrolling * @param isBrowser Whether or not execution runs in the browser * (as opposed to the server) */ function ScrollToAnimation(container, listenerTarget, isWindow, to, options, isBrowser) { var _this = this; this.container = container; this.listenerTarget = listenerTarget; this.isWindow = isWindow; this.to = to; this.options = options; this.isBrowser = isBrowser; /** Recursively loop over the Scroll Animation */ this.loop = function () { _this.timeLapsed += _this.tick; _this.percentage = (_this.timeLapsed / _this.options.duration); _this.percentage = (_this.percentage > 1) ? 1 : _this.percentage; // Position Update _this.position = _this.startPosition + ((_this.startPosition - _this.to <= 0 ? 1 : -1) * _this.distance * EASING[_this.options.easing](_this.percentage)); if (_this.lastPosition !== null && _this.position === _this.lastPosition) { _this.stop(); } else { _this.source$.next(_this.position); _this.isWindow ? _this.listenerTarget.scrollTo(0, Math.floor(_this.position)) : _this.container.scrollTop = Math.floor(_this.position); _this.lastPosition = _this.position; } }; this.tick = 16; this.interval = null; this.lastPosition = null; this.timeLapsed = 0; this.windowScrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0; if (!this.container) { this.startPosition = this.windowScrollTop; } else { this.startPosition = this.isWindow ? this.windowScrollTop : this.container.scrollTop; } // Correction for Starting Position of nested HTML Elements if (this.container && !this.isWindow) { this.to = this.to - this.container.getBoundingClientRect().top + this.startPosition; } // Set Distance var directionalDistance = this.startPosition - this.to; this.distance = this.container ? Math.abs(this.startPosition - this.to) : this.to; this.mappedOffset = this.options.offset; // Set offset from Offset Map if (this.isBrowser) { this.options .offsetMap .forEach(function (value, key) { return _this.mappedOffset = window.innerWidth > key ? value : _this.mappedOffset; }); } this.distance += this.mappedOffset * (directionalDistance <= 0 ? 1 : -1); this.source$ = new rxjs.ReplaySubject(); } /** * Start the new Scroll Animation. * * @returns Observable containing a number */ ScrollToAnimation.prototype.start = function () { clearInterval(this.interval); this.interval = setInterval(this.loop, this.tick); return this.source$.asObservable(); }; /** * Stop the current Scroll Animation Loop. * * @param force Force to stop the Animation Loop * @returns Void */ ScrollToAnimation.prototype.stop = function () { clearInterval(this.interval); this.interval = null; this.source$.complete(); }; return ScrollToAnimation; }()); /** * The Scroll To Service handles starting, interrupting * and ending the actual Scroll Animation. It provides * some utilities to find the proper HTML Element on a * given page to setup Event Listeners and calculate * distances for the Animation. */ var ScrollToService = /** @class */ (function () { /** * Construct and setup required paratemeters. * * @param document A Reference to the Document * @param platformId Angular Platform ID */ function ScrollToService(document, platformId) { this.document = document; this.platformId = platformId; this.interruptiveEvents = ['mousewheel', 'DOMMouseScroll', 'touchstart']; } /** * Target an Element to scroll to. Notice that the `TimeOut` decorator * ensures the executing to take place in the next Angular lifecycle. * This allows for scrolling to elements that are e.g. initially hidden * by means of `*ngIf`, but ought to be scrolled to eventually. * * @todo type 'any' in Observable should become custom type like 'ScrollToEvent' (base class), see issue comment: * - https://github.com/nicky-lenaers/ngx-scroll-to/issues/10#issuecomment-317198481 * * @param options Configuration Object * @returns Observable */ ScrollToService.prototype.scrollTo = function (options) { if (!common.isPlatformBrowser(this.platformId)) { return new rxjs.ReplaySubject().asObservable(); } return this.start(options); }; /** * Start a new Animation. * * @todo Emit proper events from subscription * * @param options Configuration Object * @returns Observable */ ScrollToService.prototype.start = function (options) { var _this = this; // Merge config with default values var mergedConfigOptions = Object.assign(Object.assign({}, DEFAULTS), options); if (this.animation) { this.animation.stop(); } var targetNode = this.getNode(mergedConfigOptions.target); if (mergedConfigOptions.target && !targetNode) { return rxjs.throwError(function () { return new Error('Unable to find Target Element'); }); } var container = this.getContainer(mergedConfigOptions, targetNode); if (mergedConfigOptions.container && !container) { return rxjs.throwError(function () { return new Error('Unable to find Container Element'); }); } var listenerTarget = this.getListenerTarget(container) || window; var to = container ? container.getBoundingClientRect().top : 0; if (targetNode) { to = isWindow(listenerTarget) ? window.scrollY + targetNode.getBoundingClientRect().top : targetNode.getBoundingClientRect().top; } // Create Animation this.animation = new ScrollToAnimation(container, listenerTarget, isWindow(listenerTarget), to, mergedConfigOptions, common.isPlatformBrowser(this.platformId)); var onInterrupt = function () { return _this.animation.stop(); }; this.addInterruptiveEventListeners(listenerTarget, onInterrupt); // Start Animation var animation$ = this.animation.start(); this.subscribeToAnimation(animation$, listenerTarget, onInterrupt); return animation$; }; /** * Subscribe to the events emitted from the Scrolling * Animation. Events might be used for e.g. unsubscribing * once finished. * * @param animation$ The Animation Observable * @param listenerTarget The Listener Target for events * @param onInterrupt The handler for Interruptive Events * @returns Void */ ScrollToService.prototype.subscribeToAnimation = function (animation$, listenerTarget, onInterrupt) { var _this = this; var subscription = animation$ .subscribe({ complete: function () { _this.removeInterruptiveEventListeners(_this.interruptiveEvents, listenerTarget, onInterrupt); subscription.unsubscribe(); } }); }; /** * Get the container HTML Element in which * the scrolling should happen. * * @param options The Merged Configuration Object * @param targetNode the targeted HTMLElement */ ScrollToService.prototype.getContainer = function (options, targetNode) { var container = null; if (options.container) { container = this.getNode(options.container, true); } else if (targetNode) { container = this.getFirstScrollableParent(targetNode); } return container; }; /** * Add listeners for the Animation Interruptive Events * to the Listener Target. * * @param events List of events to listen to * @param listenerTarget Target to attach the listener on * @param handler Handler for when the listener fires * @returns Void */ ScrollToService.prototype.addInterruptiveEventListeners = function (listenerTarget, handler) { var _this = this; if (!listenerTarget) { listenerTarget = window; } this.interruptiveEvents .forEach(function (event) { return listenerTarget .addEventListener(event, handler, _this.supportPassive() ? { passive: true } : false); }); }; /** * Feature-detect support for passive event listeners. * * @returns Whether or not passive event listeners are supported */ ScrollToService.prototype.supportPassive = function () { var supportsPassive = false; try { var opts = Object.defineProperty({}, 'passive', { get: function () { supportsPassive = true; } }); window.addEventListener('testPassive', null, opts); window.removeEventListener('testPassive', null, opts); } catch (e) { } return supportsPassive; }; /** * Remove listeners for the Animation Interrupt Event from * the Listener Target. Specifying the correct handler prevents * memory leaks and makes the allocated memory available for * Garbage Collection. * * @param events List of Interruptive Events to remove * @param listenerTarget Target to attach the listener on * @param handler Handler for when the listener fires * @returns Void */ ScrollToService.prototype.removeInterruptiveEventListeners = function (events, listenerTarget, handler) { if (!listenerTarget) { listenerTarget = window; } events.forEach(function (event) { return listenerTarget.removeEventListener(event, handler); }); }; /** * Find the first scrollable parent Node of a given * Element. The DOM Tree gets searched upwards * to find this first scrollable parent. Parents might * be ignored by CSS styles applied to the HTML Element. * * @param nativeElement The Element to search the DOM Tree upwards from * @returns The first scrollable parent HTML Element */ ScrollToService.prototype.getFirstScrollableParent = function (nativeElement) { var style = window.getComputedStyle(nativeElement); var overflowRegex = /(auto|scroll|overlay)/; if (style.position === 'fixed') { return null; } var parent = nativeElement; while (parent.parentElement) { parent = parent.parentElement; style = window.getComputedStyle(parent); if (style.position === 'absolute' || style.overflow === 'hidden' || style.overflowY === 'hidden') { continue; } if (overflowRegex.test(style.overflow + style.overflowY) || parent.tagName === 'BODY') { return parent; } } return null; }; /** * Get the Target Node to scroll to. * * @param id The given ID of the node, either a string or * an element reference * @param allowBodyTag Indicate whether or not the Document Body is * considered a valid Target Node * @returns The Target Node to scroll to */ ScrollToService.prototype.getNode = function (id, allowBodyTag) { if (allowBodyTag === void 0) { allowBodyTag = false; } var targetNode; if (isString(id)) { if (allowBodyTag && (id === 'body' || id === 'BODY')) { targetNode = this.document.body; } else { targetNode = this.document.getElementById(stripHash(id)); } } else if (isNumber(id)) { targetNode = this.document.getElementById(String(id)); } else if (isElementRef(id)) { targetNode = id.nativeElement; } else if (isNativeElement(id)) { targetNode = id; } return targetNode; }; /** * Retrieve the Listener target. This Listener Target is used * to attach Event Listeners on. In case of the target being * the Document Body, we need the actual `window` to listen * for events. * * @param container The HTML Container element * @returns The Listener Target to attach events on */ ScrollToService.prototype.getListenerTarget = function (container) { if (!container) { return null; } return this.isDocumentBody(container) ? window : container; }; /** * Test if a given HTML Element is the Document Body. * * @param element The given HTML Element * @returns Whether or not the Element is the * Document Body Element */ ScrollToService.prototype.isDocumentBody = function (element) { return element.tagName.toUpperCase() === 'BODY'; }; return ScrollToService; }()); ScrollToService.ɵfac = i0__namespace.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.17", ngImport: i0__namespace, type: ScrollToService, deps: [{ token: common.DOCUMENT }, { token: i0.PLATFORM_ID }], target: i0__namespace.ɵɵFactoryTarget.Injectable }); ScrollToService.ɵprov = i0__namespace.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "12.2.17", ngImport: i0__namespace, type: ScrollToService }); i0__namespace.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.17", ngImport: i0__namespace, type: ScrollToService, decorators: [{ type: i0.Injectable }], ctorParameters: function () { return [{ type: undefined, decorators: [{ type: i0.Inject, args: [common.DOCUMENT] }] }, { type: undefined, decorators: [{ type: i0.Inject, args: [i0.PLATFORM_ID] }] }]; } }); var ScrollToDirective = /** @class */ (function () { function ScrollToDirective(elementRef, scrollToService, renderer2) { this.elementRef = elementRef; this.scrollToService = scrollToService; this.renderer2 = renderer2; this.ngxScrollTo = DEFAULTS.target; this.ngxScrollToEvent = DEFAULTS.action; this.ngxScrollToDuration = DEFAULTS.duration; this.ngxScrollToEasing = DEFAULTS.easing; this.ngxScrollToOffset = DEFAULTS.offset; this.ngxScrollToOffsetMap = DEFAULTS.offsetMap; } /** * Angular Lifecycle Hook - After View Init * * @todo Implement Subscription for Events * * @returns void */ ScrollToDirective.prototype.ngAfterViewInit = function () { var _this = this; // Test Event Support if (EVENTS.indexOf(this.ngxScrollToEvent) === -1) { throw new Error("Unsupported Event '" + this.ngxScrollToEvent + "'"); } // Listen for the trigger... this.renderer2.listen(this.elementRef.nativeElement, this.ngxScrollToEvent, function (event) { _this.options = { target: _this.ngxScrollTo, duration: _this.ngxScrollToDuration, easing: _this.ngxScrollToEasing, offset: _this.ngxScrollToOffset, offsetMap: _this.ngxScrollToOffsetMap }; _this.scrollToService.scrollTo(_this.options); }); }; return ScrollToDirective; }()); ScrollToDirective.ɵfac = i0__namespace.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.17", ngImport: i0__namespace, type: ScrollToDirective, deps: [{ token: i0__namespace.ElementRef }, { token: ScrollToService }, { token: i0__namespace.Renderer2 }], target: i0__namespace.ɵɵFactoryTarget.Directive }); ScrollToDirective.ɵdir = i0__namespace.ɵɵngDeclareDirective({ minVersion: "12.0.0", version: "12.2.17", type: ScrollToDirective, selector: "[ngxScrollTo]", inputs: { ngxScrollTo: "ngxScrollTo", ngxScrollToEvent: "ngxScrollToEvent", ngxScrollToDuration: "ngxScrollToDuration", ngxScrollToEasing: "ngxScrollToEasing", ngxScrollToOffset: "ngxScrollToOffset", ngxScrollToOffsetMap: "ngxScrollToOffsetMap" }, ngImport: i0__namespace }); i0__namespace.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.17", ngImport: i0__namespace, type: ScrollToDirective, decorators: [{ type: i0.Directive, args: [{ selector: '[ngxScrollTo]' }] }], ctorParameters: function () { return [{ type: i0__namespace.ElementRef }, { type: ScrollToService }, { type: i0__namespace.Renderer2 }]; }, propDecorators: { ngxScrollTo: [{ type: i0.Input }], ngxScrollToEvent: [{ type: i0.Input }], ngxScrollToDuration: [{ type: i0.Input }], ngxScrollToEasing: [{ type: i0.Input }], ngxScrollToOffset: [{ type: i0.Input }], ngxScrollToOffsetMap: [{ type: i0.Input }] } }); /** Scroll To Module */ var ScrollToModule = /** @class */ (function () { function ScrollToModule() { } /** * Guaranteed singletons for provided Services across App. * * @return An Angular Module with Providers */ ScrollToModule.forRoot = function () { return { ngModule: ScrollToModule, providers: [ ScrollToService ] }; }; return ScrollToModule; }()); ScrollToModule.ɵfac = i0__namespace.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.17", ngImport: i0__namespace, type: ScrollToModule, deps: [], target: i0__namespace.ɵɵFactoryTarget.NgModule }); ScrollToModule.ɵmod = i0__namespace.ɵɵngDeclareNgModule({ minVersion: "12.0.0", version: "12.2.17", ngImport: i0__namespace, type: ScrollToModule, declarations: [ScrollToDirective], exports: [ScrollToDirective] }); ScrollToModule.ɵinj = i0__namespace.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "12.2.17", ngImport: i0__namespace, type: ScrollToModule }); i0__namespace.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.17", ngImport: i0__namespace, type: ScrollToModule, decorators: [{ type: i0.NgModule, args: [{ declarations: [ ScrollToDirective ], exports: [ ScrollToDirective ] }] }] }); /* * Public API Surface of ngx-scroll-to */ /** * Generated bundle index. Do not edit. */ exports.ScrollToDirective = ScrollToDirective; exports.ScrollToModule = ScrollToModule; exports.ScrollToService = ScrollToService; Object.defineProperty(exports, '__esModule', { value: true }); })); //# sourceMappingURL=akaustav-ngx-scroll-to.umd.js.map