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.
- package/client/cursor-highlights.js +88 -40
- package/client/cursor-sync.js +4 -4
- package/client/editor.html +3 -2
- package/client/markdown-editor.html +3 -1
- package/client/simpleton-sync.js +34 -69
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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:
|
|
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
|
-
|
|
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 ---
|
package/client/cursor-sync.js
CHANGED
|
@@ -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
|
|
145
|
-
// Reconnecting —
|
|
146
|
-
//
|
|
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
|
}
|
package/client/editor.html
CHANGED
|
@@ -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: (
|
|
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: (
|
|
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()
|
package/client/simpleton-sync.js
CHANGED
|
@@ -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 (
|
|
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:
|
|
82
|
-
//
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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 (
|
|
362
|
-
//
|
|
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}
|
|
343
|
+
if (!r.ok) throw new Error(`bad http status: ${r.status}`)
|
|
377
344
|
} catch (e) {
|
|
378
|
-
//
|
|
379
|
-
//
|
|
380
|
-
//
|
|
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
|
}
|