dist/up.js in upjs-rails-0.9.1 vs dist/up.js in upjs-rails-0.10.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, config, contains, copy, copyAttributes, createElement, createElementFromHtml, createSelectorFromElement, cssAnimate, debug, detect, each, endsWith, error, escapePressed, extend, findWithSelf, finishCssAnimate, forceCompositing, get, identity, ifGiven, isArray, isBlank, isDeferred, isDefined, isElement, isFunction, isGiven, isHash, isJQuery, isMissing, isNull, isObject, isPresent, isPromise, isStandardPort, isString, isUndefined, isUnmodifiedKeyEvent, isUnmodifiedMouseEvent, keys, last, locationFromXhr, map, measure, memoize, merge, methodFromXhr, nextFrame, normalizeMethod, normalizeUrl, nullJquery, once, only, option, options, presence, presentAttr, remove, resolvableWhen, resolvedDeferred, resolvedPromise, scrollbarWidth, select, setMissingAttrs, startsWith, stringifyConsoleArgs, temporaryCss, times, toArray, trim, unJquery, uniq, unwrapElement, warn; + var $createElementFromSelector, ANIMATION_PROMISE_KEY, CONSOLE_PLACEHOLDERS, ajax, cache, castedAttr, clientSize, compact, config, contains, copy, copyAttributes, createElement, createElementFromHtml, createSelectorFromElement, cssAnimate, debug, detect, each, endsWith, error, escapePressed, extend, findWithSelf, finishCssAnimate, forceCompositing, get, identity, ifGiven, isArray, isBlank, isDeferred, isDefined, isElement, isFunction, isGiven, isHash, isJQuery, isMissing, isNull, isNumber, isObject, isPresent, isPromise, isStandardPort, isString, isUndefined, isUnmodifiedKeyEvent, isUnmodifiedMouseEvent, keys, last, locationFromXhr, map, measure, memoize, merge, methodFromXhr, nextFrame, normalizeMethod, normalizeUrl, nullJquery, once, only, option, options, presence, presentAttr, remove, resolvableWhen, resolvedDeferred, resolvedPromise, scrollbarWidth, select, setMissingAttrs, startsWith, stringifyConsoleArgs, temporaryCss, times, toArray, trim, unJquery, uniq, unwrapElement, warn; memoize = function(func) { var cache, cached; cache = void 0; cached = false; return function() { @@ -333,10 +333,13 @@ return typeof object === 'function'; }; isString = function(object) { return typeof object === 'string'; }; + isNumber = function(object) { + return typeof object === 'number'; + }; isHash = function(object) { return typeof object === 'object' && !!object; }; isObject = function(object) { return isHash(object) || (typeof object === 'function'); @@ -689,16 +692,24 @@ return string.indexOf(element) === string.length - element.length; }; contains = function(stringOrArray, element) { return stringOrArray.indexOf(element) >= 0; }; - castsToTrue = function(object) { - return String(object) === "true"; + castedAttr = function($element, attrName) { + var value; + value = $element.attr(attrName); + switch (value) { + case 'false': + return false; + case 'true': + return true; + case '': + return true; + default: + return value; + } }; - castsToFalse = function(object) { - return String(object) === "false"; - }; locationFromXhr = function(xhr) { return xhr.getResponseHeader('X-Up-Location'); }; methodFromXhr = function(xhr) { return xhr.getResponseHeader('X-Up-Method'); @@ -773,16 +784,157 @@ if (index >= 0) { array.splice(index, 1); return element; } }; + + /** + @method up.util.cache + @param {Number|Function} [config.size] + Maximum number of cache entries. + Set to `undefined` to not limit the cache size. + @param {Number|Function} [config.expiry] + The number of milliseconds after which a cache entry + will be discarded. + @param {String} [config.log] + A prefix for log entries printed by this cache object. + */ + cache = function(config) { + var alias, clear, expiryMilis, isFresh, log, maxSize, normalizeStoreKey, set, store, timestamp; + store = void 0; + clear = function() { + return store = {}; + }; + clear(); + log = function() { + var args; + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + if (config.log) { + args[0] = "[" + config.log + "] " + args[0]; + return debug.apply(null, args); + } + }; + maxSize = function() { + if (isMissing(config.size)) { + return void 0; + } else if (isFunction(config.size)) { + return config.size(); + } else if (isNumber(config.size)) { + return config.size; + } else { + return error("Invalid size config: %o", config.size); + } + }; + expiryMilis = function() { + if (isMissing(config.expiry)) { + return void 0; + } else if (isFunction(config.expiry)) { + return config.expiry(); + } else if (isNumber(config.expiry)) { + return config.expiry; + } else { + return error("Invalid expiry config: %o", config.expiry); + } + }; + normalizeStoreKey = function(key) { + if (config.key) { + return config.key(key); + } else { + return key.toString(); + } + }; + trim = function() { + var oldestKey, oldestTimestamp, size, storeKeys; + storeKeys = copy(keys(store)); + size = maxSize(); + if (size && storeKeys.length > size) { + oldestKey = null; + oldestTimestamp = null; + each(storeKeys, function(key) { + var promise, timestamp; + promise = store[key]; + timestamp = promise.timestamp; + if (!oldestTimestamp || oldestTimestamp > timestamp) { + oldestKey = key; + return oldestTimestamp = timestamp; + } + }); + if (oldestKey) { + return delete store[oldestKey]; + } + } + }; + alias = function(oldKey, newKey) { + var value; + value = get(oldKey); + if (isDefined(value)) { + return set(newKey, value); + } + }; + timestamp = function() { + return (new Date()).valueOf(); + }; + set = function(key, value) { + var storeKey; + storeKey = normalizeStoreKey(key); + return store[storeKey] = { + timestamp: timestamp(), + value: value + }; + }; + remove = function(key) { + var storeKey; + storeKey = normalizeStoreKey(key); + return delete store[storeKey]; + }; + isFresh = function(entry) { + var expiry, timeSinceTouch; + expiry = expiryMilis(); + if (expiry) { + timeSinceTouch = timestamp() - entry.timestamp; + return timeSinceTouch < expiryMilis(); + } else { + return true; + } + }; + get = function(key, fallback) { + var entry, storeKey; + if (fallback == null) { + fallback = void 0; + } + storeKey = normalizeStoreKey(key); + if (entry = store[storeKey]) { + if (!isFresh(entry)) { + log("Discarding stale cache entry for %o", key); + remove(key); + return fallback; + } else { + log("Cache hit for %o", key); + return entry.value; + } + } else { + log("Cache miss for %o", key); + return fallback; + } + }; + return { + alias: alias, + get: get, + set: set, + remove: remove, + clear: clear + }; + }; config = function(factoryOptions) { var apiKeys, hash; if (factoryOptions == null) { factoryOptions = {}; } hash = { + ensureKeyExists: function(key) { + return factoryOptions.hasOwnProperty(key) || error("Unknown setting %o", key); + }, reset: function() { var j, key, len, ownKeys; ownKeys = copy(Object.getOwnPropertyNames(hash)); for (j = 0, len = ownKeys.length; j < len; j++) { key = ownKeys[j]; @@ -791,23 +943,27 @@ } } return hash.update(copy(factoryOptions)); }, update: function(options) { - var key, value; - if (options == null) { - options = {}; - } - for (key in options) { - value = options[key]; - if (factoryOptions.hasOwnProperty(key)) { - hash[key] = value; + var key, results, value; + if (options) { + if (isString(options)) { + hash.ensureKeyExists(options); + return hash[options]; } else { - error("Unknown setting %o", key); + results = []; + for (key in options) { + value = options[key]; + hash.ensureKeyExists(key); + results.push(hash[key] = value); + } + return results; } + } else { + return hash; } - return hash; } }; apiKeys = Object.getOwnPropertyNames(hash); hash.reset(); return hash; @@ -882,12 +1038,11 @@ contains: contains, startsWith: startsWith, endsWith: endsWith, isArray: isArray, toArray: toArray, - castsToTrue: castsToTrue, - castsToFalse: castsToFalse, + castedAttr: castedAttr, locationFromXhr: locationFromXhr, methodFromXhr: methodFromXhr, clientSize: clientSize, only: only, trim: trim, @@ -898,10 +1053,11 @@ setMissingAttrs: setMissingAttrs, remove: remove, memoize: memoize, scrollbarWidth: scrollbarWidth, config: config, + cache: cache, unwrapElement: unwrapElement }; })(); }).call(this); @@ -1152,11 +1308,10 @@ The arguments that describe the event. */ emit = function() { var args, callbacks, eventName; eventName = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : []; - u.debug("Emitting event %o with args %o", eventName, args); callbacks = callbacksFor(eventName); return u.each(callbacks, function(callback) { return callback.apply(null, args); }); }; @@ -1170,10 +1325,627 @@ })(); }).call(this); /** +Registering behavior and custom elements +======================================== + +Up.js keeps a persistent Javascript environment during page transitions. +To prevent memory leaks it is important to cleanly set up and tear down +event handlers and custom elements. + +\#\#\# Incomplete documentation! + +We need to work on this page: + +- Better class-level introduction for this module + +@class up.magic + */ + +(function() { + var slice = [].slice; + + up.magic = (function() { + var DESTROYABLE_CLASS, DESTROYER_KEY, applyCompiler, compile, compiler, compilers, data, defaultCompilers, defaultLiveDescriptions, destroy, live, liveDescriptions, onEscape, ready, reset, snapshot, u; + u = up.util; + DESTROYABLE_CLASS = 'up-destroyable'; + DESTROYER_KEY = 'up-destroyer'; + + /** + Binds an event handler to the document, which will be executed whenever the + given event is triggered on the given selector: + + up.on('click', '.button', function(event, $element) { + console.log("Someone clicked the button %o", $element); + }); + + This is roughly equivalent to binding a jQuery element to `document`. + + + \#\#\#\# Attaching structured data + + In case you want to attach structured data to the event you're observing, + you can serialize the data to JSON and put it into an `[up-data]` attribute: + + <span class="person" up-data="{ age: 18, name: 'Bob' }">Bob</span> + <span class="person" up-data="{ age: 22, name: 'Jim' }">Jim</span> + + The JSON will parsed and handed to your event handler as a third argument: + + up.on('click', '.person', function(event, $element, data) { + console.log("This is %o who is %o years old", data.name, data.age); + }); + + + \#\#\#\# Migrating jQuery event handlers to `up.on` + + Within the event handler, Up.js will bind `this` to the + native DOM element to help you migrate your existing jQuery code to + this new syntax. + + So if you had this before: + + $(document).on('click', '.button', function() { + $(this).something(); + }); + + ... you can simply copy the event handler to `up.on`: + + up.on('click', '.button', function() { + $(this).something(); + }); + + + @method up.on + @param {String} events + A space-separated list of event names to bind. + @param {String} selector + The selector an on which the event must be triggered. + @param {Function(event, $element, data)} behavior + The handler that should be called. + The function takes the affected element as the first argument (as a jQuery object). + If the element has an `up-data` attribute, its value is parsed as JSON + and passed as a second argument. + */ + liveDescriptions = []; + defaultLiveDescriptions = null; + live = function(events, selector, behavior) { + var description, ref; + if (!up.browser.isSupported()) { + return; + } + description = [ + events, selector, function(event) { + return behavior.apply(this, [event, $(this), data(this)]); + } + ]; + liveDescriptions.push(description); + return (ref = $(document)).on.apply(ref, description); + }; + + /** + Registers a function to be called whenever an element with + the given selector is inserted into the DOM through Up.js. + + This is a great way to integrate jQuery plugins. + Let's say your Javascript plugin wants you to call `lightboxify()` + on links that should open a lightbox. You decide to + do this for all links with an `[rel=lightbox]` attribute: + + <a href="river.png" rel="lightbox">River</a> + <a href="ocean.png" rel="lightbox">Ocean</a> + + This Javascript will do exactly that: + + up.compiler('a[rel=lightbox]', function($element) { + $element.lightboxify(); + }); + + Note that within the compiler, Up.js will bind `this` to the + native DOM element to help you migrate your existing jQuery code to + this new syntax. + + + \#\#\#\# Custom elements + + You can also use `up.compiler` to implement custom elements like this: + + <clock></clock> + + Here is the Javascript that inserts the current time into to these elements: + + up.compiler('clock', function($element) { + var now = new Date(); + $element.text(now.toString())); + }); + + + \#\#\#\# Cleaning up after yourself + + If your compiler returns a function, Up.js will use this as a *destructor* to + clean up if the element leaves the DOM. Note that in Up.js the same DOM ad Javascript environment + will persist through many page loads, so it's important to not create + [memory leaks](https://makandracards.com/makandra/31325-how-to-create-memory-leaks-in-jquery). + + You should clean up after yourself whenever your compilers have global + side effects, like a [`setInterval`](https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setInterval) + or event handlers bound to the document root. + + Here is a version of `<clock>` that updates + the time every second, and cleans up once it's done: + + up.compiler('clock', function($element) { + + function update() { + var now = new Date(); + $element.text(now.toString())); + } + + setInterval(update, 1000); + + return function() { + clearInterval(update); + }; + + }); + + If we didn't clean up after ourselves, we would have many ticking intervals + operating on detached DOM elements after we have created and removed a couple + of `<clock>` elements. + + + \#\#\#\# Attaching structured data + + In case you want to attach structured data to the event you're observing, + you can serialize the data to JSON and put it into an `[up-data]` attribute. + For instance, a container for a [Google Map](https://developers.google.com/maps/documentation/javascript/tutorial) + might attach the location and names of its marker pins: + + <div class="google-map" up-data="[ + { lat: 48.36, lng: 10.99, title: 'Friedberg' }, + { lat: 48.75, lng: 11.45, title: 'Ingolstadt' } + ]"></div> + + The JSON will parsed and handed to your event handler as a second argument: + + up.compiler('.google-map', function($element, pins) { + + var map = new google.maps.Map($element); + + pins.forEach(function(pin) { + var position = new google.maps.LatLng(pin.lat, pin.lng); + new google.maps.Marker({ + position: position, + map: map, + title: pin.title + }); + }); + + }); + + + \#\#\#\# Migrating jQuery event handlers to `up.on` + + Within the compiler, Up.js will bind `this` to the + native DOM element to help you migrate your existing jQuery code to + this new syntax. + + + @method up.compiler + @param {String} selector + The selector to match. + @param {Boolean} [options.batch=false] + If set to `true` and a fragment insertion contains multiple + elements matching the selector, `compiler` is only called once + with a jQuery collection containing all matching elements. + @param {Function($element, data)} compiler + The function to call when a matching element is inserted. + The function takes the new element as the first argument (as a jQuery object). + If the element has an `up-data` attribute, its value is parsed as JSON + and passed as a second argument. + + The function may return a destructor function that destroys the compiled + object before it is removed from the DOM. The destructor is supposed to + clear global state such as time-outs and event handlers bound to the document. + The destructor is *not* expected to remove the element from the DOM, which + is already handled by [`up.destroy`](/up.flow#up.destroy). + */ + compilers = []; + defaultCompilers = null; + compiler = function() { + var args, options, selector; + selector = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : []; + if (!up.browser.isSupported()) { + return; + } + compiler = args.pop(); + options = u.options(args[0], { + batch: false + }); + return compilers.push({ + selector: selector, + callback: compiler, + batch: options.batch + }); + }; + applyCompiler = function(compiler, $jqueryElement, nativeElement) { + var destroyer; + u.debug("Applying compiler %o on %o", compiler.selector, nativeElement); + destroyer = compiler.callback.apply(nativeElement, [$jqueryElement, data($jqueryElement)]); + if (u.isFunction(destroyer)) { + $jqueryElement.addClass(DESTROYABLE_CLASS); + return $jqueryElement.data(DESTROYER_KEY, destroyer); + } + }; + compile = function($fragment) { + var $matches, i, len, results; + u.debug("Compiling fragment %o", $fragment); + results = []; + for (i = 0, len = compilers.length; i < len; i++) { + compiler = compilers[i]; + $matches = u.findWithSelf($fragment, compiler.selector); + if ($matches.length) { + if (compiler.batch) { + results.push(applyCompiler(compiler, $matches, $matches.get())); + } else { + results.push($matches.each(function() { + return applyCompiler(compiler, $(this), this); + })); + } + } else { + results.push(void 0); + } + } + return results; + }; + destroy = function($fragment) { + return u.findWithSelf($fragment, "." + DESTROYABLE_CLASS).each(function() { + var $element, destroyer; + $element = $(this); + destroyer = $element.data(DESTROYER_KEY); + return destroyer(); + }); + }; + + /** + Checks if the given element has an `up-data` attribute. + If yes, parses the attribute value as JSON and returns the parsed object. + + Returns an empty object if the element has no `up-data` attribute. + + The API of this method is likely to change in the future, so + we can support getting or setting individual keys. + + @protected + @method up.magic.data + @param {String|Element|jQuery} elementOrSelector + */ + + /* + Stores a JSON-string with the element. + + If an element annotated with [`up-data`] is inserted into the DOM, + Up will parse the JSON and pass the resulting object to any matching + [`up.compiler`](/up.magic#up.magic.compiler) handlers. + + Similarly, when an event is triggered on an element annotated with + [`up-data`], the parsed object will be passed to any matching + [`up.on`](/up.magic#up.on) handlers. + + @ujs + @method [up-data] + @param {JSON} [up-data] + */ + data = function(elementOrSelector) { + var $element, json; + $element = $(elementOrSelector); + json = $element.attr('up-data'); + if (u.isString(json) && u.trim(json) !== '') { + return JSON.parse(json); + } else { + return {}; + } + }; + + /** + Makes a snapshot of the currently registered event listeners, + to later be restored through [`up.bus.reset`](/up.bus#up.bus.reset). + + @private + @method up.magic.snapshot + */ + snapshot = function() { + defaultLiveDescriptions = u.copy(liveDescriptions); + return defaultCompilers = u.copy(compilers); + }; + + /** + Resets the list of registered event listeners to the + moment when the framework was booted. + + @private + @method up.magic.reset + */ + reset = function() { + var description, i, len, ref; + for (i = 0, len = liveDescriptions.length; i < len; i++) { + description = liveDescriptions[i]; + if (!u.contains(defaultLiveDescriptions, description)) { + (ref = $(document)).off.apply(ref, description); + } + } + liveDescriptions = u.copy(defaultLiveDescriptions); + return compilers = u.copy(defaultCompilers); + }; + + /** + Sends a notification that the given element has been inserted + into the DOM. This causes Up.js to compile the fragment (apply + event listeners, etc.). + + This method is called automatically if you change elements through + other Up.js methods. You will only need to call this if you + manipulate the DOM without going through Up.js. + + @method up.ready + @param {String|Element|jQuery} selectorOrFragment + */ + ready = function(selectorOrFragment) { + var $fragment; + $fragment = $(selectorOrFragment); + up.bus.emit('fragment:ready', $fragment); + return $fragment; + }; + onEscape = function(handler) { + return live('keydown', 'body', function(event) { + if (u.escapePressed(event)) { + return handler(event); + } + }); + }; + up.bus.on('app:ready', (function() { + return ready(document.body); + })); + up.bus.on('fragment:ready', compile); + up.bus.on('fragment:destroy', destroy); + up.bus.on('framework:ready', snapshot); + up.bus.on('framework:reset', reset); + return { + compiler: compiler, + on: live, + ready: ready, + onEscape: onEscape, + data: data + }; + })(); + + up.compiler = up.magic.compiler; + + up.on = up.magic.on; + + up.ready = up.magic.ready; + + up.awaken = function() { + var args; + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + up.util.warn("up.awaken has been renamed to up.compiler and will be removed in a future version"); + return up.compiler.apply(up, args); + }; + +}).call(this); + +/** +Manipulating the browser history +======= + +\#\#\# Incomplete documentation! + +We need to work on this page: + +- Explain how the other modules manipulate history +- Decide whether we want to expose these methods as public API +- Document methods and parameters + +@class up.history + */ + +(function() { + up.history = (function() { + var buildState, config, currentUrl, isCurrentUrl, manipulate, nextPreviousUrl, normalizeUrl, observeNewUrl, pop, previousUrl, push, register, replace, reset, restoreStateOnPop, u; + u = up.util; + + /** + @method up.history.defaults + @param {Array<String>} [options.popTargets=['body']] + An array of CSS selectors to replace when the user goes + back in history. + @param {Boolean} [options.restoreScroll=true] + Whether to restore the known scroll positions + when the user goes back or forward in history. + */ + config = u.config({ + popTargets: ['body'], + restoreScroll: true + }); + + /** + Returns the previous URL in the browser history. + + Note that this will only work reliably for history changes that + were applied by [`up.history.push`](#up.history.replace) or + [`up.history.replace`](#up.history.replace). + + @method up.history.previousUrl + @protected + */ + previousUrl = void 0; + nextPreviousUrl = void 0; + reset = function() { + config.reset(); + previousUrl = void 0; + return nextPreviousUrl = void 0; + }; + normalizeUrl = function(url) { + return u.normalizeUrl(url, { + hash: true + }); + }; + + /** + Returns a normalized URL for the current history entry. + + @method up.history.url + @protected + */ + currentUrl = function() { + return normalizeUrl(up.browser.url()); + }; + isCurrentUrl = function(url) { + return normalizeUrl(url) === currentUrl(); + }; + observeNewUrl = function(url) { + console.log("observing new url %o", url); + if (nextPreviousUrl) { + previousUrl = nextPreviousUrl; + nextPreviousUrl = void 0; + } + return nextPreviousUrl = url; + }; + + /** + @method up.history.replace + @param {String} url + @param {Boolean} [options.force=false] + @protected + */ + replace = function(url, options) { + return manipulate('replace', url, options); + }; + + /** + @method up.history.push + @param {String} url + @protected + */ + push = function(url, options) { + return manipulate('push', url, options); + }; + manipulate = function(method, url, options) { + var fullMethod, state; + options = u.options(options, { + force: false + }); + if (options.force || !isCurrentUrl(url)) { + if (up.browser.canPushState()) { + fullMethod = method + "State"; + state = buildState(); + u.debug("Changing history to URL %o (%o)", url, method); + window.history[fullMethod](state, '', url); + return observeNewUrl(currentUrl()); + } else { + return u.error("This browser doesn't support history.pushState"); + } + } + }; + buildState = function() { + return { + fromUp: true + }; + }; + restoreStateOnPop = function(state) { + var popSelector, url; + url = currentUrl(); + u.debug("Restoring state %o (now on " + url + ")", state); + popSelector = config.popTargets.join(', '); + return up.replace(popSelector, url, { + history: false, + reveal: false, + transition: 'none', + saveScroll: false, + restoreScroll: config.restoreScroll + }); + }; + pop = function(event) { + var state; + u.debug("History state popped to URL %o", currentUrl()); + observeNewUrl(currentUrl()); + up.layout.saveScroll({ + url: previousUrl + }); + state = event.originalEvent.state; + if (state != null ? state.fromUp : void 0) { + return restoreStateOnPop(state); + } else { + return u.debug('Discarding unknown state %o', state); + } + }; + if (up.browser.canPushState()) { + register = function() { + $(window).on("popstate", pop); + return replace(currentUrl(), { + force: true + }); + }; + if (typeof jasmine !== "undefined" && jasmine !== null) { + register(); + } else { + setTimeout(register, 100); + } + } + + /** + Changes the link's destination so it points to the previous URL. + + Note that this will *not* call `location.back()`, but will set + the link's `up-href` attribute to the actual, previous URL. + + \#\#\#\# Under the hood + + This link ... + + <a href="/default" up-back> + Go back + </a> + + ... will be transformed to: + + <a href="/default" up-href="/previous-page" up-restore-scroll up-follow> + Goback + </a> + + @ujs + @method [up-back] + */ + up.compiler('[up-back]', function($link) { + console.log("up-back", $link, previousUrl); + if (u.isPresent(previousUrl)) { + u.setMissingAttrs($link, { + 'up-href': previousUrl, + 'up-restore-scroll': '' + }); + $link.removeAttr('up-back'); + return up.link.makeFollowable($link); + } + }); + up.bus.on('framework:reset', reset); + return { + defaults: config.update, + push: push, + replace: replace, + url: currentUrl, + previousUrl: function() { + return previousUrl; + }, + normalizeUrl: normalizeUrl + }; + })(); + +}).call(this); + +/** Viewport scrolling ================== This modules contains functions to scroll the viewport and reveal contained elements. @@ -1182,34 +1954,52 @@ (function() { var slice = [].slice; up.layout = (function() { - var SCROLL_PROMISE_KEY, config, findViewport, finishScrolling, measureObstruction, reset, reveal, scroll, u; + var SCROLL_PROMISE_KEY, config, finishScrolling, lastScrollTops, measureObstruction, reset, restoreScroll, reveal, saveScroll, scroll, scrollTops, u, viewportOf, viewportSelector, viewports, viewportsIn; u = up.util; /** + Configures the application layout. - @method up.layout.defaults - @param {String} [options.viewport] - @param {String} [options.fixedTop] - @param {String} [options.fixedBottom] + @param {Array<String>} [options.viewports] + An array of CSS selectors that find viewports + (containers that scroll their contents). + @param {Array<String>} [options.fixedTop] + An array of CSS selectors that find elements fixed to the + top edge of the screen (using `position: fixed`). + @param {Array<String>} [options.fixedBottom] + An array of CSS selectors that find elements fixed to the + bottom edge of the screen (using `position: fixed`). @param {Number} [options.duration] + The duration of the scrolling animation in milliseconds. + Setting this to `0` will disable scrolling animations. @param {String} [options.easing] + The timing function that controls the animation's acceleration. + See [W3C documentation](http://www.w3.org/TR/css3-transitions/#transition-timing-function) + for a list of pre-defined timing functions. @param {Number} [options.snap] + When [revealing](#up.reveal) elements, Up.js will scroll an viewport + to the top when the revealed element is closer to the top than `options.snap`. */ config = u.config({ duration: 0, - viewport: 'body, .up-modal, [up-viewport]', - fixedTop: '[up-fixed~=top]', - fixedBottom: '[up-fixed~=bottom]', + viewports: ['body', '.up-modal', '[up-viewport]'], + fixedTop: ['[up-fixed~=top]'], + fixedBottom: ['[up-fixed~=bottom]'], snap: 50, easing: 'swing' }); + lastScrollTops = u.cache({ + size: 30, + key: up.history.normalizeUrl + }); reset = function() { - return config.reset(); + config.reset(); + return lastScrollTops.clear(); }; SCROLL_PROMISE_KEY = 'up-scroll-promise'; /** Scrolls the given viewport to the given Y-position. @@ -1248,36 +2038,36 @@ The timing function that controls the acceleration for the scrolling's animation. @return {Deferred} A promise that will be resolved when the scrolling ends. */ scroll = function(viewport, scrollTop, options) { - var $view, deferred, duration, easing, targetProps; - $view = $(viewport); + var $viewport, deferred, duration, easing, targetProps; + $viewport = $(viewport); options = u.options(options); duration = u.option(options.duration, config.duration); easing = u.option(options.easing, config.easing); - finishScrolling($view); + finishScrolling($viewport); if (duration > 0) { deferred = $.Deferred(); - $view.data(SCROLL_PROMISE_KEY, deferred); + $viewport.data(SCROLL_PROMISE_KEY, deferred); deferred.then(function() { - $view.removeData(SCROLL_PROMISE_KEY); - return $view.finish(); + $viewport.removeData(SCROLL_PROMISE_KEY); + return $viewport.finish(); }); targetProps = { scrollTop: scrollTop }; - $view.animate(targetProps, { + $viewport.animate(targetProps, { duration: duration, easing: easing, complete: function() { return deferred.resolve(); } }); return deferred; } else { - $view.scrollTop(scrollTop); + $viewport.scrollTop(scrollTop); return u.resolvedDeferred(); } }; /** @@ -1303,21 +2093,21 @@ } return parseInt(anchorPosition) + $obstructor.height(); }; fixedTopBottoms = (function() { var i, len, ref, results; - ref = $(config.fixedTop); + ref = $(config.fixedTop.join(', ')); results = []; for (i = 0, len = ref.length; i < len; i++) { obstructor = ref[i]; results.push(measurePosition(obstructor, 'top')); } return results; })(); fixedBottomTops = (function() { var i, len, ref, results; - ref = $(config.fixedBottom); + ref = $(config.fixedBottom.join(', ')); results = []; for (i = 0, len = ref.length; i < len; i++) { obstructor = ref[i]; results.push(measurePosition(obstructor, 'bottom')); } @@ -1368,11 +2158,11 @@ */ reveal = function(elementOrSelector, options) { var $element, $viewport, elementDims, firstElementRow, lastElementRow, newScrollPos, obstruction, offsetShift, originalScrollPos, predictFirstVisibleRow, predictLastVisibleRow, snap, viewportHeight, viewportIsBody; options = u.options(options); $element = $(elementOrSelector); - $viewport = findViewport($element, options.viewport); + $viewport = viewportOf($element, options.viewport); snap = u.option(options.snap, config.snap); viewportIsBody = $viewport.is('body'); viewportHeight = viewportIsBody ? u.clientSize().height : $viewport.height(); originalScrollPos = $viewport.scrollTop(); newScrollPos = originalScrollPos; @@ -1412,29 +2202,137 @@ return scroll($viewport, newScrollPos, options); } else { return u.resolvedDeferred(); } }; + viewportSelector = function() { + return config.viewports.join(', '); + }; /** - @private - @method up.viewport.findViewport + Returns the viewport for the given element. + + Throws an error if no viewport could be found. + + @protected + @method up.layout.viewportOf + @param {String|Element|jQuery} selectorOrElement */ - findViewport = function($element, viewportSelectorOrElement) { - var $viewport, vieportSelector; + viewportOf = function(selectorOrElement, viewportSelectorOrElement) { + var $element, $viewport, vieportSelector; + $element = $(selectorOrElement); $viewport = void 0; if (u.isJQuery(viewportSelectorOrElement)) { $viewport = viewportSelectorOrElement; } else { - vieportSelector = u.presence(viewportSelectorOrElement) || config.viewport; + vieportSelector = u.presence(viewportSelectorOrElement) || viewportSelector(); $viewport = $element.closest(vieportSelector); } $viewport.length || u.error("Could not find viewport for %o", $element); return $viewport; }; /** + Returns a jQuery collection of all the viewports contained within the + given selector or element. + + @protected + @method up.layout.viewportsIn + @param {String|Element|jQuery} selectorOrElement + @return jQuery + */ + viewportsIn = function(selectorOrElement) { + var $element; + $element = $(selectorOrElement); + return u.findWithSelf($element, viewportSelector()); + }; + + /** + Returns a jQuery collection of all the viewports on the screen. + + @protected + @method up.layout.viewports + */ + viewports = function() { + return $(viewportSelector()); + }; + + /** + Returns a hash with scroll positions. + + Each key in the hash is a viewport selector. The corresponding + value is the viewport's top scroll position: + + up.layout.scrollTops() + => { '.main': 0, '.sidebar': 73 } + + @protected + @method up.layout.scrollTops + @return Object<String, Number> + */ + scrollTops = function() { + var $viewport, i, len, ref, topsBySelector, viewport; + topsBySelector = {}; + ref = config.viewports; + for (i = 0, len = ref.length; i < len; i++) { + viewport = ref[i]; + $viewport = $(viewport); + if ($viewport.length) { + topsBySelector[viewport] = $viewport.scrollTop(); + } + } + return topsBySelector; + }; + + /** + Saves the top scroll positions of all the + viewports configured in `up.layout.defaults('viewports'). + The saved scroll positions can be restored by calling + [`up.layout.restoreScroll()`](#up.layout.restoreScroll). + + @method up.layout.saveScroll + @param {String} [options.url] + @param {Object<String, Number>} [options.tops] + @protected + */ + saveScroll = function(options) { + var tops, url; + if (options == null) { + options = {}; + } + url = u.option(options.url, up.history.url()); + tops = u.option(options.tops, scrollTops()); + return lastScrollTops.set(url, tops); + }; + + /** + Restores the top scroll positions of all the + viewports configured in `up.layout.defaults('viewports')`. + + @method up.layout.restoreScroll + @param {String} [options.within] + @protected + */ + restoreScroll = function(options) { + var $matchingViewport, $viewports, results, scrollTop, selector, tops; + if (options == null) { + options = {}; + } + $viewports = options.within ? viewportsIn(options.within) : viewports(); + tops = lastScrollTops.get(up.history.url()); + results = []; + for (selector in tops) { + scrollTop = tops[selector]; + $matchingViewport = $viewports.filter(selector); + results.push(up.scroll($matchingViewport, scrollTop, { + duration: 0 + })); + } + return results; + }; + + /** Marks this element as a scrolling container. Apply this ttribute if your app uses a custom panel layout with fixed positioning instead of scrolling `<body>`. [`up.reveal`](/up.reveal) will always try to scroll the viewport closest to the element that is being revealed. By default this is the `<body>` element. @@ -1514,11 +2412,17 @@ up.bus.on('framework:reset', reset); return { reveal: reveal, scroll: scroll, finishScrolling: finishScrolling, - defaults: config.update + defaults: config.update, + viewportOf: viewportOf, + viewportsIn: viewportsIn, + viewports: viewports, + scrollTops: scrollTops, + saveScroll: saveScroll, + restoreScroll: restoreScroll }; })(); up.scroll = up.layout.scroll; @@ -1580,26 +2484,32 @@ @param {String|Boolean} [options.history=true] If a `String` is given, it is used as the URL the browser's location bar and history. If omitted or true, the `url` argument will be used. If set to `false`, the history will remain unchanged. @param {String|Boolean} [options.source=true] - @param {String} [options.scroll] + @param {String} [options.reveal] Up.js will try to [reveal](/up.layout#up.reveal) the element being updated, by scrolling its containing viewport. Set this option to `false` to prevent any scrolling. If omitted, this will use the [default from `up.layout`](/up.layout#up.layout.defaults). + @param {Boolean} [options.restoreScroll=`false`] + If set to true, Up.js will try to restore the scroll position + of all the viewports within the updated element. The position + will be reset to the last known top position before a previous + history change for the current URL. @param {Boolean} [options.cache] Whether to use a [cached response](/up.proxy) if available. @param {String} [options.historyMethod='push'] @return {Promise} A promise that will be resolved when the page has been updated. */ replace = function(selectorOrElement, url, options) { var promise, request, selector; + u.debug("Replace %o with %o", selectorOrElement, url); options = u.options(options); selector = u.presence(selectorOrElement) ? selectorOrElement : u.createSelectorFromElement($(selectorOrElement)); - if (!up.browser.canPushState() && !u.castsToFalse(options.history)) { + if (!up.browser.canPushState() && options.history !== false) { if (!options.preload) { up.browser.loadPage(url, u.only(options, 'method')); } return u.resolvedPromise(); } @@ -1621,14 +2531,14 @@ selector: selector }; up.proxy.alias(request, newRequest); url = currentLocation; } - if (u.isMissing(options.history) || u.castsToTrue(options.history)) { + if (options.history !== false) { options.history = url; } - if (u.isMissing(options.source) || u.castsToTrue(options.source)) { + if (options.source !== false) { options.source = url; } if (!options.preload) { return implant(selector, html, options); } @@ -1651,31 +2561,24 @@ @method up.flow.implant @protected @param {String} selector @param {String} html - @param {String} [options.title] - @param {String} [options.source] - @param {Object} [options.transition] - @param {String} [options.scroll='body'] - @param {String} [options.history] - @param {String} [options.historyMethod='push'] + @param {Object} [options] + See options for [`up.replace`](#up.replace). */ implant = function(selector, html, options) { var $new, $old, j, len, ref, response, results, step; options = u.options(options, { historyMethod: 'push' }); - if (u.castsToFalse(options.history)) { - options.history = null; - } - if (u.castsToFalse(options.scroll)) { - options.scroll = false; - } options.source = u.option(options.source, options.history); response = parseResponse(html); options.title || (options.title = response.title()); + if (options.saveScroll !== false) { + up.layout.saveScroll(); + } ref = parseImplantSteps(selector, options); results = []; for (j = 0, len = ref.length; j < len; j++) { step = ref[j]; $old = findOldFragment(step.selector); @@ -1712,16 +2615,12 @@ } } }; }; reveal = function($element, options) { - var viewport; - viewport = options.scroll; - if (viewport !== false) { - return up.reveal($element, { - viewport: viewport - }); + if (options.reveal !== false) { + return up.reveal($element); } else { return u.resolvedDeferred(); } }; elementsInserted = function($new, options) { @@ -1732,11 +2631,18 @@ if (options.title) { document.title = options.title; } up.history[options.historyMethod](options.history); } - setSource($new, options.source); + if (options.source !== false) { + setSource($new, options.source); + } + if (options.restoreScroll) { + up.layout.restoreScroll({ + within: $new + }); + } autofocus($new); return up.ready($new); }; swapElements = function($old, $new, pseudoClass, transition, options) { var $wrapper, insertionMethod; @@ -1928,508 +2834,10 @@ up.first = up.flow.first; }).call(this); /** -Registering behavior and custom elements -======================================== - -Up.js keeps a persistent Javascript environment during page transitions. -To prevent memory leaks it is important to cleanly set up and tear down -event handlers and custom elements. - -\#\#\# Incomplete documentation! - -We need to work on this page: - -- Better class-level introduction for this module - -@class up.magic - */ - -(function() { - var slice = [].slice; - - up.magic = (function() { - var DESTROYABLE_CLASS, DESTROYER_KEY, applyCompiler, compile, compiler, compilers, data, defaultCompilers, defaultLiveDescriptions, destroy, live, liveDescriptions, onEscape, ready, reset, snapshot, u; - u = up.util; - DESTROYABLE_CLASS = 'up-destroyable'; - DESTROYER_KEY = 'up-destroyer'; - - /** - Binds an event handler to the document, which will be executed whenever the - given event is triggered on the given selector: - - up.on('click', '.button', function(event, $element) { - console.log("Someone clicked the button %o", $element); - }); - - This is roughly equivalent to binding a jQuery element to `document`. - - - \#\#\#\# Attaching structured data - - In case you want to attach structured data to the event you're observing, - you can serialize the data to JSON and put it into an `[up-data]` attribute: - - <span class="person" up-data="{ age: 18, name: 'Bob' }">Bob</span> - <span class="person" up-data="{ age: 22, name: 'Jim' }">Jim</span> - - The JSON will parsed and handed to your event handler as a third argument: - - up.on('click', '.person', function(event, $element, data) { - console.log("This is %o who is %o years old", data.name, data.age); - }); - - - \#\#\#\# Migrating jQuery event handlers to `up.on` - - Within the event handler, Up.js will bind `this` to the - native DOM element to help you migrate your existing jQuery code to - this new syntax. - - So if you had this before: - - $(document).on('click', '.button', function() { - $(this).something(); - }); - - ... you can simply copy the event handler to `up.on`: - - up.on('click', '.button', function() { - $(this).something(); - }); - - - @method up.on - @param {String} events - A space-separated list of event names to bind. - @param {String} selector - The selector an on which the event must be triggered. - @param {Function(event, $element, data)} behavior - The handler that should be called. - The function takes the affected element as the first argument (as a jQuery object). - If the element has an `up-data` attribute, its value is parsed as JSON - and passed as a second argument. - */ - liveDescriptions = []; - defaultLiveDescriptions = null; - live = function(events, selector, behavior) { - var description, ref; - if (!up.browser.isSupported()) { - return; - } - description = [ - events, selector, function(event) { - return behavior.apply(this, [event, $(this), data(this)]); - } - ]; - liveDescriptions.push(description); - return (ref = $(document)).on.apply(ref, description); - }; - - /** - Registers a function to be called whenever an element with - the given selector is inserted into the DOM through Up.js. - - This is a great way to integrate jQuery plugins. - Let's say your Javascript plugin wants you to call `lightboxify()` - on links that should open a lightbox. You decide to - do this for all links with an `[rel=lightbox]` attribute: - - <a href="river.png" rel="lightbox">River</a> - <a href="ocean.png" rel="lightbox">Ocean</a> - - This Javascript will do exactly that: - - up.compiler('a[rel=lightbox]', function($element) { - $element.lightboxify(); - }); - - Note that within the compiler, Up.js will bind `this` to the - native DOM element to help you migrate your existing jQuery code to - this new syntax. - - - \#\#\#\# Custom elements - - You can also use `up.compiler` to implement custom elements like this: - - <clock></clock> - - Here is the Javascript that inserts the current time into to these elements: - - up.compiler('clock', function($element) { - var now = new Date(); - $element.text(now.toString())); - }); - - - \#\#\#\# Cleaning up after yourself - - If your compiler returns a function, Up.js will use this as a *destructor* to - clean up if the element leaves the DOM. Note that in Up.js the same DOM ad Javascript environment - will persist through many page loads, so it's important to not create - [memory leaks](https://makandracards.com/makandra/31325-how-to-create-memory-leaks-in-jquery). - - You should clean up after yourself whenever your compilers have global - side effects, like a [`setInterval`](https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setInterval) - or event handlers bound to the document root. - - Here is a version of `<clock>` that updates - the time every second, and cleans up once it's done: - - up.compiler('clock', function($element) { - - function update() { - var now = new Date(); - $element.text(now.toString())); - } - - setInterval(update, 1000); - - return function() { - clearInterval(update); - }; - - }); - - If we didn't clean up after ourselves, we would have many ticking intervals - operating on detached DOM elements after we have created and removed a couple - of `<clock>` elements. - - - \#\#\#\# Attaching structured data - - In case you want to attach structured data to the event you're observing, - you can serialize the data to JSON and put it into an `[up-data]` attribute. - For instance, a container for a [Google Map](https://developers.google.com/maps/documentation/javascript/tutorial) - might attach the location and names of its marker pins: - - <div class="google-map" up-data="[ - { lat: 48.36, lng: 10.99, title: 'Friedberg' }, - { lat: 48.75, lng: 11.45, title: 'Ingolstadt' } - ]"></div> - - The JSON will parsed and handed to your event handler as a second argument: - - up.compiler('.google-map', function($element, pins) { - - var map = new google.maps.Map($element); - - pins.forEach(function(pin) { - var position = new google.maps.LatLng(pin.lat, pin.lng); - new google.maps.Marker({ - position: position, - map: map, - title: pin.title - }); - }); - - }); - - - \#\#\#\# Migrating jQuery event handlers to `up.on` - - Within the compiler, Up.js will bind `this` to the - native DOM element to help you migrate your existing jQuery code to - this new syntax. - - - @method up.compiler - @param {String} selector - The selector to match. - @param {Boolean} [options.batch=false] - If set to `true` and a fragment insertion contains multiple - elements matching the selector, `compiler` is only called once - with a jQuery collection containing all matching elements. - @param {Function($element, data)} compiler - The function to call when a matching element is inserted. - The function takes the new element as the first argument (as a jQuery object). - If the element has an `up-data` attribute, its value is parsed as JSON - and passed as a second argument. - - The function may return a destructor function that destroys the compiled - object before it is removed from the DOM. The destructor is supposed to - clear global state such as time-outs and event handlers bound to the document. - The destructor is *not* expected to remove the element from the DOM, which - is already handled by [`up.destroy`](/up.flow#up.destroy). - */ - compilers = []; - defaultCompilers = null; - compiler = function() { - var args, options, selector; - selector = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : []; - if (!up.browser.isSupported()) { - return; - } - compiler = args.pop(); - options = u.options(args[0], { - batch: false - }); - return compilers.push({ - selector: selector, - callback: compiler, - batch: options.batch - }); - }; - applyCompiler = function(compiler, $jqueryElement, nativeElement) { - var destroyer; - u.debug("Applying compiler %o on %o", compiler.selector, nativeElement); - destroyer = compiler.callback.apply(nativeElement, [$jqueryElement, data($jqueryElement)]); - if (u.isFunction(destroyer)) { - $jqueryElement.addClass(DESTROYABLE_CLASS); - return $jqueryElement.data(DESTROYER_KEY, destroyer); - } - }; - compile = function($fragment) { - var $matches, i, len, results; - u.debug("Compiling fragment %o", $fragment); - results = []; - for (i = 0, len = compilers.length; i < len; i++) { - compiler = compilers[i]; - $matches = u.findWithSelf($fragment, compiler.selector); - if ($matches.length) { - if (compiler.batch) { - results.push(applyCompiler(compiler, $matches, $matches.get())); - } else { - results.push($matches.each(function() { - return applyCompiler(compiler, $(this), this); - })); - } - } else { - results.push(void 0); - } - } - return results; - }; - destroy = function($fragment) { - return u.findWithSelf($fragment, "." + DESTROYABLE_CLASS).each(function() { - var $element, destroyer; - $element = $(this); - destroyer = $element.data(DESTROYER_KEY); - return destroyer(); - }); - }; - - /** - Checks if the given element has an `up-data` attribute. - If yes, parses the attribute value as JSON and returns the parsed object. - - Returns an empty object if the element has no `up-data` attribute. - - The API of this method is likely to change in the future, so - we can support getting or setting individual keys. - - @protected - @method up.magic.data - @param {String|Element|jQuery} elementOrSelector - */ - - /* - Stores a JSON-string with the element. - - If an element annotated with [`up-data`] is inserted into the DOM, - Up will parse the JSON and pass the resulting object to any matching - [`up.compiler`](/up.magic#up.magic.compiler) handlers. - - Similarly, when an event is triggered on an element annotated with - [`up-data`], the parsed object will be passed to any matching - [`up.on`](/up.magic#up.on) handlers. - - @ujs - @method [up-data] - @param {JSON} [up-data] - */ - data = function(elementOrSelector) { - var $element, json; - $element = $(elementOrSelector); - json = $element.attr('up-data'); - if (u.isString(json) && u.trim(json) !== '') { - return JSON.parse(json); - } else { - return {}; - } - }; - - /** - Makes a snapshot of the currently registered event listeners, - to later be restored through [`up.bus.reset`](/up.bus#up.bus.reset). - - @private - @method up.magic.snapshot - */ - snapshot = function() { - defaultLiveDescriptions = u.copy(liveDescriptions); - return defaultCompilers = u.copy(compilers); - }; - - /** - Resets the list of registered event listeners to the - moment when the framework was booted. - - @private - @method up.magic.reset - */ - reset = function() { - var description, i, len, ref; - for (i = 0, len = liveDescriptions.length; i < len; i++) { - description = liveDescriptions[i]; - if (!u.contains(defaultLiveDescriptions, description)) { - (ref = $(document)).off.apply(ref, description); - } - } - liveDescriptions = u.copy(defaultLiveDescriptions); - return compilers = u.copy(defaultCompilers); - }; - - /** - Sends a notification that the given element has been inserted - into the DOM. This causes Up.js to compile the fragment (apply - event listeners, etc.). - - This method is called automatically if you change elements through - other Up.js methods. You will only need to call this if you - manipulate the DOM without going through Up.js. - - @method up.ready - @param {String|Element|jQuery} selectorOrFragment - */ - ready = function(selectorOrFragment) { - var $fragment; - $fragment = $(selectorOrFragment); - up.bus.emit('fragment:ready', $fragment); - return $fragment; - }; - onEscape = function(handler) { - return live('keydown', 'body', function(event) { - if (u.escapePressed(event)) { - return handler(event); - } - }); - }; - up.bus.on('app:ready', (function() { - return ready(document.body); - })); - up.bus.on('fragment:ready', compile); - up.bus.on('fragment:destroy', destroy); - up.bus.on('framework:ready', snapshot); - up.bus.on('framework:reset', reset); - return { - compiler: compiler, - on: live, - ready: ready, - onEscape: onEscape, - data: data - }; - })(); - - up.compiler = up.magic.compiler; - - up.on = up.magic.on; - - up.ready = up.magic.ready; - - up.awaken = function() { - var args; - args = 1 <= arguments.length ? slice.call(arguments, 0) : []; - up.util.warn("up.awaken has been renamed to up.compiler and will be removed in a future version"); - return up.compiler.apply(up, args); - }; - -}).call(this); - -/** -Manipulating the browser history -======= - -\#\#\# Incomplete documentation! - -We need to work on this page: - -- Explain how the other modules manipulate history -- Decide whether we want to expose these methods as public API -- Document methods and parameters - -@class up.history - */ - -(function() { - up.history = (function() { - var isCurrentUrl, manipulate, pop, push, replace, u; - u = up.util; - isCurrentUrl = function(url) { - return u.normalizeUrl(url, { - hash: true - }) === u.normalizeUrl(up.browser.url(), { - hash: true - }); - }; - - /** - @method up.history.replace - @param {String} url - @protected - */ - replace = function(url, options) { - options = u.options(options, { - force: false - }); - if (options.force || !isCurrentUrl(url)) { - return manipulate("replace", url); - } - }; - - /** - @method up.history.push - @param {String} url - @protected - */ - push = function(url) { - if (!isCurrentUrl(url)) { - return manipulate("push", url); - } - }; - manipulate = function(method, url) { - if (up.browser.canPushState()) { - method += "State"; - return window.history[method]({ - fromUp: true - }, '', url); - } else { - return u.error("This browser doesn't support history.pushState"); - } - }; - pop = function(event) { - var state; - state = event.originalEvent.state; - if (state != null ? state.fromUp : void 0) { - u.debug("Restoring state %o (now on " + (up.browser.url()) + ")", state); - return up.visit(up.browser.url(), { - historyMethod: 'replace' - }); - } else { - return u.debug('Discarding unknown state %o', state); - } - }; - if (up.browser.canPushState()) { - setTimeout((function() { - $(window).on("popstate", pop); - return replace(up.browser.url(), { - force: true - }); - }), 200); - } - return { - push: push, - replace: replace - }; - })(); - -}).call(this); - -/** Animation and transitions ========================= Any fragment change in Up.js can be animated. @@ -2553,10 +2961,13 @@ animate = function(elementOrSelector, animation, options) { var $element; $element = $(elementOrSelector); finish($element); options = animateOptions(options); + if (animation === 'none' || animation === false) { + none(); + } if (u.isFunction(animation)) { return assertIsDeferred(animation($element, options), animation); } else if (u.isString(animation)) { return animate($element, findAnimation(animation), options); } else if (u.isHash(animation)) { @@ -2709,11 +3120,11 @@ options = animateOptions(options); $old = $(source); $new = $(target); finish($old); finish($new); - if (transitionOrName === 'none') { + if (transitionOrName === 'none' || transitionOrName === false) { return none(); } else if (transition = u.presence(transitionOrName, u.isFunction) || transitions[transitionOrName]) { return withGhosts($old, $new, function($oldGhost, $newGhost) { return assertIsDeferred(transition($oldGhost, $newGhost, options), transitionOrName); }); @@ -3078,13 +3489,12 @@ @class up.proxy */ (function() { up.proxy = (function() { - var $waitingLink, SAFE_HTTP_METHODS, ajax, alias, busy, busyDelayTimer, busyEventEmitted, cache, cacheKey, cancelBusyDelay, cancelPreloadDelay, checkPreload, clear, config, get, idle, isFresh, isIdempotent, load, loadEnded, loadStarted, normalizeRequest, pendingCount, preload, preloadDelayTimer, remove, reset, set, startPreloadDelay, timestamp, trim, u; + var $waitingLink, SAFE_HTTP_METHODS, ajax, alias, busy, busyDelayTimer, busyEventEmitted, cache, cacheKey, cancelBusyDelay, cancelPreloadDelay, checkPreload, clear, config, get, idle, isIdempotent, load, loadEnded, loadStarted, normalizeRequest, pendingCount, preload, preloadDelayTimer, remove, reset, set, startPreloadDelay, u; u = up.util; - cache = void 0; $waitingLink = void 0; preloadDelayTimer = void 0; busyDelayTimer = void 0; pendingCount = void 0; busyEventEmitted = void 0; @@ -3108,55 +3518,67 @@ busyDelay: 300, preloadDelay: 75, cacheSize: 70, cacheExpiry: 1000 * 60 * 5 }); + cacheKey = function(request) { + normalizeRequest(request); + return [request.url, request.method, request.data, request.selector].join('|'); + }; + cache = u.cache({ + size: function() { + return config.cacheSize; + }, + expiry: function() { + return config.cacheExpiry; + }, + key: cacheKey, + log: 'up.proxy' + }); + + /** + @protected + @method up.proxy.get + */ + get = cache.get; + + /** + @protected + @method up.proxy.set + */ + set = cache.set; + + /** + @protected + @method up.proxy.remove + */ + remove = cache.remove; + + /** + @protected + @method up.proxy.clear + */ + clear = cache.clear; cancelPreloadDelay = function() { clearTimeout(preloadDelayTimer); return preloadDelayTimer = null; }; cancelBusyDelay = function() { clearTimeout(busyDelayTimer); return busyDelayTimer = null; }; reset = function() { - cache = {}; $waitingLink = null; cancelPreloadDelay(); cancelBusyDelay(); pendingCount = 0; config.reset(); - return busyEventEmitted = false; + busyEventEmitted = false; + return cache.clear(); }; reset(); - cacheKey = function(request) { - normalizeRequest(request); - return [request.url, request.method, request.data, request.selector].join('|'); - }; - trim = function() { - var keys, oldestKey, oldestTimestamp; - keys = u.keys(cache); - if (keys.length > config.cacheSize) { - oldestKey = null; - oldestTimestamp = null; - u.each(keys, function(key) { - var promise, timestamp; - promise = cache[key]; - timestamp = promise.timestamp; - if (!oldestTimestamp || oldestTimestamp > timestamp) { - oldestKey = key; - return oldestTimestamp = timestamp; - } - }); - if (oldestKey) { - return delete cache[oldestKey]; - } - } - }; - timestamp = function() { - return (new Date()).valueOf(); - }; + alias = cache.alias; normalizeRequest = function(request) { if (!request._normalized) { request.method = u.normalizeMethod(request.method); if (request.url) { request.url = u.normalizeUrl(request.url); @@ -3164,17 +3586,10 @@ request.selector || (request.selector = 'body'); request._normalized = true; } return request; }; - alias = function(oldRequest, newRequest) { - var promise; - u.debug("Aliasing %o to %o", oldRequest, newRequest); - if (promise = get(oldRequest)) { - return set(newRequest, promise); - } - }; /** Makes a request to the given URL and caches the response. If the response was already cached, returns the HTML instantly. @@ -3196,12 +3611,12 @@ 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, pending, promise, request; - forceCache = u.castsToTrue(options.cache); - ignoreCache = u.castsToFalse(options.cache); + forceCache = options.cache === true; + ignoreCache = options.cache === false; request = u.only(options, 'url', 'method', 'data', 'selector', '_normalized'); pending = true; if (!isIdempotent(request) && !forceCache) { clear(); promise = load(request); @@ -3282,68 +3697,10 @@ }; isIdempotent = function(request) { normalizeRequest(request); return u.contains(SAFE_HTTP_METHODS, request.method); }; - isFresh = function(promise) { - var timeSinceTouch; - timeSinceTouch = timestamp() - promise.timestamp; - return timeSinceTouch < config.cacheExpiry; - }; - - /** - @protected - @method up.proxy.get - */ - get = function(request) { - var key, promise; - key = cacheKey(request); - if (promise = cache[key]) { - if (!isFresh(promise)) { - u.debug("Discarding stale cache entry for %o (%o)", request.url, request); - remove(request); - return void 0; - } else { - u.debug("Cache hit for %o (%o)", request.url, request); - return promise; - } - } else { - u.debug("Cache miss for %o (%o)", request.url, request); - return void 0; - } - }; - - /** - @protected - @method up.proxy.set - */ - set = function(request, promise) { - var key; - trim(); - key = cacheKey(request); - promise.timestamp = timestamp(); - cache[key] = promise; - return promise; - }; - - /** - @protected - @method up.proxy.remove - */ - remove = function(request) { - var key; - key = cacheKey(request); - return delete cache[key]; - }; - - /** - @protected - @method up.proxy.clear - */ - clear = function() { - return cache = {}; - }; checkPreload = function($link) { var curriedPreload, delay; delay = parseInt(u.presentAttr($link, 'up-delay')) || config.preloadDelay; if (!$link.is($waitingLink)) { $waitingLink = $link; @@ -3501,11 +3858,11 @@ @class up.link */ (function() { up.link = (function() { - var childClicked, follow, followMethod, shouldProcessLinkEvent, u, visit; + var childClicked, follow, followMethod, makeFollowable, shouldProcessLinkEvent, u, visit; u = up.util; /** Visits the given URL without a full page load. This is done by fetching `url` through an AJAX request @@ -3552,13 +3909,12 @@ The selector to replace. Defaults to the `up-target` attribute on `link`, or to `body` if such an attribute does not exist. @param {Function|String} [options.transition] A transition function or name. - @param {Element|jQuery|String} [options.scroll] - An element or selector that will be scrolled to the top in - case the replaced element is not visible in the viewport. + @param {Element|jQuery|String} [options.reveal] + Whether to reveal the followed element within its viewport. @param {Number} [options.duration] The duration of the transition. See [`up.morph`](/up.motion#up.morph). @param {Number} [options.delay] The delay before the transition starts. See [`up.morph`](/up.motion#up.morph). @param {String} [options.easing] @@ -3568,14 +3924,15 @@ var $link, selector, url; $link = $(link); options = u.options(options); url = u.option($link.attr('up-href'), $link.attr('href')); selector = u.option(options.target, $link.attr('up-target'), 'body'); - options.transition = u.option(options.transition, $link.attr('up-transition'), $link.attr('up-animation')); - options.history = u.option(options.history, $link.attr('up-history')); - options.scroll = u.option(options.scroll, $link.attr('up-scroll'), 'body'); - options.cache = u.option(options.cache, $link.attr('up-cache')); + options.transition = u.option(options.transition, u.castedAttr($link, 'up-transition'), u.castedAttr($link, 'up-animation')); + options.history = u.option(options.history, u.castedAttr($link, 'up-history')); + options.reveal = u.option(options.reveal, u.castedAttr($link, 'up-reveal')); + options.cache = u.option(options.cache, u.castedAttr($link, 'up-cache')); + options.restoreScroll = u.option(options.restoreScroll, u.castedAttr($link, 'up-restore-scroll')); options.method = followMethod($link, options); options = u.merge(options, up.motion.animateOptions(options, $link)); return up.replace(selector, url, options); }; @@ -3640,10 +3997,13 @@ @param {String} up-target The CSS selector to replace @param [up-href] The destination URL to follow. If omitted, the the link's `href` attribute will be used. + @param [up-restore-scroll='false'] + Whether to restore the scroll position of all viewports + within the target selector. */ up.on('click', 'a[up-target], [up-href][up-target]', function(event, $link) { if (shouldProcessLinkEvent(event, $link)) { if ($link.is('[up-instant]')) { return event.preventDefault(); @@ -3693,10 +4053,27 @@ shouldProcessLinkEvent = function(event, $link) { return u.isUnmodifiedMouseEvent(event) && !childClicked(event, $link); }; /** + Makes sure that the given link is handled by Up.js. + + This is done by giving the link an `up-follow` attribute + if it doesn't already have it an `up-target` or `up-follow` attribute. + + @method up.link.makeFollowable + @protected + */ + makeFollowable = function(link) { + var $link; + $link = $(link); + if (u.isMissing($link.attr('up-target')) && u.isMissing($link.attr('up-follow'))) { + return $link.attr('up-follow', ''); + } + }; + + /** If applied on a link, Follows this link via AJAX and replaces the current `<body>` element with the response's `<body>` element. Example: @@ -3718,10 +4095,13 @@ @method a[up-follow] @ujs @param [up-href] The destination URL to follow. If omitted, the the link's `href` attribute will be used. + @param [up-restore-scroll='false'] + Whether to restore the scroll position of all viewports + within the response. */ up.on('click', 'a[up-follow], [up-href][up-follow]', function(event, $link) { if (shouldProcessLinkEvent(event, $link)) { if ($link.is('[up-instant]')) { return event.preventDefault(); @@ -3748,14 +4128,14 @@ (`up-target`, `up-instant`, `up-preload`, etc.). @ujs @method [up-expand] */ - up.compiler('[up-expand]', function($fragment) { + up.compiler('[up-expand]', function($area) { var attribute, i, len, link, name, newAttrs, ref, upAttributePattern; - link = $fragment.find('a, [up-href]').get(0); - link || u.error('No link to expand within %o', $fragment); + link = $area.find('a, [up-href]').get(0); + link || u.error('No link to expand within %o', $area); upAttributePattern = /^up-/; newAttrs = {}; newAttrs['up-href'] = $(link).attr('href'); ref = link.attributes; for (i = 0, len = ref.length; i < len; i++) { @@ -3763,13 +4143,13 @@ name = attribute.name; if (name.match(upAttributePattern)) { newAttrs[name] = attribute.value; } } - u.isGiven(newAttrs['up-target']) || (newAttrs['up-follow'] = ''); - u.setMissingAttrs($fragment, newAttrs); - return $fragment.removeAttr('up-expand'); + u.setMissingAttrs($area, newAttrs); + $area.removeAttr('up-expand'); + return makeFollowable($area); }); /** Marks up the current link to be followed *as fast as possible*. This is done by: @@ -3789,16 +4169,16 @@ @method [up-dash] @ujs */ up.compiler('[up-dash]', function($element) { var newAttrs, target; - target = $element.attr('up-dash'); + target = u.castedAttr($element, 'up-dash'); newAttrs = { 'up-preload': 'true', 'up-instant': 'true' }; - if (u.isBlank(target) || u.castsToTrue(target)) { + if (target === true) { newAttrs['up-follow'] = ''; } else { newAttrs['up-target'] = target; } u.setMissingAttrs($element, newAttrs); @@ -3806,10 +4186,11 @@ }); return { knife: eval(typeof Knife !== "undefined" && Knife !== null ? Knife.point : void 0), visit: visit, follow: follow, + makeFollowable: makeFollowable, childClicked: childClicked, followMethod: followMethod }; })(); @@ -3905,19 +4286,19 @@ options = u.options(options); successSelector = u.option(options.target, $form.attr('up-target'), 'body'); failureSelector = u.option(options.failTarget, $form.attr('up-fail-target'), function() { return u.createSelectorFromElement($form); }); - historyOption = u.option(options.history, $form.attr('up-history'), true); - successTransition = u.option(options.transition, $form.attr('up-transition')); - failureTransition = u.option(options.failTransition, $form.attr('up-fail-transition'), successTransition); + historyOption = u.option(options.history, u.castedAttr($form, 'up-history'), true); + successTransition = u.option(options.transition, u.castedAttr($form, 'up-transition')); + failureTransition = u.option(options.failTransition, u.castedAttr($form, 'up-fail-transition'), successTransition); httpMethod = u.option(options.method, $form.attr('up-method'), $form.attr('data-method'), $form.attr('method'), 'post').toUpperCase(); animateOptions = up.motion.animateOptions(options, $form); - useCache = u.option(options.cache, $form.attr('up-cache')); + useCache = u.option(options.cache, u.castedAttr($form, 'up-cache')); url = u.option(options.url, $form.attr('action'), up.browser.url()); $form.addClass('up-active'); - if (!up.browser.canPushState() && !u.castsToFalse(historyOption)) { + if (!up.browser.canPushState() && historyOption !== false) { $form.get(0).submit(); return; } request = { url: url, @@ -3926,11 +4307,20 @@ selector: successSelector, cache: useCache }; successUrl = function(xhr) { var currentLocation; - url = historyOption ? u.castsToFalse(historyOption) ? false : u.isString(historyOption) ? historyOption : (currentLocation = u.locationFromXhr(xhr)) ? currentLocation : request.type === 'GET' ? request.url + '?' + request.data : void 0 : void 0; + url = void 0; + if (u.isGiven(historyOption)) { + if (historyOption === false || u.isString(historyOption)) { + url = historyOption; + } else if (currentLocation = u.locationFromXhr(xhr)) { + url = currentLocation; + } else if (request.type === 'GET') { + url = request.url + '?' + request.data; + } + } return u.option(url, false); }; return up.proxy.ajax(request).always(function() { return $form.removeClass('up-active'); }).done(function(html, textStatus, xhr) { @@ -4296,12 +4686,12 @@ options = u.options(options); url = u.option(options.url, $link.attr('href')); selector = u.option(options.target, $link.attr('up-popup'), 'body'); position = u.option(options.position, $link.attr('up-position'), config.position); animation = u.option(options.animation, $link.attr('up-animation'), config.openAnimation); - sticky = u.option(options.sticky, $link.is('[up-sticky]')); - history = up.browser.canPushState() ? u.option(options.history, $link.attr('up-history'), false) : false; + sticky = u.option(options.sticky, u.castedAttr($link, 'up-sticky')); + history = up.browser.canPushState() ? u.option(options.history, u.castedAttr($link, 'up-history'), false) : false; animateOptions = up.motion.animateOptions(options, $link); close(); $popup = createHiddenPopup($link, selector, sticky); return up.replace(selector, url, { history: history, @@ -4657,12 +5047,12 @@ selector = u.option(options.target, $link.attr('up-modal'), 'body'); width = u.option(options.width, $link.attr('up-width'), config.width); maxWidth = u.option(options.maxWidth, $link.attr('up-max-width'), config.maxWidth); height = u.option(options.height, $link.attr('up-height'), config.height); animation = u.option(options.animation, $link.attr('up-animation'), config.openAnimation); - sticky = u.option(options.sticky, $link.is('[up-sticky]')); - history = up.browser.canPushState() ? u.option(options.history, $link.attr('up-history'), true) : false; + sticky = u.option(options.sticky, u.castedAttr($link, 'up-sticky')); + history = up.browser.canPushState() ? u.option(options.history, u.castedAttr($link, 'up-history'), true) : false; animateOptions = up.motion.animateOptions(options, $link); close(); $modal = createHiddenModal({ selector: selector, width: width, @@ -4936,11 +5326,11 @@ options = {}; } $link = $(linkOrSelector); html = u.option(options.html, $link.attr('up-tooltip'), $link.attr('title')); position = u.option(options.position, $link.attr('up-position'), 'top'); - animation = u.option(options.animation, $link.attr('up-animation'), 'fade-in'); + animation = u.option(options.animation, u.castedAttr($link, 'up-animation'), 'fade-in'); animateOptions = up.motion.animateOptions(options, $link); close(); $tooltip = createElement(html); setPosition($link, $tooltip, position); return up.animate($tooltip, animation, animateOptions); @@ -5026,21 +5416,20 @@ @method up.navigation.defaults @param {Number} [options.currentClass] The class to set on [links that point the current location](#up-current). */ config = u.config({ - currentClass: 'up-current' + currentClasses: ['up-current'] }); reset = function() { return config.reset(); }; currentClass = function() { - var klass; - klass = config.currentClass; - if (!u.contains(klass, 'up-current')) { - klass += ' up-current'; - } - return klass; + var classes; + classes = config.currentClasses; + classes = classes.concat(['up-current']); + classes = u.uniq(classes); + return classes.join(' '); }; CLASS_ACTIVE = 'up-active'; SELECTORS_SECTION = ['a', '[up-href]', '[up-alias]']; SELECTOR_SECTION = SELECTORS_SECTION.join(', '); SELECTOR_SECTION_INSTANT = ((function() {