Roundup Tracker

Allow users to upload multiple files using:

Unlike a number of tools, this uses the native html file input rather than ajax. So nothing is changed until the form is submitted. This is a progressive enhancement. If javascript is disabled, the dropzone is not shown and the user can use the original file input to upload a file.

When a file is attached to the existing file input, a new empty file input is created. This happens even if the user browses the filesystem using the input button.

Screenshots

Drag a file into the dropzone to load the second file. The first file "image.png" is a screenshot that was pasted from the clipboard.

FileDragDrop.png

Note the drop zone has changed color when hovering over it. After dropping the file, a new input is created and the old (2nd) input has the file attached:

FileDropped.png

The drop zone is still focused (blue outline) from pasting the original screenshot.

Implementation

Three changes have to be made to implement this:

  1. Change the template to add the html elements for the dropzone and div's for operating on the file inputs.
  2. Add css for the new elements
  3. Add javascript that handles the actions.

Template Changes

Move the <input type="file" name="@file"> input so it looks like:

   1    <div id="FileArea">
   2      <label class="button_label" for="@file">Add File:</label>
   3      <input type="file" id="@file" name="@file" multiple />
   4      <textarea readonly id="DropZone">
   5        paste images or drag and drop files here....
   6      </textarea>
   7    </div>

The textarea allows pasting and dropping even though it is readonly. Readonly prevents editing the message text. A div must have contenteditable=true to allow pasting. However with contenteditable=true you can then edit the text in the div and readonly, disabled etc. can't prevent that.

CSS Additions

Add the following style block to your template or add the styles to your stylesheet:

   1    <style tal:attributes="nonce request/client/client_nonce">
   2      #FileArea input[type=file] ~ input[type=file] {display:block;}
   3      #DropZone {     /* don't display dropzone by default.
   4                         Displayed as block by javascript. */
   5                   display:none;
   6                      /* override textarea inset */
   7                   border-style: solid;
   8                   padding: 3ex 0; /* larger dropzone */
   9                   /* add space below inputs */
  10                   margin-block-start: 1em;
  11                      /* lighter color */
  12                   background: rgba(255,255,255,0.4);
  13                }
  14    </style>

Javascript

