1С Битрикс · 10 мин чтения

Битрикс количество увеличивается на 2 в корзине: двойной обработчик и шаг

Жалоба звучит одинаково: битрикс количество увеличивается на 2 при одном клике по плюсу, а минус при этом срабатывает нормально или тоже отнимает сразу двойку. За этим стоят две разные причины, и лечатся они по-разному. Либо клик обрабатывается дважды (две подписки на одну кнопку), либо у товара задан шаг 2, и обработчик честно прибавляет его. Ниже я показываю, как за пять минут понять, какой из случаев ваш, и как исправить каждый без правки ядра.

Двойной обработчик или шаг: как отличить одно от другого

Первое, что я делаю на таком сайте, это не лезу в код, а проверяю поведение руками. Три простых теста дают почти однозначный ответ.

  • Вводим количество вручную в поле: набираем 5 и уходим из поля. Если после пересчёта остаётся 5, поле и сервер работают нормально, проблема в кнопках.
  • Нажимаем плюс один раз и смотрим, какие числа получаются на серии кликов: 1, 3, 5, 7 или 2, 4, 6. Если старт с 1 и дальше нечётные числа, шаг 2 на кнопке. Если сразу 2, возможно, срабатывает дубль при первом клике или у товара коэффициент 2.
  • Открываем карточку того же товара в другом шаблоне или на другой странице (каталог, детальная страница, корзина). Если дубль только в одном месте, виноват шаблон или скрипт этого места. Если везде, то смотрим данные товара.

Для быстрой сверки держу под рукой такую таблицу.

Что видно Вероятная причина Где искать
Всегда +2, любой товар, только на одной странице Обработчик клика навешен дважды Шаблон компонента, подключения скриптов, DevTools
+2 растёт до +3, +4 после обновления блока по AJAX Повторная инициализация без снятия старых обработчиков Код, который вызывается после перерисовки корзины или фильтра
+2 у одного или нескольких товаров, у остальных +1 Коэффициент единицы измерения у товара равен 2 Карточка товара в админке, таблица коэффициентов
Дубль только на телефоне События touch и click оба доходят до кнопки Свой код с touchstart или библиотека быстрых кликов
Дубль появился после копирования шаблона в /local В шаблоне остался старый скрипт плюс подключён новый template.php, script.js, component_epilog.php

Если у вас сработала вторая или третья строка, переходите сразу к разделам про шаг и про повторную инициализацию. Первый вариант разберём подробно, потому что он самый частый.

Как найти задвоенный обработчик клика

В Chrome открываем DevTools, выбираем кнопку плюс через Elements и в консоли пишем:

getEventListeners($0).click.length

$0 это выбранный в Elements узел, а getEventListeners работает только в консоли DevTools, в коде страницы её вызвать нельзя. Если на кнопке висят два слушателя клика, число будет 2. Нормальный магазин с одним скриптом показывает 1. Если на самой кнопке 1, а увеличение всё равно двойное, поднимаемся по родителям: слушатель может висеть на контейнере через делегирование, и тогда второй обработчик сидит выше. Проверяем $0.parentElement, потом дальше до document.

Чтобы понять, откуда пришёл каждый слушатель, в панели Sources ставим точку остановки на событие: раздел Event Listener Breakpoints, Mouse, click. После клика отладчик остановится в первом обработчике, дальше жмём продолжение и видим второй. По имени файла в стеке сразу понятно, чей это код: шаблона компонента, вашего скрипта в футере или стороннего виджета.

Быстрая проверка через счётчик

Если DevTools неудобны, временно добавляю в подозрительный обработчик строку лога и смотрю, сколько раз она печатается на один клик:

document.addEventListener('click', function (e) {
  if (e.target.closest('.basket-item-amount-btn-plus')) {
    console.log('plus click', e.timeStamp);
  }
}, true);

