PageSourceSearch

https://cardbear.com/js/allstores2.js?v=1784627165

js cardbear.com collected 2026-09-24 12:38:50 UTC 9,469 bytes, 279 lines download raw bytes

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.