braid-text 0.3.23 → 0.3.25

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.
@@ -1,7 +1,5 @@
1
1
  // cursor-highlights.js — Render colored cursors and selections behind a <textarea>
2
2
  //
3
- // No dependencies. Pure DOM/CSS.
4
- //
5
3
  // Usage:
6
4
  // var hl = textarea_highlights(textarea)
7
5
  // hl.set('peer-1', [{ from: 5, to: 10, color: 'rgba(97,175,239,0.25)' }])
@@ -17,13 +15,13 @@ function textarea_highlights(textarea) {
17
15
  style.textContent = `
18
16
  .textarea-hl-backdrop {
19
17
  position: absolute;
20
- top: 0; left: 0; right: 0; bottom: 0;
21
18
  white-space: pre-wrap;
22
19
  word-wrap: break-word;
23
20
  overflow-y: auto;
24
21
  pointer-events: none;
25
22
  color: transparent;
26
23
  z-index: 1;
24
+ box-sizing: border-box;
27
25
  scrollbar-width: none;
28
26
  -ms-overflow-style: none;
29
27
  }
@@ -35,45 +33,34 @@ function textarea_highlights(textarea) {
35
33
  box-decoration-break: clone;
36
34
  }
37
35
  .textarea-hl-backdrop .cursor {
38
- position: relative;
39
- display: inline-block;
40
- width: 0;
41
- height: 1em;
42
- z-index: 10;
43
- }
44
- .textarea-hl-backdrop .cursor::before {
45
- content: '';
46
- position: absolute;
47
- left: -1px;
48
- top: 0;
49
- bottom: 0;
50
- width: 2px;
51
- background-color: var(--cursor-color, #ff5722);
52
- z-index: 10;
36
+ border-left: 2px solid var(--cursor-color, #ff5722);
37
+ margin-left: -1px;
38
+ margin-right: -1px;
53
39
  }
54
40
  `
55
41
  document.head.appendChild(style)
56
42
  }
57
43
 
58
- // Ensure textarea is wrapped in a position:relative container
59
- var wrap = textarea.parentElement
60
- if (getComputedStyle(wrap).position === 'static')
61
- wrap.style.position = 'relative'
44
+ // Save original styles so we can restore on destroy
45
+ var original_bg = textarea.style.backgroundColor
46
+ var original_position = textarea.style.position
47
+ var original_zIndex = textarea.style.zIndex
62
48
 
63
- // Move textarea's background to the wrapper so backdrops show through
49
+ // Read the textarea's background color before we make it transparent.
50
+ // Walk up the DOM if the textarea itself is transparent.
64
51
  var bg = getComputedStyle(textarea).backgroundColor
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
- }
52
+ if (!bg || bg === 'rgba(0, 0, 0, 0)') {
53
+ var el = textarea.parentElement
54
+ while (el) {
55
+ var elBg = getComputedStyle(el).backgroundColor
56
+ if (elBg && elBg !== 'rgba(0, 0, 0, 0)') { bg = elBg; break }
57
+ el = el.parentElement
74
58
  }
75
- wrap.style.backgroundColor = bg || 'white'
76
59
  }
60
+ bg = bg || 'white'
61
+
62
+ // Make textarea transparent so backdrops show through.
63
+ // position:relative + z-index puts the textarea text above the backdrops.
77
64
  textarea.style.backgroundColor = 'transparent'
78
65
  textarea.style.position = 'relative'
79
66
  textarea.style.zIndex = '2'
