Last change added an index to the rulings sheet and I wrote down what it cost: 342px at the top of a 390px-wide sheet, so a dealer reading straight through scrolls past the whole thing first. That was an honest trade at the time. It is also fixable. The index was labelled with the section headings, which are sentences -- "A short all-in doesn't reopen the betting" -- so nine of them stacked one per row. A dealer hunting for that ruling is looking for the words "short all-in", not the sentence. Misdeal Board card early Out of turn Short all-in Uncalled bet Oversized chip Minimum raise Odd chip String bet phone 342px -> 143px 9 rows -> 3 desktop ~200 -> 70px -> 2 landscape -> 94px -> 2 The first ruling now starts 309px into the sheet on a phone rather than 440. Each short label lives on its own section as data-short, next to the heading it shortens, and the index falls back to the heading when there is not one -- so a ruling added later still appears, still without anyone having to write it twice. The full sentence stays as the button's aria-label, so the short label never costs a screen reader the heading. Checked at four sizes across touch and pointer contexts: nine labels, nine aria-labels, every chip 44px on touch, jumping still lands and focuses the right heading, contrast clean, no console errors. 244 tests still passing. Also looked at, and left alone: the card picker refuses a card already dealt -- disabled, titled "Already dealt to Ann" -- and every one of its 52 buttons clears 44px. A messy nine-seat table with 25/50 blinds, two folded players, a busted seat, accented and hyphenated names, a $125,000 stack and a filled notes field audits clean at every size but the landscape phone already known from #63. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
546 lines
25 KiB
JavaScript
546 lines
25 KiB
JavaScript
document.addEventListener("DOMContentLoaded", () => {
|
|
const state = HoldemState.load();
|
|
|
|
// Focus, Tab-trapping and Escape for every overlay, wired once by watching the
|
|
// `hidden` attribute they all already toggle.
|
|
Modal.mount();
|
|
|
|
// Undo works by swapping the whole table back to a snapshot. `state` is handed out
|
|
// by reference to every module at mount time, so it has to be refilled in place —
|
|
// reassigning the variable would leave the renderer and the betting bar pointing at
|
|
// the old object and quietly ignoring the undo.
|
|
const snapshotState = () => JSON.parse(JSON.stringify(state));
|
|
|
|
const restoreState = snapshot => {
|
|
Object.keys(state).forEach(key => delete state[key]);
|
|
Object.assign(state, snapshot);
|
|
HoldemState.save(state);
|
|
Render.renderAll();
|
|
};
|
|
|
|
// A gesture on a seat is a shortcut for something the dealer could do the long
|
|
// way, so each one is undoable the same way the header buttons are.
|
|
const seatAction = (index, message, mutate) => {
|
|
const snapshot = snapshotState();
|
|
mutate(state.seats[index], index);
|
|
HoldemState.save(state);
|
|
Render.renderAll();
|
|
|
|
Toast.show(message, {
|
|
actionLabel: "Undo",
|
|
onAction: () => {
|
|
restoreState(snapshot);
|
|
Toast.show("Undone.", { duration: 2500 });
|
|
},
|
|
duration: 6000,
|
|
});
|
|
};
|
|
|
|
const nameOf = index => HoldemState.displayName(state.seats[index], index);
|
|
|
|
const hooks = {
|
|
save: s => HoldemState.save(s),
|
|
rerender: () => Render.renderAll(),
|
|
|
|
onSeatDoubleTap: index => {
|
|
const player = state.seats[index];
|
|
if (!player.inHand) return;
|
|
if (player.folded) {
|
|
seatAction(index, `${nameOf(index)} is back in the hand.`, p => { p.folded = false; });
|
|
return;
|
|
}
|
|
seatAction(index, `${nameOf(index)} folds.`, () => {
|
|
if (state.actingSeatIndex === index) Betting.fold(state, index);
|
|
else player.folded = true;
|
|
});
|
|
},
|
|
|
|
onSeatLongPress: index => SeatMenu.open(state, seatMenuHooks, index),
|
|
onGiveButton: index => seatMenuHooks.makeDealer(index),
|
|
};
|
|
|
|
const seatMenuHooks = {
|
|
fold: index => hooks.onSeatDoubleTap(index),
|
|
unfold: index => seatAction(index, `${nameOf(index)} is back in the hand.`, p => { p.folded = false; }),
|
|
allIn: index => seatAction(index, `${nameOf(index)} is all-in.`, () => Betting.allIn(state, index)),
|
|
toggleDealtIn: index => {
|
|
const player = state.seats[index];
|
|
const dealtIn = player.inHand;
|
|
const handRunning = !Betting.onlyBlindsIn(state) || Betting.potSoFar(state) > 0;
|
|
const alreadyIn = Betting.handTotal(player);
|
|
|
|
// Somebody walking away mid-hand doesn't get their chips back — they're
|
|
// dead money the winner collects. Say so, because the pot total pointedly
|
|
// does not go down and a dealer needs to know that's deliberate.
|
|
const message = dealtIn
|
|
? (alreadyIn > 0
|
|
? `${nameOf(index)} is out — their ${Money.format(alreadyIn)} stays in the pot.`
|
|
: `${nameOf(index)} sits out.`)
|
|
: (handRunning
|
|
? `${nameOf(index)} is in from the next hand.`
|
|
: `${nameOf(index)} is dealt in.`);
|
|
|
|
seatAction(index, message, p => {
|
|
p.inHand = !dealtIn;
|
|
if (!p.inHand) {
|
|
// Their chips stay put; only the fold flag is cleared, since being
|
|
// out of the hand already covers not winning it.
|
|
p.folded = false;
|
|
if (state.actingSeatIndex === index) Betting.passAction(state, index);
|
|
} else if (handRunning) {
|
|
// Joining a hand already in progress means sitting this one out —
|
|
// you can't buy into a pot you haven't been paying for. `folded`
|
|
// is exactly that state, and the next hand clears it.
|
|
p.folded = true;
|
|
}
|
|
});
|
|
},
|
|
|
|
// Two players change chairs. Undoable like every other seat action, because a
|
|
// swap is easy to aim wrong and the seats look alike at a glance.
|
|
swapSeats: (from, to) => {
|
|
const movedName = nameOf(from);
|
|
const intoName = nameOf(to);
|
|
const wasEmpty = Render.isEmptySeat(state.seats[to]);
|
|
runUndoable(
|
|
wasEmpty
|
|
? `${movedName} moved to seat ${to + 1}.`
|
|
: `${movedName} and ${intoName} changed seats.`,
|
|
() => HoldemState.swapSeats(state, from, to));
|
|
},
|
|
|
|
makeDealer: index => {
|
|
// Before the deal there's nothing posted, so this is just moving a marker.
|
|
// After it, the blinds have to follow the button or the badges and the
|
|
// money disagree — but that's a real thing happening to real chips, and
|
|
// the dealer gets told rather than finding out later.
|
|
const willRepost = state.bettingOpen && Betting.onlyBlindsIn(state) &&
|
|
HoldemState.unresolvedContributions(state) > Betting.EPSILON_CHIPS;
|
|
|
|
seatAction(index, willRepost
|
|
? `Button on ${nameOf(index)} — blinds taken back and re-posted.`
|
|
: `Button on ${nameOf(index)}.`, () => {
|
|
state.dealerSeatIndex = index;
|
|
if (willRepost) Betting.repostBlinds(state);
|
|
});
|
|
},
|
|
|
|
assignBlind: (index, which) => {
|
|
const blinds = HoldemState.getBlindSeats(state);
|
|
const other = which === "sb" ? blinds.bbIndex : blinds.sbIndex;
|
|
// Naming one blind pins the other where it already is, so the dealer only
|
|
// has to state the part they're changing. If that would land both on one
|
|
// player, the other blind moves to the next seat along.
|
|
let partner = other !== null && other !== undefined && other !== index
|
|
? other
|
|
: Betting.nextLiveIndex(state, index);
|
|
if (partner === index || partner === null) partner = Betting.nextLiveIndex(state, index);
|
|
|
|
const label = which === "sb" ? "small blind" : "big blind";
|
|
seatAction(index, `${nameOf(index)} posts the ${label}.`, () => {
|
|
state.blindOverride = which === "sb"
|
|
? { sbIndex: index, bbIndex: partner }
|
|
: { sbIndex: partner, bbIndex: index };
|
|
if (Betting.onlyBlindsIn(state)) Betting.repostBlinds(state);
|
|
});
|
|
},
|
|
|
|
clearBlindOverride: () => seatAction(state.dealerSeatIndex, "Blinds follow the button again.", () => {
|
|
state.blindOverride = null;
|
|
if (Betting.onlyBlindsIn(state)) Betting.repostBlinds(state);
|
|
}),
|
|
|
|
editSeat: index => Render.toggleSeatEditor(index),
|
|
|
|
addChips: index => AddChips.open(state, index, (seatIndex, amount) => {
|
|
// Buying chips puts you back in the game — see Betting.buyInDealsThemIn.
|
|
const dealBackIn = Betting.buyInDealsThemIn(state.seats[seatIndex]);
|
|
const handRunning = !Betting.onlyBlindsIn(state) || Betting.potSoFar(state) > 0;
|
|
|
|
const message = `${nameOf(seatIndex)} added ${Money.format(amount)}` +
|
|
(dealBackIn
|
|
? handRunning ? " — in from the next hand." : " — dealt in."
|
|
: ".");
|
|
|
|
seatAction(seatIndex, message, p => {
|
|
// Chips in front and cash in are different questions — the stack moves
|
|
// every hand, what they've handed over only moves when they buy.
|
|
p.stack = Money.round(Betting.stackOf(state, p) + amount);
|
|
p.buyIns = Money.round((Number(p.buyIns) || 0) + amount);
|
|
|
|
if (dealBackIn) {
|
|
p.inHand = true;
|
|
// Same rule "Deal in" follows: you can't buy into a pot you haven't
|
|
// been paying for, and `folded` is exactly that state for one hand.
|
|
p.folded = handRunning;
|
|
}
|
|
});
|
|
}),
|
|
};
|
|
|
|
Render.mount(state, hooks);
|
|
|
|
const openPotWizard = () => {
|
|
// Chips still in front of players belong in the pot being priced, so bank them
|
|
// before the wizard reads anyone's contribution.
|
|
Betting.sweepToHandTotals(state);
|
|
hooks.save(state);
|
|
Render.renderAll();
|
|
|
|
PotWizard.open(state, payouts => {
|
|
const paid = payouts.map(p => `${p.label} ${Money.format(p.amount)}`).join(", ");
|
|
runUndoable(paid ? `Paid ${paid}.` : "Hand closed with nothing to pay out.", () => {
|
|
if (payouts.length > 0) {
|
|
HoldemState.recordHand(state, payouts);
|
|
// Winnings go straight back in front of the player, so the next
|
|
// hand's stacks are already right.
|
|
Betting.creditWinnings(state, payouts);
|
|
}
|
|
// Settling a pot ends the hand; it doesn't start the next one. The
|
|
// table goes back to setup so people can sit down or cash out before
|
|
// any blinds are committed.
|
|
HoldemState.startNextHand(state);
|
|
});
|
|
});
|
|
};
|
|
|
|
// A short history of actions taken from the bar, so a misclicked Fold can be taken
|
|
// back — and the one before it, since a dealer usually notices a beat late. Kept in
|
|
// memory only: this is for the mistake you just made, not last night's hand.
|
|
const MAX_UNDO_STEPS = 12;
|
|
const actionHistory = [];
|
|
|
|
ActionBar.mount(state, {
|
|
save: hooks.save,
|
|
rerender: hooks.rerender,
|
|
resolvePot: openPotWizard,
|
|
|
|
beginAction: label => {
|
|
actionHistory.push({ label, snapshot: snapshotState() });
|
|
if (actionHistory.length > MAX_UNDO_STEPS) actionHistory.shift();
|
|
},
|
|
|
|
lastActionLabel: () => (actionHistory.length ? actionHistory[actionHistory.length - 1].label : null),
|
|
|
|
undoLast: () => {
|
|
const step = actionHistory.pop();
|
|
if (!step) return;
|
|
restoreState(step.snapshot);
|
|
Toast.show(`Took back ${step.label}.`, { duration: 2500 });
|
|
},
|
|
});
|
|
|
|
// Anything that ends the hand makes the per-action history meaningless — the
|
|
// snapshots refer to a hand that no longer exists.
|
|
const clearActionHistory = () => { actionHistory.length = 0; };
|
|
|
|
// Keyboard shortcuts press the very same buttons a dealer would tap, rather than
|
|
// calling the betting engine themselves — there's no second code path to drift out
|
|
// of step with what the bar is actually offering right now.
|
|
const pressInBar = selector => {
|
|
const btn = document.querySelector(`#actionBar ${selector}`);
|
|
if (!btn || btn.disabled) return false;
|
|
btn.click();
|
|
return true;
|
|
};
|
|
|
|
const quickRaiseButton = label =>
|
|
[...document.querySelectorAll("#actionBar .raiseQuickBtn")]
|
|
.find(b => b.querySelector(".actionBtnLabel").textContent === label);
|
|
|
|
Shortcuts.mount({
|
|
fold: () => pressInBar(".actionFold"),
|
|
checkOrCall: () => pressInBar(".actionCheck") || pressInBar(".actionCall"),
|
|
raise: () => pressInBar(".actionRaise"),
|
|
allIn: () => {
|
|
// Open the raise pad first if it isn't already, then take its All-in amount.
|
|
if (!document.querySelector("#actionBar .raisePad") && !pressInBar(".actionRaise")) return false;
|
|
const quick = quickRaiseButton("All-in");
|
|
if (!quick) return false;
|
|
quick.click();
|
|
return true;
|
|
},
|
|
// Enter confirms a raise if one is being entered, otherwise moves the hand on.
|
|
// Dealing is an advance too — Enter should carry a dealer through setup, deal,
|
|
// streets and showdown without reaching for the trackpad.
|
|
advance: () => pressInBar(".raiseConfirm") || pressInBar(".actionAdvance") || pressInBar(".actionDeal"),
|
|
undo: () => pressInBar(".actionUndo"),
|
|
cancel: () => pressInBar(".raiseCancel"),
|
|
});
|
|
|
|
// Moving to the next hand used to ask "are you sure?" every single time —
|
|
// the most-pressed button in a session, gated behind a system dialog. It now
|
|
// just happens, and the toast offers the way back for the few seconds anyone
|
|
// would notice a misclick. The snapshot is deliberately in-memory only: an
|
|
// undo is for the mistake you just made, not one from last night's session.
|
|
const runUndoable = (message, mutate) => {
|
|
const snapshot = snapshotState();
|
|
mutate();
|
|
// Starting or settling a hand invalidates the per-action history: those
|
|
// snapshots describe a hand that's over. The whole-step Undo below replaces it.
|
|
clearActionHistory();
|
|
hooks.save(state);
|
|
Render.renderAll();
|
|
|
|
Toast.show(message, {
|
|
actionLabel: "Undo",
|
|
onAction: () => {
|
|
restoreState(snapshot);
|
|
Toast.show("Undone.", { duration: 2500 });
|
|
},
|
|
});
|
|
};
|
|
|
|
// A seat that has anything of a player in it — a name, chips, cash in, or a hand
|
|
// in progress. Shrinking the table throws these away permanently, and it also
|
|
// orphans them in the session standings, where they keep their winnings but lose
|
|
// the stack and buy-in the settle-up is worked out from.
|
|
const seatHasSomeoneIn = player =>
|
|
!!(player.name && player.name.trim()) ||
|
|
player.inHand ||
|
|
(Number(player.stack) || 0) > 0 ||
|
|
(Number(player.buyIns) || 0) > 0 ||
|
|
(Number(player.amountOverride) || 0) > 0;
|
|
|
|
document.getElementById("seatCountInput").addEventListener("change", async e => {
|
|
const requested = Math.max(2, Math.min(24, Math.floor(Number(e.target.value)) || state.seatCount));
|
|
const losing = state.seats.slice(requested).filter(seatHasSomeoneIn);
|
|
|
|
if (losing.length > 0) {
|
|
const who = losing
|
|
.map((p, i) => HoldemState.displayName(p, requested + state.seats.slice(requested).indexOf(p)))
|
|
.join(", ");
|
|
const ok = await Dialog.confirm({
|
|
title: `Remove ${losing.length} ${losing.length === 1 ? "player" : "players"}?`,
|
|
message: `Going down to ${requested} seats takes ${who} off the table along with their chips and buy-ins, and their session totals stop adding up.`,
|
|
confirmLabel: "Remove them",
|
|
danger: true,
|
|
});
|
|
if (!ok) {
|
|
e.target.value = state.seatCount;
|
|
return;
|
|
}
|
|
}
|
|
|
|
runUndoable(`Table set to ${requested} seats.`, () => HoldemState.resizeSeats(state, requested));
|
|
});
|
|
|
|
document.getElementById("directionSelect").addEventListener("change", e => {
|
|
state.direction = e.target.value;
|
|
hooks.save(state);
|
|
Render.renderSeats();
|
|
});
|
|
|
|
document.getElementById("basePotInput").addEventListener("input", e => {
|
|
state.basePotAmount = e.target.value === "" ? null : Number(e.target.value);
|
|
hooks.save(state);
|
|
Render.renderAll();
|
|
});
|
|
|
|
document.getElementById("dedicatedDealerToggle").addEventListener("change", e => {
|
|
state.dedicatedDealer = e.target.checked;
|
|
hooks.save(state);
|
|
Render.renderSeats();
|
|
Render.renderTableHint();
|
|
});
|
|
|
|
document.getElementById("tableNotes").addEventListener("input", e => {
|
|
state.notes = e.target.value;
|
|
hooks.save(state);
|
|
});
|
|
|
|
document.getElementById("addChipColorBtn").addEventListener("click", () => {
|
|
state.chipLegend.push({
|
|
id: HoldemState.makeId("chip"),
|
|
color: "#888888",
|
|
value: 1,
|
|
});
|
|
hooks.save(state);
|
|
Render.renderAll();
|
|
});
|
|
|
|
document.getElementById("nextHandBtn").addEventListener("click", async () => {
|
|
// Moving on wipes every seat's contribution, so an unresolved pot is simply
|
|
// destroyed — measured at a four-handed table, pressing this with $8 in the
|
|
// middle took the table from 800 chips to 792, and the toast said nothing
|
|
// about it. Chips only ever leave this tool because a dealer meant them to.
|
|
const unresolved = HoldemState.unresolvedContributions(state);
|
|
|
|
if (unresolved > Betting.EPSILON_CHIPS) {
|
|
const ok = await Dialog.confirm({
|
|
title: `Throw away ${Money.format(unresolved)}?`,
|
|
message: "There's still money in the middle from this hand. Starting the next one takes it off the table without paying anybody — use Resolve Pot if somebody won it.",
|
|
confirmLabel: "Throw it away",
|
|
danger: true,
|
|
});
|
|
if (!ok) return;
|
|
}
|
|
|
|
// startNextHand moves the button on by one; say where it landed so the
|
|
// dealer can confirm the rotation without hunting for the badge.
|
|
const nextDealer = HoldemState.nextSeatIndex(state, state.dealerSeatIndex, 1);
|
|
const where = state.dedicatedDealer
|
|
? `small blind on seat ${HoldemState.nextSeatIndex(state, nextDealer, 1) + 1}`
|
|
: `button on seat ${nextDealer + 1}`;
|
|
const discarded = unresolved > Betting.EPSILON_CHIPS
|
|
? ` ${Money.format(unresolved)} taken off the table.`
|
|
: "";
|
|
|
|
runUndoable(`Next hand — ${where}. Seat the table, then deal.${discarded}`, () => {
|
|
HoldemState.startNextHand(state);
|
|
});
|
|
});
|
|
|
|
document.getElementById("resetTableBtn").addEventListener("click", async () => {
|
|
const ok = await Dialog.confirm({
|
|
title: "Reset the whole table?",
|
|
message: "Every seat, chip colour and note goes back to defaults. The hand history and session standings are cleared too.",
|
|
confirmLabel: "Reset table",
|
|
danger: true,
|
|
});
|
|
if (!ok) return;
|
|
|
|
HoldemState.clear();
|
|
Object.assign(state, HoldemState.createDefault());
|
|
clearActionHistory();
|
|
Render.mount(state, hooks);
|
|
Toast.show("Table reset.", { duration: 2500 });
|
|
});
|
|
|
|
// A pre-filled amount is a suggestion, so reaching for the box replaces it.
|
|
//
|
|
// Delegated from the document rather than wired per field: the raise pad, the
|
|
// add-chips sheet and the seat editor all build their inputs fresh every render,
|
|
// and a box added later should not have to remember to ask for this.
|
|
document.addEventListener("focusin", event => {
|
|
const field = event.target;
|
|
if (!Money.selectsOnFocus(field)) return;
|
|
if (!field.value) return;
|
|
// After the browser has finished placing its own caret, or it wins the race
|
|
// and the selection collapses again the moment the tap completes.
|
|
setTimeout(() => {
|
|
if (document.activeElement !== field) return;
|
|
try {
|
|
field.select();
|
|
} catch {
|
|
// A number input refuses setSelectionRange in some browsers; select()
|
|
// is the one that works on both, and if neither does the field is
|
|
// simply left as it was.
|
|
}
|
|
}, 0);
|
|
});
|
|
|
|
document.getElementById("resolvePotBtn").addEventListener("click", openPotWizard);
|
|
|
|
[
|
|
["smallBlindInput", "smallBlind"],
|
|
["bigBlindInput", "bigBlind"],
|
|
["defaultStackInput", "defaultStack"],
|
|
].forEach(([id, key]) => {
|
|
document.getElementById(id).addEventListener("input", e => {
|
|
state[key] = e.target.value === "" ? null : Number(e.target.value);
|
|
hooks.save(state);
|
|
Render.renderAll();
|
|
});
|
|
});
|
|
|
|
document.getElementById("closeSeatEditorBtn").addEventListener("click", () => Render.closeSeatEditor());
|
|
|
|
document.getElementById("addChipsConfirmBtn").addEventListener("click", () => AddChips.confirm());
|
|
document.getElementById("addChipsCancelBtn").addEventListener("click", () => AddChips.close());
|
|
document.getElementById("addChipsOverlay").addEventListener("click", e => {
|
|
if (e.target.id === "addChipsOverlay") AddChips.close();
|
|
});
|
|
document.getElementById("addChipsInput").addEventListener("keydown", e => {
|
|
if (e.key === "Enter") AddChips.confirm();
|
|
});
|
|
|
|
// Escape is Modal's job now, for this sheet and the five others that never had it.
|
|
const rulingsOverlay = document.getElementById("rulingsOverlay");
|
|
const closeRulings = () => { rulingsOverlay.hidden = true; };
|
|
// An index across the top of the rulings sheet, one chip per ruling.
|
|
//
|
|
// This is opened mid-argument with players waiting, and it was nine rulings of
|
|
// prose with no way in: reaching the last one meant scrolling 1.6 screens on a
|
|
// desktop, 2.1 on a phone and 3.5 on a phone held sideways. A dealer looking for
|
|
// the odd-chip rule had to read past seven others to find it.
|
|
//
|
|
// Built from the sections rather than written out beside them, so a ruling added
|
|
// later turns up in the index without anyone having to remember to add it twice.
|
|
const buildRulingsIndex = () => {
|
|
const index = document.getElementById("rulingsIndex");
|
|
if (!index || index.childElementCount > 0) return;
|
|
[...rulingsOverlay.querySelectorAll(".ruling")].forEach((section, i) => {
|
|
const heading = section.querySelector("h3");
|
|
if (!heading) return;
|
|
if (!section.id) section.id = `ruling${i + 1}`;
|
|
|
|
const jump = document.createElement("button");
|
|
jump.type = "button";
|
|
jump.className = "rulingsIndexBtn";
|
|
// A short label where the ruling gives one. The headings are sentences —
|
|
// "A short all-in doesn't reopen the betting" — and nine of those stacked
|
|
// one per row made the index 342px tall on a phone, which is most of the
|
|
// sheet before the first ruling. "Short all-in" says the same thing to
|
|
// somebody who is looking for it, and three fit on a line.
|
|
jump.textContent = section.dataset.short || heading.textContent;
|
|
// The full heading is still what it announces, so the short label never
|
|
// costs a screen reader the sentence.
|
|
if (section.dataset.short) jump.setAttribute("aria-label", heading.textContent);
|
|
jump.addEventListener("click", () => {
|
|
section.scrollIntoView({ block: "start", behavior: "smooth" });
|
|
// The heading is what the dealer is now reading, so that is where the
|
|
// keyboard and a screen reader should be too.
|
|
heading.setAttribute("tabindex", "-1");
|
|
heading.focus({ preventScroll: true });
|
|
});
|
|
index.appendChild(jump);
|
|
});
|
|
};
|
|
|
|
document.getElementById("openRulingsBtn").addEventListener("click", () => {
|
|
buildRulingsIndex();
|
|
rulingsOverlay.hidden = false;
|
|
});
|
|
document.getElementById("closeRulingsBtn").addEventListener("click", closeRulings);
|
|
rulingsOverlay.addEventListener("click", e => {
|
|
if (e.target === rulingsOverlay) closeRulings();
|
|
});
|
|
|
|
document.getElementById("seatMenuCancelBtn").addEventListener("click", () => SeatMenu.close());
|
|
document.getElementById("seatMenuOverlay").addEventListener("click", e => {
|
|
if (e.target.id === "seatMenuOverlay") SeatMenu.close();
|
|
});
|
|
|
|
document.getElementById("cardPickerCloseBtn").addEventListener("click", () => CardPicker.close());
|
|
document.getElementById("cardPickerClearBtn").addEventListener("click", () => CardPicker.clearSlot());
|
|
document.getElementById("cardPickerOverlay").addEventListener("click", e => {
|
|
if (e.target.id === "cardPickerOverlay") CardPicker.close();
|
|
});
|
|
|
|
document.getElementById("closePotWizardBtn").addEventListener("click", () => PotWizard.close());
|
|
document.getElementById("potWizardOverlay").addEventListener("click", e => {
|
|
if (e.target.id === "potWizardOverlay") PotWizard.close();
|
|
});
|
|
|
|
document.getElementById("clearHistoryBtn").addEventListener("click", async () => {
|
|
const ok = await Dialog.confirm({
|
|
title: "Clear the hand history?",
|
|
message: "This wipes every logged hand and the session standings, and puts the hand counter back to #1. Export first if anyone might want the numbers.",
|
|
confirmLabel: "Clear history",
|
|
danger: true,
|
|
});
|
|
if (!ok) return;
|
|
|
|
HoldemState.clearHistory(state);
|
|
hooks.save(state);
|
|
Render.renderAll();
|
|
Toast.show("Hand history cleared.", { duration: 2500 });
|
|
});
|
|
|
|
document.getElementById("exportHistoryBtn").addEventListener("click", () => {
|
|
if ((state.handHistory || []).length === 0) return;
|
|
HandHistoryExport.download(state.handHistory, HoldemState.sessionStandings(state));
|
|
});
|
|
});
|