It’s pretty common to use SVG within an anchor link or otherwise “click/tappable thing” on a web page. It’s also increasingly common that the SVG is inline <svg>
, because it’s often nice having the SVG in the DOM since you can style it with CSS and script it with JS and such. But what does that mean for click events?
A link with an SVG icon in it might be like this:
<a href="#0" data-data="something">
<svg ... >
<rect ...>
<svg>
</a>
Now you want to bind a click event to that anchor link. In jQuery:
$("a").on("click", function(event) {
// `this` will always be the <a>
console.log($(this).data("data"));
// "something"
});
That will work perfectly fine. Note there is a data-*
attribute on the anchor link. That’s probably there specifically for JavaScript to access and use. No problem at all how we have it written right now, because within the anonymous function we have bound, this
will always be that anchor link, which has that attribute available. Even if you use event delegation and call some function who-knows-where to handle it, this
will be that anchor link and you can easily snag that data-*
attribute.
But let’s say you’re going to rock some raw JavaScript event delegation:
document.addEventListener('click', doThing);
function doThing(event) {
// test for an element match here
}
You don’t have an easy reference to your anchor link there. You’ll need to test the event.target to see if it even is the anchor link. But in the case of our SVG-in-a-link, what is that target?
It could either be:
- The
<a>
- The
<svg>
- The
<rect>
You might just have to check the tagName of the element that was clicked, and if you know it was a sub-element, move up the chain:
document.addEventListener('click', doThing);
function doThing(event) {
var el;
// we can check the tag type, and if it's not the <a>, move up.
if (event.target.tagType == "rect") {
// move up TWICE
el = event.target.parentElement.parentElement;
} else if (event.target.tagType == "svg") {
// move up ONCE
el = event.target.parentElement;
} else {
el = event.target;
}
console.log(el.getAttribute("data-data"));
}
That’s pretty nuts though. It’s too tied to the HTML and SVG structure. Toss a <g>
in there around some <path>
s, which is a perfectly fine thing to do for grouping, and it breaks. Or the tagType
is path
not a rect
, or any other DOM difference.
Personally I’ve been preferring some CSS solutions.
One way is to lay a pseudo element over top the entire anchor element, so that the clicks are guaranteed to be on the anchor element itself, nothing inside it:
a {
position: relative;
}
a::after {
content: "";
top: 0;
left: 0;
width: 100%;
height: 100%;
}
This seems to work pretty well too:
a > svg {
pointer-events: none;
}
pointer-events typically doesn’t work in IE (it does in 11+, but not lower), but it actually does when applied to SVG, in IE 9+, which is the version of IE that supports SVG anyway.
Here’s a Pen with the issue demonstrated and the fixes:
See the Pen owyFj by Chris Coyier (@chriscoyier) on CodePen.
Point is
If you’re using SVG in a click target, be very careful you’re getting the right element in JavaScript, and if you have trouble, consider a CSS cover.
This problem with raw event delegation could apply to almost any child element(s) in an anchor, not just SVG. I’ve had similar issues with animated buttons where the pointer might start the
mouseDown
in one element (say, an icon graphic) but have themouseUp
in a different element (say, text), which means it won’t trigger a properclick
event. A full-size pseudo element to cover the entire<a>
is a super clever work-around.Doesn’t the event bubble up inside
svg
as it does normally through the DOM? In which case your function could start withif (event.target.tagType !== "a") { return; }
and eventually, even if the click landed onrect
, the event would fire on thea
as it bubbles up.Perhaps I’m missing something…
It might actually. But in the case of event delegation, nothing is bound to the
a
. You need to check the target to determine if you should do anything.It’d be pretty simple to have a catch-all kind of test with jQuery that isn’t tied to the structure of the SVG (although the pseudo-element solution is probably better)
Actually, I just remembered that
.closest
checks the element it’s called on, too, so thatif
block isn’t necessary.I’m not sure i understand the problem being solved..
Exactly Nothing found in Latest Firefox.
I think David is almost there – on at least one occasion what I have done is made this recursive, something like this pseudo code:
function thisHandler(){
if (this != reqElementType){
return thisHandler(this.parentElement);
}else{
return [whatever we need to return]
}
(Plus a little check to break out if we eventually hit DOCUMENT or WINDOW or whatever.)
Prototype.js has a comfortable element method: up().
$(eventObject).up('a')
will return the surrounding<a>
element even if eventObject is the link itself.When using such a central event handler you could tag all event “catching” elements with a pseudo class, e.g. ” evReceiver”:
$(eventObject).up('.evReceiver')
You can even use it with an opposite meaning, e.g. ignore all mousemove events inside a tagged controller (a common problem when constructing self-made dropdown elements).
Of course there’s a similar way to do this with jQuery, but using prototype.js keeps your awareness closer to the underlying DOM, the “everything-is-an-object” policy and prototype extension instead of class/method thinking.
One more subject to remember:
Global event handlers can result in performance issues. If you register lots of event handlers for single elements the JS engine will handle the dispatching internally (which is fast). If you do the dispatching yourself it might be harder for the engine to optimize.
In addition a global event handler fires on many elements you don’t want to handle at all.
If you can, proceed like this:
Register event handlers for each and every element you want to handle. Don’t use inline (lambda) functions; Instead create general functions and reference them in your event registering code. This function caching might be obsolete with modern JS engines, but it’s definitely faster with older browsers.
Why not just this?
Because the
<a>
might not be theparentNode
, it might be deeper than that.The
while
loop should take care of that.Oh right! I didn’t even see that. Derp. Looks like that would work fine. Any reason on parentNode vs parentElement? I guess it doesn’t matter much in this case.
Because “back in my day, we didn’t have no fancy
parentElement
, we only hadparentNode
!” haha**I have something like this on my javascript code:
and it works pretty nice for me! ;)
You can also check the clicked element by using
event.currentTarget
Well, a little bit related with this, using a SVG inside a anchor, if the link already been visited the transition doesn’t work on webkit based browsers. Anyone had the same problem or know any workaround?
To test: http://jsfiddle.net/eEfjS/
I’ve found that applying CSS transitions to SVG can be buggy in general, but that particular trigger is new to me.
I’m wondering if what you’re experiencing might be related to this?
developer.mozilla.org/en-US/docs/Web/CSS/Privacy_and_the_:visited_selector
Gecko and Webkit could be handling this security concern differently.
They fixed it last week! https://code.google.com/p/chromium/issues/detail?id=314892 Will be avaiable in Chrome 37
As Jeremy T said, if you’re using jQuery I’m pretty sure you could just use $(event.target).closest(“a”) to always select the link anchor element. Masking it with pseudo CSS seems a bit hacky to me but it’s very clever.
If you’re willing to spare 210 bytes of JavaScript, polyfill
matches
and prollyfillclosest
.And do whatever you want.
^ posted to GitHub. https://github.com/jonathantneal/closest
In order to get the
::after
pseudo-element to work, I had to addposition: absolute
to it. Also, since I’m applying it to a button with rounded corners, I had to round the pseudo-element’s corners as well to get proper click & hover regions.Also, I’d just like to add that I’m starting to suspect that Chris is actually a Jedi Master. I encountered this problem on May 1st, and immediately after Star Wars day (May the 4th), he posted the solution. :D