← back to Dw Theme Boost Fix

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");
});