/** * Frontend JavaScript for Composable Products * * @package WC_Composable_Product */ (function($) { 'use strict'; const ComposableProduct = { /** * Initialize */ init: function() { this.bindEvents(); }, /** * Bind events */ bindEvents: function() { const self = this; // Handle checkbox changes $(document).on('change', '.composable-product-checkbox', function() { self.handleCheckboxChange($(this)); }); // Handle add to cart button $(document).on('click', '.composable-add-to-cart', function(e) { e.preventDefault(); self.addToCart($(this)); }); }, /** * Handle checkbox change * * @param {jQuery} $checkbox Checkbox element */ handleCheckboxChange: function($checkbox) { const $container = $checkbox.closest('.wc-composable-product-selector'); const selectionLimit = parseInt($container.data('selection-limit')); const pricingMode = $container.data('pricing-mode'); const $checked = $container.find('.composable-product-checkbox:checked'); // Enforce selection limit if ($checked.length > selectionLimit) { $checkbox.prop('checked', false); this.showMessage($container, wcComposableProduct.i18n.max_items, 'error'); return; } // Update visual state $checkbox.closest('.composable-product-item').toggleClass('selected', $checkbox.is(':checked')); // Update total price if in sum mode if (pricingMode === 'sum') { this.updateTotalPrice($container); } // Clear messages this.clearMessages($container); }, /** * Format price using WooCommerce settings * * @param {number} price Price amount * @return {string} Formatted price HTML */ formatPrice: function(price) { if (typeof wcComposableProduct === 'undefined' || !wcComposableProduct.price_format) { return price.toFixed(2); } const format = wcComposableProduct.price_format; const decimals = parseInt(format.decimals, 10); const decimalSep = format.decimal_separator; const thousandSep = format.thousand_separator; // Format number let priceStr = price.toFixed(decimals); const parts = priceStr.split('.'); parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, thousandSep); priceStr = parts.join(decimalSep); // Apply price format (e.g., "%1$s%2$s" for symbol+price or "%2$s%1$s" for price+symbol) let formatted = format.price_format .replace('%1$s', '' + format.currency_symbol + '') .replace('%2$s', priceStr); return '' + formatted + ''; }, /** * Update total price * * @param {jQuery} $container Container element */ updateTotalPrice: function($container) { const $checked = $container.find('.composable-product-checkbox:checked'); let total = 0; $checked.each(function() { const price = parseFloat($(this).data('price')); if (!isNaN(price)) { total += price; } }); const formattedPrice = this.formatPrice(total); $container.find('.calculated-total').html(formattedPrice); }, /** * Add to cart * * @param {jQuery} $button Button element */ addToCart: function($button) { const $container = $button.closest('.wc-composable-product-selector'); const $checked = $container.find('.composable-product-checkbox:checked'); const productId = $button.data('product-id'); // Validate selection if ($checked.length === 0) { this.showMessage($container, wcComposableProduct.i18n.min_items, 'error'); return; } // Collect selected product IDs const selectedProducts = []; $checked.each(function() { selectedProducts.push($(this).val()); }); // Disable button $button.prop('disabled', true).addClass('loading'); // Add to cart via AJAX $.ajax({ url: wc_add_to_cart_params.wc_ajax_url.toString().replace('%%endpoint%%', 'add_to_cart'), type: 'POST', data: { product_id: productId, quantity: 1, composable_products: selectedProducts }, success: function(response) { if (response.error) { this.showMessage($container, response.error, 'error'); } else { // Trigger WooCommerce event $(document.body).trigger('added_to_cart', [response.fragments, response.cart_hash, $button]); // Show success message this.showMessage($container, 'Product added to cart!', 'success'); // Reset selection setTimeout(function() { $checked.prop('checked', false); $container.find('.composable-product-item').removeClass('selected'); this.updateTotalPrice($container); }.bind(this), 1500); } }.bind(this), error: function() { this.showMessage($container, 'An error occurred. Please try again.', 'error'); }.bind(this), complete: function() { $button.prop('disabled', false).removeClass('loading'); } }); }, /** * Show message * * @param {jQuery} $container Container element * @param {string} message Message text * @param {string} type Message type (error, success) */ showMessage: function($container, message, type) { const $messages = $container.find('.composable-messages'); const $message = $('
') .addClass('composable-message') .addClass('composable-message-' + type) .text(message); $messages.empty().append($message); // Auto-hide after 5 seconds setTimeout(function() { $message.fadeOut(function() { $(this).remove(); }); }, 5000); }, /** * Clear messages * * @param {jQuery} $container Container element */ clearMessages: function($container) { $container.find('.composable-messages').empty(); } }; // Initialize on document ready $(document).ready(function() { ComposableProduct.init(); }); })(jQuery);