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

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

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

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

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

もう少し見た目重視でコーディング的にも書きやすくできないものだろうかとあれこれ試行錯誤してみた。

そして実際に作ったものが以下のものだ。

JavaScript
let result = await MessageBox.Confirm("本当に良いのですか?"); if (result == MessageBoxResult.Ok) await MessageBox.Info("良いのです"); else await MessageBox.Alert("いいえ、許しません");

ユーザの応答を待機する

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

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

JavaScript
let result = window.confirm("本当に良いのですか?"); if (result)   window.alert("良いのです"); else window.alert("いいえ、許しません");

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

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

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 写真データ販売中! STOCKPHOTO 写真のデータ販売について 写真 PHOTO
湘南ひらつか花火大会
深山唐松
衣笠草
天空の花
車百合
白山石楠花
写真の一覧へ
エリア
ネパール 北海道 屋久島 沖縄 北アルプス 石鎚山系 剣山地 鳥取大山 くじゅう連山 丹沢・大山 富士山
被写体
河川・湖沼 森林 雲・霧 石・岩 雪・氷 生物 植物 街・集落 鉄道 神社 寺院 人物 生活
季節
時間
夕方 マジックアワー
オレンジ・黄 ピンク・紫 茶色 虹色 錦繍
キーワード
石鎚神社
販売
Aflo PIXTA imagemart
タグ
トップ画像 傑作選
コラム PHOTO BLOG
コラムの一覧へ
カテゴリ
撮影 道具
開発室 DEVELOPMENT BLOG
開発室の一覧へ
カテゴリ
ASP.NET Core C# WordPress PHP TypeScript JavaScript
サイトマップ SITEMAP
スタジオ旅路
https://tabiji.gallery
渡邊 佑
tabiji.gallery (c) 2020 Yu Watanabe サイトマップ SITEMAP 写真データ販売中! STOCKPHOTO 写真のデータ販売について 写真 PHOTO
湘南ひらつか花火大会
深山唐松
衣笠草
天空の花
車百合
白山石楠花
写真の一覧へ
エリア
ネパール 北海道 屋久島 沖縄 北アルプス 石鎚山系 剣山地 鳥取大山 くじゅう連山 丹沢・大山 富士山
被写体
河川・湖沼 森林 雲・霧 石・岩 雪・氷 生物 植物 街・集落 鉄道 神社 寺院 人物 生活
季節
時間
夕方 マジックアワー
オレンジ・黄 ピンク・紫 茶色 虹色 錦繍
キーワード
石鎚神社
販売
Aflo PIXTA imagemart
タグ
トップ画像 傑作選
コラム PHOTO BLOG
コラムの一覧へ
カテゴリ
撮影 道具
開発室 DEVELOPMENT BLOG
開発室の一覧へ
カテゴリ
ASP.NET Core C# WordPress PHP TypeScript JavaScript