How to trigger mouseover event on stacked SVG elements - javascript

While the CSS tag pointer-events:none will make an SVG element not be the target of mouse events, only one stacked element can be the target of the event. Is there a simple way to make mouse events target ALL elements that are under the cursor, so that a stacked set of elements will all have their events triggered if the event occurs within their bounding box?

There is a method on SVGSVGElement (<svg>) called getIntersectionList() which will return all elements that intersect with a given rectangle.
As an example, here is a code snippet which will give every SVG shape under the click a random colour.
var mysvg = document.getElementById("mysvg");
mysvg.addEventListener('click', function(evt) {
var hitRect = mysvg.createSVGRect();
hitRect.x = evt.clientX;
hitRect.y = evt.clientY;
// (leave width & height as 0)
var elems = mysvg.getIntersectionList(hitRect, null);
for (i=0; i<elems.length; i++) {
// Give each element under the mouse a random fill colour
elems.item(i).setAttribute('fill', "rgb("+rnd()+","+rnd()+","+rnd()+")");
}
});
function rnd() {
return Math.floor(Math.random() * 255.99);
}
<svg id="mysvg">
<rect width="150" height="100" fill="#393"/>
<rect x="120" y="20" width="140" height="130" fill="orange"/>
<rect x="220" y="0" width="80" height="130" fill="red"/>
</svg>
Unfortunately, this currently only works in Chrome (maybe Safari also?). Supposedly FF implemented partial support for this, but it currently seems to have been removed.

Related

How to make SVG Elements draggable [duplicate]

I am trying to follow the html5 drag and drop tutorial here. I could not get the dragstart event to be registered on rect element. If I change the event from draggable to mousedown it calls the handleDragStart handler. Please ignore the additional blank registration in the code.
JSFiddle here
<!DOCTYPE html>
<html><head>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
<style type="text/css" media="screen">
svg rect { cursor: move; }
</style>
</head><body>
<h1>SVG/HTML 5 Example</h1>
<svg id="cvs">
<rect draggable="true" x="0" y="10" width="100" height="80" fill="#69c" />
<rect x="50" y="50" width="90" height="50" fill="#c66" />
</svg>
<script type="text/javascript" src="loc.js"></script>
</body></html>
loc.js
$(document).ready(function() {
function handleDragStart(e) {
log("handleDragStart");
this.style.opacity = '0.4'; // this ==> e.target is the source node.
};
var registercb = function () {
$("#cvs > rect").each(function() {
$(this).attr('draggable', 'true');
});
$("#cvs > rect").bind('dragstart', handleDragStart);
$(window).mousedown(function (e) {
});
$(window).mousemove(function (e) {
});
$(window).mouseup(function (e) {
log("mouseup");
});
};
function log() {
if (window.console && window.console.log)
window.console.log('[XXX] ' + Array.prototype.join.call(arguments, ' '));
};
registercb();
});
I know this is an old question, I arrived here from this other question that was marked as a duplicate of this one, and wanted to add a possible solution that doesn't require jQuery or any libraries, and that works in all major browsers. It is based on this tutorial recommended by #AmirHossein Mehrvarzi.
This small solution doesn't use the drag events, just the mousedown, mouseup and mousemove. This is how it works:
When the mouse is down on the rectangle, it saves the mouse position and the active element.
When the mouse moves, the rectangle coordinates are updated with the new mouse position.
When the mouse is up, it resets the active element.
From the code in the question above:
var selectedElement = null;
var currentX = 0;
var currentY = 0;
$(document).ready(function() {
function handleDragStart(e) {
log("handleDragStart");
this.style.opacity = '0.4'; // this ==> e.target is the source node.
};
var registercb = function () {
$("#cvs > rect").mousedown(function (e) {
// save the original values when the user clicks on the element
currentX = e.clientX;
currentY = e.clientY;
selectedElement = e.target;
}).mousemove(function (e) {
// if there is an active element, move it around by updating its coordinates
if (selectedElement) {
var dx = parseInt(selectedElement.getAttribute("x")) + e.clientX - currentX;
var dy = parseInt(selectedElement.getAttribute("y")) + e.clientY - currentY;
currentX = e.clientX;
currentY = e.clientY;
selectedElement.setAttribute("x", dx);
selectedElement.setAttribute("y", dy);
}
}).mouseup(function (e) {
// deactivate element when the mouse is up
selectedElement = null;
});
};
function log() {
if (window.console && window.console.log)
window.console.log('[XXX] ' + Array.prototype.join.call(arguments, ' '));
};
registercb();
});
rect { cursor: move; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<h1>SVG/HTML 5 Example</h1>
<svg id="cvs">
<rect x="0" y="10" width="100" height="80" fill="#69c" />
<rect x="50" y="50" width="90" height="50" fill="#c66" />
</svg>
You can also see it on this JSFiddle: http://jsfiddle.net/YNReB/61/
If you want to add drop functionality, you can modify the mouseup function to read the element on the cursor position (with document.elementFromPoint(e.clientX, e.clientY)) and then you can perform actions on the original element and the one where it was dropped.
This behavior may be caused by several reasons:
HTML 5 drag and drop is a mess, according to this article. I know it's a bit older, but the issue still seems not to be solved
jQuery does not support the SVG DOM-model. Therefore some parts of it may work, others don't (like offset() or width()).
I'd definitely not rely on HTML5 drag & drop support right now, but rather use a library to handle this issue. If you want to work with SVG, you could try Raphaël. If you need jQuery too, maybe the SVG plugin is the way to go. Note that both projects are not actively developed at the moment.
I know this is not really a satisfactory answer, but I had to learn too, that jQuery and SVG do not go that well together. Hopefully someone proves me wrong ;).

How to catch SVG click events that hit the viewbox, rather than elements on it?

I have an SVG file with various paths, it is embedded into an HTML page using the object tag. Javascript is used to provide some interactivity to each path - when it is clicked, a tooltip rect is shown. This is what it looks like:
I want the tooltip to disappear when someone clicks outside of the path the tooltip is associated with, this is implemented by adding such an event listener to every path:
path.addEventListener("click", function(event){
if (!isTipShown()){
createTooltip()
}
else{
hideTooltip()
}
})
isTipShown, createTooltip and hideTooltip are functions that check the SVG DOM and modify it accordingly.
This works, but it fails if the click goes to the empty space between the paths themselves - because there is no object to catch it.
What approach can be chosen to implement such functionality?
My current thoughts:
Create a transparent rectangle that covers the entire viewport, and use that as a click target. How would one ensure the rectangle goes to the bottom of everything?
A click handler for the entire HTML document does the trick, but only if users click outside of the viewport itself.
Tooltip on shapes not my SVG!
How to best remove the tooltip when pressing the svg?
They way i would solve it is:
ID on the tooltip.
Modify the existing tooltip with the ID.
Remove tooltip with ID when pressed anywhere else
By reusing the tooltip, there can only be one tooltip on the page at one time.
Removing the tooltip (not deleting) makes it possible to reuse the same tooltip again when a new path is presed.
Here is an example:
document.addEventListener("DOMContentLoaded", function() {
var pathRed = document.getElementById("red");
var pathOrange = document.getElementById("orange");
var pathBlue = document.getElementById("blue");
var paths = [pathRed, pathOrange, pathBlue];
var toolTip = document.createElement("div");
toolTip.id = "toolTip";
var svg = document.getElementById("box");
var shown = false;
paths.forEach(function(element) {
element.addEventListener("click", function(event) {
if (shown == false) {
toolTip.innerText = element.id;
toolTip.style.top = (event.pageY) + "px";
toolTip.style.left = (event.pageX) + "px";
document.body.appendChild(toolTip);
shown = true;
//Only click the path
event.stopPropagation();
} else {
removeToolTip();
}
});
});
svg.addEventListener("click", function(event) {
removeToolTip();
event.preventDefault();
});
function removeToolTip() {
shown = false;
if (document.body.contains(toolTip)) {
document.body.removeChild(toolTip);
}
}
});
#toolTip {
position: absolute;
background-color: #00000099;
padding: 2px;
border-radius: 2px;
color: white;
font-size: 20px;
}
<h1>Click the boxes!</h1>
<svg id="box" viewBox="0 0 15 15" width="250px">
<path id="red" fill="red" d="m0,0 5,0 0,5 -5,0Z"/>
<path id="orange" fill="orange" d="m5,5 5,0 0,5 -5,0Z"/>
<path id="blue" fill="blue" d="m10,10 5,0 0,5 -5,0Z"/>
</svg>
The solution is to make sure the rectangle goes below the paths, as if it were a bottom layer.
SVG does not have a concept of layers, but it can be achieved by making sure that the rect is before all the elements in the SVG DOM, and all subsequent elements will be placed on top of it, visually:
<rect x="0" y="0" width="30" height="30" fill="purple"/>
<rect x="20" y="5" width="30" height="30" fill="blue"/>
<rect x="40" y="10" width="30" height="30" fill="green"/>
<rect x="60" y="15" width="30" height="30" fill="yellow"/>
<rect x="80" y="20" width="30" height="30" fill="red"/>
Here is how this was accomplished in practice (the svgDoc variable is the root SVG element):
function createBackgroundRectangle(svgDoc){
var rect = svgDoc.createElementNS("http://www.w3.org/2000/svg", 'rect')
rect.setAttributeNS(null, 'height', 500)
rect.setAttributeNS(null, 'width', 900)
rect.setAttributeNS(null, 'id', 'pseudo-background')
rect.setAttributeNS(null, 'x', 0)
rect.setAttributeNS(null, 'y', 0)
// the opacity is set to 0, so it doesn't get in the way visually. For debugging
// purposes, you can change it to another value and see the actual rectangle
rect.setAttributeNS(null, 'style', 'opacity:0;fill:#ffd42a;fill-opacity:1;')
svgDoc.rootElement.insertBefore(rect, svgDoc.rootElement.children[0])
}
svgDoc.rootElement.insertBefore(rect, svgDoc.rootElement.children[0]) makes it the first, as it is inserted before the current child at index 0.

How to zoom in on a polygon in D3.js

I am trying to figure out how to do the same zooming behavior as shown in the example below, but with a normal polygon instead of the geo paths.
https://bl.ocks.org/mbostock/4699541
I have seen some answers here on SO that kind of address this, but the animation is choppy or jumps around strangely.
The html I have is
<div id="map-container">
<svg version="1.1"
xmlns="http://www.w3.org/2000/svg"
id="canvas"
viewBox="0 0 4328 2880">
<defs>
<pattern id="mapPattern"
patternUnits="userSpaceOnUse"
x="0"
y="0"
width="4328"
height="2880">
<image xlink:href="/development/data/masterplan.png"
x="0"
y="0"
width="4328"
height="2880"></image>
</pattern>
</defs>
<g id="masterGroup">
<rect fill="url(#mapPattern)"
x="0"
y="0"
width="4328"
height="2880" />
</g>
</svg>
I would like to be able to add some polygons in the same group as the map rectangle and then zoom on the polygon's boundary. Can anyone please show me a fiddle of such behaviour?
I should also add that I do not want to use the scroll wheel or panning. Just zooming in on a clicked polygon and then zooming out on another click.
Maybe this will help you. I answered a question here earlier today : D3js outer limits
Here is the fiddle I put together : http://jsfiddle.net/thatOneGuy/JnNwu/921/
I have added a transition : svg.transition().duration(1000).attr('transform',function(d){
Notice if you click one of the nodes the area moves to cater for the size of the new layout.
The basics are explained in the link to the question, but basically I got the bounding box and translated the SVG accordingly. So I translated and scaled to the size of the new rectangle.
Take a look, quite easy to understand. Here is the main part of the transition :
svg.transition().duration(1000).attr('transform',function(d){
var testScale = Math.max(rectAttr[0].width,rectAttr[0].height)
var widthScale = width/testScale
var heightScale = height/testScale
var scale = Math.max(widthScale,heightScale);
var transX = -(parseInt(d3.select('#invisRect').attr("x")) + parseInt(d3.select('#invisRect').attr("width"))/2) *scale + width/2;
var transY = -(parseInt(d3.select('#invisRect').attr("y")) + parseInt(d3.select('#invisRect').attr("height"))/2) *scale + height/2;
return 'translate(' + transX + ','+ transY + ')scale('+scale+')' ;
})
So with your code, your rectAttr values as seen in the snippet above would be the values retrieved from the getBoundingClientRect() of your polygon : x, y, width and height.
Where I have used d3.select('#invisRect'), this should be your boundingBoxRect() also. And the rest should just work.
EDIT
Here are the edits I made with the fiddle provided : http://jsfiddle.net/thatOneGuy/nzt39dym/3/
I used this function to get the bounding box of the polygon and set the rectangles values accordingly :
var bbox = d3.select(polygon).node().getBBox();
var rectAttr = {
x: bbox.x,
y: bbox.y,
width: bbox.width,
height: bbox.height,
};

How to Drag & Drop SVG elements? [duplicate]

I am trying to follow the html5 drag and drop tutorial here. I could not get the dragstart event to be registered on rect element. If I change the event from draggable to mousedown it calls the handleDragStart handler. Please ignore the additional blank registration in the code.
JSFiddle here
<!DOCTYPE html>
<html><head>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
<style type="text/css" media="screen">
svg rect { cursor: move; }
</style>
</head><body>
<h1>SVG/HTML 5 Example</h1>
<svg id="cvs">
<rect draggable="true" x="0" y="10" width="100" height="80" fill="#69c" />
<rect x="50" y="50" width="90" height="50" fill="#c66" />
</svg>
<script type="text/javascript" src="loc.js"></script>
</body></html>
loc.js
$(document).ready(function() {
function handleDragStart(e) {
log("handleDragStart");
this.style.opacity = '0.4'; // this ==> e.target is the source node.
};
var registercb = function () {
$("#cvs > rect").each(function() {
$(this).attr('draggable', 'true');
});
$("#cvs > rect").bind('dragstart', handleDragStart);
$(window).mousedown(function (e) {
});
$(window).mousemove(function (e) {
});
$(window).mouseup(function (e) {
log("mouseup");
});
};
function log() {
if (window.console && window.console.log)
window.console.log('[XXX] ' + Array.prototype.join.call(arguments, ' '));
};
registercb();
});
I know this is an old question, I arrived here from this other question that was marked as a duplicate of this one, and wanted to add a possible solution that doesn't require jQuery or any libraries, and that works in all major browsers. It is based on this tutorial recommended by #AmirHossein Mehrvarzi.
This small solution doesn't use the drag events, just the mousedown, mouseup and mousemove. This is how it works:
When the mouse is down on the rectangle, it saves the mouse position and the active element.
When the mouse moves, the rectangle coordinates are updated with the new mouse position.
When the mouse is up, it resets the active element.
From the code in the question above:
var selectedElement = null;
var currentX = 0;
var currentY = 0;
$(document).ready(function() {
function handleDragStart(e) {
log("handleDragStart");
this.style.opacity = '0.4'; // this ==> e.target is the source node.
};
var registercb = function () {
$("#cvs > rect").mousedown(function (e) {
// save the original values when the user clicks on the element
currentX = e.clientX;
currentY = e.clientY;
selectedElement = e.target;
}).mousemove(function (e) {
// if there is an active element, move it around by updating its coordinates
if (selectedElement) {
var dx = parseInt(selectedElement.getAttribute("x")) + e.clientX - currentX;
var dy = parseInt(selectedElement.getAttribute("y")) + e.clientY - currentY;
currentX = e.clientX;
currentY = e.clientY;
selectedElement.setAttribute("x", dx);
selectedElement.setAttribute("y", dy);
}
}).mouseup(function (e) {
// deactivate element when the mouse is up
selectedElement = null;
});
};
function log() {
if (window.console && window.console.log)
window.console.log('[XXX] ' + Array.prototype.join.call(arguments, ' '));
};
registercb();
});
rect { cursor: move; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<h1>SVG/HTML 5 Example</h1>
<svg id="cvs">
<rect x="0" y="10" width="100" height="80" fill="#69c" />
<rect x="50" y="50" width="90" height="50" fill="#c66" />
</svg>
You can also see it on this JSFiddle: http://jsfiddle.net/YNReB/61/
If you want to add drop functionality, you can modify the mouseup function to read the element on the cursor position (with document.elementFromPoint(e.clientX, e.clientY)) and then you can perform actions on the original element and the one where it was dropped.
This behavior may be caused by several reasons:
HTML 5 drag and drop is a mess, according to this article. I know it's a bit older, but the issue still seems not to be solved
jQuery does not support the SVG DOM-model. Therefore some parts of it may work, others don't (like offset() or width()).
I'd definitely not rely on HTML5 drag & drop support right now, but rather use a library to handle this issue. If you want to work with SVG, you could try Raphaël. If you need jQuery too, maybe the SVG plugin is the way to go. Note that both projects are not actively developed at the moment.
I know this is not really a satisfactory answer, but I had to learn too, that jQuery and SVG do not go that well together. Hopefully someone proves me wrong ;).

How to use the SVG checkintersection() function correctly?

I'm having a problem with the SVG checkintersection() function. All I want to do is to check whether a small SVG-rectangle intersects the area of an SVG-path, but I can't figure out what to call the function on (I already tried to call it on the SVG DOM object, among several other things google turned up).
So what I need to know is what to put in for the placeholder ("foo") in this snippet:
var closedPath = document.getElementById(closedPath);
var rectangle = document.getElementById(rectangle);
if (foo.checkIntersection(closedPath, rectangle)) {
//do stuff
};
with the HTML being something along the lines of
<html>
<body>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="svgroot">
<g>
<path id="closedPath" fill="{$c5}" d="M-250179-46928l-5051 1351l-867-1760l-33-146l-12-99l-82-678l-17-249l86-644l305-1800l158-2882l75-1425l-47-280l-22-131l-137-411l-300-892l1273 620l931-109l1957-734l1860-1096l292-192l884 547l2690 2153l480 963l36 244l-948 1878l-376 591l-60 567l-72 1147l97 847l-222 334l-122 117l-2403 2093l-353 76z"/>
<rect id="rectangle" fill="white" x="-126828" y="0" width="45000" height="45000"/>
</g>
</svg>
</body>
</html>
Any help would be much appreciated!
Edit: Just wanted to add that I now use a workaround, which consists of converting the SVG path to an array of point coordinates using a parser function I wrote, which is then put into a simple coordinate-test function.
Also this may have been a solution Hit-testing SVG shapes?
checkIntersection is a method on the <svg> element so you'd want something like this...
var svg = document.getElementById("svgroot");
var closedPath = document.getElementById(closedPath);
var rectangle = document.getElementById(rectangle);
var rect = svg.createSVGRect();
rect.x = rectangle.animVal.x;
rect.y = rectangle.animVal.y;
rect.height = rectangle.animVal.height;
rect.width = rectangle.animVal.width;
svg.checkIntersection(closedPath, rect) {
// do stuff
}
Note also how the second argument has to be an SVGRect and not an element.
SVG elements support SMIL animation, you could equally well write rectangle.baseVal.x etc but that wouldn't necessarily reflect the rectangle's current position if you were animating the rectangle. If you're not using SMIL then rectangle.baseVal.x = rectangle.animVal.x
Because a <rect> can have things like rounded corners it doesn't have an SVGRect interface so you have to convert from the interface it does have (SVGRectElement) to the one you need (SVGRect)
<svg width="390" height="248" viewBox="-266600, -68800, 195000, 124000" version="1.1" xmlns="http://www.w3.org/2000/svg">
<path id="closedPath" fill="#ff9966" d="M-250179-46928l-5051 1351l-867-1760l-33-146l-12-99l-82-678l-17-249l86-644l305-1800l158-2882l75-1425l-47-280l-22-131l-137-411l-300-892l1273 620l931-109l1957-734l1860-1096l292-192l884 547l2690 2153l480 963l36 244l-948 1878l-376 591l-60 567l-72 1147l97 847l-222 334l-122 117l-2403 2093l-353 76z"/>
<rect id="rectangle" fill="#66ff66" x="-126828" y="0" width="45000" height="45000"/>
</svg>
<script>
var rectangle = document.getElementById('rectangle');
var closedPath = document.getElementById('closedPath');
var svgRoot = closedPath.farthestViewportElement;
var rect = svgRoot.createSVGRect();
rect.x = rectangle.x.animVal.value;
rect.y = rectangle.y.animVal.value;
rect.height = rectangle.height.animVal.value;
rect.width = rectangle.width.animVal.value;
var hasIntersection = svgRoot.checkIntersection(closedPath, rect);
console.log(hasIntersection);
</script>

Categories

Resources