jmbds
jmbds

Reputation: 113

OnClick not being triggered when using empty SVG nested inside of other SVG

I have 2 svg elements, one empty inside of the other, and I need to get the inner svg element in my event target when I click on it but I can't make it work. The first problem I noticed is that when using Chrome the inner svg is displayed with 0 height and 0 width, something that doesn't happen in Firefox. The second problem is that after I create a onclick listener for my inner svg, the event never triggers.

I also tried:

What I can't do:

Example:

<div>
    <svg id="0" x="0" y="0" height="100%" width="100%" viewBox="0 0 1000 1000">
        <svg id="1" x="0" y="0" height="1000" width="1000" viewBox="0 0 1000 1000"></svg>
    </svg>
</div>

jsfiddle example here

Upvotes: 2

Views: 762

Answers (4)

herrstrietzel
herrstrietzel

Reputation: 17195

That's actually the expected behaviour:

The outer/parent svg is an HTML DOM node - so it will be applied css styles like width, height or background colour.

The inner/nested svg is part of the SVG DOM.

An empty nested <svg> (as well a a <g>) element will have a bounding box of 0 x 0 px.

Although, firefox shows width and height values while inspecting the nested svgs – it also returns 0x0px using

let bb = nestedSvg.getBBox();
console.log(bb)

Add transparent rectangles via javaScript helper

Appending a transparent background rectangle isn't a hack at all.

Set a fill="transparent" - this way, you get a solid/filled area for click/pointer events. (otherwise it's like knocking on a open window).

You can easily apply a helper method like so:

let svgOuter = document.getElementById("svg0");

// add transparent rectangles
let nestedSvgs = svgOuter.querySelectorAll("svg, g");
nestedSvgs.forEach((nestedSvg) => {
  let rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
  rect.setAttribute("x", "0");
  rect.setAttribute("y", "0");
  rect.setAttribute("width", "100%");
  rect.setAttribute("height", "100%");
  rect.setAttribute("fill", "transparent");
  nestedSvg.insertBefore(rect, nestedSvg.children[0]);

  // add click event
  nestedSvg.addEventListener("click", (e) => {
    let id = e.currentTarget.id;
    console.log("current id:", id);
  });
});
*{
  box-sizing:border-box
}

svg{
  border:1px solid #ccc;
}

.svgInner{
  background:red;
  stroke:red;
  stroke-width:1px;
}
<svg class="svgOuter" id="svg0" x="0" y="0" height="100%" width="100%" viewBox="0 0 1000 1000">
  <svg class="svgInner" id="svg1" x="0" y="0" height="100%" width="33.333%" viewBox="0 0 333 1000">
  </svg>
  <svg class="svgInner" id="svg2" x="33.333%" y="0" height="100%" width="33.333%" viewBox="0 0 333 1000">
  </svg>
  <g transform="translate(666.67 0)" id="testGroup"></g>
</svg>

Better use e.currentTarget since e.target will return the <rect> element.

Alternative element intersection check: Document.elementFromPoint() or Document.elementsFromPoint()

You still need a filled background area, but you can also get document.elementsFromPoint() multiple overlapping elements - might be handy in some scenarios.

let svgOuter = document.getElementById("svg0");

/**
 * alternative: Document.elementFromPoint()
 * https://developer.mozilla.org/en-US/docs/Web/API/Document/elementFromPoint
 */
svgOuter.addEventListener("click", (e) => {
  let el = document.elementFromPoint(e.clientX, e.clientY);
  let els = document.elementsFromPoint(e.clientX, e.clientY);
  let ids = els.map(el=>{return el.id? el.id : el.nodeName});
  let parentSVG = el.closest("svg");
  console.log('current element', parentSVG.id);
  console.log('all elements', ids);
});

// add transparent rectangles
let nestedSvgs = svgOuter.querySelectorAll("svg, g");
nestedSvgs.forEach((nestedSvg, i) => {
  let rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
  rect.id= 'rect'+i;
  rect.setAttribute("x", "0");
  rect.setAttribute("y", "0");
  rect.setAttribute("width", "100%");
  rect.setAttribute("height", "100%");
  rect.setAttribute("fill", "transparent");
  nestedSvg.insertBefore(rect, nestedSvg.children[0]);

  // add click event
  nestedSvg.addEventListener("click", (e) => {
    let id = e.currentTarget.id;
    console.log("current id:", id);
  });
});
*{
  box-sizing:border-box
}

svg{
  border:1px solid #ccc;
}

.svgOuter{
  background:#ccc;
}

.svgInner{
  background:red;
  stroke:red;
  stroke-width:1px;
}
<svg class="svgOuter" id="svg0" x="0" y="0" height="100%" width="100%" viewBox="0 0 1000 1000">
  <svg class="svgInner" id="svg1" x="0" y="0" height="100%" width="33.333%" viewBox="0 0 333 1000">
  </svg>
  <svg class="svgInner" id="svg2" x="33.333%" y="0" height="100%" width="33.333%" viewBox="0 0 333 1000">
  </svg>
  <g transform="translate(666.67 0)" id="testGroup"></g>
</svg>

Upvotes: 0

C.K.
C.K.

Reputation: 1569

I've encountered the same problem, and the description from @alehro is absolutely right.

The problem turns out to be that the browser doesn't think the white space in the inner svg as part of it. The empty part of the inner svg is seen to be part of the outter svg. So the solution is pretty simply: add a transparent rectangle convers the whole inner svg, and it will be alright. Do something like this:

innersvg
    .append("rect")
    .attr("x", 0)
    .attr("y", 0)
    .attr("width", innerSvgWidth)
    .attr("height", innerSvgHeight)
    .attr("fill-opacity", 0)
    .attr("fill", "white");

Note: do not set fill to none, since a non-filled rect only has 4 lines but no the inner part.

Upvotes: 0

alehro
alehro

Reputation: 2208

I have the same problem.
Here are results of my investigation.
The click event on nested svg gets triggered when we click on any svg element inside the nested svg. I used two circles to test it.
The event gets not triggered if we click on the empty space withing bounding rectangle of the svg.
The behavior is the same in Chrome and Firefox.
I ended up setting event handler on outermost svg. To get mouse coordinates relative to inner svg I use utility function from d3.js, pointer(event: any, target?: any).

@Michael Mullany recommends not to nest svgs at all. I don't agree with that. I have complex graphical editor with several drawing areas and two types of svg nesting - with viewBox and without it. So, far it is working fine. Definitely better than to have hacks like invisible rectangles.

The only other minor issue I have it is that bounding rectangle of svg in Chrome debugger gets visualized with top-left corner not at its given position. Coordinates origin is correct though. So, the issue affects only debugging.

Speaking strictly, the issue of events not being triggered on empty space depends on definition of what should be considered the area of the svg. We might define the area as union of all content elements. And then, the behavior we observe is absolutely correct.

Upvotes: 2

Atzuki
Atzuki

Reputation: 867

It looks like SVG does not supports the click listeners. But have you tried to put another div around each SVG and add click listener to the div?

<div>
  <div id="svg1">
    <svg id="0" x="0" y="0" height="100%" width="100%" viewBox="0 0 1000 1000">
        <svg id="1" x="0" y="0" height="100%" width="100%" viewBox="0 0 1000 1000"></svg>
    </svg>
  </div>
</div>
document.getElementById("svg1").addEventListener("click", yourFunc)

Upvotes: 0

Related Questions