unpoly-rails 0.37.0 → 0.50.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of unpoly-rails might be problematic. Click here for more details.

Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +127 -25
  3. data/LICENSE +1 -1
  4. data/README_RAILS.md +4 -2
  5. data/Rakefile +6 -1
  6. data/dist/unpoly.js +3192 -2198
  7. data/dist/unpoly.min.js +4 -3
  8. data/lib/assets/javascripts/unpoly/browser.coffee +51 -63
  9. data/lib/assets/javascripts/unpoly/bus.coffee +58 -33
  10. data/lib/assets/javascripts/unpoly/classes/cache.coffee +117 -0
  11. data/lib/assets/javascripts/unpoly/{dom → classes}/extract_cascade.coffee +3 -3
  12. data/lib/assets/javascripts/unpoly/{dom → classes}/extract_plan.coffee +1 -1
  13. data/lib/assets/javascripts/unpoly/classes/field_observer.coffee +57 -0
  14. data/lib/assets/javascripts/unpoly/classes/follow_variant.coffee +52 -0
  15. data/lib/assets/javascripts/unpoly/classes/motion_tracker.coffee +95 -0
  16. data/lib/assets/javascripts/unpoly/classes/record.coffee +16 -0
  17. data/lib/assets/javascripts/unpoly/classes/request.coffee +228 -0
  18. data/lib/assets/javascripts/unpoly/classes/response.coffee +138 -0
  19. data/lib/assets/javascripts/unpoly/dom.coffee +151 -142
  20. data/lib/assets/javascripts/unpoly/feedback.coffee +67 -38
  21. data/lib/assets/javascripts/unpoly/form.coffee +156 -139
  22. data/lib/assets/javascripts/unpoly/history.coffee +22 -19
  23. data/lib/assets/javascripts/unpoly/layout.coffee +108 -90
  24. data/lib/assets/javascripts/unpoly/link.coffee +159 -158
  25. data/lib/assets/javascripts/unpoly/log.coffee +5 -5
  26. data/lib/assets/javascripts/unpoly/modal.coffee +93 -81
  27. data/lib/assets/javascripts/unpoly/motion.coffee +291 -250
  28. data/lib/assets/javascripts/unpoly/popup.coffee +67 -53
  29. data/lib/assets/javascripts/unpoly/protocol.coffee +67 -16
  30. data/lib/assets/javascripts/unpoly/proxy.coffee +282 -211
  31. data/lib/assets/javascripts/unpoly/rails.coffee +3 -14
  32. data/lib/assets/javascripts/unpoly/syntax.coffee +54 -49
  33. data/lib/assets/javascripts/unpoly/tooltip.coffee +18 -25
  34. data/lib/assets/javascripts/unpoly/util.coffee +236 -477
  35. data/lib/assets/javascripts/unpoly.coffee +1 -1
  36. data/lib/unpoly/rails/inspector.rb +67 -22
  37. data/lib/unpoly/rails/version.rb +1 -1
  38. data/package.json +1 -1
  39. data/spec_app/Gemfile.lock +13 -13
  40. data/spec_app/app/assets/javascripts/integration_test.coffee +1 -0
  41. data/spec_app/app/assets/javascripts/jasmine_specs.coffee +1 -1
  42. data/spec_app/app/assets/stylesheets/jasmine_specs.sass +10 -0
  43. data/spec_app/app/controllers/binding_test_controller.rb +19 -2
  44. data/spec_app/app/controllers/method_test_controller.rb +16 -0
  45. data/spec_app/app/views/layouts/jasmine_rails/spec_runner.html.erb +20 -0
  46. data/spec_app/app/views/method_test/form_target.erb +17 -0
  47. data/spec_app/app/views/method_test/page1.erb +11 -0
  48. data/spec_app/app/views/method_test/page2.erb +6 -0
  49. data/spec_app/app/views/pages/start.erb +33 -19
  50. data/spec_app/config/initializers/assets.rb +5 -0
  51. data/spec_app/config/routes.rb +3 -0
  52. data/spec_app/spec/controllers/binding_test_controller_spec.rb +82 -27
  53. data/spec_app/spec/javascripts/helpers/agent_detector.coffee +17 -0
  54. data/spec_app/spec/javascripts/helpers/async_sequence.js.coffee +102 -0
  55. data/spec_app/spec/javascripts/helpers/last_request.js.coffee +1 -1
  56. data/spec_app/spec/javascripts/helpers/mock_ajax.js.coffee +5 -2
  57. data/spec_app/spec/javascripts/helpers/promise_state.js +18 -0
  58. data/spec_app/spec/javascripts/helpers/protect_jasmine_runner.coffee +9 -0
  59. data/spec_app/spec/javascripts/helpers/reset_history.js.coffee +22 -0
  60. data/spec_app/spec/javascripts/helpers/reset_up.js.coffee +11 -3
  61. data/spec_app/spec/javascripts/helpers/show_lib_versions.coffee +10 -0
  62. data/spec_app/spec/javascripts/helpers/to_be_error.coffee +5 -0
  63. data/spec_app/spec/javascripts/helpers/to_match_url.coffee +13 -0
  64. data/spec_app/spec/javascripts/helpers/trigger.js.coffee +13 -6
  65. data/spec_app/spec/javascripts/up/browser_spec.js.coffee +92 -33
  66. data/spec_app/spec/javascripts/up/bus_spec.js.coffee +64 -15
  67. data/spec_app/spec/javascripts/up/classes/.keep +0 -0
  68. data/spec_app/spec/javascripts/up/classes/cache_spec.js.coffee +1 -0
  69. data/spec_app/spec/javascripts/up/dom_spec.js.coffee +759 -551
  70. data/spec_app/spec/javascripts/up/feedback_spec.js.coffee +155 -82
  71. data/spec_app/spec/javascripts/up/form_spec.js.coffee +490 -349
  72. data/spec_app/spec/javascripts/up/history_spec.js.coffee +226 -179
  73. data/spec_app/spec/javascripts/up/layout_spec.js.coffee +253 -185
  74. data/spec_app/spec/javascripts/up/link_spec.js.coffee +416 -270
  75. data/spec_app/spec/javascripts/up/modal_spec.js.coffee +459 -330
  76. data/spec_app/spec/javascripts/up/motion_spec.js.coffee +198 -153
  77. data/spec_app/spec/javascripts/up/namespace_spec.js.coffee +9 -0
  78. data/spec_app/spec/javascripts/up/popup_spec.js.coffee +240 -175
  79. data/spec_app/spec/javascripts/up/protocol_spec.js.coffee +38 -0
  80. data/spec_app/spec/javascripts/up/proxy_spec.js.coffee +777 -303
  81. data/spec_app/spec/javascripts/up/rails_spec.js.coffee +24 -8
  82. data/spec_app/spec/javascripts/up/syntax_spec.js.coffee +40 -23
  83. data/spec_app/spec/javascripts/up/tooltip_spec.js.coffee +80 -66
  84. data/spec_app/spec/javascripts/up/util_spec.js.coffee +227 -201
  85. data/spec_app/vendor/asset-libs/es6-promise-4.1.6/es6-promise.auto.js +1159 -0
  86. metadata +30 -7
  87. data/spec_app/spec/javascripts/helpers/reset_path.js.coffee +0 -7
  88. data/spec_app/spec/javascripts/helpers/to_equal_url.coffee +0 -11
