|
@@ -19,8 +19,8 @@
|
|
|
borderless
|
|
borderless
|
|
|
hide-bottom-space
|
|
hide-bottom-space
|
|
|
:rules="rules"
|
|
:rules="rules"
|
|
|
- :error="error"
|
|
|
|
|
- :error-message="errorMessage"
|
|
|
|
|
|
|
+ :error="error || Boolean(sizeError)"
|
|
|
|
|
+ :error-message="sizeError || errorMessage"
|
|
|
class="image-preview-container"
|
|
class="image-preview-container"
|
|
|
>
|
|
>
|
|
|
<div
|
|
<div
|
|
@@ -51,6 +51,7 @@
|
|
|
? $t("common.ui.file.click_select_image")
|
|
? $t("common.ui.file.click_select_image")
|
|
|
: $t("common.ui.file.click_select")
|
|
: $t("common.ui.file.click_select")
|
|
|
}}
|
|
}}
|
|
|
|
|
+ <div>{{ $t("common.ui.file.max_size_hint", { max: maxSizeMB }) }}</div>
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
@@ -71,7 +72,7 @@
|
|
|
<q-file
|
|
<q-file
|
|
|
v-show="false"
|
|
v-show="false"
|
|
|
ref="fileInputRef"
|
|
ref="fileInputRef"
|
|
|
- v-model="internalFile"
|
|
|
|
|
|
|
+ v-model="pickedFile"
|
|
|
:accept="accept"
|
|
:accept="accept"
|
|
|
:multiple="multiple"
|
|
:multiple="multiple"
|
|
|
/>
|
|
/>
|
|
@@ -81,6 +82,7 @@
|
|
|
|
|
|
|
|
<script setup>
|
|
<script setup>
|
|
|
import { ref, watch, computed, onUnmounted, useTemplateRef, useAttrs } from "vue";
|
|
import { ref, watch, computed, onUnmounted, useTemplateRef, useAttrs } from "vue";
|
|
|
|
|
+import { useMediaFile } from "src/composables/useMediaFile";
|
|
|
|
|
|
|
|
defineOptions({
|
|
defineOptions({
|
|
|
inheritAttrs: false,
|
|
inheritAttrs: false,
|
|
@@ -125,12 +127,15 @@ const emit = defineEmits(["files"]);
|
|
|
|
|
|
|
|
const attrs = useAttrs();
|
|
const attrs = useAttrs();
|
|
|
const fileInputRef = useTemplateRef("fileInputRef");
|
|
const fileInputRef = useTemplateRef("fileInputRef");
|
|
|
|
|
+const { prepareFile, prepareFiles, maxSizeMB } = useMediaFile();
|
|
|
|
|
|
|
|
const model = defineModel({ type: [File, String, null], default: null });
|
|
const model = defineModel({ type: [File, String, null], default: null });
|
|
|
const base64File = defineModel("base64File", { type: String, default: null });
|
|
const base64File = defineModel("base64File", { type: String, default: null });
|
|
|
|
|
|
|
|
const isDragging = ref(false);
|
|
const isDragging = ref(false);
|
|
|
|
|
+const pickedFile = ref(null);
|
|
|
const internalFile = ref(null);
|
|
const internalFile = ref(null);
|
|
|
|
|
+const sizeError = ref(null);
|
|
|
const objectUrl = ref(null);
|
|
const objectUrl = ref(null);
|
|
|
|
|
|
|
|
const required = computed(() => props.rules.some((r) => r?.$id === "required"));
|
|
const required = computed(() => props.rules.some((r) => r?.$id === "required"));
|
|
@@ -187,12 +192,19 @@ function handleDrop(event) {
|
|
|
const dropped = Array.from(event.dataTransfer?.files ?? []).filter(matchesAccept);
|
|
const dropped = Array.from(event.dataTransfer?.files ?? []).filter(matchesAccept);
|
|
|
if (!dropped.length) return;
|
|
if (!dropped.length) return;
|
|
|
|
|
|
|
|
- if (props.multiple) {
|
|
|
|
|
- emit("files", dropped);
|
|
|
|
|
|
|
+ pickedFile.value = props.multiple ? dropped : dropped[0];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function acceptFile(file) {
|
|
|
|
|
+ if (!(file instanceof File)) {
|
|
|
|
|
+ sizeError.value = null;
|
|
|
|
|
+ internalFile.value = null;
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- internalFile.value = dropped[0];
|
|
|
|
|
|
|
+ const { file: prepared, message } = await prepareFile(file);
|
|
|
|
|
+ sizeError.value = message;
|
|
|
|
|
+ internalFile.value = prepared;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function generateBase64(file) {
|
|
function generateBase64(file) {
|
|
@@ -203,16 +215,22 @@ function generateBase64(file) {
|
|
|
reader.readAsDataURL(file);
|
|
reader.readAsDataURL(file);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-watch(internalFile, (newFile) => {
|
|
|
|
|
|
|
+watch(pickedFile, async (picked) => {
|
|
|
|
|
+ if (picked === null) return;
|
|
|
|
|
+
|
|
|
|
|
+ pickedFile.value = null;
|
|
|
|
|
+
|
|
|
if (props.multiple) {
|
|
if (props.multiple) {
|
|
|
- const selected = Array.isArray(newFile) ? newFile : [];
|
|
|
|
|
- if (selected.length) {
|
|
|
|
|
- emit("files", selected);
|
|
|
|
|
- internalFile.value = null;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const selected = Array.isArray(picked) ? picked : [picked];
|
|
|
|
|
+ const accepted = await prepareFiles(selected);
|
|
|
|
|
+ if (accepted.length) emit("files", accepted);
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ await acceptFile(picked);
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+watch(internalFile, (newFile) => {
|
|
|
if (objectUrl.value) {
|
|
if (objectUrl.value) {
|
|
|
URL.revokeObjectURL(objectUrl.value);
|
|
URL.revokeObjectURL(objectUrl.value);
|
|
|
objectUrl.value = null;
|
|
objectUrl.value = null;
|
|
@@ -232,6 +250,7 @@ watch(internalFile, (newFile) => {
|
|
|
watch(model, (val) => {
|
|
watch(model, (val) => {
|
|
|
if (!val && internalFile.value) {
|
|
if (!val && internalFile.value) {
|
|
|
internalFile.value = null;
|
|
internalFile.value = null;
|
|
|
|
|
+ sizeError.value = null;
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
|
|
|