You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
514 lines
17 KiB
514 lines
17 KiB
10 years ago
|
"use strict";
|
||
|
|
||
|
/* jshint -W058 */
|
||
|
var React = require("react");
|
||
|
var warning = require("react/lib/warning");
|
||
|
var invariant = require("react/lib/invariant");
|
||
|
var canUseDOM = require("react/lib/ExecutionEnvironment").canUseDOM;
|
||
|
var LocationActions = require("./actions/LocationActions");
|
||
|
var ImitateBrowserBehavior = require("./behaviors/ImitateBrowserBehavior");
|
||
|
var HashLocation = require("./locations/HashLocation");
|
||
|
var HistoryLocation = require("./locations/HistoryLocation");
|
||
|
var RefreshLocation = require("./locations/RefreshLocation");
|
||
|
var StaticLocation = require("./locations/StaticLocation");
|
||
|
var ScrollHistory = require("./ScrollHistory");
|
||
|
var createRoutesFromReactChildren = require("./createRoutesFromReactChildren");
|
||
|
var isReactChildren = require("./isReactChildren");
|
||
|
var Transition = require("./Transition");
|
||
|
var PropTypes = require("./PropTypes");
|
||
|
var Redirect = require("./Redirect");
|
||
|
var History = require("./History");
|
||
|
var Cancellation = require("./Cancellation");
|
||
|
var Match = require("./Match");
|
||
|
var Route = require("./Route");
|
||
|
var supportsHistory = require("./supportsHistory");
|
||
|
var PathUtils = require("./PathUtils");
|
||
|
|
||
|
/**
|
||
|
* The default location for new routers.
|
||
|
*/
|
||
|
var DEFAULT_LOCATION = canUseDOM ? HashLocation : "/";
|
||
|
|
||
|
/**
|
||
|
* The default scroll behavior for new routers.
|
||
|
*/
|
||
|
var DEFAULT_SCROLL_BEHAVIOR = canUseDOM ? ImitateBrowserBehavior : null;
|
||
|
|
||
|
function hasProperties(object, properties) {
|
||
|
for (var propertyName in properties) if (properties.hasOwnProperty(propertyName) && object[propertyName] !== properties[propertyName]) {
|
||
|
return false;
|
||
|
}return true;
|
||
|
}
|
||
|
|
||
|
function hasMatch(routes, route, prevParams, nextParams, prevQuery, nextQuery) {
|
||
|
return routes.some(function (r) {
|
||
|
if (r !== route) return false;
|
||
|
|
||
|
var paramNames = route.paramNames;
|
||
|
var paramName;
|
||
|
|
||
|
// Ensure that all params the route cares about did not change.
|
||
|
for (var i = 0, len = paramNames.length; i < len; ++i) {
|
||
|
paramName = paramNames[i];
|
||
|
|
||
|
if (nextParams[paramName] !== prevParams[paramName]) return false;
|
||
|
}
|
||
|
|
||
|
// Ensure the query hasn't changed.
|
||
|
return hasProperties(prevQuery, nextQuery) && hasProperties(nextQuery, prevQuery);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function addRoutesToNamedRoutes(routes, namedRoutes) {
|
||
|
var route;
|
||
|
for (var i = 0, len = routes.length; i < len; ++i) {
|
||
|
route = routes[i];
|
||
|
|
||
|
if (route.name) {
|
||
|
invariant(namedRoutes[route.name] == null, "You may not have more than one route named \"%s\"", route.name);
|
||
|
|
||
|
namedRoutes[route.name] = route;
|
||
|
}
|
||
|
|
||
|
if (route.childRoutes) addRoutesToNamedRoutes(route.childRoutes, namedRoutes);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function routeIsActive(activeRoutes, routeName) {
|
||
|
return activeRoutes.some(function (route) {
|
||
|
return route.name === routeName;
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function paramsAreActive(activeParams, params) {
|
||
|
for (var property in params) if (String(activeParams[property]) !== String(params[property])) {
|
||
|
return false;
|
||
|
}return true;
|
||
|
}
|
||
|
|
||
|
function queryIsActive(activeQuery, query) {
|
||
|
for (var property in query) if (String(activeQuery[property]) !== String(query[property])) {
|
||
|
return false;
|
||
|
}return true;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Creates and returns a new router using the given options. A router
|
||
|
* is a ReactComponent class that knows how to react to changes in the
|
||
|
* URL and keep the contents of the page in sync.
|
||
|
*
|
||
|
* Options may be any of the following:
|
||
|
*
|
||
|
* - routes (required) The route config
|
||
|
* - location The location to use. Defaults to HashLocation when
|
||
|
* the DOM is available, "/" otherwise
|
||
|
* - scrollBehavior The scroll behavior to use. Defaults to ImitateBrowserBehavior
|
||
|
* when the DOM is available, null otherwise
|
||
|
* - onError A function that is used to handle errors
|
||
|
* - onAbort A function that is used to handle aborted transitions
|
||
|
*
|
||
|
* When rendering in a server-side environment, the location should simply
|
||
|
* be the URL path that was used in the request, including the query string.
|
||
|
*/
|
||
|
function createRouter(options) {
|
||
|
options = options || {};
|
||
|
|
||
|
if (isReactChildren(options)) options = { routes: options };
|
||
|
|
||
|
var mountedComponents = [];
|
||
|
var location = options.location || DEFAULT_LOCATION;
|
||
|
var scrollBehavior = options.scrollBehavior || DEFAULT_SCROLL_BEHAVIOR;
|
||
|
var state = {};
|
||
|
var nextState = {};
|
||
|
var pendingTransition = null;
|
||
|
var dispatchHandler = null;
|
||
|
|
||
|
if (typeof location === "string") location = new StaticLocation(location);
|
||
|
|
||
|
if (location instanceof StaticLocation) {
|
||
|
warning(!canUseDOM || process.env.NODE_ENV === "test", "You should not use a static location in a DOM environment because " + "the router will not be kept in sync with the current URL");
|
||
|
} else {
|
||
|
invariant(canUseDOM || location.needsDOM === false, "You cannot use %s without a DOM", location);
|
||
|
}
|
||
|
|
||
|
// Automatically fall back to full page refreshes in
|
||
|
// browsers that don't support the HTML history API.
|
||
|
if (location === HistoryLocation && !supportsHistory()) location = RefreshLocation;
|
||
|
|
||
|
var Router = React.createClass({
|
||
|
|
||
|
displayName: "Router",
|
||
|
|
||
|
statics: {
|
||
|
|
||
|
isRunning: false,
|
||
|
|
||
|
cancelPendingTransition: function cancelPendingTransition() {
|
||
|
if (pendingTransition) {
|
||
|
pendingTransition.cancel();
|
||
|
pendingTransition = null;
|
||
|
}
|
||
|
},
|
||
|
|
||
|
clearAllRoutes: function clearAllRoutes() {
|
||
|
Router.cancelPendingTransition();
|
||
|
Router.namedRoutes = {};
|
||
|
Router.routes = [];
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Adds routes to this router from the given children object (see ReactChildren).
|
||
|
*/
|
||
|
addRoutes: function addRoutes(routes) {
|
||
|
if (isReactChildren(routes)) routes = createRoutesFromReactChildren(routes);
|
||
|
|
||
|
addRoutesToNamedRoutes(routes, Router.namedRoutes);
|
||
|
|
||
|
Router.routes.push.apply(Router.routes, routes);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Replaces routes of this router from the given children object (see ReactChildren).
|
||
|
*/
|
||
|
replaceRoutes: function replaceRoutes(routes) {
|
||
|
Router.clearAllRoutes();
|
||
|
Router.addRoutes(routes);
|
||
|
Router.refresh();
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Performs a match of the given path against this router and returns an object
|
||
|
* with the { routes, params, pathname, query } that match. Returns null if no
|
||
|
* match can be made.
|
||
|
*/
|
||
|
match: function match(path) {
|
||
|
return Match.findMatch(Router.routes, path);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Returns an absolute URL path created from the given route
|
||
|
* name, URL parameters, and query.
|
||
|
*/
|
||
|
makePath: function makePath(to, params, query) {
|
||
|
var path;
|
||
|
if (PathUtils.isAbsolute(to)) {
|
||
|
path = to;
|
||
|
} else {
|
||
|
var route = to instanceof Route ? to : Router.namedRoutes[to];
|
||
|
|
||
|
invariant(route instanceof Route, "Cannot find a route named \"%s\"", to);
|
||
|
|
||
|
path = route.path;
|
||
|
}
|
||
|
|
||
|
return PathUtils.withQuery(PathUtils.injectParams(path, params), query);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Returns a string that may safely be used as the href of a link
|
||
|
* to the route with the given name, URL parameters, and query.
|
||
|
*/
|
||
|
makeHref: function makeHref(to, params, query) {
|
||
|
var path = Router.makePath(to, params, query);
|
||
|
return location === HashLocation ? "#" + path : path;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Transitions to the URL specified in the arguments by pushing
|
||
|
* a new URL onto the history stack.
|
||
|
*/
|
||
|
transitionTo: function transitionTo(to, params, query) {
|
||
|
var path = Router.makePath(to, params, query);
|
||
|
|
||
|
if (pendingTransition) {
|
||
|
// Replace so pending location does not stay in history.
|
||
|
location.replace(path);
|
||
|
} else {
|
||
|
location.push(path);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Transitions to the URL specified in the arguments by replacing
|
||
|
* the current URL in the history stack.
|
||
|
*/
|
||
|
replaceWith: function replaceWith(to, params, query) {
|
||
|
location.replace(Router.makePath(to, params, query));
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Transitions to the previous URL if one is available. Returns true if the
|
||
|
* router was able to go back, false otherwise.
|
||
|
*
|
||
|
* Note: The router only tracks history entries in your application, not the
|
||
|
* current browser session, so you can safely call this function without guarding
|
||
|
* against sending the user back to some other site. However, when using
|
||
|
* RefreshLocation (which is the fallback for HistoryLocation in browsers that
|
||
|
* don't support HTML5 history) this method will *always* send the client back
|
||
|
* because we cannot reliably track history length.
|
||
|
*/
|
||
|
goBack: function goBack() {
|
||
|
if (History.length > 1 || location === RefreshLocation) {
|
||
|
location.pop();
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
warning(false, "goBack() was ignored because there is no router history");
|
||
|
|
||
|
return false;
|
||
|
},
|
||
|
|
||
|
handleAbort: options.onAbort || function (abortReason) {
|
||
|
if (location instanceof StaticLocation) throw new Error("Unhandled aborted transition! Reason: " + abortReason);
|
||
|
|
||
|
if (abortReason instanceof Cancellation) {
|
||
|
return;
|
||
|
} else if (abortReason instanceof Redirect) {
|
||
|
location.replace(Router.makePath(abortReason.to, abortReason.params, abortReason.query));
|
||
|
} else {
|
||
|
location.pop();
|
||
|
}
|
||
|
},
|
||
|
|
||
|
handleError: options.onError || function (error) {
|
||
|
// Throw so we don't silently swallow async errors.
|
||
|
throw error; // This error probably originated in a transition hook.
|
||
|
},
|
||
|
|
||
|
handleLocationChange: function handleLocationChange(change) {
|
||
|
Router.dispatch(change.path, change.type);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Performs a transition to the given path and calls callback(error, abortReason)
|
||
|
* when the transition is finished. If both arguments are null the router's state
|
||
|
* was updated. Otherwise the transition did not complete.
|
||
|
*
|
||
|
* In a transition, a router first determines which routes are involved by beginning
|
||
|
* with the current route, up the route tree to the first parent route that is shared
|
||
|
* with the destination route, and back down the tree to the destination route. The
|
||
|
* willTransitionFrom hook is invoked on all route handlers we're transitioning away
|
||
|
* from, in reverse nesting order. Likewise, the willTransitionTo hook is invoked on
|
||
|
* all route handlers we're transitioning to.
|
||
|
*
|
||
|
* Both willTransitionFrom and willTransitionTo hooks may either abort or redirect the
|
||
|
* transition. To resolve asynchronously, they may use the callback argument. If no
|
||
|
* hooks wait, the transition is fully synchronous.
|
||
|
*/
|
||
|
dispatch: function dispatch(path, action) {
|
||
|
Router.cancelPendingTransition();
|
||
|
|
||
|
var prevPath = state.path;
|
||
|
var isRefreshing = action == null;
|
||
|
|
||
|
if (prevPath === path && !isRefreshing) {
|
||
|
return;
|
||
|
} // Nothing to do!
|
||
|
|
||
|
// Record the scroll position as early as possible to
|
||
|
// get it before browsers try update it automatically.
|
||
|
if (prevPath && action === LocationActions.PUSH) Router.recordScrollPosition(prevPath);
|
||
|
|
||
|
var match = Router.match(path);
|
||
|
|
||
|
warning(match != null, "No route matches path \"%s\". Make sure you have <Route path=\"%s\"> somewhere in your routes", path, path);
|
||
|
|
||
|
if (match == null) match = {};
|
||
|
|
||
|
var prevRoutes = state.routes || [];
|
||
|
var prevParams = state.params || {};
|
||
|
var prevQuery = state.query || {};
|
||
|
|
||
|
var nextRoutes = match.routes || [];
|
||
|
var nextParams = match.params || {};
|
||
|
var nextQuery = match.query || {};
|
||
|
|
||
|
var fromRoutes, toRoutes;
|
||
|
if (prevRoutes.length) {
|
||
|
fromRoutes = prevRoutes.filter(function (route) {
|
||
|
return !hasMatch(nextRoutes, route, prevParams, nextParams, prevQuery, nextQuery);
|
||
|
});
|
||
|
|
||
|
toRoutes = nextRoutes.filter(function (route) {
|
||
|
return !hasMatch(prevRoutes, route, prevParams, nextParams, prevQuery, nextQuery);
|
||
|
});
|
||
|
} else {
|
||
|
fromRoutes = [];
|
||
|
toRoutes = nextRoutes;
|
||
|
}
|
||
|
|
||
|
var transition = new Transition(path, Router.replaceWith.bind(Router, path));
|
||
|
pendingTransition = transition;
|
||
|
|
||
|
var fromComponents = mountedComponents.slice(prevRoutes.length - fromRoutes.length);
|
||
|
|
||
|
Transition.from(transition, fromRoutes, fromComponents, function (error) {
|
||
|
if (error || transition.abortReason) return dispatchHandler.call(Router, error, transition); // No need to continue.
|
||
|
|
||
|
Transition.to(transition, toRoutes, nextParams, nextQuery, function (error) {
|
||
|
dispatchHandler.call(Router, error, transition, {
|
||
|
path: path,
|
||
|
action: action,
|
||
|
pathname: match.pathname,
|
||
|
routes: nextRoutes,
|
||
|
params: nextParams,
|
||
|
query: nextQuery
|
||
|
});
|
||
|
});
|
||
|
});
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Starts this router and calls callback(router, state) when the route changes.
|
||
|
*
|
||
|
* If the router's location is static (i.e. a URL path in a server environment)
|
||
|
* the callback is called only once. Otherwise, the location should be one of the
|
||
|
* Router.*Location objects (e.g. Router.HashLocation or Router.HistoryLocation).
|
||
|
*/
|
||
|
run: function run(callback) {
|
||
|
invariant(!Router.isRunning, "Router is already running");
|
||
|
|
||
|
dispatchHandler = function (error, transition, newState) {
|
||
|
if (error) Router.handleError(error);
|
||
|
|
||
|
if (pendingTransition !== transition) return;
|
||
|
|
||
|
pendingTransition = null;
|
||
|
|
||
|
if (transition.abortReason) {
|
||
|
Router.handleAbort(transition.abortReason);
|
||
|
} else {
|
||
|
callback.call(Router, Router, nextState = newState);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
if (!(location instanceof StaticLocation)) {
|
||
|
if (location.addChangeListener) location.addChangeListener(Router.handleLocationChange);
|
||
|
|
||
|
Router.isRunning = true;
|
||
|
}
|
||
|
|
||
|
// Bootstrap using the current path.
|
||
|
Router.refresh();
|
||
|
},
|
||
|
|
||
|
refresh: function refresh() {
|
||
|
Router.dispatch(location.getCurrentPath(), null);
|
||
|
},
|
||
|
|
||
|
stop: function stop() {
|
||
|
Router.cancelPendingTransition();
|
||
|
|
||
|
if (location.removeChangeListener) location.removeChangeListener(Router.handleLocationChange);
|
||
|
|
||
|
Router.isRunning = false;
|
||
|
},
|
||
|
|
||
|
getLocation: function getLocation() {
|
||
|
return location;
|
||
|
},
|
||
|
|
||
|
getScrollBehavior: function getScrollBehavior() {
|
||
|
return scrollBehavior;
|
||
|
},
|
||
|
|
||
|
getRouteAtDepth: function getRouteAtDepth(routeDepth) {
|
||
|
var routes = state.routes;
|
||
|
return routes && routes[routeDepth];
|
||
|
},
|
||
|
|
||
|
setRouteComponentAtDepth: function setRouteComponentAtDepth(routeDepth, component) {
|
||
|
mountedComponents[routeDepth] = component;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Returns the current URL path + query string.
|
||
|
*/
|
||
|
getCurrentPath: function getCurrentPath() {
|
||
|
return state.path;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Returns the current URL path without the query string.
|
||
|
*/
|
||
|
getCurrentPathname: function getCurrentPathname() {
|
||
|
return state.pathname;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Returns an object of the currently active URL parameters.
|
||
|
*/
|
||
|
getCurrentParams: function getCurrentParams() {
|
||
|
return state.params;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Returns an object of the currently active query parameters.
|
||
|
*/
|
||
|
getCurrentQuery: function getCurrentQuery() {
|
||
|
return state.query;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Returns an array of the currently active routes.
|
||
|
*/
|
||
|
getCurrentRoutes: function getCurrentRoutes() {
|
||
|
return state.routes;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Returns true if the given route, params, and query are active.
|
||
|
*/
|
||
|
isActive: function isActive(to, params, query) {
|
||
|
if (PathUtils.isAbsolute(to)) {
|
||
|
return to === state.path;
|
||
|
}return routeIsActive(state.routes, to) && paramsAreActive(state.params, params) && (query == null || queryIsActive(state.query, query));
|
||
|
}
|
||
|
|
||
|
},
|
||
|
|
||
|
mixins: [ScrollHistory],
|
||
|
|
||
|
propTypes: {
|
||
|
children: PropTypes.falsy
|
||
|
},
|
||
|
|
||
|
childContextTypes: {
|
||
|
routeDepth: PropTypes.number.isRequired,
|
||
|
router: PropTypes.router.isRequired
|
||
|
},
|
||
|
|
||
|
getChildContext: function getChildContext() {
|
||
|
return {
|
||
|
routeDepth: 1,
|
||
|
router: Router
|
||
|
};
|
||
|
},
|
||
|
|
||
|
getInitialState: function getInitialState() {
|
||
|
return state = nextState;
|
||
|
},
|
||
|
|
||
|
componentWillReceiveProps: function componentWillReceiveProps() {
|
||
|
this.setState(state = nextState);
|
||
|
},
|
||
|
|
||
|
componentWillUnmount: function componentWillUnmount() {
|
||
|
Router.stop();
|
||
|
},
|
||
|
|
||
|
render: function render() {
|
||
|
var route = Router.getRouteAtDepth(0);
|
||
|
return route ? React.createElement(route.handler, this.props) : null;
|
||
|
}
|
||
|
|
||
|
});
|
||
|
|
||
|
Router.clearAllRoutes();
|
||
|
|
||
|
if (options.routes) Router.addRoutes(options.routes);
|
||
|
|
||
|
return Router;
|
||
|
}
|
||
|
|
||
|
module.exports = createRouter;
|