1(function () { 2 'use strict'; 3 4 function isPostRoute(pathname) { 5 var path = String(pathname || ''); 6 if (/^\/posts\/[^/?#]+\/?$/.test(path)) { 7 return true; 8 } 9 if (/^\/pages\/posts\/[^/?#]+(?:\.html?)?\/?$/.test(path)) { 10 return true; 11 } 12 if (path === '/cgi/blog-open-post' || path.indexOf('/cgi/blog-open-post/') === 0) { 13 return true; 14 } 15 return false; 16 } 17 18 if (isPostRoute(window.location.pathname || '')) { 19 return; 20 } 21 22 var CACHE_KEY = 'wizardry_blog_posts_v2'; 23 var POSTS_CACHE_MAX_AGE_MS = 15000; 24 var BOOTSTRAP_CACHE_MAX_AGE_MS = 15000; 25 var DRAFT_NOTICE_CACHE_KEY = 'wizardry_blog_draft_notice_v1'; 26 var DRAFT_NOTICE_CACHE_MAX_AGE_MS = 10 * 60 * 1000; 27 var root = document.getElementById('blog-page-root'); 28 if (!root) { 29 return; 30 } 31 32 var slug = String(root.getAttribute('data-blog-slug') || 'blog').trim() || 'blog'; 33 var pageType = String(root.getAttribute('data-page-type') || 'blog').trim().toLowerCase() || 'blog'; 34 var els = { 35 title: document.getElementById('blog-page-title'), 36 description: document.getElementById('blog-page-description'), 37 admin: document.getElementById('blog-page-admin'), 38 validation: document.getElementById('blog-page-validation'), 39 content: document.getElementById('blog-page-content'), 40 toggle: document.getElementById('blog-filter-toggle'), 41 panel: document.getElementById('blog-filter-panel'), 42 tags: document.getElementById('blog-filter-tags'), 43 years: document.getElementById('blog-filter-years'), 44 types: document.getElementById('blog-filter-types'), 45 clear: document.getElementById('blog-clear-filters'), 46 list: document.getElementById('blog-post-list'), 47 empty: document.getElementById('blog-empty'), 48 draftNotice: document.getElementById('blog-draft-notice'), 49 composeFab: document.getElementById('blog-compose-fab'), 50 composeSlot: document.getElementById('blog-compose-slot') 51 }; 52 53 var state = { 54 payload: null, 55 authSignature: '', 56 posts: [], 57 postsLoading: true, 58 staticPrerenderListAdopted: false, 59 adoptedPostListSignature: '', 60 renderSignature: '', 61 defaultFiltersApplied: false, 62 pageSettingsOpen: false, 63 filters: { 64 tags: new Set(), 65 years: new Set(), 66 types: new Set() 67 }, 68 compose: { 69 open: false, 70 preview: false, 71 draftId: '', 72 sourcePostPath: '', 73 postTypeLocked: false, 74 postTypeChosen: false, 75 tags: [], 76 tagsOpen: false, 77 tagsDraftText: '', 78 postType: 'longform', 79 postTypeToolbarCollapsed: false, 80 postTypeToolbarCollapseTimer: null, 81 publishDestination: 'local_only', 82 shortformLimit: 280, 83 shortformLimitEditing: false, 84 linkUrl: '', 85 cameraStream: null, 86 cameraStarting: false, 87 cameraError: '', 88 cameraFullscreen: false, 89 cameraReturnToChooser: false, 90 audioStream: null, 91 audioRecorder: null, 92 audioChunks: [], 93 audioStarting: false, 94 audioRecording: false, 95 audioError: '', 96 uploading: 0, 97 uploadItems: [], 98 uploadSeq: 0, 99 uploadCleanupTimer: null, 100 pendingContentAdditions: [], 101 autosaveTimer: null, 102 busy: false, 103 output: '', 104 outputTone: '', 105 saveStatus: '' 106 }, 107 draftNotice: { 108 loading: false, 109 loaded: false, 110 requestSeq: 0, 111 drafts: [], 112 dismissedSignature: '' 113 } 114 }; 115 var panelHideTimer = null; 116 var pageSettingsHideTimer = null; 117 var composeToggleGuardUntil = 0; 118 var composeDropHoverTimer = null; 119 var authRefreshPollState = { timer: 0, startedAt: 0 }; 120 var markedUpgradeState = { timer: 0, attempts: 0 }; 121 var postsCatalogReady = false; 122 var COMPOSE_POST_TYPES = ['shortform', 'longform', 'capture-media', 'upload-media', 'attachment', 'audio-note', 'link-share', 'go-live']; 123 var routeSelfHealTriggered = false; 124 var postCardMenuBusy = false; 125 var managedSlugFromPath = window.__wizardryManagedSlugFromPath; 126 var ROUTE_REPAIR_PARAM = '__route_repair'; 127 128 function normalizeManagedSlug(value) { 129 return String(value || '').trim().toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-+|-+$/g, ''); 130 } 131 132 function managedSlugsEquivalent(expected, rootSlug) { 133 var a = normalizeManagedSlug(expected); 134 var b = normalizeManagedSlug(rootSlug); 135 return !!(a && b && (a === b || (a === 'index' && b === 'blog') || (a === 'blog' && b === 'index'))); 136 } 137 138 function overflowMenuIconSvg() { 139 return '<svg class="overflow-menu-icon-svg" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewB
139ox="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>'; 140 } 141 142 function closeIconSvg() { 143 return '<svg class="blog-draft-notice-close-svg" xmlns="http://www.w3.org/2000/svg" width="26" height="26" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M5.5 5.5 18.5 18.5M18.5 5.5 5.5 18.5" fill="none" stroke="currentColor" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round"/></svg>'; 144 } 145 146 function maybeReloadForAuthChange(event) { 147 var nextSig = window.__wizardryViewerAuthSignature(); 148 var lastSig = String(state.authSignature || ''); 149 if (nextSig === lastSig) { 150 return; 151 } 152 state.authSignature = nextSig; 153 applyOptimisticAdminFromAuthEvent(event); 154 loadPageState({ deferRender: false }); 155 } 156 157 function ensureComposeStateShape() { 158 if (!state.compose || typeof state.compose !== 'object') { 159 state.compose = {}; 160 } 161 if (typeof state.compose.open !== 'boolean') state.compose.open = false; 162 if (typeof state.compose.preview !== 'boolean') state.compose.preview = false; 163 if (typeof state.compose.draftId !== 'string') state.compose.draftId = ''; 164 if (typeof state.compose.sourcePostPath !== 'string') state.compose.sourcePostPath = ''; 165 if (typeof state.compose.postTypeLocked !== 'boolean') state.compose.postTypeLocked = false; 166 if (typeof state.compose.postTypeChosen !== 'boolean') state.compose.postTypeChosen = false; 167 if (!Array.isArray(state.compose.tags)) state.compose.tags = []; 168 if (typeof state.compose.tagsOpen !== 'boolean') state.compose.tagsOpen = false; 169 if (typeof state.compose.tagsDraftText !== 'string') state.compose.tagsDraftText = ''; 170 if (typeof state.compose.postType !== 'string') state.compose.postType = 'longform'; 171 if (typeof state.compose.postTypeToolbarCollapsed !== 'boolean') state.compose.postTypeToolbarCollapsed = false; 172 if (typeof state.compose.postTypeToolbarCollapseTimer === 'undefined') state.compose.postTypeToolbarCollapseTimer = null; 173 if (typeof state.compose.publishDestination !== 'string') state.compose.publishDestination = 'local_only'; 174 if (typeof state.compose.shortformLimit !== 'number' || !isFinite(state.compose.shortformLimit)) state.compose.shortformLimit = 280; 175 if (typeof state.compose.shortformLimitEditing !== 'boolean') state.compose.shortformLimitEditing = false; 176 if (typeof state.compose.linkUrl !== 'string') state.compose.linkUrl = ''; 177 if (typeof state.compose.cameraStarting !== 'boolean') state.compose.cameraStarting = false; 178 if (typeof state.compose.cameraError !== 'string') state.compose.cameraError = ''; 179 if (typeof state.compose.cameraStream === 'undefined') state.compose.cameraStream = null; 180 if (typeof state.compose.cameraFullscreen !== 'boolean') state.compose.cameraFullscreen = false; 181 if (typeof state.compose.cameraReturnToChooser !== 'boolean') state.compose.cameraReturnToChooser = false; 182 if (typeof state.compose.audioStream === 'undefined') state.compose.audioStream = null; 183 if (typeof state.compose.audioRecorder === 'undefined') state.compose.audioRecorder = null; 184 if (!Array.isArray(state.compose.audioChunks)) state.compose.audioChunks = []; 185 if (typeof state.compose.audioStarting !== 'boolean') state.compose.audioStarting = false; 186 if (typeof state.compose.audioRecording !== 'boolean') state.compose.audioRecording = false; 187 if (typeof state.compose.audioError !== 'string') state.compose.audioError = ''; 188 if (typeof state.compose.uploading !== 'number' || !isFinite(state.compose.uploading)) state.compose.uploading = 0; 189 if (!Array.isArray(state.compose.uploadItems)) state.compose.uploadItems = []; 190 if (typeof state.compose.uploadSeq !== 'number' || !isFinite(state.compose.uploadSeq)) state.compose.uploadSeq = 0; 191 if (typeof state.compose.uploadCleanupTimer === 'undefined') state.compose.uploadCleanupTimer = null; 192 if (!Array.isArray(state.compose.pendingContentAdditions)) state.compose.pendingContentAdditions = []; 193 if (typeof state.compose.autosaveTimer === 'undefined') state.compose.autosaveTimer = null; 194 if (typeof state.compose.busy !== 'boolean') state.compose.busy = false;
195 if (typeof state.compose.output !== 'string') state.compose.output = ''; 196 if (typeof state.compose.outputTone !== 'string') state.compose.outputTone = ''; 197 if (typeof state.compose.saveStatus !== 'string') state.compose.saveStatus = ''; 198 } 199 200 function maybeRepairRoute(reason) { 201 if (routeSelfHealTriggered) { 202 return; 203 } 204 routeSelfHealTriggered = true; 205 requestManagedRouteRepair({ 206 reason: reason, 207 preflight: function () { 208 return fetch('/cgi/blog-list-navbar-pages', { cache: 'no-store' }); 209 } 210 }); 211 } 212 213 function requestManagedRouteRepair(options) { 214 var opts = options && typeof options === 'object' ? options : {}; 215 var url; 216 try { 217 url = Object.prototype.hasOwnProperty.call(opts, 'url') 218 ? new URL(String(opts.url || ''), window.location.href) 219 : new URL(window.location.href); 220 } catch (_err) { 221 if (typeof opts.onUrlError === 'function') { 222 opts.onUrlError(); 223 return false; 224 } 225 window.location.reload(); 226 return false; 227 } 228 if (url.searchParams.get(ROUTE_REPAIR_PARAM) === '1') { 229 url.searchParams.delete(ROUTE_REPAIR_PARAM); 230 return false; 231 } 232 url.searchParams.set(ROUTE_REPAIR_PARAM, '1'); 233 var preflight = typeof opts.preflight === 'function' ? opts.preflight() : null; 234 Promise.resolve(preflight) 235 .catch(function () { 236 // Ignore preflight failures; still attempt one repair reload. 237 }) 238 .finally(function () { 239 window.setTimeout(function () { 240 if (opts.reason) { 241 try { 242 sessionStorage.setItem('wizardry_last_route_repair_reason', String(opts.reason)); 243 } catch (_storageErr) { 244 // Ignore storage errors. 245 } 246 } 247 window.location.replace(url.toString()); 248 }, Math.max(0, Number(opts.delayMs || 220) || 0)); 249 }); 250 return true; 251 } 252 253 function escapeHtml(value) { 254 return String(value || '') 255 .replace(/&/g, '&') 256 .replace(/</g, '<') 257 .replace(/>/g, '>') 258 .replace(/"/g, '"') 259 .replace(/'/g, '''); 260 } 261 262 function markdownInline(md) { 263 return window.__wizardryRenderManagedMarkdownInline(md, { 264 upgradeState: markedUpgradeState, 265 onUpgrade: renderAll, 266 renderFallback: window.__wizardryRenderManagedMarkdownInlineLinkFallback 267 }); 268 } 269 270 function markdownBlock(md) { 271 return window.__wizardryRenderManagedMarkdownBlock(md, { 272 upgradeState: markedUpgradeState, 273 onUpgrade: renderAll, 274 renderInlineFallback: window.__wizardryRenderManagedMarkdownInlineLinkFallback 275 }); 276 } 277 278 function prerenderFragment(payload, key) { 279 return String(payload && payload.prerender && payload.prerender[key] || ''); 280 } 281 282 function readPrerenderHtmlFragment(payload, name, options) { 283 var opts = options && typeof options === 'object' ? options : {}; 284 var fragmentName = String(name || '').trim(); 285 if (!fragmentName) { 286 return null; 287 } 288 var html = prerenderFragment(payload, fragmentName + '_html'); 289 if (!html && opts.allowEmpty !== true) { 290 return null; 291 } 292 return { 293 html: html, 294 hidden: /\bhidden\b/i.test(prerenderFragment(payload, fragmentName + '_attrs')) 295 }; 296 } 297 298 function mountPrerenderHtmlFragment(targetNode, payload, name, options) { 299 var fragment = readPrerenderHtmlFragment(payload, name, options); 300 if (!targetNode || !fragment) { 301 return false; 302 } 303 targetNode.innerHTML = fragment.html; 304 targetNode.hidden = fragment.hidden; 305 return true; 306 } 307 308 function titleizeSlug(value) { 309 var text = String(value || '').trim().replace(/-/g, ' '); 310 if (!text || text === 'index') { 311 return 'Blog'; 312 } 313 return text.split(/\s+/).map(function (word) { 314 return word ? (word.charAt(0).toUpperCase() + word.slice(1)) : ''; 315 }).join(' '); 316 } 317 318 function fallbackPageTitle() { 319 return window.__wizardryResolveRootAttributeFallbackText( 320 root, 321 ['data-page-title'], 322 titleizeSlug(slug) 323 ) || titleizeSlug(slug); 324 } 325 326 function normalizePageState(raw) { 327 var src = raw || {}; 328 return { 329 slug: String(src.slug || slug), 330 title: String(src.title || fallbackPageTitle()), 331 description: String(src.description || ''), 332 image: String(src.image || ''), 333 content: String(src.content || ''), 334 default_tag: String(src.default_tag || '').trim(), 335 seed_tag: String(src.seed_tag || '').trim(), 336 theme: String(src.theme || '').trim(), 337 member_addresses: Array.isArray(src.member_addresses) ? src.member_addresses.slice() : [], 338 extras_after: String(src.extras_after || '') 339 }; 340 } 341 342 function filteredPostsForPageDefaults(posts, payload) { 343 var list = Array.isArray(posts) ? posts.slice() : []; 344 var page = payload && payload.state ? normalizePageState(payload.state) : null; 345 if (pageType === 'publication') { 346 var members = new Set(page && page.member_addresses || []); 347 if (members.size) { 348 var postsByAddress = {}; 349 list.forEach(function (post) { 350 postsByAddress[String(post && post.nostr_address || '').trim().toLowerCase()] = post; 351 }); 352 return Array.from(members).map(function (address) { 353 return postsByAddress[String(address || '').trim().toLowerCase()]; 354 }
354).filter(Boolean); 355 } 356 return []; 357 } 358 var defaultTag = String(page && page.default_tag || '').trim().toLowerCase(); 359 if (!defaultTag) { 360 return list; 361 } 362 return list.filter(function (post) { 363 return normalizePostTags(post && post.tags).some(function (tag) { 364 return String(tag || '').trim().toLowerCase() === defaultTag; 365 }); 366 }); 367 } 368 369 function payloadHasDefaultPostTag(payload) { 370 var page = payload && payload.state ? normalizePageState(payload.state) : null; 371 return !!String(page && (pageType === 'publication' ? page.seed_tag : page.default_tag) || '').trim(); 372 } 373 374 function postsHaveRenderedHtml(posts) { 375 return Array.isArray(posts) && posts.every(function (post) { 376 return !!renderedPostCardHtml(post); 377 }); 378 } 379 380 function getRenderState() { 381 if (state.payload && state.payload.state) { 382 return normalizePageState(state.payload.state); 383 } 384 return normalizePageState({ title: fallbackPageTitle() }); 385 } 386 387 function hasLikelyAdminSession() { 388 var auth = window.__wizardryAuthPayload(); 389 if (!auth || !auth.session_token || !auth.csrf_token) { 390 return false; 391 } 392 try { 393 return String(localStorage.getItem('last_auth_is_admin') || '')
393=== '1'; 394 } catch (_err) { 395 return false; 396 } 397 } 398 399 function isAdmin() { 400 if (state.payload && state.payload.is_admin) { 401 return true; 402 } 403 return hasLikelyAdminSession(); 404 } 405 406 var apiPost = window.__wizardryPostFormJson; 407 408 function renderHead() { 409 var page = getRenderState(); 410 var publicationHead = root.querySelector('.list-page-head'); 411 if (pageType === 'publication' && publicationHead instanceof HTMLElement) { 412 var imageUrl = String(page.image || '').trim(); 413 var validImage = /^https?:\/\//i.test(imageUrl) || imageUrl.charAt(0) === '/'; 414 publicationHead.classList.toggle('publication-head-with-image', validImage); 415 publicationHead.style.backgroundImage = validImage 416 ? 'linear-gradient(rgba(0,0,0,.5), rgba(0,0,0,.5)), url("' + imageUrl.replace(/["\\\n\r]/g, '') + '")' 417 : ''; 418 } 419 window.__wizardryRenderManagedPageHeadForPage({ 420 isAdmin: false, 421 payload: state.payload, 422 titleNode: els.title, 423 descriptionNode: els.description, 424 titleStateOptions: { 425 renderState: page, 426 fallbackTitleText: fallbackPageTitle(), 427 useFallbackForPageTitle: true 428 }, 429 descriptionStateOptions: { 430 format: 'html', 431 text: pageType === 'publication' ? page.description : '', 432 html: pageType === 'publication' ? markdownBlock(page.description) : '', 433 hidden: pageType !== 'publication' || !page.description 434 }, 435 navbarTitleOptions: { 436 renderState: page, 437 rootNode: root, 438 fallbackAttributeNames: ['data-page-title'], 439 fallbackText: fallbackPageTitle(), 440 isAdmin: false, 441 editMode: false 442 } 443 }); 444 } 445 446 function clearDraftNoticeState() { 447 state.draftNotice.loading = false; 448 state.draftNotice.loaded = false; 449 state.draftNotice.drafts = []; 450 clearDraftNoticeCache(); 451 renderDraftNotice(); 452 } 453 454 function draftNoticeIdentity() { 455 var username = ''; 456 try { 457 username = String(localStorage.getItem('last_auth_username') || '').trim().toLowerCase(); 458 } catch (_err) { 459 username = ''; 460 } 461 if (username) { 462 return 'u:' + username; 463 } 464 var auth = window.__wizardryAuthPayload(); 465 if (auth.session_token) { 466 return 's:' + auth.session_token.slice(0, 24); 467 } 468 return ''; 469 } 470 471 function removeTimedCacheRecord(storageKey) { 472 try { 473 localStorage.removeItem(String(storageKey || '')); 474 } catch (_err) { 475 // Ignore storage failures. 476 } 477 } 478 479 function readTimedCacheRecord(storageKey, maxAgeMs) { 480 try { 481 var raw = localStorage.getItem(String(storageKey || '')); 482 if (!raw) { 483 return null; 484 } 485 var parsed = JSON.parse(raw); 486 if (!parsed || typeof parsed !== 'object') { 487 return null; 488 } 489 var savedAt = Number(parsed.saved_at || 0); 490 if (!isFinite(savedAt) || savedAt <= 0 || (Number(maxAgeMs || 0) > 0 && (Date.now() - savedAt) > Number(maxAgeMs || 0))) { 491 removeTimedCacheRecord(storageKey); 492 return null; 493 } 494 return parsed; 495 } catch (_err) { 496 return null; 497 } 498 } 499 500 function writeTimedCacheRecord(storageKey, payload) { 501 try { 502 localStorage.setItem(String(storageKey || ''), JSON.stringify(Object.assign({}, payload || {}, { 503 saved_at: Date.now() 504 }))); 505 } catch (_err) { 506 // Ignore storage failures. 507 } 508 } 509 510 function clearDraftNoticeCache() { 511 removeTimedCacheRecord(DRAFT_NOTICE_CACHE_KEY); 512 } 513 514 function readDraftNoticeCache() { 515 var parsed = readTimedCacheRecord(DRAFT_NOTICE_CACHE_KEY, DRAFT_NOTICE_CACHE_MAX_AGE_MS); 516 if (!parsed) { 517 return null; 518 } 519 try { 520 var identity = String(parsed.identity || '').trim(); 521 if (!identity || identity !== draftNoticeIdentity()) { 522 return null; 523 } 524 var drafts = Array.isArray(parsed.drafts) ? parsed.drafts : []; 525 return drafts.slice(); 526 } catch (_err) { 527 return null; 528 } 529 } 530 531 function writeDraftNoticeCache(drafts) { 532 var identity = draftNoticeIdentity(); 533 if (!identity) { 534 return; 535 } 536 writeTimedCacheRecord(DRAFT_NOTICE_CACHE_KEY, { 537 identity: identity, 538 drafts: Array.isArray(drafts) ? drafts : [] 539 }); 540 } 541 542 function hydrateDraftNoticeFromCache() { 543 if (!isAdmin()) { 544 return false;
545 } 546 var cachedDrafts = readDraftNoticeCache(); 547 if (!cachedDrafts) { 548 return false; 549 } 550 state.draftNotice.loaded = true; 551 state.draftNotice.drafts = cachedDrafts; 552 renderDraftNotice(); 553 return true; 554 } 555 556 function draftTitleFromRecord(draft) { 557 var title = String(draft && draft.title || '').trim(); 558 if (title) { 559 return title; 560 } 561 var excerpt = String(draft && draft.content_excerpt || '').trim(); 562 if (excerpt) { 563 return excerpt; 564 } 565 return 'Untitled draft'; 566 } 567 568 function draftTimestampMs(draft) { 569 var updated = String(draft && draft.updated_at || '').trim(); 570 var created = String(draft && draft.created_at || '').trim(); 571 var raw = updated || created; 572 if (!raw) { 573 return 0; 574 } 575 var ms = Date.parse(raw); 576 if (!isFinite(ms)) { 577 return 0; 578 } 579 return ms; 580 } 581 582 function pickDraftNoticeSummary(drafts) { 583 var list = Array.isArray(drafts) ? drafts : []; 584 if (!list.length) { 585 return { mode: 'none' }; 586 } 587 var sorted = list.slice().sort(function (a, b) { 588 return draftTimestampMs(b) - draftTimestampMs(a); 589 }); 590 if (sorted.length === 1) { 591 return { mode: 'continue', draft: sorted[0], count: 1 }; 592 } 593 var recentWindowMs = 36 * 60 * 60 * 1000; 594 var cutoff = Date.now() - recentWindowMs; 595 var recent = null; 596 for (var i = 0; i < sorted.length; i += 1) { 597 if (draftTimestampMs(sorted[i]) >= cutoff) { 598 recent = sorted[i]; 599 break; 600 } 601 } 602 if (recent) { 603 return { mode: 'continue', draft: recent, count: sorted.length }; 604 } 605 return { mode: 'count', count: sorted.length }; 606 } 607 608 function draftNoticeSignature(drafts) { 609 var list = Array.isArray(drafts) ? drafts : []; 610 return list.map(function (draft) { 611 return [ 612 String(draft && draft.draft_id || '').trim(), 613 String(draft && draft.updated_at || '').trim(), 614 String(draft && draft.created_at || '').trim() 615 ].join(':'); 616 }).sort().join('|'); 617 } 618 619 function draftNoticeDismissButtonHtml() { 620 return '<button type="button" class="blog-draft-notice-close" data-draft-banner-action="dismiss" aria-label="Close draft notice">' + closeIconSvg() + '</button>'; 621 } 622 623 function renderDraftNotice() { 624 var host = els.draftNotice; 625 if (!host) { 626 return; 627 } 628 if (!isAdmin()) { 629 host.hidden = true; 630 host.innerHTML = ''; 631 return; 632 } 633 if (state.compose && state.compose.open && String(state.compose.draftId || '').trim()) { 634 host.hidden = true; 635 host.innerHTML = ''; 636 return; 637 } 638 var summary = pickDraftNoticeSummary(state.draftNotice.drafts); 639 if (summary.mode === 'none') { 640 host.hidden = true; 641 host.innerHTML = ''; 642 return; 643 } 644 var signature = draftNoticeSignature(state.draftNotice.drafts); 645 if (signature && state.draftNotice.dismissedSignature === signature) { 646 host.hidden = true; 647 host.innerHTML = ''; 648 return; 649 } 650 if (summary.mode === 'count') { 651 var count = Number(summary.count) || 0; 652 var noun = count === 1 ? 'saved draft' : 'saved drafts'; 653 host.innerHTML = '<div class="blog-draft-notice-card"><span class="blog-draft-notice-main"><span>You have </span><a href="/admin#drafts" class="blog-draft-notice-link">' + escapeHtml(String(count) + ' ' + noun) + '</a><span>.</span></span>' + draftNoticeDismissButtonHtml() + '</div>'; 654 host.hidden = false; 655 return; 656 } 657 var draftId = String(summary.draft && summary.draft.draft_id || '').trim(); 658 var title = draftTitleFromRecord(summary.draft); 659 var allDraftsLink = ''; 660 if ((Number(summary.count) || 0) > 1) { 661 allDraftsLink = '<a href="/admin#drafts" class="blog-draft-notice-link blog-draft-notice-all-link">View all drafts</a>'; 662 } 663 host.innerHTML = '' + 664 '<div class="blog-draft-notice-card">' + 665 '<span class="blog-draft-notice-main">' + 666 '<span class="blog-draft-notice-text">Continue working on: </span>' + 667 '<a href="#" class="blog-draft-notice-link blog-draft-notice-continue-link" data-draft-banner-action="continue" data-draft-id="' + escapeHtml(draftId) + '"><strong>' + escapeHtml(title) + '</strong></a>' + 668 '</span>' + 669 allDraftsLink + 670 draftNoticeDismissButtonHtml() + 671 '</div>'; 672 host.hidden = false;
673 } 674 675 function loadDraftNoticeData() { 676 if (!isAdmin()) { 677 clearDraftNoticeState(); 678 return Promise.resolve(); 679 } 680 if (state.draftNotice.loading) { 681 return Promise.resolve(); 682 } 683 var auth = window.__wizardryAuthPayload(); 684 if (!auth.session_token || !auth.csrf_token) { 685 clearDraftNoticeState(); 686 return Promise.resolve(); 687 } 688 hydrateDraftNoticeFromCache(); 689 state.draftNotice.loading = true; 690 var requestSeq = state.draftNotice.requestSeq + 1; 691 state.draftNotice.requestSeq = requestSeq; 692 return apiPost('/cgi/blog-list-drafts', { 693 session_token: auth.session_token, 694 csrf_token: auth.csrf_token 695 }).then(function (data) { 696 if (requestSeq !== state.draftNotice.requestSeq) { 697 return; 698 } 699 state.draftNotice.loading = false; 700 state.draftNotice.loaded = true; 701 state.draftNotice.drafts = Array.isArray(data && data.drafts) ? data.drafts : []; 702 writeDraftNoticeCache(state.draftNotice.drafts); 703 renderDraftNotice(); 704 }).catch(function () { 705 if (requestSeq !== state.draftNotice.requestSeq) { 706 return; 707 } 708 state.draftNotice.loading = false; 709 state.draftNotice.loaded = true; 710 state.draftNotice.drafts = []; 711 renderDraftNotice(); 712 }); 713 } 714 715 function waitForInitialDraftNotice() { 716 if (!isAdmin()) { 717 clearDraftNoticeState(); 718 return Promise.resolve(); 719 } 720 return loadDraftNoticeData(); 721 } 722 723 function openDraftFromNotice(draftId) { 724 var id = String(draftId || '').trim(); 725 if (!id) { 726 return; 727 } 728 openComposeDraftInPlace(id, {}).catch(function (err) { 729 showTopToast(err && err.message ? err.message : 'Could not open draft.', 'error', 3600); 730 }); 731 } 732 733 function renderValidation() { 734 if (!els.validation) { 735 return; 736 } 737 window.__wizardryRenderManagedPayloadValidationSummary({ 738 containerNode: els.validation, 739 payload: state.payload, 740 visible: isAdmin(), 741 suppressTransientStateErrors: true 742 }); 743 } 744 745 function pageSettingsTags() { 746 var seen = {}; 747 var tags = []; 748 (Array.isArray(state.posts) ? state.posts : []).forEach(function (post) { 749 var list = Array.isArray(post && post.tags) ? post.tags : []; 750 list.forEach(function (raw) { 751 var tag = String(raw || '').trim(); 752 if (!tag || seen[tag]) { 753 return; 754 } 755 seen[tag] = true; 756 tags.push(tag); 757 }); 758 }); 759 tags.sort(function (a, b) { 760 return a.localeCompare(b); 761 }); 762 return tags; 763 } 764 765 function setPageSettingsOpen(open) { 766 if (!els.admin) { 767 return; 768 } 769 var next = !!open; 770 state.pageSettingsOpen = next; 771 var editToggle = root ? root.querySelector('[data-blog-action="toggle-page-settings"]') : null; 772 if (editToggle instanceof HTMLElement) { 773 editToggle.setAttribute('aria-expanded', next ? 'true' : 'false'); 774 } 775 if (pageSettingsHideTimer) { 776 window.clearTimeout(pageSettingsHideTimer); 777 pageSettingsHideTimer = null; 778 } 779 if (next) { 780 els.admin.hidden = false; 781 els.admin.classList.remove('is-open'); 782 void els.admin.offsetHeight; 783 window.requestAnimationFrame(function () { 784 if (els.admin) { 785 els.admin.classList.add('is-open'); 786 } 787 }); 788 return; 789 } 790 els.admin.classList.remove('is-open'); 791 pageSettingsHideTimer = window.setTimeout(function () { 792 pageSettingsHideTimer = null; 793 if (els.admin && !state.pageSettingsOpen) { 794 els.admin.hidden = true; 795 } 796 }, 240); 797 } 798 799 function renderAdmin() { 800 if (!els.admin) { 801 return; 802 } 803 window.__wizardryRenderManagedAdminActionBar({ 804 adminNode: els.admin, 805 actionsHostNode: document.getElementById('blog-page-title-actions'), 806 isAdmin: isAdmin(), 807 payload: state.payload, 808 editMode: state.pageSettingsOpen, 809 showRevert: false, 810 showPublish: false, 811 editActionAttrName: 'data-blog-action', 812 editAction: 'toggle-page-settings', 813 onHide: function () { 814 state.pageSettingsOpen = false;
815 } 816 }); 817 if (!isAdmin()) { 818 return; 819 } 820 var page = getRenderState(); 821 var selectedTag = String(pageType === 'publication' ? page.seed_tag : page.default_tag || '').trim(); 822 var tags = pageSettingsTags(); 823 var options = '<option value="">All posts</option>' + tags.map(function (tag) { 824 return '<option value="' + escapeHtml(tag) + '"' + (selectedTag === tag ? ' selected' : '') + '>' + escapeHtml(tag) + '</option>'; 825 }).join(''); 826 if (pageType === 'publication') { 827 els.admin.innerHTML = '' + 828 '<div id="blog-page-settings-panel" class="blog-page-settings-panel' + (state.pageSettingsOpen ? ' is-open' : '') + '">' + 829 '<label><span>Title</span><input type="text" data-publication-setting="title" value="' + escapeHtml(page.title) + '"></label>' + 830 '<label><span>Description</span><textarea data-publication-setting="description">' + escapeHtml(page.description) + '</textarea></label>' + 831 '<label><span>Image URL</span><input type="url" data-publication-setting="image" value="' + escapeHtml(page.image) + '"></label>' + 832 '<label><span>Homepage content (local Markdown)</span><textarea data-publication-setting="extras_after">' + escapeHtml(page.extras_after) + '</textarea></label>' + 833 '<label><span>Posts (ordered Nostr addresses)</span><textarea data-publication-setting="member_addresses">' + escapeHtml(page.member_addresses.join('\n')) + '</textarea></label>' + 834 '<div class="blog-page-settings-actions"><button type="button" data-blog-action="save-publication">Save draft</button><button type="button" data-blog-action="publish-publication">Publishâ¦</button></div>' + 835 '</div>'; 836 } else { 837 els.admin.innerHTML = '' + 838 '<div id="blog-page-settings-panel" class="blog-page-settings-panel' + (state.pageSettingsOpen ? ' is-open' : '') + '">' + 839 '<label><span>Show posts</span><select data-blog-setting="default-tag">' + options + '</select></label>' + 840 '</div>'; 841 } 842 if (state.pageSettingsOpen) { 843 els.admin.hidden = false; 844 els.admin.classList.add('is-open'); 845 } else { 846 els.admin.hidden = true; 847 els.admin.classList.remove('is-open'); 848 } 849 } 850 851 function publicationDraftFromSettings() { 852 var current = getRenderState(); 853 if (!els.admin) { 854 return current; 855 } 856 ['title', 'description', 'image', 'extras_after'].forEach(function (key) { 857 var field = els.admin.querySelector('[data-publication-setting="' + key + '"]'); 858 if (field) { 859 current[key] = String(field.value || '').trim(); 860 } 861 }); 862 var membersField = els.admin.querySelector('[data-publication-setting="member_addresses"]'); 863 if (membersField) { 864 current.member_addresses = String(membersField.value || '').split(/\r?\n/).map(function (address) { 865 return address.trim().toLowerCase(); 866 }).filter(Boolean); 867 } 868 return current; 869 } 870 871 function savePublicationDraft() { 872 if (pageType !== 'publication' || !isAdmin()) { 873 return Promise.resolve(false); 874 } 875 var draft = publicationDraftFromSettings(); 876 var auth = window.__wizardryAuthPayload(); 877 return apiPost('/cgi/blog-save-nostr-page-draft', { 878 page_slug: slug, 879 state_json: JSON.stringify(draft), 880 session_token: auth.session_token, 881 csrf_token: auth.csrf_token 882 }).then(function (data) { 883 state.payload = Object.assign({}, state.payload || {}, data || {}, { 884 state: data.state || draft, 885 draft_exists: true, 886 draft_differs: true 887 }); 888 renderAll(); 889 return true; 890 }).catch(function (err) { 891 window.alert(err && err.message ? err.message : 'Could not save publication draft'); 892 return false; 893 }); 894 } 895 896 async function publishPublication() { 897 if (!await savePublicationDraft()) { 898 return; 899 } 900 var ensure = window.__wizardryEnsureNostrPublishDialog; 901 if (typeof ensure !== 'function' || !await ensure()) { 902 window.alert('Publish dialog unavailable'); 903 return; 904 } 905 var published = await window.blogNostrPublishDialog.open({ 906 pageSlug: slug, 907 pageLabel: String(getRenderState().title || slug).trim() 908 }); 909 if (published) { 910 await loadPageState({ deferRender: false }); 911 } 912 } 913 914 function eventTargetElement(target) { 915 if (target instanceof Element) { 916 return target; 917 } 918 if (target && target.parentElement instanceof Element) { 919 return target.parentElement; 920 } 921 return null; 922 } 923 924 function ensureComposeHosts() { 925 if (!els.composeSlot || !els.composeSlot.isConnected) { 926 els.composeSlot = document.getElementById('blog-compose-slot'); 927 } 928 if (!els.composeFab || !els.composeFab.isConnected) { 929 els.composeFab = document.getElementById('blog-compose-fab'); 930 } 931 if (els.composeFab instanceof HTMLButtonElement) { 932 els.composeFab.type = 'button'; 933 els.composeFab.setAttribute('data-blog-action', 'toggle-compose'); 934 if (!els.composeFab.getAttribute('aria-label')) { 935 els.composeFab.setAttribute('aria-label', 'Compose'); 936 } 937 if (!els.composeFab.querySelector('.blog-compose-fab-icon')) { 938 els.composeFab.innerHTML = '<span class="blog-compose-fab-icon" aria-hidden="true">+</span>'; 939 } 940 } 941 } 942 943 function normalizeComposePostType(raw) { 944 var picked = String(raw || '').trim().toLowerCase(); 945 if (COMPOSE_POST_TYPES.indexOf(picked) >= 0) { 946 return picked; 947 } 948 return 'longform'; 949 } 950 951 function composePostType() { 952 ensureComposeStateShape(); 953 return normalizeComposePostType(state.compose.postType); 954 } 955 956 function composePostTypeIsTextual(type) { 957 var picked = normalizeComposePostType(type); 958 return picked === 'shortform' || picked === 'longform' || picked === 'attachment'; 959 } 960 961 function composeBackingPostType(type) { 962 var picked = normalizeComposePostType(type); 963 return picked; 964 } 965 966 function composeBuildLinkMarkdown(urlValue, bodyValue, titleValue) { 967 var url = String(urlValue || '').trim(); 968 if (!url) { 969 return ''; 970 } 971 var label = String(titleValue || '').trim() || url;
972 var out = '[' + label + '](' + url + ')'; 973 var body = String(bodyValue || '').trim(); 974 if (body) { 975 out += '\n\n' + body; 976 } 977 return out; 978 } 979 980 function parseComposeLinkShareContent(rawContent) { 981 var content = String(rawContent || ''); 982 var match = content.match(/\[[^\]]*\]\((https?:\/\/[^)\s]+)\)/i); 983 var url = ''; 984 if (match && match[1]) { 985 url = String(match[1]).trim(); 986 } 987 var body = content.replace(/\[[^\]]*\]\((https?:\/\/[^)\s]+)\)/i, '').trim(); 988 return { 989 url: url, 990 body: body 991 }; 992 } 993 994 function composeNostrTarget(postType) { 995 var type = composeBackingPostType(postType); 996 if (type === 'shortform') { 997 return { kind: '1', tags: 't=short, alt' }; 998 } 999 if (type === 'longform') { 1000 return { kind: '30023', tags: 'd, title, summary, published_at' }; 1001 } 1002 if (type === 'attachment') { 1003 return { kind: '15', tags: 'url, m, size, ox, alt' }; 1004 } 1005 if (type === 'capture-media') { 1006 return { kind: '20 or 21', tags: 'url, m=image/*|video/*, alt, dim|duration' }; 1007 } 1008 if (type === 'upload-media') { 1009 return { kind: '20 or 21', tags: 'url, m=image/*|video/*, ox, size, dim|duration' }; 1010 } 1011 if (type === 'audio-note') { 1012 return { kind: '21', tags: 'url, m=audio/*, duration, alt' }; 1013 } 1014 if (type === 'link-share') { 1015 return { kind: '1', tags: 'r, title, summary, image' }; 1016 } 1017 return { kind: '30311', tags: 'streaming, starts, status=live' }; 1018 } 1019 1020 function composePostTypeLabel(postType) { 1021 var type = normalizeComposePostType(postType); 1022 if (type === 'shortform') return 'shortform'; 1023 if (type === 'longform') return 'post'; 1024 if (type === 'capture-media') return 'capture'; 1025 if (type === 'upload-media') return 'media'; 1026 if (type === 'attachment') return 'attachment'; 1027 if (type === 'audio-note') return 'audio'; 1028 if (type === 'link-share') return 'link'; 1029 return 'go live'; 1030 } 1031 1032 function composePostKindPillText(postType) { 1033 var type = composeBackingPostType(postType); 1034 var target = composeNostrTarget(type); 1035 if (type === 'longform') return 'post (kind ' + target.kind + ')'; 1036 if (type === 'shortform') return 'shortform (kind ' + target.kind + ')'; 1037 if (type === 'capture-media') return 'capture (kind ' + target.kind + ')'; 1038 if (type === 'upload-media') return 'media (kind ' + target.kind + ')'; 1039 if (type === 'attachment') return 'attachment (kind ' + target.kind + ')'; 1040 if (type === 'audio-note') return 'audio (kind ' + target.kind + ')'; 1041 if (type === 'link-share') return 'link (kind ' + target.kind + ')'; 1042 return 'go live (kind ' + target.kind + ')'; 1043 } 1044 1045 function composePostKindPillClass(postType) { 1046 var type = composeBackingPostType(postType); 1047 if (type === 'longform') return 'is-type-nip23'; 1048 if (type === 'capture-media' || type === 'upload-media') return 'is-type-icon-gallery'; 1049 if (type === 'audio-note') return 'is-type-public-ranking'; 1050 if (type === 'link-share') return 'is-type-blog'; 1051 if (type === 'shortform') return 'is-type-list'; 1052 return 'is-type-public-ranking'; 1053 } 1054 1055 function composeNostrPillsHtml(postType) { 1056 var type = composeBackingPostType(postType); 1057 var target = composeNostrTarget(type); 1058 var kindText = composePostKindPillText(type); 1059 var badgeClass = composePostKindPillClass(type); 1060 return '<span class="nostr-page-kind-badge ' + badgeClass + '" title="' + escapeHtml(kindText + ' · ' + target.tags) + '">' + escapeHtml(kindText) + '</span>'; 1061 } 1062 1063 function clearComposePostTypeCollapseTimer() { 1064 ensureComposeStateShape(); 1065 if (state.compose.postTypeToolbarCollapseTimer) { 1066 window.clearTimeout(state.compose.postTypeToolbarCollapseTimer); 1067 state.compose.postTypeToolbarCollapseTimer = null; 1068 } 1069 } 1070 1071 function applyComposePostTypeControlState() { 1072 if (!els.composeSlot) { 1073 return; 1074 } 1075 var control = els.composeSlot.querySelector('[data-compose-type-control]'); 1076 var headRow = els.composeSlot.querySelector('[data-compose-head-row]'); 1077 if (!control) { 1078 return; 1079 } 1080 control.classList.toggle('is-collapsed', !!state.compose.postTypeToolbarCollapsed); 1081 if (headRow) { 1082 headRow.classList.toggle('is-type-collapsed', !!state.compose.postTypeToolbarCollapsed); 1083 } 1084 } 1085 1086 function setComposePostTypeToolbarCollapsed(collapsed, options) { 1087 ensureComposeStateShape(); 1088 var opts = options || {}; 1089 state.compose.postTypeToolbarCollapsed = !!collapsed; 1090 if (opts.clearTimer !== false && collapsed) { 1091 clearComposePostTypeCollapseTimer(); 1092 } 1093 if (opts.skipDom) { 1094 return; 1095 } 1096 applyComposePostTypeControlState(); 1097 } 1098 1099 function scheduleComposePostTypeCollapse(delayMs) { 1100 ensureComposeStateShape(); 1101 clearComposePostTypeCollapseTimer(); 1102 if (!state.compose.open) { 1103 return; 1104 } 1105 state.compose.postTypeToolbarCollapseTimer = window.setTimeout(function () { 1106 state.compose.postTypeToolbarCollapseTimer = null; 1107 if (!state.compose.open) { 1108 return; 1109 } 1110 setComposePostTypeToolbarCollapsed(true); 1111 }, Math.max(800, Number(delayMs) || 2600)); 1112 } 1113 1114 function composePostTypeIconSvg(type) { 1115 var picked = normalizeComposePostType(type); 1116 if (picked === 'shortform') {
1117 return '<svg class="compose-post-type-icon" viewBox="0 0 96.314 100" fill="none" aria-hidden="true">' + 1118 '<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"/>' + 1119 '</svg>'; 1120 } 1121 if (picked === 'longform') { 1122 return '<svg class="compose-post-type-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true">' + 1123 '<path d="M5 7H19M5 11H19M5 15H17M5 19H19" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>' + 1124 '</svg>'; 1125 } 1126 if (picked === 'capture-media') { 1127 return '<svg class="compose-post-type-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true">' + 1128 '<rect x="4" y="7" width="16" height="11" rx="2.2" stroke="currentColor" stroke-width="1.8"/>' + 1129 '<circle cx="12" cy="12.5" r="2.6" stroke="currentColor" stroke-width="1.8"/>' + 1130 '<path d="M9.2 7L10.4 5.2H13.6L14.8 7" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>' + 1131 '</svg>'; 1132 } 1133 if (picked === 'upload-media') { 1134 return '<svg class="compose-post-type-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true">' + 1135 '<rect x="3.6" y="5.1" width="16.8" height="13.8" rx="2.2" stroke="currentColor" stroke-width="1.8"/>' + 1136 '<circle cx="8.4" cy="9.7" r="1.4" fill="currentColor"/>' + 1137 '<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"/>' + 1138 '</svg>'; 1139 } 1140 if (picked === 'attachment') { 1141 return '<svg class="compose-post-type-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true">' + 1142 '<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"/>' + 1143 '</svg>'; 1144 } 1145 if (picked === 'audio-note') { 1146 return '<svg class="compose-post-type-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true">' + 1147 '<rect x="9" y="4.2" width="6" height="10" rx="3" stroke="currentColor" stroke-width="1.8"/>' + 1148 '<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"/>' + 1149 '</svg>'; 1150 } 1151 if (picked === 'link-share') { 1152 return '<svg class="compose-post-type-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true">' + 1153 '<rect x="2.7" y="8.5" width="8.8" height="7" rx="3.5" stroke="currentColor" stroke-width="1.8"/>' + 1154 '<rect x="12.5" y="8.5" width="8.8" height="7" rx="3.5" stroke="currentColor" stroke-width="1.8"/>' + 1155 '<path d="M9.8 12H14.2" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>' + 1156 '</svg>'; 1157 } 1158 return '<svg class="compose-post-type-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true">' + 1159 '<circle cx="12" cy="12" r="7" stroke="currentColor" stroke-width="1.8"/>' + 1160 '<circle cx="12" cy="12" r="2.5" fill="currentColor"/>' + 1161 '</svg>'; 1162 } 1163 1164 function composePreviewToggleIconSvg() { 1165 return '<svg class="compose-preview-toggle-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true">' + 1166 '<path d="M7.2 3.8H13.4L18.9 9.3V18.5C18.9 19.4 18.2 20.1 17.3 20.1H7.2C6.3 20.1 5.6 19.4 5.6 18.5V5.4C5.6 4.5 6.3 3.8 7.2 3.8Z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/>' + 1167 '<path d="M13.4 3.8V9.3H18.9" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/>' + 1168 '<circle cx="10.6" cy="13.8" r="2.1" stroke="currentColor" stroke-width="1.8"/>' + 1169 '<path d="M12.1 15.3L14.3 17.5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>' + 1170 '</svg>'; 1171 } 1172 1173 function composeCloseIconSvg() {
1174 return '<svg class="compose-close-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true">' + 1175 '<path d="M7 7L17 17" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/>' + 1176 '<path d="M17 7L7 17" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/>' + 1177 '</svg>'; 1178 } 1179 1180 function composeTypeButtonsHtml(activeType, opts) { 1181 var options = opts || {}; 1182 var lockAll = !!options.lockAll; 1183 var noActive = !!options.noActive; 1184 var current = noActive ? '' : normalizeComposePostType(activeType); 1185 function btn(type, label, disabled) { 1186 var buttonDisabled = !!disabled || lockAll; 1187 var cls = 'compose-post-type-pill'; 1188 if (type === current) { 1189 cls += ' is-active'; 1190 } 1191 if (buttonDisabled) { 1192 cls += ' is-disabled'; 1193 } 1194 var icon = composePostTypeIconSvg(type); 1195 var title = disabled ? ('Coming soon: ' + label) : (buttonDisabled ? ('Post type locked: ' + label) : label); 1196 return '<button type="button" class="' + cls + '" data-compose-action="set-post-type" data-compose-post-type="' + escapeHtml(type) + '"' + 1197 (buttonDisabled ? ' disabled aria-disabled="true"' : '') + 1198 ' aria-pressed="' + (type === current ? 'true' : 'false') + '"' + 1199 ' aria-label="' + escapeHtml(label) + '"' + 1200 ' title="' + escapeHtml(title) + '"' + 1201 '>' + icon + '</button>'; 1202 } 1203 return '' + 1204 '<div class="compose-post-type-toolbar" role="tablist" aria-label="Post type">' + 1205 btn('shortform', composePostTypeLabel('shortform'), false) + 1206 btn('longform', composePostTypeLabel('longform'), false) + 1207 btn('capture-media', composePostTypeLabel('capture-media'), false) + 1208 btn('upload-media', composePostTypeLabel('upload-media'), false) + 1209 btn('attachment', composePostTypeLabel('attachment'), false) + 1210 btn('audio-note', composePostTypeLabel('audio-note'), false) + 1211 btn('link-share', composePostTypeLabel('link-share'), false) + 1212 btn('go-live', composePostTypeLabel('go-live'), true) + 1213 '</div>'; 1214 } 1215 1216 function normalizeComposeShortformLimit(raw) { 1217 var n = parseInt(String(raw || '').trim(), 10); 1218 if (!isFinite(n) || n < 1) { 1219 return 280; 1220 } 1221 return Math.max(1, Math.min(5000, n)); 1222 } 1223 1224 function currentComposeShortformLimit() { 1225 ensureComposeStateShape(); 1226 state.compose.shortformLimit = normalizeComposeShortformLimit(state.compose.shortformLimit); 1227 return state.compose.shortformLimit; 1228 } 1229 1230 function enforceComposeShortformLimitOnFields(fields) { 1231 var data = fields || readComposeFields(); 1232 if (!data) { 1233 return; 1234 } 1235 if (normalizeComposePostType(data.postType) !== 'shortform') { 1236 return; 1237 } 1238 var limit = currentComposeShortformLimit(); 1239 if (String(data.content || '').length <= limit) { 1240 return; 1241 } 1242 var next = String(data.content || '').slice(0, limit); 1243 if (els.composeSlot) { 1244 var contentField = els.composeSlot.querySelector('[data-compose-field="content"]'); 1245 if (contentField instanceof HTMLTextAreaElement) { 1246 contentField.value = next; 1247 } 1248 } 1249 } 1250 1251 function composeShortformMeterHtml(contentValue) { 1252 var limit = currentComposeShortformLimit(); 1253 var count = String(contentValue || '').length; 1254 var editing = !!state.compose.shortformLimitEditing; 1255 return '' + 1256 '<div class="blog-compose-shortform-meter">' + 1257 '<button type="button" class="unobtrusive-icon-button blog-compose-shortform-limit-btn" data-compose-action="shortform-limit-toggle" title="Click to toggle 280/140. Double-click to set custom limit."' + (editing ? ' hidden' : '') + '>' + escapeHtml(String(count) + '/' + String(limit)) + '</button>' + 1258 '<input type="number" class="blog-compose-shortform-limit-input" data-compose-field="shortform-limit" min="1" step="1" inputmode="numeric" value="' + escapeHtml(String(limit)) + '"' + (editing ? '' : ' hidden') + '>' + 1259 '</div>'; 1260 } 1261 1262 function setComposeShortformLimit(raw, opts) { 1263 var options = opts || {}; 1264 state.compose.shortformLimit = normalizeComposeShortformLimit(raw); 1265 if (options.editing === true || options.editing === false) { 1266 state.compose.shortformLimitEditing = !!options.editing; 1267 } 1268 enforceComposeShortformLimitOnFields(); 1269 if (!options.skipRender) { 1270 renderComposeUi(); 1271 } 1272 if (!options.skipAutosave) { 1273 queueComposeAutosave(); 1274 } 1275 } 1276 1277 function refreshComposeShortformMeterFromFields() { 1278 if (!els.composeSlot || composePostType() !== 'shortform') { 1279 return; 1280 } 1281 var contentField = els.composeSlot.querySelector('[data-compose-field="content"]'); 1282 var meterButton = els.composeSlot.querySelector('[data-compose-action="shortform-limit-toggle"]'); 1283 if (!(contentField instanceof HTMLTextAreaElement) || !(meterButton instanceof HTMLButtonElement)) { 1284 return; 1285 } 1286 meterButton.textContent = String(contentField.value || '').length + '/' + String(currentComposeShortformLimit()); 1287 } 1288 1289 function composeTextareaRows(postType) { 1290 var type = normalizeComposePostType(postType); 1291 if (type === 'shortform') { 1292 return 9; 1293 } 1294 if (composePostTypeIsTextual(type)) { 1295 return 14; 1296 } 1297 return 8; 1298 } 1299 1300 function setComposePostType(nextType, options) { 1301 var opts = options || {}; 1302 var normalized = normalizeComposePostType(nextType);
1303 var wasChosen = !!state.compose.postTypeChosen; 1304 if (normalized === 'go-live') { 1305 setComposeOutput('Go Live is a future feature.', 'warn'); 1306 renderComposeStatusOnly(); 1307 return; 1308 } 1309 // For initial media selection, keep chooser mode until capture/file pick completes. 1310 if (opts.interactive && normalized === 'capture-media') { 1311 state.compose.postType = normalized; 1312 state.compose.postTypeChosen = wasChosen; 1313 state.compose.postTypeToolbarCollapsed = false; 1314 if (!opts.skipRender) { 1315 renderComposeUi(); 1316 } 1317 setTimeout(function () { 1318 openComposeCameraCapture({ returnToChooser: !wasChosen }); 1319 }, 0); 1320 return; 1321 } 1322 if (opts.interactive && normalized === 'upload-media' && !wasChosen) { 1323 state.compose.postType = normalized; 1324 state.compose.postTypeChosen = false; 1325 state.compose.postTypeToolbarCollapsed = false; 1326 if (!opts.skipRender) { 1327 renderComposeUi(); 1328 } 1329 if (!openComposePickerForType(normalized)) { 1330 setTimeout(function () { 1331 openComposePickerForType(normalized); 1332 }, 0); 1333 } 1334 return; 1335 } 1336 if (opts.interactive && normalized === 'audio-note') { 1337 state.compose.postType = normalized; 1338 state.compose.postTypeChosen = true; 1339 state.compose.postTypeToolbarCollapsed = true; 1340 if (!opts.skipRender) { 1341 renderComposeUi(); 1342 } 1343 if (!opts.skipAutosave) { 1344 queueComposeAutosave(); 1345 } 1346 setTimeout(function () { 1347 if (!state.compose.open || composePostType() !== 'audio-note') { 1348 return; 1349 } 1350 if (!state.compose.audioRecording) { 1351 startComposeAudioRecording(); 1352 } 1353 }, 0); 1354 return; 1355 } 1356 state.compose.postType = normalized; 1357 state.compose.postTypeChosen = true; 1358 state.compose.postTypeToolbarCollapsed = true; 1359 if (!opts.skipRender) { 1360 renderComposeUi(); 1361 } 1362 if (!opts.skipAutosave) { 1363 queueComposeAutosave(); 1364 } 1365 if (opts.interactive) { 1366 if (normalized === 'upload-media' || normalized === 'attachment') { 1367 if (!openComposePickerForType(normalized)) { 1368 setTimeout(function () { 1369 openComposePickerForType(normalized); 1370 }, 0); 1371 } 1372 } else if (normalized === 'capture-media') { 1373 setTimeout(function () { 1374 applyComposeModeEffects(normalized); 1375 }, 0); 1376 } 1377 } 1378 } 1379 1380 function normalizeTagValue(tag) { 1381 var value = String(tag || '').trim(); 1382 if (!value) { 1383 return ''; 1384 } 1385 value = value.replace(/^\[+|\]+$/g, ''); 1386 value = value.replace(/^["']+|["']+$/g, ''); 1387 value = value.replace(/\\+/g, ''); 1388 value = value.replace(/\s+/g, '-'); 1389 value = value.replace(/^-+|-+$/g, ''); 1390 if (!/[A-Za-z0-9]/.test(value)) { 1391 return ''; 1392 } 1393 if (/[\[\]{}]/.test(value)) { 1394 return ''; 1395 } 1396 value = value.replace(/[^A-Za-z0-9._:+/-]/g, ''); 1397 value = value.replace(/^-+|-+$/g, ''); 1398 if (!value || value.length > 64) { 1399 return ''; 1400 } 1401 return value; 1402 } 1403 1404 function syncComposeTagsField() { 1405 if (!els.composeSlot) { 1406 return; 1407 } 1408 var hidden = els.composeSlot.querySelector('[data-compose-field="tags"]'); 1409 if (hidden instanceof HTMLInputElement) { 1410 hidden.value = state.compose.tags.join(','); 1411 } 1412 } 1413 1414 function setComposeTags(tags) { 1415 var list = Array.from(tags || []) 1416 .map(normalizeTagValue) 1417 .filter(Boolean); 1418 state.compose.tags = list.filter(function (tag, idx) { 1419 return list.indexOf(tag) === idx; 1420 }); 1421 syncComposeTagsField(); 1422 } 1423 1424 function composeDefaultTag() { 1425 var page = getRenderState(); 1426 return normalizeTagValue(String(page && page.default_tag || '')); 1427 } 1428 1429 function seedComposeDefaultTag(forceReplace) { 1430 var tag = composeDefaultTag(); 1431 if (!tag) { 1432 if (forceReplace) { 1433 setComposeTags([]); 1434 } 1435 return; 1436 } 1437 if (forceReplace) { 1438 setComposeTags([tag]); 1439 return; 1440 } 1441 if (!state.compose.tags.length) { 1442 setComposeTags([tag]); 1443 } 1444 } 1445 1446 function composeTagTokenHtml(tag) { 1447 return '<span class="tag-token" contenteditable="false" data-compose-tag-token="' + escapeHtml(tag) + '" tabindex="-1"><span class="tag-token-label">' + escapeHtml(tag) + '</span></span>'; 1448 } 1449 1450 function composeTagsEditorHtml() { 1451 var tokensHtml = state.compose.tags.map(composeTagTokenHtml).join(''); 1452 return '' + 1453 '<div class="tag-token-editor" data-compose-field="tags-editor" contenteditable="true" role="textbox" aria-label="Post tags" spellcheck="false" data-placeholder="tag, tag, tag">' + 1454 tokensHtml + 1455 '<span class="tag-token-editor-draft" data-compose-tag-draft>' + escapeHtml(state.compose.tagsDraftText || '') + '</span>' + 1456 '</div>'; 1457 } 1458 1459 function composeTagsEditorNode() { 1460 if (!els.composeSlot) { 1461 return null; 1462 } 1463 var node = els.composeSlot.querySelector('[data-compose-field="tags-editor"]'); 1464 return node instanceof HTMLElement ? node : null; 1465 } 1466 1467 function composeTagsEditorDraftNode(editor) { 1468 if (!(editor instanceof HTMLElement)) { 1469 return null; 1470 } 1471 var node = editor.querySelector('[data-compose-tag-draft]'); 1472 return node instanceof HTMLElement ? node : null; 1473 } 1474 1475 function composeTagsEditorSelectedToken(editor) { 1476 if (!(editor instanceof HTMLElement)) { 1477 return null; 1478 } 1479 var node = editor.querySelector('.tag-token.is-selected[data-compose-tag-token]'); 1480 return node instanceof HTMLElement ? node : null; 1481 } 1482 1483 function composeTagsEditorSetEmptyClass(editor) { 1484 if (!(editor instanceof HTMLElement)) { 1485 return; 1486 } 1487 var draft = String(state.compose.tagsDraftText || '').trim(); 1488 var empty = !state.compose.tags.length && !draft; 1489 editor.classList.toggle('is-empty', empty); 1490 } 1491 1492 function composeTagsEditorRender(editor) { 1493 if (!(editor instanceof HTMLElement)) { 1494 return; 1495 } 1496 var html = state.compose.tags.map(composeTagTokenHtml).join(''); 1497 html += '<span class="tag-token-editor-draft" data-compose-tag-draft>' + escapeHtml(state.compose.tagsDraftText || '') + '</span>'; 1498 editor.innerHTML = html; 1499 composeTagsEditorSetEmptyClass(editor); 1500 } 1501 1502 function composeTagsEditorFocusDraft(editor) { 1503 if (!(editor instanceof HTMLElement)) { 1504 return; 1505 } 1506 var draftNode = composeTagsEditorDraftNode(editor); 1507 if (!(draftNode instanceof HTMLElement)) { 1508 return; 1509 } 1510 var range = document.createRange(); 1511 range.selectNodeContents(draftNode); 1512 range.collapse(false); 1513 var selection = window.getSelection ? window.getSelection() : null; 1514 if (selection) { 1515 selection.removeAllRanges(); 1516 selection.addRange(range); 1517 } 1518 if (document.activeElement !== editor) {
1519 try { 1520 editor.focus({ preventScroll: true }); 1521 } catch (_focusErr) { 1522 editor.focus(); 1523 } 1524 } 1525 } 1526 1527 function composeTagsEditorPlaceCaretFromPoint(editor, clientX, clientY) { 1528 if (!(editor instanceof HTMLElement)) { 1529 return false; 1530 } 1531 var selection = window.getSelection ? window.getSelection() : null; 1532 if (!selection) { 1533 return false; 1534 } 1535 var range = null; 1536 if (document.caretPositionFromPoint) { 1537 var pos = document.caretPositionFromPoint(clientX, clientY); 1538 if (pos && pos.offsetNode && editor.contains(pos.offsetNode)) { 1539 range = document.createRange(); 1540 range.setStart(pos.offsetNode, Number(pos.offset) || 0); 1541 range.collapse(true); 1542 } 1543 } else if (document.caretRangeFromPoint) { 1544 var pointRange = document.caretRangeFromPoint(clientX, clientY); 1545 if (pointRange && pointRange.startContainer && editor.contains(pointRange.startContainer)) { 1546 range = pointRange; 1547 range.collapse(true); 1548 } 1549 } 1550 if (!range) { 1551 return false; 1552 } 1553 selection.removeAllRanges(); 1554 selection.addRange(range); 1555 return true; 1556 } 1557 1558 function composeTagsEditorClearSelection(editor) { 1559 if (!(editor instanceof HTMLElement)) { 1560 return; 1561 } 1562 var selected = composeTagsEditorSelectedToken(editor); 1563 if (selected) { 1564 selected.classList.remove('is-selected'); 1565 } 1566 } 1567 1568 function composeTagsEditorSelectToken(editor, tokenNode) { 1569 if (!(editor instanceof HTMLElement) || !(tokenNode instanceof HTMLElement)) { 1570 return; 1571 } 1572 composeTagsEditorClearSelection(editor); 1573 tokenNode.classList.add('is-selected'); 1574 try { 1575 editor.focus({ preventScroll: true }); 1576 } catch (_focusErr) { 1577 editor.focus(); 1578 } 1579 } 1580 1581 function composeTagsEditorReadDraftText(editor) { 1582 if (!(editor instanceof HTMLElement)) { 1583 return ''; 1584 } 1585 var clone = editor.cloneNode(true); 1586 Array.from(clone.querySelectorAll('[data-compose-tag-token]')).forEach(function (node) { 1587 if (node && node.parentNode) { 1588 node.parentNode.removeChild(node); 1589 } 1590 }); 1591 return String(clone.textContent || '').replace(/\u00a0/g, ' '); 1592 } 1593 1594 function composeTagsEditorSyncDraft(editor) { 1595 if (!(editor instanceof HTMLElement)) { 1596 return; 1597 } 1598 state.compose.tagsDraftText = composeTagsEditorReadDraftText(editor); 1599 composeTagsEditorSetEmptyClass(editor); 1600 } 1601 1602 function composeTagsEditorCommit(editor, forceFinalize) { 1603 if (!(editor instanceof HTMLElement)) { 1604 return false; 1605 } 1606 var draft = composeTagsEditorReadDraftText(editor); 1607 var rawParts = String(draft || '').split(','); 1608 if (!rawParts.length) { 1609 return false; 1610 } 1611 var changed = false; 1612 var limit = forceFinalize ? rawParts.length : Math.max(0, rawParts.length - 1); 1613 for (var i = 0; i < limit; i += 1) { 1614 if (addComposeTag(rawParts[i])) { 1615 changed = true; 1616 } 1617 } 1618 var nextDraft = forceFinalize ? '' : String(rawParts[rawParts.length - 1] || ''); 1619 var draftChanged = nextDraft !== draft; 1620 state.compose.tagsDraftText = nextDraft; 1621 if (changed || draftChanged) { 1622 composeTagsEditorRender(editor); 1623 composeTagsEditorFocusDraft(editor); 1624 } else { 1625 composeTagsEditorSetEmptyClass(editor); 1626 } 1627 return changed || draftChanged; 1628 } 1629 1630 function composeTagsEditorRemoveTagByNode(editor, tokenNode) { 1631 if (!(editor instanceof HTMLElement) || !(tokenNode instanceof HTMLElement)) { 1632 return false; 1633 } 1634 var tag = String(tokenNode.getAttribute('data-compose-tag-token') || '').trim(); 1635 if (!tag) { 1636 return false; 1637 } 1638 if (state.compose.tags.indexOf(tag) < 0) { 1639 return false; 1640 } 1641 removeComposeTag(tag); 1642 composeTagsEditorRender(editor); 1643 composeTagsEditorFocusDraft(editor); 1644 return true; 1645 } 1646 1647 function hydrateComposeTagsEditor() { 1648 var editor = composeTagsEditorNode(); 1649 if (!editor) { 1650 return; 1651 } 1652 if (!(composeTagsEditorDraftNode(editor) instanceof HTMLElement)) { 1653 composeTagsEditorRender(editor); 1654 } else { 1655 composeTagsEditorSetEmptyClass(editor); 1656 } 1657 } 1658 1659 function addComposeTag(rawTag) { 1660 var tag = normalizeTagValue(rawTag); 1661 if (!tag || state.compose.tags.indexOf(tag) !== -1) { 1662 return false;
1663 } 1664 state.compose.tags.push(tag); 1665 state.compose.tagsOpen = true; 1666 syncComposeTagsField(); 1667 return true; 1668 } 1669 1670 function removeComposeTag(tag) { 1671 setComposeTags(state.compose.tags.filter(function (item) { 1672 return item !== tag; 1673 })); 1674 } 1675 1676 function commitComposeTagInput() { 1677 var editor = composeTagsEditorNode(); 1678 if (editor) { 1679 return composeTagsEditorCommit(editor, true); 1680 } 1681 state.compose.tagsDraftText = ''; 1682 return false; 1683 } 1684 1685 function readFileAsDataUrl(file) { 1686 return new Promise(function (resolve, reject) { 1687 var reader = new FileReader(); 1688 reader.onload = function () { resolve(String(reader.result || '')); }; 1689 reader.onerror = function () { reject(new Error('Failed to read file')); }; 1690 reader.readAsDataURL(file); 1691 }); 1692 } 1693 1694 function composeUploadMarkdown(url, file) { 1695 var safeUrl = String(url || '').trim(); 1696 if (!safeUrl) { 1697 return ''; 1698 } 1699 var name = String((file && file.name) || 'file').trim(); 1700 var mime = String((file && file.type) || '').toLowerCase(); 1701 var alt = name.replace(/\.[^.]+$/, '') || 'media'; 1702 if (mime.indexOf('image/') === 0) {
1703 return ''; 1704 } 1705 if (mime.indexOf('video/') === 0) { 1706 return '<video controls src="' + safeUrl + '"></video>'; 1707 } 1708 if (mime.indexOf('audio/') === 0) { 1709 return '<audio controls src="' + safeUrl + '"></audio>'; 1710 } 1711 return '[' + name + '](' + safeUrl + ')'; 1712 } 1713 1714 function appendComposeContent(text) { 1715 ensureComposeStateShape(); 1716 if (!els.composeSlot) { 1717 var queuedOnly = String(text || '').trim(); 1718 if (queuedOnly) { 1719 state.compose.pendingContentAdditions.push(queuedOnly); 1720 } 1721 return queuedOnly ? 'queued' : 'skipped'; 1722 } 1723 var source = els.composeSlot.querySelector('[data-compose-field="content"]'); 1724 var addition = String(text || '').trim(); 1725 if (!addition) { 1726 return 'skipped'; 1727 } 1728 if (!(source instanceof HTMLTextAreaElement)) { 1729 state.compose.pendingContentAdditions.push(addition); 1730 return 'queued'; 1731 } 1732 var current = String(source.value || ''); 1733 source.value = current.trim() ? (current.replace(/\s*$/, '') + '\n\n' + addition) : addition; 1734 return 'inserted'; 1735 } 1736 1737 function clearComposeUploadCleanupTimer() { 1738 if (!state.compose || !state.compose.uploadCleanupTimer) { 1739 return; 1740 } 1741 clearTimeout(state.compose.uploadCleanupTimer); 1742 state.compose.uploadCleanupTimer = null; 1743 } 1744 1745 function clearComposeUploadItems() { 1746 ensureComposeStateShape(); 1747 clearComposeUploadCleanupTimer(); 1748 state.compose.uploadItems = []; 1749 state.compose.uploading = 0; 1750 } 1751 1752 function composeUploadDisplayName(file) { 1753 var raw = String((file && file.name) || '').trim(); 1754 if (raw) { 1755 return raw; 1756 } 1757 return 'upload-' + String(Date.now()) + '.bin'; 1758 } 1759 1760 function addComposeUploadEntries(files) { 1761 ensureComposeStateShape(); 1762 var entries = Array.from(files || []).map(function (file) { 1763 state.compose.uploadSeq += 1; 1764 return { 1765 id: 'upload-' + String(state.compose.uploadSeq), 1766 name: composeUploadDisplayName(file), 1767 progress: 0, 1768 status: 'queued', 1769 error: '' 1770 }; 1771 }); 1772 if (entries.length) { 1773 state.compose.uploadItems = state.compose.uploadItems.concat(entries); 1774 clearComposeUploadCleanupTimer(); 1775 } 1776 return entries; 1777 } 1778 1779 function updateComposeUploadEntry(entryId, patch) { 1780 ensureComposeStateShape(); 1781 var id = String(entryId || '').trim(); 1782 if (!id) { 1783 return; 1784 } 1785 for (var idx = 0; idx < state.compose.uploadItems.length; idx += 1) { 1786 var item = state.compose.uploadItems[idx]; 1787 if (String(item && item.id || '') !== id) { 1788 continue; 1789 } 1790 var next = Object.assign({}, item, patch || {}); 1791 if (!isFinite(Number(next.progress))) { 1792 next.progress = 0; 1793 } 1794 next.progress = Math.max(0, Math.min(100, Math.round(Number(next.progress)))); 1795 state.compose.uploadItems[idx] = next; 1796 return; 1797 } 1798 } 1799 1800 function scheduleComposeUploadCleanup() { 1801 ensureComposeStateShape(); 1802 clearComposeUploadCleanupTimer(); 1803 // Keep completed uploads visible in the compose upload list until the 1804 // compose state is explicitly cleared/closed by the user. 1805 } 1806 1807 function composeUploadProgressHtml() { 1808 ensureComposeStateShape(); 1809 var items = Array.isArray(state.compose.uploadItems) ? state.compose.uploadItems : []; 1810 if (!items.length) { 1811 return '<div class="blog-compose-upload-list" data-compose-upload-progress hidden></div>'; 1812 } 1813 var rows = items.map(function (item) { 1814 var name = escapeHtml(String((item && item.name) || 'upload.bin')); 1815 var status = String((item && item.status) || 'queued'); 1816 var progress = Math.max(0, Math.min(100, Math.round(Number(item && item.progress) || 0))); 1817 var pctText = status === 'error' ? 'Failed' : (String(progress) + '%'); 1818 return '' + 1819 '<div class="blog-compose-upload-row is-' + escapeHtml(status) + '">' + 1820 '<span class="blog-compose-upload-name">' + name + '</span>' + 1821 '<div class="blog-compose-upload-progress">' + 1822 '<div class="blog-compose-upload-track"><div class="blog-compose-upload-fill" style="width:' + String(progress) + '%"></div></div>' + 1823 '<span class="blog-compose-upload-state">' + pctText + '</span>' + 1824 '</div>' + 1825 '</div>'; 1826 }).join(''); 1827 return '<div class="blog-compose-upload-list" data-compose-upload-progress>' + rows + '</div>'; 1828 } 1829 1830 function renderComposeUploadProgressOnly() { 1831 if (!els.composeSlot || !state.compose.open) { 1832 return; 1833 } 1834 var host = els.composeSlot.querySelector('[data-compose-upload-progress]'); 1835 if (!host) { 1836 return; 1837 } 1838 var html = composeUploadProgressHtml(); 1839 var wrapper = document.createElement('div'); 1840 wrapper.innerHTML = html; 1841 var nextHost = wrapper.firstChild; 1842 if (!(nextHost instanceof Element)) { 1843 host.hidden = true; 1844 host.innerHTML = ''; 1845 return; 1846 } 1847 host.hidden = !!nextHost.hidden; 1848 host.className = nextHost.className; 1849 host.innerHTML = nextHost.innerHTML; 1850 } 1851 1852 function uploadComposeFile(file, options) { 1853 var opts = options || {}; 1854 function normalizeUploadDataBase64(dataUrl) { 1855 var raw = String(dataUrl || '').trim(); 1856 if (!raw) { 1857 return ''; 1858 } 1859 var commaIdx = raw.indexOf(','); 1860 if (commaIdx > 0 && /^data:/i.test(raw.slice(0, commaIdx))) { 1861 return raw.slice(commaIdx + 1); 1862 } 1863 return raw; 1864 } 1865 function isMissingUploadPayloadError(data, statusCode) { 1866 if (!data || typeof data !== 'object') { 1867 return false;
1868 } 1869 var message = String(data.error || '').toLowerCase(); 1870 var code = String(data.code || '').toLowerCase(); 1871 if (message.indexOf('filename and data_base64 are required') < 0) { 1872 return false; 1873 } 1874 // Some deployments return this payload with HTTP 200 and success=false. 1875 return code === '' || code === 'invalid_request' || Number(statusCode) >= 200; 1876 } 1877 var auth = window.__wizardryAuthPayload(); 1878 if (!auth.session_token || !auth.csrf_token) { 1879 return Promise.reject(new Error('Sign in again to upload.')); 1880 } 1881 return readFileAsDataUrl(file).then(function (dataUrl) { 1882 var rawDataUrl = String(dataUrl || '').trim(); 1883 var bareDataBase64 = normalizeUploadDataBase64(rawDataUrl); 1884 var safeFilename = String((file && file.name) || 'upload.bin').trim() || 'upload.bin'; 1885 var safeMimeType = String((file && file.type) || ''); 1886 if (!rawDataUrl) { 1887 return Promise.reject(new Error('Failed to read file')); 1888 } 1889 return new Promise(function (resolve, reject) { 1890 var fallbackAttempted = false; 1891 function buildPayload(useBareData) { 1892 return { 1893 session_token: auth.session_token, 1894 csrf_token: auth.csrf_token, 1895 draft_id: String(state.compose.draftId || ''), 1896 filename: safeFilename, 1897 mime_type: safeMimeType, 1898 data_base64: useBareData ? bareDataBase64 : rawDataUrl 1899 }; 1900 } 1901 function sendAttempt(useBareData) { 1902 var body = new URLSearchParams(buildPayload(useBareData)).toString(); 1903 var xhr = new XMLHttpRequest(); 1904 xhr.open('POST', '/cgi/blog-upload-media', true); 1905 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8'); 1906 if (xhr.upload && typeof opts.onProgress === 'function') { 1907 xhr.upload.onprogress = function (progressEvent) { 1908 if (!progressEvent || !progressEvent.lengthComputable || progressEvent.total <= 0) { 1909 return; 1910 } 1911 opts.onProgress(progressEvent.loaded, progressEvent.total); 1912 }; 1913 } 1914 xhr.onreadystatechange = function () { 1915 if (xhr.readyState !== 4) { 1916 return; 1917 } 1918 var data; 1919 try { 1920 data = JSON.parse(String(xhr.responseText || '')); 1921 } catch (_err) { 1922 reject(new Error('Invalid JSON response')); 1923 return; 1924 } 1925 if (xhr.status < 200 || xhr.status >= 300 || !data || data.success === false) { 1926 if (!fallbackAttempted && isMissingUploadPayloadError(data, xhr.status)) { 1927 fallbackAttempted = true; 1928 sendAttempt(!useBareData); 1929 return; 1930 } 1931 reject(new Error((data && data.error) || ('Upload failed (' + xhr.status + ')'))); 1932 return; 1933 } 1934 resolve(data); 1935 }; 1936 xhr.onerror = function () { 1937 reject(new Error('Upload request failed')); 1938 }; 1939 xhr.send(body); 1940 } 1941 // Prefer bare base64 first; fallback to data URL form for compatibility. 1942 sendAttempt(true); 1943 }); 1944 }); 1945 } 1946 1947 function handleComposeUploads(files, preferredType) { 1948 ensureComposeStateShape(); 1949 var list = Array.from(files || []).filter(function (file) { 1950 return file && file.size >= 0; 1951 }); 1952 if (!list.length) { 1953 return Promise.resolve(); 1954 } 1955 if (!state.compose.open) { 1956 setComposeOpen(true); 1957 } 1958 var currentType = state.compose.postTypeChosen ? composePostType() : ''; 1959 var targetType = normalizeComposePostType(preferredType || currentType || 'attachment'); 1960 if (targetType === 'shortform' || targetType === 'capture-media' || targetType === 'go-live') { 1961 targetType = 'attachment'; 1962 } 1963 if (!state.compose.postTypeChosen || preferredType) { 1964 setComposePostType(targetType, { skipAutosave: true }); 1965 } else { 1966 renderComposeStatusOnly(); 1967 } 1968 var uploadEntries = addComposeUploadEntries(list); 1969 state.compose.uploading += list.length; 1970 setComposeOutput('', ''); 1971 showTopToast('Uploading ' + list.length + ' file(s)...', 'warn', 1600); 1972 renderComposeStatusOnly(); 1973 var insertedCount = 0; 1974 var errorCount = 0; 1975 var chain = Promise.resolve(); 1976 list.forEach(function (file, idx) { 1977 var entry = uploadEntries[idx]; 1978 chain = chain.then(function () { 1979 if (entry && entry.id) { 1980 updateComposeUploadEntry(entry.id, { status: 'uploading', progress: 2, error: '' }); 1981 renderComposeStatusOnly(); 1982 } 1983 return uploadComposeFile(file, { 1984 onProgress: function (loaded, total) { 1985 if (!entry || !entry.id) { 1986 return; 1987 } 1988 var pct = total > 0 ? (loaded / total) * 100 : 0; 1989 updateComposeUploadEntry(entry.id, { status: 'uploading', progress: Math.max(2, Math.min(98, pct)) }); 1990 renderComposeStatusOnly(); 1991 } 1992 }).then(function (data) { 1993 if (data && data.draft_id) { 1994 state.compose.draftId = String(data.draft_id); 1995 }
1996 var uploadUrl = String((data && data.url) || '').trim(); 1997 if (!uploadUrl) { 1998 var uploadedFileId = String((data && data.file_id) || '').trim(); 1999 var uploadedName = String((data && (data.filename || data.name)) || '').trim(); 2000 if (uploadedFileId) { 2001 uploadUrl = '/files/' + encodeURIComponent(uploadedFileId); 2002 if (uploadedName) { 2003 uploadUrl += '/' + encodeURIComponent(uploadedName); 2004 } 2005 } 2006 } 2007 if (!uploadUrl) { 2008 throw new Error('Upload succeeded but file URL is missing'); 2009 } 2010 var appendState = appendComposeContent(composeUploadMarkdown(uploadUrl, file)); 2011 if (appendState === 'inserted' || appendState === 'queued') { 2012 insertedCount += 1; 2013 } 2014 if (entry && entry.id) { 2015 updateComposeUploadEntry(entry.id, { status: 'done', progress: 100, error: '' }); 2016 renderComposeStatusOnly(); 2017 } 2018 }).catch(function (err) { 2019 errorCount += 1; 2020 if (entry && entry.id) { 2021 updateComposeUploadEntry(entry.id, { 2022 status: 'error', 2023 progress: 100, 2024 error: String((err && err.message) || 'Upload failed') 2025 }); 2026 renderComposeStatusOnly(); 2027 } 2028 }); 2029 }); 2030 }); 2031 return chain.then(function () { 2032 renderComposeUi(); 2033 if (insertedCount > 0) { 2034 flushComposeAutosaveNow(); 2035 } else { 2036 queueComposeAutosave(); 2037 } 2038 if (insertedCount > 0) { 2039 showTopToast('Upload complete. Added to body.', 'ok', 2600); 2040 } else if (!errorCount) { 2041 showTopToast('Upload complete.', 'ok', 2400); 2042 } 2043 if (errorCount > 0) { 2044 showTopToast('Upload failed for ' + String(errorCount) + ' file(s).', 'error', 3600); 2045 } 2046 scheduleComposeUploadCleanup(); 2047 }).finally(function () { 2048 state.compose.uploading = Math.max(0, state.compose.uploading - list.length); 2049 renderComposeStatusOnly(); 2050 }); 2051 } 2052 2053 function composeDroppedFiles(event) { 2054 var dt = event && event.dataTransfer; 2055 if (!dt || !dt.files) { 2056 return []; 2057 } 2058 return Array.from(dt.files).filter(function (file) { 2059 return file && file.size >= 0; 2060 }); 2061 } 2062 2063 function composeDragHasFiles(event) { 2064 var dt = event && event.dataTransfer; 2065 if (!dt) { 2066 return false; 2067 } 2068 if (dt.files && dt.files.length > 0) { 2069 return true; 2070 } 2071 var types = dt.types ? Array.from(dt.types) : []; 2072 return types.indexOf('Files') >= 0; 2073 } 2074 2075 function composeDropInComposeCard(target) { 2076 if (!state.compose.open || !els.composeSlot || !target || !(target instanceof Element)) { 2077 return false; 2078 } 2079 return !!target.closest('.blog-compose-card'); 2080 } 2081 2082 function clearComposeDropHover() { 2083 if (composeDropHoverTimer) { 2084 clearTimeout(composeDropHoverTimer); 2085 composeDropHoverTimer = null; 2086 } 2087 if (!els.composeSlot) { 2088 return; 2089 } 2090 var active = els.composeSlot.querySelector('.blog-compose-card.is-drop-hover'); 2091 if (active) { 2092 active.classList.remove('is-drop-hover'); 2093 } 2094 } 2095 2096 function setComposeDropHover(target) { 2097 if (!els.composeSlot || !target || !(target instanceof Element)) { 2098 return; 2099 } 2100 var card = target.closest('.blog-compose-card'); 2101 if (!card) { 2102 return; 2103 } 2104 var active = els.composeSlot.querySelector('.blog-compose-card.is-drop-hover'); 2105 if (active && active !== card) { 2106 active.classList.remove('is-drop-hover'); 2107 } 2108 card.classList.add('is-drop-hover'); 2109 if (composeDropHoverTimer) { 2110 clearTimeout(composeDropHoverTimer); 2111 } 2112 composeDropHoverTimer = setTimeout(function () { 2113 clearComposeDropHover(); 2114 }, 150); 2115 } 2116 2117 function composeCanAcceptDroppedFiles(postType) { 2118 if (state.compose.cameraFullscreen) { 2119 return false; 2120 } 2121 var type = normalizeComposePostType(postType); 2122 if (!type) { 2123 return true; 2124 } 2125 return type !== 'shortform' && type !== 'capture-media'; 2126 } 2127 2128 function composePreferredDropType(files, currentType) { 2129 var type = normalizeComposePostType(currentType); 2130 var list = Array.from(files || []); 2131 if (!list.length) { 2132 return ''; 2133 } 2134 var allAudio = list.every(function (file) { 2135 return String((file && file.type) || '').toLowerCase().indexOf('audio/') === 0; 2136 }); 2137 var allVisual = list.every(function (file) { 2138 var mime = String((file && file.type) || '').toLowerCase(); 2139 return mime.indexOf('image/') === 0 || mime.indexOf('video/') === 0; 2140 }); 2141 if (type) { 2142 if (type === 'upload-media' && !allVisual) { 2143 return 'attachment'; 2144 } 2145 if (type === 'audio-note' && !allAudio) { 2146 return 'attachment'; 2147 } 2148 if (type === 'shortform' || type === 'capture-media') { 2149 return ''; 2150 } 2151 return ''; 2152 } 2153 // When chooser is showing (no type selected), auto-pick based on dropped file kind: 2154 // image files -> image upload mode, everything else -> attachment mode. 2155 var allImages = list.every(function (file) { 2156 return String((file && file.type) || '').toLowerCase().indexOf('image/') === 0; 2157 }); 2158 if (allImages) { 2159 return 'upload-media'; 2160 } 2161 return 'attachment'; 2162 } 2163 2164 function composePickerFieldByType(type) { 2165 var picked = normalizeComposePostType(type); 2166 if (picked === 'capture-media') return 'capture-upload'; 2167 if (picked === 'upload-media') return 'media-upload'; 2168 if (picked === 'attachment') return 'file-upload'; 2169 if (picked === 'audio-note') return 'audio-upload'; 2170 return ''; 2171 } 2172 2173 function openComposePickerForType(type) { 2174 if (!els.composeSlot) { 2175 return false;
2176 } 2177 var field = composePickerFieldByType(type); 2178 if (!field) { 2179 return false; 2180 } 2181 var picker = els.composeSlot.querySelector('[data-compose-field="' + field + '"]'); 2182 if (!(picker instanceof HTMLInputElement)) { 2183 return false; 2184 } 2185 picker.click(); 2186 return true; 2187 } 2188 2189 function composeCameraOverlayHtml() { 2190 var status = state.compose.cameraStarting 2191 ? 'Requesting camera access...' 2192 : (state.compose.cameraError ? escapeHtml(state.compose.cameraError) : 'Frame your shot and capture.'); 2193 return '' + 2194 '<div class="blog-compose-camera-overlay-shell">' + 2195 '<div class="blog-compose-camera-overlay-stage">' + 2196 '<video class="blog-compose-camera-overlay-video" data-compose-camera-fullscreen-preview playsinline autoplay muted></video>' + 2197 '</div>' + 2198 '<div class="blog-compose-camera-overlay-controls">' + 2199 '<button type="button" class="unobtrusive-icon-button blog-compose-camera-control" data-compose-action="cancel-camera-capture">Cancel</button>' + 2200 '<button type="button" class="list-admin-primary-btn blog-compose-camera-capture" data-compose-action="capture-photo">Capture Photo</button>' + 2201 '</div>' + 2202 '<div class="blog-compose-camera-overlay-status">' + status + '</div>' + 2203 '</div>'; 2204 } 2205 2206 function renderComposeCameraOverlay() { 2207 if (!root) { 2208 return; 2209 } 2210 var shouldShow = !!(state.compose.open && state.compose.cameraFullscreen); 2211 var overlay = root.querySelector('.blog-compose-camera-overlay'); 2212 if (!shouldShow) { 2213 if (overlay && overlay.parentNode) { 2214 overlay.parentNode.removeChild(overlay); 2215 } 2216 if (document && document.body) { 2217 document.body.classList.remove('blog-camera-mode-active'); 2218 } 2219 return; 2220 } 2221 if (!overlay) { 2222 overlay = document.createElement('div'); 2223 overlay.className = 'blog-compose-camera-overlay'; 2224 root.appendChild(overlay); 2225 } 2226 overlay.innerHTML = composeCameraOverlayHtml(); 2227 if (document && document.body) { 2228 document.body.classList.add('blog-camera-mode-active'); 2229 } 2230 attachComposeCameraPreview(); 2231 } 2232 2233 function openComposeCameraCapture(options) { 2234 ensureComposeStateShape(); 2235 var opts = options || {}; 2236 state.compose.postType = 'capture-media'; 2237 state.compose.cameraFullscreen = true; 2238 state.compose.cameraReturnToChooser = !!opts.returnToChooser; 2239 if (opts.returnToChooser) { 2240 state.compose.postTypeChosen = false; 2241 state.compose.postTypeToolbarCollapsed = false; 2242 } 2243 renderComposeUi(); 2244 ensureComposeCameraStream(); 2245 attachComposeCameraPreview(); 2246 } 2247 2248 function closeComposeCameraCapture(options) { 2249 ensureComposeStateShape(); 2250 var opts = options || {}; 2251 var shouldReturnToChooser = !!opts.returnToChooser || !!state.compose.cameraReturnToChooser; 2252 state.compose.cameraFullscreen = false; 2253 state.compose.cameraReturnToChooser = false; 2254 stopComposeCameraStream(); 2255 if (shouldReturnToChooser) { 2256 state.compose.postTypeChosen = false; 2257 state.compose.postTypeToolbarCollapsed = false; 2258 } 2259 renderComposeUi(); 2260 } 2261 2262 function stopComposeCameraStream() { 2263 ensureComposeStateShape(); 2264 var stream = state.compose.cameraStream; 2265 if (stream && typeof stream.getTracks === 'function') { 2266 stream.getTracks().forEach(function (track) { 2267 if (track && typeof track.stop === 'function') { 2268 track.stop(); 2269 } 2270 }); 2271 } 2272 state.compose.cameraStream = null; 2273 state.compose.cameraStarting = false; 2274 } 2275 2276 function attachComposeCameraPreview() { 2277 if (!root) { 2278 return; 2279 } 2280 var previews = root.querySelectorAll('[data-compose-camera-preview], [data-compose-camera-fullscreen-preview]'); 2281 Array.prototype.forEach.call(previews, function (video) { 2282 if (!(video instanceof HTMLVideoElement)) { 2283 return; 2284 } 2285 if (state.compose.cameraStream && video.srcObject !== state.compose.cameraStream) { 2286 video.srcObject = state.compose.cameraStream; 2287 } 2288 if (video.srcObject) { 2289 video.play().catch(function () { 2290 // Ignore autoplay restrictions; controls are still available. 2291 }); 2292 } 2293 }); 2294 } 2295 2296 function ensureComposeCameraStream() { 2297 ensureComposeStateShape(); 2298 if (state.compose.cameraStream || state.compose.cameraStarting) { 2299 attachComposeCameraPreview(); 2300 return; 2301 } 2302 if (!navigator.mediaDevices || typeof navigator.mediaDevices.getUserMedia !== 'function') { 2303 state.compose.cameraError = 'Camera is unavailable in this browser.'; 2304 renderComposeUi(); 2305 return; 2306 } 2307 state.compose.cameraStarting = true; 2308 state.compose.cameraError = ''; 2309 renderComposeUi(); 2310 navigator.mediaDevices.getUserMedia({ video: { facingMode: { ideal: 'environment' } }, audio: false }) 2311 .catch(function () { 2312 return navigator.mediaDevices.getUserMedia({ video: true, audio: false }); 2313 }) 2314 .then(function (stream) { 2315 state.compose.cameraStream = stream || null; 2316 }) 2317 .catch(function (err) { 2318 state.compose.cameraError = String((err && err.message) || 'Could not access camera.'); 2319 }) 2320 .finally(function () { 2321 state.compose.cameraStarting = false;
2322 renderComposeUi(); 2323 }); 2324 } 2325 2326 function stopComposeAudioStream() { 2327 ensureComposeStateShape(); 2328 var recorder = state.compose.audioRecorder; 2329 if (recorder && recorder.state === 'recording') { 2330 recorder.onstop = null; 2331 recorder.onerror = null; 2332 try { 2333 recorder.stop(); 2334 } catch (_err) { 2335 // Ignore stop errors. 2336 } 2337 } 2338 var stream = state.compose.audioStream; 2339 if (stream && typeof stream.getTracks === 'function') { 2340 stream.getTracks().forEach(function (track) { 2341 if (track && typeof track.stop === 'function') { 2342 track.stop(); 2343 } 2344 }); 2345 } 2346 state.compose.audioStream = null; 2347 state.compose.audioRecorder = null; 2348 state.compose.audioChunks = []; 2349 state.compose.audioStarting = false; 2350 state.compose.audioRecording = false; 2351 } 2352 2353 function ensureComposeAudioStream() { 2354 ensureComposeStateShape(); 2355 if (state.compose.audioStream) { 2356 return Promise.resolve(state.compose.audioStream); 2357 } 2358 if (state.compose.audioStarting) { 2359 return Promise.resolve(null); 2360 } 2361 if (!navigator.mediaDevices || typeof navigator.mediaDevices.getUserMedia !== 'function') { 2362 state.compose.audioError = 'Microphone is unavailable in this browser.'; 2363 renderComposeUi(); 2364 return Promise.resolve(null); 2365 } 2366 state.compose.audioStarting = true; 2367 state.compose.audioError = ''; 2368 renderComposeUi(); 2369 return navigator.mediaDevices.getUserMedia({ audio: true, video: false }) 2370 .then(function (stream) { 2371 state.compose.audioStream = stream || null; 2372 return state.compose.audioStream; 2373 }) 2374 .catch(function (err) { 2375 state.compose.audioError = String((err && err.message) || 'Could not access microphone.'); 2376 return null; 2377 }) 2378 .finally(function () { 2379 state.compose.audioStarting = false; 2380 renderComposeUi(); 2381 }); 2382 } 2383 2384 function preferredComposeAudioMimeType() { 2385 if (typeof MediaRecorder === 'undefined' || typeof MediaRecorder.isTypeSupported !== 'function') { 2386 return ''; 2387 } 2388 var candidates = [ 2389 'audio/webm;codecs=opus', 2390 'audio/webm', 2391 'audio/ogg;codecs=opus', 2392 'audio/ogg', 2393 'audio/mp4' 2394 ]; 2395 for (var idx = 0; idx < candidates.length; idx += 1) { 2396 if (MediaRecorder.isTypeSupported(candidates[idx])) { 2397 return candidates[idx]; 2398 } 2399 } 2400 return ''; 2401 } 2402 2403 function startComposeAudioRecording() { 2404 ensureComposeStateShape(); 2405 if (state.compose.audioRecording || state.compose.audioStarting) { 2406 return; 2407 } 2408 if (typeof MediaRecorder === 'undefined') { 2409 state.compose.audioError = 'Audio recording is unavailable in this browser.'; 2410 renderComposeUi(); 2411 return; 2412 } 2413 ensureComposeAudioStream().then(function (stream) { 2414 if (!stream || state.compose.audioRecording) { 2415 return; 2416 } 2417 var recorder; 2418 var preferredMime = preferredComposeAudioMimeType(); 2419 try { 2420 recorder = preferredMime ? new MediaRecorder(stream, { mimeType: preferredMime }) : new MediaRecorder(stream); 2421 } catch (_err) { 2422 try { 2423 recorder = new MediaRecorder(stream); 2424 } catch (fallbackErr) { 2425 state.compose.audioError = String((fallbackErr && fallbackErr.message) || 'Could not start recording.'); 2426 renderComposeUi(); 2427 return; 2428 } 2429 } 2430 state.compose.audioChunks = []; 2431 state.compose.audioRecorder = recorder; 2432 state.compose.audioError = ''; 2433 recorder.ondataavailable = function (event) { 2434 if (event && event.data && event.data.size > 0) { 2435 state.compose.audioChunks.push(event.data); 2436 } 2437 }; 2438 recorder.onerror = function (event) { 2439 state.compose.audioError = String((event && event.error && event.error.message) || 'Recording failed.'); 2440 }; 2441 recorder.onstop = function () { 2442 var chunks = Array.isArray(state.compose.audioChunks) ? state.compose.audioChunks.slice() : []; 2443 state.compose.audioChunks = []; 2444 state.compose.audioRecorder = null; 2445 state.compose.audioRecording = false;
2446 if (!chunks.length) { 2447 renderComposeUi(); 2448 return; 2449 } 2450 var mime = (chunks[0] && chunks[0].type) ? String(chunks[0].type) : String(recorder.mimeType || preferredMime || 'audio/webm'); 2451 var blob = new Blob(chunks, { type: mime }); 2452 var ext = mime.indexOf('ogg') >= 0 ? 'ogg' : (mime.indexOf('mp4') >= 0 ? 'm4a' : 'webm'); 2453 var file = new File([blob], 'voice-note-' + Date.now() + '.' + ext, { type: mime }); 2454 handleComposeUploads([file], 'audio-note'); 2455 renderComposeUi(); 2456 }; 2457 try { 2458 recorder.start(250); 2459 } catch (err) { 2460 try { 2461 recorder.start(); 2462 } catch (fallbackStartErr) { 2463 state.compose.audioRecorder = null; 2464 state.compose.audioRecording = false; 2465 state.compose.audioError = String((fallbackStartErr && fallbackStartErr.message) || 'Could not start recording.'); 2466 renderComposeUi(); 2467 return; 2468 } 2469 } 2470 state.compose.audioRecording = true; 2471 state.compose.audioError = ''; 2472 renderComposeUi(); 2473 }); 2474 } 2475 2476 function stopComposeAudioRecording() { 2477 ensureComposeStateShape(); 2478 var recorder = state.compose.audioRecorder; 2479 if (!recorder || recorder.state !== 'recording') { 2480 return; 2481 } 2482 try { 2483 recorder.stop(); 2484 } catch (_err) { 2485 state.compose.audioRecording = false; 2486 state.compose.audioRecorder = null; 2487 renderComposeUi(); 2488 } 2489 } 2490 2491 function capturePhotoFromComposeCamera() { 2492 if (!root) { 2493 return; 2494 } 2495 var video = root.querySelector('[data-compose-camera-fullscreen-preview]') || root.querySelector('[data-compose-camera-preview]'); 2496 if (!(video instanceof HTMLVideoElement)) { 2497 setComposeOutput('Camera preview is not ready yet.', 'warn'); 2498 renderComposeStatusOnly(); 2499 return; 2500 } 2501 var width = video.videoWidth; 2502 var height = video.videoHeight; 2503 if (!width || !height) { 2504 setComposeOutput('Camera preview is still starting. Try again in a moment.', 'warn'); 2505 renderComposeStatusOnly(); 2506 return; 2507 } 2508 var canvas = document.createElement('canvas'); 2509 canvas.width = width; 2510 canvas.height = height; 2511 var ctx = canvas.getContext('2d'); 2512 if (!ctx) { 2513 setComposeOutput('Could not capture from camera.', 'error'); 2514 renderComposeStatusOnly(); 2515 return; 2516 } 2517 ctx.drawImage(video, 0, 0, width, height); 2518 canvas.toBlob(function (blob) { 2519 if (!blob) { 2520 setComposeOutput('Could not capture photo.', 'error'); 2521 renderComposeStatusOnly(); 2522 return; 2523 } 2524 var file = new File([blob], 'capture-' + Date.now() + '.jpg', { type: 'image/jpeg' }); 2525 state.compose.cameraFullscreen = false; 2526 state.compose.cameraReturnToChooser = false; 2527 state.compose.postType = 'capture-media'; 2528 state.compose.postTypeChosen = true; 2529 state.compose.preview = false; 2530 stopComposeCameraStream(); 2531 renderComposeUi(); 2532 handleComposeUploads([file], 'capture-media'); 2533 }, 'image/jpeg', 0.92); 2534 } 2535 2536 function isEditableTarget(target) { 2537 if (!target || !(target instanceof Element)) { 2538 return false; 2539 } 2540 if (target instanceof HTMLTextAreaElement || target instanceof HTMLInputElement) { 2541 return true; 2542 } 2543 if (target.isContentEditable) { 2544 return true; 2545 } 2546 return !!target.closest('textarea, input, [contenteditable="true"], [contenteditable=""]'); 2547 } 2548 2549 function clipboardImageFiles(event) { 2550 var out = []; 2551 var clip = event && event.clipboardData; 2552 if (!clip) { 2553 return out; 2554 } 2555 var items = clip.items ? Array.from(clip.items) : []; 2556 items.forEach(function (item) { 2557 if (!item || item.kind !== 'file' || String(item.type || '').indexOf('image/') !== 0) { 2558 return; 2559 } 2560 var file = item.getAsFile ? item.getAsFile() : null; 2561 if (file) { 2562 out.push(file); 2563 } 2564 }); 2565 if (out.length) { 2566 return out; 2567 } 2568 var files = clip.files ? Array.from(clip.files) : []; 2569 return files.filter(function (file) { 2570 return file && String(file.type || '').indexOf('image/') === 0; 2571 }); 2572 } 2573 2574 function composeLocalToIso(value) { 2575 if (!value) { 2576 return ''; 2577 } 2578 var dt = new Date(value); 2579 if (isNaN(dt.getTime())) { 2580 return ''; 2581 } 2582 return dt.toISOString().replace('.000Z', 'Z'); 2583 } 2584 2585 function composeNormalizeScheduledDate(value) { 2586 var raw = String(value || '').trim(); 2587 if (!raw) { 2588 return ''; 2589 } 2590 if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) { 2591 return raw; 2592 } 2593 if (/^\d{4}-\d{2}-\d{2}T/.test(raw)) { 2594 return raw.slice(0, 10); 2595 } 2596 var dt = new Date(raw); 2597 if (isNaN(dt.getTime())) { 2598 return ''; 2599 } 2600 var local = new Date(dt.getTime() - dt.getTimezoneOffset() * 60000); 2601 return local.toISOString().slice(0, 10); 2602 } 2603 2604 function composePublishMode() { 2605 if (!els.composeSlot) { 2606 return 'immediate'; 2607 } 2608 var checked = els.composeSlot.querySelector('input[name="blog-inline-compose-mode"]:checked'); 2609 var value = checked ? String(checked.value || '') : 'immediate'; 2610 if (value === 'scheduled' || value === 'drip' || value === 'immediate') { 2611 return value; 2612 } 2613 return 'immediate'; 2614 } 2615 2616 function normalizeComposePublishDestination(raw) { 2617 var picked = String(raw || '').trim().toLowerCase(); 2618 if (picked === 'nostr_now') { 2619 return 'nostr_now'; 2620 } 2621 return 'local_only'; 2622 } 2623 2624 function composePublishDestination() { 2625 ensureComposeStateShape(); 2626 if (!els.composeSlot) { 2627 return normalizeComposePublishDestination(state.compose.publishDestination); 2628 } 2629 var checked = els.composeSlot.querySelector('input[name="blog-inline-compose-destination"]:checked'); 2630 var value = checked ? String(checked.value || '') : String(state.compose.publishDestination || ''); 2631 return normalizeComposePublishDestination(value); 2632 } 2633 2634 function composePrimaryLabel(mode, destination) { 2635 var shared = (typeof window !== 'undefined' && window.BlogComposeShared) ? window.BlogComposeShared : null; 2636 if (shared && typeof shared.primaryPublishLabel === 'function') { 2637 return shared.primaryPublishLabel(mode, destination, { postTypeLocked: !!state.compose.postTypeLocked }); 2638 } 2639 if (state.compose.postTypeLocked) { 2640 return 'Publish Changes'; 2641 } 2642 if (mode === 'scheduled') { 2643 return 'Schedule Post'; 2644 } 2645 if (mode === 'drip') { 2646 return 'Enqueue Post'; 2647 } 2648 if (normalizeComposePublishDestination(destination) === 'local_only') { 2649 return 'Publish to Server'; 2650 } 2651 return 'Publish to Nostr'; 2652 } 2653 2654 function composePublishDestinationFieldHtml(destination) { 2655 var shared = (typeof window !== 'undefined' && window.BlogComposeShared) ? window.BlogComposeShared : null; 2656 if (shared && typeof shared.renderPublishDestinationField === 'function') { 2657 return shared.renderPublishDestinationField({ 2658 inputName: 'blog-inline-compose-destination', 2659 destination: destination 2660 }); 2661 } 2662 return '' + 2663 '<strong>Publish to</strong>' + 2664 '<div class="mode-row">' + 2665 '<label><input type="radio" name="blog-inline-compose-destination" value="local_only"' + (destination === 'local_only' ? ' checked' : '') + '> Server only</label>' + 2666 '<label><input type="radio" name="blog-inline-compose-destination" value="nostr_now"' + (destination === 'nostr_now' ? ' checked' : '') + '> Server + Nostr</label>' + 2667 '</div>'; 2668 } 2669 2670 function composeModeAction(mode, destination) { 2671 if (state.compose.postTypeLocked) { 2672 return 'publish_now'; 2673 } 2674 if (mode === 'scheduled') { 2675 return 'queue_scheduled'; 2676 } 2677 if (mode === 'drip') { 2678 return 'queue_drip'; 2679 } 2680 return 'publish_now'; 2681 } 2682 2683 function openComposeScheduledPicker() { 2684 if (!els.composeSlot || !state.compose.open) { 2685 return; 2686 } 2687 if (composePublishMode() !== 'scheduled') { 2688 return; 2689 } 2690 var scheduleInput = els.composeSlot.querySelector('[data-compose-field="scheduled-at"]'); 2691 if (!(scheduleInput instanceof HTMLInputElement)) { 2692 return; 2693 } 2694 var releaseRow = scheduleInput.closest('.compose-release-row'); 2695 var scheduleOption = els.composeSlot.querySelector('input[name="blog-inline-compose-mode"][value="scheduled"]'); 2696 var scheduleAnchor = scheduleOption instanceof HTMLInputElement 2697 ? (scheduleOption.closest('label') || scheduleOption) 2698 : null; 2699 if (releaseRow && scheduleAnchor instanceof Element) { 2700 var rowRect = releaseRow.getBoundingClientRect(); 2701 var anchorRect = scheduleAnchor.getBoundingClientRect(); 2702 var left = Math.max(0, Math.round(anchorRect.left - rowRect.left)); 2703 var top = Math.max(0, Math.round(anchorRect.bottom - rowRect.top + 3)); 2704 scheduleInput.style.left = String(left) + 'px'; 2705 scheduleInput.style.top = String(top) + 'px'; 2706 } 2707 try { 2708 if (typeof scheduleInput.showPicker === 'function') { 2709 scheduleInput.showPicker(); 2710 } else { 2711 scheduleInput.focus(); 2712 scheduleInput.click(); 2713 } 2714 } catch (_err) {
2715 try { 2716 scheduleInput.focus(); 2717 } catch (_focusErr) { 2718 // Ignore picker focus errors. 2719 } 2720 } 2721 } 2722 2723 function composeScheduledDisplayValue(rawValue) { 2724 var value = composeNormalizeScheduledDate(rawValue); 2725 if (!value) { 2726 return 'No schedule selected'; 2727 } 2728 return value; 2729 } 2730 2731 function composeToolbarButtonHtml(action, label, icon) { 2732 return '<button type="button" class="unobtrusive-icon-button toolbar-button" data-compose-toolbar="' + escapeHtml(action) + '" aria-label="' + escapeHtml(label) + '" title="' + escapeHtml(label) + '">' + icon + '</button>'; 2733 } 2734 2735 function composeToolbarHtml() { 2736 return '' + 2737 '<div class="toolbar blog-compose-toolbar" aria-label="Markdown toolbar">' + 2738 composeToolbarButtonHtml('bold', 'Bold', '<span class="tb-glyph tb-glyph-bold" aria-hidden="true">B</span>') + 2739 composeToolbarButtonHtml('italic', 'Italic', '<span class="tb-glyph tb-glyph-italic" aria-hidden="true">I</span>') + 2740 composeToolbarButtonHtml('h2', 'Heading 2', '<span class="tb-glyph tb-glyph-heading" aria-hidden="true">H2</span>') + 2741 composeToolbarButtonHtml('h3', 'Heading 3', '<span class="tb-glyph tb-glyph-heading" aria-hidden="true">H3</span>') + 2742 composeToolbarButtonHtml('code', 'Inline code', '<span class="tb-glyph tb-glyph-code" aria-hidden="true"></></span>') + 2743 composeToolbarButtonHtml('code_block', 'Code block', '<span class="tb-glyph tb-glyph-code" aria-hidden="true">{ }</span>') + 2744 composeToolbarButtonHtml('link', 'Insert link', '<svg class="tb-icon" viewBox="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="1.8" 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="1.8" stroke-linecap="round"/><path d="M9 12H15" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>') + 2745 composeToolbarButtonHtml('quote', 'Quote', '<span class="tb-glyph tb-glyph-quote" aria-hidden="true">ââ</span>') + 2746 composeToolbarButtonHtml('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>') + 2747 composeToolbarButtonHtml('ol', 'Numbered list', '<span class="tb-glyph tb-glyph-list" aria-hidden="true">1.</span>') + 2748 composeToolbarButtonHtml('image', 'Insert image', '<svg class="tb-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true"><rect x="3.6" y="5.1" width="16.8" height="13.8" rx="2.1" stroke="currentColor" stroke-width="1.8"/><circle cx="9.2" cy="10.2" r="1.2" fill="currentColor"/><path d="M6.2 16.1L10.7 11.7L13.2 14.2L16.1 11.5L17.8 13.2" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>') + 2749 composeToolbarButtonHtml('attachment', 'Attach file', '<svg class="tb-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M9.2 12.8L14.4 7.6C15.8 6.2 18 6.2 19.4 7.6C20.8 9 20.8 11.2 19.4 12.6L11.2 20.8C8.9 23.1 5.2 23.1 2.9 20.8C0.6 18.5 0.6 14.8 2.9 12.5L11 4.4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>') + 2750 composeToolbarButtonHtml('audio_record', 'Record audio', '<svg class="tb-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true"><circle cx="12" cy="12" r="6.2" stroke="currentColor" stroke-width="1.9"/><circle cx="12" cy="12" r="2.5" fill="currentColor"/></svg>') + 2751 '</div>'; 2752 } 2753 2754 function composeModePanelHtml(postType, fields) { 2755 var type = normalizeComposePostType(postType); 2756 if (type === 'capture-media') { 2757 return '' + 2758 '<div class="compose-media-tools compose-mode-panel compose-camera-panel">' + 2759 '<div class="compose-media-actions">' + 2760 '<button type="button" class="list-admin-primary-btn compose-media-btn compose-media-btn-primary" data-compose-action="open-camera-capture">Open Camera</button>' + 2761 '<button type="button" class="unobtrusive-icon-button compose-media-btn" data-compose-action="open-mode-picker" data-compose-mode-target="capture-media">Use Camera App / Upload</button>' + 2762 '</div>' + 2763 '<div class="compose-camera-status">' + 2764 (state.compose.cameraError ? escapeHtml(state.compose.cameraError) : 'Open camera for fullscreen capture, or upload from device.') + 2765 '</div>' + 2766 '</div>'; 2767 } 2768 if (type === 'upload-media') { 2769 return '' + 2770 '<div class="compose-media-tools compose-mode-panel">' + 2771 '<div class="compose-media-actions">' + 2772 '<button type="button" class="list-admin-primary-btn compose-media-btn" data-compose-action="open-mode-picker" data-compose-mode-target="upload-media">Upload Photo/Video</button>' + 2773 '</div>' + 2774 '</div>'; 2775 } 2776 if (type === 'attachment') { 2777 return '' + 2778 '<div class="compose-media-tools compose-mode-panel">' + 2779 '<div class="compose-media-actions">' + 2780 '<button type="button" class="list-admin-primary-btn compose-media-btn" data-compose-action="open-mode-picker" data-compose-mode-target="attachment">Upload Attachment/File</button>' + 2781 '</div>' + 2782 '</div>'; 2783 } 2784 if (type === 'audio-note') { 2785 return '' + 2786 '<div class="compose-media-tools compose-mode-panel">' + 2787 '<div class="compose-media-actions">' + 2788 '<button type="button" class="list-admin-primary-btn compose-media-btn" data-compose-action="audio-record-toggle">' + (state.compose.audioRecording ? 'Stop Recording' : 'Start Recording') + '</button>' + 2789 '<button type="button" class="unobtrusive-icon-button compose-media-btn" data-compose-action="open-mode-picker" data-compose-mode-target="audio-note">Upload Audio</button>' + 2790 '</div>' + 2791 '<div class="compose-audio-status">' + escapeHtml(state.compose.audioError || (state.compose.audioRecording ? 'Recording audio...' : 'Press Start Recording to request microphone access.')) + '</div>' + 2792 '</div>'; 2793 } 2794 if (type === 'link-share') { 2795 return '' + 2796 '<div class="compose-media-tools compose-mode-panel">' + 2797 '<div class="compose-link-fields">' + 2798 '<label><strong>Link URL</strong></label>' + 2799 '<input type="url" data-compose-field="link-url" placeholder="https://example.com" value="' + escapeHtml(fields.linkUrl) + '">' + 2800 '</div>' + 2801 '</div>'; 2802 } 2803 return ''; 2804 } 2805 2806 function applyComposeModeEffects(postType) { 2807 var type = normalizeComposePostType(postType); 2808 root.classList.remove('blog-camera-mode'); 2809 if (!state.compose.open || !state.compose.cameraFullscreen) { 2810 stopComposeCameraStream(); 2811 } else { 2812 ensureComposeCameraStream(); 2813 attachComposeCameraPreview(); 2814 } 2815 if (!state.compose.open || type !== 'audio-note') { 2816 stopComposeAudioStream(); 2817 } 2818 } 2819 2820 function renderComposePreviewHtml(title, content) { 2821 var body = String(content || '').trim(); 2822 var heading = String(title || '').trim(); 2823 if (!body && !heading) { 2824 return '<p class="placeholder">Preview will appear here...</p>'; 2825 } 2826 var html = ''; 2827 if (heading) { 2828 html += '<h2>' + escapeHtml(heading) + '</h2>'; 2829 } 2830 if (body) { 2831 html += markdownBlock(body); 2832 } 2833 return html; 2834 } 2835 2836 function composeTrashIconSvg() { 2837 return '<svg class="trash-icon-svg" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
2837<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7h16m-10 4v6m4-6v6M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2l1-12M9 7V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v3"/></svg>'; 2838 } 2839 2840 function readComposeFields() { 2841 if (!els.composeSlot) { 2842 return null; 2843 } 2844 var title = els.composeSlot.querySelector('[data-compose-field="title"]'); 2845 var content = els.composeSlot.querySelector('[data-compose-field="content"]'); 2846 var scheduled = els.composeSlot.querySelector('[data-compose-field="scheduled-at"]'); 2847 var tags = els.composeSlot.querySelector('[data-compose-field="tags"]'); 2848 var linkUrl = els.composeSlot.querySelector('[data-compose-field="link-url"]'); 2849 return { 2850 title: title instanceof HTMLInputElement ? String(title.value || '') : '', 2851 content: content instanceof HTMLTextAreaElement ? String(content.value || '') : '', 2852 scheduledAt: scheduled instanceof HTMLInputElement ? composeNormalizeScheduledDate(String(scheduled.value || '')) : '', 2853 tags: tags instanceof HTMLInputElement ? String(tags.value || '') : '', 2854 linkUrl: linkUrl instanceof HTMLInputElement ? String(linkUrl.value || '') : String(state.compose.linkUrl || ''), 2855 postType: composePostType() 2856 }; 2857 } 2858 2859 function composePayload(action) { 2860 commitComposeTagInput(); 2861 syncComposeTagsField(); 2862 var fields = readComposeFields(); 2863 if (!fields) { 2864 return null; 2865 } 2866 var payloadContent = fields.content; 2867 if (fields.postType === 'link-share') { 2868 var linkMd = composeBuildLinkMarkdown(fields.linkUrl, fields.content, fields.title); 2869 if (linkMd) { 2870 payloadContent = linkMd; 2871 } 2872 state.compose.linkUrl = String(fields.linkUrl || ''); 2873 } 2874 return { 2875 action: action, 2876 draft_id: String(state.compose.draftId || ''), 2877 source_post_path: String(state.compose.sourcePostPath || ''), 2878 title: fields.postType === 'shortform' ? '' : fields.title.trim(), 2879 tags: fields.tags.trim(), 2880 summary: '', 2881 content: payloadContent, 2882 post_type: fields.postType, 2883 scheduled_at: composeLocalToIso(fields.scheduledAt), 2884 publish_mode: composePublishMode(), 2885 publish_destination: composePublishDestination() 2886 }; 2887 } 2888 2889 function setComposeOutput(message, tone) { 2890 var text = String(message || '').trim(); 2891 var safeTone = String(tone || '').trim().toLowerCase(); 2892 if (safeTone === 'error' || safeTone === 'warn') { 2893 if (text) { 2894 showTopToast(text, safeTone === 'error' ? 'error' : 'warn', safeTone === 'error' ? 3800 : 3400); 2895 } 2896 state.compose.output = ''; 2897 state.compose.outputTone = ''; 2898 return; 2899 } 2900 state.compose.output = text; 2901 state.compose.outputTone = safeTone; 2902 } 2903 2904 function captureComposeFieldFocus() { 2905 if (!els.composeSlot) { 2906 return null; 2907 } 2908 var active = document.activeElement; 2909 var isTextControl = (active instanceof HTMLInputElement) || (active instanceof HTMLTextAreaElement); 2910 var isTagEditor = (active instanceof HTMLElement) && String(active.getAttribute('data-compose-field') || '')
2910=== 'tags-editor'; 2911 if (!isTextControl && !isTagEditor) { 2912 return null; 2913 } 2914 if (!els.composeSlot.contains(active)) { 2915 return null; 2916 } 2917 var field = String(active.getAttribute('data-compose-field') || '').trim(); 2918 if (!field) { 2919 return null; 2920 } 2921 var snapshot = { 2922 field: field, 2923 scrollTop: typeof active.scrollTop === 'number' ? active.scrollTop : 0, 2924 scrollLeft: typeof active.scrollLeft === 'number' ? active.scrollLeft : 0 2925 }; 2926 if (isTextControl) { 2927 snapshot.selectionStart = typeof active.selectionStart === 'number' ? active.selectionStart : null; 2928 snapshot.selectionEnd = typeof active.selectionEnd === 'number' ? active.selectionEnd : null; 2929 snapshot.selectionDirection = typeof active.selectionDirection === 'string' ? active.selectionDirection : 'none'; 2930 return snapshot; 2931 } 2932 snapshot.tagsDraftText = String(state.compose.tagsDraftText || ''); 2933 return snapshot; 2934 } 2935 2936 function restoreComposeFieldFocus(snapshot) { 2937 if (!snapshot || !els.composeSlot) { 2938 return; 2939 } 2940 var selector = '[data-compose-field="' + String(snapshot.field || '').replace(/"/g, '\\"') + '"]'; 2941 var target = els.composeSlot.querySelector(selector); 2942 if ((target instanceof HTMLElement) && String(snapshot.field || '') === 'tags-editor') { 2943 state.compose.tagsDraftText = typeof snapshot.tagsDraftText === 'string' ? snapshot.tagsDraftText : String(state.compose.tagsDraftText || ''); 2944 composeTagsEditorRender(target); 2945 composeTagsEditorFocusDraft(target); 2946 return; 2947 } 2948 if (!(target instanceof HTMLInputElement) && !(target instanceof HTMLTextAreaElement)) { 2949 return; 2950 } 2951 try { 2952 target.focus({ preventScroll: true }); 2953 } catch (_focusErr) { 2954 target.focus(); 2955 } 2956 if (typeof snapshot.selectionStart === 'number' && typeof snapshot.selectionEnd === 'number' && typeof target.setSelectionRange === 'function') { 2957 try { 2958 target.setSelectionRange(snapshot.selectionStart, snapshot.selectionEnd, snapshot.selectionDirection || 'none'); 2959 } catch (_selectionErr) { 2960 // Ignore selection restore errors. 2961 } 2962 } 2963 try { 2964 target.scrollTop = Number(snapshot.scrollTop) || 0; 2965 target.scrollLeft = Number(snapshot.scrollLeft) || 0; 2966 } catch (_scrollErr) { 2967 // Ignore scroll restore errors. 2968 } 2969 } 2970 2971 function renderComposeUiPreserveFieldFocus() { 2972 var focusSnapshot = captureComposeFieldFocus(); 2973 renderComposeUi(); 2974 if (focusSnapshot) { 2975 restoreComposeFieldFocus(focusSnapshot); 2976 } 2977 } 2978 2979 function toastHost() { 2980 var host = document.getElementById('nav-top-toast-host'); 2981 if (host) { 2982 return host; 2983 } 2984 if (!document || !document.body) { 2985 return null; 2986 } 2987 host = document.createElement('div'); 2988 host.id = 'nav-top-toast-host'; 2989 host.className = 'nav-top-toast-host'; 2990 document.body.appendChild(host); 2991 return host; 2992 } 2993 2994 function showTopToast(message, tone, ttlMs) { 2995 var text = String(message || '').trim(); 2996 if (!text) { 2997 return; 2998 } 2999 var host = toastHost(); 3000 if (!host) { 3001 return; 3002 } 3003 var kind = String(tone || '').trim().toLowerCase(); 3004 var safeTone = (kind === 'ok' || kind === 'warn' || kind === 'error') ? kind : ''; 3005 var toast = document.createElement('div'); 3006 toast.className = 'nav-top-toast' + (safeTone ? (' is-' + safeTone) : ''); 3007 toast.textContent = text; 3008 host.appendChild(toast); 3009 requestAnimationFrame(function () { 3010 toast.classList.add('is-visible'); 3011 }); 3012 var stayMs = Math.max(1800, Number(ttlMs) || 3200); 3013 window.setTimeout(function () { 3014 toast.classList.remove('is-visible'); 3015 toast.classList.add('is-closing'); 3016 window.setTimeout(function () { 3017 if (toast.parentNode) { 3018 toast.parentNode.removeChild(toast); 3019 } 3020 }, 240); 3021 }, stayMs); 3022 } 3023 3024 function queueComposeAutosave() { 3025 if (!isAdmin() || !state.compose.open) { 3026 return; 3027 } 3028 if (state.compose.autosaveTimer) { 3029 clearTimeout(state.compose.autosaveTimer); 3030 } 3031 state.compose.saveStatus = 'saving'; 3032 renderComposeStatusOnly(); 3033 state.compose.autosaveTimer = setTimeout(function () { 3034 state.compose.autosaveTimer = null; 3035 autosaveCompose(); 3036 }, 500); 3037 } 3038 3039 function flushComposeAutosaveNow() { 3040 if (!isAdmin() || !state.compose.open) { 3041 return; 3042 } 3043 if (state.compose.autosaveTimer) { 3044 clearTimeout(state.compose.autosaveTimer); 3045 state.compose.autosaveTimer = null; 3046 } 3047 autosaveCompose(); 3048 } 3049 3050 function afterComposePublishSuccess() { 3051 state.compose.draftId = '';
3052 state.compose.sourcePostPath = ''; 3053 state.compose.postTypeLocked = false; 3054 state.compose.postTypeChosen = false; 3055 state.compose.saveStatus = ''; 3056 state.compose.uploading = 0; 3057 state.compose.postType = 'longform'; 3058 state.compose.tagsOpen = false; 3059 state.compose.tagsDraftText = ''; 3060 state.compose.publishDestination = 'local_only'; 3061 state.compose.shortformLimitEditing = false; 3062 state.compose.linkUrl = ''; 3063 state.compose.pendingContentAdditions = []; 3064 clearComposeUploadItems(); 3065 stopComposeCameraStream(); 3066 stopComposeAudioStream(); 3067 setComposeTags([]); 3068 var fields = readComposeFields(); 3069 if (!fields || !els.composeSlot) { 3070 return; 3071 } 3072 var title = els.composeSlot.querySelector('[data-compose-field="title"]'); 3073 var content = els.composeSlot.querySelector('[data-compose-field="content"]'); 3074 var schedule = els.composeSlot.querySelector('[data-compose-field="scheduled-at"]'); 3075 var immediateMode = els.composeSlot.querySelector('input[name="blog-inline-compose-mode"][value="immediate"]'); 3076 if (title instanceof HTMLInputElement) { 3077 title.value = ''; 3078 } 3079 if (content instanceof HTMLTextAreaElement) { 3080 content.value = ''; 3081 } 3082 if (schedule instanceof HTMLInputElement) { 3083 schedule.value = ''; 3084 } 3085 if (immediateMode instanceof HTMLInputElement) { 3086 immediateMode.checked = true; 3087 } 3088 var localOnlyTarget = els.composeSlot.querySelector('input[name="blog-inline-compose-destination"][value="local_only"]'); 3089 if (localOnlyTarget instanceof HTMLInputElement) { 3090 localOnlyTarget.checked = true; 3091 } 3092 } 3093 3094 function saveCompose(action) { 3095 if (state.compose.busy || !isAdmin()) { 3096 return; 3097 } 3098 var auth = window.__wizardryAuthPayload(); 3099 if (!auth.session_token || !auth.csrf_token) { 3100 setComposeOutput('Sign in again to compose posts.', 'error'); 3101 renderComposeStatusOnly(); 3102 return; 3103 } 3104 var payload = composePayload(action); 3105 if (!payload) { 3106 return; 3107 } 3108 if (action === 'publish_now' && !payload.content.trim()) { 3109 setComposeOutput('Cannot publish an empty post.', 'warn'); 3110 renderComposeStatusOnly(); 3111 return; 3112 } 3113 if (action === 'queue_scheduled' && !payload.scheduled_at) { 3114 setComposeOutput('Scheduled posts need a release date/time.', 'warn'); 3115 renderComposeStatusOnly(); 3116 return; 3117 } 3118 payload.session_token = auth.session_token; 3119 payload.csrf_token = auth.csrf_token; 3120 state.compose.busy = true; 3121 state.compose.saveStatus = action === 'autosave' ? 'saving' : ''; 3122 renderComposeStatusOnly(); 3123 apiPost('/cgi/blog-save-post', payload).then(function (data) { 3124 if (data && data.draft_id) { 3125 state.compose.draftId = String(data.draft_id); 3126 } 3127 if (action === 'autosave') { 3128 state.compose.saveStatus = 'saved'; 3129 renderComposeStatusOnly(); 3130 return; 3131 } 3132 if (action === 'publish_now') { 3133 setComposeOutput('', ''); 3134 showTopToast('Published. Rebuild may take a few seconds.', 'ok', 2600); 3135 afterComposePublishSuccess(); 3136 toggleComposeFromUi(false); 3137 } else { 3138 setComposeOutput((data && data.message) || 'Saved.', 'ok'); 3139 } 3140 renderComposeUi(); 3141 loadPageState(); 3142 loadDraftNoticeData(); 3143 setTimeout(function () { loadPageState(); }, 2500); 3144 }).catch(function (err) { 3145 var message = err && err.message ? err.message : 'Save failed'; 3146 if (action === 'autosave') { 3147 state.compose.saveStatus = ''; 3148 renderComposeStatusOnly(); 3149 return; 3150 } 3151 setComposeOutput('Error: ' + message, 'error'); 3152 renderComposeStatusOnly(); 3153 }).finally(function () { 3154 state.compose.busy = false; 3155 renderComposeStatusOnly(); 3156 }); 3157 } 3158 3159 function clearComposeFields() { 3160 state.compose.draftId = ''; 3161 state.compose.sourcePostPath = ''; 3162 state.compose.postTypeLocked = false; 3163 state.compose.postTypeChosen = false; 3164 state.compose.saveStatus = ''; 3165 state.compose.uploading = 0; 3166 state.compose.postType = 'longform'; 3167 state.compose.tagsOpen = false;
3168 state.compose.tagsDraftText = ''; 3169 state.compose.publishDestination = 'local_only'; 3170 state.compose.shortformLimitEditing = false; 3171 state.compose.linkUrl = ''; 3172 clearComposeUploadItems(); 3173 stopComposeCameraStream(); 3174 stopComposeAudioStream(); 3175 setComposeOutput('', ''); 3176 seedComposeDefaultTag(true); 3177 if (!els.composeSlot) { 3178 renderComposeStatusOnly(); 3179 return; 3180 } 3181 var title = els.composeSlot.querySelector('[data-compose-field="title"]'); 3182 var content = els.composeSlot.querySelector('[data-compose-field="content"]'); 3183 var schedule = els.composeSlot.querySelector('[data-compose-field="scheduled-at"]'); 3184 var immediateMode = els.composeSlot.querySelector('input[name="blog-inline-compose-mode"][value="immediate"]'); 3185 if (title instanceof HTMLInputElement) { 3186 title.value = ''; 3187 } 3188 if (content instanceof HTMLTextAreaElement) { 3189 content.value = ''; 3190 } 3191 if (schedule instanceof HTMLInputElement) { 3192 schedule.value = ''; 3193 } 3194 if (immediateMode instanceof HTMLInputElement) { 3195 immediateMode.checked = true; 3196 } 3197 var localOnlyTarget = els.composeSlot.querySelector('input[name="blog-inline-compose-destination"][value="local_only"]'); 3198 if (localOnlyTarget instanceof HTMLInputElement) { 3199 localOnlyTarget.checked = true; 3200 } 3201 renderComposeUi(); 3202 } 3203 3204 function deleteComposeDraft() { 3205 var fields = readComposeFields() || { title: '', content: '' }; 3206 var hasTitle = composePostType() === 'shortform' ? false : !!String(fields.title || '').trim(); 3207 var hasContent = hasTitle || 3208 !!String(fields.content || '').trim() || 3209 !!String(fields.linkUrl || '').trim() || 3210 state.compose.tags.length > 0 || 3211 !!state.compose.draftId; 3212 if (hasContent && !window.confirm('Delete this draft?')) { 3213 return; 3214 } 3215 if (!state.compose.draftId) { 3216 clearComposeFields(); 3217 return; 3218 } 3219 var auth = window.__wizardryAuthPayload(); 3220 if (!auth.session_token || !auth.csrf_token) { 3221 setComposeOutput('Sign in again to delete draft.', 'error'); 3222 renderComposeStatusOnly(); 3223 return; 3224 } 3225 state.compose.busy = true; 3226 renderComposeStatusOnly(); 3227 apiPost('/cgi/blog-delete-draft', { 3228 draft_id: String(state.compose.draftId || ''), 3229 session_token: auth.session_token, 3230 csrf_token: auth.csrf_token 3231 }).then(function () { 3232 clearComposeFields(); 3233 setComposeOutput('Draft deleted.', 'ok'); 3234 renderComposeStatusOnly(); 3235 loadDraftNoticeData(); 3236 }).catch(function (err) { 3237 setComposeOutput('Error: ' + ((err && err.message) ? err.message : 'Delete failed'), 'error'); 3238 renderComposeStatusOnly(); 3239 }).finally(function () { 3240 state.compose.busy = false; 3241 renderComposeStatusOnly(); 3242 }); 3243 } 3244 3245 function autosaveCompose() { 3246 var payload = composePayload('autosave'); 3247 if (!payload) { 3248 return; 3249 } 3250 if (!payload.title.trim() && !payload.content.trim()) { 3251 state.compose.saveStatus = ''; 3252 renderComposeStatusOnly(); 3253 return; 3254 } 3255 saveCompose('autosave'); 3256 } 3257 3258 function setComposeOpen(open) { 3259 ensureComposeStateShape(); 3260 state.compose.open = !!open; 3261 if (!state.compose.open) { 3262 clearComposePostTypeCollapseTimer(); 3263 state.compose.postTypeToolbarCollapsed = false; 3264 state.compose.postTypeLocked = false; 3265 state.compose.sourcePostPath = ''; 3266 state.compose.cameraFullscreen = false; 3267 state.compose.cameraReturnToChooser = false; 3268 clearComposeUploadItems(); 3269 } else { 3270 state.compose.postTypeToolbarCollapsed = false; 3271 state.compose.postTypeChosen = false; 3272 state.compose.postTypeLocked = false; 3273 state.compose.sourcePostPath = ''; 3274 state.compose.cameraFullscreen = false; 3275 state.compose.cameraReturnToChooser = false; 3276 clearComposeUploadItems(); 3277 if (!state.compose.draftId && !state.compose.sourcePostPath) { 3278 seedComposeDefaultTag(false); 3279 } 3280 } 3281 try { 3282 renderComposeUi(); 3283 renderDraftNotice(); 3284 } catch (err) { 3285 state.compose.open = false;
3286 clearComposePostTypeCollapseTimer(); 3287 stopComposeCameraStream(); 3288 stopComposeAudioStream(); 3289 if (window && window.console && typeof window.console.error === 'function') { 3290 window.console.error('Compose render failed', err); 3291 } 3292 if (els.composeSlot) { 3293 els.composeSlot.hidden = true; 3294 els.composeSlot.classList.remove('is-open'); 3295 els.composeSlot.innerHTML = ''; 3296 } 3297 if (els.composeFab) { 3298 els.composeFab.classList.remove('is-open'); 3299 els.composeFab.setAttribute('aria-expanded', 'false'); 3300 els.composeFab.setAttribute('aria-pressed', 'false'); 3301 els.composeFab.setAttribute('aria-label', 'Compose'); 3302 } 3303 setComposeOutput('Compose failed to open. Reload and try again.', 'error'); 3304 return; 3305 } 3306 if (state.compose.open && els.composeSlot) { 3307 setTimeout(function () { 3308 if (els.composeSlot && typeof els.composeSlot.scrollIntoView === 'function') { 3309 try { 3310 els.composeSlot.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' }); 3311 } catch (_err) { 3312 els.composeSlot.scrollIntoView(true); 3313 } 3314 } 3315 var focusTarget = els.composeSlot.querySelector('[data-compose-action="toggle-post-type-toolbar"]'); 3316 if (!state.compose.postTypeChosen) { 3317 focusTarget = els.composeSlot.querySelector('[data-compose-post-type]'); 3318 } else if (!focusTarget) { 3319 focusTarget = composePostType() === 'shortform' 3320 ? els.composeSlot.querySelector('[data-compose-field="content"]') 3321 : els.composeSlot.querySelector('[data-compose-field="title"]'); 3322 } 3323 if (focusTarget && typeof focusTarget.focus === 'function') { 3324 focusTarget.focus(); 3325 } 3326 }, 30); 3327 } 3328 } 3329 3330 function toggleComposeFromUi(nextOpen) { 3331 var now = Date.now(); 3332 if (now < composeToggleGuardUntil) { 3333 return; 3334 } 3335 composeToggleGuardUntil = now + 120; 3336 setComposeOpen(!!nextOpen); 3337 } 3338 3339 function parseComposeTags(raw) { 3340 return String(raw || '') 3341 .split(',') 3342 .map(function (part) { return normalizeTagValue(part); }) 3343 .filter(Boolean); 3344 } 3345 3346 function openComposeDraftInPlace(draftId, options) { 3347 var opts = options || {}; 3348 var id = String(draftId || '').trim(); 3349 if (!id) { 3350 return Promise.reject(new Error('Draft id is missing.')); 3351 } 3352 var auth = window.__wizardryAuthPayload(); 3353 if (!auth.session_token || !auth.csrf_token) { 3354 return Promise.reject(new Error('Sign in as admin first.')); 3355 } 3356 return apiPost('/cgi/blog-get-draft', { 3357 draft_id: id, 3358 session_token: auth.session_token, 3359 csrf_token: auth.csrf_token 3360 }).then(function (data) { 3361 if (!data || !data.success || !data.draft) { 3362 throw new Error((data && data.error) ? data.error : 'Could not load draft'); 3363 } 3364 var draft = data.draft || {}; 3365 ensureComposeStateShape(); 3366 state.compose.open = true; 3367 state.compose.preview = false; 3368 state.compose.busy = false; 3369 state.compose.saveStatus = ''; 3370 state.compose.output = ''; 3371 state.compose.outputTone = ''; 3372 state.compose.draftId = String(draft.draft_id || id).trim(); 3373 state.compose.sourcePostPath = String(draft.source_post_path || '').trim(); 3374 state.compose.postType = normalizeComposePostType(draft.post_type || 'longform'); 3375 state.compose.postTypeChosen = true; 3376 state.compose.postTypeLocked = !!opts.lockPostType; 3377 state.compose.postTypeToolbarCollapsed = true; 3378 state.compose.tagsOpen = false; 3379 state.compose.tagsDraftText = ''; 3380 state.compose.linkUrl = ''; 3381 state.compose.shortformLimitEditing = false; 3382 state.compose.publishDestination = 'local_only'; 3383 stopComposeCameraStream(); 3384 stopComposeAudioStream(); 3385 setComposeTags(parseComposeTags(draft.tags || '')); 3386 if (els.composeSlot) { 3387 els.composeSlot.innerHTML = ''; 3388 } 3389 renderComposeUi(); 3390 renderDraftNotice(); 3391 if (els.composeSlot) { 3392 var titleInput = els.composeSlot.querySelector('[data-compose-field="title"]'); 3393 var contentInput = els.composeSlot.querySelector('[data-compose-field="content"]'); 3394 var linkUrlInput = els.composeSlot.querySelector('[data-compose-field="link-url"]'); 3395 var scheduleInput = els.composeSlot.querySelector('[data-compose-field="scheduled-at"]'); 3396 var loadedContent = String(draft.content || ''); 3397 if (state.compose.postType === 'link-share') { 3398 var parsedLink = parseComposeLinkShareContent(loadedContent); 3399 state.compose.linkUrl = parsedLink.url; 3400 loadedContent = parsedLink.body; 3401 } 3402 if (titleInput instanceof HTMLInputElement) {
3403 titleInput.value = String(draft.title || ''); 3404 } 3405 if (contentInput instanceof HTMLTextAreaElement) { 3406 contentInput.value = loadedContent; 3407 } 3408 if (linkUrlInput instanceof HTMLInputElement) { 3409 linkUrlInput.value = String(state.compose.linkUrl || ''); 3410 } 3411 if (scheduleInput instanceof HTMLInputElement) { 3412 scheduleInput.value = composeNormalizeScheduledDate(String(draft.scheduled_at_local || '')); 3413 } 3414 refreshComposeShortformMeterFromFields(); 3415 } 3416 if (els.composeSlot && typeof els.composeSlot.scrollIntoView === 'function') { 3417 try { 3418 els.composeSlot.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' }); 3419 } catch (_err) { 3420 els.composeSlot.scrollIntoView(true); 3421 } 3422 } 3423 window.setTimeout(function () { 3424 if (!els.composeSlot) { 3425 return; 3426 } 3427 var postType = normalizeComposePostType(state.compose.postType); 3428 var focusInput = postType === 'shortform' 3429 ? els.composeSlot.querySelector('[data-compose-field="content"]') 3430 : els.composeSlot.querySelector('[data-compose-field="title"]'); 3431 if (focusInput && typeof focusInput.focus === 'function') { 3432 focusInput.focus(); 3433 if (focusInput.setSelectionRange) { 3434 var length = String(focusInput.value || '').length; 3435 focusInput.setSelectionRange(length, length); 3436 } 3437 } 3438 }, 30); 3439 return true; 3440 }); 3441 } 3442 3443 function composeToolbarAction(action) { 3444 if (!els.composeSlot) { 3445 return; 3446 } 3447 var textarea = els.composeSlot.querySelector('[data-compose-field="content"]'); 3448 if (!(textarea instanceof HTMLTextAreaElement)) { 3449 return; 3450 } 3451 function placeCursor(start, end) { 3452 textarea.focus(); 3453 textarea.setSelectionRange(start, end); 3454 } 3455 function applyEdit(start, end, nextText, cursorStart, cursorEnd) { 3456 var replacement = String(nextText || ''); 3457 var selStart = Math.max(0, Number(start) || 0); 3458 var selEnd = Math.max(selStart, Number(end) || selStart); 3459 if (typeof textarea.setRangeText === 'function') { 3460 textarea.focus(); 3461 textarea.setRangeText(replacement, selStart, selEnd, 'preserve'); 3462 } else { 3463 var currentValue = String(textarea.value || ''); 3464 textarea.value = currentValue.slice(0, selStart) + replacement + currentValue.slice(selEnd); 3465 } 3466 placeCursor(selStart + (Number(cursorStart) || 0), selStart + (Number(cursorEnd) || 0)); 3467 } 3468 function replaceSelection(transformer) { 3469 var start = textarea.selectionStart; 3470 var end = textarea.selectionEnd; 3471 var selected = textarea.value.slice(start, end); 3472 var updated = transformer(selected); 3473 applyEdit(start, end, updated.text, updated.cursorStart, updated.cursorEnd); 3474 } 3475 function replaceSelectedLines(transformer) { 3476 var value = textarea.value; 3477 var selStart = textarea.selectionStart; 3478 var selEnd = textarea.selectionEnd; 3479 var lineStart = value.lastIndexOf('\n', Math.max(0, selStart - 1)) + 1; 3480 var lineEndIdx = value.indexOf('\n', selEnd); 3481 var lineEnd = lineEndIdx === -1 ? value.length : lineEndIdx; 3482 var source = value.slice(lineStart, lineEnd); 3483 var lines = source.split('\n'); 3484 var result = transformer(lines); 3485 if (!result || !Array.isArray(result.lines)) { 3486 return; 3487 } 3488 var next = result.lines.join('\n'); 3489 applyEdit(lineStart, lineEnd, next, 0, next.length); 3490 } 3491 3492 if (action === 'bold' || action === 'italic' || action === 'code') { 3493 var token = action === 'bold' ? '**' : (action === 'italic' ? '*' : '`'); 3494 replaceSelection(function (selected) { 3495 var s = selected || 'text'; 3496 if (s.startsWith(token) && s.endsWith(token)) { 3497 var unwrapped = s.slice(token.length, s.length - token.length); 3498 return { text: unwrapped, cursorStart: 0, cursorEnd: unwrapped.length }; 3499 } 3500 var wrapped = token + s + token; 3501 return { text: wrapped, cursorStart: token.length, cursorEnd: token.length + s.length }; 3502 }); 3503 } else if (action === 'h2' || action === 'h3') { 3504 var heading = action === 'h2' ? '## ' : '### '; 3505 replaceSelectedLines(function (lines) { 3506 var line = lines[0] || ''; 3507 var stripped = line.replace(/^#{1,6}\s+/, ''); 3508 lines[0] = line.startsWith(heading) ? stripped : (heading + stripped); 3509 return { lines: lines }; 3510 }); 3511 } else if (action === 'quote' || action === 'ul') { 3512 var prefix = action === 'quote' ? '> ' : '- '; 3513 replaceSelectedLines(function (lines) { 3514 var nonEmpty = lines.filter(function (line) { return line.trim() !== ''; }); 3515 var allHave = nonEmpty.length > 0 && nonEmpty.every(function (line) { 3516 return line.startsWith(prefix); 3517 }); 3518 return { 3519 lines: lines.map(function (line) { 3520 if (line.trim() === '') { 3521 return line; 3522 } 3523 if (allHave) { 3524 return line.startsWith(prefix) ? line.slice(prefix.length) : line; 3525 } 3526 return prefix + line; 3527 }) 3528 }; 3529 }); 3530 } else if (action === 'ol') { 3531 replaceSelectedLines(function (lines) { 3532 var nonEmpty = lines.filter(function (line) { return line.trim() !== ''; }); 3533 var allOrdered = nonEmpty.length > 0 && nonEmpty.every(function (line) { 3534 return /^\d+\.\s+/.test(line); 3535 }); 3536 var index = 1; 3537 return { 3538 lines: lines.map(function (line) { 3539 if (line.trim() === '') { 3540 return line; 3541 } 3542 if (allOrdered) { 3543 return line.replace(/^\d+\.\s+/, ''); 3544 } 3545 var text = line.replace(/^\d+\.\s+/, '').replace(/^-+\s+/, ''); 3546 var out = index + '. ' + text; 3547 index += 1; 3548 return out; 3549 }) 3550 }; 3551 }); 3552 } else if (action === 'code_block') { 3553 replaceSelection(function (selected) { 3554 var source = selected || ''; 3555 if (/^```[\s\S]*```$/.test(source.trim())) { 3556 var unwrapped = source.trim().replace(/^```[\n]?/, '').replace(/\n?```$/, ''); 3557 return { text: unwrapped, cursorStart: 0, cursorEnd: unwrapped.length }; 3558 } 3559 var wrapped = '```\n' + source + '\n```'; 3560 return { text: wrapped, cursorStart: 4, cursorEnd: wrapped.length - 4 }; 3561 }); 3562 } else if (action === 'link') { 3563 replaceSelection(function (selected) { 3564 var label = selected || 'link text'; 3565 var text = '[' + label + '](https://)'; 3566 var start = text.indexOf('https://'); 3567 return { text: text, cursorStart: start, cursorEnd: start + 8 }; 3568 }); 3569 }
3569 else if (action === 'image') { 3570 replaceSelection(function (selected) { 3571 var label = selected || 'image'; 3572 var text = ''; 3573 var start = text.indexOf('https://'); 3574 return { text: text, cursorStart: start, cursorEnd: start + 8 }; 3575 }); 3576 } else if (action === 'audio_record') { 3577 if (composePostType() !== 'audio-note') { 3578 setComposePostType('audio-note', { skipAutosave: true, interactive: true }); 3579 return; 3580 } 3581 if (state.compose.audioRecording) { 3582 stopComposeAudioRecording(); 3583 } else { 3584 startComposeAudioRecording(); 3585 } 3586 return; 3587 } else if (action === 'attachment') { 3588 openComposePickerForType('attachment'); 3589 return; 3590 } 3591 3592 queueComposeAutosave(); 3593 renderComposeUi(); 3594 } 3595 3596 function renderComposeUi() { 3597 ensureComposeStateShape(); 3598 ensureComposeHosts(); 3599 if (!els.composeSlot || !els.composeFab) { 3600 renderComposeCameraOverlay(); 3601 return; 3602 } 3603 var admin = isAdmin(); 3604 els.composeFab.hidden = !admin; 3605 if (!admin) { 3606 state.compose.open = false; 3607 applyComposeModeEffects('longform'); 3608 els.composeSlot.hidden = true; 3609 els.composeSlot.classList.remove('is-open'); 3610 els.composeSlot.innerHTML = ''; 3611 renderComposeCameraOverlay(); 3612 return; 3613 } 3614 els.composeFab.classList.toggle('is-open', state.compose.open); 3615 els.composeFab.setAttribute('aria-expanded', state.compose.open ? 'true' : 'false'); 3616 els.composeFab.setAttribute('aria-pressed', state.compose.open ? 'true' : 'false'); 3617 els.composeFab.setAttribute('aria-label', state.compose.open ? 'Close compose' : 'Compose'); 3618 if (!state.compose.open) { 3619 applyComposeModeEffects('longform'); 3620 els.composeSlot.classList.remove('is-open'); 3621 setTimeout(function () { 3622 if (!state.compose.open && els.composeSlot) { 3623 els.composeSlot.hidden = true; 3624 els.composeSlot.innerHTML = ''; 3625 } 3626 }, 300); 3627 renderComposeCameraOverlay(); 3628 return; 3629 } 3630 var composeSlotWasVisible = !els.composeSlot.hidden && els.composeSlot.classList.contains('is-open'); 3631 function setComposeSlotOpenHeight() { 3632 if (!els.composeSlot) { 3633 return; 3634 } 3635 var measuredHeight = Math.ceil(els.composeSlot.scrollHeight || 0); 3636 var openHeight = Math.max(120, measuredHeight + 8); 3637 els.composeSlot.style.setProperty('--compose-slot-open-max-height', String(openHeight) + 'px'); 3638 } 3639 function openComposeSlotAnimated() { 3640 if (!els.composeSlot) { 3641 return; 3642 } 3643 setComposeSlotOpenHeight(); 3644 if (composeSlotWasVisible) { 3645 els.composeSlot.classList.add('is-open'); 3646 return; 3647 } 3648 els.composeSlot.classList.remove('is-open'); 3649 // Force Firefox to commit the collapsed style before opening. 3650 void els.composeSlot.offsetHeight; 3651 requestAnimationFrame(function () { 3652 if (els.composeSlot && state.compose.open) { 3653 setComposeSlotOpenHeight(); 3654 els.composeSlot.classList.add('is-open'); 3655 } 3656 }); 3657 } 3658 3659 var fields = readComposeFields() || { 3660 title: '', 3661 content: '', 3662 scheduledAt: '', 3663 tags: state.compose.tags.join(','), 3664 linkUrl: String(state.compose.linkUrl || ''), 3665 postType: composePostType() 3666 }; 3667 fields.tags = state.compose.tags.join(','); 3668 var mode = composePublishMode(); 3669 var destination = composePublishDestination(); 3670 var postType = normalizeComposePostType(fields.postType); 3671 if (Array.isArray(state.compose.pendingContentAdditions) && state.compose.pendingContentAdditions.length) { 3672 var pendingJoined = state.compose.pendingContentAdditions 3673 .map(function (item) { return String(item || '').trim(); }) 3674 .filter(Boolean) 3675 .join('\n\n'); 3676 if (pendingJoined) { 3677 fields.content = String(fields.content || '').trim() 3678 ? (String(fields.content).replace(/\s*$/, '') + '\n\n' + pendingJoined) 3679 : pendingJoined; 3680 } 3681 state.compose.pendingContentAdditions = []; 3682 } 3683 var showTitleField = postType !== 'shortform'; 3684 if (postType !== 'shortform') { 3685 state.compose.shortformLimitEditing = false;
3686 } 3687 state.compose.postType = postType; 3688 state.compose.publishDestination = destination; 3689 state.compose.linkUrl = String(fields.linkUrl || ''); 3690 var postTypeLocked = !!state.compose.postTypeLocked; 3691 var waitingForPostType = !state.compose.postTypeChosen && !postTypeLocked; 3692 if (waitingForPostType) { 3693 var chooseHeadRowClass = 'field-row blog-compose-head-row is-type-picker-only'; 3694 var chooseTypeControlClass = 'compose-post-type-control'; 3695 els.composeSlot.hidden = false; 3696 els.composeSlot.innerHTML = '' + 3697 '<article class="post-item blog-post-item blog-compose-card blog-compose-type-only">' + 3698 '<div class="blog-compose-body">' + 3699 '<div class="blog-compose-type-title"><strong>Compose New Post</strong></div>' + 3700 '<div class="' + chooseHeadRowClass + '" data-compose-head-row>' + 3701 '<div class="' + chooseTypeControlClass + ' is-choose-mode" data-compose-type-control>' + 3702 '<div class="compose-post-type-toolbar-wrap"><div class="compose-post-type-row">' + composeTypeButtonsHtml('', { lockAll: postTypeLocked, noActive: true }) + '</div></div>' + 3703 '</div>' + 3704 '</div>' + 3705 '<input type="file" data-compose-field="capture-upload" data-compose-upload="capture-media" accept="image/*,video/*" capture="environment" multiple hidden>' + 3706 '<input type="file" data-compose-field="media-upload" data-compose-upload="upload-media" accept="image/*,video/*" multiple hidden>' + 3707 '</div>' + 3708 '</article>'; 3709 openComposeSlotAnimated(); 3710 renderComposeCameraOverlay(); 3711 return; 3712 } 3713 if (postType === 'shortform') { 3714 enforceComposeShortformLimitOnFields(fields); 3715 var shortLimit = currentComposeShortformLimit(); 3716 if (String(fields.content || '').length > shortLimit) { 3717 fields.content = String(fields.content || '').slice(0, shortLimit); 3718 } 3719 } 3720 var previewContent = fields.content; 3721 if (postType === 'link-share') { 3722 var linkPreview = composeBuildLinkMarkdown(fields.linkUrl, fields.content, fields.title); 3723 if (linkPreview) { 3724 previewContent = linkPreview; 3725 } 3726 } 3727 var previewTitle = showTitleField ? fields.title : ''; 3728 var previewHtml = renderComposePreviewHtml(previewTitle, previewContent); 3729 var contentLabel = 'Body'; 3730 var contentPlaceholder = 'Post body'; 3731 if (postType === 'shortform') { 3732 contentPlaceholder = 'Write a short post...'; 3733 } else if (postType === 'link-share') { 3734 contentPlaceholder = 'Optional commentary...'; 3735 } else if (postType === 'capture-media') { 3736 contentPlaceholder = 'Optional caption for captured media...'; 3737 } else if (postType === 'upload-media') { 3738 contentPlaceholder = 'Optional caption for uploaded media...'; 3739 } else if (postType === 'attachment') { 3740 contentPlaceholder = 'Optional note for attached files...'; 3741 } else if (postType === 'audio-note') { 3742 contentPlaceholder = 'Optional note for uploaded audio...'; 3743 } 3744 var titlePlaceholder = 'Post title'; 3745 if (postType === 'shortform') { 3746 titlePlaceholder = 'Short post'; 3747 } else if (postType === 'link-share') { 3748 titlePlaceholder = 'Link title (optional)'; 3749 } else if (postType === 'capture-media' || postType === 'upload-media') { 3750 titlePlaceholder = 'Media title (optional)'; 3751 } 3752 var mediaToolsHtml = composeModePanelHtml(postType, fields); 3753 var forcePreview = postType === 'capture-media' || postType === 'upload-media'; 3754 var showPreview = !!state.compose.preview || forcePreview; 3755 var previewIslandLayout = showPreview; 3756 var headRowClass = 'field-row blog-compose-head-row' + (state.compose.postTypeToolbarCollapsed ? ' is-type-collapsed' : ''); 3757 var typeControlClass = state.compose.postTypeToolbarCollapsed ? 'compose-post-type-control is-collapsed' : 'compose-post-type-control'; 3758 var shortform = postType === 'shortform'; 3759 var autosaveClass = 'autosave-indicator compose-editor-autosave' + 3760 (state.compose.saveStatus === 'saving' ? ' is-saving' : '') + 3761 (state.compose.saveStatus === 'error' ? ' is-error' : '') + 3762 (shortform ? ' is-shortform-corner' : ''); 3763 var shortformCornerHtml = shortform 3764 ? ('<div class="blog-compose-shortform-corner">' + 3765 '<div class="' + autosaveClass + '"' + (state.compose.saveStatus ? '' : ' hidden') + '>
3765' + (state.compose.saveStatus === 'saving' ? 'Saving...' : (state.compose.saveStatus === 'error' ? 'Save failed' : 'â Saved')) + '</div>' + 3766 composeShortformMeterHtml(fields.content) + 3767 '</div>') 3768 : ''; 3769 var editorBlockHtml = '' + 3770 '<div class="field-row">' + 3771 '<label><strong>' + escapeHtml(contentLabel) + '</strong></label>' + 3772 '<div class="editor-shell blog-compose-editor-shell">' + 3773 composeToolbarHtml() + 3774 '<textarea data-compose-field="content" rows="' + String(composeTextareaRows(postType)) + '"' + (postType === 'shortform' ? (' maxlength="' + escapeHtml(String(currentComposeShortformLimit())) + '"') : '') + ' placeholder="' + escapeHtml(contentPlaceholder) + '">' + escapeHtml(fields.content) + '</textarea>' + 3775 (shortform 3776 ? shortformCornerHtml 3777 : ('<div class="' + autosaveClass + '"' + (state.compose.saveStatus ? '' : ' hidden') + '>' + (state.compose.saveStatus === 'saving' ? 'Saving...' : (state.compose.saveStatus === 'error' ? 'Save failed' : 'â Saved')) + '</div>')) + 3778 '</div>' + 3779 composeUploadProgressHtml() + 3780 '</div>'; 3781 var contentPaneHtml = editorBlockHtml; 3782 var outputClass = 'output'; 3783 if (state.compose.outputTone) { 3784 outputClass += ' ' + state.compose.outputTone; 3785 } 3786 var composeCardClass = 'post-item blog-post-item blog-compose-card'; 3787 var previewActionLabel = 'Preview'; 3788 var previewToggleTitle = state.compose.preview ? 'Preview is on' : 'Preview'; 3789 var previewTogglePressed = state.compose.preview ? 'true' : 'false'; 3790 var previewToggleClass = 'unobtrusive-icon-button blog-compose-preview-toggle' + (state.compose.preview ? ' is-active' : ''); 3791 var previewToggleHtml = '<button type="button" class="' + previewToggleClass + '" data-compose-action="toggle-preview" aria-label="' + previewActionLabel + '" aria-pressed="' + previewTogglePressed + '" title="' + previewToggleTitle + '">' + composePreviewToggleIconSvg() + '<span class="sr-only">' + previewActionLabel + '</span></button>'; 3792 var postKindFooterHtml = composeNostrPillsHtml(postType) + (postTypeLocked ? '<span class="nostr-target-pill">Post type locked</span>' : ''); 3793 var composeCardHtml = '' + 3794 '<article class="' + composeCardClass + '">' + 3795 '<div class="blog-compose-body">' + 3796 '<div class="' + headRowClass + '" data-compose-head-row>' + 3797 '<div class="' + typeControlClass + '" data-compose-type-control>' + 3798 '<button type="button" class="compose-post-type-current-btn unobtrusive-icon-button"' + (postTypeLocked ? ' disabled aria-disabled="true" aria-label="Post type is locked for existing posts" title="Post type is locked for existing posts"' : ' data-compose-action="toggle-post-type-toolbar" aria-label="Choose post type" title="Choose post type"') + '>' + composePostTypeIconSvg(composeBackingPostType(postType)) + '</button>' + 3799 '<div class="compose-post-type-toolbar-wrap"><div class="compose-post-type-row">' + composeTypeButtonsHtml(postType, { lockAll: postTypeLocked }) + '</div></div>' + 3800 '</div>' + 3801 '<div class="blog-compose-head-actions">' + 3802 previewToggleHtml + 3803 '<button type="button" class="unobtrusive-icon-button blog-compose-close" data-compose-action="close-compose" aria-label="Close compose" title="Close compose">' + composeCloseIconSvg() + '<span class="sr-only">Close compose</span></button>' + 3804 '</div>' + 3805 '</div>' + 3806 '<div class="compose-nostr-target-row">' + postKindFooterHtml + '</div>' + 3807 '<div class="field-row blog-compose-title-row"' + (showTitleField ? '' : ' hidden aria-hidden="true"') + '>' + 3808 '<label><strong>Title</strong></label>' + 3809 '<input type="text" data-compose-field="title" placeholder="' + escapeHtml(titlePlaceholder) + '" value="' + escapeHtml(fields.title) + '">' + 3810 '</div>' + 3811 mediaToolsHtml + 3812 contentPaneHtml + 3813 '<input type="file" data-compose-field="capture-upload" data-compose-upload="capture-media" accept="image/*,video/*" capture="environment" multiple hidden>' + 3814 '<input type="file" data-compose-field="media-upload" data-compose-upload="upload-media" accept="image/*,video/*" multiple hidden>' + 3815 '<input type="file" data-compose-field="file-upload" data-compose-upload="attachment" multiple hidden>' + 3816 '<input type="file" data-compose-field="audio-upload" data-compose-upload="audio-note" accept="audio/*" multiple hidden>' + 3817 '<div class="grid-two">' + 3818 '<div class="field-row">' + 3819 '<label><strong>Tags</strong></label>' + 3820 '<input type="hidden" data-compose-field="tags" value="' + escapeHtml(fields.tags) + '">' + 3821 '<div class="tag-editor' + (state.compose.tags.length ? ' has-tags' : '') + '" role="group" aria-label="Post tags">' + 3822 composeTagsEditorHtml() + 3823 '</div>' + 3824 '</div>' + 3825 '</div>' + 3826 '<div class="field-row compose-release-row">' + 3827 '<strong>When</strong>' + 3828 '<div class="mode-row">' + 3829 '<label><input type="radio" name="blog-inline-compose-mode" value="immediate"' + (mode === 'immediate' ? ' checked' : '') + '> Now</label>' + 3830 '<label><input type="radio" name="blog-inline-compose-mode" value="drip"' + (mode === 'drip' ? ' checked' : '') + '> Queue</label>' + 3831 '<label><input type="radio" name="blog-inline-compose-mode" value="scheduled"' + (mode === 'scheduled' ? ' checked' : '') + '> Schedule...</label>' + 3832 '</div>' + 3833 '<div class="compose-scheduled-value" data-compose-scheduled-value' + (mode === 'scheduled' ? '' : ' hidden') + '>
3833' + escapeHtml(composeScheduledDisplayValue(fields.scheduledAt)) + '</div>' + 3834 '<input type="date" class="compose-scheduled-picker-hidden" data-compose-field="scheduled-at" value="' + escapeHtml(composeNormalizeScheduledDate(fields.scheduledAt)) + '" aria-label="Scheduled release date">' + 3835 '</div>' + 3836 '<div class="field-row compose-destination-row">' + 3837 composePublishDestinationFieldHtml(destination) + 3838 '</div>' + 3839 '</div>' + 3840 '<div class="compose-footer blog-compose-footer">' + 3841 '<div class="compose-actions blog-compose-footer-actions">' + 3842 '<button type="button" class="icon-danger unobtrusive-icon-button blog-compose-delete" data-compose-action="delete" aria-label="Delete draft" title="Delete draft"' + (state.compose.busy ? ' disabled aria-disabled="true"' : '') + '>' + composeTrashIconSvg() + '</button>' + 3843 '<div class="blog-compose-publish-stack">' + 3844 '<div class="blog-compose-publish-row">' + postKindFooterHtml + '<button type="button" class="list-admin-primary-btn blog-compose-btn" data-compose-action="publish"' + (state.compose.busy ? ' disabled aria-disabled="true"' : '') + '>' + escapeHtml(composePrimaryLabel(mode, destination)) + '</button></div>' + 3845 '</div>' + 3846 '</div>' + 3847 '<div class="blog-compose-status-row">' + 3848 '<div class="' + outputClass + '">' + escapeHtml(state.compose.output) + '</div>' + 3849 '</div>' + 3850 '</div>' + 3851 '</article>'; 3852 var previewCardHtml = previewIslandLayout 3853 ? '<article class="post-item blog-post-item blog-compose-preview-card"><div class="blog-compose-preview-card-body"><label><strong>Preview</strong></label><div class="preview-box blog-compose-preview">' + previewHtml + '</div></div></article>' 3854 : ''; 3855 3856 els.composeSlot.hidden = false; 3857 els.composeSlot.innerHTML = previewIslandLayout 3858 ? ('<div class="blog-compose-islands">' + composeCardHtml + previewCardHtml + '</div>') 3859 : composeCardHtml; 3860 hydrateComposeTagsEditor(); 3861 openComposeSlotAnimated(); 3862 requestAnimationFrame(function () { 3863 applyComposeModeEffects(postType); 3864 }); 3865 renderComposeCameraOverlay(); 3866 } 3867 3868 function renderComposeStatusOnly() { 3869 if (!els.composeSlot || !state.compose.open) { 3870 return; 3871 } 3872 var mode = composePublishMode(); 3873 var destination = composePublishDestination(); 3874 state.compose.publishDestination = destination; 3875 var publishBtn = els.composeSlot.querySelector('[data-compose-action="publish"]'); 3876 if (publishBtn instanceof HTMLButtonElement) { 3877 publishBtn.textContent = composePrimaryLabel(mode, destination); 3878 publishBtn.disabled = !!state.compose.busy || state.compose.uploading > 0; 3879 } 3880 var output = els.composeSlot.querySelector('.output'); 3881 if (output) { 3882 output.textContent = state.compose.output || ''; 3883 output.className = 'output' + (state.compose.outputTone ? (' ' + state.compose.outputTone) : ''); 3884 } 3885 renderComposeUploadProgressOnly(); 3886 var scheduleValue = els.composeSlot.querySelector('[data-compose-scheduled-value]'); 3887 if (scheduleValue) { 3888 scheduleValue.hidden = mode !== 'scheduled'; 3889 scheduleValue.textContent = composeScheduledDisplayValue((readComposeFields() || {}).scheduledAt || ''); 3890 } 3891 var autosave = els.composeSlot.querySelector('.compose-editor-autosave'); 3892 if (autosave) { 3893 var modeStatus = String(state.compose.saveStatus || ''); 3894 if (state.compose.uploading > 0) { 3895 autosave.hidden = false; 3896 autosave.classList.remove('is-error'); 3897 autosave.classList.add('is-saving'); 3898 autosave.textContent = 'Uploading...'; 3899 return; 3900 } 3901 autosave.hidden = !modeStatus; 3902 autosave.classList.toggle('is-saving', modeStatus === 'saving'); 3903 autosave.classList.toggle('is-error', modeStatus === 'error'); 3904 autosave.textContent = modeStatus === 'saving' ? 'Saving...' : (modeStatus === 'error' ? 'Save failed' : 'â Saved'); 3905 } 3906 } 3907 3908 function renderExtrasAfter() { 3909 if (!els.content) { 3910 return; 3911 } 3912 window.__wizardryRenderResolvedReadOnlyContent({ 3913 targetNode: els.content, 3914 payload: state.payload 3915 }); 3916 } 3917 3918 function matchFacet(set, value) { 3919 if (!set || !set.size) { 3920 return true; 3921 } 3922 return set.has(String(value || '')); 3923 } 3924 3925 function matchTags(postTags) { 3926 if (!state.filters.tags.size) { 3927 return true; 3928 } 3929 var tags = normalizePostTags(postTags); 3930 for (var i = 0; i < tags.length; i += 1) { 3931 if (state.filters.tags.has(String(tags[i]))) { 3932 return true; 3933 } 3934 } 3935 return false;
3936 } 3937 3938 function normalizePostTags(raw) { 3939 if (Array.isArray(raw)) { 3940 return raw.map(function (tag) { 3941 return String(tag || '').trim(); 3942 }).filter(Boolean); 3943 } 3944 if (typeof raw === 'string') { 3945 return raw.split(',').map(function (tag) { 3946 return String(tag || '').trim(); 3947 }).filter(Boolean); 3948 } 3949 return []; 3950 } 3951 3952 function filteredPosts() { 3953 return state.posts.filter(function (post) { 3954 return matchTags(post.tags) && 3955 matchFacet(state.filters.years, post.year) && 3956 matchFacet(state.filters.types, post.type); 3957 }); 3958 } 3959 3960 function renderedPostCardHtml(post) { 3961 return String(post && post.rendered_html || '').trim(); 3962 } 3963 3964 function postListSignature() { 3965 return JSON.stringify(filteredPosts().map(renderedPostCardHtml).filter(Boolean)); 3966 } 3967 3968 function managedBlogFilterCatalogSignature(posts) { 3969 return JSON.stringify((Array.isArray(posts) ? posts : []).map(function (post) { 3970 var tags = Array.isArray(post && post.tags) 3971 ? post.tags.map(function (tag) { 3972 return String(tag || '').trim(); 3973 }).filter(Boolean) 3974 : (typeof (post && post.tags) === 'string' 3975 ? String(post.tags).split(',').map(function (tag) { 3976 return String(tag || '').trim(); 3977 }).filter(Boolean) 3978 : []); 3979 return { 3980 tags: tags, 3981 year: String(post && post.year || ''), 3982 type: String(post && post.type || 'post') 3983 }; 3984 }).sort(function (a, b) { 3985 var left = JSON.stringify(a); 3986 var right = JSON.stringify(b); 3987 return left.localeCompare(right); 3988 })); 3989 } 3990 3991 function renderManagedBlogFilterFragments(payload) { 3992 if (!payload || !els.tags || !els.years || !els.types) { 3993 return false; 3994 } 3995 return mountPrerenderHtmlFragment(els.tags, payload, 'filter_tags', { allowEmpty: true }) && 3996 mountPrerenderHtmlFragment(els.years, payload, 'filter_years', { allowEmpty: true }) && 3997 mountPrerenderHtmlFragment(els.types, payload, 'filter_types', { allowEmpty: true }); 3998 } 3999 4000 function syncManagedBlogFilterPanelState() { 4001 [els.tags || null, els.years || null, els.types || null].forEach(function (host) { 4002 if (!host) { 4003 return; 4004 } 4005 host.querySelectorAll('[data-filter-group][data-filter-value]').forEach(function (button) { 4006 var group = String(button.getAttribute('data-filter-group') || ''); 4007 var value = String(button.getAttribute('data-filter-value') || ''); 4008 var filterSet = state.filters[group]; 4009 var active = !!(filterSet && filterSet.has && filterSet.has(value)); 4010 button.classList.toggle('is-active', active); 4011 button.setAttribute('aria-pressed', active ? 'true' : 'false'); 4012 }); 4013 }); 4014 } 4015 4016 function renderManagedBlogFilterPanel() { 4017 if (!els.tags || !els.years || !els.types) { 4018 return false; 4019 } 4020 var prerenderedPosts = readPrerenderPostsBootstrap(state.payload); 4021 if (state.payload && prerenderedPosts && 4022 managedBlogFilterCatalogSignature(state.posts) === managedBlogFilterCatalogSignature(prerenderedPosts) && 4023 renderManagedBlogFilterFragments(state.payload)) { 4024 syncManagedBlogFilterPanelState(); 4025 return true; 4026 } 4027 syncManagedBlogFilterPanelState(); 4028 return false; 4029 } 4030 4031 function renderFilters() { 4032 if (!els.tags || !els.years || !els.types) { 4033 return; 4034 } 4035 renderManagedBlogFilterPanel(); 4036 } 4037 4038 function renderList() { 4039 if (!els.list || !els.empty) { 4040 return; 4041 } 4042 4043 if (state.postsLoading && !state.posts.length) { 4044 els.list.innerHTML = ''; 4045 els.empty.hidden = true; 4046 return; 4047 } 4048 4049 var shown = filteredPosts(); 4050 var hasFilters = state.filters.tags.size || state.filters.years.size || state.filters.types.size; 4051 4052 if (!shown.length) { 4053 els.list.innerHTML = ''; 4054 if (!state.posts.length && !hasFilters) { 4055 if (!postsCatalogReady) { 4056 els.empty.hidden = true; 4057 return; 4058 } 4059 els.empty.textContent = 'No posts to show yet.'; 4060 } else { 4061 els.empty.textContent = 'No posts match these filters.'; 4062 } 4063 els.empty.hidden = false;
4064 return; 4065 } 4066 4067 els.empty.hidden = true; 4068 els.list.innerHTML = shown.map(renderedPostCardHtml).filter(Boolean).join(''); 4069 syncRenderedListDecorations(); 4070 } 4071 4072 function postCardMenuHtml(postPath) { 4073 var normalizedPath = String(postPath || '').trim(); 4074 if (!normalizedPath) { 4075 return ''; 4076 } 4077 return '' + 4078 '<div class="post-page-menu">' + 4079 '<button type="button" class="post-page-menu-trigger" data-post-card-menu-toggle="' + escapeHtml(normalizedPath) + '" aria-label="Post menu" aria-haspopup="menu" aria-expanded="false">' + overflowMenuIconSvg() + '</button>' + 4080 '<div class="post-page-menu-panel" role="menu" hidden>' + 4081 '<button type="button" data-post-card-action="edit_post" data-post-path="' + escapeHtml(normalizedPath) + '" role="menuitem">Edit post...</button>' + 4082 '<button type="button" class="post-page-menu-delete" data-post-card-action="delete_post" data-post-path="' + escapeHtml(normalizedPath) + '" role="menuitem">Delete post...</button>' + 4083 '</div>' + 4084 '</div>'; 4085 } 4086 4087 function syncRenderedPostCardMenus() { 4088 if (!els.list) { 4089 return; 4090 } 4091 var cards = els.list.querySelectorAll('.blog-post-item'); 4092 cards.forEach(function (card) { 4093 var existing = card.querySelector('.post-page-menu'); 4094 var postPath = String(card.getAttribute('data-post-path') || '').trim(); 4095 if (!isAdmin() || !postPath) { 4096 if (existing && existing.parentNode) { 4097 existing.parentNode.removeChild(existing); 4098 } 4099 return; 4100 } 4101 if (!existing) { 4102 var head = card.querySelector('.post-head'); 4103 if (!head) { 4104 return; 4105 } 4106 head.insertAdjacentHTML('beforeend', postCardMenuHtml(postPath)); 4107 } 4108 }); 4109 } 4110 4111 function syncRenderedListInlineFilterState() { 4112 if (!els.list) { 4113 return; 4114 } 4115 els.list.querySelectorAll('[data-inline-tag]').forEach(function (button) { 4116 var value = String(button.getAttribute('data-inline-tag') || ''); 4117 var active = state.filters.tags.has(value); 4118 button.classList.toggle('is-active', active); 4119 button.setAttribute('aria-pressed', active ? 'true' : 'false'); 4120 }); 4121 els.list.querySelectorAll('[data-inline-filter-group][data-inline-filter-value]').forEach(function (button) { 4122 var group = String(button.getAttribute('data-inline-filter-group') || ''); 4123 var value = String(button.getAttribute('data-inline-filter-value') || ''); 4124 var filterSet = state.filters[group]; 4125 var active = !!(filterSet && filterSet.has(value)); 4126 button.classList.toggle('is-active', active); 4127 button.setAttribute('aria-pressed', active ? 'true' : 'false'); 4128 }); 4129 } 4130 4131 function syncRenderedListDecorations() { 4132 syncRenderedPostCardMenus(); 4133 syncRenderedListInlineFilterState(); 4134 } 4135 4136 function closePostCardMenus() { 4137 if (!els.list) { 4138 return; 4139 } 4140 var panels = els.list.querySelectorAll('.post-page-menu-panel'); 4141 panels.forEach(function (panel) { 4142 panel.hidden = true; 4143 }); 4144 var triggers = els.list.querySelectorAll('.post-page-menu-trigger[data-post-card-menu-toggle]'); 4145 triggers.forEach(function (trigger) { 4146 trigger.setAttribute('aria-expanded', 'false'); 4147 }); 4148 } 4149 4150 function togglePostCardMenu(trigger) { 4151 if (!(trigger instanceof HTMLElement)) { 4152 return; 4153 } 4154 var wrap = trigger.closest('.post-page-menu'); 4155 if (!wrap) { 4156 return; 4157 } 4158 var panel = wrap.querySelector('.post-page-menu-panel'); 4159 if (!(panel instanceof HTMLElement)) { 4160 return; 4161 } 4162 var shouldOpen = !!panel.hidden; 4163 closePostCardMenus(); 4164 panel.hidden = !shouldOpen; 4165 trigger.setAttribute('aria-expanded', shouldOpen ? 'true' : 'false'); 4166 } 4167 4168 function runPostCardAction(action, postPath) { 4169 var picked = String(action || '').trim(); 4170 var path = String(postPath || '').trim(); 4171 if (!picked || !path || postCardMenuBusy) { 4172 return; 4173 } 4174 if (!isAdmin()) { 4175 window.alert('Sign in as admin first.'); 4176 return; 4177 } 4178 var auth = window.__wizardryAuthPayload(); 4179 if (!auth.session_token || !auth.csrf_token) { 4180 window.alert('Sign in as admin first.'); 4181 return; 4182 } 4183 if (picked === 'edit_post') { 4184 postCardMenuBusy = true; 4185 apiPost('/cgi/blog-create-draft-from-post', { 4186 post_path: path, 4187 session_token: auth.session_token, 4188 csrf_token: auth.csrf_token 4189 }).then(function (data) { 4190 var draftId = String((data && data.draft_id) || '').trim(); 4191 if (!draftId) { 4192 throw new Error('Draft was created but no draft id was returned'); 4193 } 4194 closePostCardMenus(); 4195 return openComposeDraftInPlace(draftId, { lockPostType: true }); 4196 }).catch(function (err) { 4197 window.alert(err && err.message ? err.message : 'Could not create draft from post'); 4198 }).finally(function () { 4199 postCardMenuBusy = false;
4200 }); 4201 return; 4202 } 4203 if (picked === 'delete_post') { 4204 if (!window.confirm('Delete this published post from this site? This cannot be undone.')) { 4205 return; 4206 } 4207 postCardMenuBusy = true; 4208 apiPost('/cgi/blog-manage-post', { 4209 action: 'delete', 4210 post_path: path, 4211 session_token: auth.session_token, 4212 csrf_token: auth.csrf_token 4213 }).then(function () { 4214 state.posts = (Array.isArray(state.posts) ? state.posts : []).filter(function (post) { 4215 return String(post && (post.source_path || post.post_path || post.path) || '').trim() !== path; 4216 }); 4217 renderList(); 4218 updateRenderSignature(); 4219 loadPageState(); 4220 }).catch(function (err) { 4221 window.alert(err && err.message ? err.message : 'Delete failed'); 4222 }).finally(function () { 4223 postCardMenuBusy = false; 4224 }); 4225 } 4226 } 4227 4228 function renderAdminChromeUi() { 4229 renderAdmin(); 4230 renderDraftNotice(); 4231 renderValidation(); 4232 renderExtrasAfter(); 4233 renderComposeUi(); 4234 syncRenderedPostCardMenus(); 4235 } 4236 4237 function renderPreservedPostListShell() { 4238 renderHead(); 4239 renderAdminChromeUi(); 4240 renderFilters(); 4241 syncRenderedListInlineFilterState(); 4242 } 4243 4244 function renderAll() { 4245 renderHead(); 4246 renderAdminChromeUi(); 4247 renderFilters(); 4248 renderList(); 4249 updateRenderSignature(); 4250 } 4251 4252 function toggleFilter(group, value, multi) { 4253 var key = String(group || ''); 4254 var target = state.filters[key]; 4255 if (!target) { 4256 return; 4257 } 4258 var normalized = String(value || ''); 4259 if (!normalized) { 4260 return; 4261 } 4262 4263 if (multi) { 4264 if (target.has(normalized)) { 4265 target.delete(normalized); 4266 } else { 4267 target.add(normalized); 4268 } 4269 } else { 4270 if (target.size === 1 && target.has(normalized)) { 4271 target.clear(); 4272 } else { 4273 target.clear(); 4274 target.add(normalized); 4275 } 4276 } 4277 renderFilters(); 4278 renderList(); 4279 updateRenderSignature(); 4280 } 4281 4282 function clearFilters() { 4283 state.filters.tags.clear(); 4284 state.filters.years.clear(); 4285 state.filters.types.clear(); 4286 renderFilters(); 4287 renderList(); 4288 updateRenderSignature(); 4289 } 4290 4291 function applyDefaultFilters() { 4292 if (state.defaultFiltersApplied) { 4293 return; 4294 } 4295 var page = getRenderState(); 4296 var defaultTag = String(page.default_tag || '').trim(); 4297 if (!defaultTag) { 4298 state.defaultFiltersApplied = true; 4299 return; 4300 } 4301 if (!state.filters.tags.size && !state.filters.years.size && !state.filters.types.size) { 4302 state.filters.tags.add(defaultTag); 4303 } 4304 state.defaultFiltersApplied = true; 4305 } 4306 4307 function setPanelOpen(open) { 4308 if (!els.panel || !els.toggle) { 4309 return; 4310 } 4311 var isOpen = !!open; 4312 els.toggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false'); 4313 if (panelHideTimer) { 4314 window.clearTimeout(panelHideTimer); 4315 panelHideTimer = null; 4316 } 4317 4318 if (isOpen) { 4319 var wasHidden = !!els.panel.hidden; 4320 els.panel.hidden = false; 4321 if (wasHidden) { 4322 els.panel.classList.remove('is-open'); 4323 void els.panel.offsetHeight; 4324 var finishOpen = function () { 4325 if (els.toggle.getAttribute('aria-expanded') === 'true') { 4326 els.panel.classList.add('is-open'); 4327 } 4328 }; 4329 window.requestAnimationFrame(finishOpen); 4330 window.setTimeout(finishOpen, 32); 4331 } else { 4332 els.panel.classList.add('is-open'); 4333 } 4334 return; 4335 } 4336 4337 els.panel.classList.remove('is-open'); 4338 panelHideTimer = window.setTimeout(function () { 4339 panelHideTimer = null; 4340 if (!els.panel.classList.contains('is-open')) { 4341 els.panel.hidden = true; 4342 } 4343 }, 420); 4344 } 4345 4346 function readCache() { 4347 var parsed = readTimedCacheRecord(CACHE_KEY, POSTS_CACHE_MAX_AGE_MS); 4348 if (!parsed) { 4349 return null; 4350 } 4351 try { 4352 if (Array.isArray(parsed.posts)) { 4353 if (!postsHaveRenderedHtml(parsed.posts)) { 4354 removeTimedCacheRecord(CACHE_KEY); 4355 return null; 4356 } 4357 return parsed.posts; 4358 } 4359 } catch (_err) { 4360 return null; 4361 } 4362 return null; 4363 } 4364 4365 function writeCache(posts) { 4366 writeTimedCacheRecord(CACHE_KEY, { 4367 posts: posts || [] 4368 }); 4369 } 4370 4371 function isExpectedPayload(payload) { 4372 var payloadType = String(payload && payload.page_type || '').trim().toLowerCase(); 4373 var payloadSlug = String(payload && payload.slug || '').trim(); 4374 return payloadType === pageType && !!payloadSlug && managedSlugsEquivalent(payloadSlug, slug); 4375 } 4376 4377 function readPrerenderPostsBootstrap(payload) { 4378 if (!payload || typeof payload !== 'object') { 4379 return null; 4380 } 4381 if (!Array.isArray(payload.bootstrap_posts)) { 4382 return null; 4383 } 4384 if (!postsHaveRenderedHtml(payload.bootstrap_posts)) { 4385 return null; 4386 } 4387 return filteredPostsForPageDefaults(payload.bootstrap_posts, payload); 4388 } 4389 4390 function applyBootstrapState(optimisticPayload) { 4391 state.payload = optimisticPayload; 4392 applyDefaultFilters(); 4393 } 4394 4395 function applyPayloadBootstrapPosts(payload) { 4396 var prerenderedPosts = readPrerenderPostsBootstrap(payload); 4397 if (!prerenderedPosts) { 4398 return false;
4399 } 4400 postsCatalogReady = true; 4401 state.posts = prerenderedPosts; 4402 if (pageType !== 'publication' && !payloadHasDefaultPostTag(payload)) { 4403 writeCache(prerenderedPosts); 4404 } 4405 return true; 4406 } 4407 4408 function primeBootstrapPosts(payload) { 4409 if (applyPayloadBootstrapPosts(payload)) { 4410 return true; 4411 } 4412 var cached = readCache(); 4413 if (!cached) { 4414 return false; 4415 } 4416 postsCatalogReady = true; 4417 state.posts = filteredPostsForPageDefaults(cached, payload); 4418 return true; 4419 } 4420 4421 function canUsePrerenderReadOnlyView(payload) { 4422 return window.__wizardryCanUsePayloadPrerenderReadOnly(payload, { 4423 allowAuthenticatedField: 'viewer_authenticated' 4424 }); 4425 } 4426 4427 function shouldPreserveAdoptedStaticPrerender() { 4428 return state.staticPrerenderListAdopted && 4429 canUsePrerenderReadOnlyView(state.payload) && 4430 state.adoptedPostListSignature === postListSignature(); 4431 } 4432 4433 function renderBootstrapPayload(payload, hasWarmPosts) { 4434 return window.__wizardryRenderManagedNostrPageBootstrapPayload(payload, { 4435 state: state, 4436 rootNode: root, 4437 contentNode: els.list, 4438 markPageReady: true, 4439 isExpectedPayload: isExpectedPayload, 4440 applyState: applyBootstrapState, 4441 canAdoptStaticPrerender: function (optimisticPayload) { 4442 return canUsePrerenderReadOnlyView(optimisticPayload); 4443 }, 4444 renderAdoptedPrerender: function () { 4445 state.postsLoading = !hasWarmPosts; 4446 state.staticPrerenderListAdopted = true; 4447 state.adoptedPostListSignature = postListSignature(); 4448 renderPreservedPostListShell(); 4449 }, 4450 renderAll: function () { 4451 state.postsLoading = !hasWarmPosts; 4452 state.staticPrerenderListAdopted = false; 4453 renderAll(); 4454 }, 4455 afterRender: function () { 4456 state.renderSignature = renderSignature(); 4457 } 4458 }); 4459 } 4460 4461 function normalizedPayloadForRenderSignature() { 4462 if (!state.payload) { 4463 return null; 4464 } 4465 return { 4466 slug: state.payload.slug || '', 4467 page_type: state.payload.page_type || '', 4468 nav_title: state.payload.nav_title || '', 4469 kind: state.payload.kind || '', 4470 is_admin: !!state.payload.is_admin, 4471 view_mode: state.payload.view_mode || '', 4472 canonical_exists: !!state.payload.canonical_exists, 4473 draft_exists: !!state.payload.draft_exists, 4474 draft_differs: !!state.payload.draft_differs, 4475 sync_status: state.payload.sync_status || null, 4476 state: state.payload.state || null, 4477 validation: state.payload.validation || null 4478 }; 4479 } 4480
4481 function renderSignature() { 4482 function sortedSet(setObj) { 4483 return Array.from(setObj || []).sort(); 4484 } 4485 return JSON.stringify({ 4486 payload: normalizedPayloadForRenderSignature(), 4487 isAdmin: isAdmin(), 4488 postList: postListSignature(), 4489 filters: { 4490 tags: sortedSet(state.filters.tags), 4491 years: sortedSet(state.filters.years), 4492 types: sortedSet(state.filters.types) 4493 } 4494 }); 4495 } 4496 4497 function updateRenderSignature() { 4498 state.renderSignature = renderSignature(); 4499 return state.renderSignature; 4500 } 4501 4502 function renderFetchedPageStateIfChanged(previousSignature) { 4503 var nextSignature = renderSignature(); 4504 return window.__wizardryReconcileManagedPageRefreshRender({ 4505 state: state, 4506 previousSignature: previousSignature, 4507 nextRenderSignature: nextSignature, 4508 renderPreserved: renderPreservedPostListShell, 4509 renderAll: renderAll 4510 }); 4511 } 4512 4513 function loadPageState(options) { 4514 var opts = options || {}; 4515 var expectedSlug = managedSlugFromPath(window.location.pathname || '/'); 4516 var requestedSlug = expectedSlug || slug; 4517 if (managedSlugsEquivalent(expectedSlug, slug) && normalizeManagedSlug(expectedSlug) === 'index' && normalizeManagedSlug(slug) === 'blog') { 4518 requestedSlug = slug; 4519 } 4520 if (expectedSlug && !managedSlugsEquivalent(expectedSlug, slug)) { 4521 maybeRepairRoute('blog-root-slug-mismatch'); 4522 return Promise.resolve(); 4523 } 4524 state.authSignature = window.__wizardryViewerAuthSignature(); 4525 function hasExpectedSlug(payload) { 4526 if (!payload || typeof payload !== 'object') { 4527 return false; 4528 } 4529 return managedSlugsEquivalent(String(payload.slug || '').trim(), slug); 4530 } 4531 function applyFetchedPageState(data) { 4532 var pageType = String(data && data.page_type || '').trim().toLowerCase(); 4533 if (pageType && pageType !== String(root.getAttribute('data-page-type') || 'blog').trim().toLowerCase()) { 4534 maybeRepairRoute('blog-template-page-type-mismatch:' + pageType); 4535 return; 4536 } 4537 var previousSignature = state.renderSignature; 4538 state.payload = data;
4539 try { 4540 if (typeof data.is_admin !== 'undefined') { 4541 localStorage.setItem('last_auth_is_admin', data.is_admin ? '1' : '0'); 4542 } 4543 } catch (_storageErr) { 4544 // Ignore storage sync failures. 4545 } 4546 applyPayloadBootstrapPosts(data); 4547 applyDefaultFilters(); 4548 if (!opts.deferRender) { 4549 renderFetchedPageStateIfChanged(previousSignature); 4550 } 4551 } 4552 var refreshOptions = { 4553 slug: slug, 4554 pageSlug: requestedSlug, 4555 authSignature: window.__wizardryViewerAuthSignature, 4556 isExpectedPayload: hasExpectedSlug, 4557 allowWrite: isExpectedPayload, 4558 unexpectedPayloadMessage: 'Unexpected page payload for blog page', 4559 renderPayload: applyFetchedPageState, 4560 handleFailurePayload: function (payload) { 4561 if (payload && payload.success === false && String(payload.code || '') === 'unknown_page') { 4562 maybeRepairRoute('unknown-page-slug'); 4563 if (!opts.deferRender) { 4564 renderFetchedPageStateIfChanged(state.renderSignature); 4565 } 4566 return true; 4567 } 4568 return false; 4569 }, 4570 handleError: function () { 4571 if (!opts.deferRender) { 4572 renderFetchedPageStateIfChanged(state.renderSignature); 4573 } 4574 }, 4575 consoleLabel: 'Blog page', 4576 state: state, 4577 contentNode: els.list, 4578 fallbackText: 'Page content is still loading. The latest blog data was not available yet.', 4579 rootNode: root, 4580 markPageReady: true, 4581 onFinally: function () { 4582 loadDraftNoticeData(); 4583 } 4584 }; 4585 if (opts.deferRender) { 4586 refreshOptions.notifyInitialContentPainted = false; 4587 } 4588 return window.__wizardryRefreshManagedNostrPageView(refreshOptions); 4589 } 4590 4591 function applyOptimisticAdminFromAuthEvent(event) { 4592 if (!window.__wizardryAuthEventIsLikelyAdmin(event) || !state.payload || state.payload.is_admin) { 4593 return; 4594 } 4595 state.payload = Object.assign({}, state.payload, { is_admin: true }); 4596 renderAdmin(); 4597 renderValidation(); 4598 renderComposeUi(); 4599 syncRenderedPostCardMenus(); 4600 updateRenderSignature(); 4601 } 4602 4603 root.addEventListener('click', function (event) { 4604 var target = eventTargetElement(event.target); 4605 var draftBannerAction = target ? target.closest('[data-draft-banner-action]') : null; 4606 if (draftBannerAction) { 4607 event.preventDefault(); 4608 event.stopPropagation(); 4609 var draftBannerActionName = String(draftBannerAction.getAttribute('data-draft-banner-action') || ''); 4610 if (draftBannerActionName === 'continue') { 4611 openDraftFromNotice(String(draftBannerAction.getAttribute('data-draft-id') || '')); 4612 } else if (draftBannerActionName === 'dismiss') { 4613 state.draftNotice.dismissedSignature = draftNoticeSignature(state.draftNotice.drafts); 4614 renderDraftNotice(); 4615 } 4616 return; 4617 } 4618 var postCardMenuTrigger = target ? target.closest('.post-page-menu-trigger[data-post-card-menu-toggle]') : null; 4619 if (postCardMenuTrigger) { 4620 event.preventDefault(); 4621 togglePostCardMenu(postCardMenuTrigger); 4622 return; 4623 } 4624 4625 var postCardMenuAction = target ? target.closest('[data-post-card-action]') : null; 4626 if (postCardMenuAction instanceof HTMLElement) { 4627 event.preventDefault(); 4628 closePostCardMenus(); 4629 runPostCardAction( 4630 String(postCardMenuAction.getAttribute('data-post-card-action') || ''), 4631 String(postCardMenuAction.getAttribute('data-post-path') || '') 4632 ); 4633 return; 4634 } 4635 4636 if (!target || !target.closest('.post-page-menu')) { 4637 closePostCardMenus(); 4638 } 4639 4640 var composeFab = target ? target.closest('[data-blog-action="toggle-compose"]') : null; 4641 if (composeFab) { 4642 event.preventDefault(); 4643 event.stopPropagation(); 4644 toggleComposeFromUi(!state.compose.open); 4645 return; 4646 } 4647 4648 var composeTagToken = target ? target.closest('[data-compose-tag-token]') : null; 4649 if (composeTagToken instanceof HTMLElement) { 4650 event.preventDefault(); 4651 event.stopPropagation(); 4652 var tokenEditor = composeTagsEditorNode(); 4653 if (tokenEditor && tokenEditor.contains(composeTagToken)) { 4654 composeTagsEditorSelectToken(tokenEditor, composeTagToken); 4655 } 4656 return; 4657 } 4658 4659 var composeTagEditor = target ? target.closest('[data-compose-field="tags-editor"]') : null; 4660 if (composeTagEditor instanceof HTMLElement) { 4661 composeTagsEditorClearSelection(composeTagEditor);
4662 try { 4663 composeTagEditor.focus({ preventScroll: true }); 4664 } catch (_focusErr) { 4665 composeTagEditor.focus(); 4666 } 4667 if (!composeTagsEditorPlaceCaretFromPoint(composeTagEditor, Number(event.clientX) || 0, Number(event.clientY) || 0)) { 4668 var draftNode = composeTagsEditorDraftNode(composeTagEditor); 4669 if (target === composeTagEditor || !(draftNode && draftNode.contains(target))) { 4670 composeTagsEditorFocusDraft(composeTagEditor); 4671 } 4672 } 4673 setTimeout(function () { 4674 if (document.activeElement === composeTagEditor) { 4675 composeTagsEditorSyncDraft(composeTagEditor); 4676 } 4677 }, 0); 4678 } 4679 4680 var composeAction = target ? target.closest('[data-compose-action]') : null; 4681 if (composeAction) { 4682 event.preventDefault(); 4683 var actionName = String(composeAction.getAttribute('data-compose-action') || ''); 4684 if (actionName === 'toggle-preview') { 4685 state.compose.preview = !state.compose.preview; 4686 renderComposeUi(); 4687 return; 4688 } 4689 if (actionName === 'close-compose') { 4690 toggleComposeFromUi(false); 4691 return; 4692 } 4693 if (actionName === 'set-post-type') { 4694 if (state.compose.postTypeLocked) { 4695 return; 4696 } 4697 setComposePostType(String(composeAction.getAttribute('data-compose-post-type') || ''), { interactive: true, skipAutosave: true }); 4698 return; 4699 } 4700 if (actionName === 'toggle-post-type-toolbar') { 4701 if (state.compose.postTypeLocked) { 4702 return; 4703 } 4704 if (!state.compose.postTypeChosen) { 4705 setComposePostTypeToolbarCollapsed(false); 4706 return; 4707 } 4708 if (state.compose.postTypeToolbarCollapsed) { 4709 setComposePostTypeToolbarCollapsed(false); 4710 } else { 4711 setComposePostTypeToolbarCollapsed(true); 4712 } 4713 return; 4714 } 4715 if (actionName === 'publish') { 4716 saveCompose(composeModeAction(composePublishMode(), composePublishDestination())); 4717 return; 4718 } 4719 if (actionName === 'delete') { 4720 deleteComposeDraft(); 4721 return; 4722 } 4723 if (actionName === 'remove-tag') { 4724 removeComposeTag(String(composeAction.getAttribute('data-compose-tag') || '')); 4725 renderComposeUi(); 4726 return; 4727 } 4728 if (actionName === 'toggle-tags') { 4729 state.compose.tagsOpen = !state.compose.tagsOpen; 4730 renderComposeUi(); 4731 return; 4732 } 4733 if (actionName === 'open-mode-picker') { 4734 openComposePickerForType(String(composeAction.getAttribute('data-compose-mode-target') || composePostType())); 4735 return; 4736 } 4737 if (actionName === 'open-camera-capture') { 4738 openComposeCameraCapture({ returnToChooser: false }); 4739 return; 4740 } 4741 if (actionName === 'cancel-camera-capture') { 4742 closeComposeCameraCapture({}); 4743 return; 4744 } 4745 if (actionName === 'capture-photo') { 4746 capturePhotoFromComposeCamera(); 4747 return; 4748 } 4749 if (actionName === 'audio-record-toggle') { 4750 if (state.compose.audioRecording) { 4751 stopComposeAudioRecording(); 4752 } else { 4753 startComposeAudioRecording(); 4754 } 4755 return; 4756 } 4757 if (actionName === 'shortform-limit-toggle') { 4758 if (event.detail > 1) { 4759 return; 4760 } 4761 if (composePostType() !== 'shortform' || state.compose.shortformLimitEditing) { 4762 return; 4763 } 4764 var currentLimit = currentComposeShortformLimit(); 4765 setComposeShortformLimit(currentLimit === 280 ? 140 : 280, { editing: false, skipAutosave: true }); 4766 return; 4767 } 4768 if (actionName === 'pick-capture-media' || actionName === 'pick-upload-media' || actionName === 'pick-upload-file' || actionName === 'pick-upload-audio') { 4769 var field = ''; 4770 var type = ''; 4771 if (actionName === 'pick-capture-media') { 4772 field = 'capture-upload'; 4773 type = 'capture-media'; 4774 } else if (actionName === 'pick-upload-media') { 4775 field = 'media-upload'; 4776 type = 'upload-media'; 4777 } else if (actionName === 'pick-upload-file') { 4778 field = 'file-upload'; 4779 type = 'attachment'; 4780 } else { 4781 type = 'audio-note'; 4782 setComposePostType(type, { skipAutosave: true, interactive: true }); 4783 return; 4784 } 4785 setComposePostType(type, { skipAutosave: true, skipRender: true }); 4786 var picker = els.composeSlot.querySelector('[data-compose-field="' + field + '"]'); 4787 if (picker instanceof HTMLInputElement) { 4788 picker.click(); 4789 } 4790 return; 4791 } 4792 } 4793 var scheduledModeClick = target ? target.closest('input[name="blog-inline-compose-mode"][value="scheduled"]') : null; 4794 if (scheduledModeClick instanceof HTMLInputElement) { 4795 setTimeout(openComposeScheduledPicker, 0); 4796 } 4797 4798 var composeToolbar = target ? target.closest('[data-compose-toolbar]') : null; 4799 if (composeToolbar) { 4800 event.preventDefault(); 4801 composeToolbarAction(String(composeToolbar.getAttribute('data-compose-toolbar') || '')); 4802 return; 4803 } 4804 4805 var toggle = target ? target.closest('[data-filter-group][data-filter-value]') : null; 4806 if (toggle) { 4807 event.preventDefault(); 4808 toggleFilter( 4809 toggle.getAttribute('data-filter-group'), 4810 toggle.getAttribute('data-filter-value'), 4811 !!(event.metaKey || event.ctrlKey) 4812 ); 4813 return; 4814 } 4815 4816 var inlineTag = target ? target.closest('[data-inline-tag]') : null; 4817 if (inlineTag) { 4818 event.preventDefault(); 4819 setPanelOpen(true); 4820 toggleFilter('tags', inlineTag.getAttribute('data-inline-tag'), !!(event.metaKey || event.ctrlKey)); 4821 return; 4822 } 4823 4824 var inlineFilter = target ? target.closest('[data-inline-filter-group][data-inline-filter-value]') : null; 4825 if (inlineFilter) { 4826 event.preventDefault(); 4827 setPanelOpen(true); 4828 toggleFilter( 4829 inlineFilter.getAttribute('data-inline-filter-group'), 4830 inlineFilter.getAttribute('data-inline-filter-value'), 4831 !!(event.metaKey || event.ctrlKey) 4832 ); 4833 return; 4834 } 4835 4836 var action = target ? target.closest('[data-blog-action]') : null; 4837 if (action) { 4838 event.preventDefault(); 4839 if (action.getAttribute('data-blog-action') === 'toggle-page-settings') { 4840 setPageSettingsOpen(!state.pageSettingsOpen); 4841 } else if (action.getAttribute('data-blog-action') === 'save-publication') { 4842 savePublicationDraft(); 4843 } else if (action.getAttribute('data-blog-action') === 'publish-publication') { 4844 publishPublication(); 4845 } 4846 } 4847 }); 4848 4849 root.addEventListener('change', function (event) { 4850 var target = event.target; 4851 if (!(target instanceof HTMLSelectElement)) { 4852 return; 4853 } 4854 if (!target.matches('[data-blog-setting="default-tag"]')) { 4855 return; 4856 } 4857 var picked = String(target.value || '').trim(); 4858 if (state.payload && state.payload.state) { 4859 state.payload.state.default_tag = picked; 4860 } 4861 state.defaultFiltersApplied = true; 4862 state.filters.tags.clear(); 4863 state.filters.years.clear(); 4864 state.filters.types.clear(); 4865 if (picked) { 4866 state.filters.tags.add(picked); 4867 } 4868 renderFilters(); 4869 renderList(); 4870 }); 4871 4872 root.addEventListener('dragover', function (event) { 4873 var target = eventTargetElement(event.target); 4874 if (!composeDropInComposeCard(target)) { 4875 return; 4876 } 4877 if (!composeDragHasFiles(event)) { 4878 return; 4879 } 4880 if (!composeCanAcceptDroppedFiles(state.compose.postTypeChosen ? composePostType() : '')) { 4881 return; 4882 } 4883 event.preventDefault(); 4884 setComposeDropHover(target); 4885 if (event.dataTransfer) { 4886 event.dataTransfer.dropEffect = 'copy'; 4887 } 4888 }); 4889 4890 root.addEventListener('dragleave', function () { 4891 clearComposeDropHover(); 4892 }); 4893 4894 root.addEventListener('drop', function (event) { 4895 var target = eventTargetElement(event.target); 4896 if (!composeDropInComposeCard(target)) { 4897 return; 4898 } 4899 var files = composeDroppedFiles(event); 4900 if (!files.length) { 4901 return; 4902 } 4903 if (!composeCanAcceptDroppedFiles(state.compose.postTypeChosen ? composePostType() : '')) { 4904 event.preventDefault(); 4905 clearComposeDropHover(); 4906 setComposeOutput('File drop is disabled for this post type.', 'warn'); 4907 renderComposeStatusOnly(); 4908 return; 4909 } 4910 event.preventDefault(); 4911 clearComposeDropHover(); 4912 var currentType = state.compose.postTypeChosen ? composePostType() : ''; 4913 var preferredType = composePreferredDropType(files, currentType); 4914 handleComposeUploads(files, preferredType || undefined); 4915 }); 4916 4917 document.addEventListener('click', function (event) { 4918 var target = eventTargetElement(event.target); 4919 if (!target) { 4920 return; 4921 } 4922 var navCompose = target.closest('.nav-compose'); 4923 if (!navCompose) { 4924 return; 4925 } 4926 if (!isAdmin()) { 4927 return; 4928 } 4929 event.preventDefault(); 4930 event.stopPropagation(); 4931 toggleComposeFromUi(true); 4932 }); 4933 4934 root.addEventListener('dblclick', function (event) { 4935 var toggle = event.target && event.target.closest('[data-compose-action="shortform-limit-toggle"]'); 4936 if (!toggle || !state.compose.open || composePostType() !== 'shortform') { 4937 return; 4938 } 4939 event.preventDefault(); 4940 state.compose.shortformLimitEditing = true; 4941 renderComposeUi(); 4942 if (els.composeSlot) { 4943 var input = els.composeSlot.querySelector('[data-compose-field="shortform-limit"]'); 4944 if (input instanceof HTMLInputElement) { 4945 input.focus(); 4946 input.select(); 4947 } 4948 } 4949 }); 4950 4951 root.addEventListener('input', function (event) { 4952 var target = event.target; 4953 if (!(target instanceof HTMLElement) || !state.compose.open) { 4954 return; 4955 } 4956 var tagEditorTarget = target.closest('[data-compose-field="tags-editor"]'); 4957 if (tagEditorTarget instanceof HTMLElement) { 4958 composeTagsEditorClearSelection(tagEditorTarget); 4959 composeTagsEditorSyncDraft(tagEditorTarget); 4960 syncComposeTagsField(); 4961 if (String(state.compose.tagsDraftText || '').trim()) { 4962 queueComposeAutosave(); 4963 } else { 4964 renderComposeStatusOnly(); 4965 } 4966 return; 4967 } 4968 if (target.matches('[data-compose-field="shortform-limit"]')) { 4969 return; 4970 } 4971 if (target.matches('[data-compose-field="title"], [data-compose-field="content"], [data-compose-field="scheduled-at"], [data-compose-field="link-url"]')) { 4972 if (target.matches('[data-compose-field="link-url"]')) { 4973 state.compose.linkUrl = String(target.value || ''); 4974 if (composePostType() !== 'link-share') { 4975 state.compose.postType = 'link-share'; 4976 state.compose.postTypeChosen = true; 4977 } 4978 } 4979 if (target.matches('[data-compose-field="content"]') && composePostType() === 'shortform') { 4980 enforceComposeShortformLimitOnFields(); 4981 if (els.composeSlot) { 4982 var meterButton = els.composeSlot.querySelector('[data-compose-action="shortform-limit-toggle"]'); 4983 if (meterButton instanceof HTMLButtonElement) { 4984 meterButton.textContent = String(target.value || '').length + '/' + String(currentComposeShortformLimit()); 4985 } 4986 } 4987 } 4988 queueComposeAutosave(); 4989 if (state.compose.preview) {
4990 renderComposeUiPreserveFieldFocus(); 4991 } else { 4992 renderComposeStatusOnly(); 4993 } 4994 return; 4995 } 4996 if (target.matches('input[name="blog-inline-compose-mode"], input[name="blog-inline-compose-destination"]')) { 4997 var shouldOpenSchedulePicker = target.matches('input[name="blog-inline-compose-mode"][value="scheduled"]') && target.checked; 4998 renderComposeUi(); 4999 if (shouldOpenSchedulePicker) { 5000 setTimeout(openComposeScheduledPicker, 0); 5001 } 5002 } 5003 }); 5004 5005 root.addEventListener('change', function (event) { 5006 var target = event.target; 5007 if (!(target instanceof HTMLInputElement) || !state.compose.open) { 5008 return; 5009 } 5010 if (target.matches('[data-compose-field="scheduled-at"]')) { 5011 queueComposeAutosave(); 5012 renderComposeStatusOnly(); 5013 return; 5014 } 5015 if (target.matches('[data-compose-field="shortform-limit"]')) { 5016 setComposeShortformLimit(target.value, { editing: false }); 5017 return; 5018 } 5019 if (!target.matches('[data-compose-upload]')) { 5020 return; 5021 } 5022 var uploadType = String(target.getAttribute('data-compose-upload') || '').trim(); 5023 var files = target.files ? Array.from(target.files) : []; 5024 if (!files.length) { 5025 return; 5026 } 5027 handleComposeUploads(files, uploadType).finally(function () { 5028 target.value = ''; 5029 }); 5030 }); 5031 5032 root.addEventListener('keydown', function (event) { 5033 var target = event.target; 5034 if (!(target instanceof HTMLElement) || !state.compose.open) { 5035 return; 5036 } 5037 if (state.compose.cameraFullscreen && event.key === 'Escape') { 5038 event.preventDefault(); 5039 closeComposeCameraCapture({}); 5040 return; 5041 } 5042 if (target.matches('[data-compose-field="shortform-limit"]')) { 5043 if (event.key === 'Enter') { 5044 event.preventDefault(); 5045 setComposeShortformLimit(target.value, { editing: false }); 5046 return; 5047 } 5048 if (event.key === 'Escape') { 5049 event.preventDefault(); 5050 state.compose.shortformLimitEditing = false; 5051 renderComposeUi(); 5052 return; 5053 } 5054 } 5055 var tagEditor = target.closest('[data-compose-field="tags-editor"]'); 5056 if (tagEditor instanceof HTMLElement) { 5057 var selectedToken = composeTagsEditorSelectedToken(tagEditor); 5058 if (event.key === ',' || event.code === 'Comma' || event.key === 'Tab') { 5059 event.preventDefault(); 5060 if (composeTagsEditorCommit(tagEditor, true)) { 5061 queueComposeAutosave(); 5062 } else { 5063 renderComposeStatusOnly(); 5064 } 5065 return; 5066 } 5067 if (selectedToken && event.key && event.key.length === 1 && !event.metaKey && !event.ctrlKey && !event.altKey) { 5068 composeTagsEditorClearSelection(tagEditor); 5069 composeTagsEditorFocusDraft(tagEditor); 5070 } 5071 if (event.key === 'Backspace' || event.key === 'Delete') { 5072 if (selectedToken) { 5073 event.preventDefault(); 5074 if (composeTagsEditorRemoveTagByNode(tagEditor, selectedToken)) { 5075 queueComposeAutosave(); 5076 } else { 5077 renderComposeStatusOnly(); 5078 } 5079 return; 5080 } 5081 var draftText = composeTagsEditorReadDraftText(tagEditor); 5082 if (!draftText.trim() && state.compose.tags.length) { 5083 event.preventDefault(); 5084 var edgeTag = event.key === 'Delete' ? state.compose.tags[0] : state.compose.tags[state.compose.tags.length - 1]; 5085 if (edgeTag) { 5086 removeComposeTag(edgeTag); 5087 composeTagsEditorRender(tagEditor); 5088 composeTagsEditorFocusDraft(tagEditor); 5089 queueComposeAutosave(); 5090 } 5091 return; 5092 } 5093 } 5094 if (event.key === 'Escape') { 5095 composeTagsEditorClearSelection(tagEditor); 5096 } 5097 } 5098 }); 5099 5100 root.addEventListener('focusout', function (event) { 5101 var target = event.target; 5102 if (!(target instanceof HTMLElement) || !state.compose.open) { 5103 return; 5104 } 5105 var tagEditorTarget = target.closest('[data-compose-field="tags-editor"]'); 5106 if (tagEditorTarget instanceof HTMLElement) { 5107 composeTagsEditorSyncDraft(tagEditorTarget); 5108 renderComposeStatusOnly(); 5109 return; 5110 } 5111 if (!(target instanceof HTMLInputElement)) { 5112 return; 5113 } 5114 if (!target.matches('[data-compose-field="shortform-limit"]')) { 5115 return; 5116 } 5117 if (!state.compose.shortformLimitEditing) { 5118 return; 5119 } 5120 setComposeShortformLimit(target.value, { editing: false }); 5121 }); 5122 5123 document.addEventListener('paste', function (event) { 5124 if (!isAdmin()) { 5125 return; 5126 } 5127 if (isEditableTarget(event.target)) { 5128 return; 5129 } 5130 var images = clipboardImageFiles(event); 5131 if (!images.length) { 5132 return; 5133 } 5134 event.preventDefault(); 5135 if (!state.compose.open) { 5136 setComposeOpen(true); 5137 } 5138 setComposePostType('upload-media', { skipAutosave: true, skipRender: true }); 5139 handleComposeUploads(images, 'upload-media'); 5140 }); 5141 5142 if (els.toggle) { 5143 els.toggle.addEventListener('click', function () { 5144 var open = els.toggle.getAttribute('aria-expanded') !== 'true'; 5145 setPanelOpen(open); 5146 }); 5147 } 5148 5149 if (els.clear) { 5150 els.clear.addEventListener('click', function () { 5151 clearFilters(); 5152 }); 5153 } 5154 5155 window.__wizardryBindSessionAuthRefresh({ 5156 onChange: maybeReloadForAuthChange, 5157 storageKeys: ['session_token', 'csrf_token', 'last_auth_is_admin'], 5158 pollState: authRefreshPollState, 5159 pollDurationMs: 15000, 5160 stopWhen: isAdmin 5161 }); 5162 5163 window.addEventListener('beforeunload', function () { 5164 clearComposePostTypeCollapseTimer(); 5165 stopComposeCameraStream(); 5166 stopComposeAudioStream(); 5167 }); 5168 5169 var composeResizeTimer = null; 5170 window.addEventListener('resize', function () { 5171 if (!state.compose.open || !state.compose.preview) { 5172 return; 5173 } 5174 if (composeResizeTimer) { 5175 window.clearTimeout(composeResizeTimer); 5176 } 5177 composeResizeTimer = window.setTimeout(function () { 5178 composeResizeTimer = null; 5179 if (state.compose.open && state.compose.preview) { 5180 renderComposeUi(); 5181 } 5182 }, 120); 5183 }); 5184 5185 window.__wizardryRemoveLegacyTitleBlock(root); 5186 (function bootstrapOnce() { 5187 hydrateDraftNoticeFromCache(); 5188 window.__wizardryRenderManagedNostrBootstrapSources({ 5189 slug: slug, 5190 maxAgeMs: BOOTSTRAP_CACHE_MAX_AGE_MS, 5191 authSignature: window.__wizardryViewerAuthSignature, 5192 isExpectedPayload: isExpectedPayload, 5193 renderPayload: function (payload) { 5194 return renderBootstrapPayload(payload, primeBootstrapPosts(payload)); 5195 } 5196 });
5197 waitForInitialDraftNotice(); 5198 5199 loadPageState({ deferRender: true }).finally(function () { 5200 state.postsLoading = false; 5201 var nextSignature = renderSignature(); 5202 window.__wizardryReconcileManagedPageRefreshRender({ 5203 state: state, 5204 previousSignature: state.renderSignature, 5205 nextRenderSignature: nextSignature, 5206 shouldPreserveAdoptedPrerender: shouldPreserveAdoptedStaticPrerender, 5207 renderPreserved: renderPreservedPostListShell, 5208 renderAll: renderAll 5209 }); 5210 window.__wizardryNotifyCurrentManagedPageReady({ 5211 state: state, 5212 rootNode: root, 5213 markPageReady: true 5214 }); 5215 }); 5216 })(); 5217})();
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.