@@ -2,14 +2,16 @@
2
2
  Browser support
3
3
  ===============
4
4
 
5
- Unpoly supports all modern browsers. It degrades gracefully with old versions of Internet Explorer:
5
+ Unpoly supports all modern browsers.
6
6
 
7
- IE11, Edge
7
+ Chrome, Firefox, Edge, Safari
8
8
  : Full support
9
9
 
10
- IE 10 or lower
11
- : Unpoly prevents itself from booting itself, leaving you with a classic server-side application.
10
+ Internet Explorer 11
11
+ : Full support with a `Promise` polyfill like [es6-promise](https://github.com/stefanpenner/es6-promise) (2.4 KB).
12
12
 
13
+ Internet Explorer 10 or lower
14
+ : Unpoly prevents itself from booting itself, leaving you with a classic server-side application.
13
15
 
14
16
  @class up.browser
15
17
  ###
@@ -18,30 +20,15 @@ up.browser = (($) ->
18
20
  u = up.util
19
21
 
20
22
  ###*
21
- @method up.browser.loadPage
22
- @param {String} url
23
- @param {String} [options.method='get']
23
+ @method up.browser.navigate
24
+ @param {string} url
25
+ @param {string} [options.method='get']
24
26
  @param {Object|Array} [options.data]
25
27
  @internal
26
28
  ###
27
- loadPage = (url, options = {}) ->
28
- method = u.option(options.method, 'get').toLowerCase()
29
- if method == 'get'
30
- query = u.requestDataAsQuery(options.data)
31
- url = "#{url}?#{query}" if query
32
- setLocationHref(url)
33
- else
34
- $form = $("<form method='post' action='#{url}' class='up-page-loader'></form>")
35
- addField = (field) ->
36
- $field = $('<input type="hidden">')
37
- $field.attr(field)
38
- $field.appendTo($form)
39
- addField(name: up.protocol.config.methodParam, value: method)
40
- if csrfField = up.rails.csrfField()
41
- addField(csrfField)
42
- u.each u.requestDataAsArray(options.data), addField
43
- $form.hide().appendTo('body')
44
- submitForm($form)
29
+ navigate = (url, options = {}) ->
30
+ request = new up.Request(u.merge(options, { url }))
31
+ request.navigate()
45
32
 
46
33
  ###*
47
34
  For mocking in specs.
@@ -51,14 +38,6 @@ up.browser = (($) ->
51
38
  submitForm = ($form) ->
52
39
  $form.submit()
53
40
 
54
- ###*
55
- For mocking in specs.
56
-
57
- @method setLocationHref
58
- ###
59
- setLocationHref = (url) ->
60
- location.href = url
61
-
62
41
  ###*
63
42
  A cross-browser way to interact with `console.log`, `console.error`, etc.
64
43
 
@@ -155,10 +134,10 @@ up.browser = (($) ->
155
134
  a request method other than GET.
156
135
 
157
136
  @function up.browser.canPushState
158
- @return {Boolean}
137
+ @return {boolean}
159
138
  @experimental
160
139
  ###
161
- canPushState = u.memoize ->
140
+ canPushState = ->
162
141
  # We cannot use pushState if the initial request method is a POST for two reasons:
163
142
  #
164
143
  # 1. Unpoly replaces the initial state so it can handle the pop event when the
@@ -168,7 +147,8 @@ up.browser = (($) ->
168
147
  # 2. Some browsers have a bug where the initial request method is used for all
169
148
  # subsequently pushed states. That means if the user reloads the page on a later
170
149
  # GET state, the browser will wrongly attempt a POST request.
171
- # Modern Firefoxes, Chromes and IE10+ don't seem to be affected by this.
150
+ # This issue affects Safari 9 and 10 (last tested in 2017-08).
151
+ # Modern Firefoxes, Chromes and IE10+ don't have this behavior.
172
152
  #
173
153
  # The way that we work around this is that we don't support pushState if the
174
154
  # initial request method was anything other than GET (but allow the rest of the
@@ -185,52 +165,62 @@ up.browser = (($) ->
185
165
  animation by instantly jumping to the last frame.
186
166
 
187
167
  @function up.browser.canCssTransition
188
- @return {Boolean}
168
+ @return {boolean}
189
169
  @internal
190
170
  ###
191
- canCssTransition = u.memoize ->
171
+ canCssTransition = ->
192
172
  'transition' of document.documentElement.style
193
173
 
194
174
  ###*
195
175
  Returns whether this browser supports the DOM event [`input`](https://developer.mozilla.org/de/docs/Web/Events/input).
196
176
 
197
177
  @function up.browser.canInputEvent
198
- @return {Boolean}
178
+ @return {boolean}
199
179
  @internal
200
180
  ###
201
- canInputEvent = u.memoize ->
181
+ canInputEvent = ->
202
182
  'oninput' of document.createElement('input')
203
183
 
184
+ ###*
185
+ Returns whether this browser supports promises.
186
+
187
+ @function up.browser.canPromise
188
+ @return {boolean}
189
+ @internal
190
+ ###
191
+ canPromise = ->
192
+ !!window.Promise
193
+
204
194
  ###*
205
195
  Returns whether this browser supports the [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData)
206
196
  interface.
207
197
 
208
198
  @function up.browser.canFormData
209
- @return {Boolean}
199
+ @return {boolean}
210
200
  @experimental
211
201
  ###
212
- canFormData = u.memoize ->
202
+ canFormData = ->
213
203
  !!window.FormData
214
204
 
215
205
  ###*
216
206
  Returns whether this browser supports the [`DOMParser`](https://developer.mozilla.org/en-US/docs/Web/API/DOMParser)
217
207
  interface.
218
208
 
219
- @function up.browser.canDomParser
220
- @return {Boolean}
209
+ @function up.browser.canDOMParser
210
+ @return {boolean}
221
211
  @internal
222
212
  ###
223
- canDomParser = u.memoize ->
213
+ canDOMParser = ->
224
214
  !!window.DOMParser
225
215
 
226
216
  ###*
227
217
  Returns whether this browser supports the [`debugging console`](https://developer.mozilla.org/en-US/docs/Web/API/Console).
228
218
 
229
219
  @function up.browser.canConsole
230
- @return {Boolean}
220
+ @return {boolean}
231
221
  @internal
232
222
  ###
233
- canConsole = u.memoize ->
223
+ canConsole = ->
234
224
  window.console &&
235
225
  console.debug &&
236
226
  console.info &&
@@ -240,7 +230,7 @@ up.browser = (($) ->
240
230
  console.groupCollapsed &&
241
231
  console.groupEnd
242
232
 
243
- isRecentJQuery = u.memoize ->
233
+ isRecentJQuery = ->
244
234
  version = $.fn.jquery
245
235
  parts = version.split('.')
246
236
  major = parseInt(parts[0])
@@ -264,15 +254,15 @@ up.browser = (($) ->
264
254
  ###*
265
255
  @function up,browser.whenConfirmed
266
256
  @return {Promise}
267
- @param {String} options.confirm
268
- @param {Boolean} options.preload
257
+ @param {string} options.confirm
258
+ @param {boolean} options.preload
269
259
  @internal
270
260
  ###
271
261
  whenConfirmed = (options) ->
272
262
  if options.preload || u.isBlank(options.confirm) || window.confirm(options.confirm)
273
- u.resolvedPromise()
263
+ Promise.resolve()
274
264
  else
275
- u.unresolvablePromise()
265
+ Promise.reject(new Error('User canceled action'))
276
266
 
277
267
  ###*
278
268
  Returns whether Unpoly supports the current browser.
@@ -283,13 +273,6 @@ up.browser = (($) ->
283
273
  This is usually a better fallback than loading incompatible Javascript and causing
284
274
  many errors on load.
285
275
 
286
- \#\#\# Graceful degradation
287
-
288
- This function also returns `true` if Unpoly only support some features, but can degrade
289
- gracefully for other features. E.g. Internet Explorer 9 is almost fully supported, but due to
290
- its lack of [`history.pushState`](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState)
291
- Unpoly falls back to a full page load when asked to manipulate history.
292
-
293
276
  @function up.browser.isSupported
294
277
  @stable
295
278
  ###
@@ -297,11 +280,13 @@ up.browser = (($) ->
297
280
  !isIE10OrWorse() &&
298
281
  isRecentJQuery() &&
299
282
  canConsole() &&
300
- canPushState() &&
301
- canDomParser() &&
283
+ # We don't require pushState in order to cater for Safari booting Unpoly with a non-GET method.
284
+ # canPushState() &&
285
+ canDOMParser() &&
302
286
  canFormData() &&
303
287
  canCssTransition() &&
304
- canInputEvent()
288
+ canInputEvent() &&
289
+ canPromise()
305
290
 
306
291
  ###*
307
292
  @internal
@@ -342,7 +327,8 @@ up.browser = (($) ->
342
327
 
343
328
  knife: eval(Knife?.point)
344
329
  url: url
345
- loadPage: loadPage
330
+ navigate: navigate
331
+ submitForm: submitForm
346
332
  canPushState: canPushState
347
333
  whenConfirmed: whenConfirmed
348
334
  isSupported: isSupported
@@ -352,5 +338,7 @@ up.browser = (($) ->
352
338
  sessionStorage: sessionStorage
353
339
  popCookie: popCookie
354
340
  hash: hash
341
+ canPushState: canPushState
355
342
 
356
343
  )(jQuery)
344
+
@@ -55,6 +55,9 @@ up.bus = (($) ->
55
55
  liveUpDescriptions = {}
56
56
  nextUpDescriptionNumber = 0
57
57
 
58
+ # A hash mapping oldEventName => newEventName
59
+ renamedEvents = {}
60
+
58
61
  ###*
59
62
  Convert an Unpoly style listener (second argument is the event target
60
63
  as a jQuery collection) to a vanilla jQuery listener
@@ -76,6 +79,13 @@ up.bus = (($) ->
76
79
  ###
77
80
  upDescriptionToJqueryDescription = (upDescription, isNew) ->
78
81
  jqueryDescription = u.copy(upDescription)
82
+
83
+ # Prefer to rename events in the copied jQuery description instead of
84
+ # changing the original up description.
85
+ fixRenamedEvents(jqueryDescription)
86
+
87
+ # We remove the listener function from the end of the description.
88
+ # We will re-push it to the description at the end.
79
89
  upListener = jqueryDescription.pop()
80
90
  jqueryListener = undefined
81
91
  if isNew
@@ -84,10 +94,19 @@ up.bus = (($) ->
84
94
  upListener._descriptionNumber = ++nextUpDescriptionNumber
85
95
  else
86
96
  jqueryListener = upListener._asJqueryListener
87
- jqueryListener or up.fail('up.off: The event listener %o was never registered through up.on')
97
+ jqueryListener or up.fail('up.off(): The callback %o was never registered through up.on()', upListener)
88
98
  jqueryDescription.push(jqueryListener)
89
99
  jqueryDescription
90
100
 
101
+ fixRenamedEvents = (description) ->
102
+ events = description[0].split(/\s+/)
103
+ events = u.map events, (event) ->
104
+ if newEvent = renamedEvents[event]
105
+ up.log.warn("#{event} has been renamed to #{newEvent}")
106
+ newEvent
107
+ else
108
+ event
109
+ description[0] = events.join(' ')
91
110
 
92
111
  ###*
93
112
  Listens to an event on `document`.
@@ -165,9 +184,9 @@ up.bus = (($) ->
165
184
  });
166
185
 
167
186
  @function up.on
168
- @param {String} events
187
+ @param {string} events
169
188
  A space-separated list of event names to bind.
170
- @param {String} [selector]
189
+ @param {string} [selector]
171
190
  The selector of an element on which the event must be triggered.
172
191
  Omit the selector to listen to all events with that name, regardless
173
192
  of the event target.
@@ -251,7 +270,7 @@ up.bus = (($) ->
251
270
  # Prints "bar" to the console
252
271
 
253
272
  @function up.emit
254
- @param {String} eventName
273
+ @param {string} eventName
255
274
  The name of the event.
256
275
  @param {Object} [eventProps={}]
257
276
  A list of properties to become part of the event object
@@ -260,7 +279,7 @@ up.bus = (($) ->
260
279
  or `stopPropagation()`.
261
280
  @param {jQuery} [eventProps.$element=$(document)]
262
281
  The element on which the event is triggered.
263
- @param {String|Array} [eventProps.message]
282
+ @param {string|Array} [eventProps.message]
264
283
  A message to print to the console when the event is emitted.
265
284
  If omitted, a default message is printed.
266
285
  Set this to `false` to prevent any console output.
@@ -303,40 +322,37 @@ up.bus = (($) ->
303
322
  has prevented the default action.
304
323
 
305
324
  @function up.bus.nobodyPrevents
306
- @param {String} eventName
325
+ @param {string} eventName
307
326
  @param {Object} eventProps
308
- @param {String|Array} [eventProps.message]
309
- @return {Boolean}
327
+ @param {string|Array} [eventProps.message]
328
+ @return {boolean}
310
329
  whether no listener has prevented the default action
311
330
  @experimental
312
331
  ###
313
332
  nobodyPrevents = (args...) ->
314
333
  event = emit(args...)
315
- if event.isDefaultPrevented()
316
- up.puts "An observer prevented the event %s", args[0]
317
- false
318
- else
319
- true
334
+ not event.isDefaultPrevented()
320
335
 
321
336
  ###*
322
337
  [Emits](/up.emit) the given event and returns a promise
323
- that will be resolved if no listener has prevented the default action.
338
+ that will be fulfilled if no listener has prevented the default action.
324
339
 
325
340
  If any listener prevented the default listener
326
341
  the returned promise will never be resolved.
327
342
 
328
343
  @function up.bus.whenEmitted
329
- @param {String} eventName
344
+ @param {string} eventName
330
345
  @param {Object} eventProps
331
- @param {String|Array} [eventProps.message]
346
+ @param {string|Array} [eventProps.message]
332
347
  @return {Promise}
333
348
  @internal
334
349
  ###
335
350
  whenEmitted = (args...) ->
336
- deferred = $.Deferred()
337
- if nobodyPrevents(args...)
338
- deferred.resolve()
339
- deferred.promise()
351
+ new Promise (resolve, reject) ->
352
+ if nobodyPrevents(args...)
353
+ resolve()
354
+ else
355
+ reject(new Error("Event #{args[0]} was prevented"))
340
356
 
341
357
  ###*
342
358
  Registers an event listener to be called when the user
@@ -386,18 +402,22 @@ up.bus = (($) ->
386
402
  @internal
387
403
  ###
388
404
  snapshot = ->
389
- for description in liveUpDescriptions
405
+ for number, description of liveUpDescriptions
390
406
  description.isDefault = true
391
407
 
392
- ###*
393
- Resets the list of registered event listeners to the
394
- moment when the framework was booted.
408
+ resetBus = ->
409
+ # Resets the list of registered event listeners to the
410
+ # moment when the framework was booted.
395
411
 
396
- @internal
397
- ###
398
- restoreSnapshot = ->
399
- doomedDescriptions = u.reject(liveUpDescriptions, (description) -> description.isDefault)
400
- unbind(description...) for description in doomedDescriptions
412
+ # Copy a list of the descriptions we're going to unbind and iterate over
413
+ # them a second time below. This way we avoid manipulate the object we're
414
+ # iterating over.
415
+ doomedDescriptions = []
416
+ for number, description of liveUpDescriptions
417
+ doomedDescriptions.push(description) unless description.isDefault
418
+
419
+ for description in doomedDescriptions
420
+ unbind(description...)
401
421
 
402
422
  ###*
403
423
  Resets Unpoly to the state when it was booted.
@@ -414,6 +434,9 @@ up.bus = (($) ->
414
434
  ###
415
435
  emitReset = ->
416
436
  emit('up:framework:reset', message: 'Resetting framework')
437
+ # Unfortunately we cannot reset up.protocol via event
438
+ # without introducing cycles in the asset load order
439
+ up.protocol.reset()
417
440
 
418
441
  ###*
419
442
  This event is [emitted](/up.emit) when Unpoly is [reset](/up.reset) during unit tests.
@@ -422,6 +445,9 @@ up.bus = (($) ->
422
445
  @experimental
423
446
  ###
424
447
 
448
+ renamedEvent = (oldEvent, newEvent) ->
449
+ renamedEvents[oldEvent] = newEvent
450
+
425
451
  ###*
426
452
  Boots the Unpoly framework.
427
453
 
@@ -435,11 +461,9 @@ up.bus = (($) ->
435
461
  ###
436
462
  boot = ->
437
463
  if up.browser.isSupported()
438
- # Can't decouple this via the event bus, since up.bus would require
439
- # up.browser.isSupported() and up.browser would require up.on()
440
464
  emit('up:framework:boot', message: 'Booting framework')
465
+ # Unpoly modules now snapshot themselves to suppot reset()
441
466
  emit('up:framework:booted', message: 'Framework booted')
442
- # User-provided compiler definitions will be registered once this function terminates.
443
467
  u.nextFrame ->
444
468
  # At this point all user-provided compilers have been registered.
445
469
  u.whenReady().then ->
@@ -457,7 +481,7 @@ up.bus = (($) ->
457
481
  ###
458
482
 
459
483
  live 'up:framework:booted', snapshot
460
- live 'up:framework:reset', restoreSnapshot
484
+ live 'up:framework:reset', resetBus
461
485
 
462
486
  knife: eval(Knife?.point)
463
487
  on: live # can't name symbols `on` in Coffeescript
@@ -469,6 +493,7 @@ up.bus = (($) ->
469
493
  emitReset: emitReset
470
494
  haltEvent: haltEvent
471
495
  consumeAction: consumeAction
496
+ renamedEvent: renamedEvent
472
497
  boot: boot
473
498
 
474
499
  )(jQuery)
@@ -0,0 +1,117 @@
1
+ u = up.util
2
+
3
+ ###*
4
+ @class up.Cache
5
+ @internal
6
+ ###
7
+ class up.Cache
8
+
9
+ ###*
10
+ @constructor
11
+ @param {number|Function() :number} [config.size]
12
+ Maximum number of cache entries.
13
+ Set to `undefined` to not limit the cache size.
14
+ @param {number|Function(): number} [config.expiry]
15
+ The number of milliseconds after which a cache entry
16
+ will be discarded.
17
+ @param {string} [config.log]
18
+ A prefix for log entries printed by this cache object.
19
+ @param {Function(any): string} [config.key]
20
+ A function that takes an argument and returns a string key
21
+ for storage. If omitted, `toString()` is called on the argument.
22
+ @param {Function(any): boolean} [config.cachable]
23
+ A function that takes a potential cache entry and returns whether
24
+ this entry can be stored in the hash. If omitted, all entries are considered
25
+ cachable.
26
+ ###
27
+ constructor: (@config = {}) ->
28
+ @store = {}
29
+
30
+ maxKeys: =>
31
+ u.evalOption(@config.size)
32
+
33
+ expiryMillis: =>
34
+ u.evalOption(@config.expiry)
35
+
36
+ normalizeStoreKey: (key) =>
37
+ if @config.key
38
+ @config.key(key)
39
+ else
40
+ @key.toString()
41
+
42
+ isEnabled: =>
43
+ @maxKeys() isnt 0 && @expiryMillis() isnt 0
44
+
45
+ isCachable: (key) =>
46
+ if @config.cachable
47
+ @config.cachable(key)
48
+ else
49
+ true
50
+
51
+ clear: =>
52
+ @store = {}
53
+
54
+ log: (args...) =>
55
+ if @config.logPrefix
56
+ args[0] = "[#{@config.logPrefix}] #{args[0]}"
57
+ up.puts(args...)
58
+
59
+ keys: =>
60
+ Object.keys(@store)
61
+
62
+ makeRoomForAnotherKey: =>
63
+ storeKeys = u.copy(@keys())
64
+ max = @maxKeys()
65
+ if max && storeKeys.length >= max
66
+ oldestKey = null
67
+ oldestTimestamp = null
68
+ u.each storeKeys, (key) =>
69
+ promise = @store[key] # we don't need to call cacheKey here
70
+ timestamp = promise.timestamp
71
+ if !oldestTimestamp || oldestTimestamp > timestamp
72
+ oldestKey = key
73
+ oldestTimestamp = timestamp
74
+ delete @store[oldestKey] if oldestKey
75
+
76
+ alias: (oldKey, newKey) =>
77
+ value = @get(oldKey, silent: true)
78
+ if u.isDefined(value)
79
+ @set(newKey, value)
80
+
81
+ timestamp: =>
82
+ (new Date()).valueOf()
83
+
84
+ set: (key, value) =>
85
+ if @isEnabled() && @isCachable(key)
86
+ @makeRoomForAnotherKey()
87
+ storeKey = @normalizeStoreKey(key)
88
+ @log("Setting entry %o to %o", storeKey, value)
89
+ @store[storeKey] =
90
+ timestamp: @timestamp()
91
+ value: value
92
+
93
+ remove: (key) =>
94
+ if @isCachable(key)
95
+ storeKey = @normalizeStoreKey(key)
96
+ delete @store[storeKey]
97
+
98
+ isFresh: (entry) =>
99
+ millis = @expiryMillis()
100
+ if millis
101
+ timeSinceTouch = @timestamp() - entry.timestamp
102
+ timeSinceTouch < millis
103
+ else
104
+ true
105
+
106
+ get: (key, options = {}) =>
107
+ if @isCachable(key) && (entry = @store[@normalizeStoreKey(key)])
108
+ if @isFresh(entry)
109
+ @log("Cache hit for '%s'", key) unless options.silent
110
+ entry.value
111
+ else
112
+ @log("Discarding stale cache entry for '%s'", key) unless options.silent
113
+ @remove(key)
114
+ undefined
115
+ else
116
+ @log("Cache miss for '%s'", key) unless options.silent
117
+ undefined
@@ -1,6 +1,6 @@
1
1
  u = up.util
2
2
 
3
- class up.dom.ExtractCascade
3
+ class up.ExtractCascade
4
4
 
5
5
  constructor: (selector, options) ->
6
6
  @options = u.options(options, humanizedTarget: 'selector', layer: 'auto')
@@ -11,7 +11,7 @@ class up.dom.ExtractCascade
11
11
  # If we're using a fallback (any candidate that's not the first),
12
12
  # the original transition might no longer be appropriate.
13
13
  planOptions.transition = up.dom.config.fallbackTransition
14
- new up.dom.ExtractPlan(candidate, planOptions)
14
+ new up.ExtractPlan(candidate, planOptions)
15
15
 
16
16
  buildCandidates: (selector) ->
17
17
  candidates = [selector, @options.fallback, up.dom.config.fallbacks]
@@ -63,7 +63,7 @@ class up.dom.ExtractCascade
63
63
  message = "Could not find #{@options.humanizedTarget} in response"
64
64
  else
65
65
  message = "Could not match #{@options.humanizedTarget} in current page and response"
66
- if @response && @options.inspectResponse
66
+ if @options.inspectResponse
67
67
  inspectAction = { label: 'Open response', callback: @options.inspectResponse }
68
68
  up.fail(["#{message} (tried %o)", @candidates], action: inspectAction)
69
69
 
@@ -1,6 +1,6 @@
1
1
  u = up.util
2
2
 
3
- class up.dom.ExtractPlan
3
+ class up.ExtractPlan
4
4
 
5
5
  constructor: (selector, options) ->
6
6
  @origin = options.origin
@@ -0,0 +1,57 @@
1
+ u = up.util
2
+
3
+ class up.FieldObserver
4
+
5
+ # Although (depending on the browser) we only need/receive either input or change,
6
+ # we always bind to both events in case another script manually triggers it.
7
+ CHANGE_EVENTS = 'input change'
8
+
9
+ constructor: (@$field, options) ->
10
+ @delay = options.delay
11
+ @callback = options.callback
12
+
13
+ start: =>
14
+ # Don't use undefined since an unchecked checkbox actually has an undefined value
15
+ @scheduledValue = null
16
+ @processedValue = @readFieldValue()
17
+ @currentTimer = undefined
18
+ @currentCallback = undefined
19
+ @$field.on(CHANGE_EVENTS, @check)
20
+
21
+ stop: =>
22
+ @$field.off(CHANGE_EVENTS, @check)
23
+ @cancelTimer()
24
+
25
+ cancelTimer: =>
26
+ clearTimeout(@currentTimer)
27
+ @currentTimer = undefined
28
+
29
+ scheduleTimer: =>
30
+ @currentTimer = u.setTimer @delay, =>
31
+ @currentTimer = undefined
32
+ @requestCallback()
33
+
34
+ isNewValue: (value) =>
35
+ value != @processedValue && (@scheduledValue == null || @scheduledValue != value)
36
+
37
+ requestCallback: =>
38
+ if @scheduledValue != null && !@currentTimer && !@currentCallback
39
+ @processedValue = @scheduledValue
40
+ @scheduledValue = null
41
+ @currentCallback = => @callback.call(@$field.get(0), @processedValue, @$field)
42
+ # If the callback returns a promise x, Promise.resolve(x) will wait for it
43
+ callbackDone = Promise.resolve(@currentCallback())
44
+ u.always callbackDone, =>
45
+ # Don't use undefined since an unchecked checkbox actually has an undefined value
46
+ @currentCallback = undefined
47
+ @requestCallback()
48
+
49
+ readFieldValue: =>
50
+ u.submittedValue(@$field)
51
+
52
+ check: =>
53
+ value = @readFieldValue()
54
+ if @isNewValue(value)
55
+ @scheduledValue = value
56
+ @cancelTimer()
57
+ @scheduleTimer()