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
@@ -17,7 +17,7 @@ In contrast, when we animate a new element without simultaneously removing an
17
17
  old element, we call it an *animation*.
18
18
 
19
19
  An example for an animation is opening a new dialog. We can animate the appearance
20
- of the dialog by adding an [`up-animation`](/up-modal#up-animation) attribute to the opening link:
20
+ of the dialog by adding an [`[up-animation]`](/a-up-modal#up-animation) attribute to the opening link:
21
21
 
22
22
  <a href="/users" up-modal=".list" up-animation="move-from-top">Show users</a>
23
23
 
@@ -35,25 +35,27 @@ up.motion = (($) ->
35
35
 
36
36
  u = up.util
37
37
 
38
- animations = {}
39
- defaultAnimations = {}
40
- transitions = {}
41
- defaultTransitions = {}
38
+ namedAnimations = {}
39
+ defaultNamedAnimations = {}
40
+ namedTransitions = {}
41
+ defaultNamedTransitions = {}
42
+
43
+ motionTracker = new up.MotionTracker('motion')
42
44
 
43
45
  ###*
44
46
  Sets default options for animations and transitions.
45
47
 
46
48
  @property up.motion.config
47
- @param {Number} [config.duration=300]
49
+ @param {number} [config.duration=300]
48
50
  The default duration for all animations and transitions (in milliseconds).
49
- @param {Number} [config.delay=0]
51
+ @param {number} [config.delay=0]
50
52
  The default delay for all animations and transitions (in milliseconds).
51
- @param {String} [config.easing='ease']
53
+ @param {string} [config.easing='ease']
52
54
  The default timing function that controls the acceleration of animations and transitions.
53
55
 
54
56
  See [W3C documentation](http://www.w3.org/TR/css3-transitions/#transition-timing-function)
55
57
  for a list of pre-defined timing functions.
56
- @param {Boolean} [config.enabled=true]
58
+ @param {boolean} [config.enabled=true]
57
59
  Whether animation is enabled.
58
60
 
59
61
  Set this to `false` to disable animation globally.
@@ -71,8 +73,8 @@ up.motion = (($) ->
71
73
 
72
74
  reset = ->
73
75
  finish()
74
- animations = u.copy(defaultAnimations)
75
- transitions = u.copy(defaultTransitions)
76
+ namedAnimations = u.copy(defaultNamedAnimations)
77
+ namedTransitions = u.copy(defaultNamedTransitions)
76
78
  config.reset()
77
79
 
78
80
  ###*
@@ -81,7 +83,7 @@ up.motion = (($) ->
81
83
  Set [`up.motion.config.enabled`](/up.motion.config) `false` in order to disable animations globally.
82
84
 
83
85
  @function up.motion.isEnabled
84
- @return {Boolean}
86
+ @return {boolean}
85
87
  @stable
86
88
  ###
87
89
  isEnabled = ->
@@ -138,19 +140,19 @@ up.motion = (($) ->
138
140
  the new animation begins.
139
141
 
140
142
  @function up.animate
141
- @param {Element|jQuery|String} elementOrSelector
143
+ @param {Element|jQuery|string} elementOrSelector
142
144
  The element to animate.
143
- @param {String|Function|Object} animation
145
+ @param {string|Function|Object} animation
144
146
  Can either be:
145
147
 
146
148
  - The animation's name
147
149
  - A function performing the animation
148
150
  - An object of CSS attributes describing the last frame of the animation
149
- @param {Number} [options.duration=300]
151
+ @param {number} [options.duration=300]
150
152
  The duration of the animation, in milliseconds.
151
- @param {Number} [options.delay=0]
153
+ @param {number} [options.delay=0]
152
154
  The delay before the animation starts, in milliseconds.
153
- @param {String} [options.easing='ease']
155
+ @param {string} [options.easing='ease']
154
156
  The timing function that controls the animation's acceleration.
155
157
 
156
158
  See [W3C documentation](http://www.w3.org/TR/css3-transitions/#transition-timing-function)
@@ -161,23 +163,136 @@ up.motion = (($) ->
161
163
  ###
162
164
  animate = (elementOrSelector, animation, options) ->
163
165
  $element = $(elementOrSelector)
164
- finish($element)
165
166
  options = animateOptions(options)
166
- if isNone(animation)
167
- none()
168
- else if u.isFunction(animation)
169
- assertIsDeferred(animation($element, options), animation)
170
- else if u.isString(animation)
171
- animate($element, findAnimation(animation), options)
172
- else if u.isHash(animation)
173
- if isEnabled()
174
- u.cssAnimate($element, animation, options)
167
+
168
+ finishOnce($element, options).then ->
169
+ if !willAnimate($element, animation, options)
170
+ skipAnimate($element, animation)
171
+ else if u.isFunction(animation)
172
+ animation($element, options)
173
+ else if u.isString(animation)
174
+ animate($element, findNamedAnimation(animation), options)
175
+ else if u.isOptions(animation)
176
+ animateWithCss($element, animation, options)
175
177
  else
176
- # Directly set the last frame
177
- $element.css(animation)
178
- u.resolvedDeferred()
179
- else
180
- up.fail("Unknown animation type for %o", animation)
178
+ # Error will be converted to rejected promise in a then() callback
179
+ up.fail('Animation must be a function, animation name or object of CSS properties, but it was %o', animation)
180
+
181
+ willAnimate = ($elements, animationOrTransition, options) ->
182
+ isEnabled() && !isNone(animationOrTransition) && options.duration > 0 && u.all($elements, u.isBodyDescendant)
183
+
184
+ skipAnimate = ($element, animation) ->
185
+ if u.isOptions(animation)
186
+ # If we are given the final animation frame as an object of CSS properties,
187
+ # the best we can do is to set the final frame without animation.
188
+ $element.css(animation)
189
+ # Signal that the animation is already done.
190
+ Promise.resolve()
191
+
192
+ ###*
193
+ Animates the given element's CSS properties using CSS transitions.
194
+
195
+ If the element is already being animated, the previous animation
196
+ will instantly jump to its last frame before the new animation begins.
197
+
198
+ To improve performance, the element will be forced into compositing for
199
+ the duration of the animation.
200
+
201
+ @function up.util.cssAnimate
202
+ @param {Element|jQuery|string} elementOrSelector
203
+ The element to animate.
204
+ @param {Object} lastFrame
205
+ The CSS properties that should be transitioned to.
206
+ @param {number} [options.duration=300]
207
+ The duration of the animation, in milliseconds.
208
+ @param {number} [options.delay=0]
209
+ The delay before the animation starts, in milliseconds.
210
+ @param {string} [options.easing='ease']
211
+ The timing function that controls the animation's acceleration.
212
+ See [W3C documentation](http://www.w3.org/TR/css3-transitions/#transition-timing-function)
213
+ for a list of pre-defined timing functions.
214
+ @return {Promise}
215
+ A promise that fulfills when the animation ends.
216
+ @internal
217
+ ###
218
+ animateWithCss = ($element, lastFrame, options) ->
219
+ startCssTransition = ->
220
+ transitionProperties = Object.keys(lastFrame)
221
+ transition =
222
+ 'transition-property': transitionProperties.join(', ')
223
+ 'transition-duration': "#{options.duration}ms"
224
+ 'transition-delay': "#{options.delay}ms"
225
+ 'transition-timing-function': options.easing
226
+ oldTransition = $element.css(Object.keys(transition))
227
+
228
+ deferred = u.newDeferred()
229
+ # May not call this finish() since this would override the global finish()
230
+ # function in this scope. We really need `let`, which CoffeeScript will never get.
231
+ fulfill = -> deferred.resolve()
232
+
233
+ onTransitionEnd = (event) ->
234
+ # Check if the transitionend event was caused by our own transition,
235
+ # and not by some other transition that happens to live on the same element.
236
+ completedProperty = event.originalEvent.propertyName
237
+ fulfill() if u.contains(transitionProperties, completedProperty)
238
+
239
+ # Animating code is expected to listen to this event to enable external code
240
+ # to fulfil the animation.
241
+ onFinish = fulfill
242
+
243
+ $element.on(motionTracker.finishEvent, onFinish)
244
+
245
+ # Ideally, we want to fulfil when we receive the `transitionend` event
246
+ $element.on('transitionend', onTransitionEnd)
247
+
248
+ # The `transitionend` event might not fire reliably if other transitions
249
+ # are interfering on the same element. This is why we register a fallback
250
+ # timeout that forces the animation to fulfil a few ms later.
251
+ transitionTimingTolerance = 5
252
+ cancelFallbackTimer = u.setTimer(options.duration + transitionTimingTolerance, fulfill)
253
+
254
+ # All clean-up is handled in the following then() handler.
255
+ # This way it will be run both when the animation finishAnimatees naturally and
256
+ # when it is finishAnimateed externally.
257
+ deferred.then ->
258
+ # Disable all three triggers that would fulfil the motion:
259
+ $element.off(motionTracker.finishEvent, onFinish)
260
+ $element.off('transitionend', onTransitionEnd)
261
+ clearTimeout(cancelFallbackTimer)
262
+
263
+ # Elements with compositing might look blurry, so undo that.
264
+ undoCompositing()
265
+
266
+ # To interrupt the running transition we *must* set it to 'none' exactly.
267
+ # We cannot simply restore the old transition properties because browsers
268
+ # would simply keep transitioning.
269
+ $element.css('transition': 'none')
270
+
271
+ # Restoring a previous transition involves forcing a repaint, so we only do it if
272
+ # we know the element was transitioning before.
273
+ # Note that the default transition for elements is actually "all 0s ease 0s"
274
+ # instead of "none", although that has the same effect as "none".
275
+ hadTransitionBefore = !(oldTransition['transition-property'] == 'none' || (oldTransition['transition-property'] == 'all' && oldTransition['transition-duration'][0] == '0'))
276
+ if hadTransitionBefore
277
+ # If there is no repaint between the "none" transition and restoring the previous
278
+ # transition, the browser will simply keep transitioning. I'm sorry.
279
+ u.forceRepaint($element)
280
+ $element.css(oldTransition)
281
+
282
+ # Push the element into its own compositing layer before we are going
283
+ # to massively change the element against background.
284
+ undoCompositing = u.forceCompositing($element)
285
+
286
+ # CSS will start animating when we set the `transition-*` properties and then change
287
+ # the animating properties to the last frame.
288
+ $element.css(transition)
289
+ $element.css(lastFrame)
290
+
291
+ # Return a promise that fulfills when either the animation ends
292
+ # or someone finishes the animation.
293
+ deferred.promise()
294
+
295
+ motionTracker.start($element, startCssTransition)
181
296
 
182
297
  ###*
183
298
  Extracts animation-related options from the given options hash.
@@ -195,19 +310,21 @@ up.motion = (($) ->
195
310
  consolidatedOptions.easing = u.option(userOptions.easing, u.presentAttr($element, 'up-easing'), moduleDefaults.easing, config.easing)
196
311
  consolidatedOptions.duration = Number(u.option(userOptions.duration, u.presentAttr($element, 'up-duration'), moduleDefaults.duration, config.duration))
197
312
  consolidatedOptions.delay = Number(u.option(userOptions.delay, u.presentAttr($element, 'up-delay'), moduleDefaults.delay, config.delay))
313
+ consolidatedOptions.finishedMotion = userOptions.finishedMotion # this is required by animate() and finishOnceBeforeMotion()
198
314
  consolidatedOptions
199
315
 
200
- findAnimation = (name) ->
201
- animations[name] or up.fail("Unknown animation %o", name)
202
-
203
- GHOSTING_DEFERRED_KEY = 'up-ghosting-deferred'
204
- GHOSTING_CLASS = 'up-ghosting'
316
+ findNamedAnimation = (name) ->
317
+ namedAnimations[name] or up.fail("Unknown animation %o", name)
205
318
 
206
- withGhosts = ($old, $new, options, block) ->
207
-
208
- # Don't create ghosts of ghosts in case a transition function is itself calling `morph`
319
+ ###*
320
+ @function withGhosts
321
+ @return {Promise}
322
+ @internal
323
+ ###
324
+ withGhosts = ($old, $new, options, transitionFn) ->
325
+ # Don't create ghosts of ghosts in case a transition function calling `morph` recursively.
209
326
  if options.copy == false || $old.is('.up-ghost') || $new.is('.up-ghost')
210
- return block($old, $new)
327
+ return transitionFn($old, $new, options)
211
328
 
212
329
  oldCopy = undefined
213
330
  newCopy = undefined
@@ -215,56 +332,51 @@ up.motion = (($) ->
215
332
  newScrollTop = undefined
216
333
 
217
334
  $viewport = up.layout.viewportOf($old)
218
- $both = $old.add($new)
219
335
 
336
+ # Right now $old and $new are visible siblings in the DOM.
337
+ # Temporarily hide $new while we copy $old and take some measurements.
220
338
  u.temporaryCss $new, display: 'none', ->
221
- # Within this block, $new is hidden but $old is visible
222
339
  oldCopy = prependCopy($old, $viewport)
223
340
  # Remember the previous scroll position in case we will reveal $new below.
224
341
  oldScrollTop = $viewport.scrollTop()
225
- # $viewport.scrollTop(oldScrollTop + 1)
226
342
 
227
- u.temporaryCss $old, display: 'none', ->
228
- # Within this block, $old is hidden but $new is visible
229
- up.layout.revealOrRestoreScroll($new, options)
343
+ # Hide $old. We will never re-show it.
344
+ # It's not our job to remove $old from the DOM.
345
+ $old.hide()
346
+
347
+ up.layout.revealOrRestoreScroll($new, options).then ->
230
348
  newCopy = prependCopy($new, $viewport)
231
349
  newScrollTop = $viewport.scrollTop()
232
350
 
233
- # Since we have scrolled the viewport (containing both $old and $new),
234
- # we must shift the old copy so it looks like it it is still sitting
235
- # in the same position.
236
- oldCopy.moveTop(newScrollTop - oldScrollTop)
237
-
238
- # Hide $old since we no longer need it.
239
- $old.hide()
351
+ # Since we have scrolled the viewport (containing both $old and $new),
352
+ # we must shift the old copy so it looks like it it is still sitting
353
+ # in the same position.
354
+ oldCopy.moveTop(newScrollTop - oldScrollTop)
355
+
356
+ # We will let $new take up space in the element flow, but hide it.
357
+ # The user will only see the two animated ghosts until the transition
358
+ # is over.
359
+ # Note that we must **not** use `visibility: hidden` to hide the new
360
+ # element. This would delay browser painting until the element is
361
+ # shown again, causing a flicker while the browser is painting.
362
+ restoreNewOpacity = u.temporaryCss($new, opacity: '0')
363
+
364
+ # Perform the transition on the ghosts.
365
+ transitionDone = transitionFn(oldCopy.$ghost, newCopy.$ghost, options)
366
+
367
+ # The animations on both ghosts should finish if someone calls finish()
368
+ # on either of the original elements.
369
+ $bothGhosts = oldCopy.$ghost.add(newCopy.$ghost)
370
+ $bothOriginals = $old.add($new)
371
+ motionTracker.forwardFinishEvent($bothOriginals, $bothGhosts, transitionDone)
372
+
373
+ transitionDone.then ->
374
+ # This will be called when the transition in the block is either done
375
+ # or when it is finished by triggering up:motion:finish on either element.
376
+ restoreNewOpacity()
377
+ oldCopy.$bounds.remove()
378
+ newCopy.$bounds.remove()
240
379
 
241
- # We will let $new take up space in the element flow, but hide it.
242
- # The user will only see the two animated ghosts until the transition
243
- # is over.
244
- # Note that we must **not** use `visibility: hidden` to hide the new
245
- # element. This would delay browser painting until the element is
246
- # shown again, causing a flicker while the browser is painting.
247
- showNew = u.temporaryCss($new, opacity: '0')
248
-
249
- deferred = block(oldCopy.$ghost, newCopy.$ghost)
250
-
251
- # Make a way to look at $old and $new and see if an animation is
252
- # already in progress. If someone attempted a new animation on the
253
- # same elements, the stored promises would be resolved by the second
254
- # animation call, making the transition jump to the last frame instantly.
255
- $both.data(GHOSTING_DEFERRED_KEY, deferred)
256
- $both.addClass(GHOSTING_CLASS)
257
-
258
- deferred.then ->
259
- $both.removeData(GHOSTING_DEFERRED_KEY)
260
- $both.removeClass(GHOSTING_CLASS)
261
- # Now that the transition is over we show $new again.
262
- showNew()
263
- oldCopy.$bounds.remove()
264
- newCopy.$bounds.remove()
265
-
266
- deferred
267
-
268
380
  ###*
269
381
  Completes [animations](/up.animate) and [transitions](/up.morph).
270
382
 
@@ -277,30 +389,13 @@ up.motion = (($) ->
277
389
  Does nothing if there are no animation to complete.
278
390
 
279
391
  @function up.motion.finish
280
- @param {Element|jQuery|String} [elementOrSelector]
392
+ @param {Element|jQuery|string} [elementOrSelector]
393
+ @return {Promise}
394
+ A promise that fulfills when animations and transitions have finished.
281
395
  @stable
282
396
  ###
283
- finish = (elementOrSelector = '.up-animating') ->
284
- # We don't need to crawl through the DOM if we aren't animating anyway
285
- return unless isEnabled()
286
-
287
- $element = $(elementOrSelector)
288
- $animatingSubtree = u.findWithSelf($element, '.up-animating')
289
- u.finishCssAnimate($animatingSubtree)
290
- $ghostingSubtree = u.findWithSelf($element, ".#{GHOSTING_CLASS}")
291
- finishGhosting($ghostingSubtree)
292
-
293
- finishGhosting = ($collection) ->
294
- $collection.each ->
295
- $element = $(this)
296
- if existingGhosting = u.pluckData($element, GHOSTING_DEFERRED_KEY)
297
- existingGhosting.resolve()
298
-
299
- assertIsDeferred = (object, source) ->
300
- if u.isDeferred(object)
301
- object
302
- else
303
- up.fail("Did not return a promise with .then and .resolve methods: %o", source)
397
+ finish = (elementOrSelector) ->
398
+ motionTracker.finish(elementOrSelector)
304
399
 
305
400
  ###*
306
401
  Performs an animated transition between two elements.
@@ -348,72 +443,80 @@ up.motion = (($) ->
348
443
  The old element remains hidden in the DOM.
349
444
 
350
445
  @function up.morph
351
- @param {Element|jQuery|String} source
352
- @param {Element|jQuery|String} target
353
- @param {Function|String} transitionOrName
354
- @param {Number} [options.duration=300]
446
+ @param {Element|jQuery|string} source
447
+ @param {Element|jQuery|string} target
448
+ @param {Function|string} transitionOrName
449
+ @param {number} [options.duration=300]
355
450
  The duration of the animation, in milliseconds.
356
- @param {Number} [options.delay=0]
451
+ @param {number} [options.delay=0]
357
452
  The delay before the animation starts, in milliseconds.
358
- @param {String} [options.easing='ease']
453
+ @param {string} [options.easing='ease']
359
454
  The timing function that controls the transition's acceleration.
360
455
 
361
456
  See [W3C documentation](http://www.w3.org/TR/css3-transitions/#transition-timing-function)
362
457
  for a list of pre-defined timing functions.
363
- @param {Boolean} [options.reveal=false]
458
+ @param {boolean} [options.reveal=false]
364
459
  Whether to reveal the new element by scrolling its parent viewport.
365
460
  @return {Promise}
366
- A promise for the transition's end.
461
+ A promise that fulfills when the transition ends.
367
462
  @stable
368
463
  ###
369
- morph = (source, target, transitionOrName, options) ->
464
+ morph = (source, target, transitionObject, options) ->
370
465
  options = u.options(options)
466
+ options = u.assign(options, animateOptions(options))
371
467
 
372
- willMorph = isEnabled() && !isNone(transitionOrName)
373
468
  $old = $(source)
374
469
  $new = $(target)
470
+ $both = $old.add($new)
471
+
472
+ transitionFn = findTransitionFn(transitionObject)
473
+ willMorph = willAnimate($both, transitionFn, options)
375
474
 
376
- up.log.group ('Morphing %o to %o (using %s, %o)' if willMorph), $old.get(0), $new.get(0), transitionOrName, options, ->
377
- parsedOptions = u.only(options, 'reveal', 'restoreScroll', 'source')
378
- parsedOptions = u.assign(parsedOptions, animateOptions(options))
379
-
380
- finish($old)
381
- finish($new)
382
-
383
- if willMorph
384
- ensureMorphable($old)
385
- ensureMorphable($new)
386
-
387
- if animation = animations[transitionOrName]
388
- skipMorph($old, $new, parsedOptions)
389
- return animate($new, animation, parsedOptions)
390
- else if transition = (u.presence(transitionOrName, u.isFunction) || transitions[transitionOrName])
391
- return withGhosts $old, $new, parsedOptions, ($oldGhost, $newGhost) ->
392
- transitionPromise = transition($oldGhost, $newGhost, parsedOptions)
393
- assertIsDeferred(transitionPromise, transitionOrName)
394
- else if u.isString(transitionOrName) && transitionOrName.indexOf('/') >= 0
395
- parts = transitionOrName.split('/')
396
- transition = ($old, $new, options) ->
397
- resolvableWhen(
398
- animate($old, parts[0], options),
399
- animate($new, parts[1], options)
400
- )
401
- return morph($old, $new, transition, parsedOptions)
475
+ up.log.group ('Morphing %o to %o with transition %o' if willMorph), $old.get(0), $new.get(0), transitionObject, ->
476
+ finishOnce($both, options).then ->
477
+ if !willMorph
478
+ skipMorph($old, $new, options)
479
+ else if transitionFn
480
+ withGhosts($old, $new, options, transitionFn)
402
481
  else
403
- up.fail("Unknown transition %o", transitionOrName)
404
- else
405
- return skipMorph($old, $new, parsedOptions)
482
+ # Exception will be converted to rejected Promise inside a then() handler
483
+ up.fail("Unknown transition %o", transitionObject)
406
484
 
407
- ensureMorphable = ($element) ->
408
- if $element.parents('body').length == 0
409
- element = $element.get(0)
410
- up.fail("Can't morph a <%s> element (%o)", element.tagName, element)
485
+ finishOnce = ($elements, options) ->
486
+ # Finish existing transitions, but only once in case morph() or animate() is called recursively.
487
+ if options.finishedMotion
488
+ Promise.resolve()
489
+ else
490
+ # Use options to persist that we have finished motion.
491
+ options.finishedMotion = true
492
+ finish($elements)
493
+
494
+ findTransitionFn = (object) ->
495
+ if isNone(object)
496
+ undefined
497
+ else if u.isFunction(object)
498
+ object
499
+ else if u.isArray(object)
500
+ if isNone(object[0]) && isNone(object[1])
501
+ # A composition of two "none" animations is again a "none" animation
502
+ undefined
503
+ else
504
+ ($old, $new, options) -> Promise.all([
505
+ animate($old, object[0], options),
506
+ animate($new, object[1], options)
507
+ ])
508
+ else if u.isString(object)
509
+ if object.indexOf('/') >= 0 # Compose a transition from two animation names
510
+ findTransitionFn(object.split('/'))
511
+ else if namedTransition = namedTransitions[object]
512
+ findTransitionFn(namedTransition)
411
513
 
412
514
  ###*
413
- This causes the side effects of a successful transition, but instantly.
515
+ This instantly causes the side effects of a successful transition.
414
516
  We use this to skip morphing for old browsers, or when the developer
415
- decides to only animate the new element (i.e. no real ghosting or transition) .
517
+ decides to only animate the new element (i.e. no real ghosting or transition).
416
518
 
519
+ @return {Promise}
417
520
  @internal
418
521
  ###
419
522
  skipMorph = ($old, $new, options) ->
@@ -492,22 +595,23 @@ up.motion = (($) ->
492
595
  If you choose to *not* use `up.animate()` and roll your own
493
596
  logic instead, your code must honor the following contract:
494
597
 
495
- 1. It must honor the passed options `{ delay, duration, easing }` if present
598
+ 1. It must honor the options `{ delay, duration, easing }` if given
496
599
  2. It must *not* remove any of the given elements from the DOM.
497
- 3. It returns a promise that is resolved when the transition ends
498
- 4. The returned promise responds to a `resolve()` function that
499
- instantly jumps to the last transition frame and resolves the promise.
600
+ 3. It returns a promise that is fulfilled when the transition has ended
601
+ 4. If during the animation an event `up:motion:finish` is emitted on
602
+ the given element, the transition instantly jumps to the last frame
603
+ and resolves the returned promise.
500
604
 
501
605
  Calling [`up.animate()`](/up.animate) with an object argument
502
606
  will take care of all these points.
503
607
 
504
608
  @function up.transition
505
- @param {String} name
609
+ @param {string} name
506
610
  @param {Function} transition
507
611
  @stable
508
612
  ###
509
- transition = (name, transition) ->
510
- transitions[name] = transition
613
+ registerTransition = (name, transition) ->
614
+ namedTransitions[name] = transition
511
615
 
512
616
  ###*
513
617
  Defines a named animation.
@@ -526,56 +630,27 @@ up.motion = (($) ->
526
630
  If you choose to *not* use `up.animate()` and roll your own
527
631
  animation code instead, your code must honor the following contract:
528
632
 
529
- 1. It must honor the passed options `{ delay, duration, easing }` if present
530
- 2. It must *not* remove the passed element from the DOM.
531
- 3. It returns a promise that is resolved when the animation ends
532
- 4. The returned promise responds to a `resolve()` function that
533
- instantly jumps to the last animation frame and resolves the promise.
633
+ 1. It must honor the options `{ delay, duration, easing }` if given
634
+ 2. It must *not* remove any of the given elements from the DOM.
635
+ 3. It returns a promise that is fulfilled when the transition has ended
636
+ 4. If during the animation an event `up:motion:finish` is emitted on
637
+ the given element, the transition instantly jumps to the last frame
638
+ and resolves the returned promise.
534
639
 
535
640
  Calling [`up.animate()`](/up.animate) with an object argument
536
641
  will take care of all these points.
537
642
 
538
643
  @function up.animation
539
- @param {String} name
644
+ @param {string} name
540
645
  @param {Function} animation
541
646
  @stable
542
647
  ###
543
- animation = (name, animation) ->
544
- animations[name] = animation
648
+ registerAnimation = (name, animation) ->
649
+ namedAnimations[name] = animation
545
650
 
546
651
  snapshot = ->
547
- defaultAnimations = u.copy(animations)
548
- defaultTransitions = u.copy(transitions)
549
-
550
- ###*
551
- Returns a new deferred that resolves once all given deferreds have resolved.
552
-
553
- Other then [`$.when` from jQuery](https://api.jquery.com/jquery.when/),
554
- the combined deferred will have a `resolve` method. This `resolve` method
555
- will resolve all the wrapped deferreds.
556
-
557
- This is important when composing multiple existing animations into
558
- a [custom transition](/up.transition), since the transition function
559
- must return a deferred with a `resolve` function that fast-forwards
560
- the animation to its last frame.
561
-
562
- @function up.motion.when
563
- @param {Array<Deferred>} deferreds...
564
- @return {Deferred} A new deferred
565
- @experimental
566
- ###
567
- resolvableWhen = u.resolvableWhen
568
-
569
- ###*
570
- Returns a no-op animation or transition which has no visual effects
571
- and completes instantly.
572
-
573
- @function up.motion.none
574
- @return {Promise}
575
- A resolved promise
576
- @stable
577
- ###
578
- none = u.resolvedDeferred
652
+ defaultNamedAnimations = u.copy(namedAnimations)
653
+ defaultNamedTransitions = u.copy(namedTransitions)
579
654
 
580
655
  ###*
581
656
  Returns whether the given animation option will cause the animation
@@ -584,17 +659,16 @@ up.motion = (($) ->
584
659
  @function up.motion.isNone
585
660
  @internal
586
661
  ###
587
- isNone = (animation) ->
588
- animation is false || animation is 'none' || u.isMissing(animation) || u.isResolvedPromise(animation)
589
-
590
- animation('none', none)
662
+ isNone = (animationOrTransition) ->
663
+ # false, undefined, null and the string "none" are all ways to skip animations
664
+ !animationOrTransition || animationOrTransition == 'none' || (u.isOptions(animationOrTransition) && u.isBlank(animationOrTransition))
591
665
 
592
- animation('fade-in', ($ghost, options) ->
666
+ registerAnimation('fade-in', ($ghost, options) ->
593
667
  $ghost.css(opacity: 0)
594
668
  animate($ghost, { opacity: 1 }, options)
595
669
  )
596
670
 
597
- animation('fade-out', ($ghost, options) ->
671
+ registerAnimation('fade-out', ($ghost, options) ->
598
672
  $ghost.css(opacity: 1)
599
673
  animate($ghost, { opacity: 0 }, options)
600
674
  )
@@ -602,63 +676,63 @@ up.motion = (($) ->
602
676
  translateCss = (x, y) ->
603
677
  { transform: "translate(#{x}px, #{y}px)" }
604
678
 
605
- animation('move-to-top', ($ghost, options) ->
679
+ registerAnimation('move-to-top', ($ghost, options) ->
606
680
  box = u.measure($ghost)
607
681
  travelDistance = box.top + box.height
608
682
  $ghost.css(translateCss(0, 0))
609
683
  animate($ghost, translateCss(0, -travelDistance), options)
610
684
  )
611
685
 
612
- animation('move-from-top', ($ghost, options) ->
686
+ registerAnimation('move-from-top', ($ghost, options) ->
613
687
  box = u.measure($ghost)
614
688
  travelDistance = box.top + box.height
615
689
  $ghost.css(translateCss(0, -travelDistance))
616
690
  animate($ghost, translateCss(0, 0), options)
617
691
  )
618
692
 
619
- animation('move-to-bottom', ($ghost, options) ->
693
+ registerAnimation('move-to-bottom', ($ghost, options) ->
620
694
  box = u.measure($ghost)
621
695
  travelDistance = u.clientSize().height - box.top
622
696
  $ghost.css(translateCss(0, 0))
623
697
  animate($ghost, translateCss(0, travelDistance), options)
624
698
  )
625
699
 
626
- animation('move-from-bottom', ($ghost, options) ->
700
+ registerAnimation('move-from-bottom', ($ghost, options) ->
627
701
  box = u.measure($ghost)
628
702
  travelDistance = u.clientSize().height - box.top
629
703
  $ghost.css(translateCss(0, travelDistance))
630
704
  animate($ghost, translateCss(0, 0), options)
631
705
  )
632
706
 
633
- animation('move-to-left', ($ghost, options) ->
707
+ registerAnimation('move-to-left', ($ghost, options) ->
634
708
  box = u.measure($ghost)
635
709
  travelDistance = box.left + box.width
636
710
  $ghost.css(translateCss(0, 0))
637
711
  animate($ghost, translateCss(-travelDistance, 0), options)
638
712
  )
639
713
 
640
- animation('move-from-left', ($ghost, options) ->
714
+ registerAnimation('move-from-left', ($ghost, options) ->
641
715
  box = u.measure($ghost)
642
716
  travelDistance = box.left + box.width
643
717
  $ghost.css(translateCss(-travelDistance, 0))
644
718
  animate($ghost, translateCss(0, 0), options)
645
719
  )
646
720
 
647
- animation('move-to-right', ($ghost, options) ->
721
+ registerAnimation('move-to-right', ($ghost, options) ->
648
722
  box = u.measure($ghost)
649
723
  travelDistance = u.clientSize().width - box.left
650
724
  $ghost.css(translateCss(0, 0))
651
725
  animate($ghost, translateCss(travelDistance, 0), options)
652
726
  )
653
727
 
654
- animation('move-from-right', ($ghost, options) ->
728
+ registerAnimation('move-from-right', ($ghost, options) ->
655
729
  box = u.measure($ghost)
656
730
  travelDistance = u.clientSize().width - box.left
657
731
  $ghost.css(translateCss(travelDistance, 0))
658
732
  animate($ghost, translateCss(0, 0), options)
659
733
  )
660
734
 
661
- animation('roll-down', ($ghost, options) ->
735
+ registerAnimation('roll-down', ($ghost, options) ->
662
736
  fullHeight = $ghost.height()
663
737
  styleMemo = u.temporaryCss($ghost,
664
738
  height: '0px'
@@ -669,42 +743,11 @@ up.motion = (($) ->
669
743
  deferred
670
744
  )
671
745
 
672
- transition('none', none)
673
-
674
- transition('move-left', ($old, $new, options) ->
675
- resolvableWhen(
676
- animate($old, 'move-to-left', options),
677
- animate($new, 'move-from-right', options)
678
- )
679
- )
680
-
681
- transition('move-right', ($old, $new, options) ->
682
- resolvableWhen(
683
- animate($old, 'move-to-right', options),
684
- animate($new, 'move-from-left', options)
685
- )
686
- )
687
-
688
- transition('move-up', ($old, $new, options) ->
689
- resolvableWhen(
690
- animate($old, 'move-to-top', options),
691
- animate($new, 'move-from-bottom', options)
692
- )
693
- )
694
-
695
- transition('move-down', ($old, $new, options) ->
696
- resolvableWhen(
697
- animate($old, 'move-to-bottom', options),
698
- animate($new, 'move-from-top', options)
699
- )
700
- )
701
-
702
- transition('cross-fade', ($old, $new, options) ->
703
- resolvableWhen(
704
- animate($old, 'fade-out', options),
705
- animate($new, 'fade-in', options)
706
- )
707
- )
746
+ registerTransition('move-left', 'move-to-left/move-from-right')
747
+ registerTransition('move-right', 'move-to-right/move-from-left')
748
+ registerTransition('move-up', 'move-to-top/move-from-bottom')
749
+ registerTransition('move-down', 'move-to-bottom/move-from-top')
750
+ registerTransition('cross-fade', 'fade-out/fade-in')
708
751
 
709
752
  up.on 'up:framework:booted', snapshot
710
753
  up.on 'up:framework:reset', reset
@@ -713,12 +756,10 @@ up.motion = (($) ->
713
756
  animate: animate
714
757
  animateOptions: animateOptions
715
758
  finish: finish
716
- transition: transition
717
- animation: animation
759
+ transition: registerTransition
760
+ animation: registerAnimation
718
761
  config: config
719
762
  isEnabled: isEnabled
720
- none: none
721
- when: resolvableWhen
722
763
  prependCopy: prependCopy
723
764
  isNone: isNone
724
765