IM-FormaDesignerのファイルアップロードアイテムで拡張子を制限する

# 対象となる開発形態
- intra-mart Accel Platform
   - IM-FormaDesigner

# やりたいこと
- IM-FormaDesignerのファイルアップロードアイテムで、「ファイルを開く」ダイアログに表示する拡張子を制限する。
- 指定した拡張子以外が選択された場合にエラーを表示し、ファイルアップロードさせない。

# サンプルコード
スクリプトアイテムを画面上に配置するか、アクション設定-初期表示イベントでカスタムスクリプトを追加し、以下のスクリプトを貼ります。

## ファイルアップロードアイテムが1つ
```js
// MutationObserverオブジェクトを生成
let observer = new MutationObserver(function(r, o) {
   // コールバック関数
   // 追加されたノードが#file-regist-formの場合
   if ($('form#file-regist-form').length) {
      // fileにaccept属性を追加
      $('input.imui-fileupload-input[type="file"]').attr('accept', 'text/html');
      // fileのchangeイベントを追加
      $('input.imui-fileupload-input[type="file"]').on('change', function() {
         // ファイルリストを取得
         let files = $('span.upload-file-name');
         for (i = 0; i < files.length; i++) {
            let splitName = $(files[i]).data('file_name')
                                       .split('.');
            // 拡張子判定
            if (splitName[splitName.length - 1].toLowerCase() !== 'html'
             && splitName[splitName.length - 1].toLowerCase() !== 'htm') {
               imuiAlert('HTML文書以外は添付できません。', '拡張子エラー', true);
               // エラーの場合、直近のtrタグごと消去
               $(files[i]).closest('tr.template-upload')
                          .remove();
            }
         }
      });
   }
});
// 監視を開始
observer.observe($('body')[0], {childList: true});
```

## 解説

### MutationObserver
ファイルアップロードダイアログは`imuiPageDialog`で作られており、「＋」ボタンが押されるまでDOM要素が構成されません。
そのため、`MutationObserver`で`<form id="file-regist-form">`が生成されるのを監視しています。
ダイアログは`body`要素直下に生成されるため、`MutationObserver`は`childList`指定で`body`要素のノード変更を全て監視することになります。
このため、コールバック関数内でノード変更内容をチェックして、生成されたノードが`<form id="file-regist-form">`かどうかを判断する必要があります。

### \<input type="file"\>のaccept属性
ノード変更時に`form#file-regist-form`が存在したら、子要素の`<input type="file>`要素に対して`accept`属性を付与します。
これにより、「ファイルを開く」ダイアログを表示した時に表示するファイルタイプを指定しています。
ただし、これだけではファイルタイプを変更された場合に指定拡張子以外でも選択できてしまうため、ファイルが追加された時に別途拡張子をチェックする処理が必要となります。

### 追加ファイル拡張子チェック
`<input type="file">`は、ファイルが追加/削除された時に`change`イベントが発生するため、これをトリガーとして追加されたファイルの拡張子チェックを実行します。
通常`<input type="file">`のファイルリストは`$('input.imui-fileupload-input[type="file"]')[0].files`で取得できますが、ファイルアップロードアイテムでは独自のファイルリストを自動的に作成するだけで`files`属性には追加がされない模様(ファイルを追加しても`length`が0になっている)。
独自ファイルリストは`<span class="upload-file-name">`のdata属性`data-file_name`に格納されており、実際のファイル追加時もこちらを判断しているようです。
そのため、ファイルリストとしてこちらを取得します。
後は、ファイル名から拡張子を取得・判定し、エラーとする場合はアラートを表示した後で直近の`<tr class="template-upload">`ごとremoveしてやればOK。

## ファイルアップロードアイテムが2つ以上

```js
// ファイルアップロードアイテムの制限内容
let checkFiles = [];
checkFiles = [{
   uploadItemId: 'item1',
   fileExt: ['html', 'htm'],
   mimeType: 'text/html',
   fileType: 'HTML文書'
},
{
   uploadItemId: 'item2',
   fileExt: ['css'],
   mimeType: 'text/css',
   fileType: 'CSSファイル'
}]
// ファイルアップロードフォーム
let checkForms = [];

// MutationObserverオブジェクトを生成
let observer = new MutationObserver(function(r, o) {
   // コールバック関数
   for (let i = 0; i < checkFiles.length; i++) {
      // uploadItemIdが一致するform要素
      checkForms[i] = $('form#file-regist-form').has('input#upload-item-id[value="' + checkFiles[i].uploadItemId + '"');
      // 追加されたノードが#file-regist-formの場合
      if (checkForms[i].length) {
         // fileにaccept属性を追加
         checkForms[i].find('input.imui-fileupload-input[type="file"]')
                      .attr('accept', checkFiles[i].mimeType);
         // for文にループインデックスiを引き渡す
         (function(i) {
            // fileのchangeイベントを追加
            checkForms[i].find('input.imui-fileupload-input[type="file"]')
                         .on('change', function() {
               // ファイルリストを取得
               let files = checkForms[i].find('span.upload-file-name');
               for (let j = 0; j < files.length; j++) {
                  let splitName = $(files[j]).data('file_name')
                                             .split('.');
                  // 拡張子判定
                  if ($.inArray(splitName[splitName.length - 1].toLowerCase(), checkFiles[i].fileExt) === -1) {
                     imuiAlert(checkFiles[i].fileType + '以外は添付できません。', '拡張子エラー', true);
                     // エラーの場合、直近のtrタグごと消去
                     $(files[j]).closest('tr.template-upload')
                                .remove();
                  }
               }
            });
         })(i);
      }
   }
});
// 監視を開始
observer.observe($('body')[0], {childList: true});
```

## 解説
基本的には1つの時とやることは変わらず、どうやって複数のファイルアップロードアイテムの処理を回すかだけです。
今回は、先にファイルアップロードアイテム毎のチェック内容をobjectに設定し、for文で設定数分ループを回す方式を採りました。

ポイントは、予め`upload-item-id`が一致するform要素を発見し、配列`checkForms`に格納している点。
以降、`<input type="file">`や`<span class="upload-file-name">`を取得する場合は、`checkForms[i]`を基準に`find()`で子ノードを取得することで、`upload-item-id`単位の処理を実現しています(実際にはファイルアップロードダイアログ自体が同時に複数存在することがないため、不要かも)。

また、複数の拡張子判定(`jpg/jpeg`など)に対応するため、拡張子指定を配列とし、チェック時にはjQuery関数`$.inArray()`で判定しています。
ファイルアップロードアイテムが1つの場合はAND条件で直接指定してやればいいので、この対応は不要です。

最後に注意点として、forループ内で`change`イベントのコールバック関数を定義しているため、`(function(i) {})(i);`を用いてcallback関数にループインデックス`i`を引き継いでいます。
これをやらないと、コールバック関数内では`i`がスコープ外になってしまうため参照できません。