Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Drag/drop files on the page -- lack of a consistent solution

Here's what i'm trying to achieve:

There are multiple dropzones on the page. Users should be able to drag files from their OS and drop them into the dropzones.

Dropzones get highlighted during dragging. There are two visually different types of highlighting: "Target" (for example, the element gets outlined with a dashed border) and "Hover" (for example, the element gets a bright background).

The Target highlighting is applied/removed on/from all dropzones at once:

  • When a user drags a file over the page, all dropzones should be highlighted with the Target highlighting.
  • When a user drags the file outside the page, or cancels the drag/drop operation, or performs the drop, then the Target highlighting should be removed from all the dropzones.

The Hover highlighting should be applied to only one dropzone:

  • When a user drags a file over a dropzone, that dropzone should be highlighted with the Hover highlighting.
  • When a user drags the file outside that dropzone, or cancels the drag/drop operation, or performs the drop, then the Target highlighting should be removed from the dropzone.

When a user drops a file on a dropzone, the file's name should appear inside the dropzone.

When a user drops a file on the page outside dropzones, all dropzones highlighting should be removed and nothing else should happen. Specifically, the dropped file should not be opened by the browser.

The solution should be as much graceful as possible: dirty hacks like using timeouts, counting dragenter/dragleave events and reapplying highlighting on every dragover are not welcome.

The solution should work in latest versions of major browsers.

Here's what i've managed to achieve so far: attempt 1, attempt 2.

Problems that i have successfully solved

  1. Dropping a file outside the dropzone resulted in the browser opening the file.

    Solution:

    $(document).on('dragover drop', function (e) {
        e.preventDefault();
    });
    
  2. Dropping a file into a dropzone generates a drop event with target equal to a child of the dropzone rather than the dropzone itself.

    Solution:

    $dropzones.on( 'drop', function (event) {
    
      /* ... */
    
      // Find the dropzone responsible for the event
      $targetDropzone = $(event.target).closest($dropzones);
    
      /* ... */
    });
    
  3. Hovering the file over dropzone's children generates multiple dragleave events, making the Hover highlighting disappear immediately (the Hover highlighting should be removed from a dropzone when the mouse cursor leaves the dropzone, so it's bound to the dragleave event).

    Solution: use the dragout event instead of dragleave. dragout is a custom event provided by the jquery.event.dragout plugin. This event won't fire for element's children.

Unsolved problems

  1. Unable to detect the moment when the dragged file leaves the document or window, so that a "remove Target highlighting" command could be executed.

    The custom dragout plugin is designed to work only for children of <body>. It works neither for document nor window.

    The official dragstart and dragend events won't work at all for dragging files. This is an expected behavior. :(

    The dragenter and dragleave events bound to document are triggered not only when the mouse pointer enters/leaves the document but also when the pointer enters/leaves document's children. Worse of all, the event.target of first $(document).on('dragenter') occurrence may appear as one element (it can be document or its child), and the last occurrence of $(document).on('dragleave') can appear as a different element, so you can't resolve the issue by comparing event.targets.

    Due to these issues, i failed to gracefully track the moment when the mouse leaves the document.

    I have attempted to use the draghover plugin that is aimed to resolve this issue. I managed to make it work (tested only in Chrome), but it would stop working correctly after the first successful drop. See the failed attempt here.

  2. Though it's impossible to tell visually, the dragenter event gets fired many times while the file hovers over the document. Thus, highlighting is applied multiple times instead of one.

  3. The mouse pointer during drag should display the browser's standard "can't drop here" icon when hovering outside dropzones and the "can drop here" icon when hovering over dropzones.


UPD 2014-03-16 15:30, reply to Ian Bytchek's answer

Hi comrade! Thank you for a detailed reply. Unfortunately, there's a number of issues with your solution.

1.

  1. Unable to detect the moment when the dragged file leaves the document or window, so that a "remove Target highlighting" command could be executed.

$(document).on('dragleave', … must do the trick, see the fiddle below.

Nope, this is very bad.

Let's say you listen for dragenter and dragleave events on <body>. Whenever a dragging mouse pointer hovers across an edge of any element, two events will trigger:

  • dragenter on <body> with event.target set to the hovered element;
  • dragleave on <body> with event.target set to parent of the hovered element.

I supposed that the Target highlighting would be applied with a .dropzone.target-higlighing selector. You did a witty trick by applying the Target highlighting with a .target-highlighting .dropzone selector.

Have a look at your code:

$('body')
    .on('dragenter', function (event) {
        $(event.target).addClass('target-highlighting');
        event.preventDefault();
    })
    .on('dragleave drop', function (event) {
        $(event.target).removeClass('target-highlighting-class');
        event.preventDefault();
    })

If the dropzone resides in a number of nested containers, dragging a file across the containers will result in the target highlighting class migrate form the outermost container to the innermost. Due to the fact that you used a .target-highlighting .dropzone selector in CSS, it looks like that the target highlighting stands...

...until you drag the file over an element which is not one of dropzone's parents. It could be a sidebar or the dropzone itself. When this happens, the .target-highlighting .dropzone selector stops being applied and the target highlighting disappears.

This is not acceptable. The target highlighting should only appear when a file is dragged onto the page and removed when the file is dragged out of the page or when the dragging has been finished (by drop or cancellation).

2.

  1. Though it's impossible to tell visually, the dragenter event gets fired many times while the file hovers over the document. Thus, highlighting is applied multiple times instead of one.

The event is fired every time when the mouse enters some element. So, as you drag over the page it enters many elements and fires many times. To avoid this you need to "disable" everything underneath each droparea, there are two ways to do this.

First, is to use css pointer events, this is the most elegant, but least browser friendly solution. It works with most recent ones, and I personally love it.

Second, is to create a transparent overlay on top of the droparea – the mouse will only hit that and not the elements underneath, which will prevent multiple drag enter events.

These solutions are acceptable for triggering the Hover highlighting which is applied while the mouse pointer is inside the dropzone. (BTW, i've found a more graceful solution for this: a dragout event plugin, see #3 in the Solved Problems section above.)

But they are totally inappropriate for the Target highlighting which should be applied while the mouse pointer is both inside and outside of the dropzone. You'd have to disable mouse events (with either pointer-events: none; or an overlay) for the whole page, and dropzones would no longer accept drops.

3.

  1. The mouse pointer during drag should display the browser's standard "can't drop here" icon when hovering outside dropzones and the "can drop here" icon when hovering over dropzones.

I'm not 100% certain on this, but on MAC I can't seem to change the icon while dragging, as it uses the special default one. I assume this can't be done, but would love to learn otherwise.

I've noticed that what i ask already works in Chrome! See the link below.

Firefox would not change the mouse pointer though. :(

A better boilerplate to test solutions on

There are also some pretty good looking libraries, like http://www.dropzonejs.com/, which I don't have experience with, yet they are a good source of "inspiration".

I've seen this plugin. It does not resolve the issues described above. The Target highlighting is not applied at all, and the Hover highlighting flickers as you drag a file over the dropzone.

Also, i can't use it because i have my own dropzone implementation. For example, my dropzone allows users to sort files added to the dropzone. I only require a solution to handle the drag events.

My personal advice would be to use per-droparea plugin approach, not per-page approach as in your examples. Those components tend to grow pretty big once you add the uploading logic, validation, etc.

You're absolutely right. In my project, i use the wonderful jQuery UI Widget Factory. It's a method of defining jQuery plugins which behave separately from each other.

Here i created a better boilerplate to test further solutions on: http://jsbin.com/rupaloba/4/edit?html,css,js,output

I hope it's not too complicated.

like image 989
Andrey Mikhaylov - lolmaus Avatar asked Mar 10 '14 18:03

Andrey Mikhaylov - lolmaus


People also ask

What is a drag and drop document?

HTML Drag and Drop interfaces enable web applications to drag and drop files on a web page. This document describes how an application can accept one or more files that are dragged from the underlying platform's file manager and dropped on a web page.

How do I enable drag and drop files?

Use the Esc Key and Left Click Locate the file or folder you wish to move by left-clicking on it on your desktop. Hit the “Escape” key on your keyboard once. Release the left-click on your mouse. Drag and drop functionality should now work as normal.

Will Windows 11 support drag and drop?

Drag and drop is now supported on the Windows 11 taskbar. Quickly drag and drop files between app windows, by hovering over apps in the taskbar to bring their windows to the foreground.


2 Answers

Privet Andrey! I've recently faced most of those, will try to share the knowledge.

1. Unable to detect the moment when the dragged file leaves the document or window, so that a "remove Target highlighting" command could be executed.

$(document).on('dragleave', … must do the trick, see the fiddle below.

2. Though it's impossible to tell visually, the dragenter event gets fired many times while the file hovers over the document. Thus, highlighting is applied multiple times instead of one.

The event is fired every time when the mouse enters some element. So, as you drag over the page it enters many elements and fires many times. To avoid this you need to "disable" everything underneath each droparea, there are two ways to do this.

First, is to use css pointer events, this is the most elegant, but least browser friendly solution. It works with most recent ones, and I personally love it.

Second, is to create a transparent overlay on top of the droparea – the mouse will only hit that and not the elements underneath, which will prevent multiple drag enter events.

3. The mouse pointer during drag should display the browser's standard "can't drop here" icon when hovering outside dropzones and the "can drop here" icon when hovering over dropzones.

I'm not 100% certain on this, but on MAC I can't seem to change the icon while dragging, as it uses the special default one. I assume this can't be done, but would love to learn otherwise. You can use a different design, like background color change or maybe add a cursor-like div that will follow the mouse. The example shows the trick with the background.

Fiddle with the examples: http://jsfiddle.net/ianbytchek/Q6uEp/8/


That concerns the questions. My personal advice would be to use per-droparea plugin approach, not per-page approach as in your examples. Those components tend to grow pretty big once you add the uploading logic, validation, etc. In a nutshell:

  1. A base jQuery plugin is extended with the logic required in two (more) components.
  2. It handles all drag and drop business + share base css / html to keep everything DRY.
  3. Somewhere in the index.js $(document).on('dragenter dragover drop', function… prevents from opening files in the browser and navigating away.

There are also some pretty good looking libraries, like http://www.dropzonejs.com/, which I don't have experience with, yet they are a good source of "inspiration".

I've also used the following in my code to cover up for pointer-events in older browsers (but never tested it really) – it checks if the mouse is outside the element's boundaries.

// jQuery event configuration.
jQuery.event.props.push('dataTransfer', 'pageX', 'pageY');

element.on('dragleave', function ( event) {                                                                                                                                        
    var elementPosition = element.offset();                                                                                                                                                 
    var elementWidth = element.width();                                                                                                                                                     
    var elementHeight = element.height();                                                                                                                                                   

    if (event.pageX < elementPosition.left || event.pageX > elementPosition.left + elementWidth || event.pageY < elementPosition.top || event.pageY > elementPosition.top + elementHeight) {
        element.removeClass(States.HIGHLIGHTED);                                                                                                                                            
    }       
    // …    
    // …    
    // …

Update 1 (2014-03-16 19:00)

@Andrey'lolmaus'Mikhaylov, you're right on those points – it's a mess once you start nesting things. It played further with it and it turned out to be a real bitch, so I got intrigued. I had little luck solving it with dragenter and dragleave events, but I'm sure the solution exists. I did comeup with something less appealing though: http://jsfiddle.net/ianbytchek/Q6uEp/14/

It's a fairly neat solution and I think it's going to be cleaner than what you can get with other apporaches. At the same time it feels a little hacky with all the coordinate checks. I'm tired looking at it, if it gets polished to a better / neater version it would be great to know.

like image 102
Ian Bytchek Avatar answered Oct 05 '22 07:10

Ian Bytchek


When i learned how the dragenter and dragleave events work, i figured out that there's no other option but to count event.target elements.

I understood that the draghover.js plugin is very close to what i need.

So i rewrote it in an $.event.special code style for more convenient usage and also modified it so that it doesn't fail after a successful drop.

Here you are: https://github.com/lolmaus/jquery.dragbetter

Demo: http://jsbin.com/rupaloba/15/edit?html,js,output

like image 39
Andrey Mikhaylov - lolmaus Avatar answered Oct 05 '22 07:10

Andrey Mikhaylov - lolmaus