PageSourceSearch

https://www.townsvilleairport.com.au/.resources/gca/webresources/js/tab~2026-09-08-00-02-10-000~cache.js

js townsvilleairport.com.au collected 2026-09-25 01:04:32 UTC 10,626 bytes, 297 lines download raw bytes

1document.addEventListener('DOMContentLoaded', function () {
2  // ===== TAB FUNCTIONALITY =====
3  // Handle each tab navigation list independently
4  const tabLists = document.querySelectorAll('ul[role="tablist"]');
5
6  tabLists.forEach((tabList, listIndex) => {
7    const tabs = tabList.querySelectorAll('[data-bs-toggle="pill"]');
8    if (tabs.length === 0) return;
9
10    // Find the container for tab panes
11    // For FIDS tabs, look for specific container
12    let paneContainer = null;
13    
14    if (tabList.hasAttribute('data-fids-tabs')) {
15      // For FIDS, the tab content has a specific ID
16      paneContainer = document.getElementById('v-pills-fids-tabContent');
17    } else {
18      // For tabs-widget, look within the parent container
19      const widgetContainer = tabList.closest('.tabs-widget');
20      if (widgetContainer) {
21        paneContainer = widgetContainer.querySelector('.tab-content');
22      }
23      
24      if (!paneContainer) {
25        // For regular tabs, it's the next sibling (.tab-content)
26        paneContainer = tabList.nextElementSibling;
27        if (!paneContainer || !paneContainer.classList.contains('tab-content')) {
28          // Try to find tab-widget__content in parent
29          paneContainer = tabList
30            .closest('.container')
31            ?.querySelector('.tab-widget__content, .tab-content');
32        }
33      }
34    }
35
36    if (!paneContainer) {
37      console.warn('Could not find pane container for tab list', tabList);
38      return;
39    }
40
41    // Get all tab panes for this specific group
42    const tabPanes = [];
43    tabs.forEach((tab) => {
44      const targetId = tab.getAttribute('data-bs-target')?.substring(1);
45      if (targetId) {
46        const pane = paneContainer.querySelector(`#${targetId}`);
47        if (pane) {
48          tabPanes.push(pane);
49          // Hide all panes initially
50          pane.classList.add('hidden');
51        }
52      }
53    });
54
55    // Deactivate all tabs
56    tabs.forEach((tab) => {
57      tab.classList.remove('active-tab');
58      tab.setAttribute('aria-selected', 'false');
59    });
60
61    // Create a unique identifier for this tab widget based on its tabs
62    // This ensures each widget has its own storage key even with duplicate IDs
63    const tabIdentifiers = Array.from(tabs).map(tab => 
64      tab.getAttribute('data-bs-target')?.substring(1) || ''
65    ).join('_');
66    const widgetSignature = tabIdentifiers.substring(0, 50); // Limit length
67    
68    // Check for saved state
69    const urlParams = new URLSearchParams(window.location.search);
70    const deeplinkedTab = urlParams.get('selectedTab');
71    const storageKey = `activeTab_${widgetSignature}_${listIndex}`;
72    const savedTabId = localStorage.getItem(storageKey);
73    let activeTabFound = false;
74
75    // Try to activate saved/deeplinked tab
76    const targetToActivate = deeplinkedTab || savedTabId;
77    if (targetToActivate) {
78      tabs.forEach((tab, index) => {
79        const targetId = tab.getAttribute('data-bs-target')?.substring(1);
80        if (targetId === targetToActivate) {
81          tab.classList.add('active-tab');
82          tab.setAttribute('aria-selected', 'true');
83          if (tabPanes[index]) {
84            tabPanes[index].classList.remove('hidden');
85            activeTabFound = true;
86          }
87        }
88      });
89    }
90
91    // If no active tab, activate the first one
92    if (!activeTabFound && tabs.length > 0) {
93      tabs[0].classList.add('active-tab');
94      tabs[0].setAttribute('aria-selected', 'true');
95      if (tabPanes[0]) {
96        tabPanes[0].classList.remove('hidden');
97      }
98    }
99
100    // Add click handlers
101    tabs.forEach((tab, tabIndex) => {
102      tab.addEventListener('click', function (e) {
103        e.preventDefault();
104
105        const clickedTargetId =
106          this.getAttribute('data-bs-target')?.substring(1);
107        if (!clickedTargetId) return;
108
109        // Check if already active to avoid unnecessary work
110        if (this.classList.contains('active-tab')) return;
111
112        // Use requestAnimationFrame to batch DOM updates
113        requestAnimationFrame(() => {
114          // Save to localStorage with the unique storage key
115          localStorage.setItem(storageKey, clickedTargetId);
116
117          // Batch all DOM reads first
118          const updates = [];
119          tabs.forEach((t, i) => {
120            updates.push({ tab: t, pane: tabPanes[i], isTarget: i === tabIndex });
121          });
122
123          // Then batch all DOM writes
124          updates.forEach(({ tab, pane, isTarget }) => {
125            if (isTarget) {
126              tab.classList.add('active-tab');
127              tab.setAttribute('aria-selected', 'true');
128              if (pane) pane.classList.remove('hidden');
129            } else {
130              tab.classList.remove('active-tab');
131              tab.setAttribute('aria-selected', 'false');
132              if (pane) pane.classList.add('hidden');
133            }
134          });
135        });
136      });
137    });
138  });
139
140  // ===== ACCORDION FUNCTIONALITY =====
141  const accordionButtons = document.querySelectorAll(
142    '.accordion-header button'
143  );
144
145  // Initialize accordions
146  accordionButtons.forEach((button) => {
147    const isExpanded = button.getAttribute('aria-expanded') === 'true';
148    const targetId = button.getAttribute('data-bs-target')?.substring(1);
149    if (!targetId) return;
150
151    const content = document.getElementById(targetId);
152    if (!content) return;
153
154    // Find the icon span (first span with class containing w-7 h-7)
155    const iconSpan = button.querySelector('accordion-item__button-icon');
156    // Find the h4 heading with text-primary class
157    const headingText = button.querySelector('accordion-item__heading');
158
159    // Set initial state
160    if (isExpanded) {
161      button.classList.add('active-tab');
162
163      // Change icon color to deep-strata-brand if expanded
164      if (iconSpan) {
165        iconSpan.classList.remove('text-primary');
166        iconSpan.classList.add('text-deep-strata-brand');
167      }
168
169      // Change heading text color to deep-strata-brand if expanded
170      if (headingText) {
171        headingText.classList.remove('text-primary');
172        headingText.classList.add('text-deep-strata-brand');
173      }
174
175      // Target the arrow specifically (assume it's the last SVG)
176      const allSvgs = button.querySelectorAll('svg');
177      const arrowSvg = allSvgs[allSvgs.length - 1];
178      if (arrowSvg) arrowSvg.classList.add('rotate-180');
179
180      content.style.display = 'block';
181    } else {
182      content.style.display = 'none';
183    }
184
185    // Add click handler - only toggle the clicked panel
186    button.addEventListener('click', function () {
187      const isCurrentlyExpanded = this.getAttribute('aria-expanded') === 'true';
188      const clickedPanelId = this.getAttribute('data-bs-target')?.substring(1);
189      if (!clickedPanelId) return;
190
191      const clickedPanel = document.getElementById(clickedPanelId);
192      if (!clickedPanel) return;
193
194      // Find the icon span (first span with class containing w-7 h-7)
195      const iconSpan = this.querySelector('.accordion-item__button-icon');
196      // Find the h4 heading with text-primary or text-deep-strata-brand class
197      const headingText = this.querySelector('h4');
198
199      // Toggle this panel only
200      if (isCurrentlyExpanded) {
201        // Close this panel
202        this.setAttribute('aria-expanded', 'false');
203        this.classList.remove('active-tab');
204
205        // Change icon color back to primary when collapsed
206        if (iconSpan) {
207          iconSpan.classList.remove('text-deep-strata-brand');
208          iconSpan.classList.add('text-primary');
209        }
210
211        // Change heading text color back to primary when collapsed
212        if (headingText) {
213          headingText.classList.remove('text-deep-strata-brand');
214          headingText.classList.add('text-primary');
215        }
216
217        // Target the arrow specifically (assume it's the last SVG)
218        const allSvgs = this.querySelectorAll('svg');
219        const arrowSvg = allSvgs[allSvgs.length - 1];
220        if (arrowSvg) arrowSvg.classList.remove('rotate-180');
221
222        clickedPanel.style.display = 'none';
223      } else {
224        // Open this panel
225        this.setAttribute('aria-expanded', 'true');
226        this.classList.add('active-tab');
227
228        // Change icon color to deep-strata-brand when expanded
229        if (iconSpan) {
230          iconSpan.classList.remove('text-primary');
231          iconSpan.classList.add('text-deep-strata-brand');
232        }
233
234        // Change heading text color to deep-strata-brand when expanded
235        if (headingText) {
236          headingText.classList.remove('text-primary');
237          headingText.classList.add('text-deep-strata-brand');
238        }
239
240        // Target the arrow specifically (assume it's the last SVG)
241        const allSvgs = this.querySelectorAll('svg');
242        const arrowSvg = allSvgs[allSvgs.length - 1];
243        if (arrowSvg) arrowSvg.classList.add('rotate-180');
244
245        clickedPanel.style.display = 'block';
246      }
247    });
248  });
249
250  // Default: Open first accordion if none is open
251  if (accordionButtons.length > 0) {
252    let hasExpandedAccordion = false;
253    accordionButtons.forEach((btn) => {
254      if (btn.getAttribute('aria-expanded') === 'true') {
255        hasExpandedAccordion = true;
256      }
257    });
258
259    if (!hasExpandedAccordion) {
260      const firstButton = accordionButtons[0];
261      const firstPanelId = firstButton
262        .getAttribute('data-bs-target')
263        ?.substring(1);
264
265      if (firstPanelId) {
266        const firstPanel = document.getElementById(firstPanelId);
267        if (firstPanel) {
268          firstButton.setAttribute('aria-expanded', 'true');
269          firstButton.classList.add('active-tab');
270
271          // Change icon color for first accordion if automatically opened
272          const iconSpan = firstButton.querySelector(
273            '.accordion-item__button-icon'
274          );
275          if (iconSpan) {
276            iconSpan.classList.remove('text-primary');
277            iconSpan.classList.add('text-deep-strata-brand');
278          }
279
280          // Change heading text color for first accordion if automatically opened
281          const headingText = firstButton.querySelector('h4');
282          if (headingText) {
283            headingText.classList.remove('text-primary');
284            headingText.classList.add('text-deep-strata-brand');
285          }
286
287          // Target the arrow specifically (assume it's the last SVG)
288          const allSvgs = firstButton.querySelectorAll('svg');
289          const arrowSvg = allSvgs[allSvgs.length - 1];
290          if (arrowSvg) arrowSvg.classList.add('rotate-180');
291
292          firstPanel.style.display = 'block';
293        }
294      }
295    }
296  }
297});

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.