react-junctions
Version:
Junction-based routing for React.
1,381 lines (1,211 loc) • 124 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('react'), require('react-dom')) :
typeof define === 'function' && define.amd ? define(['exports', 'react', 'react-dom'], factory) :
(factory((global.ReactJunctions = {}),global.React,global.ReactDOM));
}(this, (function (exports,React,ReactDOM) { 'use strict';
/**
* Copyright 2014-2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
/**
* Similar to invariant but only logs a warning if the condition is not met.
* This can be used to log issues in development environments in critical
* paths. Removing the logging code for production environments will keep the
* same logic and follow the same code paths.
*/
var __DEV__ = "development" !== 'production';
var warning = function() {};
if (__DEV__) {
warning = function(condition, format, args) {
var len = arguments.length;
args = new Array(len > 2 ? len - 2 : 0);
for (var key = 2; key < len; key++) {
args[key - 2] = arguments[key];
}
if (format === undefined) {
throw new Error(
'`warning(condition, format, ...args)` requires a warning ' +
'message argument'
);
}
if (format.length < 10 || (/^[s\W]*$/).test(format)) {
throw new Error(
'The warning format should be able to uniquely identify this ' +
'warning. Please, use a more descriptive format than: ' + format
);
}
if (!condition) {
var argIndex = 0;
var message = 'Warning: ' +
format.replace(/%s/g, function() {
return args[argIndex++];
});
if (typeof console !== 'undefined') {
console.error(message);
}
try {
// This error was thrown as a convenience so that you can use this stack
// to find the callsite that caused this warning to fire.
throw new Error(message);
} catch(x) {}
}
};
}
var warning_1 = warning;
/**
* Copyright 2013-2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
/**
* Use invariant() to assert state which your program assumes to be true.
*
* Provide sprintf-style format (only %s is supported) and arguments
* to provide information about what broke and what you were
* expecting.
*
* The invariant message will be stripped in production, but the invariant
* will remain to ensure logic does not differ in production.
*/
var NODE_ENV = "development";
var invariant = function(condition, format, a, b, c, d, e, f) {
if (NODE_ENV !== 'production') {
if (format === undefined) {
throw new Error('invariant requires an error message argument');
}
}
if (!condition) {
var error;
if (format === undefined) {
error = new Error(
'Minified exception occurred; use the non-minified dev environment ' +
'for the full error message and additional helpful warnings.'
);
} else {
var args = [a, b, c, d, e, f];
var argIndex = 0;
error = new Error(
format.replace(/%s/g, function() { return args[argIndex++]; })
);
error.name = 'Invariant Violation';
}
error.framesToPop = 1; // we don't care about invariant's own frame
throw error;
}
};
var invariant_1 = invariant;
function isAbsolute(pathname) {
return pathname.charAt(0) === '/';
}
// About 1.5x faster than the two-arg version of Array#splice()
function spliceOne(list, index) {
for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1) {
list[i] = list[k];
}
list.pop();
}
// This implementation is based heavily on node's url.parse
function resolvePathname(to) {
var from = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
var toParts = to && to.split('/') || [];
var fromParts = from && from.split('/') || [];
var isToAbs = to && isAbsolute(to);
var isFromAbs = from && isAbsolute(from);
var mustEndAbs = isToAbs || isFromAbs;
if (to && isAbsolute(to)) {
// to is absolute
fromParts = toParts;
} else if (toParts.length) {
// to is relative, drop the filename
fromParts.pop();
fromParts = fromParts.concat(toParts);
}
if (!fromParts.length) return '/';
var hasTrailingSlash = void 0;
if (fromParts.length) {
var last = fromParts[fromParts.length - 1];
hasTrailingSlash = last === '.' || last === '..' || last === '';
} else {
hasTrailingSlash = false;
}
var up = 0;
for (var i = fromParts.length; i >= 0; i--) {
var part = fromParts[i];
if (part === '.') {
spliceOne(fromParts, i);
} else if (part === '..') {
spliceOne(fromParts, i);
up++;
} else if (up) {
spliceOne(fromParts, i);
up--;
}
}
if (!mustEndAbs) for (; up--; up) {
fromParts.unshift('..');
}if (mustEndAbs && fromParts[0] !== '' && (!fromParts[0] || !isAbsolute(fromParts[0]))) fromParts.unshift('');
var result = fromParts.join('/');
if (hasTrailingSlash && result.substr(-1) !== '/') result += '/';
return result;
}
var addLeadingSlash = function addLeadingSlash(path) {
return path.charAt(0) === '/' ? path : '/' + path;
};
var hasBasename = function hasBasename(path, prefix) {
return new RegExp('^' + prefix + '(\\/|\\?|#|$)', 'i').test(path);
};
var stripBasename = function stripBasename(path, prefix) {
return hasBasename(path, prefix) ? path.substr(prefix.length) : path;
};
var stripTrailingSlash = function stripTrailingSlash(path) {
return path.charAt(path.length - 1) === '/' ? path.slice(0, -1) : path;
};
var parsePath = function parsePath(path) {
var pathname = path || '/';
var search = '';
var hash = '';
var hashIndex = pathname.indexOf('#');
if (hashIndex !== -1) {
hash = pathname.substr(hashIndex);
pathname = pathname.substr(0, hashIndex);
}
var searchIndex = pathname.indexOf('?');
if (searchIndex !== -1) {
search = pathname.substr(searchIndex);
pathname = pathname.substr(0, searchIndex);
}
return {
pathname: pathname,
search: search === '?' ? '' : search,
hash: hash === '#' ? '' : hash
};
};
var createPath = function createPath(location) {
var pathname = location.pathname,
search = location.search,
hash = location.hash;
var path = pathname || '/';
if (search && search !== '?') path += search.charAt(0) === '?' ? search : '?' + search;
if (hash && hash !== '#') path += hash.charAt(0) === '#' ? hash : '#' + hash;
return path;
};
var _extends$1 = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var createLocation = function createLocation(path, state, key, currentLocation) {
var location = void 0;
if (typeof path === 'string') {
// Two-arg form: push(path, state)
location = parsePath(path);
location.state = state;
} else {
// One-arg form: push(location)
location = _extends$1({}, path);
if (location.pathname === undefined) location.pathname = '';
if (location.search) {
if (location.search.charAt(0) !== '?') location.search = '?' + location.search;
} else {
location.search = '';
}
if (location.hash) {
if (location.hash.charAt(0) !== '#') location.hash = '#' + location.hash;
} else {
location.hash = '';
}
if (state !== undefined && location.state === undefined) location.state = state;
}
try {
location.pathname = decodeURI(location.pathname);
} catch (e) {
if (e instanceof URIError) {
throw new URIError('Pathname "' + location.pathname + '" could not be decoded. ' + 'This is likely caused by an invalid percent-encoding.');
} else {
throw e;
}
}
if (key) location.key = key;
if (currentLocation) {
// Resolve incomplete/relative pathname relative to current location.
if (!location.pathname) {
location.pathname = currentLocation.pathname;
} else if (location.pathname.charAt(0) !== '/') {
location.pathname = resolvePathname(location.pathname, currentLocation.pathname);
}
} else {
// When there is no prior location and pathname is empty, set it to /
if (!location.pathname) {
location.pathname = '/';
}
}
return location;
};
var createTransitionManager = function createTransitionManager() {
var prompt = null;
var setPrompt = function setPrompt(nextPrompt) {
warning_1(prompt == null, 'A history supports only one prompt at a time');
prompt = nextPrompt;
return function () {
if (prompt === nextPrompt) prompt = null;
};
};
var confirmTransitionTo = function 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) {
var result = typeof prompt === 'function' ? prompt(location, action) : prompt;
if (typeof result === 'string') {
if (typeof getUserConfirmation === 'function') {
getUserConfirmation(result, callback);
} else {
warning_1(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);
}
};
var listeners = [];
var appendListener = function appendListener(fn) {
var isActive = true;
var listener = function listener() {
if (isActive) fn.apply(undefined, arguments);
};
listeners.push(listener);
return function () {
isActive = false;
listeners = listeners.filter(function (item) {
return item !== listener;
});
};
};
var notifyListeners = function notifyListeners() {
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
listeners.forEach(function (listener) {
return listener.apply(undefined, args);
});
};
return {
setPrompt: setPrompt,
confirmTransitionTo: confirmTransitionTo,
appendListener: appendListener,
notifyListeners: notifyListeners
};
};
var canUseDOM = !!(typeof window !== 'undefined' && window.document && window.document.createElement);
var addEventListener = function addEventListener(node, event, listener) {
return node.addEventListener ? node.addEventListener(event, listener, false) : node.attachEvent('on' + event, listener);
};
var removeEventListener = function removeEventListener(node, event, listener) {
return node.removeEventListener ? node.removeEventListener(event, listener, false) : node.detachEvent('on' + event, listener);
};
var getConfirmation = function getConfirmation(message, callback) {
return callback(window.confirm(message));
}; // eslint-disable-line no-alert
/**
* Returns true if the HTML5 history API is supported. Taken from Modernizr.
*
* https://github.com/Modernizr/Modernizr/blob/master/LICENSE
* https://github.com/Modernizr/Modernizr/blob/master/feature-detects/history.js
* changed to avoid false negatives for Windows Phones: https://github.com/reactjs/react-router/issues/586
*/
var supportsHistory = function supportsHistory() {
var ua = window.navigator.userAgent;
if ((ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) && ua.indexOf('Mobile Safari') !== -1 && ua.indexOf('Chrome') === -1 && ua.indexOf('Windows Phone') === -1) return false;
return window.history && 'pushState' in window.history;
};
/**
* Returns true if browser fires popstate on hash change.
* IE10 and IE11 do not.
*/
var supportsPopStateOnHashChange = function supportsPopStateOnHashChange() {
return window.navigator.userAgent.indexOf('Trident') === -1;
};
/**
* Returns false if using go(n) with hash history causes a full page reload.
*/
/**
* Returns true if a given popstate event is an extraneous WebKit event.
* Accounts for the fact that Chrome on iOS fires real popstate events
* containing undefined state when pressing the back button.
*/
var isExtraneousPopstateEvent = function isExtraneousPopstateEvent(event) {
return event.state === undefined && navigator.userAgent.indexOf('CriOS') === -1;
};
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var PopStateEvent = 'popstate';
var HashChangeEvent = 'hashchange';
var getHistoryState = function getHistoryState() {
try {
return window.history.state || {};
} catch (e) {
// IE 11 sometimes throws when accessing window.history.state
// See https://github.com/ReactTraining/history/pull/289
return {};
}
};
/**
* Creates a history object that uses the HTML5 history API including
* pushState, replaceState, and the popstate event.
*/
var createBrowserHistory = function createBrowserHistory() {
var props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
invariant_1(canUseDOM, 'Browser history needs a DOM');
var globalHistory = window.history;
var canUseHistory = supportsHistory();
var needsHashChangeListener = !supportsPopStateOnHashChange();
var _props$forceRefresh = props.forceRefresh,
forceRefresh = _props$forceRefresh === undefined ? false : _props$forceRefresh,
_props$getUserConfirm = props.getUserConfirmation,
getUserConfirmation = _props$getUserConfirm === undefined ? getConfirmation : _props$getUserConfirm,
_props$keyLength = props.keyLength,
keyLength = _props$keyLength === undefined ? 6 : _props$keyLength;
var basename = props.basename ? stripTrailingSlash(addLeadingSlash(props.basename)) : '';
var getDOMLocation = function getDOMLocation(historyState) {
var _ref = historyState || {},
key = _ref.key,
state = _ref.state;
var _window$location = window.location,
pathname = _window$location.pathname,
search = _window$location.search,
hash = _window$location.hash;
var path = pathname + search + hash;
warning_1(!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, state, key);
};
var createKey = function createKey() {
return Math.random().toString(36).substr(2, keyLength);
};
var transitionManager = createTransitionManager();
var setState = function setState(nextState) {
_extends(history, nextState);
history.length = globalHistory.length;
transitionManager.notifyListeners(history.location, history.action);
};
var handlePopState = function handlePopState(event) {
// Ignore extraneous popstate events in WebKit.
if (isExtraneousPopstateEvent(event)) return;
handlePop(getDOMLocation(event.state));
};
var handleHashChange = function handleHashChange() {
handlePop(getDOMLocation(getHistoryState()));
};
var forceNextPop = false;
var handlePop = function handlePop(location) {
if (forceNextPop) {
forceNextPop = false;
setState();
} else {
var action = 'POP';
transitionManager.confirmTransitionTo(location, action, getUserConfirmation, function (ok) {
if (ok) {
setState({ action: action, location: location });
} else {
revertPop(location);
}
});
}
};
var revertPop = function revertPop(fromLocation) {
var 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.
var toIndex = allKeys.indexOf(toLocation.key);
if (toIndex === -1) toIndex = 0;
var fromIndex = allKeys.indexOf(fromLocation.key);
if (fromIndex === -1) fromIndex = 0;
var delta = toIndex - fromIndex;
if (delta) {
forceNextPop = true;
go(delta);
}
};
var initialLocation = getDOMLocation(getHistoryState());
var allKeys = [initialLocation.key];
// Public interface
var createHref = function createHref(location) {
return basename + createPath(location);
};
var push = function push(path, state) {
warning_1(!((typeof path === 'undefined' ? 'undefined' : _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');
var action = 'PUSH';
var location = createLocation(path, state, createKey(), history.location);
transitionManager.confirmTransitionTo(location, action, getUserConfirmation, function (ok) {
if (!ok) return;
var href = createHref(location);
var key = location.key,
state = location.state;
if (canUseHistory) {
globalHistory.pushState({ key: key, state: state }, null, href);
if (forceRefresh) {
window.location.href = href;
} else {
var prevIndex = allKeys.indexOf(history.location.key);
var nextKeys = allKeys.slice(0, prevIndex === -1 ? 0 : prevIndex + 1);
nextKeys.push(location.key);
allKeys = nextKeys;
setState({ action: action, location: location });
}
} else {
warning_1(state === undefined, 'Browser history cannot push state in browsers that do not support HTML5 history');
window.location.href = href;
}
});
};
var replace = function replace(path, state) {
warning_1(!((typeof path === 'undefined' ? 'undefined' : _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');
var action = 'REPLACE';
var location = createLocation(path, state, createKey(), history.location);
transitionManager.confirmTransitionTo(location, action, getUserConfirmation, function (ok) {
if (!ok) return;
var href = createHref(location);
var key = location.key,
state = location.state;
if (canUseHistory) {
globalHistory.replaceState({ key: key, state: state }, null, href);
if (forceRefresh) {
window.location.replace(href);
} else {
var prevIndex = allKeys.indexOf(history.location.key);
if (prevIndex !== -1) allKeys[prevIndex] = location.key;
setState({ action: action, location: location });
}
} else {
warning_1(state === undefined, 'Browser history cannot replace state in browsers that do not support HTML5 history');
window.location.replace(href);
}
});
};
var go = function go(n) {
globalHistory.go(n);
};
var goBack = function goBack() {
return go(-1);
};
var goForward = function goForward() {
return go(1);
};
var listenerCount = 0;
var checkDOMListeners = function checkDOMListeners(delta) {
listenerCount += delta;
if (listenerCount === 1) {
addEventListener(window, PopStateEvent, handlePopState);
if (needsHashChangeListener) addEventListener(window, HashChangeEvent, handleHashChange);
} else if (listenerCount === 0) {
removeEventListener(window, PopStateEvent, handlePopState);
if (needsHashChangeListener) removeEventListener(window, HashChangeEvent, handleHashChange);
}
};
var isBlocked = false;
var block = function block() {
var prompt = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
var unblock = transitionManager.setPrompt(prompt);
if (!isBlocked) {
checkDOMListeners(1);
isBlocked = true;
}
return function () {
if (isBlocked) {
isBlocked = false;
checkDOMListeners(-1);
}
return unblock();
};
};
var listen = function listen(listener) {
var unlisten = transitionManager.appendListener(listener);
checkDOMListeners(1);
return function () {
checkDOMListeners(-1);
unlisten();
};
};
var history = {
length: globalHistory.length,
action: 'POP',
location: initialLocation,
createHref: createHref,
push: push,
replace: replace,
go: go,
goBack: goBack,
goForward: goForward,
block: block,
listen: listen
};
return history;
};
var __assign = (undefined && undefined.__assign) || Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
function concatLocations(firstLocation) {
var locations = [];
for (var _i = 1; _i < arguments.length; _i++) {
locations[_i - 1] = arguments[_i];
}
var result = typeof firstLocation === 'string'
? { pathname: firstLocation }
: firstLocation;
for (var i = 0; i < locations.length; i++) {
var location_1 = locations[i];
if (typeof location_1 === 'string') {
location_1 = parseLocationString(location_1);
}
result = __assign({}, result, { pathname: joinPaths(result.pathname, location_1.pathname), search: joinQueryStrings(result.search, location_1.search, '?'), hash: joinQueryStrings(result.hash, location_1.hash, '#'), state: result.state || location_1.state ? Object.assign({}, result.state, location_1.state) : undefined });
}
return result;
}
var parsePattern = /^((((\/?(?:[^\/\?#]+\/+)*)([^\?#]*)))?(\?[^#]+)?)(#.*)?/;
function parseLocationString(locationString) {
var matches = parsePattern.exec(locationString);
if (!matches) {
throw new Error("Tried to parse a non-URI object.");
}
return {
pathname: matches[2],
search: matches[6],
hash: matches[7],
};
}
function joinPaths(a, b) {
if (!b) {
return a;
}
if (a[a.length - 1] === '/') {
a = a.substr(0, a.length - 1);
}
if (b[0] === '/') {
b = b.substr(1);
}
return a + '/' + b;
}
function joinQueryStrings(left, right, leadingCharacter) {
if (leadingCharacter === void 0) { leadingCharacter = '?'; }
if (!left || left[0] !== leadingCharacter) {
return right;
}
if (!right || right[0] !== leadingCharacter) {
return left;
}
return leadingCharacter + left.slice(1) + '&' + right.slice(1);
}
function createHref(location) {
return location.pathname + (location.search || '') + (location.hash || '');
}
function parseQuery(queryString, leadingCharacter) {
if (leadingCharacter === void 0) { leadingCharacter = '?'; }
if (!queryString || queryString[0] != leadingCharacter) {
return {};
}
var query = {};
var queryParts = queryString.slice(1).split('&');
for (var i = 0, len = queryParts.length; i < len; i++) {
var x = queryParts[i].split('=');
query[x[0]] = x[1] ? decodeURIComponent(x[1]) : '';
}
return query;
}
function stringifyQuery(query, leadingCharacter) {
if (leadingCharacter === void 0) { leadingCharacter = '?'; }
var keys = Object.keys(query);
if (keys.length === 0) {
return '';
}
var parts = [];
for (var i = 0, len = keys.length; i < len; i++) {
var key = keys[i];
var value = String(query[key]);
parts.push(value === '' ? key : key + '=' + encodeURIComponent(value));
}
return leadingCharacter + parts.join('&');
}
var __assign$1 = (undefined && undefined.__assign) || Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
var KEY_WILDCARD = '\0';
function createRootMountedPattern(template, relativePath) {
var rootPattern = relativePath
? compilePattern(relativePath, template)
: {
relativePattern: relativePath || '',
relativeKey: '',
relativeRegExp: new RegExp(''),
template: template
};
if (rootPattern.relativePathParams && rootPattern.relativePathParams.length > 0) {
throw new Error("Your root path may not contain parameters");
}
return __assign$1({}, rootPattern, { params: [] });
}
function compilePattern(pattern, template) {
var processedPattern = pattern;
if (processedPattern.length > 1 && processedPattern.substr(-1) === '/') {
{
console.warn("The pattern \"" + pattern + "\" ends with the character '/', so it has been automatically removed. To avoid this warning, don't add a final \"/\" to Junction patterns.");
}
processedPattern = processedPattern.substr(0, processedPattern.length - 1);
}
if (processedPattern[0] !== '/') {
{
console.warn("The pattern \"" + pattern + "\" does not start with the character '/', so it has been automatically added. To avoid this warning, make sure to add the leading \"/\" to all Junction patterns.");
}
processedPattern = '/' + processedPattern;
}
if (/\/{2,}/.test(processedPattern)) {
{
console.warn("The pattern \"" + pattern + " has adjacent '/' characters, which have been combined into single '/' characters. To avoid this warning, don't use adjacent '/' characters within patterns.");
}
processedPattern = processedPattern.replace(/\/{2,}/g, '/');
}
if (!/^([A-Za-z0-9\$\-_\.+!*'\(\),\/]|\/:)+$/.test(processedPattern)) {
{
console.warn("The pattern \"" + pattern + "\" uses non-URL safe characters. The URL-safe characters are: A-Z a-z 0-9 $ - _ . + ! * ' ( ) ,");
}
}
if (processedPattern.length === 0) {
throw new Error("You cannot use an empty string \"\" as a Junction pattern!");
}
var parts = processedPattern.split('/').slice(1);
var pathParams = [];
var keyParts = [];
var regExpParts = ['^'];
for (var i = 0; i < parts.length; i++) {
var part = parts[i];
if (part.length > 1 && part[0] === ':') {
pathParams.push(part.slice(1));
keyParts.push(KEY_WILDCARD);
regExpParts.push('([^/]+)');
}
else {
keyParts.push(part);
regExpParts.push(escapeRegExp(part));
}
}
return {
relativePattern: processedPattern,
relativeKey: keyParts.join('/'),
relativePathParams: pathParams.length ? pathParams : undefined,
relativeRegExp: new RegExp(regExpParts.join('/')),
template: template,
};
}
function createChildMountedPattern(parentPattern, compiledPattern) {
{
if (compiledPattern.relativePathParams) {
var doubleParams = compiledPattern.relativePathParams.filter(function (param) { return parentPattern.params.indexOf(param) !== -1; });
if (doubleParams.length) {
console.error("The pattern \"" + compiledPattern.relativePattern + "\" uses the param names " + doubleParams.map(function (x) { return "\"" + x + "\""; }).join(', ') + ", which have already been used by a parent junction.");
}
}
}
return __assign$1({ params: parentPattern.params.concat(compiledPattern.relativePathParams || []) }, compiledPattern);
}
function addParamsToMountedPattern(pattern, params) {
var relativeSearchParams = params || [];
// Ensure that any params in the mount's path are also specified by the
// mounted junction's "params" config.
if (pattern.relativePathParams) {
for (var i = pattern.relativePathParams.length - 1; i >= 0; i--) {
var pathParam = pattern.relativePathParams[i];
var index = relativeSearchParams.indexOf(pathParam);
if (index === -1) {
{
console.warn("The path parameter \":" + pathParam + "\" was not specified in its associated junctions' \"params\" configuration option. To avoid this warning, always specify your junctions' \"params\" object.");
}
}
else {
relativeSearchParams.splice(index, 1);
}
}
}
// If there are no search params, the mount won't chang
if (relativeSearchParams.length === 0) {
return pattern;
}
// Ensure that none of our search param names are already used by parent
// junctions.
{
var doubleParams = relativeSearchParams.filter(function (param) { return pattern.params.indexOf(param) !== -1; });
if (doubleParams.length) {
console.error("The junction mounted at \"" + pattern.relativePattern + "\" uses the param names " + doubleParams.map(function (x) { return "\"" + x + "\""; }).join(', ') + ", which have already been used by a parent junction.");
}
}
return __assign$1({}, pattern, { relativeSearchParams: relativeSearchParams.length ? relativeSearchParams : undefined });
}
function matchMountedPatternAgainstLocation(pattern, location) {
var match = pattern.relativeRegExp.exec(location.pathname);
var params = {};
if (!match) {
return;
}
// Set path params using RegExp match
if (pattern.relativePathParams) {
for (var i = 0; i < pattern.relativePathParams.length; i++) {
var paramName = pattern.relativePathParams[i];
params[paramName] = match[i + 1];
}
}
var matchedQueryParts = {};
var remainingQueryParts = parseQuery(location.search);
if (pattern.relativeSearchParams) {
for (var i = 0; i < pattern.relativeSearchParams.length; i++) {
var paramName = pattern.relativeSearchParams[i];
if (remainingQueryParts[paramName] !== undefined) {
params[paramName] = remainingQueryParts[paramName];
matchedQueryParts[paramName] = remainingQueryParts[paramName];
delete remainingQueryParts[paramName];
}
}
}
var matchedLocation = {
pathname: match[0],
search: stringifyQuery(matchedQueryParts),
};
var remainingLocation = {
pathname: location.pathname.slice(match[0].length),
search: stringifyQuery(remainingQueryParts),
hash: location.hash,
state: location.state,
};
return {
params: params,
matchedLocation: matchedLocation,
remainingLocation: remainingLocation.pathname !== '' || remainingLocation.search !== '' ? remainingLocation : undefined
};
}
// From http://stackoverflow.com/a/5306111/106302
// Originally from http://simonwillison.net/2006/Jan/20/escape/ (dead link)
function escapeRegExp(value) {
return value.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
}
var Router = /** @class */ (function () {
function Router(config) {
var _this = this;
this.listeners = [];
this.notifyListeners = function () {
for (var i = 0; i < _this.listeners.length; i++) {
_this.listeners[i]();
}
};
this.listeners = [];
this.config = config;
}
Router.prototype.getRoute = function () {
return this.rootMatcher && this.rootMatcher.getRoute();
};
/**
* If you're using code splitting, you'll need to subscribe to changes to
* Navigation state, as the state may change as new code chunks are
* received.
*/
Router.prototype.subscribe = function (onRouteChange) {
var _this = this;
this.listeners.push(onRouteChange);
return function () {
var index = _this.listeners.indexOf(onRouteChange);
if (index !== -1) {
_this.listeners.splice(index, 1);
}
};
};
Router.prototype.isBusy = function () {
if (this.rootMatcher) {
return this.rootMatcher.isBusy();
}
return false;
};
Router.prototype.setLocation = function (location) {
var locationExistenceHasChanged = (location && !this.location) ||
(!location && this.location);
var pathHasChanged, searchHasChanged;
if (location && this.location) {
pathHasChanged = location.pathname !== this.location.pathname;
searchHasChanged = location.search !== this.location.search;
}
// The router only looks at path and search, so if they haven't
// changed, nothing else will change either.
if (!(pathHasChanged || searchHasChanged || locationExistenceHasChanged)) {
this.notifyListeners();
return;
}
this.location = location;
var match = location && matchMountedPatternAgainstLocation(this.config.rootMountedPattern, location);
if (!match && this.rootMatcher) {
this.rootMatcher.willUnmount();
this.rootMatcher = undefined;
this.notifyListeners();
return;
}
else if (location && match) {
if (this.rootMatcher) {
this.rootMatcher.willUnmount();
}
this.rootMatcher = new this.config.rootJunctionTemplate({
routerConfig: this.config,
parentLocationPart: { pathname: '' },
matchableLocationPart: location,
mountedPattern: this.config.rootMountedPattern,
onChange: this.notifyListeners,
shouldFetchContent: true,
});
this.notifyListeners();
}
};
return Router;
}());
function createRouterConfig(options) {
return {
rootJunctionTemplate: options.rootJunctionTemplate,
rootMountedPattern: createRootMountedPattern(options.rootJunctionTemplate, options.rootPath),
onEvent: options.onEvent || (function () { })
};
}
var Deferred = /** @class */ (function () {
function Deferred() {
this.promise = new Promise(function (resolve, reject) {
this.resolve = resolve;
this.reject = reject;
}.bind(this));
Object.freeze(this);
}
return Deferred;
}());
var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (undefined && undefined.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = y[op[0] & 2 ? "return" : op[0] ? "throw" : "next"]) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [0, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
var defaultPredicate = function (segment) { return true; };
function createContentHelpers(routerConfig) {
function getPages(pathnames) {
if (typeof pathnames === 'string') {
return getPageNode({ pathname: pathnames }).then(function (page) {
return page ? Promise.resolve(page) : Promise.reject(undefined);
});
}
else {
var keys_1 = Object.keys(pathnames);
var promises = [];
for (var i = 0; i < keys_1.length; i++) {
var key = keys_1[i];
promises.push(getPageNode({ pathname: pathnames[key] }));
}
return Promise.all(promises).then(function (values) {
var result = {};
for (var i = 0; i < keys_1.length; i++) {
var value = values[i];
if (value === undefined) {
return Promise.reject(undefined);
}
result[keys_1[i]] = value;
}
return Promise.resolve(result);
});
}
}
/**
* Return a promise to a RouteSemgent that corresponds to the given
* pathname, or reject the promise if a corresponding segment can't be
* found.
*/
function getRouteSegment(pathname) {
var deferred = new Deferred();
var searchLocation = {
// Add a trailing slash, in case the pathname corresponds directly
// to a junction with no '/' child -- otherwise no match will be
// found.
pathname: pathname.substr(-1) === '/' ? pathname : pathname + '/'
};
processFinalNodeWithoutContent(searchLocation, function (route) {
if (route) {
var lastSegment = route[route.length - 1];
if (lastSegment.location.pathname === pathname) {
deferred.resolve(lastSegment);
}
else if (lastSegment.location.pathname === pathname + '/') {
// If the user requested a segment without a trailing
// slash, but we found one *with* a trailing slash, then
// we've found a child of a requested junction.
var lastJunction = route[route.length - 2];
if (lastJunction) {
deferred.resolve(lastJunction);
}
}
}
deferred.reject(undefined);
});
return deferred.promise;
}
function getPageMap(junction, predicate) {
if (predicate === void 0) { predicate = defaultPredicate; }
return __awaiter(this, void 0, void 0, function () {
var map, template, children, possiblePromises, queue, junction_1, _i, children_1, _a, pattern, template_1, path, segment;
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
map = {};
template = junction.template;
children = Object.entries(template.children);
queue = [junction];
_b.label = 1;
case 1:
if (!queue.length) return [3 /*break*/, 6];
junction_1 = queue.shift();
_i = 0, children_1 = children;
_b.label = 2;
case 2:
if (!(_i < children_1.length)) return [3 /*break*/, 5];
_a = children_1[_i], pattern = _a[0], template_1 = _a[1];
path = junction_1.location.pathname + pattern;
return [4 /*yield*/, getRouteSegment(path)];
case 3:
segment = _b.sent();
if (segment.type !== 'redirect' && predicate(segment)) {
if (segment.type === 'junction') {
queue.push(junction_1);
}
else {
map[path] = segment;
}
}
_b.label = 4;
case 4:
_i++;
return [3 /*break*/, 2];
case 5: return [3 /*break*/, 1];
case 6: return [2 /*return*/, map];
}
});
});
}
function getPageNode(location) {
var deferred = new Deferred();
processFinalNodeWithoutContent(location, function (route) {
var lastSegment = route && route[route.length - 1];
if (!lastSegment || lastSegment.type === 'junction') {
deferred.resolve(undefined);
}
else if (lastSegment.type === 'page') {
return deferred.resolve(lastSegment);
}
else {
getPageNode(lastSegment.to).then(deferred.resolve);
}
});
return deferred.promise;
}
function processFinalNodeWithoutContent(location, callback) {
var match = matchMountedPatternAgainstLocation(routerConfig.rootMountedPattern, location);
if (!match) {
callback();
}
else {
var deferred = new Deferred();
var handleRouteChange = function () {
if (!rootMatcher_1.isBusy()) {
callback(rootMatcher_1.getRoute());
rootMatcher_1.willUnmount();
}
};
var rootMatcher_1 = new routerConfig.rootJunctionTemplate({
parentLocationPart: { pathname: '' },
matchableLocationPart: location,
mountedPattern: routerConfig.rootMountedPattern,
onChange: handleRouteChange,
routerConfig: routerConfig,
shouldFetchContent: false,
});
handleRouteChange();
}
}
return {
getPages: getPages,
getRouteSegment: getRouteSegment,
getPageMap: getPageMap,
};
}
var BrowserNavigation = /** @class */ (function () {
function BrowserNavigation(options) {
var _this = this;
this.handleHistoryChange = function (location) {
if (_this.location &&
!!_this.disableScrollHandling &&
location.pathname === _this.location.pathname &&
location.hash === _this.location.hash &&
location.search === _this.location.search) {
_this.scrollToHash(location.hash);
}
else {
_this.router.setLocation(location);
}
};
this.handleLocationChange = function (previousLocation, nextLocation) {
if (previousLocation === nextLocation) {
return;
}
if (!_this.disableScrollHandling &&
(!previousLocation ||
previousLocation.hash !== nextLocation.hash ||
(!nextLocation.hash && previousLocation.pathname !== nextLocation.pathname))) {
_this.location = nextLocation;
if (previousLocation) {
_this.scrollToHash(nextLocation.hash);
}
}
};
this.handleRouteChange = function () {
var isBusy = _this.router.isBusy();
var route = _this.router.getRoute();
var lastSegment = route && route[route.length - 1];
var redirectTo;
var title;
if (!isBusy && lastSegment) {
if (lastSegment.type === "page" && _this.history.location.pathname.substr(-1) !== '/') {
redirectTo = concatLocations(_this.history.location, { pathname: '/' });
}
title = null;
if (lastSegment.type === "redirect") {
redirectTo = lastSegment.to;
}
else if (lastSegment.type === "page") {
if (lastSegment.contentStatus !== "busy") {
_this.waitingForInitialContent = false;
}
title = lastSegment.title;
}
else if (lastSegment.status !== "busy") {
_this.waitingForInitialContent = false;
}
}
if (_this.followRedirects && redirectTo) {
_this.replaceLocation(redirectTo);
return;
}
if (title !== undefined) {
if (_this.announceTitle) {
announce(_this.announceTitle(title));
}
if (_this.setDocumentTitle) {
document.title = _this.setDocumentTitle(title);
}
}
// Wait until all subscribers have finished handling the changes
// before emitting `handleLocationChange`.
var waitCount = 0;
var decreaseWaitCount = function () {
if (--waitCount <= 0) {
if (!isBusy) {
_this.handleLocationChange(_this.location, _this.history.location);
}
}
};
for (var _i = 0, _a = _this.subscribers; _i < _a.length; _i++) {
var subscriber = _a[_i];
if (!isBusy || !subscriber.waitForInitialContent || !_this.waitingForInitialContent) {
if (subscriber.callback.length > 0) {
waitCount++;
subscriber.callback(decreaseWaitCount);
}
else {
subscriber.callback();
}
}
}
if (waitCount === 0) {
decreaseWaitCount();
}
};
if (options.announceTitle !== false) {
this.announceTitle = typeof options.announceTitle === 'function' ? options.announceTitle : (function (x) { return x || 'Untitled Page'; });
// Add an announcer div to the DOM, if we haven't already created one
createAnnouncerDiv();
}
if (options.setDocumentTitle !== false) {
this.setDocumentTitle = typeof options.setDocumentTitle === 'function' ? options.setDocumentTitle : (function (x) { return x || 'Untitled Page'; });
}
this.autoscroll = options.autoscroll !== undefined ? options.autoscroll : true;
this.followRedirects = options.followRedirects !== undefined ? options.followRedirects : true;
this.subscribers = [];
this.waitingForInitialContent = true;
this.disableScrollHandling = !!options.disableScrollHandling;
this.history = options.history || createBrowserHistory();
this.history.listen(this.handleHistoryChange);
var routerConfig = createRouterConfig({
rootJunctionTemplate: options.rootJunctionTemplate
});
this.router = new Router(routerConfig);
this.router.subscribe(this.handleRouteChange);
Object.assign(this, createContentHelpers(routerConfig));
this.router.setLocation(this.history.location);
}
/**
* Subscribe to new states from the Navigation object
* @callback onChange - called when state changes
* @argument waitForInitialContent - if try, will not be called until the initial location's content has loaded
*/
BrowserNavigation.prototype.subscribe = function (onChange, options) {
var _this = this;
if (options === void 0) { options = {}; }
var subscriber = {
callback: onChange,
waitForInitialContent: !!options.waitForInitialContent,
};
this.subscribers.push(subscriber);
return function () {
var index = _this.subscribers.indexOf(subscriber);