Skip to content

Instantly share code, notes, and snippets.

@iErik
Created April 24, 2020 20:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save iErik/d91ab301e26c55f009d0298b4445ba12 to your computer and use it in GitHub Desktop.
Save iErik/d91ab301e26c55f009d0298b4445ba12 to your computer and use it in GitHub Desktop.
<template>
<div class="drop-zone">
<div class="drop-zone__box" @click="$refs['fileInput'].click()">
<p class="drop-zone__text">
Arraste aqui seu arquivo ou clique
<span class="drop-zone__textHighlight">
para abrir os documentos
</span>
para fazer upload do arquivo
</p>
<input
ref="fileInput"
type="file"
class="drop-zone__input"
:accept="extensions"
:multiple="multiple"
@change.prevent="emitUpload"
/>
</div>
<div v-if="$slots.default" class="drop-zone__slot">
<slot />
</div>
</div>
</template>
<script>
export default {
name: 'DropZone',
props: {
extensions: {
type: Array,
required: true
},
multiple: {
type: Boolean,
default: false
}
},
methods: {
emitUpload() {
this.$emit('upload', this.$refs.fileInput)
}
}
}
</script>
<style lang="scss">
.drop-zone {
display: flex;
flex-direction: column;
border: 1px dashed #cccccc;
border-radius: 5px;
&:hover {
border: 1px dashed var(--color-primary);
cursor: pointer;
.drop-zone__textHighlight {
text-decoration: underline;
}
}
&__box {
display: flex;
justify-content: center;
padding: 30px 0;
flex-shrink: 0;
width: 100%;
}
&__text {
text-decoration: none !important;
color: #999;
}
&__textHighlight {
color: var(--color-primary);
}
&__input {
display: none;
}
}
</style>
<template>
<div :class="rootClasses">
<div :class="previewClasses">
<div
v-if="previewable"
:style="imgStyle"
class="file-item__preview__img"
/>
<f-icon
v-else
name="file"
lib="flux"
:size="small ? 'base' : 'xl'"
color="gray-700"
class="file-item__preview__icn"
/>
</div>
<div class="file-item__title">{{ fileName }}</div>
<div class="file-item__actions">
<div class="file-item__actions__delete" @click="emitDelete">
<f-icon size="xs" name="X" lib="flux" color="red-700" />
</div>
</div>
</div>
</template>
<script>
export default {
name: 'FileItem',
props: {
/**
* The file to be processed, can be either a File object,
* an URL poiting to the file.
*/
file: {
type: [String, File],
required: true
},
/**
* Makes the FileItem's appaerance more compact.
*/
small: {
type: Boolean,
default: false
}
},
data: () => ({
loading: false,
fileObj: null
}),
computed: {
rootClasses() {
return [
'file-item',
{
'file-item--loading': this.loading,
'file-item--small': this.small
}
]
},
previewClasses() {
return [
'file-item__preview',
{
'file-item__preview--small': this.small
}
]
},
imgStyle() {
if (!this.previewable || this.loading) return {}
return { backgroundImage: `url(${this.filePath})` }
},
previewable() {
if (this.loading || !this.fileObj.type) return
return this.fileObj.type.includes('image')
},
filePath() {
if (!this.previewable || this.loading) return ''
return URL.createObjectURL(this.fileObj) || ''
},
fileName() {
return this.loading ? '' : this.fileObj.name.replace(/\//g, '')
}
},
created() {
if (typeof this.file === 'string' || this.file instanceof String)
return this.fetchFile(this.file)
this.fileObj = this.file
},
methods: {
async fetchFile(fileUrl) {
this.loading = true
const fileName = fileUrl.substring(fileUrl.lastIndexOf('/'))
const fileStream = await fetch(fileUrl, { mode: 'no-cors' })
const fileBlob = await fileStream.blob()
this.fileObj = new File([fileBlob], fileName)
this.loading = false
},
emitDelete() {
this.$emit('delete', this.file)
}
}
}
</script>
<style lang="scss">
.file-item {
display: flex;
border: 1px solid #cccccc;
border-radius: 5px;
height: 70px;
padding-right: 25px;
&--small {
height: 40px;
padding-right: 7px;
}
&__preview {
display: flex;
align-items: center;
justify-content: center;
width: 55px;
height: 55px;
margin: 7px;
margin-right: 15px;
&--small {
width: 25px;
height: 25px;
margin-right: 7px;
}
&__img {
border-radius: 3px;
width: 100%;
height: 100%;
background-size: cover;
background-repeat: no-repeat;
}
}
&__title {
display: flex;
align-items: center;
flex-grow: 1;
color: #999;
font-size: 12px;
}
&__actions {
display: flex;
align-items: center;
&__delete {
padding: 3px;
border-radius: 50%;
border: 1px solid var(--color-red-700);
cursor: pointer;
}
}
}
</style>
<template>
<div :class="classes">
<file-item
v-for="(file, index) in files"
:key="`${index}-${file.name || file}`"
:file="file"
class="file-list__item"
small
@delete="emitDelete($event, index)"
/>
</div>
</template>
<script>
import FileItem from './FileItem'
export default {
name: 'FileList',
components: { FileItem },
props: {
/**
* List of files to display
*/
files: {
type: Array,
default: () => []
}
},
computed: {
classes() {
return [
'file-list',
{
'file-list--empty': !(this.files || []).length
}
]
}
},
methods: {
emitDelete(file, index) {
this.$emit('delete', { file, index })
}
}
}
</script>
<style lang="scss">
.file-list {
&:not(&--empty) {
padding: 5px;
}
&__item:not(:last-child) {
margin-bottom: 5px;
}
}
</style>
<template>
<div
class="file-upload"
@drop.prevent.stop
@dragover.prevent.stop
@dragenter.prevent.stop
@dragleave.prevent.stop
@input.prevent.stop
>
<transition name="file-upload--fade">
<drop-zone
v-if="!hasFiles || multiple"
:multiple="multiple"
:extensions="extensions"
@upload="handleUpload"
>
<file-list v-if="multiple" :files="value" v-on="$listeners" />
</drop-zone>
</transition>
<transition name="file-upload--fade">
<file-item v-if="!multiple && value" :file="value" v-on="$listeners" />
</transition>
</div>
</template>
<script>
import DropZone from './fragments/DropZone'
import FileList from './fragments/FileList'
import FileItem from './fragments/FileItem'
export default {
name: 'FileUpload',
components: {
DropZone,
FileList,
FileItem
},
props: {
/**
* List of files.
*/
value: {
type: [File, String, Array],
required: true
},
/**
* Allowed file extensions
*/
extensions: {
type: Array,
required: true
},
/**
* Whether the component should support multiple files or not
*/
multiple: {
type: Boolean,
default: false
},
/**
* Limits the amount of files that the component should accept
* in case it is multipe.
*/
fileLimit: {
type: [Number, String],
default: ''
}
},
computed: {
hasFiles() {
if (!Array.isArray(this.value)) return !!this.value
return !!(
(this.value || []).length &&
this.value.some(f => f && Object.keys(f).length)
)
}
},
methods: {
handleUpload({ files }) {
if (this.overLimit(files)) return
Array.from(files).forEach(file => this.$emit('upload', file))
},
overLimit(files) {
if (!Array.isArray(this.values) || !this.fileLimit) return false
if (this.fileLimit && (this.value || []).length >= +this.fileLimit)
return true
return (
Array.from(files).length + (this.value || []).length >= +this.fileLimit
)
}
}
}
</script>
<style lang="scss">
.file-upload {
position: relative;
&--fade {
&-enter-active,
&-leave-active {
position: absolute;
width: 100%;
transition: opacity 200ms;
}
&-enter,
&-leave-to {
opacity: 0;
}
}
}
</style>
<template>
<div class="TestPage">
<div class="TestPage-testBox">
<file-upload
class="TestPage-fileUpload"
:value="files"
:extensions="['.jpg', '.png', '.zip', '.rar', '.dmg']"
multiple
@upload="updateFiles"
/>
</div>
<div class="TestPage-testBox">
<file-upload
class="TestPage-fileUpload"
:value="file"
:extensions="['.jpg', '.png', '.zip', '.rar', '.dmg']"
@upload="updateFile"
/>
</div>
</div>
</template>
<script>
import { FileUpload } from '@/components/molecules'
export default {
name: 'TestPage',
layout: 'internal',
components: {
FileUpload
},
data: () => ({
files: [],
file: {}
}),
methods: {
updateFiles(file) {
this.files.push(file)
},
updateFile(file) {
this.file = file
}
}
}
</script>
<style lang="scss">
.TestPage {
&-testBox {
}
&-fileUpload {
}
}
</style>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment