Сейчас узнаем, какой подарок выпадет именно Вам
Крутите колесо
Как изменить шаг количества товара в Тильда

Как изменить шаг количества товара в Тильда

1
Создали каталог, добавили товары и вывели кнопку количества в карточку товара
2
Для товаров с другим шагом создали дополнительную опцию "Упаковка" и прописали нужное значение.
Например, "по 50 шт" (смотри скрин)
3
Вставляем код на страницу в блок Т123
Код вставляется в обычный и продуктовый footer
Mo-ti Level Up
Видео инструкции по добавлению кода и работе с Zero Block.
Как изменить шаг количества товара в Тильда
Фрагмент видео
Библиотека для примера
<script>
'use strict';

(function () {
  const ready = fn => document.readyState === 'loading'
    ? document.addEventListener('DOMContentLoaded', fn)
    : fn();

  ready(function () {

    const DEBUG = false;
    const log = (...a) => DEBUG && console.log('[WS]', ...a);

    const sizeLabel = 'Упаковка';
    const zeroStartQuant = true;

    const SEL = {
      QTY_INPUT: '.t-store__prod__quantity-input, .t-catalog__prod__quantity-input',
      QTY_PLUS: '.t-store__prod__quantity__plus-wrapper, .t-catalog__prod__quantity__plus-wrapper',
      QTY_MINUS: '.t-store__prod__quantity__minus-wrapper, .t-catalog__prod__quantity__minus-wrapper',
      QTY_WRAPPER: '.t-store__prod__quantity, .t-catalog__prod__quantity',
      PRICE_WRAPPER: '.js-store-price-wrapper, .js-catalog-price-wrapper',
      PRICE_VALUE: '.js-product-price, .js-catalog-prod-price-val',
      PRICE_CURRENCY: 'div[class*="_price-currency"]'
    };

   
    function getFirstNumber(str) {
      if (!str) return 0;
      const m = String(str).match(/\d+/);
      return m ? parseInt(m[0], 10) : 0;
    }

    function cacheQty(product, val) {
      if (!product) return;
      product.dataset._qty = val;
    }

    function parsePrice(t) {
      if (!t) return NaN;
      const cleaned = String(t).replace(/[^\d.,]/g, '').replace(',', '.');
      return parseFloat(cleaned);
    }

    function formatPrice(n) {
      if (isNaN(n)) return '';
      const fixed = n.toFixed(2);
      const [intPart, decPart] = fixed.split('.');
      const withSpaces = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
      return `${withSpaces}.${decPart}`;
    }

 
    function getInv(p) {
      if (!p || !p.hasAttribute('data-product-inv')) return Infinity;
      const raw = (p.getAttribute('data-product-inv') || '').trim();
      if (raw === '') return Infinity;
      const n = parseInt(raw, 10);
      return isNaN(n) ? Infinity : n;
    }

    function getBaseStep(product) {
      const optNames = product.querySelectorAll('.js-product-option-name');
      const target = Array.from(optNames).find(el => el.textContent.trim() === sizeLabel);
      let step = 1;
      if (target) {
        const sel = target.closest('.js-product-option')?.querySelector('select');
        if (sel?.options?.length) {
          step = getFirstNumber(sel.options[0].value) || 1;
        }
      }
      return step;
    }

    function getStep(product) {
      let step = getBaseStep(product);
      const inv = getInv(product);
      if (isFinite(inv) && inv > 0 && inv < step) step = 1;
      return step;
    }

    function getMaxQty(p) {
      const inv = getInv(p);
      if (!isFinite(inv)) return Infinity;
      if (inv <= 0) return 0;
      const step = getStep(p);
      return Math.floor(inv / step) * step;
    }

 
    function setupWholesaleUI(product) {
      if (!product) return;

      const optNames = product.querySelectorAll('.js-product-option-name');
      const target = Array.from(optNames).find(el => el.textContent.trim() === sizeLabel);

      if (!target) {
        const staleWrapper = product.querySelector('.wholesale-wrapper');
        if (staleWrapper) staleWrapper.remove();
        product.querySelectorAll('.js-product-option.wholesale').forEach(el => {
          el.classList.remove('wholesale');
        });
        return;
      }

      const wrap = target.closest('.js-product-option');
      if (!wrap || !product.contains(wrap)) return;

      wrap.classList.add('wholesale', 't-text');
      const sel = wrap.querySelector('select');
      if (sel) sel.disabled = true;

      if (!sel || !sel.options.length) return;
      const optText = sel.options[0].textContent.trim();

      let wrapper = product.querySelector('.wholesale-wrapper');
      if (wrapper) {
        const title = wrapper.querySelector('.wholesale-title');
        if (title && title.textContent.trim() !== optText) {
          title.textContent = optText;
        }
        return;
      }

      const w = document.createElement('div');
      w.className = 'wholesale-wrapper t-text';
      const t = document.createElement('div');
      t.className = 'wholesale-title';
      t.textContent = optText;
      w.appendChild(t);

      const controls = product.querySelector('.js-product-controls-wrapper');
      if (controls && product.contains(controls)) {
        controls.parentNode.insertBefore(w, controls);
      } else {
        wrap.insertBefore(w, wrap.firstChild);
      }
    }

    function updateTotalLabel(p) {
      const inp = p.querySelector(SEL.QTY_INPUT);
      if (!inp) return;
      const q = parseInt(inp.value, 10);
      const pw = p.querySelector(SEL.PRICE_WRAPPER);
      if (!pw) return;

      if (isNaN(q) || q <= 0) {
        pw.removeAttribute('data-total-label');
        return;
      }
      const pe = pw.querySelector(SEL.PRICE_VALUE);
      if (!pe) return;
      const pr = parsePrice(pe.textContent);
      if (isNaN(pr) || pr <= 0) return;
      let c = '';
      const ce = pw.querySelector(SEL.PRICE_CURRENCY);
      if (ce) c = ce.textContent.trim();
      const tot = pr * q;
      pw.setAttribute('data-total-label', `${pr} x ${q} = ${formatPrice(tot)} ${c}`.trim());
      pw.classList.add('t-text');
    }

    function updateOrderButtonState(p) {
      if (!p) return;
      const inp = p.querySelector(SEL.QTY_INPUT);
      if (!inp) return;
      const q = parseInt(inp.value, 10) || 0;
      const max = getMaxQty(p);
      const disabled = q <= 0 || max <= 0;
      const links = p.querySelectorAll('a[href^="#order"]');
      links.forEach(a => {
        if (disabled) {
          a.classList.add('ws-disabled');
          a.setAttribute('data-ws-disabled', '1');
        } else {
          a.classList.remove('ws-disabled');
          a.removeAttribute('data-ws-disabled');
        }
      });
    }

    function setQty(p, value) {
      const inp = p.querySelector(SEL.QTY_INPUT);
      if (!inp) return;
      inp.value = value;
      inp.setAttribute('value', String(value));
      cacheQty(p, value);
      updateTotalLabel(p);
      updateOrderButtonState(p);
    }

    function applyInitialQty(p) {
      const inp = p.querySelector(SEL.QTY_INPUT);
      if (!inp) return;

      if (p.dataset._userTouched === '1') {
        updateTotalLabel(p);
        updateOrderButtonState(p);
        return;
      }

      if (zeroStartQuant) {
        inp.setAttribute('min', '0');
        setQty(p, 0);
      } else {
        const step = getStep(p);
        const max = getMaxQty(p);
        const start = (isFinite(max) && max > 0) ? Math.min(step, max) : step;
        inp.setAttribute('min', String(step));
        setQty(p, start);
      }
    }

    function handleBlur(e) {
      const inp = e.target;
      const p = inp.closest('.js-product');
      if (!p) return;
      p.dataset._userTouched = '1';
      const step = getStep(p);
      const max = getMaxQty(p);
      let v = parseInt(inp.value, 10);
      if (isNaN(v) || v < 0) v = 0;

      if (v > 0) {
        v = Math.max(step, Math.round(v / step) * step);
        if (v > max) v = Math.max(0, Math.floor(max / step) * step);
      } else {
        v = zeroStartQuant ? 0 : Math.min(step, max);
      }
      setQty(p, v);
    }

    function initProduct(p) {
      if (!p) return;
      if (p.closest('.t706__product')) return;

      const inp = p.querySelector(SEL.QTY_INPUT);
      const pe = p.querySelector('.js-product-price');
      if (!inp || !pe) return;

      if (p.dataset._wsInited === '1') {
        setupWholesaleUI(p);
        return;
      }
      p.dataset._wsInited = '1';

      p.classList.add('wholesale-product');

      if (zeroStartQuant) {
        inp.setAttribute('min', '0');
      } else {
        inp.setAttribute('min', String(getStep(p)));
      }

      const pw0 = p.querySelector(SEL.PRICE_WRAPPER);
      if (pw0) pw0.removeAttribute('data-total-label');

      [0, 300].forEach(t => setTimeout(() => applyInitialQty(p), t));

      if (!inp._blurAttached) {
        inp.addEventListener('blur', handleBlur);
        inp._blurAttached = true;
      }

      setupWholesaleUI(p);
    }

    function runInit() {
      document.querySelectorAll('.js-product').forEach(initProduct);
    }

  
    let moTimer;
    const mo = new MutationObserver(() => {
      clearTimeout(moTimer);
      moTimer = setTimeout(runInit, 100);
    });

    function observeTargets() {
      const targets = document.querySelectorAll(
        '.js-store, .js-catalog, .js-product-single, .t-popup, .js-store-grid-cont, .js-catalog-grid-cont'
      );
      if (!targets.length) {
        mo.observe(document.body, { childList: true, subtree: true });
        return;
      }
      targets.forEach(t => mo.observe(t, { childList: true, subtree: true }));
    }

    observeTargets();
    runInit();

   
    function restoreQtyAfterOrder(p, stepVal) {
      const inp = p.querySelector(SEL.QTY_INPUT);
      if (!inp) return;
      const restore = zeroStartQuant ? 0 : stepVal;
      inp.value = restore;
      inp.setAttribute('value', String(restore));
      cacheQty(p, restore);
      updateTotalLabel(p);
      updateOrderButtonState(p);
    }

    function scheduleRestore(p, stepVal) {
      restoreQtyAfterOrder(p, stepVal);
      [100, 300, 700, 1500].forEach(t => {
        setTimeout(() => restoreQtyAfterOrder(p, stepVal), t);
      });
    }

 
    function findCartProductsByUid(uid) {
      if (!window.tcart?.products) return [];
      return Object.values(window.tcart.products).filter(p => p.gen_uid === uid);
    }

    function waitForCartProduct(uid, cb, maxAttempts = 20, interval = 100) {
      let attempts = 0;
      const t = setInterval(() => {
        attempts++;
        if (findCartProductsByUid(uid).length) {
          clearInterval(t);
          cb();
          return;
        }
        if (attempts >= maxAttempts) clearInterval(t);
      }, interval);
    }

    function getCartInv(prod) {
      const a = Number(prod.inv);
      if (a > 0) return a;
      const b = Number(prod._wsInv);
      return b > 0 ? b : Infinity;
    }

    function getCartStep(prod) {
      const opt = (prod.options || []).find(o => o.option === sizeLabel);
      let step = (opt && getFirstNumber(opt.variant)) || 1;
      const inv = getCartInv(prod);
      if (isFinite(inv) && inv < step) step = 1;
      return step;
    }

    function getCartMax(prod, step) {
      const inv = getCartInv(prod);
      return isFinite(inv) ? Math.floor(inv / step) * step : Infinity;
    }

    
    (function cleanupOldCartField() {
      const clean = () => {
        Object.values(window.tcart?.products || {}).forEach(p => {
          if ('wholesale_option' in p) delete p.wholesale_option;
        });
      };
      if (typeof t_onReady === 'function') {
        t_onReady(() => {
          setTimeout(clean, 500);
          setTimeout(clean, 1500);
        });
      } else {
        setTimeout(clean, 500);
        setTimeout(clean, 1500);
      }
    })();


    document.addEventListener('click', function (ev) {
      const plus = ev.target.closest(SEL.QTY_PLUS);
      if (plus) {
        const p = plus.closest('.js-product');
        if (!p) return;
        p.dataset._userTouched = '1';
        ev.stopImmediatePropagation();
        ev.preventDefault();
        const step = getStep(p);
        const max = getMaxQty(p);
        const inp = plus.closest(SEL.QTY_WRAPPER)?.querySelector(SEL.QTY_INPUT);
        if (inp) {
          let v = parseInt(inp.value, 10) || 0;
          let n = (v < step) ? step : v + step;
          if (n > max) n = Math.max(v, 0);
          if (!zeroStartQuant) n = Math.max(step, n);
          setQty(p, n);
        }
        return;
      }

      const minus = ev.target.closest(SEL.QTY_MINUS);
      if (minus) {
        const p = minus.closest('.js-product');
        if (!p) return;
        p.dataset._userTouched = '1';
        ev.stopImmediatePropagation();
        ev.preventDefault();
        const step = getStep(p);
        const inp = minus.closest(SEL.QTY_WRAPPER)?.querySelector(SEL.QTY_INPUT);
        if (inp) {
          let v = parseInt(inp.value, 10) || 0;
          const minVal = zeroStartQuant ? 0 : step;
          v = Math.max(minVal, v - step);
          setQty(p, v);
        }
        return;
      }

      const orderLink = ev.target.closest('a[href^="#order"]');
      if (orderLink) {
        const p = orderLink.closest('.js-product');
        if (!p) return;
        const inp = p.querySelector(SEL.QTY_INPUT);
        if (!inp) return;
        const q = parseInt(inp.value, 10) || 0;
        const max = getMaxQty(p);
        if (q <= 0 || max <= 0) {
          ev.preventDefault();
          ev.stopImmediatePropagation();
          ev.stopPropagation();
        }
      }
    }, true);

    
    document.addEventListener('click', function (ev) {
      const link = ev.target.closest('a[href^="#order"]');
      if (!link) return;
      const p = link.closest('.js-product');
      if (!p) return;

      const inp = p.querySelector(SEL.QTY_INPUT);
      const savedQty = parseInt(p.dataset._qty, 10)
        || (inp ? parseInt(inp.value, 10) : 0)
        || 0;

      const wb = p.querySelector('.wholesale');
      if (!wb) return;
      const uid = p.dataset.productGenUid;
      if (!uid) return;
      const sel = wb.querySelector('select');
      if (!sel?.options.length) return;

      const stepVal = getStep(p);
      const invNow = getInv(p);
      const maxNow = getMaxQty(p);
      if (maxNow <= 0) return;

      waitForCartProduct(uid, () => {
        findCartProductsByUid(uid).forEach(prod => {
          delete prod.wholesale_option;

          let q = Math.max(
            stepVal,
            Math.ceil((prod.quantity || savedQty || stepVal) / stepVal) * stepVal
          );
          if (q > maxNow) q = maxNow;
          if (q <= 0) return;

          prod.quantity = q;
          if (isFinite(invNow) && invNow > 0) prod._wsInv = invNow;
          else delete prod._wsInv;

          prod.amount = typeof tcart__roundPrice === 'function'
            ? tcart__roundPrice(prod.price * q)
            : Math.round(prod.price * q * 100) / 100;
        });

        if (typeof tcart__saveLocalObj === 'function') tcart__saveLocalObj();
        if (typeof tcart__reDrawProducts === 'function') tcart__reDrawProducts();
        if (typeof tcart__reDrawTotal === 'function') tcart__reDrawTotal();
        if (typeof tcart__updateTotalProductsinCartObj === 'function') tcart__updateTotalProductsinCartObj();

        scheduleRestore(p, stepVal);
      });
    }, false);

   
    window.addEventListener('click', function (ev) {
      const btn = ev.target.closest('.t706__product-plus, .t706__product-minus');
      if (!btn) return;

      const idx = btn.closest('.t706__product')?.dataset.cartProductI;
      const prod = window.tcart?.products?.[idx];
      if (!prod) return;

      const step = getCartStep(prod);
      const max = getCartMax(prod, step);
      if (step <= 1 && !isFinite(max)) return;

      ev.preventDefault();
      ev.stopImmediatePropagation();

      const isPlus = btn.classList.contains('t706__product-plus');
      const q = prod.quantity || 0;

      let n;
      if (isPlus) {
        n = (Math.floor(q / step) + 1) * step;
        if (n > max) return;
      } else {
        n = Math.max(step, (Math.ceil(q / step) - 1) * step);
        if (n > max) n = max;
      }
      if (n === q) return;

      prod.quantity = n;
      prod.amount = typeof tcart__roundPrice === 'function'
        ? tcart__roundPrice(prod.price * n)
        : Math.round(prod.price * n * 100) / 100;

      if (typeof tcart__saveLocalObj === 'function') tcart__saveLocalObj();
      if (typeof tcart__reDrawProducts === 'function') tcart__reDrawProducts();
      if (typeof tcart__reDrawTotal === 'function') tcart__reDrawTotal();
      if (typeof tcart__updateTotalProductsinCartObj === 'function') tcart__updateTotalProductsinCartObj();
    }, true);

   
    document.addEventListener('focusout', e => {
      const inp = e.target.closest('.t706__product-quantity-inp');
      if (!inp) return;
      const idx = inp.closest('.t706__product')?.dataset.cartProductI;
      if (idx === undefined) return;
      setTimeout(() => {
        const prod = window.tcart?.products?.[idx];
        if (!prod) return;
        const step = getCartStep(prod);
        const max = getCartMax(prod, step);
        if (step <= 1 && !isFinite(max)) return;

        let v = parseInt(inp.value, 10);
        if (isNaN(v) || v <= 0) v = step;
        else v = Math.max(step, Math.round(v / step) * step);
        if (v > max) v = max;

        prod.quantity = v;
        prod.amount = typeof tcart__roundPrice === 'function'
          ? tcart__roundPrice(prod.price * v)
          : Math.round(prod.price * v * 100) / 100;
        inp.value = v;
        if (typeof tcart__saveLocalObj === 'function') tcart__saveLocalObj();
        if (typeof tcart__reDrawProducts === 'function') tcart__reDrawProducts();
        if (typeof tcart__reDrawTotal === 'function') tcart__reDrawTotal();
      }, 100);
    }, true);

 
    document.addEventListener('input', e => {
      if (e.target.matches(SEL.QTY_INPUT)) {
        const p = e.target.closest('.js-product');
        if (p) {
          p.dataset._userTouched = '1';
          cacheQty(p, e.target.value);
        }
      }
    });

  });
})();
</script>