Then add the following javascript to the template or include it from a script file:

   1    <script tal:attributes="nonce request/client/client_nonce">
   2      /* multiple file drops cause issues with redefined
   3         file-X@content issues. input multiple assumes
   4         it can start numbering from 1 for each of the
   5         multiple files. However numbering here does the
   6         same leading to duplicate file-2@content.
   7 
   8         layout needs some work, alignnment of new file
   9         input's isn't great.
  10 
  11         Need a way to delete or reset file inputs so file
  12         assigned to them isn't uploaded. Clicking on button
  13         in chrome and then canceling unsets the file. But this
  14         sequence does nothing in firefox.
  15 
  16         Pasting always uses image.<type> can't name file.
  17         Need to query user during paste for name/description.
  18      */
  19 
  20      let newInput=null;
  21      let NextInputNum = 100; /* file input 1 is hardcoded in form.
  22              It is a multiple file input control. To
  23              prevent collision, we start dynamic file controls at
  24              file-100@content. 100 is larger than we expect
  25              the number of files uploaded using file input 1.*/
  26 
  27      let target = document.getElementById('DropZone');
  28      target.style.display = "block";
  29      let body = document.body;
  30      let fileInput = document.getElementById('@file');
  31 
  32      function add_file_input () {
  33 
  34       // Only allow one change listener on newest input.
  35       fileInput.removeEventListener('change',
  36                add_file_input,
  37                false);
  38 
  39        /* create new file input to get next dragged file */
  40        /* <input type="file" name="file-2@content"> for 2,
  41           3, 4, ... */
  42        newInput=document.createElement('input');
  43        newInput.type="file";
  44        newInput.id="file-" + NextInputNum +"@content";
  45        newInput.name=newInput.id;
  46        fileInput = fileInput.insertAdjacentElement('afterend',
  47                                                     newInput);
  48        // add change hander to newest file input
  49        fileInput.addEventListener('change',
  50              add_file_input, // create new input for more files
  51              false);
  52 
  53        /* link file-N to list of files on issue.
  54           also link to msg-1 */
  55        addLink=document.createElement('input');
  56        addLink.type="hidden";
  57        addLink.id="@link@file=file-" + NextInputNum;
  58        addLink.name="@link@files"
  59        addLink.value="file-" + NextInputNum;
  60        fileInput.insertAdjacentElement('afterend', addLink);
  61 
  62        addLink=document.createElement('input');
  63        addLink.type="hidden";
  64        addLink.id="msg-1@link@files=file-" + NextInputNum;
  65        addLink.name="msg-1@link@files"
  66        addLink.value="file-" + NextInputNum
  67        fileInput.insertAdjacentElement('afterend', addLink);
  68 
  69        NextInputNum = NextInputNum+1;
  70 
  71      }
  72 
  73      function MarkDropZone(e, active) {
  74          active == true ? e.style.backgroundColor = "goldenrod" :
  75          e.style.backgroundColor = "";
  76      }
  77      fileInput.addEventListener('change',
  78               add_file_input, // create new input for more files
  79               false);
  80 
  81      target.addEventListener('dragover', (e) => {
  82          e.preventDefault();
  83          body.classList.add('dragging');
  84      });
  85 
  86      target.addEventListener('dragenter', (e) => {
  87          e.preventDefault();
  88          MarkDropZone(target, true);
  89      });
  90 
  91      target.addEventListener('dragleave', (e) => {
  92          e.preventDefault();
  93          MarkDropZone(target, false);
  94      });
  95 
  96      target.addEventListener('dragleave', () => {
  97          body.classList.remove('dragging');
  98      });
  99 
 100      target.addEventListener('drop', (e) => {
 101          body.classList.remove('dragging');
 102          MarkDropZone(target, false);
 103 
 104          // Only allow single file drop unless
 105          // fileInput name is @file that can support
 106          // multiple file drop and file drop is multiple.
 107          if (( fileInput.name != "@file" ||
 108                  ! fileInput.hasAttribute('multiple')) &&
 109                e.dataTransfer.files.length != 1 ) {
 110              alert("File input can only accept one file.")
 111              e.preventDefault();
 112              return
 113          }
 114 
 115          // set file input files to the dragged files
 116          fileInput.files = e.dataTransfer.files;
 117 
 118          add_file_input(); // create new input for more files
 119          // run last otherwise firefox empties e.dataTransfer
 120          e.preventDefault();
 121      });
 122 
 123      target.addEventListener('mouseover', (e) => {
 124          e.preventDefault();
 125          MarkDropZone(target, true);
 126      });
 127 
 128      target.addEventListener('mouseout', (e) => {
 129          e.preventDefault();
 130          MarkDropZone(target, false);
 131      });
 132 
 133 
 134      target.addEventListener('paste', (event) => {
 135          // https://mobiarch.wordpress.com/2013/09/25/upload-image-by-copy-and-paste/
 136          // add paste event listener to the page
 137 
 138          // https://stackoverflow.com/questions/50427513/
 139          // html-paste-clipboard-image-to-file-input
 140          if ( event.clipboardData.files.length == 0) {
 141             // if not file data alert
 142             alert("No image found for pasting");
 143             event.preventDefault();
 144             return;
 145          }
 146          fileInput.files = event.clipboardData.files;
 147 
 148          /* Possible enhancement if file check fails.
 149             iterate over all items 0 ...:
 150                 event.clipboardData.items.length
 151             look at all items[i].kind for 'string' and
 152             items[i].type looking for a text/plain item. If
 153             found,
 154               event.clipboardData.items[1].getAsString(
 155                 callback_fcn(s))
 156 
 157             where callback function that creates a new
 158             dataTransfer object with a file and insert the
 159             content s and assigns it to the input.
 160 
 161             https://gist.github.com/guest271314/7eac2c21911f5e40f48933ac78e518bd
 162          */
 163          add_file_input(); // create new input for more files
 164          // do not paste contents to dropzone
 165          event.preventDefault();
 166      }, false);
 167    </script>


CategoryInterfaceWeb CategoryJavascript