PageSourceSearch

https://dashboard.tastevinapp.com/assets/lib/button_spinner-d6dbaf…06adb341e822ae9b02cbb6a844f5aced55832ddf.js

js tastevinapp.com collected 2026-10-01 16:10:17 UTC 2,849 bytes, 60 lines download raw bytes

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.