1 import {Component} from './component';
2 import {Clipboard} from '../services/clipboard';
4 elem, getLoading, onSelect, removeLoading,
5 } from '../services/dom';
7 export class Dropzone extends Component {
10 this.container = this.$el;
11 this.statusArea = this.$refs.statusArea;
12 this.dropTarget = this.$refs.dropTarget;
13 this.selectButtons = this.$manyRefs.selectButton || [];
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;
25 this.setupListeners();
29 * Public method to allow external disabling/enabling of this drag+drop dropzone.
30 * @param {Boolean} active
32 toggleActive(active) {
33 this.isActive = active;
37 onSelect(this.selectButtons, this.manualSelectHandler.bind(this));
38 this.setupDropTargetHandlers();
41 setupDropTargetHandlers() {
49 this.dropTarget.addEventListener('dragenter', event => {
50 event.preventDefault();
53 if (depth === 1 && this.isActive) {
58 this.dropTarget.addEventListener('dragover', event => {
59 event.preventDefault();
62 this.dropTarget.addEventListener('dragend', reset);
63 this.dropTarget.addEventListener('dragleave', () => {
69 this.dropTarget.addEventListener('drop', event => {
70 event.preventDefault();
77 const clipboard = new Clipboard(event.dataTransfer);
78 const files = clipboard.getFiles();
79 for (const file of files) {
80 this.createUploadFromFile(file);
85 manualSelectHandler() {
86 const input = elem('input', {type: 'file', style: 'left: -400px; visibility: hidden; position: fixed;', accept: this.fileAcceptTypes});
87 this.container.append(input);
89 input.addEventListener('change', () => {
90 for (const file of input.files) {
91 this.createUploadFromFile(file);
98 const overlay = this.dropTarget.querySelector('.dropzone-overlay');
100 const zoneElem = elem('div', {class: 'dropzone-overlay'}, [this.zoneText]);
101 this.dropTarget.append(zoneElem);
106 const overlay = this.dropTarget.querySelector('.dropzone-overlay');
116 createUploadFromFile(file) {
118 dom, status, progress, dismiss,
119 } = this.createDomForFile(file);
120 this.statusArea.append(dom);
121 const component = this;
126 updateProgress(percentComplete) {
127 progress.textContent = `${percentComplete}%`;
128 progress.style.width = `${percentComplete}%`;
131 status.setAttribute('data-status', 'error');
132 status.textContent = message;
134 this.updateProgress(100);
136 markSuccess(message) {
137 status.setAttribute('data-status', 'success');
138 status.textContent = message;
140 setTimeout(dismiss, 2400);
141 component.$emit('upload-success', {
147 // Enforce early upload filesize limit
148 if (file.size > (this.uploadLimitMb * 1000000)) {
149 upload.markError(this.uploadLimitMessage);
153 this.startXhrForUpload(upload);
159 * @param {Upload} upload
161 startXhrForUpload(upload) {
162 const formData = new FormData();
163 formData.append('file', upload.file, upload.file.name);
164 const component = this;
166 const req = window.$http.createXMLHttpRequest('POST', this.url, {
168 upload.markError(component.errorMessage);
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);
182 req.upload.addEventListener('progress', evt => {
183 const percent = Math.min(Math.ceil((evt.loaded / evt.total) * 100), 100);
184 upload.updateProgress(percent);
187 req.setRequestHeader('Accept', 'application/json');
193 * @return {{image: Element, dom: Element, progress: Element, status: Element, dismiss: function}}
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]);
201 const dom = elem('div', {class: 'dropzone-file-item'}, [
203 elem('div', {class: 'dropzone-file-item-text-wrap'}, [
204 elem('div', {class: 'dropzone-file-item-label'}, [file.name]),
211 if (file.type.startsWith('image/')) {
212 image.src = URL.createObjectURL(file);
215 const dismiss = () => {
216 dom.classList.add('dismiss');
217 dom.addEventListener('animationend', () => {
222 dom.addEventListener('click', dismiss);
225 dom, progress, status, dismiss,
233 * @property {File} file
234 * @property {Element} dom
235 * @property {function(Number)} updateProgress
236 * @property {function(String)} markError
237 * @property {function(String)} markSuccess