← back to Dw Theme Hamburger
assets/custom.js.liquid
582 lines
/*========== 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");
});