PageSourceSearch

https://www.m3aawg.org/themes/custom/aero10/apb/js/apb-accordion.js?v=11.4.7

js m3aawg.org collected 2026-09-24 08:25:16 UTC 11,272 bytes, 409 lines download raw bytes

1(function (Drupal) {
2	'use strict';
3
4	/**
5	 * Accordion configuration for @accede-web/accordion
6	 */
7	const ACCORDION_CONFIG = {
8		duration: 300,
9		ariaEnabled: true,
10		collapse: true,
11		showMultiple: false,
12		onlyChildNodes: true,
13		elementClass: 'apb-accordion-item',
14		triggerClass: 'apb-accordion-title',
15		panelClass: 'apb-accordion-panel',
16		activeClass: 'apb-accordion-active',
17		// beforeOpen: (element) => scrollToAccordionItem(element),
18		onOpen: element => scrollToAccordionItem(element),
19	};
20
21	/**
22	 * Use global APB utils
23	 */
24	const DOM = window.APB.DOM;
25
26	/**
27	 * Check if GSAP is available
28	 */
29	const isGSAP = () => window?.gsap && typeof window.gsap.to === 'function';
30
31	const docTop = el => el.getBoundingClientRect().top + window.scrollY;
32
33	const canGSAPScroll = () =>
34		!!(
35			window.gsap &&
36			typeof window.gsap.to === 'function' &&
37			window.ScrollToPlugin
38		);
39
40	const scrollToAccordionItem = element => {
41		const header = DOM.one('.apb-accordion-title', element) || element;
42		if (!header) return;
43
44		const headerOffset = DOM.hasClass(document.body, 'mobile')
45			? 16
46			: (DOM.one('header.header')?.offsetHeight || 0) + 16;
47
48		const target = Math.max(0, docTop(header) - headerOffset);
49
50		requestAnimationFrame(() => {
51			if (canGSAPScroll()) {
52				gsap.to(window, {
53					duration: 0.8,
54					scrollTo: { y: target, autoKill: false },
55					ease: 'power2.out',
56					overwrite: 'auto',
57				});
58			} else {
59				window.scrollTo({ top: target, behavior: 'smooth' });
60			}
61		});
62	};
63
64	/**
65	 * Parse color style block from wrapper
66	 */
67	const parseColorStyle = wrapper => {
68		const el = DOM.one('.field--name-field-apb-color-style-02', wrapper);
69		if (!el) return null;
70
71		return {
72			name: DOM.text(DOM.one('.term-machine-name', el)),
73			title: DOM.text(DOM.one('.apb-tax-cs-title', el)),
74			mainColor: DOM.text(DOM.one('.apb-tax-cs-main-color', el)),
75			titleHover: DOM.text(DOM.one('.apb-tax-cs-title-hover', el)),
76			hover: DOM.text(DOM.one('.apb-tax-cs-hover', el)),
77			titleActive: DOM.text(DOM.one('.apb-tax-cs-title-active', el)),
78			active: DOM.text(DOM.one('.apb-tax-cs-active', el)),
79		};
80	};
81
82	/**
83	 * Extract accordion configuration from fields
84	 */
85	const getAccordionConfig = wrapper => ({
86		sectionTitle: DOM.text(
87			DOM.one('.field--name-field-apb-section-title', wrapper)
88		),
89		showSectionTitle: DOM.text(
90			DOM.one('.field--name-field-apb-show-section-title', wrapper)
91		),
92		customClassGroup: DOM.text(
93			DOM.one('.field--name-field-apb-custom-class', wrapper)
94		),
95		colorStyle: parseColorStyle(wrapper),
96		contentWidth: DOM.text(
97			DOM.one('.field--name-field-apb-width-tx .term-machine-name', wrapper)
98		).replace('_', ''),
99		accordionId: DOM.text(DOM.one('.paragraphs-item-id', wrapper)),
100	});
101
102	/**
103	 * Apply extracted configuration to wrapper
104	 */
105	const applyConfig = (config, wrapper) => {
106		if (
107			config.sectionTitle &&
108			config.showSectionTitle.toLowerCase() === 'yes'
109		) {
110			DOM.removeClass(
111				DOM.one('.field--name-field-apb-section-title', wrapper),
112				'apb-setup'
113			);
114		}
115
116		if (config.contentWidth && config.contentWidth !== '100') {
117			const style = `
118         max-width: ${config.contentWidth}% !important;
119         margin-left: auto !important;
120         margin-right: auto !important;
121       `;
122			DOM.setAttr(wrapper, 'style', style);
123		}
124
125		if (config.customClassGroup) {
126			DOM.addClass(wrapper, config.customClassGroup);
127		}
128
129		if (config.colorStyle?.name) {
130			DOM.addClass(wrapper, `ac-${config.colorStyle.name}`);
131		}
132
133		if (config.colorStyle && config.accordionId) {
134			const { mainColor, hover, active, title, titleHover, titleActive } =
135				config.colorStyle;
136
137			const cssId = `apb-accordion-${config.accordionId}`;
138			const customCSS = `
139         #${cssId} .apb-accordion-title { background: ${mainColor} !important; color: ${title} !important; }
140         #${cssId} .apb-accordion-title:hover { background: ${hover} !important; color: ${titleHover} !important; }
141         #${cssId} .apb-accordion-title.apb-accordion-active,
142         #${cssId} .apb-accordion-title.apb-accordion-active:hover { background: ${active} !important; color: ${titleActive} !important; }
143		 #${cssId} .apb-accordion-active .apb-accordion-title,
144         #${cssId} .apb-accordion-active .apb-accordion-title.apb-accordion-active:hover { background: ${active} !important; color: ${titleActive} !important; }
145       `;
146			DOM.injectCSS(customCSS);
147		}
148
149		if (config.accordionId) {
150			wrapper.id = `apb-accordion-${config.accordionId}`;
151		}
152	};
153
154	/**
155	 * Ensure the accordion structure matches what the library expects
156	 */
157	const prepareHTMLOld = accordionView => {
158		const items = (() => {
159			try {
160				return DOM.all(':scope > div', accordionView);
161			} catch {
162				return Array.from(accordionView.children).filter(
163					el => el.tagName.toLowerCase() === 'div'
164				);
165			}
166		})();
167
168		items.forEach(item => {
169			if (!item) return;
170
171			if (!item.textContent.trim()) {
172				console.warn('Skipping empty accordion item:', item);
173				return;
174			}
175
176			// Remove admin noise
177			item
178				.querySelectorAll(
179					'.contextual-links, .contextual-links-wrapper, .contextual-links__trigger'
180				)
181				.forEach(el => el.remove());
182
183			item
184				.querySelectorAll('a[href*="edit"], .edit-link')
185				.forEach(el => el.remove());
186
187			// Ensure header
188			let header = DOM.one('.apb-accordion-title', item);
189			if (!header) {
190				const maybeTitle = item.querySelector('.title, h3, span');
191				if (maybeTitle) {
192					header = document.createElement('div');
193					header.className = 'apb-accordion-title';
194					header.innerHTML = maybeTitle.innerHTML;
195					maybeTitle.remove();
196					item.insertBefore(header, item.firstChild);
197				} else {
198					console.warn('Could not create header, removing item:', item);
199					item.remove();
200					return;
201				}
202			}
203
204			if (header.tagName !== 'BUTTON') {
205				header.setAttribute('role', 'button');
206				header.setAttribute('tabindex', '0');
207
208				header.addEventListener('keydown', event => {
209					if (event.key === 'Enter' || event.key === ' ') {
210						event.preventDefault();
211						header.click();
212					}
213				});
214			}
215
216			// Ensure panel
217			let panel = DOM.one('.accordion-body', item);
218			if (!panel) {
219				console.warn('No panel found, removing item:', item);
220				item.remove();
221				return;
222			}
223			if (!DOM.hasClass(panel, 'apb-accordion-panel')) {
224				DOM.addClass(panel, 'apb-accordion-panel');
225			}
226
227			// Add base item class
228			if (!DOM.hasClass(item, 'apb-accordion-item')) {
229				DOM.addClass(item, 'apb-accordion-item');
230			}
231
232			// Apply custom class if present
233			const customClass = DOM.text(
234				DOM.one('.apb-accordion-title .apb-ac-custom-class', item)
235			);
236			if (customClass) DOM.addClass(item, customClass);
237		});
238	};
239
240	const prepareHTML = accordionView => {
241		// 1) Remove root-level noise that is not an item
242		accordionView
243			.querySelectorAll(':scope > .contextual, :scope > header')
244			.forEach(el => el.remove());
245
246		// 2) Collect candidate items among remaining direct children
247		const children = Array.from(accordionView.children).filter(
248			el => el.tagName === 'DIV'
249		);
250
251		const items = children.filter(
252			el =>
253				el.classList.contains('apb-accordion-item') ||
254				el.querySelector('.apb-accordion-title') ||
255				el.querySelector('.accordion-body')
256		);
257
258		// 3) Normalize each item
259		items.forEach(item => {
260			// Remove admin noise INSIDE items
261			item
262				.querySelectorAll(
263					'.contextual, .contextual-links, .contextual-links-wrapper, .contextual-links__trigger, a[href*="edit"], .edit-link'
264				)
265				.forEach(el => el.remove());
266
267			// Base item class
268			item.classList.add('apb-accordion-item');
269
270			// Ensure header (prefer existing)
271			let header = item.querySelector('.apb-accordion-title');
272
273			if (!header) {
274				const seed = item.querySelector('.title, h3, span');
275				if (!seed) {
276					console.warn('Item missing title; removing:', item);
277					item.remove();
278					return;
279				}
280				const btn = document.createElement('button');
281				btn.type = 'button';
282				btn.className = 'apb-accordion-title';
283				btn.innerHTML = seed.innerHTML;
284				seed.remove();
285				item.insertBefore(btn, item.firstChild);
286				header = btn;
287			} else if (header.tagName !== 'BUTTON') {
288				// Upgrade to real <button> for a11y & consistency
289				const btn = document.createElement('button');
290				btn.type = 'button';
291				btn.className = header.className;
292				btn.innerHTML = header.innerHTML;
293				header.replaceWith(btn);
294				header = btn;
295			}
296
297			// Ensure panel
298			let panel = item.querySelector('.apb-accordion-panel');
299			if (!panel) {
300				panel = item.querySelector('.accordion-body');
301				if (!panel) {
302					console.warn('Item missing panel; removing:', item);
303					item.remove();
304					return;
305				}
306				panel.classList.add('apb-accordion-panel');
307			}
308
309			// Ensure trigger BEFORE panel
310			if (panel.previousElementSibling !== header) {
311				item.insertBefore(header, panel);
312			}
313		});
314
315		// 4) Remove any leftover non-items at root
316		Array.from(accordionView.children).forEach(ch => {
317			if (!ch.classList.contains('apb-accordion-item')) ch.remove();
318		});
319	};
320
321	/**
322	 * Initialize accordion instance
323	 */
324	const initAccordion = wrapper => {
325		if (!wrapper) return console.error('Invalid wrapper for accordion');
326
327		if (DOM.hasClass(wrapper, 'apb-accordion-initialized')) {
328			// console.log('Accordion already initialized, skipping:', wrapper);
329			return;
330		}
331
332		const config = getAccordionConfig(wrapper);
333		applyConfig(config, wrapper);
334
335		const view = DOM.one('.view-apb-pi-accordion-item', wrapper);
336		if (!view) return console.warn('Accordion view not found');
337
338		prepareHTML(view);
339
340		if (typeof window.Accordion === 'undefined') {
341			console.error('@accede-web/accordion not loaded');
342			return;
343		}
344
345		if (view.children.length === 0) {
346			console.warn('No accordion items found in view');
347			return;
348		}
349
350		try {
351			const accordion = new window.Accordion(view, ACCORDION_CONFIG);
352			// console.log(`Accordion initialized with ${view.children.length} items`);
353
354			DOM.addClass(wrapper, 'apb-accordion-initialized');
355
356			// Nested support
357			const nestedAccordions = DOM.all(
358				'.view-apb-pi-accordion-item',
359				wrapper
360			);
361			nestedAccordions.forEach(nested => {
362				if (nested !== view) {
363					const nestedWrapper = nested.closest(
364						'.paragraph--type--apb-pb-accordion'
365					);
366					if (
367						nestedWrapper &&
368						!DOM.hasClass(nestedWrapper, 'apb-accordion-initialized')
369					) {
370						console.log('Initializing nested accordion:', nestedWrapper);
371						initAccordion(nestedWrapper);
372					}
373				}
374			});
375
376			return accordion;
377		} catch (err) {
378			console.error('Error initializing accordion:', err);
379		}
380	};
381
382	/**
383	 * Drupal behavior for APB Accordion
384	 */
385	Drupal.behaviors.apbAccordion = {
386		attach: context => {
387			const wrappers = DOM.all(
388				'.paragraph--type--apb-pb-accordion',
389				context
390			);
391			if (!wrappers.length) return;
392
393			if (isGSAP() && window.gsap.registerPlugin) {
394				window.gsap.registerPlugin(window.ScrollToPlugin);
395			}
396
397			const apbWrapper = DOM.one('#apb-wrapper');
398
399			wrappers.forEach(wrapper => {
400				if (apbWrapper && apbWrapper.contains(wrapper)) {
401					initAccordion(wrapper);
402				} else {
403					// Fallback: run even if not inside #apb-wrapper
404					initAccordion(wrapper);
405				}
406			});
407		},
408	};
409})(Drupal);

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.