{
  "version": 3,
  "sources": ["/cdn/shop/t/26/assets/tdhq-quickadd.js?b=20260826a"],
  "sourcesContent": ["/* ============================================================\n   TDHQ quick add - tdhq-quickadd.js\n\n   Adds a product to the basket straight from a product card.\n\n   WHY: every competitor checked puts this on the card. MyCarCleaning's card\n   carries \"In stock\", \"Add to cart\" and \"Quick view\"; TDHQ's card ended at\n   \"View Details\", so buying two known items meant two full page journeys.\n\n   PROGRESSIVE ENHANCEMENT, and it matters here more than usual because this\n   touches the basket:\n     - the card is a REAL form whose button posts to /cart/add (formaction),\n       so with no JavaScript, before this file has run, or if it fails to\n       load, the button still adds the item and Shopify redirects to the cart.\n       The form itself has no action (U.58): Shopify's analytics script reads\n       every form whose action is /cart/add at page load, logging a \"viewed\"\n       variant per card and erroring on cards outside the page's product list.\n     - this script takes the click over and posts the card's\n       data-tdhq-variant-id to /cart/add.js, after checking the stock left\n       (tdhq-qty.js knows the stock and what the basket already holds).\n     - if that request fails for ANY reason the form is submitted natively\n       rather than showing an error. A shopper who clicks \"Add\" must never end\n       up with nothing in the basket and no explanation.\n\n   The basket count and total are refreshed by re-rendering the HEADER SECTION\n   through the Section Rendering API and copying the two values across. That is\n   deliberate: formatting money in JavaScript means reimplementing the shop's\n   currency format, which drifts. Letting Liquid render it cannot drift.\n   ============================================================ */\n(function () {\n  'use strict';\n\n  var BUSY = 'is-adding';\n  var DONE_MS = 1800;\n\n  /* ONE ADD AT A TIME PER STOCK-TRACKED ITEM, until the basket is known\n     again. The add answers before the basket refresh lands, and until then\n     the stock check reads the old basket - so a quick second click, or a\n     second card for the same item, could pass it twice. */\n  var pending = {};\n  document.addEventListener('tdhq:cart:changed', function () { pending = {}; });\n\n  /* Refresh the header basket count.\n\n     The first version of this asked the Section Rendering API to re-render the\n     header section so Liquid could format everything. That does not work, and\n     the reason is a hard platform limit worth recording: the header lives in a\n     SECTION GROUP (rendered by `{% sections 'header-group' %}`), and the\n     Section Rendering API cannot render section-group sections. Requesting it\n     returned the page's own main section instead \u2014 `{\"collection\": \u2026}` \u2014 so the\n     badge silently never updated.\n\n     /cart.js is the right source anyway. The only value the badge shows is an\n     INTEGER, so there is no currency to format and nothing that can drift.\n     When the \"show basket value\" setting is on there is a money string too.\n     That is formatted with the drawer's published formatter - the shop's own\n     money_format, one implementation for the whole theme - rather than by\n     fetching and parsing a copy of the current page for one span. */\n  function refreshBasket() {\n    return window.TdhqCart.get().catch(function () { return null; })\n      .then(function (cart) {\n        if (!cart) return;\n        if (window.TdhqQty) { window.TdhqQty.absorb(cart); }\n        pending = {};\n        var badge = document.querySelector('[data-tdhq-cart-count]');\n        if (badge) {\n          /* Replace ONLY the leading text node. The badge also contains a\n             visually-hidden label, and overwriting textContent would delete it,\n             leaving screen readers with a bare number and no context. */\n          var first = badge.firstChild;\n          if (first && first.nodeType === 3) {\n            first.nodeValue = String(cart.item_count) + ' ';\n          } else {\n            badge.insertBefore(document.createTextNode(String(cart.item_count) + ' '), badge.firstChild);\n          }\n          var label = badge.querySelector('.tdhq-visually-hidden');\n          if (label) {\n            label.textContent = cart.item_count === 1 ? 'item in basket' : 'items in basket';\n          }\n          badge.classList.toggle('tdhq-hidden', cart.item_count === 0);\n        }\n\n        /* The header's basket TOTAL.\n\n           This used to be read out of a freshly fetched copy of the whole\n           current page: `fetch(location.pathname)` for the sole purpose of\n           copying one money string. Measured on the homepage that is 636 KB of\n           HTML plus a full DOMParser parse, and the submit handler awaited it\n           BEFORE flashing \"Added to basket\" and before dispatching the event\n           that opens the drawer - so the most frequent action on the site sat\n           silent for the length of a page load with nothing on screen moving.\n\n           The cart payload already in hand carries `total_price`, and the\n           drawer publishes the shop's own money formatter, so the figure is\n           written directly. If the drawer script is not on the page the total\n           is simply left alone rather than paying for a page fetch: the count\n           badge beside it has already updated, so the header is not stale in\n           any way a shopper would notice. */\n        var total = document.querySelector('[data-tdhq-cart-total]');\n        var fmt = window.tdhqFormatMoney;\n        if (!fmt) { return; }\n\n        if (total) {\n          total.textContent = fmt(cart.total_price);\n          return;\n        }\n\n        /* NO ELEMENT MEANS THE MERCHANT SAID NO. This used to build one -\n           after every add, unconditionally, with a class that has no rule\n           anywhere in the theme - so a header configured with \"show basket\n           value\" OFF grew an unstyled price the moment anything was added, and\n           kept it for the rest of the visit. The header renders the total when\n           it is wanted; when it is not, there is nothing here to update and\n           that is the correct outcome. The count badge beside it has already\n           updated either way. */\n      })\n      .catch(function () { /* a stale badge is not worth an error state */ });\n  }\n\n  /* A message beside the card that failed, in the same shape both baskets use.\n     role=\"alert\" so it is announced as well as shown, and it clears itself:\n     a card the shopper has moved past should not keep an old complaint. */\n  function showAddError(form, message) {\n    if (!form || !message) { return; }\n    var card = form.closest('.tdhq-pcard') || form.parentNode;\n    /* over the bottom of the picture, in the site's notification style (window.TdhqNote, 23 Oct 2026) */\n    var media = card.querySelector('.tdhq-pcard__media');\n    if (window.TdhqNote && media) { window.TdhqNote.show(media, message); return; }\n    var box = card.querySelector('.tdhq-pcard__adderr');\n    if (!box) {\n      box = document.createElement('p');\n      box.className = 'tdhq-pcard__adderr';\n      box.setAttribute('role', 'alert');\n      /* inside the form, under the button, so it keeps the form's side padding (23 Oct 2026) */\n      form.appendChild(box);\n    }\n    box.textContent = message;\n    if (window.TdhqMotion) { window.TdhqMotion.show(box); }\n    window.clearTimeout(box._t);\n    box._t = window.setTimeout(function () {\n      if (!box.parentNode) { return; }\n      if (window.TdhqMotion) { window.TdhqMotion.exit(box, function () { box.remove(); }); } else { box.remove(); }\n    }, 8000);\n  }\n\n  function flash(button, label) {\n    if (!button) return;\n    var original = button.getAttribute('data-tdhq-label') || button.textContent.trim();\n    button.setAttribute('data-tdhq-label', original);\n    button.textContent = label;\n    window.setTimeout(function () {\n      if (button.getAttribute('data-tdhq-label')) {\n        button.textContent = button.getAttribute('data-tdhq-label');\n      }\n    }, DONE_MS);\n  }\n\n  document.addEventListener('click', function (event) {\n    var button = event.target && event.target.closest ? event.target.closest('[data-tdhq-quickadd-btn]') : null;\n    /* `form` is the card's add form (form[data-tdhq-quickadd]) */\n    var form = button && button.closest('[data-tdhq-quickadd]');\n    if (!form) return;\n    event.preventDefault();\n    if (form.classList.contains(BUSY)) return;\n\n    var variantId = form.getAttribute('data-tdhq-variant-id');\n    if (!variantId) return;\n\n    /* NOTHING LEFT TO ADD. The basket already holds every unit in stock:\n       say so here instead of posting - Shopify does not always refuse an\n       add past the stock (an oversell went through on 23 Sep). */\n    var qty = window.TdhqQty;\n    if (qty) {\n      /* A card drawn after page load (Saved items, recently viewed) has not\n         told tdhq-qty its stock yet: its hidden quantity carries it. No max\n         means the item is not stock-limited, which retires an old ceiling. */\n      var box = form.querySelector('input[name=\"quantity\"]');\n      if (box) { qty.remember(variantId, box.getAttribute('max') || 0); }\n      if (pending[variantId]) { return; }\n      if (qty.headroom(variantId) === 0) {\n        showAddError(form, qty.reason(variantId, 'add'));\n        return;\n      }\n      if (qty.stock(variantId) !== null) {\n        pending[variantId] = true;\n        window.setTimeout(function () { delete pending[variantId]; }, 8000);\n      }\n    }\n\n    form.classList.add(BUSY);\n    if (button) button.disabled = true;\n\n    var kitList = null;\n    try { kitList = JSON.parse((form.getAttribute('data-tdhq-kit-items') || '').trim() || 'null'); } catch (err) { kitList = null; }\n    if (!Array.isArray(kitList) || !kitList.length) { kitList = null; }\n\n    fetch('/cart/add.js', {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },\n      /* A battery kit built from its parts (data-tdhq-kit-items, snippets/tdhq-kit-items.liquid) adds the parts as\n         separate basket lines, each times the quantity, never the kit itself (owner + Simon, 20 Oct 2026). */\n      body: JSON.stringify(kitList\n        ? { items: kitList.map(function (p) { return { id: p[0], quantity: p[1] }; }) }\n        : { id: parseInt(variantId, 10), quantity: 1 })\n    })\n      .then(function (r) {\n        /* Shopify explains its own refusals - \"Cannot find variant\", \"All 3 of\n           this item are in your cart\" - and that sentence is the only thing\n           that tells the shopper why nothing happened. Carry it through as the\n           error message instead of throwing a status code away. */\n        if (!r.ok) {\n          return r.json()\n            .catch(function () { throw new Error('Could not add this item. Please try again.'); })\n            .then(function (e) { throw new Error(e.description || e.message || 'Could not add this item.'); });\n        }\n        return r.json();\n      })\n      .then(function (added) {\n        /* Confirm FIRST, reconcile after. The add has already succeeded at this\n           point, so the shopper sees the label change and the drawer open in\n           the same frame; the header badge and total settle a moment later\n           without anything waiting on them. Ordering these the other way round\n           is what made a card click feel like it had not registered. */\n        form.classList.remove(BUSY);\n        if (button) button.disabled = false;\n        flash(button, 'Added to basket');\n        /* The drawer's own refresh (on tdhq:cart:added below) redraws the\n           header count and total; a second /cart.js here only raced it. */\n        if (!window.tdhqCartDrawer) { window.setTimeout(refreshBasket, 0); }\n        /* Announce it: a visual label change is invisible to a screen reader\n           when it happens outside the focused element's own text. */\n        /* Hand off to the basket drawer. This script owns ADDING; the drawer\n           owns SHOWING. Dispatching instead of calling it directly keeps the two\n           independent - with no drawer on the page nothing listens and the flash\n           message above remains the confirmation. */\n        document.dispatchEvent(new CustomEvent('tdhq:cart:added', { detail: { source: 'quickadd', productId: (form.getAttribute('data-tdhq-product-id') || null), item: added } }));\n        var live = document.getElementById('tdhq-quickadd-live');\n        if (live) {\n          live.textContent = '';\n          /* A timer rather than requestAnimationFrame: rAF is paused in a\n             background tab, and an announcement that never fires is worse than\n             one that fires 30ms late. */\n          window.setTimeout(function () {\n            live.textContent = (form.getAttribute('data-tdhq-title') || 'Item') + ' added to your basket';\n          }, 30);\n        }\n      })\n      .catch(function (err) {\n        /* SAY IT HERE. This used to call form.submit(), which posts the real\n           /cart/add form as a browser navigation - and a refusal then lands on\n           Shopify's own error page (\"Cart Error: Cannot find variant\",\n           \"Something went wrong\", one link back to the myshopify domain). The\n           shopper loses the page they were browsing to be told, badly, that\n           one card did not work. Both baskets already answer this class of\n           failure in place; so does this now. */\n        form.classList.remove(BUSY);\n        if (button) button.disabled = false;\n        delete pending[variantId];\n        showAddError(form, (err && err.message) || 'Could not add this item.');\n        /* a refused kit add may still have put some of its parts in the basket */\n        if (kitList && window.TdhqQty && window.TdhqQty.reconcile) { window.TdhqQty.reconcile(); }\n      });\n  }, true);\n})();\n\n/* CARD PHOTO FIT moved to snippets/tdhq-fit-script.liquid (2026-09-22): one\n   rule for every product picture on the site, not only non-square cards. */\n"],
  "mappings": "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",
  "names": []
}
