]> BookStack Code Mirror - bookstack/blob - resources/js/components/dropzone.js
Dropzone: Developed ux further
[bookstack] / resources / js / components / dropzone.js
1 import {Component} from './component';
2 import {Clipboard} from '../services/clipboard';
3 import {
4     elem, getLoading, onSelect, removeLoading,
5 } from '../services/dom';
6
7 export class Dropzone extends Component {
8
9     setup() {
10         this.container = this.$el;
11         this.statusArea = this.$refs.statusArea;
12         this.dropTarget = this.$refs.dropTarget;
13         this.selectButtons = this.$manyRefs.selectButton || [];
14
15         this.url = this.$opts.url;
16         this.successMessage = this.$opts.successMessage;
17         this.removeMessage = this.$opts.removeMessage;
18         this.uploadLimit = Number(this.$opts.uploadLimit); // TODO - Use
19         this.uploadLimitMessage = this.$opts.uploadLimitMessage; // TODO - Use
20         this.timeoutMessage = this.$opts.timeoutMessage; // TODO - Use
21         this.zoneText = this.$opts.zoneText;
22         // window.uploadTimeout // TODO - Use
23
24         this.setupListeners();
25     }
26
27     setupListeners() {
28         onSelect(this.selectButtons, this.manualSelectHandler.bind(this));
29
30         let depth = 0;
31
32         this.dropTarget.addEventListener('dragenter', event => {
33             event.preventDefault();
34             depth += 1;
35
36             if (depth === 1) {
37                 this.showOverlay();
38             }
39         });
40
41         this.dropTarget.addEventListener('dragover', event => {
42             event.preventDefault();
43         });
44
45         const reset = () => {
46             this.hideOverlay();
47             depth = 0;
48         };
49
50         this.dropTarget.addEventListener('dragend', event => {
51             reset();
52         });
53
54         this.dropTarget.addEventListener('dragleave', event => {
55             depth -= 1;
56             if (depth === 0) {
57                 reset();
58             }
59         });
60
61         this.dropTarget.addEventListener('drop', event => {
62             event.preventDefault();
63             reset();
64             const clipboard = new Clipboard(event.dataTransfer);
65             const files = clipboard.getFiles();
66             for (const file of files) {
67                 this.createUploadFromFile(file);
68             }
69         });
70     }
71
72     manualSelectHandler() {
73         const input = elem('input', {type: 'file', style: 'left: -400px; visibility: hidden; position: fixed;'});
74         this.container.append(input);
75         input.click();
76         input.addEventListener('change', event => {
77             for (const file of input.files) {
78                 this.createUploadFromFile(file);
79             }
80             input.remove();
81         });
82     }
83
84     showOverlay() {
85         const overlay = this.dropTarget.querySelector('.dropzone-overlay');
86         if (!overlay) {
87             const zoneElem = elem('div', {class: 'dropzone-overlay'}, [this.zoneText]);
88             this.dropTarget.append(zoneElem);
89         }
90     }
91
92     hideOverlay() {
93         const overlay = this.dropTarget.querySelector('.dropzone-overlay');
94         if (overlay) {
95             overlay.remove();
96         }
97     }
98
99     /**
100      * @param {File} file
101      * @return {Upload}
102      */
103     createUploadFromFile(file) {
104         const {dom, status, progress, dismiss} = this.createDomForFile(file);
105         this.statusArea.append(dom);
106         const component = this;
107
108         const upload = {
109             file,
110             dom,
111             updateProgress(percentComplete) {
112                 progress.textContent = `${percentComplete}%`;
113                 progress.style.width = `${percentComplete}%`;
114             },
115             markError(message) {
116                 status.setAttribute('data-status', 'error');
117                 status.textContent = message;
118                 removeLoading(dom);
119             },
120             markSuccess(message) {
121                 status.setAttribute('data-status', 'success');
122                 status.textContent = message;
123                 removeLoading(dom);
124                 setTimeout(dismiss, 2400);
125                 component.$emit('upload-success', {
126                     name: file.name,
127                 });
128             },
129         };
130
131         this.startXhrForUpload(upload);
132
133         return upload;
134     }
135
136     /**
137      * @param {Upload} upload
138      */
139     startXhrForUpload(upload) {
140         const formData = new FormData();
141         formData.append('file', upload.file, upload.file.name);
142
143         const req = window.$http.createXMLHttpRequest('POST', this.url, {
144             error() {
145                 upload.markError('Upload failed'); // TODO - Update text
146             },
147             readystatechange() {
148                 if (this.readyState === XMLHttpRequest.DONE && this.status === 200) {
149                     upload.markSuccess('Finished upload!');
150                 } else if (this.readyState === XMLHttpRequest.DONE && this.status >= 400) {
151                     const content = this.responseText;
152                     const data = content.startsWith('{') ? JSON.parse(content) : {message: content};
153                     const message = data?.message || content;
154                     upload.markError(message);
155                 }
156             },
157         });
158
159         req.upload.addEventListener('progress', evt => {
160             const percent = Math.min(Math.ceil((evt.loaded / evt.total) * 100), 100);
161             upload.updateProgress(percent);
162         });
163
164         req.setRequestHeader('Accept', 'application/json');
165         req.send(formData);
166     }
167
168     /**
169      * @param {File} file
170      * @return {{image: Element, dom: Element, progress: Element, status: Element, dismiss: function}}
171      */
172     createDomForFile(file) {
173         const image = elem('img', {src: ''});
174         const status = elem('div', {class: 'dropzone-file-item-status'}, []);
175         const progress = elem('div', {class: 'dropzone-file-item-progress'});
176         const imageWrap = elem('div', {class: 'dropzone-file-item-image-wrap'}, [image]);
177
178         const dom = elem('div', {class: 'dropzone-file-item'}, [
179             imageWrap,
180             elem('div', {class: 'dropzone-file-item-text-wrap'}, [
181                 elem('div', {class: 'dropzone-file-item-label'}, [file.name]),
182                 getLoading(),
183                 status,
184             ]),
185             progress,
186         ]);
187
188         if (file.type.startsWith('image/')) {
189             image.src = URL.createObjectURL(file);
190         }
191
192         const dismiss = () => {
193             dom.classList.add('dismiss');
194             dom.addEventListener('animationend', event => {
195                 dom.remove();
196             });
197         };
198
199         dom.addEventListener('click', dismiss);
200
201         return {
202             dom, progress, status, dismiss,
203         };
204     }
205
206 }
207
208 /**
209  * @typedef Upload
210  * @property {File} file
211  * @property {Element} dom
212  * @property {function(Number)} updateProgress
213  * @property {function(String)} markError
214  * @property {function(String)} markSuccess
215  */