UNPKG

stateman

Version:

A tiny foundation that providing nested state-based routing for complex web application.

220 lines (174 loc) 6.52 kB
// MIT // Thx Backbone.js 1.1.2 and https://github.com/cowboy/jquery-hashchange/blob/master/jquery.ba-hashchange.js // for iframe patches in old ie. var browser = require("./browser.js"); var _ = require("./util.js"); // the mode const var QUIRK = 3, HASH = 1, HISTORY = 2; // extract History for test // resolve the conficlt with the Native History function History(options){ options = options || {}; // Trick from backbone.history for anchor-faked testcase this.location = options.location || browser.location; // mode config, you can pass absolute mode (just for test); this.html5 = options.html5; this.mode = options.html5 && browser.history ? HISTORY: HASH; if( !browser.hash ) this.mode = QUIRK; if(options.mode) this.mode = options.mode; // hash prefix , used for hash or quirk mode this.prefix = "#" + (options.prefix || "") ; this.rPrefix = new RegExp(this.prefix + '(.*)$'); this.interval = options.interval || 66; // the root regexp for remove the root for the path. used in History mode this.root = options.root || "/" ; this.rRoot = new RegExp("^" + this.root); this.autolink = options.autolink!==false; this.autofix = options.autofix!==false; this.curPath = undefined; } _.extend( _.emitable(History), { // check the start: function(callback){ var path = this.getPath(); this._checkPath = _.bind(this.checkPath, this); if( this.isStart ) return; this.isStart = true; if(this.mode === QUIRK){ this._fixHashProbelm(path); } switch ( this.mode ){ case HASH: browser.on(window, "hashchange", this._checkPath); break; case HISTORY: browser.on(window, "popstate", this._checkPath); break; case QUIRK: this._checkLoop(); } // event delegate this.autolink && this._autolink(); this.autofix && this._fixInitState(); this.curPath = path; this.emit("change", path, { firstTime: true}); }, // the history teardown stop: function(){ browser.off(window, 'hashchange', this._checkPath); browser.off(window, 'popstate', this._checkPath); clearTimeout(this.tid); this.isStart = false; this._checkPath = null; }, // get the path modify checkPath: function(/*ev*/){ var path = this.getPath(), curPath = this.curPath; //for oldIE hash history issue if(path === curPath && this.iframe){ path = this.getPath(this.iframe.location); } if( path !== curPath ) { this.iframe && this.nav(path, {silent: true}); this.curPath = path; this.emit('change', path); } }, // get the current path getPath: function(location){ location = location || this.location; var tmp; if( this.mode !== HISTORY ){ tmp = location.href.match(this.rPrefix); return _.cleanPath(tmp && tmp[1]? tmp[1]: ""); }else{ return _.cleanPath(( location.pathname + location.search || "" ).replace( this.rRoot, "/" )); } }, nav: function(to, options ){ var iframe = this.iframe; options = options || {}; to = _.cleanPath(to); if(this.curPath == to) return; // pushState wont trigger the checkPath // but hashchange will // so we need set curPath before to forbit the CheckPath this.curPath = to; // 3 or 1 is matched if( this.mode !== HISTORY ){ this._setHash(this.location, to, options.replace); if( iframe && this.getPath(iframe.location) !== to ){ if(!options.replace) iframe.document.open().close(); this._setHash(this.iframe.location, to, options.replace); } }else{ this._changeState(this.location, options.title||"", _.cleanPath( this.root + to ), options.replace ) } if( !options.silent ) this.emit('change', to); }, _autolink: function(){ if(this.mode!==HISTORY) return; // only in html5 mode, the autolink is works // if(this.mode !== 2) return; var self = this; browser.on( document.body, "click", function(ev){ var target = ev.target || ev.srcElement; if( target.tagName.toLowerCase() !== "a" ) return; var tmp = browser.isSameDomain(target.href)&&(browser.getHref(target)||"").match(self.rPrefix); var hash = tmp && tmp[1]? tmp[1]: ""; if(!hash) return; ev.preventDefault && ev.preventDefault(); self.nav( hash ); return (ev.returnValue = false); } ); }, _setHash: function(location, path, replace){ var href = location.href.replace(/(javascript:|#).*$/, ''); if (replace){ location.replace(href + this.prefix+ path); } else location.hash = this.prefix+ path; }, // for browser that not support onhashchange _checkLoop: function(){ var self = this; this.tid = setTimeout( function(){ self._checkPath(); self._checkLoop(); }, this.interval ); }, // if we use real url in hash env( browser no history popstate support) // or we use hash in html5supoort mode (when paste url in other url) // then , history should repara it _fixInitState: function(){ var pathname = _.cleanPath(this.location.pathname), hash, hashInPathName; // dont support history popstate but config the html5 mode if( this.mode !== HISTORY && this.html5){ hashInPathName = pathname.replace(this.rRoot, ""); if(hashInPathName) this.location.replace(this.root + this.prefix + _.cleanPath(hashInPathName)); }else if( this.mode === HISTORY /* && pathname === this.root*/){ hash = this.location.hash.replace(this.prefix, ""); if(hash) this._changeState( this.location, document.title, _.cleanPath(this.root + hash)); } }, // ONLY for test, forbid browser to update _changeState: function(location, title, path, replace){ var history = location.history || window.history; return history[replace? 'replaceState': 'pushState']({}, title , path) }, // Thanks for backbone.history and https://github.com/cowboy/jquery-hashchange/blob/master/jquery.ba-hashchange.js // for helping stateman fixing the oldie hash history issues when with iframe hack _fixHashProbelm: function(path){ var iframe = document.createElement('iframe'), body = document.body; iframe.src = 'javascript:;'; iframe.style.display = 'none'; iframe.tabIndex = -1; iframe.title = ""; this.iframe = body.insertBefore(iframe, body.firstChild).contentWindow; this.iframe.document.open().close(); this.iframe.location.hash = '#' + path; } }); module.exports = History;