braid-text 0.3.27 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -343,7 +343,8 @@ function cursor_highlights(textarea, url, options) {
343
343
  if (!ranges.length) { hl.remove(id); continue }
344
344
  hl.set(id, ranges.map(r => ({
345
345
  from: r.from, to: r.to,
346
- color: r.from === r.to ? peer_color(id) : peer_bg_color(id)
346
+ color: peer_color(id),
347
+ bg_color: peer_bg_color(id)
347
348
  })))
348
349
  }
349
350
  hl.render()
@@ -354,16 +355,36 @@ function cursor_highlights(textarea, url, options) {
354
355
  if (destroyed) client.destroy()
355
356
  })
356
357
 
358
+ // Track the anchor (non-moving end) of selections. We record the cursor
359
+ // position whenever the selection is collapsed; when it later expands,
360
+ // the anchor tells us which end the caret is on. This works for both
361
+ // mouse drags and keyboard selections, and avoids relying on
362
+ // selectionDirection which returns "none" for mouse drags on macOS.
363
+ var anchor = null
364
+
365
+ function get_selection_range() {
366
+ var start = textarea.selectionStart
367
+ var end = textarea.selectionEnd
368
+ if (textarea.selectionDirection === 'backward')
369
+ return [end, start]
370
+ if (anchor === start || anchor === end)
371
+ return [anchor, (anchor === start) ? end : start]
372
+ return [start, end]
373
+ }
374
+
357
375
  function on_selectionchange() {
358
376
  if (applying_remote) return
359
377
  if (document.activeElement !== textarea) return
360
- if (client) client.set(textarea.selectionStart, textarea.selectionEnd)
378
+ var start = textarea.selectionStart
379
+ var end = textarea.selectionEnd
380
+ if (start === end) anchor = start
381
+ if (client) client.set(...get_selection_range())
361
382
  }
362
383
  document.addEventListener('selectionchange', on_selectionchange)
363
384
 
364
385
  // Show own cursor when blurred, hide when focused
365
386
  function on_blur() {
366
- if (client) client.set(textarea.selectionStart, textarea.selectionEnd)
387
+ if (client) client.set(...get_selection_range())
367
388
  }
