Fossil SCM

Doc additions. Resolve two corner-case bugs in the JS attachment widget.

stephan 2026-06-05 10:21 UTC attach-v2
Commit 7723db560690b745589dcc545744c16a39964a9e2eca6ca53b3af198894a3dc2
1 file changed +17 -21
--- src/fossil.attach.js
+++ src/fossil.attach.js
@@ -6,23 +6,27 @@
66
Requires that window.fossil has already been set up.
77
Depends on fossil.dom.
88
*/
99
(function(namespace){
1010
"use strict";
11
- const F = namespace, D = fossil.dom;
11
+ const F = namespace, D = F.dom;
1212
1313
let idCounter = 0;
1414
1515
/**
1616
Implements a multi-file selector widget. Intended to be plugged
1717
in to places in Fossil's UI where attachments can be assigned to
1818
an artifact.
1919
*/
2020
class Attacher {
21
+ /* Options. */
2122
#opt;
23
+ /* List of objects representing each row. */
2224
#rows = [];
25
+ /* DOM elements */
2326
#e = Object.create(null);
27
+ /* Proxy for various events this object fires. */
2428
#events = new EventTarget();
2529
2630
/**
2731
Options:
2832
@@ -147,11 +151,11 @@
147151
get isDryRun(){
148152
return !!this.#opt.dryRun;
149153
}
150154
/**
151155
Returns the DOM element (div.attach-controls) which wraps the
152
- "Add" button. Clients may add buttons to it.
156
+ "Add" button. Clients may add buttons to it.
153157
*/
154158
get controlsElement(){
155159
return this.#e.controls;
156160
}
157161
@@ -182,20 +186,17 @@
182186
row: rowObj,
183187
attacher: this
184188
})
185189
})
186190
);
187
- if( false /* arguable */
188
- && 0===this.#rows.length
189
- && this.#opt.startWith>0 ){
190
- /* Intended primarily for /addattach. */
191
- this.#addRow();
192
- }
193191
}
194192
193
+ /**
194
+ Removes all attachments and clears the error state.
195
+ */
195196
clear(){
196
- for(const r of [...this.#rows/*clone because #rows may change*/]){
197
+ for(const r of [...this.#rows/*clone because this updates #rows*/]){
197198
this.#removeRow(r);
198199
}
199200
this.reportError();
200201
}
201202
@@ -384,10 +385,14 @@
384385
}
385386
386387
#injestBlob(rowObj, file){
387388
if( !file ) return;
388389
const old = this.#rowMatchingName(file.name);
390
+ if( rowObj.overrideName ){
391
+ file = new File([file], rowObj.overrideName, {type: file.type});
392
+ rowObj.overrideName = undefined;
393
+ }
389394
if( old && rowObj !== old ){
390395
/*
391396
Fossil attachments treat the name as a unique-per-target
392397
key, with the newest one being the primary. If a name is
393398
given twice, remove the new entry and reuse the older
@@ -398,15 +403,11 @@
398403
attachment artifacts.
399404
*/
400405
/* recycle `old` instead to avoid UI flicker. */
401406
this.#rowError(old);
402407
this.#removeRow(rowObj);
403
- rowObj.e = old.e;
404
- }
405
- if( rowObj.overrideName ){
406
- file = new File([file], rowObj.overrideName, {type: file.type});
407
- rowObj.overrideName = undefined;
408
+ rowObj = old;
408409
}
409410
410411
let szLbl;
411412
if( file.size < 500000 ){
412413
szLbl = file.size + ' bytes';
@@ -540,16 +541,11 @@
540541
if( !li.length ) return;
541542
if( eBtnSubmit.dataset.submitted ) return;
542543
eBtnSubmit.dataset.submitted = 1;
543544
D.disable(eBtnSubmit);
544545
const fd = new FormData();
545
- let i = 0;
546
- for(const row of li){
547
- ++i;
548
- fd.append('file'+i, row.content);
549
- if( row.description ) fd.append('file'+i+'_desc', row.description);
550
- }
546
+ att.populateFormData(fd);
551547
for( const eIn of eAttachWrapper.querySelectorAll(
552548
'input[type="hidden"]'
553549
) ){
554550
/* Copy over hidden input fields emitted by the server. */
555551
if( eIn.name==='target' ){
@@ -581,15 +577,15 @@
581577
if( to ){
582578
if( '/'!==to[0] ){
583579
to = F.repoUrl(to);
584580
}
585581
window.location = to;
586
- }else if( target ){
582
+ }else if( zTarget ){
587583
window.location = '?target='+zTarget+'&'+Date.now();
588584
}
589585
}
590586
})/*submit handler*/;
591587
updateBtnSubmit(att);
592588
F.page.attacher = att /* only for testing via dev console */;
593589
}/* /attachadd */
594590
595591
})(window.fossil);
596592
--- src/fossil.attach.js
+++ src/fossil.attach.js
@@ -6,23 +6,27 @@
6 Requires that window.fossil has already been set up.
7 Depends on fossil.dom.
8 */
9 (function(namespace){
10 "use strict";
11 const F = namespace, D = fossil.dom;
12
13 let idCounter = 0;
14
15 /**
16 Implements a multi-file selector widget. Intended to be plugged
17 in to places in Fossil's UI where attachments can be assigned to
18 an artifact.
19 */
20 class Attacher {
 
21 #opt;
 
22 #rows = [];
 
23 #e = Object.create(null);
 
24 #events = new EventTarget();
25
26 /**
27 Options:
28
@@ -147,11 +151,11 @@
147 get isDryRun(){
148 return !!this.#opt.dryRun;
149 }
150 /**
151 Returns the DOM element (div.attach-controls) which wraps the
152 "Add" button. Clients may add buttons to it.
153 */
154 get controlsElement(){
155 return this.#e.controls;
156 }
157
@@ -182,20 +186,17 @@
182 row: rowObj,
183 attacher: this
184 })
185 })
186 );
187 if( false /* arguable */
188 && 0===this.#rows.length
189 && this.#opt.startWith>0 ){
190 /* Intended primarily for /addattach. */
191 this.#addRow();
192 }
193 }
194
 
 
 
