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.
Files changed (3) hide show
  1. package/README.md +9 -4
  2. package/img-live.js +102 -53
  3. 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 that automatically syncs any `<img>` element with a `live` attribute. Images update in real-time whenever the blob changes on the server.
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! The image will automatically stay synchronized with the server. When any client updates `/blob.png`, all `<img live src="/blob.png">` elements will update in real-time.
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
- The polyfill:
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 live image
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 live_images = new Map() // img -> ac
6
+ var subscriptions = new Map() // base_url -> sub
7
7
 
8
8
  function sync(img) {
9
- var url = img.src
10
- if (!url) return
11
-
12
- // Find an unused query parameter name for cache-busting
13
- var param = 'img-live'
14
- var u = new URL(url)
15
- while (u.searchParams.has(param)) param = '-' + param
16
- function cache_bust() {
17
- u.searchParams.set(param, Math.random().toString(36).slice(2))
18
- return u.toString()
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
- // Unsync first to handle attribute changes (e.g. droppable added/removed)
22
- unsync(img)
23
-
24
- var ac = new AbortController()
25
- var client_p = (async () => {
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
- live_images.set(img, ac)
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 = async function() {
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 ac = live_images.get(img)
92
- if (ac) {
93
- ac.abort()
94
- live_images.delete(img)
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "braid-blob",
3
- "version": "0.0.78",
3
+ "version": "0.0.80",
4
4
  "description": "Library for collaborative blobs over http using braid.",
5
5
  "author": "Braid Working Group",
6
6
  "repository": "braid-org/braid-blob",