lib/assets/javascripts/unpoly/motion.coffee.erb in unpoly-rails-0.57.0 vs lib/assets/javascripts/unpoly/motion.coffee.erb in unpoly-rails-0.60.0
- old
+ new
@@ -27,22 +27,23 @@
and [predefined animations](/up.animate#named-animations).
You can define custom animations using [`up.transition()`](/up.transition) and
[`up.animation()`](/up.animation).
-@class up.motion
+@module up.motion
###
-up.motion = (($) ->
+up.motion = do ->
u = up.util
+ e = up.element
namedAnimations = {}
defaultNamedAnimations = {}
namedTransitions = {}
defaultNamedTransitions = {}
- motionTracker = new up.MotionTracker('motion')
+ motionController = new up.MotionController('motion')
###**
Sets default options for animations and transitions.
@property up.motion.config
@@ -63,18 +64,18 @@
Regardless of this setting, all animations will be skipped on browsers
that do not support [CSS transitions](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Transitions/Using_CSS_transitions).
@stable
###
- config = u.config
+ config = new up.Config
duration: 300
delay: 0
easing: 'ease'
enabled: true
reset = ->
- motionTracker.reset()
+ motionController.reset()
namedAnimations = u.copy(defaultNamedAnimations)
namedTransitions = u.copy(defaultNamedTransitions)
config.reset()
###**
@@ -92,19 +93,19 @@
###**
Applies the given animation to the given element.
\#\#\# Example
- up.animate('.warning', 'fade-in');
+ up.animate('.warning', 'fade-in')
You can pass additional options:
up.animate('warning', '.fade-in', {
delay: 1000,
duration: 250,
easing: 'linear'
- });
+ })
\#\#\# Named animations
The following animations are pre-defined:
@@ -125,14 +126,16 @@
\#\#\# Animating CSS properties directly
By passing an object instead of an animation name, you can animate
the CSS properties of the given element:
- var $warning = $('.warning');
- $warning.css({ opacity: 0 });
- up.animate($warning, { opacity: 1 });
+ var warning = document.querySelector('.warning')
+ warning.style.opacity = 0
+ up.animate(warning, { opacity: 1 })
+ CSS properties must be given in `kebab-case`, not `camelCase`.
+
\#\#\# Multiple animations on the same element
Unpoly doesn't allow more than one concurrent animation on the same element.
If you attempt to animate an element that is already being animated,
@@ -140,16 +143,16 @@
the new animation begins.
@function up.animate
@param {Element|jQuery|string} elementOrSelector
The element to animate.
- @param {string|Function|Object} animation
+ @param {string|Function(element, options): Promise|Object} animation
Can either be:
- The animation's name
- A function performing the animation
- - An object of CSS attributes describing the last frame of the animation
+ - An object of CSS attributes describing the last frame of the animation (using kebeb-case property names)
@param {number} [options.duration=300]
The duration of the animation, in milliseconds.
@param {number} [options.delay=0]
The delay before the animation starts, in milliseconds.
@param {string} [options.easing='ease']
@@ -160,41 +163,41 @@
@return {Promise}
A promise for the animation's end.
@stable
###
animate = (elementOrSelector, animation, options) ->
- $element = $(elementOrSelector)
+ element = e.get(elementOrSelector)
options = animateOptions(options)
animationFn = findAnimationFn(animation)
- willRun = willAnimate($element, animation, options)
+ willRun = willAnimate(element, animation, options)
if willRun
- runNow = -> animationFn($element, options)
- motionTracker.claim($element, runNow, options)
+ runNow = -> animationFn(element, options)
+ motionController.startFunction(element, runNow, options)
else
- skipAnimate($element, animation)
+ skipAnimate(element, animation)
- willAnimate = ($elements, animationOrTransition, options) ->
+ willAnimate = (element, animationOrTransition, options) ->
options = animateOptions(options)
- isEnabled() && !isNone(animationOrTransition) && options.duration > 0 && !u.isSingletonElement($elements)
+ isEnabled() && !isNone(animationOrTransition) && options.duration > 0 && !e.isSingleton(element)
- skipAnimate = ($element, animation) ->
+ skipAnimate = (element, animation) ->
if u.isOptions(animation)
# If we are given the final animation frame as an object of CSS properties,
# the best we can do is to set the final frame without animation.
- u.writeInlineStyle($element, animation)
+ e.setStyle(element, animation)
# Signal that the animation is already done.
Promise.resolve()
animCount = 0
###**
Animates the given element's CSS properties using CSS transitions.
Does not track the animation, nor does it finishes existing animations
- (use `up.motion.animate()` for that). It does, however, listen to the motionTracker's
+ (use `up.motion.animate()` for that). It does, however, listen to the motionController's
finish event.
@function animateNow
@param {Element|jQuery|string} elementOrSelector
The element to animate.
@@ -210,32 +213,36 @@
for a list of pre-defined timing functions.
@return {Promise}
A promise that fulfills when the animation ends.
@internal
###
- animateNow = ($element, lastFrame, options) ->
- options = u.merge(options, finishEvent: motionTracker.finishEvent)
- cssTransition = new up.CssTransition($element, lastFrame, options)
+ animateNow = (element, lastFrame, options) ->
+ options = u.merge(options, finishEvent: motionController.finishEvent)
+ cssTransition = new up.CssTransition(element, lastFrame, options)
return cssTransition.start()
###**
Extracts animation-related options from the given options hash.
- If `$element` is given, also inspects the element for animation-related
+ If `element` is given, also inspects the element for animation-related
attributes like `up-easing` or `up-duration`.
+ @param {Object} userOptions
+ @param {Element|jQuery} [element]
+ @param {Object} [moduleDefaults]
@function up.motion.animateOptions
@internal
###
animateOptions = (args...) ->
- userOptions = args.shift() || {}
- $element = if u.isJQuery(args[0]) then args.shift() else u.nullJQuery()
- moduleDefaults = if u.isObject(args[0]) then args.shift() else {}
+ userOptions = args.shift() ? {}
+ moduleDefaults = u.extractOptions(args)
+ element = args.pop() || e.none()
+
consolidatedOptions = {}
- consolidatedOptions.easing = u.option(userOptions.easing, u.presentAttr($element, 'up-easing'), moduleDefaults.easing, config.easing)
- consolidatedOptions.duration = Number(u.option(userOptions.duration, u.presentAttr($element, 'up-duration'), moduleDefaults.duration, config.duration))
- consolidatedOptions.delay = Number(u.option(userOptions.delay, u.presentAttr($element, 'up-delay'), moduleDefaults.delay, config.delay))
- consolidatedOptions.trackMotion = userOptions.trackMotion # required by up.MotionTracker
+ consolidatedOptions.easing = userOptions.easing ? element.getAttribute('up-easing') ? moduleDefaults.easing ? config.easing
+ consolidatedOptions.duration = userOptions.duration ? e.numberAttr(element, 'up-duration') ? moduleDefaults.duration ? config.duration
+ consolidatedOptions.delay = userOptions.delay ? e.numberAttr(element, 'up-delay') ? moduleDefaults.delay ? config.delay
+ consolidatedOptions.trackMotion = userOptions.trackMotion # required by up.MotionController
consolidatedOptions
findNamedAnimation = (name) ->
namedAnimations[name] or up.fail("Unknown animation %o", name)
@@ -245,23 +252,40 @@
If called without arguments, all animations on the screen are completed.
If given an element (or selector), animations on that element and its children
are completed.
Animations are completed by jumping to the last animation frame instantly.
+ Promises returned by animation and transition functions instantly settle.
+ Emits the `up:motion:finish` event that is already handled by `up.animate()`.
+
Does nothing if there are no animation to complete.
@function up.motion.finish
@param {Element|jQuery|string} [elementOrSelector]
@return {Promise}
A promise that fulfills when animations and transitions have finished.
@stable
###
finish = (elementOrSelector) ->
- motionTracker.finish(elementOrSelector)
+ motionController.finish(elementOrSelector)
###**
+ This event is emitted on an [animating](/up.animating) element by `up.motion.finish()` to
+ request the animation to instantly finish and skip to the last frame.
+
+ Promises returned by completed animation functions are expected to settle.
+
+ Animations started by `up.animate()` already handle this event.
+
+ @event up:motion:finish
+ @param {Element} event.target
+ The animating element.
+ @experimental
+ ###
+
+ ###**
Performs an animated transition between the `source` and `target` elements.
Transitions are implement by performing two animations in parallel,
causing `source` to disappear and the `target` to appear.
@@ -309,11 +333,11 @@
The old element remains hidden in the DOM.
@function up.morph
@param {Element|jQuery|string} source
@param {Element|jQuery|string} target
- @param {Function|string} transitionOrName
+ @param {Function(oldElement, newElement)|string} transition
@param {number} [options.duration=300]
The duration of the animation, in milliseconds.
@param {number} [options.delay=0]
The delay before the animation starts, in milliseconds.
@param {string} [options.easing='ease']
@@ -323,22 +347,21 @@
for a list of pre-defined timing functions.
@param {boolean} [options.reveal=false]
Whether to reveal the new element by scrolling its parent viewport.
@return {Promise}
A promise that fulfills when the transition ends.
- @experimental
+ @stable
###
- morph = (source, target, transitionObject, options) ->
+ morph = (oldElement, newElement, transitionObject, options) ->
options = u.options(options)
- options = u.assign(options, animateOptions(options))
+ u.assign(options, animateOptions(options))
- $old = $(source)
- $new = $(target)
- $both = $old.add($new)
+ oldElement = e.get(oldElement)
+ newElement = e.get(newElement)
transitionFn = findTransitionFn(transitionObject)
- willMorph = willAnimate($old, transitionFn, options)
+ willMorph = willAnimate(oldElement, transitionFn, options)
# Remove callbacks from our options hash in case transitionFn calls morph() recursively.
# If we passed on these callbacks, we might call destructors, events, etc. multiple times.
beforeStart = u.pluckKey(options, 'beforeStart') || u.noop
afterInsert = u.pluckKey(options, 'afterInsert') || u.noop
@@ -346,59 +369,58 @@
afterDetach = u.pluckKey(options, 'afterDetach') || u.noop
beforeStart()
scrollNew = ->
- # Don't animate the scrolling. The { duration } option was meant for the transition.
- scrollOptions = u.merge(options, duration: 0)
- # Scroll $new into position before we start the enter animation.
- up.layout.scrollAfterInsertFragment($new, scrollOptions)
+ # Don't animate the scrolling.
+ scrollOptions = u.merge(options, behavior: 'instant')
+ # Scroll newElement into position before we start the enter animation.
+ up.viewport.scrollAfterInsertFragment(newElement, scrollOptions)
if willMorph
- if motionTracker.isActive($old) && options.trackMotion is false
- return transitionFn($old, $new, options)
+ if motionController.isActive(oldElement) && options.trackMotion is false
+ return transitionFn(oldElement, newElement, options)
- up.puts 'Morphing %o to %o with transition %o', $old.get(0), $new.get(0), transitionObject
+ up.puts 'Morphing %o to %o with transition %o', oldElement, newElement, transitionObject
- $viewport = up.layout.viewportOf($old)
- scrollTopBeforeReveal = $viewport.scrollTop()
+ viewport = up.viewport.closest(oldElement)
+ scrollTopBeforeReveal = viewport.scrollTop
- oldRemote = up.layout.absolutize $old,
+ oldRemote = up.viewport.absolutize oldElement,
# Because the insertion will shift elements visually, we must delay insertion
# until absolutize() has measured the bounding box of the old element.
afterMeasure: ->
- $new.insertBefore($old)
+ e.insertBefore(oldElement, newElement)
afterInsert()
trackable = ->
- # Scroll $new into position before we start the enter animation.
+ # Scroll newElement into position before we start the enter animation.
promise = scrollNew()
promise = promise.then ->
- # Since we have scrolled the viewport (containing both $old and $new),
+ # Since we have scrolled the viewport (containing both oldElement and newElement),
# we must shift the old copy so it looks like it it is still sitting
# in the same position.
- scrollTopAfterReveal = $viewport.scrollTop()
- oldRemote.moveTop(scrollTopAfterReveal - scrollTopBeforeReveal)
+ scrollTopAfterReveal = viewport.scrollTop
+ oldRemote.moveBounds(0, scrollTopAfterReveal - scrollTopBeforeReveal)
- transitionFn($old, $new, options)
+ transitionFn(oldElement, newElement, options)
promise = promise.then ->
beforeDetach()
- $old.detach()
- oldRemote.$bounds.remove()
+ e.remove(oldRemote.bounds)
afterDetach()
return promise
- motionTracker.claim($both, trackable, options)
+ motionController.startFunction([oldElement, newElement], trackable, options)
else
beforeDetach()
# Swapping the elements directly with replaceWith() will cause
# jQuery to remove all data attributes, which we use to store destructors
- swapElementsDirectly($old, $new)
+ swapElementsDirectly(oldElement, newElement)
afterInsert()
afterDetach()
promise = scrollNew()
return promise
@@ -423,77 +445,78 @@
# and should be skipped.
undefined
else
oldAnimationFn = findAnimationFn(oldAnimation) || u.asyncNoop
newAnimationFn = findAnimationFn(newAnimation) || u.asyncNoop
- ($old, $new, options) ->
+ (oldElement, newElement, options) ->
Promise.all([
- oldAnimationFn($old, options),
- newAnimationFn($new, options)
+ oldAnimationFn(oldElement, options),
+ newAnimationFn(newElement, options)
])
findAnimationFn = (object) ->
if isNone(object)
undefined
else if u.isFunction(object)
object
else if u.isString(object)
findNamedAnimation(object)
else if u.isOptions(object)
- ($element, options) -> animateNow($element, object, options)
+ (element, options) -> animateNow(element, object, options)
else
up.fail('Unknown animation %o', object)
- swapElementsDirectly = ($old, $new) ->
- # jQuery will actually let us .insertBefore the new <body> tag,
- # but that's probably bad Karma.
- $old.replaceWith($new)
+ # Have a separate function so we can mock it in specs.
+ swapElementsDirectly = (oldElement, newElement) ->
+ e.replace(oldElement, newElement)
###**
- Defines a named transition.
+ Defines a named transition that [morphs](/up.element) from one element to another.
+ \#\#\# Example
+
Here is the definition of the pre-defined `cross-fade` animation:
- up.transition('cross-fade', ($old, $new, options) ->
- up.motion.when(
- up.animate($old, 'fade-out', options),
- up.animate($new, 'fade-in', options)
- )
+ up.transition('cross-fade', (oldElement, newElement, options) ->
+ Promise.all([
+ up.animate(oldElement, 'fade-out', options),
+ up.animate(newElement, 'fade-in', options)
+ ])
)
It is recommended that your transitions use [`up.animate()`](/up.animate),
passing along the `options` that were passed to you.
If you choose to *not* use `up.animate()` and roll your own
logic instead, your code must honor the following contract:
- 1. It must honor the options `{ delay, duration, easing }` if given
+ 1. It must honor the options `{ delay, duration, easing }` if given.
2. It must *not* remove any of the given elements from the DOM.
- 3. It returns a promise that is fulfilled when the transition has ended
+ 3. It returns a promise that is fulfilled when the transition has ended.
4. If during the animation an event `up:motion:finish` is emitted on
- the given element, the transition instantly jumps to the last frame
+ either element, the transition instantly jumps to the last frame
and resolves the returned promise.
Calling [`up.animate()`](/up.animate) with an object argument
will take care of all these points.
@function up.transition
@param {string} name
- @param {Function} transition
+ @param {Function(oldElement, newElement, options): Promise|Array} transition
@stable
###
registerTransition = (name, transition) ->
namedTransitions[name] = findTransitionFn(transition)
###**
Defines a named animation.
Here is the definition of the pre-defined `fade-in` animation:
- up.animation('fade-in', function($element, options) {
- $element.css(opacity: 0);
- up.animate($element, { opacity: 1 }, options);
+ up.animation('fade-in', function(element, options) {
+ element.style.opacity = 0
+ up.animate(element, { opacity: 1 }, options)
})
It is recommended that your definitions always end by calling
calling [`up.animate()`](/up.animate) with an object argument, passing along
the `options` that were passed to you.
@@ -511,11 +534,11 @@
Calling [`up.animate()`](/up.animate) with an object argument
will take care of all these points.
@function up.animation
@param {string} name
- @param {Function} animation
+ @param {Function(element, options): Promise} animation
@stable
###
registerAnimation = (name, animation) ->
namedAnimations[name] = findAnimationFn(animation)
@@ -532,90 +555,90 @@
###
isNone = (animationOrTransition) ->
# false, undefined, '', null and the string "none" are all ways to skip animations
!animationOrTransition || animationOrTransition == 'none' || u.isBlank(animationOrTransition)
- registerAnimation('fade-in', ($element, options) ->
- u.writeInlineStyle($element, opacity: 0)
- animateNow($element, { opacity: 1 }, options)
+ registerAnimation('fade-in', (element, options) ->
+ e.setStyle(element, opacity: 0)
+ animateNow(element, { opacity: 1 }, options)
)
- registerAnimation('fade-out', ($element, options) ->
- u.writeInlineStyle($element, opacity: 1)
- animateNow($element, { opacity: 0 }, options)
+ registerAnimation('fade-out', (element, options) ->
+ e.setStyle(element, opacity: 1)
+ animateNow(element, { opacity: 0 }, options)
)
translateCss = (x, y) ->
{ transform: "translate(#{x}px, #{y}px)" }
- registerAnimation('move-to-top', ($element, options) ->
- u.writeInlineStyle($element, translateCss(0, 0))
- box = u.measure($element)
+ registerAnimation('move-to-top', (element, options) ->
+ e.setStyle(element, translateCss(0, 0))
+ box = element.getBoundingClientRect()
travelDistance = box.top + box.height
- animateNow($element, translateCss(0, -travelDistance), options)
+ animateNow(element, translateCss(0, -travelDistance), options)
)
- registerAnimation('move-from-top', ($element, options) ->
- u.writeInlineStyle($element, translateCss(0, 0))
- box = u.measure($element)
+ registerAnimation('move-from-top', (element, options) ->
+ e.setStyle(element, translateCss(0, 0))
+ box = element.getBoundingClientRect()
travelDistance = box.top + box.height
- u.writeInlineStyle($element, translateCss(0, -travelDistance))
- animateNow($element, translateCss(0, 0), options)
+ e.setStyle(element, translateCss(0, -travelDistance))
+ animateNow(element, translateCss(0, 0), options)
)
- registerAnimation('move-to-bottom', ($element, options) ->
- u.writeInlineStyle($element, translateCss(0, 0))
- box = u.measure($element)
- travelDistance = u.clientSize().height - box.top
- animateNow($element, translateCss(0, travelDistance), options)
+ registerAnimation('move-to-bottom', (element, options) ->
+ e.setStyle(element, translateCss(0, 0))
+ box = element.getBoundingClientRect()
+ travelDistance = e.root().clientHeight - box.top
+ animateNow(element, translateCss(0, travelDistance), options)
)
- registerAnimation('move-from-bottom', ($element, options) ->
- u.writeInlineStyle($element, translateCss(0, 0))
- box = u.measure($element)
- travelDistance = u.clientSize().height - box.top
- u.writeInlineStyle($element, translateCss(0, travelDistance))
- animateNow($element, translateCss(0, 0), options)
+ registerAnimation('move-from-bottom', (element, options) ->
+ e.setStyle(element, translateCss(0, 0))
+ box = element.getBoundingClientRect()
+ travelDistance = up.viewport.rootHeight() - box.top
+ e.setStyle(element, translateCss(0, travelDistance))
+ animateNow(element, translateCss(0, 0), options)
)
- registerAnimation('move-to-left', ($element, options) ->
- u.writeInlineStyle($element, translateCss(0, 0))
- box = u.measure($element)
+ registerAnimation('move-to-left', (element, options) ->
+ e.setStyle(element, translateCss(0, 0))
+ box = element.getBoundingClientRect()
travelDistance = box.left + box.width
- animateNow($element, translateCss(-travelDistance, 0), options)
+ animateNow(element, translateCss(-travelDistance, 0), options)
)
- registerAnimation('move-from-left', ($element, options) ->
- u.writeInlineStyle($element, translateCss(0, 0))
- box = u.measure($element)
+ registerAnimation('move-from-left', (element, options) ->
+ e.setStyle(element, translateCss(0, 0))
+ box = element.getBoundingClientRect()
travelDistance = box.left + box.width
- u.writeInlineStyle($element, translateCss(-travelDistance, 0))
- animateNow($element, translateCss(0, 0), options)
+ e.setStyle(element, translateCss(-travelDistance, 0))
+ animateNow(element, translateCss(0, 0), options)
)
- registerAnimation('move-to-right', ($element, options) ->
- u.writeInlineStyle($element, translateCss(0, 0))
- box = u.measure($element)
- travelDistance = u.clientSize().width - box.left
- animateNow($element, translateCss(travelDistance, 0), options)
+ registerAnimation('move-to-right', (element, options) ->
+ e.setStyle(element, translateCss(0, 0))
+ box = element.getBoundingClientRect()
+ travelDistance = up.viewport.rootWidth() - box.left
+ animateNow(element, translateCss(travelDistance, 0), options)
)
- registerAnimation('move-from-right', ($element, options) ->
- u.writeInlineStyle($element, translateCss(0, 0))
- box = u.measure($element)
- travelDistance = u.clientSize().width - box.left
- u.writeInlineStyle($element, translateCss(travelDistance, 0))
- animateNow($element, translateCss(0, 0), options)
+ registerAnimation('move-from-right', (element, options) ->
+ e.setStyle(element, translateCss(0, 0))
+ box = element.getBoundingClientRect()
+ travelDistance = up.viewport.rootWidth() - box.left
+ e.setStyle(element, translateCss(travelDistance, 0))
+ animateNow(element, translateCss(0, 0), options)
)
- registerAnimation('roll-down', ($element, options) ->
- fullHeight = $element.height()
- styleMemo = u.writeTemporaryStyle($element,
+ registerAnimation('roll-down', (element, options) ->
+ previousHeightStr = e.style(element, 'height')
+ styleMemo = e.setTemporaryStyle(element,
height: '0px'
overflow: 'hidden'
)
- deferred = animate($element, { height: "#{fullHeight}px" }, options)
+ deferred = animate(element, { height: previousHeightStr }, options)
deferred.then(styleMemo)
deferred
)
registerTransition('move-left', ['move-to-left', 'move-from-right'])
@@ -629,19 +652,16 @@
<% if ENV['JS_KNIFE'] %>knife: eval(Knife.point)<% end %>
morph: morph
animate: animate
animateOptions: animateOptions
- willAnimate: willAnimate
finish: finish
- finishCount: -> motionTracker.finishCount
+ finishCount: -> motionController.finishCount
transition: registerTransition
animation: registerAnimation
config: config
isEnabled: isEnabled
isNone: isNone
-
-)(jQuery)
up.transition = up.motion.transition
up.animation = up.motion.animation
up.morph = up.motion.morph
up.animate = up.motion.animate