PageSourceSearch

https://www.travelcounsellors.co.za/scripts/optimization/global-navigation.js

js travelcounsellors.co.za collected 2026-09-29 21:56:43 UTC 20,299 bytes, 490 lines download raw bytes

1(() => {
2    const isMobile = () => window.innerWidth < 1100;
3
4    const handleOpenMenuMobile = () => {
5        const triggerButton = document.querySelector("#global-navigation-toggle")
6        const level1Menu = document.querySelector("#level-1-list")
7        const nav = document.querySelector("#global-navigation")
8        const hero = document.getElementById("hero-banner")
9
10        triggerButton.addEventListener("click", () => {
11            const isDefault = nav.getAttribute("data-default-appearance");
12
13            triggerButton.classList.toggle("active")
14
15
16            if (level1Menu.classList.contains("level-1-open")) {
17                document.body.style.overflow = "auto"
18                level1Menu.classList.remove("level-1-open");
19                if (isDefault == "True") return
20                setTimeout(() => {
21                    // Check if hero is still intersecting with navbar
22                    if (hero && nav) {
23                        const heroRect = hero.getBoundingClientRect();
24                        const navRect = nav.getBoundingClientRect();
25                        const isIntersecting = heroRect.bottom > navRect.top;
26
27                        if (isIntersecting) {
28                            nav.classList.add("global-navigation__immersive")
29                        }
30                    }
31                }, 30)
32            } else {
33                document.body.style.overflow = "hidden"
34                nav.classList.remove("global-navigation__immersive")
35                setTimeout(() => {
36                    level1Menu.classList.add("level-1-open");
37                }, 10)
38            }
39        })
40
41    }
42
43    const handleNavbarIntersection = () => {
44        const navbar = document.getElementById("global-navigation");
45        const hero = document.getElementById("hero-banner");
46        const level1Menu = document.querySelector("#level-1-list")
47
48
49
50        if (!hero || !navbar) return;
51
52        const observer = new IntersectionObserver(
53            (entries) => {
54                const entry = entries[0];
55
56                if (entry.isIntersecting) {
57                    // navbar is overlapping / intersecting the hero
58                    navbar.classList.add("global-navigation__immersive");
59                    navbar.setAttribute("data-default-appearance", "false")
60                } else {
61                    // navbar is no longer intersecting the hero
62                    navbar.classList.remove("global-navigation__immersive");
63                    navbar.setAttribute("data-default-appearance", "true")
64                }
65            },
66            {
67                root: null, // viewport
68                threshold: 0, // trigger as soon as hero goes out of view
69                rootMargin: `-${navbar.offsetHeight}px 0px 0px 0px`
70            }
71        );
72
73        // observe the hero section
74        observer.observe(hero);
75
76    }
77
78    // const handleNavAppearanceToggle = () => {
79    //     const nav = document.querySelector("#global-navigation")
80    //     const isDefault = nav.getAttribute("data-default-appearance");
81    //     if (isDefault == "true") return
82
83    //     else
84    //         nav.classList.toggle("global-navigation__immersive")
85    // }
86
87    const handleCLickOnLevel1Button = () => {
88        const buttons = document.querySelectorAll(".global-navigation .level-1-button");
89
90        buttons.forEach(btn => {
91            btn.addEventListener("click", () => {
92                const buttonMenu = btn.parentElement.querySelector(".menu")
93                const nav = document.querySelector("#global-navigation")
94                const isDefault = nav.getAttribute("data-default-appearance") === "True";
95                const hero = document.getElementById("hero-banner");
96
97                // Close any OTHER open level-1 menus
98                nav.querySelectorAll(".menu.show").forEach(openMenu => {
99                    if (openMenu !== buttonMenu) {
100                        openMenu.classList.remove("show");
101                        openMenu.querySelectorAll(".active-level, .show").forEach(el => {
102                            el.classList.remove("active-level");
103                            el.classList.remove("show");
104                        });
105                    }
106                });
107
108                // Clean up active states within the current nav item
109                const activeMenus = btn.parentElement.querySelectorAll(".active-level, div:not(.menu).show")
110
111                activeMenus.forEach(ele => {
112                    ele.classList.remove("active-level")
113                    ele.classList.remove("show")
114                })
115
116                if (buttonMenu.classList.contains("show")) {
117                    document.body.style.overflow = "auto"
118                    buttonMenu.classList.remove("show");
119                    if (isDefault) return
120                    setTimeout(() => {
121                        // Check if hero is still intersecting with navbar
122                        if (hero && nav) {
123                            const heroRect = hero.getBoundingClientRect();
124                            const navRect = nav.getBoundingClientRect();
125                            const isIntersecting = heroRect.bottom > navRect.top;
126                            
127                            if (isIntersecting) {
128                                nav.classList.add("global-navigation__immersive")
129                            }
130                        }
131                    }, 30)
132                } else {
133                    document.body.style.overflow = "hidden"
134                    nav.classList.remove("global-navigation__immersive")
135                    setTimeout(() => {
136                        buttonMenu.classList.add("show");
137                        const firstL2Btn = buttonMenu.querySelector('.global-navigation__level-2-list li button');
138                        if (firstL2Btn) {
139                            if (isMobile()) {
140                                const level2Items = buttonMenu.querySelectorAll('.global-navigation__level-2-list li.global-navigation__level-2-item');
141                                if (level2Items.length === 1) {
142                                    firstL2Btn.click();
143                                } else {
144                                    firstL2Btn.dispatchEvent(new Event('mouseenter'));
145                                }
146                            } else {
147                                firstL2Btn.dispatchEvent(new Event('mouseenter'));
148                            }
149                        }
150                    }, 10)
151                }
152            })
153        })
154    }
155
156    /**
157     * get level 2 back button
158     * add click listener on it 
159     * hide the menu if this button is clicked 
160     */
161    const handleClickOnLevel2Back = () => {
162        document.querySelectorAll(".global-navigation__level-2-list .mobile-menu-title svg").forEach(backBtn => {
163            backBtn.addEventListener("click", () => {
164                backBtn.closest(".menu").classList.remove('show')
165            });
166        });
167    }
168
169
170    /**
171     * get buttons of level 2 
172     * add mouseenter event on them
173     * get all level 3 and level 4 menus 
174     * if we are openeing new one we close all menues in level 3 and 4 and then open the menu in level 3 correponding to this element index 
175     * if we are clicking on the same one we close all and remove active-level class from clicked buttin
176     */
177    const handleClickOnLevel2Item = () => {
178        const btns = document.querySelectorAll(".global-navigation__level-2-list li.global-navigation__level-2-item button");
179
180        const showLevel2Submenu = (btn) => {
181            const level3Menus = btn.closest(".lists-container").querySelector(".level-3-lists-container")
182            const level4Menus = btn.closest(".lists-container").querySelector(".level-4-lists-container")
183            const siblings = Array.from(btn.parentElement.parentElement.children)
184
185            Array.from(level4Menus.children).forEach(group => {
186                group.classList.remove("show")
187                Array.from(group.children).forEach(child => child.classList.remove("show"))
188            })
189
190            const listIndex = btn.getAttribute("data-controls").split("-")[1];
191
192            Array.from(level3Menus.children).forEach(ele => ele.classList.remove("show"))
193            siblings.forEach(ele => ele.classList.remove("active-level"))
194            level3Menus.children[listIndex].classList.add("show")
195            btn.parentElement.classList.add("active-level")
196            level4Menus.children[listIndex].classList.add("show")
197        };
198
199        btns.forEach(btn => {
200            btn.addEventListener("mouseenter", () => {
201                if (isMobile()) return;
202                showLevel2Submenu(btn);
203            });
204
205            btn.addEventListener("click", (e) => {
206                if (!isMobile()) return;
207                e.preventDefault();
208                e.stopPropagation();
209                showLevel2Submenu(btn);
210            });
211        })
212    }
213
214
215    /**
216     * get level 3 buttons 
217     * add click listner
218     * if level is active we close its crosponding menu in level 4 
219     * if level is not active we close all the menus in level 4 and open the menu with the same index 
220     */
221    const handleClickOnLevel3Item = () => {
222        const btns = document.querySelectorAll(".global-navigation__level-3-list .global-navigation__level-3-item button");
223
224        const showLevel3Submenu = (btn) => {
225            const level4Menus = btn.closest(".lists-container").querySelector(".level-4-lists-container")
226            const activeGroup = level4Menus.querySelector(".level-4-group.show")
227            const listIndex = btn.getAttribute("data-controls").split("-")[1];
228
229            if (activeGroup) {
230                Array.from(activeGroup.children).forEach(ele => ele.classList.remove("show"))
231                activeGroup.children[listIndex].classList.add("show")
232            }
233            btn.parentElement.classList.add("active-level")
234        };
235
236        btns.forEach(btn => {
237            btn.addEventListener("mouseenter", () => {
238                if (isMobile()) return;
239                showLevel3Submenu(btn);
240            });
241
242            btn.addEventListener("click", (e) => {
243                if (!isMobile()) return;
244                e.preventDefault();
245                e.stopPropagation();
246                showLevel3Submenu(btn);
247            });
248        })
249    }
250
251    const handleClickBackFromUpperLevels = () => {
252        const btns = document.querySelectorAll(".global-navigation .mobile-menu-title svg");
253        btns.forEach(btn => {
254            btn.addEventListener("click", () => {
255                const parentContainer = btn.parentElement.parentElement;
256                parentContainer.classList.remove("show");
257
258                // If we're on mobile and backing out from level 3+, check if we should skip level-2
259                if (isMobile()) {
260                    const listsContainer = btn.closest(".lists-container");
261                    if (listsContainer) {
262                        const level2Items = listsContainer.querySelectorAll('.global-navigation__level-2-list li.global-navigation__level-2-item');
263                        // If there's only one level-2 item and we're not already at level-2, close the entire menu
264                        if (level2Items.length === 1 && !btn.closest(".global-navigation__level-2-list")) {
265                            const menu = btn.closest(".menu");
266                            if (menu) {
267                                menu.classList.remove("show");
268                            }
269                        }
270                    }
271                }
272            })
273        })
274    }
275
276
277    const getItemMedia = (item) => {
278        if (!item) return null
279        const mediaUrl = item.dataset.mediaLink;
280        const isVideo = item.dataset.isVideo === 'true' || item.dataset.isVideo === 'True';
281
282        return { mediaUrl, isVideo }
283    }
284
285    const getCurrentMedia = ({ image, video }) => {
286        if (!video.classList.contains("d-none")) {
287            return {
288                mediaUrl: video.querySelector("source").src,
289                isVideo: true,
290            };
291        }
292
293        if (!image.classList.contains("d-none")) {
294            return {
295                mediaUrl: image.src,
296                isVideo: false,
297            };
298        }
299
300        return null;
301    };
302
303
304    const handleControlMediaShown = ({ mediaUrl, isVideo, previewImage, previewVideo }) => {
305        const current = getCurrentMedia({ image: previewImage, video: previewVideo });
306
307        if (
308            current &&
309            current.mediaUrl.includes(mediaUrl) &&
310            current.isVideo === isVideo
311        ) {
312            return;
313        }
314
315        if (isVideo) {
316            previewVideo.querySelector('source').src = mediaUrl;
317            previewVideo.load();
318            previewVideo.classList.remove('d-none');
319            previewVideo.play();
320            previewImage.classList.add('d-none');
321            previewImage.src = ""
322        } else {
323            previewImage.src = mediaUrl;
324            previewImage.classList.remove('d-none');
325            previewVideo.classList.add('d-none');
326            previewVideo.querySelector('source').src = "";
327        }
328    }
329
330    const handleChangeMediaOnClick = (li) => {
331        if (li.querySelector(".level-1-link")) return // means that level 1 has no children
332        // main li 
333        const closestNavigationItem = li.closest(".global-navigation__nav-item")
334        // levels 
335        const level2ActiveTrigger = closestNavigationItem.querySelector(".lists-container .global-navigation__level-2-list .active-level")
336        const level3ActiveTrigger = closestNavigationItem.querySelector(".lists-container .level-3-lists-container .active-level")
337        // media container 
338        const mediaContainer = closestNavigationItem.querySelector('.global-navigation__media-container');
339        const previewVideo = mediaContainer.querySelector('.previewVideo');
340        const previewImage = mediaContainer.querySelector('.previewImage');
341
342        let isVideo = false;
343        let mediaUrl = "#";
344
345        if (level3ActiveTrigger) {
346            const { mediaUrl: itemMediaUrl, isVideo: isItemMediaTypeVideo } = getItemMedia(level3ActiveTrigger) || getItemMedia(level2ActiveTrigger) || getItemMedia(closestNavigationItem)
347            isVideo = isItemMediaTypeVideo;
348            mediaUrl = itemMediaUrl
349        } else if (level2ActiveTrigger) {
350            const { mediaUrl: itemMediaUrl, isVideo: isItemMediaTypeVideo } = getItemMedia(level2ActiveTrigger) || getItemMedia(closestNavigationItem)
351            isVideo = isItemMediaTypeVideo;
352            mediaUrl = itemMediaUrl
353        } else {
354            const { mediaUrl: itemMediaUrl, isVideo: isItemMediaTypeVideo } = getItemMedia(closestNavigationItem)
355            isVideo = isItemMediaTypeVideo;
356            mediaUrl = itemMediaUrl
357        }
358
359        handleControlMediaShown({ mediaUrl, isVideo, previewImage, previewVideo })
360
361    }
362
363    const handleAddClickListenerOnBtn = () => {
364        const buttons = document.querySelectorAll(".global-navigation__nav-item, .global-navigation__nav-item li")
365        buttons.forEach((btn) => {
366            btn.addEventListener("click", () => handleChangeMediaOnClick(btn))
367        })
368    }
369
370    const handleMouseOver = () => {
371        // All nav items
372        const navItems = document.querySelectorAll('.global-navigation__nav-item li');
373
374        navItems.forEach(item => {
375            item.addEventListener('mouseenter', () => {
376                const parentElement = item.closest(".global-navigation__nav-item")
377                const mediaContainer = parentElement.querySelector('.global-navigation__media-container');
378                const previewVideo = mediaContainer.querySelector('.previewVideo');
379                const previewImage = mediaContainer.querySelector('.previewImage');
380                const mediaUrl = item.dataset.mediaLink;
381                const isVideo = item.dataset.isVideo === 'true' || item.dataset.isVideo === 'True';
382
383                if (!mediaUrl) return;
384                handleControlMediaShown({ mediaUrl, isVideo, previewImage, previewVideo })
385            });
386        });
387    }
388
389    const handleMouseLeave = () => {
390        const navItems = document.querySelectorAll('.global-navigation__nav-item li');
391        navItems.forEach(item => {
392            item.addEventListener('mouseleave', () => {
393                handleChangeMediaOnClick(item)
394            });
395        });
396    };
397
398
399    const handleOpenContactModal = () => {
400        const button = document.querySelector(".nav-contact-modal-trigger");
401        if (!button) return;
402
403        button.addEventListener("click", () => {
404            openContactModal(button);
405        })
406    }
407
408    const pushNavTrackingEvent = (element) => {
409        if (!element || !window.GTM) return;
410        
411        const linkText = element.dataset.gtmLinkText || element.textContent?.trim() || '';
412        const linkUrl = element.dataset.gtmLinkUrl || element.href || '';
413        const navLevel = element.dataset.gtmNavLevel;
414        const navLevel1 = element.dataset.gtmNavLevel1 || null;
415        const navLevel2 = element.dataset.gtmNavLevel2 || null;
416        
417        const nav = document.querySelector("#global-navigation");
418        const pageType = nav?.dataset.model || 'unknown';
419
420        if (navLevel === '1') {
421            window.GTM.push({
422                event: 'select_content',
423                content_type: 'navigation',
424                content_id: 'header_nav',
425                link_text: linkText,
426                link_url: linkUrl,
427                nav_level_1: linkText,
428                page_section: 'header',
429                page_type: pageType
430            });
431        } else {
432            let navLevel3 = null;
433            
434            if (navLevel === '2') {
435                // Level 2: level1=parent, level2=clicked, level3=null
436            } else if (navLevel === '3') {
437                navLevel3 = linkText;
438            } else if (navLevel === '4') {
439                navLevel3 = navLevel2;
440            }
441
442            window.GTM.push({
443                event: 'select_content',
444                content_type: 'navigation',
445                content_id: 'header_nav_submenu',
446                link_text: linkText,
447                link_url: linkUrl,
448                nav_level_1: navLevel1,
449                nav_level_2: navLevel === '2' ? linkText : navLevel2,
450                nav_level_3: navLevel3,
451                page_section: 'header',
452                page_type: pageType
453            });
454        }
455    }
456
457    const handleNavigationTracking = () => {
458        const trackingItems = document.querySelectorAll('.nav-tracking-item a, .nav-tracking-item button');
459        
460        trackingItems.forEach(element => {
461            element.addEventListener('click', () => {
462                const trackingItem = element.closest('.nav-tracking-item');
463                if (trackingItem) {
464                    pushNavTrackingEvent(trackingItem);
465                }
466            });
467        });
468    }
469
470    // Initialize dropdowns after page load
471    window.addEventListener("load", () => {
472        handleNavbarIntersection();
473        handleOpenMenuMobile();
474        handleCLickOnLevel1Button();
475        handleClickOnLevel2Back();
476        handleClickOnLevel2Item();
477        handleClickOnLevel3Item();
478        handleClickBackFromUpperLevels();
479        handleMouseOver();
480        handleMouseLeave();
481        handleAddClickListenerOnBtn();
482        addFallBackImage({
483            selector: ".global-navigation__TC-image",
484            fallbackUrl: "/assets/profile_placeholder_medium.png"
485        })
486        handleOpenContactModal();
487        handleNavigationTracking();
488    });
489
490})()

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.