1// Der Infobereich kann aus mehreren Gruenden gleichzeitig ausgeblendet sein 2// (Footer im Viewport, eingebettetes iFrame im Viewport). Weil sich alle 3// Observer dieselbe Klasse am body teilen, wird jeder Grund einzeln gemerkt - 4// sonst hebt ein Observer das Ausblenden eines anderen wieder auf. 5const infobereichHiddenClass = 'hw_infobereich__hidden'; 6const infobereichHideReasons = new Set(); 7 8/** 9 * Bringt die Klasse am body mit den gemerkten Gruenden in Einklang. 10 * 11 * Wird auch beim Resize aufgerufen. Bewusst wird dabei nichts zurueckgesetzt: 12 * ein IntersectionObserver meldet sich nur, wenn sich der Sichtbarkeitszustand 13 * aendert. Ein hartes Einblenden wuerde deshalb z. B. die Infobox mitten ueber 14 * einem weiterhin sichtbaren iFrame wieder auftauchen lassen - der Observer 15 * haette keinen Anlass, das zu korrigieren. 16 */ 17function refreshInfobereichHidden() { 18 document.body.classList.toggle(infobereichHiddenClass, infobereichHideReasons.size > 0); 19} 20 21/** 22 * Setzt oder entfernt einen Grund zum Ausblenden des Infobereichs. 23 * 24 * @param {string} reason Kennung des Grundes, z. B. 'footer' oder 'iframe' 25 * @param {boolean} hidden true, solange dieser Grund das Ausblenden verlangt 26 */ 27function setInfobereichHidden(reason, hidden) { 28 if (hidden) { 29 infobereichHideReasons.add(reason); 30 } else { 31 infobereichHideReasons.delete(reason); 32 } 33 34 refreshInfobereichHidden(); 35} 36 37/** 38 * Blendet den Infobereich aus, sobald der Footer in den Viewport scrollt. 39 */ 40function observeFooter() { 41 const footer = document.querySelector('footer'); 42 43 if (!footer) { 44 return; 45 } 46 47 const footerObserver = new IntersectionObserver(entries => { 48 entries.forEach(entry => { 49 setInfobereichHidden('footer', entry.isIntersecting); 50 }); 51 }, { 52 root: null, 53 rootMargin: '0px 0px -20% 0px', 54 threshold: 0, 55 }); 56 57 footerObserver.observe(footer); 58} 59 60/** 61 * Blendet den Infobereich aus, solange ein eingebettetes iFrame im Viewport
62 * liegt. Die Infobox liegt sonst ueber dem iFrame-Inhalt (z. B. ueber der 63 * Online-Terminvereinbarung) und verdeckt ihn. 64 * 65 * Mitbeobachtet wird der Platzhalter der 2-Klick-Loesung ([data-type="iframe"]), 66 * weil das eigentliche iFrame erst nach der Cookie-Einwilligung in den DOM 67 * kommt - der Platzhalter belegt aber schon vorher dieselbe Flaeche. 68 */ 69function observeIframes() { 70 const embeds = document.querySelectorAll('#content iframe, #content [data-type="iframe"]'); 71 72 if (!embeds.length) { 73 return; 74 } 75 76 // Es koennen mehrere Embeds gleichzeitig sichtbar sein, deshalb wird der 77 // Sichtbarkeitszustand pro Element gemerkt statt direkt umgeschaltet. 78 const visibleEmbeds = new Set(); 79 80 const iframeObserver = new IntersectionObserver(entries => { 81 entries.forEach(entry => { 82 if (entry.isIntersecting) { 83 visibleEmbeds.add(entry.target); 84 } else { 85 visibleEmbeds.delete(entry.target); 86 } 87 }); 88 89 setInfobereichHidden('iframe', visibleEmbeds.size > 0); 90 }, { 91 root: null, 92 93 // Wie beim Footer: das Embed muss die unteren 20% des Viewports 94 // erreichen. Ohne diesen Rand wuerde ein Embed, das weit oben auf der 95 // Seite steht, schon bei Scrollposition 0 mit wenigen Pixeln in den 96 // Viewport ragen - die Infobox waere dann nie zu sehen. 97 rootMargin: '0px 0px -20% 0px', 98 threshold: 0, 99 }); 100 101 embeds.forEach(embed => iframeObserver.observe(embed)); 102} 103 104if (document.readyState === 'complete') { 105 setupInfobereich(); 106} else { 107 document.addEventListener('readystatechange', () => { 108 if (document.readyState === 'complete') { 109 setupInfobereich(); 110 } 111 }); 112} 113 114function infobereichDesktop(infobereichWrap, infoboxen) { 115 116 if (!document.querySelector('.hw_infobereich__more_button')) { 117 const moreButton = ` 118 <button type="button" class="hw_infobereich__more_button"> 119 <i class="fa-regular fa-plus fa-2xl" aria-hidden="true"></i> 120 <span class="sr-only">Mehr Informationen einblenden</span> 121 </button>`; 122 123 const moreInformationButton = document.createRange().createContextualFragment(moreButton); 124 125 moreInformationButton.querySelector('button').addEventListener('click', (event) => { 126 event.stopImmediatePropagation(); 127 128 infobereichWrap.setAttribute('aria-expanded', true); 129 130 document.body.classList.add('hw_infobereich__open'); 131 }); 132 133 infoboxen[0].append(moreInformationButton.querySelector('button')); 134 } 135 136 if (!document.querySelector('.hw_infobereich__close_button')) { 137 138 const closeButton = ` 139 <button type="button" class="hw_infobereich__close_button"> 140 <i class="fa-regular fa-multiply fa-2xl" aria-hidden="true"></i> 141 <span class="sr-only">Informationsbereich ausblenden</span> 142 </button>`; 143 144 const closeInformationButton = document.createRange().createContextualFragment(closeButton); 145 146 147 closeInformationButton.querySelector('button').addEventListener('click', (event) => { 148 event.stopImmediatePropagation(); 149 150 infobereichWrap.setAttribute('aria-expanded', false); 151 document.body.classList.remove('hw_infobereich__open'); 152 infobereichWrap.scrollTo({ 153 top: 0, behavior: 'smooth' 154 }); 155 }); 156 157 infoboxen[0].append(closeInformationButton.querySelector('button')); 158 } 159 160 observeFooter(); 161} 162 163function infobereichMobile(infobereichWrap) { 164 setTimeout(() => { 165 const infoButton = document.querySelector('.hw_menu__mobile_button__info'); 166 167 infoButton.addEventListener('click', (event) => { 168 event.stopImmediatePropagation(); 169 170 // Sollte man vielleicht nochmal anschauen 171 172 if (document.querySelector('.hw_menu__mobile_button__menu[aria-pressed="true"]')) { 173 document.querySelector('.hw_menu__mobile_button__menu').click(); 174 } 175 176 if (document.querySelector('.hw_menu__mobile_button__search[aria-pressed="true"]')) { 177 document.querySelector('.hw_menu__mobile_button__search').click(); 178 } 179 180 let state; 181 182 if (infobereichWrap.getAttribute('aria-expanded')) { 183 state = infobereichWrap.getAttribute('aria-expanded'); 184 } else { 185 state = 'false'; 186 } 187 188 if (state === 'true') { 189 infobereichWrap.setAttribute('aria-expanded', false); 190 event.target.setAttribute('aria-pressed', false); 191 document.body.classList.remove('hw_infobereich__open'); 192 193 } else { 194 infobereichWrap.setAttribute('aria-expanded', true); 195 event.target.setAttribute('aria-pressed', true); 196 document.body.classList.add('hw_infobereich__open'); 197 } 198 }); 199 }, 1); 200} 201 202function closeInfobereich(infobereichWrap, mobileBreakPoint) { 203 if (document.body.classList.contains('hw_infobereich__open')) { 204 205 // event.stopImmediatePropagation(); 206 207 infobereichWrap.setAttribute('aria-expanded', false); 208 209 if (document.querySelector('.hw_menu__mobile_button__info')) { 210 document.querySelector('.hw_menu__mobile_button__info').setAttribute('aria-pressed', false); 211 } 212 213 document.body.classList.remove('hw_infobereich__open'); 214 215 if (window.innerWidth > mobileBreakPoint) { 216 infobereichWrap.scrollTo({ 217 top: 0, behavior: 'smooth' 218 }); 219 } 220 } 221} 222 223function moveSolrExtrasToInfobereich() { 224 return new Promise((resolve, reject) => { 225 226 const solrExtras = document.querySelector('.hw_solr__container_extra'); 227 228 Array.from(document.querySelector('#infobox').children).forEac
228h(child => { 229 child.remove(); 230 }); 231 232 document.querySelector('#infobox').append(solrExtras); 233 solrExtras.classList.add('frame', 'hw_frame'); 234 235 resolve(); 236 }); 237} 238 239const setupInfobereich = async () => { 240 241 if (document.querySelector('#infobereich_wrap')) { 242 243 // SOLR Extras werden in die Infobox gepackt 244 245 if (document.querySelector('.hw_solr__container_extra')) { 246 await moveSolrExtrasToInfobereich(); 247 } else if (document.querySelector('.hw_solr__main_search_form__wrap') && !document.querySelector('.hw_solr__extra_header')) { 248 document.body.classList.add('no-search-info'); 249 } 250 251 const infobereichWrap = document.querySelector('#infobereich_wrap'); 252 const infobox = infobereichWrap.querySelector('#infobox'); 253 const infoboxen = infobereichWrap.querySelectorAll('#infobox > .frame'); 254 const mobileBreakPoint = 1024; 255 256 observeIframes(); 257 258 if (infoboxen.length > 1) { 259 infobereichWrap.classList.add('multiple'); 260 261 // if (infobox.getBoundingClientRect().height > (infobereichWrap.getBoundingClientRect().height - parseInt(getComputedStyle(infobereichWrap).getPropertyValue('padding-top'))) || window.innerWidth <= mobileBreakPoint) { 262 if (infobox.getBoundingClientRect().height > (window.innerHeight - document.querySelector('header').getBoundingClientRect().height - 50) || window.innerWidth <= mobileBreakPoint) { 263 264 if (window.innerWidth > mobileBreakPoint) { 265 infobereichDesktop(infobereichWrap, infoboxen); 266 } else { 267 infobereichMobile(infobereichWrap); 268 } 269 270 infobereichWrap.classList.add('overflow'); 271 272 document.querySelector('.hw_menu__backdrop').addEventListener('click', (event) => { 273 closeInfobereich(infobereichWrap, mobileBreakPoint); 274 }); 275 276 document.addEventListener('keydown', event => { 277 if (event.key === 'Escape') { 278 closeInfobereich(infobereichWrap, mobileBreakPoint); 279 } 280 }); 281 282 let previousWidth = window.innerWidth; 283 let resizeRAF; 284 285 const infobereichResizeObserver = new ResizeObserver(() => { 286 if (resizeRAF) { 287 cancelAnimationFrame(resizeRAF); 288 } 289 290 refreshInfobereichHidden(); 291 292 resizeRAF = requestAnimationFrame(() => { 293 const currentWidth = window.innerWidth; 294 295 if (previousWidth <= mobileBreakPoint && currentWidth > mobileBreakPoint) { 296 infobereichDesktop(infobereichWrap, infoboxen); 297 } else if (previousWidth > mobileBreakPoint && currentWidth <= mobileBreakPoint) { 298 infobereichMobile(infobereichWrap); 299 } 300 301 previousWidth = currentWidth; 302 }); 303 }); 304 305 infobereichResizeObserver.observe(document.body); 306 } else { 307 observeFooter(); 308 } 309 310 } else { 311 infobereichWrap.classList.add('single'); 312 313 if (window.innerWidth <= mobileBreakPoint) { 314 infobereichMobile(infobereichWrap); 315 } else { 316 observeFooter(); 317 } 318 319 if (infobox.getBoundingClientRect().height > (infobereichWrap.getBoundingClientRect().height - parseInt(getComputedStyle(infobereichWrap).getPropertyValue('padding-top')))) { 320 321 // 1 Box, aber höher als das Display. overflow scroll? 322 infobereichWrap.classList.add('overflow'); 323 } 324 325 326 let previousWidth = window.innerWidth; 327 let resizeRAF; 328 329 const infobereichResizeObserver = new ResizeObserver(() => { 330 if (resizeRAF) { 331 cancelAnimationFrame(resizeRAF); 332 } 333 334 resizeRAF = requestAnimationFrame(() => { 335 const currentWidth = window.innerWidth; 336 337 refreshInfobereichHidden(); 338 339 if (previousWidth <= mobileBreakPoint && currentWidth > mobileBreakPoint) { 340 infobereichDesktop(infobereichWrap, infoboxen); 341 } else if (previousWidth > mobileBreakPoint && currentWidth <= mobileBreakPoint) { 342 infobereichMobile(infobereichWrap); 343 } 344 345 previousWidth = currentWidth; 346 }); 347 }); 348 349 infobereichResizeObserver.observe(document.body); 350 } 351 352 const structureWrap = document.querySelector('#structure-wrap'); 353 const topOffset = document.querySelector('header').getBoundingClientRect().height; 354 355 const topObserverOptions = { 356 root: null, 357 rootMargin: '0px 0px -' + (window.innerHeight - topOffset - 50) + 'px 0px', 358 threshold: 0, 359 }; 360 361 const topObserver = new IntersectionObserver(entries => {
362 entries.forEach(entry => { 363 364 if (entry.isIntersecting) { 365 if (infobox.querySelectorAll('.hw_frame')[0].getBoundingClientRect().height < (window.innerHeight - document.querySelector('header').getBoundingClientRect().height - 80)) { 366 infobereichWrap.classList.add('fixed'); 367 } 368 } else { 369 infobereichWrap.classList.remove('fixed'); 370 } 371 }); 372 }, topObserverOptions); 373 374 topObserver.observe(structureWrap); 375 } 376};
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.