rails/actionview/app/assets/javascripts/rails-ujs.esm.js
Hartley McGuire f761780899
Fix rails-ujs auto start() in bundled environments
An issue was recently raised that users were seeing an error when
upgrading `@rails/ujs` to 7.1.0+:

```
Uncaught Error: rails-ujs has already been loaded!
```

Generally this issue appears due to the difference in how rails-ujs is
expected to behave in a pure sprockets environment (js appending) vs a
bundled environment (webpack, esbuild, etc.). The sprockets environment
is supposed to call start() automatically, while the bundled environment
is supposed to require users to import and call start() themselves.

As part of the transition from coffeescript to javascript, a condition
was added that was intended to detect whether the current environment
was a bundler so that it would only call start() when in sprockets.
However, this condition is not working as expected, and I was able to
reproduce the error appearing when using rails-ujs with importmaps and
esbuild. Interestingly, the issue did not appear when using Webpack as a
bundler.

I believe the best fix here is to make the condition very explicit.
Since sprockets users should not be using the esm version of rails-ujs,
we can use the rollup replace plugin to explicitly opt the esm bundle
out of _ever_ calling start() automatically. This works because terser
will run after the replace plugin and remove the whole condition as dead
code (since it sees true == false).

Co-authored-by: Ryunosuke Sato <tricknotes.rs@gmail.com>
2023-10-17 20:47:12 -04:00

687 lines
22 KiB
JavaScript
Generated

