solidjs-dropzone
Version:
Dropzone Adapter For SOlidJS
509 lines (504 loc) • 16.2 kB
JavaScript
;
var solidJs = require('solid-js');
var store = require('solid-js/store');
var fileSelector = require('file-selector');
var accepts = require('attr-accept');
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
var accepts__default = /*#__PURE__*/_interopDefault(accepts);
// src/index.tsx
var noop = () => {
};
var getInitialState = () => ({
isFocused: false,
isFileDialogActive: false,
isDragActive: false,
isDragAccept: false,
isDragReject: false,
acceptedFiles: [],
fileRejections: []
});
exports.createDropzone = (_props = {}) => {
const props = solidJs.mergeProps({
disabled: false,
getFilesFromEvent: fileSelector.fromEvent,
maxSize: Infinity,
minSize: 0,
multiple: true,
maxFiles: 0,
preventDropOnDocument: true,
noClick: false,
noKeyboard: false,
noDrag: false,
noDragEventsBubbling: false,
validator: null,
useFsAccessApi: false,
autoFocus: false,
accept: ""
}, _props);
const [state, setState] = store.createStore(getInitialState());
const acceptAttr = solidJs.createMemo(() => acceptPropAsAcceptAttr(props.accept));
const pickerTypes = solidJs.createMemo(() => props.useFsAccessApi ? pickerOptionsFromAccept(props.accept) : void 0);
const [rootRef, setRootRef] = solidJs.createSignal();
const [inputRef, setInputRef] = solidJs.createSignal();
let fsAccessApiWorks = typeof window !== "undefined" && canUseFileSystemAccessAPI();
const onWindowFocus = () => {
if (!fsAccessApiWorks && state.isFileDialogActive) {
setTimeout(() => {
const input = inputRef();
if (input) {
const {
files
} = input;
if (!files?.length) {
setState("isFileDialogActive", false);
props.onFileDialogCancel?.();
}
}
}, 300);
}
};
solidJs.createEffect(() => {
window.addEventListener("focus", onWindowFocus, false);
solidJs.onCleanup(() => {
window.removeEventListener("focus", onWindowFocus, false);
});
});
let dragTargets = [];
const onDocumentDrop = (event) => {
const root = rootRef();
if (root && root.contains(event.target)) {
return;
}
event.preventDefault();
dragTargets = [];
};
solidJs.createEffect(() => {
if (!props.preventDropOnDocument) {
document.addEventListener("dragover", onDocumentDragOver, false);
document.addEventListener("drop", onDocumentDrop, false);
solidJs.onCleanup(() => {
document.removeEventListener("dragover", onDocumentDragOver, false);
document.removeEventListener("drop", onDocumentDrop, false);
});
}
});
solidJs.createEffect(() => {
if (!props.disabled && props.autoFocus) {
rootRef()?.focus();
}
});
const onError = (error) => {
if (props.onError) {
props.onError(error);
} else {
if (solidJs.DEV) {
console.error(error);
}
}
};
const stopPropagation = (event) => {
if (props.noDragEventsBubbling) {
event.stopPropagation();
}
};
const onDragEnter = (event) => {
event.preventDefault();
stopPropagation(event);
dragTargets = [...dragTargets, event.target];
if (isEvtWithFiles(event)) {
Promise.resolve(props.getFilesFromEvent(event)).then((files) => {
if (isPropagationStopped(event) && !props.noDragEventsBubbling) {
return;
}
const fileCount = files.length;
const isDragAccept = fileCount > 0 && allFilesAccepted({
files: files.map((fileOrDataTransferItem) => fileOrDataTransferItem instanceof File ? fileOrDataTransferItem : (
// `file-selector`
fileOrDataTransferItem.getAsFile()
)).filter(Boolean),
accept: props.accept,
minSize: props.minSize,
maxSize: props.maxSize,
multiple: props.multiple,
maxFiles: props.maxFiles,
validator: props.validator
});
const isDragReject = fileCount > 0 && !isDragAccept;
setState({
isDragAccept,
isDragReject,
isDragActive: true
});
props.onDragEnter?.(event);
}).catch((e) => onError(e));
}
};
const onDragOver = (event) => {
event.preventDefault();
stopPropagation(event);
const hasFiles = isEvtWithFiles(event);
if (hasFiles && event.dataTransfer) {
try {
event.dataTransfer.dropEffect = "copy";
} catch {
}
}
if (hasFiles) {
props.onDragOver?.(event);
}
return false;
};
const onDragLeave = (event) => {
event.preventDefault();
stopPropagation(event);
const root = rootRef();
const targets = dragTargets.filter((target) => root?.contains(target));
const targetIdx = targets.indexOf(event.target);
if (targetIdx !== -1) {
targets.splice(targetIdx, 1);
}
dragTargets = targets;
if (targets.length > 0) {
return;
}
setState({
isDragActive: false,
isDragAccept: false,
isDragReject: false
});
if (isEvtWithFiles(event)) {
props.onDragLeave?.(event);
}
};
const setFiles = (files, event) => {
const acceptedFiles = [];
const fileRejections = [];
files.forEach((file) => {
const [accepted, acceptError] = fileAccepted(file, props.accept);
const [sizeMatch, sizeError] = fileMatchSize(file, props.minSize, props.maxSize);
const customErrors = props.validator ? props.validator(file) : null;
if (accepted && sizeMatch && !customErrors) {
acceptedFiles.push(file);
} else {
let errors = [acceptError, sizeError];
if (customErrors) {
errors = errors.concat(customErrors);
}
fileRejections.push({
file,
errors: errors.filter(Boolean)
});
}
});
if (!props.multiple && acceptedFiles.length > 1 || props.multiple && props.maxFiles >= 1 && acceptedFiles.length > props.maxFiles) {
acceptedFiles.forEach((file) => {
fileRejections.push({
file,
errors: [getTooManyFilesRejectionErr()]
});
});
acceptedFiles.splice(0);
}
setState({
acceptedFiles,
fileRejections
});
props.onDrop?.(acceptedFiles, fileRejections, event);
};
const onDrop = (event) => {
event.preventDefault();
stopPropagation(event);
dragTargets = [];
if (isEvtWithFiles(event)) {
Promise.resolve(props.getFilesFromEvent(event)).then((files) => {
if (isPropagationStopped(event) && !props.noDragEventsBubbling) {
return;
}
setFiles(files, event);
}).catch((e) => onError(e));
}
setState(getInitialState());
};
const openFileDialog = () => {
if (fsAccessApiWorks) {
setState("isFileDialogActive", true);
props.onFileDialogOpen?.();
const opts = {
multiple: props.multiple,
types: pickerTypes()
};
window.showOpenFilePicker(opts).then((handles) => props.getFilesFromEvent(handles)).then((files) => {
setFiles(files, null);
setState("isFileDialogActive", false);
}).catch((e) => {
if (isAbort(e)) {
props.onFileDialogCancel?.();
setState("isFileDialogActive", false);
} else if (isSecurityError(e)) {
fsAccessApiWorks = false;
const input2 = inputRef();
if (input2) {
input2.value = null;
input2.click();
} else {
onError(new Error("Cannot open the file picker because the https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API is not supported and no <input> was provided."));
}
} else {
onError(e);
}
});
return;
}
const input = inputRef();
if (input) {
setState("isFileDialogActive", true);
props.onFileDialogOpen?.();
input.value = null;
input.click();
}
};
const onKeydown = (event) => {
const root = rootRef();
if (!root || !root.isEqualNode(event.target)) {
return;
}
if (event.key === " " || event.key === "Enter" || event.keyCode === 32 || event.keyCode === 13) {
event.preventDefault();
openFileDialog();
}
};
const onFocus = () => setState("isFocused", true);
const onBlur = () => setState("isFocused", false);
const onClick = () => {
if (props.noClick) {
return;
}
if (isIeOrEdge()) {
setTimeout(openFileDialog, 0);
} else {
openFileDialog();
}
};
const composeHandler = (fn) => {
return props.disabled ? noop : fn;
};
const composeKeyboardHandler = (fn) => {
return props.noKeyboard ? noop : composeHandler(fn);
};
const composeDragHandler = (fn) => {
return props.noDrag ? noop : composeHandler(fn);
};
const getRootProps = (_overrides = {}) => {
const [overrides, rest] = solidJs.splitProps(solidJs.mergeProps({
refKey: "ref"
}, _overrides), ["onKeyDown", "onFocus", "onBlur", "onClick", "onDragEnter", "onDragOver", "onDragLeave", "onDrop", "role", "refKey"]);
return {
onKeyDown: composeKeyboardHandler(composeEventHandlers(overrides.onKeyDown, onKeydown)),
onFocus: composeKeyboardHandler(composeEventHandlers(overrides.onFocus, onFocus)),
onBlur: composeKeyboardHandler(composeEventHandlers(overrides.onBlur, onBlur)),
onClick: composeHandler(composeEventHandlers(overrides.onClick, onClick)),
onDragEnter: composeDragHandler(composeEventHandlers(overrides.onDragEnter, onDragEnter)),
onDragOver: composeDragHandler(composeEventHandlers(overrides.onDragOver, onDragOver)),
onDragLeave: composeDragHandler(composeEventHandlers(overrides.onDragLeave, onDragLeave)),
onDrop: composeDragHandler(composeEventHandlers(overrides.onDrop, onDrop)),
role: typeof overrides.role === "string" && overrides.role ? overrides.role : "presentation",
[overrides.refKey]: setRootRef,
...!props.disabled && !props.noKeyboard ? {
tabIndex: 0
} : {},
...rest
};
};
const onInputElementClick = (event) => {
event.stopPropagation();
};
const getInputProps = (_overrides = {}) => {
const [overrides, rest] = solidJs.splitProps(solidJs.mergeProps({
refKey: "ref"
}, _overrides), ["onChange", "onClick", "refKey"]);
return {
accept: acceptAttr(),
multiple: props.multiple,
type: "file",
style: {
display: "none"
},
onChange: composeHandler(composeEventHandlers(overrides.onChange, onDrop)),
onClick: composeHandler(composeEventHandlers(overrides.onClick, onInputElementClick)),
tabIndex: -1,
[overrides.refKey]: setInputRef,
...rest
};
};
return solidJs.mergeProps(state, {
get isFocused() {
return state.isFocused && !props.disabled;
},
getInputProps,
getRootProps,
rootRef: setRootRef,
inputRef: setInputRef,
open: composeHandler(openFileDialog)
});
};
var getInvalidTypeRejectionErr = (accept) => {
accept = Array.isArray(accept) && accept.length === 1 ? accept[0] : accept;
const messageSuffix = Array.isArray(accept) ? `one of ${accept.join(", ")}` : accept;
return {
code: "file-invalid-type",
message: `File type must be ${messageSuffix}`
};
};
var getTooLargeRejectionErr = (maxSize) => {
return {
code: "file-too-large",
message: `File is larger than ${maxSize} ${maxSize === 1 ? "byte" : "bytes"}`
};
};
var getTooSmallRejectionErr = (minSize) => {
return {
code: "file-too-small",
message: `File is smaller than ${minSize} ${minSize === 1 ? "byte" : "bytes"}`
};
};
var getTooManyFilesRejectionErr = () => {
return {
code: "too-many-files",
message: "Too many files"
};
};
function fileAccepted(file, accept) {
const acceptArray = getRawAcceptArray(accept);
const isAcceptable = file.type === "application/x-moz-file" || accepts__default.default(file, acceptArray.length ? acceptArray : "");
if (isAcceptable)
return [true, null];
return [false, getInvalidTypeRejectionErr(accept)];
}
function fileMatchSize(file, minSize, maxSize) {
if (isDefined(file.size)) {
if (isDefined(minSize) && isDefined(maxSize)) {
if (file.size > maxSize)
return [false, getTooLargeRejectionErr(maxSize)];
if (file.size < minSize)
return [false, getTooSmallRejectionErr(minSize)];
} else if (isDefined(minSize) && file.size < minSize)
return [false, getTooSmallRejectionErr(minSize)];
else if (isDefined(maxSize) && file.size > maxSize)
return [false, getTooLargeRejectionErr(maxSize)];
}
return [true, null];
}
function isDefined(value) {
return value !== void 0 && value !== null;
}
function allFilesAccepted({
files,
accept,
minSize,
maxSize,
multiple,
maxFiles,
validator
}) {
if (!multiple && files.length > 1 || multiple && maxFiles >= 1 && files.length > maxFiles) {
return false;
}
return files.every((file) => {
const [accepted] = fileAccepted(file, accept);
const [sizeMatch] = fileMatchSize(file, minSize, maxSize);
const customErrors = validator ? validator(file) : null;
return accepted && sizeMatch && !customErrors;
});
}
function isPropagationStopped(event) {
if (typeof event.cancelBubble !== "undefined") {
return event.cancelBubble;
}
return false;
}
function isEvtWithFiles(event) {
if (!("dataTransfer" in event) || !event.dataTransfer) {
return !!event.target && "files" in event.target && !!event.target.files;
}
return Array.prototype.some.call(event.dataTransfer.types, (type) => type === "Files" || type === "application/x-moz-file");
}
function onDocumentDragOver(event) {
event.preventDefault();
}
function isIe(userAgent) {
return userAgent.indexOf("MSIE") !== -1 || userAgent.indexOf("Trident/") !== -1;
}
function isEdge(userAgent) {
return userAgent.indexOf("Edge/") !== -1;
}
function isIeOrEdge(userAgent = window.navigator.userAgent) {
return isIe(userAgent) || isEdge(userAgent);
}
function composeEventHandlers(...fns) {
return (event) => {
fns.some((fn) => {
if (!isPropagationStopped(event) && fn) {
fn(event);
}
return isPropagationStopped(event);
});
};
}
function canUseFileSystemAccessAPI() {
return "showOpenFilePicker" in window;
}
function pickerOptionsFromAccept(accept) {
if (isDefined(accept)) {
const acceptForPicker = {};
Object.entries(accept).forEach(([mimeType, ext]) => {
if (!isMIMEType(mimeType)) {
if (solidJs.DEV) {
console.warn(`Skipped "${mimeType}" because it is not a valid MIME type. Check https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types for a list of valid MIME types.`);
}
return;
}
if (!Array.isArray(ext) || !ext.every(isExt)) {
if (solidJs.DEV) {
console.warn(`Skipped "${mimeType}" because an invalid file extension was provided.`);
}
return;
}
acceptForPicker[mimeType] = ext;
});
return [{
// description is required due to https://crbug.com/1264708
description: "Files",
accept: acceptForPicker
}];
}
return accept;
}
var getRawAcceptArray = (accept) => {
let array;
if (Array.isArray(accept)) {
array = accept;
} else if (typeof accept === "string") {
array = accept.split(",");
} else {
array = Object.entries(accept).flat(Infinity);
}
return array.filter(Boolean);
};
function acceptPropAsAcceptAttr(accept) {
if (isDefined(accept)) {
return getRawAcceptArray(accept).filter((v) => isMIMEType(v) || isExt(v)).join(",");
}
return void 0;
}
function isAbort(v) {
return v instanceof DOMException && (v.name === "AbortError" || v.code === v.ABORT_ERR);
}
function isSecurityError(v) {
return v instanceof DOMException && (v.name === "SecurityError" || v.code === v.SECURITY_ERR);
}
function isMIMEType(v) {
return v === "audio/*" || v === "video/*" || v === "image/*" || v === "text/*" || /\w+\/[-+.\w]+/g.test(v);
}
function isExt(v) {
return /^.*\.[\w]+$/.test(v);
}