Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ant design upload 上传图片支持粘贴上传 #2

Closed
PaulaSham opened this issue Aug 28, 2019 · 5 comments
Closed

ant design upload 上传图片支持粘贴上传 #2

PaulaSham opened this issue Aug 28, 2019 · 5 comments

Comments

@PaulaSham
Copy link
Owner

PaulaSham commented Aug 28, 2019

背景

本文为了解决ant design upload组件不提供粘贴方式上传图片而生。该组件只支持打开文件夹选取图片以及拖拽图片的方式上传图片,这两种方式可以适应大部分上传图片的场景。但是当用户不想保存图片到文件夹而且不方便拖拽图片的时候,通过复制-粘贴上传图片的便捷性就体现出来了。

经测试,复制/拷贝方式包括:

  • 截图
  • 从 finder 中拷贝(finder中的复制包括拷贝+粘贴两个动作)
  • 从微信聊天框中复制
  • 网络图片双击后点复制图片

局限性: 不能双击弹出粘贴选项(因为不是输入框),只能按command+v 触发图片上传。

效果图

image

步骤

1、监听paste事件

// 监听粘贴事件
window.addEventListener('paste', this.handlePaste)

2、获取粘贴板内容:e.clipboardData.items

在chrome 控制台把该对象打印出来,如下:

image

注意: 这是一个类数组对象!不是数组。不能用 mapforEach遍历,但是可以用for...of 遍历,因为其内部实现了iterator接口。如果该对象的kind是file,则把图片读出来。

代码如下:

let items = e.clipboardData.items
// 这是一个类数组对象!!!不是数组,不能用map和forEach遍历
// 可以用for...of 遍历是因为其内部实现了可迭代协议
for (let item of items) {
// 这一步很重要,因为直接把item打印出来是看不到具体内容的(如上图),需要遍历它
  if (item.kind === 'file') {
  var pasteFile = item.getAsFile()
  // 取得文件对象,一切好办
  }
}

3、把文件读出来后,上传至服务器

代码如下:

// formData格式
let formData = new FormData()
// 需要token
formData.append('token',  this.dataObj.token)
formData.append('file', pasteFile)
// 上传文件
axios({
  method: 'post',
  url: 'https://upload.qbox.me/',
  data: formData,
})

4、生成缩略图

上传成功后返回的key和hash(两者的值一样,用于图片唯一标识),图片地址格式:http://baseUrl/${key}
另外上传成功后可以生成缩略图(base64编码)显示在前端(需要借助原始file对象,不是图片url)

代码如下:

// 图片转换成base64编码作为缩略图
const reader = new FileReader()
// 监听图片转换完成
reader.addEventListener('load', function() {
    // ...接入antd upload 的 filelist 中
},false)   
reader.readAsDataURL(pasteFile)

5、接入antd upload组件中,使之和其他方式上传图片的效果一致

fileObject是一个存储图片详细信息的对象,下面是生成图片对象的方法,传入三个参数,如下:

  • file是原始图片文件对象
  • key是上传成功后返回的值,用以唯一标识图片
  • thumbUrl是图片的base64编码 。
    为什么是这个格式?因为这是antd upload 组件fileList数组中对象的格式,fileList是上传的图片列表(数组)。可以在onchange发生后打印出来看看

代码如下:

createFileObject(file, key, thumbUrl) {
  const fileObject = {
      lastModified: file.lastModified,
      lastModifiedDate: file.lastModifiedDate,
      name: file.name,
      size: file.size,
      type: file.type,
      uid: key,
      originFileObj: file,
      percent: 100,
      key: key,
      response: { hash: key, key: key },
      status: 'done',
      thumbUrl: thumbUrl,
  }
  return fileObject
}

将此对象push进upload组件的fileList数组后就可以接入ant design upload中

代码如下:

// 伪代码 
fileList.push(fileObject)
@ldc-37
Copy link

ldc-37 commented Feb 21, 2022

