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