diff options
| author | Adam Soltys <[email protected]> | 2026-10-09 16:13:58 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-09 16:13:58 -0700 |
| commit | 52612bc8bc4c9ea18b0520fc23c98094f304cc05 (patch) | |
| tree | fc4241766ccb9ab6c9676baa34613df22796f44c /src/rich-text.js | |
| parent | 3dbd82cafb0fbada4270525bec607ba32645d26b (diff) | |
| download | coinosv3-52612bc8bc4c9ea18b0520fc23c98094f304cc05.tar.gz coinosv3-52612bc8bc4c9ea18b0520fc23c98094f304cc05.zip | |
@mentions: "@" and a few letters in a post, a reply or an edit open a list of people under the box — the ones you follow whose name matches first (at once, from what's here), then the header's people search. Picking one (tap, Tab or Enter) puts nostr:npub… in the text, shown everywhere as @name (NIP-27), and the post or reply goes out with their p tag so they're told; Escape closes. coinos-text's setSelectionRange walks into the <div>s Chrome wraps lines in after an Enter — it used to put any position past the first line at the end of the field. tools/mention-ac-test.js
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
Diffstat (limited to 'src/rich-text.js')
| -rw-r--r-- | src/rich-text.js | 38 |
1 files changed, 26 insertions, 12 deletions
diff --git a/src/rich-text.js b/src/rich-text.js index 7003f5c..b766767 100644 --- a/src/rich-text.js +++ b/src/rich-text.js @@ -46,20 +46,34 @@ function offsetOf(root, node, offset) { return t.length; } -// (node, offset) for a text position; the field is normalised to text nodes -// and <br>s, so that is all this walks. +// (node, offset) for a text position, counted the way textOf counts: text, +// <br>s, and the line break a new block (<div>, <p>) stands for — Chrome +// wraps lines in <div>s once Enter has been pressed, and a walk of the top +// level alone put every later position at the end of the field. function pointAt(root, pos) { - let left = pos; - for (const c of root.childNodes) { - if (c.nodeType === 3) { - if (left <= c.data.length) return [c, left]; - left -= c.data.length; - } else if (c.nodeName === 'BR') { - if (left === 0) return [root, [...root.childNodes].indexOf(c)]; - left -= 1; + let left = pos, out = ''; + const walk = (n) => { + const kids = [...n.childNodes]; + for (let i = 0; i < kids.length; i++) { + const c = kids[i]; + if (c.nodeType === 3) { + if (left <= c.data.length) return [c, left]; + left -= c.data.length; out += c.data; + } else if (c.nodeName === 'BR') { + if (left === 0) return [n, i]; + left -= 1; out += '\n'; + } else if (c.nodeType === 1) { + if (BLOCK.has(c.nodeName) && out && !out.endsWith('\n')) { + if (left === 0) return [n, i]; + left -= 1; out += '\n'; + } + const got = walk(c); + if (got) return got; + } } - } - return [root, root.childNodes.length]; + return null; + }; + return walk(root) || [root, root.childNodes.length]; } const mediaOf = (dt) => [...((dt && dt.files) || [])].filter((f) => /^(image|video)\//.test(f.type)); |
