소스 검색

Disable drop state for non-files (#2449)

* Only handle drag events with files

* Added modification to drag-drop package as well

* Use some() instead of find() for broader browser support

* `dataTransfer.types` is not a real array in IE10

* introduce canHandleRootDrop, so we can check if dragged items are supported by some plugin

* drag-drop should check for allowNewUpload

* refactor and simplify

* expose events for Dashboard too

* move event handler call to the bottom

Co-authored-by: Renée Kooi <renee@kooi.me>
Co-authored-by: Artur Paikin <artur@arturpaikin.com>
Lars Fernhomberg 3 년 전
부모
커밋
8985283572

+ 60 - 32
packages/@uppy/dashboard/src/index.js

@@ -580,7 +580,7 @@ module.exports = class Dashboard extends UIPlugin {
   }
 
   handlePaste = (event) => {
-    // 1. Let any acquirer plugin (Url/Webcam/etc.) handle pastes to the root
+    // Let any acquirer plugin (Url/Webcam/etc.) handle pastes to the root
     this.uppy.iteratePlugins((plugin) => {
       if (plugin.type === 'acquirer') {
         // Every Plugin with .type acquirer can define handleRootPaste(event)
@@ -588,70 +588,96 @@ module.exports = class Dashboard extends UIPlugin {
       }
     })
 
-    // 2. Add all dropped files
+    // Add all dropped files
     const files = toArray(event.clipboardData.files)
-    this.addFiles(files)
+    if (files.length > 0) {
+      this.uppy.log('[Dashboard] Files pasted')
+      this.addFiles(files)
+    }
   }
 
   handleInputChange = (event) => {
     event.preventDefault()
     const files = toArray(event.target.files)
-    this.addFiles(files)
+    if (files.length > 0) {
+      this.uppy.log('[Dashboard] Files selected through input')
+      this.addFiles(files)
+    }
   }
 
   handleDragOver = (event) => {
     event.preventDefault()
     event.stopPropagation()
 
-    if (this.opts.disabled
-      || this.opts.disableLocalFiles
-      || !this.uppy.getState().allowNewUpload) {
+    // Check if some plugin can handle the datatransfer without files —
+    // for instance, the Url plugin can import a url
+    const canSomePluginHandleRootDrop = () => {
+      let somePluginCanHandleRootDrop = true
+      this.uppy.iteratePlugins((plugin) => {
+        if (plugin.canHandleRootDrop?.(event)) {
+          somePluginCanHandleRootDrop = true
+        }
+      })
+      return somePluginCanHandleRootDrop
+    }
+
+    // Check if the "type" of the datatransfer object includes files
+    const doesEventHaveFiles = () => {
+      const { types } = event.dataTransfer
+      return types.some(type => type === 'Files')
+    }
+
+    // Deny drop, if no plugins can handle datatransfer, there are no files,
+    // or when opts.disabled is set, or new uploads are not allowed
+    const somePluginCanHandleRootDrop = canSomePluginHandleRootDrop(event)
+    const hasFiles = doesEventHaveFiles(event)
+    if (
+      (!somePluginCanHandleRootDrop && !hasFiles)
+      || this.opts.disabled
+      // opts.disableLocalFiles should only be taken into account if no plugins
+      // can handle the datatransfer
+      || (this.opts.disableLocalFiles && (hasFiles || !somePluginCanHandleRootDrop))
+      || !this.uppy.getState().allowNewUpload
+    ) {
+      event.dataTransfer.dropEffect = 'none'
+      clearTimeout(this.removeDragOverClassTimeout)
       return
     }
 
-    // 1. Add a small (+) icon on drop
+    // Add a small (+) icon on drop
     // (and prevent browsers from interpreting this as files being _moved_ into the
     // browser, https://github.com/transloadit/uppy/issues/1978).
     event.dataTransfer.dropEffect = 'copy'
 
     clearTimeout(this.removeDragOverClassTimeout)
     this.setPluginState({ isDraggingOver: true })
+
+    this.opts.onDragOver?.(event)
   }
 
   handleDragLeave = (event) => {
     event.preventDefault()
     event.stopPropagation()
 
-    if (this.opts.disabled
-      || this.opts.disableLocalFiles
-      || !this.uppy.getState().allowNewUpload) {
-      return
-    }
-
     clearTimeout(this.removeDragOverClassTimeout)
     // Timeout against flickering, this solution is taken from drag-drop library.
     // Solution with 'pointer-events: none' didn't work across browsers.
     this.removeDragOverClassTimeout = setTimeout(() => {
       this.setPluginState({ isDraggingOver: false })
     }, 50)
+
+    this.opts.onDragLeave?.(event)
   }
 
-  handleDrop = (event) => {
+  handleDrop = async (event) => {
     event.preventDefault()
     event.stopPropagation()
 
-    if (this.opts.disabled
-        || this.opts.disableLocalFiles
-        || !this.uppy.getState().allowNewUpload) {
-      return
-    }
-
     clearTimeout(this.removeDragOverClassTimeout)
 
-    // 2. Remove dragover class
     this.setPluginState({ isDraggingOver: false })
 
-    // 3. Let any acquirer plugin (Url/Webcam/etc.) handle drops to the root
+    // Let any acquirer plugin (Url/Webcam/etc.) handle drops to the root
     this.uppy.iteratePlugins((plugin) => {
       if (plugin.type === 'acquirer') {
         // Every Plugin with .type acquirer can define handleRootDrop(event)
@@ -659,25 +685,27 @@ module.exports = class Dashboard extends UIPlugin {
       }
     })
 
-    // 4. Add all dropped files
+    // Add all dropped files
     let executedDropErrorOnce = false
     const logDropError = (error) => {
       this.uppy.log(error, 'error')
 
-      // In practice all drop errors are most likely the same, so let's just show one to avoid overwhelming the user
+      // In practice all drop errors are most likely the same,
+      // so let's just show one to avoid overwhelming the user
       if (!executedDropErrorOnce) {
         this.uppy.info(error.message, 'error')
         executedDropErrorOnce = true
       }
     }
 
-    getDroppedFiles(event.dataTransfer, { logDropError })
-      .then((files) => {
-        if (files.length > 0) {
-          this.uppy.log('[Dashboard] Files were dropped')
-          this.addFiles(files)
-        }
-      })
+    // Add all dropped files
+    const files = await getDroppedFiles(event.dataTransfer, { logDropError })
+    if (files.length > 0) {
+      this.uppy.log('[Dashboard] Files dropped')
+      this.addFiles(files)
+    }
+
+    this.opts.onDrop?.(event)
   }
 
   handleRequestThumbnail = (file) => {

+ 6 - 3
packages/@uppy/dashboard/types/index.d.ts

@@ -28,7 +28,6 @@ export interface DashboardOptions extends PluginOptions {
   disablePageScrollWhenModalOpen?: boolean
   disableStatusBar?: boolean
   disableThumbnailGenerator?: boolean
-  doneButtonHandler?: () => void
   height?: string | number
   hideCancelButton?: boolean
   hidePauseResumeButton?: boolean
@@ -39,7 +38,6 @@ export interface DashboardOptions extends PluginOptions {
   locale?: DashboardLocale & StatusBarLocale
   metaFields?: MetaField[] | ((file: UppyFile) => MetaField[])
   note?: string | null
-  onRequestCloseModal?: () => void
   plugins?: string[]
   fileManagerSelectionType?: 'files' | 'folders' | 'both';
   proudlyDisplayPoweredByUppy?: boolean
@@ -54,7 +52,12 @@ export interface DashboardOptions extends PluginOptions {
   width?: string | number
   autoOpenFileEditor?: boolean
   disabled?: boolean
-    disableLocalFiles?: boolean
+  disableLocalFiles?: boolean
+  onRequestCloseModal?: () => void
+  doneButtonHandler?: () => void
+  onDragOver?: (event: DragEvent) => void
+  onDragLeave?: (event: DragEvent) => void
+  onDrop?: (event: DragEvent) => void
 }
 
 declare class Dashboard extends UIPlugin<DashboardOptions> {

+ 3 - 0
packages/@uppy/dashboard/types/index.test-d.ts

@@ -47,6 +47,9 @@ import Dashboard from '..'
         },
       },
     ],
+    onDragOver: (event) => event.clientX,
+    onDrop: (event) => event.clientX,
+    onDragLeave: (event) => event.clientX,
   })
 
   uppy.on('dashboard:file-edit-state', (file) => {

+ 40 - 23
packages/@uppy/drag-drop/src/index.js

@@ -73,9 +73,11 @@ module.exports = class DragDrop extends UIPlugin {
   }
 
   onInputChange (event) {
-    this.uppy.log('[DragDrop] Files selected through input')
     const files = toArray(event.target.files)
-    this.addFiles(files)
+    if (files.length > 0) {
+      this.uppy.log('[DragDrop] Files selected through input')
+      this.addFiles(files)
+    }
 
     // We clear the input after a file is selected, because otherwise
     // change event is not fired in Chrome and Safari when a file
@@ -87,31 +89,21 @@ module.exports = class DragDrop extends UIPlugin {
     event.target.value = null
   }
 
-  handleDrop (event) {
-    if (this.opts.onDrop) this.opts.onDrop(event)
-
+  handleDragOver (event) {
     event.preventDefault()
     event.stopPropagation()
-    clearTimeout(this.removeDragOverClassTimeout)
 
-    // 2. Remove dragover class
-    this.setPluginState({ isDraggingOver: false })
-
-    // 3. Add all dropped files
-    this.uppy.log('[DragDrop] Files were dropped')
-    const logDropError = (error) => {
-      this.uppy.log(error, 'error')
+    // Check if the "type" of the datatransfer object includes files. If not, deny drop.
+    const { types } = event.dataTransfer
+    const hasFiles = types.some(type => type === 'Files')
+    const { allowNewUpload } = this.uppy.getState()
+    if (!hasFiles || !allowNewUpload) {
+      event.dataTransfer.dropEffect = 'none'
+      clearTimeout(this.removeDragOverClassTimeout)
+      return
     }
-    getDroppedFiles(event.dataTransfer, { logDropError })
-      .then((files) => this.addFiles(files))
-  }
 
-  handleDragOver (event) {
-    if (this.opts.onDragOver) this.opts.onDragOver(event)
-    event.preventDefault()
-    event.stopPropagation()
-
-    // 1. Add a small (+) icon on drop
+    // Add a small (+) icon on drop
     // (and prevent browsers from interpreting this as files being _moved_ into the browser
     // https://github.com/transloadit/uppy/issues/1978)
     //
@@ -120,10 +112,11 @@ module.exports = class DragDrop extends UIPlugin {
 
     clearTimeout(this.removeDragOverClassTimeout)
     this.setPluginState({ isDraggingOver: true })
+
+    this.opts?.onDragOver(event)
   }
 
   handleDragLeave (event) {
-    if (this.opts.onDragLeave) this.opts.onDragLeave(event)
     event.preventDefault()
     event.stopPropagation()
 
@@ -133,6 +126,30 @@ module.exports = class DragDrop extends UIPlugin {
     this.removeDragOverClassTimeout = setTimeout(() => {
       this.setPluginState({ isDraggingOver: false })
     }, 50)
+
+    this.opts?.onDragLeave(event)
+  }
+
+  handleDrop = async (event) => {
+    event.preventDefault()
+    event.stopPropagation()
+    clearTimeout(this.removeDragOverClassTimeout)
+
+    // Remove dragover class
+    this.setPluginState({ isDraggingOver: false })
+
+    const logDropError = (error) => {
+      this.uppy.log(error, 'error')
+    }
+
+    // Add all dropped files
+    const files = await getDroppedFiles(event.dataTransfer, { logDropError })
+    if (files.length > 0) {
+      this.uppy.log('[DragDrop] Files dropped')
+      this.addFiles(files)
+    }
+
+    this.opts.onDrop?.(event)
   }
 
   renderHiddenFileInput () {

+ 23 - 5
packages/@uppy/drop-target/src/index.js

@@ -50,19 +50,37 @@ module.exports = class DropTarget extends BasePlugin {
     event.stopPropagation()
     clearTimeout(this.removeDragOverClassTimeout)
 
-    // 2. Remove dragover class
+    // Remove dragover class
     event.currentTarget.classList.remove('uppy-is-drag-over')
     this.setPluginState({ isDraggingOver: false })
 
-    // 3. Add all dropped files
-    this.uppy.log('[DropTarget] Files were dropped')
+    // Let any acquirer plugin (Url/Webcam/etc.) handle drops to the root
+    this.uppy.iteratePlugins((plugin) => {
+      if (plugin.type === 'acquirer') {
+        // Every Plugin with .type acquirer can define handleRootDrop(event)
+        plugin.handleRootDrop?.(event)
+      }
+    })
 
+    // Add all dropped files, handle errors
+    let executedDropErrorOnce = false
     const logDropError = (error) => {
       this.uppy.log(error, 'error')
+
+      // In practice all drop errors are most likely the same,
+      // so let's just show one to avoid overwhelming the user
+      if (!executedDropErrorOnce) {
+        this.uppy.info(error.message, 'error')
+        executedDropErrorOnce = true
+      }
     }
 
     const files = await getDroppedFiles(event.dataTransfer, { logDropError })
-    this.addFiles(files)
+    if (files.length > 0) {
+      this.uppy.log('[DropTarget] Files were dropped')
+      this.addFiles(files)
+    }
+
     this.opts.onDrop?.(event)
   }
 
@@ -70,7 +88,7 @@ module.exports = class DropTarget extends BasePlugin {
     event.preventDefault()
     event.stopPropagation()
 
-    // 1. Add a small (+) icon on drop
+    // Add a small (+) icon on drop
     // (and prevent browsers from interpreting this as files being _moved_ into the browser,
     // https://github.com/transloadit/uppy/issues/1978)
     event.dataTransfer.dropEffect = 'copy'

+ 8 - 0
packages/@uppy/url/src/index.js

@@ -2,6 +2,7 @@ const { UIPlugin } = require('@uppy/core')
 const { h } = require('preact')
 const { RequestClient } = require('@uppy/companion-client')
 const UrlUI = require('./UrlUI.js')
+const toArray = require('@uppy/utils/lib/toArray')
 const forEachDroppedOrPastedUrl = require('./utils/forEachDroppedOrPastedUrl')
 
 function UrlIcon () {
@@ -155,6 +156,13 @@ module.exports = class Url extends UIPlugin {
       })
   }
 
+  canHandleRootDrop (e) {
+    const items = toArray(e.dataTransfer.items)
+    const urls = items.filter((item) => item.kind === 'string'
+      && item.type === 'text/uri-list')
+    return urls.length > 0
+  }
+
   handleRootDrop (e) {
     forEachDroppedOrPastedUrl(e.dataTransfer, 'drop', (url) => {
       this.uppy.log(`[URL] Adding file from dropped url: ${url}`)

+ 20 - 0
website/src/docs/dashboard.md

@@ -526,3 +526,23 @@ Fired when the user clicks “edit” icon next to a file in the Dashboard. The
 * `file` — The [File Object](https://uppy.io/docs/uppy/#File-Objects) representing the file that was edited.
 
 Fired when the user finished editing the file metadata.
+
+### `onDragOver(event)`
+
+Callback for the [`ondragover`][ondragover] event handler.
+
+### `onDrop(event)`
+
+Callback for the [`ondrop`][ondrop] event handler.
+
+### `onDragLeave(event)`
+
+Callback for the [`ondragleave`][ondragleave] event handler.
+
+<!-- definitions -->
+
+[ondragover]: https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/ondragover
+
+[ondragleave]: https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/ondragleave
+
+[ondrop]: https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/ondrop