/** * 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
 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,
};