From 52612bc8bc4c9ea18b0520fc23c98094f304cc05 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Fri, 9 Oct 2026 16:13:58 -0700 Subject: @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
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 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3 --- src/rich-text.js | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) (limited to 'src/rich-text.js') 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
s, so that is all this walks. +// (node, offset) for a text position, counted the way textOf counts: text, +//
s, and the line break a new block (
,

) stands for — Chrome +// wraps lines in

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)); -- cgit v1.2.3