eventListener and enclosed elements - javascript

There is a piece of code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Events Examples</title>
<style>
ul {
width: 200px;
height: 200px;
margin: 10px;
background-color: #ccc;
float: left;
}
.highlight {
background-color: red;
}
</style>
</head>
<body>
<ul id="list">
<li>First</li>
<li>Second</li>
<li>Third</li>
</ul>
<script>
const ul = document.querySelector('#list');
ul.addEventListener('mouseover', highlight);
ul.addEventListener('mouseout', highlight);
function highlight(event) {
console.log(event.target);
event.target.classList.toggle('highlight');
}
</script>
</body>
</html>
When started, it looks like this:
What I expect from listeners. When I move the mouse over the grey zone of 'ul', then highlight() function should work. Well, it works fine. What I don't understand: when I move the mouse over 'li' elements, then highlight() works again for unknown reason. How can it be fixed?
I'm new to JS and I have not found the answer to the problem described.

If you're saying you only want to highlight the entire region, then use "mouseenter" and "mouseleave" instead, and this to reference the element.
Then there's no event bubbling issue to have to deal with.
const ul = document.querySelector('#list');
ul.addEventListener('mouseenter', highlight);
ul.addEventListener('mouseleave', highlight);
function highlight(event) {
this.classList.toggle('highlight');
}
ul {
width: 200px;
height: 200px;
margin: 10px;
background-color: #ccc;
float: left;
}
.highlight {
background-color: red;
}
<ul id="list">
<li>First</li>
<li>Second</li>
<li>Third</li>
</ul>

Events always bubble up, but if you want to select the element that you actually added the listener to, use event.currentTarget instead of event.target.
function highlight(event) {
console.log(event.currentTarget);
event.currentTarget.classList.toggle('highlight');
}
Information on event bubbling: What is event bubbling and capturing?
If you want to be sure that the event only gets called on the element that you registered it on, you can check if target matches currentTarget.

If you mouseover one element contained within another, the "inner" element (in your case the <li> will fire a mouseover event, and this will "bubble" up to the element where you attached the listener. The target property on the event will be the inner element that triggered the event, not the one where you attached the listener.
Rather than target, use currentTarget, which indicates the element that you attached the listener to.
update As #Terminus points out, this could lead to multiple handlings of the event, since mousing over both the li and the ul will trigger mouseover events that get handled by the listener. The solution would then be to only run the code if the target is the currentTarget
if(event.target === event.currentTarget)
event.currentTarget.classList.toggle('highlight');

Related

JavaScript click event targetting element only, not its parent container

I have this:
<div onclick="myFunc()" style="height:200px;width:200px;">
<button></button>
</div>
I want myFunc to execute when any place on the div is clicked EXCEPT for the button. How can I do this?
On the button's click event, you need to cancel propagation. Or stop 'bubbling up'.
See https://developer.mozilla.org/en-US/docs/Web/API/Event/stopPropagation
So on your button's click event - you need something like:
function button_click(event) {
event.stopPropagation();
console.log("button clicked.");
}
By default, an element's click event gets passed to its parent.
document.querySelector('#mydiv').addEventListener('click', myFunc)
function myFunc(event) {
if (event.target.tagName !== 'BUTTON') {
console.log('works')
}
}
#mydiv {
background: red;
height: 200px;
width: 200px;
}
<div id="mydiv">
<button>click me</button>
</div>
If your contents are going to be more complex than just a single button and you want to make sure you're only running when you click on the parent element, you could use: e.target === e.currentTarget to detect when the event is occurring on that element specifically. (Documentation: target, currentTarget)
This avoid having to check for every child element, or prevent every child from propagating events. (But if you only have a single child element, one of the other answers would be simpler)
document.getElementById('example').addEventListener('click', myFunc)
function myFunc(e) {
if (e.target === e.currentTarget) {
console.log('Parent div clicked')
}
}
#example {
width: 100px;
height: 100px;
}
div,
span {
border: 1px solid #555;
}
<div id="example">
<button>A</button>
<div>B</div>
<span>C</span>
</div>

JavaScript: Added EventListener to Element - But when I click on it, the Target is a Nested Element [duplicate]

