333 lines
8.5 KiB
JavaScript
333 lines
8.5 KiB
JavaScript
|
|
document.addEventListener("DOMContentLoaded", function () {
|
||
|
|
const pages = Array.from(document.querySelectorAll(".page"));
|
||
|
|
const navLinks = Array.from(document.querySelectorAll("[data-route]"));
|
||
|
|
const dropdowns = Array.from(document.querySelectorAll(".nav-dropdown"));
|
||
|
|
const siteNav = document.querySelector(".site-nav");
|
||
|
|
const mobileNavToggle = document.querySelector(".mobile-nav-toggle");
|
||
|
|
|
||
|
|
function closeMobileNav() {
|
||
|
|
if (!siteNav || !mobileNavToggle) return;
|
||
|
|
siteNav.classList.remove("open");
|
||
|
|
mobileNavToggle.setAttribute("aria-expanded", "false");
|
||
|
|
}
|
||
|
|
|
||
|
|
function openMobileNav() {
|
||
|
|
if (!siteNav || !mobileNavToggle) return;
|
||
|
|
siteNav.classList.add("open");
|
||
|
|
mobileNavToggle.setAttribute("aria-expanded", "true");
|
||
|
|
}
|
||
|
|
|
||
|
|
function toggleMobileNav() {
|
||
|
|
if (!siteNav || !mobileNavToggle) return;
|
||
|
|
const willOpen = !siteNav.classList.contains("open");
|
||
|
|
if (willOpen) {
|
||
|
|
openMobileNav();
|
||
|
|
} else {
|
||
|
|
closeMobileNav();
|
||
|
|
closeAllDropdowns();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
function closeAllDropdowns(exceptDropdown = null) {
|
||
|
|
if (mobileNavToggle) {
|
||
|
|
mobileNavToggle.addEventListener("click", (e) => {
|
||
|
|
e.preventDefault();
|
||
|
|
toggleMobileNav();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
dropdowns.forEach((dropdown) => {
|
||
|
|
if (dropdown === exceptDropdown) return;
|
||
|
|
dropdown.classList.remove("open");
|
||
|
|
const toggle = dropdown.querySelector(".nav-dropdown-toggle");
|
||
|
|
if (toggle) {
|
||
|
|
toggle.setAttribute("aria-expanded", "false");
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function syncDropdownState() {
|
||
|
|
dropdowns.forEach((dropdown) => {
|
||
|
|
const toggle = dropdown.querySelector(".nav-dropdown-toggle");
|
||
|
|
if (!toggle) return;
|
||
|
|
const expanded = dropdown.classList.contains("open") ? "true" : "false";
|
||
|
|
toggle.setAttribute("aria-expanded", expanded);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
dropdowns.forEach((dropdown) => {
|
||
|
|
const toggle = dropdown.querySelector(".nav-dropdown-toggle");
|
||
|
|
const dropdownMenuLinks = Array.from(dropdown.querySelectorAll(".nav-dropdown-menu [data-route]"));
|
||
|
|
|
||
|
|
if (!toggle) return;
|
||
|
|
|
||
|
|
toggle.addEventListener("click", (e) => {
|
||
|
|
e.preventDefault();
|
||
|
|
const parentRoute = toggle.dataset.route || "home";
|
||
|
|
|
||
|
|
if (window.innerWidth <= 900) {
|
||
|
|
closeAllDropdowns();
|
||
|
|
window.location.hash = parentRoute === "eightx8" ? "8x8" : parentRoute;
|
||
|
|
setActive(parentRoute);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const willOpen = !dropdown.classList.contains("open");
|
||
|
|
closeAllDropdowns(dropdown);
|
||
|
|
dropdown.classList.toggle("open", willOpen);
|
||
|
|
toggle.setAttribute("aria-expanded", willOpen ? "true" : "false");
|
||
|
|
|
||
|
|
if (willOpen) {
|
||
|
|
window.location.hash = parentRoute === "eightx8" ? "8x8" : parentRoute;
|
||
|
|
setActive(parentRoute);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
dropdownMenuLinks.forEach((link) => {
|
||
|
|
link.addEventListener("click", () => {
|
||
|
|
closeAllDropdowns();
|
||
|
|
|
||
|
|
window.setTimeout(() => {
|
||
|
|
if (document.activeElement) {
|
||
|
|
document.activeElement.blur();
|
||
|
|
}
|
||
|
|
}, 0);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
document.addEventListener("click", (e) => {
|
||
|
|
const clickedInsideDropdown = dropdowns.some((dropdown) => dropdown.contains(e.target));
|
||
|
|
const clickedInsideNav = siteNav ? siteNav.contains(e.target) : false;
|
||
|
|
const clickedMobileToggle = mobileNavToggle ? mobileNavToggle.contains(e.target) : false;
|
||
|
|
|
||
|
|
if (!clickedInsideDropdown) {
|
||
|
|
closeAllDropdowns();
|
||
|
|
}
|
||
|
|
|
||
|
|
if (window.innerWidth <= 900 && !clickedInsideNav && !clickedMobileToggle) {
|
||
|
|
closeMobileNav();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
document.addEventListener("keydown", (e) => {
|
||
|
|
if (e.key === "Escape") {
|
||
|
|
closeAllDropdowns();
|
||
|
|
closeMobileNav();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
const serviceRoutes = new Set([
|
||
|
|
"services",
|
||
|
|
"unified-communications",
|
||
|
|
"contact-center",
|
||
|
|
"managed-support",
|
||
|
|
"consulting-training",
|
||
|
|
"infrastructure-cabling",
|
||
|
|
"wireless-access",
|
||
|
|
"local-networking"
|
||
|
|
]);
|
||
|
|
|
||
|
|
const industryRoutes = new Set([
|
||
|
|
"industries",
|
||
|
|
"healthcare",
|
||
|
|
"retail",
|
||
|
|
"manufacturing",
|
||
|
|
"education-finance"
|
||
|
|
]);
|
||
|
|
|
||
|
|
function setActive(route) {
|
||
|
|
const requested = route || "home";
|
||
|
|
const hasRoute = pages.some((p) => p.dataset.page === requested);
|
||
|
|
const target = hasRoute ? requested : "home";
|
||
|
|
|
||
|
|
pages.forEach((p) => {
|
||
|
|
const isActive = p.dataset.page === target;
|
||
|
|
p.classList.toggle("active", isActive);
|
||
|
|
p.setAttribute("aria-hidden", isActive ? "false" : "true");
|
||
|
|
});
|
||
|
|
|
||
|
|
navLinks.forEach((a) => {
|
||
|
|
const routeName = a.dataset.route;
|
||
|
|
const isServiceParent = routeName === "services" && serviceRoutes.has(target);
|
||
|
|
const isIndustryParent = routeName === "industries" && industryRoutes.has(target);
|
||
|
|
const isActive = routeName === target || isServiceParent || isIndustryParent;
|
||
|
|
a.classList.toggle("active", isActive);
|
||
|
|
if (isActive) {
|
||
|
|
a.setAttribute("aria-current", "page");
|
||
|
|
} else {
|
||
|
|
a.removeAttribute("aria-current");
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
syncDropdownState();
|
||
|
|
|
||
|
|
window.scrollTo({ top: 0, behavior: "instant" });
|
||
|
|
|
||
|
|
if (target === "home") {
|
||
|
|
startHeroMotion();
|
||
|
|
} else {
|
||
|
|
stopHeroMotion();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function routeFromHash() {
|
||
|
|
const hash = (window.location.hash || "#home").replace("#", "").trim();
|
||
|
|
if (hash === "8x8") return "eightx8";
|
||
|
|
return hash || "home";
|
||
|
|
}
|
||
|
|
|
||
|
|
navLinks.forEach((a) => {
|
||
|
|
a.addEventListener("click", (e) => {
|
||
|
|
const route = a.dataset.route;
|
||
|
|
if (!route) return;
|
||
|
|
|
||
|
|
e.preventDefault();
|
||
|
|
window.location.hash = route === "eightx8" ? "8x8" : route;
|
||
|
|
setActive(route);
|
||
|
|
|
||
|
|
if (window.innerWidth <= 900) {
|
||
|
|
closeMobileNav();
|
||
|
|
closeAllDropdowns();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
const cta = document.getElementById("cta-consultation");
|
||
|
|
if (cta) {
|
||
|
|
cta.addEventListener("click", () => {
|
||
|
|
window.location.hash = "contact";
|
||
|
|
setActive("contact");
|
||
|
|
closeMobileNav();
|
||
|
|
closeAllDropdowns();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
window.addEventListener("hashchange", () => {
|
||
|
|
setActive(routeFromHash());
|
||
|
|
});
|
||
|
|
|
||
|
|
window.addEventListener("resize", () => {
|
||
|
|
if (window.innerWidth <= 900) {
|
||
|
|
closeAllDropdowns();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
const yearSpan = document.getElementById("year");
|
||
|
|
if (yearSpan) yearSpan.textContent = new Date().getFullYear();
|
||
|
|
|
||
|
|
const rotatorEl = document.getElementById("hero-rotator-text");
|
||
|
|
const lockupEl = document.getElementById("hero-lockup");
|
||
|
|
|
||
|
|
const phrases = [
|
||
|
|
"Technical Precision",
|
||
|
|
"Operational Clarity",
|
||
|
|
"Human-Centered Support"
|
||
|
|
];
|
||
|
|
|
||
|
|
const phraseFadeIn = 1100;
|
||
|
|
const phraseHold = 1900;
|
||
|
|
const phraseFadeOut = 1100;
|
||
|
|
const betweenGap = 500;
|
||
|
|
const lockupFadeIn = 1200;
|
||
|
|
const lockupHold = 6500;
|
||
|
|
const loopGap = 800;
|
||
|
|
|
||
|
|
let heroTimer = null;
|
||
|
|
let heroRunId = 0;
|
||
|
|
|
||
|
|
function showEl(el) {
|
||
|
|
if (!el) return;
|
||
|
|
el.classList.add("is-visible");
|
||
|
|
el.classList.remove("is-hidden");
|
||
|
|
}
|
||
|
|
|
||
|
|
function hideEl(el) {
|
||
|
|
if (!el) return;
|
||
|
|
el.classList.remove("is-visible");
|
||
|
|
el.classList.add("is-hidden");
|
||
|
|
}
|
||
|
|
|
||
|
|
function setText(el, text) {
|
||
|
|
if (!el) return;
|
||
|
|
el.textContent = text;
|
||
|
|
}
|
||
|
|
|
||
|
|
function sleep(ms) {
|
||
|
|
return new Promise((resolve) => {
|
||
|
|
heroTimer = window.setTimeout(resolve, ms);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function clearHeroTimer() {
|
||
|
|
if (heroTimer) {
|
||
|
|
window.clearTimeout(heroTimer);
|
||
|
|
heroTimer = null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function heroLoop(runId) {
|
||
|
|
if (!rotatorEl || !lockupEl) return;
|
||
|
|
|
||
|
|
hideEl(lockupEl);
|
||
|
|
hideEl(rotatorEl);
|
||
|
|
|
||
|
|
while (runId === heroRunId) {
|
||
|
|
for (let i = 0; i < phrases.length; i++) {
|
||
|
|
if (runId !== heroRunId) return;
|
||
|
|
|
||
|
|
setText(rotatorEl, phrases[i]);
|
||
|
|
|
||
|
|
rotatorEl.style.setProperty("--hero-fade-ms", `${phraseFadeIn}ms`);
|
||
|
|
rotatorEl.style.setProperty("--hero-drift-ms", `${phraseFadeIn + phraseHold + phraseFadeOut}ms`);
|
||
|
|
showEl(rotatorEl);
|
||
|
|
await sleep(phraseFadeIn);
|
||
|
|
|
||
|
|
await sleep(phraseHold);
|
||
|
|
|
||
|
|
hideEl(rotatorEl);
|
||
|
|
await sleep(phraseFadeOut);
|
||
|
|
|
||
|
|
await sleep(betweenGap);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (runId !== heroRunId) return;
|
||
|
|
|
||
|
|
lockupEl.style.setProperty("--hero-lockup-fade-ms", `${lockupFadeIn}ms`);
|
||
|
|
showEl(lockupEl);
|
||
|
|
await sleep(lockupFadeIn);
|
||
|
|
|
||
|
|
await sleep(lockupHold);
|
||
|
|
|
||
|
|
hideEl(lockupEl);
|
||
|
|
await sleep(loopGap);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function startHeroMotion() {
|
||
|
|
if (!rotatorEl || !lockupEl) return;
|
||
|
|
|
||
|
|
heroRunId += 1;
|
||
|
|
clearHeroTimer();
|
||
|
|
hideEl(lockupEl);
|
||
|
|
hideEl(rotatorEl);
|
||
|
|
heroLoop(heroRunId);
|
||
|
|
}
|
||
|
|
|
||
|
|
function stopHeroMotion() {
|
||
|
|
heroRunId += 1;
|
||
|
|
clearHeroTimer();
|
||
|
|
hideEl(lockupEl);
|
||
|
|
hideEl(rotatorEl);
|
||
|
|
}
|
||
|
|
|
||
|
|
window.addEventListener("resize", () => {
|
||
|
|
if (window.innerWidth > 900) {
|
||
|
|
closeMobileNav();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
setActive(routeFromHash());
|
||
|
|
});
|