SVG <image> element created in JS does not display - javascript

I have an <svg> element (with viewBox set) and I append a programmatically constructed <image> element to it, like this:
const img=document.createElementNS('http://www.w3.org/2000/svg','image');
img.setAttribute('width','100');
img.setAttribute('height','100');
img.setAttribute('xlink:href','data:image/png;base64,iVBORw0KGgoAAA'+
'ANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4'+
'//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU'+
'5ErkJggg==');
document.querySelector('svg').appendChild(img);
It is present in the SVG but nothing is displayed. However when I call img.outerHTML = img.outerHTML; to sort of recreate it from its text representation, it starts displaying properly.
Anyone could help me understand what's going on? How should I create the element in JS to make it display properly?
JSFiddle

For SVG-2 ready browsers you can simply do setAttribute("href", url).
However, in SVG-1.x you must use setAttributeNS('http://www.w3.org/1999/xlink', 'href', url);
Actually with setAttributeNS, you can also use 'xlink:href' for the second parameter or 'foo:href', since setAttributeNS takes care of mapping to the correct NameSpace and kind of discards what comes before :, but setAttribute doesn't and thus creates a kind of null:xlink:href attribute, which doesn't map to anything known by the browser and thus fails.
const url = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==';
const img = document.createElementNS('http://www.w3.org/2000/svg','image');
img.setAttribute('width','100');
img.setAttribute('height','100');
img.setAttribute('href', url); // SVG-2
img.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', url); // SVG-1.x
document.querySelector('svg').appendChild(img);
<svg viewBox="0 0 100 100"></svg>

Related

How can I access SVG elements in html (with CSS preferably) without pasting the code in directly?

