写真 ブログ 開発室
HOME開発室[JavaScript] 応答待機可能なメッセージボックスを作る
キーワード
JavaScript TypeScript

[JavaScript] 応答待機可能なメッセージボックスを作る

[JavaScript] 応答待機可能なメッセージボックスを作る

JavaScriptで動くメッセージボックスを自作したい

JavaScriptにはalert()confirm()といったメッセージボックスを表示する手段が用意されている。 きちんとモーダルで動作し、余計な処理が挟まれる心配がないという点では優秀なのだが、いかんせんシンプルすぎて融通が利かない。

もう少し見た目重視でコーディング的にも書きやすくできないものだろうかとあれこれ試行錯誤した結果、以下のようなコードで動作するものを作ることができた。 組み込みのものとは異なりどうしても呼び出しにawaitが必要になってしまうが、これはもう言語の性質上やむを得ないだろう。

JavaScript
let result = await MessageBox.Confirm("本当に良いのですか?"); if (result == MessageBoxResult.Ok) { // OKの場合の処理 } else { // キャンセルの場合の処理 }

ユーザの応答を待機する

これを作るにあたり、絶対に機能として欲しかったのは応答への待機だ。

例えばJavaScriptに元々組み込まれているwindow.alert()はユーザがボタンを押すまで待機し、ボタンが押されたら続く処理を実行する。

JavaScript
let result = window.confirm("本当に良いのですか?"); if (result) { // OKの場合の処理 } else { // キャンセルの場合の処理 }

これは処理の流れとしても非常にわかりやすい。

歴史上、こういった処理を自作する場合にはメッセージボックスを呼び出すメソッドに対してコールバック関数を渡すのがセオリーだった。 例えば以下のようなものだ。

JavaScript
MessageBox.Confirm("本当に良いのですか?", function() { /* OKの時の処理 */ }, function() { /* キャンセルの時の処理 */ } );

これでも目的を実現することはできるが、正直に言って処理の流れがわかりにくく可読性が低い。

もっと直感的に読めるコードを実現できないものかと考え、以下のようなコードに落ち着いた。 実際の使い方は冒頭で紹介した通りだ。

メッセージボックスを表示するコード

これはTypeScriptで書いたものだが、型指定を全て削除すればJavaScriptとしても動作するはずだ。

TypeScript
class KeyValue { public static readonly Esc = "Escape"; } export class MessageBoxResult { public static readonly Ok = "ok"; public static readonly Cancel = "cancel"; public static readonly Yes = "yes"; public static readonly No = "no"; public static readonly OkCancel = [this.Ok, this.Cancel]; public static readonly YesNo = [this.Yes, this.No]; } class MessageBoxIcon { public static readonly Information = "information"; public static readonly Exclamation = "exclamation"; public static readonly Question = "question"; } export class MessageBox { public static async Info(message: string, title: string = "情報"): Promise<string> { return new MessageBoxElement().Show(message, title, MessageBoxIcon.Information, MessageBoxResult.Ok); } public static async Alert(message: string, title: string = "エラー"): Promise<string> { return new MessageBoxElement().Show(message, title, MessageBoxIcon.Exclamation, MessageBoxResult.Ok); } public static async Confirm(message: string, title: string = "確認"): Promise<string> { return new MessageBoxElement().Show(message, title, MessageBoxIcon.Question, MessageBoxResult.OkCancel); } } export class MessageBoxElement extends HTMLElement { private Body = document.createElement("div"); private Title = document.createElement("div"); private Message = document.createElement("div"); private ButtonContainer = document.createElement("div"); private OkButton: HTMLButtonElement | null = null; private CancelButton: HTMLButtonElement | null = null; private OldActiveElement: HTMLElement | null = null; public constructor() { super(); this.Build(); } private Build(): void { this.Body.classList.add("body"); this.Title.classList.add("title"); this.Message.classList.add("message"); this.ButtonContainer.classList.add("button-container"); this.Body.append( this.Title, this.Message, this.ButtonContainer ); this.append(this.Body); this.addEventListener("keydown", this.OnKeyDown.bind(this)); } public Show(message: string, title: string, icon: string, buttonTypes: string | string[]): Promise<string> { if (!Array.isArray(buttonTypes)) buttonTypes = [buttonTypes]; this.classList.add(icon); this.OldActiveElement = document.activeElement as HTMLElement; this.Title.textContent = title; this.Message.textContent = message; let promise = new Promise<string>(resolve => { for (let type of buttonTypes) { let button = document.createElement("button"); let caption: string; switch (type) { case MessageBoxResult.Ok: caption = "OK"; break; case MessageBoxResult.Cancel: caption = "キャンセル"; break; case MessageBoxResult.Yes: caption = "はい"; break; case MessageBoxResult.No: caption = "いいえ"; break; default: throw new Error("Invalid ButtonType: " + type); } button.classList.add(type, "flat"); button.textContent = caption; button.addEventListener("click", e => { this.Close(); resolve(type); }); this.ButtonContainer.append(button); switch (type) { case MessageBoxResult.Ok: case MessageBoxResult.Yes: this.OkButton = button; break; case MessageBoxResult.Cancel: case MessageBoxResult.No: this.CancelButton = button; break; } } }); document.body.insertAdjacentElement("beforeend", this); this.SetDefaultFocus(); return promise; } private SetDefaultFocus(): void { if (this.CancelButton !== null) this.CancelButton?.focus(); else if (this.OkButton !== null) this.OkButton?.focus(); else this.focus(); } private Close(): void { this.remove(); this.OldActiveElement?.focus(); document.body.classList.remove("message-box-shown"); } private OnKeyDown(e: KeyboardEvent): void { if (e.key === KeyValue.Esc) { e.stopPropagation(); e.preventDefault(); if (this.CancelButton !== null) { this.CancelButton.focus(); this.CancelButton.click(); } else if (this.CancelButton === null && this.OkButton !== null) { this.OkButton.focus(); this.OkButton.click(); } } } } customElements.define("message-box", MessageBoxElement);

以下は装飾用のCSSになる。 なお別途Font Awesome Free 5.15.4を使用しているので、これだけをコピーしてもアイコンが表示されないので注意されたい。

SCSS
html body:has(> message-box) { overflow: hidden; } message-box { display: flex; justify-content: center; align-items: center; flex-direction: column; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.9); z-index: 100000; } message-box > .body { display: grid; grid-template-rows: auto 1fr auto; max-height: 90%; max-width: 600px; } message-box > .body > .title { color: #FFFFFF; font-size: 1.3rem; font-weight: 500; text-align: center; margin-bottom: 0.5rem; } message-box > .body > .title::before { margin-right: 0.5rem; } message-box > .body > .message { background-color: rgba(255, 255, 255, 0.95); overflow: auto; padding: 1rem; white-space: pre-wrap; } message-box > .body > .button-container { display: flex; justify-content: center; align-items: center; flex-direction: row; gap: 1rem; margin-top: 1rem; } message-box > .body > .button-container button.flat { border: none; background-color: #0854c7; border-radius: 5px; color: #FFFFFF; line-height: 2rem; padding: 0 1rem; cursor: pointer; } message-box > .body > .button-container button.flat:hover { background-color: #d82b00; } message-box.information > .body > .title::before { font-family: "Font Awesome 5 Free"; font-weight: 900; content: "\f05a"; } message-box.exclamation > .body > .title::before { font-family: "Font Awesome 5 Free"; font-weight: 900; content: "\f06a"; } message-box.question > .body > .title::before { font-family: "Font Awesome 5 Free"; font-weight: 900; content: "\f059"; }

完全なモーダルではない

最大の課題は完全なモーダルではないということだ。

半透明のレイヤーで背後の要素に触れなくなっているように見えるが、実際はTABキーによってフォーカスを移動すれば背後の要素に干渉することができる。 キー入力を徹底的に制御してそれすらさせないことも可能かもしれないが、そこまでする価値はないように思う。

あるいはHTMLの<dialog>タグを使えばもしかしたらうまく制御できるのかもしれない。 こちらはまだ研究途上なので、もしうまくいくようならいずれ改良していきたい。

ひとこと

まだまだ改良の余地があると感じつつも、比較的使いやすい形にまとまったと思う。 await/asyncPromiseが実装されてからというもの、JavaScriptの可能性がかなり広がった。 また時間があればバージョンアップしていきたいものだ。

キーワード
JavaScript TypeScript
シェアする
サイトマップ SITEMAP 広告 写真 PHOTO
年度
2024 2023 2022 2021 2020 2019 2018 2017 ~2016
アルバム
傑作選 北海道 石鎚山系 ネパール 屋久島 北アルプス
写真の一覧へ
ストックフォトで作品を探す
JOURNEY
2020年 冬の屋久島歩き旅
2019年 厳冬期北海道徒歩横断
旅の一覧へ
ブログ BLOG
カテゴリ
写真 よもやま話
タグ
スポット 道具 心得
ブログの一覧へ
開発室 DEVELOPMENT
カテゴリ
ASP.NET Core C# WordPress PHP TypeScript JavaScript Web
開発室の一覧へ
広告
サイトマップ SITEMAP
スタジオ旅路
https://tabiji.gallery
渡邊 佑
tabiji.gallery (c) 2020 Yu Watanabe サイトマップ SITEMAP 写真 PHOTO
年度
2024 2023 2022 2021 2020 2019 2018 2017 ~2016
アルバム
傑作選 北海道 石鎚山系 ネパール 屋久島 北アルプス
写真の一覧へ
ストックフォトで作品を探す
JOURNEY
2020年 冬の屋久島歩き旅
2019年 厳冬期北海道徒歩横断
旅の一覧へ
ブログ BLOG
カテゴリ
写真 よもやま話
タグ
スポット 道具 心得
ブログの一覧へ
開発室 DEVELOPMENT
カテゴリ
ASP.NET Core C# WordPress PHP TypeScript JavaScript Web
開発室の一覧へ