dist/up.js in upjs-rails-0.3.2 vs dist/up.js in upjs-rails-0.3.3

- old
+ new

@@ -23,11 +23,11 @@ (function() { var __slice = [].slice; up.util = (function() { - var $createElementFromSelector, ajax, castsToFalse, castsToTrue, clientSize, contains, copy, copyAttributes, createElement, createElementFromHtml, createSelectorFromElement, cssAnimate, detect, each, error, escapePressed, extend, findWithSelf, forceCompositing, get, ifGiven, isArray, isBlank, isDefined, isFunction, isGiven, isHash, isJQuery, isMissing, isNull, isObject, isPresent, isPromise, isStandardPort, isString, isUndefined, keys, last, locationFromXhr, measure, merge, nextFrame, normalizeUrl, only, option, options, prependGhost, presence, presentAttr, resolvedPromise, select, temporaryCss, trim, unwrap; + var $createElementFromSelector, ANIMATION_PROMISE_KEY, ajax, castsToFalse, castsToTrue, clientSize, contains, copy, copyAttributes, createElement, createElementFromHtml, createSelectorFromElement, cssAnimate, detect, each, error, escapePressed, extend, findWithSelf, finishCssAnimate, forceCompositing, get, ifGiven, isArray, isBlank, isDeferred, isDefined, isFunction, isGiven, isHash, isJQuery, isMissing, isNull, isObject, isPresent, isPromise, isStandardPort, isString, isUndefined, keys, last, locationFromXhr, measure, merge, nextFrame, normalizeUrl, only, option, options, prependGhost, presence, presentAttr, resolvableWhen, resolvedDeferred, resolvedPromise, select, temporaryCss, trim, unwrap; get = function(url, options) { options = options || {}; options.url = url; return ajax(options); }; @@ -130,16 +130,16 @@ element.innerHTML = html; } return element; }; error = function() { - var args, message; + var args, asString; args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; - message = args.length === 1 && up.util.isString(args[0]) ? args[0] : JSON.stringify(args); - console.log.apply(console, ["[UP] Error: " + message].concat(__slice.call(args))); - alert(message); - throw message; + console.log.apply(console, ["[UP] Error"].concat(__slice.call(args))); + asString = args.length === 1 && up.util.isString(args[0]) ? args[0] : JSON.stringify(args); + alert(asString); + throw asString; }; createSelectorFromElement = function($element) { var classString, classes, id, klass, selector, _i, _len; console.log("Creating selector from element", $element); classes = (classString = $element.attr("class")) ? classString.split(" ") : []; @@ -250,12 +250,15 @@ }; isJQuery = function(object) { return object instanceof jQuery; }; isPromise = function(object) { - return isFunction(object.then); + return isObject(object) && isFunction(object.then); }; + isDeferred = function(object) { + return isPromise(object) && isFunction(object.resolve); + }; ifGiven = function(object) { if (isGiven(object)) { return object; } }; @@ -406,10 +409,16 @@ }; /** Animates the given element's CSS properties using CSS transitions. + If the element is already being animated, the previous animation + will instantly jump to its last frame before the new animation begins. + + To improve performance, the element will be forced into compositing for + the duration of the animation. + @method up.util.cssAnimate @param {Element|jQuery|String} elementOrSelector The element to animate. @param {Object} lastFrame The CSS properties that should be transitioned to. @@ -423,11 +432,11 @@ for a list of pre-defined timing functions. @return A promise for the animation's end. */ cssAnimate = function(elementOrSelector, lastFrame, opts) { - var $element, deferred, transition, withoutCompositing, withoutTransition; + var $element, deferred, endTimeout, transition, withoutCompositing, withoutTransition; $element = $(elementOrSelector); if (up.browser.canCssAnimation()) { opts = options(opts, { duration: 300, delay: 0, @@ -443,19 +452,45 @@ withoutCompositing = forceCompositing($element); withoutTransition = temporaryCss($element, transition); $element.css(lastFrame); deferred.then(withoutCompositing); deferred.then(withoutTransition); - setTimeout((function() { + $element.data(ANIMATION_PROMISE_KEY, deferred); + deferred.then(function() { + return $element.removeData(ANIMATION_PROMISE_KEY); + }); + endTimeout = setTimeout((function() { return deferred.resolve(); }), opts.duration + opts.delay); - return deferred.promise(); + deferred.then(function() { + return clearTimeout(endTimeout); + }); + return deferred; } else { $element.css(lastFrame); return resolvedPromise(); } }; + ANIMATION_PROMISE_KEY = 'up-animation-promise'; + + /* + Completes the animation for the given element by jumping + to the last frame instantly. All callbacks chained to + the original animation's promise will be called. + + Does nothing if the given element is not currently animating. + + @param {Element|jQuery|String} elementOrSelector + */ + finishCssAnimate = function(elementOrSelector) { + return $(elementOrSelector).each(function() { + var existingAnimation; + if (existingAnimation = $(this).data(ANIMATION_PROMISE_KEY)) { + return existingAnimation.resolve(); + } + }); + }; measure = function($element, options) { var box, coordinates, viewport; coordinates = (options != null ? options.relative : void 0) ? $element.position() : $element.offset(); box = { left: coordinates.left, @@ -534,16 +569,30 @@ filtered[key] = object[key]; } } return filtered; }; - resolvedPromise = function() { + resolvedDeferred = function() { var deferred; deferred = $.Deferred(); deferred.resolve(); - return deferred.promise(); + return deferred; }; + resolvedPromise = function() { + return resolvedDeferred().promise(); + }; + resolvableWhen = function() { + var deferreds, joined; + deferreds = 1 <= arguments.length ? __slice.call(arguments, 0) : []; + joined = $.when.apply($, deferreds); + joined.resolve = function() { + return each(deferreds, function(deferred) { + return typeof deferred.resolve === "function" ? deferred.resolve() : void 0; + }); + }; + return joined; + }; return { presentAttr: presentAttr, createElement: createElement, normalizeUrl: normalizeUrl, createElementFromHtml: createElementFromHtml, @@ -572,17 +621,19 @@ isObject: isObject, isFunction: isFunction, isString: isString, isJQuery: isJQuery, isPromise: isPromise, + isDeferred: isDeferred, isHash: isHash, ifGiven: ifGiven, unwrap: unwrap, nextFrame: nextFrame, measure: measure, temporaryCss: temporaryCss, cssAnimate: cssAnimate, + finishCssAnimate: finishCssAnimate, forceCompositing: forceCompositing, prependGhost: prependGhost, escapePressed: escapePressed, copyAttributes: copyAttributes, findWithSelf: findWithSelf, @@ -593,11 +644,13 @@ locationFromXhr: locationFromXhr, clientSize: clientSize, only: only, trim: trim, keys: keys, - resolvedPromise: resolvedPromise + resolvedPromise: resolvedPromise, + resolvedDeferred: resolvedDeferred, + resolvableWhen: resolvableWhen }; })(); }).call(this); @@ -610,11 +663,11 @@ (function() { var __slice = [].slice; up.browser = (function() { - var canCssAnimation, canPushState, ensureConsoleExists, isSupported, loadPage, memoize, u, url; + var canCssAnimation, canInputEvent, canPushState, ensureConsoleExists, isSupported, loadPage, memoize, u, url; u = up.util; loadPage = function(url, options) { var $form, csrfParam, csrfToken, metadataInput, method, target; if (options == null) { options = {}; @@ -667,19 +720,23 @@ return u.isDefined(history.pushState); }); canCssAnimation = memoize(function() { return 'transition' in document.documentElement.style; }); + canInputEvent = memoize(function() { + return 'oninput' in document.createElement('input'); + }); isSupported = memoize(function() { return u.isDefined(document.addEventListener); }); return { url: url, ensureConsoleExists: ensureConsoleExists, loadPage: loadPage, canPushState: canPushState, canCssAnimation: canCssAnimation, + canInputEvent: canInputEvent, isSupported: isSupported }; })(); }).call(this); @@ -902,10 +959,11 @@ options.title || (options.title = (_ref = htmlElement.querySelector("title")) != null ? _ref.textContent : void 0); _ref1 = implantSteps(selector, options); _results = []; for (_i = 0, _len = _ref1.length; _i < _len; _i++) { step = _ref1[_i]; + up.motion.finish(step.selector); $old = u.presence($(".up-popup " + step.selector)) || u.presence($(".up-modal " + step.selector)) || u.presence($(step.selector)) || u.error("Could not find selector (" + step.selector + ") in current body HTML"); if (fragment = htmlElement.querySelector(step.selector)) { $new = $(fragment); _results.push(swapElements($old, $new, step.pseudoClass, step.transition, options)); } else { @@ -1386,11 +1444,11 @@ @class up.motion */ (function() { up.motion = (function() { - var animate, animation, animations, assertIsPromise, config, defaultAnimations, defaultTransitions, defaults, findAnimation, morph, none, reset, snapshot, transition, transitions, u, withGhosts; + var GHOSTING_PROMISE_KEY, animate, animation, animations, assertIsDeferred, config, defaultAnimations, defaultTransitions, defaults, findAnimation, finish, finishGhosting, morph, none, reset, resolvableWhen, snapshot, transition, transitions, u, withGhosts; u = up.util; config = { duration: 300, delay: 0, easing: 'ease' @@ -1411,10 +1469,13 @@ }; /** Animates an element. + If the element is already being animated, the previous animation + will instantly jump to its last frame before the new animation begins. + The following animations are pre-registered: - `fade-in` - `fade-out` - `move-to-top` @@ -1437,13 +1498,14 @@ A promise for the animation's end. */ animate = function(elementOrSelector, animation, options) { var $element; $element = $(elementOrSelector); + finish($element); options = u.options(options, config); if (u.isFunction(animation)) { - return assertIsPromise(animation($element, options), ["Animation did not return a Promise", animation]); + return assertIsDeferred(animation($element, options), animation); } else if (u.isString(animation)) { return animate($element, findAnimation(animation), options); } else if (u.isHash(animation)) { return u.cssAnimate($element, animation, options); } else { @@ -1451,10 +1513,11 @@ } }; findAnimation = function(name) { return animations[name] || u.error("Unknown animation", animation); }; + GHOSTING_PROMISE_KEY = 'up-ghosting-promise'; withGhosts = function($old, $new, block) { var $newGhost, $oldGhost, newCssMemo, promise; $oldGhost = null; $newGhost = null; u.temporaryCss($new, { @@ -1472,23 +1535,56 @@ }); newCssMemo = u.temporaryCss($new, { display: 'none' }); promise = block($oldGhost, $newGhost); - return promise.then(function() { + $old.data(GHOSTING_PROMISE_KEY, promise); + $new.data(GHOSTING_PROMISE_KEY, promise); + promise.then(function() { + $old.removeData(GHOSTING_PROMISE_KEY); + $new.removeData(GHOSTING_PROMISE_KEY); $oldGhost.remove(); $newGhost.remove(); $old.css({ display: 'none' }); return newCssMemo(); }); + return promise; }; - assertIsPromise = function(object, messageParts) { - u.isPromise(object) || u.error.apply(u, messageParts); - return object; + + /* + Completes all animations and transitions for the given element + by jumping to the last animation frame instantly. All callbacks chained to + the original animation's promise will be called. + + Does nothing if the given element is not currently animating. + + @param {Element|jQuery|String} elementOrSelector + */ + finish = function(elementOrSelector) { + return $(elementOrSelector).each(function() { + var $element; + $element = $(this); + u.finishCssAnimate($element); + return finishGhosting($element); + }); }; + finishGhosting = function($element) { + var existingGhosting; + if (existingGhosting = $element.data(GHOSTING_PROMISE_KEY)) { + console.log("EXISTING", existingGhosting); + return typeof existingGhosting.resolve === "function" ? existingGhosting.resolve() : void 0; + } + }; + assertIsDeferred = function(object, origin) { + if (u.isDeferred(object)) { + return object; + } else { + return u.error("Did not return a promise with .then and .resolve methods: ", origin); + } + }; /** Performs a transition between two elements. The following transitions are pre-registered: @@ -1520,29 +1616,31 @@ var $new, $old, animation, parts, transition; if (up.browser.canCssAnimation()) { options = u.options(config); $old = $(source); $new = $(target); + finish($old); + finish($new); transition = u.presence(transitionOrName, u.isFunction) || transitions[transitionOrName]; if (transition) { return withGhosts($old, $new, function($oldGhost, $newGhost) { - return assertIsPromise(transition($oldGhost, $newGhost, options), ["Transition did not return a promise", transitionOrName]); + return assertIsDeferred(transition($oldGhost, $newGhost, options), transitionOrName); }); } else if (animation = animations[transitionOrName]) { $old.hide(); return animate($new, animation, options); } else if (u.isString(transitionOrName) && transitionOrName.indexOf('/') >= 0) { parts = transitionOrName.split('/'); transition = function($old, $new, options) { - return $.when(animate($old, parts[0], options), animate($new, parts[1], options)); + return resolvableWhen(animate($old, parts[0], options), animate($new, parts[1], options)); }; return morph($old, $new, transition, options); } else { return u.error("Unknown transition: " + transitionOrName); } } else { - return u.resolvedPromise(); + return u.resolvedDeferred(); } }; /** Defines a named transition. @@ -1573,18 +1671,27 @@ animations = u.copy(defaultAnimations); return transitions = u.copy(defaultTransitions); }; /** + Returns a new promise that resolves once all promises in the given array resolve. + Other then e.g. `$.then`, the combined promise will have a `resolve` method. + + @method up.motion.when + @param promises... + */ + resolvableWhen = u.resolvableWhen; + + /** Returns a no-op animation or transition which has no visual effects and completes instantly. @method up.motion.none @return {Promise} A resolved promise */ - none = u.resolvedPromise; + none = u.resolvedDeferred; animation('none', none); animation('fade-in', function($ghost, options) { $ghost.css({ opacity: 0 }); @@ -1699,33 +1806,35 @@ height: fullHeight + "px" }, options).then(styleMemo); }); transition('none', none); transition('move-left', function($old, $new, options) { - return $.when(animate($old, 'move-to-left', options), animate($new, 'move-from-right', options)); + return resolvableWhen(animate($old, 'move-to-left', options), animate($new, 'move-from-right', options)); }); transition('move-right', function($old, $new, options) { - return $.when(animate($old, 'move-to-right', options), animate($new, 'move-from-left', options)); + return resolvableWhen(animate($old, 'move-to-right', options), animate($new, 'move-from-left', options)); }); transition('move-up', function($old, $new, options) { - return $.when(animate($old, 'move-to-top', options), animate($new, 'move-from-bottom', options)); + return resolvableWhen(animate($old, 'move-to-top', options), animate($new, 'move-from-bottom', options)); }); transition('move-down', function($old, $new, options) { - return $.when(animate($old, 'move-to-bottom', options), animate($new, 'move-from-top', options)); + return resolvableWhen(animate($old, 'move-to-bottom', options), animate($new, 'move-from-top', options)); }); transition('cross-fade', function($old, $new, options) { - return $.when(animate($old, 'fade-out', options), animate($new, 'fade-in', options)); + return resolvableWhen(animate($old, 'fade-out', options), animate($new, 'fade-in', options)); }); up.bus.on('framework:ready', snapshot); up.bus.on('framework:reset', reset); return { morph: morph, animate: animate, + finish: finish, transition: transition, animation: animation, defaults: defaults, - none: none + none: none, + when: resolvableWhen }; })(); up.transition = up.motion.transition; @@ -2042,12 +2151,11 @@ }); }); }; /** - Observes an input field by periodic polling its value. - Executes code when the value changes. + Observes an input field and executes code when its value changes. up.observe('input', { change: function(value, $input) { up.submit($input) } }); @@ -2060,56 +2168,73 @@ @param {Function(value, $field)|String} options.change The callback to execute when the field's value changes. If given as a function, it must take two arguments (`value`, `$field`). If given as a string, it will be evaled as Javascript code in a context where (`value`, `$field`) are set. - @param {Number} [options.frequency=500] + @param {Number} [options.delay=0] + The number of miliseconds to wait before executing the callback + after the input value changes. Use this to limit how often the callback + will be invoked for a fast typist. */ observe = function(fieldOrSelector, options) { - var $field, callback, check, clearTimer, codeOnChange, knownValue, resetTimer, startTimer, timer; + var $field, callback, callbackPromise, callbackTimer, changeEvents, check, clearTimer, codeOnChange, delay, knownValue, nextCallback, runNextCallback; $field = $(fieldOrSelector); - options = u.options(options, { - frequency: 500 - }); + options = u.options(options); + delay = u.option($field.attr('up-delay'), options.delay, 0); + delay = parseInt(delay); knownValue = null; - timer = null; callback = null; + callbackTimer = null; if (codeOnChange = $field.attr('up-observe')) { callback = function(value, $field) { return eval(codeOnChange); }; } else if (options.change) { callback = options.change; } else { u.error('observe: No change callback given'); } + callbackPromise = u.resolvedPromise(); + nextCallback = null; + runNextCallback = function() { + var returnValue; + if (nextCallback) { + returnValue = nextCallback(); + nextCallback = null; + return returnValue; + } + }; check = function() { var skipCallback, value; value = $field.val(); skipCallback = _.isNull(knownValue); if (knownValue !== value) { knownValue = value; if (!skipCallback) { - return callback.apply($field.get(0), [value, $field]); + clearTimer(); + nextCallback = function() { + return callback.apply($field.get(0), [value, $field]); + }; + return callbackTimer = setTimeout(function() { + return callbackPromise.then(function() { + var returnValue; + returnValue = runNextCallback(); + if (u.isPromise(returnValue)) { + return callbackPromise = returnValue; + } else { + return callbackPromise = u.resolvedPromise(); + } + }); + }, delay); } } }; - resetTimer = function() { - if (timer) { - clearTimer(); - return startTimer(); - } - }; clearTimer = function() { - clearInterval(timer); - return timer = null; + return clearTimeout(callbackTimer); }; - startTimer = function() { - return timer = setInterval(check, options.frequency); - }; - $field.bind("keyup click mousemove", resetTimer); + changeEvents = up.browser.canInputEvent() ? 'input' : 'keypress paste cut change click propertychange'; + $field.on(changeEvents, check); check(); - startTimer(); return clearTimer; }; /** Submits the form through AJAX, searches the response for the selector