Advertisement
codex23

Add-to-cart module

Mar 27th, 2025 (edited)
451
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. /**
  2.  * WooCommerce Ajax Add to Cart
  3.  *
  4.  * This module handles adding products to cart via AJAX using the WooCommerce Store API.
  5.  * It also updates the mini cart after a successful addition.
  6.  */
  7. class AddToCart {
  8.     constructor() {
  9.         this.cartButtonSelector = '.cartButton';
  10.         this.miniCartSelector = '.widget_shopping_cart_content';
  11.         this.miniCartItemSelector = '.cart_list .mini_cart_item';
  12.         this.cartCountSelector = '.mini_cart_counter .counter_span';
  13.         this.isRequestPending = false;
  14.    
  15.         this.initElements();
  16.         this.init();
  17.     }
  18.  
  19.     /**
  20.     * Initialize the module
  21.     */
  22.    
  23.     initElements() {
  24.         this.cartButtons = document.querySelectorAll('.cartButton');
  25.         this.cartContainer = document.querySelector(this.cartContainerSelector);
  26.         this.miniCartItems = document.querySelectorAll(this.miniCartItemSelector);
  27.     }  
  28.    
  29.     init() {
  30.         if (this.cartButtons.length > 0) {
  31.             this.attachEventListeners();
  32.         }
  33.     }
  34.  
  35.     /**
  36.     * Attach click event listeners to cart buttons
  37.     */
  38.     attachEventListeners() {
  39.         this.cartButtons.forEach(button => {
  40.             button.addEventListener('click', this.handleAddToCart.bind(this));
  41.         });
  42.     }
  43.  
  44.     /**
  45.     * Handle add to cart button click
  46.     *
  47.     * @param {Event} event - Click event
  48.     */
  49.     handleAddToCart(event) {
  50.         event.preventDefault();
  51.    
  52.         if (this.isRequestPending) {
  53.             return;
  54.         }
  55.  
  56.         const button = event.currentTarget;
  57.         const productInfo = this.getProductInfo(button);
  58.    
  59.         if (!productInfo) {
  60.             this.showNotification('Error', 'Product information not found', 'error');
  61.             return;
  62.         }
  63.  
  64.         this.addToCart(productInfo, button);
  65.     }
  66.  
  67.     /**
  68.     * Get product information from the hidden input
  69.     *
  70.     * @param {Element} button - The clicked cart button
  71.     * @returns {Object|null} - Product information or null if not found
  72.     */
  73.     getProductInfo(button) {
  74.         const productContainer = button.closest('.product-actions') || button.parentNode;
  75.         if (!productContainer) return null;
  76.    
  77.         const productInfoEl = productContainer.querySelector('.listingInfo');
  78.         if (!productInfoEl) return null;
  79.    
  80.         return {
  81.             id: productInfoEl.dataset.productId,
  82.             quantity: 1, // Default quantity
  83.             sku: productInfoEl.dataset.productSku,
  84.             title: productInfoEl.dataset.productTitle,
  85.             slug: productInfoEl.dataset.productSlug,
  86.             price: productInfoEl.dataset.productPrice,
  87.             type: productInfoEl.dataset.productType,
  88.         };
  89.     }
  90.  
  91.     /**
  92.     * Add product to cart via WooCommerce Store API
  93.     *
  94.     * @param {Object} product - Product information
  95.     * @param {Element} button - The clicked button element
  96.     */
  97.     async addToCart(product, button) {
  98.         console.log('this is addToCart', product);
  99.         this.isRequestPending = true;
  100.         this.setButtonLoading(button, true);
  101.    
  102.         // If it's a variable product, we would need variation_id too
  103.         if (product.type === 'variable') {
  104.             this.showNotification('Info', 'Please select product options on the product page', 'info');
  105.             this.setButtonLoading(button, false);
  106.             this.isRequestPending = false;
  107.             return;
  108.         }
  109.  
  110.         // WooCommerce Store API endpoint for adding to cart
  111.         const apiUrl = '/wp-json/wc/store/v1/cart/add-item';
  112.  
  113.         const requestData = {
  114.             id: parseInt(product.id, 10),
  115.             quantity: parseInt(product.quantity, 10) || 1
  116.         };
  117.  
  118.         try {
  119.             const response = await fetch(apiUrl, {
  120.                 method: 'POST',
  121.                 credentials: 'same-origin',
  122.                 headers: {
  123.                     'Content-Type': 'application/json',
  124.                     'Nonce': ajaxInfo.security.security_code || ''
  125.                 },
  126.                 body: JSON.stringify(requestData)
  127.             });
  128.  
  129.             if (!response.ok) {
  130.                 const errorData = await response.json().catch(() => ({}));
  131.                 throw new Error(errorData.message || `HTTP error! Status: ${response.status}`);
  132.             }
  133.  
  134.             const data = await response.json();
  135.             console.log('Add to cart response:', data);
  136.    
  137.             // Show success notification
  138.             this.showNotification('Success', `"${product.title || 'Product'}" has been added to your cart.`, 'success');
  139.    
  140.             // Update mini cart and cart count
  141.             await this.updateMiniCart();
  142.             this.updateCartCount(data.items_count || 0);
  143.    
  144.         } catch (error) {
  145.             console.error('Error adding to cart:', error);
  146.             this.showNotification('Error', 'Could not add item to cart. Please try again.', 'error');
  147.         } finally {
  148.             this.setButtonLoading(button, false);
  149.             this.isRequestPending = false;
  150.         }
  151.     }
  152.  
  153.   /**
  154.    * Update the mini cart contents
  155.    */
  156.   async updateMiniCart() {
  157.     console.log('this is updateMiniCart');
  158.     const miniCart = document.querySelector(this.miniCartSelector);
  159.     if (!miniCart) return;
  160.  
  161.     // Fetch updated mini cart HTML
  162.     await fetch('/wp-json/wc/store/v1/cart', {
  163.       method: 'GET',
  164.       credentials: 'same-origin',
  165.       headers: {
  166.         'Content-Type': 'application/json',
  167.         'Nonce': ajaxInfo.security.security_code || ''
  168.       }
  169.     })
  170.       .then(response => response.json())
  171.       .then(data => {
  172.         console.log('this is data from updateMiniCart: ', data);
  173.         // Update mini cart total
  174.         const totalEl = miniCart.querySelector('.woocommerce-mini-cart__total strong');
  175.         if (totalEl) {
  176.           totalEl.textContent = this.formatMoney(data.totals.total_price);
  177.         }
  178.  
  179.         // Update mini cart items
  180.         this.updateMiniCartItems(data.items);
  181.       })
  182.       .catch(error => {
  183.         console.error('Error updating mini cart:', error);
  184.       });
  185.   }
  186.  
  187.   /**
  188.    * Update items in the mini cart
  189.    *
  190.    * @param {Array} items - Cart items from the API
  191.    */
  192.   updateMiniCartItems(items) {
  193.     console.log('this is updateMiniCartItems');
  194.     const miniCartItemsList = document.querySelector('.cart-list');
  195.     if (!miniCartItemsList) return;
  196.  
  197.     // Clear current items
  198.     miniCartItemsList.innerHTML = '';
  199.  
  200.     if (items.length === 0) {
  201.       const emptyMessage = document.createElement('li');
  202.       emptyMessage.className = 'mini-cart-empty';
  203.       emptyMessage.textContent = 'Your cart is empty';
  204.       miniCartItemsList.appendChild(emptyMessage);
  205.       return;
  206.     }
  207.  
  208.     // Add new items
  209.     items.forEach(item => {
  210.       const itemElement = document.createElement('li');
  211.       itemElement.className = 'woocommerce-mini-cart-item';
  212.       itemElement.dataset.itemKey = item.key;
  213.  
  214.       itemElement.innerHTML = `
  215.         <div class="attachment-woocommerce_thumbnail">
  216.           <img src="${item.images[0]?.src || ''}" alt="${item.name}">
  217.         </div>
  218.         <div class="mini-cart-item-details">
  219.           <h4>${item.name}</h4>
  220.           <div class="mini-cart-item-price">
  221.             ${item.quantity} × ${this.formatMoney(item.prices.price)}
  222.           </div>
  223.         </div>
  224.         <a href="#" class="remove remove_from_cart_button" aria-label="Remove ${item.name} from cart" data-product_id="${item.id}" data-cart_item_key="${item.key}" data-product_sku="${item.sku}" data-success_message="'${item.name}' has been removed from your cart">×</a>
  225.       `;
  226.  
  227.       miniCartItemsList.appendChild(itemElement);
  228.     });
  229.  
  230.     // Add remove button listeners
  231.     this.attachRemoveItemListeners();
  232.   }
  233.  
  234.   /**
  235.    * Attach event listeners to remove buttons in mini cart
  236.    */
  237.     attachRemoveItemListeners() {
  238.       console.log('this is attachRemoveItemListeners');
  239.  
  240.       // Remove existing listeners to prevent conflicts
  241.       const removeButtons = document.querySelectorAll('.remove_from_cart_button');
  242.       removeButtons.forEach(button => {
  243.             // Clone node to remove all event listeners
  244.             const newButton = button.cloneNode(true);
  245.             button.parentNode.replaceChild(newButton, button);
  246.       });
  247.  
  248.       // Add fresh listeners
  249.       const freshButtons = document.querySelectorAll('.remove_from_cart_button');
  250.       freshButtons.forEach(button => {
  251.             button.addEventListener('click', (e) => {
  252.                 e.preventDefault(); // Prevent default link behavior
  253.                 const itemKey = e.currentTarget.dataset.cartItemKey;
  254.                 if (itemKey) {
  255.                     this.removeCartItem(itemKey, e.currentTarget);
  256.                 }
  257.             });
  258.       });
  259.     }
  260.  
  261.   /**
  262.    * Remove an item from the cart
  263.    *
  264.    * @param {string} itemKey - Cart item key
  265.    * @param {Element} button - Remove button element
  266.    */
  267.   removeCartItem(itemKey, button) {
  268.     console.log('this is removeCartItem');
  269.     const apiUrl = `/wp-json/wc/store/v1/cart/items/${itemKey}`;
  270.     // Fetch cart items
  271.     fetch(apiUrl, {
  272.       method: 'DELETE',
  273.       credentials: 'same-origin',
  274.       headers: {
  275.         'Content-Type': 'application/json',
  276.         'Nonce': ajaxInfo.security.security_code || ''
  277.       },
  278.       body: JSON.stringify({
  279.         key: itemKey
  280.       })
  281.     })
  282.     .then(response => {
  283.       console.log('removeCartItem response: ', response);
  284.       if (!response.ok) {
  285.         throw new Error(`HTTP error! Status: ${response.status}`);
  286.       }
  287.       return response.json();
  288.     })
  289.     .then(data => {
  290.         console.log('removeCartItem response: ', data);
  291.       // Update cart visually
  292.       const itemElement = button.closest('.woocommerce-mini-cart-item');
  293.       if (itemElement) {
  294.         itemElement.remove();
  295.       }
  296.      
  297.       // Update cart count
  298.       //this.updateCartCount(data.items_count || 0);
  299.      
  300.       // Update totals
  301.       const totalEl = document.querySelector('.woocommerce-mini-cart__total');
  302.       if (totalEl) {
  303.         totalEl.textContent = this.formatMoney(data.totals.total_price);
  304.       }
  305.      
  306.       // Show empty message if needed
  307.       if (data.items.length === 0) {
  308.         const miniCartItemsList = document.querySelector('.woocommerce-mini-cart');
  309.         if (miniCartItemsList) {
  310.           const emptyMessage = document.createElement('li');
  311.           emptyMessage.className = 'mini-cart-empty';
  312.           emptyMessage.textContent = 'Your cart is empty';
  313.           miniCartItemsList.appendChild(emptyMessage);
  314.         }
  315.       }
  316.     })
  317.     .catch(error => {
  318.       console.error('Error removing item:', error);
  319.       this.showNotification('Error', 'Could not remove item. Please try again.', 'error');
  320.     });
  321.   }
  322.  
  323.   /**
  324.    * Update the cart item count display
  325.    *
  326.    * @param {number} count - New cart count
  327.    */
  328.   updateCartCount(count) {
  329.     const cartCountElements = document.querySelectorAll(this.cartCountSelector);
  330.     cartCountElements.forEach(element => {
  331.       element.textContent = count.toString();
  332.      
  333.       // Add animation class
  334.       element.classList.add('cart-count-updated');
  335.      
  336.       // Remove animation class after animation completes
  337.       setTimeout(() => {
  338.         element.classList.remove('cart-count-updated');
  339.       }, 1000);
  340.     });
  341.   }
  342.  
  343.  
  344.   /**
  345.    * Set button to loading state
  346.    *
  347.    * @param {Element} button - Button element
  348.    * @param {boolean} isLoading - Whether button is in loading state
  349.    */
  350.   setButtonLoading(button, isLoading) {
  351.     if (isLoading) {
  352.       button.classList.add('loading');
  353.       button.setAttribute('disabled', 'disabled');
  354.      
  355.       // Store original text and set loading text
  356.       button.dataset.originalText = button.innerHTML;
  357.       button.innerHTML = '<span class="spinner">Adding...</span>';
  358.     } else {
  359.       button.classList.remove('loading');
  360.       button.removeAttribute('disabled');
  361.      
  362.       // Restore original text
  363.       if (button.dataset.originalText) {
  364.         button.innerHTML = button.dataset.originalText;
  365.       }
  366.     }
  367.   }
  368.  
  369.   /**
  370.    * Show a notification
  371.    *
  372.    * @param {string} title - Notification title
  373.    * @param {string} message - Notification message
  374.    * @param {string} type - Notification type (success, error, info)
  375.    */
  376.   showNotification(title, message, type = 'info') {
  377.     // Check if the site uses WooCommerce's built-in notices
  378.     const noticesContainer = document.querySelector('.woocommerce-notices-wrapper');
  379.    
  380.     if (noticesContainer) {
  381.       // Clear existing notices
  382.       noticesContainer.innerHTML = '';
  383.      
  384.       // Create new notice
  385.       const notice = document.createElement('div');
  386.       notice.className = `woocommerce-${type}`;
  387.       notice.innerHTML = message;
  388.      
  389.       noticesContainer.appendChild(notice);
  390.      
  391.       // Scroll to notice if not in viewport
  392.       if (!this.isElementInViewport(noticesContainer)) {
  393.         noticesContainer.scrollIntoView({ behavior: 'smooth' });
  394.       }
  395.     } else {
  396.       // Create a custom notification if WooCommerce notices wrapper doesn't exist
  397.       const notification = document.createElement('div');
  398.       notification.className = `ajax-notification ajax-notification-${type}`;
  399.      
  400.       notification.innerHTML = `
  401.         <div class="ajax-notification-content">
  402.           <h4>${title}</h4>
  403.           <p>${message}</p>
  404.         </div>
  405.         <button class="ajax-notification-close">&times;</button>
  406.       `;
  407.      
  408.       document.body.appendChild(notification);
  409.      
  410.       // Show with animation
  411.       setTimeout(() => {
  412.         notification.classList.add('ajax-notification-visible');
  413.       }, 10);
  414.      
  415.       // Add close button functionality
  416.       const closeBtn = notification.querySelector('.ajax-notification-close');
  417.       if (closeBtn) {
  418.         closeBtn.addEventListener('click', () => {
  419.           notification.classList.remove('ajax-notification-visible');
  420.           setTimeout(() => {
  421.             notification.remove();
  422.           }, 300);
  423.         });
  424.       }
  425.      
  426.       // Auto-remove after 5 seconds for success messages
  427.       if (type === 'success') {
  428.         setTimeout(() => {
  429.           notification.classList.remove('ajax-notification-visible');
  430.           setTimeout(() => {
  431.             notification.remove();
  432.           }, 300);
  433.         }, 5000);
  434.       }
  435.     }
  436.   }
  437.  
  438.   /**
  439.    * Check if an element is in the viewport
  440.    *
  441.    * @param {Element} element - Element to check
  442.    * @returns {boolean} - Whether element is in viewport
  443.    */
  444.   isElementInViewport(element) {
  445.     const rect = element.getBoundingClientRect();
  446.    
  447.     return (
  448.       rect.top >= 0 &&
  449.       rect.left >= 0 &&
  450.       rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
  451.       rect.right <= (window.innerWidth || document.documentElement.clientWidth)
  452.     );
  453.   }
  454.  
  455.   /**
  456.    * Format money value
  457.    *
  458.    * @param {number|string} amount - Amount to format
  459.    * @returns {string} - Formatted amount
  460.    */
  461.   formatMoney(amount) {
  462.     // Use the site's currency symbol and format from WooCommerce if available
  463.     if (window.woocommerce_price_format && window.woocommerce-Price-currencySymbol) {
  464.       const formatted = parseFloat(amount).toFixed(2);
  465.       const formattedWithSymbol = window.woocommerce_price_format
  466.         .replace('%1$s', window.woocommerce_currency_symbol)
  467.         .replace('%2$s', formatted);
  468.      
  469.       return formattedWithSymbol;
  470.     }
  471.    
  472.     // Fallback formatting
  473.     return '$' + parseFloat(amount).toFixed(2);
  474.   }
  475. }
  476.  
  477. // Export for use in other modules
  478. export default AddToCart;
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement