blog.js (5377B)
1 import { isEditableTarget } from "./dom.js"; 2 import { clearAppTitleDetail, setAppTitleDetail, updateAppTitle } from "./header.js"; 3 4 const blogHandlers = { 5 keydown: null, 6 popstate: null, 7 }; 8 9 export function initBlog() { 10 const section = document.getElementById("blog"); 11 if (!section) return; 12 13 const listView = section.querySelector("#blog-list-view"); 14 const postView = section.querySelector("#blog-post-view"); 15 const backBtn = section.querySelector(".blog-back-btn"); 16 const listItems = Array.from(section.querySelectorAll(".blog-list-item")); 17 if (!listView || !postView || listItems.length === 0) return; 18 19 let selectedIndex = 0; 20 const hasPrefilledPost = Boolean(document.getElementById("blog-post-content")?.children.length); 21 let postOpen = !postView.classList.contains("hidden") || window.location.pathname.startsWith("/blog/"); 22 window.isBlogPostOpen = () => postOpen; 23 24 if (postOpen) { 25 listView.classList.add("hidden"); 26 postView.classList.remove("hidden"); 27 } else { 28 listView.classList.remove("hidden"); 29 postView.classList.add("hidden"); 30 } 31 32 function pathBlogID() { 33 return decodeURIComponent(window.location.pathname.match(/^\/blog\/([^/]+)$/)?.[1] || ""); 34 } 35 36 function itemByBlogID(blogID) { 37 if (!blogID) return null; 38 return listItems.find((item) => item.dataset.id === blogID) || null; 39 } 40 41 function select(index) { 42 selectedIndex = index; 43 listItems.forEach((item, i) => item.classList.toggle("selected", i === index)); 44 listItems[index]?.scrollIntoView({ behavior: "smooth", block: "nearest" }); 45 46 const detail = listItems[index]?.querySelector(".blog-list-title")?.textContent?.trim() || ""; 47 setAppTitleDetail("blog", detail); 48 updateAppTitle("blog"); 49 50 const selectedItem = listItems[index]; 51 if (selectedItem) { 52 selectedItem.dispatchEvent(new Event("blog:selected", { bubbles: true })); 53 } 54 } 55 56 function openPost(blogID, updateHistory = true) { 57 postOpen = true; 58 listView.classList.add("hidden"); 59 postView.classList.remove("hidden"); 60 postView.scrollTop = 0; 61 62 if (blogID && updateHistory) { 63 const path = `/blog/${encodeURIComponent(blogID)}`; 64 if (window.location.pathname !== path) { 65 history.replaceState({ tab: "blog" }, "", path); 66 } 67 } 68 69 updateAppTitle("blog"); 70 } 71 72 function closePost(updateHistory = true) { 73 postOpen = false; 74 listView.classList.remove("hidden"); 75 postView.classList.add("hidden"); 76 clearAppTitleDetail("blog"); 77 78 if (updateHistory && window.location.pathname !== "/blog") { 79 history.replaceState({ tab: "blog" }, "", "/blog"); 80 } 81 82 updateAppTitle("blog"); 83 } 84 85 listItems.forEach((item, index) => { 86 if (item.dataset.boundClick === "1") return; 87 item.dataset.boundClick = "1"; 88 89 item.addEventListener("click", () => { 90 select(index); 91 openPost(item.dataset.id, true); 92 }); 93 }); 94 95 if (backBtn && backBtn.dataset.boundClick !== "1") { 96 backBtn.dataset.boundClick = "1"; 97 backBtn.addEventListener("click", () => closePost(true)); 98 } 99 100 if (blogHandlers.keydown) { 101 document.removeEventListener("keydown", blogHandlers.keydown); 102 } 103 104 blogHandlers.keydown = (e) => { 105 if (!section.isConnected) return; 106 if (window.isLightboxOpen?.()) return; 107 if (isEditableTarget(e.target)) return; 108 109 if (!postOpen) { 110 switch (e.key) { 111 case "ArrowUp": 112 e.preventDefault(); 113 if (selectedIndex > 0) select(selectedIndex - 1); 114 break; 115 case "ArrowDown": 116 e.preventDefault(); 117 if (selectedIndex < listItems.length - 1) select(selectedIndex + 1); 118 break; 119 case "ArrowRight": 120 case "Enter": 121 e.preventDefault(); 122 listItems[selectedIndex]?.click(); 123 break; 124 default: 125 break; 126 } 127 } else { 128 switch (e.key) { 129 case "ArrowLeft": 130 case "Escape": 131 e.preventDefault(); 132 closePost(true); 133 break; 134 case "ArrowUp": 135 e.preventDefault(); 136 postView.scrollBy({ top: -80, behavior: "smooth" }); 137 break; 138 case "ArrowDown": 139 e.preventDefault(); 140 postView.scrollBy({ top: 80, behavior: "smooth" }); 141 break; 142 default: 143 break; 144 } 145 } 146 }; 147 148 document.addEventListener("keydown", blogHandlers.keydown); 149 150 if (blogHandlers.popstate) { 151 window.removeEventListener("popstate", blogHandlers.popstate); 152 } 153 154 blogHandlers.popstate = () => { 155 if (!section.isConnected) return; 156 157 const currentID = pathBlogID(); 158 if (!currentID) { 159 closePost(false); 160 return; 161 } 162 163 const item = itemByBlogID(currentID); 164 if (!item) return; 165 166 const idx = Number(item.dataset.index || 0); 167 select(idx); 168 openPost(currentID, false); 169 170 if (!document.getElementById("blog-post-content")?.children.length) { 171 item.click(); 172 } 173 }; 174 175 window.addEventListener("popstate", blogHandlers.popstate); 176 177 const pathID = pathBlogID(); 178 const selectedFromPath = itemByBlogID(pathID); 179 const initialItem = selectedFromPath || section.querySelector(".blog-list-item.selected") || listItems[0]; 180 if (!initialItem) return; 181 182 const initialIndex = Number(initialItem.dataset.index || 0); 183 select(initialIndex); 184 185 if (postOpen && !hasPrefilledPost) { 186 initialItem.click(); 187 } 188 }