Allow users to upload multiple files using:
- normal file selection using the file upload selection button
- drag and drop of a file onto the page's dropzone
- paste a screenshot from the clipboard
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.
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:
The drop zone is still focused (blue outline) from pasting the original screenshot.
Implementation
Three changes have to be made to implement this:
- Change the template to add the html elements for the dropzone and div's for operating on the file inputs.
- Add css for the new elements
- Add javascript that handles the actions.
Template Changes
Move the <input type="file" name="@file"> input so it looks like:
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>