In MDN Event.target reference there is an example about implementing event delegation:
Event delegation example
// Assuming there is a 'list' variable containing an instance of an
// HTML ul element.
function hide(e) {
// Unless list items are separated by a margin, e.target should be
// different than e.currentTarget
e.target.style.visibility = 'hidden';
}
list.addEventListener('click', hide, false);
// If some element (<li> element or a link within an <li> element for
// instance) is clicked, it will disappear.
// It only requires a single listener to do that
Unclear part of the example
What i don't understand in the example is this comment:
// Unless list items are separated by a margin, e.target should be
// different than e.currentTarget
Question
How can margin on <li> elements make difference between Event.target and Event.currentTarget?
Have in mind what makes event.target different than event.currentTarget as stated in MDN Event.currentTarget reference:
I think the point is that if there's no margin, then it'll be impossible to click directly on the ul since the li elements will entirely fill its space.
If there is a margin, then it'll at least be possible to click the ul, in which case event.target and event.currentTarget will be the same.
function hide(e) {
document.querySelector("pre").textContent += 'e.target = ' + e.target.nodeName + ", e.currentTarget = " + e.currentTarget.nodeName + "\n";
}
document.querySelector("#list").addEventListener('click', hide, false);
ul {
border: 2px solid orange;
}
li {
padding: 10px;
color: blue;
margin: 30px;
border: 1px dashed blue;
}
<pre></pre>
<ul id="list">
<li>click me
<li>click me
<li>click me
</ul>

How to trigger a href click event from the parent element

