DOM - timing of simultaneous events vs setTimeout - javascript

Suppose I have an element containing several children and want to run some code whenever the mouse enters or leaves the container. If I naively write:
var onHover = function (el, f) {
el.addEventListener('mouseover', function () {
f(true);
});
el.addEventListener('mouseout', function () {
f(false);
});
};
Then I get the desired behavior in some cases - depending on the nature of the callback f. However, when the mouse moves from child to child within the container, f(false) runs immediately followed by f(true). I don't want this to happen - I only want f to be run when the mouse enters or leaves the container as a whole, not called machine-gun style as the user drags their mouse over the elements that are inside the container.
Here's the solution that I came up with:
var onHover = function (el, f) {
var previousMouseover = false;
var receivedMouseover = false;
var pushing = false;
var pushEv = function () {
if (pushing) { return; }
pushing = true;
setTimeout(function () {
pushing = false;
if (previousMouseover !== receivedMouseover) {
f(receivedMouseover);
previousMouseover = receivedMouseover;
}
});
};
el.addEventListener('mouseover', function () {
receivedMouseover = true;
pushEv();
});
el.addEventListener('mouseout', function () {
receivedMouseover = false;
pushEv();
});
};
This solution, like the first solution, assumes and works by the virtue that the mouseout event is sent before the mouseover event is. I would also like to know whether that is formally specified by any W3C documentation, but that is not the topic of this question, and even if it were not the case, it would be easy to write a functioning algorithm in spite of that by setting two separate variables, say receivedMouseover and receivedMouseout inside of the mouseover and mouseout callbacks, both of which are then inspected inside of the setTimeout callback.
The question is: Is it required that both the mouseover and mouseout events be processed before any setTimeout callbacks signed up by either event are run?

Use the mouseenter and mouseleave events instead of mouseover and mouseout.

Since you have attached the event listener to the parent element you may compare the event origin (event.target) with the parent element (this or event.currentTarget) before you take an action. You may do as follows;
var onHover = function (el, f) {
el.addEventListener('mouseover', function (evt) {
this === evt.target && f(true);
});
el.addEventListener('mouseout', function (evt) {
this === evt.target && f(false);
});
};
Most of the elements bubble so at some point this might be the right way to do this job.
Edit: As mentioned in the comments the mouseover and mouseout events can be problematic under some circumstances such as when the parent element has no padding or margins defined and children cover all the parent. Even if they don't the speed of the mouse could be fast enough to make the JS engine fail to sample the mouse over the parent element. This fact is beautifuly explained in this article.
So, as mentioned in the accepted answer, i suppose the mouseenter and mouseleave events are there to solve this problem. Accordingly the right code should be like;
var onHover = function (el, f) {
el.addEventListener('mouseenter', () => f(true));
el.addEventListener('mouseleave', () => f(false));
};
Edit 2: Well... Actually there is a safe way of using mouseover and mouseout in this particular condition. It's about using CSS pointer-events property on the children which disables them from event emitting for mouse activity.
var container = document.getElementById('container');
container.addEventListener('mouseover', function (ev) {
console.log(container === ev.target);
});
container.addEventListener('mouseout', function (ev) {
console.log(container === ev.target);
});
#container * {
pointer-events: none
}
<div id="container">
<div>
<span>text</span>
</div>
</div>

Related

Removing an element works only sometimes

My click event works with jQuery but not with Vanilla JS.
This is how it works with jQuery:
$(document).on('click', '.mt-remove-keyword', function() {
alert('jQuery');
});
This is how I thought that it should work but it did not (maybe because my element is dynamic):
const mtRemoveKeywords = document.querySelectorAll('.mt-remove-keyword');
for (let mtRemoveKeyword of mtRemoveKeywords) {
mtRemoveKeyword.addEventListener('click', (e) => {
alert('JS');
});
}
So I have used document as the selector to bubble it down to the main selector:
document.addEventListener('click', (e) => {
if (e.target.classList.contains('mt-remove-keyword')) {
console.log(e.target.classList);
e.target.parentElement.parentElement.remove();
}
});
The goal is to remove the parent element of the target.
The problem is, sometimes it works, sometimes it doesn't. Feel free to test it on JSFiddle. Just add a couple of texts to the yellow section and then try to delete them.
The target might be different than what you think it is. In that case the parents might be off. So better to use closest to walk the tree.
document.addEventListener('click', e => {
var mtKeyword = e.target.closest('.mt-keyword');
mtKeyword && mtKeyword.remove();
});
It would be better to bind the event listener to the parent element so not every document click is tracked.
This is how I could resolve this issue (with help of epascarello):
document.addEventListener('click', (e) => {
if (e.target.classList.contains('mt-remove-keyword') || e.target.parentElement.classList.contains('mt-remove-keyword')) {
e.target.closest('.mt-keyword').remove();
}
});

