replace upload dropzone with global intake controls.
Window drag-drop and paste share the same pick path as the file chooser; configure modal lives in the intake component.
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
<template>
|
||||
<div class="flex w-full flex-col gap-3">
|
||||
<p class="mb-3 text-base text-muted-foreground">
|
||||
Drop anywhere, paste or choose files. Max {{ maxQueueSize }} images, up to {{ maxImageSizeLabel }} each.
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<label
|
||||
class="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:opacity-90"
|
||||
:class="disabled ? 'cursor-not-allowed opacity-60' : 'cursor-pointer'"
|
||||
>
|
||||
Choose images
|
||||
<input
|
||||
ref="fileInputRef"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple
|
||||
class="hidden"
|
||||
:disabled="disabled"
|
||||
@change="onFileInputChange"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<button
|
||||
v-if="!uploadConfirmed"
|
||||
type="button"
|
||||
class="inline-flex shrink-0 items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
:disabled="!canOpenConfigure"
|
||||
@click="configureModalOpen = true"
|
||||
>
|
||||
Configure & upload
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<UploadConfigureModal
|
||||
v-model:open="configureModalOpen"
|
||||
v-model:settings="settings"
|
||||
:metadata-error-count="metadataErrorCount"
|
||||
@upload="onUploadFromModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { config } from '@/config';
|
||||
import type { UploadBatchSettingsForm } from '@/types/upload/UploadBatchSettingsForm';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
|
||||
const { maxQueueSize, maxImageBytes } = config.validation.upload;
|
||||
|
||||
const props = defineProps({
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
canOpenConfigure: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
uploadConfirmed: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
metadataErrorCount: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const settings = defineModel<UploadBatchSettingsForm>('settings', { required: true });
|
||||
|
||||
const emit = defineEmits<{
|
||||
pick: [files: File[]];
|
||||
upload: [uploadDespiteMetadataFailure?: boolean];
|
||||
}>();
|
||||
|
||||
const fileInputRef = ref<HTMLInputElement | null>(null);
|
||||
const configureModalOpen = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('dragover', onWindowDragOver);
|
||||
window.addEventListener('drop', onWindowDrop);
|
||||
window.addEventListener('paste', onWindowPaste);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('dragover', onWindowDragOver);
|
||||
window.removeEventListener('drop', onWindowDrop);
|
||||
window.removeEventListener('paste', onWindowPaste);
|
||||
});
|
||||
|
||||
const maxImageSizeLabel = computed(() => prettyBytes(maxImageBytes, { binary: true, maximumFractionDigits: 1 }));
|
||||
|
||||
const emitPicked = (picked: File[]): void => {
|
||||
if (picked.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit('pick', picked);
|
||||
};
|
||||
|
||||
const onFileInputChange = (event: Event): void => {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const picked = input.files ? [...input.files] : [];
|
||||
|
||||
emitPicked(picked);
|
||||
};
|
||||
|
||||
const onUploadFromModal = (uploadDespiteMetadataFailure?: boolean): void => {
|
||||
configureModalOpen.value = false;
|
||||
|
||||
emit('upload', uploadDespiteMetadataFailure);
|
||||
};
|
||||
|
||||
const isFileDrag = (event: DragEvent): boolean => {
|
||||
return Boolean(event.dataTransfer?.types.includes('Files'));
|
||||
};
|
||||
|
||||
const onWindowDragOver = (event: DragEvent): void => {
|
||||
if (props.disabled || !isFileDrag(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
const onWindowDrop = (event: DragEvent): void => {
|
||||
if (props.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const picked = event.dataTransfer?.files ? [...event.dataTransfer.files] : [];
|
||||
|
||||
emitPicked(picked);
|
||||
};
|
||||
|
||||
const isPasswordInputTarget = (target: EventTarget | null): boolean => {
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return target.closest('input[type="password"]') !== null;
|
||||
};
|
||||
|
||||
const onWindowPaste = (event: ClipboardEvent): void => {
|
||||
if (props.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const picked = event.clipboardData?.files ? [...event.clipboardData.files] : [];
|
||||
|
||||
if (picked.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPasswordInputTarget(event.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
emitPicked(picked);
|
||||
};
|
||||
|
||||
const clearInput = (): void => {
|
||||
if (fileInputRef.value) {
|
||||
fileInputRef.value.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const closeConfigureModal = (): void => {
|
||||
configureModalOpen.value = false;
|
||||
};
|
||||
|
||||
defineExpose({ clearInput, closeConfigureModal });
|
||||
</script>
|
||||
Reference in New Issue
Block a user