2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

プリザンターのモーダルダイアログを手軽に使えるラッパーを作ってみる

2
Posted at

はじめに

プリザンターの拡張スクリプトを書いていると、ユーザーにメッセージを見せたり、確認を取ったり、テキストを入力してもらう場面がよくあります。標準の window.alertwindow.confirm は見た目がブラウザ依存で少し味気ないですし、外部のダイアログライブラリを組み込むのも手間がかかります。

実はプリザンターには jQuery UI が組み込まれていて、.dialog() メソッドがそのまま使えます。v2 テーマではデートピッカーが flatpickr に置き換わるなど jQuery UI 離れが進んでいますが、ダイアログ機能についてはテーマバージョンを問わず jquery-ui.min.jsjquery-ui.min.css が読み込まれており、$p.openDialog / $p.closeDialog も内部で .dialog() を呼んでいます。v2 テーマの style.scss でも .ui-dialog に対して CSS カスタムプロパティでスタイルを当てているため、ダイアログは引き続き jQuery UI ベースで動作します。

ただ、毎回 HTML を組み立てて .dialog() のオプションを書くのは面倒です。

この記事では、jQuery UI の .dialog() をラップして $p.modal.alert()$p.modal.confirm()$p.modal.prompt() のように呼び出せる軽量なラッパーを拡張スクリプトで作ってみます。外部ライブラリは一切不要で、プリザンター標準の jQuery UI だけで動作します。v1・v2 どちらのテーマでも利用できます。

仕組みを整理する

ラッパーの設計方針を整理します。

項目 内容
依存ライブラリ なし(プリザンター標準の jQuery / jQuery UI のみ)
名前空間 $p.modal に各メソッドを追加
メソッド alert / confirm / prompt / show の 4 種類
戻り値 すべて jQuery Deferred(done / fail でコールバック)
スタイル 拡張スタイルで jQuery UI のデフォルトテーマを上書き

メソッド一覧

メソッド 用途 ボタン 戻り値
$p.modal.alert(msg) 情報・警告の表示 OK done で OK 押下後の処理
$p.modal.confirm(msg) 操作の確認 OK / キャンセル done(OK) / fail(キャンセル)
$p.modal.prompt(msg) テキスト入力 OK / キャンセル done(value)(入力値) / fail(キャンセル)
$p.modal.show(opt) 任意の HTML を表示 自由に設定 done(result) / fail

実装してみよう

拡張スタイルと拡張スクリプトの 2 ファイルで構成します。

拡張機能 役割
拡張スタイル jQuery UI ダイアログの見た目を調整
拡張スクリプト $p.modal 名前空間にメソッドを追加

拡張スタイル

拡張スタイルとして App_Data/Parameters/ExtendedStyles/ に配置します。前半は v1 テーマ向けの固定値スタイルで、後半の v2 テーマ向けセクションで CSS カスタムプロパティ(--base-bg / --base-text / --btn-positive-bg 等)による上書きを行います。v2 テーマでは :root にカスタムプロパティが定義されているため自動でテーマの配色に切り替わり、v1 テーマではカスタムプロパティが未定義のため前半の固定値がそのまま使われます。

ExtendedStyles/ModalWrapper.css
/* --- モーダルラッパー専用スタイル --- */

/* ── 共通 / v1テーマ向け(固定値) ─────────────── */

/* オーバーレイ */
.modal-wrapper-overlay {
  background: rgba(0, 0, 0, 0.5) !important;
  z-index: 10000 !important;
}

/* ダイアログ本体 */
.ui-dialog.modal-wrapper {
  border: none;
  border-radius: 8px;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25);
  padding: 0;
  z-index: 10001;
  font-family: inherit;
}

/* タイトルバー */
.ui-dialog.modal-wrapper .ui-dialog-titlebar {
  background: #fff;
  border: none;
  border-bottom: 1px solid #e0e0e0;
  border-radius: 8px 8px 0 0;
  padding: 16px 20px;
  font-size: 16px;
  font-weight: bold;
  color: #333;
}

/* 閉じるボタン非表示 */
.ui-dialog.modal-wrapper .ui-dialog-titlebar-close {
  display: none;
}

