Files
site-portfolio/public/js/modules/header/nav-overflow.js
T

183 lines
6.1 KiB
JavaScript

export function initNavOverflow() {
const cluster = document.querySelector(".header__cluster");
const nav = document.querySelector("[data-top-nav]");
const moreWrap = document.querySelector("[data-nav-more]");
const moreBtn = document.querySelector("[data-nav-more-btn]");
const menuDock = document.querySelector("[data-nav-more-menu]");
const menuFloating = document.querySelector("[data-nav-more-menu-floating]");
const headerEl = document.querySelector(".header");
if (!nav || !moreWrap || !moreBtn || !menuDock || !menuFloating || !headerEl) return;
const links = Array.from(nav.querySelectorAll("a"));
const MENU_GAP = 6;
function getEdgePad() {
const raw = getComputedStyle(document.documentElement).getPropertyValue("--edge-pad").trim();
if (!raw) return 8;
// resolve через временный элемент, т.к. значение — max(...clamp...)
const probe = document.createElement("span");
probe.style.all = "unset";
probe.style.position = "fixed";
probe.style.left = raw;
document.body.appendChild(probe);
const px = probe.getBoundingClientRect().left;
probe.remove();
return Math.max(0, Math.round(px));
}
let VIEWPORT_PAD = getEdgePad();
let openInFloatingMode = null;
let resizeRaf = 0;
const isFloatingHeader = () => headerEl.classList.contains("header--scrolled");
const moveOverflowToFloating = () => {
while (menuDock.firstChild) menuFloating.appendChild(menuDock.firstChild);
};
const moveOverflowToDock = () => {
while (menuFloating.firstChild) menuDock.appendChild(menuFloating.firstChild);
};
function closeMore() {
moveOverflowToDock();
menuDock.hidden = true;
menuFloating.hidden = true;
menuFloating.setAttribute("aria-hidden", "true");
moreBtn.setAttribute("aria-expanded", "false");
openInFloatingMode = null;
menuFloating.style.removeProperty("--nav-more-menu-left");
menuFloating.style.removeProperty("--nav-more-menu-top");
}
function placeMoreMenu() {
if (menuFloating.hidden) return;
const btnRect = moreBtn.getBoundingClientRect();
const menuRect = menuFloating.getBoundingClientRect();
const vw = document.documentElement.clientWidth;
const vh = document.documentElement.clientHeight;
let left = btnRect.right - menuRect.width;
left = Math.min(left, vw - menuRect.width - VIEWPORT_PAD);
left = Math.max(VIEWPORT_PAD, left);
let top = btnRect.bottom + MENU_GAP;
if (top + menuRect.height > vh - VIEWPORT_PAD) top = Math.max(VIEWPORT_PAD, btnRect.top - MENU_GAP - menuRect.height);
menuFloating.style.setProperty("--nav-more-menu-left", `${Math.round(left)}px`);
menuFloating.style.setProperty("--nav-more-menu-top", `${Math.round(top)}px`);
}
function navGapPx() {
const cs = getComputedStyle(nav);
const col = parseFloat(cs.columnGap);
if (Number.isFinite(col)) return col;
const row = parseFloat(cs.rowGap);
if (Number.isFinite(row)) return row;
const g = cs.gap;
if (!g || g === "normal") return 0;
const parts = String(g).trim().split(/\s+/);
const n = parseFloat(parts.length > 1 ? parts[1] : parts[0]);
return Number.isFinite(n) ? n : 0;
}
function rowContentWidth(el) {
const gap = navGapPx();
const kids = Array.from(el.children);
let total = 0;
kids.forEach((child, i) => {
total += child.getBoundingClientRect().width;
if (i > 0) total += gap;
});
return total;
}
function layoutLinks() {
closeMore();
links.forEach((link) => nav.appendChild(link));
menuDock.innerHTML = "";
menuFloating.innerHTML = "";
moreWrap.hidden = true;
if (rowContentWidth(nav) <= nav.clientWidth + 0.5) return;
moreWrap.hidden = false;
void nav.offsetWidth;
while (rowContentWidth(nav) > nav.clientWidth + 0.5 && nav.children.length > 1) {
const last = nav.lastElementChild;
if (!last) break;
menuDock.prepend(last);
}
}
moreBtn.addEventListener("click", (e) => {
e.stopPropagation();
const open = menuDock.hidden && menuFloating.hidden;
if (open) {
openInFloatingMode = isFloatingHeader();
if (openInFloatingMode) {
moveOverflowToFloating();
menuFloating.hidden = false;
menuFloating.setAttribute("aria-hidden", "false");
} else {
menuDock.hidden = false;
}
moreBtn.setAttribute("aria-expanded", "true");
requestAnimationFrame(() => {
placeMoreMenu();
requestAnimationFrame(placeMoreMenu);
});
} else {
closeMore();
}
});
document.addEventListener("click", (e) => {
const t = e.target;
if (!(t instanceof Node)) return;
if (moreWrap.contains(t) || menuDock.contains(t) || menuFloating.contains(t)) {
const el = t instanceof Element ? t : t.parentElement;
if (el?.closest(".nav-more__dock a, .nav-more__floating a")) closeMore();
return;
}
closeMore();
});
const onMenuLinkClick = (e) => {
const t = e.target;
const el = t instanceof Element ? t : t.parentElement;
const a = el?.closest?.("a[href]");
if (a && (menuDock.contains(a) || menuFloating.contains(a))) closeMore();
};
menuDock.addEventListener("click", onMenuLinkClick);
menuFloating.addEventListener("click", onMenuLinkClick);
const onResize = () => {
if (resizeRaf) cancelAnimationFrame(resizeRaf);
resizeRaf = requestAnimationFrame(() => {
VIEWPORT_PAD = getEdgePad();
layoutLinks();
placeMoreMenu();
resizeRaf = 0;
});
};
window.addEventListener("resize", onResize, { passive: true });
window.addEventListener(
"scroll",
() => {
requestAnimationFrame(() => {
if (openInFloatingMode !== null && isFloatingHeader() !== openInFloatingMode) closeMore();
else placeMoreMenu();
});
},
{ passive: true },
);
window.addEventListener("load", layoutLinks, { once: true });
layoutLinks();
if (typeof ResizeObserver === "function" && cluster) {
const ro = new ResizeObserver(() => {
if (!menuDock.hidden || !menuFloating.hidden) return;
layoutLinks();
});
ro.observe(cluster);
}
}