UNPKG

solidjs-dropzone

Version:

Dropzone Adapter For SOlidJS

509 lines (504 loc) 16.2 kB
'use strict'; 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); }