How to prevent people from dropping in non-droppable elements? - javascript

I am implementing drag and drop this way.
<?php echo $this->Html->script('modernizr/2.5.3/modernizr.min.js', array('block' => 'scriptTop')); ?>
$(document).ready(function () {
dropArea = document.getElementById("droparea");
dropArea.addEventListener("dragleave", function (evt) {
var target = evt.target;
if (target && target === dropArea) {
this.className = "";
}
dropArea.className = "";
evt.preventDefault();
evt.stopPropagation();
}, false);
dropArea.addEventListener("dragenter", function (evt) {
this.className = "over";
evt.preventDefault();
evt.stopPropagation();
}, false);
dropArea.addEventListener("dragover", function (evt) {
evt.preventDefault();
evt.stopPropagation();
}, false);
// this is the initial add-drop
dropArea.addEventListener("drop", function (evt) {
doStuff();
this.className = "";
evt.preventDefault();
evt.stopPropagation();
}, false);
}
There are times when my users dropped stuff NOT into #droparea.
I want to prevent them from doing that.
What should I do?

You need to add a noop dragover and drop handler for your document. This will prevent the default browser behavior of replacing current page with dropped file.
See here for a demo
// Handle dropping on non-droparea
document.addEventListener("drop", cancelEvent, false);
document.addEventListener("dragover", cancelEvent, false);
function cancelEvent(evt) {
evt.preventDefault();
evt.stopPropagation();
}

You need to restrict the drop area so that it can be dropped to only required one.
Update the drop event handler as :
dropArea.addEventListener("drop",function(evt){
//default cancel
evt.preventDefault();
evt.stopPropagation();
},true);
Note: parameter "true" is applied.
Refer to the link for more details.
You may refer to demo here

Related

When dragging file onto a HTML page, how do I stop them from opening up in full? [duplicate]

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!
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);
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.
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);
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().
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().
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>
try this:
document.body.addEventListener('drop', function(e) {
e.preventDefault();
}, false);
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);
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])
);
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.
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);
});
},
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);

HTML5 Draggable List - multiple setData?

So, I have a list of bands that I want to organise into a running order, like so:
<li data-band-id='1' draggable='true' class='band-name'>Metallica</li>
<li data-band-id='2' draggable='true' class='band-name'>Slayer</li>
<li data-band-id='3' draggable='true' class='band-name'>Paradise Lost</li>
<li data-band-id='4' draggable='true' class='band-name'>Gojira</li>
I would like to be able to drag these list items around to change each bands placement within the overall list. So far, I have the following JavaScript to do this:
var dragSatMS = null;
function handleDragStart(e) {
//this.style.color = 'green';
dragSatMS = this;
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text', this.innerHTML);
}
function handleDragOver(e) {
if (e.preventDefault) {
e.preventDefault();
}
e.dataTransfer.dropEffect = 'move';
return false;
}
function handleDragEnter(e) {
this.classList.add('over');
}
function handleDragLeave(e) {
this.classList.remove('over');
//this.style.color = '#333';
}
function handleDrop(e) {
if (e.stopPropagation) {
e.stopPropagation();
}
if (dragSatMS != this) {
dragSatMS.innerHTML = this.innerHTML;
this.innerHTML = e.dataTransfer.getData('text');
}
return false;
}
function handleDragEnd(e) {
[].forEach.call(mssatCols, function (mSatCol) {
mSatCol.classList.remove('over');
//mSatCol.style.color = '#333';
});
}
var mssatCols = document.querySelectorAll('#ms-saturday .band-name');
[].forEach.call(mssatCols, function(msSatCol) {
msSatCol.addEventListener('dragstart', handleDragStart, false);
msSatCol.addEventListener('dragenter', handleDragEnter, false);
msSatCol.addEventListener('dragover', handleDragOver, false);
msSatCol.addEventListener('dragleave', handleDragLeave, false);
msSatCol.addEventListener('drop', handleDrop, false);
msSatCol.addEventListener('dragend', handleDragEnd, false);
});
This works perfectly, I can drag and drop lists to make them change places and the name of the band swaps appropriately. However, the value of the 'data-band-id' attribute stays as it was. I know this is exactly what the code I have does and thats my issue. I'd like to amend the code so that both the name of the band being dragged and dropped and the value of the 'data-band-id' attribute are swapped.
I've Googled a lot but found nothing that can show me how to setData on multiple values, any help much appreciated.
You can query the attributes property of both items and access the value of data-band-id. Once you have both two values, you can call setAttribute("name", "value") to update the data-band-id. Your updated handleDrop method would then be:
function handleDrop(e) {
if (e.stopPropagation) {
e.stopPropagation();
}
if (dragSatMS != this) {
dragSatMS.innerHTML = this.innerHTML;
this.innerHTML = e.dataTransfer.getData('text');
//Get the data-band-id of both items.
itemToReplaceAttr = this.attributes["data-band-id"].value;
draggedItemAttr = dragSatMS.attributes["data-band-id"].value;
//Call "setAttribute" to update the attributes.
this.setAttribute("data-band-id", draggedItemAttr);
dragSatMS.setAttribute("data-band-id", itemToReplaceAttr);
}
return false;
}
Here's a demo for good measure: Fiddle
Yass' answer is one way of solving the problem.
But I will now introduce another method to you that all tutorials I have seen on drag-and-drop carefully avoid - I believe the usage of drag-and-drop API provides an ideal scenario where event delegation should shine - however, till date (and as far as I know) none of the popular tutorials have explored that approach.
If you're in a hurry to see my suggestion working, see this fiddle.
So, you may delegate the handling of the drag events to the parent of the items, ul. And instead of swapping the innerHtml of the dragged element with that of the element dropping will occur on, swap the outerHtml.
var draggedItem = null;
var ul = document.getElementById('ms-saturday');
// delegate event handling to the parent of the list items
ul.addEventListener('dragstart', buffer(handleDragStart), false);
ul.addEventListener('dragover', buffer(handleDragOver), false);
ul.addEventListener('drop', buffer(handleDrop), false);
function handleDragStart(e) {
draggedItem = e.target;
e.dataTransfer.effectAllowed = 'move';
// dataTransfer is not really required, but I think IE may need it.
// Also, not that if you must use dataTransfer API,
// IE strongly requires that the mime type must be 'text'.
e.dataTransfer.setData('text', this.innerHTML);
}
function handleDragOver(e) {
if (e.preventDefault) {
e.preventDefault();
}
e.dataTransfer.dropEffect = 'move';
return false;
}
function handleDrop(e) {
var tmp;
if (e.stopPropagation) {
e.stopPropagation();
}
if (draggedItem !== e.target) {
// swapp outerHtml here
tmp = draggedItem.outerHTML;
draggedItem.outerHTML = e.target.outerHTML;
e.target.outerHTML = tmp;;
}
return false;
}
// this is used to create the handlers so that there won't be
// a need to repeat 'if (e.target === ul) {...' for as many times
// as there are handlers needed.
function buffer(fn) {
return function(e) {
// ignore drag-and-drop on the parent element itself
if (e.target === ul) {
return;
}
fn.call(this, e);
};
}
I leave you to refine this approach to your taste.

