HTMLの<picture>要素をIE11でも使えるように見せかける簡易版JSを作った

IE11も対象の案件。ブラウザの画面幅に応じてページのレイアウトが変わる際、一部の&lt;img>要素の画像については縦横比が異なるものを表示させる必要がありました。そこで、サイズや縦横比が異なる複数の画像をあらかじめ用意しておき、画面幅に応じて自動で切り替わるようにしたい。最近のブラウザならHTMLの&lt;picture>要素をサポートしているので、mediaクエリを付けた&lt;source>要素を並べて書くだけで実現できますが、IE11は&lt;picture>要素が未対応。もちろん、polyfill [picturefill.js](http://scottjehl.github.io/picturefill/)の存在は知っています。が、今回はとりあえずmax-widthだけを基準にして画像を切り替えできれば十分だったので、自分でIE11用に簡易版のコードをJavaScriptで書いてみることにしました[^1]。
[^1]: picturefill.jsは2017年3月リリースのver.3.0.3を最後に更新が止まっているようで、projectのstatusがよくわからなかったのも１つの理由。

![HTML5 picture element for IE11-low.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469745/46f520cb-715d-7af7-571f-ce66da4baa55.gif)
※コンテントの画像 via [Unsplash](https://source.unsplash.com/)
##簡易版の仕様
簡易版jsが扱うことができるシンタックスは以下のとおりです。

- &lt;source>要素には`srcset`属性と`media`属性のみ指定します。
- `media`属性には`max-width`１つのみ、px単位で指定します。
- &lt;source>要素は`max-width`の値が小さい順に並べなければなりません。

```html:example.html
<picture>
  <source srcset="/path/to/img1" media="(max-width: 360px)">
  <source srcset=/path/to/img2" media="(max-width: 420px)">
      .....
  <source srcset="/path/to/imgN" media="(max-width: 768px)">
  <img src="/path/to/fallback/img" alt="">
</picture>
```

<p class="codepen" data-height="509" data-default-tab="html,result" data-slug-hash="yLommxj" data-user="kaz_hashimoto" style="height: 509px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/kaz_hashimoto/pen/yLommxj">
  [JS]  picture element in IE11</a> by Kazuhiro Hashimoto (<a href="https://codepen.io/kaz_hashimoto">@kaz_hashimoto</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

このCodePenはIE11以外でも動作を確認できるように[^2]、HTMLネイティブの機能の代わりに簡易版jsを使って&lt;picture>要素の画像を切り替え表示しています。実際の場面では、IE11以外には不要なコードなので、最初にブラウザのUA文字列によってIE11かどうかを判定し、IE11でなければ即returnさせます。

```javascript:javascript
document.addEventListener('DOMContentLoaded', function() {
  // 実際に使用する時は最初にUAでブラウザ判定を行い、
  // IE以外のブラウザでは以降のコードが実行されないようにする。
  // 例）
  // const parser = new UAParser();
  // const ua = parser.getResult();
  // if (ua.browser.name != 'IE') {
  //   return;
  // }
  .....
```

[^2]: CodePenはIE11をサポートしないため、このPenをIE11で確認したい時はPenのソース一式をExportし、localhost上のコンテンツとしてIE11からアクセスします。

## 実装メモ
処理の本体はIE11でも実行可能なようにコーディングする必要があるため、NodeListの各要素に対してcallback関数を呼ぶ箇所は、一旦Arrayを作ってからforEachを呼ぶ形式で書いてあります。

```javascript:javascript
const targets = document.querySelectorAll('picture');
Array.prototype.forEach.call(targets, function(picture) {
...
```

&lt;source>要素に設定した一連の`media="(max-width:`は、区間が重ならないように
"(min-width: _A_ px) and (max-width: _B_ px)"
に変換したクエリでMediaQueryListオブジェクトを作成し、リスナーを設定します。

クエリと`srcset`の値のペアは、ハンドラ内からも参照するためMapオブジェクトで保持しておきます。ここで、Mapのキーに使うクエリ文字列は、`matchMedia`の戻り値から取り出した方の`mql.media`を使います。IE11の場合、それは`matchMedia(query)`に指定した`query`文字列と必ずしも一致しないためです。

```javascript:javascript
const mql = window.matchMedia(query);
item.map.set(mql.media, source.getAttribute('srcset'));
const func = handler.bind(item)
mql.addListener(func);
func(mql);
```

変数`item`は&lt;picture>要素ごとに設定情報を保持するオブジェクトです。ハンドラー内で`item`を`this`キーワードから参照可能にするため、`bind()`を呼んで生成した関数の方をリスナーに設定します。

```javascript:javascript
function handler(mql) {
  if (!mql.matches) {
    return;
  }
  const item = this;
  const src = item.map.get(mql.media);
  if (!src) {
    return;
  }
  item.img.src = src;
}
```

ハンドラーが呼ばれると、対象の&lt;picture>要素に紐付けた`item`を取り出し、ステータスが変化したmediaクエリをキーに`srcset`の値を取り出し、&lt;img>要素の`src`に画像をセットします。