indexzero
9/26/2011 - 7:19 PM

Comments in SugarSkull

Comments in SugarSkull

(function(window, undefined) {

  var dloc = document.location;

  function regifyString(str) {
    if(~str.indexOf('*')) {
      str = str.replace(/\*/g, '([_\.\(\)!\\ %@&a-zA-Z0-9-]+)');
    }
    if(~str.indexOf(':')) {
      str = str.replace(/:.*?\/|:.*?$/g, '([a-zA-Z0-9-]+)/');
      str = str.slice(0, -1);
    }
    return str;
  }

  function regifyObject(routes) { // convert all simple param routes to regex
    if (typeof routes === 'string') {
      return false;
    }
    for (var key in routes) {
      regifyObject(routes[key]);
      if (key.indexOf(':') !== -1 || key.indexOf('*') !== -1) {
        var newKey = regifyString(key);
        routes[newKey] = routes[key];
        delete routes[key];
      }
    }
  }

  window.Router = Router;
  
  function Router(routes) {

    if(!(this instanceof Router)) return new Router(routes);

    var self = this; 

    this.routes = routes;

    var add;

    this.recurse = function(value) {
      if (value === undefined) return recurse;
      add = (this._recurse = value) === 'forward' ? 'unshift' : 'push';
    };

    this._recurse = null;
    this.recurse(null);

    this.resource = null;
    this.state = {};

    this.events = {};
    this.events.on = [];
    this.events.oneach = [];
    this.events.after = [];
    this.events.aftereach = [];
    this.events.once = [];
    this.events.notfound = null;

    this.route = null;
    this.lastroutevalue = null;
    this.alreadyrun = false;

    regifyObject(this.routes);

    function dispatch(src) {
      for (var i=0, l = self.events[src].length; i < l; i++) {

        var listener = self.events[src][i];
        var val = listener.val === null ? self.lastroutevalue : listener.val;
        
        if (typeof listener.fn === 'string') {
          listener.fn = self.resource[listener.fn];
        }
        
        if (typeof val === 'string') {
          val = [val];
        }
        
        if (listener.fn.apply(self.resource || self, val || []) === false) {
          self[src] = [];
          return false;
        }
        if (listener.val !== null) {
          self.lastroutevalue = listener.val;
        }
      
      }
    }

    function parse(routes, path, len, matched) {

      var roughmatch, exactmatch, _len;
      var route = routes[path];

      if(!route || path === '/') {
        for (var r in routes) {
          //
          // We dont have an exact match, lets explore.
          // e.g. r: `/foo/bar`,  path: `/foo/bar/buzz`
          //
          //    '/foo/bar': {
          //      '/buzz': function() {}
          //    } 
          //
          if (routes.hasOwnProperty(r)) {

            //
            // exactmatch: ['/foo/bar']
            // roughmatch: ['/foo/bar/buzz', '/buzz']
            //
            exactmatch = path.match(new RegExp('^' + r));
            roughmatch = path.match(new RegExp('^' + r + '(.*)?'));
            
            if (exactmatch) {
              //
              // Convert roughmatch to an array of names without 
              // the **leading** `/`.
              //
              // roughmatch: ['foo/bar/buzz', 'buzz']
              //
              for (var i = roughmatch.length; i >= 0; i--) {
                if (roughmatch[i]) {
                  roughmatch[i] = roughmatch[i].replace(/^\//, '');
                }
                else {
                  roughmatch.splice(i, 1);
                }
              }

              //
              // If we are at the toplevel (e.g. `/`) and there is a more
              // liberal route available then do not continue
              // evaluation of this branch of the routing table.
              //
              if (exactmatch[0] === '/' && 
                  roughmatch.length > 1 && 
                  '/([a-zA-Z0-9-]+)' in routes) {
                continue;
              }

              //
              // partsCount: 1
              // _path: buzz
              // _len: [3] - 1 = 2
              // _matched: [].concat([]) = []
              //
              var partsCount = exactmatch[0].split('/').length - 1,
                  _path = roughmatch.slice(exactmatch.length),
                  _matched = matched.concat(exactmatch.slice(1));

              _len = len - partsCount;

              if (exactmatch.length > 1) {
                //
                // If we have capture groups in the string `r`.
                // e.g. `/(\w+)\/bar/`
                //
                route = routes[r];
              }
              else {
                //
                // Otherwise it is just a plain string literal, 
                // so use the exact route. e.g. `foo/bar`
                //
                route = routes[exactmatch[0]];
              }
              
              //
              // route: { '/bazz': {...} }
              //
              if (next(_path, _len, _matched)) return true;
            }
          }
        }
      } else {
        //
        // If we hit an _exact_ match then essentially stop
        // recursing.
        //
        // _len: 2 - 1 + 1 = 0
        //
        _len = len - path.split('/').length + 1;
      }

      next('/', _len, matched);

      function next(path, len, matched) {
        if (route) {
          
          //
          // If the path is not a `string` assume it is an Array
          // and join it with the delimiter. This occurs for
          // large regexps or deep nesting structures.
          //
          if (typeof path !== 'string') {
            path = '/' + path.join('/');
          }

          //
          // _Recursive case_: If we have not yet reached the end of 
          // the `path` then recurse with the new values 
          //
          if (path !== '/') {
            parse(route, path, len, matched);
          }

          if (len === 0 || self._recurse) {

            function queue(fn, type) {
              if(fn && typeof fn !== 'string' && fn[0]) {
                for (var j = 0, m = fn.length; j < m; j++) {
                  self.events[type][add]({ fn: fn[j], val: matched || path });
                }
              }
              else {
                self.events[type][add]({ fn: fn, val: matched || path });
              }
            };

            if (typeof route === 'function' || route.on) {
              queue(route.on || route, 'on');
            }

            if (route.once && !route.once._fired){
              route.once._fired = true;
              queue(route.once, 'once');
            }

            if (route.after){
              queue(route.after, 'after');
            }

            return true;
          }
        }
        else {
          self.noroute(matched);
        }
      };
      return true;
    }

    function route(event) {

      var loc = dloc.hash.slice(1);
      var len = loc.split('/').length-1;

      self.events.after = [];

      if(parse(self.routes, loc, len, [])) {
        dispatch('on');
        dispatch('once');
        dispatch('oneach');
        self.events.on = [];
        self.events.once = [];
      }

      dispatch('after');
      dispatch('aftereach');
    }

    this.init = function(r) {

      if(dloc.hash === '' && r) { 
        dloc.hash = r; 
      }

      if(dloc.hash.length > 0) {
        route();
      }

      listener.init(route);
      return this;
    };

    this.destroy = function() {
      listener.destroy(route);
      return this;
    };

    return this;
  }

  Router.prototype.use = function(conf) {

    for(var item in conf) {
      if(conf.hasOwnProperty(item)) {

        if(item === 'recurse') {
          this.recurse(conf[item]);
          continue;
        }

        if(item === 'resource') {
          this.resource = conf[item];
          continue;
        }

        if(item === 'notfound') {
          this.events.notfound = conf[item];
          continue;
        }

        var fn = conf[item];
        var store = null;
        var type = ({}).toString.call(fn);

        if(item === 'on') { store = 'oneach'; }
        if(item === 'after') { store = 'aftereach'; }

        if(type.indexOf('Array') !== -1) {
          for (var i=0, l = fn.length; i < l; i++) {
            this.events[store].push({ fn: fn[i], val: null });
          }
        }
        else {
          this.events[store].push({ fn: fn, val: null });
        }
      }
    }
    return this;
  };

  Router.prototype.noroute = function(routename) {    
    if(this.events.notfound) {
      if(({}).toString.call(this.events.notfound).indexOf('Array') !== -1) {
        for (var i=0, l = this.events.notfound.length; i < l; i++) {
          this.events.notfound[i](routename);
        }
      }
      else {
          this.events.notfound(routename);
      }
    }    
  };

  Router.prototype.explode = function() {
    var v = dloc.hash;
    if(v[1] === '/') { v=v.slice(1); }
    return v.slice(1, v.length).split("/");
  };

  Router.prototype.setRoute = function(i, v, val) {

    var url = this.explode();

    if(typeof i === 'number' && typeof v === 'string') {
      url[i] = v;
    }
    else if(typeof val === 'string') {
      url.splice(i, v, s);
    }
    else {
      url = [i];
    }

    listener.setHash(url.join('/'));
    return url;
  };
  
  Router.prototype.getState = function() {
    return this.state;
  };
  
  Router.prototype.getRoute = function(v) {

    var ret = v;

    if(typeof v === "number") {
      ret = this.explode()[v];
    }
    else if(typeof v === "string"){
      var h = this.explode();
      ret = h.indexOf(v);
    }
    else {
      ret = this.explode();
    }
    
    return ret;
  };

  Router.prototype.on = function(route, cb) {

    var compiledRoute;

    if(this.route) {
      compiledRoute = this.route[regifyString(route)];
    }
    else {
      compiledRoute = this.route = this.routes[regifyString(route)] = {
        on: null
      };
    }

    if(compiledRoute) {
      compiledRoute.on = cb;
    }
  };

  var version = '0.4.0',
      mode = 'modern',
      listener = { 

    hash: dloc.hash,

    check:  function () {
      var h = dloc.hash;
      if (h != this.hash) {
        this.hash = h;
        this.onHashChanged();
      }
    },

    fire: function() {
      if(mode === 'modern') {
        window.onhashchange();
      }
      else {
        this.onHashChanged();
      }
    },

    init: function (fn) {

      var self = this;

      if(!window.Router.listeners) {
        window.Router.listeners = [];
      }
      
      function onchange() {
        for(var i = 0, l = window.Router.listeners.length; i < l; i++) {
          window.Router.listeners[i]();
        }
      }

      //note IE8 is being counted as 'modern' because it has the hashchange event
      if('onhashchange' in window && 
          (document.documentMode === undefined || document.documentMode > 7)) {
        window.onhashchange = onchange;
        mode = 'modern';
      }
      else { // IE support, based on a concept by Erik Arvidson ...

        var frame = document.createElement('iframe');
        frame.id = 'state-frame';
        frame.style.display = 'none';
        document.body.appendChild(frame);
        this.writeFrame('');

        if ('onpropertychange' in document && 'attachEvent' in document) {
          document.attachEvent('onpropertychange', function () {
            if (event.propertyName === 'location') {
              self.check();
            }
          });
        }

        window.setInterval(function () { self.check(); }, 50);
        
        this.onHashChanged = onchange;
        mode = 'legacy';
      }

      window.Router.listeners.push(fn);      
      
      return mode;
    },

    destroy: function (fn) {
      if (!window.Router || !window.Router.listeners) return;

      var listeners = window.Router.listeners;

      for (var i = listeners.length - 1; i >= 0; i--) {
        if (listeners[i] === fn) {
          listeners.splice(i, 1);
        }
      }
    },

    setHash: function (s) {

      // Mozilla always adds an entry to the history
      if (mode === 'legacy') {
        this.writeFrame(s);
      }
      dloc.hash = (s[0] === '/') ? s : '/' + s;
      return this;
    },

    writeFrame: function (s) { 
      // IE support...
      var f = document.getElementById('state-frame');
      var d = f.contentDocument || f.contentWindow.document;
      d.open();
      d.write("<script>_hash = '" + s + "'; onload = parent.listener.syncHash;<script>");
      d.close();
    },

    syncHash: function () { 
      // IE support...
      var s = this._hash;
      if (s != dloc.hash) {
        dloc.hash = s;
      }
      return this;
    },

    onHashChanged:  function () {}
  };
 
}(window));