JavaScript division on click trigger any given predefined event [CROSS BROWSER COMPATIBILITY]

I have a form and an internal division:
<form action="#" name="form" id="identification_form" class="classification_form">
<div name="division" id="identification_division" class="classification_division">
</div>
</form>
JavaScript manipulating with events:
document.getElementById('identification_division').onClick = function(event){
document.getElementById('identification_form').submit(); //does NOT trigger element.onsubmit event
}
document.getElementById('identification_form').onSubmit = function(event){
alert("submitted!")
}
The form successfully submits but does not call the event
The element.onsubmit event only works when using a submit button
inside the form and manually clicking the button (or pressing enter
while in focus of another input element)
How can i make element.onClick call the the onSubmit function?
when using element.onSubmit i get an undefined error in the browser
console.
My current solution:
function submition(event){
alert("submitted");
}
document.getElementById('identification_division').onClick = function(event){
submition(event || null);
document.getElementById('identification_form').submit();
}
document.getElementById('identification_form').onSubmit = function(event){
submition(event || null);
}
Note my current solution can cause a memory leak in early versions of Opera & Internet Explorer.
JQUERY is not an option
please use addEventListener or attachEvent (based on this post: MSIE and addEventListener Problem in Javascript?)
function bindEvent(el, eventName, eventHandler)
{
if (el.addEventListener)
{
el.addEventListener(eventName, eventHandler, false);
}
else if (el.attachEvent)
{
el.attachEvent('on'+eventName, eventHandler);
}
}
bindEvent(document.getElementById('identification_division'), 'click', function ()
{
document.getElementById('identification_form').submit();
});
Calling .submit() does not trigger the onsubmit event. What you can do is trigger a submit event on the form.
Cross-browser compatible, including IE8 and below:
(function() {
if (document.addEventListener) {
document.getElementById("identification_division").addEventListener("click", onClickFunction);
document.getElementById('identification_form').addEventListener('submit', onSubmitFunction, false);
} else if (document.attachEvent) {
document.getElementById("identification_division").attachEvent("onclick", onClickFunction);
document.getElementById('identification_form').attachEvent("onsubmit", onSubmitFunction);
}
function onClickFunction(e) {
alert("Clicked")
fireOnSubmit(document.getElementById('identification_form'));
}
function onSubmitFunction(e) {
alert("submitted!");
if(e.preventDefault) e.preventDefault();
else if(e.returnValue) e.returnValue = false;
else return false;
}
})();
function fireOnSubmit(element) {
var e = null;
if (document.createEventObject) {
//ie
e = document.createEventObject();
element.fireEvent('onsubmit', e);
} else {
//others
e = document.createEvent('HTMLEvents');
e.initEvent('submit', true, true);
element.dispatchEvent(e);
}
}
<form action="#" name="form" id="identification_form" class="classification_form">
<div name="division" id="identification_division" class="classification_division">
Click me for a good time
</div>
</form>
try this:
if (document.addEventListener) {
document.getElementById("identification_division").addEventListener("click", function(e){
document.getElementById('identification_form').submit();
});
document.getElementById('identification_form').addEventListener('submit', function (e) {
e.preventDefault();
alert("submitted!");
}, false);
} else if (document.attachEvent) {
document.attachEvent("onclick", function(e){
document.getElementById('identification_form').submit();
});
document.attachEvent("onsubmit", function(e){
e.preventDefault();
alert("submitted!");
});
}

