PageSourceSearch

https://austinlightrail.org/Scripts/CalendarEmbed.js

js austinlightrail.org collected 2026-10-09 01:33:57 UTC 41,802 bytes, 1,221 lines download raw bytes

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">&times;</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.