Mobile Safariでの100vhが気に入らないからPostCSSで解決した

CSSの`vh`とかの`viewport units`って画面ぴったりな要素や画面サイズに合わせた要素が簡単に作れて便利ですよね。

しかし残念ながら`vh`はMobile Safariだとぴったりいかない場合があります。
これはアドレスバーの高さを計算に入れる入れないの問題に起因しているようです。

# 色々な解決策

Mobile Safariで`100vh`を画面ぴったりにしたい場合、Stack Overflowには色々な解決策があります。

[Stack Overflow - How to fix vh(viewport unit) css in mobile Safari?](https://stackoverflow.com/questions/23223157/how-to-fix-vhviewport-unit-css-in-mobile-safari)

1. [Viewport Units Buggyfill](https://github.com/rodneyrehm/viewport-units-buggyfill)使う
  ちゃんと試せてないけど、実行時にvh単位の値を上書きするCSSを付与する様子。

2. jQueryで対応する  

    ```js
    $('.my-element').height(window.innerHeight + 'px');
    ```

3. JavaScriptで対応する  

    ```js
    document.getElementById("my-element").style.height = window.innerHeight + 'px';
    ```
4. `height: 100%`引き回す
    古き良き方法。
5. CSS カスタムプロパティで対応する  

    ```css
    .my-element {
      height: 100vh; /* Fallback for browsers that do not support Custom Properties */
      height: calc(var(--vh, 1vh) * 100);
    }
    ```

    ```js
    // First we get the viewport height and we multiple it by 1% to get a value for a vh unit
    let vh = window.innerHeight * 0.01;
    // Then we set the value in the --vh custom property to the root of the document
    document.documentElement.style.setProperty('--vh', `${vh}px`);
    ```
    [CSS-Tricks - The trick to viewport units on mobile](https://css-tricks.com/the-trick-to-viewport-units-on-mobile/)

で、最後のCSS-Tricksの解決策が個人的にいいなーと思ったのですが、`100vh`書いたところ全部に`calc(var(--vh, 1vh) * 100);`って書いていくのめんどくさいなー。PostCSSでなんとかなるんじゃないかなー。というのがこの先の解決策です。

# [CSS-Tricks - The trick to viewport units on mobile](https://css-tricks.com/the-trick-to-viewport-units-on-mobile/)をPostCSSで実施する

## PostCSSプラグインを作る

ローカルに自作のPostCSSプラグインを作ります。 

```js:postcss-viewport-units-on-mobile.js
const postcss = require('postcss')

// プラグイン定義
module.exports = postcss.plugin('postcss-viewport-units-on-mobile', () =>
  createPlugin()
)

function createPlugin() {
  return root => {
    const replaces = []
    // CSSの宣言（declaration）を走査
    root.walkDecls(decl => {
      // vh,vmax,vminの単位で宣言した値を書き換えたCSS値を作る
      const newValue = decl.value.replace(
        /\b([-+]?[\d.]+)(vh|vmax|vmin)\b/g,
        replacer
      )
      if (decl.value !== newValue) {
        // vh,vmax,vminが書き換えられていたら、そのCSSを保持しておく
        replaces.push({ decl, newValue })
      }
    })
    // vh,vmax,vminが書き換えられたCSSの宣言を元のCSSの宣言の後ろに挿入していく
    for (const { decl, newValue } of replaces) {
      decl.parent.insertAfter(decl, decl.clone({ value: newValue }))
    }
  }
}

// vh,vmax,vminの書き換え
function replacer(original, snum, unit) {
  if (isNaN(snum)) {
    return original
  }
  const num = snum - 0
  return `calc(${snum} * var(--${unit}, 1${unit}))`
}

```

このPostCSSプラグインは`vh`を使った単位を見つけたら、その下にCSS カスタムプロパティで書き換えたCSSを付与します。

つまり、このCSSは

```css:before.css
.a {
  height: 100vh;
}
.b {
  height: calc(100vh - 10px);
}
```

こうなります。

```css:after.css
.a {
  height: 100vh;
  height: calc(100 * var(--vh, 1vh));
}
.b {
  height: calc(100vh - 10px);
  height: calc(calc(100 * var(--vh, 1vh)) - 10px);
}
```

## PostCSSの処理に入れる

PostCSSの使い方次第なので、これだ！っていうのを1つ書くのはできませんが、
`postcss.config.js`に書く形だとこうですかね。

```js:postcss.config.js 
module.exports = {
  plugins: [
    // ... 
    // require('autoprefixer'),
    // ... 
    require('./path/to/postcss-viewport-units-on-mobile.js')(), // 作ったプラグイン
    // ... 
  ]
}
```

## アプリケーションにJavaScriptを埋め込む

次のJavaScriptをアプリケーションに埋め込みます。

```js
updateViewport()

window.addEventListener('resize', updateViewport)

function updateViewport() {
  const vh = window.innerHeight / 100
  const vw = window.innerWidth / 100

  const root = document.documentElement

  // 各カスタムプロパティに`window.innerHeight / 100`,`window.innerWidth / 100`の値をセット
  root.style.setProperty('--vh', `${vh}px`)
  if (vh > vw) {
    root.style.setProperty('--vmax', `${vh}px`)
    root.style.setProperty('--vmin', `${vw}px`)
  } else {
    root.style.setProperty('--vmax', `${vw}px`)
    root.style.setProperty('--vmin', `${vh}px`)
  }
}
```

# 実行結果

記述したCSS

```css
.my-element {
  height: 100vh;
}
```

は、次のように変換され、

```css
.my-element {
  height: 100vh;
  height: calc(100 * var(--vh, 1vh));
}
```

JavaScriptで、CSSカスタムプロパティ`--vh`に`window.innerHeight / 100`をセットすると、
`.my-element`の高さが`window.innerHeight`と同じ高さになり、Mobile Safariでもいい感じに画面に収まります！

# まとめ

CSSを自分で書き換えなくても、[CSS-Tricks - The trick to viewport units on mobile](https://css-tricks.com/the-trick-to-viewport-units-on-mobile/)のトリックが使えます。そうPostCSSならね。