tethys.backend/resources/js/Components/FileUpload.vue
Arno Kaimbacher ac473b1e72 - added LicenseController.ts and MimetypeController for enabling mime_types and licences
- add new authors and contributors only by unique email addresses
- allow multiple file upload
- added validation rule for validating length of uploaded files
- modified Dockerfile for starting "bin/server.js" instead of *server.js"
- npm updates
2024-06-14 12:38:04 +02:00

505 lines
21 KiB
Vue

<template>
<section aria-label="File Upload Modal"
class="relative h-full flex flex-col bg-white dark:bg-slate-900/70 shadow-xl rounded-md"
v-on:dragenter="dragEnterHandler" v-on:dragleave="dragLeaveHandler" v-on:dragover="dragOverHandler"
v-on:drop="dropHandler">
<!-- ondrop="dropHandler(event);"
ondragover="dragOverHandler(event);"
ondragleave="dragLeaveHandler(event);"
ondragenter="dragEnterHandler(event);" -->
<!-- overlay -->
<div id="overlay" ref="overlay"
class="w-full h-full absolute top-0 left-0 pointer-events-none z-50 flex flex-col items-center justify-center rounded-md">
<i>
<svg class="fill-current w-12 h-12 mb-3 text-blue-700" xmlns="http://www.w3.org/2000/svg" width="24"
height="24" viewBox="0 0 24 24">
<path
d="M19.479 10.092c-.212-3.951-3.473-7.092-7.479-7.092-4.005 0-7.267 3.141-7.479 7.092-2.57.463-4.521 2.706-4.521 5.408 0 3.037 2.463 5.5 5.5 5.5h13c3.037 0 5.5-2.463 5.5-5.5 0-2.702-1.951-4.945-4.521-5.408zm-7.479-1.092l4 4h-3v4h-2v-4h-3l4-4z" />
</svg>
</i>
<p class="text-lg text-blue-700">Drop files to upload</p>
</div>
<!-- scroll area -->
<div class="h-full p-8 w-full h-full flex flex-col">
<!-- <header id="dropzone" class="border-dashed border-2 border-gray-400 py-12 flex flex-col justify-center items-center">
<p class="mb-3 font-semibold text-gray-900 flex flex-wrap justify-center">
<span>Drag and drop your</span>&nbsp;<span>files anywhere or</span>
</p>
<input id="hidden-input" type="file" multiple class="hidden" />
<button id="button" class="mt-2 rounded-sm px-3 py-1 bg-gray-200 hover:bg-gray-300 focus:shadow-outline focus:outline-none">
Upload a file
</button>
</header> -->
<header class="flex items-center justify-center w-full">
<label for="dropzone-file"
class="flex flex-col items-center justify-center w-full h-64 border-2 border-gray-300 border-dashed rounded-lg cursor-pointer bg-gray-50 dark:bg-gray-700 hover:bg-gray-100 dark:border-gray-600 dark:hover:border-gray-500 dark:hover:bg-gray-600">
<div class="flex flex-col items-center justify-center pt-5 pb-6">
<svg aria-hidden="true" class="w-10 h-10 mb-3 text-gray-400" fill="none" stroke="currentColor"
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12">
</path>
</svg>
<p class="mb-2 text-sm text-gray-500 dark:text-gray-400">
<span class="font-semibold">Click to upload</span> or drag and drop
</p>
<!-- <p class="text-xs text-gray-500 dark:text-gray-400">SVG, PNG, JPG or GIF (MAX. 800x400px)</p> -->
</div>
<input id="dropzone-file" type="file" class="hidden" @change="onChangeFile" multiple="true"/>
</label>
</header>
<h1 class="pt-8 pb-3 font-semibold sm:text-lg text-gray-900">To Upload</h1>
<!-- <ul id="gallery" class="flex flex-1 flex-wrap -m-1"> -->
<draggable id="galleryxy" tag="ul" class="flex flex-1 flex-wrap -m-1" v-model="items" item-key="sort_order">
<!-- <li
v-if="files.length == 0"
id="empty"
class="h-full w-full text-center flex flex-col items-center justify-center items-center"
>
<svg
class="w-20 h-20 mb-3 text-gray-400"
version="1.1"
id="Capa_1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 412.533 412.533"
xml:space="preserve"
fill="#000000"
>
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
<g id="SVGRepo_iconCarrier">
<g>
<path
style="fill: #010002"
d="M412.485,203.954h0.041c0-14.323-5.609-27.336-14.729-37.042l0.016-0.016l-79.27-101.819H90.479 L13.493,168.961l0.033,0.033c-7.283,8.616-11.762,19.565-12.534,31.514C0.415,201.629,0,202.84,0,204.19v135.138 c0,4.495,3.642,8.129,8.129,8.129h396.276c4.495,0,8.129-3.633,8.129-8.129V204.19 C412.533,204.109,412.485,204.035,412.485,203.954z M97.844,81.335H311.43l48.389,68.5c-0.512-0.016-1-0.081-1.52-0.081h-97.502 v24.369c0,27.67-29.052,21.687-37.96,21.687h-32.466c-8.909,0-37.96,5.983-37.96-21.687v-24.369H54.9 c-1.016,0-2.008,0.098-3.016,0.146L97.844,81.335z M396.276,331.199H16.265V204.19c0-0.081-0.041-0.154-0.049-0.236h0.723 c0-20.923,17.029-37.944,37.96-37.944h81.253v8.112c0,27.987,21.281,37.944,54.218,37.944h32.466 c32.945,0,54.218-9.957,54.218-37.944v-8.112h81.261c10.461,0,19.948,4.251,26.824,11.12l0.016,0.016 c6.869,6.869,11.112,16.347,11.112,26.808h0.057c0,0.081-0.049,0.154-0.049,0.236C396.276,204.19,396.276,331.199,396.276,331.199z "
></path>
</g>
</g>
</svg>
<span class="text-small text-gray-500">No files selected</span>
</li> -->
<template #item="{ index, element }">
<li class="block p-1 w-1/2 sm:w-1/3 md:w-1/4 lg:w-1/6 xl:w-1/8 h-24" :key="index">
<article v-if="element.type.match('image.*')" tabindex="0"
class="bg-gray-50 group hasImage w-full h-full rounded-md cursor-pointer relative text-transparent hover:text-white shadow-sm">
<!-- :src="element.fileSrc" :src="generateURL(element)" -->
<img :alt="element.name" :src="element.fileSrc"
class="img-preview w-full h-full sticky object-cover rounded-md bg-fixed opacity-75" />
<!-- <section
class="hasError text-red-500 shadow-sm font-semibold flex flex-row rounded-md text-xs break-words w-full h-full z-21 absolute top-0 py-2 px-3"
>
<p class="p-1 text-xs" v-if="errors[`files.${index}`]">
{{ errors[`files.${index}`].join(', ') }}
</p>
<button
class="delete ml-auto focus:outline-none hover:bg-gray-300 p-1 rounded-md"
@click="removeFile(index)"
>
<DeleteIcon></DeleteIcon>
</button>
</section> -->
<section
class="flex flex-col rounded-md text-xs break-words w-full h-full z-20 absolute top-0 py-2 px-3">
<h1 class="flex-1">{{ element.name }}</h1>
<div class="flex">
<p class="p-1 size text-xs">{{ getFileSize(element) }}</p>
<p class="p-1 size text-xs text-gray-700">{{ element.sort_order }}</p>
<button class="delete ml-auto focus:outline-none hover:bg-gray-300 p-1 rounded-md"
@click="removeFile(index)">
<DeleteIcon></DeleteIcon>
</button>
</div>
</section>
<!-- <div class="text-red-400 text-sm w-full h-full rounded-md cursor-pointer relative shadow-sm" v-if="errors[`files.${index}`]">
{{ errors[`files.${index}`].join(', ') }}
</div> -->
</article>
<!-- :class="errors && errors[`files.${index}`] ? 'bg-red-400' : 'bg-gray-100'" -->
<article v-else tabindex="0"
class="bg-gray-100 group w-full h-full rounded-md cursor-pointer relative shadow-sm">
<section
class="flex flex-col rounded-md text-xs break-words w-full h-full z-20 absolute top-0 py-2 px-3">
<h1 class="flex-1 text-gray-700 group-hover:text-blue-800">{{ element.name }}</h1>
<div class="flex">
<p class="p-1 size text-xs text-gray-700">{{ getFileSize(element) }}</p>
<p class="p-1 size text-xs text-gray-700">{{ element.sort_order }}</p>
<button
class="delete ml-auto focus:outline-none hover:bg-gray-300 p-1 rounded-md text-gray-800"
@click="removeFile(index)">
<DeleteIcon></DeleteIcon>
</button>
</div>
</section>
</article>
</li>
</template>
</draggable>
<!-- </ul> -->
<div v-if="fileErrors" class="flex flex-col mt-6 animate-fade-in" v-for="fileError in fileErrors">
<div class="bg-yellow-500 border-l-4 border-orange-400 text-white p-4" role="alert">
<p class="font-bold">Be Warned</p>
<p>{{ fileError.join(', ') }}</p>
</div>
</div>
<!-- <div class="text-red-400 text-sm" v-if="errors && Array.isArray(errors['files.0'])">
{{ errors['files.0'].join(', ') }}
</div> -->
<!-- <div v-if="hasErrors">
<div class="font-medium text-red-600">Whoops! Something went wrong.</div>
<ul class="mt-3 list-disc list-inside text-sm text-red-600">
<li v-for="(error, key) in errors" :key="key">{{ error }}</li>
</ul>
</div> -->
</div>
<!-- sticky footer -->
<footer class="flex justify-end px-8 pb-8 pt-4">
<button id="cancel" class="ml-3 rounded-sm px-3 py-1 hover:bg-gray-300 focus:shadow-outline focus:outline-none"
@click="clearAllFiles">
Clear
</button>
</footer>
</section>
</template>
<script lang="ts">
import { Component, Vue, Prop, Ref, Watch } from 'vue-facing-decorator';
import { usePage } from '@inertiajs/vue3';
import DeleteIcon from '@/Components/Icons/Delete.vue';
// import { Page, PageProps, Errors, ErrorBag } from '@inertiajs/inertia';
import Draggable from 'vuedraggable';
import { Buffer } from 'buffer';
import { TethysFile } from '@/Dataset';
// lastModified: 1691759507591
// lastModifiedDate: Fri Aug 11 2023 15:11:47 GMT+0200 (Mitteleuropäische Sommerzeit)
// name: 'freieIP.png'
// size: 112237
// type: 'image/png'
// webkitRelativePath: ''
interface IDictionary {
[index: string]: Array<string>;
}
export declare type ErrorBag1 = Record<string, Array<string>>;
interface InteriaPage {
// extends Page<PageProps> {
[key: string]: boolean | number | string | Object | (string | null);
errors: IDictionary;
props: {
// [key: string]: Array<string>;
errors: IDictionary;
auth: {
user: {
name: string;
};
};
// laravelVersion: string;
// phpVersion: string;
};
}
@Component({
name: 'file-upload',
components: {
DeleteIcon,
Draggable,
},
})
class FileUploadComponent extends Vue {
/**
* Connect map id.
*/
// @Prop({
// type: Object,
// default: () => ({}),
// })
// errors: IErrorMessage;
@Ref('overlay') overlay: HTMLDivElement;
private counter: number = 0;
// @Prop() files: Array<TestFile>;
@Prop({
type: Array<File>,
default: [],
})
files: Array<TethysFile | File>;
get items(): Array<TethysFile | File> {
return this.files;
}
set items(values: Array<TethysFile | File>) {
// this.modelValue = value;
this.files.length = 0;
this.files.push(...values);
// values.forEach((item, index) => {
// item.sort_order = index + 1; // Assuming sort_order starts from 1
// this.files.push(item);
// });
}
@Watch("files", {
deep: true //also in case of pushing
})
public propertyWatcher(newItems: Array<TethysFile>) {
// Update sort_order based on the new index when the list is changed
newItems.forEach((item, index) => {
item.sort_order = index + 1; // Assuming sort_order starts from 1
});
}
public created() {
for (const file of this.files) {
if (!(file instanceof File)) {
// console.log(`${file.name} path is ${file.filePath} here.`);
this.generateURL(file);
// console.log(`${file.fileSrc} path.`);
}
}
}
public dragEnterHandler(e) {
e.preventDefault();
if (!this._hasFiles(e.dataTransfer)) {
return;
}
++this.counter && this.overlay.classList.add('draggedover');
}
public dragLeaveHandler() {
1 > --this.counter && this.overlay.classList.remove('draggedover');
}
public dragOverHandler(e) {
if (this._hasFiles(e.dataTransfer)) {
e.preventDefault();
}
}
public startDrag(evt, item) {
evt.dataTransfer.dropEffect = 'move';
evt.dataTransfer.effectAllowed = 'move';
evt.dataTransfer.setData('itemID', item.id);
}
// reset counter and append file to gallery when file is dropped
public dropHandler(event) {
event.preventDefault();
for (const file of event.dataTransfer.files) {
// let fileName = String(file.name.replace(/\.[^/.]+$/, ''));
// file.label = fileName;
// if (file.type.match('image.*')) {
// this.generateURL(file);
// }
this._addFile(file);
}
this.overlay.classList.remove('draggedover');
this.counter = 0;
}
public onChangeFile(event) {
event.preventDefault();
// let uploadedFile = event.target.files[0];
// let fileName = String(event.target.files[0].name.replace(/\.[^/.]+$/, ''));
for (const file of event.target.files) {
// let fileName = String(event.target.files[0].name.replace(/\.[^/.]+$/, ''));
// file.label = fileName;
// if (file.type.match('image.*')) {
// this.generateURL(file);
// }
this._addFile(file);
}
// this.overlay.classList.remove('draggedover');
this.counter = 0;
}
get errors(): IDictionary {
if (usePage().props.errors) {
// return usePage().props.errors;
return usePage<InteriaPage>().props.errors;
} else {
return {};
}
}
get hasErrors(): boolean {
return Object.keys(this.errors).length > 0;
}
get fileErrors() {
return Object.fromEntries(Object.entries(this.errors).filter(([key]) => key.startsWith('file')));
}
public clearAllFiles(event) {
event.preventDefault();
this.items.splice(0);
}
public removeFile(key) {
this.items.splice(key, 1);
}
public generateURL(file: TethysFile | File): string {
// const arrayBuffer = Buffer.from(file.fileData.data);
// const blob = new Blob([file.fileData.data], { type: 'application/octet-stream' });
// const blob = new Blob([file.fileData], { type: 'image/png'});
// let fileSrc = file.fileData;
let localUrl: string = "";
if (file instanceof File) {
localUrl = URL.createObjectURL(file as Blob);
} else if (file.fileData) {
// const blob = new Blob([file.fileData]);
// localUrl = URL.createObjectURL(blob);
const parsed = JSON.parse(file.fileData);
file.fileData = "";
// retrieve the original buffer of data
const buff = Buffer.from(parsed.blob, "base64");
const blob = new Blob([buff], { type: 'application/octet-stream' });
// file.blob = blob;
localUrl = URL.createObjectURL(blob);
file.fileSrc = localUrl;
}
// setTimeout(() => {
// URL.revokeObjectURL(localUrl);
// }, 1000);
return localUrl;
}
// private async downloadFile(id: number): Promise<string> {
// const response = await axios.get<Blob>(`/api/download/${id}`, {
// responseType: 'blob',
// });
// const url = URL.createObjectURL(response.data);
// setTimeout(() => {
// URL.revokeObjectURL(url);
// }, 1000);
// return url;
// }
public getFileSize(file) {
if (file.size > 1024) {
if (file.size > 1048576) {
return Math.round(file.size / 1048576) + 'mb';
} else {
return Math.round(file.size / 1024) + 'kb';
}
} else {
return file.size + 'b';
}
}
// private _addFile(file) {
// // const isImage = file.type.match('image.*');
// // const objectURL = URL.createObjectURL(file);
// // this.files[objectURL] = file;
// // let test: TethysFile = { upload: file, label: "dfdsfs", sorting: 0 };
// // file.sorting = this.files.length;
// file.sort_order = (this.items.length + 1),
// this.files.push(file);
// }
private _addFile(file: File) {
// const reader = new FileReader();
// reader.onload = (event) => {
// const base64Data = (event.target as FileReader).result as string;
// this.items.push(test);
// };
// reader.readAsDataURL(file);
if (file instanceof File) {
// const base64Data = await this.readBase64(file);
let test: TethysFile = {
label: file.name,
name: file.name,
size: file.size,
file_size: file.size,
// fileData: JSON.stringify({ blob: base64Data }),
// filePath: file.mozFullPath,
// path_name: file.mozFullPath,
webkitRelativePath: '',
lastModified: file.lastModified,
type: file.type,
mime_type: file.type,
visible_in_frontdoor: false,
visible_in_oai: false,
fileSrc: file.type.match('image.*')? this.generateURL(file) : "",
blob: file as Blob,
sort_order: (this.items.length + 1),
};
// this.items.push(test);
this.items[this.items.length] = test;
}
else {
this.items.push(file);
}
}
// private async readBase64(blob: Blob): Promise<string> {
// return new Promise<string>((resolve, reject) => {
// const reader = new FileReader();
// reader.onload = (event) => resolve((event.target as FileReader).result as string);
// reader.onerror = reject;
// reader.readAsDataURL(blob);
// });
// }
// use to check if a file is being dragged
private _hasFiles({ types = [] as Array<string> }) {
return types.indexOf('Files') > -1;
}
}
export default FileUploadComponent;
</script>
<style lang="css">
.hasImage:hover section {
background-color: rgba(5, 5, 5, 0.4);
}
.hasImage:hover button:hover {
background: rgba(5, 5, 5, 0.45);
}
section.hasError {
background-color: rgba(5, 5, 5, 0.4);
}
#overlay p,
i {
opacity: 0;
}
#overlay.draggedover {
background-color: rgba(255, 255, 255, 0.7);
}
#overlay.draggedover p,
#overlay.draggedover i {
opacity: 1;
}
.group:hover .group-hover\:text-blue-800 {
color: #2b6cb0;
}
</style>