How to get past a bug in IE6 with an YUI Overlay? - javascript

Going to play with an Overlay from YUI 2 and IE6 (old stuff, but I have to deal with these)... and encouter a problem. So, can you help with an idea or two ? :D
Suppose the Overlay is created :
var newOverlay = new YAHOO.widget.Overlay("myOverlay" ,
{
context: [someObjectToAttachTo, "tl", "bl"],
monitorresize: false,
iframe: false,
zIndex: 900 });
some content is initalized (inside a div) :
var content = new StringBuffer();
content.append('<div id="containerDiv">');
content.append('whatever! some text for the overlay');
content.append('</div>');
an event is attached to the inner div so we know when it's got the mouse over it :
YAHOO.util.Event.addListener('containerDiv', "mouseover",
function() { alert('mouse in') });
"pour" the div into the overlay :
newOverlay.setBody( content.toString() );
render the overlay, but invisible :
newOverlay.render( document.body );
newOverlay.hide();
Problem : even if the overlay is hidden, if you move the mouse in his area, you will get an alert saying "mouse in".
This does not happen in IE7 or Mozilla. Seems that it is a bug and is related to IE not repainting the DOM until after the execution context is complete Source and some info here, another StackOverflow question
the Overlay is shown and hidden by this mechanism (note: the code described above is being updated here) :
newOverlay.hideTimer = null; // new code
YAHOO.util.Event.addListener('containerDiv', "mouseover", //event existed in above code
function() {
alert('mouse in'); // line existed
clearTimeout(newOverlay.hideTimer) }); // added functionality
YAHOO.util.Event.addListener('containerDiv', "mouseout", // new event
function() { timedHide(newOverlay) });
newOverlay.setBody( content.toString() ); //old code
newOverlay.render( document.body ); //old code
newOverlay.hide(); //old code
the functions used above are :
function customShow(overlayName) {
var overlay = document.getElementById(overlayName);
clearTimeout(overlay.hideTimer);
overlay.syncPosition();
overlay.show();
}
function timedHide(overlayName) {
var overlay = document.getElementById(overlayName);
overlay.hideTimer = setTimeout(function() {overlay.hide() }, 200);
}
here is part two of the hide/show mechanism - the trigger div; please ignore the mix of html and js; you still can read it :P
<span id="triggerSpan">I will show an Overlay</span>
and its events :
YAHOO.util.Event.addListener('triggerSpan', "mouseover",
function() { customShow('myOverlay') });
YAHOO.util.Event.addListener('triggerSpan', "mouseout",
function() { timedHide('myOverlay') });
the object used in the creation of the overlay is :
var someObjectToAttachTo = document.getElementById('triggerSpan');
Long stuff...
now, can you see another way of passing by this IE bug ?
so the overlay does not take my mouse... I have stuff under that overlay that needs to be clicked and hovered (that part is not mentioned in the above code)
Can you see another way of creating/initiazing/showing/hidding that overlay ?

The solution was to change the display style of the containerDiv to none or to block when hiding/showing the overlay.
Add some functions to the Overlay that change the style of the inner div:
newOverlay.showOverlay = function() {
document.getElementById('containerDiv').style.display="block";
newOverlay.show(); };
newOverlay.hideOverlay = function() {
document.getElementById('containerDiv').style.display="none";
newOverlay.hide(); };
and call those functions instead of calling overlay.show() or overlay.hide(), inside the functions customShow and timedHide

Related

Bootstrap 5.2, prevent closing tooltip if cursor is back on triggering element

