I would like to display a <text>
in SVG what would auto-line-wrap to the container <rect>
the same way as HTML text fills <div>
elements. Is there a way to do it? I don't want to position lines separately by using <tspan>
s.
Text wrapping is not part of SVG1.1, the currently implemented spec.
In case you are going to use your SVG graphic on the Web, you can embed HTML inside SVG via the <foreignObject/>
element. Example:
<svg ...>
<switch>
<foreignObject x="20" y="90" width="150" height="200">
<p xmlns="http://www.w3.org/1999/xhtml">Text goes here</p>
</foreignObject>
<text x="20" y="20">Your SVG viewer cannot display html.</text>
</switch>
</svg>
If you are targeting a pure SVG renderer without HTML support or want your graphic to be editable using professional vector graphics manipulation software (Adobe Illustrator, Inkscape, ...), this solution will probably not suit you.
Here's an alternative:
<svg ...>
<switch>
<g requiredFeatures="http://www.w3.org/Graphics/SVG/feature/1.2/#TextFlow">
<textArea width="200" height="auto">
Text goes here
</textArea>
</g>
<foreignObject width="200" height="200"
requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<p xmlns="http://www.w3.org/1999/xhtml">Text goes here</p>
</foreignObject>
<text x="20" y="20">No automatic linewrapping.</text>
</switch>
</svg>
Noting that even though foreignObject may be reported as being supported with that featurestring, there's no guarantee that HTML can be displayed because that's not required by the SVG 1.1 specification. There is no featurestring for html-in-foreignobject support at the moment. However, it is still supported in many browsers, so it's likely to become required in the future, perhaps with a corresponding featurestring.
Note that the 'textArea' element in SVG Tiny 1.2 supports all the standard svg features, e.g advanced filling etc, and that you can specify either of width or height as auto, meaning that the text can flow freely in that direction. ForeignObject acts as clipping viewport.
Note: while the above example is valid SVG 1.1 content, in SVG 2 the 'requiredFeatures' attribute has been removed, which means the 'switch' element will try to render the first 'g' element regardless of having support for SVG 1.2 'textArea' elements. See SVG2 switch element spec.
xhtml:div
instead of div
, but that could be because of d3.js. I couldn't find any useful reference about TextFlow, does it (still) exist or was it just in some draft? –
Circe The textPath may be good for some case.
<svg width="200" height="200"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<!-- define lines for text lies on -->
<path id="path1" d="M10,30 H190 M10,60 H190 M10,90 H190 M10,120 H190"></path>
</defs>
<use xlink:href="#path1" x="0" y="35" stroke="blue" stroke-width="1" />
<text transform="translate(0,35)" fill="red" font-size="20">
<textPath xlink:href="#path1">This is a long long long text ......</textPath>
</text>
</svg>
xmlns:xlink="http://www.w3.org/1999/xlink"
attribute in the root svg element is vital for that to work. –
Petitioner The following code is working fine. Run the code snippet what it does.
Maybe it can be cleaned up or make it automatically work with all text tags in SVG.
function svg_textMultiline() {
var x = 0;
var y = 20;
var width = 360;
var lineHeight = 10;
/* get the text */
var element = document.getElementById('test');
var text = element.innerHTML;
/* split the words into array */
var words = text.split(' ');
var line = '';
/* Make a tspan for testing */
element.innerHTML = '<tspan id="PROCESSING">busy</tspan >';
for (var n = 0; n < words.length; n++) {
var testLine = line + words[n] + ' ';
var testElem = document.getElementById('PROCESSING');
/* Add line in testElement */
testElem.innerHTML = testLine;
/* Messure textElement */
var metrics = testElem.getBoundingClientRect();
testWidth = metrics.width;
if (testWidth > width && n > 0) {
element.innerHTML += '<tspan x="0" dy="' + y + '">' + line + '</tspan>';
line = words[n] + ' ';
} else {
line = testLine;
}
}
element.innerHTML += '<tspan x="0" dy="' + y + '">' + line + '</tspan>';
document.getElementById("PROCESSING").remove();
}
svg_textMultiline();
body {
font-family: arial;
font-size: 20px;
}
svg {
background: #dfdfdf;
border:1px solid #aaa;
}
svg text {
fill: blue;
stroke: red;
stroke-width: 0.3;
stroke-linejoin: round;
stroke-linecap: round;
}
<svg height="300" width="500" xmlns="http://www.w3.org/2000/svg" version="1.1">
<text id="test" y="0">GIETEN - Het college van Aa en Hunze is in de fout gegaan met het weigeren van een zorgproject in het failliete hotel Braams in Gieten. Dat stelt de PvdA-fractie in een brief aan het college. De partij wil opheldering over de kwestie en heeft schriftelijke
vragen ingediend. Verkeerde route De PvdA vindt dat de gemeenteraad eerst gepolst had moeten worden, voordat het college het plan afwees. "Volgens ons is de verkeerde route gekozen", zegt PvdA-raadslid Henk Santes.</text>
</svg>
Building on @Mike Gledhill's code, I've taken it a step further and added more parameters. If you have a SVG RECT and want text to wrap inside it, this may be handy:
function wraptorect(textnode, boxObject, padding, linePadding) {
var x_pos = parseInt(boxObject.getAttribute('x')),
y_pos = parseInt(boxObject.getAttribute('y')),
boxwidth = parseInt(boxObject.getAttribute('width')),
fz = parseInt(window.getComputedStyle(textnode)['font-size']); // We use this to calculate dy for each TSPAN.
var line_height = fz + linePadding;
// Clone the original text node to store and display the final wrapping text.
var wrapping = textnode.cloneNode(false); // False means any TSPANs in the textnode will be discarded
wrapping.setAttributeNS(null, 'x', x_pos + padding);
wrapping.setAttributeNS(null, 'y', y_pos + padding);
// Make a copy of this node and hide it to progressively draw, measure and calculate line breaks.
var testing = wrapping.cloneNode(false);
testing.setAttributeNS(null, 'visibility', 'hidden'); // Comment this out to debug
var testingTSPAN = document.createElementNS(null, 'tspan');
var testingTEXTNODE = document.createTextNode(textnode.textContent);
testingTSPAN.appendChild(testingTEXTNODE);
testing.appendChild(testingTSPAN);
var tester = document.getElementsByTagName('svg')[0].appendChild(testing);
var words = textnode.textContent.split(" ");
var line = line2 = "";
var linecounter = 0;
var testwidth;
for (var n = 0; n < words.length; n++) {
line2 = line + words[n] + " ";
testing.textContent = line2;
testwidth = testing.getBBox().width;
if ((testwidth + 2*padding) > boxwidth) {
testingTSPAN = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
testingTSPAN.setAttributeNS(null, 'x', x_pos + padding);
testingTSPAN.setAttributeNS(null, 'dy', line_height);
testingTEXTNODE = document.createTextNode(line);
testingTSPAN.appendChild(testingTEXTNODE);
wrapping.appendChild(testingTSPAN);
line = words[n] + " ";
linecounter++;
}
else {
line = line2;
}
}
var testingTSPAN = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
testingTSPAN.setAttributeNS(null, 'x', x_pos + padding);
testingTSPAN.setAttributeNS(null, 'dy', line_height);
var testingTEXTNODE = document.createTextNode(line);
testingTSPAN.appendChild(testingTEXTNODE);
wrapping.appendChild(testingTSPAN);
testing.parentNode.removeChild(testing);
textnode.parentNode.replaceChild(wrapping,textnode);
return linecounter;
}
document.getElementById('original').onmouseover = function () {
var container = document.getElementById('destination');
var numberoflines = wraptorect(this,container,20,1);
console.log(numberoflines); // In case you need it
};
boxwidth = parseInt(boxObject.getAttribute('width'))
, would just accept width in pixel, while boxwidth = parseInt(boxObject.getBBox().width)
, would accept any type of measure unit –
Kazak This functionality can also be added using JavaScript. Carto.net has an example:
http://old.carto.net/papers/svg/textFlow/
Something else that also might be useful to are you are editable text areas:
I have posted the following walkthrough for adding some fake word-wrapping to an SVG "text" element here:
You just need to add a simple JavaScript function, which splits your string into shorter "tspan" elements. Here's an example of what it looks like:
Hope this helps !
Embedded HTML in SVG: <foreignObject>
As pointed out by Erik Dahlström <foreignObject>
is currently the easiest way to achieve a multiline text layout in SVG.
Beseides, if you need responsive/dynamic line-wrapping in SVG text – it is currently your only option. <foreignObject>
– as the name implies is actually just an embedded HTML (or other XML based) content.
Not a problem for web-only applications since browser support is solid (but you might still encounter issues in more complex contexts such as applying svg filters to foreignObjects masks etc.). However, most desktop graphic editors like inkscape, Adobe Illustrator or preview apps can't render or convert <foreignObject>
contents.
Rebuild from HTML elements
This approach takes advantage of native browser APIs to get word or character positions by querying all text nodes in an HTML element.
Disclaimer: the following approaches have limitations due to the limited text capablities of SVG: it only supports some basic block and inline text elements but doesn't support e.g. lists or tables.
Example: convert HTML to native SVG text via <span>
wrapping
function foreignObjectToNativeSvgtext(foreignObject) {
let body = foreignObject.children[0];
let children = [...body.children];
// children - will become svg <text> elements
children.forEach((child, i) => {
// clone text el
let type = child.nodeName.toLowerCase();
let clone;
clone = document.createElement(type);
body.insertBefore(clone, body.children[i]);
let textNodes = textNodesInEl(child);
let textL = textNodes.length;
for (let n = 0; n < textL; n++) {
let textNode = textNodes[n];
// get computed styles for parent
let style = window.getComputedStyle(textNode.parentNode);
copyStyleProps(child, clone)
// split to words
let words = textNode.textContent.split(' ');
words.forEach((word, w) => {
let newtextNode = document.createTextNode(word);
let span = document.createElement('span');
span.classList.add('span2tspan', 'span2tspan' + type);
// get computed styles for child
copyStyleProps(textNode.parentNode, span)
span.textContent = word + ' ';
clone.appendChild(span);
})
}
// convert to svg tspan
let nativeText = htmlTextEl2Svg(clone);
//insert before foreignObject
foreignObject.parentNode.insertBefore(nativeText, foreignObject);
// delete original foreign object elements
child.remove();
})
//preserve whitespace
let svg = foreignObject.closest('svg');
svg.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve')
foreignObject.remove();
output.value = new XMLSerializer().serializeToString(svg)
}
function htmlTextEl2Svg(el) {
const ns = "http://www.w3.org/2000/svg";
let parentSVG = el.closest('svg');
let newText = document.createElementNS(ns, 'text');
let parentProps = copyStyleProps(el, newText);
let bb = el.getBoundingClientRect();
let {
x,
y
} = bb;
let point = parentSVG.createSVGPoint();
point.x = x;
point.y = y;
let ctm = parentSVG.getScreenCTM().inverse();
point = point.matrixTransform(ctm);
// round
[x, y] = [x, y].map(val => {
return +(val).toFixed(3)
})
newText.setAttribute('x', x);
newText.setAttribute('y', y);
// children
let children = el.querySelectorAll('.span2tspan');
/**
* add x and y only for vertical shifts
* (new line breaks)
*/
let xPrev = 0;
let yPrev = 0;
let prevStyle = '';
children.forEach(child => {
let bb = child.getBoundingClientRect();
let {
x,
y,
width,
height
} = bb;
let tspan = document.createElementNS(ns, 'tspan');
let style = window.getComputedStyle(child);
let currentProps = copyStyleProps(child, tspan);
// convert coordinates to svg
let point = parentSVG.createSVGPoint();
point.x = x;
point.y = y;
let ctm = parentSVG.getScreenCTM().inverse();
point = point.matrixTransform(ctm);
x = point.x;
// add fontsize to baseline shift
y = point.y + (parseFloat(style.fontSize) / 1.15);
// round
[x, y] = [x, y].map(val => {
return +(val).toFixed(3)
});
if (x !== xPrev && y !== yPrev) {
tspan.setAttribute('x', x)
}
if (y !== yPrev) {
tspan.setAttribute('y', y)
}
// text color to fill
if (currentProps.color !== 'rgb(0, 0, 0)') {
tspan.style.fill = currentProps.color;
}
// remove superfluous inherited props
tspan.style.removeProperty('margin');
tspan.style.removeProperty('padding');
if (parentProps.fontFamily == currentProps.fontFamily) {
tspan.style.removeProperty('font-family')
}
if (parentProps.fontSize == currentProps.fontSize) {
tspan.style.removeProperty('font-size')
}
if (parentProps.fontWeight == currentProps.fontWeight) {
tspan.style.removeProperty('font-weight')
}
if (parentProps.lineHeight == currentProps.lineHeight) {
tspan.style.removeProperty('line-height')
}
// copy content
tspan.textContent = child.textContent;
// stringify current style
let currentStyle = JSON.stringify(currentProps);
// add to svg
if (child.textContent.trim()) {
let prevTspans = newText.querySelectorAll('tspan');
let prev = prevTspans[prevTspans.length - 1];
if (prev && !tspan.getAttribute('x') &&
!tspan.getAttribute('y') &&
currentStyle == prevStyle
) {
prev.textContent += tspan.textContent;
} else {
newText.appendChild(tspan)
prevStyle = currentStyle;
}
}
xPrev = x;
yPrev = y;
})
return newText;
}
/**
* helper copy computed styles
*/
function copyStyleProps(el, target, styleProps = []) {
let defaultvaluesToExclude = {
'color': 'rgb(0, 0, 0)',
'fontStyle': 'normal',
'letterSpacing': 'normal',
'verticalAlign': 'baseline',
}
let currentProps = {};
// defaults
if (!styleProps.length) {
styleProps = [
'fontFamily',
'fontSize',
'fontStyle',
'fontWeight',
'lineHeight',
'letterSpacing',
'verticalAlign',
'margin',
'padding',
'color'
];
}
let style = window.getComputedStyle(el);
for (prop in style) {
let val = style[prop];
if (styleProps.includes(prop) && val !== defaultvaluesToExclude[prop]) {
target.style[prop] = val;
currentProps[prop] = val;
}
}
return currentProps;
}
/**
* Get text nodes in element
* based on:
* https://mcmap.net/q/83499/-find-all-text-nodes-in-html-page-duplicate#10730777
*/
function textNodesInEl(el) {
let textNodes = [];
for (el = el.firstChild; el; el = el.nextSibling) {
if (el.nodeType == 3) {
textNodes.push(el);
} else {
textNodes = textNodes.concat(textNodesInEl(el));
}
}
// filter empty text nodes
textNodes = textNodes.filter((node) => node.textContent.trim());
return textNodes;
}
textarea {
width: 100%;
min-height: 30em;
white-space: pre;
font-family: monospace;
font-size: 0.75em;
}
<p><button onclick="foreignObjectToNativeSvgtext(foreignObject)">Convert foreignObject</button></p>
<h3>SVG with foreignObject – text is editable</h3>
<svg id="svg" viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<style>
.foreignBody {
font-family: Georgia, serif;
font-size: 1em;
line-height: 1.5em;
}
h1 {
font-family: sans-serif;
font-size: 2em;
line-height: 1.2em;
margin: 0 0 1rem 0;
}
.author {
line-height: 1.2em;
font-style: italic;
margin-bottom: 0em;
}
p {
margin: 0 0 1rem 0;
}
sup {
line-height: 0px;
font-size: 0.5em;
}
ul li:marker{
content:'•';
color:red;
}
</style>
<foreignObject id="foreignObject" x="15" y="5" width="90%" height="90%">
<div class="foreignBody" xmlns="http://www.w3.org/1999/xhtml" contenteditable>
<p class="author">Franz Kafka</p>
<h1>The Metamorphosis</h1>
<p>One morning, when <strong>Gregor Samsa</strong> woke from troubled dreams, he found himself
transformed in
his bed into <em style="color:red; letter-spacing:0.1em">a horrible</em> vermin.<sup>1</sup></em>
</p>
<p>He lay on his armour-like back, and if he lifted his head a little he could see his brown belly,
slightly
domed and divided by arches <strong><em> into stiff sections.</em></strong> The bedding was hardly
able to
cover it and seemed ready to slide off any moment.</p>
</div>
</foreignObject>
</svg>
<h3>Output</h3>
<textarea id="output"></textarea>
How it works
loop through all block elements like
<p>
,<h1>
split text content into separate text nodes
retrieve style info for each text node via
getComputedStyle()
get positions via
getBoundingClientRect()
convert HTML coordinates to svg user units:
let point = parentSVG.createSVGPoint(); point.x = x; point.y = y; let ctm = parentSVG.getScreenCTM().inverse(); point = point.matrixTransform(ctm);
replace text nodes with
<text>
and<tspan>
elements
Future SVG2 concepts (not yet supported!)
Whitespace sensitive rendering. Firefox renders line wraps if
whitespace:pre
is applied to<text>
– on the other hand this won't work for optimized/minified svgs (when excessive white space gets removed)
text{
white-space: pre;
word-break: break-word;
}
svg{
width:20em;
border: 1px solid #ccc;
}
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 100 100" xml:space="preserve">
<text x="0" y="20" font-size="10">The Metamorphosis
One morning, when
Gregor Samsa woke
from troubled dreams,
he found himself transformed
in his bed into a
horrible vermin.1
</text>
</svg>
Related posts
- using range API: "Find rendered line breaks with javascript"
- Adding hyphenation: "Accessing displayed hyphens of an HTML element with JavaScript"
I tried all answers None of them works with me only I created noob solution but it will solve without unknown lines of code, Try to add additional text tag without content and validate the text length if it > than maximum first text length add the rest to another text tag and so on. you just need Simple JavaScript if statement and change the text Content
if (data['clinic']['cicovidcliniccity'].length > 35 && data['clinic']['cicovidcliniccity'].length < 75) {
const cname = data['clinic']['cicovidcliniccity'];
const ctext2_shodow = document.querySelector("#c_text2_shdow");
ctext2.textContent = cname.substring(1, 35)
ctext2_shodow.textContent = cname.substring(35, cname.length);
}
if (data['clinic']['cicovidcliniccity'].length > 75 && data['clinic']['cicovidcliniccity'].length < 110) {
const cname1 = data['clinic']['cicovidcliniccity'];
const ctext2_shodow = document.querySelector("#c_text2_shdow");
const ctext3_shodow = document.querySelector("#c_text3_shdow");
ctext2.textContent = cname1.substring(1, 35)
ctext2_shodow.textContent = cname1.substring(35, 75);
ctext3_shodow.textContent = cname1.substring(75, cname1.length);
}
another example with dynamic JS
const myTextContent = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's s";
const lineLength = 20;
const maxLineHeight = 15;
const mySVG = document.querySelector("svg");
let loopMax = Math.round(myTextContent.length/lineLength);
const txts = [];
for (let i=1; i<=loopMax; i++){
txts.push(myTextContent.substring((lineLength*i)-lineLength,lineLength*i));
}
txts.forEach( (txt,i)=>{
const newTxt = document.createElementNS("http://www.w3.org/2000/svg", "text");
newTxt.setAttribute("x", "0");
newTxt.setAttribute("y", `${maxLineHeight * (i+1)}`);
newTxt.setAttribute("fill", "red");
newTxt.textContent = txt;
mySVG.appendChild(newTxt);
});
<svg height="90" width="200">
</svg>
const svg=document.getElementsByTagName('svg')[0]
const type=(str,x=100,y=100,width=100,height=100,lineHeight=10)=>{
const g=(el='g',parent=svg,attrs={},html='')=>{
const child=document.createElementNS('http://www.w3.org/2000/svg',el)
parent.appendChild(child)
Object.keys(attrs).forEach(a=>child.setAttribute(a,attrs[a]))
child.innerHTML=html
return child
}
const rows=[]
for(let i=0;i<=Math.floor(height/lineHeight);i++){
rows.push(`M${x},${y+i*lineHeight} L${x+width},${y+i*lineHeight}`)
}
const id='text'+svg.children.length
const text=g('text',svg)
const underPath=g('path',text,{d:rows.join('\n'),stroke:'red',id})
const tp=g('textPath',text,{href:'#'+id},str)
//calc 1-try get 1 row, 2-try add enough spaces
let p=str.split(' ').reverse().map(a=>a+' ')
let k=[]
tp.innerHTML='.'
let currentRow=tp.getBoundingClientRect().height
do{
k=k.concat(p.reverse())
p=[]
tp.innerHTML=k.join('')
while(tp.getBoundingClientRect().height>currentRow){
p.push(k.pop())
tp.innerHTML=k.join('')
}
k[k.length-1]=(' ').repeat(120)+k[k.length-1].replace(' ','')
tp.innerHTML=k.join('')
while((tp.getBoundingClientRect().height>currentRow) && k[k.length-1].match(' ')){
k[k.length-1]=k[k.length-1].replace(' ','')
tp.innerHTML=k.join('')
}
tp.innerHTML=k.join('')+'..'
currentRow=tp.getBoundingClientRect().height
}while(p.length>1)
tp.innerHTML=k.concat(p.reverse()).join('')
}
type('Quick brown fox jumps over the lazy dog. Quick brown fox jumps over the lazy dog.',10,100,170,50)
https://codepen.io/D-K-the-lessful/pen/yLZrpOE?editors=0011 word wrapping on textPath and algorithm reduce fillers and checking size of element enter image description here
2023 Edition for Web (tested in all major browsers):
<switch>
<foreignObject requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<p id="ModernText">My really long text</p>
</foreignObject>
<text id="FallbackText">Fallback Description</text>
</switch>
© 2022 - 2024 — McMap. All rights reserved.