PageSourceSearch

https://www.hotelmanagement-network.com/wp-content/themes/goodlife-wp-B2B/assets/js/modules/tabs.js

js hotelmanagement-network.com collected 2026-09-24 18:36:51 UTC 14,380 bytes, 464 lines download raw bytes

1/**
2 * Accessible Tabs Module
3 * Manages tab navigation with accessibility features for screen readers
4 */
5
6export default function initTabs(options = {}) {
7  const {
8    excludeFromTabsSelector = [],
9  } = options;
10  // console.log('initTabs() called');
11
12  // Look for both new and old tab structures
13  const newTabsContainers = document.querySelectorAll('.tabs');
14  const oldTabsContainers = document.querySelectorAll('.gdm-tabs');
15
16  // console.log('Found tabs containers:', {
17  //   newStructure: newTabsContainers.length,
18  //   oldStructure: oldTabsContainers.length
19  // });
20
21  // Handle new structure (.tabs)
22  if (newTabsContainers.length) {
23    // console.log('Processing new tab structure...');
24    newTabsContainers.forEach((tabsContainer, index) => {
25      // console.log(`Processing new tabs container ${index}:`, tabsContainer);
26      const tabsNav = tabsContainer.querySelector('.tabs-nav');
27      const tabNavItems = filterNavItems(
28        Array.from(tabsContainer.querySelectorAll('.tabs-nav li a')),
29        excludeFromTabsSelector
30      );
31      const tabPanels = tabsContainer.querySelectorAll('.tab');
32
33      // console.log('Found elements:', {
34      //   tabsNav,
35      //   tabNavItems: tabNavItems.length,
36      //   tabPanels: tabPanels.length
37      // });
38
39      if (!tabsNav || !tabNavItems.length || !tabPanels.length) {
40        // console.log('Missing required elements, skipping this container');
41        return;
42      }
43
44      // Set up ARIA attributes for accessibility
45      setupAccessibility(tabsContainer, tabNavItems, tabPanels);
46
47      // Initialize tabs - check for hash or show first tab by default
48      initializeTabs(tabNavItems, tabPanels);
49
50      // Bind click events
51      bindTabEvents(tabNavItems, tabPanels);
52
53      // Bind keyboard events for accessibility
54      bindKeyboardEvents(tabNavItems);
55
56      // Initialize mobile menu toggle for side navigation
57      initMobileMenuToggle(tabsContainer);
58    });
59  }
60
61  // Handle old structure (.gdm-tabs) for compatibility
62  if (oldTabsContainers.length) {
63    // console.log('Processing old tab structure for compatibility...');
64    oldTabsContainers.forEach((tabsContainer, index) => {
65      // console.log(`Processing old tabs container ${index}:`, tabsContainer);
66      const tabsNav = tabsContainer.querySelector('.gdm-tabs-nav');
67      const tabNavItems = filterNavItems(
68        Array.from(tabsContainer.querySelectorAll('.gdm-tabs-nav li a')),
69        excludeFromTabsSelector
70      );
71
72      // console.log('Found old structure elements:', {
73      //   tabsNav,
74      //   tabNavItems: tabNavItems.length
75      // });
76
77      if (!tabsNav || !tabNavItems.length) {
78        // console.log('Missing required elements in old structure, skipping');
79        return;
80      }
81
82      // For old structure, just bind basic click events for mobile nav
83      bindOldStructureEvents(tabsContainer, tabNavItems);
84    });
85  }
86
87  if (!newTabsContainers.length && !oldTabsContainers.length) {
88    // console.log('No tabs containers found, exiting');
89    return;
90  }
91
92  document.documentElement.classList.add('tabs-js-enabled');
93}
94
95/**
96 * Handle old structure mobile navigation (compatibility mode)
97 */
98function bindOldStructureEvents(tabsContainer, tabNavItems) {
99  // console.log('Setting up old structure mobile navigation');
100
101  tabNavItems.forEach((navItem, index) => {
102    navItem.addEventListener('click', (e) => {
103      // console.log(`Old structure tab ${index} clicked!`, navItem);
104
105      // Close side menu when tab is clicked (for mobile)
106      const sideNav = navItem.closest('.side-nav');
107      if (sideNav && sideNav.classList.contains('active')) {
108        const menuToggle = sideNav.querySelector('.menu-toggle');
109        closeSideNav(sideNav, menuToggle);
110      }
111
112      // Check if this is mobile and if we should handle nav expansion
113      const isMobile = window.innerWidth < 768;
114      if (isMobile) {
115        // console.log('Mobile detected, checking for side nav functionality...');
116        // For the old structure, we'll need to work with what exists
117        const parentNav = navItem.closest('.gdm-tabs-nav');
118        if (parentNav) {
119          // console.log('Adding active class to parent nav for mobile');
120          parentNav.classList.add('active');
121        }
122      }
123    });
124  });
125}
126
127/**
128 * Set up ARIA attributes for screen readers
129 */
130function setupAccessibility(tabsContainer, tabNavItems, tabPanels) {
131  // Set role attributes
132  const tabsList = tabsContainer.querySelector('.tabs-nav ul') || tabsContainer.querySelector('.tabs-nav');
133  if (tabsList) {
134    tabsList.setAttribute('role', 'tablist');
135  }
136
137  tabNavItems.forEach((navItem, index) => {
138    const tabId = `tab-${index}`;
139    const panelId = `tabpanel-${index}`;
140
141    // Set up tab navigation attributes
142    navItem.setAttribute('role', 'tab');
143    navItem.setAttribute('id', tabId);
144    navItem.setAttribute('aria-controls', panelId);
145    navItem.setAttribute('tabindex', index === 0 ? '0' : '-1');
146
147    // Set up tab panel attributes
148    if (tabPanels[index]) {
149      tabPanels[index].setAttribute('role', 'tabpanel');
150      tabPanels[index].setAttribute('id', panelId);
151      tabPanels[index].setAttribute('aria-labelledby', tabId);
152      tabPanels[index].setAttribute('tabindex', '0');
153    }
154  });
155}
156
157/**
158 * Initialize tabs - check for hash or show first tab by default
159 */
160function initializeTabs(tabNavItems, tabPanels) {
161  // Hide all tabs first
162  tabPanels.forEach((tab) => {
163    tab.classList.remove('active');
164    tab.setAttribute('aria-hidden', 'true');
165  });
166
167  // Remove active from all nav items
168  tabNavItems.forEach((navItem) => {
169    navItem.setAttribute('aria-selected', 'false');
170    navItem.parentElement.classList.remove('active');
171  });
172
173  // Check for hash in URL
174  const hash = window.location.hash.slice(1); // Remove the # symbol
175  let activeIndex = 0;
176
177  if (hash) {
178    // Find tab that matches the hash
179    const matchingIndex = Array.from(tabNavItems).findIndex((navItem) => {
180      const tabText = navItem.textContent.trim().toLowerCase().replace(/\s+/g, '-');
181      return tabText === hash.toLowerCase();
182    });
183
184    if (matchingIndex !== -1) {
185      activeIndex = matchingIndex;
186    }
187  }
188
189  // Show the determined active tab
190  if (tabPanels[activeIndex]) {
191    tabPanels[activeIndex].classList.add('active');
192    tabPanels[activeIndex].setAttribute('aria-hidden', 'false');
193  }
194
195  if (tabNavItems[activeIndex]) {
196    tabNavItems[activeIndex].setAttribute('aria-selected', 'true');
197    tabNavItems[activeIndex].parentElement.classList.add('active');
198  }
199}
200
201/**
202 * Bind click events to tab navigation
203 */
204function bindTabEvents(tabNavItems, tabPanels) {
205  // console.log('bindTabEvents called with', tabNavItems.length, 'nav items');
206  tabNavItems.forEach((navItem, index) => {
207    // console.log(`Binding click event to tab ${index}:`, navItem);
208    navItem.addEventListener('click', (e) => {
209      // console.log(`Tab ${index} clicked!`, navItem);
210      e.preventDefault();
211
212      // Check if we're in mobile view and handle side nav expansion
213      if (shouldHandleMobileNavigation(navItem)) {
214        // console.log('Mobile navigation handled, preventing tab activation');
215        return; // Don't activate tab, just expand nav
216      }
217
218      // Close side menu when tab is clicked (for mobile)
219      const sideNav = navItem.closest('.side-nav');
220      if (sideNav && sideNav.classList.contains('active')) {
221        const menuToggle = sideNav.querySelector('.menu-toggle');
222        closeSideNav(sideNav, menuToggle);
223      }
224
225      // console.log('Activating tab normally');
226      activateTab(index, tabNavItems, tabPanels);
227    });
228  });
229}
230
231/**
232 * Bind keyboard events for accessibility (arrow keys, enter, space)
233 */
234function bindKeyboardEvents(tabNavItems) {
235  tabNavItems.forEach((navItem, index) => {
236    navItem.addEventListener('keydown', (e) => {
237      let targetIndex = index;
238
239      switch (e.key) {
240        case 'ArrowRight':
241        case 'ArrowDown':
242          e.preventDefault();
243          targetIndex = (index + 1) % tabNavItems.length;
244          break;
245
246        case 'ArrowLeft':
247        case 'ArrowUp':
248          e.preventDefault();
249          targetIndex = (index - 1 + tabNavItems.length) % tabNavItems.length;
250          break;
251
252        case 'Home':
253          e.preventDefault();
254          targetIndex = 0;
255          break;
256
257        case 'End':
258          e.preventDefault();
259          targetIndex = tabNavItems.length - 1;
260          break;
261
262        case 'Enter':
263        case ' ':
264          e.preventDefault();
265          activateTab(index, tabNavItems, document.querySelectorAll('.tab'));
266          return;
267
268        default:
269          return;
270      }
271
272      // Focus and activate the target tab
273      tabNavItems[targetIndex].focus();
274      activateTab(targetIndex, tabNavItems, document.querySelectorAll('.tab'));
275    });
276  });
277}
278
279/**
280 * Create hash from tab text
281 */
282function createTabHash(tabText) {
283  return tabText.trim().toLowerCase().replace(/\s+/g, '-');
284}
285
286/**
287 * Check if mobile navigation should be handled instead of tab activation
288 */
289function shouldHandleMobileNavigation(navItem) {
290  // Check if screen width is under 768px
291  const isMobile = window.innerWidth < 768;
292
293  // console.log('shouldHandleMobileNavigation called:', {
294  //   isMobile,
295  //   windowWidth: window.innerWidth
296  // });
297
298  if (!isMobile) return false;
299
300  // Find the side nav container
301  const sideNav = navItem.closest('.side-nav');
302
303  // console.log('sideNav found:', sideNav);
304
305  if (!sideNav) return false;
306
307  // If side nav is not active (expanded), expand it instead of changing tabs
308  const isActive = sideNav.classList.contains('active');
309  // console.log('sideNav active status:', isActive);
310
311  if (!isActive) {
312    const menuToggle = sideNav.querySelector('.menu-toggle');
313    // console.log('menuToggle found:', menuToggle);
314
315    if (menuToggle) {
316      // console.log('Calling openSideNav with menu toggle...');
317      openSideNav(sideNav, menuToggle);
318    } else {
319      // console.log('No menu toggle found, adding active class directly...');
320      sideNav.classList.add('active');
321      // console.log('sideNav classes after direct addition:', sideNav.classList.toString());
322    }
323    return true; // Prevent tab activation
324  }
325
326  return false; // Allow tab activation
327}
328
329/**
330 * Activate a specific tab
331 */
332function activateTab(activeIndex, tabNavItems, tabPanels) {
333  // Remove active state from all tabs and nav items
334  tabPanels.forEach((tab) => {
335    tab.classList.remove('active');
336    tab.setAttribute('aria-hidden', 'true');
337  });
338
339  tabNavItems.forEach((navItem, index) => {
340    navItem.setAttribute('aria-selected', 'false');
341    navItem.setAttribute('tabindex', '-1');
342    navItem.parentElement.classList.remove('active');
343  });
344
345  // Add active state to selected tab and nav item
346  if (tabPanels[activeIndex]) {
347    tabPanels[activeIndex].classList.add('active');
348    tabPanels[activeIndex].setAttribute('aria-hidden', 'false');
349  }
350
351  if (tabNavItems[activeIndex]) {
352    tabNavItems[activeIndex].setAttribute('aria-selected', 'true');
353    tabNavItems[activeIndex].setAttribute('tabindex', '0');
354    tabNavItems[activeIndex].parentElement.classList.add('active');
355
356    // Update URL hash
357    const tabText = tabNavItems[activeIndex].textContent;
358    const hash = createTabHash(tabText);
359    window.history.replaceState(null, null, `#${hash}`);
360  }
361}
362
363/**
364 * Initialize mobile menu toggle functionality
365 */
366function initMobileMenuToggle(tabsContainer) {
367  const menuToggle = tabsContainer.querySelector('.side-nav .menu-toggle');
368  const sideNav = tabsContainer.querySelector('.side-nav');
369
370  if (!sideNav) {
371    // console.log('No side-nav found, skipping mobile menu toggle setup');
372    return;
373  }
374
375  // console.log('Setting up mobile menu toggle for:', { sideNav, menuToggle });
376
377  // Set up ARIA attributes for accessibility (if menu toggle exists)
378  if (menuToggle) {
379    menuToggle.setAttribute('role', 'button');
380    menuToggle.setAttribute('aria-expanded', 'false');
381    menuToggle.setAttribute('aria-controls', 'side-navigation');
382    sideNav.setAttribute('id', 'side-navigation');
383
384    // Toggle menu on menu-toggle click
385    menuToggle.addEventListener('click', (e) => {
386      e.preventDefault();
387      toggleSideNav(sideNav, menuToggle);
388    });
389  }
390
391  // Close menu on Escape key
392  document.addEventListener('keydown', (e) => {
393    if (e.key === 'Escape' && sideNav.classList.contains('active')) {
394      // console.log('Escape pressed, closing side nav');
395      closeSideNav(sideNav, menuToggle);
396    }
397  });
398
399  // Close menu when clicking outside
400  document.addEventListener('click', (e) => {
401    if (sideNav.classList.contains('active') &&
402      !sideNav.contains(e.target) &&
403      (!menuToggle || !menuToggle.contains(e.target))) {
404      // console.log('Clicked outside side nav, closing');
405      closeSideNav(sideNav, menuToggle);
406    }
407  });
408}
409
410function filterNavItems(items, excludeSelectors = []) {
411  if (!excludeSelectors.length) {
412    return items;
413  }
414
415  return items.filter((item) => !excludeSelectors.some((selector) => item.matches(selector)));
416}
417
418/**
419 * Toggle the side navigation menu
420 */
421function toggleSideNav(sideNav, menuToggle) {
422  const isActive = sideNav.classList.contains('active');
423
424  if (isActive) {
425    closeSideNav(sideNav, menuToggle);
426  } else {
427    openSideNav(sideNav, menuToggle);
428  }
429}
430
431/**
432 * Open the side navigation menu
433 */
434function openSideNav(sideNav, menuToggle) {
435  // console.log('openSideNav called with:', { sideNav, menuToggle });
436  // console.log('sideNav classes before:', sideNav.classList.toString());
437
438  sideNav.classList.add('active');
439
440  // Only set aria-expanded if menuToggle exists
441  if (menuToggle) {
442    menuToggle.setAttribute('aria-expanded', 'true');
443  }
444
445  // console.log('sideNav classes after:', sideNav.classList.toString());
446  // if (menuToggle) {
447  //   console.log('menuToggle aria-expanded:', menuToggle.getAttribute('aria-expanded'));
448  // }
449}
450
451/**
452 * Close the side navigation menu
453 */
454function closeSideNav(sideNav, menuToggle) {
455  // console.log('closeSideNav called', { sideNav, menuToggle });
456  sideNav.classList.remove('active');
457
458  // Only set aria-expanded if menuToggle exists
459  if (menuToggle) {
460    menuToggle.setAttribute('aria-expanded', 'false');
461  }
462
463  // console.log('sideNav classes after closing:', sideNav.classList.toString());
464}

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.