react-router
Version:
A complete routing library for React
275 lines (217 loc) • 8.95 kB
JavaScript
exports.__esModule = true;
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; };
exports.default = createTransitionManager;
var _routerWarning = require('./routerWarning');
var _routerWarning2 = _interopRequireDefault(_routerWarning);
var _computeChangedRoutes2 = require('./computeChangedRoutes');
var _computeChangedRoutes3 = _interopRequireDefault(_computeChangedRoutes2);
var _TransitionUtils = require('./TransitionUtils');
var _isActive2 = require('./isActive');
var _isActive3 = _interopRequireDefault(_isActive2);
var _getComponents = require('./getComponents');
var _getComponents2 = _interopRequireDefault(_getComponents);
var _matchRoutes = require('./matchRoutes');
var _matchRoutes2 = _interopRequireDefault(_matchRoutes);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function hasAnyProperties(object) {
for (var p in object) {
if (Object.prototype.hasOwnProperty.call(object, p)) return true;
}return false;
}
function createTransitionManager(history, routes) {
var state = {};
// Signature should be (location, indexOnly), but needs to support (path,
// query, indexOnly)
function isActive(location, indexOnly) {
location = history.createLocation(location);
return (0, _isActive3.default)(location, indexOnly, state.location, state.routes, state.params);
}
var partialNextState = void 0;
function match(location, callback) {
if (partialNextState && partialNextState.location === location) {
// Continue from where we left off.
finishMatch(partialNextState, callback);
} else {
(0, _matchRoutes2.default)(routes, location, function (error, nextState) {
if (error) {
callback(error);
} else if (nextState) {
finishMatch(_extends({}, nextState, { location: location }), callback);
} else {
callback();
}
});
}
}
function finishMatch(nextState, callback) {
var _computeChangedRoutes = (0, _computeChangedRoutes3.default)(state, nextState),
leaveRoutes = _computeChangedRoutes.leaveRoutes,
changeRoutes = _computeChangedRoutes.changeRoutes,
enterRoutes = _computeChangedRoutes.enterRoutes;
(0, _TransitionUtils.runLeaveHooks)(leaveRoutes, state);
// Tear down confirmation hooks for left routes
leaveRoutes.filter(function (route) {
return enterRoutes.indexOf(route) === -1;
}).forEach(removeListenBeforeHooksForRoute);
// change and enter hooks are run in series
(0, _TransitionUtils.runChangeHooks)(changeRoutes, state, nextState, function (error, redirectInfo) {
if (error || redirectInfo) return handleErrorOrRedirect(error, redirectInfo);
(0, _TransitionUtils.runEnterHooks)(enterRoutes, nextState, finishEnterHooks);
});
function finishEnterHooks(error, redirectInfo) {
if (error || redirectInfo) return handleErrorOrRedirect(error, redirectInfo);
// TODO: Fetch components after state is updated.
(0, _getComponents2.default)(nextState, function (error, components) {
if (error) {
callback(error);
} else {
// TODO: Make match a pure function and have some other API
// for "match and update state".
callback(null, null, state = _extends({}, nextState, { components: components }));
}
});
}
function handleErrorOrRedirect(error, redirectInfo) {
if (error) callback(error);else callback(null, redirectInfo);
}
}
var RouteGuid = 1;
function getRouteID(route) {
var create = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
return route.__id__ || create && (route.__id__ = RouteGuid++);
}
var RouteHooks = Object.create(null);
function getRouteHooksForRoutes(routes) {
return routes.map(function (route) {
return RouteHooks[getRouteID(route)];
}).filter(function (hook) {
return hook;
});
}
function transitionHook(location, callback) {
(0, _matchRoutes2.default)(routes, location, function (error, nextState) {
if (nextState == null) {
// TODO: We didn't actually match anything, but hang
// onto error/nextState so we don't have to matchRoutes
// again in the listen callback.
callback();
return;
}
// Cache some state here so we don't have to
// matchRoutes() again in the listen callback.
partialNextState = _extends({}, nextState, { location: location });
var hooks = getRouteHooksForRoutes((0, _computeChangedRoutes3.default)(state, partialNextState).leaveRoutes);
var result = void 0;
for (var i = 0, len = hooks.length; result == null && i < len; ++i) {
// Passing the location arg here indicates to
// the user that this is a transition hook.
result = hooks[i](location);
}
callback(result);
});
}
/* istanbul ignore next: untestable with Karma */
function beforeUnloadHook() {
// Synchronously check to see if any route hooks want
// to prevent the current window/tab from closing.
if (state.routes) {
var hooks = getRouteHooksForRoutes(state.routes);
var message = void 0;
for (var i = 0, len = hooks.length; typeof message !== 'string' && i < len; ++i) {
// Passing no args indicates to the user that this is a
// beforeunload hook. We don't know the next location.
message = hooks[i]();
}
return message;
}
}
var unlistenBefore = void 0,
unlistenBeforeUnload = void 0;
function removeListenBeforeHooksForRoute(route) {
var routeID = getRouteID(route);
if (!routeID) {
return;
}
delete RouteHooks[routeID];
if (!hasAnyProperties(RouteHooks)) {
// teardown transition & beforeunload hooks
if (unlistenBefore) {
unlistenBefore();
unlistenBefore = null;
}
if (unlistenBeforeUnload) {
unlistenBeforeUnload();
unlistenBeforeUnload = null;
}
}
}
/**
* Registers the given hook function to run before leaving the given route.
*
* During a normal transition, the hook function receives the next location
* as its only argument and can return either a prompt message (string) to show the user,
* to make sure they want to leave the page; or `false`, to prevent the transition.
* Any other return value will have no effect.
*
* During the beforeunload event (in browsers) the hook receives no arguments.
* In this case it must return a prompt message to prevent the transition.
*
* Returns a function that may be used to unbind the listener.
*/
function listenBeforeLeavingRoute(route, hook) {
var thereWereNoRouteHooks = !hasAnyProperties(RouteHooks);
var routeID = getRouteID(route, true);
RouteHooks[routeID] = hook;
if (thereWereNoRouteHooks) {
// setup transition & beforeunload hooks
unlistenBefore = history.listenBefore(transitionHook);
if (history.listenBeforeUnload) unlistenBeforeUnload = history.listenBeforeUnload(beforeUnloadHook);
}
return function () {
removeListenBeforeHooksForRoute(route);
};
}
/**
* This is the API for stateful environments. As the location
* changes, we update state and call the listener. We can also
* gracefully handle errors and redirects.
*/
function listen(listener) {
function historyListener(location) {
if (state.location === location) {
listener(null, state);
} else {
match(location, function (error, redirectLocation, nextState) {
if (error) {
listener(error);
} else if (redirectLocation) {
history.replace(redirectLocation);
} else if (nextState) {
listener(null, nextState);
} else {
process.env.NODE_ENV !== 'production' ? (0, _routerWarning2.default)(false, 'Location "%s" did not match any routes', location.pathname + location.search + location.hash) : void 0;
}
});
}
}
// TODO: Only use a single history listener. Otherwise we'll end up with
// multiple concurrent calls to match.
// Set up the history listener first in case the initial match redirects.
var unsubscribe = history.listen(historyListener);
if (state.location) {
// Picking up on a matchContext.
listener(null, state);
} else {
historyListener(history.getCurrentLocation());
}
return unsubscribe;
}
return {
isActive: isActive,
match: match,
listenBeforeLeavingRoute: listenBeforeLeavingRoute,
listen: listen
};
}
module.exports = exports['default'];
;