antdmobxdragger

Antd Dragger with paste image from clipboard


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 enter image description here

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 "";
    }
}

Solution

  • 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