Is it possible to access the SMIL timer from javascript? - javascript

I'm trying to use SMIL to animate the typing of text into a field embedded in a SVG. I tried the following code in both Chrome and a SMIL-enable Firefox nightly, but it has no effect:
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:html="http://www.w3.org/1999/xhtml">
<foreignObject>
<html:input type="text" value="">
<set attributeName="value" to="Hello World"
begin="0" dur="10s" fill="freeze" />
</html:input>
</foreignObject>
</svg>
The text field appears, but remains empty. So, I thought I would register for the beginEvent and do the substitution manually. To test the events, I added:
<rect id="rect" x="0" y="0" width="10" height="10">
<animate id="dx" attributeName="x" attributeType="XML"
onbegin="console.log('onbegin')"
begin="0s" dur="1s" fill="freeze" from="0" to="-10" />
</rect>
As well as the javascript that made sense from the event model:
window.addEventListener( 'load', function() {
function listen( id ) {
var elem = document.getElementById( id )
elem.addEventListener( 'beginEvent', function() {
console.log( 'begin ' + id )
}, false )
elem.addEventListener( 'endEvent', function() {
console.log( 'end ' + id )
}, false )
}
listen( 'rect' )
listen( 'dx' )
})
But there's no events fired on either the rect or the animate in either browser. The next logical step seems to be to simulate the animation (ala. FakeSmile), but I want to use the browser's animation timer if at all possible.

RE your <set attributeName="value"> — you can't use SMIL to animate attributes of HTML elements, even if they're HTML elements embedded in SVG. (That would be a cool future extension, but its behavior isn't defined[1], so it would be a bit experimental at this point.)
RE the onbegin — yeah, Firefox doesn't fire animation events yet — that's yet-to-be-implemented.
[1] The SVG spec explicitly defines which SVG attributes and properties are animatable and which aren't. (see e.g. the "Animatable: " field below every attribute on w3.org/TR/SVG11/text.html) It does not define that for other languages (e.g. HTML), nor does HTML (because HTML doesn't have an animation component), so it's unclear which HTML attributes would be animatable in the first place.

If the textfield appears but the foreignObject element lacks a width and height attribute technically the browser that does that doesn't follow the SVG spec, since those attributes are required for the foreignObject to be visible inside the SVG.
Also the animations elements in SVG can be used on svg elements, but the SVG spec doesn't define if/how they apply to other types of markup.
An option if you want to use the animation timer might be to create an short repeating animation, e.g an animate element that animates some random attribute that isn't visible, and use the repeatEvent events as a trigger, calling back to a javascript function that modifies the html element(s).

Related

querySelector after using case sensitive setAttributeNS