@@ -95,7 +82,6 @@ function textarea_highlights(textarea) {
95
82
  var bg_height = test_span.getBoundingClientRect().height
96
83
  document.body.removeChild(test_div)
97
84
  var sel_pad = (line_height - bg_height) / 2
98
- wrap.style.setProperty('--sel-pad', sel_pad + 'px')
99
85
 
100
86
  // State
101
87
  var layer_data = {} // layer_id -> [{ from, to, color }]
@@ -110,16 +96,29 @@ function textarea_highlights(textarea) {
110
96
  }
111
97
  textarea.addEventListener('scroll', sync_scroll)
112
98
 
99
+ // Re-render when textarea resizes (user drag, window resize, CSS change)
100
+ var resize_observer = new ResizeObserver(do_render)
101
+ resize_observer.observe(textarea)
102
+
113
103
  // Build a backdrop style string matching the textarea's font/padding/border
114
104
  function backdrop_style() {
115
105
  var cs = getComputedStyle(textarea)
106
+ var bw = parseFloat(cs.borderLeftWidth) + parseFloat(cs.borderRightWidth)
107
+ var bh = parseFloat(cs.borderTopWidth) + parseFloat(cs.borderBottomWidth)
108
+ // Use clientWidth/clientHeight (content + padding, excludes scrollbar)
109
+ // plus border, so the backdrop's content area matches the textarea's
110
+ // even when the textarea reserves space for a scrollbar.
116
111
  return 'font-family:' + cs.fontFamily + ';' +
117
112
  'font-size:' + cs.fontSize + ';' +
118
113
  'line-height:' + cs.lineHeight + ';' +
119
114
  'padding:' + cs.paddingTop + ' ' + cs.paddingRight + ' ' +
120
115
  cs.paddingBottom + ' ' + cs.paddingLeft + ';' +
121
116
  'border:' + cs.borderTopWidth + ' solid transparent;' +
122
- 'border-radius:' + cs.borderRadius + ';'
117
+ 'border-radius:' + cs.borderRadius + ';' +
118
+ 'width:' + (textarea.clientWidth + bw) + 'px;' +
119
+ 'height:' + (textarea.clientHeight + bh) + 'px;' +
120
+ '--sel-pad:' + sel_pad + 'px;' +
121
+ 'background-color:' + bg + ';'
123
122
  }
124
123
 
125
124
  function escape_html(text) {
@@ -192,13 +191,17 @@ function textarea_highlights(textarea) {
192
191
  }))
193
192
 
194
193
  if (!layer_divs[id]) {
194
+ // Insert backdrop as previous sibling of textarea.
195
+ // position:absolute takes it out of flow so it doesn't
196
+ // affect layout. Without top/left set, it naturally sits
197
+ // at the textarea's position.
195
198
  var div = document.createElement('div')
196
199
  div.className = 'textarea-hl-backdrop'
197
- wrap.insertBefore(div, textarea)
200
+ textarea.parentElement.insertBefore(div, textarea)
198
201
  layer_divs[id] = div
199
202
  }
200
203
 
201
- // Font/padding/border are set inline to match textarea;
204
+ // Font/padding/border/size are set inline to match textarea;
202
205
  // positioning/pointer-events/etc come from the CSS class.
203
206
  layer_divs[id].style.cssText = style_str
204
207
 
@@ -262,9 +265,14 @@ function textarea_highlights(textarea) {
262
265
  textarea.removeEventListener('scroll', sync_scroll)
263
266
  textarea.removeEventListener('blur', on_blur)
264
267
  textarea.removeEventListener('focus', on_focus)
268
+ resize_observer.disconnect()
265
269
  for (var div of Object.values(layer_divs)) div.remove()
266
270
  layer_data = {}
267
271
  layer_divs = {}
272
+ // Restore textarea styles
273
+ textarea.style.backgroundColor = original_bg
274
+ textarea.style.position = original_position
275
+ textarea.style.zIndex = original_zIndex
268
276
  }
269
277
  }
270
278
  }