Селектор подставьте свой: у штатных шаблонов корзины и каталога классы кнопок отличаются, а у самописной вёрстки они свои. Третий аргумент true означает фазу перехвата, поэтому лог сработает раньше любых обработчиков и не зависит от того, остановил ли кто-то всплытие. Одна строка лога на клик значит, что событие приходит одно, а прибавляют дважды уже внутри двух разных функций. Две строки с одинаковым timeStamp значат, что дублируется сама подписка.

Откуда берётся второй обработчик в шаблоне

За несколько лет работы с Битриксом я видел одни и те же сценарии, и почти всегда это не баг ядра, а наложение кода.

Скрипт подключён дважды

Классика: шаблон компонента каталога уже содержит свой script.js с логикой кнопок количества, а разработчик добавил такую же логику в общий скрипт сайта (main.js, футер). Оба навешивают клик на одни и те же кнопки. Другой вариант: компонент включён на странице два раза (например, отдельный блок для мобильной версии), и скрипт шаблона инициализируется на одном и том же контейнере. Ещё один случай: после копирования шаблона в папку /local/templates/шаблон_сайта/components/ в нём остались ссылки на старый файл скрипта, а новый подключается автоматически рядом.

Повторная инициализация после AJAX

Корзина, фильтр и список товаров часто перерисовываются без перезагрузки страницы. Если вы навешиваете обработчик на элементы внутри такого блока функцией init(), и эта функция вызывается после каждого обновления, а сам блок при этом не заменяется целиком, обработчики накапливаются. После первого обновления плюс прибавляет 2, после второго 3. Если у вас именно такая лесенка, причина найдена.

Лечится делегированием с одной подпиской на документ или защитой от повторного запуска. Вот рабочая версия для собственной вёрстки с атрибутами data-qty-plus и data-qty-minus на кнопках:

(function () {
  if (window.qtyHandlerReady) return;
  window.qtyHandlerReady = true;

  document.addEventListener('click', function (e) {
    var btn = e.target.closest('[data-qty-plus], [data-qty-minus]');
    if (!btn) return;
    e.preventDefault();

    var wrap = btn.closest('[data-qty]');
    var input = wrap.querySelector('input');
    var step = parseFloat(input.dataset.step) || 1;
    var value = parseFloat(input.value) || 0;

    value += btn.hasAttribute('data-qty-plus') ? step : -step;
    input.value = Math.max(step, value);
    input.dispatchEvent(new Event('change', { bubbles: true }));
  });
})();

Обработчик один на весь документ, поэтому перерисовка блоков ему не мешает: новые кнопки подхватываются автоматически. Флаг в window страхует на случай, если сам файл подключили дважды. В jQuery то же самое делается связкой .off('click.qty').on('click.qty', '.селектор', fn): пространство имён qty позволяет снять только свой обработчик и не задеть чужие.

Этот код для самописной разметки. Если у вас штатный шаблон корзины или каталога, не переписывайте его логику целиком, а найдите, какой из двух скриптов лишний, и уберите именно его.

Кнопка внутри label и касания на телефоне

Два неочевидных случая. Первый: кнопка плюс или поле лежат внутри label. Клик по label браузер дублирует кликом по связанному элементу, и обработчик получает событие дважды. Помогает либо замена label на div, либо проверка e.target. Второй: самописный код слушает и touchstart, и click. На телефоне срабатывают оба, на десктопе только click, поэтому ошибку не видно при разработке. Оставляйте один тип события, click достаточно для всех устройств.

Бесплатный материал

🎁 Полезный скрипт в подарок

Подпишитесь на Telegram - пришлю готовый скрипт по этой теме.

Без спама. Отписка в 1 клик.

Шаг изменения количества: коэффициент единицы измерения у товара

