(function($) { 'use strict'; // Plugin configuration and state const WSDM = { config: { shipping_format: 'radio', is_blocks_enabled: false }, cache: { observers: new WeakMap(), timeouts: new Map(), convertedPackages: new Set(), // Track converted packages to prevent duplicates conversionInProgress: false, // Prevent multiple simultaneous conversions selectChangeInProgress: false // Track when our select changes are happening }, constants: { CONVERSION_DELAY: 100, SYNC_DEBOUNCE_DELAY: 50, BLOCK_SELECTORS: { CART: '.wp-block-woocommerce-cart .wc-block-components-radio-control', CHECKOUT: '.wp-block-woocommerce-checkout #shipping-option .wc-block-components-radio-control' }, CLASSES: { CONVERTED: 'wsdm-converted', SHIPPING_SELECT: 'wsdm-shipping-select', BLOCK_SELECT: 'wsdm-block-shipping-select', PACKAGE_CONVERTED: 'wsdm-package-converted' // Mark converted packages } } }; // Initialize plugin when DOM is ready $(document).ready(function() { // Enable debug mode temporarily to help diagnose the issue window.wsdm_debug = false; logDebug('DOM ready, initializing plugin'); initializePlugin(); bindEvents(); // Add manual trigger for testing window.wsdmForceConversion = function() { logDebug('Manual conversion triggered'); // Clear conversion cache before forcing WSDM.cache.convertedPackages.clear(); WSDM.cache.conversionInProgress = false; initializeShippingMethods(); }; }); /** * Initialize the plugin */ function initializePlugin() { loadConfiguration(); initializeShippingMethods(); } /** * Bind all event listeners */ function bindEvents() { // Classic cart/checkout events - handle WooCommerce HTML refreshes $('body').on('updated_cart_totals updated_checkout', debounce(function() { logDebug('Cart/checkout update event detected'); // If we had select dropdowns before but they're gone now, WooCommerce refreshed the HTML const existingSelects = $('.' + WSDM.constants.CLASSES.SHIPPING_SELECT).length; const hadConvertedPackages = WSDM.cache.convertedPackages.size > 0; const shippingMethods = $('.shipping_method').length; logDebug('Update check - existingSelects: ' + existingSelects + ', hadConverted: ' + hadConvertedPackages + ', shippingMethods: ' + shippingMethods); if (hadConvertedPackages && existingSelects === 0 && shippingMethods > 0) { logDebug('WooCommerce refreshed HTML - re-converting dropdowns...'); // Clear the converted packages cache since HTML was refreshed WSDM.cache.convertedPackages.clear(); WSDM.cache.conversionInProgress = false; // Re-convert immediately setTimeout(function() { WSDM.cache.selectChangeInProgress = false; initializeShippingMethods(); }, 100); } else if (existingSelects === 0 && !hadConvertedPackages && !WSDM.cache.conversionInProgress && shippingMethods > 0) { logDebug('Initial conversion needed...'); initializeShippingMethods(); } else { logDebug('No conversion needed - selects exist or no shipping methods found'); } }, 200)); // Global event delegation for WSDM select dropdowns as fallback $('body').on('change.wsdm', '.' + WSDM.constants.CLASSES.SHIPPING_SELECT, function() { logDebug('Global event handler triggered for select: ' + $(this).attr('id')); handleShippingSelectChange($(this)); }); // Block-based cart/checkout events if (isWpDataAvailable()) { setupStoreSubscription(); setupCleanupHandlers(); } } /** * Set up Store API subscription for blocks */ function setupStoreSubscription() { let unsubscribe = null; const subscribeToStore = () => { if (!wp.data.select('wc/store/cart')) { return; } unsubscribe = wp.data.subscribe(() => { const cart = wp.data.select('wc/store/cart').getCartData(); if (cart && cart.shippingRates) { debounce(initializeShippingMethods, WSDM.constants.CONVERSION_DELAY)(); } }); }; // Subscribe when ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', subscribeToStore); } else { subscribeToStore(); } // Store cleanup function WSDM.cache.unsubscribe = unsubscribe; } /** * Set up cleanup handlers */ function setupCleanupHandlers() { window.addEventListener('beforeunload', () => { if (WSDM.cache.unsubscribe) { WSDM.cache.unsubscribe(); } if (WSDM.cache.mutationObserver) { WSDM.cache.mutationObserver.disconnect(); } cleanupObservers(); clearAllTimeouts(); }); } /** * Load plugin configuration from localized script */ function loadConfiguration() { const params = window.wsdm_params || {}; logDebug('Loading configuration from wsdm_params: ' + JSON.stringify(params)); WSDM.config.shipping_format = sanitizeString(params.shipping_format, 'radio'); WSDM.config.is_blocks_enabled = Boolean(params.is_blocks_enabled); logDebug('Final config - shipping_format: ' + WSDM.config.shipping_format + ', is_blocks_enabled: ' + WSDM.config.is_blocks_enabled); // Maintain legacy global for backward compatibility window.wsdmConfig = WSDM.config; // If configuration is not 'select', provide a way to force it for testing if (WSDM.config.shipping_format !== 'select') { logDebug('WARNING: shipping_format is not set to "select". Current value: ' + WSDM.config.shipping_format); logDebug('To test select mode, use: window.wsdmShippingDisplayMode.debug.forceSelectMode()'); } } /** * Global handler for shipping select changes (fallback) */ function handleShippingSelectChange($select) { const selectedValue = $select.val(); const packageIndex = $select.data('package-index') || 0; logDebug('Handling select change - preventing HTML refresh'); WSDM.cache.selectChangeInProgress = true; // Store current state before triggering WooCommerce update const currentPackages = new Set(WSDM.cache.convertedPackages); // Find corresponding hidden radios const $wrapper = $select.closest('.wsdm-shipping-wrapper'); const $hiddenRadios = $wrapper.find('.wsdm-hidden-shipping-radios'); if ($hiddenRadios.length) { const $hiddenRadio = $hiddenRadios.find('input[value="' + selectedValue + '"]'); if ($hiddenRadio.length) { $hiddenRadios.find('input[type="radio"]').prop('checked', false); $hiddenRadio.prop('checked', true); // Trigger change directly without using jQuery trigger to avoid interference const event = new Event('change', { bubbles: true }); $hiddenRadio[0].dispatchEvent(event); logDebug('Global handler updated hidden radio for package ' + packageIndex); } } // Use a more direct approach for WooCommerce updates const $form = $select.closest('form'); if ($form.length && ($form.hasClass('checkout') || $form.attr('name') === 'checkout')) { // For checkout, trigger the specific update $('body').trigger('update_checkout'); } else { // For cart, trigger cart totals update $('body').trigger('updated_cart_totals'); } // Set up monitoring for HTML refresh and immediate re-conversion let checkCount = 0; const checkInterval = setInterval(function() { checkCount++; const selectsExist = $('.' + WSDM.constants.CLASSES.SHIPPING_SELECT).length > 0; if (!selectsExist && checkCount < 20) { logDebug('Select dropdowns disappeared, re-converting immediately...'); WSDM.cache.convertedPackages = currentPackages; WSDM.cache.conversionInProgress = false; initializeShippingMethods(); clearInterval(checkInterval); } else if (checkCount >= 20) { clearInterval(checkInterval); } }, 50); // Clear the flag after sufficient delay setTimeout(function() { WSDM.cache.selectChangeInProgress = false; clearInterval(checkInterval); logDebug('Cleared selectChangeInProgress flag'); }, 2000); } /** * Initialize shipping method conversion */ function initializeShippingMethods() { logDebug('Initializing shipping methods. Config format: ' + WSDM.config.shipping_format); logDebug('Blocks enabled: ' + WSDM.config.is_blocks_enabled); // Prevent multiple simultaneous conversions if (WSDM.cache.conversionInProgress) { logDebug('Conversion already in progress, skipping...'); return; } if (WSDM.config.shipping_format === 'select') { logDebug('Starting conversion to select dropdowns'); WSDM.cache.conversionInProgress = true; try { convertClassicShippingMethods(); convertBlockShippingMethods(); } finally { // Reset flag after conversion completes setTimeout(function() { WSDM.cache.conversionInProgress = false; }, 500); } } else { logDebug('Shipping format is not select, skipping conversion'); } } /** * Convert classic shipping methods to dropdown */ function convertClassicShippingMethods() { logDebug('Starting classic shipping methods conversion'); // First check if there are any shipping methods at all const $allShippingMethods = $('.shipping_method'); logDebug('Found ' + $allShippingMethods.length + ' total shipping methods on page'); if ($allShippingMethods.length === 0) { logDebug('No shipping methods found, exiting classic conversion'); return; } // Group shipping methods by package instead of processing all together const packageGroups = groupShippingMethodsByPackage(); logDebug('Detected ' + packageGroups.length + ' package groups'); if (packageGroups.length === 0) { logDebug('No package groups detected, exiting classic conversion'); return; } // Process each package separately packageGroups.forEach(function(packageData, index) { logDebug('Processing package ' + index + ' with ' + packageData.methods.length + ' methods'); if (shouldConvertClassicPackage(packageData)) { logDebug('Converting package ' + index + ' to select dropdown'); convertPackageToSelect(packageData, index); } else { logDebug('Package ' + index + ' should not be converted'); } }); } /** * Group shipping methods by package */ function groupShippingMethodsByPackage() { const packageGroups = []; logDebug('Starting package grouping'); const $allMethods = $('.shipping_method'); if ($allMethods.length === 0) { logDebug('No shipping methods found with class .shipping_method'); return packageGroups; } logDebug('Found ' + $allMethods.length + ' shipping methods total'); // Strategy 1: Look for shipping methods grouped by name attribute (for multiple packages) const methodsByName = {}; $allMethods.each(function() { const $method = $(this); const nameAttr = $method.attr('name') || 'shipping_method[0]'; logDebug('Found shipping method with name: ' + nameAttr + ', value: ' + $method.val()); if (!methodsByName[nameAttr]) { methodsByName[nameAttr] = []; } methodsByName[nameAttr].push($method); }); logDebug('Methods grouped by name attribute: ' + Object.keys(methodsByName).length + ' groups'); // Convert each name group to a package Object.entries(methodsByName).forEach(function([name, methods], index) { if (methods.length > 0) { const $methods = $(methods); // Find the best container for this package - try multiple selectors const $firstMethod = $methods.first()[0]; let $container = null; // Try different container selectors in order of preference const containerSelectors = [ '.woocommerce-shipping-methods', 'ul.woocommerce-shipping-methods', '#shipping_method', '.shipping', 'table.woocommerce-checkout-review-order-table tbody', '.shop_table tbody', 'tbody', 'ul', 'ol', '.shipping-methods', '[class*="shipping"]', 'tr.shipping', 'tr' ]; logDebug('Trying to find container for first method...'); logDebug('First method HTML: ' + $firstMethod.prop('outerHTML')); logDebug('First method parent: ' + $firstMethod.parent().prop('tagName') + ' (class: ' + $firstMethod.parent().attr('class') + ')'); for (let i = 0; i < containerSelectors.length; i++) { $container = $firstMethod.closest(containerSelectors[i]); if ($container.length) { logDebug('Found container with selector "' + containerSelectors[i] + '": ' + $container.prop('tagName')); break; } } // If still no container found, use the immediate parent if (!$container || !$container.length) { logDebug('No specific container found, using immediate parent'); $container = $firstMethod.parent(); // If parent is a label, go up one more level if ($container.is('label')) { $container = $container.parent(); logDebug('Parent was a label, using grandparent: ' + $container.prop('tagName')); } } logDebug('Final container: ' + $container.prop('tagName') + ' (class: ' + $container.attr('class') + ')'); logDebug('Container HTML preview: ' + $container.prop('outerHTML').substring(0, 200) + '...'); logDebug('Package ' + index + ' - Name: ' + name + ', Methods: ' + methods.length + ', Container: ' + $container.prop('tagName')); packageGroups.push({ container: $container, methods: $methods, packageName: name, packageIndex: index }); } }); logDebug('Created ' + packageGroups.length + ' package groups'); return packageGroups; } /** * Check if a classic package should be converted */ function shouldConvertClassicPackage(packageData) { const methodCount = packageData.methods.length; const isAlreadySelect = packageData.methods.is('select'); const isAlreadyConverted = packageData.methods.hasClass(WSDM.constants.CLASSES.SHIPPING_SELECT); const isBlockContext = packageData.container.closest('.wp-block-woocommerce-cart, .wp-block-woocommerce-checkout').length > 0; // Check if this package was already converted const packageId = packageData.packageName || packageData.packageIndex || 'unknown'; const wasAlreadyConverted = WSDM.cache.convertedPackages.has(packageId); // Check if container already has a converted package const containerHasSelect = packageData.container.find('.' + WSDM.constants.CLASSES.SHIPPING_SELECT).length > 0; // Check if any of the methods are already in a converted wrapper const methodsInWrapper = packageData.methods.closest('.wsdm-shipping-wrapper').length > 0; logDebug('Package conversion check:'); logDebug(' - Package ID: ' + packageId); logDebug(' - Methods count: ' + methodCount); logDebug(' - Is already select: ' + isAlreadySelect); logDebug(' - Is already converted: ' + isAlreadyConverted); logDebug(' - Was already converted: ' + wasAlreadyConverted); logDebug(' - Container has select: ' + containerHasSelect); logDebug(' - Methods in wrapper: ' + methodsInWrapper); logDebug(' - Is block context: ' + isBlockContext); // Convert if there are 2 or more methods, not already converted, and not in block context const shouldConvert = methodCount >= 2 && !isAlreadySelect && !isAlreadyConverted && !wasAlreadyConverted && !containerHasSelect && !methodsInWrapper && !isBlockContext; logDebug(' - Should convert: ' + shouldConvert); return shouldConvert; } /** * Convert a package to select dropdown */ function convertPackageToSelect(packageData, packageIndex) { const packageId = packageData.packageName || packageData.packageIndex || packageIndex; logDebug('Starting conversion for package ' + packageIndex + ' (ID: ' + packageId + ')'); // Mark this package as being converted WSDM.cache.convertedPackages.add(packageId); try { const $select = createClassicSelectElement(packageData.methods, packageIndex); logDebug('Created select element with ID: ' + $select.attr('id')); populateClassicOptions(packageData.methods, $select); logDebug('Populated ' + $select.find('option').length + ' options'); // Create hidden radio buttons for WooCommerce compatibility const $hiddenRadios = createHiddenRadiosForPackage(packageData); logDebug('Created hidden radios container with ' + $hiddenRadios.find('input').length + ' radios'); replaceClassicPackageElements(packageData, $select, $hiddenRadios); logDebug('Replaced package elements in DOM'); // Mark the container as converted packageData.container.addClass(WSDM.constants.CLASSES.PACKAGE_CONVERTED); bindClassicPackageEvents($select, packageData, $hiddenRadios, packageIndex); logDebug('Bound events for package ' + packageIndex); logDebug('Package ' + packageIndex + ' (ID: ' + packageId + ') conversion completed successfully'); } catch (error) { logError('Error converting package ' + packageIndex, error); // Remove from converted cache if conversion failed WSDM.cache.convertedPackages.delete(packageId); } } /** * Create hidden radio buttons for WooCommerce compatibility */ function createHiddenRadiosForPackage(packageData) { const $hiddenContainer = $('
', { style: 'display: none !important;', class: 'wsdm-hidden-shipping-radios' }); packageData.methods.each(function() { const $originalRadio = $(this); const $hiddenRadio = $originalRadio.clone(true); // Ensure the hidden radio maintains all original attributes $hiddenRadio.attr({ 'data-wsdm-hidden': 'true', 'style': 'display: none !important;' }); $hiddenContainer.append($hiddenRadio); }); return $hiddenContainer; } /** * Create select element for classic shipping */ function createClassicSelectElement($shippingOptions, packageIndex) { const $first = $shippingOptions.first(); return $('', { id: labelId + '-select', class: 'wc-blocks-components-select__select wsdm-block-shipping-select', 'data-package-id': controlName, size: 1 }); // Try to fetch method metadata from Store API to format price properly const ratesById = getStoreRatesByIdForPackage(packageIndex); $radios.each(function() { const $radio = $(this); const methodId = $radio.val(); let optionLabel; if (ratesById && ratesById[methodId]) { const meta = ratesById[methodId]; const formattedCost = formatCurrency(meta.cost); optionLabel = `${meta.label} (${formattedCost})`; } else { // Fallback to DOM parsing const $labelNode = $radioGroup.find('label[for="' + $radio.attr('id') + '"]'); const priceText = $.trim($labelNode.find('.wc-block-components-radio-control__secondary-label').text()); let nameText = $.trim( $labelNode .clone() .find('.wc-block-components-radio-control__secondary-label') .remove() .end() .text() ); nameText = nameText.replace(/\s+/g, ' ').trim(); let finalPrice = priceText; if (!finalPrice) { const matchPrice = $labelNode.text().match(/([₹$€£]\s?[\d.,]+(?:\s?[A-Z]{3})?)/); if (matchPrice) { finalPrice = matchPrice[1].trim(); } } optionLabel = finalPrice ? `${nameText} (${finalPrice})` : nameText; } $('