/*** @module up */ (function() { window.up = { version: "2.0.1" }; }).call(this); (function() { up.mockable = function(originalFn) { var obj, spy; spy = null; obj = function() { return (spy || originalFn).apply(null, arguments); }; obj.mock = function() { return spy = jasmine.createSpy('mockable', originalFn); }; document.addEventListener('up:framework:reset', function() { return spy = null; }); return obj; }; }).call(this); /*** Utility functions ================= The `up.util` module contains functions to facilitate the work with basic JavaScript values like lists, strings or functions. You will recognize many functions form other utility libraries like [Lodash](https://lodash.com/). While feature parity with Lodash is not a goal of `up.util`, you might find it sufficient to not include another library in your asset bundle. @module up.util */ (function() { var slice = [].slice, hasProp = {}.hasOwnProperty; up.util = (function() { /*** A function that does nothing. @function up.util.noop @experimental */ var APP_HOSTNAME, APP_PROTOCOL, ESCAPE_HTML_ENTITY_MAP, NORMALIZE_URL_DEFAULTS, SPRINTF_PLACEHOLDERS, abortableMicrotask, allSettled, always, arrayToSet, assign, assignPolyfill, asyncNoop, asyncify, camelToKebabCase, compact, compactObject, contains, copy, copyArrayLike, defineDelegates, defineGetter, each, eachIterator, endsWith, escapeHTML, escapeRegExp, evalOption, every, extractCallback, extractLastArg, extractOptions, filterList, findInList, findResult, flatMap, flatten, identity, intersect, isArguments, isArray, isBasicObjectProperty, isBlank, isBoolean, isCrossOrigin, isDefined, isElement, isElementish, isEqual, isEqualList, isFormData, isFunction, isGiven, isHTMLCollection, isJQuery, isList, isMissing, isNodeList, isNull, isNumber, isObject, isOptions, isPresent, isPromise, isRegExp, isStandardPort, isString, isTruthy, isUndefined, iteratee, last, literal, lowerCaseFirst, map, mapObject, matchURLs, memoize, merge, mergeDefined, methodAllowsPayload, muteRejection, newDeferred, newOptions, nextUid, noop, normalizeMethod, normalizeURL, nullToUndefined, objectContains, objectValues, omit, parseArgIntoOptions, parseURL, pick, pickBy, pluckKey, prefixCamelCase, presence, queueMicrotask, queueTask, reject, remove, renameKey, renameKeys, reverse, scheduleTimer, secondsSinceEpoch, sequence, setToArray, simpleEase, some, splitValues, sprintf, sprintfWithFormattedArgs, stringifyArg, times, toArray, uid, uniq, uniqBy, unprefixCamelCase, unresolvablePromise, upperCaseFirst, urlWithoutHost, valuesPolyfill, wrapList, wrapValue; noop = (function() {}); /*** A function that returns a resolved promise. @function up.util.asyncNoop @internal */ asyncNoop = function() { return Promise.resolve(); }; /*** Ensures that the given function can only be called a single time. Subsequent calls will return the return value of the first call. Note that this is a simple implementation that doesn't distinguish between argument lists. @function up.util.memoize @internal */ memoize = function(func) { var cached, cachedValue; cachedValue = void 0; cached = false; return function() { var args; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; if (cached) { return cachedValue; } else { cached = true; return cachedValue = func.apply(this, 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:'); }; NORMALIZE_URL_DEFAULTS = { host: 'cross-domain', stripTrailingSlash: false, search: true, hash: false }; /*** Normalizes the given URL or path. @function up.util.normalizeURL @param {boolean} [options.host='cross-domain'] Whether to include protocol, hostname and port in the normalized URL. By default the host is only included if it differ's from the page's hostname. @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=false] Whether to strip a trailing slash from the pathname @return {string} The normalized URL. @internal */ normalizeURL = function(urlOrAnchor, options) { var normalized, parts, pathname; options = newOptions(options, NORMALIZE_URL_DEFAULTS); parts = parseURL(urlOrAnchor); normalized = ''; if (options.host === 'cross-domain') { options.host = isCrossOrigin(parts); } if (options.host) { normalized += parts.protocol + "//" + parts.hostname; if (!isStandardPort(parts.protocol, parts.port)) { normalized += ":" + parts.port; } } pathname = parts.pathname; if (options.stripTrailingSlash) { pathname = pathname.replace(/\/$/, ''); } normalized += pathname; if (options.search) { normalized += parts.search; } if (options.hash) { normalized += parts.hash; } return normalized; }; urlWithoutHost = function(url) { return normalizeURL(url, { host: false }); }; matchURLs = function(leftURL, rightURL) { return normalizeURL(leftURL) === normalizeURL(rightURL); }; APP_PROTOCOL = location.protocol; APP_HOSTNAME = location.hostname; isCrossOrigin = function(urlOrAnchor) { var parts; if (isString(urlOrAnchor) && urlOrAnchor.indexOf('//') === -1) { return false; } parts = parseURL(urlOrAnchor); return APP_HOSTNAME !== parts.hostname || APP_PROTOCOL !== parts.protocol; }; /*** 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. \#\#\# Example ```js let parsed = up.util.parseURL('/path?foo=value') parsed.pathname // => '/path' parsed.search // => '/?foo=value' parsed.hash // => '' ``` @function up.util.parseURL @return {Object} The parsed URL as an object with `protocol`, `hostname`, `port`, `pathname`, `search` and `hash` properties. @stable */ parseURL = function(urlOrLink) { var link; if (isJQuery(urlOrLink)) { link = up.element.get(urlOrLink); } else if (urlOrLink.pathname) { link = urlOrLink; } else { link = document.createElement('a'); link.href = urlOrLink; } if (!link.hostname) { link.href = link.href; } if (link.pathname[0] !== '/') { link = pick(link, ['protocol', 'hostname', 'port', 'pathname', 'search', 'hash']); link.pathname = '/' + link.pathname; } return link; }; /*** @function up.util.normalizeMethod @internal */ normalizeMethod = function(method) { if (method) { return method.toUpperCase(); } else { return 'GET'; } }; /*** @function up.util.methodAllowsPayload @internal */ methodAllowsPayload = function(method) { return method !== 'GET' && method !== 'HEAD'; }; assignPolyfill = function() { var j, key, len, source, sources, target, value; target = arguments[0], sources = 2 <= arguments.length ? slice.call(arguments, 1) : []; for (j = 0, len = sources.length; j < len; j++) { source = sources[j]; for (key in source) { if (!hasProp.call(source, key)) continue; value = source[key]; target[key] = value; } } return target; }; /*** Merge the own properties of one or more `sources` into the `target` object. @function up.util.assign @param {Object} target @param {Array} sources... @stable */ assign = Object.assign || assignPolyfill; valuesPolyfill = function(object) { var key, results, value; results = []; for (key in object) { value = object[key]; results.push(value); } return results; }; /*** Returns an array of values of the given object. @function up.util.values @param {Object} object @return {Array} @stable */ objectValues = Object.values || valuesPolyfill; iteratee = function(block) { if (isString(block)) { return function(item) { return item[block]; }; } else { return block; } }; /*** Translate all items in an array to new array of items. @function up.util.map @param {Array} array @param {Function(element, index): any|String} block A function that will be called with each element and (optional) iteration index. You can also pass a property name as a String, which will be collected from each item in the array. @return {Array} A new array containing the result of each function call. @stable */ map = function(array, block) { var index, item, j, len, results; if (array.length === 0) { return []; } block = iteratee(block); results = []; for (index = j = 0, len = array.length; j < len; index = ++j) { item = array[index]; results.push(block(item, index)); } return results; }; /*** @function up.util.mapObject @internal */ mapObject = function(array, pairer) { var merger; merger = function(object, pair) { object[pair[0]] = pair[1]; return object; }; return map(array, pairer).reduce(merger, {}); }; /*** Calls the given function for each element (and, optional, index) of the given array. @function up.util.each @param {Array} array @param {Function(element, index)} block A function that will be called with each element and (optional) iteration index. @stable */ each = map; eachIterator = function(iterator, callback) { var entry, results; results = []; while ((entry = iterator.next()) && !entry.done) { results.push(callback(entry.value)); } return results; }; /*** 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 iteration, j, ref, results; results = []; for (iteration = j = 0, ref = count - 1; 0 <= ref ? j <= ref : j >= ref; iteration = 0 <= ref ? ++j : --j) { 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. By default, this function returns `true` for: - `undefined` - `null` - Empty strings - Empty arrays - A plain object without own enumerable properties All other arguments return `false`. To check implement blank-ness checks for user-defined classes, see `up.util.isBlank.key`. @function up.util.isBlank @param value The value is to check. @return {boolean} Whether the value is blank. @stable */ isBlank = function(value) { if (isMissing(value)) { return true; } if (isObject(value) && value[isBlank.key]) { return value[isBlank.key](); } if (isString(value) || isList(value)) { return value.length === 0; } if (isOptions(value)) { return Object.keys(value).length === 0; } return false; }; /*** This property contains the name of a method that user-defined classes may implement to hook into the `up.util.isBlank()` protocol. \#\#\# Example We have a user-defined `Account` class that we want to use with `up.util.isBlank()`: ``` class Account { constructor(email) { this.email = email } [up.util.isBlank.key]() { return up.util.isBlank(this.email) } } ``` Note that the protocol method is not actually named `'up.util.isBlank.key'`. Instead it is named after the *value* of the `up.util.isBlank.key` property. To do so, the code sample above is using a [computed property name](https://medium.com/front-end-weekly/javascript-object-creation-356e504173a8) in square brackets. We may now use `Account` instances with `up.util.isBlank()`: ``` foo = new Account('foo@foo.com') bar = new Account('') console.log(up.util.isBlank(foo)) // prints false console.log(up.util.isBlank(bar)) // prints true ``` @property up.util.isBlank.key @experimental */ isBlank.key = 'up.util.isBlank'; /*** Returns the given argument if the argument is [present](/up.util.isPresent), otherwise returns `undefined`. @function up.util.presence @param value @param {Function(value): boolean} [tester=up.util.isPresent] The function that will be used to test whether the argument is present. @return {any|undefined} @stable */ presence = function(value, tester) { if (tester == null) { tester = isPresent; } if (tester(value)) { return value; } 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' || object instanceof String; }; /*** Returns whether the given argument is a boolean value. @function up.util.isBoolean @param object @return {boolean} @stable */ isBoolean = function(object) { return typeof object === 'boolean' || object instanceof Boolean; }; /*** 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' || object instanceof Number; }; /*** Returns whether the given argument is an options hash, Differently from [`up.util.isObject()`], this returns false for functions, jQuery collections, promises, `FormData` instances and arrays. @function up.util.isOptions @param object @return {boolean} @internal */ isOptions = function(object) { return typeof object === 'object' && !isNull(object) && (isUndefined(object.constructor) || object.constructor === Object); }; /*** Returns whether the given argument is an object. This also returns `true` for functions, which may behave like objects in JavaScript. @function up.util.isObject @param object @return {boolean} @stable */ isObject = function(object) { var typeOfResult; typeOfResult = typeof object; return (typeOfResult === 'object' && !isNull(object)) || typeOfResult === 'function'; }; /*** Returns whether the given argument is a [DOM element](https://developer.mozilla.org/de/docs/Web/API/Element). @function up.util.isElement @param object @return {boolean} @stable */ isElement = function(object) { return object instanceof Element; }; /*** Returns whether the given argument is a [regular expression](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp). @function up.util.isRegExp @param object @return {boolean} @internal */ isRegExp = function(object) { return object instanceof RegExp; }; /*** Returns whether the given argument is a [jQuery collection](https://learn.jquery.com/using-jquery-core/jquery-object/). @function up.util.isJQuery @param object @return {boolean} @stable */ isJQuery = function(object) { return !!(object != null ? object.jquery : void 0); }; /*** @function up.util.isElementish @param object @return {boolean} @internal */ isElementish = function(object) { var ref; return !!(object && (object.addEventListener || ((ref = object[0]) != null ? ref.addEventListener : void 0))); }; /*** 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 array. @function up.util.isArray @param object @return {boolean} @stable */ isArray = Array.isArray; /*** 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 object instanceof FormData; }; /*** Converts the given [array-like value](/up.util.isList) into an array. If the given value is already an array, it is returned unchanged. @function up.util.toArray @param object @return {Array} @stable */ toArray = function(value) { if (isArray(value)) { return value; } else { return copyArrayLike(value); } }; /*** Returns whether the given argument is an array-like value. Return true for `Array`, a [`NodeList`](https://developer.mozilla.org/en-US/docs/Web/API/NodeList), the [arguments object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/arguments) or a jQuery collection. Use [`up.util.isArray()`](/up.util.isArray) to test whether a value is an actual `Array`. @function up.util.isList @param value @return {boolean} @stable */ isList = function(value) { return isArray(value) || isNodeList(value) || isArguments(value) || isJQuery(value) || isHTMLCollection(value); }; /*** Returns whether the given value is a [`NodeList`](https://developer.mozilla.org/en-US/docs/Web/API/NodeList). `NodeLists` are array-like objects returned by [`document.querySelectorAll()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/querySelectorAll). @function up.util.isNodeList @param value @return {boolean} @internal */ isNodeList = function(value) { return value instanceof NodeList; }; isHTMLCollection = function(value) { return value instanceof HTMLCollection; }; /*** Returns whether the given value is an [arguments object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/arguments). @function up.util.isArguments @param value @return {boolean} @internal */ isArguments = function(value) { return Object.prototype.toString.call(value) === '[object Arguments]'; }; nullToUndefined = function(value) { if (isNull(value)) { return void 0; } else { return value; } }; /*** Returns the given value if it is [array-like](/up.util.isList), otherwise returns an array with the given value as its only element. \#\#\# Example ```js up.util.wrapList([1, 2, 3]) // => [1, 2, 3] up.util.wrapList('foo') // => ['foo'] ``` @function up.util.wrapList @param {any} value @return {Array|NodeList|jQuery} @experimental */ wrapList = function(value) { if (isList(value)) { return value; } else if (isMissing(value)) { return []; } else { return [value]; } }; /*** Returns a shallow copy of the given value. \#\#\# Copying protocol - By default `up.util.copy()` can copy [array-like values](/up.util.isList), plain objects and `Date` instances. - Array-like objects are copied into new arrays. - Unsupported types of values are returned unchanged. - To make the copying protocol work with user-defined class, see `up.util.copy.key`. - Immutable objects, like strings or numbers, do not need to be copied. @function up.util.copy @param {any} object @return {any} @stable */ copy = function(value) { if (isObject(value) && value[copy.key]) { value = value[copy.key](); } else if (isList(value)) { value = copyArrayLike(value); } else if (isOptions(value)) { value = assign({}, value); } return value; }; copyArrayLike = function(arrayLike) { return Array.prototype.slice.call(arrayLike); }; /*** This property contains the name of a method that user-defined classes may implement to hook into the `up.util.copy()` protocol. \#\#\# Example We have a user-defined `Account` class that we want to use with `up.util.copy()`: ``` class Account { constructor(email) { this.email = email } [up.util.copy.key]() { return new Account(this.email) } } ``` Note that the protocol method is not actually named `'up.util.copy.key'`. Instead it is named after the *value* of the `up.util.copy.key` property. To do so, the code sample above is using a [computed property name](https://medium.com/front-end-weekly/javascript-object-creation-356e504173a8) in square brackets. We may now use `Account` instances with `up.util.copy()`: ``` original = new User('foo@foo.com') copy = up.util.copy(original) console.log(copy.email) // prints 'foo@foo.com' original.email = 'bar@bar.com' // change the original console.log(copy.email) // still prints 'foo@foo.com' ``` @property up.util.copy.key @param {string} key @experimental */ copy.key = 'up.util.copy'; Date.prototype[copy.key] = function() { return new Date(+this); }; /*** 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 assign.apply(null, [{}].concat(slice.call(sources))); }; /*** @function up.util.mergeDefined @param {Array} sources... @return Object @internal */ mergeDefined = function() { var j, key, len, result, source, sources, value; sources = 1 <= arguments.length ? slice.call(arguments, 0) : []; result = {}; for (j = 0, len = sources.length; j < len; j++) { source = sources[j]; if (source) { for (key in source) { value = source[key]; if (isDefined(value)) { result[key] = value; } } } } return result; }; /*** 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 */ newOptions = function(object, defaults) { if (defaults) { return merge(defaults, object); } else if (object) { return copy(object); } else { return {}; } }; parseArgIntoOptions = function(args, argKey) { var options; options = extractOptions(args); if (isDefined(args[0])) { options = copy(options); options[argKey] = args[0]; } return options; }; /*** Passes each element in the given [array-like value](/up.util.isList) 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.find @param {List} list @param {Function(value): boolean} tester @return {T|undefined} @stable */ findInList = function(list, tester) { var element, j, len, match; tester = iteratee(tester); match = void 0; for (j = 0, len = list.length; j < len; j++) { element = list[j]; if (tester(element)) { match = element; break; } } return match; }; /*** Returns whether the given function returns a truthy value for any element in the given [array-like value](/up.util.isList). @function up.util.some @param {List} list @param {Function(value, index): boolean} tester A function that will be called with each element and (optional) iteration index. @return {boolean} @stable */ some = function(list, tester) { return !!findResult(list, tester); }; /*** Consecutively calls the given function which each element in the given array. Returns the first truthy return value. Returned `undefined` iff the function does not return a truthy value for any element in the array. @function up.util.findResult @param {Array} array @param {Function(element): any} tester A function that will be called with each element and (optional) iteration index. @return {any|undefined} @experimental */ findResult = function(array, tester) { var element, index, j, len, result; tester = iteratee(tester); for (index = j = 0, len = array.length; j < len; index = ++j) { element = array[index]; if (result = tester(element, index)) { return result; } } return void 0; }; /*** Returns whether the given function returns a truthy value for all elements in the given [array-like value](/up.util.isList). @function up.util.every @param {List} list @param {Function(element, index): boolean} tester A function that will be called with each element and (optional) iteration index. @return {boolean} @experimental */ every = function(list, tester) { var element, index, j, len, match; tester = iteratee(tester); match = true; for (index = j = 0, len = list.length; j < len; index = ++j) { element = list[index]; if (!tester(element, index)) { 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 filterList(array, isGiven); }; compactObject = function(object) { return pickBy(object, isGiven); }; /*** Returns the given array without duplicates. @function up.util.uniq @param {Array} array @return {Array} @stable */ uniq = function(array) { if (array.length < 2) { return array; } return setToArray(arrayToSet(array)); }; /*** This function is like [`uniq`](/up.util.uniq), accept that the given function is invoked for each element to generate the value for which uniquness is computed. @function up.util.uniqBy @param {Array} array @param {Function(value): any} array @return {Array} @experimental */ uniqBy = function(array, mapper) { var set; if (array.length < 2) { return array; } mapper = iteratee(mapper); set = new Set(); return filterList(array, function(elem, index) { var mapped; mapped = mapper(elem, index); if (set.has(mapped)) { return false; } else { set.add(mapped); return true; } }); }; /*** @function up.util.setToArray @internal */ setToArray = function(set) { var array; array = []; set.forEach(function(elem) { return array.push(elem); }); return array; }; /*** @function up.util.arrayToSet @internal */ arrayToSet = function(array) { var set; set = new Set(); array.forEach(function(elem) { return set.add(elem); }); return set; }; /*** Returns all elements from the given [array-like value](/up.util.isList) that return a truthy value when passed to the given function. @function up.util.filter @param {List} list @param {Function(value, index): boolean} tester @return {Array} @stable */ filterList = function(list, tester) { var matches; tester = iteratee(tester); matches = []; each(list, function(element, index) { if (tester(element, index)) { return matches.push(element); } }); return matches; }; /*** Returns all elements from the given [array-like value](/up.util.isList) that do not return a truthy value when passed to the given function. @function up.util.reject @param {List} list @param {Function(element, index): boolean} tester @return {Array} @stable */ reject = function(list, tester) { tester = iteratee(tester); return filterList(list, function(element, index) { return !tester(element, index); }); }; /*** 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 filterList(array1, function(element) { return contains(array2, element); }); }; /*** Waits for the given number of milliseconds, the runs the given callback. Instead of `up.util.timer(0, fn)` you can also use [`up.util.task(fn)`](/up.util.task). @function up.util.timer @param {number} millis @param {Function()} callback @return {number} The ID of the scheduled timeout. You may pass this ID to `clearTimeout()` to un-schedule the timeout. @stable */ scheduleTimer = function(millis, callback) { return setTimeout(callback, millis); }; /*** Pushes the given function to the [JavaScript task queue](https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/) (also "macrotask queue"). Equivalent to calling `setTimeout(fn, 0)`. Also see `up.util.microtask()`. @function up.util.task @param {Function()} block @stable */ queueTask = function(block) { return setTimeout(block, 0); }; /*** Pushes the given function to the [JavaScript microtask queue](https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/). @function up.util.microtask @param {Function()} task @return {Promise} A promise that is resolved with the return value of `task`. If `task` throws an error, the promise is rejected with that error. @experimental */ queueMicrotask = function(task) { return Promise.resolve().then(task); }; abortableMicrotask = function(task) { var aborted; aborted = false; queueMicrotask(function() { if (!aborted) { return task(); } }); return function() { return aborted = true; }; }; /*** Returns the last element of the given array. @function up.util.last @param {Array} array @return {T} @stable */ last = function(array) { return array[array.length - 1]; }; /*** Returns whether the given value contains another value. If `value` is a string, this returns whether `subValue` is a sub-string of `value`. If `value` is an array, this returns whether `subValue` is an element of `value`. @function up.util.contains @param {Array|string} value @param {Array|string} subValue @stable */ contains = function(value, subValue) { return value.indexOf(subValue) >= 0; }; /*** Returns whether `object`'s entries are a superset of `subObject`'s entries. @function up.util.objectContains @param {Object} object @param {Object} subObject @internal */ objectContains = function(object, subObject) { var reducedValue; reducedValue = pick(object, Object.keys(subObject)); return isEqual(subObject, reducedValue); }; /*** Returns a copy of the given object that only contains the given keys. @function up.util.pick @param {Object} object @param {Array} keys @return {Object} @stable */ pick = function(object, keys) { var filtered, j, key, len; filtered = {}; for (j = 0, len = keys.length; j < len; j++) { key = keys[j]; if (key in object) { filtered[key] = object[key]; } } return filtered; }; /*** Returns a copy of the given object that only contains properties that pass the given tester function. @function up.util.pickBy @param {Object} object @param {Function} tester A function that will be called with each property. The arguments are the property value, key and the entire object. @return {Object} @experimental */ pickBy = function(object, tester) { var filtered, key, value; tester = iteratee(tester); filtered = {}; for (key in object) { value = object[key]; if (tester(value, key, object)) { filtered[key] = object[key]; } } return filtered; }; /*** Returns a copy of the given object that contains all except the given keys. @function up.util.omit @param {Object} object @param {Array} keys @stable */ omit = function(object, keys) { return pickBy(object, function(value, key) { return !contains(keys, key); }); }; /*** Returns a promise that will never be resolved. @function up.util.unresolvablePromise @internal */ unresolvablePromise = function() { return new Promise(noop); }; /*** Removes the given element from the given array. This changes the given array. @function up.util.remove @param {Array} array The array to change. @param {T} element The element to remove. @return {T|undefined} The removed element, or `undefined` if the array didn't contain the element. @stable */ remove = function(array, element) { var index; index = array.indexOf(element); if (index >= 0) { array.splice(index, 1); return element; } }; /*** If the given `value` is a function, calls the function with the given `args`. Otherwise it just returns `value`. \#\#\# Example ```js up.util.evalOption(5) // => 5 let fn = () => 1 + 2 up.util.evalOption(fn) // => 3 ``` @function up.util.evalOption @param {any} value @param {Array} ...args @return {any} @experimental */ 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; } }; 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. @stable */ escapeHTML = function(string) { return string.replace(/[&<>"']/g, function(char) { return ESCAPE_HTML_ENTITY_MAP[char]; }); }; /*** @function up.util.escapeRegExp @internal */ escapeRegExp = function(string) { return string.replace(/[\\^$*+?.()|[\]{}]/g, '\\$&'); }; /*** Deletes the property with the given key from the given object and returns its value. @function up.util.pluckKey @param {Object} object @param {string} key @return {any} @experimental */ pluckKey = function(object, key) { var value; value = object[key]; delete object[key]; return value; }; renameKey = function(object, oldKey, newKey) { return object[newKey] = pluckKey(object, oldKey); }; extractLastArg = function(args, tester) { var lastArg; lastArg = last(args); if (tester(lastArg)) { return args.pop(); } }; extractCallback = function(args) { return extractLastArg(args, isFunction); }; extractOptions = function(args) { return extractLastArg(args, isOptions) || {}; }; identity = function(arg) { return arg; }; /*** @function up.util.sequence @param {Array} functions @return {Function()} A function that will call all `functions` if called. @internal */ sequence = function(functions) { if (functions.length === 1) { return functions[0]; } else { return function() { return map(functions, function(f) { return f(); }); }; } }; /*** Flattens the given `array` a single depth level. \#\#\# Example ```js let nested = [1, [2, 3], [4]] up.util.flatten(nested) // => [1, 2, 3, 4] @function up.util.flatten @param {Array} array An array which might contain other arrays @return {Array} The flattened array @experimental */ flatten = function(array) { var flattened, j, len, object; flattened = []; for (j = 0, len = array.length; j < len; j++) { object = array[j]; if (isList(object)) { flattened.push.apply(flattened, object); } else { flattened.push(object); } } return flattened; }; /*** Maps each element using a mapping function, then [flattens](/up.util.flatten) the result into a new array. @function up.util.flatMap @param {Array} array @param {Function(element)} mapping @return {Array} @experimental */ flatMap = function(array, block) { return flatten(map(array, block)); }; /*** Returns whether the given value is truthy. @function up.util.isTruthy @internal */ isTruthy = function(object) { return !!object; }; /*** Sets the given callback as both fulfillment and rejection handler for the given promise. [Unlike `promise#finally()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/finally#Description), `up.util.always()` may change the settlement value of the given promise. @function up.util.always @internal */ always = function(promise, callback) { return promise.then(callback, callback); }; /*** Registers an empty rejection handler with the given promise. This prevents browsers from printing "Uncaught (in promise)" to the error console when the promise is rejected. This is helpful for event handlers where it is clear that no rejection handler will be registered: up.on('submit', 'form[up-target]', (event, $form) => { promise = up.submit($form) up.util.muteRejection(promise) }) Does nothing if passed a missing value. @function up.util.muteRejection @param {Promise|undefined|null} promise @return {Promise} @internal */ muteRejection = function(promise) { return promise != null ? promise["catch"](noop) : void 0; }; /*** @function up.util.newDeferred @internal */ newDeferred = function() { var nativePromise, rejectFn, resolveFn; resolveFn = void 0; rejectFn = void 0; nativePromise = new Promise(function(givenResolve, givenReject) { resolveFn = givenResolve; return rejectFn = givenReject; }); nativePromise.resolve = resolveFn; nativePromise.reject = rejectFn; nativePromise.promise = function() { return nativePromise; }; return nativePromise; }; asyncify = function(block) { var error; try { return Promise.resolve(block()); } catch (error1) { error = error1; return Promise.reject(error); } }; isBasicObjectProperty = function(k) { return Object.prototype.hasOwnProperty(k); }; /*** Returns whether the two arguments are equal by value. \#\#\# Comparison protocol - By default `up.util.isEqual()` can compare strings, numbers, [array-like values](/up.util.isList), plain objects and `Date` objects. - To make the copying protocol work with user-defined classes, see `up.util.isEqual.key`. - Objects without a defined comparison protocol are defined by reference (`===`). @function up.util.isEqual @param {any} a @param {any} b @return {boolean} Whether the arguments are equal by value. @experimental */ isEqual = function(a, b) { var aKeys, bKeys; if (a != null ? a.valueOf : void 0) { a = a.valueOf(); } if (b != null ? b.valueOf : void 0) { b = b.valueOf(); } if (typeof a !== typeof b) { return false; } else if (isList(a) && isList(b)) { return isEqualList(a, b); } else if (isObject(a) && a[isEqual.key]) { return a[isEqual.key](b); } else if (isOptions(a) && isOptions(b)) { aKeys = Object.keys(a); bKeys = Object.keys(b); if (isEqualList(aKeys, bKeys)) { return every(aKeys, function(aKey) { return isEqual(a[aKey], b[aKey]); }); } else { return false; } } else { return a === b; } }; /*** This property contains the name of a method that user-defined classes may implement to hook into the `up.util.isEqual()` protocol. \#\#\# Example We have a user-defined `Account` class that we want to use with `up.util.isEqual()`: ``` class Account { constructor(email) { this.email = email } [up.util.isEqual.key](other) { return this.email === other.email; } } ``` Note that the protocol method is not actually named `'up.util.isEqual.key'`. Instead it is named after the *value* of the `up.util.isEqual.key` property. To do so, the code sample above is using a [computed property name](https://medium.com/front-end-weekly/javascript-object-creation-356e504173a8) in square brackets. We may now use `Account` instances with `up.util.isEqual()`: ``` one = new User('foo@foo.com') two = new User('foo@foo.com') three = new User('bar@bar.com') isEqual = up.util.isEqual(one, two) // isEqual is now true isEqual = up.util.isEqual(one, three) // isEqual is now false ``` @property up.util.isEqual.key @param {string} key @experimental */ isEqual.key = 'up.util.isEqual'; isEqualList = function(a, b) { return a.length === b.length && every(a, function(elem, index) { return isEqual(elem, b[index]); }); }; splitValues = function(value, separator) { if (separator == null) { separator = ' '; } if (isString(value)) { value = value.split(separator); value = map(value, function(v) { return v.trim(); }); value = filterList(value, isPresent); return value; } else { return wrapList(value); } }; endsWith = function(string, search) { if (search.length > string.length) { return false; } else { return string.substring(string.length - search.length) === search; } }; simpleEase = function(x) { if (x < 0.5) { return 2 * x * x; } else { return x * (4 - x * 2) - 1; } }; wrapValue = function() { var args, constructor; constructor = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : []; if (args[0] instanceof constructor) { return args[0]; } else { return (function(func, args, ctor) { ctor.prototype = func.prototype; var child = new ctor, result = func.apply(child, args); return Object(result) === result ? result : child; })(constructor, args, function(){}); } }; nextUid = 0; uid = function() { return nextUid++; }; /*** Returns a copy of the given list, in reversed order. @function up.util.reverse @param {List} list @return {Array} @internal */ reverse = function(list) { return copy(list).reverse(); }; renameKeys = function(object, renameKeyFn) { var key, renamed, value; renamed = {}; for (key in object) { value = object[key]; renamed[renameKeyFn(key)] = value; } return renamed; }; camelToKebabCase = function(str) { return str.replace(/[A-Z]/g, function(char) { return '-' + char.toLowerCase(); }); }; prefixCamelCase = function(str, prefix) { return prefix + upperCaseFirst(str); }; unprefixCamelCase = function(str, prefix) { var match, pattern; pattern = new RegExp('^' + prefix + '(.+)$'); if (match = str.match(pattern)) { return lowerCaseFirst(match[1]); } }; lowerCaseFirst = function(str) { return str[0].toLowerCase() + str.slice(1); }; upperCaseFirst = function(str) { return str[0].toUpperCase() + str.slice(1); }; defineGetter = function(object, prop, get) { return Object.defineProperty(object, prop, { get: get }); }; defineDelegates = function(object, props, targetProvider) { return wrapList(props).forEach(function(prop) { return Object.defineProperty(object, prop, { get: function() { var target, value; target = targetProvider.call(this); value = target[prop]; if (isFunction(value)) { value = value.bind(target); } return value; }, set: function(newValue) { var target; target = targetProvider.call(this); return target[prop] = newValue; } }); }); }; literal = function(obj) { var key, result, unprefixedKey, value; result = {}; for (key in obj) { value = obj[key]; if (unprefixedKey = unprefixCamelCase(key, 'get_')) { defineGetter(result, unprefixedKey, value); } else { result[key] = value; } } return result; }; stringifyArg = function(arg) { var attr, closer, error, j, len, maxLength, ref, string, value; maxLength = 200; closer = ''; if (isString(arg)) { string = arg.replace(/[\n\r\t ]+/g, ' '); string = string.replace(/^[\n\r\t ]+/, ''); string = string.replace(/[\n\r\t ]$/, ''); } else if (isUndefined(arg)) { string = 'undefined'; } else if (isNumber(arg) || isFunction(arg)) { string = arg.toString(); } else if (isArray(arg)) { string = "[" + (map(arg, stringifyArg).join(', ')) + "]"; closer = ']'; } else if (isJQuery(arg)) { string = "$(" + (map(arg, stringifyArg).join(', ')) + ")"; closer = ')'; } else if (isElement(arg)) { string = "<" + (arg.tagName.toLowerCase()); ref = ['id', 'name', 'class']; for (j = 0, len = ref.length; j < len; j++) { attr = ref[j]; if (value = arg.getAttribute(attr)) { string += " " + attr + "=\"" + value + "\""; } } string += ">"; closer = '>'; } else if (isRegExp(arg)) { string = arg.toString(); } else { try { string = JSON.stringify(arg); } catch (error1) { error = error1; if (error.name === 'TypeError') { string = '(circular structure)'; } else { throw error; } } } if (string.length > maxLength) { string = (string.substr(0, maxLength)) + " …"; string += closer; } return string; }; SPRINTF_PLACEHOLDERS = /\%[oOdisf]/g; secondsSinceEpoch = function() { return Math.floor(Date.now() * 0.001); }; /*** See https://developer.mozilla.org/en-US/docs/Web/API/Console#Using_string_substitutions @function up.util.sprintf @internal */ sprintf = function() { var args, message; message = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : []; return sprintfWithFormattedArgs.apply(null, [identity, message].concat(slice.call(args))); }; /*** @function up.util.sprintfWithFormattedArgs @internal */ sprintfWithFormattedArgs = function() { var args, formatter, i, message; formatter = arguments[0], message = arguments[1], args = 3 <= arguments.length ? slice.call(arguments, 2) : []; if (!message) { return ''; } i = 0; return message.replace(SPRINTF_PLACEHOLDERS, function() { var arg; arg = args[i]; arg = formatter(stringifyArg(arg)); i += 1; return arg; }); }; allSettled = function(promises) { return Promise.all(map(promises, muteRejection)); }; return { parseURL: parseURL, normalizeURL: normalizeURL, urlWithoutHost: urlWithoutHost, matchURLs: matchURLs, normalizeMethod: normalizeMethod, methodAllowsPayload: methodAllowsPayload, assign: assign, assignPolyfill: assignPolyfill, copy: copy, copyArrayLike: copyArrayLike, merge: merge, mergeDefined: mergeDefined, options: newOptions, parseArgIntoOptions: parseArgIntoOptions, each: each, eachIterator: eachIterator, map: map, flatMap: flatMap, mapObject: mapObject, times: times, findResult: findResult, some: some, every: every, find: findInList, filter: filterList, reject: reject, intersect: intersect, compact: compact, compactObject: compactObject, uniq: uniq, uniqBy: uniqBy, last: last, isNull: isNull, isDefined: isDefined, isUndefined: isUndefined, isGiven: isGiven, isMissing: isMissing, isPresent: isPresent, isBlank: isBlank, presence: presence, isObject: isObject, isFunction: isFunction, isString: isString, isBoolean: isBoolean, isNumber: isNumber, isElement: isElement, isJQuery: isJQuery, isElementish: isElementish, isPromise: isPromise, isOptions: isOptions, isArray: isArray, isFormData: isFormData, isNodeList: isNodeList, isArguments: isArguments, isList: isList, isRegExp: isRegExp, timer: scheduleTimer, contains: contains, objectContains: objectContains, toArray: toArray, pick: pick, pickBy: pickBy, omit: omit, unresolvablePromise: unresolvablePromise, remove: remove, memoize: memoize, pluckKey: pluckKey, renameKey: renameKey, extractOptions: extractOptions, extractCallback: extractCallback, noop: noop, asyncNoop: asyncNoop, identity: identity, escapeHTML: escapeHTML, escapeRegExp: escapeRegExp, sequence: sequence, evalOption: evalOption, flatten: flatten, isTruthy: isTruthy, newDeferred: newDeferred, always: always, muteRejection: muteRejection, asyncify: asyncify, isBasicObjectProperty: isBasicObjectProperty, isCrossOrigin: isCrossOrigin, task: queueTask, microtask: queueMicrotask, abortableMicrotask: abortableMicrotask, isEqual: isEqual, splitValues: splitValues, endsWith: endsWith, wrapList: wrapList, wrapValue: wrapValue, simpleEase: simpleEase, values: objectValues, arrayToSet: arrayToSet, setToArray: setToArray, uid: uid, upperCaseFirst: upperCaseFirst, lowerCaseFirst: lowerCaseFirst, getter: defineGetter, delegate: defineDelegates, literal: literal, reverse: reverse, prefixCamelCase: prefixCamelCase, unprefixCamelCase: unprefixCamelCase, camelToKebabCase: camelToKebabCase, nullToUndefined: nullToUndefined, sprintf: sprintf, sprintfWithFormattedArgs: sprintfWithFormattedArgs, renameKeys: renameKeys, timestamp: secondsSinceEpoch, allSettled: allSettled }; })(); }).call(this); (function() { var slice = [].slice; up.error = (function() { var aborted, build, emitGlobal, errorInterface, failed, invalidSelector, notApplicable, notImplemented, u; u = up.util; build = function(message, props) { var error; if (props == null) { props = {}; } if (u.isArray(message)) { message = u.sprintf.apply(u, message); } error = new Error(message); u.assign(error, props); return error; }; errorInterface = function(name, init) { var fn; if (init == null) { init = build; } fn = function() { var args, error; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; error = init.apply(null, args); error.name = name; return error; }; fn.is = function(error) { return error.name === name; }; fn.async = function() { var args; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; return Promise.reject(fn.apply(null, args)); }; return fn; }; failed = errorInterface('up.Failed'); aborted = errorInterface('AbortError', function(message) { return build(message || 'Aborted'); }); notImplemented = errorInterface('up.NotImplemented'); notApplicable = errorInterface('up.NotApplicable', function(change, reason) { return build("Cannot apply change: " + change + " (" + reason + ")"); }); invalidSelector = errorInterface('up.InvalidSelector', function(selector) { return build("Cannot parse selector: " + selector); }); emitGlobal = function(error) { var message; message = error.message; return up.emit(window, 'error', { message: message, error: error, log: false }); }; return { failed: failed, aborted: aborted, invalidSelector: invalidSelector, notApplicable: notApplicable, notImplemented: notImplemented, emitGlobal: emitGlobal }; })(); }).call(this); (function() { up.migrate = { config: {} }; }).call(this); /*** Browser support =============== Unpoly supports all modern browsers. Chrome, Firefox, Edge, Safari : Full support Internet Explorer 11 : Full support with a `Promise` polyfill like [es6-promise](https://github.com/stefanpenner/es6-promise) (2.4 KB).\ Support may be removed when Microsoft retires IE11 in [June 2022](https://blogs.windows.com/windowsexperience/2021/05/19/the-future-of-internet-explorer-on-windows-10-is-in-microsoft-edge/). Internet Explorer 10 or lower : Unpoly will not boot or [run compilers](/up.compiler), leaving you with a classic server-side application. @module up.browser */ (function() { var slice = [].slice; up.browser = (function() { var assertConfirmed, callJQuery, canFormatLog, canJQuery, canPassiveEventListener, canPromise, canPushState, isIE11, isSupported, loadPage, popCookie, submitForm, u; u = up.util; /*** Makes a full-page request, replacing the entire browser environment with a new page from the server response. Also see `up.Request#loadPage()`. @function up.browser.loadPage @param {string} options.url The URL to load. @param {string} [options.method='get'] The method for the request. Methods other than GET or POST will be [wrapped](/up.protocol.config#config.methodParam) in a POST request. @param {Object|Array|FormData|string} [options.params] @experimental */ loadPage = function(requestsAttrs) { return new up.Request(requestsAttrs).loadPage(); }; /*** Submits the given form with a full page load. For mocking in specs. @function up.browser.submitForm @internal */ submitForm = function(form) { return form.submit(); }; isIE11 = u.memoize(function() { return 'ActiveXObject' in window; }); /*** 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 `pushState` (e.g. through [`up.follow()`](/up.follow)), it will gracefully fall back to a full page load. Note that Unpoly will not use `pushState` if the initial page was loaded with a request method other than GET. @function up.browser.canPushState @return {boolean} @internal */ canPushState = function() { return history.pushState && up.protocol.initialRequestMethod() === 'GET'; }; /*** Returns whether this browser supports promises. @function up.browser.canPromise @return {boolean} @internal */ canPromise = u.memoize(function() { return !!window.Promise; }); canFormatLog = u.memoize(function() { return !isIE11(); }); canPassiveEventListener = u.memoize(function() { return !isIE11(); }); canJQuery = function() { return !!window.jQuery; }; popCookie = function(name) { var ref, value; if (value = (ref = document.cookie.match(new RegExp(name + "=(\\w+)"))) != null ? ref[1] : void 0) { document.cookie = name + '=; expires=Thu, 01-Jan-70 00:00:01 GMT; path=/'; } return value; }; /*** @return {Promise} @function up,browser.ensureConfirmed @param {string} options.confirm @param {boolean} options.preload @internal */ assertConfirmed = function(options) { return !options.confirm || window.confirm(options.confirm) || (function() { throw up.error.aborted('User canceled action'); })(); }; /*** Returns whether Unpoly supports the current browser. If this returns `false` 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. This is usually a better fallback than loading incompatible Javascript and causing many errors on load. @function up.browser.isSupported @stable */ isSupported = function() { return canPromise(); }; callJQuery = function() { var args; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; canJQuery() || up.fail("jQuery must be published as window.jQuery"); return jQuery.apply(null, args); }; return u.literal({ loadPage: loadPage, submitForm: submitForm, canPushState: canPushState, canFormatLog: canFormatLog, canPassiveEventListener: canPassiveEventListener, canJQuery: canJQuery, assertConfirmed: assertConfirmed, isSupported: isSupported, popCookie: popCookie, jQuery: callJQuery, isIE11: isIE11 }); })(); }).call(this); /*** DOM helpers =========== The `up.element` module offers functions for DOM manipulation and traversal. It complements [native `Element` methods](https://www.w3schools.com/jsref/dom_obj_all.asp) and works across all [supported browsers](/up.browser). @module up.element */ (function() { var slice = [].slice; up.element = (function() { var CSS_LENGTH_PROPS, MATCH_FN_NAME, SINGLETON_PATTERN, SINGLETON_TAG_NAMES, affix, all, ancestor, around, attributeSelector, booleanAttr, booleanOrStringAttr, callbackAttr, classSelector, closest, closestAttr, computedStyle, computedStyleNumber, concludeCSSTransition, createDocumentFromHTML, createFromHTML, createFromSelector, cssLength, elementTagName, extractFromStyleObject, first, fixedToAbsolute, getList, getOne, getRoot, hasCSSTransition, hide, idSelector, inlineStyle, insertBefore, isDetached, isInSubtree, isSingleton, isSingletonSelector, isVisible, jsonAttr, matches, metaContent, normalizeStyleValueForWrite, numberAttr, paint, remove, replace, setAttrs, setInlineStyle, setMissingAttr, setMissingAttrs, setTemporaryAttrs, setTemporaryStyle, show, stringAttr, subtree, toSelector, toggle, toggleAttr, toggleClass, trueAttributeSelector, u, unwrap, upAttrs, valueToList, wrapChildren; u = up.util; MATCH_FN_NAME = up.browser.isIE11() ? 'msMatchesSelector' : 'matches'; /*** Returns the first descendant element matching the given selector. @function first @param {Element} [parent=document] The parent element whose descendants to search. If omitted, all elements in the `document` will be searched. @param {string} selector The CSS selector to match. @return {Element|undefined|null} The first element matching the selector. Returns `null` or `undefined` if no element macthes. @internal */ first = function() { var args, root, selector; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; selector = args.pop(); root = args[0] || document; return root.querySelector(selector); }; /*** Returns all descendant elements matching the given selector. @function up.element.all @param {Element} [parent=document] The parent element whose descendants to search. If omitted, all elements in the `document` will be searched. @param {string} selector The CSS selector to match. @return {NodeList|Array} A list of all elements matching the selector. Returns an empty list if there are no matches. @stable */ all = function() { var args, root, selector; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; selector = args.pop(); root = args[0] || document; return root.querySelectorAll(selector); }; /*** Returns a list of the given parent's descendants matching the given selector. The list will also include the parent element if it matches the selector itself. @function up.element.subtree @param {Element} parent The parent element for the search. @param {string} selector The CSS selector to match. @return {NodeList|Array} A list of all matching elements. @stable */ subtree = function(root, selector) { var results; results = []; if (matches(root, selector)) { results.push(root); } results.push.apply(results, all(root, selector)); return results; }; /*** Returns whether the given element is either the given root element or its descendants. @function isInSubtree @internal */ isInSubtree = function(root, selectorOrElement) { var element; element = getOne(selectorOrElement); return root.contains(element); }; /*** Returns the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree. @function up.element.closest @param {Element} element The element on which to start the search. @param {string} selector The CSS selector to match. @return {Element|null|undefined} element The matching element. Returns `null` or `undefined` if no element matches. @stable */ closest = function(element, selector) { if (element.closest) { return element.closest(selector); } else if (matches(element, selector)) { return element; } else { return ancestor(element, selector); } }; /*** Returns whether the given element matches the given CSS selector. To match against a non-standard selector like `:main`, use `up.fragment.matches()` instead. @function up.element.matches @param {Element} element The element to check. @param {string} selector The CSS selector to match. @return {boolean} Whether `element` matches `selector`. @stable */ matches = function(element, selector) { return typeof element[MATCH_FN_NAME] === "function" ? element[MATCH_FN_NAME](selector) : void 0; }; /*** @function up.element.ancestor @internal */ ancestor = function(element, selector) { var parentElement; if (parentElement = element.parentElement) { if (matches(parentElement, selector)) { return parentElement; } else { return ancestor(parentElement, selector); } } }; around = function(element, selector) { return getList(closest(element, selector), subtree(element, selector)); }; /*** Returns the native [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) for the given value. \#\#\# Casting rules - If given an element, returns that element. - If given a CSS selector string, returns the first element matching that selector. - If given a jQuery collection , returns the first element in the collection. Throws an error if the collection contains more than one element. - If given any other argument (`undefined`, `null`, `document`, `window`…), returns the argument unchanged. @function up.element.get @param {Element} [parent=document] The parent element whose descendants to search if `value` is a CSS selector string. If omitted, all elements in the `document` will be searched. @param {Element|jQuery|string} value The value to look up. @return {Element} The obtained `Element`. @stable */ getOne = function() { var args, value; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; value = args.pop(); if (u.isElement(value)) { return value; } else if (u.isString(value)) { return first.apply(null, slice.call(args).concat([value])); } else if (u.isList(value)) { if (value.length > 1) { up.fail('up.element.get(): Cannot cast multiple elements (%o) to a single element', value); } return value[0]; } else { return value; } }; /*** Composes a list of elements from the given arguments. \#\#\# Casting rules - If given a string, returns the all elements matching that string. - If given any other argument, returns the argument [wrapped as a list](/up.util.wrapList). \#\#\# Example ```javascript $jquery = $('.jquery') // returns jQuery (2) [div.jquery, div.jquery] nodeList = document.querySelectorAll('.node') // returns NodeList (2) [div.node, div.node] element = document.querySelector('.element') // returns Element div.element selector = '.selector' // returns String '.selector' elements = up.element.list($jquery, nodeList, undefined, element, selector) // returns [div.jquery, div.jquery, div.node, div.node, div.element, div.selector] ``` @function up.element.list @param {Array|String|undefined|null>} ...args @return {Array} @internal */ getList = function() { var args; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; return u.flatMap(args, valueToList); }; valueToList = function(value) { if (u.isString(value)) { return all(value); } else { return u.wrapList(value); } }; /*** Removes the given element from the DOM tree. If you don't need IE11 support you may also use the built-in [`Element#remove()`](https://developer.mozilla.org/en-US/docs/Web/API/ChildNode/remove) to the same effect. @function up.element.remove @param {Element} element The element to remove. @stable */ remove = function(element) { var parent; if (element.remove) { return element.remove(); } else if (parent = element.parentNode) { return parent.removeChild(element); } }; /*** Hides the given element. The element is hidden by setting an [inline style](https://www.codecademy.com/articles/html-inline-styles) of `{ display: none }`. Also see `up.element.show()`. @function up.element.hide @param {Element} element @stable */ hide = function(element) { return element.style.display = 'none'; }; /*** Shows the given element. Also see `up.element.hide()`. \#\#\# Limitations The element is shown by setting an [inline style](https://www.codecademy.com/articles/html-inline-styles) of `{ display: '' }`. You might have CSS rules causing the element to remain hidden after calling `up.element.show(element)`. Unpoly will not handle such cases in order to keep this function performant. As a workaround, you may manually set the `element.style.display` property. Also see discussion in jQuery issues [#88](https://github.com/jquery/jquery.com/issues/88), [#2057](https://github.com/jquery/jquery/issues/2057) and [this WHATWG mailing list post](http://lists.w3.org/Archives/Public/public-whatwg-archive/2014Apr/0094.html). @function up.element.show @param {Element} element @stable */ show = function(element) { return element.style.display = ''; }; /*** Display or hide the given element, depending on its current visibility. @function up.element.toggle @param {Element} element @param {boolean} [newVisible] Pass `true` to show the element or `false` to hide it. If omitted, the element will be hidden if shown and shown if hidden. @stable */ toggle = function(element, newVisible) { if (newVisible == null) { newVisible = !isVisible(element); } if (newVisible) { return show(element); } else { return hide(element); } }; /*** Adds or removes the given class from the given element. If you don't need IE11 support you may also use the built-in [`Element#classList.toggle(className)`](https://developer.mozilla.org/en-US/docs/Web/API/Element/classList) to the same effect. @function up.element.toggleClass @param {Element} element The element for which to add or remove the class. @param {string} className The class which should be added or removed. @param {Boolean} [newPresent] Pass `true` to add the class to the element or `false` to remove it. If omitted, the class will be added if missing and removed if present. @stable */ toggleClass = function(element, klass, newPresent) { var list; list = element.classList; if (newPresent == null) { newPresent = !list.contains(klass); } if (newPresent) { return list.add(klass); } else { return list.remove(klass); } }; toggleAttr = function(element, attr, value, newPresent) { if (newPresent == null) { newPresent = !element.hasAttribute(attr); } if (newPresent) { return element.setAttribute(attr, value); } else { return element.removeAttribute(attr); } }; /*** Sets all key/values from the given object as attributes on the given element. \#\#\# Example up.element.setAttrs(element, { title: 'Tooltip', tabindex: 1 }) @function up.element.setAttrs @param {Element} element The element on which to set attributes. @param {Object} attributes An object of attributes to set. @stable */ setAttrs = function(element, attrs) { var key, results1, value; results1 = []; for (key in attrs) { value = attrs[key]; if (u.isGiven(value)) { results1.push(element.setAttribute(key, value)); } else { results1.push(element.removeAttribute(key)); } } return results1; }; setTemporaryAttrs = function(element, attrs) { var i, key, len, oldAttrs, ref; oldAttrs = {}; ref = Object.keys(attrs); for (i = 0, len = ref.length; i < len; i++) { key = ref[i]; oldAttrs[key] = element.getAttribute(key); } setAttrs(element, attrs); return function() { return setAttrs(element, oldAttrs); }; }; /*** @function up.element.metaContent @internal */ metaContent = function(name) { var ref, selector; selector = "meta" + attributeSelector('name', name); return (ref = first(selector)) != null ? ref.getAttribute('content') : void 0; }; /*** @function up.element.insertBefore @internal */ insertBefore = function(existingElement, newElement) { return existingElement.insertAdjacentElement('beforebegin', newElement); }; /*** Replaces the given old element with the given new element. The old element will be removed from the DOM tree. If you don't need IE11 support you may also use the built-in [`Element#replaceWith()`](https://developer.mozilla.org/en-US/docs/Web/API/ChildNode/replaceWith) to the same effect. @function up.element.replace @param {Element} oldElement @param {Element} newElement @stable */ replace = function(oldElement, newElement) { return oldElement.parentElement.replaceChild(newElement, oldElement); }; /*** Creates an element matching the given CSS selector. The created element will not yet be attached to the DOM tree. Attach it with [`Element#appendChild()`](https://developer.mozilla.org/en-US/docs/Web/API/Node/appendChild) or use `up.element.affix()` to create an attached element. Use `up.hello()` to activate JavaScript behavior within the created element. \#\#\# Examples To create an element with a given tag name: element = up.element.createFromSelector('span') // element is To create an element with a given class: element = up.element.createFromSelector('.klass') // element is
To create an element with a given ID: element = up.element.createFromSelector('#foo') // element is
To create an element with a given boolean attribute: element = up.element.createFromSelector('[attr]') // element is
To create an element with a given attribute value: element = up.element.createFromSelector('[attr="value"]') // element is
You may also pass an object of attribute names/values as a second argument: element = up.element.createFromSelector('div', { attr: 'value' }) // element is
You may set the element's inner text by passing a `{ text }` option (HTML control characters will be escaped): element = up.element.createFromSelector('div', { text: 'inner text' }) // element is
inner text
You may set the element's inner HTML by passing a `{ content }` option: element = up.element.createFromSelector('div', { content: 'inner text' }) // element is
inner text
You may set inline styles by passing an object of CSS properties as a second argument: element = up.element.createFromSelector('div', { style: { color: 'red' }}) // element is
@function up.element.createFromSelector @param {string} selector The CSS selector from which to create an element. @param {Object} [attrs] An object of attributes to set on the created element. @param {Object} [attrs.text] The [text content](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent) of the created element. @param {Object} [attrs.content] The [inner HTML](https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML) of the created element. @param {Object} [attrs.style] An object of CSS properties that will be set as the inline style of the created element. The given object may use kebab-case or camelCase keys. @return {Element} The created element. @stable */ createFromSelector = function(selector, attrs) { var attrValues, classValue, contentValue, depthElement, depthSelector, depths, i, j, klass, len, len1, previousElement, ref, rootElement, selectorWithoutAttrValues, styleValue, tagName, textValue; attrValues = []; selectorWithoutAttrValues = selector.replace(/\[([\w-]+)(?:[\~\|\^\$\*]?=(["'])?([^\2\]]*?)\2)?\]/g, function(_match, attrName, _quote, attrValue) { attrValues.push(attrValue || ''); return "[" + attrName + "]"; }); depths = selectorWithoutAttrValues.split(/[ >]+/); rootElement = void 0; depthElement = void 0; previousElement = void 0; for (i = 0, len = depths.length; i < len; i++) { depthSelector = depths[i]; tagName = void 0; depthSelector = depthSelector.replace(/^[\w-]+/, function(match) { tagName = match; return ''; }); depthElement = document.createElement(tagName || 'div'); rootElement || (rootElement = depthElement); depthSelector = depthSelector.replace(/\#([\w-]+)/, function(_match, id) { depthElement.id = id; return ''; }); depthSelector = depthSelector.replace(/\.([\w-]+)/g, function(_match, className) { depthElement.classList.add(className); return ''; }); if (attrValues.length) { depthSelector = depthSelector.replace(/\[([\w-]+)\]/g, function(_match, attrName) { depthElement.setAttribute(attrName, attrValues.shift()); return ''; }); } if (depthSelector !== '') { throw up.error.invalidSelector(selector); } if (previousElement != null) { previousElement.appendChild(depthElement); } previousElement = depthElement; } if (attrs) { if (classValue = u.pluckKey(attrs, 'class')) { ref = u.wrapList(classValue); for (j = 0, len1 = ref.length; j < len1; j++) { klass = ref[j]; rootElement.classList.add(klass); } } if (styleValue = u.pluckKey(attrs, 'style')) { setInlineStyle(rootElement, styleValue); } if (textValue = u.pluckKey(attrs, 'text')) { rootElement.textContent = textValue; } if (contentValue = u.pluckKey(attrs, 'content')) { rootElement.innerHTML = contentValue; } setAttrs(rootElement, attrs); } return rootElement; }; /*** Creates an element matching the given CSS selector and attaches it to the given parent element. To create a detached element from a selector, see `up.element.createFromSelector()`. Use `up.hello()` to activate JavaScript behavior within the created element. \#\#\# Example ```js element = up.element.affix(document.body, '.klass') element.parentElement // returns document.body element.className // returns 'klass' ``` @function up.element.affix @param {Element} parent The parent to which to attach the created element. @param {string} [position='beforeend'] The position of the new element in relation to `parent`. Can be one of the following values: - `'beforebegin'`: Before `parent`, as a new sibling. - `'afterbegin'`: Just inside `parent`, before its first child. - `'beforeend'`: Just inside `parent`, after its last child. - `'afterend'`: After `parent`, as a new sibling. @param {string} selector The CSS selector from which to create an element. @param {Object} attrs An object of attributes to set on the created element. @param {Object} attrs.text The [text content](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent) of the created element. @param {Object} attrs.style An object of CSS properties that will be set as the inline style of the created element. The given object may use kebab-case or camelCase keys. @return {Element} The created element. @stable */ affix = function() { var args, attributes, element, parent, position, selector; parent = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : []; attributes = u.extractOptions(args); if (args.length === 2) { position = args[0], selector = args[1]; } else { position = 'beforeend'; selector = args[0]; } element = createFromSelector(selector, attributes); parent.insertAdjacentElement(position, element); return element; }; /*** Returns a CSS selector that matches the given element as good as possible. Alias for `up.fragment.toTarget()`. @function up.element.toSelector @param {string|Element|jQuery} The element for which to create a selector. @stable */ toSelector = function() { var args, ref; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; return (ref = up.fragment).toTarget.apply(ref, args); }; SINGLETON_TAG_NAMES = ['HTML', 'BODY', 'HEAD', 'TITLE']; SINGLETON_PATTERN = new RegExp('\\b(' + SINGLETON_TAG_NAMES.join('|') + ')\\b', 'i'); /*** @function up.element.isSingleton @internal */ isSingleton = up.mockable(function(element) { return matches(element, SINGLETON_TAG_NAMES.join(',')); }); isSingletonSelector = function(selector) { return SINGLETON_PATTERN.test(selector); }; elementTagName = function(element) { return element.tagName.toLowerCase(); }; /*** @function up.element.attributeSelector @internal */ attributeSelector = function(attribute, value) { value = value.replace(/"/g, '\\"'); return "[" + attribute + "=\"" + value + "\"]"; }; trueAttributeSelector = function(attribute) { return "[" + attribute + "]:not([" + attribute + "=false])"; }; idSelector = function(id) { if (id.match(/^[a-z0-9\-_]+$/i)) { return "#" + id; } else { return attributeSelector('id', id); } }; /*** @function up.element.classSelector @internal */ classSelector = function(klass) { klass = klass.replace(/:/g, '\\:'); return "." + klass; }; /*** Always creates a full document with a root, even if the given `html` is only a fragment. @function up.element.createDocumentFromHTML @internal */ createDocumentFromHTML = function(html) { var parser; parser = new DOMParser(); return parser.parseFromString(html, 'text/html'); }; /*** Creates an element from the given HTML fragment. Use `up.hello()` to activate JavaScript behavior within the created element. \#\#\# Example ```js element = up.element.createFromHTML('
text
') element.className // returns 'foo' element.children[0] // returns element element.children[0].textContent // returns 'text' ``` @function up.element.createFromHTML @stable */ createFromHTML = function(html) { var fragment, range; range = document.createRange(); range.setStart(document.body, 0); fragment = range.createContextualFragment(html); return fragment.childNodes[0]; }; /*** @function up.element.root @internal */ getRoot = function() { return document.documentElement; }; /*** Forces the browser to paint the given element now. @function up.element.paint @internal */ paint = function(element) { return element.offsetHeight; }; /*** @function up.element.concludeCSSTransition @internal */ concludeCSSTransition = function(element) { var undo; undo = setTemporaryStyle(element, { transition: 'none' }); paint(element); return undo; }; /*** Returns whether the given element has a CSS transition set. @function up.element.hasCSSTransition @return {boolean} @internal */ hasCSSTransition = function(elementOrStyleHash) { var duration, noTransition, prop, styleHash; if (u.isOptions(elementOrStyleHash)) { styleHash = elementOrStyleHash; } else { styleHash = computedStyle(elementOrStyleHash); } prop = styleHash.transitionProperty; duration = styleHash.transitionDuration; noTransition = prop === 'none' || (prop === 'all' && duration === 0); return !noTransition; }; /*** @function up.element.fixedToAbsolute @internal */ fixedToAbsolute = function(element) { var elementRectAsFixed, offsetParentRect; elementRectAsFixed = element.getBoundingClientRect(); element.style.position = 'absolute'; offsetParentRect = element.offsetParent.getBoundingClientRect(); return setInlineStyle(element, { left: elementRectAsFixed.left - computedStyleNumber(element, 'margin-left') - offsetParentRect.left, top: elementRectAsFixed.top - computedStyleNumber(element, 'margin-top') - offsetParentRect.top, right: '', bottom: '' }); }; /*** On the given element, set attributes that are still missing. @function up.element.setMissingAttrs @internal */ setMissingAttrs = function(element, attrs) { var key, results1, value; results1 = []; for (key in attrs) { value = attrs[key]; results1.push(setMissingAttr(element, key, value)); } return results1; }; setMissingAttr = function(element, key, value) { if (u.isMissing(element.getAttribute(key))) { return element.setAttribute(key, value); } }; /*** @function up.element.unwrap @internal */ unwrap = function(wrapper) { var parent, wrappedNodes; parent = wrapper.parentNode; wrappedNodes = u.toArray(wrapper.childNodes); u.each(wrappedNodes, function(wrappedNode) { return parent.insertBefore(wrappedNode, wrapper); }); return parent.removeChild(wrapper); }; wrapChildren = function(element, wrapperSelector) { var childNode, wrapper; if (wrapperSelector == null) { wrapperSelector = 'up-wrapper'; } wrapper = createFromSelector(wrapperSelector); while (childNode = element.firstChild) { wrapper.appendChild(childNode); } element.appendChild(wrapper); return wrapper; }; /*** Returns the given `attribute` value for the given `element`. If the element does not have the given attribute, it returns `undefined`. This is a difference to the native `Element#getAttribute()`, which [mostly returns `null` in that case](https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute#Non-existing_attributes). If the element has the attribute but without value (e.g. ''>), it returns an empty string. @function up.element.attr @stable */ stringAttr = function(element, attribute) { return u.nullToUndefined(element.getAttribute(attribute)); }; /*** Returns the value of the given attribute on the given element, cast as a boolean value. If the attribute value cannot be cast to `true` or `false`, `undefined` is returned. \#\#\# Casting rules This function deviates from the [HTML Standard for boolean attributes](https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#boolean-attributes) in order to allow `undefined` values. When an attribute is missing, Unpoly considers the value to be `undefined` (where the standard would assume `false`). Unpoly also allows `"true"` and `"false"` as attribute values. The table below shows return values for `up.element.booleanAttr(element, 'foo')` given different elements: | Element | Return value | |---------------------|--------------| | `
` | `true` | | `
` | `true` | | `
` | `true` | | `
` | `true` | | `
` | `false` | | `
` | `undefined` | | `
` | `undefined` | @function up.element.booleanAttr @param {Element} element The element from which to retrieve the attribute value. @param {string} attribute The attribute name. @return {boolean|undefined} The cast attribute value. @stable */ booleanAttr = function(element, attribute, pass) { var value; value = stringAttr(element, attribute); switch (value) { case 'false': return false; case 'true': case '': case attribute: return true; default: if (pass) { return value; } } }; /*** Returns the given attribute value cast as boolean. If the attribute value cannot be cast, returns the attribute value unchanged. @internal */ booleanOrStringAttr = function(element, attribute) { return booleanAttr(element, attribute, true); }; /*** Returns the value of the given attribute on the given element, cast to a number. If the attribute value cannot be cast to a number, `undefined` is returned. @function up.element.numberAttr @param {Element} element The element from which to retrieve the attribute value. @param {string} attribute The attribute name. @return {number|undefined} The cast attribute value. @stable */ numberAttr = function(element, attribute) { var value; if (value = element.getAttribute(attribute)) { value = value.replace(/_/g, ''); if (value.match(/^[\d\.]+$/)) { return parseFloat(value); } } }; /*** Reads the given attribute from the element, parsed as [JSON](https://www.json.org/). Returns `undefined` if the attribute value is [blank](/up.util.isBlank). Throws a `SyntaxError` if the attribute value is an invalid JSON string. @function up.element.jsonAttr @param {Element} element The element from which to retrieve the attribute value. @param {string} attribute The attribute name. @return {Object|undefined} The cast attribute value. @stable */ jsonAttr = function(element, attribute) { var json, ref; if (json = typeof element.getAttribute === "function" ? (ref = element.getAttribute(attribute)) != null ? ref.trim() : void 0 : void 0) { return JSON.parse(json); } }; callbackAttr = function(link, attr, exposedKeys) { var callback, code; if (exposedKeys == null) { exposedKeys = []; } if (code = link.getAttribute(attr)) { callback = (function(func, args, ctor) { ctor.prototype = func.prototype; var child = new ctor, result = func.apply(child, args); return Object(result) === result ? result : child; })(Function, ['event'].concat(slice.call(exposedKeys), [code]), function(){}); return function(event) { var exposedValues; exposedValues = u.values(u.pick(event, exposedKeys)); return callback.call.apply(callback, [link, event].concat(slice.call(exposedValues))); }; } }; closestAttr = function(element, attr) { var ref; return (ref = closest(element, '[' + attr + ']')) != null ? ref.getAttribute(attr) : void 0; }; /*** Temporarily sets the inline CSS styles on the given element. Returns a function that restores the original inline styles when called. \#\#\# Example element = document.querySelector('div') unhide = up.element.setTemporaryStyle(element, { 'visibility': 'hidden' }) // do things while element is invisible unhide() // element is visible again @function up.element.setTemporaryStyle @param {Element} element The element to style. @param {Object} styles An object of CSS property names and values. @return {Function()} A function that restores the original inline styles when called. @internal */ setTemporaryStyle = function(element, newStyles, block) { var oldStyles; oldStyles = inlineStyle(element, Object.keys(newStyles)); setInlineStyle(element, newStyles); return function() { return setInlineStyle(element, oldStyles); }; }; /*** Receives [computed CSS styles](https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle) for the given element. \#\#\# Examples When requesting a single CSS property, its value will be returned as a string: value = up.element.style(element, 'font-size') // value is '16px' When requesting multiple CSS properties, the function returns an object of property names and values: value = up.element.style(element, ['font-size', 'margin-top']) // value is { 'font-size': '16px', 'margin-top': '10px' } @function up.element.style @param {Element} element @param {String|Array} propOrProps One or more CSS property names in kebab-case or camelCase. @return {string|object} @stable */ computedStyle = function(element, props) { var style; style = window.getComputedStyle(element); return extractFromStyleObject(style, props); }; /*** Receives a [computed CSS property value](https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle) for the given element, casted as a number. The value is casted by removing the property's [unit](https://www.w3schools.com/cssref/css_units.asp) (which is usually `px` for computed properties). The result is then parsed as a floating point number. Returns `undefined` if the property value is missing, or if it cannot be parsed as a number. \#\#\# Examples When requesting a single CSS property, its value will be returned as a string: value = up.element.style(element, 'font-size') // value is '16px' value = up.element.styleNumber(element, 'font-size') // value is 16 @function up.element.styleNumber @param {Element} element @param {string} prop A single property name in kebab-case or camelCase. @return {number|undefined} @stable */ computedStyleNumber = function(element, prop) { var rawValue; rawValue = computedStyle(element, prop); if (u.isGiven(rawValue)) { return parseFloat(rawValue); } else { return void 0; } }; /*** Gets the given inline style(s) from the given element's `[style]` attribute. @function up.element.inlineStyle @param {Element} element @param {String|Array} propOrProps One or more CSS property names in kebab-case or camelCase. @return {string|object} @internal */ inlineStyle = function(element, props) { var style; style = element.style; return extractFromStyleObject(style, props); }; extractFromStyleObject = function(style, keyOrKeys) { if (u.isString(keyOrKeys)) { return style[keyOrKeys]; } else { return u.pick(style, keyOrKeys); } }; /*** Sets the given CSS properties as inline styles on the given element. @function up.element.setStyle @param {Element} element @param {Object} props One or more CSS properties with kebab-case keys or camelCase keys. @return {string|object} @stable */ setInlineStyle = function(element, props) { var key, results1, style, value; style = element.style; results1 = []; for (key in props) { value = props[key]; value = normalizeStyleValueForWrite(key, value); results1.push(style[key] = value); } return results1; }; normalizeStyleValueForWrite = function(key, value) { if (u.isMissing(value)) { value = ''; } else if (CSS_LENGTH_PROPS.has(key.toLowerCase().replace(/-/, ''))) { value = cssLength(value); } return value; }; CSS_LENGTH_PROPS = u.arrayToSet(['top', 'right', 'bottom', 'left', 'padding', 'paddingtop', 'paddingright', 'paddingbottom', 'paddingleft', 'margin', 'margintop', 'marginright', 'marginbottom', 'marginleft', 'borderwidth', 'bordertopwidth', 'borderrightwidth', 'borderbottomwidth', 'borderleftwidth', 'width', 'height', 'maxwidth', 'maxheight', 'minwidth', 'minheight']); /*** Converts the given value to a CSS length value, adding a `px` unit if required. @function cssLength @internal */ cssLength = function(obj) { if (u.isNumber(obj) || (u.isString(obj) && /^\d+$/.test(obj))) { return obj.toString() + "px"; } else { return obj; } }; /*** Returns whether the given element is currently visible. An element is considered visible if it consumes space in the document. Elements with `{ visibility: hidden }` or `{ opacity: 0 }` are considered visible, since they still consume space in the layout. Elements not attached to the DOM are considered hidden. @function up.element.isVisible @param {Element} element The element to check. @return {boolean} @stable */ isVisible = function(element) { return !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length); }; upAttrs = function(element) { var attribute, attrs, i, len, name, ref, upAttributePattern; upAttributePattern = /^up-/; attrs = {}; ref = element.attributes; for (i = 0, len = ref.length; i < len; i++) { attribute = ref[i]; name = attribute.name; if (name.match(upAttributePattern)) { attrs[name] = attribute.value; } } return attrs; }; /*** Returns whether the given element has been removed from the DOM tree. up.element.isDetached @param {Element} element @return {boolean} @stable */ isDetached = function(element) { return element !== document && !getRoot().contains(element); }; return u.literal({ all: all, subtree: subtree, isInSubtree: isInSubtree, closest: closest, closestAttr: closestAttr, matches: matches, ancestor: ancestor, around: around, get: getOne, list: getList, remove: remove, toggle: toggle, toggleClass: toggleClass, hide: hide, show: show, metaContent: metaContent, replace: replace, insertBefore: insertBefore, createFromSelector: createFromSelector, setAttrs: setAttrs, setTemporaryAttrs: setTemporaryAttrs, affix: affix, toSelector: toSelector, idSelector: idSelector, classSelector: classSelector, isSingleton: isSingleton, isSingletonSelector: isSingletonSelector, attributeSelector: attributeSelector, trueAttributeSelector: trueAttributeSelector, elementTagName: elementTagName, createDocumentFromHTML: createDocumentFromHTML, createFromHTML: createFromHTML, get_root: getRoot, paint: paint, concludeCSSTransition: concludeCSSTransition, hasCSSTransition: hasCSSTransition, fixedToAbsolute: fixedToAbsolute, setMissingAttrs: setMissingAttrs, setMissingAttr: setMissingAttr, unwrap: unwrap, wrapChildren: wrapChildren, attr: stringAttr, booleanAttr: booleanAttr, numberAttr: numberAttr, jsonAttr: jsonAttr, callbackAttr: callbackAttr, booleanOrStringAttr: booleanOrStringAttr, setTemporaryStyle: setTemporaryStyle, style: computedStyle, styleNumber: computedStyleNumber, inlineStyle: inlineStyle, setStyle: setInlineStyle, isVisible: isVisible, upAttrs: upAttrs, toggleAttr: toggleAttr, isDetached: isDetached }); })(); }).call(this); (function() { var u, slice = [].slice; u = up.util; up.Class = (function() { function Class() {} Class.getter = function(prop, get) { return u.getter(this.prototype, prop, get); }; Class.accessor = function(prop, descriptor) { return Object.defineProperty(this.prototype, prop, descriptor); }; Class.delegate = function(props, targetProp) { return u.delegate(this.prototype, props, function() { return this[targetProp]; }); }; Class.wrap = function() { var args; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; return u.wrapValue.apply(u, [this].concat(slice.call(args))); }; return Class; })(); }).call(this); (function() { var u, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty; u = up.util; up.Record = (function(superClass) { extend(Record, superClass); Record.prototype.keys = function() { throw 'Return an array of keys'; }; Record.prototype.defaults = function(options) { return {}; }; function Record(options) { u.assign(this, this.defaults(options), this.attributes(options)); } Record.prototype.attributes = function(source) { if (source == null) { source = this; } return u.pick(source, this.keys()); }; Record.prototype["" + u.copy.key] = function() { return this.variant(); }; Record.prototype.variant = function(changes) { if (changes == null) { changes = {}; } return new this.constructor(u.merge(this.attributes(), changes)); }; Record.prototype["" + u.isEqual.key] = function(other) { return this.constructor === other.constructor && u.isEqual(this.attributes(), other.attributes()); }; return Record; })(up.Class); }).call(this); (function() { }).call(this); (function() { var e; e = up.element; up.BodyShifter = (function() { function BodyShifter() { this.unshiftFns = []; this.reset(); } BodyShifter.prototype.reset = function() { this.unshiftNow(); return this.shiftCount = 0; }; BodyShifter.prototype.shift = function() { var anchor, body, bodyRightPadding, bodyRightShift, elementRight, elementRightShift, i, len, overflowElement, ref, results, scrollbarTookSpace, scrollbarWidth; this.shiftCount++; if (this.shiftCount > 1) { return; } scrollbarTookSpace = up.viewport.rootHasReducedWidthFromScrollbar(); overflowElement = up.viewport.rootOverflowElement(); this.changeStyle(overflowElement, { overflowY: 'hidden' }); if (!scrollbarTookSpace) { return; } body = document.body; scrollbarWidth = up.viewport.scrollbarWidth(); bodyRightPadding = e.styleNumber(body, 'paddingRight'); bodyRightShift = scrollbarWidth + bodyRightPadding; this.changeStyle(body, { paddingRight: bodyRightShift }); ref = up.viewport.anchoredRight(); results = []; for (i = 0, len = ref.length; i < len; i++) { anchor = ref[i]; elementRight = e.styleNumber(anchor, 'right'); elementRightShift = scrollbarWidth + elementRight; results.push(this.changeStyle(anchor, { right: elementRightShift })); } return results; }; BodyShifter.prototype.changeStyle = function(element, styles) { return this.unshiftFns.push(e.setTemporaryStyle(element, styles)); }; BodyShifter.prototype.unshift = function() { this.shiftCount--; if (this.shiftCount > 0) { return; } return this.unshiftNow(); }; BodyShifter.prototype.unshiftNow = function() { var results, unshiftFn; results = []; while (unshiftFn = this.unshiftFns.pop()) { results.push(unshiftFn()); } return results; }; return BodyShifter; })(); }).call(this); (function() { var u, bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, slice = [].slice; u = up.util; /*** @class up.Cache @internal */ up.Cache = (function() { /*** @constructor @param {number|Function(): number} [config.size] Maximum number of cache entries. Set to `undefined` to not limit the cache size. @param {number|Function(): number} [config.expiry] The number of milliseconds after which a cache entry will be discarded. @param {string} [config.logPrefix] A prefix for log entries printed by this cache object. @param {Function(entry): string} [config.key] A function that takes an argument and returns a string key for storage. If omitted, `toString()` is called on the argument. @param {Function(entry): boolean} [config.cacheable] A function that takes a potential cache entry and returns whether this entry can be stored in the hash. If omitted, all entries are considered cacheable. */ function Cache(config) { this.config = config != null ? config : {}; this.get = bind(this.get, this); this.isFresh = bind(this.isFresh, this); this.remove = bind(this.remove, this); this.set = bind(this.set, this); this.alias = bind(this.alias, this); this.store = this.config.store || new up.store.Memory(); } Cache.prototype.size = function() { return this.store.size(); }; Cache.prototype.maxSize = function() { return u.evalOption(this.config.size); }; Cache.prototype.expiryMillis = function() { return u.evalOption(this.config.expiry); }; Cache.prototype.normalizeStoreKey = function(key) { if (this.config.key) { return this.config.key(key); } else { return key.toString(); } }; Cache.prototype.isEnabled = function() { return this.maxSize() !== 0 && this.expiryMillis() !== 0; }; Cache.prototype.clear = function() { return this.store.clear(); }; Cache.prototype.log = function() { var args; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; if (this.config.logPrefix) { args[0] = "[" + this.config.logPrefix + "] " + args[0]; return up.puts.apply(up, ['up.Cache'].concat(slice.call(args))); } }; Cache.prototype.keys = function() { return this.store.keys(); }; Cache.prototype.each = function(fn) { return u.each(this.keys(), (function(_this) { return function(key) { var entry; entry = _this.store.get(key); return fn(key, entry.value, entry.timestamp); }; })(this)); }; Cache.prototype.makeRoomForAnotherEntry = function() { var oldestKey, oldestTimestamp; if (this.hasRoomForAnotherEntry()) { return; } oldestKey = void 0; oldestTimestamp = void 0; this.each(function(key, request, timestamp) { if (!oldestTimestamp || oldestTimestamp > timestamp) { oldestKey = key; return oldestTimestamp = timestamp; } }); if (oldestKey) { return this.store.remove(oldestKey); } }; Cache.prototype.hasRoomForAnotherEntry = function() { var maxSize; maxSize = this.maxSize(); return !maxSize || this.size() < maxSize; }; Cache.prototype.alias = function(oldKey, newKey) { var value; value = this.get(oldKey, { silent: true }); if (u.isDefined(value)) { return this.set(newKey, value); } }; Cache.prototype.timestamp = function() { return (new Date()).valueOf(); }; Cache.prototype.set = function(key, value) { var entry, storeKey; if (this.isEnabled()) { this.makeRoomForAnotherEntry(); storeKey = this.normalizeStoreKey(key); entry = { timestamp: this.timestamp(), value: value }; return this.store.set(storeKey, entry); } }; Cache.prototype.remove = function(key) { var storeKey; storeKey = this.normalizeStoreKey(key); return this.store.remove(storeKey); }; Cache.prototype.isFresh = function(entry) { var millis, timeSinceTouch; millis = this.expiryMillis(); if (millis) { timeSinceTouch = this.timestamp() - entry.timestamp; return timeSinceTouch < millis; } else { return true; } }; Cache.prototype.get = function(key, options) { var entry, storeKey; if (options == null) { options = {}; } storeKey = this.normalizeStoreKey(key); if (entry = this.store.get(storeKey)) { if (this.isFresh(entry)) { if (!options.silent) { this.log("Cache hit for '%s'", key); } return entry.value; } else { if (!options.silent) { this.log("Discarding stale cache entry for '%s'", key); } this.remove(key); return void 0; } } else { if (!options.silent) { this.log("Cache miss for '%s'", key); } return void 0; } }; return Cache; })(); }).call(this); (function() { var u, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty; u = up.util; up.Change = (function(superClass) { extend(Change, superClass); function Change(options) { this.options = options; } Change.prototype.notApplicable = function(reason) { return up.error.notApplicable(this, reason); }; Change.prototype.execute = function() { throw up.error.notImplemented(); }; Change.prototype.onFinished = function() { var base; return typeof (base = this.options).onFinished === "function" ? base.onFinished() : void 0; }; Change.prototype.improveHistoryValue = function(existingValue, newValue) { if (existingValue === false || u.isString(existingValue)) { return existingValue; } else { return newValue; } }; return Change; })(up.Class); }).call(this); (function() { var e, u, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty; u = up.util; e = up.element; up.Change.Addition = (function(superClass) { extend(Addition, superClass); function Addition(options) { Addition.__super__.constructor.call(this, options); this.responseDoc = options.responseDoc; this.acceptLayer = options.acceptLayer; this.dismissLayer = options.dismissLayer; this.eventPlans = options.eventPlans || []; } Addition.prototype.handleLayerChangeRequests = function() { if (this.layer.isOverlay()) { this.tryAcceptLayerFromServer(); this.abortWhenLayerClosed(); this.layer.tryAcceptForLocation(); this.abortWhenLayerClosed(); this.tryDismissLayerFromServer(); this.abortWhenLayerClosed(); this.layer.tryDismissForLocation(); this.abortWhenLayerClosed(); } return this.layer.asCurrent((function(_this) { return function() { var eventPlan, i, len, ref, results; ref = _this.eventPlans; results = []; for (i = 0, len = ref.length; i < len; i++) { eventPlan = ref[i]; up.emit(eventPlan); results.push(_this.abortWhenLayerClosed()); } return results; }; })(this)); }; Addition.prototype.tryAcceptLayerFromServer = function() { if (u.isDefined(this.acceptLayer) && this.layer.isOverlay()) { return this.layer.accept(this.acceptLayer); } }; Addition.prototype.tryDismissLayerFromServer = function() { if (u.isDefined(this.dismissLayer) && this.layer.isOverlay()) { return this.layer.dismiss(this.dismissLayer); } }; Addition.prototype.abortWhenLayerClosed = function() { if (this.layer.isClosed()) { throw up.error.aborted('Layer was closed'); } }; Addition.prototype.setSource = function(arg) { var newElement, oldElement, source; oldElement = arg.oldElement, newElement = arg.newElement, source = arg.source; if (source === 'keep') { source = oldElement && up.fragment.source(oldElement); } if (source) { return e.setMissingAttr(newElement, 'up-source', u.normalizeURL(source)); } }; return Addition; })(up.Change); }).call(this); (function() { var u, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty; u = up.util; up.Change.Removal = (function(superClass) { extend(Removal, superClass); function Removal() { return Removal.__super__.constructor.apply(this, arguments); } return Removal; })(up.Change); }).call(this); (function() { var e, u, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty; u = up.util; e = up.element; up.Change.CloseLayer = (function(superClass) { extend(CloseLayer, superClass); function CloseLayer(options) { var ref; CloseLayer.__super__.constructor.call(this, options); this.verb = options.verb; this.layer = up.layer.get(options); this.origin = options.origin; this.value = options.value; this.preventable = (ref = options.preventable) != null ? ref : true; } CloseLayer.prototype.execute = function() { var parent, value; if (this.origin && u.isUndefined(value)) { value = e.jsonAttr(this.origin, "up-" + this.verb); } if (!this.layer.isOpen()) { return Promise.resolve(); } up.browser.assertConfirmed(this.options); up.network.abort((function(_this) { return function(request) { return request.layer === _this.layer; }; })(this)); if (this.emitCloseEvent().defaultPrevented && this.preventable) { throw up.error.aborted('Close event was prevented'); } parent = this.layer.parent; this.layer.peel(); this.layer.stack.remove(this.layer); parent.restoreHistory(); this.handleFocus(parent); this.layer.teardownHandlers(); this.layer.destroyElements(this.options); this.emitClosedEvent(parent); }; CloseLayer.prototype.emitCloseEvent = function() { return this.layer.emit(this.buildEvent("up:layer:" + this.verb), { callback: this.layer.callback("on" + (u.upperCaseFirst(this.verb))), log: "Will " + this.verb + " " + this.layer }); }; CloseLayer.prototype.emitClosedEvent = function(formerParent) { var verbPast, verbPastUpperCaseFirst; verbPast = this.verb + "ed"; verbPastUpperCaseFirst = u.upperCaseFirst(verbPast); return this.layer.emit(this.buildEvent("up:layer:" + verbPast), { baseLayer: formerParent, callback: this.layer.callback("on" + verbPastUpperCaseFirst), ensureBubbles: true, log: verbPastUpperCaseFirst + " " + this.layer }); }; CloseLayer.prototype.buildEvent = function(name) { return up.event.build(name, { layer: this.layer, value: this.value, origin: this.origin }); }; CloseLayer.prototype.handleFocus = function(formerParent) { var ref; this.layer.overlayFocus.teardown(); if ((ref = formerParent.overlayFocus) != null) { ref.moveToFront(); } return (this.layer.origin || formerParent.element).focus({ preventScroll: true }); }; return CloseLayer; })(up.Change.Removal); }).call(this); (function() { var e, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty; e = up.element; up.Change.DestroyFragment = (function(superClass) { extend(DestroyFragment, superClass); function DestroyFragment(options) { DestroyFragment.__super__.constructor.call(this, options); this.layer = up.layer.get(options) || up.layer.current; this.element = this.options.element; this.animation = this.options.animation; this.log = this.options.log; } DestroyFragment.prototype.execute = function() { this.parent = this.element.parentNode; up.fragment.markAsDestroying(this.element); if (up.motion.willAnimate(this.element, this.animation, this.options)) { this.emitDestroyed(); return this.animate().then((function(_this) { return function() { return _this.wipe(); }; })(this)).then((function(_this) { return function() { return _this.onFinished(); }; })(this)); } else { this.wipe(); this.emitDestroyed(); return this.onFinished(); } }; DestroyFragment.prototype.animate = function() { return up.motion.animate(this.element, this.animation, this.options); }; DestroyFragment.prototype.wipe = function() { return this.layer.asCurrent((function(_this) { return function() { up.syntax.clean(_this.element, { layer: _this.layer }); if (up.browser.canJQuery()) { return jQuery(_this.element).remove(); } else { return e.remove(_this.element); } }; })(this)); }; DestroyFragment.prototype.emitDestroyed = function() { return up.fragment.emitDestroyed(this.element, { parent: this.parent, log: this.log }); }; return DestroyFragment; })(up.Change.Removal); }).call(this); (function() { var e, u, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty; u = up.util; e = up.element; up.Change.FromContent = (function(superClass) { extend(FromContent, superClass); function FromContent(options) { FromContent.__super__.constructor.call(this, options); this.layers = u.filter(up.layer.getAll(this.options), this.isRenderableLayer); this.origin = this.options.origin; this.preview = this.options.preview; this.mode = this.options.mode; if (this.origin) { this.originLayer = up.layer.get(this.origin); } } FromContent.prototype.isRenderableLayer = function(layer) { return layer === 'new' || layer.isOpen(); }; FromContent.prototype.getPlans = function() { if (!this.plans) { this.plans = []; if (this.options.fragment) { this.options.target = this.getResponseDoc().rootSelector(); } this.expandIntoPlans(this.layers, this.options.target); this.expandIntoPlans(this.layers, this.options.fallback); } return this.plans; }; FromContent.prototype.expandIntoPlans = function(layers, targets) { var change, i, layer, len, props, results, target; results = []; for (i = 0, len = layers.length; i < len; i++) { layer = layers[i]; results.push((function() { var j, len1, ref, results1; ref = this.expandTargets(targets, layer); results1 = []; for (j = 0, len1 = ref.length; j < len1; j++) { target = ref[j]; props = u.merge(this.options, { target: target, layer: layer, placement: this.defaultPlacement() }); if (layer === 'new') { change = new up.Change.OpenLayer(props); } else { change = new up.Change.UpdateLayer(props); } results1.push(this.plans.push(change)); } return results1; }).call(this)); } return results; }; FromContent.prototype.expandTargets = function(targets, layer) { return up.fragment.expandTargets(targets, { layer: layer, mode: this.mode, origin: this.origin }); }; FromContent.prototype.execute = function() { var executePlan; if (this.options.preload) { return Promise.resolve(); } executePlan = (function(_this) { return function(plan) { return plan.execute(_this.getResponseDoc()); }; })(this); return this.seekPlan(executePlan) || this.postflightTargetNotApplicable(); }; FromContent.prototype.getResponseDoc = function() { var base, docOptions; if (!(this.preview || this.responseDoc)) { docOptions = u.pick(this.options, ['target', 'content', 'fragment', 'document', 'html']); if (typeof (base = up.migrate).handleResponseDocOptions === "function") { base.handleResponseDocOptions(docOptions); } if (this.defaultPlacement() === 'content') { docOptions.target = this.firstExpandedTarget(docOptions.target); } this.responseDoc = new up.ResponseDoc(docOptions); } return this.responseDoc; }; FromContent.prototype.defaultPlacement = function() { if (!this.options.document && !this.options.fragment) { return 'content'; } }; FromContent.prototype.firstExpandedTarget = function(target) { return this.expandTargets(target || ':main', this.layers[0])[0]; }; FromContent.prototype.preflightProps = function(opts) { var getPlanProps; if (opts == null) { opts = {}; } getPlanProps = function(plan) { return plan.preflightProps(); }; return this.seekPlan(getPlanProps) || opts.optional || this.preflightTargetNotApplicable(); }; FromContent.prototype.preflightTargetNotApplicable = function() { return this.targetNotApplicable('Could not find target in current page'); }; FromContent.prototype.postflightTargetNotApplicable = function() { return this.targetNotApplicable('Could not find common target in current page and response'); }; FromContent.prototype.targetNotApplicable = function(reason) { var humanizedLayerOption, planTargets; if (this.getPlans().length) { planTargets = u.uniq(u.map(this.getPlans(), 'target')); humanizedLayerOption = up.layer.optionToString(this.options.layer); return up.fail(reason + " (tried selectors %o in %s)", planTargets, humanizedLayerOption); } else if (this.layers.length) { return up.fail('No target selector given'); } else { return up.fail('Layer %o does not exist', this.options.layer); } }; FromContent.prototype.seekPlan = function(fn) { var error, i, len, plan, ref; ref = this.getPlans(); for (i = 0, len = ref.length; i < len; i++) { plan = ref[i]; try { return fn(plan); } catch (error1) { error = error1; up.error.notApplicable.is(error) || (function() { throw error; })(); } } }; return FromContent; })(up.Change); }).call(this); (function() { var u, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty; u = up.util; up.Change.FromURL = (function(superClass) { extend(FromURL, superClass); function FromURL(options) { this.options = options; FromURL.__super__.constructor.call(this, this.options); this.options.layer = up.layer.getAll(this.options); this.options.normalizeLayerOptions = false; this.successOptions = this.options; this.failOptions = up.RenderOptions.deriveFailOptions(this.successOptions); } FromURL.prototype.execute = function() { var newPageReason, promise; if (newPageReason = this.newPageReason()) { up.puts('up.render()', newPageReason); up.browser.loadPage(this.options); return u.unresolvablePromise(); } promise = this.makeRequest(); if (this.options.preload) { return promise; } return u.always(promise, (function(_this) { return function(responseOrError) { return _this.onRequestSettled(responseOrError); }; })(this)); }; FromURL.prototype.newPageReason = function() { if (u.isCrossOrigin(this.options.url)) { return 'Loading cross-origin content in new page'; } if (!up.browser.canPushState()) { return 'Loading content in new page to restore history support'; } }; FromURL.prototype.makeRequest = function() { var failAttrs, requestAttrs, successAttrs; successAttrs = this.preflightPropsForRenderOptions(this.successOptions); failAttrs = this.preflightPropsForRenderOptions(this.failOptions, { optional: true }); requestAttrs = u.merge(this.successOptions, successAttrs, u.renameKeys(failAttrs, up.fragment.failKey)); this.request = up.request(requestAttrs); return this.request; }; FromURL.prototype.preflightPropsForRenderOptions = function(renderOptions, requestAttributesOptions) { var preview; preview = new up.Change.FromContent(u.merge(renderOptions, { preview: true })); return preview.preflightProps(requestAttributesOptions); }; FromURL.prototype.onRequestSettled = function(response) { var log; this.response = response; if (!(this.response instanceof up.Response)) { throw this.response; } else if (this.isSuccessfulResponse()) { return this.updateContentFromResponse(['Loaded fragment from successful response to %s', this.request.description], this.successOptions); } else { log = ['Loaded fragment from failed response to %s (HTTP %d)', this.request.description, this.response.status]; throw this.updateContentFromResponse(log, this.failOptions); } }; FromURL.prototype.isSuccessfulResponse = function() { return this.successOptions.fail === false || this.response.ok; }; FromURL.prototype.buildEvent = function(type, props) { var defaultProps; defaultProps = { request: this.request, response: this.response, renderOptions: this.options }; return up.event.build(type, u.merge(defaultProps, props)); }; FromURL.prototype.updateContentFromResponse = function(log, renderOptions) { var event; event = this.buildEvent('up:fragment:loaded', { renderOptions: renderOptions }); this.request.assertEmitted(event, { log: log, callback: this.options.onLoaded }); this.augmentOptionsFromResponse(renderOptions); return new up.Change.FromContent(renderOptions).execute(); }; FromURL.prototype.augmentOptionsFromResponse = function(renderOptions) { var hash, isReloadable, responseURL, serverLocation, serverTarget; responseURL = this.response.url; serverLocation = responseURL; if (hash = this.request.hash) { renderOptions.hash = hash; serverLocation += hash; } isReloadable = this.response.method === 'GET'; if (isReloadable) { renderOptions.source = this.improveHistoryValue(renderOptions.source, responseURL); } else { renderOptions.source = this.improveHistoryValue(renderOptions.source, 'keep'); renderOptions.history = !!renderOptions.location; } renderOptions.location = this.improveHistoryValue(renderOptions.location, serverLocation); renderOptions.title = this.improveHistoryValue(renderOptions.title, this.response.title); renderOptions.eventPlans = this.response.eventPlans; if (serverTarget = this.response.target) { renderOptions.target = serverTarget; } renderOptions.document = this.response.text; renderOptions.acceptLayer = this.response.acceptLayer; renderOptions.dismissLayer = this.response.dismissLayer; if (!renderOptions.document && (u.isDefined(renderOptions.acceptLayer) || u.isDefined(renderOptions.dismissLayer))) { renderOptions.target = ':none'; } return renderOptions.context = u.merge(renderOptions.context, this.response.context); }; return FromURL; })(up.Change); }).call(this); (function() { var u, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty; u = up.util; up.Change.OpenLayer = (function(superClass) { extend(OpenLayer, superClass); function OpenLayer(options) { OpenLayer.__super__.constructor.call(this, options); this.target = options.target; this.origin = options.origin; this.baseLayer = options.baseLayer; } OpenLayer.prototype.preflightProps = function() { return { layer: this.baseLayer, mode: this.options.mode, context: this.buildLayer().context, target: this.target }; }; OpenLayer.prototype.bestPreflightSelector = function() { return this.target; }; OpenLayer.prototype.execute = function(responseDoc) { if (this.target === ':none') { this.content = document.createElement('up-none'); } else { this.content = responseDoc.select(this.target); } if (!this.content || this.baseLayer.isClosed()) { throw this.notApplicable(); } up.puts('up.render()', "Opening element \"" + this.target + "\" in new overlay"); this.options.title = this.improveHistoryValue(this.options.title, responseDoc.getTitle()); if (this.emitOpenEvent().defaultPrevented) { throw up.error.aborted('Open event was prevented'); } this.baseLayer.peel(); this.layer = this.buildLayer(); up.layer.stack.push(this.layer); this.layer.createElements(this.content); this.layer.setupHandlers(); this.handleHistory(); this.setSource({ newElement: this.content, source: this.options.source }); responseDoc.finalizeElement(this.content); up.hello(this.layer.element, { layer: this.layer, origin: this.origin }); this.handleLayerChangeRequests(); this.handleScroll(); this.layer.startOpenAnimation().then((function(_this) { return function() { if (_this.layer.isOpen()) { _this.handleFocus(); } return _this.onFinished(); }; })(this)); this.layer.opening = false; this.emitOpenedEvent(); this.abortWhenLayerClosed(); return new up.RenderResult({ layer: this.layer, fragments: [this.content] }); }; OpenLayer.prototype.buildLayer = function() { return up.layer.build(u.merge(this.options, { opening: true })); }; OpenLayer.prototype.handleHistory = function() { if (this.layer.historyVisible === 'auto') { this.layer.historyVisible = up.fragment.hasAutoHistory(this.content); } this.layer.parent.saveHistory(); this.options.history = true; return this.layer.updateHistory(this.options); }; OpenLayer.prototype.handleFocus = function() { var fragmentFocus, ref; if ((ref = this.baseLayer.overlayFocus) != null) { ref.moveToBack(); } this.layer.overlayFocus.moveToFront(); fragmentFocus = new up.FragmentFocus({ fragment: this.content, layer: this.layer, autoMeans: ['autofocus', 'layer'] }); return fragmentFocus.process(this.options.focus); }; OpenLayer.prototype.handleScroll = function() { var scrolling, scrollingOptions; scrollingOptions = u.merge(this.options, { fragment: this.content, layer: this.layer, autoMeans: ['hash', 'layer'] }); scrolling = new up.FragmentScrolling(scrollingOptions); return scrolling.process(this.options.scroll); }; OpenLayer.prototype.emitOpenEvent = function() { return up.emit('up:layer:open', { origin: this.origin, baseLayer: this.baseLayer, layerOptions: this.options, log: "Opening new overlay" }); }; OpenLayer.prototype.emitOpenedEvent = function() { return this.layer.emit('up:layer:opened', { origin: this.origin, callback: this.layer.callback('onOpened'), log: "Opened new " + this.layer }); }; return OpenLayer; })(up.Change.Addition); }).call(this); (function() { var e, u, bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty; u = up.util; e = up.element; up.Change.UpdateLayer = (function(superClass) { extend(UpdateLayer, superClass); function UpdateLayer(options) { this.executeStep = bind(this.executeStep, this); UpdateLayer.__super__.constructor.call(this, options); this.layer = options.layer; this.target = options.target; this.placement = options.placement; this.context = options.context; this.parseSteps(); } UpdateLayer.prototype.preflightProps = function() { this.matchPreflight(); return { layer: this.layer, mode: this.layer.mode, context: u.merge(this.layer.context, this.context), target: this.bestPreflightSelector() }; }; UpdateLayer.prototype.bestPreflightSelector = function() { this.matchPreflight(); return u.map(this.steps, 'selector').join(', ') || ':none'; }; UpdateLayer.prototype.execute = function(responseDoc) { var swapPromises; this.responseDoc = responseDoc; this.matchPostflight(); up.puts('up.render()', "Updating \"" + (this.bestPreflightSelector()) + "\" in " + this.layer); this.options.title = this.improveHistoryValue(this.options.title, this.responseDoc.getTitle()); this.setScrollAndFocusOptions(); if (this.options.saveScroll) { up.viewport.saveScroll({ layer: this.layer }); } if (this.options.peel) { this.layer.peel(); } u.assign(this.layer.context, this.context); if (this.options.history === 'auto') { this.options.history = this.hasAutoHistory(); } this.layer.updateHistory(u.pick(this.options, ['history', 'location', 'title'])); this.handleLayerChangeRequests(); swapPromises = this.steps.map(this.executeStep); Promise.all(swapPromises).then((function(_this) { return function() { _this.abortWhenLayerClosed(); return _this.onFinished(); }; })(this)); return new up.RenderResult({ layer: this.layer, fragments: u.map(this.steps, 'newElement') }); }; UpdateLayer.prototype.executeStep = function(step) { var keepPlan, morphOptions, newWrapper, oldWrapper, parent, position, promise, wrapper, wrapperStep; this.setSource(step); switch (step.placement) { case 'swap': if (keepPlan = this.findKeepPlan(step)) { up.fragment.emitKept(keepPlan); this.handleFocus(step.oldElement, step); return this.handleScroll(step.oldElement, step); } else { this.transferKeepableElements(step); parent = step.oldElement.parentNode; morphOptions = u.merge(step, { beforeStart: function() { return up.fragment.markAsDestroying(step.oldElement); }, afterInsert: (function(_this) { return function() { _this.responseDoc.finalizeElement(step.newElement); return up.hello(step.newElement, step); }; })(this), beforeDetach: (function(_this) { return function() { return up.syntax.clean(step.oldElement, { layer: _this.layer }); }; })(this), afterDetach: function() { e.remove(step.oldElement); return up.fragment.emitDestroyed(step.oldElement, { parent: parent, log: false }); }, scrollNew: (function(_this) { return function() { _this.handleFocus(step.newElement, step); return _this.handleScroll(step.newElement, step); }; })(this) }); return up.morph(step.oldElement, step.newElement, step.transition, morphOptions); } break; case 'content': oldWrapper = e.wrapChildren(step.oldElement); newWrapper = e.wrapChildren(step.newElement); wrapperStep = u.merge(step, { placement: 'swap', oldElement: oldWrapper, newElement: newWrapper, focus: false }); promise = this.executeStep(wrapperStep); promise = promise.then((function(_this) { return function() { e.unwrap(newWrapper); return _this.handleFocus(step.oldElement, step); }; })(this)); return promise; case 'before': case 'after': wrapper = e.wrapChildren(step.newElement); position = step.placement === 'before' ? 'afterbegin' : 'beforeend'; step.oldElement.insertAdjacentElement(position, wrapper); this.responseDoc.finalizeElement(wrapper); up.hello(wrapper, step); this.handleFocus(wrapper, step); promise = this.handleScroll(wrapper, step); promise = promise.then(function() { return up.animate(wrapper, step.transition, step); }); promise = promise.then(function() { return e.unwrap(wrapper); }); return promise; default: return up.fail('Unknown placement: %o', step.placement); } }; UpdateLayer.prototype.findKeepPlan = function(options) { var lookupOpts, newElement, oldElement, partner, partnerSelector, plan; if (!options.keep) { return; } oldElement = options.oldElement, newElement = options.newElement; if (partnerSelector = e.booleanOrStringAttr(oldElement, 'up-keep')) { if (partnerSelector === true) { partnerSelector = '&'; } lookupOpts = { layer: this.layer, origin: oldElement }; if (options.descendantsOnly) { partner = up.fragment.get(newElement, partnerSelector, lookupOpts); } else { partner = up.fragment.subtree(newElement, partnerSelector, lookupOpts)[0]; } if (partner && e.matches(partner, '[up-keep]')) { plan = { oldElement: oldElement, newElement: partner, newData: up.syntax.data(partner) }; if (!up.fragment.emitKeep(plan).defaultPrevented) { return plan; } } } }; UpdateLayer.prototype.transferKeepableElements = function(step) { var j, keepPlans, keepable, keepableClone, len, plan, ref; keepPlans = []; if (step.keep) { ref = step.oldElement.querySelectorAll('[up-keep]'); for (j = 0, len = ref.length; j < len; j++) { keepable = ref[j]; if (plan = this.findKeepPlan(u.merge(step, { oldElement: keepable, descendantsOnly: true }))) { keepableClone = keepable.cloneNode(true); e.replace(keepable, keepableClone); e.replace(plan.newElement, keepable); keepPlans.push(plan); } } } return step.keepPlans = keepPlans; }; UpdateLayer.prototype.parseSteps = function() { var expressionParts, j, len, ref, results, simpleTarget, step; this.steps = []; ref = u.splitValues(this.target, ','); results = []; for (j = 0, len = ref.length; j < len; j++) { simpleTarget = ref[j]; if (simpleTarget !== ':none') { expressionParts = simpleTarget.match(/^(.+?)(?:\:(before|after))?$/) || (function() { throw up.error.invalidSelector(simpleTarget); })(); step = u.merge(this.options, { selector: expressionParts[1], placement: expressionParts[2] || this.placement || 'swap' }); results.push(this.steps.push(step)); } else { results.push(void 0); } } return results; }; UpdateLayer.prototype.matchPreflight = function() { var finder, j, len, ref, step; if (this.matchedPreflight) { return; } ref = this.steps; for (j = 0, len = ref.length; j < len; j++) { step = ref[j]; finder = new up.FragmentFinder(step); step.oldElement || (step.oldElement = finder.find() || (function() { throw this.notApplicable("Could not find element \"" + this.target + "\" in current page"); }).call(this)); } this.resolveOldNesting(); return this.matchedPreflight = true; }; UpdateLayer.prototype.matchPostflight = function() { var j, len, ref, step; if (this.matchedPostflight) { return; } this.matchPreflight(); ref = this.steps; for (j = 0, len = ref.length; j < len; j++) { step = ref[j]; step.newElement = this.responseDoc.select(step.selector) || (function() { throw this.notApplicable("Could not find element \"" + this.target + "\" in server response"); }).call(this); } if (this.options.hungry) { this.addHungrySteps(); } this.resolveOldNesting(); return this.matchedPostflight = true; }; UpdateLayer.prototype.addHungrySteps = function() { var hungries, j, len, newElement, oldElement, results, selector, transition; hungries = up.fragment.all(up.radio.hungrySelector(), this.options); results = []; for (j = 0, len = hungries.length; j < len; j++) { oldElement = hungries[j]; selector = up.fragment.toTarget(oldElement); if (newElement = this.responseDoc.select(selector)) { transition = e.booleanOrStringAttr(oldElement, 'transition'); results.push(this.steps.push({ selector: selector, oldElement: oldElement, newElement: newElement, transition: transition, placement: 'swap' })); } else { results.push(void 0); } } return results; }; UpdateLayer.prototype.containedByRivalStep = function(steps, candidateStep) { return u.some(steps, function(rivalStep) { return rivalStep !== candidateStep && (rivalStep.placement === 'swap' || rivalStep.placement === 'content') && rivalStep.oldElement.contains(candidateStep.oldElement); }); }; UpdateLayer.prototype.resolveOldNesting = function() { var compressed; compressed = u.uniqBy(this.steps, 'oldElement'); compressed = u.reject(compressed, (function(_this) { return function(step) { return _this.containedByRivalStep(compressed, step); }; })(this)); return this.steps = compressed; }; UpdateLayer.prototype.setScrollAndFocusOptions = function() { return this.steps.forEach((function(_this) { return function(step, i) { if (i > 0) { step.scroll = false; step.focus = false; } if (step.placement === 'swap' || step.placement === 'content') { step.scrollBehavior = 'auto'; return _this.focusCapsule != null ? _this.focusCapsule : _this.focusCapsule = up.FocusCapsule.preserveWithin(step.oldElement); } }; })(this)); }; UpdateLayer.prototype.handleFocus = function(fragment, step) { var fragmentFocus; fragmentFocus = new up.FragmentFocus(u.merge(step, { fragment: fragment, layer: this.layer, focusCapsule: this.focusCapsule, autoMeans: up.fragment.config.autoFocus })); return fragmentFocus.process(step.focus); }; UpdateLayer.prototype.handleScroll = function(fragment, step) { var scrolling; scrolling = new up.FragmentScrolling(u.merge(step, { fragment: fragment, layer: this.layer, autoMeans: up.fragment.config.autoScroll })); return scrolling.process(step.scroll); }; UpdateLayer.prototype.hasAutoHistory = function() { var oldFragments; oldFragments = u.map(this.steps, 'oldElement'); return u.some(oldFragments, function(oldFragment) { return up.fragment.hasAutoHistory(oldFragment); }); }; return UpdateLayer; })(up.Change.Addition); }).call(this); (function() { var e, u, bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; u = up.util; e = up.element; up.CompilerPass = (function() { function CompilerPass(root, compilers, options) { this.root = root; this.compilers = compilers; if (options == null) { options = {}; } this.isInSkippedSubtree = bind(this.isInSkippedSubtree, this); this.skipSubtrees = options.skip; if (!(this.skipSubtrees.length && this.root.querySelector('[up-keep]'))) { this.skipSubtrees = void 0; } this.layer = options.layer || up.layer.get(this.root) || up.layer.current; this.errors = []; } CompilerPass.prototype.run = function() { up.puts('up.hello()', "Compiling fragment %o", this.root); this.layer.asCurrent((function(_this) { return function() { var compiler, i, len, ref, results; ref = _this.compilers; results = []; for (i = 0, len = ref.length; i < len; i++) { compiler = ref[i]; results.push(_this.runCompiler(compiler)); } return results; }; })(this)); if (this.errors.length) { throw up.error.failed('Errors while compiling', { errors: this.errors }); } }; CompilerPass.prototype.runCompiler = function(compiler) { var base, i, len, match, matches; matches = this.select(compiler.selector); if (!matches.length) { return; } if (!compiler.isDefault) { up.puts('up.hello()', 'Compiling "%s" on %d element(s)', compiler.selector, matches.length); } if (compiler.batch) { this.compileBatch(compiler, matches); } else { for (i = 0, len = matches.length; i < len; i++) { match = matches[i]; this.compileOneElement(compiler, match); } } return typeof (base = up.migrate).postCompile === "function" ? base.postCompile(matches, compiler) : void 0; }; CompilerPass.prototype.compileOneElement = function(compiler, element) { var compileArgs, data, destructorOrDestructors, elementArg, result; elementArg = compiler.jQuery ? up.browser.jQuery(element) : element; compileArgs = [elementArg]; if (compiler.length !== 1) { data = up.syntax.data(element); compileArgs.push(data); } result = this.applyCompilerFunction(compiler, element, compileArgs); if (destructorOrDestructors = this.destructorPresence(result)) { return up.destructor(element, destructorOrDestructors); } }; CompilerPass.prototype.compileBatch = function(compiler, elements) { var compileArgs, dataList, elementsArgs, result; elementsArgs = compiler.jQuery ? up.browser.jQuery(elements) : elements; compileArgs = [elementsArgs]; if (compiler.length !== 1) { dataList = u.map(elements, up.syntax.data); compileArgs.push(dataList); } result = this.applyCompilerFunction(compiler, elements, compileArgs); if (this.destructorPresence(result)) { return up.fail('Compilers with { batch: true } cannot return destructors'); } }; CompilerPass.prototype.applyCompilerFunction = function(compiler, elementOrElements, compileArgs) { var error; try { return compiler.apply(elementOrElements, compileArgs); } catch (error1) { error = error1; this.errors.push(error); up.log.error('up.hello()', 'While compiling %o: %o', elementOrElements, error); return up.error.emitGlobal(error); } }; CompilerPass.prototype.destructorPresence = function(result) { if (u.isFunction(result) || u.isArray(result) && (u.every(result, u.isFunction))) { return result; } }; CompilerPass.prototype.select = function(selector) { var matches; matches = e.subtree(this.root, u.evalOption(selector)); if (this.skipSubtrees) { matches = u.reject(matches, this.isInSkippedSubtree); } return matches; }; CompilerPass.prototype.isInSkippedSubtree = function(element) { var parent; if (u.contains(this.skipSubtrees, element)) { return true; } else if (parent = element.parentElement) { return this.isInSkippedSubtree(parent); } else { return false; } }; return CompilerPass; })(); }).call(this); (function() { var u, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty; u = up.util; up.Config = (function(superClass) { extend(Config, superClass); function Config(blueprintFn) { this.blueprintFn = blueprintFn != null ? blueprintFn : (function() { return {}; }); this.reset(); } Config.prototype.reset = function() { return u.assign(this, this.blueprintFn()); }; return Config; })(up.Class); }).call(this); (function() { var e, u, bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; u = up.util; e = up.element; up.CSSTransition = (function() { function CSSTransition(element, lastFrameKebab, options) { this.element = element; this.lastFrameKebab = lastFrameKebab; this.onTransitionEnd = bind(this.onTransitionEnd, this); this.onFinishEvent = bind(this.onFinishEvent, this); this.lastFrameKeysKebab = Object.keys(this.lastFrameKebab); if (u.some(this.lastFrameKeysKebab, function(key) { return key.match(/A-Z/); })) { up.fail('Animation keys must be kebab-case'); } this.finishEvent = options.finishEvent; this.duration = options.duration; this.easing = options.easing; this.finished = false; } CSSTransition.prototype.start = function() { if (this.lastFrameKeysKebab.length === 0) { this.finished = true; return Promise.resolve(); } this.deferred = u.newDeferred(); this.pauseOldTransition(); this.startTime = new Date(); this.startFallbackTimer(); this.listenToFinishEvent(); this.listenToTransitionEnd(); this.startMotion(); return this.deferred.promise(); }; CSSTransition.prototype.listenToFinishEvent = function() { if (this.finishEvent) { return this.stopListenToFinishEvent = this.element.addEventListener(this.finishEvent, this.onFinishEvent); } }; CSSTransition.prototype.onFinishEvent = function(event) { event.stopPropagation(); return this.finish(); }; CSSTransition.prototype.startFallbackTimer = function() { var timingTolerance; timingTolerance = 100; return this.fallbackTimer = u.timer(this.duration + timingTolerance, (function(_this) { return function() { return _this.finish(); }; })(this)); }; CSSTransition.prototype.stopFallbackTimer = function() { return clearTimeout(this.fallbackTimer); }; CSSTransition.prototype.listenToTransitionEnd = function() { return this.stopListenToTransitionEnd = up.on(this.element, 'transitionend', this.onTransitionEnd); }; CSSTransition.prototype.onTransitionEnd = function(event) { var completedPropertyKebab, elapsed; if (event.target !== this.element) { return; } elapsed = new Date() - this.startTime; if (!(elapsed > 0.25 * this.duration)) { return; } completedPropertyKebab = event.propertyName; if (!u.contains(this.lastFrameKeysKebab, completedPropertyKebab)) { return; } return this.finish(); }; CSSTransition.prototype.finish = function() { if (this.finished) { return; } this.finished = true; this.stopFallbackTimer(); if (typeof this.stopListenToFinishEvent === "function") { this.stopListenToFinishEvent(); } if (typeof this.stopListenToTransitionEnd === "function") { this.stopListenToTransitionEnd(); } e.concludeCSSTransition(this.element); this.resumeOldTransition(); return this.deferred.resolve(); }; CSSTransition.prototype.pauseOldTransition = function() { var oldTransition, oldTransitionFrameKebab, oldTransitionProperties; oldTransition = e.style(this.element, ['transitionProperty', 'transitionDuration', 'transitionDelay', 'transitionTimingFunction']); if (e.hasCSSTransition(oldTransition)) { if (oldTransition.transitionProperty !== 'all') { oldTransitionProperties = oldTransition.transitionProperty.split(/\s*,\s*/); oldTransitionFrameKebab = e.style(this.element, oldTransitionProperties); this.setOldTransitionTargetFrame = e.setTemporaryStyle(this.element, oldTransitionFrameKebab); } return this.setOldTransition = e.concludeCSSTransition(this.element); } }; CSSTransition.prototype.resumeOldTransition = function() { if (typeof this.setOldTransitionTargetFrame === "function") { this.setOldTransitionTargetFrame(); } return typeof this.setOldTransition === "function" ? this.setOldTransition() : void 0; }; CSSTransition.prototype.startMotion = function() { e.setStyle(this.element, { transitionProperty: Object.keys(this.lastFrameKebab).join(', '), transitionDuration: this.duration + "ms", transitionTimingFunction: this.easing }); return e.setStyle(this.element, this.lastFrameKebab); }; return CSSTransition; })(); }).call(this); (function() { var e, u; u = up.util; e = up.element; up.DestructorPass = (function() { function DestructorPass(fragment, options) { this.fragment = fragment; this.options = options; this.errors = []; } DestructorPass.prototype.run = function() { var cleanable, destructor, destructors, i, j, len, len1, ref; ref = this.selectCleanables(); for (i = 0, len = ref.length; i < len; i++) { cleanable = ref[i]; if (destructors = u.pluckKey(cleanable, 'upDestructors')) { for (j = 0, len1 = destructors.length; j < len1; j++) { destructor = destructors[j]; this.applyDestructorFunction(destructor, cleanable); } } cleanable.classList.remove('up-can-clean'); } if (this.errors.length) { throw up.error.failed('Errors while destroying', { errors: this.errors }); } }; DestructorPass.prototype.selectCleanables = function() { var selectOptions; selectOptions = u.merge(this.options, { destroying: true }); return up.fragment.subtree(this.fragment, '.up-can-clean', selectOptions); }; DestructorPass.prototype.applyDestructorFunction = function(destructor, element) { var error; try { return destructor(); } catch (error1) { error = error1; this.errors.push(error); up.log.error('up.destroy()', 'While destroying %o: %o', element, error); return up.error.emitGlobal(error); } }; return DestructorPass; })(); }).call(this); (function() { var e, u, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty, slice = [].slice; u = up.util; e = up.element; up.EventEmitter = (function(superClass) { extend(EventEmitter, superClass); function EventEmitter() { return EventEmitter.__super__.constructor.apply(this, arguments); } EventEmitter.prototype.keys = function() { return ['target', 'event', 'baseLayer', 'callback', 'log', 'ensureBubbles']; }; EventEmitter.prototype.emit = function() { this.logEmission(); if (this.baseLayer) { this.baseLayer.asCurrent((function(_this) { return function() { return _this.dispatchEvent(); }; })(this)); } else { this.dispatchEvent(); } return this.event; }; EventEmitter.prototype.dispatchEvent = function() { this.target.dispatchEvent(this.event); if (this.ensureBubbles && e.isDetached(this.target)) { document.dispatchEvent(this.event); } return typeof this.callback === "function" ? this.callback(this.event) : void 0; }; EventEmitter.prototype.assertEmitted = function() { var event; event = this.emit(); return !event.defaultPrevented || (function() { throw up.error.aborted("Event " + event.type + " was prevented"); })(); }; EventEmitter.prototype.logEmission = function() { var message, messageArgs, ref, type; if (!up.log.isEnabled()) { return; } message = this.log; if (u.isArray(message)) { ref = message, message = ref[0], messageArgs = 2 <= ref.length ? slice.call(ref, 1) : []; } else { messageArgs = []; } type = this.event.type; if (u.isString(message)) { return up.puts.apply(up, [type, message].concat(slice.call(messageArgs))); } else if (message !== false) { return up.puts(type, "Event " + type); } }; EventEmitter.fromEmitArgs = function(args, defaults) { var layer, options, ref; if (defaults == null) { defaults = {}; } options = u.extractOptions(args); options = u.merge(defaults, options); if (u.isElementish(args[0])) { options.target = e.get(args.shift()); } else if (args[0] instanceof up.Layer) { options.layer = args.shift(); } if (options.layer) { layer = up.layer.get(options.layer); if (options.target == null) { options.target = layer.element; } if (options.baseLayer == null) { options.baseLayer = layer; } } if (options.baseLayer) { options.baseLayer = up.layer.get(options.baseLayer); } if (u.isString(options.target)) { options.target = up.fragment.get(options.target, { layer: options.layer }); } else if (!options.target) { options.target = document; } if ((ref = args[0]) != null ? ref.preventDefault : void 0) { options.event = args[0]; if (options.log == null) { options.log = args[0].log; } } else if (u.isString(args[0])) { options.event = up.event.build(args[0], options); } else { options.event = up.event.build(options); } return new this(options); }; return EventEmitter; })(up.Record); }).call(this); (function() { var e, u, bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty; u = up.util; e = up.element; up.EventListener = (function(superClass) { extend(EventListener, superClass); EventListener.prototype.keys = function() { return ['element', 'eventType', 'selector', 'callback', 'jQuery', 'guard', 'baseLayer', 'passive', 'once']; }; function EventListener(attributes) { this.nativeCallback = bind(this.nativeCallback, this); EventListener.__super__.constructor.call(this, attributes); this.key = this.constructor.buildKey(attributes); this.isDefault = up.framework.booting; } EventListener.prototype.bind = function() { var base, map, ref; map = ((base = this.element).upEventListeners || (base.upEventListeners = {})); if (map[this.key]) { up.fail('up.on(): The %o callback %o cannot be registered more than once', this.eventType, this.callback); } map[this.key] = this; return (ref = this.element).addEventListener.apply(ref, this.addListenerArgs()); }; EventListener.prototype.addListenerArgs = function() { var args; args = [this.eventType, this.nativeCallback]; if (this.passive && up.browser.canPassiveEventListener()) { args.push({ passive: true }); } return args; }; EventListener.prototype.unbind = function() { var map, ref; if (map = this.element.upEventListeners) { delete map[this.key]; } return (ref = this.element).removeEventListener.apply(ref, this.addListenerArgs()); }; EventListener.prototype.nativeCallback = function(event) { var applyCallback, args, data, element, elementArg, expectedArgCount; if (this.once) { this.unbind(); } element = event.target; if (this.selector) { element = e.closest(element, u.evalOption(this.selector)); } if (this.guard && !this.guard(event)) { return; } if (element) { elementArg = this.jQuery ? up.browser.jQuery(element) : element; args = [event, elementArg]; expectedArgCount = this.callback.length; if (!(expectedArgCount === 1 || expectedArgCount === 2)) { data = up.syntax.data(element); args.push(data); } applyCallback = (function(_this) { return function() { return _this.callback.apply(element, args); }; })(this); if (this.baseLayer) { return this.baseLayer.asCurrent(applyCallback); } else { return applyCallback(); } } }; EventListener.fromElement = function(attributes) { var key, map; if (map = attributes.element.upEventListeners) { key = this.buildKey(attributes); return map[key]; } }; EventListener.buildKey = function(attributes) { var base; (base = attributes.callback).upUid || (base.upUid = u.uid()); return [attributes.eventType, attributes.selector, attributes.callback.upUid].join('|'); }; EventListener.unbindNonDefault = function(element) { var i, len, listener, listeners, map, results; if (map = element.upEventListeners) { listeners = u.values(map); results = []; for (i = 0, len = listeners.length; i < len; i++) { listener = listeners[i]; if (!listener.isDefault) { results.push(listener.unbind()); } else { results.push(void 0); } } return results; } }; return EventListener; })(up.Record); }).call(this); (function() { var u, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty; u = up.util; up.EventListenerGroup = (function(superClass) { extend(EventListenerGroup, superClass); function EventListenerGroup() { return EventListenerGroup.__super__.constructor.apply(this, arguments); } EventListenerGroup.prototype.keys = function() { return ['elements', 'eventTypes', 'selector', 'callback', 'jQuery', 'guard', 'baseLayer', 'passive', 'once']; }; EventListenerGroup.prototype.bind = function() { var unbindFns; unbindFns = []; this.eachListenerAttributes(function(attrs) { var listener; listener = new up.EventListener(attrs); listener.bind(); return unbindFns.push(function() { return listener.unbind(); }); }); return u.sequence(unbindFns); }; EventListenerGroup.prototype.eachListenerAttributes = function(fn) { var element, eventType, i, len, ref, results; ref = this.elements; results = []; for (i = 0, len = ref.length; i < len; i++) { element = ref[i]; results.push((function() { var j, len1, ref1, results1; ref1 = this.eventTypes; results1 = []; for (j = 0, len1 = ref1.length; j < len1; j++) { eventType = ref1[j]; results1.push(fn(this.listenerAttributes(element, eventType))); } return results1; }).call(this)); } return results; }; EventListenerGroup.prototype.listenerAttributes = function(element, eventType) { return u.merge(this.attributes(), { element: element, eventType: eventType }); }; EventListenerGroup.prototype.unbind = function() { return this.eachListenerAttributes(function(attrs) { var listener; if (listener = up.EventListener.fromElement(attrs)) { return listener.unbind(); } }); }; /* Constructs a new up.EventListenerGroup from arguments with many different combinations: [[elements], eventTypes, [selector], [options], callback] @function up.EventListenerGroup.fromBindArgs @internal */ EventListenerGroup.fromBindArgs = function(args, defaults) { var attributes, callback, elements, eventTypes, fixTypes, options, selector; args = u.copy(args); callback = args.pop(); if (args[0].addEventListener) { elements = [args.shift()]; } else if (u.isJQuery(args[0]) || (u.isList(args[0]) && args[0][0].addEventListener)) { elements = args.shift(); } else { elements = [document]; } eventTypes = u.splitValues(args.shift()); if (fixTypes = up.migrate.fixEventTypes) { eventTypes = fixTypes(eventTypes); } options = u.extractOptions(args); selector = args[0]; attributes = u.merge({ elements: elements, eventTypes: eventTypes, selector: selector, callback: callback }, options, defaults); return new this(attributes); }; return EventListenerGroup; })(up.Record); }).call(this); (function() { var e, u, bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; u = up.util; e = up.element; up.FieldObserver = (function() { function FieldObserver(fieldOrFields, options, callback) { this.callback = callback; this.isNewValues = bind(this.isNewValues, this); this.scheduleValues = bind(this.scheduleValues, this); this.fields = e.list(fieldOrFields); this.delay = options.delay; this.batch = options.batch; } FieldObserver.prototype.start = function() { this.scheduledValues = null; this.processedValues = this.readFieldValues(); this.currentTimer = void 0; this.callbackRunning = false; return this.unbind = up.on(this.fields, 'input change', (function(_this) { return function() { return _this.check(); }; })(this)); }; FieldObserver.prototype.stop = function() { this.unbind(); return this.cancelTimer(); }; FieldObserver.prototype.cancelTimer = function() { clearTimeout(this.currentTimer); return this.currentTimer = void 0; }; FieldObserver.prototype.scheduleTimer = function() { this.cancelTimer(); return this.currentTimer = u.timer(this.delay, (function(_this) { return function() { _this.currentTimer = void 0; return _this.requestCallback(); }; })(this)); }; FieldObserver.prototype.scheduleValues = function(values) { this.scheduledValues = values; return this.scheduleTimer(); }; FieldObserver.prototype.isNewValues = function(values) { return !u.isEqual(values, this.processedValues) && !u.isEqual(this.scheduledValues, values); }; FieldObserver.prototype.requestCallback = function() { var callbackReturnValues, callbacksDone, diff, name, value; if (this.scheduledValues !== null && !this.currentTimer && !this.callbackRunning) { diff = this.changedValues(this.processedValues, this.scheduledValues); this.processedValues = this.scheduledValues; this.scheduledValues = null; this.callbackRunning = true; callbackReturnValues = []; if (this.batch) { callbackReturnValues.push(this.callback(diff)); } else { for (name in diff) { value = diff[name]; callbackReturnValues.push(this.callback(value, name)); } } callbacksDone = u.allSettled(callbackReturnValues); return callbacksDone.then((function(_this) { return function() { _this.callbackRunning = false; return _this.requestCallback(); }; })(this)); } }; FieldObserver.prototype.changedValues = function(previous, next) { var changes, i, key, keys, len, nextValue, previousValue; changes = {}; keys = Object.keys(previous); keys = keys.concat(Object.keys(next)); keys = u.uniq(keys); for (i = 0, len = keys.length; i < len; i++) { key = keys[i]; previousValue = previous[key]; nextValue = next[key]; if (!u.isEqual(previousValue, nextValue)) { changes[key] = nextValue; } } return changes; }; FieldObserver.prototype.readFieldValues = function() { return up.Params.fromFields(this.fields).toObject(); }; FieldObserver.prototype.check = function() { var values; values = this.readFieldValues(); if (this.isNewValues(values)) { return this.scheduleValues(values); } }; return FieldObserver; })(); }).call(this); (function() { var PRESERVE_KEYS, e, focusedElementWithin, transferProps, u, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty; u = up.util; e = up.element; PRESERVE_KEYS = ['selectionStart', 'selectionEnd', 'scrollLeft', 'scrollTop']; transferProps = function(from, to) { var error, i, key, len, results; results = []; for (i = 0, len = PRESERVE_KEYS.length; i < len; i++) { key = PRESERVE_KEYS[i]; try { results.push(to[key] = from[key]); } catch (error1) { error = error1; } } return results; }; focusedElementWithin = function(scopeElement) { var focusedElement; focusedElement = document.activeElement; if (e.isInSubtree(scopeElement, focusedElement)) { return focusedElement; } }; up.FocusCapsule = (function(superClass) { extend(FocusCapsule, superClass); function FocusCapsule() { return FocusCapsule.__super__.constructor.apply(this, arguments); } FocusCapsule.prototype.keys = function() { return ['selector', 'oldElement'].concat(PRESERVE_KEYS); }; FocusCapsule.prototype.restore = function(scope, options) { var rediscoveredElement; if (!this.wasLost()) { return; } if (rediscoveredElement = e.get(scope, this.selector)) { transferProps(this, rediscoveredElement); up.focus(rediscoveredElement, options); return true; } }; FocusCapsule.preserveWithin = function(oldElement) { var focusedElement, plan; if (focusedElement = focusedElementWithin(oldElement)) { plan = { oldElement: oldElement, selector: up.fragment.toTarget(focusedElement) }; transferProps(focusedElement, plan); return new this(plan); } }; FocusCapsule.prototype.wasLost = function() { return !focusedElementWithin(this.oldElement); }; return FocusCapsule; })(up.Record); }).call(this); (function() { var DESCENDANT_SELECTOR; DESCENDANT_SELECTOR = /^([^ >+(]+) (.+)$/; up.FragmentFinder = (function() { function FragmentFinder(options) { this.options = options; this.origin = this.options.origin; this.selector = this.options.selector; this.layer = this.options.layer; } FragmentFinder.prototype.find = function() { return this.findAroundOrigin() || this.findInLayer(); }; FragmentFinder.prototype.findAroundOrigin = function() { if (this.origin && up.fragment.config.matchAroundOrigin && !up.element.isDetached(this.origin)) { return this.findClosest() || this.findInVicinity(); } }; FragmentFinder.prototype.findClosest = function() { return up.fragment.closest(this.origin, this.selector, this.options); }; FragmentFinder.prototype.findInVicinity = function() { var parent, parts; if ((parts = this.selector.match(DESCENDANT_SELECTOR))) { if (parent = up.fragment.closest(this.origin, parts[1], this.options)) { return up.fragment.getDumb(parent, parts[2]); } } }; FragmentFinder.prototype.findInLayer = function() { return up.fragment.getDumb(this.selector, this.options); }; return FragmentFinder; })(); }).call(this); (function() { var u, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty; u = up.util; up.FragmentProcessor = (function(superClass) { extend(FragmentProcessor, superClass); function FragmentProcessor() { return FragmentProcessor.__super__.constructor.apply(this, arguments); } FragmentProcessor.prototype.keys = function() { return ['fragment', 'autoMeans', 'origin', 'layer']; }; FragmentProcessor.prototype.process = function(opt) { return this.tryProcess(opt); }; FragmentProcessor.prototype.tryProcess = function(opt) { var match; if (u.isArray(opt)) { return u.find(opt, (function(_this) { return function(opt) { return _this.tryProcess(opt); }; })(this)); } if (u.isFunction(opt)) { return this.tryProcess(opt(this.fragment, this.attributes())); } if (u.isElement(opt)) { return this.processElement(); } if (u.isString(opt)) { if (opt === 'auto') { return this.tryProcess(this.autoMeans); } if (match = opt.match(/^(.+?)-if-(.+?)$/)) { return this.resolveCondition(match[2]) && this.process(match[1]); } } return this.processPrimitive(opt); }; FragmentProcessor.prototype.resolveCondition = function(condition) { if (condition === 'main') { return up.fragment.contains(this.fragment, ':main'); } }; FragmentProcessor.prototype.findSelector = function(selector) { var lookupOpts, match; lookupOpts = { layer: this.layer, origin: this.origin }; if ((match = up.fragment.get(this.fragment, selector, lookupOpts) || up.fragment.get(selector, lookupOpts))) { return match; } else { up.warn('up.render()', 'Could not find an element matching "%s"', selector); } }; return FragmentProcessor; })(up.Record); }).call(this); (function() { var PREVENT_SCROLL_OPTIONS, e, u, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty; u = up.util; e = up.element; PREVENT_SCROLL_OPTIONS = { preventScroll: true }; up.FragmentFocus = (function(superClass) { extend(FragmentFocus, superClass); function FragmentFocus() { return FragmentFocus.__super__.constructor.apply(this, arguments); } FragmentFocus.prototype.keys = function() { return FragmentFocus.__super__.keys.call(this).concat(['hash', 'focusCapsule']); }; FragmentFocus.prototype.processPrimitive = function(opt) { switch (opt) { case 'keep': return this.restoreFocus(); case 'target': case true: return this.focusElement(this.fragment); case 'layer': return this.focusElement(this.layer.getFocusElement()); case 'main': return this.focusSelector(':main'); case 'hash': return this.focusHash(); case 'autofocus': return this.autofocus(); default: if (u.isString(opt)) { return this.focusSelector(opt); } } }; FragmentFocus.prototype.processElement = function(element) { return this.focusElement(element); }; FragmentFocus.prototype.resolveCondition = function(condition) { if (condition === 'lost') { return this.wasFocusLost(); } else { return FragmentFocus.__super__.resolveCondition.call(this, condition); } }; FragmentFocus.prototype.focusSelector = function(selector) { var match; if (match = this.findSelector(selector)) { return this.focusElement(match); } }; FragmentFocus.prototype.restoreFocus = function() { var ref; return (ref = this.focusCapsule) != null ? ref.restore(this.fragment, PREVENT_SCROLL_OPTIONS) : void 0; }; FragmentFocus.prototype.autofocus = function() { var autofocusElement; if (autofocusElement = e.subtree(this.fragment, '[autofocus]')[0]) { up.focus(autofocusElement, PREVENT_SCROLL_OPTIONS); return true; } }; FragmentFocus.prototype.focusElement = function(element) { up.viewport.makeFocusable(element); up.focus(element, PREVENT_SCROLL_OPTIONS); return true; }; FragmentFocus.prototype.focusHash = function() { var hashTarget; if (hashTarget = up.viewport.firstHashTarget(this.hash, { layer: this.layer })) { return this.focusElement(hashTarget); } }; FragmentFocus.prototype.wasFocusLost = function() { var ref; return (ref = this.focusCapsule) != null ? ref.wasLost() : void 0; }; return FragmentFocus; })(up.FragmentProcessor); }).call(this); (function() { var e, u, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty; u = up.util; e = up.element; up.FragmentScrolling = (function(superClass) { extend(FragmentScrolling, superClass); FragmentScrolling.prototype.keys = function() { return FragmentScrolling.__super__.keys.apply(this, arguments).concat(['hash', 'mode', 'revealTop', 'revealMax', 'revealSnap', 'scrollBehavior', 'scrollSpeed']); }; function FragmentScrolling(options) { var base; if (typeof (base = up.migrate).handleScrollOptions === "function") { base.handleScrollOptions(options); } FragmentScrolling.__super__.constructor.call(this, options); } FragmentScrolling.prototype.process = function(opt) { return FragmentScrolling.__super__.process.call(this, opt) || Promise.resolve(); }; FragmentScrolling.prototype.processPrimitive = function(opt) { switch (opt) { case 'reset': return this.reset(); case 'layer': return this.revealLayer(); case 'main': return this.revealSelector(':main'); case 'restore': return this.restore(); case 'hash': return this.hash && up.viewport.revealHash(this.hash, this.attributes()); case 'target': case 'reveal': case true: return this.revealElement(this.fragment); default: if (u.isString(opt)) { return this.revealSelector(opt); } } }; FragmentScrolling.prototype.processElement = function(element) { return this.revealElement(element); }; FragmentScrolling.prototype.revealElement = function(element) { return up.reveal(element, this.attributes()); }; FragmentScrolling.prototype.revealSelector = function(selector) { var match; if (match = this.findSelector(selector)) { return this.revealElement(match); } }; FragmentScrolling.prototype.revealLayer = function() { return this.revealElement(this.layer.getBoxElement()); }; FragmentScrolling.prototype.reset = function() { return up.viewport.resetScroll(u.merge(this.attributes(), { around: this.fragment })); }; FragmentScrolling.prototype.restore = function() { return up.viewport.restoreScroll(u.merge(this.attributes(), { around: this.fragment })); }; return FragmentScrolling; })(up.FragmentProcessor); }).call(this); (function() { var e, u, bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; u = up.util; e = up.element; up.HTMLWrapper = (function() { function HTMLWrapper(tagName, options) { var closeTag, innerHTML, openTag; this.tagName = tagName; if (options == null) { options = {}; } this.wrapMatch = bind(this.wrapMatch, this); openTag = "<" + this.tagName + "[^>]*>"; closeTag = "<\/" + this.tagName + ">"; innerHTML = "(.|\\s)*?"; this.pattern = new RegExp(openTag + innerHTML + closeTag, 'ig'); this.attrName = "up-wrapped-" + this.tagName; } HTMLWrapper.prototype.strip = function(html) { return html.replace(this.pattern, ''); }; HTMLWrapper.prototype.wrap = function(html) { return html.replace(this.pattern, this.wrapMatch); }; HTMLWrapper.prototype.wrapMatch = function(match) { this.didWrap = true; return ''; }; HTMLWrapper.prototype.unwrap = function(element) { var i, len, originalHTML, ref, restoredElement, results, wrappedChild; if (!this.didWrap) { return; } ref = element.querySelectorAll("meta[name='" + this.attrName + "']"); results = []; for (i = 0, len = ref.length; i < len; i++) { wrappedChild = ref[i]; originalHTML = wrappedChild.getAttribute('value'); restoredElement = e.createFromHTML(originalHTML); results.push(e.replace(wrappedChild, restoredElement)); } return results; }; return HTMLWrapper; })(); }).call(this); (function() { var e, u, bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty, slice = [].slice; e = up.element; u = up.util; /*** Each layer has an `up.Layer` instance. Most functions in the `up.layer` package interact with the [current layer](/up.layer.current). For example, `up.layer.dismiss()` is a shortcut for `up.layer.current.dismiss()`. `up.layer.current` is set to the right layer in compilers and most events, even if that layer is not the frontmost layer. E.g. if you're compiling a fragment for a background layer, `up.layer.current` will be the background layer during compilation. @class up.Layer */ up.Layer = (function(superClass) { extend(Layer, superClass); /*** This layer's outmost element. \#\#\# Example let rootLayer = up.layer.root let overlay = await up.layer.open() rootLayer.element // returns overlay.element // returns @property up.Layer#element @param {Element} element @stable */ /*** Whether fragment updates within this layer can affect browser history and window title. If a layer does not have visible history, its desendant layers cannot have history either. @property up.Layer#historyVisible @param {boolean} historyVisible @stable */ /*** This layer's mode which governs its appearance and behavior. @see layer-terminology @property up.Layer#mode @param {string} mode @stable */ /*** This layer's [context](/context). \#\#\# Example You may access the context properties like a regular JavaScript object. ```js let layer = up.layer.current layer.context.message = 'Please select a contact' console.log(layer.context) // logs "{ message: 'Please select a contact' }" ``` @property up.Layer#context @param {Object} context The context object. If no context has been set an empty object is returned. @experimental */ Layer.prototype.keys = function() { return ['element', 'stack', 'historyVisible', 'mode', 'context', 'lastScrollTops']; }; Layer.prototype.defaults = function() { return { context: {}, lastScrollTops: new up.Cache({ size: 30, key: up.history.normalizeURL }) }; }; function Layer(options) { if (options == null) { options = {}; } this.containsEventTarget = bind(this.containsEventTarget, this); this.contains = bind(this.contains, this); Layer.__super__.constructor.call(this, options); if (!this.mode) { throw "missing { mode } option"; } } Layer.prototype.setupHandlers = function() { return up.link.convertClicks(this); }; Layer.prototype.teardownHandlers = function() {}; Layer.prototype.mainTargets = function() { return up.layer.mainTargets(this.mode); }; /*** Synchronizes this layer with the rest of the page. For instance, a popup overlay will re-calculate its position arounds its anchoring element. You only need to call this method after DOM changes unknown to Unpoly have brought overlays out of alignment with the rest of the page. @function up.Layer#sync @experimental */ Layer.prototype.sync = function() {}; /*** [Closes this overlay](/closing-overlays) with an accepting intent, e.g. when a change was confirmed or when a value was selected. To dismiss a layer *without* an accepting intent, use `up.Layer#dismiss()` instead. @function up.Layer#accept @param {any} [value] The acceptance value that will be passed to `{ onAccepted }` callbacks. If there isn't an acceptance value, omit this argument. If you need to pass options without an acceptance value, pass `null`: ```js up.layer.accept(null, { animation: 'move-to-bottom' }) ``` @param {string} [options.confirm] A message the user needs to confirm before the overlay is closed. @param {boolean} [options.preventable=true] Whether the closing can be prevented by an event listener. @param {string|Function(Element, Object)} [options.animation] The [animation](/up.animate) to use for closing this layer. Defaults to the close animation configured for this layer mode. @param {number} [options.duration] The duration for the close animation in milliseconds. @param {number} [options.easing] The [timing function]((http://www.w3.org/TR/css3-transitions/#transition-timing-function)) that controls the acceleration of the close animation. @param {Function} [options.onFinished] A callback that will run when the elements have been removed from the DOM. If the layer has a close animation, the callback will run after the animation has finished. @stable */ Layer.prototype.accept = function() { throw up.error.notImplemented(); }; /*** [Closes this overlay](/closing-overlays) *without* an accepting intent, e.g. when a "Cancel" button was clicked. To close an overlay with an accepting intent, use `up.Layer#accept()` instead. @function up.Layer#dismiss @param {any} [value] The dismissal value that will be passed to `{ onDismissed }` callbacks. If there isn't an acceptance value, omit this argument. If you need to pass options without a dismissal value, pass `null`: ```js up.layer.dismiss(null, { animation: 'move-to-bottom' }) ``` @param {Object} [options] See options for `up.Layer#accept()`. @stable */ Layer.prototype.dismiss = function() { throw up.error.notImplemented(); }; /*** [Dismisses](/up.Layer.prototype.dismiss) all descendant overlays, making this layer the [frontmost layer](/up.layer.front) in the [layer stack](/up.layer.stack). Descendant overlays will be dismissed with value `':peel'`. @function up.Layer#peel @param {Object} options See options for `up.Layer#accept()`. @stable */ Layer.prototype.peel = function(options) { return this.stack.peel(this, options); }; Layer.prototype.evalOption = function(option) { return u.evalOption(option, this); }; /*** Returns whether this layer is the [current layer](/up.layer.current). @function up.Layer#isCurrent @return {boolean} @stable */ Layer.prototype.isCurrent = function() { return this.stack.isCurrent(this); }; /*** Returns whether this layer is the [frontmost layer](/up.layer.front). @function up.Layer#isFront @return {boolean} @stable */ Layer.prototype.isFront = function() { return this.stack.isFront(this); }; /*** Returns whether this layer is the [root layer](/up.layer.root). @function up.Layer#isRoot @return {boolean} @stable */ Layer.prototype.isRoot = function() { return this.stack.isRoot(this); }; /*** Returns whether this layer is *not* the [root layer](/up.layer.root). @function up.Layer#isOverlay @return {boolean} @stable */ Layer.prototype.isOverlay = function() { return this.stack.isOverlay(this); }; /*** Returns whether this layer is still part of the [layer stack](/up.layer.stack). A layer is considered "closed" immediately after it has been [dismissed](/up.Layer.prototype.dismiss) or [accepted](/up.Layer.prototype.dismiss). If the closing is animated, a layer may be considered "closed" while closing animation is still playing. @function up.Layer#isOpen @return {boolean} @stable */ Layer.prototype.isOpen = function() { return this.stack.isOpen(this); }; /*** Returns whether this layer is no longer part of the [layer stack](/up.layer.stack). A layer is considered "closed" immediately after it has been [dismissed](/up.Layer.prototype.dismiss) or [accepted](/up.Layer.prototype.dismiss). If the closing is animated, a layer may be considered "closed" while closing animation is still playing. @function up.Layer#isClosed @return {boolean} @stable */ Layer.prototype.isClosed = function() { return this.stack.isClosed(this); }; /*** Returns this layer's parent layer. The parent layer is the layer that opened this layer. It is visually in the background of this layer. Returns `undefined` for the [root layer](/up.layer.root). @property up.Layer#parent @param {up.Layer} parent @stable */ Layer.getter('parent', function() { return this.stack.parentOf(this); }); /*** Returns this layer's child layer. The child layer is the layer that was opened on top of this layer. It visually overlays this layer. Returns `undefined` if this layer has not opened a child layer. A layer can have at most one child layer. Opening an overlay on a layer with an existing child will first dismiss the existing child before replacing it with the new child. @property up.Layer#child @return {up.Layer} child @stable */ Layer.getter('child', function() { return this.stack.childOf(this); }); /*** Returns an array of this layer's ancestor layers. The array elements are ordered by distance to this layer. The first element is this layer's direct parent. The last element is the [root layer](/up.layer.root). @property up.Layer#ancestors @return {Array} ancestors @stable */ Layer.getter('ancestors', function() { return this.stack.ancestorsOf(this); }); /*** Returns an array of this layer's descendant layers, with the closest descendants listed first. Descendant layers are all layers that visually overlay this layer. The array elements are ordered by distance to this layer. The first element is this layer's direct child. The last element is the [frontmost layer](/up.layer.front). @property up.Layer#descendants @return {Array} descendants @stable */ Layer.getter('descendants', function() { return this.stack.descendantsOf(this); }); /*** Returns the zero-based position of this layer in the [layer stack](/up.layer.stack). The [root layer](/up.layer.root) has an index of `0`, its child overlay has an index of `1`, and so on. @property up.Layer#index @return {number} index @stable */ Layer.getter('index', function() { return this.stack.indexOf(this); }); Layer.prototype.getContentElement = function() { return this.contentElement || this.element; }; Layer.prototype.getBoxElement = function() { return this.boxElement || this.element; }; Layer.prototype.getFocusElement = function() { return this.getBoxElement(); }; Layer.prototype.getFirstSwappableElement = function() { throw up.error.notImplemented(); }; /*** Returns whether the given `element` is contained by this layer. Note that this will always return `false` for elements in [descendant](/up.Layer.prototype.descendants) overlays, even if the descendant overlay's element is nested into the DOM tree of this layer. @function up.Layer#contains @param {Element} element @return {boolean} @stable */ Layer.prototype.contains = function(element) { return e.closest(element, up.layer.anySelector()) === this.element; }; /*** Listens to a [DOM event](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Events) that originated on an element [contained](/up.Layer.prototype.contains) by this layer. This will ignore events emitted on elements in [descendant](/up.Layer.prototype.descendants) overlays, even if the descendant overlay's element is nested into the DOM tree of this layer. The arguments for this function are the same as for `up.on()`. \#\#\# Example let rootLayer = up.layer.root let overlay = await up.layer.open() rootLayer.on('foo', (event) => console.log('Listener called')) rootLayer.emit('foo') // logs "Listener called" overlay.emit('foo') // listener is not called \#\#\# Most Unpoly events have a layer reference Whenever possible Unpoly will emit its events on associated layers instead of `document`. This way you can listen to events on one layer without receiving events from other layers. E.g. to listen to all [requests](/up.request) originating from a given layer: let rootLayer = up.layer.root let rootLink = rootLayer.affix('a[href=/foo]') let overlay = await up.layer.open() let overlayLink = overlay.affix('a[href=/bar]') rootLayer.on('up:request:load', (event) => console.log('Listener called')) up.follow(rootLink) // logs "Listener called" up.follow(overlayLink) // listener is not called @function up.Layer#on @param {string} types A space-separated list of event types to bind to. @param {string|Function(): string} [selector] The selector of an element on which the event must be triggered. Omit the selector to listen to all events of the given type, regardless of the event target. If the selector is not known in advance you may also pass a function that returns the selector. The function is evaluated every time an event with the given type is observed. @param {boolean} [options.passive=false] Whether to register a [passive event listener](https://developers.google.com/web/updates/2016/06/passive-event-listeners). A passive event listener may not call `event.preventDefault()`. This in particular may improve the frame rate when registering `touchstart` and `touchmove` events. @param {boolean} [options.once=true] Whether the listener should run at most once. If `true` the listener will automatically be unbound after the first invocation. @param {Function(event, [element], [data])} listener The listener function that should be called. The function takes the affected element as the second argument. If the element has an [`up-data`](/up-data) attribute, its value is parsed as JSON and passed as a third argument. @return {Function()} A function that unbinds the event listeners when called. @stable */ Layer.prototype.on = function() { var args; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; return this.buildEventListenerGroup(args).bind(); }; /*** Unbinds an event listener previously bound with `up.Layer#on()`. @function up.Layer#off @param {string} events @param {string|Function(): string} [selector] @param {Function(event, [element], [data])} listener The listener function to unbind. Note that you must pass a reference to the same function reference that was passed to `up.Layer#on()` earlier. @stable */ Layer.prototype.off = function() { var args; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; return this.buildEventListenerGroup(args).unbind(); }; Layer.prototype.buildEventListenerGroup = function(args) { return up.EventListenerGroup.fromBindArgs(args, { guard: this.containsEventTarget, elements: [this.element], baseLayer: this }); }; Layer.prototype.containsEventTarget = function(event) { return this.contains(event.target); }; Layer.prototype.wasHitByMouseEvent = function(event) { var hittableElement; hittableElement = document.elementFromPoint(event.clientX, event.clientY); return !hittableElement || this.contains(hittableElement); }; Layer.prototype.buildEventEmitter = function(args) { return up.EventEmitter.fromEmitArgs(args, { layer: this }); }; /*** [Emits](/up.emit) an event on [this layer's element](/up.Layer.prototype.element). The value of [up.layer.current](/up.layer.current) will be set to the this layer while event listeners are running. \#\#\# Example let rootLayer = up.layer.root let overlay = await up.layer.open() rootLayer.on('foo', (event) => console.log('Listener called')) rootLayer.emit('foo') // logs "Listener called" overlay.emit('foo') // listener is not called @function up.Layer#emit @param {Element|jQuery} [target=this.element] The element on which the event is triggered. If omitted, the event will be emitted on the [this layer's element](/up.Layer.prototype.element). @param {string} eventType The event type, e.g. `my:event`. @param {Object} [props={}] A list of properties to become part of the event object that will be passed to listeners. @param {string|Array} [props.log] A message to print to the [log](/up.log) when the event is emitted. Pass `false` to not log this event emission. @param {Element|jQuery} [props.target=this.element] The element on which the event is triggered. Alternatively the target element may be passed as the first argument. @stable */ Layer.prototype.emit = function() { var args; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; return this.buildEventEmitter(args).emit(); }; Layer.prototype.isDetached = function() { return e.isDetached(this.element); }; Layer.prototype.saveHistory = function() { if (!this.isHistoryVisible()) { return; } this.savedTitle = document.title; return this.savedLocation = up.history.location; }; Layer.prototype.restoreHistory = function() { if (this.savedLocation) { up.history.push(this.savedLocation); this.savedLocation = null; } if (this.savedTitle) { document.title = this.savedTitle; return this.savedTitle = null; } }; /*** Temporarily changes the [current layer](/up.layer.current) while the given function is running. Calls the given function and restores the original current layer when the function terminates. @param {Function()} fn The synchronous function to call. Async functions are not supported. @function up.Layer#asCurrent @experimental */ Layer.prototype.asCurrent = function(fn) { return this.stack.asCurrent(this, fn); }; Layer.prototype.updateHistory = function(options) { if (!options.history) { return; } if (u.isString(options.title)) { this.title = options.title; } if (u.isString(options.location)) { return this.location = options.location; } }; /*** This layer's window title. If the [frontmost layer](/up.layer.front) does not have [visible history](/up.Layer.prototype.historyVisible), the browser window will show the title of an ancestor layer. This property will return the title the layer would use if it had visible history. If this layer does not [affect browser history](/up.Layer.prototype.historyVisible), this property will still return the title the layer would otherwise use. When this layer opens a child layer with visible history, the browser window will change to the child layer's title. When the child layer is closed, this layer's title will be restored. @property up.Layer#title @param {string} title @experimental */ Layer.accessor('title', { get: function() { if (this.showsLiveHistory()) { return document.title; } else { return this.savedTitle; } }, set: function(title) { this.savedTitle = title; if (this.showsLiveHistory()) { return document.title = title; } } }); /*** This layer's location URL. If the layer has [no visible history](/up.Layer.prototype.historyVisible), this property still returns the URL of the content in the overlay. In this case the browser's address bar will show the location of an ancestor layer. When this layer opens a child layer with visible history, the browser URL will change to the child layer's location. When the child layer is closed, this layer's location will be restored. @property up.Layer#location @param {string} location @experimental */ Layer.accessor('location', { get: function() { if (this.showsLiveHistory()) { return up.history.location; } else { return this.savedLocation; } }, set: function(location) { var previousLocation; previousLocation = this.savedLocation; location = up.history.normalizeURL(location); if (previousLocation !== location) { this.savedLocation = location; this.emit('up:layer:location:changed', { location: location, log: false }); if (this.showsLiveHistory()) { return up.history.push(location); } } } }); Layer.prototype.isHistoryVisible = function() { return this.historyVisible && (this.isRoot() || this.parent.isHistoryVisible()); }; Layer.prototype.showsLiveHistory = function() { return this.isHistoryVisible() && this.isFront() && (up.history.config.enabled || this.isRoot()); }; Layer.prototype.selector = function(part) { return this.constructor.selector(part); }; Layer.selector = function(part) { throw up.error.notImplemented(); }; Layer.prototype.toString = function() { throw up.error.notImplemented(); }; Layer.prototype.affix = function() { var args; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; return e.affix.apply(e, [this.getFirstSwappableElement()].concat(slice.call(args))); }; return Layer; })(up.Record); }).call(this); (function() { var e, u, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty; e = up.element; u = up.util; /*** @class up.Layer */ up.Layer.Overlay = (function(superClass) { extend(Overlay, superClass); /*** The link or form element that opened this overlay. @property up.Layer#origin @param {Element} origin @stable */ Overlay.prototype.keys = function() { return Overlay.__super__.keys.call(this).concat(['position', 'align', 'size', 'origin', 'class', 'backdrop', 'openAnimation', 'closeAnimation', 'openDuration', 'closeDuration', 'openEasing', 'closeEasing', 'backdropOpenAnimation', 'backdropCloseAnimation', 'dismissable', 'dismissLabel', 'dismissAriaLabel', 'onOpened', 'onAccept', 'onAccepted', 'onDismiss', 'onDismissed', 'acceptEvent', 'dismissEvent', 'acceptLocation', 'dismissLocation', 'opening']); }; function Overlay(options) { Overlay.__super__.constructor.call(this, options); if (this.dismissable === true) { this.dismissable = ['button', 'key', 'outside']; } else if (this.dismissable === false) { this.dismissable = []; } else { this.dismissable = u.splitValues(this.dismissable); } if (this.acceptLocation) { this.acceptLocation = new up.URLPattern(this.acceptLocation); } if (this.dismissLocation) { this.dismissLocation = new up.URLPattern(this.dismissLocation); } } Overlay.prototype.callback = function(name) { var fn; if (fn = this[name]) { return fn.bind(this); } }; Overlay.prototype.createElement = function(parentElement) { var elementAttrs; this.nesting || (this.nesting = this.suggestVisualNesting()); elementAttrs = u.compactObject({ align: this.align, position: this.position, size: this.size, "class": this["class"], nesting: this.nesting }); return this.element = this.affixPart(parentElement, null, elementAttrs); }; Overlay.prototype.createBackdropElement = function(parentElement) { return this.backdropElement = this.affixPart(parentElement, 'backdrop'); }; Overlay.prototype.createViewportElement = function(parentElement) { return this.viewportElement = this.affixPart(parentElement, 'viewport', { 'up-viewport': '' }); }; Overlay.prototype.createBoxElement = function(parentElement) { return this.boxElement = this.affixPart(parentElement, 'box'); }; Overlay.prototype.createContentElement = function(parentElement, content) { this.contentElement = this.affixPart(parentElement, 'content'); return this.contentElement.appendChild(content); }; Overlay.prototype.createDismissElement = function(parentElement) { this.dismissElement = this.affixPart(parentElement, 'dismiss', { 'up-dismiss': '":button"', 'aria-label': this.dismissAriaLabel }); return e.affix(this.dismissElement, 'span[aria-hidden="true"]', { text: this.dismissLabel }); }; Overlay.prototype.affixPart = function(parentElement, part, options) { if (options == null) { options = {}; } return e.affix(parentElement, this.selector(part), options); }; Overlay.selector = function(part) { return u.compact(['up', this.mode, part]).join('-'); }; Overlay.prototype.suggestVisualNesting = function() { var parent; parent = this.parent; if (this.mode === parent.mode) { return 1 + parent.suggestVisualNesting(); } else { return 0; } }; Overlay.prototype.setupHandlers = function() { var base; Overlay.__super__.setupHandlers.call(this); this.overlayFocus = new up.OverlayFocus(this); if (this.supportsDismissMethod('button')) { this.createDismissElement(this.getBoxElement()); } if (this.supportsDismissMethod('outside')) { this.unbindParentClicked = this.parent.on('up:click', (function(_this) { return function(event, element) { var originClicked; originClicked = _this.origin && _this.origin.contains(element); return _this.onOutsideClicked(event, originClicked); }; })(this)); if (this.viewportElement) { up.on(this.viewportElement, 'up:click', (function(_this) { return function(event) { if (event.target === _this.viewportElement) { return _this.onOutsideClicked(event, true); } }; })(this)); } } if (this.supportsDismissMethod('key')) { this.unbindEscapePressed = up.event.onEscape((function(_this) { return function(event) { return _this.onEscapePressed(event); }; })(this)); } this.registerClickCloser('up-accept', (function(_this) { return function(value, closeOptions) { return _this.accept(value, closeOptions); }; })(this)); this.registerClickCloser('up-dismiss', (function(_this) { return function(value, closeOptions) { return _this.dismiss(value, closeOptions); }; })(this)); if (typeof (base = up.migrate).registerLayerCloser === "function") { base.registerLayerCloser(this); } this.registerEventCloser(this.acceptEvent, this.accept); return this.registerEventCloser(this.dismissEvent, this.dismiss); }; Overlay.prototype.onOutsideClicked = function(event, halt) { if (halt) { up.event.halt(event); } return this.dismiss(':outside'); }; Overlay.prototype.onEscapePressed = function(event) { var field; if (this.isFront()) { if (field = up.form.focusedField()) { return field.blur(); } else if (this.supportsDismissMethod('key')) { up.event.halt(event); return this.dismiss(':key'); } } }; Overlay.prototype.registerClickCloser = function(attribute, closeFn) { return this.on('up:click', "[" + attribute + "]", function(event) { var closeOptions, origin, parser, value; up.event.halt(event); origin = event.target; value = e.jsonAttr(origin, attribute); closeOptions = { origin: origin }; parser = new up.OptionsParser(closeOptions, origin); parser.booleanOrString('animation'); parser.string('easing'); parser.number('duration'); parser.string('confirm'); return closeFn(value, closeOptions); }); }; Overlay.prototype.registerEventCloser = function(eventTypes, closeFn) { if (!eventTypes) { return; } return this.on(eventTypes, (function(_this) { return function(event) { event.preventDefault(); return closeFn.call(_this, event); }; })(this)); }; Overlay.prototype.tryAcceptForLocation = function() { return this.tryCloseForLocation(this.acceptLocation, this.accept); }; Overlay.prototype.tryDismissForLocation = function() { return this.tryCloseForLocation(this.dismissLocation, this.dismiss); }; Overlay.prototype.tryCloseForLocation = function(urlPattern, closeFn) { var closeValue, location, resolution; if (urlPattern && (location = this.location) && (resolution = urlPattern.recognize(location))) { closeValue = u.merge(resolution, { location: location }); return closeFn.call(this, closeValue); } }; Overlay.prototype.teardownHandlers = function() { Overlay.__super__.teardownHandlers.call(this); if (typeof this.unbindParentClicked === "function") { this.unbindParentClicked(); } if (typeof this.unbindEscapePressed === "function") { this.unbindEscapePressed(); } return this.overlayFocus.teardown(); }; /*** Destroys the elements that make up this overlay. @function up.Layer.prototype.destroyElements @param {string|Function(Element, Object)} [options.animation=this.closeAnimation] @param {number} [options.duration=this.closeDuration] @param {string} [options.easing=this.closeEasing] @param {Function} [options.onFinished] A callback that will run when the elements have been removed from the DOM. If the destruction is animated, the callback will run after the animation has finished. @return {Promise} A resolved promise. @internal */ Overlay.prototype.destroyElements = function(options) { var animation, destroyOptions, onFinished; animation = (function(_this) { return function() { return _this.startCloseAnimation(options); }; })(this); onFinished = (function(_this) { return function() { _this.onElementsRemoved(); return typeof options.onFinished === "function" ? options.onFinished() : void 0; }; })(this); destroyOptions = u.merge(options, { animation: animation, onFinished: onFinished, log: false }); return up.destroy(this.element, destroyOptions); }; Overlay.prototype.onElementsRemoved = function() {}; Overlay.prototype.startAnimation = function(options) { var backdropDone, boxDone; if (options == null) { options = {}; } boxDone = up.animate(this.getBoxElement(), options.boxAnimation, options); if (this.backdrop && !up.motion.isNone(options.boxAnimation)) { backdropDone = up.animate(this.backdropElement, options.backdropAnimation, options); } return Promise.all([boxDone, backdropDone]); }; Overlay.prototype.startOpenAnimation = function(options) { var ref; if (options == null) { options = {}; } return this.startAnimation({ boxAnimation: (ref = options.animation) != null ? ref : this.evalOption(this.openAnimation), backdropAnimation: 'fade-in', easing: options.easing || this.openEasing, duration: options.duration || this.openDuration }).then((function(_this) { return function() { return _this.wasEverVisible = true; }; })(this)); }; Overlay.prototype.startCloseAnimation = function(options) { var boxAnimation, ref; if (options == null) { options = {}; } boxAnimation = this.wasEverVisible && ((ref = options.animation) != null ? ref : this.evalOption(this.closeAnimation)); return this.startAnimation({ boxAnimation: boxAnimation, backdropAnimation: 'fade-out', easing: options.easing || this.closeEasing, duration: options.duration || this.closeDuration }); }; Overlay.prototype.accept = function(value, options) { if (value == null) { value = null; } if (options == null) { options = {}; } return this.executeCloseChange('accept', value, options); }; Overlay.prototype.dismiss = function(value, options) { if (value == null) { value = null; } if (options == null) { options = {}; } return this.executeCloseChange('dismiss', value, options); }; Overlay.prototype.supportsDismissMethod = function(method) { return u.contains(this.dismissable, method); }; Overlay.prototype.executeCloseChange = function(verb, value, options) { options = u.merge(options, { verb: verb, value: value, layer: this }); return new up.Change.CloseLayer(options).execute(); }; Overlay.prototype.getFirstSwappableElement = function() { return this.getContentElement().children[0]; }; Overlay.prototype.toString = function() { return this.mode + " overlay"; }; return Overlay; })(up.Layer); }).call(this); (function() { var e, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty; e = up.element; up.Layer.OverlayWithViewport = (function(superClass) { extend(OverlayWithViewport, superClass); function OverlayWithViewport() { return OverlayWithViewport.__super__.constructor.apply(this, arguments); } OverlayWithViewport.bodyShifter = new up.BodyShifter(); OverlayWithViewport.getParentElement = function() { return document.body; }; /*** @function up.Layer.OverlayWithViewport#openNow @param {Element} options.content @internal */ OverlayWithViewport.prototype.createElements = function(content) { this.shiftBody(); this.createElement(this.constructor.getParentElement()); if (this.backdrop) { this.createBackdropElement(this.element); } this.createViewportElement(this.element); this.createBoxElement(this.viewportElement); return this.createContentElement(this.boxElement, content); }; OverlayWithViewport.prototype.onElementsRemoved = function() { return this.unshiftBody(); }; OverlayWithViewport.prototype.shiftBody = function() { return this.constructor.bodyShifter.shift(); }; OverlayWithViewport.prototype.unshiftBody = function() { return this.constructor.bodyShifter.unshift(); }; OverlayWithViewport.prototype.sync = function() { if (this.isDetached() && this.isOpen()) { return this.constructor.getParentElement().appendChild(this.element); } }; return OverlayWithViewport; })(up.Layer.Overlay); }).call(this); (function() { var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty; up.Layer.Cover = (function(superClass) { extend(Cover, superClass); function Cover() { return Cover.__super__.constructor.apply(this, arguments); } Cover.mode = 'cover'; return Cover; })(up.Layer.OverlayWithViewport); }).call(this); (function() { var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty; up.Layer.Drawer = (function(superClass) { extend(Drawer, superClass); function Drawer() { return Drawer.__super__.constructor.apply(this, arguments); } Drawer.mode = 'drawer'; return Drawer; })(up.Layer.OverlayWithViewport); }).call(this); (function() { var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty; up.Layer.Modal = (function(superClass) { extend(Modal, superClass); function Modal() { return Modal.__super__.constructor.apply(this, arguments); } Modal.mode = 'modal'; return Modal; })(up.Layer.OverlayWithViewport); }).call(this); (function() { var e, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty; e = up.element; up.Layer.OverlayWithTether = (function(superClass) { extend(OverlayWithTether, superClass); function OverlayWithTether() { return OverlayWithTether.__super__.constructor.apply(this, arguments); } OverlayWithTether.prototype.createElements = function(content) { if (!this.origin) { up.fail('Missing { origin } option'); } this.tether = new up.Tether({ anchor: this.origin, align: this.align, position: this.position }); this.createElement(this.tether.parent); this.createContentElement(this.element, content); return this.tether.start(this.element); }; OverlayWithTether.prototype.onElementsRemoved = function() { return this.tether.stop(); }; OverlayWithTether.prototype.sync = function() { if (this.isOpen()) { if (this.isDetached() || this.tether.isDetached()) { return this.dismiss(':detached', { animation: false, preventable: false }); } else { return this.tether.sync(); } } }; return OverlayWithTether; })(up.Layer.Overlay); }).call(this); (function() { var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty; up.Layer.Popup = (function(superClass) { extend(Popup, superClass); function Popup() { return Popup.__super__.constructor.apply(this, arguments); } Popup.mode = 'popup'; return Popup; })(up.Layer.OverlayWithTether); }).call(this); (function() { var e, u, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty; u = up.util; e = up.element; up.Layer.Root = (function(superClass) { extend(Root, superClass); Root.mode = 'root'; function Root(options) { Root.__super__.constructor.call(this, options); this.setupHandlers(); } Root.getter('element', function() { return e.root; }); Root.prototype.getFirstSwappableElement = function() { return document.body; }; Root.selector = function() { return 'html'; }; Root.prototype.setupHandlers = function() { if (!this.element.upHandlersApplied) { this.element.upHandlersApplied = true; return Root.__super__.setupHandlers.call(this); } }; Root.prototype.sync = function() { return this.setupHandlers(); }; Root.prototype.accept = function() { return this.cannotCloseRoot(); }; Root.prototype.dismiss = function() { return this.cannotCloseRoot(); }; Root.prototype.cannotCloseRoot = function() { throw up.error.failed('Cannot close the root layer'); }; Root.prototype.reset = function() { return u.assign(this, this.defaults()); }; Root.prototype.toString = function() { return "root layer"; }; return Root; })(up.Layer); }).call(this); (function() { var e, u, slice = [].slice; u = up.util; e = up.element; up.LayerLookup = (function() { function LayerLookup() { var args, options, recursiveOptions, stack; stack = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : []; this.stack = stack; options = u.parseArgIntoOptions(args, 'layer'); if (options.normalizeLayerOptions !== false) { up.layer.normalizeOptions(options); } this.values = u.splitValues(options.layer); this.origin = options.origin; this.baseLayer = options.baseLayer || this.stack.current; if (u.isString(this.baseLayer)) { recursiveOptions = u.merge(options, { baseLayer: this.stack.current, normalizeLayerOptions: false }); this.baseLayer = new this.constructor(this.stack, this.baseLayer, recursiveOptions).first(); } } LayerLookup.prototype.originLayer = function() { if (this.origin) { return this.forElement(this.origin); } }; LayerLookup.prototype.first = function() { return this.all()[0]; }; LayerLookup.prototype.all = function() { var results; results = u.flatMap(this.values, (function(_this) { return function(value) { return _this.resolveValue(value); }; })(this)); results = u.compact(results); results = u.uniq(results); return results; }; LayerLookup.prototype.forElement = function(element) { element = e.get(element); return u.find(this.stack.reversed(), function(layer) { return layer.contains(element); }); }; LayerLookup.prototype.forIndex = function(value) { return this.stack[value]; }; LayerLookup.prototype.resolveValue = function(value) { if (value instanceof up.Layer) { return value; } if (u.isNumber(value)) { return this.forIndex(value); } if (/^\d+$/.test(value)) { return this.forIndex(Number(value)); } if (u.isElementish(value)) { return this.forElement(value); } switch (value) { case 'any': return [this.baseLayer].concat(slice.call(this.stack.reversed())); case 'current': return this.baseLayer; case 'closest': return this.stack.selfAndAncestorsOf(this.baseLayer); case 'parent': return this.baseLayer.parent; case 'ancestor': case 'ancestors': return this.baseLayer.ancestors; case 'child': return this.baseLayer.child; case 'descendant': case 'descendants': return this.baseLayer.descendants; case 'new': return 'new'; case 'root': return this.stack.root; case 'overlay': case 'overlays': return u.reverse(this.stack.overlays); case 'front': return this.stack.front; case 'origin': return this.originLayer(); default: return up.fail("Unknown { layer } option: %o", value); } }; return LayerLookup; })(); }).call(this); (function() { var e, u, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty, slice = [].slice; u = up.util; e = up.element; up.LayerStack = (function(superClass) { extend(LayerStack, superClass); function LayerStack() { LayerStack.__super__.constructor.call(this); this.currentOverrides = []; this.push(this.buildRoot()); } LayerStack.prototype.buildRoot = function() { return up.layer.build({ mode: 'root', stack: this }); }; LayerStack.prototype.remove = function(layer) { return u.remove(this, layer); }; LayerStack.prototype.peel = function(layer, options) { var descendants, dismissDescendant, dismissOptions, promises; descendants = u.reverse(layer.descendants); dismissOptions = u.merge(options, { preventable: false }); dismissDescendant = function(descendant) { return descendant.dismiss(':peel', dismissOptions); }; promises = u.map(descendants, dismissDescendant); return Promise.all(promises); }; LayerStack.prototype.reset = function() { this.peel(this.root, { animation: false }); this.currentOverrides = []; return this.root.reset(); }; LayerStack.prototype.isOpen = function(layer) { return layer.index >= 0; }; LayerStack.prototype.isClosed = function(layer) { return !this.isOpen(layer); }; LayerStack.prototype.parentOf = function(layer) { return this[layer.index - 1]; }; LayerStack.prototype.childOf = function(layer) { return this[layer.index + 1]; }; LayerStack.prototype.ancestorsOf = function(layer) { return u.reverse(this.slice(0, layer.index)); }; LayerStack.prototype.selfAndAncestorsOf = function(layer) { return [layer].concat(slice.call(layer.ancestors)); }; LayerStack.prototype.descendantsOf = function(layer) { return this.slice(layer.index + 1); }; LayerStack.prototype.isRoot = function(layer) { return this[0] === layer; }; LayerStack.prototype.isOverlay = function(layer) { return !this.isRoot(layer); }; LayerStack.prototype.isCurrent = function(layer) { return this.current === layer; }; LayerStack.prototype.isFront = function(layer) { return this.front === layer; }; LayerStack.prototype.get = function() { var args; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; return this.getAll.apply(this, args)[0]; }; LayerStack.prototype.getAll = function() { var args; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; return (function(func, args, ctor) { ctor.prototype = func.prototype; var child = new ctor, result = func.apply(child, args); return Object(result) === result ? result : child; })(up.LayerLookup, [this].concat(slice.call(args)), function(){}).all(); }; LayerStack.prototype.sync = function() { var i, layer, len, ref, results; ref = this; results = []; for (i = 0, len = ref.length; i < len; i++) { layer = ref[i]; results.push(layer.sync()); } return results; }; LayerStack.prototype.asCurrent = function(layer, fn) { try { this.currentOverrides.push(layer); return fn(); } finally { this.currentOverrides.pop(); } }; LayerStack.prototype.reversed = function() { return u.reverse(this); }; LayerStack.prototype.dismissOverlays = function(value, options) { var i, len, overlay, ref, results; if (value == null) { value = null; } if (options == null) { options = {}; } options.dismissable = false; ref = u.reverse(this.overlays); results = []; for (i = 0, len = ref.length; i < len; i++) { overlay = ref[i]; results.push(overlay.dismiss(value, options)); } return results; }; LayerStack.prototype["" + u.copy.key] = function() { return u.copyArrayLike(this); }; u.getter(LayerStack.prototype, 'count', function() { return this.length; }); u.getter(LayerStack.prototype, 'root', function() { return this[0]; }); u.getter(LayerStack.prototype, 'overlays', function() { return this.root.descendants; }); u.getter(LayerStack.prototype, 'current', function() { return u.last(this.currentOverrides) || this.front; }); u.getter(LayerStack.prototype, 'front', function() { return u.last(this); }); return LayerStack; })(Array); }).call(this); (function() { var u; u = up.util; up.LinkFeedbackURLs = (function() { function LinkFeedbackURLs(link) { var alias, href, normalize, upHREF; normalize = up.feedback.normalizeURL; this.isSafe = up.link.isSafe(link); if (this.isSafe) { href = link.getAttribute('href'); if (href && href !== '#') { this.href = normalize(href); } upHREF = link.getAttribute('up-href'); if (upHREF) { this.upHREF = normalize(upHREF); } alias = link.getAttribute('up-alias'); if (alias) { this.aliasPattern = new up.URLPattern(alias, normalize); } } } LinkFeedbackURLs.prototype.isCurrent = function(normalizedLocation) { return this.isSafe && !!((this.href && this.href === normalizedLocation) || (this.upHREF && this.upHREF === normalizedLocation) || (this.aliasPattern && this.aliasPattern.test(normalizedLocation, false))); }; return LinkFeedbackURLs; })(); }).call(this); (function() { var e, u, bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; u = up.util; e = up.element; up.LinkPreloader = (function() { function LinkPreloader() { this.considerPreload = bind(this.considerPreload, this); } LinkPreloader.prototype.observeLink = function(link) { if (up.link.isSafe(link)) { this.on(link, 'mouseenter', (function(_this) { return function(event) { return _this.considerPreload(event, true); }; })(this)); this.on(link, 'mousedown touchstart', (function(_this) { return function(event) { return _this.considerPreload(event); }; })(this)); return this.on(link, 'mouseleave', (function(_this) { return function(event) { return _this.stopPreload(event); }; })(this)); } }; LinkPreloader.prototype.on = function(link, eventTypes, callback) { return up.on(link, eventTypes, { passive: true }, callback); }; LinkPreloader.prototype.considerPreload = function(event, applyDelay) { var link; link = event.target; if (link !== this.currentLink) { this.reset(); this.currentLink = link; if (up.link.shouldFollowEvent(event, link)) { if (applyDelay) { return this.preloadAfterDelay(link); } else { return this.preloadNow(link); } } } }; LinkPreloader.prototype.stopPreload = function(event) { if (event.target === this.currentLink) { return this.reset(); } }; LinkPreloader.prototype.reset = function() { var ref; if (!this.currentLink) { return; } clearTimeout(this.timer); if ((ref = this.currentRequest) != null ? ref.preload : void 0) { this.currentRequest.abort(); } this.currentLink = void 0; return this.currentRequest = void 0; }; LinkPreloader.prototype.preloadAfterDelay = function(link) { var delay, ref; delay = (ref = e.numberAttr(link, 'up-delay')) != null ? ref : up.link.config.preloadDelay; return this.timer = u.timer(delay, (function(_this) { return function() { return _this.preloadNow(link); }; })(this)); }; LinkPreloader.prototype.preloadNow = function(link) { var onQueued; if (e.isDetached(link)) { this.reset(); return; } onQueued = (function(_this) { return function(request) { return _this.currentRequest = request; }; })(this); up.log.muteUncriticalRejection(up.link.preload(link, { onQueued: onQueued })); return this.queued = true; }; return LinkPreloader; })(); }).call(this); (function() { var e, u, bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; u = up.util; e = up.element; up.MotionController = (function() { function MotionController(name) { this.whileForwardingFinishEvent = bind(this.whileForwardingFinishEvent, this); this.unmarkCluster = bind(this.unmarkCluster, this); this.markCluster = bind(this.markCluster, this); this.whenElementFinished = bind(this.whenElementFinished, this); this.emitFinishEvent = bind(this.emitFinishEvent, this); this.finishOneElement = bind(this.finishOneElement, this); this.isActive = bind(this.isActive, this); this.expandFinishRequest = bind(this.expandFinishRequest, this); this.finish = bind(this.finish, this); this.startFunction = bind(this.startFunction, this); this.activeClass = "up-" + name; this.dataKey = "up-" + name + "-finished"; this.selector = "." + this.activeClass; this.finishEvent = "up:" + name + ":finish"; this.finishCount = 0; this.clusterCount = 0; } /*** Finishes all animations in the given elements' ancestors and descendants, then calls the given function. The function is expected to return a promise that is fulfilled when the animation ends. The function is also expected to listen to `this.finishEvent` and instantly skip to the last frame when the event is observed. The animation is tracked so it can be [`finished`](/up.MotionController.finish) later. @function startFunction @param {Element|List} cluster A list of elements that will be affected by the motion. @param {Function(): Promise} startMotion @param {Object} [memory.trackMotion=true] @return {Promise} A promise that fulfills when the animation ends. */ MotionController.prototype.startFunction = function(cluster, startMotion, memory) { var mutedAnimator, ref; if (memory == null) { memory = {}; } cluster = e.list(cluster); mutedAnimator = function() { return up.log.muteUncriticalRejection(startMotion()); }; memory.trackMotion = (ref = memory.trackMotion) != null ? ref : up.motion.isEnabled(); if (memory.trackMotion === false) { return u.microtask(mutedAnimator); } else { memory.trackMotion = false; return this.finish(cluster).then((function(_this) { return function() { var promise; promise = _this.whileForwardingFinishEvent(cluster, mutedAnimator); promise = promise.then(function() { return _this.unmarkCluster(cluster); }); _this.markCluster(cluster, promise); return promise; }; })(this)); } }; /*** Finishes all animations in the given elements' ancestors and descendants, then calls `motion.start()`. Also listens to `this.finishEvent` on the given elements. When this event is observed, calls `motion.finish()`. @function startMotion @param {Element|List} cluster @param {up.Motion} motion @param {Object} [memory.trackMotion=true] */ MotionController.prototype.startMotion = function(cluster, motion, memory) { var finish, promise, start, unbindFinish; if (memory == null) { memory = {}; } start = function() { return motion.start(); }; finish = function() { return motion.finish(); }; unbindFinish = up.on(cluster, this.finishEvent, finish); promise = this.startFunction(cluster, start, memory); promise = promise.then(unbindFinish); return promise; }; /*** @function finish @param {List} [elements] If no element is given, finishes all animations in the documnet. If an element is given, only finishes animations in its subtree and ancestors. @return {Promise} A promise that fulfills when animations have finished. */ MotionController.prototype.finish = function(elements) { var allFinished; this.finishCount++; if (this.clusterCount === 0 || !up.motion.isEnabled()) { return Promise.resolve(); } elements = this.expandFinishRequest(elements); allFinished = u.map(elements, this.finishOneElement); return Promise.all(allFinished); }; MotionController.prototype.expandFinishRequest = function(elements) { if (elements) { return u.flatMap(elements, (function(_this) { return function(el) { return e.list(e.closest(el, _this.selector), e.all(el, _this.selector)); }; })(this)); } else { return e.all(this.selector); } }; MotionController.prototype.isActive = function(element) { return element.classList.contains(this.activeClass); }; MotionController.prototype.finishOneElement = function(element) { this.emitFinishEvent(element); return this.whenElementFinished(element); }; MotionController.prototype.emitFinishEvent = function(element, eventAttrs) { if (eventAttrs == null) { eventAttrs = {}; } eventAttrs = u.merge({ target: element, log: false }, eventAttrs); return up.emit(this.finishEvent, eventAttrs); }; MotionController.prototype.whenElementFinished = function(element) { return element[this.dataKey] || Promise.resolve(); }; MotionController.prototype.markCluster = function(cluster, promise) { var element, i, len, results; this.clusterCount++; results = []; for (i = 0, len = cluster.length; i < len; i++) { element = cluster[i]; element.classList.add(this.activeClass); results.push(element[this.dataKey] = promise); } return results; }; MotionController.prototype.unmarkCluster = function(cluster) { var element, i, len, results; this.clusterCount--; results = []; for (i = 0, len = cluster.length; i < len; i++) { element = cluster[i]; element.classList.remove(this.activeClass); results.push(delete element[this.dataKey]); } return results; }; MotionController.prototype.whileForwardingFinishEvent = function(cluster, fn) { var doForward, unbindFinish; if (cluster.length < 2) { return fn(); } doForward = (function(_this) { return function(event) { if (!event.forwarded) { return u.each(cluster, function(element) { if (element !== event.target && _this.isActive(element)) { return _this.emitFinishEvent(element, { forwarded: true }); } }); } }; })(this); unbindFinish = up.on(cluster, this.finishEvent, doForward); return fn().then(unbindFinish); }; MotionController.prototype.reset = function() { return this.finish().then((function(_this) { return function() { _this.finishCount = 0; return _this.clusterCount = 0; }; })(this)); }; return MotionController; })(); }).call(this); (function() { var e, u; u = up.util; e = up.element; up.OptionsParser = (function() { function OptionsParser(options, element, parserOptions) { this.options = options; this.element = element; this.fail = parserOptions != null ? parserOptions.fail : void 0; } OptionsParser.prototype.string = function(key, keyOptions) { return this.parse(e.attr, key, keyOptions); }; OptionsParser.prototype.boolean = function(key, keyOptions) { return this.parse(e.booleanAttr, key, keyOptions); }; OptionsParser.prototype.number = function(key, keyOptions) { return this.parse(e.numberAttr, key, keyOptions); }; OptionsParser.prototype.booleanOrString = function(key, keyOptions) { return this.parse(e.booleanOrStringAttr, key, keyOptions); }; OptionsParser.prototype.json = function(key, keyOptions) { return this.parse(e.jsonAttr, key, keyOptions); }; OptionsParser.prototype.parse = function(attrValueFn, key, keyOptions) { var attrName, attrNames, failAttrNames, failKey, failKeyOptions, i, len, normalizeFn, ref, value; if (keyOptions == null) { keyOptions = {}; } attrNames = u.wrapList((ref = keyOptions.attr) != null ? ref : this.attrNameForKey(key)); value = this.options[key]; if (this.element) { for (i = 0, len = attrNames.length; i < len; i++) { attrName = attrNames[i]; if (value == null) { value = attrValueFn(this.element, attrName); } } } if (value == null) { value = keyOptions["default"]; } if (normalizeFn = keyOptions.normalize) { value = normalizeFn(value); } if (u.isDefined(value)) { this.options[key] = value; } if ((keyOptions.fail || this.fail) && (failKey = up.fragment.failKey(key))) { failAttrNames = u.compact(u.map(attrNames, this.deriveFailAttrName)); failKeyOptions = u.merge(keyOptions, { attr: failAttrNames, fail: false }); return this.parse(attrValueFn, failKey, failKeyOptions); } }; OptionsParser.prototype.deriveFailAttrName = function(attr) { if (attr.indexOf('up-') === 0) { return "up-fail-" + (attr.slice(3)); } }; OptionsParser.prototype.attrNameForKey = function(option) { return "up-" + (u.camelToKebabCase(option)); }; return OptionsParser; })(); }).call(this); (function() { var e, u; e = up.element; u = up.util; up.OverlayFocus = (function() { function OverlayFocus(layer) { this.layer = layer; this.focusElement = this.layer.getFocusElement(); } OverlayFocus.prototype.moveToFront = function() { if (this.enabled) { return; } this.enabled = true; this.untrapFocus = up.on('focusin', (function(_this) { return function(event) { return _this.onFocus(event); }; })(this)); this.unsetAttrs = e.setTemporaryAttrs(this.focusElement, { 'tabindex': '0', 'role': 'dialog', 'aria-modal': 'true' }); this.focusTrapBefore = e.affix(this.focusElement, 'beforebegin', 'up-focus-trap[tabindex=0]'); return this.focusTrapAfter = e.affix(this.focusElement, 'afterend', 'up-focus-trap[tabindex=0]'); }; OverlayFocus.prototype.moveToBack = function() { return this.teardown(); }; OverlayFocus.prototype.teardown = function() { if (!this.enabled) { return; } this.enabled = false; this.untrapFocus(); this.unsetAttrs(); e.remove(this.focusTrapBefore); return e.remove(this.focusTrapAfter); }; OverlayFocus.prototype.onFocus = function(event) { var target; target = event.target; if (this.processingFocusEvent) { return; } this.processingFocusEvent = true; if (target === this.focusTrapBefore) { this.focusEnd(); } else if (target === this.focusTrapAfter || !this.layer.contains(target)) { this.focusStart(); } return this.processingFocusEvent = false; }; OverlayFocus.prototype.focusStart = function(focusOptions) { return up.focus(this.focusElement, focusOptions); }; OverlayFocus.prototype.focusEnd = function() { return this.focusLastDescendant(this.layer.getBoxElement()) || this.focusStart(); }; OverlayFocus.prototype.focusLastDescendant = function(element) { var child, i, len, ref; ref = u.reverse(element.children); for (i = 0, len = ref.length; i < len; i++) { child = ref[i]; if (up.viewport.tryFocus(child) || this.focusLastDescendant(child)) { return true; } } }; return OverlayFocus; })(); }).call(this); (function() { var e, u, bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty; u = up.util; e = up.element; /*** The `up.Params` class offers a consistent API to read and manipulate request parameters independent of their type. Request parameters are used in [form submissions](/up.Params.prototype.fromForm) and [URLs](/up.Params.prototype.fromURL). Methods like `up.submit()` or `up.replace()` accept request parameters as a `{ params }` option. \#\#\# Supported parameter types The following types of parameter representation are supported: 1. An object like `{ email: 'foo@bar.com' }` 2. A query string like `'email=foo%40bar.com'` 3. An array of `{ name, value }` objects like `[{ name: 'email', value: 'foo@bar.com' }]` 4. A [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData) object. On IE 11 and Edge, `FormData` payloads require a [polyfill for `FormData#entries()`](https://github.com/jimmywarting/FormData). @class up.Params */ up.Params = (function(superClass) { extend(Params, superClass); /*** Constructs a new `up.Params` instance. @constructor up.Params @param {Object|Array|string|up.Params} [params] An existing list of params with which to initialize the new `up.Params` object. The given params value may be of any [supported type](/up.Params). @return {up.Params} @experimental */ function Params(raw) { this.arrayEntryToQuery = bind(this.arrayEntryToQuery, this); this.clear(); this.addAll(raw); } /*** Removes all params from this object. @function up.Params#clear @experimental */ Params.prototype.clear = function() { return this.entries = []; }; Params.prototype["" + u.copy.key] = function() { return new up.Params(this); }; /*** Returns an object representation of this `up.Params` instance. The returned value is a simple JavaScript object with properties that correspond to the key/values in the given `params`. \#\#\# Example var params = new up.Params('foo=bar&baz=bam') var object = params.toObject() // object is now: { // foo: 'bar', // baz: 'bam' // ] @function up.Params#toObject @return {Object} @experimental */ Params.prototype.toObject = function() { var entry, i, len, name, obj, ref, value; obj = {}; ref = this.entries; for (i = 0, len = ref.length; i < len; i++) { entry = ref[i]; name = entry.name, value = entry.value; if (!u.isBasicObjectProperty(name)) { if (this.isArrayKey(name)) { obj[name] || (obj[name] = []); obj[name].push(value); } else { obj[name] = value; } } } return obj; }; /*** Returns an array representation of this `up.Params` instance. The returned value is a JavaScript array with elements that are objects with `{ key }` and `{ value }` properties. \#\#\# Example var params = new up.Params('foo=bar&baz=bam') var array = params.toArray() // array is now: [ // { name: 'foo', value: 'bar' }, // { name: 'baz', value: 'bam' } // ] @function up.Params#toArray @return {Array} @experimental */ Params.prototype.toArray = function() { return this.entries; }; /*** Returns a [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) representation of this `up.Params` instance. \#\#\# Example var params = new up.Params('foo=bar&baz=bam') var formData = params.toFormData() formData.get('foo') // 'bar' formData.get('baz') // 'bam' @function up.Params#toFormData @return {FormData} @experimental */ Params.prototype.toFormData = function() { var entry, formData, i, len, ref; formData = new FormData(); ref = this.entries; for (i = 0, len = ref.length; i < len; i++) { entry = ref[i]; formData.append(entry.name, entry.value); } if (!formData.entries) { formData.originalArray = this.entries; } return formData; }; /*** Returns an [query string](https://en.wikipedia.org/wiki/Query_string) for this `up.Params` instance. The keys and values in the returned query string will be [percent-encoded](https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding). Non-primitive values (like [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) will be omitted from the retuned query string. \#\#\# Example var params = new up.Params({ foo: 'bar', baz: 'bam' }) var query = params.toQuery() // query is now: 'foo=bar&baz=bam' @function up.Params#toQuery @param {Object|FormData|string|Array|undefined} params the params to convert @return {string} a query string built from the given params @experimental */ Params.prototype.toQuery = function() { var parts; parts = u.map(this.entries, this.arrayEntryToQuery); parts = u.compact(parts); return parts.join('&'); }; Params.prototype.arrayEntryToQuery = function(entry) { var query, value; value = entry.value; if (this.isBinaryValue(value)) { return void 0; } query = encodeURIComponent(entry.name); if (u.isGiven(value)) { query += "="; query += encodeURIComponent(value); } return query; }; /*** Returns whether the given value cannot be encoded into a query string. We will have `File` values in our params when we serialize a form with a file input. These entries will be filtered out when converting to a query string. @function up.Params#isBinaryValue @internal */ Params.prototype.isBinaryValue = function(value) { return value instanceof Blob; }; Params.prototype.hasBinaryValues = function() { var values; values = u.map(this.entries, 'value'); return u.some(values, this.isBinaryValue); }; /*** Builds an URL string from the given base URL and this `up.Params` instance as a [query string](/up.Params.toString). The base URL may or may not already contain a query string. The additional query string will be joined with an `&` or `?` character accordingly. @function up.Params#toURL @param {string} base The base URL that will be prepended to this `up.Params` object as a [query string](/up.Params.toString). @return {string} The built URL. @experimental */ Params.prototype.toURL = function(base) { var parts, separator; parts = [base, this.toQuery()]; parts = u.filter(parts, u.isPresent); separator = u.contains(base, '?') ? '&' : '?'; return parts.join(separator); }; /*** Adds a new entry with the given `name` and `value`. An `up.Params` instance can hold multiple entries with the same name. To overwrite all existing entries with the given `name`, use `up.Params#set()` instead. \#\#\# Example var params = new up.Params() params.add('foo', 'fooValue') var foo = params.get('foo') // foo is now 'fooValue' @function up.Params#add @param {string} name The name of the new entry. @param {any} value The value of the new entry. @experimental */ Params.prototype.add = function(name, value) { return this.entries.push({ name: name, value: value }); }; /*** Adds all entries from the given list of params. The given params value may be of any [supported type](/up.Params). @function up.Params#addAll @param {Object|Array|string|up.Params|undefined} params @experimental */ Params.prototype.addAll = function(raw) { var ref, ref1; if (u.isMissing(raw)) { } else if (raw instanceof this.constructor) { return (ref = this.entries).push.apply(ref, raw.entries); } else if (u.isArray(raw)) { return (ref1 = this.entries).push.apply(ref1, raw); } else if (u.isString(raw)) { return this.addAllFromQuery(raw); } else if (u.isFormData(raw)) { return this.addAllFromFormData(raw); } else if (u.isObject(raw)) { return this.addAllFromObject(raw); } else { return up.fail("Unsupport params type: %o", raw); } }; Params.prototype.addAllFromObject = function(object) { var key, results, value, valueElement, valueElements; results = []; for (key in object) { value = object[key]; valueElements = u.isArray(value) ? value : [value]; results.push((function() { var i, len, results1; results1 = []; for (i = 0, len = valueElements.length; i < len; i++) { valueElement = valueElements[i]; results1.push(this.add(key, valueElement)); } return results1; }).call(this)); } return results; }; Params.prototype.addAllFromQuery = function(query) { var i, len, name, part, ref, ref1, results, value; ref = query.split('&'); results = []; for (i = 0, len = ref.length; i < len; i++) { part = ref[i]; if (part) { ref1 = part.split('='), name = ref1[0], value = ref1[1]; name = decodeURIComponent(name); if (u.isGiven(value)) { value = decodeURIComponent(value); } else { value = null; } results.push(this.add(name, value)); } else { results.push(void 0); } } return results; }; Params.prototype.addAllFromFormData = function(formData) { return u.eachIterator(formData.entries(), (function(_this) { return function(value) { return _this.add.apply(_this, value); }; })(this)); }; /*** Sets the `value` for the entry with given `name`. An `up.Params` instance can hold multiple entries with the same name. All existing entries with the given `name` are [deleted](/up.Params.prototype.delete) before the new entry is set. To add a new entry even if the `name` is taken, use `up.Params#add()`. @function up.Params#set @param {string} name The name of the entry to set. @param {any} value The new value of the entry. @experimental */ Params.prototype.set = function(name, value) { this["delete"](name); return this.add(name, value); }; /*** Deletes all entries with the given `name`. @function up.Params#delete @param {string} name @experimental */ Params.prototype["delete"] = function(name) { return this.entries = u.reject(this.entries, this.matchEntryFn(name)); }; Params.prototype.matchEntryFn = function(name) { return function(entry) { return entry.name === name; }; }; /*** Returns the first param value with the given `name` from the given `params`. Returns `undefined` if no param value with that name is set. If the `name` denotes an array field (e.g. `foo[]`), *all* param values with the given `name` are returned as an array. If no param value with that array name is set, an empty array is returned. To always return a single value use `up.Params#getFirst()` instead. To always return an array of values use `up.Params#getAll()` instead. \#\#\# Example var params = new up.Params({ foo: 'fooValue', bar: 'barValue' }) var params = new up.Params([ { name: 'foo', value: 'fooValue' } { name: 'bar[]', value: 'barValue1' } { name: 'bar[]', value: 'barValue2' }) ]}) var foo = params.get('foo') // foo is now 'fooValue' var bar = params.get('bar') // bar is now ['barValue1', 'barValue2'] @function up.Params#get @param {string} name @experimental */ Params.prototype.get = function(name) { if (this.isArrayKey(name)) { return this.getAll(name); } else { return this.getFirst(name); } }; /*** Returns the first param value with the given `name`. Returns `undefined` if no param value with that name is set. @function up.Params#getFirst @param {string} name @return {any} The value of the param with the given name. @internal */ Params.prototype.getFirst = function(name) { var entry; entry = u.find(this.entries, this.matchEntryFn(name)); return entry != null ? entry.value : void 0; }; /*** Returns an array of all param values with the given `name`. Returns an empty array if no param value with that name is set. @function up.Params#getAll @param {string} name @return {Array} An array of all values with the given name. @internal */ Params.prototype.getAll = function(name) { var entries; if (this.isArrayKey(name)) { return this.getAll(name); } else { entries = u.map(this.entries, this.matchEntryFn(name)); return u.map(entries, 'value'); } }; Params.prototype.isArrayKey = function(key) { return u.endsWith(key, '[]'); }; Params.prototype["" + u.isBlank.key] = function() { return this.entries.length === 0; }; /*** Constructs a new `up.Params` instance from the given `
`. The returned params may be passed as `{ params }` option to `up.request()` or `up.replace()`. The constructed `up.Params` will include exactly those form values that would be included in a regular form submission. In particular: - All `` types are suppported - Field values are usually strings, but an `` will produce [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) values. - An `` or `` will only be added if they are `[checked]`. - An `` or ``, all selected values are added. If passed a `
This would serialize the form into an array representation: var params = up.Params.fromForm('input[name=email]') var email = params.get('email') // email is now 'foo@bar.com' var pass = params.get('pass') // pass is now 'secret' @function up.Params.fromForm @param {Element|jQuery|string} form A `
` element or a selector that matches a `` element. @return {up.Params} A new `up.Params` instance with values from the given form. @experimental */ Params.fromForm = function(form) { form = up.fragment.get(form); return this.fromFields(up.form.fields(form)); }; /*** Constructs a new `up.Params` instance from one or more [HTML form field](https://www.w3schools.com/html/html_form_elements.asp). The constructed `up.Params` will include exactly those form values that would be included for the given fields in a regular form submission. If a given field wouldn't submit a value (like an unchecked ``, nothing will be added. See `up.Params.fromForm()` for more details and examples. @function up.Params.fromFields @param {Element|List|jQuery} fields @return {up.Params} @experimental */ Params.fromFields = function(fields) { var field, i, len, params, ref; params = new this(); ref = u.wrapList(fields); for (i = 0, len = ref.length; i < len; i++) { field = ref[i]; params.addField(field); } return params; }; /*** Adds params from the given [HTML form field](https://www.w3schools.com/html/html_form_elements.asp). The added params will include exactly those form values that would be included for the given field in a regular form submission. If the given field wouldn't submit a value (like an unchecked ``, nothing will be added. See `up.Params.fromForm()` for more details and examples. @function up.Params#addField @param {Element|jQuery} field @experimental */ Params.prototype.addField = function(field) { var file, i, j, len, len1, name, option, params, ref, ref1, results, results1, tagName, type; params = new this.constructor(); field = e.get(field); if ((name = field.name) && (!field.disabled)) { tagName = field.tagName; type = field.type; if (tagName === 'SELECT') { ref = field.querySelectorAll('option'); results = []; for (i = 0, len = ref.length; i < len; i++) { option = ref[i]; if (option.selected) { results.push(this.add(name, option.value)); } else { results.push(void 0); } } return results; } else if (type === 'checkbox' || type === 'radio') { if (field.checked) { return this.add(name, field.value); } } else if (type === 'file') { ref1 = field.files; results1 = []; for (j = 0, len1 = ref1.length; j < len1; j++) { file = ref1[j]; results1.push(this.add(name, file)); } return results1; } else { return this.add(name, field.value); } } }; Params.prototype["" + u.isEqual.key] = function(other) { return other && (this.constructor === other.constructor) && u.isEqual(this.entries, other.entries); }; /*** Constructs a new `up.Params` instance from the given URL's [query string](https://en.wikipedia.org/wiki/Query_string). Constructs an empty `up.Params` instance if the given URL has no query string. \#\#\# Example var params = up.Params.fromURL('http://foo.com?foo=fooValue&bar=barValue') var foo = params.get('foo') // foo is now: 'fooValue' @function up.Params.fromURL @param {string} url The URL from which to extract the query string. @return {string|undefined} The given URL's query string, or `undefined` if the URL has no query component. @experimental */ Params.fromURL = function(url) { var params, query, urlParts; params = new this(); urlParts = u.parseURL(url); if (query = urlParts.search) { query = query.replace(/^\?/, ''); params.addAll(query); } return params; }; /*** Returns the given URL without its [query string](https://en.wikipedia.org/wiki/Query_string). \#\#\# Example var url = up.Params.stripURL('http://foo.com?key=value') // url is now: 'http://foo.com' @function up.Params.stripURL @param {string} url A URL (with or without a query string). @return {string} The given URL without its query string. @experimental */ Params.stripURL = function(url) { return u.normalizeURL(url, { search: false }); }; /*** If passed an `up.Params` instance, it is returned unchanged. Otherwise constructs an `up.Params` instance from the given value. The given params value may be of any [supported type](/up.Params) The return value is always an `up.Params` instance. @function up.Params.wrap @param {Object|Array|string|up.Params|undefined} params @return {up.Params} @experimental */ return Params; })(up.Class); }).call(this); (function() { var u, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty; u = up.util; up.Rect = (function(superClass) { extend(Rect, superClass); function Rect() { return Rect.__super__.constructor.apply(this, arguments); } Rect.prototype.keys = function() { return ['left', 'top', 'width', 'height']; }; Rect.getter('bottom', function() { return this.top + this.height; }); Rect.getter('right', function() { return this.left + this.width; }); Rect.fromElement = function(element) { return new this(element.getBoundingClientRect()); }; return Rect; })(up.Record); }).call(this); (function() { var u; u = up.util; up.RenderOptions = (function() { var CONTENT_KEYS, GLOBAL_DEFAULTS, PREFLIGHT_KEYS, PRELOAD_OVERRIDES, SHARED_KEYS, assertContentGiven, deriveFailOptions, failOverrides, navigateDefaults, preloadOverrides, preprocess; GLOBAL_DEFAULTS = { hungry: true, keep: true, source: true, saveScroll: true, fail: 'auto' }; PRELOAD_OVERRIDES = { solo: false, confirm: false, feedback: false }; PREFLIGHT_KEYS = ['url', 'method', 'origin', 'headers', 'params', 'cache', 'solo', 'confirm', 'feedback', 'origin', 'baseLayer', 'fail']; SHARED_KEYS = PREFLIGHT_KEYS.concat(['keep', 'hungry', 'history', 'source', 'saveScroll', 'fallback', 'navigate']); CONTENT_KEYS = ['url', 'content', 'fragment', 'document']; navigateDefaults = function(options) { if (options.navigate) { return up.fragment.config.navigateOptions; } }; preloadOverrides = function(options) { if (options.preload) { return PRELOAD_OVERRIDES; } }; preprocess = function(options) { var base, result; if (typeof (base = up.migrate).handleRenderOptions === "function") { base.handleRenderOptions(options); } result = u.merge(GLOBAL_DEFAULTS, navigateDefaults(options), options, preloadOverrides(options)); return result; }; assertContentGiven = function(options) { if (!u.some(CONTENT_KEYS, function(contentKey) { return u.isGiven(options[contentKey]); })) { if (options.defaultToEmptyContent) { return options.content = ''; } else { return up.fail('up.render() needs either { ' + CONTENT_KEYS.join(', ') + ' } option'); } } }; failOverrides = function(options) { var key, overrides, unprefixed, value; overrides = {}; for (key in options) { value = options[key]; if (unprefixed = up.fragment.successKey(key)) { overrides[unprefixed] = value; } } return overrides; }; deriveFailOptions = function(preprocessedOptions) { var result; result = u.merge(u.pick(preprocessedOptions, SHARED_KEYS), failOverrides(preprocessedOptions)); return preprocess(result); }; return { preprocess: preprocess, assertContentGiven: assertContentGiven, deriveFailOptions: deriveFailOptions }; })(); }).call(this); (function() { var e, u, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty; u = up.util; e = up.element; /*** Instances of `up.RenderResult` describe the effects of [rendering](/up.render). It is returned by functions like `up.render()` or `up.navigate()`: ```js let result = await up.render('.target', content: 'foo') console.log(result.fragments) // => [
...
] console.log(result.layer) // => up.Layer.Root ``` @class up.RenderResult */ up.RenderResult = (function(superClass) { extend(RenderResult, superClass); function RenderResult() { return RenderResult.__super__.constructor.apply(this, arguments); } /*** An array of fragments that were inserted. @property up.RenderResult#fragments @param {Array} fragments @stable */ /*** The updated [layer](/up.layer). @property up.RenderResult#layer @param {up.Layer} layer @stable */ RenderResult.prototype.keys = function() { return ['fragments', 'layer']; }; return RenderResult; })(up.Record); }).call(this); (function() { var e, u, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty, slice = [].slice; u = up.util; e = up.element; /*** A normalized description of an [HTTP request](`up.request()`). You can queue a request using the `up.request()` method: ```js let request = up.request('/foo') console.log(request.url) // A request object is also a promise for its response let response = await request console.log(response.text) ``` @class up.Request */ up.Request = (function(superClass) { extend(Request, superClass); /*** The HTTP method for the request. @property up.Request#method @param {string} method @stable */ /*** The URL for the request. @property up.Request#url @param {string} url @stable */ /*** The [hash component](https://en.wikipedia.org/wiki/URI_fragment) of this request's URL. The `{ hash }` property is automatically extracted from the given URL: ```js let request = up.request({ url: '/path#section' }) request.url // => '/path' request.hash // => '#section' ``` @property up.Request#hash @param {string} hash @stable */ /*** [Parameters](/up.Params) that should be sent as the request's payload. @property up.Request#params @param {Object|FormData|string|Array} params @stable */ /*** The CSS selector targeted by this request. The selector will be sent as an `X-Up-Target` header. @property up.Request#target @param {string} target @stable */ /*** The CSS selector targeted by this request in case the server responds with an [error code](/server-errors). The selector will be sent as an `X-Up-Fail-Target` header. @property up.Request#failTarget @param {string} failTarget @stable */ /*** An object of additional HTTP headers. Unpoly will by default send a number of custom request headers. See `up.protocol` and `up.network.config.metaKeys` for details. @property up.Request#headers @param {Object} headers @stable */ /*** A timeout in milliseconds. If `up.network.config.maxRequests` is set, the timeout will not include the time spent waiting in the queue. @property up.Request#timeout @param {Object|undefined} timeout @stable */ /*** Whether to wrap non-standard HTTP methods in a POST request. If this is set, methods other than GET and POST will be converted to a `POST` request and carry their original method as a `_method` parameter. This is to [prevent unexpected redirect behavior](https://makandracards.com/makandra/38347). Defaults to [`up.network.config`](/up.network.config#config.wrapMethod). @property up.Request#wrapMethod @param {boolean} wrapMethod @stable */ /*** The [context](/contact) of the layer targeted by this request. The context object will be sent as an `X-Up-Context` header. @property up.Request#context @param {Object} context @experimental */ /*** The [context](/contact) of the layer targeted by this request in case the server responds with an [error code](/server-errors). The context object will be sent as an `X-Up-Fail-Context` header. @property up.Request#failContext @param {Object} failContext @experimental */ /*** The [layer](/up.layer) targeted by this request. Setting the `{ layer }` property will automatically derive `{ context }` and `{ mode }` properties. To prevent memory leaks, this property is removed shortly after the response is received. @property up.Request#layer @param {up.Layer} layer @experimental */ /*** The [layer](/up.layer) targeted by this request in case the server responds with an [error code](/server-errors). Setting the `{ failLayer }` property will automatically derive `{ failContext }` and `{ failMode }` properties. To prevent memory leaks, this property is removed shortly after the response is received. @property up.Request#failLayer @param {up.Layer} layer @experimental */ /*** The element that triggered the request. For example, when this request was triggered by a click on a link, the lonk element is set as the `{ origin }`. To prevent memory leaks, this property is removed shortly after the response is received. @property up.Request#origin @param {Element} origin @experimental */ /*** The [mode](/up.Layer.prototype.mode) of the layer targeted by this request. The value will be sent as an `X-Up-Mode` header. @property up.Request#mode @param {string} mode @stable */ /*** The [mode](/up.Layer.prototype.mode) of the layer targeted by this request in case the server responds with an [error code](/server-errors). The value will be sent as an `X-Up-Fail-Mode` header. @property up.Request#failMode @param {string} failMode @stable */ /*** The format in which the [request params](/up.Layer.prototype.params) will be encoded. @property up.Request#contentType @param {string} contentType @stable */ /*** The payload that the request will encode into its body. By default Unpoly will build a payload from the given `{ params }` option. @property up.Request#payload @param {string} payload @stable */ /*** @property up.Request#preload @param {boolean} preload @experimental */ Request.prototype.keys = function() { return ['method', 'url', 'hash', 'params', 'target', 'failTarget', 'headers', 'timeout', 'preload', 'cache', 'clearCache', 'layer', 'mode', 'context', 'failLayer', 'failMode', 'failContext', 'origin', 'solo', 'queueTime', 'wrapMethod', 'contentType', 'payload', 'onQueued']; }; /*** Creates a new `up.Request` object. This will not actually send the request over the network. For that use `up.request()`. @constructor up.Request @param {string} attrs.url @param {string} [attrs.method='get'] @param {up.Params|string|Object|Array} [attrs.params] @param {string} [attrs.target] @param {string} [attrs.failTarget] @param {Object} [attrs.headers] @param {number} [attrs.timeout] @internal */ function Request(options) { Request.__super__.constructor.call(this, options); this.params = new up.Params(this.params); this.headers || (this.headers = {}); if (this.preload) { this.cache = true; } if (this.wrapMethod == null) { this.wrapMethod = up.network.config.wrapMethod; } this.normalizeForCaching(); if (!options.basic) { this.layer = up.layer.get(this.layer); this.failLayer = up.layer.get(this.failLayer || this.layer); this.context || (this.context = this.layer.context || {}); this.failContext || (this.failContext = this.failLayer.context || {}); this.mode || (this.mode = this.layer.mode); this.failMode || (this.failMode = this.failLayer.mode); this.deferred = u.newDeferred(); this.state = 'new'; } } Request.delegate(['then', 'catch', 'finally'], 'deferred'); Request.prototype.followState = function(sourceRequest) { return u.delegate(this, ['deferred', 'state', 'preload'], function() { return sourceRequest; }); }; Request.prototype.normalizeForCaching = function() { this.method = u.normalizeMethod(this.method); this.extractHashFromURL(); this.transferParamsToURL(); return this.url = u.normalizeURL(this.url); }; Request.prototype.evictExpensiveAttrs = function() { return u.task((function(_this) { return function() { _this.layer = void 0; _this.failLayer = void 0; return _this.origin = void 0; }; })(this)); }; Request.prototype.extractHashFromURL = function() { var match, ref; if (match = (ref = this.url) != null ? ref.match(/^([^#]*)(#.+)$/) : void 0) { this.url = match[1]; return this.hash = match[2]; } }; Request.prototype.transferParamsToURL = function() { if (!this.url || this.allowsPayload() || u.isBlank(this.params)) { return; } this.url = this.params.toURL(this.url); return this.params.clear(); }; Request.prototype.isSafe = function() { return up.network.isSafeMethod(this.method); }; Request.prototype.allowsPayload = function() { return u.methodAllowsPayload(this.method); }; Request.prototype.will302RedirectWithGET = function() { return this.isSafe() || this.method === 'POST'; }; Request.prototype.willCache = function() { if (this.cache === 'auto') { return up.network.config.autoCache(this); } else { return this.cache; } }; Request.prototype.runQueuedCallbacks = function() { u.always(this, (function(_this) { return function() { return _this.evictExpensiveAttrs(); }; })(this)); return typeof this.onQueued === "function" ? this.onQueued(this) : void 0; }; Request.prototype.load = function() { if (this.state !== 'new') { return; } this.state = 'loading'; return this.xhr = new up.Request.XHRRenderer(this).buildAndSend({ onload: (function(_this) { return function() { return _this.onXHRLoad(); }; })(this), onerror: (function(_this) { return function() { return _this.onXHRError(); }; })(this), ontimeout: (function(_this) { return function() { return _this.onXHRTimeout(); }; })(this), onabort: (function(_this) { return function() { return _this.onXHRAbort(); }; })(this) }); }; /*** Loads this request object as a full-page request, replacing the entire browser environment with a new page from the server response. The full-page request will be loaded with the [URL](/up.Request.prototype.url), [method](/up.Request.prototype.method) and [params](/up.Request.prototype.params) from this request object. Properties that are not possible in a full-page request (such as custom HTTP headers) will be ignored. \#\#\# Example ```javascript let request = await up.request('/path') try { let response = await request('/path') } catch (result) { if (result.name === 'AbortError') { console.log('Request was aborted.') } } request.abort() ``` @function up.Request#loadPage @experimental */ Request.prototype.loadPage = function() { up.network.abort(); return new up.Request.FormRenderer(this).buildAndSubmit(); }; Request.prototype.onXHRLoad = function() { var log, response; response = this.extractResponseFromXHR(); log = ['Server responded HTTP %d to %s %s (%d characters)', response.status, this.method, this.url, response.text.length]; this.emit('up:request:loaded', { request: response.request, response: response, log: log }); return this.respondWith(response); }; Request.prototype.onXHRError = function() { var log; log = 'Fatal error during request'; this.deferred.reject(up.error.failed(log)); return this.emit('up:request:fatal', { log: log }); }; Request.prototype.onXHRTimeout = function() { return this.setAbortedState('Requested timed out'); }; Request.prototype.onXHRAbort = function() { return this.setAbortedState(); }; /*** Aborts this request. The request's promise will reject with an error object that has `{ name: 'AbortError' }`. \#\#\# Example ```javascript let request = await up.request('/path') try { let response = await request('/path') } catch (result) { if (result.name === 'AbortError') { console.log('Request was aborted.') } } request.abort() ``` @function up.Request#abort @experimental */ Request.prototype.abort = function() { if (this.setAbortedState() && this.xhr) { return this.xhr.abort(); } }; Request.prototype.setAbortedState = function(reason) { if (reason == null) { reason = ["Request to %s %s was aborted", this.method, this.url]; } if (!(this.state === 'new' || this.state === 'loading')) { return; } this.state = 'aborted'; this.emit('up:request:aborted', { log: reason }); this.deferred.reject(up.error.aborted(reason)); return true; }; Request.prototype.respondWith = function(response) { if (this.state !== 'loading') { return; } this.state = 'loaded'; if (response.ok) { return this.deferred.resolve(response); } else { return this.deferred.reject(response); } }; Request.prototype.csrfHeader = function() { return up.protocol.csrfHeader(); }; Request.prototype.csrfParam = function() { return up.protocol.csrfParam(); }; Request.prototype.csrfToken = function() { if (!this.isSafe() && !this.isCrossOrigin()) { return up.protocol.csrfToken(); } }; Request.prototype.isCrossOrigin = function() { return u.isCrossOrigin(this.url); }; Request.prototype.extractResponseFromXHR = function() { var methodFromResponse, responseAttrs, urlFromResponse; responseAttrs = { method: this.method, url: this.url, request: this, xhr: this.xhr, text: this.xhr.responseText, status: this.xhr.status, title: up.protocol.titleFromXHR(this.xhr), target: up.protocol.targetFromXHR(this.xhr), acceptLayer: up.protocol.acceptLayerFromXHR(this.xhr), dismissLayer: up.protocol.dismissLayerFromXHR(this.xhr), eventPlans: up.protocol.eventPlansFromXHR(this.xhr), context: up.protocol.contextFromXHR(this.xhr), clearCache: up.protocol.clearCacheFromXHR(this.xhr) }; methodFromResponse = up.protocol.methodFromXHR(this.xhr); if (urlFromResponse = up.protocol.locationFromXHR(this.xhr)) { if (!methodFromResponse && !u.matchURLs(responseAttrs.url, urlFromResponse)) { methodFromResponse = 'GET'; } responseAttrs.url = urlFromResponse; } if (methodFromResponse) { responseAttrs.method = methodFromResponse; } return new up.Response(responseAttrs); }; Request.prototype.cacheKey = function() { return JSON.stringify([this.method, this.url, this.params.toQuery(), this.metaProps()]); }; Request.prototype.metaProps = function() { var i, key, len, props, ref, value; props = {}; ref = u.evalOption(up.network.config.requestMetaKeys, this); for (i = 0, len = ref.length; i < len; i++) { key = ref[i]; value = this[key]; if (u.isGiven(value)) { props[key] = value; } } return props; }; Request.prototype.buildEventEmitter = function(args) { return up.EventEmitter.fromEmitArgs(args, { request: this, layer: this.layer }); }; Request.prototype.emit = function() { var args; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; return this.buildEventEmitter(args).emit(); }; Request.prototype.assertEmitted = function() { var args; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; return this.buildEventEmitter(args).assertEmitted(); }; Request.getter('description', function() { return this.method + ' ' + this.url; }); return Request; })(up.Record); }).call(this); (function() { var u, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty; u = up.util; up.Request.Cache = (function(superClass) { extend(Cache, superClass); function Cache() { return Cache.__super__.constructor.apply(this, arguments); } Cache.prototype.maxSize = function() { return up.network.config.cacheSize; }; Cache.prototype.expiryMillis = function() { return up.network.config.cacheExpiry; }; Cache.prototype.normalizeStoreKey = function(request) { return up.Request.wrap(request).cacheKey(); }; Cache.prototype.clear = function(pattern) { if (pattern && pattern !== '*' && pattern !== true) { pattern = new up.URLPattern(pattern); return this.each((function(_this) { return function(key, request) { if (pattern.test(request.url)) { return _this.store.remove(key); } }; })(this)); } else { return Cache.__super__.clear.call(this); } }; return Cache; })(up.Cache); }).call(this); (function() { var HTML_FORM_METHODS, e, u; u = up.util; e = up.element; HTML_FORM_METHODS = ['GET', 'POST']; up.Request.FormRenderer = (function() { function FormRenderer(request) { this.request = request; } FormRenderer.prototype.buildAndSubmit = function() { var action, contentType, csrfParam, csrfToken, method, paramsFromQuery; this.params = u.copy(this.request.params); action = this.request.url; method = this.request.method; paramsFromQuery = up.Params.fromURL(action); this.params.addAll(paramsFromQuery); action = up.Params.stripURL(action); if (!u.contains(HTML_FORM_METHODS, method)) { method = up.protocol.wrapMethod(method, this.params); } this.form = e.affix(document.body, 'form.up-request-loader', { method: method, action: action }); if (contentType = this.request.contentType) { this.form.setAttribute('enctype', contentType); } if ((csrfParam = this.request.csrfParam()) && (csrfToken = this.request.csrfToken())) { this.params.add(csrfParam, csrfToken); } u.each(this.params.toArray(), this.addField.bind(this)); return up.browser.submitForm(this.form); }; FormRenderer.prototype.addField = function(attrs) { return e.affix(this.form, 'input[type=hidden]', attrs); }; return FormRenderer; })(); }).call(this); (function() { var u, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty; u = up.util; up.Request.Queue = (function(superClass) { extend(Queue, superClass); function Queue(options) { var ref, ref1; if (options == null) { options = {}; } this.concurrency = (ref = options.concurrency) != null ? ref : function() { return up.network.config.concurrency; }; this.badResponseTime = (ref1 = options.badResponseTime) != null ? ref1 : function() { return up.network.config.badResponseTime; }; this.reset(); } Queue.prototype.reset = function() { this.queuedRequests = []; this.currentRequests = []; clearTimeout(this.checkSlowTimout); return this.emittedSlow = false; }; Queue.getter('allRequests', function() { return this.currentRequests.concat(this.queuedRequests); }); Queue.prototype.asap = function(request) { request.runQueuedCallbacks(); u.always(request, (function(_this) { return function(responseOrError) { return _this.onRequestSettled(request, responseOrError); }; })(this)); request.queueTime = new Date(); this.setSlowTimer(); if (this.hasConcurrencyLeft()) { return this.sendRequestNow(request); } else { return this.queueRequest(request); } }; Queue.prototype.promoteToForeground = function(request) { if (request.preload) { request.preload = false; return this.setSlowTimer(); } }; Queue.prototype.setSlowTimer = function() { var badResponseTime; badResponseTime = u.evalOption(this.badResponseTime); return this.checkSlowTimout = u.timer(badResponseTime, (function(_this) { return function() { return _this.checkSlow(); }; })(this)); }; Queue.prototype.hasConcurrencyLeft = function() { var maxConcurrency; maxConcurrency = u.evalOption(this.concurrency); return maxConcurrency === -1 || this.currentRequests.length < maxConcurrency; }; Queue.prototype.isBusy = function() { return this.currentRequests.length > 0; }; Queue.prototype.queueRequest = function(request) { return this.queuedRequests.push(request); }; Queue.prototype.pluckNextRequest = function() { var request; request = u.find(this.queuedRequests, function(request) { return !request.preload; }); request || (request = this.queuedRequests[0]); return u.remove(this.queuedRequests, request); }; Queue.prototype.sendRequestNow = function(request) { if (request.emit('up:request:load', { log: ['Loading %s %s', request.method, request.url] }).defaultPrevented) { return request.abort('Prevented by event listener'); } else { request.normalizeForCaching(); this.currentRequests.push(request); return request.load(); } }; Queue.prototype.onRequestSettled = function(request, responseOrError) { u.remove(this.currentRequests, request); if ((responseOrError instanceof up.Response) && responseOrError.ok) { up.network.registerAliasForRedirect(request, responseOrError); } this.checkSlow(); return u.microtask((function(_this) { return function() { return _this.poke(); }; })(this)); }; Queue.prototype.poke = function() { var request; if (this.hasConcurrencyLeft() && (request = this.pluckNextRequest())) { return this.sendRequestNow(request); } }; Queue.prototype.abort = function(conditions) { var i, len, list, matches, ref; if (conditions == null) { conditions = true; } ref = [this.currentRequests, this.queuedRequests]; for (i = 0, len = ref.length; i < len; i++) { list = ref[i]; matches = u.filter(list, (function(_this) { return function(request) { return _this.requestMatches(request, conditions); }; })(this)); matches.forEach(function(match) { match.abort(); return u.remove(list, match); }); return; } }; Queue.prototype.abortExcept = function(excusedRequest, additionalConditions) { var excusedCacheKey; if (additionalConditions == null) { additionalConditions = true; } excusedCacheKey = excusedRequest.cacheKey(); return this.abort(function(queuedRequest) { return queuedRequest.cacheKey() !== excusedCacheKey && u.evalOption(additionalConditions, queuedRequest); }); }; Queue.prototype.requestMatches = function(request, conditions) { return request === conditions || u.evalOption(conditions, request); }; Queue.prototype.checkSlow = function() { var currentSlow; currentSlow = this.isSlow(); if (this.emittedSlow !== currentSlow) { this.emittedSlow = currentSlow; if (currentSlow) { return up.emit('up:request:late', { log: 'Server is slow to respond' }); } else { return up.emit('up:request:recover', { log: 'Slow requests were loaded' }); } } }; Queue.prototype.isSlow = function() { var allForegroundRequests, delay, now, timerTolerance; now = new Date(); delay = u.evalOption(this.badResponseTime); allForegroundRequests = u.reject(this.allRequests, 'preload'); timerTolerance = 1; return u.some(allForegroundRequests, function(request) { return (now - request.queueTime) >= (delay - timerTolerance); }); }; return Queue; })(up.Class); }).call(this); (function() { var CONTENT_TYPE_FORM_DATA, CONTENT_TYPE_URL_ENCODED, u; CONTENT_TYPE_URL_ENCODED = 'application/x-www-form-urlencoded'; CONTENT_TYPE_FORM_DATA = 'multipart/form-data'; u = up.util; up.Request.XHRRenderer = (function() { function XHRRenderer(request) { this.request = request; } XHRRenderer.prototype.buildAndSend = function(handlers) { var contentType, csrfHeader, csrfToken, header, key, ref, ref1, value; this.xhr = new XMLHttpRequest(); this.params = u.copy(this.request.params); if (this.request.timeout) { this.xhr.timeout = this.request.timeout; } this.xhr.open(this.getMethod(), this.request.url); ref = this.request.metaProps(); for (key in ref) { value = ref[key]; header = up.protocol.headerize(key); this.addHeader(header, value); } ref1 = this.request.headers; for (header in ref1) { value = ref1[header]; this.addHeader(header, value); } if ((csrfHeader = this.request.csrfHeader()) && (csrfToken = this.request.csrfToken())) { this.addHeader(csrfHeader, csrfToken); } this.addHeader(up.protocol.headerize('version'), up.version); if (contentType = this.getContentType()) { this.addHeader('Content-Type', contentType); } u.assign(this.xhr, handlers); this.xhr.send(this.getPayload()); return this.xhr; }; XHRRenderer.prototype.getMethod = function() { if (!this.method) { this.method = this.request.method; if (this.request.wrapMethod && !this.request.will302RedirectWithGET()) { this.method = up.protocol.wrapMethod(this.method, this.params); } } return this.method; }; XHRRenderer.prototype.getContentType = function() { this.finalizePayload(); return this.contentType; }; XHRRenderer.prototype.getPayload = function() { this.finalizePayload(); return this.payload; }; XHRRenderer.prototype.addHeader = function(header, value) { if (u.isOptions(value) || u.isArray(value)) { value = JSON.stringify(value); } return this.xhr.setRequestHeader(header, value); }; XHRRenderer.prototype.finalizePayload = function() { if (this.payloadFinalized) { return; } this.payloadFinalized = true; this.payload = this.request.payload; this.contentType = this.request.contentType; if (!this.payload && this.request.allowsPayload()) { if (!this.contentType) { this.contentType = this.params.hasBinaryValues() ? CONTENT_TYPE_FORM_DATA : CONTENT_TYPE_URL_ENCODED; } if (this.contentType === CONTENT_TYPE_FORM_DATA) { this.contentType = null; return this.payload = this.params.toFormData(); } else { return this.payload = this.params.toQuery().replace(/%20/g, '+'); } } }; return XHRRenderer; })(); }).call(this); (function() { var u, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty; u = up.util; /*** A response to an [HTTP request](`up.request()`). \#\#\# Example up.request('/foo').then(function(response) { console.log(response.status) // 200 console.log(response.text) // "..." }) @class up.Response */ up.Response = (function(superClass) { extend(Response, superClass); function Response() { return Response.__super__.constructor.apply(this, arguments); } /*** The HTTP method used for the request that produced this response. This is usually the HTTP method used by the initial request, but if the server redirected multiple requests may have been involved. In this case this property reflects the method used by the last request. If the response's URL changed from the request's URL, Unpoly will assume a redirect and set the method to `GET`. Also see the `X-Up-Method` header. @property up.Response#method @param {string} method @stable */ /*** The URL used for the response. This is usually the requested URL, or the final URL after the server redirected. On Internet Explorer 11 this property is only set when the server sends an `X-Up-Location` header. @property up.Response#url @param {string} url @stable */ /*** The response body as a `string`. @property up.Response#text @param {string} text @stable */ /*** The response's [HTTP status code](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes) as a `number`. A successful response will usually have a `200` or `201' status code. @property up.Response#status @param {number} status @stable */ /*** The original [request](/up.Request) that triggered this response. @property up.Response#request @param {up.Request} request @experimental */ /*** The [`XMLHttpRequest`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) object that was used to create this response. @property up.Response#xhr @param {XMLHttpRequest} xhr @experimental */ /*** A [document title pushed by the server](/X-Up-Title). If the server pushed no title via HTTP header, this will be `undefined`. @property up.Response#title @param {string} [title] @experimental */ /*** A [render target pushed by the server](/X-Up-Target). If the server pushed no title via HTTP header, this will be `undefined`. @property up.Response#target @param {string} [target] @experimental */ /*** Changes to the current [context](/context) as [set by the server](/X-Up-Context). @property up.Response#context @experimental */ Response.prototype.keys = function() { return ['method', 'url', 'text', 'status', 'request', 'xhr', 'target', 'title', 'acceptLayer', 'dismissLayer', 'eventPlans', 'context', 'clearCache', 'headers']; }; Response.prototype.defaults = function() { return { headers: {} }; }; /*** Returns whether the server responded with a 2xx HTTP status. @property up.Response#ok @param {boolean} ok @stable */ Response.getter('ok', function() { return this.status && (this.status >= 200 && this.status <= 299); }); /*** Returns the HTTP header value with the given name. The search for the header name is case-insensitive. Returns `undefined` if the given header name was not included in the response. @function up.Response#getHeader @param {string} name @return {string|undefined} value @experimental */ Response.prototype.getHeader = function(name) { var ref; return this.headers[name] || ((ref = this.xhr) != null ? ref.getResponseHeader(name) : void 0); }; /*** The response's [content-type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type). @property up.Response#contentType @param {string} contentType @experimental */ Response.getter('contentType', function() { return this.getHeader('Content-Type'); }); /*** The response body parsed as a JSON string. The parsed JSON object is cached with the response object, so multiple accesses will call `JSON.parse()` only once. \#\#\# Example response = await up.request('/profile.json') console.log("User name is " + response.json.name) @property up.Response#json @param {Object} json @stable */ Response.getter('json', function() { return this.parsedJSON || (this.parsedJSON = JSON.parse(this.text)); }); return Response; })(up.Record); }).call(this); (function() { var e, u; u = up.util; e = up.element; up.ResponseDoc = (function() { function ResponseDoc(options) { this.noscriptWrapper = new up.HTMLWrapper('noscript'); this.scriptWrapper = new up.HTMLWrapper('script'); this.root = this.parseDocument(options) || this.parseFragment(options) || this.parseContent(options); } ResponseDoc.prototype.parseDocument = function(options) { return this.parse(options.document, e.createDocumentFromHTML); }; ResponseDoc.prototype.parseContent = function(options) { var content, matchingElement, target; content = options.content || ''; target = options.target || up.fail("must pass a { target } when passing { content }"); matchingElement = e.createFromSelector(target); if (u.isString(content)) { content = this.wrapHTML(content); matchingElement.innerHTML = content; } else { matchingElement.appendChild(content); } return matchingElement; }; ResponseDoc.prototype.parseFragment = function(options) { return this.parse(options.fragment); }; ResponseDoc.prototype.parse = function(value, parseFn) { if (parseFn == null) { parseFn = e.createFromHTML; } if (u.isString(value)) { value = this.wrapHTML(value); value = parseFn(value); } return value; }; ResponseDoc.prototype.rootSelector = function() { return up.fragment.toTarget(this.root); }; ResponseDoc.prototype.wrapHTML = function(html) { html = this.noscriptWrapper.wrap(html); if (up.fragment.config.runScripts) { html = this.scriptWrapper.wrap(html); } else { html = this.scriptWrapper.strip(html); } return html; }; ResponseDoc.prototype.getTitle = function() { var ref; if (!this.titleParsed) { this.title = (ref = this.root.querySelector("head title")) != null ? ref.textContent : void 0; this.titleParsed = true; } return this.title; }; ResponseDoc.prototype.select = function(selector) { return up.fragment.subtree(this.root, selector, { layer: 'any' })[0]; }; ResponseDoc.prototype.finalizeElement = function(element) { this.noscriptWrapper.unwrap(element); return this.scriptWrapper.unwrap(element); }; return ResponseDoc; })(); }).call(this); (function() { var e, u; e = up.element; u = up.util; up.RevealMotion = (function() { function RevealMotion(element, options) { var ref, ref1, ref2, ref3, ref4, ref5, ref6, ref7, viewportConfig; this.element = element; this.options = options != null ? options : {}; viewportConfig = up.viewport.config; this.viewport = e.get(this.options.viewport) || up.viewport.get(this.element); this.obstructionsLayer = up.layer.get(this.viewport); this.snap = (ref = (ref1 = this.options.snap) != null ? ref1 : this.options.revealSnap) != null ? ref : viewportConfig.revealSnap; this.padding = (ref2 = (ref3 = this.options.padding) != null ? ref3 : this.options.revealPadding) != null ? ref2 : viewportConfig.revealPadding; this.top = (ref4 = (ref5 = this.options.top) != null ? ref5 : this.options.revealTop) != null ? ref4 : viewportConfig.revealTop; this.max = (ref6 = (ref7 = this.options.max) != null ? ref7 : this.options.revealMax) != null ? ref6 : viewportConfig.revealMax; this.topObstructions = viewportConfig.fixedTop; this.bottomObstructions = viewportConfig.fixedBottom; } RevealMotion.prototype.start = function() { var diff, elementRect, maxPixels, newScrollTop, originalScrollTop, viewportRect; viewportRect = this.getViewportRect(this.viewport); elementRect = up.Rect.fromElement(this.element); if (this.max) { maxPixels = u.evalOption(this.max, this.element); elementRect.height = Math.min(elementRect.height, maxPixels); } this.addPadding(elementRect); this.substractObstructions(viewportRect); if (viewportRect.height < 0) { return up.error.failed.async('Viewport has no visible area'); } originalScrollTop = this.viewport.scrollTop; newScrollTop = originalScrollTop; if (this.top || elementRect.height > viewportRect.height) { diff = elementRect.top - viewportRect.top; newScrollTop += diff; } else if (elementRect.top < viewportRect.top) { newScrollTop -= viewportRect.top - elementRect.top; } else if (elementRect.bottom > viewportRect.bottom) { newScrollTop += elementRect.bottom - viewportRect.bottom; } else { } if (u.isNumber(this.snap) && newScrollTop < this.snap && elementRect.top < (0.5 * viewportRect.height)) { newScrollTop = 0; } if (newScrollTop !== originalScrollTop) { return this.scrollTo(newScrollTop); } else { return Promise.resolve(); } }; RevealMotion.prototype.scrollTo = function(newScrollTop) { this.scrollMotion = new up.ScrollMotion(this.viewport, newScrollTop, this.options); return this.scrollMotion.start(); }; RevealMotion.prototype.getViewportRect = function() { if (up.viewport.isRoot(this.viewport)) { return new up.Rect({ left: 0, top: 0, width: up.viewport.rootWidth(), height: up.viewport.rootHeight() }); } else { return up.Rect.fromElement(this.viewport); } }; RevealMotion.prototype.addPadding = function(elementRect) { elementRect.top -= this.padding; return elementRect.height += 2 * this.padding; }; RevealMotion.prototype.selectObstructions = function(selectors) { return up.fragment.all(selectors.join(','), { layer: this.obstructionsLayer }); }; RevealMotion.prototype.substractObstructions = function(viewportRect) { var diff, i, j, len, len1, obstruction, obstructionRect, ref, ref1, results; ref = this.selectObstructions(this.topObstructions); for (i = 0, len = ref.length; i < len; i++) { obstruction = ref[i]; obstructionRect = up.Rect.fromElement(obstruction); diff = obstructionRect.bottom - viewportRect.top; if (diff > 0) { viewportRect.top += diff; viewportRect.height -= diff; } } ref1 = this.selectObstructions(this.bottomObstructions); results = []; for (j = 0, len1 = ref1.length; j < len1; j++) { obstruction = ref1[j]; obstructionRect = up.Rect.fromElement(obstruction); diff = viewportRect.bottom - obstructionRect.top; if (diff > 0) { results.push(viewportRect.height -= diff); } else { results.push(void 0); } } return results; }; RevealMotion.prototype.finish = function() { var ref; return (ref = this.scrollMotion) != null ? ref.finish() : void 0; }; return RevealMotion; })(); }).call(this); (function() { var u, bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; u = up.util; up.ScrollMotion = (function() { var SPEED_CALIBRATION; SPEED_CALIBRATION = 0.065; function ScrollMotion(scrollable, targetTop, options) { var ref, ref1, ref2, ref3; this.scrollable = scrollable; this.targetTop = targetTop; if (options == null) { options = {}; } this.abort = bind(this.abort, this); this.behavior = (ref = (ref1 = options.behavior) != null ? ref1 : options.scrollBehavior) != null ? ref : 'auto'; this.speed = ((ref2 = (ref3 = options.speed) != null ? ref3 : options.scrollSpeed) != null ? ref2 : up.viewport.config.scrollSpeed) * SPEED_CALIBRATION; } ScrollMotion.prototype.start = function() { return new Promise((function(_this) { return function(resolve, reject) { _this.resolve = resolve; _this.reject = reject; if (_this.behavior === 'smooth' && up.motion.isEnabled()) { return _this.startAnimation(); } else { return _this.finish(); } }; })(this)); }; ScrollMotion.prototype.startAnimation = function() { this.startTime = Date.now(); this.startTop = this.scrollable.scrollTop; this.topDiff = this.targetTop - this.startTop; this.duration = Math.sqrt(Math.abs(this.topDiff)) / this.speed; return requestAnimationFrame((function(_this) { return function() { return _this.animationFrame(); }; })(this)); }; ScrollMotion.prototype.animationFrame = function() { var currentTime, timeElapsed, timeFraction; if (this.settled) { return; } if (this.frameTop && Math.abs(this.frameTop - this.scrollable.scrollTop) > 1.5) { this.abort('Animation aborted due to user intervention'); } currentTime = Date.now(); timeElapsed = currentTime - this.startTime; timeFraction = Math.min(timeElapsed / this.duration, 1); this.frameTop = this.startTop + (u.simpleEase(timeFraction) * this.topDiff); if (Math.abs(this.targetTop - this.frameTop) < 0.3) { return this.finish(); } else { this.scrollable.scrollTop = this.frameTop; return requestAnimationFrame((function(_this) { return function() { return _this.animationFrame(); }; })(this)); } }; ScrollMotion.prototype.abort = function(reason) { this.settled = true; return this.reject(up.error.aborted(reason)); }; ScrollMotion.prototype.finish = function() { this.settled = true; this.scrollable.scrollTop = this.targetTop; return this.resolve(); }; return ScrollMotion; })(); }).call(this); (function() { var e, u; e = up.element; u = up.util; up.Selector = (function() { function Selector(selectors, filters) { this.selectors = selectors; this.filters = filters != null ? filters : []; this.unionSelector = this.selectors.join(',') || 'match-none'; } Selector.prototype.matches = function(element) { return e.matches(element, this.unionSelector) && this.passesFilter(element); }; Selector.prototype.closest = function(element) { var parentElement; if (this.matches(element)) { return element; } else if ((parentElement = element.parentElement)) { return this.closest(parentElement); } }; Selector.prototype.passesFilter = function(element) { return u.every(this.filters, function(filter) { return filter(element); }); }; Selector.prototype.descendants = function(root) { var results; results = u.flatMap(this.selectors, function(selector) { return e.all(root, selector); }); return u.filter(results, (function(_this) { return function(element) { return _this.passesFilter(element); }; })(this)); }; Selector.prototype.subtree = function(root) { var results; results = []; if (this.matches(root)) { results.push(root); } results.push.apply(results, this.descendants(root)); return results; }; return Selector; })(); }).call(this); (function() { var u; up.store || (up.store = {}); u = up.util; up.store.Memory = (function() { function Memory() { this.clear(); } Memory.prototype.clear = function() { return this.data = {}; }; Memory.prototype.get = function(key) { return this.data[key]; }; Memory.prototype.set = function(key, value) { return this.data[key] = value; }; Memory.prototype.remove = function(key) { return delete this.data[key]; }; Memory.prototype.keys = function() { return Object.keys(this.data); }; Memory.prototype.size = function() { return this.keys().length; }; Memory.prototype.values = function() { return u.values(this.data); }; return Memory; })(); }).call(this); (function() { var u, bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, hasProp = {}.hasOwnProperty; u = up.util; up.store.Session = (function(superClass) { extend(Session, superClass); function Session(rootKey) { this.remove = bind(this.remove, this); this.set = bind(this.set, this); this.rootKey = rootKey; this.loadFromSessionStorage(); } Session.prototype.clear = function() { Session.__super__.clear.call(this); return this.saveToSessionStorage(); }; Session.prototype.set = function(key, value) { Session.__super__.set.call(this, key, value); return this.saveToSessionStorage(); }; Session.prototype.remove = function(key) { Session.__super__.remove.call(this, key); return this.saveToSessionStorage(); }; Session.prototype.loadFromSessionStorage = function() { var raw; try { if (raw = typeof sessionStorage !== "undefined" && sessionStorage !== null ? sessionStorage.getItem(this.rootKey) : void 0) { this.data = JSON.parse(raw); } } catch (error) { } return this.data || (this.data = {}); }; Session.prototype.saveToSessionStorage = function() { var json; json = JSON.stringify(this.data); try { return typeof sessionStorage !== "undefined" && sessionStorage !== null ? sessionStorage.setItem(this.rootKey, json) : void 0; } catch (error) { } }; return Session; })(up.store.Memory); }).call(this); (function() { var e, u, bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; u = up.util; e = up.element; up.Tether = (function() { function Tether(options) { this.sync = bind(this.sync, this); this.scheduleSync = bind(this.scheduleSync, this); var base; if (typeof (base = up.migrate).handleTetherOptions === "function") { base.handleTetherOptions(options); } this.anchor = options.anchor; this.align = options.align; this.position = options.position; this.alignAxis = this.position === 'top' || this.position === 'bottom' ? 'horizontal' : 'vertical'; this.viewport = up.viewport.get(this.anchor); this.parent = this.viewport === e.root ? document.body : this.viewport; this.syncOnScroll = !this.viewport.contains(this.anchor.offsetParent); } Tether.prototype.start = function(element) { this.element = element; this.element.style.position = 'absolute'; this.setOffset(0, 0); this.sync(); return this.changeEventSubscription('on'); }; Tether.prototype.stop = function() { return this.changeEventSubscription('off'); }; Tether.prototype.changeEventSubscription = function(fn) { up[fn](window, 'resize', this.scheduleSync); if (this.syncOnScroll) { return up[fn](this.viewport, 'scroll', this.scheduleSync); } }; Tether.prototype.scheduleSync = function() { clearTimeout(this.syncTimer); return this.syncTimer = u.task(this.sync); }; Tether.prototype.isDetached = function() { return e.isDetached(this.parent) || e.isDetached(this.anchor); }; Tether.prototype.sync = function() { var anchorBox, elementBox, elementMargin, left, top; elementBox = this.element.getBoundingClientRect(); elementMargin = { top: e.styleNumber(this.element, 'marginTop'), right: e.styleNumber(this.element, 'marginRight'), bottom: e.styleNumber(this.element, 'marginBottom'), left: e.styleNumber(this.element, 'marginLeft') }; anchorBox = this.anchor.getBoundingClientRect(); left = void 0; top = void 0; switch (this.alignAxis) { case 'horizontal': top = (function() { switch (this.position) { case 'top': return anchorBox.top - elementMargin.bottom - elementBox.height; case 'bottom': return anchorBox.top + anchorBox.height + elementMargin.top; } }).call(this); left = (function() { switch (this.align) { case 'left': return anchorBox.left + elementMargin.left; case 'center': return anchorBox.left + 0.5 * (anchorBox.width - elementBox.width); case 'right': return anchorBox.left + anchorBox.width - elementBox.width - elementMargin.right; } }).call(this); break; case 'vertical': top = (function() { switch (this.align) { case 'top': return anchorBox.top + elementMargin.top; case 'center': return anchorBox.top + 0.5 * (anchorBox.height - elementBox.height); case 'bottom': return anchorBox.top + anchorBox.height - elementBox.height - elementMargin.bottom; } }).call(this); left = (function() { switch (this.position) { case 'left': return anchorBox.left - elementMargin.right - elementBox.width; case 'right': return anchorBox.left + anchorBox.width + elementMargin.left; } }).call(this); } if (u.isDefined(left) || u.isDefined(top)) { return this.moveTo(left, top); } else { return up.fail('Invalid tether constraints: %o', this.describeConstraints()); } }; Tether.prototype.describeConstraints = function() { return { position: this.position, align: this.align }; }; Tether.prototype.moveTo = function(targetLeft, targetTop) { var elementBox; elementBox = this.element.getBoundingClientRect(); return this.setOffset(targetLeft - elementBox.left + this.offsetLeft, targetTop - elementBox.top + this.offsetTop); }; Tether.prototype.setOffset = function(left, top) { this.offsetLeft = left; this.offsetTop = top; return e.setStyle(this.element, { left: left, top: top }); }; return Tether; })(); }).call(this); (function() { var u; u = up.util; up.URLPattern = (function() { function URLPattern(fullPattern, normalizeURL) { var negativeList, positiveList; this.normalizeURL = normalizeURL != null ? normalizeURL : u.normalizeURL; this.groups = []; positiveList = []; negativeList = []; u.splitValues(fullPattern).forEach(function(pattern) { if (pattern[0] === '-') { return negativeList.push(pattern.substring(1)); } else { return positiveList.push(pattern); } }); this.positiveRegexp = this.buildRegexp(positiveList, true); this.negativeRegexp = this.buildRegexp(negativeList, false); } URLPattern.prototype.buildRegexp = function(list, capture) { var reCode; if (!list.length) { return; } reCode = list.map(this.normalizeURL).map(u.escapeRegExp).join('|'); reCode = reCode.replace(/\\\*/g, '.*?'); reCode = reCode.replace(/(\:|\\\$)([a-z][\w-]*)/ig, (function(_this) { return function(match, type, name) { if (type === '\\$') { if (capture) { _this.groups.push({ name: name, cast: Number }); } return '(\\d+)'; } else { if (capture) { _this.groups.push({ name: name, cast: String }); } return '([^/?#]+)'; } }; })(this)); return new RegExp('^' + reCode + '$'); }; URLPattern.prototype.test = function(url, doNormalize) { if (doNormalize == null) { doNormalize = true; } if (doNormalize) { url = this.normalizeURL(url); } return this.positiveRegexp.test(url) && !this.isExcluded(url); }; URLPattern.prototype.recognize = function(url, doNormalize) { var match, resolution; if (doNormalize == null) { doNormalize = true; } if (doNormalize) { url = this.normalizeURL(url); } if ((match = this.positiveRegexp.exec(url)) && !this.isExcluded(url)) { resolution = {}; this.groups.forEach((function(_this) { return function(group, groupIndex) { var value; if (value = match[groupIndex + 1]) { return resolution[group.name] = group.cast(value); } }; })(this)); return resolution; } }; URLPattern.prototype.isExcluded = function(url) { var ref; return (ref = this.negativeRegexp) != null ? ref.test(url) : void 0; }; return URLPattern; })(); }).call(this); /*** @module up.framework */ (function() { up.framework = (function() { var boot, booting, emitReset, startExtension, stopExtension, u; u = up.util; booting = true; /*** Resets Unpoly to the state when it was booted. All custom event handlers, animations, etc. that have been registered will be discarded. Emits event [`up:framework:reset`](/up:framework:reset). @function up.framework.reset @internal */ emitReset = function() { return up.emit('up:framework:reset', { log: false }); }; /*** This event is [emitted](/up.emit) when Unpoly is [reset](/up.framework.reset) during unit tests. @event up:framework:reset @internal */ /*** Boots the Unpoly framework. **This is called automatically** by including the Unpoly JavaScript files. Unpoly will not boot if the current browser is [not supported](/up.browser.isSupported). This leaves you with a classic server-side application on legacy browsers. @function up.boot @internal */ boot = function() { if (up.browser.isSupported()) { up.emit('up:framework:boot', { log: false }); booting = false; return up.event.onReady(function() { return up.emit('up:app:boot', { log: 'Booting user application' }); }); } else { return typeof console.log === "function" ? console.log("Unpoly doesn't support this browser. Framework was not booted.") : void 0; } }; startExtension = function() { return booting = true; }; stopExtension = function() { return booting = false; }; return u.literal({ boot: boot, startExtension: startExtension, stopExtension: stopExtension, reset: emitReset, get_booting: function() { return booting; } }); })(); }).call(this); /*** Events ====== This module contains functions to [emit](/up.emit) and [observe](/up.on) DOM events. While the browser also has built-in functions to work with events, you will find Unpoly's functions to be very concise and feature-rich. ## Events emitted by Unpoly Most Unpoly features emit events that are prefixed with `up:`. Unpoly's own events are documented in their respective modules, for example: | Event | Module | |-----------------------|--------------------| | `up:link:follow` | `up.link` | | `up:form:submit` | `up.form` | | `up:layer:open` | `up.layer` | | `up:request:late` | `up.network` | @see up.on @see up.emit @module up.event */ (function() { var slice = [].slice; up.event = (function() { var $bind, assertEmitted, bind, bindNow, build, buildEmitter, e, emit, executeEmitAttr, fork, halt, isUnmodified, keyModifiers, onEscape, onReady, reset, u, unbind, wasEscapePressed; u = up.util; e = up.element; reset = function() { var element, i, len, ref, results; ref = [window, document, e.root, document.body]; results = []; for (i = 0, len = ref.length; i < len; i++) { element = ref[i]; results.push(up.EventListener.unbindNonDefault(element)); } return results; }; /*** Listens to a [DOM event](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Events) on `document` or a given element. `up.on()` has some quality of life improvements over [`Element#addEventListener()`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener): - You may pass a selector for [event delegation](https://davidwalsh.name/event-delegate). - The event target is automatically passed as a second argument. - You may register a listener to multiple events by passing a space-separated list of event name (e.g. `"click mousedown"`) - You may register a listener to multiple elements in a single `up.on()` call, by passing a [list](/up.util.isList) of elements. - You use an [`[up-data]`](/up-data) attribute to [attach structured data](/up.on#attaching-structured-data) to observed elements. If an `[up-data]` attribute is set, its value will automatically be parsed as JSON and passed as a third argument. \#\#\# Basic example The code below will call the listener when a `` is clicked anywhere in the `document`: up.on('click', 'a', function(event, element) { console.log("Click on a link %o", element) }) You may also bind the listener to a given element instead of `document`: var form = document.querySelector('form') up.on(form, 'click', function(event, form) { console.log("Click within %o", form) }) \#\#\# Event delegation You may pass both an element and a selector for [event delegation](https://davidwalsh.name/event-delegate). The example below registers a single event listener to the given `form`, but only calls the listener when the clicked element is a `select` element: var form = document.querySelector('form') up.on(form, 'click', 'select', function(event, select) { console.log("Click on select %o within %o", select, form) }) \#\#\# 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: Bob Jim The JSON will be 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) }) \#\#\# Unbinding an event listener `up.on()` returns a function that unbinds the event listeners when called: // Define the listener var listener = function(event) { ... } // Binding the listener returns an unbind function var unbind = up.on('click', listener) // Unbind the listener unbind() There is also a function [`up.off()`](/up.off) which you can use for the same purpose: // Define the listener var listener = function(event) { ... } // Bind the listener up.on('click', listener) // Unbind the listener up.off('click', listener) \#\#\# Binding to multiple elements You may register a listener to multiple elements in a single `up.on()` call, by passing a [list](/up.util.isList) of elements: ```javascript let allForms = document.querySelectorAll('form') up.on(allForms, 'submit', function(event, form) { console.log('Submitting form %o', form) }) ``` \#\#\# Binding to multiple event types You may register a listener to multiple event types by passing a space-separated list of event types: ```javascript let element = document.querySelector(...) up.on(element, 'mouseenter mouseleave', function(event) { console.log('Mouse entered or left') }) ``` @function up.on @param {Element|jQuery} [element=document] The element on which to register the event listener. If no element is given, the listener is registered on the `document`. @param {string|Array} types The event types to bind to. Multiple event types may be passed as either a space-separated string or as an array of types. @param {string|Function():string} [selector] The selector of an element on which the event must be triggered. Omit the selector to listen to all events of the given type, regardless of the event target. If the selector is not known in advance you may also pass a function that returns the selector. The function is evaluated every time an event with the given type is observed. @param {boolean} [options.passive=false] Whether to register a [passive event listener](https://developers.google.com/web/updates/2016/06/passive-event-listeners). A passive event listener may not call `event.preventDefault()`. This in particular may improve the frame rate when registering `touchstart` and `touchmove` events. @param {boolean} [options.once=true] Whether the listener should run at most once. If `true` the listener will automatically be unbound after the first invocation. @param {Function(event, [element], [data])} listener The listener function that should be called. The function takes the affected element as a second argument. If the element has an [`up-data`](/up-data) attribute, its value is parsed as JSON and passed as a third argument. @return {Function()} A function that unbinds the event listeners when called. @stable */ bind = function() { var args; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; return bindNow(args); }; /*** Listens to an event on `document` or a given element. The event handler is called with the event target as a [jQuery collection](https://learn.jquery.com/using-jquery-core/jquery-object/). If you're not using jQuery, use `up.on()` instead, which calls event handlers with a native element. \#\#\# Example ``` up.$on('click', 'a', function(event, $link) { console.log("Click on a link with destination %s", $element.attr('href')) }) ``` @function up.$on @param {Element|jQuery} [element=document] The element on which to register the event listener. If no element is given, the listener is registered on the `document`. @param {string} events A space-separated list of event names to bind to. @param {string} [selector] The selector of an element on which the event must be triggered. Omit the selector to listen to all events with that name, regardless of the event target. @param {boolean} [options.passive=false] Whether to register a [passive event listener](https://developers.google.com/web/updates/2016/06/passive-event-listeners). A passive event listener may not call `event.preventDefault()`. This in particular may improve the frame rate when registering `touchstart` and `touchmove` events. @param {Function(event, [element], [data])} listener The listener function that should be called. The function takes the affected element as the first argument). If the element has an [`up-data`](/up-data) attribute, its value is parsed as JSON and passed as a second argument. @return {Function()} A function that unbinds the event listeners when called. @stable */ $bind = function() { var args; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; return bindNow(args, { jQuery: true }); }; bindNow = function(args, options) { return up.EventListenerGroup.fromBindArgs(args, options).bind(); }; /*** Unbinds an event listener previously bound with `up.on()`. \#\#\# Example Let's say you are listing to clicks on `.button` elements: var listener = function() { ... } up.on('click', '.button', listener) You can stop listening to these events like this: up.off('click', '.button', listener) @function up.off @param {Element|jQuery} [element=document] @param {string|Function(): string} events @param {string} [selector] @param {Function(event, [element], [data])} listener The listener function to unbind. Note that you must pass a reference to the same function reference that was passed to `up.on()` earlier. @stable */ unbind = function() { var args; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; return up.EventListenerGroup.fromBindArgs(args).unbind(); }; buildEmitter = function(args) { return up.EventEmitter.fromEmitArgs(args); }; /*** Emits a event with the given name and properties. The event will be triggered as an event on `document` or on the given element. Other code can subscribe to events with that name using [`Element#addEventListener()`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) or [`up.on()`](/up.on). \#\#\# Example up.on('my:event', function(event) { console.log(event.foo) }) up.emit('my:event', { foo: 'bar' }) // Prints "bar" to the console @function up.emit @param {Element|jQuery} [target=document] The element on which the event is triggered. If omitted, the event will be emitted on the `document`. @param {string} eventType The event type, e.g. `my:event`. @param {Object} [props={}] A list of properties to become part of the event object that will be passed to listeners. @param {up.Layer|string|number} [props.layer] The [layer](/up.layer) on which to emit this event. If this property is set, the event will be emitted on the [layer's outmost element](/up.Layer.prototype.element). Also [up.layer.current](/up.layer.current) will be set to the given layer while event listeners are running. @param {string|Array} [props.log] A message to print to the [log](/up.log) when the event is emitted. Pass `false` to not log this event emission. @param {Element|jQuery} [props.target=document] The element on which the event is triggered. Alternatively the target element may be passed as the first argument. @stable */ emit = function() { var args; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; return buildEmitter(args).emit(); }; /*** Builds an event with the given type and properties. The returned event is not [emitted](/up.emit). \#\#\# Example let event = up.event.build('my:event', { foo: 'bar' }) console.log(event.type) // logs "my:event" console.log(event.foo) // logs "bar" console.log(event.defaultPrevented) // logs "false" up.emit(event) // emits the event @function up.event.build @param {string} [type] The event type. May also be passed as a property `{ type }`. @param {Object} [props={}] An object with event properties. @param {string} [props.type] The event type. May also be passed as a first string argument. @return {Event} @experimental */ build = function() { var args, event, originalPreventDefault, props, type; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; props = u.extractOptions(args); type = args[0] || props.type || up.fail('Expected event type to be passed as string argument or { type } property'); event = document.createEvent('Event'); event.initEvent(type, true, true); u.assign(event, u.omit(props, ['type', 'target'])); if (up.browser.isIE11()) { originalPreventDefault = event.preventDefault; event.preventDefault = function() { originalPreventDefault.call(event); return u.getter(event, 'defaultPrevented', function() { return true; }); }; } return event; }; /*** [Emits](/up.emit) the given event and throws an `AbortError` if it was prevented. @function up.event.assertEmitted @param {string} eventType @param {Object} eventProps @param {string|Array} [eventProps.message] @internal */ assertEmitted = function() { var args; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; return buildEmitter(args).assertEmitted(); }; /*** Registers an event listener to be called when the user presses the `Escape` key. \#\#\# Example ```javascript up.event.onEscape(function(event) { console.log('Escape pressed!') }) ``` @function up.event.onEscape @param {Function(Event)} listener The listener function that will be called when `Escape` is pressed. @experimental */ onEscape = function(listener) { return bind('keydown', function(event) { if (wasEscapePressed(event)) { return listener(event); } }); }; /*** Returns whether the given keyboard event involved the ESC key. @function up.util.wasEscapePressed @param {Event} event @internal */ wasEscapePressed = function(event) { var key; key = event.key; return key === 'Escape' || key === 'Esc'; }; /*** Prevents the event from being processed further. In detail: - It prevents the event from bubbling up the DOM tree. - It prevents other event handlers bound on the same element. - It prevents the event's default action. \#\#\# Example up.on('click', 'link.disabled', function(event) { up.event.halt(event) }) @function up.event.halt @param {Event} event @stable */ halt = function(event) { event.stopImmediatePropagation(); return event.preventDefault(); }; /*** Runs the given callback when the the initial HTML document has been completely loaded. The callback is guaranteed to see the fully parsed DOM tree. This function does not wait for stylesheets, images or frames to finish loading. If `up.event.onReady()` is called after the initial document was loaded, the given callback is run immediately. @function up.event.onReady @param {Function} callback The function to call then the DOM tree is acessible. @experimental */ onReady = function(callback) { if (document.readyState !== 'loading') { return callback(); } else { return document.addEventListener('DOMContentLoaded', callback); } }; keyModifiers = ['metaKey', 'shiftKey', 'ctrlKey', 'altKey']; /*** @function up.event.isUnmodified @internal */ isUnmodified = function(event) { return (u.isUndefined(event.button) || event.button === 0) && !u.some(keyModifiers, function(modifier) { return event[modifier]; }); }; fork = function(originalEvent, newType, copyKeys) { var newEvent; if (copyKeys == null) { copyKeys = []; } newEvent = up.event.build(newType, u.pick(originalEvent, copyKeys)); newEvent.originalEvent = originalEvent; ['stopPropagation', 'stopImmediatePropagation', 'preventDefault'].forEach(function(key) { var originalMethod; originalMethod = newEvent[key]; return newEvent[key] = function() { originalEvent[key](); return originalMethod.call(newEvent); }; }); if (originalEvent.defaultPrevented) { newEvent.preventDefault(); } return newEvent; }; /*** Emits the given event when this link is clicked. When the emitted event's default' is prevented, the original `click` event's default is also prevented. You may use this attribute to emit events when clicking on areas that are no hyperlinks, by setting it on an `` element without a `[href]` attribute. \#\#\# Example This hyperlink will emit an `user:select` event when clicked: ```html Alice ``` @selector a[up-emit] @param up-emit The type of the event to be emitted. @param [up-emit-props='{}'] The event properties, serialized as JSON. @stable */ executeEmitAttr = function(event, element) { var eventProps, eventType, forkedEvent; if (!isUnmodified(event)) { return; } eventType = e.attr(element, 'up-emit'); eventProps = e.jsonAttr(element, 'up-emit-props'); forkedEvent = fork(event, eventType); u.assign(forkedEvent, eventProps); return up.emit(element, forkedEvent); }; bind('up:click', 'a[up-emit]', executeEmitAttr); bind('up:framework:reset', reset); return { on: bind, $on: $bind, off: unbind, build: build, emit: emit, assertEmitted: assertEmitted, onEscape: onEscape, halt: halt, onReady: onReady, isUnmodified: isUnmodified, fork: fork, keyModifiers: keyModifiers }; })(); up.on = up.event.on; up.$on = up.event.$on; up.off = up.event.off; up.$off = up.event.off; up.emit = up.event.emit; }).call(this); /*** Server protocol =============== You rarely need to change server-side code to use Unpoly. You don't need to provide a JSON API, or add extra routes for AJAX requests. The server simply renders a series of full HTML pages, like it would without Unpoly. There is an **optional** protocol your server may use to exchange additional information when Unpoly is [updating fragments](/up.link). The protocol mostly works by adding additional HTTP headers (like `X-Up-Target`) to requests and responses. While the protocol can help you optimize performance and handle some edge cases, implementing it is **entirely optional**. For instance, `unpoly.com` itself is a static site that uses Unpoly on the frontend and doesn't even have an active server component. ## Existing implementations You should be able to implement the protocol in a very short time. There are existing implementations for various web frameworks: - [Ruby on Rails](/install/rails) - [Roda](https://github.com/adam12/roda-unpoly) - [Rack](https://github.com/adam12/rack-unpoly) (Sinatra, Padrino, Hanami, Cuba, ...) - [Phoenix](https://elixirforum.com/t/unpoly-a-framework-like-turbolinks/3614/15) (Elixir) - [PHP](https://github.com/webstronauts/php-unpoly) (Symfony, Laravel, Stack) @module up.protocol */ (function() { up.protocol = (function() { var acceptLayerFromXHR, clearCacheFromXHR, config, contextFromXHR, csrfHeader, csrfParam, csrfToken, dismissLayerFromXHR, e, eventPlansFromXHR, extractHeader, headerize, initialRequestMethod, locationFromXHR, methodFromXHR, reset, targetFromXHR, titleFromXHR, u, wrapMethod; u = up.util; e = up.element; headerize = function(camel) { var header; header = camel.replace(/(^.|[A-Z])/g, function(char) { return '-' + char.toUpperCase(); }); return 'X-Up' + header; }; extractHeader = function(xhr, shortHeader, parseFn) { var value; if (parseFn == null) { parseFn = u.identity; } if (value = xhr.getResponseHeader(headerize(shortHeader))) { return parseFn(value); } }; /*** This request header contains the current Unpoly version to mark this request as a fragment update. Server-side code may check for the presence of an `X-Up-Version` header to distinguish [fragment updates](/up.link) from full page loads. The `X-Up-Version` header is guaranteed to be set for all [requests made through Unpoly](/up.request). \#\#\# Example ```http X-Up-Version: 1.0.0 ``` @header X-Up-Version @stable */ /*** This request header contains the CSS selector targeted for a successful fragment update. Server-side code is free to optimize its response by only rendering HTML that matches the selector. For example, you might prefer to not render an expensive sidebar if the sidebar is not targeted. Unpoly will usually update a different selector in case the request fails. This selector is sent as a second header, `X-Up-Fail-Target`. The user may choose to not send this header by configuring `up.network.config.requestMetaKeys`. \#\#\# Example ```http X-Up-Target: .menu X-Up-Fail-Target: body ``` \#\#\# Changing the render target from the server The server may change the render target context by including a CSS selector as an `X-Up-Target` header in its response. ```http Content-Type: text/html X-Up-Target: .selector-from-server
...
``` The frontend will use the server-provided target for both successful (HTTP status `200 OK`) and failed (status `4xx` or `5xx`) responses. The server may also set a target of `:none` to have the frontend render nothing. In this case no response body is required: ```http Content-Type: text/html X-Up-Target: :none ``` @header X-Up-Target @stable */ /*** This request header contains the CSS selector targeted for a failed fragment update. A fragment update is considered *failed* if the server responds with a status code other than 2xx, but still renders HTML. Server-side code is free to optimize its response to a failed request by only rendering HTML that matches the provided selector. For example, you might prefer to not render an expensive sidebar if the sidebar is not targeted. The user may choose to not send this header by configuring `up.network.config.requestMetaKeys`. \#\#\# Example ```http X-Up-Target: .menu X-Up-Fail-Target: body ``` \#\#\# Signaling failed form submissions When [submitting a form via AJAX](/form-up-submit) Unpoly needs to know whether the form submission has failed (to update the form with validation errors) or succeeded (to update the `[up-target]` selector). For Unpoly to be able to detect a failed form submission, the response must be return a non-2xx HTTP status code. We recommend to use either 400 (bad request) or 422 (unprocessable entity). To do so in [Ruby on Rails](http://rubyonrails.org/), pass a [`:status` option to `render`](http://guides.rubyonrails.org/layouts_and_rendering.html#the-status-option): ```ruby class UsersController < ApplicationController def create user_params = params[:user].permit(:email, :password) @user = User.new(user_params) if @user.save? sign_in @user else render 'form', status: :bad_request end end end ``` @header X-Up-Fail-Target @stable */ /*** This request header contains the targeted layer's [mode](/up.layer.mode). Server-side code is free to render different HTML for different modes. For example, you might prefer to not render a site navigation for overlays. The user may choose to not send this header by configuring `up.network.config.requestMetaKeys`. \#\#\# Example ```http X-Up-Mode: drawer ``` @header X-Up-Mode @stable */ /*** This request header contains the [mode](/up.layer.mode) of the layer targeted for a failed fragment update. A fragment update is considered *failed* if the server responds with a status code other than 2xx, but still renders HTML. Server-side code is free to render different HTML for different modes. For example, you might prefer to not render a site navigation for overlays. The user may choose to not send this header by configuring `up.network.config.requestMetaKeys`. \#\#\# Example ```http X-Up-Mode: drawer X-Up-Fail-Mode: root ``` @header X-Up-Fail-Mode @stable */ clearCacheFromXHR = function(xhr) { var parseValue; parseValue = function(value) { switch (value) { case 'true': return true; case 'false': return false; default: return value; } }; return extractHeader(xhr, 'clearCache', parseValue); }; /*** The server may send this optional response header with the value `clear` to [clear the cache](/up.cache.clear). \#\#\# Example ```http X-Up-Cache: clear ``` @header X-Up-Cache @param value The string `"clear"`. */ /*** This request header contains a timestamp of an existing fragment that is being [reloaded](/up.reload). The timestamp must be explicitely set by the user as an `[up-time]` attribute on the fragment. It should indicate the time when the fragment's underlying data was last changed. See `[up-time]` for a detailed example. \#\#\# Format The time is encoded is the number of seconds elapsed since the [Unix epoch](https://en.wikipedia.org/wiki/Unix_time). For instance, a modification date of December 23th, 1:40:18 PM UTC would produce the following header: ```http X-Up-Target: .unread-count X-Up-Reload-From-Time: 1608730818 ``` If no timestamp is known, Unpoly will send a value of zero (`X-Up-Reload-From-Time: 0`). @header X-Up-Reload-From-Time @stable */ contextFromXHR = function(xhr) { return extractHeader(xhr, 'context', JSON.parse); }; /*** This request header contains the targeted layer's [context](/context), serialized as JSON. The user may choose to not send this header by configuring `up.network.config.requestMetaKeys`. \#\#\# Example ```http X-Up-Context: { "lives": 3 } ``` \#\#\# Updating context from the server The server may update the layer context by sending a `X-Up-Context` response header with changed key/value pairs: ```http Content-Type: text/html X-Up-Context: { "lives": 2 } ... ``` Upon seeing the response header, Unpoly will assign the server-provided context object to the layer's context object, adding or replacing keys as needed. Client-side context keys not mentioned in the response will remain unchanged. There is no explicit protocol to *remove* keys from the context, but the server may send a key with a `null` value to effectively remove a key. The frontend will use the server-provided context upates for both successful (HTTP status `200 OK`) and failed (status `4xx` or `5xx`) responses. If no `X-Up-Context` response header is set, the updating layer's context will not be changed. It is recommended that the server only places changed key/value pairs into the `X-Up-Context` response header, and not echo the entire context object. Otherwise any client-side changes made while the request was in flight will get overridden by the server-provided context. @header X-Up-Context @experimental */ /*** This request header contains the [context](/context) of the layer targeted for a failed fragment update, serialized as JSON. A fragment update is considered *failed* if the server responds with a status code other than 2xx, but still renders HTML. Server-side code is free to render different HTML for different contexts. For example, you might prefer to not render a site navigation for overlays. The user may choose to not send this header by configuring `up.network.config.requestMetaKeys`. \#\#\# Example ```http X-Up-Fail-Context: { "context": "Choose a company contact" } ``` @header X-Up-Fail-Context @experimental */ /*** @function up.protocol.methodFromXHR @internal */ methodFromXHR = function(xhr) { return extractHeader(xhr, 'method', u.normalizeMethod); }; /*** The server may set this optional response header to change the browser location after a fragment update. Without this header Unpoly will set the browser location to the response URL, which is usually sufficient. When setting `X-Up-Location` it is recommended to also set `X-Up-Method`. If no `X-Up-Method` header is given and the response's URL changed from the request's URL, Unpoly will assume a redirect and set the method to `GET`. \#\#\# Internet Explorer 11 There is an edge case on Internet Explorer 11, where Unpoly cannot detect the final URL after a redirect. You can fix this edge case by delivering `X-Up-Location` and `X-Up-Method` headers with the *last* response in a series of redirects. The **simplest implementation** is to set these headers for every request. \#\#\# Example ```http X-Up-Location: /current-url X-Up-Method: GET ``` @header X-Up-Location @stable */ /*** The server may set this optional response header to change the HTTP method after a fragment update. Without this header Unpoly will assume a `GET` method if the response's URL changed from the request's URL, \#\#\# Example ```http X-Up-Location: /current-url X-Up-Method: GET ``` @header X-Up-Method @stable */ /*** The server may set this optional response header to change the document title after a fragment update. Without this header Unpoly will extract the `` from the server response. This header is useful when you [optimize your response](X-Up-Target) to not render the application layout unless targeted. Since your optimized response no longer includes a `<title>`, you can instead use this HTTP header to pass the document title. \#\#\# Example ```http X-Up-Title: Playlist browser ``` @header X-Up-Title @stable */ /*** This request header contains the `[name]` of a [form field being validated](/input-up-validate). When seeing this header, the server is expected to validate (but not save) the form submission and render a new copy of the form with validation errors. See the documentation for [`input[up-validate]`](/input-up-validate) for more information on how server-side validation works in Unpoly. \#\#\# Example Assume we have an auto-validating form field: ```html <fieldset> <input name="email" up-validate> </fieldset> ``` When the input is changed, Unpoly will submit the form with an additional header: ```html X-Up-Validate: email ``` @header X-Up-Validate @stable */ eventPlansFromXHR = function(xhr) { return extractHeader(xhr, 'events', JSON.parse); }; /*** The server may set this response header to [emit events](/up.emit) with the requested [fragment update](a-up-target). The header value is a [JSON](https://en.wikipedia.org/wiki/JSON) array. Each element in the array is a JSON object representing an event to be emitted on the `document`. The object property `{ "type" }` defines the event's [type](https://developer.mozilla.org/en-US/docs/Web/API/Event/type). Other properties become properties of the emitted event object. \#\#\# Example ```http Content-Type: text/html X-Up-Events: [{ "type": "user:created", "id": 5012 }, { "type": "signup:completed" }] ... <html> ... </html> ``` \#\#\# Emitting an event on a layer Instead of emitting an event on the `document`, the server may also choose to [emit the event on the layer being updated](/up.layer.emit). To do so, add a property `{ "layer": "current" }` to the JSON object of an event: ```http Content-Type: text/html X-Up-Events: [{ "type": "user:created", "name:" "foobar", "layer": "current" }] ... <html> ... </html> ``` @header X-Up-Events @stable */ acceptLayerFromXHR = function(xhr) { return extractHeader(xhr, 'acceptLayer', JSON.parse); }; /*** The server may set this response header to [accept](/up.layer.accept) the targeted overlay in response to a fragment update. Upon seeing the header, Unpoly will cancel the fragment update and accept the layer instead. If the root layer is targeted, the header is ignored and the fragment is updated with the response's HTML content. The header value is the acceptance value serialized as a JSON object. To accept an overlay without value, set the header value to the string `null`. \#\#\# Example The response below will accept the targeted overlay with the value `{user_id: 1012 }`: ```http Content-Type: text/html X-Up-Accept-Layer: {"user_id": 1012} <html> ... </html> ``` \#\#\# Rendering content The response may contain `text/html` content. If the root layer is targeted, the `X-Up-Accept-Layer` header is ignored and the fragment is updated with the response's HTML content. If you know that an overlay will be closed don't want to render HTML, have the server change the render target to `:none`: ```http Content-Type: text/html X-Up-Accept-Layer: {"user_id": 1012} X-Up-Target: :none ``` @header X-Up-Accept-Layer @stable */ dismissLayerFromXHR = function(xhr) { return extractHeader(xhr, 'dismissLayer', JSON.parse); }; /*** The server may set this response header to [dismiss](/up.layer.dismiss) the targeted overlay in response to a fragment update. Upon seeing the header, Unpoly will cancel the fragment update and dismiss the layer instead. If the root layer is targeted, the header is ignored and the fragment is updated with the response's HTML content. The header value is the dismissal value serialized as a JSON object. To accept an overlay without value, set the header value to the string `null`. \#\#\# Example The response below will dismiss the targeted overlay without a dismissal value: ```http HTTP/1.1 200 OK Content-Type: text/html X-Up-Dismiss-Layer: null <html> ... </html> ``` \#\#\# Rendering content The response may contain `text/html` content. If the root layer is targeted, the `X-Up-Accept-Layer` header is ignored and the fragment is updated with the response's HTML content. If you know that an overlay will be closed don't want to render HTML, have the server change the render target to `:none`: ```http HTTP/1.1 200 OK Content-Type: text/html X-Up-Accept-Layer: {"user_id": 1012} X-Up-Target: :none ``` @header X-Up-Dismiss-Layer @stable */ /*** Server-side companion libraries like unpoly-rails set this cookie so we have a way to detect the request method of the initial page load. There is no JavaScript API for this. @function up.protocol.initialRequestMethod @internal */ initialRequestMethod = u.memoize(function() { return u.normalizeMethod(up.browser.popCookie('_up_method')); }); /*** The server may set this optional cookie to echo the HTTP method of the initial request. If the initial page was loaded with a non-`GET` HTTP method, Unpoly prefers to make a full page load when you try to update a fragment. Once the next page was loaded with a `GET` method, Unpoly will again update fragments. This fixes two edge cases you might or might not care about: 1. Unpoly replaces the initial page state so it can later restore it when the user goes back to that initial URL. However, if the initial request was a POST, Unpoly will wrongly assume that it can restore the state by reloading with GET. 2. Some browsers have a bug where the initial request method is used for all subsequently pushed states. That means if the user reloads the page on a later GET state, the browser will wrongly attempt a POST request. This issue affects Safari 9-12 (last tested in 2019-03). Modern Firefoxes, Chromes and IE10+ don't have this behavior. In order to allow Unpoly to detect the HTTP method of the initial page load, the server must set a cookie: ```http Set-Cookie: _up_method=POST ``` When Unpoly boots it will look for this cookie and configure itself accordingly. The cookie is then deleted in order to not affect following requests. The **simplest implementation** is to set this cookie for every request that is neither `GET` nor an [Unpoly request](/X-Up-Version). For all other requests an existing `_up_method` cookie should be deleted. @cookie _up_method @stable */ /*** @function up.protocol.locationFromXHR @internal */ locationFromXHR = function(xhr) { return extractHeader(xhr, 'location') || xhr.responseURL; }; /*** @function up.protocol.titleFromXHR @internal */ titleFromXHR = function(xhr) { return extractHeader(xhr, 'title'); }; /*** @function up.protocol.targetFromXHR @internal */ targetFromXHR = function(xhr) { return extractHeader(xhr, 'target'); }; /*** Configures strings used in the optional [server protocol](/up.protocol). @property up.protocol.config @param {string} [config.csrfHeader='X-CSRF-Token'] The name of the HTTP header that will include the [CSRF token](https://en.wikipedia.org/wiki/Cross-site_request_forgery#Synchronizer_token_pattern) for AJAX requests. @param {string|Function(): string} [config.csrfParam] The `name` of the hidden `<input>` used for sending a [CSRF token](https://en.wikipedia.org/wiki/Cross-site_request_forgery#Synchronizer_token_pattern) when submitting a default, non-AJAX form. For AJAX request the token is sent as an [HTTP header](/up.protocol.config#config.csrfHeader instead. The parameter name can be configured as a string or as function that returns the parameter name. If no name is set, no token will be sent. Defaults to the `content` attribute of a `<meta>` tag named `csrf-param`: ```html <meta name="csrf-param" content="authenticity_token" /> ``` @param {string|Function(): string} [config.csrfToken] The [CSRF token](https://en.wikipedia.org/wiki/Cross-site_request_forgery#Synchronizer_token_pattern) to send for unsafe requests. The token will be sent as either a HTTP header (for AJAX requests) or hidden form `<input>` (for default, non-AJAX form submissions). The token can either be configured as a string or as function that returns the token. If no token is set, no token will be sent. Defaults to the `content` attribute of a `<meta>` tag named `csrf-token`: ``` <meta name='csrf-token' content='secret12345'> ``` @param {string} [config.methodParam='_method'] The name of request parameter containing the original request method when Unpoly needs to wrap the method. Methods must be wrapped when making a [full page request](/up.browser.loadPage) with a methods other than GET or POST. In this case Unpoly will make a POST request with the original request method in a form parameter named `_method`: ```http POST /test HTTP/1.1 Host: example.com Content-Type: application/x-www-form-urlencoded Content-Length: 11 _method=PUT ``` @stable */ config = new up.Config(function() { return { methodParam: '_method', csrfParam: function() { return e.metaContent('csrf-param'); }, csrfToken: function() { return e.metaContent('csrf-token'); }, csrfHeader: 'X-CSRF-Token' }; }); csrfHeader = function() { return u.evalOption(config.csrfHeader); }; csrfParam = function() { return u.evalOption(config.csrfParam); }; csrfToken = function() { return u.evalOption(config.csrfToken); }; /*** @internal */ wrapMethod = function(method, params) { params.add(config.methodParam, method); return 'POST'; }; reset = function() { return config.reset(); }; up.on('up:framework:reset', reset); return { config: config, reset: reset, locationFromXHR: locationFromXHR, titleFromXHR: titleFromXHR, targetFromXHR: targetFromXHR, methodFromXHR: methodFromXHR, acceptLayerFromXHR: acceptLayerFromXHR, contextFromXHR: contextFromXHR, dismissLayerFromXHR: dismissLayerFromXHR, eventPlansFromXHR: eventPlansFromXHR, clearCacheFromXHR: clearCacheFromXHR, csrfHeader: csrfHeader, csrfParam: csrfParam, csrfToken: csrfToken, initialRequestMethod: initialRequestMethod, headerize: headerize, wrapMethod: wrapMethod }; })(); }).call(this); /*** Logging ======= Unpoly can print debugging information to the [browser console](https://developer.chrome.com/docs/devtools/console/), e.g.: - Which [events](/up.event) are called - When we're [making requests to the network](/up.request) - Which [compilers](/up.syntax) are applied to which elements @see up.log.enable @see up.log.disable @module up.log */ (function() { var slice = [].slice; up.log = (function() { var config, disable, enable, fail, muteUncriticalRejection, printBanner, printToError, printToStandard, printToStream, printToWarn, reset, sessionStore, setEnabled, u; u = up.util; sessionStore = new up.store.Session('up.log'); /*** Configures the logging output on the developer console. @property up.log.config @param {boolean} [options.enabled=false] Whether Unpoly will print debugging information to the developer console. Debugging information includes which elements are being [compiled](/up.syntax) and which [events](/up.event) are being emitted. Note that errors will always be printed, regardless of this setting. @param {boolean} [options.banner=true] Print the Unpoly banner to the developer console. @stable */ config = new up.Config(function() { return { enabled: sessionStore.get('enabled'), banner: true }; }); reset = function() { return config.reset(); }; /*** Prints a logging message to the browser console. @function up.puts @param {string} message @param {Array} ...args @internal */ printToStandard = function() { var args; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; if (config.enabled) { return printToStream.apply(null, ['log'].concat(slice.call(args))); } }; /*** @function up.warn @internal */ printToWarn = function() { var args; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; return printToStream.apply(null, ['warn'].concat(slice.call(args))); }; /*** @function up.log.error @internal */ printToError = function() { var args; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; return printToStream.apply(null, ['error'].concat(slice.call(args))); }; printToStream = function() { var args, message, stream, trace; stream = arguments[0], trace = arguments[1], message = arguments[2], args = 4 <= arguments.length ? slice.call(arguments, 3) : []; if (message) { if (up.browser.canFormatLog()) { args.unshift(''); args.unshift('color: #666666; padding: 1px 3px; border: 1px solid #bbbbbb; border-radius: 2px; font-size: 90%; display: inline-block'); message = "%c" + trace + "%c " + message; } else { message = "[" + trace + "] " + message; } return console[stream].apply(console, [message].concat(slice.call(args))); } }; printBanner = function() { var color, logo, text; if (!config.banner) { return; } logo = " __ _____ ___ ___ / /_ __\n" + ("/ // / _ \\/ _ \\/ _ \\/ / // / " + up.version + "\n") + "\\___/_//_/ .__/\\___/_/\\_. / \n" + " / / / /\n\n"; text = ""; if (!up.migrate.loaded) { text += "Load unpoly-migrate.js to enable deprecated APIs.\n\n"; } if (config.enabled) { text += "Call `up.log.disable()` to disable logging for this session."; } else { text += "Call `up.log.enable()` to enable logging for this session."; } color = 'color: #777777'; if (up.browser.canFormatLog()) { return console.log('%c' + logo + '%c' + text, 'font-family: monospace;' + color, color); } else { return console.log(logo + text); } }; up.on('up:app:boot', printBanner); up.on('up:framework:reset', reset); setEnabled = function(value) { sessionStore.set('enabled', value); return config.enabled = value; }; /*** Starts printing debugging information to the developer console. Debugging information includes which elements are being [compiled](/up.syntax) and which [events](/up.event) are being emitted. Errors will always be printed, regardless of this setting. @function up.log.enable @stable */ enable = function() { return setEnabled(true); }; /*** Stops printing debugging information to the developer console. Errors will still be printed, even with logging disabled. @function up.log.disable @stable */ disable = function() { return setEnabled(false); }; /*** 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<string>} vars... A list of variables to replace any substitution marks in the error message. @experimental */ fail = function() { var args; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; printToError.apply(null, ['error'].concat(slice.call(args))); throw up.error.failed(args); }; /*** Registers an empty rejection handler in case the given promise rejects with an AbortError or a failed up.Response. This prevents browsers from printing "Uncaught (in promise)" to the error console when the promise is rejected. This is helpful for event handlers where it is clear that no rejection handler will be registered: ```js up.on('submit', 'form[up-target]', (event, form) => { promise = up.submit(form) up.util.muteRejection(promise) }) ``` @function up.log.muteUncriticalRejection @param {Promise} promise @return {Promise} @internal */ muteUncriticalRejection = function(promise) { return promise["catch"](function(error) { if (!((typeof error === 'object') && (error.name === 'AbortError' || error instanceof up.RenderResult || error instanceof up.Response))) { throw error; } }); }; return { puts: printToStandard, error: printToError, warn: printToWarn, config: config, enable: enable, disable: disable, fail: fail, muteUncriticalRejection: muteUncriticalRejection, isEnabled: function() { return config.enabled; } }; })(); up.puts = up.log.puts; up.warn = up.log.warn; up.fail = up.log.fail; }).call(this); /*** Custom JavaScript ================= The `up.syntax` package lets you pair HTML elements with JavaScript behavior. @see up.compiler @see [up-data] @see up.macro @module up.syntax */ (function() { var slice = [].slice; up.syntax = (function() { var SYSTEM_MACRO_PRIORITIES, buildCompiler, clean, compile, compilers, detectSystemMacroPriority, e, insertCompiler, macros, parseCompilerArgs, readData, registerCompiler, registerDestructor, registerJQueryCompiler, registerJQueryMacro, registerMacro, reset, u; u = up.util; e = up.element; SYSTEM_MACRO_PRIORITIES = { '[up-back]': -100, '[up-content]': -200, '[up-drawer]': -200, '[up-modal]': -200, '[up-cover]': -200, '[up-popup]': -200, '[up-tooltip]': -200, '[up-dash]': -200, '[up-expand]': -300, '[data-method]': -400, '[data-confirm]': -400 }; compilers = []; macros = []; /*** Registers a function to be called when an element with the given selector is inserted into the DOM. Use compilers to activate your custom Javascript behavior on matching elements. You should migrate your [`DOMContentLoaded`](https://developer.mozilla.org/en-US/docs/Web/API/Window/DOMContentLoaded_event) callbacks to compilers. This will make sure they run both at page load and when a new fragment is inserted later. See [Making JavaScripts work with fragment updates](/legacy-scripts) for advice on migrating legacy scripts. It will also organize your JavaScript snippets by selector. \#\#\# Example This compiler will insert the current time into a `<div class='current-time'></div>`: ```js up.compiler('.current-time', function(element) { var now = new Date() element.textContent = now.toString() }) ``` The compiler function will be called once for each matching element when the page loads, or when a matching fragment is [inserted](/up.replace) later. \#\#\# Integrating JavaScript libraries `up.compiler()` is a great way to integrate JavaScript libraries. 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 `lightbox` class: ```html <a href="river.png" class="lightbox">River</a> <a href="ocean.png" class="lightbox">Ocean</a> ``` This JavaScript will do exactly that: ```js up.compiler('a.lightbox', function(element) { lightboxify(element) }) ``` \#\#\# Cleaning up after yourself If your compiler returns a function, Unpoly will use this as a *destructor* to clean up if the element leaves the DOM. Note that in Unpoly the same DOM and 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](/up.on). Here is a version of `.current-time` that updates the time every second, and cleans up once it's done. Note how it returns a function that calls `clearInterval`: ```js up.compiler('.current-time', function(element) { let update = () => element.textContent = new Date().toString() setInterval(update, 1000) return () => 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. An alternative way to register a destructor function is `up.destructor()`. \#\#\# Passing parameters to a compiler Use the `[up-data]` attribute to attach structured data to a DOM element. The data will be parsed and passed to your compiler function. Alternatively your compiler may access attributes for the compiled element via the standard [`Element#getAttribute()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute) method. Unpoly also provides utility functions to read an element attribute and cast it to a given type: - `up.element.booleanAttr(element, attr)` - `up.element.numberAttr(element, attr)` - `up.element.jsonAttr(element, attr)` @function up.compiler @param {string} selector The selector to match. @param {number} [options.priority=0] The priority of this compiler. Compilers with a higher priority are run first. Two compilers with the same priority are run in the order they were registered. @param {boolean} [options.batch=false] If set to `true` and a fragment insertion contains multiple elements matching `selector`, the `compiler` function is only called once with all these 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. If the element has an [`up-data`](/up-data) attribute, its value is parsed as JSON and passed as a second argument. The function may return a destructor function that cleans the compiled object before it is removed from the DOM. The destructor is supposed to [clear global state](/up.compiler#cleaning-up-after-yourself) such as timeouts 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.destroy). @stable */ registerCompiler = function() { var args, compiler; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; compiler = buildCompiler(args); return insertCompiler(compilers, compiler); }; /*** Registers a function to be called when an element with the given selector is inserted into the DOM. The function is called with each matching element as a [jQuery object](https://learn.jquery.com/using-jquery-core/jquery-object/). If you're not using jQuery, use `up.compiler()` instead, which calls the compiler function with a native element. \#\#\# Example This jQuery compiler will insert the current time into a `<div class='current-time'></div>`: ```js up.$compiler('.current-time', function($element) { var now = new Date() $element.text(now.toString()) }) ``` @function up.$compiler @param {string} selector The selector to match. @param {Object} [options] See [`options` argument for `up.compiler()`](/up.compiler#parameters). @param {Function($element, data)} compiler The function to call when a matching element is inserted. See [`compiler` argument for `up.compiler()`](/up.compiler#parameters). @stable */ registerJQueryCompiler = function() { var args, compiler; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; compiler = registerCompiler.apply(null, args); compiler.jQuery = true; return compiler; }; /*** Registers a [compiler](/up.compiler) that is run before all other compilers. A macro lets you set UJS attributes that will be compiled afterwards. If you want default attributes for *every* link and form, consider customizing your [navigation options](/navigation). \#\#\# Example You will sometimes find yourself setting the same combination of UJS attributes again and again: ```html <a href="/page1" up-layer="new modal" up-class="warning" up-animation="shake">Page 1</a> <a href="/page1" up-layer="new modal" up-class="warning" up-animation="shake">Page 1</a> <a href="/page1" up-layer="new modal" up-class="warning" up-animation="shake">Page 1</a> ``` We would much rather define a new `[smooth-link]` attribute that let's us write the same links like this: ```html <a href="/page1" smooth-link>Page 1</a> <a href="/page2" smooth-link>Page 2</a> <a href="/page3" smooth-link>Page 3</a> ``` We can define the `[content-link]` attribute by registering a macro that sets the `[up-target]`, `[up-transition]` and `[up-duration]` attributes for us: ``` up.macro('[smooth-link]', function(link) { link.setAttribute('up-target', '.content') link.setAttribute('up-transition', 'cross-fade') link.setAttribute('up-duration', '300') }) ``` @function up.macro @param {string} selector The selector to match. @param {Object} options See options for [`up.compiler()`](/up.compiler). @param {Function(element, data)} macro The function to call when a matching element is inserted. See [`up.compiler()`](/up.compiler#parameters) for details. @stable */ registerMacro = function() { var args, macro; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; macro = buildCompiler(args); if (up.framework.booting) { macro.priority = detectSystemMacroPriority(macro.selector) || up.fail('Unregistered priority for system macro %o', macro.selector); } return insertCompiler(macros, macro); }; /*** Registers a [compiler](/up.compiler) that is run before all other compilers. The compiler function is called with each matching element as a [jQuery object](https://learn.jquery.com/using-jquery-core/jquery-object/). If you're not using jQuery, use `up.macro()` instead, which calls the macro function with a native element. \#\#\# Example ```js up.$macro('[content-link]', function($link) { $link.attr( 'up-target': '.content', 'up-transition': 'cross-fade', 'up-duration':'300' ) }) ``` @function up.$macro @param {string} selector The selector to match. @param {Object} options See [`options` argument for `up.compiler()`](/up.compiler#parameters). @param {Function(element, data)} macro The function to call when a matching element is inserted. See [`compiler` argument for `up.compiler()`](/up.compiler#parameters). @stable */ registerJQueryMacro = function() { var args, macro; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; macro = registerMacro.apply(null, args); macro.jQuery = true; return macro; }; detectSystemMacroPriority = function(macroSelector) { var priority, substr; macroSelector = u.evalOption(macroSelector); for (substr in SYSTEM_MACRO_PRIORITIES) { priority = SYSTEM_MACRO_PRIORITIES[substr]; if (macroSelector.indexOf(substr) >= 0) { return priority; } } }; parseCompilerArgs = function(args) { var callback, options, selector; selector = args.shift(); callback = args.pop(); options = u.extractOptions(args); return [selector, options, callback]; }; buildCompiler = function(args) { var callback, options, ref, selector; ref = parseCompilerArgs(args), selector = ref[0], options = ref[1], callback = ref[2]; options = u.options(options, { selector: selector, isDefault: up.framework.booting, priority: 0, batch: false, keep: false, jQuery: false }); return u.assign(callback, options); }; insertCompiler = function(queue, newCompiler) { var existingCompiler, index; index = 0; while ((existingCompiler = queue[index]) && (existingCompiler.priority >= newCompiler.priority)) { index += 1; } queue.splice(index, 0, newCompiler); return newCompiler; }; /*** Applies all compilers on the given element and its descendants. Unlike [`up.hello()`](/up.hello), this doesn't emit any events. @function up.syntax.compile @param {Array<Element>} [options.skip] A list of elements whose subtrees should not be compiled. @internal */ compile = function(fragment, options) { var orderedCompilers, pass; orderedCompilers = macros.concat(compilers); pass = new up.CompilerPass(fragment, orderedCompilers, options); return pass.run(); }; /*** Registers a function to be called when the given element is [destroyed](/up.destroy). \#\#\# Example ```js up.compiler('.current-time', function(element) { let update = () => element.textContent = new Date().toString() setInterval(update, 1000) up.destructor(element, () => clearInterval(update)) }) ``` An alternative way to register a destructor function is to [`return` it from your compiler function](/up.compiler#cleaning-up-after-yourself). @function up.destructor @param {Element} element @param {Function|Array<Function>} destructor One or more destructor functions. @stable */ registerDestructor = function(element, destructor) { var destructors; if (!(destructors = element.upDestructors)) { destructors = []; element.upDestructors = destructors; element.classList.add('up-can-clean'); } if (u.isArray(destructor)) { return destructors.push.apply(destructors, destructor); } else { return destructors.push(destructor); } }; /*** Runs any destructor on the given fragment and its descendants in the same layer. Unlike [`up.destroy()`](/up.destroy), this does not emit any events and does not remove the element from the DOM. @function up.syntax.clean @param {Element} fragment @param {up.Layer} options.layer @internal */ clean = function(fragment, options) { var pass; if (options == null) { options = {}; } pass = new up.DestructorPass(fragment, options); return pass.run(); }; /*** Returns the given element's `[up-data]`, parsed as a JavaScript object. Returns `undefined` if the element has no `[up-data]` attribute. \#\#\# Example You have an element with JSON data serialized into an `up-data` attribute: ```html <span class='person' up-data='{ "age": 18, "name": "Bob" }'>Bob</span> ``` Calling `up.syntax.data()` will deserialize the JSON string into a JavaScript object: ```js up.syntax.data('.person') // returns { age: 18, name: 'Bob' } ``` @function up.data @param {string|Element|jQuery} element The element for which to return data. @return The JSON-decoded value of the `up-data` attribute. Returns `undefined` if the element has no (or an empty) `up-data` attribute. @stable */ /*** Attaches structured data to an element, to be consumed by a compiler. If an element with an `[up-data]` attribute enters the DOM, Unpoly will parse the JSON and pass the resulting object to any matching [`up.compiler()`](/up.compiler) functions. \#\#\# Example 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: ```html <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 be parsed and handed to your compiler as a second argument: ```js 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 }) }) }) ``` 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.on) handlers. ```js up.on('click', '.google-map', function(event, element, pins) { console.log("There are %d pins on the clicked map", pins.length) }) ``` @selector [up-data] @param up-data A serialized JSON string @stable */ readData = function(element) { element = up.fragment.get(element); return e.jsonAttr(element, 'up-data') || {}; }; /*** Resets the list of registered compiler directives to the moment when the framework was booted. @internal */ reset = function() { compilers = u.filter(compilers, 'isDefault'); return macros = u.filter(macros, 'isDefault'); }; up.on('up:framework:reset', reset); return { compiler: registerCompiler, macro: registerMacro, $compiler: registerJQueryCompiler, $macro: registerJQueryMacro, destructor: registerDestructor, compile: compile, clean: clean, data: readData }; })(); up.compiler = up.syntax.compiler; up.$compiler = up.syntax.$compiler; up.destructor = up.syntax.destructor; up.macro = up.syntax.macro; up.$macro = up.syntax.$macro; up.data = up.syntax.data; }).call(this); /*** History ======== The `up.history` module helps you work with the browser history. @see up.history.location @see up:location:changed @module up.history */ (function() { var slice = [].slice; up.history = (function() { var buildState, config, currentLocation, e, emit, isCurrentLocation, manipulate, nextPreviousLocation, normalizeURL, pop, previousLocation, push, replace, reset, restoreStateOnPop, trackCurrentLocation, u; u = up.util; e = up.element; /*** Configures behavior when the user goes back or forward in browser history. @property up.history.config @param {Array} [config.restoreTargets=[]] A list of possible CSS selectors to [replace](/up.render) when the user goes back in history. By default the [root layer's main target](/up.fragment.config.mainTargets). @param {boolean} [config.enabled=true] Defines whether [fragment updates](/up.render) will update the browser's current URL. If set to `false` Unpoly will never change the browser URL. @param {boolean} [config.enabled=true] Whether to restore the known scroll positions when the user goes back or forward in history. @stable */ config = new up.Config(function() { return { enabled: true, restoreTargets: [':main'] }; }); /*** Returns a normalized URL for the previous history entry. Only history entries pushed by Unpoly will be considered. @property up.history.previousLocation @param {string} previousLocation @experimental */ previousLocation = void 0; nextPreviousLocation = void 0; reset = function() { config.reset(); previousLocation = void 0; nextPreviousLocation = void 0; return trackCurrentLocation(); }; normalizeURL = function(url, normalizeOptions) { if (normalizeOptions == null) { normalizeOptions = {}; } normalizeOptions.hash = true; return u.normalizeURL(url, normalizeOptions); }; /*** Returns a normalized URL for the current browser location. Note that if the current [layer](/up.layer) does not have [visible history](/up.Layer.prototype.historyVisible), the browser's address bar will show the location of an ancestor layer. To get the location of the current layer, use `up.layer.location`. @property up.history.location @param {string} location @experimental */ currentLocation = function(normalizeOptions) { return normalizeURL(location.href, normalizeOptions); }; /*** Remembers the current URL so we can use previousURL on pop. @function observeNewURL @internal */ trackCurrentLocation = function() { var url; url = currentLocation(); if (nextPreviousLocation !== url) { previousLocation = nextPreviousLocation; return nextPreviousLocation = url; } }; trackCurrentLocation(); isCurrentLocation = function(url) { var normalizeOptions; normalizeOptions = { stripTrailingSlash: true }; return normalizeURL(url, normalizeOptions) === currentLocation(normalizeOptions); }; /*** Replaces the current history entry and updates the browser's location bar with the given URL. When the user navigates to the replaced history entry at a later time, Unpoly will [`replace`](/up.replace) the document body with the body from that URL. Note that functions like [`up.replace()`](/up.replace) or [`up.submit()`](/up.submit) will automatically update the browser's location bar for you. @function up.history.replace @param {string} url @internal */ replace = function(url, options) { if (options == null) { options = {}; } if (manipulate('replaceState', url) && options.event !== false) { return emit('up:location:changed', { url: url, reason: 'replace', log: "Replaced state for " + (u.urlWithoutHost(url)) }); } }; /*** Adds a new history entry and updates the browser's address bar with the given URL. When the user restores the new history entry later, Unpoly will replace a selector from `up.history.config.restoreTargets` with the body from that URL. Note that [fragment navigation](/navigation) will automatically update the browser's location bar for you. Emits event `up:location:changed`. @function up.history.push @param {string} url The URL for the history entry to be added. @experimental */ push = function(url) { url = normalizeURL(url); if (!isCurrentLocation(url) && manipulate('pushState', url)) { return up.emit('up:location:changed', { url: url, reason: 'push', log: "Advanced to location " + (u.urlWithoutHost(url)) }); } }; /*** This event is [emitted](/up.emit) after the browser's address bar was updated with a new URL. There may be several reasons why the browser location was changed: - A fragment update changes history through [navigation](/navigation) or rendering with `{ history: true }`. - The user uses the back or forward buttons in their browser UI. - Programmatic calls to `up.history.push()`. When a [layer](/up.layer) has no [visible history](/up.Layer.prototype.historyVisible), following a link will not cause the browser's address bar to be updated. In this case no `up:location:changed` event will be emitted. However, a `up:layer:location:changed` will be emitted even if the address bar did not change. @event up:location:changed @param {string} event.url The URL for the history entry after the change. @param {string} event.reason The action that caused this change in [history state](https://developer.mozilla.org/en-US/docs/Web/API/History/state). The value of this property is either `'push'`, `'pop'` or `'replace'`. @stable */ manipulate = function(method, url) { var state; if (config.enabled) { state = buildState(); window.history[method](state, '', url); trackCurrentLocation(); return state; } }; buildState = function() { return { up: {} }; }; restoreStateOnPop = function(state) { var replaced, url; if (state != null ? state.up : void 0) { url = currentLocation(); replaced = up.render({ url: url, history: true, location: url, peel: true, layer: 'root', target: config.restoreTargets, cache: true, keep: false, scroll: 'restore', saveScroll: false }); return replaced.then(function() { url = currentLocation(); return emit('up:location:changed', { url: url, reason: 'pop', log: "Restored location " + url }); }); } else { return up.puts('pop', 'Ignoring a state not pushed by Unpoly (%o)', state); } }; pop = function(event) { var state; trackCurrentLocation(); up.viewport.saveScroll({ location: previousLocation }); state = event.state; return restoreStateOnPop(state); }; emit = function() { var args, historyLayer; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; historyLayer = u.find(up.layer.stack.reversed(), 'historyVisible'); return historyLayer.emit.apply(historyLayer, args); }; up.on('up:app:boot', function() { var register; register = function() { window.history.scrollRestoration = 'manual'; window.addEventListener('popstate', pop); if (up.protocol.initialRequestMethod() === 'GET') { return replace(currentLocation(), { event: false }); } }; if (typeof jasmine !== "undefined" && jasmine !== null) { return register(); } else { return 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. If no previous URL is known, the link will not be changed. \#\#\# Example This link ... ```html <a href="/default" up-back> Go back </a> ``` ... will be transformed to: ```html <a href="/default" up-href="/previous-page" up-scroll="restore" up-follow> Go back </a> ``` @selector a[up-back] @stable */ up.macro('a[up-back], [up-href][up-back]', function(link) { if (previousLocation) { e.setMissingAttrs(link, { 'up-href': previousLocation, 'up-scroll': 'restore' }); link.removeAttribute('up-back'); return up.link.makeFollowable(link); } }); up.on('up:framework:reset', reset); return u.literal({ config: config, push: push, replace: replace, get_location: currentLocation, get_previousLocation: function() { return previousLocation; }, isLocation: isCurrentLocation, normalizeURL: normalizeURL }); })(); }).call(this); (function() { var e, u, slice = [].slice; u = up.util; e = up.element; /*** Fragment API =========== The `up.fragment` module offers a high-level JavaScript API to work with DOM elements. A fragment is an element with some additional properties that are useful in the context of a server-rendered web application: - Fragments are [identified by a CSS selector](/up.fragment.toTarget), like a `.class` or `#id`. - Fragments are usually updated by a [link](/a-up-follow) for [form](/form-up-submits) that targets their selector. When the server renders HTML with a matching element, the fragment is swapped with a new version. - As fragments enter the page they are automatically [compiled](/up.compiler) to activate JavaScript behavior. - Fragment changes may be [animated](/up.motion). - Fragments are placed on a [layer](/up.layer) that is isolated from other layers. Unpoly features will only see or change fragments from the [current layer](/up.layer.current) unless you [explicitly target another layer](/layer-option). - Fragments [know the URL from where they were loaded](/up.source). They can be [reloaded](/up.reload) or [polled periodically](/up-polled). For low-level DOM utilities that complement the browser's native API, see `up.element`. @see up.render @see up.navigate @see up.destroy @see up.reload @see up.fragment.get @see up.hello @module up.fragment */ up.fragment = (function() { /*** Configures defaults for fragment updates. @property up.fragment.config @param {Array<string>} [config.mainTargets=['[up-main]', 'main', ':layer']] An array of CSS selectors matching default render targets. When no other render target is given, Unpoly will try to find and replace a main target. When [navigating](/navigation) to a main target, Unpoly will automatically [reset scroll positions](/scroll-option) and [update the browser history](/up.render#options.history). This property is aliased as [`up.layer.config.any.mainTargets`](up.layer.config#config.any.mainTargets). @param {Array<string|RegExp>} [config.badTargetClasses] An array of class names that should be ignored when [deriving a target selector from a fragment](/up.fragment.toTarget). The class names may also be passed as a regular expression. @param {Object} [config.navigateOptions] An object of default options to apply when [navigating](/navigation). @param {boolean} [config.matchAroundOrigin] Whether to match an existing fragment around the triggered link. If set to `false` Unpoly will replace the first fragment matching the given target selector in the link's [layer](/up.layer). @param {Array<string>} [config.autoHistoryTargets] When an updated fragments contain an element matching one of the given CSS selectors, history will be updated with `{ history: 'auto' }`. By default Unpoly will auto-update history when updating a [main target](#config.mainTargets). @param {boolean|string|Function(Element)} [config.autoScroll] How to scroll after updating a fragment with `{ scroll: 'auto' }`. See [scroll option](/scroll-option) for a list of allowed values. The default configuration tries, in this order: - If the URL has a `#hash`, scroll to the hash. - If updating a [main target](/up-main), reset scroll positions. @param {boolean|string|Function(Element)} [config.autoFocus] How to focus when updating a fragment with `{ focus: 'auto' }`. See [focus option](/focus-option) for a list of allowed values. The default configuration tries, in this order: - Focus a `#hash` in the URL. - Focus an `[autofocus]` element in the new fragment. - If focus was lost with the old fragment, focus the new fragment. - If updating a [main target](/up-main), focus the new fragment. @param {boolean} [config.runScripts=false] Whether to execute `<script>` tags in updated fragments. Scripts will load asynchronously, with no guarantee of execution order. If you set this to `true`, mind that the `<body>` element is a default [main target](/up-main). If you are including your global application scripts at the end of your `<body>` for performance reasons, swapping the `<body>` will re-execute these scripts. In that case you must configure a different main target that does not include your application scripts. @stable */ var CSS_HAS_SUFFIX_PATTERN, closest, config, contains, destroy, emitFragmentDestroyed, emitFragmentInserted, emitFragmentKeep, emitFragmentKept, emitFromKeepPlan, expandTargets, failKey, getAll, getDumb, getSmart, getSubtree, hasAutoHistory, hello, isDestroying, isGoodClassForTarget, isNotDestroying, markFragmentAsDestroying, matches, navigate, parseSelector, parseTargetAndOptions, reload, render, renderLocalContent, renderRemoteContent, reset, resolveOriginReference, sourceOf, successKey, timeOf, toTarget, visit; config = new up.Config(function() { return { badTargetClasses: [/^up-/], navigateOptions: { focus: 'auto', scroll: 'auto', solo: true, feedback: true, fallback: true, history: 'auto', peel: true, cache: 'auto' }, matchAroundOrigin: true, runScripts: false, autoHistoryTargets: [':main'], autoFocus: ['hash', 'autofocus', 'main-if-main', 'target-if-lost'], autoScroll: ['hash', 'layer-if-main'] }; }); u.delegate(config, 'mainTargets', function() { return up.layer.config.any; }); reset = function() { return config.reset(); }; /*** Returns the URL the given element was retrieved from. If the given element was never directly updated, but part of a larger fragment update, the closest known source of an ancestor element is returned. \#\#\# Example In the HTML below, the element `#one` was loaded from the URL `/foo`: ```html <div id="one" up-source"/foo"> <div id="two">...</div> </div> ``` We can now ask for the source of an element: ```javascript up.fragment.source('#two') // returns '/foo' ``` @function up.fragment.source @param {Element|string} element The element or CSS selector for which to look up the source URL. @return {string|undefined} @stable */ sourceOf = function(element, options) { if (options == null) { options = {}; } element = getSmart(element, options); return e.closestAttr(element, 'up-source'); }; /*** Returns a timestamp for the last modification of the content in the given element. @function up.fragment.time @param {Element} element @return {string} @internal */ timeOf = function(element) { return e.closestAttr(element, 'up-time') || '0'; }; /*** Sets the time when the fragment's underlying data was last changed. This can be used to avoid rendering unchanged HTML when [reloading](/up.reload) a fragment. This saves <b>CPU time</b> and reduces the <b>bandwidth cost</b> for a request/response exchange to **~1 KB**. \#\# Example Let's say we display a list of recent messages. We use the `[up-poll]` attribute to reload the `.messages` fragment every 30 seconds: ```html <div class="messages" up-poll> ... </div> ``` The list is now always up to date. But most of the time there will not be new messages, and we waste resources sending the same unchanged HTML from the server. We can improve this by setting an `[up-time]` attribute and the message list. The attribute value is the time of the most recent message. The time is encoded as the number of seconds since [Unix epoch](https://en.wikipedia.org/wiki/Unix_time). When, for instance, the last message in a list was received from December 24th, 1:51:46 PM UTC, we use the following HTML: ```html <div class="messages" up-time="1608730818" up-poll> ... </div> ``` When reloading Unpoly will echo the `[up-time]` timestamp in an `X-Up-Reload-From-Time` header: ```http X-Up-Reload-From-Time: 1608730818 ``` The server can compare the time from the request with the time of the last data update. If no more recent data is available, the server can render nothing and respond with an [`X-Up-Target: :none`](/X-Up-Target) header. Here is an example with [unpoly-rails](https://unpoly.com/install/rails): ```ruby class MessagesController < ApplicationController def index if up.reload_from_time == current_user.last_message_at up.render_nothing else @messages = current_user.messages.order(time: :desc).to_a render 'index' end end end ``` @selector [up-time] @param {string} up-time The number of seconds between the [Unix epoch](https://en.wikipedia.org/wiki/Unix_time). and the time when the element's underlying data was last changed. @experimental */ /*** Sets this element's source URL for [reloading](/up.reload) and [polling](/up-poll) When an element is reloaded, Unpoly will make a request from the URL that originally brought the element into the DOM. You may use `[up-source]` to use another URL instead. \#\#\# Example Assume an application layout with an unread message counter. You use `[up-poll]` to refresh the counter every 30 seconds. By default this would make a request to the URL that originally brought the counter element into the DOM. To save the server from rendering a lot of unused HTML, you may poll from a different URL like so: <div class="unread-count" up-poll up-source="/unread-count"> 2 new messages </div> @selector [up-source] @param {string} up-source The URL from which to reload this element. @stable */ /*** Replaces elements on the current page with matching elements from a server response or HTML string. The current and new elements must both match the same CSS selector. The selector is either given as `{ target }` option, or a [main target](/up-main) is used as default. See the [fragment placement](/fragment-placement) selector for many examples for how you can target content. This function has many options to enable scrolling, focus, request cancelation and other side effects. These options are all disabled by default and must be opted into one-by-one. To enable defaults that a user would expects for navigation (like clicking a link), pass [`{ navigate: true }`](#options.navigate) or use `up.navigate()` instead. \#\#\# Passing the new fragment The new fragment content can be passed as one of the following options: - [`{ url }`](#options.url) fetches and renders content from the server - [`{ document }`](#options.document) renders content from a given HTML document string or partial document - [`{ fragment }`](#options.fragment) renders content from a given HTML string that only contains the new fragment - [`{ content }`](#options-content) replaces the targeted fragment's inner HTML with the given HTML string \#\#\# Example Let's say your current HTML looks like this: ```html <div class="one">old one</div> <div class="two">old two</div> ``` We now replace the second `<div>` by targeting its CSS class: ```js up.render({ target: '.two', url: '/new' }) ``` The server renders a response for `/new`: ```html <div class="one">new one</div> <div class="two">new two</div> ``` Unpoly looks for the selector `.two` in the response and [implants](/up.extract) it into the current page. The current page now looks like this: ```html <div class="one">old one</div> <div class="two">new two</div> ``` Note how only `.two` has changed. The update for `.one` was discarded, since it didn't match the selector. \#\#\# Events Unpoly will emit events at various stages of the rendering process: - `up:fragment:destroyed` - `up:fragment:loaded` - `up:fragment:inserted` @function up.render @param {string|Element|jQuery|Array<string>} [target] The CSS selector to update. If omitted a [main target](/up-main) will be rendered. You may also pass a DOM element or jQuery element here, in which case a selector will be [inferred from the element attributes](/up.fragment.toTarget). The given element will also be used as [`{ origin }`](#options.origin) for the fragment update. You may also pass an array of selector alternatives. The first selector matching in both old and new content will be used. Instead of passing the target as the first argument, you may also pass it as a [´{ target }`](#options.target) option.. @param {string|Element|jQuery|Array<string>} [options.target] The CSS selector to update. See documentation for the [`target`](#target) parameter. @param {string|boolean} [options.fallback=false] Specifies behavior if the [target selector](/up.render#options.target) is missing from the current page or the server response. If set to a CSS selector string, Unpoly will attempt to replace that selector instead. If set to `true` Unpoly will attempt to replace a [main target](/up-main) instead. If set to `false` Unpoly will immediately reject the render promise. @param {boolean} [options.navigate=false] Whether this fragment update is considered [navigation](/navigation). @param {string} [options.url] The URL to fetch from the server. Instead of making a server request, you may also pass an existing HTML string as [`{ document }`](#options.document), [`{ fragment }`](#options.fragment) or [`{ content }`](#options.content) option. @param {string} [options.method='get'] The HTTP method to use for the request. Common values are `'get'`, `'post'`, `'put'`, `'patch'` and `'delete`'. The value is case insensitive. @param {Object|FormData|string|Array} [options.params] Additional [parameters](/up.Params) that should be sent as the request's [query string](https://en.wikipedia.org/wiki/Query_string) or payload. When making a `GET` request to a URL with a query string, the given `{ params }` will be added to the query parameters. @param {Object} [options.headers={}] An object with additional request headers. Note that Unpoly will by default send a number of custom request headers. E.g. the `X-Up-Target` header includes the targeted CSS selector. See `up.protocol` and `up.network.config.metaKeys` for details. @param {string|Element} [options.fragment] A string of HTML comprising *only* the new fragment. The `{ target }` selector will be derived from the root element in the given HTML: ```js // This will update .foo up.render({ fragment: '<div class=".foo">inner</div>' }) ``` If your HTML string contains other fragments that will not be rendered, use the [`{ document }`](#options.document) option instead. If your HTML string comprises only the new fragment's [inner HTML](https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML), consider the [`{ content }`](#options.content) option. @param {string|Element|Document} [options.document] A string of HTML containing the new fragment. The string may contain other HTML, but only the element matching the `{ target }` selector will be extracted and placed into the page. Other elements will be discarded. If your HTML string comprises only the new fragment, consider the [`{ fragment }`](#options.fragment) option instead. With `{ fragment }` you don't need to pass a `{ target }`, since Unpoly can derive it from the root element in the given HTML. If your HTML string comprises only the new fragment's [inner HTML](https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML), consider the [`{ content }`](#options.content) option. @param {string} [options.fail='auto'] How to render a server response with an error code. Any HTTP status code other than 2xx is considered an error code. See [handling server errors](/server-errors) for details. @param {boolean|string} [options.history] Whether the browser URL and window title will be updated. If set to `true`, the history will always be updated, using the title and URL from the server response, or from given `{ title }` and `{ location }` options. If set to `'auto'` history will be updated if the `{ target }` matches a selector in `up.fragment.config.autoHistoryTargets`. By default this contains all [main targets](/up-main). If set to `false`, the history will remain unchanged. [Overlays](/up.layer) will only change the browser URL and window title if the overlay has [visible history](/up.layer.historyVisible), even when `{ history: true }` is passed. @param {string} [options.title] An explicit document title to use after rendering. By default the title is extracted from the response's `<title>` tag. You may also pass `{ title: false }` to explicitly prevent the title from being updated. Note that the browser's window title will only be updated it you also pass a [`{ history }`](#options.history) option. @param {string} [options.location] An explicit URL to use after rendering. By default Unpoly will use the `{ url }` or the final URL after the server redirected. You may also pass `{ location: false }` to explicitly prevent the URL from being updated. Note that the browser's URL will only be updated it you also pass a [`{ history }`](#options.history) option. @param {string} [options.transition] The name of an [transition](/up.motion) to morph between the old and few fragment. If you are [prepending or appending content](/fragment-placement#appending-or-prepending-content), use the `{ animation }` option instead. @param {string} [options.animation] The name of an [animation](/up.motion) to reveal a new fragment when [prepending or appending content](/fragment-placement#appending-or-prepending-content). If you are replacing content (the default), use the `{ transition }` option instead. @param {number} [options.duration] The duration of the transition or animation (in millisconds). @param {string} [options.easing] The timing function that accelerates the transition or animation. See [W3C documentation](http://www.w3.org/TR/css3-transitions/#transition-timing-function) for a list of available timing functions. @param {boolean} [options.cache] Whether to read from and write to the [cache](/up.cache). With `{ cache: true }` Unpoly will try to re-use a cached response before connecting to the network. If no cached response exists, Unpoly will make a request and cache the server response. Also see [`up.request({ cache })`](/up.request#options.cache). @param {boolean|string} [options.clearCache] Whether existing [cache](/up.cache) entries will be [cleared](/up.cache.clear) with this request. You may also pass a [URL pattern](/url-patterns) to only clear matching requests. By default a non-GET request will clear the entire cache. Also see [`up.request({ clearCache })`](/up.request#options.clearCache) and `up.network.config.clearCache`. @param {Element|jQuery} [options.origin] The element that triggered the change. When multiple elements in the current page match the `{ target }`, Unpoly will replace an element in the [origin's vicinity](/fragment-placement). The origin's selector will be substituted for `:origin` in a target selector. @param {string|up.Layer|Element} [options.layer='origin current'] The [layer](/up.layer) in which to match and render the fragment. See [layer option](/layer-option) for a list of allowed values. To [open the fragment in a new overlay](/opening-overlays), pass `{ layer: 'new' }`. In this case options for `up.layer.open()` may also be used. @param {boolean} [options.peel] Whether to close overlays obstructing the updated layer when the fragment is updated. This is only relevant when updating a layer that is not the [frontmost layer](/up.layer.front). @param {Object} [options.context] An object that will be merged into the [context](/context) of the current layer once the fragment is rendered. @param {boolean} [options.keep=true] Whether [`[up-keep]`](/up-keep) elements will be preserved in the updated fragment. @param {boolean} [options.hungry=true] Whether [`[up-hungry]`](/up-hungry) elements outside the updated fragment will also be updated. @param {boolean|string|Element|Function} [options.scroll] How to scroll after the new fragment was rendered. See [scroll option](/scroll-option) for a list of allowed values. @param {boolean} [options.saveScroll=true] Whether to save scroll positions before updating the fragment. Saved scroll positions can later be restored with [`{ scroll: 'restore' }`](/scroll-option#restoring-scroll-options). @param {boolean|string|Element|Function} [options.focus] What to focus after the new fragment was rendered. See [focus option](/focus-option) for a list of allowed values. @param {string} [options.confirm] A message the user needs to confirm before fragments are updated. The message will be shown as a [native browser prompt](https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt). If the user does not confirm the render promise will reject and no fragments will be updated. @param {boolean|Element} [options.feedback] Whether to give the [`{ origin }`](#options.origin) element an `.up-active` class while loading and rendering content. May also pass an element which should receive the `.up-active` class. @param {Function(Event)} [options.onLoaded] A callback that will be run when when the server responds with new HTML, but before the HTML is rendered. The callback argument is a preventable `up:fragment:loaded` event. @param {Function()} [options.onFinished] A callback that will be run when all animations have concluded and elements were removed from the DOM tree. @return {Promise<up.RenderResult>} A promise that fulfills when the page has been updated. If the update is animated, the promise will be resolved *before* the existing element was removed from the DOM tree. The old element will be marked with the `.up-destroying` class and removed once the animation finishes. To run code after the old element was removed, pass an `{ onFinished }` callback. The promise will fulfill with an `up.RenderResult` that contains references to the updated fragments and layer. @stable */ render = up.mockable(function() { var args; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; return u.asyncify(function() { var guardEvent, options; options = parseTargetAndOptions(args); options = up.RenderOptions.preprocess(options); up.browser.assertConfirmed(options); if (guardEvent = u.pluckKey(options, 'guardEvent')) { guardEvent.renderOptions = options; up.event.assertEmitted(guardEvent, { target: options.origin }); } up.RenderOptions.assertContentGiven(options); if (options.url) { return renderRemoteContent(options); } else { return renderLocalContent(options); } }); }); renderRemoteContent = function(options) { return up.feedback.aroundForOptions(options, function() { return new up.Change.FromURL(options).execute(); }); }; renderLocalContent = function(options) { up.network.mimicLocalRequest(options); return new up.Change.FromContent(options).execute(); }; /*** [Navigates](/navigation) to the given URL by updating a major fragment in the current page. `up.navigate()` will mimic a click on a vanilla `<a href>` link to satisfy user expectations regarding scrolling, focus, request cancelation and [many other side effects](/navigation). Instead of calling `up.navigate()` you may also call `up.render({ navigate: true }`) option for the same effect. @function up.navigate @param {string|Element|jQuery} [target] The CSS selector to update. If omitted a [main target](/up-main) will be rendered. You can also pass a DOM element or jQuery element here, in which case a selector will be [inferred from the element attributes](/up.fragment.target). The given element will also be set as the `{ origin }` option. Instead of passing the target as the first argument, you may also pass it as [´{ target }` option](/up.fragment.render#options.target). @param {Object} [options] See options for `up.render()`. @stable */ navigate = up.mockable(function() { var args, options; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; options = parseTargetAndOptions(args); return render(u.merge(options, { navigate: true })); }); /*** This event is [emitted](/up.emit) when the server responds with the HTML, before the HTML is used to [change a fragment](/up.render). Event listeners may call `event.preventDefault()` on an `up:fragment:loaded` event to prevent any changes to the DOM and browser history. This is useful to detect an entirely different page layout (like a maintenance page or fatal server error) which should be open with a full page load: ```js up.on('up:fragment:loaded', (event) => { let isMaintenancePage = event.response.getHeader('X-Maintenance') if (isMaintenancePage) { // Prevent the fragment update and don't update browser history event.preventDefault() // Make a full page load for the same request. event.request.loadPage() } }) ``` Instead of preventing the update, listeners may also access the `event.renderOptions` object to mutate options to the `up.render()` call that will process the server response. @event up:fragment:loaded @param event.preventDefault() Event listeners may call this method to prevent the fragment change. @param {up.Request} event.request The original request to the server. @param {up.Response} event.response The server response. @param {Object} event.renderOptions Options for the `up.render()` call that will process the server response. @stable */ /*** Elements with an `up-keep` attribute will be persisted during [fragment updates](/up.fragment). The element you're keeping should have an umambiguous class name, ID or `[up-id]` attribute so Unpoly can find its new position within the page update. Emits events [`up:fragment:keep`](/up:fragment:keep) and [`up:fragment:kept`](/up:fragment:kept). \#\#\# Example The following `<audio>` element will be persisted through fragment updates as long as the responses contain an element matching `#player`: ```html <audio id="player" up-keep src="song.mp3"></audio> ``` \#\#\# Controlling if an element will be kept Unpoly will **only** keep an existing element if: - The existing element has an `up-keep` attribute - The response contains an element matching the CSS selector of the existing element - The matching element *also* has an `up-keep` attribute - The [`up:fragment:keep`](/up:fragment:keep) event that is [emitted](/up.emit) on the existing element is not prevented by a event listener. Let's say we want only keep an `<audio>` element as long as it plays the same song (as identified by the tag's `src` attribute). On the client we can achieve this by listening to an `up:keep:fragment` event and preventing it if the `src` attribute of the old and new element differ: up.compiler('audio', function(element) { element.addEventListener('up:fragment:keep', function(event) { if element.getAttribute('src') !== event.newElement.getAttribute('src') { event.preventDefault() } }) }) If we don't want to solve this on the client, we can achieve the same effect on the server. By setting the value of the `up-keep` attribute we can define the CSS selector used for matching elements. <audio up-keep="audio[src='song.mp3']" src="song.mp3"></audio> Now, if a response no longer contains an `<audio src="song.mp3">` tag, the existing element will be destroyed and replaced by a fragment from the response. @selector [up-keep] @param up-on-keep Code to run before an existing element is kept during a page update. The code may use the variables `event` (see `up:fragment:keep`), `this` (the old fragment), `newFragment` and `newData`. @stable */ /*** This event is [emitted](/up.emit) before an existing element is [kept](/up-keep) during a page update. Event listeners can call `event.preventDefault()` on an `up:fragment:keep` event to prevent the element from being persisted. If the event is prevented, the element will be replaced by a fragment from the response. @event up:fragment:keep @param event.preventDefault() Event listeners may call this method to prevent the element from being preserved. @param {Element} event.target The fragment that will be kept. @param {Element} event.newFragment The discarded element. @param {Object} event.newData The value of the [`up-data`](/up-data) attribute of the discarded element, parsed as a JSON object. @stable */ /*** This event is [emitted](/up.emit) when an existing element has been [kept](/up-keep) during a page update. Event listeners can inspect the discarded update through `event.newElement` and `event.newData` and then modify the preserved element when necessary. @event up:fragment:kept @param {Element} event.target The fragment that has been kept. @param {Element} event.newFragment The discarded fragment. @param {Object} event.newData The value of the [`up-data`](/up-data) attribute of the discarded fragment, parsed as a JSON object. @stable */ /*** Compiles a page fragment that has been inserted into the DOM by external code. **As long as you manipulate the DOM using Unpoly, you will never need to call this method.** You only need to use `up.hello()` if the DOM is manipulated without Unpoly' involvement, e.g. by setting the `innerHTML` property or calling jQuery methods like `html`, `insertAfter` or `appendTo`: ```html element = document.createElement('div') element.innerHTML = '... HTML that needs to be activated ...' up.hello(element) ``` This function emits the [`up:fragment:inserted`](/up:fragment:inserted) event. @function up.hello @param {Element|jQuery} target @param {Element|jQuery} [options.origin] @return {Element} The compiled element @stable */ hello = function(element, options) { var keepPlans, skip; if (options == null) { options = {}; } element = getSmart(element); keepPlans = options.keepPlans || []; skip = keepPlans.map(function(plan) { emitFragmentKept(plan); return plan.oldElement; }); up.syntax.compile(element, { skip: skip, layer: options.layer }); emitFragmentInserted(element, options); return element; }; /*** When any page fragment has been [inserted or updated](/up.replace), this event is [emitted](/up.emit) on the fragment. If you're looking to run code when a new fragment matches a selector, use `up.compiler()` instead. \#\#\# Example up.on('up:fragment:inserted', function(event, fragment) { console.log("Looks like we have a new %o!", fragment) }) @event up:fragment:inserted @param {Element} event.target The fragment that has been inserted or updated. @stable */ emitFragmentInserted = function(element, options) { return up.emit(element, 'up:fragment:inserted', { log: ['Inserted fragment %o', element], origin: options.origin }); }; emitFragmentKeep = function(keepPlan) { var callback, log; log = ['Keeping fragment %o', keepPlan.oldElement]; callback = e.callbackAttr(keepPlan.oldElement, 'up-on-keep', ['newFragment', 'newData']); return emitFromKeepPlan(keepPlan, 'up:fragment:keep', { log: log, callback: callback }); }; emitFragmentKept = function(keepPlan) { var log; log = ['Kept fragment %o', keepPlan.oldElement]; return emitFromKeepPlan(keepPlan, 'up:fragment:kept', { log: log }); }; emitFromKeepPlan = function(keepPlan, eventType, emitDetails) { var event, keepable; keepable = keepPlan.oldElement; event = up.event.build(eventType, { newFragment: keepPlan.newElement, newData: keepPlan.newData }); return up.emit(keepable, event, emitDetails); }; emitFragmentDestroyed = function(fragment, options) { var log, parent, ref; log = (ref = options.log) != null ? ref : ['Destroyed fragment %o', fragment]; parent = options.parent || document; return up.emit(parent, 'up:fragment:destroyed', { fragment: fragment, parent: parent, log: log }); }; isDestroying = function(element) { return !!e.closest(element, '.up-destroying'); }; isNotDestroying = function(element) { return !isDestroying(element); }; /*** Returns the first fragment matching the given selector. This function differs from `document.querySelector()` and `up.element.get()`: - This function only selects elements in the [current layer](/up.layer.current). Pass a `{ layer }`option to match elements in other layers. - This function ignores elements that are being [destroyed](/up.destroy) or that are being removed by a [transition](/up.morph). - This function prefers to match elements in the vicinity of a given `{ origin }` element (optional). - This function supports non-standard CSS selectors like `:main` and `:has()`. If no element matches these conditions, `undefined` is returned. \#\#\# Example: Matching a selector in a layer To select the first element with the selector `.foo` on the [current layer](/up.layer.current): let foo = up.fragment.get('.foo') You may also pass a `{ layer }` option to match elements within another layer: let foo = up.fragment.get('.foo', { layer: 'any' }) \#\#\# Example: Matching the descendant of an element To only select in the descendants of an element, pass a root element as the first argument: let container = up.fragment.get('.container') let fooInContainer = up.fragment.get(container, '.foo') \#\#\# Example: Matching around an origin element When processing a user interaction, it is often helpful to match elements around the link that's being clicked or the form field that's being changed. In this case you may pass the triggering element as `{ origin }` element. Assume the following HTML: ```html <div class="element"></div> <div class="element"> <a href="..."></a> </div> ``` When processing an event for the `<a href"...">` you can pass the link element as `{ origin }` to match the closest element in the link's ancestry: ```javascript let link = event.target up.fragment.get('.element') // returns the first .element up.fragment.get('.element', { origin: link }) // returns the second .element ``` When the link's does not have an ancestor matching `.element`, Unpoly will search the entire layer for `.element`. \#\#\# Example: Matching an origin sibling When processing a user interaction, it is often helpful to match elements within the same container as the the link that's being clicked or the form field that's being changed. Assume the following HTML: ```html <div class="element" id="one"> <div class="inner"></div> </div> <div class="element" id="two"> <a href="..."></a> <div class="inner"></div> </div> ``` When processing an event for the `<a href"...">` you can pass the link element as `{ origin }` to match within the link's container: ```javascript let link = event.target up.fragment.get('.element .inner') // returns the first .inner up.fragment.get('.element .inner', { origin: link }) // returns the second .inner ``` Note that when the link's `.element` container does not have a child `.inner`, Unpoly will search the entire layer for `.element .inner`. \#\#\# Similar features - The [`.up-destroying`](/up-destroying) class is assigned to elements during their removal animation. - The [`up.element.get()`](/up.element.get) function simply returns the first element matching a selector without filtering by layer or destruction state. @function up.fragment.get @param {Element|jQuery} [root=document] The root element for the search. Only the root's children will be matched. May be omitted to search through all elements in the `document`. @param {string} selector The selector to match. @param {string} [options.layer='current'] The layer in which to select elements. See `up.layer.get()` for a list of supported layer values. If a root element was passed as first argument, this option is ignored and the root element's layer is searched. @param {string|Element|jQuery} [options.origin] An second element or selector that can be referenced as `&` in the first selector. @return {Element|undefined} The first matching element, or `undefined` if no such element matched. @stable */ getSmart = function() { var args, finder, options, root, selector; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; options = u.extractOptions(args); selector = args.pop(); root = args[0]; if (u.isElementish(selector)) { return e.get(selector); } if (root) { return getDumb(root, selector, options); } finder = new up.FragmentFinder({ selector: selector, origin: options.origin, layer: options.layer }); return finder.find(); }; getDumb = function() { var args; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; return getAll.apply(null, args)[0]; }; CSS_HAS_SUFFIX_PATTERN = /\:has\(([^\)]+)\)$/; /*** Returns all elements matching the given selector, but ignores elements that are being [destroyed](/up.destroy) or that are being removed by a [transition](/up.morph). By default this function only selects elements in the [current layer](/up.layer.current). Pass a `{ layer }`option to match elements in other layers. See `up.layer.get()` for a list of supported layer values. Returns an empty list if no element matches these conditions. \#\#\# Example To select all elements with the selector `.foo` on the [current layer](/up.layer.current): let foos = up.fragment.all('.foo') You may also pass a `{ layer }` option to match elements within another layer: let foos = up.fragment.all('.foo', { layer: 'any' }) To select in the descendants of an element, pass a root element as the first argument: var container = up.fragment.get('.container') var foosInContainer = up.fragment.all(container, '.foo') \#\#\# Similar features - The [`.up-destroying`](/up-destroying) class is assigned to elements during their removal animation. - The [`up.element.all()`](/up.element.get) function simply returns the all elements matching a selector without further filtering. @function up.fragment.all @param {Element|jQuery} [root=document] The root element for the search. Only the root's children will be matched. May be omitted to search through all elements in the given [layer](#options.layer). @param {string} selector The selector to match. @param {string} [options.layer='current'] The layer in which to select elements. See `up.layer.get()` for a list of supported layer values. If a root element was passed as first argument, this option is ignored and the root element's layer is searched. @param {string|Element|jQuery} [options.origin] An second element or selector that can be referenced as `&` in the first selector: var input = document.querySelector('input.email') up.fragment.get('fieldset:has(&)', { origin: input }) // returns the <fieldset> containing input @return {Element|undefined} The first matching element, or `undefined` if no such element matched. @stable */ getAll = function() { var args, options, root, selector; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; options = u.extractOptions(args); selector = args.pop(); root = args[0]; selector = parseSelector(selector, root, options); return selector.descendants(root || document); }; /*** Your target selectors may use this pseudo-selector to replace an element with an descendant matching the given selector. \#\#\# Example `up.render('div:has(span)', { url: '...' })` replaces the first `<div>` elements with at least one `<span>` among its descendants: ```html <div> <span>Will be replaced</span> </div> <div> Will NOT be replaced </div> ``` \#\#\# Compatibility `:has()` is supported by target selectors like `a[up-target]` and `up.render({ target })`. As a [level 4 CSS selector](https://drafts.csswg.org/selectors-4/#relational), `:has()` [has yet to be implemented](https://caniuse.com/#feat=css-has) in native browser functions like [`document.querySelectorAll()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/querySelectorAll). You can also use [`:has()` in jQuery](https://api.jquery.com/has-selector/). @selector :has() @stable */ /*** Returns a list of the given parent's descendants matching the given selector. The list will also include the parent element if it matches the selector itself. @function up.fragment.subtree @param {Element} parent The parent element for the search. @param {string} selector The CSS selector to match. @param {up.Layer|string|Element} [options.layer] @return {NodeList<Element>|Array<Element>} A list of all matching elements. @experimental */ getSubtree = function(element, selector, options) { if (options == null) { options = {}; } selector = parseSelector(selector, element, options); return selector.subtree(element); }; contains = function(element, selector) { return getSubtree(element, selector).length > 0; }; /*** Returns the first element that matches the selector by testing the element itself and traversing up through ancestors in element's layers. `up.fragment.closest()` will only match elements in the same [layer](/up.layer) as the given element. To match ancestors regardless of layers, use `up.element.closest()`. @function up.fragment.closest @param {Element} element The element on which to start the search. @param {string} selector The CSS selector to match. @return {Element|null|undefined} element The matching element. Returns `null` or `undefined` if no element matches in the same layer. @experimental */ closest = function(element, selector, options) { element = e.get(element); selector = parseSelector(selector, element, options); return selector.closest(element); }; /*** Destroys the given element or selector. All [`up.compiler()`](/up.compiler) destructors, if any, are called. The element is then removed from the DOM. Emits events [`up:fragment:destroyed`](/up:fragment:destroyed). \#\#\# Animating the removal You may animate the element's removal by passing an option like `{ animate: 'fade-out' }`. Unpoly ships with a number of [predefined animations](/up.animate#named-animations) and you may so define [custom animations](/up.animation). If the element's removal is animated, the element will remain in the DOM until after the animation has completed. While the animation is running the element will be given the `.up-destroying` class. The element will also be given the `[aria-hidden]` attribute to remove it from the accessibility tree. Elements that are about to be destroyed (but still animating) are ignored by all functions for fragment lookup: - `up.fragment.all()` - `up.fragment.get()` - `up.fragment.closest()` @function up.destroy @param {string|Element|jQuery} target @param {string|Function(element, options): Promise} [options.animation='none'] The animation to use before the element is removed from the DOM. @param {number} [options.duration] The duration of the animation. See [`up.animate()`](/up.animate). @param {string} [options.easing] The timing function that controls the animation's acceleration. See [`up.animate()`](/up.animate). @param {Function} [options.onFinished] A callback that is run when any animations are finished and the element was removed from the DOM. @return undefined @stable */ destroy = function() { var args, base, options; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; options = parseTargetAndOptions(args); if (options.element = getSmart(options.target, options)) { new up.Change.DestroyFragment(options).execute(); } return typeof (base = up.migrate).formerlyAsync === "function" ? base.formerlyAsync('up.destroy()') : void 0; }; parseTargetAndOptions = function(args) { var options; options = u.parseArgIntoOptions(args, 'target'); if (u.isElement(options.target)) { options.origin || (options.origin = options.target); } return options; }; /*** Elements are assigned the `.up-destroying` class before they are [destroyed](/up.destroy) or while they are being removed by a [transition](/up.morph). If the removal is [animated](/up.destroy#animating-the-removal), the class is assigned before the animation starts. Elements that are about to be destroyed (but still animating) are ignored by all functions for fragment lookup: - `up.fragment.all()` - `up.fragment.get()` - `up.fragment.closest()` @selector .up-destroying @stable */ markFragmentAsDestroying = function(element) { element.classList.add('up-destroying'); return element.setAttribute('aria-hidden', 'true'); }; /*** This event is [emitted](/up.emit) after a page fragment was [destroyed](/up.destroy) and removed from the DOM. If the destruction is animated, this event is emitted after the animation has ended. The event is emitted on the parent element of the fragment that was removed. @event up:fragment:destroyed @param {Element} event.fragment The detached element that has been removed from the DOM. @param {Element} event.parent The former parent element of the fragment that has now been detached from the DOM. @param {Element} event.target The former parent element of the fragment that has now been detached from the DOM. @stable */ /*** Replaces the given element with a fresh copy fetched from the server. By default, reloading is not considered a [user navigation](/navigation) and e.g. will not update the browser location. You may change this with `{ navigate: true }`. \#\#\# Example up.on('new-mail', function() { up.reload('.inbox') }) \#\#\# Controlling the URL that is reloaded Unpoly remembers [the URL from which a fragment was loaded](/up.fragment.source), so you don't usually need to pass a URL when reloading. To reload from another URL, pass a `{ url }` option or set an `[up-source]` attribute on the element or its ancestors. \#\#\# Skipping updates when nothing changed You may use the `[up-time]` attribute to avoid rendering unchanged HTML when reloading a fragment. See `[up-time]` for a detailed example. @function up.reload @param {string|Element|jQuery} [target] The element that should be reloaded. If omitted, an element matching a selector in `up.fragment.config.mainTargets` will be reloaded. @param {Object} [options] See options for `up.render()`. @param {string} [options.url] The URL from which to reload the fragment. This defaults to the URL from which the fragment was originally loaded. @param {string} [options.navigate=false] Whether the reloading constitutes a [user navigation](/navigation). @stable */ reload = function() { var args, element, options; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; options = parseTargetAndOptions(args); options.target || (options.target = ':main'); element = getSmart(options.target, options); if (options.url == null) { options.url = sourceOf(element); } options.headers || (options.headers = {}); options.headers[up.protocol.headerize('reloadFromTime')] = timeOf(element); return render(options); }; /*** Fetches this given URL with JavaScript and [replaces](/up.replace) the [current layer](/up.layer.current)'s [main element](/up.fragment.config#config.mainSelectors) with a matching fragment from the server response. \#\#\# Example This would replace the current page with the response for `/users`: up.visit('/users') @function up.visit @param {string} url The URL to visit. @param {Object} [options] See options for `up.render()`. @param {up.Layer|string|number} [options.layer='current'] @stable */ visit = function(url, options) { return navigate(u.merge({ url: url }, options)); }; successKey = function(key) { return u.unprefixCamelCase(key, 'fail'); }; failKey = function(key) { if (!key.match(/^fail[A-Z]/)) { return u.prefixCamelCase(key, 'fail'); } }; /*** Returns a CSS selector that matches the given element as good as possible. To build the selector, the following element properties are used in decreasing order of priority: - The element's `[up-id]` attribute - The element's `[id]` attribute - The element's `[name]` attribute - The element's `[class]` names, ignoring `up.fragment.config.badTargetClasses`. - The element's tag name \#\#\# Example ```js element = up.element.createFromHTML('<span class="klass">...</span>') selector = up.fragment.toTarget(element) // returns '.klass' ``` @function up.fragment.toTarget @param {string|Element|jQuery} The element for which to create a selector. @stable */ toTarget = function(element) { var goodClasses, i, id, klass, len, name, selector, upId; if (u.isString(element)) { return element; } element = e.get(element); if (e.isSingleton(element)) { return e.elementTagName(element); } else if (upId = element.getAttribute("up-id")) { return e.attributeSelector('up-id', upId); } else if (id = element.getAttribute("id")) { return e.idSelector(id); } else if (name = element.getAttribute("name")) { return e.elementTagName(element) + e.attributeSelector('name', name); } else if (goodClasses = u.presence(u.filter(element.classList, isGoodClassForTarget))) { selector = ''; for (i = 0, len = goodClasses.length; i < len; i++) { klass = goodClasses[i]; selector += e.classSelector(klass); } return selector; } else { return e.elementTagName(element); } }; /*** Sets an unique identifier for this element. This identifier is used by `up.fragment.toSelector()` to create a CSS selector that matches this element precisely. If the element already has other attributes that make a good identifier, like a good `[id]` or `[class]` attribute, it is not necessary to also set `[up-id]`. \#\#\# Example Take this element: <a href="/">Homepage</a> Unpoly cannot generate a good CSS selector for this element: up.fragment.toTarget(element) // returns 'a' We can improve this by assigning an `[up-id]`: <a href="/" up-id="link-to-home">Open user 4</a> The attribute value is used to create a better selector: up.fragment.toTarget(element) // returns '[up-id="link-to-home"]' @selector [up-id] @param up-id A string that uniquely identifies this element. @stable */ isGoodClassForTarget = function(klass) { var matchesPattern; matchesPattern = function(pattern) { if (u.isRegExp(pattern)) { return pattern.test(klass); } else { return pattern === klass; } }; return !u.some(config.badTargetClasses, matchesPattern); }; resolveOriginReference = function(target, options) { var origin; if (options == null) { options = {}; } origin = options.origin; return target.replace(/&|:origin\b/, function(match) { if (origin) { return toTarget(origin); } else { return up.fail('Missing { origin } element to resolve "%s" reference (found in %s)', match, target); } }); }; /*** @internal */ expandTargets = function(targets, options) { var expanded, layer, mode, target; if (options == null) { options = {}; } layer = options.layer; if (!(layer === 'new' || (layer instanceof up.Layer))) { up.fail('Must pass an up.Layer as { layer } option, but got %o', layer); } targets = u.copy(u.wrapList(targets)); expanded = []; while (targets.length) { target = targets.shift(); if (target === ':main' || target === true) { mode = layer === 'new' ? options.mode : layer.mode; targets.unshift.apply(targets, up.layer.mainTargets(mode)); } else if (target === ':layer') { if (!(layer === 'new' || layer.opening)) { targets.unshift(layer.getFirstSwappableElement()); } } else if (u.isElementish(target)) { expanded.push(toTarget(target)); } else if (u.isString(target)) { expanded.push(resolveOriginReference(target, options)); } else { } } return u.uniq(expanded); }; parseSelector = function(selector, element, options) { var expandedTargets, filters, layers; if (options == null) { options = {}; } filters = []; if (!options.destroying) { filters.push(isNotDestroying); } options.layer || (options.layer = element); layers = up.layer.getAll(options); if (options.layer !== 'any' && !(element && e.isDetached(element))) { filters.push(function(match) { return u.some(layers, function(layer) { return layer.contains(match); }); }); } expandedTargets = up.fragment.expandTargets(selector, u.merge(options, { layer: layers[0] })); expandedTargets = expandedTargets.map(function(target) { target = target.replace(CSS_HAS_SUFFIX_PATTERN, function(match, descendantSelector) { filters.push(function(element) { return element.querySelector(descendantSelector); }); return ''; }); return target || '*'; }); return new up.Selector(expandedTargets, filters); }; hasAutoHistory = function(fragment) { if (contains(fragment, config.autoHistoryTargets)) { return true; } else { up.puts('up.render()', "Will not auto-update history because fragment doesn't contain a selector from up.fragment.config.autoHistoryTargets"); return false; } }; /*** A pseudo-selector that matches the layer's main target. Main targets are default render targets. When no other render target is given, Unpoly will try to find and replace a main target. In most app layouts the main target should match the primary content area. The default main targets are: - any element with an `[up-main]` attribute - the HTML5 [`<main>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/main) element - the current layer's [topmost swappable element](/layer) You may configure main target selectors in `up.fragment.config.mainTargets`. \#\#\# Example ```js up.render(':main', { url: '/page2' }) ``` @selector :main @experimental */ /*** Updates this element when no other render target is given. \#\#\# Example Many links simply replace the main content element in your application layout. Unpoly lets you mark this elements as a default target using the `[up-main]` attribute: ```html <body> <div class="layout"> <div class="layout--side"> ... </div> <div class="layout--content" up-main> ... </div> </div> </body> ``` Once a main target is configured, you no longer need `[up-target]` in a link.\ Use `[up-follow]` and the `[up-main]` element will be replaced: ```html <a href="/foo" up-follow>...</a> ``` If you want to update something more specific, you can still use `[up-target]`: ```html <a href="/foo" up-target=".profile">...</a> ``` Instead of assigning `[up-main]` you may also configure an existing selector in `up.fragment.config.mainTargets`: ```js up.fragment.config.mainTargets.push('.layout--content') ``` Overlays can use different main targets --------------------------------------- Overlays often use a different default selector, e.g. to exclude a navigation bar. To define a different main target for an overlay, set the [layer mode](/layer-terminology) as the value of the `[up-main]` attribute: ```html <body> <div class="layout" up-main="root"> <div class="layout--side"> ... </div> <div class="layout--content" up-main="modal"> ... </div> </div> </body> ``` Instead of assigning `[up-main]` you may also configure layer-specific targets in `up.layer.config`: ```js up.layer.config.popup.mainTargets.push('.menu') // for popup overlays up.layer.config.drawer.mainTargets.push('.menu') // for drawer overlays up.layer.config.overlay.mainTargets.push('.layout--content') // for all overlay modes ``` @selector [up-main] @param [up-main] A space-separated list of [layer modes](/layer-terminology) for which to use this main target. Omit the attribute value to define a main target for *all* layer modes. To use a different main target for all overlays (but not the root layer), set `[up-main=overlay]`. @stable */ /*** To make a server request without changing a fragment, use the `:none` selector. \#\#\# Example ```html <a href="/ping" up-target=":none">Ping server</a> ``` @selector :none @experimental */ /*** Your target selectors may use this pseudo-selector to reference the element that triggered the change. The origin element is automatically set to a link that is being [followed](/a-up-follow) or form that is being [submitted](/form-up-submit). When updating fragments programmatically through `up.render()` you may pass an origin element as an `{ origin }` option. Even without using an `:origin` reference, the [origin is considered](/fragment-placement#interaction-origin-is-considered) when matching fragments in the current page. \#\#\# Shorthand Instead of `:origin` you may also use the ampersand character (`&`). You may be familiar with the ampersand from the [Sass](https://sass-lang.com/documentation/file.SASS_REFERENCE.html#parent-selector) CSS preprocessor. @selector :origin @experimental */ /*** Your target selectors may use this pseudo-selector to replace the layer's topmost swappable element. The topmost swappable element is the first child of the layer's container element. For the [root layer](/up.layer.root) it is the `<body>` element. For an overlay it is the target with which the overlay was opened with. In canonical usage the topmost swappable element is often a [main element](/up-main). \#\#\# Example The following will replace the `<body>` element in the root layer, and the topmost swappable element in an overlay: ```js up.render(':layer', { url: '/page2' }) ``` @selector :layer @experimental */ /*** Returns whether the given element matches the given CSS selector. Other than `up.element.matches()` this function supports non-standard selectors like `:main` or `:layer`. @function up.fragment.matches @param {Element} fragment @param {string|Array<string>} selectorOrSelectors @param {string|up.Layer} options.layer The layer for which to match. Pseudo-selectors like `:main` may expand to different selectors in different layers. @param {string|up.Layer} options.mode Required if `{ layer: 'new' }` is passed. @return {boolean} @experimental */ matches = function(element, selector, options) { if (options == null) { options = {}; } element = e.get(element); selector = parseSelector(selector, element, options); return selector.matches(element); }; up.on('up:app:boot', function() { var body; body = document.body; body.setAttribute('up-source', up.history.location); hello(body); if (!up.browser.canPushState()) { return up.warn('Cannot push history changes. Next fragment update will load in a new page.'); } }); up.on('up:framework:reset', reset); return u.literal({ config: config, reload: reload, destroy: destroy, render: render, navigate: navigate, get: getSmart, getDumb: getDumb, all: getAll, subtree: getSubtree, contains: contains, closest: closest, source: sourceOf, hello: hello, visit: visit, markAsDestroying: markFragmentAsDestroying, emitInserted: emitFragmentInserted, emitDestroyed: emitFragmentDestroyed, emitKeep: emitFragmentKeep, emitKept: emitFragmentKept, successKey: successKey, failKey: failKey, expandTargets: expandTargets, toTarget: toTarget, matches: matches, hasAutoHistory: hasAutoHistory }); })(); up.replace = up.fragment.replace; up.extract = up.fragment.extract; up.reload = up.fragment.reload; up.destroy = up.fragment.destroy; up.render = up.fragment.render; up.navigate = up.fragment.navigate; up.hello = up.fragment.hello; up.visit = up.fragment.visit; /*** Returns the current [context](/context). This is aliased as `up.layer.context`. @property up.context @param {Object} context The context object. If no context has been set an empty object is returned. @experimental */ u.delegate(up, 'context', function() { return up.layer.current; }); }).call(this); /*** Scrolling viewports =================== The `up.viewport` module controls the scroll position and focus within scrollable containers ("viewports"). The default viewport for any web application is the main document. An application may define additional viewports by giving the CSS property `{ overflow-y: scroll }` to any `<div>`. Also see documentation for the [scroll option](/scroll-option) and [focus option](focus-option). @see up.reveal @see [up-fixed=top] @module up.viewport */ (function() { var slice = [].slice; up.viewport = (function() { var absolutize, allSelector, anchoredRight, closest, config, doFocus, e, f, firstHashTarget, fixedElements, getAll, getAround, getRoot, getScrollTops, getSubtree, isNativelyFocusable, isRoot, makeFocusable, parseOptions, pureHash, reset, resetScroll, restoreScroll, reveal, revealHash, rootHasReducedWidthFromScrollbar, rootHeight, rootOverflowElement, rootSelector, rootWidth, saveScroll, scroll, scrollTopKey, scrollTops, scrollbarWidth, scrollingController, setScrollTops, tryFocus, u, userScrolled, wasChosenAsOverflowingElement; u = up.util; e = up.element; f = up.fragment; /*** Configures defaults for scrolling. @property up.viewport.config @param {Array} [config.viewportSelectors] An array of CSS selectors that match viewports. @param {Array} [config.fixedTop] An array of CSS selectors that find elements fixed to the top edge of the screen (using `position: fixed`). See [`[up-fixed="top"]`](/up-fixed-top) for details. @param {Array} [config.fixedBottom] An array of CSS selectors that match elements fixed to the bottom edge of the screen (using `position: fixed`). See [`[up-fixed="bottom"]`](/up-fixed-bottom) for details. @param {Array} [config.anchoredRight] An array of CSS selectors that find elements anchored to the right edge of the screen (using `right:0` with `position: fixed` or `position: absolute`). See [`[up-anchored="right"]`](/up-anchored-right) for details. @param {number} [config.revealSnap] When [revealing](/up.reveal) elements, Unpoly will scroll an viewport to the top when the revealed element is closer to the viewport's top edge than `config.revealSnap`. Set to `0` to disable snapping. @param {number} [config.revealPadding] The desired padding between a [revealed](/up.reveal) element and the closest [viewport](/up.viewport) edge (in pixels). @param {number} [config.revealMax] A number indicating how many top pixel rows of a high element to [reveal](/up.reveal). Defaults to 50% of the available window height. You may set this to `false` to always reveal as much of the element as the viewport allows. You may also pass a function that receives an argument `{ viewportRect, elementRect }` and returns a maximum height in pixel. Each given rectangle has properties `{ top, right, buttom, left, width, height }`. @param {number} [config.revealTop=false] Whether to always scroll a [revealing](/up.reveal) element to the top. By default Unpoly will scroll as little as possible to make the element visible. @param {number} [config.scrollSpeed=1] The speed of the scrolling motion when [scrolling](/up.scroll) with `{ behavior: 'smooth' }`. The default value (`1`) roughly corresponds to the speed of Chrome's [native smooth scrolling](https://developer.mozilla.org/en-US/docs/Web/API/ScrollToOptions/behavior). @stable */ config = new up.Config(function() { return { viewportSelectors: ['[up-viewport]', '[up-fixed]'], fixedTop: ['[up-fixed~=top]'], fixedBottom: ['[up-fixed~=bottom]'], anchoredRight: ['[up-anchored~=right]', '[up-fixed~=top]', '[up-fixed~=bottom]', '[up-fixed~=right]'], revealSnap: 200, revealPadding: 0, revealTop: false, revealMax: function() { return 0.5 * window.innerHeight; }, scrollSpeed: 1 }; }); scrollingController = new up.MotionController('scrolling'); reset = function() { config.reset(); return scrollingController.reset(); }; /*** Scrolls the given viewport to the given Y-position. A "viewport" is an element that has scrollbars, e.g. `<body>` or a container with `overflow-x: scroll`. \#\#\# Example This will scroll a `<div class="main">...</div>` to a Y-position of 100 pixels: up.scroll('.main', 100) \#\#\# Animating the scrolling motion The scrolling can (optionally) be animated. up.scroll('.main', 100, { behavior: 'smooth' }) If the given viewport is already in a scroll animation when `up.scroll()` is called a second time, the previous animation will instantly jump to the last frame before the next animation is started. @function up.scroll @param {string|Element|jQuery} viewport The container element to scroll. @param {number} scrollPos The absolute number of pixels to set the scroll position to. @param {string}[options.behavior='auto'] When set to `'auto'`, this will immediately scroll to the new position. When set to `'smooth'`, this will scroll smoothly to the new position. @param {number}[options.speed] The speed of the scrolling motion when scrolling with `{ behavior: 'smooth' }`. Defaults to `up.viewport.config.scrollSpeed`. @return {Promise} A promise that will be fulfilled when the scrolling ends. @internal */ scroll = function(viewport, scrollTop, options) { var motion; if (options == null) { options = {}; } viewport = f.get(viewport, options); motion = new up.ScrollMotion(viewport, scrollTop, options); return scrollingController.startMotion(viewport, motion, options); }; /*** @function up.viewport.anchoredRight @internal */ anchoredRight = function() { var selector; selector = config.anchoredRight.join(','); return f.all(selector, { layer: 'root' }); }; /*** Scrolls the given element's viewport so the first rows of the element are visible for the user. \#\#\# Fixed elements obstructing the viewport Many applications have a navigation bar fixed to the top or bottom, obstructing the view on an element. You can make `up.reveal()` aware of these fixed elements so it can scroll the viewport far enough so the revealed element is fully visible. To make `up.reveal()` aware of fixed elements you can either: - give the element an attribute [`up-fixed="top"`](/up-fixed-top) or [`up-fixed="bottom"`](up-fixed-bottom) - [configure default options](/up.viewport.config) for `fixedTop` or `fixedBottom` @function up.reveal @param {string|Element|jQuery} element The element to reveal. @param {number} [options.scrollSpeed=1] The speed of the scrolling motion when scrolling with `{ behavior: 'smooth' }`. The default value (`1`) roughly corresponds to the speed of Chrome's [native smooth scrolling](https://developer.mozilla.org/en-US/docs/Web/API/ScrollToOptions/behavior). Defaults to `up.viewport.config.scrollSpeed`. @param {string} [options.revealSnap] When the the revealed element would be closer to the viewport's top edge than this value, Unpoly will scroll the viewport to the top. Set to `0` to disable snapping. Defaults to `up.viewport.config.revealSnap`. @param {string|Element|jQuery} [options.viewport] The scrolling element to scroll. Defaults to the [given element's viewport](/up.viewport.closest). @param {boolean} [options.top] Whether to scroll the viewport so that the first element row aligns with the top edge of the viewport. Defaults to `up.viewport.config.revealTop`. @param {string}[options.behavior='auto'] When set to `'auto'`, this will immediately scroll to the new position. When set to `'smooth'`, this will scroll smoothly to the new position. @param {number}[options.speed] The speed of the scrolling motion when scrolling with `{ behavior: 'smooth' }`. Defaults to `up.viewport.config.scrollSpeed`. @param {number} [options.padding] The desired padding between the revealed element and the closest [viewport](/up.viewport) edge (in pixels). Defaults to `up.viewport.config.revealPadding`. @param {number|boolean} [options.snap] Whether to snap to the top of the viewport if the new scroll position after revealing the element is close to the top edge. Defaults to `up.viewport.config.revealSnap`. @return {Promise} A promise that fulfills when the element is revealed. When the scrolling is animated with `{ behavior: 'smooth' }`, the promise fulfills when the animation is finished. When the scrolling is not animated, the promise will fulfill in the next [microtask](https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/). @stable */ reveal = function(element, options) { var motion; options = u.options(options); element = f.get(element, options); if (!(options.layer = up.layer.get(element))) { return up.error.failed.async('Cannot reveal a detached element'); } if (options.peel) { options.layer.peel(); } motion = new up.RevealMotion(element, options); return scrollingController.startMotion(element, motion, options); }; /*** Focuses the given element. Focusing an element will also [reveal](/up.reveal) it, unless `{ preventScroll: true }` is passed. @function up.focus @param {string|Element|jQuery} element The element to focus. @param {[options.preventScroll=false]} Whether to prevent changes to the acroll position. @experimental */ doFocus = function(element, options) { var oldScrollTop, viewport; if (options == null) { options = {}; } if (up.browser.isIE11()) { viewport = closest(element); oldScrollTop = viewport.scrollTop; element.focus(); viewport.scrollTop = oldScrollTop; } else { element.focus({ preventScroll: true }); } if (!options.preventScroll) { return reveal(element); } }; tryFocus = function(element, options) { doFocus(element, options); return element === document.activeElement; }; isNativelyFocusable = function(element) { return e.matches(element, 'a[href], button, textarea, input, select'); }; makeFocusable = function(element) { if (!(element.hasAttribute('tabindex') || isNativelyFocusable(element))) { element.setAttribute('tabindex', '-1'); return element.classList.add('up-focusable-content'); } }; /*** [Reveals](/up.reveal) an element matching the given `#hash` anchor. Other than the default behavior found in browsers, `up.revealHash` works with [multiple viewports](/up-viewport) and honors [fixed elements](/up-fixed-top) obstructing the user's view of the viewport. When the page loads initially, this function is automatically called with the hash from the current URL. If no element matches the given `#hash` anchor, a resolved promise is returned. \#\#\# Example up.revealHash('#chapter2') @function up.viewport.revealHash @param {string} hash @internal */ revealHash = function(hash, options) { var match; if (hash == null) { hash = location.hash; } if (match = firstHashTarget(hash, options)) { return up.reveal(match, { top: true }); } }; allSelector = function() { return [rootSelector()].concat(slice.call(config.viewportSelectors)).join(','); }; /*** Returns the scrolling container for the given element. Returns the [document's scrolling element](/up.viewport.root) if no closer viewport exists. @function up.viewport.get @param {string|Element|jQuery} target @return {Element} @experimental */ closest = function(target, options) { var element; if (options == null) { options = {}; } element = f.get(target, options); return e.closest(element, allSelector()); }; /*** Returns a list of all the viewports contained within the given selector or element. If the given element is itself a viewport, the element is included in the returned list. @function up.viewport.subtree @param {string|Element|jQuery} target @param {Object} options @return List<Element> @internal */ getSubtree = function(element, options) { if (options == null) { options = {}; } element = f.get(element, options); return e.subtree(element, allSelector()); }; /*** Returns a list of all viewports that are either contained within the given element or that are ancestors of the given element. This is relevant when updating a fragment with `{ scroll: 'restore' | 'reset' }`. In tht case we restore / reset the scroll tops of all viewports around the fragment. @function up.viewport.around @param {string|Element|jQuery} element @param {Object} options @return List<Element> @internal */ getAround = function(element, options) { if (options == null) { options = {}; } element = f.get(element, options); return e.around(element, allSelector()); }; /*** Returns a list of all the viewports on the current layer. @function up.viewport.all @internal */ getAll = function(options) { if (options == null) { options = {}; } return f.all(allSelector(), options); }; rootSelector = function() { var element; if (element = document.scrollingElement) { return element.tagName; } else { return 'html'; } }; /*** Return the [scrolling element](https://developer.mozilla.org/en-US/docs/Web/API/document/scrollingElement) for the browser's main content area. @function up.viewport.root @return {Element} @experimental */ getRoot = function() { return document.querySelector(rootSelector()); }; rootWidth = function() { return e.root.clientWidth; }; rootHeight = function() { return e.root.clientHeight; }; isRoot = function(element) { return e.matches(element, rootSelector()); }; /*** Returns whether the root viewport is currently showing a vertical scrollbar. Note that this returns `false` if the root viewport scrolls vertically but the browser shows no visible scroll bar at rest, e.g. on mobile devices that only overlay a scroll indicator while scrolling. @function up.viewport.rootHasReducedWidthFromScrollbar @internal */ rootHasReducedWidthFromScrollbar = function() { return window.innerWidth > document.documentElement.offsetWidth; }; /*** Returns the element that controls the `overflow-y` behavior for the [document viewport](/up.viewport.root()). @function up.viewport.rootOverflowElement @internal */ rootOverflowElement = function() { var body, element, html; body = document.body; html = document.documentElement; element = u.find([html, body], wasChosenAsOverflowingElement); return element || getRoot(); }; /*** Returns whether the given element was chosen as the overflowing element by the developer. We have no control whether developers set the property on <body> or <html>. The developer also won't know what is going to be the [scrolling element](/up.viewport.root) on the user's browser. @function wasChosenAsOverflowingElement @internal */ wasChosenAsOverflowingElement = function(element) { var overflowY; overflowY = e.style(element, 'overflow-y'); return overflowY === 'auto' || overflowY === 'scroll'; }; /*** Returns the width of a scrollbar. This only runs once per page load. @function up.viewport.scrollbarWidth @internal */ scrollbarWidth = u.memoize(function() { var outer, outerStyle, width; outerStyle = { position: 'absolute', top: '0', left: '0', width: '100px', height: '100px', overflowY: 'scroll' }; outer = up.element.affix(document.body, '[up-viewport]', { style: outerStyle }); width = outer.offsetWidth - outer.clientWidth; up.element.remove(outer); return width; }); scrollTopKey = function(viewport) { return up.fragment.toTarget(viewport); }; /*** 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.viewport.scrollTops() => { '.main': 0, '.sidebar': 73 } @function up.viewport.scrollTops @return Object<string, number> @internal */ scrollTops = function(options) { if (options == null) { options = {}; } return u.mapObject(getAll(options), function(viewport) { return [scrollTopKey(viewport), viewport.scrollTop]; }); }; /*** @function up.viewport.fixedElements @internal */ fixedElements = function(root) { var queryParts; if (root == null) { root = document; } queryParts = ['[up-fixed]'].concat(config.fixedTop).concat(config.fixedBottom); return root.querySelectorAll(queryParts.join(',')); }; /*** Saves the top scroll positions of all viewports in the current layer. The scroll positions will be associated with the current URL. They can later be restored by calling [`up.viewport.restoreScroll()`](/up.viewport.restoreScroll) at the same URL, or by following a link with an [`[scroll="restore"]`](/a-up-follow#up-restore-scroll) attribute. Unpoly automatically saves scroll positions before [navigating](/navigation). You will rarely need to call this function yourself. @function up.viewport.saveScroll @param {string} [options.location] The URL for which to save scroll positions. If omitted, the current browser location is used. @param {string} [options.layer] The layer for which to save scroll positions. If omitted, positions for the current layer will be saved. @param {Object<string, number>} [options.tops] An object mapping viewport selectors to vertical scroll positions in pixels. @experimental */ saveScroll = function() { var args, options, ref, ref1, tops, url, viewports; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; ref = parseOptions(args), viewports = ref[0], options = ref[1]; if (url = options.location || options.layer.location) { tops = (ref1 = options.tops) != null ? ref1 : getScrollTops(viewports); return options.layer.lastScrollTops.set(url, tops); } }; getScrollTops = function(viewports) { return u.mapObject(viewports, function(viewport) { return [scrollTopKey(viewport), viewport.scrollTop]; }); }; /*** Restores [previously saved](/up.viewport.saveScroll) scroll positions of viewports viewports configured in `up.viewport.config.viewportSelectors`. Unpoly automatically restores scroll positions when the user presses the back button. You can disable this behavior by setting [`up.history.config.restoreScroll = false`](/up.history.config). @function up.viewport.restoreScroll @param {Element} [viewport] @param {up.Layer|string} [options.layer] The layer on which to restore scroll positions. @return {Promise} A promise that will be fulfilled once scroll positions have been restored. @experimental */ restoreScroll = function() { var args, options, ref, scrollTopsForURL, url, viewports; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; ref = parseOptions(args), viewports = ref[0], options = ref[1]; url = options.layer.location; scrollTopsForURL = options.layer.lastScrollTops.get(url) || {}; up.puts('up.viewport.restoreScroll()', 'Restoring scroll positions for URL %s to %o', u.urlWithoutHost(url), scrollTopsForURL); return setScrollTops(viewports, scrollTopsForURL); }; parseOptions = function(args) { var options, viewports; options = u.copy(u.extractOptions(args)); options.layer = up.layer.get(options); if (args[0]) { viewports = [closest(args[0], options)]; } else if (options.around) { viewports = getAround(options.around, options); } else { viewports = getAll(options); } return [viewports, options]; }; resetScroll = function() { var args, options, ref, viewports; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; ref = parseOptions(args), viewports = ref[0], options = ref[1]; return setScrollTops(viewports, {}); }; setScrollTops = function(viewports, tops) { var allScrollPromises; allScrollPromises = u.map(viewports, function(viewport) { var key, scrollTop; key = scrollTopKey(viewport); scrollTop = tops[key] || 0; return scroll(viewport, scrollTop, { duration: 0 }); }); return Promise.all(allScrollPromises); }; /*** @internal */ absolutize = function(element, options) { var bounds, boundsRect, moveBounds, newElementRect, originalRect, viewport, viewportRect; if (options == null) { options = {}; } viewport = closest(element); viewportRect = viewport.getBoundingClientRect(); originalRect = element.getBoundingClientRect(); boundsRect = new up.Rect({ left: originalRect.left - viewportRect.left, top: originalRect.top - viewportRect.top, width: originalRect.width, height: originalRect.height }); if (typeof options.afterMeasure === "function") { options.afterMeasure(); } e.setStyle(element, { position: element.style.position === 'static' ? 'static' : 'relative', top: 'auto', right: 'auto', bottom: 'auto', left: 'auto', width: '100%', height: '100%' }); bounds = e.createFromSelector('up-bounds'); e.insertBefore(element, bounds); bounds.appendChild(element); moveBounds = function(diffX, diffY) { boundsRect.left += diffX; boundsRect.top += diffY; return e.setStyle(bounds, boundsRect); }; moveBounds(0, 0); newElementRect = element.getBoundingClientRect(); moveBounds(originalRect.left - newElementRect.left, originalRect.top - newElementRect.top); u.each(fixedElements(element), e.fixedToAbsolute); return { bounds: bounds, moveBounds: moveBounds }; }; /*** Marks this element as a scrolling container ("viewport"). Apply this attribute if your app uses a custom panel layout with fixed positioning instead of scrolling `<body>`. As an alternative you can also push a selector matching your custom viewport to the `up.viewport.config.viewportSelectors` array. [`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. \#\#\# Example Here is an example for a layout for an e-mail client, showing a list of e-mails on the left side and the e-mail text on the right side: .side { position: fixed; top: 0; bottom: 0; left: 0; width: 100px; overflow-y: scroll; } .main { position: fixed; top: 0; bottom: 0; left: 100px; right: 0; overflow-y: scroll; } This would be the HTML (notice the `up-viewport` attribute): <div class=".side" up-viewport> <a href="/emails/5001" up-target=".main">Re: Your invoice</a> <a href="/emails/2023" up-target=".main">Quote for services</a> <a href="/emails/9002" up-target=".main">Fwd: Room reservation</a> </div> <div class="main" up-viewport> <h1>Re: Your Invoice</h1> <p> Lorem ipsum dolor sit amet, consetetur sadipscing elitr. Stet clita kasd gubergren, no sea takimata sanctus est. </p> </div> @selector [up-viewport] @stable */ /*** Marks this element as being fixed to the top edge of the screen using `position: fixed`. When [following a fragment link](/a-up-follow), the viewport is scrolled so the targeted element becomes visible. By using this attribute you can make Unpoly aware of fixed elements that are obstructing the viewport contents. Unpoly will then scroll the viewport far enough that the revealed element is fully visible. Instead of using this attribute, you can also configure a selector in `up.viewport.config.fixedTop`. \#\#\# Example <div class="top-nav" up-fixed="top">...</div> @selector [up-fixed=top] @stable */ /*** Marks this element as being fixed to the bottom edge of the screen using `position: fixed`. When [following a fragment link](/a-up-follow), the viewport is scrolled so the targeted element becomes visible. By using this attribute you can make Unpoly aware of fixed elements that are obstructing the viewport contents. Unpoly will then scroll the viewport far enough that the revealed element is fully visible. Instead of using this attribute, you can also configure a selector in `up.viewport.config.fixedBottom`. \#\#\# Example <div class="bottom-nav" up-fixed="bottom">...</div> @selector [up-fixed=bottom] @stable */ /*** Marks this element as being anchored to the right edge of the screen, typically fixed navigation bars. Since [modal dialogs](/up.modal) hide the document scroll bar, elements anchored to the right appear to jump when the dialog opens or closes. Applying this attribute to anchored elements will make Unpoly aware of the issue and adjust the `right` property accordingly. You should give this attribute to layout elements with a CSS of `right: 0` with `position: fixed` or `position:absolute`. Instead of giving this attribute to any affected element, you can also configure a selector in `up.viewport.config.anchoredRight`. \#\#\# Example Here is the CSS for a navigation bar that is anchored to the top edge of the screen: .top-nav { position: fixed; top: 0; left: 0; right: 0; } By adding an `up-anchored="right"` attribute to the element, we can prevent the `right` edge from jumping when a [modal dialog](/up.modal) opens or closes: <div class="top-nav" up-anchored="right">...</div> @selector [up-anchored=right] @stable */ /*** @function up.viewport.firstHashTarget @internal */ firstHashTarget = function(hash, options) { var selector; if (options == null) { options = {}; } if (hash = pureHash(hash)) { selector = [e.attributeSelector('id', hash), 'a' + e.attributeSelector('name', hash)].join(','); return f.get(selector, options); } }; /*** Returns `'foo'` if the hash is `'#foo'`. @function pureHash @internal */ pureHash = function(value) { return value != null ? value.replace(/^#/, '') : void 0; }; userScrolled = false; up.on('scroll', { once: true }, function() { return userScrolled = true; }); up.on('up:app:boot', function() { return u.task(function() { if (!userScrolled) { return revealHash(); } }); }); up.on(window, 'hashchange', function() { return revealHash(); }); up.on('up:framework:reset', reset); return u.literal({ reveal: reveal, revealHash: revealHash, firstHashTarget: firstHashTarget, scroll: scroll, config: config, get: closest, subtree: getSubtree, around: getAround, all: getAll, rootSelector: rootSelector, get_root: getRoot, rootWidth: rootWidth, rootHeight: rootHeight, rootHasReducedWidthFromScrollbar: rootHasReducedWidthFromScrollbar, rootOverflowElement: rootOverflowElement, isRoot: isRoot, scrollbarWidth: scrollbarWidth, scrollTops: scrollTops, saveScroll: saveScroll, restoreScroll: restoreScroll, resetScroll: resetScroll, anchoredRight: anchoredRight, fixedElements: fixedElements, absolutize: absolutize, focus: doFocus, tryFocus: tryFocus, makeFocusable: makeFocusable }); })(); up.focus = up.viewport.focus; up.scroll = up.viewport.scroll; up.reveal = up.viewport.reveal; }).call(this); /*** Animation ========= When you [update a page fragment](/up.link) you can animate the change. You can add an attribute [`[up-transition]`](/a-up-transition) to your links or forms to smoothly fade out the old element while fading in the new element: ```html <a href="/users" up-target=".list" up-transition="cross-fade"> Show users </a> ``` \#\#\# Transitions vs. animations When we morph between an old and a new element, we call it a *transition*. In contrast, when we animate a new element without simultaneously removing an old element, we call it an *animation*. An example for an animation is opening a new overlay. We can animate the appearance of the dialog by adding an [`[up-animation]`](/a-up-animation) attribute to the opening link: ```html <a href="/users" up-target=".list" up-layer="new" up-animation="move-from-top"> Show users </a> ``` \#\#\# Which animations are available? Unpoly ships with a number of [predefined transitions](/up.morph#named-transitions) and [predefined animations](/up.animate#named-animations). You can define custom animations using `up.transition()` and `up.animation()`. @see a[up-transition] @see up.animation @see up.transition @module up.motion */ (function() { up.motion = (function() { var animate, animateNow, applyConfig, composeTransitionFn, config, e, findAnimationFn, findNamedAnimation, findTransitionFn, finish, isEnabled, isNone, morph, motionController, namedAnimations, namedTransitions, pickDefault, registerAnimation, registerMoveAnimations, registerOpacityAnimation, registerTransition, reset, skipAnimate, swapElementsDirectly, translateCSS, u, untranslatedBox, warnIfDisabled, willAnimate; u = up.util; e = up.element; namedAnimations = {}; namedTransitions = {}; motionController = new up.MotionController('motion'); /*** Sets default options for animations and transitions. @property up.motion.config @param {number} [config.duration=175] The default duration for all animations and transitions (in milliseconds). @param {string} [config.easing='ease'] The default timing function that controls the acceleration of animations and transitions. See [W3C documentation](http://www.w3.org/TR/css3-transitions/#transition-timing-function) for a list of pre-defined timing functions. @param {boolean} [config.enabled] Whether animation is enabled. By default animations are enabled, unless the user has configured their system to [minimize non-essential motion](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion). Set this to `false` to disable animation globally. This can be useful in full-stack integration tests. @stable */ config = new up.Config(function() { return { duration: 175, easing: 'ease', enabled: !matchMedia('(prefers-reduced-motion: reduce)').matches }; }); pickDefault = function(registry) { return u.pickBy(registry, function(value) { return value.isDefault; }); }; reset = function() { motionController.reset(); namedAnimations = pickDefault(namedAnimations); namedTransitions = pickDefault(namedTransitions); return config.reset(); }; /*** Returns whether Unpoly will perform animations. Set [`up.motion.config.enabled = false`](/up.motion.config#config.enabled) in order to disable animations globally. @function up.motion.isEnabled @return {boolean} @stable */ isEnabled = function() { return config.enabled; }; /*** Applies the given animation to the given element. \#\#\# Example ```js up.animate('.warning', 'fade-in') ``` You can pass additional options: ``` up.animate('.warning', 'fade-in', { delay: 1000, duration: 250, easing: 'linear' }) ``` \#\#\# Named animations The following animations are pre-defined: | `fade-in` | Changes the element's opacity from 0% to 100% | | `fade-out` | Changes the element's opacity from 100% to 0% | | `move-to-top` | Moves the element upwards until it exits the screen at the top edge | | `move-from-top` | Moves the element downwards from beyond the top edge of the screen until it reaches its current position | | `move-to-bottom` | Moves the element downwards until it exits the screen at the bottom edge | | `move-from-bottom` | Moves the element upwards from beyond the bottom edge of the screen until it reaches its current position | | `move-to-left` | Moves the element leftwards until it exists the screen at the left edge | | `move-from-left` | Moves the element rightwards from beyond the left edge of the screen until it reaches its current position | | `move-to-right` | Moves the element rightwards until it exists the screen at the right edge | | `move-from-right` | Moves the element leftwards from beyond the right edge of the screen until it reaches its current position | | `none` | An animation that has no visible effect. Sounds useless at first, but can save you a lot of `if` statements. | You can define additional named animations using [`up.animation()`](/up.animation). \#\#\# Animating CSS properties directly By passing an object instead of an animation name, you can animate the CSS properties of the given element: ``` var warning = document.querySelector('.warning') warning.style.opacity = 0 up.animate(warning, { opacity: 1 }) ``` CSS properties must be given in `kebab-case`, not `camelCase`. \#\#\# Multiple animations on the same element Unpoly doesn't allow more than one concurrent animation on the same element. If you attempt to animate an element that is already being animated, the previous animation will instantly jump to its last frame before the new animation begins. @function up.animate @param {Element|jQuery|string} element The element to animate. @param {string|Function(element, options): Promise|Object} animation Can either be: - The animation's name - A function performing the animation - An object of CSS attributes describing the last frame of the animation (using kebeb-case property names) @param {number} [options.duration=300] The duration of the animation, 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 {Promise} A promise for the animation's end. @stable */ animate = function(element, animation, options) { var animationFn, runNow, willRun; element = up.fragment.get(element); options = u.options(options); animationFn = findAnimationFn(animation); willRun = willAnimate(element, animation, options); if (willRun) { runNow = function() { return animationFn(element, options); }; return motionController.startFunction(element, runNow, options); } else { return skipAnimate(element, animation); } }; willAnimate = function(element, animationOrTransition, options) { applyConfig(options); return isEnabled() && !isNone(animationOrTransition) && options.duration > 0 && !e.isSingleton(element); }; skipAnimate = function(element, animation) { if (u.isOptions(animation)) { e.setStyle(element, animation); } return Promise.resolve(); }; /*** Animates the given element's CSS properties using CSS transitions. Does not track the animation, nor does it finishes existing animations (use `up.motion.animate()` for that). It does, however, listen to the motionController's finish event. @function animateNow @param {Element|jQuery|string} element 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 {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 {Promise} A promise that fulfills when the animation ends. @internal */ animateNow = function(element, lastFrame, options) { var cssTransition; options = u.merge(options, { finishEvent: motionController.finishEvent }); cssTransition = new up.CSSTransition(element, lastFrame, options); return cssTransition.start(); }; applyConfig = function(options) { options.easing || (options.easing = config.easing); return options.duration || (options.duration = config.duration); }; findNamedAnimation = function(name) { return namedAnimations[name] || up.fail("Unknown animation %o", name); }; /*** Completes [animations](/up.animate) and [transitions](/up.morph). If called without arguments, all animations on the screen are completed. If given an element (or selector), animations on that element and its children are completed. Animations are completed by jumping to the last animation frame instantly. Promises returned by animation and transition functions instantly settle. Emits the `up:motion:finish` event that is handled by `up.animate()`. Does nothing if there are no animation to complete. @function up.motion.finish @param {Element|jQuery|string} [element] The element around which to finish all animations. @return {Promise} A promise that fulfills when animations and transitions have finished. @stable */ finish = function(element) { return motionController.finish(element); }; /*** This event is emitted on an [animating](/up.animating) element by `up.motion.finish()` to request the animation to instantly finish and skip to the last frame. Promises returned by now-finished animation functions are expected to settle. Animations started by `up.animate()` already handle this event. @event up:motion:finish @param {Element} event.target The animating element. @stable */ /*** Performs an animated transition between the `source` and `target` elements. Transitions are implement by performing two animations in parallel, causing `source` to disappear and the `target` to appear. - `target` is [inserted before](https://developer.mozilla.org/en-US/docs/Web/API/Node/insertBefore) `source` - `source` is removed from the [document flow](https://developer.mozilla.org/en-US/docs/Learn/CSS/CSS_layout/Positioning) with `position: absolute`. It will be positioned over its original place in the flow that is now occupied by `target`. - Both `source` and `target` are animated in parallel - `source` is removed from the DOM \#\#\# Named transitions The following transitions are pre-defined: | `cross-fade` | Fades out the first element. Simultaneously fades in the second element. | | `move-up` | Moves the first element upwards until it exits the screen at the top edge. Simultaneously moves the second element upwards from beyond the bottom edge of the screen until it reaches its current position. | | `move-down` | Moves the first element downwards until it exits the screen at the bottom edge. Simultaneously moves the second element downwards from beyond the top edge of the screen until it reaches its current position. | | `move-left` | Moves the first element leftwards until it exists the screen at the left edge. Simultaneously moves the second element leftwards from beyond the right edge of the screen until it reaches its current position. | | `move-right` | Moves the first element rightwards until it exists the screen at the right edge. Simultaneously moves the second element rightwards from beyond the left edge of the screen until it reaches its current position. | | `none` | A transition that has no visible effect. Sounds useless at first, but can save you a lot of `if` statements. | You can define additional named transitions using [`up.transition()`](/up.transition). You can also compose a transition from two [named animations](/named-animations). separated by a slash character (`/`): - `move-to-bottom/fade-in` - `move-to-left/move-from-top` \#\#\# Implementation details During a transition both the old and new element occupy the same position on the screen. Since the CSS layout flow will usually not allow two elements to overlay the same space, Unpoly: - The old and new elements are cloned - The old element is removed from the layout flow using `display: hidden` - The new element is hidden, but still leaves space in the layout flow by setting `visibility: hidden` - The clones are [absolutely positioned](https://developer.mozilla.org/en-US/docs/Web/CSS/position#Absolute_positioning) over the original elements. - The transition is applied to the cloned elements. At no point will the hidden, original elements be animated. - When the transition has finished, the clones are removed from the DOM and the new element is shown. The old element remains hidden in the DOM. @function up.morph @param {Element|jQuery|string} source @param {Element|jQuery|string} target @param {Function(oldElement, newElement)|string} transition @param {number} [options.duration=300] The duration of the animation, in milliseconds. @param {string} [options.easing='ease'] The timing function that controls the transition's acceleration. See [W3C documentation](http://www.w3.org/TR/css3-transitions/#transition-timing-function) for a list of pre-defined timing functions. @param {boolean} [options.reveal=false] Whether to reveal the new element by scrolling its parent viewport. @return {Promise} A promise that fulfills when the transition ends. @stable */ morph = function(oldElement, newElement, transitionObject, options) { var afterDetach, afterInsert, beforeDetach, beforeStart, oldRemote, promise, scrollNew, scrollTopBeforeReveal, trackable, transitionFn, viewport, willMorph; options = u.options(options); applyConfig(options); oldElement = up.fragment.get(oldElement); newElement = up.fragment.get(newElement); transitionFn = findTransitionFn(transitionObject); willMorph = willAnimate(oldElement, transitionFn, options); beforeStart = u.pluckKey(options, 'beforeStart') || u.noop; afterInsert = u.pluckKey(options, 'afterInsert') || u.noop; beforeDetach = u.pluckKey(options, 'beforeDetach') || u.noop; afterDetach = u.pluckKey(options, 'afterDetach') || u.noop; scrollNew = u.pluckKey(options, 'scrollNew') || u.asyncNoop; beforeStart(); if (willMorph) { if (motionController.isActive(oldElement) && options.trackMotion === false) { return transitionFn(oldElement, newElement, options); } up.puts('up.morph()', 'Morphing %o to %o with transition %O', oldElement, newElement, transitionObject); viewport = up.viewport.get(oldElement); scrollTopBeforeReveal = viewport.scrollTop; oldRemote = up.viewport.absolutize(oldElement, { afterMeasure: function() { e.insertBefore(oldElement, newElement); return afterInsert(); } }); trackable = function() { var promise; promise = Promise.resolve(scrollNew()); promise = promise.then(function() { var scrollTopAfterReveal; scrollTopAfterReveal = viewport.scrollTop; oldRemote.moveBounds(0, scrollTopAfterReveal - scrollTopBeforeReveal); return transitionFn(oldElement, newElement, options); }); promise = promise.then(function() { beforeDetach(); e.remove(oldRemote.bounds); return afterDetach(); }); return promise; }; return motionController.startFunction([oldElement, newElement], trackable, options); } else { beforeDetach(); swapElementsDirectly(oldElement, newElement); afterInsert(); afterDetach(); promise = scrollNew(); return promise; } }; findTransitionFn = function(object) { var namedTransition; if (isNone(object)) { return void 0; } else if (u.isFunction(object)) { return object; } else if (u.isArray(object)) { return composeTransitionFn.apply(null, object); } else if (u.isString(object)) { if (object.indexOf('/') >= 0) { return composeTransitionFn.apply(null, object.split('/')); } else if (namedTransition = namedTransitions[object]) { return findTransitionFn(namedTransition); } } else { return up.fail("Unknown transition %o", object); } }; composeTransitionFn = function(oldAnimation, newAnimation) { var newAnimationFn, oldAnimationFn; if (isNone(oldAnimation) && isNone(oldAnimation)) { return void 0; } else { oldAnimationFn = findAnimationFn(oldAnimation) || u.asyncNoop; newAnimationFn = findAnimationFn(newAnimation) || u.asyncNoop; return function(oldElement, newElement, options) { return Promise.all([oldAnimationFn(oldElement, options), newAnimationFn(newElement, options)]); }; } }; findAnimationFn = function(object) { if (isNone(object)) { return void 0; } else if (u.isFunction(object)) { return object; } else if (u.isString(object)) { return findNamedAnimation(object); } else if (u.isOptions(object)) { return function(element, options) { return animateNow(element, object, options); }; } else { return up.fail('Unknown animation %o', object); } }; swapElementsDirectly = up.mockable(function(oldElement, newElement) { return e.replace(oldElement, newElement); }); /*** Defines a named transition that [morphs](/up.morph) from one element to another. \#\#\# Example Here is the definition of the pre-defined `cross-fade` animation: ```js up.transition('cross-fade', (oldElement, newElement, options) -> Promise.all([ up.animate(oldElement, 'fade-out', options), up.animate(newElement, 'fade-in', options) ]) ) ``` It is recommended that your transitions use [`up.animate()`](/up.animate), passing along the `options` that were passed to you. If you choose to *not* use `up.animate()` and roll your own logic instead, your code must honor the following contract: 1. It must honor the options `{ duration, easing }` if given. 2. It must *not* remove any of the given elements from the DOM. 3. It returns a promise that is fulfilled when the transition has ended. 4. If during the animation an event `up:motion:finish` is emitted on either element, the transition instantly jumps to the last frame and resolves the returned promise. Calling [`up.animate()`](/up.animate) with an object argument will take care of all these points. @function up.transition @param {string} name @param {Function(oldElement, newElement, options): Promise|Array} transition @stable */ registerTransition = function(name, transition) { var fn; fn = findTransitionFn(transition); fn.isDefault = up.framework.booting; return namedTransitions[name] = fn; }; /*** Defines a named animation. Here is the definition of the pre-defined `fade-in` animation: ```js up.animation('fade-in', function(element, options) { element.style.opacity = 0 up.animate(element, { opacity: 1 }, options) }) ``` It is recommended that your definitions always end by calling calling [`up.animate()`](/up.animate) with an object argument, passing along the `options` that were passed to you. If you choose to *not* use `up.animate()` and roll your own animation code instead, your code must honor the following contract: 1. It must honor the options `{ duration, easing }`, if given. 2. It must *not* remove any of the given elements from the DOM. 3. It returns a promise that is fulfilled when the transition has ended 4. If during the animation an event `up:motion:finish` is emitted on the given element, the transition instantly jumps to the last frame and resolves the returned promise. Calling [`up.animate()`](/up.animate) with an object argument will take care of all these points. @function up.animation @param {string} name @param {Function(element, options): Promise} animation @stable */ registerAnimation = function(name, animation) { var fn; fn = findAnimationFn(animation); fn.isDefault = up.framework.booting; return namedAnimations[name] = fn; }; warnIfDisabled = function() { if (!isEnabled()) { return up.puts('up.motion', 'Animations are disabled'); } }; /*** Returns whether the given animation option will cause the animation to be skipped. @function up.motion.isNone @internal */ isNone = function(animationOrTransition) { return !animationOrTransition || animationOrTransition === 'none'; }; registerOpacityAnimation = function(name, from, to) { return registerAnimation(name, function(element, options) { element.style.opacity = 0; e.setStyle(element, { opacity: from }); return animateNow(element, { opacity: to }, options); }); }; registerOpacityAnimation('fade-in', 0, 1); registerOpacityAnimation('fade-out', 1, 0); translateCSS = function(dx, dy) { return { transform: "translate(" + dx + "px, " + dy + "px)" }; }; untranslatedBox = function(element) { e.setStyle(element, translateCSS(0, 0)); return element.getBoundingClientRect(); }; registerMoveAnimations = function(direction, boxToTransform) { var animationFromName, animationToName; animationToName = "move-to-" + direction; animationFromName = "move-from-" + direction; registerAnimation(animationToName, function(element, options) { var box, transform; box = untranslatedBox(element); transform = boxToTransform(box); return animateNow(element, transform, options); }); return registerAnimation(animationFromName, function(element, options) { var box, transform; box = untranslatedBox(element); transform = boxToTransform(box); e.setStyle(element, transform); return animateNow(element, translateCSS(0, 0), options); }); }; registerMoveAnimations('top', function(box) { var travelDistance; travelDistance = box.top + box.height; return translateCSS(0, -travelDistance); }); registerMoveAnimations('bottom', function(box) { var travelDistance; travelDistance = up.viewport.rootHeight() - box.top; return translateCSS(0, travelDistance); }); registerMoveAnimations('left', function(box) { var travelDistance; travelDistance = box.left + box.width; return translateCSS(-travelDistance, 0); }); registerMoveAnimations('right', function(box) { var travelDistance; travelDistance = up.viewport.rootWidth() - box.left; return translateCSS(travelDistance, 0); }); registerTransition('cross-fade', ['fade-out', 'fade-in']); registerTransition('move-left', ['move-to-left', 'move-from-right']); registerTransition('move-right', ['move-to-right', 'move-from-left']); registerTransition('move-up', ['move-to-top', 'move-from-bottom']); registerTransition('move-down', ['move-to-bottom', 'move-from-top']); /*** [Follows](/a-up-follow) this link and swaps in the new fragment with an animated transition. Note that transitions are not possible when replacing the `body` element. \#\#\# Example ```html <a href="/page2" up-target=".story" up-transition="move-left"> Next page </a> ``` @selector a[up-transition] @params-note All attributes for `a[up-follow]` may also be used. @param [up-transition] The name of a [predefined transition](/up.morph#named-transitions). @param [up-fail-transition] The transition to use when the server responds with an error code. @see server-errors @stable */ /*** [Submits](/form-up-submit) this form and swaps in the new fragment with an animated transition. \#\#\# Example ```html <form action="/tasks" up-target=".content" up-transition="cross-fade"> ... </form> ``` @selector form[up-transition] @params-note All attributes for `form[up-submit]` may also be used. @param [up-transition] The name of a [predefined transition](/up.morph#named-transitions). @param [up-fail-transition] The transition to use when the server responds with an error code. @see server-errors @stable */ up.on('up:framework:boot', warnIfDisabled); up.on('up:framework:reset', reset); return { morph: morph, animate: animate, finish: finish, finishCount: function() { return motionController.finishCount; }, transition: registerTransition, animation: registerAnimation, config: config, isEnabled: isEnabled, isNone: isNone, willAnimate: willAnimate, swapElementsDirectly: swapElementsDirectly }; })(); up.transition = up.motion.transition; up.animation = up.motion.animation; up.morph = up.motion.morph; up.animate = up.motion.animate; }).call(this); (function() { var u, slice = [].slice; u = up.util; /*** Network requests ================ Unpoly ships with an optimized HTTP client for fast and effective communication with your server-side app. While you can use the browser's native `fetch()` function, Unpoly's `up.request()` has a number of convenience features: - Requests may be [cached](/up.request#options.cache) to reuse responses and enable [preloading](/a-up-preload). - Requests send [additional HTTP headers](/up.protocol) that the server may use to optimize its response. For example, when updating a [fragment](/up.fragment), the fragment's selector is automatically sent as an `X-Up-Target` header. The server may choose to only render the targeted fragment. - Useful events like `up:request:loaded` or `up:request:late` are emitted throughout the request/response lifecycle. - When too many requests are sent concurrently, excessive requests are [queued](/up.network.config#config.concurrency). This prevents exhausting the user's bandwidth and limits race conditions in end-to-end tests. - A very concise API requiring zero boilerplate code. @see up.request @see up.Response @see up:request:late @module up.network */ up.network = (function() { /*** TODO: Docs for up.network.config.clearCache @property up.network.config @param {number} [config.concurrency=4] The maximum number of concurrently loading requests. Additional requests are queued. [Preload](/a-up-preload) requests are always queued behind non-preload requests. You might find it useful to set the request concurrency `1` in end-to-end tests to prevent race conditions. Note that your browser might [impose its own request limit](http://www.browserscope.org/?category=network) regardless of what you configure here. @param {boolean} [config.wrapMethod] Whether to wrap non-standard HTTP methods in a POST request. If this is set, methods other than GET and POST will be converted to a `POST` request and carry their original method as a `_method` parameter. This is to [prevent unexpected redirect behavior](https://makandracards.com/makandra/38347). If you disable method wrapping, make sure that your server always redirects with with a 303 status code (rather than 302). @param {number} [config.cacheSize=70] The maximum number of responses to cache. If the size is exceeded, the oldest responses will be dropped from the cache. @param {number} [config.cacheExpiry=300000] The number of milliseconds until a cached response expires. Defaults to 5 minutes. @param {number} [config.badDownlink=0.6] The connection's minimum effective bandwidth estimate required to prevent Unpoly from [reducing requests](/up.network.shouldReduceRequests). The value is given in megabits per second. Higher is better. @param {number} [config.badRTT=0.6] The connection's maximum effective round-trip time required to prevent Unpoly from [reducing requests](/up.network.shouldReduceRequests). The value is given in milliseconds. Lower is better. @param {number} [config.badResponseTime=400] How long the proxy waits until emitting the [`up:request:late` event](/up:request:late). This metric is *not* considered for the decision to [reduce requests](/up.network.shouldReduceRequests). The value is given in milliseconds. @param {Function(up.Request): boolean} config.autoCache Whether to cache the given request with `{ cache: 'auto' }`. By default Unpoly will auto-cache requests with safe HTTP methods. @param {Function(up.Request, up.Response)} config.clearCache Whether to [clear the cache](/up.cache.clear) after the given request and response. By default Unpoly will clear the entire cache after a request with an unsafe HTTP method. @param {Array<string>|Function(up.Request): Array<string>} [config.requestMetaKeys] An array of request property names that are sent to the server as [HTTP headers](/up.protocol). The server may return an optimized response based on these properties, e.g. by omitting a navigation bar that is not targeted. \#\#\# Cacheability considerations Two requests with different `requestMetaKeys` are considered cache misses when [caching](/up.request) and [preloading](a-up-preload). To **improve cacheability**, you may set `up.network.config.requestMetaKeys` to a shorter list of property keys. \#\#\# Available fields The default configuration is `['target', 'failTarget', 'mode', 'failMode', 'context', 'failContext']`. This means the following properties are sent to the server: | Request property | Request header | |--------------------------|---------------------| | `up.Request#target` | `X-Up-Target` | | `up.Request#failTarget` | `X-Up-Fail-Target` | | `up.Request#context` | `X-Up-Context` | | `up.Request#failContext` | `X-Up-Fail-Context` | | `up.Request#mode` | `X-Up-Mode` | | `up.Request#failMode` | `X-Up-Fail-Mode` | \#\#\# Per-route configuration You may also configure a function that accepts an [`up.Request`](/up.Request) and returns an array of request property names that are sent to the server. With this you may send different request properties for different URLs: ```javascript up.network.config.requestMetaKeys = function(request) { if (request.url == '/search') { // The server optimizes responses on the /search route. return ['target', 'failTarget'] } else { // The server doesn't optimize any other route, // so configure maximum cacheability. return [] } } @stable */ var abortRequests, cache, config, handleCaching, isBusy, isIdle, isSafeMethod, makeRequest, mimicLocalRequest, parseRequestOptions, queue, queueRequest, registerAliasForRedirect, reset, shouldReduceRequests, useCachedRequest; config = new up.Config(function() { return { concurrency: 4, wrapMethod: true, cacheSize: 70, cacheExpiry: 1000 * 60 * 5, badDownlink: 0.6, badRTT: 750, badResponseTime: 400, autoCache: function(request) { return request.isSafe(); }, clearCache: function(request, _response) { return !request.isSafe(); }, requestMetaKeys: ['target', 'failTarget', 'mode', 'failMode', 'context', 'failContext'] }; }); queue = new up.Request.Queue(); cache = new up.Request.Cache(); /*** Returns an earlier request [matching](/up.network.config#config.requestMetaKeys) the given request options. Returns `undefined` if the given request is not currently cached. Note that `up.request()` will only write to the cache with `{ cache: true }`. \#\#\# Example ``` let request = up.cache.get({ url: '/foo' }) if (request) { let response = await request console.log("Response is %o", response) } else { console.log("The path /foo has not been requested before!") } ``` @function up.cache.get @param {Object} requestOptions The request options to match against the cache. See `options` for `up.request()` for documentation. The user may configure `up.network.config.requestMetaKeys` to define which request options are relevant for cache matching. @return {up.Request|undefined} The cached request. @experimental */ /*** Removes all [cache](/up.cache.get) entries. To only remove some cache entries, pass a [URL pattern](/url-patterns): ```js up.cache.clear('/users/*') ``` \#\#\# Other reasons the cache may clear By default Unpoly automatically clears the entire cache whenever it processes a request with an non-GET HTTP method. To customize this rule, use `up.network.config.clearCache`. The server may also clear the cache by sending an [`X-Up-Cache: clear`](/X-Up-Cache) header. @function up.cache.clear @param {string} [pattern] A [URL pattern](/url-patterns) matching cache entries that should be cleared. If omitted, the entire cache is cleared. @stable */ /*** Makes the cache assume that `newRequest` has the same response as the already cached `oldRequest`. Unpoly uses this internally when the user redirects from `/old` to `/new`. In that case, both `/old` and `/new` will cache the same response from `/new`. @function up.cache.alias @param {Object} oldRequest The earlier [request options](/up.request). @param {Object} newRequest The new [request options](/up.request). @experimental */ /*** Manually stores a request in the cache. Future calls to `up.request()` will try to re-use this request before making a new request. @function up.cache.set @param {string} request.url @param {string} [request.method='GET'] @param {string} [request.target='body'] @param {up.Request} request The request to cache. The cache is also a promise for the response. @internal */ /*** Manually removes the given request from the cache. You can also [configure](/up.network.config) when cache entries expire automatically. @function up.cache.remove @param {Object} requestOptions The request options for which to remove cached requests. See `options` for `up.request()` for documentation. @experimental */ reset = function() { abortRequests(); queue.reset(); config.reset(); return cache.clear(); }; /*** Makes an AJAX request to the given URL. Returns an `up.Request` object which contains information about the request. This request object is also a promise for an `up.Response` that contains the response text, headers, etc. \#\#\# Example ```js let request = up.request('/search', { params: { query: 'sunshine' } }) console.log('We made a request to', request.url) let response = await request console.log('The response text is', response.text) ``` \#\#\# Error handling The returned promise will fulfill with an `up.Response` when the server responds with an HTTP status of 2xx (like `200`). When the server responds with an HTTP error code (like `422` or `500`), the promise will *reject* with `up.Response`. When the request fails from a fatal error (like a timeout or loss of connectivity), the promise will reject with an [`Error`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) object. Here is an example for a complete control flow that handles both HTTP error codes and fatal errors: ```js try { let response = await up.request('/search', { params: { query: 'sunshine' } }) console.log('Successful response with text:', response.text) } catch (e) { if (e instanceof up.Response) { console.log('Server responded with HTTP status %s and text %s', e.status, e.text) } else { console.log('Fatal error during request:', e.message) } } ``` \#\#\# Caching You may cache responses by passing a `{ cache }` option. Responses for a cached request will resolve instantly. By default the cache cleared after making a request with an unsafe HTTP method. You can configure caching with the [`up.network.config`](/up.network.config) property. @function up.request @param {string} [url] The URL for the request. Instead of passing the URL as a string argument, you can also pass it as an `{ url }` option. @param {string} [options.url] The URL for the request. @param {string} [options.method='GET'] The HTTP method for the request. @param {Object|up.Params|string|Array} [options.params={}] [Parameters](/up.Params) that should be sent as the request's [query string](https://en.wikipedia.org/wiki/Query_string) or payload. When making a `GET` request to a URL with a query string, the given `{ params }` will be added to the query parameters. @param {boolean} [options.cache=false] Whether to read from and write to the [cache](/up.cache). With `{ cache: true }` Unpoly will try to re-use a cached response before connecting to the network. If no cached response exists, Unpoly will make a request and cache the server response. With `{ cache: 'auto' }` Unpoly will use the cache only if `up.network.config.autoCache` returns `true` for this request. With `{ cache: false }` (the default) Unpoly will always make a network request. @param {boolean|string} [options.clearCache] Whether to [clear](/up.cache.clear) the [cache](/up.cache.get) after this request. You may also pass a [URL pattern](/url-patterns) to only clear matching requests. Defaults to the result of `up.network.config.clearCache`, which defaults to clearing the entire cache after a non-GET request. @param {Object} [options.headers={}] An object of additional HTTP headers. Note that Unpoly will by default send a number of custom request headers. See `up.protocol` and `up.network.config.metaKeys` for details. @param {boolean} [options.wrapMethod] Whether to wrap non-standard HTTP methods in a POST request. If this is set, methods other than GET and POST will be converted to a `POST` request and carry their original method as a `_method` parameter. This is to [prevent unexpected redirect behavior](https://makandracards.com/makandra/38347). Defaults to [`up.network.config`](/up.network.config#config.wrapMethod). @param {string} [options.timeout] A timeout in milliseconds. If `up.network.config.maxRequests` is set, the timeout will not include the time spent waiting in the queue. @param {string} [options.target='body'] The CSS selector that will be sent as an `X-Up-Target` header. @param {string} [options.failTarget='body'] The CSS selector that will be sent as an `X-Up-Fail-Target` header. @param {string} [options.layer='current'] The [layer](/up.layer) this request is associated with. @param {string} [options.failLayer='current'] The [layer](/up.layer) this request is associated with if the server [sends a HTTP status code](/server-errors). @param {Element} [options.origin] The DOM element that caused this request to be sent, e.g. a hyperlink or form element. @param {Element} [options.contentType] The format in which to encode the request params. Allowed values are `application/x-www-form-urlencoded` and `multipart/form-data`. Only `multipart/form-data` can transport binary data. If this option is omitted Unpoly will prefer `application/x-www-form-urlencoded`, unless request params contains binary data. @param {string} [options.payload] A custom payload for this request. By default Unpoly will build a payload from the given `{ params }` option. Therefore this option is not required when making a standard link or form request to a server that renders HTML. A use case for this option is talking to a JSON API that expects requests with a `application/json` payload. If a `{ payload }` option is given you must also pass a `{ contentType }`. @return {up.Request} An object with information about the request. The request object is also a promise for its `up.Response`. @stable */ makeRequest = function() { var args, request, solo; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; request = new up.Request(parseRequestOptions(args)); useCachedRequest(request) || queueRequest(request); if (solo = request.solo) { queue.abortExcept(request, solo); } return request; }; mimicLocalRequest = function(options) { var clearCache, solo; if (solo = options.solo) { abortRequests(solo); } if (clearCache = options.clearCache) { return cache.clear(clearCache); } }; parseRequestOptions = function(args) { var base, options; options = u.extractOptions(args); options.url || (options.url = args[0]); if (typeof (base = up.migrate).handleRequestOptions === "function") { base.handleRequestOptions(options); } return options; }; useCachedRequest = function(request) { var cachedRequest; if (request.willCache() && (cachedRequest = cache.get(request))) { up.puts('up.request()', 'Re-using previous request to %s %s', request.method, request.url); if (!request.preload) { queue.promoteToForeground(cachedRequest); } request.followState(cachedRequest); return true; } }; queueRequest = function(request) { if (request.preload && !request.isSafe()) { up.fail('Will not preload request to %s', request.description); } handleCaching(request); return queue.asap(request); }; handleCaching = function(request) { if (request.willCache()) { cache.set(request, request); } return u.always(request, function(response) { var clearCache, ref, ref1; if (clearCache = (ref = (ref1 = response.clearCache) != null ? ref1 : request.clearCache) != null ? ref : config.clearCache(request, response)) { cache.clear(clearCache); } if (request.willCache() || cache.get(request)) { cache.set(request, request); } if (!response.ok) { return cache.remove(request); } }); }; /*** Returns whether Unpoly is not currently waiting for a [request](/up.request) to finish. @function up.network.isIdle @return {boolean} @stable */ isIdle = function() { return !isBusy(); }; /*** Returns whether Unpoly is currently waiting for a [request](/up.request) to finish. @function up.network.isBusy @return {boolean} @stable */ isBusy = function() { return queue.isBusy(); }; /*** Returns whether optional requests should be avoided where possible. We assume the user wants to avoid requests if either of following applies: - The user has enabled data saving in their browser ("Lite Mode" in Chrome for Android). - The connection's effective round-trip time is longer than `up.network.config.badRTT`. - The connection's effective bandwidth estimate is less than `up.network.config.badDownlink`. By default Unpoly will disable [preloading](/up-preload) and [polling](/up-poll) if requests should be avoided. @function up.network.shouldReduceRequests @return {boolean} Whether requests should be avoided where possible. @experimental */ shouldReduceRequests = function() { var netInfo; if (netInfo = navigator.connection) { return netInfo.saveData || (netInfo.rtt && netInfo.rtt > config.badRTT) || (netInfo.downlink && netInfo.downlink < config.badDownlink); } }; /*** Aborts pending [requests](/up.request). The event `up:request:aborted` will be emitted. The promise returned by `up.request()` will be rejected with an exception named `AbortError`: try { let response = await up.request('/path') console.log(response.text) } catch (err) { if (err.name == 'AbortError') { console.log('Request was aborted') } } \#\#\# Examples Without arguments, this will abort all pending requests: ```js up.network.abort() ``` To abort a given `up.Request` object, pass it as the first argument: ```js let request = up.request('/path') up.network.abort(request) ``` To abort all requests matching a condition, pass a function that takes a request and returns a boolean value. Unpoly will abort all request for which the given function returns `true`. E.g. to abort all requests with a HTTP method as `GET`: ```js up.network.abort((request) => request.method == 'GET') ``` @function up.network.abort @param {up.Request|boolean|Function(up.Request): boolean} [matcher=true] If this argument is omitted, all pending requests are aborted. @stable */ abortRequests = function() { var args; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; return queue.abort.apply(queue, args); }; /*** This event is [emitted](/up.emit) when an [AJAX request](/up.request) was [aborted](/up.network.abort()). The event is emitted on the layer that caused the request. @event up:request:aborted @param {up.Request} event.request The aborted request. @param {up.Layer} [event.layer] The [layer](/up.layer) that caused the request. @param event.preventDefault() @experimental */ /*** This event is [emitted](/up.emit) when [AJAX requests](/up.request) are taking long to finish. By default Unpoly will wait 800 ms for an AJAX request to finish before emitting `up:request:late`. You can configure this time like this: up.network.config.badResponseTime = 400; Once all responses have been received, an [`up:request:recover`](/up:request:recover) will be emitted. Note that if additional requests are made while Unpoly is already busy waiting, **no** additional `up:request:late` events will be triggered. \#\#\# Spinners You can [listen](/up.on) to the `up:request:late` and [`up:request:recover`](/up:request:recover) events to implement a spinner that appears during a long-running request, and disappears once the response has been received: <div class="spinner">Please wait!</div> Here is the JavaScript to make it alive: up.compiler('.spinner', function(element) { show = () => { up.element.show(element) } hide = () => { up.element.hide(element) } hide() return [ up.on('up:request:late', show), up.on('up:request:recover', hide) ] }) The `up:request:late` event will be emitted after a delay to prevent the spinner from flickering on and off. You can change (or remove) this delay like this: up.network.config.badResponseTime = 400; @event up:request:late @stable */ /*** This event is [emitted](/up.emit) when [AJAX requests](/up.request) have [taken long to finish](/up:request:late), but have finished now. See [`up:request:late`](/up:request:late) for more documentation on how to use this event for implementing a spinner that shows during long-running requests. @event up:request:recover @stable */ /*** This event is [emitted](/up.emit) before an [AJAX request](/up.request) is sent over the network. The event is emitted on the layer that caused the request. @event up:request:load @param {up.Request} event.request The request to be sent. @param {up.Layer} [event.layer] The [layer](/up.layer) that caused the request. @param event.preventDefault() Event listeners may call this method to prevent the request from being sent. @stable */ registerAliasForRedirect = function(request, response) { var newRequest; if (request.cache && response.url && request.url !== response.url) { newRequest = request.variant({ method: response.method, url: response.url }); return cache.alias(request, newRequest); } }; /*** This event is [emitted](/up.emit) when the response to an [AJAX request](/up.request) has been received. Note that this event will also be emitted when the server signals an error with an HTTP status like `500`. Only if the request encounters a fatal error (like a loss of network connectivity), [`up:request:fatal`](/up:request:fatal) is emitted instead. The event is emitted on the layer that caused the request. @event up:request:loaded @param {up.Request} event.request The request. @param {up.Response} event.response The response that was received from the server. @param {up.Layer} [event.layer] The [layer](/up.layer) that caused the request. @stable */ /*** This event is [emitted](/up.emit) when an [AJAX request](/up.request) encounters fatal error like a timeout or loss of network connectivity. Note that this event will *not* be emitted when the server produces an error message with an HTTP status like `500`. When the server can produce any response, [`up:request:loaded`](/up:request:loaded) is emitted instead. The event is emitted on the layer that caused the request. @event up:request:fatal @param {up.Request} event.request The request. @param {up.Layer} [event.layer] The [layer](/up.layer) that caused the request. @stable */ /*** @internal */ isSafeMethod = function(method) { return u.contains(['GET', 'OPTIONS', 'HEAD'], u.normalizeMethod(method)); }; up.on('up:framework:reset', reset); return { request: makeRequest, cache: cache, isIdle: isIdle, isBusy: isBusy, isSafeMethod: isSafeMethod, config: config, abort: abortRequests, registerAliasForRedirect: registerAliasForRedirect, queue: queue, shouldReduceRequests: shouldReduceRequests, mimicLocalRequest: mimicLocalRequest }; })(); up.request = up.network.request; up.cache = up.network.cache; }).call(this); (function() { var e, u, slice = [].slice; u = up.util; e = up.element; /*** Layers ====== Unpoly allows you to [open page fragments in an overlay](/opening-overlays). Overlays may be stacked infinitely. A variety of [overlay modes](/layer-terminology) are supported, such as modal dialogs, popup overlays or drawers. You may [customize their appearance and behavior](/customizing-overlays). Layers are isolated, meaning a screen in one layer will not accidentally see elements or events from another layer. For instance, [fragment links](/up.link) will only update elements from the [current layer](/up.layer.current) unless you [explicitly target another layer](/layer-option). Overlays allow you to break up a complex screen into [subinteractions](/subinteractions). Subinteractions take place in overlays and may span one or many pages. The original screen remains open in the background. Once the subinteraction is *done*, the overlay is closed and a result value is communicated back to the parent layer. @see a[up-layer=new] @see up.layer.current @see up.layer.on @see up.layer.ask @module up.layer */ up.layer = (function() { var LAYER_CLASSES, OVERLAY_CLASSES, anySelector, api, ask, build, closeCallbackAttr, config, handlers, mainTargets, modeConfigs, normalizeOptions, open, openCallbackAttr, optionToString, reset, stack; OVERLAY_CLASSES = [up.Layer.Modal, up.Layer.Popup, up.Layer.Drawer, up.Layer.Cover]; LAYER_CLASSES = [up.Layer.Root].concat(OVERLAY_CLASSES); /*** Configures default attributes for new overlays. All options for `up.layer.open()` may be configured. The configuration will also be used for `a[up-layer=new]` links. Defaults are configured separately for each [layer mode](/layer-terminology): | Object | Effect | |---------------------------|------------------------------| | `up.layer.config.root` | Defaults for the root layer | | `up.layer.config.modal` | Defaults for modal overlays | | `up.layer.config.drawer` | Defaults for drawer overlays | | `up.layer.config.popup` | Defaults for popup overlays | | `up.layer.config.cover` | Defaults for cover overlays | For convenience you may configure options that affect all layer modes or all overlay modes: | Object | Effect | |---------------------------|------------------------------| | `up.layer.config.any` | Defaults for all layers | | `up.layer.config.overlay` | Defaults for all overlays | Options configured in such a way are inherited. E.g. when you open a new drawer overlay, defaults from `up.layer.config.drawer`, `up.layer.config.overlay` and `up.layer.config.any` will be used (in decreasing priority). \#\#\# Example To make all modal overlays move in from beyond the top edge of the screen: ```js up.layer.config.modal.openAnimation = 'move-from-top' ``` To configure an additional [main target](/up-main) for overlay of any mode: ```js up.layer.config.overlay.mainTargets.unshift('.content') ``` \#\#\# Configuration inheritance @property up.layer.config @param {string} config.mode='modal' The default [mode](/layer-terminology) used when opening a new overlay. @param {object} config.any Defaults for all layer modes. @param {Array<string>} config.any.mainTargets An array of CSS selectors matching default render targets. This is an alias for `up.fragment.config.mainTargets`. @param {object} config.root Defaults for the [root layer](/layer-terminology). Inherits from `up.layer.config.any`. @param {object} config.root.mainTargets @param {object} config.overlay Defaults for all [overlays](/layer-terminology). In addition to the options documented here, all options for `up.layer.open()` may also be configured. Inherits from `up.layer.config.any`. @param {string|Function} config.overlay.openAnimation The opening animation. @param {number} config.overlay.openDuration The duration of the opening animation. @param {string} config.overlay.openEasing The easing function for the opening animation. @param {string|Function} config.overlay.closeAnimation The closing animation. @param {number} config.overlay.closeDuration The duration of the closing animation. @param {string} config.overlay.closeEasing The easing function for the opening animation. @param {string} config.overlay.dismissLabel The symbol for the dismiss icon in the top-right corner. @param {string} config.overlay.dismissAriaLabel The accessibility label for the dismiss icon in the top-right corner. @param {string|boolean} config.overlay.historyVisible='auto' Whether the layer's location or title will be visible in the browser's address bar and window title. If set to `'auto'`, the overlay will render history if its initial fragment is an [auto history target](/up.fragment.config.autoHistoryTargets). If set to `true`, the overlay will always render history. If set to `false`, the overlay will never render history. @param {object} config.modal Defaults for [modal overlays](/layer-terminology). Inherits from `up.layer.config.overlay` and `up.layer.config.any`. @param {object} config.cover Defaults for [cover overlays](/layer-terminology). Inherits from `up.layer.config.overlay` and `up.layer.config.any`. @param {object} config.drawer Defaults for [drawer overlays](/layer-terminology). Inherits from `up.layer.config.overlay` and `up.layer.config.any`. @param {object} config.popup Defaults for [popup overlays](/layer-terminology). Inherits from `up.layer.config.overlay` and `up.layer.config.any`. @stable */ config = new up.Config(function() { var Class, i, len, newConfig; newConfig = { mode: 'modal', any: { mainTargets: ["[up-main='']", 'main', ':layer'] }, root: { mainTargets: ['[up-main~=root]'], historyVisible: true }, overlay: { mainTargets: ['[up-main~=overlay]'], openAnimation: 'fade-in', closeAnimation: 'fade-out', dismissLabel: '×', dismissAriaLabel: 'Dismiss dialog', dismissable: true, historyVisible: 'auto' }, cover: { mainTargets: ['[up-main~=cover]'] }, drawer: { mainTargets: ['[up-main~=drawer]'], backdrop: true, position: 'left', size: 'medium', openAnimation: function(layer) { switch (layer.position) { case 'left': return 'move-from-left'; case 'right': return 'move-from-right'; } }, closeAnimation: function(layer) { switch (layer.position) { case 'left': return 'move-to-left'; case 'right': return 'move-to-right'; } } }, modal: { mainTargets: ['[up-main~=modal]'], backdrop: true, size: 'medium' }, popup: { mainTargets: ['[up-main~=popup]'], position: 'bottom', size: 'medium', align: 'left', dismissable: 'outside key' } }; for (i = 0, len = LAYER_CLASSES.length; i < len; i++) { Class = LAYER_CLASSES[i]; newConfig[Class.mode].Class = Class; } return newConfig; }); /*** A list of layers that are currently open. The first element in the list is the [root layer](/up.layer.root). The last element is the [frontmost layer](/up.layer.front). @property up.layer.stack @param {List<up.Layer>} stack @stable */ stack = null; handlers = []; mainTargets = function(mode) { return u.flatMap(modeConfigs(mode), 'mainTargets'); }; /*** Returns an array of config objects that apply to the given mode name. The config objects are in descending order of specificity. */ modeConfigs = function(mode) { if (mode === 'root') { return [config.root, config.any]; } else { return [config[mode], config.overlay, config.any]; } }; normalizeOptions = function(options) { var base, match, openMethod, shorthandMode; if (typeof (base = up.migrate).handleLayerOptions === "function") { base.handleLayerOptions(options); } if (u.isGiven(options.layer)) { if (match = String(options.layer).match(/^(new|shatter|swap)( (\w+))?/)) { options.layer = 'new'; openMethod = match[1]; shorthandMode = match[3]; options.mode || (options.mode = shorthandMode || config.mode); if (openMethod === 'swap') { if (up.layer.isOverlay()) { options.baseLayer = 'parent'; } } else if (openMethod === 'shatter') { options.baseLayer = 'root'; } } } else { if (options.mode) { options.layer = 'new'; } else if (u.isElementish(options.target)) { options.layer = stack.get(options.target, { normalizeLayerOptions: false }); } else if (options.origin) { options.layer = 'origin'; } else { options.layer = 'current'; } } options.context || (options.context = {}); return options.baseLayer = stack.get('current', u.merge(options, { normalizeLayerOptions: false })); }; build = function(options) { var Class, configs, handleDeprecatedConfig, mode; mode = options.mode; Class = config[mode].Class; configs = u.reverse(modeConfigs(mode)); if (handleDeprecatedConfig = up.migrate.handleLayerConfig) { configs.forEach(handleDeprecatedConfig); } options = u.mergeDefined.apply(u, slice.call(configs).concat([{ mode: mode, stack: stack }], [options])); return new Class(options); }; openCallbackAttr = function(link, attr) { return e.callbackAttr(link, attr, ['layer']); }; closeCallbackAttr = function(link, attr) { return e.callbackAttr(link, attr, ['layer', 'value']); }; reset = function() { config.reset(); stack.reset(); return handlers = u.filter(handlers, 'isDefault'); }; /*** [Opens a new overlay](/opening-overlays). Opening a layer is considered [navigation](/navigation) by default. \#\#\# Example ```js let layer = await up.layer.open({ url: '/contacts' }) console.log(layer.mode) // logs "modal" ``` @function up.layer.open @param {Object} [options] All [render options](/up.render) may be used. You may configure default layer attributes in `up.layer.config`. @param {string} [options.layer="new"] Whether to stack the new overlay or replace existing overlays. See [replacing existing overlays](/opening-layers#replacing-existing-overlays). @param {string} [options.mode] The kind of overlay to open. See [available layer modes](/layer-terminology#available-modes). @param {string} [options.size] The size of the overlay. Supported values are `'small'`, `'medium'`, `'large'` and `'grow'`: See [overlay sizes](/customizing-overlays#overlay-sizes) for details. @param {string} [options.class] An optional HTML class for the overlay's container element. See [overlay classes](/customizing-overlays#overlay-classes). @param {boolean|string|Array<string>} [options.dismissable=true] How the overlay may be [dismissed](/closing-overlays) by the user. Supported values are `'key'`, `'outside'` and `'button'`. See [customizing dismiss controls](/closing-overlays#customizing-dismiss-controls) for details. You may enable multiple dismiss controls by passing an array or a space-separated string. Passing `true` or `false` will enable or disable all dismiss controls. @param {boolean|string} [options.historyVisible] Whether history of the overlay content is visible. If set to `true` the overlay location and title will be shown in browser UI. If set to `'auto'` history will be visible if the initial overlay content matches a [main target](/up-main). @param {string|Function} [options.animation] The opening animation. @param {Function(Event)} [options.onOpened] A function that is called when the overlay was inserted into the DOM. The function argument is an `up:layer:opened` event. The overlay may still play an opening animation when this function is called. To be called when the opening animation is done, pass an [`{ onFinished }`](/up.render#options.onFinished) option. @param {Function(Event)} [options.onAccepted] A function that is called when the overlay was [accepted](/closing-overlays). The function argument is an `up:layer:accepted` event. @param {Function(Event)} [options.onDismissed] A function that is called when the overlay was [dismissed](/closing-overlays). The function argument is an `up:layer:dismissed` event. @param {string|Array<string>} [options.acceptEvent] One or more event types that will cause this overlay to automatically be [accepted](/closing-overlays) when a matching event occurs within the overlay. The [overlay result value](/closing-overlays#overlay-result-values) is the event object that caused the overlay to close. See [Closing when an event is emitted](/closing-overlays#closing-when-an-event-is-emitted). @param {string|Array<string>} [options.dismissEvent] One or more event types that will cause this overlay to automatically be [dismissed](/closing-overlays) when a matching event occurs within the overlay. The [overlay result value](/closing-overlays#overlay-result-values) is the event object that caused the overlay to close. See [Closing when an event is emitted](/closing-overlays#closing-when-an-event-is-emitted). @param {string|Array<string>} [options.acceptLocation] One or more [URL patterns](/url-patterns) that will cause this overlay to automatically be [accepted](/closing-overlays) when the overlay reaches a matching [location](/up.layer.location). The [overlay result value](/closing-overlays#overlay-result-values) is an object of [named segments matches](/url-patterns#capturing-named-segments) captured by the URL pattern. See [Closing when a location is reached](/closing-overlays#closing-when-a-location-is-reached). @param {string|Array<string>} [options.dismissLocation] One or more [URL patterns](/url-patterns) that will cause this overlay to automatically be [dismissed](/closing-overlays) when the overlay reaches a matching [location](/up.layer.location). The [overlay result value](/closing-overlays#overlay-result-values) is an object of [named segments matches](/url-patterns#capturing-named-segments) captured by the URL pattern. See [Closing when a location is reached](/closing-overlays#closing-when-a-location-is-reached). @param {Object} [options.context={}] The initial [context](/up.layer.context) object for the new overlay. @param {string} [options.position] The position of the popup relative to the `{ origin }` element that opened the overlay. Supported values are `'top'`, `'right'`, `'bottom'` and `'left'`. See [popup position](/customizing-overlays#popup-position). @param {string} [options.align] The alignment of the popup within its `{ position }`. Supported values are `'top'`, `'right'`, `'center'`, `'bottom'` and `'left'`. See [popup position](/customizing-overlays#popup-position). @return {Promise<up.Layer>} A promise for the `up.Layer` object that models the new overlay. The promise will be resolved once the overlay was placed into the DOM. @stable */ open = function(options) { options = u.options(options, { layer: 'new', defaultToEmptyContent: true, navigate: true }); return up.render(options).then(function(result) { return result.layer; }); }; /*** This event is emitted before an overlay is opened. The overlay is not yet part of the [layer stack](/up.layer.stack) and has not yet been placed in the DOM. Listeners may prevent this event to prevent the overlay from opening. The event is emitted on the `document`. \#\#\# Changing layer options Listeners may inspect and manipulate options for the overlay that is about to open. For example, to give overlays the CSS class `.warning` if the initial URL contains the word `"confirm"`: ```js up.on('up:layer:open', function(event) { if (event.layerOptions.url.includes('confirm')) { event.layerOptions.class = 'warning' } }) ``` @event up:layer:open @param {Object} event.layerOptions Options for the overlay that is about to open. Listeners may inspect and change the options. All options for `up.layer.open()` may be used. @param {Element} event.origin The link element that is opening the overlay. @param event.preventDefault() Event listeners may call this method to prevent the overlay from opening. @stable */ /*** This event is emitted after a new overlay has been placed into the DOM. The event is emitted right before the opening animation starts. Because the overlay has not been rendered by the browser, this makes it a good occasion to [customize overlay elements](/customizing-overlays#customizing-overlay-elements): ```js up.on('up:layer:opened', function(event) { if (isChristmas()) { up.element.affix(event.layer.element, '.santa-hat', text: 'Merry Christmas!') } }) ``` @event up:layer:opened @param {Element} event.origin The link element that is opening the overlay. @param {up.Layer} event.layer The [layer object](/up.Layer) that is opening. @stable */ /*** This event is emitted after a layer's [location property](/up.Layer.prototype.location) has changed value. This event is also emitted when a layer [without visible history](/up.Layer.prototype.historyVisible) has reached a new location. @param {string} event.location The new location URL. @event up:layer:location:changed @experimental */ /*** Opens an overlay and returns a promise for its [acceptance](/closing-overlays). It's useful to think of overlays as [promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) which may either be **fulfilled (accepted)** or **rejected (dismissed)**. \#\#\# Example Instead of using `up.layer.open()` and passing callbacks, you may use `up.layer.ask()`. `up.layer.ask()` returns a promise for the acceptance value, which you can `await`: ```js let user = await up.layer.ask({ url: '/users/new' }) console.log("New user is " + user) ``` @see closing-overlays @function up.layer.ask @param {Object} options See options for `up.layer.open()`. @return {Promise} A promise that will settle when the overlay closes. When the overlay was accepted, the promise will fulfill with the overlay's acceptance value. When the overlay was dismissed, the promise will reject with the overlay's dismissal value. @stable */ ask = function(options) { return new Promise(function(resolve, reject) { options = u.merge(options, { onAccepted: function(event) { return resolve(event.value); }, onDismissed: function(event) { return reject(event.value); } }); return open(options); }); }; anySelector = function() { return u.map(LAYER_CLASSES, function(Class) { return Class.selector(); }).join(','); }; optionToString = function(option) { if (u.isString(option)) { return "layer \"" + option + "\""; } else { return option.toString(); } }; /*** [Follows](/a-up-follow) this link and opens the result in a new overlay. \#\#\# Example ```html <a href="/menu" up-layer="new">Open menu</a> ``` @selector a[up-layer=new] @params-note All attributes for `a[up-follow]` may also be used. You may configure default layer attributes in `up.layer.config`. @param {string} [up-layer="new"] Whether to stack the new overlay onto the current layer or replace existing overlays. See [replacing existing overlays](/opening-layers#replacing-existing-overlays). @param [up-mode] The kind of overlay to open. See [available layer modes](/layer-terminology#available-modes). @param [up-size] The size of the overlay. See [overlay sizes](/customizing-overlays#overlay-sizes) for details. @param [up-class] An optional HTML class for the overlay's container element. See [overlay classes](/customizing-overlays#overlay-classes). @param [up-history-visible] Whether history of the overlay content is visible. If set to `true` the overlay location and title will be shown in browser UI. If set to `'auto'` history will be visible if the initial overlay content matches a [main target](/up-main). @param [up-dismissable] How the overlay may be [dismissed](/closing-overlays) by the user. See [customizing dismiss controls](/closing-overlays#customizing-dismiss-controls) for details. You may enable multiple dismiss controls by passing a space-separated string. Passing `true` or `false` will enable or disable all dismiss controls. @param [up-animation] The name of the opening animation. @param [up-on-opened] A JavaScript snippet that is called when the overlay was inserted into the DOM. The snippet runs in the following scope: | Expression | Value | |------------|------------------------------------------| | `this` | The link that opened the overlay | | `layer` | An `up.Layer` object for the new overlay | | `event` | An `up:layer:opened` event | @param [up-on-accepted] A JavaScript snippet that is called when the overlay was [accepted](/closing-overlays). The snippet runs in the following scope: | Expression | Value | |------------|-----------------------------------------------| | `this` | The link that originally opened the overlay | | `layer` | An `up.Layer` object for the accepted overlay | | `value` | The overlay's [acceptance value](/closing-overlays#overlay-result-values) | | `event` | An `up:layer:accepted` event | @param [up-on-dismissed] A JavaScript snippet that is called when the overlay was [dismissed](/closing-overlays). The snippet runs in the following scope: | Expression | Value | |------------|------------------------------------------------| | `this` | The link that originally opened the overlay | | `layer` | An `up.Layer` object for the dismissed overlay | | `value` | The overlay's [dismissal value](/closing-overlays#overlay-result-values) | | `event` | An `up:layer:dismissed` event | @param [up-accept-event] One or more event types that will cause this overlay to automatically be [accepted](/closing-overlays) when a matching event occurs within the overlay. The [overlay result value](/closing-overlays#overlay-result-values) is the event object that caused the overlay to close. See [Closing when an event is emitted](/closing-overlays#closing-when-an-event-is-emitted). @param [up-dismiss-event] One or more event types that will cause this overlay to automatically be [dismissed](/closing-overlays) when a matching event occurs within the overlay. The [overlay result value](/closing-overlays#overlay-result-values) is the event object that caused the overlay to close. See [Closing when an event is emitted](/closing-overlays#closing-when-an-event-is-emitted). @param [up-accept-location] One or more [URL patterns](/url-patterns) that will cause this overlay to automatically be [accepted](/closing-overlays) when the overlay reaches a matching [location](/up.layer.location). The [overlay result value](/closing-overlays#overlay-result-values) is an object of [named segments matches](/url-patterns#capturing-named-segments) captured by the URL pattern. See [Closing when a location is reached](/closing-overlays#closing-when-a-location-is-reached). @param [up-dismiss-location] One or more [URL patterns](/url-patterns) that will cause this overlay to automatically be [dismissed](/closing-overlays) when the overlay reaches a matching [location](/up.layer.location). The [overlay result value](/closing-overlays#overlay-result-values) is an object of [named segments matches](/url-patterns#capturing-named-segments) captured by the URL pattern. See [Closing when a location is reached](/closing-overlays#closing-when-a-location-is-reached). @param [up-context] The new overlay's [context](/up.layer.context) object, encoded as JSON. @param [up-position] The position of the popup relative to the `{ origin }` element that opened the overlay. Supported values are `top`, `right`, `bottom` and `left`. See [popup position](/customizing-overlays#popup-position). @param [up-align] The alignment of the popup within its `{ position }`. Supported values are `top`, `right`, `center`, `bottom` and `left`. See [popup position](/customizing-overlays#popup-position). @stable */ /*** [Dismisses](/closing-overlays) the [current layer](/up.layer.current) when the link is clicked. The JSON value of the `[up-accept]` attribute becomes the overlay's [dismissal value](/closing-overlays#overlay-result-values). \#\#\# Example ```html <a href='/dashboard' up-dismiss>Close</a> ``` \#\#\# Fallback for the root layer The link's `[href]` will only be followed when this link is clicked in the [root layer](/up.layer). In an overlay the `click` event's default action is prevented. You can also omit the `[href]` attribute to make a link that only works in overlays. @selector a[up-dismiss] @param [up-dismiss] The overlay's [dismissal value](/closing-overlays#overlay-result-values) as a JSON string. @param [up-confirm] A message the user needs to confirm before the layer is closed. @param [up-animation] The overlay's close animation. Defaults to overlay's [preconfigured close animation](/up.layer.config). @param [up-duration] The close animation's duration in milliseconds. @param [up-easing] The close animation's easing function. @stable */ /*** [Accepts](/closing-overlays) the [current layer](/up.layer.current) when the link is clicked. The JSON value of the `[up-accept]` attribute becomes the overlay's [acceptance value](/closing-overlays#overlay-result-values). \#\#\# Example ```html <a href='/users/5' up-accept='{ "id": 5 }'>Choose user #5</a> ``` \#\#\# Fallback for the root layer The link's `[href]` will only be followed when this link is clicked in the [root layer](/up.layer). In an overlay the `click` event's default action is prevented. You can also omit the `[href]` attribute to make a link that only works in overlays. @selector a[up-accept] @param [up-accept] The overlay's [acceptance value](/closing-overlays#overlay-result-values) as a JSON string. @param [up-confirm] A message the user needs to confirm before the layer is closed. @param [up-duration] The close animation's duration in milliseconds. @param [up-easing] The close animation's easing function. @stable */ up.on('up:fragment:destroyed', function(event) { return stack.sync(); }); up.on('up:framework:boot', function() { return stack = new up.LayerStack(); }); up.on('up:framework:reset', reset); api = u.literal({ config: config, mainTargets: mainTargets, open: open, build: build, ask: ask, normalizeOptions: normalizeOptions, openCallbackAttr: openCallbackAttr, closeCallbackAttr: closeCallbackAttr, anySelector: anySelector, optionToString: optionToString, get_stack: function() { return stack; } }); /*** Returns the current layer in the [layer stack](/up.layer.stack). The *current* layer is usually the [frontmost layer](/up.layer.front). There are however some cases where the current layer is a layer in the background: - While an element in a background layer is being [compiled](/up.compiler). - While an Unpoly event like `up:request:loaded` is being triggered from a background layer. - While an event listener bound to a background layer using `up.Layer#on()` is being called. To temporarily change the current layer from your own code, use `up.Layer#asCurrent()`. \#\#\# Remembering the current layer Most functions in the `up.layer` package affect the current layer. E.g. `up.layer.dismiss()` is shorthand for `up.layer.current.dismiss()`. As described above `up.layer.current` is set to the right layer in compilers and most events, even if that layer is not the frontmost layer. If you have async code, the current layer may change when your callback is called. To address this you may retrieve the current layer for later reference: ```js function dismissCurrentLayerIn(seconds) { let savedLayer = up.layer.current // returns an up.Layer object let dismiss = () => savedLayer.dismiss() setTimeout(dismiss, seconds * 1000) } dismissCurrentLayerIn(10) // ``` @property up.layer.current @param {up.Layer} current @stable */ /*** Returns the number of layers in the [layer stack](/up.layer.stack). The count includes the [root layer](/up.layer.root). Hence a page with a single overlay would return a count of 2. @property up.layer.count @param {number} count The number of layers in the stack. @stable */ /*** Returns an `up.Layer` object for the given [layer option](/layer-option). @function up.layer.get @param {string|up.Layer|number} [layer='current'] The [layer option](/layer-open) to look up. @return {up.Layer|undefined} The layer matching the given option. If no layer matches, `undefined` is returned. @stable */ /*** Returns an array of `up.Layer` objects matching the given [layer option](/layer-option). @function up.layer.getAll @param {string|up.Layer|number} [layer='current'] The [layer option](/layer-open) to look up. @return {Array<up.Layer>} @experimental */ /*** Returns the [root layer](/layer-terminology). The root layer represents the initial page before any overlay was [opened](/opening-overlays). The root layer always exists and cannot be closed. @property up.layer.root @param {up.Layer} root @stable */ /*** Returns an array of all [overlays](/layer-terminology). If no overlay is open, an empty array is returned. To get an array of *all* layers including the [root layer](/up.layer.root), use `up.layer.stack`. @property up.layer.overlays @param {Array<up.Layer>} overlays @stable */ /*** Returns the frontmost layer in the [layer stack](/up.layer.stack). The frontmost layer is the layer directly facing the user. If an overlay is stacked on top of the frontmost layer, that overlay becomes the new frontmost layer. In most cases you don't want to refer to the frontmost layer, but to the [current layer](/up.layer.current) instead. @property up.layer.front @param {up.Layer} front @stable */ /*** [Dismisses](/up.layer.dismiss) all overlays. Afterwards the only remaining layer will be the [root layer](/up.layer.root). @function up.layer.dismissOverlays @param {any} [value] The dismissal value. @param {Object} [options] See options for `up.layer.dismiss()`. @stable */ u.delegate(api, ['get', 'getAll', 'root', 'overlays', 'current', 'front', 'sync', 'count', 'dismissOverlays'], function() { return stack; }); /*** [Accepts](/closing-overlays) the [current layer](up.layer.current). This is a shortcut for `up.layer.current.accept()`. See `up.Layer#accept()` for more documentation. @function up.layer.accept @param {any} [value] @param {Object} [options] @stable */ /*** [Dismisses](/closing-overlays) the [current layer](up.layer.current). This is a shortcut for `up.layer.current.dismiss()`. See `up.Layer#dismiss()` for more documentation. @function up.layer.dismiss @param {any} [value] @param {Object} [options] @stable */ /*** Returns whether the [current layer](/up.layer.current) is the [root layer](/up.layer.root). This is a shortcut for `up.layer.current.isRoot()`. See `up.Layer#isRoot()` for more documentation.. @function up.layer.isRoot @return {boolean} @stable */ /*** Returns whether the [current layer](/up.layer.current) is *not* the [root layer](/up.layer.root). This is a shortcut for `up.layer.current.isOverlay()`. See `up.Layer#isOverlay()` for more documentation. @function up.layer.isOverlay @return {boolean} @stable */ /*** Returns whether the [current layer](/up.layer.current) is the [frontmost layer](/up.layer.front). This is a shortcut for `up.layer.current.isFront()`. See `up.Layer#isFront()` for more documentation. @function up.layer.isFront @return {boolean} @stable */ /*** Listens to a [DOM event](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Events) that originated on an element [contained](/up.Layer.prototype.contains) by the [current layer](/up.layer.current). This is a shortcut for `up.layer.current.on()`. See `up.Layer#on()` for more documentation. @function up.layer.on @param {string} types A space-separated list of event types to bind to. @param {string|Function(): string} [selector] The selector of an element on which the event must be triggered. @param {Object} [options] @param {Function(event, [element], [data])} listener The listener function that should be called. @return {Function()} A function that unbinds the event listeners when called. @stable */ /*** Unbinds an event listener previously bound to the [current layer](/up.layer.current). This is a shortcut for `up.layer.current.off()`. See `up.Layer#off()` for more documentation. @function up.layer.off @param {string} events @param {string|Function(): string} [selector] @param {Function(event, [element], [data])} listener The listener function to unbind. @stable */ /*** [Emits](/up.emit) an event on the [current layer](/up.layer.current)'s [element](/up.layer.element). This is a shortcut for `up.layer.current.emit()`. See `up.Layer#emit()` for more documentation. @function up.layer.emit @param {string} eventType @param {Object} [props={}] @stable */ /*** Returns the parent layer of the [current layer](/up.layer.current). This is a shortcut for `up.layer.current.parent`. See `up.Layer#parent` for more documentation. @property up.layer.parent @param {up.Layer} parent @stable */ /*** Whether fragment updates within the [current layer](/up.layer.current) can affect browser history and window title. This is a shortcut for `up.layer.current.historyVisible`. See `up.Layer#historyVisible` for more documentation. @property up.layer.historyVisible @param {boolean} historyVisible @stable */ /*** The location URL of the [current layer](/up.layer.current). This is a shortcut for `up.layer.current.location`. See `up.Layer#location` for more documentation. @property up.layer.location @param {string} location @stable */ /*** The [current layer](/up.layer.current)'s [mode](/up.layer.mode) which governs its appearance and behavior. @property up.layer.mode @param {string} mode @stable */ /*** The [context](/context) of the [current layer](/up.layer.current). This is aliased as `up.context`. @property up.layer.context @param {string} context The context object. If no context has been set an empty object is returned. @experimental */ u.delegate(api, ['accept', 'dismiss', 'isRoot', 'isOverlay', 'isFront', 'on', 'off', 'emit', 'parent', 'historyVisible', 'location', 'mode', 'context', 'element', 'contains', 'size', 'affix'], function() { return stack.current; }); return api; })(); }).call(this); /*** Linking to fragments ==================== The `up.link` module lets you build links that update fragments instead of entire pages. \#\#\# Motivation In a traditional web application, the entire page is destroyed and re-created when the user follows a link: ![Traditional page flow](/images/tutorial/fragment_flow_vanilla.svg){:width="620" class="picture has_border is_sepia has_padding"} This makes for an unfriendly experience: - State changes caused by AJAX updates get lost during the page transition. - Unsaved form changes get lost during the page transition. - The JavaScript VM is reset during the page transition. - If the page layout is composed from multiple scrollable containers (e.g. a pane view), the scroll positions get lost during the page transition. - The user sees a "flash" as the browser loads and renders the new page, even if large portions of the old and new page are the same (navigation, layout, etc.). Unpoly fixes this by letting you annotate links with an [`up-target`](/a-up-follow#up-target) attribute. The value of this attribute is a CSS selector that indicates which page fragment to update. The server **still renders full HTML pages**, but we only use the targeted fragments and discard the rest: ![Unpoly page flow](/images/tutorial/fragment_flow_unpoly.svg){:width="620" class="picture has_border is_sepia has_padding"} With this model, following links feels smooth. All transient DOM changes outside the updated fragment are preserved. Pages also load much faster since the DOM, CSS and Javascript environments do not need to be destroyed and recreated for every request. \#\#\# Example Let's say we are rendering three pages with a tabbed navigation to switch between screens: Your HTML could look like this: ``` <nav> <a href="/pages/a">A</a> <a href="/pages/b">B</a> <a href="/pages/b">C</a> </nav> <article> Page A </article> ``` Since we only want to update the `<article>` tag, we annotate the links with an `up-target` attribute: ``` <nav> <a href="/pages/a" up-target="article">A</a> <a href="/pages/b" up-target="article">B</a> <a href="/pages/b" up-target="article">C</a> </nav> ``` Note that instead of `article` you can use any other CSS selector like `#main .article`. With these [`up-target`](/a-up-follow#up-target) annotations Unpoly only updates the targeted part of the screen. The JavaScript environment will persist and the user will not see a white flash while the new page is loading. @see a[up-follow] @see a[up-instant] @see a[up-preload] @see up.follow @module up.link */ (function() { var slice = [].slice; up.link = (function() { var ATTRIBUTES_SUGGESTING_FOLLOW, LINKS_WITH_LOCAL_HTML, LINKS_WITH_REMOTE_HTML, combineFollowableSelectors, config, convertClicks, didUserDragAway, e, follow, followMethod, followOptions, followURL, forkEventAsUpClick, fullClickableSelector, fullFollowSelector, fullInstantSelector, fullPreloadSelector, isFollowDisabled, isFollowable, isInstant, isInstantDisabled, isPreloadDisabled, isSafe, lastMousedownTarget, linkPreloader, makeClickable, makeFollowable, parseRequestOptions, preload, reset, shouldFollowEvent, shouldPreload, u, willCache; u = up.util; e = up.element; linkPreloader = new up.LinkPreloader(); lastMousedownTarget = null; LINKS_WITH_LOCAL_HTML = ['a[up-content]', 'a[up-fragment]', 'a[up-document]']; LINKS_WITH_REMOTE_HTML = ['a[href]', '[up-href]']; ATTRIBUTES_SUGGESTING_FOLLOW = ['[up-follow]', '[up-target]', '[up-layer]', '[up-transition]', '[up-preload]']; combineFollowableSelectors = function(elementSelectors, attributeSelectors) { return u.flatMap(elementSelectors, function(elementSelector) { return attributeSelectors.map(function(attributeSelector) { return elementSelector + attributeSelector; }); }); }; /*** Configures defaults for link handling. In particular you can configure Unpoly to handle [all links on the page](/handling-everything) without requiring developers to set `[up-...]` attributes. @property up.link.config @param {Array<string>} config.followSelectors An array of CSS selectors matching links that will be [followed through Unpoly](/a-up-follow). You can customize this property to automatically follow *all* links on a page without requiring an `[up-follow]` attribute. See [Handling all links and forms](/handling-everything). @param {Array<string>} config.noFollowSelectors Exceptions to `config.followSelectors`. Matching links will *not* be [followed through Unpoly](/a-up-follow), even if they match `config.followSelectors`. By default Unpoly excludes: - Links with an `[up-follow=false]` attribute. - Links with a cross-origin `[href]`. - Links with a `[target]` attribute (to target an iframe or open new browser tab). - Links with a `[rel=download]` attribute. - Links with an `[href]` attribute starting with `javascript:`. - Links with an `[href="#"]` attribute that don't also have local HTML in an `[up-document]`, `[up-fragment]` or `[up-content]` attribute. @param {Array<string>} config.instantSelectors An array of CSS selectors matching links that are [followed on `mousedown`](/a-up-instant) instead of on `click`. You can customize this property to follow *all* links on `mousedown` without requiring an `[up-instant]` attribute. See [Handling all links and forms](/handling-everything). @param {Array<string>} config.noInstantSelectors Exceptions to `config.followSelectors`. Matching links will *not* be [followed through Unpoly](/a-up-follow), even if they match `config.followSelectors`. By default Unpoly excludes: - Links with an `[up-instant=false]` attribute. - Links that are [not followable](#config.noFollowSelectors). @param {Array<string>} config.preloadSelectors An array of CSS selectors matching links that are [preloaded on hover](/a-up-preload). You can customize this property to preload *all* links on `mousedown` without requiring an `[up-preload]` attribute. See [Handling all links and forms](/handling-everything). @param {Array<string>} config.noPreloadSelectors Exceptions to `config.preloadSelectors`. Matching links will *not* be [preloaded on hover](/a-up-preload), even if they match `config.preloadSelectors`. By default Unpoly excludes: - Links with an `[up-preload=false]` attribute. - Links that are [not followable](#config.noFollowSelectors). - When the link destination [cannot be cached](/up.network.config#config.autoCache). @param {number} [config.preloadDelay=75] The number of milliseconds to wait before [`[up-preload]`](/a-up-preload) starts preloading. @param {boolean|string} [config.preloadEnabled='auto'] Whether Unpoly will load [preload requests](/a-up-preload). With the default setting (`"auto"`) Unpoly will load preload requests unless `up.network.shouldReduceRequests()` detects a poor connection. If set to `true`, Unpoly will always load preload links. If set to `false`, Unpoly will never preload links. @param {Array<string>} [config.clickableSelectors] A list of CSS selectors matching elements that should behave like links or buttons. @see [up-clickable] @stable */ config = new up.Config(function() { return { followSelectors: combineFollowableSelectors(LINKS_WITH_REMOTE_HTML, ATTRIBUTES_SUGGESTING_FOLLOW).concat(LINKS_WITH_LOCAL_HTML), noFollowSelectors: ['[up-follow=false]', 'a[download]', 'a[target]', 'a[href^="#"]:not([up-content]):not([up-fragment]):not([up-document])', 'a[href^="javascript:"]'], instantSelectors: ['[up-instant]'], noInstantSelectors: ['[up-instant=false]', '[onclick]'], preloadSelectors: combineFollowableSelectors(LINKS_WITH_REMOTE_HTML, ['[up-preload]']), noPreloadSelectors: ['[up-preload=false]'], clickableSelectors: LINKS_WITH_LOCAL_HTML.concat(['[up-emit]', '[up-accept]', '[up-dismiss]', '[up-clickable]']), preloadDelay: 90, preloadEnabled: 'auto' }; }); fullFollowSelector = function() { return config.followSelectors.join(','); }; fullPreloadSelector = function() { return config.preloadSelectors.join(','); }; fullInstantSelector = function() { return config.instantSelectors.join(','); }; fullClickableSelector = function() { return config.clickableSelectors.join(','); }; /*** Returns whether the link was explicitly marked up as not followable, e.g. through `[up-follow=false]`. This differs from `config.followSelectors` in that we want users to configure simple selectors, but let users make exceptions. We also have a few built-in exceptions of our own, e.g. to never follow an `<a href="javascript:...">` link. @function isFollowDisabled @param {Element} link @return {boolean} */ isFollowDisabled = function(link) { return e.matches(link, config.noFollowSelectors.join(',')) || u.isCrossOrigin(link); }; isPreloadDisabled = function(link) { return !up.browser.canPushState() || e.matches(link, config.noPreloadSelectors.join(',')) || isFollowDisabled(link) || !willCache(link); }; willCache = function(link) { var options, request; options = parseRequestOptions(link); if (options.url) { if (options.cache == null) { options.cache = 'auto'; } options.basic = true; request = new up.Request(options); return request.willCache(); } }; isInstantDisabled = function(link) { return e.matches(link, config.noInstantSelectors.join(',')) || isFollowDisabled(link); }; reset = function() { lastMousedownTarget = null; config.reset(); return linkPreloader.reset(); }; /*** Follows the given link with JavaScript and updates a fragment with the server response. By default the layer's [main element](/up-main) will be replaced. Attributes like `a[up-target]` or `a[up-layer]` will be honored. Following a link is considered [navigation](/navigation) by default. Emits the event `up:link:follow`. \#\#\# Examples Assume we have a link with an `a[up-target]` attribute: <a href="/users" up-target=".main">Users</a> Calling `up.follow()` with this link will replace the page's `.main` fragment as if the user had clicked on the link: var link = document.querySelector('a') up.follow(link) @function up.follow @param {Element|jQuery|string} link The link to follow. @param {Object} [options] [Render options](/up.render) that should be used for following the link. Unpoly will parse render options from the given link's attributes like `[up-target]` or `[up-transition]`. See `a[up-follow]` for a list of supported attributes. You may pass this additional `options` object to supplement or override options parsed from the link attributes. @return {Promise<up.RenderResult>} A promise that will be fulfilled when the link destination has been loaded and rendered. @stable */ follow = up.mockable(function(link, options) { return up.render(followOptions(link, options)); }); parseRequestOptions = function(link, options) { var parser; options = u.options(options); parser = new up.OptionsParser(options, link); options.url = followURL(link, options); options.method = followMethod(link, options); parser.json('headers'); parser.json('params'); parser.booleanOrString('cache'); parser.booleanOrString('clearCache'); parser.boolean('solo'); parser.string('contentType', { attr: ['enctype', 'up-content-type'] }); return options; }; /*** Parses the [render](/up.render) options that would be used to [`follow`](/up.follow) the given link, but does not render. \#\#\# Example Given a link with some `[up-...]` attributes: ```html <a href="/foo" up-target=".content" up-layer="new">...</a> ``` We can parse the link's render options like this: ```js let link = document.querySelector('a[href="/foo"]') let options = up.link.followOptions(link) // => { url: '/foo', method: 'GET', target: '.content', layer: 'new', ... } ``` @function up.link.followOptions @param {Element|jQuery|string} link The link to follow. @return {Object} @stable */ followOptions = function(link, options) { var base, base1, parser; link = up.fragment.get(link); options = parseRequestOptions(link, options); parser = new up.OptionsParser(options, link, { fail: true }); parser.boolean('feedback'); parser.boolean('fail'); if ((base = parser.options).origin == null) { base.origin = link; } parser.boolean('navigate', { "default": true }); parser.string('confirm'); parser.string('target'); parser.booleanOrString('fallback'); parser.parse((function(link, attrName) { return e.callbackAttr(link, attrName, ['request', 'response', 'renderOptions']); }), 'onLoaded'); parser.string('content'); parser.string('fragment'); parser.string('document'); parser.boolean('peel'); parser.string('layer'); parser.string('baseLayer'); parser.json('context'); parser.string('mode'); parser.string('align'); parser.string('position'); parser.string('class'); parser.string('size'); parser.booleanOrString('dismissable'); parser.parse(up.layer.openCallbackAttr, 'onOpened'); parser.parse(up.layer.closeCallbackAttr, 'onAccepted'); parser.parse(up.layer.closeCallbackAttr, 'onDismissed'); parser.string('acceptEvent'); parser.string('dismissEvent'); parser.string('acceptLocation'); parser.string('dismissLocation'); parser.booleanOrString('historyVisible'); parser.booleanOrString('focus'); parser.boolean('saveScroll'); parser.booleanOrString('scroll'); parser.boolean('revealTop'); parser.number('revealMax'); parser.number('revealPadding'); parser.number('revealSnap'); parser.string('scrollBehavior'); parser.booleanOrString('history'); parser.booleanOrString('location'); parser.booleanOrString('title'); parser.booleanOrString('animation'); parser.booleanOrString('transition'); parser.string('easing'); parser.number('duration'); if (typeof (base1 = up.migrate).parseFollowOptions === "function") { base1.parseFollowOptions(parser); } options.guardEvent || (options.guardEvent = up.event.build('up:link:follow', { log: 'Following link' })); return options; }; /*** This event is [emitted](/up.emit) when a link is [followed](/up.follow) through Unpoly. The event is emitted on the `<a>` element that is being followed. \#\#\# Changing render options Listeners may inspect and manipulate [render options](/up.render) for the coming fragment update. The code below will open all form-contained links in an overlay, as to not lose the user's form data: ```js up.on('up:link:follow', function(event, link) { if (link.closest('form')) { event.renderOptions.layer = 'new' } }) ``` @event up:link:follow @param {Element} event.target The link element that will be followed. @param {Object} event.renderOptions An object with [render options](/up.render) for the coming fragment update. Listeners may inspect and modify these options. @param event.preventDefault() Event listeners may call this method to prevent the link from being followed. @stable */ /*** Preloads the given link. When the link is clicked later, the response will already be [cached](/up.cache), making the interaction feel instant. @function up.link.preload @param {string|Element|jQuery} link The element or selector whose destination should be preloaded. @param {Object} options See options for `up.follow()`. @return {Promise} A promise that will be fulfilled when the request was loaded and cached. When preloading is [disabled](/up.link.config#config.preloadEnabled) the promise rejects with an `AbortError`. @stable */ preload = function(link, options) { var guardEvent; link = up.fragment.get(link); if (!shouldPreload()) { return up.error.failed.async('Link preloading is disabled'); } guardEvent = up.event.build('up:link:preload', { log: ['Preloading link %o', link] }); return follow(link, u.merge(options, { preload: true }, { guardEvent: guardEvent })); }; shouldPreload = function() { var setting; setting = config.preloadEnabled; if (setting === 'auto') { return !up.network.shouldReduceRequests(); } return setting; }; /*** This event is [emitted](/up.emit) before a link is [preloaded](/up.preload). @event up:link:preload @param {Element} event.target The link element that will be preloaded. @param event.preventDefault() Event listeners may call this method to prevent the link from being preloaded. @stable */ /*** Returns the HTTP method that should be used when following the given link. Looks at the link's `up-method` or `data-method` attribute. Defaults to `"get"`. @function up.link.followMethod @param link @param options.method {string} @internal */ followMethod = function(link, options) { if (options == null) { options = {}; } return u.normalizeMethod(options.method || link.getAttribute('up-method') || link.getAttribute('data-method')); }; followURL = function(link, options) { var url; if (options == null) { options = {}; } url = options.url || link.getAttribute('href') || link.getAttribute('up-href'); if (url !== '#') { return url; } }; /*** Returns whether the given link will be [followed](/up.follow) by Unpoly instead of making a full page load. By default Unpoly will follow links if the element has one of the following attributes: - `[up-follow]` - `[up-target]` - `[up-layer]` - `[up-mode]` - `[up-transition]` - `[up-content]` - `[up-fragment]` - `[up-document]` To make additional elements followable, see `up.link.config.followSelectors`. @function up.link.isFollowable @param {Element|jQuery|string} link The link to check. @stable */ isFollowable = function(link) { link = up.fragment.get(link); return e.matches(link, fullFollowSelector()) && !isFollowDisabled(link); }; /*** Makes sure that the given link will be [followed](/up.follow) by Unpoly instead of making a full page load. If the link is not already [followable](/up.link.isFollowable), the link will receive an `a[up-follow]` attribute. @function up.link.makeFollowable @param {Element|jQuery|string} link The element or selector for the link to make followable. @experimental */ makeFollowable = function(link) { if (!isFollowable(link)) { return link.setAttribute('up-follow', ''); } }; makeClickable = function(link) { if (e.matches(link, 'a[href], button')) { return; } e.setMissingAttrs(link, { tabindex: '0', role: 'link' }); return link.addEventListener('keydown', function(event) { if (event.key === 'Enter' || event.key === 'Space') { return forkEventAsUpClick(event); } }); }; /*** Enables keyboard interaction for elements that should behave like links or buttons. The element will be focusable and screen readers will announce it as a link. Also see [`up.link.config.clickableSelectors`](/up.link.config#config.clickableSelectors). @selector [up-clickable] @experimental */ up.macro(fullClickableSelector, makeClickable); shouldFollowEvent = function(event, link) { var betterTarget, betterTargetSelector; if (event.defaultPrevented || isFollowDisabled(link)) { return false; } betterTargetSelector = "a, [up-href], " + (up.form.fieldSelector()); betterTarget = e.closest(event.target, betterTargetSelector); return !betterTarget || betterTarget === link; }; isInstant = function(linkOrDescendant) { var element; element = e.closest(linkOrDescendant, fullInstantSelector()); return element && !isInstantDisabled(element); }; /*** Provide an `up:click` event that improves on standard click in several ways: - It is emitted on mousedown for [up-instant] elements - It is not emitted if the element has disappeared (or was overshadowed) between mousedown and click. This can happen if mousedown creates a layer over the element, or if a mousedown handler removes a handler. TODO Docs: Is not emitted for modified clicks Stopping an up:click event will also stop the underlying event. Also see docs for `up:click`. @function up.link.convertClicks @param {up.Layer} layer @internal */ convertClicks = function(layer) { layer.on('click', function(event, element) { if (!up.event.isUnmodified(event)) { return; } if (isInstant(element) && lastMousedownTarget) { up.event.halt(event); } else if (layer.wasHitByMouseEvent(event) && !didUserDragAway(event)) { forkEventAsUpClick(event); } return lastMousedownTarget = null; }); return layer.on('mousedown', function(event, element) { if (!up.event.isUnmodified(event)) { return; } lastMousedownTarget = event.target; if (isInstant(element)) { return forkEventAsUpClick(event); } }); }; didUserDragAway = function(clickEvent) { return lastMousedownTarget && lastMousedownTarget !== clickEvent.target; }; forkEventAsUpClick = function(originalEvent) { var newEvent; newEvent = up.event.fork(originalEvent, 'up:click', ['clientX', 'clientY', 'button'].concat(slice.call(up.event.keyModifiers))); return up.emit(originalEvent.target, newEvent, { log: false }); }; /*** A `click` event that honors the [`[up-instant]`](/a-up-instant) attribute. This event is generally emitted when an element is clicked. However, for elements with an [`[up-instant]`](/a-up-instant) attribute this event is emitted on `mousedown` instead. This is useful to listen to links being activated, without needing to know whether a link is `[up-instant]`. \#\#\# Example Assume we have two links, one of which is `[up-instant]`: ```html <a href="/one">Link 1</a> <a href="/two" up-instant>Link 2</a> ``` The following event listener will be called when *either* link is activated: ```js document.addEventListener('up:click', function(event) { ... }) ``` \#\#\# Cancelation is forwarded If the user cancels an `up:click` event using `event.preventDefault()`, the underlying `click` or `mousedown` event will also be canceled. \#\#\# Accessibility If the user activates an element using their keyboard, the `up:click` event will be emitted when the key is pressed even if the element has an `[up-instant]` attribute. \#\#\# Only unmodified clicks are considered To prevent overriding native browser behavior, the `up:click` is only emitted for unmodified clicks. In particular, it is not emitted when the user holds `Shift`, `CTRL` or `Meta` while clicking. Neither it is emitted when the user clicks with a secondary mouse button. @event up:click @param {Element} event.target The clicked element. @param {Event} event.originalEvent The underlying `click` or `mousedown` event. @stable */ /*** Returns whether the given link has a [safe](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.1) HTTP method like `GET`. @function up.link.isSafe @param {Element} link @return {boolean} @stable */ isSafe = function(link) { var method; method = followMethod(link); return up.network.isSafeMethod(method); }; /*** [Follows](/up.follow) this link with JavaScript and updates a fragment with the server response. Following a link is considered [navigation](/navigation) by default. \#\#\# Example This will update the fragment `<div class="content">` with the same element fetched from `/posts/5`: ```html <a href="/posts/5" up-follow up-target=".content">Read post</a> ``` If no `[up-target]` attribute is set, the [main target](/up-main) is updated. \#\#\# Advanced fragment changes See [fragment placement](/fragment-placement) for advanced use cases like updating multiple fragments or appending content to an existing element. \#\#\# Short notation You may omit the `[up-follow]` attribute if the link has one of the following attributes: - `[up-target]` - `[up-layer]` - `[up-transition]` - `[up-content]` - `[up-fragment]` - `[up-document]` Such a link will still be followed through Unpoly. \#\#\# Following all links automatically You can configure Unpoly to follow *all* links on a page without requiring an `[up-follow]` attribute. See [Handling all links and forms](/handling-everything). @selector a[up-follow] @param [href] The URL to fetch from the server. Instead of making a server request, you may also pass an existing HTML string as `[up-document]` or `[up-content]` attribute. @param [up-target] The CSS selector to update. If omitted a [main target](/up-main) will be rendered. @param [up-fallback] Specifies behavior if the [target selector](/up.render#options.target) is missing from the current page or the server response. If set to a CSS selector, Unpoly will attempt to replace that selector instead. If set to `true` Unpoly will attempt to replace a [main target](/up-main) instead. If set to `false` Unpoly will immediately reject the render promise. @param [up-navigate='true'] Whether this fragment update is considered [navigation](/navigation). @param [up-method='get'] The HTTP method to use for the request. Common values are `get`, `post`, `put`, `patch` and `delete`. `The value is case insensitive. The HTTP method may also be passed as an `[data-method]` attribute. @param [up-params] A JSON object with additional [parameters](/up.Params) that should be sent as the request's [query string](https://en.wikipedia.org/wiki/Query_string) or payload. When making a `GET` request to a URL with a query string, the given `{ params }` will be added to the query parameters. @param [up-headers] A JSON object with additional request headers. Note that Unpoly will by default send a number of custom request headers. E.g. the `X-Up-Target` header includes the targeted CSS selector. See `up.protocol` and `up.network.config.metaKeys` for details. @param [up-fragment] A string of HTML comprising *only* the new fragment. No server request will be sent. The `[up-target]` selector will be derived from the root element in the given HTML: ```html <!-- This will update .foo --> <a up-fragment='<div class=".foo">inner</div>'>Click me</a> ``` If your HTML string contains other fragments that will not be rendered, use the `[up-document]` attribute instead. If your HTML string comprises only the new fragment's [inner HTML](https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML), consider the `[up-content]` attribute instead. @param [up-document] A string of HTML containing the new fragment. The string may contain other HTML, but only the element matching the `[up-target]` selector will be extracted and placed into the page. Other elements will be discarded. If your HTML string comprises only the new fragment, consider the `[up-fragment]` attribute instead. With `[up-fragment]` you don't need to pass a `[up-target]`, since Unpoly can derive it from the root element in the given HTML. If your HTML string comprises only the new fragment's [inner HTML](https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML), consider the `[up-content]` attribute. @param [up-fail='auto'] How to render a server response with an error code. Any HTTP status code other than 2xx is considered an error code. See [handling server errors](/server-errors) for details. @param [up-history] Whether the browser URL and window title will be updated. If set to `true`, the history will always be updated, using the title and URL from the server response, or from given `[up-title]` and `[up-location]` attributes. If set to `auto` history will be updated if the `[up-target]` matches a selector in `up.fragment.config.autoHistoryTargets`. By default this contains all [main targets](/up-main). If set to `false`, the history will remain unchanged. [Overlays](/up.layer) will only change the browser URL and window title if the overlay has [visible history](/up.layer.historyVisible), even when `[up-history=true]` is set. @param [up-title] An explicit document title to use after rendering. By default the title is extracted from the response's `<title>` tag. You may also set `[up-title=false]` to explicitly prevent the title from being updated. Note that the browser's window title will only be updated it you also set an `[up-history]` attribute. @param [up-location] An explicit URL to use after rendering. By default Unpoly will use the link's `[href]` or the final URL after the server redirected. You may also set `[up-location=false]` to explicitly prevent the URL from being updated. Note that the browser's URL will only be updated it you also set an `[up-history]` attribute. @param [up-transition] The name of an [transition](/up.motion) to morph between the old and few fragment. If you are [prepending or appending content](/fragment-placement#appending-or-prepending-content), use the `[up-animation]` attribute instead. @param [up-animation] The name of an [animation](/up.motion) to reveal a new fragment when [prepending or appending content](/fragment-placement#appending-or-prepending-content). If you are replacing content (the default), use the `[up-transition]` attribute instead. @param [up-duration] The duration of the transition or animation (in millisconds). @param [up-easing] The timing function that accelerates the transition or animation. See [W3C documentation](http://www.w3.org/TR/css3-transitions/#transition-timing-function) for a list of available timing functions. @param [up-cache] Whether to read from and write to the [cache](/up.cache). With `[up-cache=true]` Unpoly will try to re-use a cached response before connecting to the network. If no cached response exists, Unpoly will make a request and cache the server response. With `[up-cache=auto]` Unpoly will use the cache only if `up.network.config.autoCache` returns `true` for the request. Also see [`up.request({ cache })`](/up.request#options.cache). @param [up-clear-cache] Whether existing [cache](/up.cache) entries will be cleared with this request. By default a non-GET request will clear the entire cache. You may also pass a [URL pattern](/url-patterns) to only clear matching requests. Also see [`up.request({ clearCache })`](/up.request#options.clearCache) and `up.network.config.clearCache`. @param [up-layer='origin current'] The [layer](/up.layer) in which to match and render the fragment. See [layer option](/layer-option) for a list of allowed values. To [open the fragment in a new overlay](/opening-overlays), pass `[up-layer=new]`. In this case attributes for `a[up-layer=new]` may also be used. @param [up-peel] Whether to close overlays obstructing the updated layer when the fragment is updated. This is only relevant when updating a layer that is not the [frontmost layer](/up.layer.front). @param [up-context] A JSON object that will be merged into the [context](/context) of the current layer once the fragment is rendered. @param [up-keep='true'] Whether [`[up-keep]`](/up-keep) elements will be preserved in the updated fragment. @param [up-hungry='true'] Whether [`[up-hungry]`](/up-hungry) elements outside the updated fragment will also be updated. @param [up-scroll] How to scroll after the new fragment was rendered. See [scroll option](/scroll-option) for a list of allowed values. @param [up-save-scroll] Whether to save scroll positions before updating the fragment. Saved scroll positions can later be restored with [`[up-scroll=restore]`](/scroll-option#restoring-scroll-options). @param [up-focus] What to focus after the new fragment was rendered. See [focus option](/focus-option) for a list of allowed values. @param [up-confirm] A message the user needs to confirm before fragments are updated. The message will be shown as a [native browser prompt](https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt). If the user does not confirm the render promise will reject and no fragments will be updated. @param [up-feedback] Whether to give the link an `.up-active` class while loading and rendering content. @param [up-on-loaded] A JavaScript snippet that is called when when the server responds with new HTML, but before the HTML is rendered. The callback argument is a preventable `up:fragment:loaded` event. @param [up-on-finished] A JavaScript snippet that is called when all animations have concluded and elements were removed from the DOM tree. @stable */ up.on('up:click', fullFollowSelector, function(event, link) { if (shouldFollowEvent(event, link)) { up.event.halt(event); return up.log.muteUncriticalRejection(follow(link)); } }); /*** Follows this link on `mousedown` instead of `click`. This will save precious milliseconds that otherwise spent on waiting for the user to release the mouse button. Since an AJAX request will be triggered right way, the interaction will appear faster. Note that using `[up-instant]` will prevent a user from canceling a click by moving the mouse away from the link. However, for navigation actions this isn't needed. E.g. popular operation systems switch tabs on `mousedown` instead of `click`. \#\#\# Example <a href="/users" up-follow up-instant>User list</a> \#\#\# Accessibility If the user activates an element using their keyboard, the `up:click` event will be emitted on `click`, even if the element has an `[up-instant]` attribute. @selector a[up-instant] @stable */ /*** Add an `[up-expand]` attribute to any element to enlarge the click area of a descendant link. `[up-expand]` honors all the Unppoly attributes in expanded links, like `a[up-target]`, `a[up-instant]` or `a[up-preload]`. It also expands links that open [modals](/up.modal) or [popups](/up.popup). \#\#\# Example <div class="notification" up-expand> Record was saved! <a href="/records">Close</a> </div> In the example above, clicking anywhere within `.notification` element would [follow](/up.follow) the *Close* link. \#\#\# Elements with multiple contained links If a container contains more than one link, you can set the value of the `up-expand` attribute to a CSS selector to define which link should be expanded: <div class="notification" up-expand=".close"> Record was saved! <a class="details" href="/records/5">Details</a> <a class="close" href="/records">Close</a> </div> \#\#\# Limitations `[up-expand]` has some limitations for advanced browser users: - Users won't be able to right-click the expanded area to open a context menu - Users won't be able to `CTRL`+click the expanded area to open a new tab To overcome these limitations, consider nesting the entire clickable area in an actual `<a>` tag. [It's OK to put block elements inside an anchor tag](https://makandracards.com/makandra/43549-it-s-ok-to-put-block-elements-inside-an-a-tag). @selector [up-expand] @param [up-expand] A CSS selector that defines which containing link should be expanded. If omitted, the first link in this element will be expanded. @stable */ up.macro('[up-expand]', function(area) { var areaAttrs, childLink, selector; selector = area.getAttribute('up-expand') || 'a, [up-href]'; if (childLink = e.get(area, selector)) { areaAttrs = e.upAttrs(childLink); areaAttrs['up-href'] || (areaAttrs['up-href'] = childLink.getAttribute('href')); e.setMissingAttrs(area, areaAttrs); return makeFollowable(area); } }); /*** Preloads this link when the user hovers over it. When the link is clicked later the response will already be cached, making the interaction feel instant. @selector a[up-preload] @param [up-delay] The number of milliseconds to wait between hovering and preloading. Increasing this will lower the load in your server, but will also make the interaction feel less instant. Defaults to `up.link.config.preloadDelay`. @stable */ up.compiler(fullPreloadSelector, function(link) { if (!isPreloadDisabled(link)) { return linkPreloader.observeLink(link); } }); up.on('up:framework:reset', reset); return { follow: follow, followOptions: followOptions, preload: preload, makeFollowable: makeFollowable, makeClickable: makeClickable, isSafe: isSafe, isFollowable: isFollowable, shouldFollowEvent: shouldFollowEvent, followMethod: followMethod, convertClicks: convertClicks, config: config, combineFollowableSelectors: combineFollowableSelectors }; })(); up.follow = up.link.follow; }).call(this); /*** Forms ===== The `up.form` module helps you work with non-trivial forms. @see form[up-submit] @see form[up-validate] @see input[up-switch] @see form[up-autosubmit] @module up.form */ (function() { var slice = [].slice; up.form = (function() { var ATTRIBUTES_SUGGESTING_SUBMIT, abortScheduledValidate, autosubmit, config, e, fieldSelector, findFields, findSwitcherForTarget, findValidateTarget, findValidateTargetFromConfig, focusedField, fullSubmitSelector, getContainer, isSubmitDisabled, isSubmittable, observe, observeCallbackFromElement, reset, submit, submitButtonSelector, submitOptions, submittingButton, switchTarget, switchTargets, switcherValues, u, validate; u = up.util; e = up.element; ATTRIBUTES_SUGGESTING_SUBMIT = ['[up-submit]', '[up-target]', '[up-layer]', '[up-transition]']; /*** Sets default options for form submission and validation. @property up.form.config @param {number} [config.observeDelay=0] The number of miliseconds to wait before [`up.observe()`](/up.observe) runs the callback after the input value changes. Use this to limit how often the callback will be invoked for a fast typist. @param {Array<string>} [config.submitSelectors] An array of CSS selectors matching forms that will be [submitted through Unpoly](/form-up-follow). You can configure Unpoly to handle *all* forms on a page without requiring an `[up-submit]` attribute: ```js up.form.config.submitSelectors.push('form'] ``` Individual forms may opt out with an `[up-submit=follow]` attribute. You may configure additional exceptions in `config.noSubmitSelectors`. @param {Array<string>} [config.noSubmitSelectors] Exceptions to `config.submitSelectors`. Matching forms will *not* be [submitted through Unpoly](/form-up-submit), even if they match `config.submitSelectors`. @param {Array<string>} [config.validateTargets=['[up-fieldset]:has(&)', 'fieldset:has(&)', 'label:has(&)', 'form:has(&)']] An array of CSS selectors that are searched around a form field that wants to [validate](/up.validate). The first matching selector will be updated with the validation messages from the server. By default this looks for a `<fieldset>`, `<label>` or `<form>` around the validating input field. @param {string} [config.fieldSelectors] An array of CSS selectors that represent form fields, such as `input` or `select`. @param {string} [config.submitButtonSelectors] An array of CSS selectors that represent submit buttons, such as `input[type=submit]`. @stable */ config = new up.Config(function() { return { validateTargets: ['[up-fieldset]:has(:origin)', 'fieldset:has(:origin)', 'label:has(:origin)', 'form:has(:origin)'], fieldSelectors: ['select', 'input:not([type=submit]):not([type=image])', 'button[type]:not([type=submit])', 'textarea'], submitSelectors: up.link.combineFollowableSelectors(['form'], ATTRIBUTES_SUGGESTING_SUBMIT), noSubmitSelectors: ['[up-submit=false]', '[target]'], submitButtonSelectors: ['input[type=submit]', 'input[type=image]', 'button[type=submit]', 'button:not([type])'], observeDelay: 0 }; }); fullSubmitSelector = function() { return config.submitSelectors.join(','); }; abortScheduledValidate = null; reset = function() { return config.reset(); }; /*** @function up.form.fieldSelector @internal */ fieldSelector = function(suffix) { if (suffix == null) { suffix = ''; } return config.fieldSelectors.map(function(field) { return field + suffix; }).join(','); }; /*** Returns a list of form fields within the given element. You can configure what Unpoly considers a form field by adding CSS selectors to the `up.form.config.fieldSelectors` array. If the given element is itself a form field, a list of that given element is returned. @function up.form.fields @param {Element|jQuery} root The element to scan for contained form fields. If the element is itself a form field, a list of that element is returned. @return {NodeList<Element>|Array<Element>} @experimental */ findFields = function(root) { var fields, outsideFieldSelector, outsideFields; root = e.get(root); fields = e.subtree(root, fieldSelector()); if (e.matches(root, 'form[id]')) { outsideFieldSelector = fieldSelector(e.attributeSelector('form', root.getAttribute('id'))); outsideFields = e.all(outsideFieldSelector); fields.push.apply(fields, outsideFields); fields = u.uniq(fields); } return fields; }; /*** @function up.form.submittingButton @param {Element} form @internal */ submittingButton = function(form) { var focusedElement, selector; selector = submitButtonSelector(); focusedElement = document.activeElement; if (focusedElement && e.matches(focusedElement, selector) && form.contains(focusedElement)) { return focusedElement; } else { return e.get(form, selector); } }; /*** @function up.form.submitButtonSelector @internal */ submitButtonSelector = function() { return config.submitButtonSelectors.join(','); }; /*** Submits a form via AJAX and updates a page fragment with the response. up.submit('form.new-user', { target: '.main' }) Instead of loading a new page, the form is submitted via AJAX. The response is parsed for a CSS selector and the matching elements will replace corresponding elements on the current page. The unobtrusive variant of this is the `form[up-submit]` selector. See its documentation to learn how form submissions work in Unpoly. Submitting a form is considered [navigation](/navigation). Emits the event [`up:form:submit`](/up:form:submit). @function up.submit @param {Element|jQuery|string} form The form to submit. If the argument points to an element that is not a form, Unpoly will search its ancestors for the [closest](/up.fragment.closest) form. @param {Object} [options] [Render options](/up.render) that should be used for submitting the form. Unpoly will parse render options from the given form's attributes like `[up-target]` or `[up-transition]`. See `form[up-submit]` for a list of supported attributes. You may pass this additional `options` object to supplement or override options parsed from the form attributes. @return {Promise<up.RenderResult>} A promise that will be fulfilled when the server response was rendered. @stable */ submit = up.mockable(function(form, options) { return up.render(submitOptions(form, options)); }); /*** Parses the [render](/up.render) options that would be used to [`submit`](/up.submit) the given form, but does not render. \#\#\# Example Given a form element: ```html <form action="/foo" method="post" up-target=".content"> ... </form> ``` We can parse the link's render options like this: ```js let form = document.querySelector('form') let options = up.form.submitOptions(form) // => { url: '/foo', method: 'POST', target: '.content', ... } ``` @param {Element|jQuery|string} form The form to submit. @param {Object} [options] Additional options for the form submission. Will override any attribute values set on the given form element. See `up.render()` for detailed documentation of individual option properties. @function up.form.submitOptions @return {Object} @stable */ submitOptions = function(form, options) { var params, parser, submitButton; options = u.options(options); form = up.fragment.get(form); form = e.closest(form, 'form'); parser = new up.OptionsParser(options, form); params = up.Params.fromForm(form); if (submitButton = submittingButton(form)) { params.addField(submitButton); options.method || (options.method = submitButton.getAttribute('formmethod')); options.url || (options.url = submitButton.getAttribute('formaction')); } params.addAll(options.params); options.params = params; parser.string('url', { attr: 'action', "default": up.fragment.source(form) }); parser.string('method', { attr: ['up-method', 'data-method', 'method'], "default": 'GET', normalize: u.normalizeMethod }); if (options.method === 'GET') { options.url = up.Params.stripURL(options.url); } parser.string('failTarget', { "default": up.fragment.toTarget(form) }); options.guardEvent || (options.guardEvent = up.event.build('up:form:submit', { log: 'Submitting form' })); u.assign(options, up.link.followOptions(form, options)); return options; }; /*** This event is [emitted](/up.emit) when a form is [submitted](/up.submit) through Unpoly. The event is emitted on the `<form>` element. When the form is being [validated](/input-up-validate), this event is not emitted. Instead an `up:form:validate` event is emitted. \#\#\# Changing render options Listeners may inspect and manipulate [render options](/up.render) for the coming fragment update. The code below will use a custom [transition](/up-transition) when a form submission [fails](/server-errors): ```js up.on('up:form:submit', function(event, form) { event.renderOptions.failTransition = 'shake' }) ``` @event up:form:submit @param {Element} event.target The `<form>` element that will be submitted. @param {Object} event.renderOptions An object with [render options](/up.render) for the fragment update Listeners may inspect and modify these options. @param event.preventDefault() Event listeners may call this method to prevent the form from being submitted. @stable */ up.on('up:click', submitButtonSelector, function(event, button) { var form; form = e.closest(button, 'form'); if (form && isSubmittable(form)) { return button.focus(); } }); /*** Observes form fields and runs a callback when a value changes. This is useful for observing text fields while the user is typing. The unobtrusive variant of this is the [`[up-observe]`](/up-observe) attribute. \#\#\# Example The following would print to the console whenever an input field changes: up.observe('input.query', function(value) { console.log('Query is now %o', value) }) Instead of a single form field, you can also pass multiple fields, a `<form>` or any container that contains form fields. The callback will be run if any of the given fields change: up.observe('form', function(value, name) { console.log('The value of %o is now %o', name, value) }) You may also pass the `{ batch: true }` option to receive all changes since the last callback in a single object: up.observe('form', { batch: true }, function(diff) { console.log('Observed one or more changes: %o', diff) }) @function up.observe @param {string|Element|Array<Element>|jQuery} elements The form fields that will be observed. You can pass one or more fields, a `<form>` or any container that contains form fields. The callback will be run if any of the given fields change. @param {boolean} [options.batch=false] If set to `true`, the `onChange` callback will receive multiple detected changes in a single diff object as its argument. @param {number} [options.delay=up.form.config.observeDelay] The number of miliseconds to wait before executing the callback after the input value changes. Use this to limit how often the callback will be invoked for a fast typist. @param {Function(value, name): string} onChange The callback to run when the field's value changes. If given as a function, it receives two arguments (`value`, `name`). `value` is a string with the new attribute value and `string` is the name of the form field that changed. If given as a string, it will be evaled as JavaScript code in a context where (`value`, `name`) are set. A long-running callback function may return a promise that settles when the callback completes. In this case the callback will not be called again while it is already running. @return {Function()} A destructor function that removes the observe watch when called. @stable */ observe = function() { var args, callback, elements, fields, observer, options, ref, ref1, ref2, ref3; elements = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : []; elements = e.list(elements); fields = u.flatMap(elements, findFields); callback = (ref = (ref1 = u.extractCallback(args)) != null ? ref1 : observeCallbackFromElement(elements[0])) != null ? ref : up.fail('up.observe: No change callback given'); options = u.extractOptions(args); options.delay = (ref2 = (ref3 = options.delay) != null ? ref3 : e.numberAttr(elements[0], 'up-delay')) != null ? ref2 : config.observeDelay; observer = new up.FieldObserver(fields, options, callback); observer.start(); return function() { return observer.stop(); }; }; observeCallbackFromElement = function(element) { var rawCallback; if (rawCallback = element.getAttribute('up-observe')) { return new Function('value', 'name', rawCallback); } }; /*** [Observes](/up.observe) a field or form and submits the form when a value changes. Both the form and the changed field will be assigned a CSS class [`.up-active`](/form-up-active) while the autosubmitted form is processing. The unobtrusive variant of this is the [`[up-autosubmit]`](/form-up-autosubmit) attribute. @function up.autosubmit @param {string|Element|jQuery} target The field or form to observe. @param {Object} [options] See options for [`up.observe()`](/up.observe) @return {Function()} A destructor function that removes the observe watch when called. @stable */ autosubmit = function(target, options) { return observe(target, options, function() { return submit(target); }); }; findValidateTarget = function(element, options) { var container, givenTarget; container = getContainer(element); if (u.isElementish(options.target)) { return up.fragment.toTarget(options.target); } else if (givenTarget = options.target || element.getAttribute('up-validate') || container.getAttribute('up-validate')) { return givenTarget; } else if (e.matches(element, 'form')) { return up.fragment.toTarget(element); } else { return findValidateTargetFromConfig(element, options) || up.fail('Could not find validation target for %o (tried defaults %o)', element, config.validateTargets); } }; findValidateTargetFromConfig = function(element, options) { var layer; layer = up.layer.get(element); return u.findResult(config.validateTargets, function(defaultTarget) { if (up.fragment.get(defaultTarget, u.merge(options, { layer: layer }))) { return defaultTarget; } }); }; /*** Performs a server-side validation of a form field. `up.validate()` submits the given field's form with an additional `X-Up-Validate` HTTP header. Upon seeing this header, the server is expected to validate (but not save) the form submission and render a new copy of the form with validation errors. The unobtrusive variant of this is the [`input[up-validate]`](/input-up-validate) selector. See the documentation for [`input[up-validate]`](/input-up-validate) for more information on how server-side validation works in Unpoly. \#\#\# Example ```js up.validate('input[name=email]', { target: '.email-errors' }) ``` @function up.validate @param {string|Element|jQuery} field The form field to validate. @param {string|Element|jQuery} [options.target] The element that will be [updated](/up.render) with the validation results. @return {Promise} A promise that fulfills when the server-side validation is received and the form was updated. @stable */ validate = function(field, options) { field = up.fragment.get(field); options = u.options(options); options.navigate = false; options.origin = field; options.history = false; options.target = findValidateTarget(field, options); options.focus = 'keep'; options.fail = false; options.headers || (options.headers = {}); options.headers[up.protocol.headerize('validate')] = field.getAttribute('name') || ':unknown'; options.guardEvent = up.event.build('up:form:validate', { field: field, log: 'Validating form' }); return submit(field, options); }; /*** This event is emitted before a field is being [validated](/input-up-validate). @event up:form:validate @param {Element} event.field The form field that has been changed and caused the validated request. @param {Object} event.renderOptions An object with [render options](/up.render) for the fragment update that will show the validation results. Listeners may inspect and modify these options. @param event.preventDefault() Event listeners may call this method to prevent the validation request being sent to the server. @stable */ switcherValues = function(field) { var checkedButton, form, groupName, meta, value, values; value = void 0; meta = void 0; if (e.matches(field, 'input[type=checkbox]')) { if (field.checked) { value = field.value; meta = ':checked'; } else { meta = ':unchecked'; } } else if (e.matches(field, 'input[type=radio]')) { form = getContainer(field); groupName = field.getAttribute('name'); checkedButton = form.querySelector("input[type=radio]" + (e.attributeSelector('name', groupName)) + ":checked"); if (checkedButton) { meta = ':checked'; value = checkedButton.value; } else { meta = ':unchecked'; } } else { value = field.value; } values = []; if (u.isPresent(value)) { values.push(value); values.push(':present'); } else { values.push(':blank'); } if (u.isPresent(meta)) { values.push(meta); } return values; }; /*** Shows or hides a target selector depending on the value. See [`input[up-switch]`](/input-up-switch) for more documentation and examples. This function does not currently have a very useful API outside of our use for `up-switch`'s UJS behavior, that's why it's currently still marked `@internal`. @function up.form.switchTargets @param {Element} switcher @param {string} [options.target] The target selectors to switch. Defaults to an `[up-switch]` attribute on the given field. @internal */ switchTargets = function(switcher, options) { var fieldValues, form, ref, targetSelector; if (options == null) { options = {}; } targetSelector = (ref = options.target) != null ? ref : switcher.getAttribute('up-switch'); form = getContainer(switcher); targetSelector || up.fail("No switch target given for %o", switcher); fieldValues = switcherValues(switcher); return u.each(e.all(form, targetSelector), function(target) { return switchTarget(target, fieldValues); }); }; /*** @internal */ switchTarget = up.mockable(function(target, fieldValues) { var hideValues, show, showValues; fieldValues || (fieldValues = switcherValues(findSwitcherForTarget(target))); if (hideValues = target.getAttribute('up-hide-for')) { hideValues = u.splitValues(hideValues); show = u.intersect(fieldValues, hideValues).length === 0; } else { if (showValues = target.getAttribute('up-show-for')) { showValues = u.splitValues(showValues); } else { showValues = [':present', ':checked']; } show = u.intersect(fieldValues, showValues).length > 0; } e.toggle(target, show); return target.classList.add('up-switched'); }); /*** @internal */ findSwitcherForTarget = function(target) { var form, switcher, switchers; form = getContainer(target); switchers = e.all(form, '[up-switch]'); switcher = u.find(switchers, function(switcher) { var targetSelector; targetSelector = switcher.getAttribute('up-switch'); return e.matches(target, targetSelector); }); return switcher || up.fail('Could not find [up-switch] field for %o', target); }; getContainer = function(element) { return element.form || e.closest(element, "form, " + (up.layer.anySelector())); }; focusedField = function() { var element; if ((element = document.activeElement) && e.matches(element, fieldSelector())) { return element; } }; /*** Returns whether the given form will be [submitted](/up.follow) through Unpoly instead of making a full page load. By default Unpoly will follow forms if the element has one of the following attributes: - `[up-submit]` - `[up-target]` - `[up-layer]` - `[up-transition]` To consider other selectors to be submittable, see `up.form.config.submitSelectors`. @function up.form.isSubmittable @param {Element|jQuery|string} form The form to check. @stable */ isSubmittable = function(form) { form = up.fragment.get(form); return e.matches(form, fullSubmitSelector()) && !isSubmitDisabled(form); }; isSubmitDisabled = function(form) { return e.matches(form, config.noSubmitSelectors.join(',')); }; /*** Submits this form via JavaScript and updates a fragment with the server response. The server response is searched for the selector given in `up-target`. The selector content is then [replaced](/up.replace) in the current page. The programmatic variant of this is the [`up.submit()`](/up.submit) function. \#\#\# Example ```html <form method="post" action="/users" up-submit> ... </form> ``` \#\#\# Handling validation errors When the server was unable to save the form due to invalid params, it will usually re-render an updated copy of the form with validation messages. For Unpoly to be able to detect a failed form submission, the form must be re-rendered with a non-200 HTTP status code. We recommend to use either 400 (bad request) or 422 (unprocessable entity). In Ruby on Rails, you can pass a [`:status` option to `render`](http://guides.rubyonrails.org/layouts_and_rendering.html#the-status-option) for this: ```ruby class UsersController < ApplicationController def create user_params = params[:user].permit(:email, :password) @user = User.new(user_params) if @user.save? sign_in @user else render 'form', status: :bad_request end end end ``` You may define different option for the failure case by infixing an attribute with `fail`: ```html <form method="post" action="/action" up-target=".content" up-fail-target="form" up-scroll="auto" up-fail-scroll=".errors"> ... </form> ``` See [handling server errors](/server-errors) for details. Note that you can also use [`input[up-validate]`](/input-up-validate) to perform server-side validations while the user is completing fields. \#\#\# Giving feedback while the form is processing The `<form>` element will be assigned a CSS class [`.up-active`](/form.up-active) while the submission is loading. \#\#\# Short notation You may omit the `[up-submit]` attribute if the form has one of the following attributes: - `[up-target]` - `[up-layer]` - `[up-transition]` Such a form will still be submitted through Unpoly. \#\#\# Handling all forms automatically You can configure Unpoly to handle *all* forms on a page without requiring an `[up-submit]` attribute. See [Handling all links and forms](/handling-everything). @selector form[up-submit] @params-note All attributes for `a[up-follow]` may also be used. @stable */ up.on('submit', fullSubmitSelector, function(event, form) { if (event.defaultPrevented || isSubmitDisabled(form)) { return; } if (typeof abortScheduledValidate === "function") { abortScheduledValidate(); } up.event.halt(event); return up.log.muteUncriticalRejection(submit(form)); }); /*** When a form field with this attribute is changed, the form is validated on the server and is updated with validation messages. To validate the form, Unpoly will submit the form with an additional `X-Up-Validate` HTTP header. When seeing this header, the server is expected to validate (but not save) the form submission and render a new copy of the form with validation errors. The programmatic variant of this is the [`up.validate()`](/up.validate) function. \#\#\# Example Let's look at a standard registration form that asks for an e-mail and password: <form action="/users"> <label> E-mail: <input type="text" name="email" /> </label> <label> Password: <input type="password" name="password" /> </label> <button type="submit">Register</button> </form> When the user changes the `email` field, we want to validate that the e-mail address is valid and still available. Also we want to change the `password` field for the minimum required password length. We can do this by giving both fields an `up-validate` attribute: <form action="/users"> <label> E-mail: <input type="text" name="email" up-validate /> </label> <label> Password: <input type="password" name="password" up-validate /> </label> <button type="submit">Register</button> </form> Whenever a field with `up-validate` changes, the form is POSTed to `/users` with an additional `X-Up-Validate` HTTP header. When seeing this header, the server is expected to validate (but not save) the form submission and render a new copy of the form with validation errors. In Ruby on Rails the processing action should behave like this: class UsersController < ApplicationController * This action handles POST /users def create user_params = params[:user].permit(:email, :password) @user = User.new(user_params) if request.headers['X-Up-Validate'] @user.valid? # run validations, but don't save to the database render 'form' # render form with error messages elsif @user.save? sign_in @user else render 'form', status: :bad_request end end end Note that if you're using the `unpoly-rails` gem you can simply say `up.validate?` instead of manually checking for `request.headers['X-Up-Validate']`. The server now renders an updated copy of the form with eventual validation errors: <form action="/users"> <label class="has-error"> E-mail: <input type="text" name="email" value="foo@bar.com" /> Has already been taken! </label> <button type="submit">Register</button> </form> The `<label>` around the e-mail field is now updated to have the `has-error` class and display the validation message. \#\#\# How validation results are displayed Although the server will usually respond to a validation with a complete, fresh copy of the form, Unpoly will by default not update the entire form. This is done in order to preserve volatile state such as the scroll position of `<textarea>` elements. By default Unpoly looks for a `<fieldset>`, `<label>` or `<form>` around the validating input field, or any element with an `up-fieldset` attribute. With the Bootstrap bindings, Unpoly will also look for a container with the `form-group` class. You can change this default behavior by setting `up.form.config.validateTargets`: // Always update the entire form containing the current field ("&") up.form.config.validateTargets = ['form &'] You can also individually override what to update by setting the `up-validate` attribute to a CSS selector: <input type="text" name="email" up-validate=".email-errors"> <span class="email-errors"></span> \#\#\# Updating dependent fields The `[up-validate]` behavior is also a great way to partially update a form when one fields depends on the value of another field. Let's say you have a form with one `<select>` to pick a department (sales, engineering, ...) and another `<select>` to pick an employeee from the selected department: <form action="/contracts"> <select name="department">...</select> <!-- options for all departments --> <select name="employeed">...</select> <!-- options for employees of selected department --> </form> The list of employees needs to be updated as the appartment changes: <form action="/contracts"> <select name="department" up-validate="[name=employee]">...</select> <select name="employee">...</select> </form> In order to update the `department` field in addition to the `employee` field, you could say `up-validate="&, [name=employee]"`, or simply `up-validate="form"` to update the entire form. @selector input[up-validate] @param up-validate The CSS selector to update with the server response. This defaults to a fieldset or form group around the validating field. @stable */ /*** Validates this form on the server when any field changes and shows validation errors. You can configure what Unpoly considers a fieldset by adding CSS selectors to the `up.form.config.validateTargets` array. See `input[up-validate]` for detailed documentation. @selector form[up-validate] @param up-validate The CSS selector to update with the server response. This defaults to a fieldset or form group around the changing field. @stable */ up.on('change', '[up-validate]', function(event) { var field; field = findFields(event.target)[0]; return abortScheduledValidate = u.abortableMicrotask(function() { return up.log.muteUncriticalRejection(validate(field)); }); }); /*** Show or hide elements when a form field is set to a given value. \#\#\# Example: Select options The controlling form field gets an `up-switch` attribute with a selector for the elements to show or hide: <select name="advancedness" up-switch=".target"> <option value="basic">Basic parts</option> <option value="advanced">Advanced parts</option> <option value="very-advanced">Very advanced parts</option> </select> The target elements can use [`[up-show-for]`](/up-show-for) and [`[up-hide-for]`](/up-hide-for) attributes to indicate for which values they should be shown or hidden: <div class="target" up-show-for="basic"> only shown for advancedness = basic </div> <div class="target" up-hide-for="basic"> hidden for advancedness = basic </div> <div class="target" up-show-for="advanced very-advanced"> shown for advancedness = advanced or very-advanced </div> \#\#\# Example: Text field The controlling `<input>` gets an `up-switch` attribute with a selector for the elements to show or hide: <input type="text" name="user" up-switch=".target"> <div class="target" up-show-for="alice"> only shown for user alice </div> You can also use the pseudo-values `:blank` to match an empty input value, or `:present` to match a non-empty input value: <input type="text" name="user" up-switch=".target"> <div class="target" up-show-for=":blank"> please enter a username </div> \#\#\# Example: Checkbox For checkboxes you can match against the pseudo-values `:checked` or `:unchecked`: <input type="checkbox" name="flag" up-switch=".target"> <div class="target" up-show-for=":checked"> only shown when checkbox is checked </div> <div class="target" up-show-for=":cunhecked"> only shown when checkbox is unchecked </div> Of course you can also match against the `value` property of the checkbox element: <input type="checkbox" name="flag" value="active" up-switch=".target"> <div class="target" up-show-for="active"> only shown when checkbox is checked </div> @selector input[up-switch] @param up-switch A CSS selector for elements whose visibility depends on this field's value. @stable */ /*** Only shows this element if an input field with [`[up-switch]`](/input-up-switch) has one of the given values. See [`input[up-switch]`](/input-up-switch) for more documentation and examples. @selector [up-show-for] @param [up-show-for] A space-separated list of input values for which this element should be shown. @stable */ /*** Hides this element if an input field with [`[up-switch]`](/input-up-switch) has one of the given values. See [`input[up-switch]`](/input-up-switch) for more documentation and examples. @selector [up-hide-for] @param [up-hide-for] A space-separated list of input values for which this element should be hidden. @stable */ up.compiler('[up-switch]', function(switcher) { return switchTargets(switcher); }); up.on('change', '[up-switch]', function(event, switcher) { return switchTargets(switcher); }); up.compiler('[up-show-for]:not(.up-switched), [up-hide-for]:not(.up-switched)', function(element) { return switchTarget(element); }); /*** Observes this field and runs a callback when a value changes. This is useful for observing text fields while the user is typing. If you want to submit the form after a change see [`input[up-autosubmit]`](/input-up-autosubmit). The programmatic variant of this is the [`up.observe()`](/up.observe) function. \#\#\# Example The following would run a global `showSuggestions(value)` function whenever the `<input>` changes: <input name="query" up-observe="showSuggestions(value)"> Note that the parameter name in the markup must be called `value` or it will not work. The parameter name can be called whatever you want in the JavaScript, however. Also note that the function must be declared on the `window` object to work, like so: window.showSuggestions = function(selectedValue) { console.log(`Called showSuggestions() with ${selectedValue}`); } \#\#\# Callback context The script given to `[up-observe]` runs with the following context: | Name | Type | Description | | -------- | --------- | ------------------------------------- | | `value` | `string` | The current value of the field | | `this` | `Element` | The form field | | `$field` | `jQuery` | The form field as a jQuery collection | \#\#\# Observing radio buttons Multiple radio buttons with the same `[name]` (a radio button group) produce a single value for the form. To observe radio buttons group, use the `[up-observe]` attribute on an element that contains all radio button elements with a given name: <div up-observe="formatSelected(value)"> <input type="radio" name="format" value="html"> HTML format <input type="radio" name="format" value="pdf"> PDF format <input type="radio" name="format" value="txt"> Text format </div> @selector input[up-observe] @param up-observe The code to run when the field's value changes. @param up-delay The number of miliseconds to wait after a change before the code is run. @stable */ /*** Observes this form and runs a callback when any field changes. This is useful for observing text fields while the user is typing. If you want to submit the form after a change see [`input[up-autosubmit]`](/input-up-autosubmit). The programmatic variant of this is the [`up.observe()`](/up.observe) function. \#\#\# Example The would call a function `somethingChanged(value)` when any `<input>` within the `<form>` changes: <form up-observe="somethingChanged(value)"> <input name="foo"> <input name="bar"> </form> \#\#\# Callback context The script given to `[up-observe]` runs with the following context: | Name | Type | Description | | -------- | --------- | ------------------------------------- | | `value` | `string` | The current value of the field | | `this` | `Element` | The form field | | `$field` | `jQuery` | The form field as a jQuery collection | @selector form[up-observe] @param up-observe The code to run when any field's value changes. @param up-delay The number of miliseconds to wait after a change before the code is run. @stable */ up.compiler('[up-observe]', function(formOrField) { return observe(formOrField); }); /*** Submits this field's form when this field changes its values. Both the form and the changed field will be assigned a CSS class [`.up-active`](/form-up-active) while the autosubmitted form is loading. The programmatic variant of this is the [`up.autosubmit()`](/up.autosubmit) function. \#\#\# Example The following would automatically submit the form when the query is changed: <form method="GET" action="/search"> <input type="search" name="query" up-autosubmit> <input type="checkbox" name="archive"> Include archive </form> \#\#\# Auto-submitting radio buttons Multiple radio buttons with the same `[name]` (a radio button group) produce a single value for the form. To auto-submit radio buttons group, use the `[up-submit]` attribute on an element that contains all radio button elements with a given name: <div up-autosubmit> <input type="radio" name="format" value="html"> HTML format <input type="radio" name="format" value="pdf"> PDF format <input type="radio" name="format" value="txt"> Text format </div> @selector input[up-autosubmit] @param [up-delay] The number of miliseconds to wait after a change before the form is submitted. @stable */ /*** Submits the form when any field changes. Both the form and the field will be assigned a CSS class [`.up-active`](/form-up-active) while the autosubmitted form is loading. The programmatic variant of this is the [`up.autosubmit()`](/up.autosubmit) function. \#\#\# Example This will submit the form when either query or checkbox was changed: <form method="GET" action="/search" up-autosubmit> <input type="search" name="query"> <input type="checkbox" name="archive"> Include archive </form> @selector form[up-autosubmit] @param [up-delay] The number of miliseconds to wait after a change before the form is submitted. @stable */ up.compiler('[up-autosubmit]', function(formOrField) { return autosubmit(formOrField); }); up.on('up:framework:reset', reset); return { config: config, submit: submit, submitOptions: submitOptions, isSubmittable: isSubmittable, observe: observe, validate: validate, autosubmit: autosubmit, fieldSelector: fieldSelector, fields: findFields, focusedField: focusedField, switchTarget: switchTarget }; })(); up.submit = up.form.submit; up.observe = up.form.observe; up.autosubmit = up.form.autosubmit; up.validate = up.form.validate; }).call(this); /*** Navigation feedback =================== The `up.feedback` module adds useful CSS classes to links while they are loading, or when they point to the current URL. By styling these classes you may provide instant feedback to user interactions, improving the perceived speed of your interface. \#\#\# Example Let's say we have an `<nav>` element with two links, pointing to `/foo` and `/bar` respectively: ```html <nav> <a href="/foo" up-follow>Foo</a> <a href="/bar" up-follow>Bar</a> </nav> ``` By giving the navigation bar the `[up-nav]` attribute, links pointing to the current browser address are highlighted as we navigate through the site. If the current URL is `/foo`, the first link is automatically marked with an [`.up-current`](/a.up-current) class: ```html <nav up-nav> <a href="/foo" up-follow class="up-current">Foo</a> <a href="/bar" up-follow>Bar</a> </nav> ``` When the user clicks on the `/bar` link, the link will receive the [`up-active`](/a.up-active) class while it is waiting for the server to respond: ``` <nav up-nav> <a href="/foo" up-follow class="up-current">Foo</a> <a href="/bar" up-follow class="up-active">Bar</a> </div> ``` Once the response is received the URL will change to `/bar` and the `up-active` class is removed: ```html <nav up-nav> <a href="/foo" up-follow>Foo</a> <a href="/bar" up-follow class="up-current">Bar</a> </nav> ``` @see [up-nav] @see a.up-current @see a.up-active @module up.feedback */ (function() { up.feedback = (function() { var CLASS_ACTIVE, SELECTOR_LINK, around, aroundForOptions, config, e, findActivatableArea, getLayerLocation, linkURLs, navSelector, normalizeURL, onBrowserLocationChanged, reset, start, stop, u, updateFragment, updateLayerIfLocationChanged, updateLinks, updateLinksWithinNavs; u = up.util; e = up.element; /*** Sets default options for this package. @property up.feedback.config @param {Array<string>} [config.currentClasses] An array of classes to set on [links that point the current location](/a.up-current). @param {Array<string>} [config.navSelectors] An array of CSS selectors that match [navigation components](/up-nav). @stable */ config = new up.Config(function() { return { currentClasses: ['up-current'], navSelectors: ['[up-nav]', 'nav'] }; }); reset = function() { return config.reset(); }; CLASS_ACTIVE = 'up-active'; SELECTOR_LINK = 'a, [up-href]'; navSelector = function() { return config.navSelectors.join(','); }; normalizeURL = function(url) { if (url) { return u.normalizeURL(url, { stripTrailingSlash: true }); } }; linkURLs = function(link) { return link.upFeedbackURLs || (link.upFeedbackURLs = new up.LinkFeedbackURLs(link)); }; updateFragment = function(fragment) { var layerOption, links; layerOption = { layer: up.layer.get(fragment) }; if (up.fragment.closest(fragment, navSelector(), layerOption)) { links = up.fragment.subtree(fragment, SELECTOR_LINK, layerOption); return updateLinks(links, layerOption); } else { return updateLinksWithinNavs(fragment, layerOption); } }; updateLinksWithinNavs = function(fragment, options) { var links, navs; navs = up.fragment.subtree(fragment, navSelector(), options); links = u.flatMap(navs, function(nav) { return e.subtree(nav, SELECTOR_LINK); }); return updateLinks(links, options); }; getLayerLocation = function(layer) { return layer.feedbackLocation || layer.location; }; updateLinks = function(links, options) { var layer, layerLocation; if (options == null) { options = {}; } if (!links.length) { return; } layer = options.layer || up.layer.get(links[0]); if (layerLocation = getLayerLocation(layer)) { return u.each(links, function(link) { var currentClass, i, isCurrent, len, ref; isCurrent = linkURLs(link).isCurrent(layerLocation); ref = config.currentClasses; for (i = 0, len = ref.length; i < len; i++) { currentClass = ref[i]; e.toggleClass(link, currentClass, isCurrent); } return e.toggleAttr(link, 'aria-current', 'page', isCurrent); }); } }; /*** @function findActivatableArea @param {string|Element|jQuery} element @internal */ findActivatableArea = function(element) { return e.ancestor(element, SELECTOR_LINK) || element; }; /*** Marks the given element as currently loading, by assigning the CSS class [`up-active`](/a.up-active). This happens automatically when following links or submitting forms through the Unpoly API. Use this function if you make custom network calls from your own JavaScript code. If the given element is a link within an [expanded click area](/up-expand), the class will be assigned to the expanded area. \#\#\# Example var button = document.querySelector('button') button.addEventListener('click', () => { up.feedback.start(button) up.request(...).then(() => { up.feedback.stop(button) }) }) @function up.feedback.start @param {Element} element The element to mark as active @internal */ start = function(element) { return findActivatableArea(element).classList.add(CLASS_ACTIVE); }; /*** Links that are currently [loading through Unpoly](/a-up-follow) are assigned the `.up-active` class automatically. Style `.up-active` in your CSS to improve the perceived responsiveness of your user interface. The `.up-active` class will be removed when the link is done loading. \#\#\# Example We have a link: ```html <a href="/foo" up-follow>Foo</a> ``` The user clicks on the link. While the request is loading, the link has the `up-active` class: ```html <a href="/foo" up-follow class="up-active">Foo</a> ``` Once the link destination has loaded and rendered, the `.up-active` class is removed and the [`.up-current`](/a.up-current) class is added: ```html <a href="/foo" up-follow class="up-current">Foo</a> ``` @selector a.up-active @stable */ /*** Forms that are currently [loading through Unpoly](/form-up-submit) are assigned the `.up-active` class automatically. Style `.up-active` in your CSS to improve the perceived responsiveness of your user interface. The `.up-active` class will be removed as soon as the response to the form submission has been received. \#\#\# Example We have a form: ```html <form up-target=".foo"> <button type="submit">Submit</button> </form> ``` The user clicks on the submit button. While the form is being submitted and waiting for the server to respond, the form has the `up-active` class: ```html <form up-target=".foo" class="up-active"> <button type="submit">Submit</button> </form> ``` Once the link destination has loaded and rendered, the `.up-active` class is removed. @selector form.up-active @stable */ /*** Marks the given element as no longer loading, by removing the CSS class [`.up-active`](/a.up-active). This happens automatically when network requests initiated by the Unpoly API have completed. Use this function if you make custom network calls from your own JavaScript code. @function up.feedback.stop @param {Element} element The link or form that has finished loading. @internal */ stop = function(element) { return findActivatableArea(element).classList.remove(CLASS_ACTIVE); }; around = function(element, fn) { var result; start(element); result = fn(); u.always(result, function() { return stop(element); }); return result; }; aroundForOptions = function(options, fn) { var element, feedbackOpt; if (feedbackOpt = options.feedback) { if (u.isBoolean(feedbackOpt)) { element = options.origin; } else { element = feedbackOpt; } } if (element) { element = up.fragment.get(element); return around(element, fn); } else { return fn(); } }; /*** Marks this element as a navigation component, such as a menu or navigation bar. When a link within an `[up-nav]` element points to [its layer's location](/up.layer.location), it is assigned the [`.up-current`](/a.up-current) class. When the browser navigates to another location, the class is removed automatically. You may also assign `[up-nav]` to an individual link instead of an navigational container. If you don't want to manually add this attribute to every navigational element, you can configure selectors to automatically match your navigation components in `up.feedback.config.navs`. \#\#\# Example Let's take a simple menu with two links. The menu has been marked with the `[up-nav]` attribute: ```html <div up-nav> <a href="/foo">Foo</a> <a href="/bar">Bar</a> </div> ``` If the browser location changes to `/foo`, the first link is marked as `.up-current`: ```html <div up-nav> <a href="/foo" class="up-current">Foo</a> <a href="/bar">Bar</a> </div> ``` If the browser location changes to `/bar`, the first link automatically loses its `.up-current` class. Now the second link is marked as `.up-current`: ```html <div up-nav> <a href="/foo">Foo</a> <a href="/bar" class="up-current">Bar</a> </div> ``` \#\#\# When is a link "current"? When no [overlay](/up.layer) is open, the current location is the URL displayed in the browser's address bar. When the link in question is placed in an overlay, the current location is the location of that overlay, even if that overlay doesn't have [visible history](/up.Layer.prototype.historyVisible). A link matches the current location (and is marked as `.up-current`) if it matches either: - the link's `[href]` attribute - the link's `[up-href]` attribute - the URL pattern in the link's [`[up-alias]`](/a-up-alias) attribute @selector [up-nav] @stable */ /*** Links within `[up-nav]` may use the `[up-alias]` attribute to pass a [URL pattern](/url-patterns) for which they should also be highlighted as [`.up-current`](a.up-current). \#\#\# Example The link below will be highlighted with `.up-current` at both `/profile` and `/profile/edit` locations: ```html <div up-nav> <a href="/profile" up-alias="/profile/edit">Profile</a> </div> ``` To pass more than one alternative URLs, use a [URL pattern](/url-patterns). @selector a[up-alias] @param up-alias A [URL pattern](/url-patterns) with alternative URLs. @stable */ /*** When a link within an `[up-nav]` element points to the current location, it is assigned the `.up-current` class. See [`[up-nav]`](/up-nav) for more documentation and examples. @selector a.up-current @stable */ updateLayerIfLocationChanged = function(layer) { var currentLocation, processedLocation; processedLocation = layer.feedbackLocation; currentLocation = normalizeURL(layer.location); if (!processedLocation || processedLocation !== currentLocation) { layer.feedbackLocation = currentLocation; return updateLinksWithinNavs(layer.element, { layer: layer }); } }; onBrowserLocationChanged = function() { var frontLayer; frontLayer = up.layer.front; if (frontLayer.showsLiveHistory()) { return updateLayerIfLocationChanged(frontLayer); } }; up.on('up:location:changed', function(_event) { return onBrowserLocationChanged(); }); up.on('up:fragment:inserted', function(_event, newFragment) { return updateFragment(newFragment); }); up.on('up:layer:location:changed', function(event) { return updateLayerIfLocationChanged(event.layer); }); up.on('up:framework:reset', reset); return { config: config, start: start, stop: stop, around: around, aroundForOptions: aroundForOptions, normalizeURL: normalizeURL }; })(); }).call(this); /*** Passive updates =============== This package contains functionality to passively receive updates from the server. @see [up-hungry] @see [up-poll] @module up.radio */ (function() { up.radio = (function() { var config, e, hungrySelector, reset, shouldPoll, startPolling, stopPolling, u; u = up.util; e = up.element; /*** Configures defaults for passive updates. @property up.radio.config @param {Array<string>} [config.hungrySelectors] An array of CSS selectors that is replaced whenever a matching element is found in a response. These elements are replaced even when they were not targeted directly. By default this contains the [`[up-hungry]`](/up-hungry) attribute. @param {number} [config.pollInterval=30000] The default [polling](/up-poll] interval in milliseconds. @param {boolean|string|Function(Element)} [config.pollEnabled=true] Whether Unpoly will follow instructions to poll fragments, like the `[up-poll]` attribute. When set to `'auto'` Unpoly will poll if one of the following applies: - The browser tab is in the foreground - The fragment's layer is the [frontmost layer](/up.layer.front). - We should not [avoid optional requests](/up.network.shouldReduceRequests) When set to `true`, Unpoly will always allow polling. When set to `false`, Unpoly will never allow polling. You may also pass a function that accepts the polling fragment and returns `true`, `false` or `'auto'`. @stable */ config = new up.Config(function() { return { hungrySelectors: ['[up-hungry]'], pollInterval: 30000, pollEnabled: 'auto' }; }); reset = function() { return config.reset(); }; /*** @function up.radio.hungrySelector @internal */ hungrySelector = function() { return config.hungrySelectors.join(','); }; /*** Elements with an `[up-hungry]` attribute are updated whenever the server sends a matching element, even if the element isn't targeted. Use cases for this are unread message counters or notification flashes. Such elements often live in the layout, outside of the content area that is being replaced. @selector [up-hungry] @param [up-transition] The transition to use when this element is updated. @stable */ /*** Starts [polling](/up-poll) the given element. @function up.radio.startPolling @param {Element|jQuery|string} fragment The fragment to reload periodically. @param {number} options.interval The reload interval in milliseconds. Defaults to `up.radio.config.pollInterval`. @stable */ startPolling = function(fragment, options) { var destructor, doReload, doSchedule, interval, lastRequest, ref, ref1, stopped; if (options == null) { options = {}; } interval = (ref = (ref1 = options.interval) != null ? ref1 : e.numberAttr(fragment, 'up-interval')) != null ? ref : config.pollInterval; stopped = false; lastRequest = null; options.onQueued = function(request) { return lastRequest = request; }; doReload = function() { if (stopped) { return; } if (shouldPoll(fragment)) { return u.always(up.reload(fragment, options), doSchedule); } else { up.puts('[up-poll]', 'Polling is disabled'); return doSchedule(Math.min(10 * 1000, interval)); } }; doSchedule = function(delay) { if (delay == null) { delay = interval; } if (stopped) { return; } return setTimeout(doReload, delay); }; destructor = function() { stopped = true; return lastRequest != null ? lastRequest.abort() : void 0; }; up.on(fragment, 'up:poll:stop', destructor); doSchedule(); return destructor; }; /*** Stops [polling](/up-poll) the given element. @function up.radio.stopPolling @param {Element|jQuery|string} fragment The fragment to stop reloading. @stable */ stopPolling = function(element) { return up.emit(element, 'up:poll:stop'); }; shouldPoll = function(fragment) { var ref, setting; setting = u.evalOption(config.pollEnabled, fragment); if (setting === 'auto') { return !document.hidden && !up.network.shouldReduceRequests() && ((ref = up.layer.get(fragment)) != null ? typeof ref.isFront === "function" ? ref.isFront() : void 0 : void 0); } return setting; }; /*** Elements with an `[up-poll]` attribute are [reloaded](/up.reload) from the server periodically. \#\#\# Example Assume an application layout with an unread message counter. You can use `[up-poll]` to refresh the counter every 30 seconds: ```html <div class="unread-count" up-poll> 2 new messages </div> ``` \#\#\# Controlling the reload interval You may set an optional `[up-interval]` attribute to set the reload interval in milliseconds: ```html <div class="unread-count" up-poll up-interval="10000"> 2 new messages </div> ``` If the value is omitted, a global default is used. You may configure the default like this: ```js up.radio.config.pollInterval = 10000 ``` \#\#\# Controlling the source URL The element will be reloaded from the URL from which it was originally loaded. To reload from another URL, set an `[up-source]` attribute on the polling element: ```html <div class="unread-count" up-poll up-source="/unread-count"> 2 new messages </div> ``` \#\#\# Skipping updates when nothing changed When polling a fragment periodically we want to avoid rendering unchanged content. This saves <b>CPU time</b> and reduces the <b>bandwidth cost</b> for a request/response exchange to **~1 KB**. To achieve this we timestamp your fragments with an `[up-time]` attribute to indicate when the underlying data was last changed. See `[up-time]` for a detailed example. @selector [up-poll] @param [up-interval] The reload interval in milliseconds. Defaults to `up.radio.config.pollInterval`. @stable */ up.compiler('[up-poll]', startPolling); up.on('up:framework:reset', reset); return { config: config, hungrySelector: hungrySelector, startPolling: startPolling, stopPolling: stopPolling }; })(); }).call(this); /*** Play nice with Rails UJS ======================== */ (function() { up.rails = (function() { var e, isRails, u; u = up.util; e = up.element; isRails = function() { var ref; return window._rails_loaded || window.Rails || ((ref = window.jQuery) != null ? ref.rails : void 0); }; return u.each(['method', 'confirm'], function(feature) { var dataAttribute, upAttribute; dataAttribute = "data-" + feature; upAttribute = "up-" + feature; return up.macro("a[" + dataAttribute + "]", function(link) { if (isRails() && up.link.isFollowable(link)) { e.setMissingAttr(link, upAttribute, link.getAttribute(dataAttribute)); return link.removeAttribute(dataAttribute); } }); }); })(); }).call(this); (function() { up.framework.boot(); }).call(this);