/
app.coffee
311 lines (261 loc) · 8.96 KB
/
app.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
define "kopi/app", (require, exports, module) ->
$ = require "jquery"
settings = require "kopi/settings"
logging = require "kopi/logging"
events = require "kopi/events"
utils = require "kopi/utils"
uri = require "kopi/utils/uri"
support = require "kopi/utils/support"
text = require "kopi/utils/text"
array = require "kopi/utils/array"
klass = require "kopi/utils/klass"
viewport = require "kopi/ui/viewport"
overlays = require "kopi/ui/notification/overlays"
win = $(window)
hist = history
loc = location
baseURL = uri.current()
logger = logging.logger(module.id)
###
Application class
Usage
class BookApp extends App
onstart: ->
super
# Do stuff when app starts
self = this
self.bookNavBar = new BookNavBar()
self.bookViewer = new BookViewer()
self.taskQueue = new TaskQueue()
self.taskWorker = new TaskWorker(self.taskQueue)
onrequest: (e, url) ->
if url.match(/^\/books$/)
# Do stuff when url matches /books
else if url.match(/^\/books/\d+$/)
# Do stuff when url matches /books/1
super
$ ->
new BookApp().start()
###
class App extends events.EventEmitter
@START_EVENT = "start"
@REQUEST_EVENT = "request"
@VIEW_LOAD_EVENT = "viewload"
@LOCK_EVENT = "lock"
@UNLOCK_EVENT = "unlock"
klass.singleton this
klass.configure this
klass.accessor @prototype, "router"
constructor: (options={}) ->
@_isSingleton()
self = this
self.guid = utils.guid("app")
self.started = false
self.locked = false
self.currentURL = null
self.currentView = null
self._views = {}
self._interval = null
self._router = null
self.configure settings.kopi.app, options
lock: ->
self = this
return self if self.locked
cls = this.constructor
overlays.show(transparent: true)
self.emit(cls.LOCK_EVENT)
unlock: ->
self = this
return self unless self.locked
cls = this.constructor
overlays.hide()
self.emit(cls.UNLOCK_EVENT)
onlock: ->
this.locked = true
onunlock: ->
this.locked = false
###
Launch the application
###
start: ->
cls = this.constructor
self = this
if self.started
logger.warn("[app:start] App has already been launched.")
return self
# Ensure layout elements
self.container = $("body")
self.viewport = viewport.instance()
self.viewport.skeleton().render()
self._listenToURLChange()
self.emit(cls.START_EVENT)
logger.info "[app:start] Start app: #{self.guid}"
self.started = true
# Load current URL
unless support.history and self._options.usePushState
self.load(self._options.startURL or self.getCurrentURL())
self
stop: ->
self = this
self._stopListenToURLChange()
self.started = false
self
getCurrentURL: ->
url = uri.parse(location.href)
uri.absolute((if url.fragment then url.fragment.substr(1) else ""), url.urlNoQuery)
###
load URL
@param {String} url URL must be an absolute path without query string and fragment
@param {Hash} options
###
load: (url, options) ->
logger.info "[app:load] Load URL: #{url}"
cls = this.constructor
self = this
url = uri.parse uri.absolute(url)
# When using hash, absolute URLs will be converted to relative hashes.
# e.g.
# If baseURI is /foo and absolute URL is /foo/bar
# The request url will be /foo#/bar
if support.history and self._options.usePushState
if self._options.alwaysUseHash
state = "#" + uri.relative(url.urlNoQuery, baseURL)
else
state = url.path
self.once cls.VIEW_LOAD_EVENT, ->
hist.pushState(null, null, state)
else if self._options.useHashChange or self._options.useInterval
# Set hash until view is loaded
self.once cls.VIEW_LOAD_EVENT, ->
loc.hash = uri.relative(url.urlNoQuery, baseURL)
self.emit(cls.REQUEST_EVENT, [url, options])
self
###
callback when app receives new request
@param {Event} e
@param {kopi.utils.uri.URI} url
###
onrequest: (e, url, options) ->
logger.info "[app:onrequest] Receive request: #{url.path}"
self = this
cls = this.constructor
match = self._match(url)
if not match
logger.info "[app:onrequest] No matching view found."
if self._options.redirectWhenNoRouteFound
url = uri.unparse url
logger.info("[app:onrequest] Redirect to URL: #{url}")
uri.goto url
return
[view, request] = match
isUpdate = false
# If views are same, update the current view
if self.currentView and self.currentView.equals(view)
isUpdate = true
self.currentView.update(request.url, request.params, options)
return
# If view is not created, create view then start
view.create() unless view.created
view.start(request.url, request.params, options)
# If views are different, stop current view and start target view
if not isUpdate and self.currentView and self.currentView.started
self.currentView.stop(options)
self.currentView = view
self.currentURL = url
self.emit(cls.VIEW_LOAD_EVENT)
###
Listen to URL change events.
For HTML5 browsers, listen to `onpopstate` event by default.
For HTML4 browsers, listen to `onhashchange` event by default.
For Legacy browsers, check url change by interval.
###
_listenToURLChange: ->
self = this
checkFn = -> self._checkURLChange()
if support.history and self._options.usePushState
self._useHash = self._options.alwaysUseHash
win.bind 'popstate', checkFn
else if support.hash and (self._options.usePushState or self._options.useHashChange)
self._useHash = true
win.bind "hashchange", checkFn
else if self._options.useInterval
self._useHash = true
self._interval = setInterval checkFn, self._options.interval
else
logger.warn("[app:_listenToURLChange] App will not repond to url change")
return
###
Listen to URL change events.
For HTML5 browsers, stop listen to `onpopstate` event by default.
For HTML4 browsers, stop listen listen to `onhashchange` event by default.
For Legacy browsers, stop listen check url change by interval.
###
_stopListenToURLChange: ->
self = this
if support.history and self._options.usePushState
win.unbind 'popstate'
else if support.hash and self._options.useHashChange
win.unbind "hashchange"
else if self._options.useInterval
if self._interval
clearInterval(self._interval)
self._interval = null
return
###
Check if URL is different from last state
###
_checkURLChange: ->
cls = this.constructor
self = this
url = uri.parse(location.href)
if self._useHash
# Combine path and hash
url.path = uri.absolute(url.fragment.replace(/^#/, ''), url.path)
if not self.currentURL or url.path != self.currentURL.path
self.currentURL = url
self.emit(cls.REQUEST_EVENT, [url])
self
###
Find existing view in stack
@param {kopi.utils.uri.URI} url
@return {kopi.views.View}
###
_match: (url) ->
return logger.warn "[app:_match] Router is not provided" unless @_router
self = this
path = uri.parse(url).path
request = @_router.match(path)
# If no proper router is found
return logger.warn("[app:_match] Can not find proper route for path: #{path}") unless request
route = request.route
for guid, view of self._views
# If `group` is `true`, use same view for every URL matches route
if route.group is true and view.constructor.viewName() == route.view.viewName()
logger.log "[app:_match] group: true"
return [view, request]
# If `group` is `string`, use same view for every URL matches route
else if text.isString(route.group)
if view.params[route.group] == route.params[route.group]
return [view, request]
# If `group` is `array`, use same view for every URL matches route
else if array.isArray(route.group)
matches = true
for param in route.group
if view.params[param] != route.params[param]
matches = false
break
if matches
return [view, request]
# If `group` is false, use different view for different URLs
else
if view.url.path == path
return [view, request]
# Create view and add it to list
view = new route.view(self, request.url, request.params)
self._views[view.guid] = view
[view, request]
App: App
# DEPRECATED
# Use App.instance() instead
# -- Wu Yuntao, 2013-07-01
instance: -> App.instance()