1(() => { 2 "use strict"; 3 4 const categoryNav = document.querySelector("[data-category-nav]"); 5 6 if (categoryNav instanceof HTMLElement) { 7 const strip = categoryNav.querySelector("[data-category-strip]"); 8 const leftButton = categoryNav.querySelector("[data-category-scroll-left]"); 9 const rightButton = categoryNav.querySelector("[data-category-scroll-right]"); 10 const leftFade = categoryNav.querySelector("[data-category-fade-left]"); 11 const rightFade = categoryNav.querySelector("[data-category-fade-right]"); 12 const moreDetails = categoryNav.querySelector("[data-category-more]"); 13 const moreMenu = categoryNav.querySelector("[data-category-more-menu]"); 14 const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; 15 let categoryFrame = 0; 16 17 const setArrowAvailability = (button, available) => { 18 if (!(button instanceof HTMLButtonElement)) { 19 return; 20 } 21 22 button.hidden = false; 23 button.disabled = !available; 24 button.tabIndex = available ? 0 : -1; 25 button.setAttribute("aria-hidden", available ? "false" : "true"); 26 button.classList.toggle("opacity-0", !available); 27 button.classList.toggle("pointer-events-none", !available); 28 button.classList.toggle("opacity-100", available); 29 button.classList.toggle("pointer-events-auto", available); 30 }; 31 32 const positionMoreMenu = () => { 33 if ( 34 !(moreDetails instanceof HTMLDetailsElement) || 35 !(moreMenu instanceof HTMLElement) || 36 !moreDetails.open 37 ) { 38 moreMenu?.classList.add("invisible"); 39 return; 40 } 41 42 const trigger = moreDetails.querySelector("summary"); 43 44 if (!(trigger instanceof HTMLElement)) { 45 return; 46 } 47 48 const triggerRect = trigger.getBoundingClientRect(); 49 const menuWidth = moreMenu.offsetWidth || 240; 50 const left = Math.max(8, Math.min(triggerRect.left, window.innerWidth - menuWidth - 8)); 51 const belowTop = triggerRect.bottom + 8; 52 const menuHeight = moreMenu.offsetHeight; 53 const top = belowTop + menuHeight <= window.innerHeight - 8 54 ? belowTop 55 : Math.max(8, triggerRect.top - menuHeight - 8); 56 57 moreMenu.style.left = `${left}px`; 58 moreMenu.style.top = `${top}px`; 59 moreMenu.classList.remove("invisible"); 60 }; 61 62 const updateCategoryNav = () => { 63 categoryFrame = 0; 64 65 if (!(strip instanceof HTMLElement)) { 66 return; 67 } 68 69 const maximumScroll = Math.max(0, strip.scrollWidth - strip.clientWidth); 70 const hasOverflow = maximumScroll > 2; 71 const atStart = strip.scrollLeft <= 2; 72 const atEnd = strip.scrollLeft >= maximumScroll - 2; 73 const showDesktopArrows = window.matchMedia("(min-width: 768px)").matches; 74 75 setArrowAvailability(leftButton, showDesktopArrows && hasOverflow && !atStart); 76 setArrowAvailability(rightButton, showDesktopArrows && hasOverflow && !atEnd); 77 78 if (leftFade instanceof HTMLElement) { 79 leftFade.hidden = !hasOverflow || atStart; 80 } 81 82 if (rightFade instanceof HTMLElement) { 83 rightFade.hidden = !hasOverflow || atEnd; 84 } 85 86 positionMoreMenu(); 87 }; 88 89 const scheduleCategoryUpdate = () => { 90 if (categoryFrame === 0) { 91 categoryFrame = window.requestAnimationFrame(updateCategoryNav); 92 } 93 }; 94 95 const scrollCategories = (direction) => { 96 if (strip instanceof HTMLElement) { 97 strip.scrollBy({ 98 left: strip.clientWidth * 0.7 * direction, 99 behavior: reducedMotion ? "auto" : "smooth", 100 }); 101 } 102 }; 103 104 leftButton?.addEventListener("click", () => scrollCategories(-1)); 105 rightButton?.addEventListener("click", () => scrollCategories(1)); 106 strip?.addEventListener("scroll", scheduleCategoryUpdate, { passive: true }); 107 window.addEventListener("resize", scheduleCategoryUpdate, { passive: true }); 108 109 if ("ResizeObserver" in window && strip instanceof HTMLElement) { 110 const categoryResizeObserver = new ResizeObserver(scheduleCategoryUpdate); 111 categoryResizeObserver.observe(strip); 112 } 113 114 moreDetails?.addEventListener("toggle", () => { 115 window.requestAnimationFrame(positionMoreMenu); 116 }); 117 118 document.addEventListener("pointerdown", (event) => { 119 if ( 120 moreDetails instanceof HTMLDetailsElement && 121 moreDetails.open && 122 event.target instanceof Node && 123 !moreDetails.contains(event.target) 124 ) { 125 moreDetails.open = false;
126 } 127 }); 128 129 moreDetails?.addEventListener("keydown", (event) => { 130 if (event.key === "Escape" && moreDetails instanceof HTMLDetailsElement) { 131 moreDetails.open = false; 132 moreDetails.querySelector("summary")?.focus(); 133 } 134 }); 135 136 const activePrimaryCategory = strip?.querySelector(':scope > a[aria-current="page"]'); 137 const activeCategory = activePrimaryCategory instanceof HTMLAnchorElement 138 ? activePrimaryCategory 139 : moreDetails instanceof HTMLDetailsElement && moreDetails.querySelector('[aria-current="page"]') 140 ? moreDetails 141 : null; 142 143 if (activeCategory instanceof HTMLElement) { 144 window.requestAnimationFrame(() => { 145 activeCategory.scrollIntoView({ behavior: "auto", block: "nearest", inline: "center" }); 146 scheduleCategoryUpdate(); 147 }); 148 } else { 149 scheduleCategoryUpdate(); 150 } 151 } 152 153 const grid = document.querySelector("[data-gift-card-grid]"); 154 const loadMore = document.querySelector("[data-load-more]"); 155 const loadM
155oreWrap = document.querySelector("[data-load-more-wrap]"); 156 const loadMoreCount = document.querySelector("[data-load-more-count]"); 157 const loadMoreStatus = document.querySelector("[data-load-more-status]"); 158 const loadMoreError = document.querySelector("[data-load-more-error]"); 159 160 if (!(grid instanceof HTMLElement) || !(loadMore instanceof HTMLButtonElement)) { 161 return; 162 } 163 164 let isLoading = false; 165 let nextOffset = Number.parseInt(grid.dataset.nextOffset || "0", 10); 166 let loadedCount = Number.parseInt(grid.dataset.loadedCount || "0", 10); 167 const totalCount = Number.parseInt(grid.dataset.totalCount || "0", 10); 168 const endpoint = grid.dataset.endpoint || "/allstores.php"; 169 const category = grid.dataset.category || ""; 170 const sort = grid.dataset.sort || "popular"; 171 const normalText = loadMore.textContent || "Load More Gift Cards"; 172 const loadedStoreIds = new Set( 173 Array.from(grid.querySelectorAll("[data-gift-card-card]")) 174 .map((card) => (card instanceof HTMLAnchorElement ? card.dataset.storeId || "" : "")) 175 .filter(Boolean) 176 ); 177 178 loadMore.disabled = false; 179 180 const setStatus = (message) => { 181 if (loadMoreStatus instanceof HTMLElement) { 182 loadMoreStatus.textContent = message; 183 } 184 }; 185 186 const updateCount = () => { 187 if (loadMoreCount instanceof HTMLElement && totalCount > 0) { 188 loadMoreCount.textContent = `Showing ${loadedCount.toLocaleString()} of ${totalCount.toLocaleString()}`; 189 } 190 }; 191 192 const setLoading = (loading) => { 193 isLoading = loading; 194 loadMore.disabled = loading; 195 loadMore.setAttribute("aria-busy", loading ? "true" : "false"); 196 grid.setAttribute("aria-busy", loading ? "true" : "false"); 197 loadMore.textContent = loading ? "Loading..." : normalText; 198 }; 199 200 loadMore.addEventListener("click", async () => { 201 if (isLoading) { 202 return; 203 } 204 205 setLoading(true); 206 207 if (loadMoreError instanceof HTMLElement) { 208 loadMoreError.hidden = true; 209 } 210 211 try { 212 const params = new URLSearchParams({ 213 load_more: "1", 214 offset: String(nextOffset), 215 sort, 216 }); 217 218 if (category !== "") { 219 params.set("category", category); 220 } 221 222 const response = await fetch(`${endpoint}?${params.toString()}`, { 223 headers: { Accept: "application/json" }, 224 }); 225 226 if (!response.ok) { 227 throw new Error("Unable to load more gift cards."); 228 } 229 230 const payload = await response.json(); 231 const template = document.createElement("template"); 232 template.innerHTML = typeof payload.html === "string" ? payload.html : ""; 233 const nextCards = Array.from(template.content.querySelectorAll("[data-gift-card-card]")); 234 let appendedCount = 0; 235 236 nextCards.forEach((card) => { 237 if (!(card instanceof HTMLAnchorElement)) { 238 return; 239 } 240 241 const storeId = card.dataset.storeId || card.href; 242 243 if (loadedStoreIds.has(storeId)) { 244 return; 245 } 246 247 loadedStoreIds.add(storeId); 248 grid.appendChild(document.importNode(card, true)); 249 appendedCount += 1; 250 }); 251 252 nextOffset = Number.isFinite(Number(payload.nextOffset)) ? Number(payload.nextOffset) : nextOffset + appendedCount; 253 loadedCount += appendedCount; 254 grid.dataset.nextOffset = String(nextOffset); 255 grid.dataset.loadedCount = String(loadedCount); 256 updateCount(); 257 258 if (appendedCount > 0) { 259 setStatus(`${appendedCount} more gift cards loaded.`); 260 } 261 262 if (payload.hasMore === true) { 263 setLoading(false); 264 } else { 265 setLoading(false); 266 setStatus("All gift cards have been loaded."); 267 loadMoreWrap?.remove(); 268 } 269 } catch { 270 setLoading(false); 271 272 if (loadMoreError instanceof HTMLElement) { 273 loadMoreError.hidden = false; 274 } 275 276 setStatus("Unable to load more gift cards. Please try again."); 277 } 278 }); 279})();
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.