htsign's blog

ノンジャンルで書くつもりだけど技術系が多いんじゃないかと思います

マルチメディア要素へのリンクのクリックイベントに<audio>や<video>なウィンドウをポップアップする機能をオーバーライドするブックマークレット

たぶんこれで完成形。

比較的新しいAPIをフル活用しているので、動かなかったらそんなブラウザを使っている自分が悪いと思ってください。
一応IE10とChrome26で動作を確認しています。
Firefox20ではちょっと確認した限り動作しませんでした。まぁFirefoxはクズってことで


(function(){
  var checked = [];        // 同じURLに複数回リクエストを送るのを防ぐためのリストを格納
  var timeout = 30 * 1000; // タイムアウトになるまでの時間(単位:ms)
  
  var allowList = ["audio", "video"];

  if (!HTMLElement.prototype.addPopup) { // addPopupメソッドを要素に追加

    Object.defineProperty(HTMLElement.prototype, "addPopup", {
      value: function(url){
        var self = this;

        var xhr = new XMLHttpRequest(); // HEADリクエストを送る
        xhr.open("HEAD", url, true);
        xhr.onreadystatechange = function(){
          if (xhr.readyState === 4) {
            console.log(xhr.status + " : " + url);

            if (xhr.status !== 200) return;

            var header = xhr.getResponseHeader("Content-Type");
            var type   = header.split("/")[0];

            if (allowList.some(function(v){ return v === type })) { // いずれかにヒットすれば true
              console.log(header + " - OK");
              addPopupItem(self, type);
            }
          }
        };
        xhr.send(null);

        window.setTimeout(function(){ // 一定時間でタイムアウト
          xhr.abort();
        }, timeout);
      }
    });
  }
  else {
    return false;
  }

  var elems = document.querySelectorAll("a[href], area[href]");
  elems = Array.prototype.slice.call(elems); // リンカブル要素を全て抽出

  for (var i in elems) {
    var url = elems[i].href;

    if (url.indexOf("http") === 0) {

      url = url.split("#")[0]; // URLに # を含む場合は除去

      if (checked.every(function(li){ return url !== li })) { // 全てとアンマッチなら true
        checked.push(url);
        elems[i].addPopup(url);
      }
    }
  }

  function addPopupItem(target, mediaType) { // ポップアップを生成
    target.onclick = function(evt){
      if (evt.ctrlKey && evt.altKey) {
        return true; // Ctrl+Alt を押しながらクリックすれば普段のクリックと同じ効果
      }
      else {
        openPopup();
        evt.preventDefault();
      }
    };

    function openPopup() {
      var popup = window.open("", "media-popup", "titlebar=no,menubar=no,toolbar=no,location=no,status=no,scrollbars=no");
      var doc   = popup.document;
      var rect;
      var element;

      var diff = {
        width:  popup.outerWidth  - doc.documentElement.clientWidth,
        height: popup.outerHeight - doc.documentElement.clientHeight
      };

      if (!rect) {
        element = doc.createElement(mediaType);
        element.setAttribute("controls", "controls");
        doc.body.appendChild(element);
        rect = element.getBoundingClientRect();
        doc.body.removeChild(element);
      }
      
      doc.body.addEventListener("DOMSubtreeModified", function(){ // 要素数が変化したらウィンドウサイズを変える
        var len = this.childNodes.length;
        popup.resizeTo( rect.width + diff.width, rect.height * len + diff.height );
      }, false);

      var style = doc.createElement("style");
      doc.querySelector("head").appendChild(style);
      var s = style.sheet, i = 0;
      [
        "body {"
        + "margin: 0;"
        + "padding: 0;"
        + "}",
        "audio {"
        + "display: block;"
        + "margin: auto;"
        + "}"
      ].forEach(function(value){
        s.insertRule(value, i++);
      });

      element = doc.createElement(mediaType);
      element.setAttribute("src", target.href);
      element.setAttribute("autoplay", "autoplay");
      element.setAttribute("controls", "controls");
      element.addEventListener("ended", function(){ // 終端まで移動したら自らを削除する
        this.parentElement.removeChild(this);
        close();
      }, false);

      /*
        末尾に挿入する
        appendChildでないのはIEがDOMSubtreeModifiedに反応しない(らしい)ため
        http://msdn.microsoft.com/en-us/library/gg558014(v=vs.85).aspx
      */
      doc.body.insertBefore(element, null);

      function close() { // ポップアップ内のメディアがゼロのとき、子ウィンドウを閉じる
        doc.querySelectorAll( allowList.join(",") ).length === 0
        ? popup.close() : 0;
      }
    }
  }

})();

