| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587 | 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 APIsvar 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;
 |