TLDR: moving the cursor from tooltip back to triggering element closes, shows and closes the tooltip (flickers).
I need to make the tooltips open on hover and make their content clickable. I have found a working example here on SO.
As you hover over the element it shows you a tooltip which can be interacted with, once you move the cursor away from the tooltip it closes.
There is a problem though.
If you leave the tooltip and move the cursor back on the element which triggered the tooltip, the tooltip pops back up, but dissapears after a moment ("flickering"). You need to move the cursor away from the element and back on the element for the tooltip to show again.
What I am trying to do, is check if the cursor is back on the triggering element and if that is the case not run the closing function (tooltip.hide()).
I have tried to do this by imitating the existing process from the example found on SO. That is, check if the tooltip has lost :hover, setTimout (300ms) and check if cursor is now positioned on the triggering element or back on the tooltip.
Here is a jsFiddle example.
This is the code. The problematic code is between the two looong comment lines.
Note: Moving the cursor away from the triggering element and back on the triggering element also triggers the flickering.
//https://stackoverflow.com/questions/67993080/bootstrap-5-make-tooltip-hoverable-and-link-clickable
var tooltipTriggerList = [].slice.call(document.querySelectorAll('button'))
for (let tt of tooltipTriggerList){
tt.setAttribute("data-bs-placement","top")
}
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
const tooltip = new bootstrap.Tooltip(tooltipTriggerEl, {
trigger: "manual",
'customClass': 'custom-tooltip'
})
let tooltipElTimeout;
let currentToolTip;
let currentTooltipTimeout;
tooltipTriggerEl.addEventListener("mouseenter", function () {
let toolTipID;
// Clear Set Timeout
clearTimeout(currentTooltipTimeout);
// Show Tooltip
tooltip.show();
// Assign current tooltip ID to toolTipID variable
toolTipID = tooltipTriggerEl.getAttribute("aria-describedby");
// Assign current tooltip to currentToolTip variable
currentToolTip = document.querySelector(`#${toolTipID}`);
/*******************************************************************/
// Hide tooltip on tooltip mouse leave
currentToolTip.addEventListener("mouseleave", function () {
currentTooltipTimeout = setTimeout(()=>{
console.log("!currentToolTip.matches(':hover')");
console.log(!currentToolTip.matches(":hover"));
if(!tooltipTriggerEl.matches(":hover")){
console.log("!tooltipTriggerEl.matches(':hover')");
console.log(!tooltipTriggerEl.matches(":hover"));
if (!currentToolTip.matches(":hover")) {
tooltip.hide();
}
}
}, 300)
});
/***********************************************************************/
});
tooltipTriggerEl.addEventListener("mouseleave", function () {
// SetTimeout before tooltip disappears
tooltipTimeout = setTimeout(function () {
// Hide tooltip if not hovered.
if (!currentToolTip.matches(":hover")) {
tooltip.hide();
}
}, 100);
});
return tooltip;
})
Thank you
Edit:
Amine Ramouls answer is correct. isHidden also needs to bet set to false on the 2cnd eventListener, otherwise the tooltips no longer work (problem with aria-describedby).
in your code you have an event listener wich add an event listner and that's a big mistake because it add an infinit number of eveneent listner to your element.
so you juste have to organize your code like this :
//https://stackoverflow.com/questions/67993080/bootstrap-5-make-tooltip-hoverable-and-link-clickable
var tooltipTriggerList = [].slice.call(document.querySelectorAll('button'))
for (let tt of tooltipTriggerList){
tt.setAttribute("data-bs-placement","top")
}
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
const tooltip = new bootstrap.Tooltip(tooltipTriggerEl, {
trigger: "manual",
'customClass': 'custom-tooltip'
})
let isHidden = true;
let currentTooltipTimeout;
tooltipTriggerEl.addEventListener("mouseenter", function () {
let toolTipID;
// Clear Set Timeout
clearTimeout(tooltipElTimeout);
clearTimeout(currentTooltipTimeout);
if (isHidden)
{
tooltip.show();
isHidden=false;
}
});
// Hide tooltip on tooltip mouse leave
tooltipTriggerEl.addEventListener("mouseleave", function () {
console.log("!currentToolTip.matches(':hover')");
if(!tooltipTriggerEl.matches(":hover")){
currentTooltipTimeout=setTimeout(()=>{
if (!isHidden && !tooltipTriggerEl.matches(":hover")){
tooltip.hide();
isHidden=true;
}
console.log("!tooltipTriggerEl.matches(':hover')");
console.log(!tooltipTriggerEl.matches(":hover"));
}, 3000)
}
});
return tooltip;
})
now as you can see i juste added the isHidden var to check if the popup info is hidden or not, you can do that with the element if you can get it by a query selector request. that's it. enjoy your life.
Edit: i forget to tell you that i have put 3 seconde before checking the if the popup is hidden or not.

IFrame scrollbar disappears on chrome when visibility changes

