intra-martのスクリプト開発でimuiPageDialogのbeforeclose時に確認ダイアログを表示する

# 対象となる開発形態
- intra-mart AccelPlatform
   - スクリプト開発

# やりたいこと
`imuiPageDialog`で内容が更新されているのにページダイアログを閉じようとした時、`beforeclose`イベントで確認ダイアログを表示し、OKなら閉じる/キャンセルなら閉じないようにする。

# 問題点
1. `beforeclose`イベントでページダイアログ内の入力内容変更チェックを行いたいため、呼び出し元ページでイベント設定ができない。
2. 確認ダイアログは`imuiMessageDialog`を利用するが、非同期で動作する(確認ダイアログが表示されても、ボタン押下を待たずに次の処理に進んでfunctionが終了してしまう)ため同期的に動かす仕組みが必要になる。
3. 同期的に動かした場合、ボタン押下で再度ページダイアログの`beforeclose`イベントが発動するため、処理がループしてしまう。

# 解決策
## beforecloseイベントをページダイアログ側でバインドする
ページダイアログ側で`beforeclose`イベントをバインドするには、対象の`imuiPageDialog`に対して`on('imuipagedialogbeforeclose', function(event, ui) {})`を実行します。
ただし、ページダイアログ側では呼び出し元で何というIDを指定したか分からないため、呼び出し元からリクエストパラメータとして受け取って指定します。

```js:sample.html
<imart type="imuiButton" id="dialogButton" value="ボタン" class="imui-medium-button" />

<script type="text/javascript">
   $(function() {
      // ページダイアログID
      var pageDialogId = 'samplePageDialog';

      // ページダイアログ呼び出し
      $('<div></div>').attr('id', pageDialogId)
                      .appendTo(document.body)
                      .imuiPageDialog({
            title: 'ページダイアログ',
            url: 'pageDialog',
            parameter: {
               'imui-theme-builder-module': 'notheme',
               pageDialogId: pageDialogId
            },
            position: [650, 300],
            modal: true,
            height: 750,
            width: 465,
            close: function(event, ui) {
               // imuiPageDialogIdを作成したdivを削除
               $('#' + pageDialogId).remove();
            }
         });
      });
   });
</script>
```
```html:pageDialog.html
<imart type="hidden"
   pageDialogId=$pageDialogId
/>

<imart type="imuiTextbox" id=textbox1" value="text1" />

<script type="text/javascript">
   $(function() {
      // 引数で渡されたページダイアログID
      var pageDialogId = $('input[name="pageDialogId"]').val();

      // ページダイアログのbeforeCloseイベントをバインド
      $('#' + pageDialogId).on('imuipagedialogbeforeclose', function(event, ui) {
         // imuiMessageDialog表示
         var dialogId = 'dialog_saveConfirmDialog';
         $('<div></div>').text('入力内容を保存しますか？')
                         .attr('id', dialogId)
                         .imuiMessageDialog({
            iconType: 'info',
            title: '確認',
            verticalAlign: 'top',
            modal: true,
            buttons: [
               {
                  'id': 'save',
                  'text': '保存',
                  'click': function() {
                     save();
                     $('#' + dialogId).imuiMessageDialog('close');
                     return true;
                  }
               },
               {
                  'id': 'unsave',
                  'text': '破棄',
                  'click': function() {
                     $('#' + dialogId).imuiMessageDialog('close');
                     return true;
                  }
               },
               {
                  'id': 'cancel',
                  'text': 'キャンセル',
                  'click': function() {
                     $('#' + dialogId).imuiMessageDialog('close');
                     return false;
                  }
               }
            ],
            close: function(event, ui) {
               $('#' + dialogId).remove();
            }
         });
      });
   });
</script>
```
```js:pageDialog.js
// リクエストパラメータのページダイアログID
var $pageDialogId;

function init(request) {
   $pageDialogId = request.pageDialogId;
}
```
なお、上記のソースでは`imuiMessageDialog`の同期処理対応ができていないため、実行すると確認ダイアログが表示された瞬間にページダイアログが閉じてしまいます。

## 確認ダイアログを同期的に動作させる
`imuiMessageDialog`を同期的に動作させるため、処理を`deferred`を使って書き換えます。
ついでに、保存処理についても`deferred`を使用するようにしています。

