iOS、Safariの<input type="file" multiple>で発生するエラーに対応した際の備忘録

# はじめに
とあるサイトを制作していた時、iOS、Safariの`<input type="file" multiple>`でファイルを選択した場合にエラーが発生する事象に遭遇しました。
ネット上で有益な情報を見つけられなかったので、備忘録として残します。

# 事象
下記のような、選択したファイル名をリスト表示させるものを作る際に発生しました。

## ソース

```html:HTML
<form action="" method="get">
<div>
<ul>
</ul>
<label>
<input type="file" multiple="multiple">
</label>
</div>
</form>
```

```javascript:JavaScript
$(function () {
  /*------------------------------------------
    ファイルアップロード
  --------------------------------------------*/ 
  $('body').on('change', 'input[type="file"]', function() {
    var $this = $(this)
    var $ul = $this.parent('label').siblings('ul')
    var listHtml = ''
    
    if($ul.length > 0) {
      var files = $this[0].files
      var filesLength = files.length
      
      // ファイルを1件以上選択している場合
      if(filesLength > 0) {
        // HTMLを生成
        for(var i = 0; i < filesLength; i++) {
          listHtml += '<li>' + files[i].name + '</li>'
        }
        
        // HTMLに反映
        $ul.html(listHtml)
      } else {
        // ファイルの選択をキャンセルした場合
        // リストを削除する
        $ul.html('')
      }
    }
  })
})
```

<p class="codepen" data-height="500" data-theme-id="dark" data-default-tab="html,result" data-user="inumberx" data-slug-hash="aMYVPo" data-preview="true" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid black; margin: 1em 0; padding: 1em;" data-pen-title="MultipleFileSample01">
  <span>See the Pen <a href="https://codepen.io/inumberx/pen/aMYVPo/">
  MultipleFileSample01</a> by N/NE (<a href="https://codepen.io/inumberx">@inumberx</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

## 発生端末

| OS | iOS 12.1.4 |
|:-----------------|------------------:|
| **端末** | **iPhoneX** |
| **ブラウザ** | **Safari、Google Chrome** |

## 現象

`<input type="file" multiple>`でファイルを複数選択した後に、再度ファイルを**再選択・キャンセル**すると下記画像のようなエラーが発生するというものです。

<img width="375" alt="img_001.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/191403/bcd72559-9ed9-f6b0-e6b7-cfb900408210.jpeg">

- `問題が発生したため、このWebページが再読み込みされました。`というメッセージが表示される

<img width="375" alt="img_002.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/191403/bb55b9fd-c5b2-1012-4a9a-76e39821267b.jpeg">

- `"https://XXX/YYY/ZZZ"で問題が繰り返し起きました。`というメッセージが表示される

## 発生タイミング

色々と検証した結果、エラーは下記のようなタイミングで発生しているようでした。

- ファイル選択済の状態でファイルを再選択する
- ファイル選択済の状態でキャンセルする
- ファイルを選択した時に、既にファイル選択済の他の`<input type="file" multiple>`が表示状態（display:none;以外）で存在する

# 対応方法

エラーを発生させないためには、ファイルを選択した時に既にファイル選択済の`<input type="file" multiple>`がdisplay:none;以外で存在していてはいけません。
そこで、ファイルを選択した時にJS（jQuery）でその要素を非表示にし、新しく空の`<input type="file" multiple>`を作ることによってエラーが発生しないようにしました。

## ソース

```html:HTML
<form action="" method="get">
<div>
<ul>
</ul>
<label>
<input type="file" multiple="multiple">
</label>
</div>
</form>
```

```css:CSS
input[type="file"][multiple="multiple"].ios {
position: absolute;
opacity: 1;
z-index: 1;
}
input[type="file"][multiple="multiple"].ios + input[type="file"][multiple="multiple"] {
display: none;
}
```

```javascript:JavaScript
$(function () {
  /*------------------------------------------
    ファイルアップロード
  --------------------------------------------*/
  // iOSの場合
  // すでにファイルを選択済みの<input type="file" multiple>の場合、ファイルを再選択・キャンセルした時にブラウザのエラーが発生するので個別対応を行う
  if(isIOs && $('input[type="file"][multiple="multiple"]').length > 0) {
    var $file = $('input[type="file"][multiple="multiple"]')
    var fileLength = $file.length
    
    for(var i = 0; i < fileLength; i++) {
      var $thisFile = $($file[i])
      // inputを複製
      var $cloneFile = $thisFile.clone(true)
      // 複製した要素にクラスを付与
      $cloneFile.val('').addClass('ios')
      // 複製した要素はabsoluteで複製元の要素に被せるため、親要素をrelativeにする
      $thisFile.parent().css(
        {
          'position': 'relative',
          'min-width': $thisFile.outerWidth(true) + 'px',
          'min-height': $thisFile.outerHeight(true) + 'px'
        }
      );
      // 複製した要素をDOMに追加
      $thisFile.before($cloneFile)
    }
    
    // 複製した要素に隣り合う<input type="file" multiple>がタッチされた時の処理
    $('body').on('touchstart', 'input[type="file"][multiple="multiple"].ios + input[type="file"][multiple="multiple"]', function() {
      // 何もしない
      return false
    })
    
    // サブミット時の処理
    $('body').on('submit', function() {
      // 複製した要素を削除する
      $('input[type="file"][multiple="multiple"].ios').remove()
    })
  }
  
  $('body').on('change', 'input[type="file"]', function() {
    var $this = $(this)
    var $ul = $this.parent('label').siblings('ul')
    var listHtml = ''
    
    if($ul.length > 0) {
      var files = $this[0].files
      var filesLength = files.length
      
      // ファイルを1件以上選択している場合
      if(filesLength > 0) {
        // HTMLを生成
        for(var i = 0; i < filesLength; i++) {
          listHtml += '<li>' + files[i].name + '</li>';
        }
        
        // HTMLに反映
        $ul.html(listHtml)
      } else {
        // ファイルの選択をキャンセルした場合
        // リストを削除する
        $ul.html('')
      }
    }
    
    // iOSの場合
    if($this.hasClass('ios')) {
      // inputを複製
      var $cloneFile = $this.clone(true)
      // 複製した要素のvalueを空にする
      $cloneFile.val('')
      // すでに存在するinputを削除する
      $this.siblings('input[type="file"][multiple="multiple"]').remove()
      // 複製した要素をDOMに追加
      $this.removeClass('ios').before($cloneFile)
    }
  })
})

/*------------------------------------------
　iOSチェック
--------------------------------------------*/
function isIOs() {
  var ua = navigator.userAgent
  var bw = window.navigator.userAgent.toLowerCase()
  
  // iOSの場合
  if(ua.indexOf('iPhone') > 0 || ua.indexOf('iPod') > 0 ) {
    return true
  }
  
  return false
}
```

<p class="codepen" data-height="265" data-theme-id="dark" data-default-tab="js,result" data-user="inumberx" data-slug-hash="MxVOLL" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid black; margin: 1em 0; padding: 1em;" data-pen-title="MultipleFileSample02">
  <span>See the Pen <a href="https://codepen.io/inumberx/pen/MxVOLL/">
  MultipleFileSample02</a> by N/NE (<a href="https://codepen.io/inumberx">@inumberx</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

## 問題点
- 常に「ファイル未選択」と表示される → 選択したファイル数を「ファイル未選択」の文言の上に無理やり被せる？
- 根本的な解決方法ではない気がする → もっと良い解決方法をご存知の方がいらっしゃったらご教示ください