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