Skip to content

Instantly share code, notes, and snippets.

@NicolasDurant
Last active August 25, 2023 00:11
Show Gist options
  • Save NicolasDurant/0396a7af24882d666e3b87328b0fcd5d to your computer and use it in GitHub Desktop.
Save NicolasDurant/0396a7af24882d666e3b87328b0fcd5d to your computer and use it in GitHub Desktop.
[Vue / Nuxt - Image Drag & Drop or Choose Image] Component that provides an area for the user to drop an image onto. It's restricted to image file types only. As soon as the image is uploaded it will trigger an emit event 'uploaded', which the parent can listen to. Also includes the ChooseImage Component, but without adding another image preview…
<template>
<div
class="d-flex flex-column justify-center align-end"
style="height: 100%; width: 100%"
>
<!-- DRAG & DROP AREA -->
<div
class="drop d-flex flex-column justify-center align-center"
:class="classes"
@dragover.prevent="dragOver"
@dragleave.prevent="dragLeave"
@drop.prevent="drop($event)"
>
<!-- IMAGE PREVIEW -->
<img
v-if="uploadedImage"
:src="uploadedImage"
alt="Image uploaded for configuration"
/>
<!-- INFO TEXT -->
<div
v-if="
!uploadedImage &&
!isDragging &&
!wrongFile &&
!wrongCount &&
!wrongSize
"
class="d-flex flex-column justify-center align-center"
>
<v-icon x-large class="font-settings">mdi-package-down</v-icon>
<h2 class="font-settings">{{ $t('dragdrop.drop') }}</h2>
<p>
{{ $t('dragdrop.size') }}
</p>
</div>
<!-- WRONG FILE TYPE -->
<div
v-if="wrongFile"
class="info-text d-flex flex-column justify-center align-center"
>
<v-icon x-large>mdi-file-cancel</v-icon>
<h2>{{ $t('dragdrop.wrong') }}</h2>
<p>
{{ $t('dragdrop.allowed') }}
</p>
</div>
<!-- WRONG FILE COUNT (x>1) -->
<div
v-if="wrongCount"
class="info-text d-flex flex-column justify-center align-center"
>
<v-icon x-large>mdi-file-document-multiple</v-icon>
<h2>{{ $t('dragdrop.wrongCount') }}</h2>
<p>
{{ $t('dragdrop.allowedCount') }}
</p>
</div>
<!-- WRONG FILE SIZE (x>10MB) -->
<div
v-if="wrongSize"
class="info-text d-flex flex-column justify-center align-center"
>
<v-icon x-large>mdi-file-cog</v-icon>
<h2>{{ $t('dragdrop.wrongSize') }}</h2>
<p>
{{ $t('dragdrop.allowedSize') }}
</p>
</div>
</div>
<!-- FILE INPUT (HIDDEN) -->
<input ref="myFile" type="file" accept="image/*" @change="previewFile" />
<!-- BUTTON (TRIGGERS INPUT) -->
<v-btn class="mt-3" outlined color="primary" @click="$refs.myFile.click()">
<v-icon class="mr-2">mdi-plus</v-icon>
{{ $t('dragdrop.choose') }}
</v-btn>
</div>
</template>
<script>
import { Component, Vue } from 'vue-property-decorator'
@Component({})
/**
* @author Nicolas Durant
* @description Component that provides an area for the user to drop an image onto. It's restricted to image file types only.
* As soon as the image is uploaded it will trigger an emit event 'uploaded', which the parent can listen to. Also includes the ChooseImage Component,
* but without adding another image preview.
* @event @uploaded
*/
export default class DragDrop extends Vue {
/**
* Control for displaying user guidance.
* @type {boolean}
*/
isDragging = false
/**
* Control for displaying user guidance. File type !== image.
* @type {boolean}
*/
wrongFile = false
/**
* Control for displaying user guidance. Count > 1 error.
* @type {boolean}
*/
wrongCount = false
/**
* Control for displaying user guidance. Size > 10MB error.
* @type {boolean}
*/
wrongSize = false
/**
* Holds the image uploaded by the user.
* @type {string}
*/
uploadedImage = ''
/**
* Gets called when the user drags a file above the area.
*/
dragOver() {
this.isDragging = true
}
/**
* Gets called when the user drags a file out of the area.
*/
dragLeave() {
this.isDragging = false
}
/**
* Validates the file & generates a file reader, when the user drops an image into the area.
* @param e {event} - event that gets provided by the drop API
*/
drop(e) {
const files = e.dataTransfer.files
if (this.filesValid(files)) {
this.fileReader(files[0])
}
}
/**
* Generates a file reader for a given file.
* Will then emit the uploaded event when finished. Also checks for the correct file type and shows a notification otherwise.
* @param file - file provided by drag & drop, or selection
*/
fileReader(file) {
const reader = new FileReader()
reader.onload = (e) => {
this.wrongCount = false
this.wrongFile = false
this.wrongSize = false
this.uploadedImage = e.target.result
this.isDragging = false
this.$emit('uploaded', e.target.result)
}
reader.readAsDataURL(file)
}
/**
* Validates the given files.
* 1. there should only be 1 file
* 2. it has to be an image
* 3. it has to be smaller than 10MB
* @param files - files provided by drag & drop, or selection
* @return {boolean} - true when all tests passed
*/
filesValid(files) {
this.wrongCount = false
this.wrongFile = false
this.wrongSize = false
// only 1 file
if (files.length > 1) {
this.wrongCount = true
this.uploadedImage = null
this.isDragging = false
return false
}
const file = files[0]
// only image files
if (!file.type.includes('image/')) {
this.uploadedImage = null
this.wrongFile = true
this.isDragging = false
return false
}
// only <10MB
if (file.size / 1000000 > 10) {
this.uploadedImage = null
this.wrongSize = true
this.isDragging = false
return false
}
return true
}
/**
* Validates the file & generates a file reader, so the user can select an image from their filesystem to use.
*/
previewFile() {
const files = this.$refs.myFile.files
if (this.filesValid(files)) {
this.fileReader(files[0])
}
}
/**
* Computed property for dynamic classes, depending on the state of the component.
*/
get classes() {
return { isDragging: this.isDragging }
}
}
</script>
<style scoped>
.drop {
width: 100%;
height: 90%;
background-color: #f9f9fa;
background-image: url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='4' ry='4' stroke='%2341B773FF' stroke-width='2' stroke-dasharray='10' stroke-dashoffset='0' stroke-linecap='butt'/%3e%3c/svg%3e");
border-radius: 4px;
padding: 1rem;
transition: background-color 0.2s ease-in-out;
}
.font-settings {
color: #41b773;
}
.isDragging {
background-color: #e5e5e5;
}
img {
width: 100%;
height: 100%;
object-fit: contain;
}
input[type='file'] {
display: none;
}
</style>
<template>
<div
class="h-full w-full flex flex-col justify-center items-end"
>
<!-- DRAG & DROP AREA -->
<div
class="w-full h-full bg-shades border-2 border-dotted rounded-md py-8 px-2 flex flex-col justify-center items-center hover:bg-opacity-70 hover:text-opacity-70"
:class="[classes, wrongCount || wrongSize || wrongFile ? 'border-warn-dark' : 'border-primary-dark']"
@dragover.prevent="dragOver"
@dragleave.prevent="dragLeave"
@drop.prevent="drop($event)"
>
<!-- FILE PREVIEW -->
<div v-if="uploadedFile" class="flex flex-col justify-center items-center">
<DocumentArrowUpIcon class="h-12 w-12 text-primary"></DocumentArrowUpIcon>
<h2 class="text-2xl text-primary mt-4">{{fileName}}</h2>
<p>
Datei kann nun hochgeladen werden
</p>
</div>
<!-- INFO TEXT -->
<div
v-if="
!uploadedFile &&
!wrongFile &&
!wrongCount &&
!wrongSize
"
class="flex flex-col justify-center items-center"
>
<ArrowDownTrayIcon class="h-12 w-12 text-primary"></ArrowDownTrayIcon>
<h2 class="text-2xl text-primary mt-4">.XLIFF</h2>
<p>
Eine Übersetzungs-Datei in diesem Feld ablegen
</p>
</div>
<!-- WRONG FILE TYPE -->
<div
v-if="wrongFile"
class="info-text flex flex-col justify-center items-center"
>
<DocumentMinusIcon class="h-12 w-12 text-warn"></DocumentMinusIcon>
<h2 class="text-2xl text-warn mt-4">Falscher Datei-Typ</h2>
<p>
Es muss eine Datei von Typ ".xliff" hochgeladen werden
</p>
</div>
<!-- WRONG FILE COUNT (x>1) -->
<div
v-if="wrongCount"
class="info-text flex flex-col justify-center items-center"
>
<DocumentMinusIcon class="h-12 w-12 text-warn"></DocumentMinusIcon>
<h2 class="text-2xl text-warn mt-4">Zu viele Dateien</h2>
<p>
Bitte maximal eine Datei hochladen
</p>
</div>
<!-- WRONG FILE SIZE (x>10MB) -->
<div
v-if="wrongSize"
class="info-text flex flex-col justify-center items-center"
>
<DocumentMinusIcon class="h-12 w-12 text-warn"></DocumentMinusIcon>
<h2 class="text-2xl text-warn mt-4">Datei ist zu groß</h2>
<p>
Die maximale Upload-Größe beträgt 10MB
</p>
</div>
</div>
<!-- FILE INPUT (HIDDEN) -->
<input ref="myFile" type="file" accept="application/x-xliff+xml" @change="previewFile"/>
<!-- BUTTON (TRIGGERS INPUT) -->
<div class="flex justify-center items-center gap-x-2 mt-4">
<span>Alternativ:</span>
<BaseButton @click="$refs.myFile.click()" label="Datei auswählen"></BaseButton>
<BaseButton :disabled="uploadedFile === ''" label="Hochladen"></BaseButton>
</div>
</div>
</template>
<script>/**
* @author Nicolas Durant
* @description Component that provides an area for the user to drop an image onto. It's restricted to image file types only.
* As soon as the image is uploaded it will trigger an emit event 'uploaded', which the parent can listen to. Also includes the ChooseImage Component,
* but without adding another image preview.
* @event @uploaded
*/
import {ArrowDownTrayIcon, DocumentArrowUpIcon, DocumentMinusIcon} from "@heroicons/vue/24/outline";
import BaseButton from "./BaseButton";
export default {
components: {
ArrowDownTrayIcon,
DocumentMinusIcon,
DocumentArrowUpIcon,
BaseButton
},
data() {
return {
/**
* Control for displaying user guidance.
* @type {boolean}
*/
isDragging: false,
/**
* Control for displaying user guidance. File type !== image.
* @type {boolean}
*/
wrongFile: false,
/**
* Control for displaying user guidance. Count > 1 error.
* @type {boolean}
*/
wrongCount: false,
/**
* Control for displaying user guidance. Size > 10MB error.
* @type {boolean}
*/
wrongSize: false,
/**
* The original file name..
* @type {string}
*/
fileName: '',
/**
* Holds the image uploaded by the user.
* @type {string}
*/
uploadedFile: ''
}
},
methods: {
/**
* Gets called when the user drags a file above the area.
*/
dragOver() {
this.isDragging = true
},
/**
* Gets called when the user drags a file out of the area.
*/
dragLeave() {
this.isDragging = false
},
/**
* Validates the file & generates a file reader, when the user drops an image into the area.
* @param e {event} - event that gets provided by the drop API
*/
drop(e) {
const files = e.dataTransfer.files
if (this.filesValid(files)) {
this.fileReader(files[0])
}
},
/**
* Generates a file reader for a given file.
* Will then emit the uploaded event when finished. Also checks for the correct file type and shows a notification otherwise.
* @param file - file provided by drag & drop, or selection
*/
fileReader(file) {
const reader = new FileReader()
reader.onload = (e) => {
this.wrongCount = false
this.wrongFile = false
this.wrongSize = false
this.uploadedFile = e.target.result
this.isDragging = false
this.$emit('uploaded', e.target.result)
}
reader.readAsDataURL(file)
},
/**
* Validates the given files.
* 1. there should only be 1 file
* 2. it has to be an xliff file
* 3. it has to be smaller than 10MB
* @param files - files provided by drag & drop, or selection
* @return {boolean} - true when all tests passed
*/
filesValid(files) {
this.wrongCount = false
this.wrongFile = false
this.wrongSize = false
this.fileName = ''
// only 1 file
if (files.length > 1) {
this.wrongCount = true
this.uploadedFile = ''
this.isDragging = false
return false
}
const file = files[0]
this.fileName = file.name
// only xliff files
if (!file.type.includes("application/x-xliff+xml")) {
this.uploadedFile = ''
this.wrongFile = true
this.isDragging = false
return false
}
// only <10MB
if (file.size / 1000000 > 10) {
this.uploadedFile = ''
this.wrongSize = true
this.isDragging = false
return false
}
return true
},
/**
* Validates the file & generates a file reader, so the user can select an image from their filesystem to use.
*/
previewFile() {
const files = this.$refs.myFile.files
if (this.filesValid(files)) {
this.fileReader(files[0])
}
}
},
computed: {
/**
* Computed property for dynamic classes, depending on the state of the component.
*/
classes() {
return {isDragging: this.isDragging}
}
}
}
</script>
<style scoped>
.isDragging {
@apply bg-opacity-70 text-opacity-70;
}
img {
width: 100%;
height: 100%;
object-fit: contain;
}
input[type='file'] {
display: none;
}
</style>
@r-this
Copy link

r-this commented Aug 23, 2023

Thanks for posting these, I appreciated reading them

(Sorry if notifications from my clicking around. Still learning the github social features.)

@NicolasDurant
Copy link
Author

Hey @10xRod, no worries and glad you find it useful. I usually post this stuff as gist, so if I have to make a similar implementation in another project again, I find them fast and can simply change the visuals and so on. I am curious how did you stumble upon it? Did you simply search in Gists or via search engine? Didn't think another person would actually check them out haha. Cheers

@r-this
Copy link

r-this commented Aug 25, 2023 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment