PageSourceSearch

https://ameripharmaspecialty.com/wp-content/plugins/gridxflex-announcement-bars/assets/js/public.js?ver=1.1.0

js ameripharmaspecialty.com collected 2026-10-01 15:33:55 UTC 15,251 bytes, 438 lines download raw bytes

1/**
2 * Gridxflex Announcement Bars with CTA - Frontend JavaScript
3 * v1.1.0
4 */
5
6(function () {
7  "use strict";
8
9  class GABC_Stack {
10    constructor() {
11      this.cookiePrefix = "gabc_dismissed_";
12      this.cookieExpiration = 30;
13      this.topBars = [];
14      this.bottomBars = []; // sticky bottom — pushes body padding
15      this.bottomBarsFixed = []; // non-sticky bottom — fixed but no body padding
16      this.adminBarHeight = 0;
17
18      // Trigger state
19      this._scrollListenerBound = null;
20      this._exitListenerBound = null;
21      this._pendingTriggerBars = []; // bars waiting for a trigger to fire
22      this._animOnLoad = []; // non-triggered bars with entrance animation
23    }
24
25    init() {
26      this.setupEventListeners();
27      this.detectAdminBar();
28      this.collectBars();
29
30      if (
31        this.topBars.length === 0 &&
32        this.bottomBars.length === 0 &&
33        this.bottomBarsFixed.length === 0 &&
34        this._pendingTriggerBars.length === 0
35      ) {
36        return;
37      }
38
39      this.stackBars();
40      this.updateBodyPadding();
41      this.setupTriggers();
42
43      // Play entrance animations for non-triggered bars after layout settles.
44      if (this._animOnLoad.length > 0) {
45        const barsToAnimate = this._animOnLoad.slice();
46        this._animOnLoad = [];
47        requestAnimationFrame(() => {
48          requestAnimationFrame(() => {
49            barsToAnimate.forEach((bar) => this._playEntrance(bar));
50          });
51        });
52      }
53
54      window.addEventListener("resize", () => {
55        this.detectAdminBar();
56        this.stackBars();
57        this.updateBodyPadding();
58      });
59    }
60
61    // ─── Event listeners (close button) ─────────────────────────────────────
62
63    setupEventListeners() {
64      document.addEventListener("click", (e) => {
65        const closeBtn = e.target.closest(".gabc-notice-bar__close");
66        if (closeBtn) {
67          e.preventDefault();
68          const bar = closeBtn.closest(".gabc-notice-bar");
69          if (bar) this.dismiss(bar);
70        }
71
72        // CTA button click tracking.
73        const ctaBtn = e.target.closest(".gabc-notice-bar__button");
74        if (ctaBtn) {
75          const bar = ctaBtn.closest(".gabc-notice-bar");
76          if (bar) {
77            const noticeId = bar.getAttribute("data-notice-id");
78            if (noticeId) this.trackClick(noticeId);
79          }
80        }
81      });
82
83      document.addEventListener("keydown", (e) => {
84        const closeBtn = e.target.closest(".gabc-notice-bar__close");
85        if (closeBtn && (e.key === "Enter" || e.key === " ")) {
86          e.preventDefault();
87          const bar = closeBtn.closest(".gabc-notice-bar");
88          if (bar) this.dismiss(bar);
89        }
90      });
91    }
92
93    // ─── Admin bar ───────────────────────────────────────────────────────────
94
95    detectAdminBar() {
96      const adminBar = document.getElementById("wpadminbar");
97      this.adminBarHeight = adminBar ? adminBar.offsetHeight : 0;
98    }
99
100    // ─── Collect bars ────────────────────────────────────────────────────────
101
102    collectBars() {
103      const bars = document.querySelectorAll(".gabc-notice-bar");
104
105      bars.forEach((bar) => {
106        const noticeId = bar.getAttribute("data-notice-id");
107        const dismissible = bar.getAttribute("data-dismissible") === "true";
108
109        if (dismissible && this.isDismissed(noticeId)) {
110          bar.style.display = "none";
111          return;
112        }
113
114        // If the bar has a trigger, keep it hidden and queue it.
115        if (bar.classList.contains("gabc-trigger-hidden")) {
116          this._pendingTriggerBars.push(bar);
117          return; // not added to topBars/bottomBars yet
118        }
119
120        bar.style.display = "";
121
122        const sticky = bar.getAttribute("data-sticky") === "true";
123        const position = bar.getAttribute("data-position");
124
125        // Queue entrance animation for non-triggered animated bars.
126        if (bar.classList.contains("gabc-anim-ready")) {
127          this._animOnLoad.push(bar);
128        }
129
130        if (position === "top" && sticky) {
131          this.topBars.push(bar);
132        } else if (position === "bottom" && sticky) {
133          this.bottomBars.push(bar);
134        } else if (position === "bottom" && !sticky) {
135          this.bottomBarsFixed.push(bar);
136        }
137        // non-sticky top: rendered inline in flow, no stacking needed
138      });
139    }
140
141    // ─── Trigger setup ───────────────────────────────────────────────────────
142
143    setupTriggers() {
144      if (this._pendingTriggerBars.length === 0) return;
145
146      let needsScroll = false;
147      let needsExitIntent = false;
148
149      this._pendingTriggerBars.forEach((bar) => {
150        const delay = parseInt(bar.getAttribute("data-trigger-delay"), 10) || 0;
151        const scroll =
152          parseInt(bar.getAttribute("data-trigger-scroll"), 10) || 0;
153        const exitIntent =
154          bar.getAttribute("data-trigger-exit-intent") === "true";
155
156        // Priority: exit intent > scroll > delay
157        // Each bar only needs one trigger to fire.
158
159        if (exitIntent) {
160          needsExitIntent = true;
161          bar._triggerMode = "exit";
162        } else if (scroll > 0) {
163          needsScroll = true;
164          bar._triggerMode = "scroll";
165          bar._triggerScrollPct = scroll;
166        } else {
167          // delay (0 = immediate on next tick, >0 = after N seconds)
168          bar._triggerMode = "delay";
169          bar._triggerDelay = delay;
170          setTimeout(() => this.revealBar(bar), delay * 1000);
171        }
172      });
173
174      if (needsScroll) {
175        this._scrollListenerBound = this._onScroll.bind(this);
176        window.addEventListener("scroll", this._scrollListenerBound, {
177          passive: true,
178        });
179        // Check once immediately in case the page loaded already scrolled.
180        this._onScroll();
181      }
182
183      if (needsExitIntent) {
184        this._exitListenerBound = this._onExitIntent.bind(this);
185        document.addEventListener("mouseleave", this._exitListenerBound);
186      }
187    }
188
189    _onScroll() {
190      const scrollTop = window.scrollY || document.documentElement.scrollTop;
191      const docHeight =
192        document.documentElement.scrollHeight - window.innerHeight;
193      const scrolledPct = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0;
194
195      const remaining = [];
196      this._pendingTriggerBars.forEach((bar) => {
197        if (bar._triggerMode !== "scroll") {
198          remaining.push(bar);
199          return;
200        }
201        if (scrolledPct >= bar._triggerScrollPct) {
202          this.revealBar(bar);
203          // Don't push back to remaining — it's been revealed.
204        } else {
205          remaining.push(bar);
206        }
207      });
208
209      this._pendingTriggerBars = remaining;
210
211      // Clean up listener once all scroll-triggered bars have fired.
212      if (!this._pendingTriggerBars.some((b) => b._triggerMode === "scroll")) {
213        if (this._scrollListenerBound) {
214          window.removeEventListener("scroll", this._scrollListenerBound);
215          this._scrollListenerBound = null;
216        }
217      }
218    }
219
220    _onExitIntent(e) {
221      // Only fire when cursor leaves through the top of the viewport.
222      if (e.clientY > 10) return;
223
224      const toReveal = this._pendingTriggerBars.filter(
225        (b) => b._triggerMode === "exit",
226      );
227      toReveal.forEach((bar) => this.revealBar(bar));
228
229      this._pendingTriggerBars = this._pendingTriggerBars.filter(
230        (b) => b._triggerMode !== "exit",
231      );
232
233      // Remove listener — exit intent fires once per page load.
234      if (this._exitListenerBound) {
235        document.removeEventListener("mouseleave", this._exitListenerBound);
236        this._exitListenerBound = null;
237      }
238    }
239
240    // ─── Reveal a triggered bar ──────────────────────────────────────────────
241
242    revealBar(bar) {
243      const anim = bar.getAttribute("data-animation");
244
245      if (anim && anim !== "none") {
246        this._playEntrance(bar);
247      } else {
248        bar.classList.remove("gabc-trigger-hidden");
249        bar.classList.add("gabc-trigger-visible");
250      }
251
252      const sticky = bar.getAttribute("data-sticky") === "true";
253      const position = bar.getAttribute("data-position");
254
255      if (position === "top" && sticky) {
256        this.topBars.push(bar);
257        this.stackBars();
258        this.updateBodyPadding();
259      } else if (position === "bottom" && sticky) {
260        this.bottomBars.push(bar);
261        this.stackBars();
262        this.updateBodyPadding();
263      } else if (position === "bottom" && !sticky) {
264        this.bottomBarsFixed.push(bar);
265        this.stackBars();
266        this.updateBodyPadding(); // FIX: non-sticky bottom also needs padding update
267      }
268    }
269
270    _playEntrance(bar) {
271      const duration =
272        parseInt(bar.getAttribute("data-animation-duration"), 10) || 400;
273
274      // Set animation-duration directly — more reliable than CSS variable
275      bar.style.animationDuration = duration + "ms";
276
277      bar.classList.add("gabc-entrance");
278      bar.classList.remove("gabc-anim-ready");
279      bar.classList.remove("gabc-trigger-hidden");
280      bar.style.pointerEvents = "";
281
282      const cleanup = () => {
283        bar.classList.remove("gabc-entrance");
284        bar.style.opacity = "1";
285        bar.style.transform = "";
286        bar.style.animationDuration = ""; // clean up
287      };
288
289      bar.addEventListener("animationend", cleanup, { once: true });
290      setTimeout(cleanup, duration + 100);
291    }
292
293    // ─── Stacking & padding ──────────────────────────────────────────────────
294
295    stackBars() {
296      let topOffset = this.adminBarHeight;
297      this.topBars.forEach((bar) => {
298        bar.style.top = topOffset + "px";
299        topOffset += bar.offsetHeight;
300      });
301
302      // Sticky bottom bars — stacked from the bottom edge upward.
303      let bottomOffset = 0;
304      this.bottomBars.forEach((bar) => {
305        bar.style.bottom = bottomOffset + "px";
306        bottomOffset += bar.offsetHeight;
307      });
308
309      // Non-sticky bottom bars — also fixed at bottom, stacked above sticky ones.
310      this.bottomBarsFixed.forEach((bar) => {
311        bar.style.bottom = bottomOffset + "px";
312        bottomOffset += bar.offsetHeight;
313      });
314    }
315
316    updateBodyPadding() {
317      let totalTop = this.adminBarHeight;
318      let totalBottom = 0;
319
320      this.topBars.forEach((bar) => (totalTop += bar.offsetHeight));
321
322      // FIX: Both sticky and non-sticky bottom bars are position:fixed at the
323      // bottom of the viewport, so both must contribute to body padding-bottom.
324      // Without this, fixed bottom bars overlap and hide the page footer.
325      this.bottomBars.forEach((bar) => (totalBottom += bar.offsetHeight));
326      this.bottomBarsFixed.forEach((bar) => (totalBottom += bar.offsetHeight));
327
328      document.body.style.setProperty("--gabc-top-bar-height", totalTop + "px");
329      document.body.style.setProperty(
330        "--gabc-bottom-bar-height",
331        totalBottom + "px",
332      );
333
334      if (totalTop > this.adminBarHeight) {
335        document.body.classList.add("gabc-has-top-bar");
336      } else {
337        document.body.classList.remove("gabc-has-top-bar");
338      }
339
340      if (totalBottom > 0) {
341        document.body.classList.add("gabc-has-bottom-bar");
342      } else {
343        document.body.classList.remove("gabc-has-bottom-bar");
344      }
345    }
346
347    // ─── Dismiss ─────────────────────────────────────────────────────────────
348
349    dismiss(bar) {
350      const noticeId = bar.getAttribute("data-notice-id");
351      const dismissible = bar.getAttribute("data-dismissible") === "true";
352
353      if (!dismissible || !noticeId) return;
354
355      const sticky = bar.getAttribute("data-sticky") === "true";
356      const position = bar.getAttribute("data-position");
357
358      bar.classList.add("gabc-sliding-out");
359
360      setTimeout(() => {
361        bar.style.display = "none";
362        this.setCookie(noticeId);
363
364        if (position === "top" && sticky) {
365          const idx = this.topBars.indexOf(bar);
366          if (idx !== -1) this.topBars.splice(idx, 1);
367          this.stackBars();
368          this.updateBodyPadding();
369        } else if (position === "bottom" && sticky) {
370          const idx = this.bottomBars.indexOf(bar);
371          if (idx !== -1) this.bottomBars.splice(idx, 1);
372          this.stackBars();
373          this.updateBodyPadding();
374        } else if (position === "bottom" && !sticky) {
375          const idx = this.bottomBarsFixed.indexOf(bar);
376          if (idx !== -1) this.bottomBarsFixed.splice(idx, 1);
377          this.stackBars();
378          this.updateBodyPadding(); // FIX: recalculate padding after dismiss too
379        } else {
380          // non-sticky top: remove from DOM
381          bar.remove();
382        }
383      }, 300);
384    }
385
386    // ─── Click tracking ──────────────────────────────────────────────────────
387
388    trackClick(noticeId) {
389      if (!window.gabcData || !window.gabcData.nonce) return;
390
391      const data = new FormData();
392      data.append("action", "gabc_track_click");
393      data.append("nonce", window.gabcData.nonce);
394      data.append("notice_id", noticeId);
395
396      // Use sendBeacon when available so the request survives page navigation
397      // (e.g. button opens link in same tab).
398      const url = window.gabcData.ajaxurl || "/wp-admin/admin-ajax.php";
399      if (navigator.sendBeacon) {
400        navigator.sendBeacon(url, data);
401      } else {
402        fetch(url, { method: "POST", body: data, keepalive: true }).catch(
403          () => {},
404        );
405      }
406    }
407
408    // ─── Cookie helpers ──────────────────────────────────────────────────────
409
410    isDismissed(noticeId) {
411      const cookieName = this.cookiePrefix + noticeId;
412      return document.cookie.split(";").some((c) => {
413        return c.trim().startsWith(cookieName + "=");
414      });
415    }
416
417    setCookie(noticeId) {
418      const date = new Date();
419      date.setTime(
420        date.getTime() + this.cookieExpiration * 24 * 60 * 60 * 1000,
421      );
422      document.cookie =
423        this.cookiePrefix +
424        noticeId +
425        "=true; expires=" +
426        date.toUTCString() +
427        "; path=/";
428    }
429  }
430
431  if (document.readyState === "loading") {
432    document.addEventListener("DOMContentLoaded", () =>
433      new GABC_Stack().init(),
434    );
435  } else {
436    new GABC_Stack().init();
437  }
438})();

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.