PageSourceSearch

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

js m3aawg.org collected 2026-09-24 08:25:27 UTC 16,022 bytes, 575 lines download raw bytes

1(function (Drupal) {
2	Drupal.behaviors.apbTabs = {
3		attach: function (context, settings) {
4			const { all } = APB.DOM;
5
6			// Search in context or document
7			const root = context instanceof HTMLElement ? context : document;
8			const tabsEls = all('.paragraph--type--apb-pb-tabs', root);
9
10			if (tabsEls.length) {
11				tabsEls.forEach(processTabsContainer);
12			}
13		},
14	};
15
16	// ===== GENERAL FUNCTIONS =====
17	function processTabsContainer(mainParent) {
18		const { one, text, setAttr } = APB.DOM;
19
20		// Flag per Paragraph → prevents duplication
21		if (mainParent.dataset.apbTabs === 'initialized') {
22			return;
23		}
24		mainParent.dataset.apbTabs = 'initialized';
25
26		// Get configuration from DOM
27		const config = getTabsConfig(mainParent);
28		// console.log('config:', config);
29
30		// Apply general settings
31		applyGeneralSettings(mainParent, config);
32
33		// Generate unique ID for this tabs container
34		const paragraphId = text(one('.paragraphs-item-id', mainParent));
35		const containerId = `apb-tabs-container-${paragraphId}`;
36		setAttr(mainParent, 'id', containerId);
37
38		// Create tabs structure
39		createTabsStructure(mainParent, config, containerId, paragraphId);
40
41		// Setup scroll functionality for tab content
42		setupTabsScroll(mainParent);
43
44		// Inject CSS styles
45		injectTabsCSS(containerId, config);
46
47		// Initialize tabs functionality
48		initializeTabsFunctionality(mainParent, containerId);
49	}
50
51	function getTabsConfig(mainParent) {
52		const { one, text } = APB.DOM;
53
54		return {
55			sectionTitle: text(
56				one('.field--name-field-apb-section-title', mainParent)
57			),
58			showSectionTitle: text(
59				one('.field--name-field-apb-show-section-title', mainParent)
60			),
61			customClassGroup: text(
62				one('.field--name-field-apb-custom-class', mainParent)
63			),
64			tbTitleMainColor: text(
65				one(
66					'.field--name-field-apb-color-style-02 .apb-tax-cs-title',
67					mainParent
68				)
69			),
70			tbMainColor: text(
71				one(
72					'.field--name-field-apb-color-style-02 .apb-tax-cs-main-color',
73					mainParent
74				)
75			),
76			tbTitleHover: text(
77				one(
78					'.field--name-field-apb-color-style-02 .apb-tax-cs-title-hover',
79					mainParent
80				)
81			),
82			tbHover: text(
83				one(
84					'.field--name-field-apb-color-style-02 .apb-tax-cs-hover',
85					mainParent
86				)
87			),
88			tbTitleActive: text(
89				one(
90					'.field--name-field-apb-color-style-02 .apb-tax-cs-title-active',
91					mainParent
92				)
93			),
94			tbActive: text(
95				one(
96					'.field--name-field-apb-color-style-02 .apb-tax-cs-active',
97					mainParent
98				)
99			),
100			tbHeight: text(
101				one(
102					'.field--name-field-apb-tabs-height-tx .term-machine-name',
103					mainParent
104				)
105			).replace('_', ''),
106			tbOrientation: text(
107				one('.field--name-field-apb-tabs-orientation', mainParent)
108			).replace('_', ''),
109			contentWidth: text(
110				one(
111					'.field--name-field-apb-width-tx .term-machine-name',
112					mainParent
113				)
114			).replace('_', ''),
115		};
116	}
117
118	function applyGeneralSettings(mainParent, config) {
119		const { one, addClass, removeClass, setAttr } = APB.DOM;
120		const { toBoolean } = APB.Data;
121
122		// SHOW SECTION TITLE
123		if (config.sectionTitle && toBoolean(config.showSectionTitle)) {
124			const titleEl = one(
125				'.field--name-field-apb-section-title',
126				mainParent
127			);
128			if (titleEl) removeClass(titleEl, 'apb-setup');
129		}
130
131		// CONTENT WIDTH
132		if (config.contentWidth !== '100') {
133			setAttr(
134				mainParent,
135				'style',
136				`max-width: ${config.contentWidth}%; margin-left: auto !important; margin-right: auto !important;`
137			);
138		}
139
140		// CUSTOM CLASS GROUP
141		if (config.customClassGroup) {
142			addClass(mainParent, config.customClassGroup);
143		}
144	}
145
146	function createTabsStructure(mainParent, config, containerId, paragraphId) {
147		const { one, all, text, addClass, setAttr, createElement } = APB.DOM;
148
149		const viewsContainer = one('.views-element-container', mainParent);
150		const tabRows = all('.views-element-container .apb-tabs-row', mainParent);
151
152		// Create accordion wrapper
153		const accordionWrapper = createElement('section', 'apb-tabs-acc');
154		// Add orientation class if defined
155		if (config.tbOrientation) {
156			addClass(accordionWrapper, `apb-tabs-acc__${config.tbOrientation}`);
157		} else {
158			addClass(accordionWrapper, 'apb-tabs-acc__horizontal');
159		}
160
161		// Create tabs navigation (desktop)
162		const tabsNav = createElement(
163			'section',
164			'apb-tabs-acc-tabs apb-tabs-nav'
165		);
166		setAttr(tabsNav, 'role', 'tablist');
167		setAttr(tabsNav, 'aria-label', 'Tab navigation');
168
169		// Create content wrapper
170		const contentWrapper = createElement(
171			'section',
172			'apb-tabs-acc-content apb-tabs-content'
173		);
174
175		// Process each tab row
176		tabRows.forEach((row, index) => {
177			const tabTitle = text(one('.apb-tabs-title', row)).trim();
178			const tabBody = one('.apb-tabs-body', row);
179			const tabCustomClass = text(one('.apb-tabs-custom-class', row)).trim();
180
181			// Generate unique tab ID including paragraph ID
182			const baseTabId = tabTitle
183				.replace(/[^a-z0-9\s]/gi, '')
184				.replace(/[_\s]+/g, '-')
185				.toLowerCase();
186			const tabId = `${paragraphId}-${baseTabId}-${index}`;
187
188			// Create tab button (desktop)
189			const tabButton = createElement('button', 'apb-tabs-acc-tab');
190			if (index === 0) addClass(tabButton, 'apb-tabs-acc-active');
191			setAttr(tabButton, 'role', 'tab');
192			setAttr(tabButton, 'aria-selected', index === 0 ? 'true' : 'false');
193			setAttr(tabButton, 'aria-controls', `apb-tabs-panel-${tabId}`);
194			setAttr(tabButton, 'id', `apb-tabs-tab-${tabId}`);
195			setAttr(tabButton, 'data-actab-group', containerId);
196			setAttr(tabButton, 'data-actab-id', index.toString());
197			setAttr(tabButton, 'tabindex', index === 0 ? '0' : '-1');
198			tabButton.textContent = tabTitle;
199
200			// Create tab panel (mobile accordion + desktop content)
201			const tabPanel = createElement('article', 'apb-tabs-acc-item');
202			if (index === 0) addClass(tabPanel, 'apb-tabs-acc-active');
203			setAttr(tabPanel, 'role', 'tabpanel');
204			setAttr(tabPanel, 'id', `apb-tabs-panel-${tabId}`);
205			setAttr(tabPanel, 'aria-labelledby', `apb-tabs-tab-${tabId}`);
206			setAttr(tabPanel, 'data-actab-group', containerId);
207			setAttr(tabPanel, 'data-actab-id', index.toString());
208
209			// Create accordion label (mobile)
210			const accordionLabel = createElement(
211				'div',
212				'apb-tabs-acc-item__label'
213			);
214			setAttr(accordionLabel, 'tabindex', '0');
215			accordionLabel.textContent = tabTitle;
216
217			// Create content container
218			const contentContainer = createElement(
219				'div',
220				'apb-tabs-acc-item__container'
221			);
222
223			// Move original content to new structure
224			if (tabBody) {
225				contentContainer.appendChild(tabBody.cloneNode(true));
226			}
227
228			// Apply custom class if exists
229			if (tabCustomClass) {
230				addClass(tabPanel, tabCustomClass);
231			}
232
233			// Assemble structures
234			tabPanel.appendChild(accordionLabel);
235			tabPanel.appendChild(contentContainer);
236			tabsNav.appendChild(tabButton);
237			contentWrapper.appendChild(tabPanel);
238		});
239
240		// Assemble final structure
241		accordionWrapper.appendChild(tabsNav);
242		accordionWrapper.appendChild(contentWrapper);
243
244		// Replace original content
245		viewsContainer.innerHTML = '';
246		viewsContainer.appendChild(accordionWrapper);
247	}
248
249	function setupTabsScroll(mainParent) {
250		const { all, addStyles } = APB.DOM;
251
252		// Get all content containers
253		const contentContainers = all('.apb-tabs-acc-content', mainParent);
254
255		contentContainers.forEach(container => {
256			// Add overflow styles
257			addStyles(container, {
258				'overflow-y': 'auto',
259				'overflow-x': 'hidden',
260			});
261
262			// Initialize custom scrollbar using APB.DOM.scrollBox
263			if (typeof APB.DOM.scrollBox === 'function') {
264				APB.DOM.scrollBox(container, {
265					scrollbars: {
266						theme: 'os-theme-light',
267						visibility: 'visible',
268						autoHide: 'never',
269						dragScroll: true,
270						clickScroll: true,
271						touchSupport: true,
272					},
273					overflow: {
274						x: 'hidden',
275						y: 'scroll',
276					},
277					paddingAbsolute: true,
278					showNativeOverlaidScrollbars: false,
279				});
280			}
281		});
282	}
283
284	function injectTabsCSS(containerId, config) {
285		const { injectCSS } = APB.DOM;
286
287		const cssRules = `
288			/* Desktop tabs styles */
289			#${containerId} .apb-tabs-acc-tab {
290				background: ${config.tbMainColor} !important;
291				color: ${config.tbTitleMainColor} !important;
292			}
293			#${containerId} .apb-tabs-acc-tab:hover {
294				background: ${config.tbHover} !important;
295				color: ${config.tbTitleHover} !important;
296			}
297			#${containerId} .apb-tabs-acc-tab.apb-tabs-acc-active {
298				background: ${config.tbActive} !important;
299				color: ${config.tbTitleActive} !important;
300			}
301
302			/* Mobile accordion styles */
303			#${containerId} .apb-tabs-acc-item__label {
304				background: ${config.tbMainColor} !important;
305				color: ${config.tbTitleMainColor} !important;
306			}
307			#${containerId} .apb-tabs-acc-item__label:hover {
308				background: ${config.tbHover} !important;
309				color: ${config.tbTitleHover} !important;
310			}
311			#${containerId} .apb-tabs-acc-item.apb-tabs-acc-active .apb-tabs-acc-item__label {
312				background: ${config.tbActive} !important;
313				color: ${config.tbTitleActive} !important;
314			}
315
316			@media screen and (min-width: 600px) {
317            /* Content height */
318			   #${containerId} .apb-tabs-acc-content {
319					height: ${config.tbHeight} !important;
320				}
321			}
322		`;
323
324		injectCSS(cssRules);
325	}
326
327	function initializeTabsFunctionality(mainParent, containerId) {
328		const { all } = APB.DOM;
329
330		const labels = all('.apb-tabs-acc-item__label', mainParent);
331		const tabs = all('.apb-tabs-acc-tab', mainParent);
332		let currentAnimation = null;
333		let isInitialized = false;
334
335		// Detects if we are in mobile mode (accordion)
336		function isMobileMode() {
337			return window.innerWidth < 600;
338		}
339
340		// Initializes the correct state of containers on mobile
341		function initializeMobileState() {
342			if (isMobileMode() && !isInitialized) {
343				labels.forEach(function (label) {
344					const tabItem = label.parentElement;
345					const container = tabItem.querySelector(
346						'.apb-tabs-acc-item__container'
347					);
348
349					if (!tabItem.classList.contains('apb-tabs-acc-active')) {
350						// Ensures that closed items start with height 0
351
352						gsap.set(container, {
353							height: 0,
354							paddingTop: 0,
355							paddingBottom: 0,
356							opacity: 0,
357							overflow: 'hidden',
358						});
359					} else {
360						// Ensures that the active item starts with auto height
361						gsap.set(container, {
362							height: 'auto',
363							paddingTop: '1.25rem',
364							paddingBottom: '1.25rem',
365							opacity: 1,
366							overflow: 'hidden',
367						});
368					}
369				});
370				isInitialized = true;
371			}
372		}
373
374		// Call initialization when DOM loads
375		initializeMobileState();
376
377		function toggleShow() {
378			const target = this;
379			const item = target.classList.contains('apb-tabs-acc-tab')
380				? target
381				: target.parentElement;
382			const group = item.dataset.actabGroup;
383			const id = item.dataset.actabId;
384			const isAlreadyActive = item.classList.contains('apb-tabs-acc-active');
385
386			// Cancel previous animation if it exists
387			if (currentAnimation && typeof gsap !== 'undefined') {
388				currentAnimation.kill();
389			}
390
391			tabs.forEach(function (tab) {
392				if (tab.dataset.actabGroup === group) {
393					if (tab.dataset.actabId === id) {
394						tab.classList.add('apb-tabs-acc-active');
395						tab.setAttribute('aria-selected', 'true');
396						tab.setAttribute('tabindex', '0');
397					} else {
398						tab.classList.remove('apb-tabs-acc-active');
399						tab.setAttribute('aria-selected', 'false');
400						tab.setAttribute('tabindex', '-1');
401					}
402				}
403			});
404
405			labels.forEach(function (label) {
406				const tabItem = label.parentElement;
407				const container = tabItem.querySelector(
408					'.apb-tabs-acc-item__container'
409				);
410
411				if (tabItem.dataset.actabGroup === group) {
412					if (tabItem.dataset.actabId === id) {
413						// If already active and we're on mobile, close
414						if (isAlreadyActive && isMobileMode()) {
415							tabItem.classList.remove('apb-tabs-acc-active');
416
417							// Closing animation
418
419							// Kill any running animations on this container
420							gsap.killTweensOf(container);
421							currentAnimation = gsap.to(container, {
422								height: 0,
423								paddingTop: 0,
424								paddingBottom: 0,
425								opacity: 0,
426								duration: 0.8,
427								ease: 'power2.inOut',
428							});
429						} else {
430							// Add active class
431							tabItem.classList.add('apb-tabs-acc-active');
432
433							// Opening animation (mobile only)
434							if (isMobileMode()) {
435								// Kill any running animations on this container
436								gsap.killTweensOf(container);
437
438								// Calculate real content height WITH padding
439								gsap.set(container, {
440									height: 'auto',
441									opacity: 1,
442									paddingTop: '1.25rem',
443									paddingBottom: '1.25rem',
444								});
445								const contentHeight = container.scrollHeight;
446
447								// Define altura inicial
448								gsap.set(container, {
449									height: 0,
450									paddingTop: 0,
451									paddingBottom: 0,
452									opacity: 0,
453								});
454
455								// Anima para a altura real
456								currentAnimation = gsap.to(container, {
457									height: contentHeight,
458									paddingTop: '1.25rem',
459									paddingBottom: '1.25rem',
460									opacity: 1,
461									duration: 0.8,
462									ease: 'power2.out',
463									onComplete: function () {
464										gsap.set(container, { height: 'auto' });
465									},
466								});
467							}
468						}
469					} else {
470						// Close other items only on mobile
471						if (
472							isMobileMode() &&
473							tabItem.classList.contains('apb-tabs-acc-active')
474						) {
475							tabItem.classList.remove('apb-tabs-acc-active');
476
477							// Kill any running animations on this container
478							gsap.killTweensOf(container);
479							gsap.to(container, {
480								height: 0,
481								paddingTop: 0,
482								paddingBottom: 0,
483								opacity: 0,
484								duration: 0.8,
485								ease: 'power2.inOut',
486							});
487						} else {
488							tabItem.classList.remove('apb-tabs-acc-active');
489						}
490					}
491				}
492			});
493		}
494
495		// Keyboard navigation for tabs
496		function handleKeyboardNavigation(e) {
497			const currentTab = e.target;
498			const group = currentTab.dataset.actabGroup;
499			const groupTabs = Array.from(tabs).filter(
500				tab => tab.dataset.actabGroup === group
501			);
502			const currentIndex = groupTabs.indexOf(currentTab);
503
504			let nextIndex;
505
506			switch (e.key) {
507				case 'ArrowRight':
508					e.preventDefault();
509					nextIndex = (currentIndex + 1) % groupTabs.length;
510					groupTabs[nextIndex].focus();
511					groupTabs[nextIndex].click();
512					break;
513
514				case 'ArrowLeft':
515					e.preventDefault();
516					nextIndex =
517						(currentIndex - 1 + groupTabs.length) % groupTabs.length;
518					groupTabs[nextIndex].focus();
519					groupTabs[nextIndex].click();
520					break;
521
522				case 'Home':
523					e.preventDefault();
524					groupTabs[0].focus();
525					groupTabs[0].click();
526					break;
527
528				case 'End':
529					e.preventDefault();
530					groupTabs[groupTabs.length - 1].focus();
531					groupTabs[groupTabs.length - 1].click();
532					break;
533			}
534		}
535
536		// Keyboard navigation for labels (mobile accordion)
537		function handleLabelKeyboard(e) {
538			if (e.key === 'Enter' || e.key === ' ') {
539				e.preventDefault();
540				toggleShow.call(e.target);
541			}
542		}
543
544		// Add event listeners
545		labels.forEach(function (label) {
546			label.addEventListener('click', toggleShow);
547			label.addEventListener('keydown', handleLabelKeyboard);
548		});
549
550		tabs.forEach(function (tab) {
551			tab.addEventListener('click', toggleShow);
552			tab.addEventListener('keydown', handleKeyboardNavigation);
553		});
554
555		// Reset styles when changing from mobile to desktop
556		let lastMode = isMobileMode();
557		window.addEventListener('resize', function () {
558			const currentMode = isMobileMode();
559			if (lastMode !== currentMode) {
560				if (currentMode) {
561					// Entering mobile mode
562					isInitialized = false;
563					initializeMobileState();
564				} else {
565					// Entering desktop mode: clear all inline styles
566					labels.forEach(function (label) {
567						const container = label.nextElementSibling;
568						gsap.set([label, container], { clearProps: 'all' });
569					});
570				}
571				lastMode = currentMode;
572			}
573		});
574	}
575})(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.