braid-text 0.3.8 → 0.3.10

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.
@@ -62,8 +62,18 @@ function textarea_highlights(textarea) {
62
62
 
63
63
  // Move textarea's background to the wrapper so backdrops show through
64
64
  var bg = getComputedStyle(textarea).backgroundColor
65
- if (!wrap.style.backgroundColor)
66
- wrap.style.backgroundColor = (!bg || bg === 'rgba(0, 0, 0, 0)') ? 'white' : bg
65
+ if (!wrap.style.backgroundColor) {
66
+ if (!bg || bg === 'rgba(0, 0, 0, 0)') {
67
+ // Walk up the DOM to find the effective background
68
+ var el = wrap
69
+ while (el) {
70
+ var elBg = getComputedStyle(el).backgroundColor
71
+ if (elBg && elBg !== 'rgba(0, 0, 0, 0)') { bg = elBg; break }
72
+ el = el.parentElement
73
+ }
74
+ }
75
+ wrap.style.backgroundColor = bg || 'white'
76
+ }
67
77
  textarea.style.backgroundColor = 'transparent'
68
78
  textarea.style.position = 'relative'
69
79
  textarea.style.zIndex = '2'
@@ -158,6 +168,77 @@ function textarea_highlights(textarea) {
158
168
  return result
159
169
  }
160
170
 
171
+ // --- render implementation ---
172
+
173
+ function do_render() {
174
+ var text = textarea.value
175
+ var len = text.length
176
+ var style_str = backdrop_style()
177
+
178
+ // Remove divs for layers that no longer exist
179
+ for (var id of Object.keys(layer_divs)) {
180
+ if (!layer_data[id]) {
181
+ layer_divs[id].remove()
182
+ delete layer_divs[id]
183
+ }
184
+ }
185
+
186
+ // Render each layer
187
+ for (var id of Object.keys(layer_data)) {
188
+ var highlights = layer_data[id].map(h => ({
189
+ from: Math.min(h.from, len),
190
+ to: Math.min(h.to, len),
191
+ color: h.color
192
+ }))
193
+
194
+ if (!layer_divs[id]) {
195
+ var div = document.createElement('div')
196
+ div.className = 'textarea-hl-backdrop'
197
+ wrap.insertBefore(div, textarea)
198
+ layer_divs[id] = div
199
+ }
200
+
201
+ // Font/padding/border are set inline to match textarea;
202
+ // positioning/pointer-events/etc come from the CSS class.
203
+ layer_divs[id].style.cssText = style_str
204
+
205
+ layer_divs[id].innerHTML = build_html(text, highlights)
206
+ layer_divs[id].scrollTop = textarea.scrollTop
207
+ layer_divs[id].scrollLeft = textarea.scrollLeft
208
+ }
209
+ }
210
+
211
+ // --- show local cursor/selection when textarea is not focused ---
212
+
213
+ var local_id = '__local__'
214
+
215
+ function on_blur() {
216
+ var from = textarea.selectionStart
217
+ var to = textarea.selectionEnd
218
+ var color = getComputedStyle(textarea).caretColor
219
+ if (!color || color === 'auto') color = getComputedStyle(textarea).color
220
+ if (from === to) {
221
+ layer_data[local_id] = [{ from, to, color: color }]
222
+ } else {
223
+ var match = color.match(/(\d+),\s*(\d+),\s*(\d+)/)
224
+ var sel_color = match ? 'rgba(' + match[1] + ', ' + match[2] + ', ' + match[3] + ', 0.3)' : color
225
+ layer_data[local_id] = [{ from, to, color: sel_color }]
226
+ }
227
+ do_render()
228
+ }
229
+
230
+ function on_focus() {
231
+ delete layer_data[local_id]
232
+ if (layer_divs[local_id]) {
233
+ layer_divs[local_id].remove()
234
+ delete layer_divs[local_id]
235
+ }
236
+ do_render()
237
+ }
238
+
239
+ textarea.addEventListener('blur', on_blur)
240
+ textarea.addEventListener('focus', on_focus)
241
+
161
242
  return {
162
243
  set: function(layer_id, highlights) {
163
244
  layer_data[layer_id] = highlights
@@ -171,43 +252,7 @@ function textarea_highlights(textarea) {
171
252
  }
172
253
  },
173
254
 
174
- render: function() {
175
- var text = textarea.value
176
- var len = text.length
177
- var style_str = backdrop_style()
178
-
179
- // Remove divs for layers that no longer exist
180
- for (var id of Object.keys(layer_divs)) {
181
- if (!layer_data[id]) {
182
- layer_divs[id].remove()
183
- delete layer_divs[id]
184
- }
185
- }
186
-
187
- // Render each layer
188
- for (var id of Object.keys(layer_data)) {
189
- var highlights = layer_data[id].map(h => ({
190
- from: Math.min(h.from, len),
191
- to: Math.min(h.to, len),
192
- color: h.color
193
- }))
194
-
195
- if (!layer_divs[id]) {
196
- var div = document.createElement('div')
197
- div.className = 'textarea-hl-backdrop'
198
- wrap.insertBefore(div, textarea)
199
- layer_divs[id] = div
200
- }
201
-
202
- // Font/padding/border are set inline to match textarea;
203
- // positioning/pointer-events/etc come from the CSS class.
204
- layer_divs[id].style.cssText = style_str
205
-
206
- layer_divs[id].innerHTML = build_html(text, highlights)
207
- layer_divs[id].scrollTop = textarea.scrollTop
208
- layer_divs[id].scrollLeft = textarea.scrollLeft
209
- }
210
- },
255
+ render: do_render,
211
256
 
212
257
  layers: function() {
213
258
  return Object.keys(layer_data)
@@ -215,6 +260,8 @@ function textarea_highlights(textarea) {
215
260
 
216
261
  destroy: function() {
217
262
  textarea.removeEventListener('scroll', sync_scroll)
263
+ textarea.removeEventListener('blur', on_blur)
264
+ textarea.removeEventListener('focus', on_focus)
218
265
  for (var div of Object.values(layer_divs)) div.remove()
219
266
  layer_data = {}
220
267
  layer_divs = {}
@@ -238,7 +285,8 @@ function peer_bg_color(peer_id) {
238
285
  var r = parseInt(c.slice(1, 3), 16)
239
286
  var g = parseInt(c.slice(3, 5), 16)
240
287
  var b = parseInt(c.slice(5, 7), 16)
241
- return `rgba(${r}, ${g}, ${b}, 0.25)`
288
+ var dark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
289
+ return `rgba(${r}, ${g}, ${b}, ${dark ? 0.4 : 0.25})`
242
290
  }
243
291
 
244
292
  // --- High-level wrapper ---
@@ -141,13 +141,13 @@ async function cursor_client(url, { peer, get_text, on_change }) {
141
141
  braid_fetch(url, {
142
142
  subscribe: true,
143
143
  retry: { onRes: function() {
144
- if (connected_before && online) {
145
- // Reconnecting — go offline to clear stale cursors.
146
- // The application will call online() again when text is ready.
144
+ if (connected_before) {
145
+ // Reconnecting — clear stale cursors; fresh snapshot incoming.
146
+ // Stay in current online state so the snapshot is processed
147
+ // immediately (the text subscription manages online/offline).
147
148
  var changed = {}
148
149
  for (var id of Object.keys(selections)) changed[id] = []
149
150
  selections = {}
150
- online = false
151
151
  pending = null
152
152
  if (on_change && Object.keys(changed).length) on_change(changed)
153
153
  }
@@ -1,5 +1,5 @@
1
1
  <body style="margin: 0px; padding: 0px; box-sizing: border-box">
2
- <textarea id="the_editor" style="width: 100%; height: 100%;"></textarea>
2
+ <textarea id="the_editor" style="width: 100%; height: 100%;" disabled></textarea>
3
3
  </body>
4
4
  <script src="https://braid.org/code/myers-diff1.js"></script>
5
5
  <script src="https://unpkg.com/braid-http@~1.3/braid-http-client.js"></script>
@@ -12,8 +12,9 @@
12
12
  var cursors = cursor_highlights(the_editor, location.pathname)
13
13
 
14
14
  var simpleton = simpleton_client(location.pathname, {
15
- on_online: ({online}) => { online ? cursors.online() : cursors.offline() },
15
+ on_online: (online) => { online ? cursors.online() : cursors.offline() },
16
16
  on_patches: (patches) => {
17
+ the_editor.disabled = false
17
18
  apply_patches_and_update_selection(the_editor, patches)
18
19
  cursors.on_patches(patches)
19
20
  },
@@ -18,6 +18,7 @@
18
18
  ">
19
19
  <textarea
20
20
  id="the_editor"
21
+ disabled
21
22
  style="
22
23
  width: 100%;
23
24
  height: 100%;
@@ -48,8 +49,9 @@ var render_delay = 100
48
49
  var cursors = cursor_highlights(the_editor, location.pathname)
49
50
 
50
51
  var simpleton = simpleton_client(location.pathname, {
51
- on_online: ({online}) => { online ? cursors.online() : cursors.offline() },
52
+ on_online: (online) => { online ? cursors.online() : cursors.offline() },
52
53
  on_patches: (patches) => {
54
+ the_editor.disabled = false
53
55
  apply_patches_and_update_selection(the_editor, patches)
54
56
  cursors.on_patches(patches)
55
57
  update_markdown_later()
@@ -45,15 +45,6 @@
45
45
  // get_state: () => current_state
46
46
  // returns the current state (e.g., textarea.value)
47
47
  //
48
- // [DEPRECATED] apply_remote_update: ({patches, state}) => {...}
49
- // this is for incoming changes;
50
- // one of these will be non-null,
51
- // and can be applied to the current state.
52
- //
53
- // [DEPRECATED] generate_local_diff_update: (client_state) => {...}
54
- // this is to generate outgoing changes,
55
- // and if there are changes, returns { patches, new_state }
56
- //
57
48
  // content_type: used for Accept and Content-Type headers
58
49
  //
59
50
  // returns { changed, abort }
@@ -75,18 +66,12 @@
75
66
  //
76
67
  // PUT requests:
77
68
  // retry: (res) => res.status !== 550 — retry all errors EXCEPT
78
- // HTTP 550 (permanent rejection by the server). This means:
69
+ // HTTP 550 (Repr-Digest mismatch, meaning client is out of sync).
70
+ // This means:
79
71
  // - Connection failure: retried with backoff
80
- // - HTTP 408, 429, 500, 502, 503, 504, etc.: retried
81
- // - HTTP 550: give up, throw error, outstanding_changes stays
82
- // incremented (potential throttle leak see note below)
83
- // - HTTP 401, 403: retried by braid_fetch, but the !r.ok check
84
- // throws, which calls on_error and exits the async loop
85
- //
86
- // NOTE: When a PUT permanently fails (550 or !r.ok), outstanding_changes
87
- // is incremented but never decremented. If this happens repeatedly, the
88
- // client will eventually hit max_outstanding_changes and stop sending.
89
- // This is arguably a bug in the JS implementation too.
72
+ // - HTTP 401, 403, 408, 429, 500, 502, 503, 504, etc.: retried
73
+ // - HTTP 550: out of sync — stop retrying, throw error. The
74
+ // client must be torn down and restarted from scratch.
90
75
  //
91
76
  // --- Local Edit Absorption ---
92
77
  //
@@ -107,8 +92,6 @@ function simpleton_client(url, {
107
92
  on_state,
108
93
  get_patches,
109
94
  get_state,
110
- apply_remote_update, // DEPRECATED
111
- generate_local_diff_update, // DEPRECATED
112
95
  content_type,
113
96
 
114
97
  on_error,
@@ -127,10 +110,6 @@ function simpleton_client(url, {
127
110
  var throttled_update = null
128
111
  var ac = new AbortController()
129
112
 
130
- // temporary: our old code uses this deprecated api,
131
- // and our old code wants to send digests..
132
- if (apply_remote_update) send_digests = true
133
-
134
113
  // ── Subscription (GET) ──────────────────────────────────────────────
135
114
  //
136
115
  // Opens a long-lived GET subscription with retry: () => true, meaning
@@ -148,7 +127,7 @@ function simpleton_client(url, {
148
127
  ...(content_type ? {Accept: content_type} : {}) },
149
128
  subscribe: true,
150
129
  retry: () => true,
151
- onSubscriptionStatus: (status) => { if (on_online) on_online(status) },
130
+ onSubscriptionStatus: (status) => { if (on_online) on_online(status.online) },
152
131
  parents: () => client_version.length ? client_version : null,
153
132
  peer,
154
133
  signal: ac.signal
@@ -211,28 +190,23 @@ function simpleton_client(url, {
211
190
  }
212
191
 
213
192
  // ── Apply the update ────────────────────────────────
214
- if (apply_remote_update) {
215
- // DEPRECATED path
216
- client_state = apply_remote_update(update)
193
+ // Convert initial snapshot body to a patch replacing
194
+ // [0,0] — so initial load follows the same code path
195
+ // as incremental patches.
196
+ var patches = update.patches ||
197
+ [{range: [0, 0], content: update.state}]
198
+ if (on_patches) {
199
+ // EXTERNAL MODE: Apply patches to the UI, then
200
+ // read back the full state. Note: this absorbs
201
+ // any un-flushed local edits into client_state.
202
+ on_patches(patches)
203
+ client_state = get_state()
217
204
  } else {
218
- // Convert initial snapshot body to a patch replacing
219
- // [0,0] so initial load follows the same code path
220
- // as incremental patches.
221
- var patches = update.patches ||
222
- [{range: [0, 0], content: update.state}]
223
- if (on_patches) {
224
- // EXTERNAL MODE: Apply patches to the UI, then
225
- // read back the full state. Note: this absorbs
226
- // any un-flushed local edits into client_state.
227
- on_patches(patches)
228
- client_state = get_state()
229
- } else {
230
- // INTERNAL MODE: Apply patches to our internal
231
- // state only. Local edits in the UI are NOT
232
- // absorbed — they will be captured by the next
233
- // changed() diff.
234
- client_state = apply_patches(client_state, patches)
235
- }
205
+ // INTERNAL MODE: Apply patches to our internal
206
+ // state only. Local edits in the UI are NOT
207
+ // absorbed they will be captured by the next
208
+ // changed() diff.
209
+ client_state = apply_patches(client_state, patches)
236
210
  }
237
211
 
238
212
  // ── Digest verification ─────────────────────────────
@@ -278,18 +252,11 @@ function simpleton_client(url, {
278
252
  // during a PUT round-trip are eventually sent.
279
253
  changed: () => {
280
254
  function get_change() {
281
- if (generate_local_diff_update) {
282
- // DEPRECATED
283
- var update = generate_local_diff_update(client_state)
284
- if (!update) return null
285
- return update
286
- } else {
287
- var new_state = get_state()
288
- if (new_state === client_state) return null
289
- var patches = get_patches ? get_patches(client_state) :
290
- [simple_diff(client_state, new_state)]
291
- return {patches, new_state}
292
- }
255
+ var new_state = get_state()
256
+ if (new_state === client_state) return null
257
+ var patches = get_patches ? get_patches(client_state) :
258
+ [simple_diff(client_state, new_state)]
259
+ return {patches, new_state}
293
260
  }
294
261
 
295
262
  var change = get_change()
@@ -357,9 +324,9 @@ function simpleton_client(url, {
357
324
  // Uses braid_fetch with retry: (res) => res.status !== 550
358
325
  // This means:
359
326
  // - Network failures: retried with backoff
360
- // - HTTP 408, 429, 500, 502, 503, 504: retried
361
- // - HTTP 550 (permanent rejection): give up, throw
362
- // - Other non-ok: throws via !r.ok check below
327
+ // - HTTP 401, 403, 408, 429, 500, 502, 503, 504: retried
328
+ // - HTTP 550 (Repr-Digest mismatch / out of sync):
329
+ // give up, throw client must be re-created
363
330
  outstanding_changes++
364
331
  try {
365
332
  var r = await braid_fetch(url, {
@@ -373,13 +340,11 @@ function simpleton_client(url, {
373
340
  version, parents, patches,
374
341
  peer
375
342
  })
376
- if (!r.ok) throw new Error(`bad http status: ${r.status}${(r.status === 401 || r.status === 403) ? ` (access denied)` : ''}`)
343
+ if (!r.ok) throw new Error(`bad http status: ${r.status}`)
377
344
  } catch (e) {
378
- // On error, notify and exit the loop.
379
- // NOTE: outstanding_changes is NOT decremented here.
380
- // This is arguably a bug — repeated failures will
381
- // eventually hit max_outstanding_changes and stop
382
- // the client from sending any more edits.
345
+ // A 550 means Repr-Digest check failed — we're out
346
+ // of sync. The client must be torn down and
347
+ // re-created from scratch.
383
348
  on_error(e)
384
349
  throw e
385
350
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braid-text",
3
- "version": "0.3.8",
3
+ "version": "0.3.10",
4
4
  "description": "Library for collaborative text over http using braid.",
5
5
  "author": "Braid Working Group",
6
6
  "repository": "braid-org/braid-text",