Multi-line text box


16 Aug 2011 Code on Github

One feature that I know a lot of people, myself included, would like in SVG is a multi-line text box. This is my solution to the problem, which converts a <text> element into a series of <tspan> to avoid having the text go beyond a certain length. <tspan> elements are sections of text that go inside a text element, allowing you to style or reposition bits of text.

I've explained roughly how it works below and you can find all the code here.

Alice was beginning to get very tired of sitting by her sister on the bank, and of having nothing to do: once or twice she had peeped into the book her sister was reading, but it had no pictures or conversations in it, "and what is the use of a book," thought Alice "without pictures or conversation?" So she was considering in her own mind (as well as she could, for the hot day made her feel very sleepy and stupid), whether the pleasure of making a daisy- chain would be worth the trouble of getting up and picking the daisies, when suddenly a White Rabbit with pink eyes ran close by her.

The SVG contains two texts element:

<text x="5" y="20" class="multiline" data-width="280">...</text>
<text x="160" y="160" class="multiline" data-width="200">...</text>

The SVG also contains a script element, that looks for elements with a class "multiline" and ensures the width of their text is no more than the value in the data-width attribute.

How it works

The script starts by getting all the elements with a class of "multiline", and passing them into a function createMultiline().

var textBoxes = document.getElementsByClassName('multiline');
for (var i = 0; i < textBoxes.length; i++) {
    createMultiline(textBoxes[i])
}

Inside the createMultiline() function we first get some attributes from the textElement. words is the text string split into words, startX is the x coordinate for the text element, and width is the target width.

function createMultiline(textElement) {
    var words = textElement.firstChild.data.split(' ');
    var startX = textElement.getAttributeNS(null, 'x');
    var width = parseFloat(textElement.getAttributeNS(null, 'data-width'));

Then we can remove the words from the original text element.

textElement.firstChild.data = "";

Next we create a <tspan> element as a child of the textElement and give it a textNode containing the first word.

var tspanElement = document.createElementNS("http://www.w3.org/2000/svg", "tspan");
var textNode = document.createTextNode(words[0]);
tspanElement.appendChild(textNode);
textElement.appendChild(tspanElement);

This means we'll end up with a structure like this:

<text>
  <tspan>Alice<tspan>
</text>

Then we loop through the rest of the words (starting at index 1 because we've already added the first word).

for (var i = 1; i < words.length; i++) {

Then we record the current length of the text in terms of number of characters, before adding a space and the next word.

var len = textNode.data.length;
tspanElement.firstChild.data += " " + words[i];

Then we calculate the new length of the <tspan> element in pixels, and test whether its longer than the target width.

if (tspanElement.getComputedTextLength() > width) {

If we have exceeded the width, the we use the len variable to reduce the string length back to the previous word.

tspanElement.firstChild.data = tspanElement.firstChild.data.slice(0, len);

Then we create a new <tspan> element, setting its x attribute the original x value, and its dy attribute to move it down a line. I've used 20 units, but you can change it depending on what you want the line height to be.

tspanElement = document.createElementNS("http://www.w3.org/2000/svg", "tspan");
tspanElement.setAttributeNS(null, "x", startX);
tspanElement.setAttributeNS(null, "dy", 20);

textNode = document.createTextNode(words[i]);
tspanElement.appendChild(textNode);
textElement.appendChild(tspanElement);

The loop continues until all the words have been added.

Comments (2)

Jelle on 14 May 2012, 12:27 p.m.

Peter,

Isn't it possible to just create a <text class="multiline" value="280" /> to avoid Firefox from not loading it? Current version of (12.0) still doesn't support onload :-(

I haven't tested it right now, but would this also support xlink:href? Otherwise it may be an option to just have the script read an external .txt file and fill it in. As such it may need a height declaration as well and a way to add a carriage return for paragraphing.

Peter on 4 May 2018, 11:52 a.m.

Six years later I've updated my approach to basically work that way.