I was trying to work through this tutorial as a practice for a bigger project I'm working on.
My actual SVG is really big, so I would rather not just copy paste the whole giant code directly into HTML.
But here's the practice one I made (i'm also not sure how to clean SVGs made in Inkscape, but I deleted some of the stuff that didn't seem necessary.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns="http://www.w3.org/2000/svg"
id="svg1"
version="1.1"
viewBox="49.595489 30.040314 84.135223 84.305336"
height="84.305336mm"
width="84.135223mm">
<path class="path5076"
id="path5076"
d="m 70.303571,78.340773 c -4.032971,0.006 -8.033187,1.698025 -10.862132,4.572387 -2.828946,2.874362 -4.455685,6.891674 -4.445904,10.924637 0.0095,3.927963 1.572604,7.841853 4.315065,10.653953 2.74246,2.8121 6.641232,4.47709 10.569138,4.45364 4.633366,-0.0277 9.108311,-2.43049 12.384652,-5.70683 3.574526,-3.57453 6.411017,-6.242046 9.347584,-9.825986 0,0 7.17598,-6.918764 10.743336,-10.51178 3.56737,-3.593016 7.41006,-7.169152 11.08478,-10.843875 3.34645,-3.346446 6.32139,-6.581106 9.51049,-9.812482 3.3753,-3.420038 5.15813,-7.12199 5.18334,-11.661986 0.0216,-3.889398 -1.60848,-8.155743 -4.38434,-10.880165 -2.77587,-2.724421 -6.6563,-4.279784 -10.54572,-4.261811 -3.8759,0.01791 -7.72562,1.595418 -10.48769,4.314587 -2.762056,2.71917 -5.002206,6.149863 -4.776456,11.428746 -0.0484,4.514439 2.874106,9.098792 5.148056,11.372746 3.19237,3.192372 6.9848,6.227335 10.17717,9.419709 3.20164,3.201638 6.0452,5.990107 9.58187,9.526778 1.80732,1.807321 3.93629,5.149881 4.68721,7.593023 0.75092,2.443141 1.01197,5.054051 0.5999,7.576553 -0.55185,3.378163 -2.33545,6.072793 -4.93781,8.296363 -2.60235,2.22358 -5.80201,3.69214 -9.22483,3.7206 -4.69281,0.039 -9.04011,-1.51725 -12.0905,-4.81311 -3.187696,-3.44421 -7.211206,-7.037566 -10.268806,-10.463896 -3.057595,-3.42633 -6.28628,-6.607684 -9.408672,-9.762441 -3.174881,-3.207791 -7.386446,-5.316042 -11.899731,-5.30936 z"
style="fill:none;fill-opacity:1;stroke:#febc00;stroke-width:10.80000019;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</svg>
In the tutorial it says:
Let's target the path with CSS (assuming we're using inline SVG here,
or via an 'object')
which is great because just uploading svg without copy-pasting is exactly what I wanted. But I'm not sure how to proceed now. From what I could find, the only way to access the inside parts of SVG is through javascript, which I've never done before as I don't know javascript.
So I have this
<object id="svg1" data="path.svg" type="image/svg+xml"></object>
Now I'm trying to figure out how to access the path inside the SVG.
I tried putting in this code that I found, but it didn't do anything.
<script>
window.onload=function() {
var a = document.getElementById("svg1");
var svgDoc = a.contentDocument;
var svgItem = svgDoc.getElementById("path5076");
svgItem.setAttribute("fill", "lime");
};
</script>
I then found someone saying that I had to let the SVG load first, so I added
<script>
var mySVG = document.getElementById("svg1");
var svgDoc;
mySVG.addEventListener("load",function() {
svgDoc = mySVG.contentDocument;
alert("SVG contentDocument Loaded!");
}, false);
</script>
but that also didn't seem to work.
This is my first stab at JS, so any help is appreciated.
But if you know how this can be done with CSS PLEASE let me know, I'm much more comfortable with it (as comfortable as I can be after two weeks of learning it, as opposed to 0 weeks with JS)
Edited as per suggestion, but I think I'm still doing it wrong.
Also, I only left "fill:none;fill-opacity:1;stroke:#febc00;stroke-width:10.80000019" in style and removed everything else. Does that mean I can still assign attributes like stroke-dasharray or do they have to be predefined inside style first?
still hoping someone has some other input
The style precedence, from lowest to highest, is
attributes
CSS style sheets
inline styles
Your SVG uses inline styles (style="fill:none;fill-opacity:1;...).
So your code is correct but no matter what you do with the "fill" attribute, it is overridden by the inline style, so svgItem.setAttribute("fill", "lime"); is useless.
You need to alter the inline style somehow.
The easiest solution is to change the inline style by calling:
svgItem.setAttribute("style", svgItem.getAttribute("style").replace("fill:none;","fill:lime;"))
but you can also remove the inline style completely and work with the attributes/external CSS instead.
another option is to convert inline style to attributes:
<script type="text/javascript">
window.onload=function() {
var a = document.getElementById("svg1");
var svgDoc = a.contentDocument;
var svgItem = svgDoc.getElementById("path5076");
// replace inline style with attributes
var styleText = svgItem.getAttribute("style");
svgItem.removeAttribute("style");
for (let pair of styleText.split(';')) {
let [key, value] = pair.split(':');
svgItem.setAttribute(key,value);
}
// now you are ready to work with the attributes
svgItem.setAttribute("fill", "lime");
};
</script>

Convert svg into base64

SVG which was created programmatically does not convert into base64 correctly.
In my application I've got a service which get a response with g-element and then put into created svg-element and convert it into base64, but if I try to open a link I find that svg does not render on the page.
var xmlns = 'http://www.w3.org/2000/svg',
IMAGE_TEMPLATE = document.createElementNS(xmlns, 'svg');
IMAGE_TEMPLATE.appendChild(document.body.querySelector('#ico-appliance-thermostat-128'));
IMAGE_TEMPLATE.setAttribute('id', 'svg');
IMAGE_TEMPLATE.setAttributeNS(null, 'width', 128);
IMAGE_TEMPLATE.setAttributeNS(null, 'height', 128);
IMAGE_TEMPLATE.setAttributeNS(null, 'viewBox', '0 0 128 128');
document.body.querySelector('#test').appendChild(IMAGE_TEMPLATE);
test = function(){
var s = new XMLSerializer().serializeToString(document.getElementById("svg"))
var encodedData = window.btoa(s);
console.log('data:image/svg+xml;base64,' + encodedData);
}
https://jsfiddle.net/6sra5c5L/
Try adding <svg></svg> around <g> element , closing } at test function ; defining test as a named function
var xmlns = 'http://www.w3.org/2000/svg',
IMAGE_TEMPLATE = document.createElementNS(xmlns, 'svg');
IMAGE_TEMPLATE.appendChild(document.body.querySelector('#ico-appliance-thermostat-128'));
IMAGE_TEMPLATE.setAttribute('id', 'svg');
IMAGE_TEMPLATE.setAttributeNS(null, 'width', 128);
IMAGE_TEMPLATE.setAttributeNS(null, 'height', 128);
IMAGE_TEMPLATE.setAttributeNS(null, 'viewBox', '0 0 128 128');
document.body.querySelector('#test').appendChild(IMAGE_TEMPLATE);
function test() {
var s = new XMLSerializer().serializeToString(document.getElementById("svg"))
console.log(document.getElementById("svg"))
console.log(s)
var encodedData = window.btoa(s);
console.log('data:image/svg+xml;base64,' + encodedData);
}
<svg>
<g id="ico-appliance-thermostat-128" transform="scale(2)">
<path d="M106.949,128.009 L105.294,124.692 C115.967,119.333 123.298,108.278 123.298,95.500 C123.298,82.722 115.967,71.666 105.294,66.308 L106.949,62.990 C118.835,68.958 126.999,81.270 126.999,95.500 C126.999,109.730 118.835,122.042 106.949,128.009 ZM117.376,95.500 C117.376,105.954 111.378,115.000 102.645,119.384 L100.990,116.067 C108.510,112.292 113.676,104.502 113.676,95.500 C113.676,86.497 108.510,78.708 100.990,74.933 L102.645,71.615 C111.378,76.000 117.376,85.045 117.376,95.500 ZM106.999,95.213 C106.999,98.063 104.756,100.373 101.988,100.373 C100.251,100.373 98.720,99.462 97.822,98.080 L91.490,98.080 L85.840,116.427 L85.662,116.427 L85.840,117.000 L80.829,117.000 L70.082,82.422 L65.795,97.506 L65.795,98.080 L54.999,98.080 L54.999,92.920 L62.087,92.920 L67.465,74.000 L72.477,74.000 L83.234,108.615 L88.067,92.920 L89.738,92.920 L93.079,92.920 L97.504,92.920 C98.324,91.222 100.021,90.053 101.988,90.053 C104.756,90.053 106.999,92.363 106.999,95.213 ZM24.999,128.000 C11.787,128.000 0.999,117.189 0.999,103.993 C0.999,96.779 4.177,90.380 8.986,85.988 C8.986,85.173 8.986,84.776 8.986,83.981 L8.986,15.997 C8.986,7.193 16.177,-0.000 24.979,-0.000 C33.780,-0.000 40.972,7.193 40.972,15.997 L40.972,83.981 C40.972,84.796 40.972,85.194 40.972,85.988 C45.780,90.380 48.979,96.779 48.999,103.993 C48.999,117.210 38.212,128.000 24.999,128.000 ZM33.999,90.000 L33.999,17.000 C33.999,12.373 29.662,8.009 24.988,8.009 C20.314,8.009 16.000,12.373 16.000,17.000 L16.000,90.000 C10.903,92.952 7.985,97.813 7.985,104.136 C7.985,113.411 15.641,120.990 25.011,120.990 C34.380,120.990 42.037,113.389 41.992,104.114 C41.992,97.791 39.118,92.952 33.999,90.000 ZM24.999,112.990 C19.904,112.990 15.999,109.082 15.999,103.983 C15.999,100.092 18.383,96.796 21.989,95.588 C21.989,95.290 21.989,95.290 21.989,94.992 L21.989,38.991 C21.989,37.500 23.181,35.994 24.984,35.994 C26.787,35.994 27.979,37.187 27.979,38.991 L27.979,95.008 C27.979,95.306 27.979,95.306 27.979,95.604 C31.585,96.812 33.984,100.107 33.999,103.983 C33.999,109.082 30.095,112.990 24.999,112.990 Z"
style="fill: #5aac21;fill-rule: evenodd;"></path>
</g>
</svg>
<div id="test"></div>
<button onclick="test()">Test</button>
jsfiddle https://jsfiddle.net/6sra5c5L/5/
Difference between #guest271314 and my answer:
#guest271314 his answer:
Wrap the g element inside a svg element to make sure that the browser renders the g element onload.
My answer:
Forces the svg element inside #test to render the g element since the g element wasn't rendered onload.
Best answer in this case: #guest271314
Reason: A g element should be inside a svg element in valid html.
When should my answer be used?
In the case that the g element is not an element in the html document.
You didn't close test() with a }.
Code below returns a base64 encoded svg:
https://jsfiddle.net/seahorsepip/6sra5c5L/1/
Edit:
svg render issue is something I ran into myself before, here's the fix with a line of jquery:
https://jsfiddle.net/seahorsepip/6sra5c5L/3/
//Force refresh svg
$("#test").html($("#test").html());
Here's the original SO thread about the issue: jquery's append not working with svg element?
I don't know the javascript equivalent for the jQuery code I added, I tried to write it but it didn't work :/
Edit 2:
Here's the pure js equivalent:
https://jsfiddle.net/seahorsepip/6sra5c5L/4/
//Force refresh svg
var svg = document.body.querySelector('#test').innerHTML;
document.body.querySelector('#test').innerHTML = "";
document.body.querySelector('#test').innerHTML = svg;
HTML elements and SVG elements have different namespaces. By putting the <g> element in your HTML you have created an <html:g> element. When it is moved inside the <svg>, it is still an <html:g> and won't be recognised by the SVG renderer.
You either have to put it inside soem <svg> tags as #guest271314 described. Or, after you append to the SVG, go through all the elements in the <g> and change all their namespaces to the SVG one.
I don't see a full explanation what actually happened anywhere here, so here it goes.
Browsers parse HTML as HTML
While this seems obvious, it is not so in the case, when you put non-HTML (SVG, XML, ...) elements into it.
Browsers are too nice and when you provide valid SGML (parent of XML, HTML, ...) and they find element that doesn't belong there(<g>) according to what you said will be there (HTML) - they don't complain and only treat the unknown elmenents as unknown HTML elements, resulting in class HTMLUnknownElement.
Browsers generally don't change element object class
So you are basically putting HTMLUnknownElement into SVGSVGElement, which will put the tag content into the svg, but since the underlying object is not a valid subelement of SVG (SVGGElement) it won't do anything.
This is the same reason why dynamically created <svg> has to be created with namespace => the namespace, when combined with <svg> element, is telling browser - hey!, this tag is from SVG specification => treat it as an SVG!
Solutions
Your current example code has a syntax error, which I will not address, since that is not the real issue.
Statically defined <g> template
If the template is already in the original document - force browser to take it as SVGSVGElement by wrapping with <svg xmlns="http://www.w3.org/2000/svg"></svg> (browsers may eat it without xmlns attribute, but it is the safer way).
Worried that it would display? Just hide it with CSS display: none; - it will not affect the rendering.
Dynamically defined <g> template
If you are creating <g> element dynamically, you should create it while specifying the namespace using createElementNS() like:
document.createElementNS('http://www.w3.org/2000/svg', 'g');
Static, but non-changable <g> template
If you are stuck in the middle and can't change the static template, you can still build upon the dynamic approach:
create new <g> with the right namespace
var newGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
get the "raw" old <g> - it will be HTMLUnknownElement, but it is ok, since it is built upon HTMLElement and that gives us enaugh API to transfer all we need
var oldGroup = document.querySelector('oldGroupSelector');
possibly transfer old group attributes to new group - handy is element.attributes
for (var i = 0; i < oldGroup.attributes.length; ++i) {
newGroup.setAttribute(
oldGroup.attributes.item(i).name,
oldGroup.attributes.item(i).value
);
}
Now browser already knows contents of newGroup <g> are actually svg - newGroup is SVGGElement - now we can just refill the contents
newGroup.innerHTML = oldGroup.innerHTML;

