如何在JavaScript中实现SVG文本换行?

145

这是我拥有的内容:

<path class="..." onmousemove="show_tooltip(event,'very long text 
    \\\n I would like to linebreak')" onmouseout="hide_tooltip()" d="..."/>

<rect class="tooltip_bg" id="tooltip_bg" ... />
<text class="tooltip" id="tooltip" ...>Tooltip</text>

<script>
<![CDATA[
function show_tooltip(e,text) {
    var tt = document.getElementById('tooltip');
    var bg = document.getElementById('tooltip_bg');

    // set position ...

    tt.textContent=text;

    bg.setAttribute('width',tt.getBBox().width+10);
    bg.setAttribute('height',tt.getBBox().height+6);

    // set visibility ...
}
...

现在我的非常长的工具提示文本没有换行,即使我使用alert();它向我显示文本实际上确实有两行。(它包含一个"\",顺便问一下,我该如何删除这个字符?)
无论我在哪里都无法让CDATA起作用。

7个回答

195

SVG 1.1不支持此功能。SVG 1.2具有带有自动换行的textArea元素,但并非所有浏览器都实现了它。SVG 2 不打算实现textArea,但它确实具有自动换行文本

然而,如果您已经知道应该出现哪些换行符,您可以将文本分成多个<tspan>,每个<tspan>均具有x="0"dy="1.4em"以模拟实际的文本行。例如:

<g transform="translate(123 456)"><!-- replace with your target upper left corner coordinates -->
  <text x="0" y="0">
    <tspan x="0" dy="1.2em">very long text</tspan>
    <tspan x="0" dy="1.2em">I would like to linebreak</tspan>
  </text>
</g>
当然,由于你想要从JavaScript中执行,你需要手动创建并将每个元素插入到DOM中。

2
我该如何识别在哪里放置<tspan>标签呢?是替换还是分割? - sollniss
2
尝试了一下
var tspan = document.createElement('tspan') tspan.setAttribute('x','0'); tspan.setAttribute('dy','1.2em'); tspan.textContent = text; tt.appendChild(tspan); 根本没有显示任何文本。
- sollniss
2
你能详细说明一下为什么需要 x='0' dy='1.2em' 吗?它确实有效,就像你说的那样。但是,我本来期望它可以在没有这些属性的情况下工作。相反,什么都没有显示出来... 另外,我也不太清楚为什么会发生换行。我们并没有设置容器的宽度为固定值,以便它可以强制换行,对吧? - Konrad Viltersten
4
x=0 是一个绝对坐标:将文本片段移动到当前坐标系的原点。g 元素上的 transform 属性定义了一个新的当前坐标系,假设文本是左对齐的,那么 tspan 就会向左移动。这就像是回车指令一样。dy=1.2em 是一个相对坐标:将文本碎片相对于当前文本碎片移动这个量。这就像是换行指令一样。结合在一起,你就得到了一个 CR/LF。 - Sergiu Dumitriu
1
@Richard 它确实可以运行,但有一个小缺点,你必须自己计算每一行的坐标。x = 0 dy = 1.2em更简单,对于每一行都是相同的,而且不会依赖于字体大小。另一个优点是,如果你想移动整个文本,你只需要更新组的“transform”属性中的一对值,而不是更新每个tspan的坐标。 - Sergiu Dumitriu
显示剩余3条评论

36

我想你已经成功解决了这个问题,但如果有人正在寻找类似的解决方案,那么这对我很有效:

 g.append('svg:text')
  .attr('x', 0)
  .attr('y', 30)
  .attr('class', 'id')
  .append('svg:tspan')
  .attr('x', 0)
  .attr('dy', 5)
  .text(function(d) { return d.name; })
  .append('svg:tspan')
  .attr('x', 0)
  .attr('dy', 20)
  .text(function(d) { return d.sname; })
  .append('svg:tspan')
  .attr('x', 0)
  .attr('dy', 20)
  .text(function(d) { return d.idcode; })

这里有三行文字,分别用换行符分隔。


31
FWIW:看起来原帖作者使用的是纯JavaScript;这个回答似乎利用了D3 - Ben Mosher
我正在使用D3,你的方法对我很有用。谢谢你发布它。我发现在附加新的tspan之前,需要先删除旧的tspan,像这样:focus.selectAll("tspan").remove(); - Darren Parker
1
请注意,此方法嵌套了<tspan>标签,因为它使用了.append()。这可能会导致一些与CSS有关的小问题,具体取决于您想要做什么。 - seneyr
请点击这里查看一种避免@seneyr所描述的嵌套的方法。 - maltem-za
通过现代的d3,可以利用.call()方法来避免对每个append组进行嵌套。 - Simon

22

使用tspan解决方案,假设您事先不知道在哪里放置换行符:可以使用我在这里找到的这个好用的函数: http://bl.ocks.org/mbostock/7555321

它可以自动为给定像素宽度的长文本SVG进行换行。

function wrap(text, width) {
  text.each(function() {
    var text = d3.select(this),
        words = text.text().split(/\s+/).reverse(),
        word,
        line = [],
        lineNumber = 0,
        lineHeight = 1.1, // ems
        y = text.attr("y"),
        dy = parseFloat(text.attr("dy")) || 0,
        tspan = text.text(null).append("tspan").attr("x", 0).attr("y", y).attr("dy", dy + "em");
    while (word = words.pop()) {
      line.push(word);
      tspan.text(line.join(" "));
      if (tspan.node().getComputedTextLength() > width) {
        line.pop();
        tspan.text(line.join(" "));
        line = [word];
        tspan = text.append("tspan").attr("x", 0).attr("y", y).attr("dy", ++lineNumber * lineHeight + dy + "em").text(word);
      }
    }
  });
}

17

使用HTML替代Javascript

限制:SVG渲染器必须支持HTML渲染

例如,Inkscape无法渲染此类SVG文件

<html>
  <head><style> * { margin: 0; padding: 0; } </style></head>
  <body>
    <h1>svg foreignObject to embed html</h1>

    <svg
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 300 300"
      x="0" y="0" height="300" width="300"
    >

      <circle
        r="142" cx="150" cy="150"
        fill="none" stroke="#000000" stroke-width="2"
      />

      <foreignObject
        x="50" y="50" width="200" height="200"
      >
        <div
          xmlns="http://www.w3.org/1999/xhtml"
          style="
            width: 196px; height: 196px;
            border: solid 2px #000000;
            font-size: 32px;
            overflow: auto; /* scroll */
          "
        >
          <p>this is html in svg 1</p>
          <p>this is html in svg 2</p>
          <p>this is html in svg 3</p>
          <p>this is html in svg 4</p>
        </div>
      </foreignObject>

    </svg>

</body></html>


我认为你的意思是“使用SVG而不是JavaScript”。 - Valerio Bozz
这是“SVG中的HTML”,对我来说是最好的解决方案! - Kévin Berthommier
想不到啊! 有什么陷阱吗? - Nicolas Reibnitz
添加了“限制”。 - milahu

10

我认为这样做可以满足你的需求:

function ShowTooltip(evt, mouseovertext){
    // Make tooltip text        
    var tooltip_text = tt.childNodes.item(1);
    var words = mouseovertext.split("\\\n");
    var max_length = 0;

    for (var i=0; i<3; i++){
        tooltip_text.childNodes.item(i).firstChild.data = i<words.length ?  words[i] : " ";
        length = tooltip_text.childNodes.item(i).getComputedTextLength();
        if (length > max_length) {max_length = length;}
    }

    var x = evt.clientX + 14 + max_length/2;
    var y = evt.clientY + 29;
    tt.setAttributeNS(null,"transform", "translate(" + x + " " + y + ")")

    // Make tooltip background
    bg.setAttributeNS(null,"width", max_length+15);
    bg.setAttributeNS(null,"height", words.length*15+6);
    bg.setAttributeNS(null,"x",evt.clientX+8);
    bg.setAttributeNS(null,"y",evt.clientY+14);

    // Show everything
    tt.setAttributeNS(null,"visibility","visible");
    bg.setAttributeNS(null,"visibility","visible");
}

它将文本按\\\n进行分割,并将每个片段放入一个tspan中。然后根据最长的文本长度和行数计算所需的框大小。您还需要更改提示文本元素,使其包含三个tspan:

<g id="tooltip" visibility="hidden">
    <text><tspan>x</tspan><tspan x="0" dy="15">x</tspan><tspan x="0" dy="15">x</tspan></text>
</g>

这假设你从未拥有超过三行。如果你想要更多的行数,你可以添加更多的 tspans 并增加 for 循环的长度。


1
为什么是"\\\n"而不是"\n" - ralien

2

我稍微改进了@steco的解决方案,将依赖项从d3切换到jquery并添加文本元素的height作为参数。

function wrap(text, width, height) {
  text.each(function(idx,elem) {
    var text = $(elem);
    text.attr("dy",height);
        var words = text.text().split(/\s+/).reverse(),
        word,
        line = [],
        lineNumber = 0,
        lineHeight = 1.1, // ems
        y = text.attr("y"),
        dy = parseFloat( text.attr("dy") ),
        tspan = text.text(null).append("tspan").attr("x", 0).attr("y", y).attr("dy", dy + "em");
    while (word = words.pop()) {
      line.push(word);
      tspan.text(line.join(" "));
      if (elem.getComputedTextLength() > width) {
        line.pop();
        tspan.text(line.join(" "));
        line = [word];
        tspan = text.append("tspan").attr("x", 0).attr("y", y).attr("dy", ++lineNumber * lineHeight + dy + "em").text(word);
      }
    }
  });
}

0
解决方案对我的情况不起作用,因为我在我的React应用程序中需要一个动态的解决方法(尤其是我在一个多语言应用程序上工作,每种语言的换行可能是不同的)。所以我在需要换行的文本中使用了\n,然后使用map方法在tspan元素中创建多行文本,其中有一个新行。
const SvgComponent = ({ text }) => {
// Split the text into an array of lines based on line breaks ("\n")
const lines = text.split('\n');

return (
  <svg width="200" height="200">
    {lines.map((line, index) => (
      // Use your desired x and y coordinates and line space
      <text key={index} x="10" y={10 + index * 20}>
        <tspan>{line}</tspan>
      </text>
    ))}
  </svg>
);
};

export default SvgComponent;

当您将带有换行符的文本作为文本属性传递给此组件时,它将在SVG中的每一行文本上呈现一个新行。例如:
<SvgComponent text="Line 1\nLine 2\nLine 3" />

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接