UNPKG

ngx-scroll-position-restoration

Version:
950 lines (943 loc) 53.7 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@angular/core'), require('@angular/common'), require('@angular/router'), require('rxjs'), require('rxjs/operators'), require('@medv/finder')) : typeof define === 'function' && define.amd ? define('ngx-scroll-position-restoration', ['exports', '@angular/core', '@angular/common', '@angular/router', 'rxjs', 'rxjs/operators', '@medv/finder'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global["ngx-scroll-position-restoration"] = {}, global.ng.core, global.ng.common, global.ng.router, global.rxjs, global.rxjs.operators, global.finder)); })(this, (function (exports, core, common, router, rxjs, operators, finder) { 'use strict'; /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise */ var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; return extendStatics(d, b); }; function __extends(d, b) { if (typeof b !== "function" && b !== null) throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); } var __assign = function () { __assign = Object.assign || function __assign(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; function __rest(s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; } function __decorate(decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; } function __param(paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); }; } function __metadata(metadataKey, metadataValue) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(metadataKey, metadataValue); } function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } function __generator(thisArg, body) { var _ = { label: 0, sent: function () { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function () { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (_) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } } var __createBinding = Object.create ? (function (o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function () { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function (o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; }); function __exportStar(m, o) { for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(o, p)) __createBinding(o, m, p); } function __values(o) { var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; if (m) return m.call(o); if (o && typeof o.length === "number") return { next: function () { if (o && i >= o.length) o = void 0; return { value: o && o[i++], done: !o }; } }; throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); } function __read(o, n) { var m = typeof Symbol === "function" && o[Symbol.iterator]; if (!m) return o; var i = m.call(o), r, ar = [], e; try { while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); } catch (error) { e = { error: error }; } finally { try { if (r && !r.done && (m = i["return"])) m.call(i); } finally { if (e) throw e.error; } } return ar; } /** @deprecated */ function __spread() { for (var ar = [], i = 0; i < arguments.length; i++) ar = ar.concat(__read(arguments[i])); return ar; } /** @deprecated */ function __spreadArrays() { for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length; for (var r = Array(s), k = 0, i = 0; i < il; i++) for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++) r[k] = a[j]; return r; } function __spreadArray(to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); } function __await(v) { return this instanceof __await ? (this.v = v, this) : new __await(v); } function __asyncGenerator(thisArg, _arguments, generator) { if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); var g = generator.apply(thisArg, _arguments || []), i, q = []; return i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i; function verb(n) { if (g[n]) i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; } function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } } function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); } function fulfill(value) { resume("next", value); } function reject(value) { resume("throw", value); } function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); } } function __asyncDelegator(o) { var i, p; return i = {}, verb("next"), verb("throw", function (e) { throw e; }), verb("return"), i[Symbol.iterator] = function () { return this; }, i; function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: n === "return" } : f ? f(v) : v; } : f; } } function __asyncValues(o) { if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); var m = o[Symbol.asyncIterator], i; return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i); function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; } function settle(resolve, reject, d, v) { Promise.resolve(v).then(function (v) { resolve({ value: v, done: d }); }, reject); } } function __makeTemplateObject(cooked, raw) { if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; } return cooked; } ; var __setModuleDefault = Object.create ? (function (o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function (o, v) { o["default"] = v; }; function __importStar(mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; } function __importDefault(mod) { return (mod && mod.__esModule) ? mod : { default: mod }; } function __classPrivateFieldGet(receiver, state, kind, f) { if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); } function __classPrivateFieldSet(receiver, state, value, kind, f) { if (kind === "m") throw new TypeError("Private method is not writable"); if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; } function __classPrivateFieldIn(state, receiver) { if (receiver === null || (typeof receiver !== "object" && typeof receiver !== "function")) throw new TypeError("Cannot use 'in' operator on non-object"); return typeof state === "function" ? receiver === state : state.has(receiver); } var WINDOW_SELECTOR = '__window-selector__'; /** * DomUtils * * I provide a unified interface for dealing with scroll offsets across different types of targets (elements vs. windows). */ /** * I get the scroll-top of the given target in the active DOM. */ function getScrollTop(target) { if (target instanceof Window) { return window.scrollY; } else { return target.scrollTop; } } /** * I return the CSS selector for the given target. * ___ * NOTE: The generated selector is intended to be consumed by this class only - it may not produce a valid CSS selector. */ function getSelector(target) { // NOTE: I am breaking this apart because TypeScript was having trouble dealing // with type-guard. I believe this is part of this bug: // -- // https://github.com/Microsoft/TypeScript/issues/7271#issuecomment-360123191 if (target instanceof Window) { return WINDOW_SELECTOR; } else { // If the given element is not part of the active document, there's no way for us // to calculate a selector for it. if (!document.body.contains(target)) { return null; } return finder.finder(target); } } /** * I get the scrollable target for the given 'scroll' event. * ___ * NOTE: If you want to ignore (ie, not reinstate the scroll) of a particular type of DOM element, return NULL from this method. */ function getTargetFromScrollEvent(event) { var node = event.target; if (node instanceof HTMLDocument) { return window; } else if (node instanceof Element) { return node; } return null; } /** * I attempt to scroll the given target to the given scrollTop and return the resultant value presented by the target. * @param target * @param scrollTop * @returns resultant scroll top. */ function scrollTo(target, scrollTop) { if (target instanceof Window) { target.scrollTo(0, scrollTop); return target.scrollY; } else if (target instanceof Element) { target.scrollTop = scrollTop; return target.scrollTop; } return null; } /** * I return the target accessible at the given CSS selector. */ function select(selector) { if (selector === WINDOW_SELECTOR) { return window; } else { return document.querySelector(selector); } } /** * Source: * - https://www.bennadel.com/blog/3534-restoring-and-resetting-the-scroll-position-using-the-navigationstart-event-in-angular-7-0-4.htm * - http://bennadel.github.io/JavaScript-Demos/demos/router-retain-scroll-polyfill-angular7/ * - https://github.com/bennadel/JavaScript-Demos/tree/master/demos/router-retain-scroll-polyfill-angular7 */ var NGX_SCROLL_POSITION_RESTORATION_CONFIG_INJECTION_TOKEN = new core.InjectionToken('ngx_scroll_position_restoration_config_injection_token'); var NgxScrollPositionRestorationService = /** @class */ (function () { function NgxScrollPositionRestorationService(router, zone, platformId, config) { this.router = router; this.zone = zone; this.platformId = platformId; this.config = config; this.applyStateToDomTimer = 0; this.currentPageState = {}; this.lastNavigationStartAt = 0; this.navigationIDs = []; this.pageStates = {}; this.scrolledElements = new Set(); this.maximumNumberOfCachedPageStates = 20; this.serviceDestroyed$ = new rxjs.Subject(); } /** * Initialize NgxScrollPositionRestorationService. */ NgxScrollPositionRestorationService.prototype.initialize = function () { var _this = this; if (common.isPlatformServer(this.platformId)) { return; } this.setupScrollBinding(); // this.setupRouterBinding(); // I bind to the router events and perform to primary actions: // -- // NAVIGATION START: When the user is about to navigate away from the current view, // I inspect the current DOM state and commit any scrolled-element offsets to the // in-memory cache of the page state (scroll events were recorded during the lifetime // of the current router state). // -- // NAVIGATION END: When the user completes a navigation to a new view, I check to see // if the new view is really the restoration of a previously cached page state; and, // if so, I try to reinstate the old scrolled-element offsets in the rendered DOM. this.router.events.pipe(operators.takeUntil(this.serviceDestroyed$)).subscribe(function (event) { // Filter navigation event streams to the appropriate event handlers. if (event instanceof router.NavigationStart) { _this.handleNavigationStart(event); } else if (event instanceof router.NavigationEnd) { _this.handleNavigationEnd(); } }); // Since we're going to be implementing a custom scroll retention algorithm, // let's disable the one that is provided by the browser. This will keep our // polyfill the source of truth. this.disableBrowserDefaultScrollRestoration(); }; NgxScrollPositionRestorationService.prototype.ngOnDestroy = function () { this.serviceDestroyed$.next(); this.serviceDestroyed$.complete(); }; NgxScrollPositionRestorationService.prototype.clearSavedWindowScrollTopInLastNavigation = function () { var lastNavigationId = this.navigationIDs[this.navigationIDs.length - 1]; if (lastNavigationId) { if (this.config.debug && this.pageStates[lastNavigationId][WINDOW_SELECTOR]) { console.log('Navigation in a "secondary" router-outlet - Remove window scroll position from recorded scroll positions.'); } delete (this.pageStates[lastNavigationId][WINDOW_SELECTOR]); } }; /** * I attempt to apply the given page-state to the rendered DOM. I will continue to poll the document until all states have been reinstated; or, until the poll duration has been exceeded; or, until a subsequent navigation takes place. */ NgxScrollPositionRestorationService.prototype.applyPageStateToDom = function (pageState) { var _this = this; if (this.config.debug) { this.debugPageState(pageState, 'Attempting to reapply scroll positions after a popstate navigation (backward or forward).'); } if (this.objectIsEmpty(pageState)) { return; } // Let's create a copy of the page state so that we can safely delete keys from // it as we successfully apply them to the rendered DOM. var pendingPageState = Object.assign({}, pageState); // Setup the scroll retention timer outside of the Angular Zone so that it // doesn't trigger any additional change-detection digests. this.zone.runOutsideAngular(function () { var startedAt = Date.now(); _this.applyStateToDomTimer = setInterval(function () { for (var selector in pendingPageState) { var target = select(selector); // If the target element doesn't exist in the DOM yet, it // could be an indication of asynchronous loading and // rendering. Move onto the next selector while we still // have time. if (!target) { continue; } // If the element in question has been scrolled (by the user) // while we're attempting to reinstate the previous scroll // offsets, then ignore this state - the user's action should // take precedence. if (_this.scrolledElements.has(target)) { delete (pendingPageState[selector]); // Otherwise, let's try to restore the scroll for the target. } else { var scrollTop = pendingPageState[selector]; var resultantScrollTop = scrollTo(target, scrollTop); // If the attempt to restore the element to its previous // offset resulted in a match, then stop tracking this // element. Otherwise, we'll continue to try and scroll // it in the subsequent tick. // -- // NOTE: We continue to try and update it because the // target element may exist in the DOM but also be // loading asynchronous data that is required for the // previous scroll offset. if (resultantScrollTop === scrollTop) { delete (pendingPageState[selector]); } } } // If there are no more elements to scroll or, we've exceeded our // poll duration, then stop watching the DOM. if (_this.objectIsEmpty(pendingPageState) || ((Date.now() - startedAt) >= _this.config.pollDuration)) { clearTimeout(_this.applyStateToDomTimer); if (_this.config.debug) { if (_this.objectIsEmpty(pendingPageState)) { console.log('%c Successfully reapplied all recorded scroll positions to the DOM.', 'color: #2ecc71'); } else { console.warn("Could not reapply following recorded scroll positions to the DOM after a poll duration of: " + _this.config.pollDuration + " milliseconds:"); _this.debugPageState(pendingPageState); } } } }, _this.config.pollCadence); }); }; /** * I get the page state from the given set of nodes. This extracts the CSS selectors and offsets from the recorded elements. */ NgxScrollPositionRestorationService.prototype.getPageStateFromNodes = function (nodes) { var pageState = {}; nodes.forEach(function (target) { // Generate a CSS selector from the given target. // -- // TODO: Right now, this algorithm creates the selector by walking up the // DOM tree and using the simulated encapsulation attributes. But, it // would be cool to have a configuration option that tells this algorithm // to look for a specific id-prefix or attribute or something. This would // require the developer to provide those; but it would be optimal. var selector = getSelector(target); // If the given Target is no longer part of the active DOM, the selector // will be null. if (selector) { pageState[selector] = getScrollTop(target); } }); return pageState; }; /** * I determine if the given object is empty (ie, has no keys). */ NgxScrollPositionRestorationService.prototype.objectIsEmpty = function (object) { for (var key in object) { return false; } return true; }; // The goal of the NavigationStart event is to take changes that have been made // to the current DOM and store them in the render-state tree so they can be // reinstated at a future date. NgxScrollPositionRestorationService.prototype.handleNavigationStart = function (event) { this.lastNavigationStartAt = Date.now(); // Get the navigation ID and the restored navigation ID for use in the // NavigationEnd event handler. this.navigationID = event.id; /** * Maybe in future update @todo: use ngx-navigation-trigger here, like: * (event.restoredState && this.whenShouldScrollPositionBeRestored.has(this.navigationTrigger)) */ this.restoredNavigationID = event.restoredState ? event.restoredState.navigationId : null; // If the user is navigating away from the current view, kill any timers that // may be trying to reinstate a page-state. clearTimeout(this.applyStateToDomTimer); // Before we navigate away from the current page state, let's commit any // scroll-elements to the current page state. Object.assign(this.currentPageState, this.getPageStateFromNodes(this.scrolledElements)); this.scrolledElements.clear(); if (this.config.debug) { this.debugPageState(this.currentPageState, 'Recorded scroll positions.'); } }; ; // The primary goal of the NavigationEnd event is to reinstate a cached page // state in the event that the navigation is restoring a previously rendered page // as the result of a popstate event (ex, the user hit the Back or Forward // buttons). NgxScrollPositionRestorationService.prototype.handleNavigationEnd = function () { var previousPageState = this.currentPageState; // Now that we know the navigation was successful, let's start and store a // new page state to track future scrolling. this.currentPageState = this.pageStates[this.navigationID] = {}; // While we are going to track elements that will be scrolled during the // current page rendering, it is possible that there are elements that were // scrolled during a prior page rendering that still exist on the page, but // were not scrolled recently (such as a secondary router-outlet). As such, // let's look at the previous page state and 'pull forward' any state that // still pertains to the current page. if (!this.restoredNavigationID) { for (var selector in previousPageState) { var target = select(selector); // Only pull the selector forward if it corresponds to an element // that still exists in the rendered page. if (!target) { continue; } // Only pull the selector forward if the target is still at the same // offset after the navigation has taken place. In other words, if // the offset has somehow changed in between the NavigationStart and // NavigationEnd events, then ignore it. To be honest, this really // only applies to the WINDOW, which can change in offset due to the // change in what the Router is actively rendering in the DOM. if (getScrollTop(target) !== previousPageState[selector]) { continue; } this.currentPageState[selector] = previousPageState[selector]; if (this.config.debug) { console.group('Pulling scroll position from previous page state in current page state.'); console.log({ selector: selector, scrollPosition: this.currentPageState[selector] }); console.groupEnd(); } } // If we're restoring a previous page state AND we have that previous page // state cached in-memory, let's copy the previous state and then restore the // offsets in the DOM. } else if (this.restoredNavigationID && this.pageStates[this.restoredNavigationID]) { // NOTE: We're copying the offsets from the restored state into the // current state instead of just swapping the references because these // navigations are different in the Router history. Since each navigation // - imperative or popstate - gets a unique ID, we never truly 'go back' // in history; the Router only 'goes forward', with the notion that we're // recreating a previous state sometimes. this.applyPageStateToDom(Object.assign(this.currentPageState, this.pageStates[this.restoredNavigationID])); } // Keep track of the navigation event so we can limit the size of our // in-memory page state cache. this.navigationIDs.push(this.navigationID); // Trim the oldest page states as we go so that the in-memory cache doesn't // grow, unbounded. while (this.navigationIDs.length > this.maximumNumberOfCachedPageStates) { delete (this.pageStates[this.navigationIDs.shift()]); } }; ; /** * I bind to the scroll event and keep track of any elements that are scrolled in the rendered document. */ NgxScrollPositionRestorationService.prototype.setupScrollBinding = function () { var _this = this; /** * Maybe @todo: You should try to find a way to get scrollable (scrolled) elements only during NavigationStart. * Advantages: * - Better performance: no need to listen to the scroll event the whole time. * - Some elements might be added to the `scrolledElements` are not part of the DOM any more. * Disavantages: * - during NavigationStart scrollable elements that are maybe present after the intialization of page (before any user-interactions that can remove them) might be not part DOM any more. * */ // Add scroll-binding outside of the Angular Zone so it doesn't trigger any // additional change-detection digests. this.zone.runOutsideAngular(function () { // When navigating, the browser emits some scroll events as the DOM // (Document Object Model) changes shape in a way that forces the various // scroll offsets to change. Since these scroll events are not indicative // of a user's actual scrolling intent, we're going to ignore them. This // needs to be done on both sides of the navigation event (for reasons // that are not fully obvious or logical -- basically, the window's // scroll changes at a time that is not easy to tap into). Ignoring these // scroll events is important because the polyfilly stops trying to // reinstate a scroll-offset if it sees that the given element has // already been scrolled during the current rendering. var scrollBufferWindow = 100; var target; window.addEventListener('scroll', function (event) { // If the scroll event happens immediately following a // navigation event, then ignore it - it is likely a scroll that // was forced by the browser's native behavior. if ((Date.now() - _this.lastNavigationStartAt) < scrollBufferWindow) { return; } // The target will return NULL for elements that have irrelevant // scroll behaviors (like textarea inputs). As such, we have to // check to see if the domUtils returned anything. target = getTargetFromScrollEvent(event); if (target) { _this.scrolledElements.add(target); } }, // We have to use the CAPTURING phase. Scroll events DO NOT BUBBLE. // As such, if we want to listen for all scroll events in the // document, we have to use the capturing phase (as the event travels // down through the DOM tree). true); }); }; NgxScrollPositionRestorationService.prototype.debugPageState = function (pageState, message) { var e_1, _a; if (this.objectIsEmpty(pageState)) { return; } console.group(message || ''); try { for (var _b = __values(Object.entries(pageState)), _c = _b.next(); !_c.done; _c = _b.next()) { var _d = __read(_c.value, 2), selector = _d[0], scrollPosition = _d[1]; console.log({ selector: selector, scrollPosition: scrollPosition }); } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_1) throw e_1.error; } } console.groupEnd(); }; /** * Disable browser default scroll restoration. * * Documentation: * - https://developer.mozilla.org/en-US/docs/Web/API/History/scrollRestoration */ NgxScrollPositionRestorationService.prototype.disableBrowserDefaultScrollRestoration = function () { if ('scrollRestoration' in history) { history.scrollRestoration = 'manual'; } }; return NgxScrollPositionRestorationService; }()); NgxScrollPositionRestorationService.decorators = [ { type: core.Injectable } ]; NgxScrollPositionRestorationService.ctorParameters = function () { return [ { type: router.Router }, { type: core.NgZone }, { type: String, decorators: [{ type: core.Inject, args: [core.PLATFORM_ID,] }] }, { type: undefined, decorators: [{ type: core.Inject, args: [NGX_SCROLL_POSITION_RESTORATION_CONFIG_INJECTION_TOKEN,] }] } ]; }; /** * Source: * - https://www.bennadel.com/blog/3534-restoring-and-resetting-the-scroll-position-using-the-navigationstart-event-in-angular-7-0-4.htm * - http://bennadel.github.io/JavaScript-Demos/demos/router-retain-scroll-polyfill-angular7/ * - https://github.com/bennadel/JavaScript-Demos/tree/master/demos/router-retain-scroll-polyfill-angular7 */ var ANGULAR_DEFAULT_ROUTER_OUTLET_NAME = 'primary'; /** * I co-opt the <router-outlet> element selector so that I can tap into the life-cycle of the core RouterOutlet directive. * * REASON: When the user clicks on a link, it's quite hard to differentiate between a primary navigation, which should probably scroll the user back to the top of the viewport; and, something like a tabbed-navigation, which should probably keep the user's scroll around the offset associated with the tab. As such, we are going to rely on the inherent scroll-position of the view as the router-outlet target is pulled out of the DOM. * PS: Keep in mind in Angular per default scroll position is maintained on navigation. */ var CustomRouterOutletDirective = /** @class */ (function () { function CustomRouterOutletDirective(elementRef, router, routerOutlet, ngxScrollPositionRestorationService, platformId, config) { this.elementRef = elementRef; this.router = router; this.routerOutlet = routerOutlet; this.ngxScrollPositionRestorationService = ngxScrollPositionRestorationService; this.platformId = platformId; this.config = config; this.recordedScrollPositions = []; this.directiveDestroyed$ = new rxjs.Subject(); } CustomRouterOutletDirective.prototype.ngOnInit = function () { var _this = this; if (common.isPlatformServer(this.platformId)) { return; } this.routerOutlet.activateEvents.pipe(operators.takeUntil(this.directiveDestroyed$)).subscribe(function () { return _this.handleActivateEvent(); }); this.routerOutlet.deactivateEvents.pipe(operators.takeUntil(this.directiveDestroyed$)).subscribe(function () { return _this.handleDectivateEvent(); }); this.router.events.pipe(operators.takeUntil(this.directiveDestroyed$)).subscribe(function (event) { return _this.handleNavigationEvent(event); }); }; CustomRouterOutletDirective.prototype.ngOnDestroy = function () { this.directiveDestroyed$.next(); this.directiveDestroyed$.complete(); }; /** * Called when a router-outlet component has been rendered. */ CustomRouterOutletDirective.prototype.handleActivateEvent = function () { var e_1, _c; var _a; var currentRouterOutletName = this.routerOutlet.activatedRoute.outlet; // A Check because there is no `router.getCurrentNavigation` function in Angular 6. var currentNavigation = typeof this.router.getCurrentNavigation === 'function' ? this.router.getCurrentNavigation() : null; if (currentRouterOutletName !== ANGULAR_DEFAULT_ROUTER_OUTLET_NAME && !((_a = currentNavigation === null || currentNavigation === void 0 ? void 0 : currentNavigation.extras) === null || _a === void 0 ? void 0 : _a.skipLocationChange)) { this.ngxScrollPositionRestorationService.clearSavedWindowScrollTopInLastNavigation(); } var isRootRouterOutlet = this.isRootRouterOutlet(this.routerOutlet.activatedRoute); if (isRootRouterOutlet && this.navigationTrigger === 'imperative' && this.routerOutlet.activatedRoute.outlet === ANGULAR_DEFAULT_ROUTER_OUTLET_NAME) { scrollTo(window, 0); if (this.config.debug) { console.log('Imperative navigation: scrolled to the top (scrollTop = 0) of the window.'); } } else { // At this point, the View-in-question has been mounted in the DOM (Document // Object Model). We can now walk back up the DOM and make sure that the // previously-recorded offsets (in the last 'deactivate' event) are being applied // to the ancestral elements. This will prevent the browser's native desire to // auto-scroll-down a document once the view has been injected. Essentially, this // ensures that we scroll back to the 'expected top' as the user clicks through // the application. if (this.config.debug) { console.group("router-outlet (\"" + (this.elementRef.nativeElement.getAttribute('name') || ANGULAR_DEFAULT_ROUTER_OUTLET_NAME) + "\") - Reapply recorded scroll positions."); console.log(this.recordedScrollPositions.slice()); console.groupEnd(); } if (this.recordedScrollPositions.length === 0) { return; } try { for (var _d = __values(this.recordedScrollPositions), _e = _d.next(); !_e.done; _e = _d.next()) { var _f = _e.value, elementSelector = _f.elementSelector, scrollPosition = _f.scrollPosition; if (elementSelector) { var element = select(elementSelector); if (element) { scrollTo(element, scrollPosition); } } } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (_e && !_e.done && (_c = _d.return)) _c.call(_d); } finally { if (e_1) throw e_1.error; } } this.recordedScrollPositions = []; } }; /** * Called when a router-outlet component has been destroyed from the DOM. This means, at this point, the scroll position of the scrollable element containing the router-outlet component should be `0` (@todo: (BUG) but this seems not to work in Angular@13.1.1: component is not destroyed at this point). */ CustomRouterOutletDirective.prototype.handleDectivateEvent = function () { // At this point, the View-in-question has already been removed from the // document. Let's walk up the DOM (Document Object Model) and record the scroll // position of all scrollable elements. This will give us a sense of what the DOM // should look like after the next View is injected. var node = this.elementRef.nativeElement.parentNode; while (node && node.tagName !== 'BODY') { // If this is an "Element" node, capture its offset. if (node.nodeType === 1) { var scrollTop = getScrollTop(node); var elementSelector = getSelector(node); this.recordedScrollPositions.push({ elementSelector: elementSelector, target: node, scrollPosition: scrollTop }); } node = node.parentNode; } if (this.config.debug) { console.group("router-outlet (\"" + (this.elementRef.nativeElement.getAttribute('name') || ANGULAR_DEFAULT_ROUTER_OUTLET_NAME) + "\") - Recorded scroll positions."); console.log(this.recordedScrollPositions.slice()); console.groupEnd(); } }; /** * I get called whenever a router event is raised. */ CustomRouterOutletDirective.prototype.handleNavigationEvent = function (event) { if (event instanceof router.NavigationStart) { this.navigationTrigger = event.navigationTrigger; } // The 'offsets' are only meant to be used across a single navigation. As such, // let's clear out the offsets at the end of each navigation in order to ensure // that old offsets don't accidentally get applied to a future view mounted by // the current router-outlet. if (event instanceof router.NavigationEnd) { this.recordedScrollPositions = []; } }; /** * Is root "primary" (or any secondary) router-outet. */ CustomRouterOutletDirective.prototype.isRootRouterOutlet = function (actvitedRoute) { var e_2, _c; var _a, _b; var currentComponent = actvitedRoute.component; var parentChildren = (_b = (_a = actvitedRoute.parent) === null || _a === void 0 ? void 0 : _a.routeConfig) === null || _b === void 0 ? void 0 : _b.children; if (!Array.isArray(parentChildren)) { return true; } try { for (var parentChildren_1 = __values(parentChildren), parentChildren_1_1 = parentChildren_1.next(); !parentChildren_1_1.done; parentChildren_1_1 = parentChildren_1.next()) { var route = parentChildren_1_1.value; if (route.component === currentComponent) { return false; } } } catch (e_2_1) { e_2 = { error: e_2_1 }; } finally { try { if (parentChildren_1_1 && !parentChildren_1_1.done && (_c = parentChildren_1.return)) _c.call(parentChildren_1); } finally { if (e_2) throw e_2.error; } } return true; // Alternative: solution 02 (but not valid for secondary router-outlet) // if (actvitedRoute.parent?.component) { // return false; // } else { // return true; // } }; return CustomRouterOutletDire