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.
- package/client/cursor-highlights.js +51 -40
- package/client/cursor-sync.js +4 -2
- package/client/simpleton-sync.js +3 -3
- package/package.json +1 -1
- package/server.js +14 -7
|
@@ -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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
//
|
|
59
|
-
var
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
//
|
|
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 (!
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
var
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/client/cursor-sync.js
CHANGED
|
@@ -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
|
},
|
package/client/simpleton-sync.js
CHANGED
|
@@ -106,7 +106,7 @@ function simpleton_client(url, {
|
|
|
106
106
|
get_patches,
|
|
107
107
|
get_state,
|
|
108
108
|
content_type,
|
|
109
|
-
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: { ...
|
|
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
|
-
...
|
|
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
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,
|
|
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
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
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)
|