mirror of https://github.com/wekan/wekan
The Open Source kanban (built with Meteor). Keep variable/table/field names camelCase. For translations, only add Pull Request changes to wekan/i18n/en.i18n.json , other translations are done at https://transifex.com/wekan/wekan only.
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.
587 lines
17 KiB
587 lines
17 KiB
Router = function () {
|
|
var self = this;
|
|
this.globals = [];
|
|
this.subscriptions = Function.prototype;
|
|
|
|
this._tracker = this._buildTracker();
|
|
this._current = {};
|
|
|
|
// tracks the current path change
|
|
this._onEveryPath = new Tracker.Dependency();
|
|
|
|
this._globalRoute = new Route(this);
|
|
|
|
// holds onRoute callbacks
|
|
this._onRouteCallbacks = [];
|
|
|
|
// if _askedToWait is true. We don't automatically start the router
|
|
// in Meteor.startup callback. (see client/_init.js)
|
|
// Instead user need to call `.initialize()
|
|
this._askedToWait = false;
|
|
this._initialized = false;
|
|
this._triggersEnter = [];
|
|
this._triggersExit = [];
|
|
this._routes = [];
|
|
this._routesMap = {};
|
|
this._updateCallbacks();
|
|
this.notFound = this.notfound = null;
|
|
// indicate it's okay (or not okay) to run the tracker
|
|
// when doing subscriptions
|
|
// using a number and increment it help us to support FlowRouter.go()
|
|
// and legitimate reruns inside tracker on the same event loop.
|
|
// this is a solution for #145
|
|
this.safeToRun = 0;
|
|
|
|
// Meteor exposes to the client the path prefix that was defined using the
|
|
// ROOT_URL environement variable on the server using the global runtime
|
|
// configuration. See #315.
|
|
this._basePath = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || '';
|
|
|
|
// this is a chain contains a list of old routes
|
|
// most of the time, there is only one old route
|
|
// but when it's the time for a trigger redirect we've a chain
|
|
this._oldRouteChain = [];
|
|
|
|
this.env = {
|
|
replaceState: new Meteor.EnvironmentVariable(),
|
|
reload: new Meteor.EnvironmentVariable(),
|
|
trailingSlash: new Meteor.EnvironmentVariable()
|
|
};
|
|
|
|
// redirect function used inside triggers
|
|
this._redirectFn = function(pathDef, fields, queryParams) {
|
|
if (/^http(s)?:\/\//.test(pathDef)) {
|
|
var message = "Redirects to URLs outside of the app are not supported in this version of Flow Router. Use 'window.location = yourUrl' instead";
|
|
throw new Error(message);
|
|
}
|
|
self.withReplaceState(function() {
|
|
var path = FlowRouter.path(pathDef, fields, queryParams);
|
|
self._page.redirect(path);
|
|
});
|
|
};
|
|
this._initTriggersAPI();
|
|
};
|
|
|
|
Router.prototype.route = function(pathDef, options, group) {
|
|
if (!/^\/.*/.test(pathDef)) {
|
|
var message = "route's path must start with '/'";
|
|
throw new Error(message);
|
|
}
|
|
|
|
options = options || {};
|
|
var self = this;
|
|
var route = new Route(this, pathDef, options, group);
|
|
|
|
// calls when the page route being activates
|
|
route._actionHandle = function (context, next) {
|
|
var oldRoute = self._current.route;
|
|
self._oldRouteChain.push(oldRoute);
|
|
|
|
var queryParams = self._qs.parse(context.querystring);
|
|
// _qs.parse() gives us a object without prototypes,
|
|
// created with Object.create(null)
|
|
// Meteor's check doesn't play nice with it.
|
|
// So, we need to fix it by cloning it.
|
|
// see more: https://github.com/meteorhacks/flow-router/issues/164
|
|
queryParams = JSON.parse(JSON.stringify(queryParams));
|
|
|
|
self._current = {
|
|
path: context.path,
|
|
context: context,
|
|
params: context.params,
|
|
queryParams: queryParams,
|
|
route: route,
|
|
oldRoute: oldRoute
|
|
};
|
|
|
|
// we need to invalidate if all the triggers have been completed
|
|
// if not that means, we've been redirected to another path
|
|
// then we don't need to invalidate
|
|
var afterAllTriggersRan = function() {
|
|
self._invalidateTracker();
|
|
};
|
|
|
|
var triggers = self._triggersEnter.concat(route._triggersEnter);
|
|
Triggers.runTriggers(
|
|
triggers,
|
|
self._current,
|
|
self._redirectFn,
|
|
afterAllTriggersRan
|
|
);
|
|
};
|
|
|
|
// calls when you exit from the page js route
|
|
route._exitHandle = function(context, next) {
|
|
var triggers = self._triggersExit.concat(route._triggersExit);
|
|
Triggers.runTriggers(
|
|
triggers,
|
|
self._current,
|
|
self._redirectFn,
|
|
next
|
|
);
|
|
};
|
|
|
|
this._routes.push(route);
|
|
if (options.name) {
|
|
this._routesMap[options.name] = route;
|
|
}
|
|
|
|
this._updateCallbacks();
|
|
this._triggerRouteRegister(route);
|
|
|
|
return route;
|
|
};
|
|
|
|
Router.prototype.group = function(options) {
|
|
return new Group(this, options);
|
|
};
|
|
|
|
Router.prototype.path = function(pathDef, fields, queryParams) {
|
|
if (this._routesMap[pathDef]) {
|
|
pathDef = this._routesMap[pathDef].pathDef;
|
|
}
|
|
|
|
var path = "";
|
|
|
|
// Prefix the path with the router global prefix
|
|
if (this._basePath) {
|
|
path += "/" + this._basePath + "/";
|
|
}
|
|
|
|
fields = fields || {};
|
|
var regExp = /(:[\w\(\)\\\+\*\.\?]+)+/g;
|
|
path += pathDef.replace(regExp, function(key) {
|
|
var firstRegexpChar = key.indexOf("(");
|
|
// get the content behind : and (\\d+/)
|
|
key = key.substring(1, (firstRegexpChar > 0)? firstRegexpChar: undefined);
|
|
// remove +?*
|
|
key = key.replace(/[\+\*\?]+/g, "");
|
|
|
|
// this is to allow page js to keep the custom characters as it is
|
|
// we need to encode 2 times otherwise "/" char does not work properly
|
|
// So, in that case, when I includes "/" it will think it's a part of the
|
|
// route. encoding 2times fixes it
|
|
return encodeURIComponent(encodeURIComponent(fields[key] || ""));
|
|
});
|
|
|
|
// Replace multiple slashes with single slash
|
|
path = path.replace(/\/\/+/g, "/");
|
|
|
|
// remove trailing slash
|
|
// but keep the root slash if it's the only one
|
|
path = path.match(/^\/{1}$/) ? path: path.replace(/\/$/, "");
|
|
|
|
// explictly asked to add a trailing slash
|
|
if(this.env.trailingSlash.get() && _.last(path) !== "/") {
|
|
path += "/";
|
|
}
|
|
|
|
var strQueryParams = this._qs.stringify(queryParams || {});
|
|
if(strQueryParams) {
|
|
path += "?" + strQueryParams;
|
|
}
|
|
|
|
return path;
|
|
};
|
|
|
|
Router.prototype.go = function(pathDef, fields, queryParams) {
|
|
var path = this.path(pathDef, fields, queryParams);
|
|
|
|
var useReplaceState = this.env.replaceState.get();
|
|
if(useReplaceState) {
|
|
this._page.replace(path);
|
|
} else {
|
|
this._page(path);
|
|
}
|
|
};
|
|
|
|
Router.prototype.reload = function() {
|
|
var self = this;
|
|
|
|
self.env.reload.withValue(true, function() {
|
|
self._page.replace(self._current.path);
|
|
});
|
|
};
|
|
|
|
Router.prototype.redirect = function(path) {
|
|
this._page.redirect(path);
|
|
};
|
|
|
|
Router.prototype.setParams = function(newParams) {
|
|
if(!this._current.route) {return false;}
|
|
|
|
var pathDef = this._current.route.pathDef;
|
|
var existingParams = this._current.params;
|
|
var params = {};
|
|
_.each(_.keys(existingParams), function(key) {
|
|
params[key] = existingParams[key];
|
|
});
|
|
|
|
params = _.extend(params, newParams);
|
|
var queryParams = this._current.queryParams;
|
|
|
|
this.go(pathDef, params, queryParams);
|
|
return true;
|
|
};
|
|
|
|
Router.prototype.setQueryParams = function(newParams) {
|
|
if(!this._current.route) {return false;}
|
|
|
|
var queryParams = _.clone(this._current.queryParams);
|
|
_.extend(queryParams, newParams);
|
|
|
|
for (var k in queryParams) {
|
|
if (queryParams[k] === null || queryParams[k] === undefined) {
|
|
delete queryParams[k];
|
|
}
|
|
}
|
|
|
|
var pathDef = this._current.route.pathDef;
|
|
var params = this._current.params;
|
|
this.go(pathDef, params, queryParams);
|
|
return true;
|
|
};
|
|
|
|
// .current is not reactive
|
|
// This is by design. use .getParam() instead
|
|
// If you really need to watch the path change, use .watchPathChange()
|
|
Router.prototype.current = function() {
|
|
// We can't trust outside, that's why we clone this
|
|
// Anyway, we can't clone the whole object since it has non-jsonable values
|
|
// That's why we clone what's really needed.
|
|
var current = _.clone(this._current);
|
|
current.queryParams = EJSON.clone(current.queryParams);
|
|
current.params = EJSON.clone(current.params);
|
|
return current;
|
|
};
|
|
|
|
// Implementing Reactive APIs
|
|
var reactiveApis = [
|
|
'getParam', 'getQueryParam',
|
|
'getRouteName', 'watchPathChange'
|
|
];
|
|
reactiveApis.forEach(function(api) {
|
|
Router.prototype[api] = function(arg1) {
|
|
// when this is calling, there may not be any route initiated
|
|
// so we need to handle it
|
|
var currentRoute = this._current.route;
|
|
if(!currentRoute) {
|
|
this._onEveryPath.depend();
|
|
return;
|
|
}
|
|
|
|
// currently, there is only one argument. If we've more let's add more args
|
|
// this is not clean code, but better in performance
|
|
return currentRoute[api].call(currentRoute, arg1);
|
|
};
|
|
});
|
|
|
|
Router.prototype.subsReady = function() {
|
|
var callback = null;
|
|
var args = _.toArray(arguments);
|
|
|
|
if (typeof _.last(args) === "function") {
|
|
callback = args.pop();
|
|
}
|
|
|
|
var currentRoute = this.current().route;
|
|
var globalRoute = this._globalRoute;
|
|
|
|
// we need to depend for every route change and
|
|
// rerun subscriptions to check the ready state
|
|
this._onEveryPath.depend();
|
|
|
|
if(!currentRoute) {
|
|
return false;
|
|
}
|
|
|
|
var subscriptions;
|
|
if(args.length === 0) {
|
|
subscriptions = _.values(globalRoute.getAllSubscriptions());
|
|
subscriptions = subscriptions.concat(_.values(currentRoute.getAllSubscriptions()));
|
|
} else {
|
|
subscriptions = _.map(args, function(subName) {
|
|
return globalRoute.getSubscription(subName) || currentRoute.getSubscription(subName);
|
|
});
|
|
}
|
|
|
|
var isReady = function() {
|
|
var ready = _.every(subscriptions, function(sub) {
|
|
return sub && sub.ready();
|
|
});
|
|
|
|
return ready;
|
|
};
|
|
|
|
if (callback) {
|
|
Tracker.autorun(function(c) {
|
|
if (isReady()) {
|
|
callback();
|
|
c.stop();
|
|
}
|
|
});
|
|
} else {
|
|
return isReady();
|
|
}
|
|
};
|
|
|
|
Router.prototype.withReplaceState = function(fn) {
|
|
return this.env.replaceState.withValue(true, fn);
|
|
};
|
|
|
|
Router.prototype.withTrailingSlash = function(fn) {
|
|
return this.env.trailingSlash.withValue(true, fn);
|
|
};
|
|
|
|
Router.prototype._notfoundRoute = function(context) {
|
|
this._current = {
|
|
path: context.path,
|
|
context: context,
|
|
params: [],
|
|
queryParams: {},
|
|
};
|
|
|
|
// XXX this.notfound kept for backwards compatibility
|
|
this.notFound = this.notFound || this.notfound;
|
|
if(!this.notFound) {
|
|
console.error("There is no route for the path:", context.path);
|
|
return;
|
|
}
|
|
|
|
this._current.route = new Route(this, "*", this.notFound);
|
|
this._invalidateTracker();
|
|
};
|
|
|
|
Router.prototype.initialize = function(options) {
|
|
options = options || {};
|
|
|
|
if(this._initialized) {
|
|
throw new Error("FlowRouter is already initialized");
|
|
}
|
|
|
|
var self = this;
|
|
this._updateCallbacks();
|
|
|
|
// Implementing idempotent routing
|
|
// by overriding page.js`s "show" method.
|
|
// Why?
|
|
// It is impossible to bypass exit triggers,
|
|
// because they execute before the handler and
|
|
// can not know what the next path is, inside exit trigger.
|
|
//
|
|
// we need override both show, replace to make this work
|
|
// since we use redirect when we are talking about withReplaceState
|
|
_.each(['show', 'replace'], function(fnName) {
|
|
var original = self._page[fnName];
|
|
self._page[fnName] = function(path, state, dispatch, push) {
|
|
var reload = self.env.reload.get();
|
|
if (!reload && self._current.path === path) {
|
|
return;
|
|
}
|
|
|
|
original.call(this, path, state, dispatch, push);
|
|
};
|
|
});
|
|
|
|
// this is very ugly part of pagejs and it does decoding few times
|
|
// in unpredicatable manner. See #168
|
|
// this is the default behaviour and we need keep it like that
|
|
// we are doing a hack. see .path()
|
|
this._page.base(this._basePath);
|
|
this._page({
|
|
decodeURLComponents: true,
|
|
hashbang: !!options.hashbang
|
|
});
|
|
|
|
this._initialized = true;
|
|
};
|
|
|
|
Router.prototype._buildTracker = function() {
|
|
var self = this;
|
|
|
|
// main autorun function
|
|
var tracker = Tracker.autorun(function () {
|
|
if(!self._current || !self._current.route) {
|
|
return;
|
|
}
|
|
|
|
// see the definition of `this._processingContexts`
|
|
var currentContext = self._current;
|
|
var route = currentContext.route;
|
|
var path = currentContext.path;
|
|
|
|
if(self.safeToRun === 0) {
|
|
var message =
|
|
"You can't use reactive data sources like Session" +
|
|
" inside the `.subscriptions` method!";
|
|
throw new Error(message);
|
|
}
|
|
|
|
// We need to run subscriptions inside a Tracker
|
|
// to stop subs when switching between routes
|
|
// But we don't need to run this tracker with
|
|
// other reactive changes inside the .subscription method
|
|
// We tackle this with the `safeToRun` variable
|
|
self._globalRoute.clearSubscriptions();
|
|
self.subscriptions.call(self._globalRoute, path);
|
|
route.callSubscriptions(currentContext);
|
|
|
|
// otherwise, computations inside action will trigger to re-run
|
|
// this computation. which we do not need.
|
|
Tracker.nonreactive(function() {
|
|
var isRouteChange = currentContext.oldRoute !== currentContext.route;
|
|
var isFirstRoute = !currentContext.oldRoute;
|
|
// first route is not a route change
|
|
if(isFirstRoute) {
|
|
isRouteChange = false;
|
|
}
|
|
|
|
// Clear oldRouteChain just before calling the action
|
|
// We still need to get a copy of the oldestRoute first
|
|
// It's very important to get the oldest route and registerRouteClose() it
|
|
// See: https://github.com/kadirahq/flow-router/issues/314
|
|
var oldestRoute = self._oldRouteChain[0];
|
|
self._oldRouteChain = [];
|
|
|
|
currentContext.route.registerRouteChange(currentContext, isRouteChange);
|
|
route.callAction(currentContext);
|
|
|
|
Tracker.afterFlush(function() {
|
|
self._onEveryPath.changed();
|
|
if(isRouteChange) {
|
|
// We need to trigger that route (definition itself) has changed.
|
|
// So, we need to re-run all the register callbacks to current route
|
|
// This is pretty important, otherwise tracker
|
|
// can't identify new route's items
|
|
|
|
// We also need to afterFlush, otherwise this will re-run
|
|
// helpers on templates which are marked for destroying
|
|
if(oldestRoute) {
|
|
oldestRoute.registerRouteClose();
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
self.safeToRun--;
|
|
});
|
|
|
|
return tracker;
|
|
};
|
|
|
|
Router.prototype._invalidateTracker = function() {
|
|
var self = this;
|
|
this.safeToRun++;
|
|
this._tracker.invalidate();
|
|
// After the invalidation we need to flush to make changes imediately
|
|
// otherwise, we have face some issues context mix-maches and so on.
|
|
// But there are some cases we can't flush. So we need to ready for that.
|
|
|
|
// we clearly know, we can't flush inside an autorun
|
|
// this may leads some issues on flow-routing
|
|
// we may need to do some warning
|
|
if(!Tracker.currentComputation) {
|
|
// Still there are some cases where we can't flush
|
|
// eg:- when there is a flush currently
|
|
// But we've no public API or hacks to get that state
|
|
// So, this is the only solution
|
|
try {
|
|
Tracker.flush();
|
|
} catch(ex) {
|
|
// only handling "while flushing" errors
|
|
if(!/Tracker\.flush while flushing/.test(ex.message)) {
|
|
return;
|
|
}
|
|
|
|
// XXX: fix this with a proper solution by removing subscription mgt.
|
|
// from the router. Then we don't need to run invalidate using a tracker
|
|
|
|
// this happens when we are trying to invoke a route change
|
|
// with inside a route chnage. (eg:- Template.onCreated)
|
|
// Since we use page.js and tracker, we don't have much control
|
|
// over this process.
|
|
// only solution is to defer route execution.
|
|
|
|
// It's possible to have more than one path want to defer
|
|
// But, we only need to pick the last one.
|
|
// self._nextPath = self._current.path;
|
|
Meteor.defer(function() {
|
|
var path = self._nextPath;
|
|
if(!path) {
|
|
return;
|
|
}
|
|
|
|
delete self._nextPath;
|
|
self.env.reload.withValue(true, function() {
|
|
self.go(path);
|
|
});
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
Router.prototype._updateCallbacks = function () {
|
|
var self = this;
|
|
|
|
self._page.callbacks = [];
|
|
self._page.exits = [];
|
|
|
|
_.each(self._routes, function(route) {
|
|
self._page(route.pathDef, route._actionHandle);
|
|
self._page.exit(route.pathDef, route._exitHandle);
|
|
});
|
|
|
|
self._page("*", function(context) {
|
|
self._notfoundRoute(context);
|
|
});
|
|
};
|
|
|
|
Router.prototype._initTriggersAPI = function() {
|
|
var self = this;
|
|
this.triggers = {
|
|
enter: function(triggers, filter) {
|
|
triggers = Triggers.applyFilters(triggers, filter);
|
|
if(triggers.length) {
|
|
self._triggersEnter = self._triggersEnter.concat(triggers);
|
|
}
|
|
},
|
|
|
|
exit: function(triggers, filter) {
|
|
triggers = Triggers.applyFilters(triggers, filter);
|
|
if(triggers.length) {
|
|
self._triggersExit = self._triggersExit.concat(triggers);
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
Router.prototype.wait = function() {
|
|
if(this._initialized) {
|
|
throw new Error("can't wait after FlowRouter has been initialized");
|
|
}
|
|
|
|
this._askedToWait = true;
|
|
};
|
|
|
|
Router.prototype.onRouteRegister = function(cb) {
|
|
this._onRouteCallbacks.push(cb);
|
|
};
|
|
|
|
Router.prototype._triggerRouteRegister = function(currentRoute) {
|
|
// We should only need to send a safe set of fields on the route
|
|
// object.
|
|
// This is not to hide what's inside the route object, but to show
|
|
// these are the public APIs
|
|
var routePublicApi = _.pick(currentRoute, 'name', 'pathDef', 'path');
|
|
var omittingOptionFields = [
|
|
'triggersEnter', 'triggersExit', 'action', 'subscriptions', 'name'
|
|
];
|
|
routePublicApi.options = _.omit(currentRoute.options, omittingOptionFields);
|
|
|
|
_.each(this._onRouteCallbacks, function(cb) {
|
|
cb(routePublicApi);
|
|
});
|
|
};
|
|
|
|
Router.prototype._page = page;
|
|
Router.prototype._qs = qs;
|
|
|