<template> <div class="row mb-3 gy-2 gx-2 p-2 scrolling-container"> <h3>Files <button type="button" class="btn btn-primary upload-btn" @click="chooseFiles">Upload</button></h3> <div class="col-sm-12"> <div class="form-floating mb-3"> <input type="text" class="form-control" id="floatingInput" placeholder="Search" v-model="search"> <label for="floatingInput">Search</label> </div> </div> <div v-for="file in searchedFiles" :key="file.id" class="col-4" style="position: relative;"> <div class="btn-group dropstart context-menu"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="dropdown-toggle bi bi-three-dots-vertical" data-bs-toggle="dropdown" aria-expanded="false" viewBox="0 0 16 16"> <path d="M9.5 13a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z"/> </svg> <ul class="dropdown-menu small-dropdown"> <li><a class="dropdown-item" href="#" @click="deleteFileBtn(file)">Delete</a></li> </ul> </div> <img @click="toggleSelection(file)" :class="getSelectedClass(file)" class="file-thumbnail img-fluid img-thumbnail" :title="file.name" :src="file.thumbnail"/> <figcaption class="figure-caption">{{ getFileName(file) }}</figcaption> </div> <div v-for="upload in uploads" :key="upload.id" class="col-4"> <div class="row h-100"> <div class="col-sm-12 my-auto p-3"> <div class="progress align-middle"> <div class="progress-bar" role="progressbar" v-bind:style="{ width: upload.progress + '%'}" :aria-valuenow="upload.progress" aria-valuemin="0" aria-valuemax="100"></div> </div> </div> </div> </div> <input ref="fileUpload" type="file" @change="fileChanged" multiple hidden> </div> </template> <script> import { mapActions, mapState, mapMutations } from "vuex"; import path from 'path' export default { name: "Files.vue", props: ['project'], data() { return { search: "" } }, methods: { chooseFiles() { this.$refs.fileUpload.click() }, async fileChanged(event) { let results = [] for (let file of event.target.files) { let data = new FormData(); data.append('media', file); data.append('project', this.project.url); data.append('json', "{}"); let payload = { project_id: this.project.id, project_url: this.project.url, data } // Invoke async upload (keep track of Promise) results.push(this.createFile(payload)) } for (let result in results) { await result } // Clear file input this.$refs.fileUpload.value = null }, async deleteFileBtn(file) { await this.deleteFile(file.id) }, toggleSelection(fileObject) { if (fileObject != this.previewFile) { this.setPreviewFile(fileObject) } else { this.setPreviewFile(null) } }, getSelectedClass(fileObject) { if (this.previewFile && fileObject.id == this.previewFile.id) { return 'selected' } else { return '' } }, getFileName(fileObject) { let base = path.basename(fileObject.media) if (base.length < 20) { return base } else { return `${base.substr(0, 18)}...` } }, ...mapActions(['loadFiles', 'createFile', 'deleteFile']), ...mapMutations((['setPreviewFile', 'setFiles'])) }, computed: { searchedFiles() { return this.files.filter(file => path.basename(file.media).toLowerCase().includes(this.search.toLowerCase()) && file.thumbnail) }, ...mapState(['files', 'previewFile', 'uploads']) }, mounted() { this.setFiles([]) this.loadFiles(this.project.id) }, unmounted() { this.setFiles([]) this.setPreviewFile(null) } } </script> <style scoped> .scrolling-container { max-height: 300px; overflow: scroll; } .upload-btn { float: right; } .selected { border: #0d6efd 4px solid; } .figure-caption { font-size: 0.8em; } .file-thumbnail { cursor: pointer; } .context-menu { color: #ffffff; position: absolute; top: 0%; right: 0%; padding: 10px; cursor: pointer; } .small-dropdown { min-width: 5em; opacity: 80%; } </style>