Weird behaviour with on click event binding; event not firing - javascript

So I came across this weird issue and I don't know if I'm blatantly missing something. Visit this page (or any medium article). Open console and inject the following JS code.
for (const elem of document.querySelectorAll('.progressiveMedia-image.js-progressiveMedia-image')) {
elem.addEventListener('click', function () {
console.log(event.target);
});
}
Now click on the big images, expected behaviour should be (please correct me cause I seem to be wrong) that the target element is printed when you click on it the first time and also the second time. But as it turns out when you click on the image (zoomed) the second time (to zoom out) it doesn't print the target in the console.
I thought that there might be some overlay element and hence I bind the event on body to capture all of the events using the following injected JS.
document.body.addEventListener('click', function () {
console.log(event.target);
}, true);
But even with that I only get one console print of the target.
One theory for delegation using body not working might be following -
The newly created element would not be in the body in its time of creation, it will be moved to its place in the DOM tree later on. And hence delegation is not able to find it when did via body but able to capture it via document.
After a bit more exploring and injecting the following JS (taken from here and I know break point can be added, I did do that earlier but to no end so resorted to this.)
var observer = new MutationObserver(function (mutationsList, observer) {
for (var mutation of mutationsList) {
if (mutation.type == 'childList') {
console.log('A child node has been added or removed.');
}
else if (mutation.type == 'attributes') {
console.log('The ' + mutation.attributeName + ' attribute was modified.');
}
}
});
observer.observe(document, {
attributes: true,
childList: true,
subtree: true
});
I don't see any element being added to the DOM on click (it is being added on load) so that theory might not be correct. So I guess now the real question is why Event Capturing through document is able to find the click event where else not from body. I don't think delegation works on initial DOM structure since it would break the purpose.
Also if it is a duplicate please let me know, since I don't really know what to exactly search for.

probably because there is something in front of the zoomed image that intercepts the click event in capture mode and stops propagation.
I've got success with this
document.addEventListener("click", function(e){ console.log(e.target); }, true);

The image (you are trying to target) is dynamically made. After you already clicked the image once you should be able to target it.

document.querySelectorAll('.progressiveMedia-image.js-progressiveMedia-image')
This queries the DOM for all elements that have both the class progressiveMedia-image and js-progressiveMedia-image. You iterate over the result and bind an event listener to the click event of each element.
When you do click on one of the images, the JavaScript that is already running in the page creates new elements and displays them. These new elements might have the same classes, but did not exist originally when you searched the DOM. As such, they do not have their click event bound.

Related

Removing event listener from custom element

I have a problem removing event listener from a custom created element. The code below runs but I still get more event listeners (showed on screens below in chrome devtools).
stopPropagation(event) {
event.stopPropagation();
},
saveFileDocument() {
let elementToDownload = document.createElement("a");
// Some code to add attributes and stuff
// like "download" (i'm not sure it this is relevant)
document.body.appendChild(elementToDownload);
elementToDownload.addEventListener("click", this.stopPropagation);
elementToDownload.click();
elementToDownload.removeEventListener("click", this.stopPropagation);
document.body.removeChild(elementToDownload);
elementToDownload = null;
}
Screen from devtools (before and after click):
Before (549 listeners)
After (551 listeners)
The funny thing: when I remove the line with removing event listener the listnener count only increases by 1 on each function call.
Frontend framework I work on is Vue but I don't think it's Vue related.
This is my first question so sorry if I didn't go in too many details. I can add them when requested.
Thanks,
Paweł

Redirect an event to an inner element

I have a special scenario where I need to capture key events on a higher level, and redirect them to a lower level. I'm trying to write code like this, but actually it seems like jQuery is dumbly using the exact same event object, so target is not changed, so Maximum callstack size exceeded...
$("#outer").on("keydown", function(evt) {
if (evt.target !== $("#inner")[0] && !$.contains($("#inner")[0], evt.target)) {
$("#inner").trigger(evt);
}
});
With a sample markup
<div id="#outer">
<div id="#inner">
</div>
</div>
What is the proper way to redirect an event to another element? I could of course manually "hack" the target, but is it safe?
I'm working on a plugin which dynamically shows a menu as a popup or as a modal based on screen size. If I have a modal, I need this key redirection. The consumer should not know about wether it's a popup or a modal. I'm using bootstrap's modal plugin, which captures key events because of a "focus-lock thing", so the only way I can make it transparent is that I redirect these events to my real menu.
You could attach the event handler onto the outer and specify a selector to use to match the event:
$('#outer').on('keydown', '#inner', event => {
/* this is executed when #inner is clicked */
});
If you really want to use trigger, you could put it in a setTimeout:
$('#outer').on('keydown', event => {
if (yourCondition) {
setTimeout(() => $('#inner').trigger(event), 0);
}
});
The event will bubble up and you're right, it's the same event. This isn't a jQuery thing but actually just how javascript works. You may want to set a listener on the child element, and use preventDefault();
$("#outer").on("keydown", doSomeThing);
$("#inner").on("keydown", doSomeThingElse);
function doSomeThing(e) {
}
function doSomeThingElse(e) {
e.preventDefault();
}
this will allow you to separate your listeners into distinct functions.
Thank you all for the answers and comments. A few of you suggested preventing propagation of the event at the #inner div. I need to avoid this, I need a way where any external consumer will see this event just as it was triggered by the #inner element for real.
In the meantime I digged into jQuery source and found this line in trigger.
if ( !event.target ) {
event.target = elem;
}
So when jQuery initializes the event to trigger, it only assignes the element to the target if the target is not yet specified. sure they have some good reasons for this behavior, which I cannot see at this moment.
So the best thing I could come up with is a function like this.
function redirectEvent(currentTarget, evt, newTarget) {
if (currentTarget !== newTarget && !$.contains(currentTarget, newTarget)) {
evt = $.Event(evt.type, evt);
delete evt.target;
$(newTarget).trigger(evt);
}
}
As far as first tests go, I can't see any side-effect or drawback.

