Coordinates of foreignObject HTML elements in SVG space - javascript

I have an SVG file with an XHTML table. I want to connect portions of the table with SVG drawing (via JavaScript). For example, in the following file I want to place each of the yellow circles centered on the right ends of the red borders:
<?xml version="1.0"?>
<svg viewBox="0 0 1000 650" version="1.1" baseProfile="full"
xmlns="http://www.w3.org/2000/svg">
<title>Connect</title>
<style type="text/css"><![CDATA[
table { border-collapse:collapse; margin:0 auto; }
table td { padding:0; border-bottom:1px solid #c00;}
circle.dot { fill:blue }
]]></style>
<foreignObject x="100" width="800" height="600" y="400"><body xmlns="http://www.w3.org/1999/xhtml"><table><thead><tr>
<th>Space</th>
</tr></thead><tbody>
<tr><td><input name="name" type="text" value="One"/></td></tr>
<tr><td><input name="name" type="text" value="Two"/></td></tr>
</tbody></table></body></foreignObject>
<circle class="dot" id="dot1" cx="100" cy="100" r="5" />
<circle class="dot" id="dot2" cx="200" cy="100" r="5" />
</svg>
How can I best find the location of an arbitrary HTML element in global SVG coordinate space?
For simplicity, feel free to ignore browser resizing, and any transformation stacks that may wrap the <foreignObject>.

Here's what I have come up with, in action: HTML Element Location in SVG (v2)
// Find the four corners (nw/ne/sw/se) of any HTML element embedded in
// an SVG document, transformed into the coordinates of an arbitrary SVG
// element in the document.
var svgCoordsForHTMLElement = (function(svg){
var svgNS= svg.namespaceURI, xhtmlNS = "http://www.w3.org/1999/xhtml";
var doc = svg.ownerDocument;
var wrap = svg.appendChild(doc.createElementNS(svgNS,'foreignObject'));
var body = wrap.appendChild(doc.createElementNS(xhtmlNS,'body'));
if (typeof body.getBoundingClientRect != 'function') return;
body.style.margin = body.style.padding = 0;
wrap.setAttribute('x',100);
var broken = body.getBoundingClientRect().left != 0;
svg.removeChild(wrap);
var pt = svg.createSVGPoint();
return function svgCoordsForHTMLElement(htmlEl,svgEl){
if (!svgEl) svgEl = htmlEl.ownerDocument.documentElement;
for (var o=htmlEl;o&&o.tagName!='foreignObject';o=o.parentNode){}
var xform = o.getTransformToElement(svgEl);
if (broken) xform = o.getScreenCTM().inverse().multiply(xform);
var coords = {};
var rect = htmlEl.getBoundingClientRect();
pt.x = rect.left; pt.y = rect.top;
coords.nw = pt.matrixTransform(xform);
pt.y = rect.bottom; coords.sw = pt.matrixTransform(xform);
pt.x = rect.right; coords.se = pt.matrixTransform(xform);
pt.y = rect.top; coords.ne = pt.matrixTransform(xform);
return coords;
};
})(document.documentElement);
And here's how you use it:
// Setting SVG group origin to bottom right of HTML element
var pts = svgCoordsForHTMLElement( htmlEl );
var x = pts.sw.x, y = pts.sw.y;
svgGroup.setAttribute( 'transform', 'translate('+x+','+y+')' );
Caveats:
It works perfectly in Firefox v7
It works for Chrome v16 and Safari v5, except:
It does not work well if the browser zoom has been adjusted.
It does not work if the <foreignObject> has been rotated or skewed, due to this Webkit bug.
It does not work for IE9 (XHTML isn't showing up, and getTransformToElement does not work).

Related

SVGGeometryElement.isPointInFill() does not work with Chromium but works with Firefox

I'm trying to work on a way to detect if a path of an svg file that is within a canvas is clicked on. My code works in Firefox however it does not work on Chromium browsers. I have surrounded the code with try and catch and I receive no errors on Firefox and everything works however on Chromium browsers I receive the error:
TypeError: Failed to execute 'isPointInFill' on 'SVGGeometryElement': parameter 1 is not of type 'SVGPoint'.
at getIdOfElementAtPoint (main.js:39:27)
at HTMLCanvasElement.<anonymous> (main.js:70:9)
However on the next piece of code no error is thrown.
This is my code:
function getIdOfElementAtPoint(event) {
var paths = svg.getElementsByTagName("path");
//loop through all the path element in svg
for (var i = 0; i < paths.length; i++) {
var path = paths[i];
// Check if point (x, y) is inside the fill of the path
let inFill = false;
try {
inFill = path.isPointInFill(new DOMPoint(event.clientX, event.clientY))
} catch(error) {
console.log(error)
try {
const point = svg.createSVGPoint();
// Get the coordinates of the click
point.x = event.clientX;
point.y = event.clientY;
inFill = path.isPointInFill(point)
} catch(error) {
console.log(error)
}
}
if (inFill) {
console.log("The point is inside the element with id: " + path.getAttribute("id"));
return path.getAttribute("id");
}
}
console.log("The point is outside of any element.");
}
As commented by #Kaiido:
document.elementsFromPoint() or
document.elementFromPoint()
are probably the better options to get SVG elements at certain cursor positions.
However you address several issues:
browser support for DOMPoint() used for SVG methods (firefox vs. chromium vs. webkit)
translation between HTML DOM coordinates (e.g. via mouse inputs) to SVG user units
retrieving all underlying elements at certain coordinates
Example snippet
/**
* static example: is #circleInside within #circle2
*/
let isInfill = false;
let p1 = { x: circleInside.cx.baseVal.value, y: circleInside.cy.baseVal.value };
let inFill1 = isPointInFill1(circle2, p1);
if (inFill1) {
circleInside.setAttribute("fill", "green");
}
function isPointInFill1(el, p) {
let log = [];
let point;
try {
point = new DOMPoint(p.x, p.y);
el.isPointInFill(point);
log.push("DOMPoint");
} catch {
let svg = el.nearestViewportElement;
point = svg.createSVGPoint();
[point.x, point.y] = [p.x, p.y];
log.push("SVGPoint");
}
console.log(log.join(" "));
let inFill = el.isPointInFill(point);
return inFill;
}
document.addEventListener("mousemove", (e) => {
let cursorPos = { x: e.clientX, y: e.clientY };
// move svg cursor
let svgCursor = screenToSVG(svg, cursorPos);
cursor.setAttribute("cx", svgCursor.x);
cursor.setAttribute("cy", svgCursor.y);
/**
* pretty nuts:
* we're just testing the reversal of svg units to HTML DOM units
* just use the initial: e.clientX, e.clientY
*/
// move html cursor
let domCursorPos = SVGToScreen(svg, svgCursor);
cursorDOM.style.left = domCursorPos.x + "px";
cursorDOM.style.top = domCursorPos.y + "px";
// highlight
let elsInPoint = document.elementsFromPoint(cursorPos.x, cursorPos.y);
let log = ['elementsFromPoint: '];
elsInPoint.forEach((el) => {
if (el instanceof SVGGeometryElement && el!==cursor) {
log.push(el.id);
}
});
result.textContent = log.join(" | ");
});
/**
* helper function to translate between
* svg and HTML DOM coordinates:
* based on #Paul LeBeau's anser to
* "How to convert svg element coordinates to screen coordinates?"
* https://stackoverflow.com/questions/48343436/how-to-convert-svg-element-coordinates-to-screen-coordinates/48354404#48354404
*/
function screenToSVG(svg, p) {
let pSvg = svg.createSVGPoint();
pSvg.x = p.x;
pSvg.y = p.y;
return pSvg.matrixTransform(svg.getScreenCTM().inverse());
}
function SVGToScreen(svg, pSvg) {
let p = svg.createSVGPoint();
p.x = pSvg.x;
p.y = pSvg.y;
return p.matrixTransform(svg.getScreenCTM());
}
body{
margin: 0em
}
svg{
width:25%;
border:1px solid #ccc
}
.highlight{
opacity:0.5
}
.cursorDOM{
display:block;
position:absolute;
top:0;
left:0;
margin-left:-0.5em;
margin-top:-0.5em;
font-size:2em;
width:1em;
height:1em;
outline: 1px solid green;
border-radius:50%;
pointer-events:none;
}
<p id="result"></p>
<p id="resultNext"></p>
<div id="cursorDOM" class="cursorDOM"></div>
<svg id="svg" viewBox="0 0 100 100">
<rect id="rectBg" x="0" y="0" width="100%" height="100%" fill="#eee" />
<circle id="circle0" cx="75" cy="50" r="66" fill="#ccc" />
<circle id="circle1" cx="75" cy="50" r="50" fill="#999" />
<circle id="circle2" cx="75" cy="50" r="33" />
<circle id="circleInside" cx="95" cy="50" r="5" fill="red" />
<circle id="cursor" cx="0" cy="0" r="2" fill="red" />
</svg>
1. DOMPoint() or createSVGPoint() for pointInFill()?
You're right: DOMPoint() is currently (as of 2023) not supported by chromium (blink) for some svg related methods.
createSVGPoint() works well in chromium as well as in firefox – although it's classified as deprecated.
Quite likely chromium will catch up to firefox.
But isPointInFill() or isPointInStroke() are used for checking point intersection for single elements.
2. Translate coordinates
Depending on your layout, you probably need to convert coordinates.
See #Paul LeBeau's answer: "How to convert svg element coordinates to screen coordinates?"
3. Get all underlying elements
document.elementsFromPoint() is probably the best way to go.
However, this method will also return HTML DOM elements.
So you might need some filtering for svg geometry elements like so:
let elsInPoint = document.elementsFromPoint(cursorPos.x, cursorPos.y);
elsInPoint.forEach((el) => {
if (el instanceof SVGGeometryElement) {
console.log(el)
}
});

spawn & drag of SVG elements - approach

I am on my learning curve for Javascript/SVG combo (animating and making interactive SVGs).
I wanted to create a code snippet where menu elements ("inventory") can be dragged over to the main screen ("canvas") while the originating element would remain in its place (as if one would move a copy of it off the original element).
Here I crafted the code snippet as best as I could:
http://codepen.io/cmer41k/pen/f2b5eea274cdde29b0b2dc8a2424a645
So I sort of managed to do something but its buggy:
I could deal with 1 copy and making it draggable, but then I don't know how to deal with IDs for all those spawning elements, which causes dragging issues
I fail to understand how to make it work indefinitely (so that it can spawn any amount of circles draggable to canvas)
Draggable elements in canvas often overlap and I fail to attach the listeners the way they don't overlap so that the listener on the element I am dragging would propagate "through" whatever other elements there;(
Question is basically - can someone suggest logic that I should put into this snippet so that it was not as cumbersome. I am pretty sure I am missing something here;( (e.g. it should not be that hard is it not?)
HTML:
<body>
<svg id="svg"
height="800"
width="480"
viewbox="0 0 480 800"
preserveAspectRatio="xMinYMin meet"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<rect id="canvasBackground" width="480" height="480" x="0" y="0"/>
<rect id="inventoryBackground" width="480" height="100" x="0" y="480"/>
<g id="inventory">
<path id="curve4" class="inventory" d="M60,530 A35,35 0 1,1 60,531" />
<path id="curve3" class="inventory" d="M150,530 A35,35 0 1,1 150,531" />
<path id="curve2" class="inventory" d="M240,530 A35,35 0 1,1 240,531" />
<path id="curve1" class="inventory" d="M330,530 A35,35 0 1,1 330,531" />
</g>
<g id="canvas">
</g>
</svg>
</body>
Javascript:
// define meta objects
var drag = null;
// this stores all "curves"-circles
var curves = {};
var canvas = {};
var inventory = {};
window.onload = function() {
// creates the curve-circles in the object and at their initial x,y coords
curves.curve1 = document.getElementById("curve1");
curves.curve1.x = 0;
curves.curve1.y = 0;
curves.curve2 = document.getElementById("curve2");
curves.curve2.x = 0;
curves.curve2.y = 0;
curves.curve3 = document.getElementById("curve3");
curves.curve3.x = 0;
curves.curve3.y = 0;
curves.curve4 = document.getElementById("curve4");
curves.curve4.x = 0;
curves.curve4.y = 0;
canvas = document.getElementById("canvas");
inventory = document.getElementById("inventory");
// attach events listeners
AttachListeners();
}
function AttachListeners() {
var ttt = document.getElementsByClassName('inventory'), i;
for (i = 0; i < ttt.length; i++) {
document.getElementsByClassName("inventory")[i].onmousedown=Drag;
document.getElementsByClassName("inventory")[i].onmousemove=Drag;
document.getElementsByClassName("inventory")[i].onmouseup=Drag;
}
}
// Drag function that needs to be modified;//
function Drag(e) {
e.stopPropagation();
var t = e.target, id = t.id, et = e.type; m = MousePos(e);
if (!drag && (et == "mousedown")) {
if (t.className.baseVal=="inventory") { //if its inventory class item, this should get cloned into draggable?
copy = t.cloneNode(true);
copy.onmousedown=copy.onmousemove=onmouseup=Drag;
inventory.insertBefore(copy, inventory.firstChild);
drag = t;
dPoint = m;
}
if (t.className.baseVal=="draggable") { //if its just draggable class - it can be dragged around
drag = t;
dPoint = m;
}
}
// drag the spawned/copied draggable element now
if (drag && (et == "mousemove")) {
curves[id].x += m.x - dPoint.x;
curves[id].y += m.y - dPoint.y;
dPoint = m;
curves[id].setAttribute("transform", "translate(" +curves[id].x+","+curves[id].y+")");
}
// stop drag
if (drag && (et == "mouseup")) {
t.className.baseVal="draggable";
drag = null;
}
}
// adjust mouse position to the matrix of SVG & screen size
function MousePos(event) {
var p = svg.createSVGPoint();
p.x = event.clientX;
p.y = event.clientY;
var matrix = svg.getScreenCTM();
p = p.matrixTransform(matrix.inverse());
return {
x: p.x,
y: p.y
}
}
You were close. You had a couple of bugs. Eg.
copy.onmousedown=copy.onmousemove=onmouseup=Drag;
should have been:
copy.onmousedown=copy.onmousemove=copy.onmouseup=Drag;
And drag = t should have been drag = copy (?)
Also you were appending the clones to the inventory section, when I think you intended to append them to the "canvas" section.
But there were also also some less-obvious mistakes that were contributing to the unreliableness. For example, if you attach the mousemove and mouseup events to the inventory and clone shapes, then you will won't get the events if you drag too fast. The mouse will get outside the shape, and the events won't be passed to the shapes. The fix is to move those event handlers to the root SVG.
Another change I made was to store the x and y positions in the DOM for the clone as _x and _y. This makes it easier than trying to keep them in a separate array.
Anyway, here's my modified version of your example which works a lot more reliably.
// define meta objects
var drag = null;
var canvas = {};
var inventory = {};
window.onload = function() {
canvas = document.getElementById("canvas");
inventory = document.getElementById("inventory");
// attach events listeners
AttachListeners();
}
function AttachListeners() {
var ttt = document.getElementsByClassName('inventory'), i;
for (i = 0; i < ttt.length; i++) {
document.getElementsByClassName("inventory")[i].onmousedown=Drag;
}
document.getElementById("svg").onmousemove=Drag;
document.getElementById("svg").onmouseup=Drag;
}
// Drag function that needs to be modified;//
function Drag(e) {
var t = e.target, id = t.id, et = e.type; m = MousePos(e);
if (!drag && (et == "mousedown")) {
if (t.className.baseVal=="inventory") { //if its inventory class item, this should get cloned into draggable?
copy = t.cloneNode(true);
copy.onmousedown = Drag;
copy.removeAttribute("id");
copy._x = 0;
copy._y = 0;
canvas.appendChild(copy);
drag = copy;
dPoint = m;
}
else if (t.className.baseVal=="draggable") { //if its just draggable class - it can be dragged around
drag = t;
dPoint = m;
}
}
// drag the spawned/copied draggable element now
if (drag && (et == "mousemove")) {
drag._x += m.x - dPoint.x;
drag._y += m.y - dPoint.y;
dPoint = m;
drag.setAttribute("transform", "translate(" +drag._x+","+drag._y+")");
}
// stop drag
if (drag && (et == "mouseup")) {
drag.className.baseVal="draggable";
drag = null;
}
}
// adjust mouse position to the matrix of SVG & screen size
function MousePos(event) {
var p = svg.createSVGPoint();
p.x = event.clientX;
p.y = event.clientY;
var matrix = svg.getScreenCTM();
p = p.matrixTransform(matrix.inverse());
return {
x: p.x,
y: p.y
}
}
/* SVG styles */
path
{
stroke-width: 4;
stroke: #000;
stroke-linecap: round;
}
path.fill
{
fill: #3ff;
}
html, body {
margin: 0;
padding: 0;
border: 0;
overflow:hidden;
background-color: #fff;
}
body {
-ms-touch-action: none;
}
#canvasBackground {
fill: lightgrey;
}
#inventoryBackground {
fill: grey;
}
.inventory {
fill: red;
}
.draggable {
fill: green;
}
svg {
position: fixed;
top:0%;
left:0%;
width:100%;
height:100%;
}
<svg id="svg"
height="800"
width="480"
viewbox="0 0 480 800"
preserveAspectRatio="xMinYMin meet"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<rect id="canvasBackground" width="480" height="480" x="0" y="0"/>
<rect id="inventoryBackground" width="480" height="100" x="0" y="480"/>
<g id="inventory">
<path id="curve4" class="inventory" d="M60,530 A35,35 0 1,1 60,531" />
<path id="curve3" class="inventory" d="M150,530 A35,35 0 1,1 150,531" />
<path id="curve2" class="inventory" d="M240,530 A35,35 0 1,1 240,531" />
<path id="curve1" class="inventory" d="M330,530 A35,35 0 1,1 330,531" />
</g>
<g id="canvas">
</g>
</svg>

Fetching coordinates from svg with respect to its viewBox coordinate system [duplicate]

I want to implement the functionality like svg-edit on rectangle element
Rotate rectangle
Resizing
Drag
Rotating the SVG rectangle it works fine, but when I want to resize the rectangle it has a problem. The coordinates are not working right; I use the transform matrix to rotate
targetelement.setAttribute(transform,rotate(45,cx,cy))
but when the element has been rotated the coordinates are moved. I'm also using inverse function to inverse the transform matrix it resolves the problem but its not working with drag function.
I have created a working example of what I believe you are describing on my site here:
http://phrogz.net/svg/drag_under_transformation.xhtml
In general, you convert the mouse cursor into the local space of an object by:
Creating a mousemove event handler:
var svg = document.getElementsByTagName('svg')[0];
document.documentElement.addEventListener('mousemove',function(evt){
...
},false);
In that event handler, convert the mouse coordinates (in pixels) into the global space of your SVG document:
var pt = svg.createSVGPoint();
pt.x = evt.clientX;
pt.y = evt.clientY;
var globalPoint = pt.matrixTransform(svg.getScreenCTM().inverse());
Convert the global point into the space of the object you are dragging:
var globalToLocal = dragObject.getTransformToElement(svg).inverse();
var inObjectSpace = globalPoint.matrixTransform( globalToLocal );
For Stack Overflow posterity, here's the full source of my SVG+XHTML demo (in case my site is down):
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"><head>
<meta http-equiv="content-type" content="application/xhtml+xml;charset=utf-8"/>
<title>Dragging Transformed SVG Elements</title>
<style type="text/css" media="screen">
html, body {
background:#eee; margin:0;
user-select:none; -moz-user-select:none; -webkit-user-select:none;
}
p { margin:0.5em; text-align:center }
svg {
position:absolute; top:5%; left:5%; width:90%; height:90%;
background:#fff; border:1px solid #ccc
}
svg rect { stroke:#333 }
svg .drag { cursor:move }
svg .sizer { opacity:0.3; fill:#ff0; stroke:#630;}
#footer {
position:absolute; bottom:0.5em; margin-bottom:0;
width:40em; margin-left:-20em; left:50%; color:#666;
font-style:italic; font-size:85%
}
#dragcatch { position:absolute; left:0; right:0; top:0; bottom:0; z-index:-1}
</style>
</head><body>
<p>Showing how to drag points inside a transformation hierarchy.</p>
<svg viewBox="0 0 200 100" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full">
<g transform="scale(1.2,0.8)">
<rect transform="translate(50,20) rotate(30)"
class="drag resize" x="50" y="30" width="50" height="30" fill="#69c" />
<rect class="drag resize" x="5" y="5" width="90" height="50" fill="#c66" />
</g>
</svg>
<p id="footer">
Copyright © 2011 Gavin Kistner.
Comments/criticisms welcome.
</p>
<script type="text/javascript"><![CDATA[
var svg = document.getElementsByTagName('svg')[0];
var svgNS = svg.getAttribute('xmlns');
var pt = svg.createSVGPoint();
function createOn(root,name,prop){
var el = document.createElementNS(svgNS,name);
for (var a in prop) if (prop.hasOwnProperty(a)) el.setAttribute(a,prop[a]);
return root.appendChild(el);
}
function rectCorner(rect){
pt.x = rect.x.animVal.value + rect.width.animVal.value;
pt.y = rect.y.animVal.value + rect.height.animVal.value;
return pt.matrixTransform(rect.getTransformToElement(svg));
}
function pointIn(el,x,y){
pt.x = x; pt.y = y;
return pt.matrixTransform(el.getTransformToElement(svg).inverse());
}
function cursorPoint(evt){
pt.x = evt.clientX; pt.y = evt.clientY;
return pt.matrixTransform(svg.getScreenCTM().inverse());
}
// Make all rects resizable before drag, so the drag handles become drag
for (var a=svg.querySelectorAll('rect.resize'),i=0,len=a.length;i<len;++i){
(function(rect){
var dot = createOn(svg,'circle',{'class':'drag sizer',cx:0,cy:0,r:5});
var moveDotToRect = function(){
var corner = rectCorner(rect);
dot.setAttribute('cx',corner.x);
dot.setAttribute('cy',corner.y);
}
moveDotToRect();
rect.addEventListener('dragged',moveDotToRect,false);
dot.addEventListener('dragged',function(){
var rectXY = pointIn(rect,dot.cx.animVal.value,dot.cy.animVal.value);
var w = Math.max( rectXY.x-rect.x.animVal.value, 1 );
var h = Math.max( rectXY.y-rect.y.animVal.value, 1 );
rect.setAttribute('width', w);
rect.setAttribute('height',h);
},false);
})(a[i]);
}
for (var a=svg.querySelectorAll('.drag'),i=0,len=a.length;i<len;++i){
(function(el){
var onmove; // make inner closure available for unregistration
el.addEventListener('mousedown',function(e){
el.parentNode.appendChild(el); // move to top
var x = el.tagName=='circle' ? 'cx' : 'x';
var y = el.tagName=='circle' ? 'cy' : 'y';
var mouseStart = cursorPoint(e);
var elementStart = { x:el[x].animVal.value, y:el[y].animVal.value };
onmove = function(e){
var current = cursorPoint(e);
pt.x = current.x - mouseStart.x;
pt.y = current.y - mouseStart.y;
var m = el.getTransformToElement(svg).inverse();
m.e = m.f = 0;
pt = pt.matrixTransform(m);
el.setAttribute(x,elementStart.x+pt.x);
el.setAttribute(y,elementStart.y+pt.y);
var dragEvent = document.createEvent("Event");
dragEvent.initEvent("dragged", true, true);
el.dispatchEvent(dragEvent);
};
document.body.addEventListener('mousemove',onmove,false);
},false);
document.body.addEventListener('mouseup',function(){
document.body.removeEventListener('mousemove',onmove,false);
},false);
})(a[i]);
}
]]></script>
<div id="dragcatch"></div>
</body></html>
For those who use Chrome please add the following lines after
var pt = svg.createSVGPoint();
SVGElement.prototype.getTransformToElement = SVGElement.prototype.getTransformToElement || function(elem) {
return elem.getScreenCTM().inverse().multiply(this.getScreenCTM());
};
More info here: https://github.com/cpettitt/dagre-d3/issues/202

Dragging SVG element over another SVG element

Is there any way i can drag an SVG element over another SVG element? I tried but like in this tutorial i can only drag the one i placed the second over the first one. There is no way i can drag first one over the second without problems.
Does anyone know how to solve this?
Here is the whole tutorial: http://www.petercollingridge.co.uk/book/export/html/437
I was writing it before I saw that #Strat-O gave you the same approach.
So here is a commented example of that :
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" version="1.1" width="400" height="200">
<style>
.draggable {
cursor: move;
}
</style>
<script type="text/ecmascript"><![CDATA[
var selectedElement = 0;
var currentX = 0;
var currentY = 0;
var currentMatrix = 0;
function cloneToTop(oldEl){
// already at top, don't go farther…
if(oldEl.atTop==true) return oldEl;
// make a copy of this node
var el = oldEl.cloneNode(true);
// select all draggable elements, none of them are at top anymore
var dragEls= oldEl.ownerDocument.documentElement.querySelectorAll('.draggable');
for(i=0; i<dragEls.length; i++){
dragEls[i].atTop=null;
}
var parent = oldEl.parentNode;
// remove the original node
parent.removeChild(oldEl);
// insert our new node at top (last element drawn is first visible in svg)
parent.appendChild(el);
// Tell the world that our new element is at Top
el.atTop= true;
return el;
}
function selectElement(evt) {
selectedElement = cloneToTop(evt.target);
currentX = evt.clientX;
currentY = evt.clientY;
currentMatrix = selectedElement.getAttributeNS(null, "transform").slice(7,-1).split(' ');
for(var i=0; i<currentMatrix.length; i++) {
currentMatrix[i] = parseFloat(currentMatrix[i]);
}
selectedElement.setAttributeNS(null, "onmousemove", "moveElement(evt)");
selectedElement.setAttributeNS(null, "onmouseout", "deselectElement(evt)");
selectedElement.setAttributeNS(null, "onmouseup", "deselectElement(evt)");
}
function moveElement(evt) {
var dx = evt.clientX - currentX;
var dy = evt.clientY - currentY;
currentMatrix[4] += dx;
currentMatrix[5] += dy;
selectedElement.setAttributeNS(null, "transform", "matrix(" + currentMatrix.join(' ') + ")");
currentX = evt.clientX;
currentY = evt.clientY;
}
function deselectElement(evt) {
if(selectedElement != 0){
selectedElement.removeAttributeNS(null, "onmousemove");
selectedElement.removeAttributeNS(null, "onmouseout");
selectedElement.removeAttributeNS(null, "onmouseup");
selectedElement = 0;
}
}
]]> </script>
<g>
<circle/>
</g>
<rect x="0.5" y="0.5" width="399" height="199" fill="none" stroke="black"/>
<rect class="draggable" id="blue" x="30" y="30" width="80" height="80" fill="blue" transform="matrix(1 0 0 1 46 18)" onmousedown="selectElement(evt)"/>
<rect class="draggable" id="green" x="160" y="50" width="50" height="50" fill="green" transform="matrix(1 0 0 1 51 11)" onmousedown="selectElement(evt)"/>
</svg>
Unfortunately, there is only one way to make an element appear in front of another element in SVG and that is to remove the lower element then turn around and redraw it. There is no z-index or other helpful attribute that you can set. I spent a bit of time on this and that is my conclusion.
There is one upside and that is by removing and redrawing it, it ensures that the display order of all of the elements is maintained whereas if you have to maintain z-indexes, managing the numbers can cause its own set of issues.

Inline text editing in SVG

I render an inline SVG in a website, and have to enable the user to add and modify texts in that SVG, in a WYSIWYG manner. Basically I need something that works like svg-edit. However I don't need a fully WYSIWYG editor, but just the inline text editing part. I have looked at svg-edit's source code and it seems to be very hard to extract only that part of it.
So what I am looking for is an easy way (maybe with a third-party library) to implement inline SVG text editing. I already thought about replacing the SVG text with an HTML text input when focused, but the text must be rendered when in edit-mode exactly as it is rendered in the resulting SVG.
I made a fiddle that created editable text wherever you click in an SVG. A final step would be to grab the HTML text and put it in an SVG element.
http://jsfiddle.net/brx3xm59/
Code follows:
var mousedownonelement = false;
window.getlocalmousecoord = function (svg, evt) {
var pt = svg.createSVGPoint();
pt.x = evt.clientX;
pt.y = evt.clientY;
var localpoint = pt.matrixTransform(svg.getScreenCTM().inverse());
localpoint.x = Math.round(localpoint.x);
localpoint.y = Math.round(localpoint.y);
return localpoint;
};
window.createtext = function (localpoint, svg) {
var myforeign = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject')
var textdiv = document.createElement("div");
var textnode = document.createTextNode("Click to edit");
textdiv.appendChild(textnode);
textdiv.setAttribute("contentEditable", "true");
textdiv.setAttribute("width", "auto");
myforeign.setAttribute("width", "100%");
myforeign.setAttribute("height", "100%");
myforeign.classList.add("foreign"); //to make div fit text
textdiv.classList.add("insideforeign"); //to make div fit text
textdiv.addEventListener("mousedown", elementMousedown, false);
myforeign.setAttributeNS(null, "transform", "translate(" + localpoint.x + " " + localpoint.y + ")");
svg.appendChild(myforeign);
myforeign.appendChild(textdiv);
};
function elementMousedown(evt) {
mousedownonelement = true;
}
$(('#thesvg')).click(function (evt) {
var svg = document.getElementById('thesvg');
var localpoint = getlocalmousecoord(svg, evt);
if (!mousedownonelement) {
createtext(localpoint, svg);
} else {
mousedownonelement = false;
}
});
Edit: Update sample to work with Edge. Attributes of client bounding box are different there - it may be the issue with older IPads and Safari reported below. I have tested this on Edge, Chrome, FF ,Safari (Mac) and Chrome, FF, Safari(IPad). On Edge, the cursor is broken but editing still works.
I realize this is an old question, but the contentEditable trick is still all we have if, you don't want to implement your own input element behavior. If you use a single svg text node (as opposed to HTML foreign object) as an overlay to the text under edit, you can get true WSYWIG in that you can use the same font etc. as the original text. You can also choose which elements can be edited. Yes the cursor is weird in Safari. A fiddle that demonstrates this can be found here:
https://jsfiddle.net/AaronDavidNewman/ta0jhw1q/
HTML/SVG:
<div id="yyy">
<div id="xxx">
<svg width="500" height="500" viewBox="0 0 500 500">
<text x="0" y="25" id="target1" font-size="1.8em">Change me</text>
<text x="0" y="50" id="targetx" font-size="1.8em">You can't edit me</text>
<text x="0" y="75" id="target2" font-size="1.8em">Edit me</text>
<text x="0" y="100" id="targety" font-size="1.8em">You can't edit me</text>
<text x="0" y="125" id="target3" font-size="1.8em">Improve me</text>
</svg>
</div>
<div id="aaa" contentEditable="true" class="hide">
<svg width="500" height="50" viewBox="0 0 500 50">
<text x="0" y="50" id="input-area" font-size="1.8em"></text>
</svg>
</div>
</div>
Javascript:
// Create in-place editable text elements in svg. Click inside the element
// to edit it, and away to stop editing and switch to another element
var editing = false;
var svgns = "http://www.w3.org/2000/svg";
$('body').css('overflow','hidden');
// Poll on changes to input element. A better approach might be
// to update after keyboard events
var editElement = function(aaa, xxx) {
setTimeout(function() {
xxx.textContent = aaa.textContent;
if (editing) {
editElement(aaa, xxx);
}
}, 250);
}
// Make sure the input svg element is placed directly over the
// target element
var fixOffset = function(aaa, xxx) {
var svg = $('#xxx').find('svg')[0];
$('.underEdit').remove();
var rect = xxx.getBoundingClientRect();
var offset = aaa.getBoundingClientRect();
$('#aaa').css('left', rect.left + (rect.left - offset.left));
$('#aaa').css('top', rect.top + (rect.top - offset.top));
var bb = xxx.getBBox();
var margin = 10;
}
// Based on a click in the element svg area, edit that element
var editId = function(id) {
var aaa = document.getElementById('input-area');
var xxx = document.getElementById(id);
var rect = xxx.getBoundingClientRect();
$('#aaa').css('left', rect.left);
$('#aaa').css('top', rect.top);
setTimeout(function() {
fixOffset(aaa, xxx);
}, 1);
aaa.textContent = xxx.textContent;
editing = true;
editElement(aaa, xxx);
}
// see if a click intersects an editable element
var getIntersection = function(objs, point) {
var rv = null;
$(objs).each(function(ix, obj) {
var i1 = point.x - obj.box.x;
var i2 = point.y - obj.box.y;
// If inside the box, we have an element to edit
if (i1 > 0 && i1 < obj.box.width && i2 > 0 && i2 < obj.box.height) {
rv = obj;
return false;
} else if (i1 > -10 && i1 < obj.box.width + 10 && i2 > -10 && i2 < obj.box.height + 10) {
// edit a nearby click, if a better match isn't found
rv = obj;
}
});
return rv;
}
// bind editable elements to mouse click
var bind = function(texts) {
var objs = [];
// find geometry of each editable element
texts.forEach((id) => {
var el = document.getElementById(id);
var bbox = el.getBoundingClientRect();
bbox = { x: bbox.left, y: bbox.top, width: bbox.width, height: bbox.height };
objs.push({id: id, box: bbox });
});
// bind click event globally, then try to find the intersection.
$(document).off('click').on('click', function(ev) {
var point = {x: ev.clientX, y: ev.clientY };
console.log('x:' + point.x + 'y:' + point.y);
var obj = getIntersection(objs, point);
if (obj && !editing) {
$('#aaa').removeClass('hide');
editing = true;
console.log('start edit on ' + obj.id);
editId(obj.id);
} else if (!obj) {
{
$('#aaa').addClass('hide');
editing = false;
$('.underEdit').remove();
console.log('stop editing');
}
}
});
}
bind(['target1', 'target2', 'target3']);
CSS:
#yyy {
position: relative;
width: 500px;
height: 500px;
}
#xxx {
position: absolute;
left: 100px;
top: 100px;
z-index: 1;
}
#aaa {
position: absolute;
left: 100px;
top: 100px;
z-index: 2;
overflow:hidden;
}
.hide {
display: none;
}
Here is an example where you can get and change the text from a textnode. I suggest to write a JavaScript function that puts an editable div or something like that in place of the textnode and when saved replaces the textnode with the innerHTML of the div.
Please post the final code here when you succeed.
<html>
<head>
<script>
function svgMod(){
var circle1 = document.getElementById("circle1");
circle1.style.fill="blue";
}
function svgMod2(){
var circle1 = document.getElementById("circle1");
t1.textContent="New content";
}
</script>
</head>
<body>
<svg id="svg1" xmlns="http://www.w3.org/2000/svg" style="width: 800; height: 1000">
<circle id="circle1" r="100" cx="134" cy="134" style="fill: red; stroke: blue; stroke-width: 2"/>
<text id="t1" x="50" y="120" onclick="alert(t1.textContent)">Example SVG text 1</text>
</svg>
<button onclick=circle1.style.fill="yellow";>Click to change to yellow</button>
<button onclick=javascript:svgMod();>Click to change to blue</button>
<button onclick=javascript:svgMod2();>Click to change text</button>
</body>
</html>
Here is a demo, which can edit text that already exists (instead of creating new text entries):
https://jsfiddle.net/qkrLy9gu
<!DOCTYPE html>
<html>
<body>
<svg height="100" width="200">
<circle cx="50" cy="50" r="40" fill="red"/>
<text x="50" y="50" onclick="edittext(this)">a circle [click to edit]</text>
</svg>
<script>
function edittext(svgtext){
var input = document.createElement("input");
input.value = svgtext.textContent
input.onkeyup = function(e){
if (["Enter", "Escape"].includes(e.key)) {this.blur(); return};
svgtext.textContent = this.value
}
input.onblur = function(e){
myforeign.remove()
}
var myforeign = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject')
myforeign.setAttribute("width", "100%");
myforeign.setAttribute("height", "100%");
myforeign.append(input);
svg = svgtext.parentNode
svg.append(myforeign);
input.focus()
}
</script>
</body>
</html>

Categories

Resources