diff options
| author | Adam Soltys <[email protected]> | 2026-09-29 11:18:21 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-09-29 11:18:21 -0700 |
| commit | 689e21d5e77765661c44d60c10b18134b995162a (patch) | |
| tree | 2a6e878c9b8e400d3fcd60022d72f7d4bf3030ca /src/app.js | |
| parent | c7186db46f6e45969f01709bc4a0a9472a40dff1 (diff) | |
| download | coinosv3-689e21d5e77765661c44d60c10b18134b995162a.tar.gz coinosv3-689e21d5e77765661c44d60c10b18134b995162a.zip | |
Composers take pictures from the keyboard: the reply, post and chat fields are contenteditable.
Android keyboards (Gboard GIFs, stickers) only offer images to fields that
accept them, and Chrome says so for contenteditable, never <textarea> —
hence the greyed-out GIF key and "This app does not support images here".
<coinos-text> (src/rich-text.js) is a contenteditable that acts like a
textarea to the code around it (value, selectionStart/End, setSelection-
Range, placeholder), keeps text plain, and hands any pasted/dropped/
keyboard picture to onMedia — the paperclip's upload. The morph syncs its
value like a field's and leaves its own editing attributes alone.
composer-media-test: types like a textarea, strips pasted formatting,
a pasted GIF uploads and lands in the reply, still editable after repaint.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Du1qT1wbkYwYK6zkGH79Rp
Diffstat (limited to 'src/app.js')
| -rw-r--r-- | src/app.js | 16 |
1 files changed, 13 insertions, 3 deletions
@@ -6,6 +6,7 @@ import { Wallet, newMnemonic, isValidMnemonic, accountXpubFor, cacheKeyFor, utxoId, parseExtendedKey, xpubToZpub, encryptVault, decryptVault } from './wallet.js'; import { qrSvg } from './qr.js'; +import './rich-text.js'; // <coinos-text>, the composers' field import { makeSearcher, resultRows, searchable, punkUrl, warmSearch } from './recipient-search.js'; import { npubOf, seedPubkey, neventOf } from './nostr.js'; import { nip98Header } from './nip98.js'; @@ -233,7 +234,10 @@ function morph(a, b) { // <details open> is the user's doing (the browser toggles the attribute on // click), like a field's value: a render that doesn't set `open` leaves it // alone instead of collapsing the section on every background repaint. - const userOwned = (at) => a.nodeName === 'DETAILS' && at.name === 'open'; + // <coinos-text> sets its own editing attributes when it joins the page; + // the freshly built copy doesn't have them yet, and must not strip them + const userOwned = (at) => (a.nodeName === 'DETAILS' && at.name === 'open') + || (a.nodeName === 'COINOS-TEXT' && /^(contenteditable|role|aria-multiline|aria-placeholder|data-empty)$/.test(at.name)); for (const at of [...a.attributes]) { if (b.hasAttribute(at.name) || userOwned(at)) continue; if (at.name === 'src' && b.hasAttribute('data-lazy-src')) continue; // the lazy source stands for it @@ -261,6 +265,12 @@ function morph(a, b) { } if (a.disabled !== b.disabled) a.disabled = b.disabled; } + // the contenteditable field: its text is its value, owned like a + // textarea's — synced when not being edited, children never diffed + if (tag === 'COINOS-TEXT') { + if (document.activeElement !== a && a.value !== b.value) a.value = b.value; + return; + } // innerHTML-authored subtrees (svg icons, QRs): compare source, not nodes if (b._html != null) { if (a._html !== b._html) a.innerHTML = b._html; @@ -724,7 +734,7 @@ function renderInner() { // payment push, an SP scan) can't kick the user out of a field they're editing. const a = document.activeElement; let fpath = null, selStart = null, selEnd = null; - if (a && root.contains(a) && /^(INPUT|SELECT|TEXTAREA)$/.test(a.tagName)) { + if (a && root.contains(a) && /^(INPUT|SELECT|TEXTAREA|COINOS-TEXT)$/.test(a.tagName)) { fpath = focusPath(a); try { selStart = a.selectionStart; selEnd = a.selectionEnd; } catch {} } @@ -789,7 +799,7 @@ function renderInner() { try { document.documentElement.classList.toggle('no-scroll', !!ui.lightbox); } catch {} if (fpath) { const el = nodeAtPath(fpath); - if (el && el !== a && /^(INPUT|SELECT|TEXTAREA)$/.test(el.tagName)) { + if (el && el !== a && /^(INPUT|SELECT|TEXTAREA|COINOS-TEXT)$/.test(el.tagName)) { try { el.focus({ preventScroll: true }); if (selStart != null && el.setSelectionRange) el.setSelectionRange(selStart, selEnd); } catch {} } } |