How to append an svg loaded by d3.xml into an svg

Trying to load external svgs in to a dynamically created svg but still access the properties of the loaded svgs. That is why I'm using d3.xml but haven't figured out how to integrate it in to the dynamic svg created with d3.
Code and js console here
The code produces this svg
<svg id = "svgObj">
<g class = "grp"></g>
<g class = "grp"></g>
</svg>
I'm trying to load circle.svg within each element so it looks like this
<svg id = "svgObj">
<g class = "grp"><svg id=minus>...</svg></g>
<g class = "grp"><svg id=minus>...</svg></g>
</svg>
I tried the code below but console errors that there is no appendChild method
var grps = d3.selectAll( "g" );
img = grps.appendChild( svgNode.cloneNode( true ) );
Thanks ahead
As #helderdarocha explained, you're mixing up your d3 methods with your plain Javascript methods. That answer gave you how to do it with plain Javascript methods, I'll balance that out by explaining how to do it with d3 methods.
To append a new element within each element of a d3 selection, the method name is simply append, not appendChild. The parameter to append is either a tag name (for which d3 creates a new element of that type for each element in the selection) or a function that returns an actual DOM element (the function will get called for each element in the selection with the data value and index as parameters). Since you're cloning an existing node, that's the version you want to use:
var grps = d3.selectAll( "g" );
img = grps.append( function(){return svgNode.cloneNode( true );} );
I haven't used D3 before but it seems easy to understand. The error message says that Array has no appendChild() method. I read the documentation and discovered that the selections return as double nodes (see Operating on selections) so you would have to add [0][0] (which would select the first node) to be able to use appendChild().
This selects the first node using plain DOM and produces no error (and draws a partial shape on the output):
var grps = d3.selectAll( "g" )[0][0];
Since you need to insert code in each node, you can use each() (see Control) like this:
d3.selectAll( "g" ).each(function() {
img = this.appendChild( svgNode.cloneNode( true ) );
});
I tested it on your code and it produces a black circle with a white dash in the middle. Is that what you expected?