195 clear(){
196 for(const r of [...this.#rows/*clone because #rows may change*/]){
197 this.#removeRow(r);
198 }
199 this.reportError();
200 }
201
@@ -384,10 +385,14 @@
384 }
385
386 #injestBlob(rowObj, file){
387 if( !file ) return;
388 const old = this.#rowMatchingName(file.name);
 
 
 
 
389 if( old && rowObj !== old ){
390 /*
391 Fossil attachments treat the name as a unique-per-target
392 key, with the newest one being the primary. If a name is
393 given twice, remove the new entry and reuse the older
@@ -398,15 +403,11 @@
398 attachment artifacts.
399 */
400 /* recycle `old` instead to avoid UI flicker. */
401 this.#rowError(old);
402 this.#removeRow(rowObj);
403 rowObj.e = old.e;
404 }
405 if( rowObj.overrideName ){
406 file = new File([file], rowObj.overrideName, {type: file.type});
407 rowObj.overrideName = undefined;
408 }
409
410 let szLbl;
411 if( file.size < 500000 ){
412 szLbl = file.size + ' bytes';
@@ -540,16 +541,11 @@
540 if( !li.length ) return;
541 if( eBtnSubmit.dataset.submitted ) return;
542 eBtnSubmit.dataset.submitted = 1;
543 D.disable(eBtnSubmit);
544 const fd = new FormData();
545 let i = 0;
546 for(const row of li){
547 ++i;
548 fd.append('file'+i, row.content);
549 if( row.description ) fd.append('file'+i+'_desc', row.description);
550 }
551 for( const eIn of eAttachWrapper.querySelectorAll(
552 'input[type="hidden"]'
553 ) ){
554 /* Copy over hidden input fields emitted by the server. */
555 if( eIn.name==='target' ){
@@ -581,15 +577,15 @@
581 if( to ){
582 if( '/'!==to[0] ){
583 to = F.repoUrl(to);
584 }
585 window.location = to;
586 }else if( target ){
587 window.location = '?target='+zTarget+'&'+Date.now();
588 }
589 }
590 })/*submit handler*/;
591 updateBtnSubmit(att);
592 F.page.attacher = att /* only for testing via dev console */;
593 }/* /attachadd */
594
595 })(window.fossil);
596
--- src/fossil.attach.js
+++ src/fossil.attach.js
@@ -6,23 +6,27 @@
6 Requires that window.fossil has already been set up.
7 Depends on fossil.dom.
8 */
9 (function(namespace){
10 "use strict";
11 const F = namespace, D = F.dom;
12
13 let idCounter = 0;
14
15 /**
16 Implements a multi-file selector widget. Intended to be plugged
17 in to places in Fossil's UI where attachments can be assigned to
18 an artifact.
19 */
20 class Attacher {
21 /* Options. */
22 #opt;
23 /* List of objects representing each row. */
24 #rows = [];
25 /* DOM elements */
26 #e = Object.create(null);
27 /* Proxy for various events this object fires. */
28 #events = new EventTarget();
29
30 /**
31 Options:
32
@@ -147,11 +151,11 @@
151 get isDryRun(){
152 return !!this.#opt.dryRun;
153 }
154 /**
155 Returns the DOM element (div.attach-controls) which wraps the
156 "Add" button. Clients may add buttons to it.
157 */
158 get controlsElement(){
159 return this.#e.controls;
160 }
161
@@ -182,20 +186,17 @@
186 row: rowObj,
187 attacher: this
188 })
189 })
190 );
 
 
 
 
 
 
191 }
192
193 /**
194 Removes all attachments and clears the error state.
195 */
196 clear(){
197 for(const r of [...this.#rows/*clone because this updates #rows*/]){
198 this.#removeRow(r);
199 }
200 this.reportError();
201 }
202
@@ -384,10 +385,14 @@
385 }
386
387 #injestBlob(rowObj, file){
388 if( !file ) return;
389 const old = this.#rowMatchingName(file.name);
390 if( rowObj.overrideName ){
391 file = new File([file], rowObj.overrideName, {type: file.type});
392 rowObj.overrideName = undefined;
393 }
394 if( old && rowObj !== old ){
395 /*
396 Fossil attachments treat the name as a unique-per-target
397 key, with the newest one being the primary. If a name is
398 given twice, remove the new entry and reuse the older
@@ -398,15 +403,11 @@
403 attachment artifacts.
404 */
405 /* recycle `old` instead to avoid UI flicker. */
406 this.#rowError(old);
407 this.#removeRow(rowObj);
408 rowObj = old;
 
 
 
 
409 }
410
411 let szLbl;
412 if( file.size < 500000 ){
413 szLbl = file.size + ' bytes';
@@ -540,16 +541,11 @@
541 if( !li.length ) return;
542 if( eBtnSubmit.dataset.submitted ) return;
543 eBtnSubmit.dataset.submitted = 1;
544 D.disable(eBtnSubmit);
545 const fd = new FormData();
546 att.populateFormData(fd);
 
 
 
 
 
547 for( const eIn of eAttachWrapper.querySelectorAll(
548 'input[type="hidden"]'
549 ) ){
550 /* Copy over hidden input fields emitted by the server. */
551 if( eIn.name==='target' ){
@@ -581,15 +577,15 @@
577 if( to ){
578 if( '/'!==to[0] ){
579 to = F.repoUrl(to);
580 }
581 window.location = to;
582 }else if( zTarget ){
583 window.location = '?target='+zTarget+'&'+Date.now();
584 }
585 }
586 })/*submit handler*/;
587 updateBtnSubmit(att);
588 F.page.attacher = att /* only for testing via dev console */;
589 }/* /attachadd */
590
591 })(window.fossil);
592

Keyboard Shortcuts

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