/* コンテンツ */
.ui-dialog.modal-wrapper .ui-dialog-content {
  padding: 20px;
  font-size: 14px;
  line-height: 1.7;
  color: #444;
}

/* メッセージテキスト */
.ui-dialog.modal-wrapper .mw-message {
  white-space: pre-wrap;
  word-break: break-word;
}

/* 入力欄 */
.ui-dialog.modal-wrapper .mw-input {
  width: 100%;
  padding: 8px 12px;
  border: 1px solid #ccc;
  border-radius: 4px;
  font-size: 14px;
  box-sizing: border-box;
  margin-top: 12px;
  color: #333;
  background: #fff;
}

.ui-dialog.modal-wrapper .mw-input:focus {
  outline: none;
  border-color: #1976d2;
  box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.2);
}

/* ボタン領域 */
.ui-dialog.modal-wrapper .ui-dialog-buttonpane {
  border-top: 1px solid #e0e0e0;
  padding: 12px 20px;
  background: #fafafa;
  border-radius: 0 0 8px 8px;
  margin-top: 0;
}

/* ボタン共通 */
.ui-dialog.modal-wrapper .ui-dialog-buttonpane button {
  border: none;
  border-radius: 4px;
  padding: 8px 24px;
  font-size: 14px;
  cursor: pointer;
  transition: background 0.2s;
  margin-left: 8px;
}

/* プライマリボタン(OK) */
.ui-dialog.modal-wrapper .ui-dialog-buttonpane button.mw-primary {
  background: #1976d2;
  color: #fff;
}

.ui-dialog.modal-wrapper .ui-dialog-buttonpane button.mw-primary:hover {
  background: #1565c0;
}

/* セカンダリボタン(キャンセル) */
.ui-dialog.modal-wrapper .ui-dialog-buttonpane button.mw-secondary {
  background: #e0e0e0;
  color: #333;
}

.ui-dialog.modal-wrapper .ui-dialog-buttonpane button.mw-secondary:hover {
  background: #bdbdbd;
}

/* ── v2テーマ向け(CSSカスタムプロパティで自動切替) ── */
/* v2テーマ(cerulean / green-tea / mandarin / midnight)では
   :root にカスタムプロパティが定義されるため以下で上書きされます。
   v1テーマではカスタムプロパティが未定義のため
   上の固定値がそのまま使われます。                        */

.ui-dialog.modal-wrapper {
  box-shadow: 0 8px 32px var(--base-shadow, rgba(0, 0, 0, 0.25));
}

