|
|
@@ -4,30 +4,31 @@
|
|
|
v-bind="$attrs"
|
|
|
borderless
|
|
|
:rules="rules"
|
|
|
- :error
|
|
|
- :error-message
|
|
|
+ :error="error"
|
|
|
+ :error-message="errorMessage"
|
|
|
+ class="custom-file-input"
|
|
|
>
|
|
|
<div class="column flex-center q-mb-sm full-width">
|
|
|
- <span class="text-grey-6">{{ label }}</span>
|
|
|
+ <span v-if="label" class="text-grey-6 q-mb-xs">{{ label }}</span>
|
|
|
<div
|
|
|
class="image-preview-container"
|
|
|
:class="{
|
|
|
'has-image': preview,
|
|
|
'is-dragging': isDragging,
|
|
|
}"
|
|
|
- @click="pickImage"
|
|
|
- @dragover.prevent="handleDragOver"
|
|
|
- @dragleave.prevent="handleDragLeave"
|
|
|
- @drop.prevent="handleDrop"
|
|
|
+ @click="pickFile"
|
|
|
+ @dragover="handleDragOver"
|
|
|
+ @dragleave="handleDragLeave"
|
|
|
+ @drop="handleDrop"
|
|
|
>
|
|
|
<template v-if="!preview">
|
|
|
<q-icon
|
|
|
:name="
|
|
|
isDragging
|
|
|
- ? 'file_upload'
|
|
|
- : type == 'image'
|
|
|
- ? 'add_photo_alternate'
|
|
|
- : 'insert_drive_file'
|
|
|
+ ? 'mdi-upload'
|
|
|
+ : type === 'image'
|
|
|
+ ? 'mdi-image-plus'
|
|
|
+ : 'mdi-file-plus'
|
|
|
"
|
|
|
size="48px"
|
|
|
color="grey-6"
|
|
|
@@ -45,40 +46,47 @@
|
|
|
}}
|
|
|
</div>
|
|
|
</template>
|
|
|
+
|
|
|
<q-img
|
|
|
- v-else-if="type == 'image'"
|
|
|
+ v-else-if="type === 'image'"
|
|
|
:src="preview"
|
|
|
fit="cover"
|
|
|
class="full-height"
|
|
|
- >
|
|
|
- <div class="absolute-bottom text-right">
|
|
|
- <q-btn
|
|
|
- flat
|
|
|
- dense
|
|
|
- round
|
|
|
- color="negative"
|
|
|
- icon="delete"
|
|
|
- @click.stop="clearImage"
|
|
|
- />
|
|
|
- </div>
|
|
|
- </q-img>
|
|
|
+ />
|
|
|
+
|
|
|
<div v-else class="position-relative column full-height flex-center">
|
|
|
<q-icon name="mdi-file-check" size="48px" color="grey-6" />
|
|
|
<div
|
|
|
class="absolute-bottom text-caption text-grey-6 text-center q-mb-sm q-px-md"
|
|
|
>
|
|
|
- {{ preview }}
|
|
|
+ {{ model.name }}
|
|
|
</div>
|
|
|
</div>
|
|
|
+
|
|
|
+ <div v-if="preview" class="absolute-top-right q-ma-xs">
|
|
|
+ <q-btn
|
|
|
+ flat
|
|
|
+ dense
|
|
|
+ round
|
|
|
+ color="negative"
|
|
|
+ icon="mdi-close"
|
|
|
+ @click.stop="clearFile"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
</div>
|
|
|
|
|
|
- <q-file v-show="false" ref="fileInput" v-model="model" :accept="accept" />
|
|
|
+ <q-file
|
|
|
+ v-show="false"
|
|
|
+ ref="fileInputRef"
|
|
|
+ v-model="model"
|
|
|
+ :accept="accept"
|
|
|
+ />
|
|
|
</div>
|
|
|
</q-field>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { ref, useTemplateRef, watch } from "vue";
|
|
|
+import { ref, watch, onUnmounted, useTemplateRef } from "vue";
|
|
|
|
|
|
const { label, rules, accept, type, initialImage } = defineProps({
|
|
|
label: {
|
|
|
@@ -111,58 +119,66 @@ const { label, rules, accept, type, initialImage } = defineProps({
|
|
|
},
|
|
|
});
|
|
|
|
|
|
-const model = defineModel();
|
|
|
+const model = defineModel({ type: [File, String, null], default: null });
|
|
|
const base64File = defineModel("base64File", { type: String, default: null });
|
|
|
-const preview = ref(initialImage);
|
|
|
+
|
|
|
const isDragging = ref(false);
|
|
|
-const fileInput = useTemplateRef("fileInput");
|
|
|
+const fileInputRef = useTemplateRef("fileInputRef");
|
|
|
+const preview = ref(initialImage || null);
|
|
|
+let objectUrl = null;
|
|
|
|
|
|
-const processFile = async (file) => {
|
|
|
- if (!file) {
|
|
|
- console.error("No file provided");
|
|
|
- return;
|
|
|
+const cleanupObjectURL = () => {
|
|
|
+ if (objectUrl) {
|
|
|
+ URL.revokeObjectURL(objectUrl);
|
|
|
+ objectUrl = null;
|
|
|
}
|
|
|
+};
|
|
|
+onUnmounted(cleanupObjectURL);
|
|
|
|
|
|
- if (type == "image" && !file.type.startsWith("image/")) {
|
|
|
- console.error("Invalid file type");
|
|
|
+const generateBase64 = (file) => {
|
|
|
+ if (!file) {
|
|
|
+ base64File.value = null;
|
|
|
return;
|
|
|
}
|
|
|
+ const reader = new FileReader();
|
|
|
+ reader.onload = (e) => {
|
|
|
+ base64File.value = e.target.result;
|
|
|
+ };
|
|
|
+ reader.onerror = () => {
|
|
|
+ console.error("FileReader failed to read file.");
|
|
|
+ base64File.value = null;
|
|
|
+ };
|
|
|
+ reader.readAsDataURL(file);
|
|
|
+};
|
|
|
+
|
|
|
+watch(model, (newFile) => {
|
|
|
+ cleanupObjectURL();
|
|
|
|
|
|
- if (type == "file") {
|
|
|
- const blob = new Blob([file], { type: file.type });
|
|
|
- preview.value = file.name;
|
|
|
- return new Promise((resolve) => {
|
|
|
- const reader = new FileReader();
|
|
|
- reader.onload = (e) => {
|
|
|
- base64File.value = e.target.result;
|
|
|
- console.log(preview.value);
|
|
|
- resolve();
|
|
|
- };
|
|
|
- reader.readAsDataURL(blob);
|
|
|
- });
|
|
|
+ if (newFile && newFile instanceof File) {
|
|
|
+ if (type === "image") {
|
|
|
+ objectUrl = URL.createObjectURL(newFile);
|
|
|
+ preview.value = objectUrl;
|
|
|
+ } else {
|
|
|
+ preview.value = "file_selected";
|
|
|
+ }
|
|
|
+ generateBase64(newFile);
|
|
|
} else {
|
|
|
- return new Promise((resolve) => {
|
|
|
- const reader = new FileReader();
|
|
|
- reader.onload = (e) => {
|
|
|
- base64File.value = e.target.result;
|
|
|
- preview.value = e.target.result;
|
|
|
- resolve();
|
|
|
- };
|
|
|
- reader.readAsDataURL(file);
|
|
|
- });
|
|
|
+ preview.value = initialImage || null;
|
|
|
+ base64File.value = null;
|
|
|
}
|
|
|
-};
|
|
|
+});
|
|
|
|
|
|
-const pickImage = () => {
|
|
|
- fileInput.value?.pickFiles();
|
|
|
+const pickFile = () => {
|
|
|
+ fileInputRef.value?.pickFiles();
|
|
|
};
|
|
|
|
|
|
-const clearImage = () => {
|
|
|
+const clearFile = () => {
|
|
|
model.value = null;
|
|
|
- preview.value = null;
|
|
|
};
|
|
|
|
|
|
-const handleDragOver = () => {
|
|
|
+const handleDragOver = (event) => {
|
|
|
+ event.preventDefault();
|
|
|
+ event.dataTransfer.dropEffect = "copy";
|
|
|
isDragging.value = true;
|
|
|
};
|
|
|
|
|
|
@@ -170,26 +186,31 @@ const handleDragLeave = () => {
|
|
|
isDragging.value = false;
|
|
|
};
|
|
|
|
|
|
-const handleDrop = async (event) => {
|
|
|
+const handleDrop = (event) => {
|
|
|
+ event.preventDefault();
|
|
|
isDragging.value = false;
|
|
|
+
|
|
|
const file = event.dataTransfer?.files?.[0];
|
|
|
+ if (!file) return;
|
|
|
|
|
|
- if (file) {
|
|
|
- model.value = file;
|
|
|
- await processFile(file);
|
|
|
- }
|
|
|
-};
|
|
|
+ const acceptedMime = accept;
|
|
|
|
|
|
-watch(
|
|
|
- () => model.value,
|
|
|
- async (value, oldValue) => {
|
|
|
- if (value != oldValue) {
|
|
|
- await processFile(value);
|
|
|
- } else {
|
|
|
- preview.value = null;
|
|
|
+ if (acceptedMime.endsWith("/*")) {
|
|
|
+ const baseMime = acceptedMime.replace("/*", "");
|
|
|
+ if (file.type.startsWith(baseMime + "/")) {
|
|
|
+ model.value = file;
|
|
|
}
|
|
|
- },
|
|
|
-);
|
|
|
+ } else {
|
|
|
+ if (
|
|
|
+ acceptedMime
|
|
|
+ .split(",")
|
|
|
+ .map((m) => m.trim())
|
|
|
+ .includes(file.type)
|
|
|
+ ) {
|
|
|
+ model.value = file;
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
</script>
|
|
|
|
|
|
<style lang="scss" scoped>
|