assets/unpoly/unpoly.js in unpoly-rails-2.2.1 vs assets/unpoly/unpoly.js in unpoly-rails-2.3.0

- old
+ new

@@ -6,11 +6,11 @@ /*** @module up */ window.up = { - version: '2.2.1' + version: '2.3.0' }; /***/ }), /* 2 */ @@ -1209,11 +1209,11 @@ 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<string, string, object>} tester + @param {Function(string, string, object): boolean} tester A function that will be called with each property. The arguments are the property value, key and the entire object. @return {Object} @experimental @@ -2018,11 +2018,11 @@ sprintf, sprintfWithFormattedArgs, renameKeys, timestamp: secondsSinceEpoch, allSettled, - negate + negate, }; })(); /***/ }), @@ -2091,52 +2091,20 @@ /***/ }), /* 6 */ /***/ (() => { /*- -Browser support -=============== +Browser interface +================= -Unpoly supports all modern browsers. +We tunnel some browser APIs through this module for easier mocking in tests. -### 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 */ up.browser = (function () { const 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 - */ - function loadPage(requestsAttrs) { - new up.Request(requestsAttrs).loadPage(); - } - /*- Submits the given form with a full page load. For mocking in specs. @function up.browser.submitForm @@ -2146,10 +2114,15 @@ form.submit(); } function isIE11() { return 'ActiveXObject' in window; // this is undefined, but the key is set } + function isEdge18() { + // Edge 18: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.19582 + // Edge 92: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36 Edg/92.0.902.78 + return u.contains(navigator.userAgent, ' Edge/'); + } /*- 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 @@ -2195,10 +2168,21 @@ const canPassiveEventListener = u.negate(isIE11); // Don't memoize so a build may publish window.jQuery after Unpoly was loaded function canJQuery() { return !!window.jQuery; } + const canEval = u.memoize(function () { + try { + // Don't use eval() which would prevent minifiers from compressing local variables. + return new Function('return true')(); + } + catch { + // With a strict CSP this will be an error like: + // Uncaught EvalError: call to Function() blocked by CSP + return false; + } + }); // IE11: Use the browser.cookies API instead. function popCookie(name) { let value = document.cookie.match(new RegExp(name + "=(\\w+)"))?.[1]; if (value) { document.cookie = name + '=;Max-Age=0;Path=/'; @@ -2223,46 +2207,23 @@ if (!confirmed) { throw up.error.aborted('User canceled action'); } return true; } - /*- - Returns whether Unpoly supports the current browser. - - If this returns `false` Unpoly will prevent itself from booting - 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 - */ - function isSupported() { - return !supportIssue(); - } - function supportIssue() { - if (!canPromise()) { - return "Browser doesn't support promises"; - } - if (document.compatMode === 'BackCompat') { - return 'Browser is in quirks mode (missing DOCTYPE?)'; - } - } return { - loadPage, submitForm, canPushState, canFormatLog, canPassiveEventListener, canJQuery, + canPromise, + canEval, assertConfirmed, - isSupported, - supportIssue, popCookie, get jQuery() { return getJQuery(); }, - isIE11 + isIE11, + isEdge18, }; })(); /***/ }), @@ -3221,11 +3182,11 @@ } function callbackAttr(link, attr, exposedKeys = []) { let code = link.getAttribute(attr); if (code) { // Allow callbacks to refer to an exposed property directly instead of through `event.value`. - const callback = new Function('event', ...exposedKeys, code); + const callback = up.NonceableCallback.fromString(code).toFunction('event', ...exposedKeys); // Emulate the behavior of the `onclick` attribute, // where `this` refers to the clicked element. return function (event) { const exposedValues = u.values(u.pick(event, exposedKeys)); return callback.call(link, event, ...exposedValues); @@ -4007,20 +3968,21 @@ } bestPreflightSelector() { // We assume that the server will respond with our target. return this.target; } - execute(responseDoc) { + execute(responseDoc, onApplicable) { 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(); } + onApplicable(); 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) { // We cannot use @abortWhenLayerClosed() here, // because the layer is not even in the stack yet. @@ -4177,15 +4139,16 @@ } bestPreflightSelector() { this.matchPreflight(); return u.map(this.steps, 'selector').join(', ') || ':none'; } - execute(responseDoc) { + execute(responseDoc, onApplicable) { this.responseDoc = responseDoc; // For each step, find a step.alternative that matches in both the current page // and the response document. this.matchPostflight(); + onApplicable(); // Don't log @target since that does not include hungry elements up.puts('up.render()', `Updating "${this.bestPreflightSelector()}" in ${this.layer}`); this.options.title = this.improveHistoryValue(this.options.title, this.responseDoc.getTitle()); // Make sure only the first step will have scroll-related options. this.setScrollAndFocusOptions(); @@ -4571,11 +4534,11 @@ const verbPastUpperCaseFirst = u.upperCaseFirst(verbPast); // layer.emit({ ensureBubbles: true }) will automatically emit a second event on document // because the layer is detached. We do not want to emit it on the parent layer where users // might confuse it with an event for the parent layer itself. Since @layer.element // is now detached, the event will no longer bubble up to the document where global - // event listeners can receive it. So we explicitely emit the event a second time + // event listeners can receive it. So we explicitly emit the event a second time // on the document. return this.layer.emit(this.buildEvent(`up:layer:${verbPast}`), { // Set up.layer.current to the parent of the closed layer, which is now likely // to be the front layer. baseLayer: formerParent, @@ -4669,16 +4632,24 @@ execute() { // Preloading from local content is a no-op. if (this.options.preload) { return Promise.resolve(); } - const executePlan = plan => plan.execute(this.getResponseDoc()); - return this.seekPlan(executePlan) || this.postflightTargetNotApplicable(); + return this.seekPlan(this.executePlan.bind(this)) || this.postflightTargetNotApplicable(); } + executePlan(matchedPlan) { + return matchedPlan.execute(this.getResponseDoc(), this.onPlanApplicable.bind(this, matchedPlan)); + } + onPlanApplicable(plan) { + let primaryPlan = this.getPlans()[0]; + if (plan !== primaryPlan) { + up.puts('up.render()', 'Could not match primary target (%s). Updating a fallback target (%s).', primaryPlan.target, plan.target); + } + } getResponseDoc() { if (!this.preview && !this.responseDoc) { - const docOptions = u.pick(this.options, ['target', 'content', 'fragment', 'document', 'html']); + const docOptions = u.pick(this.options, ['target', 'content', 'fragment', 'document', 'html', 'cspNonces']); up.migrate.handleResponseDocOptions?.(docOptions); // If neither { document } nor { fragment } source is given, we assume { content }. if (this.defaultPlacement() === 'content') { // When processing { content }, ResponseDoc needs a { target } // to create a matching element. @@ -4764,11 +4735,11 @@ } execute() { let newPageReason = this.newPageReason(); if (newPageReason) { up.puts('up.render()', newPageReason); - up.browser.loadPage(this.options); + up.network.loadPage(this.options); // Prevent our caller from executing any further code, since we're already // navigating away from this JavaScript environment. return u.unresolvablePromise(); } const promise = this.makeRequest(); @@ -4831,21 +4802,25 @@ // Although processResponse() will fulfill with a successful replacement of options.failTarget, // we still want to reject the promise that's returned to our API client. isSuccessfulResponse() { return (this.successOptions.fail === false) || this.response.ok; } - buildEvent(type, props) { - const defaultProps = { request: this.request, response: this.response, renderOptions: this.options }; - return up.event.build(type, u.merge(defaultProps, props)); - } + // buildEvent(type, props) { + // const defaultProps = { request: this.request, response: this.response, renderOptions: this.options } + // return up.event.build(type, u.merge(defaultProps, props)) + // } updateContentFromResponse(log, renderOptions) { // Allow listeners to inspect the response and either prevent the fragment change // or manipulate change options. An example for when this is useful is a maintenance // page with its own layout, that cannot be loaded as a fragment and must be loaded // with a full page load. - const event = this.buildEvent('up:fragment:loaded', { renderOptions }); - this.request.assertEmitted(event, { log, callback: this.options.onLoaded }); + this.request.assertEmitted('up:fragment:loaded', { + callback: this.options.onLoaded, + response: this.response, + log, + renderOptions, + }); // The response might carry some updates for our change options, // like a server-set location, or server-sent events. this.augmentOptionsFromResponse(renderOptions); return new up.Change.FromContent(renderOptions).execute(); } @@ -4885,10 +4860,11 @@ renderOptions.target = ':none'; } // If the server has provided an update to our context via the X-Up-Context // response header, merge it into our existing { context } option. renderOptions.context = u.merge(renderOptions.context, this.response.context); + renderOptions.cspNonces = this.response.cspNonces; } }; /***/ }), @@ -5334,17 +5310,24 @@ 'callback', 'jQuery', 'guard', 'baseLayer', 'passive', - 'once' + 'once', + 'beforeBoot', ]; } constructor(attributes) { super(attributes); this.key = this.constructor.buildKey(attributes); - this.isDefault = up.framework.booting; + this.isDefault = up.framework.evaling; + // We don't usually run up.on() listeners before Unpoly has booted. + // This is done so incompatible code is not called on browsers that don't support Unpoly. + // Listeners that do need to run before Unpoly boots can pass { beforeBoot: true } to override. + // We also default to { beforeBoot: true } for framework events that are emitted + // before booting. + this.beforeBoot ?? (this.beforeBoot = this.eventType.indexOf('up:framework:') === 0); // Need to store the bound nativeCallback function because addEventListener() // and removeEventListener() need to see the exact same reference. this.nativeCallback = this.nativeCallback.bind(this); } bind() { @@ -5369,10 +5352,13 @@ delete map[this.key]; } this.element.removeEventListener(...this.addListenerArgs()); } nativeCallback(event) { + if (up.framework.beforeBoot && !this.beforeBoot) { + return; + } // Once we drop IE11 support we can forward the { once } option // to Element#addEventListener(). if (this.once) { this.unbind(); } @@ -5455,11 +5441,12 @@ 'callback', 'jQuery', 'guard', 'baseLayer', 'passive', - 'once' + 'once', + 'beforeBoot', ]; } bind() { const unbindFns = []; this.eachListenerAttributes(function (attrs) { @@ -7731,10 +7718,123 @@ /* 49 */ /***/ (() => { const u = up.util; const e = up.element; +up.NonceableCallback = class NonceableCallback { + constructor(script, nonce) { + this.script = script; + this.nonce = nonce; + } + static fromString(string) { + let match = string.match(/^(nonce-([^\s]+)\s)?(.*)$/); + return new this(match[3], match[2]); + } + /*- + Replacement for `new Function()` that can take a nonce to work with a strict Content Security Policy. + + It also prints an error when a strict CSP is active, but user supplies no nonce. + + ### Examples + + ```js + new up.NonceableCallback('1 + 2', 'secret').toFunction() + ``` + + @function up.NonceableCallback#toFunction + @internal + */ + toFunction(...argNames) { + if (up.browser.canEval()) { + return new Function(...argNames, this.script); + } + else if (this.nonce) { + // Don't return a bound function so callers can re-bind to a different this. + let callbackThis = this; + return function (...args) { + return callbackThis.runAsNoncedFunction(this, argNames, args); + }; + } + else { + return this.cannotRun.bind(this); + } + } + toString() { + return `nonce-${this.nonce} ${this.script}`; + } + cannotRun() { + throw new Error(`Your Content Security Policy disallows inline JavaScript (${this.script}). See https://unpoly.com/csp for solutions.`); + } + runAsNoncedFunction(thisArg, argNames, args) { + let wrappedScript = ` + try { + up.noncedEval.value = (function(${argNames.join(',')}) { + ${this.script} + }).apply(up.noncedEval.thisArg, up.noncedEval.args) + } catch (error) { + up.noncedEval.error = error + } + `; + let script; + try { + up.noncedEval = { args, thisArg: thisArg }; + script = up.element.affix(document.body, 'script', { nonce: this.nonce, text: wrappedScript }); + if (up.noncedEval.error) { + throw up.noncedEval.error; + } + else { + return up.noncedEval.value; + } + } + finally { + up.noncedEval = undefined; + if (script) { + up.element.remove(script); + } + } + } + allowedBy(allowedNonces) { + return this.nonce && u.contains(allowedNonces, this.nonce); + } + static adoptNonces(element, allowedNonces) { + if (!allowedNonces?.length) { + return; + } + // Looking up a nonce requires a DOM query. + // For performance reasons we only do this when we're actually rewriting + // a nonce, and only once per response. + const getPageNonce = u.memoize(up.protocol.cspNonce); + u.each(up.protocol.config.nonceableAttributes, (attribute) => { + let matches = e.subtree(element, `[${attribute}^="nonce-"]`); + u.each(matches, (match) => { + let attributeValue = match.getAttribute(attribute); + let callback = this.fromString(attributeValue); + let warn = (message, ...args) => up.log.warn('up.render()', `Cannot use callback [${attribute}="${attributeValue}"]: ${message}`, ...args); + if (!callback.allowedBy(allowedNonces)) { + // Don't rewrite a nonce that the browser would have rejected. + return warn("Callback's CSP nonce (%o) does not match response header (%o)", callback.nonce, allowedNonces); + } + // Replace the nonce with that of the current page. + // This will allow the handler to run via #toFunction(). + let pageNonce = getPageNonce(); + if (!pageNonce) { + return warn("Current page's CSP nonce is unknown"); + } + callback.nonce = pageNonce; + match.setAttribute(attribute, callback.toString()); + }); + }); + } +}; + + +/***/ }), +/* 50 */ +/***/ (() => { + +const u = up.util; +const e = up.element; up.OptionsParser = class OptionsParser { constructor(options, element, parserOptions) { this.options = options; this.element = element; this.fail = parserOptions?.fail; @@ -7793,11 +7893,11 @@ } }; /***/ }), -/* 50 */ +/* 51 */ /***/ (() => { const e = up.element; const u = up.util; up.OverlayFocus = class OverlayFocus { @@ -7884,11 +7984,11 @@ } }; /***/ }), -/* 51 */ +/* 52 */ /***/ (() => { const u = up.util; const e = up.element; /*- @@ -8471,11 +8571,11 @@ } }; /***/ }), -/* 52 */ +/* 53 */ /***/ (() => { const e = up.element; const TRANSITION_DELAY = 300; up.ProgressBar = class ProgressBar { @@ -8528,11 +8628,11 @@ } }; /***/ }), -/* 53 */ +/* 54 */ /***/ (() => { const u = up.util; up.RenderOptions = (function () { const GLOBAL_DEFAULTS = { @@ -8666,11 +8766,11 @@ }; })(); /***/ }), -/* 54 */ +/* 55 */ /***/ (() => { /*- Instances of `up.RenderResult` describe the effects of [rendering](/up.render). @@ -8707,11 +8807,11 @@ } }; /***/ }), -/* 55 */ +/* 56 */ /***/ (() => { const u = up.util; /*- A normalized description of an [HTTP request](/up.request). @@ -9256,11 +9356,15 @@ buildEventEmitter(args) { // We prefer emitting request-related events on the targeted layer. // This way listeners can observe event-related events on a given layer. // This request has an optional { layer } attribute, which is used by // EventEmitter. - return up.EventEmitter.fromEmitArgs(args, { request: this, layer: this.layer }); + return up.EventEmitter.fromEmitArgs(args, { + layer: this.layer, + request: this, + origin: this.origin + }); } emit(...args) { return this.buildEventEmitter(args).emit(); } assertEmitted(...args) { @@ -9268,15 +9372,31 @@ } get description() { return this.method + ' ' + this.url; } }; +// A request is also a promise ("thenable") for its response. u.delegate(up.Request.prototype, ['then', 'catch', 'finally'], function () { return this.deferred; }); +up.Request.tester = function (condition) { + if (u.isFunction(condition)) { + return condition; + } + else if (condition instanceof this) { + return (request) => condition === request; + } + else if (u.isString(condition)) { + let pattern = new up.URLPattern(condition); + return (request) => pattern.test(request.url); + } + else { // boolean, truthy/falsy values + return (_request) => condition; + } +}; /***/ }), -/* 56 */ +/* 57 */ /***/ (() => { let u = up.util; up.Request.Cache = class Cache extends up.Cache { maxSize() { @@ -9303,28 +9423,25 @@ // # will use when they want to replace the entire page. Hence we consider it // # a suitable match for all other selectors, excluding `html`. // candidates.push(request.variant(target: 'body')) // // u.findResult candidates, (candidate) => super(candidate) - clear(pattern) { - if (pattern && (pattern !== '*') && (pattern !== true)) { - pattern = new up.URLPattern(pattern); - return this.each((key, request) => { - if (pattern.test(request.url)) { - this.store.remove(key); - } - }); - } - else { - super.clear(); - } + clear(condition = true) { + let tester = up.Request.tester(condition); + this.each((key, request) => { + if (tester(request)) { + // It is generally not a great idea to manipulate the list we're iterating over, + // but the implementation of up.Cache#each copies keys before iterating. + this.store.remove(key); + } + }); } }; /***/ }), -/* 57 */ +/* 58 */ /***/ (() => { const u = up.util; up.Request.Queue = class Queue { constructor(options = {}) { @@ -9417,25 +9534,24 @@ return this.sendRequestNow(request); } } // Aborting a request will cause its promise to reject, which will also uncache it abort(conditions = true) { + let tester = up.Request.tester(conditions); for (let list of [this.currentRequests, this.queuedRequests]) { - const matches = u.filter(list, request => this.requestMatches(request, conditions)); - matches.forEach(function (match) { - match.abort(); - u.remove(list, match); + const abortableRequests = u.filter(list, tester); + abortableRequests.forEach(function (abortableRequest) { + abortableRequest.abort(); + // Avoid changing the list we're iterating over. + u.remove(list, abortableRequest); }); } } abortExcept(excusedRequest, additionalConditions = true) { const excusedCacheKey = excusedRequest.cacheKey(); this.abort(queuedRequest => (queuedRequest.cacheKey() !== excusedCacheKey) && u.evalOption(additionalConditions, queuedRequest)); } - requestMatches(request, conditions) { - return (request === conditions) || u.evalOption(conditions, request); - } checkSlow() { const currentSlow = this.isSlow(); if (this.emittedSlow !== currentSlow) { this.emittedSlow = currentSlow; if (currentSlow) { @@ -9459,11 +9575,11 @@ } }; /***/ }), -/* 58 */ +/* 59 */ /***/ (() => { const u = up.util; const e = up.element; // In HTML5, forms may only have a GET or POST method. @@ -9487,11 +9603,11 @@ // HTML forms can only have a GET or POST method. Other HTTP methods will be converted // to a `POST` request and carry their original method as a `_method` parameter. method = up.protocol.wrapMethod(method, this.params); } this.form = e.affix(document.body, 'form.up-request-loader', { method, action }); - // We only need an [enctype] attribute if the user has explicitely + // We only need an [enctype] attribute if the user has explicitly // requested one. If none is given, we can use the browser's default // [enctype]. Binary values cannot be sent by this renderer anyway, so // we don't need to default to multipart/form-data in this case. let contentType = this.request.contentType; if (contentType) { @@ -9511,11 +9627,11 @@ } }; /***/ }), -/* 59 */ +/* 60 */ /***/ (() => { const CONTENT_TYPE_URL_ENCODED = 'application/x-www-form-urlencoded'; const CONTENT_TYPE_FORM_DATA = 'multipart/form-data'; const u = up.util; @@ -9618,11 +9734,11 @@ } }; /***/ }), -/* 60 */ +/* 61 */ /***/ (() => { /*- A response to an [HTTP request](/up.request). @@ -9774,10 +9890,13 @@ @experimental */ get contentType() { return this.getHeader('Content-Type'); } + get cspNonces() { + return up.protocol.cspNoncesFromHeader(this.getHeader('Content-Security-Policy')); + } /*- 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. @@ -9796,11 +9915,11 @@ } }; /***/ }), -/* 61 */ +/* 62 */ /***/ (() => { const u = up.util; const e = up.element; up.ResponseDoc = class ResponseDoc { @@ -9826,10 +9945,11 @@ this.scriptWrapper = new up.HTMLWrapper('script'); this.root = this.parseDocument(options) || this.parseFragment(options) || this.parseContent(options); + this.cspNonces = options.cspNonces; } parseDocument(options) { return this.parse(options.document, e.createDocumentFromHTML); } parseContent(options) { @@ -9892,18 +10012,20 @@ return up.fragment.subtree(this.root, selector, { layer: 'any' })[0]; } finalizeElement(element) { // Restore <noscript> tags so they become available to compilers. this.noscriptWrapper.unwrap(element); + // Rewrite per-request CSP nonces to match that of the current page. + up.NonceableCallback.adoptNonces(element, this.cspNonces); // Restore <script> so they will run. this.scriptWrapper.unwrap(element); } }; /***/ }), -/* 62 */ +/* 63 */ /***/ (() => { const e = up.element; const u = up.util; up.RevealMotion = class RevealMotion { @@ -9935,11 +10057,11 @@ } const originalScrollTop = this.viewport.scrollTop; let newScrollTop = originalScrollTop; if (this.top || (elementRect.height > viewportRect.height)) { // Element is either larger than the viewport, - // or the user has explicitely requested for the element to align at top + // or the user has explicitly requested for the element to align at top // => Scroll the viewport so the first element row is the first viewport row const diff = elementRect.top - viewportRect.top; newScrollTop += diff; } else if (elementRect.top < viewportRect.top) { @@ -10015,11 +10137,11 @@ } }; /***/ }), -/* 63 */ +/* 64 */ /***/ (() => { const u = up.util; // We want to make the default speed mimic Chrome's smooth scrolling behavior. // We also want to keep the default value in up.viewport.config.scrollSpeed to be 1. @@ -10094,11 +10216,11 @@ } }; /***/ }), -/* 64 */ +/* 65 */ /***/ (() => { const e = up.element; const u = up.util; up.Selector = class Selector { @@ -10145,11 +10267,11 @@ } }; /***/ }), -/* 65 */ +/* 66 */ /***/ (() => { const u = up.util; up.store || (up.store = {}); up.store.Memory = class Memory { @@ -10179,11 +10301,11 @@ } }; /***/ }), -/* 66 */ +/* 67 */ /***/ (() => { //# // Store implementation backed by window.sessionStorage // ==================================================== @@ -10249,11 +10371,11 @@ } }; /***/ }), -/* 67 */ +/* 68 */ /***/ (() => { const u = up.util; const e = up.element; up.Tether = class Tether { @@ -10409,11 +10531,11 @@ } }; /***/ }), -/* 68 */ +/* 69 */ /***/ (() => { const u = up.util; up.URLPattern = class URLPattern { constructor(fullPattern, normalizeURL = u.normalizeURL) { @@ -10434,11 +10556,21 @@ } buildRegexp(list, capture) { if (!list.length) { return; } - let reCode = list.map(this.normalizeURL).map(u.escapeRegExp).join('|'); + list = list.map((url) => { + // If the current browser location is multiple directories deep (e.g. /foo/bar), + // a leading asterisk would be normalized to /foo/*. So we prepend a slash. + if (url[0] === '*') { + url = '/' + url; + } + url = this.normalizeURL(url); + url = u.escapeRegExp(url); + return url; + }); + let reCode = list.join('|'); reCode = reCode.replace(/\\\*/g, '.*?'); reCode = reCode.replace(/(:|\\\$)([a-z][\w-]*)/ig, (match, type, name) => { // It's \\$ instead of $ because we do u.escapeRegExp above if (type === '\\$') { if (capture) { @@ -10486,18 +10618,36 @@ } }; /***/ }), -/* 69 */ +/* 70 */ /***/ (() => { /*- +Framework initialization +======================== + +The `up.framework` module lets you customize Unpoly's [initialization sequence](/install#initialization). + +@see up.boot +@see script[up-boot=manual] +@see up.framework.isSupported + @module up.framework */ up.framework = (function () { - let booting = true; + // Event up.framework.readyState document.readyState + // ------------------------------------------------------------------------------------------------------ + // Browser starts parsing HTML - loading + // Unpoly script is running evaling loading (if sync) or interactive (if defered) + // ... submodules are running evaling loading (if sync) or interactive (if defered) + // User scripts are running configuring loading (if sync) or interactive (if defered) + // DOMContentLoaded configuring => booting interactive + // Initial page is compiling booting interactive + // Document resources loaded booted complete + let readyState = 'evaling'; // evaling => configuring => booting => booted /*- Resets Unpoly to the state when it was booted. All custom event handlers, animations, etc. that have been registered will be discarded. @@ -10514,68 +10664,176 @@ @event up:framework:reset @internal */ /*- - Boots the Unpoly framework. + Manually boots the Unpoly framework. - **This is called automatically** by including the Unpoly JavaScript files. + It is not usually necessary to call `up.boot()` yourself. When you load [Unpoly's JavaScript file](/install), + Unpoly will automatically boot on [`DOMContentLoaded`](https://developer.mozilla.org/en-US/docs/Web/API/Window/DOMContentLoaded_event). + There are only two cases when you would boot manually: - Unpoly will not boot if the current browser is [not supported](/up.browser.isSupported). + - When you load Unpoly with `<script async>` + - When you explicitly ask to manually boot by loading Unpoly with [`<script up-boot="manual">`](/script-up-boot-manual). + + Before you manually boot, Unpoly should be configured and compilers should be registered. + Booting will cause Unpoly to [compile](/up.hello) the initial page. + + Unpoly will refuse to boot if the current browser is [not supported](/up.framework.isSupported). This leaves you with a classic server-side application on legacy browsers. @function up.boot - @internal + @experimental */ function boot() { + if (readyState !== 'configuring') { + // In an app with a lot of async script the user may attempt to boot us twice. + console.error('Unpoly has already booted'); + return; + } // This is called synchronously after all Unpoly modules have been parsed // and executed. We cannot delay booting until the DOM is ready, since by then // all user-defined event listeners and compilers will have registered. // Note that any non-async scripts after us will delay DOMContentLoaded. - let supportIssue = up.browser.supportIssue(); + let supportIssue = up.framework.supportIssue(); if (!supportIssue) { - // Some Unpoly modules will use the up:framework:boot event to: - // - // - Snapshot their state before user-defined compilers, handlers, etc. have - // been registered. We need to know this state for when we up.reset() later. - // - Run delayed initialization that could not run at load time due to - // circular dependencies. + // Change the state in case any user-provided compiler calls up.boot(). + // up.boot() is a no-op unless readyState === 'configuring'. + readyState = 'booting'; up.emit('up:framework:boot', { log: false }); - booting = false; - // From here on, all event handlers (both Unpoly's and user code) want to - // work with the DOM, so wait for the DOM to be ready. - up.event.onReady(function () { - // By now all non-sync <script> tags have been loaded and called, including - // those after us. All user-provided compilers, event handlers, etc. have - // been registered. - // - // The following event will cause Unpoly to compile the <body>. - up.emit('up:app:boot', { log: 'Booting user application' }); - }); + readyState = 'booted'; } else { - console.error("Unpoly cannot load: %s", supportIssue); + console.error("Unpoly cannot boot: %s", supportIssue); } } + function mustBootManually() { + let unpolyScript = document.currentScript; + // If we're is loaded via <script async>, there are no guarantees + // when we're called or when subsequent scripts that configure Unpoly + // have executed + if (unpolyScript?.async) { + return true; + } + // If we're loaded with <script up-boot="manual"> the user explicitly + // requested to boot Unpoly manually. + if (unpolyScript?.getAttribute('up-boot') === 'manual') { + return true; + } + // If we're loaded this late, someone loads us dynamically. + // We don't know when subsequent scripts that configure Unpoly + // have executed. + if (document.readyState === 'complete') { + return true; + } + } + /*- + Prevent Unpoly from booting automatically. + + By default Unpoly [automatically boots](/install#initialization) + on [`DOMContentLoaded`](https://developer.mozilla.org/en-US/docs/Web/API/Window/DOMContentLoaded_event). + To prevent this, add an `[up-boot="manual"]` attribute to the `<script>` element + that loads Unpoly: + + ```html + <script src="unpoly.js" up-boot="manual"></script> + ``` + You may then call `up.boot()` to manually boot Unpoly at a later time. + + ### Browser support + + To use this feature in Internet Explorer 11 you need a polyfill for `document.currentScript`. + + @selector script[up-boot=manual] + @experimental + */ + function onEvaled() { + up.emit('up:framework:evaled', { log: false }); + if (mustBootManually()) { + console.debug('Call up.boot() after you have configured Unpoly'); + } + else { + // (1) On DOMContentLoaded we know that all non-[async] scripts have executed. + // (2) Deferred scripts execute after the DOM was parsed (document.readyState === 'interactive'), + // but before DOMContentLoaded. That's why we must *not* boot synchonously when + // document.readyState === 'interactive'. We must wait until DOMContentLoaded, when we know that + // subsequent users scripts have executed and (possibly) configured Unpoly. + // (3) There are no guarantees when [async] scripts execute. These must boot Unpoly manually. + document.addEventListener('DOMContentLoaded', boot); + } + // After this line user scripts may run and configure Unpoly, add compilers, etc. + readyState = 'configuring'; + } function startExtension() { - booting = true; + if (readyState !== 'configuring') { + throw new Error('Unpoly extensions must be loaded before booting'); + } + readyState = 'evaling'; } function stopExtension() { - booting = false; + readyState = 'configuring'; } + /*- + Returns whether Unpoly can boot in the current browser. + + If this returns `false` Unpoly will prevent itself from [booting](/up.boot) + and will not [compile](/up.compiler) the initial page. + This leaves you with a classic server-side application. + + ### Browser support + + Unpoly aims to 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. + + @function up.framework.isSupported + @stable + */ + function isSupported() { + return !supportIssue(); + } + function supportIssue() { + if (!up.browser.canPromise()) { + return "Browser doesn't support promises"; + } + if (document.compatMode === 'BackCompat') { + return 'Browser is in quirks mode (missing DOCTYPE?)'; + } + if (up.browser.isEdge18()) { + return 'Edge 18 or lower is unsupported'; + } + } return { + onEvaled, boot, startExtension, stopExtension, reset: emitReset, - get booting() { return booting; } + get evaling() { return readyState === 'evaling'; }, + get booted() { return readyState === 'booted'; }, + get beforeBoot() { return readyState !== 'booting' && readyState !== 'booted'; }, + isSupported, + supportIssue, }; })(); +up.boot = up.framework.boot; /***/ }), -/* 70 */ +/* 71 */ /***/ (() => { /*- Events ====== @@ -10583,11 +10841,11 @@ 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 +### 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: @@ -10622,10 +10880,11 @@ `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. + - Your event listener will not be called when Unpoly has not [booted](/up.boot) in an unsupported browser - 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. @@ -11039,34 +11298,10 @@ */ function halt(event) { event.stopImmediatePropagation(); 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 - */ - function onReady(callback) { - // Values are "loading", "interactive" and "completed". - // https://developer.mozilla.org/en-US/docs/Web/API/Document/readyState - if (document.readyState !== 'loading') { - callback(); - } - else { - document.addEventListener('DOMContentLoaded', callback); - } - } const keyModifiers = ['metaKey', 'shiftKey', 'ctrlKey', 'altKey']; /*- @function up.event.isUnmodified @internal */ @@ -11147,11 +11382,10 @@ build, emit, assertEmitted, onEscape, halt, - onReady, isUnmodified, fork, keyModifiers }; })(); @@ -11161,11 +11395,11 @@ up.$off = up.event.off; // it's the same as up.off() up.emit = up.event.emit; /***/ }), -/* 71 */ +/* 72 */ /***/ (() => { /*- Server protocol =============== @@ -11180,11 +11414,11 @@ 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 +### Existing implementations You should be able to implement the protocol in a very short time. There are existing implementations for various web frameworks: @@ -11378,27 +11612,43 @@ } function clearCacheFromXHR(xhr) { return extractHeader(xhr, 'clearCache', parseClearCacheValue); } /*- - The server may send this optional response header with the value `clear` to [clear the cache](/up.cache.clear). + The server may send this optional response header to control which previously cached responses should be [uncached](/up.cache.clear) after this response. - ### Example + The value of this header is a [URL pattern](/url-patterns) matching responses that should be uncached. + For example, to uncache all responses to URLs starting with `/notes/`: + ```http - X-Up-Cache: clear + X-Up-Clear-Cache: /notes/* ``` - @header X-Up-Cache - @param value - The string `"clear"`. + ### Overriding the client-side default + + If the server does not send an `X-Up-Clear-Cache` header, Unpoly will [clear the entire cache](/up.network.config#config.clearCache) after a non-GET request. + + You may force Unpoly to *keep* the cache after a non-GET request: + + ```http + X-Up-Clear-Cache: false + ``` + + You may also force Unpoly to *clear* the cache after a GET request: + + ```http + X-Up-Clear-Cache: * + ``` + + @header X-Up-Clear-Cache @stable */ /*- 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. + The timestamp must be explicitly 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 @@ -11708,20 +11958,20 @@ ``` ### 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 `X-Up-Dismiss-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-Dismiss-Layer: {"user_id": 1012} X-Up-Target: :none ``` @header X-Up-Dismiss-Layer @stable @@ -11839,15 +12089,31 @@ ``` <meta name='csrf-token' content='secret12345'> ``` + @param {string|Function(): string} [config.cspNonce] + A [CSP script nonce](https://content-security-policy.com/nonce/) + for the initial page that [booted](/up.boot) Unpoly. + + The nonce let Unpoly run JavaScript in HTML attributes like + [`[up-on-loaded]`](/a-up-follow#up-on-loaded) or [`[up-on-accepted]`](/a-up-layer-new#up-on-accepted). + See [Working with a strict Content Security Policy](/csp). + + The nonce can either be configured as a string or as function that returns the nonce. + + Defaults to the `content` attribute of a `<meta>` tag named `csp-nonce`: + + ``` + <meta name='csrf-token' content='secret98765'> + ``` + @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 + Methods must be wrapped when making a [full page request](/up.network.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 @@ -11861,21 +12127,42 @@ */ const config = new up.Config(() => ({ methodParam: '_method', csrfParam() { return e.metaContent('csrf-param'); }, csrfToken() { return e.metaContent('csrf-token'); }, - csrfHeader: 'X-CSRF-Token' // Used by Rails. Other frameworks use different headers. + cspNonce() { return e.metaContent('csp-nonce'); }, + csrfHeader: 'X-CSRF-Token', + nonceableAttributes: ['up-observe', 'up-on-accepted', 'up-on-dismissed', 'up-on-loaded', 'up-on-finished', 'up-observe'], })); function csrfHeader() { return u.evalOption(config.csrfHeader); } function csrfParam() { return u.evalOption(config.csrfParam); } function csrfToken() { return u.evalOption(config.csrfToken); } + function cspNonce() { + return u.evalOption(config.cspNonce); + } + function cspNoncesFromHeader(cspHeader) { + let nonces = []; + if (cspHeader) { + let parts = cspHeader.split(/\s*;\s*/); + for (let part of parts) { + if (part.indexOf('script-src') === 0) { + let noncePattern = /'nonce-([^']+)'/g; + let match; + while (match = noncePattern.exec(part)) { + nonces.push(match[1]); + } + } + } + } + return nonces; + } function wrapMethod(method, params) { params.add(config.methodParam, method); return 'POST'; } function reset() { @@ -11895,19 +12182,21 @@ eventPlansFromXHR, clearCacheFromXHR, csrfHeader, csrfParam, csrfToken, + cspNonce, initialRequestMethod, headerize, - wrapMethod + wrapMethod, + cspNoncesFromHeader, }; })(); /***/ }), -/* 72 */ +/* 73 */ /***/ (() => { /*- Logging ======= @@ -11927,17 +12216,17 @@ const sessionStore = new up.store.Session('up.log'); /*- Configures the logging output on the developer console. @property up.log.config - @param {boolean} [options.enabled=false] + @param {boolean} [config.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] + @param {boolean} [config.banner=true] Print the Unpoly banner to the developer console. @stable */ const config = new up.Config(() => ({ enabled: sessionStore.get('enabled'), @@ -11991,11 +12280,11 @@ message = `[${trace}] ${message}`; } console[stream](message, ...args); } } - const printBanner = function () { + function printBanner() { if (!config.banner) { return; } // The ASCII art looks broken in code since we need to escape backslashes const logo = " __ _____ ___ ___ / /_ __\n" + @@ -12017,12 +12306,12 @@ console.log('%c' + logo + '%c' + text, 'font-family: monospace;' + color, color); } else { console.log(logo + text); } - }; - up.on('up:app:boot', printBanner); + } + up.on('up:framework:boot', printBanner); up.on('up:framework:reset', reset); function setEnabled(value) { sessionStore.set('enabled', value); config.enabled = value; } @@ -12114,20 +12403,20 @@ config, enable, disable, fail, muteUncriticalRejection, - isEnabled() { return config.enabled; } + isEnabled() { return config.enabled; }, }; })(); up.puts = up.log.puts; up.warn = up.log.warn; up.fail = up.log.fail; /***/ }), -/* 73 */ +/* 74 */ /***/ (() => { /*- Custom JavaScript ================= @@ -12369,11 +12658,11 @@ See [`up.compiler()`](/up.compiler#parameters) for details. @stable */ function registerMacro(...args) { const macro = buildCompiler(args); - if (up.framework.booting) { + if (up.framework.evaling) { macro.priority = detectSystemMacroPriority(macro.selector) || up.fail('Unregistered priority for system macro %o', macro.selector); } return insertCompiler(macros, macro); } @@ -12430,19 +12719,22 @@ }; function buildCompiler(args) { let [selector, options, callback] = parseCompilerArgs(args); options = u.options(options, { selector, - isDefault: up.framework.booting, + isDefault: up.framework.evaling, priority: 0, batch: false, keep: false, jQuery: false }); return u.assign(callback, options); } function insertCompiler(queue, newCompiler) { + if (up.framework.booted) { + up.puts('up.compiler()', 'Compiler %s was registered after booting Unpoly. Compiler will run for future fragments.', newCompiler.selector); + } let existingCompiler; let index = 0; while ((existingCompiler = queue[index]) && (existingCompiler.priority >= newCompiler.priority)) { index += 1; } @@ -12625,11 +12917,11 @@ up.$macro = up.syntax.$macro; up.data = up.syntax.data; /***/ }), -/* 74 */ +/* 75 */ /***/ (() => { /*- History ======== @@ -12852,11 +13144,11 @@ if (up.protocol.initialRequestMethod() === 'GET') { // Replace the vanilla state of the initial page load with an Unpoly-enabled state replace(currentLocation(), { event: false }); } } - up.on('up:app:boot', function () { + up.on('up:framework:boot', function () { if ('jasmine' in window) { // Can't delay this in tests. register(); } else { @@ -12917,14 +13209,14 @@ }; })(); /***/ }), -/* 75 */ +/* 76 */ /***/ ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => { -__webpack_require__(76); +__webpack_require__(77); const u = up.util; const e = up.element; /*- Fragment API =========== @@ -13320,11 +13612,11 @@ @param {string|Element} [options.content] The new [inner HTML](https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML) for the fragment. @param {string|Element} [options.fragment] - A string of HTML comprising *only* the new fragment. + A string of HTML comprising *only* the new fragment's [outer HTML](https://developer.mozilla.org/en-US/docs/Web/API/Element/outerHTML). The `{ target }` selector will be derived from the root element in the given HTML: ```js @@ -13420,16 +13712,22 @@ Also see [`up.request({ cache })`](/up.request#options.cache). @param {boolean|string} [options.clearCache] Whether existing [cache](/up.request#caching) entries will be [cleared](/up.cache.clear) with 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. - By default a non-GET request will clear the entire cache. + To only uncache some requests, pass an [URL pattern](/url-patterns) that matches requests to uncache. + You may also pass a function that accepts an existing `up.Request` and returns a boolean value. - Also see [`up.request({ clearCache })`](/up.request#options.clearCache) and `up.network.config.clearCache`. + @param {boolean|string|Function(request): boolean} [options.solo] + With `{ solo: true }` Unpoly will [abort](/up.network.abort) all other requests before laoding the new fragment. + To only abort some requests, pass an [URL pattern](/url-patterns) that matches requests to abort. + You may also pass a function that accepts an existing `up.Request` and returns a boolean value. + @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). @@ -13603,16 +13901,23 @@ 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 {Element} [event.origin] + The link or form element that caused the fragment update. + @param {Object} event.renderOptions Options for the `up.render()` call that will process the server response. @stable */ /*- @@ -13715,18 +14020,20 @@ 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 + Manually compiles a page fragment that has been inserted into the DOM by external code. + All registered [compilers](/up.compiler) and [macros](/up.macro) will be called + with matches in the given `element`. + **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 + need to call `up.hello()`.** 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`: + the `innerHTML` property: ```html element = document.createElement('div') element.innerHTML = '... HTML that needs to be activated ...' up.hello(element) @@ -13734,11 +14041,11 @@ This function emits the [`up:fragment:inserted`](/up:fragment:inserted) event. @function up.hello - @param {Element|jQuery} target + @param {Element|jQuery} element @param {Element|jQuery} [options.origin] @return {Element} The compiled element @stable */ @@ -14646,11 +14953,11 @@ function matches(element, selector, options = {}) { element = e.get(element); selector = parseSelector(selector, element, options); return selector.matches(element); } - up.on('up:app:boot', function () { + up.on('up:framework:boot', function () { const { body } = document; 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.'); @@ -14705,20 +15012,20 @@ */ u.delegate(up, 'context', () => up.layer.current); /***/ }), -/* 76 */ +/* 77 */ /***/ (() => { // extracted by mini-css-extract-plugin /***/ }), -/* 77 */ +/* 78 */ /***/ ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => { -__webpack_require__(78); +__webpack_require__(79); /*- Scrolling viewports =================== The `up.viewport` module controls the scroll position and focus within scrollable containers ("viewports"). @@ -15204,26 +15511,10 @@ }); function scrollTopKey(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 - */ - function scrollTops(options = {}) { - return u.mapObject(getAll(options), viewport => [scrollTopKey(viewport), viewport.scrollTop]); - } - /*- @function up.viewport.fixedElements @internal */ function fixedElements(root = document) { const queryParts = ['[up-fixed]'].concat(config.fixedTop).concat(config.fixedBottom); @@ -15257,10 +15548,23 @@ if (url) { const tops = options.tops ?? getScrollTops(viewports); options.layer.lastScrollTops.set(url, tops); } } + /*- + 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: + + getScrollTops() + => { '.main': 0, '.sidebar': 73 } + + @function up.viewport.getScrollTops + @return Object<string, number> + @internal + */ function getScrollTops(viewports) { return u.mapObject(viewports, viewport => [scrollTopKey(viewport), viewport.scrollTop]); } /*- Restores [previously saved](/up.viewport.saveScroll) scroll positions of viewports @@ -15518,27 +15822,28 @@ */ function pureHash(value) { return value?.replace(/^#/, ''); } let userScrolled = false; - up.on('scroll', { once: true }, () => userScrolled = true); - up.on('up:app:boot', () => // When the initial URL contains an #anchor link, the browser will automatically - - // reveal a matching fragment. We want to override that behavior with our own, - // so we can honor configured obstructions. Since we cannot disable the automatic - // browser behavior we need to ensure our code runs after it. - // - // In Chrome, when reloading, the browser behavior happens before DOMContentLoaded. - // However, when we follow a link with an #anchor URL, the browser - // behavior happens *after* DOMContentLoaded. Hence we wait one more task. - u.task(function () { - // If the user has scrolled while the page was loading, we will - // not reset their scroll position by revealing the #anchor fragment. - if (!userScrolled) { - return revealHash(); - } - })); + up.on('scroll', { once: true, beforeBoot: true }, () => userScrolled = true); + up.on('up:framework:boot', function () { + // When the initial URL contains an #anchor link, the browser will automatically + // reveal a matching fragment. We want to override that behavior with our own, + // so we can honor configured obstructions. Since we cannot disable the automatic + // browser behavior we need to ensure our code runs after it. + // + // In Chrome, when reloading, the browser behavior happens before DOMContentLoaded. + // However, when we follow a link with an #anchor URL, the browser + // behavior happens *after* DOMContentLoaded. Hence we wait one more task. + u.task(function () { + // If the user has scrolled while the page was loading, we will + // not reset their scroll position by revealing the #anchor fragment. + if (!userScrolled) { + return revealHash(); + } + }); + }); up.on(window, 'hashchange', () => revealHash()); up.on('up:framework:reset', reset); return { reveal, revealHash, @@ -15555,35 +15860,33 @@ rootHeight, rootHasReducedWidthFromScrollbar, rootOverflowElement, isRoot, scrollbarWidth, - scrollTops, saveScroll, restoreScroll, resetScroll, anchoredRight, - fixedElements, absolutize, focus: doFocus, tryFocus, - makeFocusable + makeFocusable, }; })(); up.focus = up.viewport.focus; up.scroll = up.viewport.scroll; up.reveal = up.viewport.reveal; /***/ }), -/* 78 */ +/* 79 */ /***/ (() => { // extracted by mini-css-extract-plugin /***/ }), -/* 79 */ +/* 80 */ /***/ (() => { /*- Animation ========= @@ -16083,11 +16386,11 @@ @param {Function(oldElement, newElement, options): Promise|Array} transition @stable */ function registerTransition(name, transition) { const fn = findTransitionFn(transition); - fn.isDefault = up.framework.booting; + fn.isDefault = up.framework.evaling; namedTransitions[name] = fn; } /*- Defines a named animation. @@ -16122,20 +16425,20 @@ @param {Function(element, options): Promise} animation @stable */ function registerAnimation(name, animation) { const fn = findAnimationFn(animation); - fn.isDefault = up.framework.booting; + fn.isDefault = up.framework.evaling; namedAnimations[name] = fn; } - function warnIfDisabled() { + up.on('up:framework:boot', function () { // Explain to the user why animations aren't working. // E.g. the user might have disabled animations in her OS. if (!isEnabled()) { up.puts('up.motion', 'Animations are disabled'); } - } + }); /*- Returns whether the given animation option will cause the animation to be skipped. @function up.motion.isNone @@ -16248,11 +16551,10 @@ 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, animate, finish, @@ -16271,14 +16573,14 @@ up.morph = up.motion.morph; up.animate = up.motion.animate; /***/ }), -/* 80 */ +/* 81 */ /***/ ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => { -__webpack_require__(81); +__webpack_require__(82); const u = up.util; /*- Network requests ================ @@ -16500,11 +16802,11 @@ ### 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. + The server may also clear the cache by sending an [`X-Up-Clear-Cache`](/X-Up-Clear-Cache) header. @function up.cache.clear @param {string} [pattern] A [URL pattern](/url-patterns) matching cache entries that should be cleared. @@ -16646,15 +16948,21 @@ 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. + You may also pass a [URL pattern](/url-patterns) to only uncache matching responses. + + @param {boolean|string|Function} [options.solo] + With `{ solo: true }` Unpoly will [abort](/up.network.abort) all other requests before making this new request. + + To only abort some requests, pass an [URL pattern](/url-patterns) that matches requests to abort. + You may also pass a function that accepts an existing `up.Request` and returns a boolean value. + @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.requestMetaKeys` for details. @@ -16680,10 +16988,15 @@ 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. + If this request is intended to update an existing fragment, this is that fragment's layer. + + If this request is intended to [open an overlay](/opening-overlays), + the associated layer is the future overlay's parent layer. + @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. @@ -16830,10 +17143,28 @@ @return {boolean} @stable */ const isIdle = u.negate(isBusy); /*- + 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.network.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 + */ + function loadPage(requestsAttrs) { + new up.Request(requestsAttrs).loadPage(); + } + /*- 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). @@ -16912,15 +17243,27 @@ 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. + The [layer](/up.layer) this request is associated with. + + If this request was intended to update an existing fragment, this is that fragment's layer. + + If this request was intended to [open an overlay](/opening-overlays), + the associated layer is the future overlay's parent layer. + + @param {Element} [event.origin] + The link or form element that caused the request. + @param event.preventDefault() + @experimental */ /*- This event is [emitted](/up.emit) when [AJAX requests](/up.request) are taking long to finish. @@ -16995,11 +17338,18 @@ @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. + The [layer](/up.layer) this request is associated with. + + If this request is intended to update an existing fragment, this is that fragment's layer. + + If this request is intended to [open an overlay](/opening-overlays), + the associated layer is the future overlay's parent layer. + @param {Element} [event.origin] + The link or form element that caused the request. @param event.preventDefault() Event listeners may call this method to prevent the request from being sent. @stable */ function registerAliasForRedirect(request, response) { @@ -17021,16 +17371,28 @@ [`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. + The [layer](/up.layer) this request is associated with. + + If this request is intended to update an existing fragment, this is that fragment's layer. + + If this request is intended to [open an overlay](/opening-overlays), + the associated layer is the future overlay's parent layer. + + @param {Element} [event.origin] + The link or form element 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. @@ -17040,14 +17402,25 @@ 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. + The failed request. + @param {up.Layer} [event.layer] - The [layer](/up.layer) that caused the request. + The [layer](/up.layer) this request is associated with. + + If this request was intended to update an existing fragment, this is that fragment's layer. + + If this request was intended to [open an overlay](/opening-overlays), + the associated layer is the future overlay's parent layer. + + @param {Element} [event.origin] + The link or form element that caused the request. + @stable */ function isSafeMethod(method) { return u.contains(['GET', 'OPTIONS', 'HEAD'], u.normalizeMethod(method)); } @@ -17071,28 +17444,29 @@ config, abort: abortRequests, registerAliasForRedirect, queue, shouldReduceRequests, - mimicLocalRequest + mimicLocalRequest, + loadPage, }; })(); up.request = up.network.request; up.cache = up.network.cache; /***/ }), -/* 81 */ +/* 82 */ /***/ (() => { // extracted by mini-css-extract-plugin /***/ }), -/* 82 */ +/* 83 */ /***/ ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => { -__webpack_require__(83); +__webpack_require__(84); const u = up.util; const e = up.element; /*- Layers ====== @@ -17784,10 +18158,12 @@ | `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 | + With a strict Content Security Policy [additional rules apply](/csp). + @param [up-on-dismissed] A JavaScript snippet that is called when the overlay was [dismissed](/closing-overlays). The snippet runs in the following scope: @@ -17796,10 +18172,12 @@ | `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 | + With a strict Content Security Policy [additional rules apply](/csp). + @param [up-accept-event] One or more space-separated 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) @@ -17921,11 +18299,14 @@ @stable */ up.on('up:fragment:destroyed', function () { stack.sync(); }); - up.on('up:framework:boot', function () { + up.on('up:framework:evaled', function () { + // Due to circular dependencies we must delay initialization of the stack until all of + // Unpoly's submodules have been evaled. We cannot delay initialization until up:framework:boot, + // since by then user scripts have run and event listeners will no longer register as "default". stack = new up.LayerStack(); }); up.on('up:framework:reset', reset); const api = { config, @@ -18288,20 +18669,20 @@ return api; })(); /***/ }), -/* 83 */ +/* 84 */ /***/ (() => { // extracted by mini-css-extract-plugin /***/ }), -/* 84 */ +/* 85 */ /***/ ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => { -__webpack_require__(85); +__webpack_require__(86); /*- Linking to fragments ==================== The `up.link` module lets you build links that update fragments instead of entire pages. @@ -18532,11 +18913,11 @@ const options = parseRequestOptions(link); if (options.url) { if (options.cache == null) { options.cache = 'auto'; } - options.basic = true; // + options.basic = true; const request = new up.Request(options); return request.willCache(); } } function isInstantDisabled(link) { @@ -18587,10 +18968,17 @@ of supported attributes. You may pass this additional `options` object to supplement or override options parsed from the link attributes. + @param {boolean} [options.navigate=true] + Whether this fragment update is considered [navigation](/navigation). + + Setting this to `false` will disable most defaults, causing + Unpoly to render a fragment without side-effects like updating history + or scrolling. + @return {Promise<up.RenderResult>} A promise that will be fulfilled when the link destination has been loaded and rendered. @stable @@ -18605,11 +18993,11 @@ options.method = followMethod(link, options); parser.json('headers'); parser.json('params'); parser.booleanOrString('cache'); parser.booleanOrString('clearCache'); - parser.boolean('solo'); + parser.booleanOrString('solo'); parser.string('contentType', { attr: ['enctype', 'up-content-type'] }); return options; } /*- Parses the [render](/up.render) options that would be used to @@ -18795,11 +19183,11 @@ */ function followMethod(link, options = {}) { return u.normalizeMethod(options.method || link.getAttribute('up-method') || link.getAttribute('data-method')); } function followURL(link, options = {}) { - const url = options.url || link.getAttribute('href') || link.getAttribute('up-href'); + const url = options.url || link.getAttribute('up-href') || link.getAttribute('href'); // Developers sometimes make a <a href="#"> to give a JavaScript interaction standard // link behavior (like keyboard navigation or default styles). However, we don't want to // consider this a link with remote content, and rather honor [up-content], [up-document] // and [up-fragment] attributes. if (url !== '#') { @@ -19060,10 +19448,17 @@ See [Handling all links and forms](/handling-everything). @selector a[up-follow] + @param [up-navigate='true'] + Whether this fragment update is considered [navigation](/navigation). + + Setting this to `false` will disable most defaults documented below, + causing Unpoly to render a fragment without side-effects like updating history + or scrolling. + @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. @@ -19071,22 +19466,19 @@ @param [up-target] The CSS selector to update. If omitted a [main target](/up-main) will be rendered. - @param [up-fallback] + @param [up-fallback='true'] 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. @@ -19107,12 +19499,19 @@ 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.requestMetaKeys` for details. + @param [up-content] + A string for the fragment's new [inner HTML](https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML). + + If your HTML string also contains the fragment's [outer HTML](https://developer.mozilla.org/en-US/docs/Web/API/Element/outerHTML), + consider the `[up-fragment]` attribute instead. + @param [up-fragment] - A string of HTML comprising *only* the new fragment. No server request will be sent. + A string of HTML comprising *only* the new fragment's + [outer HTML](https://developer.mozilla.org/en-US/docs/Web/API/Element/outerHTML). The `[up-target]` selector will be derived from the root element in the given HTML: ```html @@ -19145,11 +19544,11 @@ Any HTTP status code other than 2xx is considered an error code. See [handling server errors](/server-errors) for details. - @param [up-history] + @param [up-history='auto'] 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. @@ -19196,39 +19595,46 @@ The timing function that accelerates the transition or animation. See [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/CSS/transition-timing-function) for a list of available timing functions. - @param [up-cache] + @param [up-cache='auto'] Whether to read from and write to the [cache](/up.request#caching). 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. + With `[up-cache=false]` Unpoly will always make a network request. + Also see [`up.request({ cache })`](/up.request#options.cache). @param [up-clear-cache] Whether existing [cache](/up.request#caching) 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-solo='true'] + With `[up-solo=true]` Unpoly will [abort](/up.network.abort) all other requests before laoding the new fragment. + + To only abort some requests, pass an [URL pattern](/url-patterns) that matches requests to abort. + @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] + @param [up-peel='true'] 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] @@ -19239,21 +19645,21 @@ 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] + @param [up-scroll='auto'] 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-positions). - @param [up-focus] + @param [up-focus='auto'] What to focus after the new fragment was rendered. See [focus option](/focus-option) for a list of allowed values. @param [up-confirm] @@ -19261,24 +19667,28 @@ 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] + @param [up-feedback='true'] 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. + With a strict Content Security Policy [additional rules apply](/csp). + @param [up-on-finished] A JavaScript snippet that is called when all animations have concluded and elements were removed from the DOM tree. + With a strict Content Security Policy [additional rules apply](/csp). + @stable */ up.on('up:click', fullFollowSelector, function (event, link) { if (shouldFollowEvent(event, link)) { up.event.halt(event); @@ -19405,17 +19815,17 @@ })(); up.follow = up.link.follow; /***/ }), -/* 85 */ +/* 86 */ /***/ (() => { // extracted by mini-css-extract-plugin /***/ }), -/* 86 */ +/* 87 */ /***/ (() => { /*- Forms ===== @@ -19447,11 +19857,11 @@ An array of CSS selectors matching forms that will be [submitted through Unpoly](/form-up-submit). You can configure Unpoly to handle *all* forms on a page without requiring an `[up-submit]` attribute: ```js - up.form.config.submitSelectors.push('form'] + 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`. @@ -19589,10 +19999,15 @@ of supported attributes. You may pass this additional `options` object to supplement or override options parsed from the form attributes. + @param {boolean} [options.navigate=true] + Whether this fragment update is considered [navigation](/navigation). + + Setting this to `false` will disable most defaults. + @return {Promise<up.RenderResult>} A promise that will be fulfilled when the server response was rendered. @stable */ @@ -19782,21 +20197,30 @@ @stable */ const observe = function (elements, ...args) { elements = e.list(elements); const fields = u.flatMap(elements, findFields); + const unnamedFields = u.reject(fields, 'name'); + if (unnamedFields.length) { + // (1) We do not need to exclude the unnamed fields for up.FieldObserver, since that + // parses values with up.Params.fromFields(), and that ignores unnamed fields. + // (2) Only warn, don't crash. There are some legitimate cases for having unnamed + // a mix of named and unnamed fields in a form, and we don't want to prevent + // <form up-observe> in that case. + up.warn('up.observe()', 'Will not observe fields without a [name]: %o', unnamedFields); + } const callback = u.extractCallback(args) || observeCallbackFromElement(elements[0]) || up.fail('up.observe: No change callback given'); const options = u.extractOptions(args); options.delay = options.delay ?? e.numberAttr(elements[0], 'up-delay') ?? config.observeDelay; const observer = new up.FieldObserver(fields, options, callback); observer.start(); return () => observer.stop(); }; function observeCallbackFromElement(element) { let rawCallback = element.getAttribute('up-observe'); if (rawCallback) { - return new Function('value', 'name', rawCallback); + return up.NonceableCallback.fromString(rawCallback).toFunction('value', 'name'); } } /*- [Observes](/up.observe) a field or form and submits the form when a value changes. @@ -20135,11 +20559,11 @@ See [Handling all links and forms](/handling-everything). @selector form[up-submit] @params-note - All attributes for `a[up-follow]` may also be used. + All attributes for `a[up-follow]` may be used. @stable */ up.on('submit', fullSubmitSelector, function (event, form) { // Users may configure up.form.config.submitSelectors.push('form') @@ -20466,10 +20890,12 @@ 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). + With a strict Content Security Policy [additional rules apply](/csp). + The programmatic variant of this is the [`up.observe()`](/up.observe) function. ### Example The following would run a global `showSuggestions(value)` function @@ -20527,10 +20953,12 @@ 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). + With a strict Content Security Policy [additional rules apply](/csp). + The programmatic variant of this is the [`up.observe()`](/up.observe) function. ### Example The would call a function `somethingChanged(value)` @@ -20646,11 +21074,11 @@ up.autosubmit = up.form.autosubmit; up.validate = up.form.validate; /***/ }), -/* 87 */ +/* 88 */ /***/ (() => { /*- Navigation feedback =================== @@ -21082,11 +21510,11 @@ }; })(); /***/ }), -/* 88 */ +/* 89 */ /***/ (() => { /*- Passive updates =============== @@ -21295,11 +21723,11 @@ }; })(); /***/ }), -/* 89 */ +/* 90 */ /***/ (() => { /* Play nice with Rails UJS ======================== @@ -21434,19 +21862,20 @@ __webpack_require__(71); __webpack_require__(72); __webpack_require__(73); __webpack_require__(74); __webpack_require__(75); -__webpack_require__(77); -__webpack_require__(79); +__webpack_require__(76); +__webpack_require__(78); __webpack_require__(80); -__webpack_require__(82); -__webpack_require__(84); -__webpack_require__(86); +__webpack_require__(81); +__webpack_require__(83); +__webpack_require__(85); __webpack_require__(87); __webpack_require__(88); __webpack_require__(89); -up.framework.boot(); +__webpack_require__(90); +up.framework.onEvaled(); })(); /******/ })() ; \ No newline at end of file