webtools/site/finska/scripts/controllers.js
exe.dev user 3953adedd3
All checks were successful
/ publish (push) Successful in 1m40s
Allowed renaming of teams in Finska score card
2026-04-04 03:47:57 +00:00

254 lines
7.3 KiB
JavaScript

import { Controller } from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js";
import { Scorecard, getStoreDAO } from "./models.js";
const storeDao = getStoreDAO();
export class FinskaScorecardController extends Controller {
static targets = ["score1Input", "score2Input", "scoreTable", "team1Header", "team2Header"];
static values = {
maxScore: Number,
overflowScoreTo: Number
}
connect() {
let rules = {
maxScore: Math.floor(this.maxScoreValue),
overflowScoreTo: Math.floor(this.overflowScoreToValue)
};
this._undoStack = [];
this._scorecard = storeDao.loadOrCreate(rules);
this._loadTeamNames();
this.updateTable();
}
_loadTeamNames() {
const stored1 = localStorage.getItem('finska-team1-name');
const stored2 = localStorage.getItem('finska-team2-name');
this._team1Name = stored1 || 'Team A';
this._team2Name = stored2 || 'Team B';
this._renderTeamHeader(this.team1HeaderTarget, this._team1Name, 'editTeam1');
this._renderTeamHeader(this.team2HeaderTarget, this._team2Name, 'editTeam2');
}
_renderTeamHeader(td, name, editAction) {
td.innerHTML = `<span class="team-header">
<span class="team-name">${this._escapeHtml(name)}</span>
<button class="edit-btn" data-action="finska-scorecard#${editAction}" aria-label="Edit ${this._escapeHtml(name)} name">&#9998;</button>
</span>`;
}
_renderEditHeader(td, currentName, teamNum) {
td.innerHTML = `<span class="team-header-edit">
<input type="text" value="${this._escapeAttr(currentName)}" data-action="keydown->finska-scorecard#editKeyDown${teamNum}">
<button class="confirm-btn" data-action="finska-scorecard#confirmTeam${teamNum}" aria-label="Confirm">&#10003;</button>
<button class="cancel-btn" data-action="finska-scorecard#cancelTeam${teamNum}" aria-label="Cancel">&#10007;</button>
</span>`;
td.querySelector('input').focus();
td.querySelector('input').select();
}
editTeam1() {
this._renderEditHeader(this.team1HeaderTarget, this._team1Name, '1');
}
editTeam2() {
this._renderEditHeader(this.team2HeaderTarget, this._team2Name, '2');
}
confirmTeam1() {
const input = this.team1HeaderTarget.querySelector('input');
const newName = input.value.trim() || 'Team A';
this._team1Name = newName;
localStorage.setItem('finska-team1-name', newName);
this._renderTeamHeader(this.team1HeaderTarget, this._team1Name, 'editTeam1');
}
confirmTeam2() {
const input = this.team2HeaderTarget.querySelector('input');
const newName = input.value.trim() || 'Team B';
this._team2Name = newName;
localStorage.setItem('finska-team2-name', newName);
this._renderTeamHeader(this.team2HeaderTarget, this._team2Name, 'editTeam2');
}
cancelTeam1() {
this._renderTeamHeader(this.team1HeaderTarget, this._team1Name, 'editTeam1');
}
cancelTeam2() {
this._renderTeamHeader(this.team2HeaderTarget, this._team2Name, 'editTeam2');
}
editKeyDown1(e) {
this._editKeyDown(e, this.confirmTeam1.bind(this), this.cancelTeam1.bind(this));
}
editKeyDown2(e) {
this._editKeyDown(e, this.confirmTeam2.bind(this), this.cancelTeam2.bind(this));
}
_editKeyDown(e, confirmFn, cancelFn) {
if (e.key === 'Enter') {
e.preventDefault();
confirmFn();
} else if (e.key === 'Escape') {
e.preventDefault();
cancelFn();
}
}
_escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
_escapeAttr(str) {
return str.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
updateTable() {
let tableBody = this.scoreTableTarget;
let tableRows = tableBody.querySelectorAll("tr");
let pairs = this._scorecard.pairs();
for (let pairIndex = 0; pairIndex < pairs.length; pairIndex++) {
let tableRow;
if (pairIndex >= tableRows.length) {
tableRow = this._appendRow();
} else {
tableRow = tableRows[pairIndex];
}
this._updateRow(tableRow, pairs[pairIndex])
}
// Remove any extra rows
for (let i = pairs.length; i < tableRows.length; i++) {
tableBody.removeChild(tableRows[i]);
}
console.log(JSON.stringify(this._scorecard.toJson()));
}
_updateRow(tableRow, pair) {
let tds = tableRow.querySelectorAll("td");
this._updateCell(pair.p1, tds[0], tds[1]);
this._updateCell(pair.p2, tds[2], tds[3]);
}
_updateCell(score, scoreCell, totalCell) {
scoreCell.classList.value = "";
totalCell.classList.value = "";
if (score != null) {
scoreCell.textContent = score.score;
totalCell.textContent = score.total;
if (score.score === 0) {
scoreCell.classList.add("score-foul");
totalCell.classList.add("score-foul");
} else if (score.wasOverflow) {
scoreCell.classList.add("score-overflow");
totalCell.classList.add("score-overflow");
} else if (score.wasWin) {
scoreCell.classList.add("score-win");
totalCell.classList.add("score-win");
}
} else {
scoreCell.textContent = "";
totalCell.textContent = "";
}
}
_appendRow() {
let newRow = document.createElement("tr");
newRow.classList.add("score-entry");
for (let i = 0; i < 4; i++) {
newRow.appendChild(document.createElement("td"));
}
this.scoreTableTarget.appendChild(newRow);
return newRow;
}
addScore1() {
this._addScore(this.score1InputTarget, this.score2InputTarget,
this._scorecard.addPlayer1Score.bind(this._scorecard),
this._scorecard.removeLastPlayer1Score.bind(this._scorecard));
}
addScore2() {
this._addScore(this.score2InputTarget, this.score1InputTarget,
this._scorecard.addPlayer2Score.bind(this._scorecard),
this._scorecard.removeLastPlayer2Score.bind(this._scorecard));
}
_addScore(inputElem, focusToInputElem, addScoreFn, queueUndoFn) {
let score = parseInt(inputElem.value);
if (isNaN(score)) {
score = 0;
}
addScoreFn(score);
this._undoStack.push(queueUndoFn);
inputElem.value = "";
storeDao.save(this._scorecard);
this.updateTable();
focusToInputElem.focus();
}
score1KeyDown(e) {
this._handleKeyDown(e, this.addScore1.bind(this));
}
score2KeyDown(e) {
this._handleKeyDown(e, this.addScore2.bind(this));
}
_handleKeyDown(e, addScoreFn) {
if (e.key === "Enter") {
e.preventDefault();
addScoreFn();
}
}
undoLast() {
if (this._undoStack.length === 0) {
return;
}
if (!confirm("Really undo last move?")) {
return;
}
(this._undoStack.pop())();
storeDao.save(this._scorecard);
this.updateTable();
}
resetAll() {
if (!confirm("Really reset?")) {
return;
}
this._scorecard.reset();
storeDao.clear();
this._undoStack = [];
this._team1Name = 'Team A';
this._team2Name = 'Team B';
localStorage.removeItem('finska-team1-name');
localStorage.removeItem('finska-team2-name');
this._renderTeamHeader(this.team1HeaderTarget, this._team1Name, 'editTeam1');
this._renderTeamHeader(this.team2HeaderTarget, this._team2Name, 'editTeam2');
this.updateTable();
}
}