1const mainToggle = document.querySelector("#drop"); 2const menuLabel = document.querySelector('label[for="drop"]'); 3const submenuInputs = [...document.querySelectorAll('.jfh-menu input[id^="drop-"]:not(#drop)')]; 4const nav = document.querySelector(".jfh-menu"); 5const menu = document.querySelector(".jfh-menu > .menu"); 6const backdrop = document.querySelector(".menu-backdrop"); 7const mobileQuery = window.matchMedia("(max-width: 760px)"); 8const revealButton = document.querySelector(".menu-reveal"); 9const desktopSearchToggle = document.querySelector(".desktop-search-toggle"); 10const desktopSearchPanel = document.querySelector(".desktop-search-panel"); 11const desktopSearchForm = document.querySelector(".desktop-search-panel"); 12const mobileOpenContexts = new WeakMap(); 13 14function syncState(input) { 15 const label = document.querySelector(`label[for="${input.id}"]`); 16 if (label) label.setAttribute("aria-expanded", String(input.checked)); 17 if (input === mainToggle) { 18 nav.classList.toggle("menu-is-open", input.checked); 19 menuLabel.setAttribute("aria-label", input.checked ? "Close main menu" : "Open main menu"); 20 } 21} 22 23menuLabel.setAttribute("role", "button"); 24menuLabel.setAttribute("tabindex", "0"); 25menuLabel.setAttribute("aria-expanded", "false"); 26menuLabel.setAttribute("aria-controls", "jfh-menu-list"); 27document.querySelector(".jfh-menu > .menu").id = "jfh-menu-list"; 28 29function directChild(element, selector) { 30 return [...element.children].find((child) => child.matches(selector)); 31} 32 33const mediumMoreSections = ["Ministry", "Podcast", "Videos", "Features", "Extras"]; 34 35function cloneDesktopMenuList(sourceList) { 36 const clonedList = document.createElement("ul"); 37 [...sourceList.children].forEach((sourceItem) => { 38 const sourceLink = directChild(sourceItem, "a"); 39 if (!sourceLink) return; 40 const item = document.createElement("li"); 41 item.appendChild(sourceLink.cloneNode(true)); 42 const nestedList = directChild(sourceItem, "ul"); 43 if (nestedList) item.appendChild(cloneDesktopMenuList(nestedList)); 44 clonedList.appendChild(item); 45 }); 46 return clonedList; 47} 48 49const mediumMoreItem = document.createElement("li"); 50const mediumMoreLink = document.createElement("a"); 51const mediumMoreMenu = document.createElement("ul"); 52mediumMoreItem.className = "mid-more-item"; 53mediumMoreLink.href = "#"; 54mediumMoreLink.textContent = "More"; 55mediumMoreLink.setAttribute("aria-expanded", "false"); 56mediumMoreSections.forEach((label) => { 57 const sourceItem = [...menu.children].find((item) => { 58 const link = directChild(item, "a"); 59 return link && link.textContent.trim() === label; 60 }); 61 if (!sourceItem) return; 62 const item = document.createElement("li"); 63 item.appendChild(directChild(sourceItem, "a").cloneNode(true)); 64 const sourceList = directChild(sourceItem, "ul"); 65 if (sourceList) item.appendChild(cloneDesktopMenuList(sourceList)); 66 mediumMoreMenu.appendChild(item); 67}); 68mediumMoreItem.append(mediumMoreLink, mediumMoreMenu); 69menu.insertBefore(mediumMoreItem, document.querySelector(".desktop-search-item")); 70mediumMoreLink.addEventListener("click", (event) => { 71 event.preventDefault(); 72 const opening = !mediumMoreItem.classList.contains("is-hover-open"); 73 mediumMoreItem.classList.toggle("is-hover-open", opening); 74 mediumMoreLink.setAttribute("aria-expanded", String(opening)); 75}); 76document.addEventListener("click", (event) => { 77 if (mediumMoreItem.contains(event.target)) return; 78 mediumMoreItem.classList.remove("is-hover-open"); 79 mediumMoreLink.setAttribute("aria-expanded", "false"); 80}); 81 82function rememberMobileSectionPosition(input, label) { 83 if (!mobileQuery.matches || input.checked) return; 84 mobileOpenContexts.set(input, { 85 scrollTop: menu.scrollTop, 86 labelTop: label.getBoundingClientRect().top 87 }); 88} 89 90function revealOpenedMobileSection(input) { 91 if (!mobileQuery.matches || !input.checked) return; 92 requestAnimationFrame(() => requestAnimationFrame(() => { 93 const item = input.parentElement; 94 const label = directChild(item, `label[for="${input.id}"]`); 95 const submenu = directChild(item, "ul"); 96 if (!label || !submenu) return; 97 98 const context = mobileOpenContexts.get(input); 99 const maximumScroll = Math.max(0, menu.scrollHeight - menu.clientHeight); 100 if (context) { 101 const positionShift = label.getBoundingClientRect().top - context.labelTop; 102 menu.scrollTop = Math.min(maximumScroll, Math.max(0, context.scrollTop + positionShift)); 103 mobileOpenContexts.delete(input); 104 } 105 106 requestAnimationFrame(() => { 107 const menuRect = menu.getBoundingClientRect(); 108 const labelRect = label.getBoundingClientRect(); 109 const submenuRect = submenu.getBoundingClientRect();
110 const viewportBottom = window.visualViewport ? window.visualViewport.height : window.innerHeight; 111 const visibleBottom = Math.min(menuRect.bottom, viewportBottom) - 24; 112 const desiredBottom = Math.min(submenuRect.bottom, labelRect.bottom + 132); 113 const neededNudge = Math.max(0, desiredBottom - visibleBottom); 114 const safeNudge = Math.min(neededNudge, Math.max(0, labelRect.top - menuRect.top - 6)); 115 if (safeNudge < 2) return; 116 const currentMaximum = Math.max(0, menu.scrollHeight - menu.clientHeight); 117 menu.scrollTop = Math.min(currentMaximum, menu.scrollTop + safeNudge); 118 }); 119 })); 120} 121 122submenuInputs.forEach((input) => { 123 const item = input.parentElement; 124 const label = directChild(item, `label[for="${input.id}"]`); 125 const link = directChild(item, "a"); 126 const submenu = directChild(item, "ul"); 127 if (!label || !link || !submenu) return; 128 const categoryName = link.textContent.trim(); 129 const homeItem = document.createElement("li"); 130 const homeLink = document.createElement("a"); 131 homeItem.className = "mobile-category-home"; 132 homeLink.href = link.getAttribute("href"); 133 homeLink.textContent = `${categoryName} Home`; 134 homeItem.appendChild(homeLink); 135 submenu.insertBefore(homeItem, submenu.firstChild); 136 item.classList.add("has-mobile-submenu"); 137 link.classList.add("mobile-category-link"); 138 link.setAttribute("aria-label", `Visit ${categoryName}`); 139 label.textContent = categoryName; 140 label.setAttribute("role", "button"); 141 label.setAttribute("tabindex", "0"); 142 label.addEventListener("click", () => rememberMobileSectionPosition(input, label)); 143 label.addEventListener("keydown", (event) => { 144 if (event.key === "Enter" || event.key === " ") { 145 event.preventDefault(); 146 rememberMobileSectionPosition(input, label); 147 input.checked = !input.checked; 148 input.dispatchEvent(new Event("change")); 149 } 150 }); 151}); 152 153[mainToggle, ...submenuInputs].forEach((input) => { 154 syncState(input); 155 input.addEventListener("change", () => { 156 syncState(input); 157 if (input === mainToggle) { 158 backdrop.hidden = !input.checked; 159 backdrop.classList.toggle("is-open", input.checked); 160 document.body.style.overflow = input.checked && mobileQuery.matches ? "hidden" : ""; 161 } 162 if (input !== mainToggle && input.checked) { 163 const siblings = [...input.closest("ul").querySelectorAll(":scope > li > input[id^='drop-']")]; 164 siblings.forEach((sibling) => { 165 if (sibling !== input) { 166 sibling.checked = false; 167 syncState(sibling); 168 } 169 }); 170 revealOpenedMobileSection(input); 171 } 172 }); 173}); 174 175menuLabel.addEventListener("click", () => { 176 requestAnimationFrame(() => { 177 syncState(mainToggle); 178 backdrop.hidden = !mainToggle.checked; 179 backdrop.classList.toggle("is-open", mainToggle.checked); 180 document.body.style.overflow = mainToggle.checked && mobileQuery.matches ? "hidden" : ""; 181 }); 182}); 183 184function closeMobileMenu(returnFocus = false) { 185 mainToggle.checked = false; 186 syncState(mainToggle); 187 submenuInputs.forEach((input) => { 188 input.checked = false; 189 syncState(input); 190 }); 191 backdrop.hidden = true; 192 backdrop.classList.remove("is-open"); 193 document.body.style.overflow = ""; 194 if (returnFocus) menuLabel.focus(); 195} 196 197backdrop.addEventListener("click", () => closeMobileMenu(true)); 198menu.addEventListener("click", (event) => { 199 if (mobileQuery.matches && event.target.closest("a")) closeMobileMenu(false); 200}); 201 202const searchForm = document.querySelector(".mobile-menu-search"); 203 204function openSearchResults(query) { 205 const resultsUrl = new URL(window.location.href); 206 resultsUrl.searchParams.set("q", query); 207 resultsUrl.hash = ""; 208 window.location.href = resultsUrl.toString(); 209} 210 211document.addEventListener("click", (event) => { 212 const resultLink = event.target.closest(".gsc-results a[target='_blank']"); 213 if (resultLink) resultLink.removeAttribute("target"); 214}, true); 215 216searchForm.addEventListener("submit", (event) => { 217 event.preventDefault(); 218 const query = searchForm.querySelector("input").value.trim(); 219 if (query) openSearchResults(query); 220}); 221 222desktopSearchToggle.addEventListener("click", () => { 223 const opening = !desktopSearchPanel.classList.contains("is-open"); 224 desktopSearchPanel.classList.toggle("is-open", opening); 225 desktopSearchToggle.setAttribute("aria-expanded", String(opening)); 226 if (opening) desktopSearchPanel.querySelector("input").focus(); 227}); 228desktopSearchForm.addEventListener("submit", (event) => { 229 event.preventDefault(); 230 const query = desktopSearchForm.querySelector("input").value.trim(); 231 if (query) openSearchResults(query); 232}); 233menuLabel.addEventListener("keydown", (event) => { 234 if (event.key === "Enter" || event.key === " ") { 235 event.preventDefault(); 236 mainToggle.checked = !mainToggle.checked; 237 mainToggle.dispatchEvent(new Event("change")); 238 } 239}); 240 241document.addEventListener("keydown", (event) => { 242 if (event.key !== "Escape") return; 243 mediumMoreItem.classList.remove("is-hover-open"); 244 mediumMoreLink.setAttribute("aria-expanded", "false"); 245 desktopSearchPanel.classList.remove("is-open"); 246 desktopSearchToggle.setAttribute("aria-expanded", "false"); 247 closeMobileMenu(true); 248}); 249 250window.matchMedia("(min-width: 761px)").addEventListener("change", (event) => { 251 if (event.matches) { 252 mainToggle.checked = false;
253 syncState(mainToggle); 254 backdrop.hidden = true; 255 backdrop.classList.remove("is-open"); 256 document.body.style.overflow = ""; 257 } 258}); 259 260const sectionRules = [ 261 ["Reviews", /\/cdreviews\//], 262 ["Indies", /\/(indiemusic|indies)\//], 263 ["Interviews", /\/interviews\//], 264 ["Lyrics", /\/lyrics\//], 265 ["Ministry", /\/(ministry|devotionals|prayers)/], 266 ["Movies", /\/movies\//], 267 ["Podcast", /\/podcasts\//], 268 ["Videos", /\/videoclips\//], 269 ["Features", /\/features\//], 270 ["Extras", /\/(extras|newsletter|contests|downloads|about|contactus|donate)/], 271 ["Artists", /\/artists\//], 272 ["Music", /\/(music|releases|news|concerts|listeningparty|jfhblog)/] 273]; 274const livePath = location.protocol === "file:" ? "/default.asp" : location.pathname.toLowerCase(); 275let currentLabel = "Home"; 276for (const [label, pattern] of sectionRules) { 277 if (pattern.test(livePath)) { 278 currentLabel = label; 279 break; 280 } 281} 282if (mediumMoreSections.includes(currentLabel)) { 283 mediumMoreItem.classList.add("is-current"); 284} 285const topItems = [...document.querySelectorAll(".jfh-menu > .menu > li:not(.mobile-search-item)")]; 286topItems.forEach((item) => { 287 const link = directChild(item, "a"); 288 const label = directChild(item, "label"); 289 const text = (link?.textContent || label?.textContent || "").trim(); 290 if (text.startsWith(currentLabel)) { 291 item.classList.add("is-current"); 292 if (link) link.setAttribute("aria-current", "page"); 293 } 294}); 295 296const hoverTimers = new WeakMap(); 297document.querySelectorAll(".jfh-menu li").forEach((item) => { 298 if (!directChild(item, "ul")) return; 299 item.addEventListener("mouseenter", () => { 300 if (mobileQuery.matches) return; 301 [...item.parentElement.children].forEach((sibling) => { 302 if (sibling === item) return; 303 clearTimeout(hoverTimers.get(sibling)); 304 sibling.classList.remove("is-hover-open"); 305 }); 306 clearTimeout(hoverTimers.get(item)); 307 item.classList.add("is-hover-open"); 308 }); 309 item.addEventListener("mouseleave", () => { 310 if (mobileQuery.matches) return; 311 hoverTimers.set(item, setTimeout(() => item.classList.remove("is-hover-open"), 240)); 312 }); 313}); 314 315document.querySelectorAll(".jfh-menu > .menu > li > ul").forEach((dropdown) => { 316 const hasSideFlyout = [...dropdown.children].some((item) => directChild(item, "ul")); 317 if (!hasSideFlyout) dropdown.classList.add("can-scroll"); 318}); 319 320let previousScroll = window.scrollY; 321let accumulatedDelta = 0; 322let revealGraceUntil = 0; 323revealButton.addEventListener("click", () => { 324 nav.classList.remove("is-hidden"); 325 revealButton.classList.remove("is-visible"); 326 revealGraceUntil = Date.now() + 1800; 327 menuLabel.focus(); 328}); 329window.addEventListener("scroll", () => { 330 const currentScroll = window.scrollY; 331 const delta = currentScroll - previousScroll; 332 accumulatedDelta = Math.sign(delta) === Math.sign(accumulatedDelta) ? accumulatedDelta + delta : delta; 333 const menuIsOpen = mainToggle.checked || desktopSearchPanel.classList.contains("is-open") || document.querySelector(".jfh-menu .is-hover-open"); 334 if (!menuIsOpen && Date.now() > revealGraceUntil && currentScroll > 180 && accumulatedDelta > 24) { 335 nav.classList.add("is-hidden"); 336 revealButton.classList.add("is-visible"); 337 accumulatedDelta = 0; 338 } else if (accumulatedDelta < -14 || currentScroll < 80) { 339 nav.classList.remove("is-hidden"); 340 revealButton.classList.remove("is-visible"); 341 accumulatedDelta = 0; 342 } 343 previousScroll = currentScroll; 344}, { passive: true });
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.