-
-
-
-
+
+
+
+
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
Statistics
-
-
-
-
-
-
-
-
-
High scores
-
-
-
-
-
+
+
+
+
+
+
+
-
-
+
diff --git a/src/main/js/Common.ts b/src/main/js/Common.ts
index fa54791..15887c6 100644
--- a/src/main/js/Common.ts
+++ b/src/main/js/Common.ts
@@ -119,7 +119,7 @@ export function waitForForkAwesome(onSuccess: () => void, onFailure: () => void,
ctx.textBaseline = "middle";
const startTime = performance.now();
- const failTime = timeout === null ? null : startTime + timeout;
+ const failTime = timeout == null ? null : startTime + timeout;
requestAnimationFrame(fontOnload);
/**
@@ -129,7 +129,7 @@ export function waitForForkAwesome(onSuccess: () => void, onFailure: () => void,
*/
function fontOnload(time: number): void {
const currentCount = getPixelCount();
- if (failTime !== null && time > failTime) onFailure();
+ if (failTime != null && time > failTime) onFailure();
else if (currentCount < targetPixelCount) requestAnimationFrame(fontOnload);
else onSuccess();
}
diff --git a/src/main/js/Display.ts b/src/main/js/Display.ts
index 95e325f..0b730c6 100644
--- a/src/main/js/Display.ts
+++ b/src/main/js/Display.ts
@@ -156,7 +156,7 @@ export class Display {
* @private
*/
private getFieldOffset(): { x: number, y: number } {
- if (this.field === null) return {x: 0, y: 0};
+ if (this.field == null) return {x: 0, y: 0};
if (this.field.width >= this.minSquareWidth) return {x: 0, y: 0};
return {x: Math.floor((this.minSquareWidth - this.field.width) * this.scale / 2), y: 0};
}
@@ -169,7 +169,7 @@ export class Display {
setField(field: Field | null): void {
this.hintSquare = null;
this.field = field;
- if (this.field === null) return;
+ if (this.field == null) return;
this.canvas.width = Math.max(this.minSquareWidth, this.field.width) * this.scale;
this.canvas.height = this.field.height * this.scale + this.scale;
@@ -213,7 +213,7 @@ export class Display {
const {x, y} = this.getFieldOffset();
this.clearCanvas(ctx);
- if (this.field === null) return;
+ if (this.field == null) return;
ctx.save();
ctx.translate(x, y);
@@ -247,7 +247,7 @@ export class Display {
* @private
*/
private drawGrid(ctx: CanvasRenderingContext2D): void {
- if (this.field === null) return;
+ if (this.field == null) return;
ctx.save();
ctx.beginPath();
@@ -271,7 +271,7 @@ export class Display {
* @private
*/
private drawCovers(ctx: CanvasRenderingContext2D): void {
- if (this.field === null) return;
+ if (this.field == null) return;
ctx.save();
ctx.fillStyle = "#555";
@@ -279,7 +279,7 @@ export class Display {
.filter(it => it.isCovered)
.filter(it => {
// True if square should be covered
- if (this.field!.hasLost || this.field!.hasWon || this.mouseSquare === null)
+ if (this.field!.hasLost || this.field!.hasWon || this.mouseSquare == null)
return true;
if (this.mouseHoldUncover && this.mouseSquare === it)
return it.hasFlag || it.hasMark;
@@ -299,7 +299,7 @@ export class Display {
* @private
*/
private drawHints(ctx: CanvasRenderingContext2D): void {
- if (this.field === null) return;
+ if (this.field == null) return;
if (this.preferences.showTooManyFlagsHints) {
ctx.save();
@@ -311,7 +311,7 @@ export class Display {
ctx.restore();
}
- if (this.hintSquare !== null) {
+ if (this.hintSquare != null) {
ctx.save();
ctx.fillStyle = "rgba(0, 255, 0, 0.3)";
ctx.fillRect(this.hintSquare.x * this.scale, this.hintSquare.y * this.scale, this.scale, this.scale);
@@ -326,11 +326,11 @@ export class Display {
* @private
*/
private drawSymbols(ctx: CanvasRenderingContext2D): void {
- if (this.field === null) return;
+ if (this.field == null) return;
ctx.save();
this.field.squareList.forEach(square => {
- if (this.field === null) return;
+ if (this.field == null) return;
let icon;
if (square.hasFlag)
@@ -354,7 +354,7 @@ export class Display {
* @private
*/
private drawStatusBar(ctx: CanvasRenderingContext2D): void {
- if (this.field === null) return;
+ if (this.field == null) return;
ctx.save();
ctx.fillStyle = "#000";
@@ -378,7 +378,7 @@ export class Display {
// Deaths
let deathsSymbol;
if (this.field.hasLost) {
- if (this.loseTime === null)
+ if (this.loseTime == null)
this.loseTime = Date.now();
deathsSymbol = Math.floor((Date.now() - this.loseTime) / 1000) % 2 === 0
@@ -422,10 +422,10 @@ export class Display {
* @private
*/
private drawWinConfetti(): void {
- if (this.field === null) return;
+ if (this.field == null) return;
const rect = this.canvas.getBoundingClientRect();
- if (this.field.hasWon && this.winTime === null) {
+ if (this.field.hasWon && this.winTime == null) {
confetti({
origin: {
x: (rect.left + rect.width / 2) / document.documentElement.clientWidth,
diff --git a/src/main/js/Field.ts b/src/main/js/Field.ts
index 5d5fcf3..8c1b0ab 100644
--- a/src/main/js/Field.ts
+++ b/src/main/js/Field.ts
@@ -1,3 +1,5 @@
+const {MemoryStorage} = (window as any).fwdekker.storage;
+
// @ts-ignore
import alea from "alea";
import {Action, ActionHistory} from "./Action";
@@ -7,8 +9,6 @@ import {HighScores} from "./HighScores";
import {Solver} from "./Solver";
import {Statistics} from "./Statistics";
import {Timer} from "./Timer";
-// @ts-ignore
-const {MemoryStorage} = window.fwdekker.storage;
/**
@@ -127,13 +127,15 @@ export class Field {
/**
- * Returns the square at the given coordinates, or `orElse` if there is no square there.
+ * Returns the square at the given coordinates, `orElse` if there is no square there, or `undefined` if there is no
+ * square there and `orElse` is not given.
*
* @param coords the coordinates of the square to look up
* @param orElse the value to return if there is no square at the given coordinates
- * @returns the square at the given coordinates, or `orElse` if there is no square there
+ * @returns the square at the given coordinates, `orElse` if there is no square there, or `undefined` if there is no
+ * square there and `orElse` is not given
*/
- getSquareOrElse(coords: { x: number, y: number }, orElse: any = null): Square | any {
+ getSquareOrElse
(coords: { x: number, y: number }, orElse?: T): Square | T {
return this.squares[coords.y]?.[coords.x] ?? orElse;
}
@@ -523,15 +525,15 @@ export class Square {
get neighbors(): Square[] {
if (this._neighbors === undefined) {
this._neighbors = [
- this.field.getSquareOrElse({x: this.x - 1, y: this.y - 1}),
- this.field.getSquareOrElse({x: this.x, y: this.y - 1}),
- this.field.getSquareOrElse({x: this.x + 1, y: this.y - 1}),
- this.field.getSquareOrElse({x: this.x - 1, y: this.y}),
- this.field.getSquareOrElse({x: this.x + 1, y: this.y}),
- this.field.getSquareOrElse({x: this.x - 1, y: this.y + 1}),
- this.field.getSquareOrElse({x: this.x, y: this.y + 1}),
- this.field.getSquareOrElse({x: this.x + 1, y: this.y + 1}),
- ].filter(it => it !== null);
+ this.field.getSquareOrElse({x: this.x - 1, y: this.y - 1}, null),
+ this.field.getSquareOrElse({x: this.x, y: this.y - 1}, null),
+ this.field.getSquareOrElse({x: this.x + 1, y: this.y - 1}, null),
+ this.field.getSquareOrElse({x: this.x - 1, y: this.y}, null),
+ this.field.getSquareOrElse({x: this.x + 1, y: this.y}, null),
+ this.field.getSquareOrElse({x: this.x - 1, y: this.y + 1}, null),
+ this.field.getSquareOrElse({x: this.x, y: this.y + 1}, null),
+ this.field.getSquareOrElse({x: this.x + 1, y: this.y + 1}, null),
+ ].filter((it): it is Square => it !== null);
}
return this._neighbors!;
diff --git a/src/main/js/Game.ts b/src/main/js/Game.ts
index 8bdf827..4e872a3 100644
--- a/src/main/js/Game.ts
+++ b/src/main/js/Game.ts
@@ -1,16 +1,16 @@
-// @ts-ignore
-const {$} = window.fwdekker;
+const {$, stringToHtml} = (window as any).fwdekker;
+
// @ts-ignore
import alea from "alea";
-import {blurActiveElement, stringToHash} from "./Common";
+import {stringToHash} from "./Common";
import {customDifficulty, defaultDifficulty, difficulties} from "./Difficulty";
import {Display} from "./Display";
import {Field} from "./Field";
import {HighScores} from "./HighScores";
+import {ModalDialog} from "./ModalDialog";
import {Preferences} from "./Preferences";
import {Solver} from "./Solver";
import {Statistics} from "./Statistics";
-import {Overlay} from "./UI";
/**
@@ -22,33 +22,13 @@ export class Game {
private statisticsTimer: number | undefined;
private readonly canvas: HTMLCanvasElement;
- private readonly difficultySelect: HTMLSelectElement;
- private readonly newGameForm: HTMLFormElement;
- private readonly restartForm: HTMLFormElement;
- private readonly seedOverlay: Overlay;
- private readonly seedOpenForm: HTMLFormElement;
- private readonly seedInput: HTMLFormElement;
- private readonly undoForm: HTMLFormElement;
- private readonly redoForm: HTMLFormElement;
- private readonly hintForm: HTMLFormElement;
- private readonly solveForm: HTMLFormElement;
- private readonly customDifficultyOverlay: Overlay;
+ private readonly customDifficultyOverlay: ModalDialog;
private readonly widthInput: HTMLInputElement;
private readonly heightInput: HTMLInputElement;
private readonly minesInput: HTMLInputElement;
private readonly solvableInput: HTMLInputElement;
- private readonly preferencesOverlay: Overlay;
- private readonly enableMarksInput: HTMLInputElement;
- private readonly showTooManyFlagsHintsInput: HTMLInputElement;
- private readonly preferencesOpenForm: HTMLFormElement;
- private readonly statisticsOverlay: Overlay;
private readonly statisticsDiv: HTMLDivElement;
- private readonly statisticsResetForm: HTMLFormElement;
- private readonly statisticsOpenForm: HTMLFormElement;
- private readonly highScoresOverlay: Overlay;
private readonly highScoresDiv: HTMLDivElement;
- private readonly highScoresResetForm: HTMLFormElement;
- private readonly highScoresOpenForm: HTMLFormElement;
private readonly rng: any;
private seed: string;
@@ -71,7 +51,7 @@ export class Game {
this.field = null; // Placeholder until `initNewField`
this.display = new Display(this.canvas, this.field, preferences);
this.display.startDrawLoop();
- this.canvas.classList.remove("invisible");
+ this.canvas.classList.remove("hidden");
this.rng = alea("" + Date.now());
this.seed = "" + this.rng.uint32();
@@ -83,44 +63,40 @@ export class Game {
// Settings
- this.difficultySelect = $("#difficulty");
+ const difficultySelect = $("#difficulty");
difficulties.forEach(it => {
- const option = document.createElement("option");
- option.value = it.name;
- option.innerHTML = `${it.name}${it.description !== null ? ` (${it.description})` : ""}`;
- this.difficultySelect.add(option);
+ const description = `${it.name}${it.description != null ? ` (${it.description})` : ""}`;
+ difficultySelect.appendChild(stringToHtml(``));
});
- this.difficultySelect.addEventListener(
+ difficultySelect.addEventListener("click", (event: MouseEvent) => event.stopPropagation());
+ difficultySelect.addEventListener(
"change",
- event => {
- event.preventDefault();
-
- const difficulty = difficulties[this.difficultySelect.selectedIndex - 1];
- this.difficultySelect.selectedIndex = 0;
+ () => {
+ const difficulty = difficulties[difficultySelect.selectedIndex - 1];
+ difficultySelect.selectedIndex = 0;
if (difficulty === undefined) return;
- if (difficulty.name !== customDifficulty.name) {
- this.difficultySelect.selectedIndex = 0;
+ if (difficulty.name === customDifficulty.name)
+ this.customDifficultyOverlay.open();
+ else
this.initNewField(difficulty.width, difficulty.height, difficulty.mineCount, difficulty.solvable);
- return;
- }
+ }
+ );
- this.customDifficultyOverlay.show();
+ // Custom difficulty
+ this.customDifficultyOverlay = new ModalDialog({
+ dialog: $("#custom-difficulty-dialog"),
+ onOpen: () => {
this.widthInput.value = "" + (this.field?.width ?? defaultDifficulty.width);
this.heightInput.value = "" + (this.field?.height ?? defaultDifficulty.height);
this.minesInput.value = "" + (this.field?.mineCount ?? defaultDifficulty.mineCount);
this.solvableInput.checked = this.field?.isSolvable ?? defaultDifficulty.solvable;
this.setMineLimit();
- this.widthInput.focus();
- }
- );
-
- // Custom difficulty
- this.customDifficultyOverlay = new Overlay(
- $("#customDifficultyOverlay"),
- $("#customDifficultyForm"),
- $("#customDifficultyCancelForm"),
- () => {
+ },
+ form: $("#custom-difficulty-form"),
+ closeButton: $("#custom-difficulty-cancel"),
+ submitButton: $("#custom-difficulty-submit"),
+ onSubmit: () => {
this.initNewField(
+this.widthInput.value,
+this.heightInput.value,
@@ -128,218 +104,135 @@ export class Game {
this.solvableInput.checked
);
}
- );
+ });
- this.widthInput = $("#settingsWidth");
+ this.widthInput = $("#settings-width");
this.widthInput.addEventListener("change", _ => this.setMineLimit());
- this.heightInput = $("#settingsHeight");
+ this.heightInput = $("#settings-height");
this.heightInput.addEventListener("change", _ => this.setMineLimit());
- this.minesInput = $("#settingsMines");
- this.solvableInput = $("#settingsSolvable");
+ this.minesInput = $("#settings-mines");
+ this.solvableInput = $("#settings-solvable");
// New game form
- this.newGameForm = $("#newGameForm");
- this.newGameForm.addEventListener(
- "submit",
- event => {
- event.preventDefault();
-
- this.initNewField(this.field?.width, this.field?.height, this.field?.mineCount, this.field?.isSolvable);
- blurActiveElement();
- }
+ $("#new-game").addEventListener(
+ "click",
+ () =>
+ this.initNewField(this.field?.width, this.field?.height, this.field?.mineCount, this.field?.isSolvable)
);
// Restart
- this.restartForm = $("#restartForm");
- this.restartForm.addEventListener(
- "submit",
- event => {
- event.preventDefault();
-
- this.field?.undo(); // Undoes all
- blurActiveElement();
- }
- );
+ $("#restart").addEventListener("click", () => this.field?.undo()); // Undoes all
// Seed
- this.seedInput = $("#seed");
- this.seedOpenForm = $("#seedOpenForm");
- this.seedOpenForm.addEventListener(
- "submit",
- event => {
- event.preventDefault();
-
- this.seedOverlay.show();
- this.seedInput.value = this.seed;
- this.seedInput.focus();
- setTimeout(() => this.seedInput.select(), 0);
- }
- );
- this.seedOverlay = new Overlay(
- $("#seedOverlay"),
- $("#seedForm"),
- $("#seedCancelForm"),
- () => {
+ const seedInput = $("#seed");
+ const seedDialog = new ModalDialog({
+ dialog: $("#seed-dialog"),
+ openButton: $("#seed-open"),
+ onOpen: () => seedInput.value = this.seed,
+ form: $("#seed-form"),
+ closeButton: $("#seed-cancel"),
+ submitButton: $("#seed-submit"),
+ onSubmit: () => {
this.initNewField(
this.field?.width,
this.field?.height,
this.field?.mineCount,
this.field?.isSolvable,
- this.seedInput.value
+ seedInput.value
);
+ seedDialog.close();
}
- );
+ });
// Undo
- this.undoForm = $("#undoForm");
- this.undoForm.addEventListener(
- "submit",
- event => {
- event.preventDefault();
-
- this.field?.undo(1);
- blurActiveElement();
- }
- );
+ $("#undo").addEventListener("click", () => this.field?.undo(1));
// Redo
- this.redoForm = $("#redoForm");
- this.redoForm.addEventListener(
- "submit",
- event => {
- event.preventDefault();
-
- this.field?.redo(1);
- blurActiveElement();
- }
- );
+ $("#redo").addEventListener("click", () => this.field?.redo(1));
// Hint
- this.hintForm = $("#hintForm");
- this.hintForm.addEventListener(
- "submit",
- event => {
- event.preventDefault();
-
- if (this.field !== null) {
+ $("#hint").addEventListener(
+ "click",
+ () => {
+ if (this.field != null) {
this.statistics.hintsRequested++;
this.display.hintSquare = Solver.getHint(this.field);
}
- blurActiveElement();
}
);
// Solve
- this.solveForm = $("#solveForm");
- this.solveForm.addEventListener(
- "submit",
- event => {
- event.preventDefault();
-
- if (this.field !== null) {
+ $("#solve").addEventListener(
+ "click",
+ () => {
+ if (this.field != null) {
this.statistics.solverUsages++;
Solver.solve(this.field);
}
- blurActiveElement();
}
);
// Preferences
- this.enableMarksInput = $("#preferencesEnableMarks");
- this.showTooManyFlagsHintsInput = $("#preferencesShowTooManyFlagsHints");
- this.preferencesOpenForm = $("#preferencesOpenForm");
- this.preferencesOpenForm.addEventListener(
- "submit",
- event => {
- event.preventDefault();
+ const enableMarksInput = $("#preferences-enable-marks");
+ const showTooManyFlagsHintsInput = $("#preferences-show-too-many-flags-hints");
+ const preferencesDialog = new ModalDialog({
+ dialog: $("#preferences-dialog"),
+ openButton: $("#preferences-open"),
+ onOpen: () => {
+ enableMarksInput.checked = preferences.marksEnabled;
+ showTooManyFlagsHintsInput.checked = preferences.showTooManyFlagsHints;
+ },
+ form: $("#preferences-form"),
+ closeButton: $("#preferences-cancel"),
+ submitButton: $("#preferences-submit"),
+ onSubmit: () => {
+ preferences.marksEnabled = enableMarksInput.checked;
+ preferences.showTooManyFlagsHints = showTooManyFlagsHintsInput.checked;
- this.enableMarksInput.checked = preferences.marksEnabled;
- this.showTooManyFlagsHintsInput.checked = preferences.showTooManyFlagsHints;
- this.preferencesOverlay.show();
- blurActiveElement();
+ preferencesDialog.close();
}
- );
- this.preferencesOverlay = new Overlay(
- $("#preferencesOverlay"),
- $("#preferencesForm"),
- $("#preferencesCancelForm"),
- () => {
- preferences.marksEnabled = this.enableMarksInput.checked;
- preferences.showTooManyFlagsHints = this.showTooManyFlagsHintsInput.checked;
- }
- );
+ });
// Statistics
- this.statisticsDiv = $("#statisticsDiv");
- this.statisticsOpenForm = $("#statisticsOpenForm");
- this.statisticsOpenForm.addEventListener(
- "submit",
- event => {
- event.preventDefault();
-
- this.statisticsOverlay.show();
- blurActiveElement();
- }
- );
- this.statisticsOverlay = new Overlay(
- $("#statisticsOverlay"),
- null,
- $("#statisticsCloseForm")
- );
- this.statisticsResetForm = $("#statisticsResetForm");
- this.statisticsResetForm.addEventListener(
- "submit",
- event => {
- event.preventDefault();
-
+ this.statisticsDiv = $("#statistics-div");
+ new ModalDialog({
+ dialog: $("#statistics-dialog"),
+ openButton: $("#statistics-open"),
+ onOpen: () => this.updateStatistics(),
+ closeButton: $("#statistics-close"),
+ submitButton: $("#statistics-reset"),
+ onSubmit: () => {
if (!window.confirm("Are you sure you want to reset all statistics? This cannot be undone."))
return;
this.statistics.clear();
+ this.updateStatistics();
}
- );
- this.updateStatistics();
+ });
// High scores
- this.highScoresDiv = $("#highScoresDiv");
- this.highScoresOpenForm = $("#highScoresOpenForm");
- this.highScoresOpenForm.addEventListener(
- "submit",
- event => {
- event.preventDefault();
-
- this.highScoresDiv.innerHTML = this.highScores.generateHtmlReport();
- this.highScoresOverlay.show();
- blurActiveElement();
- }
- );
- this.highScoresOverlay = new Overlay(
- $("#highScoresOverlay"),
- null,
- $("#highScoresCloseForm")
- );
- this.highScoresResetForm = $("#highScoresResetForm");
- this.highScoresResetForm.addEventListener(
- "submit",
- event => {
- event.preventDefault();
-
+ this.highScoresDiv = $("#high-scores-div");
+ new ModalDialog({
+ dialog: $("#high-scores-dialog"),
+ openButton: $("#high-scores-open"),
+ onOpen: () => this.highScoresDiv.innerHTML = this.highScores.generateHtmlReport(),
+ closeButton: $("#high-scores-close"),
+ submitButton: $("#high-scores-reset"),
+ onSubmit: () => {
if (!window.confirm("Are you sure you want to reset all high scores? This cannot be undone."))
return;
this.highScores.clear();
this.highScoresDiv.innerHTML = this.highScores.generateHtmlReport();
}
- );
+ });
// Canvas
this.canvas.addEventListener(
"mousemove",
event => {
- this.display.mouseSquare = this.field?.getSquareOrElse(
- this.display.posToSquare({x: event.clientX, y: event.clientY}),
- null
- );
+ const squarePos = this.display.posToSquare({x: event.clientX, y: event.clientY});
+ this.display.mouseSquare = this.field?.getSquareOrElse(squarePos, null) ?? null;
}
);
this.canvas.addEventListener(
@@ -357,11 +250,11 @@ export class Game {
"mousedown",
event => {
event.preventDefault();
- if (this.field === null) return;
+ if (this.field == null) return;
this.field.runUndoably(() => {
const coords = this.display.posToSquare({x: event.clientX, y: event.clientY});
- if (this.field === null || !this.field.hasSquareAt(coords)) return;
+ if (this.field == null || !this.field.hasSquareAt(coords)) return;
switch (event.button) {
case 0:
@@ -370,7 +263,7 @@ export class Game {
case 2:
if (!this.leftDown) {
const square = this.field.getSquareOrElse(coords);
- if (square !== null) {
+ if (square != null) {
if (square.hasFlag) {
this.field.toggleFlag(coords);
if (preferences.marksEnabled)
@@ -397,11 +290,11 @@ export class Game {
"mouseup",
event => {
event.preventDefault();
- if (this.field === null) return;
+ if (this.field == null) return;
this.field.runUndoably(() => {
const coords = this.display.posToSquare({x: event.clientX, y: event.clientY});
- if (this.field === null || !this.field.hasSquareAt(coords)) return;
+ if (this.field == null || !this.field.hasSquareAt(coords)) return;
switch (event.button) {
case 0:
@@ -459,7 +352,7 @@ export class Game {
* @param height the height of the field
* @param mineCount the number of mines to place in the field
* @param solvable whether the field is guaranteed to be solvable
- * @param seed the seed to generate the field width, or `null` if a new field should be chosen
+ * @param seed the seed to generate the field width, or `undefined` to use a random seed
* @private
*/
private initNewField(
@@ -467,7 +360,7 @@ export class Game {
height: number = defaultDifficulty.height,
mineCount: number = defaultDifficulty.mineCount,
solvable: boolean = defaultDifficulty.solvable,
- seed: string | null = null
+ seed?: string
) {
this.seed = seed ?? "" + this.rng.uint32();
this.field = new Field(
@@ -482,13 +375,11 @@ export class Game {
let lastTime: number | null = null;
window.clearInterval(this.statisticsTimer);
this.statisticsTimer = window.setInterval(() => {
- if (this.field === null) return;
+ if (this.field == null) return;
const elapsedTime = this.field.elapsedTime;
this.statistics.timeSpent += elapsedTime - (lastTime ?? 0);
lastTime = elapsedTime;
-
- this.updateStatistics();
}, 1000);
}
}
diff --git a/src/main/js/HighScores.ts b/src/main/js/HighScores.ts
index eb30f13..f7022e3 100644
--- a/src/main/js/HighScores.ts
+++ b/src/main/js/HighScores.ts
@@ -1,7 +1,7 @@
+const {LocalStorage} = (window as any).fwdekker.storage;
+
import {formatTime} from "./Common";
import {difficulties, Difficulty} from "./Difficulty";
-// @ts-ignore
-const {Storage, LocalStorage} = window.fwdekker.storage;
/**
diff --git a/src/main/js/Main.ts b/src/main/js/Main.ts
index e1d741c..d169b4c 100644
--- a/src/main/js/Main.ts
+++ b/src/main/js/Main.ts
@@ -1,5 +1,5 @@
-// @ts-ignore
-const {$, doAfterLoad, footer, header, nav} = window.fwdekker;
+const {doAfterLoad} = (window as any).fwdekker;
+
import {waitForForkAwesome} from "./Common";
import {BasicIconFont, ForkAwesomeFont} from "./Display";
import {Game} from "./Game";
@@ -7,17 +7,6 @@ import {Preferences} from "./Preferences";
doAfterLoad(() => {
- // Initialize template
- $("#nav").appendChild(nav("/Tools/Minesweeper/"));
- $("#header").appendChild(header({title: "Minesweeper"}));
- $("#footer").appendChild(footer({
- vcsURL: "https://git.fwdekker.com/tools/minesweeper/",
- version: "v%%VERSION_NUMBER%%"
- }));
- $("main").classList.remove("hidden");
-
-
- // Start game
const preferences = new Preferences();
waitForForkAwesome(
() => {
diff --git a/src/main/js/ModalDialog.ts b/src/main/js/ModalDialog.ts
new file mode 100644
index 0000000..fdce313
--- /dev/null
+++ b/src/main/js/ModalDialog.ts
@@ -0,0 +1,128 @@
+const {$} = (window as any).fwdekker;
+
+import {blurActiveElement} from "./Common";
+
+
+/**
+ * A modal dialog displayed in HTML.
+ */
+export class ModalDialog {
+ private readonly dialog: HTMLElement;
+ private readonly openButton?: HTMLElement;
+ private readonly onOpen?: () => void;
+
+
+ /**
+ * Constructs a new modal dialog wrapper.
+ *
+ * @param dialog the dialog maintained by this instance
+ * @param openButton the element that opens the dialog when clicked
+ * @param onOpen the callback to invoke when the dialog is opened
+ * @param form the form contained in the dialog
+ * @param closeButton the element that closes the dialog's form when clicked
+ * @param submitButton the element that submits the dialog's form when clicked
+ * @param onSubmit the callback to invoke when the dialog's form is submitted
+ */
+ constructor(
+ {
+ dialog,
+ openButton,
+ onOpen,
+ form,
+ closeButton,
+ submitButton,
+ onSubmit
+ }: {
+ dialog: HTMLElement,
+ openButton?: HTMLElement,
+ onOpen?: (() => void),
+ form?: HTMLFormElement,
+ closeButton?: HTMLElement,
+ submitButton?: HTMLElement,
+ onSubmit?: (() => void)
+ }
+ ) {
+ this.dialog = dialog;
+ this.openButton = openButton;
+ this.onOpen = onOpen;
+
+ document.addEventListener(
+ "click",
+ event => {
+ if (!(event.target instanceof Node)) return;
+
+ console.log("close");
+ if (event.target !== openButton && !this.dialog.contains(event.target) || this.dialog === event.target)
+ this.close();
+ }
+ );
+ document.addEventListener(
+ "keydown",
+ event => {
+ if (event.key === "Escape" && this.isOpen())
+ this.close();
+ }
+ );
+
+ openButton?.addEventListener(
+ "click",
+ event => {
+ event.preventDefault();
+
+ this.open();
+ }
+ );
+ closeButton?.addEventListener(
+ "click",
+ event => {
+ event.preventDefault();
+ this.close();
+ }
+ );
+
+ form?.addEventListener(
+ "submit",
+ event => {
+ event.preventDefault();
+ onSubmit?.();
+ }
+ );
+ submitButton?.addEventListener(
+ "click",
+ event => {
+ event.preventDefault();
+ onSubmit?.();
+ }
+ );
+ }
+
+
+ /**
+ * Opens the dialog.
+ */
+ open(): void {
+ console.log("opening");
+ blurActiveElement();
+ setTimeout(() => $("[autofocus]", this.dialog)?.focus(), 100);
+
+ this.dialog.setAttribute("open", "true");
+ this.onOpen?.();
+ }
+
+ /**
+ * Closes the dialog.
+ */
+ close(): void {
+ if (this.isOpen()) {
+ this.dialog.removeAttribute("open");
+ setTimeout(() => this.openButton?.focus(), 100);
+ }
+ }
+
+ /**
+ * Returns `true` if and only if this dialog is currently open.
+ */
+ isOpen(): boolean {
+ return this.dialog.hasAttribute("open");
+ }
+}
diff --git a/src/main/js/Preferences.ts b/src/main/js/Preferences.ts
index 714a4f6..fd5ade6 100644
--- a/src/main/js/Preferences.ts
+++ b/src/main/js/Preferences.ts
@@ -1,6 +1,6 @@
+const {LocalStorage} = (window as any).fwdekker.storage;
+
import {BasicIconFont, IconFont} from "./Display";
-// @ts-ignore
-const {Storage, LocalStorage} = window.fwdekker.storage;
/**
diff --git a/src/main/js/Solver.ts b/src/main/js/Solver.ts
index e590966..b002d87 100644
--- a/src/main/js/Solver.ts
+++ b/src/main/js/Solver.ts
@@ -220,7 +220,8 @@ export class Solver {
if (!adjacentSquaresOnly)
matrix.push(Array(unknowns.length).fill(1).concat(field.mineCount - field.flagCount));
- return new Matrix(matrix).solveBinary()
+ return (new Matrix(matrix))
+ .solveBinary()
.map((it, i) => it === undefined ? undefined : [it, unknowns[i]]);
}
diff --git a/src/main/js/Statistics.ts b/src/main/js/Statistics.ts
index 6c68060..10049eb 100644
--- a/src/main/js/Statistics.ts
+++ b/src/main/js/Statistics.ts
@@ -1,6 +1,6 @@
+const {LocalStorage} = (window as any).fwdekker.storage;
+
import {formatTime} from "./Common";
-// @ts-ignore
-const {Storage, LocalStorage} = window.fwdekker.storage;
/**
diff --git a/src/main/js/UI.ts b/src/main/js/UI.ts
deleted file mode 100644
index 558e13b..0000000
--- a/src/main/js/UI.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-import {blurActiveElement} from "./Common";
-
-
-/**
- * An overlay displayed in HTML.
- */
-export class Overlay {
- private readonly overlay: HTMLDivElement;
- private readonly submitForm: HTMLFormElement | null;
- private readonly cancelForm: HTMLFormElement | null;
-
-
- /**
- * Constructs a new overlay.
- *
- * @param overlay the overlay element to show and hide
- * @param submitForm the form that invokes `onSubmit` and closes the overlay when submitted
- * @param cancelForm the form that closes the overlay when submitted
- * @param onSubmit the callback to invoke when the form is submit
- */
- constructor(
- overlay: HTMLDivElement,
- submitForm: HTMLFormElement | null,
- cancelForm: HTMLFormElement | null,
- onSubmit: (() => void) | null = null
- ) {
- this.overlay = overlay;
- overlay.addEventListener(
- "mousedown",
- event => {
- if (event.target === overlay)
- this.hide();
- }
- );
- document.addEventListener(
- "keydown",
- event => {
- if (event.key === "Escape" && this.isVisible())
- this.hide();
- }
- );
-
- this.submitForm = submitForm;
- submitForm?.addEventListener(
- "submit",
- event => {
- event.preventDefault();
-
- this.hide();
- onSubmit?.();
- }
- );
-
- this.cancelForm = cancelForm;
- cancelForm?.addEventListener(
- "submit",
- event => {
- event.preventDefault();
-
- this.hide();
- }
- );
- }
-
-
- /**
- * Shows the overlay.
- */
- show(): void {
- this.overlay.classList.remove("hidden");
- }
-
- /**
- * Hides the overlay.
- */
- hide(): void {
- this.overlay.classList.add("hidden");
- blurActiveElement();
- }
-
- /**
- * Returns `true` if and only if this overlay is currently visible.
- */
- isVisible(): boolean {
- return !this.overlay.classList.contains("hidden");
- }
-}