proxy-based Twister client written with react-js
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.
 

512 lines
17 KiB

/* jshint -W058 */
'use strict';
var React = require('react');
var warning = require('./warning');
var invariant = require('invariant');
var canUseDOM = require('can-use-dom');
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;