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