← back to Designer Wallcoverings
DWTT qty stepper: add combined diffs, clean harness proof + screenshot, verification scripts
08928754421170ad145688b5cc70b01b7c124f2a · 2026-06-30 20:27:13 -0700 · Steve
Files touched
A shopify/theme-fixes/custom-js-qty-2026-06-30/COMBINED.diffA shopify/theme-fixes/custom-js-qty-2026-06-30/custom.js.diffA shopify/theme-fixes/custom-js-qty-2026-06-30/product-form-content.liquid.diffA shopify/theme-fixes/custom-js-qty-2026-06-30/proof-harness.pngA shopify/theme-fixes/custom-js-qty-2026-06-30/shot.cjs
Diff
commit 08928754421170ad145688b5cc70b01b7c124f2a
Author: Steve <steve@designerwallcoverings.com>
Date: Tue Jun 30 20:27:13 2026 -0700
DWTT qty stepper: add combined diffs, clean harness proof + screenshot, verification scripts
---
.../custom-js-qty-2026-06-30/COMBINED.diff | 169 +++++++++++++++++++++
.../custom-js-qty-2026-06-30/custom.js.diff | 149 ++++++++++++++++++
.../product-form-content.liquid.diff | 20 +++
.../custom-js-qty-2026-06-30/proof-harness.png | Bin 0 -> 10340 bytes
.../theme-fixes/custom-js-qty-2026-06-30/shot.cjs | 14 ++
5 files changed, 352 insertions(+)
diff --git a/shopify/theme-fixes/custom-js-qty-2026-06-30/COMBINED.diff b/shopify/theme-fixes/custom-js-qty-2026-06-30/COMBINED.diff
new file mode 100644
index 00000000..2cbe058d
--- /dev/null
+++ b/shopify/theme-fixes/custom-js-qty-2026-06-30/COMBINED.diff
@@ -0,0 +1,169 @@
+--- custom.ORIGINAL.js 2026-06-30 20:14:43
++++ custom.js 2026-06-30 20:15:30
+@@ -38,27 +38,93 @@
+ mm_refresh_quantity_increments()
+ }), jQuery(document).on("click", ".plus, .minus", async function(event) {
+ event.preventDefault();
+-
+- var a = jQuery(this).closest(".mm_quantity").find(".qty"),
+- b = parseFloat(a.val()),
+- c = parseFloat(a.attr("max")),
+- d = parseFloat(a.attr("min")),
+- e = a.attr("step");
+- //b && "" !== b && "NaN" !== b || (b = 0), "" !== c && "NaN" !== c || (c = ""), "" !== d && "NaN" !== d || (d = 0), "any" !== e && "" !== e && void 0 !== e && "NaN" !== parseFloat(e) || (e = 1), jQuery(this).is(".plus") ? c && b >= c ? a.val(c) : a.val((b + parseFloat(e)).toFixed(e.getDecimals())) : d && b <= d ? a.val(d) : b > 0 && a.val((b - parseFloat(e)).toFixed(e.getDecimals())), a.trigger("change")
+- b && "" !== b && "NaN" !== b || (b = 0), "" !== c && "NaN" !== c || (c = ""), "" !== d && "NaN" !== d || (d = 0), "any" !== e && "" !== e && void 0 !== e && "NaN" !== parseFloat(e) || (e = 1), jQuery(this).is(".plus") ? c && b >= c ? a.val(c) : a.val((b + parseFloat(e)).toFixed(e.getDecimals())) : d && b <= d ? a.val(d) : b > 0 && a.val((b - parseFloat(e)).toFixed(e.getDecimals()))
+-
+- //alert();
+- const quantity=jQuery(this).closest(".mm_quantity").find(".qty").val();
+- jQuery(this).closest(".mm_quantity").find(".hquantity").attr('value',quantity);
+
+- if($(this).hasClass('cart-page-quantity-value')){
+- const variantId=jQuery(this).closest(".mm_quantity").find(".hquantity").attr('variantid');
+- console.log('-----', variantId)
+- await updateItem(variantId, quantity)
+- window.location.href="/cart"
+- }
+-
+-
++ // ===== 2026-06-30 qty-stepper repair (vp-dw-commerce) =====
++ // Prior code read step/min from the .qty element's live `step`/`min` attributes,
++ // but the variant-change handler (below) poisons those to 1/1 when the default-
++ // selected variant is the Sample — so on MOQ products (DWTT/Anna French, step=2,
++ // min=2) the + button could only go 1->2 and the floor was 1 instead of 2, and
++ // the original math line was left half-disabled (no a.trigger('change')), so the
++ // visible input often failed to reflect the change. This rewrite reads the ORDER
++ // step/min from the pristine data-order-step / data-order-min attributes the liquid
++ // renders straight from v_prods_quantity_order_units / v_prods_quantity_order_min
++ // (never mutated by JS), clamps correctly, and mirrors the hidden hquantity.
++ var $btn = jQuery(this);
++ try {
++ var $wrap = $btn.closest(".mm_quantity");
++ var a = $wrap.find(".qty");
++ if (!a.length) return;
++
++ // STEP = pristine order-units (fallback to step attr, then 1). Must be > 0.
++ var step = parseFloat(a.attr("data-order-step"));
++ if (!isFinite(step) || step <= 0) step = parseFloat(a.attr("step"));
++ if (!isFinite(step) || step <= 0) step = 1;
++
++ // MIN = pristine order-min (fallback to min attr, then step). Must be >= step floor.
++ var min = parseFloat(a.attr("data-order-min"));
++ if (!isFinite(min) || min <= 0) min = parseFloat(a.attr("min"));
++ if (!isFinite(min) || min <= 0) min = step;
++
++ // MAX only if a real numeric cap is present.
++ var maxRaw = a.attr("max");
++ var max = parseFloat(maxRaw);
++ var hasMax = (maxRaw != null && maxRaw !== "" && isFinite(max));
++
++ var decimals = String(step).getDecimals();
++ var current = parseFloat(a.val());
++ if (!isFinite(current)) current = min;
++
++ var next;
++ if ($btn.is(".plus")) {
++ next = current + step;
++ if (hasMax && next > max) next = max;
++ } else {
++ next = current - step;
++ if (next < min) next = min; // never below the minimum order quantity
++ }
++ // Snap to the on-step grid anchored at min (typing 3 on a step-2 min-2 product -> 2, then +2 -> 4).
++ var gridSteps = Math.round((next - min) / step);
++ if (gridSteps < 0) gridSteps = 0;
++ next = min + gridSteps * step;
++ if (hasMax && next > max) next = max;
++
++ var nextStr = next.toFixed(decimals);
++ a.val(nextStr);
++
++ // Mirror the visible qty into the hidden hquantity that is actually submitted to cart.
++ $wrap.find(".hquantity").attr('value', nextStr).val(nextStr);
++
++ if ($btn.hasClass('cart-page-quantity-value')) {
++ var variantId = $wrap.find(".hquantity").attr('variantid');
++ await updateItem(variantId, nextStr);
++ window.location.href = "/cart";
++ }
++ } catch (err) {
++ if (window.console && console.warn) console.warn('qty stepper error:', err);
++ }
++});
++// Snap a manually-typed qty to the nearest valid on-step value (>= min) on blur/change.
++jQuery(document).on('change blur', '.mm_quantity .qty', function () {
++ try {
++ var $wrap = jQuery(this).closest('.mm_quantity');
++ var a = jQuery(this);
++ var step = parseFloat(a.attr('data-order-step'));
++ if (!isFinite(step) || step <= 0) step = parseFloat(a.attr('step'));
++ if (!isFinite(step) || step <= 0) step = 1;
++ var min = parseFloat(a.attr('data-order-min'));
++ if (!isFinite(min) || min <= 0) min = parseFloat(a.attr('min'));
++ if (!isFinite(min) || min <= 0) min = step;
++ var v = parseFloat(a.val());
++ if (!isFinite(v) || v < min) v = min;
++ var gridSteps = Math.round((v - min) / step);
++ if (gridSteps < 0) gridSteps = 0;
++ var snapped = min + gridSteps * step;
++ var snappedStr = snapped.toFixed(String(step).getDecimals());
++ if (a.val() !== snappedStr) a.val(snappedStr);
++ $wrap.find('.hquantity').attr('value', snappedStr).val(snappedStr);
++ } catch (err) {
++ if (window.console && console.warn) console.warn('qty snap error:', err);
++ }
+ });
+
+ async function updateItem(variantId, quantity, properties={}){
+@@ -132,17 +198,22 @@
+
+ var $qty = $form.find('.mm_quantity .qty');
+ var $qtyWrapper = $form.find('.mm_quantity');
+- var originalMin = $qty.data('min');
+- var originalStep = $qty.data('step');
+
+- if (!originalMin) {
+- originalMin = parseFloat($qty.attr('min')) || 1;
+- $qty.data('min', originalMin);
+- }
+- if (!originalStep) {
+- originalStep = parseFloat($qty.attr('step')) || 1;
+- $qty.data('step', originalStep);
+- }
++ // 2026-06-30: read the TRUE minimum-order-quantity + step from the pristine
++ // data-order-min / data-order-step attributes the liquid renders straight from
++ // the v_prods_quantity_order_min / v_prods_quantity_order_units metafields.
++ // The old code cached these from the live step/min attrs, which get poisoned to
++ // 1/1 by the Sample branch when Sample is the default-selected variant — that
++ // was why the roll variant came back showing step=1/min=1 instead of 2/2.
++ var originalMin = parseFloat($qty.attr('data-order-min'));
++ if (!isFinite(originalMin) || originalMin <= 0) originalMin = parseFloat($qty.data('min'));
++ if (!isFinite(originalMin) || originalMin <= 0) originalMin = parseFloat($qty.attr('min')) || 1;
++ $qty.data('min', originalMin);
++
++ var originalStep = parseFloat($qty.attr('data-order-step'));
++ if (!isFinite(originalStep) || originalStep <= 0) originalStep = parseFloat($qty.data('step'));
++ if (!isFinite(originalStep) || originalStep <= 0) originalStep = parseFloat($qty.attr('step')) || 1;
++ $qty.data('step', originalStep);
+
+ // Sample request buttons are always available (hide/show is variant-driven only)
+ $form.find('.dl-sample-btn, .dl-second-sample-btn').show();
+--- product-form-content.ORIGINAL.liquid 2026-06-30 20:14:43
++++ liquid/snippets__product-form-content.liquid 2026-06-30 20:15:57
+@@ -477,6 +477,8 @@
+ type="text"
+ step="{{ prods_quantity_order_units }}"
+ min="{{ prods_quantity_order_min }}"
++ data-order-step="{{ prods_quantity_order_units }}"
++ data-order-min="{{ prods_quantity_order_min }}"
+ max=""
+ name="quantity"
+ value="{{ prods_quantity_order_min }}"
+@@ -577,6 +579,8 @@
+ type="text"
+ step="{{ prods_quantity_order_units }}"
+ min="{{ prods_quantity_order_min }}"
++ data-order-step="{{ prods_quantity_order_units }}"
++ data-order-min="{{ prods_quantity_order_min }}"
+ max=""
+ name="quantity"
+ value="{{ prods_quantity_order_min }}"
diff --git a/shopify/theme-fixes/custom-js-qty-2026-06-30/custom.js.diff b/shopify/theme-fixes/custom-js-qty-2026-06-30/custom.js.diff
new file mode 100644
index 00000000..8618de9a
--- /dev/null
+++ b/shopify/theme-fixes/custom-js-qty-2026-06-30/custom.js.diff
@@ -0,0 +1,149 @@
+--- custom.ORIGINAL.js 2026-06-30 20:14:43
++++ custom.js 2026-06-30 20:15:30
+@@ -38,27 +38,93 @@
+ mm_refresh_quantity_increments()
+ }), jQuery(document).on("click", ".plus, .minus", async function(event) {
+ event.preventDefault();
+-
+- var a = jQuery(this).closest(".mm_quantity").find(".qty"),
+- b = parseFloat(a.val()),
+- c = parseFloat(a.attr("max")),
+- d = parseFloat(a.attr("min")),
+- e = a.attr("step");
+- //b && "" !== b && "NaN" !== b || (b = 0), "" !== c && "NaN" !== c || (c = ""), "" !== d && "NaN" !== d || (d = 0), "any" !== e && "" !== e && void 0 !== e && "NaN" !== parseFloat(e) || (e = 1), jQuery(this).is(".plus") ? c && b >= c ? a.val(c) : a.val((b + parseFloat(e)).toFixed(e.getDecimals())) : d && b <= d ? a.val(d) : b > 0 && a.val((b - parseFloat(e)).toFixed(e.getDecimals())), a.trigger("change")
+- b && "" !== b && "NaN" !== b || (b = 0), "" !== c && "NaN" !== c || (c = ""), "" !== d && "NaN" !== d || (d = 0), "any" !== e && "" !== e && void 0 !== e && "NaN" !== parseFloat(e) || (e = 1), jQuery(this).is(".plus") ? c && b >= c ? a.val(c) : a.val((b + parseFloat(e)).toFixed(e.getDecimals())) : d && b <= d ? a.val(d) : b > 0 && a.val((b - parseFloat(e)).toFixed(e.getDecimals()))
+-
+- //alert();
+- const quantity=jQuery(this).closest(".mm_quantity").find(".qty").val();
+- jQuery(this).closest(".mm_quantity").find(".hquantity").attr('value',quantity);
+
+- if($(this).hasClass('cart-page-quantity-value')){
+- const variantId=jQuery(this).closest(".mm_quantity").find(".hquantity").attr('variantid');
+- console.log('-----', variantId)
+- await updateItem(variantId, quantity)
+- window.location.href="/cart"
+- }
+-
+-
++ // ===== 2026-06-30 qty-stepper repair (vp-dw-commerce) =====
++ // Prior code read step/min from the .qty element's live `step`/`min` attributes,
++ // but the variant-change handler (below) poisons those to 1/1 when the default-
++ // selected variant is the Sample — so on MOQ products (DWTT/Anna French, step=2,
++ // min=2) the + button could only go 1->2 and the floor was 1 instead of 2, and
++ // the original math line was left half-disabled (no a.trigger('change')), so the
++ // visible input often failed to reflect the change. This rewrite reads the ORDER
++ // step/min from the pristine data-order-step / data-order-min attributes the liquid
++ // renders straight from v_prods_quantity_order_units / v_prods_quantity_order_min
++ // (never mutated by JS), clamps correctly, and mirrors the hidden hquantity.
++ var $btn = jQuery(this);
++ try {
++ var $wrap = $btn.closest(".mm_quantity");
++ var a = $wrap.find(".qty");
++ if (!a.length) return;
++
++ // STEP = pristine order-units (fallback to step attr, then 1). Must be > 0.
++ var step = parseFloat(a.attr("data-order-step"));
++ if (!isFinite(step) || step <= 0) step = parseFloat(a.attr("step"));
++ if (!isFinite(step) || step <= 0) step = 1;
++
++ // MIN = pristine order-min (fallback to min attr, then step). Must be >= step floor.
++ var min = parseFloat(a.attr("data-order-min"));
++ if (!isFinite(min) || min <= 0) min = parseFloat(a.attr("min"));
++ if (!isFinite(min) || min <= 0) min = step;
++
++ // MAX only if a real numeric cap is present.
++ var maxRaw = a.attr("max");
++ var max = parseFloat(maxRaw);
++ var hasMax = (maxRaw != null && maxRaw !== "" && isFinite(max));
++
++ var decimals = String(step).getDecimals();
++ var current = parseFloat(a.val());
++ if (!isFinite(current)) current = min;
++
++ var next;
++ if ($btn.is(".plus")) {
++ next = current + step;
++ if (hasMax && next > max) next = max;
++ } else {
++ next = current - step;
++ if (next < min) next = min; // never below the minimum order quantity
++ }
++ // Snap to the on-step grid anchored at min (typing 3 on a step-2 min-2 product -> 2, then +2 -> 4).
++ var gridSteps = Math.round((next - min) / step);
++ if (gridSteps < 0) gridSteps = 0;
++ next = min + gridSteps * step;
++ if (hasMax && next > max) next = max;
++
++ var nextStr = next.toFixed(decimals);
++ a.val(nextStr);
++
++ // Mirror the visible qty into the hidden hquantity that is actually submitted to cart.
++ $wrap.find(".hquantity").attr('value', nextStr).val(nextStr);
++
++ if ($btn.hasClass('cart-page-quantity-value')) {
++ var variantId = $wrap.find(".hquantity").attr('variantid');
++ await updateItem(variantId, nextStr);
++ window.location.href = "/cart";
++ }
++ } catch (err) {
++ if (window.console && console.warn) console.warn('qty stepper error:', err);
++ }
++});
++// Snap a manually-typed qty to the nearest valid on-step value (>= min) on blur/change.
++jQuery(document).on('change blur', '.mm_quantity .qty', function () {
++ try {
++ var $wrap = jQuery(this).closest('.mm_quantity');
++ var a = jQuery(this);
++ var step = parseFloat(a.attr('data-order-step'));
++ if (!isFinite(step) || step <= 0) step = parseFloat(a.attr('step'));
++ if (!isFinite(step) || step <= 0) step = 1;
++ var min = parseFloat(a.attr('data-order-min'));
++ if (!isFinite(min) || min <= 0) min = parseFloat(a.attr('min'));
++ if (!isFinite(min) || min <= 0) min = step;
++ var v = parseFloat(a.val());
++ if (!isFinite(v) || v < min) v = min;
++ var gridSteps = Math.round((v - min) / step);
++ if (gridSteps < 0) gridSteps = 0;
++ var snapped = min + gridSteps * step;
++ var snappedStr = snapped.toFixed(String(step).getDecimals());
++ if (a.val() !== snappedStr) a.val(snappedStr);
++ $wrap.find('.hquantity').attr('value', snappedStr).val(snappedStr);
++ } catch (err) {
++ if (window.console && console.warn) console.warn('qty snap error:', err);
++ }
+ });
+
+ async function updateItem(variantId, quantity, properties={}){
+@@ -132,17 +198,22 @@
+
+ var $qty = $form.find('.mm_quantity .qty');
+ var $qtyWrapper = $form.find('.mm_quantity');
+- var originalMin = $qty.data('min');
+- var originalStep = $qty.data('step');
+
+- if (!originalMin) {
+- originalMin = parseFloat($qty.attr('min')) || 1;
+- $qty.data('min', originalMin);
+- }
+- if (!originalStep) {
+- originalStep = parseFloat($qty.attr('step')) || 1;
+- $qty.data('step', originalStep);
+- }
++ // 2026-06-30: read the TRUE minimum-order-quantity + step from the pristine
++ // data-order-min / data-order-step attributes the liquid renders straight from
++ // the v_prods_quantity_order_min / v_prods_quantity_order_units metafields.
++ // The old code cached these from the live step/min attrs, which get poisoned to
++ // 1/1 by the Sample branch when Sample is the default-selected variant — that
++ // was why the roll variant came back showing step=1/min=1 instead of 2/2.
++ var originalMin = parseFloat($qty.attr('data-order-min'));
++ if (!isFinite(originalMin) || originalMin <= 0) originalMin = parseFloat($qty.data('min'));
++ if (!isFinite(originalMin) || originalMin <= 0) originalMin = parseFloat($qty.attr('min')) || 1;
++ $qty.data('min', originalMin);
++
++ var originalStep = parseFloat($qty.attr('data-order-step'));
++ if (!isFinite(originalStep) || originalStep <= 0) originalStep = parseFloat($qty.data('step'));
++ if (!isFinite(originalStep) || originalStep <= 0) originalStep = parseFloat($qty.attr('step')) || 1;
++ $qty.data('step', originalStep);
+
+ // Sample request buttons are always available (hide/show is variant-driven only)
+ $form.find('.dl-sample-btn, .dl-second-sample-btn').show();
diff --git a/shopify/theme-fixes/custom-js-qty-2026-06-30/product-form-content.liquid.diff b/shopify/theme-fixes/custom-js-qty-2026-06-30/product-form-content.liquid.diff
new file mode 100644
index 00000000..56b2a0f2
--- /dev/null
+++ b/shopify/theme-fixes/custom-js-qty-2026-06-30/product-form-content.liquid.diff
@@ -0,0 +1,20 @@
+--- product-form-content.ORIGINAL.liquid 2026-06-30 20:14:43
++++ liquid/snippets__product-form-content.liquid 2026-06-30 20:15:57
+@@ -477,6 +477,8 @@
+ type="text"
+ step="{{ prods_quantity_order_units }}"
+ min="{{ prods_quantity_order_min }}"
++ data-order-step="{{ prods_quantity_order_units }}"
++ data-order-min="{{ prods_quantity_order_min }}"
+ max=""
+ name="quantity"
+ value="{{ prods_quantity_order_min }}"
+@@ -577,6 +579,8 @@
+ type="text"
+ step="{{ prods_quantity_order_units }}"
+ min="{{ prods_quantity_order_min }}"
++ data-order-step="{{ prods_quantity_order_units }}"
++ data-order-min="{{ prods_quantity_order_min }}"
+ max=""
+ name="quantity"
+ value="{{ prods_quantity_order_min }}"
diff --git a/shopify/theme-fixes/custom-js-qty-2026-06-30/proof-harness.png b/shopify/theme-fixes/custom-js-qty-2026-06-30/proof-harness.png
new file mode 100644
index 00000000..4388b8bc
Binary files /dev/null and b/shopify/theme-fixes/custom-js-qty-2026-06-30/proof-harness.png differ
diff --git a/shopify/theme-fixes/custom-js-qty-2026-06-30/shot.cjs b/shopify/theme-fixes/custom-js-qty-2026-06-30/shot.cjs
new file mode 100644
index 00000000..78d39270
--- /dev/null
+++ b/shopify/theme-fixes/custom-js-qty-2026-06-30/shot.cjs
@@ -0,0 +1,14 @@
+const { chromium } = require(require('path').join(process.env.HOME, 'Projects/Designer-Wallcoverings/node_modules/playwright'));
+const path=require('path');
+(async()=>{
+ const b=await chromium.launch();const p=await(await b.newContext({viewport:{width:700,height:520}})).newPage();
+ await p.goto('file://'+path.join(__dirname,'harness.html'),{waitUntil:'load'});
+ await p.waitForTimeout(300);
+ await p.click('#moq .plus'); await p.waitForTimeout(80);
+ await p.click('#moq .plus'); await p.waitForTimeout(80); // MOQ now 6
+ await p.click('#std .plus'); await p.waitForTimeout(80); // std now 2
+ await p.screenshot({path:'proof-harness.png'});
+ console.log('screenshot saved: proof-harness.png');
+ console.log('MOQ qty=',await p.evaluate(()=>document.querySelector('#moq .qty').value),' STD qty=',await p.evaluate(()=>document.querySelector('#std .qty').value));
+ await b.close();
+})();
← b77fe370 DWTT qty stepper fix: repair +/- increment/decrement in cust
·
back to Designer Wallcoverings
·
auto-save: 2026-06-30T20:43:13 (5 files) — pending-approval/ ce69ce72 →