183 lines
6.1 KiB
JavaScript
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);
|
|
}
|
|
}
|