Referring to tooltip within event listener to keep it open on hover

Finding myself in a bit of a strange position where I have to reference the tooltip within an instantiation for all tooltips.
$('body').tooltip({
selector: '[data-toggle="tooltip"]',
html: true,
animation: false,
}).on("mouseenter", function (e) {
e.stopPropagation();
var _this = e.target;
$(_this).tooltip("show");
$(".tooltip").on("mouseleave", function () {
$(_this).tooltip('hide');
});
}).on("mouseleave", function (e) {
e.stopPropagation();
var _this = e.target;
setTimeout(function () {
if (!$(".tooltip:hover").length) {
$(_this).tooltip("hide");
}
}, 300);
});
That being said, how can I:
Reference the actual element that is triggering this jQuery call
Keep the tooltip open while either the actual tooltip or element that generated it are being hovered over?
Here is a link to a JSFiddle prototype:
https://jsfiddle.net/cwv57weu/8/
Within your '.on()' call, you can add an 'event' argument to your anonymous function. This will contain all of the data from the event including the element which triggered the event (it will be referenced as 'target').
}).on("mouseenter", function (event) {
$(event.target).tooltip("show");
})
the event argument contains a ton of data, I would play around with it by console.log(event) within your anonymous function to get a feel as to what data is available to you.
Use event.target.
$('body').tooltip({
selector: '[data-toggle="tooltip"]',
html: true,
animation: false,
}).on("mouseenter", function (e) {
var _this = e.target;
$(_this).tooltip("show");
$(".tooltip").one("mouseleave", function () {
$(_this).tooltip('hide');
});
}).on("mouseleave", function (e) {
var _this = e.target;
setTimeout(function () {
if (!$(".tooltip:hover").length) {
$(_this).tooltip("hide");
}
}, 300);
});
e.target is the actual element that the event originated on, while this is the element that the event listener was attached to (equivalent to e.currentTarget).
Note that because of event bubbling, the event will fire on all the containing elements up to body. You may want to use e.stopPropagation() to prevent bubbling, so you only process the deepest element.
I also changed the mouseleave handler on .tooltip to use .one(). Otherwise, every time you enter something, you'll add another mouseleave handler to all the tooltips, without removing the previous one, and soon there will be thousands of handlers running (this is why it's generally wrong to bind event handlers inside other event handlers). I'm not really sure you need both that mouseleave handler and the one you attach to body.

Best way to catch mouseup outside of mousedown element

$('#clickableElement').bind({
mousedown: function(e)
{
console.log('mousedown on element');
$(document).bind('mouseup',function(e){
console.log('mouseup caught');
//Do some magic here
$(this).unbind('mouseup');
});
},
mouseup:function(e)
{
//mouseup within element, no use here.
}
});
I'm trying to catch the mouseup event from a mousedown that's released inside or outside of an element. This code almost works, but the problem is the unbind('mouseup') which is unbinding other scripts attached to the mouseup event (jqueryui). If unbind() is not set then the code gets stacked within mouseup event and called x number of times, where x is the times you've mousedowned.
Route 1: is there some kind of self destructing function that calls itself once and destroys?
Route 2: any way to copy/clone the mouseup function prior to inserting the code, then unbind, then set as previous?
Ideally I'd like to keep this code structure for neatness as I have lots of clickable elements, so binding the document.mouseup outside of element.mousedown would be messy.
Here's the Fiddle I forgot to add http://jsfiddle.net/9gFNk/
Can giv your click event a namespace so only that namespaced event gets unbound, and not any others
$(document).on('mouseup.clickableElement',function(e){
console.log('mouseup caught');
//Do some magic here
$(this).off('mouseup.clickableElement');
});
I created a global object to catch mouse events from the document. It's currently set for mouseup only but can be easily expanded for others. The mouseup code is still customizable within the mousedown functions of the clickable elements so it this handy if you have lots of clickable things like I do.
var MouseCatcher=function()
{
this.init=function()
{
var mc = this;
$(document).bind({
mouseup:function(e)
{
mc.mouseup();
}
});
}
this.mouseup=function()
{
return false;
}
}
var mouseCatcher = new MouseCatcher();
mouseCatcher.init();
$('#clickableElement').bind({
mousedown: function(e)
{
console.log('mousedown on element');
mouseCatcher.mouseup=function()
{
console.log('mouseup called from MouseCatcher');
this.mouseup = function(){return false;}
}
},
mouseup:function(e)
{
//mouseup within element, no use here.
}
});
With "on" event its possible, its may not be an exact solution. Please refer this code
$(document).on('mousedown', function() {
$('#clickableElement').css('display', 'none');
$(document).bind('mouseup', function() {
$('#clickableElement').css('display', 'block');
});
});
http://jsfiddle.net/9gFNk/13/

