I use Antd+Mobx and I want to modify Dragger so that it receives a file from the clipboard. Main panel has image paste handler that add new file to fileList in store. Finally, dragged files and files from dialog successfully loaded, but files from clipboard only add to fileList, not triggered for loaded (onChange not called). How I can trigger that for load to server?
This image handled from clipboard, but not sent to server
Main panel:
const MathNewTaskPanel: React.FC<NewTaskProps> = inject('newTaskProps')(observer((props: NewTaskProps) => {
...
const handlePaste = (event: ClipboardEvent<HTMLDivElement>) => {
if (event.clipboardData.files.length) {
const ff = event.clipboardData.files[0];
const file: UploadFile = {
name: ff.name,
size: ff.size,
type: ff.type,
uid: uuid(),
originFileObj: ff
};
//main panel has two draggers. Only one is active at once
photoUploaderStore.uploader.setFile(file);
solutionUploaderStore.uploader.setFile(file);
}
};
return (
<div onPaste={handlePaste}>
...
</div>
);
}));
Modified dragger:
const ImageUploader: React.FC<IProps> = inject('uploader')(observer((props: IProps) => {
const antUploaderProps = {
name: 'file',
multiple: true,
listType: 'picture' as UploadListType,
action: props.uploader.url,
progress: 'line' as ProgressProps,
fileList: props.uploader.files,
disabled: props.uploader.inactive,
onChange(info: UploadChangeParam<UploadFile<any>>) {
props.uploader.handleOnChange(info);
}
};
return (
<div>
<Dragger className={props.uploader.inactive ? "inactive-element" : ""} {...antUploaderProps}>
<p className="ant-upload-drag-icon"><InboxOutlined /></p>
</Dragger>
</div>
);
}));
Dragger store:
export default class ImageUploaderStore {
@observable
inactive: boolean;
@observable
taskId: number|null;
@observable
url: string;
@observable
files: UploadFile[] | [];
isMainPhoto: boolean;
subject: Subjects;
mathTaskService: MathTaskService;
constructor(subject: Subjects, isMainPhoto: boolean) {
this.inactive = true;
this.subject = subject;
this.taskId = null;
this.url = "";
this.mathTaskService = new MathTaskService();
this.isMainPhoto = isMainPhoto;
this.files = [];
}
@action
changeActivated(active: boolean) {
this.inactive = !active;
}
@action
setTaskId(taskId: number) {
this.taskId = taskId;
this.url = this.getUrl(taskId);
}
@action
setFile(newFile: UploadFile) {
if (!this.inactive) {
this.files = [...this.files, newFile];
}
}
@action
setFiles(newFiles: UploadFile[]) {
if (!this.inactive) {
this.files = newFiles;
}
}
@action
handleOnChange(info: UploadChangeParam<UploadFile<any>>) {
runInAction(() => {
const { status } = info.file;
if (status === 'done') {
message.success(`${info.file.name} is successful loaded`);
} else if (status === 'error') {
message.error(`${info.file.name} loading failed`);
}
this.files = info.fileList;
});
}
getUrl(taskId: number): string {
if (this.subject === Subjects.MATH) {
return this.isMainPhoto
? this.mathTaskService.getPhotoUploadUrl(taskId)
: this.mathTaskService.getSolutionPhotoUploadUrl(taskId);
}
return "";
}
}
I've met similar ussue while working with antd upload. After searching for the solution I've found this topic, however the solution they provided was not perfect. I've spend some time to make it better.
Here is my solution of this problem: In my solution I dispatch drop event in input of antd Upload
First of all we have to access to the hidden <input type="file" .../> of antd. They uses rc-upload, where uploader is an private method and they don't have any methods to get uploader. We can use "backdoor" of typescript to override it interface:
...
import { UploadRef } from 'antd/es/upload/Upload'
import RcUpload from 'rc-upload'
const { Dragger } = Upload
interface ExtendedUploadRef<T = any> extends Omit<UploadRef<T>, 'upload'> {
upload: Omit<RcUpload, 'uploader'> & {
uploader: any
}
}
const uploadRef = useRef<ExtendedUploadRef<any> | null>(null)
This ref should be passed to antd component but typescript will not like it, this why we'll pass it as React.RefObject<UploadRef<any>>
:
<Dragger
className="chat_file_upload_dragger"
{...draggerProps}
ref={uploadRef as React.RefObject<UploadRef<any>>}
>
<div className="chat_file_upload_middle">
<p className="ant-upload-drag-icon">
<FaFileUpload />
</p>
<p className="ant-upload-text">
{t('Click or drag file to this area to upload')}
</p>
</div>
</Dragger>
now we can go to the most interesting part, first of all we have to handle past event, to do so I used my reusable custom hook declared somewhere in project:
import { useEffect, useRef } from "react"
export default function useEventListener(
eventType: any,
callback: any,
element = window
) {
const callbackRef = useRef(callback)
useEffect(() => {
callbackRef.current = callback
}, [callback])
useEffect(() => {
if (element == null) return
const handler = (e: any) => callbackRef.current(e)
element.addEventListener(eventType, handler)
return () => element.removeEventListener(eventType, handler)
}, [eventType, element])
}
Then we tell our component with antd Upload to listen to this event:
useEventListener('paste', handlePasteFiles)
All the magic happening in handlePasteFiles function which looks like:
function handlePasteFiles(e: ClipboardEvent) {
const items = e.clipboardData?.items
if (!items) return
const arrItems = Array.from(items)
if (arrItems.every((item) => item.kind !== 'file')) return
e.preventDefault() //to not paste file path if focused in some input text
const fileList = new DataTransfer()
arrItems.forEach((item) => {
const file = item.getAsFile()
if (!file) return
fileList.items.add(file)
})
if (fileList.items.length > 0) {
const dropEvent = new DragEvent('drop', {
dataTransfer: fileList,
bubbles: true,
cancelable: true,
})
uploadRef.current?.upload?.uploader.fileInput.dispatchEvent(dropEvent)
}
}
So, antd does everything with files pasted from clipboard as it was dragged and dropped to it's area