From 73e265d8fd050ae3daa67472b4465a5c49d68910 Mon Sep 17 00:00:00 2001 From: Lauri Ojansivu Date: Sat, 20 Apr 2019 15:18:33 +0300 Subject: Include to Wekan packages directory contents, so that meteor command would build all directly. This also simplifies build scripts. Thanks to xet7 ! --- packages/kadira-flow-router/client/_init.js | 11 + packages/kadira-flow-router/client/group.js | 57 +++ packages/kadira-flow-router/client/modules.js | 2 + packages/kadira-flow-router/client/route.js | 125 ++++++ packages/kadira-flow-router/client/router.js | 587 +++++++++++++++++++++++++ packages/kadira-flow-router/client/triggers.js | 112 +++++ 6 files changed, 894 insertions(+) create mode 100644 packages/kadira-flow-router/client/_init.js create mode 100644 packages/kadira-flow-router/client/group.js create mode 100644 packages/kadira-flow-router/client/modules.js create mode 100644 packages/kadira-flow-router/client/route.js create mode 100644 packages/kadira-flow-router/client/router.js create mode 100644 packages/kadira-flow-router/client/triggers.js (limited to 'packages/kadira-flow-router/client') diff --git a/packages/kadira-flow-router/client/_init.js b/packages/kadira-flow-router/client/_init.js new file mode 100644 index 00000000..a18fdc89 --- /dev/null +++ b/packages/kadira-flow-router/client/_init.js @@ -0,0 +1,11 @@ +// Export Router Instance +FlowRouter = new Router(); +FlowRouter.Router = Router; +FlowRouter.Route = Route; + +// Initialize FlowRouter +Meteor.startup(function () { + if(!FlowRouter._askedToWait) { + FlowRouter.initialize(); + } +}); diff --git a/packages/kadira-flow-router/client/group.js b/packages/kadira-flow-router/client/group.js new file mode 100644 index 00000000..b93296bc --- /dev/null +++ b/packages/kadira-flow-router/client/group.js @@ -0,0 +1,57 @@ +Group = function(router, options, parent) { + options = options || {}; + + if (options.prefix && !/^\/.*/.test(options.prefix)) { + var message = "group's prefix must start with '/'"; + throw new Error(message); + } + + this._router = router; + this.prefix = options.prefix || ''; + this.name = options.name; + this.options = options; + + this._triggersEnter = options.triggersEnter || []; + this._triggersExit = options.triggersExit || []; + this._subscriptions = options.subscriptions || Function.prototype; + + this.parent = parent; + if (this.parent) { + this.prefix = parent.prefix + this.prefix; + + this._triggersEnter = parent._triggersEnter.concat(this._triggersEnter); + this._triggersExit = this._triggersExit.concat(parent._triggersExit); + } +}; + +Group.prototype.route = function(pathDef, options, group) { + options = options || {}; + + if (!/^\/.*/.test(pathDef)) { + var message = "route's path must start with '/'"; + throw new Error(message); + } + + group = group || this; + pathDef = this.prefix + pathDef; + + var triggersEnter = options.triggersEnter || []; + options.triggersEnter = this._triggersEnter.concat(triggersEnter); + + var triggersExit = options.triggersExit || []; + options.triggersExit = triggersExit.concat(this._triggersExit); + + return this._router.route(pathDef, options, group); +}; + +Group.prototype.group = function(options) { + return new Group(this._router, options, this); +}; + +Group.prototype.callSubscriptions = function(current) { + if (this.parent) { + this.parent.callSubscriptions(current); + } + + this._subscriptions.call(current.route, current.params, current.queryParams); +}; diff --git a/packages/kadira-flow-router/client/modules.js b/packages/kadira-flow-router/client/modules.js new file mode 100644 index 00000000..7b734f44 --- /dev/null +++ b/packages/kadira-flow-router/client/modules.js @@ -0,0 +1,2 @@ +page = require('page'); +qs = require('qs'); diff --git a/packages/kadira-flow-router/client/route.js b/packages/kadira-flow-router/client/route.js new file mode 100644 index 00000000..b82e9721 --- /dev/null +++ b/packages/kadira-flow-router/client/route.js @@ -0,0 +1,125 @@ +Route = function(router, pathDef, options, group) { + options = options || {}; + + this.options = options; + this.pathDef = pathDef + + // Route.path is deprecated and will be removed in 3.0 + this.path = pathDef; + + if (options.name) { + this.name = options.name; + } + + this._action = options.action || Function.prototype; + this._subscriptions = options.subscriptions || Function.prototype; + this._triggersEnter = options.triggersEnter || []; + this._triggersExit = options.triggersExit || []; + this._subsMap = {}; + this._router = router; + + this._params = new ReactiveDict(); + this._queryParams = new ReactiveDict(); + this._routeCloseDep = new Tracker.Dependency(); + + // tracks the changes in the URL + this._pathChangeDep = new Tracker.Dependency(); + + this.group = group; +}; + +Route.prototype.clearSubscriptions = function() { + this._subsMap = {}; +}; + +Route.prototype.register = function(name, sub, options) { + this._subsMap[name] = sub; +}; + + +Route.prototype.getSubscription = function(name) { + return this._subsMap[name]; +}; + + +Route.prototype.getAllSubscriptions = function() { + return this._subsMap; +}; + +Route.prototype.callAction = function(current) { + var self = this; + self._action(current.params, current.queryParams); +}; + +Route.prototype.callSubscriptions = function(current) { + this.clearSubscriptions(); + if (this.group) { + this.group.callSubscriptions(current); + } + + this._subscriptions(current.params, current.queryParams); +}; + +Route.prototype.getRouteName = function() { + this._routeCloseDep.depend(); + return this.name; +}; + +Route.prototype.getParam = function(key) { + this._routeCloseDep.depend(); + return this._params.get(key); +}; + +Route.prototype.getQueryParam = function(key) { + this._routeCloseDep.depend(); + return this._queryParams.get(key); +}; + +Route.prototype.watchPathChange = function() { + this._pathChangeDep.depend(); +}; + +Route.prototype.registerRouteClose = function() { + this._params = new ReactiveDict(); + this._queryParams = new ReactiveDict(); + this._routeCloseDep.changed(); + this._pathChangeDep.changed(); +}; + +Route.prototype.registerRouteChange = function(currentContext, routeChanging) { + // register params + var params = currentContext.params; + this._updateReactiveDict(this._params, params); + + // register query params + var queryParams = currentContext.queryParams; + this._updateReactiveDict(this._queryParams, queryParams); + + // if the route is changing, we need to defer triggering path changing + // if we did this, old route's path watchers will detect this + // Real issue is, above watcher will get removed with the new route + // So, we don't need to trigger it now + // We are doing it on the route close event. So, if they exists they'll + // get notify that + if(!routeChanging) { + this._pathChangeDep.changed(); + } +}; + +Route.prototype._updateReactiveDict = function(dict, newValues) { + var currentKeys = _.keys(newValues); + var oldKeys = _.keys(dict.keyDeps); + + // set new values + // params is an array. So, _.each(params) does not works + // to iterate params + _.each(currentKeys, function(key) { + dict.set(key, newValues[key]); + }); + + // remove keys which does not exisits here + var removedKeys = _.difference(oldKeys, currentKeys); + _.each(removedKeys, function(key) { + dict.set(key, undefined); + }); +}; diff --git a/packages/kadira-flow-router/client/router.js b/packages/kadira-flow-router/client/router.js new file mode 100644 index 00000000..ae91751f --- /dev/null +++ b/packages/kadira-flow-router/client/router.js @@ -0,0 +1,587 @@ +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; diff --git a/packages/kadira-flow-router/client/triggers.js b/packages/kadira-flow-router/client/triggers.js new file mode 100644 index 00000000..7733332c --- /dev/null +++ b/packages/kadira-flow-router/client/triggers.js @@ -0,0 +1,112 @@ +// a set of utility functions for triggers + +Triggers = {}; + +// Apply filters for a set of triggers +// @triggers - a set of triggers +// @filter - filter with array fileds with `only` and `except` +// support only either `only` or `except`, but not both +Triggers.applyFilters = function(triggers, filter) { + if(!(triggers instanceof Array)) { + triggers = [triggers]; + } + + if(!filter) { + return triggers; + } + + if(filter.only && filter.except) { + throw new Error("Triggers don't support only and except filters at once"); + } + + if(filter.only && !(filter.only instanceof Array)) { + throw new Error("only filters needs to be an array"); + } + + if(filter.except && !(filter.except instanceof Array)) { + throw new Error("except filters needs to be an array"); + } + + if(filter.only) { + return Triggers.createRouteBoundTriggers(triggers, filter.only); + } + + if(filter.except) { + return Triggers.createRouteBoundTriggers(triggers, filter.except, true); + } + + throw new Error("Provided a filter but not supported"); +}; + +// create triggers by bounding them to a set of route names +// @triggers - a set of triggers +// @names - list of route names to be bound (trigger runs only for these names) +// @negate - negate the result (triggers won't run for above names) +Triggers.createRouteBoundTriggers = function(triggers, names, negate) { + var namesMap = {}; + _.each(names, function(name) { + namesMap[name] = true; + }); + + var filteredTriggers = _.map(triggers, function(originalTrigger) { + var modifiedTrigger = function(context, next) { + var routeName = context.route.name; + var matched = (namesMap[routeName])? 1: -1; + matched = (negate)? matched * -1 : matched; + + if(matched === 1) { + originalTrigger(context, next); + } + }; + return modifiedTrigger; + }); + + return filteredTriggers; +}; + +// run triggers and abort if redirected or callback stopped +// @triggers - a set of triggers +// @context - context we need to pass (it must have the route) +// @redirectFn - function which used to redirect +// @after - called after if only all the triggers runs +Triggers.runTriggers = function(triggers, context, redirectFn, after) { + var abort = false; + var inCurrentLoop = true; + var alreadyRedirected = false; + + for(var lc=0; lc