stateman
Version:
A tiny foundation that providing nested state-based routing for complex web application.
1,319 lines (1,011 loc) • 34.3 kB
JavaScript
/**
@author leeluolee
@version 0.2.0
@homepage https://github.com/leeluolee/stateman
*/
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory();
else if(typeof define === 'function' && define.amd)
define([], factory);
else if(typeof exports === 'object')
exports["StateMan"] = factory();
else
root["StateMan"] = factory();
})(this, function() {
return /******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId])
/******/ return installedModules[moduleId].exports;
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ exports: {},
/******/ id: moduleId,
/******/ loaded: false
/******/ };
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/ // Flag the module as loaded
/******/ module.loaded = true;
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/ // Load entry module and return exports
/******/ return __webpack_require__(0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ function(module, exports, __webpack_require__) {
var stateman;
if( typeof window === 'object' ){
stateman = __webpack_require__(1);
stateman.History = __webpack_require__(4);
stateman.util = __webpack_require__(3);
stateman.isServer = false;
}else{
stateman = __webpack_require__(7);
stateman.isServer = true;
}
stateman.State = __webpack_require__(2);
module.exports = stateman;
/***/ },
/* 1 */
/***/ function(module, exports, __webpack_require__) {
var State = __webpack_require__(2),
History = __webpack_require__(4),
Base = __webpack_require__(6),
_ = __webpack_require__(3),
baseTitle = document.title,
stateFn = State.prototype.state;
function StateMan(options){
if(this instanceof StateMan === false){ return new StateMan(options); }
options = options || {};
Base.call(this, options);
if(options.history) this.history = options.history;
this._stashCallback = [];
this.current = this.active = this;
// auto update document.title, when navigation has been down
this.on("end", function( options ){
var cur = this.current;
document.title = cur.getTitle( options ) || baseTitle ;
});
}
var o =_.inherit( StateMan, Base.prototype );
_.extend(o , {
start: function(options, callback){
this._startCallback = callback;
if( !this.history ) this.history = new History(options);
if( !this.history.isStart ){
this.history.on("change", _.bind(this._afterPathChange, this));
this.history.start();
}
return this;
},
stop: function(){
this.history.stop();
},
// @TODO direct go the point state
go: function(state, option, callback){
option = option || {};
var statename;
if(typeof state === "string") {
statename = state;
state = this.state(state);
}
if(!state) return this._notfound({state:statename});
if(typeof option === "function"){
callback = option;
option = {};
}
if(option.encode !== false){
var url = state.encode(option.param);
option.path = url;
this.nav(url, {silent: true, replace: option.replace});
}
this._go(state, option, callback);
return this;
},
nav: function(url, options, callback){
if(typeof options === "function"){
callback = options;
options = {};
}
options = options || {};
options.path = url;
this.history.nav( url, _.extend({silent: true}, options));
if(!options.silent) this._afterPathChange( _.cleanPath(url) , options , callback);
return this;
},
// after pathchange changed
// @TODO: afterPathChange need based on decode
_afterPathChange: function(path, options ,callback){
this.emit("history:change", path);
var found = this.decode(path);
options = options || {};
options.path = path;
if(!found){
return this._notfound(options);
}
options.param = found.param;
if( options.firstTime && !callback){
callback = this._startCallback;
delete this._startCallback;
}
this._go( found.state, options, callback );
},
_notfound: function(options){
return this.emit("notfound", options);
},
// goto the state with some option
_go: function(state, option, callback){
var over;
if(state.hasNext && this.strict) return this._notfound({name: state.name});
option.param = option.param || {};
var current = this.current,
baseState = this._findBase(current, state),
prepath = this.path,
self = this;
if( typeof callback === "function" ) this._stashCallback.push(callback);
// if we done the navigating when start
function done(success){
over = true;
if( success !== false ) self.emit("end", option);
self.pending = null;
self._popStash(option);
}
option.previous = current;
option.current = state;
if(current !== state){
option.stop = function(){
done(false);
self.nav( prepath? prepath: "/", {silent:true});
};
self.emit("begin", option);
}
// if we stop it in 'begin' listener
if(over === true) return;
option.phase = 'permission';
this._walk(current, state, option, true , _.bind( function( notRejected ){
if( notRejected===false ){
// if reject in callForPermission, we will return to old
prepath && this.nav( prepath, {silent: true});
done(false, 2);
return this.emit('abort', option);
}
// stop previous pending.
if(this.pending) this.pending.stop();
this.pending = option;
this.path = option.path;
this.current = option.current;
this.param = option.param;
this.previous = option.previous;
option.phase = 'navigation';
this._walk(current, state, option, false, _.bind(function( notRejected ){
if( notRejected === false ){
this.current = this.active;
done(false);
return this.emit('abort', option);
}
this.active = option.current;
option.phase = 'completion';
return done();
}, this) );
}, this) );
},
_popStash: function(option){
var stash = this._stashCallback, len = stash.length;
this._stashCallback = [];
if(!len) return;
for(var i = 0; i < len; i++){
stash[i].call(this, option);
}
},
// the transition logic Used in Both canLeave canEnter && leave enter LifeCycle
_walk: function(from, to, option, callForPermit , callback){
// if(from === to) return callback();
// nothing -> app.state
var parent = this._findBase(from , to);
var self = this;
option.backward = true;
this._transit( from, parent, option, callForPermit , function( notRejected ){
if( notRejected === false ) return callback( notRejected );
// only actual transiton need update base state;
option.backward = false;
self._walkUpdate(self, parent, option, callForPermit, function(notRejected){
if(notRejected === false) return callback(notRejected);
self._transit( parent, to, option, callForPermit, callback);
});
});
},
_transit: function(from, to, option, callForPermit, callback){
// touch the ending
if( from === to ) return callback();
var back = from.name.length > to.name.length;
var method = back? 'leave': 'enter';
var applied;
// use canEnter to detect permission
if( callForPermit) method = 'can' + method.replace(/^\w/, function(a){ return a.toUpperCase(); });
var loop = _.bind(function( notRejected ){
// stop transition or touch the end
if( applied === to || notRejected === false ) return callback(notRejected);
if( !applied ) {
applied = back? from : this._computeNext(from, to);
}else{
applied = this._computeNext(applied, to);
}
if( (back && applied === to) || !applied )return callback( notRejected );
this._moveOn( applied, method, option, loop );
}, this);
loop();
},
_moveOn: function( applied, method, option, callback){
var isDone = false;
var isPending = false;
option.async = function(){
isPending = true;
return done;
};
function done( notRejected ){
if( isDone ) return;
isPending = false;
isDone = true;
callback( notRejected );
}
option.stop = function(){
done( false );
};
this.active = applied;
var retValue = applied[method]? applied[method]( option ): true;
if(method === 'enter') applied.visited = true;
// promise
// need breadk , if we call option.stop first;
if( _.isPromise(retValue) ){
return this._wrapPromise(retValue, done);
}
// if haven't call option.async yet
if( !isPending ) done( retValue );
},
_wrapPromise: function( promise, next ){
return promise.then( next, function(err){
//TODO: 万一promise中throw了Error如何处理?
if(err instanceof Error) throw err;
next(false);
}) ;
},
_computeNext: function( from, to ){
var fname = from.name;
var tname = to.name;
var tsplit = tname.split('.');
var fsplit = fname.split('.');
var tlen = tsplit.length;
var flen = fsplit.length;
if(fname === '') flen = 0;
if(tname === '') tlen = 0;
if( flen < tlen ){
fsplit[flen] = tsplit[flen];
}else{
fsplit.pop();
}
return this.state(fsplit.join('.'));
},
_findQuery: function(querystr){
var queries = querystr && querystr.split("&"), query= {};
if(queries){
var len = queries.length;
for(var i =0; i< len; i++){
var tmp = queries[i].split("=");
query[tmp[0]] = tmp[1];
}
}
return query;
},
_sortState: function( a, b ){
return ( b.priority || 0 ) - ( a.priority || 0 );
},
// find the same branch;
_findBase: function(now, before){
if(!now || !before || now == this || before == this) return this;
var np = now, bp = before, tmp;
while(np && bp){
tmp = bp;
while(tmp){
if(np === tmp) return tmp;
tmp = tmp.parent;
}
np = np.parent;
}
},
// check the query and Param
_walkUpdate: function(baseState, to, options, callForPermit, done){
var method = callForPermit? 'canUpdate': 'update';
var from = baseState;
var self = this;
var pathes = [], node = to;
while(node !== this){
pathes.push( node );
node = node.parent;
}
var loop = function( notRejected ){
if( notRejected === false ) return done( false );
if( !pathes.length ) return done();
from = pathes.pop();
self._moveOn( from, method, options, loop )
}
self._moveOn( from, method, options, loop )
}
}, true);
module.exports = StateMan;
/***/ },
/* 2 */
/***/ function(module, exports, __webpack_require__) {
var _ = __webpack_require__(3);
function State(option){
this._states = {};
this._pending = false;
this.visited = false;
if(option) this.config(option);
}
//regexp cache
State.rCache = {};
_.extend( _.emitable( State ), {
getTitle: function(options){
var cur = this ,title;
while( cur ){
title = cur.title;
if(title) return typeof title === 'function'? cur.title(options): cur.title
cur = cur.parent;
}
return title;
},
state: function(stateName, config){
if(_.typeOf(stateName) === "object"){
var keys = _.values(stateName, true);
keys.sort(function(ka, kb){
return _.countDot(ka) - _.countDot(kb);
});
for(var i = 0, len = keys.length; i< len ;i++){
var key = keys[i];
this.state(key, stateName[key])
}
return this;
}
var current = this, next, nextName, states = this._states, i=0;
if( typeof stateName === "string" ) stateName = stateName.split(".");
var slen = stateName.length;
var stack = [];
do{
nextName = stateName[i];
next = states[nextName];
stack.push(nextName);
if(!next){
if(!config) return;
next = states[nextName] = new State();
_.extend(next, {
parent: current,
manager: current.manager || current,
name: stack.join("."),
currentName: nextName
});
current.hasNext = true;
next.configUrl();
}
current = next;
states = next._states;
}while((++i) < slen )
if(config){
next.config(config);
return this;
} else {
return current;
}
},
config: function(configure){
configure = this._getConfig(configure);
for(var i in configure){
var prop = configure[i];
switch(i){
case "url":
if(typeof prop === "string"){
this.url = prop;
this.configUrl();
}
break;
case "events":
this.on(prop);
break;
default:
this[i] = prop;
}
}
},
// children override
_getConfig: function(configure){
return typeof configure === "function"? {enter: configure} : configure;
},
//from url
configUrl: function(){
var url = "" , base = this;
while( base ){
url = (typeof base.url === "string" ? base.url: (base.currentName || "")) + "/" + url;
// means absolute;
if(url.indexOf("^/") === 0) {
url = url.slice(1);
break;
}
base = base.parent;
}
this.pattern = _.cleanPath("/" + url);
var pathAndQuery = this.pattern.split("?");
this.pattern = pathAndQuery[0];
// some Query we need watched
_.extend(this, _.normalize(this.pattern), true);
},
encode: function(param){
var state = this;
param = param || {};
var matched = "%";
var url = state.matches.replace(/\(([\w-]+)\)/g, function(all, capture){
var sec = param[capture];
var stype = typeof sec;
if(stype === 'boolean' || stype === 'number') sec = ''+sec;
sec = sec || '';
matched+= capture + "%";
return sec;
}) + "?";
// remained is the query, we need concat them after url as query
for(var i in param) {
if( matched.indexOf("%"+i+"%") === -1) url += i + "=" + param[i] + "&";
}
return _.cleanPath( url.replace(/(?:\?|&)$/,"") );
},
decode: function( path ){
var matched = this.regexp.exec(path),
keys = this.keys;
if(matched){
var param = {};
for(var i =0,len=keys.length;i<len;i++){
param[keys[i]] = matched[i+1];
}
return param;
}else{
return false;
}
},
// by default, all lifecycle is permitted
async: function(){
throw new Error( 'please use option.async instead');
}
});
module.exports = State;
/***/ },
/* 3 */
/***/ function(module, exports) {
var _ = module.exports = {};
var slice = [].slice, o2str = ({}).toString;
// merge o2's properties to Object o1.
_.extend = function(o1, o2, override){
for(var i in o2) if(override || o1[i] === undefined){
o1[i] = o2[i];
}
return o1;
};
var rDot = /\./g;
_.countDot = function(word){
var ret = word.match(rDot)
return ret? ret.length: 0;
}
_.values = function( o, key){
var keys = [];
for(var i in o) if( o.hasOwnProperty(i) ){
keys.push( key? i: o[i] );
}
return keys;
};
_.inherit = function( cstor, o ){
function Faker(){}
Faker.prototype = o;
cstor.prototype = new Faker();
cstor.prototype.constructor = cstor;
return o;
}
_.slice = function(arr, index){
return slice.call(arr, index);
};
_.typeOf = function typeOf (o) {
return o == null ? String(o) : o2str.call(o).slice(8, -1).toLowerCase();
};
//strict eql
_.eql = function(o1, o2){
var t1 = _.typeOf(o1), t2 = _.typeOf(o2);
if( t1 !== t2) return false;
if(t1 === 'object'){
// only check the first's properties
for(var i in o1){
// Immediately return if a mismatch is found.
if( o1[i] !== o2[i] ) return false;
}
return true;
}
return o1 === o2;
};
// small emitter
_.emitable = (function(){
function norm(ev){
var eventAndNamespace = (ev||'').split(':');
return {event: eventAndNamespace[0], namespace: eventAndNamespace[1]};
}
var API = {
once: function(event, fn){
var callback = function(){
fn.apply(this, arguments);
this.off(event, callback);
};
return this.on(event, callback);
},
on: function(event, fn) {
if(typeof event === 'object'){
for (var i in event) {
this.on(i, event[i]);
}
return this;
}
var ne = norm(event);
event=ne.event;
if(event && typeof fn === 'function' ){
var handles = this._handles || (this._handles = {}),
calls = handles[event] || (handles[event] = []);
fn._ns = ne.namespace;
calls.push(fn);
}
return this;
},
off: function(event, fn) {
var ne = norm(event); event = ne.event;
if(!event || !this._handles) this._handles = {};
var handles = this._handles;
var calls = handles[event];
if (calls) {
if (!fn && !ne.namespace) {
handles[event] = [];
}else{
for (var i = 0, len = calls.length; i < len; i++) {
if ( (!fn || fn === calls[i]) && (!ne.namespace || calls[i]._ns === ne.namespace) ) {
calls.splice(i, 1);
return this;
}
}
}
}
return this;
},
emit: function(event){
var ne = norm(event); event = ne.event;
var args = _.slice(arguments, 1),
handles = this._handles, calls;
if (!handles || !(calls = handles[event])) return this;
for (var i = 0, len = calls.length; i < len; i++) {
var fn = calls[i];
if( !ne.namespace || fn._ns === ne.namespace ) fn.apply(this, args);
}
return this;
}
};
return function(obj){
obj = typeof obj == "function" ? obj.prototype : obj;
return _.extend(obj, API);
};
})();
_.bind = function(fn, context){
return function(){
return fn.apply(context, arguments);
};
};
var rDbSlash = /\/+/g, // double slash
rEndSlash = /\/$/; // end slash
_.cleanPath = function (path){
return ("/" + path).replace( rDbSlash,"/" ).replace( rEndSlash, "" ) || "/";
};
// normalize the path
function normalizePath(path) {
// means is from
// (?:\:([\w-]+))?(?:\(([^\/]+?)\))|(\*{2,})|(\*(?!\*)))/g
var preIndex = 0;
var keys = [];
var index = 0;
var matches = "";
path = _.cleanPath(path);
var regStr = path
// :id(capture)? | (capture) | ** | *
.replace(/\:([\w-]+)(?:\(([^\/]+?)\))?|(?:\(([^\/]+)\))|(\*{2,})|(\*(?!\*))/g,
function(all, key, keyformat, capture, mwild, swild, startAt) {
// move the uncaptured fragment in the path
if(startAt > preIndex) matches += path.slice(preIndex, startAt);
preIndex = startAt + all.length;
if( key ){
matches += "(" + key + ")";
keys.push(key);
return "("+( keyformat || "[\\w-]+")+")";
}
matches += "(" + index + ")";
keys.push( index++ );
if( capture ){
// sub capture detect
return "(" + capture + ")";
}
if(mwild) return "(.*)";
if(swild) return "([^\\/]*)";
});
if(preIndex !== path.length) matches += path.slice(preIndex);
return {
regexp: new RegExp("^" + regStr +"/?$"),
keys: keys,
matches: matches || path
};
}
_.log = function(msg, type){
typeof console !== "undefined" && console[type || "log"](msg); //eslint-disable-line no-console
};
_.isPromise = function( obj ){
return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function';
};
_.normalize = normalizePath;
/***/ },
/* 4 */
/***/ function(module, exports, __webpack_require__) {
// 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 = __webpack_require__(5);
var _ = __webpack_require__(3);
// 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;
/***/ },
/* 5 */
/***/ function(module, exports) {
var win = window,
doc = document;
module.exports = {
hash: "onhashchange" in win && (!doc.documentMode || doc.documentMode > 7),
history: win.history && "onpopstate" in win,
location: win.location,
isSameDomain: function(url){
var matched = url.match(/^.*?:\/\/([^/]*)/);
if(matched){
return matched[0] == this.location.origin;
}
return true;
},
getHref: function(node){
return "href" in node ? node.getAttribute("href", 2) : node.getAttribute("href");
},
on: "addEventListener" in win ? // IE10 attachEvent is not working when binding the onpopstate, so we need check addEventLister first
function(node,type,cb){return node.addEventListener( type, cb )}
: function(node,type,cb){return node.attachEvent( "on" + type, cb )},
off: "removeEventListener" in win ?
function(node,type,cb){return node.removeEventListener( type, cb )}
: function(node,type,cb){return node.detachEvent( "on" + type, cb )}
}
/***/ },
/* 6 */
/***/ function(module, exports, __webpack_require__) {
var State = __webpack_require__(2),
_ = __webpack_require__(3),
stateFn = State.prototype.state;
function BaseMan( options ){
options = options || {};
this._states = {};
this.strict = options.strict;
this.title = options.title;
if(options.routes) this.state(options.routes);
}
_.extend( _.emitable( BaseMan ), {
// keep blank
name: '',
root: true,
state: function(stateName){
var active = this.active;
var args = _.slice(arguments, 1);
if(typeof stateName === "string" && active){
stateName = stateName.replace("~", active.name);
if(active.parent) stateName = stateName.replace("^", active.parent.name || "");
}
// ^ represent current.parent
// ~ represent current
// only
args.unshift(stateName);
return stateFn.apply(this, args);
},
decode: function(path, needLocation){
var pathAndQuery = path.split("?");
var query = this._findQuery(pathAndQuery[1]);
path = pathAndQuery[0];
var found = this._findState(this, path);
if(found) _.extend(found.param, query);
return found;
},
encode: function(stateName, param, needLink){
var state = this.state(stateName);
var history = this.history;
if(!state) return;
var url = state.encode(param);
return needLink? (history.mode!==2? history.prefix + url : url ): url;
},
// notify specify state
// check the active statename whether to match the passed condition (stateName and param)
is: function(stateName, param, isStrict){
if(!stateName) return false;
stateName = (stateName.name || stateName);
var current = this.current, currentName = current.name;
var matchPath = isStrict? currentName === stateName : (currentName + ".").indexOf(stateName + ".")===0;
return matchPath && (!param || _.eql(param, this.param));
},
_wrapPromise: function( promise, next ){
return promise.then( next, function(){ next(false); }) ;
},
_findQuery: function(querystr){
var queries = querystr && querystr.split("&"), query= {};
if(queries){
var len = queries.length;
for(var i =0; i< len; i++){
var tmp = queries[i].split("=");
query[tmp[0]] = tmp[1];
}
}
return query;
},
_findState: function(state, path){
var states = state._states, found, param;
// leaf-state has the high priority upon branch-state
if(state.hasNext){
var stateList = _.values( states ).sort( this._sortState );
var len = stateList.length;
for(var i = 0; i < len; i++){
found = this._findState( stateList[i], path );
if( found ) return found;
}
}
// in strict mode only leaf can be touched
// if all children is don. will try it self
param = state.regexp && state.decode(path);
if(param){
return {
state: state,
param: param
}
}else{
return false;
}
},
_sortState: function( a, b ){
return ( b.priority || 0 ) - ( a.priority || 0 );
},
// find the same branch;
_findBase: function(now, before){
if(!now || !before || now == this || before == this) return this;
var np = now, bp = before, tmp;
while(np && bp){
tmp = bp;
while(tmp){
if(np === tmp) return tmp;
tmp = tmp.parent;
}
np = np.parent;
}
},
}, true);
module.exports = BaseMan;
/***/ },
/* 7 */
/***/ function(module, exports, __webpack_require__) {
var _ = __webpack_require__(3);
var Base = __webpack_require__(6);
function ServerManager( options ){
if(this instanceof ServerManager === false){ return new ServerManager(options); }
Base.apply( this, arguments );
}
var o =_.inherit( ServerManager, Base.prototype );
_.extend(o , {
exec: function ( path ){
var found = this.decode(path);
if( !found ) return;
var param = found.param;
//@FIXIT: We NEED decodeURIComponent in server side!!
for(var i in param){
if(typeof param[i] === 'string') param[i] = decodeURIComponent(param[i]);
}
var states = [];
var state = found.state;
this.current = state;
while(state && !state.root){
states.unshift( state );
state = state.parent;
}
return {
states: states,
param: param
}
}
})
module.exports = ServerManager
/***/ }
/******/ ])
});
;