0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

編集画面のリンク項目の作成ボタンを拡張機能だけで制御してみる

0
Posted at

はじめに

プリザンターの編集画面には、現在のレコードとリンク関係にあるレコードの一覧を表示する「リンク」セクションがあります。サイト設定でリンクを定義すると、このセクションにリンク先テーブルへの「作成」ボタンが表示され、クリックするだけでリンク済みの新規レコード作成画面へ移動できます。

この作成ボタンを押すと現在の編集画面から離れてしまうため、フォームに未保存のデータがある場合は入力途中の内容が失われる可能性があります。今回は、そのような状況に対応するための 4 つの制御パターンを拡張スクリプトだけで実装してみます。

パターン 動作
パターン 1 未保存のデータがある場合、作成ボタンを無効化し「未保存データあり」を表示する
パターン 2 未保存のデータがある場合、作成ボタンに「未保存データあり」を表示する(通常挙動)
パターン 3 未保存のデータがある場合、作成ボタン押下で自動保存してから遷移する
パターン 4 未保存のデータがある場合、作成ボタン押下で確認ダイアログを表示する

バージョン 1.5 以降を対象にしています。

仕組みを整理する

作成ボタンの HTML 構造

編集画面のリンクセクションに表示される作成ボタンは、次のような HTML で構成されています。

<fieldset class="enclosed link-creations is-sources">
  <legend>リンク</legend>
  <div>
    <button
      class="button button-icon confirm-unload"
      onclick="$p.new($(this));"
      data-id="{リンクID}"
      data-from-site-id="{現在のサイトID}"
      data-to-site-id="{リンク先サイトID}"
      from-tab-index="{タブインデックス}"
      do-not-return-parent="false"
    >
      リンク先テーブル名
    </button>
  </div>
</fieldset>

注目すべきポイントは 2 つです。

  • class="button button-icon confirm-unload": confirm-unload クラスが付与されています。このクラスが付いた要素の内側にある入力欄が変更されると、プリザンターはフォームを「変更済み」状態として扱います
  • onclick="$p.new($(this));": クリック時に $p.new() を呼び出します。これが遷移処理の本体です
