prime8dev

prime8dev
git clone https://git.prime8.dev/prime8dev.git
Log | Files | Refs | README | LICENSE

header.js (5797B)


      1 import { isEditableTarget } from "./dom.js";
      2 
      3 const detailTitles = {};
      4 const headerHandlers = {
      5   keydown: null,
      6   afterSwap: null,
      7   popstate: null,
      8 };
      9 
     10 export function initHeader() {
     11   const headerEl = document.getElementById("tabs");
     12   if (!headerEl) return;
     13 
     14   const order = (headerEl.getAttribute("data-tabs") || "about").split(",").filter(Boolean);
     15   const fallback = order[0] || "about";
     16 
     17   function goTo(tab) {
     18     const link = document.querySelector(`.tab[data-tab="${tab}"]`);
     19     if (!link) return;
     20 
     21     const path = link.getAttribute("hx-get") || link.getAttribute("href");
     22     if (window.htmx && path) {
     23       setActiveTabUI(tab);
     24       if (window.location.pathname !== path) {
     25         history.pushState({ tab }, "", path);
     26       }
     27 
     28       window.htmx.ajax("GET", path, {
     29         target: "#content",
     30         swap: "innerHTML",
     31       });
     32       return;
     33     }
     34 
     35     link.click();
     36   }
     37 
     38   if (headerHandlers.keydown) {
     39     document.removeEventListener("keydown", headerHandlers.keydown);
     40   }
     41 
     42   headerHandlers.keydown = (e) => {
     43     if (isEditableTarget(e.target)) return;
     44     if (typeof window.isLightboxOpen === "function" && window.isLightboxOpen()) return;
     45 
     46     const isBlogPostOpen = typeof window.isBlogPostOpen === "function" && window.isBlogPostOpen();
     47 
     48     const current = currentFromPath(order, fallback);
     49     const index = order.indexOf(current);
     50 
     51     if (e.key >= "1" && e.key <= String(order.length)) {
     52       const i = Number(e.key) - 1;
     53       if (order[i]) {
     54         e.preventDefault();
     55         if (order[i] === current) return;
     56         goTo(order[i]);
     57       }
     58       return;
     59     }
     60 
     61     if (isBlogPostOpen) return;
     62 
     63     switch (e.key) {
     64       case "Tab": {
     65         e.preventDefault();
     66         const next = order[(index + (e.shiftKey ? -1 : 1) + order.length) % order.length];
     67         goTo(next);
     68         break;
     69       }
     70       case "ArrowLeft":
     71         if (index > 0) {
     72           e.preventDefault();
     73           goTo(order[index - 1]);
     74         }
     75         break;
     76       case "ArrowRight":
     77         if (index < order.length - 1) {
     78           e.preventDefault();
     79           goTo(order[index + 1]);
     80         }
     81         break;
     82       default:
     83         break;
     84     }
     85   };
     86 
     87   document.addEventListener("keydown", headerHandlers.keydown);
     88 
     89   if (headerHandlers.afterSwap) {
     90     document.removeEventListener("htmx:afterSwap", headerHandlers.afterSwap);
     91   }
     92 
     93   headerHandlers.afterSwap = (e) => {
     94     if (!e.target) return;
     95 
     96     switch (e.target.id) {
     97       case "content":
     98         setActiveTabUI(currentFromPath(order, fallback));
     99         break;
    100       case "project-detail":
    101       case "blog-post-content":
    102         updateAppTitle();
    103         break;
    104       default:
    105         break;
    106     }
    107   };
    108 
    109   document.addEventListener("htmx:afterSwap", headerHandlers.afterSwap);
    110 
    111   if (headerHandlers.popstate) {
    112     window.removeEventListener("popstate", headerHandlers.popstate);
    113   }
    114 
    115   headerHandlers.popstate = () => {
    116     setActiveTabUI(currentFromPath(order, fallback));
    117   };
    118 
    119   window.addEventListener("popstate", headerHandlers.popstate);
    120 
    121   setActiveTabUI(currentFromPath(order, fallback));
    122 }
    123 
    124 export function updateAppTitle(activeTab = "") {
    125   const headerEl = document.getElementById("tabs");
    126   if (!headerEl) return;
    127 
    128   const order = (headerEl.getAttribute("data-tabs") || "about").split(",").filter(Boolean);
    129   const fallback = order[0] || "about";
    130   const tab = activeTab || currentFromPath(order, fallback);
    131 
    132   const tabEl = document.querySelector(`.tab[data-tab="${tab}"]`);
    133   const tabLabel = tabEl?.getAttribute("data-label") || tab;
    134   const siteName = document.getElementById("app")?.dataset.bootTitle || "prime8.dev";
    135 
    136   let detailLabel = "";
    137   const path = window.location.pathname;
    138 
    139   switch (tab) {
    140     case "projects":
    141       if (/^\/projects\/[^/]+$/.test(path)) {
    142         const projectID = path.match(/^\/projects\/([^/]+)$/)?.[1] || "";
    143         detailLabel =
    144           detailTitles.projects ||
    145           document.querySelector("#project-detail h2")?.textContent?.trim() ||
    146           selectedListTitle(".list-item[data-id]", ".list-title", projectID);
    147       }
    148       break;
    149     case "blog":
    150       if (/^\/blog\/[^/]+$/.test(path)) {
    151         const blogID = path.match(/^\/blog\/([^/]+)$/)?.[1] || "";
    152         detailLabel =
    153           detailTitles.blog ||
    154           document.querySelector(".blog-article-title")?.textContent?.trim() ||
    155           selectedListTitle(".blog-list-item[data-id]", ".blog-list-title", blogID);
    156       }
    157       break;
    158     default:
    159       break;
    160   }
    161 
    162   if (detailLabel) {
    163     document.title = `${detailLabel} | ${tabLabel} | ${siteName}`;
    164     return;
    165   }
    166 
    167   document.title = `${tabLabel} | ${siteName}`;
    168 }
    169 
    170 export function setAppTitleDetail(tab, value) {
    171   if (value) {
    172     detailTitles[tab] = value;
    173   } else {
    174     delete detailTitles[tab];
    175   }
    176 }
    177 
    178 export function clearAppTitleDetail(tab) {
    179   delete detailTitles[tab];
    180 }
    181 
    182 function setActiveTabUI(tab) {
    183   document.querySelectorAll(".tab").forEach((el) => {
    184     const isActive = el.getAttribute("data-tab") === tab;
    185     el.classList.toggle("active", isActive);
    186     if (isActive) {
    187       el.setAttribute("aria-current", "page");
    188     } else {
    189       el.removeAttribute("aria-current");
    190     }
    191   });
    192 
    193   updateAppTitle(tab);
    194 }
    195 
    196 function currentFromPath(order, fallback) {
    197   const path = window.location.pathname;
    198   if (path === "/" || path === "") return fallback;
    199   const tab = path.replace(/^\//, "").split("/")[0];
    200   return order.includes(tab) ? tab : fallback;
    201 }
    202 
    203 function selectedListTitle(selector, titleSelector, id) {
    204   if (!id) return "";
    205 
    206   const normalized = decodeURIComponent(id);
    207   const items = Array.from(document.querySelectorAll(selector));
    208   const item = items.find((el) => (el.dataset.id || "") === normalized);
    209   if (!item) return "";
    210 
    211   return item.querySelector(titleSelector)?.textContent?.trim() || "";
    212 }