avalon-mmRouter-rails 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +17 -0
- data/Rakefile +2 -0
- data/avalon-mmRouter-rails.gemspec +23 -0
- data/lib/avalon/mmRouter/rails.rb +13 -0
- data/lib/avalon/mmRouter/rails/version.rb +7 -0
- data/vendor/assets/javascripts/avalon/mmHistory.js +310 -0
- data/vendor/assets/javascripts/avalon/mmPromise.js +221 -0
- data/vendor/assets/javascripts/avalon/mmRouter.js +351 -0
- data/vendor/assets/javascripts/avalon/mmState.js +1027 -0
- data/vendor/assets/javascripts/avalon/old-mmState.js +771 -0
- metadata +86 -0
@@ -0,0 +1,351 @@
|
|
1
|
+
define(["./mmHistory"], function() {
|
2
|
+
|
3
|
+
function Router() {
|
4
|
+
var table = {}
|
5
|
+
"get,post,delete,put".replace(avalon.rword, function(name) {
|
6
|
+
table[name] = []
|
7
|
+
})
|
8
|
+
this.routingTable = table
|
9
|
+
}
|
10
|
+
|
11
|
+
function parseQuery(url) {
|
12
|
+
var array = url.split("?"), query = {}, path = array[0], querystring = array[1]
|
13
|
+
if (querystring) {
|
14
|
+
var seg = querystring.split("&"),
|
15
|
+
len = seg.length, i = 0, s;
|
16
|
+
for (; i < len; i++) {
|
17
|
+
if (!seg[i]) {
|
18
|
+
continue
|
19
|
+
}
|
20
|
+
s = seg[i].split("=")
|
21
|
+
query[decodeURIComponent(s[0])] = decodeURIComponent(s[1])
|
22
|
+
}
|
23
|
+
}
|
24
|
+
return {
|
25
|
+
path: path,
|
26
|
+
query: query
|
27
|
+
}
|
28
|
+
}
|
29
|
+
|
30
|
+
|
31
|
+
function queryToString(obj) {
|
32
|
+
if(typeof obj == 'string') return obj
|
33
|
+
var str = []
|
34
|
+
for(var i in obj) {
|
35
|
+
if(i == "query") continue
|
36
|
+
str.push(i + '=' + encodeURIComponent(obj[i]))
|
37
|
+
}
|
38
|
+
return str.length ? '?' + str.join("&") : ''
|
39
|
+
}
|
40
|
+
|
41
|
+
var placeholder = /([:*])(\w+)|\{(\w+)(?:\:((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g
|
42
|
+
Router.prototype = {
|
43
|
+
error: function(callback) {
|
44
|
+
this.errorback = callback
|
45
|
+
},
|
46
|
+
_pathToRegExp: function(pattern, opts) {
|
47
|
+
var keys = opts.keys = [],
|
48
|
+
// segments = opts.segments = [],
|
49
|
+
compiled = '^', last = 0, m, name, regexp, segment;
|
50
|
+
|
51
|
+
while ((m = placeholder.exec(pattern))) {
|
52
|
+
name = m[2] || m[3]; // IE[78] returns '' for unmatched groups instead of null
|
53
|
+
regexp = m[4] || (m[1] == '*' ? '.*' : 'string')
|
54
|
+
segment = pattern.substring(last, m.index);
|
55
|
+
var type = this.$types[regexp]
|
56
|
+
var key = {
|
57
|
+
name: name
|
58
|
+
}
|
59
|
+
if (type) {
|
60
|
+
regexp = type.pattern
|
61
|
+
key.decode = type.decode
|
62
|
+
}
|
63
|
+
keys.push(key)
|
64
|
+
compiled += quoteRegExp(segment, regexp, false)
|
65
|
+
// segments.push(segment)
|
66
|
+
last = placeholder.lastIndex
|
67
|
+
}
|
68
|
+
segment = pattern.substring(last);
|
69
|
+
compiled += quoteRegExp(segment) + (opts.strict ? opts.last : "\/?") + '$';
|
70
|
+
var sensitive = typeof opts.caseInsensitive === "boolean" ? opts.caseInsensitive : true
|
71
|
+
// segments.push(segment);
|
72
|
+
opts.regexp = new RegExp(compiled, sensitive ? 'i' : undefined);
|
73
|
+
return opts
|
74
|
+
|
75
|
+
},
|
76
|
+
//添加一个路由规则
|
77
|
+
add: function(method, path, callback, opts) {
|
78
|
+
var array = this.routingTable[method.toLowerCase()]
|
79
|
+
if (path.charAt(0) !== "/") {
|
80
|
+
throw "path必须以/开头"
|
81
|
+
}
|
82
|
+
opts = opts || {}
|
83
|
+
opts.callback = callback
|
84
|
+
if (path.length > 2 && path.charAt(path.length - 1) === "/") {
|
85
|
+
path = path.slice(0, -1)
|
86
|
+
opts.last = "/"
|
87
|
+
}
|
88
|
+
avalon.Array.ensure(array, this._pathToRegExp(path, opts))
|
89
|
+
},
|
90
|
+
//判定当前URL与已有状态对象的路由规则是否符合
|
91
|
+
route: function(method, path, query) {
|
92
|
+
path = path.trim()
|
93
|
+
var states = this.routingTable[method]
|
94
|
+
for (var i = 0, el; el = states[i++]; ) {
|
95
|
+
var args = path.match(el.regexp)
|
96
|
+
if (args) {
|
97
|
+
el.query = query || {}
|
98
|
+
el.path = path
|
99
|
+
el.params = {}
|
100
|
+
var keys = el.keys
|
101
|
+
args.shift()
|
102
|
+
if (keys.length) {
|
103
|
+
this._parseArgs(args, el)
|
104
|
+
}
|
105
|
+
return el.callback.apply(el, args)
|
106
|
+
}
|
107
|
+
}
|
108
|
+
if (this.errorback) {
|
109
|
+
this.errorback()
|
110
|
+
}
|
111
|
+
},
|
112
|
+
_parseArgs: function(match, stateObj) {
|
113
|
+
var keys = stateObj.keys
|
114
|
+
for (var j = 0, jn = keys.length; j < jn; j++) {
|
115
|
+
var key = keys[j]
|
116
|
+
var value = match[j] || ""
|
117
|
+
if (typeof key.decode === "function") {//在这里尝试转换参数的类型
|
118
|
+
var val = key.decode(value)
|
119
|
+
} else {
|
120
|
+
try {
|
121
|
+
val = JSON.parse(value)
|
122
|
+
} catch (e) {
|
123
|
+
val = value
|
124
|
+
}
|
125
|
+
}
|
126
|
+
match[j] = stateObj.params[key.name] = val
|
127
|
+
}
|
128
|
+
},
|
129
|
+
getLastPath: function() {
|
130
|
+
return getCookie("msLastPath")
|
131
|
+
},
|
132
|
+
setLastPath: function(path) {
|
133
|
+
setCookie("msLastPath", path)
|
134
|
+
},
|
135
|
+
/*
|
136
|
+
* @interface avalon.router.redirect
|
137
|
+
* @param hash 访问的url hash
|
138
|
+
*/
|
139
|
+
redirect: function(hash) {
|
140
|
+
this.navigate(hash, {replace: true})
|
141
|
+
},
|
142
|
+
/*
|
143
|
+
* @interface avalon.router.navigate
|
144
|
+
* @param hash 访问的url hash
|
145
|
+
* @param options 扩展配置
|
146
|
+
* @param options.replace true替换history,否则生成一条新的历史记录
|
147
|
+
* @param options.silent true表示只同步url,不触发url变化监听绑定
|
148
|
+
*/
|
149
|
+
navigate: function(hash, options) {
|
150
|
+
var parsed = parseQuery((hash.charAt(0) !== "/" ? "/" : "") + hash),
|
151
|
+
options = options || {}
|
152
|
+
if(hash.charAt(0) === "/")
|
153
|
+
hash = hash.slice(1)// 修正出现多扛的情况 fix http://localhost:8383/index.html#!//
|
154
|
+
// 在state之内有写history的逻辑
|
155
|
+
if(!avalon.state || options.silent) avalon.history && avalon.history.updateLocation(hash, avalon.mix({}, options, {silent: true}))
|
156
|
+
// 只是写历史而已
|
157
|
+
if(!options.silent) {
|
158
|
+
this.route("get", parsed.path, parsed.query, options)
|
159
|
+
}
|
160
|
+
},
|
161
|
+
/*
|
162
|
+
* @interface avalon.router.when 配置重定向规则
|
163
|
+
* @param path 被重定向的表达式,可以是字符串或者数组
|
164
|
+
* @param redirect 重定向的表示式或者url
|
165
|
+
*/
|
166
|
+
when: function(path, redirect) {
|
167
|
+
var me = this,
|
168
|
+
path = path instanceof Array ? path : [path]
|
169
|
+
avalon.each(path, function(index, p) {
|
170
|
+
me.add("get", p, function() {
|
171
|
+
var info = me.urlFormate(redirect, this.params, this.query)
|
172
|
+
me.navigate(info.path + info.query, {replace: true})
|
173
|
+
})
|
174
|
+
})
|
175
|
+
return this
|
176
|
+
},
|
177
|
+
/*
|
178
|
+
* @interface avalon.router.get 添加一个router规则
|
179
|
+
* @param path url表达式
|
180
|
+
* @param callback 对应这个url的回调
|
181
|
+
*/
|
182
|
+
get: function(path, callback) {},
|
183
|
+
urlFormate: function(url, params, query) {
|
184
|
+
var query = query ? queryToString(query) : "",
|
185
|
+
hash = url.replace(placeholder, function(mat) {
|
186
|
+
var key = mat.replace(/[\{\}]/g, '').split(":")
|
187
|
+
key = key[0] ? key[0] : key[1]
|
188
|
+
return params[key] !== undefined ? params[key] : ''
|
189
|
+
}).replace(/^\//g, '')
|
190
|
+
return {
|
191
|
+
path: hash,
|
192
|
+
query: query
|
193
|
+
}
|
194
|
+
},
|
195
|
+
/* *
|
196
|
+
`'/hello/'` - 匹配'/hello/'或'/hello'
|
197
|
+
`'/user/:id'` - 匹配 '/user/bob' 或 '/user/1234!!!' 或 '/user/' 但不匹配 '/user' 与 '/user/bob/details'
|
198
|
+
`'/user/{id}'` - 同上
|
199
|
+
`'/user/{id:[^/]*}'` - 同上
|
200
|
+
`'/user/{id:[0-9a-fA-F]{1,8}}'` - 要求ID匹配/[0-9a-fA-F]{1,8}/这个子正则
|
201
|
+
`'/files/{path:.*}'` - Matches any URL starting with '/files/' and captures the rest of the
|
202
|
+
path into the parameter 'path'.
|
203
|
+
`'/files/*path'` - ditto.
|
204
|
+
*/
|
205
|
+
// avalon.router.get("/ddd/:dddID/",callback)
|
206
|
+
// avalon.router.get("/ddd/{dddID}/",callback)
|
207
|
+
// avalon.router.get("/ddd/{dddID:[0-9]{4}}/",callback)
|
208
|
+
// avalon.router.get("/ddd/{dddID:int}/",callback)
|
209
|
+
// 我们甚至可以在这里添加新的类型,avalon.router.$type.d4 = { pattern: '[0-9]{4}', decode: Number}
|
210
|
+
// avalon.router.get("/ddd/{dddID:d4}/",callback)
|
211
|
+
$types: {
|
212
|
+
date: {
|
213
|
+
pattern: "[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[1-2][0-9]|3[0-1])",
|
214
|
+
decode: function(val) {
|
215
|
+
return new Date(val.replace(/\-/g, "/"))
|
216
|
+
}
|
217
|
+
},
|
218
|
+
string: {
|
219
|
+
pattern: "[^\\/]*"
|
220
|
+
},
|
221
|
+
bool: {
|
222
|
+
decode: function(val) {
|
223
|
+
return parseInt(val, 10) === 0 ? false : true;
|
224
|
+
},
|
225
|
+
pattern: "0|1"
|
226
|
+
},
|
227
|
+
'int': {
|
228
|
+
decode: function(val) {
|
229
|
+
return parseInt(val, 10);
|
230
|
+
},
|
231
|
+
pattern: "\\d+"
|
232
|
+
}
|
233
|
+
}
|
234
|
+
}
|
235
|
+
|
236
|
+
"get,put,delete,post".replace(avalon.rword, function(method) {
|
237
|
+
return Router.prototype[method] = function(a, b, c) {
|
238
|
+
this.add(method, a, b, c)
|
239
|
+
}
|
240
|
+
})
|
241
|
+
function quoteRegExp(string, pattern, isOptional) {
|
242
|
+
var result = string.replace(/[\\\[\]\^$*+?.()|{}]/g, "\\$&");
|
243
|
+
if (!pattern)
|
244
|
+
return result;
|
245
|
+
var flag = isOptional ? '?' : '';
|
246
|
+
return result + flag + '(' + pattern + ')' + flag;
|
247
|
+
}
|
248
|
+
function supportLocalStorage() {
|
249
|
+
try {
|
250
|
+
localStorage.setItem("avalon", 1)
|
251
|
+
localStorage.removeItem("avalon")
|
252
|
+
return true
|
253
|
+
} catch (e) {
|
254
|
+
return false
|
255
|
+
}
|
256
|
+
}
|
257
|
+
|
258
|
+
if (supportLocalStorage()) {
|
259
|
+
Router.prototype.getLastPath = function() {
|
260
|
+
return localStorage.getItem("msLastPath")
|
261
|
+
}
|
262
|
+
var cookieID
|
263
|
+
Router.prototype.setLastPath = function (path) {
|
264
|
+
if (cookieID) {
|
265
|
+
clearTimeout(cookieID)
|
266
|
+
cookieID = null
|
267
|
+
}
|
268
|
+
localStorage.setItem("msLastPath", path)
|
269
|
+
cookieID = setTimeout(function () {
|
270
|
+
localStorage.removItem("msLastPath")
|
271
|
+
}, 1000 * 60 * 60 * 24)
|
272
|
+
}
|
273
|
+
}
|
274
|
+
|
275
|
+
|
276
|
+
|
277
|
+
function escapeCookie(value) {
|
278
|
+
return String(value).replace(/[,;"\\=\s%]/g, function(character) {
|
279
|
+
return encodeURIComponent(character)
|
280
|
+
});
|
281
|
+
}
|
282
|
+
function setCookie(key, value) {
|
283
|
+
var date = new Date()//将date设置为1天以后的时间
|
284
|
+
date.setTime(date.getTime() + 1000 * 60 * 60 * 24)
|
285
|
+
document.cookie = escapeCookie(key) + '=' + escapeCookie(value) + ";expires=" + date.toGMTString()
|
286
|
+
}
|
287
|
+
function getCookie(name) {
|
288
|
+
var m = String(document.cookie).match(new RegExp('(?:^| )' + name + '(?:(?:=([^;]*))|;|$)')) || ["", ""]
|
289
|
+
return decodeURIComponent(m[1])
|
290
|
+
}
|
291
|
+
|
292
|
+
avalon.router = new Router
|
293
|
+
|
294
|
+
return avalon
|
295
|
+
})
|
296
|
+
/*
|
297
|
+
<!DOCTYPE html>
|
298
|
+
<html>
|
299
|
+
<head>
|
300
|
+
<meta charset="utf-8">
|
301
|
+
<title>路由系统</title>
|
302
|
+
<script src="avalon.js"></script>
|
303
|
+
<script>
|
304
|
+
require(["mmRouter"], function() {
|
305
|
+
var model = avalon.define('xxx', function(vm) {
|
306
|
+
vm.currPath = ""
|
307
|
+
})
|
308
|
+
avalon.router.get("/aaa", function(a) {
|
309
|
+
model.currPath = this.path
|
310
|
+
})
|
311
|
+
avalon.router.get("/bbb", function(a) {
|
312
|
+
model.currPath = this.path
|
313
|
+
})
|
314
|
+
avalon.router.get("/ccc", function(a) {
|
315
|
+
model.currPath = this.path
|
316
|
+
})
|
317
|
+
avalon.router.get("/ddd/:ddd", function(a) {//:ddd为参数
|
318
|
+
avalon.log(a)
|
319
|
+
model.currPath = this.path
|
320
|
+
})
|
321
|
+
avalon.router.get("/eee", function(a) {
|
322
|
+
model.currPath = this.path
|
323
|
+
})
|
324
|
+
avalon.history.start({
|
325
|
+
html5Mode: true,
|
326
|
+
basepath: "/avalon"
|
327
|
+
})
|
328
|
+
avalon.scan()
|
329
|
+
})
|
330
|
+
</script>
|
331
|
+
</head>
|
332
|
+
<body >
|
333
|
+
<div ms-controller="xxx">
|
334
|
+
<ul>
|
335
|
+
<li><a href="#!/aaa">aaa</a></li>
|
336
|
+
<li><a href="#!/bbb">bbb</a></li>
|
337
|
+
<li><a href="#!/ccc">ccc</a></li>
|
338
|
+
<li><a href="#!/ddd/222">ddd</a></li>
|
339
|
+
<li><a href="#!/eee">eee</a></li>
|
340
|
+
</ul>
|
341
|
+
<div style="color:red">{{currPath}}</div>
|
342
|
+
<div style="height: 600px;width:1px;">
|
343
|
+
|
344
|
+
</div>
|
345
|
+
<p id="eee">会定位到这里</p>
|
346
|
+
</div>
|
347
|
+
|
348
|
+
</body>
|
349
|
+
</html>
|
350
|
+
|
351
|
+
*/
|
@@ -0,0 +1,1027 @@
|
|
1
|
+
define(["./mmPromise", "./mmRouter"], function() {
|
2
|
+
//重写mmRouter中的route方法
|
3
|
+
avalon.router.route = function(method, path, query, options) {
|
4
|
+
path = path.trim()
|
5
|
+
var states = this.routingTable[method]
|
6
|
+
for (var i = 0, el; el = states[i++]; ) {//el为一个个状态对象,状态对象的callback总是返回一个Promise
|
7
|
+
var args = path.match(el.regexp)
|
8
|
+
if (args && el.abstract !== true) {//不能是抽象状态
|
9
|
+
var newParams = {params: {}}
|
10
|
+
avalon.mix(newParams.params, el.params)
|
11
|
+
newParams.keys = el.keys
|
12
|
+
newParams.params.query = query || {}
|
13
|
+
args.shift()
|
14
|
+
if (el.keys.length) {
|
15
|
+
this._parseArgs(args, newParams)
|
16
|
+
}
|
17
|
+
if (el.stateName) {
|
18
|
+
mmState.transitionTo(mmState.currentState, el, newParams.params, options)
|
19
|
+
} else {
|
20
|
+
el.callback.apply(el, args)
|
21
|
+
}
|
22
|
+
return
|
23
|
+
}
|
24
|
+
}
|
25
|
+
if (this.errorback) {
|
26
|
+
this.errorback()
|
27
|
+
}
|
28
|
+
}
|
29
|
+
var _root, undefine, _controllers = {}, _states = {}
|
30
|
+
/*
|
31
|
+
* @interface avalon.router.go 跳转到一个已定义状态上,params对参数对象
|
32
|
+
* @param toName 状态name
|
33
|
+
* @param params 附加参数
|
34
|
+
* @param params.query 在hash后面附加的类似search的参数对
|
35
|
+
* @param options 扩展配置
|
36
|
+
* @param options.reload true强制reload,即便url、参数并未发生变化
|
37
|
+
* @param options.replace true替换history,否则生成一条新的历史记录
|
38
|
+
* @param options.confirmed true不触发onBeforeUnload,$onBeforeUnload,onBeforeExit
|
39
|
+
*/
|
40
|
+
avalon.router.go = function(toName, params, options) {
|
41
|
+
var from = mmState.currentState,
|
42
|
+
to = StateModel.is(toName) ? toName : getStateByName(toName),
|
43
|
+
params = params || {}
|
44
|
+
var params = avalon.mix(true, {}, to.params, params)
|
45
|
+
if (to) {
|
46
|
+
mmState.transitionTo(from, to, params, options)
|
47
|
+
}
|
48
|
+
}
|
49
|
+
// 事件管理器
|
50
|
+
var Event = window.$eventManager = avalon.define("$eventManager", function(vm) {
|
51
|
+
vm.$flag = 0;
|
52
|
+
vm.uiqKey = function() {
|
53
|
+
vm.$flag++
|
54
|
+
return "flag" + vm.$flag++
|
55
|
+
}
|
56
|
+
})
|
57
|
+
function removeOld() {
|
58
|
+
var nodes = mmState.oldNodes
|
59
|
+
while(nodes.length) {
|
60
|
+
var i = nodes.length - 1,
|
61
|
+
node = nodes[i]
|
62
|
+
node.parentNode && node.parentNode.removeChild(node)
|
63
|
+
nodes.splice(i, 1)
|
64
|
+
node = null
|
65
|
+
}
|
66
|
+
}
|
67
|
+
Event.$watch("onAbort", removeOld)
|
68
|
+
var mmState = window.mmState = {
|
69
|
+
prevState: NaN,
|
70
|
+
currentState: NaN, // 当前状态,可能还未切换到该状态
|
71
|
+
activeState: NaN, // 当前实际处于的状态
|
72
|
+
oldNodes: [],
|
73
|
+
query: {}, // 从属于currentState
|
74
|
+
popOne: function(chain, params, callback, notConfirmed) {
|
75
|
+
if(mmState._toParams !== params) return callback(false, {type: "abort"})
|
76
|
+
var cur = chain.pop(), me = this
|
77
|
+
if(!cur) return callback()
|
78
|
+
// 阻止退出
|
79
|
+
if(notConfirmed && cur.onBeforeExit() === false) return callback(false)
|
80
|
+
me.activeState = cur.parentState || _root
|
81
|
+
cur.done = function(success) {
|
82
|
+
cur._pending = false
|
83
|
+
cur.done = null
|
84
|
+
cur._local = null
|
85
|
+
if(success !== false) {
|
86
|
+
if(me.activeState) return me.popOne(chain, params, callback, notConfirmed)
|
87
|
+
}
|
88
|
+
return callback(success)
|
89
|
+
}
|
90
|
+
var success = cur.onExit()
|
91
|
+
if(!cur._pending && cur.done) cur.done(success)
|
92
|
+
},
|
93
|
+
pushOne: function(chain, params, callback, _local, toLocals) {
|
94
|
+
if(mmState._toParams !== params) return callback(false, {type: "abort"})
|
95
|
+
var cur = chain.shift(), me = this
|
96
|
+
// 退出
|
97
|
+
if(!cur) {
|
98
|
+
chain = null
|
99
|
+
return callback()
|
100
|
+
}
|
101
|
+
cur.syncParams(params)
|
102
|
+
// 阻止进入该状态
|
103
|
+
if(cur.onBeforeEnter() === false) {
|
104
|
+
// 恢复params
|
105
|
+
cur.syncParams(cur.oldParams)
|
106
|
+
return callback(false)
|
107
|
+
}
|
108
|
+
var _local = inherit(_local)
|
109
|
+
me.activeState = cur // 更新当前实际处于的状态
|
110
|
+
cur.done = function(success) {
|
111
|
+
// 防止async处触发已经销毁的done
|
112
|
+
if(!cur.done) return
|
113
|
+
cur._pending = false
|
114
|
+
cur.done = null
|
115
|
+
cur.visited = true
|
116
|
+
// 退出
|
117
|
+
if(success === false) {
|
118
|
+
// 这里斟酌一下 - 去掉
|
119
|
+
// cur.callback.apply(cur, [params, _local])
|
120
|
+
return callback(success)
|
121
|
+
}
|
122
|
+
var resolved = cur.callback.apply(cur, [params, _local])
|
123
|
+
resolved.$then(function(res) {
|
124
|
+
// sync params to oldParams
|
125
|
+
avalon.mix(true, cur.oldParams, cur.params)
|
126
|
+
// 继续状态链
|
127
|
+
me.pushOne(chain, params, callback, _local)
|
128
|
+
})
|
129
|
+
}
|
130
|
+
// 一般在这个回调里准备数据
|
131
|
+
var args = []
|
132
|
+
avalon.each(cur.keys, function(index, item) {
|
133
|
+
var key = item.name
|
134
|
+
args.push(cur.params[key])
|
135
|
+
})
|
136
|
+
cur._onEnter.apply(cur, args)
|
137
|
+
if(!cur._pending && cur.done) cur.done()
|
138
|
+
},
|
139
|
+
transitionTo: function(fromState, toState, toParams, options) {
|
140
|
+
var toParams = toParams || toState.params, fromAbort
|
141
|
+
// state machine on transition
|
142
|
+
if(this.activeState && (this.activeState != this.currentState)) {
|
143
|
+
avalon.log("navigating to [" +
|
144
|
+
this.currentState.stateName +
|
145
|
+
"] will be stopped, redirect to [" +
|
146
|
+
toState.stateName + "] now")
|
147
|
+
this.activeState.done && this.activeState.done(!"stopped")
|
148
|
+
fromState = this.activeState // 更新实际的fromState
|
149
|
+
fromAbort = true
|
150
|
+
}
|
151
|
+
mmState.oldNodes = []
|
152
|
+
var info = avalon.router.urlFormate(toState.url, toParams, toParams.query),
|
153
|
+
me = this,
|
154
|
+
options = options || {},
|
155
|
+
// 是否强制reload,参照angular,这个时候会触发整个页面重刷
|
156
|
+
reload = options.reload,
|
157
|
+
over,
|
158
|
+
fromChain = fromState && fromState.chain || [],
|
159
|
+
toChain = toState.chain,
|
160
|
+
i = 0,
|
161
|
+
changeType, // 是params变化?query变化?这个东西可以用来配置是否屏蔽视图刷新逻辑
|
162
|
+
state = toChain[i],
|
163
|
+
_local = _root.sourceLocal,
|
164
|
+
toLocals = []
|
165
|
+
if(!reload) {
|
166
|
+
// 找到共有父状态chain,params未变化
|
167
|
+
while(state && state === fromChain[i] && !state.paramsChanged(toParams)) {
|
168
|
+
_local = toLocals[i] = state._local
|
169
|
+
i++
|
170
|
+
state = toChain[i]
|
171
|
+
}
|
172
|
+
}
|
173
|
+
var exitChain = fromChain.slice(i),// 需要退出的chain
|
174
|
+
enterChain = toChain.slice(i),// 需要进入的chain
|
175
|
+
commonLocal = _local
|
176
|
+
// 建立toLocals,用来计算哪些view会被替换
|
177
|
+
while(state = toChain[i]) {
|
178
|
+
_local = toLocals[i] = inherit(_local, state.sourceLocal)
|
179
|
+
i++
|
180
|
+
}
|
181
|
+
mmState._local = _local
|
182
|
+
done = function(success, e) {
|
183
|
+
if(over) return
|
184
|
+
over = true
|
185
|
+
me.currentState = me.activeState
|
186
|
+
enterChain = exitChain = commonLocal = _local = toParams= null
|
187
|
+
mmState.oldNodes = []
|
188
|
+
if(success !== false) {
|
189
|
+
mmState.lastLocal = mmState.currentState._local
|
190
|
+
_root.fire("updateview", me.currentState, changeType)
|
191
|
+
avalon.log("transitionTo " + toState.stateName + " success")
|
192
|
+
callStateFunc("onLoad", me, fromState, toState)
|
193
|
+
} else {
|
194
|
+
return callStateFunc("onError", me, {
|
195
|
+
type: "transition",
|
196
|
+
message: "transitionTo " + toState.stateName + " faild",
|
197
|
+
error: e,
|
198
|
+
fromState:fromState,
|
199
|
+
toState:toState,
|
200
|
+
params: toParams
|
201
|
+
}, me.currentState)
|
202
|
+
}
|
203
|
+
}
|
204
|
+
toState.path = ("/" + info.path).replace(/^[\/]{2,}/g, "/")
|
205
|
+
if(!reload && fromState === toState) {
|
206
|
+
changeType = toState.paramsChanged(toParams)
|
207
|
+
if(!changeType) {
|
208
|
+
// redirect的目的状态 == this.activeState && abort
|
209
|
+
if(toState == this.activeState && fromAbort) return done()
|
210
|
+
// 重复点击直接return
|
211
|
+
return
|
212
|
+
}
|
213
|
+
}
|
214
|
+
|
215
|
+
mmState.query = avalon.mix({}, toParams.query)
|
216
|
+
|
217
|
+
// onBeforeUnload check
|
218
|
+
if(options && !options.confirmed && (callStateFunc("onBeforeUnload", this, fromState, toState) === false || broadCastBeforeUnload(exitChain, enterChain, fromState, toState) === false)) {
|
219
|
+
return callStateFunc("onAbort", this, fromState, toState)
|
220
|
+
}
|
221
|
+
if(over === true) {
|
222
|
+
return
|
223
|
+
}
|
224
|
+
avalon.log("begin transitionTo " + toState.stateName + " from " + (fromState && fromState.stateName || "unknown"))
|
225
|
+
callStateFunc("onUnload", this, fromState, toState)
|
226
|
+
this.currentState = toState
|
227
|
+
this.prevState = fromState
|
228
|
+
mmState._toParams = toParams
|
229
|
+
if(info && avalon.history) avalon.history.updateLocation(info.path + info.query, avalon.mix({}, options, {silent: true}), !fromState && location.hash)
|
230
|
+
callStateFunc("onBegin", this, fromState, toState)
|
231
|
+
this.popOne(exitChain, toParams, function(success) {
|
232
|
+
// 中断
|
233
|
+
if(success === false) return done(success)
|
234
|
+
me.pushOne(enterChain, toParams, done, commonLocal, toLocals)
|
235
|
+
}, !(options && options.confirmed))
|
236
|
+
}
|
237
|
+
}
|
238
|
+
//将template,templateUrl,templateProvider等属性从opts对象拷贝到新生成的view对象上的
|
239
|
+
function copyTemplateProperty(newObj, oldObj, name) {
|
240
|
+
if(name in oldObj) {
|
241
|
+
newObj[name] = oldObj[name]
|
242
|
+
delete oldObj[name]
|
243
|
+
}
|
244
|
+
}
|
245
|
+
function getCacheContainer() {
|
246
|
+
return document.getElementsByTagName("avalon")[0]
|
247
|
+
}
|
248
|
+
var templateCache = {},
|
249
|
+
cacheContainer = getCacheContainer()
|
250
|
+
function loadCache(name) {
|
251
|
+
var fragment = document.createDocumentFragment(),
|
252
|
+
divPlaceHolder = document.getElementById(name),
|
253
|
+
f,
|
254
|
+
eles = divPlaceHolder.eles,
|
255
|
+
i = 0
|
256
|
+
if(divPlaceHolder) {
|
257
|
+
while(f = eles[i]) {
|
258
|
+
fragment.appendChild(f)
|
259
|
+
i++
|
260
|
+
}
|
261
|
+
}
|
262
|
+
return fragment
|
263
|
+
}
|
264
|
+
function setCache(name, element) {
|
265
|
+
var fragment = document.createDocumentFragment(),
|
266
|
+
divPlaceHolder = document.getElementById(name),
|
267
|
+
f
|
268
|
+
if(!divPlaceHolder) {
|
269
|
+
divPlaceHolder = document.createElement("div")
|
270
|
+
divPlaceHolder.id = name
|
271
|
+
cacheContainer.appendChild(divPlaceHolder)
|
272
|
+
}
|
273
|
+
// 引用
|
274
|
+
if(divPlaceHolder.eles) {
|
275
|
+
avalon.each(divPlaceHolder.eles, function(index, ele) {
|
276
|
+
fragment.appendChild(ele)
|
277
|
+
})
|
278
|
+
} else {
|
279
|
+
divPlaceHolder.eles = []
|
280
|
+
while(f = element.firstChild) {
|
281
|
+
fragment.appendChild(f)
|
282
|
+
divPlaceHolder.eles.push(f)
|
283
|
+
}
|
284
|
+
templateCache[name] = true
|
285
|
+
}
|
286
|
+
divPlaceHolder.appendChild(fragment)
|
287
|
+
}
|
288
|
+
function broadCastBeforeUnload(exitChain, enterChain, fromState, toState) {
|
289
|
+
var lastLocal = mmState.lastLocal
|
290
|
+
if(!lastLocal || !enterChain[0] && !exitChain[0]) return
|
291
|
+
var newLocal = mmState._local,
|
292
|
+
cacheQueue = []
|
293
|
+
for(var i in lastLocal) {
|
294
|
+
var local = lastLocal[i]
|
295
|
+
// 所有被更新的view
|
296
|
+
if(!(i in newLocal) || newLocal[i] != local) {
|
297
|
+
if(local.$ctrl && ("$onBeforeUnload" in local.$ctrl)) {
|
298
|
+
if(local.$ctrl.$onBeforeUnload(fromState, toState) === false) return false
|
299
|
+
}
|
300
|
+
if(local.element && (exitChain[0] != enterChain[0])) cacheQueue.push(local)
|
301
|
+
}
|
302
|
+
}
|
303
|
+
avalon.each(cacheQueue, function(index, local) {
|
304
|
+
var ele = local.element,
|
305
|
+
name = avalon(ele).data("currentCache")
|
306
|
+
if(name) {
|
307
|
+
setCache(name, ele)
|
308
|
+
}
|
309
|
+
})
|
310
|
+
cacheQueue = null
|
311
|
+
}
|
312
|
+
// 靠谱的解决方法
|
313
|
+
avalon.bindingHandlers.view = function(data) {
|
314
|
+
var vmodels = data.vmodels || arguments[1]
|
315
|
+
var currentState = mmState.currentState,
|
316
|
+
element = data.element,
|
317
|
+
$element = avalon(element),
|
318
|
+
viewname = data.value || data.expr || "",
|
319
|
+
comment = document.createComment("ms-view:" + viewname),
|
320
|
+
par = element.parentNode,
|
321
|
+
defaultHTML = element.innerHTML,
|
322
|
+
statename = $element.data("statename") || "",
|
323
|
+
parentState = getStateByName(statename) || _root,
|
324
|
+
currentLocal = {},
|
325
|
+
oldElement = element,
|
326
|
+
tpl = element.outerHTML
|
327
|
+
element.removeAttribute("ms-view") // remove right now
|
328
|
+
par.insertBefore(comment, element)
|
329
|
+
function update(firsttime, currentState, changeType) {
|
330
|
+
// node removed, remove callback
|
331
|
+
if(!document.contains(comment)) {
|
332
|
+
data = vmodels = element = par = comment = $element = oldElement = update = null
|
333
|
+
return !"delete from watch"
|
334
|
+
}
|
335
|
+
var definedParentStateName = $element.data("statename") || "",
|
336
|
+
parentState = getStateByName(definedParentStateName) || _root,
|
337
|
+
_local
|
338
|
+
if (viewname.indexOf("@") < 0) viewname += "@" + parentState.stateName
|
339
|
+
_local = mmState.currentState._local && mmState.currentState._local[viewname]
|
340
|
+
if(firsttime && !_local || currentLocal === _local) return
|
341
|
+
currentLocal = _local
|
342
|
+
_currentState = _local && _local.state
|
343
|
+
// 缓存,如果加载dom上,则是全局配置,针对template还可以开一个单独配置
|
344
|
+
var cacheTpl = $element.data("viewCache"),
|
345
|
+
lastCache = $element.data("currentCache")
|
346
|
+
if(_local) {
|
347
|
+
cacheTpl = (_local.viewCache === false ? false : _local.viewCache || cacheTpl) && (viewname + "@" + _currentState.stateName)
|
348
|
+
} else if(cacheTpl) {
|
349
|
+
cacheTpl = viewname + "@__default__"
|
350
|
+
}
|
351
|
+
// stateB->stateB,配置了参数变化不更新dom
|
352
|
+
if(_local && _currentState === currentState && _local.ignoreChange && _local.ignoreChange(changeType, viewname)) return
|
353
|
+
// 需要load和使用的cache是一份
|
354
|
+
if(cacheTpl && cacheTpl === lastCache) return
|
355
|
+
compileNode(tpl, element, $element, _currentState)
|
356
|
+
var html = _local ? _local.template : defaultHTML,
|
357
|
+
fragment
|
358
|
+
if(cacheTpl) {
|
359
|
+
if(_local) {
|
360
|
+
_local.element = element
|
361
|
+
} else {
|
362
|
+
mmState.currentState._local[viewname] = {
|
363
|
+
state: mmState.currentState,
|
364
|
+
template: defaultHTML,
|
365
|
+
element: element
|
366
|
+
}
|
367
|
+
}
|
368
|
+
}
|
369
|
+
avalon.clearHTML(element)
|
370
|
+
// oldElement = element
|
371
|
+
element.removeAttribute("ms-view")
|
372
|
+
element.setAttribute("ui-view", data.value || data.expr || "")
|
373
|
+
// 本次更新的dom需要用缓存
|
374
|
+
if(cacheTpl) {
|
375
|
+
// 已缓存
|
376
|
+
if(templateCache[cacheTpl]) {
|
377
|
+
fragment = loadCache(cacheTpl)
|
378
|
+
// 未缓存
|
379
|
+
} else {
|
380
|
+
fragment = avalon.parseHTML(html)
|
381
|
+
}
|
382
|
+
element.appendChild(fragment)
|
383
|
+
// 更新现在使用的cache名字
|
384
|
+
$element.data("currentCache", cacheTpl)
|
385
|
+
if(templateCache[cacheTpl]) return
|
386
|
+
} else {
|
387
|
+
element.innerHTML = html
|
388
|
+
$element.data("currentCache", false)
|
389
|
+
}
|
390
|
+
// default
|
391
|
+
if(!_local && cacheTpl) $element.data("currentCache", cacheTpl)
|
392
|
+
avalon.each(getViewNodes(element), function(i, node) {
|
393
|
+
avalon(node).data("statename", _currentState && _currentState.stateName || "")
|
394
|
+
})
|
395
|
+
// merge上下文vmodels + controller指定的vmodels
|
396
|
+
avalon.scan(element, (_local && _local.vmodels || []).concat(vmodels || []))
|
397
|
+
// 触发视图绑定的controller的事件
|
398
|
+
if(_local && _local.$ctrl) {
|
399
|
+
_local.$ctrl.$onRendered && _local.$ctrl.$onRendered.apply(element, [_local])
|
400
|
+
}
|
401
|
+
}
|
402
|
+
update("firsttime")
|
403
|
+
_root.watch("updateview", function(state, changeType) {
|
404
|
+
return update.call(this, undefine, state, changeType)
|
405
|
+
})
|
406
|
+
}
|
407
|
+
if(avalon.directives) {
|
408
|
+
avalon.directive("view", {
|
409
|
+
init: avalon.bindingHandlers.view
|
410
|
+
})
|
411
|
+
}
|
412
|
+
function compileNode(tpl, element, $element, _currentState) {
|
413
|
+
if($element.hasClass("oni-mmRouter-slide")) {
|
414
|
+
// 拷贝一个镜像
|
415
|
+
var copy = element.cloneNode(true)
|
416
|
+
copy.setAttribute("ms-skip", "true")
|
417
|
+
avalon(copy).removeClass("oni-mmRouter-enter").addClass("oni-mmRouter-leave")
|
418
|
+
avalon(element).addClass("oni-mmRouter-enter")
|
419
|
+
element.parentNode.insertBefore(copy, element)
|
420
|
+
mmState.oldNodes.push(copy)
|
421
|
+
callStateFunc("onViewEnter", _currentState, element, copy)
|
422
|
+
}
|
423
|
+
return element
|
424
|
+
}
|
425
|
+
|
426
|
+
function inherit(parent, extra) {
|
427
|
+
return avalon.mix(new (avalon.mix(function() {}, { prototype: parent }))(), extra);
|
428
|
+
}
|
429
|
+
|
430
|
+
/*
|
431
|
+
* @interface avalon.state 对avalon.router.get 进行重新封装,生成一个状态对象
|
432
|
+
* @param stateName 指定当前状态名
|
433
|
+
* @param opts 配置
|
434
|
+
* @param opts.url 当前状态对应的路径规则,与祖先状态们组成一个完整的匹配规则
|
435
|
+
* @param {Function} opts.ignoreChange 当mmState.currentState == this时,更新视图的时候调用该函数,return true mmRouter则不会去重写视图和scan,请确保该视图内用到的数据没有放到avalon vmodel $skipArray内
|
436
|
+
* @param opts.controller 如果不写views属性,则默认view为"",为默认的view指定一个控制器,该配置会直接作为avalon.controller的参数生成一个$ctrl对象
|
437
|
+
* @param opts.controllerUrl 指定默认view控制器的路径,适用于模块化开发,该情形下默认通过avalon.controller.loader去加载一个符合amd规范,并返回一个avalon.controller定义的对象,传入opts.params作参数
|
438
|
+
* @param opts.controllerProvider 指定默认view控制器的提供者,它可以是一个Promise,也可以为一个函数,传入opts.params作参数
|
439
|
+
@param opts.viewCache 是否缓存这个模板生成的dom,设置会覆盖dom元素上的data-view-cache,也可以分别配置到views上每个单独的view上
|
440
|
+
* @param opts.views: 如果不写views属性,则默认view【ms-view=""】为"",也可以通过指定一个viewname属性来配置【ms-view="viewname"】,对多个[ms-view]容器进行处理,每个对象应拥有template, templateUrl, templateProvider,可以给每个对象搭配一个controller||controllerUrl||controllerProvider属性
|
441
|
+
* views的结构为
|
442
|
+
*<pre>
|
443
|
+
* {
|
444
|
+
* "": {template: "xxx"}
|
445
|
+
* "aaa": {template: "xxx"}
|
446
|
+
* "bbb@": {template: "xxx"}
|
447
|
+
* }
|
448
|
+
*</pre>
|
449
|
+
* views的每个键名(keyname)的结构为viewname@statename,
|
450
|
+
* 如果名字不存在@,则viewname直接为keyname,statename为opts.stateName
|
451
|
+
* 如果名字存在@, viewname为match[0], statename为match[1]
|
452
|
+
* @param opts.views.{viewname}.template 指定当前模板,也可以为一个函数,传入opts.params作参数,* @param opts.views.viewname.cacheController 是否缓存view的控制器,默认true
|
453
|
+
* @param opts.views.{viewname}.templateUrl 指定当前模板的路径,也可以为一个函数,传入opts.params作参数
|
454
|
+
* @param opts.views.{viewname}.templateProvider 指定当前模板的提供者,它可以是一个Promise,也可以为一个函数,传入opts.params作参数
|
455
|
+
* @param opts.views.{viewname}.ignoreChange 用法同state.ignoreChange,只是针对的粒度更细一些,针对到具体的view
|
456
|
+
* @param {Function} opts.onBeforeEnter 切入某个state之前触发,this指向对应的state,如果return false则会中断并退出整个状态机
|
457
|
+
* @param {Function} opts.onEnter 进入状态触发,可以返回false,或任意不为true的错误信息或一个promise对象,用法跟视图的$onEnter一致
|
458
|
+
* @param {Function} onEnter.params 视图所属的state的参数
|
459
|
+
* @param {Function} onEnter.resolve $onEnter return false的时候,进入同步等待,直到手动调用resolve
|
460
|
+
* @param {Function} onEnter.reject 数据加载失败,调用
|
461
|
+
* @param {Function} opts.onBeforeExit state退出前触发,this指向对应的state,如果return false则会中断并退出整个状态机
|
462
|
+
* @param {Function} opts.onExit 退出后触发,this指向对应的state
|
463
|
+
* @param opts.ignoreChange.changeType 值为"param",表示params变化,值为"query",表示query变化
|
464
|
+
* @param opts.ignoreChange.viewname 关联的ms-view name
|
465
|
+
* @param opts.abstract 表示它不参与匹配,this指向对应的state
|
466
|
+
* @param {private} opts.parentState 父状态对象(框架内部生成)
|
467
|
+
*/
|
468
|
+
avalon.state = function(stateName, opts) {
|
469
|
+
var state = StateModel(stateName, opts)
|
470
|
+
avalon.router.get(state.url, function(params, _local) {
|
471
|
+
var me = this, promises = [], _resovle, _reject, _data = [], _callbacks = []
|
472
|
+
state.resolved = getPromise(function(rs, rj) {
|
473
|
+
_resovle = rs
|
474
|
+
_reject = rj
|
475
|
+
})
|
476
|
+
avalon.each(state.views, function(name, view) {
|
477
|
+
var params = me.params,
|
478
|
+
reason = {
|
479
|
+
type: "view",
|
480
|
+
name: name,
|
481
|
+
params: params,
|
482
|
+
state: state,
|
483
|
+
view: view
|
484
|
+
},
|
485
|
+
viewLocal = _local[name] = {
|
486
|
+
name: name,
|
487
|
+
state: state,
|
488
|
+
params: state.filterParams(params),
|
489
|
+
ignoreChange: "ignoreChange" in view ? view.ignoreChange : me.ignoreChange,
|
490
|
+
viewCache: "viewCache" in view ? view.viewCache : me.viewCache
|
491
|
+
},
|
492
|
+
promise = fromPromise(view, params, reason)
|
493
|
+
promises.push(promise)
|
494
|
+
// template不能cache
|
495
|
+
promise.then(function(s) {
|
496
|
+
viewLocal.template = s
|
497
|
+
}, avalon.noop) // 捕获模板报错
|
498
|
+
var prom,
|
499
|
+
callback = function($ctrl) {
|
500
|
+
viewLocal.vmodels = $ctrl.$vmodels
|
501
|
+
view.$controller = viewLocal.$ctrl = $ctrl
|
502
|
+
resolveData()
|
503
|
+
},
|
504
|
+
resolveData = function() {
|
505
|
+
var $onEnter = view.$controller && view.$controller.$onEnter
|
506
|
+
if($onEnter) {
|
507
|
+
var innerProm = getPromise(function(rs, rj) {
|
508
|
+
var reason = {
|
509
|
+
type: "data",
|
510
|
+
state: state,
|
511
|
+
params: params
|
512
|
+
},
|
513
|
+
res = $onEnter(params, rs, function(message) {
|
514
|
+
reason.message = message
|
515
|
+
rj(reason)
|
516
|
+
})
|
517
|
+
// if promise
|
518
|
+
if(res && res.then) {
|
519
|
+
_data.push(res)
|
520
|
+
res.then(function() {
|
521
|
+
rs(res)
|
522
|
+
})
|
523
|
+
// error msg
|
524
|
+
} else if(res && res !== true) {
|
525
|
+
reason.message = res
|
526
|
+
rj(reason)
|
527
|
+
} else if(res === undefine) {
|
528
|
+
rs()
|
529
|
+
}
|
530
|
+
// res === false will pause here
|
531
|
+
})
|
532
|
+
innerProm = innerProm.then(function(cb) {
|
533
|
+
avalon.isFunction(cb) && _callbacks.push(cb)
|
534
|
+
})
|
535
|
+
_data.push(innerProm)
|
536
|
+
}
|
537
|
+
}
|
538
|
+
// controller似乎可以缓存着
|
539
|
+
if(view.$controller && view.cacheController !== false) {
|
540
|
+
return callback(view.$controller)
|
541
|
+
}
|
542
|
+
// 加载controller模块
|
543
|
+
if(view.controller) {
|
544
|
+
prom = promise.then(function() {
|
545
|
+
callback(avalon.controller(view.controller))
|
546
|
+
})
|
547
|
+
} else if(view.controllerUrl) {
|
548
|
+
prom = getPromise(function(rs, rj) {
|
549
|
+
var url = avalon.isFunction(view.controllerUrl) ? view.controllerUrl(params) : view.controllerUrl
|
550
|
+
url = url instanceof Array ? url : [url]
|
551
|
+
avalon.controller.loader(url, function($ctrl) {
|
552
|
+
promise.then(function() {
|
553
|
+
callback($ctrl)
|
554
|
+
rs()
|
555
|
+
})
|
556
|
+
})
|
557
|
+
})
|
558
|
+
} else if(view.controllerProvider) {
|
559
|
+
var res = avalon.isFunction(view.controllerProvider) ? view.controllerProvider(params) : view.controllerProvider
|
560
|
+
prom = getPromise(function(rs, rj) {
|
561
|
+
// if promise
|
562
|
+
if(res && res.then) {
|
563
|
+
_data.push(res)
|
564
|
+
res.then(function(r) {
|
565
|
+
promise.then(function() {
|
566
|
+
callback(r)
|
567
|
+
rs()
|
568
|
+
})
|
569
|
+
}, function(e) {
|
570
|
+
reason.message = e
|
571
|
+
rj(reason)
|
572
|
+
})
|
573
|
+
// error msg
|
574
|
+
} else {
|
575
|
+
promise.then(function() {
|
576
|
+
callback(res)
|
577
|
+
rs()
|
578
|
+
})
|
579
|
+
}
|
580
|
+
})
|
581
|
+
}
|
582
|
+
// is promise
|
583
|
+
if(prom && prom.then) {
|
584
|
+
promises.push(prom)
|
585
|
+
}
|
586
|
+
})
|
587
|
+
// 模板和controller就绪
|
588
|
+
getPromise(promises).$then(function(values) {
|
589
|
+
state._local = _local
|
590
|
+
// 数据就绪
|
591
|
+
getPromise(_data).$then(function() {
|
592
|
+
avalon.each(_callbacks, function(i, func) {
|
593
|
+
func()
|
594
|
+
})
|
595
|
+
promises = _data = _callbacks = null
|
596
|
+
_resovle()
|
597
|
+
})
|
598
|
+
})
|
599
|
+
return state.resolved
|
600
|
+
|
601
|
+
}, state)
|
602
|
+
|
603
|
+
return this
|
604
|
+
}
|
605
|
+
|
606
|
+
function isError(e) {
|
607
|
+
return e instanceof Error
|
608
|
+
}
|
609
|
+
|
610
|
+
// 将所有的promise error适配到这里来
|
611
|
+
function promiseError(e) {
|
612
|
+
if(isError(e)) {
|
613
|
+
throw e
|
614
|
+
} else {
|
615
|
+
callStateFunc("onError", mmState, e, e && e.state)
|
616
|
+
}
|
617
|
+
}
|
618
|
+
|
619
|
+
function getPromise(excutor) {
|
620
|
+
var prom = avalon.isFunction(excutor) ? new Promise(excutor) : Promise.all(excutor)
|
621
|
+
return prom
|
622
|
+
}
|
623
|
+
Promise.prototype.$then = function(onFulfilled, onRejected) {
|
624
|
+
var prom = this.then(onFulfilled, onRejected)
|
625
|
+
prom["catch"](promiseError)
|
626
|
+
return prom
|
627
|
+
}
|
628
|
+
avalon.state.onViewEntered = function(newNode, oldNode) {
|
629
|
+
if(newNode != oldNode) oldNode.parentNode.removeChild(oldNode)
|
630
|
+
}
|
631
|
+
/*
|
632
|
+
* @interface avalon.state.config 全局配置
|
633
|
+
* @param {Object} config 配置对象
|
634
|
+
* @param {Function} config.onBeforeUnload 开始切前的回调,this指向router对象,第一个参数是fromState,第二个参数是toState,return false可以用来阻止切换进行
|
635
|
+
* @param {Function} config.onAbort onBeforeUnload return false之后,触发的回调,this指向mmState对象,参数同onBeforeUnload
|
636
|
+
* @param {Function} config.onUnload url切换时候触发,this指向mmState对象,参数同onBeforeUnload
|
637
|
+
* @param {Function} config.onBegin 开始切换的回调,this指向mmState对象,参数同onBeforeUnload,如果配置了onBegin,则忽略begin
|
638
|
+
* @param {Function} config.onLoad 切换完成并成功,this指向mmState对象,参数同onBeforeUnload
|
639
|
+
* @param {Function} config.onViewEnter 视图插入动画函数,有一个默认效果
|
640
|
+
* @param {Node} config.onViewEnter.arguments[0] 新视图节点
|
641
|
+
* @param {Node} config.onViewEnter.arguments[1] 旧的节点
|
642
|
+
* @param {Function} config.onError 出错的回调,this指向对应的state,第一个参数是一个object,object.type表示出错的类型,比如view表示加载出错,object.name则对应出错的view name,object.xhr则是当使用默认模板加载器的时候的httpRequest对象,第二个参数是对应的state
|
643
|
+
*/
|
644
|
+
avalon.state.config = function(config) {
|
645
|
+
avalon.mix(avalon.state, config || {})
|
646
|
+
return avalon
|
647
|
+
}
|
648
|
+
function callStateFunc(name, state) {
|
649
|
+
Event.$fire.apply(Event, arguments)
|
650
|
+
return avalon.state[name] ? avalon.state[name].apply(state || mmState.currentState, [].slice.call(arguments, 2)) : 0
|
651
|
+
}
|
652
|
+
// 状态原型,所有的状态都要继承这个原型
|
653
|
+
function StateModel(stateName, options) {
|
654
|
+
if(this instanceof StateModel) {
|
655
|
+
this.stateName = stateName
|
656
|
+
this.formate(options)
|
657
|
+
} else {
|
658
|
+
var state = _states[stateName] = new StateModel(stateName, options || {})
|
659
|
+
return state
|
660
|
+
}
|
661
|
+
}
|
662
|
+
StateModel.is = function(state) {
|
663
|
+
return state instanceof StateModel
|
664
|
+
}
|
665
|
+
StateModel.prototype = {
|
666
|
+
formate: function(options) {
|
667
|
+
avalon.mix(true, this, options)
|
668
|
+
var stateName = this.stateName,
|
669
|
+
me = this,
|
670
|
+
chain = stateName.split("."),
|
671
|
+
len = chain.length - 1,
|
672
|
+
sourceLocal = me.sourceLocal = {}
|
673
|
+
this.chain = []
|
674
|
+
avalon.each(chain, function(key, name) {
|
675
|
+
if(key == len) {
|
676
|
+
me.chain.push(me)
|
677
|
+
} else {
|
678
|
+
var n = chain.slice(0, key + 1).join("."),
|
679
|
+
state = getStateByName(n)
|
680
|
+
if(!state) throw new Error("必须先定义" + n)
|
681
|
+
me.chain.push(state)
|
682
|
+
}
|
683
|
+
})
|
684
|
+
if (this.url === void 0) {
|
685
|
+
this.abstract = true
|
686
|
+
}
|
687
|
+
var parent = this.chain[len - 1] || _root
|
688
|
+
if (parent) {
|
689
|
+
this.url = parent.url + (this.url || "")
|
690
|
+
this.parentState = parent
|
691
|
+
}
|
692
|
+
if (!this.views && stateName != "") {
|
693
|
+
var view = {}
|
694
|
+
"template,templateUrl,templateProvider,controller,controllerUrl,controllerProvider,viewCache".replace(/\w+/g, function(prop) {
|
695
|
+
copyTemplateProperty(view, me, prop)
|
696
|
+
})
|
697
|
+
var viewname = "viewname" in this ? this.viewname : ""
|
698
|
+
this.views = {}
|
699
|
+
this.views[viewname] = view
|
700
|
+
}
|
701
|
+
var views = {},
|
702
|
+
viewsIsArray = this.views instanceof Array // 如果是一个数组
|
703
|
+
|
704
|
+
avalon.each(this.views, function(maybeName, view) {
|
705
|
+
var name = viewsIsArray ? view.name || "" : maybeName // 默认缺省
|
706
|
+
if (name.indexOf("@") < 0) {
|
707
|
+
name += "@" + (parent ? parent.stateName || "" : "")
|
708
|
+
}
|
709
|
+
views[name] = view
|
710
|
+
sourceLocal[name] = {}
|
711
|
+
})
|
712
|
+
this.views = views
|
713
|
+
this._self = options
|
714
|
+
this._pending = false
|
715
|
+
this.visited = false
|
716
|
+
this.params = inherit(parent && parent.params || {})
|
717
|
+
this.oldParams = {}
|
718
|
+
this.keys = []
|
719
|
+
|
720
|
+
this.events = {}
|
721
|
+
},
|
722
|
+
watch: function(eventName, func) {
|
723
|
+
var events = this.events[eventName] || []
|
724
|
+
this.events[eventName] = events
|
725
|
+
events.push(func)
|
726
|
+
return func
|
727
|
+
},
|
728
|
+
fire: function(eventName, state) {
|
729
|
+
var events = this.events[eventName] || [], i = 0
|
730
|
+
while(events[i]) {
|
731
|
+
var res = events[i].apply(this, [].slice.call(arguments, 1))
|
732
|
+
if(res === false) {
|
733
|
+
events.splice(i, 1)
|
734
|
+
} else {
|
735
|
+
i++
|
736
|
+
}
|
737
|
+
}
|
738
|
+
},
|
739
|
+
unwatch: function(eventName, func) {
|
740
|
+
var events = this.events[eventName]
|
741
|
+
if(!events) return
|
742
|
+
var i = 0
|
743
|
+
while(events[i]) {
|
744
|
+
if(events[i] == func) return events.splice(i, 1)
|
745
|
+
i++
|
746
|
+
}
|
747
|
+
},
|
748
|
+
paramsChanged: function(toParams) {
|
749
|
+
var changed = false, keys = this.keys, me= this, params = this.params
|
750
|
+
avalon.each(keys, function(index, item) {
|
751
|
+
var key = item.name
|
752
|
+
if(params[key] != toParams[key]) changed = "param"
|
753
|
+
})
|
754
|
+
// query
|
755
|
+
if(!changed && mmState.currentState === this) {
|
756
|
+
changed = !objectCompare(toParams.query, mmState.query) && "query"
|
757
|
+
}
|
758
|
+
return changed
|
759
|
+
},
|
760
|
+
filterParams: function(toParams) {
|
761
|
+
var params = avalon.mix(true, {}, this.params), keys = this.keys
|
762
|
+
avalon.each(keys, function(index, item) {
|
763
|
+
params[item.name] = toParams[item.name]
|
764
|
+
})
|
765
|
+
return params
|
766
|
+
},
|
767
|
+
syncParams: function(toParams) {
|
768
|
+
var me = this
|
769
|
+
avalon.each(this.keys, function(index, item) {
|
770
|
+
var key = item.name
|
771
|
+
if(key in toParams) me.params[key] = toParams[key]
|
772
|
+
})
|
773
|
+
},
|
774
|
+
_onEnter: function() {
|
775
|
+
this.query = this.getQuery()
|
776
|
+
var me = this,
|
777
|
+
arg = Array.prototype.slice.call(arguments),
|
778
|
+
done = me._async(),
|
779
|
+
prom = getPromise(function(rs, rj) {
|
780
|
+
var reason = {
|
781
|
+
type: "data",
|
782
|
+
state: me,
|
783
|
+
params: me.params
|
784
|
+
},
|
785
|
+
_reject = function(message) {
|
786
|
+
reason.message = message
|
787
|
+
done.apply(me, [false])
|
788
|
+
rj(reason)
|
789
|
+
},
|
790
|
+
_resovle = function() {
|
791
|
+
done.apply(me)
|
792
|
+
rs()
|
793
|
+
},
|
794
|
+
res = me.onEnter.apply(me, arg.concat([_resovle, _reject]))
|
795
|
+
// if promise
|
796
|
+
if(res && res.then) {
|
797
|
+
res.then(_resovle)["catch"](promiseError)
|
798
|
+
// error msg
|
799
|
+
} else if(res && res !== true) {
|
800
|
+
_reject(res)
|
801
|
+
} else if(res === undefine) {
|
802
|
+
_resovle()
|
803
|
+
}
|
804
|
+
// res === false will pause here
|
805
|
+
})
|
806
|
+
},
|
807
|
+
/*
|
808
|
+
* @interface state.getQuery 获取state的query,等价于state.query
|
809
|
+
*<pre>
|
810
|
+
* onEnter: function() {
|
811
|
+
* var query = this.getQuery()
|
812
|
+
* or
|
813
|
+
* this.query
|
814
|
+
* }
|
815
|
+
*</pre>
|
816
|
+
*/
|
817
|
+
getQuery: function() {return mmState.query},
|
818
|
+
/*
|
819
|
+
* @interface state.getParams 获取state的params,等价于state.params
|
820
|
+
*<pre>
|
821
|
+
* onEnter: function() {
|
822
|
+
* var params = this.getParams()
|
823
|
+
* or
|
824
|
+
* this.params
|
825
|
+
* }
|
826
|
+
*</pre>
|
827
|
+
*/
|
828
|
+
getParams: function() {return this.params},
|
829
|
+
_async: function() {
|
830
|
+
// 没有done回调的时候,防止死球
|
831
|
+
if(this.done) this._pending = true
|
832
|
+
return this.done || avalon.noop
|
833
|
+
},
|
834
|
+
onBeforeEnter: avalon.noop, // 切入某个state之前触发
|
835
|
+
onEnter: avalon.noop, // 切入触发
|
836
|
+
onBeforeExit: avalon.noop, // state退出前触发
|
837
|
+
onExit: avalon.noop // 退出后触发
|
838
|
+
}
|
839
|
+
|
840
|
+
_root = StateModel("", {
|
841
|
+
url: "",
|
842
|
+
views: null,
|
843
|
+
"abstract": true
|
844
|
+
})
|
845
|
+
|
846
|
+
/*
|
847
|
+
* @interface avalon.controller 给avalon.state视图对象配置控制器
|
848
|
+
* @param name 控制器名字
|
849
|
+
* @param {Function} factory 控制器函数,传递一个内部生成的控制器对象作为参数
|
850
|
+
* @param {Object} factory.arguments[0] $ctrl 控制器的第一个参数:实际生成的控制器对象
|
851
|
+
* @param {Object} $ctrl.$vmodels 给视图指定一个scan的vmodels数组,实际scan的时候$vmodels.concat(dom树上下文继承的vmodels)
|
852
|
+
* @param {Function} $ctrl.$onBeforeUnload 该视图被卸载前触发,return false可以阻止视图卸载,并阻止跳转
|
853
|
+
* @param {Function} $ctrl.$onEnter 给该视图加载数据,可以返回false,或任意不为true的错误信息或一个promise对象,传递3个参数
|
854
|
+
* @param {Object} $ctrl.$onEnter.arguments[0] params第一个参数:视图所属的state的参数
|
855
|
+
* @param {Function} $ctrl.$onEnter.arguments[1] resolve $onEnter 第二个参数:return false的时候,进入同步等待,直到手动调用resolve
|
856
|
+
* @param {Function} $ctrl.$onEnter.arguments[2] reject 第三个参数:数据加载失败,调用
|
857
|
+
* @param {Function} $ctrl.$onRendered 视图元素scan完成之后,调用
|
858
|
+
*/
|
859
|
+
avalon.controller = function() {
|
860
|
+
var first = arguments[0],
|
861
|
+
second = arguments[1]
|
862
|
+
if(first && (first instanceof _controller)) return first
|
863
|
+
var $ctrl = _controller()
|
864
|
+
if(avalon.isFunction(first)) {
|
865
|
+
first($ctrl)
|
866
|
+
} else if(avalon.isFunction(second)) {
|
867
|
+
$ctrl.name = first
|
868
|
+
second($ctrl)
|
869
|
+
} else if(typeof first == "string" || typeof first == "object") {
|
870
|
+
first = first instanceof Array ? first : Array.prototype.slice.call(arguments)
|
871
|
+
avalon.each(first, function(index, item) {
|
872
|
+
if(typeof item == "string") {
|
873
|
+
first[index] = avalon.vmodels[item]
|
874
|
+
}
|
875
|
+
item = first[index]
|
876
|
+
if("$onRendered" in item) $ctrl.$onRendered = item["$onRendered"]
|
877
|
+
if("$onEnter" in item) $ctrl.$onEnter = item["$onEnter"]
|
878
|
+
})
|
879
|
+
$ctrl.$vmodels = first
|
880
|
+
} else {
|
881
|
+
throw new Error("参数错误" + arguments)
|
882
|
+
}
|
883
|
+
return $ctrl
|
884
|
+
}
|
885
|
+
/*
|
886
|
+
* @interface avalon.controller.loader avalon.controller异步引入模块的加载器,默认是通过avalon.require加载
|
887
|
+
*/
|
888
|
+
avalon.controller.loader = function(url, callback) {
|
889
|
+
// 没有错误回调...
|
890
|
+
avalon.require(url, function($ctrl) {
|
891
|
+
callback && callback($ctrl)
|
892
|
+
})
|
893
|
+
}
|
894
|
+
|
895
|
+
function _controller() {
|
896
|
+
if(!(this instanceof _controller)) return new _controller
|
897
|
+
this.$vmodels = []
|
898
|
+
}
|
899
|
+
_controller.prototype = {
|
900
|
+
}
|
901
|
+
|
902
|
+
function objectCompare(objA, objB) {
|
903
|
+
for(var i in objA) {
|
904
|
+
if(!(i in objB) || objA[i] !== objB[i]) return false
|
905
|
+
}
|
906
|
+
for(var i in objB) {
|
907
|
+
if(!(i in objA) || objA[i] !== objB[i]) return false
|
908
|
+
}
|
909
|
+
return true
|
910
|
+
}
|
911
|
+
|
912
|
+
//【avalon.state】的辅助函数,确保返回的是函数
|
913
|
+
function getFn(object, name) {
|
914
|
+
return typeof object[name] === "function" ? object[name] : avalon.noop
|
915
|
+
}
|
916
|
+
|
917
|
+
function getStateByName(stateName) {
|
918
|
+
return _states[stateName]
|
919
|
+
}
|
920
|
+
function getViewNodes(node, query) {
|
921
|
+
var nodes, query = query || "ms-view"
|
922
|
+
if(node.querySelectorAll) {
|
923
|
+
nodes = node.querySelectorAll("[" + query + "]")
|
924
|
+
} else {
|
925
|
+
nodes = Array.prototype.filter.call(node.getElementsByTagName("*"), function(node) {
|
926
|
+
return typeof node.getAttribute(query) === "string"
|
927
|
+
})
|
928
|
+
}
|
929
|
+
return nodes
|
930
|
+
}
|
931
|
+
|
932
|
+
// 【avalon.state】的辅助函数,opts.template的处理函数
|
933
|
+
function fromString(template, params, reason) {
|
934
|
+
var promise = getPromise(function(resolve, reject) {
|
935
|
+
var str = typeof template === "function" ? template(params) : template
|
936
|
+
if (typeof str == "string") {
|
937
|
+
resolve(str)
|
938
|
+
} else {
|
939
|
+
reason.message = "template必须对应一个字符串或一个返回字符串的函数"
|
940
|
+
reject(reason)
|
941
|
+
}
|
942
|
+
})
|
943
|
+
return promise
|
944
|
+
}
|
945
|
+
// 【fromUrl】的辅助函数,得到一个XMLHttpRequest对象
|
946
|
+
var getXHR = function() {
|
947
|
+
return new (window.XMLHttpRequest || ActiveXObject)("Microsoft.XMLHTTP")
|
948
|
+
}/*
|
949
|
+
* @interface avalon.state.templateLoader 通过url异步加载模板的函数,默认是通过内置的httpRequest去加载,但是在node-webkit环境是不work的,因此开放了这个配置,用以自定义url模板加载器,会在一个promise实例里调用这个方法去加载模板
|
950
|
+
* @param url 模板地址
|
951
|
+
* @param resolve 加载成功,如果需要缓存模板,请调用<br>
|
952
|
+
resolve(avalon.templateCache[url] = templateString)<br>
|
953
|
+
否则,请调用<br>
|
954
|
+
resolve(templateString)<br>
|
955
|
+
* @param reject 加载失败,请调用reject(reason)
|
956
|
+
* @param reason 挂在失败原因的对象
|
957
|
+
*/
|
958
|
+
avalon.state.templateLoader = function(url, resolve, reject, reason) {
|
959
|
+
var xhr = getXHR()
|
960
|
+
xhr.onreadystatechange = function() {
|
961
|
+
if (xhr.readyState === 4) {
|
962
|
+
var status = xhr.status;
|
963
|
+
if (status > 399 && status < 600) {
|
964
|
+
reason.message = "templateUrl对应资源不存在或没有开启 CORS"
|
965
|
+
reason.status = status
|
966
|
+
reason.xhr = xhr
|
967
|
+
reject(reason)
|
968
|
+
} else {
|
969
|
+
resolve(avalon.templateCache[url] = xhr.responseText)
|
970
|
+
}
|
971
|
+
}
|
972
|
+
}
|
973
|
+
xhr.open("GET", url, true)
|
974
|
+
if ("withCredentials" in xhr) {
|
975
|
+
xhr.withCredentials = true
|
976
|
+
}
|
977
|
+
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest")
|
978
|
+
xhr.send()
|
979
|
+
}
|
980
|
+
// 【avalon.state】的辅助函数,opts.templateUrl的处理函数
|
981
|
+
function fromUrl(url, params, reason) {
|
982
|
+
var promise = getPromise(function(resolve, reject) {
|
983
|
+
if (typeof url === "function") {
|
984
|
+
url = url(params)
|
985
|
+
}
|
986
|
+
if (typeof url !== "string") {
|
987
|
+
reason.message = "templateUrl必须对应一个URL"
|
988
|
+
return reject(reason)
|
989
|
+
}
|
990
|
+
if (avalon.templateCache[url]) {
|
991
|
+
return resolve(avalon.templateCache[url])
|
992
|
+
}
|
993
|
+
avalon.state.templateLoader(url, resolve, reject, reason)
|
994
|
+
})
|
995
|
+
return promise
|
996
|
+
}
|
997
|
+
// 【avalon.state】的辅助函数,opts.templateProvider的处理函数
|
998
|
+
function fromProvider(fn, params, reason) {
|
999
|
+
var promise = getPromise(function(resolve, reject) {
|
1000
|
+
if (typeof fn === "function") {
|
1001
|
+
var ret = fn(params)
|
1002
|
+
if (ret && ret.then || typeof ret == "string") {
|
1003
|
+
resolve(ret)
|
1004
|
+
} else {
|
1005
|
+
reason.message = "templateProvider为函数时应该返回一个Promise或thenable对象或字符串"
|
1006
|
+
reject(reason)
|
1007
|
+
}
|
1008
|
+
} else if (fn && fn.then) {
|
1009
|
+
resolve(fn)
|
1010
|
+
} else {
|
1011
|
+
reason.message = "templateProvider不为函数时应该对应一个Promise或thenable对象"
|
1012
|
+
reject(reason)
|
1013
|
+
}
|
1014
|
+
})
|
1015
|
+
return promise
|
1016
|
+
}
|
1017
|
+
// 【avalon.state】的辅助函数,将template或templateUrl或templateProvider转换为可用的Promise对象
|
1018
|
+
function fromPromise(config, params, reason) {
|
1019
|
+
return config.template ? fromString(config.template, params, reason) :
|
1020
|
+
config.templateUrl ? fromUrl(config.templateUrl, params, reason) :
|
1021
|
+
config.templateProvider ? fromProvider(config.templateProvider, params, reason) :
|
1022
|
+
getPromise(function(resolve, reject) {
|
1023
|
+
reason.message = "必须存在template, templateUrl, templateProvider中的一个"
|
1024
|
+
reject(reason)
|
1025
|
+
})
|
1026
|
+
}
|
1027
|
+
})
|