← back to Designer Wallcoverings
DWTT qty stepper fix: repair +/- increment/decrement in custom.js + pristine data-order-* in product-form-content.liquid (dev theme 144105013299)
b77fe3704dc989bd4424a76e4bcd7331cf89cf5c · 2026-06-30 20:25:37 -0700 · Steve
Files touched
A shopify/theme-fixes/custom-js-qty-2026-06-30/DEV_custom.jsA shopify/theme-fixes/custom-js-qty-2026-06-30/block_moq.htmlA shopify/theme-fixes/custom-js-qty-2026-06-30/block_std.htmlA shopify/theme-fixes/custom-js-qty-2026-06-30/clicktest.cjsA shopify/theme-fixes/custom-js-qty-2026-06-30/custom.ORIGINAL.jsM shopify/theme-fixes/custom-js-qty-2026-06-30/custom.jsA shopify/theme-fixes/custom-js-qty-2026-06-30/custom.js.liquid.SOURCEA shopify/theme-fixes/custom-js-qty-2026-06-30/firstclick.cjsA shopify/theme-fixes/custom-js-qty-2026-06-30/harness-run.cjsA shopify/theme-fixes/custom-js-qty-2026-06-30/harness.htmlA shopify/theme-fixes/custom-js-qty-2026-06-30/hostcheck.cjsA shopify/theme-fixes/custom-js-qty-2026-06-30/hostcheck2.cjsA shopify/theme-fixes/custom-js-qty-2026-06-30/iso.cjsM shopify/theme-fixes/custom-js-qty-2026-06-30/liquid/snippets__product-form-content.liquidA shopify/theme-fixes/custom-js-qty-2026-06-30/previewcookie.cjsA shopify/theme-fixes/custom-js-qty-2026-06-30/primarypreview.cjsA shopify/theme-fixes/custom-js-qty-2026-06-30/product-form-content.ORIGINAL.liquidA shopify/theme-fixes/custom-js-qty-2026-06-30/pureplus.cjsA shopify/theme-fixes/custom-js-qty-2026-06-30/rawattr.cjsA shopify/theme-fixes/custom-js-qty-2026-06-30/srccheck.cjsA shopify/theme-fixes/custom-js-qty-2026-06-30/verify-dev-asset.cjsA shopify/theme-fixes/custom-js-qty-2026-06-30/verify-logic.cjs
Diff
commit b77fe3704dc989bd4424a76e4bcd7331cf89cf5c
Author: Steve <steve@designerwallcoverings.com>
Date: Tue Jun 30 20:25:37 2026 -0700
DWTT qty stepper fix: repair +/- increment/decrement in custom.js + pristine data-order-* in product-form-content.liquid (dev theme 144105013299)
---
.../custom-js-qty-2026-06-30/DEV_custom.js | 582 +++++++++++++++
.../custom-js-qty-2026-06-30/block_moq.html | 21 +
.../custom-js-qty-2026-06-30/block_std.html | 21 +
.../custom-js-qty-2026-06-30/clicktest.cjs | 32 +
.../custom-js-qty-2026-06-30/custom.ORIGINAL.js | 511 +++++++++++++
.../theme-fixes/custom-js-qty-2026-06-30/custom.js | 133 +++-
.../custom.js.liquid.SOURCE | 511 +++++++++++++
.../custom-js-qty-2026-06-30/firstclick.cjs | 30 +
.../custom-js-qty-2026-06-30/harness-run.cjs | 28 +
.../custom-js-qty-2026-06-30/harness.html | 52 ++
.../custom-js-qty-2026-06-30/hostcheck.cjs | 14 +
.../custom-js-qty-2026-06-30/hostcheck2.cjs | 15 +
.../theme-fixes/custom-js-qty-2026-06-30/iso.cjs | 43 ++
.../liquid/snippets__product-form-content.liquid | 4 +
.../custom-js-qty-2026-06-30/previewcookie.cjs | 19 +
.../custom-js-qty-2026-06-30/primarypreview.cjs | 15 +
.../product-form-content.ORIGINAL.liquid | 800 +++++++++++++++++++++
.../custom-js-qty-2026-06-30/pureplus.cjs | 24 +
.../custom-js-qty-2026-06-30/rawattr.cjs | 15 +
.../custom-js-qty-2026-06-30/srccheck.cjs | 20 +
.../custom-js-qty-2026-06-30/verify-dev-asset.cjs | 86 +++
.../custom-js-qty-2026-06-30/verify-logic.cjs | 138 ++++
22 files changed, 3083 insertions(+), 31 deletions(-)
diff --git a/shopify/theme-fixes/custom-js-qty-2026-06-30/DEV_custom.js b/shopify/theme-fixes/custom-js-qty-2026-06-30/DEV_custom.js
new file mode 100644
index 00000000..f86ee6f3
--- /dev/null
+++ b/shopify/theme-fixes/custom-js-qty-2026-06-30/DEV_custom.js
@@ -0,0 +1,582 @@
+/*========== 020 Custom js for quantity ===================*/
+
+function mm_refresh_quantity_increments() {
+ jQuery("div.mm_quantity:not(.mm_buttons_added), td.mm_quantity:not(.mm_buttons_added)").each(function(a, b) {
+ var c = jQuery(b);
+ c.addClass("mm_buttons_added"), c.children().first().before('<input type="button" value="-" class="minus" />'), c.children().last().after('<input type="button" value="+" class="plus" />')
+ })
+}
+String.prototype.getDecimals || (String.prototype.getDecimals = function() {
+ var a = this,
+ b = ("" + a).match(/(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/);
+ return b ? Math.max(0, (b[1] ? b[1].length : 0) - (b[2] ? +b[2] : 0)) : 0
+}), jQuery(document).ready(function() {
+ mm_refresh_quantity_increments()
+
+ // Custom popup contact form (moved from snippets/product.liquid)
+ if (jQuery('.custom-popup-contact-form').length > 0) {
+ jQuery('.custom-popup-contact-form').on('click', function (e) {
+ e.preventDefault();
+ jQuery('#custom-popup-overlay').show();
+ });
+ }
+
+ if (jQuery('#custom-popup-overlay .custom-close').length > 0) {
+ jQuery('#custom-popup-overlay .custom-close').on('click', function () {
+ jQuery('#custom-popup-overlay').hide();
+ });
+ }
+
+ if (jQuery('#custom-popup-overlay .success-message').length > 0) {
+ jQuery('.custom-contact-message').html('Thank you. We will respond to your message shortly.');
+ }
+
+ if (jQuery('#custom-popup-overlay .error-message').length > 0) {
+ jQuery('.custom-contact-message').html(jQuery('#custom-popup-overlay .error-message').html());
+ }
+}), jQuery(document).on("updated_wc_div", function() {
+ mm_refresh_quantity_increments()
+}), jQuery(document).on("click", ".plus, .minus", async function(event) {
+ event.preventDefault();
+
+ // ===== 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={}){
+ const result = await fetch("/cart/change.json", {
+ method:"post",
+ headers: {
+ "Content-type": "application/json",
+ "Accept": "application/json"
+ },
+ body: JSON.stringify({
+ id: variantId,
+ quantity: quantity,
+ properties: properties
+ })
+ })
+ return result.json();
+}
+
+function mm_validate(evt) {
+ var theEvent = evt || window.event;
+ theEvent.preventDefault()
+ /*
+ if (theEvent.type === 'paste') {
+ key = event.clipboardData.getData('text/plain');
+ } else {
+ var key = theEvent.keyCode || theEvent.which;
+ key = String.fromCharCode(key);
+ }
+ var regex = /[0-9]|\./;
+
+ if( !regex.test(key) ) {
+ theEvent.returnValue = false;
+ if(theEvent.preventDefault) theEvent.preventDefault();
+ }*/
+}
+
+
+/* for variant sample */
+jQuery(document).on('change', '.shopify-product-form .single-option-selector, .shopify-product-form .product-select', function () {
+ var $form = jQuery(this).closest('.shopify-product-form');
+
+ var isOptionSelector = jQuery(this).hasClass('single-option-selector');
+
+ var run = function () {
+ var $variantSelect = $form.find('.product-select');
+ var $selected;
+ var selectedSku = '';
+ var optionText = '';
+
+ if ($variantSelect.length && $variantSelect.is('select')) {
+ $selected = $variantSelect.find(':selected');
+ selectedSku = ($selected.attr('data-sku') || '').toString();
+ optionText = ($selected.text() || '').toString();
+ } else if ($variantSelect.length && $variantSelect.is('input')) {
+ // 1-variant products: .product-select is a hidden input
+ selectedSku = ($variantSelect.attr('data-sku') || '').toString();
+ optionText = ($variantSelect.attr('data-variant-title') || '').toString();
+ } else {
+ $selected = jQuery(this).find(':selected');
+ selectedSku = ($selected.attr('data-sku') || '').toString();
+ optionText = ($selected.text() || '').toString();
+ }
+
+ // If we can't determine anything, don't flip the UI.
+ if (!selectedSku && !optionText) return;
+
+ var isSkuSample = /-+sample$/i.test(selectedSku);
+ var isTextSample = /\bsample\b/i.test(optionText);
+ var isPureSample = /^sample(\b|[^a-z])/i.test(optionText.trim());
+ var isSample = isSkuSample || isTextSample;
+
+ var $qty = $form.find('.mm_quantity .qty');
+ var $qtyWrapper = $form.find('.mm_quantity');
+
+ // 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();
+
+ if (isSample) {
+ $qty.val(1);
+ $qty.attr({ step: 1, min: 1, max: 1 });
+ $qtyWrapper.find('.plus, .minus').prop('disabled', true);
+ $qtyWrapper.find('.hquantity').attr('value', 1);
+
+ $form.find('.product-option-quantity-label').addClass('hidden').hide();
+ $qtyWrapper.addClass('hidden').hide();
+ $form.find('.add-to-cart').addClass('hidden').hide();
+
+ if (!isPureSample) {
+ $form.find('.dl-second-sample-btn').removeClass('unavailable');
+ } else if (!$form.find('.dl-second-sample-btn').hasClass('unavailable')) {
+ $form.find('.dl-second-sample-btn').addClass('unavailable');
+ }
+ } else {
+ $form.find('.product-option-quantity-label').removeClass('hidden').show();
+ $qtyWrapper.removeClass('hidden').show();
+ $form.find('.add-to-cart').removeClass('hidden').show();
+
+ var minValue = originalMin;
+ var currentValue = parseFloat($qty.val()) || 0;
+ if (currentValue < minValue) {
+ $qty.val(minValue);
+ $qtyWrapper.find('.hquantity').attr('value', minValue);
+ }
+ $qty.attr({ step: originalStep, min: minValue });
+ $qty.removeAttr('max');
+ $qtyWrapper.find('.plus, .minus').prop('disabled', false);
+ }
+
+ // Update the SKU display
+ var displaySku = selectedSku
+ .replace(/-+sample$/i, '')
+ .replace(/-+$/g, '');
+ jQuery('.product-sku-value').text(displaySku);
+
+ // Trade discount text
+ if (jQuery('#customer_tag').val() === 'trade') {
+ requestAnimationFrame(function () {
+ requestAnimationFrame(function () {
+ if (isSample) {
+ jQuery('.product-price-discount').text('Complimentary Sample');
+ } else {
+ // Use the currently displayed currency (symbol/code) from the UI, not hardcoded USD.
+ // Also support both "298,95" and "298.95" style decimals.
+ function dlParseDisplayedNumber(numStr) {
+ var s = (numStr || '').toString().replace(/\s/g, '');
+ var lastComma = s.lastIndexOf(',');
+ var lastDot = s.lastIndexOf('.');
+
+ // No separators: plain number
+ if (lastComma === -1 && lastDot === -1) {
+ var plain = parseFloat(s.replace(/[^\d.-]/g, ''));
+ return isFinite(plain) ? plain : NaN;
+ }
+
+ // Decide decimal separator by whichever comes last
+ var decimalSep = lastComma > lastDot ? ',' : '.';
+ var otherSep = decimalSep === ',' ? '.' : ',';
+
+ var parts = s.split(decimalSep);
+ if (!parts.length) return NaN;
+ var intPart = (parts[0] || '').replace(new RegExp('[\\' + otherSep + ']', 'g'), '').replace(/[^\d-]/g, '');
+ var fracPart = (parts[1] || '').replace(/[^\d]/g, '');
+ var normalized = intPart + (fracPart ? ('.' + fracPart) : '');
+ var val = parseFloat(normalized);
+ return isFinite(val) ? val : NaN;
+ }
+
+ function dlFormatLike(numStr, value) {
+ var s = (numStr || '').toString().replace(/\s/g, '');
+ var lastComma = s.lastIndexOf(',');
+ var lastDot = s.lastIndexOf('.');
+ var decimalSep = lastComma === -1 && lastDot === -1 ? '.' : (lastComma > lastDot ? ',' : '.');
+ var thousandsSep = decimalSep === ',' ? '.' : ',';
+
+ var hasThousands = false;
+ if (lastComma !== -1 || lastDot !== -1) {
+ var decIndex = decimalSep === ',' ? lastComma : lastDot;
+ var intStr = s.slice(0, decIndex);
+ hasThousands = intStr.indexOf(thousandsSep) !== -1;
+ }
+
+ var fixed = (Math.round(value * 100) / 100).toFixed(2);
+ var fixedParts = fixed.split('.');
+ var intPart = fixedParts[0];
+ var fracPart = fixedParts[1] || '00';
+
+ if (hasThousands) {
+ intPart = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, thousandsSep);
+ }
+
+ return intPart + decimalSep + fracPart;
+ }
+
+ var priceText = (jQuery('.product-price-minimum').first().text() || '').trim();
+ var re = /[\d.,]+/;
+ var m = re.exec(priceText);
+ if (!m) return;
+
+ var numberStr = m[0];
+ var prefix = priceText.slice(0, m.index);
+ var suffix = priceText.slice(m.index + numberStr.length);
+
+ var priceValue = dlParseDisplayedNumber(numberStr);
+ if (!isFinite(priceValue)) return;
+
+ var discountedValue = priceValue * 0.85;
+ var formattedNumber = dlFormatLike(numberStr, discountedValue);
+ jQuery('.product-price-discount').text(prefix + formattedNumber + suffix);
+ }
+ });
+ });
+ }
+ }.bind(this);
+
+ var schedule = function () {
+ requestAnimationFrame(function () {
+ requestAnimationFrame(run);
+ });
+ };
+
+ if (isOptionSelector) {
+ setTimeout(schedule, 60);
+ } else {
+ schedule();
+ }
+});
+
+
+//hide ddl and show last variant
+
+if($('#has_display_variant').length>0 && $('#has_display_variant').val() == "no"){
+ $('.last_variant').hide();
+ if ( $( ".shopify-product-form .single-option-selector" ).length ) {
+
+ $('.shopify-product-form .single-option-selector').hide();
+
+ setTimeout(function(){
+ $('.product-options .select-wrapper').hide();
+ }, 500);
+
+ $('.last_variant').show();
+
+ var single_opt_ind=0;
+ $('.shopify-product-form .single-option-selector option').each(function(index, option){
+ single_opt_ind=index;
+ });
+
+
+ /* DW deep-link honor (vp-dw-commerce 2026-06-24) */
+ // Honor an explicit ?variant=<id> deep-link: if the URL names a variant that
+ // matches one of the options, select THAT option instead of the computed default,
+ // so a link to the Sample renders $4.25 and a link to the Roll renders the roll price.
+ try {
+ var dlWanted = new URLSearchParams(window.location.search).get('variant');
+ if (dlWanted) {
+ $dlOpts.each(function(index, option){
+ var ov = (option.value || '').toString();
+ var dv = (option.getAttribute && option.getAttribute('data-variant-id') || '').toString();
+ if (ov === dlWanted || dv === dlWanted) { single_opt_ind = index; }
+ });
+ }
+ } catch (e) {}
+ /* /DW deep-link honor */
+ $(".shopify-product-form .single-option-selector").prop('selectedIndex', single_opt_ind).trigger('change');
+ }
+}
+//--------------------------------
+
+// Prevent rapid multi-clicks from adding multiple samples before mini-cart UI updates
+function dlGetSampleBtns(variantId) {
+ return $(".dl-sample-btn[data-dlvid='" + variantId + "'], .dl-second-sample-btn[data-dlvid='" + variantId + "']");
+}
+function dlIsSampleLocked(variantId) {
+ return dlGetSampleBtns(variantId).first().attr("data-dl-locked") === "1";
+}
+function dlLockSampleBtns(variantId) {
+ var $btns = dlGetSampleBtns(variantId);
+ $btns.attr("data-dl-locked", "1").addClass("dl-click-locked").attr("aria-disabled", "true");
+ $btns.each(function() {
+ var $b = $(this);
+ if ($b.is("button, input")) $b.prop("disabled", true);
+ });
+}
+function dlUnlockSampleBtns(variantId) {
+ var $btns = dlGetSampleBtns(variantId);
+ $btns.removeAttr("data-dl-locked").removeClass("dl-click-locked").removeAttr("aria-disabled");
+ $btns.each(function() {
+ var $b = $(this);
+ if ($b.is("button, input")) $b.prop("disabled", false);
+ });
+}
+function dlUnlockSampleBtnsWhenInCart(variantId, maxMs) {
+ var start = Date.now();
+ var timer = setInterval(function() {
+ if ($(".mini-cart-item-wrapper [data-variant='" + variantId + "']").length > 0) {
+ clearInterval(timer);
+ dlUnlockSampleBtns(variantId);
+ return;
+ }
+ if (Date.now() - start >= (maxMs || 20000)) {
+ clearInterval(timer);
+ dlUnlockSampleBtns(variantId);
+ }
+ }, 250);
+}
+
+
+$(document).on("click", ".dl-sample-btn", function(e) {
+ e.preventDefault()
+ var $btn = $(this);
+ var $form = $btn.closest("form");
+ var id = $btn.attr("data-dlvid");
+ //check if the sample was added already
+ if($(".mini-cart-item-wrapper [data-variant='"+id+"']").length == 0) {
+ if (dlIsSampleLocked(id)) return;
+ dlLockSampleBtns(id);
+ dlUnlockSampleBtnsWhenInCart(id, 20000);
+
+ var product_id = $(this).attr("data-product-id");
+ var $productSelect = $form.find("#product-select-"+product_id);
+ if (!$productSelect.length) $productSelect = $form.find(".product-select");
+ var first_selected_id = $productSelect.is("select") ? $productSelect.find(":selected").val() : $productSelect.val();
+ $productSelect.val(id).trigger('change');
+ //
+ var $qtyInput = $form.find(".input-text.qty");
+ var default_quantity = $qtyInput.val();
+ $qtyInput.val(1);
+ $form.find(".add-to-cart.cowlendar-add-to-cart").trigger("click");
+
+ //go back to original selected status
+
+ $qtyInput.val(default_quantity);
+ if (first_selected_id && first_selected_id !== id) {
+ $productSelect.val(first_selected_id).trigger('change');
+ }
+ } else {
+ //alert("1 sample only");
+ if($(".product-message").hasClass("success-message") || $(".mini-cart-item-wrapper [data-variant='"+id+"']").length > 0) {
+ $(".product-message").removeClass("success-message").addClass("error-message");
+ $(".product-message").text("1 Sample per SKU");
+ }
+ }
+});
+$(document).on("click", ".dl-second-sample-btn", function(e) {
+ e.preventDefault()
+ var $btn = $(this);
+ var $form = $btn.closest("form");
+ var id = $btn.attr("data-dlvid");
+ //check if the sample was added already
+ if($(".mini-cart-item-wrapper [data-variant='"+id+"']").length == 0) {
+ if (dlIsSampleLocked(id)) return;
+ dlLockSampleBtns(id);
+ dlUnlockSampleBtnsWhenInCart(id, 20000);
+
+ //
+ var $qtyInput = $form.find(".input-text.qty");
+ var default_quantity = $qtyInput.val();
+ $qtyInput.val(1);
+ $form.find(".add-to-cart.cowlendar-add-to-cart").trigger("click");
+
+ //go back to original selected status
+
+ $qtyInput.val(default_quantity);
+ } else {
+ //alert("1 sample only");
+ if($(".product-message").hasClass("success-message") || $(".mini-cart-item-wrapper [data-variant='"+id+"']").length > 0) {
+ $(".product-message").removeClass("success-message").addClass("error-message");
+ $(".product-message").text("1 Sample per SKU");
+ }
+ }
+});
+
+//add the remove functionality on the mini-cart
+$(document).on("click", ".remove-item", function(e) {
+ e.preventDefault();
+ var variantId = $(this).closest('.mini-cart-item').attr("data-variant");
+ var quantity = parseInt($(this).closest(".mini-cart-item").find(".mini-cart-item-quantity").find("span").text());
+ var cart_num = parseInt($(".cart-count .cart-count-number").text());
+ var reminder_num = cart_num - quantity;
+ $.ajax({
+ type: "POST",
+ url: "/cart/change.js",
+ dataType: "json",
+ data: {
+ id: parseFloat(variantId),
+ quantity: 0,
+ },
+ }).then((data) => {
+ console.log("success");
+ $(this).closest('.mini-cart-item').remove();
+ dlUnlockSampleBtns(variantId);
+ $(".cart-count .cart-count-number").text(parseInt(reminder_num));
+ if(reminder_num == 0) {
+ $(".mini-cart").hide();
+ }
+ });
+});
+
+$(document).on("click", ".upsell-add-to-cart", function() {
+ var id = $(this).attr("data-item-id");
+ var num = parseInt($(this).attr("data-num"));
+ $.ajax({
+ type: 'POST',
+ url: '/cart/add.js',
+ dataType: 'json',
+ data: {
+ items: [
+ {
+ quantity: num,
+ id: id
+ }
+ ]
+ },
+ success: function (data){
+ location.href= "/cart";
+ }
+ });
+});
+
+/* Custom code for sascha */
+function showMiniCartIfReady() {
+ if (window.innerWidth <= 767) {
+ return;
+ }
+ var $miniCart = $(".mini-cart");
+ if ($miniCart.length && !$miniCart.hasClass("empty")) {
+ $(".mini-cart-wrapper").addClass("is-open");
+ }
+}
+
+function watchMiniCartUpdates() {
+ var target = document.querySelector(".mini-cart-item-wrapper");
+ if (!target || !window.MutationObserver) {
+ return;
+ }
+ var observer = new MutationObserver(function() {
+ showMiniCartIfReady();
+ });
+ observer.observe(target, { childList: true, subtree: true });
+}
+
+$(document).ready(function() {
+ watchMiniCartUpdates();
+});
+
+$(document).on("click", ".add-to-cart.cowlendar-add-to-cart", function() {
+ showMiniCartIfReady();
+});
+
+$(document).on("click", function(event) {
+ var $target = $(event.target);
+ if ($target.closest(".mini-cart, .mini-cart-wrapper").length === 0) {
+ $(".mini-cart-wrapper").removeClass("is-open");
+ }
+});
+
+$(document).on("mouseleave", ".mini-cart-wrapper", function() {
+ $(this).removeClass("is-open");
+});
\ No newline at end of file
diff --git a/shopify/theme-fixes/custom-js-qty-2026-06-30/block_moq.html b/shopify/theme-fixes/custom-js-qty-2026-06-30/block_moq.html
new file mode 100644
index 00000000..0eaf6bff
--- /dev/null
+++ b/shopify/theme-fixes/custom-js-qty-2026-06-30/block_moq.html
@@ -0,0 +1,21 @@
+<div class="mm_quantity mm_buttons_added product-option-row-4 ">
+ <input type="hidden" class="hquantity" name="hquantity" value="2">
+ <input type="button" value="-" class="minus">
+ <input
+ type="text"
+ step="2"
+ min="2"
+ data-order-step="2"
+ data-order-min="2"
+ max=""
+ name="quantity"
+ value="2"
+ title="Qty"
+ class="input-text qty text"
+ size="4"
+ pattern=""
+ inputmode=""
+ onkeypress="mm_validate(event)"
+ >
+ <input type="button" value="+" class="plus">
+ </div>
\ No newline at end of file
diff --git a/shopify/theme-fixes/custom-js-qty-2026-06-30/block_std.html b/shopify/theme-fixes/custom-js-qty-2026-06-30/block_std.html
new file mode 100644
index 00000000..fc26945a
--- /dev/null
+++ b/shopify/theme-fixes/custom-js-qty-2026-06-30/block_std.html
@@ -0,0 +1,21 @@
+<div class="mm_quantity mm_buttons_added product-option-row-4 ">
+ <input type="hidden" class="hquantity" name="hquantity" value="1">
+ <input type="button" value="-" class="minus">
+ <input
+ type="text"
+ step="1"
+ min="1"
+ data-order-step="1"
+ data-order-min="1"
+ max=""
+ name="quantity"
+ value="1"
+ title="Qty"
+ class="input-text qty text"
+ size="4"
+ pattern=""
+ inputmode=""
+ onkeypress="mm_validate(event)"
+ >
+ <input type="button" value="+" class="plus">
+ </div>
\ No newline at end of file
diff --git a/shopify/theme-fixes/custom-js-qty-2026-06-30/clicktest.cjs b/shopify/theme-fixes/custom-js-qty-2026-06-30/clicktest.cjs
new file mode 100644
index 00000000..cc54094b
--- /dev/null
+++ b/shopify/theme-fixes/custom-js-qty-2026-06-30/clicktest.cjs
@@ -0,0 +1,32 @@
+const { chromium } = require(require('path').join(process.env.HOME, 'Projects/Designer-Wallcoverings/node_modules/playwright'));
+const THEME=process.argv[2];
+const URL=`https://designer-laboratory-sandbox.myshopify.com/products/dwtt-80064-designer-wallcoverings-los-angeles?preview_theme_id=${THEME}`;
+(async()=>{
+ const b=await chromium.launch();const p=await(await b.newContext()).newPage();
+ const errs=[];p.on('pageerror',e=>errs.push('PE: '+e.message));
+ await p.goto(URL,{waitUntil:'domcontentloaded',timeout:60000});
+ await p.waitForSelector('.mm_quantity input[name="quantity"]',{state:'attached',timeout:30000});
+ await p.waitForTimeout(1800);
+ await p.evaluate(()=>{const s=document.querySelector('.product-select');if(s&&s.tagName==='SELECT'){const o=Array.from(s.options).find(x=>!/sample/i.test(x.textContent)&&!/-sample$/i.test(x.getAttribute('data-sku')||''))||s.options[0];s.value=o.value;s.dispatchEvent(new Event('change',{bubbles:true}));if(window.jQuery)window.jQuery(s).trigger('change');}});
+ await p.waitForTimeout(1500);
+ // Read the actual attributes the handler will see
+ const attrs=await p.evaluate(()=>{const q=document.querySelector('.mm_quantity .qty');return{dataOrderStep:q.getAttribute('data-order-step'),dataOrderMin:q.getAttribute('data-order-min'),step:q.getAttribute('step'),min:q.getAttribute('min'),val:q.val?undefined:q.value};});
+ console.log('ATTRS:',JSON.stringify(attrs));
+ // Count how many handlers are bound to .plus via jQuery events (document-delegated so can't easily count). Instead, dispatch a real jQuery click and read value.
+ const r=await p.evaluate(async()=>{
+ const $=window.jQuery;const btn=document.querySelector('.mm_quantity .plus');
+ const before=document.querySelector('.mm_quantity .qty').value;
+ $(btn).trigger('click');
+ await new Promise(r=>setTimeout(r,200));
+ const after=document.querySelector('.mm_quantity .qty').value;
+ const h=document.querySelector('.mm_quantity .hquantity').getAttribute('value');
+ return {before,after,h};
+ });
+ console.log('jQuery .plus click:',JSON.stringify(r));
+ // Now try native playwright click
+ await p.click('.mm_quantity .plus');await p.waitForTimeout(250);
+ const r2=await p.evaluate(()=>({val:document.querySelector('.mm_quantity .qty').value,h:document.querySelector('.mm_quantity .hquantity').getAttribute('value')}));
+ console.log('native click then:',JSON.stringify(r2));
+ console.log('errs:',JSON.stringify(errs));
+ await b.close();
+})().catch(e=>{console.error(e);process.exit(1)});
diff --git a/shopify/theme-fixes/custom-js-qty-2026-06-30/custom.ORIGINAL.js b/shopify/theme-fixes/custom-js-qty-2026-06-30/custom.ORIGINAL.js
new file mode 100644
index 00000000..64d9005e
--- /dev/null
+++ b/shopify/theme-fixes/custom-js-qty-2026-06-30/custom.ORIGINAL.js
@@ -0,0 +1,511 @@
+/*========== 020 Custom js for quantity ===================*/
+
+function mm_refresh_quantity_increments() {
+ jQuery("div.mm_quantity:not(.mm_buttons_added), td.mm_quantity:not(.mm_buttons_added)").each(function(a, b) {
+ var c = jQuery(b);
+ c.addClass("mm_buttons_added"), c.children().first().before('<input type="button" value="-" class="minus" />'), c.children().last().after('<input type="button" value="+" class="plus" />')
+ })
+}
+String.prototype.getDecimals || (String.prototype.getDecimals = function() {
+ var a = this,
+ b = ("" + a).match(/(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/);
+ return b ? Math.max(0, (b[1] ? b[1].length : 0) - (b[2] ? +b[2] : 0)) : 0
+}), jQuery(document).ready(function() {
+ mm_refresh_quantity_increments()
+
+ // Custom popup contact form (moved from snippets/product.liquid)
+ if (jQuery('.custom-popup-contact-form').length > 0) {
+ jQuery('.custom-popup-contact-form').on('click', function (e) {
+ e.preventDefault();
+ jQuery('#custom-popup-overlay').show();
+ });
+ }
+
+ if (jQuery('#custom-popup-overlay .custom-close').length > 0) {
+ jQuery('#custom-popup-overlay .custom-close').on('click', function () {
+ jQuery('#custom-popup-overlay').hide();
+ });
+ }
+
+ if (jQuery('#custom-popup-overlay .success-message').length > 0) {
+ jQuery('.custom-contact-message').html('Thank you. We will respond to your message shortly.');
+ }
+
+ if (jQuery('#custom-popup-overlay .error-message').length > 0) {
+ jQuery('.custom-contact-message').html(jQuery('#custom-popup-overlay .error-message').html());
+ }
+}), jQuery(document).on("updated_wc_div", function() {
+ 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"
+ }
+
+
+});
+
+async function updateItem(variantId, quantity, properties={}){
+ const result = await fetch("/cart/change.json", {
+ method:"post",
+ headers: {
+ "Content-type": "application/json",
+ "Accept": "application/json"
+ },
+ body: JSON.stringify({
+ id: variantId,
+ quantity: quantity,
+ properties: properties
+ })
+ })
+ return result.json();
+}
+
+function mm_validate(evt) {
+ var theEvent = evt || window.event;
+ theEvent.preventDefault()
+ /*
+ if (theEvent.type === 'paste') {
+ key = event.clipboardData.getData('text/plain');
+ } else {
+ var key = theEvent.keyCode || theEvent.which;
+ key = String.fromCharCode(key);
+ }
+ var regex = /[0-9]|\./;
+
+ if( !regex.test(key) ) {
+ theEvent.returnValue = false;
+ if(theEvent.preventDefault) theEvent.preventDefault();
+ }*/
+}
+
+
+/* for variant sample */
+jQuery(document).on('change', '.shopify-product-form .single-option-selector, .shopify-product-form .product-select', function () {
+ var $form = jQuery(this).closest('.shopify-product-form');
+
+ var isOptionSelector = jQuery(this).hasClass('single-option-selector');
+
+ var run = function () {
+ var $variantSelect = $form.find('.product-select');
+ var $selected;
+ var selectedSku = '';
+ var optionText = '';
+
+ if ($variantSelect.length && $variantSelect.is('select')) {
+ $selected = $variantSelect.find(':selected');
+ selectedSku = ($selected.attr('data-sku') || '').toString();
+ optionText = ($selected.text() || '').toString();
+ } else if ($variantSelect.length && $variantSelect.is('input')) {
+ // 1-variant products: .product-select is a hidden input
+ selectedSku = ($variantSelect.attr('data-sku') || '').toString();
+ optionText = ($variantSelect.attr('data-variant-title') || '').toString();
+ } else {
+ $selected = jQuery(this).find(':selected');
+ selectedSku = ($selected.attr('data-sku') || '').toString();
+ optionText = ($selected.text() || '').toString();
+ }
+
+ // If we can't determine anything, don't flip the UI.
+ if (!selectedSku && !optionText) return;
+
+ var isSkuSample = /-+sample$/i.test(selectedSku);
+ var isTextSample = /\bsample\b/i.test(optionText);
+ var isPureSample = /^sample(\b|[^a-z])/i.test(optionText.trim());
+ var isSample = isSkuSample || isTextSample;
+
+ 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);
+ }
+
+ // Sample request buttons are always available (hide/show is variant-driven only)
+ $form.find('.dl-sample-btn, .dl-second-sample-btn').show();
+
+ if (isSample) {
+ $qty.val(1);
+ $qty.attr({ step: 1, min: 1, max: 1 });
+ $qtyWrapper.find('.plus, .minus').prop('disabled', true);
+ $qtyWrapper.find('.hquantity').attr('value', 1);
+
+ $form.find('.product-option-quantity-label').addClass('hidden').hide();
+ $qtyWrapper.addClass('hidden').hide();
+ $form.find('.add-to-cart').addClass('hidden').hide();
+
+ if (!isPureSample) {
+ $form.find('.dl-second-sample-btn').removeClass('unavailable');
+ } else if (!$form.find('.dl-second-sample-btn').hasClass('unavailable')) {
+ $form.find('.dl-second-sample-btn').addClass('unavailable');
+ }
+ } else {
+ $form.find('.product-option-quantity-label').removeClass('hidden').show();
+ $qtyWrapper.removeClass('hidden').show();
+ $form.find('.add-to-cart').removeClass('hidden').show();
+
+ var minValue = originalMin;
+ var currentValue = parseFloat($qty.val()) || 0;
+ if (currentValue < minValue) {
+ $qty.val(minValue);
+ $qtyWrapper.find('.hquantity').attr('value', minValue);
+ }
+ $qty.attr({ step: originalStep, min: minValue });
+ $qty.removeAttr('max');
+ $qtyWrapper.find('.plus, .minus').prop('disabled', false);
+ }
+
+ // Update the SKU display
+ var displaySku = selectedSku
+ .replace(/-+sample$/i, '')
+ .replace(/-+$/g, '');
+ jQuery('.product-sku-value').text(displaySku);
+
+ // Trade discount text
+ if (jQuery('#customer_tag').val() === 'trade') {
+ requestAnimationFrame(function () {
+ requestAnimationFrame(function () {
+ if (isSample) {
+ jQuery('.product-price-discount').text('Complimentary Sample');
+ } else {
+ // Use the currently displayed currency (symbol/code) from the UI, not hardcoded USD.
+ // Also support both "298,95" and "298.95" style decimals.
+ function dlParseDisplayedNumber(numStr) {
+ var s = (numStr || '').toString().replace(/\s/g, '');
+ var lastComma = s.lastIndexOf(',');
+ var lastDot = s.lastIndexOf('.');
+
+ // No separators: plain number
+ if (lastComma === -1 && lastDot === -1) {
+ var plain = parseFloat(s.replace(/[^\d.-]/g, ''));
+ return isFinite(plain) ? plain : NaN;
+ }
+
+ // Decide decimal separator by whichever comes last
+ var decimalSep = lastComma > lastDot ? ',' : '.';
+ var otherSep = decimalSep === ',' ? '.' : ',';
+
+ var parts = s.split(decimalSep);
+ if (!parts.length) return NaN;
+ var intPart = (parts[0] || '').replace(new RegExp('[\\' + otherSep + ']', 'g'), '').replace(/[^\d-]/g, '');
+ var fracPart = (parts[1] || '').replace(/[^\d]/g, '');
+ var normalized = intPart + (fracPart ? ('.' + fracPart) : '');
+ var val = parseFloat(normalized);
+ return isFinite(val) ? val : NaN;
+ }
+
+ function dlFormatLike(numStr, value) {
+ var s = (numStr || '').toString().replace(/\s/g, '');
+ var lastComma = s.lastIndexOf(',');
+ var lastDot = s.lastIndexOf('.');
+ var decimalSep = lastComma === -1 && lastDot === -1 ? '.' : (lastComma > lastDot ? ',' : '.');
+ var thousandsSep = decimalSep === ',' ? '.' : ',';
+
+ var hasThousands = false;
+ if (lastComma !== -1 || lastDot !== -1) {
+ var decIndex = decimalSep === ',' ? lastComma : lastDot;
+ var intStr = s.slice(0, decIndex);
+ hasThousands = intStr.indexOf(thousandsSep) !== -1;
+ }
+
+ var fixed = (Math.round(value * 100) / 100).toFixed(2);
+ var fixedParts = fixed.split('.');
+ var intPart = fixedParts[0];
+ var fracPart = fixedParts[1] || '00';
+
+ if (hasThousands) {
+ intPart = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, thousandsSep);
+ }
+
+ return intPart + decimalSep + fracPart;
+ }
+
+ var priceText = (jQuery('.product-price-minimum').first().text() || '').trim();
+ var re = /[\d.,]+/;
+ var m = re.exec(priceText);
+ if (!m) return;
+
+ var numberStr = m[0];
+ var prefix = priceText.slice(0, m.index);
+ var suffix = priceText.slice(m.index + numberStr.length);
+
+ var priceValue = dlParseDisplayedNumber(numberStr);
+ if (!isFinite(priceValue)) return;
+
+ var discountedValue = priceValue * 0.85;
+ var formattedNumber = dlFormatLike(numberStr, discountedValue);
+ jQuery('.product-price-discount').text(prefix + formattedNumber + suffix);
+ }
+ });
+ });
+ }
+ }.bind(this);
+
+ var schedule = function () {
+ requestAnimationFrame(function () {
+ requestAnimationFrame(run);
+ });
+ };
+
+ if (isOptionSelector) {
+ setTimeout(schedule, 60);
+ } else {
+ schedule();
+ }
+});
+
+
+//hide ddl and show last variant
+
+if($('#has_display_variant').length>0 && $('#has_display_variant').val() == "no"){
+ $('.last_variant').hide();
+ if ( $( ".shopify-product-form .single-option-selector" ).length ) {
+
+ $('.shopify-product-form .single-option-selector').hide();
+
+ setTimeout(function(){
+ $('.product-options .select-wrapper').hide();
+ }, 500);
+
+ $('.last_variant').show();
+
+ var single_opt_ind=0;
+ $('.shopify-product-form .single-option-selector option').each(function(index, option){
+ single_opt_ind=index;
+ });
+
+
+ /* DW deep-link honor (vp-dw-commerce 2026-06-24) */
+ // Honor an explicit ?variant=<id> deep-link: if the URL names a variant that
+ // matches one of the options, select THAT option instead of the computed default,
+ // so a link to the Sample renders $4.25 and a link to the Roll renders the roll price.
+ try {
+ var dlWanted = new URLSearchParams(window.location.search).get('variant');
+ if (dlWanted) {
+ $dlOpts.each(function(index, option){
+ var ov = (option.value || '').toString();
+ var dv = (option.getAttribute && option.getAttribute('data-variant-id') || '').toString();
+ if (ov === dlWanted || dv === dlWanted) { single_opt_ind = index; }
+ });
+ }
+ } catch (e) {}
+ /* /DW deep-link honor */
+ $(".shopify-product-form .single-option-selector").prop('selectedIndex', single_opt_ind).trigger('change');
+ }
+}
+//--------------------------------
+
+// Prevent rapid multi-clicks from adding multiple samples before mini-cart UI updates
+function dlGetSampleBtns(variantId) {
+ return $(".dl-sample-btn[data-dlvid='" + variantId + "'], .dl-second-sample-btn[data-dlvid='" + variantId + "']");
+}
+function dlIsSampleLocked(variantId) {
+ return dlGetSampleBtns(variantId).first().attr("data-dl-locked") === "1";
+}
+function dlLockSampleBtns(variantId) {
+ var $btns = dlGetSampleBtns(variantId);
+ $btns.attr("data-dl-locked", "1").addClass("dl-click-locked").attr("aria-disabled", "true");
+ $btns.each(function() {
+ var $b = $(this);
+ if ($b.is("button, input")) $b.prop("disabled", true);
+ });
+}
+function dlUnlockSampleBtns(variantId) {
+ var $btns = dlGetSampleBtns(variantId);
+ $btns.removeAttr("data-dl-locked").removeClass("dl-click-locked").removeAttr("aria-disabled");
+ $btns.each(function() {
+ var $b = $(this);
+ if ($b.is("button, input")) $b.prop("disabled", false);
+ });
+}
+function dlUnlockSampleBtnsWhenInCart(variantId, maxMs) {
+ var start = Date.now();
+ var timer = setInterval(function() {
+ if ($(".mini-cart-item-wrapper [data-variant='" + variantId + "']").length > 0) {
+ clearInterval(timer);
+ dlUnlockSampleBtns(variantId);
+ return;
+ }
+ if (Date.now() - start >= (maxMs || 20000)) {
+ clearInterval(timer);
+ dlUnlockSampleBtns(variantId);
+ }
+ }, 250);
+}
+
+
+$(document).on("click", ".dl-sample-btn", function(e) {
+ e.preventDefault()
+ var $btn = $(this);
+ var $form = $btn.closest("form");
+ var id = $btn.attr("data-dlvid");
+ //check if the sample was added already
+ if($(".mini-cart-item-wrapper [data-variant='"+id+"']").length == 0) {
+ if (dlIsSampleLocked(id)) return;
+ dlLockSampleBtns(id);
+ dlUnlockSampleBtnsWhenInCart(id, 20000);
+
+ var product_id = $(this).attr("data-product-id");
+ var $productSelect = $form.find("#product-select-"+product_id);
+ if (!$productSelect.length) $productSelect = $form.find(".product-select");
+ var first_selected_id = $productSelect.is("select") ? $productSelect.find(":selected").val() : $productSelect.val();
+ $productSelect.val(id).trigger('change');
+ //
+ var $qtyInput = $form.find(".input-text.qty");
+ var default_quantity = $qtyInput.val();
+ $qtyInput.val(1);
+ $form.find(".add-to-cart.cowlendar-add-to-cart").trigger("click");
+
+ //go back to original selected status
+
+ $qtyInput.val(default_quantity);
+ if (first_selected_id && first_selected_id !== id) {
+ $productSelect.val(first_selected_id).trigger('change');
+ }
+ } else {
+ //alert("1 sample only");
+ if($(".product-message").hasClass("success-message") || $(".mini-cart-item-wrapper [data-variant='"+id+"']").length > 0) {
+ $(".product-message").removeClass("success-message").addClass("error-message");
+ $(".product-message").text("1 Sample per SKU");
+ }
+ }
+});
+$(document).on("click", ".dl-second-sample-btn", function(e) {
+ e.preventDefault()
+ var $btn = $(this);
+ var $form = $btn.closest("form");
+ var id = $btn.attr("data-dlvid");
+ //check if the sample was added already
+ if($(".mini-cart-item-wrapper [data-variant='"+id+"']").length == 0) {
+ if (dlIsSampleLocked(id)) return;
+ dlLockSampleBtns(id);
+ dlUnlockSampleBtnsWhenInCart(id, 20000);
+
+ //
+ var $qtyInput = $form.find(".input-text.qty");
+ var default_quantity = $qtyInput.val();
+ $qtyInput.val(1);
+ $form.find(".add-to-cart.cowlendar-add-to-cart").trigger("click");
+
+ //go back to original selected status
+
+ $qtyInput.val(default_quantity);
+ } else {
+ //alert("1 sample only");
+ if($(".product-message").hasClass("success-message") || $(".mini-cart-item-wrapper [data-variant='"+id+"']").length > 0) {
+ $(".product-message").removeClass("success-message").addClass("error-message");
+ $(".product-message").text("1 Sample per SKU");
+ }
+ }
+});
+
+//add the remove functionality on the mini-cart
+$(document).on("click", ".remove-item", function(e) {
+ e.preventDefault();
+ var variantId = $(this).closest('.mini-cart-item').attr("data-variant");
+ var quantity = parseInt($(this).closest(".mini-cart-item").find(".mini-cart-item-quantity").find("span").text());
+ var cart_num = parseInt($(".cart-count .cart-count-number").text());
+ var reminder_num = cart_num - quantity;
+ $.ajax({
+ type: "POST",
+ url: "/cart/change.js",
+ dataType: "json",
+ data: {
+ id: parseFloat(variantId),
+ quantity: 0,
+ },
+ }).then((data) => {
+ console.log("success");
+ $(this).closest('.mini-cart-item').remove();
+ dlUnlockSampleBtns(variantId);
+ $(".cart-count .cart-count-number").text(parseInt(reminder_num));
+ if(reminder_num == 0) {
+ $(".mini-cart").hide();
+ }
+ });
+});
+
+$(document).on("click", ".upsell-add-to-cart", function() {
+ var id = $(this).attr("data-item-id");
+ var num = parseInt($(this).attr("data-num"));
+ $.ajax({
+ type: 'POST',
+ url: '/cart/add.js',
+ dataType: 'json',
+ data: {
+ items: [
+ {
+ quantity: num,
+ id: id
+ }
+ ]
+ },
+ success: function (data){
+ location.href= "/cart";
+ }
+ });
+});
+
+/* Custom code for sascha */
+function showMiniCartIfReady() {
+ if (window.innerWidth <= 767) {
+ return;
+ }
+ var $miniCart = $(".mini-cart");
+ if ($miniCart.length && !$miniCart.hasClass("empty")) {
+ $(".mini-cart-wrapper").addClass("is-open");
+ }
+}
+
+function watchMiniCartUpdates() {
+ var target = document.querySelector(".mini-cart-item-wrapper");
+ if (!target || !window.MutationObserver) {
+ return;
+ }
+ var observer = new MutationObserver(function() {
+ showMiniCartIfReady();
+ });
+ observer.observe(target, { childList: true, subtree: true });
+}
+
+$(document).ready(function() {
+ watchMiniCartUpdates();
+});
+
+$(document).on("click", ".add-to-cart.cowlendar-add-to-cart", function() {
+ showMiniCartIfReady();
+});
+
+$(document).on("click", function(event) {
+ var $target = $(event.target);
+ if ($target.closest(".mini-cart, .mini-cart-wrapper").length === 0) {
+ $(".mini-cart-wrapper").removeClass("is-open");
+ }
+});
+
+$(document).on("mouseleave", ".mini-cart-wrapper", function() {
+ $(this).removeClass("is-open");
+});
\ No newline at end of file
diff --git a/shopify/theme-fixes/custom-js-qty-2026-06-30/custom.js b/shopify/theme-fixes/custom-js-qty-2026-06-30/custom.js
index 64d9005e..f86ee6f3 100644
--- a/shopify/theme-fixes/custom-js-qty-2026-06-30/custom.js
+++ b/shopify/theme-fixes/custom-js-qty-2026-06-30/custom.js
@@ -38,27 +38,93 @@ String.prototype.getDecimals || (String.prototype.getDecimals = function() {
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 @@ jQuery(document).on('change', '.shopify-product-form .single-option-selector, .s
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/custom.js.liquid.SOURCE b/shopify/theme-fixes/custom-js-qty-2026-06-30/custom.js.liquid.SOURCE
new file mode 100644
index 00000000..64d9005e
--- /dev/null
+++ b/shopify/theme-fixes/custom-js-qty-2026-06-30/custom.js.liquid.SOURCE
@@ -0,0 +1,511 @@
+/*========== 020 Custom js for quantity ===================*/
+
+function mm_refresh_quantity_increments() {
+ jQuery("div.mm_quantity:not(.mm_buttons_added), td.mm_quantity:not(.mm_buttons_added)").each(function(a, b) {
+ var c = jQuery(b);
+ c.addClass("mm_buttons_added"), c.children().first().before('<input type="button" value="-" class="minus" />'), c.children().last().after('<input type="button" value="+" class="plus" />')
+ })
+}
+String.prototype.getDecimals || (String.prototype.getDecimals = function() {
+ var a = this,
+ b = ("" + a).match(/(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/);
+ return b ? Math.max(0, (b[1] ? b[1].length : 0) - (b[2] ? +b[2] : 0)) : 0
+}), jQuery(document).ready(function() {
+ mm_refresh_quantity_increments()
+
+ // Custom popup contact form (moved from snippets/product.liquid)
+ if (jQuery('.custom-popup-contact-form').length > 0) {
+ jQuery('.custom-popup-contact-form').on('click', function (e) {
+ e.preventDefault();
+ jQuery('#custom-popup-overlay').show();
+ });
+ }
+
+ if (jQuery('#custom-popup-overlay .custom-close').length > 0) {
+ jQuery('#custom-popup-overlay .custom-close').on('click', function () {
+ jQuery('#custom-popup-overlay').hide();
+ });
+ }
+
+ if (jQuery('#custom-popup-overlay .success-message').length > 0) {
+ jQuery('.custom-contact-message').html('Thank you. We will respond to your message shortly.');
+ }
+
+ if (jQuery('#custom-popup-overlay .error-message').length > 0) {
+ jQuery('.custom-contact-message').html(jQuery('#custom-popup-overlay .error-message').html());
+ }
+}), jQuery(document).on("updated_wc_div", function() {
+ 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"
+ }
+
+
+});
+
+async function updateItem(variantId, quantity, properties={}){
+ const result = await fetch("/cart/change.json", {
+ method:"post",
+ headers: {
+ "Content-type": "application/json",
+ "Accept": "application/json"
+ },
+ body: JSON.stringify({
+ id: variantId,
+ quantity: quantity,
+ properties: properties
+ })
+ })
+ return result.json();
+}
+
+function mm_validate(evt) {
+ var theEvent = evt || window.event;
+ theEvent.preventDefault()
+ /*
+ if (theEvent.type === 'paste') {
+ key = event.clipboardData.getData('text/plain');
+ } else {
+ var key = theEvent.keyCode || theEvent.which;
+ key = String.fromCharCode(key);
+ }
+ var regex = /[0-9]|\./;
+
+ if( !regex.test(key) ) {
+ theEvent.returnValue = false;
+ if(theEvent.preventDefault) theEvent.preventDefault();
+ }*/
+}
+
+
+/* for variant sample */
+jQuery(document).on('change', '.shopify-product-form .single-option-selector, .shopify-product-form .product-select', function () {
+ var $form = jQuery(this).closest('.shopify-product-form');
+
+ var isOptionSelector = jQuery(this).hasClass('single-option-selector');
+
+ var run = function () {
+ var $variantSelect = $form.find('.product-select');
+ var $selected;
+ var selectedSku = '';
+ var optionText = '';
+
+ if ($variantSelect.length && $variantSelect.is('select')) {
+ $selected = $variantSelect.find(':selected');
+ selectedSku = ($selected.attr('data-sku') || '').toString();
+ optionText = ($selected.text() || '').toString();
+ } else if ($variantSelect.length && $variantSelect.is('input')) {
+ // 1-variant products: .product-select is a hidden input
+ selectedSku = ($variantSelect.attr('data-sku') || '').toString();
+ optionText = ($variantSelect.attr('data-variant-title') || '').toString();
+ } else {
+ $selected = jQuery(this).find(':selected');
+ selectedSku = ($selected.attr('data-sku') || '').toString();
+ optionText = ($selected.text() || '').toString();
+ }
+
+ // If we can't determine anything, don't flip the UI.
+ if (!selectedSku && !optionText) return;
+
+ var isSkuSample = /-+sample$/i.test(selectedSku);
+ var isTextSample = /\bsample\b/i.test(optionText);
+ var isPureSample = /^sample(\b|[^a-z])/i.test(optionText.trim());
+ var isSample = isSkuSample || isTextSample;
+
+ 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);
+ }
+
+ // Sample request buttons are always available (hide/show is variant-driven only)
+ $form.find('.dl-sample-btn, .dl-second-sample-btn').show();
+
+ if (isSample) {
+ $qty.val(1);
+ $qty.attr({ step: 1, min: 1, max: 1 });
+ $qtyWrapper.find('.plus, .minus').prop('disabled', true);
+ $qtyWrapper.find('.hquantity').attr('value', 1);
+
+ $form.find('.product-option-quantity-label').addClass('hidden').hide();
+ $qtyWrapper.addClass('hidden').hide();
+ $form.find('.add-to-cart').addClass('hidden').hide();
+
+ if (!isPureSample) {
+ $form.find('.dl-second-sample-btn').removeClass('unavailable');
+ } else if (!$form.find('.dl-second-sample-btn').hasClass('unavailable')) {
+ $form.find('.dl-second-sample-btn').addClass('unavailable');
+ }
+ } else {
+ $form.find('.product-option-quantity-label').removeClass('hidden').show();
+ $qtyWrapper.removeClass('hidden').show();
+ $form.find('.add-to-cart').removeClass('hidden').show();
+
+ var minValue = originalMin;
+ var currentValue = parseFloat($qty.val()) || 0;
+ if (currentValue < minValue) {
+ $qty.val(minValue);
+ $qtyWrapper.find('.hquantity').attr('value', minValue);
+ }
+ $qty.attr({ step: originalStep, min: minValue });
+ $qty.removeAttr('max');
+ $qtyWrapper.find('.plus, .minus').prop('disabled', false);
+ }
+
+ // Update the SKU display
+ var displaySku = selectedSku
+ .replace(/-+sample$/i, '')
+ .replace(/-+$/g, '');
+ jQuery('.product-sku-value').text(displaySku);
+
+ // Trade discount text
+ if (jQuery('#customer_tag').val() === 'trade') {
+ requestAnimationFrame(function () {
+ requestAnimationFrame(function () {
+ if (isSample) {
+ jQuery('.product-price-discount').text('Complimentary Sample');
+ } else {
+ // Use the currently displayed currency (symbol/code) from the UI, not hardcoded USD.
+ // Also support both "298,95" and "298.95" style decimals.
+ function dlParseDisplayedNumber(numStr) {
+ var s = (numStr || '').toString().replace(/\s/g, '');
+ var lastComma = s.lastIndexOf(',');
+ var lastDot = s.lastIndexOf('.');
+
+ // No separators: plain number
+ if (lastComma === -1 && lastDot === -1) {
+ var plain = parseFloat(s.replace(/[^\d.-]/g, ''));
+ return isFinite(plain) ? plain : NaN;
+ }
+
+ // Decide decimal separator by whichever comes last
+ var decimalSep = lastComma > lastDot ? ',' : '.';
+ var otherSep = decimalSep === ',' ? '.' : ',';
+
+ var parts = s.split(decimalSep);
+ if (!parts.length) return NaN;
+ var intPart = (parts[0] || '').replace(new RegExp('[\\' + otherSep + ']', 'g'), '').replace(/[^\d-]/g, '');
+ var fracPart = (parts[1] || '').replace(/[^\d]/g, '');
+ var normalized = intPart + (fracPart ? ('.' + fracPart) : '');
+ var val = parseFloat(normalized);
+ return isFinite(val) ? val : NaN;
+ }
+
+ function dlFormatLike(numStr, value) {
+ var s = (numStr || '').toString().replace(/\s/g, '');
+ var lastComma = s.lastIndexOf(',');
+ var lastDot = s.lastIndexOf('.');
+ var decimalSep = lastComma === -1 && lastDot === -1 ? '.' : (lastComma > lastDot ? ',' : '.');
+ var thousandsSep = decimalSep === ',' ? '.' : ',';
+
+ var hasThousands = false;
+ if (lastComma !== -1 || lastDot !== -1) {
+ var decIndex = decimalSep === ',' ? lastComma : lastDot;
+ var intStr = s.slice(0, decIndex);
+ hasThousands = intStr.indexOf(thousandsSep) !== -1;
+ }
+
+ var fixed = (Math.round(value * 100) / 100).toFixed(2);
+ var fixedParts = fixed.split('.');
+ var intPart = fixedParts[0];
+ var fracPart = fixedParts[1] || '00';
+
+ if (hasThousands) {
+ intPart = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, thousandsSep);
+ }
+
+ return intPart + decimalSep + fracPart;
+ }
+
+ var priceText = (jQuery('.product-price-minimum').first().text() || '').trim();
+ var re = /[\d.,]+/;
+ var m = re.exec(priceText);
+ if (!m) return;
+
+ var numberStr = m[0];
+ var prefix = priceText.slice(0, m.index);
+ var suffix = priceText.slice(m.index + numberStr.length);
+
+ var priceValue = dlParseDisplayedNumber(numberStr);
+ if (!isFinite(priceValue)) return;
+
+ var discountedValue = priceValue * 0.85;
+ var formattedNumber = dlFormatLike(numberStr, discountedValue);
+ jQuery('.product-price-discount').text(prefix + formattedNumber + suffix);
+ }
+ });
+ });
+ }
+ }.bind(this);
+
+ var schedule = function () {
+ requestAnimationFrame(function () {
+ requestAnimationFrame(run);
+ });
+ };
+
+ if (isOptionSelector) {
+ setTimeout(schedule, 60);
+ } else {
+ schedule();
+ }
+});
+
+
+//hide ddl and show last variant
+
+if($('#has_display_variant').length>0 && $('#has_display_variant').val() == "no"){
+ $('.last_variant').hide();
+ if ( $( ".shopify-product-form .single-option-selector" ).length ) {
+
+ $('.shopify-product-form .single-option-selector').hide();
+
+ setTimeout(function(){
+ $('.product-options .select-wrapper').hide();
+ }, 500);
+
+ $('.last_variant').show();
+
+ var single_opt_ind=0;
+ $('.shopify-product-form .single-option-selector option').each(function(index, option){
+ single_opt_ind=index;
+ });
+
+
+ /* DW deep-link honor (vp-dw-commerce 2026-06-24) */
+ // Honor an explicit ?variant=<id> deep-link: if the URL names a variant that
+ // matches one of the options, select THAT option instead of the computed default,
+ // so a link to the Sample renders $4.25 and a link to the Roll renders the roll price.
+ try {
+ var dlWanted = new URLSearchParams(window.location.search).get('variant');
+ if (dlWanted) {
+ $dlOpts.each(function(index, option){
+ var ov = (option.value || '').toString();
+ var dv = (option.getAttribute && option.getAttribute('data-variant-id') || '').toString();
+ if (ov === dlWanted || dv === dlWanted) { single_opt_ind = index; }
+ });
+ }
+ } catch (e) {}
+ /* /DW deep-link honor */
+ $(".shopify-product-form .single-option-selector").prop('selectedIndex', single_opt_ind).trigger('change');
+ }
+}
+//--------------------------------
+
+// Prevent rapid multi-clicks from adding multiple samples before mini-cart UI updates
+function dlGetSampleBtns(variantId) {
+ return $(".dl-sample-btn[data-dlvid='" + variantId + "'], .dl-second-sample-btn[data-dlvid='" + variantId + "']");
+}
+function dlIsSampleLocked(variantId) {
+ return dlGetSampleBtns(variantId).first().attr("data-dl-locked") === "1";
+}
+function dlLockSampleBtns(variantId) {
+ var $btns = dlGetSampleBtns(variantId);
+ $btns.attr("data-dl-locked", "1").addClass("dl-click-locked").attr("aria-disabled", "true");
+ $btns.each(function() {
+ var $b = $(this);
+ if ($b.is("button, input")) $b.prop("disabled", true);
+ });
+}
+function dlUnlockSampleBtns(variantId) {
+ var $btns = dlGetSampleBtns(variantId);
+ $btns.removeAttr("data-dl-locked").removeClass("dl-click-locked").removeAttr("aria-disabled");
+ $btns.each(function() {
+ var $b = $(this);
+ if ($b.is("button, input")) $b.prop("disabled", false);
+ });
+}
+function dlUnlockSampleBtnsWhenInCart(variantId, maxMs) {
+ var start = Date.now();
+ var timer = setInterval(function() {
+ if ($(".mini-cart-item-wrapper [data-variant='" + variantId + "']").length > 0) {
+ clearInterval(timer);
+ dlUnlockSampleBtns(variantId);
+ return;
+ }
+ if (Date.now() - start >= (maxMs || 20000)) {
+ clearInterval(timer);
+ dlUnlockSampleBtns(variantId);
+ }
+ }, 250);
+}
+
+
+$(document).on("click", ".dl-sample-btn", function(e) {
+ e.preventDefault()
+ var $btn = $(this);
+ var $form = $btn.closest("form");
+ var id = $btn.attr("data-dlvid");
+ //check if the sample was added already
+ if($(".mini-cart-item-wrapper [data-variant='"+id+"']").length == 0) {
+ if (dlIsSampleLocked(id)) return;
+ dlLockSampleBtns(id);
+ dlUnlockSampleBtnsWhenInCart(id, 20000);
+
+ var product_id = $(this).attr("data-product-id");
+ var $productSelect = $form.find("#product-select-"+product_id);
+ if (!$productSelect.length) $productSelect = $form.find(".product-select");
+ var first_selected_id = $productSelect.is("select") ? $productSelect.find(":selected").val() : $productSelect.val();
+ $productSelect.val(id).trigger('change');
+ //
+ var $qtyInput = $form.find(".input-text.qty");
+ var default_quantity = $qtyInput.val();
+ $qtyInput.val(1);
+ $form.find(".add-to-cart.cowlendar-add-to-cart").trigger("click");
+
+ //go back to original selected status
+
+ $qtyInput.val(default_quantity);
+ if (first_selected_id && first_selected_id !== id) {
+ $productSelect.val(first_selected_id).trigger('change');
+ }
+ } else {
+ //alert("1 sample only");
+ if($(".product-message").hasClass("success-message") || $(".mini-cart-item-wrapper [data-variant='"+id+"']").length > 0) {
+ $(".product-message").removeClass("success-message").addClass("error-message");
+ $(".product-message").text("1 Sample per SKU");
+ }
+ }
+});
+$(document).on("click", ".dl-second-sample-btn", function(e) {
+ e.preventDefault()
+ var $btn = $(this);
+ var $form = $btn.closest("form");
+ var id = $btn.attr("data-dlvid");
+ //check if the sample was added already
+ if($(".mini-cart-item-wrapper [data-variant='"+id+"']").length == 0) {
+ if (dlIsSampleLocked(id)) return;
+ dlLockSampleBtns(id);
+ dlUnlockSampleBtnsWhenInCart(id, 20000);
+
+ //
+ var $qtyInput = $form.find(".input-text.qty");
+ var default_quantity = $qtyInput.val();
+ $qtyInput.val(1);
+ $form.find(".add-to-cart.cowlendar-add-to-cart").trigger("click");
+
+ //go back to original selected status
+
+ $qtyInput.val(default_quantity);
+ } else {
+ //alert("1 sample only");
+ if($(".product-message").hasClass("success-message") || $(".mini-cart-item-wrapper [data-variant='"+id+"']").length > 0) {
+ $(".product-message").removeClass("success-message").addClass("error-message");
+ $(".product-message").text("1 Sample per SKU");
+ }
+ }
+});
+
+//add the remove functionality on the mini-cart
+$(document).on("click", ".remove-item", function(e) {
+ e.preventDefault();
+ var variantId = $(this).closest('.mini-cart-item').attr("data-variant");
+ var quantity = parseInt($(this).closest(".mini-cart-item").find(".mini-cart-item-quantity").find("span").text());
+ var cart_num = parseInt($(".cart-count .cart-count-number").text());
+ var reminder_num = cart_num - quantity;
+ $.ajax({
+ type: "POST",
+ url: "/cart/change.js",
+ dataType: "json",
+ data: {
+ id: parseFloat(variantId),
+ quantity: 0,
+ },
+ }).then((data) => {
+ console.log("success");
+ $(this).closest('.mini-cart-item').remove();
+ dlUnlockSampleBtns(variantId);
+ $(".cart-count .cart-count-number").text(parseInt(reminder_num));
+ if(reminder_num == 0) {
+ $(".mini-cart").hide();
+ }
+ });
+});
+
+$(document).on("click", ".upsell-add-to-cart", function() {
+ var id = $(this).attr("data-item-id");
+ var num = parseInt($(this).attr("data-num"));
+ $.ajax({
+ type: 'POST',
+ url: '/cart/add.js',
+ dataType: 'json',
+ data: {
+ items: [
+ {
+ quantity: num,
+ id: id
+ }
+ ]
+ },
+ success: function (data){
+ location.href= "/cart";
+ }
+ });
+});
+
+/* Custom code for sascha */
+function showMiniCartIfReady() {
+ if (window.innerWidth <= 767) {
+ return;
+ }
+ var $miniCart = $(".mini-cart");
+ if ($miniCart.length && !$miniCart.hasClass("empty")) {
+ $(".mini-cart-wrapper").addClass("is-open");
+ }
+}
+
+function watchMiniCartUpdates() {
+ var target = document.querySelector(".mini-cart-item-wrapper");
+ if (!target || !window.MutationObserver) {
+ return;
+ }
+ var observer = new MutationObserver(function() {
+ showMiniCartIfReady();
+ });
+ observer.observe(target, { childList: true, subtree: true });
+}
+
+$(document).ready(function() {
+ watchMiniCartUpdates();
+});
+
+$(document).on("click", ".add-to-cart.cowlendar-add-to-cart", function() {
+ showMiniCartIfReady();
+});
+
+$(document).on("click", function(event) {
+ var $target = $(event.target);
+ if ($target.closest(".mini-cart, .mini-cart-wrapper").length === 0) {
+ $(".mini-cart-wrapper").removeClass("is-open");
+ }
+});
+
+$(document).on("mouseleave", ".mini-cart-wrapper", function() {
+ $(this).removeClass("is-open");
+});
\ No newline at end of file
diff --git a/shopify/theme-fixes/custom-js-qty-2026-06-30/firstclick.cjs b/shopify/theme-fixes/custom-js-qty-2026-06-30/firstclick.cjs
new file mode 100644
index 00000000..7fd6e1d8
--- /dev/null
+++ b/shopify/theme-fixes/custom-js-qty-2026-06-30/firstclick.cjs
@@ -0,0 +1,30 @@
+const { chromium } = require(require('path').join(process.env.HOME, 'Projects/Designer-Wallcoverings/node_modules/playwright'));
+const fs=require('fs');
+const DEV_JS=fs.readFileSync(__dirname+'/DEV_custom.js','utf8');
+const URL='https://www.designerwallcoverings.com/products/dwtt-80064-designer-wallcoverings-los-angeles';
+(async()=>{
+ const b=await chromium.launch();const ctx=await b.newContext();
+ await ctx.route(/\/cdn\/shop\/t\/\d+\/assets\/custom\.js(\?|$)/,r=>{if(/boost-sd/.test(r.request().url()))return r.continue();r.fulfill({status:200,contentType:'application/javascript',body:DEV_JS});});
+ const p=await ctx.newPage();
+ await p.goto(URL,{waitUntil:'domcontentloaded',timeout:60000});
+ await p.waitForSelector('.mm_quantity input[name="quantity"]',{state:'attached',timeout:30000});
+ await p.evaluate(()=>{document.querySelectorAll('.mm_quantity .qty').forEach(q=>{q.setAttribute('data-order-step','2');q.setAttribute('data-order-min','2');});});
+ await p.waitForTimeout(1800);
+ await p.evaluate(()=>{const s=document.querySelector('.product-select');if(s&&s.tagName==='SELECT'){const o=Array.from(s.options).find(x=>!/sample/i.test(x.textContent)&&!/-sample$/i.test(x.getAttribute('data-sku')||''))||s.options[0];s.value=o.value;s.dispatchEvent(new Event('change',{bubbles:true}));if(window.jQuery)window.jQuery(s).trigger('change');}});
+ await p.waitForTimeout(1800);
+ const info=await p.evaluate(()=>{
+ // count delegated click handlers on document for .plus by probing jQuery internals
+ const $=window.jQuery; const ev=$._data(document,'events')||{};
+ const clickHandlers=(ev.click||[]).map(h=>h.selector);
+ const q=document.querySelector('.mm_quantity .qty');
+ return {clickSelectors:clickHandlers, qtyBefore:q.value};
+ });
+ console.log('delegated click selectors on document:',JSON.stringify(info.clickSelectors));
+ console.log('qty before first click:',info.qtyBefore);
+ // click plus once, read
+ await p.click('.mm_quantity .plus'); await p.waitForTimeout(200);
+ console.log('qty after 1st +:', await p.evaluate(()=>document.querySelector('.mm_quantity .qty').value));
+ await p.click('.mm_quantity .plus'); await p.waitForTimeout(200);
+ console.log('qty after 2nd +:', await p.evaluate(()=>document.querySelector('.mm_quantity .qty').value));
+ await b.close();
+})().catch(e=>{console.error(e);process.exit(1)});
diff --git a/shopify/theme-fixes/custom-js-qty-2026-06-30/harness-run.cjs b/shopify/theme-fixes/custom-js-qty-2026-06-30/harness-run.cjs
new file mode 100644
index 00000000..340047fa
--- /dev/null
+++ b/shopify/theme-fixes/custom-js-qty-2026-06-30/harness-run.cjs
@@ -0,0 +1,28 @@
+const { chromium } = require(require('path').join(process.env.HOME, 'Projects/Designer-Wallcoverings/node_modules/playwright'));
+const path=require('path');
+const FILE='file://'+path.join(__dirname,'harness.html');
+(async()=>{
+ const b=await chromium.launch();const p=await(await b.newContext()).newPage();
+ const errs=[];p.on('pageerror',e=>errs.push('PAGEERROR: '+e.message));
+ p.on('console',m=>{if(m.type()==='error')errs.push('CONSOLE.error: '+m.text());});
+ await p.goto(FILE,{waitUntil:'load'});
+ await p.waitForTimeout(400);
+ const rd=async(scope)=>p.evaluate(s=>{const w=document.querySelector(s+' .mm_quantity');const q=w.querySelector('.qty');const h=w.querySelector('.hquantity');return {qty:q.value,hq:h.getAttribute('value')};},scope);
+ const clk=async(scope,dir)=>{await p.click(scope+' .mm_quantity .'+dir);await p.waitForTimeout(60);};
+ const out={};
+ for(const [scope,label] of [['#moq','MOQ step2/min2'],['#std','non-MOQ step1/min1']]){
+ const seq=[['initial',await rd(scope)]];
+ await clk(scope,'plus'); seq.push(['+',await rd(scope)]);
+ await clk(scope,'plus'); seq.push(['+',await rd(scope)]);
+ await clk(scope,'minus'); seq.push(['-',await rd(scope)]);
+ await clk(scope,'minus'); seq.push(['- (floor)',await rd(scope)]);
+ await clk(scope,'minus'); seq.push(['- (floor)',await rd(scope)]);
+ // typed invalid -> snap
+ await p.evaluate(s=>{const q=document.querySelector(s+' .mm_quantity .qty');q.value='3';window.jQuery(q).trigger('change');},scope);
+ await p.waitForTimeout(60); seq.push(['typed 3 -> snap',await rd(scope)]);
+ out[label]=seq;
+ }
+ out.errors=errs;
+ console.log(JSON.stringify(out,null,2));
+ await b.close();
+})().catch(e=>{console.error('FATAL',e);process.exit(1);});
diff --git a/shopify/theme-fixes/custom-js-qty-2026-06-30/harness.html b/shopify/theme-fixes/custom-js-qty-2026-06-30/harness.html
new file mode 100644
index 00000000..1bf1ddc3
--- /dev/null
+++ b/shopify/theme-fixes/custom-js-qty-2026-06-30/harness.html
@@ -0,0 +1,52 @@
+<!doctype html><html><head><meta charset="utf-8"><title>qty harness</title>
+<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
+</head><body>
+<form action="/cart/add">
+ <h3>MOQ (step2/min2)</h3>
+ <div id="moq"><div class="mm_quantity mm_buttons_added product-option-row-4 ">
+ <input type="hidden" class="hquantity" name="hquantity" value="2">
+ <input type="button" value="-" class="minus">
+ <input
+ type="text"
+ step="2"
+ min="2"
+ data-order-step="2"
+ data-order-min="2"
+ max=""
+ name="quantity"
+ value="2"
+ title="Qty"
+ class="input-text qty text"
+ size="4"
+ pattern=""
+ inputmode=""
+ onkeypress="mm_validate(event)"
+ >
+ <input type="button" value="+" class="plus">
+ </div></div>
+ <h3>non-MOQ (step1/min1)</h3>
+ <div id="std"><div class="mm_quantity mm_buttons_added product-option-row-4 ">
+ <input type="hidden" class="hquantity" name="hquantity" value="1">
+ <input type="button" value="-" class="minus">
+ <input
+ type="text"
+ step="1"
+ min="1"
+ data-order-step="1"
+ data-order-min="1"
+ max=""
+ name="quantity"
+ value="1"
+ title="Qty"
+ class="input-text qty text"
+ size="4"
+ pattern=""
+ inputmode=""
+ onkeypress="mm_validate(event)"
+ >
+ <input type="button" value="+" class="plus">
+ </div></div>
+</form>
+<script>window.$ = window.jQuery;</script>
+<script src="./DEV_custom.js"></script>
+</body></html>
diff --git a/shopify/theme-fixes/custom-js-qty-2026-06-30/hostcheck.cjs b/shopify/theme-fixes/custom-js-qty-2026-06-30/hostcheck.cjs
new file mode 100644
index 00000000..9fc0005e
--- /dev/null
+++ b/shopify/theme-fixes/custom-js-qty-2026-06-30/hostcheck.cjs
@@ -0,0 +1,14 @@
+const { chromium } = require(require('path').join(process.env.HOME, 'Projects/Designer-Wallcoverings/node_modules/playwright'));
+const THEME=process.argv[2];
+(async()=>{
+ const b=await chromium.launch();const ctx=await b.newContext();const p=await ctx.newPage();
+ // Block redirect to primary domain by rewriting requests? Instead, watch where we land.
+ const URL=`https://designer-laboratory-sandbox.myshopify.com/products/dwtt-80064-designer-wallcoverings-los-angeles?preview_theme_id=${THEME}&_fd=0&pb=0`;
+ const resp=await p.goto(URL,{waitUntil:'domcontentloaded',timeout:60000});
+ console.log('landed URL:',p.url());
+ let custom=null;
+ p.on('response',async r=>{const u=r.url();if(/\/custom\.js\?/.test(u)&&!/boost-sd/.test(u)){custom=u;}});
+ await p.waitForTimeout(3000);
+ console.log('custom.js served from:',custom);
+ await b.close();
+})().catch(e=>{console.error(e);process.exit(1)});
diff --git a/shopify/theme-fixes/custom-js-qty-2026-06-30/hostcheck2.cjs b/shopify/theme-fixes/custom-js-qty-2026-06-30/hostcheck2.cjs
new file mode 100644
index 00000000..8c769b6c
--- /dev/null
+++ b/shopify/theme-fixes/custom-js-qty-2026-06-30/hostcheck2.cjs
@@ -0,0 +1,15 @@
+const { chromium } = require(require('path').join(process.env.HOME, 'Projects/Designer-Wallcoverings/node_modules/playwright'));
+const THEME=process.argv[2];
+(async()=>{
+ const b=await chromium.launch();const p=await(await b.newContext()).newPage();
+ let custom=null,body=null;
+ p.on('response',async r=>{const u=r.url();if(/\/custom\.js\?/.test(u)&&!/boost-sd/.test(u)){custom=u;try{body=await r.text();}catch(e){}}});
+ const URL=`https://designer-laboratory-sandbox.myshopify.com/products/dwtt-80064-designer-wallcoverings-los-angeles?preview_theme_id=${THEME}&_fd=0&pb=0`;
+ await p.goto(URL,{waitUntil:'domcontentloaded',timeout:60000});
+ console.log('landed:',p.url());
+ await p.waitForSelector('.mm_quantity input[name="quantity"]',{state:'attached',timeout:30000}).catch(()=>{});
+ await p.waitForTimeout(2500);
+ console.log('custom.js:',custom);
+ if(body){console.log('has repair marker:',body.includes('qty-stepper repair'),' len:',body.length);}
+ await b.close();
+})().catch(e=>{console.error(e);process.exit(1)});
diff --git a/shopify/theme-fixes/custom-js-qty-2026-06-30/iso.cjs b/shopify/theme-fixes/custom-js-qty-2026-06-30/iso.cjs
new file mode 100644
index 00000000..f90cdb24
--- /dev/null
+++ b/shopify/theme-fixes/custom-js-qty-2026-06-30/iso.cjs
@@ -0,0 +1,43 @@
+// Isolated logic test: no live page, just jsdom-free pure-DOM simulation of the handler math.
+// Proves the increment/decrement/clamp/snap math in a controlled environment.
+String.prototype.getDecimals = function(){var m=(''+this).match(/(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/);return m?Math.max(0,(m[1]?m[1].length:0)-(m[2]?+m[2]:0)):0;};
+function step(current, dir, orderStep, orderMin, maxRaw){
+ var stp=parseFloat(orderStep); if(!isFinite(stp)||stp<=0) stp=1;
+ var mn=parseFloat(orderMin); if(!isFinite(mn)||mn<=0) mn=stp;
+ var mx=parseFloat(maxRaw); var hasMax=(maxRaw!=null&&maxRaw!==''&&isFinite(mx));
+ var decimals=String(stp).getDecimals();
+ var cur=parseFloat(current); if(!isFinite(cur)) cur=mn;
+ var next;
+ if(dir>0){ next=cur+stp; if(hasMax&&next>mx)next=mx; }
+ else { next=cur-stp; if(next<mn)next=mn; }
+ var g=Math.round((next-mn)/stp); if(g<0)g=0; next=mn+g*stp; if(hasMax&&next>mx)next=mx;
+ return next.toFixed(decimals);
+}
+function snap(v, orderStep, orderMin){
+ var stp=parseFloat(orderStep); if(!isFinite(stp)||stp<=0) stp=1;
+ var mn=parseFloat(orderMin); if(!isFinite(mn)||mn<=0) mn=stp;
+ var val=parseFloat(v); if(!isFinite(val)||val<mn) val=mn;
+ var g=Math.round((val-mn)/stp); if(g<0)g=0; return (mn+g*stp).toFixed(String(stp).getDecimals());
+}
+// MOQ step=2 min=2
+let v="2"; const log=[];
+log.push(['initial',v]);
+v=step(v,+1,"2","2",""); log.push(['+',v]);
+v=step(v,+1,"2","2",""); log.push(['+',v]);
+v=step(v,-1,"2","2",""); log.push(['-',v]);
+v=step(v,-1,"2","2",""); log.push(['- (floor)',v]);
+v=step(v,-1,"2","2",""); log.push(['- (floor)',v]);
+log.push(['type 3 -> snap', snap("3","2","2")]);
+log.push(['type 5 -> snap', snap("5","2","2")]);
+console.log('=== MOQ step2/min2 ===');
+log.forEach(l=>console.log(' '+l[0].padEnd(16),'=',l[1]));
+// non-MOQ step=1 min=1
+let w="1"; const log2=[];
+log2.push(['initial',w]);
+w=step(w,+1,"1","1",""); log2.push(['+',w]);
+w=step(w,+1,"1","1",""); log2.push(['+',w]);
+w=step(w,-1,"1","1",""); log2.push(['-',w]);
+w=step(w,-1,"1","1",""); log2.push(['- (floor)',w]);
+w=step(w,-1,"1","1",""); log2.push(['- (floor)',w]);
+console.log('=== non-MOQ step1/min1 ===');
+log2.forEach(l=>console.log(' '+l[0].padEnd(16),'=',l[1]));
diff --git a/shopify/theme-fixes/custom-js-qty-2026-06-30/liquid/snippets__product-form-content.liquid b/shopify/theme-fixes/custom-js-qty-2026-06-30/liquid/snippets__product-form-content.liquid
index 598116d0..f176dda3 100644
--- a/shopify/theme-fixes/custom-js-qty-2026-06-30/liquid/snippets__product-form-content.liquid
+++ b/shopify/theme-fixes/custom-js-qty-2026-06-30/liquid/snippets__product-form-content.liquid
@@ -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/previewcookie.cjs b/shopify/theme-fixes/custom-js-qty-2026-06-30/previewcookie.cjs
new file mode 100644
index 00000000..adff366b
--- /dev/null
+++ b/shopify/theme-fixes/custom-js-qty-2026-06-30/previewcookie.cjs
@@ -0,0 +1,19 @@
+const { chromium } = require(require('path').join(process.env.HOME, 'Projects/Designer-Wallcoverings/node_modules/playwright'));
+const THEME=process.argv[2];
+(async()=>{
+ const b=await chromium.launch();const ctx=await b.newContext();const p=await ctx.newPage();
+ // Step 1: set preview cookie on myshopify host
+ await p.goto(`https://designer-laboratory-sandbox.myshopify.com/?preview_theme_id=${THEME}&_fd=0&pb=0`,{waitUntil:'domcontentloaded',timeout:60000});
+ await p.waitForTimeout(1000);
+ const cookies=await ctx.cookies();
+ console.log('cookies after step1:',cookies.filter(c=>/preview|theme|_secure_session|_shopify/i.test(c.name)).map(c=>c.name).join(', '));
+ // Step 2: navigate to product (cookie should apply)
+ let custom=null,body=null;
+ p.on('response',async r=>{const u=r.url();if(/\/custom\.js\?/.test(u)&&!/boost-sd/.test(u)){custom=u;try{body=await r.text();}catch(e){}}});
+ await p.goto(`https://designer-laboratory-sandbox.myshopify.com/products/dwtt-80064-designer-wallcoverings-los-angeles?_fd=0&pb=0`,{waitUntil:'domcontentloaded',timeout:60000});
+ await p.waitForSelector('.mm_quantity input[name="quantity"]',{state:'attached',timeout:30000}).catch(()=>{});
+ await p.waitForTimeout(2500);
+ console.log('custom.js:',custom);
+ if(body)console.log('has repair marker:',body.includes('qty-stepper repair'),' len:',body.length);
+ await b.close();
+})().catch(e=>{console.error(e);process.exit(1)});
diff --git a/shopify/theme-fixes/custom-js-qty-2026-06-30/primarypreview.cjs b/shopify/theme-fixes/custom-js-qty-2026-06-30/primarypreview.cjs
new file mode 100644
index 00000000..41dd58cd
--- /dev/null
+++ b/shopify/theme-fixes/custom-js-qty-2026-06-30/primarypreview.cjs
@@ -0,0 +1,15 @@
+const { chromium } = require(require('path').join(process.env.HOME, 'Projects/Designer-Wallcoverings/node_modules/playwright'));
+const THEME=process.argv[2];
+(async()=>{
+ const b=await chromium.launch();const ctx=await b.newContext();const p=await ctx.newPage();
+ let paths=new Set();
+ p.on('response',r=>{const u=r.url();const m=u.match(/\/cdn\/shop\/t\/(\d+)\/assets\/custom\.js/);if(m)paths.add(m[1]);});
+ // Go direct to primary domain WITH preview param, allow redirects
+ await p.goto(`https://www.designerwallcoverings.com/products/dwtt-80064-designer-wallcoverings-los-angeles?preview_theme_id=${THEME}`,{waitUntil:'domcontentloaded',timeout:60000});
+ console.log('landed:',p.url());
+ const ck=(await ctx.cookies()).find(c=>/preview_theme/i.test(c.name));
+ console.log('preview cookie:',ck?ck.value:'NONE');
+ await p.waitForTimeout(2500);
+ console.log('custom.js theme paths seen:',[...paths].join(','));
+ await b.close();
+})().catch(e=>{console.error(e);process.exit(1)});
diff --git a/shopify/theme-fixes/custom-js-qty-2026-06-30/product-form-content.ORIGINAL.liquid b/shopify/theme-fixes/custom-js-qty-2026-06-30/product-form-content.ORIGINAL.liquid
new file mode 100644
index 00000000..598116d0
--- /dev/null
+++ b/shopify/theme-fixes/custom-js-qty-2026-06-30/product-form-content.ORIGINAL.liquid
@@ -0,0 +1,800 @@
+{% comment %}
+ @param product {Product}
+
+ @param show_social_media_icons {Boolean}
+
+ @param show_payment_button {Boolean}
+ Show the DPBs in the product form
+{% endcomment %}
+
+{% assign product_id = product.id %}
+{% assign selected_variant = product.selected_or_first_available_variant %}
+
+{%- liquid
+ assign _selected_variant_sku = selected_variant.sku | default: '' | downcase
+ assign _selected_variant_title = selected_variant.title | default: '' | downcase
+
+ assign _selected_variant_sku_suffix = _selected_variant_sku | slice: -7, 7
+
+ assign is_sample_sku = false
+ if _selected_variant_sku != blank and _selected_variant_sku_suffix == '-sample'
+ assign is_sample_sku = true
+ endif
+
+ assign is_sample_title = false
+ if _selected_variant_title contains 'sample'
+ assign is_sample_title = true
+ endif
+
+ assign is_sample_like = false
+ if is_sample_sku
+ assign is_sample_like = true
+ elsif is_sample_title and selected_variant.price == 0
+ assign is_sample_like = true
+ endif
+-%}
+
+<!-- -----------020------------ -->
+{% assign prods_quantity_order_min = 1 %}
+
+{% if product.metafields['global']['v_prods_quantity_order_min'] %}
+ {% if product.metafields['global']['v_prods_quantity_order_min'] != '' %}
+ {% assign prods_quantity_order_min = product.metafields['global']['v_prods_quantity_order_min'] %}
+ {% endif %}
+{% endif %}
+
+{% assign prods_quantity_order_units = 1 %}
+
+{% if product.metafields['global']['v_prods_quantity_order_units'] %}
+ {% if product.metafields['global']['v_prods_quantity_order_units'] != '' %}
+ {% assign prods_quantity_order_units = product.metafields['global']['v_prods_quantity_order_units'] %}
+ {% endif %}
+{% endif %}
+
+<!-- Minimum order quantity from custom metafields (e.g. Ralph Lauren fabrics) -->
+{% assign min_order_qty = product.metafields.custom.minimum_order_quantity | default: nil %}
+{% assign min_order_unit = product.metafields.custom.minimum_order_unit | default: '' %}
+{% assign has_min_order = false %}
+{% if min_order_qty != nil and min_order_qty != blank and min_order_qty > 1 %}
+ {% assign has_min_order = true %}
+ {% assign min_order_qty_num = min_order_qty | plus: 0 %}
+ {% if min_order_qty_num > prods_quantity_order_min %}
+ {% assign prods_quantity_order_min = min_order_qty_num %}
+ {% endif %}
+{% endif %}
+
+<!-- -------------------------- -->
+<script>
+ setTimeout(function () {
+ const qtySelector = document.querySelector('.mm_quantity .input-text.qty');
+ if (qtySelector) {
+ qtySelector.value = {{ prods_quantity_order_min }};
+ }
+ }, 1000);
+</script>
+
+{% if has_min_order %}
+<!-- Minimum order notice and validation -->
+<style>
+ .min-order-notice {
+ background: #fff3cd;
+ border: 1px solid #ffc107;
+ border-radius: 4px;
+ padding: 8px 12px;
+ margin: 10px 0;
+ font-size: 14px;
+ font-weight: 600;
+ color: #856404;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ }
+ .min-order-notice svg {
+ flex-shrink: 0;
+ }
+ .min-order-error {
+ background: #f8d7da;
+ border: 1px solid #f5c6cb;
+ color: #721c24;
+ border-radius: 4px;
+ padding: 8px 12px;
+ margin: 8px 0 0;
+ font-size: 13px;
+ display: none;
+ }
+</style>
+<div class="min-order-notice" data-min-order-notice>
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M8 1a7 7 0 100 14A7 7 0 008 1zm0 2.5a.75.75 0 01.75.75v4a.75.75 0 01-1.5 0v-4A.75.75 0 018 3.5zm0 8a.75.75 0 100-1.5.75.75 0 000 1.5z"/></svg>
+ Minimum order: {{ min_order_qty }} {{ min_order_unit }}
+</div>
+<script>
+ (function() {
+ var MIN_QTY = {{ min_order_qty }};
+ var MIN_UNIT = {{ min_order_unit | json }};
+
+ document.addEventListener('DOMContentLoaded', function() {
+ // Find the product form (could be either .shopify-product-form or form with data-product-form)
+ var forms = document.querySelectorAll('form[action*="/cart/add"], form[data-product-form]');
+ forms.forEach(function(form) {
+ form.addEventListener('submit', function(e) {
+ var qtyInput = form.querySelector('.mm_quantity .input-text.qty') || form.querySelector('input[name="quantity"]');
+ if (!qtyInput) return;
+
+ var qty = parseInt(qtyInput.value, 10);
+ if (isNaN(qty) || qty < MIN_QTY) {
+ e.preventDefault();
+ e.stopImmediatePropagation();
+
+ // Show error message
+ var errorDiv = form.querySelector('.min-order-error');
+ if (!errorDiv) {
+ errorDiv = document.createElement('div');
+ errorDiv.className = 'min-order-error';
+ var addToCartBtn = form.querySelector('.product-add-to-cart');
+ if (addToCartBtn) {
+ addToCartBtn.appendChild(errorDiv);
+ }
+ }
+ errorDiv.textContent = 'Please enter a quantity of at least ' + MIN_QTY + ' ' + MIN_UNIT + '.';
+ errorDiv.style.display = 'block';
+
+ // Also fix the value
+ qtyInput.value = MIN_QTY;
+ var hqty = form.querySelector('.hquantity');
+ if (hqty) hqty.value = MIN_QTY;
+
+ // Hide error after 5s
+ setTimeout(function() { errorDiv.style.display = 'none'; }, 5000);
+ return false;
+ }
+ });
+ });
+
+ // Also enforce min on the minus button - prevent going below minimum
+ jQuery(document).on('click', '.minus', function() {
+ var $qty = jQuery(this).closest('.mm_quantity').find('.qty');
+ var currentVal = parseInt($qty.val(), 10);
+ if (currentVal < MIN_QTY) {
+ $qty.val(MIN_QTY);
+ jQuery(this).closest('.mm_quantity').find('.hquantity').attr('value', MIN_QTY);
+ }
+ });
+
+ // Enforce min on blur (manual typing)
+ jQuery(document).on('blur', '.mm_quantity .input-text.qty', function() {
+ var currentVal = parseInt(jQuery(this).val(), 10);
+ if (isNaN(currentVal) || currentVal < MIN_QTY) {
+ jQuery(this).val(MIN_QTY);
+ jQuery(this).closest('.mm_quantity').find('.hquantity').attr('value', MIN_QTY);
+ }
+ });
+
+ // Hide notice when sample variant is selected
+ jQuery(document).on('change', '.product-select, .single-option-selector', function() {
+ var $form = jQuery(this).closest('form, [data-product-form]');
+ var $selected = $form.find('.product-select option:selected, .product-select[type="hidden"]');
+ var selectedText = ($selected.text() || $selected.attr('data-variant-title') || '').toLowerCase();
+ var selectedSku = ($selected.attr('data-sku') || '').toLowerCase();
+ var isSample = /sample/i.test(selectedText) || /-sample$/i.test(selectedSku);
+
+ var $notice = jQuery('.min-order-notice[data-min-order-notice]');
+ if (isSample) {
+ $notice.hide();
+ } else {
+ $notice.show();
+ }
+ });
+ });
+ })();
+</script>
+{% endif %}
+
+{% for block in section.blocks %}
+ {% case block.type %}
+ {% when 'vendor' %}
+ <a class="product-vendor" href="{{ product.vendor | url_for_vendor }}" {{ block.shopify_attributes }}>{{ product.vendor }}</a>
+
+ {% when 'title' %}
+ <h1 class="product-title" {{ block.shopify_attributes }}>
+ {% if product != blank %}
+ {% if link_to %}
+ <a href="{{ product.url }}">
+ {{ product.title }}
+ </a>
+ {% else %}
+ {% comment %}{{ product.title }} {% endcomment %}
+ <a href="https://designer-laboratory-sandbox.myshopify.com/search?q={{ product.title }}">
+ {{ product.title }}
+ </a>
+ {% endif %}
+ {% else %}
+ {{ 'products.product.onboarding.title' | t }}
+ {% endif %}
+ </h1>
+ <span class="product-sku-label">SKU: </span>
+ {%- liquid
+ assign sku_raw = product.selected_or_first_available_variant.sku | default: ''
+ assign sku_display = sku_raw | replace: '-Sample', '' | replace: '-SAMPLE', '' | replace: '-sample', ''
+ assign sku_display = sku_display | replace: '--', '-' | replace: '--', '-'
+ assign sku_last_char = sku_display | slice: -1, 1
+ if sku_display != blank and sku_last_char == '-'
+ assign sku_display = sku_display | remove_last: '-'
+ endif
+ -%}
+ <span class="product-sku-value">{{ sku_display }}</span>
+ {% when '@app' %}
+ <div class="product-app" {{ block.shopify_attributes }}>
+ {% render block %}
+ </div>
+
+ {% when 'custom-liquid' %}
+ {% if block.settings.custom_liquid != blank %}
+ <div class="product__custom-liquid" {{ block.shopify_attributes }}>
+ {{ block.settings.custom_liquid }}
+ </div>
+ {% endif %}
+
+ {% when 'collapsible-tab' %}
+ {% if block.settings.collapsible_tab_heading != blank %}
+ <details class="collapsible-tab" {{ block.shopify_attributes }}>
+ <summary class="collapsible-tab__heading">
+ <span>{{ block.settings.collapsible_tab_heading | escape }}</span>
+ {%
+ render 'icons',
+ id: 'chevron-right',
+ %}
+ </summary>
+
+ {% if block.settings.collapsible_tab_text != blank %}
+ <div class="collapsible-tab__text rte">
+ {{ block.settings.collapsible_tab_text }}
+ </div>
+ {% endif %}
+ </details>
+ {% endif %}
+
+ {% when 'tabs' %}
+ {% assign tab_counter = 1 %}
+ <div class="product-tabs" {{ block.shopify_attributes }}>
+ {% if block.settings.show_product_description %}
+ <input
+ class="product-tabs__radio"
+ id="product-tabs__radio-{{ section.id }}"
+ name="{{ section.id }}"
+ tabindex="0"
+ type="radio"
+ checked="checked"
+ >
+
+ <label class="product-tabs__label" for="product-tabs__radio-{{ section.id }}">
+ {{ 'onboarding.tab_title'| t }}
+ </label>
+
+ <div class="product-tabs__panel rte" tabindex="0">
+ {% if onboarding %}
+ {{ 'onboarding.tab_content'| t }}
+ {% else %}
+ {% if product.description != blank %}
+ {{ product.description }}
+ {% else %}
+ {{ 'onboarding.tab_content'| t }}
+ {% endif %}
+ {% endif %}
+ </div>
+ {% endif %}
+
+ {% for i in (1..3) %}
+ {% assign tab_text_key = 'tab_text_' | append: i %}
+ {% assign tab_heading_key = 'tab_heading_' | append: i %}
+ {% assign tab_heading_value = block.settings[tab_heading_key] %}
+ {% assign tab_text_value = block.settings[tab_text_key] %}
+
+ {% if tab_heading_value != blank %}
+ <input
+ class="product-tabs__radio"
+ id="product-tabs__radio-{{ i }}-{{ section.id }}"
+ name="{{ section.id }}"
+ tabindex="0"
+ type="radio"
+ {% if tab_counter == 1 and block.settings.show_product_description == false %}
+ checked
+ {% endif %}
+ >
+
+ <label class="product-tabs__label" for="product-tabs__radio-{{ i }}-{{ section.id }}">
+ {{ tab_heading_value | escape }}
+ </label>
+
+ <div class="product-tabs__panel rte" tabindex="0">
+ {{ tab_text_value }}
+ </div>
+
+ {% assign tab_counter = tab_counter | plus: 1 %}
+ {% endif %}
+ {% endfor %}
+ </div>
+
+ {% when 'price' %}
+ <div class="product__price" {{ block.shopify_attributes }}>
+ {%- comment -%}
+ DW trade/designer pricing tiers (2026-06-11): any customer carrying one of these
+ professional tags sees the trade price display (15% off, ×0.85). This restores the
+ pre-app behavior where every designer/trade tier got the discount — not just 'trade'.
+ Matches *any* tag containing 'designer' (interior_designer, commercial_designer, …)
+ plus the explicit tiers below. Add new tiers to the explicit list as needed.
+ {%- endcomment -%}
+ {%- assign dw_is_trade = false -%}
+ {%- for dw_t in customer.tags -%}
+ {%- assign dw_tl = dw_t | downcase | strip -%}
+ {%- if dw_tl contains 'designer' or dw_tl == 'trade' or dw_tl == 'architect' or dw_tl == 'stager' or dw_tl == 'hospitality' or dw_tl == 'retailer' -%}
+ {%- assign dw_is_trade = true -%}
+ {%- break -%}
+ {%- endif -%}
+ {%- endfor -%}
+ <input type="hidden" id="customer_tag" value="{% if dw_is_trade %}trade{% endif %}">
+ <p class="product-price">
+ {% comment %} SA Wholesale (saw_/wbuyx/"WHO") app removed 2026-06-11 — it left the price span blank. Render native variant price unconditionally. {% endcomment %}
+ <!-- native price markup -->
+ <meta itemprop="price" content="{{ selected_variant.price | money_without_currency }}">
+ <span class="product-price-minimum money {% if dw_is_trade %}original{% endif %}">
+ {% if product != blank %}
+ {{ selected_variant.price | money }}
+ {% else %}
+ {{ 'products.product.onboarding.price' | t }}
+ {% endif %}
+ </span>
+ {% if dw_is_trade %}
+ <span
+ class="product-price-discount"
+ >
+ {% assign target_product = selected_variant %}
+ {% assign com_selected_variant_title = selected_variant.title | capitalize %}
+ {% assign com_selected_variant_sku = selected_variant.sku | default: '' | downcase %}
+ {% if com_selected_variant_title == "Sample" or com_selected_variant_sku contains "sample" %}
+ {% comment %} {% render 'money', value: 0 %} {% endcomment %}
+ Complimentary Sample
+ {% else %}
+ {% assign com_variant_first_title = product.variants[0].title | capitalize %}
+ {% if com_variant_first_title == "Sample" and product.variants[1] != blank %}
+ {% assign target_product = product.variants[1] %}
+ {% endif %}
+ {% assign discounted_price = target_product.price | times:0.85 | round: 2 %}
+ {% render 'money', value: discounted_price %}
+ {% endif %}
+
+ </span>
+ {% endif %}
+ {% comment %}
+ <span
+ class="product-price-compare money original {% if selected_variant.compare_at_price > selected_variant.price %}visible{% endif %}"
+ >
+ {{ selected_variant.compare_at_price | money }}
+ </span>
+ {% endcomment %}
+ </p>
+
+ {% assign variant_for_unit_price = selected_variant %}
+ {% comment %}Inject unit-price begin{% endcomment %}
+ {% comment %}
+ @param variant_for_unit_price
+ Product variant for price
+ @param tax_text
+ String containing 'tax included' text
+ {% endcomment %}
+
+ {% capture total_quantity %}
+ <span class="product-price__unit-price-total-quantity" data-unit-price-quantity>
+ {{ variant_for_unit_price.unit_price_measurement.quantity_value }}{{ variant_for_unit_price.unit_price_measurement.quantity_unit }}
+ </span>
+ {% endcapture %}
+
+
+ {% capture unit_price %}
+ <span class="product-price__unit-price-amount money" data-unit-price-amount>
+ {{ variant_for_unit_price.unit_price | money }}
+ </span>
+ {% endcapture %}
+ {% capture unit_measure %}
+ <span class="product-price__unit-price-measure" data-unit-price-measure>
+ {%- if variant_for_unit_price.unit_price_measurement.reference_value != 1 -%}
+ {{ variant_for_unit_price.unit_price_measurement.reference_value }}
+ {%- endif %}
+ {{ variant_for_unit_price.unit_price_measurement.reference_unit }}
+ </span>
+ {% endcapture %}
+
+ <div
+ class="
+ product-price__unit-price
+ {% unless variant_for_unit_price.unit_price_measurement %}hidden{% endunless %}
+ "
+ data-unit-price
+ >
+ {{ 'products.product.price_per_unit_html' | t: total_quantity: total_quantity, unit_price: unit_price, unit_measure: unit_measure | strip_newlines }}
+ </div>
+
+ {% assign variant_for_unit_price = blank %}
+ {% comment %}Inject unit-price end{% endcomment %}
+
+
+ {%- capture tax_text -%}
+ {{ 'products.product.tax_line_html' | t }}
+ {%- endcapture -%}
+
+ {%- if tax_text != blank -%}
+ <div class="
+ product-price__tax
+ {% unless selected_variant.taxable %}hidden{% endunless %}
+ "
+ data-tax-line
+ >
+ {{ tax_text }}
+ </div>
+ {%- endif -%}
+
+ {{ form | payment_terms }}
+ </div>
+
+ {% when 'form' %}
+
+ <div class="product__form" {{ block.shopify_attributes }}>
+
+ {% unless product.has_only_default_variant %}
+ <div class="product-options">
+ {%-
+ render 'product-options-dropdown',
+ product: product,
+ form_id: product_id
+ -%}
+
+ <label
+ class="
+ product-option-column-1
+ product-option-row-4
+ product-option-quantity-label
+ {% if is_sample_like %}hidden{% endif %}
+ "
+ for="default-variant-quantity-input"
+ >
+ <strong>{{ 'general.general.quantity' | t }}:</strong>
+ </label>
+ {% comment %}
+ <input
+ class="product-option-quantity"
+ id="default-variant-quantity-input"
+ data-product-quantity-input
+ type="number"
+ inputmode="numeric"
+ name="quantity"
+ value="1"
+ >
+ {% endcomment %}
+ <!-- 020 -->
+ <div class="mm_quantity mm_buttons_added product-option-row-4 {% if is_sample_like %}hidden{% endif %}">
+ <input type="hidden" class="hquantity" name="hquantity" value="{{ prods_quantity_order_min }}">
+ <input type="button" value="-" class="minus">
+ <input
+ type="text"
+ step="{{ prods_quantity_order_units }}"
+ min="{{ prods_quantity_order_min }}"
+ max=""
+ name="quantity"
+ value="{{ prods_quantity_order_min }}"
+ title="Qty"
+ class="input-text qty text"
+ size="4"
+ pattern=""
+ inputmode=""
+ onkeypress="mm_validate(event)"
+ >
+ <input type="button" value="+" class="plus">
+ </div>
+ <!-- -->
+
+ <div class="selector-wrapper no-js-required">
+ <label for="product-select-{{ product_id }}"></label>
+ <select
+ class="product-select"
+ name="id"
+ id="product-select-{{ product_id }}"
+ data-select-ignore
+ >
+ {% for variant in product.variants %}
+ {% if variant.available %}
+ {% assign com_variant_title = variant.title | capitalize %}
+ {% if com_variant_title == "Sample" %}
+ {% assign dl_sample_variant = variant.id %}
+ {% assign dl_sample_variant_title = product.title | append: ' - ' | append: variant.title %}
+ {% assign dl_sample = 'small_sample' %}
+ {% endif %}
+
+ {% if com_variant_title == "Large Sample" %}
+ {% assign dl_sample_variant2 = variant.id %}
+ {% assign dl_sample_variant_title2 = product.title | append: ' - ' | append: variant.title %}
+ {% assign dl_sample2 = 'large_sample' %}
+ {% endif %}
+
+ <option
+ {% if variant == selected_variant %}selected="selected"{% endif %}
+ value="{{ variant.id }}"
+ data-variant-id="{{ variant.id }}"
+ data-sku="{{ variant.sku }}"
+ data-vprice="{{ variant.price }}"
+ >
+ {{ variant.title }} - {{ variant.price | money }}
+ </option>
+ {% else %}
+ <option
+ disabled="disabled"
+ data-variant-id="{{ variant.id }}"
+ value="{{ variant.id }}"
+ data-sku="{{ variant.sku }}">
+ {{ variant.title }} - {{ 'products.product.sold_out' | t }}
+ </option>
+ {% endif %}
+ {% endfor %}
+ </select>
+ </div>
+ </div>
+ {% else %}
+ <div class="product-options product-options-default-only">
+ <input
+ class="product-select"
+ name="id"
+ value="{{ product.variants[0].id }}"
+ type="hidden"
+ data-variant-title="{{ product.variants[0].title }}"
+ data-sku="{{ product.variants[0].sku }}"
+ data-vprice="{{ product.variants[0].price }}"
+ />
+
+ <label
+ class="
+ product-option-column-1
+ product-option-row-1
+ product-option-quantity-label
+ {% if is_sample_like %}hidden{% endif %}
+ "
+ for="quantity-input"
+ >
+ <strong>{{ 'general.general.quantity' | t }}:</strong>
+ </label>
+
+<!-- <input
+ class="product-option-quantity"
+ id="quantity-input"
+ data-product-quantity-input
+ type="number"
+ inputmode="numeric"
+ name="quantity"
+ value="1"
+ > -->
+ <!-- 020 -->
+ <div class="mm_quantity mm_buttons_added product-option-row-1 {% if is_sample_like %}hidden{% endif %}">
+ <input type="hidden" class="hquantity" name="hquantity" value="{{ prods_quantity_order_min }}">
+ <input type="button" value="-" class="minus">
+ <input
+ type="text"
+ step="{{ prods_quantity_order_units }}"
+ min="{{ prods_quantity_order_min }}"
+ max=""
+ name="quantity"
+ value="{{ prods_quantity_order_min }}"
+ title="Qty"
+ class="input-text qty text"
+ size="4"
+ pattern=""
+ inputmode=""
+ onkeypress="mm_validate(event)"
+ >
+ <input type="button" value="+" class="plus">
+ </div>
+ <!-- -->
+ </div>
+ {% endunless %}
+
+ {% for tag in product.tags %}
+ {% if tag contains 'custom mural' %}
+ <!-- Custom Price Calculator -->
+ <div id="custom-calculator" style="display:none" data-calculatorid="62388fabd48c8"></div>
+ <script>
+ document.querySelector(".product-options .last_variant").innerHTML = "Custom Sizes";
+
+ var appendCalc = setInterval(moveCalc, 300);
+
+ function moveCalc() {
+ var calc = document.getElementById("calculator");
+ if (calc) {
+ document
+ .querySelector(".product-options")
+ .insertBefore(
+ document.getElementById("calculator"),
+ document.querySelector(".product-options").children[2]
+ );
+ clearInterval(appendCalc);
+ }
+ }
+ </script>
+ <style>
+ .product-options {
+ display: flex;
+ flex-direction: column;
+ align-items: baseline;
+ margin-left: 0;
+ }
+ .product-options .product-option-quantity-label,
+ .mm_quantity.mm_buttons_added {
+ display: none !important;
+ }
+ </style>
+ {% endif %}
+ {% endfor %}
+
+ {% if product.available %}
+ <div id="infiniteoptions-container"></div>
+ <div id="uploadery-container"></div>
+ {% endif %}
+
+ <!-- Print PDF button -->
+ {% render 'button-product-pdf-print' %}
+
+ <div
+ class="
+ product-add-to-cart
+ {% if product != blank and show_payment_button %}
+ product-smart-payments
+ {% endif %}
+ "
+ >
+ {% if selected_variant.available and template.name != 'password' %}
+ <!-- 020 -->
+ <div class="custom-contact-message"></div>
+
+ <a
+ href="#"
+ class="dl-contact-btn custom-popup-contact-form"
+ {% if variant.price == 0 %}
+ style="display:inline-block;"
+ {% else %}
+ style="display:none;"
+ {% endif %}
+ >
+ Contact Us
+ </a>
+
+ {% assign title = 'Request Sample' %}
+ {% assign showonly = blank %}
+ {% if product.tags contains "Showroom only" and product.price == 0 %}
+ {% assign showonly = 'showonly' %}
+ {% endif %}
+ {% if product.tags contains "Showroom Only" and product.price == 0 %}
+ {% assign showonly = 'showonly' %}
+ {% endif %}
+
+ {% if is_sample_sku and dl_sample == blank and dl_sample2 == blank %}
+ <a
+ href=""
+ data-dlvid="{{ selected_variant.id }}"
+ data-dlvtitle="{{ product.title | append: ' - Sample' }}"
+ class="dl-sample-btn {{ showonly }}"
+ data-product-id="{{ product.id }}"
+ >
+ {{ title }}
+ </a>
+ {% endif %}
+
+ {% if dl_sample != blank and dl_sample2 != blank and dl_sample == 'small_sample' and dl_sample2 == 'large_sample' %}
+ {% assign title = 'Small Sample' %}
+ {% endif %}
+
+ {% if dl_sample == 'small_sample' and dl_sample_variant !='' and dl_sample_variant_title != '' %}
+ <a
+ href=""
+ data-dlvid="{{ dl_sample_variant }}"
+ data-dlvtitle="{{ dl_sample_variant_title }}"
+ class="dl-sample-btn {{ showonly }}"
+ data-product-id="{{ product.id }}"
+ >
+ {{ title }}
+ </a>
+ {% endif %}
+
+ {% if dl_sample2 == 'large_sample' and dl_sample_variant2 !='' and dl_sample_variant_title2 != '' %}
+ <a
+ href=""
+ data-dlvid="{{ dl_sample_variant2 }}"
+ data-dlvtitle="{{ dl_sample_variant_title2 }}"
+ class="dl-sample-btn {{ showonly }}"
+ data-product-id="{{ product.id }}"
+ >
+ Large Sample
+ </a>
+ {% endif %}
+ {% for variant_item in product.variants %}
+ {% if variant_item.price == 0 and variant_item.title contains "Sample" and variant_item.title != "Sample" %}
+ {% assign showing_title = variant_item.title %}
+ {% if showing_title == "Textured Vinyl Sample" %}
+ {% assign showing_title = "Vinyl Sample" %}
+ {% endif %}
+ <a
+ href=""
+ data-dlvid="{{ variant_item.id }}"
+ data-dlvtitle="{{ variant_item.title }}"
+ class="dl-second-sample-btn {% if variant_item.id != product.selected_or_first_available_variant.id %}unavailable{% endif %} {{ showonly }}"
+ data-product-id="{{ product.id }}"
+ >
+ Request {{ showing_title }}
+ </a>
+ {% endif %}
+ {% endfor %}
+
+ {% render 'spec-sheet-button' %}
+
+<!-- <input class="add-to-cart" type="submit" value="{{ 'products.product.add_to_cart' | t }}" /> -->
+ {% assign com_first_variant_title = product.selected_or_first_available_variant.title | capitalize %}
+ <input class="add-to-cart {% if is_sample_like %}hidden{% endif %} {% if product.variants.size == 1 and com_first_variant_title == "Sample" %}hidden{% endif %} {% if product.selected_or_first_available_variant.price == 0 %}unavailable{% endif %}" type="submit" value="{{ 'products.product.add_to_cart' | t }}"/>
+ {% else %}
+ <input class="add-to-cart disabled" type="submit" value="{{ 'products.product.sold_out' | t }}" disabled/>
+ {% endif %}
+
+ <!--020 -->
+ {% for t in product.tags %}
+ {% if t == 'Customize' %}
+ <a href="#" class="dl-customize-btn custom-popup-contact-form">Customize this Pattern</a>
+ {% endif %}
+ {% endfor %}
+
+ {% if show_payment_button %}
+ {{ form | payment_button }}
+ {% endif %}
+
+ {% capture the_snippet_fave %}{% include 'socialshopwave-widget-fave' %}{% endcapture %}
+ {% unless the_snippet_fave contains 'Liquid error' %}
+ {{ the_snippet_fave }}
+ {% endunless %}
+ </div>
+ <div class="product-message"></div>
+
+ <div data-surface-pick-up></div>
+ {%-
+ render 'modal',
+ modal_id: 'surface-pick-up',
+ modal_class: 'surface-pick-up-modal',
+ -%}
+ </div>
+
+ {% when 'social' %}
+ <div class="product__social" {{ block.shopify_attributes }}>
+ {% render 'share-buttons' %}
+ </div>
+
+ {% when 'description' %}
+ {% if product != blank and product.description != blank %}
+ <div class="product-description rte" {{ block.shopify_attributes }}>
+ {{ product.description }}
+ </div>
+ {% elsif product == blank %}
+ <div class="product-description rte" {{ block.shopify_attributes }}>
+ {{ 'products.product.onboarding.description' | t }}
+ </div>
+ {% endif %}
+
+ {%- when 'rating' -%}
+ {%- if product.metafields.reviews.rating.value != blank -%}
+ <div class="product__rating rating" {{ block.shopify_attributes }}>
+ {%
+ render 'rating-stars',
+ value: product.metafields.reviews.rating.value.rating,
+ scale_max: product.metafields.reviews.rating.value.scale_max,
+ %}
+ <p class="rating__text">
+ <span aria-hidden="true">{{ product.metafields.reviews.rating.value }} / {{ product.metafields.reviews.rating.value.scale_max }}</span>
+ </p>
+ <p class="rating__count">
+ <span aria-hidden="true">({{ product.metafields.reviews.rating_count }})</span>
+ <span class="visually-hidden">{{ product.metafields.reviews.rating_count }} {{ "general.accessibility.total_reviews" | t }}</span>
+ </p>
+ </div>
+ {%- endif -%}
+ {% endcase %}
+{% endfor %}
diff --git a/shopify/theme-fixes/custom-js-qty-2026-06-30/pureplus.cjs b/shopify/theme-fixes/custom-js-qty-2026-06-30/pureplus.cjs
new file mode 100644
index 00000000..3e971113
--- /dev/null
+++ b/shopify/theme-fixes/custom-js-qty-2026-06-30/pureplus.cjs
@@ -0,0 +1,24 @@
+const { chromium } = require(require('path').join(process.env.HOME, 'Projects/Designer-Wallcoverings/node_modules/playwright'));
+const fs=require('fs');
+const DEV_JS=fs.readFileSync(__dirname+'/DEV_custom.js','utf8');
+const URL='https://www.designerwallcoverings.com/products/dwtt-80064-designer-wallcoverings-los-angeles';
+(async()=>{
+ const b=await chromium.launch();const ctx=await b.newContext();
+ await ctx.route(/\/cdn\/shop\/t\/\d+\/assets\/custom\.js(\?|$)/,r=>{if(/boost-sd/.test(r.request().url()))return r.continue();r.fulfill({status:200,contentType:'application/javascript',body:DEV_JS});});
+ const p=await ctx.newPage();
+ await p.goto(URL,{waitUntil:'domcontentloaded',timeout:60000});
+ await p.waitForSelector('.mm_quantity input[name="quantity"]',{state:'attached',timeout:30000});
+ await p.evaluate(()=>{document.querySelectorAll('.mm_quantity .qty').forEach(q=>{q.setAttribute('data-order-step','2');q.setAttribute('data-order-min','2');});});
+ await p.waitForTimeout(1800);
+ await p.evaluate(()=>{const s=document.querySelector('.product-select');if(s&&s.tagName==='SELECT'){const o=Array.from(s.options).find(x=>!/sample/i.test(x.textContent)&&!/-sample$/i.test(x.getAttribute('data-sku')||''))||s.options[0];s.value=o.value;s.dispatchEvent(new Event('change',{bubbles:true}));if(window.jQuery)window.jQuery(s).trigger('change');}});
+ await p.waitForTimeout(1800);
+ // Remove ALL change/blur listeners on .qty (including mine) to test raw +/- clicks
+ await p.evaluate(()=>{const $=window.jQuery;$(document).off('change blur','.mm_quantity .qty');$(document).off('change','.mm_quantity .qty');$(document).off('blur','.mm_quantity .qty');$(document).off('blur','.mm_quantity .input-text.qty');$(document).off('click','.minus');});
+ const rd=async()=>p.evaluate(()=>document.querySelector('.mm_quantity .qty').value);
+ console.log('init',await rd());
+ for(const dir of ['plus','plus','plus','minus','minus','minus','minus']){
+ await p.click('.mm_quantity .'+dir); await p.waitForTimeout(180);
+ console.log(dir,'->',await rd());
+ }
+ await b.close();
+})().catch(e=>{console.error(e);process.exit(1)});
diff --git a/shopify/theme-fixes/custom-js-qty-2026-06-30/rawattr.cjs b/shopify/theme-fixes/custom-js-qty-2026-06-30/rawattr.cjs
new file mode 100644
index 00000000..e7924ff5
--- /dev/null
+++ b/shopify/theme-fixes/custom-js-qty-2026-06-30/rawattr.cjs
@@ -0,0 +1,15 @@
+const { chromium } = require(require('path').join(process.env.HOME, 'Projects/Designer-Wallcoverings/node_modules/playwright'));
+const THEME=process.argv[2];
+const URL=`https://designer-laboratory-sandbox.myshopify.com/products/dwtt-80064-designer-wallcoverings-los-angeles?preview_theme_id=${THEME}`;
+(async()=>{
+ const b=await chromium.launch();const p=await(await b.newContext()).newPage();
+ await p.goto(URL,{waitUntil:'domcontentloaded',timeout:60000});
+ await p.waitForSelector('.mm_quantity input[name="quantity"]',{state:'attached',timeout:30000});
+ // read IMMEDIATELY, before jQuery ready timers mutate — but DOMReady likely already ran. Read anyway at t=0.
+ const now=await p.evaluate(()=>{const q=document.querySelector('.mm_quantity input[name="quantity"]');return{step:q.getAttribute('step'),min:q.getAttribute('min'),val:q.value,defaultSelectedIsSample:(()=>{const s=document.querySelector('.product-select');if(!s||s.tagName!=='SELECT')return 'no-select';const o=s.options[s.selectedIndex];return {text:o.textContent.trim(),sku:o.getAttribute('data-sku')};})()};});
+ console.log('IMMEDIATE (post-domcontentloaded):',JSON.stringify(now));
+ await p.waitForTimeout(2000);
+ const later=await p.evaluate(()=>{const q=document.querySelector('.mm_quantity input[name="quantity"]');return{step:q.getAttribute('step'),min:q.getAttribute('min'),val:q.value};});
+ console.log('AFTER 2s:',JSON.stringify(later));
+ await b.close();
+})().catch(e=>{console.error(e);process.exit(1)});
diff --git a/shopify/theme-fixes/custom-js-qty-2026-06-30/srccheck.cjs b/shopify/theme-fixes/custom-js-qty-2026-06-30/srccheck.cjs
new file mode 100644
index 00000000..89b724a5
--- /dev/null
+++ b/shopify/theme-fixes/custom-js-qty-2026-06-30/srccheck.cjs
@@ -0,0 +1,20 @@
+const { chromium } = require(require('path').join(process.env.HOME, 'Projects/Designer-Wallcoverings/node_modules/playwright'));
+const THEME=process.argv[2];
+const URL=`https://designer-laboratory-sandbox.myshopify.com/products/dwtt-80064-designer-wallcoverings-los-angeles?preview_theme_id=${THEME}`;
+(async()=>{
+ const b=await chromium.launch();const p=await(await b.newContext()).newPage();
+ let customUrl=null,customBody=null;
+ p.on('response',async resp=>{const u=resp.url();if(/\/custom\.js(\?|$)/.test(u)&&!/boost-sd/.test(u)){customUrl=u;try{customBody=await resp.text();}catch(e){}}});
+ await p.goto(URL,{waitUntil:'domcontentloaded',timeout:60000});
+ await p.waitForSelector('.mm_quantity input[name="quantity"]',{state:'attached',timeout:30000});
+ await p.waitForTimeout(2500);
+ console.log('custom.js URL:',customUrl);
+ if(customBody){
+ console.log('loaded custom.js has qty-stepper repair marker:', customBody.includes('qty-stepper repair'));
+ console.log('loaded custom.js has data-order-step:', customBody.includes('data-order-step'));
+ console.log('loaded custom.js length:', customBody.length);
+ } else {
+ console.log('did not capture custom.js body via response; probing window for handler');
+ }
+ await b.close();
+})().catch(e=>{console.error(e);process.exit(1)});
diff --git a/shopify/theme-fixes/custom-js-qty-2026-06-30/verify-dev-asset.cjs b/shopify/theme-fixes/custom-js-qty-2026-06-30/verify-dev-asset.cjs
new file mode 100644
index 00000000..08e5792f
--- /dev/null
+++ b/shopify/theme-fixes/custom-js-qty-2026-06-30/verify-dev-asset.cjs
@@ -0,0 +1,86 @@
+// Load the real product page but SERVE THE DEV THEME'S custom.js bytes (route-intercept),
+// and stamp the data-order-* attrs the DEV product-form-content.liquid now renders, so the
+// dev theme's own DOMReady binds the FIXED handlers. This exercises the exact pushed asset.
+const { chromium } = require(require('path').join(process.env.HOME, 'Projects/Designer-Wallcoverings/node_modules/playwright'));
+const fs = require('fs');
+
+const HANDLE = process.argv[2] || 'dwtt-80064-designer-wallcoverings-los-angeles';
+const EXPECT_STEP = process.argv[3] ? Number(process.argv[3]) : 2;
+const EXPECT_MIN = process.argv[4] ? Number(process.argv[4]) : 2;
+const LABEL = process.argv[5] || HANDLE;
+const URL = `https://www.designerwallcoverings.com/products/${HANDLE}`;
+const DEV_JS = fs.readFileSync(__dirname + '/DEV_custom.js', 'utf8');
+
+(async () => {
+ const browser = await chromium.launch();
+ const ctx = await browser.newContext();
+ // Intercept the published custom.js and return the DEV theme bytes.
+ await ctx.route(/\/cdn\/shop\/t\/\d+\/assets\/custom\.js(\?|$)/, route => {
+ if (/boost-sd/.test(route.request().url())) return route.continue();
+ route.fulfill({ status: 200, contentType: 'application/javascript', body: DEV_JS });
+ });
+ const page = await ctx.newPage();
+ const errors = [];
+ page.on('pageerror', e => errors.push('PAGEERROR: ' + e.message));
+ page.on('console', m => { if (m.type() === 'error' && !/Content Security Policy|Failed to load resource/.test(m.text())) errors.push('CONSOLE.error: ' + m.text()); });
+
+ await page.goto(URL, { waitUntil: 'domcontentloaded', timeout: 60000 });
+ await page.waitForSelector('.mm_quantity input[name="quantity"]', { state: 'attached', timeout: 30000 });
+
+ // Stamp data-order-* onto qty inputs (what the DEV product-form-content.liquid renders).
+ await page.evaluate(({ step, min }) => {
+ document.querySelectorAll('.mm_quantity .qty').forEach(q => {
+ q.setAttribute('data-order-step', String(step));
+ q.setAttribute('data-order-min', String(min));
+ });
+ }, { step: EXPECT_STEP, min: EXPECT_MIN });
+
+ await page.waitForTimeout(1800);
+ // Select roll variant
+ await page.evaluate(() => {
+ const sel = document.querySelector('.product-select');
+ if (sel && sel.tagName === 'SELECT') {
+ const opts = Array.from(sel.options);
+ const roll = opts.find(o => !/sample/i.test(o.textContent) && !/-sample$/i.test(o.getAttribute('data-sku') || '')) || opts[0];
+ sel.value = roll.value; sel.dispatchEvent(new Event('change', { bubbles: true }));
+ if (window.jQuery) window.jQuery(sel).trigger('change');
+ }
+ });
+ await page.waitForTimeout(1600); // past the leftover 1000ms liquid reset
+
+ // The published page's INLINE product-form-content min-enforcement handlers (blur/change/
+ // setTimeout snap-to-min) are still bound and fight the stepper — on the DEV theme the fixed
+ // product-form-content.liquid replaces them. Remove those competing document-delegated
+ // handlers so this test reflects the dev-theme environment (custom.js stepper only).
+ await page.evaluate(() => {
+ const $ = window.jQuery;
+ // Rebind: keep ONLY the custom.js stepper handlers. The inline liquid ones were bound as
+ // jQuery(document).on('click','.minus',...) / on('blur','.mm_quantity .input-text.qty',...)
+ // — we can't selectively remove by function ref, but the dev liquid simply doesn't emit them,
+ // so neutralize by removing the specific selectors the inline block used.
+ $(document).off('click', '.minus'); // inline min-enforcement (plus is untouched)
+ $(document).off('blur', '.mm_quantity .input-text.qty'); // inline blur snap
+ });
+
+ const getState = async () => page.evaluate(() => {
+ const q = document.querySelector('.mm_quantity .qty');
+ const h = document.querySelector('.mm_quantity .hquantity');
+ return { qty: q ? q.value : null, hquantity: h ? h.getAttribute('value') : null, step: q ? q.getAttribute('data-order-step') : null, min: q ? q.getAttribute('data-order-min') : null };
+ });
+ const clickPlus = async () => { await page.click('.mm_quantity .plus'); await page.waitForTimeout(160); };
+ const clickMinus = async () => { await page.click('.mm_quantity .minus'); await page.waitForTimeout(160); };
+
+ const out = { label: LABEL, url: URL, expectStep: EXPECT_STEP, expectMin: EXPECT_MIN, steps: [] };
+ out.steps.push(['initial', await getState()]);
+ await clickPlus(); out.steps.push(['after +', await getState()]);
+ await clickPlus(); out.steps.push(['after +', await getState()]);
+ await clickMinus(); out.steps.push(['after -', await getState()]);
+ await clickMinus(); out.steps.push(['after -', await getState()]);
+ await clickMinus(); out.steps.push(['after - (floor)', await getState()]);
+ await page.evaluate((m) => { const q = document.querySelector('.mm_quantity .qty'); q.value = String(m + 1); window.jQuery(q).trigger('change'); }, EXPECT_MIN);
+ await page.waitForTimeout(160);
+ out.steps.push(['typed min+1 -> snap', await getState()]);
+ out.errors = errors;
+ console.log(JSON.stringify(out, null, 2));
+ await browser.close();
+})().catch(e => { console.error('FATAL', e); process.exit(1); });
diff --git a/shopify/theme-fixes/custom-js-qty-2026-06-30/verify-logic.cjs b/shopify/theme-fixes/custom-js-qty-2026-06-30/verify-logic.cjs
new file mode 100644
index 00000000..60291cbd
--- /dev/null
+++ b/shopify/theme-fixes/custom-js-qty-2026-06-30/verify-logic.cjs
@@ -0,0 +1,138 @@
+// Verify the qty-stepper FIX LOGIC against the real product DOM.
+// Preview-asset routing serves the published theme's custom.js, so instead we
+// (1) load the real page, (2) apply the liquid change (add data-order-step / data-order-min
+// exactly as the fixed product-form-content.liquid now renders them), (3) tear down the OLD
+// document-delegated .plus/.minus handlers and bind the NEW fixed handler (byte-identical to
+// what was pushed to assets/custom.js.liquid on the dev theme), then (4) drive the buttons.
+const { chromium } = require(require('path').join(process.env.HOME, 'Projects/Designer-Wallcoverings/node_modules/playwright'));
+const fs = require('fs');
+const path = require('path');
+
+const HANDLE = process.argv[2] || 'dwtt-80064-designer-wallcoverings-los-angeles';
+const EXPECT_STEP = process.argv[3] ? Number(process.argv[3]) : 2;
+const EXPECT_MIN = process.argv[4] ? Number(process.argv[4]) : 2;
+const LABEL = process.argv[5] || HANDLE;
+const URL = `https://www.designerwallcoverings.com/products/${HANDLE}`;
+
+(async () => {
+ const browser = await chromium.launch();
+ const page = await (await browser.newContext()).newPage();
+ const errors = [];
+ page.on('pageerror', e => errors.push('PAGEERROR: ' + e.message));
+ page.on('console', m => { if (m.type() === 'error' && !/Content Security Policy|Failed to load resource/.test(m.text())) errors.push('CONSOLE.error: ' + m.text()); });
+
+ await page.goto(URL, { waitUntil: 'domcontentloaded', timeout: 60000 });
+ await page.waitForSelector('.mm_quantity input[name="quantity"]', { state: 'attached', timeout: 30000 });
+ await page.waitForTimeout(1800);
+
+ // Select the roll (non-sample) variant like a shopper would.
+ await page.evaluate(() => {
+ const sel = document.querySelector('.product-select');
+ if (sel && sel.tagName === 'SELECT') {
+ const opts = Array.from(sel.options);
+ const roll = opts.find(o => !/sample/i.test(o.textContent) && !/-sample$/i.test(o.getAttribute('data-sku') || '')) || opts[0];
+ sel.value = roll.value; sel.dispatchEvent(new Event('change', { bubbles: true }));
+ if (window.jQuery) window.jQuery(sel).trigger('change');
+ }
+ });
+ await page.waitForTimeout(1200);
+
+ // Apply the liquid fix + swap in the fixed handler.
+ await page.evaluate(({ step, min }) => {
+ const $ = window.jQuery;
+ // (a) liquid fix: stamp pristine data-order-* onto every qty input (what the fixed liquid emits)
+ document.querySelectorAll('.mm_quantity .qty').forEach(q => {
+ q.setAttribute('data-order-step', String(step));
+ q.setAttribute('data-order-min', String(min));
+ });
+ // (b) tear down the OLD delegated plus/minus handlers (published theme) so only ours runs
+ $(document).off('click', '.plus, .minus');
+ $(document).off('click', '.minus'); // liquid min-enforcement minus handler
+ $(document).off('change blur', '.mm_quantity .qty');
+ $(document).off('blur', '.mm_quantity .input-text.qty');
+
+ // (c) bind the NEW fixed handler (mirror of assets/custom.js.liquid, DWTT lines 39+)
+ $(document).on('click', '.plus, .minus', async function (event) {
+ event.preventDefault();
+ var $btn = $(this);
+ try {
+ var $wrap = $btn.closest('.mm_quantity');
+ var a = $wrap.find('.qty');
+ if (!a.length) return;
+ var stp = parseFloat(a.attr('data-order-step'));
+ if (!isFinite(stp) || stp <= 0) stp = parseFloat(a.attr('step'));
+ if (!isFinite(stp) || stp <= 0) stp = 1;
+ var mn = parseFloat(a.attr('data-order-min'));
+ if (!isFinite(mn) || mn <= 0) mn = parseFloat(a.attr('min'));
+ if (!isFinite(mn) || mn <= 0) mn = stp;
+ var maxRaw = a.attr('max');
+ var mx = parseFloat(maxRaw);
+ var hasMax = (maxRaw != null && maxRaw !== '' && isFinite(mx));
+ var decimals = String(stp).getDecimals();
+ var current = parseFloat(a.val());
+ if (!isFinite(current)) current = mn;
+ var next;
+ if ($btn.is('.plus')) { next = current + stp; if (hasMax && next > mx) next = mx; }
+ else { next = current - stp; if (next < mn) next = mn; }
+ var gridSteps = Math.round((next - mn) / stp);
+ if (gridSteps < 0) gridSteps = 0;
+ next = mn + gridSteps * stp;
+ if (hasMax && next > mx) next = mx;
+ var nextStr = next.toFixed(decimals);
+ a.val(nextStr);
+ $wrap.find('.hquantity').attr('value', nextStr).val(nextStr);
+ } catch (err) { if (window.console && console.warn) console.warn('qty stepper error:', err); }
+ });
+ $(document).on('change blur', '.mm_quantity .qty', function () {
+ try {
+ var $wrap = $(this).closest('.mm_quantity');
+ var a = $(this);
+ var stp = parseFloat(a.attr('data-order-step'));
+ if (!isFinite(stp) || stp <= 0) stp = parseFloat(a.attr('step'));
+ if (!isFinite(stp) || stp <= 0) stp = 1;
+ var mn = parseFloat(a.attr('data-order-min'));
+ if (!isFinite(mn) || mn <= 0) mn = parseFloat(a.attr('min'));
+ if (!isFinite(mn) || mn <= 0) mn = stp;
+ var v = parseFloat(a.val());
+ if (!isFinite(v) || v < mn) v = mn;
+ var gridSteps = Math.round((v - mn) / stp);
+ if (gridSteps < 0) gridSteps = 0;
+ var snapped = mn + gridSteps * stp;
+ var snappedStr = snapped.toFixed(String(stp).getDecimals());
+ if (a.val() !== snappedStr) a.val(snappedStr);
+ $wrap.find('.hquantity').attr('value', snappedStr).val(snappedStr);
+ } catch (err) {}
+ });
+
+ // Set the visible value to min like the fixed variant-handler / liquid would.
+ document.querySelectorAll('.mm_quantity .qty').forEach(q => {
+ if (parseFloat(q.value) < min) { q.value = String(min); }
+ const h = q.closest('.mm_quantity').querySelector('.hquantity');
+ if (h) { h.setAttribute('value', q.value); h.value = q.value; }
+ });
+ }, { step: EXPECT_STEP, min: EXPECT_MIN });
+
+ const getState = async () => page.evaluate(() => {
+ const q = document.querySelector('.mm_quantity .qty');
+ const h = document.querySelector('.mm_quantity .hquantity');
+ return { qty: q ? q.value : null, hquantity: h ? (h.getAttribute('value')) : null };
+ });
+ const clickPlus = async () => { await page.evaluate(() => window.jQuery('.mm_quantity .plus').first().trigger('click')); await page.waitForTimeout(120); };
+ const clickMinus = async () => { await page.evaluate(() => window.jQuery('.mm_quantity .minus').first().trigger('click')); await page.waitForTimeout(120); };
+
+ await page.waitForTimeout(1400); // let leftover liquid setTimeout(1000ms) reset fire BEFORE we start clicking
+ const out = { label: LABEL, url: URL, expectStep: EXPECT_STEP, expectMin: EXPECT_MIN, steps: [] };
+ out.steps.push(['initial', await getState()]);
+ await clickPlus(); out.steps.push(['after +', await getState()]);
+ await clickPlus(); out.steps.push(['after +', await getState()]);
+ await clickMinus(); out.steps.push(['after -', await getState()]);
+ await clickMinus(); out.steps.push(['after -', await getState()]);
+ await clickMinus(); out.steps.push(['after - (floor)', await getState()]);
+ // invalid-typed value snaps
+ await page.evaluate((m) => { const q=document.querySelector('.mm_quantity .qty'); q.value=String(m+1); window.jQuery(q).trigger('change'); }, EXPECT_MIN);
+ await page.waitForTimeout(120);
+ out.steps.push(['typed min+1 -> snap', await getState()]);
+ out.errors = errors;
+ console.log(JSON.stringify(out, null, 2));
+ await browser.close();
+})().catch(e => { console.error('FATAL', e); process.exit(1); });
← 67ffd784 auto-save: 2026-06-30T20:13:04 (3 files) — pending-approval/
·
back to Designer Wallcoverings
·
DWTT qty stepper: add combined diffs, clean harness proof + 08928754 →