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!
Related
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);
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.
I have a website that generates tables based on results from a database. I would like the user to have the ability to move the tables (which are nested in cells) around to reorder them.
I found an article Here which is pretty close. so I tried messing around with a jsfiddle but I can't get much to work.
Here's the JavaScript:
var dragSrcEl = null;
function handleDragStart(e) {
this.style.opacity = '0.4';
dragSrcEl = this;
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/html', 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');
}
function handleDrop(e) {
// this/e.target is current target element.
if (e.stopPropagation) {
e.stopPropagation(); // Stops some browsers from redirecting.
}
// Don't do anything if dropping the same column we're dragging.
if (dragSrcEl != this) {
// Set the source column's HTML to the HTML of the column we dropped on.
dragSrcEl.innerHTML = this.innerHTML;
this.innerHTML = e.dataTransfer.getData('text/html');
}
return false;
}
function handleDragEnd(e) {
[].forEach.call(cols, function (col) {
col.classList.remove('over');
});
}
var cols = document.querySelectorAll('td.DashPad');
[].forEach.call(cols, function (col) {
col.addEventListener('dragstart', handleDragStart, false);
col.addEventListener('dragenter', handleDragEnter, false);
col.addEventListener('dragover', handleDragOver, false);
col.addEventListener('dragleave', handleDragLeave, false);
col.addEventListener('drop', handleDrop, false);
col.addEventListener('dragend', handleDragEnd, false);
});
It changes the opacity of the first table upon move, but none of the others. And it does not do drag and drop at all.
Is it possible to do what I am trying to do with table cells that hold tables?
I recommend jQuery for this. There is a sortable method that takes care of this task pretty easy. All I did was add class="sortable" to the outer table and replaced all your javascript with the following:
$('.sortable').sortable({items: '.DashPad'});
Here is a working copy on JSFiddle http://jsfiddle.net/d1s5ur48/3/
More on jQuery sortable:
https://jqueryui.com/sortable/
I'm building a decision tree in JavaScript. I do not have jQuery available to me for this project.
I would like to be able to have buttons, placed anywhere in the decision tree (Hidden or displayed anywhere on the page), with the same class name. The listener on the JS side would then run a function.
Here is what I am using for and ID based listener. It works well but I need to be able to have multiple buttons with the same class or name available. Although I have seen examples of this, I cannot get it to function properly.
function q1a1() {
var q1a1button = document.getElementById("q1answer1");
if(q1a1button.addEventListener){
q1a1button.addEventListener("click", function() { q1answer1();}, false);
} else if(q1a1button.attachEvent){
q1a1button.attachEvent("onclick", function() { q1answer1();});
}
};
if(window.addEventListener){
window.addEventListener("load", q1a1, false);
} else if(window.attachEvent){
window.attachEvent("onload", q1a1);
} else{
document.addEventListener("load", q1a1, false);
}
function q1answer1() {
//DO SOME STUFF
}
This also needs to work in as many versions of IE as possible. For single class handling I'm using querySelectorAll.
What you are really looking for is JavaScript Event Delegation. In your case, you have BUTTON elements, which I'm going to assume are <button> tags. Now you want to know when one of those buttons was clicked and then run a function:
if (document.addEventListener) {
document.addEventListener("click", handleClick, false);
}
else if (document.attachEvent) {
document.attachEvent("onclick", handleClick);
}
function handleClick(event) {
event = event || window.event;
event.target = event.target || event.srcElement;
var element = event.target;
// Climb up the document tree from the target of the event
while (element) {
if (element.nodeName === "BUTTON" && /foo/.test(element.className)) {
// The user clicked on a <button> or clicked on an element inside a <button>
// with a class name called "foo"
doSomething(element);
break;
}
element = element.parentNode;
}
}
function doSomething(button) {
// do something with button
}
Anywhere on the page that a <button class="foo">...</button> element appears, clicking it, or any HTML tag inside of it, will run the doSomething function.
Update: Since Event Delegation is used, only a single click handler is registered on the document object. If more <button>s are created as a result of an AJAX call, you don't have to register click handlers on those new <button>s since we take advantage of the click event bubbling up from the element the user clicked on to the document object itself.
If you don't have jquery:
if (document.body.addEventListener){
document.body.addEventListener('click',yourHandler,false);
}
else{
document.body.attachEvent('onclick',yourHandler);//for IE
}
function yourHandler(e){
e = e || window.event;
var target = e.target || e.srcElement;
if (target.className.match(/keyword/))
{
//an element with the keyword Class was clicked
}
}
If you use a cross browser library like jquery:
HTML:
<div class="myClass">sample</div>
<div class="myClass">sample 2</div>
JS:
function theFuncToCall(event){
//func code
}
$(document).on('click', '.myClass', theFuncToCall);
var buttons = document.querySelectorAll(".MyClassName");
var i = 0, length = buttons.length;
for (i; i < length; i++) {
if (document.addEventListener) {
buttons[i].addEventListener("click", function() {
// use keyword this to target clicked button
});
} else {
buttons[i].attachEvent("onclick", function() {
// use buttons[i] to target clicked button
});
};
};
This answer is a bit overkill, but it should show you ways you could structure your code in a "modern" way even if you're still targeting old browsers
Write code to add event listeners so there is minimal difference between new and old browsers
var listen = (function () { // will return the handler for use in unlisten
if (window.addEventHandler) {
return function (node, type, handler) {
node.addEventListener(type, handler);
return handler;
};
} else if (window.attachEvent) {
return function (node, type, handler) {
var fn = function (e) {
if (!e) {
e = window.event;
}
if (!e.target && e.srcElement) {
e.target = e.srcElement;
}
return handler.call(this, e);
};
node.attachEvent('on' + type, fn);
return fn;
};
} else {
throw new Error('Events not supported in this environment');
// or
// return function ... node['on' + type] = function () { ... };
}
}());
and if you'd like the reverse, too
var unlisten = (function () { // use handler given by listen
if (window.removeEventListener) {
return function (node, type, handler) {
node.removeEventListener(type, handler);
};
} else if (window.detachEvent) {
return function (node, type, handler) {
node.detachEvent('on' + type, handler);
};
} else {
throw new Error('Events not supported in this environment');
// or
// return function ... node['on' + type] = null;
}
}());
Write your click handler
function clickHandler(e) {
// do stuff
}
Wrap your click handler in a function to choose only clicks on buttons with the right class
function wrappedClickHandler(e) {
var tokens, i;
if (e.target.tagName !== 'INPUT' && e.target.tagName !== 'BUTTON') {
return;
}
tokens = (e.target.className || '').split(' ');
for (i = 0; i < tokens.length; ++i) {
if (tokens[i] === 'theClassTokenWeWant') {
return clickHandler.call(this, e);
// or
// return clickHandler.call(e.target, e);
}
}
}
Add this as a listener to a common ancestor node
var h = listen(document, 'click', wrappedClickHandler);
// .. later, if desired
unlisten(document, 'click', h);
Would the simpler way of writing the event delegation function be to add it to the container of the buttons? For example,
// Select Container Element
const questionContainer = document.querySelector(".container");
// Listen For Clicks Within Container
questionContainer.onclick = function (event) {
// Prevent default behavior of button
event.preventDefault();
// Store Target Element In Variable
const element = event.target;
// If Target Element Is a Button
if (element.nodeName === 'BUTTON') {
// Event Code
}
}
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);