Files
Poker/public/assets/common.js
T
agomizi1cmandClaude Opus 5.5 680aa20d30 Replace native confirm() with an in-site confirmation dialog
Add App.confirm() in common.js: a promise-based dialog styled like the
site's modals, with a title, multi-line message and custom button labels.
Destructive actions use a red OK button and focus Cancel by default;
Escape and backdrop clicks cancel, and Tab stays inside the dialog.

Use it for folding when a check is possible, leaving the table,
confirming showdown winners and deleting tables from the lobby. After the
dialog closes, re-check that the action is still valid (turn not passed,
pot not already awarded, table still listed) before sending it.

Also add a "poker-test" launch config that runs on port 4672 with the
example keys from .env.example, for local browser testing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 00:34:43 +09:00

172 lines
6.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 各ページ共通のユーティリティ
(function () {
'use strict';
function lsGet(key) {
try { return localStorage.getItem(key); } catch { return null; }
}
function lsSet(key, value) {
try { localStorage.setItem(key, value); } catch { /* プライベートモード等 */ }
}
// 非HTTPS環境でも動くよう crypto.randomUUID ではなく getRandomValues を使う
function randomHex(bytes) {
const a = new Uint8Array(bytes);
crypto.getRandomValues(a);
return Array.from(a, (b) => b.toString(16).padStart(2, '0')).join('');
}
const Store = {
get name() { return lsGet('poker.name') || ''; },
set name(v) { lsSet('poker.name', v); },
// 席の再接続用。ブラウザごとに固定
get token() {
let t = lsGet('poker.token');
if (!t || !/^[0-9a-f]{32}$/.test(t)) {
t = randomHex(16);
lsSet('poker.token', t);
}
return t;
},
};
function esc(s) {
return String(s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
}
function chips(n) {
return '$' + Number(n).toLocaleString('en-US');
}
let toastTimer;
function toast(msg) {
let el = document.querySelector('.toast');
if (!el) {
el = document.createElement('div');
el.className = 'toast';
document.body.appendChild(el);
}
el.textContent = msg;
el.hidden = false;
clearTimeout(toastTimer);
toastTimer = setTimeout(() => { el.hidden = true; }, 2500);
}
// ブラウザ標準の confirm() の代わりの確認ダイアログ。OK なら true で解決する。
// opts: { title, message(改行可), ok, cancel, danger(OK を赤にする) }
// 新しいダイアログを開くと、開いていたものはキャンセル扱いで閉じる
let closeConfirm = null;
function confirmDialog(opts) {
const o = typeof opts === 'string' ? { message: opts } : opts;
if (closeConfirm) closeConfirm(false);
const backdrop = document.createElement('div');
backdrop.className = 'modal-backdrop confirm-backdrop';
backdrop.innerHTML =
'<div class="modal confirm-modal" role="alertdialog" aria-modal="true">' +
(o.title ? '<h2 class="confirm-title"></h2>' : '') +
'<p class="confirm-message"></p>' +
'<div class="confirm-buttons">' +
'<button type="button" class="btn btn-dark" data-confirm="cancel"></button>' +
`<button type="button" class="btn ${o.danger ? 'btn-red' : 'btn-orange'}" data-confirm="ok"></button>` +
'</div></div>';
const dialog = backdrop.firstChild;
const msgEl = dialog.querySelector('.confirm-message');
msgEl.textContent = o.message || '';
msgEl.id = 'confirm-message-' + randomHex(4);
dialog.setAttribute('aria-describedby', msgEl.id);
if (o.title) {
const titleEl = dialog.querySelector('.confirm-title');
titleEl.textContent = o.title;
titleEl.id = 'confirm-title-' + randomHex(4);
dialog.setAttribute('aria-labelledby', titleEl.id);
}
const okBtn = dialog.querySelector('[data-confirm="ok"]');
const cancelBtn = dialog.querySelector('[data-confirm="cancel"]');
okBtn.textContent = o.ok || 'OK';
cancelBtn.textContent = o.cancel || 'キャンセル';
const prevFocus = document.activeElement;
return new Promise((resolve) => {
function close(result) {
if (closeConfirm !== close) return;
closeConfirm = null;
document.removeEventListener('keydown', onKey, true);
backdrop.remove();
if (prevFocus && prevFocus.focus && document.contains(prevFocus)) prevFocus.focus();
resolve(result);
}
function onKey(e) {
if (e.key === 'Escape') {
e.preventDefault();
close(false);
} else if (e.key === 'Tab') {
// フォーカスをダイアログの2つのボタンの中に閉じ込める
e.preventDefault();
(document.activeElement === okBtn ? cancelBtn : okBtn).focus();
}
}
okBtn.addEventListener('click', () => close(true));
cancelBtn.addEventListener('click', () => close(false));
// 背景を押したらキャンセル(ダイアログ内から背景へのドラッグは除く)
backdrop.addEventListener('pointerdown', (e) => {
backdrop.dataset.downOnBackdrop = e.target === backdrop ? '1' : '';
});
backdrop.addEventListener('click', (e) => {
if (e.target === backdrop && backdrop.dataset.downOnBackdrop) close(false);
});
document.addEventListener('keydown', onKey, true);
closeConfirm = close;
document.body.appendChild(backdrop);
// 危険な操作はうっかり Enter で確定しないようキャンセルにフォーカスする
(o.danger ? cancelBtn : okBtn).focus();
});
}
async function api(method, url, body) {
const res = await fetch(url, {
method,
headers: body ? { 'Content-Type': 'application/json' } : undefined,
body: body ? JSON.stringify(body) : undefined,
credentials: 'same-origin',
});
if (res.status === 401 && url !== '/api/auth') {
location.href = '/key?next=' + encodeURIComponent(location.pathname);
throw new Error('unauthorized');
}
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.error || 'エラーが発生しました');
return data;
}
// サーバは WebSocket のみ受け付ける。接続を拒否されたら鍵が無効になっていないか確認する
function connectSocket() {
const socket = io({ transports: ['websocket'] });
socket.on('connect_error', () => {
fetch('/api/tables', { credentials: 'same-origin' })
.then((res) => {
if (res.status === 401) location.href = '/key?next=' + encodeURIComponent(location.pathname);
})
.catch(() => {});
});
return socket;
}
// 自サイト内のパスだけ許可("//x" や "/\x" は外部サイト扱いになるので拒否)
function safeNext(next) {
return typeof next === 'string' && /^\/(?![/\\])[^\\\s]*$/.test(next) && next.length <= 200 ? next : '/';
}
const PHASE_LABELS = {
WAITING: 'WAITING',
PREFLOP: 'PREFLOP',
FLOP: 'FLOP',
TURN: 'TURN',
RIVER: 'RIVER',
SHOWDOWN: 'SHOWDOWN',
FINISHING: 'FINISHING',
};
window.App = { Store, esc, chips, toast, confirm: confirmDialog, api, connectSocket, safeNext, PHASE_LABELS };
})();