Fossil SCM
Try to avoid a generic paste-screenshot name of 'image.png'. Cosmetic tweaks.
Commit
c51c545faf1304249e42071d87af1c15f2ad93717d25a3e4d0746678a363628b
Parent
e7f2da1886482db…
2 files changed
+16
-10
+42
-21
+16
-10
| --- src/default.css | ||
| +++ src/default.css | ||
| @@ -2015,64 +2015,70 @@ | ||
| 2015 | 2015 | margin-bottom: 1em; |
| 2016 | 2016 | display: flex; |
| 2017 | 2017 | flex-direction: column; |
| 2018 | 2018 | gap: 0.75em; |
| 2019 | 2019 | } |
| 2020 | -.attach-row { | |
| 2020 | +.attach-container > .attach-row { | |
| 2021 | 2021 | display: flex; |
| 2022 | 2022 | flex-direction: column; |
| 2023 | 2023 | gap: 0.5em; |
| 2024 | 2024 | padding: 0.75em; |
| 2025 | 2025 | border: 1px dashed #ccc; |
| 2026 | - border-radius: 4px; | |
| 2026 | + border-radius: 0.25em; | |
| 2027 | 2027 | background-color: #fafafa; |
| 2028 | 2028 | } |
| 2029 | -.attach-dropzone { | |
| 2029 | + | |
| 2030 | +.attach-container > .attach-row > .attach-dropzone { | |
| 2030 | 2031 | padding: 1em; |
| 2031 | 2032 | text-align: center; |
| 2032 | 2033 | background: #ffffff; |
| 2033 | 2034 | border: 1px solid #ddd; |
| 2034 | 2035 | cursor: pointer; |
| 2035 | 2036 | border-radius: 2px; |
| 2036 | 2037 | transition: background-color 0.15s ease-in-out; |
| 2038 | + display: flex; | |
| 2039 | + flex-direction: row; | |
| 2040 | + flex-wrap: nowrap; | |
| 2037 | 2041 | } |
| 2038 | -.attach-dropzone.dragover { | |
| 2042 | +.attach-container > .attach-row > .attach-dropzone.dragover { | |
| 2039 | 2043 | background-color: #e1f5fe; |
| 2040 | 2044 | border-color: #03a9f4; |
| 2041 | 2045 | } |
| 2042 | -.attach-dropzone.populated { | |
| 2046 | +.attach-container > .attach-row > .attach-dropzone.populated { | |
| 2043 | 2047 | background-color: #f1f8e9; |
| 2044 | 2048 | border-color: #8bc34a; |
| 2045 | 2049 | border-style: solid; |
| 2046 | 2050 | } |
| 2047 | -.attach-row-info { | |
| 2051 | +.attach-container > .attach-row .attach-row-info { | |
| 2048 | 2052 | font-family: monospace; |
| 2049 | 2053 | font-size: 0.9em; |
| 2050 | 2054 | color: #555; |
| 2055 | + flex-grow: 1; | |
| 2051 | 2056 | } |
| 2052 | -.attach-desc-input { | |
| 2057 | +.attach-container > .attach-row .attach-desc { | |
| 2058 | + max-width: initial; | |
| 2053 | 2059 | width: 100%; |
| 2054 | 2060 | box-sizing: border-box; |
| 2055 | 2061 | min-height: 4em; |
| 2056 | 2062 | padding: 0.5em; |
| 2057 | 2063 | font-family: inherit; |
| 2058 | 2064 | font-size: 0.9em; |
| 2059 | 2065 | resize: vertical; |
| 2060 | 2066 | } |
| 2061 | -.attach-row-remove { | |
| 2067 | +.attach-container > .attach-row .attach-row-remove { | |
| 2062 | 2068 | align-self: flex-end; |
| 2063 | 2069 | padding: 0.25em 0.75em; |
| 2064 | 2070 | background-color: #d32f2f; |
| 2065 | 2071 | color: #fff; |
| 2066 | 2072 | border: none; |
| 2067 | 2073 | border-radius: 2px; |
| 2068 | 2074 | cursor: pointer; |
| 2069 | 2075 | } |
| 2070 | -.attach-row-remove:hover { | |
| 2076 | +.attach-container > .attach-row .attach-row-remove:hover { | |
| 2071 | 2077 | background-color: #b71c1c; |
| 2072 | 2078 | } |
| 2073 | -.attach-add-button { | |
| 2079 | +.attach-container > .attach-add-button { | |
| 2074 | 2080 | padding: 0.5em 1em; |
| 2075 | 2081 | cursor: pointer; |
| 2076 | 2082 | } |
| 2077 | 2083 | |
| 2078 | 2084 | /* Objects in the "desktoponly" class are invisible on mobile */ |
| 2079 | 2085 |
| --- 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 |
+42
-21
| --- src/fossil.attach.js | ||
| +++ src/fossil.attach.js | ||
| @@ -38,18 +38,10 @@ | ||
| 38 | 38 | this.#e.list = D.addClass(D.div(), 'attach-container') |
| 39 | 39 | opt.container.appendChild(this.#e.list); |
| 40 | 40 | this.#e.list.appendChild(eBtnAdd); |
| 41 | 41 | } |
| 42 | 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 | 43 | #addRow(){ |
| 52 | 44 | const id = ++idCounter; |
| 53 | 45 | const rowObj = { |
| 54 | 46 | id, file: null, mimeType: '' |
| 55 | 47 | }; |
| @@ -59,24 +51,30 @@ | ||
| 59 | 51 | const eFile = D.addClass( |
| 60 | 52 | D.input('file'), 'attach-file-input', 'hidden' |
| 61 | 53 | ); |
| 62 | 54 | const eInfo = D.append( |
| 63 | 55 | 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." | |
| 65 | 58 | ); |
| 66 | - D.append(eDropzone, eInfo, eFile); | |
| 59 | + | |
| 67 | 60 | const eDesc = D.addClass( |
| 68 | 61 | D.attr(D.textarea(), 'placeholder', |
| 69 | 62 | 'Optional description...'), |
| 70 | - 'hidden' | |
| 63 | + 'hidden', 'attach-desc' | |
| 71 | 64 | ); |
| 72 | 65 | 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 | + }), | |
| 74 | 71 | 'attach-row-remove' |
| 75 | 72 | ); |
| 76 | 73 | eRemove.type = 'button'; |
| 77 | 74 | |
| 75 | + D.append(eDropzone, eInfo, eFile, eRemove); | |
| 78 | 76 | eDropzone.addEventListener('click', ()=>eFile.click()); |
| 79 | 77 | eFile.addEventListener('change', (ev)=>{ |
| 80 | 78 | if( ev.target.files.length ){ |
| 81 | 79 | this.#injestBlob(rowObj, ev.target.files[0]); |
| 82 | 80 | } |
| @@ -107,33 +105,55 @@ | ||
| 107 | 105 | this.#injestBlob(rowObj, blob); |
| 108 | 106 | break; |
| 109 | 107 | }else if( item.type === 'text/plain' ){ |
| 110 | 108 | e.preventDefault(); |
| 111 | 109 | 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'}); | |
| 114 | 112 | this.#injestBlob(rowObj, blob); |
| 115 | 113 | }); |
| 116 | 114 | break; |
| 117 | 115 | } |
| 118 | 116 | } |
| 119 | 117 | }); |
| 120 | - D.append(eRow, eDropzone, eDesc, eRemove); | |
| 118 | + D.append(eRow, eDropzone, eDesc); | |
| 121 | 119 | rowObj.eDropzone = eDropzone; |
| 122 | 120 | rowObj.eInfo = eInfo; |
| 123 | 121 | rowObj.eDesc = eDesc; |
| 124 | 122 | this.#rows.push( rowObj ); |
| 125 | 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 | + } | |
| 126 | 131 | } |
| 127 | 132 | |
| 128 | 133 | #injestBlob(rowObj, file){ |
| 129 | 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 | + } | |
| 130 | 143 | rowObj.file = file; |
| 131 | 144 | rowObj.mimeType = file.type || 'application/octet-stream'; |
| 132 | 145 | |
| 133 | 146 | 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 | + } | |
| 135 | 155 | rowObj.eInfo.textContent = `${lbl} (${szLbl}, ${rowObj.mimeType})`; |
| 136 | 156 | rowObj.eDropzone.classList.add('populated'); |
| 137 | 157 | rowObj.eDesc.classList.remove('hidden'); |
| 138 | 158 | } |
| 139 | 159 | |
| @@ -146,11 +166,11 @@ | ||
| 146 | 166 | for(let r of this.#rows){ |
| 147 | 167 | if( !r.eDropzone?.classList?.contains?.('populated') ){ |
| 148 | 168 | continue; |
| 149 | 169 | } |
| 150 | 170 | 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'}`, | |
| 152 | 172 | content: r.file, |
| 153 | 173 | description: r.eDesc?.value || '', |
| 154 | 174 | mimeType: r.mimeType |
| 155 | 175 | })); |
| 156 | 176 | } |
| @@ -163,21 +183,22 @@ | ||
| 163 | 183 | being a 1-based incremental counter. For entries which have a |
| 164 | 184 | description, it also sets ${namePrefix}${N}_desc. |
| 165 | 185 | */ |
| 166 | 186 | populateFormData(fd, namePrefix='file'){ |
| 167 | 187 | 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; | |
| 171 | 192 | fd.append(`${namePrefix}${suffix}`, s.content, s.name); |
| 172 | 193 | const d = s.description?.trim?.(); |
| 173 | 194 | if( d ){ |
| 174 | 195 | fd.append(`${namePrefix}${suffix}_desc`, d); |
| 175 | 196 | } |
| 176 | 197 | } |
| 177 | - return st.length; | |
| 198 | + return i; | |
| 178 | 199 | } |
| 179 | 200 | }/*Attacher*/; |
| 180 | 201 | |
| 181 | 202 | F.Attacher = Attacher; |
| 182 | 203 | |
| 183 | 204 | })(window.fossil); |
| 184 | 205 |
| --- 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 |