With below code I'm trying to click a tag click event from the parent li tag. But it gives me this error:
Why I want to do this:
When I click the PHP PDO link we need to cursor move in to that text not the li tag. I'm trying to fix it using this. But I know we can call that href from the li click event by getting the a href attribute and set it into the window.location.href. But still trying to trigger the a href click event when I click the li tag.
HTML
<li class="tags" style="cursor: pointer;">
<a class="link_em" href="?l=1" id="1">List1</a>
</li>
Jquery:
$('li.tags').on('click', function (e) {
$(this).children('a').click();
return false;
});
Error:
I got this error when I use above code.
Uncaught RangeError: Maximum call stack size exceeded
UI:
https://jsfiddle.net/k92dep45/
Edited:
I done this ugly thing, but I am still looking proper solution:
$('li.tags a').on('click', function (e) {
e.stopPropagation();
});
$('li.tags').on('click', function (e) {
window.location.href = $(this).children('a.link_em').attr('href');
e.preventDefault();
});
You can use below version for the above code
$('li.tags').on('click', 'a', function (e) {
// Do your stuff.
e.preventDefault();
window.location.href= thishref;
});
Here the second argument is the children 'a' in the click event of jQuery.
The problem is that you keep calling the same element which leads to the Maximum call stack size exceeded.
Look at the below example.
function foo(){
foo();
}
foo();
In the above code, we are calling foo() again and again and it will also produce the same result.
You need to add event.stopPropagation() on a element and prevent event "bubbling" otherwise you create infinite loop and that is why you get that error.
$('li.tags').on('click', function() {
$(this).children('a').click();
});
$('li.tags a').click(function(e) {
e.stopPropagation()
e.preventDefault() // This is just for demo
console.log($(this).text())
})
a {
border: 1px solid black;
}
li {
padding: 50px;
border: 1px solid red;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<ul>
<li class="tags" style="cursor: pointer;">
<a class="link_em" href="?l=1" id="1">List1</a>
</li>
</ul>
You can do it differently by changing the href of the window to the href stored in your a tag, getted by using this instruction:
$(this).children('a').attr("href");
Try to implement, or run the following code snippet to confirm if its resolving your problem & get the expected render:
$('li.tags').on('click', function (e) {
window.location.href= $(this).children('a').attr("href");
return false;
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<li class="tags" style="cursor: pointer;">
<a class="link_em" href="?l=1" id="1">List1</a>
</li>
Edit
After rereading the question, I think that OP has over complicated things (as have I obviously). Added a simpler version of Demo 1 labeled Demo 2. And added Demo 3 which is solution that uses no JavaScript/jQuery just CSS.
Capture Phase
When you need fine grained control over the event chain .addEventListener() is better suited to handle particular aspects like firing the event on the capture phase instead of the bubbling phase. The reason why is because li.tag will be before a.link on the first phase (i.e. capture phase) of the event chain. We assign capture or bubbling phase by assigning the appropriate boolean value to the third parameter:
document.addEventListener('click', function(e) {...}, false);
This is default, which is set at false for the bubbling phase.
document.addEventListener('click', function(e) {...}, true);
This will make the registered object (e.g.document in this example) listen and act on the capture phase.
The following demo -- during capture phase -- will:
assign the clicked li.tag as the event.target...
...then invoke e.stopPropagation() so the a.link nested within the li.tag will not be included in the event chain. The bubbling phase is skipped as well.
Instead, this a.link becomes e.target on a new event chain because the trigger method .click() is invoked on it.
Test
The 3rd link of List0 tests whether a.link is indeed triggered by .click() or firing during capture or bubbling phase, or is the event.target. Because each a.link in List0 has pointer-events:none which makes a.link oblivious to any mouse or click event from a user, we can conclude:
that any activation of said a.link is entirely dependant upon its parent li.tag being clicked by a user.
during the capture phase, no click event will reach a.link due to e.stopPropagation() and pointer-events:none it can't be e.target.
bubbling phase isn't even there for a.link because of e.stopPropagation()
As that event chain dies off .click() is fired and a.link goes off like a second stage rocket 🚀
When clicking anywhere on the 3rd list item we get these results:
log: Target: LI
Does not jump to List2
When clicking the first or second list item of List0 we get these results:
log: LinkID: {0-1 or 0-2}
log: Target: A
log: Target: LI
Jumps to List1 or List2
Errors
In the Fiddle, there were 2 instances of invalid HTML:
Error 1: <ui>
Correct: <ul>
Error 2: All <a> had the same id, all id must be unique
<a class="link_em" href="?l=1" id="1">List1</a>
<a class="link_em" href="?l=2" id="1">List2</a>
Correct:
<a href='#l1-1' class='link' id='0-1'></a>
<a href='#l1-2' class='link' id='0-2'></a>
You'll notice that the classes and ids in HTML are slightly different, but it'll be ok to change back to your own as long as you mind the errors mentioned above.
Demo 1
document.addEventListener('click', function(e) {
if (e.target.className === 'tag') {
e.stopPropagation();
var link = e.target.querySelector('a');
console.log('ID: ' + link.id);
link.click();
console.log('Target: ' + e.target.tagName);
return false;
} else {
console.log('Target: ' + e.target.tagName);
return false;
}
}, true);
ul {
height: 90px;
outline: 1px solid red;
}
li {
height: 30px;
outline: 1px solid blue;
cursor: pointer
}
.link {
display: block;
width: 10px;
height: 10px;
background: red;
pointer-events: none;
cursor: default;
position: relative;
z-index: 1;
}
b {
float: left;
}
hr {
margin-bottom: 1500px
}
.as-console {
position: fixed;
bottom: 30px;
right: 0;
max-width: 150px;
color: blue;
}
.top {
width: 100px;
pointer-events: auto;
}
<h1>Capture Phase Demo</h1>
<details>
<summary>Test</summary>
<p>Third list item is not a .tag.</p>
<dl>
<dt>Results:</dt>
<dd>Logged: "Target: LI"(or "Target: "B")</dd>
<dd>Did not log: "ID:0-2"</dd>
<dd>Did not jump to List2</dd>
<dt>Conclusion:</dt>
<dd>Links on List0 do not respond to mouse or click events from user. Therefore in order to jump from List0 the link must be triggered programmatically.</dd>
</dl>
</details>
<h2 id='l1-0'>List0</h2>
<ul>
<li class='tag'>
<a href='#l1-1' class='link' id='0-1'>List1</a>
</li>
<li class='tag'>
<a href='#l1-2' class='link' id='0-2'>List2</a>
</li>
<li>
<a href='#l1-2' class='link' id='0-2'>Test</a>
</li>
</ul>
<b>1500px to List1 🔻</b>
<hr>
<h2 id='l1-1'>List1</h2>
<ul>
<li><a class='top' href='#l1-0'>Back to Top</a></li>
<li>Item</li>
</ul>
<b>1500px to List2 🔻</b>
<hr>
<h2 id='l1-2'>List2</h2>
<ul>
<li>
<a class='top' href='#l1-0'>Back to Top</a>
</li>
<li>Item</li>
</ul>
Demo 2
document.addEventListener('click', function(e) {
if (e.target.className === 'tag') {
e.stopPropagation();
e.target.querySelector('a').click();
}
}, true);
ul {
height: 100px;
outline: 1px solid red
}
li {
height: 100%;
cursor:pointer;
}
<h1 id='list0'>List0</h1>
<ul>
<li class='tag'>
<a href='#list1'>List1</a>
</li>
</ul>
<hr>
<h1 id='list1'>List1</h1>
<ul>
<li class='tag'>
<a href='#list0'>List0</a>
</li>
Demo 3
ul {
height: 100px;
outline: 1px solid blue
}
li {
height: 100%;
padding: 0
}
a {
display: block;
height: 100%;
width: 100%;
}
<h1 id='list0'>List0
<h1>
<ul>
<li>
<a href='#list1'>List1</a>
</li>
</ul>
<hr>
<h1 id='list1'>List1
<h1>
<ul>
<li>
<a href='#list0'>List0</a>
</li>
</ul>

Click event not emitted when mousedown/mouseup do add/removeClass on element

I'm attaching mousedown, mouseup and click handlers to an element. On mousedown I add a class to the element, on mouseup I remove the class, and on click I do some work. (This is a simplification of the context. In my project the click event is handled by a 3rd party component.)
The problem I'm having is that the click event is never emitted in Safari and Firefox, but it works just fine in Chrome. (I don't know what IE does. I don't have access to it, and don't care about it.)
The code is as follows:
HTML:
<div id="clickme">
<div class="normal"></div>
<div class="highlight"></div>
</div>
<input type="text" id="textinput"/>
CSS:
#clickme:not(.active) > .highlight {
display: none;
}
#clickme.active > .normal {
display: none;
}
.normal, .highlight {
width: 100px;
height: 100px;
}
.normal {
background: blue;
}
.highlight {
background: red;
}
JS:
var clickme = $('#clickme');
var textinput = $('#textinput');
clickme.on('mousedown', function(e) {
clickme.addClass('active');
// ^-- comment this out and the click event starts working
});
clickme.on('mouseup', function(e) {
clickme.removeClass('active');
// ^-- comment this out and the click event starts working after the second click
});
clickme.on('click', function(e) {
e.preventDefault();
textinput.val(Date.now());
});
JSFiddle: https://jsfiddle.net/xLskk3po/14/
JSFiddle without JQuery: https://jsfiddle.net/xLskk3po/15/ It shows that it's not a JQuery problem.
I stumbled upon this SO question: When a mousedown and mouseup event don't equal a click and it looks like my issue is similar to that. So I did something silly: I put a transparent, absolutely positioned element on top.
HTML:
<div id="clickme">
<div class="normal"></div>
<div class="highlight"></div>
<div class="abs"></div> <!-- this is the absolute element, covering #clickme -->
</div>
<input type="text" id="textinput"/>
That fixed it.

Prevent custom event from executing

I want to prevent custom event on parent when child is clicked. Note that I don't have access to the code of parent event. I've tried doing e.preventDefault() on the button itself but it doesn't help.
Is there any way of ignoring all parent events when something inside of it is clicked?
$(function(){
// Note that this is just an example, I don't have access to this code
// This is some custom event inside custom plugin
$('.container').on('click', function() {
alert('This should be alerted only if you click on green box');
});
$('.btn').on('click', function() {
// Here I want to make sure that *parent* events are not triggered.
alert('Button is triggered, green box should be not triggered');
});
});
.container {
width: 300px;
height: 200px;
background: green;
padding-top: 100px;
}
.btn {
width: 100px;
height: 100px;
display: block;
margin: auto;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="container">
<button class="btn">Click Me</button>
</div>
Since you're using jQuery, you can use the event.stopPropagation() method. The event.stopPropagation() method stops the bubbling of an event to parent elements, preventing any parent event handlers from being executed. You can see it in action here
$(document).ready(function () {
$("#button").click(function (event) {
alert("This is the button.");
// Comment the following to see the difference
event.stopPropagation();
});
$("#outerdiv").click(function (event) {
alert("This is the outer div.");
});
});
In this simple example, if you click on the button, the event is handled by its own handler and it won't bubble up the DOM hierarchy. You can add a very simple handler calling event.stopPropagation() on the button and it won't bubble up. No need to mess with the parent's JS.

Categories