projects.js (3924B)
1 import { isEditableTarget } from "./dom.js"; 2 import { setAppTitleDetail, updateAppTitle } from "./header.js"; 3 4 const projectsHandlers = { 5 keydown: null, 6 }; 7 8 export function initProjects() { 9 const section = document.getElementById("projects"); 10 if (!section) return; 11 12 const items = Array.from(section.querySelectorAll(".list-item")); 13 if (items.length === 0) return; 14 15 const detailView = section.querySelector(".detail-view"); 16 17 let selectedIndex = 0; 18 19 function readmeScroller() { 20 if (!detailView) return null; 21 22 const readme = detailView.querySelector(".readme-content"); 23 if (readme && readme.scrollHeight > readme.clientHeight) { 24 return readme; 25 } 26 27 if (detailView.scrollHeight > detailView.clientHeight) { 28 return detailView; 29 } 30 31 return null; 32 } 33 34 function setSelected(selectedItem) { 35 items.forEach((item) => item.classList.toggle("selected", item === selectedItem)); 36 selectedIndex = items.indexOf(selectedItem); 37 if (selectedIndex < 0) selectedIndex = 0; 38 39 selectedItem.scrollIntoView({ behavior: "smooth", block: "nearest" }); 40 41 const detail = selectedItem.querySelector(".list-title")?.textContent?.trim() || ""; 42 setAppTitleDetail("projects", detail); 43 updateAppTitle("projects"); 44 } 45 46 function updateProjectURL(projectID) { 47 if (!projectID) return; 48 const path = `/projects/${encodeURIComponent(projectID)}`; 49 if (window.location.pathname !== path) { 50 history.replaceState({ tab: "projects" }, "", path); 51 updateAppTitle("projects"); 52 } 53 } 54 55 function loadDetailForItem(item) { 56 const url = item.getAttribute("hx-get"); 57 if (!url) return; 58 59 updateProjectURL(item.dataset.id); 60 61 if (window.htmx && typeof window.htmx.ajax === "function") { 62 window.htmx.ajax("GET", url, "#project-detail"); 63 return; 64 } 65 66 item.click(); 67 } 68 69 function selectByIndex(index, loadDetail) { 70 if (index < 0 || index >= items.length) return; 71 setSelected(items[index]); 72 if (loadDetail) loadDetailForItem(items[index]); 73 } 74 75 function openExternalForIndex(index) { 76 if (index < 0 || index >= items.length) return; 77 const url = items[index]?.dataset.url || ""; 78 if (!url) return; 79 window.open(url, "_blank", "noopener,noreferrer"); 80 } 81 82 items.forEach((item, index) => { 83 if (item.dataset.boundClick === "1") return; 84 item.dataset.boundClick = "1"; 85 item.addEventListener("click", () => { 86 selectedIndex = index; 87 setSelected(item); 88 updateProjectURL(item.dataset.id); 89 }); 90 }); 91 92 if (projectsHandlers.keydown) { 93 document.removeEventListener("keydown", projectsHandlers.keydown); 94 } 95 96 projectsHandlers.keydown = (e) => { 97 if (!section.isConnected) return; 98 if (isEditableTarget(e.target)) return; 99 if (window.isLightboxOpen?.()) return; 100 101 if ((e.key === "ArrowUp" || e.key === "ArrowDown") && e.shiftKey) { 102 const scroller = readmeScroller(); 103 if (scroller) { 104 e.preventDefault(); 105 scroller.scrollBy({ top: e.key === "ArrowUp" ? -80 : 80, behavior: "smooth" }); 106 return; 107 } 108 } 109 110 switch (e.key) { 111 case "ArrowUp": 112 e.preventDefault(); 113 selectByIndex(selectedIndex - 1, true); 114 break; 115 case "ArrowDown": 116 e.preventDefault(); 117 selectByIndex(selectedIndex + 1, true); 118 break; 119 case "Enter": 120 e.preventDefault(); 121 openExternalForIndex(selectedIndex); 122 break; 123 default: 124 break; 125 } 126 }; 127 128 document.addEventListener("keydown", projectsHandlers.keydown); 129 130 const initialItem = section.querySelector(".list-item.selected") || items[0]; 131 setSelected(initialItem); 132 133 const detailContainer = document.getElementById("project-detail"); 134 const needsInitialFetch = detailContainer?.querySelector("[data-project-placeholder='true']"); 135 if (needsInitialFetch) { 136 setTimeout(() => loadDetailForItem(initialItem), 0); 137 } 138 }