@@ -297,7 +305,7 @@ function peer_bg_color(peer_id) {
297
305
  // cursors.on_edit(patches) // call after local edit; patches optional
298
306
  // cursors.destroy()
299
307
  //
300
- function cursor_highlights(textarea, url) {
308
+ function cursor_highlights(textarea, url, options) {
301
309
  var peer = Math.random().toString(36).slice(2)
302
310
  var hl = textarea_highlights(textarea)
303
311
  var applying_remote = false
@@ -307,6 +315,7 @@ function cursor_highlights(textarea, url) {
307
315
 
308
316
  cursor_client(url, {
309
317
  peer,
318
+ headers: options?.headers,
310
319
  get_text: () => textarea.value,
311
320
  on_change: (sels) => {
312
321
  for (var [id, ranges] of Object.entries(sels)) {
@@ -324,11 +333,12 @@ function cursor_highlights(textarea, url) {
324
333
  if (destroyed) client.destroy()
325
334
  })
326
335
 
327
- document.addEventListener('selectionchange', function() {
336
+ function on_selectionchange() {
328
337
  if (applying_remote) return
329
338
  if (document.activeElement !== textarea) return
330
339
  if (client) client.set(textarea.selectionStart, textarea.selectionEnd)
331
- })
340
+ }
341
+ document.addEventListener('selectionchange', on_selectionchange)
332
342
 
333
343
  return {
334
344
  online: function() {
@@ -356,6 +366,7 @@ function cursor_highlights(textarea, url) {
356
366
 
357
367
  destroy: function() {
358
368
  destroyed = true
369
+ document.removeEventListener('selectionchange', on_selectionchange)
359
370
  if (client) client.destroy()
360
371
  hl.destroy()
361
372
  }
@@ -15,12 +15,12 @@
15
15
  // cursors.changed(patches)
16
16
  // cursors.destroy()
17
17
  //
18
- async function cursor_client(url, { peer, get_text, on_change }) {
18
+ async function cursor_client(url, { peer, get_text, on_change, headers: custom_headers }) {
19
19
  // --- feature detection: HEAD probe ---
20
20
  try {
21
21
  var head_res = await braid_fetch(url, {
22
22
  method: 'HEAD',
23
- headers: { 'Accept': 'application/text-cursors+json' }
23
+ headers: { ...custom_headers, 'Accept': 'application/text-cursors+json' }
24
24
  })
25
25
  var ct = head_res.headers.get('content-type') || ''
26
26
  if (!ct.includes('application/text-cursors+json')) return null
@@ -124,6 +124,7 @@ async function cursor_client(url, { peer, get_text, on_change }) {
124
124
  braid_fetch(url, {
125
125
  method: 'PUT',
126
126
  headers: {
127
+ ...custom_headers,
127
128
  'Content-Type': 'application/text-cursors+json',
128
129
  Peer: peer,
129
130
  'Content-Range': 'json [' + JSON.stringify(peer) + ']',
@@ -155,6 +156,7 @@ async function cursor_client(url, { peer, get_text, on_change }) {
155
156
  }},
156
157
  peer,
157
158
  headers: {
159
+ ...custom_headers,
158
160
  Accept: 'application/text-cursors+json',
159
161
  Heartbeats: '10',
160
162
  },
@@ -106,7 +106,7 @@ function simpleton_client(url, {
106
106
  get_patches,
107
107
  get_state,
108
108
  content_type,
109
- headers: user_headers, // The user can pass in custom headers
109
+ headers: custom_headers, // The user can pass in custom headers
110
110
  // that are forwared into the fetch
111
111
  on_error,
112
112
  on_res,
@@ -144,7 +144,7 @@ function simpleton_client(url, {
144
144
  retry: () => true,
145
145
  parents: () => client_version.length ? client_version : null,
146
146
  onSubscriptionStatus: status => on_online && on_online(status.online),
147
- headers: { ...user_headers,
147
+ headers: { ...custom_headers,
148
148
  "Merge-Type": "simpleton",
149
149
  ...content_type && {Accept: content_type} },
150
150
  }).then(res => {
@@ -332,7 +332,7 @@ function simpleton_client(url, {
332
332
  peer, version, parents, patches,
333
333
  retry: (res) => res.status !== 550,
334
334
  headers: {
335
- ...user_headers,
335
+ ...custom_headers,
336
336
  "Merge-Type": "simpleton",
337
337
  ...send_digests && {
338
338
  "Repr-Digest": await get_digest(client_state) },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braid-text",
3
- "version": "0.3.23",
3
+ "version": "0.3.25",
4
4
  "description": "Library for collaborative text over http using braid.",
5
5
  "author": "Braid Working Group",
6
6
  "repository": "braid-org/braid-text",
package/server.js CHANGED
@@ -578,7 +578,9 @@ function create_braid_text() {
578
578
 
579
579
  res.setHeader("Version", get_current_version())
580
580
 
581
- options.put_cb(options.key, resource.val, {old_val, patches: put_patches, version: resource.version, parents: old_version})
581
+ options.put_cb(options.key, resource.val,
582
+ {old_val, patches: put_patches,
583
+ version: resource.version, parents: old_version})
582
584
  } catch (e) {
583
585
  console.log(`${req.method} ERROR: ${e.stack}`)
584
586
  return done_my_turn(500, "The server failed to apply this version. The error generated was: " + e)
@@ -3114,12 +3116,17 @@ async function handle_cursors(resource, req, res) {
3114
3116
  res.sendUpdate({ body: JSON.stringify(cursors.snapshot()) })
3115
3117
  }
3116
3118
  } else if (req.method === 'PUT' || req.method === 'POST' || req.method === 'PATCH') {
3117
- var raw_body = await new Promise((resolve, reject) => {
3118
- var chunks = []
3119
- req.on('data', chunk => chunks.push(chunk))
3120
- req.on('end', () => resolve(Buffer.concat(chunks).toString()))
3121
- req.on('error', reject)
3122
- })
3119
+ var raw_body
3120
+ if (req.already_buffered_body != null) {
3121
+ raw_body = req.already_buffered_body.toString()
3122
+ } else {
3123
+ raw_body = await new Promise((resolve, reject) => {
3124
+ var chunks = []
3125
+ req.on('data', chunk => chunks.push(chunk))
3126
+ req.on('end', () => resolve(Buffer.concat(chunks).toString()))
3127
+ req.on('error', reject)
3128
+ })
3129
+ }
3123
3130
  var range = req.headers['content-range']
3124
3131
  if (!range || !range.startsWith('json ')) {
3125
3132
  res.writeHead(400)