mirror of
https://github.com/go-vikunja/vikunja.git
synced 2025-12-05 19:16:51 -06:00
157 lines
4.7 KiB
TypeScript
157 lines
4.7 KiB
TypeScript
import type { MaybeRef, MaybeRefOrGetter, ShallowRef } from 'vue'
|
|
import { isClient } from '@vueuse/shared'
|
|
import { shallowRef, unref } from 'vue'
|
|
|
|
import { useEventListener } from '@vueuse/core'
|
|
|
|
|
|
/////////
|
|
// Temporary copy until https://github.com/vueuse/vueuse/pull/5169 is merged and released
|
|
////////
|
|
|
|
/* eslint-disable */
|
|
|
|
|
|
export interface UseDropZoneReturn {
|
|
files: ShallowRef<File[] | null>
|
|
isOverDropZone: ShallowRef<boolean>
|
|
}
|
|
|
|
export interface UseDropZoneOptions {
|
|
/**
|
|
* Allowed data types, if not set, all data types are allowed.
|
|
* Also can be a function to check the data types.
|
|
*/
|
|
dataTypes?: MaybeRef<readonly string[]> | ((types: readonly string[]) => boolean)
|
|
/**
|
|
* Similar to dataTypes, but exposes the DataTransferItemList for custom validation.
|
|
* If provided, this function takes precedence over dataTypes.
|
|
*/
|
|
checkValidity?: (items: DataTransferItemList) => boolean
|
|
onDrop?: (files: File[] | null, event: DragEvent) => void
|
|
onEnter?: (files: File[] | null, event: DragEvent) => void
|
|
onLeave?: (files: File[] | null, event: DragEvent) => void
|
|
onOver?: (files: File[] | null, event: DragEvent) => void
|
|
/**
|
|
* Allow multiple files to be dropped. Defaults to true.
|
|
*/
|
|
multiple?: boolean
|
|
/**
|
|
* Prevent default behavior for unhandled events. Defaults to false.
|
|
*/
|
|
preventDefaultForUnhandled?: boolean
|
|
}
|
|
|
|
export function useDropZone(
|
|
target: MaybeRefOrGetter<HTMLElement | Document | null | undefined>,
|
|
options: UseDropZoneOptions | UseDropZoneOptions['onDrop'] = {},
|
|
): UseDropZoneReturn {
|
|
const isOverDropZone = shallowRef(false)
|
|
const files = shallowRef<File[] | null>(null)
|
|
let counter = 0
|
|
let isValid = true
|
|
|
|
if (isClient) {
|
|
const _options = typeof options === 'function' ? { onDrop: options } : options
|
|
const multiple = _options.multiple ?? true
|
|
const preventDefaultForUnhandled = _options.preventDefaultForUnhandled ?? false
|
|
|
|
const getFiles = (event: DragEvent) => {
|
|
const list = Array.from(event.dataTransfer?.files ?? [])
|
|
return list.length === 0 ? null : (multiple ? list : [list[0]])
|
|
}
|
|
|
|
const checkDataTypes = (types: string[]) => {
|
|
const dataTypes = unref(_options.dataTypes)
|
|
|
|
if (typeof dataTypes === 'function')
|
|
return dataTypes(types)
|
|
|
|
if (!dataTypes?.length)
|
|
return true
|
|
|
|
if (types.length === 0)
|
|
return false
|
|
|
|
return types.every(type =>
|
|
dataTypes.some(allowedType => type.includes(allowedType)),
|
|
)
|
|
}
|
|
|
|
const checkValidity = (items: DataTransferItemList) => {
|
|
if (_options.checkValidity) {
|
|
return _options.checkValidity(items)
|
|
}
|
|
|
|
const types = Array.from(items ?? []).map(item => item.type)
|
|
|
|
const dataTypesValid = checkDataTypes(types)
|
|
const multipleFilesValid = multiple || items.length <= 1
|
|
|
|
return dataTypesValid && multipleFilesValid
|
|
}
|
|
|
|
const isSafari = () => (
|
|
/^(?:(?!chrome|android).)*safari/i.test(navigator.userAgent)
|
|
&& !('chrome' in window)
|
|
)
|
|
|
|
const handleDragEvent = (event: DragEvent, eventType: 'enter' | 'over' | 'leave' | 'drop') => {
|
|
const dataTransferItemList = event.dataTransfer?.items
|
|
isValid = (dataTransferItemList && checkValidity(dataTransferItemList)) ?? false
|
|
|
|
if (preventDefaultForUnhandled) {
|
|
event.preventDefault()
|
|
}
|
|
|
|
if (!isSafari() && !isValid) {
|
|
if (event.dataTransfer) {
|
|
event.dataTransfer.dropEffect = 'none'
|
|
}
|
|
return
|
|
}
|
|
|
|
event.preventDefault()
|
|
if (event.dataTransfer) {
|
|
event.dataTransfer.dropEffect = 'copy'
|
|
}
|
|
|
|
const currentFiles = getFiles(event)
|
|
|
|
switch (eventType) {
|
|
case 'enter':
|
|
counter += 1
|
|
isOverDropZone.value = true
|
|
_options.onEnter?.(null, event)
|
|
break
|
|
case 'over':
|
|
_options.onOver?.(null, event)
|
|
break
|
|
case 'leave':
|
|
counter -= 1
|
|
if (counter === 0)
|
|
isOverDropZone.value = false
|
|
_options.onLeave?.(null, event)
|
|
break
|
|
case 'drop':
|
|
counter = 0
|
|
isOverDropZone.value = false
|
|
if (isValid) {
|
|
files.value = currentFiles
|
|
_options.onDrop?.(currentFiles, event)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
useEventListener<DragEvent>(target, 'dragenter', event => handleDragEvent(event, 'enter'))
|
|
useEventListener<DragEvent>(target, 'dragover', event => handleDragEvent(event, 'over'))
|
|
useEventListener<DragEvent>(target, 'dragleave', event => handleDragEvent(event, 'leave'))
|
|
useEventListener<DragEvent>(target, 'drop', event => handleDragEvent(event, 'drop'))
|
|
}
|
|
|
|
return {
|
|
files,
|
|
isOverDropZone,
|
|
}
|
|
} |