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 + '">↻</span>'; 652 } else if (props.eventType === 'yes') { 653 html += ' <span class="mep-cal-event-badge" title="' + mepCalendar.i18n.multiDate + '">📅</span>'; 654 } 655 if (props.eventMode === 'online') { 656 html += ' <span class="mep-cal-event-badge" title="' + mepCalendar.i18n.virtual + '">🌐</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">×</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 = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; 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.