3f6a33519f4372f64f0102c0e3d1b3eda048577a
[citadel.git] / webcit-ng / static / js / upload.js
1 // Handle any tasks which require uploading files to the server (such as attachments)
2 // h/t https://www.smashingmagazine.com/2018/01/drag-drop-file-uploader-vanilla-js/ which inspired the design of this module
3 //
4 // Copyright (c) 2016-2023 by the citadel.org team
5 //
6 // This program is open source software.  Use, duplication, or
7 // disclosure are subject to the GNU General Public License v3.
8
9 var uploads_in_progress = 0;
10 var uploads = [] ;                                                              // everything the user has uploaded
11 var attachment_counter_divs = [] ;                                              // list of divs containing attachment counters
12
13 // Remove the upload window completely (even if it's hidden)
14 function deactivate_uploads() {
15         upload_window = document.getElementById('ctdl-upload');
16         if (upload_window) {
17                 upload_window.remove();
18         }
19 }
20
21
22 // Turn the specified div into a place where we can upload.  (Note: permanently changes the drag-and-drop behavior of that div.)
23 function activate_uploads(parent_div) {
24
25                 attachment_counter_divs = [ "num_attachments" ] ;
26
27                 document.getElementById(parent_div).innerHTML += `
28                         <div class="ctdl-upload" id="ctdl-upload">
29                                 <div id="ctdl_attachments_title" class="ctdl-compose-attachments-title">
30                                         <div><h1><i class="fa fa-paperclip" style="color:grey"></i>` + _("Attachments:") + ` <span id="${attachment_counter_divs[0]}">` + uploads.length + `</span></h1></div>
31                                         <div><h1><i class="fas fa-window-close" style="color:red" onClick="show_or_hide_upload_window()"></i></h1></div>
32                                 </div>
33                                 <br>
34                                 <ul id="ctdl-upload_list">
35                                 </ul>
36                                 <br>
37                                 <div id="drop-area" class="ctdl-upload-drop-area">
38                                         <form class="ctdl-upload-form">
39                                                 <p>${_("Drop files here to upload")}</p>
40                                                 <input type="file" id="fileElem" multiple accept="*/*" onChange="handle_upload_files(this.files)">
41                                         </form>
42                                 </div>
43                         </div>
44                 `;
45
46                 // activate drag and drop
47                 let dropArea = document.getElementById(parent_div);
48                 ;['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
49                         dropArea.addEventListener(eventName, upload_prevent_defaults, false)
50                 })
51                 ;["dragenter", "dragover"].forEach(eventName => {
52                         dropArea.addEventListener(eventName, upload_highlight, false)
53                 })
54                 ;['dragleave', 'drop'].forEach(eventName => {
55                         dropArea.addEventListener(eventName, upload_unhighlight, false)
56                 })
57                 dropArea.addEventListener('drop', upload_handle_drop, false);
58 }
59
60
61 // prevent drag and drop events from propagating up through the DOM
62 function upload_prevent_defaults(e) {
63         e.preventDefault();
64         e.stopPropagation();
65 }
66
67
68 function upload_handle_drop(e) {
69         let dt = e.dataTransfer;
70         let files = dt.files;
71         handle_upload_files(files);
72 }
73
74
75 function handle_upload_files(files) {
76         ([...files]).forEach(upload_file)
77 }
78
79
80 // update the attachment counter div(s) with the current number of items
81 function update_attachment_count() {
82         for (let i = 0; i < attachment_counter_divs.length; ++i) {
83                 document.getElementById(attachment_counter_divs[i]).innerHTML = uploads.length;
84         }
85 }
86
87
88 // Delete an uploaded item from the list
89 delete_upload = async(ref) => {
90
91         response = await fetch(
92                 "/ctdl/p/" + ref, { method: "DELETE" }
93         );
94
95         if (response.ok) {                                                                      // If the server accepted the delete...
96                 var el = document.getElementById(ref);                                          // ...remove it from the screen...
97                 el.parentNode.removeChild(el);
98                 uploads = uploads.filter((r) => r.ref != ref);                                  // ...remove it from the array...
99                 update_attachment_count();
100         }
101 }
102
103
104 // When an upload (or reupload) is complete, call this function to add it to the on-screen attachments list.
105 function add_upload_to_displayed_list(item) {
106         let new_upl = document.createElement("li");
107         new_upl.setAttribute("id", item["ref"]);        // set the element id to the upload reference
108         new_upl.innerHTML = `<i class="fa-solid fa-circle-xmark" style="color:red" onClick="delete_upload('` + item["ref"] + `')"></i>`
109         + `&nbsp;`
110         + item["uploadfilename"] + " (" + item["contenttype"] + ", " + item["contentlength"] + " " + _("bytes") + ")";
111         document.getElementById("ctdl-upload_list").appendChild(new_upl);
112
113         // append it to the global list of uploads
114         uploads.push(item);
115         update_attachment_count();
116 }
117
118
119 function upload_file(file) {
120         var url = '/ctdl/p/';
121         var xhr = new XMLHttpRequest();
122         var formData = new FormData();
123         xhr.open('POST', url, true);
124       
125         xhr.addEventListener('readystatechange', function(e) {
126                 if (xhr.readyState == 4 && xhr.status == 200) {
127                         // remove the "uploading in progress" message
128                         let li = document.getElementById("ctdl_uploading_" + uploads_in_progress.toString());
129                         li.parentNode.removeChild(li);
130                         uploads_in_progress -= 1;
131
132                         // The response body will be a JSON array of completed uploads.
133                         var j_response = JSON.parse(xhr.response);
134
135                         // Add these uploads to the displayed list
136                         j_response.forEach((item) => add_upload_to_displayed_list(item));
137                 }
138                 else if (xhr.readyState == 4 && xhr.status != 200) {
139                         // remove the "uploading in progress" message (there was an error, so just let it disappear)
140                         let li = document.getElementById("ctdl_uploading_" + uploads_in_progress.toString());
141                         li.parentNode.removeChild(li);
142                         uploads_in_progress -= 1;
143                 }
144         })
145  
146         formData.append('file', file);
147         xhr.send(formData);
148         uploads_in_progress += 1;
149
150         // Make an "uploading in progress" message appear in the uploads list!
151         progress = document.createElement("li");
152         progress.setAttribute("id", "ctdl_uploading_" + uploads_in_progress.toString());
153         progress.innerHTML = `<img src="/ctdl/s/images/dotcrawl.gif" /> ` + _("Processing dropped files...");
154         document.getElementById("ctdl-upload_list").appendChild(progress);
155 }
156
157
158 // called when the user drags a file into the upload area
159 function upload_highlight(e) {
160         let dropArea = document.getElementById("ctdl-upload");
161         dropArea.classList.add('highlight')
162
163         document.getElementById("ctdl-upload").style.display = "block";                 // also make it appear
164 }
165
166
167 // called when the user is no longer dragging a file into the upload area
168 function upload_unhighlight(e) {
169         let dropArea = document.getElementById("ctdl-upload");
170         dropArea.classList.remove('highlight')
171 }
172
173
174 // Show or hide the attachments window in the composer
175 function show_or_hide_upload_window() {
176
177         if (document.getElementById("ctdl-upload").style.display == "block") {
178                 document.getElementById("ctdl-upload").style.display = "none";          // turn it off
179         }
180         else {
181                 document.getElementById("ctdl-upload").style.display = "block";         // turn it on
182         }
183 }
184
185
186 // Helper function for flush_uploads()
187 flush_one_upload = async(ref) => {
188         response = await fetch(
189                 "/ctdl/p/" + ref, { method: "DELETE" }
190         );
191         // We don't have any interest in the server response.
192 }
193
194
195 // Flush all uploaded files and close the window
196 function flush_uploads() {
197         upload_window = document.getElementById('ctdl-upload');
198
199         if (upload_window) {
200                 upload_window.style.display='none';
201         }
202
203         // tell the server to delete the files
204         uploads.forEach(u => {
205                 flush_one_upload(u.ref);
206         });
207         uploads=[];
208         update_attachment_count();
209
210         deactivate_uploads();   // this makes the window get destroyed too
211 }
212
213
214 // Preload a forwarded message's attachments into the new copy currently being composed.
215 function forward_attachments(msgnum) {
216         fetch(
217                 "/ctdl/p/" + msgnum, { method: "GET" }
218         )
219         .then(response => response.json())
220         .then(j => {
221                 j.forEach((item) => {
222                         add_upload_to_displayed_list(item);
223                 })
224         })
225 }