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