<style>
.t706__cartwin-content {
    max-width: 800px;
}


.js-product.wholesale-product .js-store-price-wrapper:after, 
.js-product.wholesale-product .js-catalog-price-wrapper:after
{
    content: "Всего: " attr(data-total-label);
    display: block;
    font-size: 14px;
    width: 100%;
}


.js-product.wholesale-product .js-store-price-wrapper:not([data-total-label]):after,
.js-product.wholesale-product .js-store-price-wrapper[data-total-label=""]:after,
.js-product.wholesale-product .js-catalog-price-wrapper:not([data-total-label]):after,
.js-product.wholesale-product .js-catalog-price-wrapper[data-total-label=""]:after
{
    display: none;
    content: none;
}



.js-product a[href^="#order"].ws-disabled {
    pointer-events: none;
    cursor: not-allowed;
}



.js-product a[href^="#order"].ws-disabled.t-btn {
    opacity: 0.4;
    filter: grayscale(0.3);
}


.wholesale-title {
    font-weight: 400 !important;
}

.wholesale-wrapper.t-text {
    padding-left: 0 !important;
}


.wholesale-wrapper {
    font-size: 15px;
    opacity: 0.4;
    padding-left: 0;
}
.wholesale-title {
    font-weight: 400;
}
.js-product-option.wholesale select[disabled] {
    pointer-events: none;
    opacity: 0.5;
}
.wholesale .js-product-option-name,
.wholesale .t-product__option-variants
{
    display: none;
}
.js-product-controls-wrapper.t-store__card__prod-controls-wrapper,
.js-product-controls-wrapper.t-catalog__card__prod-controls-wrapper
{
    padding-top: 10px;
}

.t-product__option.js-product-option.wholesale {
    margin-top: 0;
}

</style>
Made on
Tilda