@hickory/browser
Version:
Hickory's browser history
412 lines (400 loc) • 15.9 kB
JavaScript
(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 });
}));