// dialog表示中は reset.css の `:has(dialog[open]){overflow:hidden}` が背景を固定するが、 // overflow切替そのものがスクロール位置を0にリセットするブラウザ挙動があるため、 // position:fixed方式で見た目のスクロール位置を保持する(ダイアログを開くたびにトップへ戻る不具合の対策)。 let _dialogScrollLockCount = 0; let _dialogScrollLockY = 0; function _lockDialogScroll() { if (_dialogScrollLockCount === 0) { _dialogScrollLockY = window.scrollY; document.body.style.position = 'fixed'; document.body.style.top = '-' + _dialogScrollLockY + 'px'; document.body.style.left = '0'; document.body.style.right = '0'; } _dialogScrollLockCount += 1; } function _unlockDialogScroll() { _dialogScrollLockCount = Math.max(0, _dialogScrollLockCount - 1); if (_dialogScrollLockCount === 0) { document.body.style.position = ''; document.body.style.top = ''; document.body.style.left = ''; document.body.style.right = ''; window.scrollTo(0, _dialogScrollLockY); } } // showModal()の直前にロックし、dialogがDOMから消えた時点(close()経由・remove()経由どちらでも)でアンロックする共通処理。 // closeイベントだけに頼らないのは、close()を呼ばずにremove()だけで閉じている既存コードがあるため。 function _openDialogWithScrollLock(dialog) { _lockDialogScroll(); dialog.dataset.scrollLocked = '1'; const unlock = () => { if (dialog.dataset.scrollLocked !== '1') return; delete dialog.dataset.scrollLocked; _unlockDialogScroll(); }; dialog.addEventListener('close', unlock); const observer = new MutationObserver(() => { if (!dialog.isConnected) { unlock(); observer.disconnect(); } }); observer.observe(document.body, { childList: true }); dialog.showModal(); } // variant: '' なら従来どおり(情報表示)。'warn' 等を渡すと alert- クラスを // 付与し、呼び出し側CSSで注意・警告色に切り替えられる(後方互換:既定は無指定)。 function alertDialog(message, autoClose = 2000, variant = '') { const dialog = document.createElement('dialog'); dialog.classList.add('alert-dialog'); if (variant) dialog.classList.add('alert-' + variant); dialog.innerHTML = message; document.body.appendChild(dialog); _openDialogWithScrollLock(dialog); if (autoClose > 0) { setTimeout(() => { document.body.removeChild(dialog); }, autoClose); } else { const button = document.createElement('button'); button.textContent = '閉じる'; button.classList.add('dialog-close'); dialog.appendChild(button); button.addEventListener('click', () => { dialog.remove(); }); } } //confirm()のような仕組みをdialogで実装する async function customConfirm(message, cancelButton = false) { return new Promise((resolve) => { const dialog = document.createElement('dialog'); dialog.classList.add('confirm-dialog'); const messageP = document.createElement('p'); messageP.innerHTML = message; dialog.appendChild(messageP); const buttonContainer = document.createElement('div'); buttonContainer.classList.add('confirm-buttons'); const yesButton = document.createElement('button'); yesButton.textContent = 'はい'; yesButton.classList.add('confirm-yes'); yesButton.addEventListener('click', () => { dialog.remove(); resolve(true); }); buttonContainer.appendChild(yesButton); const noButton = document.createElement('button'); noButton.textContent = 'いいえ'; noButton.classList.add('confirm-no'); noButton.addEventListener('click', () => { dialog.remove(); resolve(false); }); buttonContainer.appendChild(noButton); if (cancelButton) { const cancelButton = document.createElement('button'); cancelButton.textContent = 'キャンセル'; cancelButton.classList.add('confirm-cancel'); cancelButton.addEventListener('click', () => { dialog.remove(); resolve(null); }); buttonContainer.appendChild(cancelButton); } dialog.appendChild(buttonContainer); // Escape等で閉じた場合も確認待ちを終了する(取消として扱う)。 dialog.addEventListener('close', () => { dialog.remove(); resolve(false); }); document.body.appendChild(dialog); _openDialogWithScrollLock(dialog); }); } //window.prompt のような、入力ダイアログ /** * window.prompt のような非同期関数 * @param {string} message - 表示メッセージ * @param {string} defaultValue - 入力欄の初期値 (省略可) * @returns {Promise} - OKなら入力値、キャンセルなら null */ async function customPrompt(message, defaultValue = "") { return new Promise((resolve) => { // ダイアログ生成 const dialog = document.createElement('dialog'); dialog.classList.add('prompt-dialog'); // クラス名を prompt- 用に変更 // メッセージ生成 const messageP = document.createElement('p'); messageP.innerHTML = message; dialog.appendChild(messageP); // 入力欄生成(ここがconfirmとの違い) const input = document.createElement('input'); input.type = 'text'; input.value = defaultValue; input.classList.add('prompt-input'); // Enterキーで送信するためのイベント input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); // フォーム送信などを防ぐ dialog.remove(); resolve(input.value || false); } }); dialog.appendChild(input); // ボタンコンテナ const buttonContainer = document.createElement('div'); buttonContainer.classList.add('prompt-buttons'); // OKボタン const okButton = document.createElement('button'); okButton.textContent = 'OK'; okButton.classList.add('prompt-ok'); okButton.addEventListener('click', () => { dialog.remove(); resolve(input.value || false); // 入力値を返す }); buttonContainer.appendChild(okButton); // キャンセルボタン const cancelButton = document.createElement('button'); cancelButton.textContent = 'キャンセル'; cancelButton.classList.add('prompt-cancel'); cancelButton.addEventListener('click', () => { dialog.remove(); resolve(false); // キャンセルは false を返す }); buttonContainer.appendChild(cancelButton); dialog.appendChild(buttonContainer); // ダイアログが閉じられたとき(Escキーなど)の処理 // ※ remove() だけだとPromiseが解決されないままになるのを防ぐため、ここでもresolveする dialog.addEventListener('close', () => { // すでにDOMから削除されていない場合のみ処理 if (document.body.contains(dialog)) { document.body.removeChild(dialog); } resolve(false); }); // DOMに追加して表示 document.body.appendChild(dialog); _openDialogWithScrollLock(dialog); // 入力欄にフォーカスを当てる(UX向上) input.focus(); }); } function loadingDialog(message) { const dialog = document.createElement('dialog'); dialog.classList.add('loading-dialog'); dialog.innerHTML = `
${message}
`; document.body.appendChild(dialog); _openDialogWithScrollLock(dialog); return dialog; }