PageSourceSearch

https://kagi.com/_mkt/js/marketing-header.js

js kagi.com collected 2026-09-24 08:43:36 UTC 4,464 bytes, 171 lines download raw bytes

1(() => {
2	const SELECTORS = {
3		header: '[data-marketing-header]',
4		menuButton: '[data-menu-toggle]',
5		nav: '[data-marketing-nav]',
6		dropdown: '[data-nav-dropdown]',
7		dropdownButton: '[data-dropdown-toggle]'
8	};
9
10	const CLASSES = {
11		menuOpen: 'is-menu-open',
12		dropdownOpen: 'is-open',
13		bodyMenuOpen: 'marketing-menu-open'
14	};
15
16	const mobileMediaQuery = window.matchMedia('(max-width: 991px)');
17	const hoverMediaQuery = window.matchMedia('(hover: hover) and (pointer: fine)');
18
19	const initMarketingHeader = () => {
20		const header = document.querySelector(SELECTORS.header);
21		if (!(header instanceof HTMLElement)) {
22			return;
23		}
24
25		const menuButton = header.querySelector(SELECTORS.menuButton);
26		const nav = header.querySelector(SELECTORS.nav);
27		const dropdowns = Array.from(document.querySelectorAll(SELECTORS.dropdown)).filter(
28			(dropdown) => dropdown instanceof HTMLElement
29		);
30
31		const setDropdownOpen = (dropdown, isOpen) => {
32			const button = dropdown.querySelector(SELECTORS.dropdownButton);
33			if (!(button instanceof HTMLElement)) {
34				return;
35			}
36
37			dropdown.classList.toggle(CLASSES.dropdownOpen, isOpen);
38			if (dropdown instanceof HTMLDetailsElement) {
39				dropdown.open = isOpen;
40			}
41			button.setAttribute('aria-expanded', String(isOpen));
42		};
43
44		const closeDropdowns = (exceptDropdown = null) => {
45			dropdowns.forEach((dropdown) => {
46				if (dropdown === exceptDropdown) {
47					return;
48				}
49
50				setDropdownOpen(dropdown, false);
51			});
52		};
53
54		const setMenuOpen = (isOpen) => {
55			header.classList.toggle(CLASSES.menuOpen, isOpen);
56			document.body.classList.toggle(CLASSES.bodyMenuOpen, isOpen);
57
58			if (menuButton instanceof HTMLButtonElement) {
59				menuButton.setAttribute('aria-expanded', String(isOpen));
60			}
61
62			if (!isOpen) {
63				closeDropdowns();
64			}
65		};
66
67		dropdowns.forEach((dropdown) => {
68			setDropdownOpen(dropdown, false);
69
70			const button = dropdown.querySelector(SELECTORS.dropdownButton);
71			if (!(button instanceof HTMLElement)) {
72				return;
73			}
74
75			button.addEventListener('click', (event) => {
76				// The same handler drives buttons and native disclosure summaries.
77				if (dropdown instanceof HTMLDetailsElement) event.preventDefault();
78				const isOpen = dropdown.classList.contains(CLASSES.dropdownOpen);
79				closeDropdowns(dropdown);
80				setDropdownOpen(dropdown, !isOpen);
81			});
82
83			dropdown.addEventListener('mouseenter', () => {
84				if (!hoverMediaQuery.matches) {
85					return;
86				}
87
88				closeDropdowns(dropdown);
89				setDropdownOpen(dropdown, true);
90			});
91
92			dropdown.addEventListener('mouseleave', () => {
93				if (!hoverMediaQuery.matches) {
94					return;
95				}
96
97				const activeElement = document.activeElement;
98				if (activeElement instanceof HTMLElement && dropdown.contains(activeElement)) {
99					return;
100				}
101
102				setDropdownOpen(dropdown, false);
103			});
104
105			dropdown.addEventListener('focusout', (event) => {
106				const nextFocused = event.relatedTarget;
107				// Safari may blur the trigger without focusing a tapped link. Closing
108				// here would hide the link before its click. Only a known focus
109				// destination outside is an exit; outside clicks and Escape still close.
110				if (!(nextFocused instanceof Node) || dropdown.contains(nextFocused)) {
111					return;
112				}
113
114				setDropdownOpen(dropdown, false);
115			});
116		});
117
118		if (menuButton instanceof HTMLButtonElement) {
119			menuButton.addEventListener('click', () => {
120				setMenuOpen(!header.classList.contains(CLASSES.menuOpen));
121			});
122		}
123
124		if (nav instanceof HTMLElement) {
125			nav.addEventListener('click', (event) => {
126				if (!mobileMediaQuery.matches) {
127					return;
128				}
129
130				const target = event.target;
131				if (!(target instanceof Element) || !target.closest('a')) {
132					return;
133				}
134
135				setMenuOpen(false);
136			});
137		}
138
139		document.addEventListener('click', (event) => {
140			const target = event.target;
141			if (
142				target instanceof Node &&
143				(header.contains(target) || dropdowns.some((dropdown) => dropdown.contains(target)))
144			) {
145				return;
146			}
147
148			setMenuOpen(false);
149		});
150
151		document.addEventListener('keydown', (event) => {
152			if (event.key !== 'Escape') {
153				return;
154			}
155
156			setMenuOpen(false);
157		});
158
159		mobileMediaQuery.addEventListener('change', (event) => {
160			if (!event.matches) {
161				setMenuOpen(false);
162			}
163		});
164	};
165
166	if (document.readyState === 'loading') {
167		document.addEventListener('DOMContentLoaded', initMarketingHeader, { once: true });
168	} else {
169		initMarketingHeader();
170	}
171})();

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.