← 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
M DW-Websites/variant-min-quantity.liquid
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 →