mirror of
https://github.com/twisterarmy/twister-react.git
synced 2025-02-04 11:04:19 +00:00
514 lines
17 KiB
JavaScript
Executable File
514 lines
17 KiB
JavaScript
Executable File
/* 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; |