PageSourceSearch

https://boydwi.gov/theme.js?v=55f511d6

js boydwi.gov collected 2026-10-09 17:07:56 UTC 63,200 bytes, 1,745 lines download raw bytes

1/**
2 * Events Calendar - Homepage section -----------------------------------------------------------------
3 * Functionality for events calendar on homepage in section - events-calendar
4 */
5let timezone = "America/Chicago"; // Timezone From Settings
6if (typeof jTimezone !== "undefined") {
7  timezone = jTimezone;
8}
9let today = new Date();
10let todayClone = new Date(today);
11let currentMonth = today.getMonth();
12let currentYear = today.getFullYear();
13let firstDayOfCalendar = setFirstDayOfCalendar(todayClone, currentMonth, currentYear);
14let lastDayOfCalendar = setLastDayOfCalendar(todayClone, currentMonth, currentYear);
15let firstDayOfMonth = new Date(todayClone.setFullYear(currentYear, currentMonth, 1));
16let lastDayOfMonth = new Date(todayClone.setFullYear(currentYear, currentMonth + 1, 0));
17let todayDay = today.getDate();
18let todayMonth = today.getMonth();
19let todayYear = today.getFullYear();
20let events = [];
21// let eventCategories = [];
22let eventCategory = null;
23let monthAndYear = "";
24
25let eventsList, eventsListContainer, eventsListDate, calendarBodyTable;
26
27eventsList = waitForElement("#events-calendar-list");
28eventsListContainer = waitForElement("#eventsCalendarListContainerContent");
29eventsListDate = waitForElement("#events-calendar-list-date");
30calendarBodyTable = waitForElement("#calendar-body");
31
32var eventsListEmpty = "No content.";
33var calendar = document.getElementById("events-calendar-table");
34
35var months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "
35November", "December"];
36var dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
37
38var dayNamesContent = "<tr>";
39dayNames.forEach((name) => {
40  dayNamesContent += `<th class="text-center text-capitalize" data-days="${name}">${name}</th>`;
41});
42dayNamesContent += "</tr>";
43if ((jActiveHomepageSections && jActiveHomepageSections.includes("Events-calendar") && window.location.pathname == "/") || window.location.pathname.includes("customizer")) {
44  let headMonth = document.getElementById("thead-month");
45  if (headMonth !== null) {
46    headMonth.innerHTML = dayNamesContent;
47    monthAndYear = document.getElementById("monthAndYearTitle");
48  }
49}
50
51// Handle next month
52async function eventCalendarNext() {
53  currentYear = currentMonth === 11 ? currentYear + 1 : currentYear;
54  currentMonth = (currentMonth + 1) % 12;
55  let cloneDate = new Date(today);
56  firstDayOfCalendar = setFirstDayOfCalendar(cloneDate, currentMonth, currentYear);
57  lastDayOfCalendar = setLastDayOfCalendar(cloneDate, currentMonth, currentYear);
58  firstDayOfMonth = new Date(cloneDate.setFullYear(currentYear, currentMonth, 1));
59  lastDayOfMonth = new Date(cloneDate.setFullYear(currentYear, currentMonth + 1, 0));
60  events = await getEvents(eventCategory);
61  showCalendar(currentMonth, currentYear);
62}
63
64// Handle previous month
65async function eventCalendarPrev() {
66  currentYear = currentMonth === 0 ? currentYear - 1 : currentYear;
67  currentMonth = currentMonth === 0 ? 11 : currentMonth - 1;
68  let cloneDate = new Date(today);
69  firstDayOfCalendar = setFirstDayOfCalendar(cloneDate, currentMonth, currentYear);
70  lastDayOfCalendar = setLastDayOfCalendar(cloneDate, currentMonth, currentYear);
71  firstDayOfMonth = new Date(cloneDate.setFullYear(currentYear, currentMonth, 1));
72  lastDayOfMonth = new Date(cloneDate.setFullYear(currentYear, currentMonth + 1, 0));
73  events = await getEvents(eventCategory);
74  showCalendar(currentMonth, currentYear);
75}
76
77// Show Calendar
78async function showCalendar(month, year) {
79  // console.log(events)
80  var firstDay = new Date(year, month).getDay();
81  let tbl = document.getElementById("calendar-body");
82  // console.log(tbl)
83  tbl.innerHTML = "";
84  monthAndYear.innerHTML = months[month] + " " + year;
85  let calendarRows = getCalendarRows();
86  // console.log(calendarRows)
87  // Create all calendar items
88
89  calendarRows.forEach((calendarRow) => {
90    var row = document.createElement("tr");
91    calendarRow.forEach((day) => {
92      let cell = document.createElement("td");
93      cell.setAttribute("role", "gridcell");
94      cell.setAttribute("tabindex", "0");
95      cell.setAttribute("aria-label", `${months[day.getMonth()]} ${day.getDate()}, ${day.getFullYear()}`);
96
97      if (isDateOfPreviousMonth(day)) {
98        cell.classList.add("previous-month");
99        cell.setAttribute("aria-disabled", "true");
100        cell.setAttribute("tabindex", "-1");
101      }
102
103      if (isDateOfNextMonth(day)) {
104        cell.classList.add("next-month");
105        cell.setAttribute("aria-disabled", "true");
106        cell.setAttribute("tabindex", "-1");
107      }
108
109      // check if calendar date has events
110      let dayEvents = getEventOnDay(day);
111
112      let div = document.createElement("div");
113      div.className = "text-center";
114
115      let todayCloneForCalendarCell = new Date(today);
116      todayCloneForCalendarCell.setHours(0, 0, 0, 0);
117      if (todayCloneForCalendarCell.getTime() === day.getTime()) {
118        cell.classList.add("tw-calendar-day-active");
119        cell.setAttribute("aria-current", "date");
120      }
121
122      if (dayEvents.length) {
123        cell.classList.add("event-item-btn");
124        cell.setAttribute("aria-selected", "false");
125        div.classList.add("cursor-pointer");
126        div.innerHTML = `<div class="text-center">${day.getDate()}</div>`;
127
128        let moreEvents = 0;
129
130        dayEvents.forEach((event, index) => {
131          if (index > 1) {
132            moreEvents += 1;
133          } else {
134            let eventStartDate = new Date(
135              new Date(event.starts_at).toLocaleString("en", {
136                timeZone: timezone,
137              }),
138            );
139
140            let timeString = eventStartDate
141              .toLocaleString("en-US", {
142                hour: "numeric",
143                minute: "numeric",
144                hour12: true,
145              })
146              .replace(" AM", "am")
147              .replace(" PM", "pm");
148            div.innerHTML += `<div class="tw-calendar-day-event-title ps-1">${timeString} ${event.name}</div>`;
149          }
150        });
151
152        if (moreEvents) {
153          div.innerHTML += `<div class="text-start ps-1">+${moreEvents} more</div>`;
154        }
155        cell.setAttribute("aria-label", `${months[day.getMonth()]} ${day.getDate()}, ${day.getFullYear()}, ${dayEvents.length} event${dayEvents.length === 1 ? "" : "s"}`);
156      } else {
157        div.innerText = day.getDate();
158        cell.setAttribute("aria-label", `${months[day.getMonth()]} ${day.getDate()}, ${day.getFullYear()}, no events`);
159      }
160
161      cell.appendChild(div);
162
163      cell.setAttribute("data-date", day);
164      cell.setAttribute("data-month", day.getMonth() + 1);
165      cell.setAttribute("data-year", day.getFullYear());
166      cell.setAttribute("data-month_name", months[day.getMonth()]);
167
168      cell.addEventListener("keydown", (event) => {
169        if (event.key === "Enter" || event.key === " ") {
170          event.preventDefault();
171          cell.click();
172        }
173      });
174
175      row.appendChild(cell);
176    });
177    if (tbl) {
178      tbl.appendChild(row);
179    }
180  });
181}
182
183function getCalendarRows() {
184  let calendarDays = getDaysBetweenDates(firstDayOfCalendar, lastDayOfCalendar);
185
186  let rows = [];
187
188  while (calendarDays.length > 0) {
189    rows.push(calendarDays.splice(0, 7));
190  }
191
192  return rows;
193}
194
195function setFirstDayOfCalendar(date, month, year) {
196  let cloneDate = new Date(date);
197  let firstDayOfMonth = new Date(cloneDate.setFullYear(year, month, 1));
198  return getFirstDayOfWeek(firstDayOfMonth, 0);
199}
200
201function setLastDayOfCalendar(date, month, year) {
202  let cloneDate = new Date(date);
203  let lastDayOfMonth = new Date(cloneDate.setFullYear(year, month + 1, 0));
204  return getLastDayOfWeek(lastDayOfMonth, 6);
205}
206
207function getFirstDayOfWeek(dateObject, firstDayOfWeekIndex) {
208  const dayOfWeek = dateObject.getDay(),
209    firstDayOfWeek = new Date(dateObject),
210    diff = dayOfWeek >= firstDayOfWeekIndex ? dayOfWeek - firstDayOfWeekIndex : 6 - dayOfWeek;
211
212  firstDayOfWeek.setDate(dateObject.getDate() - diff);
213  firstDayOfWeek.setHours(0, 0, 0, 0);
214
215  return new Date(firstDayOfWeek);
216}
217
218function getLastDayOfWeek(dateObject, lastDayOfWeekIndex) {
219  const dayOfWeek = dateObject.getDay(),
220    lastDayOfWeek = new Date(dateObject),
221    diff = dayOfWeek >= lastDayOfWeekIndex ? dayOfWeek - lastDayOfWeekIndex : 6 - dayOfWeek;
222
223  lastDayOfWeek.setDate(dateObject.getDate() + diff);
224  lastDayOfWeek.setHours(0, 0, 0, 0);
225
226  return new Date(lastDayOfWeek);
227}
228
229function getDaysBetweenDates(startDate, endDate) {
230  let dates = [];
231
232  while (startDate.getTime() <= endDate.getTime()) {
233    let toPush = new Date(startDate);
234    dates.push(toPush);
235    let cloneStartDate = new Date(startDate);
236    startDate = new Date(cloneStartDate.setDate(startDate.getDate() + 1));
237  }
238
239  return dates;
240}
241
242// check if date is of calendar current motnh
243function isDateOfCurrentMonth(date) {
244  return date.getTime() >= firstDayOfMonth.getTime() && date.getTime <= lastDayOfMonth.getTime();
245}
246
247// check if date is from calendar previous month
248function isDateOfPreviousMonth(date) {
249  let firstDayOfMonthClone = new Date(firstDayOfMonth);
250  firstDayOfMonthClone.setHours(0, 0, 0, 0);
251  return date.getTime() < firstDayOfMonthClone.getTime();
252}
253
254// check if date is from calendar next month
255function isDateOfNextMonth(date) {
256  let lastDayOfMonthClone = new Date(lastDayOfMonth);
257  lastDayOfMonthClone.setHours(0, 0, 0, 0);
258  return date.getTime() > lastDayOfMonthClone.getTime();
259}
260
261// Get Events for selected period - Getting Events from HeyGov
262async function getEvents(categoryId = null, args = null) {
263  let startDate = formatDate(new Date(firstDayOfCalendar)) + "T00:00:00.000Z";
264  let endDate = formatDate(new Date(lastDayOfCalendar)) + "T23:59:59.000Z";
265
266  let categoryFilter = "";
267  if (categoryId && categoryId !== "0") {
268    categoryFilter += "&category=" + categoryId;
269  }
270
271  let requestUrl = `https://api.heygov.com/${jHeyGovJurisdiction}/events?`;
272  let response;
273
274  if (args) {
275    let urlSearchParamsArr = [];
276
277    if (args.date_start && args.date_end) {
278      urlSearchParamsArr.push(`date_start=${args.date_start}&date_end=${args.date_end}`);
279    }
280
281    if (args.limit) {
282      urlSearchParamsArr.push(`limit=${args.limit}`);
283    }
284
285    let urlSearchParams = urlSearchParamsArr.join("&");
286
287    requestUrl += urlSearchParams;
288
289    response = await fetch(requestUrl + categoryFilter);
290  } else {
291    response = await fetch(requestUrl + "date_start=" + startDate + "&date_end=" + endDate + categoryFilter);
292  }
293
294  const events = await response.json();
295  return events;
296}
297
298// Get Event Categories for jurisdiction - Getting Them from HeyGov
299async function getEventCategories() {
300  const response = await fetch("https://api.heygov.com/" + jHeyGovJurisdiction + "/events/categories");
301  // const eventCategories = await response.json()
302  // return eventCategories;
303}
304
305// Get Events on selected day of month
306function getEventOnDay(day) {
307  let dayEvents = [];
308  let cloneDay = new Date(day);
309  cloneDay.setHours(0, 0, 0, 0);
310  events.forEach((event) => {
311    let eventStartDate = new Date(
312      new Date(event.starts_at).toLocaleString("en", {
313        timeZone: timezone,
314      }),
315    );
316
317    eventStartDate.setHours(0, 0, 0, 0);
318    if (eventStartDate.getTime() === cloneDay.getTime()) {
319      dayEvents.push(event);
320    }
321  });
322  return dayEvents;
323}
324
325// Set date for events list (list of day events)
326function setEventsListDate(date) {
327  let value = "";
328  value = formatProperDate(date);
329  eventsListDate.innerHTML = value;
330}
331
332// Generate list of day events
333async function generateDayEventsList(day, month, year) {
334  let dayClone = new Date(day);
335
336  let dayEvents = getEventOnDay(dayClone);
337
338  let eventsList = "";
339
340  if (dayEvents.length > 0) {
341    let eDate = new Date(
342      new Date(dayEvents[0].starts_at).toLocaleString("en", {
343        timeZone: timezone,
344      }),
345    );
346    setEventsListDate(eDate);
347  } else {
348    let eDate = new Date(new Date(year, month, dayClone.getDate()).toLocaleString("en"));
349    /* let eDate = new Date(
350        new Date(year, month, day).toLocaleString("en", {
351            timeZone: "Etc/UTC"
352        })
353        ) */
354    setEventsListDate(eDate);
355  }
356
357  let moreEvents = 0;
358
359  dayEvents.forEach((e, index) => {
360    if (index > 1) {
361      moreEvents += 1;
362    } else {
363      let startsAt = new Date(
364        new Date(e.starts_at).toLocaleString("en", {
365          timeZone: timezone,
366        }),
367      );
368      let endsAt = new Date(
369        new Date(e.starts_at).toLocaleString("en", {
370          timeZone: timezone,
371        }),
372      );
373      let startTime = formatAMPM(startsAt);
374      // let endTime = formatAMPM(endsAt)
375      if (e.metadata.hide_end_time) {
376        endTime = "";
377      }
378
379      eventsList += `<a class="d-inline-flex mb-3 w-100" href="/${jEventsPage}/?heygov-event=${e.pid}">
380                <article class="tw-calendar-event-article bg-white w-100">
381                    <div class="row">
382                        <div class="col-3 col-md-2">
383                            <div class="tw-calendar-event-article-time"><span>${startTime}</span></div>
384                        </div>
385                        <div class="col">
386                            <div class="tw-calendar-event-article-title mb-3">
387                                ${e.name}
388                            </div>
389                            <div class="tw-calendar-event-article-venue">${e.venues.length ? e.venues[0] : ""}</div>
390                        </div>
391                    </div>
392                </article>
393            </a>`;
394    }
395  });
396
397  if (moreEvents) {
398    eventsList += `<div>+${moreEvents} more event${moreEvents === 1 ? "" : "s"}</div>`;
399  }
400
401  if (eventsList == "") {
402    eventsList = eventsListEmpty;
403  }
404  eventsListContainer.then((el) => (el.innerHTML = eventsList));
405}
406
407// Format Time to AM/PM
408const formatAMPM = (date) => {
409  let hours = date.getHours();
410  let minutes = date.getMinutes();
411  const ampm = hours >= 12 ? "pm" : "am";
412
413  hours %= 12;
414  hours = hours || 12;
415  minutes = minutes < 10 ? `0${minutes}` : minutes;
416
417  const strTime = `${hours}:${minutes}${ampm}`;
418
419  return strTime;
420};
421
422// Format Date to Sunday, January 14, 2023
423const formatProperDate = (date) => {
424  let dateFormated = "";
425  let weekday = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][date.getDay()];
426  let monthName = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "
426November", "December"][date.getMonth()];
427  let dateNumber = date.getDate();
428  let yearNumber = date.getFullYear();
429  dateFormated = weekday + ", " + monthName + " " + dateNumber + ", " + yearNumber;
430  return dateFormated;
431};
432
433// Format date
434function formatDate(date = new Date()) {
435  const year = date.toLocaleString("default", { year: "numeric" });
436  const month = date.toLocaleString("default", {
437    month: "2-digit",
438  });
439  const day = date.toLocaleString("default", { day: "2-digit" });
440
441  return [year, month, day].join("-");
442}
443
444// Calculate days in month
445function daysInMonth(iMonth, iYear) {
446  return 32 - new Date(iYear, iMonth, 32).getDate();
447}
448
449/**
450 * END Events Calendar - Homepage section ---------------------------------------------------------------------------
451 */
452
453/**
454 * Meetings ---------------------------------------------------------------------------------------------------------
455 * Function is created with vanila JavaScript
456 */
457var meetingItemsPerTable = 10;
458var searchCategory = "";
459var dateFrom = "";
460var dateTo = "";
461var categories = [];
462var pager = 1;
463var pagerCategory = 0;
464
465// Fetch categories from JSON
466function fetchCategories() {
467  fetch(`/json/meetings/categories.json?sk=${Date.now()}`)
468    .then((response) => response.json())
469    .then((data) => {
470      categories = data;
471      console.log("Categories Loaded ✅");
472      if (window.location.pathname.includes(jmeetingPage)) {
473        generateMeetingTables();
474        generateCategoriesSelect();
475      }
476    })
477    .catch((error) => {
478      console.error("Error loading categories ❌", error);
479    });
480}
481
482// Search Meetings by Date or Category
483function searchMeetings() {
484  let categoryId = document.getElementById("categoriesSelect").value;
485  searchCategory = categoryId;
486  dateFrom = document.getElementById("dateFrom").value;
487  dateTo = document.getElementById("dateTo").value;
488  generateMeetingTables();
489}
490
491// Generate Categories Select Input for search
492function generateCategoriesSelect() {
493  var categoriesSelect = document.getElementById("categoriesSelect");
494  var categoriesSelectOptions = `<option value="" aria-label="All Categories">All Categories</option>`;
495  categories.forEach((category) => {
496    categoriesSelectOptions += `<option value="${category.id}" aria-label="${category.name}">${category.name}</option>`;
497  });
498  categoriesSelect.innerHTML = categoriesSelectOptions;
499}
500
501// Load more items in table per category
502function loadMore(categoryId) {
503  if (categoryId == pagerCategory) {
504    pager = pager + 1;
505  } else {
506    pagerCategory = categoryId;
507    pager = 2;
508  }
509  var loader = document.getElementById("load-" + categoryId);
510  loader.classList.add("fa-spin");
511  let category = categories.find((element) => element.id == categoryId);
512  category.perPage = meetingItemsPerTable * pager;
513  populateCategoryTable(category);
514}
515
516// Generate tables for categories with no data
517function generateCategoryEmptyTable(category) {
518  // Get a reference to the table element
519  var content = document.getElementById("juniperMeetings");
520  let html = `<div id="Category-${category.id}">
521                    <h3 class="twd-category-name"><span>${category.name}</span></h3>
522                    <table class="table table-striped twd-table tw-meeting-repo-table">
523                    <thead>
524                        <tr>`;
525  if (jMeetingActiveColumns.includes("meeting_date")) {
526    html += `<th class="meeting-date-column">Meeting Date</th>`;
527  }
528  if (jMeetingActiveColumns.includes("agenda")) {
529    html += `<th class="agenda-column">Agenda</th>`;
530  }
531  if (jMeetingActiveColumns.includes("agenda_packet")) {
532    html += `<th class="agenda-packet-column">Agenda Packet</th>`;
533  }
534  if (jMeetingActiveColumns.includes("minutes")) {
535    html += `<th class="meeting-minutes-column">Meeting Minutes</th>`;
536  }
537  if (jMeetingActiveColumns.includes("audio")) {
538    html += `<th class="audio-column">Audio</th>`;
539  }
540  if (jMeetingActiveColumns.includes("video")) {
541    html += `<th class="video-column">Video</th>`;
542  }
543  if (jMeetingActiveColumns.includes("additional_documents")) {
544    html += `<th class="additional-documents-column">Additional Documents</th>`;
545  }
546  if (jMeetingActiveColumns.includes("notes")) {
547    html += `<th class="notes-column">Notes</th>`;
548  }
549  html += `</tr>
550                    </thead>
551                    <tbody id="table-category-${category.id}">
552
553                    </tbody>
554                    <tfoot id="table-category-footer-${category.id}"></tfoot>
555                    </table>
556                </div>`;
557
558  content.innerHTML = content.innerHTML + html;
559}
560
561// Check is meeting date in range between search dates
562function inRange(meetingDate) {
563  let show = false;
564  if (dateFrom !== "") {
565    dateF = Date.parse(dateFrom);
566  } else {
567    dateF = "";
568  }
569  if (dateTo !== "") {
570    dateT = Date.parse(dateTo);
571  } else {
572    dateT = "";
573  }
574  if (dateF !== "" && dateT !== "") {
575    if (dateF <= meetingDate && dateT >= meetingDate) {
576      show = true;
577    }
578  }
579  if (dateF !== "" && dateT == "") {
580    if (dateF <= meetingDate) {
581      show = true;
582    }
583  }
584  if (dateF == "" && dateT !== "") {
585    if (dateT >= meetingDate) {
586      show = true;
587    }
588  }
589  if (dateF == "" && dateT == "") {
590    show = true;
591  }
592  return show;
593}
594
595// Filtering meetings by date
596function filterDateData(data) {
597  let filteredData = [];
598  data.forEach((item) => {
599    let checkDate = Date.parse(item.meeting_date);
600    if (inRange(checkDate)) {
601      filteredData.push(item);
602    }
603  });
604  return filteredData;
605}
606
607// Generate data for category table
608function populateCategoryTable(category) {
609  fetch(`/json/meetings/category-${category.id}.json?sk=${Date.now()}`)
610    .then((response) => response.json())
611    .then((data) => {
612      let html = "";
613      data = filterDateData(data);
614      // Generate table rows for the municipalities
615      for (var i = 0; i < category.perPage; i++) {
616        if (i >= data.length) {
617          break;
618        }
619        html += `<tr>`;
620
621        if (jMeetingActiveColumns.includes("meeting_date")) {
622          html += `<td class="meeting-date-column">${new Date(data[i].meeting_date).toLocaleDateString("default", {
623            dateStyle: "medium",
624          })}</td>`;
625        }
626
627        if (jMeetingActiveColumns.includes("agenda")) {
628          if (data[i].agenda_file_path) {
629            html += `<td class="agenda-column">
630                                <a href="${data[i].agenda_file_path}" target="_blank" aria-label="Download PDF Agenda for ${data[i].title} ${data[i].meeting_date}"><i class="fa-solid fa-file-pdf"></i> Agenda</a>
631                            </td>`;
632          } else {
633            html += `<td class="agenda-column"></td>`;
634          }
635        }
636
637        if (jMeetingActiveColumns.includes("agenda_packet")) {
638          if (data[i].agenda_pack_file_path) {
639            html += `<td class="agenda-packet-column">
640                                <a href="${data[i].agenda_pack_file_path}" target="_blank" aria-label="Download PDF Agenda Pack for ${data[i].title} ${data[i].meeting_date}"><i class="fa-solid fa-file-pdf"></i> Packet</a>
641                            </td>`;
642          } else {
643            html += `<td class="agenda-packet-column"></td>`;
644          }
645        }
646
647        if (jMeetingActiveColumns.includes("minutes")) {
648          if (data[i].meeting_minutes_file_path) {
649            html += `<td class="meeting-minutes-column">
650                                <a href="${data[i].meeting_minutes_file_path}" target="_blank" aria-label="Download PDF Minutes for ${data[i].title} ${data[i].meeting_date}"><i class="fa-solid fa-file-pdf"></i> Minutes</a>
651                            </td>`;
652          } else {
653            html += `<td class="meeting-minutes-column"></td>`;
654          }
655        }
656
657        if (jMeetingActiveColumns.includes("audio")) {
658          if (data[i].audio_public_url && data[i].audio_public_url !== "") {
659            html += `<td class="audio-column">
660                                <a href="${data[i].audio_public_url}" target="_blank" aria-label="Download Audio for ${data[i].title} ${data[i].meeting_date}"><i class="fa-solid fa-file-pdf"></i> Audio</a>
661                            </td>`;
662          } else {
663            html += `<td class="audio-column"></td>`;
664          }
665        }
666
667        if (jMeetingActiveColumns.includes("video")) {
668          if (data[i].video_public_url && data[i].video_public_url !== "") {
669            html += `<td class="video-column">
670                                <a href="${data[i].video_public_url}" target="_blank" aria-label="Download Video for ${data[i].title} ${data[i].meeting_date}"><i class="fa-solid fa-file-video"></i> Video</a>
671                            </td>`;
672          } else {
673            html += `<td class="video-column"></td>`;
674          }
675        }
676
677        if (jMeetingActiveColumns.includes("additional_documents")) {
678          if (data[i].additional_file_path && data[i].additional_file_path !== "") {
679            html += `<td class="additional-documents-column">
680                          <a href="${data[i].additional_file_path}" target="_blank" aria-label="Download Video for ${data[i].title} ${data[i].meeting_date}"><i class="fa-solid fa-file-pdf"></i> ${data[i].additional_doc}</a>
681                     </td>`;
682          } else if (data[i].additional_url && data[i].additional_url !== "") {
683            html += `<td class="additional-documents-column">
684                          <a href="${data[i].additional_url}" target="_blank" aria-label="Download Video for ${data[i].title} ${data[i].meeting_date}"><i class="fa-solid fa-file-pdf"></i> ${data[i].additional_doc}</a>
685                     </td>`;
686          } else {
687            html += `<td class="additional-documents-column"></td>`;
688          }
689        }
690
691        if (jMeetingActiveColumns.includes("notes")) {
692          if (data[i].notes && data[i].notes !== "") {
693            html += `<td class="notes-column">
694                                ${data[i].notes}
695                            </td>`;
696          } else {
697            html += `<td class="notes-column"></td>`;
698          }
699        }
700
701        html += `</tr>`;
702      }
703
704      if (data.length == 0) {
705        html += `<tr><td class="text-center" colspan="8">No data in this category</td></tr>`;
706      }
707
708      if (category.perPage < data.length) {
709        let tfootElement = document.getElementById(`table-category-footer-${category.id}`);
710        tfoot = `<tr>
711                            <td colspan="8">
712                                <a
713                                    tabindex="0"
714                                    aria-label="View more agendas &amp; minutes"
715                                    role="button"
716                                    onclick="loadMore(${category.id})"
717                                    class="load-more"
718                                >
719                                    <i id="load-${category.id}" class="fa fa-refresh"></i> View more
720                                </a>
721                            </td>
722                        </tr>`;
723        tfootElement.innerHTML = tfoot;
724      } else {
725        let tfootElement = document.getElementById(`table-category-footer-${category.id}`);
726        tfootElement.innerHTML = "";
727      }
728      let categoryContent = document.getElementById("table-category-" + category.id);
729      categoryContent.innerHTML = html;
730    })
731    .catch((error) => {
732      console.error("Error fetching data:", error);
733    });
734}
735
736// Generate meering tables
737function generateMeetingTables() {
738  var content = document.getElementById("juniperMeetings");
739  content.innerHTML = "";
740  // Initiali populate tables
741  categories.forEach((category) => {
742    if (searchCategory == "") {
743      category.perPage = meetingItemsPerTable;
744      generateCategoryEmptyTable(category);
745      populateCategoryTable(category);
746    } else {
747      if (searchCategory == category.id) {
748        category.perPage = meetingItemsPerTable;
749        generateCategoryEmptyTable(category);
750        populateCategoryTable(category);
751      }
752    }
753  });
754}
755
756// END Meetings ----------------------------------------------------------------------------------------------------------------
757
758/**
759 * Handle category selection from parameter
760 */
761
762// Function to get the value of a URL parameter by name
763function getParameterByName(name) {
764  const urlParams = new URLSearchParams(window.location.search);
765  return urlParams.get(name);
766}
767
768function updateURLParameter(param, paramVal) {
769  const url = new URL(window.location.href);
770  url.searchParams.set(param, paramVal);
771  window.history.replaceState(null, null, url);
772}
773
774function handleMeetingCategoryFromUrl() {
775  // Get the 'category' parameter from the URL
776  const categoryText = getParameterByName("category");
777
778  // Find the select box
779  const selectBox = document.getElementById("categoriesSelect");
780
781  // If the category parameter exists, find the option with matching inner text
782  if (categoryText) {
783    setTimeout(() => {
784      const options = selectBox.options;
785      for (let i = 0; i < options.length; i++) {
786        if (options[i].innerText === categoryText) {
787          selectBox.selectedIndex = i;
788          break;
789        }
790      }
791      // Automatically click the button after selecting the option
792      document.getElementById("searchMeetings").click();
793    }, 500);
794  }
795
796  // Listen for changes in the select box
797  const setCat = document.getElementById("searchMeetings");
798  if (setCat) {
799    document.getElementById("searchMeetings").addEventListener("click", function () {
800      const selectedText = selectBox.options[selectBox.selectedIndex].text;
801      // Update the URL with the new category parameter
802      updateURLParameter("category", selectedText);
803    });
804  }
805}
806
807$(document).ready(async function ($) {
808  handleMeetingCategoryFromUrl();
809  // EVENTS CALENDAR HOMEPAGE ---------------------------------------------------
810  //   eventCategories = await getEventCategories();
811  // Load events from HeyGov
812  eventCategory = $("#events-calendar-2019 .tw-data").data("event_categories_filter");
813
814  if (eventCategory) {
815    eventCategory = eventCategory.trim();
816    let lastChar = eventCategory.slice(-1);
817    if (lastChar == ",") {
818      eventCategory = eventCategory.slice(0, -1);
819    }
820  }
821  // Handle events only if pathname is / and events-calendar is active on homepage
822  if ((jActiveHomepageSections && jActiveHomepageSections.includes("Events-calendar") && window.location.pathname == "/") || window.location.pathname.includes("customizer")) {
823    calendarBodyTable.then(async () => {
824      events = await getEvents();
825      showCalendar(currentMonth, currentYear);
826      setEventsListDate(today);
827      generateDayEventsList(today, todayMonth, todayYear);
828    });
829  }
830  // Events calendar item click
831  $("body").on("click", ".event-item-btn", function () {
832    // tw-calendar-day-active
833    $("#events-calendar-table .event-item-btn").removeClass("tw-calendar-day-active").attr("aria-selected", "false");
834    $(this).addClass("tw-calendar-day-active").attr("aria-selected", "true");
835    generateDayEventsList($(this).data("date"), $(this).data("month"), $(this).data("year"));
836  });
837  // Events calendar next month
838  $("body").on("click", ".event-calendar-next", function () {
839    eventCalendarNext();
840  });
841  // Events calendar prev month
842  $("body").on("click", ".event-calendar-prev", function () {
843    eventCalendarPrev();
844  });
845  //   Events calendar filter by category
846  $("body").on("click", "#eventsCalendarListContainer .tw-event-category-tab", async function (e) {
847    let targetIsActive = $(this).hasClass("active");
848
849    $("#eventsCalendarListContainer .tw-event-category-tab").removeClass("active");
850    eventCategory = null;
851
852    if (!targetIsActive) {
853      $(this).addClass("active");
854      eventCategory = $(this).data("tw-event-category-id");
855    }
856    events = await getEvents(eventCategory);
857    showCalendar(currentMonth, currentYear);
858    generateDayEventsList(today, todayMonth, todayYear);
859  });
860
861  // #EVENTS CALENDAR HOMEPAGE ---------------------------------------------------
862
863  // Fetch categories for Meetings
864  if (window.location.pathname == "/" + jmeetingPage || window.location.pathname == "/" + jmeetingPage + "/") {
865    fetchCategories();
866  }
867
868  let $window = $(window),
869    height = $window.height(),
870    width = $window.width(),
871    navheight = $("#defaultHeader #topHeader").height();
872
873  let scrollDirection, oldScrollDirection;
874  let oldScrollTop = 0;
875  let fixedInitClass = "fixed-init";
876
877  let headerHeight = $("#defaultHeader").height();
878  let headerBottomHeight = $("#defaultHeader #bottomHeader").height();
879  if ($("div#menuHero ul").length) {
880    let heroLineMenuTopOffset = $("div#menuHero ul").offset().top;
881    let headerHeroMenuDistance = heroLineMenuTopOffset - headerHeight;
882  }
883  let stickyPoint = false;
884  $window.on("resize orientationchange", setMeasures);
885
886  // Menu snippet
887  let viewWidth = document.documentElement.clientWidth;
888  let elements = document.querySelectorAll(".menu-level-one li > ul");
889  const resizeWatcher = new ResizeObserver((entries) => {
890    for (const entry of entries) {
891      let elementObject = entry.target.getBoundingClientRect();
892
893      if (elementObject.right > viewWidth) {
894        entry.target.style.left = "auto";
895        entry.target.style.right = "100%";
896      }
897    }
898  });
899
900  elements.forEach((element) => {
901    resizeWatcher.observe(element);
902  });
903
904  function setMeasures() {
905    ((height = $window.height()), (width = $window.width()), (navheight = $("#defaultHeader #topHeader").height()));
906
907    headerHeight = $("#defaultHeader").height();
908    headerBottomHeight = $("#defaultHeader #bottomHeader").height();
909
910    if ($("div#menuHero ul").length) {
911      heroLineMenuTopOffset = $("div#menuHero ul").offset().top;
912      headerHeroMenuDistance = heroLineMenuTopOffset - headerHeight;
913    }
914  }
915
916  // Function Menu Hero Sticky
917  //   function sticky() {
918  //     var scrollTop = $window.scrollTop()
919  //     if (scrollTop > oldScrollTop) {
920  //       scrollDirection = "down"
921  //     } else {
922  //       scrollDirection = "up"
923  //     }
924  //     oldScrollTop = scrollTop
925
926  //     if (!$("#defaultHeader #bottomHeader").hasClass(scrollDirection)) {
927  //       $("#defaultHeader #bottomHeader").removeClass(oldScrollDirection).addClass(scrollDirection)
928  //     }
929
930  //     if (scrollTop > navheight) {
931  //       $("#defaultHeader").addClass("fixed-header")
932  //       $("#defaultHeader #bottomHeader").addClass("fixed")
933  //       $("#defaultHeader #bottomHeader").removeClass("fixed-init").addClass(fixedInitClass)
934  //       $("#defaultHeader nav.navbar").addClass("fixed-navbar")
935  //       fixedInitClass = ""
936  //       $('body [data-bs-toggle="tooltip"]').tooltip("enable")
937  //       if ($("div#menuHero ul").length) {
938  //         if (!stickyPoint) {
939  //           $("div#menuHero").css({
940  //             top: "180px"
941  //           })
942  //         }
943  //         stickyPoint = true
944  //         $("div#menuHero").addClass("fixed-side-menu").removeClass("absolute-side-menu")
945  //       }
946  //     } else {
947  //       $("#defaultHeader").removeClass("fixed-header")
948  //       $("#defaultHeader #bottomHeader").removeClass("fixed")
949  //       $("#defaultHeader nav.navbar").removeClass("fixed-navbar")
950  //       $('body [data-bs-toggle="tooltip"]').tooltip("disable")
951
952  //       fixedInitClass = "fixed-init"
953
954  //       if ($("div#menuHero ul").length) {
955  //         if (stickyPoint) {
956  //           $("div#menuHero").css({
957  //             top: 0
958  //           })
959  //         }
960  //         stickyPoint = false
961  //         $("div#menuHero").addClass("absolute-side-menu").removeClass("fixed-side-menu")
962  //       }
963  //     }
964  //     oldScrollDirection = scrollDirection
965  //   }
966
967  //   $window.on("scroll resize orientationchange", sticky)
968
969  // Back to top button
970  $(window).scroll(function () {
971    if ($(this).scrollTop() != 0) {
972      $("#back-to-top").fadeIn();
973    } else {
974      $("#back-to-top").fadeOut();
975    }
976  });
977
978  $("#back-to-top").on("click touchstart", function (e) {
979    e.preventDefault();
980    $("body,html").animate({ scrollTop: 0 }, 700);
981    return false;
982  });
983  // Tickerbar marquee -----------------------------------------------------------------------------------------------
984  $(".marquee").marquee({
985    //duration in milliseconds of the marquee
986    // duration: 7000,
987    speed: 120,
988    //gap in pixels between the tickers
989    gap: 50,
990    //time in milliseconds before the marquee will start animating
991    delayBeforeStart: 0,
992    //'left' or 'right'
993    direction: "left",
994    //true or false - should the marquee be duplicated to show an effect of continues flow
995    duplicated: false,
996    //true or false - should the marquee animation pause on hover
997    pauseOnHover: true,
998  });
999
1000  let owl = $("#sectionNewsDefault .owl-carousel");
1001  let owlOptions = {
1002    stagePadding: 0,
1003    loop: true,
1004    dots: true,
1005    margin: 0,
1006    nav: true,
1007    navText: ['<i class="fas fa-angle-left fa-2x" aria-hidden="true"></i>', '<i class="fas fa-angle-right fa-2x" aria-hidden="true"></i>'],
1008    // navContainer: '.custom-nav',
1009    navClass: ["owl-prev text-white", "owl-next float-end tw-secondary"],
1010    items: 3,
1011    autoWidth: false,
1012    center: true,
1013    URLhashListener: true,
1014    autoplayHoverPause: false,
1015    startPosition: "URLHash",
1016    responsive: {
1017      0: {
1018        items: 1,
1019      },
1020      992: {
1021        items: 1,
1022      },
1023    },
1024  };
1025  var owlActive = owl.owlCarousel(owlOptions);
1026  $("#sectionNewsDefault .owl-prev").attr("aria-label", "Previous news item").removeAttr("role");
1027  $("#sectionNewsDefault .owl-next").attr("aria-label", "Next news item").removeAttr("role");
1028
1029  $(".customizer-content a").click(function (e) {
1030    e.preventDefault();
1031  });
1032
1033  let owlEvents = $("#tw-events .owl-carousel");
1034  let owlOptionsEvents = {
1035    // stagePadding: 0,
1036    loop: false,
1037    dots: false,
1038    // margin:0,
1039    nav: true,
1040    navText: ['<i class="fas fa-angle-left fa-2x" aria-hidden="true"></i>', '<i class="fas fa-angle-right fa-2x" aria-hidden="true"></i>'],
1041    // navContainer: '.custom-nav',
1042    navClass: ["owl-prev tw-secondary", "owl-next float-end tw-secondary"],
1043    items: 5,
1044    autoWidth: false,
1045    center: false,
1046    URLhashListener: true,
1047    autoplayHoverPause: false,
1048    startPosition: "URLHash",
1049    responsive: {
1050      0: {
1051        items: 2,
1052      },
1053      768: {
1054        items: 3,
1055      },
1056      992: {
1057        items: 4,
1058      },
1059      1184: {
1060        items: 5,
1061      },
1062    },
1063  };
1064  var owlActiveEvents = owlEvents.owlCarousel(owlOptionsEvents);
1065  $("#tw-events .owl-prev").attr("aria-label", "Previous events").removeAttr("role");
1066  $("#tw-events .owl-next").attr("aria-label", "Next events").removeAttr("role");
1067
1068  let owlHeroQuickLinks = $("#homepage-nav.owl-carousel");
1069  let owlOptionsHeroQuickLinks = {
1070    items: 4,
1071    nav: true,
1072    loop: false,
1073    rewind: true,
1074    dots: false,
1075    margin: 0,
1076    checkVisible: false,
1077    responsiveBaseElement: "#homepage-nav",
1078    responsiveRefreshRate: "0ms",
1079    navText: ['<i class="fa fa-chevron-left fa-1x" aria-hidden="true"></i>', '<i class="fa fa-chevron-right fa-1x" aria-hidden="true"></i>'],
1080    responsive: {
1081      1280: {
1082        items: 4,
1083      },
1084      1199: {
1085        margin: 140,
1086      },
1087      991: {
1088        items: 4,
1089        margin: 90,
1090      },
1091      767: {
1092        items: 3,
1093        margin: 15,
1094      },
1095      400: {
1096        items: 2,
1097        margin: 0,
1098      },
1099      0: {
1100        items: 1,
1101        margin: 0,
1102        autoHeight: true,
1103        autoHeightClass: "owl-height",
1104      },
1105    },
1106  };
1107  var owlActiveHeroQuickLinks = owlHeroQuickLinks.owlCarousel(owlOptionsHeroQuickLinks);
1108  $("#homepage-nav .owl-prev").attr("aria-label", "Previous quick links").removeAttr("role");
1109  $("#homepage-nav .owl-next").attr("aria-label", "Next quick links").removeAttr("role");
1110
1111  function markHeroClonesInert() {
1112    $("#homepage-nav .owl-item.cloned").each(function () {
1113      var $clone = $(this);
1114      $clone.attr({ "aria-hidden": "true", inert: "" });
1115      $clone.find("a").each(function () {
1116        var $a = $(this);
1117        if ($a.is("[href]") && !$a.data("twOrigHref")) {
1118          $a.data("twOrigHref", $a.attr("href"));
1119        }
1120        $a.removeAttr("href")
1121          .attr({ "tabindex": "-1", "aria-hidden": "true", role: "presentation" });
1122      });
1123    });
1124  }
1125  markHeroClonesInert();
1126  owlHeroQuickLinks.on("translated.owl.carousel refreshed.owl.carousel resized.owl.carousel", markHeroClonesInert);
1127
1128  // jQuery(".homepage-nav").owlCarousel({
1129  //     items:4,
1130  //     nav:true,
1131  //     dots: false,
1132  //     loop:true,
1133  //     margin:0,
1134  //     checkVisible: false,
1135  //     responsiveBaseElement: '.homepage-menu',
1136  //     responsiveRefreshRate: '0ms',
1137  //     navText:['<i class="fa fa-chevron-left fa-1x"></i>','<i class="fa fa-chevron-right fa-1x"></i>'],
1138  //     responsive:{
1139  //         1280:{
1140  //             items:4
1141  //         },
1142  //         1199:{
1143  //             margin:140
1144  //         },
1145  //         991:{
1146  //             items:4,
1147  //             margin:90
1148  //         },
1149  //         767:{
1150  //             items:3,
1151  //             margin:15
1152  //         },
1153  //         479:{
1154  //             items:2,
1155  //             margin:0
1156  //         },
1157  //         0:{
1158  //             items:1,
1159  //             margin:0,
1160  //             autoHeight: true,
1161  //             autoHeightClass: 'owl-height'
1162  //         }
1163  //     }
1164  // });
1165
1166  // Handle search -----------------------------------------------------------------------------------------------------------------------------------------------
1167  function generateSearchItem(item) {
1168    if (jActiveAdvancedSearch == 0) {
1169      var dom = new DOMParser().parseFromString(item.content, "text/html");
1170      var content = dom.body.textContent;
1171      content = content.substring(0, 150) + "...";
1172    }
1173    let html = "";
1174    html = `
1175    <div class='col-md-4 mb-3'><h2><a href="
1176        ${jActiveAdvancedSearch == 1 ? item.url : window.location.protocol + "//" + window.location.host + item.slug}
1177      ">
1178      ${item.title}
1179      </a></h2><div>
1180      ${content || item.content}
1181      </div></div>
1182    `;
1183    return html;
1184  }
1185
1186  if (window.location.pathname.includes("search")) {
1187    const queryString = window.location.search;
1188    const urlParams = new URLSearchParams(queryString);
1189    const q = urlParams.get("q");
1190    // Load the locations once, on page-load.
1191    if (jActiveAdvancedSearch == 1) {
1192      $(async function () {
1193        const res = await fetch("/search/search.php", {
1194          method: "POST",
1195          headers: {
1196            "Content-Type": "application/json",
1197          },
1198          body: JSON.stringify({ q: q }),
1199        });
1200        const data = await res.json();
1201
1202        let searchContainer = document.getElementById("search-results");
1203        let searchString = document.getElementById("search-string");
1204        searchString.innerHTML = q;
1205        let searchArr = [];
1206        // Iterate over each element in the array
1207        for (var i = 0; i < data.length; i++) {
1208          searchContainer.innerHTML += generateSearchItem(data[i]);
1209          searchArr.push(data[i]);
1210        }
1211        if (searchArr.length < 1) {
1212          searchContainer.innerHTML += '<div class="text-center text-warning fs-1">There are no results found</div>';
1213        }
1214        // console.log(data);
1215      });
1216    } else {
1217      $(function () {
1218        $.getJSON(window.location.protocol + "//" + window.location.host + `/json/search/search.json?sk=${Date.now()}`)
1219          .done(function (data) {
1220            let searchContainer = document.getElementById("search-results");
1221            let searchString = document.getElementById("search-string");
1222            searchString.innerHTML = q;
1223            let searchArr = [];
1224            // Iterate over each element in the array
1225            for (var i = 0; i < data.length; i++) {
1226              var content = data[i].content;
1227              // look for the entry with a matching `code` value
1228              if (data[i].title.toLowerCase().includes(q.toLowerCase())) {
1229                searchContainer.innerHTML += generateSearchItem(data[i]);
1230                searchArr.push(data[i]);
1231              } else {
1232                if (content && content.toLowerCase().includes(q.toLowerCase())) {
1233                  searchContainer.innerHTML += generateSearchItem(data[i]);
1234                  searchArr.push(data[i]);
1235                }
1236              }
1237            }
1238            if (searchArr.length < 1) {
1239              searchContainer.innerHTML += '<div class="text-center text-warning fs-1">There are no results found</div>';
1240            }
1241          })
1242          .fail(function (data) {
1243            console.log("Error loading results ❌");
1244          });
1245      });
1246    }
1247  }
1248
1249  // Handle Contact Us -----------------------------------------------------------------------------------------------------------------------------------------------------
1250  // $(".contact-form #submit-btn").click(function (e) {
1251  //   e.preventDefault()
1252  //   data = $(".contact-form").serialize()
1253  //   url = window.location.protocol + "//" + window.location.host
1254  //   $.ajax({
1255  //     method: "post",
1256  //     url: jurl + "/static/contact", // NEED TO UPDATE
1257  //     data: $(".contact-form").serialize() + "&url=" + url,
1258  //     success: function (msg) {
1259  //       if (msg.message == "Success") {
1260  //         $(".contact-success").removeClass("d-none")
1261  //         $(".contact-success").html(jcontactSuccessMsg)
1262  //         setTimeout(cleanContactUs, 3000)
1263  //       }
1264  //     },
1265  //     fail: function (err) {
1266  //       console.log("Error submitting contact form ❌")
1267  //     }
1268  //   })
1269  // })
1270
1271  $("form.contact-form").submit(function (e) {
1272    data = $("form.contact-form").serialize();
1273    url = window.location.protocol + "//" + window.location.host;
1274    $.ajax({
1275      method: "post",
1276      url: jurl + "/static/contact", // NEED TO UPDATE
1277      data: $("form.contact-form").serialize() + "&url=" + url,
1278      success: function (msg) {
1279        if (msg.message == "Success") {
1280          $(".contact-success").removeClass("d-none");
1281          $(".contact-success").html(jcontactSuccessMsg);
1282          setTimeout(cleanContactUs, 3000);
1283        }
1284      },
1285      error: function (xhr, status, error) {
1286        console.log("Error submitting contact form ❌");
1287      },
1288    });
1289  });
1290
1291  // Clean inputs and hide success message after 3000ms
1292  function cleanContactUs() {
1293    $("#first").val("");
1294    $("#last").val("");
1295    $("#email").val("");
1296    $("#phone").val("");
1297    $("#message").val("");
1298    $(".contact-success").addClass("d-none");
1299  }
1300
1301  // Handle Quick Subscribe -------------------------------------------------------------------------------------------------------------------------------------------------
1302
1303  function validateEmail(email) {
1304    var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
1305    return re.test(String(email).toLowerCase());
1306  }
1307
1308  $("#sub_phone").mask("999-999-9999", {
1309    placeholder: "___-___-____",
1310    clearIfNotMatch: true,
1311  });
1312
1313  $("#subs_button").click(function (e) {
1314    e.preventDefault();
1315    username = $("#sub_name").val();
1316    if (username.length === 0) {
1317      $("#sub_name").addClass("has-error");
1318    } else {
1319      if ($("#sub_name").hasClass("has-error")) {
1320        $("#sub_name").removeClass("has-error");
1321      }
1322    }
1323
1324    email = $("#sub_email").val();
1325    if (email.length === 0 || !validateEmail(email)) {
1326      $("#sub_email").addClass("has-error");
1327    } else {
1328      if ($("#sub_email").hasClass("has-error")) {
1329        $("#sub_email").removeClass("has-error");
1330      }
1331    }
1332
1333    phone = $("#sub_phone").val();
1334    if (email.length === 0 || !validateEmail(email)) {
1335      $("#sub_email").addClass("has-error");
1336    } else {
1337      if ($("#sub_email").hasClass("has-error")) {
1338        $("#sub_email").removeClass("has-error");
1339      }
1340    }
1341    if (username != "" && email != "" && validateEmail(email)) {
1342      let btn_html = $("#subs_button").html();
1343      $("#subs_button + div.alert").remove();
1344      $("#subs_button").html('<i class="fa fa-spinner fa-spin"></i> Working');
1345
1346      $("#sub_name").attr("readonly", true);
1347      $("#sub_email").attr("readonly", true);
1348      $("#sub_phone").attr("readonly", true);
1349
1350      $.ajax({
1351        method: "post",
1352        url: jurl + "/subscribers/register?client_id=" + jid,
1353        data: {
1354          name: username,
1355          email: email,
1356          phone: phone,
1357          quick: true,
1358        },
1359        success: function (msg) {
1360          //remove errors and display success
1361          if ($("#sub_name").hasClass("has-error")) {
1362            $("#sub_name").removeClass("has-error");
1363          }
1364          if ($("#sub_email").hasClass("has-error")) {
1365            $("#sub_email").removeClass("has-error");
1366          }
1367          if (msg.type != "success") {
1368            errrMessage = msg.message;
1369            if (msg && msg.errors && msg.errors.email) {
1370              errrMessage = msg.errors.email[0];
1371            }
1372            $("#subs_button").addClass("btn-danger homepage-subscribed-danger font-weight-bold");
1373            $("#subs_button").html("Error Occurred!");
1374            $("#homepage-subscriber-form").after(
1375              `<div class="row ms-1"><div id="subscribe-error" class="alert alert-danger alert-dismissible" role="alert"> ${errrMessage}<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></div></div>`,
1376            );
1377            //Remove alert, button danger & button text after 3 sec
1378            setTimeout(function () {
1379              $("#subs_button").removeClass("btn-danger");
1380              $("#subs_button").addClass("subscribe-button");
1381              $("#subs_button").html("Subscribe now");
1382              $("#subscribe-error").fadeOut();
1383            }, 6000);
1384          } else {
1385            if ($("#subs_button").hasClass("homepage-subscribed-danger")) {
1386              $("#subs_button").removeClass("homepage-subscribed-danger");
1387            }
1388            if ($("#subs_button").hasClass("btn-danger")) {
1389              $("#subs_button").removeClass("btn-danger");
1390            }
1391            $("#subs_button").addClass("btn-success homepage-subscribed-success font-weight-bold");
1392            $("#subs_button").html("Successfully Subscribed!");
1393            // Remove success class after 3 sec
1394            setTimeout(function () {
1395              $("#subs_button").removeClass("btn-success");
1396              $("#subs_button").addClass("subscribe-button");
1397              $("#subs_button").html("Subscribe now");
1398            }, 6000);
1399          }
1400          //clear input values
1401          $("#sub_name").val("");
1402          $("#sub_email").val("");
1403          $("#sub_phone").val("");
1404          $("#sub_name").attr("readonly", false);
1405          $("#sub_email").attr("readonly", false);
1406          $("#sub_phone").attr("readonly", false);
1407        },
1408        fail: function (jqXHR, textStatus) {
1409          //display error message
1410          if ($("#sub_name").hasClass("has-error")) {
1411            $("#sub_name").removeClass("has-error");
1412          }
1413          if ($("#sub_email").hasClass("has-error")) {
1414            $("#sub_email").removeClass("has-error");
1415          }
1416          $("#subs_button").html("Error");
1417
1418          //clear input values
1419          $("#sub_name").val("");
1420          $("#sub_email").val("");
1421          $("#sub_phone").val("");
1422          $("#sub_name").attr("readonly", false);
1423          $("#sub_email").attr("readonly", false);
1424          $("#sub_phone").attr("readonly", false);
1425        },
1426      });
1427    }
1428  });
1429
1430  // Hande Subscribe ------------------------------------------------------------------------------------------------------------------------------------------------------------
1431
1432  // Create meeting categories
1433  function generateMeetingCategories() {
1434    let categoriesHtml = "";
1435    let categoriesContainer = $("#juniper-meetings-categories");
1436    let meetingCategories = JSON.parse(jMeetingsSubscribableCategories);
1437    meetingCategories.forEach((category) => {
1438      categoriesHtml += `<div class="sub">
1439                            <input id="juniper-subscriber-agenda-${category.id}" class="juniper-child-checkbox" type="checkbox" name="agenda_terms[]" value="${category.id}">
1440                            <label for="juniper-subscriber-agenda-${category.id}">${category.name}</label><br>
1441                          </div>`;
1442    });
1443    categoriesContainer.html(categoriesHtml);
1444  }
1445
1446  function generateNewsCategories() {
1447    let newsCategoriesHtml = "";
1448    let newsCategoriesContainer = $("#juniper-news-categories");
1449    let newsCategories = JSON.parse(jNewsSubscribableCategories);
1450    newsCategories.forEach((category) => {
1451      newsCategoriesHtml += `<div class="sub">
1452                              <input id="juniper-subscriber-news-${category.id}" class="juniper-child-checkbox" type="checkbox" name="post_terms[]" value="${category.id}">
1453                              <label for="juniper-subscriber-news-${category.id}">${category.name}</label><br>
1454                            </div>`;
1455    });
1456    newsCategoriesContainer.html(newsCategoriesHtml);
1457  }
1458
1459  // Invoke generate meeting categories
1460  generateMeetingCategories();
1461  // Invoke generate news categories
1462  generateNewsCategories();
1463
1464  // Mask for Phone
1465  $("#juniper-subscriber-phone").mask("(999) 999-9999");
1466
1467  // Checkbox
1468  $(".juniper-parent-checkbox")
1469    .click(function () {
1470      $(this).parent().find(".juniper-child-checkbox").prop("disabled", !this.checked).prop("checked", this.checked);
1471    })
1472    .trigger("click");
1473
1474  // Phone required when sms checked
1475  $("#juniper-subscriber-receive-text-message")
1476    .click(function () {
1477      $("#juniper-subscriber-phone, #juniper-subscriber-carrier").prop("required", this.checked);
1478    })
1479    .trigger("click");
1480
1481  // Handle subscribe now button
1482  $("body").on("submit", "#juniper-subscriber-form", function () {
1483    $.ajax({
1484      type: "POST",
1485      url: jurl + "/subscribers/register?client_id=" + jid,
1486      data: $(this).serialize(),
1487      success: function (data) {
1488        if (data.type == "success") {
1489          $("#juniper-subscribe-complete").removeClass("d-none");
1490        }
1491        if (data.type == "error") {
1492          let text = "";
1493          for (const [key, value] of Object.entries(data.errors)) {
1494            text += `<p>${value}</p>`;
1495          }
1496          $("#juniper-subscribe-error").removeClass("d-none");
1497          $("#juniper-subscribe-error").html(text);
1498        }
1499      },
1500      fail: function (error) {
1501        console.log("Error submiting subscriber form ❌");
1502      },
1503    });
1504
1505    return true;
1506  });
1507
1508  // Handle unsubscribe button
1509  $("body").on("click", "#twd-unsubscribe-btn", function () {
1510    if (confirm("Are you sure?")) {
1511      $.ajax({
1512        type: "GET",
1513        url: $(this).attr("href"),
1514        dataType: "html",
1515      }).done(function (date) {
1516        $("#twd-subscribe-complete").html(date);
1517        $("#twd-subscriber-submit").val("Subscribe");
1518        $("#twd-subscriber-form").slideDown();
1519      });
1520    }
1521    return false;
1522  });
1523});
1524
1525function waitForElement(selector) {
1526  return new Promise((resolve) => {
1527    if (document.querySelector(selector)) {
1528      return resolve(document.querySelector(selector));
1529    }
1530
1531    const observer = new MutationObserver((mutations) => {
1532      if (document.querySelector(selector)) {
1533        observer.disconnect();
1534        resolve(document.querySelector(selector));
1535      }
1536    });
1537
1538    observer.observe(document.body, {
1539      childList: true,
1540      subtree: true,
1541    });
1542  });
1543}
1544
1545/*****************************************/
1546/*****************************************/
1547/***********CALENDAR 2019*****************/
1548/*****************************************/
1549/*****************************************/
1550jQuery(document).ready(function () {
1551  (async () => {
1552    $('body [data-bs-toggle="tooltip"]').tooltip("enable");
1553    let eventsList2019, eventsListContainer2019, headMonth2019, monthAndYear2019, calendarBodyTable2019;
1554
1555    eventsList2019 = waitForElement("#events-calendar-2019-list");
1556    eventsListContainer2019 = waitForElement("#events-calendar-2019-list-container");
1557    headMonth2019 = waitForElement("#thead-month-2019");
1558    monthAndYear2019 = waitForElement("#monthAndYearTitle2019");
1559    calendarBodyTable2019 = waitForElement("#calendar-2019-body");
1560
1561    let eventsListEmpty = "No Events Today.";
1562
1563    let dayNames2019 = ["S", "M", "T", "W", "T", "F", "S"];
1564
1565    let dayNames2019Content = "<tr>";
1566    dayNames2019.forEach((name) => {
1567      dayNames2019Content += `<th class="text-center text-capitalize" data-days="${name}">${name}</th>`;
1568    });
1569    dayNames2019Content += "</tr>";
1570
1571    headMonth2019.then((el) => (el.innerHTML = dayNames2019Content));
1572
1573    if ((jActiveHomepageSections && jActiveHomepageSections.includes("Events-calendar-2019") && window.location.pathname == "/") || window.location.pathname.includes("customizer")) {
1574      calendarBodyTable2019.then(async () => {
1575        events = await getEvents(eventCategory);
1576        showCalendar2019(currentMonth, currentYear);
1577        // setEventsListDate(today)
1578        generateDayEvents2019List(today, todayMonth, todayYear);
1579      });
1580    }
1581
1582    // Show Calendar 2019
1583    async function showCalendar2019(month, year) {
1584      let firstDay = new Date(year, month).getDay();
1585      let tbl = await waitForElement("#calendar-2019-body");
1586      tbl.innerHTML = "";
1587      monthAndYear2019.then((el) => (el.innerHTML = months[month] + ", " + year));
1588      // Create all calendar items
1589
1590      let calendarRows = getCalendarRows();
1591      // console.log(calendarRows)
1592      // Create all calendar items
1593
1594      calendarRows.forEach((calendarRow) => {
1595        var row = document.createElement("tr");
1596        calendarRow.forEach((day) => {
1597          let cell = document.createElement("td");
1598
1599          if (isDateOfPreviousMonth(day)) {
1600            cell.classList.add("previous-month");
1601          }
1602
1603          if (isDateOfNextMonth(day)) {
1604            cell.classList.add("next-month");
1605          }
1606
1607          // check if calendar date has events
1608          let dayEvents = getEventOnDay(day);
1609
1610          let div = document.createElement("div");
1611          div.className = "text-center";
1612
1613          let todayCloneForCalendarCell = new Date(today);
1614          todayCloneForCalendarCell.setHours(0, 0, 0, 0);
1615          if (todayCloneForCalendarCell.getTime() === day.getTime()) {
1616            cell.classList.add("tw-calendar-day-active");
1617          }
1618
1619          if (dayEvents.length) {
1620            cell.classList.add("has-events");
1621            div.classList.add("cursor-pointer");
1622            div.innerHTML = `<a href="${jEventsPage}/?heygov-event=${dayEvents[0]["pid"]}" class="d-flex justify-content-center text-decoration-none">${day.getDate()}</a>`;
1623          } else {
1624            div.innerText = day.getDate();
1625          }
1626
1627          cell.appendChild(div);
1628
1629          cell.setAttribute("data-date", day);
1630          cell.setAttribute("data-month", day.getMonth() + 1);
1631          cell.setAttribute("data-year", day.getFullYear());
1632          cell.setAttribute("data-month_name", months[day.getMonth()]);
1633
1634          row.appendChild(cell);
1635        });
1636        if (tbl) {
1637          tbl.appendChild(row);
1638        }
1639      });
1640    }
1641
1642    // Generate list of day events
1643    async function generateDayEvents2019List(day, month, year) {
1644      let dateEnd = new Date(day);
1645      dateEnd.setFullYear(dateEnd.getFullYear() + 2);
1646
1647      let args = {
1648        limit: 6,
1649        date_start: formatDate(day),
1650        date_end: formatDate(dateEnd),
1651      };
1652
1653      let eventsFor2019List = await getEvents(eventCategory, args);
1654      let events2019List = "";
1655
1656      eventsFor2019List.forEach((e, index) => {
1657        let startsAt = new Date(
1658          new Date(e.starts_at).toLocaleString("en", {
1659            timeZone: timezone,
1660          }),
1661        );
1662
1663        let dateStart = startsAt.toLocaleString("en-US", { month: "long" }) + " " + startsAt.getDate() + ", " + startsAt.getFullYear();
1664
1665        events2019List += `<div class="single-homepage-event d-flex pb-3">
1666                            <article class="d-flex flex-grow-1">
1667                                <div class="d-flex flex-grow-1 flex-column event-item">
1668                                    <div class="article-thumbnail">
1669                                        ${
1670                                          e.image_path || e.image_url || jeventDefaultFeaturedImage
1671                                            ? `<a href="${jEventsPage}/?heygov-event=${e.pid}">
1672                                                <img src="${e.image_path || e.image_url ? (e.image_path ? jHeyGovStorage + e.image_path : e.image_url) : jeventDefaultFeaturedImage}" class="img-responsive" data-uw-rm-alt-original="" alt="text" data-uw-rm-alt="BE">
1673
1674                                            </a>`
1675                                            : ""
1676                                        }
1677                                    </div>
1678
1679                                    <div class="event-item-card d-flex flex-grow-1 flex-column">
1680                                        <div class="event-date">
1681                                            ${dateStart}
1682                                        </div>
1683                                        <div class="">
1684                                            <a href="${jEventsPage}/?heygov-event=${e.pid}">
1685                                                <h4>${e.name}</h4>
1686                                            </a>
1687                                            <div class="entry-content">
1688                                                <p>
1689                                                    ${(() => {
1690                                                      if (!e.description) return "";
1691                                                      // Remove HTML links <a>...</a> but keep the text
1692                                                      const descWithoutLinks = e.description
1693                                                        .replace(/<a[^>]*>([^<]*)<\/a>/gi, "$1") // Remove <a> tags but keep inner text
1694                                                        .replace(/\[([^\]]+)\]\([^\)]+\)/g, "$1") // Markdown links [text](url) -> text
1695                                                        .replace(/https?:\/\/[^\s]+/g, ""); // Plain URLs
1696                                                      if (descWithoutLinks.length > 100) {
1697                                                        return descWithoutLinks.slice(0, 100).replace(/\p{P}\s*$/u, "") + "...";
1698                                                      }
1699                                                      return descWithoutLinks;
1700                                                    })()}
1701
1702                                                </p>
1703                                            </div>
1704                                            ${e.description && e.description.length > 100 ? `<div class="tw-read-more mt-auto"><a class="read-more-link" href="${jEventsPage}/?heygov-event=${e.pid}">Read More</a></div>` : ""}
1705                                        </div>
1706                                    </div>
1707                                </div>
1708                            </article>
1709                        </div>`;
1710      });
1711
1712      if (events2019List == "") {
1713        events2019List = eventsListEmpty;
1714      }
1715      eventsListContainer2019.then((el) => (el.innerHTML = events2019List));
1716
1717      // Fix for tooltip to go allways left
1718      const tooltipLinks = document.querySelectorAll('#tw-quick-links a[data-bs-toggle="tooltip"]');
1719      tooltipLinks.forEach((link) => {
1720        const tooltipInstance = bootstrap.Tooltip.getInstance(link);
1721        if (tooltipInstance) {
1722          tooltipInstance.dispose();
1723        }
1724        link.setAttribute("data-bs-placement", "left");
1725        new bootstrap.Tooltip(link);
1726      });
1727    }
1728  })();
1729  document.querySelectorAll("#main-menu .dropdown-menu .dropdown-toggle").forEach((item) => {
1730    item.addEventListener("click", function (event) {
1731      event.stopPropagation(); // Prevent Bootstrap from closing the entire dropdown
1732      let submenu = this.nextElementSibling;
1733      if (submenu) {
1734        submenu.classList.toggle("show");
1735      }
1736    });
1737  });
1738
1739  // Close all submenus when clicking outside
1740  document.addEventListener("click", function () {
1741    document.querySelectorAll("#main-menu .dropdown-menu .show").forEach((submenu) => {
1742      submenu.classList.remove("show");
1743    });
1744  });
1745});

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.