Compare commits

...

2 Commits

Author SHA1 Message Date
Sprite 8d86f09b50 refactor(attachments): register drag listeners only when edit is enabled
Instead of checking props.editEnabled in every event handler, only
register the listeners when editing is enabled and remove them when
disabled.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 23:38:35 +01:00
Sprite f86be0adf0 fix(attachments): only show dropzone overlay for file drags, not text drags
Replaced useDropZone from VueUse with custom drag event handlers that
check dataTransfer.types for 'Files' before calling preventDefault().

The key fix is in handleDragOver - by only preventing default for actual
file drags, native text dragging operations are no longer blocked.

Fixes #1663

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 23:38:35 +01:00
1 changed files with 98 additions and 43 deletions

View File

@ -172,8 +172,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {ref, shallowReactive, computed, watch} from 'vue' import {ref, shallowReactive, computed, watch, onMounted, onUnmounted} from 'vue'
import {useDropZone} from '@vueuse/core'
import User from '@/components/misc/User.vue' import User from '@/components/misc/User.vue'
import ProgressBar from '@/components/misc/ProgressBar.vue' import ProgressBar from '@/components/misc/ProgressBar.vue'
@ -227,6 +226,17 @@ function eventTargetsEditor(event: Event | null | undefined): boolean {
return false return false
} }
function isFileDrag(event: DragEvent | null | undefined): boolean {
if (!event?.dataTransfer) {
return false
}
// Check if the drag contains files
// dataTransfer.types is a DOMStringList containing the types of data
// 'Files' indicates a file drag from the OS or another application
return Array.from(event.dataTransfer.types).includes('Files')
}
const taskStore = useTaskStore() const taskStore = useTaskStore()
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
@ -239,67 +249,112 @@ const loading = computed(() => attachmentService.loading || taskStore.isLoading)
const isDraggingFiles = ref(false) const isDraggingFiles = ref(false)
const isDragOverEditor = ref(false) const isDragOverEditor = ref(false)
// Track drag depth to handle nested elements properly
const dragDepth = ref(0)
function resetDragState() { function resetDragState() {
isDraggingFiles.value = false isDraggingFiles.value = false
isDragOverEditor.value = false isDragOverEditor.value = false
dragDepth.value = 0
} }
const {isOverDropZone} = useDropZone(document, { function handleDragEnter(event: DragEvent) {
onEnter(files, event) { // Only handle file drags - let text drags work natively
if (!props.editEnabled) { if (!isFileDrag(event)) {
return return
} }
dragDepth.value++
if (dragDepth.value === 1) {
isDraggingFiles.value = true isDraggingFiles.value = true
isDragOverEditor.value = eventTargetsEditor(event) }
}, isDragOverEditor.value = eventTargetsEditor(event)
onOver(files, event) { }
if (!props.editEnabled) {
return
}
isDragOverEditor.value = eventTargetsEditor(event) function handleDragOver(event: DragEvent) {
}, // Only prevent default for file drags - this is the key fix!
onLeave(files, event) { // Not preventing default allows native text dragging to work
if (!props.editEnabled) { if (!isFileDrag(event)) {
return return
} }
if (!isOverDropZone.value) { event.preventDefault()
resetDragState() isDragOverEditor.value = eventTargetsEditor(event)
return }
}
isDragOverEditor.value = eventTargetsEditor(event) function handleDragLeave(event: DragEvent) {
}, if (!isFileDrag(event)) {
onDrop(files, event) { return
if (!props.editEnabled) { }
return
}
const dropOverEditor = eventTargetsEditor(event) dragDepth.value--
if (dragDepth.value === 0) {
resetDragState() resetDragState()
} else {
isDragOverEditor.value = eventTargetsEditor(event)
}
}
// Ignore drops over editor - let TipTap handle them function handleDrop(event: DragEvent) {
if (dropOverEditor || !files || files.length === 0) { // Only handle file drops - let text drops work natively
return if (!isFileDrag(event)) {
} return
}
uploadFilesToTask(files) event.preventDefault()
}, dragDepth.value = 0
const dropOverEditor = eventTargetsEditor(event)
resetDragState()
// Ignore drops over editor - let TipTap handle them
const files = event.dataTransfer?.files
if (dropOverEditor || !files || files.length === 0) {
return
}
uploadFilesToTask(files)
}
function addDragListeners() {
document.addEventListener('dragenter', handleDragEnter)
document.addEventListener('dragover', handleDragOver)
document.addEventListener('dragleave', handleDragLeave)
document.addEventListener('drop', handleDrop)
}
function removeDragListeners() {
document.removeEventListener('dragenter', handleDragEnter)
document.removeEventListener('dragover', handleDragOver)
document.removeEventListener('dragleave', handleDragLeave)
document.removeEventListener('drop', handleDrop)
}
onMounted(() => {
if (props.editEnabled) {
addDragListeners()
}
})
onUnmounted(() => {
removeDragListeners()
})
watch(() => props.editEnabled, (enabled) => {
if (enabled) {
addDragListeners()
} else {
removeDragListeners()
resetDragState()
}
}) })
const showDropzone = computed(() => const showDropzone = computed(() =>
props.editEnabled && isDraggingFiles.value && !isDragOverEditor.value, props.editEnabled && isDraggingFiles.value && !isDragOverEditor.value,
) )
watch(() => props.editEnabled, enabled => {
if (!enabled) {
resetDragState()
}
})
function downloadAttachment(attachment: IAttachment) { function downloadAttachment(attachment: IAttachment) {
attachmentService.download(attachment) attachmentService.download(attachment)
} }