diff --git a/static/index.html b/static/index.html index 959adaef3..35388819f 100644 --- a/static/index.html +++ b/static/index.html @@ -219,8 +219,8 @@ }, { once: true }); })(); - - + + @@ -2512,7 +2512,7 @@ - + diff --git a/static/js/document.js b/static/js/document.js index 4e67cd520..6413cf94b 100644 --- a/static/js/document.js +++ b/static/js/document.js @@ -36,6 +36,7 @@ import { bindMenuDismiss, dismissOrRemove } from './escMenuStack.js'; let _emailStreamRenderedBody = ''; let _emailStreamTargetBody = ''; let _emailLocalDraftDebounce = null; + let _emailRichbodySaveDebounce = null; const _EMAIL_LOCAL_DRAFT_PREFIX = 'odysseus.email.replyDraft.v1:'; // Diff mode state @@ -2353,7 +2354,7 @@ import { bindMenuDismiss, dismissOrRemove } from './escMenuStack.js'; function _persistEmailLocalDraftSoon() { clearTimeout(_emailLocalDraftDebounce); - _emailLocalDraftDebounce = setTimeout(_persistEmailLocalDraftNow, 250); + _emailLocalDraftDebounce = setTimeout(_persistEmailLocalDraftNow, 800); } function _clearEmailLocalDraft(sourceUid, sourceFolder, inReplyTo) { @@ -2393,20 +2394,41 @@ import { bindMenuDismiss, dismissOrRemove } from './escMenuStack.js'; const ta = document.getElementById('doc-editor-textarea'); if (!ta) return; ta.value = rich.innerText; - ta.dispatchEvent(new Event('input', { bubbles: true })); + const doc = activeDocId && docs.get(activeDocId); + if (doc && doc.language === 'email') { + const fields = _parseEmailHeader(doc.content || ''); + doc.content = _buildEmailContent( + document.getElementById('doc-email-to')?.value || fields.to || '', + document.getElementById('doc-email-subject')?.value || fields.subject || '', + document.getElementById('doc-email-in-reply-to')?.value || fields.inReplyTo || '', + document.getElementById('doc-email-references')?.value || fields.references || '', + rich.innerHTML, + document.getElementById('doc-email-source-uid')?.value || fields.sourceUid || '', + document.getElementById('doc-email-source-folder')?.value || fields.sourceFolder || '', + document.getElementById('doc-email-cc')?.value || fields.cc || '', + document.getElementById('doc-email-bcc')?.value || fields.bcc || '', + ); + } + } + function _scheduleEmailRichbodySave() { + _persistEmailLocalDraftSoon(); + clearTimeout(_emailRichbodySaveDebounce); + _emailRichbodySaveDebounce = setTimeout(() => { saveDocument({ silent: true }); }, 2500); } function _wireEmailRichbody(rich) { if (rich._wired) { _syncEmailRichbody(rich); return; } rich._wired = true; rich.addEventListener('input', () => { _syncEmailRichbody(rich); - _persistEmailLocalDraftSoon(); + _scheduleEmailRichbodySave(); }); // Highlight toolbar buttons (B / I / S, headings, lists) when the caret // sits inside formatted text. queryCommandState reflects the live // selection — we just translate that into .is-active classes the CSS // already understands. - const syncActive = () => { + let syncActiveFrame = 0; + const syncActiveNow = () => { + syncActiveFrame = 0; if (!rich.isConnected || rich.style.display === 'none') return; // Only sync when focus is inside the rich body — otherwise selection // outside it (e.g. clicking the toolbar itself) gives misleading state. @@ -2430,10 +2452,13 @@ import { bindMenuDismiss, dismissOrRemove } from './escMenuStack.js'; if (lBtn) lBtn.classList.toggle('is-active', !!inList); } catch (_) {} }; + const syncActive = () => { + if (syncActiveFrame) return; + syncActiveFrame = requestAnimationFrame(syncActiveNow); + }; rich.addEventListener('keyup', syncActive); rich.addEventListener('mouseup', syncActive); rich.addEventListener('focus', syncActive); - rich.addEventListener('input', syncActive); // selectionchange fires on the document; filter to selections inside rich. document.addEventListener('selectionchange', () => { const sel = window.getSelection(); diff --git a/static/sw.js b/static/sw.js index 3df82dc17..3590ad362 100644 --- a/static/sw.js +++ b/static/sw.js @@ -7,7 +7,7 @@ // - Other static assets (images/fonts/libs): cache-first with bg refresh. // - API / non-GET: never cached. // Bump CACHE_NAME whenever the precache list or SW logic changes. -const CACHE_NAME = 'odysseus-v331'; +const CACHE_NAME = 'odysseus-v332'; // Core shell precached on install so repeat opens are instant without any // network wait. Keep this list in sync with the