.ui-dialog.modal-wrapper .ui-dialog-titlebar {
  background: var(--base-bg, #fff);
  border-bottom-color: var(--base-border, #e0e0e0);
  color: var(--base-text, #333);
}

.ui-dialog.modal-wrapper .ui-dialog-content {
  color: var(--base-text, #444);
}

.ui-dialog.modal-wrapper .mw-input {
  border-color: var(--base-border, #ccc);
  color: var(--base-text, #333);
  background: var(--base-bg, #fff);
}

.ui-dialog.modal-wrapper .mw-input:focus {
  border-color: var(--primaryColor, #1976d2);
}

.ui-dialog.modal-wrapper .ui-dialog-buttonpane {
  border-top-color: var(--base-border, #e0e0e0);
  background: var(--base-bg-light, #fafafa);
}

.ui-dialog.modal-wrapper .ui-dialog-buttonpane button.mw-primary {
  background: var(--btn-positive-bg, #1976d2);
  color: var(--btn-positive-label, #fff);
}

.ui-dialog.modal-wrapper .ui-dialog-buttonpane button.mw-primary:hover {
  background: var(--btn-positive-hover, #1565c0);
}

.ui-dialog.modal-wrapper .ui-dialog-buttonpane button.mw-secondary {
  background: var(--btn-normal-bg, #e0e0e0);
  color: var(--btn-normal-label, #333);
}

.ui-dialog.modal-wrapper .ui-dialog-buttonpane button.mw-secondary:hover {
  background: var(--btn-normal-hover, #bdbdbd);
}

ポイントをまとめます。

  • .modal-wrapper クラスでスコープを限定し、プリザンター標準のダイアログに影響しないようにしています
  • jQuery UI が自動生成する .ui-dialog / .ui-dialog-titlebar / .ui-dialog-buttonpane を上書きしてモダンな見た目に変更しています
  • 前半は v1 テーマ向けの固定値スタイルです。v1 テーマでは CSS カスタムプロパティが未定義のため、この固定値がそのまま適用されます
  • 後半の v2 テーマ向けセクションで CSS カスタムプロパティ(--base-bg / --base-text / --btn-positive-bg 等)による上書きを行います。v2 テーマ(cerulean / green-tea / mandarin / midnight)では :root にカスタムプロパティが定義されているため、テーマの配色に自動で切り替わります
  • .modal-wrapper-overlay クラスで jQuery UI のオーバーレイ(背景の半透明マスク)をカスタマイズします
  • z-index: 10000 以上にすることで、プリザンター標準の UI 要素より前面に表示されます
  • 閉じるボタン(×)は非表示にし、明示的にボタン操作で閉じるようにしています

拡張スクリプト

拡張スクリプトとして App_Data/Parameters/ExtendedScripts/ に配置します。

ExtendedScripts/ModalWrapper.js
$(function () {
  // ─── $p.modal 名前空間 ─────────────────────────────
  $p.modal = {};

  // ─── 共通:ダイアログ生成 ──────────────────────────
  function createDialog(title, $content, buttons, options) {
    var dfd = $.Deferred();
    var $dlg = $('<div></div>').append($content);
    var dialogClass = 'modal-wrapper'
      + (options && options.dialogClass ? ' ' + options.dialogClass : '');

    $dlg.dialog($.extend({
      title: title,
      modal: true,
      width: 420,
      resizable: false,
      closeOnEscape: false,
      dialogClass: dialogClass,
      buttons: buttons(dfd, $dlg),
      create: function () {
        // オーバーレイにカスタムクラスを付与
        $(this).closest('.ui-dialog')
          .prev('.ui-widget-overlay')
          .addClass('modal-wrapper-overlay');
      },
      close: function () {
        $(this).dialog('destroy').remove();
        if (dfd.state() === 'pending') dfd.reject();
      }
    }, options || {}));

    return dfd.promise();
  }

  // ─── alert ─────────────────────────────────────────
  $p.modal.alert = function (message, title) {
    var $content = $('<div class="mw-message"></div>').text(message);
    return createDialog(title || 'メッセージ', $content, function (dfd, $dlg) {
      return [
        {
          text: 'OK',
          class: 'mw-primary',
          click: function () {
            dfd.resolve();
            $dlg.dialog('close');
          }
        }
      ];
    });
  };

  // ─── confirm ───────────────────────────────────────
  $p.modal.confirm = function (message, title) {
    var $content = $('<div class="mw-message"></div>').text(message);
    return createDialog(title || '確認', $content, function (dfd, $dlg) {
      return [
        {
          text: 'キャンセル',
          class: 'mw-secondary',
          click: function () {
            dfd.reject();
            $dlg.dialog('close');
          }
        },
        {
          text: 'OK',
          class: 'mw-primary',
          click: function () {
            dfd.resolve();
            $dlg.dialog('close');
          }
        }
      ];
    });
  };

  // ─── prompt ────────────────────────────────────────
  $p.modal.prompt = function (message, title, defaultValue) {
    var $content = $('<div></div>')
      .append($('<div class="mw-message"></div>').text(message))
      .append(
        $('<input type="text" class="mw-input">')
          .val(defaultValue || '')
      );

    return createDialog(title || '入力', $content, function (dfd, $dlg) {
      return [
        {
          text: 'キャンセル',
          class: 'mw-secondary',
          click: function () {
            dfd.reject();
            $dlg.dialog('close');
          }
        },
        {
          text: 'OK',
          class: 'mw-primary',
          click: function () {
            dfd.resolve($dlg.find('.mw-input').val());
            $dlg.dialog('close');
          }
        }
      ];
    }, {
      open: function () {
        var $input = $(this).find('.mw-input');
        $input.trigger('focus');
        $input.on('keydown', function (e) {
          if (e.key === 'Enter') {
            $(this).closest('.ui-dialog')
              .find('.mw-primary')
              .trigger('click');
          }
        });
      }
    });
  };

  // ─── show(汎用) ──────────────────────────────────
  $p.modal.show = function (options) {
    var opt = $.extend({
      title: 'ダイアログ',
      content: '',
      width: 420,
      buttons: []
    }, options);

    var $content = typeof opt.content === 'string'
      ? $('<div></div>').html(opt.content)
      : opt.content;

    return createDialog(opt.title, $content, function (dfd, $dlg) {
      if (!opt.buttons.length) {
        return [{
          text: '閉じる',
          class: 'mw-secondary',
          click: function () {
            dfd.resolve();
            $dlg.dialog('close');
          }
        }];
      }
      return opt.buttons.map(function (btn) {
        return {
          text: btn.text,
          class: btn.class || 'mw-secondary',
          click: function () {
            if (btn.action) {
              btn.action(dfd, $dlg);
            } else {
              dfd.resolve(btn.text);
              $dlg.dialog('close');
            }
          }
        };
      });
    }, { width: opt.width, dialogClass: opt.dialogClass || '' });
  };
});

各処理のポイントを見ていきましょう。

共通関数 createDialog

すべてのメソッドの中核となる関数です。

引数 説明
title string ダイアログのタイトル
$content jQuery ダイアログ本文の jQuery オブジェクト
buttons function Deferred と $dlg を受け取り、ボタン配列を返す関数
options object jQuery UI の追加オプション

$.Deferred() を使って非同期の結果を返します。ボタンが押されたら resolve(OK)または reject(キャンセル)を呼び、呼び出し側は .done() / .fail() で後続処理を書けます。

close イベントでは destroyremove で DOM をクリーンアップします。Escape キーや×ボタンで閉じた場合も reject されるため、明示的にキャンセル扱いになります。

$p.modal.alert

メッセージと OK ボタンだけのシンプルなダイアログです。OK を押すと done が呼ばれます。

$p.modal.confirm

OK とキャンセルの 2 ボタン構成です。OK で done、キャンセルで fail が呼ばれます。

$p.modal.prompt

テキスト入力欄付きのダイアログです。OK で入力値が done に渡されます。Enter キーで OK ボタンを押せるようにしています。

$p.modal.show

任意の HTML コンテンツとボタンを自由に設定できる汎用メソッドです。ボタンを省略すると「閉じる」ボタンだけが表示されます。

使い方

ここからは実際の利用例を紹介します。拡張スクリプトやスクリプト項目の中で $p.modal を呼び出します。

メッセージを表示する

$p.modal.alert('処理が完了しました。');

タイトルを変えることもできます。

$p.modal.alert('入力内容に不備があります。', '入力エラー');

確認してから処理を実行する

$p.modal.confirm('この操作は取り消せません。実行しますか?')
  .done(function () {
    // OK が押された場合の処理
    $p.send($('#UpdateButton'));
  });

fail でキャンセル時の処理も書けます。

$p.modal.confirm('レコードを削除しますか?', '削除確認')
  .done(function () {
    $p.send($('#DeleteButton'));
  })
  .fail(function () {
    console.log('キャンセルされました');
  });

テキストを入力してもらう

$p.modal.prompt('コメントを入力してください', 'コメント追加')
  .done(function (value) {
    // 入力された値を分類項目にセット
    $p.set($p.getControl('ClassA'), value);
  });

初期値を指定することもできます。

$p.modal.prompt('理由を入力してください', '却下理由', '内容不備のため')
  .done(function (value) {
    $p.set($p.getControl('DescriptionA'), value);
    $p.send($('#UpdateButton'));
  });

自由なコンテンツを表示する

$p.modal.show({
  title: 'レコード情報',
  width: 500,
  content:
    '<table style="width:100%; border-collapse:collapse;">' +
      '<tr><th style="text-align:left; padding:4px;">ID</th>' +
          '<td style="padding:4px;">' + $p.getControl('ResultId').val() + '</td></tr>' +
      '<tr><th style="text-align:left; padding:4px;">状態</th>' +
          '<td style="padding:4px;">' + $p.getControl('Status').val() + '</td></tr>' +
    '</table>',
  buttons: [
    { text: 'コピー', class: 'mw-primary', action: function (dfd, $dlg) {
        navigator.clipboard.writeText($p.getControl('ResultId').val());
        dfd.resolve('copied');
        $dlg.dialog('close');
      }
    },
    { text: '閉じる', class: 'mw-secondary' }
  ]
}).done(function (result) {
  if (result === 'copied') {
    $p.modal.alert('ID をクリップボードにコピーしました。');
  }
});

イベントと組み合わせる

$p.events と組み合わせて、ボタンクリック時の確認ダイアログとして使えます。

$p.events.on_editor_load = function () {
  // 更新ボタンにハンドラを追加
  $('#UpdateButton').on('click', function (e) {
    e.preventDefault();
    e.stopImmediatePropagation();
    $p.modal.confirm('変更内容を保存しますか?', '保存確認')
      .done(function () {
        // 確認後に送信
        $p.send($('#UpdateButton'));
      });
    return false;
  });
};

カスタマイズ

ダイアログの幅を変える

alertconfirmprompt のデフォルト幅は 420px です。拡張スクリプト内の createDialog 関数の width: 420 を変更するか、show メソッドの width オプションで個別に指定します。

ボタンの色を変える

v1 テーマ向けの固定値を変更します。v2 テーマではテーマの配色が自動で適用されるため、v2 テーマ向けセクションのフォールバック値もあわせて変更します。

ExtendedStyles/ModalWrapper.css
 /* v1テーマ向け(固定値) */
 .ui-dialog.modal-wrapper .ui-dialog-buttonpane button.mw-primary {
-  background: #1976d2;
+  background: #2e7d32;
 }

 .ui-dialog.modal-wrapper .ui-dialog-buttonpane button.mw-primary:hover {
-  background: #1565c0;
+  background: #1b5e20;
 }

 /* v2テーマ向け(CSS変数のフォールバック値もあわせて変更) */
 .ui-dialog.modal-wrapper .ui-dialog-buttonpane button.mw-primary {
-  background: var(--btn-positive-bg, #1976d2);
+  background: var(--btn-positive-bg, #2e7d32);
 }

 .ui-dialog.modal-wrapper .ui-dialog-buttonpane button.mw-primary:hover {
-  background: var(--btn-positive-hover, #1565c0);
+  background: var(--btn-positive-hover, #1b5e20);
 }

タイトルバーの背景色を変える

ExtendedStyles/ModalWrapper.css
 /* v1テーマ向け(固定値) */
 .ui-dialog.modal-wrapper .ui-dialog-titlebar {
-  background: #fff;
+  background: #e3f2fd;
 }

 /* v2テーマ向け(CSS変数のフォールバック値もあわせて変更) */
 .ui-dialog.modal-wrapper .ui-dialog-titlebar {
-  background: var(--base-bg, #fff);
+  background: var(--base-bg, #e3f2fd);
 }

ブラウザ標準ダイアログとの比較

ブラウザ標準のダイアログと比較した場合の違いを整理します。

項目 ブラウザ標準 $p.modal
見た目 ブラウザ依存 CSS でカスタマイズ可能
処理の流れ 同期(ページが止まる) 非同期(done / fail コールバック)
入力の種類 テキストのみ HTML で自由に構成可能
複数ボタン 対応・非対応(2 種類まで) 自由に追加可能
外部ライブラリ 不要 不要(jQuery UI を使用)
他のダイアログとの共存 非対応(ブロック) 対応

まとめ

  • プリザンターに組み込まれている jQuery UI の .dialog() をラップして、$p.modal.alert / confirm / prompt / show の 4 メソッドを提供するラッパーを作りました
  • 外部ライブラリを追加せずに、拡張スクリプトと拡張スタイルの 2 ファイルだけで動作します
  • v1・v2 どちらのテーマでも利用できます。v1 テーマでは固定値のスタイルが適用され、v2 テーマでは CSS カスタムプロパティによりテーマの配色に自動で切り替わります
  • $.Deferred を使った非同期 API なので、done / fail で直感的にコールバックを書けます
  • $p.modal.show で任意の HTML コンテンツとボタンを自由に構成でき、汎用的に使えます
  • CSS を変更するだけで見た目をカスタマイズできます
  • $p.events と組み合わせることで、ボタン操作の確認ダイアログとしても活用できます
2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?