PageSourceSearch

https://keelesu.com/pagestylesheet/scripts.js?v=1050

js keelesu.com collected 2026-10-02 19:06:54 UTC 12,867 bytes, 327 lines download raw bytes

1var sabbsChosen = true;
2var eventPosition = 1;
3
4document.addEventListener("DOMContentLoaded", function () {
5  // ===== Officers (unchanged) =====
6  document.querySelectorAll("#officers ul li").forEach(function (li) {
7    var desc = li.querySelector(".msl-imagenav-description");
8    var title = li.querySelector(".msl-imagenav-title");
9    if (desc && title) {
10      var wrapper = document.createElement("div");
11      wrapper.className = "sabbs_description";
12      wrapper.appendChild(desc);
13      wrapper.appendChild(title);
14      li.appendChild(wrapper);
15    }
16  });
17
18  document.querySelectorAll(".sabbs_description").forEach(function (desc) {
19    var title = desc.querySelector(".msl-imagenav-title");
20    var description = desc.querySelector(".msl-imagenav-description");
21    if (title && description) {
22      desc.insertBefore(description, title);
23    }
24  });
25
26  document.querySelectorAll("#officers ul li").forEach(function (li) {
27    var image = li.querySelector(".msl-imagenav-image");
28    var desc = li.querySelector(".sabbs_description");
29    if (image && desc) {
30      image.insertAdjacentElement("afterend", desc);
31    }
32  });
33
34  document.querySelectorAll("#scroller .news_item, #news .news_item").forEach(function (item) {
35    item.addEventListener("click", function () {
36      var link = item.querySelector("a");
37      if (link) window.location = link.getAttribute("href");
38    });
39  });
40
41  // ===== Events: Slider (scroll-snap based, no external library) =====
42  //
43  // CHANGED FROM PREVIOUS VERSION:
44  // The horizontal scroll-snap layout used to be applied entirely via inline
45  // styles set here in JS. That meant if JS was slow, blocked, or failed,
46  // MSL's raw {eventScroller} output rendered as an unstyled vertical list -
47  // which is what prompted this rewrite. That base layout now lives in
48  // event-widget.css as plain CSS targeting MSL's real markup
49  // (#eventCarousel .msl_eventlist > div), so the slider works - including
50  // touch swipe, which is native browser behaviour for overflow-x:auto - even
51  // with this script disabled. This script now only adds progressive
52  // enhancements: the wrapped card markup, lazy-loading attributes, arrow
53  // buttons (vertically centered on the left/right edges), and light
54  // autoplay that always moves exactly one card at a time.
55  //
56  // Deliberately NOT using a third-party slider library (e.g. Swiper): its
57  // bundle (~70KB JS + ~13KB CSS minified) is far heavier than this entire
58  // file, which would work against the goal of reducing page weight.
59  var eventCarousel = document.getElementById("eventCarousel");
60  if (eventCarousel) {
61    eventCarousel.classList.add("container-fluid");
62
63    var eventList = eventCarousel.querySelector(".msl_eventlist");
64    if (eventList) {
65      initEventsScroller(eventList);
66      observeEventsList(eventList);
67    }
68  }
69
70  // Officer nav image handling (unchanged) - real <img>, lazy-loaded.
71  document.querySelectorAll(".msl-imagenav-image").forEach(function (img) {
72    var wrapper = document.createElement("div");
73    wrapper.className = "sabbImage";
74    img.loading = "lazy";
75    img.decoding = "async";
76    if (!img.getAttribute("alt")) {
77      img.setAttribute("alt", "");
78    }
79    img.parentNode.insertBefore(wrapper, img);
80    wrapper.appendChild(img);
81  });
82
83  if (!sabbsChosen) {
84    document.querySelectorAll(".sabbImage img").forEach(function (el) {
85      el.src = "https://keelesu.com/assets/site_resources/mysterysabb.png";
86    });
87    document.querySelectorAll(".msl-imagenav-description").forEach(function (desc) {
88      desc.textContent = "To be elected:";
89    });
90  }
91
92  document.querySelectorAll(".msl-imagenav-title").forEach(function (title) {
93    var h4 = document.createElement("h4");
94    h4.appendChild(title.cloneNode(true));
95    title.replaceWith(h4);
96  });
97
98  document.querySelectorAll(".msl-imagenav-description").forEach(function (desc) {
99    var h3 = document.createElement("h3");
100    h3.appendChild(desc.cloneNode(true));
101    desc.replaceWith(h3);
102  });
103
104  var prefersReducedMotion = window.matchMedia &&
105    window.matchMedia("(prefers-reduced-motion: reduce)").matches;
106
107  // ===== Helper functions for Events Slider =====
108  function initEventsScroller(eventList) {
109    // The widget renders one inner <div> that holds .event_item. Select it safely:
110    var inner = eventList.querySelector(":scope > div");
111    if (!inner) return;
112
113    // Layout itself now comes from CSS (#eventCarousel .msl_eventlist > div).
114    // Just flag it as JS-enhanced for anything that still targets this class.
115    inner.classList.add("events-scroll");
116    inner.setAttribute("role", "region");
117    inner.setAttribute("aria-label", "Upcoming events, scrollable");
118    inner.setAttribute("tabindex", "0");
119
120    // Process each event tile safely (no reordering of <dl> contents)
121    var items = inner.querySelectorAll(".event_item");
122    items.forEach(function (item) {
123      if (item.dataset.scrollerReady === "true") return;
124
125      item.style.scrollSnapAlign = "start";
126
127      // Wrap <dl> inside .cardWrapper > .card (without changing order)
128      if (!item.querySelector(".cardWrapper")) {
129        var dl = item.querySelector("dl");
130        if (dl) {
131          var wrapper = document.createElement("div");
132          wrapper.className = "cardWrapper";
133          var card = document.createElement("div");
134          card.className = "card";
135          card.appendChild(dl);
136          wrapper.appendChild(card);
137          item.appendChild(wrapper);
138        }
139      }
140
141      // Image performance + accessibility
142      item.querySelectorAll("img").forEach(function (img) {
143        img.loading = "lazy";
144        img.decoding = "async";
145        var nameEl = item.querySelector(".msl_event_name");
146        if (img.getAttribute("alt") === "" && nameEl) {
147          img.setAttribute("alt", nameEl.textContent.trim());
148        }
149      });
150
151      item.dataset.scrollerReady = "true";
152    });
153
154    // Arrow buttons - appended directly to #eventCarousel (not eventList),
155    // since the CSS now positions them as absolute children of the carousel
156    // itself so they sit vertically centered over the left/right edges of
157    // the card row rather than in a row underneath it.
158    var prevBtn = eventCarousel.querySelector(".slider-prev");
159    var nextBtn = eventCarousel.querySelector(".slider-next");
160    if (!prevBtn) {
161      prevBtn = document.createElement("button");
162      prevBtn.className = "slider-prev";
163      prevBtn.type = "button";
164      prevBtn.setAttribute("aria-label", "Previous event");
165      prevBtn.innerHTML = '<i class="fas fa-chevron-left" aria-hidden="true"></i>';
166      eventCarousel.appendChild(prevBtn);
167
168      prevBtn.addEventListener("click", function () {
169        var items = Array.prototype.slice.call(inner.querySelectorAll(".event_item"));
170        goToIndex(inner, items, getCurrentIndex(inner, items) - 1);
171      });
172    }
173    if (!nextBtn) {
174      nextBtn = document.createElement("button");
175      nextBtn.className = "slider-next";
176      nextBtn.type = "button";
177      nextBtn.setAttribute("aria-label", "Next event");
178      nextBtn.innerHTML = '<i class="fas fa-chevron-right" aria-hidden="true"></i>';
179      eventCarousel.appendChild(nextBtn);
180
181      nextBtn.addEventListener("click", function () {
182        var items = Array.prototype.slice.call(inner.querySelectorAll(".event_item"));
183        goToIndex(inner, items, getCurrentIndex(inner, items) + 1);
184      });
185    }
186
187    updateButtonState(inner, prevBtn, nextBtn);
188
189    // Keep button disabled-state in sync as the user scrolls/swipes manually
190    var scrollTimer = null;
191    inner.addEventListener("scroll", function () {
192      if (scrollTimer) window.clearTimeout(scrollTimer);
193      scrollTimer = window.setTimeout(function () {
194        updateButtonState(inner, prevBtn, nextBtn);
195      }, 100);
196    }, { passive: true });
197
198    // Light autoplay - moves exactly ONE card at a time (regardless of how
199    // many are visible per view), paused on hover/focus/touch, and skipped
200    // entirely if the visitor has requested reduced motion.
201    var autoplayId = null;
202    var cardCount = items.length;
203    if (!prefersReducedMotion && cardCount > 1 && inner.scrollWidth > inner.clientWidth) {
204      startAutoplay();
205      inner.addEventListener("mouseenter", stopAutoplay);
206      inner.addEventListener("mouseleave", startAutoplay);
207      inner.addEventListener("focusin", stopAutoplay);
208      inner.addEventListener("focusout", startAutoplay);
209      inner.addEventListener("touchstart", stopAutoplay, { passive: true });
210    }
211
212    function startAutoplay() {
213      stopAutoplay();
214      autoplayId = window.setInterval(function () {
215        var currentItems = Array.prototype.slice.call(inner.querySelectorAll(".event_item"));
216        var idx = getCurrentIndex(inner, currentItems);
217        var nextIdx = idx + 1 >= currentItems.length ? 0 : idx + 1;
218        goToIndex(inner, currentItems, nextIdx);
219      }, 6000);
220    }
221    function stopAutoplay() {
222      if (autoplayId) {
223        window.clearInterval(autoplayId);
224        autoplayId = null;
225      }
226    }
227
228    // No events fallback
229    if (cardCount < 1) {
230      inner.innerHTML = "No upcoming events";
231      inner.style.justifyContent = "center";
232      inner.style.alignItems = "center";
233      inner.style.color = "white";
234      inner.style.fontSize = "2vw";
235      prevBtn.style.display = "none";
236      nextBtn.style.display = "none";
237    }
238  }
239
240  // Which card is currently front-and-center in the scroll container -
241  // found by measuring actual rendered positions rather than assuming a
242  // fixed card width, so it stays correct at every viewport size.
243  function getCurrentIndex(inner, items) {
244    var innerLeft = inner.getBoundingClientRect().left;
245    var closestIndex = 0;
246    var closestDist = Infinity;
247    items.forEach(function (item, i) {
248      var dist = Math.abs(item.getBoundingClientRect().left - innerLeft);
249      if (dist < closestDist) {
250        closestDist = dist;
251        closestIndex = i;
252      }
253    });
254    return closestIndex;
255  }
256
257  // Scrolls so that items[index] becomes front-and-center - always exactly
258  // ONE card of movement per call, never a full "page" of several cards.
259  function goToIndex(inner, items, index, smooth) {
260    index = Math.max(0, Math.min(items.length - 1, index));
261    var item = items[index];
262    if (!item) return;
263    var delta = item.getBoundingClientRect().left - inner.getBoundingClientRect().left;
264    inner.scrollTo({
265      left: inner.scrollLeft + delta,
266      behavior: smooth === false ? "auto" : "smooth"
267    });
268  }
269
270  function updateButtonState(inner, prevBtn, nextBtn) {
271    var items = Array.prototype.slice.call(inner.querySelectorAll(".event_item"));
272    var hasOverflow = inner.scrollWidth > inner.clientWidth + 1;
273
274    if (!hasOverflow) {
275      prevBtn.style.display = "none";
276      nextBtn.style.display = "none";
277      return;
278    }
279    prevBtn.style.display = "";
280    nextBtn.style.display = "";
281
282    var idx = getCurrentIndex(inner, items);
283    prevBtn.disabled = idx <= 0;
284    nextBtn.disabled = idx >= items.length - 1;
285  }
286
287  function observeEventsList(eventList) {
288    var inner = eventList.querySelector(":scope > div");
289    var target = inner || eventList;
290
291    var debounceTimer = null;
292    var scheduleInit = function () {
293      if (debounceTimer) window.clearTimeout(debounceTimer);
294      debounceTimer = window.setTimeout(function () {
295        initEventsScroller(eventList);
296      }, 120);
297    };
298
299    var observer = new MutationObserver(function (mutations) {
300      var shouldUpdate = mutations.some(function (m) {
301        if (m.type === "childList") return true;
302        if (m.type === "attributes") {
303          return m.target && m.target.classList && m.target.classList.contains("event_item");
304        }
305        return false;
306      });
307      if (shouldUpdate) scheduleInit();
308    });
309
310    observer.observe(target, {
311      childList: true,
312      subtree: true,
313      attributes: true,
314      attributeFilter: ["class"]
315    });
316
317    window.addEventListener("resize", function () {
318      var innerNow = eventList.querySelector(".events-scroll") || eventList.querySelector(":scope > div");
319      var eventCarouselNow = eventList.closest("#eventCarousel");
320      var prevBtnNow = eventCarouselNow && eventCarouselNow.querySelector(".slider-prev");
321      var nextBtnNow = eventCarouselNow && eventCarouselNow.querySelector(".slider-next");
322      if (innerNow && prevBtnNow && nextBtnNow) {
323        updateButtonState(innerNow, prevBtnNow, nextBtnNow);
324      }
325    });
326  }
327});

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.