<template> <div class="vue_component__upload--image" v-bind:class="{ 'dragover': onDragover }"> <form v-bind:id="'upload_image_form--' + input_id" enctype="multipart/form-data"> <div class="upload_image_form__thumbnails"> <div v-for="(value, key) in files" class="upload_image_form__thumbnail" v-on:click="fileClick($event, key)" v-bind:class="{ 'uploaded': value.uploaded, 'bad-size': value.bad_size }"> <span v-on:click="fileDelete($event, key)"> ✖ </span> <img v-bind:src="image[key]" v-bind:class="{ 'show': image[key]}"> </div> </div> <input type="file" v-bind:id="'upload_image_form__input--' + input_id" hidden multiple/> <div> <button type="submit" v-bind:class="button_class" v-on:click="submit" v-bind:disabled="onUploading" v-html="button_html"></button> </div> </form> </div> </template> <script> export default { name: 'upload-image', props: { input_id: { type: String, required: false, default: "default" }, url: { type: String, required: true, default: null }, name: { type: String, required: false, default: 'images[]' }, disable_upload: { type: Boolean, required: false, default: false }, max_batch: { type: Number, required: false, default: 0 }, max_files: { type: Number, required: false, default: 10 }, max_filesize: { type: Number, required: false, default: 8000 }, resize_enabled: { type: Boolean, required: false, default: false }, resize_max_width: { type: Number, required: false, default: 800 }, resize_max_height: { type: Number, required: false, default: 600 }, button_html: { type: String, required: false, default: 'Upload Images' }, button_class: { type: String, required: false, default: 'btn btn-primary' } }, data: function () { return { form: null, input: null, index: 0, total: 0, files: {}, image: {}, batch: {}, onDragover: false, onUploading: false } }, mounted: function () { this.form = document.getElementById('upload_image_form--' + this.input_id); this.input = document.getElementById('upload_image_form__input--' + this.input_id); ['drag', 'dragstart', 'dragend', 'dragover', 'dragenter', 'dragleave', 'drop'].forEach(event => this.form.addEventListener(event, (e) => { e.preventDefault(); e.stopPropagation(); })); ['dragover', 'dragenter'] .forEach(event => this.form.addEventListener(event, this.dragEnter)); ['dragleave', 'dragend', 'drop'] .forEach(event => this.form.addEventListener(event, this.dragLeave)); ['drop'] .forEach(event => this.form.addEventListener(event, this.fileDrop)); ['change'] .forEach(event => this.input.addEventListener(event, this.fileDrop)); this.form.addEventListener('click', (e) => { this.input.click(); }); }, methods: { _can_xhr() { if (this.total >= this.max_files) { return false; } return true; }, _can_upload_file(key) { let file = this.files[key]; if (file.attempted || file.bad_size) { return false; } return true; }, _xhr: function (formData, keys, callback) { this.onUploading = true; this.$emit('upload-image-attempt', formData); keys.forEach((key) => { this.$set(this.files[key], 'attempted', true); }); this.$http.post(this.url, formData).then((response) => { keys.forEach((key) => { this.$set(this.files[key], 'uploaded', true); this.total++; }); this.$emit('upload-image-success', [formData, response]); }, (response) => { this.$emit('upload-image-failure', [formData, response]); }).then((response) => { this.onUploading = false; callback(); }); }, upload: function () { if (!this._can_xhr()) return false; for (let key in this.files) { if (!this._can_upload_file(key)) continue; let formData = new FormData(); formData.append(this.name, this.files[key].file, this.files[key].name); this._xhr(formData, [key], this.upload); return true; } }, upload_batch: function () { if (!this._can_xhr()) return false; for (let key in this.batch) { this._xhr(this.batch[key].form, this.batch[key].keys, this.upload_batch); delete this.batch[key]; return true; } }, create_batch: function () { let index = 0; let count = 0; this.batch = {}; for (let key in this.files) { if (!this._can_upload_file(key)) continue; if (this.batch[index] == null || count == this.max_batch) { index++; count = 0; this.batch[index] = {form: new FormData(), keys: []}; } count++; this.batch[index]['keys'].push(key); this.batch[index]['form'].append(this.name, this.files[key].file, this.files[key].name); } }, submit: function (e) { e.preventDefault(); e.stopPropagation(); this.$emit('upload-image-submit', this.files); if(!this.disable_upload && !this.onUploading) { if (this.max_batch > 1) { this.create_batch(); return this.upload_batch(); } this.upload(); } }, dragEnter: function (e) { e.preventDefault(); this.onDragover = true; }, dragLeave: function (e) { e.preventDefault(); this.onDragover = false; }, fileDrop: function (e) { e.preventDefault(); let newFiles = e.target.files || e.dataTransfer.files; for (let i = 0; i < newFiles.length; i++) { this.$set(this.files, this.index, newFiles[i]); if (newFiles[i].type.match(/image.*/)) { this.fileInit(this.index); this.fileRead(this.index); this.index++; } ; } e.target.value = ''; }, fileInit: function (key) { let file = this.files[key]; this.files[key] = { name: this.files[key].name, file: this.files[key] }; if ((file.size * 0.001) > this.max_filesize) { this.$set(this.files[key], 'bad_size', true); } }, fileRead: function (key) { let reader = new FileReader(); reader.addEventListener("load", (e) => { this.$set(this.image, key, reader.result); if (this.resize_enabled) { let imager = new Image(); imager.onload = () => { let width = imager.width; let height = imager.height; if (width > this.resize_max_width || height > this.resize_max_height) { if ((height / width) - (this.resize_max_height / this.resize_max_width) > 0) { width = this.resize_max_height / height * width; height = this.resize_max_height; } else { height = this.resize_max_width / width * height; width = this.resize_max_width; } } let canvas = document.createElement("canvas"); canvas.width = width; canvas.height = height; let ctx = canvas.getContext("2d"); ctx.drawImage(imager, 0, 0, width, height); let newImageData = canvas.toDataURL("image/png"); this.$set(this.image, key, newImageData); // let img = atob(newImageData.split(',')[1]); let img_buffer = []; let i = 0; while (i < img.length) { img_buffer.push(img.charCodeAt(i)); i++; } let u8Image = new Uint8Array(img_buffer); this.$set(this.files, key, { name: this.files[key].name, file: new Blob([u8Image], {filename: this.files[key].name}) }); this.$emit('upload-image-loaded', this.files[key]); }; imager.src = reader.result; } }); reader.readAsDataURL(this.files[key].file); }, fileDelete: function (e, key) { this.$emit('upload-image-removed', this.files[key]); this.$delete(this.files, key); this.$delete(this.image, key); }, fileClick: function (e, key) { e.preventDefault(); e.stopPropagation(); this.$emit('upload-image-clicked', this.files[key]); } } } </script> <style lang="css" scoped> .vue_component__upload--image { padding: 5px; cursor: pointer; min-height: 80px; border-radius: 5px; } .vue_component__upload--image.dragover { } .vue_component__upload--image form > div { text-align: center; } .vue_component__upload--image .upload_image_form__thumbnails { margin-bottom: 1em; } .vue_component__upload--image .upload_image_form__thumbnail { border-radius: 2.5px; position: relative; width: 20%; padding: 20% 0 0; overflow: hidden; margin: 10px; display: inline-block; } .vue_component__upload--image .upload_image_form__thumbnail img { position: absolute; top: 50%; left: 50%; min-width: 100%; min-height: 100%; max-height: 150%; opacity: 0; transform: translateX(-50%) translateY(-50%); transition: 1s opacity; } .vue_component__upload--image .upload_image_form__thumbnail img.show { opacity: 1; } .vue_component__upload--image .upload_image_form__thumbnail img:hover { filter: blur(2px); } .vue_component__upload--image .upload_image_form__thumbnail.bad-size img { filter: grayscale(100%); } .vue_component__upload--image .upload_image_form__thumbnail.uploaded img { opacity: 0.1; } .vue_component__upload--image .upload_image_form__thumbnail span { position: absolute; top: -5px; left: 0px; z-index: 100; padding: 0px 1px; border-radius: 2px; background-color: grey; } </style>