dist/up.js in upjs-rails-0.7.8 vs dist/up.js in upjs-rails-0.8.0

- old
+ new

@@ -23,11 +23,11 @@ (function() { var slice = [].slice; up.util = (function() { - var $createElementFromSelector, ANIMATION_PROMISE_KEY, CONSOLE_PLACEHOLDERS, ajax, castsToFalse, castsToTrue, clientSize, compact, contains, copy, copyAttributes, createElement, createElementFromHtml, createSelectorFromElement, cssAnimate, debug, detect, each, error, escapePressed, extend, findWithSelf, finishCssAnimate, forceCompositing, get, ifGiven, isArray, isBlank, isDeferred, isDefined, isElement, isFunction, isGiven, isHash, isJQuery, isMissing, isNull, isObject, isPresent, isPromise, isStandardPort, isString, isUndefined, isUnmodifiedKeyEvent, isUnmodifiedMouseEvent, keys, last, locationFromXhr, measure, merge, methodFromXhr, nextFrame, normalizeMethod, normalizeUrl, nullJquery, only, option, options, prependGhost, presence, presentAttr, resolvableWhen, resolvedDeferred, resolvedPromise, select, setMissingAttrs, stringifyConsoleArgs, temporaryCss, times, toArray, trim, unwrap, warn; + var $createElementFromSelector, ANIMATION_PROMISE_KEY, CONSOLE_PLACEHOLDERS, ajax, castsToFalse, castsToTrue, clientSize, compact, contains, copy, copyAttributes, createElement, createElementFromHtml, createSelectorFromElement, cssAnimate, debug, detect, each, error, escapePressed, extend, findWithSelf, finishCssAnimate, forceCompositing, get, ifGiven, isArray, isBlank, isDeferred, isDefined, isElement, isFunction, isGiven, isHash, isJQuery, isMissing, isNull, isObject, isPresent, isPromise, isStandardPort, isString, isUndefined, isUnmodifiedKeyEvent, isUnmodifiedMouseEvent, keys, last, locationFromXhr, measure, merge, methodFromXhr, nextFrame, normalizeMethod, normalizeUrl, nullJquery, only, option, options, prependGhost, presence, presentAttr, remove, resolvableWhen, resolvedDeferred, resolvedPromise, select, setMissingAttrs, stringifyConsoleArgs, temporaryCss, times, toArray, trim, unwrap, warn; get = function(url, options) { options = options || {}; options.url = url; return ajax(options); }; @@ -707,10 +707,18 @@ results.push(void 0); } } return results; }; + remove = function(array, element) { + var index; + index = array.indexOf(element); + if (index >= 0) { + array.splice(index, 1); + return element; + } + }; return { presentAttr: presentAttr, createElement: createElement, normalizeUrl: normalizeUrl, normalizeMethod: normalizeMethod, @@ -776,11 +784,12 @@ trim: trim, keys: keys, resolvedPromise: resolvedPromise, resolvedDeferred: resolvedDeferred, resolvableWhen: resolvableWhen, - setMissingAttrs: setMissingAttrs + setMissingAttrs: setMissingAttrs, + remove: remove }; })(); }).call(this); @@ -930,20 +939,20 @@ We need to work on this page: - Decide whether to refactor this into document events - Decide whether `fragment:enter` and `fragment:leave` would be better names +- Decide if we wouldn't rather document events in the respective module (e.g. proxy). - @class up.bus */ (function() { var slice = [].slice; up.bus = (function() { - var callbacksByEvent, callbacksFor, defaultCallbacksByEvent, emit, listen, reset, snapshot, u; + var callbacksByEvent, callbacksFor, defaultCallbacksByEvent, emit, listen, reset, snapshot, stopListen, u; u = up.util; callbacksByEvent = {}; defaultCallbacksByEvent = {}; callbacksFor = function(event) { return callbacksByEvent[event] || (callbacksByEvent[event] = []); @@ -981,20 +990,50 @@ /** Registers an event handler to be called when the given event is triggered. @method up.bus.on - @param {String} eventName - The event name to match. + @param {String} eventNames + A space-separated list of event names to match. @param {Function} handler The event handler to be called with the event arguments. + @return {Function} + A function that unregisters the given handlers */ - listen = function(eventName, handler) { - return callbacksFor(eventName).push(handler); + listen = function(eventNames, handler) { + var eventName, i, len, ref; + ref = eventNames.split(' '); + for (i = 0, len = ref.length; i < len; i++) { + eventName = ref[i]; + callbacksFor(eventName).push(handler); + } + return function() { + return stopListen(eventNames, handler); + }; }; /** + Unregisters the given handler from the given events. + + @method up.bus.off + @param {String} eventNames + A space-separated list of event names . + @param {Function} handler + The event handler that should stop listening. + */ + stopListen = function(eventNames, handler) { + var eventName, i, len, ref, results; + ref = eventNames.split(' '); + results = []; + for (i = 0, len = ref.length; i < len; i++) { + eventName = ref[i]; + results.push(u.remove(callbacksFor(eventName), handler)); + } + return results; + }; + + /** Triggers an event over the framework bus. All arguments will be passed as arguments to event listeners: up.bus.on('foo:bar', function(x, y) { @@ -1026,10 +1065,11 @@ }; listen('framework:ready', snapshot); listen('framework:reset', reset); return { on: listen, + off: stopListen, emit: emit }; })(); }).call(this); @@ -1242,11 +1282,12 @@ } request = { url: url, method: options.method, selector: selector, - cache: options.cache + cache: options.cache, + preload: options.preload }; promise = up.proxy.ajax(request); promise.done(function(html, textStatus, xhr) { var currentLocation, newRequest; if (currentLocation = u.locationFromXhr(xhr)) { @@ -2619,32 +2660,91 @@ Caching and preloading ====================== All HTTP requests go through the Up.js proxy. It caches a [limited](/up.proxy#up.proxy.defaults) number of server responses - for a [limited](/up.proxy#up.proxy.defaults) amount of time, +for a [limited](/up.proxy#up.proxy.defaults) amount of time, making requests to these URLs return insantly. The cache is cleared whenever the user makes a non-`GET` request (like `POST`, `PUT` or `DELETE`). The proxy can also used to speed up reaction times by preloading links when the user hovers over the click area (or puts the mouse/finger down before releasing). This way the -response will already be cached when the user performs the click. +response will already be cached when the user performs the click. +Spinners +--------- + +You can listen to [framework events](/up.bus) to implement a spinner +(progress indicator) that appears during a long-running request, +and disappears once the response has been received: + + <div class="spinner">Please wait!</div> + +Here is the Javascript to make it alive: + + up.compiler('.spinner', function($element) { + + show = function() { $element.show() }; + hide = function() { $element.hide() }; + + up.bus.on('proxy:busy', show); + up.bus.on('proxy:idle', hide); + + return function() { + up.bus.off('proxy:busy', show); + up.bus.off('proxy:idle', hide); + }; + + }); + +The `proxy:busy` event will be emitted after a delay of 300 ms +to prevent the spinner from flickering on and off. +You can change (or remove) this delay like this: + + up.proxy.defaults({ busyDelay: 150 }); + @class up.proxy */ (function() { up.proxy = (function() { - var $waitingLink, SAFE_HTTP_METHODS, ajax, alias, cache, cacheKey, cancelDelay, checkPreload, clear, config, defaults, delayTimer, get, isFresh, isIdempotent, normalizeRequest, preload, remove, reset, set, startDelay, timestamp, trim, u; - config = { + var $waitingLink, FACTORY_CONFIG, SAFE_HTTP_METHODS, ajax, alias, busy, busyDelayTimer, busyEventEmitted, cache, cacheKey, cancelBusyDelay, cancelPreloadDelay, checkPreload, clear, config, defaults, get, idle, isFresh, isIdempotent, load, loadEnded, loadStarted, normalizeRequest, pendingCount, preload, preloadDelayTimer, remove, reset, set, startPreloadDelay, timestamp, trim, u; + u = up.util; + cache = void 0; + $waitingLink = void 0; + preloadDelayTimer = void 0; + busyDelayTimer = void 0; + pendingCount = void 0; + config = void 0; + busyEventEmitted = void 0; + FACTORY_CONFIG = { + busyDelay: 300, preloadDelay: 75, cacheSize: 70, cacheExpiry: 1000 * 60 * 5 }; + cancelPreloadDelay = function() { + clearTimeout(preloadDelayTimer); + return preloadDelayTimer = null; + }; + cancelBusyDelay = function() { + clearTimeout(busyDelayTimer); + return busyDelayTimer = null; + }; + reset = function() { + cache = {}; + $waitingLink = null; + cancelPreloadDelay(); + cancelBusyDelay(); + pendingCount = 0; + config = u.copy(FACTORY_CONFIG); + return busyEventEmitted = false; + }; + reset(); /** @method up.proxy.defaults @param {Number} [options.preloadDelay=75] The number of milliseconds to wait before [`[up-preload]`](#up-preload) @@ -2653,18 +2753,17 @@ The maximum number of responses to cache. If the size is exceeded, the oldest items will be dropped from the cache. @param {Number} [options.cacheExpiry=300000] The number of milliseconds until a cache entry expires. Defaults to 5 minutes. + @param {Number} [options.busyDelay=300] + How long the proxy waits until emitting the `proxy:busy` [event](/up.bus). + Use this to prevent flickering of spinners. */ defaults = function(options) { return u.extend(config, options); }; - cache = {}; - u = up.util; - $waitingLink = null; - delayTimer = null; cacheKey = function(request) { normalizeRequest(request); return [request.url, request.method, request.data, request.selector].join('|'); }; trim = function() { @@ -2716,35 +2815,107 @@ If requesting a URL that is not read-only, the response will not be cached and the entire cache will be cleared. Only requests with a method of `GET`, `OPTIONS` and `HEAD` are considered to be read-only. + If a network connection is attempted, the proxy will emit + a `proxy:load` event with the `request` as its argument. + Once the response is received, a `proxy:receive` event will + be emitted. + @method up.proxy.ajax @param {String} request.url @param {String} [request.method='GET'] @param {String} [request.selector] @param {Boolean} [request.cache] Whether to use a cached response, if available. If set to `false` a network connection will always be attempted. */ ajax = function(options) { - var forceCache, ignoreCache, promise, request; + var forceCache, ignoreCache, pending, promise, request; forceCache = u.castsToTrue(options.cache); ignoreCache = u.castsToFalse(options.cache); request = u.only(options, 'url', 'method', 'data', 'selector', '_normalized'); + pending = true; if (!isIdempotent(request) && !forceCache) { clear(); - promise = u.ajax(request); + promise = load(request); } else if ((promise = get(request)) && !ignoreCache) { - promise; + pending = promise.state() === 'pending'; } else { - promise = u.ajax(request); + promise = load(request); set(request, promise); } + if (pending && !options.preload) { + loadStarted(); + promise.then(loadEnded); + } return promise; }; SAFE_HTTP_METHODS = ['GET', 'OPTIONS', 'HEAD']; + + /** + Returns `true` if the proxy is not currently waiting + for a request to finish. Returns `false` otherwise. + + The proxy will also emit an `proxy:idle` [event](/up.bus) if it + used to busy, but is now idle. + + @method up.proxy.idle + @return {Boolean} Whether the proxy is idle + */ + idle = function() { + return pendingCount === 0; + }; + + /** + Returns `true` if the proxy is currently waiting + for a request to finish. Returns `false` otherwise. + + The proxy will also emit an `proxy:busy` [event](/up.bus) if it + used to idle, but is now busy. + + @method up.proxy.busy + @return {Boolean} Whether the proxy is busy + */ + busy = function() { + return pendingCount > 0; + }; + loadStarted = function() { + var emission, wasIdle; + wasIdle = idle(); + pendingCount += 1; + if (wasIdle) { + emission = function() { + if (busy()) { + up.bus.emit('proxy:busy'); + return busyEventEmitted = true; + } + }; + if (config.busyDelay > 0) { + return busyDelayTimer = setTimeout(emission, config.busyDelay); + } else { + return emission(); + } + } + }; + loadEnded = function() { + pendingCount -= 1; + if (idle() && busyEventEmitted) { + up.bus.emit('proxy:idle'); + return busyEventEmitted = false; + } + }; + load = function(request) { + var promise; + up.bus.emit('proxy:load', request); + promise = u.ajax(request); + promise.then(function() { + return up.bus.emit('proxy:receive', request); + }); + return promise; + }; isIdempotent = function(request) { normalizeRequest(request); return u.contains(SAFE_HTTP_METHODS, request.method); }; isFresh = function(promise) { @@ -2808,24 +2979,20 @@ checkPreload = function($link) { var curriedPreload, delay; delay = parseInt(u.presentAttr($link, 'up-delay')) || config.preloadDelay; if (!$link.is($waitingLink)) { $waitingLink = $link; - cancelDelay(); + cancelPreloadDelay(); curriedPreload = function() { return preload($link); }; - return startDelay(curriedPreload, delay); + return startPreloadDelay(curriedPreload, delay); } }; - startDelay = function(block, delay) { - return delayTimer = setTimeout(block, delay); + startPreloadDelay = function(block, delay) { + return preloadDelayTimer = setTimeout(block, delay); }; - cancelDelay = function() { - clearTimeout(delayTimer); - return delayTimer = null; - }; /** @protected @method up.proxy.preload @return @@ -2845,14 +3012,10 @@ } else { u.debug("Won't preload %o due to unsafe method %o", $link, method); return u.resolvedPromise(); } }; - reset = function() { - cancelDelay(); - return cache = {}; - }; up.bus.on('framework:reset', reset); /** Links with an `up-preload` attribute will silently fetch their target when the user hovers over the click area, or when the user puts her @@ -2878,9 +3041,11 @@ get: get, set: set, alias: alias, clear: clear, remove: remove, + idle: idle, + busy: busy, defaults: defaults }; })(); }).call(this);