Is Google Chrome on Windows having problems to render iframe scrollbars?
I wrote a very simple code to show what is happening (at least with me on chrome 52.0.2743.82 m):
<button>Toggle visibility</button>
<br />
<iframe scrolling="yes" seamless src="https://en.wikipedia.org/wiki/Lorem_ipsum" frameborder="0" style="width: 700px; height: 300px"></iframe>
<script type="text/javascript">
$("button").on("click", function() {
$("iframe").toggle();
});
</script>
Plunker link to code
When the page is loaded, the iframe as it scrollbar are visible.
Hide and show the iframe clicking the button. The scrollbar disappears.
This issue apparently occurs only in chrome.
Anyone is experiencing this too? Any fixes/workarounds?
It seems that bug appeared with the update Chrome 52.0.2743.82 (http://googlechromereleases.blogspot.fr/2016/07/stable-channel-update.html)
One possible workaround is to use the attribute visibility with position: absolute instead of display to show or hide the iframe.
A chrome bug ticket exists for this item: https://bugs.chromium.org/p/chromium/issues/detail?id=641881
I've had this problem, and using visibility instead of display: none wasn't an option.
My workaround was to set overflow: scroll on the <body> of the document being displayed in the iframe, whenever I set the iframe to be visible again. This seems to force the scrollbar to appear on the iframe again. You can then reset the overflow to its old value, and the scrollbar will remain on the iframe. You need to wait for a repaint before you can reset the overflow, though, so I put this in a timeout with delay 0.
function showIframe(iframe) {
var iframeBody = iframe.contentDocument.body;
$(iframe).show();
var oldOverflow = iframeBody.css("overflow");
iframeBody.css("overflow", "scroll");
window.setTimeout(function () {
iframeBody.css("overflow", oldOverflow);
}, 0);
}
There is a "flash" of scrollbar with this workaround if the iframe in question doesn't need to scroll, though, so it might be worth using the visibility workaround for that brief moment where the repaint is required, to avoid the flash.
Here's a workaround I've developed for an application I'm building. It has multiple <iframe> elements in a Foundation tab-control.
I used MutationObserver to observe when the <iframe>'s parent element (a Foundation div.tabs-content div.content element) becomes active, then I toggle the iframe's document's overflow property. The runtime effect is imperceivable.
I originally wanted to observe the <iframe> directly, however no DOM mutation events were raised when the iframe itself's changed display property, I guess because technically speaking element.style values are not part of the DOM-structure proper.
Here's my code (Vanilla.js, no jQuery). If you're using in your application you will want to replace my visibility-detection code with something that is applicable to your document:
window.addEventListener('DOMContentLoaded', function(e) {
var observer = new MutationObserver( onContentMutated );
var options = { attributes: true, childList: false, characterData: false, subtree: false, attributeFilter: ['class'] };
var iframeContainers = document.querySelectorAll('.tabs-content .content');
for(var i = 0; i < iframeContainers.length; i++) {
observer.observe( iframeContainers[i], options );
}
});
function onContentMutated(mutations) {
for(var i = 0; i < mutations.length; i++) {
var m = mutations[i];
var thisIsNowAnActiveTab = m.target.classList.contains('active');
if( thisIsNowAnActiveTab ) {
// get the corresponding iframe and fiddle with its DOM
var iframes = m.target.getElementsByTagName("iframe");
if( iframes.length == 0 ) continue;
var iframe = iframes[0];
iframe.contentWindow.document.documentElement.style.overflow = 'hidden';
// the timeout is to trigger Chrome to recompute the necessity of the scrollbars, which makes them visible again. Because the timeout period is 0 there should be no visible change to users.
setTimeout( function(s) {
s.overflow = 'auto';
}, 0, iframe.contentWindow.document.documentElement.style );
}
console.log( m.type );
}
}
For the given example you can do:
$("iframe").toggle(1)
In my case, it worked by setting back the height:
$("iframe").css("height", "100%")
I had a similar issue on Chrome with an iframe embedded into a jQuery UI tab. When the tab containing the iframe is first displayed, the scrollbar appears. But when I switch to another tab and back to the tab with the iframe then the scrollbar disappears. All the solutions proposed here didn't work for me.
Here is what I did to fix the issue :
First, I create the tabs :
$("#mytabs").tabs();
Then I bind a function to the event "tabsactivate" and I check if the target tab is the one containing the iframe. If it is the case I call a function fixChromeScrollBar() described later on :
$("#mytabs").on("tabsactivate", function(event, ui) {
if ($(event.originalEvent.target).attr("href") == "#mytab-with-iframe") {
fixChromeScrollBar();
}
});
And finally here is the function fixChromeScrollBar() which sets the overflow style attribute of the iframe body (as already said) to either "scroll" or "auto". I noticed that when I only define the "auto" or "scroll" value then if I switch to another tab and back to the iframe I lose the scrollbars. The only way to maintain them is to alternate between the two values each time the iframe appears. It is weird but it works :
function fixChromeScrollBar() {
var iFrameBody = $("#myiframe").contents().find("body");
var originalOverflow = $(iFrameBody).css("overflow");
if (originalOverflow == "visible" || originalOverflow == "auto") {
$(iFrameBody).css("overflow", "scroll");
} else {
$(iFrameBody).css("overflow", "auto");
}
}
You can notice that this method is called only if you switch to the tab containing the iframe so if you click multiple times on this tab without switching to another one this code will only be executed the first time.
Apparently setting src refreshes iframe in chrome, for given example code will be:
<script type="text/javascript">
$("button").on("click", function() {
$('iframe').toggle().attr('src', function(i, val) { return val; });
});
</script>
I adapted Dai's example to my own React IFrame component. I have an iframe within a tab panel, which is itself in a collapsible panel. When either of those get toggled, I force the iframe to repaint. It works wonderfully.
private iframe: HTMLIFrameElement;
private displayObserver: MutationObserver;
componentDidMount() {
// Detect style attribute changes for the containing collapsible components
// If the display goes from 'none' to something else, then we need to redraw the iframe
// so we get the scrollbar back as it should be.
if (isChrome()) {
this.displayObserver = new MutationObserver(this.onContentMutated);
const options = { attributes: true, childList: false, characterData: false, subtree: false, attributeFilter: ['style'] };
const tabPanelAncestor = this.findAncestor(this.iframe, '.tab-panel-content');
if (tabPanelAncestor) {
this.displayObserver.observe(tabPanelAncestor, options);
}
const collapsibleAncestor = this.findAncestor(this.iframe, '.collapsible');
if (collapsibleAncestor) {
this.displayObserver.observe(collapsibleAncestor, options);
}
}
}
private readonly onContentMutated = (mutations: Array<MutationRecord>) => {
R.forEach( (mutation) => {
const targetElement = mutation.target as Element;
const style = targetElement.getAttribute('style');
if (style && !style.match(/display: none/)) {
this.iframe.contentWindow.location.reload(true);
}
}, mutations);
}
private readonly findAncestor = (element: HTMLElement, sel: string): Node | null => {
if (typeof element.closest === 'function') {
return element.closest(sel) || null;
}
let ancestor: HTMLElement | null = element;
while (ancestor) {
if (ancestor.matches(sel)) {
return ancestor;
}
ancestor = ancestor.parentElement;
}
return null;
}

Simple onClick effect in jQuery

I have a very simple web page. Here is my Demo.
Desktop: When I hover over the circle, it expands.
iPad: When I click the circle, it expands.
However, is it possible (on iPad) that when the circle is in the "expanded" state, if I click it again, it contracts?
How would I do this using my existing code?
Javascript:
$('.circle').on('touchstart',function(){});
Many thanks for any help with this.
var events = "mouseover mouseout";
if (navigator.userAgent.match(/(iPod|iPhone|iPad)/)) {
events = "touchstart";
};
$('.circle').on(events,function(){
if ($(this).hasClass("expanded")) {
// contract the circle
$(this).addClass("contracted").removeClass("expanded");
} else {
// expand the circle
$(this).addClass("expanded").removeClass("contracted");
};
});
Now add class contracted to all your circles.
I would try using jQuery toggle: http://api.jquery.com/toggle-event/
$('.circle').toggle(function() {
$(this).addClass("expanded").removeClass("contracted");
}, function() {
$(this).removeClass("expanded").addClass("contracted");
});

How to force Dojo TitlePane to overlap with CSS?

I have this jsfiddle which has two TitlePane widgets in the top right of the center pane. Right now, when the right TitlePane ("Switch Basemap") is clicked, it causes the left TitlePane ("Map Overlays") to shift to the left. Instead, I would like for the right TitlePane, when opened, to hide the left TitlePane rather than shift it.
I have tried playing with z-index, overflow, position, and float, but no luck so far. I am fairly new to Dojo so I think I haven't found the correct combination of style parameters.
Does anyone have any ideas?
Thanks in advance!
Try giving your titlepanes an id, and then add the evenhooks as shown below. To give a widget an id, simply put it into the props as id:'leftId'. I will use the classNames of yours as id in example.
dojo.addOnLoad(function() {
var wLeft = dijit.byId('leftTitlePane');
var wRight = dijit.byId('rightTitlePane');
dojo.connect(wLeft, 'onShow', function() {
wRight.domNode.style.display = 'none';
});
dojo.connect(wLeft, 'onHide', function() {
// wait a few for collapse to finish
setTimeout(function() {wRight.domNode.style.display = '';}, 120);
});
dojo.connect(wRight, 'onShow', function() {
wLeft.domNode.style.display = 'none';
});
dojo.connect(wRight, 'onHide', function() {
// wait a few for collapse to finish
setTimeout(function() {wLeft.domNode.style.display = '';}, 120);
});
});

HTML5 dragleave fired when hovering a child element

The problem I'm having is that the dragleave event of an element is fired when hovering a child element of that element. Also, dragenter is not fired when hovering back the parent element again.
I made a simplified fiddle: http://jsfiddle.net/pimvdb/HU6Mk/1/.
HTML:
<div id="drag" draggable="true">drag me</div>
<hr>
<div id="drop">
drop here
<p>child</p>
parent
</div>
with the following JavaScript:
$('#drop').bind({
dragenter: function() {
$(this).addClass('red');
},
dragleave: function() {
$(this).removeClass('red');
}
});
$('#drag').bind({
dragstart: function(e) {
e.allowedEffect = "copy";
e.setData("text/plain", "test");
}
});
What it is supposed to do is notifying the user by making the drop div red when dragging something there. This works, but if you drag into the p child, the dragleave is fired and the div isn't red anymore. Moving back to the drop div also doesn't make it red again. It's necessary to move completely out of the drop div and drag back into it again to make it red.
Is it possible to prevent dragleave from firing when dragging into a child element?
2017 Update: TL;DR, Look up CSS pointer-events: none; as described in #H.D.'s answer below that works in modern browsers and IE11.
You just need to keep a reference counter, increment it when you get a dragenter, decrement when you get a dragleave. When the counter is at 0 - remove the class.
var counter = 0;
$('#drop').bind({
dragenter: function(ev) {
ev.preventDefault(); // needed for IE
counter++;
$(this).addClass('red');
},
dragleave: function() {
counter--;
if (counter === 0) {
$(this).removeClass('red');
}
}
});
Note: In the drop event, reset counter to zero, and clear the added class.
You can run it here
Is it possible to prevent dragleave from firing when dragging into a child element?
Yes.
#drop * {pointer-events: none;}
That CSS seem to be enough for Chrome.
While using it with Firefox, the #drop shouldn't have text nodes directly (else there's a strange issue where a element "leave it to itself"), so I suggest to leave it with only one element (e.g., use a div inside #drop to put everything inside)
Here's a jsfiddle solving the original question (broken) example.
I've also made a simplified version forked from the #Theodore Brown example, but based only in this CSS.
Not all browsers have this CSS implemented, though:
http://caniuse.com/pointer-events
Seeing the Facebook source code I could find this pointer-events: none; several times, however it's probably used together with graceful degradation fallbacks. At least it's so simple and solves the problem for a lot of environments.
It has been quite some time after this question is asked and a lot of solutions (including ugly hacks) are provided.
I managed to fix the same problem I had recently thanks to the answer in this answer and thought it may be helpful to someone who comes through to this page.
The whole idea is to store the evenet.target in ondrageenter everytime it is called on any of the parent or child elements. Then in ondragleave check if the current target (event.target) is equal to the object you stored in ondragenter.
The only case these two are matched is when your drag is leaving the browser window.
The reason that this works fine is when the mouse leaves an element (say el1) and enters another element (say el2), first the el2.ondragenter is called and then el1.ondragleave. Only when the drag is leaving/entering the browser window, event.target will be '' in both el2.ondragenter and el1.ondragleave.
Here is my working sample. I have tested it on IE9+, Chrome, Firefox and Safari.
(function() {
var bodyEl = document.body;
var flupDiv = document.getElementById('file-drop-area');
flupDiv.onclick = function(event){
console.log('HEy! some one clicked me!');
};
var enterTarget = null;
document.ondragenter = function(event) {
console.log('on drag enter: ' + event.target.id);
enterTarget = event.target;
event.stopPropagation();
event.preventDefault();
flupDiv.className = 'flup-drag-on-top';
return false;
};
document.ondragleave = function(event) {
console.log('on drag leave: currentTarget: ' + event.target.id + ', old target: ' + enterTarget.id);
//Only if the two target are equal it means the drag has left the window
if (enterTarget == event.target){
event.stopPropagation();
event.preventDefault();
flupDiv.className = 'flup-no-drag';
}
};
document.ondrop = function(event) {
console.log('on drop: ' + event.target.id);
event.stopPropagation();
event.preventDefault();
flupDiv.className = 'flup-no-drag';
return false;
};
})();
And here is a simple html page:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Multiple File Uploader</title>
<link rel="stylesheet" href="my.css" />
</head>
<body id="bodyDiv">
<div id="cntnr" class="flup-container">
<div id="file-drop-area" class="flup-no-drag">blah blah</div>
</div>
<script src="my.js"></script>
</body>
</html>
With proper styling what I have done is to make the inner div (#file-drop-area) much bigger whenever a file is dragged into the screen so that the user can easily drop the files into the proper place.
Here, the simplest Cross-Browser solution (seriously):
jsfiddle <-- try dragging some file inside the box
You can do something like that:
var dropZone= document.getElementById('box');
var dropMask = document.getElementById('drop-mask');
dropZone.addEventListener('dragover', drag_over, false);
dropMask.addEventListener('dragleave', drag_leave, false);
dropMask.addEventListener('drop', drag_drop, false);
In a few words, you create a "mask" inside the dropzone, with width & height inherited, position absolute, that will just show when the dragover starts.
So, after showing that mask, you can do the trick by attaching the others dragleave & drop events on it.
After leaving or dropping, you just hide the mask again.
Simple and without complications.
(Obs.: Greg Pettit advice -- You must be sure that the mask hover the entire box, including the border)
This fairly simple solution is working for me so far, assuming your event is attached to each drag element individually.
if (evt.currentTarget.contains(evt.relatedTarget)) {
return;
}
The "right" way to solve this issue is to disable pointer events on child elements of the drop target (as in #H.D.'s answer). Here's a jsFiddle I created which demonstrates this technique. Unfortunately, this doesn't work in versions of Internet Explorer prior to IE11, since they didn't support pointer events.
Luckily, I was able to come up with a workaround which does work in old versions of IE. Basically, it involves identifying and ignoring dragleave events which occur when dragging over child elements. Because the dragenter event is fired on child nodes before the dragleave event on the parent, separate event listeners can be added to each child node which add or remove an "ignore-drag-leave" class from the drop target. Then the drop target's dragleave event listener can simply ignore calls which occur when this class exists. Here's a jsFiddle demonstrating this workaround. It is tested and working in Chrome, Firefox, and IE8+.
Update:
I created a jsFiddle demonstrating a combined solution using feature detection, where pointer events are used if supported (currently Chrome, Firefox, and IE11), and the browser falls back to adding events to child nodes if pointer event support isn't available (IE8-10).
if you are using HTML5, you can get the parent's clientRect:
let rect = document.getElementById("drag").getBoundingClientRect();
Then in the parent.dragleave():
dragleave(e) {
if(e.clientY < rect.top || e.clientY >= rect.bottom || e.clientX < rect.left || e.clientX >= rect.right) {
//real leave
}
}
here is a jsfiddle
A very simple solution is to use the pointer-events CSS property. Just set its value to none upon dragstart on every child element. These elements won't trigger mouse-related events anymore, so they won't catch the mouse over them and thus won't trigger the dragleave on the parent.
Don't forget to set this property back to auto when finishing the drag ;)
A simple solution is to add the css rule pointer-events: none to the child component to prevent the trigger of ondragleave. See example:
function enter(event) {
document.querySelector('div').style.border = '1px dashed blue';
}
function leave(event) {
document.querySelector('div').style.border = '';
}
div {
border: 1px dashed silver;
padding: 16px;
margin: 8px;
}
article {
border: 1px solid silver;
padding: 8px;
margin: 8px;
}
p {
pointer-events: none;
background: whitesmoke;
}
<article draggable="true">drag me</article>
<div ondragenter="enter(event)" ondragleave="leave(event)">
drop here
<p>child not triggering dragleave</p>
</div>
The problem is that the dragleave event is being fired when the mouse goes in front of the child element.
I've tried various methods of checking to see if the e.target element is the same as the this element, but couldn't get any improvement.
The way I fixed this problem was a bit of a hack, but works 100%.
dragleave: function(e) {
// Get the location on screen of the element.
var rect = this.getBoundingClientRect();
// Check the mouseEvent coordinates are outside of the rectangle
if(e.x > rect.left + rect.width || e.x < rect.left
|| e.y > rect.top + rect.height || e.y < rect.top) {
$(this).removeClass('red');
}
}
Very simple solution:
parent.addEventListener('dragleave', function(evt) {
if (!parent.contains(evt.relatedTarget)) {
// Here it is only dragleave on the parent
}
}
I was having the same issue and tried to use pk7s solution. It worked but it could be done a little bit better without any extra dom elements.
Basicly the idea is same - add an extra unvisible overlay over droppable area. Only lets do this without any extra dom elements. Here is the part were CSS pseudo-elements come to play.
Javascript
var dragOver = function (e) {
e.preventDefault();
this.classList.add('overlay');
};
var dragLeave = function (e) {
this.classList.remove('overlay');
};
var dragDrop = function (e) {
this.classList.remove('overlay');
window.alert('Dropped');
};
var dropArea = document.getElementById('box');
dropArea.addEventListener('dragover', dragOver, false);
dropArea.addEventListener('dragleave', dragLeave, false);
dropArea.addEventListener('drop', dragDrop, false);
CSS
This after rule will create a fully covered overlay for droppable area.
#box.overlay:after {
content:'';
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
z-index: 1;
}
Here is the full solution: http://jsfiddle.net/F6GDq/8/
I hope it helps anyone with the same problem.
You can fix it in Firefox with a little inspiration from the jQuery source code:
dragleave: function(e) {
var related = e.relatedTarget,
inside = false;
if (related !== this) {
if (related) {
inside = jQuery.contains(this, related);
}
if (!inside) {
$(this).removeClass('red');
}
}
}
Unfortunately it doesn't work in Chrome because relatedTarget appears not to exist on dragleave events, and I assume you're working in Chrome because your example did't work in Firefox. Here's a version with the above code implemented.
And here it goes, a solution for Chrome:
.bind('dragleave', function(event) {
var rect = this.getBoundingClientRect();
var getXY = function getCursorPosition(event) {
var x, y;
if (typeof event.clientX === 'undefined') {
// try touch screen
x = event.pageX + document.documentElement.scrollLeft;
y = event.pageY + document.documentElement.scrollTop;
} else {
x = event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
y = event.clientY + document.body.scrollTop + document.documentElement.scrollTop;
}
return { x: x, y : y };
};
var e = getXY(event.originalEvent);
// Check the mouseEvent coordinates are outside of the rectangle
if (e.x > rect.left + rect.width - 1 || e.x < rect.left || e.y > rect.top + rect.height - 1 || e.y < rect.top) {
console.log('Drag is really out of area!');
}
})
Here's another solution using document.elementFromPoint:
dragleave: function(event) {
var event = event.originalEvent || event;
var newElement = document.elementFromPoint(event.pageX, event.pageY);
if (!this.contains(newElement)) {
$(this).removeClass('red');
}
}
Hope this works, here's a fiddle.
An alternate working solution, a little simpler.
//Note: Due to a bug with Chrome the 'dragleave' event is fired when hovering the dropzone, then
// we must check the mouse coordinates to be sure that the event was fired only when
// leaving the window.
//Facts:
// - [Firefox/IE] e.originalEvent.clientX < 0 when the mouse is outside the window
// - [Firefox/IE] e.originalEvent.clientY < 0 when the mouse is outside the window
// - [Chrome/Opera] e.originalEvent.clientX == 0 when the mouse is outside the window
// - [Chrome/Opera] e.originalEvent.clientY == 0 when the mouse is outside the window
// - [Opera(12.14)] e.originalEvent.clientX and e.originalEvent.clientY never get
// zeroed if the mouse leaves the windows too quickly.
if (e.originalEvent.clientX <= 0 || e.originalEvent.clientY <= 0) {
I know this is a old question but wanted to add my preference. I deal with this by adding class triggered css :after element at a higher z-index then your content. This will filter out all the garbage.
.droppable{
position: relative;
z-index: 500;
}
.droppable.drag-over:after{
content: "";
display:block;
position:absolute;
left:0;
right:0;
top:0;
bottom:0;
z-index: 600;
}
Then just add the drag-over class on your first dragenter event and none of the child elements trigger the event any longer.
dragEnter(event){
dropElement.classList.add('drag-over');
}
dragLeave(event){
dropElement.classList.remove('drag-over');
}
Not sure if this cross browser, but I tested in Chrome and it solves my problem:
I want to drag and drop a file over entire page, but my dragleave is fired when i drag over child element. My fix was to look at the x and y of mouse:
i have a div that overlays my entire page, when the page loads i hide it.
when you drag over document i show it, and when you drop on the parent it handles it, and when you leave the parent i check x and y.
$('#draganddrop-wrapper').hide();
$(document).bind('dragenter', function(event) {
$('#draganddrop-wrapper').fadeIn(500);
return false;
});
$("#draganddrop-wrapper").bind('dragover', function(event) {
return false;
}).bind('dragleave', function(event) {
if( window.event.pageX == 0 || window.event.pageY == 0 ) {
$(this).fadeOut(500);
return false;
}
}).bind('drop', function(event) {
handleDrop(event);
$(this).fadeOut(500);
return false;
});
I've stumbled into the same problem and here's my solution - which I think is much easier then above. I'm not sure if it's crossbrowser (might depend on even bubbling order)
I'll use jQuery for simplicity, but solution should be framework independent.
The event bubbles to parent either way so given:
<div class="parent">Parent <span>Child</span></div>
We attach events
el = $('.parent')
setHover = function(){ el.addClass('hovered') }
onEnter = function(){ setTimeout(setHover, 1) }
onLeave = function(){ el.removeClass('hovered') }
$('.parent').bind('dragenter', onEnter).bind('dragleave', onLeave)
And that's about it. :) it works because even though onEnter on child fires before onLeave on parent, we delay it slightly reversing the order, so class is removed first then reaplied after a milisecond.
I've written a little library called Dragster to handle this exact issue, works everywhere except silently doing nothing in IE (which doesn't support DOM Event Constructors, but it'd be pretty easy to write something similar using jQuery's custom events)
Just check if the dragged over element is a child, if it is, then don't remove your 'dragover' style class. Pretty simple and works for me:
$yourElement.on('dragleave dragend drop', function(e) {
if(!$yourElement.has(e.target).length){
$yourElement.removeClass('is-dragover');
}
})
I wrote a drag-and-drop module called drip-drop that fixes this weirdo behavior, among others. If you're looking for a good low-level drag-and-drop module you can use as the basis for anything (file upload, in-app drag-and-drop, dragging from or to external sources), you should check this module out:
https://github.com/fresheneesz/drip-drop
This is how you would do what you're trying to do in drip-drop:
$('#drop').each(function(node) {
dripDrop.drop(node, {
enter: function() {
$(node).addClass('red')
},
leave: function() {
$(node).removeClass('red')
}
})
})
$('#drag').each(function(node) {
dripDrop.drag(node, {
start: function(setData) {
setData("text", "test") // if you're gonna do text, just do 'text' so its compatible with IE's awful and restrictive API
return "copy"
},
leave: function() {
$(node).removeClass('red')
}
})
})
To do this without a library, the counter technique is what I used in drip-drop, tho the highest rated answer misses important steps that will cause things to break for everything except the first drop. Here's how to do it properly:
var counter = 0;
$('#drop').bind({
dragenter: function(ev) {
ev.preventDefault()
counter++
if(counter === 1) {
$(this).addClass('red')
}
},
dragleave: function() {
counter--
if (counter === 0) {
$(this).removeClass('red');
}
},
drop: function() {
counter = 0 // reset because a dragleave won't happen in this case
}
});
I found a simple solution to this problem so sharing it. It works well in my case.
jsfiddle try it.
You can actually achieve this only via the dragenter event and you don't even need to register a dragleave. All you need is to have a no-drop area around your dropzones and that's it.
You can also have nested dropzones and this works perfectly. Check this as well nested dropzones.
$('.dropzone').on("dragenter", function(e) {
e.preventDefault();
e.stopPropagation();
$(this).addClass("over");
$(".over").not(this).removeClass("over"); // in case of multiple dropzones
});
$('.dropzone-leave').on("dragenter", function(e) {
e.preventDefault();
e.stopPropagation();
$(".over").removeClass("over");
});
// UPDATE
// As mar10 pointed out, the "Esc" key needs to be managed,
// the easiest approach is to detect the key and clean things up.
$(document).on('keyup', function(e){
if (e.key === "Escape") {
$(".over").removeClass("over");
}
});
After spending so many hours I got that suggestion working exactly as intended. I wanted to provide a cue only when files were dragged over, and document dragover, dragleave was causing painful flickers on Chrome browser.
This is how I solved it, also throwing in proper cues for user.
$(document).on('dragstart dragenter dragover', function(event) {
// Only file drag-n-drops allowed, http://jsfiddle.net/guYWx/16/
if ($.inArray('Files', event.originalEvent.dataTransfer.types) > -1) {
// Needed to allow effectAllowed, dropEffect to take effect
event.stopPropagation();
// Needed to allow effectAllowed, dropEffect to take effect
event.preventDefault();
$('.dropzone').addClass('dropzone-hilight').show(); // Hilight the drop zone
dropZoneVisible= true;
// http://www.html5rocks.com/en/tutorials/dnd/basics/
// http://api.jquery.com/category/events/event-object/
event.originalEvent.dataTransfer.effectAllowed= 'none';
event.originalEvent.dataTransfer.dropEffect= 'none';
// .dropzone .message
if($(event.target).hasClass('dropzone') || $(event.target).hasClass('message')) {
event.originalEvent.dataTransfer.effectAllowed= 'copyMove';
event.originalEvent.dataTransfer.dropEffect= 'move';
}
}
}).on('drop dragleave dragend', function (event) {
dropZoneVisible= false;
clearTimeout(dropZoneTimer);
dropZoneTimer= setTimeout( function(){
if( !dropZoneVisible ) {
$('.dropzone').hide().removeClass('dropzone-hilight');
}
}, dropZoneHideDelay); // dropZoneHideDelay= 70, but anything above 50 is better
});
"dragleave" event is fired when mouse pointer exits the dragging area of the target container.
Which makes a lot of sense as in many cases only the parent may be droppable and not the descendants.
I think event.stopPropogation() should have handled this case but seems like it doesn't do the trick.
Above mentioned some solutions do seem to work for most of the cases, but fails in case of those children which does not support dragenter / dragleave events, such as iframe.
1 workaround is to check the event.relatedTarget and verify if it resides inside the container then ignore the dragleave event as I have done here:
function isAncestor(node, target) {
if (node === target) return false;
while(node.parentNode) {
if (node.parentNode === target)
return true;
node=node.parentNode;
}
return false;
}
var container = document.getElementById("dropbox");
container.addEventListener("dragenter", function() {
container.classList.add("dragging");
});
container.addEventListener("dragleave", function(e) {
if (!isAncestor(e.relatedTarget, container))
container.classList.remove("dragging");
});
You can find a working fiddle here!
Solved ..!
Declare any array for ex:
targetCollection : any[]
dragenter: function(e) {
this.targetCollection.push(e.target); // For each dragEnter we are adding the target to targetCollection
$(this).addClass('red');
},
dragleave: function() {
this.targetCollection.pop(); // For every dragLeave we will pop the previous target from targetCollection
if(this.targetCollection.length == 0) // When the collection will get empty we will remove class red
$(this).removeClass('red');
}
No need to worry about child elements.
You can use a timeout with a transitioning flag and listen on the top element. dragenter / dragleave from child events will bubble up to the container.
Since dragenter on the child element fires before dragleave of the container, we will set the flag show as transitioning for 1ms... the dragleave listener will check for the flag before the 1ms is up.
The flag will be true only during transitions to child elements, and will not be true when transitioning to a parent element (of the container)
var $el = $('#drop-container'),
transitioning = false;
$el.on('dragenter', function(e) {
// temporarily set the transitioning flag for 1 ms
transitioning = true;
setTimeout(function() {
transitioning = false;
}, 1);
$el.toggleClass('dragging', true);
e.preventDefault();
e.stopPropagation();
});
// dragleave fires immediately after dragenter, before 1ms timeout
$el.on('dragleave', function(e) {
// check for transitioning flag to determine if were transitioning to a child element
// if not transitioning, we are leaving the container element
if (transitioning === false) {
$el.toggleClass('dragging', false);
}
e.preventDefault();
e.stopPropagation();
});
// to allow drop event listener to work
$el.on('dragover', function(e) {
e.preventDefault();
e.stopPropagation();
});
$el.on('drop', function(e) {
alert("drop!");
});
jsfiddle: http://jsfiddle.net/ilovett/U7mJj/
I had a similar problem — my code for hiding the dropzone on dragleave event for body was fired contatantly when hovering child elements making the dropzone flicker in Google Chrome.
I was able to solve this by scheduling the function for hiding dropzone instead of calling it right away. Then, if another dragover or dragleave is fired, the scheduled function call is cancelled.
body.addEventListener('dragover', function() {
clearTimeout(body_dragleave_timeout);
show_dropzone();
}, false);
body.addEventListener('dragleave', function() {
clearTimeout(body_dragleave_timeout);
body_dragleave_timeout = setTimeout(show_upload_form, 100);
}, false);
dropzone.addEventListener('dragover', function(event) {
event.preventDefault();
dropzone.addClass("hover");
}, false);
dropzone.addEventListener('dragleave', function(event) {
dropzone.removeClass("hover");
}, false);
I struggeled a LOT with this, even after reading through all of these answers, and thought I may share my solution with you, because I figured it may be one of the simpler approaches, somewhat different though. My thought was of simply omitting the dragleave event listener completely, and coding the dragleave behaviour with each new dragenter event fired, while making sure that dragenter events won't be fired unnecessarily.
In my example below, I have a table, where I want to be able to exchange table row contents with each other via drag & drop API. On dragenter, a CSS class shall be added to the row element into which you're currently dragging your element, to highlight it, and on dragleave, this class shall be removed.
Example:
Very basic HTML table:
<table>
<tr>
<td draggable="true" class="table-cell">Hello</td>
</tr>
<tr>
<td draggable="true" clas="table-cell">There</td>
</tr>
</table>
And the dragenter event handler function, added onto each table cell (aside dragstart, dragover, drop, and dragend handlers, which are not specific to this question, so not copied here):
/*##############################################################################
## Dragenter Handler ##
##############################################################################*/
// When dragging over the text node of a table cell (the text in a table cell),
// while previously being over the table cell element, the dragleave event gets
// fired, which stops the highlighting of the currently dragged cell. To avoid
// this problem and any coding around to fight it, everything has been
// programmed with the dragenter event handler only; no more dragleave needed
// For the dragenter event, e.target corresponds to the element into which the
// drag enters. This fact has been used to program the code as follows:
var previousRow = null;
function handleDragEnter(e) {
// Assure that dragenter code is only executed when entering an element (and
// for example not when entering a text node)
if (e.target.nodeType === 1) {
// Get the currently entered row
let currentRow = this.closest('tr');
// Check if the currently entered row is different from the row entered via
// the last drag
if (previousRow !== null) {
if (currentRow !== previousRow) {
// If so, remove the class responsible for highlighting it via CSS from
// it
previousRow.className = "";
}
}
// Each time an HTML element is entered, add the class responsible for
// highlighting it via CSS onto its containing row (or onto itself, if row)
currentRow.className = "ready-for-drop";
// To know which row has been the last one entered when this function will
// be called again, assign the previousRow variable of the global scope onto
// the currentRow from this function run
previousRow = currentRow;
}
}
Very basic comments left in code, such that this code suits for beginners too. Hope this will help you out! Note that you will of course need to add all the event listeners I mentioned above onto each table cell for this to work.
Here is another approach based on the timing of events.
The dragenter event dispatched from the child element can be captured by the parent element and it always occurs before the dragleave. The timing between these two events is really short, shorter than any possible human mouse action. So, the idea is to memorize the time when a dragenter happens and filter dragleave events that occurs "not too quickly" after ...
This short example works on Chrome and Firefox:
var node = document.getElementById('someNodeId'),
on = function(elem, evt, fn) { elem.addEventListener(evt, fn, false) },
time = 0;
on(node, 'dragenter', function(e) {
e.preventDefault();
time = (new Date).getTime();
// Drag start
})
on(node, 'dragleave', function(e) {
e.preventDefault();
if ((new Date).getTime() - time > 5) {
// Drag end
}
})

Categories

Resources