@@ -33,6 +33,9 @@ import {
3333 useState ,
3434} from "react" ;
3535import { clsx } from "@heroui/shared-utils" ;
36+ import { useTranslation } from "react-i18next" ;
37+
38+ import { PasteIcon } from "../icons" ;
3639
3740import { UseFileUploadProps , useFileUpload } from "./use-file-upload" ;
3841import FileUploadItem from "./file-upload-item" ;
@@ -61,6 +64,9 @@ const FileUpload = forwardRef<"div", FileUploadProps>((props, ref) => {
6164 fileItemElement,
6265 topbar,
6366 onChange,
67+ showPasteButton = false ,
68+ pasteButtonText = "Paste" ,
69+ pasteButton,
6470 ...otherProps
6571 } = useFileUpload ( { ...props , ref } ) ;
6672
@@ -69,6 +75,8 @@ const FileUpload = forwardRef<"div", FileUploadProps>((props, ref) => {
6975 const [ files , setFiles ] = useState < File [ ] > ( initialFiles ?? [ ] ) ;
7076 const [ isDragging , setIsDragging ] = useState ( false ) ;
7177
78+ const { t } = useTranslation ( ) ;
79+
7280 useEffect ( ( ) => {
7381 initialFiles && setFiles ( initialFiles ) ;
7482 } , [ initialFiles ] ) ;
@@ -177,6 +185,7 @@ const FileUpload = forwardRef<"div", FileUploadProps>((props, ref) => {
177185 const acceptableFiles = filterAcceptableFiles ( droppedFiles ) ;
178186
179187 if ( acceptableFiles . length === 0 ) {
188+ // eslint-disable-next-line no-console
180189 console . warn ( "No acceptable file types were dropped" ) ;
181190
182191 return ;
@@ -194,6 +203,176 @@ const FileUpload = forwardRef<"div", FileUploadProps>((props, ref) => {
194203 [ props . isDisabled , multiple , files , filterAcceptableFiles , updateFiles ] ,
195204 ) ;
196205
206+ // Add new state to track paste errors
207+ const [ pasteError , setPasteError ] = useState < string | null > ( null ) ;
208+
209+ // Function to convert clipboard items to files
210+ const clipboardItemToFile = useCallback (
211+ async ( item : ClipboardItem ) : Promise < File | null > => {
212+ // Get all types from the clipboard item
213+ const types = item . types ;
214+
215+ // If accept prop exists, check if the item type is acceptable
216+ if ( accept ) {
217+ const acceptableTypes = accept . split ( "," ) . map ( ( type ) => type . trim ( ) ) ;
218+
219+ // Find a matching type
220+ const matchingType = types . find ( ( type ) => {
221+ // Check for direct match
222+ if ( acceptableTypes . includes ( type ) ) return true ;
223+
224+ // Check for wildcard matches (e.g., "image/*")
225+ return acceptableTypes . some ( ( acceptType ) => {
226+ if ( acceptType . endsWith ( "/*" ) ) {
227+ const category = acceptType . split ( "/" ) [ 0 ] ;
228+
229+ return type . startsWith ( `${ category } /` ) ;
230+ }
231+
232+ return false ;
233+ } ) ;
234+ } ) ;
235+
236+ if ( ! matchingType ) {
237+ // No acceptable types found
238+ return null ;
239+ }
240+
241+ // Try to get the blob for the matching type
242+ try {
243+ const blob = await item . getType ( matchingType ) ;
244+
245+ // Create a file from the blob
246+ const fileName = `pasted-${ new Date ( ) . getTime ( ) } .${ getExtensionFromMimeType ( matchingType ) } ` ;
247+
248+ return new File ( [ blob ] , fileName , { type : matchingType } ) ;
249+ } catch ( error ) {
250+ // eslint-disable-next-line no-console
251+ console . error ( "Error getting clipboard item:" , error ) ;
252+
253+ return null ;
254+ }
255+ } else {
256+ // If no accept prop, try to get the first type
257+ try {
258+ const firstType = types [ 0 ] ;
259+ const blob = await item . getType ( firstType ) ;
260+
261+ // Create a file from the blob
262+ const fileName = `pasted-${ new Date ( ) . getTime ( ) } .${ getExtensionFromMimeType ( firstType ) } ` ;
263+
264+ return new File ( [ blob ] , fileName , { type : firstType } ) ;
265+ } catch ( error ) {
266+ // eslint-disable-next-line no-console
267+ console . error ( "Error getting clipboard item:" , error ) ;
268+
269+ return null ;
270+ }
271+ }
272+ } ,
273+ [ accept ] ,
274+ ) ;
275+
276+ // Helper function to get file extension from MIME type
277+ const getExtensionFromMimeType = ( mimeType : string ) : string => {
278+ const mimeToExt : Record < string , string > = {
279+ "image/png" : "png" ,
280+ "image/jpeg" : "jpg" ,
281+ "image/jpg" : "jpg" ,
282+ "image/gif" : "gif" ,
283+ "image/webp" : "webp" ,
284+ "image/svg+xml" : "svg" ,
285+ "text/plain" : "txt" ,
286+ "application/pdf" : "pdf" ,
287+ "application/json" : "json" ,
288+ "application/xml" : "xml" ,
289+ "text/html" : "html" ,
290+ "text/csv" : "csv" ,
291+ } ;
292+
293+ return mimeToExt [ mimeType ] || "bin" ;
294+ } ;
295+
296+ // Function to handle pasting from clipboard
297+ const handlePaste = useCallback ( async ( ) => {
298+ try {
299+ setPasteError ( null ) ;
300+
301+ // Check if clipboard API is available
302+ if ( ! navigator . clipboard || ! navigator . clipboard . read ) {
303+ setPasteError ( t ( "clipboard-api-not-supported-in-this-browser" ) ) ;
304+
305+ return ;
306+ }
307+
308+ // Read clipboard data
309+ const clipboardItems = await navigator . clipboard . read ( ) ;
310+
311+ if ( clipboardItems . length === 0 ) {
312+ setPasteError ( t ( "clipboard-is-empty" ) ) ;
313+
314+ return ;
315+ }
316+
317+ // Convert clipboard items to files
318+ const newFiles : File [ ] = [ ] ;
319+
320+ for ( const item of clipboardItems ) {
321+ const file = await clipboardItemToFile ( item ) ;
322+
323+ if ( file ) {
324+ newFiles . push ( file ) ;
325+ }
326+ }
327+
328+ if ( newFiles . length === 0 ) {
329+ setPasteError (
330+ t ( "no-acceptable-content-found-in-clipboard" , {
331+ acceptableTypes : accept ? accept : "" ,
332+ } ) ,
333+ ) ;
334+
335+ return ;
336+ }
337+
338+ // Update files based on multiple flag
339+ if ( multiple ) {
340+ updateFiles ( [ ...files , ...newFiles ] ) ;
341+ } else {
342+ // If not multiple, just use the first file
343+ updateFiles ( [ newFiles [ 0 ] ] ) ;
344+ }
345+ } catch ( error ) {
346+ // eslint-disable-next-line no-console
347+ console . error ( "Error pasting from clipboard:" , error ) ;
348+ setPasteError ( t ( "failed-to-paste-from-clipboard" ) ) ;
349+ }
350+ } , [ multiple , files , clipboardItemToFile , updateFiles , accept ] ) ;
351+
352+ // Create paste button element
353+ const pasteButtonElement = useMemo (
354+ ( ) =>
355+ pasteButton ? (
356+ cloneElement ( pasteButton , {
357+ disabled : props . isDisabled ,
358+ onPress : ( ev ) => {
359+ handlePaste ( ) ;
360+ pasteButton . props . onPress ?.( ev ) ;
361+ } ,
362+ } )
363+ ) : (
364+ < Button
365+ color = "secondary"
366+ disabled = { props . isDisabled }
367+ startContent = { < PasteIcon /> }
368+ onPress = { handlePaste }
369+ >
370+ { pasteButtonText }
371+ </ Button >
372+ ) ,
373+ [ pasteButton , pasteButtonText , handlePaste , props . isDisabled ] ,
374+ ) ;
375+
197376 const topbarElement = useMemo ( ( ) => {
198377 if ( topbar ) {
199378 return cloneElement ( topbar , {
@@ -308,7 +487,7 @@ const FileUpload = forwardRef<"div", FileUploadProps>((props, ref) => {
308487 } )
309488 ) : (
310489 < Button
311- color = "warning "
490+ color = "primary "
312491 disabled = { props . isDisabled }
313492 onPress = { ( ) => {
314493 onReset ( ) ;
@@ -331,6 +510,7 @@ const FileUpload = forwardRef<"div", FileUploadProps>((props, ref) => {
331510 { multiple && files . length !== 0 && addButtonElement }
332511 { files . length !== 0 && resetButtonElement }
333512 { browseButtonElement }
513+ { showPasteButton && pasteButtonElement }
334514 { uploadButtonElement }
335515 </ div >
336516 ) ;
@@ -354,6 +534,8 @@ const FileUpload = forwardRef<"div", FileUploadProps>((props, ref) => {
354534 addButtonElement ,
355535 resetButtonElement ,
356536 uploadButton ,
537+ showPasteButton ,
538+ pasteButtonElement ,
357539 ] ) ;
358540
359541 // Add dragOver styles to the base styles
@@ -418,6 +600,12 @@ const FileUpload = forwardRef<"div", FileUploadProps>((props, ref) => {
418600
419601 { topbarElement }
420602
603+ { pasteError && (
604+ < div className = "text-danger text-sm p-2 mt-1 bg-danger-50 rounded" >
605+ { pasteError }
606+ </ div >
607+ ) }
608+
421609 { isDragging && (
422610 < div className = "absolute inset-0 flex items-center justify-center bg-primary-50 bg-opacity-80 text-primary-600 text-xl font-medium rounded-lg z-10" >
423611 { dragDropZoneText }
0 commit comments