I'm trying to create web-page, and to improve performance, I decide to use event delegation, instead of direct binding, but I came across with strange behavior of event delegation or I missed something...
Here is structure of my HTML
<div>
<section class="myClass" data-link="section 1">
<h1>Heading 1</h1>
<p>Some text....</p>
</section>
</div>
I want to entire section was clickable, but while clicking h1 or p element in section event.target on that moment is h1 or p and not section, so expression in if statement fails...
function delegate(ele) {
ele.parentNode.addEventListener("click", function (e) {
if (e.target.nodeName.toLowerCase() === "section" && e.target.classList.contains("myClass")) {
console.log("delegate " + e.target.getAttribute("data-link"));
}
}, false);
}
I already have solution with direct binding, but it would be good is achieved in the same result with event delegation.
Here is jsFiddle
function direct(ele) {
[].forEach.call(ele, function (value, index, array) {
value.addEventListener("click", function(e) {
console.log("direct " + value.getAttribute("data-link"));
}, false);
});
}
Thanks in advance!
Once you handled the click, you can walk the visual tree, starting from the target, to find the information you need.
You might seek by type (node name), by tag, ... for the sake of the example, i just seek on element up to get a 'data-link' attribute if i don't find one on the target, but you have many choices here.
Edit : another idea is to use event.currentTarget, to get the element on which you hooked the event.
The updated fiddle will print :
delegate call on h1 or p of section 1
when you click on h1 or p1
and it will print :
delegate section 1
when you click on the whole section.
http://jsfiddle.net/KMJnA/4/
function delegate(ele) {
ele.parentNode.addEventListener("click", delegateHandler, false);
}
function delegateHandler (e) {
var target = e.target;
var attribute = target.getAttribute("data-link");
// if target has no attribute
// seek it on its parent
if (!attribute) {
attribute = target.parentNode.getAttribute("data-link");
}
if ( target.classList.contains("myClass") ) {
console.log("delegate " + attribute);
}
else console.log('delegate call on h1 or p of ' + attribute);
}
Rq : i didn't get why you hook the event on the parent of the element, might be simpler not to do it. :-)
I found the solution with CSS pointer-events
jsFiddle with CSS
.myClass * {
pointer-events: none;
}
or with JavaScript
jsFiddle with JavaScript
(function () {
"use strict";
var ele = document.querySelector(".myClass").parentNode;
delegate(ele);
function delegate(ele) {
ele.addEventListener("click", function (e) {
var target = e.target;
while (!(target.nodeName.toLowerCase() === "section" && target.classList.contains("myClass"))) {
target = target.parentNode;
if (target === ele) {
break;
}
}
if (target.nodeName.toLowerCase() === "section" && target.classList.contains("myClass")) {
console.log("delegate " + target.getAttribute("data-link"));
}
});
}
}());
Add the event listener to ele instead of ele.parentNode, and use this instead of e.target.
Demo
function delegate(ele) {
ele.addEventListener("click", function (e) {
if (this.nodeName.toLowerCase() === "section" && this.classList.contains("myClass")) {
console.log("delegate " + this.getAttribute("data-link"));
}
}, false);
}
Be aware that e.target is the element the event was dispatched on. But in your case you don't want that, you want the element which has the event handler, so use this (or ele).
Related
I wrote an event delegation function in javascript:
function matches(el, selector) {
var test = (el.matches || el.matchesSelector || el.msMatchesSelector || el.mozMatchesSelector || el.webkitMatchesSelector || el.oMatchesSelector);
if (test)
return test.call(el, selector);
return false;
}
function delegation(node, child, evt, fn, limit) {
node.addEventListener(evt, function (e) {
//maximum number of ancestors i'm going to check
limit = limit ? limit : 2;
e = e || event;
var target = e.target || e.srcElement, i = 0, fire = false;
while (target) {
if (matches(target, child)) {
//break out of the loop if i find the matching DOM element, then fire the event
fire = true;
break;
}
if (i > limit) {
break;
}
i++;
//If event.target doesn't has id/class/tag "child", check its ancestors.
target = target.parentNode;
}
if (fire) {
fn(target, e);
}
}, false);
}
Usage: delegation(document, 'class-or-id-or-tagName', 'event-name', function, query-limit);
It works relatively well until I stumbled upon mouse enter and mouse leave events. The problem is that the events are only triggered when my mouse leave or enter document window, not DOM element, I do understand the problem why but I can't seem to fix it. Is there any way to replicate
$(document).on('mouseenter, DOM , function).on('mouseleave', DOM, function);
in pure Javascript.
Edit: Thanks for all the comments, I found out that there's nothing wrong with my code. I just need to use the correct event name when calling the delegation function, mouseenter should be mouseover, mouseleave should be mouseout.
Changing from
delegation(document, '.some-class-name', 'mouseenter', function(){});
to
delegation(document, '.some-class-name', 'mouseover', function(){});
works wonder.
I need to translate this jquery to vanilla js:
$(document).on('click', 'a', function(){
//do something
});
I've tried with
document.addEventListener('click', function(e) {
if(e.target.tagName === 'A')
{
//do something
}
});
But it's not working if the element clicked is a child of an a, for example
<a href="...">
<!-- if I click on the image, e.target.tagName === 'IMG' -->
<img src="img.jpg">
</a>
I can't use document.getElementsByTagName('a'), because It should work even with those created dynamically.
Also, I'd need to access the href property of the a.
What is the simplest way to do this?
In modern browsers you can use Element.closest() - Not supported in IE
document.addEventListener('click', function(e) {
e.preventDefault();
if (Element.prototype.closest) {
if (e.target.closest('a')) {
console.log('found')
}
} else {
//else the long way
var el = e.target;
while (el && el.tagName != 'A') {
el = el.parentNode;
}
if (el) {
console.log('found')
}
}
});
<a href="...">
This and image
<img src="img.jpg">
</a>
Not here
Take the DOM for a Walk
You have a anchor tag that contains an image and you want catch click events.
The currentTarget property (as suggested in a comment) is not useful since the handler is attached to document, i.e., currentTarget = document.
The solution is to catch clicks on the image and then walk up the DOM tree to check if the parent element is an anchor tag.
The code below illustrates how you might accomplish this check using a while loop. It also displays target, currentTarget, and parentElement. As you can see, clicking on the text within the link produces different output than clicking on the image.
Run the snippet to try
document.addEventListener('click', function(e) {
var t = e.target;
while (t) {
if (t.tagName === 'A') {
// do something ...
debug.innerHTML += (
'target = ' + e.target +
'\ncurrentTarget = ' + e.currentTarget +
'\nparentElement.tagName = ' + t.tagName + '\n'
);
break;
}
t = t.parentElement;
}
});
// dynamically add link with image
var a = document.createElement('A');
a.href = 'javascript:void(0)';
a.innerHTML = 'Click Me!<br><img src="http://lorempixel.com/100/50">';
document.getElementById('content').appendChild(a);
img {width:100px;height:50px;background-color:aliceblue;}
<span id="content"></span>
<xmp id="debug"></xmp>
I need to delegate a 'tap' event to a close button in a custom element, and in turn call a close() method on the root element. Here is an example:
xtag.register('settings-pane', {
lifecycle: {
created: function () {
var tpl = document.getElementById('settings-pane'),
clone = document.importNode(tpl.content, true);
this.appendChild(clone);
}
},
events: {
'tap:delegate(button.close)': function (e) {
rootElement.close(); // <- I don't know the best way to get rootElement
}
},
methods: {
close: function () {
this.classList.add('hidden');
}
}
});
<template id="settings-pane">
<button class="close">✖</button>
</template>
Hey there, this is the library author, let me clear this up:
For any listener you set in the DOM, X-Tag or vanilla JS, you can always use the standard property e.currentTarget to access the node the listener was attached to. For X-Tag, whether you're using the delegate pseudo or not, e.currentTarget will always refer to your custom element:
xtag.register('x-foo', {
content: '<input /><span></span>',
events: {
focus: function(e){
// e.currentTarget === your x-foo element
},
'tap:delegate(span)': function(e){
// e.currentTarget still === your x-foo element
// 'this' reference set to matched span element
}
}
});
Remember, this is a standard API for accessing the element an event listener was attached to, more here: https://developer.mozilla.org/en-US/docs/Web/API/Event/currentTarget
The best approach to this problem, after dealing with it for several months, is to create another "pseudo", similar to the :delegate() pseudo, but that calls the callback differently. This adds the :descendant() pseudo to xtag:
xtag.pseudos.descendant = {
action: function descendantAction(pseudo, event) {
var match,
target = event.target,
origin = target,
root = event.currentTarget;
while (!match && target && target != root) {
if (target.tagName && xtag.matchSelector(target, pseudo.value)) match = target;
target = target.parentNode;
}
if (!match && root.tagName && xtag.matchSelector(root, pseudo.value)) match = root;
return match ? pseudo.listener = pseudo.listener.bind({component: this, target: match, origin: origin}) : null;
}
};
You would use this exactly as you would :delegate(), but this will reference an object which will in turn reference the target element (the element matching the CSS selector e.g. button.close), the origin (the element that received the event), and the component (the custom element itself). Usage example:
xtag.register('settings-pane', {
methods: {
close: function () {
this.classList.add('hidden');
}
},
events: {
'tap:descendant(button.close)': function (e) {
this.origin; // <button> or some descendant thereof that was tapped
this.target; // <button> element
this.component; // <settings-pane> element
this.component.close();
}
}
});
I have searched for a good solution everywhere, yet I can't find one which does not use jQuery.
Is there a cross-browser, normal way (without weird hacks or easy to break code), to detect a click outside of an element (which may or may not have children)?
Add an event listener to document and use Node.contains() to find whether the target of the event (which is the inner-most clicked element) is inside your specified element. It works even in IE5
const specifiedElement = document.getElementById('a')
// I'm using "click" but it works with any event
document.addEventListener('click', event => {
const isClickInside = specifiedElement.contains(event.target)
if (!isClickInside) {
// The click was OUTSIDE the specifiedElement, do something
}
})
var specifiedElement = document.getElementById('a');
//I'm using "click" but it works with any event
document.addEventListener('click', function(event) {
var isClickInside = specifiedElement.contains(event.target);
if (isClickInside) {
alert('You clicked inside A')
} else {
alert('You clicked outside A')
}
});
div {
margin: auto;
padding: 1em;
max-width: 6em;
background: rgba(0, 0, 0, .2);
text-align: center;
}
Is the click inside A or outside?
<div id="a">A
<div id="b">B
<div id="c">C</div>
</div>
</div>
You need to handle the click event on document level. In the event object, you have a target property, the inner-most DOM element that was clicked. With this you check itself and walk up its parents until the document element, if one of them is your watched element.
See the example on jsFiddle
document.addEventListener("click", function (e) {
var level = 0;
for (var element = e.target; element; element = element.parentNode) {
if (element.id === 'x') {
document.getElementById("out").innerHTML = (level ? "inner " : "") + "x clicked";
return;
}
level++;
}
document.getElementById("out").innerHTML = "not x clicked";
});
As always, this isn't cross-bad-browser compatible because of addEventListener/attachEvent, but it works like this.
A child is clicked, when not event.target, but one of it's parents is the watched element (i'm simply counting level for this). You may also have a boolean var, if the element is found or not, to not return the handler from inside the for clause. My example is limiting to that the handler only finishes, when nothing matches.
Adding cross-browser compatability, I'm usually doing it like this:
var addEvent = function (element, eventName, fn, useCapture) {
if (element.addEventListener) {
element.addEventListener(eventName, fn, useCapture);
}
else if (element.attachEvent) {
element.attachEvent(eventName, function (e) {
fn.apply(element, arguments);
}, useCapture);
}
};
This is cross-browser compatible code for attaching an event listener/handler, inclusive rewriting this in IE, to be the element, as like jQuery does for its event handlers. There are plenty of arguments to have some bits of jQuery in mind ;)
How about this:
jsBin demo
document.onclick = function(event){
var hasParent = false;
for(var node = event.target; node != document.body; node = node.parentNode)
{
if(node.id == 'div1'){
hasParent = true;
break;
}
}
if(hasParent)
alert('inside');
else
alert('outside');
}
you can use composePath() to check if the click happened outside or inside of a target div that may or may not have children:
const targetDiv = document.querySelector('#targetDiv')
document.addEventListener('click', (e) => {
const isClickedInsideDiv = e.composedPath().includes(targetDiv)
if (isClickedInsideDiv) {
console.log('clicked inside of div')
} else {
console.log('clicked outside of div')
}
})
I did a lot of research on it to find a better method. JavaScript method .contains go recursively in DOM to check whether it contains target or not. I used it in one of react project but when react DOM changes on set state, .contains method does not work. SO i came up with this solution
//Basic Html snippet
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="mydiv">
<h2>
click outside this div to test
</h2>
Check click outside
</div>
</body>
</html>
//Implementation in Vanilla javaScript
const node = document.getElementById('mydiv')
//minor css to make div more obvious
node.style.width = '300px'
node.style.height = '100px'
node.style.background = 'red'
let isCursorInside = false
//Attach mouseover event listener and update in variable
node.addEventListener('mouseover', function() {
isCursorInside = true
console.log('cursor inside')
})
/Attach mouseout event listener and update in variable
node.addEventListener('mouseout', function() {
isCursorInside = false
console.log('cursor outside')
})
document.addEventListener('click', function() {
//And if isCursorInside = false it means cursor is outside
if(!isCursorInside) {
alert('Outside div click detected')
}
})
WORKING DEMO jsfiddle
using the js Element.closest() method:
let popup = document.querySelector('.parent-element')
popup.addEventListener('click', (e) => {
if (!e.target.closest('.child-element')) {
// clicked outside
}
});
To hide element by click outside of it I usually apply such simple code:
var bodyTag = document.getElementsByTagName('body');
var element = document.getElementById('element');
function clickedOrNot(e) {
if (e.target !== element) {
// action in the case of click outside
bodyTag[0].removeEventListener('click', clickedOrNot, true);
}
}
bodyTag[0].addEventListener('click', clickedOrNot, true);
Another very simple and quick approach to this problem is to map the array of path into the event object returned by the listener. If the id or class name of your element matches one of those in the array, the click is inside your element.
(This solution can be useful if you don't want to get the element directly (e.g: document.getElementById('...'), for example in a reactjs/nextjs app, in ssr..).
Here is an example:
document.addEventListener('click', e => {
let clickedOutside = true;
e.path.forEach(item => {
if (!clickedOutside)
return;
if (item.className === 'your-element-class')
clickedOutside = false;
});
if (clickedOutside)
// Make an action if it's clicked outside..
});
I hope this answer will help you !
(Let me know if my solution is not a good solution or if you see something to improve.)
I want to listen for events on <p> elements at window or document level as there are too many such elements to attach an onclick event hander for each.
This is what I've got:
window.onload=function()
{
window.addEventListener('click',onClick,false);
}
function onClick(event)
{
alert(event.target.nodeName.toString());
}
I need advice on the code above, is it good?
And also, how can I check if the clicked element is a <p> element other than checking nodeName?
For example, if the <p> element contains a <b> element and that is clicked, the nodeType will be b not p.
Thank you.
I think it is good, and you can perform checking this way
var e = event.target
while (e.tagName != 'P')
if (!(e = e.parentNode))
return
alert(e)
If I was you I'd register for the "load" event in the same way that you are for "click". It's a more modern way of event handling, but might not be supported by some older browsers.
Checking the nodeName is the best way to interrogate the node:
function onClick(event) {
var el = event.target;
while (el && "P" != el.nodeName) {
el = el.parentNode;
}
if (el) {
console.log("found a P!");
}
}
Consider this:
(function () {
// returns true if the given node or any of its ancestor nodes
// is of the given type
function isAncestor( node, type ) {
while ( node ) {
if ( node.nodeName.toLowerCase() === type ) {
return true;
}
node = node.parentNode;
}
return false;
}
// page initialization; runs on page load
function init() {
// global click handler
window.addEventListener( 'click', function (e) {
if ( isAncestor( e.target, 'p' ) ) {
// a P element was clicked; do your thing
}
}, false );
}
window.onload = init;
})();
Live demo: http://jsfiddle.net/xWybT/