|
154 | 154 | }); |
155 | 155 | } |
156 | 156 |
|
| 157 | + // ---------- RANDOM PICK ---------- |
| 158 | + // Weighted toward older posts so the button truly helps you "discover" |
| 159 | + // something you wrote long ago and forgot about. Weight = (now - date). |
| 160 | + // If you only have a few posts (<=3) just pick uniformly to avoid surprises. |
| 161 | + const randomBtn = document.getElementById('archive-random-btn'); |
| 162 | + |
| 163 | + function pickRandom() { |
| 164 | + const now = Date.now(); |
| 165 | + const candidates = entries.filter((el) => !el.dataset.hidden); |
| 166 | + if (candidates.length === 0) return; |
| 167 | + |
| 168 | + let pick; |
| 169 | + if (candidates.length <= 3) { |
| 170 | + pick = candidates[Math.floor(Math.random() * candidates.length)]; |
| 171 | + } else { |
| 172 | + const weights = candidates.map((el) => { |
| 173 | + const t = new Date(el.dataset.date || 0).getTime(); |
| 174 | + // Older => larger weight; clamp to >=1 day so same-day posts still have mass |
| 175 | + return Math.max(now - t, 24 * 3600 * 1000); |
| 176 | + }); |
| 177 | + const total = weights.reduce((a, b) => a + b, 0); |
| 178 | + let r = Math.random() * total; |
| 179 | + pick = candidates[candidates.length - 1]; |
| 180 | + for (let i = 0; i < candidates.length; i++) { |
| 181 | + r -= weights[i]; |
| 182 | + if (r <= 0) { pick = candidates[i]; break; } |
| 183 | + } |
| 184 | + } |
| 185 | + |
| 186 | + const link = pick.querySelector('.archive-entry-link'); |
| 187 | + if (link && link.href) { |
| 188 | + // Tiny visual feedback: brief pulse on the picked card before navigating |
| 189 | + pick.classList.add('archive-entry--picked'); |
| 190 | + window.setTimeout(() => { window.location.href = link.href; }, 180); |
| 191 | + } |
| 192 | + } |
| 193 | + |
| 194 | + if (randomBtn) { |
| 195 | + randomBtn.addEventListener('click', pickRandom); |
| 196 | + } |
| 197 | + |
| 198 | + |
| 199 | + // ---------- PAGINATOR ---------- |
| 200 | + // Client-side pagination: respect search/sort visibility, sync URL hash. |
| 201 | + const PAGE_SIZE = 10; |
| 202 | + const paginatorEl = document.getElementById('archive-paginator'); |
| 203 | + let currentPage = 1; |
| 204 | + |
| 205 | + function visibleEntries() { |
| 206 | + return entries.filter((el) => !el.dataset.hidden); |
| 207 | + } |
| 208 | + |
| 209 | + function readHashPage() { |
| 210 | + const m = /(?:#|&)page=(\d+)/.exec(window.location.hash || ''); |
| 211 | + const n = m ? parseInt(m[1], 10) : 1; |
| 212 | + return Number.isFinite(n) && n > 0 ? n : 1; |
| 213 | + } |
| 214 | + |
| 215 | + function writeHashPage(n) { |
| 216 | + const target = '#page=' + n; |
| 217 | + if (window.location.hash !== target) { |
| 218 | + // replaceState avoids spamming browser history |
| 219 | + try { history.replaceState(null, '', window.location.pathname + window.location.search + target); } |
| 220 | + catch (_) { window.location.hash = target; } |
| 221 | + } |
| 222 | + } |
| 223 | + |
| 224 | + function applyPage(page) { |
| 225 | + const total = visibleEntries().length; |
| 226 | + const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)); |
| 227 | + currentPage = Math.min(Math.max(1, page), totalPages); |
| 228 | + |
| 229 | + // Show only entries within the current page slice |
| 230 | + const visible = visibleEntries(); |
| 231 | + const slice = visible.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE); |
| 232 | + const sliceSet = new Set(slice); |
| 233 | + entries.forEach((el) => { |
| 234 | + // Within current page slice AND not already hidden by search/filter => display |
| 235 | + if (sliceSet.has(el)) { |
| 236 | + el.style.display = ''; |
| 237 | + } else if (el.dataset.hidden) { |
| 238 | + el.style.display = 'none'; |
| 239 | + } else { |
| 240 | + // Outside page slice but still matches current filter => hide by page |
| 241 | + el.style.display = 'none'; |
| 242 | + } |
| 243 | + }); |
| 244 | + |
| 245 | + // Year/month grouping: hide headers whose entries are all hidden (page OR search) |
| 246 | + document.querySelectorAll('.archive-month').forEach((m) => { |
| 247 | + const any = Array.from(m.querySelectorAll('.archive-entry')).some((e) => e.style.display !== 'none'); |
| 248 | + m.style.display = any ? '' : 'none'; |
| 249 | + }); |
| 250 | + document.querySelectorAll('.archive-year').forEach((y) => { |
| 251 | + const any = Array.from(y.querySelectorAll('.archive-month')).some((m) => m.style.display !== 'none'); |
| 252 | + y.style.display = any ? '' : 'none'; |
| 253 | + }); |
| 254 | + |
| 255 | + // Render the paginator control bar |
| 256 | + if (paginatorEl) { |
| 257 | + paginatorEl.innerHTML = ''; |
| 258 | + if (totalPages <= 1) return; // single page: no controls |
| 259 | + |
| 260 | + const mkBtn = (label, page, opts = {}) => { |
| 261 | + const b = document.createElement('button'); |
| 262 | + b.type = 'button'; |
| 263 | + b.className = 'archive-page-btn' + (opts.current ? ' archive-page-btn--current' : '') + (opts.disabled ? ' archive-page-btn--disabled' : ''); |
| 264 | + b.textContent = label; |
| 265 | + if (!opts.disabled && !opts.current) { |
| 266 | + b.addEventListener('click', () => { |
| 267 | + writeHashPage(page); |
| 268 | + applyPage(page); |
| 269 | + paginatorEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); |
| 270 | + }); |
| 271 | + } |
| 272 | + return b; |
| 273 | + }; |
| 274 | + |
| 275 | + paginatorEl.appendChild(mkBtn('« 上一页', currentPage - 1, { disabled: currentPage === 1 })); |
| 276 | + |
| 277 | + // Compact page-number list: show first, last, current ±1 |
| 278 | + const numbers = new Set([1, totalPages, currentPage - 1, currentPage, currentPage + 1]); |
| 279 | + [...numbers].filter((n) => n >= 1 && n <= totalPages).sort((a, b) => a - b).reduce((prev, n) => { |
| 280 | + if (prev !== null && n - prev > 1) { |
| 281 | + const ell = document.createElement('span'); |
| 282 | + ell.className = 'archive-page-ellipsis'; |
| 283 | + ell.textContent = '…'; |
| 284 | + paginatorEl.appendChild(ell); |
| 285 | + } |
| 286 | + paginatorEl.appendChild(mkBtn(String(n), n, { current: n === currentPage })); |
| 287 | + return n; |
| 288 | + }, null); |
| 289 | + |
| 290 | + paginatorEl.appendChild(mkBtn('下一页 »', currentPage + 1, { disabled: currentPage === totalPages })); |
| 291 | + |
| 292 | + const summary = document.createElement('span'); |
| 293 | + summary.className = 'archive-page-summary'; |
| 294 | + summary.textContent = `第 ${currentPage} / ${totalPages} 页 · 共 ${total} 篇`; |
| 295 | + paginatorEl.appendChild(summary); |
| 296 | + } |
| 297 | + } |
| 298 | + |
| 299 | + // Hook paginator into existing search/sort flow. |
| 300 | + // Easiest: re-run applyPage after every applySearch/sortEntries/hideEmptyMonths. |
| 301 | + // We add it as a wrapper to applySearch without changing its body. |
| 302 | + const _origApplySearch = applySearch; |
| 303 | + applySearch = function (q) { _origApplySearch(q); applyPage(1); writeHashPage(1); }; |
| 304 | + const _origSortEntries = sortEntries; |
| 305 | + sortEntries = function (m) { _origSortEntries(m); applyPage(currentPage); }; |
| 306 | + const _origHideEmptyMonths = hideEmptyMonths; |
| 307 | + hideEmptyMonths = function () { _origHideEmptyMonths(); }; |
| 308 | + |
| 309 | + window.addEventListener('hashchange', () => applyPage(readHashPage())); |
| 310 | + |
157 | 311 | // ---------- INIT ---------- |
158 | 312 | buildHistogram(); |
| 313 | + applyPage(readHashPage()); |
159 | 314 | })(); |
0 commit comments