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.method = (this.$opts.method || 'post').toUpperCase();
19 this.successMessage = this.$opts.successMessage;
20 this.errorMessage = this.$opts.errorMessage;
21 this.uploadLimitMb = Number(this.$opts.uploadLimit);
22 this.uploadLimitMessage = this.$opts.uploadLimitMessage;
23 this.zoneText = this.$opts.zoneText;
24 this.fileAcceptTypes = this.$opts.fileAccept;
25 this.allowMultiple = this.$opts.allowMultiple === 'true';
27 this.setupListeners();
31 * Public method to allow external disabling/enabling of this drag+drop dropzone.
32 * @param {Boolean} active
34 toggleActive(active) {
35 this.isActive = active;
39 onSelect(this.selectButtons, this.manualSelectHandler.bind(this));
40 this.setupDropTargetHandlers();
43 setupDropTargetHandlers() {
51 this.dropTarget.addEventListener('dragenter', event => {
52 event.preventDefault();
55 if (depth === 1 && this.isActive) {
60 this.dropTarget.addEventListener('dragover', event => {
61 event.preventDefault();
64 this.dropTarget.addEventListener('dragend', reset);
65 this.dropTarget.addEventListener('dragleave', () => {
71 this.dropTarget.addEventListener('drop', event => {
72 event.preventDefault();
79 const clipboard = new Clipboard(event.dataTransfer);
80 const files = clipboard.getFiles();
81 for (const file of files) {
82 this.createUploadFromFile(file);
87 manualSelectHandler() {
88 const input = elem('input', {
90 style: 'left: -400px; visibility: hidden; position: fixed;',
91 accept: this.fileAcceptTypes,
92 multiple: this.allowMultiple ? '' : null,
94 this.container.append(input);
96 input.addEventListener('change', () => {
97 for (const file of input.files) {
98 this.createUploadFromFile(file);
105 const overlay = this.dropTarget.querySelector('.dropzone-overlay');
107 const zoneElem = elem('div', {class: 'dropzone-overlay'}, [this.zoneText]);
108 this.dropTarget.append(zoneElem);
113 const overlay = this.dropTarget.querySelector('.dropzone-overlay');
123 createUploadFromFile(file) {
125 dom, status, progress, dismiss,
126 } = this.createDomForFile(file);
127 this.statusArea.append(dom);
128 const component = this;
133 updateProgress(percentComplete) {
134 progress.textContent = `${percentComplete}%`;
135 progress.style.width = `${percentComplete}%`;
138 status.setAttribute('data-status', 'error');
139 status.textContent = message;
141 this.updateProgress(100);
143 markSuccess(message) {
144 status.setAttribute('data-status', 'success');
145 status.textContent = message;
147 setTimeout(dismiss, 2400);
148 component.$emit('upload-success', {
154 // Enforce early upload filesize limit
155 if (file.size > (this.uploadLimitMb * 1000000)) {
156 upload.markError(this.uploadLimitMessage);
160 this.startXhrForUpload(upload);
166 * @param {Upload} upload
168 startXhrForUpload(upload) {
169 const formData = new FormData();
170 formData.append('file', upload.file, upload.file.name);
171 if (this.method !== 'POST') {
172 formData.append('_method', this.method);
174 const component = this;
176 const req = window.$http.createXMLHttpRequest('POST', this.url, {
178 upload.markError(component.errorMessage);
181 if (this.readyState === XMLHttpRequest.DONE && this.status === 200) {
182 upload.markSuccess(component.successMessage);
183 } else if (this.readyState === XMLHttpRequest.DONE && this.status >= 400) {
184 const content = this.responseText;
185 const data = content.startsWith('{') ? JSON.parse(content) : {message: content};
186 const message = data?.message || data?.error || content;
187 upload.markError(message);
192 req.upload.addEventListener('progress', evt => {
193 const percent = Math.min(Math.ceil((evt.loaded / evt.total) * 100), 100);
194 upload.updateProgress(percent);
197 req.setRequestHeader('Accept', 'application/json');
203 * @return {{image: Element, dom: Element, progress: Element, status: Element, dismiss: function}}
205 createDomForFile(file) {
206 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"});
207 const status = elem('div', {class: 'dropzone-file-item-status'}, []);
208 const progress = elem('div', {class: 'dropzone-file-item-progress'});
209 const imageWrap = elem('div', {class: 'dropzone-file-item-image-wrap'}, [image]);
211 const dom = elem('div', {class: 'dropzone-file-item'}, [
213 elem('div', {class: 'dropzone-file-item-text-wrap'}, [
214 elem('div', {class: 'dropzone-file-item-label'}, [file.name]),
221 if (file.type.startsWith('image/')) {
222 image.src = URL.createObjectURL(file);
225 const dismiss = () => {
226 dom.classList.add('dismiss');
227 dom.addEventListener('animationend', () => {
232 dom.addEventListener('click', dismiss);
235 dom, progress, status, dismiss,
243 * @property {File} file
244 * @property {Element} dom
245 * @property {function(Number)} updateProgress
246 * @property {function(String)} markError
247 * @property {function(String)} markSuccess