PageSourceSearch

https://www.ammersbek.de/wp-content/themes/cv-bootscore-child/assets/js/custom.js?ver=202601071220

js ammersbek.de collected 2026-10-02 21:33:34 UTC 11,816 bytes, 350 lines download raw bytes

1import {flip, computePosition} from "@floating-ui/dom";
2
3jQuery(function ($) {
4
5    // const $navbar = $('#bootscore-navbar');
6	const $offcanvas_navbar = $('#offcanvas-navbar');
7	const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]');
8	const popoverList = [...popoverTriggerList].map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl));
9	const $submenuToggler = $('#bootscore-navbar a.dropdown-toggle');
10
11	// --- A11y helpers for keyboard navigation in multi-level dropdowns ---
12	const AUTO_CLOSE_OPTS = { autoClose: false };
13	function bsDropdown(toggleEl) {
14		return bootstrap.Dropdown.getOrCreateInstance(toggleEl, AUTO_CLOSE_OPTS);
15	}
16	function openMenu(toggleEl) {
17		bsDropdown(toggleEl).show();
18		toggleEl.setAttribute('aria-expanded', 'true');
19	}
20	function closeMenu(toggleEl) {
21		bsDropdown(toggleEl).hide();
22		toggleEl.setAttribute('aria-expanded', 'false');
23	}
24	function getMenu(toggleEl) {
25		return toggleEl?.parentElement?.querySelector(':scope > .dropdown-menu') || null;
26	}
27	function focusFirstItem(menuEl) {
28		const first = menuEl && menuEl.querySelector('.dropdown-item, .dropdown-toggle');
29		if (first) first.focus();
30	}
31	function getParentToggleFromItem(itemEl) {
32		const parentMenu = itemEl.closest('ul.dropdown-menu');
33		const parentLi = parentMenu?.closest('li.dropdown');
34		return parentLi?.querySelector(':scope > a.dropdown-toggle, :scope > button.dropdown-toggle') || null;
35	}
36
37	function hideOtherDropdowns() {
38		const $ul = $(this).closest('ul');
39		// Only hide siblings at the current level
40		$ul.find('> li > .dropdown-toggle').not(this).each(function(){
41			$(this).removeAttr('data-clicked');
42			closeMenu(this);
43		});
44	}
45
46	function hideAllDropdowns() {
47		$('.dropdown-menu.show').dropdown('hide');
48	}
49
50	/**
51	 * This event listener is used to reset the dropdown logic for tablet devices.
52	 */
53	$('.dropdown-toggle').on('click mouseenter', hideOtherDropdowns);
54
55	/**
56	 * This event listener is used to reset the dropdown logic when navigating with the keyboard.
57	 */
58	$('.nav-link, .dropdown-item').focus(hideOtherDropdowns);
59
60	/**
61	 * This event listener is used to handle the click event on the dropdown toggle in the navbar for tablet devices.
62	 */
63	$(('#bootscore-navbar .dropdown-toggle')).click(function(event) {
64		// Do nothing when on desktop device
65		if (!isTouchDevice()) {
66			window.location.href = $(this).attr('href');
67			return;
68		}
69
70		// Check if data attribute is true
71		if ($(this).attr('data-clicked')) {
72			event.preventDefault();
73			window.location.href = $(this).attr('href');
74			return;
75		}
76
77		event.preventDefault();
78		$(this).attr('data-clicked', true);
79		// Trigger bootstrap show
80		$(this).dropdown('toggle')
81	});
82
83	// Comprehensive keyboard handler: Enter/Space/Arrows/Escape
84	$('#bootscore-navbar').on('keydown', 'a.dropdown-toggle, button.dropdown-toggle, a.dropdown-item', function (e) {
85		const el = this;
86		const isToggle = el.classList.contains('dropdown-toggle');
87		const key = e.key;
88		const submenu = isToggle ? getMenu(el) : null;
89		const hasSubmenu = !!submenu;
90
91		// ENTER / SPACE toggles
92		if ((key === 'Enter' || key === ' ') && isToggle) {
93			e.preventDefault();
94			e.stopPropagation();
95			const expanded = el.getAttribute('aria-expanded') === 'true';
96			if (expanded) {
97				closeMenu(el);
98			} else {
99				openMenu(el);
100				focusFirstItem(getMenu(el));
101			}
102			return;
103		}
104
105		// SPACE on non-toggle links/items should activate the item, not scroll the page
106		if (key === ' ' && !isToggle) {
107			e.preventDefault();
108			e.stopPropagation();
109			// Trigger the default action of the link/button
110			if (typeof el.click === 'function') el.click();
111			return;
112		}
113
114		// ArrowRight → open submenu ONLY if the focused entry itself has a submenu
115		if (key === 'ArrowRight' && isToggle && hasSubmenu) {
116			e.preventDefault();
117			openMenu(el);
118			focusFirstItem(submenu);
119			return;
120		}
121
122		// ArrowLeft ← ONLY act when the focused entry has its own submenu: close it
123		if (key === 'ArrowLeft' && isToggle && hasSubmenu) {
124			e.preventDefault();
125			closeMenu(el);
126			el.focus();
127			return;
128		}
129
130		// ArrowDown / ArrowUp navigate items within open menu
131		if (key === 'ArrowDown' || key === 'ArrowUp') {
132			let menu = null;
133			if (isToggle && el.getAttribute('aria-expanded') === 'true') {
134				menu = getMenu(el);
135			} else {
136				menu = el.closest('ul.dropdown-menu');
137			}
138			if (menu) {
139				e.preventDefault();
140				const items = Array.from(menu.querySelectorAll('.dropdown-item, .dropdown-toggle'));
141				const current = document.activeElement;
142				const idx = items.indexOf(current);
143				let nextIdx = idx;
144				if (key === 'ArrowDown') nextIdx = idx === -1 ? 0 : Math.min(items.length - 1, idx + 1);
145				if (key === 'ArrowUp')   nextIdx = idx === -1 ? items.length - 1 : Math.max(0, idx 
145- 1);
146				items[nextIdx]?.focus();
147			}
148			return;
149		}
150
151		// ESC close current dropdown contextually (global handler remains)
152		if (key === 'Escape') {
153			const parentToggle = getParentToggleFromItem(el) || (isToggle ? el : null);
154			if (parentToggle) {
155				e.preventDefault();
156				closeMenu(parentToggle);
157				parentToggle.focus();
158			}
159		}
160	});
161
162	$(document).keydown(function(event) {
163		if (event.key === 'Escape') {
164			event.preventDefault();
165			hideAllDropdowns();
166		}
167	});
168
169	// Sync aria-expanded for all dropdowns on mouse-driven open/close as well
170	document.addEventListener('show.bs.dropdown', function (e) {
171		const toggle = e.target.querySelector(':scope > a.dropdown-toggle, :scope > button.dropdown-toggle');
172		if (toggle) toggle.setAttribute('aria-expanded', 'true');
173	});
174	document.addEventListener('hide.bs.dropdown', function (e) {
175		const toggle = e.target.querySelector(':scope > a.dropdown-toggle, :scope > button.dropdown-toggle');
176		if (toggle) toggle.setAttribute('aria-expanded', 'false');
177	});
178
179	$offcanvas_navbar.on('hide.bs.offcanvas show.bs.offcanvas hidePrevented.bs.offcanvas', function(event) {
180		// console.log('event
181
182		$('button[data-bs-target="#offcanvas-navbar"]').attr('aria-expanded', event.type === 'show');
183	});
184
185	// change the positioning of the dropdown menus in the main navigation if they don't fit into the view
186	// Note: key-driven openings are handled via 'shown.bs.dropdown' below to avoid keyup quirks
187	$submenuToggler.on('click mouseenter', async function(event) {
188		const submenu = $(this).next('.dropdown-menu').get(0);
189
190		if(submenu) {
191			let fallbacks = ['left-start', 'left'];
192			const isSubmenu = $(submenu).hasClass('sub-menu');
193
194
195			if(!isSubmenu) {
196				fallbacks = ['bottom-end'];
197			}
198
199			const { x, y } = await computePosition(this, submenu, {
200				middleware: [flip({
201					fallbackPlacements: fallbacks,
202				})],
203				// needs to be right-start, else it positions the submenu with multiple entries centered on the right side
204				placement: isSubmenu ? 'right-start' : 'bottom-start',
205			});
206
207			// Set the position of the dropdown
208			Object.assign(submenu.style, {
209				left: `${x}px`,
210				top: `${y}px`,
211			});
212
213			// submenu.style.visibility = 'visible';
214		}
215	});
216
217	// Also reposition when dropdowns are opened by keyboard (Enter) or via API
218	$('#bootscore-navbar').on('shown.bs.dropdown', 'a.dropdown-toggle, button.dropdown-toggle', async function () {
219		const submenu = $(this).next('.dropdown-menu').get(0);
220		if (!submenu) return;
221		const isSubmenu = $(submenu).hasClass('sub-menu');
222		const fallbacks = isSubmenu ? ['left-start', 'left'] : ['bottom-end'];
223		const { x, y } = await computePosition(this, submenu, {
224			middleware: [flip({ fallbackPlacements: fallbacks })],
225			placement: isSubmenu ? 'right-start' : 'bottom-start',
226		});
227		Object.assign(submenu.style, { left: `${x}px`, top: `${y}px` });
228	});
229
230	// Close any open menus that are not part of the current focused branch
231	function closeMenusOutsideBranch(focusedEl) {
232		const openMenus = document.querySelectorAll('#bootscore-navbar .dropdown-menu.show');
233		openMenus.forEach((menu) => {
234			const li = menu.parentElement; // <li class="dropdown">
235			// If focus is neither inside this menu nor inside its parent <li> subtree, close it
236			if (!menu.contains(focusedEl) && !li.contains(focusedEl)) {
237				const toggle = li.querySelector(':scope > a.dropdown-toggle, :scope > button.dropdown-toggle');
238				if (toggle) {
239					closeMenu(toggle);
240				}
241			}
242		});
243	}
244
245	// Run the branch-closing logic whenever focus moves within the navbar
246	$('#bootscore-navbar').on('focusin', 'a, button', function () {
247		closeMenusOutsideBranch(this);
248	});
249
250	// Also guard Tab navigation so stray menus don’t remain open
251	$('#bootscore-navbar').on('keydown', function (e) {
252		if (e.key === 'Tab') {
253			// Let focus move first, then close others on the next task
254			setTimeout(() => {
255				closeMenusOutsideBranch(document.activeElement);
256			}, 0);
257		}
258	});
259
260	function isTouchDevice() {
261		return 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
262	}
263
264	/**
265	 * The function that finds the correct anchor link and marks it as active.
266	 */
267	function updateActiveLink() {
268		// Get the current URL's path and hash.
269		// e.g., for "https://example.com/path/page#section", path is "/path/page", hash is "#section"
270		const currentPath = window.location.pathname;
271		const currentHash = window.location.hash;
272
273		// Get all navigation links within the main menu.
274		const navLinks = document.querySelectorAll('#bootscore-navbar a.dropdown-item[href*="#"]');
275
276		let targetLink = null;
277
278		// Loop through all found navigation links.
279		navLinks.forEach(link => {
280			// First, remove the 'active' class from the link itself.
281			// We will add it back only to the single correct one.
282			link.classList.remove('active');
283
284			// Check if this link is the correct one to be active.
285			const linkUrl = new URL(link.href);
286			if (linkUrl.pathname === currentPath) {
287				// If the URL has a hash, the link's hash must match exactly.
288				if (currentHash && linkUrl.hash.toLowerCase() === currentHash.toLowerCase()) {
289					targetLink = link;
290				}
291				// If the URL does NOT have a hash, the link must also not have a hash.
292				else if (!currentHash && !linkUrl.hash) {
293					targetLink = link;
294				}
295			}
296		});
297
298		// If we found the uniquely correct link to activate...
299		if (targetLink) {
300			targetLink.classList.add('active');
301		}
302	}
303
304	// Run the function once the page has loaded.
305	updateActiveLink();
306
307	// Also, run the function every time the URL hash changes (e.g., when a user clicks an in-page link).
308	$(window).on('hashchange', updateActiveLink);
309
310	/**
311	 * This event listener is used to handle the orientation change and resize events
312	 */
313	$(window).on('orientationchange resize', function () {
314		const $container = $('.wpfd-container');
315		$container.each(function () {
316			const $container = $(this);
317			handleMenuChange($container);
318		});
319	});
320
321
322	/**
323	 * This function triggers the automatic column resizing of the media table plugin
324	 */
325	$('.wpfd-container').each( function () {
326		const $container = $(this);
327		handleMenuChange($container);
328	});
329
330	/**
331	 * This function handles the change of the menu in the media table plugin.
332	 * @param $container A WPFD-Plugin Container
333	 */
334	function handleMenuChange($container) {
335		const $theadRow = $container.find('table.wpfd-search-result thead > tr');
336		const $tbodyRow = $container.find('table.wpfd-search-result tbody > tr.file');
337		let checkedCount = $container.find('.mediaTableMenu input:checked').length;
338
339		$container.find('.optional').removeClass('wpfd-col-hidden').css('display', 'block');
340
341		$tbodyRow.each(function () {
342			checkedCount = $(this).children('.optional').not('.file_desc').length;
343			$(this).css('grid-template-columns', `repeat(${checkedCount}, 1fr)`);
344			$(this).find('.optional').last().css('padding-right', '10px');
345		});
346
347		$theadRow.css('grid-template-columns', `repeat(${checkedCount}, 1fr) 205px`);
348	}
349
350}); // jQuery End

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.