Nested Drop won't fire

I'm building a Taxonomy Manager in plain javascript (No jQuery), part of this requires that nodes (LI elements) can be dragged and turned into child elements on another LI. I have created a "Child" UL dynamically and a placeholder LI and attached a set of nested event listeners to the UL in order to handle the "drop" part. Sadly the "dragover" works correctly and is firing but the "drop" event is not. What am I missing?
I have pasted my code below but its rather a lot! I have the complete source in codepen here .
TaxonomyManager.prototype.attachDragDropEventListeners = function () {
var manager = this;
[].forEach.call(this._nodes, function(item) {
item.draggable = true;
item.addEventListener('dragstart', dragStartHandler, false);
item.addEventListener('dragover', dragOverHandler, false);
item.addEventListener('dragleave', dragLeaveHandler, false);
item.addEventListener('drop', dropHandler, false);
item.addEventListener('dragend', dragEndHandler, false);
manager._dragSource = null;
function dragStartHandler(e) {
manager._dragSource = this;
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/html', this.innerHTML);
}
function dragOverHandler(e) {
if (e.preventDefault) {
e.preventDefault(); // Necessary. Allows us to drop.
}
removeNewSubTaxonomyPlaceholder();
if ((this.children.length === 0) && (manager._dragSource !== this)) {
var ul = document.createElement('UL');
ul.classList.add('new-sub-taxonomy');
ul.draggable = true;
ul.addEventListener('dragstart', dragStartHandler, false);
ul.addEventListener('dragover', dragOverPlaceholderHandler, false);
ul.addEventListener('dragleave', dragLeaveHandler, false);
ul.addEventListener('drop', dropPlaceholderHandler, false);
ul.addEventListener('dragend', dragEndHandler, false);
var li = document.createElement("LI");
li.classList.add('new-sub-taxonomy-placeholder');
var liText = document.createTextNode("Drop here");
li.appendChild(liText);
ul.appendChild(li);
this.appendChild(ul);
}
e.dataTransfer.dropEffect = 'move';
return false;
}
function dragLeaveHandler(e) {
return false;
}
function dropHandler(e) {
if (e.stopPropagation) {
e.stopPropagation(); // stops the browser from redirecting.
}
if (manager._dragSource !== this) {
var temp = document.createElement("li");
manager._dragSource.parentNode.insertBefore(temp, manager._dragSource);
this.parentNode.insertBefore(manager._dragSource, this);
temp.parentNode.insertBefore(this, temp);
temp.parentNode.removeChild(temp);
}
return false;
}
function dragEndHandler(e) {
removeNewSubTaxonomyPlaceholder();
return false;
}
function dragOverPlaceholderHandler(e) {
if (e.preventDefault) {
e.preventDefault(); // Necessary. Allows us to drop.
}
console.log('this fires');
e.dataTransfer.dropEffect = 'move';
return false;
}
function dropPlaceholderHandler(e) {
console.log('this does not fire');
if (e.stopPropagation) {
e.stopPropagation(); // stops the browser from redirecting.
}
manager.addLeaf(manager._dragSource.firstChild, this.parentNode.parentNode.dataset.id);
manager.renderTree();
return false;
}
function removeNewSubTaxonomyPlaceholder() {
var placeholder = document.querySelector('.new-sub-taxonomy');
if (placeholder) {
placeholder.parentNode.removeChild(placeholder);
}
}
});
};
Take A look at this :GITHUB link
Jquery JS link : https://github.com/ilikenwf/nestedSortable/blob/2.0alpha/jquery.mjs.nestedSortable.js
Some Custom Details of used functions
disableParentChange (2.0)
Set this to true to lock the parentship of items. They can only be re-ordered within theire current parent container.
doNotClear (2.0)
Set this to true if you don't want empty lists to be removed. Default: false
expandOnHover (2.0)
How long (in ms) to wait before expanding a collapsed node (useful only if isTree: true). Default: 700
isAllowed (function)
You can specify a custom function to verify if a drop location is allowed. Default: function (placeholder, placeholderParent, currentItem) { return true; }
isTree (2.0)
Set this to true if you want to use the new tree functionality. Default: false
Basically the issue was I needed to add more e.stopPropagation() and e.preventDefault() statements to the dragover and drop event handlers on both the parent and child nodes that were hooked up to those events.
I guess that's what you get for coding at 3am!

Prevent browser from loading a drag-and-dropped file

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!
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);
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.
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);
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().
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().
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>
try this:
document.body.addEventListener('drop', function(e) {
e.preventDefault();
}, false);
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);
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])
);
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.
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);
});
},
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);

Categories