Fossil SCM

Try to avoid a generic paste-screenshot name of 'image.png'. Cosmetic tweaks.

stephan 2026-06-02 14:25 UTC attach-v2
Commit c51c545faf1304249e42071d87af1c15f2ad93717d25a3e4d0746678a363628b
2 files changed +16 -10 +42 -21
+16 -10
--- src/default.css
+++ src/default.css
@@ -2015,64 +2015,70 @@
20152015
margin-bottom: 1em;
20162016
display: flex;
20172017
flex-direction: column;
20182018
gap: 0.75em;
20192019
}
2020
-.attach-row {
2020
+.attach-container > .attach-row {
20212021
display: flex;
20222022
flex-direction: column;
20232023
gap: 0.5em;
20242024
padding: 0.75em;
20252025
border: 1px dashed #ccc;
2026
- border-radius: 4px;
2026
+ border-radius: 0.25em;
20272027
background-color: #fafafa;
20282028
}
2029
-.attach-dropzone {
2029
+
2030
+.attach-container > .attach-row > .attach-dropzone {
20302031
padding: 1em;
20312032
text-align: center;
20322033
background: #ffffff;
20332034
border: 1px solid #ddd;
20342035
cursor: pointer;
20352036
border-radius: 2px;
20362037
transition: background-color 0.15s ease-in-out;
2038
+ display: flex;
2039
+ flex-direction: row;
2040
+ flex-wrap: nowrap;
20372041
}
2038
-.attach-dropzone.dragover {
2042
+.attach-container > .attach-row > .attach-dropzone.dragover {
20392043
background-color: #e1f5fe;
20402044
border-color: #03a9f4;
20412045
}
2042
-.attach-dropzone.populated {
2046
+.attach-container > .attach-row > .attach-dropzone.populated {
20432047
background-color: #f1f8e9;
20442048
border-color: #8bc34a;
20452049
border-style: solid;
20462050
}
2047
-.attach-row-info {
2051
+.attach-container > .attach-row .attach-row-info {
20482052
font-family: monospace;
20492053
font-size: 0.9em;
20502054
color: #555;
2055
+ flex-grow: 1;
20512056
}
2052
-.attach-desc-input {
2057
+.attach-container > .attach-row .attach-desc {
2058
+ max-width: initial;
20532059
width: 100%;
20542060
box-sizing: border-box;
20552061
min-height: 4em;
20562062
padding: 0.5em;
20572063
font-family: inherit;
20582064
font-size: 0.9em;
20592065
resize: vertical;
20602066
}
2061
-.attach-row-remove {
2067
+.attach-container > .attach-row .attach-row-remove {
20622068
align-self: flex-end;
20632069
padding: 0.25em 0.75em;
20642070
background-color: #d32f2f;
20652071
color: #fff;
20662072
border: none;
20672073
border-radius: 2px;
20682074
cursor: pointer;
20692075
}
2070
-.attach-row-remove:hover {
2076
+.attach-container > .attach-row .attach-row-remove:hover {
20712077
background-color: #b71c1c;
20722078
}
2073
-.attach-add-button {
2079
+.attach-container > .attach-add-button {
20742080
padding: 0.5em 1em;
20752081
cursor: pointer;
20762082
}
20772083
20782084
/* Objects in the "desktoponly" class are invisible on mobile */
20792085
--- src/default.css
+++ src/default.css
@@ -2015,64 +2015,70 @@
2015 margin-bottom: 1em;
2016 display: flex;
2017 flex-direction: column;
2018 gap: 0.75em;
2019 }
2020 .attach-row {
2021 display: flex;
2022 flex-direction: column;
2023 gap: 0.5em;
2024 padding: 0.75em;
2025 border: 1px dashed #ccc;
2026 border-radius: 4px;
2027 background-color: #fafafa;
2028 }
2029 .attach-dropzone {
 
2030 padding: 1em;
2031 text-align: center;
2032 background: #ffffff;
2033 border: 1px solid #ddd;
2034 cursor: pointer;
2035 border-radius: 2px;
2036 transition: background-color 0.15s ease-in-out;
 
 
 
2037 }
2038 .attach-dropzone.dragover {
2039 background-color: #e1f5fe;
2040 border-color: #03a9f4;
2041 }
2042 .attach-dropzone.populated {
2043 background-color: #f1f8e9;
2044 border-color: #8bc34a;
2045 border-style: solid;
2046 }
2047 .attach-row-info {
2048 font-family: monospace;
2049 font-size: 0.9em;
2050 color: #555;
 
2051 }
2052 .attach-desc-input {
 
2053 width: 100%;
2054 box-sizing: border-box;
2055 min-height: 4em;
2056 padding: 0.5em;
2057 font-family: inherit;
2058 font-size: 0.9em;
2059 resize: vertical;
2060 }
2061 .attach-row-remove {
2062 align-self: flex-end;
2063 padding: 0.25em 0.75em;
2064 background-color: #d32f2f;
2065 color: #fff;
2066 border: none;
2067 border-radius: 2px;
2068 cursor: pointer;
2069 }
2070 .attach-row-remove:hover {
2071 background-color: #b71c1c;
2072 }
2073 .attach-add-button {
2074 padding: 0.5em 1em;
2075 cursor: pointer;
2076 }
2077
2078 /* Objects in the "desktoponly" class are invisible on mobile */
2079
--- src/default.css
+++ src/default.css
@@ -2015,64 +2015,70 @@
2015 margin-bottom: 1em;
2016 display: flex;
2017 flex-direction: column;
2018 gap: 0.75em;
2019 }
2020 .attach-container > .attach-row {
2021 display: flex;
2022 flex-direction: column;
2023 gap: 0.5em;
2024 padding: 0.75em;
2025 border: 1px dashed #ccc;
2026 border-radius: 0.25em;
2027 background-color: #fafafa;
2028 }
2029
2030 .attach-container > .attach-row > .attach-dropzone {
2031 padding: 1em;
2032 text-align: center;
2033 background: #ffffff;
2034 border: 1px solid #ddd;
2035 cursor: pointer;
2036 border-radius: 2px;
2037 transition: background-color 0.15s ease-in-out;
2038 display: flex;
2039 flex-direction: row;
2040 flex-wrap: nowrap;
2041 }
2042 .attach-container > .attach-row > .attach-dropzone.dragover {
2043 background-color: #e1f5fe;
2044 border-color: #03a9f4;
2045 }
2046 .attach-container > .attach-row > .attach-dropzone.populated {
2047 background-color: #f1f8e9;
2048 border-color: #8bc34a;
2049 border-style: solid;
2050 }
2051 .attach-container > .attach-row .attach-row-info {
2052 font-family: monospace;
2053 font-size: 0.9em;
2054 color: #555;
2055 flex-grow: 1;
2056 }
2057 .attach-container > .attach-row .attach-desc {
2058 max-width: initial;
2059 width: 100%;
2060 box-sizing: border-box;
2061 min-height: 4em;
2062 padding: 0.5em;
2063 font-family: inherit;
2064 font-size: 0.9em;
2065 resize: vertical;
2066 }
2067 .attach-container > .attach-row .attach-row-remove {
2068 align-self: flex-end;
2069 padding: 0.25em 0.75em;
2070 background-color: #d32f2f;
2071 color: #fff;
2072 border: none;
2073 border-radius: 2px;
2074 cursor: pointer;
2075 }
2076 .attach-container > .attach-row .attach-row-remove:hover {
2077 background-color: #b71c1c;
2078 }
2079 .attach-container > .attach-add-button {
2080 padding: 0.5em 1em;
2081 cursor: pointer;
2082 }
2083
2084 /* Objects in the "desktoponly" class are invisible on mobile */
2085
--- src/fossil.attach.js
+++ src/fossil.attach.js
@@ -38,18 +38,10 @@
3838
this.#e.list = D.addClass(D.div(), 'attach-container')
3939
opt.container.appendChild(this.#e.list);
4040
this.#e.list.appendChild(eBtnAdd);
4141
}
4242
43
- #removeRow(id){
44
- const e = this.#opt.container.querySelector('[data-id="'+id+'"]');
45
- if( e ){
46
- e.remove();
47
- this.#rows = this.#rows.filter(v=>v.id!==+id);
48
- }
49
- }
50
-
5143
#addRow(){
5244
const id = ++idCounter;
5345
const rowObj = {
5446
id, file: null, mimeType: ''
5547
};
@@ -59,24 +51,30 @@
5951
const eFile = D.addClass(
6052
D.input('file'), 'attach-file-input', 'hidden'
6153
);
6254
const eInfo = D.append(
6355
D.addClass(D.span(), 'attach-row-info'),
64
- "Click to select, drop file, or paste content here"
56
+ "Select/drop file or click the outer border and tap your "+
57
+ "platform's conventional Paste keyboard shortcut."
6558
);
66
- D.append(eDropzone, eInfo, eFile);
59
+
6760
const eDesc = D.addClass(
6861
D.attr(D.textarea(), 'placeholder',
6962
'Optional description...'),
70
- 'hidden'
63
+ 'hidden', 'attach-desc'
7164
);
7265
const eRemove = D.addClass(
73
- D.button('Remove', ()=>this.#removeRow(id)),
66
+ D.button('Remove', (ev)=>{
67
+ ev.stopPropagation();
68
+ eRow.remove();
69
+ this.#rows = this.#rows.filter(v=>v!==rowObj);
70
+ }),
7471
'attach-row-remove'
7572
);
7673
eRemove.type = 'button';
7774
75
+ D.append(eDropzone, eInfo, eFile, eRemove);
7876
eDropzone.addEventListener('click', ()=>eFile.click());
7977
eFile.addEventListener('change', (ev)=>{
8078
if( ev.target.files.length ){
8179
this.#injestBlob(rowObj, ev.target.files[0]);
8280
}
@@ -107,33 +105,55 @@
107105
this.#injestBlob(rowObj, blob);
108106
break;
109107
}else if( item.type === 'text/plain' ){
110108
e.preventDefault();
111109
item.getAsString((text) => {
112
- const blob = new Blob([text], {type: 'text/plain'});
113
- blob.name = `pasted-text-${id}.txt`;
110
+ const blob = new File([text], `pasted-text-${id}.txt`,
111
+ {type: 'text/plain'});
114112
this.#injestBlob(rowObj, blob);
115113
});
116114
break;
117115
}
118116
}
119117
});
120
- D.append(eRow, eDropzone, eDesc, eRemove);
118
+ D.append(eRow, eDropzone, eDesc);
121119
rowObj.eDropzone = eDropzone;
122120
rowObj.eInfo = eInfo;
123121
rowObj.eDesc = eDesc;
124122
this.#rows.push( rowObj );
125123
this.#e.list.append(eRow, this.#e.btnAdd);
124
+ if( 0 ){
125
+ /* To allow immediate ctrl-v, we need a trick...
126
+ But don't do this because it will interfere with, e.g.,
127
+ the forum editor. */
128
+ D.attr(eRow, 'tabindex', '-1');
129
+ eRow.focus();
130
+ }
126131
}
127132
128133
#injestBlob(rowObj, file){
129134
if( !file ) return;
135
+ if( file.name === 'image.png' ){
136
+ /* Workaround to attempt to avoid name collisions when
137
+ pasting multiple images. We cannot, at this level, unambiguously
138
+ distinguish a ctrl-v of bitmap data vs a ctrl-v of an
139
+ image file using a desktop file manager. */
140
+ file = new File([file], `pasted-image-${rowObj.id}.png`,
141
+ {type: file.type});
142
+ }
130143
rowObj.file = file;
131144
rowObj.mimeType = file.type || 'application/octet-stream';
132145
133146
const lbl = file.name || 'Pasted Content';
134
- const szLbl = (file.size / 1024).toFixed(2)+' KB';
147
+ let szLbl;
148
+ if( file.size < 500000 ){
149
+ szLbl = file.size + ' bytes';
150
+ }else if( file.size < 1000000 ){
151
+ szLbl = (file.size / 1024).toFixed(2)+' KB';
152
+ }else{
153
+ szLbl = (file.size / (1024 * 1024)).toFixed(2)+' MB';
154
+ }
135155
rowObj.eInfo.textContent = `${lbl} (${szLbl}, ${rowObj.mimeType})`;
136156
rowObj.eDropzone.classList.add('populated');
137157
rowObj.eDesc.classList.remove('hidden');
138158
}
139159
@@ -146,11 +166,11 @@
146166
for(let r of this.#rows){
147167
if( !r.eDropzone?.classList?.contains?.('populated') ){
148168
continue;
149169
}
150170
rv.push(Object.assign(Object.create(null),{
151
- name: r.file.name || `pasted-content-${r.id}.${row.mimeType.split('/')[1] || 'txt'}`,
171
+ name: r.file.name || `pasted-content-${r.id}.${r.mimeType.split('/')[1] || 'txt'}`,
152172
content: r.file,
153173
description: r.eDesc?.value || '',
154174
mimeType: r.mimeType
155175
}));
156176
}
@@ -163,21 +183,22 @@
163183
being a 1-based incremental counter. For entries which have a
164184
description, it also sets ${namePrefix}${N}_desc.
165185
*/
166186
populateFormData(fd, namePrefix='file'){
167187
const st = this.collectState();
168
- for(const ndx in st){
169
- const s = st[ndx];
170
- const suffix = ndx+1;
188
+ let i = 0;
189
+ for( ; i < st.length; ++i){
190
+ const s = st[i];
191
+ const suffix = i+1;
171192
fd.append(`${namePrefix}${suffix}`, s.content, s.name);
172193
const d = s.description?.trim?.();
173194
if( d ){
174195
fd.append(`${namePrefix}${suffix}_desc`, d);
175196
}
176197
}
177
- return st.length;
198
+ return i;
178199
}
179200
}/*Attacher*/;
180201
181202
F.Attacher = Attacher;
182203
183204
})(window.fossil);
184205
--- src/fossil.attach.js
+++ src/fossil.attach.js
@@ -38,18 +38,10 @@
38 this.#e.list = D.addClass(D.div(), 'attach-container')
39 opt.container.appendChild(this.#e.list);
40 this.#e.list.appendChild(eBtnAdd);
41 }
42
43 #removeRow(id){
44 const e = this.#opt.container.querySelector('[data-id="'+id+'"]');
45 if( e ){
46 e.remove();
47 this.#rows = this.#rows.filter(v=>v.id!==+id);
48 }
49 }
50
51 #addRow(){
52 const id = ++idCounter;
53 const rowObj = {
54 id, file: null, mimeType: ''
55 };
@@ -59,24 +51,30 @@
59 const eFile = D.addClass(
60 D.input('file'), 'attach-file-input', 'hidden'
61 );
62 const eInfo = D.append(
63 D.addClass(D.span(), 'attach-row-info'),
64 "Click to select, drop file, or paste content here"
 
65 );
66 D.append(eDropzone, eInfo, eFile);
67 const eDesc = D.addClass(
68 D.attr(D.textarea(), 'placeholder',
69 'Optional description...'),
70 'hidden'
71 );
72 const eRemove = D.addClass(
73 D.button('Remove', ()=>this.#removeRow(id)),
 
 
 
 
74 'attach-row-remove'
75 );
76 eRemove.type = 'button';
77
 
78 eDropzone.addEventListener('click', ()=>eFile.click());
79 eFile.addEventListener('change', (ev)=>{
80 if( ev.target.files.length ){
81 this.#injestBlob(rowObj, ev.target.files[0]);
82 }
@@ -107,33 +105,55 @@
107 this.#injestBlob(rowObj, blob);
108 break;
109 }else if( item.type === 'text/plain' ){
110 e.preventDefault();
111 item.getAsString((text) => {
112 const blob = new Blob([text], {type: 'text/plain'});
113 blob.name = `pasted-text-${id}.txt`;
114 this.#injestBlob(rowObj, blob);
115 });
116 break;
117 }
118 }
119 });
120 D.append(eRow, eDropzone, eDesc, eRemove);
121 rowObj.eDropzone = eDropzone;
122 rowObj.eInfo = eInfo;
123 rowObj.eDesc = eDesc;
124 this.#rows.push( rowObj );
125 this.#e.list.append(eRow, this.#e.btnAdd);
 
 
 
 
 
 
 
