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.