unpoly-rails 0.55.1 → 0.56.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 (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)