Implem.Pleasanter/Libraries/HtmlParts/HtmlLinkCreations.cs
return hb.Button(
    attributes: new HtmlAttributes()
        .Class("button button-icon confirm-unload")
        .OnClick("$p.new($(this));")
        ...

$p.formChanged:未保存状態の検知

プリザンターのフロントエンドには、フォームの変更状態を管理する $p.formChanged フラグがあります。.confirm-unload 内の入力要素(input・select・textarea 等)が変更されると $p.setFormChanged() が呼び出され、$p.formChangedtrue に設定されます。

_form.js
$p.setFormChanged = function ($control) {
    if (!$control.hasClass('not-set-form-changed')) {
        $p.formChanged = true;
    }
};

拡張スクリプトからこのフラグを参照することで、フォームの未保存状態を判定できます。

$p.new():遷移処理の本体

作成ボタンのクリックで呼び出される $p.new() は、ボタンのデータ属性を使ってリンク先テーブルの新規作成 URL を構築し、$p.transition() で遷移します。

item.js
$p.new = function ($control) {
    $p.transition(
        $('#BaseUrl').val() +
            $control.attr('data-to-site-id') +
            '/new' +
            '?FromSiteId=' + $control.attr('data-from-site-id') +
            '&LinkId=' + $control.attr('data-id') + ...
    );
};

4 つのパターンの制御フロー

パターンごとの処理の流れは次のとおりです。

実装してみよう

4 つのパターンはいずれも拡張スクリプトとして App_Data/Parameters/ExtendedScripts/ に配置します。以下のいずれか 1 つを選択してください。

パターン 1:ボタンを無効化する

未保存のデータがある間、作成ボタンを無効化(グレーアウト)し、ボタン内に jQuery UI アイコン付きで「未保存データあり」を表示します。ユーザーが保存するまでリンクレコードの作成へ進めないようになります。

ExtendedScripts/LinkCreationDisable.js
$(function () {
    var _orig = $p.setFormChanged;
    $p.setFormChanged = function ($control) {
        _orig.call($p, $control);
        updateButtons();
    };

    $p.events['ajax_after_done_UpdateCommand'] = function (args) {
        if (args.ret === 0) {
            $p.formChanged = false;
            updateButtons();
        }
    };

    function updateButtons() {
        $('.link-creations button.confirm-unload').each(function () {
            var $btn = $(this);
            if (!$btn.data('lcb-original-text')) {
                $btn.data('lcb-original-text', $.trim($btn.text()));
            }
            if ($p.formChanged) {
                $btn.prop('disabled', true);
                $btn.html(
                    '<span class="ui-icon ui-icon-alert" style="display:inline-block;vertical-align:middle;margin-right:4px;"></span>' +
                    '<span style="vertical-align:middle;">' +
                    $btn.data('lcb-original-text') +
                    '(未保存データあり)</span>'
                );
            } else {
                $btn.prop('disabled', false);
                $btn.text($btn.data('lcb-original-text'));
            }
        });
    }

    updateButtons();
});

パターン 2:ボタンに状態だけ表示する(通常挙動)

未保存のデータがある間、作成ボタン内に jQuery UI アイコン付きで「未保存データあり」を表示します。ボタンは無効化せず、クリック時の挙動は標準の $p.new() のままです。

ExtendedScripts/LinkCreationNoticeOnly.js
$(function () {
    var _orig = $p.setFormChanged;
    $p.setFormChanged = function ($control) {
        _orig.call($p, $control);
        updateButtons();
    };

    $p.events['ajax_after_done_UpdateCommand'] = function (args) {
        if (args.ret === 0) {
            $p.formChanged = false;
            updateButtons();
        }
    };

    function updateButtons() {
        $('.link-creations button.confirm-unload').each(function () {
            var $btn = $(this);
            if (!$btn.data('lcb-original-text')) {
                $btn.data('lcb-original-text', $.trim($btn.text()));
            }
            if ($p.formChanged) {
                $btn.html(
                    '<span class="ui-icon ui-icon-alert" style="display:inline-block;vertical-align:middle;margin-right:4px;"></span>' +
                    '<span style="vertical-align:middle;">' +
                    $btn.data('lcb-original-text') +
                    '(未保存データあり)</span>'
                );
            } else {
                $btn.text($btn.data('lcb-original-text'));
            }
        });
    }

    updateButtons();
});

パターン 3:強制保存してから遷移する

作成ボタンを押したとき、未保存のデータがあれば自動的に保存(更新)を行い、保存が成功した場合のみリンクレコードの新規作成画面へ遷移します。バリデーションエラーなどで保存が失敗した場合は遷移しません。

ExtendedScripts/LinkCreationAutoSave.js
$(function () {
    function initButtons() {
        $('.link-creations button.confirm-unload').each(function () {
            var $btn = $(this);
            if ($btn.data('lcb-ready')) return;
            $btn.data('lcb-ready', true);
            $btn.removeAttr('onclick');
            $btn.on('click.lcb', function () {
                var $self = $(this);
                if (!$p.formChanged) {
                    $p.new($self);
                    return;
                }
                var $updateBtn = $('#UpdateCommand');
                if ($updateBtn.length === 0) {
                    $p.new($self);
                    return;
                }
                var error = $p.syncSend($updateBtn);
                if (error === 0) {
                    $p.new($self);
                }
            });
        });
    }

    initButtons();
    $(document).ajaxComplete(initButtons);
});

パターン 4:アラートを表示する

作成ボタンを押したとき、未保存のデータがあれば確認ダイアログを表示します。ユーザーが「OK」を選択した場合はそのまま遷移し、「キャンセル」を選択した場合は留まります。

ExtendedScripts/LinkCreationAlert.js
$(function () {
    function initButtons() {
        $('.link-creations button.confirm-unload').each(function () {
            var $btn = $(this);
            if ($btn.data('lcb-ready')) return;
            $btn.data('lcb-ready', true);
            $btn.removeAttr('onclick');
            $btn.on('click.lcb', function () {
                if (!$p.confirmReload()) return;
                $p.new($(this));
            });
        });
    }

    initButtons();
    $(document).ajaxComplete(initButtons);
});

各処理のポイント

ボタンのクリックハンドラーを置き換える

パターン 3・4 では、作成ボタンに設定されている onclick="$p.new($(this));" 属性を削除して、jQuery のクリックハンドラーに置き換えます。

$btn.removeAttr('onclick');
$btn.on('click.lcb', function () {
    // 独自の処理
});

removeAttr('onclick') で元のインラインハンドラーを除去し、.on('click.lcb', ...) で名前空間付きのイベントを追加しています。名前空間(.lcb)は、イベントを後から選択的に解除できるようにするための識別子です。

重複初期化の防止

$(document).ajaxComplete() で Ajax 処理後にも initButtons() を再実行するため、data-lcb-ready フラグで二重初期化を防いでいます。

if ($btn.data('lcb-ready')) return;
$btn.data('lcb-ready', true);

$p.setFormChanged のオーバーライド(パターン 1)

パターン 1 とパターン 2 では $p.setFormChanged() をオーバーライドして、フォームが変更されたタイミングでボタンの状態を更新します。元の処理を _orig に退避させてから呼び出すことで、既存の動作を維持しながら拡張しています。

var _orig = $p.setFormChanged;
$p.setFormChanged = function ($control) {
    _orig.call($p, $control);
    updateButtons();
};

$p.events による保存後の再有効化(パターン 1)

プリザンターのフロントエンドには $p.events というイベントバスがあり、$p.execEvents() を通じて次の命名規則でイベントが発火します。

{イベント名}_{controlId}

更新ボタンは id="UpdateCommand" を持つため、$p.events['ajax_after_done_UpdateCommand'] に関数を登録すると、更新ボタンの Ajax 完了時だけフックできます。ajax_after_donesetByJson() でレスポンスが DOM に反映されたあとに発火するため、args.ret には保存の成否(0:成功、-1:エラー)が入っています。

$p.events['ajax_after_done_UpdateCommand'] = function (args) {
    if (args.ret === 0) {
        $p.formChanged = false; // プリザンターが戻さないフラグを手動リセット
        updateButtons();        // ボタンを再有効化
    }
};

$p.formChanged をリセットしたうえで updateButtons() を呼ぶことで、保存成功後に作成ボタンが再び押せる状態に戻ります。バリデーションエラー等で保存が失敗した場合(args.ret === -1)は何もしないため、ボタンは無効化されたままです。

プリザンター本体は保存後に $p.formChangedfalse へ戻しません。パターン 1 とパターン 2 では $p.events['ajax_after_done_UpdateCommand'] フックを使って更新ボタンの Ajax 完了を検知し、保存成功時(args.ret === 0)に $p.formChanged を手動でリセットすることで表示状態を戻しています。

$p.syncSend による同期保存(パターン 3)

パターン 3 では $p.syncSend($('#UpdateCommand')) でフォームを同期保存します。これはプリザンター内部でも $p.copy() など複数の箇所で使われているパターンです。

var error = $p.syncSend($updateBtn);
if (error === 0) {
    $p.new($self);
}

$p.syncSend() は保存の成否を戻り値で返します(0:成功、-1:エラー)。保存が成功した場合のみ遷移する分岐が簡潔に書けるのが特徴です。#UpdateCommand が存在しない場合(更新権限のないレコードなど)は保存をスキップして遷移します。

$p.confirmReload の活用(パターン 4)

パターン 4 では $p.confirmReload() を利用しています。これはプリザンターが「前のレコード」「次のレコード」への移動時にも使う既存の確認ダイアログ関数です。

confirm.js
$p.confirmReload = function confirmReload() {
    if ($p.formChanged) {
        return confirm($p.display('ConfirmUnload'));
    } else {
        return true;
    }
};

未保存データがあるときはブラウザ標準の confirm() ダイアログを表示し、true(OK)または false(キャンセル)を返します。表示されるメッセージは「このページを離れますか? 行った変更が保存されない可能性があります。」です。このためパターン 4 のクリックハンドラーを非常にシンプルに記述できます。

まとめ

プリザンターの編集画面に表示されるリンク項目の作成ボタンを、拡張スクリプトだけで制御する 4 つのパターンを紹介しました。

  • パターン 1(無効化): $p.setFormChanged() をオーバーライドして未保存時にボタンを disabled にし、jQuery UI アイコン付きで「未保存データあり」を表示する。誤遷移を確実に防止できる
  • パターン 2(表示のみ): ボタンは通常どおり押せるまま、jQuery UI アイコン付きで「未保存データあり」を表示する。既存挙動を維持しつつ注意喚起だけ追加できる
  • パターン 3(自動保存): $p.syncSend() でフォームを保存してから遷移する。バリデーションエラー時は遷移せず、ユーザーへの確認も不要なのでスムーズな操作感を提供できる
  • パターン 4(確認ダイアログ): プリザンター標準の $p.confirmReload() で確認ダイアログを表示し、ユーザーが判断できる。コードが最もシンプルで、プリザンター標準の UX パターンと一致する

どのパターンを選ぶかは、業務フローや UX 要件に応じて決めてください。データ損失を完全に防ぎたい場合はパターン 3、ユーザーの自由度を残したい場合はパターン 4 が適しています。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?