]> BookStack Code Mirror - bookstack/blob - resources/js/components/dropzone.js
Dropzone: started on design/ui of uploading
[bookstack] / resources / js / components / dropzone.js
1 import {Component} from './component';
2 import {Clipboard} from '../services/clipboard';
3 import {
4     elem, getLoading, 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
13         this.url = this.$opts.url;
14         this.successMessage = this.$opts.successMessage;
15         this.removeMessage = this.$opts.removeMessage;
16         this.uploadLimit = Number(this.$opts.uploadLimit); // TODO - Use
17         this.uploadLimitMessage = this.$opts.uploadLimitMessage; // TODO - Use
18         this.timeoutMessage = this.$opts.timeoutMessage; // TODO - Use
19         this.zoneText = this.$opts.zoneText;
20         // window.uploadTimeout // TODO - Use
21         // TODO - Click-to-upload buttons/areas
22         // TODO - Drop zone highlighting of existing element
23         //   (Add overlay via additional temp element).
24
25         this.setupListeners();
26     }
27
28     setupListeners() {
29         let depth = 0;
30
31         this.container.addEventListener('dragenter', event => {
32             event.preventDefault();
33             depth += 1;
34
35             if (depth === 1) {
36                 this.showOverlay();
37             }
38         });
39
40         this.container.addEventListener('dragover', event => {
41             event.preventDefault();
42         });
43
44         const reset = () => {
45             this.hideOverlay();
46             depth = 0;
47         };
48
49         this.container.addEventListener('dragend', event => {
50             reset();
51         });
52
53         this.container.addEventListener('dragleave', event => {
54             depth -= 1;
55             if (depth === 0) {
56                 reset();
57             }
58         });
59
60         this.container.addEventListener('drop', event => {
61             event.preventDefault();
62             reset();
63             const clipboard = new Clipboard(event.dataTransfer);
64             const files = clipboard.getFiles();
65             for (const file of files) {
66                 this.createUploadFromFile(file);
67             }
68         });
69     }
70
71     showOverlay() {
72         const overlay = this.container.querySelector('.dropzone-overlay');
73         if (!overlay) {
74             const zoneElem = elem('div', {class: 'dropzone-overlay'}, [this.zoneText]);
75             this.container.append(zoneElem);
76         }
77     }
78
79     hideOverlay() {
80         const overlay = this.container.querySelector('.dropzone-overlay');
81         if (overlay) {
82             overlay.remove();
83         }
84     }
85
86     /**
87      * @param {File} file
88      * @return {Upload}
89      */
90     createUploadFromFile(file) {
91         const {dom, status, progress} = this.createDomForFile(file);
92         this.container.append(dom);
93
94         const upload = {
95             file,
96             dom,
97             updateProgress(percentComplete) {
98                 console.log(`progress: ${percentComplete}%`);
99                 progress.textContent = `${percentComplete}%`;
100                 progress.style.width = `${percentComplete}%`;
101             },
102             markError(message) {
103                 status.setAttribute('data-status', 'error');
104                 status.textContent = message;
105                 removeLoading(dom);
106             },
107             markSuccess(message) {
108                 status.setAttribute('data-status', 'success');
109                 status.textContent = message;
110                 removeLoading(dom);
111             },
112         };
113
114         this.startXhrForUpload(upload);
115
116         return upload;
117     }
118
119     /**
120      * @param {Upload} upload
121      */
122     startXhrForUpload(upload) {
123         const formData = new FormData();
124         formData.append('file', upload.file, upload.file.name);
125
126         const req = window.$http.createXMLHttpRequest('POST', this.url, {
127             error() {
128                 upload.markError('Upload failed'); // TODO - Update text
129             },
130             readystatechange() {
131                 if (this.readyState === XMLHttpRequest.DONE && this.status === 200) {
132                     upload.markSuccess('Finished upload!');
133                 } else if (this.readyState === XMLHttpRequest.DONE && this.status >= 400) {
134                     const content = this.responseText;
135                     const data = content.startsWith('{') ? JSON.parse(content) : {message: content};
136                     const message = data?.message || content;
137                     upload.markError(message);
138                 }
139             },
140         });
141
142         req.upload.addEventListener('progress', evt => {
143             const percent = Math.min(Math.ceil((evt.loaded / evt.total) * 100), 100);
144             upload.updateProgress(percent);
145         });
146
147         req.setRequestHeader('Accept', 'application/json');
148         req.send(formData);
149     }
150
151     /**
152      * @param {File} file
153      * @return {{image: Element, dom: Element, progress: Element, label: Element, status: Element}}
154      */
155     createDomForFile(file) {
156         const image = elem('img', {src: ''});
157         const status = elem('div', {class: 'dropzone-file-item-status'}, []);
158         const progress = elem('div', {class: 'dropzone-file-item-progress'});
159         const imageWrap = elem('div', {class: 'dropzone-file-item-image-wrap'}, [image]);
160
161         const dom = elem('div', {class: 'dropzone-file-item'}, [
162             imageWrap,
163             elem('div', {class: 'dropzone-file-item-text-wrap'}, [
164                 elem('div', {class: 'dropzone-file-item-label'}, [file.name]),
165                 getLoading(),
166                 status,
167             ]),
168             progress,
169         ]);
170
171         if (file.type.startsWith('image/')) {
172             image.src = URL.createObjectURL(file);
173         }
174
175         return {
176             dom, progress, status,
177         };
178     }
179
180 }
181
182 /**
183  * @typedef Upload
184  * @property {File} file
185  * @property {Element} dom
186  * @property {function(Number)} updateProgress
187  * @property {function(String)} markError
188  * @property {function(String)} markSuccess
189  */