/*
Unobtrusive JavaScript
https://github.com/rails/rails/blob/main/actionview/app/javascript
Released under the MIT license
*/
const linkClickSelector = "a[data-confirm], a[data-method], a[data-remote]:not([disabled]), a[data-disable-with], a[data-disable]";
const buttonClickSelector = {
selector: "button[data-remote]:not([form]), button[data-confirm]:not([form])",
exclude: "form button"
};
const inputChangeSelector = "select[data-remote], input[data-remote], textarea[data-remote]";
const formSubmitSelector = "form:not([data-turbo=true])";
const formInputClickSelector = "form:not([data-turbo=true]) input[type=submit], form:not([data-turbo=true]) input[type=image], form:not([data-turbo=true]) button[type=submit], form:not([data-turbo=true]) button:not([type]), input[type=submit][form], input[type=image][form], button[type=submit][form], button[form]:not([type])";
const formDisableSelector = "input[data-disable-with]:enabled, button[data-disable-with]:enabled, textarea[data-disable-with]:enabled, input[data-disable]:enabled, button[data-disable]:enabled, textarea[data-disable]:enabled";
const formEnableSelector = "input[data-disable-with]:disabled, button[data-disable-with]:disabled, textarea[data-disable-with]:disabled, input[data-disable]:disabled, button[data-disable]:disabled, textarea[data-disable]:disabled";
const fileInputSelector = "input[name][type=file]:not([disabled])";
const linkDisableSelector = "a[data-disable-with], a[data-disable]";
const buttonDisableSelector = "button[data-remote][data-disable-with], button[data-remote][data-disable]";
let nonce = null;
const loadCSPNonce = () => {
const metaTag = document.querySelector("meta[name=csp-nonce]");
return nonce = metaTag && metaTag.content;
};
const cspNonce = () => nonce || loadCSPNonce();
const m = Element.prototype.matches || Element.prototype.matchesSelector || Element.prototype.mozMatchesSelector || Element.prototype.msMatchesSelector || Element.prototype.oMatchesSelector || Element.prototype.webkitMatchesSelector;
const matches = function(element, selector) {
if (selector.exclude) {
return m.call(element, selector.selector) && !m.call(element, selector.exclude);
} else {
return m.call(element, selector);
}
};
const EXPANDO = "_ujsData";
const getData = (element, key) => element[EXPANDO] ? element[EXPANDO][key] : undefined;
const setData = function(element, key, value) {
if (!element[EXPANDO]) {
element[EXPANDO] = {};
}
return element[EXPANDO][key] = value;
};
const $ = selector => Array.prototype.slice.call(document.querySelectorAll(selector));
const isContentEditable = function(element) {
var isEditable = false;
do {
if (element.isContentEditable) {
isEditable = true;
break;
}
element = element.parentElement;
} while (element);
return isEditable;
};
const csrfToken = () => {
const meta = document.querySelector("meta[name=csrf-token]");
return meta && meta.content;
};
const csrfParam = () => {
const meta = document.querySelector("meta[name=csrf-param]");
return meta && meta.content;
};
const CSRFProtection = xhr => {
const token = csrfToken();
if (token) {
return xhr.setRequestHeader("X-CSRF-Token", token);
}
};
const refreshCSRFTokens = () => {
const token = csrfToken();
const param = csrfParam();
if (token && param) {
return $('form input[name="' + param + '"]').forEach((input => input.value = token));
}
};
const AcceptHeaders = {
"*": "*/*",
text: "text/plain",
html: "text/html",
xml: "application/xml, text/xml",
json: "application/json, text/javascript",
script: "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"
};
const ajax = options => {
options = prepareOptions(options);
var xhr = createXHR(options, (function() {
const response = processResponse(xhr.response != null ? xhr.response : xhr.responseText, xhr.getResponseHeader("Content-Type"));
if (Math.floor(xhr.status / 100) === 2) {
if (typeof options.success === "function") {
options.success(response, xhr.statusText, xhr);
}
} else {
if (typeof options.error === "function") {
options.error(response, xhr.statusText, xhr);
}
}
return typeof options.complete === "function" ? options.complete(xhr, xhr.statusText) : undefined;
}));
if (options.beforeSend && !options.beforeSend(xhr, options)) {
return false;
}
if (xhr.readyState === XMLHttpRequest.OPENED) {
return xhr.send(options.data);
}
};
var prepareOptions = function(options) {
options.url = options.url || location.href;
options.type = options.type.toUpperCase();
if (options.type === "GET" && options.data) {
if (options.url.indexOf("?") < 0) {
options.url += "?" + options.data;
} else {
options.url += "&" + options.data;
}
}
if (!(options.dataType in AcceptHeaders)) {
options.dataType = "*";
}
options.accept = AcceptHeaders[options.dataType];
if (options.dataType !== "*") {
options.accept += ", */*; q=0.01";
}
return options;
};
var createXHR = function(options, done) {
const xhr = new XMLHttpRequest;
xhr.open(options.type, options.url, true);
xhr.setRequestHeader("Accept", options.accept);
if (typeof options.data === "string") {
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
}
if (!options.crossDomain) {
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
CSRFProtection(xhr);
}
xhr.withCredentials = !!options.withCredentials;
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.DONE) {
return done(xhr);
}
};
return xhr;
};
var processResponse = function(response, type) {
if (typeof response === "string" && typeof type === "string") {
if (type.match(/\bjson\b/)) {
try {
response = JSON.parse(response);
} catch (error) {}
} else if (type.match(/\b(?:java|ecma)script\b/)) {
const script = document.createElement("script");
script.setAttribute("nonce", cspNonce());
script.text = response;
document.head.appendChild(script).parentNode.removeChild(script);
} else if (type.match(/\b(xml|html|svg)\b/)) {
const parser = new DOMParser;
type = type.replace(/;.+/, "");
try {
response = parser.parseFromString(response, type);
} catch (error1) {}
}
}
return response;
};
const href = element => element.href;
const isCrossDomain = function(url) {
const originAnchor = document.createElement("a");
originAnchor.href = location.href;
const urlAnchor = document.createElement("a");
try {
urlAnchor.href = url;
return !((!urlAnchor.protocol || urlAnchor.protocol === ":") && !urlAnchor.host || originAnchor.protocol + "//" + originAnchor.host === urlAnchor.protocol + "//" + urlAnchor.host);
} catch (e) {
return true;
}
};
let preventDefault;
let {CustomEvent: CustomEvent} = window;
if (typeof CustomEvent !== "function") {
CustomEvent = function(event, params) {
const evt = document.createEvent("CustomEvent");
evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
return evt;
};
CustomEvent.prototype = window.Event.prototype;
({preventDefault: preventDefault} = CustomEvent.prototype);
CustomEvent.prototype.preventDefault = function() {
const result = preventDefault.call(this);
if (this.cancelable && !this.defaultPrevented) {
Object.defineProperty(this, "defaultPrevented", {
get() {
return true;
}
});
}
return result;
};
}
const fire = (obj, name, data) => {
const event = new CustomEvent(name, {
bubbles: true,
cancelable: true,
detail: data
});
obj.dispatchEvent(event);
return !event.defaultPrevented;
};
const stopEverything = e => {
fire(e.target, "ujs:everythingStopped");
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
};
const delegate = (element, selector, eventType, handler) => element.addEventListener(eventType, (function(e) {
let {target: target} = e;
while (!!(target instanceof Element) && !matches(target, selector)) {
target = target.parentNode;
}
if (target instanceof Element && handler.call(target, e) === false) {
e.preventDefault();
e.stopPropagation();
}
}));
const toArray = e => Array.prototype.slice.call(e);
const serializeElement = (element, additionalParam) => {
let inputs = [ element ];
if (matches(element, "form")) {
inputs = toArray(element.elements);
}
const params = [];
inputs.forEach((function(input) {
if (!input.name || input.disabled) {
return;
}
if (matches(input, "fieldset[disabled] *")) {
return;
}
if (matches(input, "select")) {
toArray(input.options).forEach((function(option) {
if (option.selected) {
params.push({
name: input.name,
value: option.value
});
}
}));
} else if (input.checked || [ "radio", "checkbox", "submit" ].indexOf(input.type) === -1) {
params.push({
name: input.name,
value: input.value
});
}
}));
if (additionalParam) {
params.push(additionalParam);
}
return params.map((function(param) {
if (param.name) {
return `${encodeURIComponent(param.name)}=${encodeURIComponent(param.value)}`;
} else {
return param;
}
})).join("&");
};
const formElements = (form, selector) => {
if (matches(form, "form")) {
return toArray(form.elements).filter((el => matches(el, selector)));
} else {
return toArray(form.querySelectorAll(selector));
}
};
const handleConfirmWithRails = rails => function(e) {
if (!allowAction(this, rails)) {
stopEverything(e);
}
};
const confirm = (message, element) => window.confirm(message);
var allowAction = function(element, rails) {
let callback;
const message = element.getAttribute("data-confirm");
if (!message) {
return true;
}
let answer = false;
if (fire(element, "confirm")) {
try {
answer = rails.confirm(message, element);
} catch (error) {}
callback = fire(element, "confirm:complete", [ answer ]);
}
return answer && callback;
};
const handleDisabledElement = function(e) {
const element = this;
if (element.disabled) {
stopEverything(e);
}
};
const enableElement = e => {
let element;
if (e instanceof Event) {
if (isXhrRedirect(e)) {
return;
}
element = e.target;
} else {
element = e;
}
if (isContentEditable(element)) {
return;
}
if (matches(element, linkDisableSelector)) {
return enableLinkElement(element);
} else if (matches(element, buttonDisableSelector) || matches(element, formEnableSelector)) {
return enableFormElement(element);
} else if (matches(element, formSubmitSelector)) {
return enableFormElements(element);
}
};
const disableElement = e => {
const element = e instanceof Event ? e.target : e;
if (isContentEditable(element)) {
return;
}
if (matches(element, linkDisableSelector)) {
return disableLinkElement(element);
} else if (matches(element, buttonDisableSelector) || matches(element, formDisableSelector)) {
return disableFormElement(element);
} else if (matches(element, formSubmitSelector)) {
return disableFormElements(element);
}
};
var disableLinkElement = function(element) {
if (getData(element, "ujs:disabled")) {
return;
}
const replacement = element.getAttribute("data-disable-with");
if (replacement != null) {
setData(element, "ujs:enable-with", element.innerHTML);
element.innerHTML = replacement;
}
element.addEventListener("click", stopEverything);
return setData(element, "ujs:disabled", true);
};
var enableLinkElement = function(element) {
const originalText = getData(element, "ujs:enable-with");
if (originalText != null) {
element.innerHTML = originalText;
setData(element, "ujs:enable-with", null);
}
element.removeEventListener("click", stopEverything);
return setData(element, "ujs:disabled", null);
};
var disableFormElements = form => formElements(form, formDisableSelector).forEach(disableFormElement);
var disableFormElement = function(element) {
if (getData(element, "ujs:disabled")) {
return;
}
const replacement = element.getAttribute("data-disable-with");
if (replacement != null) {
if (matches(element, "button")) {
setData(element, "ujs:enable-with", element.innerHTML);
element.innerHTML = replacement;
} else {
setData(element, "ujs:enable-with", element.value);
element.value = replacement;
}
}
element.disabled = true;
return setData(element, "ujs:disabled", true);
};
var enableFormElements = form => formElements(form, formEnableSelector).forEach((element => enableFormElement(element)));
var enableFormElement = function(element) {
const originalText = getData(element, "ujs:enable-with");
if (originalText != null) {
if (matches(element, "button")) {
element.innerHTML = originalText;
} else {
element.value = originalText;
}
setData(element, "ujs:enable-with", null);
}
element.disabled = false;
return setData(element, "ujs:disabled", null);
};
var isXhrRedirect = function(event) {
const xhr = event.detail ? event.detail[0] : undefined;
return xhr && xhr.getResponseHeader("X-Xhr-Redirect");
};
const handleMethodWithRails = rails => function(e) {
const link = this;
const method = link.getAttribute("data-method");
if (!method) {
return;
}
if (isContentEditable(this)) {
return;
}
const href = rails.href(link);
const csrfToken$1 = csrfToken();
const csrfParam$1 = csrfParam();
const form = document.createElement("form");
let formContent = `<input name='_method' value='${method}' type='hidden' />`;
if (csrfParam$1 && csrfToken$1 && !isCrossDomain(href)) {
formContent += `<input name='${csrfParam$1}' value='${csrfToken$1}' type='hidden' />`;
}
formContent += '<input type="submit" />';
form.method = "post";
form.action = href;
form.target = link.target;
form.innerHTML = formContent;
form.style.display = "none";
document.body.appendChild(form);
form.querySelector('[type="submit"]').click();
stopEverything(e);
};
const isRemote = function(element) {
const value = element.getAttribute("data-remote");
return value != null && value !== "false";
};
const handleRemoteWithRails = rails => function(e) {
let data, method, url;
const element = this;
if (!isRemote(element)) {
return true;
}
if (!fire(element, "ajax:before")) {
fire(element, "ajax:stopped");
return false;
}
if (isContentEditable(element)) {
fire(element, "ajax:stopped");
return false;
}
const withCredentials = element.getAttribute("data-with-credentials");
const dataType = element.getAttribute("data-type") || "script";
if (matches(element, formSubmitSelector)) {
const button = getData(element, "ujs:submit-button");
method = getData(element, "ujs:submit-button-formmethod") || element.getAttribute("method") || "get";
url = getData(element, "ujs:submit-button-formaction") || element.getAttribute("action") || location.href;
if (method.toUpperCase() === "GET") {
url = url.replace(/\?.*$/, "");
}
if (element.enctype === "multipart/form-data") {
data = new FormData(element);
if (button != null) {
data.append(button.name, button.value);
}
} else {
data = serializeElement(element, button);
}
setData(element, "ujs:submit-button", null);
setData(element, "ujs:submit-button-formmethod", null);
setData(element, "ujs:submit-button-formaction", null);
} else if (matches(element, buttonClickSelector) || matches(element, inputChangeSelector)) {
method = element.getAttribute("data-method");
url = element.getAttribute("data-url");
data = serializeElement(element, element.getAttribute("data-params"));
} else {
method = element.getAttribute("data-method");
url = rails.href(element);
data = element.getAttribute("data-params");
}
ajax({
type: method || "GET",
url: url,
data: data,
dataType: dataType,
beforeSend(xhr, options) {
if (fire(element, "ajax:beforeSend", [ xhr, options ])) {
return fire(element, "ajax:send", [ xhr ]);
} else {
fire(element, "ajax:stopped");
return false;
}
},
success(...args) {
return fire(element, "ajax:success", args);
},
error(...args) {
return fire(element, "ajax:error", args);
},
complete(...args) {
return fire(element, "ajax:complete", args);
},
crossDomain: isCrossDomain(url),
withCredentials: withCredentials != null && withCredentials !== "false"
});
stopEverything(e);
};
const formSubmitButtonClick = function(e) {
const button = this;
const {form: form} = button;
if (!form) {
return;
}
if (button.name) {
setData(form, "ujs:submit-button", {
name: button.name,
value: button.value
});
}
setData(form, "ujs:formnovalidate-button", button.formNoValidate);
setData(form, "ujs:submit-button-formaction", button.getAttribute("formaction"));
return setData(form, "ujs:submit-button-formmethod", button.getAttribute("formmethod"));
};
const preventInsignificantClick = function(e) {
const link = this;
const method = (link.getAttribute("data-method") || "GET").toUpperCase();
const data = link.getAttribute("data-params");
const metaClick = e.metaKey || e.ctrlKey;
const insignificantMetaClick = metaClick && method === "GET" && !data;
const nonPrimaryMouseClick = e.button != null && e.button !== 0;
if (nonPrimaryMouseClick || insignificantMetaClick) {
e.stopImmediatePropagation();
}
};
const Rails = {
$: $,
ajax: ajax,
buttonClickSelector: buttonClickSelector,
buttonDisableSelector: buttonDisableSelector,
confirm: confirm,
cspNonce: cspNonce,
csrfToken: csrfToken,
csrfParam: csrfParam,
CSRFProtection: CSRFProtection,
delegate: delegate,
disableElement: disableElement,
enableElement: enableElement,
fileInputSelector: fileInputSelector,
fire: fire,
formElements: formElements,
formEnableSelector: formEnableSelector,
formDisableSelector: formDisableSelector,
formInputClickSelector: formInputClickSelector,
formSubmitButtonClick: formSubmitButtonClick,
formSubmitSelector: formSubmitSelector,
getData: getData,
handleDisabledElement: handleDisabledElement,
href: href,
inputChangeSelector: inputChangeSelector,
isCrossDomain: isCrossDomain,
linkClickSelector: linkClickSelector,
linkDisableSelector: linkDisableSelector,
loadCSPNonce: loadCSPNonce,
matches: matches,
preventInsignificantClick: preventInsignificantClick,
refreshCSRFTokens: refreshCSRFTokens,
serializeElement: serializeElement,
setData: setData,
stopEverything: stopEverything
};
const handleConfirm = handleConfirmWithRails(Rails);
Rails.handleConfirm = handleConfirm;
const handleMethod = handleMethodWithRails(Rails);
Rails.handleMethod = handleMethod;
const handleRemote = handleRemoteWithRails(Rails);
Rails.handleRemote = handleRemote;
const start = function() {
if (window._rails_loaded) {
throw new Error("rails-ujs has already been loaded!");
}
window.addEventListener("pageshow", (function() {
$(formEnableSelector).forEach((function(el) {
if (getData(el, "ujs:disabled")) {
enableElement(el);
}
}));
$(linkDisableSelector).forEach((function(el) {
if (getData(el, "ujs:disabled")) {
enableElement(el);
}
}));
}));
delegate(document, linkDisableSelector, "ajax:complete", enableElement);
delegate(document, linkDisableSelector, "ajax:stopped", enableElement);
delegate(document, buttonDisableSelector, "ajax:complete", enableElement);
delegate(document, buttonDisableSelector, "ajax:stopped", enableElement);
delegate(document, linkClickSelector, "click", preventInsignificantClick);
delegate(document, linkClickSelector, "click", handleDisabledElement);
delegate(document, linkClickSelector, "click", handleConfirm);
delegate(document, linkClickSelector, "click", disableElement);
delegate(document, linkClickSelector, "click", handleRemote);
delegate(document, linkClickSelector, "click", handleMethod);
delegate(document, buttonClickSelector, "click", preventInsignificantClick);
delegate(document, buttonClickSelector, "click", handleDisabledElement);
delegate(document, buttonClickSelector, "click", handleConfirm);
delegate(document, buttonClickSelector, "click", disableElement);
delegate(document, buttonClickSelector, "click", handleRemote);
delegate(document, inputChangeSelector, "change", handleDisabledElement);
delegate(document, inputChangeSelector, "change", handleConfirm);
delegate(document, inputChangeSelector, "change", handleRemote);
delegate(document, formSubmitSelector, "submit", handleDisabledElement);
delegate(document, formSubmitSelector, "submit", handleConfirm);
delegate(document, formSubmitSelector, "submit", handleRemote);
delegate(document, formSubmitSelector, "submit", (e => setTimeout((() => disableElement(e)), 13)));
delegate(document, formSubmitSelector, "ajax:send", disableElement);
delegate(document, formSubmitSelector, "ajax:complete", enableElement);
delegate(document, formInputClickSelector, "click", preventInsignificantClick);
delegate(document, formInputClickSelector, "click", handleDisabledElement);
delegate(document, formInputClickSelector, "click", handleConfirm);
delegate(document, formInputClickSelector, "click", formSubmitButtonClick);
document.addEventListener("DOMContentLoaded", refreshCSRFTokens);
document.addEventListener("DOMContentLoaded", loadCSPNonce);
return window._rails_loaded = true;
};
Rails.start = start;
if (typeof jQuery !== "undefined" && jQuery && jQuery.ajax) {
if (jQuery.rails) {
throw new Error("If you load both jquery_ujs and rails-ujs, use rails-ujs only.");
}
jQuery.rails = Rails;
jQuery.ajaxPrefilter((function(options, originalOptions, xhr) {
if (!options.crossDomain) {
return CSRFProtection(xhr);
}
}));
}
export { Rails as default };