initial commit
This commit is contained in:
@@ -0,0 +1,371 @@
|
||||
'use strict';
|
||||
|
||||
const SELECTORS = {
|
||||
navbar: '#navbar',
|
||||
navbarToggle: '#navbarToggle',
|
||||
navbarMenu: '#navbarMenu',
|
||||
productsNavItem: '#productsNavItem',
|
||||
faqItems: '.faq-item',
|
||||
faqQuestion: '.faq-question',
|
||||
faqAnswer: '.faq-answer',
|
||||
fadeInElements: '.fade-in',
|
||||
citationCopy: '.citation-copy',
|
||||
};
|
||||
|
||||
const MOBILE_NAV_MQ = window.matchMedia('(max-width: 768px)');
|
||||
|
||||
function isMobileNav() {
|
||||
return MOBILE_NAV_MQ.matches;
|
||||
}
|
||||
|
||||
function initNavbarScroll() {
|
||||
const navbar = document.querySelector(SELECTORS.navbar);
|
||||
if (!navbar) return;
|
||||
|
||||
const onScroll = () => {
|
||||
if (window.scrollY > 20) {
|
||||
navbar.classList.add('scrolled');
|
||||
} else {
|
||||
navbar.classList.remove('scrolled');
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', onScroll, { passive: true });
|
||||
onScroll();
|
||||
}
|
||||
|
||||
function closeMobileNavMenu() {
|
||||
const toggle = document.querySelector(SELECTORS.navbarToggle);
|
||||
const menu = document.querySelector(SELECTORS.navbarMenu);
|
||||
const productsItem = document.querySelector(SELECTORS.productsNavItem);
|
||||
if (!toggle || !menu) return;
|
||||
|
||||
menu.classList.remove('open');
|
||||
toggle.classList.remove('open');
|
||||
toggle.setAttribute('aria-expanded', 'false');
|
||||
|
||||
if (productsItem) {
|
||||
productsItem.classList.remove('open');
|
||||
const productsTrigger = productsItem.querySelector('.nav-link');
|
||||
if (productsTrigger) productsTrigger.setAttribute('aria-expanded', 'false');
|
||||
productsItem.querySelectorAll('.megamenu-product-item.has-subproducts.open').forEach((item) => {
|
||||
item.classList.remove('open');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function initMobileMenu() {
|
||||
const toggle = document.querySelector(SELECTORS.navbarToggle);
|
||||
const menu = document.querySelector(SELECTORS.navbarMenu);
|
||||
if (!toggle || !menu) return;
|
||||
|
||||
toggle.addEventListener('click', () => {
|
||||
const isOpen = menu.classList.toggle('open');
|
||||
toggle.classList.toggle('open', isOpen);
|
||||
toggle.setAttribute('aria-expanded', String(isOpen));
|
||||
if (!isOpen) {
|
||||
const productsItem = document.querySelector(SELECTORS.productsNavItem);
|
||||
if (productsItem) {
|
||||
productsItem.classList.remove('open');
|
||||
const productsTrigger = productsItem.querySelector('.nav-link');
|
||||
if (productsTrigger) productsTrigger.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && menu.classList.contains('open')) {
|
||||
closeMobileNavMenu();
|
||||
toggle.focus();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!menu.contains(e.target) && !toggle.contains(e.target)) {
|
||||
closeMobileNavMenu();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initMegaMenu() {
|
||||
const productsItem = document.querySelector(SELECTORS.productsNavItem);
|
||||
if (!productsItem) return;
|
||||
|
||||
const trigger = productsItem.querySelector('.nav-link');
|
||||
const megamenu = productsItem.querySelector('.megamenu');
|
||||
if (!trigger || !megamenu) return;
|
||||
|
||||
const CLOSE_DELAY_MS = 150;
|
||||
let hideTimer = null;
|
||||
|
||||
function isPointerInMenu(target) {
|
||||
if (!target || !(target instanceof Node)) return false;
|
||||
return productsItem.contains(target) || megamenu.contains(target);
|
||||
}
|
||||
|
||||
function cancelClose() {
|
||||
clearTimeout(hideTimer);
|
||||
hideTimer = null;
|
||||
megamenu.classList.remove('is-closing');
|
||||
}
|
||||
|
||||
function openMenu() {
|
||||
if (isMobileNav()) return;
|
||||
cancelClose();
|
||||
productsItem.classList.add('open');
|
||||
trigger.setAttribute('aria-expanded', 'true');
|
||||
}
|
||||
|
||||
function beginClose() {
|
||||
if (isMobileNav()) return;
|
||||
productsItem.classList.remove('open');
|
||||
trigger.setAttribute('aria-expanded', 'false');
|
||||
megamenu.classList.add('is-closing');
|
||||
}
|
||||
|
||||
function scheduleClose(event) {
|
||||
if (isMobileNav()) return;
|
||||
if (event && isPointerInMenu(event.relatedTarget)) return;
|
||||
|
||||
clearTimeout(hideTimer);
|
||||
hideTimer = setTimeout(beginClose, CLOSE_DELAY_MS);
|
||||
}
|
||||
|
||||
megamenu.addEventListener('transitionend', (event) => {
|
||||
if (event.target !== megamenu) return;
|
||||
if (event.propertyName !== 'opacity' && event.propertyName !== 'visibility') return;
|
||||
if (productsItem.classList.contains('open')) return;
|
||||
megamenu.classList.remove('is-closing');
|
||||
});
|
||||
|
||||
productsItem.addEventListener('mouseenter', openMenu);
|
||||
productsItem.addEventListener('mouseleave', scheduleClose);
|
||||
|
||||
megamenu.addEventListener('mouseenter', openMenu);
|
||||
megamenu.addEventListener('mouseleave', scheduleClose);
|
||||
|
||||
trigger.addEventListener('click', (e) => {
|
||||
if (!isMobileNav()) return;
|
||||
e.preventDefault();
|
||||
const isOpen = productsItem.classList.toggle('open');
|
||||
trigger.setAttribute('aria-expanded', String(isOpen));
|
||||
});
|
||||
|
||||
trigger.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
if (isMobileNav()) {
|
||||
const isOpen = productsItem.classList.toggle('open');
|
||||
trigger.setAttribute('aria-expanded', String(isOpen));
|
||||
return;
|
||||
}
|
||||
const isOpen = productsItem.classList.toggle('open');
|
||||
if (isOpen) {
|
||||
openMenu();
|
||||
} else {
|
||||
cancelClose();
|
||||
beginClose();
|
||||
}
|
||||
trigger.setAttribute('aria-expanded', String(isOpen));
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
if (isMobileNav()) {
|
||||
productsItem.classList.remove('open');
|
||||
trigger.setAttribute('aria-expanded', 'false');
|
||||
return;
|
||||
}
|
||||
cancelClose();
|
||||
beginClose();
|
||||
}
|
||||
});
|
||||
|
||||
productsItem.querySelectorAll('.megamenu-product-item.has-subproducts').forEach((item) => {
|
||||
const link = item.querySelector('.megamenu-product-link');
|
||||
if (!link) return;
|
||||
|
||||
link.addEventListener('click', (e) => {
|
||||
if (!isMobileNav()) return;
|
||||
if (item.classList.contains('open')) return;
|
||||
e.preventDefault();
|
||||
item.classList.add('open');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initFAQAccordion() {
|
||||
const faqItems = document.querySelectorAll(SELECTORS.faqItems);
|
||||
faqItems.forEach((item) => {
|
||||
const question = item.querySelector(SELECTORS.faqQuestion);
|
||||
const answer = item.querySelector(SELECTORS.faqAnswer);
|
||||
if (!question || !answer) return;
|
||||
|
||||
question.addEventListener('click', () => {
|
||||
const isOpen = item.classList.contains('open');
|
||||
|
||||
faqItems.forEach((other) => {
|
||||
if (other !== item) {
|
||||
other.classList.remove('open');
|
||||
const otherQuestion = other.querySelector(SELECTORS.faqQuestion);
|
||||
const otherAnswer = other.querySelector(SELECTORS.faqAnswer);
|
||||
if (otherQuestion) otherQuestion.setAttribute('aria-expanded', 'false');
|
||||
if (otherAnswer) otherAnswer.hidden = true;
|
||||
}
|
||||
});
|
||||
|
||||
item.classList.toggle('open', !isOpen);
|
||||
question.setAttribute('aria-expanded', String(!isOpen));
|
||||
answer.hidden = isOpen;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initIntersectionObserver() {
|
||||
const elements = document.querySelectorAll(SELECTORS.fadeInElements);
|
||||
if (!elements.length) return;
|
||||
|
||||
if (!('IntersectionObserver' in window)) {
|
||||
elements.forEach((el) => el.classList.add('visible'));
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('visible');
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 0.12, rootMargin: '0px 0px -40px 0px' }
|
||||
);
|
||||
|
||||
elements.forEach((el, index) => {
|
||||
el.style.transitionDelay = `${index * 0.06}s`;
|
||||
observer.observe(el);
|
||||
});
|
||||
}
|
||||
|
||||
function initReadMore() {
|
||||
var chevronSVG = '<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true"><path d="M2 4L6 8L10 4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
||||
|
||||
document.querySelectorAll('[data-readmore]').forEach(function (el) {
|
||||
var threshold = parseInt(el.getAttribute('data-readmore'), 10) || 120;
|
||||
var fullHeight = el.scrollHeight;
|
||||
|
||||
if (fullHeight <= threshold + 12) return;
|
||||
|
||||
var storedHeight = fullHeight;
|
||||
|
||||
el.classList.add('readmore-wrap', 'is-clamped');
|
||||
el.style.maxHeight = threshold + 'px';
|
||||
|
||||
var btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'readmore-btn';
|
||||
btn.setAttribute('aria-expanded', 'false');
|
||||
btn.innerHTML = 'Read more ' + chevronSVG;
|
||||
el.insertAdjacentElement('afterend', btn);
|
||||
|
||||
btn.addEventListener('click', function () {
|
||||
var collapsed = el.classList.toggle('is-clamped');
|
||||
if (collapsed) {
|
||||
el.style.maxHeight = threshold + 'px';
|
||||
btn.innerHTML = 'Read more ' + chevronSVG;
|
||||
btn.setAttribute('aria-expanded', 'false');
|
||||
btn.classList.remove('is-open');
|
||||
} else {
|
||||
el.style.maxHeight = storedHeight + 'px';
|
||||
btn.innerHTML = 'Read less ' + chevronSVG;
|
||||
btn.setAttribute('aria-expanded', 'true');
|
||||
btn.classList.add('is-open');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initCitationCopy() {
|
||||
const copyButtons = document.querySelectorAll(SELECTORS.citationCopy);
|
||||
if (!copyButtons.length) return;
|
||||
|
||||
copyButtons.forEach((btn) => {
|
||||
const url = btn.getAttribute('data-citation-url');
|
||||
const feedback = btn.querySelector('.citation-copy-feedback');
|
||||
if (!url || !feedback) return;
|
||||
|
||||
let resetTimer = null;
|
||||
|
||||
btn.addEventListener('click', async () => {
|
||||
clearTimeout(resetTimer);
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(url);
|
||||
} catch {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = url;
|
||||
textarea.setAttribute('readonly', '');
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.left = '-9999px';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
|
||||
feedback.hidden = false;
|
||||
btn.classList.add('is-copied');
|
||||
btn.setAttribute('aria-label', 'Link copied');
|
||||
|
||||
resetTimer = setTimeout(() => {
|
||||
feedback.hidden = true;
|
||||
btn.classList.remove('is-copied');
|
||||
btn.setAttribute('aria-label', 'Copy citation link');
|
||||
}, 2000);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initProjectFilters() {
|
||||
document.querySelectorAll('[data-projects-section]').forEach((section) => {
|
||||
const buttons = section.querySelectorAll('[data-project-filter]');
|
||||
const cards = section.querySelectorAll('[data-project-status]');
|
||||
if (!buttons.length || !cards.length) return;
|
||||
|
||||
const applyFilter = (filter) => {
|
||||
buttons.forEach((btn) => {
|
||||
const isActive = btn.getAttribute('data-project-filter') === filter;
|
||||
btn.classList.toggle('is-active', isActive);
|
||||
btn.setAttribute('aria-selected', String(isActive));
|
||||
});
|
||||
|
||||
cards.forEach((card) => {
|
||||
const status = card.getAttribute('data-project-status') || '';
|
||||
const visible = filter === 'all' || status === filter;
|
||||
card.classList.toggle('is-hidden', !visible);
|
||||
});
|
||||
};
|
||||
|
||||
buttons.forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
applyFilter(btn.getAttribute('data-project-filter') || 'all');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
initNavbarScroll();
|
||||
initMobileMenu();
|
||||
initMegaMenu();
|
||||
initFAQAccordion();
|
||||
initIntersectionObserver();
|
||||
initReadMore();
|
||||
initCitationCopy();
|
||||
initProjectFilters();
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
Reference in New Issue
Block a user