230

I'm adding an html5 drag and drop uploader to my page.

When a file is dropped into the upload area, everything works great.

However, if I accidentally drop the file outside of the upload area, the browser loads the local file as if it is a new page.

How can I prevent this behavior?

Thanks!

dtbarne
  • 8,110
  • 5
  • 43
  • 49
Travis
  • 2,303
  • 2
  • 14
  • 4
  • 2
    Just curious what code you are using to handle the html5 drag/drop uploading. Thanks. – robertwbradford Jul 31 '11 at 03:52
  • The problem you have is caused by either missing e.dataTransfer() or missing a preventDefault() on drop/dragenter/etc. events. But I can't tell without a code sample. – HoldOffHunger Apr 12 '18 at 14:54

12 Answers12

361

You can add a event listener to the window that calls preventDefault() on all dragover and drop events.
Example:

window.addEventListener("dragover",function(e){
  e = e || event;
  e.preventDefault();
},false);
window.addEventListener("drop",function(e){
  e = e || event;
  e.preventDefault();
},false);
Digital Plane
  • 37,354
  • 7
  • 57
  • 59
  • 54
    dragover is the piece I was missing. – cgatian Feb 17 '14 at 14:48
  • 14
    I confirm that both `dragover` and `drop` handlers are needed to prevent the browser from loading the dropped file. (Chrome latest 2015/08/03). The solution works on FF latest, too. – Offirmo Aug 03 '15 at 12:23
  • 5
    This works perfectly, and I can confirm that it can be used in combination with page elements that are configured to accept drop events, such as those from drag-and-drop file upload scrips like resumable.js. It is useful to prevent the default browser behavior in cases where a user accidentally drops a file they want to upload outside of the actual file-upload drop-zone, and then wonders why they now see that same file rendered directly in the browser window (assuming a compatible file type like an image or video was dropped), rather than the expected behavior of seeing their file upload. – bluebinary Aug 08 '16 at 00:24
  • 17
    Note: this also disables dragging files onto a ``. It is necessary to check whether `e.target` is a file input and let such events through. – Sebastian Nowak Oct 19 '16 at 19:39
  • 8
    what ? why should window dragover load the file ? this makes no sense ... – L.Trabacchin Jan 26 '18 at 14:53
  • 2
    By any means is it also possible to not show move/copy icon? – AbbasFaisal Oct 10 '18 at 13:48
  • 3
    You have to add e.dataTransfer.dropEffect = "none"; to prevent the visual feedback. – Emperor Eto Sep 28 '19 at 20:10
  • 1
    What's the need for `e || event`? Shouldn't e always contain the event? – Nilpo Oct 23 '20 at 05:40
  • FWIW, this solution also disables drag and drop on all child elements even if they have listeners explicitly set in Chrome 86. – Nilpo Oct 23 '20 at 05:49
  • @Nilpo how did you solve the problem of child elements with explict listeners? – BenKoshy Nov 10 '20 at 04:49
  • @PeterMoore `e.dataTransfer.dropEffect = "none"` does not work for me, neither does setting the `dropEffect` to `none` (of course both in the `dragstart` event). The browser still displays the icon and the tooltip, so as a user you get the visual feedback that you can drop something, but you actually cant because weve prevented it in the drop event listener. – Nicolas Stadler Feb 12 '23 at 17:09
  • * effectAllowed – Nicolas Stadler Feb 12 '23 at 17:17
  • WOW! I just added this code to a Greasemonkey script so that it would be injected on every page I ever open… and now it stops unintended drag-and-drops! I have lost many filled-out web forms due to dragging and dropping photos into the form, only to have the browser open the photo and lose all of the contents on the form I filled out. This safeguards me against this problem, whilst still being compatible with intentional drag-and-drop areas when available. Thank you! – JKVeganAbroad Jul 19 '23 at 17:36
48

After a lot of fiddling around, I found this to be the stablest solution:

var dropzoneId = "dropzone";

window.addEventListener("dragenter", function(e) {
  if (e.target.id != dropzoneId) {
    e.preventDefault();
    e.dataTransfer.effectAllowed = "none";
    e.dataTransfer.dropEffect = "none";
  }
}, false);

window.addEventListener("dragover", function(e) {
  if (e.target.id != dropzoneId) {
    e.preventDefault();
    e.dataTransfer.effectAllowed = "none";
    e.dataTransfer.dropEffect = "none";
  }
});

window.addEventListener("drop", function(e) {
  if (e.target.id != dropzoneId) {
    e.preventDefault();
    e.dataTransfer.effectAllowed = "none";
    e.dataTransfer.dropEffect = "none";
  }
});
<div id="dropzone">...</div>

Setting both effectAllow and dropEffect unconditionally on the window causes my drop zone not to accept any d-n-d any longer, regardless whether the properties are set new or not.

1.21 gigawatts
  • 16,517
  • 32
  • 123
  • 231
