274 lines
8.8 KiB
JavaScript
274 lines
8.8 KiB
JavaScript
(function () {
|
|
"use strict";
|
|
|
|
const ready = (callback) => {
|
|
if (document.readyState === "loading") {
|
|
document.addEventListener("DOMContentLoaded", callback, { once: true });
|
|
return;
|
|
}
|
|
|
|
callback();
|
|
};
|
|
|
|
ready(function () {
|
|
const header = document.querySelector(".site-header");
|
|
const toggle = document.querySelector("[data-nav-toggle], .nav-toggle, .menu-toggle");
|
|
const navigation = document.querySelector("[data-primary-nav], .primary-nav, .primary-navigation");
|
|
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
let lastFocusedElement = null;
|
|
|
|
const updateHeader = () => {
|
|
if (!header) {
|
|
return;
|
|
}
|
|
|
|
header.classList.toggle("is-scrolled", window.scrollY > 24);
|
|
};
|
|
|
|
updateHeader();
|
|
window.addEventListener("scroll", updateHeader, { passive: true });
|
|
|
|
if (toggle && navigation) {
|
|
const drawerClose = navigation.querySelector("[data-nav-close]");
|
|
const navigationParent = navigation.parentNode;
|
|
const navigationNextSibling = navigation.nextSibling;
|
|
let navigationIsDrawer = false;
|
|
|
|
const placeNavigation = (asDrawer) => {
|
|
if (asDrawer && !navigationIsDrawer) {
|
|
document.body.appendChild(navigation);
|
|
navigationIsDrawer = true;
|
|
} else if (!asDrawer && navigationIsDrawer) {
|
|
navigationParent.insertBefore(navigation, navigationNextSibling);
|
|
navigationIsDrawer = false;
|
|
}
|
|
};
|
|
|
|
if (!navigation.id) {
|
|
navigation.id = "site-navigation";
|
|
}
|
|
|
|
toggle.setAttribute("aria-controls", navigation.id);
|
|
toggle.setAttribute("aria-expanded", "false");
|
|
navigation.setAttribute("aria-hidden", "true");
|
|
|
|
const focusableSelector = [
|
|
"a[href]",
|
|
"button:not([disabled])",
|
|
"input:not([disabled])",
|
|
"select:not([disabled])",
|
|
"textarea:not([disabled])",
|
|
"[tabindex]:not([tabindex='-1'])",
|
|
].join(",");
|
|
|
|
const closeMenu = (restoreFocus) => {
|
|
toggle.setAttribute("aria-expanded", "false");
|
|
const toggleLabel = toggle.querySelector(".menu-toggle__label");
|
|
if (toggleLabel && window.gustavooPortfolioNavigation) {
|
|
toggleLabel.textContent = window.gustavooPortfolioNavigation.expand;
|
|
}
|
|
navigation.setAttribute("aria-hidden", "true");
|
|
navigation.classList.remove("is-open");
|
|
navigation.classList.remove("has-more-content");
|
|
document.body.classList.remove("nav-open");
|
|
|
|
if (restoreFocus && lastFocusedElement) {
|
|
lastFocusedElement.focus();
|
|
}
|
|
};
|
|
|
|
const openMenu = () => {
|
|
lastFocusedElement = document.activeElement;
|
|
toggle.setAttribute("aria-expanded", "true");
|
|
const toggleLabel = toggle.querySelector(".menu-toggle__label");
|
|
if (toggleLabel && window.gustavooPortfolioNavigation) {
|
|
toggleLabel.textContent = window.gustavooPortfolioNavigation.collapse;
|
|
}
|
|
navigation.setAttribute("aria-hidden", "false");
|
|
navigation.classList.add("is-open");
|
|
document.body.classList.add("nav-open");
|
|
|
|
window.requestAnimationFrame(function () {
|
|
updateNavigationScrollHint();
|
|
});
|
|
|
|
const firstFocusable = navigation.querySelector(focusableSelector);
|
|
if (firstFocusable) {
|
|
window.requestAnimationFrame(() => firstFocusable.focus());
|
|
}
|
|
};
|
|
|
|
const updateNavigationScrollHint = () => {
|
|
const hasMoreContent = navigation.scrollHeight > navigation.clientHeight + 4
|
|
&& navigation.scrollTop + navigation.clientHeight < navigation.scrollHeight - 4;
|
|
navigation.classList.toggle("has-more-content", hasMoreContent);
|
|
};
|
|
|
|
toggle.addEventListener("click", function () {
|
|
if (toggle.getAttribute("aria-expanded") === "true") {
|
|
closeMenu(false);
|
|
} else {
|
|
openMenu();
|
|
}
|
|
});
|
|
|
|
if (drawerClose) {
|
|
drawerClose.addEventListener("click", function () {
|
|
closeMenu(true);
|
|
});
|
|
}
|
|
|
|
navigation.addEventListener("click", function (event) {
|
|
if (event.target.closest("a")) {
|
|
closeMenu(false);
|
|
}
|
|
});
|
|
|
|
navigation.addEventListener("scroll", updateNavigationScrollHint, { passive: true });
|
|
window.addEventListener("resize", updateNavigationScrollHint, { passive: true });
|
|
|
|
document.addEventListener("keydown", function (event) {
|
|
if (toggle.getAttribute("aria-expanded") !== "true") {
|
|
return;
|
|
}
|
|
|
|
if (event.key === "Escape") {
|
|
event.preventDefault();
|
|
closeMenu(true);
|
|
return;
|
|
}
|
|
|
|
if (event.key !== "Tab") {
|
|
return;
|
|
}
|
|
|
|
const focusable = Array.from(navigation.querySelectorAll(focusableSelector));
|
|
focusable.push(toggle);
|
|
|
|
if (!focusable.length) {
|
|
return;
|
|
}
|
|
|
|
const first = focusable[0];
|
|
const last = focusable[focusable.length - 1];
|
|
|
|
if (event.shiftKey && document.activeElement === first) {
|
|
event.preventDefault();
|
|
last.focus();
|
|
} else if (!event.shiftKey && document.activeElement === last) {
|
|
event.preventDefault();
|
|
first.focus();
|
|
}
|
|
});
|
|
|
|
const desktopQuery = window.matchMedia("(min-width: 921px)");
|
|
const handleDesktop = (event) => {
|
|
placeNavigation(!event.matches);
|
|
|
|
if (event.matches) {
|
|
closeMenu(false);
|
|
navigation.removeAttribute("aria-hidden");
|
|
} else if (toggle.getAttribute("aria-expanded") !== "true") {
|
|
navigation.setAttribute("aria-hidden", "true");
|
|
}
|
|
};
|
|
|
|
handleDesktop(desktopQuery);
|
|
desktopQuery.addEventListener("change", handleDesktop);
|
|
}
|
|
|
|
if (window.location.hash.length > 1) {
|
|
const hashId = decodeURIComponent(window.location.hash.slice(1));
|
|
const hashTarget = document.getElementById(hashId);
|
|
|
|
if (hashTarget) {
|
|
window.requestAnimationFrame(function () {
|
|
window.setTimeout(function () {
|
|
hashTarget.scrollIntoView({ behavior: "auto", block: "start" });
|
|
}, 120);
|
|
});
|
|
}
|
|
}
|
|
|
|
document.querySelectorAll('a[href^="#"]:not([href="#"])').forEach(function (link) {
|
|
link.addEventListener("click", function (event) {
|
|
const id = link.getAttribute("href").slice(1);
|
|
const target = document.getElementById(id);
|
|
|
|
if (!target) {
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
|
|
// The header becomes fixed after the page is scrolled, so
|
|
// scrollIntoView() cannot scroll the document when targeting it.
|
|
if (id === "masthead") {
|
|
window.scrollTo({
|
|
top: 0,
|
|
behavior: reduceMotion.matches ? "auto" : "smooth",
|
|
});
|
|
} else {
|
|
target.scrollIntoView({
|
|
behavior: reduceMotion.matches ? "auto" : "smooth",
|
|
block: "start",
|
|
});
|
|
}
|
|
|
|
if (window.history && window.history.replaceState) {
|
|
window.history.replaceState(null, "", `#${id}`);
|
|
}
|
|
});
|
|
});
|
|
|
|
const revealItems = Array.from(document.querySelectorAll("[data-reveal]"));
|
|
|
|
if (revealItems.length && "IntersectionObserver" in window && !reduceMotion.matches) {
|
|
document.documentElement.classList.add("has-reveal");
|
|
|
|
const revealObserver = new IntersectionObserver(
|
|
function (entries, observer) {
|
|
entries.forEach(function (entry) {
|
|
if (!entry.isIntersecting) {
|
|
return;
|
|
}
|
|
|
|
const delay = Number.parseInt(entry.target.dataset.revealDelay || "0", 10);
|
|
if (delay > 0) {
|
|
entry.target.style.transitionDelay = `${Math.min(delay, 500)}ms`;
|
|
}
|
|
|
|
entry.target.classList.add("is-visible");
|
|
observer.unobserve(entry.target);
|
|
});
|
|
},
|
|
{ rootMargin: "0px 0px -8%", threshold: 0.12 }
|
|
);
|
|
|
|
revealItems.forEach((item) => revealObserver.observe(item));
|
|
} else {
|
|
revealItems.forEach((item) => item.classList.add("is-visible"));
|
|
}
|
|
|
|
const progress = document.querySelector("[data-reading-progress]");
|
|
const article = document.querySelector(".entry-content");
|
|
|
|
if (progress && article) {
|
|
const updateProgress = () => {
|
|
const articleTop = article.getBoundingClientRect().top + window.scrollY;
|
|
const articleHeight = article.offsetHeight;
|
|
const viewportHeight = window.innerHeight;
|
|
const distance = articleHeight - viewportHeight;
|
|
const current = window.scrollY - articleTop;
|
|
const percentage = distance > 0 ? Math.min(100, Math.max(0, (current / distance) * 100)) : 100;
|
|
progress.style.setProperty("--reading-progress", `${percentage}%`);
|
|
progress.setAttribute("aria-valuenow", String(Math.round(percentage)));
|
|
};
|
|
|
|
updateProgress();
|
|
window.addEventListener("scroll", updateProgress, { passive: true });
|
|
window.addEventListener("resize", updateProgress, { passive: true });
|
|
}
|
|
});
|
|
})();
|