braid-text 0.3.24 → 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 +49 -39
- package/client/cursor-sync.js +4 -4
- package/client/simpleton-sync.js +3 -3
- package/package.json +1 -1
|
@@ -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
|
}
|
|
@@ -325,11 +333,12 @@ function cursor_highlights(textarea, url, options) {
|
|
|
325
333
|
if (destroyed) client.destroy()
|
|
326
334
|
})
|
|
327
335
|
|
|
328
|
-
|
|
336
|
+
function on_selectionchange() {
|
|
329
337
|
if (applying_remote) return
|
|
330
338
|
if (document.activeElement !== textarea) return
|
|
331
339
|
if (client) client.set(textarea.selectionStart, textarea.selectionEnd)
|
|
332
|
-
}
|
|
340
|
+
}
|
|
341
|
+
document.addEventListener('selectionchange', on_selectionchange)
|
|
333
342
|
|
|
334
343
|
return {
|
|
335
344
|
online: function() {
|
|
@@ -357,6 +366,7 @@ function cursor_highlights(textarea, url, options) {
|
|
|
357
366
|
|
|
358
367
|
destroy: function() {
|
|
359
368
|
destroyed = true
|
|
369
|
+
document.removeEventListener('selectionchange', on_selectionchange)
|
|
360
370
|
if (client) client.destroy()
|
|
361
371
|
hl.destroy()
|
|
362
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, headers:
|
|
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: { ...
|
|
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,7 +124,7 @@ async function cursor_client(url, { peer, get_text, on_change, headers: user_hea
|
|
|
124
124
|
braid_fetch(url, {
|
|
125
125
|
method: 'PUT',
|
|
126
126
|
headers: {
|
|
127
|
-
...
|
|
127
|
+
...custom_headers,
|
|
128
128
|
'Content-Type': 'application/text-cursors+json',
|
|
129
129
|
Peer: peer,
|
|
130
130
|
'Content-Range': 'json [' + JSON.stringify(peer) + ']',
|
|
@@ -156,7 +156,7 @@ async function cursor_client(url, { peer, get_text, on_change, headers: user_hea
|
|
|
156
156
|
}},
|
|
157
157
|
peer,
|
|
158
158
|
headers: {
|
|
159
|
-
...
|
|
159
|
+
...custom_headers,
|
|
160
160
|
Accept: 'application/text-cursors+json',
|
|
161
161
|
Heartbeats: '10',
|
|
162
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) },
|