If I have a case sensitive attribute like nativeAttr I can then use querySelector to find the element by its attribute name.
But if I programmatically add a case sensitive attribute like setAttr, then querySelector no longer finds the html element.
How can I set the case sensitive attribute and also make it work with querySelector?
const node = document.getElementById('node')
node.setAttributeNS(null, 'setAttr', 'set')
console.log('nativeAttr', document.querySelector('[nativeattr]')?.id)
console.log('nativeAttr', document.querySelector('[nativeAttr]')?.id)
console.log('setAttr', document.querySelector('[setattr]')?.id)
console.log('setAttr', document.querySelector('[setAttr]')?.id)
// the attribute is set in camelCase correctly
console.log(node.getAttributeNS(null, 'setAttr'))
// here are the names of the attributes; seems that nativeAttr is lowercase
console.log('nativeAttr', node.attributes[1].name, node.attributes[1].localName)
console.log('setAttr', node.attributes[2].name, node.attributes[2].localName)
<div id="node" nativeAttr="native"></div>
Case sensitive attributes are used by svg elements, so it's a valid use case. For example:
// only one viewBox element
console.log(document.querySelectorAll('[viewBox]').length)
// add the viewBox attribute to the second svg
const svg2 = document.getElementById('svg2')
svg2.setAttributeNS(null, 'viewBox', '0 0 50 50')
// now both svg elements show up
console.log(document.querySelectorAll('[viewBox]').length)
<svg id="svg1" width="30" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="50"/>
</svg>
<svg id="svg2" width="30" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="50"/>
</svg>
So, as you can see it works for svg, but not for regular html elements.
According to the spec, uppercase letter are allowed in the attribute names.
In the HTML syntax, attribute names, even those for foreign elements, may be written with any mix of ASCII lower and ASCII upper alphas.
Attributes are always processed in lower case, so if you provide the attribute nativeAttr, this will be transformed to nativeattr.
As it is described in the HTML standard:
All attribute names on HTML elements in HTML documents get ASCII-lowercased automatically
It is recommended to use kebab-case with attribute names. Check out this minimal example below:
const el = document.getElementById('node');
console.log('before', el.getAttributeNS(null, 'native-attr' ))
el.setAttributeNS(null, 'native-attr', 'changed');
console.log('after', el.getAttributeNS(null, 'native-attr' ))
<div id="node" native-attr="native"></div>
Also read here: are html5 data attributes case insensitive?
The appropriate character for selecting namespaced attributes is the pipe character (|). If there is no namespace prefix, you just use nothing before the pipe, so [*|setAttr] is supposed to select your element. However, in my testing, it does not do so, via CSS nor querySelector:
const node = document.getElementById('node');
node.setAttributeNS(null, 'setAttr', 'set');
node.setAttributeNS('https://example.com/namespace', 'ns:someAttr', 'set');
console.log('html', node.outerHTML);
console.log('nativeAttr', document.querySelector('[|nativeAttr]')?.id);
console.log('setAttr', document.querySelector('[*|setAttr]')?.id);
console.log('someAttr', document.querySelector('[*|someAttr]')?.id);
#namespace ns url("https://example.com/namespace");
div[|nativeAttr] { color: blue; }
div[*|setAttr] { color: red; }
div[ns|someAttr] { color: green; }
<div id="node" nativeAttr="native">Ran</div>
The syntax does not work even when a prefix and namespace is provided, so that seems to be a red herring as well.
This answer is less an answer as it is documentation of findings, along with code demonstrating what should work. The tests were run in Microsoft Edge Version 97.0.1072.62 (Official build) (64-bit) on Window 10 Version 10.0.19043.1466.

how to change an attribute of an svg file with javascript?

I know this has been answered before but no solutions are working for me. I want to render an svg file on an html page and access its code elements from javascript. I want to change the attributes of specific elements. Specifically I want to change add an attribute style="fill:#FFFFFF;". Later I will want to remove this. I want to access the element with the id property of the element. I tried doing this with a plethora of javascript code and rendering the svg as an img and an object and nothing works. Is this possible? If so how? Thanks in advance.
You can create a react component for your SVG and control their properties with props in react way:
const SampleIcon = ({fill='red',backgroundColor='white', width='300', height='200'}) => (
<svg version="1.1"
width={width} height={height}
xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill={backgroundColor} />
<text x="150" y="125" font-size="60" text-anchor="middle" fill={fill}>SVG</text>
</svg>
)
Now use it:
const App = () => (
<div>
<SampleIcon fill='green' backgroundColor='#ff0'/>
<SampleIcon with={400} height={300}/>
</div>
)
There is no way to change the fill property of an SVG you want to display in an img tag. However, you can change the filter property.
So what you want to do is display the svg file like so:
<img class="svg-icon" src="./your.svg"/>
You have to give your img tag a classname (also, do not set the fill property inline).
Then you can simply apply a filter and the color of the svg will be changed, like so:
.your-icon {
filter: invert(14%) sepia(95%) saturate(6069%) hue-rotate(360deg) brightness(108%) contrast(117%);
}
The above filter is red. Use this hex to filter converter to find the right color for you: hex to filter.
Here is a codesandbox demo.

How to access the description of a svg element in js?

