1function CalendarEmbed(config) { 2 var x = this; 3 var defaults = { 4 $container: $("#events"), 5 $hubEventSideBar: $("#eventSideBar"), 6 $eventList: $(".eventsListView"), 7 $eventsCalendar: $("#eventsCalendar"), 8 $calendarCategories: $("#event-calendar-category-select"), 9 $calendarSearch: $(".calendar-search-outer"), 10 $meetingDisplayFormatToggle: $(".meeting-display-format-toggle"), 11 $eventsCalendarInfoModal: $("#eventsCalendarInfoModal"), 12 $eventsContainer: $("#events").parent(), 13 customerId: -1, 14 projectId: -1, 15 departmentId: -1, 16 assetTagIdList: [], 17 electedOfficialId: -1, 18 userId: "", 19 buildHubUI: false, 20 searchString: "", 21 hubEventsOnly: false, 22 buildElectedOfficialUI: false, 23 buildMeetingSeriesUI: false, 24 displayFormat: "list", 25 displayFormatChangeCallback: function (format) {} 26 }; 27 28 config = $.extend({}, defaults, config || {}); 29 x.config = config; 30 x.$container = config.$container; 31 x.$hubEventSideBar = config.$hubEventSideBar; 32 x.$eventList = config.$eventList; 33 x.$eventsCalendar = config.$eventsCalendar; 34 x.$calendarCategories = config.$calendarCategories; 35 x.$calendarSearch = config.$calendarSearch; 36 x.$meetingDisplayFormatToggle = config.$meetingDisplayFormatToggle; 37 x.$eventsCalendarInfoModal = config.$eventsCalendarInfoModal; 38 x.$eventsContainer = config.$eventsContainer; 39 x.userId = config.userId; 40 x.customerId = config.customerId; 41 x.departmentId = config.departmentId; 42 x.projectId = config.projectId; 43 x.electedOfficialId = config.electedOfficialId; 44 x.hubEventsOnly = config.hubEventsOnly; 45 x.clearLoadedItemCache(); 46 x.UI = config.pageUI; 47 x.modelData = config.modelData || {}; 48 x.mdScreenBreakpoint = 991; 49 50 x.modelData.eventIsToday = x.$eventList 51 .find(".meeting-preview-outer") 52 .data("event-is-today"); 53 if (x.config.buildHubUI) { 54 x.buildHubUI(); 55 } 56 if (x.config.buildElectedOfficialUI) { 57 x.buildElectedOfficialUI(); 58 } 59} 60 61CalendarEmbed.prototype.buildHubUI = function () { 62 var x = this; 63 var DEFAULT_PAGE_SIZE = 5; // should match 'PageSize' field's default value in DataModels/OfflineEvent/OfflineEventSearchViewModel.cs 64 65 x.$container.html(TOOLS.spinBlock("Loading meetings & events")); 66 $("#home-events").html(TOOLS.spinBlock("Loading meetings & events")); 67 x.setDisplayFormat(x.config.displayFormat); 68 x.initUiListeners(); 69 70 $.post("/Calendar/Index", { 71 customerId: x.customerId, 72 departmentId: x.departmentId, 73 projectId: x.projectId, 74 assetTagIdList: x.config.assetTagIdList, 75 hubEventsOnly: x.hubEventsOnly, 76 pageSize: DEFAULT_PAGE_SIZE 77 }) 78 .done(function (data) { 79 var $calendarEventList = x.initLayout(data); 80 81 x.$container.html($calendarEventList); 82 x.setDisplayFormat(x.config.displayFormat); 83 84 var $search = $(data).find(".calendar-search-outer"); 85 $search.removeClass("col-md-4 col-md-push-8"); 86 87 function moveSearch() { 88 // if mobile, put search at top 89 if ($(window).width() <= x.mdScreenBreakpoint) { 90 x.$container.prepend($search); 91 } else { 92 // put in sidebar 93 x.$hubEventSideBar.append($search); 94 } 95 // Bug 8417: chosen.js width is computed from parent at init; reflow after each move. 96 $search.find(".chosen-select").trigger("chosen:updated"); 97 } 98 99 moveSearch(); 100 $(window).on("resize", TOOLS.debounce(moveSearch, 500)); 101 102 x.$calendarSearch = $search; 103 x.$eventList = x.$container; 104 // home page 105 var $calendarEventGrid = $(data).find(".calendar-events-grid-outer"); 106 var hasUpcomingEvents = $calendarEventGrid.find( 107 ".upcoming-events .card" 108 ).length; 109 if (hasUpcomingEvents) { 110 var sectionTitle = $( 111 '<h3 class="page-title-h3">Featured Upcoming Events</h3>' 112 ); 113 $calendarEventGrid.prepend(sectionTitle); 114 } 115 116 $calendarEventGrid.find(".showMore").hide(); 117 var viewMoreEventsBtn = $( 118 '<div><button class="btn btn-primary pi-btn-primary btn-s-md btn-trans
118parent btn-borderless pull-right switchTabs" data-rel="events">View all meetings & events <span class="fas fa-chevron-right"></span></button></div>' 119 ); 120 $calendarEventGrid.append(viewMoreEventsBtn); 121 var $clearfixDiv = $('<div style="clear: both;"></div>'); 122 $calendarEventGrid.append($clearfixDiv); 123 $("#home-events").html($calendarEventGrid); 124 125 x.init(); 126 }) 127 .fail(TOOLS.failDefault); 128}; 129 130CalendarEmbed.prototype.buildElectedOfficialUI = function () { 131 var x = this; 132 x.$container.html(TOOLS.spinBlock("Loading meetings & events")); 133 134 $.post("/Calendar/Index", { 135 customerId: x.customerId, 136 departmentId: x.departmentId, 137 projectId: x.projectId, 138 hubEventsOnly: x.hubEventsOnly, 139 isElectedOfficialCategory: true, 140 electedOfficialId: x.electedOfficialId 141 }) 142 .done(function (data) { 143 var $calendarEventGrid = $(data).find(".calendar-events-grid-outer"); 144 x.$container.html($calendarEventGrid); 145 x.$eventList = x.$container; 146 x.init(); 147 }) 148 .fail(TOOLS.failDefault); 149}; 150 151CalendarEmbed.prototype.init = function () { 152 var x = this; 153 x.initDateRangePicker(); 154 TOOLS.requireCSSFile("/Content/Comments.css"); 155 x.initPostLoadUIFunctions(); 156 157 if ($(window).width() <= x.mdScreenBreakpoint) { 158 $(".searchBar").removeClass("in"); 159 $(".searchBar") 160 .parent() 161 .find(".glyphicon-minus") 162 .removeClass("glyphicon-minus") 163 .addClass("glyphicon-plus"); 164 } 165 166 $(".searchBar") 167 .on("shown.bs.collapse", function () { 168 $(this) 169 .parent() 170 .find(".glyphicon-plus") 171 .removeClass("glyphicon-plus") 172 .addClass("glyphicon-minus"); 173 }) 174 .on("hidden.bs.collapse", function () { 175 $(this) 176 .parent() 177 .find(".glyphicon-minus") 178 .removeClass("glyphicon-minus") 179 .addClass("glyphicon-plus"); 180 }); 181 182 $(window).resize(function () { 183 if ($(window).width() > x.mdScreenBreakpoint) { 184 $(".searchBar").addClass("in"); 185 $(".searchBar") 186 .parent() 187 .find(".glyphicon-plus") 188 .removeClass("glyphicon-plus") 189 .addClass("glyphicon-minus"); 190 } 191 }); 192 x.initEventListingUiListeners(); 193 194 if (x.config.buildMeetingSeriesUI) { 195 const event = new Event("initCompleted"); 196 window.dispatchEvent(event); 197 } 198}; 199 200CalendarEmbed.prototype.initDateRangePicker = function () { 201 var x = this; 202 if (x.dateRangePickerInitialized) { 203 return; 204 } 205 x.dateRangePickerInitialized = true; 206 207 var $dateInputs = x.$calendarSearch.find("#events-daterange input"); 208 if (!$dateInputs.length) { 209 return; 210 } 211 212 // Use native HTML5 date inputs as fallback for BS5 compatibility 213 $dateInputs.attr("type", "date"); 214}; 215 216CalendarEmbed.prototype.registrationHandler = function (button) { 217 var eventId = $(button).data("offline-event-id"); 218 // show spinner while waiting for load 219 var $registrationBtn = $(button); 220 var $spinner = $registrationBtn.next(".event-registration-modal-loading"); 221 $registrationBtn.hide(); 222 $spinner.show(); 223 224 this.loadEventRegistrationModal(eventId, function () { 225 $spinner.hide(); 226 $registrationBtn.show(); 227 }); 228}; 229 230CalendarEmbed.prototype.submitSearch = function (callback) { 231 var formData = this.$calendarSearch 232 .find("#calendarSearchForm") 233 .serializeObject(); 234 this.search(formData, callback); 235}; 236 237CalendarEmbed.prototype.initUiListeners = function () { 238 /** 239 * Initializes the listeners for ui elements not dependent on the event list UI 240 */ 241 var x = this; 242 243 x.$meetingDisplayFormatToggle.on("click", "button", function (e) { 244 var format = $(this).data("display-format"); 245 246 x.setDisplayFormat(format); 247 x._updateUrlFromSelectedEventFormat(); 248 }); 249}; 250 251// Bug 8417: chosen() inherits the underlying <select>'s computed width at init time, and 252// the search panel is initially inside #eventSideBar (display:none) before being reparented 253// for mobile, so without an explicit width it renders a 0-width control. The mobile branch 254// of search() also removes .chosen-container after each search and expects re-init destroy 255// the prior instance first so chosen() actually re-runs (otherwise the dropdown reverts to 256// an unstyled native <select>). 257CalendarEmbed.prototype.initChosen = function () { 258 var x = this; 259 if (x.$calendarSearch.length === 0) { 260 return; 261 } 262 // Degrade to the plain <select> if the host view didn't load chosen â throwing here would 263 // abort initEventListingUiListeners() and leave every listener bound after it unwired. 264 if (typeof $.fn.chosen !== "function") { 265 return; 266 } 267 var $select = x.$calendarSearch.find(".chosen-select"); 268 if (!$select.length) { 269 return; 270 } 271 if ($select.data("chosen")) { 272 $select.chosen("destroy"); 273 } 274 $select.chosen({ 275 // eslint-disable-next-line camelcase 276 placeholder_text_multiple: "Select categories", 277 width: "100%" 278 }); 279 $select.trigger("chosen:updated"); 280}; 281 282CalendarEmbed.prototype.initEventListingUiListeners = function () { 283 /** 284 * Initializes the on-click listeners for the event listing ui - run after the UI partial is loaded on the page. 285 */ 286 var x = this; 287 288 // Prevent double-binding by checking if already initialized 289 if (x.eventListenersInitialized) { 290 console.warn("Event listeners already initialized for CalendarEmbed"); 291 return; 292 } 293 x.eventListenersInitialized = true; 294 295 if (x.$calendarSearch.length > 0) { 296 if (window.innerWidth > x.mdScreenBreakpoint) { 297 // remove placeholder option in large viewports 298 x.$calendarSearch.find(".chosen-select option:first-child").remove(); 299 } 300 x.initChosen(); 301 302 x.$calendarSearch.on("submit", "#calendarSearchForm", function (e) { 303 e.preventDefault(); 304 var formData = $(this).serializeObject(); 305 x.search(formData); 306 });
307 308 x.$calendarSearch.on("change", "[name=Categories]", function () { 309 x.submitSearch(function () { 310 if (x.$calendarSearch.find("[name=Categories]").val().length) { 311 x.$calendarSearch.find(".clearSearchBtn.clear-categories").show(); 312 } 313 }); 314 if (x.$calendarSearch.find("[name=Categories]").val().length) { 315 x.$calendarSearch.find(".clearSearchBtn.clear-categories").show(); 316 } 317 }); 318 319 x.$calendarSearch.on("input", "[name=SearchString]", function () { 320 x.$calendarSearch.find(".clearSearchBtn.clear-text").show(); 321 clearTimeout(x.searchInputTimeout); 322 x.searchInputTimeout = setTimeout(function () { 323 x.submitSearch(function () { 324 x.$calendarSearch.find(".clearSearchBtn.clear-text").show(); 325 }); 326 }, 800); 327 }); 328 329 x.$calendarSearch.on("change", ".input-daterange input", function () { 330 x.submitSearch(function () { 331 if ( 332 x.$calendarSearch.find("input[name='StartDate']").val().length || 333 Number(x.$calendarSearch.find("input[name='EndDate']").val().length) 334 ) { 335 x.$calendarSearch.find(".clearSearchBtn.clear-date").show(); 336 } 337 }); 338 339 if ( 340 x.$calendarSearch.find("input[name='StartDate']").val().length || 341 Number(x.$calendarSearch.find("input[name='EndDate']").val().length) 342 ) { 343 x.$calendarSearch.find(".clearSearchBtn.clear-date").show(); 344 } 345 }); 346 347 x.$calendarSearch.on("click", ".clearSearchBtn", function () { 348 if ($(this).hasClass("clear-text")) { 349 x.$calendarSearch.find("#SearchString").val(""); 350 x.$calendarSearch.find(".clearSearchBtn.clear-text").hide(); 351 clearTimeout(x.searchInputTimeout); 352 x.submitSearch(); 353 } else if ($(this).hasClass("clear-date")) { 354 x.$calendarSearch.find("input[name='StartDate']").val(""); 355 x.$calendarSearch.find("input[name='EndDate']").val(""); 356 x.$calendarSearch.find(".clearSearchBtn.clear-date").hide(); 357 x.submitSearch(); 358 } else if ($(this).hasClass("clear-categories")) { 359 x.$calendarSearch 360 .find("[name=Categories]") 361 .val("") 362 .trigger("chosen:updated"); 363 x.$calendarSearch.find(".clearSearchBtn.clear-categories").hide(); 364 x.submitSearch(); 365 } 366 }); 367 368 x.$calendarSearch.one( 369 "mousedown", 370 ".form-control.chosen-select", 371 function () { 372 // deselect placeholder upon interacting with select 373 $(this).find("option:first-child").prop("selected", false); 374 } 375 ); 376 } 377 378 var excludedElements = 379 "a, button, .dropdown-toggle, .dropdown-menu, .event-registration-modal, .meeting-tab-content, .action-btns, .add-to-calendar"; 380 381 // Custom collapse for card elements excluding interactive elements 382 x.$eventList.on("click", ".cz-card", function (e) { 383 // If the click target is an excluded element or within one, do nothing 384 if ( 385 !$(e.target).is(excludedElements) && 386 !$(e.target).closest(excludedElements).length 387 ) { 388 let $collapseDiv = $(this).find(".panel-collapse"); 389 390 // Check if the collapse element exists 391 if ($collapseDiv.length) { 392 // Toggle the collapse state 393 $collapseDiv.collapse("toggle"); 394 } 395 } 396 }); 397 398 x.$eventList.on("click", ".showMore", function (e) { 399 // Prevent double-clicks and duplicate requests 400 if ($(this).hasClass("loading") || $(this).is(":disabled")) { 401 return false; 402 } 403 404 var isPastEvent = 405 $(this).data("past-event") === true || 406 $(this).data("past-event") === "true"; 407 408 var postData = { 409 page: parseInt($(this).data("page")), 410 pastEventsOnly: isPastEvent, 411 futureEventsOnly: !isPastEvent 412 }; 413 x.showMore(postData, e); 414 }); 415 416 x.$eventList.on("click", ".load-agenda-ui", function () { 417 x.meetingAgendaId = $(this).data("agenda-id"); 418 x.eventId = $(this).data("offline-event-id"); 419 x.loadAgendaUI(); 420 }); 421 422 x.$eventList.on("click", ".calendar-video-toggle", function () { 423 var $videoUI = x.$eventList.find($(this).attr("href")); 424 setTimeout(function () { 425 $videoUI.find("iframe").css("width", "100%"); 426 }, 100); 427 }); 428 429 x.$eventList.on("click", ".load-comments-ui", function () { 430 x.eventId = $(this).data("offline-event-id"); 431 x.projectId = $(this).data("project-id"); 432 x.loadCommentSection(); 433 }); 434 435 x.$eventList.on("click", ".toggle-event-comments", function () { 436 x.$eventList.find(".comment-container-parent").show(); 437 }); 438 439 x.$eventList.on("click", ".download-offline-document", function (e) { 440 e.preventDefault(); 441 window.location.href = "/DownloadDocument/" + $(this).data("storage-id"); 442 }); 443 444 x.$eventList.on("shown.bs.collapse", ".collapse", function (e) { 445 // we just opened this event 446 var $outerDiv = $(this).closest(".meeting-preview-outer"); 447 var offlineEventId = $outerDiv.data("offline-event-id"); 448 x.eventId = offlineEventId; 449 450 var $inlineRsvp = $outerDiv.find(".rsvp-outer-inline");
451 if ($inlineRsvp.length) { 452 x.loadRegistrationTab(offlineEventId, $inlineRsvp); 453 return; 454 } 455 456 var $tabLi = $outerDiv.find(".tab:visible").first(); 457 $tabLi.addClass("active"); 458 var tabLink = $tabLi.find("a").attr("href"); 459 $tabLi.find("a").attr("aria-selected", "true"); 460 $(tabLink).addClass("active"); 461 if ($tabLi.find("a").hasClass("toggle-event-comments")) { 462 $tabLi.find("a").trigger("click"); 463 } 464 if ($tabLi.find("a").hasClass("load-registration-tab")) { 465 x.loadRegistrationTab(offlineEventId, $(tabLink)); 466 } 467 }); 468 469 x.$eventList.on("click", ".tab", function () { 470 var $clickedLi = $(this); 471 var tabLink = $(this).find("a").attr("href"); 472 var $outerDiv = $(this).closest(".meeting-preview-outer"); 473 var $currentActiveLi = $outerDiv.find(".tab.active"); 474 var $currentActiveTabPane = $outerDiv.find(".tab-pane.active"); 475 if ($clickedLi.hasClass("active")) { 476 return; 477 } 478 $currentActiveLi.removeClass("active"); 479 $currentActiveLi.find("a").attr("aria-selected", "false"); 480 $currentActiveTabPane.removeClass("active"); 481 $(tabLink).addClass("active"); 482 $clickedLi.addClass("active"); 483 $clickedLi.find("a").attr("aria-selected", "true"); 484 485 if ($clickedLi.find("a").hasClass("toggle-event-comments")) { 486 $clickedLi.find("a").trigger("click"); 487 } 488 if ($clickedLi.find("a").hasClass("load-registration-tab")) { 489 var eventId = $clickedLi.find("a").data("offline-event-id"); 490 x.loadRegistrationTab(eventId, $(tabLink)); 491 } 492 }); 493 494 x.$eventList.on("click", ".event-registration-modal", function () { 495 x.registrationHandler.call(x, this); 496 }); 497 498 /* custom collapse event for filter and calendar search to override BS5 classes for board commission and meeting series */ 499 x.$calendarSearch.on("click", ".calendar-search-toggle", function () { 500 let $btn = $(this); 501 let $calendarFilterCollapse = $($btn.attr("data-href")); 502 if ($calendarFilterCollapse.is(":visible")) { 503 $btn.find(".fa-regular").addClass("fa-plus").removeClass("fa-minus"); 504 $btn.attr("aria-expanded", "false"); 505 } else { 506 $btn.find(".fa-regular").addClass("fa-minus").removeClass("fa-plus"); 507 $btn.attr("aria-expanded", "true"); 508 } 509 $calendarFilterCollapse.slideToggle(); 510 }); 511 512 // Ensure accessibility watchers are set up for expand buttons 513 x.initExpandButtonAccessibilityWatcher(); 514}; 515 516CalendarEmbed.prototype.getDisplayFormat = function () { 517 var x = this; 518 var format = x.$meetingDisplayFormatToggle.find("input").val(); 519 520 return format; 521}; 522 523CalendarEmbed.prototype.setDisplayFormat = function (format) { 524 var x = this; 525 526 x.config.displayFormat = format; 527 x.$meetingDisplayFormatToggle.find("input").val(format); 528 529 x.updateUiToSelectedDisplayFormat(format); 530}; 531 532CalendarEmbed.prototype.updateUiToSelectedDisplayFormat = function () { 533 var x = this; 534 var format = x.getDisplayFormat(); 535 536 x.$meetingDisplayFormatToggle 537 .find("button") 538 .removeClass("active") 539 .attr("aria-pressed", "false"); 540 x.$meetingDisplayFormatToggle 541 .find("button[data-display-format='" + format + "']") 542 .addClass("active") 543 .attr("aria-pressed", "true"); 544 545 if (format === "calendar") { 546 x.$eventList.hide(); 547 x.$hubEventSideBar.hide(); 548 x.$eventsCalendar.show(); 549 x.$calendarCategories.show(); 550 } else { 551 x.$eventList.show(); 552 x.$hubEventSideBar.show(); 553 x.$eventsCalendar.hide(); 554 x.$calendarCategories.hide(); 555 } 556 557 x.config.displayFormatChangeCallback(format); 558}; 559CalendarEmbed.prototype._updateUrlFromSelectedEventFormat = function () { 560 var x = this; 561 562 var format = x.getDisplayFormat(); 563 564 // Get current URL parameters 565 var urlParams = new URLSearchParams(window.location.search); 566 567 // Update only the mtgdisplay parameter 568 if (format == "calendar") { 569 urlParams.set("mtgdisplay", "calendar"); 570 } else { 571 urlParams.set("mtgdisplay", "list"); 572 } 573 574 // Construct new URL with all parameters preserved 575 let newUrl = `${window.location.origin}${ 576 window.location.pathname 577 }
577?${urlParams.toString()}#events`; 578 579 window.history.replaceState({}, "mtgdisplay", newUrl); 580}; 581 582CalendarEmbed.prototype.showEventsInfoModal = function (data) { 583 var x = this; 584 585 var now = new Date(data.event._instance.range.start); 586 var event_date = new Date(now.toUTCString().slice(0, -4)); 587 var daysOfWeek = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; 588 var months = [ 589 "Jan", 590 "Feb", 591 "Mar", 592 "Apr", 593 "May", 594 "Jun", 595 "Jul", 596 "Aug", 597 "Sep", 598 "Oct", 599 "Nov", 600 "Dec" 601 ]; 602 var dayOfWeek = daysOfWeek[event_date.getDay()]; 603 var month = months[event_date.getMonth()]; 604 var dayOfMonth = event_date.getDate(); 605 var hours = event_date.getHours() % 12 || 12; 606 var minutes = event_date.getMinutes(); 607 var amPm = event_date.getHours() < 12 ? "AM" : "PM"; 608 var formattedDate = `${dayOfWeek}, ${month} ${dayOfMonth} ${( 609 "0" + hours 610 ).slice(-2)}:${("0" + minutes).slice(-2)} ${amPm}`; 611 612 let event_data_obj = { 613 "calendar-event-title": data.event._def.title || "Untitled Event", 614 "calender-event-address": data.event._def.extendedProps.address, 615 "calendar-event-date-time": formattedDate, 616 "calendar-event-month": month, 617 "calendar-event-date": dayOfMonth 618 }; 619 620 if ( 621 data.event._def.extendedProps.address.length == 0 || 622 data.event._def.extendedProps.address == "" 623 ) { 624 x.$eventsCalendarInfoModal.find(".event-address-wrapper").hide(); 625 } else { 626 x.$eventsCalendarInfoModal.find(".event-address-wrapper").show(); 627 } 628 var arrObj = Object.entries(event_data_obj); 629 arrObj.forEach((ele) => { 630 x.$eventsCalendarInfoModal.find(`.${ele[0]}`).html(ele[1]); 631 }); 632 x.$eventsCalendarInfoModal 633 .find(".calendar-event-page-url") 634 .attr("href", data.event._def.extendedProps.pageurl), 635 x.$eventsCalendarInfoModal.modal("show"); 636}; 637 638CalendarEmbed.prototype.loadAgendaUI = function () { 639 var x = this; 640 var eventId = x.eventId; 641 var $agendaUI = x.$eventList.find("#event_agenda_" + eventId); 642 643 if (!$agendaUI.length) { 644 return; 645 } 646 647 if (x.loadedAgendas.includes(eventId)) { 648 return; 649 } 650 651 // Nested event lists (e.g. upcoming + past on the project page) both 652 // delegate the agenda-tab click, so two CalendarEmbed instances can target 653 // the same pane. The flag lives on the shared DOM node, so only the first 654 // handler issues the request. 655 if ($agendaUI.data("agenda-loading") || $agendaUI.data("agenda-loaded")) { 656 return; 657 } 658 $agendaUI.data("agenda-loading", true); 659 660 $agendaUI.html(TOOLS.spinBlock("Loading agenda")); 661 662 $.post("/MeetingAgenda/ListAgendasOnLiveSite/" + eventId) 663 .done(function (data) { 664 $agendaUI.html(data); 665 $agendaUI.find("[data-toggle=tooltip]").tooltip(); 666 $agendaUI.data("agenda-loaded", true); 667 x.loadedAgendas.push(eventId); 668 }) 669 .fail(function (xhr) { 670 $agendaUI.text( 671 "The agenda could not be loaded. Select the Agenda tab to try again." 672 ); 673 TOOLS.failDefault(xhr); 674 }) 675 .always(function () { 676 $agendaUI.removeData("agenda-loading"); 677 }); 678}; 679 680CalendarEmbed.prototype.loadEventRegistrationModal = function ( 681 eventId, 682 callback 683) { 684 var x = this; 685 x.makeExistEventRegistrationModal(); 686 x.$eventRegistrationModal.modal("show"); 687 688 if (x.$eventRegistrationModal.data("offline-event-id") == eventId) { 689 x.$eventRegistrationModal.show(); 690 x.focusOnMeetingRegistrationModal(); 691 callback && callback(); 692 return; 693 } 694 695 x.$eventRegistrationModal 696 .find(".rsvp-form-outer") 697 .html(TOOLS.spinBlock("Loading registration")); 698 699 $.ajax({ 700 url: "/Events/LoadEventRegistrationModal", 701 type: "POST", 702 data: { id: eventId }, 703 success: function (data) { 704 x.$eventRegistrationModal.data("offline-event-id", eventId); 705 x.$eventRegistrationModal.find(".rsvp-form-outer").html(data); 706 x.loadedRegistration.push(eventId); 707 x._initOfflineEventUI(eventId); 708 x.focusOnMeetingRegistrationModal(); 709 callback && callback(); 710 } 711 }); 712}; 713
714CalendarEmbed.prototype.focusOnMeetingRegistrationModal = function () { 715 var x = this; 716 717 x.$eventRegistrationModal.find("#MeetingRegistrationFormModal").focus(); 718}; 719 720CalendarEmbed.prototype.makeExistEventRegistrationModal = function () { 721 var that = this; 722 723 that.$eventRegistrationModal = $("#event-rsvp-modal"); 724 var modalTitle = "Registration form"; 725 // If SA 726 if (that.config && that.config.customerId == 2641) { 727 modalTitle = "Request To Speak"; 728 } 729 730 if (that.$eventRegistrationModal.length == 0) { 731 that.$eventRegistrationModal = $( 732 '<div class="modal fade event-rsvp-modal" id="event-rsvp-modal" aria-hidden="true">' + 733 '<div class="modal-dialog modal-dialog-centered">' + 734 '<div id="MeetingRegistrationFormModal" class="modal-content" tabindex="0">' + 735 '<div class="modal-header customer-color">' + 736 '<button type="button" class="close" data-dismiss="modal" data-bs-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>' + 737 '<h4 class="modal-title">' + 738 modalTitle + 739 "</h4>" + 740 "</div>" + 741 '<div class="modal-body m-b">' + 742 '<div class="rsvp-form-outer">' + 743 "</div>" + 744 "</div>" + 745 "</div>" + 746 "</div>" + 747 "</div>" 748 ); 749 $("body").append(that.$eventRegistrationModal); 750 } else { 751 // update extising modal title 752 that.$eventRegistrationModal.first().find(".modal-title").text(modalTitle); 753 } 754}; 755 756CalendarEmbed.prototype.showMore = function (postData, e) { 757 var x = this; 758 postData.customerId = x.customerId; 759 postData.departmentId = x.departmentId; 760 postData.hubEventsOnly = x.hubEventsOnly; 761 postData.projectId = x.projectId; 762 postData.isGridLayout = Boolean( 763 $("input[name='eventLayout']:checked").val() == "grid" || 764 x.electedOfficialId > 0 765 ); 766 if ( 767 x.$calendarSearch.find("#calendarSearchForm [name=SearchString]").length 768 ) { 769 postData.searchString = x.$calendarSearch 770 .find("#calendarSearchForm [name=SearchString]") 771 .val(); 772 } 773 774 var $showMoreButton = e == null ? $() : $(e.target); 775 776 // Set loading state to prevent duplicate requests 777 $showMoreButton.addClass("loading").prop("disabled", true); 778 $showMoreButton.hide(); 779 if (postData.pastEventsOnly) { 780 var $spinner = x.$eventList.find(".pastShowMoreSpinner"); 781 var $destination = postData.isGridLayout 782 ? x.$eventList.find(".past-events .card-deck") 783 : x.$eventList.find(".past-events .accordion"); 784 } else { 785 var $spinner = x.$eventList.find(".showMoreSpinner"); 786 var $destination = postData.isGridLayout 787 ? x.$eventList.find(".upcoming-events .card-deck") 788 : x.$eventList.find(".upcoming-events .accordion"); 789 } 790 791 $spinner.html(TOOLS.spinBlock("Loading more...")); 792 793 $.ajax({ 794 url: "/Calendar/ShowMoreEvents", 795 type: "POST", 796 data: postData, 797 success: function (data) { 798 $spinner.remove(); 799 800 var $pagination = $(data).find(".pagination-wrapper"); 801 802 if ($pagination.length) { 803 $showMoreButton.closest(".pagination-wrapper").replaceWith($pagination); 804 } else { 805 $showMoreButton.remove(); 806 } 807 808 if (postData.isGridLayout) { 809 var $searchedList = $(data) 810 .find(".calendar-events-grid-outer .card-deck") 811 .children(); 812 } else { 813 var $searchedList = $(data) 814 .find(".calendar-events-list-outer .accordion") 815 .children(); 816 } 817 818 $destination.append($searchedList); 819 // No need to bind click handlers here - event delegation in initEventListingUiListeners handles all buttons 820 x.initPostLoadUIFunctions(); 821 }, 822 error: function () { 823 // Remove loading state on error 824 $showMoreButton.removeClass("loading").prop("disabled", false).show(); 825 $spinner.remove(); 826 } 827 }); 828}; 829CalendarEmbed.prototype.search = function (formData, callback) { 830 var x = this; 831 var layoutSelected = $("input[name='eventLayout']:checked").val(); 832 833 formData.customerId = x.customerId; 834 formData.departmentId = x.departmentId; 835 formData.projectId = x.projectId; 836 formData.hubEventsOnly = x.hubEventsOnly; 837 formData.isGridLayout = layoutSelected == "grid"; 838 839 x.$eventList.html(TOOLS.spinBlock("Loading matching events and meetings...")); 840 841 $.post("/Calendar/SearchCalendarEvents", formData) 842 .done(function (data) { 843 var $calendarEventList = x.initLayout(data); 844 845 // if mobile, put search at top 846 if ($(window).width() <= x.mdScreenBreakpoint) { 847 x.$calendarSearch.find(".chosen-container").remove(); 848 $calendarEventList.prepend(x.$calendarSearch); 849 x.dateRangePickerInitialized = false; 850 x.init(); 851 // Bug 8417 mobile post-search: x.init() initEventListingUiListeners() short-circuits 852 // on the eventListenersInitialized guard, so chosen() never re-runs after we removed 853 // .chosen-container above. Re-init the chosen widget explicitly. 854 x.initChosen(); 855 } 856 857 if (formData.isGridLayout) { 858 $calendarEventList.find(".calendar-events-grid-outer").show(); 859 $calendarEventList.find(".list-view-events").hide(); 860 } else { 861 $calendarEventList.find(".calendar-events-grid-outer").hide(); 862 $calendarEventList.find(".list-view-events").show(); 863 } 864 865 x.$eventList.html($calendarEventList); 866 x.clearLoadedItemCache(); 867 868 x.initPostLoadUIFunctions(); 869 callback && callback(); 870 }) 871 .fail(TOOLS.failDefault); 872};
873 874CalendarEmbed.prototype.clearLoadedItemCache = function () { 875 var x = this; 876 x.loadedAgendas = []; 877 x.loadedComments = []; 878 x.loadedRegistration = []; 879}; 880 881CalendarEmbed.prototype.initPostLoadUIFunctions = function () { 882 var x = this; 883 $.fn.tooltip && x.$container.find("[data-toggle=tooltip]").tooltip(); 884 885 // Prevent aria-hidden="true" from being dynamically added to expand buttons 886 x.initExpandButtonAccessibilityWatcher(); 887}; 888 889CalendarEmbed.prototype.initExpandButtonAccessibilityWatcher = function () { 890 var x = this; 891 892 // Find all expand buttons using a comprehensive selector that covers all cases 893 var expandButtonSelector = 'button[id^="angle-down-btn"]'; 894 var expandButtons = x.$eventList.find(expandButtonSelector).get(); 895 896 expandButtons.forEach(function (expandBtn, index) { 897 // Skip if already being watched 898 if (expandBtn._ariaWatcher) { 899 return; 900 } 901 902 // Create MutationObserver to prevent aria-hidden from being added 903 var ariaObserver = new MutationObserver(function (mutations) { 904 mutations.forEach(function (mutation) { 905 var target = mutation.target; 906 if (target.getAttribute("aria-hidden") === "true") { 907 target.removeAttribute("aria-hidden"); 908 } 909 }); 910 }); 911 912 ariaObserver.observe(expandBtn, { 913 attributes: true, 914 attributeFilter: ["aria-hidden"] 915 }); 916 917 // Mark as watched to prevent duplicate observers 918 expandBtn._ariaWatcher = ariaObserver; 919 }); 920}; 921 922// Meetings that collect comments per agenda item render the React PublicComment module -- 923// the same UI the meeting detail page shows -- instead of the legacy comment section. The 924// server marks those events with a .public-comment-root carrying the meeting's config, so 925// the branch is a DOM lookup rather than anything this file has to infer. 926CalendarEmbed.prototype.loadPublicCommentApp = function ($root) { 927 var x = this; 928 929 if ($root.data("public-comment-initialized")) { 930 return; 931 } 932 $root.data("public-comment-initialized", true); 933 934 var $mount = $root.closest(".public-comment-mount"); 935 936 var config; 937 try { 938 config = JSON.parse($mount.find("script.public-comment-config").first().text()); 939 } catch (err) { 940 $root.data("public-comment-initialized", false); 941 console.error("Public Comment: could not parse config for event " + x.eventId, err); 942 x.showCommentLoadError($root); 943 return; 944 } 945 if (!config) { 946 $root.data("public-comment-initialized", false); 947 console.error("Public Comment: empty config for event " + x.eventId); 948 x.showCommentLoadError($root); 949 return; 950 } 951 952 // Neither the hub layout nor the standalone calendar page defines 953 // window.ajaxAntiForgeryToken (only _ProjectFrontLayout does), so seed it from the single 954 // page-level token the events list renders. Deliberately not scoped to $mount: the token 955 // is rendered once per response, because a per-meeting token would be signed against a
956 // cookie the browser never keeps (see _PartialCalendarEventList.cshtml). 957 if (!window.ajaxAntiForgeryToken) { 958 var token = $('.calendar-antiforgery input[name="__RequestVerificationToken"]') 959 .first() 960 .val(); 961 if (token) { 962 window.ajaxAntiForgeryToken = token; 963 } 964 } 965 966 x.announceCommentStatus($root, "Loading public comment form"); 967 968 var mount = function () { 969 if (typeof window.renderPublicComment !== "function") { 970 console.error("Public Comment: bundle loaded but renderPublicComment is missing"); 971 x.showCommentLoadError($root); 972 return; 973 } 974 // renderPublicComment swallows its own exceptions, so branch on its return value 975 // rather than assuming success -- announcing "loaded" over an unreplaced spinner is 976 // worse for a screen-reader user than announcing nothing. 977 if (window.renderPublicComment($root[0], config) === false) { 978 console.error("Public Comment: render failed for event " + x.eventId); 979 x.showCommentLoadError($root); 980 return; 981 } 982 x.announceCommentStatus($root, "Public comment form loaded"); 983 }; 984 985 if (typeof window.renderPublicComment === "function") { 986 mount(); 987 return; 988 } 989 990 // Page-global, not per-instance: what this guards (window.renderPublicComment and the 991 // bundle's own React root registry) is global to the bundle, and a page can carry more 992 // than one CalendarEmbed. Two instances each holding their own promise would execute the 993 // bundle twice and hand the same container two React roots. 994 // $.ajax with cache:true rather than $.getScript, which forces cache:false for scripts and 995 // appends a _=<timestamp> that defeats the ?v=<ticks> cache-buster BundleSrc exists to set. 996 if (!window.__publicCommentBundlePromise) { 997 window.__publicCommentBundlePromise = $.ajax({ 998 url: $root.attr("data-public-comment-bundle"), 999 dataType: "script", 1000 cache: true 1001 }); 1002 } 1003 window.__publicCommentBundlePromise.done(mount).fail(function (err) { 1004 window.__publicCommentBundlePromise = null; 1005 $root.data("public-comment-initialized", false); 1006 console.error("Public Comment: failed to load bundle", err); 1007 x.showCommentLoadError($root); 1008 }); 1009}; 1010 1011// Writes into the live region rendered next to the mount point. Empty first so the region 1012// registers a change even when the same message repeats. 1013CalendarEmbed.prototype.announceCommentStatus = function ($root, message) { 1014 var $status = $root.closest(".public-comment-mount").find(".public-comment-status"); 1015 if (!$status.length) { 1016 return; 1017 } 1018 $status.text(""); 1019 window.setTimeout(function () { 1020 $status.text(message); 1021 }, 100); 1022}; 1023 1024CalendarEmbed.prototype.showCommentLoadError = function ($root) { 1025 // Insert the alert empty, then fill it: VoiceOver in particular often stays silent for a 1026 // role="alert" node that arrives already populated. 1027 var $alert = $("<p>").addClass("comments-disabled-note pc-load-error").attr("role", "alert"); 1028 $root.empty().append($alert); 1029 window.setTimeout(function () { 1030 $alert.text("Comments could not be loaded. Please refresh the page and try again."); 1031 }, 50); 1032}; 1033 1034CalendarEmbed.prototype.loadCommentSection = function () { 1035 var x = this; 1036 1037 var $publicCommentRoot = x.$eventList.find( 1038 '.public-comment-root[data-offline-event-id="' + x.eventId + '"]' 1039 ); 1040 if ($publicCommentRoot.length) { 1041 x.loadPublicCommentApp($publicCommentRoot); 1042 return; 1043 } 1044 1045 var $container = x.$eventList.find(".comment-container-outer-" + x.eventId); 1046 if (!$container.length) { 1047 return; 1048 } 1049 // Per-meeting guard on the container itself. The old guards marked the whole 1050 // list initialized once the FIRST meeting's comments loaded (a global 1051 // .comment-display-outer data flag), so every other meeting's Comments tab 1052 // stayed permanently empty. The container flag is per meeting and also 1053 // dedupes the nested upcoming/past CalendarEmbed instances, which both 1054 // receive the same bubbled click. 1055 if ($container.data("comments-initialized")) { 1056 return; 1057 } 1058 $container.data("comments-initialized", true); 1059 1060 var commentsConfig = { 1061 $container: $container, 1062 dataUrl: "/Comments/GetOfflineEventCommentSectionJson", 1063 updatedCommentsUrl: "/Comments/GetUpdatedEventComments", 1064 cz: $.extend(true, {}, x.UI.config) 1065 }; 1066 commentsConfig.cz.assetId = x.eventId; 1067 commentsConfig.cz.key = "event"; 1068 commentsConfig.modelData = x.UI.modelData;
1069 commentsConfig.offlineEventId = x.eventId; 1070 commentsConfig.cz.custId = x.customerId; 1071 commentsConfig.cz.projectId = x.projectId; 1072 ScriptLoader.loadScriptArray( 1073 ScriptLoader.scriptArrays.commentSection, 1074 function () { 1075 x.commentSection = new CommentSection(commentsConfig); 1076 x.commentSection.Init(function () { 1077 var $commentDisplay = $container.find(".comment-display-outer"); 1078 1079 $commentDisplay.addClass("comment-scrollbox"); 1080 x.resizeScrollingCommentBox(); 1081 $(window).on("resize", function () { 1082 x.resizeScrollingCommentBox(); 1083 }); 1084 1085 // Friendly empty state: when the section initializes without any 1086 // comment UI (comments disabled for this meeting), say so instead of 1087 // leaving a blank pane. 1088 if ( 1089 !$container.find( 1090 ".comment-entry-outer, .comment-display-outer, .comment-section-outer" 1091 ).length 1092 ) { 1093 $container 1094 .empty() 1095 .append( 1096 $("<p>") 1097 .addClass("comments-disabled-note") 1098 .text("Comments are not available for this meeting.") 1099 ); 1100 } 1101 }); 1102 } 1103 ); 1104 x.loadedComments.push(x.eventId); 1105}; 1106 1107CalendarEmbed.prototype.resizeScrollingCommentBox = function () { 1108 var x = this; 1109 var $commentSection = x.commentSection.ui.$container; 1110 var $livestream = x.$eventList.find(".liveStream"); 1111 if ($livestream != null && $livestream.length && x.commentSection != null) { 1112 var commentSectionHeight = Math.max($livestream.outerHeight(), 400); 1113 var displayScrollHeight = commentSectionHeight; 1114 if ($commentSection != null && $commentSection.length) { 1115 var $entry = $commentSection.find(".comment-entry-outer"); 1116 if ($entry != null && $entry.length) { 1117 displayScrollHeight = displayScrollHeight - $entry.outerHeight(); 1118 } 1119 } 1120 displayScrollHeight = Math.max(300, displayScrollHeight); 1121 $commentSection 1122 ?.find(".comment-display-outer") 1123 .css("max-height", displayScrollHeight); 1124 } 1125}; 1126 1127CalendarEmbed.prototype.initLayout = function (data) { 1128 var $calendarEventList = $(data).find(".calendar-events-list-outer"); 1129 $calendarEventList.removeClass("col-md-8 col-md-pull-4"); 1130 $calendarEventList.addClass("col-md-12"); 1131 1132 var $calendarEventGrid = $(data).find(".calendar-events-grid-outer"); 1133 $calendarEventGrid.hide(); 1134 $calendarEventList.append($calendarEventGrid); 1135 1136 return $calendarEventList; 1137}; 1138 1139CalendarEmbed.prototype._initOfflineEventUI = function (eventId) { 1140 var x = this; 1141 if (window.offlineEventUI && window.offlineEventUI.id == eventId) { 1142 return; 1143 } 1144 1145 $.ajax({ 1146 url: "/Events/GetOfflineEventById", 1147 type: "POST", 1148 data: { id: eventId }, 1149 success: function (data) { 1150 var offlineEvent = data; 1151 ScriptLoader.loadScriptArray( 1152 ScriptLoader.scriptArrays.offlineEvent, 1153 function () { 1154 var config = { 1155 $element: x.$eventRegistrationModal.find(".modal-body"), 1156 pageUI: x.UI, 1157 offlineEvent: offlineEvent, 1158 id: eventId, 1159 isModal: true, 1160 $rsvpModal: x.$eventRegistrationModal, 1161 $rsvpForm: x.$eventRegistrationModal.find(".rsvp-form") 1162 }; 1163 window.offlineEventUI = new OfflineEventUI(config); 1164 } 1165 ); 1166 } 1167 }); 1168}; 1169 1170CalendarEmbed.prototype.loadRegistrationTab = function (eventId, $container) { 1171 var x = this; 1172 if ( 1173 $container.data("registration-loaded") || 1174 $container.data("registration-loading") 1175 ) { 1176 return; 1177 } 1178 $container.data("registration-loading", true); 1179 $container.html(TOOLS.spinBlock("Loading registration")); 1180 $.ajax({ 1181 url: "/Events/LoadEventRegistrationModal", 1182 type: "POST", 1183 data: { id: eventId }, 1184 success: function (data) { 1185 $container.html(data); 1186 $container.data("registration-loading", false); 1187 $container.data("registration-loaded", true); 1188 x._initRegistrationTabUI(eventId, $container); 1189 } 1190 }); 1191}; 1192 1193CalendarEmbed.prototype._initRegistrationTabUI = function ( 1194 eventId, 1195 $container 1196) { 1197 var x = this; 1198 $.ajax({
1199 url: "/Events/GetOfflineEventById", 1200 type: "POST", 1201 data: { id: eventId }, 1202 success: function (data) { 1203 ScriptLoader.loadScriptArray( 1204 ScriptLoader.scriptArrays.offlineEvent, 1205 function () { 1206 var config = { 1207 $element: $container, 1208 pageUI: x.UI, 1209 offlineEvent: data, 1210 id: eventId, 1211 isModal: false, 1212 $rsvpModal: null, 1213 $rsvpForm: $container.find(".rsvp-form") 1214 }; 1215 window.registrationTabUIs = window.registrationTabUIs || {}; 1216 window.registrationTabUIs[eventId] = new OfflineEventUI(config); 1217 } 1218 ); 1219 } 1220 }); 1221};
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.