UNPKG

@stencil/router

Version:
646 lines (636 loc) 25.4 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); const __chunk_1 = require('./stencilrouter-a3d77a87.js'); const __chunk_2 = require('./chunk-94c92d88.js'); const __chunk_4 = require('./chunk-e6311a56.js'); const __chunk_5 = require('./chunk-b9bd6b52.js'); const warning = (value, ...args) => { if (!value) { console.warn(...args); } }; // Adapted from the https://github.com/ReactTraining/history and converted to TypeScript const createTransitionManager = () => { let prompt; let listeners = []; const setPrompt = (nextPrompt) => { warning(prompt == null, 'A history supports only one prompt at a time'); prompt = nextPrompt; return () => { if (prompt === nextPrompt) { prompt = null; } }; }; const confirmTransitionTo = (location, action, getUserConfirmation, callback) => { // TODO: If another transition starts while we're still confirming // the previous one, we may end up in a weird state. Figure out the // best way to handle this. if (prompt != null) { const result = typeof prompt === 'function' ? prompt(location, action) : prompt; if (typeof result === 'string') { if (typeof getUserConfirmation === 'function') { getUserConfirmation(result, callback); } else { warning(false, 'A history needs a getUserConfirmation function in order to use a prompt message'); callback(true); } } else { // Return false from a transition hook to cancel the transition. callback(result !== false); } } else { callback(true); } }; const appendListener = (fn) => { let isActive = true; const listener = (...args) => { if (isActive) { fn(...args); } }; listeners.push(listener); return () => { isActive = false; listeners = listeners.filter(item => item !== listener); }; }; const notifyListeners = (...args) => { listeners.forEach(listener => listener(...args)); }; return { setPrompt, confirmTransitionTo, appendListener, notifyListeners }; }; const createScrollHistory = (win, applicationScrollKey = 'scrollPositions') => { let scrollPositions = new Map(); const set = (key, value) => { scrollPositions.set(key, value); if (__chunk_5.storageAvailable(win, 'sessionStorage')) { const arrayData = []; scrollPositions.forEach((value, key) => { arrayData.push([key, value]); }); win.sessionStorage.setItem('scrollPositions', JSON.stringify(arrayData)); } }; const get = (key) => { return scrollPositions.get(key); }; const has = (key) => { return scrollPositions.has(key); }; const capture = (key) => { set(key, [win.scrollX, win.scrollY]); }; if (__chunk_5.storageAvailable(win, 'sessionStorage')) { const scrollData = win.sessionStorage.getItem(applicationScrollKey); scrollPositions = scrollData ? new Map(JSON.parse(scrollData)) : scrollPositions; } if ('scrollRestoration' in win.history) { history.scrollRestoration = 'manual'; } return { set, get, has, capture }; }; // Adapted from the https://github.com/ReactTraining/history and converted to TypeScript const PopStateEvent = 'popstate'; const HashChangeEvent = 'hashchange'; /** * Creates a history object that uses the HTML5 history API including * pushState, replaceState, and the popstate event. */ const createBrowserHistory = (win, props = {}) => { let forceNextPop = false; const globalHistory = win.history; const globalLocation = win.location; const globalNavigator = win.navigator; const canUseHistory = __chunk_5.supportsHistory(win); const needsHashChangeListener = !__chunk_5.supportsPopStateOnHashChange(globalNavigator); const scrollHistory = createScrollHistory(win); const forceRefresh = (props.forceRefresh != null) ? props.forceRefresh : false; const getUserConfirmation = (props.getUserConfirmation != null) ? props.getUserConfirmation : __chunk_5.getConfirmation; const keyLength = (props.keyLength != null) ? props.keyLength : 6; const basename = props.basename ? __chunk_4.stripTrailingSlash(__chunk_4.addLeadingSlash(props.basename)) : ''; const getHistoryState = () => { try { return win.history.state || {}; } catch (e) { // IE 11 sometimes throws when accessing window.history.state // See https://github.com/ReactTraining/history/pull/289 return {}; } }; const getDOMLocation = (historyState) => { historyState = historyState || {}; const { key, state } = historyState; const { pathname, search, hash } = globalLocation; let path = pathname + search + hash; warning((!basename || __chunk_4.hasBasename(path, basename)), 'You are attempting to use a basename on a page whose URL path does not begin ' + 'with the basename. Expected path "' + path + '" to begin with "' + basename + '".'); if (basename) { path = __chunk_4.stripBasename(path, basename); } return __chunk_4.createLocation(path, state, key || __chunk_4.createKey(keyLength)); }; const transitionManager = createTransitionManager(); const setState = (nextState) => { // Capture location for the view before changing history. scrollHistory.capture(history.location.key); Object.assign(history, nextState); // Set scroll position based on its previous storage value history.location.scrollPosition = scrollHistory.get(history.location.key); history.length = globalHistory.length; transitionManager.notifyListeners(history.location, history.action); }; const handlePopState = (event) => { // Ignore extraneous popstate events in WebKit. if (!__chunk_5.isExtraneousPopstateEvent(globalNavigator, event)) { handlePop(getDOMLocation(event.state)); } }; const handleHashChange = () => { handlePop(getDOMLocation(getHistoryState())); }; const handlePop = (location) => { if (forceNextPop) { forceNextPop = false; setState(); } else { const action = 'POP'; transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => { if (ok) { setState({ action, location }); } else { revertPop(location); } }); } }; const revertPop = (fromLocation) => { const toLocation = history.location; // TODO: We could probably make this more reliable by // keeping a list of keys we've seen in sessionStorage. // Instead, we just default to 0 for keys we don't know. let toIndex = allKeys.indexOf(toLocation.key); let fromIndex = allKeys.indexOf(fromLocation.key); if (toIndex === -1) { toIndex = 0; } if (fromIndex === -1) { fromIndex = 0; } const delta = toIndex - fromIndex; if (delta) { forceNextPop = true; go(delta); } }; const initialLocation = getDOMLocation(getHistoryState()); let allKeys = [initialLocation.key]; let listenerCount = 0; let isBlocked = false; // Public interface const createHref = (location) => { return basename + __chunk_4.createPath(location); }; const push = (path, state) => { warning(!(typeof path === 'object' && path.state !== undefined && state !== undefined), 'You should avoid providing a 2nd state argument to push when the 1st ' + 'argument is a location-like object that already has state; it is ignored'); const action = 'PUSH'; const location = __chunk_4.createLocation(path, state, __chunk_4.createKey(keyLength), history.location); transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => { if (!ok) { return; } const href = createHref(location); const { key, state } = location; if (canUseHistory) { globalHistory.pushState({ key, state }, '', href); if (forceRefresh) { globalLocation.href = href; } else { const prevIndex = allKeys.indexOf(history.location.key); const nextKeys = allKeys.slice(0, prevIndex === -1 ? 0 : prevIndex + 1); nextKeys.push(location.key); allKeys = nextKeys; setState({ action, location }); } } else { warning(state === undefined, 'Browser history cannot push state in browsers that do not support HTML5 history'); globalLocation.href = href; } }); }; const replace = (path, state) => { warning(!(typeof path === 'object' && path.state !== undefined && state !== undefined), 'You should avoid providing a 2nd state argument to replace when the 1st ' + 'argument is a location-like object that already has state; it is ignored'); const action = 'REPLACE'; const location = __chunk_4.createLocation(path, state, __chunk_4.createKey(keyLength), history.location); transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => { if (!ok) { return; } const href = createHref(location); const { key, state } = location; if (canUseHistory) { globalHistory.replaceState({ key, state }, '', href); if (forceRefresh) { globalLocation.replace(href); } else { const prevIndex = allKeys.indexOf(history.location.key); if (prevIndex !== -1) { allKeys[prevIndex] = location.key; } setState({ action, location }); } } else { warning(state === undefined, 'Browser history cannot replace state in browsers that do not support HTML5 history'); globalLocation.replace(href); } }); }; const go = (n) => { globalHistory.go(n); }; const goBack = () => go(-1); const goForward = () => go(1); const checkDOMListeners = (delta) => { listenerCount += delta; if (listenerCount === 1) { win.addEventListener(PopStateEvent, handlePopState); if (needsHashChangeListener) { win.addEventListener(HashChangeEvent, handleHashChange); } } else if (listenerCount === 0) { win.removeEventListener(PopStateEvent, handlePopState); if (needsHashChangeListener) { win.removeEventListener(HashChangeEvent, handleHashChange); } } }; const block = (prompt = '') => { const unblock = transitionManager.setPrompt(prompt); if (!isBlocked) { checkDOMListeners(1); isBlocked = true; } return () => { if (isBlocked) { isBlocked = false; checkDOMListeners(-1); } return unblock(); }; }; const listen = (listener) => { const unlisten = transitionManager.appendListener(listener); checkDOMListeners(1); return () => { checkDOMListeners(-1); unlisten(); }; }; const history = { length: globalHistory.length, action: 'POP', location: initialLocation, createHref, push, replace, go, goBack, goForward, block, listen, win: win }; return history; }; // Adapted from the https://github.com/ReactTraining/history and converted to TypeScript const HashChangeEvent$1 = 'hashchange'; const HashPathCoders = { hashbang: { encodePath: (path) => path.charAt(0) === '!' ? path : '!/' + __chunk_4.stripLeadingSlash(path), decodePath: (path) => path.charAt(0) === '!' ? path.substr(1) : path }, noslash: { encodePath: __chunk_4.stripLeadingSlash, decodePath: __chunk_4.addLeadingSlash }, slash: { encodePath: __chunk_4.addLeadingSlash, decodePath: __chunk_4.addLeadingSlash } }; const createHashHistory = (win, props = {}) => { let forceNextPop = false; let ignorePath = null; let listenerCount = 0; let isBlocked = false; const globalLocation = win.location; const globalHistory = win.history; const canGoWithoutReload = __chunk_5.supportsGoWithoutReloadUsingHash(win.navigator); const keyLength = (props.keyLength != null) ? props.keyLength : 6; const { getUserConfirmation = __chunk_5.getConfirmation, hashType = 'slash' } = props; const basename = props.basename ? __chunk_4.stripTrailingSlash(__chunk_4.addLeadingSlash(props.basename)) : ''; const { encodePath, decodePath } = HashPathCoders[hashType]; const getHashPath = () => { // We can't use window.location.hash here because it's not // consistent across browsers - Firefox will pre-decode it! const href = globalLocation.href; const hashIndex = href.indexOf('#'); return hashIndex === -1 ? '' : href.substring(hashIndex + 1); }; const pushHashPath = (path) => (globalLocation.hash = path); const replaceHashPath = (path) => { const hashIndex = globalLocation.href.indexOf('#'); globalLocation.replace(globalLocation.href.slice(0, hashIndex >= 0 ? hashIndex : 0) + '#' + path); }; const getDOMLocation = () => { let path = decodePath(getHashPath()); warning((!basename || __chunk_4.hasBasename(path, basename)), 'You are attempting to use a basename on a page whose URL path does not begin ' + 'with the basename. Expected path "' + path + '" to begin with "' + basename + '".'); if (basename) { path = __chunk_4.stripBasename(path, basename); } return __chunk_4.createLocation(path, undefined, __chunk_4.createKey(keyLength)); }; const transitionManager = createTransitionManager(); const setState = (nextState) => { Object.assign(history, nextState); history.length = globalHistory.length; transitionManager.notifyListeners(history.location, history.action); }; const handleHashChange = () => { const path = getHashPath(); const encodedPath = encodePath(path); if (path !== encodedPath) { // Ensure we always have a properly-encoded hash. replaceHashPath(encodedPath); } else { const location = getDOMLocation(); const prevLocation = history.location; if (!forceNextPop && __chunk_4.locationsAreEqual(prevLocation, location)) { return; // A hashchange doesn't always == location change. } if (ignorePath === __chunk_4.createPath(location)) { return; // Ignore this change; we already setState in push/replace. } ignorePath = null; handlePop(location); } }; const handlePop = (location) => { if (forceNextPop) { forceNextPop = false; setState(); } else { const action = 'POP'; transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => { if (ok) { setState({ action, location }); } else { revertPop(location); } }); } }; const revertPop = (fromLocation) => { const toLocation = history.location; // TODO: We could probably make this more reliable by // keeping a list of paths we've seen in sessionStorage. // Instead, we just default to 0 for paths we don't know. let toIndex = allPaths.lastIndexOf(__chunk_4.createPath(toLocation)); let fromIndex = allPaths.lastIndexOf(__chunk_4.createPath(fromLocation)); if (toIndex === -1) { toIndex = 0; } if (fromIndex === -1) { fromIndex = 0; } const delta = toIndex - fromIndex; if (delta) { forceNextPop = true; go(delta); } }; // Ensure the hash is encoded properly before doing anything else. const path = getHashPath(); const encodedPath = encodePath(path); if (path !== encodedPath) { replaceHashPath(encodedPath); } const initialLocation = getDOMLocation(); let allPaths = [__chunk_4.createPath(initialLocation)]; // Public interface const createHref = (location) => ('#' + encodePath(basename + __chunk_4.createPath(location))); const push = (path, state) => { warning(state === undefined, 'Hash history cannot push state; it is ignored'); const action = 'PUSH'; const location = __chunk_4.createLocation(path, undefined, __chunk_4.createKey(keyLength), history.location); transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => { if (!ok) { return; } const path = __chunk_4.createPath(location); const encodedPath = encodePath(basename + path); const hashChanged = getHashPath() !== encodedPath; if (hashChanged) { // We cannot tell if a hashchange was caused by a PUSH, so we'd // rather setState here and ignore the hashchange. The caveat here // is that other hash histories in the page will consider it a POP. ignorePath = path; pushHashPath(encodedPath); const prevIndex = allPaths.lastIndexOf(__chunk_4.createPath(history.location)); const nextPaths = allPaths.slice(0, prevIndex === -1 ? 0 : prevIndex + 1); nextPaths.push(path); allPaths = nextPaths; setState({ action, location }); } else { warning(false, 'Hash history cannot PUSH the same path; a new entry will not be added to the history stack'); setState(); } }); }; const replace = (path, state) => { warning(state === undefined, 'Hash history cannot replace state; it is ignored'); const action = 'REPLACE'; const location = __chunk_4.createLocation(path, undefined, __chunk_4.createKey(keyLength), history.location); transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => { if (!ok) { return; } const path = __chunk_4.createPath(location); const encodedPath = encodePath(basename + path); const hashChanged = getHashPath() !== encodedPath; if (hashChanged) { // We cannot tell if a hashchange was caused by a REPLACE, so we'd // rather setState here and ignore the hashchange. The caveat here // is that other hash histories in the page will consider it a POP. ignorePath = path; replaceHashPath(encodedPath); } const prevIndex = allPaths.indexOf(__chunk_4.createPath(history.location)); if (prevIndex !== -1) { allPaths[prevIndex] = path; } setState({ action, location }); }); }; const go = (n) => { warning(canGoWithoutReload, 'Hash history go(n) causes a full page reload in this browser'); globalHistory.go(n); }; const goBack = () => go(-1); const goForward = () => go(1); const checkDOMListeners = (win, delta) => { listenerCount += delta; if (listenerCount === 1) { win.addEventListener(HashChangeEvent$1, handleHashChange); } else if (listenerCount === 0) { win.removeEventListener(HashChangeEvent$1, handleHashChange); } }; const block = (prompt = '') => { const unblock = transitionManager.setPrompt(prompt); if (!isBlocked) { checkDOMListeners(win, 1); isBlocked = true; } return () => { if (isBlocked) { isBlocked = false; checkDOMListeners(win, -1); } return unblock(); }; }; const listen = (listener) => { const unlisten = transitionManager.appendListener(listener); checkDOMListeners(win, 1); return () => { checkDOMListeners(win, -1); unlisten(); }; }; const history = { length: globalHistory.length, action: 'POP', location: initialLocation, createHref, push, replace, go, goBack, goForward, block, listen, win: win }; return history; }; const getLocation = (location, root) => { // Remove the root URL if found at beginning of string const pathname = location.pathname.indexOf(root) == 0 ? '/' + location.pathname.slice(root.length) : location.pathname; return Object.assign({}, location, { pathname }); }; const HISTORIES = { 'browser': createBrowserHistory, 'hash': createHashHistory }; /** * @name Router * @module ionic * @description */ class Router { constructor(hostRef) { __chunk_1.registerInstance(this, hostRef); this.root = '/'; this.historyType = 'browser'; // A suffix to append to the page title whenever // it's updated through RouteTitle this.titleSuffix = ''; this.routeViewsUpdated = (options = {}) => { if (this.history && options.scrollToId && this.historyType === 'browser') { const elm = this.history.win.document.getElementById(options.scrollToId); if (elm) { return elm.scrollIntoView(); } } this.scrollTo(options.scrollTopOffset || this.scrollTopOffset); }; this.isServer = __chunk_1.getContext(this, "isServer"); this.queue = __chunk_1.getContext(this, "queue"); } componentWillLoad() { this.history = HISTORIES[this.historyType](this.el.ownerDocument.defaultView); this.history.listen((location) => { location = getLocation(location, this.root); this.location = location; }); this.location = getLocation(this.history.location, this.root); } scrollTo(scrollToLocation) { const history = this.history; if (scrollToLocation == null || this.isServer || !history) { return; } if (history.action === 'POP' && Array.isArray(history.location.scrollPosition)) { return this.queue.write(() => { if (history && history.location && Array.isArray(history.location.scrollPosition)) { history.win.scrollTo(history.location.scrollPosition[0], history.location.scrollPosition[1]); } }); } // okay, the frame has passed. Go ahead and render now return this.queue.write(() => { history.win.scrollTo(0, scrollToLocation); }); } render() { if (!this.location || !this.history) { return; } const state = { historyType: this.historyType, location: this.location, titleSuffix: this.titleSuffix, root: this.root, history: this.history, routeViewsUpdated: this.routeViewsUpdated }; return (__chunk_1.h(__chunk_2.ActiveRouter.Provider, { state: state }, __chunk_1.h("slot", null))); } get el() { return __chunk_1.getElement(this); } } exports.stencil_router = Router;