]> BookStack Code Mirror - bookstack/blob - resources/js/components/dropzone.js
Dropzone: Swapped fetch for XHR for progress tracking
[bookstack] / resources / js / components / dropzone.js
1 import {Component} from './component';
2 import {Clipboard} from '../services/clipboard';
3
4 export class Dropzone extends Component {
5
6     setup() {
7         this.container = this.$el;
8         this.url = this.$opts.url;
9         this.successMessage = this.$opts.successMessage;
10         this.removeMessage = this.$opts.removeMessage;
11         this.uploadLimit = Number(this.$opts.uploadLimit); // TODO - Use
12         this.uploadLimitMessage = this.$opts.uploadLimitMessage; // TODO - Use
13         this.timeoutMessage = this.$opts.timeoutMessage; // TODO - Use
14         // window.uploadTimeout // TODO - Use
15         // TODO - Click-to-upload buttons/areas
16         // TODO - Drop zone highlighting of existing element
17         //   (Add overlay via additional temp element).
18
19         this.setupListeners();
20     }
21
22     setupListeners() {
23         this.container.addEventListener('dragenter', event => {
24             this.container.style.border = '1px dotted tomato';
25             event.preventDefault();
26         });
27
28         this.container.addEventListener('dragover', event => {
29             event.preventDefault();
30         });
31
32         const reset = () => {
33             this.container.style.border = null;
34         };
35
36         this.container.addEventListener('dragend', event => {
37             reset();
38         });
39
40         this.container.addEventListener('dragleave', event => {
41             reset();
42         });
43
44         this.container.addEventListener('drop', event => {
45             event.preventDefault();
46             const clipboard = new Clipboard(event.dataTransfer);
47             const files = clipboard.getFiles();
48             for (const file of files) {
49                 this.createUploadFromFile(file);
50             }
51         });
52     }
53
54     /**
55      * @param {File} file
56      * @return {Upload}
57      */
58     createUploadFromFile(file) {
59         const {dom, status, progress} = this.createDomForFile(file);
60         this.container.append(dom);
61
62         const upload = {
63             file,
64             dom,
65             updateProgress(percentComplete) {
66                 console.log(`progress: ${percentComplete}%`);
67                 progress.textContent = `${percentComplete}%`;
68                 progress.style.width = `${percentComplete}%`;
69             },
70             markError(message) {
71                 status.setAttribute('data-status', 'error');
72                 status.textContent = message;
73             },
74             markSuccess(message) {
75                 status.setAttribute('data-status', 'success');
76                 status.textContent = message;
77             },
78         };
79
80         this.startXhrForUpload(upload);
81
82         return upload;
83     }
84
85     /**
86      * @param {Upload} upload
87      */
88     startXhrForUpload(upload) {
89         const formData = new FormData();
90         formData.append('file', upload.file, upload.file.name);
91
92         const req = window.$http.createXMLHttpRequest('POST', this.url, {
93             error() {
94                 upload.markError('Upload failed'); // TODO - Update text
95             },
96             readystatechange() {
97                 if (this.readyState === XMLHttpRequest.DONE && this.status === 200) {
98                     upload.markSuccess('Finished upload!');
99                 } else if (this.readyState === XMLHttpRequest.DONE && this.status >= 400) {
100                     const content = this.responseText;
101                     const data = content.startsWith('{') ? JSON.parse(content) : {message: content};
102                     const message = data?.message || content;
103                     upload.markError(message);
104                 }
105             },
106         });
107
108         req.upload.addEventListener('progress', evt => {
109             const percent = Math.min(Math.ceil((evt.loaded / evt.total) * 100), 100);
110             upload.updateProgress(percent);
111         });
112
113         req.setRequestHeader('Accept', 'application/json');
114         req.send(formData);
115     }
116
117     /**
118      * @param {File} file
119      * @return {{image: Element, dom: Element, progress: Element, label: Element, status: Element}}
120      */
121     createDomForFile(file) {
122         const dom = document.createElement('div');
123         const label = document.createElement('div');
124         const status = document.createElement('div');
125         const progress = document.createElement('div');
126         const image = document.createElement('img');
127
128         dom.classList.add('dropzone-file-item');
129         status.classList.add('dropzone-file-item-status');
130         progress.classList.add('dropzone-file-item-progress');
131
132         image.src = ''; // TODO - file icon
133         label.innerText = file.name;
134
135         if (file.type.startsWith('image/')) {
136             image.src = URL.createObjectURL(file);
137         }
138
139         dom.append(image, label, progress, status);
140         return {
141             dom, label, image, progress, status,
142         };
143     }
144
145 }
146
147 /**
148  * @typedef Upload
149  * @property {File} file
150  * @property {Element} dom
151  * @property {function(Number)} updateProgress
152  * @property {function(String)} markError
153  * @property {function(String)} markSuccess
154  */