Mercurial > p > roundup > code
comparison website/issues/html/issue.item.html @ 6454:83b67f65b8ff
Implement file upload by drag and drop and paste. Table formatting
Move file description from below the file upload into the table on
same line as file upload.
Implement
https://wiki.roundup-tracker.org/FileUploadViaDragDropAndPaste
for table structure. Added creation of description field along with
file input field.
On tables align header fields that do not span rows (i.e. column
headers) on the left. Table titles/grouping span multiple columns.
Also do not align column headers left on the index page which uses the
.list class.
Also add some padding to th to increase space and improve readability.
For message and files tables make them 95% of the width of their
container. It makes the headers for messages and files more readable
by adding space.
| author | John Rouillard <rouilj@ieee.org> |
|---|---|
| date | Mon, 05 Jul 2021 01:24:37 -0400 |
| parents | 6ad3b2d46852 |
| children | a2c340261290 |
comparison
equal
deleted
inserted
replaced
| 6453:3e7fc096fe5b | 6454:83b67f65b8ff |
|---|---|
| 155 </td> | 155 </td> |
| 156 </tr> | 156 </tr> |
| 157 | 157 |
| 158 <tr tal:condition="context/is_edit_ok"> | 158 <tr tal:condition="context/is_edit_ok"> |
| 159 <th><label for="file-1@content" i18n:translate="">File</label>:</th> | 159 <th><label for="file-1@content" i18n:translate="">File</label>:</th> |
| 160 <td colspan=3> | 160 <td> |
| 161 <input type="hidden" name="@link@files" value="file-1"> | 161 <input type="hidden" name="@link@files" value="file-1"> |
| 162 <input type="file" id="file-1@content" name="file-1@content" size="40"> | 162 <input type="file" id="file-1@content" name="file-1@content" size="40"> |
| 163 </td> | 163 </td> |
| 164 <th><label for="file-1@description" i18n:translate="">File Description</label>:</th> | |
| 165 <td colspan=3><input type="edit" id="file-1@description" name="file-1@description" size="40"></td> | |
| 164 </tr> | 166 </tr> |
| 165 <tr tal:condition="context/is_edit_ok"> | 167 <tr tal:condition="context/is_edit_ok"> |
| 166 <th><label for="file-1@description" i18n:translate="">File Description</label>:</th> | 168 <td colspan=4> |
| 167 <td colspan=3><input type="edit" id="file-1@description" name="file-1@description" size="40"></td> | 169 <textarea readonly id="DropZone"> |
| 170 paste images or drag and drop files here.... | |
| 171 </textarea> | |
| 172 </td> | |
| 168 </tr> | 173 </tr> |
| 169 </table> | 174 </table> |
| 170 </fieldset> | 175 </fieldset> |
| 171 <table class="form"> | 176 <table class="form"> |
| 172 <tr tal:condition="context/is_edit_ok"> | 177 <tr tal:condition="context/is_edit_ok"> |
| 181 i18n:translate="">Make a copy</a> | 186 i18n:translate="">Make a copy</a> |
| 182 </td> | 187 </td> |
| 183 </tr> | 188 </tr> |
| 184 </table> | 189 </table> |
| 185 </form> | 190 </form> |
| 191 | |
| 192 <!-- drag and drop code --> | |
| 193 <script tal:attributes="nonce request/client/client_nonce"> | |
| 194 /* multiple file drops cause issues with redefined | |
| 195 file-X@content issues. input multiple assumes | |
| 196 it can start numbering from 1 for each of the | |
| 197 multiple files. However numbering here does the | |
| 198 same leading to duplicate file-2@content. | |
| 199 | |
| 200 layout needs some work, alignnment of new file | |
| 201 input's isn't great. | |
| 202 | |
| 203 Need a way to delete or reset file inputs so file | |
| 204 assigned to them isn't uploaded. Clicking on button | |
| 205 in chrome and then canceling unsets the file. But this | |
| 206 sequence does nothing in firefox. | |
| 207 | |
| 208 Pasting always uses image.<type> can't name file. | |
| 209 Need to query user during paste for name/description. | |
| 210 */ | |
| 211 | |
| 212 let newInput=null; | |
| 213 let NextInputNum = 100; /* file input 1 is hardcoded in form. | |
| 214 It is a multiple file input control. To | |
| 215 prevent collision, we start dynamic file controls at | |
| 216 file-100@content. 100 is larger than we expect | |
| 217 the number of files uploaded using file input 1.*/ | |
| 218 | |
| 219 let target = document.getElementById('DropZone'); | |
| 220 target.style.display = "block"; | |
| 221 let body = document.body; | |
| 222 let fileInput = document.getElementById('file-1@content'); | |
| 223 let fileDesc = document.getElementById('file-1@description'); | |
| 224 | |
| 225 function add_file_input () { | |
| 226 | |
| 227 // Only allow one change listener on newest input. | |
| 228 fileInput.removeEventListener('change', | |
| 229 add_file_input, | |
| 230 false); | |
| 231 | |
| 232 /* create new file input to get next dragged file */ | |
| 233 /* <input type="file" name="file-2@content"> for 2, | |
| 234 3, 4, ... */ | |
| 235 newInput=document.createElement('input'); | |
| 236 newInput.type="file"; | |
| 237 newInput.id="file-" + NextInputNum +"@content"; | |
| 238 newInput.name=newInput.id; | |
| 239 fileInput = fileInput.insertAdjacentElement('afterend', | |
| 240 newInput); | |
| 241 // add change hander to newest file input | |
| 242 fileInput.addEventListener('change', | |
| 243 add_file_input, // create new input for more files | |
| 244 false); | |
| 245 | |
| 246 /* link file-N to list of files on issue. | |
| 247 also link to msg-1 */ | |
| 248 addLink=document.createElement('input'); | |
| 249 addLink.type="hidden"; | |
| 250 addLink.id="@link@file=file-" + NextInputNum; | |
| 251 addLink.name="@link@files" | |
| 252 addLink.value="file-" + NextInputNum; | |
| 253 fileInput.insertAdjacentElement('afterend', addLink); | |
| 254 | |
| 255 addLink=document.createElement('input'); | |
| 256 addLink.type="hidden"; | |
| 257 addLink.id="msg-1@link@files=file-" + NextInputNum; | |
| 258 addLink.name="msg-1@link@files" | |
| 259 addLink.value="file-" + NextInputNum | |
| 260 fileInput.insertAdjacentElement('afterend', addLink); | |
| 261 | |
| 262 addLink=document.createElement('input'); | |
| 263 addLink.type="edit"; | |
| 264 addLink.id="file-" + NextInputNum + "@description"; | |
| 265 addLink.name=addLink.id | |
| 266 addLink.size = 40 | |
| 267 fileDesc=fileDesc.insertAdjacentElement('afterend', addLink); | |
| 268 | |
| 269 NextInputNum = NextInputNum+1; | |
| 270 | |
| 271 } | |
| 272 | |
| 273 function MarkDropZone(e, active) { | |
| 274 active == true ? e.style.backgroundColor = "goldenrod" : | |
| 275 e.style.backgroundColor = ""; | |
| 276 } | |
| 277 fileInput.addEventListener('change', | |
| 278 add_file_input, // create new input for more files | |
| 279 false); | |
| 280 | |
| 281 target.addEventListener('dragover', (e) => { | |
| 282 e.preventDefault(); | |
| 283 body.classList.add('dragging'); | |
| 284 }); | |
| 285 | |
| 286 target.addEventListener('dragenter', (e) => { | |
| 287 e.preventDefault(); | |
| 288 MarkDropZone(target, true); | |
| 289 }); | |
| 290 | |
| 291 | |
| 292 target.addEventListener('dragleave', (e) => { | |
| 293 e.preventDefault(); | |
| 294 MarkDropZone(target, false); | |
| 295 }); | |
| 296 | |
| 297 target.addEventListener('dragleave', () => { | |
| 298 body.classList.remove('dragging'); | |
| 299 }); | |
| 300 | |
| 301 target.addEventListener('drop', (e) => { | |
| 302 body.classList.remove('dragging'); | |
| 303 MarkDropZone(target, false); | |
| 304 | |
| 305 // Only allow single file drop unless | |
| 306 // fileInput name is @file that can support | |
| 307 // multiple file drop and file drop is multiple. | |
| 308 if (( fileInput.name != "@file" || | |
| 309 ! fileInput.hasAttribute('multiple')) && | |
| 310 e.dataTransfer.files.length != 1 ) { | |
| 311 alert("File input can only accept one file.") | |
| 312 e.preventDefault(); | |
| 313 return | |
| 314 } | |
| 315 // set file input files to the dragged files | |
| 316 fileInput.files = e.dataTransfer.files; | |
| 317 | |
| 318 add_file_input(); // create new input for more files | |
| 319 // run last otherwise firefox empties e.dataTransfer | |
| 320 e.preventDefault(); | |
| 321 }); | |
| 322 | |
| 323 target.addEventListener('mouseover', (e) => { | |
| 324 e.preventDefault(); | |
| 325 MarkDropZone(target, true); | |
| 326 }); | |
| 327 | |
| 328 target.addEventListener('mouseout', (e) => { | |
| 329 e.preventDefault(); | |
| 330 MarkDropZone(target, false); | |
| 331 }); | |
| 332 | |
| 333 target.addEventListener('paste', (event) => { | |
| 334 // https://mobiarch.wordpress.com/2013/09/25/upload-image-by-copy-and-paste/ | |
| 335 // add paste event listener to the page | |
| 336 | |
| 337 // https://stackoverflow.com/questions/50427513/ | |
| 338 // html-paste-clipboard-image-to-file-input | |
| 339 if ( event.clipboardData.files.length == 0) { | |
| 340 // if not file data alert | |
| 341 alert("No image found for pasting"); | |
| 342 event.preventDefault(); | |
| 343 return; | |
| 344 } | |
| 345 fileInput.files = event.clipboardData.files; | |
| 346 | |
| 347 /* Possible enhancement if file check fails. | |
| 348 iterate over all items 0 ...: | |
| 349 event.clipboardData.items.length | |
| 350 look at all items[i].kind for 'string' and | |
| 351 items[i].type looking for a text/plain item. If | |
| 352 found, | |
| 353 event.clipboardData.items[1].getAsString( | |
| 354 callback_fcn(s)) | |
| 355 | |
| 356 where callback function that creates a new | |
| 357 dataTransfer object with a file and insert the | |
| 358 content s and assigns it to the input. | |
| 359 | |
| 360 https://gist.github.com/guest271314/7eac2c21911f5e40f489\33ac78e518bd | |
| 361 */ | |
| 362 add_file_input(); // create new input for more files | |
| 363 // do not paste contents to dropzone | |
| 364 event.preventDefault(); | |
| 365 }, false); | |
| 366 </script> | |
| 367 <style tal:attributes="nonce request/client/client_nonce"> | |
| 368 #FileArea input[type=file] ~ input[type=file] {display:block;} | |
| 369 #DropZone { /* don't display dropzone by default. | |
| 370 Displayed as block by javascript. */ | |
| 371 display:none; | |
| 372 width: 100%; | |
| 373 /* override textarea inset */ | |
| 374 border-style: dashed; | |
| 375 padding: 3ex 0; /* larger dropzone */ | |
| 376 /* add space below inputs */ | |
| 377 margin-block-start: 1em; | |
| 378 /* lighter color */ | |
| 379 background: rgba(255,255,255,0.4); | |
| 380 } | |
| 381 </style> | |
| 186 | 382 |
| 187 <p tal:condition="context/id" i18n:translate=""> | 383 <p tal:condition="context/id" i18n:translate=""> |
| 188 Created on <b><tal:x replace="python:context.creation.pretty('%Y-%m-%d %H:%M')" i18n:name="creation" /></b> | 384 Created on <b><tal:x replace="python:context.creation.pretty('%Y-%m-%d %H:%M')" i18n:name="creation" /></b> |
| 189 by <b><tal:x replace="context/creator" i18n:name="creator" /></b>, | 385 by <b><tal:x replace="context/creator" i18n:name="creator" /></b>, |
| 190 last changed <b><tal:x replace="python:context.activity.pretty('%Y-%m-%d %H:%M')" i18n:name="activity" /></b> | 386 last changed <b><tal:x replace="python:context.activity.pretty('%Y-%m-%d %H:%M')" i18n:name="activity" /></b> |
| 257 </div> | 453 </div> |
| 258 | 454 |
| 259 </td> | 455 </td> |
| 260 | 456 |
| 261 </tal:block> | 457 </tal:block> |
| 458 <!-- SHA: ad841842c0da5f9d1a7f69a1e0c847a549b75bf2 --> |