```html:pageDialog.html
<imart type="hidden"
   pageDialogId=$pageDialogId
/>

<imart type="imuiTextbox" id=textbox1" value="text1" />

<script type="text/javascript">
   // 名前空間
   if (typeof pageDialog === 'undefined') {
      var pageDialog = {}; 
   }

   // 引数で渡されたページダイアログID
   pageDialog.pageDialogId = $('input[name="pageDialogId"]').val();

   $(function() {
      // ページダイアログのbeforeCloseイベントをバインド
      $('#' + pageDialog.pageDialogId).on('imuipagedialogbeforeclose', function(event, ui) {
         var confirmPromise = changeConfirm();
         confirmPromise.done(function() {
            return true;
         });
         confirmPromise.fail(function() {
            return false;
         });
      });
   });

   function changeConfirm() {
      // deferredオブジェクト
      var confirmDeferred = new $.Deferred();

      // imuiMessageDialog表示
      var dialogId = 'dialog_saveConfirmDialog';
      $('<div></div>').text('入力内容を保存しますか？')
                      .attr('id', dialogId)
                      .imuiMessageDialog({
         iconType: 'info',
         title: '確認',
         verticalAlign: 'top',
         modal: true,
         buttons: [
            {
               'id': 'save',
               'text': '保存',
               'click': function() {
                  var savePromise = save();
                  savePromise.done(function() {
                     imuiShowSuccessMessage('保存しました。');
                     $('#' + dialogId).imuiMessageDialog('close');
                     // 保存成功時はページダイアログを閉じる
                     confirmDeferred.resolve();
                  });
                  savePromise.fail(function() {
                     imuiShowErrorMessage('保存に失敗しました。');
                     $('#' + dialogId).imuiMessageDialog('close');
                     // 保存失敗時はページダイアログを閉じない
                     confirmDeferred.reject();
                  });
               }
            },
            {
               'id': 'unsave',
               'text': '破棄',
               'click': function() {
                  imuiShowSuccessMessage('変更を破棄しました。');
                  $('#' + dialogId).imuiMessageDialog('close');
                  // 変更破棄時はページダイアログを閉じる
                  confirmDeferred.resolve();
               }
            },
            {
               'id': 'cancel',
               'text': 'キャンセル',
               'click': function() {
                  $('#' + dialogId).imuiMessageDialog('close');
                  // キャンセル時はページダイアログを閉じない
                  confirmDeferred.reject();
               }
            }
         ],
         close: function(event, ui) {
            $('#' + dialogId).remove();
         }
      });
      return confirmDeferred.promise();
   }

   function save() {
      // deferredオブジェクト
      var saveDeferred = new $.Deferred();

      // 保存処理
      $.ajax({
         type: 'POST',
         url: 'save',
         async: false,
         data: {data: $('#textbox1').val()},
         success: function(res, dataType) {
            imuiShowSuccessMessage('保存しました。');
            saveDeferred.resolve();
         },
         error: function(res, textStatus, xhr) {
            imuiShowErrorMessage(res.responseText);
            saveDeferred.reject();
         }
      });
      return saveDeferred.promise();
   }
</script>
```
```js:pageDialog.js
// リクエストパラメータのページダイアログID
var $pageDialogId;

function init(request) {
   $pageDialogId = request.pageDialogId;
}
```

書き換えてもやはり確認ダイアログ表示と同時にページダイアログは閉じてしまいます。
処理は待機してボタン押下と同時に`promise`オブジェクトのfunctionは実行されるのですが、`beforeclose`イベント自体はそのまま終了してしまうからです。
同期処理にした意味がない……。

## 変更状態を監視して同期処理の結果から処理を実行させる
`deferred`で同期的に処理が実行できる所までは解決できました。
ここからは多少力技ですが、`mutationObserver`を利用して確認ダイアログ押下状態を監視、deferredの同期処理で押下状態を変更することで`MutationObserver`に検知させることにしました。
先頭の`<imart type="hidden">`タグに、確認ダイアログ押下状態を格納する`confirmStatus`を追加し、`mutationObserver`の監視対象に指定しています。
属性監視なので監視タイプは`attributes`、フィルタで`value`属性のみ監視するよう設定しました。

