UNPKG

@hickory/browser

Version:

Hickory's browser history

412 lines (400 loc) 15.9 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = global || self, factory(global.HickoryBrowser = {})); }(this, function (exports) { 'use strict'; function ensureBeginsWith(str, prefix) { if (!str) { return ""; } return str.indexOf(prefix) === 0 ? str : prefix + str; } function defaultParseQuery(query) { return query ? query : ""; } function defaultStringifyQuery(query) { return query ? query : ""; } function locationUtils(options) { if (options === void 0) { options = {}; } var _a = options.query, _b = _a === void 0 ? {} : _a, _c = _b.parse, parseQuery = _c === void 0 ? defaultParseQuery : _c, _d = _b.stringify, stringifyQuery = _d === void 0 ? defaultStringifyQuery : _d, base = options.base; return { location: function (value, current) { var url = value.url, state = value.state; // special cases for empty/hash URLs if (url === "" || url.charAt(0) === "#") { if (!current) { current = { pathname: "/", hash: "", query: parseQuery() }; } var details_1 = { pathname: current.pathname, hash: url.charAt(0) === "#" ? url.substring(1) : current.hash, query: current.query }; if (state) { details_1.state = state; } return details_1; } // hash is always after query, so split it off first var hashIndex = url.indexOf("#"); var hash; if (hashIndex !== -1) { hash = url.substring(hashIndex + 1); url = url.substring(0, hashIndex); } else { hash = ""; } var queryIndex = url.indexOf("?"); var rawQuery; if (queryIndex !== -1) { rawQuery = url.substring(queryIndex + 1); url = url.substring(0, queryIndex); } var query = parseQuery(rawQuery); var pathname = base ? base.remove(url) : url; if (pathname === "") { pathname = "/"; } var details = { hash: hash, query: query, pathname: pathname }; if (state) { details.state = state; } return details; }, keyed: function (location, key) { location.key = key; return location; }, stringify: function (location) { if (typeof location === "string") { var firstChar = location.charAt(0); if (firstChar === "#" || firstChar === "?") { return location; } return base ? base.add(location) : location; } var pathname = location.pathname !== undefined ? base ? base.add(location.pathname) : location.pathname : ""; return (pathname + ensureBeginsWith(stringifyQuery(location.query), "?") + ensureBeginsWith(location.hash, "#")); } }; } function createKeyGenerator() { var major = 0; return { major: function (previous) { if (previous) { major = previous[0] + 1; } return [major++, 0]; }, minor: function (current) { return [current[0], current[1] + 1]; } }; } function navigateWith(args) { var responseHandler = args.responseHandler, utils = args.utils, keygen = args.keygen, current = args.current, push = args.push, replace = args.replace; var pending; function createNavigation(location, action, finish, cancel) { var navigation = { location: location, action: action, finish: function () { if (pending !== navigation) { return; } finish(); pending = undefined; }, cancel: function (nextAction) { if (pending !== navigation) { return; } cancel(nextAction); navigation.cancelled = true; pending = undefined; }, cancelled: false }; return navigation; } function emitNavigation(nav) { pending = nav; responseHandler(nav); } function cancelPending(action) { if (pending) { pending.cancel(action); pending = undefined; } } function prepare(to, navType) { var currentLocation = current(); var location = utils.location(to, currentLocation); switch (navType) { case "anchor": return utils.stringify(location) === utils.stringify(currentLocation) ? replaceNav(location) : pushNav(location); case "push": return pushNav(location); case "replace": return replaceNav(location); default: throw new Error("Invalid navigation type: " + navType); } } function replaceNav(location) { var keyed = utils.keyed(location, keygen.minor(current().key)); return createNavigation(keyed, "replace", replace.finish(keyed), replace.cancel); } function pushNav(location) { var keyed = utils.keyed(location, keygen.major(current().key)); return createNavigation(keyed, "push", push.finish(keyed), push.cancel); } return { prepare: prepare, emitNavigation: emitNavigation, createNavigation: createNavigation, cancelPending: cancelPending }; } function noop() { } function confirmation() { var confirmFn; return { confirmNavigation: function (info, allow, prevent) { if (!confirmFn) { allow(); } else { confirmFn(info, allow, prevent || noop); } }, confirm: function (fn) { confirmFn = fn ? fn : null; } }; } function hasBase(path, prefix) { return new RegExp("^" + prefix + "(\\/|\\?|#|$)", "i").test(path); } function createBase(base, options) { if (typeof base !== "string" || base.charAt(0) !== "/" || base.charAt(base.length - 1) === "/") { throw new Error('The base segment "' + base + '" is not valid.' + ' The "base" option must begin with a forward slash and end with a' + " non-forward slash character."); } var _a = options || {}, _b = _a.emptyRoot, emptyRoot = _b === void 0 ? false : _b, _c = _a.strict, strict = _c === void 0 ? false : _c; return { add: function (path) { if (emptyRoot) { if (path === "/") { return base; } else if (path.startsWith("/?") || path.startsWith("/#")) { return "" + base + path.substr(1); } } else if (path.charAt(0) === "?" || path.charAt(0) === "#") { return path; } return "" + base + path; }, remove: function (pathname) { if (pathname === "") { return ""; } var exists = hasBase(pathname, base); if (!exists) { if (strict) { throw new Error("Expected a string that begins with \"" + base + "\", but received \"" + pathname + "\"."); } else { return pathname; } } if (pathname === base) { if (strict && !emptyRoot) { throw new Error("Received string \"" + base + "\", which is the same as the base, but \"emptyRoot\" is not true."); } return "/"; } return pathname.substr(base.length); } }; } function domExists() { return !!(window && window.location); } /* * Ignore popstate events that don't define event.state * unless they come from Chrome on iOS (because it emits * events where event.state is undefined when you click * the back button) */ function ignorablePopstateEvent(event) { return (event.state === undefined && navigator.userAgent.indexOf("CriOS") === -1); } /* * IE 11 might throw, so just catch and return empty object when that happens */ function getStateFromHistory() { try { return window.history.state || {}; } catch (e) { return {}; } } function noop$1() { } function browser(fn, options) { if (options === void 0) { options = {}; } if (!domExists()) { throw new Error("Cannot use @hickory/browser without a DOM"); } var utils = locationUtils(options); var keygen = createKeyGenerator(); var _a = confirmation(), confirm = _a.confirm, confirmNavigation = _a.confirmNavigation; function fromBrowser(providedState) { var _a = window.location, pathname = _a.pathname, search = _a.search, hash = _a.hash; var url = pathname + search + hash; var _b = providedState || getStateFromHistory(), key = _b.key, state = _b.state; if (!key) { key = keygen.major(); window.history.replaceState({ key: key, state: state }, "", url); } var location = utils.location({ url: url, state: state }); return utils.keyed(location, key); } function url(location) { return utils.stringify(location); } // set action before location because fromBrowser enforces // that the location has a key var lastAction = getStateFromHistory().key !== undefined ? "pop" : "push"; var _b = navigateWith({ responseHandler: fn, utils: utils, keygen: keygen, current: function () { return browserHistory.location; }, push: { finish: function (location) { return function () { var path = url(location); var key = location.key, state = location.state; try { window.history.pushState({ key: key, state: state }, "", path); } catch (e) { window.location.assign(path); } browserHistory.location = location; lastAction = "push"; }; }, cancel: noop$1 }, replace: { finish: function (location) { return function () { var path = url(location); var key = location.key, state = location.state; try { window.history.replaceState({ key: key, state: state }, "", path); } catch (e) { window.location.replace(path); } browserHistory.location = location; lastAction = "replace"; }; }, cancel: noop$1 } }), emitNavigation = _b.emitNavigation, cancelPending = _b.cancelPending, createNavigation = _b.createNavigation, prepare = _b.prepare; // when true, pop will ignore the navigation var reverting = false; function popstate(event) { if (reverting) { reverting = false; return; } if (ignorablePopstateEvent(event)) { return; } cancelPending("pop"); var location = fromBrowser(event.state); var diff = browserHistory.location.key[0] - location.key[0]; var revert = function () { reverting = true; window.history.go(diff); }; confirmNavigation({ to: location, from: browserHistory.location, action: "pop" }, function () { emitNavigation(createNavigation(location, "pop", function () { browserHistory.location = location; lastAction = "pop"; }, function (nextAction) { if (nextAction !== "pop") { revert(); } })); }, revert); } window.addEventListener("popstate", popstate, false); var browserHistory = { location: fromBrowser(), current: function () { emitNavigation(createNavigation(browserHistory.location, lastAction, noop$1, noop$1)); }, url: url, navigate: function (to, navType) { if (navType === void 0) { navType = "anchor"; } var navigation = prepare(to, navType); cancelPending(navigation.action); confirmNavigation({ to: navigation.location, from: browserHistory.location, action: navigation.action }, function () { emitNavigation(navigation); }); }, go: function (num) { window.history.go(num); }, confirm: confirm, cancel: function () { cancelPending(); }, destroy: function () { window.removeEventListener("popstate", popstate); emitNavigation = noop$1; } }; return browserHistory; } exports.createBase = createBase; exports.browser = browser; Object.defineProperty(exports, '__esModule', { value: true }); }));