1/** 2 * Handles all sticky logic on the frontend: 3 * - Sticky Position : top | bottom | column 4 * - Sticky Until : stop at a CSS ID 5 * - Sticky Offset : px gap from viewport edge 6 * - Background/Shadow change when sticky activates 7 * - Device Control : skip sticky on specific breakpoints 8 * - Entrance Animation: fade | slide-down | slide-up 9 */ 10(function ($) { 11 "use strict"; 12 13 // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 14 // BREAKPOINT DETECTION 15 // Matches Elementor's default breakpoints 16 // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 17 var getDevice = function () { 18 var w = window.innerWidth; 19 if (w <= 767) return "mobile"; 20 if (w <= 1024) return "tablet"; 21 return "desktop"; 22 }; 23 24 // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 25 // ENTRANCE ANIMATION 26 // Called once when the element becomes sticky for the first time 27 // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 28 var playEntranceAnimation = function ($el, type, duration) { 29 if (!type || type === "none") return; 30 31 $el.css({ "animation-duration": duration + "ms" }); 32 $el.addClass("pea-sticky-anim-" + type); 33 34 // Remove class after animation ends so it can replay next time 35 setTimeout(function () { 36 $el.removeClass("pea-sticky-anim-" + type); 37 }, duration + 50); 38 }; 39 40 // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 41 // SINGLE ELEMENT STICKY HANDLER 42 // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 43 var PeaStickyHandler = function ($el) { 44 var raw = $el.attr("data-pea-sticky"); 45 if (!raw) return; 46 47 var cfg; 48 try { 49 cfg = JSON.parse(raw); 50 } catch (e) { 51 return; 52 } 53 54 if (!cfg || !cfg.enabled) return; 55 56 // Device check â bail if sticky is disabled for this breakpoint 57 var device = getDevice(); 58 if (device === "desktop" && !cfg.desktop) return; 59 if (device === "tablet" && !cfg.tablet) return; 60 if (device === "mobile" && !cfg.mobile) return; 61 62 var position = cfg.position || "top"; 63 var offset = parseInt(cfg.offset, 10) || 0; 64 var untilSel = cfg.until || ""; 65 var animation = cfg.animation || "none"; 66 var animDur = parseInt(cfg.animDuration, 10) || 400; 67 68 var bgEnable = !!cfg.bgEnable; 69 var bgColor = cfg.bgColor || "rgba(255,255,255,1)"; 70 var transition = parseInt(cfg.transition, 10) || 300; 71 var shadowEnable = !!cfg.shadowEnable; 72 var shadowColor = cfg.shadowColor || "rgba(0,0,0,0.12)"; 73 74 // ââ COLUMN MODE âââââââââââââââââââââââââââââââââââââââââââââââââââââââ 75 // Sticky within its parent container rather than the viewport. 76 // Uses position:sticky on the element itself. 77 if (position === "column") { 78 $el.css({ 79 position: "sticky", 80 top: offset + "px", 81 "z-index": 99, 82 }); 83 84 // Apply bg/shadow as permanent style for column mode 85 if (bgEnable) { 86 $el.css({ 87 "background-color": bgColor, 88 transition: 89 "background-color " + 90 transition + 91 "ms ease, box-shadow " + 92 transition + 93 "ms ease", 94 }); 95 if (shadowEnable) { 96 $el.css("box-shadow", "0 4px 24px " + shadowColor); 97 } 98 } 99 return; // Column mode requires no scroll listener 100 } 101 102 // ââ TOP / BOTTOM MODE âââââââââââââââââââââââââââââââââââââââââââââââââ 103 var $placeholder = null; // Reserve space when element is lifted out of flow 104 var isSticky = false;
105 var originalStyle = {}; // Store original inline styles for restore 106 107 // Snapshot of the element's natural layout position 108 var elNaturalTop = 0; 109 var elNaturalBottom = 0; 110 var elHeight = 0; 111 var elWidth = 0; 112 113 // Read natural position once before any scroll 114 var cacheNaturalMetrics = function () { 115 // Temporarily detach sticky so measurements are natural 116 $el.removeClass("pea-is-sticky"); 117 if ($placeholder) { 118 $placeholder.css("height", 0); 119 } 120 121 var rect = $el[0].getBoundingClientRect(); 122 var scrollY = window.pageYOffset; 123 elNaturalTop = rect.top + scrollY; 124 elNaturalBottom = rect.bottom + scrollY; 125 elHeight = $el.outerHeight(true); 126 elWidth = $el.outerWidth(); 127 }; 128 129 // Create a transparent placeholder div to hold the space 130 var ensurePlaceholder = function () { 131 if (!$placeholder) { 132 $placeholder = $( 133 '<div class="pea-sticky-placeholder" aria-hidden="true"></div>', 134 ); 135 $el.after($placeholder); 136 } 137 }; 138 139 var makeSticky = function () { 140 if (isSticky) return; 141 isSticky = true; 142 143 ensurePlaceholder(); 144 $placeholder.css({ 145 height: elHeight + "px", 146 width: elWidth + "px", 147 display: "block", 148 }); 149 150 $el.addClass("pea-is-sticky"); 151 152 var stickyCSS = { 153 position: "fixed", 154 "z-index": 9999, 155 width: elWidth + "px", 156 left: $placeholder.offset().left + "px", 157 }; 158 159 if (position === "top") { 160 stickyCSS.top = offset + "px"; 161 stickyCSS.bottom = "auto"; 162 } else { 163 stickyCSS.bottom = offset + "px"; 164 stickyCSS.top = "auto"; 165 } 166 167 // Background change 168 if (bgEnable) { 169 stickyCSS["background-color"] = bgColor; 170 stickyCSS["transition"] = 171 "background-color " + 172 transition + 173 "ms ease, box-shadow " + 174 transition + 175 "ms ease"; 176 if (shadowEnable) { 177 stickyCSS["box-shadow"] = "0 4px 24px " + shadowColor; 178 } 179 } 180 181 $el.css(stickyCSS); 182 183 // Entrance animation (play once per sticky activation) 184 playEntranceAnimation($el, animation, animDur); 185 }; 186 187 var makeUnSticky = function () { 188 if (!isSticky) return; 189 isSticky = false; 190 191 $el.removeClass("pea-is-sticky"); 192 $el.css({ 193 position: "", 194 top: "", 195 bottom: "", 196 left: "", 197 width: "", 198 "z-index": "", 199 "background-color": "", 200 "box-shadow": "", 201 transition: "", 202 }); 203 204 if ($placeholder) { 205 $placeholder.css({ height: 0, display: "none" }); 206 } 207 }; 208 209 // ââ SCROLL HANDLER ââââââââââââââââââââââââââââââââââââââââââââââââââââ 210 var onScroll = function () { 211 var scrollY = window.pageYOffset; 212 var vpHeight = window.innerHeight; 213 214 // ââ "Sticky Until" stop logic ââ 215 var shouldStop = false; 216 217 if (untilSel) { 218 var $stopEl = $(untilSel); 219 220 if ($stopEl.length && position === "top") { 221 var stopTop = $stopEl.offset().top; 222 223 shouldStop = scrollY + offset + elHeight >= stopTop; 224 } 225 } 226 227 // ââ Top position ââ 228 if (position === "top") { 229 if (scrollY + offset >= elNaturalTop && !shouldStop) { 230 makeSticky(); 231 } else { 232 makeUnSticky(); 233 } 234 return; 235 } 236 237 // ââ Bottom position ââ 238 if (position === "bottom") { 239 // Stick when the element's natural bottom is scrolled out of viewport 240 if (scrollY + vpHeight <= elNaturalBottom) { 241 makeSticky(); 242 } else { 243 makeUnSticky(); 244 } 245 } 246 }; 247 248 // ââ RESIZE HANDLER ââââââââââââââââââââââââââââââââââââââââââââââââââââ 249 var onResize = function () { 250 makeUnSticky(); 251 cacheNaturalMetrics(); 252 253 // Re-check device 254 var newDevice = getDevice(); 255 if (newDevice === "desktop" && !cfg.desktop) return; 256 if (newDevice === "tablet" && !cfg.tablet) return; 257 if (newDevice === "mobile" && !cfg.mobile) return; 258 259 onScroll(); 260 }; 261 262 // ââ INIT âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 263 cacheNaturalMetrics(); 264 265 // Use passive listeners for performance 266 window.addEventListener("scroll", onScroll, { passive: true }); 267 window.addEventListener("resize", debounce(onResize, 150), { 268 passive: true, 269 }); 270 271 // Run once on init in case page loads already scrolled 272 onScroll(); 273 }; 274 275 // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 276 // DEBOUNCE UTILITY 277 // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 278 var debounce = function (fn, delay) { 279 var timer; 280 return function () { 281 clearTimeout(timer); 282 timer = setTimeout(fn, delay); 283 }; 284 }; 285 286 // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 287 // ELEMENTOR FRONTEND INIT 288 // Init sticky on sections, columns, and containers 289 // âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 290 var initAllStickyElements = function ($scope) { 291 // The scope itself might be a sticky element 292 if ($scope.is("[data-pea-sticky]")) { 293 PeaStickyHandler($scope); 294 } 295 // Or it contains sticky elements 296 $scope.find("[data-pea-sticky]").each(function () { 297 PeaStickyHandler($(this)); 298 }); 299 }; 300 301 $(window).on("elementor/frontend/init", function () { 302 // Section 303 elementorFrontend.hooks.addAction( 304 "frontend/element_ready/section", 305 initAllStickyElements, 306 ); 307 // Column 308 elementorFrontend.hooks.addAction( 309 "frontend/element_ready/column", 310 initAllStickyElements, 311 ); 312 // Container (Flexbox / Grid) 313 elementorFrontend.hooks.addAction( 314 "frontend/element_ready/container", 315 initAllStickyElements, 316 ); 317 }); 318 319 // Also init on DOMContentLoaded for non-widget page loads 320 $(document).ready(function () { 321 $("[data-pea-sticky]").each(function () { 322 PeaStickyHandler($(this)); 323 }); 324 }); 325})(jQuery);
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.