]> BookStack Code Mirror - bookstack/blob - resources/js/components/dropzone.js
Added system cli, and created backups directory
[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.isActive = true;
16
17         this.url = this.$opts.url;
18         this.successMessage = this.$opts.successMessage;
19         this.errorMessage = this.$opts.errorMessage;
20         this.uploadLimitMb = Number(this.$opts.uploadLimit);
21         this.uploadLimitMessage = this.$opts.uploadLimitMessage;
22         this.zoneText = this.$opts.zoneText;
23         this.fileAcceptTypes = this.$opts.fileAccept;
24
25         this.setupListeners();
26     }
27
28     /**
29      * Public method to allow external disabling/enabling of this drag+drop dropzone.
30      * @param {Boolean} active
31      */
32     toggleActive(active) {
33         this.isActive = active;
34     }
35
36     setupListeners() {
37         onSelect(this.selectButtons, this.manualSelectHandler.bind(this));
38         this.setupDropTargetHandlers();
39     }
40
41     setupDropTargetHandlers() {
42         let depth = 0;
43
44         const reset = () => {
45             this.hideOverlay();
46             depth = 0;
47         };
48
49         this.dropTarget.addEventListener('dragenter', event => {
50             event.preventDefault();
51             depth += 1;
52
53             if (depth === 1 && this.isActive) {
54                 this.showOverlay();
55             }
56         });
57
58         this.dropTarget.addEventListener('dragover', event => {
59             event.preventDefault();
60         });
61
62         this.dropTarget.addEventListener('dragend', reset);
63         this.dropTarget.addEventListener('dragleave', () => {
64             depth -= 1;
65             if (depth === 0) {
66                 reset();
67             }
68         });
69         this.dropTarget.addEventListener('drop', event => {
70             event.preventDefault();
71             reset();
72
73             if (!this.isActive) {
74                 return;
75             }
76
77             const clipboard = new Clipboard(event.dataTransfer);
78             const files = clipboard.getFiles();
79             for (const file of files) {
80                 this.createUploadFromFile(file);
81             }
82         });
83     }
84
85     manualSelectHandler() {
86         const input = elem('input', {type: 'file', style: 'left: -400px; visibility: hidden; position: fixed;', accept: this.fileAcceptTypes});
87         this.container.append(input);
88         input.click();
89         input.addEventListener('change', () => {
90             for (const file of input.files) {
91                 this.createUploadFromFile(file);
92             }
93             input.remove();
94         });
95     }
96
97     showOverlay() {
98         const overlay = this.dropTarget.querySelector('.dropzone-overlay');
99         if (!overlay) {
100             const zoneElem = elem('div', {class: 'dropzone-overlay'}, [this.zoneText]);
101             this.dropTarget.append(zoneElem);
102         }
103     }
104
105     hideOverlay() {
106         const overlay = this.dropTarget.querySelector('.dropzone-overlay');
107         if (overlay) {
108             overlay.remove();
109         }
110     }
111
112     /**
113      * @param {File} file
114      * @return {Upload}
115      */
116     createUploadFromFile(file) {
117         const {
118             dom, status, progress, dismiss,
119         } = this.createDomForFile(file);
120         this.statusArea.append(dom);
121         const component = this;
122
123         const upload = {
124             file,
125             dom,
126             updateProgress(percentComplete) {
127                 progress.textContent = `${percentComplete}%`;
128                 progress.style.width = `${percentComplete}%`;
129             },
130             markError(message) {
131                 status.setAttribute('data-status', 'error');
132                 status.textContent = message;
133                 removeLoading(dom);
134                 this.updateProgress(100);
135             },
136             markSuccess(message) {
137                 status.setAttribute('data-status', 'success');
138                 status.textContent = message;
139                 removeLoading(dom);
140                 setTimeout(dismiss, 2400);
141                 component.$emit('upload-success', {
142                     name: file.name,
143                 });
144             },
145         };
146
147         // Enforce early upload filesize limit
148         if (file.size > (this.uploadLimitMb * 1000000)) {
149             upload.markError(this.uploadLimitMessage);
150             return upload;
151         }
152
153         this.startXhrForUpload(upload);
154
155         return upload;
156     }
157
158     /**
159      * @param {Upload} upload
160      */
161     startXhrForUpload(upload) {
162         const formData = new FormData();
163         formData.append('file', upload.file, upload.file.name);
164         const component = this;
165
166         const req = window.$http.createXMLHttpRequest('POST', this.url, {
167             error() {
168                 upload.markError(component.errorMessage);
169             },
170             readystatechange() {
171                 if (this.readyState === XMLHttpRequest.DONE && this.status === 200) {
172                     upload.markSuccess(component.successMessage);
173                 } else if (this.readyState === XMLHttpRequest.DONE && this.status >= 400) {
174                     const content = this.responseText;
175                     const data = content.startsWith('{') ? JSON.parse(content) : {message: content};
176                     const message = data?.message || data?.error || content;
177                     upload.markError(message);
178                 }
179             },
180         });
181
182         req.upload.addEventListener('progress', evt => {
183             const percent = Math.min(Math.ceil((evt.loaded / evt.total) * 100), 100);
184             upload.updateProgress(percent);
185         });
186
187         req.setRequestHeader('Accept', 'application/json');
188         req.send(formData);
189     }
190
191     /**
192      * @param {File} file
193      * @return {{image: Element, dom: Element, progress: Element, status: Element, dismiss: function}}
194      */
195     createDomForFile(file) {
196         const image = elem('img', {src: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M9.224 7.373a.924.924 0 0 0-.92.925l-.006 7.404c0 .509.412.925.921.925h5.557a.928.928 0 0 0 .926-.925v-5.553l-2.777-2.776Zm3.239 3.239V8.067l2.545 2.545z' style='fill:%23000;fill-opacity:.75'/%3E%3C/svg%3E"});
197         const status = elem('div', {class: 'dropzone-file-item-status'}, []);
198         const progress = elem('div', {class: 'dropzone-file-item-progress'});
199         const imageWrap = elem('div', {class: 'dropzone-file-item-image-wrap'}, [image]);
200
201         const dom = elem('div', {class: 'dropzone-file-item'}, [
202             imageWrap,
203             elem('div', {class: 'dropzone-file-item-text-wrap'}, [
204                 elem('div', {class: 'dropzone-file-item-label'}, [file.name]),
205                 getLoading(),
206                 status,
207             ]),
208             progress,
209         ]);
210
211         if (file.type.startsWith('image/')) {
212             image.src = URL.createObjectURL(file);
213         }
214
215         const dismiss = () => {
216             dom.classList.add('dismiss');
217             dom.addEventListener('animationend', () => {
218                 dom.remove();
219             });
220         };
221
222         dom.addEventListener('click', dismiss);
223
224         return {
225             dom, progress, status, dismiss,
226         };
227     }
228
229 }
230
231 /**
232  * @typedef Upload
233  * @property {File} file
234  * @property {Element} dom
235  * @property {function(Number)} updateProgress
236  * @property {function(String)} markError
237  * @property {function(String)} markSuccess
238  */