PageSourceSearch

https://earthx.org/wp-content/themes/earthx2026/js/main.js?ver=1.0.0

js earthx.org collected 2026-10-02 04:20:37 UTC 12,101 bytes, 377 lines download raw bytes

1$ = jQuery.noConflict(); //makes use of the $ sign for jQuery in WP
2
3/**
4 * Initialize all Swiper sliders on the page.
5 * Each slider must have `data-swiper` on the root element.
6 * Pass per-slider config via `data-options='{"autoplay":{"delay":5000}}'`.
7 */
8
9document.addEventListener('DOMContentLoaded', function () {
10	document.querySelectorAll('[data-swiper]').forEach(function (root) {
11		let parsed = {};
12		try {
13			parsed = JSON.parse(root.getAttribute('data-options') || '{}');
14		} catch (e) {}
15
16		new Swiper(root, Object.assign({
17			loop: true,
18			pagination: { el: root.querySelector('.swiper-pagination'), clickable: true },
19			navigation: {
20				nextEl: root.querySelector('.swiper-button-next'),
21				prevEl: root.querySelector('.swiper-button-prev')
22			}
23		}, parsed));
24	});
25});
26
27// ------------------------------------------------
28// ACCORDION FUNCTION
29// ------------------------------------------------
30
31jQuery(document).ready(function($) {
32		// Add a click event to the accordion titles
33		$('.accordion-toggle').click(function(event) {
34				// Prevent the default behavior of anchor links if used
35				event.preventDefault();
36
37				// Toggle the 'active-tab' class on the clicked accordion title
38				$(this).toggleClass('active-tab');
39
40				// Toggle the visibility of the corresponding accordion content
41				$(this).closest('.accordion-item').find('.accordion-content').toggleClass("open").slideToggle();
42		});
43});
44
45// ------------------------------------------------
46// TABS FUNCTION
47// ------------------------------------------------
48document.addEventListener('DOMContentLoaded', function () {
49	const esc = (s) => (window.CSS && CSS.escape ? CSS.escape(s) : String(s).replace(/([ #.;?+*~':"!^$[\](){}/|-])/g, '\\$1'));
50
51	document.querySelectorAll('.tab-menu').forEach(function (menu) {
52		const links = Array.from(menu.querySelectorAll('a[href^="#"]'));
53		if (!links.length) return;
54
55		// Panels = only those IDs referenced by this menu's links
56		const panels = links
57			.map(a => document.getElementById((a.getAttribute('href') || '').slice(1)))
58			.filter(Boolean);
59
60		if (!panels.length) return;
61
62		function show(idx) {
63			links.forEach(a => a.classList.remove('active'));
64			panels.forEach(p => { p.style.display = 'none'; p.classList.remove('is-active'); });
65
66			const link  = links[idx] || links[0];
67			const id    = (link.getAttribute('href') || '').slice(1);
68			const panel = document.getElementById(esc(id));
69			if (!panel) return;
70
71			link.classList.add('active');
72			panel.style.display = 'block';
73			panel.classList.add('is-active');
74
75			// Refresh only Swiper(s) inside this panel
76			panel.querySelectorAll('.swiper').forEach(el => el.swiper && el.swiper.update());
77		}
78
79		// Start: prefer a link already marked .active, else URL hash match, else first
80		let start = links.findIndex(a => a.classList.contains('active'));
81		if (start < 0 && location.hash) {
82			start = panels.findIndex(p => ('#' + p.id) === location.hash);
83		}
84		if (start < 0) start = 0;
85		show(start);
86
87		// Clicks for THIS menu only
88		menu.addEventListener('click', function (e) {
89			const a = e.target.closest('a[href^="#"]');
90			if (!a || !menu.contains(a)) return;
91			const idx = links.indexOf(a);
92			if (idx === -1) return;
93			e.preventDefault();
94			show(idx);
95		});
96	});
97});
98
99
100// Mobile Menu Toggle
101document.addEventListener('DOMContentLoaded', () => {
102	const btn = document.querySelector('.mobile-menu-btn');
103	const nav = document.querySelector('.site-nav');
104	if (!btn) return;
105
106	btn.addEventListener('click', () => {
107		const isActive = btn.classList.toggle('is-active');
108		btn.setAttribute('aria-expanded', String(isActive));
109		nav.classList.toggle('open');
110	});
111});
112
113// Counters
114document.addEventListener('DOMContentLoaded', () => {
115	const els = [...document.querySelectorAll('.countdown[data-target]')];
116	if (!els.length) return;
117
118	const pad = n => String(n).padStart(2, '0');
119
120	const getParts = ms => {
121		if (ms <= 0) {
122			return { d: '00', h: '00', m: '00', s: '00', done: true };
123		}
124		const s = Math.floor(ms / 1000);
125		const d = Math.floor(s / 86400);
126		const h = Math.floor((s % 86400) / 3600);
127		const m = Math.floor((s % 3600) / 60);
128		const sec = s % 60;
129		return { d: pad(d), h: pad(h), m: pad(m), s: pad(sec), done: false };
130	};
131
132	// Ensure each countdown element has structure with labels
133	els.forEach(el => {
134		if (!el.querySelector('.countdown-part')) {
135			el.innerHTML = `
136				<span class="countdown-part days">
137					<span class="value"></span>
138					<span class="label">Days</span>
139				</span>
140				<span class="countdown-part hours">
141					<span class="value"></span>
142					<span class="label">Hours</span>
143				</span>
144				<span class="countdown-part minutes">
145					<span class="value"></span>
146					<span class="label">Minutes</span>
147				</span>
148				<span class="countdown-part seconds">
149					<span class="value"></span>
150					<span class="label">Seconds</span>
151				</span>
152			`;
153		}
154	});
155
156	const tick = () => {
157		const now = Date.now();
158		els.forEach(el => {
159			const t = Date.parse(el.dataset.target);
160			if (Number.isNaN(t)) return;
161			const { d, h, m, s, done } = getParts(t - now);
162
163			el.querySelector('.days .value').textContent = d;
164			el.querySelector('.hours .value').textContent = h;
165			el.querySelector('.minutes .value').textContent = m;
166			el.querySelector('.seconds .value').textContent = s;
167
168			if (done) el.classList.add('is-complete');
169		});
170	};
171
172	tick();
173	const interval = setInterval(() => {
174		tick();
175		if (els.every(el => el.classList.contains('is-complete'))) clearInterval(interval);
176	}, 1000);
177});
178
179// Scroll top header out of view and back into view
180document.addEventListener('DOMContentLoaded', () => {
181	const topContent = document.querySelector('.site-header-top-content');
182	if (!topContent) return;
183
184	const fullHeight = topContent.offsetHeight; // capture its natural height once
185
186	const update = () => {
187		if (window.scrollY > 0) {
188			topContent.style.height = '0';
189			topContent.style.overflow = 'hidden';
190			topContent.style.transition = 'height 0.2s ease';
191		} else {
192			topContent.style.height = fullHeight + 'px';
193			topContent.style.transition = 'height 0.2s ease';
194		}
195	};
196
197	update(); // run on load
198	window.addEventListener('scroll', update);
199});
200
201// Read More Button For Text-Image Section
202document.addEventListener('DOMContentLoaded', () => {
203	document.addEventListener('click', (e) => {
204		const btn = e.target.closest('.read-more-btn');
205		const btnText = e.target.closest('.read-more-btn > span');
206		if (!btn) return;
207
208		const block = btn.closest('.text-image-text .body-text');
209		if (!block) return;
210
211		const expanded = block.classList.toggle('expanded');
212		btn.setAttribute('aria-expanded', String(expanded));
213		btnText.textContent = expanded ? 'Read less' : 'Read more';
214	});
215});
216
217// Site Search
218document.addEventListener("DOMContentLoaded", () => {
219	const toggleBtn = document.querySelector(".site-search-toggle");
220	const searchForm = document.querySelector("#search-form");
221
222	if (toggleBtn && searchForm) {
223		toggleBtn.addEventListener("click", () => {
224			searchForm.classList.toggle("is-visible");
225		});
226	}
227});
228
229// ------------------------------------------------
230// ADD BACKGROUND GRADIENT TO EVERY OTHER SECTION
231// ------------------------------------------------
232
233document.addEventListener('DOMContentLoaded', () => {
234	const sections = Array.from(document.querySelectorAll('section'));
235	if (!sections.length) return;
236
237	// Clean slate
238	sections.forEach(s => s.classList.remove('gradient-bg'));
239
240	// Find the first hero section (class="hero-slider")
241	const heroIdx = sections.findIndex(s => s.classList.contains('hero-slider'));
242
243	// Start index: if hero exists, skip hero and the next section; otherwise start at 0
244	const start = heroIdx !== -1 ? heroIdx + 2 : 0;
245
246	// Add gradient to every other section from 'start', never to hero sections
247	for (let i = start; i < sections.length; i += 2) {
248		if (!sections[i].classList.contains('hero-slider')) {
249			sections[i].classList.add('gradient-bg');
250		}
251	}
252});
253
254// ------------------------------------------------
255// BLOG FUNCTIONS
256// ------------------------------------------------
257
258// Read timer for blog posts
259if (document.getElementById("post-copy")) {
260	function readingTime() {
261		const text = document.getElementById("post-copy").innerText;
262		const wpm = 265;
263		const words = text.trim().split(/\s+/).length;
264		const time = Math.ceil(words / wpm);
265		document.getElementById("time-read").innerText = time;
266	}
267	readingTime();
268}
269
270// wrap images and fig captions in a div within blog post content
271$(".post-content img,.post-content figure").each(function(index, element) {
272		$(element).wrap("<div class='post-img'></div>");
273});
274
275// ------------------------------------------------
276// MODAL FUNCTIONS
277// ------------------------------------------------
278
279// Add data-fancybox attribute to pop-up/modal elements
280// Then initialize fancybox elements, which allows any element with that attribute
281// to open its target inside a fancybox modal
282
283document.addEventListener('DOMContentLoaded', () => {
284	document.querySelectorAll('.popup-form a').forEach(a => {
285		a.setAttribute('data-fancybox', '');
286	});
287	// Pricing cards modal buttons
288	const pricingCardsModalScope = document.querySelector('.cards-pricing-with-modal');
289	if (pricingCardsModalScope) {
290		pricingCardsModalScope.querySelectorAll('a[href="#pricing-cards-modal"]').forEach((a, idx) => {
291			a.setAttribute('data-fancybox', '');
292			a.setAttribute('data-type', 'inline');
293			
294			// 0-based index for Swiper
295			a.dataset.slideIndex = String(idx);
296		});
297		
298		// IMPORTANT: handle pricing modal triggers manually so we can set the slide BEFORE opening
299		pricingCardsModalScope.addEventListener('click', (e) => {
300			const a = e.target.closest('a[href="#pricing-cards-modal"][data-sli
300de-index]');
301			if (!a) return;
302			
303			e.preventDefault();
304			
305			const index = Number(a.dataset.slideIndex ?? 0);
306			
307			// 1) Move the (already-initialized) swiper while hidden
308			const modal = document.querySelector('#pricing-cards-modal');
309			const swiperRoot = modal?.querySelector('[data-swiper]'); // this matches your Swiper initializer
310			const swiper = swiperRoot?.swiper;
311			
312			if (swiper) {
313				// If loop is on, use slideToLoop so logical slide # maps correctly
314				if (swiper.params?.loop && typeof swiper.slideToLoop === 'function') {
315					swiper.slideToLoop(index, 0);
316				} else {
317					swiper.slideTo(index, 0);
318				}
319				// Hidden sliders sometimes need a refresh once shown; harmless to call now
320				swiper.update();
321			} else {
322				// Fallback: click the pagination bullet if swiper instance isn't accessible for some reason
323				const bullets = modal?.querySelectorAll('.swiper-pagination .swiper-pagination-bullet') || [];
324				bullets[index]?.click();
325			}
326			
327			// 2) Open the modal AFTER the slide is set
328			if (window.Fancybox) {
329				Fancybox.show(
330					[{ src: '#pricing-cards-modal', type: 'inline' }],
331					{ autoFocus: true, dragToClose: true }
332				);
333			}
334		});
335	}
336
337	if (window.Fancybox) {
338		Fancybox.bind("[data-fancybox]", { autoFocus: true, dragToClose: true });
339	}
340});
341
342// (optional) callable function if you inject links later:
343function addFancyboxToPopupLinks() {
344	document.querySelectorAll('.popup-form a:not([data-fancybox])')
345		.forEach(a => a.setAttribute('data-fancybox', ''));
346}
347
348// ------------------------------------------------
349// SMOOTH SCROLL BEHAVIOR
350// ------------------------------------------------
351
352document.addEventListener('DOMContentLoaded', () => {
353	document.querySelectorAll('a[href^="#"]').forEach(link => {
354		
355		// If a link has a data-fancybox attribute, ignore it
356		// Otherwise the scroll-to will override it and fancybox won't work properly
357		if (link.hasAttribute('data-fancybox')) return;
358		
359		link.addEventListener('click', event => {
360			const href = link.getAttribute('href');
361			const target = href ? document.querySelector(href) : null;
362			if (!target) return;
363			
364			event.preventDefault();
365			
366			const y =
367				target.getBoundingClientRect().top +
368				window.scrollY -
369				80; // header height offset
370				
371			window.scrollTo({
372				top: y,
373				behavior: 'smooth'
374			});
375		});
376	});
377});

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.