PageSourceSearch

https://www.oberteuringen.de/fileadmin/templates/js/parts/hw_infobereich.js?1789992426

js oberteuringen.de collected 2026-10-02 16:53:45 UTC 11,693 bytes, 376 lines download raw bytes

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.