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
/* jshint -W058 */ |
|
'use strict'; |
|
|
|
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; |