PageSourceSearch

https://www.srimed.ro/wp-content/plugins/mage-eventpress/assets/helper/calendar/calendar.js?ver=1790325015

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