website

Personal website
git clone https://github.com/michaelneuper/website.git
README | Files | Log | Refs

website/src/scripts/emacs/buffers.js (3549B)


// Every <section> in <main> is a buffer: scrolling to one puts its file name
// and major mode in the modeline and marks its link in the top line. goto()
// jumps to one, and find-file picks one by name.

import { $, $$, emit, reducedMotion } from "../util.js";
import { message } from "./echo.js";
import { defcommand } from "./commands.js";
import { setSegment } from "./modeline.js";
import { completingRead } from "./minibuffer.js";

export const sections = $$("main > section");
const navLinks = $$(".nav a");
const sectionIO = new IntersectionObserver((entries) => {
  for (const e of entries) {
    if (!e.isIntersecting) continue;
    setSegment("buf", e.target.dataset.file);
    setSegment("mode", e.target.dataset.mode);
    for (const a of navLinks) a.setAttribute("aria-current", String(a.hash === `#${e.target.id}`));
    emit("section", e.target.id);
  }
}, { rootMargin: "-40% 0px -55% 0px" });
sections.forEach((s) => sectionIO.observe(s));

export function goto(id, quiet = false) {
  const s = document.getElementById(id);
  if (!s) return;
  // Scroll to the section's header line rather than the padding above it.
  ($(".winbar", s) ?? s).scrollIntoView({ behavior: reducedMotion.matches ? "auto" : "smooth", block: "start" });
  history.replaceState(null, "", id === "top" ? location.pathname : `#${id}`);
  if (!quiet) message(`"${s.dataset.file}"`);
}

document.addEventListener("click", (e) => {
  const a = e.target.closest('a[href^="#"]');
  if (!a || e.defaultPrevented || e.metaKey || e.ctrlKey || e.shiftKey) return;
  const id = a.hash.slice(1);
  if (!sections.some((s) => s.id === id)) return;
  e.preventDefault();
  goto(id, true);
});

// A link from another page (/#posts) lands on the section's header line. The
// browser jumps there before the sections above it have finished building (the
// fonts, the git log, the projects wheel and demo), and they push it down, so
// keep it in place until the page settles or the reader starts scrolling.
const landing = location.hash && sections.find((s) => `#${s.id}` === location.hash);
if (landing && landing.id !== "top") {
  const bar = $(".winbar", landing) ?? landing;
  const ro = new ResizeObserver(() => bar.scrollIntoView({ behavior: "instant", block: "start" }));
  const inputs = ["wheel", "touchstart", "keydown", "pointerdown"];
  const stop = () => {
    ro.disconnect();
    for (const t of inputs) removeEventListener(t, stop, true);
  };
  for (const t of inputs) addEventListener(t, stop, { capture: true, passive: true });
  ro.observe(document.body);
  setTimeout(stop, 3000);
}

export function findSection(word = "") {
  const w = word.toLowerCase();
  return sections.find((s) => s.id === w || s.dataset.file === w) || sections.find((s) => s.id.startsWith(w) || s.dataset.file.startsWith(w));
}

defcommand("find-file", async () => {
  const id = await completingRead("Find file: ", sections.map((s) => ({ value: s.dataset.file, ann: s.querySelector("h1, h2")?.textContent ?? "" })));
  const s = id && sections.find((x) => x.dataset.file === id);
  if (s) goto(s.id);
}, { doc: "Jump to a section by its file name", key: "SPC ." });

for (const s of sections) {
  defcommand(`goto-${s.id}`, () => goto(s.id), { hidden: true });
}

// The home page's sections, which the SPC keys jump to: on other pages those
// keys go back to the home page instead.
for (const id of ["top", "about", "projects", "posts", "contact"]) {
  if (!sections.some((s) => s.id === id)) defcommand(`goto-${id}`, () => location.assign(`/#${id}`), { hidden: true });
}