diff --git a/frontend/src/components/upload/UploadIntakeControls.vue b/frontend/src/components/upload/UploadIntakeControls.vue index c0fd2ef..cf29350 100644 --- a/frontend/src/components/upload/UploadIntakeControls.vue +++ b/frontend/src/components/upload/UploadIntakeControls.vue @@ -38,6 +38,16 @@ :metadata-error-count="metadataErrorCount" @upload="onUploadFromModal" /> + + +
+
+

Drop files

+
+
+
@@ -77,19 +87,28 @@ const emit = defineEmits<{ const fileInputRef = ref(null); const configureModalOpen = ref(false); +const fileDragDepth = ref(0); onMounted(() => { + window.addEventListener('dragenter', onWindowDragEnter); + window.addEventListener('dragleave', onWindowDragLeave); window.addEventListener('dragover', onWindowDragOver); window.addEventListener('drop', onWindowDrop); + window.addEventListener('dragend', onWindowDragEnd); window.addEventListener('paste', onWindowPaste); }); onBeforeUnmount(() => { + window.removeEventListener('dragenter', onWindowDragEnter); + window.removeEventListener('dragleave', onWindowDragLeave); window.removeEventListener('dragover', onWindowDragOver); window.removeEventListener('drop', onWindowDrop); + window.removeEventListener('dragend', onWindowDragEnd); window.removeEventListener('paste', onWindowPaste); }); +const showFileDragOverlay = computed(() => fileDragDepth.value > 0 && !props.disabled); + const maxImageSizeLabel = computed(() => prettyBytes(maxImageBytes, { binary: true, maximumFractionDigits: 1 })); const emitPicked = (picked: File[]): void => { @@ -117,15 +136,37 @@ const isFileDrag = (event: DragEvent): boolean => { return Boolean(event.dataTransfer?.types.includes('Files')); }; +const onWindowDragEnter = (event: DragEvent): void => { + if (props.disabled || !isFileDrag(event)) { + return; + } + + fileDragDepth.value += 1; +}; + +const onWindowDragLeave = (event: DragEvent): void => { + if (!isFileDrag(event)) { + return; + } + + fileDragDepth.value = Math.max(0, fileDragDepth.value - 1); +}; + const onWindowDragOver = (event: DragEvent): void => { if (props.disabled || !isFileDrag(event)) { return; } event.preventDefault(); + + if (fileDragDepth.value === 0) { + fileDragDepth.value = 1; + } }; const onWindowDrop = (event: DragEvent): void => { + resetFileDragDepth(); + if (props.disabled) { return; } @@ -137,6 +178,10 @@ const onWindowDrop = (event: DragEvent): void => { emitPicked(picked); }; +const onWindowDragEnd = (): void => { + resetFileDragDepth(); +}; + const isPasswordInputTarget = (target: EventTarget | null): boolean => { if (!(target instanceof HTMLElement)) { return false; @@ -175,5 +220,9 @@ const closeConfigureModal = (): void => { configureModalOpen.value = false; }; +const resetFileDragDepth = (): void => { + fileDragDepth.value = 0; +}; + defineExpose({ clearInput, closeConfigureModal });