avalon-mmRouter-rails 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
})
|