I'm making extensive use of the HTML5 native drag & drop, and it's almost entirely behaving itself, with one small exception.
I'm trying to highlight my dropzones when anything is dragged over the page. I originally tried to accomplish this by putting jQuery listeners on the document body, like this:
$("body").live('dragover',function(event){lightdz(event)});
$("body").live('dragexit dragleave drop',function(event){dimdz(event)});
with lightdz() and dimdz() changing the background-color style property of all dropzones on the page to make them stand out. This didn't work. Whenever a dragged object entered a child element on the page (like a div container), the listener would flag this up as a dragleave event and dim the dropzones.
I got around this by applying the listener to all visible elements on the page, instead of just the body. There was occasionally a slight visible flickering on the dropzones when it crossed the boundary between one element and another, but it looked fine.
Anyway, now I've changed lightdz() and dimdz() so that they apply a quick jQuery fadeTo() animation to all non-dropzones. This looks awesome when it works, and makes it very apparent to the user what they can and can't drop things on. The trouble is that when it passes between element boundaries, it applies the fade animation. This is a lot more apparent than the occasional flicker of background-color, especially since if the object is dragged over multiple boundaries very quickly, it will queue the animations and have the page fade in and out repeatedly.
Even if I don't bother with the fadeTo() animation, and just change the opacity, it's a lot more visible than the background-color flicker, because the entire page changes rather than just the dropzone elements.
Is there any way to reference the entire page as a single element for purposes of dragover and dragleave events? Failing that, is there any way to detect a drop that takes place outside of the browser window? If I skip the dragleave event, it looks fine, but if any object is dragged over the browser window and then dropped outside it, the whole page stays faded.
I'm genuinely embarrassed by how easy this one was.
$("*:visible").live('dragenter dragover',function(event){lightdz(event)});
$("#page").live('dragleave dragexit',function(event)
{
if(event.pageX == "0")
dimdz(event);
});
$("*:visible").live('drop',function(event){dimdz(event)});
#page is a page-wide container. If the dragleave event takes the dragged object outside of the browser window, event.pageX will have a value of 0. If it happens anywhere else, it'll have a non-zero value.
I may be getting overly complex here but I would do something like this:
var draggingFile = false;
var event2;
//elements with the class hotspots are OK
var hotspots = $(".hotspots");
//Handlers on the body for drag start & stop
$("body").live("dragover", function(event){ draggingFile = true; event2 = event; });
$("body").live("dragexit dragleave drop", function(event){ draggingFile = false; event2 = event; });
//Function checks to see if file is being dragged over an OK hotspot regardless of other elements infront
var isTargetOK = function(x, y){
hotspots.each(function(i, el){
el2 = $(el);
var pos = el2.offset();
if(x => pos.left && x <= pos.left+el2.width() && y => pos.top && y <= post.top+el2.height()){
return true;
}
});
return false;
};
//Mousemove handler on body
$("body").mousemove(function(e){
//if user is dragging a file
if(draggingFile){
//Check to see if this is an OK element with mouse X & Y
if(isOKTarget(e.pageX, e.pageY)){
//Light em' up!
lightdz(event2);
} else { /* Fade em' :( */ dimdz(event2); }
} else {
dimdz(); //Having no parematers means just makes sure hotspots are off
}
});
BTW that's probably not going to work straight off the bat, so you'll have to tweak it a bit to work with your code.
I tried the accepted solution here, but ended up using setTimeout to overcome the issue. I was having a ton of trouble with the page-wide container blocking the drop element if it was floated on top, and still causing the problem if it was the drop element.
<body style="border: 1px solid black;">
<div id="d0" style="border: 1px solid black;"> </div>
<div id="d1" style="border: 1px solid black; display: none; background-color: red;">-> drop here <-</div>
<div id="d2" style="border: 1px solid black;"> </div>
<div style="float: left;">other element</div>
<div style="float: left;"> - </div>
<div style="float: left;">another element</div>
<br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>
</body>
<script type="text/javascript">
var resetTimer;
var f = function(e)
{
if (e.type == "dragover")
{
e.stopPropagation();
e.preventDefault();
if (resetTimer)
{
clearTimeout(resetTimer);
}
document.getElementById('d1').style.display = '';
}
else
{
var f = function()
{
document.getElementById('d1').style.display = 'none';
};
resetTimer = window.setTimeout(f, 25);
}
};
document.body.addEventListener("dragover", f, true);
document.body.addEventListener("dragleave", f, true);
document.getElementById('d1').addEventListener("drop", function(e){ f(); alert('dropped'); }, false);
</script>
If you were to just call f(); instead of window.setTimeout(f, 250);, you'll see some nasty flickering of the element showing and hiding.
http://jsfiddle.net/guYWx/
Related
I wanted to click on an element, and then have another element respond to pointer move events from all elements in the html document. However it appears that onpointermove events are only captured when the pointer is kept depressed. For example in the following simplified code the onpointermove handler fires when you mouse over the div, or when you click down on the div, move around the screen and then click up anywhere. But the captured is not maintained if you release the mouse button and click up anywhere.
This behaviour kind of makes sense in case you fail to invoke releasePointerCapture but I can't see this mentioned in the MDN docs yet and before I contemplate contributing I was wondering if anyone knew if this was documented somewhere else / correct my interpretation of it.
let captured = false
function toggle_capture (e) {
if (captured) capture_div.releasePointerCapture(e.pointerId)
else capture_div.setPointerCapture(e.pointerId)
captured = !captured
}
const capture_div = document.getElementById("capture_div")
capture_div.onpointerdown = toggle_capture
capture_div.onpointermove = (e) => {
capture_div.innerText = `captured: ${captured} x: ${Math.round(e.clientX)}`
}
div
{
background-color: lightgray;
width: 200px;
height: 60px;
margin: 10px;
}
<div id="capture_div">Mouse over, click or click and hold,
Then move pointer around the screen</div>
<div>Other divs</div>
<div>Other divs</div>
This is correct. The MDN documentation has been updated and the documentation of the spec contains this information.
One can determine the element below the mouse cursor (i.e. the top-most hovered element) with the following techniques:
Listen for the mousemove event. The target is
event.target or
document.elementFromPoint(event.clientX, event.clientY).
This does not work when scrolling while not moving the mouse. Then, the mouse technically doesn’t move; thus, no mouse event will fire.
Unfortunately, both techniques from above are no longer applicable when listening for the scroll event. event.target will be whichever element is scrolled (or document). Also, the mouse cursor position is not exposed on the event object.
As described in this answer to “Determine which element the mouse pointer is on top of in Javascript”, one possible solution is querying the hovered element via the CSS :hover pseudo-class.
document.addEventListener('scroll', () => {
const hoverTarget = document.querySelector('.element:hover');
if (hoverTarget) {
hover(hoverTarget);
}
});
However, this is not usable because it is very inefficient and inaccurate. The scroll event is one of the rapidly firing events and needs to be slowed down when performing anything mildly costly (e.g. querying the DOM).
Also, the hovered element lags behind when scrolling. You can observe this on any kind of website with a lot of links: Hover over one of them and scroll to another link without moving the mouse. It updates only after a few milliseconds.
Is there any way, this can be implemented nicely and efficient? Basically, I want the inverse of mouseenter: Instead of knowing when the mouse enters and element, I want to know when an element intersects with the mouse (e.g. when the mouse is not moved but the element [i.e. when scrolling]).
One approach of tackling this is storing the mouse cursor location with the mousemove event and in the scroll event use document.elementFromPoint(x, y) to figure out the element that should be hovered.
Keep in mind that this is still pretty inefficient due to the scroll event being fired with such a high frequency. The event handler should be debounced to limit execution of the function to once per delay. David Walsh explains how to do this in JavaScript Debounce Function.
let hoveredElement;
let mouseX = 0, mouseY = 0;
document.addEventListener('DOMContentLoaded', () => {
document.addEventListener('mousemove', event => {
mouseX = event.clientX;
mouseY = event.clientY;
hover(event.target);
});
document.addEventListener('scroll', () => {
const hoverTarget = document.elementFromPoint(mouseX, mouseY);
if (hoverTarget) {
hover(hoverTarget);
}
});
});
function hover(targetElement) {
// If the target and stored element are the same, return early
// because setting it again is unnecessary.
if (hoveredElement === targetElement) {
return;
}
// On first run, `hoveredElement` is undefined.
if (hoveredElement) {
hoveredElement.classList.remove('hover');
}
hoveredElement = targetElement;
hoveredElement.classList.add('hover');
}
.element {
height: 200px;
border: 2px solid tomato;
}
.element.hover {
background-color: lavender;
}
<div class="container">
<div class="element element-1">1</div>
<div class="element element-2">2</div>
<div class="element element-3">3</div>
<div class="element element-4">4</div>
<div class="element element-5">5</div>
</div>
Currently, the solution will hover the top-most element under the mouse both when moving the mouse and when scrolling. It might be more suitable for your needs to attach the mousemove listener to a set of specific elements and then always hover event.currentTarget (i.e. the element the event listener was attached to). As for the scroll part, you can use hoverTarget.closest to find the suitable element up in the DOM tree.
I'm waist deep in my own React virtualization implementation and one of the minor issues that has been annoying me is that if I middle click on an item in my list and start scrolling, once that element is removed from the DOM the scrolling halts. My first theory was that the element was gaining focus and that preventing that would solve the issue, but what I've tried hasn't been working and I'm not even sure that's the issue.
How can I prevent this from happening?
See this fiddle for a basic demonstration:
https://jsfiddle.net/v169xkym/2/
And the relevant bit of code that handles virtualization:
$('#container').scroll(function(e) {
$('#container').children().each(function(i) {
if ($('.item:eq(' + i + ')').length > 0) {
if ($('.item:eq(' + i + ')').offset().top < 0) {
$('.item:eq(' + i + ')').remove();
$('#topPadding').height($('#topPadding').height() + 45);
}
}
});
});
Basically, I'm using the standard method of removing the element and upping the padding. In my React implementation this is handled different but here you get a basic functional representation.
you can get around this by not having the disappearing element register mouse events.
this can be done with CSS3 :
div.item {
pointer-events : none;
}
(Not entirely sure why, but my guess is that once the element disappears, the origin of the event is missing, so browsers simply stop doing what they were doing.)
Jsfiddle here
Maybe a bit late to the party. A workaround I am using on a virtual scroller is to detect when there is a scroll event, and when there has been no new events for a time, I consider the scroll is complete.
let scrollTimer = null;
let isScrolling = false;
window.addEventListener('scroll', function() {
clearTimeout(scrollTimer);
isScrolling = true;
scrollTimer = setTimeout(()=>{
isScrolling = false;
},500);
}, false);
I then grab a reference to the element that is hovered at the time isScrolling becomes true (using mouseOver) and prevent this element being unloaded until isScrolling is false. It is a bit of a juggle, but works. I am hoping I can find something simpler as it only seems to be a Chrome problem.
Update: It seems to be a known bug, about to be fixed related to pointer-events: none on something that overlays a virtual scroller (reproduction by someone https://codepen.io/flachware/pen/WNMzKav). I have no idea why my work around above works, but nice to know it wont be needed come Chrome 103. https://bugs.chromium.org/p/chromium/issues/detail?id=1330045&q=chrome%20scroll&can=2&sort=-opened
I am currently switching the menu of my site from pure JavaScript to jQuery. My menu has a rollout / rollin effect.
The menu has an outer wrapper which has an onmouseout event set. If this fires, the relatedTarget is checked whether it's a child of the outer wrapper. If not, the rollin shall happen.
What happens right now is, that if the mouse is moved from the menu's inner wrapper (this is to center the actual menu) to the menu's outer wrapper, the onmouseout fires. There seems to be a tiny part which doesn't belong to the menuOuterWrapper.
The site isn't online right now, so I've prepared a Fiddle here. You will see the problem if you move your mouse from the gray area above the handle to the left or right dark area. The menu will roll in and then immediately out again. The rollin shall only occur when the mouse is moved out of the outer wrapper, i.e. under the dark gray area (or the light gray handle area). To see the dark gray areas, you might have to increase the width of the result block. [EDIT: I reduced the width of inner to 600px, so the dark side areas should be visible by default now.]
SO tells me that I shall include code when linking to JSFiddle. I don't want to break the rules but I'll be honest: I'm clueless where the problem comes from. My best idea is that I made a mistake in my isChildOf implementation, so I'll give you this:
jQuery.fn.isChildOf = function (parentId) {
if ($(this).parents("#" + parentId).length > 0) {
return true;
} else {
return false;
}
};
$('#outer').on('mouseout', function(event) {
if (!$(event.relatedTarget).isChildOf("outer")) {
mouseIsOverMenu = false;
menu_rollin();
}
});
Although this is a minimal example, I did nearly the same with pure JS, where it worked fine. So I guess it's something in the jQuery part. Since these are my first steps with jQuery, it is even more likely.
Every help you can provide is highly appreciated :)
[UPDATE]
I got it working now. The problem was that I didn't check for the relatedTarget to be "outer" itself. So when the mouse leaves the content div and enters the outer div, mouseout fires and of course, outer is no child of itself. So I amended it to
$('#outer').on('mouseout', function(event) {
if (!(event.relatedTarget.id == "outer") &&
!$(event.relatedTarget).isChildOf("outer")) {
mouseIsOverMenu = false;
menu_rollin();
}
});
and that fixed the problem.
if i understood your question right.
This might help
$('#inner').on('mouseover', function() {
mouseIsOverMenu = true;
setTimeout(menu_rollout, 500);
});
$('#inner').on('mouseout', function(event) {
if (!$(event.relatedTarget).isChildOf("outer")) {
mouseIsOverMenu = false;
menu_rollin();
}
});
What i did is i have changed the id of #outer to #inner.
This is a dirty hack, but your problem seems to be with the mouseout function applying too frequently, and what functionality you really want is capturing the mouse leaving the bottom of the menu/content.
Here's some code that will do just that.
$('#outer').on('mouseout', function(event) {
if(event.clientY >= document.getElementById('outer').offsetHeight){
mouseIsOverMenu = false;
menu_rollin();
}
});
here's the associated jsFiddle
I'm customizing forum software
each page has 20 threads
each thread is contained inside a div whose class="threadpreview"
inside of each "threadpreview" divs are the LINKs to the full thread, and a 500 character preview of the thread right beneath the link
when the page loads up, I have all the divs' height set to 19px and overflow:hidden so that the preview of the thread is hidden and you can only see the LINK so the divs look "rolled up"
when a user mouses over the LINK for that thread, the threadpreview div should "unroll" to it's original height to show the content, and onmouseout it should roll back up to 19px to hide the content.
(I'm not using jQuery)
EDIT: If jQuery can do this easily I'll give it a shot
Something like this should point you in the right direction:
function init() {
var lDivs = document.getElementsByTagName("div");
for(var i = lDivs.length; i--;) {
if(lDivs[i].className == "threadpreview") {
lDivs[i].onmouseover = function() { this.style.height = ""; } // probably better to also use lDivs[i].addEventListener("mouseover", ..., false);
lDivs[i].onmouseout = function() { this.style.height = "19px"; }
}
}
}
window.addEventListener("load", init, false); // or window.attachEvent for IE, or window.onload = ...
You'll have to use the appropriate event-attachment function (or switch between them).