Fossil SCM

When pasting images multiple times in the same attachment slot, remove stale thumbnails. Use timestamps instead of slot numbers when generating a name for pasted content.

stephan 2026-06-02 21:12 UTC attach-v2
Commit 307213a0631ec193239c094073fd5c1684f6443bd6d7c4417d0fc1db68189706
1 file changed +14 -10
--- src/fossil.attach.js
+++ src/fossil.attach.js
@@ -167,13 +167,13 @@
167167
}
168168
}
169169
170170
#addRow(){
171171
const id = ++idCounter;
172
- const rowObj = {
172
+ const rowObj = F.nu({
173173
id, file: null, mimeType: ''
174
- };
174
+ });
175175
const eRow = D.addClass(D.div(), 'attach-row');
176176
eRow.dataset.id = id;
177177
const eDropzone = D.addClass(D.div(), 'attach-dropzone');
178178
const eFile = D.addClass(
179179
D.input('file'), 'attach-file-input', 'hidden'
@@ -231,11 +231,12 @@
231231
this.#injestBlob(rowObj, blob);
232232
break;
233233
}else if( item.type === 'text/plain' ){
234234
e.preventDefault();
235235
item.getAsString((text) => {
236
- const blob = new File([text], `pasted-text-${id}.txt`,
236
+ rowObj.name = `pasted-text-${Date.now()}.txt`;
237
+ const blob = new File([text], rowObj.name,
237238
{type: 'text/plain'});
238239
this.#injestBlob(rowObj, blob);
239240
});
240241
break;
241242
}
@@ -279,12 +280,14 @@
279280
if( file.name === 'image.png' ){
280281
/* Workaround to attempt to avoid name collisions when pasting
281282
multiple images. We cannot, at this level, unambiguously
282283
distinguish a ctrl-v of bitmap data vs a ctrl-v of an image
283284
file copied via a desktop file manager. */
284
- file = new File([file], `pasted-image-${rowObj.id}.png`,
285
- {type: file.type});
285
+ rowObj.name = `pasted-image-${Date.now()}.png`;
286
+ }
287
+ if( rowObj.name && rowObj.name!==file.name ){
288
+ file = new File([file], rowObj.name, {type: file.type});
286289
}
287290
/*
288291
Fossil attachments treat the name as a unique-per-target key,
289292
with the newest one being the primary. If a name is given
290293
twice, replace the prior entry before adding the new
@@ -291,14 +294,10 @@
291294
one. There are conceivable, but also unlikely, cases where
292295
this will have unintended side-effects, but that seems like a
293296
lesser evil than attaching the same file N times, leading to N
294297
attachment artifacts.
295298
*/
296
- const old = this.#rowMatchingName(file.name);
297
- if( old && rowObj !== old){
298
- this.#removeRow(old);
299
- }
300299
rowObj.file = file;
301300
rowObj.mimeType = file.type || 'application/octet-stream';
302301
303302
const lbl = file.name || 'Pasted Content';
304303
let szLbl;
@@ -311,13 +310,18 @@
311310
}
312311
D.append(
313312
D.clearElement(rowObj.eInfo),
314313
lbl, D.br(), szLbl, ' ', rowObj.mimeType || ''
315314
);
315
+ const old = this.#rowMatchingName(file.name);
316
+ if( old && rowObj !== old){
317
+ this.#removeRow(old);
318
+ }
316319
rowObj.eDropzone.classList.add('populated');
317320
rowObj.eDesc.classList.remove('hidden');
318321
if( file.type?.startsWith?.('image/') || file.type==='BITMAP' ){
322
+ rowObj.eDropzone.querySelectorAll('img.thumbnail').forEach(e=>e.remove());
319323
const img = D.img();
320324
img.classList.add('thumbnail');
321325
rowObj.eDropzone.insertBefore(img, rowObj.eRemove);
322326
const reader = new FileReader();
323327
reader.onload = (e)=>img.setAttribute('src', e.target.result);
@@ -343,11 +347,11 @@
343347
for(let r of this.#rows){
344348
if( !r.eDropzone?.classList?.contains?.('populated') ){
345349
continue;
346350
}
347351
rv.push(F.nu({
348
- name: r.file.name || `pasted-content-${r.id}.${r.mimeType.split('/')[1] || 'txt'}`,
352
+ name: r.name || r.file.name || `pasted-content-${r.id}.${r.mimeType.split('/')[1] || 'txt'}`,
349353
content: r.file,
350354
description: r.eDesc?.value || '',
351355
mimeType: r.mimeType
352356
}));
353357
}
354358
--- src/fossil.attach.js
+++ src/fossil.attach.js
@@ -167,13 +167,13 @@
167 }
168 }
169
170 #addRow(){
171 const id = ++idCounter;
172 const rowObj = {
173 id, file: null, mimeType: ''
174 };
175 const eRow = D.addClass(D.div(), 'attach-row');
176 eRow.dataset.id = id;
177 const eDropzone = D.addClass(D.div(), 'attach-dropzone');
178 const eFile = D.addClass(
179 D.input('file'), 'attach-file-input', 'hidden'
@@ -231,11 +231,12 @@
231 this.#injestBlob(rowObj, blob);
232 break;
233 }else if( item.type === 'text/plain' ){
234 e.preventDefault();
235 item.getAsString((text) => {
236 const blob = new File([text], `pasted-text-${id}.txt`,
 
237 {type: 'text/plain'});
238 this.#injestBlob(rowObj, blob);
239 });
240 break;
241 }
@@ -279,12 +280,14 @@
279 if( file.name === 'image.png' ){
280 /* Workaround to attempt to avoid name collisions when pasting
281 multiple images. We cannot, at this level, unambiguously
282 distinguish a ctrl-v of bitmap data vs a ctrl-v of an image
283 file copied via a desktop file manager. */
284 file = new File([file], `pasted-image-${rowObj.id}.png`,
285 {type: file.type});
 
 
286 }
287 /*
288 Fossil attachments treat the name as a unique-per-target key,
289 with the newest one being the primary. If a name is given
290 twice, replace the prior entry before adding the new
@@ -291,14 +294,10 @@
291 one. There are conceivable, but also unlikely, cases where
292 this will have unintended side-effects, but that seems like a
293 lesser evil than attaching the same file N times, leading to N
294 attachment artifacts.
295 */
296 const old = this.#rowMatchingName(file.name);
297 if( old && rowObj !== old){
298 this.#removeRow(old);
299 }
300 rowObj.file = file;
301 rowObj.mimeType = file.type || 'application/octet-stream';
302
303 const lbl = file.name || 'Pasted Content';
304 let szLbl;
@@ -311,13 +310,18 @@
311 }
312 D.append(
313 D.clearElement(rowObj.eInfo),
314 lbl, D.br(), szLbl, ' ', rowObj.mimeType || ''
315 );
 
 
 
 
316 rowObj.eDropzone.classList.add('populated');
317 rowObj.eDesc.classList.remove('hidden');
318 if( file.type?.startsWith?.('image/') || file.type==='BITMAP' ){
 
319 const img = D.img();
320 img.classList.add('thumbnail');
321 rowObj.eDropzone.insertBefore(img, rowObj.eRemove);
322 const reader = new FileReader();
323 reader.onload = (e)=>img.setAttribute('src', e.target.result);
@@ -343,11 +347,11 @@
343 for(let r of this.#rows){
344 if( !r.eDropzone?.classList?.contains?.('populated') ){
345 continue;
346 }
347 rv.push(F.nu({
348 name: r.file.name || `pasted-content-${r.id}.${r.mimeType.split('/')[1] || 'txt'}`,
349 content: r.file,
350 description: r.eDesc?.value || '',
351 mimeType: r.mimeType
352 }));
353 }
354
--- src/fossil.attach.js
+++ src/fossil.attach.js
@@ -167,13 +167,13 @@
167 }
168 }
169
170 #addRow(){
171 const id = ++idCounter;
172 const rowObj = F.nu({
173 id, file: null, mimeType: ''
174 });
175 const eRow = D.addClass(D.div(), 'attach-row');
176 eRow.dataset.id = id;
177 const eDropzone = D.addClass(D.div(), 'attach-dropzone');
178 const eFile = D.addClass(
179 D.input('file'), 'attach-file-input', 'hidden'
@@ -231,11 +231,12 @@
231 this.#injestBlob(rowObj, blob);
232 break;
233 }else if( item.type === 'text/plain' ){
234 e.preventDefault();
235 item.getAsString((text) => {
236 rowObj.name = `pasted-text-${Date.now()}.txt`;
237 const blob = new File([text], rowObj.name,
238 {type: 'text/plain'});
239 this.#injestBlob(rowObj, blob);
240 });
241 break;
242 }
@@ -279,12 +280,14 @@
280 if( file.name === 'image.png' ){
281 /* Workaround to attempt to avoid name collisions when pasting
282 multiple images. We cannot, at this level, unambiguously
283 distinguish a ctrl-v of bitmap data vs a ctrl-v of an image
284 file copied via a desktop file manager. */
285 rowObj.name = `pasted-image-${Date.now()}.png`;
286 }
287 if( rowObj.name && rowObj.name!==file.name ){
288 file = new File([file], rowObj.name, {type: file.type});
289 }
290 /*
291 Fossil attachments treat the name as a unique-per-target key,
292 with the newest one being the primary. If a name is given
293 twice, replace the prior entry before adding the new
@@ -291,14 +294,10 @@
294 one. There are conceivable, but also unlikely, cases where
295 this will have unintended side-effects, but that seems like a
296 lesser evil than attaching the same file N times, leading to N
297 attachment artifacts.
298 */
 
 
 
 
299 rowObj.file = file;
300 rowObj.mimeType = file.type || 'application/octet-stream';
301
302 const lbl = file.name || 'Pasted Content';
303 let szLbl;
@@ -311,13 +310,18 @@
310 }
311 D.append(
312 D.clearElement(rowObj.eInfo),
313 lbl, D.br(), szLbl, ' ', rowObj.mimeType || ''
314 );
315 const old = this.#rowMatchingName(file.name);
316 if( old && rowObj !== old){
317 this.#removeRow(old);
318 }
319 rowObj.eDropzone.classList.add('populated');
320 rowObj.eDesc.classList.remove('hidden');
321 if( file.type?.startsWith?.('image/') || file.type==='BITMAP' ){
322 rowObj.eDropzone.querySelectorAll('img.thumbnail').forEach(e=>e.remove());
323 const img = D.img();
324 img.classList.add('thumbnail');
325 rowObj.eDropzone.insertBefore(img, rowObj.eRemove);
326 const reader = new FileReader();
327 reader.onload = (e)=>img.setAttribute('src', e.target.result);
@@ -343,11 +347,11 @@
347 for(let r of this.#rows){
348 if( !r.eDropzone?.classList?.contains?.('populated') ){
349 continue;
350 }
351 rv.push(F.nu({
352 name: r.name || r.file.name || `pasted-content-${r.id}.${r.mimeType.split('/')[1] || 'txt'}`,
353 content: r.file,
354 description: r.eDesc?.value || '',
355 mimeType: r.mimeType
356 }));
357 }
358

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button