svg elements can have descriptions as described here
https://developer.mozilla.org/en-US/docs/Web/SVG/Element/desc
Assuming I got the handle of the circle via getElementById, how can I access the content of the <desc> tag?
Adding an id to the tag cannot be a solution given the SVG comes from an editor that allows me to add id's in the elements, but not to their descriptions.
PS. It's not relevant, but the idea is to add javascript code in the descriptor to be executed (via Eval) to update the owner element.
Thanks!
You can retrieve the content of tag <desc> with textContent.
I made an example for you with the ability to change the content of the <desc> tag through <textarea>, by pressing a button
The first console.log() displays the default content of the <desc> tag.
All subsequent console.log() show modified content.
let desc = document.querySelector('desc');
let textarea = document.querySelector('textarea');
let button = document.querySelector('button');
console.log(desc.textContent.trim());
button.onclick = function() {
desc.textContent = textarea.value;
console.log(desc.textContent.trim());
}
<textarea></textarea>
<button>change desc</button>
<svg viewBox="0 0 10 10" xmlns="http://www.w3.org/2000/svg">
<circle cx="5" cy="5" r="4">
<desc>
I'm a circle and that description is here to
demonstrate how I can be described, but is it
really necessary to describe a simple circle
like me?
</desc>
</circle>
</svg>
assuming you've the circle then calling
let desc = circle.getElementsByTagName("desc");
will get you all the desc elements that are descentants of the circle element.
You can loop over those and do what you want with them. Note that you'll get a collection of desc elements so if there's only one element it will be the first and only element in that collection.

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;

Is there some innerHTML replacement in SVG/XML?