Axel Amthor
  • 10,980
  • 1
  • 25
  • 44
  • e.dataTransfer() is the critical piece here that makes this work, which the "accepted answer" failed to mention. – HoldOffHunger Apr 12 '18 at 14:53
  • Instead of checking `e.target.id`, you could call `event.stopPropagation()` from the drop zone's event handlers. Also, it is not necessary to set `effectedAlled` here as @HoldOffHunger alluded to. – AJ Richardson Sep 11 '20 at 15:45
  • I've added [this answer](https://stackoverflow.com/questions/6756583/prevent-browser-from-loading-a-drag-and-dropped-file/72444599#72444599) with more modern ES6 syntax. – Nilpo May 31 '22 at 08:38
10

To allow drag-and-drop only on some elements, you could do something like:

window.addEventListener("dragover",function(e){
  e = e || event;
  console.log(e);
  if (e.target.tagName != "INPUT") { // check which element is our target
    e.preventDefault();
  }
},false);
window.addEventListener("drop",function(e){
  e = e || event;
  console.log(e);
  if (e.target.tagName != "INPUT") {  // check which element is our target
    e.preventDefault();
  }  
},false);
enthus1ast
  • 2,099
  • 15
  • 22
9

For jQuery the correct answer will be:

$(document).on({
    dragover: function() {
        return false;
    },
    drop: function() {
        return false;
    }
});

Here return false will behave as event.preventDefault() and event.stopPropagation().

VisioN
  • 143,310
  • 32
  • 282
  • 281
6

Note: Although the OP did not ask for an Angular solution, I came here looking for that. So this is to share what I found to be a viable solution, if you use Angular.

In my experience this problem first arises when you add file drop functionality to a page. Therefore my opinion is that the component that adds this, should also be responsible for preventing drop outside of the drop zone.

In my solution the drop zone is an input with a class, but any unambiguous selector works.

import { Component, HostListener } from '@angular/core';
//...

@Component({
  template: `
    <form>
      <!-- ... -->
      <input type="file" class="dropzone" />
    </form>
  `
})
export class MyComponentWithDropTarget {

  //...

  @HostListener('document:dragover', ['$event'])
  @HostListener('drop', ['$event'])
  onDragDropFileVerifyZone(event) {
    if (event.target.matches('input.dropzone')) {
      // In drop zone. I don't want listeners later in event-chain to meddle in here
      event.stopPropagation();
    } else {
      // Outside of drop zone! Prevent default action, and do not show copy/move icon
      event.preventDefault();
      event.dataTransfer.effectAllowed = 'none';
      event.dataTransfer.dropEffect = 'none';
    }
  }
}

The listeners are added/removed automatically when component is created/destroyed, and other components using the same strategy on the same page do not interfere with each other due to the stopPropagation().

Superole
  • 1,329
  • 21
  • 29
  • 1
    This works like a charm !! The browser even change the mouse cursor by adding a ban icon which is so great !! – pti_jul Feb 06 '20 at 10:13
  • "`Therefore my opinion is that the component that adds this, should also be responsible for preventing drop outside of the drop zone.`" Good point! – johannes Aug 05 '21 at 11:17
3

Here's a little more modernized version of this answer using ES6 syntax.

let dropzoneId = 'dropzone'

const dragEventHandler = e => {
  if (e.target.id !== dropzoneId) {
    e.preventDefault
    e.dataTransfer.effectAllowed = 'none'
    e.dataTransfer.dropEffect = 'none'
  }
}

// window.addEventListener("dragenter", dragEventHandler, false)
// window.addEventListener("dragover", dragEventHandler, false)
// window.addEventListener("drop", dragEventHandler, false)
['dragenter', 'dragover', 'drop'].forEach(ev => window.addEventListener(ev, dragEventHandler, false))
<div id="dropzone">...</div>
Nilpo
  • 4,675
  • 1
  • 25
  • 39
  • This almost works, but if you have multiple dropzones and child elements in the dropzones the `e.target.id != dropzoneId` won't work. So you need to collect all dropzone IDs, then collect all ancestor IDs of the ev.target, and if the intersection is empty, then you can proceed disabling drop. – Iskren Ivov Chernev Jun 06 '23 at 15:09
  • @IskrenIvovChernev You are correct, but that's not what the question was. You're also overcomplicating the issue. This is meant to be a simple answer to a simple problem. Having multiple drop zones or nested containers haven't been considered. If you must have more than one, just make an array `dropzoneIds` and check for an id in that array instead. – Nilpo Jul 06 '23 at 13:53
2

Preventing all drag and drop operations by default might not be what you want. It's possible to check if the drag source is an external file, at least in some browsers. I've included a function to check if the drag source is an external file in this StackOverflow answer.

Modifying Digital Plane's answer, you could do something like this:

function isDragSourceExternalFile() {
     // Defined here: 
     // https://stackoverflow.com/a/32044172/395461
}

window.addEventListener("dragover",function(e){
    e = e || event;
    var IsFile = isDragSourceExternalFile(e.originalEvent.dataTransfer);
    if (IsFile) e.preventDefault();
},false);
window.addEventListener("drop",function(e){
    e = e || event;
    var IsFile = isDragSourceExternalFile(e.originalEvent.dataTransfer);
    if (IsFile) e.preventDefault();
},false);
Community
  • 1
  • 1
