PageSourceSearch

https://britishfantasysociety.org/wp-content/plugins/mage-eventpre…/helper/calendar/calendar.js?ver=1790242431

js britishfantasysociety.org collected 2026-09-24 09:34:19 UTC 75,350 bytes, 2,136 lines download raw bytes

1/**
2 * MEP Event Calendar - Frontend JavaScript
3 * 
4 * Initializes FullCalendar instances, handles AJAX event loading,
5 * tooltips, search, and category filtering.
6 */
7(function($) {
8    'use strict';
9
10    // Store calendar instances
11    var calendars = {};
12    var stockRequestCache = {};
13    var tooltipPointerState = {};
14
15    function normalizeCalendarLocaleCode(value) {
16        var locale = String(value || '').trim();
17
18        if (!locale || locale.toLowerCase() === 'auto') {
19            return '';
20        }
21
22        return locale.replace(/_/g, '-');
23    }
24
25    function resolveCalendarLocaleCode(value) {
26        var locale = normalizeCalendarLocaleCode(value);
27        var docLocale;
28        var browserLocale;
29
30        if (locale) {
31            return locale;
32        }
33
34        docLocale = normalizeCalendarLocaleCode(document.documentElement && document.documentElement.lang ? document.documentElement.lang : '');
35        if (docLocale) {
36            return docLocale;
37        }
38
39        browserLocale = normalizeCalendarLocaleCode(window.navigator && window.navigator.language ? window.navigator.language : '');
40        return browserLocale || '';
41    }
42
43    function getSupportedFullCalendarLocaleCode(localeCode) {
44        var normalized = normalizeCalendarLocaleCode(localeCode);
45        var locales = window.FullCalendar && Array.isArray(window.FullCalendar.globalLocales) ? window.FullCalendar.globalLocales : [];
46        var normalizedLower = normalized.toLowerCase();
47        var baseCode = normalizedLower.split('-')[0];
48        var i;
49        var code;
50
51        if (!normalized || !locales.length) {
52            return '';
53        }
54
55        for (i = 0; i < locales.length; i++) {
56            code = normalizeCalendarLocaleCode(locales[i] && locales[i].code ? locales[i].code : '');
57            if (code && code.toLowerCase() === normalizedLower) {
58                return code;
59            }
60        }
61
62        for (i = 0; i < locales.length; i++) {
63            code = normalizeCalendarLocaleCode(locales[i] && locales[i].code ? locales[i].code : '');
64            if (code && code.toLowerCase() === baseCode) {
65                return code;
66            }
67        }
68
69        return '';
70    }
71
72    function getCalendarIntlLocale(localeCode) {
73        return resolveCalendarLocaleCode(localeCode) || undefined;
74    }
75
76    function formatIntlDate(date, localeCode, options) {
77        var d = normalizeDateInput(date);
78        var locale = getCalendarIntlLocale(localeCode);
79
80        if (!d || isNaN(d.getTime())) {
81            return '';
82        }
83
84        try {
85            return new Intl.DateTimeFormat(locale, options).format(d);
86        } catch (err) {
87            return d.toLocaleDateString(undefined, options);
88        }
89    }
90
91    function formatIntlTime(date, localeCode, options) {
92        var d = normalizeDateInput(date);
93        var locale = getCalendarIntlLocale(localeCode);
94
95        if (!d || isNaN(d.getTime())) {
96            return '';
97        }
98
99        try {
100            return new Intl.DateTimeFormat(locale, options).format(d);
101        } catch (err) {
102            return d.toLocaleTimeString(undefined, options);
103        }
104    }
105
106    function addDays(date, amount) {
107        var d = normalizeDateInput(date);
108
109        if (!d || isNaN(d.getTime())) {
110            return d;
111        }
112
113        d = new Date(d.getTime());
114        d.setDate(d.getDate() + amount);
115        return d;
116    }
117
118    function formatIntlRange(startDate, endDate, localeCode, options) {
119        var locale = getCalendarIntlLocale(localeCode);
120        var start = normalizeDateInput(startDate);
121        var end = normalizeDateInput(endDate);
122        var formatter;
123
124        if (!start || !end || isNaN(start.getTime()) || isNaN(end.getTime())) {
125            return '';
126        }
127
128        try {
129            formatter = new Intl.DateTimeFormat(locale, options);
130            if (typeof formatter.formatRange === 'function') {
131                return formatter.formatRange(start, end);
132            }
133
134            return formatter.format(start) + ' - ' + formatter.format(end);
135        } catch (err) {
136            return formatIntlDate(start, localeCode, options) + ' - ' + formatIntlDate(end, localeCode, options);
137        }
138    }
139
140    function formatCalendarTitle(view, localeCode) {
141        var currentStart;
142        var currentEnd;
143
144        if (!view) {
145            return '';
146        }
147
148        currentStart = normalizeDateInput(view.currentStart || view.activeStart || view.start);
149        currentEnd = normalizeDateInput(view.currentEnd || view.activeEnd || view.end);
150
151        if (view.type === 'dayGridMonth' || view.type === 'listMonth') {
152            return formatIntlDate(currentStart, localeCode, { month: 'long', year: 'numeric' });
153        }
154
155        if (view.type === 'timeGridWeek') {
156            return formatIntlRange(currentStart, addDays(currentEnd, -1), localeCode, {
157                month: 'short',
158                day: 'numeric',
159                year: 'numeric'
160            });
161        }
162
163        if (view.type === 'timeGridDay') {
164            return formatIntlDate(currentStart, localeCode, {
165                weekday: 'long',
166                month: 'long',
167                day: 'numeric',
168                year: 'numeric'
169            });
170        }
171
172        return formatIntlDate(currentStart, localeCode, { month: 'long', year: 'numeric' });
173    }
174
175    function formatCalendarDayHeader(date, viewType, localeCode) {
176        var d = normalizeDateInput(date);
177        var locale = normalizeCalendarLocaleCode(localeCode).toLowerCase();
178        var baseCode = locale.split('-')[0];
179        var localizedWeekdays = {
180            de: ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa']
181        };
182        var weekdayLabel = '';
183
184        if (d && !isNaN(d.getTime()) && localizedWeekdays[baseCode]) {
185            weekdayLabel = localizedWeekdays[baseCode][d.getDay()] || '';
186        }
187
188        if (viewType === 'timeGridWeek' || viewType === 'timeGridDay') {
189            if (weekdayLabel) {
190                return weekdayLabel + ' ' + d.getDate() + '.' + (d.getMonth() + 1) + '.';
191            }
192
193            return formatIntlDate(date, localeCode, {
194                weekday: 'short',
195                month: 'numeric',
196                day: 'numeric'
197            });
198        }
199
200        if (weekdayLabel) {
201            return weekdayLabel;
202        }
203
204        return formatIntlDate(date, localeCode, { weekday: 'short' });
205    }
206
207    function getLocaleUiText(localeCode, key, defaultValue) {
208        var locale = normalizeCalendarLocaleCode(localeCode).toLowerCase();
209        var baseCode = locale.split('-')[0];
210        var translations = {
211            bn: {
212                today: 'আজ',
213                month: 'মাস',
214                week: 'সপ্তাহ',
215                day: 'দিন',
216                list: 'তালিকা',
217                allDay: 'সারাদিন',
218                noEvents: 'কোনও ইভেন্ট নেই',
219                viewAll: 'সব {num}টি ইভেন্ট দেখুন'
220            },
221            de: {
222                today: 'Heute',
223                month: 'Monat',
224                week: 'Woche',
225                day: 'Tag',
226                list: 'Liste',
227                allDay: 'Ganztagig',
228                noEvents: 'Keine Ereignisse',
229                viewAll: 'Alle {num} Ereignisse anzeigen'
230            }
231        };
232
233        if (translations[locale] && translations[locale][key]) {
234            return translations[locale][key];
235        }
236
237        if (translations[baseCode] && translations[baseCode][key]) {
238            return translations[baseCode][key];
239        }
240
241        return defaultValue;
242    }
243
244    function getCalendarButtonText(localeCode, defaults) {
245        return {
246            today: getLocaleUiText(localeCode, 'today', defaults.today),
247            month: getLocaleUiText(localeCode, 'month', defaults.month),
248            week: getLocaleUiText(localeCode, 'week', defaults.week),
249            day: getLocaleUiText(localeCode, 'day', defaults.day),
250            list: getLocaleUiText(localeCode, 'list', defaults.list)
251        };
252    }
253
254    function buildMoreLinkText(localeCode, num) {
255        return getLocaleUiText(localeCode, 'viewAll', 'View all {num} events').replace('{num}', String(num));
256    }
257
258    function updateCalendarToolbarTitle(calendar, localeCode) {
259        var titleEl;
260        var titleText;
261
262        if (!calendar || !calendar.el || !localeCode) {
263            return;
264        }
265
266        titleEl = calendar.el.querySelector('.fc-toolbar-title');
267        titleText = formatCalendarTitle(calendar.view, localeCode);
268
269        if (titleEl && titleText) {
270            titleEl.textContent = titleText;
271        }
272    }
273
274    function updateCalendarDayHeaders(calendar, localeCode) {
275        var cells;
276        var viewType;
277        var baseDate;
278
279        if (!calendar || !calendar.el || !localeCode || !calendar.view) {
280            return;
281        }
282
283        cells = calendar.el.querySelectorAll('.fc-col-header-cell');
284        viewType = calendar.view.type || '';
285        baseDate = calendar.view.activeStart || calendar.view.currentStart || null;
286
287        Array.prototype.forEach.call(cells, function(cell, index) {
288            var target = cell.querySelector('.fc-col-header-cell-cushion') || cell.querySelector('.fc-scrollgrid-sync-inner') || cell;
289            var headerDate = baseDate ? addDays(baseDate, index) : cell.getAttribute('data-date');
290            var label;
291
292            if (!target || !headerDate) {
293                return;
294            }
295
296            label = formatCalendarDayHeader(headerDate, viewType, localeCode);
297            if (label) {
298                target.textContent = label;
299            }
300        });
301    }
302
303    function updateCalendarListHeaders(calendar, localeCode) {
304        var rows;
305        var visibleDates;
306
307        if (!calendar || !calendar.el || !localeCode || !calendar.view || calendar.view.type !== 'listMonth') {
308            return;
309        }
310
311        rows = calendar.el.querySelectorAll('.fc-list-day');
312        visibleDates = getVisibleListHeaderDates(calendar);
313        Array.prototype.forEach.call(rows, function(row) {
314            var rowIndex = Array.prototype.indexOf.call(rows, row);
315            var dateStr = row.getAttribute('data-date') || visibleDates[rowIndex] || '';
316            var dateEl = row.querySelector('.fc-list-day-text');
317            var sideEl = row.querySelector('.fc-list-day-side-text');
318            var dateText;
319            var sideText;
320
321            if (!dateStr) {
322                return;
323            }
324
325            dateText = formatIntlDate(dateStr, localeCode, {
326                month: 'long',
327                day: 'numeric',
328                year: 'numeric'
329            });
330            sideText = formatIntlDate(dateStr, localeCode, {
331                weekday: 'long'
332            });
333
334            if (dateEl && dateText) {
335                dateEl.textContent = dateText;
336            }
337
338            if (sideEl && sideText) {
339                sideEl.textContent = sideText;
340            }
341        });
342    }
343
344    function getVisibleListHeaderDates(calendar) {
345        var map = {};
346        var viewStart;
347        var viewEnd;
348        var events;
349
350        if (!calendar || !calendar.view || typeof calendar.getEvents !== 'function') {
351            return [];
352        }
353
354        viewStart = normalizeDateInput(calendar.view.currentStart || calendar.view.activeStart || calendar.view.start);
355        viewEnd = normalizeDateInput(calendar.view.currentEnd || c
355alendar.view.activeEnd || calendar.view.end);
356        events = calendar.getEvents();
357
358        if (!viewStart || !viewEnd || isNaN(viewStart.getTime()) || isNaN(viewEnd.getTime())) {
359            return [];
360        }
361
362        events.forEach(function(event) {
363            var start = normalizeDateInput(event && event.start ? event.start : null);
364            var end = normalizeDateInput(event && event.end ? event.end : event.start);
365            var cursor;
366            var lastDate;
367
368            if (!start || !end || isNaN(start.getTime()) || isNaN(end.getTime())) {
369                return;
370            }
371
372            if (event.allDay && end.getTime() > start.getTime()) {
373                end = new Date(end.getTime() - 1000);
374            }
375
376            if (end < viewStart || start >= viewEnd) {
377                return;
378            }
379
380            if (start < viewStart) {
381                start = new Date(viewStart.getTime());
382            }
383
384            if (end >= viewEnd) {
385                end = new Date(viewEnd.getTime() - 1000);
386            }
387
388            cursor = new Date(start.getFullYear(), start.getMonth(), start.getDate());
389            lastDate = new Date(end.getFullYear(), end.getMonth(), end.getDate());
390
391            while (cursor <= lastDate) {
392                map[formatDateKey(cursor)] = true;
393                cursor.setDate(cursor.getDate() + 1);
394            }
395        });
396
397        return Object.keys(map).sort();
398    }
399
400    function localizeCalendarChrome(calendar, localeCode) {
401        updateCalendarToolbarTitle(calendar, localeCode);
402        updateCalendarDayHeaders(calendar, localeCode);
403        updateCalendarListHeaders(calendar, localeCode);
404    }
405
406    function applyCalendarLocaleOptions(calendarOptions, localeCode) {
407        var supportedLocale = getSupportedFullCalendarLocaleCode(localeCode);
408        var locales = window.FullCalendar && Array.isArray(window.FullCalendar.globalLocales) ? window.FullCalendar.globalLocales : [];
409
410        if (!localeCode) {
411            return;
412        }
413
414        if (locales.length) {
415            calendarOptions.locales = locales;
416        }
417
418        if (supportedLocale) {
419            calendarOptions.locale = supportedLocale;
420        }
421    }
422
423    $(document).ready(function() {
424        if (typeof window.mepCalendar === 'undefined') {
425            return;
426        }
427
428        initAllCalendars();
429        initFilters();
430    });
431
432    /**
433     * Initialize all calendar containers on the page
434     */
435    function initAllCalendars() {
436        $('.mep-calendar-container').each(function() {
437            var $el = $(this);
438            var calId = $el.attr('id');
439            if (!calId) return;
440            if ($el.attr('data-mep-cal-rendered') === 'yes') return;
441
442            resetCalendarContainer($el, calId);
443
444            var style = $el.data('style') || 'full';
445
446            if (style === 'lite') {
447                initLiteCalendar($el, calId);
448            } else {
449                initFullCalendar($el, calId);
450            }
451        });
452    }
453
454    /**
455     * Initialize FullCalendar instance
456     */
457    function initFullCalendar($el, calId) {
458        var containerEl = document.getElementById(calId);
459        if (!containerEl) return;
460        if (typeof window.FullCalendar === 'undefined') {
461            renderCalendarError($el);
462            return;
463        }
464
465        var settings = mepCalendar.settings || {};
466        var defaultView = $el.data('default-view') || 'dayGridMonth';
467        var firstDay = parseInt($el.data('first-day')) || 0;
468        var height = normalizeCalendarHeight($el.data('height') || 'auto');
469        var showNav = $el.data('show-navigation') !== 'no';
470        var showViewSwitcher = $el.data('show-view-switcher') !== 'no';
471        var showYearNav = $el.data('show-year-nav') !== 'no';
472        var showPrevNext = $el.data('show-prev-next') !== 'no';
473        var hideTooltip = $el.data('hide-tooltip') === 'yes';
474        var clickAction = $el.data('event-click') || settings.mep_cal_event_click || 'navigate';
475        var expiredColor = $el.data('expired-event-color') || settings.mep_cal_expired_event_c
475olor || '#999999';
476        var expiredTextColor = $el.data('expired-text-color') || settings.mep_cal_expired_text_color || '#ffffff';
477        var expiredOpacity = $el.data('expired-opacity') || settings.mep_cal_expired_opacity || '0.6';
478        var locale = resolveCalendarLocaleCode(settings.mep_cal_locale || '');
479        var explicitEventColor = String($el.data('event-color') || '').trim();
480
481        // Build header toolbar
482        var headerLeftParts = [];
483        if (showNav && showPrevNext) headerLeftParts.push('prev,next');
484        if (showNav && showYearNav) headerLeftParts.push('prevYear,nextYear');
485        if (showNav) headerLeftParts.push('today');
486        var headerLeft = headerLeftParts.join(' ');
487        var headerCenter = 'title';
488        var headerRight = showNav && showViewSwitcher ? 'dayGridMonth,timeGridWeek,timeGridDay,listMonth' : '';
489
490        var calendarOptions = {
491            initialView: defaultView,
492            firstDay: firstDay,
493            height: height,
494            headerToolbar: {
495                left: headerLeft,
496                center: headerCenter,
497                right: headerRight
498            },
499            buttonText: getCalendarButtonText(locale, {
500                today: mepCalendar.i18n.today,
501                month: mepCalendar.i18n.month,
502                week: mepCalendar.i18n.week,
503                day: mepCalendar.i18n.day,
504                list: mepCalendar.i18n.list
505            }),
506            noEventsText: getLocaleUiText(locale, 'noEvents', mepCalendar.i18n.noEvents),
507            allDayText: getLocaleUiText(locale, 'allDay', mepCalendar.i18n.allDay),
508            editable: false,
509            selectable: false,
510            expandRows: false,
511            dayMaxEvents: true,
512            navLinks: true,
513            eventDisplay: 'block',
514            slotEventOverlap: false,
515            moreLinkClick: 'popover',
516            views: {
517                dayGridMonth: {
518                    eventDisplay: 'list-item',
519                    dayMaxEvents: 4,
520                    dayMaxEventRows: 4
521                },
522                timeGridWeek: {
523                    dayMaxEventRows: 2,
524                    eventMaxStack: 2
525                },
526                timeGridDay: {
527                    dayMaxEventRows: 2,
528                    eventMaxStack: 2
529                }
530            },
531
532            moreLinkContent: function(args) {
533                return {
534                    html: '<span class="mep-cal-more-link-text">' + escapeHtml(buildMoreLinkText(locale, args.num)) + '</span>'
535                };
536            },
537
538            dayHeaderDidMount: function(info) {
539                applyCalendarDayAppearance($el, info.el, info.date, true);
540            },
541
542            dayCellDidMount: function(info) {
543                applyCalendarDayAppearance($el, info.el, info.date, false);
544            },
545
546            // Style expired events — use !important to override theme CSS
547            eventDidMount: function(info) {
548                var props = info.event.extendedProps || {};
549                if (props.isExpired) {
550                    info.el.classList.add('mep-cal-expired-event');
551                    info.el.style.setProperty('background-color', expiredColor, 'important');
552                    info.el.style.setProperty('border-color', expiredColor, 'important');
553                    info.el.style.setProperty('color', expiredTextColor, 'important');
554                    info.el.style.setProperty('background-image', 'none', 'important');
555                    info.el.style.opacity = expiredOpacity;
556                    if (props.expiredBadge) {
557                        info.el.title = mepCalendar.i18n.expired + ': ' + info.event.title;
558                    }
559                }
560
561                styleCalendarEventCard(info, explicitEventColor);
562                stabilizeMonthGridEvent(info);
563                stabilizeTimeGridAllDayEvent(info);
564            },
565
566            eventsSet: function(events) {
567                var eventsSnapshot = events ? events.slice() : [];
568                setTimeout(function() {
569                    renderTimeGridDaySummaryButtons($el, calId, eventsSnapshot);
570                    if (calendars[calId]) {
571                        styleMonthMoreLinks($el);
572                        localizeCalendarChrome(calendars[calId], locale);
573                    }
574                }, 0);
575            },
576
577            datesSet: function() {
578                setTimeout(function() {
579                    if (calendars[calId]) {
580                        if (typeof calendars[calId].getEvents === 'function') {
581                            renderTimeGridDaySummaryButtons($el, calId, calendars[calId].getEvents());
582                        }
583                        styleMonthMoreLinks($el);
584                        localizeCalendarChrome(calendars[calId], locale);
585                    }
586                }, 0);
587            },
588
589            // Event source via AJAX
590            events: function(info, successCallback, failureCallback) {
591                loadEvents($el, calId, info, successCallback, failureCallback);
592            },
593
594            // Click to navigate to event page
595            eventClick: function(info) {
596                info.jsEvent.preventDefault();
597                if (clickAction === 'none') {
598                    return;
599                }
600                if (clickAction === 'tooltip') {
601                    showTooltip(info.event, info.jsEvent, calId);
602                    return;
603                }
604                if (info.event.url) {
605                    window.location.href = info.event.url;
606                }
607            },
608
609            // Tooltip on hover
610            eventMouseEnter: function(info) {
611                toggleCalendarEventHoverState(info, true);
612                if (hideTooltip) return;
613                showTooltip(info.event, info.jsEvent, calId);
614            },
615
616            eventMouseLeave: function(info) {
617                toggleCalendarEventHoverState(info, false);
618                hideTooltipEl(calId);
619            },
620
621            // Add event content customization
622            eventContent: function(arg) {
623                var props = arg.event.extendedProps || {};
624                var showBadge = $el.data('show-recurring-badge') !== 'no';
625                var hideTime = props.hideTime === 'yes';
626                var isTimeGrid = arg.view && isTimeGridViewType(arg.view.type);
627                var isMonthGrid = arg.view && arg.view.type === 'dayGridMonth';
628
629                var html = '<div class="mep-cal-event-inner' + (isTimeGrid ? ' mep-cal-event-inner-timegrid' : '') + (isMonthGrid ? ' mep-cal-event-inner-month' : '') + '">';
630                
631                // Time
632                if (!hideTime && !arg.event.allDay) {
633                    var displayTime = formatEventTimeText(arg);
634                    if (displayTime) {
635                        html += '<span class="mep-cal-event-time">' + escapeHtml(displayTime) + ' </span>';
636                    }
637                }
638                
639                // Title
640                html += '<span class="mep-cal-event-title">' + escapeHtml(arg.event.title) + '</span>';
641
642                // Badge
643                if (showBadge && !isMonthGrid) {
644                    if (props.eventType === 'everyday') {
645                        html += ' <span class="mep-cal-event-badge" title="' + mepCalendar.i18n.recurring + '">&#8635;</span>';
646                    } else if (props.eventType === 'yes') {
647                        html += ' <span class="mep-cal-event-badge" title="' + mepCalendar.i18n.multiDate + '">&#128197;</span>';
648                    }
649                    if (props.eventMode === 'online') {
650                        html += ' <span class="mep-cal-event-badge" title="' + mepCalendar.i18n.virtual + '">&#127760;</span>';
651                    }
652                }
653
654                // Stock indicator dot
655                if (!isMonthGrid && props.showStock === 'yes' && props.regStatus === 'on' && props.totalSeats > 0) {
656                    if (props.availableSeats <= 0) {
657                        html += ' <span class="mep-cal-stock-dot mep-cal-stock-dot-soldout" title="' + mepCalendar.i18n.soldOut + '">●</span>';
658                    }
659                }
660
661                html += '</div>';
662
663                return { html: html };
664            }
665        };
666        applyCalendarLocaleOptions(calendarOptions, locale);
667
668        var calendar = new FullCalendar.Calendar(containerEl, calendarOptions);
669        calendar.render();
670        calendars[calId] = calendar;
671        localizeCalendarChrome(calendar, locale);
672        $el.attr('data-mep-cal-rendered', 'yes');
673    }
674
675    /**
676     * Initialize Lite style calendar (simple month grid)
677     */
678    function initLiteCalendar($el, calId) {
679        // Lite mode still uses FullCalendar but with simpler options
680        var containerEl = document.getElementById(calId);
681        if (!containerEl) return;
682        if (typeof window.FullCalendar === 'undefined') {
683            renderCalendarError($el);
684            return;
685        }
686
687        var firstDay = parseInt($el.data('first-day')) || 0;
688        var hideTooltip = $el.data('hide-tooltip') === 'yes';
689        var clickAction = $el.data('event-click') || 'navigate';
690        var settings = mepCalendar.settings || {};
691        var locale = resolveCalendarLocaleCode(settings.mep_cal_locale || '');
692        var height = normalizeCalendarHeight($el.data('height') || 'auto');
693
694        var calendarOptions = {
695            initialView: 'dayGridMonth',
696            firstDay: firstDay,
697            height: height,
698            headerToolbar: {
699                left: 'prev,next',
700                center: 'title',
701                right: 'today'
702            },
703            buttonText: {
704                today: getLocaleUiText(locale, 'today', mepCalendar.i18n.today)
705            },
706            noEventsText: getLocaleUiText(locale, 'noEvents', mepCalendar.i18n.noEvents),
707            editable: false,
708            selectable: false,
709            dayMaxEvents: 3,
710            navLinks: false,
711            eventDisplay: 'block',
712
713            dayHeaderDidMount: function(info) {
714                applyCalendarDayAppearance($el, info.el, info.date, true);
715            },
716
717            dayCellDidMount: function(info) {
718                applyCalendarDayAppearance($el, info.el, info.date, false);
719            },
720
721            events: function(info, successCallback, failureCallback) {
722                loadEvents($el, calId, info, successCallback, failureCallback);
723            },
724
725            eventClick: function(info) {
726                info.jsEvent.preventDefault();
727                if (clickAction === 'none') {
728                    return;
729                }
730                if (clickAction === 'tooltip') {
731                    showTooltip(info.event, info.jsEvent, calId);
732                    return;
733                }
734                if (info.event.url) {
735                    window.location.href = info.event.url;
736                }
737            },
738
739            eventMouseEnter: function(info) {
740                toggleCalendarEventHoverState(info, true);
741                if (hideTooltip) return;
742                showTooltip(info.event, info.jsEvent, calId);
743            },
744
745            eventMouseLeave: function(info) {
746                toggleCalendarEventHoverState(info, false);
747                hideTooltipEl(calId);
748            },
749
750            datesSet: function() {
751                setTimeout(function() {
752                    if (calendars[calId]) {
753                        styleMonthMoreLinks($el);
754                        localizeCalendarChrome(calendars[calId], locale);
755                    }
756                }, 0);
757            },
758
759            eventContent: function(arg) {
760                var html = '<div class="mep-cal-event-inner mep-cal-lite-event">';
761                html += '<span class="mep-cal-event-title">' + escapeHtml(arg.event.title) + '</span>';
762                html += '</div>';
763                return { html: html };
764            }
765        };
766        applyCalendarLocaleOptions(calendarOptions, locale);
767
768        var calendar = new FullCalendar.Calendar(containerEl, calendarOptions);
769        calendar.render();
770        calendars[calId] = calendar;
771        localizeCalendarChrome(calendar, locale);
772        $el.attr('data-mep-cal-rendered', 'yes');
773    }
774
775    function resetCalendarContainer($el, calId) {
776        if (calendars[calId] && typeof calendars[calId].destroy === 'function') {
777            calendars[calId].destroy();
778            delete calendars[calId];
779        }
780
781        if ($el.attr('data-mep-cal-rendered') === 'yes') {
782            $el.empty();
783            $el.removeAttr('data-mep-cal-rendered');
784        }
785    }
786
787    /**
788     * Strip timezone and time from FullCalendar ISO 8601 strings.
789     * Week/Day views send strings like "2026-05-11T00:00:00+06:00".
790     * We send only plain "Y-m-d" to PHP to avoid strtotime() inconsistencies on live servers.
791     *
792     * @param {string} isoStr FullCalendar date string.
793     * @returns {string} Plain date YYYY-MM-DD, or original string if not ISO 8601.
794     */
795    function stripIsoTimezone(isoStr) {
796        if (typeof isoStr !== 'string' || !isoStr) {
797            return isoStr || '';
798        }
799        var match = isoStr.match(/^(\d{4}-\d{2}-\d{2})[T ]/);
800        if (match) {
801            return match[1];
802        }
803        return isoStr;
804    }
805
806    /**
807     * Load events via AJAX
808     */
809    function loadEvents($el, calId, info, successCallback, failureCallback) {
810        var data = {
811            action: 'mep_calendar_get_events',
812            nonce: mepCalendar.nonce,
813            visible_start: info && info.startStr ? stripIsoTimezone(info.startStr) : '',
814            visible_end: info && info.endStr ? stripIsoTimezone(info.endStr) : '',
815            view_type: info && info.view && info.view.type ? info.view.type : '',
816            cat: $el.data('cat') || '',
817            org: $el.data('org') || '',
818            tag: $el.data('tag') || '',
819            city: $el.data('city') || '',
820            country: $el.data('country') || '',
821            status: $el.data('status') || 'upcoming',
822            event_source: $el.data('event-source') || 'all',
823            specific_events: $el.data('specific-events') || '',
824            event_limit: $el.data('event-limit') || '-1',
825            event_color: $el.data('event-color') || '',
826            text_color: $el.data('text-color') || '',
827            show_stock_details: $el.data('show-stock') || 'no',
828            hide_time: $el.data('hide-time') || 'no',
829            split_multi_day: $el.data('split-multi-day') || 'no',
830            hide_tooltip: $el.data('hide-tooltip') || 'no',
831            show_price: $el.data('show-price') || 'no',
832            show_location: $el.data('show-location') || 'no',
833            show_organizer: $el.data('show-organizer') || 'no',
834            show_expired_events: $el.data('show-expired-events') || 'yes'
835        };
836
837        // Add search filter if present
838        var $wrapper = $el.closest('.mep-calendar-wrapper');
839        var $searchInput = $wrapper.find('.mep-cal-search-input');
840        if ($searchInput.length && $searchInput.val()) {
841            data.search = $searchInput.val();
842        }
843
844        // Add category filter from dropdown
845        var $catSelect = $wrapper.find('.mep-cal-category-select');
846        if ($catSelect.length && $catSelect.val()) {
847            data.cat_filter = $catSelect.val();
848        }
849
850        var $orgSelect = $wrapper.find('.mep-cal-organizer-select');
851        if ($orgSelect.length && $orgSelect.val()) {
852            data.org_filter = $orgSelect.val();
853        }
854
855        var $locationInput = $wrapper.find('.mep-cal-location-input');
856        if ($locationInput.length && $locationInput.val()) {
857            data.location_filter = $locationInput.val();
858        }
859
860        var $dateStart = $wrapper.find('.mep-cal-date-start');
861        if ($dateStart.length && $dateStart.val()) {
862            data.date_start = $dateStart.val();
863        }
864
865        var $dateEnd = $wrapper.find('.mep-cal-date-end');
866        if ($dateEnd.length && $dateEnd.val()) {
867            data.date_end = $dateEnd.val();
868        }
869
870        $.ajax({
871            url: mepCalendar.ajaxurl,
872            type: 'POST',
873            data: data,
874            dataType: 'json',
875            timeout: 30000,
876            success: function(response) {
877                if (response.success && response.data) {
878                    successCallback(response.data);
879                } else {
880                    successCallback([]);
881                }
882            },
883            error: function() {
884                successCallback([]);
885            }
886        });
887    }
888
889    /**
890     * Show tooltip near the event
891     */
892    function showTooltip(event, jsEvent, calId) {
893        var $tooltip = $('#' + calId + '-tooltip');
894        if (!$tooltip.length) return;
895
896        var props = event.extendedProps || {};
897        tooltipPointerState[calId] = {
898            clientX: jsEvent.clientX,
899            clientY: jsEvent.clientY
900        };
901        var html = buildTooltipHtml(event, props);
902        
903        $tooltip.html(html).show();
904
905        // Position tooltip near mouse
906        positionTooltip($tooltip, jsEvent);
907        maybeLoadStockDetails(event, calId);
908    }
909
910    /**
911     * Build tooltip HTML content
912     */
913    function buildTooltipHtml(event, props) {
914        var html = '';
915
916        // Header with thumbnail
917        html += '<div class="mep-cal-tooltip-header">';
918        if (props.thumbnail) {
919            html += '<img class="mep-cal-tooltip-thumb" src="' + escapeHtml(props.thumbnail) + '" alt="" />';
920        }
921        html += '<div>';
922        html += '<div class="mep-cal-tooltip-title">' + escapeHtml(event.title) + '</div>';
923        
924        // Badges
925        html += '<div class="mep-cal-tooltip-badges">';
926        if (props.eventType === 'everyday') {
927            html += '<span class="mep-cal-tooltip-badge mep-cal-badge-recurring">' + mepCalendar.i18n.recurring + '</span>';
928        } else if (props.eventType === 'yes') {
929            html += '<span class="mep-cal-tooltip-badge mep-cal-badge-multidate">' + mepCalendar.i18n.multiDate + '</span>';
930        }
931        if (props.eventMode === 'online') {
932            html += '<span class="mep-cal-tooltip-badge mep-cal-badge-virtual">' + mepCalendar.i18n.virtual + '</span>';
933        }
934        if (props.regStatus === 'on' && props.totalSeats > 0 && props.availableSeats <= 0) {
935            html += '<span class="mep-cal-tooltip-badge mep-cal-badge-soldout">' + mepCalendar.i18n.soldOut + '</span>';
936        }
937        html += '</div>';
938        html += '</div>';
939        html += '</div>';
940
941        // Body
942        html += '<div class="mep-cal-tooltip-body">';
943
944        // Date/Time
945        if (event.start) {
946            var dateStr = formatDate(event.start);
947            if (props.hideTime !== 'yes' && !event.allDay) {
948                dateStr += ' ' + formatTime(event.start);
949                if (event.end) {
950                    dateStr += ' - ' + formatTime(event.end);
951                }
952            }
953            html += '<div class="mep-cal-tooltip-row"><i class="far fa-calendar-alt"></i> <span>' + escapeHtml(dateStr) + '</span></div>';
954        }
955
956        // Location
957        if (props.showLocation === 'yes' && props.location) {
958            html += '<div class="mep-cal-tooltip-row"><i class="fas fa-map-marker-alt"></i> <span>' + escapeHtml(props.location) + '</span></div>';
959        }
960
961        // Organizer
962        if (props.showOrganizer === 'yes' && props.organizer) {
963            html += '<div class="mep-cal-tooltip-row"><i class="far fa-user"></i> <span>' + escapeHtml(props.organizer) + '</span></div>';
964        }
965
966        // Categories
967        if (props.categories && props.categories.length > 0) {
968            html += '<div class="mep-cal-tooltip-row"><i class="fas fa-tags"></i> <span>' + escapeHtml(props.categories.join(', ')) + '</span></div>';
969        }
970
971        // Price
972        if (props.showPrice === 'yes') {
973            html += buildPriceHtml(props);
974        }
975
976        // Stock Details
977        if (props.showStock === 'yes') {
978            if (props.stockLoaded === 'no') {
979                html += '<div class="mep-cal-tooltip-row"><span>' + escapeHtml((mepCalendar.i18n && mepCalendar.i18n.loadingSeats) ? mepCalendar.i18n.loadingSeats : 'Loading seat details...') + '</span></div>';
980            } else if ((props.totalSeats > 0) || (props.ticketTypes && props.ticketTypes.length > 0)) {
981                html += buildStockHtml(props);
982            }
983        }
984
985        html += '</div>';
986
987        return html;
988    }
989
990    /**
991     * Build stock details HTML
992     */
993    function buildStockHtml(props) {
994        var html = '<div class="mep-cal-tooltip-stock">';
995        
996        // Summary
997        var stockClass = 'mep-cal-stock-available';
998        var settings = mepCalendar.settings || {};
999        var threshold = parseInt(settings.mep_cal_low_stock_threshold) || 5;
1000
1001        if (props.availableSeats <= 0) {
1002            stockClass = 'mep-cal-stock-soldout';
1003        } else if (props.availableSeats <= threshold) {
1004            stockClass = 'mep-cal-stock-low';
1005        }
1006
1007        html += '<div class="mep-cal-tooltip-stock-header">';
1008        html += '<span>' + mepCalendar.i18n.seats + '</span>';
1009        html += '<span class="mep-cal-tooltip-stock-summary ' + stockClass + '">';
1010        html += props.availableSeats + ' / ' + props.totalSeats + ' ' + mepCalendar.i18n.available;
1011        html += '</span>';
1012        html += '</div>';
1013
1014        // Progress bar
1015        var fillPercent = props.totalSeats > 0 ? ((props.totalSeats - props.availableSeats) / props.totalSeats * 100) : 0;
1016        var barColor = '#28a745';
1017        if (props.availableSeats <= 0) {
1018            barColor = settings.mep_cal_sold_out_color || '#dc3545';
1019        } else if (props.availableSeats <= threshold) {
1020            barColor = settings.mep_cal_low_stock_color || '#ffc107';
1021        }
1022
1023        html += '<div class="mep-cal-stock-bar"><div class="mep-cal-stock-bar-fill" style="width:' + fillPercent + '%;background:' + barColor + ';"></div></div>';
1024
1025        // Ticket type breakdown
1026        if (props.ticketTypes && props.ticketTypes.length > 0) {
1027            html += '<div class="mep-cal-tooltip-ticket-types">';
1028            for (var i = 0; i < props.ticketTypes.length; i++) {
1029                var t = props.ticketTypes[i];
1030                html += '<div class="mep-cal-ticket-row">';
1031                html += '<span class="ticket-name">' + escapeHtml(t.name) + '</span>';
1032                html += '<span class="ticket-avail">' + t.available + '/' + t.total;
1033                if (props.showPrice === 'yes' && t.priceHtml) {
1034                    html += ' (' + escapeHtml(t.priceHtml) + ')';
1035                }
1036                html += '</span>';
1037                html += '</div>';
1038            }
1039            html += '</div>';
1040        }
1041
1042        html += '</div>';
1043        return html;
1044    }
1045
1046    /**
1047     * Build price rows for each ticket type
1048     */
1049    function buildPriceHtml(props) {
1050        var html = '';
1051
1052        if (props.ticketTypes && props.ticketTypes.length > 0) {
1053            html += '<div class="mep-cal-tooltip-price-block">';
1054            html += '<div class="mep-cal-tooltip-price-heading">' + escapeHtml(mepCalendar.i18n.price) + '</div>';
1055
1056            for (var i = 0; i < props.ticketTypes.length; i++) {
1057                var ticket = props.ticketTypes[i];
1058                if (!ticket.priceHtml) {
1059                    continue;
1060                }
1061
1062                html += '<div class="mep-cal-tooltip-price-row">';
1063                html += '<span class="price-ticket-name">' + escapeHtml(ticket.name) + '</span>';
1064                html += '<span class="price-ticket-value">' + escapeHtml(ticket.priceHtml) + '</span>';
1065                html += '</div>';
1066            }
1067
1068            html += '</div>';
1069            return html;
1070        }
1071
1072        if (props.minPriceHtml) {
1073            html += '<div class="mep-cal-tooltip-row"><i class="fas fa-tag"></i> <span><strong>' + mepCalendar.i18n.price + ':</strong> ' + escapeHtml(props.minPriceHtml) + '</span></div>';
1074        }
1075
1076        return html;
1077    }
1078
1079    /**
1080     * Position tooltip near the mouse event
1081     */
1082    function positionTooltip($tooltip, jsEvent) {
1083        var x = jsEvent.clientX;
1084        var y = jsEvent.clientY;
1085        var tooltipW = $tooltip.outerWidth();
1086        var tooltipH = $tooltip.outerHeight();
1087        var winW = $(window).width();
1088        var winH = $(window).height();
1089
1090        // Position to the right of cursor by default
1091        var left = x + 15;
1092        var top = y - 10;
1093
1094        // If would overflow right, show to the left
1095        if (left + tooltipW > winW - 10) {
1096            left = x - tooltipW - 15;
1097        }
1098
1099        // If would overflow bottom, move up
1100        if (top + tooltipH >
1100 winH - 10) {
1101            top = winH - tooltipH - 10;
1102        }
1103
1104        // Don't go above viewport
1105        if (top < 10) {
1106            top = 10;
1107        }
1108
1109        // Don't go left of viewport
1110        if (left < 10) {
1111            left = 10;
1112        }
1113
1114        $tooltip.css({
1115            left: left + 'px',
1116            top: top + 'px'
1117        });
1118    }
1119
1120    function maybeLoadStockDetails(event, calId) {
1121        var props = event.extendedProps || {};
1122        var cacheKey;
1123
1124        if (props.showStock !== 'yes' || !props.eventId || !props.eventDate) {
1125            return;
1126        }
1127
1128        if (props.stockLoaded === 'yes') {
1129            return;
1130        }
1131
1132        cacheKey = String(props.eventId) + '|' + String(props.eventDate);
1133
1134        if (stockRequestCache[cacheKey]) {
1135            applyStockDataToEvent(event, stockRequestCache[cacheKey]);
1136            refreshTooltipContent(event, calId);
1137            return;
1138        }
1139
1140        $.ajax({
1141            url: mepCalendar.ajaxurl,
1142            type: 'POST',
1143            dataType: 'json',
1144            data: {
1145                action: 'mep_calendar_get_event_stock',
1146                nonce: mepCalendar.nonce,
1147                event_id: props.eventId,
1148                event_date: props.eventDate,
1149                show_price: props.showPrice || 'no'
1150            },
1151            success: function(response) {
1152                if (!response || !response.success || !response.data) {
1153                    return;
1154                }
1155
1156                stockRequestCache[cacheKey] = response.data;
1157                applyStockDataToEvent(event, response.data);
1158                refreshTooltipContent(event, calId);
1159            }
1160        });
1161    }
1162
1163    function applyStockDataToEvent(event, stockData) {
1164        if (!event || !stockData) {
1165            return;
1166        }
1167
1168        var mappedProps = {
1169            totalSeats: parseInt(stockData.totalSeats, 10) || 0,
1170            availableSeats: parseInt(stockData.availableSeats, 10) || 0,
1171            soldSeats: parseInt(stockData.soldSeats, 10) || 0,
1172            reservedSeats: parseInt(stockData.reservedSeats, 10) || 0,
1173            ticketTypes: Array.isArray(stockData.ticketTypes) ? stockData.ticketTypes : [],
1174            stockLoaded: stockData.stockLoaded || 'yes'
1175        };
1176
1177        if (typeof stockData.minPriceHtml !== 'undefined' && stockData.minPriceHtml !== null && stockData.minPriceHtml !== '') {
1178            mappedProps.minPriceHtml = stockData.minPriceHtml;
1179        }
1180
1181        Object.keys(mappedProps).forEach(function(key) {
1182            if (typeof event.setExtendedProp === 'function') {
1183                event.setExtendedProp(key, mappedProps[key]);
1184            } else {
1185                event.extendedProps[key] = mappedProps[key];
1186            }
1187        });
1188
1189        applyStockStylesToEvent(event, $.extend({}, event.extendedProps || {}, mappedProps));
1190    }
1191
1192    function refreshTooltipContent(event, calId) {
1193        var $tooltip = $('#' + calId + '-tooltip');
1194        var pointer = tooltipPointerState[calId];
1195
1196        if (!$tooltip.length || !$tooltip.is(':visible')) {
1197            return;
1198        }
1199
1200        $tooltip.html(buildTooltipHtml(event, event.extendedProps || {}));
1201
1202        if (pointer) {
1203            positionTooltip($tooltip, pointer);
1204        }
1205    }
1206
1207    function applyStockStylesToEvent(event, props) {
1208        if (!event || !props || props.isExpired || props.showStock !== 'yes' || props.regStatus !== 'on') {
1209            return;
1210        }
1211
1212        var settings = mepCalendar.settings || {};
1213        var threshold = parseInt(settings.mep_cal_low_stock_threshold, 10) || 5;
1214        var defaultBg = props.defaultBackgroundColor || '';
1215        var defaultBorder = props.defaultBorderColor || defaultBg;
1216        var defaultText = props.defaultTextColor || '#ffffff';
1217        var nextBg = defaultBg;
1218        var nextBorder = defaultBorder;
1219        var nextText = defaultText;
1220
1221        if (parseInt(props.totalSeats, 10) > 0) {
1222            if (parseInt(props.availableSeats, 10) <= 0) {
1223                nextBg = settings.mep_cal_sold_out_color || '#dc3545';
1224                nextBorder = nextBg;
1225            } else if (parseInt(props.availableSeats, 10) <= threshold) {
1226                nextBg = settings.mep_cal_low_stock_color || '#ffc107';
1227                nextBorder = nextBg;
1228                nextText = '#333333';
1229            }
1230        }
1231
1232        if (typeof event.setProp === 'function') {
1233            event.setProp('backgroundColor', nextBg);
1234            event.setProp('borderColor', nextBorder);
1235            event.setProp('textColor', nextText);
1236        }
1237    }
1238
1239    function styleCalendarEventCard(info, explicitEventColor) {
1240        if (!info || !info.el || !info.event || !info.view) {
1241            return;
1242        }
1243
1244        if (info.view.type === 'dayGridMonth') {
1245            return;
1246        }
1247
1248        var props = info.event.extendedProps || {};
1249        if (props.isExpired) {
1250            return;
1251        }
1252
1253        var isTimeGrid = isTimeGridViewType(info.view.type);
1254        var baseColor = explicitEventColor || props.paletteAccent || info.event.backgroundColor || props.defaultBackgroun
1254dColor || '#3a87ad';
1255
1256        if (!explicitEventColor) {
1257            baseColor = props.paletteAccent || getEventPaletteColor(info.event, baseColor);
1258        }
1259
1260        var palette = buildSoftEventPalette(baseColor);
1261        var mainEl = info.el.querySelector('.fc-event-main');
1262
1263        info.el.style.setProperty('--mep-cal-event-accent', palette.accent);
1264        info.el.style.setProperty('--mep-cal-event-soft-bg', palette.softBackground);
1265        info.el.style.setProperty('--mep-cal-event-soft-border', palette.softBorder);
1266        info.el.style.setProperty('--mep-cal-event-soft-text', palette.text);
1267        info.el.style.setProperty('--mep-cal-event-dot', palette.accent);
1268        info.el.style.backgroundColor = palette.softBackground;
1269        info.el.style.borderColor = palette.softBorder;
1270        info.el.style.color = palette.text;
1271        info.el.style.borderLeftColor = palette.accent;
1272
1273        if (mainEl) {
1274            mainEl.style.color = palette.text;
1275        }
1276
1277        if (!isTimeGrid) {
1278            return;
1279        }
1280    }
1281
1282    function isTimeGridViewType(viewType) {
1283        return viewType === 'timeGridWeek' || viewType === 'timeGridDay';
1284    }
1285
1286    function stabilizeTimeGridAllDayEvent(info) {
1287        var nodes = [];
1288        var innerEl;
1289        var titleEl;
1290        var harnessEl;
1291
1292        if (!info || !info.el || !info.view || !isTimeGridViewType(info.view.type) || !info.event || !info.event.allDay) {
1293            return;
1294        }
1295
1296        harnessEl = info.el.closest('.fc-daygrid-event-harness');
1297        innerEl = info.el.querySelector('.mep-cal-event-inner');
1298        titleEl = info.el.querySelector('.mep-cal-event-title');
1299
1300        nodes.push(info.el);
1301        if (harnessEl) {
1302            nodes.push(harnessEl);
1303        }
1304
1305        [
1306            info.el.querySelector('.fc-event-main'),
1307            info.el.querySelector('.fc-event-main-frame'),
1308            info.el.querySelector('.fc-event-title-container'),
1309            info.el.querySelector('.fc-event-title'),
1310            innerEl,
1311            titleEl
1312        ].forEach(function(node) {
1313            if (node) {
1314                nodes.push(node);
1315            }
1316        });
1317
1318        nodes.forEach(function(node) {
1319            node.style.maxWidth = '100%';
1320            node.style.minWidth = '0';
1321            node.style.overflow = 'hidden';
1322            node.style.boxSizing = 'border-box';
1323        });
1324
1325        if (innerEl) {
1326            innerEl.style.display = 'flex';
1327            innerEl.style.alignItems = 'center';
1328            innerEl.style.gap = '4px';
1329            innerEl.style.whiteSpace = 'nowrap';
1330        }
1331
1332        if (titleEl) {
1333            titleEl.style.display = 'block';
1334            titleEl.style.whiteSpace = 'nowrap';
1335            titleEl.style.textOverflow = 'ellipsis';
1336        }
1337    }
1338
1339    function getEventPaletteColor(event, fallbackColor) {
1340        var props = event && event.extendedProps ? event.extendedProps : {};
1341        var seed = String(
1342            (props.eventId || event.id || '') +
1343            '|' +
1344            (event.title || '') +
1345            '|' +
1346            ((props.categories && props.categories[0]) ? props.categories[0] : '')
1347        );
1348        var hash = Math.abs(hashString(seed || fallbackColor || 'mep-calendar-event'));
1349        var hue = hash % 360;
1350        var saturation = 62 + (hash % 12);
1351        var lightness = 44 + (hash % 8);
1352
1353        return hslToHex(hue, saturation, lightness);
1354    }
1355
1356    function buildSoftEventPalette(color) {
1357        var accent = normalizeHexColor(color) || '#3a87ad';
1358
1359        return {
1360            accent: accent,
1361            softBackground: hexToRgba(accent, 0.18),
1362            softBorder: hexToRgba(accent, 0.92),
1363            text: shadeHexColor(accent, -0.42) || accent
1364        };
1365    }
1366
1367    function normalizeHexColor(color) {
1368        if (!color) {
1369            return '';
1370        }
1371
1372        color = String(color).trim();
1373        if (!/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(color)) {
1374            return '';
1375        }
1376
1377        if (color.length === 4) {
1378            return '#' + color.charAt(1) + color.charAt(1) + color.charAt(2) + color.charAt(2) + color.charAt(3) + color.charAt(3);
1379        }
1380
1381        return color.toLowerCase();
1382    }
1383
1384    function hexToRgba(color, alpha) {
1385        var hex = normalizeHexColor(color);
1386        if (!hex) {
1387            return color;
1388        }
1389
1390        var r = parseInt(hex.substr(1, 2), 16);
1391        var g = parseInt(hex.substr(3, 2), 16);
1392        var b = parseInt(hex.substr(5, 2), 16);
1393
1394        return 'rgba(' + r + ', ' + g + ', ' + b + ', ' + alpha + ')';
1395    }
1396
1397    function shadeHexColor(color, percent) {
1398        var hex = normalizeHexColor(color);
1399        if (!hex) {
1400            return color;
1401        }
1402
1403        var factor = typeof percent === 'number' ? percent : 0;
1404        var r = parseInt(hex.substr(1, 2), 16);
1405        var g = parseInt(hex.substr(3, 2), 16);
1406        var b = parseInt(hex.substr(5, 2), 16);
1407
1408        r = clampColorChannel(Math.round(r * (1 + factor)));
1409        g = clampColorChannel(Math.round(g * (1 + factor)));
1410        b = clampColorChannel(Math.round(b * (1 + factor)));
1411
1412        return '#' + toHex(r) + toHex(g) + toHex(b);
1413    }
1414
1415    function clampColorChannel(value) {
1416        return Math.max(0, Math.min(255, value));
1417    }
1418
1419    function toHex(value) {
1420        return value.toString(16).padStart(2, '0');
1421    }
1422
1423    function hslToHex(h, s, l) {
1424        var hue = ((h % 360) + 360) % 360;
1425        var sat = Math.max(0, Math.min(100, s)) / 100;
1426        var light = Math.max(0, Math.min(100, l)) / 100;
1427        var c = (1 - Math.abs(2 * light - 1)) * sat;
1428        var x = c * (1 - Math.abs((hue / 60) % 2 - 1));
1429        var m = light - c / 2;
1430        var r = 0;
1431        var g = 0;
1432        var b = 0;
1433
1434        if (hue < 60) {
1435            r = c; g = x; b = 0;
1436        } else if (hue < 120) {
1437            r = x; g = c; b = 0;
1438        } else if (hue < 180) {
1439            r = 0; g = c; b = x;
1440        } else if (hue < 240) {
1441            r = 0; g = x; b = c;
1442        } else if (hue < 300) {
1443            r = x; g = 0; b = c;
1444        } else {
1445            r = c; g = 0; b = x;
1446        }
1447
1448        return '#' + toHex(Math.round((r + m) * 255)) + toHex(Math.round((g + m) * 255)) + toHex(Math.round((b + m) * 255));
1449    }
1450
1451    function hashString(value) {
1452        var str = String(value);
1453        var hash = 0;
1454        var i;
1455
1456        for (i = 0; i < str.length; i++) {
1457            hash = ((hash << 5) - hash) + str.charCodeAt(i);
1458            hash |= 0;
1459        }
1460
1461        return hash;
1462    }
1463
1464    function renderTimeGridDaySummaryButtons($el, calId, events) {
1465        var calendar = calendars[calId];
1466        var viewType;
1467        var $existingButtons;
1468
1469        if (!calendar || !calendar.view) {
1470            return;
1471        }
1472
1473        viewType = calendar.view.type;
1474        $existingButtons = $el.find('.mep-cal-day-summary-button');
1475        $existingButtons.remove();
1476
1477        if (!isTimeGridViewType(viewType)) {
1478            hideDayEventsPopover(calId);
1479            return;
1480        }
1481
1482        $el.find('.fc-timegrid-col').each(function() {
1483            var colEl = this;
1484            var dateStr = colEl.getAttribute('data-date');
1485            var dayEvents;
1486            var buttonText;
1487            var buttonEl;
1488
1489            if (!dateStr) {
1490                return;
1491            }
1492
1493            dayEvents = getEventsForDate(events, dateStr);
1494            if (!dayEvents.length) {
1495                return;
1496            }
1497
1498            buttonText = 'View all ' + dayEvents.length + ' events';
1499            buttonEl = document.createElement('button');
1500            buttonEl.type = 'button';
1501            buttonEl.className = 'mep-cal-day-summary-button';
1502            buttonEl.setAttribute('data-calendar-id', calId);
1503            buttonEl.setAttribute('data-date', dateStr);
1504            buttonEl.textContent = buttonText;
1505
1506            colEl.appendChild(buttonEl);
1507        });
1508    }
1509
1510    // Fixed by Shahnur -- 2026-04-22 12:31 PM (Asia/Dhaka)
1511    function styleMonthMoreLinks($el) {
1512        var calendarEl;
1513        var moreLinks;
1514
1515        if (!$el || !$el.length) {
1516            return;
1517        }
1518
1519        calendarEl = $el.get(0);
1520        if (!calendarEl) {
1521            return;
1522        }
1523
1524        moreLinks = calendarEl.querySelectorAll('.fc-dayGridMonth-view .fc-daygri
1524d-day-bottom a, .fc-dayGridMonth-view .fc-daygrid-day-bottom .fc-more-link, .fc-dayGridMonth-view .fc-daygrid-day-bottom .fc-daygrid-more-link');
1525
1526        moreLinks.forEach(function(linkEl) {
1527            var textNodes = linkEl.querySelectorAll('.fc-sticky, .mep-cal-more-link-text');
1528
1529            linkEl.classList.add('mep-cal-more-link-button');
1530            linkEl.style.setProperty('display', 'inline-flex', 'important');
1531            linkEl.style.setProperty('align-items', 'center', 'important');
1532            linkEl.style.setProperty('justify-content', 'center', 'important');
1533            linkEl.style.setProperty('width', 'auto', 'important');
1534            linkEl.style.setProperty('max-width', '100%', 'important');
1535            linkEl.style.setProperty('padding', '5px 10px', 'important');
1536            linkEl.style.setProperty('border', '1px solid rgba(44, 62, 80, 0.22)', 'important');
1537            linkEl.style.setProperty('border-radius', '999px', 'important');
1538            linkEl.style.setProperty('background', '#34495e', 'important');
1539            linkEl.style.setProperty('color', '#ffffff', 'important');
1540            linkEl.style.setProperty('font-size', '12px', 'important');
1541            linkEl.style.setProperty('font-weight', '600', 'important');
1542            linkEl.style.setProperty('line-height', '1.2', 'important');
1543            linkEl.style.setProperty('text-decoration', 'none', 'important');
1544            linkEl.style.setProperty('box-shadow', '0 2px 8px rgba(15, 23, 42, 0.12)', 'important');
1545            linkEl.style.setProperty('cursor', 'pointer', 'important');
1546
1547            textNodes.forEach(function(textNode) {
1548                textNode.style.setProperty('display', 'inline-flex', 'important');
1549                textNode.style.setProperty('align-items', 'center', 'important');
1550                textNode.style.setProperty('justify-content', 'center', 'important');
1551                textNode.style.setProperty('position', 'static', 'important');
1552                textNode.style.setProperty('color', 'inherit', 'important');
1553                textNode.style.setProperty('white-space', 'nowrap', 'important');
1554            });
1555        });
1556    }
1557
1558    function stabilizeMonthGridEvent(info) {
1559        var nodes = [];
1560        var harnessEl;
1561        var titleEl;
1562        var innerEl;
1563        var isDotEvent;
1564
1565        if (!info || !info.el || !info.view || info.view.type !== 'dayGridMonth') {
1566            return;
1567        }
1568
1569        harnessEl = info.el.closest('.fc-daygrid-event-harness');
1570        titleEl = info.el.querySelector('.mep-cal-event-title');
1571        innerEl = info.el.querySelector('.mep-cal-event-inner, .mep-cal-event-inner-month');
1572        isDotEvent = info.el.classList.contains('fc-daygrid-dot-event');
1573
1574        [
1575            harnessEl,
1576            info.el,
1577            info.el.querySelector('.fc-event-main'),
1578            info.el.querySelector('.fc-event-main-frame'),
1579            info.el.querySelector('.fc-event-title-container'),
1580            info.el.querySelector('.fc-event-title'),
1581            info.el.querySelector('.fc-event-time'),
1582            titleEl,
1583            innerEl
1584        ].forEach(function(node) {
1585            if (node) {
1586                nodes.push(node);
1587            }
1588        });
1589
1590        nodes.forEach(function(node) {
1591            node.style.maxWidth = '100%';
1592            node.style.minWidth = '0';
1593            node.style.overflow = 'hidden';
1594            node.style.boxSizing = 'border-box';
1595        });
1596
1597        if (harnessEl) {
1598            harnessEl.style.left = '0';
1599            harnessEl.style.right = '0';
1600            harnessEl.style.width = '100%';
1601            harnessEl.style.maxWidth = '100%';
1602            harnessEl.style.top = 'auto';
1603            harnessEl.style.bottom = 'auto';
1604        }
1605
1606        info.el.style.width = '100%';
1607        info.el.style.maxWidth = '100%';
1608        info.el.style.display = 'flex';
1609        info.el.style.alignItems = 'center';
1610
1611        // Fixed by Shahnur — 2026-04-22 11:27 AM (Asia/Dhaka)
1612        if (isDotEvent) {
1613            if (harnessEl) {
1614                harnessEl.style.position = 'static';
1615                harnessEl.style.marginTop = '2px';
1616            }
1617
1618            info.el.style.background = 'transparent';
1619            info.el.style.border = 'none';
1620            info.el.style.boxShadow = 'none';
1621            info.el.style.padding = '2px 4px';
1622        }
1623
1624        if (titleEl) {
1625            titleEl.style.display = 'block';
1626            titleEl.style.width = '100%';
1627            titleEl.style.whiteSpace = 'nowrap';
1628            titleEl.style.textOverflow = 'ellipsis';
1629        }
1630    }
1631
1632    // Fixed by Shahnur -- 2026-04-22 12:08 PM (Asia/Dhaka)
1633    function toggleCalendarEventHoverState(info, isActive) {
1634        var harnessEl;
1635        var innerEl;
1636        var mainEl;
1637        var highlightEl;
1638        var textNodes;
1639
1640        if (!info || !info.el) {
1641            return;
1642        }
1643
1644        harnessEl = info.el.closest('.fc-daygrid-event-harness, .fc-daygrid-event-harness-abs');
1645        innerEl = info.el.querySelector('.mep-cal-event-inner, .mep-cal-event-inner-month');
1646        mainEl = info.el.querySelector('.fc-event-main');
1647        highlightEl = innerEl || mainEl || info.el;
1648        textNodes = info.el.querySelectorAll('.mep-cal-event-time, .mep-cal-event-title, .fc-event-time, .fc-event-title');
1649
1650        info.el.classList.toggle('mep-cal-event-hovered', !!isActive);
1651
1652        if (harnessEl) {
1653            harnessEl.classList.toggle('mep-cal-event-hovered-wrap', !!isActive);
1654            if (isActive) {
1655                harnessEl.style.setProperty('z-index', '4', 'important');
1656            } else {
1657                harnessEl.style.removeProperty('z-index');
1658            }
1659        }
1660
1661        if (innerEl) {
1662            innerEl.classList.toggle('mep-cal-event-hovered-inner', !!isActive);
1663        }
1664
1665        if (mainEl) {
1666            mainEl.classList.toggle('mep-cal-event-hovered-main', !!isActive);
1667        }
1668
1669        if (highlightEl) {
1670            if (isActive) {
1671                highlightEl.style.setProperty('background', 'rgba(58, 135, 173, 0.16)', 'important');
1672                highlightEl.style.setProperty('box-shadow', 'inset 0 0 0 1px rgba(58, 135, 173, 0.42)', 'important');
1673                highlightEl.style.setProperty('border-radius', '6px', 'important');
1674                highlightEl.style.setProperty('padding', '2px 4px', 'important');
1675            } else {
1676                highlightEl.style.removeProperty('background');
1677                highlightEl.style.removeProperty('box-shadow');
1678                highlightEl.style.removeProperty('border-radius');
1679                highlightEl.style.removeProperty('padding');
1680            }
1681        }
1682
1683        textNodes.forEach(function(node) {
1684            if (isActive) {
1685                node.style.setProperty('color', '#b71c1c', 'important');
1686                node.style.setProperty('font-weight', '600', 'important');
1687            } else {
1688                node.style.removeProperty('color');
1689                node.style.removeProperty('font-weight');
1690            }
1691        });
1692    }
1693
1694    function getEventsForDate(events, dateStr) {
1695        var list = Array.isArray(events) ? events.slice() : [];
1696
1697        return list.filter(function(event) {
1698            return eventOccursOnDate(event, dateStr);
1699        }).sort(function(a, b) {
1700            var startA = a && a.start ? a.start.getTime() : 0;
1701            var startB = b && b.start ? b.start.getTime() : 0;
1702
1703            return startA - startB;
1704        });
1705    }
1706
1707    function eventOccursOnDate(event, dateStr) {
1708        var start;
1709        var end;
1710        var dateStart;
1711        var dateEnd;
1712
1713        if (!event || !event.start || !dateStr) {
1714            return false;
1715        }
1716
1717        start = new Date(event.start);
1718        end = event.end ? new Date(event.end) : new Date(event.start);
1719        dateStart = new Date(dateStr + 'T00:00:00');
1720        dateEnd = new Date(dateStr + 'T23:59:59');
1721
1722        if (event.allDay && end.getTime() > start.getTime()) {
1723            end = new Date(end.getTime() - 1000);
1724        }
1725
1726        return start <= dateEnd && end >= dateStart;
1727    }
1728
1729    function ensureDayEventsPopover($el, calId) {
1730        var $popover = $('#' + calId + '-day-events-popover');
1731
1732        if ($popover.length) {
1733            return $popover;
1734        }
1735
1736        $popover = $('<div/>', {
1737            id: calId + '-day-events-popover',
1738            class: 'mep-cal-day-events-popover',
1739            css: { display: 'none' }
1740        });
1741
1742        $el.closest('.mep-calendar-wrapper').append($popover);
1743        return $popover;
1744    }
1745
1746    function showDayEventsPopover($el, calId, dateStr, triggerEl) {
1747        var calendar = calendars[calId];
1748        var $popover = ensureDayEventsPopover($el, calId);
1749        var events;
1750        var html;
1751
1752        if (!calendar || !calendar.view) {
1753            return;
1754        }
1755
1756        events = getEventsForDate(calendar.getEvents(), dateStr);
1757        if (!events.length) {
1758            hideDayEventsPopover(calId);
1759            return;
1760        }
1761
1762        html = buildDayEventsPopoverHtml(events, dateStr);
1763        $popover.html(html).show();
1764        positionDayEventsPopover($popover, triggerEl);
1765    }
1766
1767    function hideDayEventsPopover(calId) {
1768        $('#' + calId + '-day-events-popover').hide();
1769    }
1770
1771    function buildDayEventsPopoverHtml(events, dateStr) {
1772        var html = '';
1773        var i;
1774
1775        html += '<div class="mep-cal-day-popover-header">';
1776        html += '<span class="mep-cal-day-popover-title">All Events</span>';
1777        html += '<button type="button" class="mep-cal-day-popover-close" aria-label="Close">&times;</button>';
1778        html += '</div>';
1779        html += '<div class="mep-cal-day-popover-date">' + escapeHtml(formatDate(dateStr)) + '</div>';
1780        html += '<div class="mep-cal-day-popover-list">';
1781
1782        for (i = 0; i < events.length; i++) {
1783            html += buildDayEventsPopoverItem(events[i]);
1784        }
1785
1786        html += '</div>';
1787
1788        return html;
1789    }
1790
1791    function buildDayEventsPopoverItem(event) {
1792        var props = event && event.extendedProps ? event.extendedProps : {};
1793        var accent = props.paletteAccent || getEventPaletteColor(event, event.backgroundColor || '#3a87ad');
1794        var timeText = getEventSummaryTimeText(event);
1795        var url = event && event.url ? String(event.url) : '';
1796        var wrapperTag = url ? 'a' : 'div';
1797        var wrapperAttr = url ? ' href="' + escapeHtml(url) + '"' : '';
1798        var html = '';
1799
1800        html += '<' + wrapperTag + ' class="mep-cal-day-popover-item"' + wrapperAttr + '>';
1801        html += '<span class="mep-cal-day-popover-dot" style="background:' + escapeHtml(accent) + ';"></span>';
1802        html += '<span class="mep-cal-day-popover-content">';
1803        html += '<span class="mep-cal-day-popover-item-title">' + escapeHtml(event.title) + '</span>';
1804        if (timeText) {
1805            html += '<span class="mep-cal-day-popover-item-time">' + escapeHtml(timeText) + '</span>';
1806        }
1807        html += '</span>';
1808        html += '</' + wrapperTag + '>';
1809
1810        return html;
1811    }
1812
1813    function positionDayEventsPopover($popover, triggerEl) {
1814        var rect;
1815        var popoverW;
1816        var popoverH;
1817        var left;
1818        var top;
1819        var viewportW = $(window).width();
1820        var viewportH = $(window).height();
1821
1822        if (!$popover.length || !triggerEl || !triggerEl.getBoundingClientRect) {
1823            return;
1824        }
1825
1826        rect = triggerEl.getBoundingClientRect();
1827        popoverW = $popover.outerWidth();
1828        popoverH = $popover.outerHeight();
1829        left = rect.right - popoverW;
1830        top = rect.bottom - popoverH - 12;
1831
1832        if (left < 12) {
1833            left = 12;
1834        }
1835
1836        if (left + popoverW > viewportW - 12) {
1837            left = viewportW - popoverW - 12;
1838        }
1839
1840        if (top < 12) {
1841            top = rect.top + 12;
1842        }
1843
1844        if (top + popoverH > viewportH - 12) {
1845            top = viewportH - popoverH - 12;
1846        }
1847
1848        $popover.css({
1849            left: left + 'px',
1850            top: top + 'px'
1851        });
1852    }
1853
1854    function getEventSummaryTimeText(event) {
1855        if (!event || !event.start) {
1856            return '';
1857        }
1858
1859        if (event.allDay || (event.extendedProps && event.extendedProps.hideTime === 'yes')) {
1860            return mepCalendar.i18n && mepCalendar.i18n.allDay ? mepCalendar.i18n.allDay : 'All day';
1861        }
1862
1863        if (event.end) {
1864            return formatTime(event.start) + ' - ' + formatTime(event.end);
1865        }
1866
1867        return formatTime(event.start);
1868    }
1869
1870    /**
1871     * Hide tooltip
1872     */
1873    function hideTooltipEl(calId) {
1874        $('#' + calId + '-tooltip').hide();
1875    }
1876
1877    /**
1878     * Show a visible message instead of a blank area if FullCalendar fails to load
1879     */
1880    function renderCalendarError($el) {
1881        $el.addClass('mep-cal-init-error');
1882        if (!$el.children().length) {
1883            $el.html('<div class="mep-cal-error-message">Calendar could not load. Please check the FullCalendar script and refresh the page.</div>');
1884        }
1885    }
1886
1887    function applyCalendarDayAppearance($el, cellEl, date, isHeader) {
1888        if (!cellEl || !date) {
1889            return;
1890        }
1891
1892        var appearance = getCalendarDayAppearance(date);
1893        var targetEl = getCalendarDayTarget(cellEl, isHeader);
1894
1895        if (!appearance.color && !appearance.image) {
1896            return;
1897        }
1898
1899        if (appearance.color) {
1900            targetEl.style.setProperty('background-color', appearance.color, 'important');
1901        }
1902
1903        if (appearance.image && !isHeader) {
1904            targetEl.classList.add('mep-cal-day-has-bg-image');
1905            targetEl.style.setProperty('background-image', 'linear-gradient(rgba(255,255,255,0.76), rgba(255,255,255,0.76)), url("' + appearance.image.replace(/"/g, '\\"') + '")');
1906            targetEl.style.setProperty('background-size', 'cover');
1907            targetEl.style.setProperty('background-position', 'center center');
1908            targetEl.style.setProperty('background-repeat', 'no-repeat');
1909        }
1910
1911        if (appearance.color || appearance.image) {
1912            targetEl.classList.add('mep-cal-day-customized');
1913        }
1914    }
1915
1916    function getCalendarDayTarget(cellEl, isHeader) {
1917        if (isHeader) {
1918            return cellEl.querySelector('.fc-scrollgrid-sync-inner') || cellEl;
1919        }
1920
1921        return cellEl.querySelector('.fc-daygrid-day-frame') ||
1922            cellEl.querySelector('.fc-timegrid-col-frame') ||
1923            cellEl;
1924    }
1925
1926    function getCalendarDayAppearance(date) {
1927        var settings = mepCalendar.settings || {};
1928        var weekdayKey = getWeekdayKey(date);
1929        var dateKey = formatDateKey(date);
1930        var rules = Array.isArray(settings.mep_cal_day_background_rules) ? settings.mep_cal_day_background_rules : [];
1931        var appearance = {
1932            color: settings['mep_cal_weekday_bg_' + weekdayKey] || '',
1933            image: ''
1934        };
1935
1936        for (var i = 0; i < rules.length; i++) {
1937            if (rules[i] && rules[i].type === 'weekday' && rules[i].value === weekdayKey && rules[i].image) {
1938                appearance.image = rules[i].image;
1939            }
1940        }
1941
1942        for (var j = 0; j < rules.length; j++) {
1943            if (rules[j] && rules[j].type === 'date' && rules[j].value === dateKey && rules[j].image) {
1944                appearance.image = rules[j].image;
1945                break;
1946            }
1947        }
1948
1949        return appearance;
1950    }
1951
1952    function getWeekdayKey(date) {
1953        var day = new Date(date).getDay();
1954        var map = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
1955        return map[day] || 'sun';
1956    }
1957
1958    function formatDateKey(date) {
1959        var d = new Date(date);
1960        var year = d.getFullYear();
1961        var month = String(d.getMonth() + 1).padStart(2, '0');
1962        var day = String(d.getDate()).padStart(2, '0');
1963        return year + '-' + month + '-' + day;
1964    }
1965
1966    /**
1967     * Initialize filter controls
1968     */
1969    function initFilters() {
1970        // Search input with debounce
1971        var searchTimer;
1972        $(document).on('input', '.mep-cal-search-input', function() {
1973            var $input = $(this);
1974            var calId = $input.data('calendar-id');
1975            
1976            clearTimeout(searchTimer);
1977            searchTimer = setTimeout(function() {
1978                if (calendars[calId]) {
1979                    calendars[calId].refetchEvents();
1980                }
1981            }, 400);
1982        });
1983
1984        // Category select filter
1985        $(document).on('change', '.mep-cal-category-select', function() {
1986            var calId = $(this).data('calendar-id');
1987            if (calendars[calId]) {
1988                calendars[calId].refetchEvents();
1989            }
1990        });
1991
1992        $(document).on('change', '.mep-cal-organizer-select, .mep-cal-date-start, .mep-cal-date-end', function() {
1993            var calId = $(this).data('calendar-id');
1994            if (calendars[calId]) {
1995                calendars[calId].refetchEvents();
1996            }
1997        });
1998
1999        $(document).on('input', '.mep-cal-location-input', function() {
2000            var $input = $(this);
2001            var calId = $input.data('calendar-id');
2002
2003            clearTimeout(searchTimer);
2004            searchTimer = setTimeout(function() {
2005                if (calendars[calId]) {
2006                    calendars[calId].refetchEvents();
2007                }
2008            }, 400);
2009        });
2010
2011        $(document).on('click', '.mep-cal-reset-button', function() {
2012            var calId = $(this).data('calendar-id');
2013            var $wrapper = $(this).closest('.mep-calendar-wrapper');
2014
2015            $wrapper.find('.mep-cal-search-input').val('');
2016            $wrapper.find('.mep-cal-category-select').val('');
2017            $wrapper.find('.mep-cal-organizer-select').val('');
2018            $wrapper.find('.mep-cal-location-input').val('');
2019            $wrapper.find('.mep-cal-date-start').val('');
2020            $wrapper.find('.mep-cal-date-end').val('');
2021
2022            if (calendars[calId]) {
2023                calendars[calId].refetchEvents();
2024            }
2025        });
2026
2027        $(document).on('click', '.mep-cal-day-summary-button', function(e) {
2028            var $button = $(this);
2029            var calId = $button.data('calendar-id');
2030            var dateStr = $button.data('date');
2031            var $calendarEl = $('#' + calId);
2032            var $popover = $('#' + calId + '-day-events-popover');
2033
2034            e.preventDefault();
2035            e.stopPropagation();
2036
2037            if ($popover.length && $popover.is(':visible') && $popover.data('activeDate') === dateStr) {
2038                hideDayEventsPopover(calId);
2039                $popover.removeData('activeDate');
2040                return;
2041            }
2042
2043            showDayEventsPopover($calendarEl, calId, dateStr, this);
2044            $('#' + calId + '-day-events-popover').data('activeDate', dateStr);
2045        });
2046
2047        $(document).on('click', '.mep-cal-day-popover-close', function(e) {
2048            var $popover = $(this).closest('.mep-cal-day-events-popover');
2049
2050            e.preventDefault();
2051            $popover.hide().removeData('activeDate');
2052        });
2053
2054        $(document).on('click', function(e) {
2055            if (!$(e.target).closest('.mep-cal-day-events-popover, .mep-cal-day-summary-button').length) {
2056                $('.mep-cal-day-events-popover').hide().removeData('activeDate');
2057            }
2058        });
2059    }
2060
2061    /**
2062     * Format date for display
2063     */
2064    function formatDate(date) {
2065        if (!date) return '';
2066        return formatIntlDate(date, '', { year: 'numeric', month: 'short', day: 'numeric' });
2067    }
2068
2069    /**
2070     * Format time for display
2071     */
2072    function formatTime(date) {
2073        if (!date) return '';
2074        return formatIntlTime(date, '', { hour: '2-digit', minute: '2-digit' });
2075    }
2076
2077    function normalizeDateInput(date) {
2078        if (date instanceof Date) {
2079            return date;
2080        }
2081
2082        if (typeof date === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(date)) {
2083            return new Date(date + 'T00:00:00');
2084        }
2085
2086        return new Date(date);
2087    }
2088
2089    /**
2090     * Use a readable event time instead of FullCalendar's compact month token like "9a"
2091     */
2092    function formatEventTimeText(arg) {
2093        if (!arg || !arg.event || !arg.event.start) return '';
2094
2095        var startText = formatTime(arg.event.start);
2096        if (!startText) return '';
2097
2098        if (arg.view && arg.view.type === 'timeGridDay' && arg.event.end) {
2099            return startText + ' - ' + formatTime(arg.event.end);
2100        }
2101
2102        return startText;
2103    }
2104
2105    /**
2106     * Normalize height values passed from shortcode/admin settings
2107     */
2108    function normalizeCalendarHeight(value) {
2109        if (!value) return 'auto';
2110
2111        value = String(value).trim();
2112        if (!value || value === 'auto' || value === 'parent') {
2113            return value || 'auto';
2114        }
2115
2116        if (/^\d+$/.test(value)) {
2117            return parseInt(value, 10);
2118        }
2119
2120        return value;
2121    }
2122
2123    /**
2124     * HTML escape utility
2125     */
2126    function escapeHtml(text) {
2127        if (!text) return '';
2128        text = String(text);
2129        var map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' };
2130        return text.replace(/[&<>"']/g, function(m) { return map[m]; });
2131    }
2132
2133    // Expose init function for AJAX-loaded calendars
2134    window.mepCalendarInit = initAllCalendars;
2135
2136})(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.