PageSourceSearch

https://fastspring.com/wp-content/themes/fastspring-bamboo/assets/dist/js/new_navigation.js?ver=1790786653

js fastspring.com collected 2026-10-01 07:36:32 UTC 18,185 bytes, 466 lines download raw bytes

1// ============================================================================
2// ALTERNATE NAVIGATION ACCESSIBILITY ENHANCEMENTS
3// ============================================================================
4// This code duplicates the functionality above but adds accessibility features
5// specifically for the alternate navigation (.nav-bamboo.alternate)
6// ============================================================================
7
8document.addEventListener("DOMContentLoaded", function() {
9    // Early exit if navigation is disabled or if alternate navigation doesn't exist
10    if (window.fsNavDisabled) return;
11    
12    const alternateNav = document.querySelector('.nav-bamboo.alternate');
13    if (!alternateNav) return; // Only run this code if the alternate navigation exists
14    
15    // Add visually-hidden class if it doesn't exist
16    if (!document.querySelector('style#accessibility-styles')) {
17      const style = document.createElement('style');
18      style.id = 'accessibility-styles';
19      style.textContent = `
20        .visually-hidden {
21          position: absolute;
22          width: 1px;
23          height: 1px;
24          margin: -1px;
25          padding: 0;
26          overflow: hidden;
27          clip: rect(0, 0, 0, 0);
28          border: 0;
29        }
30        
31        /* Focus styles for keyboard navigation */
32        .nav-bamboo.alternate a:focus,
33        .nav-bamboo.alternate button:focus {
34          outline: 2px solid #4A90E2; 
35          outline-offset: 2px;
36        }
37
38        /* Remove focus styles for mouse users in browsers that support :focus-visible */
39        .nav-bamboo.alternate a:focus:not(:focus-visible),
40        .nav-bamboo.alternate button:focus:not(:focus-visible) {
41          outline: none;
42        }
43
44        /* Apply focus styles for keyboard navigation where :focus-visible is supported */
45        .nav-bamboo.alternate a:focus-visible,
46        .nav-bamboo.alternate button:focus-visible {
47          outline: 2px solid #4A90E2;
48          outline-offset: 2px;
49        }
50        
51      `;
52      document.head.appendChild(style);
53    }
54  
55    // State management variables
56    let isAlternateDropdownContainerShowing = false;
57    let isAlternateSearchContainerShowing = false;
58  
59    // DOM elements
60    const alternateSearchContainer = document.getElementById("nav-bamboo-search-container");
61    const alternateSearchTrigger = alternateNav.querySelector('#trigger-search');
62    const alternateHamburgerToggle = alternateNav.querySelector('#nav-bamboo-toggle');
63    
64    // Create a namespace for alternate navigation functions
65    window.fsAltNav = {
66      dropdownContainer: document.getElementById("nav-bamboo-dropdown-container"),
67      currentDropdownId: null,
68      lastInteractionWasKeyboard: false,
69      
70      setOffsetAndSize(dropdownEl, leftPx, centerPx) {
71        const dataWidth = parseInt(dropdownEl.getAttribute('data-width')) || 0;
72        const align = dropdownEl.getAttribute('data-align');
73        const correct = parseInt(dropdownEl.getAttribute('data-correct')) || 0;
74        const newOffset = align === "left" ? leftPx + correct : align === "center" ? centerPx - (dataWidth / 2) : 0;
75  
76        dropdownEl.style.width = `${dataWidth}px`;
77        dropdownEl.style.left = `${newOffset}px`;
78      },
79      
80      showDropdownContainer(clickedLink, dropdownId, leftPx, centerPx) {
81        resetAlternateTriggerStates();
82        clickedLink.classList.add('open');
83        
84        // Update ARIA attributes for accessibility
85        clickedLink.setAttribute('aria-expanded', 'true');
86        
87        hideAlternateSearchContainer();
88        fsAltNav.hideAllVisibleDropdowns();
89  
90        const dropdown = document.getElementById(dropdownId);
91        if (dropdown) {
92          dropdown.classList.add('visible');
93          this.setOffsetAndSize(dropdown, leftPx, centerPx);
94  
95          // Add mouseleave event listener to close the dropdown when mouse exits
96          dropdown.addEventListener('mouseleave', () => {
97            dropdown.classList.remove('visible');
98            isAlternateDropdownContainerShowing = false;
99            
100            // Update ARIA attributes
101            clickedLink.setAttribute('aria-expanded', 'false');
102          }, { once: true }); // Ensures the listener only triggers once
103        }
104  
105        if (!isAlternateDropdownContainerShowing) {
106          this.dropdownContainer.classList.add('visible');
107          isAlternateDropdownContainerShowing = true;
108        }
109        
110        // Only focus the first item if the dropdown was triggered by keyboard
111        if (this.lastInteractionWasKeyboard && dropdown) {
112          setTimeout(() => {
113            const firstItem = dropdown.querySelector('.dropdown-links a');
114            if (firstItem) {
115              firstItem.focus();
116            } else {
117              console.error('[FS] Navigation: no focusable item found in dropdown', dropdownId);
118            }
119          }, 100);
120        }
121      },
122         
123      hideAllVisibleDropdowns() {
124        document.querySelectorAll(".nav-bamboo-dropdown.visible").forEach(dropdown => {
125          dropdown.classList.remove('visible');
126          
127          // Find the associated trigger and update its ARIA attributes
128          const dropdownId = dropdown.id.replace('dropdown-', '');
129          const trigger = alternateNav.querySelector(`[data-dropdown="${dropdownId}"]`);
130          if (trigger) {
131            trigger.setAttribute('aria-expanded', 'false');
132          }
133        });
134      },
135    };
136        
137    // First, define the handleAlternateDropdown function
138    const handleAlternateDropdown = (event) => {
139      event.preventDefault();
140      const clickedLink = event.target.closest('.trigger-dropdown');
141      if (!clickedLink) return;
142      
143      const dropdownId = clickedLink.getAttribute('data-dropdown');
144      const eventLabel = clickedLink.getAttribute('data-event') || "No Label Specified";
145  
146      if (dropdownId === "signin" && window.outerWidth < 1222) {
147        location.href = "https://app.fastspring.com/";
148      } else {
149        const rect = clickedLink.getBoundingClientRect();
150        const centerPx = Math.round(rect.left + rect.width / 2);
151        const leftPx = Math.round(rect.left);
152        const dropdownSelector = `dropdown-${dropdownId}`;
153        
154        fsAltNav.showDropdownContainer(clickedLink, dropdownSelector, leftPx, centerPx);
155        fsAltNav.currentDropdownId = dropdownId;
156      }
157    };
158  
159    // Function to reset trigger states by removing the 'open' class
160    const resetAlternateTriggerStates = () => {
161      alternateDropdownTriggers.forEach(trigger => {
162        trigger.classList.remove('open');
163        trigger.setAttribute('aria-expanded', 'false');
164      });
165    };
166  
167    // Function to hide the dropdown container
168    const hideAlternateDropdownContainer = () => {
169      fsAltNav.dropdownContainer.classList.remove('visible');
170      alternateHamburgerToggle.classList.remove('open');
171      alternateHamburgerToggle.setAttribute('aria-expanded', 'false');
172      isAlternateDropdownContainerShowing = false;
173    };
174  
175    // Function to hide the search container
176    const hideAlternateSearchContainer = () => {
177      if (alternateSearchContainer) {
178        alternateSearchContainer.classList.remove("visible");
179      }
180      isAlternateSearchContainerShowing = false;
181      if (alternateSearchTrigger) {
182        alternateSearchTrigger.classList.remove('open');
183        alternateSearchTrigger.setAttribute('aria-expanded', 'false');
184      }
185    };
186  
187    // Function to close and reset dropdowns
188    const closeAndResetAlternateDropdowns = () => {
189      fsAltNav.lastInteractionWasKeyboard = false;
190      hideAlternateDropdownContainer();
191      fsAltNav.hideAllVisibleDropdowns();
192      resetAlternateTriggerStates();
193      hideAlternateSearchContainer();
194    };
195  
196    // Get all dropdown triggers in the alternate navigation
197    const alternateDropdownTriggers = alternateNav.querySelectorAll(".trigger-dropdown");
198  
199    // Function to add event listeners to dropdown triggers
200    const addAlternateDropdownEventListeners = (trigger) => {
201      // Handle click event
202      trigger.addEventListener("click", (event) => {
203        fsAltNav.lastInteractionWasKeyboard = false;
204        handleAlternateDropdown(event);
205      });
206
207      // Handle mouseover event for wider screens
208      trigger.addEventListener("mouseover", (event) => {
209        if (window.outerWidth >= 1222) {
210          fsAltNav.lastInteractionWasKeyboard = false;
211          handleAlternateDropdown(event);
212        }
213      });
214
215      // Handle keyboard events for accessibility
216      trigger.addEventListener("keydown", (event) => {
217        // Open dropdown on Enter or Space
218        if (event.key === 'Enter' || event.key === ' ') {
219          event.preventDefault();
220          fsAltNav.lastInteractionWasKeyboard = true;
221          handleAlternateDropdown(event);
222        }
223      });
224    };
225  
226    // Loop through dropdown triggers and set up event listeners
227    alternateDropdownTriggers.forEach(addAlternateDropdownEventListeners);
228      
229    // Get all top-level links and add event listeners
230    const alternateTopLevelLinks = alternateNav.querySelectorAll(".top-level-link");
231  
232    // Add mouseover event to top-level links to reset dropdowns
233    alternateTopLevelLinks.forEach(link => {
234      link.addEventListener("mouseover", closeAndResetAlternateDropdowns);
235    });
236  
237    // Event listener to handle mouse movement outside of the header
238    document.addEventListener("mousemove", (event) => {
239      if (window.outerWidth >= 1222) {
240        const isMouseInside = alternateNav.contains(event.target);
241        if (!isMouseInside) {
242          closeAndResetAlternateDropdowns();
243        }
244      }
245    });
246      
247    // Event listener to close dropdowns when mouse leaves the window
248    document.addEventListener("mouseleave", (event) => {
249      const isOutsideViewport = event.clientY <= 0 || event.clientX <= 0 || 
250                                event.clientX >= window.innerWidth || event.clientY >= window.innerHeight;
251  
252      if (window.outerWidth >= 1222 && isOutsideViewport) {
253        closeAndResetAlternateDropdowns();
254      }
255    });
256      
257    // Set up mobile toggle button
258    if (alternateHamburgerToggle) {
259      alternateHamburgerToggle.addEventListener("click", (event) => {
260        if (!isAlternateDropdownContainerShowing) {
261          fsAltNav.showDropdownContainer(event.target, '');
262          event.target.classList.add('open');
263          event.target.setAttribute('aria-expanded', 'true');
264        } else {
265          hideAlternateDropdownContainer();
266          event.target.classList.remove('open');
267          event.target.setAttribute('aria-expanded', 'false');
268        }
269      });
270    }
271      
272    // Set up the collapsing / expanding mobile nav sections
273    const alternateMobileDropdownTitles = alternateNav.querySelectorAll(".has-sub-items");
274      
275    // Function to toggle the dropdown visibility
276    const toggleAlternateDropdown = (event) => {
277      event.preventDefault();
278      const clickedElement = event.target.querySelector('.has-sub-items') ||
279                           event.target.parentElement.querySelector('.has-sub-items');
280
281      if (!clickedElement) return; // Safeguard in case closest fails
282
283      // Determine if we're opening (not currently expanded)
284      const wasExpanded = clickedElement.getAttribute('aria-expanded') === 'true';
285      const isOpening = !wasExpanded;
286
287      // Toggle 'open' class on the clicked element
288      clickedElement.classList.toggle('open');
289
290      // Toggle aria-expanded attribute
291      clickedElement.setAttribute('aria-expanded', isOpening);
292
293      // Toggle visibility of the target menus
294      const targetClass = clickedElement.getAttribute('data-dropdown');
295      const targetMenus = document.querySelectorAll(`.${targetClass}`);
296
297      targetMenus.forEach(menu => {
298        const isVisible = menu.getAttribute('data-visible') === "true";
299        menu.classList.toggle('visible', !isVisible);
300        menu.setAttribute('data-visible', !isVisible);
301      });
302
303      // Focus first link in the expanded menu if opened via keyboard
304      if (isOpening && fsAltNav.lastInteractionWasKeyboard) {
305        setTimeout(() => {
306          const firstLink = targetMenus[0] && targetMenus[0].querySelector('a');
307          if (firstLink) {
308            firstLink.focus();
309          }
310        }, 100);
311      }
312    };
313
314    // Add click event listeners to each mobile dropdown title
315    alternateMobileDropdownTitles.forEach(title => {
316      title.addEventListener("click", (event) => {
317        fsAltNav.lastInteractionWasKeyboard = false;
318        toggleAlternateDropdown(event);
319      });
320
321      // Add keyboard event listener for accessibility
322      title.addEventListener("keydown", (event) => {
323        if (event.key === 'Enter' || event.key === ' ') {
324          event.preventDefault();
325          fsAltNav.lastInteractionWasKeyboard = true;
326          toggleAlternateDropdown(event);
327        }
328      });
329    });
330  
331    // Search trigger keyboard support
332    if (alternateSearchTrigger) {
333      alternateSearchTrigger.addEventListener('keydown', (event) => {
334        if (event.key === 'Enter' || event.key === ' ') {
335          event.preventDefault();
336          alternateSearchTrigger.click();
337
338          // Poll for the search input which renders asynchronously
339          let attempts = 0;
340          const maxAttempts = 20;
341          const pollInterval = 150;
342          const focusSearchInput = setInterval(() => {
343            attempts++;
344            const searchInput = document.querySelector('.ais-SearchBox-input');
345            if (searchInput) {
346              clearInterval(focusSearchInput);
347              searchInput.focus();
348            } else if (attempts >= maxAttempts) {
349              clearInterval(focusSearchInput);
350              console.error('[FS] Navigation: search input not found after polling', {
351                page: window.location.pathname,
352                attempts: maxAttempts
353              });
354            }
355          }, pollInterval);
356        }
357      });
358
359      // Escape key handler on search container to close and return focus
360      if (alternateSearchContainer) {
361        alternateSearchContainer.addEventListener('keydown', (event) => {
362          if (event.key === 'Escape') {
363            event.preventDefault();
364            hideAlternateSearchContainer();
365            alternateSearchTrigger.focus();
366          }
367        });
368      }
369    }
370
371    // Close all dropdowns when hovering over the sign-up button
372    const alternateSignUpButton = alternateNav.querySelector('#main-nav-sign-up a');
373    if (alternateSignUpButton) {
374      alternateSignUpButton.addEventListener("mouseover", () => {
375        fsAltNav.hideAllVisibleDropdowns();
376      });
377    }
378    
379    // Handle keyboard navigation within dropdowns
380    document.addEventListener('keydown', (event) => {
381      // Only process if we're in a visible dropdown
382      const activeDropdown = document.querySelector('.nav-bamboo-dropdown.visible');
383      if (!activeDropdown) return;
384      
385      // Only process if the dropdown is related to the alternate navigation
386      const dropdownId = activeDropdown.id.replace('dropdown-', '');
387      const trigger = alternateNav.querySelector(`[data-dropdown="${dropdownId}"]`);
388      if (!trigger) return;
389      
390      // Get all focusable link elements in the dropdown
391      const menuItems = activeDropdown.querySelectorAll('.dropdown-links a');
392      if (!menuItems.length) return;
393      
394      // Find the currently focused item
395      const focusedItem = document.activeElement;
396      const focusedIndex = Array.from(menuItems).indexOf(focusedItem);
397      
398      switch (event.key) {
399        case 'ArrowDown':
400          event.preventDefault();
401          // Focus the next item, or the first if at the end
402          const nextIndex = (focusedIndex + 1) % menuItems.length;
403          menuItems[nextIndex].focus();
404          break;
405          
406        case 'ArrowUp':
407          event.preventDefault();
408          // Focus the previous item, or the last if at the beginning
409          const prevIndex = (focusedIndex - 1 + menuItems.length) % menuItems.length;
410          menuItems[prevIndex].focus();
411          break;
412          
413        case 'Escape':
414          event.preventDefault();
415          // Close the dropdown fully and return focus to the trigger
416          closeAndResetAlternateDropdowns();
417          trigger.focus();
418          break;
419          
420        case 'Tab':
421          // If tabbing out of the last item, close the dropdown
422          if (!event.shiftKey && focusedIndex === menuItems.length - 1) {
423            setTimeout(() => {
424              closeAndResetAlternateDropdowns();
425            }, 0);
426          }
427          // If shift+tabbing out of the first item, close the dropdown
428          else if (event.shiftKey && focusedIndex === 0) {
429            setTimeout(() => {
430              closeAndResetAlternateDropdowns();
431            }, 0);
432          }
433          break;
434      }
435    });
436    
437    // Add ARIA attributes to all dropdowns in the alternate navigation
438    document.querySelectorAll('.nav-bamboo-dropdown').forEach(dropdown => {
439      const dropdownId = dropdown.id.replace('dropdown-', '');
440      const trigger = alternateNav.querySelector(`[data-dropdown="${dropdownId}"]`);
441      
442      // Only process dropdowns that are triggered from the alternate navigation
443      if (trigger) {
444        // Add attributes to the dropdown
445        dropdown.setAttribute('aria-labelledby', `trigger-${dropdownId}`);
446        
447        // Add ID to the trigger if it doesn't have one
448        if (!trigger.id) {
449          trigger.id = `trigger-${dropdownId}`;
450        }
451        
452        // Add attributes to the trigger
453        trigger.setAttribute('aria-expanded', 'false');
454        trigger.setAttribute('aria-controls', dropdown.id);
455        
456        // Add aria-hidden to SVG icons
457        trigger.querySelectorAll('svg').forEach(svg => {
458          svg.setAttribute('aria-hidden', 'true');
459        });
460        
461        dropdown.querySelectorAll('svg').forEach(svg => {
462          svg.setAttribute('aria-hidden', 'true');
463        });
464      }
465    });
466  });

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.