1/** 2 * Sticky Sidebar (desktop-only) â pure JS fixed-mode (no CSS sticky) 3 * ----------------------------------------------------------------- 4 * - Works in legacy float/layouts where CSS sticky fails. 5 * - Desktop-only (>= 1151px). 6 * - Avoids footer overlap and enables internal scrolling when taller than viewport. 7 * - Adds placeholder to prevent layout jumps. 8 * - WordPress-safe, no dependencies. 9 * 10 * Configurable constants are at the top. 11 */ 12 13(function () { 14 'use strict'; 15 16 // ----------------------------- 17 // Config 18 // ----------------------------- 19 var DEBUG = false; 20 var DESKTOP_MEDIA = '(min-width: 1151px)'; 21 var TOP_OFFSET_BASE = 12; // base gap to viewport top (px) 22 var BOTTOM_GAP = 12; // gap above footer (px) 23 var SIDEBAR_SELECTOR = '.sidebar, .Sidebar'; 24 var FOOTER_SELECTOR = 'footer.footer'; 25 var INIT_POLL_MS = 120; 26 var INIT_POLL_MAX = 25; 27 28 // ----------------------------- 29 // Logging helpers 30 // ----------------------------- 31 var NS='[StickySidebar/Fixed]'; 32 function log(){ if (DEBUG) console.log.apply(console,[NS].concat([].slice.call(arguments))); } 33 function warn(){ if (DEBUG) console.warn.apply(console,[NS].concat([].slice.call(arguments))); } 34 function info(){ if (DEBUG) console.info.apply(console,[NS].concat([].slice.call(arguments))); } 35 function table(rows){ if (DEBUG && console.table) console.table(rows); } 36 37 // ----------------------------- 38 // State 39 // ----------------------------- 40 var sidebar, footer, inner, placeholder, mq; 41 var isFixed = false; 42 var startStickY = 0; // absolute Y (document) ab dem fixiert wird 43 var topOffset = TOP_OFFSET_BASE; // inkl. Adminbar 44 var lastLeft = 0, lastWidth = 0; 45 var ro = null; 46 47 // ----------------------------- 48 // Utils 49 // ----------------------------- 50 function getScrollY(){ return window.pageYOffset || document.documentElement.scrollTop || 0; } 51 function rect(el){ return el.getBoundingClientRect(); } 52 function absTop(el){ var r=rect(el); return r.top + getScrollY(); } 53 function docHeight(){ 54 var b=document.body, e=document.documentElement; 55 return Math.max(b.scrollHeight,e.scrollHeight,b.offsetHeight,e.offsetHeight,b.clientHeight,e.clientHeight); 56 } 57 function footerAbsTop(){ return footer ? absTop(footer) : docHeight(); } 58 function adminBarHeight(){ 59 var bar = document.getElementById('wpadminbar'); 60 if (!bar) return 0; 61 // typische Werte: 32px Desktop, 46px mobil 62 var h = bar.offsetHeight || 0; 63 return h; 64 } 65 66 // ----------------------------- 67 // Core geometry 68 // ----------------------------- 69 function computeStart(){ 70 // Startpunkt ist der Sidebar-Top (relativ zum Dokument) minus (topOffset) 71 var sRect = rect(sidebar); 72 startStickY = sRect.top + getScrollY() - topOffset; 73 lastLeft = sRect.left; 74 lastWidth = sRect.width; 75 76 // Admin-Bar berücksichtigen 77 topOffset = TOP_OFFSET_BASE + adminBarHeight(); 78 79 info('computeStart â startStickY:', startStickY, 'left:', lastLeft, 'width:', lastWidth, 'topOffset (with adminbar):', topOffset); 80 } 81 82 function clampHeight(){ 83 var scrollY = getScrollY(); 84 var vpH = window.innerHeight || document.documentElement.clientHeight; 85 var byViewport = Math.max(0, vpH - topOffset); 86 var fTop = footerAbsTop(); 87 var byFooter = Math.max(0, fTop - scrollY - topOffset - BOTTOM_GAP); 88 var allowed = Math.max(100, Math.min(byViewport, byFooter)); 89 90 inner.style.maxHeight = allowed + 'px'; 91 // internes Scrollen nur wenn nötig 92 // setTimeout â Style zuerst anwenden, dann MaÃe prüfen 93 setTimeout(function(){ 94 var needs = inner.scrollHeight > inner.clientHeight + 1; 95 inner.style.overflowY = needs ? 'auto' : 'visible'; 96 if (DEBUG) table([ 97 {key:'scrollY', value: scrollY}, 98 {key:'viewportH', value: vpH}, 99 {key:'footerTopAbs', value: fTop}, 100 {key:'allowed maxHeight', value: allowed}, 101 {key:'inner.scrollHeight', value: inner.scrollHeight}, 102 {key:'inner.clientHeight', value: inner.clientHeight}, 103 {key:'needsScroll', value: needs}, 104 {key:'isFixed', value: isFixed} 105 ]); 106 },0); 107 } 108 109 // ----------------------------- 110 // Fixed mode 111 // ----------------------------- 112 function enterFixed(){ 113 if (isFixed) return; 114 isFixed = true; 115 116 // Platzhalter einfügen, damit die Spalte ihre Höhe behält 117 if (!placeholder){ 118 placeholder = document.createElement('div'); 119 placeholder.className = 'gt-sticky-sidebar-spacer'; 120 } 121 placeholder.style.height = inner.getBoundingClientRect().height + 'px'; 122 placeholder.style.width = lastWidth + 'px'; 123 sidebar.insertBefore(placeholder, inner); 124 125 // inner fixieren 126 inner.style.position = 'fixed'; 127 inner.style.top = topOffset + 'px'; 128 inner.style.left = lastLeft + 'px'; 129 inner.style.width = lastWidth + 'px'; 130 inner.style.boxSizing = 'border-box'; 131 132 sidebar.classList.add('gt-fixed-active'); 133 log('â enterFixed'); 134 } 135 136 function updateFixed(){ 137 if (!isFixed) return; 138 // Bei Resize/Layout-Shift Left/Width nachziehen 139 var sRect = rect(sidebar); 140 lastLeft = sRect.left; 141 lastWidth = sRect.width; 142 inner.style.left = lastLeft + 'px'; 143 inner.style.width = lastWidth + 'px'; 144 145 // Spacer-Höhe an Inhalt anpassen (z.B. Ads) 146 placeholder.style.height = inner.getBoundingClientRect().height + 'px'; 147 placeholder.style.width = lastWidth + 'px'; 148 } 149 150 function leaveFixed(){ 151 if (!isFixed) return; 152 isFixed = false;
153 154 inner.style.position = 'static'; 155 inner.style.top = ''; 156 inner.style.left = ''; 157 inner.style.width = ''; 158 sidebar.classList.remove('gt-fixed-active'); 159 160 if (placeholder && placeholder.parentNode === sidebar){ 161 sidebar.removeChild(placeholder); 162 } 163 log('â leaveFixed'); 164 } 165 166 // ----------------------------- 167 // Scroll/resize tick 168 // ----------------------------- 169 function tick(reason){ 170 if (!mq.matches){ 171 // reset auf Mobil/Tablet 172 leaveFixed(); 173 inner.style.maxHeight = ''; 174 inner.style.overflowY = ''; 175 return; 176 } 177 178 // mit Adminbar neu kalkulieren (falls Login-Status/Stickybar wechselte) 179 topOffset = TOP_OFFSET_BASE + adminBarHeight(); 180 181 var y = getScrollY(); 182 var shouldFix = y >= startStickY; 183 184 if (shouldFix) enterFixed(); else leaveFixed(); 185 186 clampHeight(); 187 188 if (isFixed) updateFixed(); 189 190 if (DEBUG) log('tick(', reason, ') â y:', y, 'startStickY:', startStickY, 'shouldFix:', shouldFix); 191 } 192 193 // ----------------------------- 194 // Init 195 // ----------------------------- 196 function initCore(){ 197 // Inner-Wrapper bauen 198 inner = document.createElement('div'); 199 inner.className = 'gt-sticky-sidebar-inner'; 200 // move children 201 while (sidebar.firstChild) inner.appendChild(sidebar.firstChild); 202 sidebar.appendChild(inner); 203 204 // Basistyle für inner (nicht sticky, nur für Box-Sizing & Scrollbar) 205 var style = document.createElement('style'); 206 style.textContent = [ 207 '.gt-sticky-sidebar-inner{box-sizing:border-box;}', 208 '.gt-sticky-sidebar-inner::-webkit-scrollbar{width:10px;}', 209 '.gt-sticky-sidebar-inner::-webkit-scrollbar-thumb{background:rgba(0,0,0,.2);border-radius:6px;}', 210 '.gt-fixed-active .gt-sticky-sidebar-inner{ /* marker class, no visual needed */ }' 211 ].join('\n'); 212 document.head.appendChild(style); 213 214 mq = window.matchMedia(DESKTOP_MEDIA); 215 216 computeStart(); 217 tick('init'); 218 219 window.addEventListener('scroll', function(){ tick('scroll'); }, {passive:true}); 220 window.addEventListener('resize', function(){ 221 computeStart(); 222 tick('resize'); 223 }, {passive:true}); 224 225 // Reagiere auf dynamische GröÃen (Ads/Widgets/Footer) 226 if ('ResizeObserver' in window){ 227 ro = new ResizeObserver(function(){ 228 computeStart(); 229 tick('resize-observer'); 230 }); 231 ro.observe(inner); 232 ro.observe(sidebar); 233 if (footer) ro.observe(footer); 234 } 235 236 info('initialized (pure fixed mode). breakpoint:', DESKTOP_MEDIA, 'TOP_OFFSET_BASE:', TOP_OFFSET_BASE, 'BOTTOM_GAP:', BOTTOM_GAP); 237 } 238 239 function start(){ 240 sidebar = document.querySelector(SIDEBAR_SELECTOR); 241 footer = document.querySelector(FOOTER_SELECTOR) || null; 242 243 if (!sidebar){ 244 warn('sidebar not found, pollingâ¦'); 245 var tries = 0; 246 var t = setInterval(function(){ 247 tries++; 248 sidebar = document.querySelector(SIDEBAR_SELECTOR); 249 if (sidebar){ 250 clearInterval(t); 251 footer = document.querySelector(FOOTER_SELECTOR) || null; 252 info('sidebar found after', tries, 'tries'); 253 initCore(); 254 } else if (tries >= INIT_POLL_MAX){ 255 clearInterval(t); 256 warn('giving up, no sidebar after', tries, 'tries'); 257 } 258 }, INIT_POLL_MS); 259 return; 260 } 261 initCore(); 262 } 263 264 // DOM ready 265 if (document.readyState === 'loading') { 266 document.addEventListener('DOMContentLoaded', start); 267 } else { 268 start(); 269 } 270})();
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.