mirror of
https://codeberg.org/Kankys/ApolloNvim2025.git
synced 2026-11-11 08:34:36 +01:00
refactor code
This commit is contained in:
1 parent
9a38077576
commit
b13422d4af
13 files changed
+11934
No files matched your search
+647
@@ -0,0 +1,647 @@
|
||||
/**
|
||||
* main.js — ApolloNvim Web Interaktivita
|
||||
* Vanilla ES6+ JavaScript bez externích závislostí.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// 8. LOADING SCREEN — fade-out při načtení stránky
|
||||
// (registrujeme jako první, aby běžel co nejdříve)
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function initLoadingScreen() {
|
||||
const loader = document.getElementById("loading-screen");
|
||||
if (!loader) return;
|
||||
|
||||
const hide = () => {
|
||||
loader.classList.add("fade-out");
|
||||
// Po dokončení animace element skryjeme úplně
|
||||
loader.addEventListener(
|
||||
"transitionend",
|
||||
() => {
|
||||
loader.style.display = "none";
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
};
|
||||
|
||||
if (document.readyState === "complete") {
|
||||
hide();
|
||||
} else {
|
||||
window.addEventListener("load", hide);
|
||||
}
|
||||
}
|
||||
|
||||
initLoadingScreen();
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// Spuštění zbytku funkcí po načtení DOM
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
// ── Pořadí inicializace ──────────────────────────────────────────────────
|
||||
initNavigation();
|
||||
initHamburger();
|
||||
initScrollAnimations();
|
||||
initTabs();
|
||||
initTypingEffects();
|
||||
initCounters();
|
||||
initCopyButtons();
|
||||
initPluginFilters();
|
||||
initTooltips();
|
||||
initBackToTop();
|
||||
});
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// 1. NAVIGATION
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function initNavigation() {
|
||||
const navbar =
|
||||
document.querySelector(".navbar") || document.querySelector("nav");
|
||||
const hamburger =
|
||||
document.querySelector(".hamburger") ||
|
||||
document.querySelector(".menu-toggle");
|
||||
const navMenu =
|
||||
document.querySelector(".nav-menu") || document.querySelector(".nav-links");
|
||||
const navLinks = document.querySelectorAll(".nav-links a, .nav-menu a");
|
||||
|
||||
// ── Třída "scrolled" při odrolování ──
|
||||
if (navbar) {
|
||||
const onScroll = () => {
|
||||
navbar.classList.toggle("scrolled", window.scrollY > 50);
|
||||
};
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
onScroll(); // Okamžitá inicializace
|
||||
}
|
||||
|
||||
// ── Hamburger menu toggle ──
|
||||
if (hamburger && navMenu) {
|
||||
hamburger.addEventListener("click", () => {
|
||||
const isOpen = navMenu.classList.toggle("open");
|
||||
hamburger.classList.toggle("active", isOpen);
|
||||
hamburger.setAttribute("aria-expanded", String(isOpen));
|
||||
// Zabránit scrollování pozadí při otevřeném menu
|
||||
document.body.classList.toggle("menu-open", isOpen);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Smooth scroll pro anchor linky ──
|
||||
document.querySelectorAll('a[href^="#"]').forEach((link) => {
|
||||
link.addEventListener("click", (e) => {
|
||||
const targetId = link.getAttribute("href");
|
||||
if (targetId === "#") return;
|
||||
|
||||
const target = document.querySelector(targetId);
|
||||
if (!target) return;
|
||||
|
||||
e.preventDefault();
|
||||
const navHeight = navbar ? navbar.offsetHeight : 0;
|
||||
const top =
|
||||
target.getBoundingClientRect().top + window.scrollY - navHeight - 16;
|
||||
|
||||
window.scrollTo({ top, behavior: "smooth" });
|
||||
|
||||
// Zavření mobilního menu po kliknutí na odkaz
|
||||
closeNavMenu(navMenu, hamburger);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Zavření menu kliknutím mimo ──
|
||||
document.addEventListener("click", (e) => {
|
||||
if (!navMenu || !hamburger) return;
|
||||
if (!navMenu.contains(e.target) && !hamburger.contains(e.target)) {
|
||||
closeNavMenu(navMenu, hamburger);
|
||||
}
|
||||
});
|
||||
|
||||
// ── Aktivní stav nav linků dle URL ──
|
||||
setActiveNavLink(navLinks);
|
||||
}
|
||||
|
||||
/** Zavře mobilní navigation menu */
|
||||
function closeNavMenu(navMenu, hamburger) {
|
||||
if (!navMenu) return;
|
||||
navMenu.classList.remove("open");
|
||||
document.body.classList.remove("menu-open");
|
||||
if (hamburger) {
|
||||
hamburger.classList.remove("active");
|
||||
hamburger.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
}
|
||||
|
||||
/** Označí aktivní nav link dle aktuální URL */
|
||||
function setActiveNavLink(links) {
|
||||
const path = window.location.pathname;
|
||||
links.forEach((link) => {
|
||||
const href = link.getAttribute("href") || "";
|
||||
// Přesná shoda nebo shoda části cesty (pro víceúrovňové URL)
|
||||
const isActive =
|
||||
href !== "#" &&
|
||||
(path === href ||
|
||||
path.endsWith(href) ||
|
||||
(href !== "/" && path.includes(href)));
|
||||
link.classList.toggle("active", isActive);
|
||||
if (isActive) link.setAttribute("aria-current", "page");
|
||||
});
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// 2. SCROLL ANIMACE — Intersection Observer
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function initScrollAnimations() {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add("visible");
|
||||
// Odpojit observer po animaci (každý element se animuje jen jednou)
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
threshold: 0.1,
|
||||
rootMargin: "0px 0px -40px 0px", // spustí se dříve, než element dosáhne dna okna
|
||||
},
|
||||
);
|
||||
|
||||
document
|
||||
.querySelectorAll(".animate-on-scroll")
|
||||
.forEach((el) => observer.observe(el));
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// 3. TAB SYSTEM — CZ / EN přepínač (a jakékoli další záložky)
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function initTabs() {
|
||||
// Podpora více nezávislých tab skupin na stránce
|
||||
document.querySelectorAll("[data-tab-group]").forEach((group) => {
|
||||
const buttons = group.querySelectorAll(".tab-btn");
|
||||
const contents = group.querySelectorAll(".tab-content");
|
||||
const storageKey = `apollo-tab-${group.dataset.tabGroup}`;
|
||||
|
||||
/** Přepne na záložku s daným id */
|
||||
const switchTab = (targetId, saveToStorage = true) => {
|
||||
buttons.forEach((btn) => {
|
||||
const active = btn.dataset.tab === targetId;
|
||||
btn.classList.toggle("active", active);
|
||||
btn.setAttribute("aria-selected", String(active));
|
||||
});
|
||||
|
||||
contents.forEach((content) => {
|
||||
if (content.dataset.tabContent === targetId) {
|
||||
content.classList.add("active");
|
||||
content.classList.remove("hidden");
|
||||
// Fade-in efekt
|
||||
content.style.opacity = "0";
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
content.style.opacity = "1";
|
||||
});
|
||||
});
|
||||
} else {
|
||||
content.classList.remove("active");
|
||||
content.classList.add("hidden");
|
||||
}
|
||||
});
|
||||
|
||||
if (saveToStorage) {
|
||||
try {
|
||||
localStorage.setItem(storageKey, targetId);
|
||||
} catch (_) {}
|
||||
}
|
||||
};
|
||||
|
||||
// Event listenery
|
||||
buttons.forEach((btn) => {
|
||||
btn.addEventListener("click", () => switchTab(btn.dataset.tab));
|
||||
});
|
||||
|
||||
// Obnova uložené preference
|
||||
let savedTab = null;
|
||||
try {
|
||||
savedTab = localStorage.getItem(storageKey);
|
||||
} catch (_) {}
|
||||
|
||||
const firstBtn = buttons[0];
|
||||
const initTab = savedTab || (firstBtn ? firstBtn.dataset.tab : null);
|
||||
if (initTab) switchTab(initTab, false);
|
||||
});
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// 4. TERMINAL TYPING EFFECT
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Animuje psaní textu do elementu znak po znaku.
|
||||
* @param {HTMLElement} element - Cílový element
|
||||
* @param {string} text - Text, který se "napíše"
|
||||
* @param {number} speed - Prodleva mezi znaky v ms (default 60)
|
||||
* @param {Function} callback - Volitelný callback po dokončení
|
||||
*/
|
||||
function typeWriter(element, text, speed = 60, callback = null) {
|
||||
if (!element) return;
|
||||
element.textContent = "";
|
||||
element.style.borderRight = "2px solid currentColor"; // kurzor
|
||||
|
||||
let i = 0;
|
||||
const tick = () => {
|
||||
if (i < text.length) {
|
||||
element.textContent += text.charAt(i);
|
||||
i++;
|
||||
setTimeout(tick, speed + Math.random() * 30); // mírná variabilita
|
||||
} else {
|
||||
// Blikající kurzor po dokončení
|
||||
element.classList.add("typing-done");
|
||||
if (typeof callback === "function") callback();
|
||||
}
|
||||
};
|
||||
tick();
|
||||
}
|
||||
|
||||
/** Aplikuje typing efekt na všechny elementy s atributem [data-typewriter] */
|
||||
function initTypingEffects() {
|
||||
const elements = document.querySelectorAll("[data-typewriter]");
|
||||
if (!elements.length) return;
|
||||
|
||||
// Spustíme postupně s malým zpožděním pro každý element
|
||||
elements.forEach((el, idx) => {
|
||||
const text = el.dataset.typewriter || el.textContent;
|
||||
const speed = parseInt(el.dataset.typeSpeed, 10) || 60;
|
||||
const delay = parseInt(el.dataset.typeDelay, 10) || idx * 300;
|
||||
|
||||
el.textContent = ""; // Skryjeme původní text
|
||||
setTimeout(() => typeWriter(el, text, speed), delay);
|
||||
});
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// 5. SMOOTH COUNTER ANIMATION
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function initCounters() {
|
||||
const counters = document.querySelectorAll("[data-count]");
|
||||
if (!counters.length) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (!entry.isIntersecting) return;
|
||||
observer.unobserve(entry.target);
|
||||
animateCounter(entry.target);
|
||||
});
|
||||
},
|
||||
{ threshold: 0.5 },
|
||||
);
|
||||
|
||||
counters.forEach((el) => observer.observe(el));
|
||||
}
|
||||
|
||||
/**
|
||||
* Animuje čítač od 0 do cílové hodnoty.
|
||||
* @param {HTMLElement} el - Element s atributem data-count
|
||||
*/
|
||||
function animateCounter(el) {
|
||||
const target = parseInt(el.dataset.count, 10);
|
||||
const duration = parseInt(el.dataset.countDuration, 10) || 1500; // ms
|
||||
const suffix = el.dataset.countSuffix || "";
|
||||
const start = performance.now();
|
||||
|
||||
const update = (now) => {
|
||||
const elapsed = now - start;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
// Ease-out cubic
|
||||
const eased = 1 - Math.pow(1 - progress, 3);
|
||||
const current = Math.floor(eased * target);
|
||||
|
||||
el.textContent = current.toLocaleString("cs-CZ") + suffix;
|
||||
|
||||
if (progress < 1) requestAnimationFrame(update);
|
||||
else el.textContent = target.toLocaleString("cs-CZ") + suffix;
|
||||
};
|
||||
|
||||
requestAnimationFrame(update);
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// 6. COPY TO CLIPBOARD
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function initCopyButtons() {
|
||||
// Podpora pro explicitní tlačítka s třídou .copy-btn
|
||||
document.querySelectorAll(".copy-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
const targetSelector = btn.dataset.copyTarget;
|
||||
const codeEl = targetSelector
|
||||
? document.querySelector(targetSelector)
|
||||
: btn.closest(".code-block, pre")?.querySelector("code") ||
|
||||
btn.previousElementSibling;
|
||||
|
||||
if (!codeEl) return;
|
||||
copyText(codeEl.textContent.trim(), btn);
|
||||
});
|
||||
});
|
||||
|
||||
// Automaticky přidá tlačítka ke všem <pre><code> blokům bez tlačítka
|
||||
document.querySelectorAll("pre:not(.no-copy)").forEach((pre) => {
|
||||
if (pre.querySelector(".copy-btn")) return; // Již má tlačítko
|
||||
|
||||
const btn = document.createElement("button");
|
||||
btn.className = "copy-btn copy-btn--auto";
|
||||
btn.textContent = "Kopírovat";
|
||||
btn.setAttribute("aria-label", "Kopírovat kód");
|
||||
pre.style.position = "relative";
|
||||
pre.appendChild(btn);
|
||||
|
||||
btn.addEventListener("click", () => {
|
||||
const code = pre.querySelector("code") || pre;
|
||||
copyText(code.textContent.trim(), btn);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Zkopíruje text do schránky a ukáže vizuální feedback na tlačítku.
|
||||
* @param {string} text - Text ke zkopírování
|
||||
* @param {HTMLElement} btn - Tlačítko pro vizuální feedback
|
||||
*/
|
||||
function copyText(text, btn) {
|
||||
navigator.clipboard
|
||||
.writeText(text)
|
||||
.then(() => {
|
||||
const original = btn.textContent;
|
||||
btn.textContent = "✓ Zkopírováno";
|
||||
btn.classList.add("copied");
|
||||
btn.disabled = true;
|
||||
|
||||
setTimeout(() => {
|
||||
btn.textContent = original;
|
||||
btn.classList.remove("copied");
|
||||
btn.disabled = false;
|
||||
}, 2000);
|
||||
})
|
||||
.catch(() => {
|
||||
// Fallback pro starší prohlížeče
|
||||
const ta = document.createElement("textarea");
|
||||
ta.value = text;
|
||||
ta.style.cssText = "position:fixed;top:-9999px;left:-9999px";
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(ta);
|
||||
|
||||
btn.textContent = "✓ Zkopírováno";
|
||||
btn.classList.add("copied");
|
||||
setTimeout(() => {
|
||||
btn.textContent = "Kopírovat";
|
||||
btn.classList.remove("copied");
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// 7. THEME PARTICLE COLORS (při hover na sekce)
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function initParticleTheme() {
|
||||
if (!window.particleSystem) return;
|
||||
|
||||
// Mapování sekcí na palety barev
|
||||
const sectionThemes = {
|
||||
"#plugins": ["#bd93f9", "#ff79c6", "#ffffff"],
|
||||
"#themes": ["#ff79c6", "#ffb86c", "#ffffff"],
|
||||
"#installation": ["#50fa7b", "#8be9fd", "#ffffff"],
|
||||
"#tutorials": ["#f1fa8c", "#ffb86c", "#ffffff"],
|
||||
};
|
||||
|
||||
Object.entries(sectionThemes).forEach(([selector, colors]) => {
|
||||
const section = document.querySelector(selector);
|
||||
if (!section) return;
|
||||
|
||||
section.addEventListener("mouseenter", () => {
|
||||
window.particleSystem.config.colors = colors;
|
||||
});
|
||||
section.addEventListener("mouseleave", () => {
|
||||
// Obnovení výchozích barev
|
||||
window.particleSystem.config.colors = [
|
||||
"#ffffff",
|
||||
"#8be9fd",
|
||||
"#bd93f9",
|
||||
"#ff79c6",
|
||||
];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Spustíme až po inicializaci particle systému (je v DOMContentLoaded v particles.js)
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
// Malé zpoždění pro jistotu, že particleSystem je vytvořen
|
||||
setTimeout(initParticleTheme, 100);
|
||||
});
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// 9. TOOLTIP SYSTEM
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function initTooltips() {
|
||||
let tooltip = document.getElementById("tooltip-global");
|
||||
|
||||
// Vytvoření globálního tooltip elementu pokud neexistuje
|
||||
if (!tooltip) {
|
||||
tooltip = document.createElement("div");
|
||||
tooltip.id = "tooltip-global";
|
||||
tooltip.className = "tooltip";
|
||||
tooltip.setAttribute("role", "tooltip");
|
||||
tooltip.setAttribute("aria-hidden", "true");
|
||||
document.body.appendChild(tooltip);
|
||||
}
|
||||
|
||||
let hideTimer = null;
|
||||
|
||||
/** Zobrazí tooltip */
|
||||
const show = (text, anchorEl) => {
|
||||
clearTimeout(hideTimer);
|
||||
tooltip.textContent = text;
|
||||
tooltip.style.display = "block";
|
||||
tooltip.removeAttribute("aria-hidden");
|
||||
|
||||
// Pozicování nad elementem
|
||||
const rect = anchorEl.getBoundingClientRect();
|
||||
const tRect = tooltip.getBoundingClientRect();
|
||||
let left = rect.left + rect.width / 2 - tRect.width / 2 + window.scrollX;
|
||||
let top = rect.top - tRect.height - 8 + window.scrollY;
|
||||
|
||||
// Zamezit přetečení přes pravý okraj
|
||||
if (left + tRect.width > window.innerWidth - 8) {
|
||||
left = window.innerWidth - tRect.width - 8;
|
||||
}
|
||||
if (left < 8) left = 8;
|
||||
|
||||
tooltip.style.left = `${left}px`;
|
||||
tooltip.style.top = `${top}px`;
|
||||
tooltip.classList.add("visible");
|
||||
};
|
||||
|
||||
/** Skryje tooltip */
|
||||
const hide = () => {
|
||||
hideTimer = setTimeout(() => {
|
||||
tooltip.classList.remove("visible");
|
||||
tooltip.setAttribute("aria-hidden", "true");
|
||||
}, 150);
|
||||
};
|
||||
|
||||
// Event delegation — posloucháme na document
|
||||
document.addEventListener("mouseover", (e) => {
|
||||
const target = e.target.closest("[data-tooltip]");
|
||||
if (target) show(target.dataset.tooltip, target);
|
||||
});
|
||||
|
||||
document.addEventListener("mouseout", (e) => {
|
||||
if (e.target.closest("[data-tooltip]")) hide();
|
||||
});
|
||||
|
||||
// Klávesnicová přístupnost
|
||||
document.addEventListener("focusin", (e) => {
|
||||
const target = e.target.closest("[data-tooltip]");
|
||||
if (target) show(target.dataset.tooltip, target);
|
||||
});
|
||||
|
||||
document.addEventListener("focusout", (e) => {
|
||||
if (e.target.closest("[data-tooltip]")) hide();
|
||||
});
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// 10. BACK TO TOP BUTTON
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function initBackToTop() {
|
||||
let btn = document.getElementById("back-to-top");
|
||||
|
||||
// Vytvoření tlačítka pokud neexistuje v HTML
|
||||
if (!btn) {
|
||||
btn = document.createElement("button");
|
||||
btn.id = "back-to-top";
|
||||
btn.className = "back-to-top";
|
||||
btn.innerHTML = "⇧"; // ⇧
|
||||
btn.setAttribute("aria-label", "Zpět nahoru");
|
||||
btn.setAttribute("title", "Zpět nahoru");
|
||||
document.body.appendChild(btn);
|
||||
}
|
||||
|
||||
// Zobrazení/skrytí dle scroll pozice
|
||||
const SHOW_THRESHOLD = 300;
|
||||
|
||||
const toggleVisibility = () => {
|
||||
const visible = window.scrollY > SHOW_THRESHOLD;
|
||||
btn.classList.toggle("visible", visible);
|
||||
btn.setAttribute("aria-hidden", String(!visible));
|
||||
};
|
||||
|
||||
window.addEventListener("scroll", toggleVisibility, { passive: true });
|
||||
toggleVisibility();
|
||||
|
||||
// Scroll nahoru po kliknutí
|
||||
btn.addEventListener("click", () => {
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
});
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// UTILITY — veřejné API pro použití z HTML atributů nebo jiných skriptů
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// 11. HAMBURGER MENU (pro HTML strukturu s .navbar__hamburger a .navbar__mobile)
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function initHamburger() {
|
||||
const hamburger = document.getElementById("hamburger");
|
||||
const mobileMenu = document.getElementById("mobile-menu");
|
||||
if (!hamburger || !mobileMenu) return;
|
||||
|
||||
hamburger.addEventListener("click", () => {
|
||||
const isHidden = mobileMenu.hasAttribute("hidden");
|
||||
if (isHidden) {
|
||||
mobileMenu.removeAttribute("hidden");
|
||||
hamburger.setAttribute("aria-expanded", "true");
|
||||
hamburger.classList.add("open");
|
||||
} else {
|
||||
mobileMenu.setAttribute("hidden", "");
|
||||
hamburger.setAttribute("aria-expanded", "false");
|
||||
hamburger.classList.remove("open");
|
||||
}
|
||||
});
|
||||
|
||||
// Zavřít při kliknutí mimo menu
|
||||
document.addEventListener("click", (e) => {
|
||||
if (
|
||||
!mobileMenu.hasAttribute("hidden") &&
|
||||
!mobileMenu.contains(e.target) &&
|
||||
!hamburger.contains(e.target)
|
||||
) {
|
||||
mobileMenu.setAttribute("hidden", "");
|
||||
hamburger.setAttribute("aria-expanded", "false");
|
||||
hamburger.classList.remove("open");
|
||||
}
|
||||
});
|
||||
|
||||
// Zavřít při kliknutí na odkaz v menu
|
||||
mobileMenu.querySelectorAll("a").forEach((link) => {
|
||||
link.addEventListener("click", () => {
|
||||
mobileMenu.setAttribute("hidden", "");
|
||||
hamburger.setAttribute("aria-expanded", "false");
|
||||
hamburger.classList.remove("open");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// 12. PLUGIN CATEGORY FILTER (pro pluginy.html)
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function initPluginFilters() {
|
||||
const filterContainer = document.getElementById("plugin-filters");
|
||||
const pluginsGrid = document.getElementById("plugins-grid");
|
||||
if (!filterContainer || !pluginsGrid) return;
|
||||
|
||||
const buttons = filterContainer.querySelectorAll(".filter-btn");
|
||||
const cards = pluginsGrid.querySelectorAll(".plugin-card");
|
||||
|
||||
buttons.forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
// Aktualizace aktivního tlačítka
|
||||
buttons.forEach((b) => b.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
|
||||
const filter = btn.dataset.filter;
|
||||
|
||||
// Zobrazit / skrýt karty s animací
|
||||
cards.forEach((card) => {
|
||||
const category = card.dataset.category || "";
|
||||
const match = filter === "all" || category === filter;
|
||||
|
||||
if (match) {
|
||||
card.classList.remove("hidden");
|
||||
// Krátké zpoždění pro plynulý efekt
|
||||
requestAnimationFrame(() => {
|
||||
card.style.opacity = "1";
|
||||
card.style.transform = "";
|
||||
});
|
||||
} else {
|
||||
card.classList.add("hidden");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
window.ApolloNvim = {
|
||||
typeWriter,
|
||||
copyText,
|
||||
};
|
||||
Reference in new issue
Block a user