700 lines
22 KiB
JavaScript
700 lines
22 KiB
JavaScript
/**
|
|
* Navier Instruments Theme - Main JavaScript
|
|
*
|
|
* @package Navier_Instruments
|
|
*/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
/**
|
|
* Header scroll behavior
|
|
*/
|
|
const header = document.querySelector('.site-header');
|
|
let lastScrollTop = 0;
|
|
let ticking = false;
|
|
|
|
function updateHeader(scrollTop) {
|
|
if (!header) return;
|
|
|
|
// Add scrolled class
|
|
if (scrollTop > 50) {
|
|
header.classList.add('scrolled');
|
|
} else {
|
|
header.classList.remove('scrolled');
|
|
}
|
|
|
|
// Hide/show header on scroll
|
|
if (scrollTop > lastScrollTop && scrollTop > 200) {
|
|
header.classList.add('hidden');
|
|
} else {
|
|
header.classList.remove('hidden');
|
|
}
|
|
|
|
lastScrollTop = scrollTop;
|
|
}
|
|
|
|
window.addEventListener('scroll', function() {
|
|
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
|
|
|
if (!ticking) {
|
|
window.requestAnimationFrame(function() {
|
|
updateHeader(scrollTop);
|
|
ticking = false;
|
|
});
|
|
|
|
ticking = true;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Mobile menu toggle
|
|
*/
|
|
const menuToggle = document.querySelector('.menu-toggle');
|
|
const navigation = document.querySelector('.main-navigation');
|
|
|
|
if (menuToggle && navigation) {
|
|
menuToggle.addEventListener('click', function() {
|
|
const isExpanded = menuToggle.getAttribute('aria-expanded') === 'true';
|
|
menuToggle.setAttribute('aria-expanded', !isExpanded);
|
|
navigation.classList.toggle('active');
|
|
document.body.classList.toggle('menu-open');
|
|
});
|
|
|
|
// Close menu when clicking outside
|
|
document.addEventListener('click', function(e) {
|
|
if (!navigation.contains(e.target) && !menuToggle.contains(e.target)) {
|
|
menuToggle.setAttribute('aria-expanded', 'false');
|
|
navigation.classList.remove('active');
|
|
document.body.classList.remove('menu-open');
|
|
}
|
|
});
|
|
|
|
// Close menu on escape key
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Escape' && navigation.classList.contains('active')) {
|
|
menuToggle.setAttribute('aria-expanded', 'false');
|
|
navigation.classList.remove('active');
|
|
document.body.classList.remove('menu-open');
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Smooth scroll for anchor links
|
|
*/
|
|
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
|
anchor.addEventListener('click', function(e) {
|
|
const href = this.getAttribute('href');
|
|
|
|
if (href === '#') return;
|
|
|
|
const target = document.querySelector(href);
|
|
|
|
if (target) {
|
|
e.preventDefault();
|
|
|
|
const headerHeight = header ? header.offsetHeight : 0;
|
|
const targetPosition = target.getBoundingClientRect().top + window.pageYOffset - headerHeight;
|
|
|
|
window.scrollTo({
|
|
top: targetPosition,
|
|
behavior: 'smooth'
|
|
});
|
|
|
|
// Close mobile menu if open
|
|
if (navigation && navigation.classList.contains('active')) {
|
|
menuToggle.setAttribute('aria-expanded', 'false');
|
|
navigation.classList.remove('active');
|
|
document.body.classList.remove('menu-open');
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Scroll animations (Intersection Observer)
|
|
*/
|
|
const animateElements = document.querySelectorAll('.animate');
|
|
|
|
if (animateElements.length > 0 && 'IntersectionObserver' in window) {
|
|
const animationObserver = new IntersectionObserver((entries) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
entry.target.classList.add('animated');
|
|
animationObserver.unobserve(entry.target);
|
|
}
|
|
});
|
|
}, {
|
|
threshold: 0.1,
|
|
rootMargin: '0px 0px -50px 0px'
|
|
});
|
|
|
|
animateElements.forEach(element => {
|
|
animationObserver.observe(element);
|
|
});
|
|
} else {
|
|
// Fallback for browsers without IntersectionObserver
|
|
animateElements.forEach(element => {
|
|
element.classList.add('animated');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Contact form AJAX submission
|
|
*/
|
|
const contactForm = document.getElementById('navier-contact-form');
|
|
const formResponse = document.getElementById('form-response');
|
|
|
|
if (contactForm && typeof navierData !== 'undefined') {
|
|
contactForm.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
|
|
const submitButton = contactForm.querySelector('button[type="submit"]');
|
|
const originalButtonText = submitButton.innerHTML;
|
|
|
|
// Disable button and show loading state
|
|
submitButton.disabled = true;
|
|
submitButton.innerHTML = '<svg class="spinner" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10" opacity="0.25"/><path d="M12 2a10 10 0 0 1 10 10" stroke-linecap="round"><animateTransform attributeName="transform" type="rotate" from="0 12 12" to="360 12 12" dur="1s" repeatCount="indefinite"/></path></svg> Sending...';
|
|
|
|
// Collect form data
|
|
const formData = new FormData(contactForm);
|
|
formData.append('action', 'navier_contact');
|
|
formData.append('nonce', navierData.nonce);
|
|
|
|
// Send AJAX request
|
|
fetch(navierData.ajaxUrl, {
|
|
method: 'POST',
|
|
body: formData,
|
|
credentials: 'same-origin'
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
formResponse.style.display = 'block';
|
|
|
|
if (data.success) {
|
|
formResponse.innerHTML = '<div class="alert alert-success" style="padding: var(--spacing-md); background: rgba(46, 204, 113, 0.1); border: 1px solid var(--navier-secondary); border-radius: var(--border-radius-md); color: var(--navier-secondary-dark);">' + data.data.message + '</div>';
|
|
contactForm.reset();
|
|
} else {
|
|
formResponse.innerHTML = '<div class="alert alert-error" style="padding: var(--spacing-md); background: rgba(231, 76, 60, 0.1); border: 1px solid var(--navier-error); border-radius: var(--border-radius-md); color: var(--navier-error);">' + data.data.message + '</div>';
|
|
}
|
|
})
|
|
.catch(error => {
|
|
formResponse.style.display = 'block';
|
|
formResponse.innerHTML = '<div class="alert alert-error" style="padding: var(--spacing-md); background: rgba(231, 76, 60, 0.1); border: 1px solid var(--navier-error); border-radius: var(--border-radius-md); color: var(--navier-error);">An error occurred. Please try again.</div>';
|
|
})
|
|
.finally(() => {
|
|
submitButton.disabled = false;
|
|
submitButton.innerHTML = originalButtonText;
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Dropdown menu keyboard navigation
|
|
*/
|
|
const menuItems = document.querySelectorAll('.nav-menu > li');
|
|
|
|
menuItems.forEach(item => {
|
|
const link = item.querySelector('a');
|
|
const submenu = item.querySelector('.sub-menu');
|
|
|
|
if (submenu) {
|
|
// Show submenu on focus
|
|
link.addEventListener('focus', () => {
|
|
item.classList.add('focus');
|
|
});
|
|
|
|
// Hide submenu when focus leaves the item
|
|
item.addEventListener('focusout', (e) => {
|
|
if (!item.contains(e.relatedTarget)) {
|
|
item.classList.remove('focus');
|
|
}
|
|
});
|
|
|
|
// Toggle submenu with keyboard
|
|
link.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
e.preventDefault();
|
|
item.classList.toggle('open');
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Back to top button
|
|
*/
|
|
const createBackToTop = () => {
|
|
const button = document.createElement('button');
|
|
button.className = 'back-to-top';
|
|
button.setAttribute('aria-label', 'Back to top');
|
|
button.innerHTML = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="18 15 12 9 6 15"></polyline></svg>';
|
|
|
|
// Styles
|
|
button.style.cssText = `
|
|
position: fixed;
|
|
bottom: 30px;
|
|
right: 30px;
|
|
width: 50px;
|
|
height: 50px;
|
|
background: var(--navier-primary);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 50%;
|
|
cursor: pointer;
|
|
opacity: 0;
|
|
visibility: hidden;
|
|
transition: all 0.3s ease;
|
|
z-index: 999;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
box-shadow: 0 4px 14px rgba(10, 77, 140, 0.3);
|
|
`;
|
|
|
|
document.body.appendChild(button);
|
|
|
|
// Show/hide on scroll
|
|
window.addEventListener('scroll', () => {
|
|
if (window.pageYOffset > 500) {
|
|
button.style.opacity = '1';
|
|
button.style.visibility = 'visible';
|
|
} else {
|
|
button.style.opacity = '0';
|
|
button.style.visibility = 'hidden';
|
|
}
|
|
});
|
|
|
|
// Scroll to top on click
|
|
button.addEventListener('click', () => {
|
|
window.scrollTo({
|
|
top: 0,
|
|
behavior: 'smooth'
|
|
});
|
|
});
|
|
|
|
// Hover effect
|
|
button.addEventListener('mouseenter', () => {
|
|
button.style.transform = 'translateY(-3px)';
|
|
button.style.boxShadow = '0 6px 20px rgba(10, 77, 140, 0.4)';
|
|
});
|
|
|
|
button.addEventListener('mouseleave', () => {
|
|
button.style.transform = 'translateY(0)';
|
|
button.style.boxShadow = '0 4px 14px rgba(10, 77, 140, 0.3)';
|
|
});
|
|
};
|
|
|
|
createBackToTop();
|
|
|
|
/**
|
|
* Lazy loading images with blur-up effect
|
|
*/
|
|
const lazyImages = document.querySelectorAll('img[loading="lazy"]');
|
|
|
|
lazyImages.forEach(img => {
|
|
img.style.transition = 'filter 0.3s ease';
|
|
|
|
if (img.complete) {
|
|
img.style.filter = 'blur(0)';
|
|
} else {
|
|
img.style.filter = 'blur(10px)';
|
|
img.addEventListener('load', () => {
|
|
img.style.filter = 'blur(0)';
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Add loading state to buttons
|
|
*/
|
|
document.querySelectorAll('form button[type="submit"]').forEach(button => {
|
|
button.addEventListener('click', function() {
|
|
const form = this.closest('form');
|
|
if (form && form.checkValidity()) {
|
|
this.classList.add('loading');
|
|
}
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Counter animation for stats
|
|
*/
|
|
const animateCounter = (element, target, duration = 2000) => {
|
|
let start = 0;
|
|
const increment = target / (duration / 16);
|
|
const suffix = element.textContent.replace(/[0-9]/g, '');
|
|
|
|
const step = () => {
|
|
start += increment;
|
|
if (start < target) {
|
|
element.textContent = Math.floor(start) + suffix;
|
|
requestAnimationFrame(step);
|
|
} else {
|
|
element.textContent = target + suffix;
|
|
}
|
|
};
|
|
|
|
step();
|
|
};
|
|
|
|
// Animate stats when they come into view
|
|
const statValues = document.querySelectorAll('.hero-stat-value, .about-badge-value');
|
|
|
|
if (statValues.length > 0 && 'IntersectionObserver' in window) {
|
|
const statsObserver = new IntersectionObserver((entries) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
const element = entry.target;
|
|
const text = element.textContent;
|
|
const number = parseInt(text.replace(/\D/g, ''));
|
|
|
|
if (!isNaN(number)) {
|
|
animateCounter(element, number);
|
|
}
|
|
|
|
statsObserver.unobserve(element);
|
|
}
|
|
});
|
|
}, {
|
|
threshold: 0.5
|
|
});
|
|
|
|
statValues.forEach(stat => {
|
|
statsObserver.observe(stat);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Form validation enhancement
|
|
*/
|
|
document.querySelectorAll('.form-input, .form-textarea').forEach(input => {
|
|
input.addEventListener('blur', function() {
|
|
if (this.value.trim() !== '') {
|
|
this.classList.add('filled');
|
|
} else {
|
|
this.classList.remove('filled');
|
|
}
|
|
|
|
// Custom validation feedback
|
|
if (!this.validity.valid) {
|
|
this.classList.add('error');
|
|
} else {
|
|
this.classList.remove('error');
|
|
}
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Scroll Reveal Animation System
|
|
*/
|
|
function initScrollReveal() {
|
|
const revealElements = document.querySelectorAll('[data-reveal]');
|
|
|
|
if (revealElements.length === 0) return;
|
|
|
|
const observerOptions = {
|
|
root: null,
|
|
rootMargin: '0px 0px -50px 0px',
|
|
threshold: 0.1
|
|
};
|
|
|
|
const revealObserver = new IntersectionObserver((entries) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
const el = entry.target;
|
|
const delay = el.dataset.delay || 0;
|
|
|
|
setTimeout(() => {
|
|
el.classList.add('revealed');
|
|
}, parseInt(delay));
|
|
|
|
revealObserver.unobserve(el);
|
|
}
|
|
});
|
|
}, observerOptions);
|
|
|
|
revealElements.forEach(el => {
|
|
revealObserver.observe(el);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Temperature Bar Animation
|
|
*/
|
|
function initTempBarAnimation() {
|
|
const tempBars = document.querySelectorAll('.temp-bar-fill');
|
|
|
|
if (tempBars.length === 0) return;
|
|
|
|
const observerOptions = {
|
|
threshold: 0.5
|
|
};
|
|
|
|
const tempObserver = new IntersectionObserver((entries) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
entry.target.classList.add('animate');
|
|
tempObserver.unobserve(entry.target);
|
|
}
|
|
});
|
|
}, observerOptions);
|
|
|
|
tempBars.forEach(bar => {
|
|
tempObserver.observe(bar);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Parallax Background Effect
|
|
*/
|
|
function initParallax() {
|
|
const parallaxBgs = document.querySelectorAll('[data-parallax-bg]');
|
|
|
|
if (parallaxBgs.length === 0) return;
|
|
|
|
let ticking = false;
|
|
|
|
function updateParallax() {
|
|
const scrolled = window.pageYOffset;
|
|
|
|
parallaxBgs.forEach(el => {
|
|
const rect = el.getBoundingClientRect();
|
|
const speed = 0.3;
|
|
|
|
if (rect.bottom > 0 && rect.top < window.innerHeight) {
|
|
const yPos = -(scrolled * speed);
|
|
el.style.backgroundPositionY = `calc(50% + ${yPos}px)`;
|
|
}
|
|
});
|
|
|
|
ticking = false;
|
|
}
|
|
|
|
window.addEventListener('scroll', function() {
|
|
if (!ticking) {
|
|
window.requestAnimationFrame(updateParallax);
|
|
ticking = true;
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Smooth Scroll to Anchor
|
|
*/
|
|
function initSmoothScroll() {
|
|
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
|
anchor.addEventListener('click', function(e) {
|
|
const targetId = this.getAttribute('href');
|
|
|
|
if (targetId === '#') return;
|
|
|
|
const target = document.querySelector(targetId);
|
|
|
|
if (target) {
|
|
e.preventDefault();
|
|
|
|
const headerHeight = document.querySelector('.site-header')?.offsetHeight || 0;
|
|
const targetPosition = target.getBoundingClientRect().top + window.pageYOffset - headerHeight - 20;
|
|
|
|
window.scrollTo({
|
|
top: targetPosition,
|
|
behavior: 'smooth'
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Image Tilt Effect (hover)
|
|
*/
|
|
function initTiltEffect() {
|
|
const tiltElements = document.querySelectorAll('[data-tilt]');
|
|
|
|
tiltElements.forEach(el => {
|
|
el.addEventListener('mousemove', function(e) {
|
|
const rect = el.getBoundingClientRect();
|
|
const x = e.clientX - rect.left;
|
|
const y = e.clientY - rect.top;
|
|
|
|
const centerX = rect.width / 2;
|
|
const centerY = rect.height / 2;
|
|
|
|
const rotateX = (y - centerY) / 20;
|
|
const rotateY = (centerX - x) / 20;
|
|
|
|
el.style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) scale(1.02)`;
|
|
});
|
|
|
|
el.addEventListener('mouseleave', function() {
|
|
el.style.transform = 'perspective(1000px) rotateX(0) rotateY(0) scale(1)';
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Device Screen Scroll Animation
|
|
* Changes device screen content based on scroll position
|
|
*/
|
|
function initDeviceScrollAnimation() {
|
|
const section = document.getElementById('device-scroll-section');
|
|
if (!section) return;
|
|
|
|
const slides = section.querySelectorAll('.screen-slide');
|
|
const dots = section.querySelectorAll('.scroll-dot');
|
|
|
|
if (slides.length === 0) return;
|
|
|
|
let currentSlide = 0;
|
|
let isAnimating = false;
|
|
let lastScrollY = window.pageYOffset;
|
|
|
|
// Function to change slide
|
|
function changeSlide(index) {
|
|
if (index < 0 || index >= slides.length || isAnimating) return;
|
|
if (index === currentSlide) return;
|
|
|
|
isAnimating = true;
|
|
|
|
// Remove active from all
|
|
slides.forEach(s => s.classList.remove('active'));
|
|
dots.forEach(d => d.classList.remove('active'));
|
|
|
|
// Add active to new slide
|
|
slides[index].classList.add('active');
|
|
dots[index].classList.add('active');
|
|
|
|
currentSlide = index;
|
|
|
|
setTimeout(() => {
|
|
isAnimating = false;
|
|
}, 600);
|
|
}
|
|
|
|
// Scroll-based animation using IntersectionObserver
|
|
const observer = new IntersectionObserver((entries) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
// Section is visible, start scroll listening
|
|
window.addEventListener('scroll', handleScroll);
|
|
} else {
|
|
// Section not visible, stop listening
|
|
window.removeEventListener('scroll', handleScroll);
|
|
}
|
|
});
|
|
}, {
|
|
threshold: 0.3
|
|
});
|
|
|
|
observer.observe(section);
|
|
|
|
// Handle scroll to change slides
|
|
function handleScroll() {
|
|
const rect = section.getBoundingClientRect();
|
|
const sectionTop = rect.top;
|
|
const sectionHeight = rect.height;
|
|
const windowHeight = window.innerHeight;
|
|
|
|
// Calculate progress through section (0 to 1)
|
|
const scrollProgress = 1 - ((sectionTop + sectionHeight/2) / windowHeight);
|
|
|
|
// Determine which slide to show based on scroll progress
|
|
const slideIndex = Math.min(
|
|
slides.length - 1,
|
|
Math.max(0, Math.floor(scrollProgress * slides.length * 1.5))
|
|
);
|
|
|
|
changeSlide(slideIndex);
|
|
}
|
|
|
|
// Allow clicking on dots
|
|
dots.forEach((dot, index) => {
|
|
dot.addEventListener('click', () => {
|
|
changeSlide(index);
|
|
});
|
|
});
|
|
|
|
// Auto-advance if section is visible but not scrolling
|
|
let autoAdvanceTimer;
|
|
|
|
function startAutoAdvance() {
|
|
stopAutoAdvance();
|
|
autoAdvanceTimer = setInterval(() => {
|
|
const nextSlide = (currentSlide + 1) % slides.length;
|
|
changeSlide(nextSlide);
|
|
}, 3000);
|
|
}
|
|
|
|
function stopAutoAdvance() {
|
|
if (autoAdvanceTimer) {
|
|
clearInterval(autoAdvanceTimer);
|
|
}
|
|
}
|
|
|
|
// Start auto-advance when section is in view
|
|
const autoObserver = new IntersectionObserver((entries) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
startAutoAdvance();
|
|
} else {
|
|
stopAutoAdvance();
|
|
}
|
|
});
|
|
}, {
|
|
threshold: 0.5
|
|
});
|
|
|
|
autoObserver.observe(section);
|
|
}
|
|
|
|
/**
|
|
* Initialize all features
|
|
*/
|
|
function initializeAll() {
|
|
// Prevent double initialization
|
|
if (document.body.classList.contains('loaded')) return;
|
|
|
|
// Add loaded class to body for any CSS transitions
|
|
document.body.classList.add('loaded');
|
|
|
|
// Check for reduced motion preference
|
|
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
document.querySelectorAll('.animate').forEach(el => {
|
|
el.classList.add('animated');
|
|
});
|
|
// Still reveal elements, just without animation
|
|
document.querySelectorAll('[data-reveal]').forEach(el => {
|
|
el.classList.add('revealed');
|
|
});
|
|
} else {
|
|
// Initialize scroll reveal animations
|
|
initScrollReveal();
|
|
initTempBarAnimation();
|
|
initParallax();
|
|
initTiltEffect();
|
|
initDeviceScrollAnimation();
|
|
}
|
|
|
|
// Always init smooth scroll
|
|
initSmoothScroll();
|
|
|
|
console.log('Navier theme initialized successfully');
|
|
}
|
|
|
|
/**
|
|
* Initialize on DOM ready or immediately if already loaded
|
|
*/
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', initializeAll);
|
|
} else {
|
|
// DOM is already ready
|
|
initializeAll();
|
|
}
|
|
|
|
// Also try on window load as fallback
|
|
window.addEventListener('load', initializeAll);
|
|
|
|
})();
|