PageSourceSearch

https://gdekomfort.ru/templates/komfort/resource/js/rscomponent/cart.js

js gdekomfort.ru collected 2026-10-05 02:26:15 UTC 25,829 bytes, 693 lines download raw bytes

1/**
2 * Компонент инициализирует работу кнопок "В корзину", а также самой корзины
3 */
4new class Cart extends RsJsCore.classes.component {
5
6    constructor(settings)
7    {
8        super();
9        let defaults = {
10            cartBlockMain: '#rs-cart',
11            cartBlock:'.rs-cart-block',
12            context: '[data-id]',
13
14            addToCart: '.rs-to-cart',
15            reserve: '.rs-reserve',
16            alreadyInCartClass: 'added',
17            addFromDialogClass: 'rs-buy',
18            alreadyInCartClassTimeout:5,
19
20            noShowModalCartClass: 'rs-no-modal-cart',
21
22            offerFormName: '[name="offer"]',
23            amountFormName: '[name="amount"]',
24            multiOfferFormName: '[name^="multioffers"]',
25            concomitantFormName: '[name^="concomitant"]',
26
27            cartTotalPrice: '.rs-cart-items-price',
28            cartTotalItems: '.rs-cart-items-count',
29            cartMobileTotalItems: '.rs-mobile-cart-items-count',
30            cartActiveClass: 'active',
31
32            checkoutButton: '.rs-go-checkout',
33            checkoutButtonActiveClass: 'active',
34
35            cartAmountField: '.rs-amount',
36            cartPage: '#rs-cart-page',
37            cartForm: '#rs-cart-form',
38            cartItem: '.rs-cart-item',
39            cartPageInfo: '.rs-cart-page-info',
40            cartPageInfoText: '',
41            cartItemRemove: '.rs-remove',
42            cartItemRemoveConcomitant: '.rs-remove-concomitant',
43            cartItemOffer: '.rs-offer',
44            cartItemMultiOffer: '.rs-multioffer',
45            cartGoBackButton: '.rs-go-back',
46            cartItemHiddenOffers: '.rs-hidden-multioffer',
47            cartClean: '.rs-clean',
48            cartApplyCoupon: '.rs-apply-coupon',
49            productWrapper: '.rs-product-item',
50            inLoadingClass: 'in-loading',
51            cartConcomitantCheckbox: '.rs-concomitant-checkbox',
52
53            //Умные кнопки, превращающиеся в количество
54
55        };
56
57        this.settings = {...defaults, ...this.getExtendsSettings(), ...settings};
58    }
59
60    init() {
61        this.utils.on('click', this.settings.addToCart, event => this.addToCart(event));
62        this.utils.on('click', this.settings.reserve, event => this.reserve(event));
63        this.utils.on('click', this.settings.checkoutButton, event => this.checkout(event));
64
65        this.cartBlockMain = document.querySelector(this.settings.cartBlockMain);
66
67        let cartPage = document.querySelector(this.settings.cartPage);
68        if (cartPage) {
69            this.initCart(cartPage.parentNode);
70
71            // Сохраняем содержимое блока доставки и оплаты
72            const cartInfo = this.cartPage.querySelector(this.settings.cartPageInfo);
73
74            if (cartInfo) {
75                this.settings.cartPageInfoText = cartInfo.innerHTML;
76            }
77        }
78
79        //Умные кнопки В корзину с количеством
80        this.utils.on('click', this.settings.smartAmountBuyButton, event => this.smartAmountAdd(event));
81    }
82
83    /**
84     * Добавляет товар в корзину и открывает окно упрощенной корзины
85     *
86     * @param event
87     */
88    addToCart(event) {
89        let button = event.rsTarget;
90        let context = button.closest(this.settings.context);
91        this.changeStateToAdded(button);
92        let formData = this.getProductParams(context);
93
94        if (this.needShowSelectMultiofferDialog(button)) {
95            let url = button.dataset.selectMultiofferHref;
96            this.plugins.openDialog.show({
97                url: url
98            });
99        } else {
100            let url = button.dataset.href ? button.dataset.href : button.getAttribute('href');
101
102            let noShowModalCart = !this.cartBlockMain
103                || this.cartBlockMain.classList.contains(this.settings.noShowModalCartClass)
104                || button.classList.contains(this.settings.noShowModalCartClass);
105
106            this.requestToCart(url, formData, noShowModalCart);
107
108            // if (button.classList.contains(this.settings.addFromDialogClass) && noShowModalCart) {
109            //     this.plugins.modal.close();
110            // }
111        }
112    }
113
114    /**
115     * Вызывает окно выбора комплектации или окно резервирования товара
116     *
117     * @param event
118     */
119    reserve(event) {
120        let button = event.rsTarget;
121        let url;
122
123        if (this.needShowSelectMultiofferDialog(button)) {
124            url = button.dataset.selectMultiofferHref;
125        } else {
126            url = button.dataset.href;
127        }
128
129        if (url) {
130            this.plugins.openDialog.show({
131                url: url
132            });
133        }
134    }
135
136    /**
137     * Возвращает true, если по нажатию на данную кнопку в
138     * текущей ситуации нужно отобразить диалог выбора комплектаций
139     *
140     * @param button
141     */
142    needShowSelectMultiofferDialog(button)
143    {
144        if (!button.dataset.selectMultiofferHref) return false;
145        let showOffersInListThemeOption = button.closest('[data-sol]');
146        return !showOffersInListThemeOption || !window.matchMedia('(min-width: 992px)').matches;
147    }
148
149    /**
150     * Выполняет запрос за сервер
151     * @param url
152     * @param formData
153     * @param noShowModalCartClass
154     */
155    requestToCart(url, formData, noShowModalCart) {
156        this.utils.fetchJSON(url, {
157            method:'POST',
158            body: formData
159        }).then((response) => {
160            this.updateCartBlock(response);
161     
162            if (!noShowModalCart) {
163                this.plugins.modal.open(response.html, (event) => {
164                    this.initCart(event.target);
165                });
166            }
167        });
168    }
169
170    /**
171     * Инициализирует события внутри корзины
172     *
173     * @param context
174     */
175    initCart(context) {
176        console.log(context)
177
178        this.cartPage = context.querySelector(this.settings.cartPage);
179
180        if (this.cartPage) {
181            this.utils.on('change', this.settings.cartAmountField, event => this.refresh(), this.cartPage);
182            this.utils.on('click', this.settings.cartItemRemove, event => this.removeProduct(event), this.cartPage);
183            this.utils.on('click', this.settings.cartItemRemoveConcomitant, event => this.removeConcomitant(event), this.cartPage);
184            this.utils.on('change', this.settings.cartConcomitantCheckbox, () => this.refresh(), this.cartPage);
185            this.utils.on('click', this.settings.cartGoBackButton, () => this.goBack(), this.cartPage);
186            this.utils.on('change', this.settings.cartItemOffer, () => this.refresh(), this.cartPage);
187            this.utils.on('change', this.settings.cartItemMultiOffer, (event) => this.changeMultiOffer(event), this.cartPage);
188            this.utils.on('click', this.settings.cartClean, event => this.cleanCart(event), this.cartPage);
189            this.utils.on('click', this.settings.cartApplyCoupon, () => this.refresh(), this.cartPage);
190
191            let form = this.cartPage.querySelector(this.settings.cartForm);
192            if (form) {
193                form.addEventListener('submit', (event) => {
194                    clearTimeout(this.cartPage.changeTimer);
195                    this.refresh();
196                    event.preventDefault();
197                });
198            }
199        }
200    }
201
202    /**
203     *
204     * @param context
205     */
206    getCurrentValueMatrix(context) {
207        let multiofferValues = [];
208
209        context.querySelectorAll('[data-prop-title]').forEach(element => {
210            multiofferValues.push([element.dataset.propTitle, element.value]);
211        });
212
213        return multiofferValues;
214    }
215
216    changeMultiOffer(event) {
217        let context = event.target.closest(this.settings.cartItem);
218        let values = this.getCurrentValueMatrix(context);
219
220        let hiddenOffers = context.querySelectorAll(this.settings.cartItemHiddenOffers);
221        let offerInput = context.querySelector(this.settings.cartItemOffer);
222
223        let foundOfferInput;
224        hiddenOffers.forEach(element => {
225            if (element.dataset.info) {
226                let info = JSON.parse(element.dataset.info);
227                let counter = 0;
228                info.forEach(inputPair => {
229                    values.forEach(valuesPair => {
230                        if (inputPair[0] === valuesPair[0]
231                            && inputPair[1] === valuesPair[1]) counter++;
232                    });
233                });
234
235                if (counter === values.length) {
236                    foundOfferInput = element;
237                }
238            }
239        });
240
241        if (foundOfferInput){ //Если комплектация найдена
242            offerInput.value = foundOfferInput.value;
243        } else { //Если не найдена комплектация, выберем нулевую
244            offerInput.value = 0;
245        }
246
247        this.refresh();
248    }
249
250    /**
251     * Удаляет один товар из корзины
252     *
253     * @param event
254     */
255    removeProduct(event) {
256        event.preventDefault();
257        if (!this.isLoading()) {
258            let removeButton = event.rsTarget;
259            let cartItem = removeButton.closest(this.settings.cartItem);
260            if (cartItem) {
261                cartItem.style.opacity = 0.5;
262                let other = cartItem.parentNode.querySelectorAll('[data-id="' + cartItem.dataset.productId + '"]');
263
264                if (!other.length) {
265                    document.querySelectorAll(
266                        this.settings.productWrapper + '[data-id="' + cartItem.dataset.productId + '"] ' + this.settings.addToCart)
267                        .forEach(element => {
268                            element.classList.remove(this.settings.alreadyInCartClass);
269                        });
270                }
271            }
272
273            this.refresh(removeButton.getAttribute('href'));
274        }
275    }
276
277    /**
278     * Обновляет корзину
279     *
280     * @param url
281     * @param callback
282     */
283    refresh(url, callback) {
284
285        let cartForm = this.cartPage.querySelector(this.settings.cartForm);
286        let formData = new FormData(cartForm);
287
288        if (!url) {
289            url = cartForm.getAttribute('action');
290        }
291
292        cartForm.querySelectorAll('input, select, button').forEach(element => {
293            element.disabled = true;
294        });
295
296        this.showLoading();
297        this.utils.fetchJSON(url, {
298            method:'POST',
299            body: formData
300        }).then(response => {
301            if (response.redirect) {
302                location.href = response.redirect;
303            }
304
305            if (response.cart.items_count === 0 && this.plugins.modal.isOpen()) {
306                this.plugins.modal.close();
307            } else {
308                let tmpDiv = document.createElement('div');
309                tmpDiv.innerHTML = response.html;
310                let element = tmpDiv.querySelector(this.settings.cartPage);
311                if (element) {
312                    this.cartPage.insertAdjacentElement('afterend', element);
313                    let parent = this.cartPage.parentNode;
314                    this.cartPage.remove();
315                    this.initCart(parent);
316                    parent.dispatchEvent(new CustomEvent('new-content', {bubbles: true}));
317                    parent.dispatchEvent(new CustomEvent('cart.change', {"bubbles": true, "cancelable": true}));
318                }
319            }
320            this.updateCartBlock(response);
321            if (callback) callback(response);
322        });
323    }
324
325    /**
326     * Обновляет параметры
327     *
328     * @param response
329     */
330    updateCartBlock(response) {
331        if (response) {
332            global.cartProducts = response.cart.session_cart_products;
333            document.querySelectorAll(this.settings.cartBlock).forEach((cartBlock) => {
334
335                let totalItems = cartBlock.querySelector(this.settings.cartTotalItems);
336                let mobileTotalItems = document.querySelector(this.settings.cartMobileTotalItems);
337                let totalPrice = cartBlock.querySelector(this.settings.cartTotalPrice);
338                let checkoutButton = cartBlock.querySelector(this.settings.checkoutButton);
339
340                totalItems && (totalItems.innerText = response.cart.items_count);
341                mobileTotalItems && (mobileTotalItems.innerText = response.cart.items_count);
342                totalPrice && (totalPrice.innerText = response.cart.total_price);
343
344                if (checkoutButton) {
345                    if (response.cart.can_checkout && parseFloat(response.cart.items_count) > 0) {
346                        checkoutButton.classList.add(this.settings.checkoutButtonActiveClass);
347                    } else {
348                        checkoutButton.classList.remove(this.settings.checkoutButtonActiveClass);
349                    }
350                }
351
352                if (parseFloat(response.cart.items_count) > 0) {
353                    cartBlock.classList.add(this.settings.cartActiveClass);
354                } else {
355                    cartBlock.classList.remove(this.settings.cartActiveClass);
356                }
357                cartBlock.dispatchEvent(new CustomEvent('cartblock.update', {"bubbles": true, "cancelable": true}));
358
359                if (this.cartPage) {
360                    const cartInfo = this.cartPage.querySelector(this.settings.cartPageInfo);
361
362                    if (cartInfo) {
363                        cartInfo.innerHTML = this.settings.cartPageInfoText;
364                    }
365
366                    const productSwipers = document.querySelectorAll('.swiper-products');
367
368                    if (productSwipers.length > 0)
369                        RsJsCore.components.productsSlider.initSwipers();
370                }
371            });
372        }
373    }
374
375    /**
376     * Показ загрузки
377     */
378    showLoading() {
379        this.cartPage.classList.add(this.settings.inLoadingClass);
380    }
381
382    /**
383     * Возвращает true, если в настоящее время идет загрузка
384     *
385     * @returns {boolean}
386     */
387    isLoading() {
388        return this.cartPage.classList.contains(this.settings.inLoadingClass);
389    }
390
391    /**
392     * Собирает параметры товара, который будет добавлен в корзин
392у
393     *
394     * @param context
395     */
396    getProductParams(context) {
397        let data = new FormData();
398        if (context) {
399            let radioInput = context.querySelector(this.settings.offerFormName + ':checked');
400            let hiddenInput = context.querySelector(this.settings.offerFormName);
401            let offerId = radioInput ? radioInput.value : (hiddenInput ? hiddenInput.value : 0);
402
403            //Комплектация
404            if (offerId) {
405                data.append('offer', offerId);
406            }
407
408            //Количество
409            let amountElement = context.querySelector(this.settings.amountFormName);
410            let amount = amountElement && amountElement.value;
411            if (amount) {
412                data.append('amount', amount);
413            }
414
415            //Многомерные комплектации
416            context.querySelectorAll(this.settings.multiOfferFormName + ':checked').forEach(element => {
417                data.append(element.getAttribute('name'), element.value);
418            });
419
420            //Сопутствующие товары
421            context.querySelectorAll(this.settings.concomitantFormName + ':checked').forEach(element => {
422                data.append(element.getAttribute('name'), element.value);
423            });
424        }
425
426        data.append('floatCart', 1); //Сообщаем, что возвращать надо упрощенную корзину
427
428        return data;
429    }
430
431    /**
432     * Изменяет надпись на кнопке, при добавлении в корзину
433     *
434     * @param button
435     */
436    changeStateToAdded(button) {
437        if (button.timeoutText) {
438            clearTimeout(button.timeoutText);
439        }
440
441        button.classList.add(this.settings.alreadyInCartClass);
442        if (!button.dataset.storedText && button.dataset.addText) {
443            button.dataset.storedText = button.innerHTML;
444            button.innerHTML = button.dataset.addText;
445        }
446
447        if (this.settings.alreadyInCartClassTimeout) {
448            button.timeoutText = setTimeout(() => {
449                button.classList.remove(this.settings.alreadyInCartClass);
450                if (button.dataset.storedText) {
451                    button.innerHTML = button.dataset.storedText;
452                    delete button.dataset.storedText;
453                }
454            }, this.settings.alreadyInCartClassTimeout * 1000);
455        }
456    }
457
458    cleanCart(event) {
459        event.preventDefault();
460        document.querySelectorAll(this.settings.productWrapper + '[data-id] ' + this.settings.addToCart)
461            .forEach((element) => {
462                element.classList.remove(this.settings.alreadyInCartClass)
463            });
464
465        this.refresh(event.rsTarget.getAttribute('href'));
466    }
467
468    /**
469     * Возвращает пользователя назад к покупкам
470     */
471    goBack() {
472        history.back();
473    }
474
475    /**
476     * Проверяет корзину на ошибки и если их нет, то перенаправляет
477     * пользователя на оформление заказа
478     * @param event
479     */
480    checkout(event) {
481        if (this.cartPage) {
482            let cartForm = this.cartPage.querySelector(this.settings.cartForm);
483            let url = cartForm.getAttribute('action');
484            let checkoutParam = (url.indexOf('?') > -1 ? '&' : '?') + 'checkout=1';
485            this.refresh(url + checkoutParam);
486            event.preventDefault();
487        }
488    }
489
490    onDocumentReady() {
491        this.init();
492    }
493
494    onContentReady() {
495        //Инициализируем умные кнопки добавления товара в корзину
496        SmartAmount.init();
497    }
498};
499
500
501
502/**
503 * ====================================================
504 * Класс описывает работу одной умной кнопки В корзину
505 */
506class SmartAmount
507{
508    constructor(element, settings) {
509
510        let defaults = {
511            smartAmount: '.rs-sa',
512            smartAmountActiveClass: 'item-product-cart-action_amount',
513            smartAmountBuyButton: '.rs-to-cart',
514            smartAmountIncButton: '.rs-sa-inc',
515            smartAmountDecButton: '.rs-sa-dec',
516            smartAmountInput: '.rs-sa-input',
517            productContext: '[data-id]',
518            delayChangeAmount: 700
519        };
520
521        this.settings = {...settings, ...defaults};
522        this.smartButton = element;
523
524        this.smartButton.querySelector(this.settings.smartAmountBuyButton)
525            .addEventListener('click', event => this.addProduct(event));
526
527        this.smartButton.querySelector(this.settings.smartAmountIncButton)
528            .addEventListener('click', event => this.incButton(event));
529
530        this.smartButton.querySelector(this.settings.smartAmountDecButton)
531            .addEventListener('click', event => this.decButton(event));
532
533        this.amountInput = this.smartButton.querySelector(this.settings.smartAmountInput);
534        this.amountInput.addEventListener('keyup', event => this.changeAmount(event));
535        this.amountInput.addEventListener('blur', event => this.blur(event));
536        this.amountParams = JSON.parse(this.smartButton.dataset.amountParams);
537
538        document.addEventListener('cart.removeProduct', event => this.onRemoveProduct(event));
539
540        this.restoreFromCache();
541    }
542
543    restoreFromCache() {
544        let productId = this.smartButton.closest(this.settings.productContext).dataset.id;
545        if (global.cartProducts) {
546            let numList = global.cartProducts[productId];
547            if (numList) {
548                let total = 0;
549                for (let num in numList) {
550                    total = total + numList[num];
551                }
552                this.amountInput.value = total;
553                this.smartButton.classList.add(this.settings.smartAmountActiveClass);
554            } else {
555                this.amountInput.value = 0;
556                this.smartButton.classList.remove(this.settings.smartAmountActiveClass);
557            }
558        }
559    }
560
561    onRemoveProduct(event) {
562        let productId = this.smartButton.closest(this.settings.productContext).dataset.id;
563        if (event.detail.productId === productId) {
564            this.amountInput.value = 0;
565            this.amountInput.dispatchEvent(new CustomEvent('keyup', {
566                bubbles: true,
567                detail: {
568                    noRefreshCart: true
569                }
570            }));
571        }
572    }
573
574    /**
575     * Добавляет товар в корзину, переключает состояние кнопки на количество
576     */
577    addProduct() {
578        this.amountInput.value = this.amountParams.amountAddToCart;
579        this.smartButton.classList.add(this.settings.smartAmountActiveClass);
580        this.smartButton.dispatchEvent(new CustomEvent('add-product', {bubbles: true}));
581    }
582
583    /**
584     * Увеличивает количество товара в корзине
585     */
586    incButton() {
587        let oldValue = parseFloat(this.amountInput.value);
588        let newValue = Math.round((oldValue + this.amountParams.amountStep) * 1000) / 1000;
589        let breakpoint = parseFloat(this.amountParams.amountBreakPoint);
590
591        if (newValue < this.amountParams.minAmount) {
592            newValue = this.amountParams.minAmount;
593        }
594        if (oldValue < breakpoint && newValue > breakpoint) {
595            newValue = breakpoint;
596        }
597        if (this.amountParams.maxAmount !== null && newValue > this.amountParams.maxAmount) {
598            newValue = this.amountParams.maxAmount;
599            this.smartButton.dispatchEvent(new CustomEvent('max-limit', {bubbles:true}));
600        } else {
601            this.smartButton.dispatchEvent(new CustomEvent('increase-amount', {bubbles:true}));
602        }
603        this.amountInput.value = newValue;
604        this.amountInput.dispatchEvent(new Event('keyup', {bubbles: true}));
605    }
606
607    /**
608     * Уменьшает количество товара в корзине
609     */
610    decButton() {
611
612        let oldValue = parseFloat(this.amountInput.value);
613        let newValue = Math.round((oldValue - this.amountParams.amountStep) * 1000) / 1000;
614        let breakpoint = parseFloat(this.amountParams.amountBreakPoint);
615
616        if (newValue < this.amountParams.minAmount) {
617            newValue = 0;
618        }
619
620        if (oldValue > breakpoint && newValue < breakpoint) {
621            newValue = breakpoint;
622        }
623
624        if (newValue != 0 || !this.amountParams.forbidRemoveProducts) {
625            this.smartButton.dispatchEvent(new CustomEvent('decrease-amount', {bubbles:true}));
626            this.amountInput.value = newValue;
627            this.amountInput.dispatchEvent(new Event('keyup', {bubbles: true}));
628        }
629
630        return false;
631    }
632
633    /**
634     * Изменяет количество товара на введенное вручную
635     */
636    changeAmount(event) {
637        clearTimeout(this.changeTimeout);
638        this.changeTimeout = setTimeout(() => {
639            let noChangesKeycodes = [16, 17, 18, 35, 36, 37, 39];
640            if (noChangesKeycodes.includes(event.keyCode)) {
641                return false;
642            }
643
644            let amount = this.amountInput.value;
645
646            if (amount === '') {
647                return false;
648            }
649
650            if (this.amountParams.maxAmount !== null && amount > parseFloat(this.amountParams.maxAmount)) {
651                amount = this.amountParams.maxAmount;
652                this.amountInput.value = amount;
653                this.smartButton.dispatchEvent(new CustomEvent('max-limit', {bubbles: true}));
654            }
655
656            if (amount == 0) {
657                this.smartButton.classList.remove(this.settings.smartAmountActiveClass);
658                this.smartButton.dispatchEvent(new CustomEvent('remove-product', {bubbles: true}));
659            }
660
661            if (!event.detail || !event.detail.noRefreshCart) {
662                let formData = new FormData();
663                formData.append('id', this.smartButton.closest(this.settings.productContext).dataset.id);
664                formData.append('amount', amount);
665
666                RsJsCore.utils.fetchJSON(this.smartButton.dataset.url, {
667                    method: 'post',
668                    body: formData
669                }).then(response => {
670                    if (response.success) {
671                        // Обновляем корзину на экране
672                        RsJsCore.components.cart.updateCartBlock(response);
673                    }
674                });
675            }
676        }, this.settings.delayChangeAmount);
677    }
678
679    blur() {
680        if (this.amountInput.value === '') {
681            this.amountInput.value = 0;
682            this.amountInput.dispatchEvent(new Event('keyup', {bubbles: true}));
683        }
684    }
685
686    static init(selector) {
687        document.querySelectorAll(selector ? selector : '.rs-sa').forEach(element => {
688            if (!element.smartAmount) {
689                element.smartAmount = new SmartAmount(element);
690            }
691        });
692    }
693};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.