index.ts 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. import type { Body, Meta } from '@uppy/utils/lib/UppyFile'
  2. import type { Uppy } from '@uppy/core/lib/Uppy.js'
  3. import type { DefinePluginOpts, PluginOpts } from '@uppy/core/lib/BasePlugin.js'
  4. import BasePlugin from '@uppy/core/lib/BasePlugin.js'
  5. import getDroppedFiles from '@uppy/utils/lib/getDroppedFiles'
  6. import toArray from '@uppy/utils/lib/toArray'
  7. // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  8. // @ts-ignore We don't want TS to generate types for the package.json
  9. import packageJson from '../package.json'
  10. export interface DropTargetOptions extends PluginOpts {
  11. target?: HTMLElement | string | null
  12. onDrop?: (event: DragEvent) => void
  13. onDragOver?: (event: DragEvent) => void
  14. onDragLeave?: (event: DragEvent) => void
  15. }
  16. // Default options
  17. const defaultOpts = {
  18. target: null,
  19. } satisfies DropTargetOptions
  20. interface DragEventWithFileTransfer extends DragEvent {
  21. dataTransfer: NonNullable<DragEvent['dataTransfer']>
  22. }
  23. function isFileTransfer(event: DragEvent): event is DragEventWithFileTransfer {
  24. return event.dataTransfer?.types?.some((type) => type === 'Files') ?? false
  25. }
  26. /**
  27. * Drop Target plugin
  28. *
  29. */
  30. export default class DropTarget<
  31. M extends Meta,
  32. B extends Body,
  33. > extends BasePlugin<
  34. DefinePluginOpts<DropTargetOptions, keyof typeof defaultOpts>,
  35. M,
  36. B
  37. > {
  38. static VERSION = packageJson.version
  39. private nodes?: Array<HTMLElement>
  40. constructor(uppy: Uppy<M, B>, opts?: DropTargetOptions) {
  41. super(uppy, { ...defaultOpts, ...opts })
  42. this.type = 'acquirer'
  43. this.id = this.opts.id || 'DropTarget'
  44. }
  45. addFiles = (files: Array<File>): void => {
  46. const descriptors = files.map((file) => ({
  47. source: this.id,
  48. name: file.name,
  49. type: file.type,
  50. data: file,
  51. meta: {
  52. // path of the file relative to the ancestor directory the user selected.
  53. // e.g. 'docs/Old Prague/airbnb.pdf'
  54. relativePath: (file as any).relativePath || null,
  55. } as any,
  56. }))
  57. try {
  58. this.uppy.addFiles(descriptors)
  59. } catch (err) {
  60. this.uppy.log(err)
  61. }
  62. }
  63. handleDrop = async (event: DragEvent): Promise<void> => {
  64. if (!isFileTransfer(event)) {
  65. return
  66. }
  67. event.preventDefault()
  68. event.stopPropagation()
  69. // Remove dragover class
  70. ;(event.currentTarget as HTMLElement)?.classList.remove('uppy-is-drag-over')
  71. this.setPluginState({ isDraggingOver: false })
  72. // Let any acquirer plugin (Url/Webcam/etc.) handle drops to the root
  73. this.uppy.iteratePlugins((plugin) => {
  74. if (plugin.type === 'acquirer') {
  75. // @ts-expect-error Every Plugin with .type acquirer can define handleRootDrop(event)
  76. plugin.handleRootDrop?.(event)
  77. }
  78. })
  79. // Add all dropped files, handle errors
  80. let executedDropErrorOnce = false
  81. const logDropError = (error: Error): void => {
  82. this.uppy.log(error, 'error')
  83. // In practice all drop errors are most likely the same,
  84. // so let's just show one to avoid overwhelming the user
  85. if (!executedDropErrorOnce) {
  86. this.uppy.info(error.message, 'error')
  87. executedDropErrorOnce = true
  88. }
  89. }
  90. const files = await getDroppedFiles(event.dataTransfer, { logDropError })
  91. if (files.length > 0) {
  92. this.uppy.log('[DropTarget] Files were dropped')
  93. this.addFiles(files)
  94. }
  95. this.opts.onDrop?.(event)
  96. }
  97. handleDragOver = (event: DragEvent): void => {
  98. if (!isFileTransfer(event)) {
  99. return
  100. }
  101. event.preventDefault()
  102. event.stopPropagation()
  103. // Add a small (+) icon on drop
  104. // (and prevent browsers from interpreting this as files being _moved_ into the browser,
  105. // https://github.com/transloadit/uppy/issues/1978)
  106. event.dataTransfer.dropEffect = 'copy' // eslint-disable-line no-param-reassign
  107. ;(event.currentTarget as HTMLElement).classList.add('uppy-is-drag-over')
  108. this.setPluginState({ isDraggingOver: true })
  109. this.opts.onDragOver?.(event)
  110. }
  111. handleDragLeave = (event: DragEvent): void => {
  112. if (!isFileTransfer(event)) {
  113. return
  114. }
  115. event.preventDefault()
  116. event.stopPropagation()
  117. this.setPluginState({ isDraggingOver: false })
  118. ;(event.currentTarget as HTMLElement)?.classList.remove('uppy-is-drag-over')
  119. this.opts.onDragLeave?.(event)
  120. }
  121. addListeners = (): void => {
  122. const { target } = this.opts
  123. if (target instanceof Element) {
  124. this.nodes = [target]
  125. } else if (typeof target === 'string') {
  126. this.nodes = toArray(document.querySelectorAll(target))
  127. }
  128. if (!this.nodes || this.nodes.length === 0) {
  129. throw new Error(`"${target}" does not match any HTML elements`)
  130. }
  131. this.nodes.forEach((node) => {
  132. node.addEventListener('dragover', this.handleDragOver, false)
  133. node.addEventListener('dragleave', this.handleDragLeave, false)
  134. node.addEventListener('drop', this.handleDrop, false)
  135. })
  136. }
  137. removeListeners = (): void => {
  138. if (this.nodes) {
  139. this.nodes.forEach((node) => {
  140. node.removeEventListener('dragover', this.handleDragOver, false)
  141. node.removeEventListener('dragleave', this.handleDragLeave, false)
  142. node.removeEventListener('drop', this.handleDrop, false)
  143. })
  144. }
  145. }
  146. install(): void {
  147. this.setPluginState({ isDraggingOver: false })
  148. this.addListeners()
  149. }
  150. uninstall(): void {
  151. this.removeListeners()
  152. }
  153. }