unpoly-rails 0.55.1 → 0.56.0

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.

Potentially problematic release.


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

Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +59 -2
  3. data/dist/unpoly-bootstrap3.js +6 -4
  4. data/dist/unpoly-bootstrap3.min.js +1 -1
  5. data/dist/unpoly.js +1323 -805
  6. data/dist/unpoly.min.js +4 -3
  7. data/lib/assets/javascripts/unpoly-bootstrap3/{navigation-ext.coffee → feedback-ext.coffee} +2 -0
  8. data/lib/assets/javascripts/unpoly/browser.coffee.erb +7 -7
  9. data/lib/assets/javascripts/unpoly/bus.coffee.erb +5 -6
  10. data/lib/assets/javascripts/unpoly/classes/css_transition.coffee +127 -0
  11. data/lib/assets/javascripts/unpoly/classes/extract_plan.coffee +1 -1
  12. data/lib/assets/javascripts/unpoly/classes/motion_tracker.coffee +62 -32
  13. data/lib/assets/javascripts/unpoly/classes/url_set.coffee +27 -0
  14. data/lib/assets/javascripts/unpoly/dom.coffee.erb +78 -99
  15. data/lib/assets/javascripts/unpoly/feedback.coffee +147 -96
  16. data/lib/assets/javascripts/unpoly/form.coffee.erb +26 -2
  17. data/lib/assets/javascripts/unpoly/history.coffee +2 -1
  18. data/lib/assets/javascripts/unpoly/layout.coffee.erb +68 -12
  19. data/lib/assets/javascripts/unpoly/link.coffee.erb +10 -4
  20. data/lib/assets/javascripts/unpoly/modal.coffee.erb +11 -9
  21. data/lib/assets/javascripts/unpoly/{motion.coffee → motion.coffee.erb} +184 -322
  22. data/lib/assets/javascripts/unpoly/popup.coffee.erb +13 -12
  23. data/lib/assets/javascripts/unpoly/radio.coffee +1 -1
  24. data/lib/assets/javascripts/unpoly/syntax.coffee +8 -17
  25. data/lib/assets/javascripts/unpoly/tooltip.coffee +11 -11
  26. data/lib/assets/javascripts/unpoly/util.coffee +332 -145
  27. data/lib/unpoly/rails/version.rb +1 -1
  28. data/package.json +1 -1
  29. data/spec_app/Gemfile.lock +1 -1
  30. data/spec_app/app/assets/javascripts/integration_test.coffee +1 -0
  31. data/spec_app/app/assets/stylesheets/integration_test.sass +1 -0
  32. data/spec_app/app/assets/stylesheets/jasmine_specs.sass +4 -0
  33. data/spec_app/app/views/motion_test/transitions.erb +13 -0
  34. data/spec_app/app/views/pages/start.erb +1 -0
  35. data/spec_app/spec/javascripts/helpers/to_be_attached.coffee +5 -0
  36. data/spec_app/spec/javascripts/helpers/to_be_detached.coffee +5 -0
  37. data/spec_app/spec/javascripts/helpers/to_contain.js.coffee +1 -1
  38. data/spec_app/spec/javascripts/helpers/to_have_opacity.coffee +11 -0
  39. data/spec_app/spec/javascripts/helpers/to_have_own_property.js.coffee +5 -0
  40. data/spec_app/spec/javascripts/up/dom_spec.js.coffee +217 -102
  41. data/spec_app/spec/javascripts/up/feedback_spec.js.coffee +162 -44
  42. data/spec_app/spec/javascripts/up/layout_spec.js.coffee +97 -10
  43. data/spec_app/spec/javascripts/up/link_spec.js.coffee +3 -3
  44. data/spec_app/spec/javascripts/up/modal_spec.js.coffee +22 -20
  45. data/spec_app/spec/javascripts/up/motion_spec.js.coffee +344 -228
  46. data/spec_app/spec/javascripts/up/popup_spec.js.coffee +1 -1
  47. data/spec_app/spec/javascripts/up/syntax_spec.js.coffee +1 -1
  48. data/spec_app/spec/javascripts/up/tooltip_spec.js.coffee +1 -1
  49. data/spec_app/spec/javascripts/up/util_spec.js.coffee +194 -0
  50. metadata +11 -4
