import React, {useCallback, useEffect, useId, useMemo, useRef, useState} from 'react'; import renderHtmlAttributes from "../../utils/renderHtmlAttributes"; import useFormSubmitSuccess from "../../hooks/useFormSubmitSuccess"; import type {FileRejection} from "react-dropzone"; import {useDropzone} from "react-dropzone"; import convertMBToBytes from "../../utils/convertMBToBytes"; import fetcher from "../../utils/fetcher"; import {filesize} from "filesize"; import uuid from "../../utils/uuid"; import {Icon} from "@wordpress/components"; import {closeSmall} from "@wordpress/icons"; import {__, _n, sprintf} from "@wordpress/i18n"; import getFileExtension from "../../utils/getFileExtension"; import UpgradeOverlay from "../../components/UpgradeOverlay"; import uniqId from '../../utils/uniqid'; import {type AjaxEventDetail, setupAjaxInterceptor} from '../../utils/ajaxComplete'; import type {FreemiusProps} from "../../types/freemius"; type FieldProps = { itemId: string formId: string attrs: Record attachmentType: 'link' | 'attach' | 'both' fileSizes: number fileTypes: { [key: string]: readonly string[] } fileTypesRaw: string allowMultipleUpload: boolean maxFiles: number generic: string custom_id: string text: string restUrl: string nonce: string GROUP: string } & FreemiusProps type AcceptedFilesProps = { file: File; uuid: string; progress: string; error: string; isPaused: boolean; preview: any; } function isImage(file: any) { const isMimeTypeImage = file.type.startsWith('image/'); const isExtensionImage = /\.(jpg|jpeg|png|gif|bmp|webp|svg)$/i.test(file.name); return isMimeTypeImage && isExtensionImage; } const Field = (props: FieldProps) => { const containerRef = useRef(null); const [acceptedFiles, setAcceptedFiles] = useState([]); const [rejectedFiles, setRejectedFiles] = useState([]); const attrs = useMemo(() => renderHtmlAttributes(props.attrs), [props.attrs]); async function onDropAccepted(files: T[]) { if (!props.freemius.can_use_premium_code) { return; } const newFiles = files.map((file) => ({ file, uuid: uuid(), progress: '0', error: '', isPaused: false, preview: isImage(file) ? URL.createObjectURL(file) : null, })); if (acceptedFiles.length >= props.maxFiles) { const container = containerRef.current as HTMLDivElement; const messageElement = container.parentElement?.querySelector('.elementor-message'); /* translators: %d: number of files */ const errorMessage = sprintf(_n('You can upload only %d file.', 'You can upload up to %d files.', props.maxFiles, 'advanced-fields-for-elementor-forms'), props.maxFiles); if (messageElement) { messageElement.textContent = errorMessage; } else { const messageCreate = document.createElement('span'); messageCreate.classList.add('elementor-message', 'elementor-message-danger'); messageCreate.textContent = errorMessage; container.parentElement?.appendChild(messageCreate); } } else { setAcceptedFiles(prevState => { return [...prevState, ...newFiles.slice(0, props.maxFiles)]; }); } } function onDropRejected(fileRejections: FileRejection[]) { const filteredFiles = fileRejections.filter(file => file.errors.length === 1 && file.errors.some(error => error.code === "too-many-files") ); if (filteredFiles.length > props.maxFiles) { const container = containerRef.current as HTMLDivElement; const messageElement = container.parentElement?.querySelector('.elementor-message'); const errorMessage = `${__('The maximum number of files is', 'advanced-fields-for-elementor-forms')} ${props.maxFiles}`; if (messageElement) { messageElement.textContent = errorMessage; } else { const messageCreate = document.createElement('span'); messageCreate.classList.add('elementor-message', 'elementor-message-danger'); messageCreate.textContent = errorMessage; container.parentElement?.appendChild(messageCreate); } } // Now filter out the filteredFiles from fileRejections const remainingFiles = fileRejections.filter(file => !filteredFiles.includes(file)); setRejectedFiles(prevState => [...prevState, ...remainingFiles]); } const maxSize = useMemo(() => convertMBToBytes(props.fileSizes), [props.fileSizes]); const {getRootProps, getInputProps} = useDropzone({ ...(props.fileTypes && {accept: props.fileTypes}), ...(props.maxFiles && {maxFiles: props.maxFiles}), ...(props.fileSizes && {maxSize}), multiple: props.allowMultipleUpload, onDropAccepted, onDropRejected, }); const genericIcon = useMemo(() => decodeURIComponent(props.generic), [props.generic]); const handleRemove = useCallback((e: React.MouseEvent, uuid3: string) => { e.preventDefault(); setAcceptedFiles(prevState => { return prevState.filter((file) => file.uuid !== uuid3); }); }, []); const handleProgress = useCallback((val: string, uuid1: string) => { setAcceptedFiles(prevState => prevState.map(value => { return value.uuid === uuid1 ? { ...value, progress: val } : value; })); }, []); const getName = useMemo(() => { if (props.allowMultipleUpload) { return `${attrs.name}[]`; } return attrs.name; }, [attrs.name, props.allowMultipleUpload]); const newAttrs = useMemo(() => { const {name, ...rest} = attrs; return rest; }, [attrs]); const mainId = useId(); function handleRemoveRejectedFiles(e: React.MouseEvent, index: number) { e.preventDefault(); setRejectedFiles(prevState => { return prevState.filter((_, index1) => index1 !== index); }); } // Reset after form submit useFormSubmitSuccess(containerRef, () => { setAcceptedFiles([]); setRejectedFiles([]); }, []); // On ajax complete useEffect(() => { const handleAjaxComplete = (detail: AjaxEventDetail) => { const data = JSON.parse(detail.response); // Remove uploaded item if file not exit if (data?.data?.data?.notExistFiles.length > 0) { const wrapperElement = containerRef.current; if (!wrapperElement) { return; } const notExistFiles: string[] = data?.data?.data?.notExistFiles; notExistFiles.forEach(tempName => { const element: HTMLButtonElement | null = wrapperElement.querySelector(`[data-temp-name="${tempName}"]`); element?.click(); }); } }; // Set up the interceptor setupAjaxInterceptor(handleAjaxComplete); }, []); return ( <> {!props.freemius.can_use_premium_code && }

{props.text}

{acceptedFiles.length > 0 && (
    {acceptedFiles.map((item) => (
  • handleProgress(val, item.uuid)} onRemove={(e: any) => handleRemove(e, item.uuid)} getName={getName} restUrl={props.restUrl} GROUP={props.GROUP} />
  • ))}
)} {rejectedFiles.length > 0 && (
    {rejectedFiles.map((item, index) => (
  • handleRemoveRejectedFiles(e, index)} maxSize={maxSize} />
  • ))}
)}
); }; function FileUpload(item: any) { const getFileName = useCallback((name: string) => getFileExtension(name), []); const [pausedCallback, setPausedCallback] = useState<() => void>(() => { }); const [removeCallback, setRemoveCallback] = useState<() => void>(() => { }); const [error, setError] = useState(''); const [value, setValue] = useState(''); const [tempName, setTempName] = useState(''); const uploadRef = useRef(null); async function handleUpload(e: React.MouseEvent) { e.preventDefault(); setError(''); try { const chunkSize = 2 * 1024 * 1024; // 2MB chunks const totalChunks = Math.ceil(item.file.size / chunkSize); let chunkIndex = 0; let isPaused = false; let controller: AbortController; const _uniqId = uniqId(); async function uploadNextChunk() { const start = chunkIndex * chunkSize; const end = Math.min(start + chunkSize, item.file.size); const chunk = item.file.slice(start, end); const formData = new FormData(); formData.append('file_name', item.file.name); formData.append('uniq_id', _uniqId); formData.append('chunk_index', chunkIndex.toString()); formData.append('total_chunks', totalChunks.toString()); formData.append('file_chunk', chunk); // Create a new controller for this request controller = new AbortController(); const res: { message: string tempName: string name: string value: string } = await fetcher(`${item.restUrl}${item.GROUP}/v1/dropzone/upload`, { method: "POST", headers: {}, body: formData, signal: controller.signal, }); setRemoveCallback(() => () => { controller.abort(); item.onRemove(e); }); chunkIndex++; item.onProgress(((chunkIndex / totalChunks) * 100).toFixed(0)); if (chunkIndex < totalChunks) { setPausedCallback(() => () => isPaused = !isPaused); if (isPaused) { await new Promise(resolve => { const checkInterval = setInterval(() => { if (!isPaused) { clearInterval(checkInterval); resolve(); // Resume once isPaused becomes false } }, 400); // Check every 400ms }); } await uploadNextChunk(); } else { setValue(res.value); setTempName(res.tempName); } } await uploadNextChunk(); } catch (err: any) { setError(err?.message || JSON.stringify(e)); setRemoveCallback(() => () => { item.onRemove(e); }); } } const isMount = useRef(false); useEffect(() => { if (!uploadRef.current) { return; } if (isMount.current) { return; } isMount.current = true; uploadRef.current.click(); }, []); return ( <>
{item.preview ? (
{item.file.name} { URL.revokeObjectURL(item.preview) }} />
) : ( <>
{getFileName(item.file.name)}
)}
{item.file.name}
({filesize(item.file.size, {standard: "jedec"})})
{item.progress}%
{error &&
{error}
} ) } function FileRejected(item: any) { const getFileName = useCallback((name: string) => getFileExtension(name), []); const mainId = useId(); const getErrorMessage = useCallback((message: string) => { return message .replace(item.maxSize, filesize(item.maxSize, {standard: "jedec"})) .replace('bytes', ''); }, [item.maxSize]); const preview = useMemo(() => { return isImage(item.file) ? URL.createObjectURL(item.file) : null }, [item.file]); // const isTooManyFiles = useMemo(() => { // return !!(item.errors.length === 1 && item.errors.some((error: any) => error.code === "too-many-files")); // }, [item.errors]); // // if (isTooManyFiles) { // return __('The maximum number of files is 3', 'dropzone-field-for-elementor-form') // } return ( <>
{preview ? (
{item.file.name} { URL.revokeObjectURL(preview) }} />
) : ( <>
{getFileName(item.file.name)}
)}
{item.file.name}
({filesize(item.file.size, {standard: "jedec"})})
{item.errors.map((data: any, index: number) => (
{getErrorMessage(data.message)}
))} ) } export default Field;