1export class HwContentOverlay { 2 #overlaySelector; 3 static #instance = null; 4 5 /** 6 * Initializes content overlays. 7 * 8 * If new dialog elements are dynamically added via JavaScript, 9 * call the following to initialize any missing dialogs: 10 * 11 * document.body._HwContentOverlay.init(); 12 */ 13 constructor() { 14 if (HwContentOverlay.#instance) { 15 16 // Return instance 17 return HwContentOverlay.#instance; 18 } 19 20 this.overlayList = new Set(); 21 22 // Configuration 23 this.#overlaySelector = 'dialog:not([id^="hw_datenschutz_banner"])'; 24 25 // Init all currently existing content overlays 26 this.init(); 27 28 // Add handler for url hashes 29 this.#initHash(); 30 31 // Global Events per Instance 32 this.#setupGlobalCloseEvent(); 33 34 // Add to document.body for external scripts 35 document.body._HwContentOverlay = this; 36 37 // Set instance 38 HwContentOverlay.#instance = this; 39 40 // Return instance 41 return HwContentOverlay.#instance; 42 } 43 44 #updateOverlayList() { 45 document.querySelectorAll(this.#overlaySelector).forEach(overlay => { 46 this.overlayList.add(overlay); 47 }); 48 } 49 50 init(reInit = false) { 51 52 // Update list of overlays 53 this.#updateOverlayList(); 54 55 // Init not yet initialized content overlays 56 this.overlayList.forEach(overlay => { 57 if (reInit) { 58 if (overlay.parentNode !== document.body ) { 59 delete overlay._contentOverlay; 60 61 // Clone Element to remove all event listeners 62 const overlayClone = overlay.cloneNode(true); 63 overlay.parentNode.replaceChild(overlayClone, overlay); 64 65 overlay.remove(); 66 } 67 } 68 69 if (overlay._contentOverlay) { 70 71 // Already initialized 72 return; 73 } 74 75 new HwContentOverlayElement(overlay); 76 }); 77 } 78 79 #initHash() { 80 81 // Hash on page load 82 if (window.location.hash) { 83 const hash = window.location.hash.slice(1); 84 85 this.#hashHandler(hash); 86 } 87 88 // Listen for hashchange event 89 window.addEventListener('hashchange', () => { 90 const hash = window.location.hash.slice(1); 91 92 this.#hashHandler(hash); 93 }); 94 95 // On anchor link click 96 document.querySelectorAll('a[href^="#"]').forEach((anchorLink) => { 97 anchorLink.addEventListener('click', (e) => { 98 e.preventDefault(); 99 100 const hash = anchorLink.href.split('#')[1]; 101 102 this.#hashHandler(hash); 103 }); 104 }); 105 } 106 107 #setupGlobalCloseEvent() { 108 109 // Keydown Event 110 document.addEventListener('keydown', (event) => { 111 if (event.key !== 'Escape') { 112 return; 113 } 114 115 event.preventDefault(); 116 117 document.querySelectorAll(this.#overlaySelector + '[open]').forEach((overlayElement) => { 118 overlayElement.close(); 119 }); 120 }); 121 } 122 123 #hashHandler(hash) { 124 const element = document.getElementById(hash); 125 126 if (element) { 127 128 const dialogElement = element.closest('dialog'); 129 130 if (dialogElement && dialogElement.overlayElement) { 131 dialogElement.overlayElement.open(); 132 } 133 } 134 } 135} 136 137class HwContentOverlayElement { 138 #overlayButtonOpen; 139 #overlayButtonClose; 140 #overlayContentWrap; 141 #overlayHeader; 142 #overlayContentElements; 143 #overlayMansoryEvent; 144 145 constructor(element) { 146 this.overlayElement = element; 147 148 // Added object to node for external scripts 149 element._contentOverlay = this; 150 151 this.#updateOverlayProperties(); 152 153 // Custom Event for Mansory layout 154 this.#overlayMansoryEvent = new CustomEvent('Hw:content_overlay_mansory', { 155 bubbles: true, // Allow the event to bubble up the DOM tree 156 cancelable: true // Allow the event to be canceled 157 }); 158 159 // Initialize overlay 160 this.#initializeOverlay(); 161 162 // Move element to document.body 163 if (element.parentNode !== document.body) { 164 document.body.appendChild(element); 165 } 166 } 167 168 #updateOverlayProperties() { 169 170 // Initialize overlay open button 171 this.#overlayButtonOpen = null; 172 173 if (this.overlayElement.id && document.querySelector(`[aria-controls="${this.overlayElement.id}"]`)) { 174 175 // Open button found for dialog id 176 this.#overlayButtonOpen = document.querySelector(`[aria-controls="${this.overlayElement.id}"]`); 177 } else if (this.overlayElement?.previousElementSibling?.querySelector('&:not(main) button')) { 178 179 // Open button found as previous element sibling 180 this.#overlayButtonOpen = this.overlayElement.previousElementSibling; 181 } 182 183 // Initialize overlay close button 184 this.#overlayButtonClose = this.overlayElement.querySelector('hw-content-overlay-button-close > button') || null; 185 186 // Initialize overlay content wrap 187 this.#overlayContentWrap = this.overlayElement.querySelector('hw-content-overlay-wrap') || null; 188 189 // Initialize overlay content header 190 this.#overlayHeader = this.overlayElement?.closest('hw-content-header, hw-content-element')?.querySelector('*[data-hw-content-role="overlay-header"], .hw_content__first_title') || null; 191 192 // Initialize overlay content elements 193 this.#overlayContentElements = this.overlayElement.querySelectorAll('hw-content-overlay-element') || null; 194 } 195 196 #initializeOverlay() { 197 this.#setupOpenButton(); 198 this.#setupCloseButton(); 199 this.#setupCloseEvent(); 200 201 // Save the original .show() method 202 const originalShowFunction = this.overlayElement.show.bind(this.overlayElement); 203 204 // Overwrite open function 205 this.overlayElement.show = () => { 206 this.#updateOverlayProperties(); 207 208 // Call the original .show() method to perform the default behavior 209 originalShowFunction(); 210 }; 211 212 if (this.overlayElement.closest('#structure-wrap')) { 213 const resizeObserver = new ResizeObserver(() => { 214 this.#setupMansoryLayout(); 215 }); 216 217 resizeObserver.observe(this.overlayElement); 218 } 219 } 220 221 #setupOpenButton() { 222 if (!this.#overlayButtonOpen) { 223 return; 224 } 225 226 if (this.#overlayHeader && !this.overlayElement.closest('hw-content-image-links-wrap')) { 227 this.#overlayHeader.after(this.#overlayHeader); 228 } 229 230 this.#overlayButtonOpen.addEventListener('click', (event) => { 231 event.preventDefault(); 232 this.overlayElement.show(); 233 }); 234 } 235 236 #setupCloseButton() { 237 if (!this.#overlayButtonClose) { 238 return; 239 } 240 241 this.#overlayButtonClose.addEventListener('click', (event) => { 242 event.preventDefault(); 243 this.overlayElement.close(); 244 }); 245 } 246 247 #setupCloseEvent() { 248 249 this.overlayElement.addEventListener('pointerdown', (event) => { 250 251 // Only clicks with main button 252 if (!(event.pointerType === 'mouse' && event.button === 0)) { 253 return; 254 } 255 256 const focusoutElements = this.overlayElement.querySelectorAll('hw-content-overlay-header, hw-content-overlay > *, hw-content-overlay-button-close, hw-content-overlay-wrap'); 257 258 // Elements that focus out without checking child nodes 259 const forceFocusoutElements = this.overlayElement.querySelectorAll('hw-content-overlay, hw-content-overlay-element'); 260 261 let elementWithin = false;
262 let forceClose = false; 263 264 forceFocusoutElements.forEach((forceFocusoutElement) => { 265 if (event.target === forceFocusoutElement) { 266 forceClose = true; 267 } 268 }); 269 270 focusoutElements.forEach((focusoutElement) => { 271 if (event.target === focusoutElement || focusoutElement.contains(event.target)) { 272 elementWithin = true; 273 } 274 }); 275 276 if (!elementWithin || forceClose) { 277 this.overlayElement.close(); 278 } 279 }); 280 281 // Focusout Event 282 this.overlayElement.addEventListener('focusout', (event) => { 283 event.preventDefault(); 284 285 if (this.overlayElement.contains(event.target)) { 286 287 // Focus is still within the container, don't treat it as a real focusout 288 event.stopPropagation(); 289 290 return; 291 } 292 293 this.overlayElement.close(); 294 }); 295 } 296 297 #setupMansoryLayout() { 298 this.overlayElement.addEventListener('hw:content_overlay_mansory', (event) => { 299 event.target.removeAttribute('style'); 300 }, { 301 once: true 302 }); 303 304 this.#initMansoryLayout(); 305 } 306 307 #initMansoryLayout() { 308 const hwContentOverlayElementsColumnCount = this.#hwGetCSSVariableValue(this.#overlayContentWrap, '--hw-content-overlay-columns'); 309 310 if (this.#overlayContentElements.length > 0) { 311 312 const hwContentOverlayElementContent = Array.from(this.#overlayContentElements[0].parentElement.children); 313 const hwContentOverlayElementColumns = Array.from({ 314 length: hwContentOverlayElementsColumnCount 315 }, () => []); 316 317 hwContentOverlayElementContent.forEach((hwContentOverlayElement, index) => { 318 hwContentOverlayElement.dataset.hwContentOverlayColumns = hwContentOverlayElementContent.length === 1 ? 1 : hwContentOverlayElementsColumnCount; 319 320 const hwContentOverlayColumnIndex = index % hwContentOverlayElementsColumnCount; 321 322 hwContentOverlayElementColumns[hwContentOverlayColumnIndex].push(hwContentOverlayElement); 323 hwContentOverlayElement.style.setProperty('--hw-content-overlay-animation-delay', index); 324 }); 325 326 hwContentOverlayElementColumns.forEach((hwContentOverlayColumn, hwContentOverlayColumnIndex) => { 327 let hwContentOverlayElementsCurrentTop = 0; 328 329 hwContentOverlayColumn.forEach(hwContentOverlayElement => { 330 hwContentOverlayElement.style.top = `${hwContentOverlayElementsCurrentTop / 16}rem`; 331 332 if (hwContentOverlayElementContent.length !== 1) { 333 hwContentOverlayElement.style.left = `calc((100% / (var(--hw-content-overlay-columns)) * ${hwContentOverlayColumnIndex})`; 334 } 335 336 hwContentOverlayElementsCurrentTop += hwContentOverlayElement.offsetHeight; 337 }); 338 339 hwContentOverlayColumn.hwContentOverlayColumnHeight = hwContentOverlayElementsCurrentTop; 340 }); 341 342 343 // Balancing (Only for the last item) 344 const hwContentOverlayLastItemIndex = hwContentOverlayElementContent.length - 1; 345 const hwContentOverlayLastItemOriginalColumnIndex = hwContentOverlayLastItemIndex % hwContentOverlayElementsColumnCount; 346 const hwContentOverlayLastItem = hwContentOverlayElementColumns[hwContentOverlayLastItemOriginalColumnIndex].pop(); 347 348 let hwContentOverlayShortestColumnIndex = 0; 349 let hwContentOverlayShortestColumnHeight = hwContentOverlayElementColumns[0].hwContentOverlayColumnHeight || 0; 350 351 for (let i = 0; i < hwContentOverlayElementColumns.length; i++) { 352 const currentColumnHeight = hwContentOverlayElementColumns[i].hwContentOverlayColumnHeight || 0; 353 354 if (currentColumnHeight < hwContentOverlayShortestColumnHeight) { 355 hwContentOverlayShortestColumnIndex = i; 356 } 357 } 358 359 360 if (hwContentOverlayElementsColumnCount > 1) { 361 let hwContentOverlayNewColumnIndex; 362 363 if (hwContentOverlayElementColumns.length === 2 && hwContentOverlayShortestColumnIndex === 0) { 364 hwContentOverlayNewColumnIndex = 1; 365 } else { 366 hwContentOverlayNewColumnIndex = hwContentOverlayElementColumns.length - hwContentOverlayShortestColumnIndex; 367 } 368 369 hwContentOverlayElementColumns[hwContentOverlayNewColumnIndex].push(hwContentOverlayLastItem); 370 371 let hwContentOverlayLastItemTop = 0; 372 373 hwContentOverlayShortestColumnHeight = 0; 374 375 hwContentOverlayElementColumns[hwContentOverlayNewColumnIndex].forEach(hwContentOverlayElement => { 376 if (hwContentOverlayElement !== hwContentOverlayLastItem) { 377 hwContentOverlayLastItemTop += hwContentOverlayElement.offsetHeight; 378 } 379 380 hwContentOverlayShortestColumnHeight += hwContentOverlayElement.offsetHeight; 381 }); 382 383 hwContentOverlayElementColumns[hwContentOverlayNewColumnIndex].hwContentOverlayColumnHeight = hwContentOverlayShortestColumnHeight; 384 hwContentOverlayLastItem.style.top = `${hwContentOverlayLastItemTop / 16}rem`; 385 386 if (hwContentOverlayElementContent.length !== 1) { 387 hwContentOverlayLastItem.style.left = `calc((100% / (var(--hw-content-overlay-columns)) * ${hwContentOverlayNewColumnIndex})`; 388 } else { 389 hwContentOverlayLastItem.style.left = `calc((100% / (var(--hw-content-overlay-columns)) * ${hwContentOverlayNewColumnIndex} / 2)`; 390 } 391 392 hwContentOverlayElementColumns.forEach((hwContentElementColumn, index) => {
393 hwContentElementColumn.forEach(hwContentOverlayElement => { 394 hwContentOverlayElement.removeAttribute('data-hw-content-overlay-first-column'); 395 hwContentOverlayElement.removeAttribute('data-hw-content-overlay-last-column'); 396 397 if (index === 0) { 398 hwContentOverlayElement.dataset.hwContentOverlayFirstColumn = ''; 399 } else if (index === hwContentOverlayElementColumns.length - 1) { 400 hwContentOverlayElement.dataset.hwContentOverlayLastColumn = ''; 401 } 402 }); 403 }); 404 this.overlayElement.querySelector('hw-content-overlay-elements').style.height = `${(hwContentOverlayLastItemTop + hwContentOverlayLastItem.offsetHeight) / 16}rem`; 405 } else { 406 this.overlayElement.querySelector('hw-content-overlay-elements').style.height = `${(hwContentOverlayLastItem.offsetHeight) / 16}rem`; 407 } 408 409 requestAnimationFrame(() => { 410 this.overlayElement.dispatchEvent(this.#overlayMansoryEvent); 411 }); 412 } 413 } 414 415 #hwGetCSSVariableValue(element, variableName) { 416 417 const computedStyle = getComputedStyle(element); 418 419 return computedStyle.getPropertyValue(variableName).trim(); 420 } 421}
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.