UNPKG

@layr/browser-navigator

Version:

Provides a navigation system for a Layr app running in a browser

206 lines 8.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.BrowserNavigator = void 0; const tslib_1 = require("tslib"); const navigator_1 = require("@layr/navigator"); const utilities_1 = require("@layr/utilities"); const debounce_1 = tslib_1.__importDefault(require("lodash/debounce")); const possibly_async_1 = require("possibly-async"); /** * *Inherits from [`Navigator`](https://layrjs.com/docs/v2/reference/navigator).* * * A [`Navigator`](https://layrjs.com/docs/v2/reference/navigator) relying on the browser's [History API](https://developer.mozilla.org/en-US/docs/Web/API/History) to determine the current [route](https://layrjs.com/docs/v2/reference/route). * * #### Usage * * If you are using [React](https://reactjs.org/), the easiest way to set up a `BrowserNavigator` in your app is to use the [`BrowserNavigatorView`](https://layrjs.com/docs/v2/reference/react-integration#browser-navigator-view-react-component) React component that is provided by the `@layr/react-integration` package. * * See an example of use in the [`BrowserNavigatorView`](https://layrjs.com/docs/v2/reference/react-integration#browser-navigator-view-react-component) React component. */ class BrowserNavigator extends navigator_1.Navigator { /** * Creates a [`BrowserNavigator`](https://layrjs.com/docs/v2/reference/browser-navigator). * * @returns The [`BrowserNavigator`](https://layrjs.com/docs/v2/reference/browser-navigator) instance that was created. * * @category Creation */ constructor(options = {}) { super(options); this.afterConstruct(); } mount() { // --- 'popstate' event --- this._popStateHandler = () => { if (!this._ignorePopStateHandler) { return (0, possibly_async_1.possiblyAsync)(!this.getIsBlocked() || this.confirmNavigation(), (canNavigate) => { const cancelNavigation = (error) => { return (0, possibly_async_1.possiblyAsync)(this._go(this.getInternalHistoryIndex() - this.getHistoryIndex()), () => { this.setInternalHistoryIndex(this.getHistoryIndex()); if (error) { throw error; } }); }; if (!canNavigate) { return cancelNavigation(); } return (0, possibly_async_1.possiblyAsync)(this.callBeforeNavigate(), () => { this.setInternalHistoryIndex(this.getHistoryIndex()); this.callObservers(); }, (error) => { return cancelNavigation(error); }); }); } }; this._ignorePopStateHandler = false; window.addEventListener('popstate', this._popStateHandler); // --- 'beforeunload' event --- this._beforeUnloadHandler = (event) => { if (this.getIsBlocked() || this.getBeforeNavigate() !== undefined) { // Navigation is blocked or a `beforeNavigate` hook is set event.preventDefault(); // Not supported in all browsers // The following message will not be be displayed in modern browsers, // but it is required to set it so Chrome can prevent navigation event.returnValue = 'Are you sure you want to leave this page? Changes you made may not be saved.'; return event.returnValue; } }; window.addEventListener('beforeunload', this._beforeUnloadHandler, { capture: true }); // --- 'mouseup' event --- this._mouseUpHandler = (event) => { // Detect Ctrl+Click (Windows/Linux), Cmd+Click (Mac), Shift+Click (Popup) or Middle-Click if (event.ctrlKey || event.metaKey || event.shiftKey || event.button === 1) { this._openNewWindowUntil = Date.now() + 3 * utilities_1.SECOND; this._openNewWindowPopup = event.shiftKey; } else { this._openNewWindowUntil = undefined; this._openNewWindowPopup = undefined; } }; window.addEventListener('mouseup', this._mouseUpHandler, { capture: true }); // --- 'layrNavigatorNavigate' event --- this._navigateHandler = (event) => { this.navigate(event.detail.url); }; document.body.addEventListener('layrNavigatorNavigate', this._navigateHandler); // --- Hash navigation fix --- this._scrollToHash = (0, debounce_1.default)(() => { const element = document.getElementById(this._expectedHash); if (element !== null) { element.scrollIntoView({ block: 'start', behavior: 'smooth' }); this._expectedHash = undefined; } }, 50); this._mutationObserver = new MutationObserver(() => { if (this._expectedHash !== undefined) { this._scrollToHash(); } }); this._mutationObserver.observe(document.body, { attributes: true, characterData: true, childList: true, subtree: true }); this._fixScrollPosition(); } unmount() { window.removeEventListener('popstate', this._popStateHandler); window.removeEventListener('beforeunload', this._beforeUnloadHandler, { capture: true }); window.removeEventListener('mouseup', this._mouseUpHandler, { capture: true }); document.body.removeEventListener('layrNavigatorNavigate', this._navigateHandler); this._mutationObserver.disconnect(); } // === Component Registration === /** * See the methods that are inherited from the [`Navigator`](https://layrjs.com/docs/v2/reference/navigator#component-registration) class. * * @category Component Registration */ // === Routes === /** * See the methods that are inherited from the [`Navigator`](https://layrjs.com/docs/v2/reference/navigator#routes) class. * * @category Routes */ // === Current Location === /** * See the methods that are inherited from the [`Navigator`](https://layrjs.com/docs/v2/reference/navigator#current-location) class. * * @category Current Location */ _getCurrentURL() { return (0, navigator_1.normalizeURL)(window.location.href); } // === Navigation === /** * See the methods that are inherited from the [`Navigator`](https://layrjs.com/docs/v2/reference/navigator#navigation) class. * * @category Navigation */ _navigate(url) { if (!this._possiblyOpenNewWindow(url)) { window.history.pushState({ index: this._getHistoryIndex() + 1 }, '', (0, navigator_1.stringifyURL)(url)); this._fixScrollPosition(); } } _redirect(url) { if (!this._possiblyOpenNewWindow(url)) { window.history.replaceState({ index: this._getHistoryIndex() }, '', (0, navigator_1.stringifyURL)(url)); this._fixScrollPosition(); } } shouldOpenNewWindow() { return this._openNewWindowUntil !== undefined && Date.now() < this._openNewWindowUntil; } _possiblyOpenNewWindow(url) { if (this.shouldOpenNewWindow()) { const isPopup = this._openNewWindowPopup === true; this._openNewWindowUntil = undefined; this._openNewWindowPopup = undefined; // Try to open the URL in a new window if (window.open((0, navigator_1.stringifyURL)(url), '_blank', isPopup ? 'popup' : undefined)) { return true; } } return false; } _fixScrollPosition() { window.scrollTo(0, 0); const hash = this.getCurrentHash(); if (hash !== undefined) { this._expectedHash = hash; this._scrollToHash(); } } _reload(url) { if (url !== undefined) { window.location.replace((0, navigator_1.stringifyURL)(url)); } else { window.location.reload(); } } _go(delta) { return new Promise((resolve) => { this._ignorePopStateHandler = true; window.addEventListener('popstate', () => { this._ignorePopStateHandler = false; resolve(); }, { once: true }); window.history.go(delta); }); } _getHistoryLength() { return window.history.length; } _getHistoryIndex() { return (window.history.state?.index ?? 0); } } exports.BrowserNavigator = BrowserNavigator; //# sourceMappingURL=browser-navigator.js.map