Issue with event functions

I have a strange issue with jQuery.
I have a function, which gets executed on an event of a <a> tag.
link.click(someAction);
In the action, I modify another div-element, where I simply set a few CSS parameters and modify the classes.
This works as expected.
Now, I wanted to expand someAction with a bool parameter.
I figured that I could call the method now as followed:
link.click(function () {
someAction(true);
});
Unfortunately, this does not work. I have no idea why.
The method gets called and everything, but the CSS & classes simply do not change.
Then again by calling exactly the same method with link.click(someAction); it works.
Can anyone tell me why?
Here's some code
var openPopover = function( showOverlay ){
if (typeof showOverlay === "undefined" || showOverlay === null) showOverlay = true;
if (showOverlay) {
// Add transparent overlay
overlay.show();
}
// Select popover next to the clicked item
popover = $(this).next("div.popover");
// It positioned underneath the clicked item, with the spacing above
// Display the popover
popover.show();
// Reset classes
popover.removeClass("hide-animation");
popover.addClass("show-animation");
var animationEnd = function() {
$(overlay).off("webkitTransitionEnd");
$(overlay).off("oTransitionEnd");
$(overlay).off("transitionend");
};
// Add animation did end observer
$(overlay).on("webkitTransitionEnd", animationEnd);
$(overlay).on("oTransitionEnd", animationEnd);
$(overlay).on("transitionend", animationEnd);
// Run animations
popover.addClass("shown");
overlay.addClass("shown");
// If the browser doesn't support css3 animations, we call it manually
if (!supportsCSSAnimations) {
setTimeout(animationEnd, animationDuration);
}
};
selectButton.hover(openPopover); // Opens the popover correctly
selectButton.hover(function () {
openPopover(true); // Doesn't work
});
After your changes:
this in the following line, will point to window:
popover = $(this).next("div.popover");
whereas before, it pointed to selectButton. Try:
selectButton.hover(function () {
openPopover.call(this, true);
});
Make sure to preventDefault on the link once it has been clicked:
link.click(function (e) {
e.preventDefault();
someAction(true);
});

jquery: how do I trigger an event after a click-hold-release action?

I want to show a menu after a click, drag, and release action.
How do I trigger that with jQuery?
Listen for a mousedown event on whatever should be clicked on.
Add a mousemove and mouseup event handler to the window
In the mouseup event handler call trigger('yourcustomeventhere') on whatever element you please. Also, remove the mouseup and mousemove event handlers from window
...?
profit.
jQuery is the library that will do this for you. I thought I explained the code well enough, but apparantly not:
$(anElement).mousedown(foodown);
function foodown(){
$(window).mousemove(foomove).mouseup(fooup);
//stuff
}
function foomove(){
//stuff
}
function fooup(){
$(someElement).trigger('yourcustomevent');
$(window).unbind('mousemove', foomove).unbind('mouseup', fooup);
}
/**
* Dragondrop jQuery plugin by zzzzBov
*/
(function ($) {
"use strict";
var $window;
function begin(e) {
var event;
$window.mousemove(drag).mouseup(end);
event = $.Event('beginDragon');
$(e.target).trigger(event);
if (event.isDefaultPrevented()) {
e.preventDefault();
}
}
function drag(e) {
var event;
event = $.Event('dragDragon');
$(e.target).trigger(event);
if (event.isDefaultPrevented()) {
e.preventDefault();
}
}
function end(e) {
var event;
event = $.Event('endDragon');
$(e.target).trigger(event);
$window.unbind('mousemove', drag).unbind('mouseup', end);
if (event.isDefaultPrevented()) {
e.preventDefault();
}
}
$.each('beginDragon dragDragon endDragon'.split(' '), function (i, name) {
$.fn[name] = function(data,fn) {
if (fn == null) {
fn = data;
data = null;
}
return arguments.length > 0 ?
this.bind(name, data, fn) :
this.trigger(name);
};
});
$window = $(window);
$window.mousedown(begin);
}(jQuery));
You could use the jQueryUI and let it do a lot for you. It also comes with a create UI (of course, because it's jQuery UI)
Take a look at this: http://jqueryui.com/demos/droppable/
edit:
Or take a look here: http://jqueryui.com/demos/draggable/
Take a close look to the events used here.
jQuery UI has a drag and drop implementation. If that doesn't do what you do, you'll have to roll your own implementation by tracking the mouseup and mousedown events on the element yourself. (And possibly mouseleave to detect if the mouse left the area you want to track the gesture in.)

Categories