Newer
Older
express-blog / public / js / post.js
@Jason Jason on 19 Jun 3 KB modified: README.md
// static/js/post.js

// Syntax Highlighting with Prism.js (include Prism CSS/JS separately)
// Automatically highlight all <pre><code> blocks
document.querySelectorAll("pre code").forEach((block) => {
  Prism.highlightElement(block);
});

// Copy-to-Clipboard Buttons for Code Blocks
document.querySelectorAll("pre").forEach((pre) => {
  const btn = document.createElement("button");
  btn.textContent = "Copy";
  btn.type = "button";
  btn.className = "copy-btn";
  btn.style.position = "absolute";
  btn.style.top = "0.5rem";
  btn.style.right = "0.5rem";

  btn.addEventListener("click", () => {
    const code = pre.querySelector("code");
    if (!code) return;
    navigator.clipboard.writeText(code.innerText).then(() => {
      btn.textContent = "Copied";
      setTimeout(() => (btn.textContent = "Copy"), 2000);
    });
  });

  pre.style.position = "relative";
  pre.appendChild(btn);
});

// Table of Contents Generation from h2, h3 in .post-content
(function generateTOC() {
  const content = document.querySelector(".post-content");
  if (!content) return;
  const tocContainer = document.createElement("nav");
  tocContainer.className = "toc";
  const tocList = document.createElement("ul");

  content.querySelectorAll("h2, h3").forEach((heading) => {
    if (!heading.id)
      heading.id = heading.textContent
        .trim()
        .toLowerCase()
        .replace(/\s+/g, "-");

    const li = document.createElement("li");
    li.className = heading.tagName.toLowerCase();

    const a = document.createElement("a");
    a.href = `#${heading.id}`;
    a.textContent = heading.textContent;

    li.appendChild(a);
    tocList.appendChild(li);
  });

  tocContainer.appendChild(tocList);

  // Insert TOC before post content or in sidebar if exists
  const sidebar = document.querySelector(".sidebar nav");
  if (sidebar) sidebar.appendChild(tocContainer);
  else content.parentNode.insertBefore(tocContainer, content);
})();

// Smooth Scroll for Anchor Links
document.querySelectorAll('a[href^="#"]').forEach((anchor) => {
  anchor.addEventListener("click", (e) => {
    const target = document.querySelector(anchor.getAttribute("href"));
    if (!target) return;
    e.preventDefault();
    target.scrollIntoView({ behavior: "smooth" });
  });
});

// Dark Mode Toggle
(function darkModeToggle() {
  const toggle = document.createElement("button");
  toggle.textContent = "Toggle Dark Mode";
  toggle.id = "dark-mode-toggle";
  document.body.prepend(toggle);

  function applyMode(dark) {
    document.documentElement.setAttribute(
      "data-theme",
      dark ? "dark" : "light"
    );
    localStorage.setItem("darkMode", dark);
  }

  toggle.addEventListener("click", () => {
    const dark = document.documentElement.getAttribute("data-theme") === "dark";
    applyMode(!dark);
  });

  // Initialize from saved preference or system
  const saved = localStorage.getItem("darkMode");
  if (saved !== null) applyMode(saved === "true");
  else applyMode(window.matchMedia("(prefers-color-scheme: dark)").matches);
})();

// External Link Detection and Attributes
document.querySelectorAll('.post-content a[href^="http"]').forEach((link) => {
  if (new URL(link.href).origin !== location.origin) {
    link.setAttribute("target", "_blank");
    link.setAttribute("rel", "noopener noreferrer");
    // Optionally append icon for external link:
    const extIcon = document.createElement("span");
    extIcon.textContent = " ↗";
    extIcon.setAttribute("aria-hidden", "true");
    link.appendChild(extIcon);
  }
});

// Accessibility: Focus outlines on keyboard navigation
(function manageFocusOutline() {
  function handleFirstTab(e) {
    if (e.key === "Tab") {
      document.body.classList.add("user-is-tabbing");
      window.removeEventListener("keydown", handleFirstTab);
    }
  }
  window.addEventListener("keydown", handleFirstTab);
})();