PageSourceSearch

https://act.mqg.org.il/wp-content/plugins/woo-shipping-display-mod…o-shipping-display-mode-public.js?ver=1.0.0

js mqg.org.il collected 2026-10-02 12:45:55 UTC 52,654 bytes, 1,303 lines download raw bytes

1(function($) {
2    'use strict';
3
4    // Plugin configuration and state
5    const WSDM = {
6        config: {
7            shipping_format: 'radio',
8            is_blocks_enabled: false
9        },
10        cache: {
11            observers: new WeakMap(),
12            timeouts: new Map(),
13            convertedPackages: new Set(), // Track converted packages to prevent duplicates
14            conversionInProgress: false,  // Prevent multiple simultaneous conversions
15            selectChangeInProgress: false // Track when our select changes are happening
16        },
17        constants: {
18            CONVERSION_DELAY: 100,
19            SYNC_DEBOUNCE_DELAY: 50,
20            BLOCK_SELECTORS: {
21                CART: '.wp-block-woocommerce-cart .wc-block-components-radio-control',
22                CHECKOUT: '.wp-block-woocommerce-checkout #shipping-option .wc-block-components-radio-control'
23            },
24            CLASSES: {
25                CONVERTED: 'wsdm-converted',
26                SHIPPING_SELECT: 'wsdm-shipping-select',
27                BLOCK_SELECT: 'wsdm-block-shipping-select',
28                PACKAGE_CONVERTED: 'wsdm-package-converted' // Mark converted packages
29            }
30        }
31    };
32
33    // Initialize plugin when DOM is ready
34    $(document).ready(function() {
35        // Enable debug mode temporarily to help diagnose the issue
36        window.wsdm_debug = false;
37        
38        logDebug('DOM ready, initializing plugin');
39        initializePlugin();
40        bindEvents();
41        
42        // Add manual trigger for testing
43        window.wsdmForceConversion = function() {
44            logDebug('Manual conversion triggered');
45            // Clear conversion cache before forcing
46            WSDM.cache.convertedPackages.clear();
47            WSDM.cache.conversionInProgress = false;
48            initializeShippingMethods();
49        };
50    });
51
52    /**
53     * Initialize the plugin
54     */
55    function initializePlugin() {
56        loadConfiguration();
57        initializeShippingMethods();
58    }
59
60    /**
61     * Bind all event listeners
62     */
63    function bindEvents() {
64        // Classic cart/checkout events - handle WooCommerce HTML refreshes
65        $('body').on('updated_cart_totals updated_checkout', debounce(function() {
66            logDebug('Cart/checkout update event detected');
67            
68            // If we had select dropdowns before but they're gone now, WooCommerce refreshed the HTML
69            const existingSelects = $('.' + WSDM.constants.CLASSES.SHIPPING_SELECT).length;
70            const hadConvertedPackages = WSDM.cache.convertedPackages.size > 0;
71            const shippingMethods = $('.shipping_method').length;
72            
73            logDebug('Update check - existingSelects: ' + existingSelects + ', hadConverted: ' + hadConvertedPackages + ', shippingMethods: ' + shippingMethods);
74            
75            if (hadConvertedPackages && existingSelects === 0 && shippingMethods > 0) {
76                logDebug('WooCommerce refreshed HTML - re-converting dropdowns...');
77                // Clear the converted packages cache since HTML was refreshed
78                WSDM.cache.convertedPackages.clear();
79                WSDM.cache.conversionInProgress = false;
80                // Re-convert immediately
81                setTimeout(function() {
82                    WSDM.cache.selectChangeInProgress = false;
83                    initializeShippingMethods();
84                }, 100);
85            } else if (existingSelects === 0 && !hadConvertedPackages && !WSDM.cache.conversionInProgress && shippingMethods > 0) {
86                logDebug('Initial conversion needed...');
87                initializeShippingMethods();
88            } else {
89                logDebug('No conversion needed - selects exist or no shipping methods found');
90            }
91        }, 200));
92
93        // Global event delegation for WSDM select dropdowns as fallback
94        $('body').on('change.wsdm', '.' + WSDM.constants.CLASSES.SHIPPING_SELECT, function() {
95            logDebug('Global event handler triggered for select: ' + $(this).attr('id'));
96            handleShippingSelectChange($(this));
97        });
98
99        // Block-based cart/checkout events
100        if (isWpDataAvailable()) {
101            setupStoreSubscription();
102            setupCleanupHandlers();
103        }
104    }
105
106    /**
107     * Set up Store API subscription for blocks
108     */
109    function setupStoreSubscription() {
110        let unsubscribe = null;
111        
112        const subscribeToStore = () => {
113            if (!wp.data.select('wc/store/cart')) {
114                return;
115            }
116            
117            unsubscribe = wp.data.subscribe(() => {
118                const cart = wp.data.select('wc/store/cart').getCartData();
119                if (cart && cart.shippingRates) {
120                    debounce(initializeShippingMethods, WSDM.constants.CONVERSION_DELAY)();
121                }
122            });
123        };
124
125        // Subscribe when ready
126        if (document.readyState === 'loading') {
127            document.addEventListener('DOMContentLoaded', subscribeToStore);
128        } else {
129            subscribeToStore();
130        }
131
132        // Store cleanup function
133        WSDM.cache.unsubscribe = unsubscribe;
134    }
135
136    /**
137     * Set up cleanup handlers
138     */
139    function setupCleanupHandlers() {
140        window.addEventListener('beforeunload', () => {
141            if (WSDM.cache.unsubscribe) {
142                WSDM.cache.unsubscribe();
143            }
144            if (WSDM.cache.mutationObserver) {
145                WSDM.cache.mutationObserver.disconnect();
146            }
147            cleanupObservers();
148            clearAllTimeouts();
149        });
150    }
151
152    /**
153     * Load plugin configuration from localized script
154     */
155    function loadConfiguration() {
156        const params = window.wsdm_params || {};
157        logDebug('Loading configuration from wsdm_params: ' + JSON.stringify(params));
158        
159        WSDM.config.shipping_format = sanitizeString(params.shipping_format, 'radio');
160        WSDM.config.is_blocks_enabled = Boolean(params.is_blocks_enabled);
161        
162        logDebug('Final config - shipping_format: ' + WSDM.config.shipping_format + ', is_blocks_enabled: ' + WSDM.config.is_blocks_enabled);
163        
164        // Maintain legacy global for backward compatibility
165        window.wsdmConfig = WSDM.config;
166        
167        // If configuration is not 'select', provide a way to force it for testing
168        if (WSDM.config.shipping_format !== 'select') {
169            logDebug('WARNING: shipping_format is not set to "select". Current value: ' + WSDM.config.shipping_format);
170            logDebug('To test select mode, use: window.wsdmShippingDisplayMode.debug.forceSelectMode()');
171        }
172    }
173
174    /**
175     * Global handler for shipping select changes (fallback)
176     */
177    function handleShippingSelectChange($select) {
178        const selectedValue = $select.val();
179        const packageIndex = $select.data('package-index') || 0;
180        
181        logDebug('Handling select change - preventing HTML refresh');
182        WSDM.cache.selectChangeInProgress = true;
183        
184        // Store current state before triggering WooCommerce update
185        const currentPackages = new Set(WSDM.cache.convertedPackages);
186        
187        // Find corresponding hidden radios
188        const $wrapper = $select.closest('.wsdm-shipping-wrapper');
189        const $hiddenRadios = $wrapper.find('.wsdm-hidden-shipping-radios');
190        
191        if ($hiddenRadios.length) {
192            const $hiddenRadio = $hiddenRadios.find('input[value="' + selectedValue + '"]');
193            if ($hiddenRadio.length) {
194                $hiddenRadios.find('input[type="radio"]').prop('checked', false);
195                $hiddenRadio.prop('checked', true);
196                
197                // Trigger change directly without using jQuery trigger to avoid interference
198                const event = new Event('change', { bubbles: true });
199                $hiddenRadio[0].dispatchEvent(event);
200                
201                logDebug('Global handler updated hidden radio for package ' + packageIndex);
202            }
203        }
204        
205        // Use a more direct approach for WooCommerce updates
206        const $form = $select.closest('form');
207        if ($form.length && ($form.hasClass('checkout') || $form.attr('name') === 'checkout')) {
208            // For checkout, trigger the specific update
209            $('body').trigger('update_checkout');
210        } else {
211            // For cart, trigger cart totals update
212            $('body').trigger('updated_cart_totals');
213        }
214        
215        // Set up monitoring for HTML refresh and immediate re-conversion
216        let checkCount = 0;
217        const checkInterval = setInterval(function() {
218            checkCount++;
219            const selectsExist = $('.' + WSDM.constants.CLASSES.SH
219IPPING_SELECT).length > 0;
220            
221            if (!selectsExist && checkCount < 20) {
222                logDebug('Select dropdowns disappeared, re-converting immediately...');
223                WSDM.cache.convertedPackages = currentPackages;
224                WSDM.cache.conversionInProgress = false;
225                initializeShippingMethods();
226                clearInterval(checkInterval);
227            } else if (checkCount >= 20) {
228                clearInterval(checkInterval);
229            }
230        }, 50);
231        
232        // Clear the flag after sufficient delay
233        setTimeout(function() {
234            WSDM.cache.selectChangeInProgress = false;
235            clearInterval(checkInterval);
236            logDebug('Cleared selectChangeInProgress flag');
237        }, 2000);
238    }
239
240    /**
241     * Initialize shipping method conversion
242     */
243    function initializeShippingMethods() {
244        logDebug('Initializing shipping methods. Config format: ' + WSDM.config.shipping_format);
245        logDebug('Blocks enabled: ' + WSDM.config.is_blocks_enabled);
246        
247        // Prevent multiple simultaneous conversions
248        if (WSDM.cache.conversionInProgress) {
249            logDebug('Conversion already in progress, skipping...');
250            return;
251        }
252        
253        if (WSDM.config.shipping_format === 'select') {
254            logDebug('Starting conversion to select dropdowns');
255            WSDM.cache.conversionInProgress = true;
256            
257            try {
258                convertClassicShippingMethods();
259                convertBlockShippingMethods();
260            } finally {
261                // Reset flag after conversion completes
262                setTimeout(function() {
263                    WSDM.cache.conversionInProgress = false;
264                }, 500);
265            }
266        } else {
267            logDebug('Shipping format is not select, skipping conversion');
268        }
269    }
270
271    /**
272     * Convert classic shipping methods to dropdown
273     */
274    function convertClassicShippingMethods() {
275        logDebug('Starting classic shipping methods conversion');
276        
277        // First check if there are any shipping methods at all
278        const $allShippingMethods = $('.shipping_method');
279        logDebug('Found ' + $allShippingMethods.length + ' total shipping methods on page');
280        
281        if ($allShippingMethods.length === 0) {
282            logDebug('No shipping methods found, exiting classic conversion');
283            return;
284        }
285        
286        // Group shipping methods by package instead of processing all together
287        const packageGroups = groupShippingMethodsByPackage();
288        logDebug('Detected ' + packageGroups.length + ' package groups');
289        
290        if (packageGroups.length === 0) {
291            logDebug('No package groups detected, exiting classic conversion');
292            return;
293        }
294        
295        // Process each package separately
296        packageGroups.forEach(function(packageData, index) {
297            logDebug('Processing package ' + index + ' with ' + packageData.methods.length + ' methods');
298            
299            if (shouldConvertClassicPackage(packageData)) {
300                logDebug('Converting package ' + index + ' to select dropdown');
301                convertPackageToSelect(packageData, index);
302            } else {
303                logDebug('Package ' + index + ' should not be converted');
304            }
305        });
306    }
307
308    /**
309     * Group shipping methods by package
310     */
311    function groupShippingMethodsByPackage() {
312        const packageGroups = [];
313        logDebug('Starting package grouping');
314        
315        const $allMethods = $('.shipping_method');
316        if ($allMethods.length === 0) {
317            logDebug('No shipping methods found with class .shipping_method');
318            return packageGroups;
319        }
320        
321        logDebug('Found ' + $allMethods.length + ' shipping methods total');
322        
323        // Strategy 1: Look for shipping methods grouped by name attribute (for multiple packages)
324        const methodsByName = {};
325        $allMethods.each(function() {
326            const $method = $(this);
327            const nameAttr = $method.attr('name') || 'shipping_method[0]';
328            logDebug('Found shipping method with name: ' + nameAttr + ', value: ' + $method.val());
329            
330            if (!methodsByName[nameAttr]) {
331                methodsByName[nameAttr] = [];
332            }
333            methodsByName[nameAttr].push($method);
334        });
335        
336        logDebug('Methods grouped by name attribute: ' + Object.keys(methodsByName).length + ' groups');
337        
338        // Convert each name group to a package
339        Object.entries(methodsByName).forEach(function([name, methods], index) {
340            
341            if (methods.length > 0) {
342                const $methods = $(methods);
343                
344                // Find the best container for this package - try multiple selectors
345                const $firstMethod = $methods.first()[0];
346                
347                let $container = null;
348                
349                // Try different container selectors in order of preference
350                const containerSelectors = [
351                    '.woocommerce-shipping-methods',
352                    'ul.woocommerce-shipping-methods', 
353                    '#shipping_method',
354                    '.shipping',
355                    'table.woocommerce-checkout-review-order-table tbody',
356                    '.shop_table tbody',
357                    'tbody',
358                    'ul',
359                    'ol',
360                    '.shipping-methods',
361                    '[class*="shipping"]',
362                    'tr.shipping',
363                    'tr'
364                ];
365                
366                logDebug('Trying to find container for first method...');
367                logDebug('First method HTML: ' + $firstMethod.prop('outerHTML'));
368                logDebug('First method parent: ' + $firstMethod.parent().prop('tagName') + ' (class: ' + $firstMethod.parent().attr('class') + ')');
369                
370                for (let i = 0; i < containerSelectors.length; i++) {
371                    $container = $firstMethod.closest(containerSelectors[i]);
372                    if ($container.length) {
373                        logDebug('Found container with selector "' + containerSelectors[i] + '": ' + $container.prop('tagName'));
374                        break;
375                    }
376                }
377                
378                // If still no container found, use the immediate parent
379                if (!$container || !$container.length) {
380                    logDebug('No specific container found, using immediate parent');
381                    $container = $firstMethod.parent();
382                    
383                    // If parent is a label, go up one more level
384                    if ($container.is('label')) {
385                        $container = $container.parent();
386                        logDebug('Parent was a label, using grandparent: ' + $container.prop('tagName'));
387                    }
388                }
389                
390                logDebug('Final container: ' + $container.prop('tagName') + ' (class: ' + $container.attr('class') + ')');
391                logDebug('Container HTML preview: ' + $container.prop('outerHTML').substring(0, 200) + '...');
392                
393                logDebug('Package ' + index + ' - Name: ' + name + ', Methods: ' + methods.length + ', Container: ' + $container.prop('tagName'));
394                
395                packageGroups.push({
396                    container: $container,
397                    methods: $methods,
398                    packageName: name,
399                    packageIndex: index
400                });
401            }
402        });
403        
404        logDebug('Created ' + packageGroups.length + ' package groups');
405        return packageGroups;
406    }
407
408    /**
409     * Check if a classic package should be converted
410     */
411    function shouldConvertClassicPackage(packageData) {
412        const methodCount = packageData.methods.length;
413        const isAlreadySelect = packageData.methods.is('select');
414        const isAlreadyConverted = packageData.methods.hasClass(WSDM.constants.CLASSES.SHIPPING_SELECT);
415        const isBlockContext = packageData.container.closest('.wp-block-woocommerce-cart, .wp-block-woocommerce-checkout').length > 0;
416        
417        // Check if this package was already converted
418        const packageId = packageData.packageName || packageData.packageIndex || 'unknown';
419        const wasAlreadyConverted = WSDM.cache.convertedPackages.has(packageId);
420        
421        // Check if container already has a converted package
422        const containerHasSelect = packageData.container.find('.' + WSDM.constants.CLASSES.SHIPPING_SELECT).length > 0;
423        
424        // Check if any of the methods are already in a converted wrapper
425        const methodsInWrapper = packageData.methods.closest('.wsdm-shipping-wrapper').length > 0;
426        
427        logDebug('Package conversion check:');
428        logDebug('  - Package ID: ' + packageId);
429        logDebug('  - Methods count: ' + methodCount);
430        logDebug('  - Is already select: ' + isAlreadySelect);
431        logDebug('  - Is already converted: ' + isAlreadyConverted);
432        logDebug('  - Was already converted: ' + wasAlreadyConverted);
433        logDebug('  - Container has select: ' + containerHasSelect);
434        logDebug('  - Methods in wrapper: ' + methodsInWrapper);
435        logDebug('  - Is block context: ' + isBlockContext);
436        
437        // Convert if there are 2 or more methods, not already converted, and not in block context
438        const shouldConvert = methodCount >= 2 && 
439                             !isAlreadySelect && 
440                             !isAlreadyConverted && 
441                             !wasAlreadyConverted && 
442                             !containerHasSelect && 
443                             !methodsInWrapper && 
444                             !isBlockContext;
445        
446        logDebug('  - Should convert: ' + shouldConvert);
447        
448        return shouldConvert;
449    }
450
451    /**
452     * Convert a package to select dropdown
453     */
454    function convertPackageToSelect(packageData, packageIndex) {
455        const packageId = packageData.packageName || packageData.packageIndex || packageIndex;
456        logDebug('Starting conversion for package ' + packageIndex + ' (ID: ' + packageId + ')');
457        
458        // Mark this package as being converted
459        WSDM.cache.convertedPackages.add(packageId);
460        
461        try {
462            const $select = createClassicSelectElement(packageData.methods, packageIndex);
463            logDebug('Created select element with ID: ' + $select.attr('id'));
464            
465            populateClassicOptions(packageData.methods, $select);
466            logDebug('Populated ' + $select.find('option').length + ' options');
467            
468            // Create hidden radio buttons for WooCommerce compatibility
469            const $hiddenRadios = createHiddenRadiosForPackage(packageData);
470            logDebug('Created hidden radios container with ' + $hiddenRadios.find('input').length + ' radios');
471            
472            replaceClassicPackageElements(packageData, $select, $hiddenRadios);
473            logDebug('Replaced package elements in DOM');
474            
475            // Mark the container as converted
476            packageData.container.addClass(WSDM.constants.CLASSES.PACKAGE_CONVERTED);
477            
478            bindClassicPackageEvents($select, packageData, $hiddenRadios, packageIndex);
479            logDebug('Bound events for package ' + packageIndex);
480            
481            logDebug('Package ' + packageIndex + ' (ID: ' + packageId + ') conversion completed successfully');
482            
483        } catch (error) {
484            logError('Error converting package ' + packageIndex, error);
485            // Remove from converted cache if conversion failed
486            WSDM.cache.convertedPackages.delete(packageId);
487        }
488    }
489
490    /**
491     * Create hidden radio buttons for WooCommerce compatibility
492     */
493    function createHiddenRadiosForPackage(packageData) {
494        const $hiddenContainer = $('<div>', {
495            style: 'display: none !important;',
496            class: 'wsdm-hidden-shipping-radios'
497        });
498        
499        packageData.methods.each(function() {
500            const $originalRadio = $(this);
501            const $hiddenRadio = $originalRadio.clone(true);
502            
503            // Ensure the hidden radio maintains all original attributes
504            $hiddenRadio.attr({
505                'data-wsdm-hidden': 'true',
506                'style': 'display: none !important;'
507            });
508            
509            $hiddenContainer.append($hiddenRadio);
510        });
511        
512        return $hiddenContainer;
513    }
514
515    /**
516     * Create select element for classic shipping
517     */
518    function createClassicSelectElement($shippingOptions, packageIndex) {
519        const $first = $shippingOptions.first();
520        return $('<select>', {
521            class: `${WSDM.constants.CLASSES.SHIPPING_SELECT}`,
522            name: sanitizeAttribute($first.attr('name')),
523            'data-index': sanitizeAttribute($first.attr('data-index')),
524            'data-package-index': packageIndex || 0,
525            'id': 'wsdm-shipping-select-' + (packageIndex || 0)
526        });
527    }
528
529    /**
530     * Populate classic select options
531     */
532    function populateClassicOptions($shippingOptions, $select) {
533        $shippingOptions.each(function() {
534            const $radio = $(this);
535            const label = sanitizeString($radio.closest('li').text().trim());
536            const value = sanitizeAttribute($radio.val());
537            const isChecked = $radio.prop('checked');
538            
539            $('<option>', {
540                value: value,
541                text: label,
542                selected: isChecked
543            }).appendTo($select);
544        });
545    }
546
547    /**
548     * Replace classic package elements with select
549     */
550    function replaceClassicPackageElements(packageData, $select, $hiddenRadios) {
551        logDebug('Starting element replacement');
552        logDebug('Package container: ' + packageData.container.prop('tagName') + ' (class: ' + packageData.container.attr('class') + ')');
553        
554        // Create a wrapper div for select and hidden radios
555        const $wrapper = $('<div>', { 
556            class: 'wsdm-shipping-wrapper',
557            'data-package': packageData.packageName || 'unknown'
558        });
559        $wrapper.append($select, $hiddenRadios);
560        
561        // Try different replacement strategies
562        let replacementSuccess = false;
563        
564        // Strategy 1: Replace individual method containers
565        const $methodContainers = packageData.methods.closest('li, tr, .shipping-method-item, label');
566        logDebug('Found ' + $methodContainers.length + ' method containers');
567        
568        if ($methodContainers.length > 0) {
569            try {
570                const $firstContainer = $methodContainers.first();
571                logDebug('Method containers found. First container: ' + $firstContainer.prop('tagName') + ' (class: ' + $firstContainer.attr('class') + ')');
572                
573                // Remove all other method containers in this package
574                $methodContainers.not($firstContainer).remove();
575                logDebug('Removed ' + ($methodContainers.length - 1) + ' other method containers');
576                
577                // Replace the first container with our wrapper
578                $firstContainer.replaceWith($wrapper);
579                logDebug('Replaced first container with wrapper');
580                replacementSuccess = true;
581                
582            } catch (error) {
583                logError('Error replacing method containers', error);
584            }
585        }
586        
587        // Strategy 2: Replace all methods within their common container
588        if (!replacementSuccess) {
589            try {
590                logDebug('Trying to replace all methods within container');
591                
592                // Hide all the original radio methods
593                // packageData.methods.each(function() {
594                //     $(this).closest('li, tr, label, div').hide();
595                // });
596                
597                // Insert the wrapper at the beginning of the container
598                packageData.container.prepend($wrapper);
599                logDebug('Prepended wrapper to container');
600                replacementSuccess = true;
601                
602            } catch (error) {
603                logError('Error with container replacement', error);
604            }
605        }
606        
607        // Strategy 3: Replace the entire container content (last resort)
608        if (!replacementSuccess) {
609            try {
610                logDebug('Last resort: replacing entire container content');
611                packageData.container.html($wrapper);
612                logDebug('Replaced container content');
613                replacementSuccess = true;
614                
615            } catch (error) {
616                logError('Error replacing container content', error);
617            }
618        }
619        
620        // Verify the replacement worked
621        setTimeout(function() {
622            const $insertedSelect = $('.' + WSDM.constants.CLASSES.SH
622IPPING_SELECT);
623            logDebug('Verification: Found ' + $insertedSelect.length + ' select elements after replacement');
624            
625            if ($insertedSelect.length === 0) {
626                logError('No select elements found after replacement!');
627            }
628        }, 100);
629    }
630
631    /**
632     * Bind events for classic package select
633     */
634    function bindClassicPackageEvents($select, packageData, $hiddenRadios, packageIndex) {
635        logDebug('Binding events for package ' + packageIndex + ' select with ID: ' + $select.attr('id'));
636        
637        // Remove any existing event handlers first
638        $select.off('change.wsdm');
639        
640        $select.on('change.wsdm', function() {
641            const selectedValue = $(this).val();
642            const selectId = $(this).attr('id');
643            logDebug('Package ' + packageIndex + ' select changed to: ' + selectedValue + ' (ID: ' + selectId + ')');
644            
645            // Set flag to prevent re-initialization
646            WSDM.cache.selectChangeInProgress = true;
647            
648            // Update the corresponding hidden radio button
649            const $hiddenRadio = $hiddenRadios.find('input[value="' + selectedValue + '"]');
650            if ($hiddenRadio.length) {
651                // Uncheck all hidden radios in this package first
652                $hiddenRadios.find('input[type="radio"]').prop('checked', false);
653                
654                // Check the selected one
655                $hiddenRadio.prop('checked', true);
656                
657                // Trigger change event on the hidden radio for WooCommerce
658                $hiddenRadio.trigger('change');
659                logDebug('Hidden radio updated for package ' + packageIndex);
660            } else {
661                logDebug('No hidden radio found for value: ' + selectedValue);
662            }
663            
664            // Get form context for proper WooCommerce integration
665            const $form = $(this).closest('form');
666            
667            // Trigger appropriate WooCommerce events
668            if ($form.length && ($form.hasClass('checkout') || $form.attr('name') === 'checkout')) {
669                logDebug('Triggering checkout update events for package ' + packageIndex);
670                // For checkout page
671                $('body').trigger('update_checkout');
672                $form.trigger('checkout_updated');
673            } else {
674                logDebug('Triggering cart update events for package ' + packageIndex);
675                // For cart page or other contexts
676                $('body').trigger('updated_cart_totals');
677                
678                // Also trigger WooCommerce cart update if available
679                if (typeof wc_cart_params !== 'undefined') {
680                    $('body').trigger('wc_update_cart');
681                }
682            }
683            
684            // Additional fallback for legacy WooCommerce versions
685            if (typeof wc_checkout_params !== 'undefined') {
686                $(this).trigger('change');
687            }
688            
689            // Force form submission for immediate update - delay to prevent conflicts
690            setTimeout(function() {
691                if ($form.length) {
692                    const $updateBtn = $form.find('[name="update_cart"], .button[name="update_cart"]');
693                    if ($updateBtn.length) {
694                        logDebug('Triggering update cart button for package ' + packageIndex);
695                        $updateBtn.trigger('click');
696                    }
697                }
698            }, 150);
699            
700            // Clear the flag after sufficient delay
701            setTimeout(function() {
702                WSDM.cache.selectChangeInProgress = false;
703                logDebug('Cleared selectChangeInProgress flag after package ' + packageIndex + ' change');
704            }, 1200);
705        });
706        
707        logDebug('Events bound successfully for package ' + packageIndex);
708    }
709
710    /**
711     * Convert block-based shipping methods to dropdown
712     */
713    function convertBlockShippingMethods() {
714        if (!WSDM.config.is_blocks_enabled || WSDM.config.shipping_format !== 'select') {
715            return;
716        }
717
718        // Use timeout to ensure block rendering is complete
719        const timeoutId = setTimeout(() => {
720            processBlockShippingMethods();
721            WSDM.cache.timeouts.delete('blockConversion');
722        }, WSDM.constants.CONVERSION_DELAY);
723        
724        WSDM.cache.timeouts.set('blockConversion', timeoutId);
725    }
726
727    /**
728     * Process all block shipping methods for conversion
729     */
730    function processBlockShippingMethods() {
731        const selector = `${WSDM.constants.BLOCK_SELECTORS.CART}, ${WSDM.constants.BLOCK_SELECTORS.CHECKOUT}`;
732        const $methods = $(selector);
733        
734        $methods.each(function() {
735            const $radioGroup = $(this);
736            const $radios = $radioGroup.find('input[type="radio"]');
737            
738            if (shouldConvertBlockMethod($radioGroup, $radios)) {
739                convertBlockRadioToSelect($radioGroup, $radios);
740                $radioGroup.addClass(WSDM.constants.CLASSES.CONVERTED);
741            }
742        });
743    }
744
745    /**
746     * Check if block method should be converted
747     */
748    function shouldConvertBlockMethod($radioGroup, $radios) {
749        return $radios.length > 1 && 
750               !$radioGroup.hasClass(WSDM.constants.CLASSES.CONVERTED) && 
751               $radioGroup.is(':visible');
752    }
753
754    function convertBlockRadioToSelect($radioGroup, $radios) {
755        const controlName = $radios.first().attr('name');
756        const labelId = $radioGroup.closest('[id]').attr('id') || ('shipping-option-' + Math.random().toString(36).slice(2));
757
758        // Resolve package index from name like radio-control-<index>
759        const pkgMatch = String(controlName || '').match(/radio-control-(\d+)/);
760        const packageIndex = pkgMatch ? parseInt(pkgMatch[1], 10) : 0;
761
762        // Build WC Blocks select structure
763        const $outer = $('<div>', { class: 'wc-blocks-components-select' });
764        const $container = $('<div>', { class: 'wc-blocks-components-select__container' });
765
766        // Try to find the group label text from preceding h3/label; fallback to Shipping options
767        let groupLabel = window.wsdm_params.shipping_options_label;
768        const $maybeHeading = $radioGroup.prev('h3, h4, label').first();
769        if ($maybeHeading.length) {
770            groupLabel = $.trim($maybeHeading.text());
771        }
772
773        const $label = $('<label>', {
774            class: 'wc-blocks-components-select__label',
775            for: labelId + '-select'
776        }).text(groupLabel);
777
778        const $select = $('<select>', {
779            id: labelId + '-select',
780            class: 'wc-blocks-components-select__select wsdm-block-shipping-select',
781            'data-package-id': controlName,
782            size: 1
783        });
784
785        // Try to fetch method metadata from Store API to format price properly
786        const ratesById = getStoreRatesByIdForPackage(packageIndex);
787        
788        $radios.each(function() {
789            const $radio = $(this);
790            const methodId = $radio.val();
791            let optionLabel;
792
793            if (ratesById && ratesById[methodId]) {
794                const meta = ratesById[methodId];
795                const formattedCost = formatCurrency(meta.cost);
796                optionLabel = `${meta.label} (${formattedCost})`;
797            } else {
798                // Fallback to DOM parsing
799                const $labelNode = $radioGroup.find('label[for="' + $radio.attr('id') + '"]');
800                const priceText = $.trim($labelNode.find('.wc-block-components-radio-control__secondary-label').text());
801                let nameText = $.trim(
802                    $labelNode
803                        .clone()
804                        .find('.wc-block-components-radio-control__secondary-label')
805                        .remove()
806                        .end()
807                        .text()
808                );
809                nameText = nameText.replace(/\s+/g, ' ').trim();
810                let finalPrice = priceText;
811                if (!finalPrice) {
812                    const matchPrice = $labelNode.text().match(/([₹$€£]\s?[\d.,]+(?:\s?[A-Z]{3})?)/);
813                    if (matchPrice) {
814                        finalPrice = matchPrice[1].trim();
815                    }
816                }
817                optionLabel = finalPrice ? `${nameText} (${finalPrice})` : nameText;
818            }
819
820            $('<option>', {
821                value: methodId,
822                text: optionLabel,
823                selected: $radio.is(':checked')
824            }).appendTo($select);
825        });
826
827        // SVG arrow same as country select
828        const $svg = $(
829            '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" width="24" height="24" class="wc-blocks-components-select__expand" aria-hidden="true" focusable="false">' +
830            '<path d="M17.5 11.6L12 16l-5.5-4.4.9-1.2L12 14l4.5-3.6 1 1.2z"></path>' +
831            '</svg>'
832        );
833
834        $container.append($label, $select, $svg);
835        $outer.append($container);
836
837        // Hide the original radio wrapper but keep it for React updates
838        const $wrapper = $radioGroup.closest('.wc-block-components-radio-control');
839        $wrapper.hide().after($outer);
840        
841        // Store reference to hidden wrapper for syncing
842        $outer.data('original-wrapper', $wrapper);
843
844        // Handle selection changes -> sync with hidden radio and trigger Store API
845        $select.on('change', function() {
846            const selectedRate = $(this).val();
847            
848            // Update hidden radio to keep React state in sync
849            const $hiddenRadio = $wrapper.find('input[type="radio"][value="' + selectedRate + '"]');
850            if ($hiddenRadio.length) {
851                $hiddenRadio.prop('checked', true).trigger('change');
852            }
853            
854            // Also dispatch to Store API as backup
855            const match = String($(this).attr('data-package-id') || '').match(/radio-control-(\d+)/);
856            const packageId = match ? parseInt(match[1], 10) : 0;
857            
858            if (typeof wp !== 'undefined' && wp.data && wp.data.dispatch('wc/store/cart')) {
859                wp.data.dispatch('wc/store/cart').selectShippingRate(selectedRate, packageId);
860            }
861        });
862
863        // Set up observer to sync dropdown when React updates the hidden radios
864        setupRadioSyncObserver($wrapper, $select);
865    }
866
867    /**
868     * Set up mutation observer to sync dropdown with hidden radios
869     */
870    function setupRadioSyncObserver($hiddenWrapper, $dropdown) {
871        if (!window.MutationObserver) {
872            return;
873        }
874
875        const syncHandler = debounce(() => {
876            syncDropdownWithRadios($hiddenWrapper, $dropdown);
877        }, WSDM.constants.SYNC_DEBOUNCE_DELAY);
878        
879        const observer = new MutationObserver((mutations) => {
880            const shouldSync = mutations.some(mutation => 
881                mutation.type === 'childList' || 
882                (mutation.type === 'attributes' && 
883                 mutation.target.type === 'radio' && 
884                 ['checked', 'value'].includes(mutation.attributeName))
885            );
886
887            if (shouldSync) {
888                syncHandler();
889            }
890        });
891
892        try {
893            observer.observe($hiddenWrapper[0], {
894                childList: true,
895                subtree: true,
896                attributes: true,
897                attributeFilter: ['checked', 'value']
898            });
899
900            // Store observer reference for cleanup
901            $dropdown.data('radio-observer', observer);
902            WSDM.cache.observers.set($dropdown[0], observer);
903        } catch (error) {
904            logError('Failed to set up mutation observer', error);
905        }
906    }
907
908    function syncDropdownWithRadios($hiddenWrapper, $dropdown) {
909        try {
910            // Check if dropdown still exists and is visible
911            if (!$dropdown.length || !$dropdown.is(':visible')) {
912                return;
913            }
914            
915            const $radios = $hiddenWrapper.find('input[type="radio"]');
916            
917            // If no radios found, don't update
918            if ($radios.length === 0) {
919                return;
920            }
921            
922            const currentValue = $dropdown.val();
923            
924            // Check if options actually changed by comparing values
925            const newValues = $radios.map(function() { return $(this).val(); }).get().sort();
926            const oldValues = $dropdown.find('option').map(function() { return $(this).val(); }).get().sort();
927            
928            if (JSON.stringify(newValues) === JSON.stringify(oldValues)) {
929                // Options haven't changed, just update selection
930                const checkedValue = $radios.filter(':checked').val();
931                if (checkedValue && checkedValue !== currentValue) {
932                    $dropdown.val(checkedValue);
933                }
934                return;
935            }
936            
937            // Clear and rebuild dropdown options
938            $dropdown.empty();
939            
940            const packageIndex = getPackageIndexFromWrapper($hiddenWrapper);
941            const ratesById = getStoreRatesByIdForPackage(packageIndex);
942            
943            let hasCurrentValue = false;
944            let firstValue = null;
945            let checkedValue = null;
946            
947            $radios.each(function() {
948                const $radio = $(this);
949                const methodId = $radio.val();
950                if (!firstValue) {
951                    firstValue = methodId;
952                }
953                if (methodId === currentValue) {
954                    hasCurrentValue = true;
955                }
956                if ($radio.is(':checked')) {
957                    checkedValue = methodId;
958                }
959                
960                let optionLabel;
961                
962                if (ratesById && ratesById[methodId]) {
963                    const meta = ratesById[methodId];
964                    const formattedCost = formatCurrency(meta.cost);
965                    optionLabel = `${meta.label} (${formattedCost})`;
966                } else {
967                    // Fallback to DOM parsing from hidden radio label
968                    const $labelNode = $hiddenWrapper.find('label[for="' + $radio.attr('id') + '"]');
969                    const priceText = $.trim($labelNode.find('.wc-block-components-radio-control__secondary-label').text());
970                    let nameText = $.trim(
971                        $labelNode
972                            .clone()
973                            .find('.wc-block-components-radio-control__secondary-label')
974                            .remove()
975                            .end()
976                            .text()
977                    );
978                    nameText = nameText.replace(/\s+/g, ' ').trim();
979                    let finalPrice = priceText;
980                    if (!finalPrice) {
981                        const matchPrice = $labelNode.text().match(/([₹$€£]\s?[\d.,]+(?:\s?[A-Z]{3})?)/);
982                        if (matchPrice) {
983                            finalPrice = matchPrice[1].trim();
984                        }
985                    }
986                    optionLabel = finalPrice ? `${nameText} (${finalPrice})` : nameText;
987                }
988                
989                $('<option>', {
990                    value: methodId,
991                    text: optionLabel,
992                    selected: $radio.is(':checked')
993                }).appendTo($dropdown);
994            });
995            
996            // Set the correct value based on radio state or fallback
997            if (checkedValue) {
998                $dropdown.val(checkedValue);
999            } else if (!hasCurrentValue && firstValue) {
1000                // Auto-select first option if current selection is no longer available
1001                $dropdown.val(firstValue);
1002                const $firstRadio = $hiddenWrapper.find('input[type="radio"][value="' + firstValue + '"]');
1003                if ($firstRadio.length) {
1004                    $firstRadio.prop('checked', true).trigger('change');
1005                }
1006                // Also dispatch to Store API as backup for update in cart
1007                const match = String($dropdown.attr('data-package-id') || '').match(/radio-control-(\d+)/);
1008                const packageId = match ? parseInt(match[1], 10) : 0;
1009                if (typeof wp !== 'undefined' && wp.data && wp.data.dispatch('wc/store/cart')) {
1010                    wp.data.dispatch('wc/store/cart').selectShippingRate(firstValue, packageId);
1011                }
1012            }
1013            
1014        } catch (error) {
1015            logError('Failed to sync dropdown with radios', error);
1016        }
1017    }
1018
1019    function getPackageIndexFromWrapper($wrapper) {
1020        const $radios = $wrapper.find('input[type="radio"]');
1021        if ($radios.length > 0) {
1022            const name = $radios.first().attr('name');
1023            const match = String(name || '').match(/radio-control-(\d+)/);
1024            return match ? parseInt(match[1], 10) : 0;
1025        }
1026        return 0;
1027    }
1028
1029    /**
1030     * Get shipping rates by ID for a specific package
1031     */
1032    function getStoreRatesByIdForPackage(packageIndex) {
1033        try {
1034            if (!isWpDataAvailable()) {
1035                return null;
1036            }
1037            
1038            const store = wp.data.select('wc/store/cart');
1039            if (!store) {
1040                return null;
1041            }
1042            
1043            const cart = store.getCartData?.() || null;
1044            if (!cart) {
1045                return null;
1046            }
1047            
1048            // Try our extension data first
1049            const extensionRates = getExtensionRates(cart, packageIndex);
1050            if (extensionRates) {
1051                return extensionRates;
1052            }
1053            
1054            // Fallback to native shipping rates
1055            return getNativeRates(cart, packageIndex);
1056            
1057        } catch (error) {
1058            logError('Failed to get store rates', error);
1059            return null;
1060        }
1061    }
1062
1063    /**
1064     * Get rates from our extension data
1065     */
1066    function getExtensionRates(cart, packageIndex) {
1067        const ext = cart.extensions?.['woo-shipping-display-mode'];
1068        if (!ext?.available_packages?.[packageIndex]) {
1069            return null;
1070        }
1071        
1072        const methods = ext.available_packages[packageIndex].available_methods || [];
1073        const ratesMap = {};
1074        
1075        methods.forEach(method => {
1076            if (method.id && method.label) {
1077                ratesMap[method.id] = {
1078                    label: sanitizeString(method.label),
1079                    cost: parseFloat(method.cost) || 0
1080                };
1081            }
1082        });
1083        
1084        return Object.keys(ratesMap).length > 0 ? ratesMap : null;
1085    }
1086
1087    /**
1088     * Get rates from native WooCommerce data
1089     */
1090    function getNativeRates(cart, packageIndex) {
1091        const pkg = cart.shippingRates?.[packageIndex];
1092        if (!pkg?.rates || !Array.isArray(pkg.rates)) {
1093            return null;
1094        }
1095        
1096        const ratesMap = {};
1097        
1098        pkg.rates.forEach(rate => {
1099            const id = rate.rate_id || rate.id;
1100            if (id) {
1101                ratesMap[id] = {
1102                    label: sanitizeString(rate.name || rate.label || rate.method_name),
1103                    cost: parseFloat(rate.price || rate.cost) || 0
1104                };
1105            }
1106        });
1107        
1108        return Object.keys(ratesMap).length > 0 ? ratesMap : null;
1109    }
1110
1111    function formatCurrency(amount) {
1112        const cur = (window.wcSettings && window.wcSettings.currency) || {};
1113        const symbol = cur.symbol || '$';
1114        const position = cur.symbolPosition || 'left';
1115        const thousand = cur.thousandSeparator || ',';
1116        const decimal = cur.decimalSeparator || '.';
1117        const precision = typeof cur.precision === 'number' ? cur.precision : 2;
1118
1119        const n = isNaN(amount) ? 0 : Number(amount);
1120        const fixed = n.toFixed(precision);
1121        const parts = fixed.split('.');
1122        parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, thousand);
1123        const number = precision > 0 ? parts.join(decimal) : parts[0];
1124
1125        switch (position) {
1126            case 'left_space':
1127                return symbol + ' ' + number;
1128            case 'right':
1129                return number + symbol;
1130            case 'right_space':
1131                return number + ' ' + symbol;
1132            case 'left':
1133            default:
1134                return symbol + number;
1135        }
1136    }
1137
1138    // =========================
1139    // UTILITY FUNCTIONS
1140    // =========================
1141
1142    /**
1143     * Check if WordPress data is available
1144     */
1145    function isWpDataAvailable() {
1146        return typeof wp !== 'undefined' && wp.data;
1147    }
1148
1149    /**
1150     * Sanitize string input with fallback
1151     */
1152    function sanitizeString(input, fallback = '') {
1153        if (typeof input !== 'string') {
1154            return fallback;
1155        }
1156        return input.trim() || fallback;
1157    }
1158
1159    /**
1160     * Sanitize attribute value
1161     */
1162    function sanitizeAttribute(input) {
1163        return sanitizeString(String(input || ''));
1164    }
1165
1166    /**
1167     * Debounce function execution
1168     */
1169    function debounce(func, wait) {
1170        let timeout;
1171        return function executedFunction(...args) {
1172            const later = () => {
1173                clearTimeout(timeout);
1174                func.apply(this, args);
1175            };
1176            clearTimeout(timeout);
1177            timeout = setTimeout(later, wait);
1178        };
1179    }
1180
1181    /**
1182     * Clear all cached timeouts
1183     */
1184    function clearAllTimeouts() {
1185        WSDM.cache.timeouts.forEach((timeoutId) => {
1186            clearTimeout(timeoutId);
1187        });
1188        WSDM.cache.timeouts.clear();
1189    }
1190
1191    /**
1192     * Clean up mutation observers
1193     */
1194    function cleanupObservers() {
1195        $(`.${WSDM.constants.CLASSES.BLOCK_SELECT}`).each(function() {
1196            const observer = $(this).data('radio-observer');
1197            if (observer && typeof observer.disconnect === 'function') {
1198                observer.disconnect();
1199            }
1200        });
1201    }
1202
1203    /**
1204     * Safe console logging for debug
1205     */
1206    function logDebug(message) {
1207        if (typeof console !== 'undefined' && console.log && window.wsdm_debug) {
1208            console.log(`WSDM Debug: ${message}`);
1209        }
1210    }
1211
1212    /**
1213     * Safe console logging
1214     */
1215    function logError(message, error) {
1216        if (typeof console !== 'undefined' && console.warn) {
1217            console.warn(`WSDM: ${message}`, error);
1218        }
1219    }
1220
1221    // =========================
1222    // PUBLIC API
1223    // =========================
1224
1225    /**
1226     * Public API for external use
1227     */
1228    window.wsdmShippingDisplayMode = Object.freeze({
1229        init: initializeShippingMethods,
1230        cleanup: cleanupObservers,
1231        config: WSDM.config,
1232        debug: {
1233            forceConversion: function() {
1234                logDebug('Force conversion called via API');
1235                initializeShippingMethods();
1236            },
1237            forceSelectMode: function() {
1238                logDebug('Forcing select mode for testing');
1239                WSDM.config.shipping_format = 'select';
1240                // Clear conversion state
1241                WSDM.cache.convertedPackages.clear();
1242                WSDM.cache.conversionInProgress = false;
1243                initializeShippingMethods();
1244            },
1245            resetConversionState: function() {
1246                logDebug('Resetting conversion state');
1247                WSDM.cache.convertedPackages.clear();
1248                WSDM.cache.conversionInProgress = false;
1249                WSDM.cache.selectChangeInProgress = false;
1250                // Remove converted classes
1251                $('.' + WSDM.constants.CLASSES.PACKAGE_CONVERTED).removeClass(WSDM.constants.CLASSES.PACKAGE_CONVERTED);
1252                $('.' + WSDM.constants.CLASSES.SHIPPING_SELECT).remove();
1253                $('.wsdm-shipping-wrapper').remove();
1254            },
1255            getPackageGroups: function() {
1256                return groupShippingMethodsByPackage();
1257            },
1258            getConfig: function() {
1259                return WSDM.config;
1260            },
1261            getStatus: function() {
1262                const status = {
1263                    config: WSDM.config,
1264                    shippingMethods: $('.shipping_method').length,
1265                    selectDropdowns: $('.' + WSDM.constants.CLASSES.SHIPPING_SELECT).length,
1266                    packageGroups: groupShippingMethodsByPackage().length,
1267                    blockElements: $('.wp-block-woocommerce-cart, .wp-block-woocommerce-checkout').length
1268                };
1269                
1270                console.log('WSDM Status Report:', status);
1271                return status;
1272            },
1273            analyzeHTML: function() {
1274                const $methods = $('.shipping_method');
1275                console.log('=== HTML Structure Analysis ===');
1276                console.log('Found ' + $methods.length + ' shipping methods');
1277                
1278                $methods.each(function(index) {
1279                    const $method = $(this);
1280                    console.log('\n--- Method ' + index + ' ---');
1281                    console.log('HTML:', $method.prop('outerHTML'));
1282                    console.log('Name:', $method.attr('name'));
1283                    console.log('Value:', $method.val());
1284                    console.log('Parent:', $method.parent().prop('tagName'), $method.parent().attr('class'));
1285                    console.log('Grandparent:', $method.parent().parent().prop('tagName'), $method.parent().parent().attr('class'));
1286                    console.log('Great-grandparent:', $method.parent().parent().parent().prop('tagName'), $method.parent().parent().parent().attr('class'));
1287                });
1288                
1289                return {
1290                    methods: $methods.length,
1291                    structure: 'Check console for detailed structure'
1292                };
1293            },
1294            enableDebug: function() {
1295                window.wsdm_debug = true;
1296            },
1297            disableDebug: function() {
1298                window.wsdm_debug = false;
1299            }
1300        }
1301    });
1302
1303})(jQuery);

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.