In HTML I can build a simple templating system by providing a template in form of a string, replace some parts of it and then assign it using innerHTML to some container.
var templ = '<span>{myText}</span>'
var newContent = templ.replace( '{myText}', someVariable );
document.querySelector( '#myContainer' ).innerHTML = newContent;
This way I can take advantage of the browser's HTML parser and do not have to repeatedly use document.createElement(). The later can be quite cumbersome, if the templates grows beyond a few elements.
In SVG, however, there is no property on the elements as innerHTML or even innerSVG for that matter.
So my question is: Is there anything I can use in SVG ro resemble the approach from the example above or am I stuck with document.createElement() (or respectivly some lib that uses it)?
As always with my questions: Vanilla JavaScript solutions are preferred, but any pointer to a lib providing a solution is appreciated.
You can use DOMParser to parse XML. You can then use importNode to get that into your existing document if you want via importNode to end up with something like this...
var doc = new DOMParser().parseFromString(
'<svg xmlns="http://www.w3.org/2000/svg"><circle cx="100" cy="100" r="20"/></svg>',
'application/xml');
someElement.appendChild(
someElement.ownerDocument.importNode(doc.documentElement, true));
Check out the innerSVG javascript shim, it provides the functionality you want.
2014 update: The DOM parsing spec defines innerHTML and outerHTML on Element, which makes these available on svg and xml elements. This has been shipping in Blink for a while now, first versions to support this was Chrome 32 / Opera 19, more details can be found in this bugreport.
Here I write a dirty way...
innerHTML workaround for SVG
http://jsfiddle.net/microbians/8ztNU/
<html>
<body>
<svg id="svgcanvas">
</svg>
<script>
var twocircles='<circle cx="253" cy="562" r="10" stroke="black" stroke-width="2" fill="red"></circle> \
<circle cx="353" cy="562" r="10" stroke="black" stroke-width="2" fill="red"></circle>'
var receptacle = document.createElement('div');
var svgfragment='<svg>'+twocircles+'</svg>';
receptacle.innerHTML=''+svgfragment;
var Nodes=Array.prototype.slice.call(receptacle.childNodes[0].childNodes);
Nodes.forEach(function(el){document.getElementById('svgcanvas').appendChild(el)})
</script>
</body>
</html>
Enjoy!
The short answer is "No, there is nothing equivalent in the world of XML that lets you hand it a bit of markup and have it automatically create all the elements and attributes in the proper namespaces for the location where you insert it."
The closest direct answer is what #Robert has. As noted in my comments, even then you'll need to create any snippets inside an SVG document that has the same namespaces and prefixes as the document into which you'll be inserting the fragment.
Instead, you might find it is as easy (or easier) to use a convenience method on the standard DOM methods:
// Create a named SVG element on a node, with attributes and optional text
function appendTo(node,name,attrs,text){
var p,ns=appendTo.ns,svg=node,doc=node.ownerDocument;
if (!ns){ // cache namespaces by prefix once
while (svg&&svg.tagName!='svg') svg=svg.parentNode;
ns=appendTo.ns={svg:svg.namespaceURI};
for (var a=svg.attributes,i=a.length;i--;){
if (a[i].namespaceURI) ns[a[i].localName]=a[i].nodeValue;
}
}
var el = doc.createElementNS(ns.svg,name);
for (var attr in attrs){
if (!attrs.hasOwnProperty(attr)) continue;
if (!(p=attr.split(':'))[1]) el.setAttribute(attr,attrs[attr]);
else el.setAttributeNS(ns[p[0]]||null,p[1],attrs[attr]);
}
if (text) el.appendChild(doc.createTextNode(text));
return node.appendChild(el);
}
function clear(node){
while (node.lastChild) node.removeChild(node.lastChild);
}
With this you can do things like:
var icons={
Apps : "/images/apps.png",
Games : "/images/games.png"
}
var wrap = document.querySelector('#container');
clear(wrap);
for (var label in icons){
if (!icons.hasOwnProperty(label)) continue;
var icon = appendTo(wrap,'g',{'class':'icon'});
appendTo(icon,'image',{'xlink:href':icons[label]});
appendTo(icon,'text',{x:10,y:20},label);
}
This is IMHO cleaner than trying to construct the raw SVG markup using string concatenation:
var svg = [];
for (var label in icons){
if (!icons.hasOwnProperty(label)) continue;
svg.push('<g class="icon">');
svg.push('<image xlink:href="'+icons[label]+'" />');
svg.push('<text x="10" y="20">'+label+'</text>');
svg.push('</g>');
}
wrap.innerSVG = svg.join(''); // doesn't work, of course
How about my innerSVG shim? CoffeeScript source is below, compiled JavaScript is on https://gist.github.com/2648095
# Important: You must serve your pages as XHTML for this shim to work,
# otherwise namespaced attributes and elements will get messed up.
Object.defineProperty SVGElement.prototype, 'innerHTML',
get: () ->
$temp = document.createElement 'div'
$node = #cloneNode true
for $child in $node.children
$temp.appendChild $child
return $temp.innerHTML
set: (markup) ->
while #firstChild
#firstChild.parentNode.removeChild #firstChild
markup = "<svg id='wrapper' xmlns='http://www.w3.org/2000/svg'>#{markup}</svg>"
$div = document.createElement 'div'
$div.innerHTML = markup
$svg = $div.querySelector 'svg#wrapper'
for $element in $svg.children
#appendChild $element
enumerable : false
configurable : true
With jQuery, you can do it this way:
Let's suppose your svgString contains your svg image after the replacing operations.
$(svgString)[0] to create a svg tag corresponding to your string. Then you can append this element where you want in the dom to draw the image.
I hope this helps
It looks like at least on Chrome and Safari, you can wrap your SVG element in an HTML element and ask for innerHTML. The following HTML file renders a red rectangle, even though the SVG source specifies green.
<body>
<div id="wrapper">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="120" height="120" viewBox="0 0 236 120">
<rect x="14" y="23" width="250" height="50" fill="green" stroke="black" stroke-width="1" />
</svg>
</div>
<script>
var el = document.getElementById('wrapper');
var t = el.innerHTML;
t = t.replace(/green/g, "red");
el.innerHTML = t;
</script>
</body>
More simply, document.querySelector('#myContainer').textContent = newContent; has pretty good support, def. to IE9+, Safari, FF, Chrome.
Mike Bostock's my hero for these kinds of things: the D3.js source code is my go-to for SVG questions.

Categories

Resources