Если лог показал один вызов, а количество всё равно растёт по 2, смотрим данные товара. В Битриксе шаг количества в каталоге задаёт коэффициент единицы измерения (ratio). Штатные шаблоны каталога и корзины берут его из данных товара и прибавляют именно это число. Для штучного товара должна быть единица, для товара, который продаётся парами или упаковками, может стоять 2, 6, 10. Если коэффициент 2 попал в карточку случайно, например при импорте или копировании товара, получаем ровно ваш симптом: плюс даёт +2, минус даёт ‑2, а вручную можно вписать только чётное число.

Проверить коэффициенты можно прямо из PHP, не открывая каждую карточку. Для этого подходит таблица MeasureRatioTable модуля catalog:

<?php
use Bitrix\Main\Loader;
use Bitrix\Catalog\MeasureRatioTable;

Loader::includeModule('catalog');

$rows = MeasureRatioTable::getList([
    'select' => ['PRODUCT_ID', 'RATIO', 'IS_DEFAULT'],
    'filter' => ['=PRODUCT_ID' => $productId],
])->fetchAll();

print_r($rows);

В выводе нас интересует поле RATIO. Если там 2 у товара, который продаётся поштучно, исправляем значение в карточке товара в админке (коэффициент единицы измерения). Выводить через print_r здесь безопасно, потому что fetchAll() возвращает обычные массивы, но объекты результата выборки Битрикса целиком печатать не стоит: внутри них лежит подключение к базе с паролем.

Чтобы найти все товары с нестандартным шагом, уберите фильтр по PRODUCT_ID и поставьте условие '!=RATIO' => 1 (в массиве PHP с =>). Список обычно получается короткий, и по нему сразу видно, где коэффициент поставлен намеренно, а где ошибочно. Массовую правку делаю через API модуля, а не прямым запросом к таблице, чтобы не оставить устаревший кэш.

Когда шаг 2 нужен, а баг всё равно есть

Бывает и обратная ситуация: товар действительно продаётся по 2 штуки, но вёрстка добавляет свою единицу. Например, шаблон читает шаг из data-step, а кастомный скрипт ещё раз прибавляет коэффициент. Тогда на каждом клике получается 2 + 2 или 1 + 2. Здесь снова помогает лог: смотрим, какое значение приходит в функцию, и убираем один из двух источников шага. Правило простое: шаг читается один раз, из данных товара, и применяется в одном месте.

Как исправить аккуратно, без правки ядра

Все правки делаю только в /local. В документации Битрикса прямо сказано, что при обработке папок приоритет всегда у /local перед /bitrix, поэтому шаблон компонента копируется в /local/templates/шаблон_сайта/components/namespace/название_компонента/название_шаблона, и дальше правится уже копия. Файлы в /bitrix/components/ после обновления ядра перезапишутся, и баг вернётся.

Мой порядок работы такой.

  1. Находим по стеку из DevTools, какой файл навешивает лишний обработчик.
  2. Если это скрипт из шаблона компонента, а ту же логику повторяет общий скрипт сайта, удаляем дубль из общего скрипта. Если общий скрипт свой, а штатный шаблон работает хорошо, убираем наш.
  3. Если обработчики накапливаются после AJAX, переносим подписку на делегирование на document, как в коде выше, или проверяем флаг инициализации у контейнера.
  4. Если причина в коэффициенте, правим данные товара, а не скрипт.
  5. Сбрасываем кэш компонента и объединённые файлы css и js, иначе браузер продолжит получать старый скрипт.

Про последний пункт стоит помнить особенно. После правки script.js шаблона кажется, что ничего не изменилось, хотя исправление просто не доехало до страницы. Если включено объединение скриптов, объединённый файл лежит в bitrix/cache/js/<сайт>/<шаблон>/, и его нужно пересоздать. Проверяйте не в обычном окне, а в режиме без кэша или в приватном.

