I am a Javascript beginner trying to use local storage to store a set of button toggle states (on/off). My goal is to be able to toggle a set of svg fills, on and off, and have the states stored, returning to the selected state, after a browser refresh.
//LOOP 1 - GET STORED VALUES AND SET SVGs ACCORDINGLY
function grabStoredStates(){
for (let i=0; i < localStorage.length; i++) {
let key = localStorage.key(i);
let value = localStorage.getItem(key);
if (value == "on") {
//"cbox" below is from LOOP 2 - maybe it's causing the problem
cbox[i].style.fill = "coral";
}
}
}
//LOOP 2 - TOGGLE AND STORE TOGGLE STATES
let cbox = document.querySelectorAll(".toggleMe");
for (let i = 0; i < cbox.length; i++) {
cbox[i].addEventListener("click", function() {
if (cbox[i].style.fill == "coral") {
cbox[i].style.fill = "white";
localStorage.setItem(cbox[i].id, "off");
}else{
cbox[i].style.fill = "coral";
localStorage.setItem(cbox[i].id, "on");
}
});
}
<body onload="grabStoredStates()">
<svg height="300" width="700>
<g pointer-events="all">
<rect class="toggleMe" cursor:pointer; id="rect1" width="100" height="100" x="50" y="80" fill="aliceblue" stroke="black" stroke-width="2" />
<rect class="toggleMe" cursor:pointer; id="rect2" width="100" height="100" x="175" y="80" fill="aliceblue" stroke="black" stroke-width="2" />
<rect class="toggleMe" cursor:pointer; id="rect3" width="100" height="100" x="300" y="80" fill="aliceblue" stroke="black" stroke-width="2" />
<rect class="toggleMe" cursor:pointer; id="rect4" width="100" height="100" x="425" y="80" fill="aliceblue" stroke="black" stroke-width="2" />
</g>
</svg>
</body>
My problem is that if you load the page, and then open local storage in Chrome Dev Tools, (Application > Storage > Local Storage), and then click the rectangles in sequence from left to right you should see this, as they toggle correctly from light blue to orange:
Key Value
rect1 on
rect2 on
rect3 on
rect4 on
In local storage, the keys and values are, to that point, matched properly, showing all squares to be orange, as intended. However, if you then click rect3, (the third rectangle from the left), for instance, toggling it to light blue, with an expected corresponding "off" state -- on refreshing the page, the stored values appear to get jumbled, with the loop apparently renumbering the rectangles as if it was starting from [0] -- ignoring the intended key/value pairing. You then see this:
Key Value [PROBLEM]
rect1 on
rect2 on [ --> Displays as Off]
rect3 off [ --> Displays as on]
rect4 on
Is there any way that I can make the keys and values stay together, regardless of loop called. The "grabStoredStates()" loop appears to work, as tested by manually changing values and refreshing... I suspect, then, that it has to do with the transition between two separate for loops? Maybe the "cbox" loop should also be in a function? I looked at the prior stack overflow "can't use booleans in local storage" answer, but as I'm using "on" and "off" that doesn't seem to apply. I also saw a jQuery answer, but I'm not familiar with that library yet. (I wouldn't, however, turn down a jQuery answer if offered.)
Again, my original goal was to be able to toggle svg fills on and off and have the states stored after browser refresh. I'd love a more elegant way if there is one...
Thanks everyone, in advance, for your help.
Here I tried to make a simpler version. The presentation of the <rect>s relies on an attribute called data-state. When the document is loaded the attributes will be updated (I commented out the code for localStorage and replaced it with the array testArr). An event listener on <g> will handle click events and test if a <rect> was clicked, if so, the attribute is updated/toggled. After that the values are all saved to localStorage/testArr.
var cbox;
var testArr = ['on', 'off', 'off', 'on']; // for testing
document.addEventListener('DOMContentLoaded', e => {
cbox = document.querySelectorAll(".toggleMe");
/* // Code for localStorage:
Array.from(Array(localStorage.length).keys()).forEach(i => {
let key = localStorage.key(i);
let value = localStorage.getItem(key);
cbox[i].attributes['data-state'] = value;
});*/
testArr.forEach((state, i) => cbox[i].attributes['data-state'].value = state); // for testing
document.querySelector('g').addEventListener('click', e => {
if(e.target.classList.contains('toggleMe')){
let currentValue = e.target.attributes['data-state'].value;
e.target.attributes['data-state'].value = (currentValue == 'on') ? 'off' : 'on';
/* // Code for localStorage:
[...cbox].forEach((elm, i) => {
let value = elm.attributes['data-state'].value;
localStorage.setItem(`Rect${i}`, value);
});*/
testArr = [...cbox].map(elm => elm.attributes['data-state'].value); // for testing
console.log(testArr.join(',')); // for testing
}
});
});
rect.toggleMe {
cursor: pointer;
width: 50px;
height: 50px;
stroke: black;
stroke-width: 2px;
}
rect[data-state="on"] {
fill: aliceblue;
}
rect[data-state="off"] {
fill: coral;
}
<body>
<svg height="300" width="300">
<g>
<rect class="toggleMe" data-state="off" x="50" y="80" />
<rect class="toggleMe" data-state="off" x="110" y="80" />
<rect class="toggleMe" data-state="off" x="170" y="80" />
<rect class="toggleMe" data-state="off" x="230" y="80" />
</g>
</svg>
</body>
Related
I currently have a file on Illustrator that needs to be manipulated. In the file (SVG), there are many rectangles or 'paths' which are named as such (paths). On top of these paths, there is text which I want to use in order to rename all of the paths within the document to their specific (ID).
enter image description here ID is simply the text that sits on top of the path in the document. Hopefully you are able to see in the image above what I mean.
Currently that text and those boxes are separate, and I need to rename the boxes with the corresponding text that sits on top of them.
I am currently doing this by manually copy and pasting the text and renaming the boxes.
Does anyone know how to write something which would automate this for me?
I am very new to the programming world. I don't even know where to start.
I was thinking a library on JS like Snap.svg would be good?
Any help would be greatly appreciated as this document is very very big (not just that picture) and it takes a long time to do this.
I have tried looking at Snap.svg but due to my lack of experience I dont know where to start. someone with more experience might find this very simple and take them a few minutes to come up with a solution.
Search for the underlying elements via document.elementsFromPoint()
Keep in mind: svg <text> elements can't be nested within shapes like <rects> – so we can't query for child elements.
document.elementsFromPoint() returns an array of elements at specific point coordinates and is natively supported by all major browsers - no need for snap.svg or othe libraries.
query for all <text> elements
get center coordinates:
let bb = label.getBoundingClientRect();
let x = bb.left + bb.width/2;
let y = bb.top + bb.height/2;
find elements let els = document.elementsFromPoint(x, y)
let svg = document.querySelector('svg');
let labels = document.querySelectorAll('text');
labels.forEach(label => {
let bb = label.getBoundingClientRect();
let x = bb.left + bb.width / 2;
let y = bb.top + bb.height / 2;
// sanitize id string
let id = label.textContent.replaceAll(' ', '-').replaceAll('#', '').toLowerCase();
// filter only geometry elements
let els = document.elementsFromPoint(x, y).filter(el => el instanceof SVGGeometryElement);
// optional: select only first underlying element
let onlyFirstUnderlying = false;
if (onlyFirstUnderlying) {
els = [els[0]];
}
els.forEach((el, i) => {
el.id = id
// add incremental suffix to prevent duplicate ids
if (els.length > 1 && i > 0) {
el.id += '_' + i
}
})
let newSvg = new XMLSerializer().serializeToString(svg);
output.value = newSvg;
})
svg {
max-width: 10em;
height: auto;
border: 1px solid #ccc;
}
text {
font-size: 10px
}
#output {
display: block;
width: 100%;
min-height: 20em;
}
<svg id="svg" viewBox="0 0 100 100">
<rect x="0" y="0" width="50" height="50" fill="yellow" />
<text x="10" y="25">Label 01</text>
<rect x="0" y="50" width="50" height="50" fill="orange" />
<rect x="0" y="50" width="40" height="50" fill="purple" />
<rect x="0" y="50" width="30" height="50" fill="pink" />
<text x="10" y="75">Label 02</text>
</svg>
<h3>Output</h3>
<textarea id="output"></textarea>
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.
I have put together some SVG circles, with the colour of the circles supposed to alternate on each click between blue and red.
I'm getting some inconsistent behaviour from this, as when I click on the circles in my localhost environment, it works, but the colour doesn't change to red until the second click.
When I put the code into jsFiddle, one of the circles works, one doesn't, even though the code is the same, and they both give the same error in the console
[Error] ReferenceError: Can't find variable: changeColor1
onclick (_display:79)
var shapeClick = document.getElementById("circle0").addEventListener("click", changeColor);
var clicks = 0;
var colorToggle = true;
function changeColor() {
var color = colorToggle ? "#ff0000" : "#1dacf9";
circle0.setAttribute('fill', color);
colorToggle = !colorToggle;
}
var shapeClick = document.getElementById("circle1").addEventListener("click", changeColor1);
var clicks = 0;
var colorToggle = true;
function changeColor1() {
var color = colorToggle ? "#ff0000" : "#1dacf9";
circle1.setAttribute('fill', color);
colorToggle = !colorToggle;
}
<svg id="table1" width="66%" height="100vh" viewBox="0 0 700 666">
<circle id="circle0" class="circles" cx="170" cy="125" r="20" fill="#1dacf9" stroke="black" stroke-width="2" onclick="changeColor()"/>
<circle id="circle1" class="circles" cx="250" cy="45" r="20" fill="#1dacf9" stroke="black" stroke-width="2" onclick="changeColor1()"/>
</svg>
I'm sure there is a more streamline way to do this, especially if I scale it up and add 100 circles, I can't think of a way to do it without using a massive amount of code.
In your case, you are redefining variables that already exist, and both circles are referencing the same value for their toggle.
var shapeClick = ...
...
var shapeClick = ...
Also, you don't need to do onclick="function()" and addEventListener; they do the same thing.
As far as streamlining, there are a couple things you should do:
Create an object to hold toggle states. Keys should be IDs of the circles, and the values will be true or false to signify each circle's state.
Make one handler function, and use event.target.getAttribute('id') to identify which circle was clicked.
Should end up looking something like this:
var state = {};
var circleClickHandler = function (event) {
var id = event.target.getAttribute('id');
var color = state[id] ? "#ff0000" : "#1dacf9";
event.target.setAttribute('fill', color);
state[id] = !state[id];
};
// loop over each circle on the DOM
document.querySelectorAll('circle').forEach(function (element) {
element.addEventListener('click', circleClickHandler);
});
you are missing you opening svg tag
https://jsfiddle.net/9oagrcfu/
<svg>
<circle id="circle0" class="circles" cx="170" cy="125" r="20" fill="#1dacf9" stroke="black" stroke-width="2" onclick="changeColor()"/>
<circle id="circle1" class="circles" cx="250" cy="45" r="20" fill="#1dacf9" stroke="black" stroke-width="2" onclick="changeColor1()"/>
</svg>
I'm learning CSS and working on a problem involving dynamic color selection of a path.
I've a SVG which can have any number of paths (1+) and there are pre-defined colors which should be assigned to paths depending on their order. (A web service returns a collection of paths).
For simplicity, Let's say I've 4 known paths and there are two colors possible: Green and deepPink.
Question: How can I assign following colors dynamically (without creating individual classes for each path):
Path1: Green
Path2: deepPink
Path3: Green
Path4: deepPink
.path_group {
stroke="black";
fill="none";
stroke-width="2";
}
.myPath{
stroke:deepPink;
stroke-width:1.3;
}
/*
Color1: Green
Color2: deepPink
*/
<svg width="100%" height="100%" viewBox="0 0 260 200"
xmlns="http://www.w3.org/2000/svg">
<g class="path_group" >
<path class="myPath" d="M10,25 L110,10" />
<path class="myPath" d="M10,35 L110,20" />
<path class="myPath" d="M10,45 L110,30" />
<path class="myPath" d="M10,55 L110,40" />
</g>
</svg>
I'm more interested in doing it in natively using CSS/JS instead of adding any framework or library to my project. Suggestion regarding any particular concept is appreciated.
Demo: Codepen
Current approach
this.colors = ["green", "deepPink"];
this.getColor = function (pathIndex) {
return this.colors[pathIndex % this.colors.length];
};
You can use CSS3 :nth-child() selector (w3schools) as this:
.myPath { stroke-width:1.3; }
.path_group .myPath:nth-child(4n+1)
{ stroke:red; }
.path_group .myPath:nth-child(4n+2)
{ stroke:blue; }
.path_group .myPath:nth-child(4n+3)
{ stroke:green; }
.path_group .myPath:nth-child(4n+4)
{ stroke:deepPink; }
See updated codepen here.
I'm developing a map, in Javascript using SVG to draw the lines.
I would like to add a feature where you can search for a road, and if the road is found, a circle appears on the map.
I know i can draw a circle in SVG, but my problem is that, the size of the circle should not change depending on the zoom-level. In other words the circle must have the same size at all times.
The roads on my map have this feature, all i had to do was add
vector-effect="non-scaling-stroke"
to the line attributes..
A line looks like this.
<line vector-effect="non-scaling-stroke" stroke-width="3" id = 'line1' x1 = '0' y1 = '0' x2 = '0' y2 = '0' style = 'stroke:rgb(255,215,0);'/>
The circle looks like this.
<circle id = "pointCircle" cx="0" cy="0" r="10" stroke="red" stroke-width="1" fill = "red"/>
Is it possible to define the circle as "non-scaling" somehow?
It took me a while, but I finally got the math clean. This solution requires three things:
Include this script in your page (along with the SVGPan.js script), e.g.
<script xlink:href="SVGPanUnscale.js"></script>
Identify the items you want not to scale (e.g. place them in a group with a special class or ID, or put a particular class on each element) and then tell the script how to find those items, e.g.
unscaleEach("g.non-scaling > *, circle.non-scaling");
Use transform="translate(…,…)" to place each element on the diagram, not cx="…" cy="…".
With just those steps, zooming and panning using SVGPan will not affect the scale (or rotation, or skew) of marked elements.
Demo: http://phrogz.net/svg/scale-independent-elements.svg
Library
// Copyright 2012 © Gavin Kistner, !#phrogz.net
// License: http://phrogz.net/JS/_ReuseLicense.txt
// Undo the scaling to selected elements inside an SVGPan viewport
function unscaleEach(selector){
if (!selector) selector = "g.non-scaling > *";
window.addEventListener('mousewheel', unzoom, false);
window.addEventListener('DOMMouseScroll', unzoom, false);
function unzoom(evt){
// getRoot is a global function exposed by SVGPan
var r = getRoot(evt.target.ownerDocument);
[].forEach.call(r.querySelectorAll(selector), unscale);
}
}
// Counteract all transforms applied above an element.
// Apply a translation to the element to have it remain at a local position
function unscale(el){
var svg = el.ownerSVGElement;
var xf = el.scaleIndependentXForm;
if (!xf){
// Keep a single transform matrix in the stack for fighting transformations
// Be sure to apply this transform after existing transforms (translate)
xf = el.scaleIndependentXForm = svg.createSVGTransform();
el.transform.baseVal.appendItem(xf);
}
var m = svg.getTransformToElement(el.parentNode);
m.e = m.f = 0; // Ignore (preserve) any translations done up to this point
xf.setMatrix(m);
}
Demo Code
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Scale-Independent Elements</title>
<style>
polyline { fill:none; stroke:#000; vector-effect:non-scaling-stroke; }
circle, polygon { fill:#ff9; stroke:#f00; opacity:0.5 }
</style>
<g id="viewport" transform="translate(500,300)">
<polyline points="-100,-50 50,75 100,50" />
<g class="non-scaling">
<circle transform="translate(-100,-50)" r="10" />
<polygon transform="translate(100,50)" points="0,-10 10,0 0,10 -10,0" />
</g>
<circle class="non-scaling" transform="translate(50,75)" r="10" />
</g>
<script xlink:href="SVGPan.js"></script>
<script xlink:href="SVGPanUnscale.js"></script>
<script>
unscaleEach("g.non-scaling > *, circle.non-scaling");
</script>
</svg>
If you are looking for a fully static way of doing this, you might be able to combine non-scaling-stroke with markers to get this, since the markers can be relative to the stroke-width.
In other words, you could wrap the circles in a <marker> element and then use those markers where you need them.
<svg width="500" height="500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2000 2000">
<marker id="Triangle"
viewBox="0 0 10 10" refX="0" refY="5"
markerUnits="strokeWidth"
markerWidth="4" markerHeight="3"
orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" />
</marker>
<path d="M 100 100 l 200 0" vector-effect="non-scaling-stroke"
fill="none" stroke="black" stroke-width="10"
marker-end="url(#Triangle)" />
<path d="M 100 200 l 200 0"
fill="none" stroke="black" stroke-width="10"
marker-end="url(#Triangle)" />
</svg>
The same can also be viewed and tweaked here. The svg spec isn't fully explicit about what should happen in this case (since markers are not in SVG Tiny 1.2, and vector-effect isn't in SVG 1.1). My current line of thinking was that it should probably affect the size of the marker, but it seems no viewers do that at the moment (try in a viewer that supports vector-effect, e.g Opera or Chrome).
Looks like some work was done in webkit (maybe related to this bug: 320635) and the new transform doesn't stick around when simply appended like that
transform.baseVal.appendItem
This seems to work better. Even works in IE 10.
EDIT: Fixed the code for more general case of multiple translate transformations in the front and possible other transformations after. First matrix transformation after all translates must be reserved for unscale though.
translate(1718.07 839.711) translate(0 0) matrix(0.287175 0 0 0.287175 0 0) rotate(45 100 100)
function unscale()
{
var xf = this.ownerSVGElement.createSVGTransform();
var m = this.ownerSVGElement.getTransformToElement(this.parentNode);
m.e = m.f = 0; // Ignore (preserve) any translations done up to this point
xf.setMatrix(m);
// Keep a single transform matrix in the stack for fighting transformations
// Be sure to apply this transform after existing transforms (translate)
var SVG_TRANSFORM_MATRIX = 1;
var SVG_TRANSFORM_TRANSLATE = 2;
var baseVal = this.transform.baseVal;
if(baseVal.numberOfItems == 0)
baseVal.appendItem(xf);
else
{
for(var i = 0; i < baseVal.numberOfItems; ++i)
{
if(baseVal.getItem(i).type == SVG_TRANSFORM_TRANSLATE && i == baseVal.numberOfItems - 1)
{
baseVal.appendItem(xf);
}
if(baseVal.getItem(i).type != SVG_TRANSFORM_TRANSLATE)
{
if(baseVal.getItem(i).type == SVG_TRANSFORM_MATRIX)
baseVal.replaceItem(xf, i);
else
baseVal.insertItemBefore(xf, i);
break;
}
}
}
}
EDIT2:
Chrome killed getTransformToElement for some reason, so the matrix needs to be retrieved manually:
var m = this.parentNode.getScreenCTM().inverse().multiply(this.ownerSVGElement.getScreenCTM());
It's discussed here and here
It looks like current browsers don't do the expected thing, so one needs to apply the inverse transform of the zoom (scale) on the contents of the <marker>, eg. transorm: scaleX(5) on the user of the <marker> etc. will need to be accompanied by a transform: translate(...) scaleX(0.2) inside the <pattern>, also factoring in possible x/y/width/height/transform-origin values inside the pattern if needed