Load SVG into a specific div with Snap SVG

What is the correct way to load an SVG file into a specific div using SnapSVG?
Following the documentation I have this JS:
var s = Snap();
Snap.load("fox.svg", function (f) {
s.append(f.select("g#fox"));
});
This loads the SVG just above the body tag, however if I try to set it's location, nothing happens, there is no error. This is what I have attempted so far:
var s = Snap('#myDiv');
Where am I going wrong?
This should work, its not far removed from your example, so its hard to tell whats wrong with yours without a live example and the svg to look at.
If you want to upload a fiddle or something, it may help.
var s = Snap("#svgdiv");
var l = Snap.load("path.svg", onSVGLoaded ) ;
function onSVGLoaded( data ){
s.append( data );
}
Edit: Just to extend on Duopixels answer, the element you are trying to add, should be an svg element (ie
<svg id="mysvgtoload">...</svg> // you can add an svg to a div
<g id="mygrouptoload">...</g> // you can't add this to a div, but could to an svg element
in the file) or add the element (g or path or whatever) to an existing svg tag/element in your html. I suspect you may be trying to add a element direct to a div, which won't work, but its hard to tell without the file.
Also double check that Snap is loaded fine, and you can do a console.log( data ) in the function to check that it has loaded the markup correct.

How can I programmatically copy all of the style attributes from one DOM element to another