两年后可以做一些补充。上文提到「局限性: 不能双击弹出粘贴选项(因为不是输入框),只能按command+v 触发图片上传。」,这种情况现在有一些变化,借助浏览器较新的 navigator.clipboard.read() 方法,可以做到读取用户剪切板中 部分形式(见 限制) 的图片。

参考用法

const handleClickPaste = async () => {
    try {
      const read = await (navigator.clipboard as any).read(); // 此 API 较新,类型不完善(typescript4.4- 无此方法)
      const foundImageMimeType = read[0].types.find((val: string) => val.startsWith('image/png'));
      if (foundImageMimeType) {
        const blob = await read[0].getType(foundImageMimeType);
        // 截止 Chrome98 / Safari15,仍然不支持本地文件上传
        const file = new File([blob], 'screenshot.png', {
          type: foundImageMimeType,
        });
        manuallyUpload(file); // => 上传文件并添加到 fileList
      } else {
        message.info('剪切板中没有 png 类型图片');
      }
    } catch (e) {
      const err = e as Error;
      let errTips = '';
      if (err.name === 'NotAllowedError') {
        errTips = '您没有授权使用剪切板,Chrome 用户请刷新按钮右边的「锁」形 icon 打开权限';
      } else if (err.name === 'DataError') {
        errTips = '不支持读取剪切板中的图片文件,请按 Ctrl+V';
      } else {
        errTips = '浏览器可能不支持读取剪切板,请手动上传';
      }
      message.error(`${errTips} (${err.message})`, 5);
    }
  };
<p>
  <span>同时支持拖拽文件到虚线框内,或者在页面上按 Ctrl+V</span>
  {(navigator?.clipboard as any)?.read && (
    <>
      <span>,也可以 </span>
      <span onClick={handleClickPaste}>
        从剪切板上传截图
      </span>
      <Tooltip color="gray" title="首次使用时,需要点击同意授权。仅支持微信等工具的截图!">
        <QuestionCircleOutlined />
      </Tooltip>
    </>
  )}
</p>

👆🏻 当然这里也可以用自定义 ContextMenu 组件(右键菜单)。

API 限制

  • 对于 Chrome 浏览器,read 方法需要用户授权(会自动弹出),也可以手动用 navigator.permissions.query({name: 'clipboard-read'}) 申请权限。

image

  • 根据 Caniuseread 方法自 Chrome76 起才支持读取 MIME 类型为 image/png 的图片,至今(Chrome98)仍然不支持读取文件。换句话说,此方法只能读取截图工具采集的截图。
  • Safari 似乎同样只支持截图,它不需要用户授权,但是会弹出一个如下图的 contextMenu:

image

  • Firefox 大概是考虑到隐私问题,虽然早早就有了这个能力, 但至今(Firefox97)仍要打开相应的 flag 才能调用该方法。值得一提的是,clipboard.readText() 等方法也没有直接提供。

总结

虽然勉强算是能用,但是吧……永远不要相信用户对报错的理解能力。与其冒这个风险,拖拽上传 / Ctrl+V 不香吗。
因此个人认为除非是个人或对内的中小型项目,或是纯粹想要炫技,否则 不建议 使用这些实验性质的 API。

@quanbisen
Copy link

您好,我想请问一下,ant-design的Upload,这种方式的粘贴上次似乎还无法做到Upload组件显示上传进度。感觉需要Upload组件提供一个调用上传的api才可以。

@Mrcxt
Copy link

Mrcxt commented Nov 7, 2023

你这个实现跟ant design一点关系也没有啊,这样自定义上传就无法触发上传进度等交互行为,和点击上传的表现行为不一致

@BogdanGorelkin
Copy link

BogdanGorelkin commented Feb 2, 2024

Sorry guys, I don't speak chainese, but I do speak typescript :)

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)
  }
}

I think my solution is better, because you can avoid using function createFileObject and don't create antd file object manually.
Also it is better then solution of @PaulaSham , because it can handle uploading process if action has been passed as props to antd Dragger.

@ifyour
Copy link

ifyour commented Apr 28, 2024

@BogdanGorelkin Wow, that's so cool, it works!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants