Move element to front without breaking events - javascript

I'm trying to fix some event code that I'm working on. In this particular case I need to be able to subscribe to a click event on an svg:circle. However there is also a need to move the circle to the top of the z-index on mousedown such that the element can be dragged over the top of other elements.
The way this is done is to take the element out of the DOM and re-insert it in the correct place using a helper function that I'm using from http://bl.ocks.org/eesur/4e0a69d57d3bfc8a82c2 . The problem with doing this is that the event chain seems to have broken taking the element out of the dom preventing the click event firing.
I'm wondering if anyone can come up with a better way of doing this which will ensure click fires correctly yet still allows changing of the z-index somewhere during the drag lifecycle?
This small example shows how the z-index changes, but the click events don't fire in the console. Clicking on the element again once it's on top does then fire the click correctly.
d3.selectAll("circle")
.on("mousedown", function() {
d3.select(this).moveToFront();
})
.on("click", function() {
var fill = d3.select(this).style("fill");
console.log("You clicked on : " + fill);
});
d3.selection.prototype.moveToFront = function() {
return this.each(function() {
this.parentNode.appendChild(this);
});
};
.red {
fill: red;
}
.blue {
fill: blue;
}
.green {
fill: green;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.3.13/d3.min.js"></script>
<svg width="600" height="600">
<circle class="red" cx="50" cy="50" r="50" />
<circle class="blue" cx="100" cy="100" r="50" />
<circle class="green" cx="150" cy="150" r="50" />
</svg>

I've managed to come up with a slightly wacky idea, which seems to work, although I'm a little anxious about the performance if there are many elements within the DOM.
The idea is essentially, instead of moving the selection (on mousedown) to the top, to move everything else (of the same type, in this case svg:circle) behind the element that was mouse-downed upon.
d3.selectAll("circle")
.on("mousedown", function() {
var that = this;
d3.select(this.parentNode)
.selectAll("circle")
.filter(function() { return this !== that; })
.moveBehind(that);
})
.on("click", function() {
var fill = d3.select(this).style("fill");
console.log("You clicked on : " + fill);
});
d3.selection.prototype.moveToFront = function() {
return this.each(function() {
this.parentNode.appendChild(this);
});
};
d3.selection.prototype.moveBehind = function(element) {
return this.each(function() {
this.parentNode.insertBefore(this, element);
});
};
.red {
fill: red;
}
.blue {
fill: blue;
}
.green {
fill: green;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.3.13/d3.min.js"></script>
<svg width="600" height="600">
<circle class="red" cx="50" cy="50" r="50" />
<circle class="blue" cx="100" cy="100" r="50" />
<circle class="green" cx="150" cy="150" r="50" />
</svg>

I wondered if the problem was with d3 so I wrote the equivalent in pure JS and had exactly the same result on Chrome as you did with d3. I was able to get it to work by replacing click with mouseup and comparing it with the mousedown element, like in my comment. You stated in your comment that this solution was not possible in your case, due to other project restrictions. I thought that I'd post my solution anyway, as it was mentioned that behaviour was different in FF and IE.
const circles = Array.from(document.getElementsByTagName('circle'));
let mousedown;
for (let circle of circles) {
circle.addEventListener('mousedown', (e) => {
mousedown = e.target;
e.target.parentNode.appendChild(e.target);
}, false);
circle.addEventListener('mouseup', (e) => {
if (mousedown === e.target) {
console.log('You clicked on : ' + window.getComputedStyle(e.target).fill);
}
mousedown = null;
}, false);
}
.red {
fill: red;
}
.blue {
fill: blue;
}
.green {
fill: green;
}
<svg width="600" height="600">
<circle class="red" cx="50" cy="50" r="50" />
<circle class="blue" cx="100" cy="100" r="50" />
<circle class="green" cx="150" cy="150" r="50" />
</svg>

Related

onmouseenter event triggered on any movement in an SVG use element

When the "mouseenter" event is triggered on the green circle a 1 is printed in the console, when it is triggered on the blue circle a 2 is printed in the console. Note that when the mouse enters the green circle, 1 is printed exactly once. When the mouse enters the blue circle any mouse movement within the circle causes a 2 to be printed.
var gr = document.getElementById("greenOne");
var bl = document.getElementById("blueOneCopy");
gr.onmouseenter = function() {gr.parentNode.appendChild(gr); console.log(1);};
bl.onmouseenter = function() {bl.parentNode.appendChild(bl); console.log(2);};
<html>
<head>
<script src="https://d3js.org/d3.v3.min.js"></script>
</head>
<body>
<svg height="500px" width="500px">
<defs>
<circle id="blueOne" cx="100" cy="100" r="50" style="fill: #0091EA"></circle>
</defs>
<use id="blueOneCopy" href="#blueOne"></use>
<circle id="greenOne" cx="150" cy="100" r="50" style="fill: #00C853"></circle>
</svg>
</body>
</html>
I've managed to get blueOneCopy to behave similarly to greenOne by checking whether the relatedTarget property of the mouse event isn't blueOne before executing the rest of the function. I'm not sure if this is the best solution, but it appears to work.
var gr = document.getElementById("greenOne");
var bl = document.getElementById("blueOneCopy");
gr.onmouseenter = function() {
gr.parentNode.appendChild(gr);
console.log(1);
};
bl.onmouseenter = function(e) {
if (e.relatedTarget.id !== "blueOne") {
bl.parentNode.appendChild(bl);
console.log(2);
}
};
<html>
<head>
<script src="https://d3js.org/d3.v3.min.js"></script>
</head>
<body>
<svg height="500px" width="500px">
<defs>
<circle id="blueOne" cx="100" cy="100" r="50" style="fill: #0091EA"></circle>
</defs>
<use id="blueOneCopy" href="#blueOne"></use>
<circle id="greenOne" cx="150" cy="100" r="50" style="fill: #00C853"></circle>
</svg>
</body>
</html>
Another option is to check whether the element is the last one in the SVG before moving it there. See below
I expect that this issue will have something to do with how events behave when Shadow DOM elements are attached to the DOM. Since Chrome and Firefox behave the same, I expect it is behaviour described in the spec. I'm just too lazy right now to check :)
Note that, with both mine and RFoxtea's solutions, the mouseenter event is still being fired. We are just detecting the first occurrence. Be careful you don't slow down your app by putting code in the event that unnecessarily runs every time.
var gr = document.getElementById("greenOne");
var bl = document.getElementById("blueOneCopy");
gr.onmouseenter = bringToFront;
bl.onmouseenter = bringToFront;
function bringToFront(evt) {
let el = evt.target;
if (el.nextSibling != null) {
el.parentNode.appendChild(el);
console.log(el.id == 'greenOne' ? 1 : 2);
}
};
<html>
<head>
<script src="https://d3js.org/d3.v3.min.js"></script>
</head>
<body>
<svg height="500px" width="500px">
<defs>
<circle id="blueOne" cx="100" cy="100" r="50" style="fill: #0091EA"></circle>
</defs>
<use id="blueOneCopy" href="#blueOne"></use>
<circle id="greenOne" cx="150" cy="100" r="50" style="fill: #00C853"></circle>
</svg>
</body>
</html>

How to change color of svg-element back and fourth by clicking?

I want to make an SVG-circle (a sun) that goes from yellow to grey by clicking on it, and then goes back to yellow if you click it again (and so on). I have in different ways been able to make the sun change color by clicking it once, but it won't change back on the second click.
Why doesn't this work? How am I supposed to do it?
Sorry if I am making this post in a wrong way, this is my first post here.
// Trying to make it as one function with if-else
$(document).ready(function(){
$("#sun").click(function() {
if ($(this).attr("fill") == "yellow") {
$(this).css({ fill: "grey" });
$(this).css({ stroke: "grey" });
} else {
$("#sun").css({ fill: "yellow" });
$("#sun").css({ stroke: "orange" });
}
});
});
// Trying to have the two clicks in different functions
$(document).ready(function(){
$("#sun").click(function() {
if ($(this).attr("fill") == "yellow") {
$(this).css({ fill: "grey" });
$(this).css({ stroke: "grey" });
}
});
});
$(document).ready(function(){
$("#sun").click(function() {
if ($(this).attr("fill") == "grey") {
$("#sun").css({ fill: "yellow" });
$("#sun").css({ stroke: "orange" });
}
});
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.0/jquery.min.js"></script>
<svg width="300" height="200">
<circle id="sun" cx="220" cy="40" r="20" stroke="orange" stroke-width="4" fill="yellow" />
</svg>
// Trying to make one function with if-else
$(document).ready(function(){
$("#sun").click(function() {
if ($(this).attr("fill") == "yellow") {
$(this).attr("fill", "grey");
$(this).css({ stroke: "grey" });
} else {
$("#sun").attr("fill", "yellow");
$("#sun").css({ stroke: "orange" });
}
});
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.0/jquery.min.js"></script>
<svg width="300" height="200">
<circle id="sun" cx="220" cy="40" r="20" stroke="orange" stroke-width="4" fill="yellow" />
</svg>
It didn't work because $(this).css change the inline style (check the html with inspect in your snippet) instead use $(this).attr to the change the fill attr (not the style fill)
You can toggle a class like so:
sun.addEventListener("click",()=>{sun.classList.toggle("night")})
#sun{stroke:orange;fill:yellow;}
#sun.night{stroke:black;fill:silver;}
<svg width="300" height="200">
<circle id="sun" cx="220" cy="40" r="20" stroke-width="4" fill="yellow" />
</svg>

How to switch position of D3 SVG elements

I have some SVG elements on my page that I created with D3. All are children of one parent SVG. Each contains some other D3 elements like paths and text. On the click of a button, I want two of these child SVGs to switch positions, so they move up or down on the page (all are placed above/below each other).
I already tried creating groups ("g") instead of the child SVGs and accessing/changing their positions. However, I can't seem to access the y position of the element.
I also tried using "insertAfter" but this only works with divs, not with SVGs (however, I'm looking for a similar behaviour).
$(".move_up").click(function() {
var svg = $("#second_child"); //ID is actually retrieved from an attribute of the button
svg.insertBefore(svg.prev()); //does obviously not work
});
HTML for move up button (one per child SVG exists):
<a class="move_up">
<span class="grey glyphicon glyphicon-chevron-up" title="Move up"></span>
</a>
HTML for SVG:
<div>
<svg id="parent">
<svg id="first_child"
<path></path>
<rect></rect>
<text></text>
...
</svg>
<svg id="second_child"
<path></path>
<rect></rect>
<text></text>
...
</svg>
<rect></rect>
<text></text>
...
</svg>
</div>
I want the first and second child SVGs to switch positions, when the move up (or respectively a move down) button is used.
This is what I ended up doing:
let group = $("#first_group");
let next_group = $('#second_group");
let diff = 58; // Height of a group
let translate_y = 0;
let translate_y_next = 0;
if (next_group.offset()) {
if (group.attr("transform")) {
let string = group.attr("transform");
translate_y = parseInt(string.substring(string.indexOf("(")+1, string.indexOf(")")).split(",")[1]);
}
if (prev_group.attr("transform")) {
let string_next = prev_group.attr("transform");
translate_y_next = parseInt(string_next.substring(string_next.indexOf("(")+1, string_next.indexOf(")")).split(",")[1]);
}
group.attr("transform", `translate(0, ${translate_y + diff})`);
next_group.attr("transform", `translate(0, ${translate_y_next - diff})`);
}
Works similar for a "Move up" button. Just make sure to change the sign in the last two lines!
May not be super elegant, but does the job.
You are using an SVG as wrapper and the positions are different to html. In SVG You need to define the X and Y position.
let ids = ['ex1', 'ex2', 'ex3', 'ex4', 'ex5']
let btn = document.getElementById('move')
const sortArrayAsYouWish = (array) => {
array.sort(() => Math.random() - 0.5);
}
const changeOrder = () => {
let posY = 35
sortArrayAsYouWish(ids) // change order
ids.forEach((id, i) => {
let $el = document.getElementById(id)
$el.style.transform = `translate(0, ${posY*i}px)`
})
}
btn.onclick = changeOrder
changeOrder()
svg {
width: 500px;
height: 340px;
border: solid 1px #ccc;
}
g {
transition: transform 0.4s;
}
text {
fill: #fff;
text-anchor: middle;
}
#ex2 rect {
fill: blue;
}
#ex3 rect {
fill: yellow;
}
#ex4 rect {
fill: red;
}
#ex5 rect {
fill: cyan;
}
<div><button id="move">Move</button></div>
<svg>
<g id="ex1">
<rect width="120" height="30" x="0" y="0" />
<text x="60" y="15">Hello example 1</text>
</g>
<g id="ex2">
<rect width="120" height="30" x="0" y="0" />
<text x="60" y="15">Hello example 2</text>
</g>
<g id="ex3">
<rect width="120" height="30" x="0" y="0" />
<text x="60" y="15">Hello example 3</text>
</g>
<g id="ex4">
<rect width="120" height="30" x="0" y="0" />
<text x="60" y="15">Hello example 4</text>
</g>
<g id="ex5">
<rect width="120" height="30" x="0" y="0" />
<text x="60" y="15">Hello example 5</text>
</g>
</svg>

d3 change class of clicked node text

I'm trying to add a class to the label, that was clicked in my force diagram. For this I use a "click" function, which hands the clicked item to the update function (it updates the class of the item). I tried using this but then the console will report "nodeClicked.childNodes[1].classed is not a function".
Then I googled and tried to use "d3.select(this).select("text");" which does not report an error, nor update the text.
A node is a < g > Element which has two children: a circle and the text (the text I want to give a css class)
If you want to, you can look at my code snippet:
// Toggle children on click.
// Also save information about what was clicked and update the GUI
function click(d) {
if (d.children) {
d._children = d.children;
d.children = null;
}
else{
d.children = d._children;
d._children = null;
}
// Save which node was just clicked now
nodeClicked = d3.select(this).select("text");
// Save the d3 data of this node
nodeClickedData = d;
}
To change the class I tested both:
$(nodeClicked).addClass("nodeGreenMarked");
and:
nodeClicked.classed("nodeBlueMarked", true);
But none did anything. If I check in the console what content nodeClicked is, it tells me "Array[1]" and when I open it: "0:< text >"
You can add class in two ways.
d3.select(this).select("text").attr("class",className);
OR
d3.select(this).select("text").classed(className,true);
Working Snippet:
d3.selectAll(".node").on("click", function() {
//check if node is already selected
var text = d3.select(this).select("text");
if (text.classed("selectedText")) {
text.classed("selectedText", false);
//Remove class selectedNode
} else {
text.classed("selectedText", true);
//Adds class selectedNode
}
});
.node {
cursor: pointer;
}
.node circle {
fill: #fff;
stroke: steelblue;
stroke-width: 3px;
}
.node text {
font: 12px sans-serif;
}
.link {
fill: none;
stroke: #ccc;
stroke-width: 2px;
}
.selectedText {
fill: red;
}
<script src="https://d3js.org/d3.v3.min.js"></script>
<svg width="960" height="500">
<g transform="translate(120,20)">
<path class="link" d="M0,262.85714285714283C90,262.85714285714283 90,197.1428571428571 180,197.1428571428571"></path>
<path class="link" d="M0,262.85714285714283C90,262.85714285714283 90,328.57142857142856 180,328.57142857142856"></path>
<path class="link" d="M180,197.1428571428571C270,197.1428571428571 270,131.42857142857142 360,131.42857142857142"></path>
<path class="link" d="M180,197.1428571428571C270,197.1428571428571 270,262.85714285714283 360,262.85714285714283"></path>
<g class="node" transform="translate(180,328.5714416503906)">
<circle r="10" style="fill: rgb(255, 255, 255);"></circle>
<text x="13" dy=".35em" text-anchor="start" style="fill-opacity: 1;">Level 2: B</text>
</g>
<g class="node" transform="translate(180,197.14285278320312)">
<circle r="10" style="fill: rgb(255, 255, 255);"></circle>
<text x="-13" dy=".35em" text-anchor="end" style="fill-opacity: 1;">Level 2: A</text>
</g>
<g class="node" transform="translate(0,262.8571472167969)">
<circle r="10" style="fill: rgb(255, 255, 255);"></circle>
<text x="-13" dy=".35em" text-anchor="end" style="fill-opacity: 1;">Top Level</text>
</g>
<g class="node" transform="translate(360,262.8571472167969)">
<circle r="10" style="fill: rgb(255, 255, 255);"></circle>
<text x="13" dy=".35em" text-anchor="start" style="fill-opacity: 1;">Daughter of A</text>
</g>
<g class="node" transform="translate(360,131.42857360839844)">
<circle r="10" style="fill: rgb(255, 255, 255);"></circle>
<text x="13" dy=".35em" text-anchor="start" style="fill-opacity: 1;">Son of A</text>
</g>
</g>
</svg>
D3 did somehow not correctly address the correct element (the text of the group element), no matter which of the possible methods I used (jQuery did somehow also fail). I must have keeping to oversee something really important, though I checked everything like 5 times.
No matter, I found a working solution: by adressing the DOM elements with pure javascript. The command I used to solve this and get the class applied correctly was
nodeClicked = this;
...
nodeClicked.classList.add("greenNode");
This adds the class greenNode to the group element. In my CSS file I wrote .nodeClicked text and put in the necessary CSS. So if you once also have problems addressing the correct element, you can also try to use this method.

Detect which element is moused over and pass to function

I'm using greensock to animate an svg clippath, and it works great with one clippath, and hardcoded variables. Now I'm needing to add more clippaths, and I need each one to animate independently. So I need to build some sort of function to detect which circle is being moused over/ moused out and then call the timeline, passing it the correct parameters (the clippath and overlay circle). I'm sure I could probably do that with 'this' but I'm still at the point where 'this' makes my brain melt. Here's the codepen I'm working on.
http://codepen.io/kathryncrawford/pen/JYvdzx
HTML
<svg class="svg-graphic" width="500" height="500" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" version="1.1">
<defs>
<clipPath id="clippath">
<circle id="clip1" cx="200" cy="200" r="2.5"/>
<circle id="clip2" cx="400" cy="200" r="3.2"/>
</clipPath>
</defs>
<image class="svg-image1" xlink:href="http://lorempixel.com/300/300/" width="300" height="300" x="80" y="80"/>
<circle id="circle1" fill="#CC66FF" cx="200" cy="200" r="30"/>
<image class="svg-image2" xlink:href="http://lorempixel.com/300/300/" width="300" height="300" x="380" y="80"/>
<circle id="circle2" fill="#CC66FF" cx="400" cy="200" r="30"/>
JS
var clip = document.getElementById("clip1");
var circles = document.getElementById("circle1");
circles.addEventListener("mouseenter", expand);
circles.addEventListener("mouseleave", contract);
var tl = new TimelineMax({paused: true});
tl.to(clip, 0.5, {
attr: {
r: 120
},
transformOrigin: "50% 50%",
ease: Power4.easeInOut
})
.to(circles, 0.5, {alpha:0, ease:Power4.easeInOut}, '-0.1');
function expand() {
tl.play();
}
function contract() {
tl.reverse();
}
All right, this is what I was able to create by forking your pen.
And here is what has changed:
In HTML, I have removed unique IDs set on each of the circle HTML elements present inside the clipPath HTML element i.e. clipPath's children. Instead, I have given all of these circle tags a clip class.
The other circle elements that are siblings of the said clipPath i.e. present on the same level as clipPath element, have been given a circle class.
And as for the image elements, I have done similar thing. Removed unique IDs from them and instead gave them a common svg-image class.
This is HTML done.
In HTML, since the unique IDs have been removed such as #circle1, #circle2, #svg-image1 and #svg-image2, I have removed them from CSS as well and instead applied exactly the same rules they had on the newly created classes i.e. .circle and .svg-image respectively.
In JavaScript, the clip and circle elements as well as the total number of clip elements are first stored in variables clips, circles and numClips respectively.
There is also an empty array of timelines created initially.
Then there is a loop being initiated which goes up till the length of numClips and which does two things:
createTimeline as the name suggests, is supposed to create a TimelineMax instance which looks similar to what you previously had i.e. it adds two tweens, one for animating the opacity on the current circle element (remember, we are inside a loop and we have a reference of current circle element by the use of circles[i]) and the other for animating r of the current clip element.
assignListeners is for listening to mouseenter and mouseleave events on each of the circle elements.
And finally, expand and collapse methods are for playing or reversing the current timeline instance. (again, we have the reference of the timeline that should be playing when hovered or out using timelines[i] reference).
HTML:
<svg class="svg-graphic" width="500" height="500" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" version="1.1">
<defs>
<clipPath id="clippath">
<circle class="clip" cx="200" cy="200" r="20" />
<circle class="clip" cx="400" cy="200" r="20" />
<circle class="clip" cx="600" cy="200" r="20" />
</clipPath>
</defs>
<image class="svg-image" xlink:href="http://lorempixel.com/300/300/" width="300" height="300" x="80" y="80" />
<circle class="circle" fill="#CC66FF" cx="200" cy="200" r="20" />
<image class="svg-image" xlink:href="http://lorempixel.com/300/300/" width="300" height="300" x="380" y="80" />
<circle class="circle" fill="#CC66FF" cx="400" cy="200" r="20" />
<image class="svg-image" xlink:href="http://lorempixel.com/300/300/" width="300" height="300" x="680" y="80" />
<circle class="circle" fill="#CC66FF" cx="600" cy="200" r="20" />
</svg>
CSS:
*{
box-sizing: border-box;
}
body{
margin: 0;
padding: 0
}
.circle{
position: absolute;
margin: 0;
z-index: 1;
clip-path: url("#clippath");
}
.svg-image {
z-index: 3;
clip-path: url(#clippath);
}
svg{
overflow: visible;
}
.svg-graphic {
position: absolute;
}
.imgContainer {
position: relative;
width: 800px;
height: 800px;
}
JavaScript:
var clips = document.getElementsByClassName('clip');
var circles = document.getElementsByClassName('circle');
var numClips = clips.length;
var timelines = [];
for (var i = 0; i < numClips; i += 1) {
createTimeline(i);
assignListeners(i);
}
function createTimeline(i) {
var timeline = new TimelineMax({ paused: true });
timeline.to(circles[i], 0.6, { opacity: 0, ease: Expo.easeInOut }, 0);
timeline.to(clips[i], 0.6, {
attr: { r: 120 },
transformOrigin: '50% 50%',
ease: Expo.easeInOut
}, 0.1);
timelines[i] = timeline;
}
function assignListeners(i) {
(function(i) {
circles[i].addEventListener('mouseenter', function(e) { expand(e, i); }, false);
circles[i].addEventListener('mouseleave', function(e) { contract(e, i); }, false);
}(i));
}
function expand(e, i) { timelines[i].play(); }
function contract(e, i) { timelines[i].reverse(); }
Hope this helps.

Categories

Resources