♪1999 Scalable Object 輝ける時代の救世主 (c) 聖飢魔II

リンクやボタンの子孫object要素で読み込んだSVGをインタラクティブにする

a要素の子孫のobject要素やbutton要素の子孫のobject要素でSVGを読み込む場合には、そのままではリンクやクリックが効かず、ちょっと困ったことになる。

その対処法はすでにいろんな方が指摘していて、


a, button {
  display: inline-block;
}
a object, button object {
  pointer-events: none;
}

といった具合にCSSを指定すればよい。
SVGをobject要素で活用する | 水無月ばけらのえび日記
SVGをobject要素で表示してリンクにする - ういはるかぜの化学 - subtech
Retina対応にSVGを使う方法とリンクを張る時の注意点 - ROCHAS

ところが、当然のことながらSVGにマウスイベントなどが伝わらないので、:hoverで色が変わるようにSVG内にCSSで指定していても色は変わらない…orz

そこで、a要素やbutton要素が受け取ったイベントを利用して、子孫のSVGをインタラクティブにするJavaScriptを書いてみた。 object要素のSVGでインタラクティブにCSSを変更するテスト

スクリプトを利用するにあたり事前の準備としてHTMLとSVGに一手間が必要で、SVG内のCSSには、たとえば


/* SVGのCSS */
#target {
  fill: green;
}
#target:hover, #target.hover {
  fill: red;
}

といった具合に:hover:focus:activeのいわゆるダイナミック疑似クラスと同じ名前のclass名の指定を付記しておく。

一方、HTML側では、object要素にdata-svgtarget属性を付与して、SVGで変化させたい要素のid値を指定しておく。 これは必須ではなく、省略されていた場合はルートのsvg要素にclassが付与される。


<a href="http://asamuzak.jp">
  <object type="image/svg+xml" data="mysvg.svg" data-svgtarget="mytarget">
  </object>
</a>

JavaScript (最終更新:2016/3/14)


/*
 * Make SVG Interactive
 * Toggle SVG class from an ancestor anchor element or button element's event type and make SVG interactive
 * Copyright (c) 2016 Kazz
 * http://asamuzak.jp
 * Dual licensed under MIT or GPL
 * http://asamuzak.jp/license
 */

