PageSourceSearch

https://relevantradio.com/wp-content/plugins/joan/assets/js/joan.js?ver=6.1.3

js relevantradio.com collected 2026-09-24 09:00:56 UTC 56,418 bytes, 1,356 lines download raw bytes

1/**
2 * JOAN JS v6.0.9
3 * Author: G & D Enterprises, Inc.
4 * FIXED: Day separation now works with translated day names
5 */
6
7jQuery(function($) {
8    // Cache for faster subsequent loads
9    let showCache = null;
10    let cacheTime = 0;
11    let errorCount = 0;
12    let maxErrors = 3;
13    const CACHE_DURATION = 60000; // 1 minute cache
14
15    function initializeDarkMode() {
16        const settings = window.joan_ajax ? window.joan_ajax.settings : {};
17        const darkModeSetting = settings.joan_dark_mode || 'auto';
18        const allowOverride = settings.joan_dark_mode_override === '1';
19        
20        // Get stored user preference if override is allowed
21        let userPreference = null;
22        if (allowOverride) {
23            try {
24                userPreference = localStorage.getItem('joan_dark_mode_preference');
25            } catch (e) {
26                // localStorage not available
27            }
28        }
29        
30        // Determine if dark mode should be active
31        let shouldUseDarkMode = false;
32        
33        switch (darkModeSetting) {
34            case 'dark':
35                shouldUseDarkMode = true;
36                break;
37            case 'light':
38                shouldUseDarkMode = false;
39                break;
40            case 'disabled':
41                shouldUseDarkMode = false;
42                break;
43            case 'auto':
44            default:
45                // Check user preference first, then system preference
46                if (userPreference === 'dark') {
47                    shouldUseDarkMode = true;
48                } else if (userPreference === 'light') {
49                    shouldUseDarkMode = false;
50                } else {
51                    // Fall back to system preference
52                    shouldUseDarkMode = window.matchMedia && 
53                                      window.matchMedia('(prefers-color-scheme: dark)').matches;
54                }
55                break;
56        }
57        
58        // Apply dark mode class
59        applyDarkMode(shouldUseDarkMode);
60        
61        // Add toggle buttons if override is allowed
62        if (allowOverride && darkModeSetting !== 'disabled') {
63            addDarkModeToggles(shouldUseDarkMode);
64        }
65        
66        // Listen for system preference changes if in auto mode
67        if (darkModeSetting === 'auto' && window.matchMedia && !userPreference) {
68            const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
69            const handleChange = (e) => {
70                if (!localStorage.getItem('joan_dark_mode_preference')) {
71                    applyDarkMode(e.matches);
72                    updateToggleButtons(e.matches);
73                }
74            };
75            
76            // Use modern addEventListener if available, fallback to addListener
77            if (mediaQuery.addEventListener) {
78                mediaQuery.addEventListener('change', handleChange);
79            } else if (mediaQuery.addListener) {
80                mediaQuery.addListener(handleChange);
81            }
82        }
83    }
84    
85    function applyDarkMode(isDark) {
86        const body = document.body;
87        // Define selectors for all JOAN elements that support theme switching
88        const selectors = [
89            '.joan-now-playing',
90            '.joan-schedule-container',
91            'table.joan-schedule',
92            '.joan-schedule-day-section',
93            '.joan-schedule-day-table',
94            '.joan-schedule-day-header',
95            '.joan-schedule-controls',
96            '.joan-schedule-empty-day',
97            '.joan-sunday-header',
98            '.joan-monday-header',
99            '.joan-tuesday-header',
100            '.joan-wednesday-header',
101            '.joan-thursday-header',
102            '.joan-friday-header',
103            '.joan-saturday-header',
104            '.joan-sunday-table',
105            '.joan-monday-table',
106            '.joan-tuesday-table',
107            '.joan-wednesday-table',
108            '.joan-thursday-table',
109            '.joan-friday-table',
110            '.joan-saturday-table',
111            '.joan-today-highlight',
112            '.joan-current-show-highlight'
113        ];
114        // Toggle body classes based on dark/light state
115        if (isDark) {
116            body.classList.add('joan-dark-mode');
117            body.classList.remove('joan-light-mode');
118        } else {
119            body.classList.add('joan-light-mode');
120            body.classList.remove('joan-dark-mode');
121        }
122        // Toggle corresponding classes on all JOAN elements
123        selectors.forEach(selector => {
124            const elements = document.querySelectorAll(selector);
125            elements.forEach(element => {
126                if (isDark) {
127                    element.classList.add('joan-dark-mode');
128                    element.classList.remove('joan-light-mode');
129                } else {
130                    element.classList.add('joan-light-mode');
131                    element.classList.remove('joan-dark-mode');
132                }
133            });
134        });
135    }
136    
137    function addDarkModeToggles(currentState) {
138        $('.joan-now-playing').each(function() {
139            const $container = $(this);
140            
141            // Don't add toggle if one already exists
142            if ($container.find('.joan-dark-mode-toggle').length > 0) {
143                return;
144            }
145            
146            const $toggle = $('<button type="button" class="joan-dark-mode-toggle"></button>');
147            updateToggleButton($toggle, currentState);
148            
149            $toggle.on('click', function(e) {
150                e.preventDefault();
151                toggleDarkMode();
152            });
153            
154            $container.append($toggle);
155        });
156    }
157    
158    function updateToggleButton($button, isDark) {
159        // TRANSLATION: Use localized strings for dark mode toggle
160        const i18n = joan_ajax.i18n || {};
161        const lightModeText = i18n.switch_to_light_mode || 'Switch to light mode';
162        const darkModeText = i18n.switch_to_dark_mode || 'Switch to dark mode';
163        
164        $button.text(isDark ? '🌕' : '☀️')
165            .attr('title', isDark ? lightModeText : darkModeText)
166            .attr('aria-label', isDark ? lightModeText : darkModeText);
167    }
168    
169    function updateToggleButtons(isDark) {
170        $('.joan-dark-mode-toggle').each(function() {
171            updateToggleButton($(this), isDark);
172        });
173    }
174    
175    function toggleDarkMode() {
176        const isDarkMode = document.body.classList.contains('joan-dark-mode');
177        const newState = !isDarkMode;
178        
179        // Apply the change
180        applyDarkMode(newState);
181        updateToggleButtons(newState);
182        
183        // Store user preference
184        try {
185            localStorage.setItem('joan_dark_mode_preference', newState ? 'dark' : 'light');
186        } catch (e) {
187            // localStorage not available
188        }
189    }
190    
191    function getPreferredTimezone() {
192        return localStorage.getItem("joan_timezone") || Intl.DateTimeFormat().resolvedOptions().timeZone;
193    }
194
195    function setPreferredTimezone(tz) {
196        localStorage.setItem("joan_timezone", tz);
197    }
198
199    /**
200     * Escape HTML special characters in a string.
201     *
202     * JavaScript templates in this script interpolate strings directly into
203     * HTML. To avoid introducing markup or XSS vulnerabilities when
204     * displaying show names or host names, we replace special characters
205     * with their HTML entities. Similar to PHP's esc_html(), this
206     * function ensures that dynamic content is rendered safely.
207     *
208     * @param {string} str The raw string to escape.
209     * @returns {string} The escaped string.
210     */
211    function escapeHtml(str) {
212        return String(str)
213            .replace(/&/g, '&amp;')
214            .replace(/</g, '&lt;')
215            .replace(/>/g, '&gt;')
216            .replace(/"/g, '&quot;')
217            .replace(/'/g, '&#039;');
218    }
219
220    function buildTimezoneDropdown(currentTZ, container, settings) {
221        // Check if timezone selector should be shown based on settings
222        const allowTimezoneSelector = settings.joan_allow_timezone_selector === '1';
223        const showTimezone = container.data('show-timezone') !== 'no' && allowTimezoneSelector;
224        
225        if (!showTimezone || container.find('.joan-timezone-selector').length) return;
226
227        const wrapper = document.createElement('div');
228        wrapper.className = 'joan-timezone-selector';
229
230        const label = document.createElement('label');
231        // TRANSLATION: Use localized string
232        label.textContent = joan_ajax.i18n.switch_timezone;
233        label.htmlFor = "joan-tz-select-" + Math.random().toString(36).substr(2, 9);
234
235        const select = document.createElement('select');
236        select.id = label.htmlFor;
237
238        const timezones = Intl.supportedValuesOf('timeZone');
239        timezones.forEach(zone => {
240            const opt = document.createElement('option');
241            opt.value = zone;
242            opt.textContent = zone;
243            if (zone === currentTZ) opt.selected = true;
244            select.appendChild(opt);
245        });
246
247        // Add time display within timezone selector
248        const timeDisplay = document.createElement('div');
249        timeDisplay.className = 'joan-timezone-time';
250        
251        // Function to update the time display
252        function updateTimezoneTime() {
253            const now = new Date();
254            const selectedTimezone = select.value || currentTZ;
255            const timeFormat = settings.time_format || '12';
256            
257            let formatted;
258            try {
259                if (timeFormat === '24') {
260                    formatted = now.toLocaleTimeString(undefined, {
261                        timeZone: selectedTimezone,
262                        hour: '2-digit', 
263                        minute: '2-digit',
264                        hour12: false,
265                        timeZoneName: 'short'
266                    });
267                } else {
268                    formatted = now.toLocaleTimeString(undefined, {
269                        timeZone: selectedTimezone,
270                        hour: 'numeric', 
271                        minute: '2-digit', 
272                        hour12: true, 
273                        timeZoneName: 'short'
274                    });
275                }
276                // TRANSLATION: Use localized string
277                timeDisplay.textContent = joan_ajax.i18n.current_time + " " + formatted;
278            } catch (e) {
279                // TRANSLATION: Use localized string
280                timeDisplay.textContent = joan_ajax.i18n.time_display_unavailable;
281            }
282        }
283
284        select.addEventListener('change', function () {
285            setPreferredTimezone(this.value);
286            updateTimezoneTime(); // Update time display immediately
287            clearScheduleCache(); // FIX: Clear cache on timezone change
288            errorCount = 0; // Reset error count on user interaction
289            fetchCurrentShow(); // re-render on change
290        });
291
292        wrapper.appendChild(label);
293        wrapper.appendChild(select);
294        wrapper.appendChild(timeDisplay);
295        
296        // Insert timezone selector appropriately
297        const existingTimeDisplay = container.find('.joan-current-time');
298        if (existingTimeDisplay.length > 0) {
299            // Replace the basic time display with enhanced timezone selector
300            existingTimeDisplay.replaceWith(wrapper);
301        } else {
302            // Add at the beginning
303            container[0].insertBefore(wrapper, container[0].firstChild);
304        }
305        
306        // Initial time update
307        updateTimezoneTime();
308        
309        // Update time every minute
310        setInterval(updateTimezoneTime, 60000);
311    }
312
313    // FIX: Add cache clearing function
314    function clearScheduleCache() {
315        showCache = null;
316        cacheTime = 0;
317        console.log('JOAN: Schedule cache cleared');
318    }
319
320    // FIX: Check for admin updates by monitoring page focus
321    function initializeCacheInvalidation() {
322        // Clear cache when page regains focus (user might have made admin changes)
323        let lastFocusTime = Date.now();
324        
325        window.addEventListener('focus', function() {
326            const now = Date.now();
327            // If page was out of focus for more than 30 seconds, clear cache
328            if (now - lastFocusTime > 30000) {
329                clearScheduleCache();
330                console.log('JOAN: Cache cleared due to page refocus after admin activity');
331            }
332        });
333        
334        window.addEventListener('blur', function() {
335            lastFocusTime = Date.now();
336        });
337        
338        // Also periodically clear cache to ensure fresh data
339        setInterval(function() {
340            if (showCache && (Date.now() - cacheTime) > 300000) { // 5 minutes
341                clearScheduleCache();
342            }
343        }, 60000); // Check every minute
344    }
345
346    function getImageConstraints(container, settings) {
347        // Get max width from settings or container data
348        let maxWidth = parseInt(settings.widget_max_width) || parseInt(container.data('max-width')) || 300;
349        const minWidth = 150;
350        
351        // Ensure constraints are within bounds
352        if (maxWidth < minWidth) maxWidth = minWidth;
353        if (maxWidth > 500) maxWidth = 500;
354        
355        // Get container width for responsive sizing
356        const containerWidth = container.width();
357        
358        // Adjust max width based on container size for better responsive behavior
359        if (containerWidth > 0 && containerWidth < maxWidth) {
360            maxWidth = Math.max(containerWidth - 40, minWidth); // Leave some padding
361        }
362        
363        return { minWidth, maxWidth, containerWidth };
364    }
365
366    function formatTime(isoString, settings, timezone = null) {
367        if (!isoString) return '';
368        
369        const date = new Date(isoString);
370        const timeFormat = settings.time_format || '12';
371        const selectedTimezone = timezone || getPreferredTimezone();
372        
373        if (timeFormat === '24') {
374            return date.toLocaleTimeString([], { 
375                timeZone: selectedTimezone,
376                hour: '2-digit', 
377                minute: '2-digit', 
378                hour12: false 
379            });
380        } else {
381            return date.toLocaleTimeString([], { 
382                timeZone: selectedTimezone,
383                hour: 'numeric', 
384                minute: '2-digit', 
385                hour12: true 
386            });
387        }
388    }
389
390    function formatTimeRange(startIso, endIso, settings, timezone = null) {
391        const startText = formatTime(startIso, settings, timezone);
392        const endText = formatTime(endIso, settings, timezone);
393        return startText && endText ? `${startText} - ${endText}` : '';
394    }
395
396    function handleImageSizing(imageUrl, showName, container, settings, c
396allback) {
397        // Check if images should be shown
398        if (settings.show_jock_image !== '1' || !imageUrl) {
399            callback('', '');
400            return;
401        }
402
403        const constraints = getImageConstraints(container, settings);
404        const img = new Image();
405        
406        img.onload = function() {
407            const naturalWidth = this.naturalWidth;
408            const naturalHeight = this.naturalHeight;
409            let imageHtml = '';
410            let imagePosition = '';
411            
412            // Calculate optimal display size
413            let displayWidth = naturalWidth;
414            let displayHeight = naturalHeight;
415            
416            // Constrain to our limits
417            if (displayWidth > constraints.maxWidth) {
418                displayHeight = (displayHeight * constraints.maxWidth) / displayWidth;
419                displayWidth = constraints.maxWidth;
420            }
421            if (displayWidth < constraints.minWidth) {
422                displayHeight = (displayHeight * constraints.minWidth) / displayWidth;
423                displayWidth = constraints.minWidth;
424            }
425
426            // Only use large image placement for very large images
427            if (naturalWidth >= 350 && constraints.containerWidth > 400) {
428                // Very large image: place under time, above "up next"
429                imageHtml = `<div class="joan-image-large">
430                    <img class="joan-thumb joan-thumb-large" 
431                         src="${imageUrl}" 
432                         alt="${showName}" />
433                </div>`;
434                imagePosition = 'bottom';
435            } else {
436                // All other images: standard placement under show info
437                imageHtml = `<div class="joan-image-standard">
438                    <img class="joan-thumb" 
439                         src="${imageUrl}" 
440                         alt="${showName}" />
441                </div>`;
442                imagePosition = 'standard';
443            }
444
445            callback(imageHtml, imagePosition);
446        };
447
448        img.onerror = function() {
449            // If image fails to load, use standard placement
450            const imageHtml = `<div class="joan-image-standard">
451                <img class="joan-thumb" 
452                     src="${imageUrl}" 
453                     alt="${showName}" />
454            </div>`;
455            callback(imageHtml, 'standard');
456        };
457
458        img.src = imageUrl;
459    }
460
461    function applyStatusStyling($container, response) {
462        // Remove any existing status classes
463        $container.removeClass('joan-status-active joan-status-suspended joan-status-off_air');
464        
465        // Get schedule status from response
466        const status = response.schedule_status || 'active';
467        const statusType = response.status_type || 'active';
468        
469        // Add appropriate status class
470        if (status !== 'active') {
471            $container.addClass('joan-status-' + status);
472            
473            // Add body class for global styling
474            $('body').addClass('joan-schedule-' + status);
475        } else {
476            // Remove body classes when active
477            $('body').removeClass('joan-schedule-suspended joan-schedule-off_air');
478        }
479        
480        return { status, statusType };
481    }
482
483    function renderStatusMessage(response, $container, settings) {
484        const { status } = applyStatusStyling($container, response);
485        
486        // Get the appropriate message (with fallback to localized default)
487        let message = response.off_air_message || joan_ajax.i18n.off_air_default;
488        
489        // Add status icon based on status type
490        let statusIcon = '';
491        if (status === 'suspended') {
492            statusIcon = '<span class="joan-status-icon">⏸</span>';
493        } else if (status === 'off_air') {
494            statusIcon = '<span class="joan-status-icon">🔴</span>';
495        }
496        
497        // Show upcoming show if enabled and available
498        if (settings.show_next_show === '1' && response.upcoming_show && settings.joan_jock_only_mode !== '1') {
499            const upcoming = response.upcoming_show;
500            const timeRange = formatTimeRange(upcoming.start_iso, upcoming.end_iso, settings, getPreferredTimezone());
501            // TRANSLATION: Use localized string
502            message += `<div class="upcoming-show">
503                <strong>${joan_ajax.i18n.up_next}</strong> ${upcoming.show_name}
504                ${timeRange ? `<span class="upcoming-time">${timeRange}</span>` : ''}
505            </div>`;
506        }
507        
508        $container.html(`<div class="joan-status-message">${statusIcon}${message}</div>`);
509    }
510
511    function renderShow(response, $container) {
512        // Apply max width setting to container
513        const settings = response.settings || joan_ajax.settings || {};
514        const maxWidth = parseInt(settings.widget_max_width) || 300;
515        $container.css('max-width', maxWidth + 'px');
516
517        // Apply center title setting
518        const centerTitle = settings.joan_center_widget_title === '1';
519        if (centerTitle) {
520            $container.addClass('joan-center-title');
521        } else {
522            $container.removeClass('joan-center-title');
523        }
524
525        // Check for jock-only mode
526        const jockOnlyMode = settings.joan_jock_only_mode === '1';
527        if (jockOnlyMode) {
528            $container.addClass('joan-jock-only-mode');
529        } else {
530            $container.removeClass('joan-jock-only-mode');
531        }
532
533        // Apply dark mode if appropriate
534        const isDarkMode = document.body.classList.contains('joan-dark-mode');
535        if (isDarkMode) {
536            $container.addClass('joan-dark-mode');
537        } else {
538            $container.removeClass('joan-dark-mode');
539        }
540
541        // Check for off-air conditions or non-active status
542        if (response.schedule_status !== 'active' || !response.current_show) {
543            renderStatusMessage(response, $container, settings);
544            
545            // Show timezone selector based on settings, but not in jock-only mode or off-air status
546            const allowTimezoneSelector = settings.joan_allow_timezone_selector === '1';
547            if (!jockOnlyMode && response.schedule_status !== 'off_air' && allowTimezoneSelector) {
548                buildTimezoneDropdown(getPreferredTimezone(), $container, settings);
549            }
550            
551            // Add dark mode toggle if appropriate
552            const allowOverride = settings.joan_dark_mode_override === '1';
553            const darkModeSetting = settings.joan_dark_mode || 'auto';
554            if (allowOverride && darkModeSetting !== 'disabled') {
555                addDarkModeToggles(isDarkMode);
556            }
557            
558            return;
559        }
560
561        // Apply active status styling
562        applyStatusStyling($container, response);
563
564        const show = response.current_show;
565        const upcoming = response.upcoming_show;
566
567        const selectedTimezone = getPreferredTimezone();
568        const timeRange = formatTimeRange(show.start_iso, show.end_iso, settings, selectedTimezone);
569        const jock = show.dj_name || "";
570        const image = show.image_url || "";
571        const link = show.link_url || "";
572
573        // Handle jock-only mode rendering
574        if (jockOnlyMode) {
575            let html = '';
576            
577            // In jock-only mode, only show image and time
578            if (image && settings.show_jock_image === '1') {
579                handleImageSizing(image, show.show_name, $container, settings, function(imageHtml, imagePosition) {
580                    let jockOnlyHtml = `<div class="joan-show-info">
581                        ${timeRange ? `<div class="show-time">${timeRange}</div>` : ""}
582                    </div>`;
583
584                    // If a link URL is provided, wrap the image in an anchor so that clicking
585                    // the image navigates to the same destination as the host/show link.
586                    if (link) {
587                        // Replace the <img> tag with a linked version. This regex captures the
588                        // entire img tag (including attributes) and wraps it in an <a> element.
589                        imageHtml = imageHtml.replace(/(<img[^>]*>)/i, `<a href="${link}" target="_blank">$1</a>`);
590                    }
591
592                    // Add image after time (simplified for jock-only mode)
593                    jockOnlyHtml += imageHtml;
594                    
595                    $container.html(jockOnlyHtml);
596                    
597                    // Add timezone selector if enabled (this will include time in the selector area)
598                    const allowTimezoneSelector = settings.joan_allow_timezone_selector === '1';
599                    if (allowTimezoneSelector) {
600                        buildTimezoneDropdown(getPreferredTimezone(), $container, settings);
601                    }
602                    
603                    // Add dark mode toggle
604                    const allowOverride = settings.joan_dark_mode_override === '1';
605                    const darkModeSetting = settings.joan_dark_mode || 'auto';
606                    if (allowOverride && darkModeSetting !== 'disabled') {
607                        addDarkModeToggles(isDarkMode);
608                    }
609                });
610            } else {
611                // No image, just time
612                html = `<div class="joan-show-info">
613                    ${timeRange ? `<div class="show-time">${timeRange}</div>` : ""}
614                </div>`;
615                
616                $container.html(html);
617                
618                // Add timezone selector if enabled
619                const allowTimezoneSelector = settings.joan_allow_timezone_selector === '1';
620                if (allowTimezoneSelector) {
621                    buildTimezoneDropdown(getPreferredTimezone(), $container, settings);
622                }
623                
624                // Add dark mode toggle
625                const allowOverride = settings.joan_dark_mode_override === '1';
626                const darkModeSetting = settings.joan_dark_mode || 'auto';
627                if (allowOverride && darkModeSetting !== 'disabled') {
628                    addDarkModeToggles(isDarkMode);
629                }
630            }
631            
632            return;
633        }
634
635        // Regular mode rendering with FIXED image positioning
636        handleImageSizing(image, show.show_name, $container, settings, function(imageHtml, imagePosition) {
637            let html = '';
638
639            // FIXED: All images now go under show info, no right positioning
640            // Build show and host/jock display using new field usage and link assignment logic.
641            const linkAssignment = settings.joan_link_assignment || 'jock_name';
642            const jockFieldLabel = settings.joan_jock_field_label || '';
643            let displayJock = '';
644            if (jockFieldLabel && jockFieldLabel.trim() !== '') {
645                // If a global host name is provided, it overrides the per-show jock name
646                displayJock = jockFieldLabel.trim();
647            } else if (jock && jock.trim() !== '') {
648                // Otherwise use the jock name from the show record
649                displayJock = jock.trim();
650            }
651
652            // Escape the show title for safe HTML output
653            const showTitleEscaped = escapeHtml(show.show_name || '');
654            let showTitleHtml = showTitleEscaped;
655            // Apply link to show title if link assignment is set to show_title or both
656            if (link && (linkAssignment === 'show_title' || linkAssignment === 'both')) {
657                showTitleHtml = `<a href="${link}" target="_blank">${showTitleEscaped}</a>`;
658            }
659
660            // Build jock/host HTML if a display name exists
661            let jockHtml = '';
662            if (displayJock) {
663                const jockEscaped = escapeHtml(displayJock);
664                if (link && (linkAssignment === 'jock_name' || linkAssignment === 'both')) {
665                    jockHtml = `<a href="${link}" target="_blank">${jockEscaped}</a>`;
666                } else {
667                    jockHtml = jockEscaped;
668                }
669            }
670
671            html = `<div class="joan-show-info">
672                <h3 class="current-show">${showTitleHtml}</h3>
673                ${jockHtml ? `<div class="joan-jock">${jockHtml}</div>` : ""}
674                ${timeRange ? `<div class="show-time">${timeRange}</div>` : ""}
675            </div>`;
676
677            // Append the image after show info (standard placement)
678            html += imageHtml;
679
680            // Add upcoming show section if enabled
681            if (settings.show_next_show === '1' && upcoming) {
682                const upTimeRange = formatTimeRange(upcoming.start_iso, upcoming.end_iso, settings, selectedTimezone);
683                // TRANSLATION: Use localized string
684                html += `
685                    <div class="upcoming-show">
686                        <strong>${joan_ajax.i18n.up_next}</strong> ${upcoming.show_name}
687                        ${upTimeRange ? `<span class="upcoming-time">${upTimeRange}</span>` : ''}
688                    </div>
689                `;
690            }
691
692            $container.html(html);
693            
694            // Add timezone selector if enabled (this provides timezone selection without duplicate time)
695            const allowTimezoneSelector = settings.joan_allow_timezone_selector === '1';
696            if (allowTimezoneSelector) {
697                buildTimezoneDropdown(getPreferredTimezone(), $container, settings);
698            }
699            
700            // Add dark mode toggle if appropriate
701            const allowOverride = settings.joan_dark_mode_override === '1';
702            const darkModeSetting = settings.joan_dark_mode || 'auto';
703            if (allowOverride && darkModeSetting !== 'disabled') {
704                addDarkModeToggles(isDarkMode);
705            }
706        });
707        
708        // Trigger custom event to update local time displays
709        const event = new CustomEvent('joanSettingsUpdated');
710        document.dispatchEvent(event);
711    }
712
713    function fetchCurrentShow(force = false) {
714        const now = Date.now();
715        
716        // Stop retrying if too many errors occurred
717        if (errorCount >= maxErrors && !force) {
718            console.warn('JOAN: Stopped retrying due to repeated failures. Refresh page to retry.');
719            return;
720        }
721        
722        // Use cache if available and not expired (for instant loading)
723        if (!force && showCache && (now - cacheTime) < CACHE_DURATION && errorCount === 0) {
724            $('.joan-now-playing').each(function() {
725                renderShow(showCache, $(this));
726            });
727            return;
728        }
729
730        $('.joan-now-playing').each(function() {
731            const $container = $(this);
732            
733            // Show loading only for slow connections (after 1 second for first try, 3 seconds for retries)
734            const loadingDelay = errorCount > 0 ? 3000 : 1000;
735            const loadingTimer = setTimeout(() => {
736                if ($container.html().indexOf('Loading') === -1 && $container.html().indexOf('Unable') === -1) {
737                    // TRANSLATION: Use localized strings
738                    const loadingText = errorCount > 0 ? joan_ajax.i18n.retrying : joan_ajax.i18n.loading;
739                    $container.html(`<div class="joan-loading">${loadingText}</div>`);
740                }
741            }, loadingDelay);
742            
743            // Rocket Loader compatibility - delay AJAX request
744            const ajaxDelay = window.rocketLoaderDelay || (typeof rocket_loader !== 'undefined' ? 1000 : 0);
745            
746            setTimeout(() => {
747                // Check if joan_ajax is properly loaded
748                if (typeof joan_ajax === 'undefined' || !joan_ajax.ajaxurl) {
749                    console.error('JOAN: joan_ajax not loaded properly');
750                    // TRANSLATION: Use localized strings
751                    $container.html('<div class="joan-error">' + joan_ajax.i18n.config_error + ' <a href="#" onclick="window.location.reload(); return false;">' + joan_ajax.i18n.refresh_page + '</a></div>');
752                    return;
753                }
754                
755                console.log('JOAN: Sending AJAX request to:', joan_ajax.ajaxurl);
756                
757                $.ajax({
758                    url: joan_ajax.ajaxurl,
759                    type: 'POST',
760                    data: {
761                        action: 'show_time_curd',
762                        'crud-action': 'read',
763                        'read-type': 'current'
764                    },
765                    timeout: 30000,
766                    cache: false,
767                    dataType: 'json',
768                    beforeSend: function(xhr, ajaxSettings) {
769                        console.log('JOAN: AJAX request starting...', ajaxSettings.data);
770                        xhr.setRequestHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
771                        xhr.setRequestHeader('Pragma', 'no-cache');
772                        xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
773                    }
774                })
775                .done(function(response) {
776                    console.log('JOAN: AJAX success:', response);
777                    clearTimeout(loadingTimer);
778                    
779                    errorCount = 0;
780                    showCache = response;
781                    cacheTime = now;
782                    
783                    renderShow(response, $container);
784                })
785                .fail(function(xhr, status, error) {
786                    console.error('JOAN: AJAX failed:', {
787                        status: status,
788                        error: error,
789                        responseText: xhr.responseText,
790                        readyState: xhr.readyState,
791                        statusCode: xhr.status
792                    });
793                    
794                    clearTimeout(loadingTimer);
795                    errorCount++;
796                    
797                    // TRANSLATION: Use localized strings for all error messages
798                    let errorMessage = joan_ajax.i18n.unable_to_load + ' ';
799                    if (status === 'timeout') {
800                        if (errorCount === 1) {
801                            errorMessage = joan_ajax.i18n.server_slow + ' ';
802                        } else {
803                            errorMessage = joan_ajax.i18n.connection_timeout + ` (attempt ${errorCount}/${maxErrors}). `;
804                        }
805                    } else if (status === 'error') {
806                        if (xhr.status === 0) {
807                            errorMessage = joan_ajax.i18n.request_blocked + ' ';
808                        } else if (xhr.status === 403) {
809                            errorMessage = joan_ajax.i18n.access_denied + ' ';
810                        } else if (xhr.status === 500) {
811                            errorMessage = joan_ajax.i18n.server_internal_error + ' ';
812                        } else {
813                            errorMessage = joan_ajax.i18n.server_error + ` (${xhr.status}). `;
814                        }
815                    } else if (status === 'parsererror') {
816                        errorMessage = joan_ajax.i18n.invalid_response + ' ';
817                    }
818                    
819                    if (errorCount >= maxErrors) {
820                        // TRANSLATION: Use localized string for automatic retries stopped
821                        errorMessage += '<strong>' + joan_ajax.i18n.automatic_retries_stopped + '</strong> ';
822                    }
823                    
824                    errorMessage += '<a href="#" onclick="window.location.reload(); return false;">' + joan_ajax.i18n.refresh_page + '</a>';
825                    
826                    $container.html(`<div class="joan-error">${errorMessage}</div>`);
827                });
828            }, ajaxDelay);
829        });
830    }
831
832    // ============================================================================
833    // FIXED: separateScheduleByDays() - Now works with translated day names
834    // ============================================================================
835    function separateScheduleByDays() {
836        $('table.joan-schedule').each(function() {
837            const $table = $(this);
838            
839            if ($table.hasClass('joan-schedule-processed')) {
840                return;
841            }
842            
843            // Check if this is a filtered schedule (should not be separated by days)
844            const isFilteredSchedule = 
845                $table.attr('data-filter-day') !== undefined ||
846                $table.data('filter-day') !== undefined ||
847                $table.hasClass('joan-filtered-schedule') ||
848                $table.closest('.joan-filtered-schedule').length > 0 ||
849                $table.closest('[data-filter-day]').length > 0 ||
850                $table.find('tbody tr').length <= 5 &&
851                (function() {
852                    const days = new Set();
853                    $table.find('tbody tr').each(function() {
854                        const dayText = $(this).find('td').eq(1).text().trim();
855                        if (dayText) days.add(dayText);
856                    });
857                    return days.size <= 1;
858                })();
859            
860            if (isFilteredSchedule) {
861                console.log('JOAN: Skipping day separation for filtered schedule table');
862                $table.addClass('joan-schedule-processed');
863                return;
864            }
865            
866            $table.addClass('joan-schedule-processed');
867            
868            const $rows = $table.find('tbody tr');
869            if ($rows.length === 0) {
870                return;
871            }
872            
873            // CRITICAL FIX: Get translated day names from localized script
874            const translatedDays = joan_ajax.i18n && joan_ajax.i18n.days ? joan_ajax.i18n.days : {
875                'Sunday': 'Sunday',
876                'Monday': 'Monday',
877                'Tuesday': 'Tuesday',
878                'Wednesday': 'Wednesday',
879                'Thursday': 'Thursday',
880                'Friday': 'Friday',
881                'Saturday': 'Saturday'
882            };
883            
884            // Build dayGroups using English keys
885            const englishDays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
886            const dayGroups = {};
887            
888            englishDays.forEach(day => {
889                dayGroups[day] = [];
890            });
891            
892            // Create reverse mapping: translated name -> English name
893            const translatedToEnglish = {};
894            Object.keys(translatedDays).forEach(englishDay => {
895                const translatedDay = translatedDays[englishDay];
896                translatedToEnglish[translatedDay] = englishDay;
897            });
898            
899            const $headerRow = $table.find('thead tr').clone();
900            
901            // FIXED: Process rows and group by English day name
902            $rows.each(function() {
903                const $row = $(this);
904                
905                // Try to get day from data attribute first (always English)
906                let englishDay = $row.attr('data-day') || $row.data('day');
907                
908                if (!englishDay) {
909                    // Fallback: get from table cell and translate back to English
910                    const dayCell = $row.find('td').eq(1);
911                    const displayDay = dayCell.text().trim();
912                    englishDay = translatedToEnglish[displayDay] || displayDay;
913                }
914                
915                if (dayGroups.hasOwnProperty(englishDay)) {
916                    dayGroups[englishDay].push($row);
917                }
918            });
919            
920            // Check if we need day separation
921            const daysWithShows = englishDays.filter(day => dayGroups[day].length > 0);
922            if (daysWithShows.length <= 1) {
923                console.log('JOAN: Skipping day separation - only one day has shows');
924                return;
925            }
926            
927            const $container = $('<div class="joan-schedule-container"></div>');
928            const uniqueId = 'joan-day-filter-' + Math.random().toString(36).substr(2, 9);
929            
930            const i18n = joan_ajax.i18n || {};
931            
932            // Build day filter dropdown with translated names
933            const $controls = $(`
934                <div class="joan-schedule-controls">
935                    <label for="${uniqueId}">${i18n.filter_by_day || 'Filter by day:'}</label>
936                    <select id="${uniqueId}" class="joan-day-filter">
937                        <option value="">${i18n.all_days || 'All Days'}</option>
938                    </select>
939                </div>
940            `);
941            
942            const $select = $controls.find('select');
943            
944            // Add options using translated day names
945            englishDays.forEach(englishDay => {
946                if (dayGroups[englishDay].length > 0) {
947                    const translatedName = translatedDays[englishDay] || englishDay;
948                    $select.append(`<option value="${englishDay}">${translatedName}</option>`);
949                }
950            });
951            
952            $container.append($controls);
953            
954            const dayClassMap = {
955                'Sunday': 'sunday',
956                'Monday': 'monday',
957                'Tuesday': 'tuesday',
958                'Wednesday': 'wednesday',
959                'Thursday': 'thursday',
960                'Friday': 'friday',
961                'Saturday': 'saturday'
962            };
963            
964            const dayEmojis = {
965                'Sunday': 'He',
966                'Monday': 'Ag', 
967                'Tuesday': 'Fe',
968                'Wednesday': 'Hg',
969                'Thursday': 'Au',
970                'Friday': 'Cu',
971                'Saturday': 'Pb'
972            };
973            
974            const dayTooltips = {
975                'Sunday': i18n.tooltip_sunday || 'Helium (He) - Second most abundant element in the universe, powers the sun through nuclear fusion',
976                'Monday': i18n.tooltip_monday || 'Silver (Ag) - Precious metal with highest electrical conductivity, reflects light like moonlight',
977                'Tuesday': i18n.tooltip_tuesday || 'Iron (Fe) - Most abundant metal on Earth, essential for steel and human blood', 
978                'Wednesday': i18n.tooltip_wednesday || 'Mercury (Hg) - Only metal that is liquid at room temperature, used in thermometers',
979                'Thursday': i18n.tooltip_thursday || 'Gold (Au) - Noble metal that never tarnishes, symbol of value and permanence',
980                'Friday': i18n.tooltip_friday || 'Copper (Cu) - Reddish metal essential for electrical wiring, naturally antibacterial',
981                'Saturday': i18n.tooltip_saturday || 'Lead (Pb) - Dense metal historically used for protection, now known to be toxic'
982            };
983            
984            // Build day sections with translated names
985            englishDays.forEach(englishDay => {
986                const showsForDay = dayGroups[englishDay];
987                if (showsForDay.length === 0) return;
988                
989                const translatedDayName = translatedDays[englishDay] || englishDay;
990                const dayClass = dayClassMap[englishDay] || englishDay.toLowerCase();
991                const emoji = dayEmojis[englishDay] || '📅';
992                const scheduleLabel = i18n.schedule || 'Schedule';
993                
994                const $daySection = $(`
995                    <div class="joan-schedule-day-section" data-day="${englishDay}">
996                        <div class="joan-schedule-day-header joan-${dayClass}-header" title="${dayTooltips[englishDay] || ''}">
997                            [${emoji}] ${translatedDayName} ${scheduleLabel}
998                        </div>
999                    </div>
1000                `);
1001                
1002                // Remove emoji if setting is disabled
1003                const showDayEmoji = (typeof joan_ajax !== 'undefined' && joan_ajax.settings && joan_ajax.settings.joan_show_day_emoji === '1');
1004                if (!showDayEmoji) {
1005                    const headerElem = $daySection.find('.joan-schedule-day-header');
1006                    headerElem.removeAttr('title');
1007                    headerElem.text(`${translatedDayName} ${scheduleLabel}`);
1008                }
1009                
1010                if (showsForDay.length > 0) {
1011                    const $dayTable = $(`
1012                        <table class="joan-schedule-day-table joan-${dayClass}-table">
1013                            <thead></thead>
1014                            <tbody></tbody>
1015                        </table>
1016                    `);
1017                    
1018                    const $dayHeader = $headerRow.clone();
1019                   // Only remove Day column if it exists (check if second column is "Day")
1020                   const secondHeader = $dayHeader.find('th').eq(1);
1021                   if (secondHeader.length && secondHeader.text().trim() === (i18n.day || 'Day')) {
1022                   secondHeader.remove();
1023                  }
1024                  $dayTable.find('thead').append($dayHeader);
1025                    
1026                    showsForDay.sort((a, b) => {
1027                        const timeA = $(a).attr('data-time') || $(a).data('time') || '';
1028                        const timeB = $(b).attr('data-time') || $(b).data('time') || '';
1029                        return timeA.localeCompare(timeB);
1030                    });
1031                    
1032                    showsForDay.forEach($row => {
1033                         const $dayRow = $row.clone();
1034                          // Only remove Day cell if the header has a Day column
1035                         const hasDayColumn = $dayHeader.find('th').filter(function() {
1036                               return $(this).text().trim() === (i18n.day || 'Day');
1037                         }).length > 0;
1038    
1039                         if (hasDayColumn) {
1040                         $dayRow.find('td').eq(1).remove();
1041                        }
1042                         $dayTable.find('tbody').append($dayRow);
1043                       });
1044                    
1045                    $daySection.append($dayTable);
1046                } else {
1047                    $daySection.append(`
1048                        <div class="joan-schedule-empty-day">
1049                            ${i18n.no_shows_scheduled_for || 'No shows scheduled for'} ${translatedDayName}
1050                        </div>
1051                    `);
1052                }
1053                
1054                $container.append($daySection);
1055            });
1056            
1057            // Replace original table
1058            $table.replaceWith($container);
1059            
1060            // Apply dark mode only if explicitly enabled
1061            // const isDarkMode = document.body.classList.contains('joan-dark-mode');
1062            // if (isDarkMode) {
1063            // setTimeout(() => {
1064            // applyDarkMode(true);
1065        
1066            // $container.addClass('joan-dark-mode');
1067            // $container.removeClass('joan-light-mode');
1068          //}, 100);
1069     //} else {
1070          // Don't force light mode class - let theme styles apply naturally
1071         //$container.removeClass('joan-dark-mode joan-light-mode');
1072    //}
1073            
1074            // Day filtering event handler
1075            $container.find('.joan-day-filter').on('change', function() {
1076                const selectedDay = $(this).val();
1077                
1078                $container.find('.joan-schedule-day-section').each(function() {
1079                    const $daySection = $(this);
1080                    const dayName = $daySection.data('day');
1081                    
1082                    if (selectedDay === '' || selectedDay === dayName) {
1083                        $daySection.show();
1084                    } else {
1085                        $daySection.hide();
1086                    }
1087                });
1088            });
1089        });
1090    }
1091
1092    const initialDelay = typeof rocket_loader !== 'undefined' || window.rocketLoaderDelay ? 2000 : 500;
1093    setTimeout(() => {
1094        initializeDarkMode();
1095        initializeCacheInvalidation();
1096        fetchCurrentShow();
1097        separateScheduleByDays();
1098        
1099        setTimeout(() => {
1100            const isDarkMode = document.body.classList.contains('joan-dark-mode');
1101            applyDarkMode(isDarkMode);
1102        }, 300);
1103    }, initialDelay);
1104    
1105    setInterval(() => {
1106        if (errorCount < maxErrors) {
1107            fetchCurrentShow(true);
1108        }
1109    }, 600000);
1110    
1111    let resizeTimeout;
1112    $(window).on('resize', function() {
1113        clearTimeout(resizeTimeout);
1114        resizeTimeout = setTimeout(function() {
1115            if (showCache) {
1116                $('.joan-now-playing').each(function() {
1117                    renderShow(showCache, $(this));
1118                });
1119            }
1120        }, 100);
1121    });
1122
1123    window.joanFetchCurrentShow = function() {
1124        errorCount = 0;
1125        clearScheduleCache();
1126        fetchCurrentShow(true);
1127    };
1128    
1129    window.joanSeparateScheduleByDays = separateScheduleByDays;
1130    
1131    window.joanDarkMode = {
1132        initialize: initializeDarkMode,
1133        toggle: toggleDarkMode,
1134        apply: applyDarkMode
1135    };
1136    
1137    window.joanClearCache = clearScheduleCache;
1138    
1139    $(document).on('click', '.joan-error a[onclick*="reload"]', function(e) {
1140        e.preventDefault();
1141        const $container = $(this).closest('.joan-now-playing');
1142        // TRANSLATION: Use localized string
1143        $container.html('<div class="joan-loading">' + joan_ajax.i18n.refreshing + '</div>');
1144        window.joanFetchCurrentShow();
1145        return false;
1146    });
1147    
1148    window.joanStatusUtils = {
1149        applyStatusStyling: applyStatusStyling,
1150        renderStatusMessage: renderStatusMessage
1151    };
1152    
1153    if (typeof rocket_loader !== 'undefined' || window.rocketLoaderDelay) {
1154        console.log('JOAN: Cloudflare Rocket Loader detected, using compatibility mode');
1155    }
1156});
1157
1158function initializeLocalTime() {
1159    function updateCurrentTime() {
1160        document.querySelectorAll('.joan-current-time').forEach(function(element) {
1161            const timeSettings = (window.joan_ajax && window.joan_ajax.settings) || {};
1162            const container = element.closest('.joan-now-playing');
1163            
1164            const hasTimezoneSelector = container && container.querySelector('.joan-timezone-selector');
1165            if (hasTimezoneSelector) {
1166                element.style.display = 'none';
1167                return;
1168            }
1169
1170            const allowTimezoneSelector = timeSettings.joan_allow_timezone_selector === '1';
1171            if (!allowTimezoneSelector) {
1172                element.style.display = 'none';
1173                return;
1174            }
1175
1176            if (allowTimezoneSelector) {
1177                element.style.display = 'none';
1178                return;
1179            }
1180        });
1181    }
1182    
1183    updateCurrentTime();
1184    setInterval(updateCurrentTime, 60000);
1185    document.addEventListener('joanSettingsUpdated', updateCurrentTime);
1186    
1187    document.querySelectorAll('.show-time[data-time]').forEach(function(el) {
1188        const time = el.dataset.time;
1189        if (time) {
1190            const timeSettings = window.joan_ajax ? window.joan_ajax.settings : {};
1191            const timeFormat = timeSettings.time_format || '12';
1192            
1193            let local;
1194            if (timeFormat === '24') {
1195                local = new Date(time).toLocaleTimeString(undefined, {
1196                    hour: '2-digit',
1197                    minute: '2-digit',
1198                    hour12: false,
1199                    timeZoneName: 'short'
1200                });
1201            } else {
1202                local = new Date(time).toLocaleTimeString(undefined, {
1203                    hour: 'numeric',
1204                    minute: '2-digit',
1205                    hour12: true,
1206                    timeZoneName: 'short'
1207                });
1208            }
1209            
1210            el.textContent = local;
1211        }
1212    });
1213}
1214
1215function initializeScheduleHighlight() {
1216    const dayNames = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
1217    const today = dayNames[new Date().getDay()];
1218
1219    document.querySelectorAll('.joan-schedule tbody tr').forEach(row => {
1220        const dayCell = row.querySelector('td:nth-child(2)');
1221        if (dayCell && dayCell.textContent.trim() === today) {
1222            row.classList.add('joan-today-highlight');
1223        }
1224    });
1225
1226    document.querySelectorAll('.joan-schedule-day-section').forEach(section => {
1227        if (section.dataset.day === today) {
1228            section.classList.add('joan-today-highlight');
1229        }
1230    });
1231
1232    document.querySelectorAll('.joan-day-filter').forEach(filter => {
1233        filter.addEventListener('change', function () {
1234            const selected = this.value;
1235            const parentTable = this.closest('.joan-schedule-container')?.querySelector('.joan-schedule') || 
1236                               document.querySelector('.joan-schedule');
1237            
1238            if (parentTable) {
1239                parentTable.querySelectorAll('tbody tr').forEach(row => {
1240                    const day = row.querySelector('td:nth-child(2)')?.textContent.trim();
1241                    row.style.display = (selected === '' || selected === day) ? '' : 'none';
1242                });
1243            }
1244        });
1245    });
1246}
1247
1248if (document.readyState === 'loading') {
1249    document.addEventListener('DOMContentLoaded', initializeLocalTime);
1250    document.addEventListener('DOMContentLoaded', initializeScheduleHighlight);
1251} else {
1252    setTimeout(initializeLocalTime, typeof rocket_loader !== 'undefined' ? 1500 : 100);
1253    setTimeout(initializeScheduleHighlight, typeof rocket_loader !== 'undefined' ? 1500 : 100);
1254}
1255
1256function initializePageBuilders() {
1257    function initElementorIntegration() {
1258        if (window.elementorFrontend && window.elementorFrontend.hooks && window.elementorFrontend.hooks.addAction) {
1259            elementorFrontend.hooks.addAction('frontend/element_ready/global', function($scope) {
1260                if ($scope.find('.joan-now-playing').length) {
1261                    if (window.joanFetchCurrentShow) {
1262                        setTimeout(window.joanFetchCurrentShow, 500);
1263                    }
1264                }
1265                
1266                if ($scope.find('table.joan-schedule').length) {
1267                    if (window.joanSeparateScheduleByDays) {
1268                        setTimeout(window.joanSeparateScheduleByDays, 500);
1269                    }
1270                }
1271            });
1272        }
1273    }
1274
1275    function initVCIntegration() {
1276        if (window.vc_js && typeof jQuery !== 'undefined') {
1277            jQuery(document).on('vc_reload', function() {
1278                setTimeout(function() {
1279                    if (window.joanFetchCurrentShow) {
1280                        window.joanFetchCurrentShow();
1281                    }
1282                    if (window.joanSeparateScheduleByDays) {
1283                        window.joanSeparateScheduleByDays();
1284                    }
1285                    if (window.joanDarkMode) {
1286                        window.joanDarkMode.initialize();
1287                    }
1288                }, 500);
1289            });
1290        }
1291    }
1292
1293    try {
1294        initElementorIntegration();
1295        initVCIntegration();
1296    } catch (error) {
1297        console.log('JOAN: Page builder integration not available:', error.message);
1298    }
1299
1300    let checkCount = 0;
1301    const checkInterval = setInterval(() => {
1302        checkCount++;
1303        
1304        try {
1305            if (window.elementorFrontend && !window.joanElementorInit) {
1306                initElementorIntegration();
1307                window.joanElementorInit = true;
1308            }
1309            
1310            if (window.vc_js && !window.joanVCInit) {
1311                initVCIntegration();
1312                window.joanVCInit = true;
1313            }
1314        } catch (error) {
1315            // Silent
1316        }
1317        
1318        if (checkCount > 10) {
1319            clearInterval(checkInterval);
1320        }
1321    }, 500);
1322}
1323
1324if (document.readyState === 'loading') {
1325    document.addEventListener('DOMContentLoaded', function() {
1326        setTimeout(initializePageBuilders, typeof rocket_loader !== 'undefined' ? 2000 : 1000);
1327    });
1328} else {
1329    setTimeout(initializePageBuilders, typeof rocket_loader !== 'undefined' ? 2500 : 1000);
1330}
1331
1332// ============================================================================
1333// WPBAKERY DYNAMIC CONTENT FIX - v6.0.9
1334// Ensure dropdowns are visible using CSS classes (no inline styles)
1335// ============================================================================
1336(function() {
1337    if (typeof jQuery === 'undefined') return;
1338    
1339    const $ = jQuery;
1340    
1341    // Simple check to ensure elements are shown (uses classes only)
1342    let checkCount = 0;
1343    const checkInterval = setInterval(function() {
1344        checkCount++;
1345        
1346        // Add a class to force visibility if needed
1347        $('.joan-timezone-selector:hidden').addClass('joan-force-visible').show();
1348        $('.joan-schedule-controls:hidden').addClass('joan-force-visible').show();
1349        
1350        if (checkCount >= 10) {
1351            clearInterval(checkInterval);
1352        }
1353    }, 1000);
1354    
1355    console.log('JOAN v6.0.9: WPBakery dropdown visibility check active');
1356})();

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.