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 }