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.