DOM Exception 9 when inserting SVG element with Ember/Handlebars - svg

I have a problem using Ember/Handlebars with SVG elements:
Controller:
display: function() {
this.set('isDisplayed', true);
}
Template:
<button {{action display}}>Display</button>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
{{#if isDisplayed}}
<text>Foo</text>
{{/if}}
</svg>
When I click on the button, I get this error: Uncaught Error: NotSupportedError: DOM Exception 9 (ember.js:18709: Metamorph#htmlFunc on the createContextualFragment line)
Is Ember/Handlebars correctly handling SVG ? What should I do to make this work ?
[EDIT] A little JSBin to see this in action: http://jsbin.com/onejec/2/edit

Handling SVG is somewhat tricky if you want to rely on ember to render DOM elements for you.
But as a starting point you could consider creating a svg wrapper like this:
App.SVGView = Ember.View.extend({
tagName: 'svg',
attributeBindings: ['height', 'width', 'xmlns', 'version'],
height: 100,
width: 100,
xmlns: 'http://www.w3.org/2000/svg',
version: '1.1',
render: function(buffer) {
return buffer.push('<text x="20" y="20" font-family="sans-serif" font-size="20px">Foo!</text>');
}
});
And then hook into the render method to inject your text tag.
This results in the following HTML markup:
<svg id="svg_view" class="ember-view" height="100" width="100" xmlns="http://www.w3.org/2000/svg" version="1.1">
<text x="20" y="20" font-family="sans-serif" font-size="20px">Foo!</text>
</svg>
Here also your working modified jsbin.
Edit
If you need to re-render the view based on some properties that might change you could add an observer to the correspondent property and call this.rerender() inside that method, something like this:
App.SVGView = Ember.View.extend({
tagName: 'svg',
attributeBindings: ['height', 'width', 'xmlns', 'version'],
height: 100,
width: 100,
xmlns: 'http://www.w3.org/2000/svg',
version: '1.1',
render: function(buffer) {
return buffer.push('<text x="20" y="20" font-family="sans-serif" font-size="20px">Foo!</text>');
},
myProperty: 'This value might change',
myPropertyChanged: function() {
this.rerender();
}.observes('myProperty')
});
Hope it helps.

Related

SVG elements to zoom whole SVG group on click or mouseover

I would like to use the circles within my SVG file to trigger a zoom in centred on the circle. I have got it working with a div acting as the trigger for the zoom but if I instead apply id="pin" to one of the circle elements within the SVG it no longer zooms in. Can anyone tell me why this is?
Is there a better way for me to achieve what I am trying to do? Ideally, I would like it to be possible to click to zoom and then to access other interactivity within the SVG while zoomed in.
If this is not possible is there a simple way to zoom and pan an SVG and to be able to access SVG interactivity while zoomed?
If I have missed something obvious please forgive me, I’m very much still learning the basics!
Rough example:
CodePen link
<div id="pin">click to trigger zoom</div>
<div class="map" id="mapFrame">
<svg class="image" id="mapSVG" version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1920 1442.5"" style="
enable-background:new 0 0 1920 924.9;" xml:space="preserve">
<g id="Layer_7" data-name="Layer 7">
<image width="1800" height="1350" transform="translate(0) scale(1.069)" opacity="0.3"
xlink:href="https://media.npr.org/assets/img/2020/07/04/seamus-coronavirus-d3-world-map-20200323_wide-a3888a851b91a905e9ad054ea03e177e23620015.png" />
</g>
<g id="one">
<circle cx="929.664" cy="944.287" r="81.191"/>
</g>
<g id="two">
<circle cx="638.164" cy="456.863" r="81.191" />
</g>
<g id="three">
<circle cx="1266.164" cy="498.868" r="81.191" />
</g>
</svg>
</div>
<script src="app.js"></script>
svg {
width: 100%;
height: auto;
}
#pin {
position: absolute;
height: 65px;
width: 75px;
top: 300px;
left: 550px;
padding: 10px;
background-color: yellow;
}
let imgElement = document.querySelector('#mapFrame');
let pinElement = document.querySelector('#pin');
pinElement.addEventListener('click', function (e) {
imgElement.style.transform = 'translate(-' + 0 + 'px,-' + 0 + 'px) scale(2)';
pinElement.style.display = 'none';
});
imgElement.addEventListener('click', function (e) {
imgElement.style.transform = null;
pinElement.style.display = 'block';
});
When you click on the circle, you are also clicking on the background image as well, triggering two events which is essentially cancelling the zoom. You can see this if you place alert('click 1'); and alert('click 2'); in your listeners.
This doesn't happen on the #pin element because it's outside background div and avoids the event bubbling up. This is solved by adding event.stopPropagation();
Code from your CodePen:
let imgElement = document.querySelector('#mapFrame');
let pinElement = document.querySelector('#one'); //changed to #one
pinElement.addEventListener('click', function (e) {
imgElement.style.transform = 'translate(-' + 0 + 'px,-' + 0 + 'px) scale(2)';
pinElement.style.display = 'none';
event.stopPropagation(); //added to prevent bubbling
});
imgElement.addEventListener('click', function (e) {
imgElement.style.transform = null;
pinElement.style.display = 'block';
});

Permissions (Add/Delete but not Edit)

I know that in order to allow editing of the whole widget you use attribute enableEdit=true (or false to disable) , but how can I disable "editing" of nodes but allow add/delete only?
Thank you,
George
A work-around , although I believe not the best way but it will do the trick until this is served as an option into the api.
<style>
/* use a CSS selector */
[data-btn-action="edit"] { display:none !important; }
</style>
Similarly for 'add' and 'del'. Hope this helps others too.
George
You can display your own buttons into the squares and add Listeners like this:
For display customized buttons:
var btnAdd = '<g data-action="add" id="btnInsertNode" class="btn" transform="matrix(0.14,0,0,0.14,0,0)"><rect style="opacity:0" x="0" y="0" height="300" width="300" /><path fill="#686868" d="M149.996,0C67.157,0,0.001,67.158,0.001,149.997c0,82.837,67.156,150,149.995,150s150-67.163,150-150 C299.996,67.156,232.835,0,149.996,0z M149.996,59.147c25.031,0,45.326,20.292,45.326,45.325 c0,25.036-20.292,45.328-45.326,45.328s-45.325-20.292-45.325-45.328C104.671,79.439,124.965,59.147,149.996,59.147z M168.692,212.557h-0.001v16.41v2.028h-18.264h-0.864H83.86c0-44.674,24.302-60.571,40.245-74.843 c7.724,4.15,16.532,6.531,25.892,6.601c9.358-0.07,18.168-2.451,25.887-6.601c7.143,6.393,15.953,13.121,23.511,22.606h-7.275 v10.374v13.051h-13.054h-10.374V212.557z M218.902,228.967v23.425h-16.41v-23.425h-23.428v-16.41h23.428v-23.425H218.9v23.425 h23.423v16.41H218.902z"/></g>';
var btnEdit = '<g data-action="edit" id="btnUpdateNode" class="btn" transform="matrix(0.14,0,0,0.14,50,0)"><rect style="opacity:0" x="0" y="0" height="300" width="300" /><path fill="#686868" d="M149.996,0C67.157,0,0.001,67.161,0.001,149.997S67.157,300,149.996,300s150.003-67.163,150.003-150.003 S232.835,0,149.996,0z M221.302,107.945l-14.247,14.247l-29.001-28.999l-11.002,11.002l29.001,29.001l-71.132,71.126 l-28.999-28.996L84.92,186.328l28.999,28.999l-7.088,7.088l-0.135-0.135c-0.786,1.294-2.064,2.238-3.582,2.575l-27.043,6.03 c-0.405,0.091-0.817,0.135-1.224,0.135c-1.476,0-2.91-0.581-3.973-1.647c-1.364-1.359-1.932-3.322-1.512-5.203l6.027-27.035 c0.34-1.517,1.286-2.798,2.578-3.582l-0.137-0.137L192.3,78.941c1.678-1.675,4.404-1.675,6.082,0.005l22.922,22.917 C222.982,103.541,222.982,106.267,221.302,107.945z"/></g>';
var btnDel = '<g data-action="delete" id="btnRemoveNode" class="btn" transform="matrix(0.14,0,0,0.14,100,0)"><rect style="opacity:0" x="0" y="0" height="300" width="300" /><path fill="#686868" d="M112.782,205.804c10.644,7.166,23.449,11.355,37.218,11.355c36.837,0,66.808-29.971,66.808-66.808 c0-13.769-4.189-26.574-11.355-37.218L112.782,205.804z"/> <path stroke="#686868" fill="#686868" d="M150,83.542c-36.839,0-66.808,29.969-66.808,66.808c0,15.595,5.384,29.946,14.374,41.326l93.758-93.758 C179.946,88.926,165.595,83.542,150,83.542z"/><path stroke="#686868" fill="#686868" d="M149.997,0C67.158,0,0.003,67.161,0.003,149.997S67.158,300,149.997,300s150-67.163,150-150.003S232.837,0,149.997,0z M150,237.907c-48.28,0-87.557-39.28-87.557-87.557c0-48.28,39.277-87.557,87.557-87.557c48.277,0,87.557,39.277,87.557,87.557 C237.557,198.627,198.277,237.907,150,237.907z"/></g>';
getOrgChart.themes.monica.box += '<g transform="matrix(1,0,0,1,350,10)">'
+ btnAdd
+ btnEdit
+ btnDel
+ '</g>';
For Listeners:
document.getElementById("btnInsertNode").addEventListener("click", function () {
orgchart.insertNode(2, { Name: "New Node" }/*, id optional*/);
});
document.getElementById("btnUpdateNode").addEventListener("click", function () {
orgchart.updateNode(7, 1, { Name: "7 New the parent node is 1" });
});
document.getElementById("btnRemoveNode").addEventListener("click", function () {
orgchart.removeNode(6);
});
This should help you: http://www.getorgchart.com/QuickStart/Methods/editNodeMethods.html

Vue can not attach event listener

I have an svg with some elements in it, the complete code is in here capture event, the aircraft image is positioned via transform attribute in such a way that it falls into the image with href2. The problem is Vue is unable to detect the click event on the aircraft image.
I can't seem to find a way to go around this. I want to be able to attach an event listener to the aircraft image regardless of where on the screen is located.
In jQuery solving these kind of situations is like a breeze of air, but with Vue seems to be a different story.
Here is the HTML
<div id="app">
<svg xmlns="http://www.w3.org/2000/svg" width="1015px" height="580px" viewBox="-50 -50 1015 580" preserveAspectRatio="xMidYMid meet" version="1.1" id="svg">
<g #click="showFlightCard" v-for="(ge, index) in this.gEl" :key="index">
<path :id="index+1" d="M 400 100 L 150 150" stroke="red" stroke-width="3" fill="none" />
<image :id="index" :href="href1" width="48" height="24" transform="translate(251,143)"></image>
</g>
<image x="250" y="10" width="522" height="402.452" id="e4_image" preserveAspectRatio="xMidYMid meet" :href="href2" style="stroke:black;stroke-width:1px;fill:khaki;"/>
</svg>
</div>
Here is the JS, for clarity I avoided the href in here due to 64 bit encoding, which is too long, please look at the jsfiddle which contains the href as well.
var app = new Vue({
el: '#app',
methods: {
showFlightCard: function (e) {
console.log('Click')
}
},
data: {
message: 'Hello Vue!',
gEl: ['A', 'B', 'C'],
href1: 'look at the jsfiddle',
href2: 'look at the jsfiddle'
}
})
You can attach the click listener on the path of the svg itself, or on the parent element of the svg. A click listener on the svg tag itself doesn't seem to fire events.

React & SVG: How do I make <path> support onClick?

This is what React SVG currently supports: http://facebook.github.io/react/docs/tags-and-attributes.html#svg-attributes
I'm trying to figure out how to make a shape I drew using the SVG path clickable.
If there is another way to draw a shape that can be made clickable, that works too.
Thanks!
I wrap my SVG with a div and apply any attributes that I desire (click handlers, fill colors, classes, width, etc..), like so (fiddle link):
import React, { PropTypes } from 'react'
function XMark({ width, height, fill, onClick }) {
return (
<div className="xmark-container" onClick={onClick}>
<svg className='xmark' viewBox="67 8 8 8" width={width} height={height} version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink">
<polygon stroke="none" fill={fill} fillRule="evenodd" points="74.0856176 9.4287633 71.5143809 12 74.0856176 14.5712367 73.5712367 15.0856176 71 12.5143809 68.4287633 15.0856176 67.9143824 14.5712367 70.4856191 12 67.9143824 9.4287633 68.4287633 8.91438245 71 11.4856191 73.5712367 8.91438245 74.0856176 9.4287633 74.0856176 9.4287633 74.0856176 9.4287633" />
</svg>
</div>
)
}
XMark.propTypes = {
width: PropTypes.number,
height: PropTypes.number,
fill: PropTypes.string,
onClick: PropTypes.func,
}
XMark.defaultProps = {
width: 8,
height: 8,
fill: '#979797',
onClick: null,
}
export default XMark
You can of course ditch the wrapper and apply the onClick to the svg element as well, but I've found this approach works well for me!
(I also try and use pure functions when possible https://hackernoon.com/react-stateless-functional-components-nine-wins-you-might-have-overlooked-997b0d933dbc)
This worked for me.
svg {
pointer-events: none;
}
path{
pointer-events: auto;
}
Then we can add on click event on path. worked!! thanks
I did it this way:
Just using polyline for example, it could be any SVG element.
export default class Polyline extends React.Component {
componentDidMount() {
this.polyline.addEventListener('click', this.props.onClick);
}
componentWillUnmount(){
this.polyline.removeEventListener('click', this.props.onClick);
}
render() {
const {points, style, markerEnd} = this.props;
return <polyline points={points}
ref={ref => this.polyline = ref}
style={style}
markerEnd={markerEnd}/>;
}
}
get ref in ref callback
on componentDidMount add click event listener to the ref
remove event listener in componentWillUnmount
I had similar problem with react, I was trying to handle onclick event for svg.
Simple css solved problem for me:
svg {
pointer-events: none;
}
You can use onClick as you do with other DOM elements.
Two major ways to do this:
You put an HTML event listener on the 'path' tag in the svg code. You will have to escape your code properly if you choose this method.
The following example features a star shape cut in two paths each of which logs "Hello" in the console ( console.log("Hello") )
Example:
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 225 213.6" style="enable-background:new 0 0 225 213.6;" xml:space="preserve">
<style type="text/css">
.st0{fill:#4D4D4D;}
.st1{fill:#494949;}
</style>
<g>
<path id="right" onmouseover="console.log("Hello")" class="st0" d="M43.5,131c4.4,4.2,6.3,8.7,5,15.1
c-3.4,17.2-6,34.6-9,51.9c-1.5,8.5,1.6,14.4,9.1,15.5c3.1,0.5,6.8-0.9,9.8-2.4c14.9-7.6,29.8-15.4,44.5-23.5c3-1.6,5.8-2.5,8.6-2.7
V0.1c-1.1,0.2-2.1,0.5-3.2,1.1c-2.7,1.4-5.2,4.2-6.7,7c-7.8,15.2-15.6,30.5-22.8,46C75.6,61,71,64.7,63.5,65.7
c-16.6,2.2-33.1,5.1-49.7,6.9C6.4,73.5,2.4,77.2,0,83.5c0,0.7,0,1.3,0,2c3.5,4.5,6.7,9.4,10.7,13.4C21.4,109.8,32.5,120.4,43.5,131
z"/>
<path id="left" onmouseover="console.log("Hello")" class="st1" d="M206.4,71.9c-15.9-2.1-31.8-4.7-47.7-6.9c-5.3-0.7-8.8-3.5-11-8.2c-8-16.2-16-32.5-24.1-48.7
c-2.9-5.8-7.3-8.8-12-8v184.8c3.5-0.2,6.9,0.7,10.6,2.7c14.7,8.1,29.6,15.8,44.5,23.5c2.9,1.5,6.7,2.9,9.8,2.4
c7.3-1,10.5-6.8,9.2-15c-3-17.8-5.9-35.6-9.2-53.3c-1.1-5.6,0.7-9.8,4.5-13.4c11.2-10.9,22.6-21.7,33.6-32.8c4-4,7.1-8.9,10.7-13.4
c0-0.7,0-1.3,0-2C222.3,74.1,214.5,72.9,206.4,71.9z"/>
</g>
</svg>
Example: https://svgshare.com/i/aex.svg
In Adobe Illustrator there is (currently called) SVG Iteractivity tool.
You can find it under the Window top menu.
Then select the path you need with direct selection tool, choose the HTML event from the SVG Interactivity widow and write your Javascript Code below.
Then click 'Export Selection' from the file menu or Save As... and save all as .svg
The result will be .svg file with automatically properly escaped code and HTML event on the 'path' tag.

Using Meteor to create SVG in template works, but not in #each loop

Update: as of February 2014, Meteor supports reactive SVG, so no workaround is necessary.
Meteor 0.5.9
I would like to create a group of shapes, one for each document in the collection. I can create shapes one at a time in a template, but not inside of an {{#each loop}}.
This works:
<Template name="map">
<svg viewBox="0 0 500 600" version="1.1">
<rect x="0" y="0" width="100" height="100" fill={{color}}/>
</svg>
</Template>
Template.map.color = function() {
return "green";
};
This does not:
<Template name="map">
<svg viewBox="0 0 500 600" version="1.1">
{{#each colors}}
<rect x="0" y="0" width="100" height="100" fill={{color}}/>
{{/each}}
</svg>
</Template>
Template.map.colors = function() {
return [{color: "red"}, {color: "blue"}];
}
Anything I try to create inside of using {{#each}} just doesn't show up, even though I can create them manually, even with attributes inserted by Meteor through the template.
I also tried just sending a single object {color: "red"} to the template and using {{#with colors}}, and that does not work either. In addition to the SVG, I've also put plain s into the templates to make sure information gets to the template correctly, and those are all working as expected, with {{#each}} and with {{#with}}.
Should I be able to do what I'm trying to do?
(Updated April 1, 2013)
Found a way that combines Handlebars with insertion by Javascript. Have to give credit to this blog entry for figuring this one out:
http://nocircleno.com/blog/svg-and-handlebars-js-templates/
I created the following two files, placed them inside the client folder of a new Meteor directory and I got the html successfully.
Testing.js:
<head>
<title>testing</title>
</head>
<body>
</body>
<template name="map">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg">
{{#each colors}}
<rect x="0" y="{{yPosition}}" width="100" height="100" fill="{{color}}"/>
{{/each}}
</svg>
</template>
Testing.html:
(function () {
var count = 0;
Template.map.yPosition = function() {
count++;
return (count-1) * 100;
};
Template.map.colors = function() {
return [{color: "red"}, {color: "blue"}];
};
Meteor.startup(function() {
var svgElement = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svgElement.width = 500;
svgElement.height = 600;
document.getElementsByTagName("body")[0].appendChild(svgElement);
var svgFragment = new DOMParser().parseFromString(Template.map(), "text/xml");
svgElement.appendChild(svgFragment.documentElement);
});
})();
I came across the same problem experimenting with Meteor and SVG elements and discovered that you can add elements and get them to show up with the two methods below. One option is to just wrap the elements in the each loop in an <svg></svg>, like this:
<svg viewbox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
{{#each pieces}}
<svg xmlns="http://www.w3.org/2000/svg"><circle cx="{{x}}" cy="{{y}}" r="1" fill="{{color}}"></circle></svg>
{{/each}}
</svg>
Another options is to (on template render) create an svg element with jQuery that contains the element you want to insert, then use jQuery to grab that inner element and insert it into the svg element already in the DOM, like so (in coffeescript):
for piece in Pieces.find().fetch()
$el = $("<svg><circle cx='#{piece.x}' cy='#{piece.y}' r='1' class='a'></circle></svg>")
$el.find('circle').appendTo #$('svg')
You could also use something like d3 or RaphaelJS to do the inserting. You can even make the individual elements reactive to your Collection and animate easily by using a library like d3 in the Collection observer callbacks like so (again, coffeescript):
Pieces.find().observe {
added: (piece)=>
# using jquery (could use d3 instead)
$el = $("<svg><circle cx='#{piece.x}' cy='#{piece.y}' r='1' fill='#{piece.color}' data-id='#{piece._id}'></circle></svg>")
$el.find('circle').appendTo #$('svg')
changed: (newPiece, oldPiece)=>
# using d3 to animate change
d3.select("[data-id=#{oldPiece._id}]").transition().duration(1000).attr {
cx: newPiece.x
cy: newPiece.y
fill: newPiece.color
}
removed: (piece)=>
#$("[data-id=#{piece._id}]").remove()
}
These methods seem to work in latest Chrome, Safari, Firefox browsers on Mac, but I haven't tested in others.
According to the Using Blaze page, Meteor will have first class support of SVG when Blaze is released.

Resources