解説書くのすげーめんどくさいんですが一応解説

前回はウィンドウのサイズを中身の大きさに合わせて変更する術が思いつかなかったので適当にサイズ決めてましたが、
ウィンドウサイズはwindow.outerWidthwindow.outerHeightで取れるし
ウィンドウの中身はdocument.documentElement.clientWidthdocument.documentElement.clientHeightで取れることに
今更気づいてその辺をルーチンに組み込みました。
気づいてみれば呆気ないものですね…。


で、ウィンドウサイズを変更するタイミングですが、当然中身のサイズが変わったときです。
中身のサイズが変わるということはDOMツリーに変化があるということ。

DOMツリーが変わると発火する便利なイベントがあります。
それがMutationEventsと呼ばれるものです。
実はこのMutationEvents、利用を推奨されていません
可能ならばDOM4のMutationObserverを使って欲しいと、MDNに書かれています。*1
しかしながら、MutationObserverはIEでは実装されていません。
機能的には同じなので今回はMutationEventsを利用しました。

var diff = {
  width:  popup.outerWidth  - doc.documentElement.clientWidth,
  height: popup.outerHeight - doc.documentElement.clientHeight
};

if (!rect) {
  element = doc.createElement(mediaType);
  element.setAttribute("controls", "controls");
  doc.body.appendChild(element);
  rect = element.getBoundingClientRect();
  doc.body.removeChild(element);
}

doc.body.addEventListener("DOMSubtreeModified", function(){ // 要素数が変化したらウィンドウサイズを変える
  var len = this.childNodes.length;
  popup.resizeTo( rect.width + diff.width, rect.height * len + diff.height );
}, false);

この部分が今回のキモです。

一旦要素の大きさを測るために空のメディア要素を作ってすぐ削除しています。
そしてこれを元にして、DOMツリーが変化する度にウィンドウの大きさを変えていくのです。

これを見れば勘のいい方はすぐ気づくかと思いますが、
最初に作成される要素が<audio><video>かで大きさの基準が決まってしまいます。
つまり<audio><video>が混在する場合、ウィンドウの大きさがえらいことになってしまいます。
なのでこれは運用で回避してください。めんどくさいので仕様バグとします。


イベントはDOMSubtreeModifiedで拾っています。
読んで字のごとく、ツリーに変化があったときに発火します。
これ以外にもMutationEventsには、属性に変化があった時を検知するDOMAttrModifiedや、ツリーへの挿入時のみ検知するDOMNodeInsertedなど便利なイベントがたくさんあります。
しかし繰り返しますがMutationEventsは非推奨です

非推奨と言えば、(使うのは)やめろやめろと言われていたにもかかわらずHTML5で正式に採用が決定した<iframe>とか、
標準技術じゃないよーと言われているにもかかわらずかなり普及しているinnerHTMLとかいろいろありますねー。

まぁMutationEventsに関してはあまり普及してない感じしますし、
IE11がMutationObserverをどうするのかは知りませんが、採用するのであれば取って代わられるのかもしれません。
もしかしたらそのまま残るのかもしれません。
別にどっちでもいいです。

*1:https://developer.mozilla.org/en-US/docs/DOM/Mutation_events
URLでは言及されていませんが、非推奨なのはどうやら同期的に動作するから、らしいです。
つまりイベントで発火した処理が全て終わるまで次の処理に移れない、ということみたいですね。