PageSourceSearch

https://seisenegg.at/wp-content/plugins/mage-eventpress/assets/helper/calendar/calendar.js?ver=1791509965

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