(function() { var world; world = typeof global !== 'undefined' ? global : this; /** @module up */ world.up = { version: "0.31.1" }; }).call(this); /** Utility functions ================= Unpoly comes with a number of utility functions that might save you from loading something like [Underscore.js](http://underscorejs.org/). @class up.util */ (function() { var slice = [].slice, bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; up.util = (function($) { /** A function that does nothing. @function up.util.noop @experimental */ var $createElementFromSelector, $createPlaceholder, ANIMATION_DEFERRED_KEY, DivertibleChain, ESCAPE_HTML_ENTITY_MAP, all, any, appendRequestData, cache, castedAttr, clientSize, compact, config, contains, copy, copyAttributes, createElement, createElementFromHtml, cssAnimate, detect, documentHasVerticalScrollbar, each, escapeHtml, escapePressed, evalOption, except, extend, extractOptions, fail, findWithSelf, finishCssAnimate, fixedToAbsolute, forceCompositing, forceRepaint, horizontalScreenHalf, identity, intersect, isArray, isBlank, isDeferred, isDefined, isDetached, isElement, isFixed, isFormData, isFunction, isGiven, isHash, isJQuery, isMissing, isNull, isNumber, isObject, isPresent, isPromise, isStandardPort, isString, isUndefined, isUnmodifiedKeyEvent, isUnmodifiedMouseEvent, last, locationFromXhr, map, measure, memoize, merge, methodFromXhr, multiSelector, nextFrame, nonUpClasses, noop, normalizeMethod, normalizeUrl, nullJQuery, offsetParent, once, only, opacity, openConfig, option, options, parseUrl, pluckData, pluckKey, presence, presentAttr, previewable, promiseTimer, reject, remove, requestDataAsArray, requestDataAsQuery, requestDataFromForm, resolvableWhen, resolvedDeferred, resolvedPromise, scrollbarWidth, select, selectorForElement, sequence, setMissingAttrs, setTimer, submittedValue, temporaryCss, times, titleFromXhr, toArray, trim, unJQuery, uniq, unresolvableDeferred, unresolvablePromise, unwrapElement, whenReady; noop = $.noop; /** @function up.util.memoize @internal */ memoize = function(func) { var cache, cached; cache = void 0; cached = false; return function() { var args; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; if (cached) { return cache; } else { cached = true; return cache = func.apply(null, args); } }; }; /** Returns if the given port is the default port for the given protocol. @function up.util.isStandardPort @internal */ isStandardPort = function(protocol, port) { port = port.toString(); return ((port === "" || port === "80") && protocol === 'http:') || (port === "443" && protocol === 'https:'); }; /** Normalizes relative paths and absolute paths to a full URL that can be checked for equality with other normalized URLs. By default hashes are ignored, search queries are included. @function up.util.normalizeUrl @param {Boolean} [options.hash=false] Whether to include an `#hash` anchor in the normalized URL @param {Boolean} [options.search=true] Whether to include a `?query` string in the normalized URL @param {Boolean} [options.stripTrailingSlash=true] Whether to strip a trailing slash from the pathname @internal */ normalizeUrl = function(urlOrAnchor, options) { var anchor, normalized, pathname; anchor = parseUrl(urlOrAnchor); normalized = anchor.protocol + "//" + anchor.hostname; if (!isStandardPort(anchor.protocol, anchor.port)) { normalized += ":" + anchor.port; } pathname = anchor.pathname; if (pathname[0] !== '/') { pathname = "/" + pathname; } if ((options != null ? options.stripTrailingSlash : void 0) !== false) { pathname = pathname.replace(/\/$/, ''); } normalized += pathname; if ((options != null ? options.hash : void 0) === true) { normalized += anchor.hash; } if ((options != null ? options.search : void 0) !== false) { normalized += anchor.search; } return normalized; }; /** Parses the given URL into components such as hostname and path. If the given URL is not fully qualified, it is assumed to be relative to the current page. @function up.util.parseUrl @return {Object} The parsed URL as an object with `protocol`, `hostname`, `port`, `pathname`, `search` and `hash` properties. @experimental */ parseUrl = function(urlOrAnchor) { var anchor; anchor = null; if (isString(urlOrAnchor)) { anchor = $('').attr({ href: urlOrAnchor }).get(0); if (isBlank(anchor.hostname)) { anchor.href = anchor.href; } } else { anchor = unJQuery(urlOrAnchor); } return anchor; }; /** @function up.util.normalizeMethod @internal */ normalizeMethod = function(method) { if (method) { return method.toUpperCase(); } else { return 'GET'; } }; /** @function $createElementFromSelector @internal */ $createElementFromSelector = function(selector) { var $element, $parent, $root, classes, conjunction, depthSelector, expression, html, i, id, iteration, j, len, len1, path, tag; path = selector.split(/[ >]/); $root = null; for (iteration = i = 0, len = path.length; i < len; iteration = ++i) { depthSelector = path[iteration]; conjunction = depthSelector.match(/(^|\.|\#)[A-Za-z0-9\-_]+/g); tag = "div"; classes = []; id = null; for (j = 0, len1 = conjunction.length; j < len1; j++) { expression = conjunction[j]; switch (expression[0]) { case ".": classes.push(expression.substr(1)); break; case "#": id = expression.substr(1); break; default: tag = expression; } } html = "<" + tag; if (classes.length) { html += " class=\"" + classes.join(" ") + "\""; } if (id) { html += " id=\"" + id + "\""; } html += ">"; $element = $(html); if ($parent) { $element.appendTo($parent); } if (iteration === 0) { $root = $element; } $parent = $element; } return $root; }; createElement = function(tagName, html) { var element; element = document.createElement(tagName); if (isPresent(html)) { element.innerHTML = html; } return element; }; /** @function $create */ $createPlaceholder = function(selector, container) { var $placeholder; if (container == null) { container = document.body; } $placeholder = $createElementFromSelector(selector); $placeholder.addClass('up-placeholder'); $placeholder.appendTo(container); return $placeholder; }; /** Returns a CSS selector that matches the given element as good as possible. This uses, in decreasing order of priority: - The element's `up-id` attribute - The element's ID - The element's name - The element's classes - The element's tag names @function up.util.selectorForElement @param {String|Element|jQuery} The element for which to create a selector. @experimental */ selectorForElement = function(element) { var $element, classes, i, id, klass, len, name, selector, upId; $element = $(element); selector = void 0; up.puts("Creating selector from element %o", $element.get(0)); if (upId = presence($element.attr("up-id"))) { selector = "[up-id='" + upId + "']"; } else if (id = presence($element.attr("id"))) { selector = "#" + id; } else if (name = presence($element.attr("name"))) { selector = "[name='" + name + "']"; } else if (classes = presence(nonUpClasses($element))) { selector = ''; for (i = 0, len = classes.length; i < len; i++) { klass = classes[i]; selector += "." + klass; } } else { selector = $element.prop('tagName').toLowerCase(); } return selector; }; nonUpClasses = function($element) { var classString, classes; classString = $element.attr('class') || ''; classes = classString.split(' '); return select(classes, function(klass) { return isPresent(klass) && !klass.match(/^up-/); }); }; createElementFromHtml = function(html) { var anything, bodyElement, bodyMatch, bodyPattern, capture, closeTag, headElement, htmlElement, openTag, titleElement, titleMatch, titlePattern; openTag = function(tag) { return "<" + tag + "(?: [^>]*)?>"; }; closeTag = function(tag) { return ""; }; anything = '(?:.|\\s)*?'; capture = function(pattern) { return "(" + pattern + ")"; }; titlePattern = new RegExp(openTag('title') + capture(anything) + closeTag('title'), 'i'); bodyPattern = new RegExp(openTag('body') + capture(anything) + closeTag('body'), 'i'); if (bodyMatch = html.match(bodyPattern)) { htmlElement = document.createElement('html'); bodyElement = createElement('body', bodyMatch[1]); htmlElement.appendChild(bodyElement); if (titleMatch = html.match(titlePattern)) { headElement = createElement('head'); htmlElement.appendChild(headElement); titleElement = createElement('title', titleMatch[1]); headElement.appendChild(titleElement); } return htmlElement; } else { return createElement('div', html); } }; /** Merge the contents of two or more objects together into the first object. @function up.util.extend @param {Object} target @param {Array} sources... @stable */ extend = $.extend; /** Returns a new string with whitespace removed from the beginning and end of the given string. @param {String} A string that might have whitespace at the beginning and end. @return {String} The trimmed string. @stable */ trim = $.trim; /** Calls the given function for each element (and, optional, index) of the given array. @function up.util.each @param {Array} array @param {Function} block A function that will be called with each element and (optional) iteration index. @stable */ each = function(array, block) { var i, index, item, len, results; results = []; for (index = i = 0, len = array.length; i < len; index = ++i) { item = array[index]; results.push(block(item, index)); } return results; }; /** Translate all items in an array to new array of items. @function up.util.map @param {Array} array @param {Function} block A function that will be called with each element and (optional) iteration index. @return {Array} A new array containing the result of each function call. @stable */ map = each; /** Calls the given function for the given number of times. @function up.util.times @param {Number} count @param {Function} block @stable */ times = function(count, block) { var i, iteration, ref, results; results = []; for (iteration = i = 0, ref = count - 1; 0 <= ref ? i <= ref : i >= ref; iteration = 0 <= ref ? ++i : --i) { results.push(block(iteration)); } return results; }; /** Returns whether the given argument is `null`. @function up.util.isNull @param object @return {Boolean} @stable */ isNull = function(object) { return object === null; }; /** Returns whether the given argument is `undefined`. @function up.util.isUndefined @param object @return {Boolean} @stable */ isUndefined = function(object) { return object === void(0); }; /** Returns whether the given argument is not `undefined`. @function up.util.isDefined @param object @return {Boolean} @stable */ isDefined = function(object) { return !isUndefined(object); }; /** Returns whether the given argument is either `undefined` or `null`. Note that empty strings or zero are *not* considered to be "missing". For the opposite of `up.util.isMissing` see [`up.util.isGiven`](/up.util.isGiven). @function up.util.isMissing @param object @return {Boolean} @stable */ isMissing = function(object) { return isUndefined(object) || isNull(object); }; /** Returns whether the given argument is neither `undefined` nor `null`. Note that empty strings or zero *are* considered to be "given". For the opposite of `up.util.isGiven` see [`up.util.isMissing`](/up.util.isMissing). @function up.util.isGiven @param object @return {Boolean} @stable */ isGiven = function(object) { return !isMissing(object); }; /** Return whether the given argument is considered to be blank. This returns `true` for: - `undefined` - `null` - Empty strings - Empty arrays - An object without own enumerable properties All other arguments return `false`. @function up.util.isBlank @param object @return {Boolean} @stable */ isBlank = function(object) { return isMissing(object) || (isObject(object) && Object.keys(object).length === 0) || (object.length === 0); }; /** Returns the given argument if the argument is [present](/up.util.isPresent), otherwise returns `undefined`. @function up.util.presence @param object @param {Function} [tester=up.util.isPresent] The function that will be used to test whether the argument is present. @return {T|Undefined} @stable */ presence = function(object, tester) { if (tester == null) { tester = isPresent; } if (tester(object)) { return object; } else { return void 0; } }; /** Returns whether the given argument is not [blank](/up.util.isBlank). @function up.util.isPresent @param object @return {Boolean} @stable */ isPresent = function(object) { return !isBlank(object); }; /** Returns whether the given argument is a function. @function up.util.isFunction @param object @return {Boolean} @stable */ isFunction = function(object) { return typeof object === 'function'; }; /** Returns whether the given argument is a string. @function up.util.isString @param object @return {Boolean} @stable */ isString = function(object) { return typeof object === 'string'; }; /** Returns whether the given argument is a number. Note that this will check the argument's *type*. It will return `false` for a string like `"123"`. @function up.util.isNumber @param object @return {Boolean} @stable */ isNumber = function(object) { return typeof object === 'number'; }; /** Returns whether the given argument is an object, but not a function. @function up.util.isHash @param object @return {Boolean} @stable */ isHash = function(object) { return typeof object === 'object' && !!object; }; /** Returns whether the given argument is an object. This also returns `true` for functions, which may behave like objects in Javascript. For an alternative that returns `false` for functions, see [`up.util.isHash`](/up.util.isHash). @function up.util.isObject @param object @return {Boolean} @stable */ isObject = function(object) { return isHash(object) || (typeof object === 'function'); }; /** Returns whether the given argument is a DOM element. @function up.util.isElement @param object @return {Boolean} @stable */ isElement = function(object) { return !!(object && object.nodeType === 1); }; /** Returns whether the given argument is a jQuery collection. @function up.util.isJQuery @param object @return {Boolean} @stable */ isJQuery = function(object) { return object instanceof jQuery; }; /** Returns whether the given argument is an object with a `then` method. @function up.util.isPromise @param object @return {Boolean} @stable */ isPromise = function(object) { return isObject(object) && isFunction(object.then); }; /** Returns whether the given argument is an object with `then` and `resolve` methods. @function up.util.isDeferred @param object @return {Boolean} @stable */ isDeferred = function(object) { return isPromise(object) && isFunction(object.resolve); }; /** Returns whether the given argument is an array. @function up.util.isArray @param object @return {Boolean} @stable */ isArray = Array.isArray || function(object) { return Object.prototype.toString.call(object) === '[object Array]'; }; /** Returns whether the given argument is a `FormData` instance. Always returns `false` in browsers that don't support `FormData`. @function up.util.isFormData @param object @return {Boolean} @internal */ isFormData = function(object) { return up.browser.canFormData() && object instanceof FormData; }; /** Converts the given array-like argument into an array. Returns the array. @function up.util.toArray @param object @return {Array} @stable */ toArray = function(object) { return Array.prototype.slice.call(object); }; /** Shallow-copies the given array or object into a new array or object. Returns the new array or object. @function up.util.copy @param {Object|Array} object @return {Object|Array} @stable */ copy = function(object) { if (isArray(object)) { return object.slice(); } else if (isHash(object)) { return extend({}, object); } }; /** If given a jQuery collection, returns the underlying array of DOM element. If given any other argument, returns the argument unchanged. @function up.util.unJQuery @param object @internal */ unJQuery = function(object) { if (isJQuery(object)) { return object.get(0); } else { return object; } }; /** Creates a new object by merging together the properties from the given objects. @function up.util.merge @param {Array} sources... @return Object @stable */ merge = function() { var sources; sources = 1 <= arguments.length ? slice.call(arguments, 0) : []; return extend.apply(null, [{}].concat(slice.call(sources))); }; /** Creates an options hash from the given argument and some defaults. The semantics of this function are confusing. We want to get rid of this in the future. @function up.util.options @param {Object} object @param {Object} [defaults] @return {Object} @internal */ options = function(object, defaults) { var defaultValue, key, merged, value; merged = object ? copy(object) : {}; if (defaults) { for (key in defaults) { defaultValue = defaults[key]; value = merged[key]; if (!isGiven(value)) { merged[key] = defaultValue; } else if (isObject(defaultValue) && isObject(value)) { merged[key] = options(value, defaultValue); } } } return merged; }; /** Returns the first argument that is considered [given](/up.util.isGiven). This function is useful when you have multiple option sources and the value can be boolean. In that case you cannot change the sources with a `||` operator (since that doesn't short-circuit at `false`). @function up.util.option @param {Array} args... @internal */ option = function() { var args; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; return detect(args, isGiven); }; /** Passes each element in the given array to the given function. Returns the first element for which the function returns a truthy value. If no object matches, returns `undefined`. @function up.util.detect @param {Array} array @param {Function} tester @return {T|Undefined} @stable */ detect = function(array, tester) { var element, i, len, match; match = void 0; for (i = 0, len = array.length; i < len; i++) { element = array[i]; if (tester(element)) { match = element; break; } } return match; }; /** Returns whether the given function returns a truthy value for any element in the given array. @function up.util.any @param {Array} array @param {Function} tester @return {Boolean} @experimental */ any = function(array, tester) { var element, i, len, match; match = false; for (i = 0, len = array.length; i < len; i++) { element = array[i]; if (tester(element)) { match = true; break; } } return match; }; /** Returns whether the given function returns a truthy value for all elements in the given array. @function up.util.all @param {Array} array @param {Function} tester @return {Boolean} @experimental */ all = function(array, tester) { var element, i, len, match; match = true; for (i = 0, len = array.length; i < len; i++) { element = array[i]; if (!tester(element)) { match = false; break; } } return match; }; /** Returns all elements from the given array that are neither `null` or `undefined`. @function up.util.compact @param {Array} array @return {Array} @stable */ compact = function(array) { return select(array, isGiven); }; /** Returns the given array without duplicates. @function up.util.uniq @param {Array} array @return {Array} @stable */ uniq = function(array) { var seen; seen = {}; return select(array, function(element) { if (seen.hasOwnProperty(element)) { return false; } else { return seen[element] = true; } }); }; /** Returns all elements from the given array that return a truthy value when passed to the given function. @function up.util.select @param {Array} array @return {Array} @stable */ select = function(array, tester) { var matches; matches = []; each(array, function(element) { if (tester(element)) { return matches.push(element); } }); return matches; }; /** Returns all elements from the given array that do not return a truthy value when passed to the given function. @function up.util.reject @param {Array} array @return {Array} @stable */ reject = function(array, tester) { return select(array, function(element) { return !tester(element); }); }; /** Returns the intersection of the given two arrays. Implementation is not optimized. Don't use it for large arrays. @function up.util.intersect @internal */ intersect = function(array1, array2) { return select(array1, function(element) { return contains(array2, element); }); }; /** Returns the first [present](/up.util.isPresent) element attribute among the given list of attribute names. @function up.util.presentAttr @internal */ presentAttr = function() { var $element, attrName, attrNames, values; $element = arguments[0], attrNames = 2 <= arguments.length ? slice.call(arguments, 1) : []; values = (function() { var i, len, results; results = []; for (i = 0, len = attrNames.length; i < len; i++) { attrName = attrNames[i]; results.push($element.attr(attrName)); } return results; })(); return detect(values, isPresent); }; /** Waits for the given number of milliseconds, the nruns the given callback. If the number of milliseconds is zero, the callback is run in the current execution frame. See [`up.util.nextFrame`] for running a function in the next executation frame. @function up.util.setTimer @param {Number} millis @param {Function} callback @experimental */ setTimer = function(millis, callback) { if (millis > 0) { return setTimeout(callback, millis); } else { callback(); return void 0; } }; /** Schedules the given function to be called in the next Javascript execution frame. @function up.util.nextFrame @param {Function} block @stable */ nextFrame = function(block) { return setTimeout(block, 0); }; /** Returns the last element of the given array. @function up.util.last @param {Array} array @return {T} */ last = function(array) { return array[array.length - 1]; }; /** Measures the drawable area of the document. @function up.util.clientSize @internal */ clientSize = function() { var element; element = document.documentElement; return { width: element.clientWidth, height: element.clientHeight }; }; /** Returns the width of a scrollbar. This only runs once per page load. @function up.util.scrollbarWidth @internal */ scrollbarWidth = memoize(function() { var $outer, outer, width; $outer = $('
'); $outer.attr('up-viewport', ''); $outer.css({ position: 'absolute', top: '0', left: '0', width: '100px', height: '100px', overflowY: 'scroll' }); $outer.appendTo(document.body); outer = $outer.get(0); width = outer.offsetWidth - outer.clientWidth; $outer.remove(); return width; }); /** Returns whether the given element is currently showing a vertical scrollbar. @function up.util.documentHasVerticalScrollbar @internal */ documentHasVerticalScrollbar = function() { var $body, body, bodyOverflow, forcedHidden, forcedScroll, html; body = document.body; $body = $(body); html = document.documentElement; bodyOverflow = $body.css('overflow-y'); forcedScroll = bodyOverflow === 'scroll'; forcedHidden = bodyOverflow === 'hidden'; return forcedScroll || (!forcedHidden && html.scrollHeight > html.clientHeight); }; /** Modifies the given function so it only runs once. Subsequent calls will return the previous return value. @function up.util.once @param {Function} fun @experimental */ once = function(fun) { var result; result = void 0; return function() { var args; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; if (fun != null) { result = fun.apply(null, args); } fun = void 0; return result; }; }; /** Temporarily sets the CSS for the given element. @function up.util.temporaryCss @param {jQuery} $element @param {Object} css @param {Function} [block] If given, the CSS is set, the block is called and the old CSS is restored. @return {Function} A function that restores the original CSS when called. @internal */ temporaryCss = function(elementOrSelector, css, block) { var $element, memo, oldCss; $element = $(elementOrSelector); oldCss = $element.css(Object.keys(css)); $element.css(css); memo = function() { return $element.css(oldCss); }; if (block) { block(); return memo(); } else { return once(memo); } }; /** Forces the given jQuery element into an accelerated compositing layer. @function up.util.forceCompositing @internal */ forceCompositing = function($element) { var memo, oldTransforms; oldTransforms = $element.css(['transform', '-webkit-transform']); if (isBlank(oldTransforms) || oldTransforms['transform'] === 'none') { memo = function() { return $element.css(oldTransforms); }; $element.css({ 'transform': 'translateZ(0)', '-webkit-transform': 'translateZ(0)' }); } else { memo = function() {}; } return memo; }; /** Forces a repaint of the given element. @function up.util.forceRepaint @internal */ forceRepaint = function(element) { element = unJQuery(element); return element.offsetHeight; }; /** 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. @function up.util.cssAnimate @param {Element|jQuery|String} elementOrSelector The element to animate. @param {Object} lastFrame The CSS properties that should be transitioned to. @param {Number} [options.duration=300] The duration of the animation, in milliseconds. @param {Number} [options.delay=0] The delay before the animation starts, in milliseconds. @param {String} [options.easing='ease'] 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. @return {Deferred} A promise for the animation's end. @internal */ cssAnimate = function(elementOrSelector, lastFrame, opts) { var $element, deferred, oldTransition, onTransitionEnd, transition, transitionFinished, transitionProperties, withoutCompositing; $element = $(elementOrSelector); opts = options(opts, { duration: 300, delay: 0, easing: 'ease' }); if (opts.duration === 0) { $element.css(lastFrame); return resolvedDeferred(); } deferred = $.Deferred(); transitionProperties = Object.keys(lastFrame); transition = { 'transition-property': transitionProperties.join(', '), 'transition-duration': opts.duration + "ms", 'transition-delay': opts.delay + "ms", 'transition-timing-function': opts.easing }; oldTransition = $element.css(Object.keys(transition)); $element.addClass('up-animating'); transitionFinished = function() { $element.removeClass('up-animating'); return $element.off('transitionend', onTransitionEnd); }; onTransitionEnd = function(event) { var completedProperty; completedProperty = event.originalEvent.propertyName; if (contains(transitionProperties, completedProperty)) { deferred.resolve(); return transitionFinished(); } }; $element.on('transitionend', onTransitionEnd); deferred.then(transitionFinished); withoutCompositing = forceCompositing($element); $element.css(transition); $element.css(lastFrame); $element.data(ANIMATION_DEFERRED_KEY, deferred); deferred.then(function() { var hadTransitionBefore; $element.removeData(ANIMATION_DEFERRED_KEY); withoutCompositing(); $element.css({ 'transition': 'none' }); hadTransitionBefore = !(oldTransition['transition-property'] === 'none' || (oldTransition['transition-property'] === 'all' && oldTransition['transition-duration'][0] === '0')); if (hadTransitionBefore) { forceRepaint($element); return $element.css(oldTransition); } }); return deferred; }; ANIMATION_DEFERRED_KEY = 'up-animation-deferred'; /** 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. Also see [`up.motion.finish`](/up.motion.finish). @function up.util.finishCssAnimate @param {Element|jQuery|String} elementOrSelector @internal */ finishCssAnimate = function(elementOrSelector) { return $(elementOrSelector).each(function() { var existingAnimation; if (existingAnimation = pluckData(this, ANIMATION_DEFERRED_KEY)) { return existingAnimation.resolve(); } }); }; /** Measures the given element. @function up.util.measure @internal */ measure = function($element, opts) { var $context, box, contextCoords, coordinates, elementCoords; opts = options(opts, { relative: false, inner: false }); if (opts.relative) { if (opts.relative === true) { coordinates = $element.position(); } else { $context = $(opts.relative); elementCoords = $element.offset(); if ($context.is(document)) { coordinates = elementCoords; } else { contextCoords = $context.offset(); coordinates = { left: elementCoords.left - contextCoords.left, top: elementCoords.top - contextCoords.top }; } } } else { coordinates = $element.offset(); } box = { left: coordinates.left, top: coordinates.top }; if (opts.inner) { box.width = $element.width(); box.height = $element.height(); } else { box.width = $element.outerWidth(); box.height = $element.outerHeight(); } return box; }; /** Copies all attributes from the source element to the target element. @function up.util.copyAttributes @internal */ copyAttributes = function($source, $target) { var attr, i, len, ref, results; ref = $source.get(0).attributes; results = []; for (i = 0, len = ref.length; i < len; i++) { attr = ref[i]; if (attr.specified) { results.push($target.attr(attr.name, attr.value)); } else { results.push(void 0); } } return results; }; /** Looks for the given selector in the element and its descendants. @function up.util.findWithSelf @internal */ findWithSelf = function($element, selector) { return $element.find(selector).addBack(selector); }; /** Returns whether the given keyboard event involved the ESC key. @function up.util.escapePressed @internal */ escapePressed = function(event) { return event.keyCode === 27; }; /** Returns whether the given array or string contains the given element or substring. @function up.util.contains @param {Array|String} arrayOrString @param elementOrSubstring @stable */ contains = function(arrayOrString, elementOrSubstring) { return arrayOrString.indexOf(elementOrSubstring) >= 0; }; /** @function up.util.castedAttr @internal */ 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; } }; /** @function up.util.locationFromXhr @internal */ locationFromXhr = function(xhr) { return xhr.getResponseHeader('X-Up-Location'); }; /** @function up.util.titleFromXhr @internal */ titleFromXhr = function(xhr) { return xhr.getResponseHeader('X-Up-Title'); }; /** @function up.util.methodFromXhr @internal */ methodFromXhr = function(xhr) { return xhr.getResponseHeader('X-Up-Method'); }; /** Returns a copy of the given object that only contains the given properties. @function up.util.only @param {Object} object @param {Array} keys... @stable */ only = function() { var filtered, i, len, object, properties, property; object = arguments[0], properties = 2 <= arguments.length ? slice.call(arguments, 1) : []; filtered = {}; for (i = 0, len = properties.length; i < len; i++) { property = properties[i]; if (object.hasOwnProperty(property)) { filtered[property] = object[property]; } } return filtered; }; /** Returns a copy of the given object that contains all except the given properties. @function up.util.except @param {Object} object @param {Array} keys... @stable */ except = function() { var filtered, i, len, object, properties, property; object = arguments[0], properties = 2 <= arguments.length ? slice.call(arguments, 1) : []; filtered = copy(object); for (i = 0, len = properties.length; i < len; i++) { property = properties[i]; delete filtered[property]; } return filtered; }; /** @function up.util.isUnmodifiedKeyEvent @internal */ isUnmodifiedKeyEvent = function(event) { return !(event.metaKey || event.shiftKey || event.ctrlKey); }; /** @function up.util.isUnmodifiedMouseEvent @internal */ isUnmodifiedMouseEvent = function(event) { var isLeftButton; isLeftButton = isUndefined(event.button) || event.button === 0; return isLeftButton && isUnmodifiedKeyEvent(event); }; /** Returns a [Deferred object](https://api.jquery.com/category/deferred-object/) that is already resolved. @function up.util.resolvedDeferred @return {Deferred} @stable */ resolvedDeferred = function() { var deferred; deferred = $.Deferred(); deferred.resolve(); return deferred; }; /** Returns a promise that is already resolved. @function up.util.resolvedPromise @return {Promise} @stable */ resolvedPromise = function() { return resolvedDeferred().promise(); }; /** Returns a [Deferred object](https://api.jquery.com/category/deferred-object/) that will never be resolved. @function up.util.unresolvableDeferred @return {Deferred} @experimental */ unresolvableDeferred = function() { return $.Deferred(); }; /** Returns a promise that will never be resolved. @function up.util.unresolvablePromise @experimental */ unresolvablePromise = function() { return unresolvableDeferred().promise(); }; /** Returns an empty jQuery collection. @function up.util.nullJQuery @internal */ nullJQuery = function() { return $(); }; /** Returns a new promise that resolves once all promises in arguments resolve. Other then [`$.when` from jQuery](https://api.jquery.com/jquery.when/), the combined promise will have a `resolve` method. This `resolve` method will resolve all the wrapped promises. @function up.util.resolvableWhen @internal */ resolvableWhen = function() { var deferreds, joined; deferreds = 1 <= arguments.length ? slice.call(arguments, 0) : []; joined = $.when.apply($, [resolvedDeferred()].concat(slice.call(deferreds))); joined.resolve = memoize(function() { return each(deferreds, function(deferred) { return deferred.resolve(); }); }); return joined; }; /** On the given element, set attributes that are still missing. @function up.util.setMissingAttrs @internal */ setMissingAttrs = function($element, attrs) { var key, results, value; results = []; for (key in attrs) { value = attrs[key]; if (isMissing($element.attr(key))) { results.push($element.attr(key, value)); } else { results.push(void 0); } } return results; }; /** Removes the given element from the given array. This changes the given array. @function up.util.remove @param {Array} array @param {T} element @stable */ remove = function(array, element) { var index; index = array.indexOf(element); if (index >= 0) { array.splice(index, 1); return element; } }; /** @function up.util.multiSelector @internal */ multiSelector = function(parts) { var combinedSelector, elements, i, len, obj, part, selectors; obj = {}; selectors = []; elements = []; for (i = 0, len = parts.length; i < len; i++) { part = parts[i]; if (isString(part)) { selectors.push(part); } else { elements.push(part); } } obj.parsed = elements; if (selectors.length) { combinedSelector = selectors.join(', '); obj.parsed.push(combinedSelector); } obj.select = function() { return obj.find(void 0); }; obj.find = function($root) { var $matches, $result, j, len1, ref, selector; $result = nullJQuery(); ref = obj.parsed; for (j = 0, len1 = ref.length; j < len1; j++) { selector = ref[j]; $matches = $root ? $root.find(selector) : $(selector); $result = $result.add($matches); } return $result; }; obj.findWithSelf = function($start) { var $matches; $matches = obj.find($start); if (obj.doesMatch($start)) { $matches = $matches.add($start); } return $matches; }; obj.doesMatch = function(element) { var $element; $element = $(element); return any(obj.parsed, function(selector) { return $element.is(selector); }); }; obj.seekUp = function(start) { var $element, $result, $start; $start = $(start); $element = $start; $result = void 0; while ($element.length) { if (obj.doesMatch($element)) { $result = $element; break; } $element = $element.parent(); } return $result || nullJQuery(); }; return obj; }; /** If the given `value` is a function, calls the function with the given `args`. Otherwise it just returns `value`. @function up.util.evalOption @internal */ evalOption = function() { var args, value; value = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : []; if (isFunction(value)) { return value.apply(null, args); } else { return value; } }; /** @function 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. @param {Function} [config.key] A function that takes an argument and returns a `String` key for storage. If omitted, `toString()` is called on the argument. @internal */ cache = function(config) { var alias, clear, expiryMillis, get, isEnabled, isFresh, keys, log, makeRoomForAnotherKey, maxKeys, normalizeStoreKey, set, store, timestamp; if (config == null) { config = {}; } store = void 0; maxKeys = function() { return evalOption(config.size); }; expiryMillis = function() { return evalOption(config.expiry); }; normalizeStoreKey = function(key) { if (config.key) { return config.key(key); } else { return key.toString(); } }; isEnabled = function() { return maxKeys() !== 0 && expiryMillis() !== 0; }; clear = function() { return store = {}; }; clear(); log = function() { var args; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; if (config.logPrefix) { args[0] = "[" + config.logPrefix + "] " + args[0]; return up.puts.apply(up, args); } }; keys = function() { return Object.keys(store); }; makeRoomForAnotherKey = function() { var max, oldestKey, oldestTimestamp, storeKeys; storeKeys = copy(keys()); max = maxKeys(); if (max && storeKeys.length >= max) { 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, { silent: true }); if (isDefined(value)) { return set(newKey, value); } }; timestamp = function() { return (new Date()).valueOf(); }; set = function(key, value) { var storeKey; if (isEnabled()) { makeRoomForAnotherKey(); 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 millis, timeSinceTouch; millis = expiryMillis(); if (millis) { timeSinceTouch = timestamp() - entry.timestamp; return timeSinceTouch < millis; } else { return true; } }; get = function(key, options) { var entry, storeKey; if (options == null) { options = {}; } storeKey = normalizeStoreKey(key); if (entry = store[storeKey]) { if (isFresh(entry)) { if (!options.silent) { log("Cache hit for '%s'", key); } return entry.value; } else { if (!options.silent) { log("Discarding stale cache entry for '%s'", key); } remove(key); return void 0; } } else { if (!options.silent) { log("Cache miss for '%s'", key); } return void 0; } }; return { alias: alias, get: get, set: set, remove: remove, clear: clear, keys: keys }; }; /** @function up.util.config @param {Object|Function} blueprint Default configuration options. Will be restored by calling `reset` on the returned object. @return {Object} An object with a `reset` function. @internal */ config = function(blueprint) { var hash; hash = openConfig(blueprint); Object.preventExtensions(hash); return hash; }; /** @function up.util.openConfig @internal */ openConfig = function(blueprint) { var hash; if (blueprint == null) { blueprint = {}; } hash = {}; hash.reset = function() { var newOptions; newOptions = blueprint; if (isFunction(newOptions)) { newOptions = newOptions(); } return extend(hash, newOptions); }; hash.reset(); return hash; }; /** @function up.util.unwrapElement @internal */ unwrapElement = function(wrapper) { var parent, wrappedNodes; wrapper = unJQuery(wrapper); parent = wrapper.parentNode; wrappedNodes = toArray(wrapper.childNodes); each(wrappedNodes, function(wrappedNode) { return parent.insertBefore(wrappedNode, wrapper); }); return parent.removeChild(wrapper); }; /** @function up.util.offsetParent @internal */ offsetParent = function($element) { var $match, position; $match = void 0; while (($element = $element.parent()) && $element.length) { position = $element.css('position'); if (position === 'absolute' || position === 'relative' || $element.is('body')) { $match = $element; break; } } return $match; }; /** Returns if the given element has a `fixed` position. @function up.util.isFixed @internal */ isFixed = function(element) { var $element, position; $element = $(element); while (true) { position = $element.css('position'); if (position === 'fixed') { return true; } else { $element = $element.parent(); if ($element.length === 0 || $element.is(document)) { return false; } } } }; /** @function up.util.fixedToAbsolute @internal */ fixedToAbsolute = function(element, $viewport) { var $element, $futureOffsetParent, elementCoords, futureParentCoords; $element = $(element); $futureOffsetParent = offsetParent($element); elementCoords = $element.position(); futureParentCoords = $futureOffsetParent.offset(); return $element.css({ position: 'absolute', left: elementCoords.left - futureParentCoords.left, top: elementCoords.top - futureParentCoords.top + $viewport.scrollTop(), right: '', bottom: '' }); }; /** Normalizes the given params object to the form returned by [`jQuery.serializeArray`](https://api.jquery.com/serializeArray/). @function up.util.requestDataAsArray @param {Object|Array|Undefined|Null} data @internal */ requestDataAsArray = function(data) { var array, i, len, pair, part, query, ref; if (isFormData(data)) { return up.fail('Cannot convert FormData into an array'); } else { query = requestDataAsQuery(data); array = []; ref = query.split('&'); for (i = 0, len = ref.length; i < len; i++) { part = ref[i]; if (isPresent(part)) { pair = part.split('='); array.push({ name: decodeURIComponent(pair[0]), value: decodeURIComponent(pair[1]) }); } } return array; } }; /** Returns an URL-encoded query string for the given params object. @function up.util.requestDataAsQuery @param {Object|Array|Undefined|Null} data @internal */ requestDataAsQuery = function(data) { var query; if (isFormData(data)) { return up.fail('Cannot convert FormData into a query string'); } else if (isPresent(data)) { query = $.param(data); query = query.replace(/\+/g, '%20'); return query; } else { return ""; } }; /** Serializes the given form into a request data representation. @function up.util.requestDataFromForm @return {Array|FormData} @internal */ requestDataFromForm = function(form) { var $form, hasFileInputs; $form = $(form); hasFileInputs = $form.find('input[type=file]').length; if (hasFileInputs && up.browser.canFormData()) { return new FormData($form.get(0)); } else { return $form.serializeArray(); } }; /** Adds a key/value pair to the given request data representation. This mutates the given `data` if `data` is a `FormData`, an object or an array. When `data` is `String` a new string with the appended key/value pair is returned. @function up.util.appendRequestData @param {FormData|Object|Array|Undefined|Null} data @param {String} key @param {String|Blob|File} value @internal */ appendRequestData = function(data, name, value) { var newPair; if (isFormData(data)) { data.append(name, value); } else if (isArray(data)) { data.push({ name: name, value: value }); } else if (isObject(data)) { data[name] = value; } else if (isString(data) || isMissing(data)) { newPair = requestDataAsQuery([ { name: name, value: value } ]); if (isPresent(data)) { data = [data, newPair].join('&'); } else { data = newPair; } } return data; }; /** Throws a [Javascript error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) with the given message. The message will also be printed to the [error log](/up.log.error). Also a notification will be shown at the bottom of the screen. The message may contain [substitution marks](https://developer.mozilla.org/en-US/docs/Web/API/console#Using_string_substitutions). \#\#\# Examples up.fail('Division by zero') up.fail('Unexpected result %o', result) @function up.fail @param {String} message A message with details about the error. The message can contain [substitution marks](https://developer.mozilla.org/en-US/docs/Web/API/console#Using_string_substitutions) like `%s` or `%o`. @param {Array} vars... A list of variables to replace any substitution marks in the error message. @experimental */ fail = function() { var args, asString, messageArgs, ref, ref1, toastOptions; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; if (isArray(args[0])) { messageArgs = args[0]; toastOptions = args[1] || {}; } else { messageArgs = args; toastOptions = {}; } (ref = up.log).error.apply(ref, messageArgs); whenReady().then(function() { return up.toast.open(messageArgs, toastOptions); }); asString = (ref1 = up.browser).sprintf.apply(ref1, messageArgs); throw new Error(asString); }; ESCAPE_HTML_ENTITY_MAP = { "&": "&", "<": "<", ">": ">", '"': '"' }; /** Escapes the given string of HTML by replacing control chars with their HTML entities. @function up.util.escapeHtml @param {String} string The text that should be escaped @experimental */ escapeHtml = function(string) { return string.replace(/[&<>"]/g, function(char) { return ESCAPE_HTML_ENTITY_MAP[char]; }); }; pluckKey = function(object, key) { var value; value = object[key]; delete object[key]; return value; }; pluckData = function(elementOrSelector, key) { var $element, value; $element = $(elementOrSelector); value = $element.data(key); $element.removeData(key); return value; }; extractOptions = function(args) { var lastArg; lastArg = last(args); if (isHash(lastArg) && !isJQuery(lastArg)) { return args.pop(); } else { return {}; } }; opacity = function(element) { var rawOpacity; rawOpacity = $(element).css('opacity'); if (isGiven(rawOpacity)) { return parseFloat(rawOpacity); } else { return void 0; } }; whenReady = memoize(function() { var deferred; if ($.isReady) { return resolvedPromise(); } else { deferred = $.Deferred(); $(function() { return deferred.resolve(); }); return deferred.promise(); } }); identity = function(arg) { return arg; }; /** Returns whether the given element has been detached from the DOM (or whether it was never attached). @function up.util.isDetached @internal */ isDetached = function(element) { element = unJQuery(element); return !jQuery.contains(document.documentElement, element); }; /** Given a function that will return a promise, returns a proxy function with an additional `.promise` attribute. When the proxy is called, the inner function is called. The proxy's `.promise` attribute is available even before the function is called and will resolve when the inner function's returned promise resolves. If the inner function does not return a promise, the proxy's `.promise` attribute will resolve as soon as the inner function returns. @function up.util.previewable @internal */ previewable = function(fun) { var deferred, preview; deferred = $.Deferred(); preview = function() { var args, funValue; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; funValue = fun.apply(null, args); if (isPromise(funValue)) { funValue.then(function() { return deferred.resolve(funValue); }); } else { deferred.resolve(funValue); } return funValue; }; preview.promise = deferred.promise(); return preview; }; /** A linear task queue whose (2..n)th tasks can be changed at any time. @function up.util.DivertibleChain @internal */ DivertibleChain = (function() { function DivertibleChain() { this.asap = bind(this.asap, this); this.poke = bind(this.poke, this); this.allTasks = bind(this.allTasks, this); this.promise = bind(this.promise, this); this.reset = bind(this.reset, this); this.reset(); } DivertibleChain.prototype.reset = function() { this.queue = []; return this.currentTask = void 0; }; DivertibleChain.prototype.promise = function() { var promises; promises = map(this.allTasks(), function(task) { return task.promise; }); return $.when.apply($, promises); }; DivertibleChain.prototype.allTasks = function() { var tasks; tasks = []; if (this.currentTask) { tasks.push(this.currentTask); } tasks = tasks.concat(this.queue); return tasks; }; DivertibleChain.prototype.poke = function() { var promise; if (!this.currentTask) { if (this.currentTask = this.queue.shift()) { promise = this.currentTask(); return promise.always((function(_this) { return function() { _this.currentTask = void 0; return _this.poke(); }; })(this)); } } }; DivertibleChain.prototype.asap = function() { var newTasks; newTasks = 1 <= arguments.length ? slice.call(arguments, 0) : []; this.queue = map(newTasks, previewable); return this.poke(); }; return DivertibleChain; })(); /** @function up.util.submittedValue @internal */ submittedValue = function(fieldOrSelector) { var $field; $field = $(fieldOrSelector); if ($field.is('[type=checkbox], [type=radio]') && !$field.is(':checked')) { return void 0; } else { return $field.val(); } }; /** @function up.util.sequence @internal */ sequence = function() { var functions; functions = 1 <= arguments.length ? slice.call(arguments, 0) : []; return function() { return map(functions, function(f) { return f(); }); }; }; /** @function up.util.promiseTimer @internal */ promiseTimer = function(ms) { var deferred, timeout; deferred = $.Deferred(); timeout = setTimer(ms, function() { return deferred.resolve(); }); deferred.cancel = function() { return clearTimeout(timeout); }; return deferred; }; /** Returns `'left'` if the center of the given element is in the left 50% of the screen. Otherwise returns `'right'`. @function up.util.horizontalScreenHalf @internal */ horizontalScreenHalf = function($element) { var elementDims, elementMid, screenDims, screenMid; elementDims = measure($element); screenDims = clientSize(); elementMid = elementDims.left + 0.5 * elementDims.width; screenMid = 0.5 * screenDims.width; if (elementMid < screenMid) { return 'left'; } else { return 'right'; } }; return { isDetached: isDetached, requestDataAsArray: requestDataAsArray, requestDataAsQuery: requestDataAsQuery, appendRequestData: appendRequestData, requestDataFromForm: requestDataFromForm, offsetParent: offsetParent, fixedToAbsolute: fixedToAbsolute, isFixed: isFixed, presentAttr: presentAttr, createElement: createElement, parseUrl: parseUrl, normalizeUrl: normalizeUrl, normalizeMethod: normalizeMethod, createElementFromHtml: createElementFromHtml, $createElementFromSelector: $createElementFromSelector, $createPlaceholder: $createPlaceholder, selectorForElement: selectorForElement, extend: extend, copy: copy, merge: merge, options: options, option: option, fail: fail, each: each, map: map, times: times, any: any, all: all, detect: detect, select: select, reject: reject, intersect: intersect, compact: compact, uniq: uniq, last: last, isNull: isNull, isDefined: isDefined, isUndefined: isUndefined, isGiven: isGiven, isMissing: isMissing, isPresent: isPresent, isBlank: isBlank, presence: presence, isObject: isObject, isFunction: isFunction, isString: isString, isNumber: isNumber, isElement: isElement, isJQuery: isJQuery, isPromise: isPromise, isDeferred: isDeferred, isHash: isHash, isArray: isArray, isFormData: isFormData, isUnmodifiedKeyEvent: isUnmodifiedKeyEvent, isUnmodifiedMouseEvent: isUnmodifiedMouseEvent, nullJQuery: nullJQuery, unJQuery: unJQuery, setTimer: setTimer, nextFrame: nextFrame, measure: measure, temporaryCss: temporaryCss, cssAnimate: cssAnimate, finishCssAnimate: finishCssAnimate, forceCompositing: forceCompositing, forceRepaint: forceRepaint, escapePressed: escapePressed, copyAttributes: copyAttributes, findWithSelf: findWithSelf, contains: contains, toArray: toArray, castedAttr: castedAttr, locationFromXhr: locationFromXhr, titleFromXhr: titleFromXhr, methodFromXhr: methodFromXhr, clientSize: clientSize, only: only, except: except, trim: trim, unresolvableDeferred: unresolvableDeferred, unresolvablePromise: unresolvablePromise, resolvedPromise: resolvedPromise, resolvedDeferred: resolvedDeferred, resolvableWhen: resolvableWhen, setMissingAttrs: setMissingAttrs, remove: remove, memoize: memoize, scrollbarWidth: scrollbarWidth, documentHasVerticalScrollbar: documentHasVerticalScrollbar, config: config, openConfig: openConfig, cache: cache, unwrapElement: unwrapElement, multiSelector: multiSelector, error: fail, pluckData: pluckData, pluckKey: pluckKey, extractOptions: extractOptions, isDetached: isDetached, noop: noop, opacity: opacity, whenReady: whenReady, identity: identity, escapeHtml: escapeHtml, DivertibleChain: DivertibleChain, submittedValue: submittedValue, sequence: sequence, promiseTimer: promiseTimer, previewable: previewable, evalOption: evalOption, horizontalScreenHalf: horizontalScreenHalf }; })($); up.fail = up.util.fail; }).call(this); /** Browser interface ================= Some browser-interfacing methods and switches that we can't currently get rid off. @class up.browser */ (function() { var slice = [].slice; up.browser = (function($) { var CONSOLE_PLACEHOLDERS, canCssTransition, canFormData, canInputEvent, canLogSubstitution, canPushState, initialRequestMethod, installPolyfills, isIE8OrWorse, isIE9OrWorse, isRecentJQuery, isSupported, loadPage, popCookie, puts, sessionStorage, setLocationHref, sprintf, sprintfWithFormattedArgs, stringifyArg, submitForm, u, url, whenConfirmed; u = up.util; /** @method up.browser.loadPage @param {String} url @param {String} [options.method='get'] @param {Object|Array} [options.data] @internal */ loadPage = function(url, options) { var $form, addField, csrfField, method, query; if (options == null) { options = {}; } method = u.option(options.method, 'get').toLowerCase(); if (method === 'get') { query = u.requestDataAsQuery(options.data); if (query) { url = url + "?" + query; } return setLocationHref(url); } else { $form = $("
"); addField = function(field) { var $field; $field = $(''); $field.attr(field); return $field.appendTo($form); }; addField({ name: up.proxy.config.wrapMethodParam, value: method }); if (csrfField = up.rails.csrfField()) { addField(csrfField); } u.each(u.requestDataAsArray(options.data), addField); $form.hide().appendTo('body'); return submitForm($form); } }; /** For mocking in specs. @method submitForm */ submitForm = function($form) { return $form.submit(); }; /** For mocking in specs. @method setLocationHref */ setLocationHref = function(url) { return location.href = url; }; /** A cross-browser way to interact with `console.log`, `console.error`, etc. This function falls back to `console.log` if the output stream is not implemented. It also prints substitution strings (e.g. `console.log("From %o to %o", "a", "b")`) as a single string if the browser console does not support substitution strings. \#\#\# Example up.browser.puts('log', 'Hi world'); up.browser.puts('error', 'There was an error in %o', obj); @function up.browser.puts @internal */ puts = function() { var args, message, stream; stream = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : []; if (canLogSubstitution()) { return console[stream].apply(console, args); } else { message = sprintf.apply(null, args); return console[stream](message); } }; CONSOLE_PLACEHOLDERS = /\%[odisf]/g; stringifyArg = function(arg) { var $arg, attr, closer, j, len, maxLength, ref, string, value; maxLength = 200; closer = ''; if (u.isString(arg)) { string = arg.replace(/[\n\r\t ]+/g, ' '); string = string.replace(/^[\n\r\t ]+/, ''); string = string.replace(/[\n\r\t ]$/, ''); string = "\"" + string + "\""; closer = '"'; } else if (u.isUndefined(arg)) { string = 'undefined'; } else if (u.isNumber(arg) || u.isFunction(arg)) { string = arg.toString(); } else if (u.isArray(arg)) { string = "[" + (u.map(arg, stringifyArg).join(', ')) + "]"; closer = ']'; } else if (u.isJQuery(arg)) { string = "$(" + (u.map(arg, stringifyArg).join(', ')) + ")"; closer = ')'; } else if (u.isElement(arg)) { $arg = $(arg); string = "<" + (arg.tagName.toLowerCase()); ref = ['id', 'name', 'class']; for (j = 0, len = ref.length; j < len; j++) { attr = ref[j]; if (value = $arg.attr(attr)) { string += " " + attr + "=\"" + value + "\""; } } string += ">"; closer = '>'; } else { string = JSON.stringify(arg); } if (string.length > maxLength) { string = (string.substr(0, maxLength)) + " …"; string += closer; } return string; }; /** See https://developer.mozilla.org/en-US/docs/Web/API/Console#Using_string_substitutions @function up.browser.sprintf @internal */ sprintf = function() { var args, message; message = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : []; return sprintfWithFormattedArgs.apply(null, [u.identity, message].concat(slice.call(args))); }; /** @function up.browser.sprintfWithBounds @internal */ sprintfWithFormattedArgs = function() { var args, formatter, i, message; formatter = arguments[0], message = arguments[1], args = 3 <= arguments.length ? slice.call(arguments, 2) : []; if (u.isBlank(message)) { return ''; } i = 0; return message.replace(CONSOLE_PLACEHOLDERS, function() { var arg; arg = args[i]; arg = formatter(stringifyArg(arg)); i += 1; return arg; }); }; url = function() { return location.href; }; isIE8OrWorse = u.memoize(function() { return u.isUndefined(document.addEventListener); }); isIE9OrWorse = u.memoize(function() { return isIE8OrWorse() || navigator.appVersion.indexOf('MSIE 9.') !== -1; }); /** Returns whether this browser supports manipulation of the current URL via [`history.pushState`](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState). When Unpoly is asked to change history on a browser that doesn't support `pushState` (e.g. through [`up.follow`](/up.follow)), it will gracefully fall back to a full page load. @function up.browser.canPushState @return {Boolean} @experimental */ canPushState = u.memoize(function() { return u.isDefined(history.pushState) && initialRequestMethod() === 'get'; }); /** Returns whether this browser supports animation using [CSS transitions](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Transitions). When Unpoly is asked to animate history on a browser that doesn't support CSS transitions (e.g. through [`up.animate`](/up.animate)), it will skip the animation by instantly jumping to the last frame. @function up.browser.canCssTransition @return {Boolean} @experimental */ canCssTransition = u.memoize(function() { return 'transition' in document.documentElement.style; }); /** Returns whether this browser supports the DOM event [`input`](https://developer.mozilla.org/de/docs/Web/Events/input). @function up.browser.canInputEvent @return {Boolean} @experimental */ canInputEvent = u.memoize(function() { return 'oninput' in document.createElement('input'); }); /** Returns whether this browser supports the [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) interface. @function up.browser.canFormData @return {Boolean} @experimental */ canFormData = u.memoize(function() { return !!window.FormData; }); /** Returns whether this browser supports [string substitution](https://developer.mozilla.org/en-US/docs/Web/API/console#Using_string_substitutions) in `console` functions. \#\#\# Example for string substition console.log("Hello %o!", "Judy"); @function up.browser.canLogSubstitution @return {Boolean} @internal */ canLogSubstitution = u.memoize(function() { return !isIE9OrWorse(); }); isRecentJQuery = u.memoize(function() { var major, minor, parts, version; version = $.fn.jquery; parts = version.split('.'); major = parseInt(parts[0]); minor = parseInt(parts[1]); return major >= 2 || (major === 1 && minor >= 9); }); popCookie = function(name) { var ref, value; value = (ref = document.cookie.match(new RegExp(name + "=(\\w+)"))) != null ? ref[1] : void 0; if (u.isPresent(value)) { document.cookie = name + '=; expires=Thu, 01-Jan-70 00:00:01 GMT; path=/'; } return value; }; /** @function up,browser.whenConfirmed @return {Promise} @param {String} options.confirm @param {Boolean} options.preload @internal */ whenConfirmed = function(options) { if (options.preload || u.isBlank(options.confirm) || window.confirm(options.confirm)) { return u.resolvedPromise(); } else { return u.unresolvablePromise(); } }; initialRequestMethod = u.memoize(function() { return (popCookie('_up_request_method') || 'get').toLowerCase(); }); /** Returns whether Unpoly supports the current browser. This also returns `true` if Unpoly only support some features, but falls back gracefully for other features. E.g. IE9 is almost fully supported, but due to its lack of [`history.pushState`](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState) Unpoly falls back to a full page load when asked to manipulate history. Currently Unpoly supports IE9 with jQuery 1.9+. On older browsers Unpoly will prevent itself from [booting](/up.boot) and ignores all registered [event handlers](/up.on) and [compilers](/up.compiler). This leaves you with a classic server-side application. @function up.browser.isSupported @experimental */ isSupported = function() { return (!isIE8OrWorse()) && isRecentJQuery(); }; /** @internal */ installPolyfills = function() { var j, len, method, ref; if (window.console == null) { window.console = {}; } ref = ['debug', 'info', 'warn', 'error', 'group', 'groupCollapsed', 'groupEnd']; for (j = 0, len = ref.length; j < len; j++) { method = ref[j]; if (console[method] == null) { console[method] = function() { var args; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; return puts.apply(null, ['log'].concat(slice.call(args))); }; } } return console.log != null ? console.log : console.log = u.noop; }; /** @internal */ sessionStorage = u.memoize(function() { return window.sessionStorage || { getItem: u.noop, setItem: u.noop, removeItem: u.noop }; }); return { knife: eval(typeof Knife !== "undefined" && Knife !== null ? Knife.point : void 0), url: url, loadPage: loadPage, whenConfirmed: whenConfirmed, canPushState: canPushState, canCssTransition: canCssTransition, canInputEvent: canInputEvent, canFormData: canFormData, canLogSubstitution: canLogSubstitution, isSupported: isSupported, installPolyfills: installPolyfills, puts: puts, sprintf: sprintf, sprintfWithFormattedArgs: sprintfWithFormattedArgs, sessionStorage: sessionStorage }; })(jQuery); }).call(this); /** Events ====== Most Unpoly interactions emit DOM events that are prefixed with `up:`. $(document).on('up:modal:opened', function(event) { console.log('A new modal has just opened!'); }); Events often have both present ([`up:modal:open`](/up:modal:open)) and past forms ([`up:modal:opened`](/up:modal:opened)). You can usually prevent an action by listening to the present form and call `preventDefault()` on the `event` object: $(document).on('up:modal:open', function(event) { if (event.url == '/evil') { // Prevent the modal from opening event.preventDefault(); } }); A better way to bind event listeners ------------------------------------ Instead of using jQuery to bind an event handler to `document`, you can also use the more convenient [`up.on`](/up.on): up.on('click', 'button', function(event, $button) { // $button is a jQuery collection containing // the clicked