@@ -283,10 +283,16 @@ up.link = (($) ->
283
283
  $link.attr('up-follow', '')
284
284
 
285
285
  shouldProcessEvent = (event, $link) ->
286
- $target = $(event.target)
287
- $targetedChildLink = $target.closest('a, [up-href]').not($link)
288
- $targetedInput = up.form.fieldSelector().seekUp($target)
289
- $targetedChildLink.length == 0 && $targetedInput.length == 0 && u.isUnmodifiedMouseEvent(event)
286
+ target = event.target
287
+ # We never handle events for the right mouse button, or when Shift/CTRL/Meta is pressed
288
+ return false unless u.isUnmodifiedMouseEvent(event)
289
+ # If we actually targeted $link, save ourselves the expensive DOM traversal below
290
+ return true if target == $link.get(0)
291
+ # If user clicked on a child link of $link, or in an <input> within an [up-expand][up-href]
292
+ # we want those other elements handle the click.
293
+ $betterTarget = $(target).closest("a, [up-href], #{up.form.fieldSelector()}").not($link)
294
+ return false if $betterTarget.length
295
+ return true
290
296
 
291
297
  ###**
292
298
  Returns whether the given link has a [safe](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.1)
@@ -245,10 +245,11 @@ up.modal = (($) ->
245
245
  $modal = $(templateHtml())
246
246
  $modal.attr('up-flavor', state.flavor)
247
247
  $modal.attr('up-position', state.position) if u.isPresent(state.position)
248
+
248
249
  $dialog = $modal.find('.up-modal-dialog')
249
- $dialog.css('width', options.width) if u.isPresent(options.width)
250
- $dialog.css('max-width', options.maxWidth) if u.isPresent(options.maxWidth)
251
- $dialog.css('height', options.height) if u.isPresent(options.height)
250
+ dialogStyles = u.only(options, 'width', 'maxWidth', 'height')
251
+ u.writeInlineStyle($dialog, dialogStyles)
252
+
252
253
  $modal.find('.up-modal-close').remove() unless state.closable
253
254
  $content = $modal.find('.up-modal-content')
254
255
  # Create an empty element that will match the
@@ -272,18 +273,18 @@ up.modal = (($) ->
272
273
  if u.documentHasVerticalScrollbar()
273
274
  $body = $('body')
274
275
  scrollbarWidth = u.scrollbarWidth()
275
- bodyRightPadding = parseFloat($body.css('padding-right'))
276
+ bodyRightPadding = u.readComputedStyleNumber($body, 'paddingRight')
276
277
  bodyRightShift = scrollbarWidth + bodyRightPadding
277
- unshiftBody = u.temporaryCss($body,
278
- 'padding-right': "#{bodyRightShift}px",
279
- 'overflow-y': 'hidden'
278
+ unshiftBody = u.writeTemporaryStyle($body,
279
+ paddingRight: bodyRightShift
280
+ overflowY: 'hidden'
280
281
  )
281
282
  state.unshifters.push(unshiftBody)
282
283
  up.layout.anchoredRight().each ->
283
284
  $element = $(this)
284
- elementRight = parseFloat($element.css('right'))
285
+ elementRight = u.readComputedStyleNumber($element, 'right')
285
286
  elementRightShift = scrollbarWidth + elementRight
286
- unshifter = u.temporaryCss($element, 'right': elementRightShift)
287
+ unshifter = u.writeTemporaryStyle($element, right: elementRightShift)
287
288
  state.unshifters.push(unshifter)
288
289
 
289
290
 
@@ -456,6 +457,7 @@ up.modal = (($) ->
456
457
  options.layer = 'modal'
457
458
  options.failTarget = u.option(options.failTarget, $link.attr('up-fail-target'))
458
459
  options.failLayer = u.option(options.failLayer, $link.attr('up-fail-layer'), 'auto')
460
+
459
461
  animateOptions = up.motion.animateOptions(options, $link, duration: flavorDefault('openDuration', options.flavor), easing: flavorDefault('openEasing', options.flavor))
460
462
 
461
463
  # Although we usually fall back to full page loads if a browser doesn't support pushState,
@@ -72,7 +72,7 @@ up.motion = (($) ->
72
72
  enabled: true
73
73
 
74
74
  reset = ->
75
- finish()
75
+ motionTracker.reset()
76
76
  namedAnimations = u.copy(defaultNamedAnimations)
77
77
  namedTransitions = u.copy(defaultNamedTransitions)
78
78
  config.reset()
@@ -165,41 +165,41 @@ up.motion = (($) ->
165
165
  $element = $(elementOrSelector)
166
166
  options = animateOptions(options)
167
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)
177
- else
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)
168
+ animationFn = findAnimationFn(animation)
169
+ willRun = willAnimate($element, animation, options)
170
+
171
+ if willRun
172
+ runNow = -> animationFn($element, options)
173
+ motionTracker.claim($element, runNow, options)
174
+ else
175
+ skipAnimate($element, animation)
180
176
 
181
177
  willAnimate = ($elements, animationOrTransition, options) ->
182
178
  options = animateOptions(options)
183
- isEnabled() && !isNone(animationOrTransition) && options.duration > 0 && u.all($elements, u.isBodyDescendant)
179
+ isEnabled() && !isNone(animationOrTransition) && options.duration > 0 && !isSingletonElement($elements)
180
+
181
+ isSingletonElement = ($element) ->
182
+ # jQuery's is() returns true if at least one element in the collection matches the selector
183
+ $element.is('body')
184
184
 
185
185
  skipAnimate = ($element, animation) ->
186
186
  if u.isOptions(animation)
187
187
  # If we are given the final animation frame as an object of CSS properties,
188
188
  # the best we can do is to set the final frame without animation.
189
- $element.css(animation)
189
+ u.writeInlineStyle($element, animation)
190
190
  # Signal that the animation is already done.
191
191
  Promise.resolve()
192
192
 
193
+ animCount = 0
194
+
193
195
  ###**
194
196
  Animates the given element's CSS properties using CSS transitions.
195
197
 
196
- If the element is already being animated, the previous animation
197
- will instantly jump to its last frame before the new animation begins.
198
+ Does not track the animation, nor does it finishes existing animations
199
+ (use `up.motion.animate()` for that). It does, however, listen to the motionTracker's
200
+ finish event.
198
201
 
199
- To improve performance, the element will be forced into compositing for
200
- the duration of the animation.
201
-
202
- @function up.util.cssAnimate
202
+ @function animateNow
203
203
  @param {Element|jQuery|string} elementOrSelector
204
204
  The element to animate.
205
205
  @param {Object} lastFrame
@@ -216,84 +216,10 @@ up.motion = (($) ->
216
216
  A promise that fulfills when the animation ends.
217
217
  @internal
218
218
  ###
219
- animateWithCss = ($element, lastFrame, options) ->
220
- startCssTransition = ->
221
- transitionProperties = Object.keys(lastFrame)
222
- transition =
223
- 'transition-property': transitionProperties.join(', ')
224
- 'transition-duration': "#{options.duration}ms"
225
- 'transition-delay': "#{options.delay}ms"
226
- 'transition-timing-function': options.easing
227
- oldTransition = $element.css(Object.keys(transition))
228
-
229
- deferred = u.newDeferred()
230
- # May not call this finish() since this would override the global finish()
231
- # function in this scope. We really need `let`, which CoffeeScript will never get.
232
- fulfill = -> deferred.resolve()
233
-
234
- onTransitionEnd = (event) ->
235
- # Check if the transitionend event was caused by our own transition,
236
- # and not by some other transition that happens to live on the same element.
237
- completedProperty = event.originalEvent.propertyName
238
- fulfill() if u.contains(transitionProperties, completedProperty)
239
-
240
- # Animating code is expected to listen to this event to enable external code
241
- # to fulfil the animation.
242
- onFinish = fulfill
243
-
244
- $element.on(motionTracker.finishEvent, onFinish)
245
-
246
- # Ideally, we want to fulfil when we receive the `transitionend` event
247
- $element.on('transitionend', onTransitionEnd)
248
-
249
- # The `transitionend` event might not fire reliably if other transitions
250
- # are interfering on the same element. This is why we register a fallback
251
- # timeout that forces the animation to fulfil a few ms later.
252
- transitionTimingTolerance = 5
253
- cancelFallbackTimer = u.setTimer(options.duration + transitionTimingTolerance, fulfill)
254
-
255
- # All clean-up is handled in the following then() handler.
256
- # This way it will be run both when the animation finishAnimatees naturally and
257
- # when it is finishAnimateed externally.
258
- deferred.then ->
259
- # Disable all three triggers that would fulfil the motion:
260
- $element.off(motionTracker.finishEvent, onFinish)
261
- $element.off('transitionend', onTransitionEnd)
262
- clearTimeout(cancelFallbackTimer)
263
-
264
- # Elements with compositing might look blurry, so undo that.
265
- undoCompositing()
266
-
267
- # To interrupt the running transition we *must* set it to 'none' exactly.
268
- # We cannot simply restore the old transition properties because browsers
269
- # would simply keep transitioning.
270
- $element.css('transition': 'none')
271
-
272
- # Restoring a previous transition involves forcing a repaint, so we only do it if
273
- # we know the element was transitioning before.
274
- # Note that the default transition for elements is actually "all 0s ease 0s"
275
- # instead of "none", although that has the same effect as "none".
276
- hadTransitionBefore = !(oldTransition['transition-property'] == 'none' || (oldTransition['transition-property'] == 'all' && oldTransition['transition-duration'][0] == '0'))
277
- if hadTransitionBefore
278
- # If there is no repaint between the "none" transition and restoring the previous
279
- # transition, the browser will simply keep transitioning. I'm sorry.
280
- u.forceRepaint($element)
281
- $element.css(oldTransition)
282
-
283
- # Push the element into its own compositing layer before we are going
284
- # to massively change the element against background.
285
- undoCompositing = u.forceCompositing($element)
286
-
287
- # CSS will start animating when we set the `transition-*` properties and then change
288
- # the animating properties to the last frame.
289
- $element.css(transition)
290
- $element.css(lastFrame)
291
-
292
- # Return a promise that fulfills when either the animation ends
293
- # or someone finishes the animation.
294
- deferred.promise()
295
-
296
- motionTracker.start($element, startCssTransition)
219
+ animateNow = ($element, lastFrame, options) ->
220
+ options = u.merge(options, finishEvent: motionTracker.finishEvent)
221
+ cssTransition = new up.CssTransition($element, lastFrame, options)
222
+ return cssTransition.start()
297
223
 
298
224
  ###**
299
225
  Extracts animation-related options from the given options hash.
@@ -311,76 +237,12 @@ up.motion = (($) ->
311
237
  consolidatedOptions.easing = u.option(userOptions.easing, u.presentAttr($element, 'up-easing'), moduleDefaults.easing, config.easing)
312
238
  consolidatedOptions.duration = Number(u.option(userOptions.duration, u.presentAttr($element, 'up-duration'), moduleDefaults.duration, config.duration))
313
239
  consolidatedOptions.delay = Number(u.option(userOptions.delay, u.presentAttr($element, 'up-delay'), moduleDefaults.delay, config.delay))
314
- consolidatedOptions.finishedMotion = userOptions.finishedMotion # this is required by animate() and finishOnceBeforeMotion()
240
+ consolidatedOptions.trackMotion = userOptions.trackMotion # required by up.MotionTracker
315
241
  consolidatedOptions
316
242
 
317
243
  findNamedAnimation = (name) ->
318
244
  namedAnimations[name] or up.fail("Unknown animation %o", name)
319
245
 
320
- ###**
321
- @function withGhosts
322
- @return {Promise}
323
- @internal
324
- ###
325
- withGhosts = ($old, $new, options, transitionFn) ->
326
- # Don't create ghosts of ghosts in case a transition function calling `morph` recursively.
327
- if options.copy == false || $old.is('.up-ghost') || $new.is('.up-ghost')
328
- return transitionFn($old, $new, options)
329
-
330
- oldCopy = undefined
331
- newCopy = undefined
332
- oldScrollTop = undefined
333
- newScrollTop = undefined
334
-
335
- $viewport = up.layout.viewportOf($old)
336
-
337
- # Right now $old and $new are visible siblings in the DOM.
338
- # Temporarily hide $new while we copy $old and take some measurements.
339
- u.temporaryCss $new, display: 'none', ->
340
- oldCopy = prependCopy($old, $viewport)
341
- # Remember the previous scroll position in case we will reveal $new below.
342
- oldScrollTop = $viewport.scrollTop()
343
-
344
- # Hide $old. We will never re-show it.
345
- # It's not our job to remove $old from the DOM.
346
- $old.hide()
347
-
348
- # Don't animate the scrolling.
349
- # We just want to scroll $new into position before we start the enter animation.
350
- scrollOptions = u.merge(options, { duration: 0})
351
- up.layout.revealOrRestoreScroll($new, scrollOptions).then ->
352
- newCopy = prependCopy($new, $viewport)
353
- newScrollTop = $viewport.scrollTop()
354
-
355
- # Since we have scrolled the viewport (containing both $old and $new),
356
- # we must shift the old copy so it looks like it it is still sitting
357
- # in the same position.
358
- oldCopy.moveTop(newScrollTop - oldScrollTop)
359
-
360
- # We will let $new take up space in the element flow, but hide it.
361
- # The user will only see the two animated ghosts until the transition
362
- # is over.
363
- # Note that we must **not** use `visibility: hidden` to hide the new
364
- # element. This would delay browser painting until the element is
365
- # shown again, causing a flicker while the browser is painting.
366
- restoreNewOpacity = u.temporaryCss($new, opacity: '0')
367
-
368
- # Perform the transition on the ghosts.
369
- transitionDone = transitionFn(oldCopy.$ghost, newCopy.$ghost, options)
370
-
371
- # The animations on both ghosts should finish if someone calls finish()
372
- # on either of the original elements.
373
- $bothGhosts = oldCopy.$ghost.add(newCopy.$ghost)
374
- $bothOriginals = $old.add($new)
375
- motionTracker.forwardFinishEvent($bothOriginals, $bothGhosts, transitionDone)
376
-
377
- transitionDone.then ->
378
- # This will be called when the transition in the block is either done
379
- # or when it is finished by triggering up:motion:finish on either element.
380
- restoreNewOpacity()
381
- oldCopy.$bounds.remove()
382
- newCopy.$bounds.remove()
383
-
384
246
  ###**
385
247
  Completes [animations](/up.animate) and [transitions](/up.morph).
386
248
 
@@ -402,12 +264,16 @@ up.motion = (($) ->
402
264
  motionTracker.finish(elementOrSelector)
403
265
 
404
266
  ###**
405
- Performs an animated transition between two elements.
267
+ Performs an animated transition between the `source` and `target` elements.
268
+
406
269
  Transitions are implement by performing two animations in parallel,
407
- causing one element to disappear and the other to appear.
270
+ causing `source` to disappear and the `target` to appear.
408
271
 
409
- Note that the transition does not remove any elements from the DOM.
410
- The first element will remain in the DOM, albeit hidden using `display: none`.
272
+ - `target` is [inserted before](https://developer.mozilla.org/en-US/docs/Web/API/Node/insertBefore) `source`
273
+ - `source` is removed from the [document flow](https://developer.mozilla.org/en-US/docs/Learn/CSS/CSS_layout/Positioning) with `position: absolute`.
274
+ It will be positioned over its original place in the flow that is now occupied by `target`.
275
+ - Both `source` and `target` are animated in parallel
276
+ - `source` is removed from the DOM
411
277
 
412
278
  \#\#\# Named transitions
413
279
 
@@ -463,7 +329,7 @@ up.motion = (($) ->
463
329
  Whether to reveal the new element by scrolling its parent viewport.
464
330
  @return {Promise}
465
331
  A promise that fulfills when the transition ends.
466
- @stable
332
+ @experimental
467
333
  ###
468
334
  morph = (source, target, transitionObject, options) ->
469
335
  options = u.options(options)
@@ -474,26 +340,66 @@ up.motion = (($) ->
474
340
  $both = $old.add($new)
475
341
 
476
342
  transitionFn = findTransitionFn(transitionObject)
477
- willMorph = willAnimate($both, transitionFn, options)
478
-
479
- up.log.group ('Morphing %o to %o with transition %o' if willMorph), $old.get(0), $new.get(0), transitionObject, ->
480
- finishOnce($both, options).then ->
481
- if !willMorph
482
- skipMorph($old, $new, options)
483
- else if transitionFn
484
- withGhosts($old, $new, options, transitionFn)
485
- else
486
- # Exception will be converted to rejected Promise inside a then() handler
487
- up.fail("Unknown transition %o", transitionObject)
488
-
489
- finishOnce = ($elements, options) ->
490
- # Finish existing transitions, but only once in case morph() or animate() is called recursively.
491
- if options.finishedMotion
492
- Promise.resolve()
343
+ willMorph = willAnimate($old, transitionFn, options)
344
+
345
+ options.afterInsert ||= u.noop
346
+ options.beforeDetach ||= u.noop
347
+ options.afterDetach ||= u.noop
348
+
349
+ scrollNew = ->
350
+ # Don't animate the scrolling. The { duration } option was meant for the transition.
351
+ scrollOptions = u.merge(options, duration: 0)
352
+ # Scroll $new into position before we start the enter animation.
353
+ up.layout.revealOrRestoreScroll($new, scrollOptions)
354
+
355
+ if willMorph
356
+ if motionTracker.isActive($old) && options.trackMotion is false
357
+ return transitionFn($old, $new, options)
358
+
359
+ up.puts 'Morphing %o to %o with transition %o', $old.get(0), $new.get(0), transitionObject
360
+
361
+ $viewport = up.layout.viewportOf($old)
362
+ scrollTopBeforeReveal = $viewport.scrollTop()
363
+
364
+ oldRemote = up.layout.absolutize $old,
365
+ # Because the insertion will shift elements visually, we must delay insertion
366
+ # until absolutize() has measured the bounding box of the old element.
367
+ afterMeasure: ->
368
+ $new.insertBefore($old)
369
+ options.afterInsert()
370
+
371
+ trackable = ->
372
+ # Scroll $new into position before we start the enter animation.
373
+ promise = scrollNew()
374
+
375
+ promise = promise.then ->
376
+ # Since we have scrolled the viewport (containing both $old and $new),
377
+ # we must shift the old copy so it looks like it it is still sitting
378
+ # in the same position.
379
+ scrollTopAfterReveal = $viewport.scrollTop()
380
+ oldRemote.moveTop(scrollTopAfterReveal - scrollTopBeforeReveal)
381
+
382
+ transitionFn($old, $new, options)
383
+
384
+ promise = promise.then ->
385
+ options.beforeDetach()
386
+ $old.detach()
387
+ oldRemote.$bounds.remove()
388
+ options.afterDetach()
389
+
390
+ return promise
391
+
392
+ motionTracker.claim($both, trackable, options)
393
+
493
394
  else
494
- # Use options to persist that we have finished motion.
495
- options.finishedMotion = true
496
- finish($elements)
395
+ options.beforeDetach()
396
+ # Swapping the elements directly with replaceWith() will cause
397
+ # jQuery to remove all data attributes, which we use to store destructors
398
+ swapElementsDirectly($old, $new)
399
+ options.afterInsert()
400
+ options.afterDetach()
401
+ promise = scrollNew()
402
+ return promise
497
403
 
498
404
  findTransitionFn = (object) ->
499
405
  if isNone(object)
@@ -501,90 +407,45 @@ up.motion = (($) ->
501
407
  else if u.isFunction(object)
502
408
  object
503
409
  else if u.isArray(object)
504
- if isNone(object[0]) && isNone(object[1])
505
- # A composition of two "none" animations is again a "none" animation
506
- undefined
507
- else
508
- ($old, $new, options) -> Promise.all([
509
- animate($old, object[0], options),
510
- animate($new, object[1], options)
511
- ])
410
+ composeTransitionFn(object...)
512
411
  else if u.isString(object)
513
412
  if object.indexOf('/') >= 0 # Compose a transition from two animation names
514
- findTransitionFn(object.split('/'))
413
+ composeTransitionFn(object.split('/')...)
515
414
  else if namedTransition = namedTransitions[object]
516
415
  findTransitionFn(namedTransition)
416
+ else
417
+ up.fail("Unknown transition %o", object)
517
418
 
518
- ###**
519
- This instantly causes the side effects of a successful transition.
520
- We use this to skip morphing for old browsers, or when the developer
521
- decides to only animate the new element (i.e. no real ghosting or transition).
522
-
523
- @return {Promise}
524
- @internal
525
- ###
526
- skipMorph = ($old, $new, options) ->
527
- # Simply hide the old element, which would be the side effect of withGhosts(...) below.
528
- $old.hide()
529
-
530
- # Don't animate the scrolling.
531
- # We just want to scroll $new into position before we start the enter animation.
532
- scrollOptions = u.merge(options, { duration: 0})
419
+ composeTransitionFn = (oldAnimation, newAnimation) ->
420
+ if isNone(oldAnimation) && isNone(oldAnimation)
421
+ # A composition of two null-animations is a null-transform
422
+ # and should be skipped.
423
+ undefined
424
+ else
425
+ oldAnimationFn = findAnimationFn(oldAnimation) || u.asyncNoop
426
+ newAnimationFn = findAnimationFn(newAnimation) || u.asyncNoop
427
+ ($old, $new, options) ->
428
+ Promise.all([
429
+ oldAnimationFn($old, options),
430
+ newAnimationFn($new, options)
431
+ ])
533
432
 
534
- # Since we cannot rely on withGhosts to control the scroll position
535
- # in this branch, we need to do it ourselves.
536
- up.layout.revealOrRestoreScroll($new, scrollOptions)
433
+ findAnimationFn = (object) ->
434
+ if isNone(object)
435
+ undefined
436
+ else if u.isFunction(object)
437
+ object
438
+ else if u.isString(object)
439
+ findNamedAnimation(object)
440
+ else if u.isOptions(object)
441
+ ($element, options) -> animateNow($element, object, options)
442
+ else
443
+ up.fail('Unknown animation %o', object)
537
444
 
538
- ###**
539
- @internal
540
- ###
541
- prependCopy = ($element, $viewport) ->
542
- elementDims = u.measure($element, relative: true, inner: true)
543
-
544
- $ghost = $element.clone()
545
- $ghost.find('script').remove()
546
- $ghost.css
547
- # If the element had a layout context before, make sure the
548
- # ghost will have layout context as well (and vice versa).
549
- position: if $element.css('position') == 'static' then 'static' else 'relative'
550
- top: 'auto'
551
- right: 'auto'
552
- bottom: 'auto'
553
- left: 'auto'
554
- width: '100%'
555
- height: '100%'
556
- $ghost.addClass('up-ghost')
557
-
558
- # Wrap the ghost in another container so its margin can expand
559
- # freely. If we would position the element directly (old implementation),
560
- # it would gain a layout context which cannot be crossed by margins.
561
- $bounds = $('<div class="up-bounds"></div>')
562
- $bounds.css(position: 'absolute')
563
- $bounds.css(elementDims)
564
-
565
- top = elementDims.top
566
-
567
- moveTop = (diff) ->
568
- if diff != 0
569
- top += diff
570
- $bounds.css(top: top)
571
-
572
- $ghost.appendTo($bounds)
573
- $bounds.insertBefore($element)
574
-
575
- # In theory, $ghost should now sit over $element perfectly.
576
- # However, $element might collapse its margin against a previous sibling
577
- # element, and $ghost does not have the same sibling.
578
- # So we manually correct $ghost's top position so it aligns with $element.
579
- moveTop($element.offset().top - $ghost.offset().top)
580
-
581
- $fixedElements = up.layout.fixedChildren($ghost)
582
- for fixedElement in $fixedElements
583
- u.fixedToAbsolute(fixedElement, $viewport)
584
-
585
- $ghost: $ghost
586
- $bounds: $bounds
587
- moveTop: moveTop
445
+ swapElementsDirectly = ($old, $new) ->
446
+ # jQuery will actually let us .insertBefore the new <body> tag,
447
+ # but that's probably bad Karma.
448
+ $old.replaceWith($new)
588
449
 
589
450
  ###**
590
451
  Defines a named transition.
@@ -620,7 +481,7 @@ up.motion = (($) ->
620
481
  @stable
621
482
  ###
622
483
  registerTransition = (name, transition) ->
623
- namedTransitions[name] = transition
484
+ namedTransitions[name] = findTransitionFn(transition)
624
485
 
625
486
  ###**
626
487
  Defines a named animation.
@@ -629,7 +490,7 @@ up.motion = (($) ->
629
490
 
630
491
  up.animation('fade-in', function($element, options) {
631
492
  $element.css(opacity: 0);
632
- up.animate($ghost, { opacity: 1 }, options);
493
+ up.animate($element, { opacity: 1 }, options);
633
494
  })
634
495
 
635
496
  It is recommended that your definitions always end by calling
@@ -655,7 +516,7 @@ up.motion = (($) ->
655
516
  @stable
656
517
  ###
657
518
  registerAnimation = (name, animation) ->
658
- namedAnimations[name] = animation
519
+ namedAnimations[name] = findAnimationFn(animation)
659
520
 
660
521
  snapshot = ->
661
522
  defaultNamedAnimations = u.copy(namedAnimations)
@@ -669,112 +530,113 @@ up.motion = (($) ->
669
530
  @internal
670
531
  ###
671
532
  isNone = (animationOrTransition) ->
672
- # false, undefined, null and the string "none" are all ways to skip animations
673
- !animationOrTransition || animationOrTransition == 'none' || (u.isOptions(animationOrTransition) && u.isBlank(animationOrTransition))
533
+ # false, undefined, '', null and the string "none" are all ways to skip animations
534
+ !animationOrTransition || animationOrTransition == 'none' || u.isBlank(animationOrTransition)
674
535
 
675
- registerAnimation('fade-in', ($ghost, options) ->
676
- $ghost.css(opacity: 0)
677
- animate($ghost, { opacity: 1 }, options)
536
+ registerAnimation('fade-in', ($element, options) ->
537
+ u.writeInlineStyle($element, opacity: 0)
538
+ animateNow($element, { opacity: 1 }, options)
678
539
  )
679
540
 
680
- registerAnimation('fade-out', ($ghost, options) ->
681
- $ghost.css(opacity: 1)
682
- animate($ghost, { opacity: 0 }, options)
541
+ registerAnimation('fade-out', ($element, options) ->
542
+ u.writeInlineStyle($element, opacity: 1)
543
+ animateNow($element, { opacity: 0 }, options)
683
544
  )
684
545
 
685
546
  translateCss = (x, y) ->
686
547
  { transform: "translate(#{x}px, #{y}px)" }
687
548
 
688
- registerAnimation('move-to-top', ($ghost, options) ->
689
- $ghost.css(translateCss(0, 0))
690
- box = u.measure($ghost)
549
+ registerAnimation('move-to-top', ($element, options) ->
550
+ u.writeInlineStyle($element, translateCss(0, 0))
551
+ box = u.measure($element)
691
552
  travelDistance = box.top + box.height
692
- animate($ghost, translateCss(0, -travelDistance), options)
553
+ animateNow($element, translateCss(0, -travelDistance), options)
693
554
  )
694
555
 
695
- registerAnimation('move-from-top', ($ghost, options) ->
696
- $ghost.css(translateCss(0, 0))
697
- box = u.measure($ghost)
556
+ registerAnimation('move-from-top', ($element, options) ->
557
+ u.writeInlineStyle($element, translateCss(0, 0))
558
+ box = u.measure($element)
698
559
  travelDistance = box.top + box.height
699
- $ghost.css(translateCss(0, -travelDistance))
700
- animate($ghost, translateCss(0, 0), options)
560
+ u.writeInlineStyle($element, translateCss(0, -travelDistance))
561
+ animateNow($element, translateCss(0, 0), options)
701
562
  )
702
563
 
703
- registerAnimation('move-to-bottom', ($ghost, options) ->
704
- $ghost.css(translateCss(0, 0))
705
- box = u.measure($ghost)
564
+ registerAnimation('move-to-bottom', ($element, options) ->
565
+ u.writeInlineStyle($element, translateCss(0, 0))
566
+ box = u.measure($element)
706
567
  travelDistance = u.clientSize().height - box.top
707
- animate($ghost, translateCss(0, travelDistance), options)
568
+ animateNow($element, translateCss(0, travelDistance), options)
708
569
  )
709
570
 
710
- registerAnimation('move-from-bottom', ($ghost, options) ->
711
- $ghost.css(translateCss(0, 0))
712
- box = u.measure($ghost)
571
+ registerAnimation('move-from-bottom', ($element, options) ->
572
+ u.writeInlineStyle($element, translateCss(0, 0))
573
+ box = u.measure($element)
713
574
  travelDistance = u.clientSize().height - box.top
714
- $ghost.css(translateCss(0, travelDistance))
715
- animate($ghost, translateCss(0, 0), options)
575
+ u.writeInlineStyle($element, translateCss(0, travelDistance))
576
+ animateNow($element, translateCss(0, 0), options)
716
577
  )
717
578
 
718
- registerAnimation('move-to-left', ($ghost, options) ->
719
- $ghost.css(translateCss(0, 0))
720
- box = u.measure($ghost)
579
+ registerAnimation('move-to-left', ($element, options) ->
580
+ u.writeInlineStyle($element, translateCss(0, 0))
581
+ box = u.measure($element)
721
582
  travelDistance = box.left + box.width
722
- animate($ghost, translateCss(-travelDistance, 0), options)
583
+ animateNow($element, translateCss(-travelDistance, 0), options)
723
584
  )
724
585
 
725
- registerAnimation('move-from-left', ($ghost, options) ->
726
- $ghost.css(translateCss(0, 0))
727
- box = u.measure($ghost)
586
+ registerAnimation('move-from-left', ($element, options) ->
587
+ u.writeInlineStyle($element, translateCss(0, 0))
588
+ box = u.measure($element)
728
589
  travelDistance = box.left + box.width
729
- $ghost.css(translateCss(-travelDistance, 0))
730
- animate($ghost, translateCss(0, 0), options)
590
+ u.writeInlineStyle($element, translateCss(-travelDistance, 0))
591
+ animateNow($element, translateCss(0, 0), options)
731
592
  )
732
593
 
733
- registerAnimation('move-to-right', ($ghost, options) ->
734
- $ghost.css(translateCss(0, 0))
735
- box = u.measure($ghost)
594
+ registerAnimation('move-to-right', ($element, options) ->
595
+ u.writeInlineStyle($element, translateCss(0, 0))
596
+ box = u.measure($element)
736
597
  travelDistance = u.clientSize().width - box.left
737
- animate($ghost, translateCss(travelDistance, 0), options)
598
+ animateNow($element, translateCss(travelDistance, 0), options)
738
599
  )
739
600
 
740
- registerAnimation('move-from-right', ($ghost, options) ->
741
- $ghost.css(translateCss(0, 0))
742
- box = u.measure($ghost)
601
+ registerAnimation('move-from-right', ($element, options) ->
602
+ u.writeInlineStyle($element, translateCss(0, 0))
603
+ box = u.measure($element)
743
604
  travelDistance = u.clientSize().width - box.left
744
- $ghost.css(translateCss(travelDistance, 0))
745
- animate($ghost, translateCss(0, 0), options)
605
+ u.writeInlineStyle($element, translateCss(travelDistance, 0))
606
+ animateNow($element, translateCss(0, 0), options)
746
607
  )
747
608
 
748
- registerAnimation('roll-down', ($ghost, options) ->
749
- fullHeight = $ghost.height()
750
- styleMemo = u.temporaryCss($ghost,
609
+ registerAnimation('roll-down', ($element, options) ->
610
+ fullHeight = $element.height()
611
+ styleMemo = u.writeTemporaryStyle($element,
751
612
  height: '0px'
752
613
  overflow: 'hidden'
753
614
  )
754
- deferred = animate($ghost, { height: "#{fullHeight}px" }, options)
615
+ deferred = animate($element, { height: "#{fullHeight}px" }, options)
755
616
  deferred.then(styleMemo)
756
617
  deferred
757
618
  )
758
619
 
759
- registerTransition('move-left', 'move-to-left/move-from-right')
760
- registerTransition('move-right', 'move-to-right/move-from-left')
761
- registerTransition('move-up', 'move-to-top/move-from-bottom')
762
- registerTransition('move-down', 'move-to-bottom/move-from-top')
763
- registerTransition('cross-fade', 'fade-out/fade-in')
620
+ registerTransition('move-left', ['move-to-left', 'move-from-right'])
621
+ registerTransition('move-right', ['move-to-right', 'move-from-left'])
622
+ registerTransition('move-up', ['move-to-top', 'move-from-bottom'])
623
+ registerTransition('move-down', ['move-to-bottom', 'move-from-top'])
624
+ registerTransition('cross-fade', ['fade-out', 'fade-in'])
764
625
 
765
626
  up.on 'up:framework:booted', snapshot
766
627
  up.on 'up:framework:reset', reset
767
628
 
629
+ <% if ENV['JS_KNIFE'] %>knife: eval(Knife.point)<% end %>
768
630
  morph: morph
769
631
  animate: animate
770
632
  animateOptions: animateOptions
771
633
  willAnimate: willAnimate
772
634
  finish: finish
635
+ finishCount: -> motionTracker.finishCount
773
636
  transition: registerTransition
774
637
  animation: registerAnimation
775
638
  config: config
776
639
  isEnabled: isEnabled
777
- prependCopy: prependCopy
778
640
  isNone: isNone
779
641
 
780
642
  )(jQuery)