[object Object]

← back to Designer Wallcoverings

product min-qty: target real mm_ quantity widget + single-option-selector; verified on live DOM (Sample=1, PerYard=5, Wallcovering=10, clamp+step1)

9fcc5ff85e7f480be2e85f069c00020e8be9260a · 2026-06-29 11:26:47 -0700 · Steve Abrams

Files touched

Diff

commit 9fcc5ff85e7f480be2e85f069c00020e8be9260a
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Jun 29 11:26:47 2026 -0700

    product min-qty: target real mm_ quantity widget + single-option-selector; verified on live DOM (Sample=1, PerYard=5, Wallcovering=10, clamp+step1)
---
 DW-Websites/variant-min-quantity.liquid | 117 +++++++++++++++-----------------
 1 file changed, 56 insertions(+), 61 deletions(-)

diff --git a/DW-Websites/variant-min-quantity.liquid b/DW-Websites/variant-min-quantity.liquid
index 98c51045..a8e7296b 100644
--- a/DW-Websites/variant-min-quantity.liquid
+++ b/DW-Websites/variant-min-quantity.liquid
@@ -1,91 +1,86 @@
 {%- comment -%}
-  variant-min-quantity — make the Quantity field's minimum follow the selected Size
-  variant. The Size option text already encodes the minimum, e.g.
-  "Sold Per Yard (5 yd min)" -> 5, "Wallcovering (10 yd min)" -> 10, Sample -> 1.
-  Pure client-side, no metafield changes. Include near the end of the product
-  template:  {%- include 'variant-min-quantity' -%}
+  variant-min-quantity — make the DW MinMax (mm_) Quantity stepper's minimum follow
+  the selected Size variant. The Size option text encodes the minimum:
+  "Sold Per Yard (5 yd min)" -> 5, "Wallcovering (10 yd min)" -> 10, "Sample" -> 1.
+  Above the minimum the client orders any quantity in increments of 1; the field
+  cannot go below the minimum (typing or the − button).
+
+  Real DOM (live theme):
+    qty input  : .mm_quantity .input-text.qty   (type=text, ± buttons, onkeypress=mm_validate)
+    size picker: select.single-option-selector whose option text contains "yd min"
+    note       : the theme force-resets qty to 1 at ~1000ms — we re-apply after that.
+
+  Include near the end of the product template:  {%- include 'variant-min-quantity' -%}
 {%- endcomment -%}
 <script>
 (function () {
-  // pull the integer minimum out of an option label like "...(10 yd min)"
   function minFromText(s) {
+    if (/sample/i.test(s)) return 1;                 // a sample is always a single unit
     var m = String(s || '').match(/(\d+)\s*yd\s*min/i);
     return m ? parseInt(m[1], 10) : null;
   }
   function qtyInput() {
-    return document.querySelector(
-      'input[name="quantity"], input#Quantity, .quantity__input, input.quantity-selector, [data-quantity-input]'
-    );
+    return document.querySelector('.mm_quantity .input-text.qty, .mm_quantity input.qty, .input-text.qty');
   }
-  function selectedSizeText() {
-    // Shopify <select name="options[Size]"> (or generic single-option-selector)
-    var sels = document.querySelectorAll(
-      'select[name="options[Size]"], select.single-option-selector'
-    );
+  // the Size selector = the single-option-selector whose selected label encodes a "yd min"
+  function sizeText() {
+    var sels = document.querySelectorAll('select.single-option-selector, select[name^="options"]');
     for (var i = 0; i < sels.length; i++) {
-      var sel = sels[i];
-      var label = (sel.getAttribute('data-option-name') || sel.name || '').toLowerCase();
-      if (sels.length === 1 || label.indexOf('size') !== -1) {
-        if (sel.selectedIndex >= 0) return sel.options[sel.selectedIndex].textContent;
-      }
-    }
-    // radio-style variant pickers
-    var r = document.querySelector('input[type="radio"][name*="Size"]:checked, .variant-input input:checked');
-    if (r) {
-      var lbl = r.id && document.querySelector('label[for="' + r.id + '"]');
-      return (lbl ? lbl.textContent : r.value);
+      var s = sels[i];
+      if (s.selectedIndex < 0 || !s.options[s.selectedIndex]) continue;
+      var t = s.options[s.selectedIndex].textContent;
+      if (/yd\s*min|wallcovering|sold\s*per\s*yard|sample/i.test(t)) return t;
     }
     return '';
   }
-  // never let the field sit below its minimum (typing, blur, or a minus button)
+  function currentMin() { return minFromText(sizeText()) || 1; }
+
   function clampUp(qi) {
     var min = parseInt(qi.getAttribute('min'), 10) || 1;
     var v = parseInt(qi.value, 10);
     if (isNaN(v) || v < min) { if (String(qi.value) !== String(min)) qi.value = min; }
   }
-  function wireClamp(qi) {
-    if (qi.__minClamp) return; qi.__minClamp = true;
-    ['input', 'change', 'blur', 'keyup'].forEach(function (ev) {
-      qi.addEventListener(ev, function () { clampUp(qi); });
-    });
-  }
   function applyMin(forceValue) {
     var qi = qtyInput();
     if (!qi) return;
-    wireClamp(qi);
-    var txt = selectedSizeText();
-    // a Sample is always a single unit, even if its label includes a "yd min" phrase
-    var n = /sample/i.test(txt) ? 1 : minFromText(txt);
-    var min = n || 1;
-    if (String(qi.min) !== String(min)) { qi.min = min; qi.setAttribute('min', min); }
-    // floor is the minimum; ABOVE it the client orders any quantity in increments of 1
-    if (String(qi.step) !== '1') { qi.step = 1; qi.setAttribute('step', 1); }
+    var min = currentMin();
+    if (String(qi.getAttribute('min')) !== String(min)) qi.setAttribute('min', min);
+    if (qi.getAttribute('step') !== '1') qi.setAttribute('step', 1);  // floor only; +1 increments above it
     var cur = parseInt(qi.value, 10);
     if (forceValue || isNaN(cur) || cur < min) {
-      if (String(qi.value) !== String(min)) {
-        qi.value = min;
-        qi.dispatchEvent(new Event('change', { bubbles: true }));
-      }
-    } else {
-      clampUp(qi);
-    }
+      if (String(qi.value) !== String(min)) { qi.value = min; qi.dispatchEvent(new Event('change', { bubbles: true })); }
+    } else { clampUp(qi); }
   }
+
   function wire() {
-    document
-      .querySelectorAll('select[name="options[Size]"], select.single-option-selector, input[type="radio"][name*="Size"]')
-      .forEach(function (el) {
-        if (el.__minWired) return; el.__minWired = true;
-        el.addEventListener('change', function () { setTimeout(function () { applyMin(true); }, 30); });
-      });
+    var qi = qtyInput();
+    if (qi && !qi.__minClamp) {
+      qi.__minClamp = true;
+      ['input', 'change', 'blur', 'keyup'].forEach(function (ev) { qi.addEventListener(ev, function () { clampUp(qi); }); });
+    }
+    // re-clamp after the ± buttons act
+    document.querySelectorAll('.mm_quantity .minus, .mm_quantity .plus').forEach(function (b) {
+      if (b.__minClamp) return; b.__minClamp = true;
+      b.addEventListener('click', function () { setTimeout(function () { clampUp(qtyInput() || {}); }, 20); });
+    });
+    // variant pickers -> jump to the new floor
+    document.querySelectorAll('select.single-option-selector, select[name^="options"], input[type="radio"][name*="ption"]').forEach(function (el) {
+      if (el.__minWired) return; el.__minWired = true;
+      el.addEventListener('change', function () { setTimeout(function () { applyMin(true); }, 40); });
+    });
     applyMin(true);
   }
-  if (document.readyState !== 'loading') wire();
-  else document.addEventListener('DOMContentLoaded', wire);
-  // re-wire + keep min synced when the theme re-renders the form (debounced; never forces value)
-  var t;
-  new MutationObserver(function () {
-    clearTimeout(t);
-    t = setTimeout(function () { wire(); applyMin(false); }, 120);
-  }).observe(document.documentElement, { subtree: true, childList: true });
+
+  function start() {
+    wire();
+    // the theme resets qty to 1 at ~1000ms — re-apply after that so the floor wins
+    [1100, 1600, 2500].forEach(function (t) { setTimeout(function () { applyMin(true); }, t); });
+  }
+  if (document.readyState !== 'loading') start();
+  else document.addEventListener('DOMContentLoaded', start);
+  // re-wire if the form re-renders (debounced; keeps min synced, never forces value)
+  var d;
+  new MutationObserver(function () { clearTimeout(d); d = setTimeout(function () { wire(); applyMin(false); }, 150); })
+    .observe(document.documentElement, { subtree: true, childList: true });
 })();
 </script>

← abe1470c appointments LIVE at api.designerwallcoverings.com: migratio  ·  back to Designer Wallcoverings  ·  deploy: push-min-qty-to-dev-theme.py — inject per-variant mi 31e6eaee →