Reputation: 2215
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.
Upvotes: 30
Views: 31373
Reputation: 3734
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>
Upvotes: 0
Reputation: 626
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;
}
Upvotes: 2
Reputation: 6615
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.
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;
}
});
Upvotes: 20
Reputation: 42192
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>
Upvotes: 0