Best practices for where to add event listeners

On my page, the user clicks on an element in order to edit it. To facilitate this, I assign the class editable to all such elements.
How should I listen for clicks on all these elements? Currently, I'm doing this:
document.body.addEventListener("click", (event) => {
if (event.target.classList.contains("editable")) {
// do stuff
}
});
The alternative would be to set a listener on every element, like this:
const editables = document.getElementsByClassName("editable");
for (const editable of editables) {
editable.addEventListener("click", editElement);
}
It seems to me that the first way must be better for performance, since it's only one element being listened on, but is it possible to degrade performance by attaching all such events to the body element? Are there any other considerations (e.g. browser implementations of event handling) that I'm neglecting which would suggest doing it the second way?
Short answer: definitely do it the first way. Event delegation is way more performant, but requires extra conditionals in your code, so it's basically a complexity versus performance tradeoff.
Longer Answer: For a small number of elements, adding individual event handlers works fine. However, as you add more and more event handlers, the browser's performance begins to degrade. The reason is that listening for events is memory intensive.
However, in the DOM, events "bubble up" from the most specific target to the most general triggering any event handlers along the way. Here's an example:
<html>
<body>
<div>
<a>
<img>
</a>
</div>
</body>
</html>
If you clicked on the <img> tag, that click event would fire any event handlers in this order:
img
a
div
body
html
document object
Event delegation is the technique of listening to a parent (say <div>) for a bunch of event handlers instead of the specific element you care about (say <img>). The event object will have a target property which points to the specific dom element from which the event originated (in this case <img>).
Your code for event delegation might look something like this:
$(document).ready(function(){
$('<div>').on('click', function(e) {
// check if e.target is an img tag
// do whatever in response to the image being clicked
});
});
For more information checkout Dave Walsh's blog post on Event Delegation or duckduckgo "event delegation".
NOTE ON CODE SAMPLE IN OP: In the first example, target.hasClass('editable') means that the specific thing clicked on must have the class editable for the if block to execute. As one of the commenters pointed out, that's probably not what you want. You might want to try something along these lines instead:
$(document).on('click', function(e) {
if ($(e.target).parents(".editable").length) {
// Do whatever
}
});
Let's break that down a bit:
$(e.target) - anything that on the page that was clicked converted to jQuery
.parents(".editable") - find all the ancestors of the element clicked, then filter to only include ones with the class "editable"
.length - this should be an integer. If 0, this means there are no parents with "editable" class
Another plus point for the first method
I was using the second (alternative) method that you have mentioned I noticed that when the ajax loaded... the newly created elements were not listening the event. I had to redo the for loop after ajax every time.
With the first method which looks like following in my code also works with ajax.
document.addEventListener('click', function (e) {
if (hasClass(e.target, 'classname')) {
// do stuff
}
}, false);
So first one is better

Event not attached

