(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 $('