126 }
127
128 #injestBlob(rowObj, file){
129 if( !file ) return;
 
 
 
 
 
 
 
 
130 rowObj.file = file;
131 rowObj.mimeType = file.type || 'application/octet-stream';
132
133 const lbl = file.name || 'Pasted Content';
134 const szLbl = (file.size / 1024).toFixed(2)+' KB';
 
 
 
 
 
 
 
135 rowObj.eInfo.textContent = `${lbl} (${szLbl}, ${rowObj.mimeType})`;
136 rowObj.eDropzone.classList.add('populated');
137 rowObj.eDesc.classList.remove('hidden');
138 }
139
@@ -146,11 +166,11 @@
146 for(let r of this.#rows){
147 if( !r.eDropzone?.classList?.contains?.('populated') ){
148 continue;
149 }
150 rv.push(Object.assign(Object.create(null),{
151 name: r.file.name || `pasted-content-${r.id}.${row.mimeType.split('/')[1] || 'txt'}`,
152 content: r.file,
153 description: r.eDesc?.value || '',
154 mimeType: r.mimeType
155 }));
156 }
@@ -163,21 +183,22 @@
163 being a 1-based incremental counter. For entries which have a
164 description, it also sets ${namePrefix}${N}_desc.
165 */
166 populateFormData(fd, namePrefix='file'){
167 const st = this.collectState();
168 for(const ndx in st){
169 const s = st[ndx];
170 const suffix = ndx+1;
 
171 fd.append(`${namePrefix}${suffix}`, s.content, s.name);
172 const d = s.description?.trim?.();
173 if( d ){
174 fd.append(`${namePrefix}${suffix}_desc`, d);
175 }
176 }
177 return st.length;
178 }
179 }/*Attacher*/;
180
181 F.Attacher = Attacher;
182
183 })(window.fossil);
184
--- src/fossil.attach.js
+++ src/fossil.attach.js
@@ -38,18 +38,10 @@
38 this.#e.list = D.addClass(D.div(), 'attach-container')
39 opt.container.appendChild(this.#e.list);
40 this.#e.list.appendChild(eBtnAdd);
41 }
42
 
 
 
 
 
 
 
 
43 #addRow(){
44 const id = ++idCounter;
45 const rowObj = {
46 id, file: null, mimeType: ''
47 };
@@ -59,24 +51,30 @@
51 const eFile = D.addClass(
52 D.input('file'), 'attach-file-input', 'hidden'
53 );
54 const eInfo = D.append(
55 D.addClass(D.span(), 'attach-row-info'),
56 "Select/drop file or click the outer border and tap your "+
57 "platform's conventional Paste keyboard shortcut."
58 );
59
60 const eDesc = D.addClass(
61 D.attr(D.textarea(), 'placeholder',
62 'Optional description...'),
63 'hidden', 'attach-desc'
64 );
65 const eRemove = D.addClass(
66 D.button('Remove', (ev)=>{
67 ev.stopPropagation();
68 eRow.remove();
69 this.#rows = this.#rows.filter(v=>v!==rowObj);
70 }),
71 'attach-row-remove'
72 );
73 eRemove.type = 'button';
74
75 D.append(eDropzone, eInfo, eFile, eRemove);
76 eDropzone.addEventListener('click', ()=>eFile.click());
77 eFile.addEventListener('change', (ev)=>{
78 if( ev.target.files.length ){
79 this.#injestBlob(rowObj, ev.target.files[0]);
80 }
@@ -107,33 +105,55 @@
105 this.#injestBlob(rowObj, blob);
106 break;
107 }else if( item.type === 'text/plain' ){
108 e.preventDefault();
109 item.getAsString((text) => {
110 const blob = new File([text], `pasted-text-${id}.txt`,
111 {type: 'text/plain'});
112 this.#injestBlob(rowObj, blob);
113 });
114 break;
115 }
116 }
117 });
118 D.append(eRow, eDropzone, eDesc);
119 rowObj.eDropzone = eDropzone;
120 rowObj.eInfo = eInfo;
121 rowObj.eDesc = eDesc;
122 this.#rows.push( rowObj );
123 this.#e.list.append(eRow, this.#e.btnAdd);
124 if( 0 ){
125 /* To allow immediate ctrl-v, we need a trick...
126 But don't do this because it will interfere with, e.g.,
127 the forum editor. */
128 D.attr(eRow, 'tabindex', '-1');
129 eRow.focus();
130 }
131 }
132
133 #injestBlob(rowObj, file){
134 if( !file ) return;
135 if( file.name === 'image.png' ){
136 /* Workaround to attempt to avoid name collisions when
137 pasting multiple images. We cannot, at this level, unambiguously
138 distinguish a ctrl-v of bitmap data vs a ctrl-v of an
139 image file using a desktop file manager. */
140 file = new File([file], `pasted-image-${rowObj.id}.png`,
141 {type: file.type});
142 }
143 rowObj.file = file;
144 rowObj.mimeType = file.type || 'application/octet-stream';
145
146 const lbl = file.name || 'Pasted Content';
147 let szLbl;
148 if( file.size < 500000 ){
149 szLbl = file.size + ' bytes';
150 }else if( file.size < 1000000 ){
151 szLbl = (file.size / 1024).toFixed(2)+' KB';
152 }else{
153 szLbl = (file.size / (1024 * 1024)).toFixed(2)+' MB';
154 }
155 rowObj.eInfo.textContent = `${lbl} (${szLbl}, ${rowObj.mimeType})`;
156 rowObj.eDropzone.classList.add('populated');
157 rowObj.eDesc.classList.remove('hidden');
158 }
159
@@ -146,11 +166,11 @@
166 for(let r of this.#rows){
167 if( !r.eDropzone?.classList?.contains?.('populated') ){
168 continue;
169 }
170 rv.push(Object.assign(Object.create(null),{
171 name: r.file.name || `pasted-content-${r.id}.${r.mimeType.split('/')[1] || 'txt'}`,
172 content: r.file,
173 description: r.eDesc?.value || '',
174 mimeType: r.mimeType
175 }));
176 }
@@ -163,21 +183,22 @@
183 being a 1-based incremental counter. For entries which have a
184 description, it also sets ${namePrefix}${N}_desc.
185 */
186 populateFormData(fd, namePrefix='file'){
187 const st = this.collectState();
188 let i = 0;
189 for( ; i < st.length; ++i){
190 const s = st[i];
191 const suffix = i+1;
192 fd.append(`${namePrefix}${suffix}`, s.content, s.name);
193 const d = s.description?.trim?.();
194 if( d ){
195 fd.append(`${namePrefix}${suffix}_desc`, d);
196 }
197 }
198 return i;
199 }
200 }/*Attacher*/;
201
202 F.Attacher = Attacher;
203
204 })(window.fossil);
205

Keyboard Shortcuts

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