368
389
  function on_focus() {
369
390
  hl.remove(peer)
@@ -5,7 +5,7 @@
5
5
  <script src="https://unpkg.com/braid-http@~1.3/braid-http-client.js"></script>
6
6
  <script src="/web-utils.js"></script>
7
7
  <script src="/simpleton-sync.js"></script>
8
- <script src="/cursor-highlights.js"></script>
8
+ <script src="/textarea-highlights.js"></script>
9
9
  <script src="/cursor-sync.js"></script>
10
10
  <script>
11
11
 
@@ -4,7 +4,7 @@
4
4
  <script src="https://unpkg.com/braid-http@~1.3/braid-http-client.js"></script>
5
5
  <script src="/web-utils.js"></script>
6
6
  <script src="/simpleton-sync.js"></script>
7
- <script src="/cursor-highlights.js"></script>
7
+ <script src="/textarea-highlights.js"></script>
8
8
  <script src="/cursor-sync.js"></script>
9
9
  <body>
10
10
  <div id="markdown_container" style="max-width: 750px; padding: 10px 60px"></div>
@@ -447,14 +447,36 @@ function simpleton_client(url, {
447
447
  var min_len = Math.min(old_str.length, new_str.length)
448
448
  while (prefix_len < min_len && old_str[prefix_len] === new_str[prefix_len]) prefix_len++
449
449
 
450
+ // Don't split a surrogate pair: if prefix ends on a low surrogate,
451
+ // the preceding high surrogate only matched by coincidence (same
452
+ // Unicode block), so back up to include the whole pair in the diff.
453
+ if (prefix_len > 0 && is_low_surrogate(old_str, prefix_len))
454
+ prefix_len--
455
+
450
456
  // Find common suffix length (from what remains after prefix)
451
457
  var suffix_len = 0
452
458
  min_len -= prefix_len
453
459
  while (suffix_len < min_len && old_str[old_str.length - suffix_len - 1] === new_str[new_str.length - suffix_len - 1]) suffix_len++
454
460
 
461
+ // Same guard for suffixes: if the range end (old_str.length - suffix_len)
462
+ // lands on a low surrogate, the suffix consumed it without its high
463
+ // surrogate, so back up.
464
+ if (suffix_len > 0 && is_low_surrogate(old_str, old_str.length - suffix_len))
465
+ suffix_len--
466
+
455
467
  return {range: [prefix_len, old_str.length - suffix_len], content: new_str.slice(prefix_len, new_str.length - suffix_len)}
456
468
  }
457
469
 
470
+ function is_low_surrogate(str, i) {
471
+ var c = str.charCodeAt(i)
472
+ return c >= 0xdc00 && c <= 0xdfff
473
+ }
474
+
475
+ function is_high_surrogate(str, i) {
476
+ var c = str.charCodeAt(i)
477
+ return c >= 0xd800 && c <= 0xdbff
478
+ }
479
+
458
480
  // ── apply_patches ───────────────────────────────────────────────────
459
481
  // Applies patches to a string, tracking cumulative offset.
460
482
  // Used when on_patches is not provided, to update
@@ -8,7 +8,7 @@
8
8
  //
9
9
  // Usage:
10
10
  // var hl = textarea_highlights(textarea)
11
- // hl.set('peer-1', [{ from: 5, to: 10, color: 'rgba(97,175,239,0.25)' }])
11
+ // hl.set('peer-1', [{ from: 5, to: 10, color: '#61afef', bg_color: 'rgba(97,175,239,0.25)' }])
12
12
  // hl.render()
13
13
  // hl.remove('peer-1')
14
14
  // hl.destroy()
@@ -134,38 +134,33 @@ function textarea_highlights(textarea) {
134
134
  }
135
135
 
136
136
  function build_html(text, highlights) {
137
- var cursors = highlights.filter(h => h.from === h.to)
138
- var sels = highlights.filter(h => h.from !== h.to)
137
+ var items = highlights.map(h => ({
138
+ ...h,
139
+ start: Math.min(h.from, h.to),
140
+ end: Math.max(h.from, h.to)
141
+ }))
139
142
 
140
- var items = []
141
- for (var s of sels)
142
- items.push({ type: 'selection', start: s.from, end: s.to, color: s.color })
143
- for (var c of cursors)
144
- items.push({ type: 'cursor', pos: c.from, color: c.color })
145
-
146
- items.sort((a, b) => {
147
- var pa = a.type === 'cursor' ? a.pos : a.start
148
- var pb = b.type === 'cursor' ? b.pos : b.start
149
- return pa - pb
150
- })
143
+ items.sort((a, b) => a.start - b.start)
151
144
 
152
145
  var result = ''
153
146
  var last = 0
154
147
 
155
148
  for (var item of items) {
156
- if (item.type === 'selection') {
157
- if (item.start < last) continue
158
- result += escape_html(text.substring(last, item.start))
159
- var sel_text = text.substring(item.start, item.end)
160
- var sel_html = escape_html(sel_text).replace(/\n/g, ' \n')
161
- result += '<span class="sel" style="background-color:' + item.color + ';">' + sel_html + '</span>'
162
- last = item.end
149
+ if (item.start < last) continue
150
+ result += escape_html(text.substring(last, item.start))
151
+
152
+ var cursor_html = '<span class="cursor" style="--cursor-color:' + item.color + ';"></span>'
153
+
154
+ if (item.start === item.end) {
155
+ result += cursor_html
163
156
  } else {
164
- if (item.pos < last) continue
165
- result += escape_html(text.substring(last, item.pos))
166
- result += '<span class="cursor" style="--cursor-color:' + item.color + ';"></span>'
167
- last = item.pos
157
+ var sel_html = escape_html(text.substring(item.start, item.end)).replace(/\n/g, ' \n')
158
+ var before = item.to === item.start ? cursor_html : ''
159
+ var after = item.to === item.start ? '' : cursor_html
160
+ result += '<span class="sel" style="background-color:' + item.bg_color + ';">' + before + sel_html + '</span>' + after
168
161
  }
162
+
163
+ last = item.end
169
164
  }
170
165
 
171
166
  result += escape_html(text.substring(last))
@@ -191,9 +186,9 @@ function textarea_highlights(textarea) {
191
186
  // Render each layer
192
187
  for (var id of Object.keys(layer_data)) {
193
188
  var highlights = layer_data[id].map(h => ({
189
+ ...h,
194
190
  from: Math.min(h.from, len),
195
191
  to: Math.min(h.to, len),
196
- color: h.color
197
192
  }))
198
193
 
199
194
  if (!layer_divs[id]) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braid-text",
3
- "version": "0.3.27",
3
+ "version": "0.5.3",
4
4
  "description": "Library for collaborative text over http using braid.",
5
5
  "author": "Braid Working Group",
6
6
  "repository": "braid-org/braid-text",
@@ -16,7 +16,6 @@
16
16
  "simpleton-client.js",
17
17
  "client/simpleton-sync.js",
18
18
  "client/cursor-sync.js",
19
- "client/cursor-highlights.js",
20
19
  "client/textarea-highlights.js",
21
20
  "client/web-utils.js",
22
21
  "README.md",
@@ -27,5 +26,13 @@
27
26
  "dependencies": {
28
27
  "@braid.org/diamond-types-node": "^2.0.1",
29
28
  "braid-http": "^1.3.106"
29
+ },
30
+ "peerDependencies": {
31
+ "yjs": "^13.6.0"
32
+ },
33
+ "peerDependenciesMeta": {
34
+ "yjs": {
35
+ "optional": true
36
+ }
30
37
  }
31
38
  }
package/server-demo.js CHANGED
@@ -21,7 +21,7 @@ var server = require("http").createServer(async (req, res) => {
21
21
  }
22
22
 
23
23
  if (req.url === '/simpleton-sync.js' || req.url === '/web-utils.js'
24
- || req.url === '/cursor-highlights.js' || req.url === '/cursor-sync.js') {
24
+ || req.url === '/textarea-highlights.js' || req.url === '/cursor-sync.js') {
25
25
  res.writeHead(200, { "Content-Type": "text/javascript", "Cache-Control": "no-cache" })
26
26
  require("fs").createReadStream("./client" + req.url).pipe(res)
27
27
  return