(function(_win, _doc) {
  "use strict";
  function toggleClassByEventType(obj, type) {
    if(obj) {
      var x = "";
      switch(type) {
        case "pointerover":
        case "pointerout":
        case "touchstart":
        case "mouseover":
        case "mouseout":
          x = "hover";
          break;
        case "focus":
          x = "focus";
          break;
        case "pointerdown":
        case "pointerup":
        case "mousedown":
        case "mouseup":
        case "keydown":
        case "keyup":
          x = "active";
          break;
        default:
      }
      if(obj.classList) {
        obj = obj.classList;
        switch(type) {
          case "pointerover":
          case "touchstart":
          case "mouseover":
          case "focus":
          case "pointerdown":
          case "mousedown":
          case "keydown":
            obj.add(x);
            break;
          case "pointerout":
          case "mouseout":
          case "pointerup":
          case "mouseup":
          case "keyup":
            obj.remove(x);
            break;
          case "blur":
            obj.remove("hover", "focus", "active");
            break;
          default:
        }
      }
      else {
        if(obj.hasAttributeNS(null, "class")) {
          var a = (obj.getAttributeNS(null, "class")).trim(), b, c, i, l;
          switch(type) {
            case "pointerover":
            case "touchstart":
            case "mouseover":
            case "focus":
            case "pointerdown":
            case "mousedown":
            case "keydown":
              a.indexOf(x) === -1 &&
                obj.setAttributeNS(null, "class", (a + " " + x).trim());
              break;
            case "pointerout":
            case "mouseout":
            case "pointerup":
            case "mouseup":
            case "keyup":
              if(a.indexOf(x) !== -1) {
                for(a = a.split(/\s+/), b = [], i = 0, l = a.length; i < l; i++) {
                  c = a[i];
                  c !== x && b.push(c);
                }
                obj.setAttributeNS(null, "class", b.join(" "));
              }
              break;
            case "blur":
              for(a = a.split(/\s+/), b = [], i = 0, l = a.length; i < l; i++) {
                c = a[i];
                c !== "hover" && c !== "focus" && c !== "active" && b.push(c);
              }
              obj.setAttributeNS(null, "class", b.join(" "));
              break;
            default:
          }
        }
        else {
          obj.setAttributeNS(null, "class", x);
        }
      }
    }
  }
  function getSvgObject(evt) {
    var x = evt.target.querySelectorAll("[data-svgtarget]"),
        l = x.length;
    if(l > 0) {
      for(var i = 0; i < l; i++) {
        var y = x[i];
        if(y.contentDocument) {
          var z = y.contentDocument,
              a = y.getAttribute("data-svgtarget");
          toggleClassByEventType(
            a !== "_svg" && z.getElementById(a) ?
              z.getElementById(a) : z.documentElement,
            evt.type
          );
        }
      }
    }
  }
  _doc.addEventListener("DOMContentLoaded", function() {
    var x = _doc.querySelectorAll("a object, button object"),
        l = x.length;
    if(l > 0) {
      for(var i = 0; i < l; i++) {
        var y = x[i];
        if(y.hasAttribute("type") &&
           y.getAttribute("type") === "image/svg+xml" ||
           y.hasAttribute("data") &&
           /^data:image\/svg\+xml|\.svgz?$/.test(y.getAttribute("data"))) {
          var z = y.parentNode;
          while(z.parentNode) {
            if(/^a|button$/i.test(z.nodeName)) {
              break;
            }
            z = z.parentNode;
          }
          !y.hasAttribute("data-svgtarget") &&
            y.setAttribute("data-svgtarget", "_svg");
          if(_win.PointerEvent) {
            z.addEventListener("pointerover", getSvgObject, false);
            z.addEventListener("pointerdown", getSvgObject, false);
            z.addEventListener("pointerup", getSvgObject, false);
            z.addEventListener("pointerout", getSvgObject, false);
          }
          else {
            _win.TouchEvent &&
              z.addEventListener("touchstart", getSvgObject, false);
            z.addEventListener("mouseover", getSvgObject, false);
            z.addEventListener("mousedown", getSvgObject, false);
            z.addEventListener("mouseup", getSvgObject, false);
            z.addEventListener("mouseout", getSvgObject, false);
          }
          z.addEventListener("focus", getSvgObject, false);
          z.addEventListener("keydown", getSvgObject, false);
          z.addEventListener("keyup", getSvgObject, false);
          z.addEventListener("blur", getSvgObject, false);
        }
      }
    }
  }, false);
})(window, document);

注意点

  • iOSではa要素やbutton要素の子孫のobject要素に設定したpointer-events:none;が効かず、祖先がイベントのターゲットにならない。 WebKit Bugzillaには報告してあるが、このバグが修正されるまでは次のようなハックが必要。
    
    a, button {
      display: inline-block;
      position: relative;
    }
    a::after, button::after {
      position: absolute;
      top: 0;
      right: 0;
      bottom: 0;
      left: 0;
      content: "";
    }
    
    なんとも前世紀の遺物的なハックだが…w
  • EdgeやIEでは、ある状態(たとえば:hover)でtransform:translate()で要素を移動しようとしても移動しない。 Chromeにも、transform:translate()で要素を移動したら元の位置に戻らないというバグを見つけたがあっという間にFixされて51.0.2672.1で修正済み。 Issue 592206 - chromium
  • IE9では、buttonの子孫objectでSVGを読み込んだ場合に、SVG上ではbuttonの:hoverが外れてclickイベントも伝わらない。 IEは今後修正されることはないので、2017年春にVistaのサポートが切れるまでは、button要素でSVGを使いたい場合は、object要素ではなくimg要素で読み込む方が吉かと。 IE11ではOK。 IE9もa要素では問題ない。

"♪1999 Scalable Object 輝ける時代の救世主 (c) 聖飢魔II"へのTwitter上でのコメントやRT

7件のツイートがあります。

ツイート 1

ツイート 2

ツイート 3

ツイート 4

ツイート 5

ツイート 6

ツイート 7