I have a function which dynamically creates an element and attaches a click event to this new element.
In the current state of my app, this function is called 5 times: for the 4 first created elements, all works fine, but the 5th one has no event attached!
I insist: I'm not merely saying that click doesn't work: Using $._data(myElemn,'events') in the console, I get Object { click=[1]} returned for the 4 working elements, but "undefined" for the last one.
Here is the code.
But I don't think is where the problem lies: since it works for other elements, It seems that the difference should come from the particular context of the 5th element.
So my question is rather: can we imagine which particular conditions may cause and event not to be attached (obviously,without any error message).
var createDDT= function(element) { /*
---------
Creates a drop-down toggle button embedded into element.
*/
$(element).css({position:'relative'}) // (relative: since DDT pos is absolute)
.append(
$('<span \/>').addClass(DDT)
.css({display:'none',})
.append($('<span \/>'))
// when click, toggle submenu:
.click(function(event) {
// hide or show current %Submenu:
var submenu=$(event.target).closest('li').find(jqSUBMENU);
submenu.toggleClass(OPEN);
// hide any other %Open %Submenu:
$(jqOPEN).not(submenu).removeClass(OPEN);
setTimeout(liveWidthDisplay,_params.cssTimeout); // adjust LWD's position
return false; // avoid following link, if embedded in <a>
})
);
}
[EDIT] As I previously said, the issue resides probably outside of the function. To emphasize it, in my app I tried replacing the code by the following:
var createDDT= function(element) { /*
---------
Creates a drop-down toggle button embedded into element.
*/
$(element).css({position:'relative'}) // (relative: since DDT pos is absolute)
.append(
$('<span \/>').addClass(DDT)
.css({display:'none',})
.append($('<span \/>'))
// when click, toggle submenu:
.click(function(event) {
alert(event.target.id);
})
);
}
Then the result is unchanged: $._data(myElem,'events') returns "undefined".
Unfortunately, I can't realistically add a significant context into jsFiddle, since it is a huge app.
can we imagine which particular conditions may cause and event not to
be attached
Yes, either you aren't actually passing your 5th element to the createDDT function - or else you have some other code that is destroying the event handler - possibly innerHTML or some such.
In any case a better way to handle this, that will ensure you get the event is to attach a single event handler higher up in the DOM. For example on the body (although any parent element that exists in the DOM will do).
// presuming DDT is a classname such as ".foo"
$('body').on('click', DDT, function(event) {
alert(event.target.id);
});
This way you have a single event that you don't need to create for each element regardless of it is dynamically added to the DOM or not.

How attach document.click event without touching some element?

I need to click on a document to call some function, but the problem is that when I click on some element that want it doesnt react, so the code:
<body>
<div class="some_element">
some element
</div>
</body>
and js:
$(document).click(function(){
//something to happen
})
and now if I click on the div with class="some_element" the document.click event will be called, but I need to call that event ONLY when I click on the document; or it is possible the make this element an exception?
More detailed:
$('#forma').click(function(e){
e.stopPropagation();
$('#assignment_type_list').slideUp();
})
Lets say #forma - its a parent element of those element, so when I click on the page I want to slideUp someElement and:
$('#assignment_type_select, #assignment_type_label').click(function(){
$('#assignment_type_list').slideToggle();
})
this is the elements when they are clicked the other element is toggled, but the problem is that when I click on this elements the $('#forma').click - also executes, because its parent and the e.stopPropagation() - doesn't help.
All this stopPropagation stuff is right, though this'll cause your script to throw errors on older versions of a certain browser. Guess which one? a cross-browser way:
$('#elem').click(function(e)
{
e = e || window.event;//IE doesn't pass the event object as standard to the handler
//event would normally work, but if you declared some event variable in the current scope
//all falls to pieces, so this e || window.event; thing is to be preferred (IMO)
if (e.stopPropagation)//check if method exists
{
e.stopPropagation();
return;
}
e.cancelBubble = true;//for IE
});
However, you wanted to check if the element that was actually clicked, is the one you need. The problem with that is, that the way the event is passed through the DOM. In W3C browsers the event is first passed to the document, and then clambers down to the element that was actually clicked (propagates through the dom). By contrast IE dispatches its events on the element itself, and then sends it up to the document (except for the change event triggered by select elements... to add insult to injury). What this effectively means is that a click event that is registered in to body element in W3C browsers might be on its way to a checkbox of sorts, or it could be a click inside an empty div. Again, in IE, when a click event reaches the body tag, it could have been dispatched too any element on the page. So it may prove useful in your case to google: event delegation, or turn to jQuery's .delegate() method.
Or check the event object to see if the event is allowed to propagate through or not:
var target = e.target || e.srcElement;//target now holds a reference to the clicked element
The property names neatly show the difference between the bubbling model and the propagating one: in the first case (srcElement), the event is coming from a source element in the dom. In the W3C propagating model, the event is cought while it's headed for a target element somewhere in the dom. Look at it like a heat-seeking missile (w3c) versus a shower of debris after the target was shot down (IE, always the destructive one, and in this case often to late to respond to the events, and therefore to late to handle them:P)
One way to do it is to check for the event's target.
$('html').click(function(event){
if (event.target != this){
}else{
//do stuff
}
});
Here's a working fiddle
Elements on the document are part of the document, so if you click "some_element" in the document, it is obvious that event registered on document will be fired/triggered. If you dont want to execute code which was for "document" then first get the element OR "event source" which originates this event, and check if it was "some_element" in your question above.

Categories