prime8dev

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

projects.js (5640B)


      1 import { isEditableTarget } from "./dom.js";
      2 import { 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 
     42   function updateProjectURL(projectID) {
     43     if (!projectID) return;
     44     const path = `/projects/${encodeURIComponent(projectID)}`;
     45     if (window.location.pathname !== path) {
     46       history.replaceState({ tab: "projects" }, "", path);
     47       updateAppTitle();
     48     }
     49   }
     50 
     51   function loadDetailForItem(item) {
     52     const url = item.getAttribute("hx-get");
     53     if (!url) return;
     54 
     55     updateProjectURL(item.dataset.id);
     56 
     57     if (window.htmx && typeof window.htmx.ajax === "function") {
     58       window.htmx.ajax("GET", url, "#project-detail");
     59       return;
     60     }
     61 
     62     item.click();
     63   }
     64 
     65   function selectByIndex(index, loadDetail) {
     66     if (index < 0 || index >= items.length) return;
     67     setSelected(items[index]);
     68     if (loadDetail) loadDetailForItem(items[index]);
     69   }
     70 
     71   function copyCloneCommand() {
     72     section.querySelector("[data-clone-copy]")?.click();
     73   }
     74 
     75   items.forEach((item, index) => {
     76     if (item.dataset.boundClick === "1") return;
     77     item.dataset.boundClick = "1";
     78     item.addEventListener("click", () => {
     79       selectedIndex = index;
     80       setSelected(item);
     81       updateProjectURL(item.dataset.id);
     82     });
     83   });
     84 
     85   if (projectsHandlers.keydown) {
     86     document.removeEventListener("keydown", projectsHandlers.keydown);
     87   }
     88 
     89   projectsHandlers.keydown = (e) => {
     90     if (!section.isConnected) return;
     91     if (isEditableTarget(e.target)) return;
     92     if (window.isLightboxOpen?.()) return;
     93 
     94     if ((e.key === "ArrowUp" || e.key === "ArrowDown") && e.shiftKey) {
     95       const scroller = readmeScroller();
     96       if (scroller) {
     97         e.preventDefault();
     98         scroller.scrollBy({ top: e.key === "ArrowUp" ? -80 : 80, behavior: "smooth" });
     99         return;
    100       }
    101     }
    102 
    103     switch (e.key) {
    104       case "ArrowUp":
    105         e.preventDefault();
    106         selectByIndex(selectedIndex - 1, true);
    107         break;
    108       case "ArrowDown":
    109         e.preventDefault();
    110         selectByIndex(selectedIndex + 1, true);
    111         break;
    112       case "Enter":
    113         e.preventDefault();
    114         copyCloneCommand();
    115         break;
    116       default:
    117         break;
    118     }
    119   };
    120 
    121   document.addEventListener("keydown", projectsHandlers.keydown);
    122 
    123   initCloneCopy(section);
    124 
    125   const initialItem = section.querySelector(".list-item.selected") || items[0];
    126   setSelected(initialItem);
    127   // /projects shows the first project, so the URL and title name it too.
    128   if (window.location.pathname.replace(/\/+$/, "") === "/projects") {
    129     updateProjectURL(initialItem?.dataset.id);
    130   }
    131 
    132   const detailContainer = document.getElementById("project-detail");
    133   const needsInitialFetch = detailContainer?.querySelector("[data-project-placeholder='true']");
    134   if (needsInitialFetch) {
    135     setTimeout(() => loadDetailForItem(initialItem), 0);
    136   }
    137 }
    138 
    139 const CLONE_PROTO_KEY = "clone-proto";
    140 
    141 function initCloneCopy(section) {
    142   const bar = section.querySelector("[data-clone]");
    143   if (!bar || bar.dataset.bound === "true") return;
    144   bar.dataset.bound = "true";
    145 
    146   const tabs = Array.from(bar.querySelectorAll(".clone-tab"));
    147   const urlEl = bar.querySelector("[data-clone-url]");
    148   const copyButton = bar.querySelector("[data-clone-copy]");
    149   if (!tabs.length || !urlEl || !copyButton) return;
    150 
    151   const select = (proto, persist) => {
    152     const tab = tabs.find((t) => t.dataset.proto === proto) || tabs[0];
    153     tabs.forEach((t) => {
    154       const on = t === tab;
    155       t.classList.toggle("active", on);
    156       t.setAttribute("aria-checked", on ? "true" : "false");
    157     });
    158     urlEl.textContent = tab.dataset.addr || "";
    159     if (persist) {
    160       try {
    161         window.localStorage.setItem(CLONE_PROTO_KEY, tab.dataset.proto || "");
    162       } catch {
    163         // storage unavailable
    164       }
    165     }
    166   };
    167 
    168   let saved = "";
    169   try {
    170     saved = window.localStorage.getItem(CLONE_PROTO_KEY) || "";
    171   } catch {
    172     // storage unavailable
    173   }
    174   select(saved || "https", false);
    175 
    176   tabs.forEach((t) => t.addEventListener("click", () => select(t.dataset.proto, true)));
    177 
    178   let resetTimer = 0;
    179   copyButton.addEventListener("click", async () => {
    180     const url = (urlEl.textContent || "").trim();
    181     if (!url) return;
    182 
    183     let copied = false;
    184     try {
    185       await navigator.clipboard.writeText(url);
    186       copied = true;
    187     } catch {
    188       copied = false;
    189     }
    190 
    191     copyButton.textContent = copied ? "copied" : "failed";
    192     copyButton.classList.toggle("copied", copied);
    193     window.clearTimeout(resetTimer);
    194     resetTimer = window.setTimeout(() => {
    195       if (!copyButton.isConnected) return;
    196       copyButton.textContent = "copy";
    197       copyButton.classList.remove("copied");
    198     }, 1200);
    199   });
    200 }