Shannon Matthews
  • 9,649
  • 7
  • 44
  • 75
  • 2
    What's the point of `e || event;`? Where is `event` defined? Nevermind. It looks like it's a global object in IE? I found this quote, `"In Microsoft Visual Basic Scripting Edition (VBScript), you must access the event object through the window object."` [here](https://msdn.microsoft.com/en-us/library/ms535863(v=vs.85).aspx) – 1.21 gigawatts Jan 17 '17 at 05:48
2

try this:

document.body.addEventListener('drop', function(e) {
    e.preventDefault();
}, false);
moe
  • 28,814
  • 4
  • 19
  • 16
1

To build on the "check the target" method outlined in a few other answers, here is a more generic/functional method:

function preventDefaultExcept(predicates) {
  return function (e) {
    var passEvery = predicates.every(function (predicate) { return predicate(e); })
    if (!passEvery) {
      e.preventDefault();
    }
  };
}

Called like:

function isDropzone(e) { return e.target.id === 'dropzone'; }
function isntParagraph(e) { return e.target.tagName !== 'p'; }

window.addEventListener(
  'dragover',
  preventDefaultExcept([isDropzone, isntParagraph])
);
window.addEventListener(
  'drop',
  preventDefaultExcept([isDropzone])
);
scott_trinh
  • 155
  • 1
  • 8
  • Also, could add some ES6 here: `function preventDefaultExcept(...predicates){}`. And then use it like `preventDefaultExcept(isDropzone, isntParagraph)` – Marian Aug 16 '18 at 11:47
1

For what its worth, I use the following. Nice and explicit if not particularly elegant perhaps?

var myDropZone = document.getElementById('drop_zone');

// first, inhibit the default behaviour throughout the window
window.addEventListener('drop', () => { 
  event.preventDefault(); 
} );
window.addEventListener('dragover', () => { 
  event.dataTransfer.dropEffect = 'none'; // dont allow drops
  event.preventDefault(); 
} );

// Next, allow the cursor to show 'copy' as it is dragged over 
// my drop zone but dont forget to stop the event propagating

myDropZone.addEventListener('dragover', () => { 
  event.dataTransfer.dropEffect = 'copy';
  event.stopPropagation(); // important !!
  event.preventDefault();
} );

// In my drop zone, deal with files as they are dropped
myDropZone.addEventListener('drop', myDropHandler);
D.Gibson
  • 79
  • 6
0

I have an HTML object (embed) that fills the width and height of the page. The answer by @digital-plane works on normal web pages but not if the user drops onto an embedded object. So I needed a different solution.

If we switch to using the event capture phase we can get the events before the embedded object receives them (notice the true value at the end of the event listener call):

// document.body or window
document.body.addEventListener("dragover", function(e){
  e = e || event;
  e.preventDefault();
  console.log("over true");
}, true);

document.body.addEventListener("drop", function(e){
  e = e || event;
  e.preventDefault();
  console.log("drop true");
}, true);

Using the following code (based on @digital-plane's answer) the page becomes a drag target, it prevents object embeds from capturing the events and then loads our images:

document.body.addEventListener("dragover", function(e){
  e = e || event;
  e.preventDefault();
  console.log("over true");
}, true);

document.body.addEventListener("drop",function(e){
  e = e || event;
  e.preventDefault();
  console.log("Drop true");

  // begin loading image data to pass to our embed
  var droppedFiles = e.dataTransfer.files;
  var fileReaders = {};
  var files = {};
  var reader;

  for (var i = 0; i < droppedFiles.length; i++) {
    files[i] = droppedFiles[i]; // bc file is ref is overwritten
    console.log("File: " + files[i].name + " " + files[i].size);
    reader = new FileReader();
    reader.file = files[i]; // bc loadend event has no file ref

    reader.addEventListener("loadend", function (ev, loadedFile) {
      var fileObject = {};
      var currentReader = ev.target;

      loadedFile = currentReader.file;
      console.log("File loaded:" + loadedFile.name);
      fileObject.dataURI = currentReader.result;
      fileObject.name = loadedFile.name;
      fileObject.type = loadedFile.type;
      // call function on embed and pass file object
    });

    reader.readAsDataURL(files[i]);
  }

}, true);

Tested on Firefox on Mac.

1.21 gigawatts
  • 16,517
  • 32
  • 123
  • 231
0

I am using a class selector for multiple upload areas so my solution took this less pure form

Based on Axel Amthor's answer, with dependency on jQuery (aliased to $)

_stopBrowserFromOpeningDragAndDropPDFFiles = function () {

        _preventDND = function(e) {
            if (!$(e.target).is($(_uploadBoxSelector))) {
                e.preventDefault();
                e.dataTransfer.effectAllowed = 'none';
                e.dataTransfer.dropEffect = 'none';
            }
        };

        window.addEventListener('dragenter', function (e) {
            _preventDND(e);
        }, false);

        window.addEventListener('dragover', function (e) {
            _preventDND(e);
        });

        window.addEventListener('drop', function (e) {
            _preventDND(e);
        });
    },
hngr18
  • 817
  • 10
  • 13