14

I'm trying to wrap text by building up a text string, and using getComputedTextLength to find out when the text goes beyond the allowed width. However, I can't find a simple way to incrementally build up the text which will work with getComputedTextLength.

The general idea is:

  str = svgDocument.createTextNode(myText[word]); // first word on new line
  word++;
  obj = text.cloneNode(true);                     // new text element for this line
  obj.appendChild(str);
  svgDocument.documentElement.appendChild(obj);   // reqd for getComputedTextLength?
  for( ; word < myText.length; word++) {
     next_width = obj.getComputedTextLength();    // get current line width
     if(next_width >= extent)
        break;
     str += " ";                                  // add next word to the line
     str += myText[word];
     ...
  }

Can anyone tell me how to get this to work? Presumably str is copied rather than referenced in obj, but I've also tried putting obj.removeChild(str) and obj.appendChild(str) in the loop, but the appendChild crashes. I've also tried various combinations of moving around the documentElement.appendChild, and removing obj and re-appending it, and so on.

Drew Gaynor
  • 8,292
  • 5
  • 40
  • 53
EML
  • 9,619
  • 6
  • 46
  • 78

2 Answers2

21

This should work:

    var svgNS = "http://www.w3.org/2000/svg";
    var width = 200;

    function init(evt)
    {
        if ( window.svgDocument == null ) {
            svgDocument = evt.target.ownerDocument;
        }
        create_multiline("Whatever text you want here.");
    }

    function create_multiline(text) {
        var words = text.split(' ');                        
        var text_element = svgDocument.getElementById('multiline-text');
        var tspan_element = document.createElementNS(svgNS, "tspan");   // Create first tspan element
        var text_node = svgDocument.createTextNode(words[0]);           // Create text in tspan element

        tspan_element.appendChild(text_node);                           // Add tspan element to DOM
        text_element.appendChild(tspan_element);                        // Add text to tspan element

        for(var i=1; i<words.length; i++)
        {
            var len = tspan_element.firstChild.data.length;             // Find number of letters in string
            tspan_element.firstChild.data += " " + words[i];            // Add next word

            if (tspan_element.getComputedTextLength() > width)
            {
                tspan_element.firstChild.data = tspan_element.firstChild.data.slice(0, len);    // Remove added word

                var tspan_element = document.createElementNS(svgNS, "tspan");       // Create new tspan element
                tspan_element.setAttributeNS(null, "x", 10);
                tspan_element.setAttributeNS(null, "dy", 18);
                text_node = svgDocument.createTextNode(words[i]);
                tspan_element.appendChild(text_node);
                text_element.appendChild(tspan_element);
            }
        }


    }
]]>
</script>

<text x="10" y="50" id="multiline-text"> </text>

It works by adding tspan elements to the text element and then adding text to each of them.

The result is something like:

<text>
  <tspan>Whatever text</tspan>
  <tspan>you want here.</tspan>
</text>

In order for getComputerTextLength to work, you need to create the tspan (or text) element first and make sure it is in DOM. Also note that in order to add text to a tspan element, you need to use createTextNode() and append the result.

Peter Collingridge
  • 10,849
  • 3
  • 44
  • 61
  • Hi Peter - thanks for the code. I was looking at my original code today and found that my basic problem was misunderstanding how to grow text in a text node. I got basic wrapping working with appendChild, but your tspan stuff looks good - I'll check it out. Thanks -Al – EML Aug 14 '11 at 22:01
  • Cool - the tspans are useful as it means you can select the multiple lines of text at once. – Peter Collingridge Aug 14 '11 at 22:06
  • Still helpful 7 years later, which I suppose is both good and bad! – kiminoa Jan 01 '18 at 01:13
4

a wrapper function for overflowing text:

    function wrap() {
        var self = d3.select(this),
            textLength = self.node().getComputedTextLength(),
            text = self.text();
        while (textLength > (width - 2 * padding) && text.length > 0) {
            text = text.slice(0, -1);
            self.text(text + '...');
            textLength = self.node().getComputedTextLength();
        }
    } 

usage:

text.append('tspan').text(function(d) { return d.name; }).each(wrap);
user2846569
  • 2,752
  • 2
  • 23
  • 24
  • The OP does not appear to be using D3. – Robert Longson Dec 31 '14 at 16:45
  • 1
    What does OP stand for? – user2846569 Dec 31 '14 at 16:47
  • 1
    Original Poster. What does D3 stand for? :) – EML Dec 31 '14 at 17:24
  • 4
    D3 is JavaScript Library used to manipulate SVG. @RobertLongson, OP is not the only one who reads this. There is other people who needs solution for this and even more, some uses D3 as was in my case. Cheers! – user2846569 Dec 31 '14 at 17:30
  • Indeed which is why there's a d3 question about this very topic that you already attached this exact answer to! (http://stackoverflow.com/questions/15975440/add-ellipses-to-overflowing-text-in-svg) – Robert Longson Dec 31 '14 at 17:31
  • 1
    Plain JS/DOM questions are STILL answered with jQuery snippets, yet the first question I see answered with d3.js is criticized. The answer from user2846569 is useful, because 1) d3 users interested in the topic may find his answer useful if they land on this answer first, 2) Core d3.js can be called the 'jQuery of SVG on steroids' - there is a large overlap between d3 users and those that need text truncation in SVG; 3) The OP or a reader might learn that what's a rather long, imperative, entangled code in the OP's question might be alternatively done with the more declarative d3 library. – Robert Monfera Dec 04 '15 at 08:38
  • 1
    Seems this requires that the SVG be visible: For example, if it's wrapped by something with `display:none;`, then `getComputedTextLength()` will return 0. This is my browser's behavior. Can anyone confirm? – Nate Anderson Nov 09 '16 at 20:24
  • might be so ... you can try hiding parent with "opacity: 0" – user2846569 Nov 10 '16 at 21:17