たれぱんのびぼーろく

わたしの備忘録、生物学とプログラミングが多いかも

SVGのtextをその場で書き換える

下のtext要素をクリックしてごらん、なんと編集できるよ!!
私はtext要素。でも編集できるよ! (クリックしてみ)

わかる人向け: 仕組みを一言でいうと

contentEditableなHTML要素の下にtext要素を配置し、このセットをforeignObjectへ入れてSVG要素下に吊るす.

仕組み

contentEditable

HTML5にはcontentEditableという属性がある。

<div contenteditable>編集可能</div>

のように属性を付与すると、

divが編集可能 (クリックしてみ)

になる.

SVGのcontentEditable

contentEditableがSVGで使えれば万々歳、なのだが、使えない...
(次のSVGに導入しようぜ、という提案はされてたりする Add `contenteditable` attribute for SVG text elements · Issue #332 · w3c/svgwg · GitHub)

\\動かない例
<svg>
 <text contenteditable>SVGは編集不可 (クリックしても無駄無駄、無駄ァ)</text>
</svg>

SVGは編集不可 (クリックしても無駄無駄、無駄ァ)

しかし嬉しいことに、contentEditableなHTML要素の下にSVG要素を配置すると, contentEditableが引き継がれる https://github.com/w3c/svgwg/issues/332 (標準化された動作じゃない...と思う)
なので

<div contenteditable>
  <text>divに入れるとSVGを編集可能</text> // SVGのtext要素
</div>

とすると、

divに入れるとSVGを編集可能 (クリックしてみ)

になる.
やったねたえちゃん、SVGが編集できるよ

foreignObject

ここで問題になるのが<svg>下のHTML要素.
おそらくこのページに来た人は、SVGで作りこんだ図に編集可能textを入れ込みたいのだと思う.
つまり、<svg>要素下にrectやらcircleやら置いてあることになる.
textを編集可能にするために、上記の<div contenteditable>を利用すればよいわけだが...
<svg>下にHTML要素(例えば<div>)を置くことはできない javascript - How to append div tag into a SVG rect element? - Stack Overflow
そこで登場するのがforeignObject.
この要素を配置するとあら不思議、HTML要素をforeignObject下に配置しても無問題!!
よって

<svg>
 <foreignObject>
   <div contenteditable>
     <text>私はtext要素。でも編集できるよ! (クリックしてみ)</text>
   </div>
 </foreignObject>
</svg>

のようにすれば、
私はtext要素。でも編集できるよ! (クリックしてみ)

となる。

問題点

  • contentEditableはあくまでHTML属性
    • SVGの仕様で定められた、互換性のあるものではない
  • foreignObject要素への対応
    • 一部のブラウザでforeignObject要素を適切に処理できない場合があるとかないとか

まとめ

  • foreignObject下にcontentEditable HTML要素、その下にtextを置けば編集できる
  • 互換性などの点でベストとはとても言い難いよ、でも便利だね

おまけ

d3.jsと組み合わせると

HTML, non-contentEditable
HTML contentEditable
HTML, non-contentEditable. But enabled by d3.js!!
<div id="divv2">HTML, non-contentEditable. But enabled by d3.js!!</div>
d3.select("#divv2").attr("contenteditable", true);

d3.select("#svgEdit")
  .append("foreignObject")
    .attr("width", 600)
    .attr("height", 50)
    .append("xhtml:div")
      .attr("contenteditable", true)
      .append("text")
        .text("dynamic editable text by d3.js!!!");