1(function () { 2 var currentRelPath = ''; 3 var currentNostrAddress = ''; 4 var currentNostrEventId = ''; 5 var currentNostrPubkey = ''; 6 var currentNostrKind = ''; 7 var currentNostrD = ''; 8 var currentNostrRelays = []; 9 var refreshInFlight = false; 10 var submitInFlight = false; 11 var postMenuBusy = false; 12 var lastPostMenuTouchAt = 0; 13 var addToListState = { 14 postPath: '', 15 loading: false, 16 submitting: false, 17 lists: [], 18 message: '', 19 tone: '' 20 }; 21 var inlineEditState = { 22 open: false, 23 busy: false, 24 draftId: '', 25 sourcePostPath: '', 26 postFilename: '', 27 postFilenameEditing: false, 28 postType: 'longform', 29 title: '', 30 content: '', 31 tags: '', 32 tagList: [], 33 tagsDraftText: '', 34 output: '', 35 outputTone: '', 36 autosaveTimer: null, 37 saveStatus: '', 38 dirtyDuringSave: false 39 }; 40 41 function overflowMenuIconSvg() { 42 return '<svg class="overflow-menu-icon-svg" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="5.5" r="1.9" fill="currentColor"/><circle cx="12" cy="12" r="1.9" fill="currentColor"/><circle cx="12" cy="18.5" r="1.9" fill="currentColor"/></svg>'; 43 } 44 45 function isPostPage(pathname) { 46 var path = String(pathname || ''); 47 if (/^\/posts\/[^/?#]+\/?$/.test(path)) { 48 return true; 49 } 50 if (/^\/pages\/posts\/[^/?#]+(?:\.html?)?\/?$/.test(path)) { 51 return true; 52 } 53 if (path === '/cgi/blog-open-post' || path.indexOf('/cgi/blog-open-post/') === 0) { 54 return true; 55 } 56 return false; 57 } 58 59 function normalizePostMdPath(raw) { 60 var value = String(raw || '').trim(); 61 if (!value) { 62 return ''; 63 } 64 value = value 65 .replace(/^https?:\/\/[^/]+\//, '') 66 .replace(/^\/+/, '') 67 .replace(/^pages\//, '') 68 .replace(/^posts\//, ''); 69 if (!value) { 70 return ''; 71 } 72 if (/\.html?$/i.test(value)) { 73 value = value.replace(/\.html?$/i, ''); 74 } else if (/\.md$/i.test(value)) { 75 value = value.replace(/\.md$/i, ''); 76 } 77 value = value.replace(/^\/*/, '').replace(/\/*$/, ''); 78 if (!value || value.indexOf('..') !== -1 || value.indexOf('\\') !== -1) { 79 return ''; 80 } 81 return 'posts/' + value + '.md'; 82 } 83 84 function normalizePostFilename(raw) { 85 var value = String(raw || '').trim(); 86 if (!value) { 87 return ''; 88 } 89 value = value 90 .replace(/^https?:\/\/[^/]+\//i, '') 91 .replace(/^\/+/, '') 92 .replace(/^pages\//i, '') 93 .replace(/^posts\//i, ''); 94 if (value.indexOf('/') >= 0) { 95 value = value.split('/').pop(); 96 } 97 value = value.replace(/\.html?$/i, '').replace(/\.md$/i, ''); 98 value = value.toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, ''); 99 return value; 100 } 101 102 function postSlugFromMdPath(mdPath) { 103 var normalized = normalizePostMdPath(mdPath); 104 if (!normalized) { 105 return ''; 106 } 107 return normalized.replace(/^posts\//, '').replace(/\.md$/i, ''); 108 } 109 110 function maybeRepairWrongPostShell(mdPath) { 111 var slug = postSlugFromMdPath(mdPath); 112 if (!slug) { 113 return false; 114 } 115 if (window.__wizardryPostRouteRepairing) { 116 return true; 117 } 118 window.__wizardryPostRouteRepairing = true; 119 var endpoint = '/cgi/blog-open-post?path=' + encodeURIComponent(slug) + '&__route_repair=1'; 120 fetch(endpoint, { credentials: 'same-origin', cache: 'no-store' }) 121 .then(function (res) { 122 if (!res.ok) { 123 throw new Error('repair fetch failed'); 124 } 125 return res.text(); 126 }) 127 .then(function (html) { 128 var text = String(html || ''); 129 if (text.indexOf('<html') === -1) { 130 throw new Error('repair payload invalid'); 131 } 132 document.open(); 133 document.write(text); 134 document.close(); 135 }) 136 .catch(function () { 137 window.location.replace(endpoint); 138 }); 139 return true; 140 } 141 142 function filenameFromPostPath(rawPath) { 143 return normalizePostFilename(normalizePostMdPath(rawPath)); 144 } 145 146 function escapeHtml(value) { 147 return String(value) 148 .replace(/&/g, '&') 149 .replace(/</g, '<') 150 .replace(/>/g, '>') 151 .replace(/"/g, '"') 152 .replace(/'/g, '''); 153 } 154 155 function longTimestampTitle(raw) {
156 var value = String(raw || '').trim(); 157 if (!value || value.indexOf('T') < 0) { 158 return ''; 159 } 160 var date = new Date(value); 161 if (!isFinite(date.getTime())) { 162 return value; 163 } 164 try { 165 return new Intl.DateTimeFormat('en-US', { 166 month: 'long', 167 day: 'numeric', 168 year: 'numeric', 169 hour: 'numeric', 170 minute: '2-digit', 171 second: '2-digit', 172 timeZone: 'UTC', 173 timeZoneName: 'short' 174 }).format(date); 175 } catch (_err) { 176 return value; 177 } 178 } 179 180 function postDateHtml(current, className) { 181 var label = String(current && current.published_date || '').trim(); 182 var explicit = String(current && current.published_timestamp || '').trim(); 183 var title = explicit || longTimestampTitle(current && current.published_at); 184 return '<span class="' + escapeHtml(className || 'post-date') + '"' + (title ? ' title="' + escapeHtml(title) + '"' : '') + '>' + escapeHtml(label) + '</span>'; 185 } 186 187 function postHeaderMetaHtml(current) { 188 return '' + 189 '<div class="post-head-divider" aria-hidden="true"></div>' + 190 '<div class="post-byline post-byline-bottom">' + 191 '<span class="post-author">' + escapeHtml(current.author || 'Blog Author') + '</span>' + 192 '<span class="post-reading-inline">' + escapeHtml(String(current.reading_minutes || 1)) + ' min read</span>' + 193 postDateHtml(current, 'post-date') + 194 '</div>'; 195 } 196 197 function normalizeListSlug(raw) { 198 return String(raw || '') 199 .trim() 200 .toLowerCase() 201 .replace(/[^a-z0-9-]+/g, '-') 202 .replace(/-+/g, '-') 203 .replace(/^-|-$/g, ''); 204 } 205 206 function getSessionToken() { 207 try { 208 return String(localStorage.getItem('session_token') || '').trim(); 209 } catch (_err) { 210 return ''; 211 } 212 } 213 214 function getCsrfToken() { 215 try { 216 return String(localStorage.getItem('csrf_token') || '').trim(); 217 } catch (_err) { 218 return ''; 219 } 220 } 221 222 function postForm(path, payload) { 223 var params = new URLSearchParams(); 224 Object.keys(payload || {}).forEach(function (key) { 225 var value = payload[key]; 226 if (value === undefined || value === null) { 227 return; 228 } 229 params.set(key, String(value)); 230 }); 231 return fetch(path, { 232 method: 'POST', 233 headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, 234 credentials: 'same-origin', 235 body: params.toString() 236 }).then(function (res) { return res.json(); }); 237 } 238 239 function closePostPageMenu() { 240 var panel = document.querySelector('.post-page-menu-panel'); 241 var trigger = document.querySelector('.post-page-menu-trigger'); 242 if (panel) { 243 panel.hidden = true; 244 } 245 if (trigger) { 246 trigger.setAttribute('aria-expanded', 'false'); 247 } 248 } 249 250 function togglePostPageMenu() { 251 var panel = document.querySelector('.post-page-menu-panel'); 252 var trigger = document.querySelector('.post-page-menu-trigger'); 253 var menu = trigger ? trigger.closest('.post-page-menu') : null; 254 if (!panel || !trigger || (menu instanceof HTMLElement && menu.hidden)) { 255 return; 256 } 257 var nextOpen = !!panel.hidden; 258 panel.hidden = !nextOpen; 259 trigger.setAttribute('aria-expanded', nextOpen ? 'true' : 'false'); 260 } 261 262 function activatePostPageMenuFromEvent(event) { 263 var now = Date.now(); 264 if (now - lastPostMenuTouchAt < 700) { 265 event.preventDefault(); 266 event.stopPropagation(); 267 return true; 268 } 269 lastPostMenuTouchAt = now; 270 event.preventDefault(); 271 event.stopPropagation(); 272 togglePostPageMenu(); 273 return true; 274 } 275 276 function nostrToolsNip19() { 277 return window.NostrTools && window.NostrTools.nip19 ? window.NostrTools.nip19 : null; 278 } 279 280 function currentNostrNaddr() { 281 var nip19 = nostrToolsNip19(); 282 var kind = parseInt(currentNostrKind, 10); 283 if (nip19 && typeof nip19.naddrEncode === 'function' && currentNostrPubkey && Number.isFinite(kind) && currentNostrD) { 284 return nip19.naddrEncode({ 285 identifier: currentNostrD, 286 pubkey: currentNostrPubkey, 287 kind: kind, 288 relays: currentNostrRelays 289 }); 290 } 291 return currentNostrAddress || ''; 292 } 293 294 function currentNostrNevent() { 295 var nip19 = nostrToolsNip19(); 296 var kind = parseInt(currentNostrKind, 10); 297 if (nip19 && typeof nip19.neventEncode === 'function' && currentNostrEventId) { 298 return nip19.neventEncode({ 299 id: currentNostrEventId, 300 author: currentNostrPubkey || undefined, 301 kind: Number.isFinite(kind) ? kind : undefined, 302 relays: currentNostrRelays 303 }); 304 } 305 return currentNostrEventId || ''; 306 } 307 308 function currentNostrOpenValue() { 309 return currentNostrNaddr() || currentNostrNevent(); 310 } 311 312 function writeClipboardText(value) { 313 var text = String(value || '').trim(); 314 if (!text) { 315 return Promise.reject(new Error('Nothing to copy yet.')); 316 } 317 if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') { 318 return navigator.clipboard.writeText(text); 319 } 320 return new Promise(function (resolve, reject) { 321 var node = document.createElement('textarea'); 322 node.value = text; 323 node.setAttribute('readonly', 'readonly'); 324 node.style.position = 'fixed'; 325 node.style.left = '-9999px'; 326 node.style.top = '0'; 327 document.body.appendChild(node); 328 node.select();
329 try { 330 if (document.execCommand('copy')) { 331 resolve(); 332 } else { 333 reject(new Error('Copy was not available in this browser.')); 334 } 335 } catch (err) { 336 reject(err); 337 } finally { 338 if (node.parentNode) { 339 node.parentNode.removeChild(node); 340 } 341 } 342 }); 343 } 344 345 function setReaderMenuEnabled() { 346 var hasAddress = !!currentNostrAddress || (!!currentNostrPubkey && !!currentNostrKind && !!currentNostrD); 347 var hasEvent = !!currentNostrEventId; 348 document.querySelectorAll('[data-post-page-reader-action="copy_nostr_address"]').forEach(function (node) { 349 node.hidden = !hasAddress; 350 }); 351 document.querySelectorAll('[data-post-page-reader-action="copy_nostr_event"]').forEach(function (node) { 352 node.hidden = !hasEvent; 353 }); 354 document.querySelectorAll('[data-post-page-reader-action="open_nostr"]').forEach(function (node) { 355 node.hidden = !(hasAddress || hasEvent); 356 }); 357 return hasAddress || hasEvent; 358 } 359 360 function postPageReaderMenuItemHtml(action, label) { 361 return '<button type="button" data-post-page-action="' + escapeHtml(action) + '" data-post-page-reader-action="' + escapeHtml(action) + '" role="menuitem" hidden>' + escapeHtml(label) + '</button>'; 362 } 363 364 function postPageMenuHtml() { 365 return '' + 366 '<button type="button" class="post-page-menu-trigger" aria-label="Post menu" aria-haspopup="menu" aria-expanded="false">' + overflowMenuIconSvg() + '</button>' + 367 '<div class="post-page-menu-panel" role="menu" hidden>' + 368 postPageReaderMenuItemHtml('copy_nostr_address', 'Copy Nostr address') + 369 postPageReaderMenuItemHtml('copy_nostr_event', 'Copy Nostr event') + 370 postPageReaderMenuItemHtml('open_nostr', 'Open in Nostr client') + 371 '<div class="post-page-menu-separator" data-post-page-admin-separator hidden></div>' + 372 '<button type="button" data-post-page-action="edit_post" data-post-page-admin-action role="menuitem" hidden>Edit post...</button>' + 373 '<button type="button" data-post-page-action="add_to_list" data-post-page-admin-action role="menuitem" hidden>Add to list...</button>' + 374 '<button type="button" class="post-page-menu-delete" data-post-page-action="delete_post" data-post-page-admin-action role="menuitem" hidden>Delete post...</button>' + 375 '</div>'; 376 } 377 378 function normalizePostPageMenu(menu) { 379 if (!(menu instanceof HTMLElement)) { 380 return null; 381 } 382 var panel = menu.querySelector('.post-page-menu-panel'); 383 if (!(panel instanceof HTMLElement)) { 384 return menu; 385 } 386 if (menu.getAttribute('data-post-page-menu-normalized') === 'true') { 387 return menu; 388 } 389 390 menu.hidden = true; 391 panel.hidden = true; 392 var trigger = menu.querySelector('.post-page-menu-trigger'); 393 if (trigger) { 394 trigger.innerHTML = overflowMenuIconSvg(); 395 trigger.setAttribute('aria-expanded', 'false'); 396 } 397 398 if (!panel.querySelector('[data-post-page-reader-action="copy_nostr_address"]')) { 399 panel.insertAdjacentHTML('afterbegin', postPageReaderMenuItemHtml('copy_nostr_address', 'Copy Nostr address')); 400 } 401 if (!panel.querySelector('[data-post-page-reader-action="copy_nostr_event"]')) { 402 var addressNode = panel.querySelector('[data-post-page-reader-action="copy_nostr_address"]'); 403 if (addressNode) { 404 addressNode.insertAdjacentHTML('afterend', postPageReaderMenuItemHtml('copy_nostr_event', 'Copy Nostr event')); 405 } else { 406 panel.insertAdjacentHTML('afterbegin', postPageReaderMenuItemHtml('copy_nostr_event', 'Copy Nostr event')); 407 } 408 } 409 if (!panel.querySelector('[data-post-page-reader-action="open_nostr"]')) { 410 var eventNode = panel.querySelector('[data-post-page-reader-action="copy_nostr_event"]'); 411 if (eventNode) { 412 eventNode.insertAdjacentHTML('afterend', postPageReaderMenuItemHtml('open_nostr', 'Open in Nostr client')); 413 } else { 414 panel.insertAdjacentHTML('afterbegin', postPageReaderMenuItemHtml('open_nostr', 'Open in Nostr client')); 415 } 416 } 417 418 panel.querySelectorAll('[data-post-page-action="edit_post"], [data-post-page-action="add_to_list"], [data-post-page-action="delete_post"]').forEach(function (node) { 419 node.setAttribute('data-post-page-admin-action', ''); 420 node.hidden = true; 421 }); 422 423 if (!panel.querySelector('[data-post-page-admin-separator]')) { 424 var firstAdminAction = panel.querySelector('[data-post-page-admin-action]'); 425 if (firstAdminAction) { 426 firstAdminAction.insertAdjacentHTML('beforebegin', '<div class="post-page-menu-separator" data-post-page-admin-separator hidden></div>'); 427 } 428 } 429 panel.querySelectorAll('[data-post-page-admin-separator]').forEach(function (node) { 430 node.hidden = true; 431 }); 432 433 menu.setAttribute('data-post-page-menu-normalized', 'true'); 434 return menu; 435 } 436 437 function ensurePostPageMenu(layout) { 438 if (!layout || !layout.card) { 439 return null; 440 } 441 var head = layout.card.querySelector('.post-head'); 442 if (!head) { 443 return null; 444 } 445 var existing = head.querySelector('.post-page-menu'); 446 if (existing) { 447 return normalizePostPageMenu(existing); 448 } 449 var wrap = document.createElement('div'); 450 wrap.className = 'post-page-menu'; 451 wrap.hidden = true; 452 wrap.innerHTML = postPageMenuHtml(); 453 head.appendChild(wrap); 454 wrap.setAttribute('data-post-page-menu-normalized', 'true'); 455 return wrap; 456 } 457 458 function checkAdminSession() { 459 var token = getSessionToken(); 460 if (!token || token === 'undefined' || token === 'null') { 461 return Promise.resolve(false); 462 }
463 return fetch('/cgi/ssh-auth-check-session?session_token=' + encodeURIComponent(token), { 464 credentials: 'same-origin' 465 }) 466 .then(function (res) { return res.json(); }) 467 .then(function (data) { 468 return !!(data && data.authenticated && data.is_admin); 469 }) 470 .catch(function () { 471 return false; 472 }); 473 } 474 475 function setPostPageMenuVisibility(menu, isAdmin, hasReaderActions) { 476 menu.querySelectorAll('[data-post-page-admin-action]').forEach(function (node) { 477 node.hidden = !isAdmin; 478 }); 479 menu.querySelectorAll('[data-post-page-admin-separator]').forEach(function (node) { 480 node.hidden = !(isAdmin && hasReaderActions); 481 }); 482 menu.hidden = !(isAdmin || hasReaderActions); 483 if (menu.hidden) { 484 closePostPageMenu(); 485 } 486 } 487 488 function eventHasVerifiedAdminState(event) { 489 return !!(event && event.detail && event.detail.authenticated === true && typeof event.detail.is_admin !== 'undefined'); 490 } 491 492 function refreshPostPageMenuVisibility(event) { 493 var menu = normalizePostPageMenu(document.querySelector('.post-page-menu')); 494 if (!menu) { 495 return; 496 } 497 var hasReaderActions = setReaderMenuEnabled(); 498 if (eventHasVerifiedAdminState(event)) { 499 setPostPageMenuVisibility(menu, event.detail.is_admin === true, hasReaderActions); 500 return; 501 } 502 checkAdminSession().then(function (isAdmin) { 503 setPostPageMenuVisibility(menu, isAdmin, hasReaderActions); 504 }); 505 } 506 507 function addToListDialogNode() { 508 return document.getElementById('post-page-add-to-list-dialog'); 509 } 510 511 function closeAddToListDialog() { 512 var dialog = addToListDialogNode(); 513 if (dialog && dialog.parentNode) { 514 dialog.parentNode.removeChild(dialog); 515 } 516 addToListState.postPath = ''; 517 addToListState.loading = false; 518 addToListState.submitting = false; 519 addToListState.lists = []; 520 addToListState.message = ''; 521 addToListState.tone = ''; 522 } 523 524 function addToListSelectedSlug(dialog) { 525 var select = dialog ? dialog.querySelector('[data-post-add-list-select]') : null; 526 var newInput = dialog ? dialog.querySelector('[data-post-add-list-new-slug]') : null; 527 var selected = select ? String(select.value || '').trim() : ''; 528 if (selected === '__new__') { 529 return normalizeListSlug(newInput ? newInput.value : ''); 530 } 531 return normalizeListSlug(selected || 'list'); 532 } 533 534 function syncAddToListDialogNewRow(dialog) { 535 var select = dialog ? dialog.querySelector('[data-post-add-list-select]') : null; 536 var row = dialog ? dialog.querySelector('[data-post-add-list-new-row]') : null; 537 var newInput = dialog ? dialog.querySelector('[data-post-add-list-new-slug]') : null; 538 if (!select || !row) { 539 return; 540 } 541 var useNew = String(select.value || '') === '__new__'; 542 row.hidden = !useNew; 543 if (useNew && newInput) { 544 newInput.focus(); 545 newInput.select(); 546 } 547 } 548 549 function renderAddToListDialog() { 550 var dialog = addToListDialogNode(); 551 if (!dialog) { 552 dialog = document.createElement('div'); 553 dialog.id = 'post-page-add-to-list-dialog'; 554 dialog.className = 'post-page-modal-shell'; 555 dialog.setAttribute('role', 'presentation'); 556 document.body.appendChild(dialog); 557 } 558 559 var lists = Array.isArray(addToListState.lists) ? addToListState.lists : []; 560 var optionsHtml = ''; 561 lists.forEach(function (item) { 562 var slug = normalizeListSlug(item && item.slug); 563 if (!slug) { 564 return; 565 } 566 var title = String((item && item.title) || slug).trim(); 567 optionsHtml += '<option value="' + escapeHtml(slug) + '">' + escapeHtml(title || slug) + '</option>'; 568 }); 569 optionsHtml += '<option value="__new__">New list...</option>'; 570 var disabledAttr = addToListState.loading || addToListState.submitting ? ' disabled aria-disabled="true"' : ''; 571 var statusHtml = addToListState.message 572 ? '<p class="post-page-modal-status is-' + escapeHtml(addToListState.tone || 'info') + '">' + escapeHtml(addToListState.message) + '</p>' 573 : ''; 574 575 dialog.innerHTML = '' + 576 '<div class="post-page-modal-backdrop" data-post-add-list-action="cancel"></div>' + 577 '<form class="post-page-modal-panel" data-post-add-list-form="true" role="dialog" aria-modal="true" aria-labelledby="post-page-add-to-list-title">' + 578 '<h3 id="post-page-add-to-list-title">Add Post to List</h3>' + 579 '<label class="post-page-modal-field">' + 580 '<span>List</span>' + 581 '<select data-post-add-list-select="true"' + disabledAttr + '>' + optionsHtml + '</select>' + 582 '</label>' + 583 '<label class="post-page-modal-field" data-post-add-list-new-row="true" hidden>' + 584 '<span>
584New list slug</span>' + 585 '<input type="text" data-post-add-list-new-slug="true" placeholder="new-list"' + disabledAttr + '>' + 586 '</label>' + 587 '<label class="post-page-modal-field">' + 588 '<span>Date <small>(optional)</small></span>' + 589 '<input type="text" data-post-add-list-date="true" placeholder="YYYY or YYYY-MM or YYYY-MM-DD"' + disabledAttr + '>' + 590 '</label>' + 591 '<label class="post-page-modal-field">' + 592 '<span>Text <small>(optional)</small></span>' + 593 '<input type="text" data-post-add-list-markdown="true" placeholder="Markdown line for this entry"' + disabledAttr + '>' + 594 '</label>' + 595 statusHtml + 596 '<div class="post-page-modal-actions">' + 597 '<button type="button" data-post-add-list-action="cancel"' + disabledAttr + '>Cancel</button>' + 598 '<button type="submit" class="post-page-modal-primary"' + disabledAttr + '>' + (addToListState.submitting ? 'Adding...' : 'Add to list') + '</button>' + 599 '</div>' + 600 '</form>'; 601 602 var select = dialog.querySelector('[data-post-add-list-select]'); 603 if (select && lists.length) { 604 select.value = normalizeListSlug(lists[0] && lists[0].slug) || 'list'; 605 } else if (select) { 606 select.value = '__new__'; 607 } 608 syncAddToListDialogNewRow(dialog); 609 var firstField = dialog.querySelector(lists.length ? '[data-post-add-list-select]' : '[data-post-add-list-new-slug]'); 610 if (firstField && !addToListState.loading && !addToListState.submitting) { 611 firstField.focus(); 612 } 613 } 614 615 function openAddToListDialog(postPath, token, csrf) { 616 addToListState.postPath = postPath; 617 addToListState.loading = true; 618 addToListState.submitting = false; 619 addToListState.lists = []; 620 addToListState.message = 'Loading lists...'; 621 addToListState.tone = 'info'; 622 renderAddToListDialog(); 623 postMenuBusy = true; 624 postForm('/cgi/blog-list-pages', { 625 session_token: token, 626 csrf_token: csrf 627 }).then(function (listsData) { 628 if (!listsData || !listsData.success) { 629 throw new Error((listsData && listsData.error) || 'Could not load lists'); 630 } 631 addToListState.lists = Array.isArray(listsData.lists) ? listsData.lists : []; 632 addToListState.message = ''; 633 addToListState.tone = ''; 634 }).catch(function (err) { 635 addToListState.lists = [{ slug: 'list', title: 'List' }]; 636 addToListState.message = err && err.message ? err.message : 'Could not load lists. You can still choose a list slug.'; 637 addToListState.tone = 'error'; 638 }).finally(function () { 639 addToListState.loading = false; 640 postMenuBusy = false; 641 renderAddToListDialog(); 642 }); 643 } 644 645 function submitAddToListDialog(form) { 646 if (addToListState.submitting || !addToListState.postPath) { 647 return; 648 } 649 var token = getSessionToken(); 650 var csrf = getCsrfToken(); 651 if (!token || !csrf) { 652 addToListState.message = 'Sign in as admin first.'; 653 addToListState.tone = 'error'; 654 renderAddToListDialog(); 655 return; 656 } 657 var slug = addToListSelectedSlug(form); 658 if (!slug) { 659 addToListState.message = 'List slug is required.'; 660 addToListState.tone = 'error'; 661 renderAddToListDialog(); 662 return; 663 } 664 var dateInput = form.querySelector('[data-post-add-list-date]'); 665 var markdownInput = form.querySelector('[data-post-add-list-markdown]'); 666 addToListState.submitting = true; 667 addToListState.message = 'Adding post to list draft...'; 668 addToListState.tone = 'info'; 669 renderAddToListDialog(); 670 postForm('/cgi/blog-add-post-to-list', { 671 list_slug: slug, 672 post_path: addToListState.postPath, 673 date: dateInput ? String(dateInput.value || '').trim() : '', 674 markdown: markdownInput ? String(markdownInput.value || '').trim() : '', 675 marker: 'list', 676 session_token: token, 677 csrf_token: csrf 678 }).then(function (data) { 679 if (!data || !data.success) { 680 throw new Error((data && data.error) || 'Could not add post to list'); 681 } 682 addToListState.message = (data && data.message) || ('Added to ' + slug + ' draft.'); 683 addToListState.tone = 'ok'; 684 addToListState.submitting = false;
685 renderAddToListDialog(); 686 window.setTimeout(closeAddToListDialog, 700); 687 }).catch(function (err) { 688 addToListState.message = err && err.message ? err.message : 'Could not add post to list.'; 689 addToListState.tone = 'error'; 690 addToListState.submitting = false; 691 renderAddToListDialog(); 692 }); 693 } 694 695 function runPostPageAction(action) { 696 var picked = String(action || '').trim(); 697 if (!picked || postMenuBusy || !currentRelPath) { 698 return; 699 } 700 701 if (picked === 'copy_nostr_address') { 702 writeClipboardText(currentNostrNaddr()) 703 .then(function () { 704 closePostPageMenu(); 705 }) 706 .catch(function (err) { 707 window.alert((err && err.message) || 'Could not copy Nostr address.'); 708 }); 709 return; 710 } 711 712 if (picked === 'copy_nostr_event') { 713 writeClipboardText(currentNostrNevent()) 714 .then(function () { 715 closePostPageMenu(); 716 }) 717 .catch(function (err) { 718 window.alert((err && err.message) || 'Could not copy Nostr event.'); 719 }); 720 return; 721 } 722 723 if (picked === 'open_nostr') { 724 var openValue = currentNostrOpenValue(); 725 if (!openValue) { 726 window.alert('This post does not have a Nostr address yet.'); 727 return; 728 } 729 closePostPageMenu(); 730 window.location.href = openValue.indexOf('nostr:') === 0 ? openValue : 'nostr:' + openValue; 731 return; 732 } 733 734 var token = getSessionToken(); 735 var csrf = getCsrfToken(); 736 if (!token || !csrf) { 737 window.alert('Sign in as admin first.'); 738 return; 739 } 740 741 if (picked === 'edit_post') { 742 postMenuBusy = true; 743 postForm('/cgi/blog-create-draft-from-post', { 744 post_path: currentRelPath, 745 session_token: token, 746 csrf_token: csrf 747 }) 748 .then(function (data) { 749 if (!data || !data.success) { 750 throw new Error((data && data.error) || 'Could not create draft from post'); 751 } 752 closePostPageMenu(); 753 var draftId = String((data && data.draft_id) || '').trim(); 754 if (!draftId) { 755 throw new Error('Draft was created but no draft id was returned'); 756 } 757 return openInlineEditorForDraft(draftId); 758 }) 759 .catch(function (err) { 760 window.alert(err.message || 'Could not create draft from post'); 761 }) 762 .finally(function () { 763 postMenuBusy = false; 764 }); 765 return; 766 } 767 768 if (picked === 'add_to_list') { 769 closePostPageMenu(); 770 openAddToListDialog(currentRelPath, token, csrf); 771 return; 772 } 773 774 if (picked === 'delete_post') { 775 if (!window.confirm('Delete this published post from this site? This cannot be undone.')) { 776 return; 777 } 778 postMenuBusy = true; 779 postForm('/cgi/blog-manage-post', { 780 action: 'delete', 781 post_path: currentRelPath, 782 session_token: token, 783 csrf_token: csrf 784 }) 785 .then(function (data) { 786 if (!data || !data.success) { 787 throw new Error((data && data.error) || 'Delete failed'); 788 } 789 window.location.href = '/archive'; 790 }) 791 .catch(function (err) { 792 window.alert(err.message || 'Delete failed'); 793 }) 794 .finally(function () { 795 postMenuBusy = false; 796 }); 797 } 798 } 799 800 function ensureMeta(name, value, attrType) { 801 if (!value) { 802 return; 803 } 804 var selector = attrType === 'property' 805 ? 'meta[property="' + name + '"]' 806 : 'meta[name="' + name + '"]'; 807 var node = document.querySelector(selector); 808 if (!node) { 809 node = document.createElement('meta'); 810 node.setAttribute(attrType === 'property' ? 'property' : 'name', name); 811 document.head.appendChild(node); 812 } 813 node.setAttribute('content', value); 814 } 815 816 function formatType(value) { 817 var raw = String(value || '').trim(); 818 if (!raw) { 819 return 'Post'; 820 } 821 return raw.replace(/[_-]+/g, ' ').replace(/\b\w/g, function (m) { return m.toUpperCase(); }); 822 } 823 824 function postYear(current) { 825 var explicitYear = String((current && current.year) || '').trim(); 826 if (/^[0-9]{4}$/.test(explicitYear)) { 827 return explicitYear; 828 } 829 var published = String((current && current.published_at) || '').trim(); 830 var match = published.match(/^([0-9]{4})/); 831 return match ? match[1] : ''; 832 } 833 834 function renderTagChip(tag, className) { 835 var t = String(tag || '').trim(); 836 if (!t) { 837 return ''; 838 } 839 return '<a class="' + escapeHtml(className || 'tag') + '" href="/tags#' + encodeURIComponent(t) + '">' + escapeHtml(t) + '</a>'; 840 } 841 842 function renderTags(current) { 843 var clean = Array.isArray(current && current.tags) ? current.tags : []; 844 var chips = []; 845 var type = String((current && current.type) || 'post').trim() || 'post'; 846 var year = postYear(current); 847 var seen = {}; 848 849 chips.push('<a class="tag blog-type-pill" href="/blog?type=' + encodeURIComponent(type) + '">' + escapeHtml(formatType(type)) + '</a>'); 850 if (year) { 851 chips.push('<a class="tag blog-year-pill" href="/blog?year=' + encodeURIComponent(year) + '">' + escapeHtml(year) + '</a>'); 852 seen[year.toLowerCase()] = true; 853 } 854 seen[type.toLowerCase()] = true; 855 seen[formatType(type).toLowerCase()] = true; 856 857 clean.forEach(function (tag) { 858 var t = String(tag || '').trim(); 859 if (!t) { 860 return; 861 } 862 if (seen[t.toLowerCase()]) { 863 return; 864 } 865 seen[t.toLowerCase()] = true; 866 chips.push(renderTagChip(t, 'tag')); 867 }); 868 869 if (!chips.length) { 870 return ''; 871 } 872 return '<div class="tags post-context-tags">' + chips.join('') + '</div>'; 873 } 874 875 function syncStatusConfig(status) { 876 switch (status) { 877 case 'local_newer_than_nostr': 878 return { 879 label: 'Server newer', 880 message: 'Server copy is newer than the latest published Nostr state. Visitors see the server copy.', 881 className: 'status-local-newer-than-nostr' 882 }; 883 case 'nostr_newer_than_local': 884 return { 885 label: 'Nostr newer', 886 message: 'Published Nostr state is newer than the server copy. Visitors still see the server copy until the site source changes.', 887 className: 'status-nostr-newer-than-local' 888 }; 889 case 'in_sync': 890 return { 891 label: 'Synced', 892 message: 'Server copy and published Nostr state are in sync.', 893 className: 'status-in-sync' 894 }; 895 case 'unpublished_local_changes': 896 return { 897 label: 'Server only', 898 message: 'This page exists only on the server so far.
898Visitors see the server copy.', 899 className: 'status-unpublished-local-changes' 900 }; 901 default: 902 return { 903 label: 'Sync unknown', 904 message: 'Cannot determine local-vs-Nostr sync status yet.', 905 className: 'status-unknown' 906 }; 907 } 908 } 909 910 function postSyncStatusPillHtml(payload) { 911 var sync = payload && payload.sync_status && typeof payload.sync_status === 'object' ? payload.sync_status : {}; 912 var status = String(sync.status || 'unknown').trim(); 913 var config = syncStatusConfig(status); 914 var message = String(sync.message || config.message).trim(); 915 return '<span class="page-sync-status-pill ' + config.className + '" title="' + escapeHtml(message) + '">' + escapeHtml(config.label) + '</span>'; 916 } 917 918 function existingSinglePostLayout() { 919 var root = document.body; 920 if (!root) { 921 return null; 922 } 923 var card = root.querySelector('.post-single-item'); 924 if (!card) { 925 return null; 926 } 927 return { 928 anchor: root, 929 card: card, 930 body: card.querySelector('.post-single-body') || card 931 }; 932 } 933 934 function renderPostEndTags(current) { 935 var content = renderTags(current); 936 if (!content) { 937 return ''; 938 } 939 return '<section class="post-end-tags">' + content + '</section>'; 940 } 941 942 function ensureZapHost(layout) { 943 if (!layout || !layout.card) { 944 return null; 945 } 946 var existing = layout.card.querySelector('.zap-inline-host.is-compact'); 947 if (existing) { 948 return existing; 949 } 950 var head = layout.card.querySelector('.post-head'); 951 if (!head) { 952 return null; 953 } 954 var host = document.createElement('div'); 955 host.className = 'post-zap-host'; 956 head.appendChild(host); 957 return host; 958 } 959 960 function ensurePostSyncStatusHost(layout) { 961 if (!layout || !layout.card) { 962 return null; 963 } 964 var main = layout.card.querySelector('.post-head-main'); 965 if (!main) { 966 return null; 967 } 968 var existing = main.querySelector('.post-sync-status-row'); 969 if (existing) { 970 return existing; 971 } 972 var host = document.createElement('div'); 973 host.className = 'post-sync-status-row'; 974 var byline = main.querySelector('.post-byline'); 975 if (byline && byline.parentNode === main) { 976 byline.insertAdjacentElement('afterend', host); 977 } else { 978 main.appendChild(host); 979 } 980 return host; 981 } 982 983 function renderPostSyncStatusPill(payload, layout) { 984 var host = ensurePostSyncStatusHost(layout); 985 if (!host) { 986 return; 987 } 988 host.innerHTML = postSyncStatusPillHtml(payload); 989 } 990 991 function renderPostZapUi(payload, layout) { 992 if (!window.blogZapUi || typeof window.blogZapUi.render !== 'function') { 993 return; 994 } 995 var current = payload && payload.current ? payload.current : null; 996 var nostr = current && current.nostr ? current.nostr : null; 997 if (!nostr) { 998 return; 999 } 1000 var host = ensureZapHost(layout); 1001 if (!host) { 1002 return; 1003 } 1004 window.blogZapUi.render(host, { 1005 zapConfig: payload ? payload.zap_config : null, 1006 display: 'compact', 1007 title: current ? current.title : '', 1008 target: { 1009 label: 'post', 1010 title: current ? current.title : '', 1011 recipientPubkey: nostr ? nostr.pubkey : '', 1012 eventId: nostr ? nostr.id : '', 1013 address: nostr ? nostr.address : '', 1014 kind: nostr ? nostr.kind : '' 1015 } 1016 }); 1017 } 1018 1019 function postNeedsWideLayout(body) { 1020 if (!(body instanceof HTMLElement)) { 1021 return false; 1022 } 1023 // Semantic wide-content blocks should opt into a wider card automatically. 1024 if (body.querySelector('table, pre, .table-wrapper, .wide-content, iframe, video')) { 1025 return true; 1026 } 1027 var candidates = body.querySelectorAll('table, pre, img, iframe, video, svg, canvas'); 1028 for (var i = 0; i < candidates.length; i += 1) { 1029 var el = candidates[i]; 1030 if (!(el instanceof HTMLElement)) { 1031 continue; 1032 } 1033 if ((Number(el.scrollWidth) || 0) - (Number(el.clientWidth) || 0) > 16) { 1034 return true; 1035 } 1036 } 1037 return false;
1038 } 1039 1040 function syncSinglePostLayoutForContent(layout) { 1041 if (!layout || !(layout.card instanceof HTMLElement)) { 1042 return; 1043 } 1044 var body = layout.body instanceof HTMLElement ? layout.body : layout.card.querySelector('.post-single-body'); 1045 var wide = postNeedsWideLayout(body); 1046 layout.card.classList.toggle('post-single-has-wide-content', wide); 1047 if (document && document.body) { 1048 document.body.classList.toggle('single-post-has-wide-content', wide); 1049 } 1050 } 1051 1052 function normalizeInlinePostType(raw) { 1053 var value = String(raw || '').trim().toLowerCase(); 1054 return value || 'longform'; 1055 } 1056 1057 function normalizeInlineTags(raw) { 1058 var seen = {}; 1059 var value = Array.isArray(raw) ? raw.join(',') : String(raw || ''); 1060 value = value.trim(); 1061 if (value === '[]') { 1062 return ''; 1063 } 1064 value = value.replace(/^\[+|\]+$/g, ''); 1065 return value 1066 .split(',') 1067 .map(function (tag) { 1068 var clean = String(tag || '').trim(); 1069 clean = clean.replace(/^["']+|["']+$/g, ''); 1070 clean = clean.replace(/^\[+|\]+$/g, ''); 1071 return clean; 1072 }) 1073 .filter(function (tag) { 1074 var key = tag.toLowerCase(); 1075 if (!tag || seen[key]) { 1076 return false; 1077 } 1078 seen[key] = true; 1079 return true; 1080 }) 1081 .join(', '); 1082 } 1083 1084 function parseInlineTagList(raw) { 1085 var normalized = normalizeInlineTags(raw); 1086 if (!normalized) { 1087 return []; 1088 } 1089 return normalized.split(',').map(function (tag) { 1090 return String(tag || '').trim(); 1091 }).filter(Boolean); 1092 } 1093 1094 function syncInlineTagsFromList() { 1095 inlineEditState.tags = normalizeInlineTags(inlineEditState.tagList || []); 1096 var hiddenTags = document.querySelector('input[data-post-inline-field="tags"]'); 1097 if (hiddenTags instanceof HTMLInputElement) { 1098 hiddenTags.value = inlineEditState.tags; 1099 } 1100 } 1101 1102 function inlineTagTokenHtml(tag) { 1103 var label = String(tag || '').trim(); 1104 return '<span class="tag-token" data-post-inline-tag-token="' + escapeHtml(label) + '">' + 1105 '<span class="tag-token-label">' + escapeHtml(label) + '</span>' + 1106 '<button type="button" class="tag-token-remove" data-post-inline-tag-remove="' + escapeHtml(label) + '" aria-label="Remove tag ' + escapeHtml(label) + '">Ã</button>' + 1107 '</span>'; 1108 } 1109 1110 function inlineTagsEditorHtml() { 1111 var tokensHtml = (inlineEditState.tagList || []).map(inlineTagTokenHtml).join(''); 1112 var emptyClass = ((inlineEditState.tagList || []).length || String(inlineEditState.tagsDraftText || '').trim()) ? '' : ' is-empty'; 1113 return '' + 1114 '<div class="tag-token-editor' + emptyClass + '" data-post-inline-field="tags-editor" role="group" aria-label="Post tags">' + 1115 tokensHtml + 1116 '<input type="text" class="tag-token-editor-input" data-post-inline-field="tags-input" value="' + escapeHtml(inlineEditState.tagsDraftText || '') + '" placeholder="tag, tag, tag" autocomplete="off" autocapitalize="none" spellcheck="false">' + 1117 '</div>'; 1118 } 1119 1120 function inlineTagsEditorNode() { 1121 return document.querySelector('[data-post-inline-field="tags-editor"]'); 1122 } 1123 1124 function inlineTagsEditorInputNode(editor) { 1125 if (!(editor instanceof HTMLElement)) { 1126 return null; 1127 } 1128 return editor.querySelector('[data-post-inline-field="tags-input"]'); 1129 } 1130 1131 function inlineTagsEditorReadDraft(editor) { 1132 var node = inlineTagsEditorInputNode(editor); 1133 if (!(node instanceof HTMLInputElement)) { 1134 return ''; 1135 } 1136 return String(node.value || ''); 1137 } 1138 1139 function inlineTagsEditorSyncDraft(editor) { 1140 inlineEditState.tagsDraftText = inlineTagsEditorReadDraft(editor); 1141 if (editor instanceof HTMLElement) { 1142 editor.classList.toggle('is-empty', !(inlineEditState.tagList || []).length && !String(inlineEditState.tagsDraftText || '').trim()); 1143 } 1144 } 1145 1146 function inlineTagsEditorRender(editor) { 1147 if (!(editor instanceof HTMLElement)) { 1148 return; 1149 } 1150 var html = (inlineEditState.tagList || []).map(inlineTagTokenHtml).join(''); 1151 html += '<input type="text" class="tag-token-editor-input" data-post-inline-field="tags-input" value="' + escapeHtml(inlineEditState.tagsDraftText || '') + '" placeholder="tag, tag, tag" autocomplete="off" autocapitalize="none" spellcheck="false">'; 1152 editor.innerHTML = html; 1153 editor.classList.toggle('is-empty', !(inlineEditState.tagList || []).length && !String(inlineEditState.tagsDraftText || '').trim()); 1154 } 1155 1156 function inlineTagsEditorFocusDraft(editor) { 1157 var node = inlineTagsEditorInputNode(editor); 1158 if (!(node instanceof HTMLInputElement)) { 1159 return; 1160 } 1161 if (document.activeElement !== node) {
1162 try { 1163 node.focus({ preventScroll: true }); 1164 } catch (_focusErr) { 1165 node.focus(); 1166 } 1167 } 1168 try { 1169 node.setSelectionRange(node.value.length, node.value.length); 1170 } catch (_selectionErr) {} 1171 } 1172 1173 function inlineTagsEditorCommit(editor, force) { 1174 if (!(editor instanceof HTMLElement)) { 1175 return false; 1176 } 1177 var raw = inlineTagsEditorReadDraft(editor); 1178 inlineEditState.tagsDraftText = raw; 1179 var value = String(raw || ''); 1180 var hasSplit = /[,\n]/.test(value); 1181 if (!hasSplit && !force) { 1182 return false; 1183 } 1184 var pieces = value.split(/[,\n]/g).map(function (piece) { 1185 return String(piece || '').trim(); 1186 }).filter(Boolean); 1187 if (!pieces.length) { 1188 inlineEditState.tagsDraftText = hasSplit ? '' : value; 1189 inlineTagsEditorRender(editor); 1190 syncInlineTagsFromList(); 1191 return false; 1192 } 1193 var changed = false; 1194 pieces.forEach(function (tag) { 1195 if (!tag) { 1196 return; 1197 } 1198 var lowerTag = tag.toLowerCase(); 1199 var exists = (inlineEditState.tagList || []).some(function (item) { 1200 return String(item || '').trim().toLowerCase() === lowerTag; 1201 }); 1202 if (exists) { 1203 return; 1204 } 1205 inlineEditState.tagList.push(tag); 1206 changed = true; 1207 }); 1208 inlineEditState.tagsDraftText = ''; 1209 syncInlineTagsFromList(); 1210 inlineTagsEditorRender(editor); 1211 inlineTagsEditorFocusDraft(editor); 1212 return changed || hasSplit; 1213 } 1214 1215 function inlineRemoveTag(tag) { 1216 var value = String(tag || '').trim(); 1217 if (!value) { 1218 return false; 1219 } 1220 var next = (inlineEditState.tagList || []).filter(function (item) { 1221 return String(item || '').trim().toLowerCase() !== value.toLowerCase(); 1222 }); 1223 if (next.length === (inlineEditState.tagList || []).length) { 1224 return false; 1225 } 1226 inlineEditState.tagList = next; 1227 syncInlineTagsFromList(); 1228 return true; 1229 } 1230 1231 function inlinePostTypeLabel(postType) { 1232 var value = normalizeInlinePostType(postType); 1233 if (value === 'shortform') return 'shortform'; 1234 if (value === 'capture-media') return 'capture'; 1235 if (value === 'upload-media') return 'media'; 1236 if (value === 'attachment') return 'attachment'; 1237 if (value === 'audio-note') return 'audio'; 1238 if (value === 'link-share') return 'link'; 1239 if (value === 'go-live') return 'go live'; 1240 return 'post'; 1241 } 1242 1243 function inlinePostKindPillClass(postType) { 1244 var value = normalizeInlinePostType(postType); 1245 if (value === 'shortform') return 'is-shortform'; 1246 if (value === 'capture-media') return 'is-capture-media'; 1247 if (value === 'upload-media') return 'is-upload-media'; 1248 if (value === 'attachment') return 'is-attachment'; 1249 if (value === 'audio-note') return 'is-audio-note'; 1250 if (value === 'link-share') return 'is-link-share'; 1251 if (value === 'go-live') return 'is-go-live'; 1252 return 'is-longform'; 1253 } 1254 1255 function inlineComposePostTypeIconSvg(type) { 1256 var value = normalizeInlinePostType(type); 1257 if (value === 'shortform') { 1258 return '<svg class="compose-post-type-icon" viewBox="0 0 96.314 100" fill="none" aria-hidden="true">' + 1259 '<path fill="currentColor" stroke="none" d="M84.39,11.825C83.396,8.452,80.595,5,75.097,5c-5.605,0-16.216,3.389-16.216,16.216c0,2.613-1.786,3.205-3.243,3.243c-27.467,0-47.354,32.62-48.187,34.006L5,62.549l4.7,0.785c14.088,2.349,26.242,3.646,36.208,3.879V95h6.486V67.204c1.035-0.03,2.055-0.068,3.041-0.127V95h6.486V66.438c15.209-2.16,22.906-8.613,22.906-19.276V24.459h6.486v-3.243C91.314,17.868,89.414,13.124,84.39,11.825z M78.34,47.162c0,3.367,0,13.606-29.252,13.606c-9.03,0-20.227-1.017-33.326-3.031c6.043-8.383,21.414-26.792,39.876-26.792c3.908,0,9.729-2.591,9.729-9.729c0-9.157,8.12-9.711,9.729-9.73c2.609,0,3.205,1.783,3.243,3.243V47.162z M75.446,17.837c0,1.384-1.128,2.505-2.509,2.505s-2.508-1.121-2.508-2.505c0-1.384,1.127-2.505,2.508-2.505S75.446,16.453,75.446,17.837z"/>' + 1260 '</svg>'; 1261 } 1262 if (value === 'longform') { 1263 return '<svg class="compose-post-type-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true">' + 1264 '<path d="M5 7H19M5 11H19M5 15H17M5 19H19" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>' + 1265 '</svg>'; 1266 } 1267 if (value === 'capture-media') {
1268 return '<svg class="compose-post-type-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true">' + 1269 '<rect x="4" y="7" width="16" height="11" rx="2.2" stroke="currentColor" stroke-width="1.8"/>' + 1270 '<circle cx="12" cy="12.5" r="2.6" stroke="currentColor" stroke-width="1.8"/>' + 1271 '<path d="M9.2 7L10.4 5.2H13.6L14.8 7" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>' + 1272 '</svg>'; 1273 } 1274 if (value === 'upload-media') { 1275 return '<svg class="compose-post-type-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true">' + 1276 '<rect x="3.6" y="5.1" width="16.8" height="13.8" rx="2.2" stroke="currentColor" stroke-width="1.8"/>' + 1277 '<circle cx="8.4" cy="9.7" r="1.4" fill="currentColor"/>' + 1278 '<path d="M5.8 16.4L10.2 12.1L13.1 15L15.8 12.5L18.2 16.4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>' + 1279 '</svg>'; 1280 } 1281 if (value === 'attachment') { 1282 return '<svg class="compose-post-type-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true">' + 1283 '<path d="M21.4 11.1L12.9 19.6C10.6 21.9 6.8 21.9 4.5 19.6C2.2 17.3 2.2 13.5 4.5 11.2L13 2.8C14.6 1.2 17.1 1.2 18.7 2.8C20.2 4.4 20.2 6.9 18.7 8.5L10.2 16.9C9.3 17.8 7.8 17.8 6.9 16.9C6 16 6 14.5 6.9 13.6L14.8 5.7" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>' + 1284 '</svg>'; 1285 } 1286 if (value === 'audio-note') { 1287 return '<svg class="compose-post-type-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true">' + 1288 '<rect x="9" y="4.2" width="6" height="10" rx="3" stroke="currentColor" stroke-width="1.8"/>' + 1289 '<path d="M6.6 11.2C6.6 14.4 9.1 16.9 12 16.9C14.9 16.9 17.4 14.4 17.4 11.2M12 16.9V20.2M9.3 20.2H14.7" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>' + 1290 '</svg>'; 1291 } 1292 if (value === 'link-share') { 1293 return '<svg class="compose-post-type-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true">' + 1294 '<rect x="2.7" y="8.5" width="8.8" height="7" rx="3.5" stroke="currentColor" stroke-width="1.8"/>' + 1295 '<rect x="12.5" y="8.5" width="8.8" height="7" rx="3.5" stroke="currentColor" stroke-width="1.8"/>' + 1296 '<path d="M9.8 12H14.2" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>' + 1297 '</svg>'; 1298 } 1299 if (value === 'go-live') { 1300 return '<svg class="compose-post-type-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true">' + 1301 '<circle cx="12" cy="12" r="7" stroke="currentColor" stroke-width="1.8"/>' + 1302 '<circle cx="12" cy="12" r="2.5" fill="currentColor"/>' + 1303 '</svg>'; 1304 } 1305 return '<svg class="compose-post-type-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true">' + 1306 '<path d="M5 7H19M5 11H19M5 15H17M5 19H19" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>' + 1307 '</svg>'; 1308 } 1309 1310 function inlineComposeCloseIconSvg() { 1311 return '<svg class="compose-close-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M6 6L18 18" stroke="currentColor" stroke-width="2.1" stroke-linecap="round"/><path d="M18 6L6 18" stroke="currentColor" stroke-width="2.1" stroke-linecap="round"/></svg>'; 1312 } 1313 1314 function inlineComposeTrashIconSvg() { 1315 return '<svg class="compose-trash-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M4.5 7.2H19.5" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/><path d="M9.4 7.2V5.4C9.4 4.6 10 4 10.8 4H13.2C14 4 14.6 4.6 14.6 5.4V7.2" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/><path d="M7.6 7.2L8.4 18.1C8.5 19.2 9.4 20 10.5 20H13.5C14.6 20 15.5 19.2 15.6 18.1L16.4 7.2" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/><path d="M10.4 10.4V16.1" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/><path d="M13.6 10.4V16.1" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg>'; 1316 } 1317 1318 function inlineToolbarButtonHtml(action, label, glyph) { 1319 return '<button type="button" class="unobtrusive-icon-button toolbar-button" data-post-inline-toolbar="' + escapeHtml(action) + '" aria-label="' + escapeHtml(label) + '" title="' + escapeHtml(label) + '">' + glyph + '</button>'; 1320 } 1321 1322 function inlineToolbarHtml() { 1323 return '' + 1324 '<div class="toolbar blog-compose-toolbar" aria-label="Markdown toolbar">' + 1325 inlineToolbarButtonHtml('bold', 'Bold', '<span class="tb-glyph tb-glyph-bold" aria-hidden="true">B</span>') + 1326 inlineToolbarButtonHtml('italic', 'Italic', '<span class="tb-glyph tb-glyph-italic" aria-hidden="true">I</span>') + 1327 inlineToolbarButtonHtml('h2', 'Heading 2', '<span class="tb-glyph tb-glyph-heading" aria-hidden="true">H2</span>') + 1328 inlineToolbarButtonHtml('h3', 'Heading 3', '<span class="tb-glyph tb-glyph-heading" aria-hidden="true">H3</span>') + 1329 inlineToolbarButtonHtml('code', 'Inline code', '<span class="tb-glyph tb-glyph-code" aria-hidden="true"></></span>') + 1330 inlineToolbarButtonHtml('code_block', 'Code block', '<span class="tb-glyph tb-glyph-code" aria-hidden="true">{ }</span>') + 1331 inlineToolbarButtonHtml('link', 'Insert link', '<svg class="tb-icon" viewB
1331ox="0 0 24 24" fill="none" aria-hidden="true"><path d="M10 13.9L8.4 15.5C6.8 17.1 4.2 17.1 2.6 15.5C1 13.9 1 11.3 2.6 9.7L4.2 8.1" stroke="currentColor" stroke-width="2.15" stroke-linecap="round"/><path d="M14 10.1L15.6 8.5C17.2 6.9 19.8 6.9 21.4 8.5C23 10.1 23 12.7 21.4 14.3L19.8 15.9" stroke="currentColor" stroke-width="2.15" stroke-linecap="round"/><path d="M9 12H15" stroke="currentColor" stroke-width="2.15" stroke-linecap="round"/></svg>') + 1332 inlineToolbarButtonHtml('quote', 'Quote', '<span class="tb-glyph tb-glyph-quote" aria-hidden="true">ââ</span>') + 1333 inlineToolbarButtonHtml('ul', 'Bullet list', '<svg class="tb-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true"><circle cx="5.1" cy="7.2" r="1.2" fill="currentColor"/><circle cx="5.1" cy="12" r="1.2" fill="currentColor"/><circle cx="5.1" cy="16.8" r="1.2" fill="currentColor"/><path d="M9.2 7.2H19" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/><path d="M9.2 12H19" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/><path d="M9.2 16.8H19" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg>') + 1334 inlineToolbarButtonHtml('ol', 'Numbered list', '<span class="tb-glyph tb-glyph-list" aria-hidden="true">1.</span>') + 1335 '</div>'; 1336 } 1337 1338 function inlineInsertMarkdown(action) { 1339 var textarea = document.querySelector('textarea[data-post-inline-field="content"]'); 1340 if (!(textarea instanceof HTMLTextAreaElement)) { 1341 return; 1342 } 1343 var start = textarea.selectionStart; 1344 var end = textarea.selectionEnd; 1345 var value = String(textarea.value || ''); 1346 var selected = value.slice(start, end); 1347 var before = value.slice(0, start); 1348 var after = value.slice(end); 1349 var replacement = selected; 1350 var selectStart = start; 1351 var selectEnd = end; 1352 1353 if (action === 'bold') { 1354 replacement = '**' + selected + '**'; 1355 selectStart = start + 2; 1356 selectEnd = end + 2; 1357 } else if (action === 'italic') { 1358 replacement = '*' + selected + '*'; 1359 selectStart = start + 1; 1360 selectEnd = end + 1; 1361 } else if (action === 'h2') { 1362 replacement = '## ' + (selected || 'Heading'); 1363 selectStart = start + 3; 1364 selectEnd = start + replacement.length; 1365 } else if (action === 'h3') { 1366 replacement = '### ' + (selected || 'Heading'); 1367 selectStart = start + 4; 1368 selectEnd = start + replacement.length; 1369 } else if (action === 'code') { 1370 replacement = '`' + (selected || 'code') + '`'; 1371 selectStart = start + 1; 1372 selectEnd = start + replacement.length - 1; 1373 } else if (action === 'code_block') { 1374 replacement = '```\n' + (selected || '') + '\n```'; 1375 selectStart = start + 4; 1376 selectEnd = start + replacement.length - 4; 1377 } else if (action === 'link') { 1378 replacement = '[' + (selected || 'text') + '](https://)'; 1379 selectStart = start + replacement.length - 9; 1380 selectEnd = start + replacement.length - 1; 1381 } else if (action === 'quote') { 1382 replacement = '> ' + (selected || 'Quote'); 1383 selectStart = start + 2; 1384 selectEnd = start + replacement.length; 1385 } else if (action === 'ul') { 1386 replacement = '- ' + (selected || 'List item'); 1387 selectStart = start + 2; 1388 selectEnd = start + replacement.length; 1389 } else if (action === 'ol') { 1390 replacement = '1. ' + (selected || 'List item'); 1391 selectStart = start + 3; 1392 selectEnd = start + replacement.length; 1393 } 1394 1395 textarea.value = before + replacement + after; 1396 textarea.selectionStart = selectStart; 1397 textarea.selectionEnd = selectEnd; 1398 textarea.focus(); 1399 inlineEditState.content = String(textarea.value || ''); 1400 queueInlineAutosave(); 1401 } 1402 1403 function renderInlineEditorStatusOnly() { 1404 var host = ensureInlineEditorHost(); 1405 if (!host || !inlineEditState.open) { 1406 return; 1407 } 1408 var output = host.querySelector('.blog-compose-status-row .output'); 1409 if (output) { 1410 output.className = 'output' + (inlineEditState.outputTone ? (' ' + inlineEditState.outputTone) : ''); 1411 output.textContent = inlineEditState.output || ''; 1412 } 1413 var autosave = host.querySelector('.compose-editor-autosave'); 1414 if (autosave) { 1415 var status = String(inlineEditState.saveStatus || ''); 1416 autosave.hidden = !status; 1417 autosave.classList.toggle('is-saving', status === 'saving'); 1418 autosave.classList.toggle('is-error', status === 'error'); 1419 autosave.textContent = status === 'saving' ? 'Saving...' : (status === 'error' ? 'Save failed' : 'â Saved'); 1420 } 1421 } 1422 1423 function inlineAuthPayload() { 1424 return { 1425 session_token: getSessionToken(), 1426 csrf_token: getCsrfToken() 1427 }; 1428 } 1429 1430 function ensureInlineEditorHost() { 1431 var card = document.querySelector('.post-single-item'); 1432 if (!card) { 1433 return null; 1434 } 1435 var existing = card.querySelector('.post-inline-editor-host'); 1436 if (existing) { 1437 return existing; 1438 } 1439 var host = document.createElement('div'); 1440 host.className = 'post-inline-editor-host'; 1441 var body = card.querySelector('.post-single-body'); 1442 if (body && body.parentNode === card) { 1443 card.insertBefore(host, body); 1444 } else { 1445 card.appendChild(host); 1446 } 1447 return host; 1448 } 1449 1450 function renderInlineEditor() { 1451 var host = ensureInlineEditorHost(); 1452 if (!host) { 1453 return; 1454 } 1455 if (!inlineEditState.open) { 1456 host.innerHTML = ''; 1457 host.hidden = true; 1458 return; 1459 } 1460 host.hidden = false;
1461 if (!Array.isArray(inlineEditState.tagList)) { 1462 inlineEditState.tagList = parseInlineTagList(inlineEditState.tags || ''); 1463 } 1464 if (typeof inlineEditState.tagsDraftText !== 'string') { 1465 inlineEditState.tagsDraftText = ''; 1466 } 1467 var outputClass = 'output' + (inlineEditState.outputTone ? (' ' + inlineEditState.outputTone) : ''); 1468 var hasEditableFilename = !!String(inlineEditState.sourcePostPath || '').trim(); 1469 var showTitleField = normalizeInlinePostType(inlineEditState.postType) !== 'shortform'; 1470 var currentFilename = normalizePostFilename(inlineEditState.postFilename || filenameFromPostPath(inlineEditState.sourcePostPath)); 1471 if (!currentFilename) { 1472 currentFilename = 'post'; 1473 } 1474 var filenameDisplayHidden = inlineEditState.postFilenameEditing ? ' hidden' : ''; 1475 var filenameEditHidden = inlineEditState.postFilenameEditing ? '' : ' hidden'; 1476 var filenameRow = ''; 1477 if (hasEditableFilename) { 1478 filenameRow = '' + 1479 '<div class="field-row compose-post-filename-row">' + 1480 '<label><strong>Slug/Filename</strong></label>' + 1481 '<div class="compose-post-filename-display"' + filenameDisplayHidden + '>' + 1482 '<code class="compose-post-filename-value">posts/' + escapeHtml(currentFilename) + '.md</code>' + 1483 '<button type="button" class="list-inline-edit-link" data-post-inline-action="edit_filename">Edit...</button>' + 1484 '</div>' + 1485 '<div class="compose-post-filename-edit-wrap"' + filenameEditHidden + '>' + 1486 '<span class="compose-post-filename-prefix">posts/</span>' + 1487 '<input type="text" data-post-inline-field="post_filename" inputmode="url" spellcheck="false" value="' + escapeHtml(currentFilename) + '" placeholder="my-post-slug">' + 1488 '<span class="compose-post-filename-suffix">.md</span>' + 1489 '</div>' + 1490 '</div>'; 1491 } 1492 var postTypeIcon = inlineComposePostTypeIconSvg(inlineEditState.postType); 1493 var postTypePill = '<span class="nostr-page-kind-badge ' + inlinePostKindPillClass(inlineEditState.postType) + '">' + escapeHtml(inlinePostTypeLabel(inlineEditState.postType)) + '</span><span class="nostr-target-pill">Post type locked</span>'; 1494 host.innerHTML = '' + 1495 '<article class="post-item blog-post-item blog-compose-card">' + 1496 '<div class="blog-compose-body">' + 1497 '<div class="field-row blog-compose-head-row is-type-collapsed" data-compose-head-row>' + 1498 '<div class="compose-post-type-control is-collapsed" data-compose-type-control>' + 1499 '<button type="button" class="compose-post-type-current-btn unobtrusive-icon-button" disabled aria-disabled="true" aria-label="Post type is locked for existing posts" title="Post type is locked for existing posts">' + postTypeIcon + '</button>' + 1500 '</div>' + 1501 '<div class="blog-compose-head-actions">' + 1502 '<button type="button" class="unobtrusive-icon-button blog-compose-close" data-post-inline-action="close" aria-label="Close compose" title="Close compose">' + inlineComposeCloseIconSvg() + '<span class="sr-only">Close compose</span></button>' + 1503 '</div>' + 1504 '</div>' + 1505 '<div class="field-row blog-compose-title-row"' + (showTitleField ? '' : ' hidden aria-hidden="true"') + '>' + 1506 '<label><strong>Title</strong></label>' + 1507 '<input type="text" data-compose-field="title" data-post-inline-field="title" placeholder="Post title" value="' + escapeHtml(inlineEditState.title) + '">' + 1508 '</div>' + 1509 filenameRow + 1510 '<div class="field-row">' + 1511 '<label><strong>Body</strong></label>' + 1512 '<div class="editor-shell blog-compose-editor-shell">' + 1513 inlineToolbarHtml() + 1514 '<textarea data-post-inline-field="content" rows="14" placeholder="Post body">' + escapeHtml(inlineEditState.content) + '</textarea>' + 1515 '<div class="autosave-indicator compose-editor-autosave' + (inlineEditState.saveStatus === 'saving' ? ' is-saving' : '') + (inlineEditState.saveStatus === 'error' ? ' is-error' : '') + '"' + (inlineEditState.saveStatus ? '' : ' hidden') + '>
1515' + (inlineEditState.saveStatus === 'saving' ? 'Saving...' : (inlineEditState.saveStatus === 'error' ? 'Save failed' : 'â Saved')) + '</div>' + 1516 '</div>' + 1517 '</div>' + 1518 '<div class="field-row compose-tags-row">' + 1519 '<label><strong>Tags</strong></label>' + 1520 '<input type="hidden" data-post-inline-field="tags" value="' + escapeHtml(inlineEditState.tags) + '">' + 1521 '<div class="tag-editor">' + inlineTagsEditorHtml() + '</div>' + 1522 '</div>' + 1523 '</div>' + 1524 '<div class="compose-footer blog-compose-footer">' + 1525 '<div class="compose-actions blog-compose-footer-actions">' + 1526 '<button type="button" class="icon-danger unobtrusive-icon-button blog-compose-delete" data-post-inline-action="delete" aria-label="Delete draft" title="Delete draft"' + (inlineEditState.busy ? ' disabled aria-disabled="true"' : '') + '>' + inlineComposeTrashIconSvg() + '</button>' + 1527 '<div class="blog-compose-publish-stack">' + 1528 '<div class="blog-compose-publish-row">' + 1529 postTypePill + 1530 '<button type="button" class="list-admin-primary-btn blog-compose-btn" data-post-inline-action="save" title="Saves unpublished changes only"' + (inlineEditState.busy ? ' disabled aria-disabled="true"' : '') + '>Save Draft</button>' + 1531 '<button type="button" class="list-admin-primary-btn blog-compose-btn" data-post-inline-action="publish"' + (inlineEditState.busy ? ' disabled aria-disabled="true"' : '') + '>Publish Changes</button>' + 1532 '</div>' + 1533 '</div>' + 1534 '</div>' + 1535 '<div class="blog-compose-status-row">' + 1536 '<div class="' + outputClass + '">' + escapeHtml(inlineEditState.output) + '</div>' + 1537 '</div>' + 1538 '</div>' + 1539 '</article>'; 1540 } 1541 1542 function setInlineOutput(message, tone) { 1543 inlineEditState.output = String(message || ''); 1544 inlineEditState.outputTone = String(tone || ''); 1545 } 1546 1547 function inlineEditorPayload(action) { 1548 var postType = normalizeInlinePostType(inlineEditState.postType || 'longform'); 1549 return { 1550 action: action, 1551 draft_id: String(inlineEditState.draftId || ''), 1552 source_post_path: String(inlineEditState.sourcePostPath || ''), 1553 post_filename: String(inlineEditState.postFilename || ''), 1554 title: postType === 'shortform' ? '' : String(inlineEditState.title || '').trim(), 1555 tags: normalizeInlineTags(inlineEditState.tags || ''), 1556 summary: '', 1557 content: String(inlineEditState.content || ''), 1558 post_type: postType, 1559 scheduled_at: '', 1560 publish_mode: 'immediate', 1561 publish_destination: 'local_only' 1562 }; 1563 } 1564 1565 function saveInlineDraft(action) { 1566 if (!inlineEditState.open) { 1567 return Promise.resolve(false); 1568 } 1569 if (action !== 'autosave' && inlineEditState.autosaveTimer) { 1570 window.clearTimeout(inlineEditState.autosaveTimer); 1571 inlineEditState.autosaveTimer = null; 1572 } 1573 if (inlineEditState.busy) { 1574 inlineEditState.dirtyDuringSave = true; 1575 if (action !== 'autosave') { 1576 setInlineOutput('Finishing the current save. Try again in a moment.', 'warn'); 1577 renderInlineEditorStatusOnly(); 1578 } 1579 return Promise.resolve(false); 1580 } 1581 var auth = inlineAuthPayload(); 1582 if (!auth.session_token || !auth.csrf_token) { 1583 setInlineOutput('Sign in again to edit this post.', 'error'); 1584 renderInlineEditor(); 1585 return Promise.resolve(false); 1586 } 1587 var payload = inlineEditorPayload(action); 1588 payload.session_token = auth.session_token; 1589 payload.csrf_token = auth.csrf_token; 1590 inlineEditState.busy = true; 1591 inlineEditState.dirtyDuringSave = false; 1592 if (action === 'autosave') { 1593 inlineEditState.saveStatus = 'saving'; 1594 } 1595 if (action === 'autosave') { 1596 renderInlineEditorStatusOnly(); 1597 } else { 1598 renderInlineEditor(); 1599 } 1600 return postForm('/cgi/blog-save-post', payload).then(function (data) { 1601 if (data && data.draft_id) { 1602 inlineEditState.draftId = String(data.draft_id || '').trim(); 1603 } 1604 inlineEditState.tags = normalizeInlineTags(inlineEditState.tags || ''); 1605 inlineEditState.postFilename = normalizePostFilename(inlineEditState.postFilename || filenameFromPostPath(inlineEditState.sourcePostPath)); 1606 if (action === 'autosave') { 1607 inlineEditState.saveStatus = 'saved'; 1608 renderInlineEditorStatusOnly(); 1609 } 1610 if (action === 'publish_now') { 1611 setInlineOutput('Published. Reloading postâ¦', 'ok'); 1612 renderInlineEditor(); 1613 window.setTimeout(function () { 1614 window.location.reload(); 1615 }, 350); 1616 } else if (action === 'save_draft') { 1617 setInlineOutput('Working draft saved. Live post is unchanged.', 'ok'); 1618 } 1619 return true; 1620 }).catch(function (err) { 1621 if (action === 'autosave') { 1622 inlineEditState.saveStatus = 'error'; 1623 renderInlineEditorStatusOnly(); 1624 } else { 1625 setInlineOutput(err && err.message ? err.message : 'Save failed', 'error'); 1626 } 1627 renderInlineEditor(); 1628 return false;
1629 }).finally(function () { 1630 inlineEditState.busy = false; 1631 if (inlineEditState.dirtyDuringSave) { 1632 inlineEditState.dirtyDuringSave = false; 1633 queueInlineAutosave(); 1634 if (action !== 'autosave') { 1635 renderInlineEditorStatusOnly(); 1636 } 1637 return; 1638 } 1639 if (action === 'autosave') { 1640 renderInlineEditorStatusOnly(); 1641 } else { 1642 renderInlineEditor(); 1643 } 1644 }); 1645 } 1646 1647 function queueInlineAutosave() { 1648 if (!inlineEditState.open) { 1649 return; 1650 } 1651 if (inlineEditState.busy) { 1652 inlineEditState.dirtyDuringSave = true; 1653 } 1654 if (inlineEditState.autosaveTimer) { 1655 window.clearTimeout(inlineEditState.autosaveTimer); 1656 } 1657 inlineEditState.saveStatus = 'saving'; 1658 renderInlineEditorStatusOnly(); 1659 inlineEditState.autosaveTimer = window.setTimeout(function () { 1660 inlineEditState.autosaveTimer = null; 1661 saveInlineDraft('autosave'); 1662 }, 1200); 1663 } 1664 1665 function closeInlineEditor() { 1666 if (inlineEditState.autosaveTimer) { 1667 window.clearTimeout(inlineEditState.autosaveTimer); 1668 inlineEditState.autosaveTimer = null; 1669 } 1670 inlineEditState.open = false; 1671 inlineEditState.busy = false; 1672 inlineEditState.draftId = ''; 1673 inlineEditState.sourcePostPath = ''; 1674 inlineEditState.postFilename = ''; 1675 inlineEditState.postFilenameEditing = false; 1676 inlineEditState.postType = 'longform'; 1677 inlineEditState.title = ''; 1678 inlineEditState.content = ''; 1679 inlineEditState.tags = ''; 1680 inlineEditState.tagList = []; 1681 inlineEditState.tagsDraftText = ''; 1682 inlineEditState.output = ''; 1683 inlineEditState.outputTone = ''; 1684 inlineEditState.saveStatus = ''; 1685 renderInlineEditor(); 1686 } 1687 1688 function deleteInlineDraft() { 1689 if (!inlineEditState.draftId || inlineEditState.busy) { 1690 closeInlineEditor(); 1691 return; 1692 } 1693 if (!window.confirm('Delete this draft?')) { 1694 return; 1695 } 1696 var auth = inlineAuthPayload(); 1697 if (!auth.session_token || !auth.csrf_token) { 1698 setInlineOutput('Sign in again to delete this draft.', 'error'); 1699 renderInlineEditor(); 1700 return; 1701 } 1702 inlineEditState.busy = true; 1703 renderInlineEditor(); 1704 postForm('/cgi/blog-delete-draft', { 1705 draft_id: String(inlineEditState.draftId || ''), 1706 session_token: auth.session_token, 1707 csrf_token: auth.csrf_token 1708 }).then(function () { 1709 closeInlineEditor(); 1710 }).catch(function (err) { 1711 inlineEditState.busy = false; 1712 setInlineOutput(err && err.message ? err.message : 'Could not delete draft', 'error'); 1713 renderInlineEditor(); 1714 }); 1715 } 1716 1717 function openInlineEditorForDraft(draftId) { 1718 var auth = inlineAuthPayload(); 1719 if (!auth.session_token || !auth.csrf_token) { 1720 return Promise.reject(new Error('Sign in as admin first.')); 1721 } 1722 return postForm('/cgi/blog-get-draft', { 1723 draft_id: String(draftId || ''), 1724 session_token: auth.session_token, 1725 csrf_token: auth.csrf_token 1726 }).then(function (data) { 1727 if (!data || !data.success || !data.draft) { 1728 throw new Error((data && data.error) || 'Could not load draft'); 1729 } 1730 var draft = data.draft || {}; 1731 inlineEditState.open = true; 1732 inlineEditState.busy = false; 1733 inlineEditState.draftId = String(draft.draft_id || draftId || '').trim(); 1734 inlineEditState.sourcePostPath = normalizePostMdPath(draft.source_post_path || ''); 1735 inlineEditState.postFilename = normalizePostFilename(draft.post_filename || filenameFromPostPath(inlineEditState.sourcePostPath)); 1736 inlineEditState.postFilenameEditing = false; 1737 inlineEditState.postType = normalizeInlinePostType(draft.post_type || 'longform'); 1738 inlineEditState.title = String(draft.title || ''); 1739 inlineEditState.content = String(draft.content || ''); 1740 inlineEditState.tags = normalizeInlineTags(draft.tags || ''); 1741 inlineEditState.tagList = parseInlineTagList(inlineEditState.tags); 1742 inlineEditState.tagsDraftText = ''; 1743 inlineEditState.output = ''; 1744 inlineEditState.outputTone = ''; 1745 inlineEditState.saveStatus = ''; 1746 renderInlineEditor(); 1747 var host = ensureInlineEditorHost(); 1748 if (host && typeof host.scrollIntoView === 'function') { 1749 host.scrollIntoView({ behavior: 'smooth', block: 'start', inline: '
1749nearest' }); 1750 } 1751 return true; 1752 }); 1753 } 1754 1755 function navColumn(label, post, cls) { 1756 if (!post) { 1757 return '<div class="' + cls + '"><span class="post-nav-empty">' + escapeHtml(label) + ': none</span></div>'; 1758 } 1759 return '<div class="' + cls + '">' + 1760 '<span class="post-nav-label">' + escapeHtml(label) + '</span>' + 1761 '<a href="' + escapeHtml(post.url || '#') + '">' + escapeHtml(post.title || 'Untitled') + '</a>' + 1762 '</div>'; 1763 } 1764 1765 function postNavHtml(newerHtml, olderHtml) { 1766 return '<nav class="post-nav post-nav-enhanced" aria-label="Post navigation">' + newerHtml + olderHtml + '</nav>'; 1767 } 1768 1769 function renderPostNav(payload) { 1770 return postNavHtml( 1771 navColumn('Newer', payload.newer, 'post-nav-prev'), 1772 navColumn('Older', payload.older, 'post-nav-next') 1773 ); 1774 } 1775 1776 function normalizeMarkdownSeparators(body) { 1777 if (!(body instanceof HTMLElement)) { 1778 return; 1779 } 1780 Array.prototype.forEach.call(body.querySelectorAll('p'), function (node) { 1781 var text = String(node.textContent || '').replace(/\s+/g, '').trim(); 1782 if (text === '***') { 1783 node.classList.add('markdown-separator'); 1784 } 1785 }); 1786 } 1787 1788 function normalizeAllMarkdownSeparators() { 1789 Array.prototype.forEach.call(document.querySelectorAll('.post-single-body, .post-content, .list-entry-markdown, .nostr-page-extra'), normalizeMarkdownSeparators); 1790 } 1791 1792 function renderCommentRow(comment) { 1793 var created = comment.created_at_iso ? escapeHtml(comment.created_at_iso.replace('T', ' ').replace('Z', ' UTC')) : ''; 1794 var pubkey = escapeHtml(String(comment.pubkey || '').slice(0, 16)); 1795 var body = escapeHtml(comment.content || '').replace(/\n/g, '<br>'); 1796 return '<article class="post-comment post-comment-island">' + 1797 '<header><span class="post-comment-author">' + pubkey + '</span>' + (created ? ' <span class="post-comment-time">' + created + '</span>' : '') + '</header>' + 1798 '<p>' + body + '</p>' + 1799 '</article>'; 1800 } 1801 1802 function renderComments(comments) { 1803 var list = Array.isArray(comments) ? comments : []; 1804 var container = document.getElementById('post-comments-list'); 1805 if (!container) { 1806 return; 1807 } 1808 if (!list.length) { 1809 container.innerHTML = '<p class="placeholder post-comments-empty post-comment-island">No comments mirrored yet.</p>'; 1810 return; 1811 } 1812 container.innerHTML = list.map(renderCommentRow).join(''); 1813 } 1814 1815 function setCommentCount(count) { 1816 var badge = document.getElementById('post-comments-count'); 1817 if (!badge) { 1818 return; 1819 } 1820 var n = Number(count || 0); 1821 if (!Number.isFinite(n) || n < 0) { 1822 n = 0; 1823 } 1824 badge.textContent = String(n); 1825 } 1826 1827 function setCommentStatus(message, kind) { 1828 var status = document.getElementById('post-comments-status'); 1829 if (!status) { 1830 return; 1831 } 1832 status.className = 'post-comments-status'; 1833 if (kind) { 1834 status.classList.add('is-' + kind); 1835 } 1836 status.textContent = message || ''; 1837 } 1838 1839 function setRefreshBusy(isBusy) { 1840 refreshInFlight = !!isBusy; 1841 var button = document.getElementById('post-comments-refresh'); 1842 if (!button) { 1843 return; 1844 } 1845 button.disabled = refreshInFlight; 1846 button.textContent = refreshInFlight ? 'Refreshing...' : 'Refresh comments'; 1847 } 1848 1849 function setSubmitBusy(isBusy) { 1850 submitInFlight = !!isBusy; 1851 var button = document.getElementById('post-comment-submit'); 1852 if (!button) { 1853 return; 1854 } 1855 button.disabled = submitInFlight; 1856 button.textContent = submitInFlight ? 'Posting...' : 'Post comment'; 1857 } 1858 1859 function loadComments() { 1860 if (!currentRelPath) { 1861 return; 1862 } 1863 fetch('/cgi/blog-comments?path=' + encodeURIComponent(currentRelPath), { credentials: 'same-origin' }) 1864 .then(function (res) { return res.json(); }) 1865 .then(function (data) { 1866 if (!data || !data.success) { 1867 return; 1868 } 1869 var list = data.comments || []; 1870 renderComments(list); 1871 setCommentCount(list.length || 0); 1872 setCommentStatus('', ''); 1873 }) 1874 .catch(function () { 1875 setCommentStatus('Failed to load mirrored comments.', 'warn'); 1876 }); 1877 } 1878 1879 function refreshComments() { 1880 if (refreshInFlight) { 1881 return; 1882 } 1883 if (!currentRelPath) { 1884 return; 1885 } 1886 setRefreshBusy(true); 1887 setCommentStatus('Refreshing comments from relays...', 'info'); 1888 fetch('/cgi/blog-refresh-comments', { 1889 method: 'POST', 1890 headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, 1891 body: 'path=' + encodeURIComponent(currentRelPath), 1892 credentials: 'same-origin' 1893 }) 1894 .then(function (res) { return res.json(); }) 1895 .then(function (data) { 1896 if (!data || !data.success) { 1897 var msg = (data && data.error) ? data.error : 'Comment refresh failed.'; 1898 setCommentStatus(msg, 'warn'); 1899 return; 1900 } 1901 loadComments(); 1902 setCommentStatus('Comments refreshed.', 'ok'); 1903 }) 1904 .catch(function () { 1905 setCommentStatus('Comment refresh failed.', 'warn'); 1906 }) 1907 .finally(function () { 1908 setRefreshBusy(false); 1909 }); 1910 } 1911 1912 function parseEventJson(raw) { 1913 try { 1914 return JSON.parse(String(raw || '')); 1915 } catch (_) { 1916 return null; 1917 } 1918 } 1919 1920 function signCommentEvent(payload) { 1921 if (!window.nostr) { 1922 return Promise.reject(new Error('No browser Nostr signer detected. Install a NIP-07 extension.')); 1923 } 1924 if (typeof window.nostr.signEvent === 'function') { 1925 return Promise.resolve(window.nostr.signEvent(payload)); 1926 } 1927 return Promise.reject(new Error('Browser signer does not expose signEvent.')); 1928 } 1929 1930 function submitComment() { 1931 if (submitInFlight) { 1932 return; 1933 } 1934 var textarea = document.getElementById('post-comment-input'); 1935 if (!textarea) { 1936 return; 1937 } 1938 var content = String(textarea.value || '').trim(); 1939 if (!content) { 1940 setCommentStatus('Comment text is required.', 'warn'); 1941 return; 1942 } 1943 if (!currentNostrAddress || !currentNostrEventId) { 1944 setCommentStatus('Post Nostr metadata is missing for comment submit.', 'w
1944arn'); 1945 return; 1946 } 1947 var sessionToken = localStorage.getItem('session_token') || ''; 1948 var csrfToken = localStorage.getItem('csrf_token') || ''; 1949 if (!sessionToken || !csrfToken) { 1950 setCommentStatus('Sign in first to post comments.', 'warn'); 1951 return; 1952 } 1953 1954 var createdAt = Math.floor(Date.now() / 1000); 1955 var relayHint = currentNostrRelays.length ? String(currentNostrRelays[0] || '') : ''; 1956 function referencedTag(name, value) { 1957 return relayHint ? [name, value, relayHint] : [name, value]; 1958 } 1959 var kindText = String(currentNostrKind || '30023'); 1960 var draftEvent = { 1961 kind: 1111, 1962 created_at: createdAt, 1963 tags: [ 1964 referencedTag('A', currentNostrAddress), 1965 ['K', kindText], 1966 referencedTag('P', currentNostrPubkey), 1967 referencedTag('a', currentNostrAddress), 1968 referencedTag('e', currentNostrEventId), 1969 ['k', kindText], 1970 referencedTag('p', currentNostrPubkey) 1971 ], 1972 content: content 1973 }; 1974 1975 setSubmitBusy(true); 1976 setCommentStatus('Signing comment event...', 'info'); 1977 signCommentEvent(draftEvent) 1978 .then(function (signed) { 1979 var signedEvent = signed; 1980 if (typeof signedEvent === 'string') { 1981 signedEvent = parseEventJson(signedEvent); 1982 } 1983 if (!signedEvent || typeof signedEvent !== 'object') { 1984 throw new Error('Signer returned an invalid event payload.'); 1985 } 1986 setCommentStatus('Submitting signed comment...', 'info'); 1987 return fetch('/cgi/blog-submit-comment', { 1988 method: 'POST', 1989 headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, 1990 body: 'session_token=' + encodeURIComponent(sessionToken) + 1991 '&csrf_token=' + encodeURIComponent(csrfToken) + 1992 '&path=' + encodeURIComponent(currentRelPath) + 1993 '&event_json=' + encodeURIComponent(JSON.stringify(signedEvent)), 1994 credentials: 'same-origin' 1995 }); 1996 }) 1997 .then(function (res) { return res.json(); }) 1998 .then(function (data) { 1999 if (!data || !data.success) { 2000 var msg = (data && data.error) ? data.error : 'Comment submit failed.'; 2001 throw new Error(msg); 2002 } 2003 textarea.value = ''; 2004 setCommentStatus('Comment stored locally. Refreshing comments...', 'ok'); 2005 loadComments(); 2006 }) 2007 .catch(function (err) { 2008 setCommentStatus(err.message || 'Comment submit failed.', 'warn'); 2009 }) 2010 .finally(function () { 2011 setSubmitBusy(false); 2012 }); 2013 } 2014 2015 function ensureCommentShell(layout) { 2016 if (document.querySelector('.post-comments-shell')) { 2017 return; 2018 } 2019 var anchor = layout && layout.card ? layout.card : document.body; 2020 anchor.insertAdjacentHTML(anchor === document.body ? 'beforeend' : 'afterend', 2021 '<section class="post-comments-shell">' + 2022 '<div class="post-comments-head post-comment-island">' + 2023 '<h3>Comments (<span id="post-comments-count">0</span>)</h3>' + 2024 '<button type="button" id="post-comments-refresh">Refresh comments</button>' + 2025 '</div>' + 2026 '<div class="post-comments-compose post-comment-island">' + 2027 '<textarea id="post-comment-input" rows="3" placeholder="Write a Nostr-signed reply..."></textarea>' + 2028 '<button type="button" id="post-comment-submit">Post comment</button>' + 2029 '<p class="post-comments-shortcut">Press Ctrl/Cmd + Enter to post quickly.</p>' + 2030 '<p id="post-comments-status" class="post-comments-status"></p>' + 2031 '</div>' + 2032 '<div id="post-comments-list" class="post-comments-list"><p class="placeholder post-comments-empty post-comment-island">No comments mirrored yet.</p></div>' + 2033 '</section>' 2034 ); 2035 var refreshButton = document.getElementById('post-comments-refresh'); 2036 if (refreshButton) { 2037 refreshButton.addEventListener('click', refreshComments); 2038 } 2039 var submitButton = document.getElementById('post-comment-submit'); 2040 if (submitButton) { 2041 submitButton.addEventListener('click', submitComment); 2042 } 2043 var input = document.getElementById('post-comment-input'); 2044 if (input) { 2045 input.addEventListener('keydown', function (event) { 2046 if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') { 2047 event.preventDefault(); 2048 submitComment(); 2049 } 2050 }); 2051 } 2052 } 2053 2054 function applyEnhancements(payload) { 2055 if (!payload || !payload.current) { 2056 return; 2057 } 2058 var canonicalPath = normalizePostMdPath(payload.current.source_path || payload.current.post_path || payload.current.path || payload.current.url || ''); 2059 if (canonicalPath) { 2060 currentRelPath = canonicalPath; 2061 } 2062 2063 var layout = existingSinglePostLayout(); 2064 if (!layout || !layout.body) { 2065 return; 2066 } 2067 2068 if (!layout.card.querySelector('.post-end-tags')) { 2069 var tagsHtml = renderPostEndTags(payload.current); 2070 if (tagsHtml) { 2071 layout.body.insertAdjacentHTML('beforeend', tagsHtml); 2072 } 2073 } 2074 2075 if (!layout.card.querySelector('.post-nav-enhanced')) { 2076 layout.body.insertAdjacentHTML('beforeend', renderPostNav(payload)); 2077 } 2078 2079 syncSinglePostLayoutForContent(layout); 2080 normalizeAllMarkdownSeparators(); 2081 2082 ensurePostPageMenu(layout); 2083 refreshPostPageMenuVisibility(); 2084 renderPostSyncStatusPill(payload, layout); 2085 renderPostZapUi(payload, layout); 2086 2087 if (payload.current.nostr) { 2088 currentNostrAddress = payload.current.nostr.address || ''; 2089 currentNostrEventId = payload.current.nostr.id || ''; 2090 currentNostrPubkey = payload.current.nostr.pubkey || ''; 2091 currentNostrKind = payload.current.nostr.kind || ''; 2092 currentNostrD = payload.current.nostr.d || ''; 2093 currentNostrRelays = Array.isArray(payload.current.nostr.relays) ? payload.current.nostr.relays.slice() : []; 2094 refreshPostPageMenuVisibility(); 2095 ensureCommentShell(layout); 2096 loadComments(); 2097 } else { 2098 currentNostrAddress = ''; 2099 currentNostrEventId = ''; 2100 currentNostrPubkey = ''; 2101 currentNostrKind = ''; 2102 currentNostrD = ''; 2103 currentNostrRelays = []; 2104 refreshPostPageMenuVisibility(); 2105 } 2106 2107 ensureMeta('description', payload.current.summary || '', 'name'); 2108 ensureMeta('og:description', payload.current.summary || '', 'property'); 2109 ensureMeta('article:published_time', payload.current.published_at || '', 'property'); 2110 ensureMeta('twitter:description', payload.current.summary || '', 'name'); 2111 } 2112
2113 function loadPostContext() { 2114 if (!isPostPage(window.location.pathname)) { 2115 return; 2116 } 2117 if (window.location.pathname === '/cgi/blog-open-post' || window.location.pathname.indexOf('/cgi/blog-open-post/') === 0) { 2118 var query = new URLSearchParams(window.location.search || ''); 2119 var fromPathInfo = String(window.location.pathname || '').replace(/^\/cgi\/blog-open-post\/?/, ''); 2120 currentRelPath = normalizePostMdPath(query.get('path') || fromPathInfo || ''); 2121 } else { 2122 currentRelPath = normalizePostMdPath(window.location.pathname || ''); 2123 } 2124 if (!currentRelPath) { 2125 return; 2126 } 2127 if (maybeRepairWrongPostShell(currentRelPath)) { 2128 return; 2129 } 2130 fetch('/cgi/blog-post-context?path=' + encodeURIComponent(currentRelPath), { credentials: 'same-origin' }) 2131 .then(function (res) { return res.json(); }) 2132 .then(function (data) { 2133 if (!data || !data.success) { 2134 return; 2135 } 2136 try { 2137 applyEnhancements(data); 2138 } catch (err) { 2139 window.__postContextError = err && err.message ? err.message : String(err || 'post context error'); 2140 if (window.console && typeof window.console.warn === 'function') { 2141 window.console.warn('Post context enhancement failed:', err); 2142 } 2143 } 2144 }) 2145 .catch(function () { 2146 // Post page should remain readable even if enhancement fetch fails. 2147 }); 2148 } 2149 2150 function resolveCurrentPostPathFromLocation() { 2151 if (!isPostPage(window.location.pathname)) { 2152 return ''; 2153 } 2154 if (window.location.pathname === '/cgi/blog-open-post' || window.location.pathname.indexOf('/cgi/blog-open-post/') === 0) { 2155 var query = new URLSearchParams(window.location.search || ''); 2156 var fromPathInfo = String(window.location.pathname || '').replace(/^\/cgi\/blog-open-post\/?/, ''); 2157 return normalizePostMdPath(query.get('path') || fromPathInfo || ''); 2158 } 2159 return normalizePostMdPath(window.location.pathname || ''); 2160 } 2161 2162 function triggerEarlyPostRouteRepair() { 2163 if (!isPostPage(window.location.pathname)) { 2164 return false; 2165 } 2166 var resolvedPath = resolveCurrentPostPathFromLocation(); 2167 if (!resolvedPath) { 2168 return false; 2169 } 2170 if (document.querySelector('.post-single-item')) { 2171 return false; 2172 } 2173 currentRelPath = resolvedPath; 2174 // Keep current shell visible while we fetch and swap in the canonical 2175 // post document so navigation never flashes to a blank frame. 2176 maybeRepairWrongPostShell(currentRelPath); 2177 return true; 2178 } 2179 2180 document.addEventListener('click', function (event) { 2181 if (!isPostPage(window.location.pathname)) { 2182 return; 2183 } 2184 var target = event.target; 2185 if (!(target instanceof Element)) { 2186 return; 2187 } 2188 var trigger = target.closest('.post-page-menu-trigger'); 2189 if (trigger) { 2190 activatePostPageMenuFromEvent(event); 2191 return; 2192 } 2193 var actionNode = target.closest('[data-post-page-action]'); 2194 if (actionNode instanceof HTMLElement) { 2195 event.preventDefault(); 2196 if (actionNode.hidden) { 2197 closePostPageMenu(); 2198 return; 2199 } 2200 var action = actionNode.getAttribute('data-post-page-action'); 2201 runPostPageAction(action); 2202 return; 2203 } 2204 var addListAction = target.closest('[data-post-add-list-action]'); 2205 if (addListAction instanceof HTMLElement) { 2206 event.preventDefault(); 2207 if (String(addListAction.getAttribute('data-post-add-list-action') || '')
2207=== 'cancel') { 2208 closeAddToListDialog(); 2209 } 2210 return; 2211 } 2212 var menu = document.querySelector('.post-page-menu'); 2213 if (menu && !menu.hidden && !menu.contains(target)) { 2214 closePostPageMenu(); 2215 } 2216 2217 var inlineAction = target.closest('[data-post-inline-action]'); 2218 if (inlineAction instanceof HTMLElement) { 2219 event.preventDefault(); 2220 var actionName = String(inlineAction.getAttribute('data-post-inline-action') || ''); 2221 if (actionName === 'close') { 2222 closeInlineEditor(); 2223 } else if (actionName === 'save') { 2224 saveInlineDraft('save_draft'); 2225 } else if (actionName === 'publish') { 2226 saveInlineDraft('publish_now'); 2227 } else if (actionName === 'delete') { 2228 deleteInlineDraft(); 2229 } else if (actionName === 'edit_filename') { 2230 inlineEditState.postFilenameEditing = true; 2231 renderInlineEditor(); 2232 window.setTimeout(function () { 2233 var input = document.querySelector('[data-post-inline-field="post_filename"]'); 2234 if (input && typeof input.focus === 'function') { 2235 input.focus(); 2236 } 2237 if (input && typeof input.select === 'function') { 2238 input.select(); 2239 } 2240 }, 0); 2241 } 2242 return; 2243 } 2244 2245 var inlineTagRemove = target.closest('[data-post-inline-tag-remove]'); 2246 if (inlineTagRemove instanceof HTMLElement) { 2247 event.preventDefault(); 2248 var tagValue = String(inlineTagRemove.getAttribute('data-post-inline-tag-remove') || '').trim(); 2249 if (inlineRemoveTag(tagValue)) { 2250 var editorNode = inlineTagsEditorNode(); 2251 if (editorNode) { 2252 inlineTagsEditorRender(editorNode); 2253 inlineTagsEditorFocusDraft(editorNode); 2254 } 2255 queueInlineAutosave(); 2256 } 2257 return; 2258 } 2259 2260 var inlineTagEditor = target.closest('[data-post-inline-field="tags-editor"]'); 2261 if (inlineTagEditor instanceof HTMLElement) { 2262 inlineTagsEditorSyncDraft(inlineTagEditor); 2263 inlineTagsEditorFocusDraft(inlineTagEditor); 2264 return; 2265 } 2266 2267 var inlineToolbar = target.closest('[data-post-inline-toolbar]'); 2268 if (inlineToolbar instanceof HTMLElement) { 2269 event.preventDefault(); 2270 inlineInsertMarkdown(String(inlineToolbar.getAttribute('data-post-inline-toolbar') || '')); 2271 return; 2272 } 2273 }); 2274 2275 document.addEventListener('pointerup', function (event) { 2276 if (!isPostPage(window.location.pathname) || event.pointerType === 'mouse') { 2277 return; 2278 } 2279 var target = event.target; 2280 if (!(target instanceof Element)) { 2281 return; 2282 } 2283 if (target.closest('.post-page-menu-trigger')) { 2284 activatePostPageMenuFromEvent(event); 2285 } 2286 }, { passive: false }); 2287 2288 if (isPostPage(window.location.pathname)) { 2289 normalizePostPageMenu(document.querySelector('.post-page-menu')); 2290 } 2291 2292 document.addEventListener('input', function (event) { 2293 var target = event.target; 2294 if (!(target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLElement)) { 2295 return; 2296 } 2297 if (!inlineEditState.open) { 2298 return; 2299 } 2300 if (target instanceof HTMLInputElement && String(target.getAttribute('data-post-inline-field') || '')
2300=== 'tags-input') { 2301 var editorNode = target.closest('[data-post-inline-field="tags-editor"]'); 2302 inlineTagsEditorSyncDraft(editorNode); 2303 if (/[,\n]/.test(String(target.value || ''))) { 2304 if (inlineTagsEditorCommit(editorNode, false)) { 2305 queueInlineAutosave(); 2306 } 2307 } 2308 var hiddenTags = document.querySelector('input[data-post-inline-field="tags"]'); 2309 if (hiddenTags instanceof HTMLInputElement) { 2310 hiddenTags.value = inlineEditState.tags; 2311 } 2312 return; 2313 } 2314 if (!(target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement)) { 2315 return; 2316 } 2317 var field = String(target.getAttribute('data-post-inline-field') || ''); 2318 if (!field) { 2319 return; 2320 } 2321 if (field === 'title') { 2322 inlineEditState.title = String(target.value || ''); 2323 } else if (field === 'content') { 2324 inlineEditState.content = String(target.value || ''); 2325 } else if (field === 'post_filename') { 2326 inlineEditState.postFilename = normalizePostFilename(String(target.value || '')); 2327 } 2328 queueInlineAutosave(); 2329 }); 2330 2331 document.addEventListener('change', function (event) { 2332 var target = event.target; 2333 if (target instanceof Element && target.closest('[data-post-add-list-select]')) { 2334 syncAddToListDialogNewRow(addToListDialogNode()); 2335 return; 2336 } 2337 if (!(target instanceof HTMLInputElement)) { 2338 return; 2339 } 2340 if (!inlineEditState.open) { 2341 return; 2342 } 2343 if (!target.hasAttribute('data-post-inline-field')) { 2344 return; 2345 } 2346 var changedField = String(target.getAttribute('data-post-inline-field') || ''); 2347 if (changedField === 'post_filename') { 2348 var nextFilename = normalizePostFilename(target.value || filenameFromPostPath(inlineEditState.sourcePostPath)); 2349 if (!nextFilename) { 2350 nextFilename = filenameFromPostPath(inlineEditState.sourcePostPath) || 'post'; 2351 } 2352 inlineEditState.postFilename = nextFilename; 2353 inlineEditState.postFilenameEditing = false; 2354 target.value = nextFilename; 2355 renderInlineEditor(); 2356 queueInlineAutosave(); 2357 } 2358 }); 2359 2360 document.addEventListener('submit', function (event) { 2361 if (!isPostPage(window.location.pathname)) { 2362 return; 2363 } 2364 var form = event.target; 2365 if (!(form instanceof Element) || !form.closest('[data-post-add-list-form]')) { 2366 return; 2367 } 2368 event.preventDefault(); 2369 submitAddToListDialog(form); 2370 }); 2371 2372 document.addEventListener('keydown', function (event) { 2373 var target = event.target; 2374 if (!(target instanceof HTMLElement) || !inlineEditState.open) { 2375 return; 2376 } 2377 if (String(target.getAttribute('data-post-inline-field') || '') !== 'tags-input') { 2378 return; 2379 } 2380 var editorNode = target.closest('[data-post-inline-field="tags-editor"]'); 2381 if (event.key === 'Enter') { 2382 event.preventDefault(); 2383 if (inlineTagsEditorCommit(editorNode, true)) { 2384 queueInlineAutosave(); 2385 } 2386 return; 2387 } 2388 if (event.key === ',' || event.code === 'Comma' || event.key === 'Tab') { 2389 if (event.key === 'Tab' || inlineTagsEditorReadDraft(editorNode).indexOf(',') !== -1 || /\S/.test(inlineTagsEditorReadDraft(editorNode))) { 2390 event.preventDefault(); 2391 if (inlineTagsEditorCommit(editorNode, true)) { 2392 queueInlineAutosave(); 2393 } 2394 } 2395 return; 2396 } 2397 if (event.key === 'Backspace') { 2398 var draft = inlineTagsEditorReadDraft(editorNode); 2399 if (!String(draft || '').trim() && (inlineEditState.tagList || []).length) { 2400 event.preventDefault(); 2401 var last = inlineEditState.tagList[inlineEditState.tagList.length - 1]; 2402 if (inlineRemoveTag(last)) { 2403 inlineTagsEditorRender(editorNode); 2404 inlineTagsEditorFocusDraft(editorNode); 2405 queueInlineAutosave(); 2406 } 2407 } 2408 } 2409 }); 2410 2411 document.addEventListener('focusout', function (event) { 2412 var target = event.target; 2413 if (!(target instanceof HTMLElement) || !inlineEditState.open) { 2414 return; 2415 } 2416 if (String(target.getAttribute('data-post-inline-field') || '') !== 'tags-input') { 2417 return; 2418 } 2419 var editorNode = target.closest('[data-post-inline-field="tags-editor"]'); 2420 window.setTimeout(function () { 2421 var active = document.activeElement; 2422 if (editorNode instanceof HTMLElement && active instanceof HTMLElement && editorNode.contains(active)) { 2423 return; 2424 } 2425 if (inlineTagsEditorCommit(editorNode, true)) { 2426 queueInlineAutosave(); 2427 } 2428 }, 0); 2429 }); 2430 2431 if (!triggerEarlyPostRouteRepair()) { 2432 if (document.readyState === 'loading') { 2433 document.addEventListener('DOMContentLoaded', function () { 2434 normalizeAllMarkdownSeparators();
2435 loadPostContext(); 2436 }); 2437 } else { 2438 normalizeAllMarkdownSeparators(); 2439 loadPostContext(); 2440 } 2441 window.__wizardryBindSessionAuthRefresh({ 2442 onChange: refreshPostPageMenuVisibility, 2443 storageKeys: ['session_token', 'csrf_token', 'last_auth_is_admin'] 2444 }); 2445 } 2446})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.