|
|
@@ -19,8 +19,8 @@
|
|
|
borderless
|
|
|
hide-bottom-space
|
|
|
:rules="rules"
|
|
|
- :error="error"
|
|
|
- :error-message="errorMessage"
|
|
|
+ :error="error || Boolean(sizeError)"
|
|
|
+ :error-message="sizeError || errorMessage"
|
|
|
class="image-preview-container"
|
|
|
>
|
|
|
<div
|
|
|
@@ -51,6 +51,7 @@
|
|
|
? $t("common.ui.file.click_select_image")
|
|
|
: $t("common.ui.file.click_select")
|
|
|
}}
|
|
|
+ <div>{{ $t("common.ui.file.max_size_hint", { max: maxSizeMB }) }}</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
@@ -71,8 +72,9 @@
|
|
|
<q-file
|
|
|
v-show="false"
|
|
|
ref="fileInputRef"
|
|
|
- v-model="internalFile"
|
|
|
+ v-model="pickedFile"
|
|
|
:accept="accept"
|
|
|
+ :multiple="multiple"
|
|
|
/>
|
|
|
</q-field>
|
|
|
</div>
|
|
|
@@ -80,6 +82,7 @@
|
|
|
|
|
|
<script setup>
|
|
|
import { ref, watch, computed, onUnmounted, useTemplateRef, useAttrs } from "vue";
|
|
|
+import { useMediaFile } from "src/composables/useMediaFile";
|
|
|
|
|
|
defineOptions({
|
|
|
inheritAttrs: false,
|
|
|
@@ -102,6 +105,10 @@ const props = defineProps({
|
|
|
type: String,
|
|
|
default: "image",
|
|
|
},
|
|
|
+ multiple: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false,
|
|
|
+ },
|
|
|
initialImage: {
|
|
|
type: String,
|
|
|
default: null,
|
|
|
@@ -116,14 +123,19 @@ const props = defineProps({
|
|
|
},
|
|
|
});
|
|
|
|
|
|
+const emit = defineEmits(["files"]);
|
|
|
+
|
|
|
const attrs = useAttrs();
|
|
|
const fileInputRef = useTemplateRef("fileInputRef");
|
|
|
+const { prepareFile, prepareFiles, maxSizeMB } = useMediaFile();
|
|
|
|
|
|
const model = defineModel({ type: [File, String, null], default: null });
|
|
|
const base64File = defineModel("base64File", { type: String, default: null });
|
|
|
|
|
|
const isDragging = ref(false);
|
|
|
+const pickedFile = ref(null);
|
|
|
const internalFile = ref(null);
|
|
|
+const sizeError = ref(null);
|
|
|
const objectUrl = ref(null);
|
|
|
|
|
|
const required = computed(() => props.rules.some((r) => r?.$id === "required"));
|
|
|
@@ -162,23 +174,37 @@ function handleDragLeave() {
|
|
|
isDragging.value = false;
|
|
|
}
|
|
|
|
|
|
-function handleDrop(event) {
|
|
|
- event.preventDefault();
|
|
|
- isDragging.value = false;
|
|
|
- const file = event.dataTransfer?.files?.[0];
|
|
|
- if (!file) return;
|
|
|
+function matchesAccept(file) {
|
|
|
const acceptedMimes = props.accept
|
|
|
.split(",")
|
|
|
.map((m) => m.trim())
|
|
|
.filter(Boolean);
|
|
|
|
|
|
- const matches = acceptedMimes.some((mime) =>
|
|
|
+ return acceptedMimes.some((mime) =>
|
|
|
mime.endsWith("/*") ? file.type.startsWith(mime.slice(0, -1)) : mime === file.type,
|
|
|
);
|
|
|
+}
|
|
|
+
|
|
|
+function handleDrop(event) {
|
|
|
+ event.preventDefault();
|
|
|
+ isDragging.value = false;
|
|
|
|
|
|
- if (matches) {
|
|
|
- internalFile.value = file;
|
|
|
+ const dropped = Array.from(event.dataTransfer?.files ?? []).filter(matchesAccept);
|
|
|
+ if (!dropped.length) return;
|
|
|
+
|
|
|
+ pickedFile.value = props.multiple ? dropped : dropped[0];
|
|
|
+}
|
|
|
+
|
|
|
+async function acceptFile(file) {
|
|
|
+ if (!(file instanceof File)) {
|
|
|
+ sizeError.value = null;
|
|
|
+ internalFile.value = null;
|
|
|
+ return;
|
|
|
}
|
|
|
+
|
|
|
+ const { file: prepared, message } = await prepareFile(file);
|
|
|
+ sizeError.value = message;
|
|
|
+ internalFile.value = prepared;
|
|
|
}
|
|
|
|
|
|
function generateBase64(file) {
|
|
|
@@ -189,6 +215,21 @@ function generateBase64(file) {
|
|
|
reader.readAsDataURL(file);
|
|
|
}
|
|
|
|
|
|
+watch(pickedFile, async (picked) => {
|
|
|
+ if (picked === null) return;
|
|
|
+
|
|
|
+ pickedFile.value = null;
|
|
|
+
|
|
|
+ if (props.multiple) {
|
|
|
+ const selected = Array.isArray(picked) ? picked : [picked];
|
|
|
+ const accepted = await prepareFiles(selected);
|
|
|
+ if (accepted.length) emit("files", accepted);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ await acceptFile(picked);
|
|
|
+});
|
|
|
+
|
|
|
watch(internalFile, (newFile) => {
|
|
|
if (objectUrl.value) {
|
|
|
URL.revokeObjectURL(objectUrl.value);
|
|
|
@@ -209,6 +250,7 @@ watch(internalFile, (newFile) => {
|
|
|
watch(model, (val) => {
|
|
|
if (!val && internalFile.value) {
|
|
|
internalFile.value = null;
|
|
|
+ sizeError.value = null;
|
|
|
}
|
|
|
});
|
|
|
|