braid-blob 0.0.78 → 0.0.80
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/README.md +9 -4
- package/img-live.js +102 -53
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -347,21 +347,26 @@ Parameters:
|
|
|
347
347
|
|
|
348
348
|
## Live Image Polyfill
|
|
349
349
|
|
|
350
|
-
A polyfill
|
|
350
|
+
A polyfill for real-time collaborative images. Add `live` to any `<img>` element to keep it synced across all clients, and add `droppable` to let users drag-and-drop or paste new images directly onto it.
|
|
351
351
|
|
|
352
352
|
```html
|
|
353
353
|
<script src="https://unpkg.com/braid-http@~1.3/braid-http-client.js"></script>
|
|
354
354
|
<script src="https://unpkg.com/braid-blob/client.js"></script>
|
|
355
355
|
<script src="https://unpkg.com/braid-blob/img-live.js"></script>
|
|
356
356
|
|
|
357
|
+
<!-- Read-only: stays in sync with the server -->
|
|
357
358
|
<img live src="/blob.png">
|
|
359
|
+
|
|
360
|
+
<!-- Read-write: users can drag-and-drop or paste to update -->
|
|
361
|
+
<img live droppable src="/blob.png">
|
|
358
362
|
```
|
|
359
363
|
|
|
360
|
-
That's it!
|
|
364
|
+
That's it! When any client updates `/blob.png`, all `<img live src="/blob.png">` elements across all clients will update in real-time. With `droppable`, users can drag and drop an image file onto the element, or click it and paste from the clipboard — the new image is uploaded to the server and synced to everyone.
|
|
361
365
|
|
|
362
|
-
|
|
366
|
+
Under the hood, the polyfill:
|
|
363
367
|
- Observes the DOM for `<img live>` elements (added, removed, or attribute changes)
|
|
364
|
-
- Creates a `braid_blob_client` subscription for each
|
|
368
|
+
- Creates a `braid_blob_client` subscription for each unique image URL, shared across elements
|
|
369
|
+
- Appends a cache-busting query parameter (e.g. `?img-live=k7x2f9m`) to ensure the browser doesn't serve stale versions
|
|
365
370
|
- Cleans up subscriptions when images are removed from the DOM
|
|
366
371
|
|
|
367
372
|
## Improving this Package
|
package/img-live.js
CHANGED
|
@@ -3,57 +3,74 @@
|
|
|
3
3
|
|
|
4
4
|
;(function() {
|
|
5
5
|
|
|
6
|
-
var
|
|
6
|
+
var subscriptions = new Map() // base_url -> sub
|
|
7
7
|
|
|
8
8
|
function sync(img) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
9
|
+
if (!img._img_live_base_url) img._img_live_base_url = img.src
|
|
10
|
+
var base_url = img._img_live_base_url
|
|
11
|
+
|
|
12
|
+
var sub = subscriptions.get(base_url)
|
|
13
|
+
if (sub) {
|
|
14
|
+
// Cancel any pending teardown
|
|
15
|
+
clearTimeout(sub.teardown_timer)
|
|
16
|
+
sub.teardown_timer = null
|
|
17
|
+
} else {
|
|
18
|
+
// Create cache-bust helper for this base URL
|
|
19
|
+
var param = 'img-live'
|
|
20
|
+
var u = new URL(base_url)
|
|
21
|
+
while (u.searchParams.has(param)) param = '-' + param
|
|
22
|
+
function cache_bust() {
|
|
23
|
+
u.searchParams.set(param, Math.random().toString(36).slice(2))
|
|
24
|
+
return u.toString()
|
|
25
|
+
}
|
|
20
26
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
var res = await braid_fetch(cache_bust(), {
|
|
27
|
-
method: 'HEAD',
|
|
28
|
-
headers: { "Merge-Type": "aww" },
|
|
29
|
-
subscribe: true,
|
|
30
|
-
retry: () => true,
|
|
31
|
-
signal: ac.signal
|
|
32
|
-
})
|
|
33
|
-
return braid_blob_client(cache_bust(), {
|
|
34
|
-
signal: ac.signal,
|
|
35
|
-
parents: res.version,
|
|
36
|
-
on_update: (body, content_type, version, from_local_update) => {
|
|
37
|
-
if (from_local_update) {
|
|
38
|
-
var blob = new Blob([body], { type: content_type || 'image/png' })
|
|
39
|
-
img.src = URL.createObjectURL(blob)
|
|
40
|
-
} else {
|
|
41
|
-
img.src = ''
|
|
42
|
-
img.src = cache_bust()
|
|
43
|
-
}
|
|
44
|
-
},
|
|
45
|
-
on_delete: () => {
|
|
46
|
-
img.src = ''
|
|
47
|
-
img.src = cache_bust()
|
|
48
|
-
},
|
|
49
|
-
on_error: (error) => {
|
|
50
|
-
console.error('Live image error for', url, error)
|
|
51
|
-
}
|
|
27
|
+
subscriptions.set(base_url, sub = {
|
|
28
|
+
imgs: new Set(),
|
|
29
|
+
ac: new AbortController(),
|
|
30
|
+
current_src: null,
|
|
31
|
+
teardown_timer: null
|
|
52
32
|
})
|
|
53
|
-
|
|
54
|
-
|
|
33
|
+
|
|
34
|
+
function set_src(src) {
|
|
35
|
+
sub.current_src = src
|
|
36
|
+
sub.imgs.forEach(img => img.src = sub.current_src)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
var client_p = (async () => {
|
|
40
|
+
var res = await braid_fetch(cache_bust(), {
|
|
41
|
+
method: 'HEAD',
|
|
42
|
+
headers: { "Merge-Type": "aww" },
|
|
43
|
+
subscribe: true,
|
|
44
|
+
retry: () => true,
|
|
45
|
+
signal: sub.ac.signal
|
|
46
|
+
})
|
|
47
|
+
return braid_blob_client(cache_bust(), {
|
|
48
|
+
signal: sub.ac.signal,
|
|
49
|
+
parents: res.version,
|
|
50
|
+
on_update: (body, content_type, version, from_local_update) =>
|
|
51
|
+
set_src(!from_local_update ? cache_bust() :
|
|
52
|
+
URL.createObjectURL(new Blob(
|
|
53
|
+
[body], { type: content_type || 'image/png' }))),
|
|
54
|
+
on_delete: () => set_src(cache_bust()),
|
|
55
|
+
on_error: (error) =>
|
|
56
|
+
console.error('Live image error for', base_url, error)
|
|
57
|
+
})
|
|
58
|
+
})()
|
|
59
|
+
|
|
60
|
+
sub.update = async (body, content_type) => {
|
|
61
|
+
await (await client_p).update(body, content_type)
|
|
62
|
+
set_src(cache_bust())
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
sub.imgs.add(img)
|
|
67
|
+
|
|
68
|
+
// Immediately set to the most recent known src
|
|
69
|
+
if (sub.current_src) img.src = sub.current_src
|
|
55
70
|
|
|
56
71
|
if (img.hasAttribute('droppable')) {
|
|
72
|
+
if (!img.hasAttribute('tabindex')) img.setAttribute('tabindex', '0')
|
|
73
|
+
|
|
57
74
|
img.addEventListener('dragenter', function() {
|
|
58
75
|
img.style.outline = '3px dashed #007bff'
|
|
59
76
|
img.style.outlineOffset = '3px'
|
|
@@ -78,20 +95,52 @@ function sync(img) {
|
|
|
78
95
|
if (!file || !file.type.startsWith('image/')) return
|
|
79
96
|
|
|
80
97
|
var reader = new FileReader()
|
|
81
|
-
reader.onload =
|
|
82
|
-
await (await client_p).update(reader.result, file.type)
|
|
83
|
-
img.src = cache_bust()
|
|
84
|
-
}
|
|
98
|
+
reader.onload = () => sub.update(reader.result, file.type)
|
|
85
99
|
reader.readAsArrayBuffer(file)
|
|
86
100
|
})
|
|
101
|
+
|
|
102
|
+
img.addEventListener('click', function() {
|
|
103
|
+
img.focus()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
img.addEventListener('focus', function() {
|
|
107
|
+
img.style.outline = '3px dashed #007bff'
|
|
108
|
+
img.style.outlineOffset = '3px'
|
|
109
|
+
|
|
110
|
+
document.addEventListener('paste', on_paste)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
img.addEventListener('blur', function() {
|
|
114
|
+
img.style.outline = ''
|
|
115
|
+
img.style.outlineOffset = ''
|
|
116
|
+
|
|
117
|
+
document.removeEventListener('paste', on_paste)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
function on_paste(e) {
|
|
121
|
+
var items = e.clipboardData.items
|
|
122
|
+
for (var i = 0; i < items.length; i++) {
|
|
123
|
+
if (items[i].type.startsWith('image/')) {
|
|
124
|
+
e.preventDefault()
|
|
125
|
+
var file = items[i].getAsFile()
|
|
126
|
+
var reader = new FileReader()
|
|
127
|
+
reader.onload = () => sub.update(reader.result, file.type)
|
|
128
|
+
reader.readAsArrayBuffer(file)
|
|
129
|
+
break
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
87
133
|
}
|
|
88
134
|
}
|
|
89
135
|
|
|
90
136
|
function unsync(img) {
|
|
91
|
-
var
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
137
|
+
var base_url = img._img_live_base_url
|
|
138
|
+
var sub = subscriptions.get(base_url)
|
|
139
|
+
if (sub?.imgs.delete(img) && !sub.imgs.size) {
|
|
140
|
+
sub.teardown_timer = setTimeout(() => {
|
|
141
|
+
sub.ac.abort()
|
|
142
|
+
subscriptions.delete(base_url)
|
|
143
|
+
}, 5000)
|
|
95
144
|
}
|
|
96
145
|
}
|
|
97
146
|
|