Ещё одна ловушка касается ассетов компонента с включённым кэшем. Если скрипт подключается из template.php кэшируемого компонента, то при попадании в кэш этот файл не выполняется, и подключение срабатывает только на первом запросе после сброса. Такой скрипт может то появляться, то пропадать, а вместе с ним то появляться, то исчезать и дубль. Подключайте внешние скрипты из header.php или footer.php шаблона сайта либо из component_epilog.php: он выполняется и при попадании в кэш.

Если шаблон корзины сильно переделан и вы не хотите разбираться в чужом коде, можно заказать доработку и поддержку 1С-Битрикс: задача про двойной клик обычно укладывается в один-два часа работы.

Проверка после правки: что смотреть, чтобы баг не вернулся

После исправления проверяю не только счёт кликов, но и сценарии, на которых дубли обычно всплывают снова.

  • Серия из 10 кликов подряд по плюсу и минусу: числа должны идти с шагом из карточки товара без пропусков.
  • Ручной ввод в поле, потом клик по плюсу: результат должен считаться от введённого числа.
  • Обновление блока по AJAX (смена фильтра, пересчёт корзины, добавление товара) и повторная серия кликов: накопления обработчиков быть не должно.
  • Мобильная версия, реальное устройство или режим эмуляции касаний в DevTools.
  • Товар с коэффициентом больше единицы: шаг равен коэффициенту, а не коэффициенту плюс единица.
  • Оформление заказа: количество в корзине совпадает с количеством в итоговом заказе.

Последний пункт я не пропускаю никогда. Интерфейс может показывать 3 штуки, а на сервер уходит запрос с другим значением, потому что обработчик отправил два запроса подряд. В журнале запросов на вкладке Network это видно сразу: на один клик должен уходить один запрос пересчёта.

Как закрепить результат

После правки оставляю в шаблоне короткий комментарий о том, откуда берётся шаг и какой скрипт отвечает за кнопки, чтобы при следующем подключении виджета никто не добавил вторую подписку. На сайтах с частыми обновлениями каталога полезно раз в несколько месяцев прогонять выборку коэффициентов, о которой писал выше: ошибочная двойка после очередного импорта находится за минуту.

Условия и возможности 1С-Битрикс меняются от версии к версии, поэтому перед правкой шаблонов я сверяюсь с актуальной документацией разработчика и версией ядра на конкретном сайте.

Частые вопросы

Почему количество в корзине Битрикса растёт на 2 только после обновления страницы или AJAX-перерисовки?

Обычно после каждой перерисовки блока функция инициализации снова навешивает обработчик на те же кнопки, а старый не снимается. Поэтому после первого обновления плюс даёт +2, после второго +3. Решение: делегирование на document с одной подпиской либо проверка флага инициализации у контейнера.

Как понять, что шаг 2 задан в данных товара, а не в скрипте?

Введите количество вручную и попробуйте нечётное число, например 3. Если после пересчёта оно округляется до чётного, шаг задан коэффициентом единицы измерения у товара. Дополнительно проверьте таблицу коэффициентов запросом через MeasureRatioTable, как показано в статье.

Можно ли править скрипт прямо в /bitrix/components/?

Технически можно, но после обновления ядра правки перезапишутся, и дубль вернётся. Копию шаблона компонента кладите в /local/templates/шаблон_сайта/components/: там приоритет выше, и обновления её не затрагивают.

Исправил скрипт, но на сайте ничего не изменилось. Почему?

Скорее всего, браузер получает старый объединённый файл или отдаётся кэш компонента. Сбросьте кэш компонента и объединённые файлы в bitrix/cache/js/, затем проверьте страницу в приватном окне. Если скрипт подключается из template.php кэшируемого компонента, перенесите подключение в component_epilog.php или в шаблон сайта.

Есть задача?

Обсудим в мессенджере

Расскажите, что нужно сделать — отвечу в течение 4 часов в рабочее время. Первая консультация бесплатно.

Продолжая пользование настоящим сайтом Вы выражаете своё согласие на обработку Ваших персональных данных (файлов куки) с использованием Yandex.Metrika.
Понятно