I have a page with two frames, and I need to (via javascript) copy an element and all of its nested elements (it's a ul/li tree) and most importantly it's style from one frame to the other.
I get all the content via assigning innerhtml, and I am able to position the new element in the second frame with dest.style.left and dest.style.top and it works.
But I'm trying to get all the style information and nothing's happening.
I'm using getComputedStyle to get the final style for each source element as I loop through each node then and assigning them to the same position in the destination nodelist and nothing happens to visually change the style.
What am I missing?
With getComputedStyle, you can get the cssText property which will fetch the entire computed style in a CSS string:
var completeStyle = window.getComputedStyle(element1, null).cssText;
element2.style.cssText = completeStyle;
Unfortunately, getComputedStyle isn't supported by Internet Explorer, which uses currentStyle instead. Doubly unfortunate is the fact that currentStyle returns null for cssText, so the same method cannot be applied to IE. I'll try and figure something out for you, if nobody beats me to it :-)
I thought about it and you could emulate the above in IE using a for...in statement:
var completeStyle = "";
if ("getComputedStyle" in window)
completeStyle = window.getComputedStyle(element1, null).cssText;
else
{
var elStyle = element1.currentStyle;
for (var k in elStyle) { completeStyle += k + ":" + elStyle[k] + ";"; }
}
element2.style.cssText = completeStyle;
Have you tried cloneNode? It can copy the element and all of its children in one fell swoop.
http://www.w3schools.com/dom/met_element_clonenode.asp
Well I gave up the original tack of trying to get the [computed] style information out of the source tag, I just never got it to work.
So instead I tried this, and it did work...
First I grabbed the -style- tag from the source frame, then I appendChilded it to the end of the -head- tag of the second frame.
For which this proved useful...
How do you copy an inline style element in IE?
Then I made a few nested div elements, each having an id or style class I needed to inherit so that the hierarchy matched the first frame.
Then I shoved the innerhtml of the source frame's tag that I wanted to copy and then finally appendChilded it to the body of the second frame, and magically, it all worked.
var topd = doc.createElement('div'); // make a div that we can attach all our styles to so they'll be inherited
topd.id = 'menuanchorelement';
// shove our desired tag in the middle of a few nested elements that have all the classes we need to inherit...
topd.innerHTML = "<div id='multi-level'><ul class='menu'>" + dh.innerHTML + "</ul></div>";
doc.body.appendChild(topd); // voila

Categories

Resources