├── .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 |
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 |
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 |
79 |
80 |
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 | [](https://gitter.im/leeluolee/stateman?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
5 |
6 |
7 | [](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 |
--------------------------------------------------------------------------------