├── .DS_Store ├── docs ├── .DS_Store └── pages │ ├── lifecycle.png │ ├── example │ ├── lifecycle.html │ ├── api.html │ └── layout.html │ ├── index.html │ ├── flat-style.css │ └── document │ ├── API-zh.md │ └── API_v0.2-zh.md ├── .npmignore ├── test ├── route.js ├── spec │ ├── dom.exports.js │ ├── dom-histery.js │ ├── test-state.js │ └── dom-stateman.js ├── runner │ ├── index.html │ └── vendor │ │ ├── util.js │ │ └── mocha.css └── fixtures │ ├── server.js │ ├── histery.html │ └── index.html ├── .travis.yml ├── .gitignore ├── src ├── index.js ├── browser.js ├── state.js ├── util.js ├── histery.js └── stateman.js ├── component.json ├── bower.json ├── License ├── scripts ├── gulp-trans.js └── release.js ├── package.json ├── example ├── lifecycle.html ├── api.html └── layout.html ├── README.md ├── gulpfile.js ├── .jshintrc └── stateman.min.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leeluolee/stateman/HEAD/.DS_Store -------------------------------------------------------------------------------- /docs/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leeluolee/stateman/HEAD/docs/.DS_Store -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | docs/ 2 | example/ 3 | scripts/ 4 | test/ 5 | coverage/ 6 | gulpfile.js 7 | -------------------------------------------------------------------------------- /docs/pages/lifecycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leeluolee/stateman/HEAD/docs/pages/lifecycle.png -------------------------------------------------------------------------------- /test/route.js: -------------------------------------------------------------------------------- 1 | var fs = require("fs"); 2 | module.exports = { 3 | "/a**": "fixtures/index.html" 4 | } -------------------------------------------------------------------------------- /test/spec/dom.exports.js: -------------------------------------------------------------------------------- 1 | 2 | require("./test-state.js"); 3 | require("./dom-histery.js"); 4 | require("./dom-stateman.js"); 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | branches: 5 | only: 6 | - master 7 | - develop 8 | before_install: 9 | - npm install -g casperjs phantomjs gulp -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .* 3 | *node_modules* 4 | *sublime* 5 | npm-debug.log 6 | docs/pages/example/ 7 | docs/pages/document/ 8 | */tmp* 9 | test/tmp/* 10 | test/coverage/* 11 | coverage* 12 | out/* 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 2 | var StateMan = require("./stateman.js"); 3 | StateMan.Histery = require("./histery.js"); 4 | StateMan.util = require("./util.js"); 5 | StateMan.State = require("./state.js"); 6 | 7 | module.exports = StateMan; 8 | -------------------------------------------------------------------------------- /component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stateman", 3 | "version": "0.2.0", 4 | "main": "src/index.js", 5 | "description": "A tiny foundation that providing nested state-based routing for complex web application.", 6 | "keywords": [ 7 | "router", 8 | "state", 9 | "spa" 10 | ], 11 | "scripts": [ 12 | "src/index.js", 13 | "src/browser.js", 14 | "src/histery.js", 15 | "src/state.js", 16 | "src/stateman.js", 17 | "src/util.js" 18 | ] 19 | } -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stateman", 3 | "version": "0.2.0", 4 | "main": "stateman.js", 5 | "description": "A tiny foundation that providing nested state-based routing for complex web application.", 6 | "keywords": [ 7 | "router", 8 | "state", 9 | "spa" 10 | ], 11 | "authors": "@leeluolee <87399126@163.com>", 12 | "license": "MIT", 13 | "ignore": [ 14 | ".*", 15 | "node_modules", 16 | "bin", 17 | "test", 18 | "docs", 19 | "gulpfile.js", 20 | "*.json", 21 | "*.md" 22 | ] 23 | } -------------------------------------------------------------------------------- /test/runner/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | StateMan Test 6 | 7 | 8 | 9 |
10 |
11 | 12 | 13 | 16 | 17 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /test/fixtures/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var express = require("express"); 6 | var http = require('http'); 7 | var fs = require('fs'); 8 | var path = require('path'); 9 | var router = express.Router(); 10 | 11 | var app = express(); 12 | 13 | app.use(express['static'](path.join(__dirname, '../..'),{})); 14 | app.use("/a",router) 15 | 16 | router.get("*", function(req,res){ 17 | res.send(fs.readFileSync("./index.html", "utf8")); 18 | }) 19 | 20 | 21 | // process.env.LOGGER_LINE = true; 22 | 23 | http.createServer(app).listen(8001, function(err) { 24 | if(err) throw err 25 | console.log("start at http://localhost:8001") 26 | }); 27 | 28 | -------------------------------------------------------------------------------- /test/fixtures/histery.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 18 | 19 | 20 | 21 | 22 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/browser.js: -------------------------------------------------------------------------------- 1 | 2 | var win = window, 3 | doc = document; 4 | 5 | var b = module.exports = { 6 | hash: "onhashchange" in win && (!doc.documentMode || doc.documentMode > 7), 7 | history: win.history && "onpopstate" in win, 8 | location: win.location, 9 | isSameDomain: function(url){ 10 | var matched = url.match(/^.*?:\/\/([^/]*)/); 11 | if(matched){ 12 | return matched[0] == this.location.origin; 13 | } 14 | return true; 15 | }, 16 | getHref: function(node){ 17 | return "href" in node ? node.getAttribute("href", 2) : node.getAttribute("href"); 18 | }, 19 | on: "addEventListener" in win ? // IE10 attachEvent is not working when binding the onpopstate, so we need check addEventLister first 20 | function(node,type,cb){return node.addEventListener( type, cb )} 21 | : function(node,type,cb){return node.attachEvent( "on" + type, cb )}, 22 | 23 | off: "removeEventListener" in win ? 24 | function(node,type,cb){return node.removeEventListener( type, cb )} 25 | : function(node,type,cb){return node.detachEvent( "on" + type, cb )} 26 | } 27 | 28 | -------------------------------------------------------------------------------- /License: -------------------------------------------------------------------------------- 1 | 2 | (The MIT License) 3 | 4 | Copyright (c) 2012-2013 NetEase, Inc. and stateman contributors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | 'Software'), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 21 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 22 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /scripts/gulp-trans.js: -------------------------------------------------------------------------------- 1 | // gulp mcss plugin 2 | var through = require('through2'); 3 | var gutil = require('gulp-util'); 4 | var path = require('path'); 5 | var fs = require('fs'); 6 | 7 | var PluginError = gutil.PluginError; 8 | 9 | var extend = function(o1, o2, override){ 10 | for(var i in o2) if(override || o1[i] === undefined){ 11 | o1[i] = o2[i] 12 | } 13 | return o1; 14 | } 15 | 16 | module.exports = function (opt) { 17 | 18 | function transform(file, enc, cb) { 19 | if (file.isNull()) return cb(null, file); 20 | if (file.isStream()) return cb(new PluginError('gulp-mcss', 'Streaming not supported')); 21 | 22 | var str = file.contents.toString('utf8'); 23 | 24 | var str_zh = str.replace(/<\!-- t -->[\s\S]*?<\!-- s -->/g, "").replace(/\{([^{}]+)\%([^{}]+)\}/g,function(all, one, two){ 25 | return two; 26 | }) 27 | var str_en = str.replace(/<\!-- s -->[\s\S]*?<\!-- \/t -->/g, "").replace(/\{([^\{}]+)\%([^\{}]+)\}/g,function(all, one, two){ 28 | return one; 29 | }) 30 | 31 | 32 | 33 | var file_zh = new gutil.File({ 34 | cwd: "", 35 | base: "", 36 | path: path.basename(file.path).replace(".md", "-zh.md"), 37 | contents: new Buffer(str_zh) 38 | }); 39 | var file_en = new gutil.File({ 40 | cwd: "", 41 | base: "", 42 | path: path.basename(file.path).replace(".md", "-en.md"), 43 | contents: new Buffer(str_en) 44 | }); 45 | 46 | this.push(file_zh); 47 | this.push(file_en); 48 | cb(null); 49 | } 50 | 51 | return through.obj(transform); 52 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stateman", 3 | "version": "0.2.0", 4 | "description": "A tiny foundation that providing nested state-based routing for complex web application.", 5 | "keywords": [ 6 | "router", 7 | "state", 8 | "spa" 9 | ], 10 | "main": "src/index.js", 11 | "repository": { 12 | "type": "git", 13 | "url": "git@github.com:leeluolee/stateman" 14 | }, 15 | "scripts": { 16 | "test": "gulp travis --phantomjs" 17 | }, 18 | "author": { 19 | "name": "leeluolee", 20 | "email": "oe.zheng@gmail.com" 21 | }, 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/leeluolee/stateman/issues" 25 | }, 26 | "homepage": "https://github.com/leeluolee/stateman", 27 | "devDependencies": { 28 | "expect.js": "^0.3.1", 29 | "gulp": "^3.8.10", 30 | "gulp-bump": "^0.1.10", 31 | "gulp-gh-pages": "^0.5.2", 32 | "gulp-git": "^1.4.0", 33 | "gulp-jshint": "~1.5.3", 34 | "gulp-mocha": "~0.4.1", 35 | "gulp-prompt": "^0.1.1", 36 | "gulp-run-sequence": "^0.3.2", 37 | "gulp-shell": "^0.2.4", 38 | "gulp-tag-version": "^1.0.2", 39 | "gulp-uglify": "~0.2.1", 40 | "gulp-util": "~2.2.14", 41 | "gulp-webpack": "^0.3.0", 42 | "karma": "~0.12.6", 43 | "karma-chrome-launcher": "~0.1.4", 44 | "karma-commonjs": "0.0.12", 45 | "karma-coverage": "~0.2.4", 46 | "karma-firefox-launcher": "~0.1.3", 47 | "karma-ie-launcher": "~0.1.5", 48 | "karma-mocha": "~0.1.3", 49 | "karma-phantomjs-launcher": "~0.1.4", 50 | "mocha": "~1.18.2", 51 | "through2": "~0.4.1", 52 | "yargs": "^1.3.1" 53 | }, 54 | "spm": { 55 | "main": "src/index.js", 56 | "ignore": [ 57 | "docs", 58 | "example", 59 | "scripts", 60 | "test" 61 | ] 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /test/runner/vendor/util.js: -------------------------------------------------------------------------------- 1 | var ao = expect.Assertion.prototype; 2 | 3 | 4 | ao.typeEqual = function(list){ 5 | if(typeof list == 'string') list = list.split(',') 6 | var types = this.obj.map(function(item){ 7 | return item.type 8 | }); 9 | this.assert( 10 | expect.eql(types, list) 11 | , function(){ return 'expected ' + list + ' to equal ' + types } 12 | , function(){ return 'expected ' + list + ' to not equal ' + types }); 13 | return this; 14 | } 15 | 16 | 17 | expect.template = (function(){ 18 | var cache = {}; 19 | return { 20 | get: function(name){ 21 | return cache[name]; 22 | }, 23 | set: function(fn){ 24 | return (cache[fn.name] = fn.toString().match(/\/\*([\s\S]*)\*\//)[1].trim()) 25 | } 26 | } 27 | })() 28 | 29 | 30 | var dispatchMockEvent = (function(){ 31 | 32 | var rMouseEvent = /^(?:click|dblclick|contextmenu|DOMMouseScroll|mouse(?:\w+))$/ 33 | var rKeyEvent = /^key(?:\w+)$/ 34 | function findEventType(type){ 35 | if(rMouseEvent.test(type)) return 'MouseEvent'; 36 | else if(rKeyEvent.test(type)) return 'KeyboardEvent'; 37 | else return 'HTMLEvents' 38 | } 39 | return function(el, type){ 40 | var EventType = findEventType(type), ev; 41 | 42 | if(document.createEvent){ // if support createEvent 43 | 44 | switch(EventType){ 45 | 46 | case 'MouseEvent': 47 | ev = document.createEvent('MouseEvent'); 48 | ev.initMouseEvent(type, true, true, null, 1, 0, 0, 0, 0, false, false, false, false, 0, null) 49 | break; 50 | 51 | case 'KeyboardEvent': 52 | ev = document.createEvent(EventType || 'MouseEvent'), 53 | initMethod = ev.initKeyboardEvent ? 'initKeyboardEvent': 'initKeyEvent'; 54 | ev[initMethod]( type, true, true, null, false, false, false, false, 9, 0 ) 55 | break; 56 | 57 | case 'HTMLEvents': 58 | ev = document.createEvent('HTMLEvents') 59 | ev.initEvent(type, true, true) 60 | } 61 | el.dispatchEvent(ev); 62 | }else{ 63 | try{ 64 | el[type]() 65 | }catch(e){ 66 | // TODO... 67 | } 68 | } 69 | } 70 | })(); 71 | 72 | -------------------------------------------------------------------------------- /example/lifecycle.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | StateMan Test 6 | 7 | 8 | 9 | 10 | 15 | 16 | 88 | 89 | -------------------------------------------------------------------------------- /docs/pages/example/lifecycle.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | StateMan Test 6 | 7 | 8 | 9 | 10 | 15 | 16 | 88 | 89 | -------------------------------------------------------------------------------- /scripts/release.js: -------------------------------------------------------------------------------- 1 | // form https://github.com/lfender6445/gulp-release-tasks/blob/master/package.json 2 | // but stateman also need release to component.json 3 | module.exports = function (gulp) { 4 | 5 | var argv = require('yargs').argv; 6 | var bump = require('gulp-bump'); 7 | var fs = require('fs'); 8 | var git = require('gulp-git'); 9 | var runSequence = require('gulp-run-sequence'); 10 | var spawn = require('child_process').spawn; 11 | var tag_version = require('gulp-tag-version'); 12 | var through = require('through2'); 13 | 14 | var branch = argv.branch || 'master'; 15 | var rootDir = require('path').resolve(argv.rootDir || './') + '/'; 16 | 17 | var commitIt = function (file, enc, cb) { 18 | if (file.isNull()) return cb(null, file); 19 | if (file.isStream()) return cb(new Error('Streaming not supported')); 20 | 21 | var commitMessage = "Bumps version to " + require(file.path).version; 22 | gulp.src('./*.json', {cwd: rootDir}).pipe(git.commit(commitMessage, {cwd: rootDir})); 23 | }; 24 | 25 | var paths = { 26 | versionsToBump: ['package.json', 'bower.json', 'manifest.json', 'component.json'].map(function (fileName) { 27 | return rootDir + fileName; 28 | }) 29 | }; 30 | 31 | // gulp.task('release', function (cb) { 32 | // runSequence('tag-and-push', 'npm-publish', 'bump', cb); 33 | // }); 34 | 35 | gulp.task('tag-and-push', function () { 36 | var pkg = require(rootDir + 'package.json'); 37 | 38 | return gulp.src('./', {cwd: rootDir}) 39 | .pipe(tag_version({version: pkg.version, cwd: rootDir})) 40 | .on('end', function () { 41 | git.push('origin', branch, {args: '--tags', cwd: rootDir}); 42 | }); 43 | }); 44 | 45 | var versioning = function () { 46 | if (argv.minor) { 47 | return 'minor'; 48 | } 49 | if (argv.major) { 50 | return 'major'; 51 | } 52 | return 'patch'; 53 | }; 54 | 55 | gulp.task('bump', function () { 56 | gulp.src(paths.versionsToBump, {cwd: rootDir}) 57 | .pipe(bump({type: versioning()})) 58 | .pipe(gulp.dest('./', {cwd: rootDir})) 59 | .pipe(through.obj(commitIt)) 60 | // .pipe(git.push('origin', branch, {cwd: rootDir})); 61 | }); 62 | 63 | gulp.task('npm-publish', function (done) { 64 | spawn('npm', ['publish', rootDir], {stdio: 'inherit'}).on('close', done); 65 | }); 66 | 67 | }; 68 | -------------------------------------------------------------------------------- /example/api.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | StateMan API test 6 | 7 | 8 | 9 | 10 |

OPEN console to find the log

11 | 12 | 21 | 22 | 73 | 74 | -------------------------------------------------------------------------------- /docs/pages/example/api.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | StateMan API test 6 | 7 | 8 | 9 | 10 |

OPEN console to find the log

11 | 12 | 21 | 22 | 73 | 74 | -------------------------------------------------------------------------------- /test/fixtures/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 19 | 20 | 21 | 22 | 32 | 33 | 34 | 35 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /example/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 25 |
26 | 27 |
28 |
29 | 35 |
36 |
37 |
38 |
39 |
40 |
41 | 42 | 43 | 58 | 59 | 60 | 61 | 136 | 137 | -------------------------------------------------------------------------------- /docs/pages/example/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 25 |
26 | 27 |
28 |
29 | 35 |
36 |
37 |
38 |
39 |
40 |
41 | 42 | 43 | 58 | 59 | 60 | 61 | 136 | 137 | -------------------------------------------------------------------------------- /src/state.js: -------------------------------------------------------------------------------- 1 | var _ = require("./util.js"); 2 | 3 | 4 | 5 | function State(option){ 6 | this._states = {}; 7 | this._pending = false; 8 | this.visited = false; 9 | if(option) this.config(option); 10 | } 11 | 12 | 13 | //regexp cache 14 | State.rCache = {}; 15 | 16 | _.extend( _.emitable( State ), { 17 | 18 | state: function(stateName, config){ 19 | if(_.typeOf(stateName) === "object"){ 20 | for(var i in stateName){ 21 | this.state(i, stateName[i]) 22 | } 23 | return this; 24 | } 25 | var current, next, nextName, states = this._states, i=0; 26 | 27 | if( typeof stateName === "string" ) stateName = stateName.split("."); 28 | 29 | var slen = stateName.length, current = this; 30 | var stack = []; 31 | 32 | 33 | do{ 34 | nextName = stateName[i]; 35 | next = states[nextName]; 36 | stack.push(nextName); 37 | if(!next){ 38 | if(!config) return; 39 | next = states[nextName] = new State(); 40 | _.extend(next, { 41 | parent: current, 42 | manager: current.manager || current, 43 | name: stack.join("."), 44 | currentName: nextName 45 | }) 46 | current.hasNext = true; 47 | next.configUrl(); 48 | } 49 | current = next; 50 | states = next._states; 51 | }while((++i) < slen ) 52 | 53 | if(config){ 54 | next.config(config); 55 | return this; 56 | } else { 57 | return current; 58 | } 59 | }, 60 | 61 | config: function(configure){ 62 | 63 | configure = this._getConfig(configure); 64 | 65 | for(var i in configure){ 66 | var prop = configure[i]; 67 | switch(i){ 68 | case "url": 69 | if(typeof prop === "string"){ 70 | this.url = prop; 71 | this.configUrl(); 72 | } 73 | break; 74 | case "events": 75 | this.on(prop) 76 | break; 77 | default: 78 | this[i] = prop; 79 | } 80 | } 81 | }, 82 | 83 | // children override 84 | _getConfig: function(configure){ 85 | return typeof configure === "function"? {enter: configure} : configure; 86 | }, 87 | 88 | //from url 89 | 90 | configUrl: function(){ 91 | var url = "" , base = this, currentUrl; 92 | var _watchedParam = []; 93 | 94 | while( base ){ 95 | 96 | url = (typeof base.url === "string" ? base.url: (base.currentName || "")) + "/" + url; 97 | 98 | // means absolute; 99 | if(url.indexOf("^/") === 0) { 100 | url = url.slice(1); 101 | break; 102 | } 103 | base = base.parent; 104 | } 105 | this.pattern = _.cleanPath("/" + url); 106 | var pathAndQuery = this.pattern.split("?"); 107 | this.pattern = pathAndQuery[0]; 108 | // some Query we need watched 109 | 110 | _.extend(this, _.normalize(this.pattern), true); 111 | }, 112 | encode: function(param){ 113 | var state = this; 114 | param = param || {}; 115 | 116 | var matched = "%"; 117 | 118 | var url = state.matches.replace(/\(([\w-]+)\)/g, function(all, capture){ 119 | var sec = param[capture] || ""; 120 | matched+= capture + "%"; 121 | return sec; 122 | }) + "?"; 123 | 124 | // remained is the query, we need concat them after url as query 125 | for(var i in param) { 126 | if( matched.indexOf("%"+i+"%") === -1) url += i + "=" + param[i] + "&"; 127 | } 128 | return _.cleanPath( url.replace(/(?:\?|&)$/,"") ) 129 | }, 130 | decode: function( path ){ 131 | var matched = this.regexp.exec(path), 132 | keys = this.keys; 133 | 134 | if(matched){ 135 | 136 | var param = {}; 137 | for(var i =0,len=keys.length;i 2 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | StateMan Reference 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 58 | 59 | 60 | 61 | 62 | 63 |
64 |
65 |

StateMan reference

66 | 74 |
75 |
76 | 77 |
78 |
79 | 80 |
81 | 86 |
87 |
88 | 89 | 156 | 157 | -------------------------------------------------------------------------------- /test/runner/vendor/mocha.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | body { 4 | margin:0; 5 | } 6 | 7 | #mocha { 8 | font: 20px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif; 9 | margin: 60px 50px; 10 | } 11 | 12 | #mocha ul, #mocha li { 13 | margin: 0; 14 | padding: 0; 15 | } 16 | 17 | #mocha ul { 18 | list-style: none; 19 | } 20 | 21 | #mocha h1, #mocha h2 { 22 | margin: 0; 23 | } 24 | 25 | #mocha h1 { 26 | margin-top: 15px; 27 | font-size: 1em; 28 | font-weight: 200; 29 | } 30 | 31 | #mocha h1 a { 32 | text-decoration: none; 33 | color: inherit; 34 | } 35 | 36 | #mocha h1 a:hover { 37 | text-decoration: underline; 38 | } 39 | 40 | #mocha .suite .suite h1 { 41 | margin-top: 0; 42 | font-size: .8em; 43 | } 44 | 45 | #mocha .hidden { 46 | display: none; 47 | } 48 | 49 | #mocha h2 { 50 | font-size: 12px; 51 | font-weight: normal; 52 | cursor: pointer; 53 | } 54 | 55 | #mocha .suite { 56 | margin-left: 15px; 57 | } 58 | 59 | #mocha .test { 60 | margin-left: 15px; 61 | overflow: hidden; 62 | } 63 | 64 | #mocha .test.pending:hover h2::after { 65 | content: '(pending)'; 66 | font-family: arial, sans-serif; 67 | } 68 | 69 | #mocha .test.pass.medium .duration { 70 | background: #C09853; 71 | } 72 | 73 | #mocha .test.pass.slow .duration { 74 | background: #B94A48; 75 | } 76 | 77 | #mocha .test.pass::before { 78 | content: '✓'; 79 | font-size: 12px; 80 | display: block; 81 | float: left; 82 | margin-right: 5px; 83 | color: #00d6b2; 84 | } 85 | 86 | #mocha .test.pass .duration { 87 | font-size: 9px; 88 | margin-left: 5px; 89 | padding: 2px 5px; 90 | color: white; 91 | -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.2); 92 | -moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.2); 93 | box-shadow: inset 0 1px 1px rgba(0,0,0,.2); 94 | -webkit-border-radius: 5px; 95 | -moz-border-radius: 5px; 96 | -ms-border-radius: 5px; 97 | -o-border-radius: 5px; 98 | border-radius: 5px; 99 | } 100 | 101 | #mocha .test.pass.fast .duration { 102 | display: none; 103 | } 104 | 105 | #mocha .test.pending { 106 | color: #0b97c4; 107 | } 108 | 109 | #mocha .test.pending::before { 110 | content: '◦'; 111 | color: #0b97c4; 112 | } 113 | 114 | #mocha .test.fail { 115 | color: #c00; 116 | } 117 | 118 | #mocha .test.fail pre { 119 | color: black; 120 | } 121 | 122 | #mocha .test.fail::before { 123 | content: '✖'; 124 | font-size: 12px; 125 | display: block; 126 | float: left; 127 | margin-right: 5px; 128 | color: #c00; 129 | } 130 | 131 | #mocha .test pre.error { 132 | color: #c00; 133 | max-height: 300px; 134 | overflow: auto; 135 | } 136 | 137 | #mocha .test pre { 138 | display: block; 139 | float: left; 140 | clear: left; 141 | font: 12px/1.5 monaco, monospace; 142 | margin: 5px; 143 | padding: 15px; 144 | border: 1px solid #eee; 145 | border-bottom-color: #ddd; 146 | -webkit-border-radius: 3px; 147 | -webkit-box-shadow: 0 1px 3px #eee; 148 | -moz-border-radius: 3px; 149 | -moz-box-shadow: 0 1px 3px #eee; 150 | } 151 | 152 | #mocha .test h2 { 153 | position: relative; 154 | } 155 | 156 | #mocha .test a.replay { 157 | position: absolute; 158 | top: 3px; 159 | right: 0; 160 | text-decoration: none; 161 | vertical-align: middle; 162 | display: block; 163 | width: 15px; 164 | height: 15px; 165 | line-height: 15px; 166 | text-align: center; 167 | background: #eee; 168 | font-size: 15px; 169 | -moz-border-radius: 15px; 170 | border-radius: 15px; 171 | -webkit-transition: opacity 200ms; 172 | -moz-transition: opacity 200ms; 173 | transition: opacity 200ms; 174 | opacity: 0.3; 175 | color: #888; 176 | } 177 | 178 | #mocha .test:hover a.replay { 179 | opacity: 1; 180 | } 181 | 182 | #mocha-report.pass .test.fail { 183 | display: none; 184 | } 185 | 186 | #mocha-report.fail .test.pass { 187 | display: none; 188 | } 189 | 190 | #mocha-report.pending .test.pass 191 | #mocha-report.pending .test.fail, { 192 | display: none; 193 | } 194 | #mocha-report.pending .test.pass.pending { 195 | display: block; 196 | } 197 | 198 | #mocha-error { 199 | color: #c00; 200 | font-size: 1.5em; 201 | font-weight: 100; 202 | letter-spacing: 1px; 203 | } 204 | 205 | #mocha-stats { 206 | position: fixed; 207 | top: 15px; 208 | right: 10px; 209 | font-size: 12px; 210 | margin: 0; 211 | color: #888; 212 | z-index: 1; 213 | } 214 | 215 | #mocha-stats .progress { 216 | float: right; 217 | padding-top: 0; 218 | } 219 | 220 | #mocha-stats em { 221 | color: black; 222 | } 223 | 224 | #mocha-stats a { 225 | text-decoration: none; 226 | color: inherit; 227 | } 228 | 229 | #mocha-stats a:hover { 230 | border-bottom: 1px solid #eee; 231 | } 232 | 233 | #mocha-stats li { 234 | display: inline-block; 235 | margin: 0 5px; 236 | list-style: none; 237 | padding-top: 11px; 238 | } 239 | 240 | #mocha-stats canvas { 241 | width: 40px; 242 | height: 40px; 243 | } 244 | 245 | #mocha code .comment { color: #ddd } 246 | #mocha code .init { color: #2F6FAD } 247 | #mocha code .string { color: #5890AD } 248 | #mocha code .keyword { color: #8A6343 } 249 | #mocha code .number { color: #2F6FAD } 250 | 251 | @media screen and (max-device-width: 480px) { 252 | #mocha { 253 | margin: 60px 0px; 254 | } 255 | 256 | #mocha #stats { 257 | position: absolute; 258 | } 259 | } -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | var _ = module.exports = {}; 2 | var slice = [].slice, o2str = ({}).toString; 3 | 4 | 5 | // merge o2's properties to Object o1. 6 | _.extend = function(o1, o2, override){ 7 | for(var i in o2) if(override || o1[i] === undefined){ 8 | o1[i] = o2[i] 9 | } 10 | return o1; 11 | } 12 | 13 | 14 | 15 | _.slice = function(arr, index){ 16 | return slice.call(arr, index); 17 | } 18 | 19 | _.typeOf = function typeOf (o) { 20 | return o == null ? String(o) : o2str.call(o).slice(8, -1).toLowerCase(); 21 | } 22 | 23 | //strict eql 24 | _.eql = function(o1, o2){ 25 | var t1 = _.typeOf(o1), t2 = _.typeOf(o2); 26 | if( t1 !== t2) return false; 27 | if(t1 === 'object'){ 28 | // only check the first's properties 29 | for(var i in o1){ 30 | // Immediately return if a mismatch is found. 31 | if( o1[i] !== o2[i] ) return false; 32 | } 33 | return true; 34 | } 35 | return o1 === o2; 36 | } 37 | 38 | 39 | // small emitter 40 | _.emitable = (function(){ 41 | function norm(ev){ 42 | var eventAndNamespace = (ev||'').split(':'); 43 | return {event: eventAndNamespace[0], namespace: eventAndNamespace[1]} 44 | } 45 | var API = { 46 | once: function(event, fn){ 47 | var callback = function(){ 48 | fn.apply(this, arguments) 49 | this.off(event, callback) 50 | } 51 | return this.on(event, callback) 52 | }, 53 | on: function(event, fn) { 54 | if(typeof event === 'object'){ 55 | for (var i in event) { 56 | this.on(i, event[i]); 57 | } 58 | return this; 59 | } 60 | var ne = norm(event); 61 | event=ne.event; 62 | if(event && typeof fn === 'function' ){ 63 | var handles = this._handles || (this._handles = {}), 64 | calls = handles[event] || (handles[event] = []); 65 | fn._ns = ne.namespace; 66 | calls.push(fn); 67 | } 68 | return this; 69 | }, 70 | off: function(event, fn) { 71 | var ne = norm(event); event = ne.event; 72 | if(!event || !this._handles) this._handles = {}; 73 | 74 | var handles = this._handles , calls; 75 | 76 | if (calls = handles[event]) { 77 | if (!fn && !ne.namespace) { 78 | handles[event] = []; 79 | }else{ 80 | for (var i = 0, len = calls.length; i < len; i++) { 81 | if ( (!fn || fn === calls[i]) && (!ne.namespace || calls[i]._ns === ne.namespace) ) { 82 | calls.splice(i, 1); 83 | return this; 84 | } 85 | } 86 | } 87 | } 88 | return this; 89 | }, 90 | emit: function(event){ 91 | var ne = norm(event); event = ne.event; 92 | 93 | var args = _.slice(arguments, 1), 94 | handles = this._handles, calls; 95 | 96 | if (!handles || !(calls = handles[event])) return this; 97 | for (var i = 0, len = calls.length; i < len; i++) { 98 | var fn = calls[i]; 99 | if( !ne.namespace || fn._ns === ne.namespace ) fn.apply(this, args) 100 | } 101 | return this; 102 | } 103 | } 104 | return function(obj){ 105 | obj = typeof obj == "function" ? obj.prototype : obj; 106 | return _.extend(obj, API) 107 | } 108 | })(); 109 | 110 | 111 | 112 | _.bind = function(fn, context){ 113 | return function(){ 114 | return fn.apply(context, arguments); 115 | } 116 | } 117 | 118 | var rDbSlash = /\/+/g, // double slash 119 | rEndSlash = /\/$/; // end slash 120 | 121 | _.cleanPath = function (path){ 122 | return ("/" + path).replace( rDbSlash,"/" ).replace( rEndSlash, "" ) || "/"; 123 | } 124 | 125 | // normalize the path 126 | function normalizePath(path) { 127 | // means is from 128 | // (?:\:([\w-]+))?(?:\(([^\/]+?)\))|(\*{2,})|(\*(?!\*)))/g 129 | var preIndex = 0; 130 | var keys = []; 131 | var index = 0; 132 | var matches = ""; 133 | 134 | path = _.cleanPath(path); 135 | 136 | var regStr = path 137 | // :id(capture)? | (capture) | ** | * 138 | .replace(/\:([\w-]+)(?:\(([^\/]+?)\))?|(?:\(([^\/]+)\))|(\*{2,})|(\*(?!\*))/g, 139 | function(all, key, keyformat, capture, mwild, swild, startAt) { 140 | // move the uncaptured fragment in the path 141 | if(startAt > preIndex) matches += path.slice(preIndex, startAt); 142 | preIndex = startAt + all.length; 143 | if( key ){ 144 | matches += "(" + key + ")"; 145 | keys.push(key) 146 | return "("+( keyformat || "[\\w-]+")+")"; 147 | } 148 | matches += "(" + index + ")"; 149 | 150 | keys.push( index++ ); 151 | 152 | if( capture ){ 153 | // sub capture detect 154 | return "(" + capture + ")"; 155 | } 156 | if(mwild) return "(.*)"; 157 | if(swild) return "([^\\/]*)"; 158 | }) 159 | 160 | if(preIndex !== path.length) matches += path.slice(preIndex) 161 | 162 | return { 163 | regexp: new RegExp("^" + regStr +"/?$"), 164 | keys: keys, 165 | matches: matches || path 166 | } 167 | } 168 | 169 | _.log = function(msg, type){ 170 | typeof console !== "undefined" && console[type || "log"](msg) 171 | } 172 | 173 | _.isPromise = function( obj ){ 174 | 175 | return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function'; 176 | 177 | } 178 | 179 | 180 | 181 | _.normalize = normalizePath; 182 | 183 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | StateMan 2 | ======= 3 | 4 | [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/leeluolee/stateman?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 5 | 6 | 7 | [![Build Status](http://img.shields.io/travis/regularjs/regular/master.svg?style=flat-square)](http://travis-ci.org/regularjs/regular) 8 | 9 | 10 | stateman: A tiny foundation that provides nested state-based routing for complex web applications. 11 | 12 | 13 | stateman is highly inspired by [ui-router](https://github.com/angular-ui/ui-router); you will find many features similar to it. 14 | 15 | But stateman is a __standalone library__ with an extremely tiny codebase (5kb minified). Feel free to integrate it with whatever framework you like! 16 | 17 | 18 | 19 | ## Reference 20 | 21 | - [English](http://leeluolee.github.io/stateman/) 22 | - [中文手册](http://leeluolee.github.io/stateman/?API-zh) 23 | 24 | 25 | ## Feature 26 | 27 | 0. nested routing support. 28 | 1. standalone with tiny codebase. 29 | 2. async routing support if you need asynchronous logic in navigating. Support Promise 30 | 3. html5 history supported, fallback to hash-based in old browser. 31 | 5. [concise API](https://github.com/leeluolee/stateman/tree/master/docs/API.md), deadly simple to getting start with it. 32 | 6. support IE6+ and other modern browsers. 33 | 7. __well tested, born in large product.__ 34 | 35 | 36 | ## Quick Start 37 | 38 | You may need a static server to run the demo. [puer](https://github.com/leeluolee/puer) is simple to get start. 39 | 40 | just paste the code to your own `index.html`, and load it up in a browser. 41 | 42 | ```html 43 | 44 | 45 | 46 | 47 | 48 | StateMan Test 49 | 50 | 51 | 52 | 53 | 61 | 62 | 90 | 91 | 92 | 93 | ``` 94 | 95 | open the console to see the output when navigating. 96 | 97 | 98 | ## Demos 99 | 100 | ###1. [Simple Layout Demo:](http://leeluolee.github.io/stateman/example/layout.html) 101 | 102 | The code in this demo is for demonstration only. In a production development, you will want a view layer to create nested views. 103 | 104 | ###2. A simple SPA built upon [Regularjs (Living Template)](https://github.com/regularjs/regular) + requirejs + stateman: [Link](http://regularjs.github.io/regular-state/requirejs/index-min.html) 105 | 106 | I create a simple wrapping ([regular-state](https://github.com/regularjs/regular-state)) to integrate stateman with Regularjs, which makes it easy to build a single Page Application. thanks to the concise API, [the code](https://github.com/regularjs/regular-state/blob/master/example/requirejs/index.js#L83) is very clean. You will find that integrating stateman with other libraries is also simple. 107 | 108 | 109 | 110 | 111 | ## Browser Support 112 | 113 | 1. Modern browsers, including mobile devices 114 | 2. IE6+ 115 | 116 | 117 | ## Installation 118 | 119 | ### Bower 120 | 121 | ```javascript 122 | bower install stateman 123 | ``` 124 | 125 | `stateman.js` have been packaged as a standard UMD, so you can use it in AMD, CommonJS and as a global. 126 | 127 | ### npm (browserify or other based on commonjs) 128 | 129 | ```js 130 | npm install stateman 131 | ``` 132 | 133 | To use: 134 | 135 | ```js 136 | var StateMan = require('stateman'); 137 | ``` 138 | 139 | ### [spm](http://spmjs.io/package/stateman) 140 | 141 | ```js 142 | spm install stateman 143 | ``` 144 | 145 | To use: 146 | 147 | ```js 148 | var StateMan = require('stateman'); 149 | ``` 150 | 151 | ### Component 152 | 153 | ```js 154 | component install leeluolee/stateman 155 | ``` 156 | 157 | To use: 158 | 159 | ```js 160 | var StateMan = require('leeluolee/stateman'); 161 | ``` 162 | 163 | 164 | 165 | ### Direct downloads 166 | 167 | 1. [stateman.js](https://rawgit.com/leeluolee/stateman/master/stateman.js) 168 | 2. [stateman.min.js](https://rawgit.com/leeluolee/stateman/master/stateman.min.js) 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | ## Examples 177 | 178 | Some basic examples can be found in [the examples directory](https://github.com/leeluolee/stateman/tree/master/example). 179 | 180 | __run demo local__ 181 | 182 | 1. clone this repo 183 | 2. `npm install gulp -g && npm install` 184 | 3. `gulp server` 185 | 4. check the example folder 186 | 187 | 188 | 189 | ## LICENSE 190 | 191 | MIT. 192 | 193 | 194 | ## ChangLog 195 | 196 | 197 | 198 | 199 | 200 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var through = require('through2'); 2 | var deploy = require("gulp-gh-pages"); 3 | var shell = require("gulp-shell"); 4 | var gulp = require('gulp'); 5 | var webpack = require('gulp-webpack'); 6 | var jshint = require('gulp-jshint'); 7 | var mocha = require('gulp-mocha'); 8 | var uglify = require('gulp-uglify'); 9 | var _ = require("./src/util.js"); 10 | var karma = require("karma").server; 11 | var translate = require("./scripts/gulp-trans.js") 12 | 13 | 14 | var pkg = require("./package.json"); 15 | 16 | // release 17 | require("./scripts/release.js")(gulp); 18 | 19 | 20 | var wpConfig = { 21 | output: { 22 | filename: "stateman.js", 23 | library: "StateMan", 24 | libraryTarget: "umd" 25 | } 26 | } 27 | 28 | var testConfig = { 29 | output: { 30 | filename: "dom.bundle.js" 31 | } 32 | } 33 | 34 | var karmaCommonConf = { 35 | browsers: ['Chrome', 'Firefox', 'IE', 'IE9', 'IE8', 'IE7', 'PhantomJS'], 36 | frameworks: ['mocha', 'commonjs'], 37 | files: [ 38 | 'test/runner/vendor/expect.js', 39 | 'src/**/*.js', 40 | 'test/spec/test-*.js', 41 | 'test/spec/dom-*.js' 42 | ], 43 | client: { 44 | mocha: {ui: 'bdd'} 45 | }, 46 | customLaunchers: { 47 | IE9: { 48 | base: 'IE', 49 | 'x-ua-compatible': 'IE=EmulateIE9' 50 | }, 51 | IE8: { 52 | base: 'IE', 53 | 'x-ua-compatible': 'IE=EmulateIE8' 54 | }, 55 | IE7: { 56 | base: 'IE', 57 | 'x-ua-compatible': 'IE=EmulateIE7' 58 | } 59 | }, 60 | 61 | preprocessors: { 62 | 'src/**/*.js': ['commonjs', 'coverage'], 63 | 'test/spec/test-*.js': ['commonjs'], 64 | 'test/spec/dom-*.js': ['commonjs'], 65 | 'test/runner/vendor/expect.js': ['commonjs'] 66 | }, 67 | 68 | // coverage reporter generates the coverage 69 | reporters: ['progress', 'coverage'], 70 | 71 | // preprocessors: { 72 | // // source files, that you wanna generate coverage for 73 | // // do not include tests or libraries 74 | // // (these files will be instrumented by Istanbul) 75 | // // 'test/regular.js': ['coverage'] 76 | // }, 77 | 78 | // optionally, configure the reporter 79 | coverageReporter: { 80 | type: 'html' 81 | } 82 | 83 | }; 84 | 85 | 86 | 87 | 88 | 89 | gulp.task('jshint', function(){ 90 | // jshint 91 | gulp.src(['src/**/*.js']) 92 | .pipe(jshint()) 93 | .pipe(jshint.reporter('default')) 94 | 95 | }) 96 | 97 | 98 | gulp.task('build', ['jshint'], function() { 99 | gulp.src("src/index.js") 100 | .pipe(webpack(wpConfig)) 101 | .pipe(wrap(signatrue)) 102 | .pipe(gulp.dest('./')) 103 | .pipe(wrap(mini)) 104 | .pipe(uglify()) 105 | .pipe(gulp.dest('./')) 106 | .on("error", function(err){ 107 | throw err 108 | }) 109 | }); 110 | 111 | gulp.task('testbundle', function(){ 112 | gulp.src("test/spec/dom.exports.js") 113 | .pipe(webpack(testConfig)) 114 | .pipe(gulp.dest('test/runner')) 115 | .on("error", function(err){ 116 | throw err 117 | }) 118 | }) 119 | 120 | gulp.task('mocha', function(){ 121 | 122 | return gulp.src(['test/spec/test-*.js']) 123 | 124 | .pipe(mocha({reporter: 'spec' }) ) 125 | 126 | .on('error', function(err){ 127 | console.log(err) 128 | console.log('\u0007'); 129 | }) 130 | .on('end', function(){ 131 | // before_mocha.clean(); 132 | }); 133 | }) 134 | 135 | 136 | gulp.task('watch', ["build", 'testbundle'], function(){ 137 | gulp.watch(['src/**/*.js'], ['build']); 138 | gulp.watch(['docs/src/*.md'], ['doc']); 139 | 140 | gulp.watch(['test/spec/*.js', 'src/**/*.js'], ['testbundle']) 141 | }) 142 | 143 | 144 | gulp.task('default', [ 'watch']); 145 | 146 | 147 | gulp.task('mocha', function() { 148 | 149 | return gulp.src(['test/spec/test-*.js', 'test/spec/node-*.js' ]) 150 | .pipe(mocha({reporter: 'spec' }) ) 151 | .on('error', function(){ 152 | // gutil.log.apply(this, arguments); 153 | console.log('\u0007'); 154 | }) 155 | .on('end', function(){ 156 | global.expect = null; 157 | }); 158 | }); 159 | 160 | 161 | gulp.task('karma', function (done) { 162 | var config = _.extend({}, karmaCommonConf); 163 | if(process.argv[3] === '--phantomjs'){ 164 | config.browsers=["PhantomJS"] 165 | config.coverageReporter = {type : 'text-summary'} 166 | 167 | karma.start(_.extend(config, {singleRun: true}), done); 168 | 169 | }else if(process.argv[3] === '--browser'){ 170 | config.browsers = null; 171 | karma.start(_.extend(config, {singleRun: true}), done); 172 | }else{ 173 | karma.start(_.extend(config, {singleRun: true}), done); 174 | } 175 | }); 176 | 177 | 178 | gulp.task("test", ["mocha", "karma"]) 179 | 180 | gulp.task('doc', function(){ 181 | return gulp.src(["docs/src/API*.md"]) 182 | .pipe(translate({})) 183 | .pipe(gulp.dest("docs/pages/document")) 184 | }) 185 | 186 | // 187 | gulp.task("release", ["tag"]) 188 | 189 | 190 | gulp.task('travis', ['jshint' ,'build','mocha', 'karma']); 191 | 192 | 193 | 194 | gulp.task('server', ['build'], shell.task([ 195 | "./node_modules/puer/bin/puer" 196 | ])) 197 | 198 | 199 | gulp.task('example', function(){ 200 | gulp.src("example/*.html") 201 | .pipe( 202 | gulp.dest('docs/pages/example') 203 | ); 204 | gulp.src("./stateman.js") 205 | .pipe( 206 | gulp.dest('docs/pages') 207 | ); 208 | }) 209 | gulp.task('gh-pages', ['example', 'doc'], function () { 210 | gulp.src("docs/pages/**/*.*") 211 | .pipe(deploy({ 212 | remoteUrl: "git@github.com:leeluolee/stateman", 213 | branch: "gh-pages" 214 | })) 215 | .on("error", function(err){ 216 | console.log(err) 217 | }) 218 | }); 219 | 220 | 221 | 222 | 223 | function wrap(fn){ 224 | return through.obj(fn); 225 | } 226 | 227 | function signatrue(file, enc, cb){ 228 | var sign = '/**\n'+ '@author\t'+ pkg.author.name + '\n'+ '@version\t'+ pkg.version + 229 | '\n'+ '@homepage\t'+ pkg.homepage + '\n*/\n'; 230 | file.contents = Buffer.concat([new Buffer(sign), file.contents]); 231 | cb(null, file); 232 | } 233 | 234 | function mini(file, enc, cb){ 235 | file.path = file.path.replace('.js', '.min.js'); 236 | cb(null, file) 237 | } 238 | -------------------------------------------------------------------------------- /test/spec/test-state.js: -------------------------------------------------------------------------------- 1 | var State = require("../../src/state.js"); 2 | var expect = require("../runner/vendor/expect.js") 3 | 4 | 5 | 6 | function expectUrl(url, option){ 7 | return expect(new State({url: url}).encode(option)) 8 | } 9 | 10 | function expectMatch(url, path){ 11 | return expect(new State({url: url}).decode(path)) 12 | } 13 | 14 | describe("State", function(){ 15 | 16 | describe("state.state", function(){ 17 | it("can defined nested statename that parentState is not defined", function(){ 18 | var state = new State(); 19 | 20 | state.state('contact.detail.message', {}); 21 | 22 | expect(state.state("contact").name).to.equal("contact") 23 | expect(state.state("contact.detail").name).to.equal("contact.detail") 24 | expect(state.state("contact.detail.message").name).to.equal("contact.detail.message") 25 | }) 26 | it("we can define absolute url when start with '^'", function(){ 27 | var state = new State(); 28 | state.state('contact.detail', {}); 29 | state.state('contact.detail.app', {url: '^/home/code/:id'}); 30 | expect(state.state("contact.detail.app").encode({id: 1})).to.equal("/home/code/1") 31 | 32 | }) 33 | it("state.async has been removed after v0.2.0", function(){ 34 | var state = new State(); 35 | expect(function(){ 36 | state.async() 37 | }).to.throwError(); 38 | 39 | }) 40 | 41 | }) 42 | 43 | 44 | describe("state.encode", function(){ 45 | 46 | 47 | it("no param and query should work", function(){ 48 | 49 | expectUrl("/home/code").to.equal("/home/code") 50 | 51 | expectUrl("/home/code", {name: 'hello', age: 1} ) 52 | .to.equal("/home/code?name=hello&age=1"); 53 | 54 | }) 55 | it("with uncatched param should work", function(){ 56 | 57 | expectUrl("/home/code/:id").to.equal("/home/code") 58 | 59 | expectUrl("/home/code/:id", { 60 | id: 100, name: 'hello', age: 1 61 | }).to.equal("/home/code/100?name=hello&age=1"); 62 | 63 | }) 64 | 65 | it("with unnamed param should work", function(){ 66 | 67 | expectUrl("/home/code/(\\d+)", { 68 | name: 'hello', age: 1, 0:100 69 | }).to.equal("/home/code/100?name=hello&age=1"); 70 | }) 71 | 72 | it("with named and catched param should work", function(){ 73 | 74 | expectUrl("/home/code/:id(\\d+)", { 75 | name: 'hello', 76 | age: 1, 77 | id: 100 78 | }).to.equal("/home/code/100?name=hello&age=1"); 79 | 80 | }) 81 | 82 | it("with wildcard should work", function(){ 83 | 84 | expectUrl("/home/**/code", { 85 | name: 'hello', age: 1, 0: "/name/100" 86 | }).to.equal("/home/name/100/code?name=hello&age=1"); 87 | 88 | expectUrl("/home/*/code", { 89 | name: 'hello', age: 1, 0: "name" 90 | }).to.equal("/home/name/code?name=hello&age=1"); 91 | 92 | }) 93 | 94 | it("complex testing should work as expect", function(){ 95 | 96 | expectUrl("/home/code/:id(\\d+)/:name/prefix(1|2|3)suffix/**", { 97 | name: 'leeluolee', age: 1 ,id: 100, 0: 1, 1: "last" 98 | }).to.equal("/home/code/100/leeluolee/prefix1suffix/last?age=1"); 99 | 100 | }) 101 | 102 | it("nested state testing", function(){ 103 | var state = new State({url: "home"}) 104 | .state("home", {}) 105 | .state("home.list", {url: ""}) 106 | .state("home.list.message", {url: "/:id/message"}) 107 | 108 | var url =state.state("home.list.message").encode({ 109 | id: 1000 ,name:1, age: "ten" 110 | }) 111 | expect(url).to.equal("/home/home/1000/message?name=1&age=ten"); 112 | }) 113 | 114 | 115 | }) 116 | 117 | 118 | describe("state.match", function(){ 119 | 120 | it("basic usage", function(){ 121 | expectMatch("/home/code", "/home/code/").to.eql({}); 122 | expectMatch("/home/code", "/home/code").to.eql({}); 123 | }) 124 | 125 | it("simple named param", function(){ 126 | expectMatch("/home/code/:id", "/home/code/100/").to.eql({id:"100"}); 127 | }) 128 | 129 | it("simple catched param", function(){ 130 | expectMatch("/home/code/(\\d+)", "/home/code/100/").to.eql({0:"100"}); 131 | }) 132 | 133 | it("simple catched and named param", function(){ 134 | expectMatch("/home/code/:id(\\d+)", "/home/code/100/").to.eql({id:"100"}); 135 | }) 136 | 137 | it("simple wild param", function(){ 138 | expectMatch("/home/code/:id(\\d+)", "/home/code/100/").to.eql({id:"100"}); 139 | }) 140 | 141 | it("complex composite param", function(){ 142 | 143 | expectMatch("/home/code/:id(\\d+)/([0-9])/(\\d{1,3})/home-:name/*/level", 144 | "/home/code/100/1/44/home-hello/wild/level").to .eql({id:"100", "0": 1, "1": 44, "2": "wild", name: "hello"}); 145 | 146 | }) 147 | 148 | }) 149 | 150 | }) 151 | 152 | 153 | describe("state.event", function(){ 154 | var state = new State(); 155 | it("event base", function(){ 156 | var locals = {on:0}; 157 | function callback(num){locals.on+=num||1} 158 | 159 | state.on("change", callback); 160 | state.emit("change", 2); 161 | expect(locals.on).to.equal(2); 162 | state.off("change", callback); 163 | state.emit("change"); 164 | expect(locals.on).to.equal(2); 165 | }) 166 | it("event once", function(){ 167 | var locals = {once:0}; 168 | function callback(num){locals.once+=num||1} 169 | 170 | state.once("once", callback); 171 | state.emit("once") 172 | expect(locals.once).to.equal(1); 173 | state.emit("once") 174 | expect(locals.once).to.equal(1); 175 | }) 176 | it("batch operate", function(){ 177 | var locals = {on:0}; 178 | function callback(name1,name2){locals.on+=name2||1} 179 | 180 | state.on({ 181 | "change": callback, 182 | "change2": callback 183 | }) 184 | 185 | state.emit("change", 1,2); 186 | expect(locals.on).to.equal(2); 187 | state.emit("change2"); 188 | expect(locals.on).to.equal(3); 189 | 190 | state.off(); 191 | 192 | state.emit("change"); 193 | expect(locals.on).to.equal(3); 194 | state.emit("change2"); 195 | expect(locals.on).to.equal(3); 196 | }) 197 | }) 198 | 199 | -------------------------------------------------------------------------------- /src/histery.js: -------------------------------------------------------------------------------- 1 | 2 | // MIT 3 | // Thx Backbone.js 1.1.2 and https://github.com/cowboy/jquery-hashchange/blob/master/jquery.ba-hashchange.js 4 | // for iframe patches in old ie. 5 | 6 | var browser = require("./browser.js"); 7 | var _ = require("./util.js"); 8 | 9 | 10 | // the mode const 11 | var QUIRK = 3, 12 | HASH = 1, 13 | HISTORY = 2; 14 | 15 | 16 | 17 | // extract History for test 18 | // resolve the conficlt with the Native History 19 | function Histery(options){ 20 | options = options || {}; 21 | 22 | // Trick from backbone.history for anchor-faked testcase 23 | this.location = options.location || browser.location; 24 | 25 | // mode config, you can pass absolute mode (just for test); 26 | this.html5 = options.html5; 27 | this.mode = options.html5 && browser.history ? HISTORY: HASH; 28 | if( !browser.hash ) this.mode = QUIRK; 29 | if(options.mode) this.mode = options.mode; 30 | 31 | // hash prefix , used for hash or quirk mode 32 | this.prefix = "#" + (options.prefix || "") ; 33 | this.rPrefix = new RegExp(this.prefix + '(.*)$'); 34 | this.interval = options.interval || 66; 35 | 36 | // the root regexp for remove the root for the path. used in History mode 37 | this.root = options.root || "/" ; 38 | this.rRoot = new RegExp("^" + this.root); 39 | 40 | this._fixInitState(); 41 | 42 | this.autolink = options.autolink!==false; 43 | 44 | this.curPath = undefined; 45 | } 46 | 47 | _.extend( _.emitable(Histery), { 48 | // check the 49 | start: function(){ 50 | var path = this.getPath(); 51 | this._checkPath = _.bind(this.checkPath, this); 52 | 53 | if( this.isStart ) return; 54 | this.isStart = true; 55 | 56 | if(this.mode === QUIRK){ 57 | this._fixHashProbelm(path); 58 | } 59 | 60 | switch ( this.mode ){ 61 | case HASH: 62 | browser.on(window, "hashchange", this._checkPath); 63 | break; 64 | case HISTORY: 65 | browser.on(window, "popstate", this._checkPath); 66 | break; 67 | case QUIRK: 68 | this._checkLoop(); 69 | } 70 | // event delegate 71 | this.autolink && this._autolink(); 72 | 73 | this.curPath = path; 74 | 75 | this.emit("change", path); 76 | }, 77 | // the history teardown 78 | stop: function(){ 79 | 80 | browser.off(window, 'hashchange', this._checkPath) 81 | browser.off(window, 'popstate', this._checkPath) 82 | clearTimeout(this.tid); 83 | this.isStart = false; 84 | this._checkPath = null; 85 | }, 86 | // get the path modify 87 | checkPath: function(ev){ 88 | 89 | var path = this.getPath(), curPath = this.curPath; 90 | 91 | //for oldIE hash history issue 92 | if(path === curPath && this.iframe){ 93 | path = this.getPath(this.iframe.location); 94 | } 95 | 96 | if( path !== curPath ) { 97 | this.iframe && this.nav(path, {silent: true}); 98 | this.curPath = path; 99 | this.emit('change', path); 100 | } 101 | }, 102 | // get the current path 103 | getPath: function(location){ 104 | var location = location || this.location, tmp; 105 | if( this.mode !== HISTORY ){ 106 | tmp = location.href.match(this.rPrefix); 107 | return tmp && tmp[1]? tmp[1]: ""; 108 | 109 | }else{ 110 | return _.cleanPath(( location.pathname + location.search || "" ).replace( this.rRoot, "/" )) 111 | } 112 | }, 113 | 114 | nav: function(to, options ){ 115 | 116 | var iframe = this.iframe; 117 | 118 | options = options || {}; 119 | 120 | to = _.cleanPath(to); 121 | 122 | if(this.curPath == to) return; 123 | 124 | // pushState wont trigger the checkPath 125 | // but hashchange will 126 | // so we need set curPath before to forbit the CheckPath 127 | this.curPath = to; 128 | 129 | // 3 or 1 is matched 130 | if( this.mode !== HISTORY ){ 131 | this._setHash(this.location, to, options.replace) 132 | if( iframe && this.getPath(iframe.location) !== to ){ 133 | if(!options.replace) iframe.document.open().close(); 134 | this._setHash(this.iframe.location, to, options.replace) 135 | } 136 | }else{ 137 | history[options.replace? 'replaceState': 'pushState']( {}, options.title || "" , _.cleanPath( this.root + to ) ) 138 | } 139 | 140 | if( !options.silent ) this.emit('change', to); 141 | }, 142 | _autolink: function(){ 143 | if(this.mode!==HISTORY) return; 144 | // only in html5 mode, the autolink is works 145 | // if(this.mode !== 2) return; 146 | var prefix = this.prefix, self = this; 147 | browser.on( document.body, "click", function(ev){ 148 | 149 | var target = ev.target || ev.srcElement; 150 | if( target.tagName.toLowerCase() !== "a" ) return; 151 | var tmp = browser.isSameDomain(target.href)&&(browser.getHref(target)||"").match(self.rPrefix); 152 | 153 | var hash = tmp && tmp[1]? tmp[1]: ""; 154 | 155 | if(!hash) return; 156 | 157 | ev.preventDefault && ev.preventDefault(); 158 | self.nav( hash ) 159 | return (ev.returnValue = false); 160 | } ) 161 | }, 162 | _setHash: function(location, path, replace){ 163 | var href = location.href.replace(/(javascript:|#).*$/, ''); 164 | if (replace){ 165 | location.replace(href + this.prefix+ path); 166 | } 167 | else location.hash = this.prefix+ path; 168 | }, 169 | // for browser that not support onhashchange 170 | _checkLoop: function(){ 171 | var self = this; 172 | this.tid = setTimeout( function(){ 173 | self._checkPath(); 174 | self._checkLoop(); 175 | }, this.interval ); 176 | }, 177 | // if we use real url in hash env( browser no history popstate support) 178 | // or we use hash in html5supoort mode (when paste url in other url) 179 | // then , histery should repara it 180 | _fixInitState: function(){ 181 | var pathname = _.cleanPath(this.location.pathname), hash, hashInPathName; 182 | 183 | // dont support history popstate but config the html5 mode 184 | if( this.mode !== HISTORY && this.html5){ 185 | 186 | hashInPathName = pathname.replace(this.rRoot, "") 187 | if(hashInPathName) this.location.replace(this.root + this.prefix + hashInPathName); 188 | 189 | }else if( this.mode === HISTORY /* && pathname === this.root*/){ 190 | 191 | hash = this.location.hash.replace(this.prefix, ""); 192 | if(hash) history.replaceState({}, document.title, _.cleanPath(this.root + hash)) 193 | 194 | } 195 | }, 196 | // Thanks for backbone.history and https://github.com/cowboy/jquery-hashchange/blob/master/jquery.ba-hashchange.js 197 | // for helping stateman fixing the oldie hash history issues when with iframe hack 198 | _fixHashProbelm: function(path){ 199 | var iframe = document.createElement('iframe'), body = document.body; 200 | iframe.src = 'javascript:;'; 201 | iframe.style.display = 'none'; 202 | iframe.tabIndex = -1; 203 | iframe.title = ""; 204 | this.iframe = body.insertBefore(iframe, body.firstChild).contentWindow; 205 | this.iframe.document.open().close(); 206 | this.iframe.location.hash = '#' + path; 207 | } 208 | 209 | }) 210 | 211 | 212 | 213 | 214 | 215 | module.exports = Histery; 216 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // -------------------------------------------------------------------- 3 | // JSHint Configuration, Strict Edition 4 | // -------------------------------------------------------------------- 5 | // 6 | // This is a options template for [JSHint][1], using [JSHint example][2] 7 | // and [Ory Band's example][3] as basis and setting config values to 8 | // be most strict: 9 | // 10 | // * set all enforcing options to true 11 | // * set all relaxing options to false 12 | // * set all environment options to false, except the browser value 13 | // * set all JSLint legacy options to false 14 | // 15 | // [1]: http://www.jshint.com/ 16 | // [2]: https://github.com/jshint/node-jshint/blob/master/example/config.json 17 | // [3]: https://github.com/oryband/dotfiles/blob/master/jshintrc 18 | // 19 | // @author http://michael.haschke.biz/ 20 | // @license http://unlicense.org/ 21 | 22 | // == Enforcing Options =============================================== 23 | // 24 | // These options tell JSHint to be more strict towards your code. Use 25 | // them if you want to allow only a safe subset of JavaScript, very 26 | // useful when your codebase is shared with a big number of developers 27 | // with different skill levels. 28 | 29 | "bitwise" : false, // Prohibit bitwise operators (&, |, ^, etc.). 30 | "curly" : false, // Require {} for every new block or scope. 31 | "eqeqeq" : false, // Require triple equals i.e. `===`. 32 | "forin" : false, // Tolerate `for in` loops without `hasOwnPrototype`. 33 | "immed" : true, // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );` 34 | "latedef" : false, // Prohibit variable use before definition. 35 | "newcap" : true, // Require capitalization of all constructor functions e.g. `new F()`. 36 | "noarg" : true, // Prohibit use of `arguments.caller` and `arguments.callee`. 37 | "noempty" : true, // Prohibit use of empty blocks. 38 | "nonew" : true, // Prohibit use of constructors for side-effects. 39 | "plusplus" : false, // Prohibit use of `++` & `--`. 40 | "regexp" : true, // Prohibit `.` and `[^...]` in regular expressions. 41 | "undef" : true, // Require all non-global variables be declared before they are used. 42 | "strict" : false, // Require `use strict` pragma in every file. 43 | "trailing" : true, // Prohibit trailing whitespaces. 44 | 45 | // == Relaxing Options ================================================ 46 | // 47 | // These options allow you to suppress certain types of warnings. Use 48 | // them only if you are absolutely positive that you know what you are 49 | // doing. 50 | 51 | "asi" : true, // Tolerate Automatic Semicolon Insertion (no semicolons). 52 | "boss" : true, // Tolerate assignments inside if, for & while. Usually conditions & loops are for comparison, not assignments. 53 | "debug" : false, // Allow debugger statements e.g. browser breakpoints. 54 | "eqnull" : true, // Tolerate use of `== null`. 55 | "es5" : false, // Allow EcmaScript 5 syntax. 56 | "esnext" : false, // Allow ES.next specific features such as `const` and `let`. 57 | "evil" : true, // Tolerate use of `eval`. 58 | "expr" : true, // Tolerate `ExpressionStatement` as Programs. 59 | "funcscope" : false, // Tolerate declarations of variables inside of control structures while accessing them later from the outside. 60 | "globalstrict" : true, // Allow global "use strict" (also enables 'strict'). 61 | "iterator" : false, // Allow usage of __iterator__ property. 62 | "lastsemic" : false, // Tolerat missing semicolons when the it is omitted for the last statement in a one-line block. 63 | "laxbreak" : false, // Tolerate unsafe line breaks e.g. `return [\n] x` without semicolons. 64 | "laxcomma" : false, // Suppress warnings about comma-first coding style. 65 | "loopfunc" : false, // Allow functions to be defined within loops. 66 | "multistr" : false, // Tolerate multi-line strings. 67 | "onecase" : false, // Tolerate switches with just one case. 68 | "proto" : false, // Tolerate __proto__ property. This property is deprecated. 69 | "regexdash" : false, // Tolerate unescaped last dash i.e. `[-...]`. 70 | "scripturl" : true, // Tolerate script-targeted URLs. 71 | "smarttabs" : false, // Tolerate mixed tabs and spaces when the latter are used for alignmnent only. 72 | "shadow" : true, // Allows re-define variables later in code e.g. `var x=1; x=2;`. 73 | "sub" : false, // Tolerate all forms of subscript notation besides dot notation e.g. `dict['key']` instead of `dict.key`. 74 | "supernew" : true, // Tolerate `new function () { ... };` and `new Object;`. 75 | "validthis" : false, // Tolerate strict violations when the code is running in strict mode and you use this in a non-constructor function. 76 | "unused" : false, // Errors on unused variables 77 | 78 | // == Environments ==================================================== 79 | // 80 | // These options pre-define global variables that are exposed by 81 | // popular JavaScript libraries and runtime environments—such as 82 | // browser or node.js. 83 | 84 | "browser" : true, // Standard browser globals e.g. `window`, `document`. 85 | "couch" : false, // Enable globals exposed by CouchDB. 86 | "devel" : false, // Allow development statements e.g. `console.log();`. 87 | "dojo" : false, // Enable globals exposed by Dojo Toolkit. 88 | "jquery" : false, // Enable globals exposed by jQuery JavaScript library. 89 | "mootools" : false, // Enable globals exposed by MooTools JavaScript framework. 90 | "node" : true, // Enable globals available when code is running inside of the NodeJS runtime environment. 91 | "nonstandard" : false, // Define non-standard but widely adopted globals such as escape and unescape. 92 | "prototypejs" : false, // Enable globals exposed by Prototype JavaScript framework. 93 | "rhino" : false, // Enable globals available when your code is running inside of the Rhino runtime environment. 94 | "wsh" : false, // Enable globals available when your code is running as a script for the Windows Script Host. 95 | 96 | // == JSLint Legacy =================================================== 97 | // 98 | // These options are legacy from JSLint. Aside from bug fixes they will 99 | // not be improved in any way and might be removed at any point. 100 | 101 | "nomen" : false, // Prohibit use of initial or trailing underbars in names. 102 | "onevar" : false, // Allow only one `var` statement per function. 103 | "passfail" : false, // Stop on first error. 104 | "white" : false, // Check against strict whitespace and indentation rules. 105 | 106 | // == Undocumented Options ============================================ 107 | // 108 | // While I've found these options in [example1][2] and [example2][3] 109 | // they are not described in the [JSHint Options documentation][4]. 110 | // 111 | // [4]: http://www.jshint.com/options/ 112 | 113 | "maxerr" : 100, // Maximum errors before stopping. 114 | "predef" : [ // Extra globals. 115 | //"exampleVar", 116 | //"anotherCoolGlobal", 117 | //"iLoveDouglas" 118 | ], 119 | "indent" : false // Specify indentation spacing 120 | } 121 | -------------------------------------------------------------------------------- /stateman.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.StateMan=e():t.StateMan=e()}(this,function(){return function(t){function e(i){if(n[i])return n[i].exports;var r=n[i]={exports:{},id:i,loaded:!1};return t[i].call(r.exports,r,r.exports,e),r.loaded=!0,r.exports}var n={};return e.m=t,e.c=n,e.p="",e(0)}([function(t,e,n){var i=n(1);i.Histery=n(4),i.util=n(3),i.State=n(2),t.exports=i},function(t,e,n){function i(t){return this instanceof i==!1?new i(t):(t=t||{},this._states={},this._stashCallback=[],this.strict=t.strict,this.current=this.active=this,this.title=t.title,void this.on("end",function(){for(var t,e=this.current;e&&!(t=e.title);)e=e.parent;document.title="function"==typeof t?e.title():String(t||o)}))}var r=n(2),a=n(4),s=(n(5),n(3)),o=document.title,h=r.prototype.state;s.extend(s.emitable(i),{name:"",state:function(t,e){var n=this.active;return"string"==typeof t&&n&&(t=t.replace("~",n.name),n.parent&&(t=t.replace("^",n.parent.name||""))),h.apply(this,arguments)},start:function(t){return this.history||(this.history=new a(t)),this.history.isStart||(this.history.on("change",s.bind(this._afterPathChange,this)),this.history.start()),this},stop:function(){this.history.stop()},go:function(t,e,n){if(e=e||{},"string"==typeof t&&(t=this.state(t)),t){if("function"==typeof e&&(n=e,e={}),e.encode!==!1){var i=t.encode(e.param);e.path=i,this.nav(i,{silent:!0,replace:e.replace})}return this._go(t,e,n),this}},nav:function(t,e,n){return"function"==typeof e&&(n=e,e={}),e=e||{},e.path=t,this.history.nav(t,s.extend({silent:!0},e)),e.silent||this._afterPathChange(s.cleanPath(t),e,n),this},decode:function(t){var e=t.split("?"),n=this._findQuery(e[1]);t=e[0];var i=this._findState(this,t);return i&&s.extend(i.param,n),i},encode:function(t,e){var n=this.state(t);return n?n.encode(e):""},is:function(t,e,n){if(!t)return!1;var t=t.name||t,i=this.current,r=i.name,a=n?r===t:0===(r+".").indexOf(t+".");return a&&(!e||s.eql(e,this.param))},_afterPathChange:function(t,e,n){this.emit("history:change",t);var i=this.decode(t);return e=e||{},e.path=t,i?(e.param=i.param,void this._go(i,e,n)):this._notfound(e)},_notfound:function(t){return this.emit("notfound",t)},_go:function(t,e,n){function i(t){r=!0,t!==!1&&c.emit("end"),c.pending=null,c._popStash(e)}var r;if(t.hasNext&&this.strict)return this._notfound({name:t.name});e.param=e.param||{};var a=this.current,o=this._findBase(a,t),h=this.path,c=this;"function"==typeof n&&this._stashCallback.push(n),e.previous=a,e.current=t,a!==t&&(e.stop=function(){i(!1),c.nav(h?h:"/",{silent:!0})},c.emit("begin",e)),r!==!0&&(a!==t?(e.phase="permission",this._walk(a,t,e,!0,s.bind(function(n){return n===!1?(h&&this.nav(h,{silent:!0}),i(!1,2),this.emit("abort",e)):(this.pending&&this.pending.stop(),this.pending=e,this.path=e.path,this.current=e.current,this.param=e.param,this.previous=e.previous,e.phase="navigation",void this._walk(a,t,e,!1,s.bind(function(t){return t===!1?(this.current=this.active,i(!1),this.emit("abort",e)):(this.active=e.current,e.phase="completion",i())},this)))},this))):(c._checkQueryAndParam(o,e),this.pending=null,i()))},_popStash:function(t){var e=this._stashCallback,n=e.length;if(this._stashCallback=[],n)for(var i=0;n>i;i++)e[i].call(this,t)},_walk:function(t,e,n,i,r){var a=this._findBase(t,e);n.basckward=!0,this._transit(t,a,n,i,s.bind(function(t){return t===!1?r(t):(i||this._checkQueryAndParam(a,n),n.basckward=!1,void this._transit(a,e,n,i,r))},this))},_transit:function(t,e,n,i,r){if(t===e)return r();var a,o=t.name.length>e.name.length,h=o?"leave":"enter";i&&(h="can"+h.replace(/^\w/,function(t){return t.toUpperCase()}));var c=s.bind(function(i){return a===e||i===!1?r(i):(a=a?this._computeNext(a,e):o?t:this._computeNext(t,e),o&&a===e||!a?r(i):void this._moveOn(a,h,n,c))},this);c()},_moveOn:function(t,e,n,i){function r(t){a||(o=!1,a=!0,i(t))}var a=!1,o=!1;n.async=function(){return o=!0,r},n.stop=function(){r(!1)},this.active=t;var h=t[e]?t[e](n):!0;return"enter"===e&&(t.visited=!0),s.isPromise(h)?this._wrapPromise(h,r):void(o||r(h))},_wrapPromise:function(t,e){return t.then(e,function(){e(!1)})},_computeNext:function(t,e){var n=t.name,i=e.name,r=i.split("."),a=n.split("."),s=r.length,o=a.length;return""===n&&(o=0),""===i&&(s=0),s>o?a[o]=r[o]:a.pop(),this.state(a.join("."))},_findQuery:function(t){var e=t&&t.split("&"),n={};if(e)for(var i=e.length,n={},r=0;i>r;r++){var a=e[r].split("=");n[a[0]]=a[1]}return n},_findState:function(t,e){var n,i,r=t._states;if(t.hasNext)for(var a in r)if(r.hasOwnProperty(a)&&(n=this._findState(r[a],e)))return n;return i=t.regexp&&t.decode(e),i?(t.param=i,t):!1},_findBase:function(t,e){if(!t||!e||t==this||e==this)return this;for(var n,i=t,r=e;i&&r;){for(n=r;n;){if(i===n)return n;n=n.parent}i=i.parent}},_checkQueryAndParam:function(t,e){for(var n=t;n!==this;)n.update&&n.update(e),n=n.parent}},!0),t.exports=i},function(t,e,n){function i(t){this._states={},this._pending=!1,this.visited=!1,t&&this.config(t)}var r=n(3);i.rCache={},r.extend(r.emitable(i),{state:function(t,e){if("object"===r.typeOf(t)){for(var n in t)this.state(n,t[n]);return this}var a,s,o,h=this._states,n=0;"string"==typeof t&&(t=t.split("."));var c=t.length,a=this,u=[];do{if(o=t[n],s=h[o],u.push(o),!s){if(!e)return;s=h[o]=new i,r.extend(s,{parent:a,manager:a.manager||a,name:u.join("."),currentName:o}),a.hasNext=!0,s.configUrl()}a=s,h=s._states}while(++nr;r++)i[n[r]]=e[r+1];return i}return!1},async:function(){throw new Error("please use option.async instead")}}),t.exports=i},function(t,e){function n(t){var e=0,n=[],r=0,a="";t=i.cleanPath(t);var s=t.replace(/\:([\w-]+)(?:\(([^\/]+?)\))?|(?:\(([^\/]+)\))|(\*{2,})|(\*(?!\*))/g,function(i,s,o,h,c,u,f){return f>e&&(a+=t.slice(e,f)),e=f+i.length,s?(a+="("+s+")",n.push(s),"("+(o||"[\\w-]+")+")"):(a+="("+r+")",n.push(r++),h?"("+h+")":c?"(.*)":u?"([^\\/]*)":void 0)});return e!==t.length&&(a+=t.slice(e)),{regexp:new RegExp("^"+s+"/?$"),keys:n,matches:a||t}}var i=t.exports={},r=[].slice,a={}.toString;i.extend=function(t,e,n){for(var i in e)(n||void 0===t[i])&&(t[i]=e[i]);return t},i.slice=function(t,e){return r.call(t,e)},i.typeOf=function(t){return null==t?String(t):a.call(t).slice(8,-1).toLowerCase()},i.eql=function(t,e){var n=i.typeOf(t),r=i.typeOf(e);if(n!==r)return!1;if("object"===n){var a=!0;for(var s in t)t[s]!==e[s]&&(a=!1);return a}return t===e},i.emitable=function(){function t(t){var e=(t||"").split(":");return{event:e[0],namespace:e[1]}}var e={once:function(t,e){var n=function(){e.apply(this,arguments),this.off(t,n)};return this.on(t,n)},on:function(e,n){if("object"==typeof e){for(var i in e)this.on(i,e[i]);return this}var r=t(e);if(e=r.event,e&&"function"==typeof n){var a=this._handles||(this._handles={}),s=a[e]||(a[e]=[]);n._ns=r.namespace,s.push(n)}return this},off:function(e,n){var i=t(e);e=i.event,e&&this._handles||(this._handles={});var r,a=this._handles;if(r=a[e])if(n||i.namespace){for(var s=0,o=r.length;o>s;s++)if(!(n&&n!==r[s]||i.namespace&&r[s]._ns!==i.namespace))return r.splice(s,1),this}else a[e]=[];return this},emit:function(e){var n=t(e);e=n.event;var r,a=i.slice(arguments,1),s=this._handles;if(!s||!(r=s[e]))return this;for(var o=0,h=r.length;h>o;o++){var c=r[o];n.namespace&&c._ns!==n.namespace||c.apply(this,a)}return this}};return function(t){return t="function"==typeof t?t.prototype:t,i.extend(t,e)}}(),i.bind=function(t,e){return function(){return t.apply(e,arguments)}};var s=/\/+/g,o=/\/$/;i.cleanPath=function(t){return("/"+t).replace(s,"/").replace(o,"")||"/"},i.log=function(t,e){"undefined"!=typeof console&&console[e||"log"](t)},i.isPromise=function(t){return!!t&&("object"==typeof t||"function"==typeof t)&&"function"==typeof t.then},i.normalize=n},function(t,e,n){function i(t){t=t||{},this.location=t.location||r.location,this.html5=t.html5,this.mode=t.html5&&r.history?h:o,r.hash||(this.mode=s),t.mode&&(this.mode=t.mode),this.prefix="#"+(t.prefix||""),this.rPrefix=new RegExp(this.prefix+"(.*)$"),this.interval=t.interval||66,this.root=t.root||"/",this.rRoot=new RegExp("^"+this.root),this._fixInitState(),this.autolink=t.autolink!==!1,this.curPath=void 0}var r=n(5),a=n(3),s=3,o=1,h=2;a.extend(a.emitable(i),{start:function(){var t=this.getPath();if(this._checkPath=a.bind(this.checkPath,this),!this.isStart){switch(this.isStart=!0,this.mode===s&&this._fixHashProbelm(t),this.mode){case o:r.on(window,"hashchange",this._checkPath);break;case h:r.on(window,"popstate",this._checkPath);break;case s:this._checkLoop()}this.autolink&&this._autolink(),this.curPath=t,this.emit("change",t)}},stop:function(){r.off(window,"hashchange",this._checkPath),r.off(window,"popstate",this._checkPath),clearTimeout(this.tid),this.isStart=!1,this._checkPath=null},checkPath:function(t){var e=this.getPath(),n=this.curPath;e===n&&this.iframe&&(e=this.getPath(this.iframe.location)),e!==n&&(this.iframe&&this.nav(e,{silent:!0}),this.curPath=e,this.emit("change",e))},getPath:function(t){var e,t=t||this.location;return this.mode!==h?(e=t.href.match(this.rPrefix),e&&e[1]?e[1]:""):a.cleanPath((t.pathname+t.search||"").replace(this.rRoot,"/"))},nav:function(t,e){var n=this.iframe;e=e||{},t=a.cleanPath(t),this.curPath!=t&&(this.curPath=t,this.mode!==h?(this._setHash(this.location,t,e.replace),n&&this.getPath(n.location)!==t&&(e.replace||n.document.open().close(),this._setHash(this.iframe.location,t,e.replace))):history[e.replace?"replaceState":"pushState"]({},e.title||"",a.cleanPath(this.root+t)),e.silent||this.emit("change",t))},_autolink:function(){if(this.mode===h){var t=(this.prefix,this);r.on(document.body,"click",function(e){var n=e.target||e.srcElement;if("a"===n.tagName.toLowerCase()){var i=(r.getHref(n)||"").match(t.rPrefix),a=i&&i[1]?i[1]:"";if(a)return e.preventDefault&&e.preventDefault(),t.nav(a),e.returnValue=!1}})}},_setHash:function(t,e,n){var i=t.href.replace(/(javascript:|#).*$/,"");n?t.replace(i+this.prefix+e):t.hash=this.prefix+e},_checkLoop:function(){var t=this;this.tid=setTimeout(function(){t._checkPath(),t._checkLoop()},this.interval)},_fixInitState:function(){var t,e,n=a.cleanPath(this.location.pathname);this.mode!==h&&this.html5?(e=n.replace(this.rRoot,""),e&&this.location.replace(this.root+this.prefix+e)):this.mode===h&&(t=this.location.hash.replace(this.prefix,""),t&&history.replaceState({},document.title,a.cleanPath(this.root+t)))},_fixHashProbelm:function(t){var e=document.createElement("iframe"),n=document.body;e.src="javascript:;",e.style.display="none",e.tabIndex=-1,e.title="",this.iframe=n.insertBefore(e,n.firstChild).contentWindow,this.iframe.document.open().close(),this.iframe.location.hash="#"+t}}),t.exports=i},function(t,e){var n=window,i=document;t.exports={hash:"onhashchange"in n&&(!i.documentMode||i.documentMode>7),history:n.history&&"onpopstate"in n,location:n.location,getHref:function(t){return"href"in t?t.getAttribute("href",2):t.getAttribute("href")},on:"addEventListener"in n?function(t,e,n){return t.addEventListener(e,n)}:function(t,e,n){return t.attachEvent("on"+e,n)},off:"removeEventListener"in n?function(t,e,n){return t.removeEventListener(e,n)}:function(t,e,n){return t.detachEvent("on"+e,n)}}}])}); -------------------------------------------------------------------------------- /src/stateman.js: -------------------------------------------------------------------------------- 1 | var State = require("./state.js"), 2 | Histery = require("./histery.js"), 3 | brow = require("./browser.js"), 4 | _ = require("./util.js"), 5 | baseTitle = document.title, 6 | stateFn = State.prototype.state; 7 | 8 | 9 | function StateMan(options){ 10 | 11 | if(this instanceof StateMan === false){ return new StateMan(options)} 12 | options = options || {}; 13 | // if(options.history) this.history = options.history; 14 | 15 | this._states = {}; 16 | this._stashCallback = []; 17 | this.strict = options.strict; 18 | this.current = this.active = this; 19 | this.title = options.title; 20 | this.on("end", function(){ 21 | var cur = this.current,title; 22 | while( cur ){ 23 | title = cur.title; 24 | if(title) break; 25 | cur = cur.parent; 26 | } 27 | document.title = typeof title === "function"? cur.title(): String( title || baseTitle ) ; 28 | }) 29 | 30 | } 31 | 32 | 33 | _.extend( _.emitable( StateMan ), { 34 | // keep blank 35 | name: '', 36 | 37 | state: function(stateName, config){ 38 | 39 | var active = this.active; 40 | if(typeof stateName === "string" && active){ 41 | stateName = stateName.replace("~", active.name) 42 | if(active.parent) stateName = stateName.replace("^", active.parent.name || ""); 43 | } 44 | // ^ represent current.parent 45 | // ~ represent current 46 | // only 47 | return stateFn.apply(this, arguments); 48 | 49 | }, 50 | start: function(options){ 51 | 52 | if( !this.history ) this.history = new Histery(options); 53 | if( !this.history.isStart ){ 54 | this.history.on("change", _.bind(this._afterPathChange, this)); 55 | this.history.start(); 56 | } 57 | return this; 58 | 59 | }, 60 | stop: function(){ 61 | this.history.stop(); 62 | }, 63 | // @TODO direct go the point state 64 | go: function(state, option, callback){ 65 | option = option || {}; 66 | var statename; 67 | if(typeof state === "string") { 68 | statename = state; 69 | state = this.state(state); 70 | } 71 | 72 | if(!state) return this._notfound({state:statename}); 73 | 74 | if(typeof option === "function"){ 75 | callback = option; 76 | option = {}; 77 | } 78 | 79 | if(option.encode !== false){ 80 | var url = state.encode(option.param) 81 | option.path = url; 82 | this.nav(url, {silent: true, replace: option.replace}); 83 | } 84 | 85 | this._go(state, option, callback); 86 | 87 | return this; 88 | }, 89 | nav: function(url, options, callback){ 90 | if(typeof options === "function"){ 91 | callback = options; 92 | options = {}; 93 | } 94 | options = options || {}; 95 | 96 | options.path = url; 97 | 98 | this.history.nav( url, _.extend({silent: true}, options)); 99 | if(!options.silent) this._afterPathChange( _.cleanPath(url) , options , callback) 100 | 101 | return this; 102 | }, 103 | decode: function(path){ 104 | 105 | var pathAndQuery = path.split("?"); 106 | var query = this._findQuery(pathAndQuery[1]); 107 | path = pathAndQuery[0]; 108 | var state = this._findState(this, path); 109 | if(state) _.extend(state.param, query); 110 | return state; 111 | 112 | }, 113 | encode: function(stateName, param){ 114 | var state = this.state(stateName); 115 | return state? state.encode(param) : ''; 116 | }, 117 | // notify specify state 118 | // check the active statename whether to match the passed condition (stateName and param) 119 | is: function(stateName, param, isStrict){ 120 | if(!stateName) return false; 121 | var stateName = (stateName.name || stateName); 122 | var current = this.current, currentName = current.name; 123 | var matchPath = isStrict? currentName === stateName : (currentName + ".").indexOf(stateName + ".")===0; 124 | return matchPath && (!param || _.eql(param, this.param)); 125 | }, 126 | // after pathchange changed 127 | // @TODO: afterPathChange need based on decode 128 | _afterPathChange: function(path, options ,callback){ 129 | 130 | this.emit("history:change", path); 131 | 132 | var found = this.decode(path); 133 | 134 | options = options || {}; 135 | 136 | options.path = path; 137 | 138 | if(!found){ 139 | // loc.nav("$default", {silent: true}) 140 | return this._notfound(options); 141 | } 142 | 143 | options.param = found.param; 144 | 145 | this._go( found, options, callback ); 146 | }, 147 | _notfound: function(options){ 148 | 149 | // var $notfound = this.state("$notfound"); 150 | 151 | // if( $notfound ) this._go($notfound, options); 152 | 153 | return this.emit("notfound", options); 154 | }, 155 | // goto the state with some option 156 | _go: function(state, option, callback){ 157 | 158 | var over; 159 | 160 | // if(typeof state === "string") state = this.state(state); 161 | 162 | // if(!state) return _.log("destination is not defined") 163 | 164 | if(state.hasNext && this.strict) return this._notfound({name: state.name}); 165 | 166 | // not touch the end in previous transtion 167 | 168 | // if( this.pending ){ 169 | // var pendingCurrent = this.pending.current; 170 | // this.pending.stop(); 171 | // _.log("naving to [" + pendingCurrent.name + "] will be stoped, trying to ["+state.name+"] now"); 172 | // } 173 | // if(this.active !== this.current){ 174 | // // we need return 175 | // _.log("naving to [" + this.current.name + "] will be stoped, trying to ["+state.name+"] now"); 176 | // this.current = this.active; 177 | // // back to before 178 | // } 179 | option.param = option.param || {}; 180 | 181 | var current = this.current, 182 | baseState = this._findBase(current, state), 183 | prepath = this.path, 184 | self = this; 185 | 186 | 187 | if( typeof callback === "function" ) this._stashCallback.push(callback); 188 | // if we done the navigating when start 189 | function done(success){ 190 | over = true; 191 | if( success !== false ) self.emit("end"); 192 | self.pending = null; 193 | self._popStash(option); 194 | } 195 | 196 | option.previous = current; 197 | option.current = state; 198 | 199 | if(current !== state){ 200 | option.stop = function(){ 201 | done(false); 202 | self.nav( prepath? prepath: "/", {silent:true}); 203 | } 204 | self.emit("begin", option); 205 | 206 | } 207 | // if we stop it in 'begin' listener 208 | if(over === true) return; 209 | 210 | if(current !== state){ 211 | // option as transition object. 212 | 213 | option.phase = 'permission'; 214 | this._walk(current, state, option, true , _.bind( function( notRejected ){ 215 | 216 | if( notRejected===false ){ 217 | // if reject in callForPermission, we will return to old 218 | prepath && this.nav( prepath, {silent: true}) 219 | 220 | done(false, 2) 221 | 222 | return this.emit('abort', option); 223 | 224 | } 225 | 226 | // stop previous pending. 227 | if(this.pending) this.pending.stop() 228 | this.pending = option; 229 | this.path = option.path; 230 | this.current = option.current; 231 | this.param = option.param; 232 | this.previous = option.previous; 233 | option.phase = 'navigation'; 234 | this._walk(current, state, option, false, _.bind(function( notRejected ){ 235 | 236 | if( notRejected === false ){ 237 | this.current = this.active; 238 | done(false) 239 | return this.emit('abort', option); 240 | } 241 | 242 | 243 | this.active = option.current; 244 | 245 | option.phase = 'completion'; 246 | return done() 247 | 248 | }, this) ) 249 | 250 | }, this) ) 251 | 252 | }else{ 253 | self._checkQueryAndParam(baseState, option); 254 | this.pending = null; 255 | done(); 256 | } 257 | 258 | }, 259 | _popStash: function(option){ 260 | 261 | var stash = this._stashCallback, len = stash.length; 262 | 263 | this._stashCallback = []; 264 | 265 | if(!len) return; 266 | 267 | for(var i = 0; i < len; i++){ 268 | stash[i].call(this, option) 269 | } 270 | }, 271 | 272 | // the transition logic Used in Both canLeave canEnter && leave enter LifeCycle 273 | 274 | _walk: function(from, to, option, callForPermit , callback){ 275 | 276 | // nothing -> app.state 277 | var parent = this._findBase(from , to); 278 | 279 | 280 | option.basckward = true; 281 | this._transit( from, parent, option, callForPermit , _.bind( function( notRejected ){ 282 | 283 | if( notRejected === false ) return callback( notRejected ); 284 | 285 | // only actual transiton need update base state; 286 | if( !callForPermit ) this._checkQueryAndParam(parent, option) 287 | 288 | option.basckward = false; 289 | this._transit( parent, to, option, callForPermit, callback) 290 | 291 | }, this) ) 292 | 293 | }, 294 | 295 | _transit: function(from, to, option, callForPermit, callback){ 296 | // touch the ending 297 | if( from === to ) return callback(); 298 | 299 | var back = from.name.length > to.name.length; 300 | var method = back? 'leave': 'enter'; 301 | var applied; 302 | 303 | // use canEnter to detect permission 304 | if( callForPermit) method = 'can' + method.replace(/^\w/, function(a){ return a.toUpperCase() }); 305 | 306 | var loop = _.bind(function( notRejected ){ 307 | 308 | 309 | // stop transition or touch the end 310 | if( applied === to || notRejected === false ) return callback(notRejected); 311 | 312 | if( !applied ) { 313 | 314 | applied = back? from : this._computeNext(from, to); 315 | 316 | }else{ 317 | 318 | applied = this._computeNext(applied, to); 319 | } 320 | 321 | if( (back && applied === to) || !applied )return callback( notRejected ) 322 | 323 | this._moveOn( applied, method, option, loop ); 324 | 325 | }, this); 326 | 327 | loop(); 328 | }, 329 | 330 | _moveOn: function( applied, method, option, callback){ 331 | 332 | var isDone = false; 333 | var isPending = false; 334 | 335 | option.async = function(){ 336 | 337 | isPending = true; 338 | 339 | return done; 340 | } 341 | 342 | function done( notRejected ){ 343 | if( isDone ) return; 344 | isPending = false; 345 | isDone = true; 346 | callback( notRejected ); 347 | } 348 | 349 | 350 | 351 | option.stop = function(){ 352 | done( false ); 353 | } 354 | 355 | 356 | this.active = applied; 357 | var retValue = applied[method]? applied[method]( option ): true; 358 | 359 | if(method === 'enter') applied.visited = true; 360 | // promise 361 | // need breadk , if we call option.stop first; 362 | 363 | if( _.isPromise(retValue) ){ 364 | 365 | return this._wrapPromise(retValue, done); 366 | 367 | } 368 | 369 | // if haven't call option.async yet 370 | if( !isPending ) done( retValue ) 371 | 372 | }, 373 | 374 | 375 | _wrapPromise: function( promise, next ){ 376 | 377 | return promise.then( next, function(){next(false)}) ; 378 | 379 | }, 380 | 381 | _computeNext: function( from, to ){ 382 | 383 | var fname = from.name; 384 | var tname = to.name; 385 | 386 | var tsplit = tname.split('.') 387 | var fsplit = fname.split('.') 388 | 389 | var tlen = tsplit.length; 390 | var flen = fsplit.length; 391 | 392 | if(fname === '') flen = 0; 393 | if(tname === '') tlen = 0; 394 | 395 | if( flen < tlen ){ 396 | fsplit[flen] = tsplit[flen]; 397 | }else{ 398 | fsplit.pop(); 399 | } 400 | 401 | return this.state(fsplit.join('.')) 402 | 403 | }, 404 | 405 | _findQuery: function(querystr){ 406 | 407 | var queries = querystr && querystr.split("&"), query= {}; 408 | if(queries){ 409 | var len = queries.length; 410 | var query = {}; 411 | for(var i =0; i< len; i++){ 412 | var tmp = queries[i].split("="); 413 | query[tmp[0]] = tmp[1]; 414 | } 415 | } 416 | return query; 417 | 418 | }, 419 | _findState: function(state, path){ 420 | var states = state._states, found, param; 421 | 422 | // leaf-state has the high priority upon branch-state 423 | if(state.hasNext){ 424 | for(var i in states) if(states.hasOwnProperty(i)){ 425 | found = this._findState( states[i], path ); 426 | if( found ) return found; 427 | } 428 | } 429 | // in strict mode only leaf can be touched 430 | // if all children is don. will try it self 431 | param = state.regexp && state.decode(path); 432 | if(param){ 433 | state.param = param; 434 | return state; 435 | }else{ 436 | return false; 437 | } 438 | }, 439 | // find the same branch; 440 | _findBase: function(now, before){ 441 | 442 | if(!now || !before || now == this || before == this) return this; 443 | var np = now, bp = before, tmp; 444 | while(np && bp){ 445 | tmp = bp; 446 | while(tmp){ 447 | if(np === tmp) return tmp; 448 | tmp = tmp.parent; 449 | } 450 | np = np.parent; 451 | } 452 | }, 453 | // check the query and Param 454 | _checkQueryAndParam: function(baseState, options){ 455 | 456 | var from = baseState; 457 | while( from !== this ){ 458 | from.update && from.update(options); 459 | from = from.parent; 460 | } 461 | 462 | } 463 | 464 | }, true) 465 | 466 | 467 | 468 | module.exports = StateMan; 469 | 470 | -------------------------------------------------------------------------------- /docs/pages/flat-style.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Please don't edit this file directly. 4 | Instead, edit the stylus (.styl) files and compile it to CSS on your machine. 5 | 6 | */ 7 | /* ---------------------------------------------------------------------------- 8 | * Fonts 9 | */ 10 | @import url("//fonts.googleapis.com/css?family=Montserrat:700|Open+Sans:300"); 11 | /* ---------------------------------------------------------------------------- 12 | * Base 13 | */ 14 | html, 15 | body, 16 | div, 17 | span, 18 | applet, 19 | object, 20 | iframe, 21 | h1, 22 | h2, 23 | h3, 24 | h4, 25 | h5, 26 | h6, 27 | p, 28 | blockquote, 29 | pre, 30 | a, 31 | abbr, 32 | acronym, 33 | address, 34 | big, 35 | cite, 36 | code, 37 | del, 38 | dfn, 39 | em, 40 | img, 41 | ins, 42 | kbd, 43 | q, 44 | s, 45 | samp, 46 | small, 47 | strike, 48 | strong, 49 | sub, 50 | sup, 51 | tt, 52 | var, 53 | dl, 54 | dt, 55 | dd, 56 | ol, 57 | ul, 58 | li, 59 | fieldset, 60 | form, 61 | label, 62 | legend, 63 | table, 64 | caption, 65 | tbody, 66 | tfoot, 67 | thead, 68 | tr, 69 | th, 70 | td { 71 | margin: 0; 72 | padding: 0; 73 | border: 0; 74 | outline: 0; 75 | font-weight: inherit; 76 | font-style: inherit; 77 | font-family: inherit; 78 | font-size: 100%; 79 | vertical-align: baseline; 80 | } 81 | body { 82 | line-height: 1; 83 | color: #000; 84 | background: #fff; 85 | } 86 | ol, 87 | ul { 88 | list-style: none; 89 | } 90 | table { 91 | border-collapse: separate; 92 | border-spacing: 0; 93 | vertical-align: middle; 94 | } 95 | caption, 96 | th, 97 | td { 98 | text-align: left; 99 | font-weight: normal; 100 | vertical-align: middle; 101 | } 102 | a img { 103 | border: none; 104 | } 105 | html, 106 | body { 107 | height: 100%; 108 | } 109 | html { 110 | overflow-x: hidden; 111 | } 112 | body, 113 | td, 114 | textarea, 115 | input { 116 | font-family: Helvetica Neue, Open Sans, sans-serif; 117 | line-height: 1.6; 118 | font-size: 13px; 119 | color: #505050; 120 | } 121 | @media (max-width: 480px) { 122 | body, 123 | td, 124 | textarea, 125 | input { 126 | font-size: 12px; 127 | } 128 | } 129 | a { 130 | color: #2badad; 131 | text-decoration: none; 132 | } 133 | a:hover { 134 | color: #228a8a; 135 | } 136 | /* ---------------------------------------------------------------------------- 137 | * Content styling 138 | */ 139 | .content p, 140 | .content ul, 141 | .content ol, 142 | .content h1, 143 | .content h2, 144 | .content h3, 145 | .content h4, 146 | .content h5, 147 | .content h6, 148 | .content pre, 149 | .content blockquote { 150 | padding: 10px 0; 151 | -webkit-box-sizing: border-box; 152 | -moz-box-sizing: border-box; 153 | box-sizing: border-box; 154 | } 155 | .content h1, 156 | .content h2, 157 | .content h3, 158 | .content h4, 159 | .content h5, 160 | .content h6 { 161 | font-weight: bold; 162 | -webkit-font-smoothing: antialiased; 163 | text-rendering: optimizeLegibility; 164 | } 165 | .content pre { 166 | font-family: Menlo, monospace; 167 | } 168 | .content ul > li { 169 | list-style-type: disc; 170 | } 171 | .content ol > li { 172 | list-style-type: decimal; 173 | } 174 | .content ul, 175 | .content ol { 176 | margin-left: 20px; 177 | } 178 | .content ul > li { 179 | list-style-type: none; 180 | position: relative; 181 | } 182 | .content ul > li:before { 183 | content: ''; 184 | display: block; 185 | position: absolute; 186 | left: -17px; 187 | top: 7px; 188 | width: 5px; 189 | height: 5px; 190 | -webkit-border-radius: 4px; 191 | border-radius: 4px; 192 | -webkit-box-sizing: border-box; 193 | -moz-box-sizing: border-box; 194 | box-sizing: border-box; 195 | background: #fff; 196 | border: solid 1px #9090aa; 197 | } 198 | .content li > :first-child { 199 | padding-top: 0; 200 | } 201 | .content strong, 202 | .content b { 203 | font-weight: bold; 204 | } 205 | .content i, 206 | .content em { 207 | font-style: italic; 208 | color: #9090aa; 209 | } 210 | .content code { 211 | font-family: Menlo, monospace; 212 | background: #f3f6fb; 213 | padding: 1px 3px; 214 | font-size: 0.95em; 215 | } 216 | .content pre > code { 217 | display: block; 218 | background: transparent; 219 | font-size: 0.85em; 220 | letter-spacing: -1px; 221 | } 222 | .content blockquote :first-child { 223 | padding-top: 0; 224 | } 225 | .content blockquote :last-child { 226 | padding-bottom: 0; 227 | } 228 | .content table { 229 | margin-top: 10px; 230 | margin-bottom: 10px; 231 | padding: 0; 232 | border-collapse: collapse; 233 | clear: both; 234 | } 235 | .content table tr { 236 | border-top: 1px solid #ccc; 237 | background-color: #fff; 238 | margin: 0; 239 | padding: 0; 240 | } 241 | .content table tr :nth-child(2n) { 242 | background-color: #f8f8f8; 243 | } 244 | .content table tr th { 245 | text-align: auto; 246 | font-weight: bold; 247 | border: 1px solid #ccc; 248 | margin: 0; 249 | padding: 6px 13px; 250 | } 251 | .content table tr td { 252 | text-align: auto; 253 | border: 1px solid #ccc; 254 | margin: 0; 255 | padding: 6px 13px; 256 | } 257 | .content table tr th :first-child, 258 | .content table tr td :first-child { 259 | margin-top: 0; 260 | } 261 | .content table tr th :last-child, 262 | .content table tr td :last-child { 263 | margin-bottom: 0; 264 | } 265 | /* ---------------------------------------------------------------------------- 266 | * Content 267 | */ 268 | .content-root { 269 | min-height: 90%; 270 | position: relative; 271 | } 272 | .content { 273 | padding-top: 30px; 274 | padding-bottom: 40px; 275 | padding-left: 40px; 276 | padding-right: 40px; 277 | zoom: 1; 278 | max-width: 700px; 279 | } 280 | .content:before, 281 | .content:after { 282 | content: ""; 283 | display: table; 284 | } 285 | .content:after { 286 | clear: both; 287 | } 288 | .content blockquote { 289 | color: #9090aa; 290 | text-shadow: 0 1px 0 rgba(255,255,255,0.5); 291 | } 292 | .content h1, 293 | .content h2, 294 | .content h3 { 295 | -webkit-font-smoothing: antialiased; 296 | text-rendering: optimizeLegibility; 297 | font-family: montserrat; 298 | padding-bottom: 0; 299 | } 300 | .content h1 + p, 301 | .content h2 + p, 302 | .content h3 + p, 303 | .content h1 ul, 304 | .content h2 ul, 305 | .content h3 ul, 306 | .content h1 ol, 307 | .content h2 ol, 308 | .content h3 ol { 309 | padding-top: 10px; 310 | } 311 | .content h1, 312 | .content h2 { 313 | text-transform: uppercase; 314 | letter-spacing: 1px; 315 | font-size: 1.5em; 316 | } 317 | .content h3 { 318 | font-size: 1.2em; 319 | } 320 | .content h1, 321 | .content h2, 322 | .content .big-heading, 323 | body.big-h3 .content h3 { 324 | padding-top: 80px; 325 | } 326 | .content h1:before, 327 | .content h2:before, 328 | .content .big-heading:before, 329 | body.big-h3 .content h3:before { 330 | display: block; 331 | content: ''; 332 | background: -webkit-gradient(linear, left top, right top, color-stop(0.8, #dfe2e7), color-stop(1, rgba(223,226,231,0))); 333 | background: -webkit-linear-gradient(left, #dfe2e7 80%, rgba(223,226,231,0) 100%); 334 | background: -moz-linear-gradient(left, #dfe2e7 80%, rgba(223,226,231,0) 100%); 335 | background: -o-linear-gradient(left, #dfe2e7 80%, rgba(223,226,231,0) 100%); 336 | background: -ms-linear-gradient(left, #dfe2e7 80%, rgba(223,226,231,0) 100%); 337 | background: linear-gradient(left, #dfe2e7 80%, rgba(223,226,231,0) 100%); 338 | -webkit-box-shadow: 0 1px 0 rgba(255,255,255,0.4); 339 | box-shadow: 0 1px 0 rgba(255,255,255,0.4); 340 | height: 1px; 341 | position: relative; 342 | top: -40px; 343 | left: -40px; 344 | width: 100%; 345 | } 346 | @media (max-width: 768px) { 347 | .content h1, 348 | .content h2, 349 | .content .big-heading, 350 | body.big-h3 .content h3 { 351 | padding-top: 40px; 352 | } 353 | .content h1:before, 354 | .content h2:before, 355 | .content .big-heading:before, 356 | body.big-h3 .content h3:before { 357 | background: #dfe2e7; 358 | left: -40px; 359 | top: -20px; 360 | width: 120%; 361 | } 362 | } 363 | .content h4, 364 | .content h5, 365 | .content .small-heading, 366 | body:not(.big-h3) .content h3 { 367 | border-bottom: solid 1px rgba(0,0,0,0.07); 368 | color: #9090aa; 369 | padding-top: 30px; 370 | padding-bottom: 10px; 371 | } 372 | body:not(.big-h3) .content h3 { 373 | font-size: 0.9em; 374 | } 375 | .content h1:first-child { 376 | padding-top: 0; 377 | } 378 | .content h1:first-child, 379 | .content h1:first-child a, 380 | .content h1:first-child a:visited { 381 | color: #505050; 382 | } 383 | .content h1:first-child:before { 384 | display: none; 385 | } 386 | @media (max-width: 768px) { 387 | .content h4, 388 | .content h5, 389 | .content .small-heading, 390 | body:not(.big-h3) .content h3 { 391 | padding-top: 20px; 392 | } 393 | } 394 | @media (max-width: 480px) { 395 | .content { 396 | padding: 20px; 397 | padding-top: 40px; 398 | } 399 | .content h4, 400 | .content h5, 401 | .content .small-heading, 402 | body:not(.big-h3) .content h3 { 403 | padding-top: 10px; 404 | } 405 | } 406 | body.no-literate .content pre > code { 407 | background: #f3f6fb; 408 | border: solid 1px #e7eaee; 409 | border-top: solid 1px #dbdde2; 410 | border-left: solid 1px #e2e5e9; 411 | display: block; 412 | padding: 10px; 413 | -webkit-border-radius: 2px; 414 | border-radius: 2px; 415 | overflow: auto; 416 | } 417 | body.no-literate .content pre > code { 418 | -webkit-overflow-scrolling: touch; 419 | } 420 | body.no-literate .content pre > code::-webkit-scrollbar { 421 | width: 15px; 422 | height: 15px; 423 | } 424 | body.no-literate .content pre > code::-webkit-scrollbar-thumb { 425 | background: #ddd; 426 | -webkit-border-radius: 8px; 427 | border-radius: 8px; 428 | border: solid 4px #f3f6fb; 429 | } 430 | body.no-literate .content pre > code:hover::-webkit-scrollbar-thumb { 431 | background: #999; 432 | -webkit-box-shadow: inset 2px 2px 3px rgba(0,0,0,0.2); 433 | box-shadow: inset 2px 2px 3px rgba(0,0,0,0.2); 434 | } 435 | @media (max-width: 1180px) { 436 | .content pre > code { 437 | background: #f3f6fb; 438 | border: solid 1px #e7eaee; 439 | border-top: solid 1px #dbdde2; 440 | border-left: solid 1px #e2e5e9; 441 | display: block; 442 | padding: 10px; 443 | -webkit-border-radius: 2px; 444 | border-radius: 2px; 445 | overflow: auto; 446 | } 447 | .content pre > code { 448 | -webkit-overflow-scrolling: touch; 449 | } 450 | .content pre > code::-webkit-scrollbar { 451 | width: 15px; 452 | height: 15px; 453 | } 454 | .content pre > code::-webkit-scrollbar-thumb { 455 | background: #ddd; 456 | -webkit-border-radius: 8px; 457 | border-radius: 8px; 458 | border: solid 4px #f3f6fb; 459 | } 460 | .content pre > code:hover::-webkit-scrollbar-thumb { 461 | background: #999; 462 | -webkit-box-shadow: inset 2px 2px 3px rgba(0,0,0,0.2); 463 | box-shadow: inset 2px 2px 3px rgba(0,0,0,0.2); 464 | } 465 | } 466 | .button { 467 | -webkit-font-smoothing: antialiased; 468 | text-rendering: optimizeLegibility; 469 | font-family: montserrat, sans-serif; 470 | letter-spacing: -1px; 471 | font-weight: bold; 472 | display: inline-block; 473 | padding: 3px 25px; 474 | border: solid 2px #2badad; 475 | -webkit-border-radius: 20px; 476 | border-radius: 20px; 477 | margin-right: 15px; 478 | } 479 | .button, 480 | .button:visited { 481 | background: #2badad; 482 | color: #fff; 483 | text-shadow: none; 484 | } 485 | .button:hover { 486 | border-color: #111; 487 | background: #111; 488 | color: #fff; 489 | } 490 | .button.light, 491 | .button.light:visited { 492 | background: transparent; 493 | color: #9090aa; 494 | border-color: #9090aa; 495 | text-shadow: none; 496 | } 497 | .button.light:hover { 498 | border-color: #9090aa; 499 | background: #9090aa; 500 | color: #fff; 501 | } 502 | .content .button + em { 503 | color: #9090aa; 504 | } 505 | @media (min-width: 1180px) { 506 | body:not(.no-literate) .content-root { 507 | background-color: #f3f6fb; 508 | -webkit-box-shadow: inset 780px 0 #fff, inset 781px 0 #dfe2e7, inset 790px 0 5px -10px rgba(0,0,0,0.1); 509 | box-shadow: inset 780px 0 #fff, inset 781px 0 #dfe2e7, inset 790px 0 5px -10px rgba(0,0,0,0.1); 510 | } 511 | } 512 | @media (min-width: 1180px) { 513 | body:not(.no-literate) .content { 514 | padding-left: 0; 515 | padding-right: 0; 516 | width: 930px; 517 | max-width: none; 518 | } 519 | body:not(.no-literate) .content > p, 520 | body:not(.no-literate) .content > ul, 521 | body:not(.no-literate) .content > ol, 522 | body:not(.no-literate) .content > h1, 523 | body:not(.no-literate) .content > h2, 524 | body:not(.no-literate) .content > h3, 525 | body:not(.no-literate) .content > h4, 526 | body:not(.no-literate) .content > h5, 527 | body:not(.no-literate) .content > h6, 528 | body:not(.no-literate) .content > pre, 529 | body:not(.no-literate) .content > blockquote { 530 | width: 550px; 531 | -webkit-box-sizing: border-box; 532 | -moz-box-sizing: border-box; 533 | box-sizing: border-box; 534 | padding-right: 40px; 535 | padding-left: 40px; 536 | } 537 | body:not(.no-literate) .content > h1, 538 | body:not(.no-literate) .content > h2, 539 | body:not(.no-literate) .content > h3 { 540 | clear: both; 541 | width: 100%; 542 | } 543 | body:not(.no-literate) .content > pre, 544 | body:not(.no-literate) .content > blockquote { 545 | width: 380px; 546 | padding-left: 20px; 547 | padding-right: 20px; 548 | float: right; 549 | clear: right; 550 | } 551 | body:not(.no-literate) .content > pre + p, 552 | body:not(.no-literate) .content > blockquote + p, 553 | body:not(.no-literate) .content > pre + ul, 554 | body:not(.no-literate) .content > blockquote + ul, 555 | body:not(.no-literate) .content > pre + ol, 556 | body:not(.no-literate) .content > blockquote + ol, 557 | body:not(.no-literate) .content > pre + h4, 558 | body:not(.no-literate) .content > blockquote + h4, 559 | body:not(.no-literate) .content > pre + h5, 560 | body:not(.no-literate) .content > blockquote + h5, 561 | body:not(.no-literate) .content > pre + h6, 562 | body:not(.no-literate) .content > blockquote + h6 { 563 | clear: both; 564 | } 565 | body:not(.no-literate) .content > p, 566 | body:not(.no-literate) .content > ul, 567 | body:not(.no-literate) .content > ol, 568 | body:not(.no-literate) .content > h4, 569 | body:not(.no-literate) .content > h5, 570 | body:not(.no-literate) .content > h6 { 571 | float: left; 572 | clear: left; 573 | } 574 | body:not(.no-literate) .content > h4, 575 | body:not(.no-literate) .content > h5, 576 | body:not(.no-literate) .content > .small-heading, 577 | body:not(.big-h3) body:not(.no-literate) .content > h3 { 578 | margin-left: 40px; 579 | width: 470px; 580 | margin-bottom: 3px; 581 | padding-left: 0; 582 | padding-right: 0; 583 | } 584 | body:not(.no-literate) .content > table { 585 | margin-left: 40px; 586 | margin-right: 40px; 587 | max-width: 470px; 588 | } 589 | body:not(.no-literate):not(.big-h3) .content > h3 { 590 | margin-left: 40px; 591 | width: 470px; 592 | margin-bottom: 3px; 593 | padding-left: 0; 594 | padding-right: 0; 595 | } 596 | } 597 | .header { 598 | background: #f3f6fb; 599 | text-shadow: 0 1px 0 rgba(255,255,255,0.5); 600 | border-bottom: solid 1px #dfe2e7; 601 | padding: 15px 15px 15px 30px; 602 | zoom: 1; 603 | line-height: 20px; 604 | position: relative; 605 | } 606 | .header:before, 607 | .header:after { 608 | content: ""; 609 | display: table; 610 | } 611 | .header:after { 612 | clear: both; 613 | } 614 | .header .left { 615 | float: left; 616 | } 617 | .header .right { 618 | text-align: right; 619 | position: absolute; 620 | right: 15px; 621 | top: 15px; 622 | } 623 | .header .right iframe { 624 | display: inline-block; 625 | vertical-align: middle; 626 | } 627 | .header h1 { 628 | -webkit-font-smoothing: antialiased; 629 | text-rendering: optimizeLegibility; 630 | font-weight: bold; 631 | font-family: montserrat, sans-serif; 632 | font-size: 13px; 633 | } 634 | .header h1, 635 | .header h1 a, 636 | .header h1 a:visited { 637 | color: #9090aa; 638 | } 639 | .header h1 a:hover { 640 | color: #505050; 641 | } 642 | .header li a { 643 | font-size: 0.88em; 644 | color: #9090aa; 645 | display: block; 646 | } 647 | .header li a:hover { 648 | color: #3a3a44; 649 | } 650 | @media (min-width: 480px) { 651 | .header h1 { 652 | float: left; 653 | } 654 | .header ul, 655 | .header li { 656 | display: block; 657 | float: left; 658 | } 659 | .header ul { 660 | margin-left: -15px; 661 | } 662 | .header h1 + ul { 663 | border-left: solid 1px #dfe2e7; 664 | margin-left: 15px; 665 | } 666 | .header li { 667 | border-left: solid 1px rgba(255,255,255,0.5); 668 | border-right: solid 1px #dfe2e7; 669 | } 670 | .header li:last-child { 671 | border-right: 0; 672 | } 673 | .header li a { 674 | padding: 0 15px; 675 | } 676 | } 677 | @media (max-width: 480px) { 678 | .right { 679 | display: none; 680 | } 681 | } 682 | .menubar { 683 | -webkit-font-smoothing: antialiased; 684 | text-rendering: optimizeLegibility; 685 | } 686 | .menubar .section { 687 | padding: 30px 30px; 688 | -webkit-box-sizing: border-box; 689 | -moz-box-sizing: border-box; 690 | box-sizing: border-box; 691 | } 692 | .menubar .section + .section { 693 | border-top: solid 1px #dfe2e7; 694 | } 695 | .menubar .section.no-line { 696 | border-top: 0; 697 | padding-top: 0; 698 | } 699 | a.big.button { 700 | display: block; 701 | -webkit-box-sizing: border-box; 702 | -moz-box-sizing: border-box; 703 | box-sizing: border-box; 704 | width: 100%; 705 | padding: 10px 20px; 706 | text-align: center; 707 | font-weight: bold; 708 | font-size: 1.1em; 709 | background: transparent; 710 | border: solid 3px #2badad; 711 | -webkit-border-radius: 30px; 712 | border-radius: 30px; 713 | font-family: montserrat, sans-serif; 714 | } 715 | a.big.button, 716 | a.big.button:visited { 717 | color: #2badad; 718 | text-decoration: none; 719 | } 720 | a.big.button:hover { 721 | background: #2badad; 722 | } 723 | a.big.button:hover, 724 | a.big.button:hover:visited { 725 | color: #fff; 726 | } 727 | @media (max-width: 480px) { 728 | .menubar { 729 | padding: 20px; 730 | border-bottom: solid 1px #dfe2e7; 731 | } 732 | } 733 | @media (max-width: 768px) { 734 | .menubar { 735 | display: none; 736 | } 737 | } 738 | @media (min-width: 768px) { 739 | .content-root { 740 | padding-left: 230px; 741 | } 742 | .menubar { 743 | position: absolute; 744 | left: 0; 745 | top: 0; 746 | bottom: 0; 747 | width: 230px; 748 | border-right: solid 1px #dfe2e7; 749 | } 750 | .menubar.fixed { 751 | position: fixed; 752 | overflow-y: auto; 753 | } 754 | .menubar.fixed { 755 | -webkit-overflow-scrolling: touch; 756 | } 757 | .menubar.fixed::-webkit-scrollbar { 758 | width: 15px; 759 | height: 15px; 760 | } 761 | .menubar.fixed::-webkit-scrollbar-thumb { 762 | background: #ddd; 763 | -webkit-border-radius: 8px; 764 | border-radius: 8px; 765 | border: solid 4px #fff; 766 | } 767 | .menubar.fixed:hover::-webkit-scrollbar-thumb { 768 | background: #999; 769 | -webkit-box-shadow: inset 2px 2px 3px rgba(0,0,0,0.2); 770 | box-shadow: inset 2px 2px 3px rgba(0,0,0,0.2); 771 | } 772 | } 773 | .menubar { 774 | font-size: 0.9em; 775 | } 776 | .menu ul.level-1 > li + li { 777 | margin-top: 20px; 778 | } 779 | .menu a { 780 | -webkit-box-sizing: border-box; 781 | -moz-box-sizing: border-box; 782 | box-sizing: border-box; 783 | position: relative; 784 | display: block; 785 | padding-top: 1px; 786 | padding-bottom: 1px; 787 | margin-right: -30px; 788 | } 789 | .menu a, 790 | .menu a:visited { 791 | color: #2badad; 792 | } 793 | .menu a:hover { 794 | color: #228a8a; 795 | } 796 | .menu a.level-1 { 797 | font-family: montserrat, sans-serif; 798 | text-transform: uppercase; 799 | font-size: 0.9em; 800 | font-weight: bold; 801 | } 802 | .menu a.level-1, 803 | .menu a.level-1:visited { 804 | color: #9090aa; 805 | } 806 | .menu a.level-1:hover { 807 | color: #565666; 808 | } 809 | .menu a.level-2 { 810 | font-weight: normal; 811 | } 812 | .menu a.level-3 { 813 | font-weight: normal; 814 | font-size: 0.9em; 815 | padding-left: 10px; 816 | } 817 | .menu a.active { 818 | font-weight: bold !important; 819 | } 820 | .menu a.active, 821 | .menu a.active:visited, 822 | .menu a.active:hover { 823 | color: #505050 !important; 824 | } 825 | .menu a.active:after { 826 | content: ''; 827 | display: block; 828 | -webkit-box-sizing: border-box; 829 | -moz-box-sizing: border-box; 830 | box-sizing: border-box; 831 | position: absolute; 832 | top: 10px; 833 | right: 30px; 834 | width: 9px; 835 | height: 3px; 836 | -webkit-border-radius: 2px; 837 | border-radius: 2px; 838 | background: #2badad; 839 | } 840 | code .string, 841 | code .number { 842 | color: #3ac; 843 | } 844 | code .init { 845 | color: #383; 846 | } 847 | code .keyword { 848 | font-weight: bold; 849 | } 850 | code .comment { 851 | color: #adadcc; 852 | } 853 | .large-brief .content > h1:first-child + p, 854 | .content > p.brief { 855 | font-size: 1.3em; 856 | font-family: Open Sans, sans-serif; 857 | font-weight: 300; 858 | } 859 | .title-area { 860 | min-height: 100px; 861 | -webkit-box-sizing: border-box; 862 | -moz-box-sizing: border-box; 863 | box-sizing: border-box; 864 | -webkit-font-smoothing: antialiased; 865 | text-rendering: optimizeLegibility; 866 | text-align: center; 867 | border-bottom: solid 1px #dfe2e7; 868 | overflow: hidden; 869 | } 870 | .title-area > img.bg { 871 | z-index: 0; 872 | position: absolute; 873 | left: -9999px; 874 | } 875 | .title-area > div { 876 | position: relative; 877 | z-index: 1; 878 | } 879 | 880 | -------------------------------------------------------------------------------- /docs/pages/document/API-zh.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | > 微量的api在0.2版本有所修改 4 | 5 | 6 | ## 0.2.x的改进 7 | 8 | 9 | - 增加了一个[askForPermission](#permission), 来帮助我们阻止一次跳转(比如在离开时, 加个通知用户是否要保存) 10 | - 现在你可以在`enter`, `leave` 以及新增加的 `canLeave`, `canEnter` 方法中来返回Promise对象, 这对于一些异步的跳转非常有帮助 11 | - 事件现在支持[命名空间](#event)了 12 | - 移除了[state.async] 方法, 如果你的运行环境不支持Promise, 你仍然可以使用 `option.async` 来获得一样的效果 13 | 14 | 15 | # StateMan 文档 16 | 17 | 18 | 19 | 为了更好的理解这不算多的API, 在文档开始前,假设我们已经配置了这样一段路由脚本. 20 | 21 | 22 | 23 | ```js 24 | 25 | var config = { 26 | enter: function(option){ 27 | console.log("enter: " + this.name + "; param: " + JSON.stringify(option.param)) 28 | }, 29 | leave: function(option){ 30 | console.log("leave: " + this.name + "; param: " + JSON.stringify(option.param)) 31 | }, 32 | update: function(option){ 33 | console.log("update: " + this.name + "; param: " + JSON.stringify(option.param)) 34 | }, 35 | } 36 | 37 | function cfg(o){ 38 | o.enter = o.enter || config.enter 39 | o.leave = o.leave || config.leave 40 | o.update = o.update || config.update 41 | return o; 42 | } 43 | 44 | var stateman = new StateMan(); 45 | 46 | stateman.state({ 47 | 48 | "app": config, 49 | "app.contact": config, 50 | "app.contact.detail": cfg({url: ":id(\\d+)"}), 51 | "app.contact.detail.setting": config, 52 | "app.contact.message": config, 53 | "app.user": cfg({ 54 | enter: function( option ){ 55 | var done = option.async(); 56 | console.log(this.name + "is pending, 1s later to enter next state") 57 | setTimeout(done, 1000) 58 | }, 59 | leave: function( option ){ 60 | var done = option.async(); 61 | console.log(this.name + "is pending, 1s later to leave out") 62 | setTimeout(done, 1000) 63 | } 64 | }), 65 | "app.user.list": cfg({url: ""}) 66 | 67 | }).on("notfound", function(){ 68 | this.go('app') // if not found 69 | }).start(); 70 | 71 | ``` 72 | 73 | 74 | 对象`config`用来输出navgating的相关信息, 你不需要立刻理解这个例子, 稍后文档会慢慢告诉你一切. 75 | 76 | 你可以直接通过【[在线DEMO](./example/api.html)】 访问到这个例子, 有时候试着在console中测试API可以帮助你更好的理解它们 77 | 78 | 79 | 80 | 81 | ## API 82 | 83 | ### new StateMan 84 | 85 | __Usage__ 86 | 87 | `new StateMan(option)` 88 | 89 | __Arguments__ 90 | 91 | |Param|Type|Detail| 92 | |--|--|--| 93 | |option.strict|Boolean| Default: false . 是否只有叶子节点可以被访问到 | 94 | |option.title| String| 设置文档标题, 见 [config.title](#title)| 95 | 96 | 97 | __Return__ 98 | 99 | [Stateman] : StateMan的实例 100 | 101 | __Example__ 102 | 103 | ```javascript 104 | var StateMan = require("stateman"); 105 | 106 | var stateman = new StateMan({ 107 | title: "Application", 108 | strict: true 109 | }); 110 | // or... 111 | var stateman = StateMan(); 112 | ``` 113 | 114 | 如果strict为true, 会导致上例的 `app.contact`等状态不能被直接定位(即无法成为stateman.current了). 只有像`app.contact.message` 这样的叶子节点可以被直接访问. 115 | 116 | 117 | ### stateman.state 118 | 119 | __Usage__ 120 | 121 | `stateman.state(stateName[, config])` 122 | 123 | 124 | stateman.state 用来增加/更新一个state, 或获取指定的state对象(假如 config 参数没有指定的话) 125 | 126 | 127 | 128 | __Arguments__ 129 | 130 | |Param|Type|Detail| 131 | |--|--|--| 132 | |stateName|String Object| state名称,假如传入了一个对象,将成为一个多重设置| 133 | |config(optional)|Function Object| 如果config没有指定,将会返回指定的state, 假如传入的是一个函数,则相当于传入了`config.enter`, 如果指定的state已经存在,原设置会被覆盖 | 134 | 135 | __Return__ : 136 | 137 | StateMan or State (if config is not passed) 138 | 139 | 140 | __Example__ 141 | 142 | ```js 143 | 144 | stateman 145 | .state("app", { 146 | enter: function(){ 147 | console.log("enter the state: app") 148 | }, 149 | leave: function(){ 150 | console.log("leave the state: app") 151 | } 152 | }) 153 | // is equals to {enter: config} 154 | .state("app.contact", function(){ 155 | console.log("enter the app.contact state") 156 | }) 157 | 158 | // pass in a Object for multiple operation 159 | stateman.state({ 160 | "demo.detail": function(){ 161 | console.log("enter the demo.detail state") 162 | }, 163 | "demo.list": { 164 | enter: function(){} 165 | leave: function(){} 166 | } 167 | }) 168 | 169 | 170 | ``` 171 | 172 | 173 | 诚如你所见,我们并不需要在定义`demo.detail`之前定义`demo`, stateman会帮你创建中间state 174 | 175 | 如果config参数没有传入,则会返回对应的state对象 176 | 177 | 178 | ```js 179 | 180 | // return the demo.list state 181 | var state = stateman.state('demo.list'); 182 | 183 | ``` 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | ### > 关于`config` 192 | 193 | 194 | 所有config中的属性都会成为对应state的实例属性,但在这里, 需要对一些特殊的属性做说明 195 | 196 | 197 | 198 | #### lifecycle related 199 | 200 | 201 | 202 | 这里有五个生命周期相关的方法可供你用来控制 路由的逻辑, 它们都是可选择实现的 , 查看[生命周期](#lifecycle)了解更多 203 | 204 | * __config.enter(option)__: 一个函数,当状态被__进入时__会被调用 205 | * __config.leave(option)__: 一个函数,当状态被__离开时__会被调用 206 | * __config.update(option)__: 一个函数,当状态__更新时__会被调用, 更新的意思是,路径有变化,但是此状态仍未被退出. 207 | * __config.canEnter(option)__: 请求是否可进入 208 | * __config.canLeave(option)__: 请求是否可退出 209 | 210 | 211 | 212 | 213 | 214 | #### config.url: 215 | 216 | `url` 属性用来配置state(非全路径)的url片段 217 | 218 | 219 | 220 | 每个state对应的捕获url是所有在到此状态路径上的state的结合. 比如`app.contact.detail` 是 `app`,`app.contact` 和`app.contact.detail`的路径结合 221 | 222 | 223 | 224 | 225 | __Example__ 226 | 227 | ```js 228 | 229 | state.state("app", {}) 230 | .state("app.contact", "users") 231 | .state("app.contact.detail", "/:id") 232 | 233 | ``` 234 | 235 | 236 | `app.contact.detail`的完整捕获路径就是`/app/users/:id`. 当然,如你所见, 我们可以在url中定义我们的[路径参数](#param) 237 | 238 | 239 | 240 | 241 | 242 | 243 | __ 绝对路径__ 244 | 245 | 如果你不需要父级的url定义,你可以使用`^`符号来使当前状态的url 246 | 247 | 248 | 249 | ```js 250 | state.state("app.contact.detail", "^/detail/:id"); 251 | state.state("app.contact.detail.message", "message"); 252 | ``` 253 | 254 | 255 | 这样`app.contact.detail`的路径会直接变成 `/detail/:id`,子状态会被影响到也变为`/detail/:id/message` 256 | 257 | 258 | 259 | 260 | 261 | __空url__: 放弃当前这级的路径配置 262 | 263 | 如果你传入`""`, 你会放弃当前url配置, 这样你的捕获路径会与父状态一致(不过匹配优先级更高) 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | #### config.title 273 | 274 | 275 | 一旦跳转结束, 我们可以控制标签栏的title值(当然 你可以在enter, update, leave中来更精确的手动使用document.title来设置) 276 | 277 | 278 | 279 | __Argument__ 280 | 281 | - config.title [String or Function]: 如果是函数,document会设置成其返回值 282 | 283 | __Example__ 284 | 285 | ``` 286 | stateman.state({ 287 | "app": { 288 | title: "APP" 289 | }, 290 | "app.test": { 291 | title: "App test" 292 | }, 293 | "app.exam": { 294 | url: "exam/:id", 295 | title: function(){ 296 | return "Exam " + stateman.param.id 297 | } 298 | }, 299 | "app.notitle": {} 300 | }) 301 | 302 | stateman.go("app.test"); 303 | 304 | // document.title === "App test" 305 | 306 | stateman.nav("/app/test/1"); 307 | 308 | // document.title === "Exam 1" 309 | 310 | stateman.nav("/app/notitle"); 311 | 312 | // document.title === "App" 313 | ``` 314 | 315 | 316 | 正如你所见, 如果当前状态的title没有配置,stateman会去找它父状态的title, 直到stateman本身为止. 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | ### stateman.start 327 | 328 | 启动stateman, 路由开始 329 | 330 | __Usage__ 331 | 332 | `stateman.start(option)` 333 | 334 | __option__ 335 | 336 | 337 | |Param|Type|Detail| 338 | |--|--|--| 339 | |html5 |Boolean|(default false) 是否开启html5支持, 即使用pushState等API代替hash设置| 340 | |root |String|(default '/') 程序根路径,影响到你使用html5模式的表现| 341 | |prefix| String | 配置hashban, 例如你可以传入'!'来达到`#!/contact/100`的hash表现| 342 | |autolink| Boolean | (defualt true) 是否代理所有的`#`开始的链接来达到跳转, 只对html5下有用| 343 | 344 | 345 | __Example__ 346 | 347 | ```js 348 | stateman.start({ 349 | "html5": true, 350 | "prefix": "!", 351 | "root": "/blog" //the app is begin with '/blog' 352 | }) 353 | 354 | ``` 355 | 356 | __Warning__ 357 | 358 | 359 | 如果你在不支持html5 pushState的浏览器开启了`html=true`, stateman会自动降级到hash的路由. 360 | 361 | 362 | 363 | 364 | 就如同上例的配置. 365 | 366 | 1. 如果我们在不支持html5(pushState相关)的浏览器访问`/blog/app`, stateman会降级到hash的路由,并自动定向到`/blog#/app`的初始状态. 367 | 368 | 2. 如果我们在__支持html5__的浏览器范围__`/blog#/app`__, stateman同样会使用history的路由路径,并自动返回到 `/blog/app`的初始状态. 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | ### stateman.nav 378 | 379 | 380 | 跳转到指定路径,[url中匹配到的参数](#param)会合并到option, 并最终传给之前提到的`enter`, `leave`, `update`函数. 381 | 382 | 383 | 384 | __Usage__ 385 | 386 | `stateman.nav(url[, option][, callback])`; 387 | 388 | 389 | 390 | __Argument__ 391 | 392 | |Param|Type|Detail| 393 | |--|--|--| 394 | |url |String| 跳转url| 395 | |option(optional) |Object| [路由option](#option). url参数会作为option的param参数. | 396 | |callback(optional)|Function| 跳转结束后,此函数会被调用| 397 | 398 | 399 | __control option__ 400 | 401 | 402 | 403 | * option.silent: 如果传入silent, 则只有url路径会发生改变,但是不触发stateman内部的状态改变, 即不会有enter, leave或updatec触发 404 | * option.replace: 如果replace === true, 之前历史的记录会被当前替换,即你无法通过浏览器的后退,回到原状态了 405 | 406 | 407 | 408 | 409 | __Example__ 410 | 411 | `stateman.nav("/app/contact/1?name=leeluolee", {data: 1}); ` 412 | 413 | 414 | 415 | 最终传入到enter, leave与update的option参数会是 416 | 417 | 418 | 419 | 420 | 421 | `{param: {id: "1", name:"leeluolee"}, data: 1}`. 422 | 423 | 424 | 425 | 426 | 427 | 428 | ### stateman.go 429 | 430 | 431 | 跳转到特定状态, 与 stateman.nav 非常相似,但是stateman.go 使用状态来代替url路径进行跳转 432 | 433 | 434 | 435 | __Usage__ 436 | 437 | `stateman.go(stateName [, option][, callback])`; 438 | 439 | 440 | __Arguments__ 441 | 442 | - stateName [String]: 目标状态 443 | 444 | - option [Object]: [Routing Option](#option) 445 | 446 | - option.encode: 447 | 448 | 默认是true, 如果encode是false. 则地址栏的url将不会发生变化,仅仅只是触发了内部状态的跳转. 当encode为true时, stateman会使用[encode](#encode) 函数去反推出真实的url路径显示在location中. 449 | 450 | - option.param: 451 | 452 | nav与go的最大区别就在于param参数 453 | 454 | 如果你的路径中带有参数,则需要传入param来帮助encode函数推算出url路径 455 | 456 | you can use [stateman.encode](#encode) to test how stateman compute url from a state with specifed param 457 | 458 | - option.replace: 见[stateman.nav](#nav) 459 | 460 | - calback [Function]: 同nav 461 | 462 | 463 | 所有其它的option属性将会与param一起传入 `enter`, `leave` , `update`中. 464 | 465 | 466 | 467 | __Example__ 468 | 469 | ``` 470 | stateman.go('app.contact.detail', {param: {id:1, name: 'leeluolee'}}); 471 | ``` 472 | 473 | 474 | 地址会跳转到`#/app/contact/1?name=leeluolee`, 你可以发现未命名参数会直接拼接在url后方作为queryString 存在. 475 | 476 | 477 | 478 | __Tips__: 479 | 480 | 481 | 作者始终推荐在大型项目中使用go代替nav来进行跳转, 来获得更灵活安全的状态控制. 482 | 483 | 484 | 485 | 486 | 487 | 488 | __相对跳转__: 489 | 490 | 491 | stateman预定义了一些符号帮助你进行相对路径的跳转 492 | 493 | 494 | 495 | 1. "~": 代表当前所处的active状态 496 | 2. "^": 代表active状态的父状态; 497 | 498 | __example__ 499 | 500 | ```js 501 | stateman.state({ 502 | "app.user": function(){ 503 | stateman.go("~.detail") // will navigate to app.user.detail 504 | }, 505 | "app.contact.detail": function(){ 506 | stateman.go("^.message") // will navigate to app.contact.message 507 | } 508 | 509 | }) 510 | 511 | ``` 512 | 513 | 514 | 515 | 516 | ### stateman.is 517 | 518 | __Usage__ 519 | 520 | `stateman.is( stateName[, param] [, isStrict] )` 521 | 522 | 523 | 判断当前状态是否满足传入的stateName. 如果有param 参数传入,则除了状态,param也必须匹配. 你不必传入所有的参数, is 只会判断你传入的参数是否匹配. 524 | 525 | 526 | 527 | __Arguments__ 528 | 529 | |Param|Type|Detail| 530 | |--|--|--| 531 | |stateName |String| 用测试的stateName | 532 | |param(optional)|Object| 用于测试的参数对象| 533 | |isStrict(optional)|Boolean| 传入状态是否要严格匹配当前状态| 534 | 535 | 536 | 537 | __example__ 538 | 539 | ```js 540 | stateman.nav("#/app/contact/1?name=leeluolee"); 541 | stateman.is("app.contact.detail") // return true 542 | stateman.is("app.contact", {}, true) // return false, 543 | stateman.is("app.contact.detail", {name: "leeluolee"}, true) // return true 544 | stateman.is("app.contact.detail", {id: "1"}) // return true 545 | stateman.is("app.contact.detail", {id: "2"}) // return false 546 | stateman.is("app.contact.detail", {id: "2", name: "leeluolee"}) // return false 547 | ``` 548 | 549 | 550 | ### stateman.encode 551 | 552 | 553 | 根据状态名和参数获得指定url. 554 | 555 | go函数即基于此方法 556 | 557 | 558 | 559 | 560 | 561 | __Usage__ 562 | 563 | `stateman.encode( stateName[, param] )` 564 | 565 | 566 | __Arguments__ 567 | 568 | 569 | |Param|Type|Detail| 570 | |--|--|--| 571 | |stateName |String| stateName | 572 | |param(optional)|Object| 用于组装url的参数对象| 573 | 574 | 575 | ```js 576 | stateman.encode("app.contact.detail", {id: "1", name: "leeluolee"}) 577 | // === "/app/contact/1?name=leeluolee" 578 | 579 | ``` 580 | 581 | 582 | ### stateman.decode 583 | 584 | 585 | 解码传入的url, 获得匹配的状态,状态同时会带上计算出的参数对象 586 | 587 | 方法[__nav__](#nav) 就是基于此方法实现 588 | 589 | 590 | __Usage__ 591 | 592 | `stateman.decode( url )` 593 | 594 | 595 | __Example__ 596 | 597 | ```js 598 | var state = stateman.decode("/app/contact/1?name=leeluolee") 599 | 600 | state.name === 'app.contact.detail' 601 | state.param // =>{id: "1", name: "leeluolee"} 602 | 603 | ``` 604 | 605 | 606 | 607 | ### stateman.stop 608 | 609 | __Usage__ 610 | 611 | `stateman.stop()` 612 | 613 | stop the stateman. 614 | 615 | 616 | 617 | 618 | 619 | ### stateman.on 620 | 621 | 为指定函数名添加监听器 622 | 623 | 624 | 625 | __Usage__ 626 | 627 | `stateman.on(event, handle)` 628 | 629 | 630 | StateMan内置了一个小型Emitter 来帮助实现事件驱动的开发, 在 [Routing Event](#event) 中查看内建事件 631 | 632 | 633 | 634 | 635 | you 可以使用 `[event]:[namespace]`的格式区创建一个带有namespace的事件 636 | 637 | 638 | __Example__ 639 | 640 | ``` 641 | stateman 642 | .on('begin', beginListener) 643 | // there will be a multiply binding 644 | .on({ 645 | 'end': endListener, 646 | 'custom': customListener, 647 | 'custom:name1': customListenerWithNameSpace 648 | }) 649 | ``` 650 | 651 | 652 | 653 | 654 | ### stateman.off 655 | 656 | 解绑一个监听器 657 | 658 | 659 | __Usage__ 660 | 661 | `stateman.off(event, handle)` 662 | 663 | 664 | __Example__ 665 | 666 | 667 | 这里有多种参数可能 668 | 669 | 670 | 671 | ```js 672 | // unbind listener with specified handle 673 | stateman.off('begin', beginListener ) 674 | // unbind all listener whose eventName is custom and namespace is name1 675 | .off('custom:name1') 676 | // unbind listener whose name is 'custom' (ignore namespace) 677 | .off('custom') 678 | // clear all event bindings of stateman 679 | .off() 680 | ``` 681 | 682 | 683 | 684 | 685 | ### stateman.emit 686 | 687 | 688 | 触发一个事件,你可以传入对应的参数 689 | 690 | 691 | 692 | 693 | __Usage__ 694 | 695 | `stateman.emit(event, param)` 696 | 697 | 698 | 699 | 与stateman.off类似, namespace会影响函数的表现, 我们用例子来说明一下 700 | 701 | 702 | 703 | __Example__ 704 | 705 | ```js 706 | // emit all listeners named `begin` (ignore namespace) 707 | stateman.emit('begin') 708 | // emit all listeners named `begin`, and with namespace `name1` 709 | .emit('custom:name1') 710 | 711 | ``` 712 | 713 | 714 | ## 理解Routing 715 | 716 | 717 | 718 | ### Routing 生命周期 719 | 720 | 721 | > 722 | 723 | There are three stages in one routing. 724 | 725 | - permission 726 | - navigation 727 | - completion 728 | 729 | let's talk about `navigation` first. 730 | 731 | 732 | 733 | #### navigation: enter, leave , update: 734 | 735 | 736 | `enter`, `update` and `leave`是生命周期中最重要的三个时期, 会在这个stata被进入、离开和更新时被调用 737 | 738 | 739 | 740 | __Example__: 741 | 742 | 743 | 744 | 假设当前状态为`app.contact.detail.setting`, 当我们跳转到 `app.contact.message`. 完整的动作是 745 | 746 | 747 | 1. leave: app.contact.detail.setting 748 | 2. leave: app.contact.detail 749 | 3. update: app.contact 750 | 4. update: app 751 | 5. enter: app.contact.message 752 | 753 | 754 | 755 | 你可以直接在这里页面来查看完整过程: [api.html](./example/api.html); 756 | 757 | 基本上,这里没有难度去理解`enter` 和 `leave`方法,但是`update`何时被调用呢? 758 | 759 | 先看下我们文章开始的[【例子】](./example/api.html)中定义的`app.contact.detail.setting`. 当我们从 `/app/contact/3/setting`跳转到`app/contact/2/setting`时,实际上stateman的当前状态并没有变化, 都是`app.contact.detail.setting`, 但是参数id改变了,这时我们称之为update, 所有被当前状态包含的状态(但没被enter和leave)都会运行update方法. 760 | 761 | 762 | 763 | 764 | 765 | #### permission: canEnter canLeave 766 | 767 | 768 | 有时候, 我们需要在跳转真正开始前阻止它, 一种解决方案是使用[`begin`](#event) 769 | 770 | 771 | ```js 772 | stateman.on('begin', function(option){ 773 | if( option.current.name === 'app.user' && !isUser){ 774 | option.stop() 775 | } 776 | 777 | }) 778 | ``` 779 | 780 | 781 | 在版本0.2之后, stateman提供了一种额外的方法, 可供每个状态自己控制是否允许被跳入或跳出, 我们称之为 __"请求权限"__的过程,这个过程发生在跳转(`enter`, `leave`)之前 782 | 783 | 784 | ```js 785 | stateman.state('app.user',{ 786 | 'canEnter': function(){ 787 | return !!isUser; 788 | } 789 | }) 790 | 791 | ``` 792 | 793 | 794 | 在上面的例子中,如果`false`被返回了, 则此跳转会被,并恢复之前的url. 795 | 796 | __你当然也可以使用[Promise](#control) 来实现异步的流程控制__ 797 | 798 | 现在扩展在[navigation](#navigation)中提到的例子,当你从 `app.contact.detail.setting` 跳转到 `app.contact.message`. 现在完整的流程是 799 | 800 | 801 | 802 | 1. __canLeave: app.contact.detail.setting__ 803 | 2. __canLeave: app.contact.detail__ 804 | 3. __canEnter: app.contact.message__ 805 | 4. leave: app.contact.detail.setting 806 | 5. leave: app.contact.detail 807 | 6. update: app.contact 808 | 7. update: app 809 | 8. enter: app.contact.message 810 | 811 | 812 | 813 | 如果任何一个过程没有被定义, 它会被忽略, 所以都是可选的, 我们只需要实现我们跳转逻辑中需要实现的方法. 814 | 815 | 816 | 817 | 818 | 819 | ### Routing 控制 820 | 821 | 822 | Stateman 提供几种方式来帮助你实现 __异步或同步__ 的跳转控制. 你可以在[lifecyle.html](./example/lifecycle.html)查找到当前的页面. 823 | 824 | 825 | 826 | 827 | #### [__Promise__](#Promise) 828 | 829 | 830 | 如果你的运行环境有Promise支持(浏览器或引入符合规范的Promise/A+的polyfill), 建议你使用Promise控制. 831 | 832 | 833 | __Example__ 834 | 835 | ```js 836 | 837 | var delayBlog = false; 838 | stateman.state('app.blog',{ 839 | // ... 840 | 'enter': function(option){ 841 | delayBlog = true; 842 | 843 | return new Promise(function(resolve, reject){ 844 | console.log('get into app.blog after 3s') 845 | setTimeout(function(){ 846 | delayBlog = false; 847 | resolve(); 848 | }, 3000) 849 | }) 850 | } 851 | // ... 852 | }) 853 | 854 | ``` 855 | 856 | 857 | 858 | 如果Promise对象被reject 或 resolve(false), 跳转会直接停止, 如果还在`askForPermission`阶段,url也会被恢复 859 | 860 | 861 | 862 | 863 | #### 返回值控制 864 | 865 | 866 | 你可以在`enter`, `leave` 等函数中返回`false`来__同步的__阻止这次跳转 867 | 868 | 869 | ```js 870 | stateman.state('app.blog',{ 871 | // ... 872 | 'canLeave': function(){ 873 | 874 | if(delayBlog){ 875 | return confirm('blog is pending, want to leave?') 876 | } 877 | 878 | } 879 | // ... 880 | }) 881 | ``` 882 | 883 | 884 | #### `option.async` 885 | 886 | 887 | stateman 没有与任何promise的polyfill 绑定, 如果你没有在旧版本浏览器主动引入Promise的垫片, 你也可以使用`option.async`来实现同样的功能 888 | 889 | 890 | 891 | 892 | 893 | 894 | ### Routing Option 895 | 896 | 897 | 898 | `enter`,`leave`,`update`, `canEnter` 和 `canLeave` 都接受一个称之为 __Routing Option__ 的参数. 这个参数同时也会传递事件 `begin` 和 `end`. 899 | 这个option和你传入go 和 nav的option 是同一个引用, 不过stateman让它携带了其它重要信息 900 | 901 | 902 | 903 | 904 | ```js 905 | 906 | stateman.state({ 907 | 'app': { 908 | enter: function( option ){ 909 | console.log(option)// routing option 910 | } 911 | } 912 | }) 913 | 914 | ``` 915 | 916 | 917 | __option__ 918 | 919 | |Property|Type|Detail| 920 | |--|--|--| 921 | |option.phase| String| 跳转所处阶段| 922 | |option.param| Object| 捕获到的参数| 923 | |option.previous| State| 跳转前的状态| 924 | |option.current| State| 要跳转到的状态| 925 | |option.async| Function| 异步跳转的一种补充方式, 建议使用Promise| 926 | |option.stop | Function| 是否阻止这次事件| 927 | 928 | #### 0. option.phase 929 | 930 | 931 | 代表现在跳转进行到了什么阶段. 现在跳转分为三个阶段 932 | 933 | - permission: 请求跳转阶段 934 | - navigation: 跳转阶段 935 | - completion: 完成 936 | 937 | 938 | 939 | 940 | 941 | #### 1. option.async 942 | 943 | 944 | 如果你需要在不支持Promise的环境(同时没有手动添加任何Promise的Polyfill), 你需要option.async来帮助你实现异步的跳转 945 | 946 | 947 | 948 | __Return __ 949 | 950 | 951 | 返回一个释放当前装状态的函数 952 | 953 | 954 | 955 | ```js 956 | 957 | "app.user": { 958 | enter: function(option){ 959 | var resolve = option.async(); 960 | setTimeout(resolve, 3000); 961 | } 962 | } 963 | 964 | ``` 965 | 966 | 967 | 968 | 这个返回的`resolve`函数非常接近Promise中的`resolve`函数, 如果你传入false, 跳转会被终止 969 | 970 | 971 | 972 | ```js 973 | 974 | "app.user": { 975 | canEnter: function(option){ 976 | var resolve = option.async(); 977 | resolve(false); 978 | } 979 | } 980 | 981 | ``` 982 | 983 | > `false` is a special signal used for rejecting a state throughout this guide. 984 | 985 | 986 | 987 | 988 | #### 1. option.current 989 | 990 | 目标state 991 | 992 | #### 2. option.previous 993 | 994 | 上任state 995 | 996 | #### 3. option.param: see [Routing Params](#param) 997 | 998 | #### 4. option.stop 999 | 1000 | 1001 | 手动结束这次跳转, 一般你可能会在begin事件中使用它. 1002 | 1003 | 1004 | 1005 | 1006 | 1007 | ### Routing 参数 1008 | 1009 | ####1. 命名参数但是未指定捕获pattern. 1010 | 1011 | 1012 | __Example__ 1013 | 1014 | 1015 | 1016 | 捕获url`/contact/:id`可以匹配路径`/contact/1`, 并得到param`{id:1}` 1017 | 1018 | 事实上,所有的命名参数的默认匹配规则为`[-\$\w]+`. 当然你可以设置自定规则. 1019 | 1020 | 1021 | 1022 | ####2. 命名参数并指定的规则 1023 | 1024 | 1025 | 1026 | 命名参数紧接`(RegExp)`可以限制此参数的匹配(不要再使用子匹配). 例如 1027 | 1028 | 1029 | 1030 | now , only the number is valid to match the id param of `/contact/:id(\\d+)` 1031 | 1032 | 1033 | ####3. 未命名的匹配规则 1034 | 1035 | 1036 | 1037 | 你可以只捕获不命名 1038 | 1039 | 1040 | 1041 | __Example__ 1042 | 1043 | ```sh 1044 | /contact/(friend|mate)/:id([0-9])/(message|option) 1045 | ``` 1046 | 1047 | 1048 | 这个捕获路径可以匹配`/contact/friend/4/message` 并获得参数 `{0: "friend", id: "4", 1: "message"}` 1049 | 1050 | 如你所见,未命名参数会以自增+1的方式放置在参数对象中. 1051 | 1052 | 1053 | 1054 | #### 4. param in search 1055 | 1056 | 1057 | 你当然也可以设置querystring . 以 `/contact/:id`为例. 1058 | 1059 | 1060 | 1061 | 输入`/contact/1?name=heloo&age=1`, 最终你会获得参数`{id:'1', name:'heloo', age: '1'}`. 1062 | 1063 | 1064 | 1065 | #### 5. 隐式 param 1066 | 1067 | 1068 | 1069 | 就像使用POST HTTP 请求一样, 参数不会显示在url上. 同样的, 利用一些小技巧可以让你在stateman实现隐式的参数传递. 换句话说, 你也可以传递非字符串型的参数了。 1070 | 1071 | 1072 | 1073 | __Example__ 1074 | 1075 | ```js 1076 | 1077 | stateman.state('app.blog', { 1078 | enter: function(option){ 1079 | console.log(option.blog.title) 1080 | } 1081 | }) 1082 | 1083 | stateman.go('app.blog', { 1084 | blog: {title: 'blog title', content: 'content blog'} 1085 | }) 1086 | 1087 | ``` 1088 | 1089 | Any properties kept in option except `param` won't be showed in url. 1090 | 1091 | 1092 | 1093 | ### Routing 事件 1094 | 1095 | #### begin 1096 | 1097 | 1098 | 每当跳转开始时触发, 回调会获得一个特殊的参数`evt`用来控制跳转. 1099 | 1100 | 1101 | 1102 | 1103 | Because the navigating isn't really start, property like `previous`, `current` and `param` haven't been assigned to stateman. 1104 | 1105 | __Tips__ 1106 | 1107 | 你可以通过注册begin事件来阻止某次跳转 1108 | 1109 | 1110 | ```js 1111 | stateman.on("begin", function(evt){ 1112 | if(evt.current.name === 'app.contact.message' && evt.previous.name.indexOf("app.user") === 0){ 1113 | evt.stop(); 1114 | alert(" nav from 'app.user.*' to 'app.contact.message' is stoped"); 1115 | } 1116 | }) 1117 | 1118 | ``` 1119 | 1120 | 1121 | 将上述代码复制到[http://leeluolee.github.io/stateman/api.html#/app/user](./example/api.html#/app/user).并点击 `app.contact.message`. 你会发现跳转被终止了. 1122 | 1123 | 1124 | 1125 | #### end 1126 | 1127 | Emitted when a navigating is end. 1128 | 1129 | ``` 1130 | stateman.on('end', function(option){ 1131 | console.log(option.phase) // the phase, routing was end with 1132 | }) 1133 | ``` 1134 | 1135 | see [Option](#option) for other parameter on option. 1136 | 1137 | 1138 | #### notfound: 1139 | 1140 | Emitted when target state is not founded. 1141 | 1142 | __Tips__ 1143 | 1144 | 你可以监听notfound事件,来将页面导向默认状态 1145 | 1146 | 1147 | __Example__ 1148 | 1149 | ```js 1150 | 1151 | stateman.on("notfound", function(){ 1152 | this.go("app.contact"); 1153 | }) 1154 | ``` 1155 | 1156 | 1157 | ## 其它属性 1158 | 1159 | Some living properties. 1160 | 1161 | 1162 | ### __stateman.current__: 1163 | 1164 | The target state. the same as option.current 1165 | 1166 | 1167 | ### __stateman.previous__: 1168 | 1169 | The previous state. the same as option.previous 1170 | 1171 | 1172 | ### __stateman.active__: 1173 | 1174 | The active state, represent the state that still in pending. 1175 | 1176 | Imagine that you are navigating from __'app.contact.detail'__ to __'app.user'__, __current__ will point to `app.user` and __previous__ will point to 'app.contact.detail'. But the active state is dynamic, it is changed from `app.contact.detail` to `app.user`. 1177 | 1178 | __example__ 1179 | 1180 | ```javascript 1181 | var stateman = new StateMan(); 1182 | 1183 | var config = { 1184 | enter: function(option){ console.log("enter: " + this.name + "; active: " + stateman.active.name )}, 1185 | leave: function(option){ console.log("leave: " + this.name + "; active: " + stateman.active.name) } 1186 | } 1187 | 1188 | function cfg(o){ 1189 | o.enter = o.enter || config.enter 1190 | o.leave = o.leave || config.leave 1191 | o.update = o.update || config.update 1192 | return o; 1193 | } 1194 | 1195 | 1196 | stateman.state({ 1197 | 1198 | "app": config, 1199 | "app.contact": config, 1200 | "app.contact.detail": config, 1201 | "app.user": config 1202 | 1203 | }).start(); 1204 | 1205 | 1206 | ``` 1207 | 1208 | 1209 | 1210 | 1211 | 4. __stateman.param__: 1212 | 1213 | The current param captured from url or be passed to the method __stateman.go__. 1214 | 1215 | __Example__ 1216 | 1217 | ```js 1218 | 1219 | stateman.nav("app.detail", {}) 1220 | 1221 | ``` 1222 | 1223 | 1224 | 1225 | 1226 | 1227 | 1228 | ## Class: State 1229 | 1230 | you can use `stateman.state(stateName)` to get the target state. each state is instanceof `StateMan.State`. the context of the methods you defined in config(`enter`, `leave`, `update`) is belongs to state. 1231 | 1232 | ```js 1233 | 1234 | var state = stataeman.state("app.contact.detail"); 1235 | state.name = "app.contact.detail" 1236 | 1237 | ``` 1238 | 1239 | __ state's properties __ 1240 | 1241 | 1. state.async (REMOVED!) : use option.async instead 1242 | 2. state.name [String]: the state's stateName 1243 | 3. state.visited [Boolean]: whether the state have been entered. 1244 | 4. state.parent [State or StateMan]: state's parent state.for example, the parent of 'app.user.list' is 'app.user'. 1245 | 5. state.manager [StateMan]: represent the stateman instance; 1246 | 1247 | 1248 | 1249 | 1250 | -------------------------------------------------------------------------------- /docs/pages/document/API_v0.2-zh.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | > 微量的api在0.2版本有所修改 4 | 5 | 6 | ## 0.2.x的改进 7 | 8 | 9 | - 增加了一个[askForPermission](#permission), 来帮助我们阻止一次跳转(比如在离开时, 加个通知用户是否要保存) 10 | - 现在你可以在`enter`, `leave` 以及新增加的 `canLeave`, `canEnter` 方法中来返回Promise对象, 这对于一些异步的跳转非常有帮助 11 | - 事件现在支持[命名空间](#event)了 12 | - 移除了[state.async] 方法, 如果你的运行环境不支持Promise, 你仍然可以使用 `option.async` 来获得一样的效果 13 | 14 | 15 | # StateMan 文档 16 | 17 | 18 | 19 | 为了更好的理解这不算多的API, 在文档开始前,假设我们已经配置了这样一段路由脚本. 20 | 21 | 22 | 23 | ```js 24 | 25 | var config = { 26 | enter: function(option){ 27 | console.log("enter: " + this.name + "; param: " + JSON.stringify(option.param)) 28 | }, 29 | leave: function(option){ 30 | console.log("leave: " + this.name + "; param: " + JSON.stringify(option.param)) 31 | }, 32 | update: function(option){ 33 | console.log("update: " + this.name + "; param: " + JSON.stringify(option.param)) 34 | }, 35 | } 36 | 37 | function cfg(o){ 38 | o.enter = o.enter || config.enter 39 | o.leave = o.leave || config.leave 40 | o.update = o.update || config.update 41 | return o; 42 | } 43 | 44 | var stateman = new StateMan(); 45 | 46 | stateman.state({ 47 | 48 | "app": config, 49 | "app.contact": config, 50 | "app.contact.detail": cfg({url: ":id(\\d+)"}), 51 | "app.contact.detail.setting": config, 52 | "app.contact.message": config, 53 | "app.user": cfg({ 54 | enter: function( option ){ 55 | var done = option.async(); 56 | console.log(this.name + "is pending, 1s later to enter next state") 57 | setTimeout(done, 1000) 58 | }, 59 | leave: function( option ){ 60 | var done = option.async(); 61 | console.log(this.name + "is pending, 1s later to leave out") 62 | setTimeout(done, 1000) 63 | } 64 | }), 65 | "app.user.list": cfg({url: ""}) 66 | 67 | }).on("notfound", function(){ 68 | this.go('app') // if not found 69 | }).start(); 70 | 71 | ``` 72 | 73 | 74 | 对象`config`用来输出navgating的相关信息, 你不需要立刻理解这个例子, 稍后文档会慢慢告诉你一切. 75 | 76 | 你可以直接通过【[在线DEMO](./example/api.html)】 访问到这个例子, 有时候试着在console中测试API可以帮助你更好的理解它们 77 | 78 | 79 | 80 | 81 | ## API 82 | 83 | ### new StateMan 84 | 85 | __Usage__ 86 | 87 | `new StateMan(option)` 88 | 89 | __Arguments__ 90 | 91 | |Param|Type|Detail| 92 | |--|--|--| 93 | |option.strict|Boolean| Default: false . 是否只有叶子节点可以被访问到 | 94 | |option.title| String| 设置文档标题, 见 [config.title](#title)| 95 | 96 | 97 | __Return__ 98 | 99 | [Stateman] : StateMan的实例 100 | 101 | __Example__ 102 | 103 | ```javascript 104 | var StateMan = require("stateman"); 105 | 106 | var stateman = new StateMan({ 107 | title: "Application", 108 | strict: true 109 | }); 110 | // or... 111 | var stateman = StateMan(); 112 | ``` 113 | 114 | 如果strict为true, 会导致上例的 `app.contact`等状态不能被直接定位(即无法成为stateman.current了). 只有像`app.contact.message` 这样的叶子节点可以被直接访问. 115 | 116 | 117 | ### stateman.state 118 | 119 | __Usage__ 120 | 121 | `stateman.state(stateName[, config])` 122 | 123 | 124 | stateman.state 用来增加/更新一个state, 或获取指定的state对象(假如 config 参数没有指定的话) 125 | 126 | 127 | 128 | __Arguments__ 129 | 130 | |Param|Type|Detail| 131 | |--|--|--| 132 | |stateName|String Object| state名称,假如传入了一个对象,将成为一个多重设置| 133 | |config(optional)|Function Object| 如果config没有指定,将会返回指定的state, 假如传入的是一个函数,则相当于传入了`config.enter`, 如果指定的state已经存在,原设置会被覆盖 | 134 | 135 | __Return__ : 136 | 137 | StateMan or State (if config is not passed) 138 | 139 | 140 | __Example__ 141 | 142 | ```js 143 | 144 | stateman 145 | .state("app", { 146 | enter: function(){ 147 | console.log("enter the state: app") 148 | }, 149 | leave: function(){ 150 | console.log("leave the state: app") 151 | } 152 | }) 153 | // is equals to {enter: config} 154 | .state("app.contact", function(){ 155 | console.log("enter the app.contact state") 156 | }) 157 | 158 | // pass in a Object for multiple operation 159 | stateman.state({ 160 | "demo.detail": function(){ 161 | console.log("enter the demo.detail state") 162 | }, 163 | "demo.list": { 164 | enter: function(){} 165 | leave: function(){} 166 | } 167 | }) 168 | 169 | 170 | ``` 171 | 172 | 173 | 诚如你所见,我们并不需要在定义`demo.detail`之前定义`demo`, stateman会帮你创建中间state 174 | 175 | 如果config参数没有传入,则会返回对应的state对象 176 | 177 | 178 | ```js 179 | 180 | // return the demo.list state 181 | var state = stateman.state('demo.list'); 182 | 183 | ``` 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | ### > 关于`config` 192 | 193 | 194 | 所有config中的属性都会成为对应state的实例属性,但在这里, 需要对一些特殊的属性做说明 195 | 196 | 197 | 198 | #### lifecycle related 199 | 200 | 201 | 202 | 这里有五个生命周期相关的方法可供你用来控制 路由的逻辑, 它们都是可选择实现的 , 查看[生命周期](#lifecycle)了解更多 203 | 204 | * __config.enter(option)__: 一个函数,当状态被__进入时__会被调用 205 | * __config.leave(option)__: 一个函数,当状态被__离开时__会被调用 206 | * __config.update(option)__: 一个函数,当状态__更新时__会被调用, 更新的意思是,路径有变化,但是此状态仍未被退出. 207 | * __config.canEnter(option)__: 请求是否可进入 208 | * __config.canLeave(option)__: 请求是否可退出 209 | 210 | 211 | 212 | 213 | 214 | #### config.url: 215 | 216 | `url` 属性用来配置state(非全路径)的url片段 217 | 218 | 219 | 220 | 每个state对应的捕获url是所有在到此状态路径上的state的结合. 比如`app.contact.detail` 是 `app`,`app.contact` 和`app.contact.detail`的路径结合 221 | 222 | 223 | 224 | 225 | __Example__ 226 | 227 | ```js 228 | 229 | state.state("app", {}) 230 | .state("app.contact", "users") 231 | .state("app.contact.detail", "/:id") 232 | 233 | ``` 234 | 235 | 236 | `app.contact.detail`的完整捕获路径就是`/app/users/:id`. 当然,如你所见, 我们可以在url中定义我们的[路径参数](#param) 237 | 238 | 239 | 240 | 241 | 242 | 243 | __ 绝对路径__ 244 | 245 | 如果你不需要父级的url定义,你可以使用`^`符号来使当前状态的url 246 | 247 | 248 | 249 | ```js 250 | state.state("app.contact.detail", "^/detail/:id"); 251 | state.state("app.contact.detail.message", "message"); 252 | ``` 253 | 254 | 255 | 这样`app.contact.detail`的路径会直接变成 `/detail/:id`,子状态会被影响到也变为`/detail/:id/message` 256 | 257 | 258 | 259 | 260 | 261 | __空url__: 放弃当前这级的路径配置 262 | 263 | 如果你传入`""`, 你会放弃当前url配置, 这样你的捕获路径会与父状态一致(不过匹配优先级更高) 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | #### config.title 273 | 274 | 275 | 一旦跳转结束, 我们可以控制标签栏的title值(当然 你可以在enter, update, leave中来更精确的手动使用document.title来设置) 276 | 277 | 278 | 279 | __Argument__ 280 | 281 | - config.title [String or Function]: 如果是函数,document会设置成其返回值 282 | 283 | __Example__ 284 | 285 | ``` 286 | stateman.state({ 287 | "app": { 288 | title: "APP" 289 | }, 290 | "app.test": { 291 | title: "App test" 292 | }, 293 | "app.exam": { 294 | url: "exam/:id", 295 | title: function(){ 296 | return "Exam " + stateman.param.id 297 | } 298 | }, 299 | "app.notitle": {} 300 | }) 301 | 302 | stateman.go("app.test"); 303 | 304 | // document.title === "App test" 305 | 306 | stateman.nav("/app/test/1"); 307 | 308 | // document.title === "Exam 1" 309 | 310 | stateman.nav("/app/notitle"); 311 | 312 | // document.title === "App" 313 | ``` 314 | 315 | 316 | 正如你所见, 如果当前状态的title没有配置,stateman会去找它父状态的title, 直到stateman本身为止. 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | ### stateman.start 327 | 328 | 启动stateman, 路由开始 329 | 330 | __Usage__ 331 | 332 | `stateman.start(option)` 333 | 334 | __option__ 335 | 336 | 337 | |Param|Type|Detail| 338 | |--|--|--| 339 | |html5 |Boolean|(default false) 是否开启html5支持, 即使用pushState等API代替hash设置| 340 | |root |String|(default '/') 程序根路径,影响到你使用html5模式的表现| 341 | |prefix| String | 配置hashban, 例如你可以传入'!'来达到`#!/contact/100`的hash表现| 342 | |autolink| Boolean | (defualt true) 是否代理所有的`#`开始的链接来达到跳转, 只对html5下有用| 343 | 344 | 345 | __Example__ 346 | 347 | ```js 348 | stateman.start({ 349 | "html5": true, 350 | "prefix": "!", 351 | "root": "/blog" //the app is begin with '/blog' 352 | }) 353 | 354 | ``` 355 | 356 | __Warning__ 357 | 358 | 359 | 如果你在不支持html5 pushState的浏览器开启了`html=true`, stateman会自动降级到hash的路由. 360 | 361 | 362 | 363 | 364 | 就如同上例的配置. 365 | 366 | 1. 如果我们在不支持html5(pushState相关)的浏览器访问`/blog/app`, stateman会降级到hash的路由,并自动定向到`/blog#/app`的初始状态. 367 | 368 | 2. 如果我们在__支持html5__的浏览器范围__`/blog#/app`__, stateman同样会使用history的路由路径,并自动返回到 `/blog/app`的初始状态. 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | ### stateman.nav 378 | 379 | 380 | 跳转到指定路径,[url中匹配到的参数](#param)会合并到option, 并最终传给之前提到的`enter`, `leave`, `update`函数. 381 | 382 | 383 | 384 | __Usage__ 385 | 386 | `stateman.nav(url[, option][, callback])`; 387 | 388 | 389 | 390 | __Argument__ 391 | 392 | |Param|Type|Detail| 393 | |--|--|--| 394 | |url |String| 跳转url| 395 | |option(optional) |Object| [路由option](#option). url参数会作为option的param参数. | 396 | |callback(optional)|Function| 跳转结束后,此函数会被调用| 397 | 398 | 399 | __control option__ 400 | 401 | 402 | 403 | * option.silent: 如果传入silent, 则只有url路径会发生改变,但是不触发stateman内部的状态改变, 即不会有enter, leave或updatec触发 404 | * option.replace: 如果replace === true, 之前历史的记录会被当前替换,即你无法通过浏览器的后退,回到原状态了 405 | 406 | 407 | 408 | 409 | __Example__ 410 | 411 | `stateman.nav("/app/contact/1?name=leeluolee", {data: 1}); ` 412 | 413 | 414 | 415 | 最终传入到enter, leave与update的option参数会是 416 | 417 | 418 | 419 | 420 | 421 | `{param: {id: "1", name:"leeluolee"}, data: 1}`. 422 | 423 | 424 | 425 | 426 | 427 | 428 | ### stateman.go 429 | 430 | 431 | 跳转到特定状态, 与 stateman.nav 非常相似,但是stateman.go 使用状态来代替url路径进行跳转 432 | 433 | 434 | 435 | __Usage__ 436 | 437 | `stateman.go(stateName [, option][, callback])`; 438 | 439 | 440 | __Arguments__ 441 | 442 | - stateName [String]: 目标状态 443 | 444 | - option [Object]: [Routing Option](#option) 445 | 446 | - option.encode: 447 | 448 | 默认是true, 如果encode是false. 则地址栏的url将不会发生变化,仅仅只是触发了内部状态的跳转. 当encode为true时, stateman会使用[encode](#encode) 函数去反推出真实的url路径显示在location中. 449 | 450 | - option.param: 451 | 452 | nav与go的最大区别就在于param参数 453 | 454 | 如果你的路径中带有参数,则需要传入param来帮助encode函数推算出url路径 455 | 456 | you can use [stateman.encode](#encode) to test how stateman compute url from a state with specifed param 457 | 458 | - option.replace: 见[stateman.nav](#nav) 459 | 460 | - calback [Function]: 同nav 461 | 462 | 463 | 所有其它的option属性将会与param一起传入 `enter`, `leave` , `update`中. 464 | 465 | 466 | 467 | __Example__ 468 | 469 | ``` 470 | stateman.go('app.contact.detail', {param: {id:1, name: 'leeluolee'}}); 471 | ``` 472 | 473 | 474 | 地址会跳转到`#/app/contact/1?name=leeluolee`, 你可以发现未命名参数会直接拼接在url后方作为queryString 存在. 475 | 476 | 477 | 478 | __Tips__: 479 | 480 | 481 | 作者始终推荐在大型项目中使用go代替nav来进行跳转, 来获得更灵活安全的状态控制. 482 | 483 | 484 | 485 | 486 | 487 | 488 | __相对跳转__: 489 | 490 | 491 | stateman预定义了一些符号帮助你进行相对路径的跳转 492 | 493 | 494 | 495 | 1. "~": 代表当前所处的active状态 496 | 2. "^": 代表active状态的父状态; 497 | 498 | __example__ 499 | 500 | ```js 501 | stateman.state({ 502 | "app.user": function(){ 503 | stateman.go("~.detail") // will navigate to app.user.detail 504 | }, 505 | "app.contact.detail": function(){ 506 | stateman.go("^.message") // will navigate to app.contact.message 507 | } 508 | 509 | }) 510 | 511 | ``` 512 | 513 | 514 | 515 | 516 | ### stateman.is 517 | 518 | __Usage__ 519 | 520 | `stateman.is( stateName[, param] [, isStrict] )` 521 | 522 | 523 | 判断当前状态是否满足传入的stateName. 如果有param 参数传入,则除了状态,param也必须匹配. 你不必传入所有的参数, is 只会判断你传入的参数是否匹配. 524 | 525 | 526 | 527 | __Arguments__ 528 | 529 | |Param|Type|Detail| 530 | |--|--|--| 531 | |stateName |String| 用测试的stateName | 532 | |param(optional)|Object| 用于测试的参数对象| 533 | |isStrict(optional)|Boolean| 传入状态是否要严格匹配当前状态| 534 | 535 | 536 | 537 | __example__ 538 | 539 | ```js 540 | stateman.nav("#/app/contact/1?name=leeluolee"); 541 | stateman.is("app.contact.detail") // return true 542 | stateman.is("app.contact", {}, true) // return false, 543 | stateman.is("app.contact.detail", {name: "leeluolee"}, true) // return true 544 | stateman.is("app.contact.detail", {id: "1"}) // return true 545 | stateman.is("app.contact.detail", {id: "2"}) // return false 546 | stateman.is("app.contact.detail", {id: "2", name: "leeluolee"}) // return false 547 | ``` 548 | 549 | 550 | ### stateman.encode 551 | 552 | 553 | 根据状态名和参数获得指定url. 554 | 555 | go函数即基于此方法 556 | 557 | 558 | 559 | 560 | 561 | __Usage__ 562 | 563 | `stateman.encode( stateName[, param] )` 564 | 565 | 566 | __Arguments__ 567 | 568 | 569 | |Param|Type|Detail| 570 | |--|--|--| 571 | |stateName |String| stateName | 572 | |param(optional)|Object| 用于组装url的参数对象| 573 | 574 | 575 | ```js 576 | stateman.encode("app.contact.detail", {id: "1", name: "leeluolee"}) 577 | // === "/app/contact/1?name=leeluolee" 578 | 579 | ``` 580 | 581 | 582 | ### stateman.decode 583 | 584 | 585 | 解码传入的url, 获得匹配的状态,状态同时会带上计算出的参数对象 586 | 587 | 方法[__nav__](#nav) 就是基于此方法实现 588 | 589 | 590 | __Usage__ 591 | 592 | `stateman.decode( url )` 593 | 594 | 595 | __Example__ 596 | 597 | ```js 598 | var state = stateman.decode("/app/contact/1?name=leeluolee") 599 | 600 | state.name === 'app.contact.detail' 601 | state.param // =>{id: "1", name: "leeluolee"} 602 | 603 | ``` 604 | 605 | 606 | 607 | ### stateman.stop 608 | 609 | __Usage__ 610 | 611 | `stateman.stop()` 612 | 613 | stop the stateman. 614 | 615 | 616 | 617 | 618 | 619 | ### stateman.on 620 | 621 | 为指定函数名添加监听器 622 | 623 | 624 | 625 | __Usage__ 626 | 627 | `stateman.on(event, handle)` 628 | 629 | 630 | StateMan内置了一个小型Emitter 来帮助实现事件驱动的开发, 在 [Routing Event](#event) 中查看内建事件 631 | 632 | 633 | 634 | 635 | you 可以使用 `[event]:[namespace]`的格式区创建一个带有namespace的事件 636 | 637 | 638 | __Example__ 639 | 640 | ``` 641 | stateman 642 | .on('begin', beginListener) 643 | // there will be a multiply binding 644 | .on({ 645 | 'end': endListener, 646 | 'custom': customListener, 647 | 'custom:name1': customListenerWithNameSpace 648 | }) 649 | ``` 650 | 651 | 652 | 653 | 654 | ### stateman.off 655 | 656 | 解绑一个监听器 657 | 658 | 659 | __Usage__ 660 | 661 | `stateman.off(event, handle)` 662 | 663 | 664 | __Example__ 665 | 666 | 667 | 这里有多种参数可能 668 | 669 | 670 | 671 | ```js 672 | // unbind listener with specified handle 673 | stateman.off('begin', beginListener ) 674 | // unbind all listener whose eventName is custom and namespace is name1 675 | .off('custom:name1') 676 | // unbind listener whose name is 'custom' (ignore namespace) 677 | .off('custom') 678 | // clear all event bindings of stateman 679 | .off() 680 | ``` 681 | 682 | 683 | 684 | 685 | ### stateman.emit 686 | 687 | 688 | 触发一个事件,你可以传入对应的参数 689 | 690 | 691 | 692 | 693 | __Usage__ 694 | 695 | `stateman.emit(event, param)` 696 | 697 | 698 | 699 | 与stateman.off类似, namespace会影响函数的表现, 我们用例子来说明一下 700 | 701 | 702 | 703 | __Example__ 704 | 705 | ```js 706 | // emit all listeners named `begin` (ignore namespace) 707 | stateman.emit('begin') 708 | // emit all listeners named `begin`, and with namespace `name1` 709 | .emit('custom:name1') 710 | 711 | ``` 712 | 713 | 714 | ## 理解Routing 715 | 716 | 717 | 718 | ### Routing 生命周期 719 | 720 | 721 | > 722 | 723 | There are three stages in one routing. 724 | 725 | - permission 726 | - navigation 727 | - completion 728 | 729 | let's talk about `navigation` first. 730 | 731 | 732 | 733 | #### navigation: enter, leave , update: 734 | 735 | 736 | `enter`, `update` and `leave`是生命周期中最重要的三个时期, 会在这个stata被进入、离开和更新时被调用 737 | 738 | 739 | 740 | __Example__: 741 | 742 | 743 | 744 | 假设当前状态为`app.contact.detail.setting`, 当我们跳转到 `app.contact.message`. 完整的动作是 745 | 746 | 747 | 1. leave: app.contact.detail.setting 748 | 2. leave: app.contact.detail 749 | 3. update: app.contact 750 | 4. update: app 751 | 5. enter: app.contact.message 752 | 753 | 754 | 755 | 你可以直接在这里页面来查看完整过程: [api.html](./example/api.html); 756 | 757 | 基本上,这里没有难度去理解`enter` 和 `leave`方法,但是`update`何时被调用呢? 758 | 759 | 先看下我们文章开始的[【例子】](./example/api.html)中定义的`app.contact.detail.setting`. 当我们从 `/app/contact/3/setting`跳转到`app/contact/2/setting`时,实际上stateman的当前状态并没有变化, 都是`app.contact.detail.setting`, 但是参数id改变了,这时我们称之为update, 所有被当前状态包含的状态(但没被enter和leave)都会运行update方法. 760 | 761 | 762 | 763 | 764 | 765 | #### permission: canEnter canLeave 766 | 767 | 768 | 有时候, 我们需要在跳转真正开始前阻止它, 一种解决方案是使用[`begin`](#event) 769 | 770 | 771 | ```js 772 | stateman.on('begin', function(option){ 773 | if( option.current.name === 'app.user' && !isUser){ 774 | option.stop() 775 | } 776 | 777 | }) 778 | ``` 779 | 780 | 781 | 在版本0.2之后, stateman提供了一种额外的方法, 可供每个状态自己控制是否允许被跳入或跳出, 我们称之为 __"请求权限"__的过程,这个过程发生在跳转(`enter`, `leave`)之前 782 | 783 | 784 | ```js 785 | stateman.state('app.user',{ 786 | 'canEnter': function(){ 787 | return !!isUser; 788 | } 789 | }) 790 | 791 | ``` 792 | 793 | 794 | 在上面的例子中,如果`false`被返回了, 则此跳转会被,并恢复之前的url. 795 | 796 | __你当然也可以使用[Promise](#control) 来实现异步的流程控制__ 797 | 798 | 现在扩展在[navigation](#navigation)中提到的例子,当你从 `app.contact.detail.setting` 跳转到 `app.contact.message`. 现在完整的流程是 799 | 800 | 801 | 802 | 1. __canLeave: app.contact.detail.setting__ 803 | 2. __canLeave: app.contact.detail__ 804 | 3. __canEnter: app.contact.message__ 805 | 4. leave: app.contact.detail.setting 806 | 5. leave: app.contact.detail 807 | 6. update: app.contact 808 | 7. update: app 809 | 8. enter: app.contact.message 810 | 811 | 812 | 813 | 如果任何一个过程没有被定义, 它会被忽略, 所以都是可选的, 我们只需要实现我们跳转逻辑中需要实现的方法. 814 | 815 | 816 | 817 | 818 | 819 | ### Routing 控制 820 | 821 | 822 | Stateman 提供几种方式来帮助你实现 __异步或同步__ 的跳转控制. 你可以在[lifecyle.html](./example/lifecycle.html)查找到当前的页面. 823 | 824 | 825 | 826 | 827 | #### [__Promise__](#Promise) 828 | 829 | 830 | 如果你的运行环境有Promise支持(浏览器或引入符合规范的Promise/A+的polyfill), 建议你使用Promise控制. 831 | 832 | 833 | __Example__ 834 | 835 | ```js 836 | 837 | var delayBlog = false; 838 | stateman.state('app.blog',{ 839 | // ... 840 | 'enter': function(option){ 841 | delayBlog = true; 842 | 843 | return new Promise(function(resolve, reject){ 844 | console.log('get into app.blog after 3s') 845 | setTimeout(function(){ 846 | delayBlog = false; 847 | resolve(); 848 | }, 3000) 849 | }) 850 | } 851 | // ... 852 | }) 853 | 854 | ``` 855 | 856 | 857 | 858 | 如果Promise对象被reject 或 resolve(false), 跳转会直接停止, 如果还在`askForPermission`阶段,url也会被恢复 859 | 860 | 861 | 862 | 863 | #### 返回值控制 864 | 865 | 866 | 你可以在`enter`, `leave` 等函数中返回`false`来__同步的__阻止这次跳转 867 | 868 | 869 | ```js 870 | stateman.state('app.blog',{ 871 | // ... 872 | 'canLeave': function(){ 873 | 874 | if(delayBlog){ 875 | return confirm('blog is pending, want to leave?') 876 | } 877 | 878 | } 879 | // ... 880 | }) 881 | ``` 882 | 883 | 884 | #### `option.async` 885 | 886 | 887 | stateman 没有与任何promise的polyfill 绑定, 如果你没有在旧版本浏览器主动引入Promise的垫片, 你也可以使用`option.async`来实现同样的功能 888 | 889 | 890 | 891 | 892 | 893 | 894 | ### Routing Option 895 | 896 | 897 | 898 | `enter`,`leave`,`update`, `canEnter` 和 `canLeave` 都接受一个称之为 __Routing Option__ 的参数. 这个参数同时也会传递事件 `begin` 和 `end`. 899 | 这个option和你传入go 和 nav的option 是同一个引用, 不过stateman让它携带了其它重要信息 900 | 901 | 902 | 903 | 904 | ```js 905 | 906 | stateman.state({ 907 | 'app': { 908 | enter: function( option ){ 909 | console.log(option)// routing option 910 | } 911 | } 912 | }) 913 | 914 | ``` 915 | 916 | 917 | __option__ 918 | 919 | |Property|Type|Detail| 920 | |--|--|--| 921 | |option.phase| String| 跳转所处阶段| 922 | |option.param| Object| 捕获到的参数| 923 | |option.previous| State| 跳转前的状态| 924 | |option.current| State| 要跳转到的状态| 925 | |option.async| Function| 异步跳转的一种补充方式, 建议使用Promise| 926 | |option.stop | Function| 是否阻止这次事件| 927 | 928 | #### 0. option.phase 929 | 930 | 931 | 代表现在跳转进行到了什么阶段. 现在跳转分为三个阶段 932 | 933 | - permission: 请求跳转阶段 934 | - navigation: 跳转阶段 935 | - completion: 完成 936 | 937 | 938 | 939 | 940 | 941 | #### 1. option.async 942 | 943 | 944 | 如果你需要在不支持Promise的环境(同时没有手动添加任何Promise的Polyfill), 你需要option.async来帮助你实现异步的跳转 945 | 946 | 947 | 948 | __Return __ 949 | 950 | 951 | 返回一个释放当前装状态的函数 952 | 953 | 954 | 955 | ```js 956 | 957 | "app.user": { 958 | enter: function(option){ 959 | var resolve = option.async(); 960 | setTimeout(resolve, 3000); 961 | } 962 | } 963 | 964 | ``` 965 | 966 | 967 | 968 | 这个返回的`resolve`函数非常接近Promise中的`resolve`函数, 如果你传入false, 跳转会被终止 969 | 970 | 971 | 972 | ```js 973 | 974 | "app.user": { 975 | canEnter: function(option){ 976 | var resolve = option.async(); 977 | resolve(false); 978 | } 979 | } 980 | 981 | ``` 982 | 983 | > `false` is a special signal used for rejecting a state throughout this guide. 984 | 985 | 986 | 987 | 988 | #### 1. option.current 989 | 990 | 目标state 991 | 992 | #### 2. option.previous 993 | 994 | 上任state 995 | 996 | #### 3. option.param: see [Routing Params](#param) 997 | 998 | #### 4. option.stop 999 | 1000 | 1001 | 手动结束这次跳转, 一般你可能会在begin事件中使用它. 1002 | 1003 | 1004 | 1005 | 1006 | 1007 | ### Routing 参数 1008 | 1009 | ####1. 命名参数但是未指定捕获pattern. 1010 | 1011 | 1012 | __Example__ 1013 | 1014 | 1015 | 1016 | 捕获url`/contact/:id`可以匹配路径`/contact/1`, 并得到param`{id:1}` 1017 | 1018 | 事实上,所有的命名参数的默认匹配规则为`[-\$\w]+`. 当然你可以设置自定规则. 1019 | 1020 | 1021 | 1022 | ####2. 命名参数并指定的规则 1023 | 1024 | 1025 | 1026 | 命名参数紧接`(RegExp)`可以限制此参数的匹配(不要再使用子匹配). 例如 1027 | 1028 | 1029 | 1030 | now , only the number is valid to match the id param of `/contact/:id(\\d+)` 1031 | 1032 | 1033 | ####3. 未命名的匹配规则 1034 | 1035 | 1036 | 1037 | 你可以只捕获不命名 1038 | 1039 | 1040 | 1041 | __Example__ 1042 | 1043 | ```sh 1044 | /contact/(friend|mate)/:id([0-9])/(message|option) 1045 | ``` 1046 | 1047 | 1048 | 这个捕获路径可以匹配`/contact/friend/4/message` 并获得参数 `{0: "friend", id: "4", 1: "message"}` 1049 | 1050 | 如你所见,未命名参数会以自增+1的方式放置在参数对象中. 1051 | 1052 | 1053 | 1054 | #### 4. param in search 1055 | 1056 | 1057 | 你当然也可以设置querystring . 以 `/contact/:id`为例. 1058 | 1059 | 1060 | 1061 | 输入`/contact/1?name=heloo&age=1`, 最终你会获得参数`{id:'1', name:'heloo', age: '1'}`. 1062 | 1063 | 1064 | 1065 | #### 5. 隐式 param 1066 | 1067 | 1068 | 1069 | 就像使用POST HTTP 请求一样, 参数不会显示在url上. 同样的, 利用一些小技巧可以让你在stateman实现隐式的参数传递. 换句话说, 你也可以传递非字符串型的参数了。 1070 | 1071 | 1072 | 1073 | __Example__ 1074 | 1075 | ```js 1076 | 1077 | stateman.state('app.blog', { 1078 | enter: function(option){ 1079 | console.log(option.blog.title) 1080 | } 1081 | }) 1082 | 1083 | stateman.go('app.blog', { 1084 | blog: {title: 'blog title', content: 'content blog'} 1085 | }) 1086 | 1087 | ``` 1088 | 1089 | Any properties kept in option except `param` won't be showed in url. 1090 | 1091 | 1092 | 1093 | ### Routing 事件 1094 | 1095 | #### begin 1096 | 1097 | 1098 | 每当跳转开始时触发, 回调会获得一个特殊的参数`evt`用来控制跳转. 1099 | 1100 | 1101 | 1102 | 1103 | Because the navigating isn't really start, property like `previous`, `current` and `param` haven't been assigned to stateman. 1104 | 1105 | __Tips__ 1106 | 1107 | 你可以通过注册begin事件来阻止某次跳转 1108 | 1109 | 1110 | ```js 1111 | stateman.on("begin", function(evt){ 1112 | if(evt.current.name === 'app.contact.message' && evt.previous.name.indexOf("app.user") === 0){ 1113 | evt.stop(); 1114 | alert(" nav from 'app.user.*' to 'app.contact.message' is stoped"); 1115 | } 1116 | }) 1117 | 1118 | ``` 1119 | 1120 | 1121 | 将上述代码复制到[http://leeluolee.github.io/stateman/api.html#/app/user](./example/api.html#/app/user).并点击 `app.contact.message`. 你会发现跳转被终止了. 1122 | 1123 | 1124 | 1125 | #### end 1126 | 1127 | Emitted when a navigating is end. 1128 | 1129 | ``` 1130 | stateman.on('end', function(option){ 1131 | console.log(option.phase) // the phase, routing was end with 1132 | }) 1133 | ``` 1134 | 1135 | see [Option](#option) for other parameter on option. 1136 | 1137 | 1138 | #### notfound: 1139 | 1140 | Emitted when target state is not founded. 1141 | 1142 | __Tips__ 1143 | 1144 | 你可以监听notfound事件,来将页面导向默认状态 1145 | 1146 | 1147 | __Example__ 1148 | 1149 | ```js 1150 | 1151 | stateman.on("notfound", function(){ 1152 | this.go("app.contact"); 1153 | }) 1154 | ``` 1155 | 1156 | 1157 | ## 其它属性 1158 | 1159 | Some living properties. 1160 | 1161 | 1162 | ### __stateman.current__: 1163 | 1164 | The target state. the same as option.current 1165 | 1166 | 1167 | ### __stateman.previous__: 1168 | 1169 | The previous state. the same as option.previous 1170 | 1171 | 1172 | ### __stateman.active__: 1173 | 1174 | The active state, represent the state that still in pending. 1175 | 1176 | Imagine that you are navigating from __'app.contact.detail'__ to __'app.user'__, __current__ will point to `app.user` and __previous__ will point to 'app.contact.detail'. But the active state is dynamic, it is changed from `app.contact.detail` to `app.user`. 1177 | 1178 | __example__ 1179 | 1180 | ```javascript 1181 | var stateman = new StateMan(); 1182 | 1183 | var config = { 1184 | enter: function(option){ console.log("enter: " + this.name + "; active: " + stateman.active.name )}, 1185 | leave: function(option){ console.log("leave: " + this.name + "; active: " + stateman.active.name) } 1186 | } 1187 | 1188 | function cfg(o){ 1189 | o.enter = o.enter || config.enter 1190 | o.leave = o.leave || config.leave 1191 | o.update = o.update || config.update 1192 | return o; 1193 | } 1194 | 1195 | 1196 | stateman.state({ 1197 | 1198 | "app": config, 1199 | "app.contact": config, 1200 | "app.contact.detail": config, 1201 | "app.user": config 1202 | 1203 | }).start(); 1204 | 1205 | 1206 | ``` 1207 | 1208 | 1209 | 1210 | 1211 | 4. __stateman.param__: 1212 | 1213 | The current param captured from url or be passed to the method __stateman.go__. 1214 | 1215 | __Example__ 1216 | 1217 | ```js 1218 | 1219 | stateman.nav("app.detail", {}) 1220 | 1221 | ``` 1222 | 1223 | 1224 | 1225 | 1226 | 1227 | 1228 | ## Class: State 1229 | 1230 | you can use `stateman.state(stateName)` to get the target state. each state is instanceof `StateMan.State`. the context of the methods you defined in config(`enter`, `leave`, `update`) is belongs to state. 1231 | 1232 | ```js 1233 | 1234 | var state = stataeman.state("app.contact.detail"); 1235 | state.name = "app.contact.detail" 1236 | 1237 | ``` 1238 | 1239 | __ state's properties __ 1240 | 1241 | 1. state.async (REMOVED!) : use option.async instead 1242 | 2. state.name [String]: the state's stateName 1243 | 3. state.visited [Boolean]: whether the state have been entered. 1244 | 4. state.parent [State or StateMan]: state's parent state.for example, the parent of 'app.user.list' is 'app.user'. 1245 | 5. state.manager [StateMan]: represent the stateman instance; 1246 | 1247 | 1248 | 1249 | 1250 | -------------------------------------------------------------------------------- /test/spec/dom-stateman.js: -------------------------------------------------------------------------------- 1 | 2 | // THX for Backbone for some testcase from https://github.com/jashkenas/backbone/blob/master/test/router.js 3 | // to help stateman becoming robust soon. 4 | 5 | // Backbone.js 1.1.2 6 | // (c) 2010-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 7 | // Backbone may be freely distributed under the MIT license. 8 | // For all details and documentation: 9 | // http://backbonejs.org 10 | 11 | var StateMan = require("../../src/stateman.js"); 12 | var expect = require("../runner/vendor/expect.js") 13 | var _ = require("../../src/util.js"); 14 | var doc = typeof document !== "undefined"? document: {}; 15 | 16 | 17 | // Backbone.js Trick for mock the location service 18 | var a = document.createElement('a'); 19 | function loc(href){ 20 | return ({ 21 | replace: function(href) { 22 | a.href = href; 23 | _.extend(this, { 24 | href: a.href, 25 | hash: a.hash, 26 | host: a.host, 27 | fragment: a.fragment, 28 | pathname: a.pathname, 29 | search: a.search 30 | }, true) 31 | if (!/^\//.test(this.pathname)) this.pathname = '/' + this.pathname; 32 | return this; 33 | } 34 | }).replace(href) 35 | } 36 | 37 | 38 | function reset(stateman){ 39 | stateman._states = {}; 40 | stateman.off(); 41 | } 42 | 43 | describe("stateman", function(){ 44 | 45 | describe("Util", function(){ 46 | it("util.eql", function( ){ 47 | expect(_.eql({a:1}, [2,1])).to.be.equal(false); 48 | expect(_.eql({a:1, b:3}, {a:1})).to.be.equal(false); 49 | expect(_.eql({a:1, b:3}, {a:1, b:3})).to.be.equal(true); 50 | expect(_.eql(1, 1)).to.be.equal(true); 51 | }) 52 | it("util.emitable:basic", function(){ 53 | var emitter = _.emitable({}); 54 | var obj = {basic1:0,basic2:0,basic3:0}; 55 | 56 | emitter.on('basic1', function(){ 57 | obj.basic1++ 58 | }) 59 | emitter.on('basic2', function(){ 60 | obj.basic2++ 61 | }) 62 | emitter.on('basic3', function(){ 63 | obj.basic3++ 64 | }) 65 | emitter.emit('basic1'); 66 | expect(obj.basic1).to.equal(1); 67 | emitter.off('basic1') 68 | emitter.emit('basic1'); 69 | expect(obj.basic1).to.equal(1); 70 | emitter.off() 71 | emitter.emit('basic2'); 72 | emitter.emit('basic3'); 73 | expect(obj.basic2).to.equal(0); 74 | expect(obj.basic2).to.equal(0); 75 | 76 | }) 77 | it("util.emitable:namespace", function(){ 78 | var emitter = _.emitable({}); 79 | obj = {enter_app: 0, enter_blog: 0} 80 | 81 | emitter.on('enter:app', function(){ 82 | obj.enter_app++ 83 | }) 84 | emitter.on('enter:blog', function(){ 85 | obj.enter_blog++ 86 | }) 87 | emitter.off('enter:app') 88 | emitter.emit('enter'); 89 | expect(obj.enter_blog).to.equal(1) 90 | expect(obj.enter_app).to.equal(0) 91 | emitter.emit('enter:blog') 92 | expect(obj.enter_blog).to.equal(2) 93 | emitter.off('enter'); 94 | emitter.emit('enter'); 95 | expect(obj.enter_blog).to.equal(2) 96 | }) 97 | }) 98 | 99 | describe("stateman:basic", function(){ 100 | var stateman = StateMan( {} ); 101 | var location = loc("http://leeluolee.github.io/homepage"); 102 | 103 | 104 | var obj = {}; 105 | 106 | stateman 107 | .state('l0_not_next', function(){ 108 | obj.l0 = true 109 | }) 110 | .state('l1_has_next', { 111 | enter: function(){obj.l1 = true}, 112 | leave: function(){obj.l1 = false} 113 | }) 114 | .state('l1_has_next.l11_has_next', { 115 | enter: function(){obj.l12 = true}, 116 | leave: function(){obj.l12 = false} 117 | }) 118 | .state('l1_has_next.l11_has_next.l12_not_next', { 119 | url:'', 120 | enter: function(){obj.l13 = true}, 121 | leave: function(){obj.l13 = false} 122 | }) 123 | .state('book', { 124 | enter: function(){obj.book = true}, 125 | leave: function(){obj.book = false} 126 | }) 127 | .state('book.detail', { 128 | url: "/:bid", 129 | enter: function(option){obj.book_detail = option.param.bid}, 130 | leave: function(){obj.book_detail = false}, 131 | update: function(option){obj.book_detail_update = option.param.bid} 132 | }) 133 | .state('book.detail.index', { 134 | url: "", 135 | enter: function(option){obj.book_detail_index = option.param.bid}, 136 | leave: function(){obj.book_detail_index = false} 137 | 138 | }) 139 | .state('book.detail.message', { 140 | enter: function(option){ obj.book_detail_message = option.param.bid }, 141 | leave: function(){obj.book_detail_message = false}, 142 | update: function(option){obj.book_detail_message_update = option.param.bid} 143 | }) 144 | .state('book.list', { 145 | url: "", 146 | enter: function(){obj.book_list = true}, 147 | leave: function(){obj.book_list = false} 148 | }) 149 | .state('$notfound', { 150 | enter: function(){ 151 | obj.notfound = true 152 | }, 153 | leave: function(){ 154 | obj.notfound = false; 155 | } 156 | }) 157 | .start({ 158 | location: location 159 | }); 160 | 161 | after(function(){ 162 | stateman.stop(); 163 | }) 164 | it("we can directly vist the leave1 leaf state", function(){ 165 | 166 | stateman.nav("/l0_not_next"); 167 | expect(obj.l0).to.equal(true); 168 | 169 | }) 170 | it("some Edge test should not throw error", function(){ 171 | 172 | expect(function(){ 173 | stateman.nav(""); 174 | stateman.nav(undefined); 175 | stateman.go(); 176 | }).to.not.throwError() 177 | 178 | }) 179 | 180 | it("in strict mode, we can not touched the non-leaf state",function(){ 181 | var location = loc("http://leeluolee.github.io/homepage"); 182 | var stateman = new StateMan( {strict: true} ); 183 | stateman.start({location: location}); 184 | 185 | stateman.state("app.state", {}) 186 | stateman.go("app"); 187 | expect(stateman.current.name).to.not.equal("app"); 188 | }) 189 | 190 | it("we can directly vist the branch state, but have low poririty than leaf", function(){ 191 | stateman.nav("/l1_has_next/l11_has_next") 192 | expect(obj.l13).to.equal(true); 193 | 194 | }) 195 | 196 | it("we can define the id in url", function(){ 197 | stateman.nav("/book/1"); 198 | expect(obj.book).to.equal(true) 199 | expect(obj.book_detail).to.equal("1") 200 | }) 201 | it("we can also assign the blank url", function(){ 202 | stateman.nav("/book"); 203 | expect(obj.book).to.equal(true) 204 | expect(obj.book_detail).to.equal(false) 205 | expect(obj.book_list).to.equal(true) 206 | }) 207 | 208 | it("the ancestor should update if the path is change, but the state is not change", function(){ 209 | stateman.nav("/book/1"); 210 | expect(obj.book).to.equal(true) 211 | expect(obj.book_detail_index).to.equal("1") 212 | stateman.nav("/book/2/message"); 213 | expect(obj.book).to.equal(true) 214 | expect(obj.book_detail_update).to.equal("2") 215 | expect(obj.book_detail_message).to.equal("2") 216 | stateman.nav("/book/3/message"); 217 | expect(obj.book_detail_update).to.equal("3") 218 | expect(obj.book_detail_message_update).to.equal("3") 219 | }) 220 | it("the ancestor before basestate between current and previouse should update", function(){ 221 | stateman.nav("/book/6/message"); 222 | stateman.nav("/book/4/message"); 223 | expect(obj.book_detail_update).to.equal("4") 224 | expect(obj.book_detail_message_update).to.equal("4") 225 | stateman.nav("/book/5"); 226 | expect(obj.book_detail_update).to.equal("5") 227 | expect(obj.book_detail_message_update).to.equal("4") 228 | }) 229 | 230 | 231 | it("we can directly define the nested state", function(){ 232 | stateman.state('directly.skip.parent.state', function(){ 233 | obj.directly_skip_parent_state = true; 234 | }).nav("/directly/skip/parent/state") 235 | 236 | expect(obj.directly_skip_parent_state).to.equal(true) 237 | 238 | }) 239 | 240 | 241 | 242 | }) 243 | 244 | 245 | describe("stateman:navigation", function(){ 246 | 247 | var location = loc("http://leeluolee.github.io/stateman"); 248 | 249 | var obj = {}; 250 | 251 | var stateman = new StateMan() 252 | .state("home", {}) 253 | .state("contact.id", { 254 | }) 255 | }) 256 | 257 | 258 | // current previous pending and others 259 | describe("stateman:property", function(){ 260 | }) 261 | 262 | 263 | describe("stateman:transition", function(){ 264 | 265 | var location = loc("http://leeluolee.github.io/homepage"); 266 | 267 | 268 | var obj = {}; 269 | 270 | var stateman = new StateMan(); 271 | stateman 272 | 273 | .state("home", {}) 274 | .state("contact", { 275 | // animation basic 276 | enter: function(option){ 277 | var done = option.async(); 278 | setTimeout(done, 100) 279 | }, 280 | 281 | leave: function(option){ 282 | var done = option.async(); 283 | setTimeout(done, 100) 284 | } 285 | }) 286 | .state("contact.list", { 287 | url: "", 288 | // animation basic 289 | enter: function(){ 290 | }, 291 | 292 | leave: function(){ 293 | } 294 | }) 295 | .state("contact.detail", { 296 | url: ":id", 297 | // animation basic 298 | enter: function(option){ 299 | var done = option.async(); 300 | setTimeout(function(){ 301 | obj.contact_detail = option.param.id 302 | done(); 303 | }, 100) 304 | 305 | }, 306 | 307 | leave: function(option){ 308 | obj.contact_detail = option.param.id 309 | } 310 | }) 311 | .state("book", { 312 | // animation basic 313 | enter: function(option){ 314 | var done = option.async(); 315 | setTimeout(function(){ 316 | obj.book = true; 317 | done() 318 | }, 100) 319 | }, 320 | 321 | leave: function(option){ 322 | var done = option.async(); 323 | setTimeout(function(){ 324 | obj.book = false; 325 | done(); 326 | }, 100) 327 | } 328 | }) 329 | .state("book.id", { 330 | url: ":id", 331 | // animation basic 332 | enter: function(option){ 333 | obj.book_detail = option.param.id 334 | }, 335 | 336 | leave: function(option){ 337 | delete obj.book_detail; 338 | } 339 | }) 340 | .start({ 341 | location: location 342 | }) 343 | 344 | after(function(){ 345 | stateman.stop(); 346 | }) 347 | 348 | it("we can use callback as second param", function(done){ 349 | 350 | stateman.state('callback.second', {}) 351 | stateman.nav('/callback/second', function(option){ 352 | expect(option.current.name ==='callback.second') 353 | done(); 354 | }) 355 | 356 | }) 357 | it("we can use transition in enter and leave", function(done){ 358 | 359 | stateman.nav("/book/1" , {},function(){ 360 | 361 | expect(obj.book).to.equal(true) 362 | expect(obj.book_detail).to.equal("1") 363 | 364 | stateman.nav("/contact/2", {}, function(){ 365 | 366 | expect(obj.book).to.equal(false) 367 | expect(obj.contact_detail).to.equal("2") 368 | done(); 369 | }); 370 | 371 | expect(obj.book).to.equal(true) 372 | expect(obj.contact_detail).to.equal(undefined) 373 | // sync enter will directly done 374 | expect(obj.book_detail).to.equal(undefined) 375 | }) 376 | 377 | expect(obj.book).to.equal(undefined) 378 | expect(obj.book_detail).to.equal(undefined) 379 | 380 | }) 381 | 382 | it("will forbit previous async nav if next is comming", function(done){ 383 | stateman.nav("/book/1").nav("/home", {}, function(){ 384 | expect(stateman.current.name).to.equal("home") 385 | done(); 386 | }); 387 | }) 388 | 389 | // done (false) 390 | var loc2 = loc("http://leeluolee.github.io/homepage"); 391 | var obj2 = {}; 392 | var stateman2 = new StateMan(); 393 | 394 | stateman2.start({location: loc2}) 395 | after(function(){ 396 | stateman.stop(); 397 | }) 398 | 399 | it("enter return false can stop a navigation", function(){ 400 | stateman2.state("contact", { 401 | enter: function( option ){ 402 | return !option.ban; 403 | } 404 | }); 405 | stateman2.state("contact.detail", { 406 | leave: function(option){ 407 | return !option.ban 408 | } 409 | }) 410 | 411 | stateman2.go("contact.detail", { ban:true }) 412 | expect( stateman2.active.name ).to.equal("contact") 413 | stateman2.go("contact.detail", { ban: false }) 414 | stateman2.go("contact", { ban:true}); 415 | expect(stateman2.current.name).to.equal("contact.detail") 416 | }) 417 | it("pass false to done, can stop a async ", function(done){ 418 | 419 | stateman2.state("user", { 420 | enter: function(option){ 421 | var done = option.async(); 422 | setTimeout(function(){ 423 | done(option.success) 424 | },50) 425 | } 426 | }); 427 | stateman2.state("user.detail", { 428 | leave: function(option){ 429 | var done = option.async(); 430 | setTimeout(function(){ 431 | done(option.success) 432 | },50) 433 | } 434 | }) 435 | stateman2.state("user.message", { 436 | enter: function(){ 437 | option.async() 438 | } 439 | }) 440 | 441 | stateman2.go("user.detail", { success:false }, function(){ 442 | expect(stateman2.current.name).to.equal("user") 443 | stateman2.go("user.detail", { success: true }, function(){ 444 | expect(stateman2.current.name).to.equal("user.detail") 445 | stateman2.go("user", {success: false}, function(){ 446 | expect(stateman2.current.name).to.equal("user.detail") 447 | done() 448 | }); 449 | }) 450 | }) 451 | 452 | }) 453 | 454 | }) 455 | 456 | describe("stateman:redirect", function(){ 457 | 458 | var location = loc("http://leeluolee.github.io/homepage"); 459 | var obj = {}; 460 | var stateman = new StateMan(); 461 | 462 | stateman.start({location: location}) 463 | after(function(){ 464 | stateman.stop(); 465 | }) 466 | 467 | 468 | it("we can redirect at branch state, if it is not async", function(){ 469 | reset(stateman); 470 | stateman 471 | .state("branch1", { 472 | enter: function(opt){ 473 | if(opt.param.id == "1"){ 474 | opt.stop(); 475 | 476 | stateman.nav("branch2") 477 | 478 | } 479 | }, 480 | leave: function(){ 481 | obj["branch1_leave"] = true; 482 | } 483 | 484 | }) 485 | .state("branch1.leaf", function(){ 486 | obj["branch1_leaf"] = true; 487 | }) 488 | .state("branch2", function(){ 489 | obj.branch2 = true 490 | }) 491 | 492 | stateman.nav("/branch1/leaf?id=1"); 493 | 494 | expect(stateman.current.name).to.equal("branch2"); 495 | expect(obj.branch1_leave).to.equal(true); 496 | expect(obj.branch2).to.equal(true); 497 | expect(obj.branch1_leaf).to.equal(undefined); 498 | }) 499 | 500 | it("we can redirect at leaf state also", function(){ 501 | reset(stateman); 502 | 503 | stateman.state("branch1.leaf", function(){ 504 | 505 | stateman.go("branch2.leaf") 506 | 507 | }).state("branch2.leaf", function(){ 508 | 509 | obj.branch2_leaf = true; 510 | 511 | }) 512 | 513 | stateman.nav("/branch1/leaf"); 514 | 515 | expect(stateman.current.name).to.equal("branch2.leaf"); 516 | 517 | }) 518 | it("we can redirect if the async state is done before redirect", function(done){ 519 | 520 | var location = loc("http://leeluolee.github.io/homepage"); 521 | var obj = {}; 522 | var stateman = new StateMan(); 523 | 524 | stateman.start({location: location}) 525 | done(); 526 | 527 | // // beacuse the last state dont need async will dont need to async forever 528 | // stateman.state("branch2", function(){ 529 | // var over = this.async() 530 | // setTimeout(function(){ 531 | // over(); 532 | // stateman.go("branch3.leaf", null, function(){ 533 | // expect(this.current.name).to.equal("branch3.leaf"); 534 | // expect(obj.branch2_leaf).to.equal(true) 535 | // expect(obj.branch3_leaf).to.equal(true) 536 | // done() 537 | // }) 538 | // },100) 539 | // }) 540 | // .state("branch2.leaf", function(){ 541 | // obj.branch2_leaf = true 542 | // }) 543 | // .state("branch3.leaf", function(){ 544 | // obj.branch3_leaf = true; 545 | // }) 546 | 547 | // stateman.nav("/branch2/leaf") 548 | 549 | }) 550 | 551 | }) 552 | 553 | describe("stateman:other", function(){ 554 | var location = loc("http://leeluolee.github.io/homepage"); 555 | var obj = {}; 556 | var stateman = new StateMan(); 557 | 558 | stateman 559 | .start({location: location}) 560 | .state("contact.detail", { 561 | events: { 562 | notify: function(option){ 563 | obj.contact_detail = true; 564 | } 565 | }, 566 | enter: function(){ 567 | obj.manager = this.manager; 568 | } 569 | }) 570 | 571 | after(function(){ 572 | stateman.stop(); 573 | }) 574 | 575 | 576 | it("visited flag will add if the state is entered", function(){ 577 | expect(stateman.state("contact.detail").visited).to.equal(false) 578 | stateman.go("contact.detail") 579 | expect(stateman.state("contact.detail").visited).to.equal(true) 580 | expect(obj.manager).to.equal(stateman) 581 | }) 582 | 583 | 584 | it("stateman.decode should return the parsed state", function(){ 585 | 586 | var state = stateman.state("book.detail", {url: ":id"}).decode("/book/a?name=12") 587 | expect(state.param).to.eql({id:"a", name: "12"}) 588 | }) 589 | 590 | it("stateman.go should also assign the stateman.path", function(){ 591 | 592 | var state = stateman.state("book.message", {}).go("book.message") 593 | expect(state.path).to.eql("/book/message") 594 | }) 595 | 596 | it("stateman.encode should return the url", function(){ 597 | 598 | expect(stateman.encode("contact.detail")).to.equal("/contact/detail") 599 | var state = stateman.state("encode.detail", {url: ':id'}).go("book.message") 600 | expect(stateman.encode("encode.detail", {id:1, name:2})).to.equal("/encode/1?name=2") 601 | 602 | }) 603 | 604 | }) 605 | 606 | 607 | describe("stateman: matches and relative go", function(){ 608 | var location = loc("http://leeluolee.github.io/homepage"); 609 | var obj = {}; 610 | 611 | var stateman = new StateMan(); 612 | stateman.state("contact.detail.message", {}) 613 | .state("contact.list", { 614 | enter: function(){ 615 | stateman.go("^.detail.message"); 616 | expect(stateman.current.name).to.equal('contact.detail.message'); 617 | } 618 | }) 619 | .state("contact.list.option", {}) 620 | .state("contact.user.param", {url: ":id"}) 621 | .start({location: location}); 622 | 623 | after(function(){ 624 | stateman.stop(); 625 | }) 626 | 627 | it("relative to parent(^) should work as expect", function(){ 628 | stateman.go("contact.detail.message"); 629 | expect(stateman.current.name).to.equal("contact.detail.message"); 630 | stateman.go("^"); 631 | expect(stateman.current.name).to.equal("contact.detail"); 632 | stateman.go("^.list.option"); 633 | }) 634 | it("relative to parent(~) should work as expect", function(){ 635 | stateman.go("contact.detail"); 636 | expect(stateman.current.name).to.equal("contact.detail"); 637 | stateman.go("~.message"); 638 | expect(stateman.current.name).to.equal("contact.detail.message"); 639 | }) 640 | 641 | it("stateman.is should work as expect", function(){ 642 | stateman.go("contact.detail.message"); 643 | expect( stateman.is("contact.detail", {})).to.equal(true); 644 | expect( stateman.is("contact.detail", {}, true)).to.equal(false); 645 | expect( stateman.is("detail.message", {})).to.equal(false); 646 | expect( stateman.is("contact.detail.message", {})).to.equal(true); 647 | stateman.nav("/contact/user/1"); 648 | 649 | expect( stateman.is("contact.user.param")).to.equal(true); 650 | expect( stateman.is("contact.user.param", {})).to.equal(true); 651 | expect( stateman.is("contact.user", {id: "1"})).to.equal(true); 652 | expect( stateman.is("contact.user", {id: "2"})).to.equal(false); 653 | expect( stateman.is()).to.equal(false); 654 | 655 | stateman.state("contactmanage.detail",{}) 656 | 657 | stateman.go("contactmanage.detail"); 658 | expect(stateman.is("contact")).to.equal(false) 659 | }) 660 | 661 | }) 662 | 663 | describe("LifeCycle: callForPermission", function(){ 664 | 665 | var location = loc("http://leeluolee.github.io/homepage"); 666 | var obj = {}; 667 | var stateman = StateMan(); 668 | stateman.state({ 669 | "normal": { }, 670 | "normal.blog": { 671 | canEnter: function(){ 672 | return false 673 | }, 674 | enter: function(){ 675 | obj['normal.blog'] = true 676 | } 677 | }, 678 | 'normal.user': { 679 | canLeave: function(){ 680 | return false 681 | }, 682 | canEnter: function(){ 683 | 684 | }, 685 | enter: function(){ 686 | obj['normal.chat'] = true 687 | } 688 | }, 689 | 'normal.chat': { 690 | enter: function(){ 691 | obj['normal.chat'] = true 692 | } 693 | } 694 | }) 695 | .start({location: location}); 696 | it("return false in canEnter or canLeave, will stop navigation", function( ){ 697 | stateman.go('normal.chat') 698 | expect(stateman.current.name).to.equal('normal.chat') 699 | stateman.go('normal.blog') 700 | expect(stateman.current.name).to.equal('normal.chat') 701 | stateman.go('normal.user') 702 | expect(stateman.current.name).to.equal('normal.user') 703 | expect(stateman.active.name).to.equal('normal.user') 704 | expect(stateman.previous.name).to.equal('normal.chat') 705 | stateman.go('normal.chat') 706 | expect(stateman.current.name).to.equal('normal.user') 707 | expect(stateman.active.name).to.equal('normal.user') 708 | expect(stateman.previous.name).to.equal('normal.chat') 709 | 710 | }) 711 | if(typeof Promise !== 'undefined'){ 712 | var obj = {}; 713 | var pstateman = StateMan(); 714 | 715 | pstateman.state({ 716 | "promise": { }, 717 | "promise.blog": { 718 | canEnter: function(){ 719 | return Promise.reject() 720 | }, 721 | enter: function(){ 722 | pstateman['promise.blog'] = true 723 | } 724 | }, 725 | 'promise.user': { 726 | canLeave: function(){ 727 | return new Promise(function(resolve, reject){ 728 | setTimeout(reject, 300) 729 | }) 730 | } 731 | }, 732 | 'promise.chat': { 733 | enter: function(){ 734 | return new Promise(function(resolve, reject){ 735 | setTimeout(function(){ 736 | pstateman['promise.chat'] = true; 737 | resolve(); 738 | }, 300) 739 | }) 740 | }, 741 | leave: function(){ 742 | return new Promise(function(resolve, reject){ 743 | setTimeout(function(){ 744 | pstateman['promise.chat'] = false; 745 | resolve(); 746 | }, 300) 747 | }) 748 | } 749 | } 750 | }).start({'location':loc("http://leeluolee.github.io/homepage")} ) 751 | it("canEnter and canLeave accpet [Promise] as return value", function( done ){ 752 | pstateman.go('promise.chat', function(option){ 753 | expect(option.phase).to.equal('completion') 754 | expect(pstateman['promise.chat']).to.equal(true) 755 | pstateman.go('promise.blog', function(option){ 756 | expect(option.phase).to.equal('permission') 757 | expect(pstateman['promise.blog']).to.equal(undefined) 758 | pstateman.go('promise.user', function(option){ 759 | expect(option.phase).to.equal('completion') 760 | expect(pstateman.current.name).to.equal('promise.user') 761 | expect(pstateman.active.name).to.equal('promise.user') 762 | expect(pstateman.previous.name).to.equal('promise.chat') 763 | pstateman.nav('/promise/chat', function(option){ 764 | expect(option.phase).to.equal('permission') 765 | expect(pstateman.current.name).to.equal('promise.user') 766 | expect(pstateman.active.name).to.equal('promise.user') 767 | expect(pstateman.previous.name).to.equal('promise.chat') 768 | done() 769 | }) 770 | 771 | }) 772 | }) 773 | expect(pstateman['promise.blog']).to.equal(undefined) 774 | }) 775 | expect(pstateman['promise.chat']).to.equal(undefined) 776 | }) 777 | it("if you resolve promise with `false`, it is same as reject", function(){ 778 | 779 | }) 780 | } 781 | 782 | }) 783 | 784 | describe("LifeCycle: Navigating", function(){ 785 | var location = loc("http://leeluolee.github.io/"); 786 | var obj = {}; 787 | var stateman = new StateMan(); 788 | 789 | stateman 790 | .state("app", { 791 | enter: function(){ 792 | } 793 | }) 794 | .start({location: location}) 795 | 796 | 797 | after(function(){ 798 | stateman.stop(); 799 | }) 800 | 801 | 802 | it("redirect at root, should stop navigating and redirect to new current", function(){ 803 | var index =0, blog=0; 804 | stateman.state("app.index", { 805 | enter:function(){ 806 | index++ 807 | } 808 | }) 809 | .state("app.blog", {enter: function(){ 810 | blog++; 811 | }}) 812 | .on("begin", function( option ){ 813 | if(option.current.name !== "app.index"){ 814 | option.stop(); // @TODO tongyi 815 | stateman.go("app.index") 816 | } 817 | }) 818 | 819 | 820 | var end = 0; 821 | stateman.on("end", function(){ 822 | end++; 823 | }) 824 | stateman.nav("/app/blog", {} ); 825 | expect( blog ).to.equal( 0 ); 826 | expect( index ).to.equal( 1 ); 827 | expect( end ).to.equal( 1 ); 828 | 829 | stateman.nav("/app/blog", {} ); 830 | expect( end ).to.equal( 2 ); 831 | expect( blog ).to.equal( 0 ); 832 | stateman.off(); 833 | stateman._states = {} 834 | }) 835 | 836 | it("redirect at root, during redirect the callback should stashed, when end all callbacks should emit ", function(done){ 837 | stateman 838 | .state( "app1.index", { 839 | enter:function(){ 840 | stateman.go("app1.blog", function(){ 841 | expect(stateman.active.name === "app1.blog").to.equal(true); 842 | expect(stateman._stashCallback.length).to.equal(0); 843 | done(); 844 | }) 845 | } 846 | }) 847 | .state( "app1.blog", {enter: function(){}}) 848 | 849 | stateman.go("app1.index") 850 | }) 851 | 852 | it("option passed to nav, should also passed in enter, update and leave", function(done){ 853 | var data = {id:1}, num =0; 854 | stateman.state("app2.index", { 855 | url: "index/:id", 856 | enter: function(option){ 857 | expect(option.data).to.equal(data); 858 | expect(option.data).to.equal(data); 859 | expect(option.param.id).to.equal("2"); 860 | done(); 861 | } 862 | }) 863 | stateman.nav("/app2/index/2", {data: data}) 864 | 865 | }) 866 | 867 | it("stateman.back should return the previous state", function(){ 868 | 869 | }) 870 | 871 | 872 | }) 873 | 874 | describe("Config", function(){ 875 | var location = loc("http://leeluolee.github.io/"); 876 | var obj = {}; 877 | var stateman = new StateMan(); 878 | stateman.start({location: location}) 879 | 880 | after(function(){ 881 | stateman.stop(); 882 | }) 883 | 884 | it("title should accept String", function(){ 885 | var baseTitle = doc.title; 886 | stateman.state({ 887 | "app.hello": { 888 | title: "hello app" 889 | }, 890 | "app.exam": { 891 | url: "exam/:id", 892 | title: function(){ 893 | return "hello " + this.name + " " + stateman.param.id; 894 | } 895 | }, 896 | "app.third": {} 897 | }) 898 | stateman.go("app.hello") 899 | expect(doc.title).to.equal("hello app") 900 | stateman.go("app.exam", {param: {id: 1}}) 901 | expect(doc.title).to.equal("hello app.exam 1") 902 | stateman.nav("/app/third"); 903 | expect(doc.title).to.equal(baseTitle); 904 | }) 905 | 906 | it("title should Recursive up search", function(){ 907 | var baseTitle = doc.title; 908 | stateman.state({ 909 | "app2": { 910 | title: "APP" 911 | }, 912 | "app2.third": {} 913 | }) 914 | stateman.go("app2.third") 915 | 916 | expect(doc.title).to.equal("APP") 917 | }) 918 | }) 919 | }) 920 | --------------------------------------------------------------------------------