forked from cordjs/core
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathContext.coffee
267 lines (220 loc) · 8.59 KB
/
Context.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
define [
'cord!Collection'
'cord!Model'
'cord!utils/Defer'
'cord!utils/Future'
'postal'
'underscore'
'cord!Console'
'cord!isBrowser'
], (Collection, Model, Defer, Future, postal, _, _console, isBrowser) ->
class Context
constructor: (arg1, arg2) ->
###
@param {Object|String} arg1 initial context values or widget ID
@param (optional) {Object} arg2 initial context values (if first value is ID
###
@[':internal'] = {}
@[':internal'].version = 0
initCtx = {}
if _.isObject(arg1)
initCtx = arg1
else
@id = arg1
initCtx = arg2 if _.isObject(arg2)
for key, value of initCtx
@[key] = value
@_initDeferredDebug(key)
setOwnerWidget: (owner) ->
if owner
Object.defineProperty @, "_ownerWidget",
value: owner
writable: true
enumerable: false
@_ownerWidget
set: (args...) ->
triggerChange = false
if args.length == 0
throw "Invalid number of arguments! Should be 1 or 2."
else if args.length == 1
pairs = args[0]
if typeof pairs is 'object'
for key, value of pairs
if @setSingle key, value
triggerChange = true
else
throw "Invalid argument! Single argument must be key-value pair (object)."
else if @setSingle args[0], args[1]
triggerChange = true
#Prevent multiple someChange events in one tick
if triggerChange && !@_someChangeNotHappened
@_someChangeNotHappened = true
Defer.nextTick =>
postal.publish "widget.#{ @id }.someChange", {}
@_someChangeNotHappened = false
setSingle: (name, newValue, callbackPromise) ->
###
Sets single context param's value
@deprecated Use Context::set() instead
@param String name param name
@param Any newValue param value
@param (optional)Future callbackPromise promise to support setWithCallback() method functionality
@return Boolean true if the change event was triggered (the value was changed)
###
stashChange = true
if newValue != undefined
if @[name] == ':deferred'
# if the current value special :deferred than event should be triggered even if the new value is null
triggerChange = (newValue != ':deferred')
@_clearDeferredDebug(name) if triggerChange
# stashing should be turned off for modifying from :deferred except the value has become :deferred during
# widget template rendering (when stashing is enabled)
stashChange = @[':internal'].deferredStash?[name]
else
oldValue = @[name]
if oldValue == null
# null needs special check because null == null in javascript isn't true
triggerChange = (newValue != null)
else
triggerChange = (newValue != oldValue)
else
triggerChange = false
# _console.log "setSingle -> #{ name } = #{ newValue } (oldValue = #{ @[name] }) trigger = #{ triggerChange } -> #{ (new Date).getTime() }"
# never change value to 'undefined' (don't mix up with 'null' value)
@[name] = newValue if newValue != undefined
if triggerChange
callbackPromise.fork() if callbackPromise
curVersion = ++@[':internal'].version
if @[':internal'].stash
if newValue == ':deferred'
# if the value become deferred during enabled stashing then we should remember it to allow stashing
# when it'll set again. Otherwise behaviour can miss some change events emitted during widget rendering.
@[':internal'].deferredStash ?= {}
@[':internal'].deferredStash[name] = true
else if stashChange
cursor = _.uniqueId()
@[':internal'].stash.push
id: @id
name: name
newValue: newValue
oldValue: oldValue
cursor: cursor
version: curVersion
Defer.nextTick =>
_console.log "publish widget.#{ @id }.change.#{ name }" if global.config.debug.widget
postal.publish "widget.#{ @id }.change.#{ name }",
name: name
value: newValue
oldValue: oldValue
callbackPromise: callbackPromise
cursor: cursor
version: curVersion
callbackPromise.resolve() if callbackPromise
@_initDeferredDebug(name)
triggerChange
setDeferred: (args...) ->
for name in args
@setSingle(name, ':deferred')
setServerDeferred: (args...) ->
if not isBrowser
for name in args
@setSingle(name, ':deferred')
isDeferred: (name) ->
@[name] is ':deferred'
isEmpty: (name) ->
(not @[name]?) or @isDeferred(name)
setWithFeedback: (name, value) ->
###
Sets the context param's value as usual but injects future to the event data and returns it.
By default if event handlers doesn't support injected callback promise, the future will be completed immediately
after calling all event handlers. But some event handlers can support the promise and defer their completion
depending of some of their async activity.
@param String name param name
@param Any value param value
@return Future
###
callbackPromise = new Future('Context::setWithFeedback')
@setSingle(name, value, callbackPromise)
callbackPromise
stashEvents: ->
@[':internal'].stash = []
replayStashedEvents: ->
###
Re-triggers stashed context-change events.
Stashing is needed after Widget::setParams() is already processed but browserInit() still didn't executed,
so child widget's and behaviour will miss context changing which ocasionally happens during that time.
@browser-only
###
if @[':internal'].stash and @[':internal'].stash.length
originalStash = @[':internal'].stash
@[':internal'].stash = null
@[':internal'].deferredStash = null
Defer.nextTick =>
for ev in originalStash
postal.publish "widget.#{ ev.id }.change.#{ ev.name }",
name: ev.name
value: ev.newValue
oldValue: ev.oldValue
cursor: ev.cursor
version: ev.version
stashed: true
getVersion: ->
@[':internal'].version
toJSON: ->
result = {}
for key, value of this
if value instanceof Collection
result[key] = value.serializeLink()
else if value instanceof Model
result[key] = value.serializeLink()
else if _.isArray(value) and value[0] instanceof Model
result[key] = (m.serializeLink() for m in value)
else if key != ':internal'
result[key] = value
result
@fromJSON: (obj, ioc) ->
promise = new Future('Context::fromJSON')
for key, value of obj
do (key, value) ->
if Collection.isSerializedLink(value)
promise.fork()
Collection.unserializeLink value, ioc, (collection) ->
obj[key] = collection
promise.resolve()
else if Model.isSerializedLink(value)
promise.fork()
Model.unserializeLink value, ioc, (model) ->
obj[key] = model
promise.resolve()
else if _.isArray(value) and Model.isSerializedLink(value[0])
obj[key] = []
for link in value
promise.fork()
Model.unserializeLink link, ioc, (model) ->
obj[key].push(model)
promise.resolve()
else
obj[key] = value
promise.then => new this(obj)
clearDeferredTimeouts: ->
###
Prevents debug timeouts for deferred values to be redundantly logged when the owner widget is going to die
###
if @[':internal'].deferredTimeouts?
clearTimeout(timeout) for timeout in @[':internal'].deferredTimeouts
@[':internal'].deferredTimeouts = null
_initDeferredDebug: (name) ->
timeout = global.config?.debug.deferred.timeout
if @[name] == ':deferred' and timeout > 0
@[':internal'].deferredTimeouts ?= {}
dt = @[':internal'].deferredTimeouts
clearTimeout(dt[name]) if dt[name]
dt[name] = setTimeout =>
_console.warn '### Deferred timeout', name, @id, @_owner?.constructor.__name if @[name] == ':deferred'
delete dt[name]
, timeout
_clearDeferredDebug: (name) ->
dt = @[':internal'].deferredTimeouts
if dt and dt[name]
clearTimeout(dt[name])
delete dt[name]