```html:pageDialog.html
<imart type="hidden"
   pageDialogId=$pageDialogId
   confirmStatus=""
/>

<imart type="imuiTextbox" id=textbox1" value="text1" />

<script type="text/javascript">
   // 名前空間
   if (typeof pageDialog === 'undefined') {
      var pageDialog = {}; 
   }

   // 引数で渡されたページダイアログID
   pageDialog.pageDialogId = $('input[name="pageDialogId"]').val();

   // 確認ダイアログのボタン押下状態監視
   pageDialog.observer = new MutationObserver(checkConfirmStatus);

   $(function() {
      // ページダイアログのbeforeCloseイベントをバインド
      $('#' + pageDialog.pageDialogId).on('imuipagedialogbeforeclose', function(event, ui) {
         if ($('input[name="confirmStatus"]').val() === '') {
            // 確認ダイアログのボタンが押下されていない場合、状態をwaitにして確認ダイアログを表示する
            $('input[name="confirmStatus"]').val('wait');
            var confirmPromise = changeConfirm();
            confirmPromise.done(function() {
               // promiseがresolveになった場合(更新成功/破棄時)、状態をcloseにする
               $('input[name="confirmStatus"]').val('close');
            });
            confirmPromise.fail(function() {
               // promiseがrejectになった場合(更新失敗/キャンセル時)、状態をcancelにする
               $('input[name="confirmStatus"]').val('cancel');
            });
         }

         switch ($('input[name="confirmStatus"]').val()) {
         case 'wait':
            // 状態がwaitの場合、MutationObserverの監視を開始してfalseを返却(ページダイアログを閉じない)
            pageDialog.observer.observe($('input[name="confirmStatus"]')[0], {attributes: true, attributeFilter: ['value']});
            return false;
         case 'close':
            // 状態がcloseの場合、trueを返却(ページダイアログを閉じる)
            return true;
         case 'cancel':
            // 状態がcancelの場合、falseを返却(ページダイアログを閉じない)
            return false;
         }
      });
   });

   function checkConfirmStatus() {
      switch ($('input[name="confirmStatus"]').val()) {
      case 'wait':
         // 状態がwaitの場合、何もしない
         break;
      case 'close':
         // 状態がcloseに変化した場合、監視を停止して再度ページダイアログをcloseする
         pageDialog.observer.disconnect();
         $('#' + pageDialog.pageDialogId).imuiPageDialog('close');
         break;
      case 'cancel':
         // 状態がcancelに変化した場合、監視を停止して状態を初期状態に戻す
         pageDialog.observer.disconnect();
         $('input[name="confirmStatus"]').val('');
         break;
      }
   }

   function changeConfirm() {
      // deferredオブジェクト
      var confirmDeferred = new $.Deferred();

      // imuiMessageDialog表示
      var dialogId = 'dialog_saveConfirmDialog';
      $('<div></div>').text('入力内容を保存しますか？')
                      .attr('id', dialogId)
                      .imuiMessageDialog({
         iconType: 'info',
         title: '確認',
         verticalAlign: 'top',
         modal: true,
         buttons: [
            {
               'id': 'save',
               'text': '保存',
               'click': function() {
                  var savePromise = save();
                  savePromise.done(function() {
                     imuiShowSuccessMessage('保存しました。');
                     $('#' + dialogId).imuiMessageDialog('close');
                     // 保存成功時はページダイアログを閉じる
                     confirmDeferred.resolve();
                  });
                  savePromise.fail(function() {
                     imuiShowErrorMessage('保存に失敗しました。');
                     $('#' + dialogId).imuiMessageDialog('close');
                     // 保存失敗時はページダイアログを閉じない
                     confirmDeferred.reject();
                  });
               }
            },
            {
               'id': 'unsave',
               'text': '破棄',
               'click': function() {
                  imuiShowSuccessMessage('変更を破棄しました。');
                  $('#' + dialogId).imuiMessageDialog('close');
                  // 変更破棄時はページダイアログを閉じる
                  confirmDeferred.resolve();
               }
            },
            {
               'id': 'cancel',
               'text': 'キャンセル',
               'click': function() {
                  $('#' + dialogId).imuiMessageDialog('close');
                  // キャンセル時はページダイアログを閉じない
                  confirmDeferred.reject();
               }
            }
         ],
         close: function(event, ui) {
            $('#' + dialogId).remove();
         }
      });
      return confirmDeferred.promise();
   }

   function save() {
      // deferredオブジェクト
      var saveDeferred = new $.Deferred();

      // 保存処理
      $.ajax({
         type: 'POST',
         url: 'save',
         async: false,
         data: {data: $('#textbox1').val()},
         success: function(res, dataType) {
            imuiShowSuccessMessage('保存しました。');
            saveDeferred.resolve();
         },
         error: function(res, textStatus, xhr) {
            imuiShowErrorMessage(res.responseText);
            saveDeferred.reject();
         }
      });
      return saveDeferred.promise();
   }
</script>
```
```js:pageDialog.js
// リクエストパラメータのページダイアログID
var $pageDialogId;

function init(request) {
   $pageDialogId = request.pageDialogId;
}
```
1回目の`beforeclose`イベントでは確認ダイアログ押下状態は初期値です。ここから状態は`wait`に変化し、`mutationObserver`が監視を開始します。
1回目の`beforeclose`イベントは、`false`を返却してこれで終了。
状態を変化させるトリガーとなるのは、確認ダイアログのボタンを押下です。押下したボタンに応じて、`beforeclose`イベント内にある`confirmPromise`の`promise.done()`/`promise.fail()`いずれかが発動して、状態が`close`/`cancel`に変化します。
この変更を`mutationObserver`が捕捉するとコールバック関数`checkConfirmStatus()`が動作し、`close`に変化していたら再度ページダイアログの`close`イベントを発動します(1回目の`close`イベントは`beforeclose`イベントで`false`を返しているので動作していない)。
ここで2回目の`beforeclose`イベントが動作するのですが、この時状態は`close`となっているので、他の処理は一切行わず`true`を返却するだけです。これにより、2回`close`イベントが動いても正しくクローズできるという仕組みです。
`cancel`に変化していたら確認ダイアログを閉じて状態を初期状態に戻してやれば、再び`close`イベントの発動待ち状態になります。

