aboutsummaryrefslogtreecommitdiffstats
path: root/src/rich-text.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-09 16:13:58 -0700
committerAdam Soltys <[email protected]>2026-10-09 16:13:58 -0700
commit52612bc8bc4c9ea18b0520fc23c98094f304cc05 (patch)
treefc4241766ccb9ab6c9676baa34613df22796f44c /src/rich-text.js
parent3dbd82cafb0fbada4270525bec607ba32645d26b (diff)
downloadcoinosv3-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.js38
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));