UNPKG

@stencil/router

Version:
235 lines (234 loc) 9.73 kB
// Adapted from the https://github.com/ReactTraining/history and converted to TypeScript import { createLocation, locationsAreEqual, createKey } from './location-utils'; import { warning } from './log'; import { addLeadingSlash, stripLeadingSlash, stripTrailingSlash, hasBasename, stripBasename, createPath } from './path-utils'; import createTransitionManager from './createTransitionManager'; import { getConfirmation, supportsGoWithoutReloadUsingHash } from './dom-utils'; const HashChangeEvent = 'hashchange'; const HashPathCoders = { hashbang: { encodePath: (path) => path.charAt(0) === '!' ? path : '!/' + stripLeadingSlash(path), decodePath: (path) => path.charAt(0) === '!' ? path.substr(1) : path }, noslash: { encodePath: stripLeadingSlash, decodePath: addLeadingSlash }, slash: { encodePath: addLeadingSlash, decodePath: 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 = supportsGoWithoutReloadUsingHash(win.navigator); const keyLength = (props.keyLength != null) ? props.keyLength : 6; const { getUserConfirmation = getConfirmation, hashType = 'slash' } = props; const basename = props.basename ? stripTrailingSlash(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 || 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 = stripBasename(path, basename); } return createLocation(path, undefined, 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 && locationsAreEqual(prevLocation, location)) { return; // A hashchange doesn't always == location change. } if (ignorePath === 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(createPath(toLocation)); let fromIndex = allPaths.lastIndexOf(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 = [createPath(initialLocation)]; // Public interface const createHref = (location) => ('#' + encodePath(basename + createPath(location))); const push = (path, state) => { warning(state === undefined, 'Hash history cannot push state; it is ignored'); const action = 'PUSH'; const location = createLocation(path, undefined, createKey(keyLength), history.location); transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => { if (!ok) { return; } const path = 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(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 = createLocation(path, undefined, createKey(keyLength), history.location); transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => { if (!ok) { return; } const path = 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(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, handleHashChange); } else if (listenerCount === 0) { win.removeEventListener(HashChangeEvent, 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; }; export default createHashHistory;