assets/unpoly/unpoly.js in unpoly-rails-2.4.1 vs assets/unpoly/unpoly.js in unpoly-rails-2.5.0

- old
+ new

@@ -6,11 +6,11 @@ /*** @module up */ window.up = { - version: '2.4.1' + version: '2.5.0' }; /***/ }), /* 2 */ @@ -2961,12 +2961,16 @@ // create intert <script> elements. // (3) Using Range#createContextualFragment() is significantly faster than setting // innerHTML on Chrome. See https://jsben.ch/QQngJ const range = document.createRange(); range.setStart(document.body, 0); - const fragment = range.createContextualFragment(html); - return fragment.childNodes[0]; + const fragment = range.createContextualFragment(html.trim()); + let elements = fragment.childNodes; + if (elements.length !== 1) { + throw new Error('HTML must have a single root element'); + } + return elements[0]; } /*- @function up.element.root @internal */ @@ -4339,10 +4343,13 @@ partnerSelector = '&'; } const lookupOpts = { layer: this.layer, origin: oldElement }; let partner; if (options.descendantsOnly) { + // Since newElement is from a freshly parsed HTML document, we could use + // up.element functions to match the selector. However, since we also want + // to use custom selectors like ":main" or "&" we use up.fragment.get(). partner = up.fragment.get(newElement, partnerSelector, lookupOpts); } else { partner = up.fragment.subtree(newElement, partnerSelector, lookupOpts)[0]; } @@ -4826,15 +4833,15 @@ else if (this.isSuccessfulResponse()) { return this.updateContentFromResponse(['Loaded fragment from successful response to %s', this.request.description], this.successOptions); } else { const log = ['Loaded fragment from failed response to %s (HTTP %d)', this.request.description, this.response.status]; + // Although updateContentFromResponse() will fulfill with a successful replacement of options.failTarget, + // we still want to reject the promise that's returned to our API client. Hence we throw. throw this.updateContentFromResponse(log, this.failOptions); } } - // 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 } @@ -10309,11 +10316,11 @@ const u = up.util; up.store || (up.store = {}); up.store.Memory = class Memory { constructor() { - this.clear(); + this.data = {}; } clear() { this.data = {}; } get(key) { @@ -10618,11 +10625,11 @@ this.groups.push({ name, cast: String }); } return '([^/?#]+)'; } }); - return new RegExp('^' + reCode + '$'); + return new RegExp('^(?:' + reCode + ')$'); } // This method is performance-sensitive. It's called for every link in an [up-nav] // after every fragment update. test(url, doNormalize = true) { if (doNormalize) { @@ -12259,15 +12266,18 @@ 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} [config.banner=true] Print the Unpoly banner to the developer console. + @param {boolean} [config.format=!isIE11] + Format output using CSS. @stable */ const config = new up.Config(() => ({ enabled: sessionStore.get('enabled'), - banner: true + banner: true, + format: up.browser.canFormatLog() })); function reset() { config.reset(); } // ###** @@ -12304,11 +12314,11 @@ @internal */ const printToError = (...args) => printToStream('error', ...args); function printToStream(stream, trace, message, ...args) { if (message) { - if (up.browser.canFormatLog()) { + if (config.format) { args.unshift(''); // Reset 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 { @@ -12335,11 +12345,11 @@ } else { text += "Call `up.log.enable()` to enable logging for this session."; } const color = 'color: #777777'; - if (up.browser.canFormatLog()) { + if (config.format) { console.log('%c' + logo + '%c' + text, 'font-family: monospace;' + color, color); } else { console.log(logo + text); } @@ -12974,13 +12984,15 @@ /*- 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. + A list of possible CSS selectors to [replace](/up.render) when the user goes back or forward in history. - By default the [root layer's main target](/up.fragment.config#config.mainTargets). + If more than one target is configured, the first selector matching both the current page and server response will be updated. + + If nothing is configured, the `<body>` element will be replaced. @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] @@ -13194,11 +13206,10 @@ // We will close all overlays and update the root layer. peel: true, layer: 'root', target: config.restoreTargets, cache: true, - keep: false, scroll: 'restore', // Since the URL was already changed by the browser, don't save scroll state. saveScroll: false }); url = currentLocation(); @@ -13340,11 +13351,12 @@ @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 no other render target is given, Unpoly will update the first selector matching both + the current page and the server response. When [navigating](/navigation) to a main target, Unpoly will automatically [reset scroll positions](/scroll-option) and [update the browser history](/up.render#options.history). @@ -16085,11 +16097,10 @@ You can pass additional options: ```js up.animate('.warning', 'fade-in', { - delay: 1000, duration: 250, easing: 'linear' }) ``` @@ -18083,11 +18094,11 @@ @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. + This event is emitted after a new overlay was 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): @@ -18170,11 +18181,11 @@ else { return option.toString(); } } /*- - [Follows](/a-up-follow) this link and opens the result in a new overlay. + [Follows](/a-up-follow) this link and [opens the result in a new overlay](/opening-overlays). ### Example ```html <a href="/menu" up-layer="new">Open menu</a> @@ -18549,10 +18560,38 @@ @param {any} [value] @param {Object} [options] @stable */ /*- + This event is emitted before a layer is [accepted](/closing-overlays). + + The event is emitted on the [element of the layer](/up.layer.element) that is about to close. + + @event up:layer:accept + @param {up.Layer} event.layer + The layer that is about to close. + @param {Element} [event.origin] + The element that is causing the layer to close. + @param event.preventDefault() + Event listeners may call this method to prevent the overlay from closing. + @stable + */ + /*- + This event is emitted after a layer was [accepted](/closing-overlays). + + The event is emitted on the [layer's](/up.layer.element) when the close animation + is starting. If the layer has no close animaton and was already removed from the DOM, + the event is emitted a second time on the `document`. + + @event up:layer:accepted + @param {up.Layer} event.layer + The layer that was closed. + @param {Element} [event.origin] + The element that has caused the layer to close. + @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. @@ -18560,10 +18599,38 @@ @param {any} [value] @param {Object} [options] @stable */ /*- + This event is emitted before a layer is [dismissed](/closing-overlays). + + The event is emitted on the [element of the layer](/up.layer.element) that is about to close. + + @event up:layer:dismiss + @param {up.Layer} event.layer + The layer that is about to close. + @param {Element} [event.origin] + The element that is causing the layer to close. + @param event.preventDefault() + Event listeners may call this method to prevent the overlay from closing. + @stable + */ + /*- + This event is emitted after a layer was [dismissed](/closing-overlays). + + The event is emitted on the [layer's](/up.layer.element) when the close animation + is starting. If the layer has no close animaton and was already removed from the DOM, + the event is emitted a second time on the `document`. + + @event up:layer:dismissed + @param {up.Layer} event.layer + The layer that was closed. + @param {Element} [event.origin] + The element that has caused the layer to close. + @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.. @@ -19117,10 +19184,11 @@ @stable */ function followOptions(link, options) { // If passed a selector, up.fragment.get() will prefer a match on the current layer. link = up.fragment.get(link); + // Request options options = parseRequestOptions(link, options); const parser = new up.OptionsParser(options, link, { fail: true }); // Feedback options parser.boolean('feedback'); // Fragment options @@ -19333,11 +19401,12 @@ if (e.matches(link, 'a[href], button')) { return; } e.setMissingAttrs(link, { tabindex: '0', - role: 'link' // Make screen readers pronounce "link" + role: 'link', + 'up-clickable': '' // Get pointer pointer from link.css }); link.addEventListener('keydown', function (event) { if ((event.key === 'Enter') || (event.key === 'Space')) { return forkEventAsUpClick(event); } @@ -19864,10 +19933,14 @@ if (!areaAttrs['up-href']) { areaAttrs['up-href'] = childLink.getAttribute('href'); } e.setMissingAttrs(area, areaAttrs); makeFollowable(area); + // We could also consider making the area clickable, via makeClickable(). + // However, since the original link is already present within the area, + // we would not add accessibility benefits. We might also confuse screen readers + // with a nested link. } }); /*- Preloads this link when the user hovers over it. @@ -20138,25 +20211,44 @@ @function up.form.submitOptions @return {Object} @stable */ function submitOptions(form, options) { + form = getForm(form); + options = parseBasicOptions(form, options); + let parser = new up.OptionsParser(options, form); + parser.string('failTarget', { default: up.fragment.toTarget(form) }); + // The guardEvent will also be assigned an { renderOptions } property in up.render() + options.guardEvent || (options.guardEvent = up.event.build('up:form:submit', { + submitButton: options.submitButton, + params: options.params, + log: 'Submitting form' + })); + // Now that we have extracted everything form-specific into options, we can call + // up.link.followOptions(). This will also parse the myriads of other options + // that are possible on both <form> and <a> elements. + u.assign(options, up.link.followOptions(form, options)); + return options; + } + // This was extracted from submitOptions(). + // Validation needs to submit a form without options intended for the final submission, + // like [up-scroll], [up-confirm], etc. + function parseBasicOptions(form, options) { options = u.options(options); - form = up.fragment.get(form); - form = e.closest(form, 'form'); + form = getForm(form); const parser = new up.OptionsParser(options, form); // Parse params from form fields. const params = up.Params.fromForm(form); - let submitButton = submittingButton(form); - if (submitButton) { + options.submitButton || (options.submitButton = submittingButton(form)); + if (options.submitButton) { // Submit buttons with a [name] attribute will add to the params. // Note that addField() will only add an entry if the given button has a [name] attribute. - params.addField(submitButton); + params.addField(options.submitButton); // Submit buttons may have [formmethod] and [formaction] attribute // that override [method] and [action] attribute from the <form> element. - options.method || (options.method = submitButton.getAttribute('formmethod')); - options.url || (options.url = submitButton.getAttribute('formaction')); + options.method || (options.method = options.submitButton.getAttribute('formmethod')); + options.url || (options.url = options.submitButton.getAttribute('formaction')); } params.addAll(options.params); options.params = params; parser.string('url', { attr: 'action', default: up.fragment.source(form) }); parser.string('method', { @@ -20169,17 +20261,10 @@ // The URLs search part will be replaced with the serialized form data. // See design/query-params-in-form-actions/cases.html for // a demo of vanilla browser behavior. options.url = up.Params.stripURL(options.url); } - parser.string('failTarget', { default: up.fragment.toTarget(form) }); - // The guardEvent will also be assigned an { renderOptions } property in up.render() - options.guardEvent || (options.guardEvent = up.event.build('up:form:submit', { log: 'Submitting form' })); - // Now that we have extracted everything form-specific into options, we can call - // up.link.followOptions(). This will also parse the myriads of other options - // that are possible on both <form> and <a> elements. - 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. @@ -20202,12 +20287,16 @@ ``` @event up:form:submit @param {Element} event.target The `<form>` element that will be submitted. + @param {up.Params} event.params + The [form parameters](/up.Params) that will be send as the form's request payload. + @param {Element} [event.submitButton] + The button used to submit the form. @param {Object} event.renderOptions - An object with [render options](/up.render) for the fragment update + 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 @@ -20216,11 +20305,11 @@ // That means that submittingButton() cannot rely on document.activeElement. // See https://github.com/unpoly/unpoly/issues/103 up.on('up:click', submitButtonSelector, function (event, button) { // Don't mess with focus unless we know that we're going to handle the form. // https://groups.google.com/g/unpoly/c/wsiATxepVZk - const form = e.closest(button, 'form'); + const form = getForm(button); if (form && isSubmittable(form)) { button.focus(); } }); /*- @@ -20285,11 +20374,11 @@ it is already running. @return {Function()} A destructor function that removes the observe watch when called. @stable */ - const observe = function (elements, ...args) { + function observe(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 @@ -20303,11 +20392,11 @@ 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 up.NonceableCallback.fromString(rawCallback).toFunction('value', 'name'); } @@ -20397,14 +20486,12 @@ @stable */ function validate(field, options) { // If passed a selector, up.fragment.get() will prefer a match on the current layer. field = up.fragment.get(field); - options = u.options(options); - options.navigate = false; + options = parseBasicOptions(field, options); options.origin = field; - options.history = false; options.target = findValidateTarget(field, options); options.focus = 'keep'; // The protocol doesn't define whether the validation results in a status code. // Hence we use the same options for both success and failure. options.fail = false; @@ -20412,11 +20499,11 @@ // knows that it should not persist the form submission options.headers || (options.headers = {}); options.headers[up.protocol.headerize('validate')] = field.getAttribute('name') || ':unknown'; // The guardEvent will also be assigned a { renderOptions } attribute in up.render() options.guardEvent = up.event.build('up:form:validate', { field, log: 'Validating form' }); - return submit(field, options); + return up.render(options); } /*- This event is emitted before a field is being [validated](/input-up-validate). @event up:form:validate @@ -20512,23 +20599,27 @@ // is checked or entered. showValues = showValues ? u.splitValues(showValues) : [':present', ':checked']; show = u.intersect(fieldValues, showValues).length > 0; } e.toggle(target, show); - return target.classList.add('up-switched'); + target.classList.add('up-switched'); }); function findSwitcherForTarget(target) { const form = getContainer(target); const switchers = e.all(form, '[up-switch]'); const switcher = u.find(switchers, function (switcher) { const targetSelector = switcher.getAttribute('up-switch'); return e.matches(target, targetSelector); }); return switcher || up.fail('Could not find [up-switch] field for %o', target); } - function getContainer(element) { + function getForm(elementOrTarget, fallbackSelector) { + const element = up.fragment.get(elementOrTarget); // Element#form will also work if the element is outside the form with an [form=form-id] attribute - return element.form || e.closest(element, `form, ${up.layer.anySelector()}`); + return element.form || e.closest(element, 'form') || (fallbackSelector && e.closest(element, fallbackSelector)); + } + function getContainer(element) { + return getForm(element, up.layer.anySelector()); } function isField(element) { return e.matches(element, fieldSelector()); } function focusedField() { \ No newline at end of file