# サンプルコード
という訳で、できあがったコードは以下となりました。
`input`の変更をチェックして、変更されている場合のみ確認ダイアログを出すようにしてあります。

```html:sample.html
<imart type="imuiButton" id="dialogButton" value="ボタン" class="imui-medium-button" />

<script type="text/javascript">
   $(function() {
      $('#dialogButton').on('click', function() {
         // ページダイアログID
         var pageDialogId = 'samplePageDialog';

         // ページダイアログ呼び出し
         $('<div></div>').attr('id', pageDialogId)
                         .appendTo(document.body)
                         .imuiPageDialog({
            title: 'ページダイアログ',
            url: 'pageDialog',
            parameter: {
               'imui-theme-builder-module': 'notheme',
               pageDialogId: pageDialogId
            },
            position: [650, 300],
            modal: true,
            height: 750,
            width: 465,
            close: function(event, ui) {
               // imuiPageDialogIdを作成したdivを削除
               $('#' + pageDialogId).remove();
            }
         });
      });
   });
</script>
```
```html:pageDialog.html
<imart type="hidden"
   pageDialogId=$pageDialogId
   confirmStatus=""
/>

<imart type="imuiTextbox" id=textbox1" value="text1" />

<script type="text/javascript">
   // 名前空間
   if (typeof pageDialog === 'undefined') {
      var pageDialog = {}; 
   }

   // 引数で渡されたページダイアログID
   pageDialog.pageDialogId = $('input[name="pageDialogId"]').val();

   // 確認ダイアログのボタン押下状態監視
   pageDialog.observer = new MutationObserver(checkConfirmStatus);

   // input要素の変更状態
   pageDialog.isChanged = false;

   $(function() {
      $('input:not([type="hidden"],[type="submit"],[type="button"],[type="reset"],[type="image"])').on('change', function() {
         // 入力可能なinput要素の内容が変更されたら変更状態をtrueにする
         pageDialog.isChanged = true;
      });

      // ページダイアログのbeforeCloseイベントをバインド
      $('#' + pageDialog.pageDialogId).on('imuipagedialogbeforeclose', function(event, ui) {
         if ($('input[name="confirmStatus"]').val() === '') {
            // 確認ダイアログのボタンが押下されていない場合、状態をwaitにして確認ダイアログを表示する
            $('input[name="confirmStatus"]').val('wait');
            var confirmPromise = changeConfirm();
            confirmPromise.done(function() {
               // promiseがresolveになった場合(更新成功/破棄時)、状態をcloseにする
               $('input[name="confirmStatus"]').val('close');
            });
            confirmPromise.fail(function() {
               // promiseがrejectになった場合(更新失敗/キャンセル時)、状態をcancelにする
               $('input[name="confirmStatus"]').val('cancel');
            });
         }

         switch ($('input[name="confirmStatus"]').val()) {
         case 'wait':
            // 状態がwaitの場合、MutationObserverの監視を開始してfalseを返却(ページダイアログを閉じない)
            pageDialog.observer.observe($('input[name="confirmStatus"]')[0], {attributes: true, attributeFilter: ['value']});
            return false;
         case 'close':
            // 状態がcloseの場合、trueを返却(ページダイアログを閉じる)
            return true;
         case 'cancel':
            // 状態がcancelの場合、falseを返却(ページダイアログを閉じない)
            return false;
         }
      });
   });

   function checkConfirmStatus() {
      switch ($('input[name="confirmStatus"]').val()) {
      case 'wait':
         // 状態がwaitの場合、何もしない
         break;
      case 'close':
         // 状態がcloseに変化した場合、監視を停止して再度ページダイアログをcloseする
         pageDialog.observer.disconnect();
         $('#' + pageDialog.pageDialogId).imuiPageDialog('close');
         break;
      case 'cancel':
         // 状態がcancelに変化した場合、監視を停止して状態を初期状態に戻す
         pageDialog.observer.disconnect();
         $('input[name="confirmStatus"]').val('');
         break;
      }
   }

   function changeConfirm() {
      // deferredオブジェクト
      var confirmDeferred = new $.Deferred();

      // inputに変更があった場合のみダイアログを表示
      if (pageDialog.isChanged) {
         // imuiMessageDialog表示
         var dialogId = 'dialog_saveConfirmDialog';
         $('<div></div>').text('入力内容を保存しますか？')
                         .attr('id', dialogId)
                         .imuiMessageDialog({
            iconType: 'info',
            title: '確認',
            verticalAlign: 'top',
            modal: true,
            buttons: [
               {
                  'id': 'save',
                  'text': '保存',
                  'click': function() {
                     var savePromise = save();
                     savePromise.done(function() {
                        imuiShowSuccessMessage('保存しました。');
                        $('#' + dialogId).imuiMessageDialog('close');
                        // 保存成功時はページダイアログを閉じる
                        confirmDeferred.resolve();
                     });
                     savePromise.fail(function() {
                        imuiShowErrorMessage('保存に失敗しました。');
                        $('#' + dialogId).imuiMessageDialog('close');
                        // 保存失敗時はページダイアログを閉じない
                        confirmDeferred.reject();
                     });
                  }
               },
               {
                  'id': 'unsave',
                  'text': '破棄',
                  'click': function() {
                     imuiShowSuccessMessage('変更を破棄しました。');
                     $('#' + dialogId).imuiMessageDialog('close');
                     // 変更破棄時はページダイアログを閉じる
                     confirmDeferred.resolve();
                  }
               },
               {
                  'id': 'cancel',
                  'text': 'キャンセル',
                  'click': function() {
                     $('#' + dialogId).imuiMessageDialog('close');
                     // キャンセル時はページダイアログを閉じない
                     confirmDeferred.reject();
                  }
               }
            ],
            close: function(event, ui) {
               $('#' + dialogId).remove();
            }
         });
      } else {
         // inputに変更がない場合はページダイアログを閉じる
         confirmDeferred.resolve();
      }
      return confirmDeferred.promise();
   }

   function save() {
      // deferredオブジェクト
      var saveDeferred = new $.Deferred();

      // 保存処理
      $.ajax({
         type: 'POST',
         url: 'save',
         async: false,
         data: {data: $('#textbox1').val()},
         success: function(res, dataType) {
            imuiShowSuccessMessage('保存しました。');
            saveDeferred.resolve();
         },
         error: function(res, textStatus, xhr) {
            imuiShowErrorMessage(res.responseText);
            saveDeferred.reject();
         }
      });
      return saveDeferred.promise();
   }
</script>
```
```js:pageDialog.js
// リクエストパラメータのページダイアログID
var $pageDialogId;

function init(request) {
   $pageDialogId = request.pageDialogId;
}
```