1// Shared in-control loading spinner. 2// 3// Used by the global turbo:submit-start/end listeners in v3.js (every Turbo 4// form submit) and by controllers that save via fetch() outside Turbo 5// (e.g. beverage_skus_stock#bulkDelete). Injects a <span class="ui-button__spinner"> 6// as the button's first child (so it sits before the label) and disables the 7// button; stop() reverses it. Idempotent and submitter-safe â a null or 8// non-button submitter (e.g. a programmatic form.submit()) is a no-op, leaving 9// the Turbo progress bar as the only cue. 10 11const SPINNER_CLASS = "ui-button__spinner" 12const LOADING_CLASS = "ui-button--loading" 13const WAS_DISABLED_ATTR = "data-button-spinner-was-disabled" 14 15function isSpinnable(el) { 16 if (!el || !el.tagName) return false 17 if (el.dataset && el.dataset.buttonSpinner === "false") return false 18 if (el.tagName === "BUTTON") return true 19 if (el.tagName === "INPUT") { 20 const t = (el.getAttribute("type") || "").toLowerCase() 21 return t === "submit" || t === "image" 22 } 23 return false 24} 25 26// `manageDisabled: false` â only show/hide the visual spinner and leave the 27// button's `disabled` property alone. Turbo already disables a form's submitter 28// for the duration of a submit and re-enables it on completion, so for Turbo 29// submits the spinner must NOT also toggle disabled: it would read Turbo's 30// transient disable as the "original" state and then restore the button to 31// disabled on stop, leaving it stuck (#1773 â only surfaced once the success 32// path stopped doing a full page reload). fetch-based callers keep the default 33// (true) so they still get double-submit protection. 34export function startButtonSpinner(button, { manageDisabled = true } = {}) { 35 if (!isSpinnable(button)) return 36 if (button.classList.contains(LOADING_CLASS)) return // already spinning 37 if (manageDisabled) button.setAttribute(WAS_DISABLED_ATTR, button.disabled ? "true" : "false") 38 // <input> elements can't hold children â class-only fallback there. 39 if (button.tagName === "BUTTON") { 40 const spinner = document.createElement("span") 41 spinner.className = SPINNER_CLASS 42 spinner.setAttribute("aria-hidden", "true") 43 button.insertBefore(spinner, button.firstChild) 44 } 45 button.classList.add(LOADING_CLASS) 46 button.setAttribute("aria-busy", "true") 47 if (manageDisabled) button.disabled = true 48} 49 50export function stopButtonSpinner(button, { manageDisabled = true } = {}) { 51 if (!button || !button.classList || !button.classList.contains(LOADING_CLASS)) return 52 const spinner = button.querySelector(`.${SPINNER_CLASS}`) 53 if (spinner) spinner.remove() 54 button.classList.remove(LOADING_CLASS) 55 button.removeAttribute("aria-busy") 56 if (manageDisabled) { 57 button.disabled = button.getAttribute(WAS_DISABLED_ATTR) === "true" 58 button.removeAttribute(WAS_DISABLED_ATTR) 59 } 60};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.