@sv-use/core
Version:
A collection of Svelte 5 utilities.
119 lines (118 loc) • 4.33 kB
JavaScript
import { handleEventListener } from '../handle-event-listener/index.svelte.js';
import { noop, normalizeValue, toArray } from '../__internal__/utils.svelte.js';
import { untrack } from 'svelte';
/**
* Creates a zone where files can be dropped.
* @param target The element that acts as the drop zone.
* @param options Additional options to customize the behavior.
* @see https://svelte-librarian.github.io/sv-use/docs/core/create-drop-zone
*/
export function createDropZone(target, options = {}) {
const { allowedDataTypes = '*', multiple = true, preventDefaultForUnhandled = false, onDrop = noop, onEnter = noop, onLeave = noop, onOver = noop } = options;
let cleanups = [];
let counter = 0;
let isValid = true;
const _target = $derived(normalizeValue(target));
let isOver = $state(false);
let files = $state(null);
$effect(() => {
if (_target) {
untrack(() => {
cleanups.push(handleEventListener(_target, 'dragenter', (event) => handleDragEvent(event, 'enter')), handleEventListener(_target, 'dragover', (event) => handleDragEvent(event, 'over')), handleEventListener(_target, 'dragleave', (event) => handleDragEvent(event, 'leave')), handleEventListener(_target, 'drop', (event) => handleDragEvent(event, 'drop')));
});
}
return () => {
cleanup();
cleanups = [];
};
});
function getFiles(event) {
const list = Array.from(event.dataTransfer?.files ?? []);
return list.length === 0 ? null : multiple ? list : [list[0]];
}
function checkDataTypes(types) {
if (types.length === 0)
return false;
if (allowedDataTypes instanceof Function && allowedDataTypes.length > 0) {
return allowedDataTypes(types);
}
if (allowedDataTypes === '*')
return true;
const _allowedTypes = allowedDataTypes;
return types.every((type) => {
return toArray(normalizeValue(_allowedTypes)).some((allowedType) => {
if (allowedType.split('/')[1] === '*') {
return type.startsWith(allowedType.split('/')[0]);
}
return type === allowedType;
});
});
}
function checkValidity(items) {
const types = Array.from(items ?? []).map((item) => item.type);
const dataTypesValid = checkDataTypes(types);
const multipleFilesValid = multiple || items.length <= 1;
return dataTypesValid && multipleFilesValid;
}
function isSafari() {
return /^(?:(?!chrome|android).)*safari/i.test(navigator.userAgent) && !('chrome' in window);
}
function handleDragEvent(event, eventType) {
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;
isOver = true;
onEnter(event);
break;
case 'over':
onOver(event);
break;
case 'leave':
counter -= 1;
if (counter === 0) {
isOver = false;
}
onLeave(event);
break;
case 'drop':
counter = 0;
isOver = false;
if (isValid) {
files = currentFiles;
onDrop(currentFiles, event);
}
break;
}
}
function cleanup() {
cleanups.map((fn) => fn());
}
return {
get files() {
return files;
},
set files(v) {
files = v;
},
get isOver() {
return isOver;
},
cleanup
};
}