Upgrade template to v3

This commit is contained in:
Florine W. Dekker 2022-11-26 12:08:20 +01:00
parent 0e0238687a
commit b4f2586aa8
Signed by: FWDekker
GPG Key ID: D3DCFAA8A4560BE0
15 changed files with 456 additions and 573 deletions

BIN
package-lock.json generated

Binary file not shown.

View File

@ -1,6 +1,6 @@
{ {
"name": "minesweeper", "name": "minesweeper",
"version": "0.82.16", "version": "0.83.0",
"description": "Just Minesweeper!", "description": "Just Minesweeper!",
"author": "Florine W. Dekker", "author": "Florine W. Dekker",
"browser": "dist/bundle.js", "browser": "dist/bundle.js",
@ -17,21 +17,21 @@
}, },
"dependencies": { "dependencies": {
"alea": "^1.0.1", "alea": "^1.0.1",
"canvas-confetti": "^1.5.1" "canvas-confetti": "^1.6.0"
}, },
"devDependencies": { "devDependencies": {
"grunt": "^1.4.1", "grunt": "^1.5.3",
"grunt-cli": "^1.4.3", "grunt-cli": "^1.4.3",
"grunt-contrib-clean": "^2.0.0", "grunt-contrib-clean": "^2.0.1",
"grunt-contrib-copy": "^1.0.0", "grunt-contrib-copy": "^1.0.0",
"grunt-contrib-watch": "^1.1.0", "grunt-contrib-watch": "^1.1.0",
"grunt-focus": "^1.0.0", "grunt-focus": "^1.0.0",
"grunt-text-replace": "^0.4.0", "grunt-text-replace": "^0.4.0",
"grunt-webpack": "^5.0.0", "grunt-webpack": "^5.0.0",
"ts-loader": "^9.2.6", "ts-loader": "^9.4.1",
"ts-node": "^10.5.0", "ts-node": "^10.9.1",
"typescript": "^4.5.5", "typescript": "^4.9.3",
"webpack": "^5.69.1", "webpack": "^5.75.0",
"webpack-cli": "^4.9.2" "webpack-cli": "^5.0.0"
} }
} }

View File

@ -1,71 +1,42 @@
form {
display: inline;
}
form button.cancel {
background-color: #606c76;
border-color: #606c76;
}
/* Controls */ /* Controls */
.row.controls { .controls-group {
text-align: center; display: flex;
flex-direction: column;
gap: 1em;
} }
.row.controls input, .row.controls button { .controls {
display: inline; display: flex;
gap: 1em;
flex-wrap: wrap;
align-items: start;
justify-content: center;
justify-items: center;
} }
.row.controls select, .row.controls input { .controls select {
width: unset; width: unset;
margin-bottom: 0;
} }
/* Canvas */ /* Canvas */
#canvasContainer { #canvas-container {
text-align: center; text-align: center;
} }
#canvas { #canvas {
display: inline;
box-sizing: border-box;
border: 4mm ridge #bdbdbd; border: 4mm ridge #bdbdbd;
} }
#canvas.invisible {
visibility: hidden !important;
}
/* Overlay */
.overlayWrapper {
z-index: 20;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
/* Dialogs */
#high-scores-dialog article,
#statistics-dialog article {
width: 100%; width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(0, 0, 0, 0.5);
} }
.overlay { #statistics-dialog td {
padding: 6rem;
min-width: 33%;
max-height: 80%;
overflow: auto;
background-color: white;
}
#statisticsOverlay td {
text-align: right; text-align: right;
} }

View File

@ -8,18 +8,24 @@
<meta name="description" content="Just Minesweeper!" /> <meta name="description" content="Just Minesweeper!" />
<meta name="theme-color" content="#0033cc" /> <meta name="theme-color" content="#0033cc" />
<meta name="fwd:auto:show-main" />
<meta name="fwd:nav:target" content="#nav" />
<meta name="fwd:nav:highlight-path" content="/Tools/Minesweeper/" />
<meta name="fwd:footer:target" content="#footer" />
<meta name="fwd:footer:vcs-url" content="https://git.fwdekker.com/tools/minesweeper/" />
<meta name="fwd:footer:version" content="v%%VERSION_NUMBER%%" />
<title>Minesweeper | FWDekker</title> <title>Minesweeper | FWDekker</title>
<link rel="stylesheet" href="https://static.fwdekker.com/fonts/roboto/roboto.css" />
<link rel="stylesheet" href="https://static.fwdekker.com/fonts/fork-awesome/1.x.x/fork-awesome.css" /> <link rel="stylesheet" href="https://static.fwdekker.com/fonts/fork-awesome/1.x.x/fork-awesome.css" />
<link rel="stylesheet" href="https://static.fwdekker.com/lib/template/2.x.x/template.css" /> <link rel="stylesheet" href="https://static.fwdekker.com/lib/template/3.x.x/template.css?v=%%VERSION_NUMBER%%" />
<!--suppress HtmlUnknownTarget --> <!--suppress HtmlUnknownTarget -->
<link rel="stylesheet" href="main.css?v=%%VERSION_NUMBER%%" /> <link rel="stylesheet" href="main.css?v=%%VERSION_NUMBER%%" />
<script async src="https://stats.fwdekker.com/count.js" <script async src="https://stats.fwdekker.com/count.js"
data-goatcounter="https://stats.fwdekker.com/count"></script> data-goatcounter="https://stats.fwdekker.com/count"></script>
</head> </head>
<body> <body>
<noscript> <noscript class="fwd-js-notice">
<img src="https://stats.fwdekker.com/count?p=/tools/minesweeper/" alt="Counting pixel" /> <img src="https://stats.fwdekker.com/count?p=/tools/minesweeper/" alt="Counting pixel" />
<p> <p>
@ -28,184 +34,166 @@
instructions on how to enable JavaScript in your web browser</a>. instructions on how to enable JavaScript in your web browser</a>.
</p> </p>
</noscript> </noscript>
<main class="hidden"> <nav id="nav"></nav>
<div id="nav"></div> <main class="container hidden">
<div id="contents"> <div role="document">
<div id="header"></div> <section>
<header class="fwd-header">
<hgroup>
<h1><a href=".">Minesweeper</a></h1>
<h2>Just Minesweeper!</h2>
</hgroup>
</header>
<section class="container"> <article>
<!-- Controls --> <header class="controls-group">
<div class="row controls"> <div class="controls">
<div class="column"> <a role="button" href="#" id="preferences-open"><i class="fa fa-cogs"></i>&emsp;Preferences</a>
<!-- Preferences --> <a role="button" href="#" id="statistics-open"><i class="fa fa-tachometer"></i>&emsp;Statistics</a>
<form id="preferencesOpenForm"> <a role="button" href="#" id="high-scores-open">
<button><i class="fa fa-cogs"></i>&emsp;Preferences</button> <i class="fa fa-trophy"></i>&emsp;High scores</a>
</form>
<!-- Statistics -->
<form id="statisticsOpenForm">
<button><i class="fa fa-tachometer"></i>&emsp;Statistics</button>
</form>
<!-- High scores -->
<form id="highScoresOpenForm">
<button><i class="fa fa-trophy"></i>&emsp;High scores</button>
</form>
</div>
</div>
<div class="row controls">
<div class="column">
<!-- Difficulty -->
<label for="difficulty"></label>
<select id="difficulty">
<option>Select difficulty</option>
</select>
<!-- New game -->
<form id="newGameForm">
<button><i class="fa fa-random"></i>&emsp;New game</button>
</form>
<!-- Restart -->
<form id="restartForm">
<button><i class="fa fa-sync"></i>&emsp;Restart game</button>
</form>
<!-- Seed -->
<form id="seedOpenForm">
<button><i class="fa fa-tree"></i>&emsp;Enter seed</button>
</form>
</div>
</div>
<div class="row controls">
<div class="column">
<!-- Undo -->
<form id="undoForm">
<button><i class="fa fa-undo"></i>&emsp;Undo</button>
</form>
<!-- Redo -->
<form id="redoForm">
<button><i class="fa fa-repeat"></i>&emsp;Redo</button>
</form>
<!-- Hint -->
<form id="hintForm">
<button><i class="fa fa-lightbulb-o"></i>&emsp;Hint</button>
</form>
<!-- Solver -->
<form id="solveForm">
<button><i class="fa fa-key"></i>&emsp;Solve</button>
</form>
</div>
</div>
<br />
<!-- Field -->
<div class="row">
<div class="column">
<!-- Canvas -->
<div id="canvasContainer">
<canvas id="canvas" class="invisible" width="1" height="1">
Your browser must support the &lt;canvas&gt; element to run this game.
</canvas>
</div> </div>
<div class="controls">
<a role="button" href="#" id="new-game"><i class="fa fa-random"></i>&emsp;New game</a>
<a role="button" href="#" id="restart"><i class="fa fa-sync"></i>&emsp;Restart game</a>
<a role="button" href="#" id="seed-open"><i class="fa fa-tree"></i>&emsp;Enter seed</a>
</div>
<div class="controls">
<!--suppress HtmlFormInputWithoutLabel -->
<select id="difficulty">
<option>Select difficulty</option>
</select>
</div>
</header>
<div id="canvas-container">
<canvas id="canvas" class="hidden" width="1" height="1">
Your browser must support the &lt;canvas&gt; element to run this game.
</canvas>
</div> </div>
</div> <footer class="controls">
<a role="button" href="#" id="undo"><i class="fa fa-undo"></i>&emsp;Undo</a>
<a role="button" href="#" id="redo"><i class="fa fa-repeat"></i>&emsp;Redo</a>
<a role="button" href="#" id="hint"><i class="fa fa-lightbulb-o"></i>&emsp;Hint</a>
<a role="button" href="#" id="solve"><i class="fa fa-key"></i>&emsp;Solve</a>
</footer>
</article>
</section> </section>
<footer id="footer"></footer>
</div> </div>
<div id="footer"></div>
<!-- Custom difficulty overlay --> <!-- Custom difficulty dialog -->
<div class="overlayWrapper hidden" id="customDifficultyOverlay"> <dialog id="custom-difficulty-dialog">
<div class="overlay"> <article>
<h2>Custom difficulty</h2> <header>
<form id="customDifficultyForm"> <hgroup>
<label for="settingsWidth">Width</label> <h1>Custom difficulty</h1>
<input type="number" id="settingsWidth" min="3" max="99" value="9" /> <h2>Configure a custom difficulty.</h2>
</hgroup>
</header>
<form id="custom-difficulty-form">
<label for="settings-width">Width</label>
<input type="number" id="settings-width" min="3" max="99" value="9" autofocus />
<label for="settingsHeight">Height</label> <label for="settings-height">Height</label>
<input type="number" id="settingsHeight" min="3" max="99" value="9" /> <input type="number" id="settings-height" min="3" max="99" value="9" />
<label for="settingsMines">Mines</label> <label for="settings-mines">Mines</label>
<input type="number" id="settingsMines" min="0" value="10" /> <input type="number" id="settings-mines" min="0" value="10" />
<label for="settingsSolvable">Ensure solvability (slow for complex settings)</label> <input type="checkbox" id="settings-solvable" />
<input type="checkbox" id="settingsSolvable" /> <label for="settings-solvable">Ensure solvability. <small>(Slow for complex settings.)</small></label>
<br /><br />
<button>New game</button>
</form> </form>
<form id="customDifficultyCancelForm"> <footer>
<button class="cancel">Cancel</button> <a role="button" href="#" id="custom-difficulty-cancel" class="secondary">Cancel</a>
</form> <a role="button" href="#" id="custom-difficulty-submit">New game</a>
</div> </footer>
</div> </article>
<!-- Seed input overlay --> </dialog>
<div class="overlayWrapper hidden" id="seedOverlay"> <!-- Seed input dialog -->
<div class="overlay"> <dialog id="seed-dialog">
<h2>Seed</h2> <article>
<form id="seedForm"> <header>
<hgroup>
<h1>Seed</h1>
<h2>
Enter a number to generate a minefield with.
Given a difficulty level and first click position, the same seed always generates the exact same
minefield.
</h2>
</hgroup>
</header>
<form id="seed-form">
<!--suppress HtmlFormInputWithoutLabel Only one input, so header already explains everything --> <!--suppress HtmlFormInputWithoutLabel Only one input, so header already explains everything -->
<input id="seed" /> <input id="seed" autofocus />
<button>New game</button>
</form> </form>
<form id="seedCancelForm"> <footer>
<button class="cancel">Cancel</button> <a role="button" href="#" id="seed-cancel" class="secondary">Cancel</a>
</form> <a role="button" href="#" id="seed-submit">New game</a>
</div> </footer>
</div> </article>
<!-- Preferences overlay --> </dialog>
<div class="overlayWrapper hidden" id="preferencesOverlay"> <!-- Preferences dialog -->
<div class="overlay"> <dialog id="preferences-dialog">
<h2>Preferences</h2> <article>
<form id="preferencesForm"> <header>
<label for="preferencesEnableMarks">Enable question marks</label> <hgroup>
<input type="checkbox" id="preferencesEnableMarks" /> <h1>Preferences</h1>
<h2>
<label for="preferencesShowTooManyFlagsHints">Highlight squares with too many flags around them</label> Configure the gameplay to your liking.
<input type="checkbox" id="preferencesShowTooManyFlagsHints" /> </h2>
</hgroup>
</header>
<form id="preferences-form">
<input role="switch" type="checkbox" id="preferences-enable-marks" autofocus />
<label for="preferences-enable-marks">Right-clicking a square twice places a question mark.</label>
<br /><br /> <br /><br />
<button>Save</button> <input role="switch" type="checkbox" id="preferences-show-too-many-flags-hints" />
<label for="preferences-show-too-many-flags-hints">
Highlight squares with too many flags around them.
</label>
</form> </form>
<form id="preferencesCancelForm"> <footer>
<button class="cancel">Cancel</button> <a role="button" href="#" id="preferences-cancel" class="secondary">Cancel</a>
</form> <a role="button" href="#" id="preferences-submit">Save</a>
</div> </footer>
</div> </article>
<!-- Statistics overlay --> </dialog>
<div class="overlayWrapper hidden" id="statisticsOverlay"> <!-- Statistics dialog -->
<div class="overlay"> <dialog id="statistics-dialog">
<h2>Statistics</h2> <article tabindex="-1" autofocus>
<div id="statisticsDiv"></div> <header>
<form id="statisticsResetForm"> <hgroup>
<button>Reset</button> <h1>Statistics</h1>
</form> <h2>Your achievements expressed in numbers.</h2>
<form id="statisticsCloseForm"> </hgroup>
<button class="cancel">Close</button> </header>
</form> <div id="statistics-div"></div>
</div> <footer>
</div> <a role="button" href="#" id="statistics-close" class="secondary">Close</a>
<!-- High scores overlay --> <a role="button" href="#" id="statistics-reset" class="secondary">Reset</a>
<div class="overlayWrapper hidden" id="highScoresOverlay"> </footer>
<div class="overlay"> </article>
<h2>High scores</h2> </dialog>
<div id="highScoresDiv"></div> <!-- High scores dialog -->
<form id="highScoresResetForm"> <dialog id="high-scores-dialog">
<button>Reset</button> <article tabindex="-1" autofocus>
</form> <header>
<form id="highScoresCloseForm"> <hgroup>
<button class="cancel">Close</button> <h1>High scores</h1>
</form> <h2>Your best moments playing Minesweeper.</h2>
</div> </hgroup>
</div> </header>
<div id="high-scores-div"></div>
<footer>
<a role="button" href="#" id="high-scores-close" class="secondary">Close</a>
<a role="button" href="#" id="high-scores-reset" class="secondary">Reset</a>
</footer>
</article>
</dialog>
</main> </main>
<script src="https://static.fwdekker.com/lib/template/2.x.x/template.js"></script> <script src="https://static.fwdekker.com/lib/template/3.x.x/template.js?v=%%VERSION_NUMBER%%"></script>
<script src="https://static.fwdekker.com/lib/template/2.x.x/storage.js"></script>
<!--suppress HtmlUnknownTarget --> <!--suppress HtmlUnknownTarget -->
<script src="bundle.js?v=%%VERSION_NUMBER%%"></script> <script src="bundle.js?v=%%VERSION_NUMBER%%"></script>
</body> </body>

View File

@ -119,7 +119,7 @@ export function waitForForkAwesome(onSuccess: () => void, onFailure: () => void,
ctx.textBaseline = "middle"; ctx.textBaseline = "middle";
const startTime = performance.now(); const startTime = performance.now();
const failTime = timeout === null ? null : startTime + timeout; const failTime = timeout == null ? null : startTime + timeout;
requestAnimationFrame(fontOnload); requestAnimationFrame(fontOnload);
/** /**
@ -129,7 +129,7 @@ export function waitForForkAwesome(onSuccess: () => void, onFailure: () => void,
*/ */
function fontOnload(time: number): void { function fontOnload(time: number): void {
const currentCount = getPixelCount(); const currentCount = getPixelCount();
if (failTime !== null && time > failTime) onFailure(); if (failTime != null && time > failTime) onFailure();
else if (currentCount < targetPixelCount) requestAnimationFrame(fontOnload); else if (currentCount < targetPixelCount) requestAnimationFrame(fontOnload);
else onSuccess(); else onSuccess();
} }

View File

@ -156,7 +156,7 @@ export class Display {
* @private * @private
*/ */
private getFieldOffset(): { x: number, y: number } { 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}; 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}; 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 { setField(field: Field | null): void {
this.hintSquare = null; this.hintSquare = null;
this.field = field; 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.width = Math.max(this.minSquareWidth, this.field.width) * this.scale;
this.canvas.height = this.field.height * this.scale + this.scale; this.canvas.height = this.field.height * this.scale + this.scale;
@ -213,7 +213,7 @@ export class Display {
const {x, y} = this.getFieldOffset(); const {x, y} = this.getFieldOffset();
this.clearCanvas(ctx); this.clearCanvas(ctx);
if (this.field === null) return; if (this.field == null) return;
ctx.save(); ctx.save();
ctx.translate(x, y); ctx.translate(x, y);
@ -247,7 +247,7 @@ export class Display {
* @private * @private
*/ */
private drawGrid(ctx: CanvasRenderingContext2D): void { private drawGrid(ctx: CanvasRenderingContext2D): void {
if (this.field === null) return; if (this.field == null) return;
ctx.save(); ctx.save();
ctx.beginPath(); ctx.beginPath();
@ -271,7 +271,7 @@ export class Display {
* @private * @private
*/ */
private drawCovers(ctx: CanvasRenderingContext2D): void { private drawCovers(ctx: CanvasRenderingContext2D): void {
if (this.field === null) return; if (this.field == null) return;
ctx.save(); ctx.save();
ctx.fillStyle = "#555"; ctx.fillStyle = "#555";
@ -279,7 +279,7 @@ export class Display {
.filter(it => it.isCovered) .filter(it => it.isCovered)
.filter(it => { .filter(it => {
// True if square should be covered // 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; return true;
if (this.mouseHoldUncover && this.mouseSquare === it) if (this.mouseHoldUncover && this.mouseSquare === it)
return it.hasFlag || it.hasMark; return it.hasFlag || it.hasMark;
@ -299,7 +299,7 @@ export class Display {
* @private * @private
*/ */
private drawHints(ctx: CanvasRenderingContext2D): void { private drawHints(ctx: CanvasRenderingContext2D): void {
if (this.field === null) return; if (this.field == null) return;
if (this.preferences.showTooManyFlagsHints) { if (this.preferences.showTooManyFlagsHints) {
ctx.save(); ctx.save();
@ -311,7 +311,7 @@ export class Display {
ctx.restore(); ctx.restore();
} }
if (this.hintSquare !== null) { if (this.hintSquare != null) {
ctx.save(); ctx.save();
ctx.fillStyle = "rgba(0, 255, 0, 0.3)"; ctx.fillStyle = "rgba(0, 255, 0, 0.3)";
ctx.fillRect(this.hintSquare.x * this.scale, this.hintSquare.y * this.scale, this.scale, this.scale); 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
*/ */
private drawSymbols(ctx: CanvasRenderingContext2D): void { private drawSymbols(ctx: CanvasRenderingContext2D): void {
if (this.field === null) return; if (this.field == null) return;
ctx.save(); ctx.save();
this.field.squareList.forEach(square => { this.field.squareList.forEach(square => {
if (this.field === null) return; if (this.field == null) return;
let icon; let icon;
if (square.hasFlag) if (square.hasFlag)
@ -354,7 +354,7 @@ export class Display {
* @private * @private
*/ */
private drawStatusBar(ctx: CanvasRenderingContext2D): void { private drawStatusBar(ctx: CanvasRenderingContext2D): void {
if (this.field === null) return; if (this.field == null) return;
ctx.save(); ctx.save();
ctx.fillStyle = "#000"; ctx.fillStyle = "#000";
@ -378,7 +378,7 @@ export class Display {
// Deaths // Deaths
let deathsSymbol; let deathsSymbol;
if (this.field.hasLost) { if (this.field.hasLost) {
if (this.loseTime === null) if (this.loseTime == null)
this.loseTime = Date.now(); this.loseTime = Date.now();
deathsSymbol = Math.floor((Date.now() - this.loseTime) / 1000) % 2 === 0 deathsSymbol = Math.floor((Date.now() - this.loseTime) / 1000) % 2 === 0
@ -422,10 +422,10 @@ export class Display {
* @private * @private
*/ */
private drawWinConfetti(): void { private drawWinConfetti(): void {
if (this.field === null) return; if (this.field == null) return;
const rect = this.canvas.getBoundingClientRect(); const rect = this.canvas.getBoundingClientRect();
if (this.field.hasWon && this.winTime === null) { if (this.field.hasWon && this.winTime == null) {
confetti({ confetti({
origin: { origin: {
x: (rect.left + rect.width / 2) / document.documentElement.clientWidth, x: (rect.left + rect.width / 2) / document.documentElement.clientWidth,

View File

@ -1,3 +1,5 @@
const {MemoryStorage} = (window as any).fwdekker.storage;
// @ts-ignore // @ts-ignore
import alea from "alea"; import alea from "alea";
import {Action, ActionHistory} from "./Action"; import {Action, ActionHistory} from "./Action";
@ -7,8 +9,6 @@ import {HighScores} from "./HighScores";
import {Solver} from "./Solver"; import {Solver} from "./Solver";
import {Statistics} from "./Statistics"; import {Statistics} from "./Statistics";
import {Timer} from "./Timer"; 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 coords the coordinates of the square to look up
* @param orElse the value to return if there is no square at the given coordinates * @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<T = void>(coords: { x: number, y: number }, orElse?: T): Square | T {
return this.squares[coords.y]?.[coords.x] ?? orElse; return this.squares[coords.y]?.[coords.x] ?? orElse;
} }
@ -523,15 +525,15 @@ export class Square {
get neighbors(): Square[] { get neighbors(): Square[] {
if (this._neighbors === undefined) { if (this._neighbors === undefined) {
this._neighbors = [ this._neighbors = [
this.field.getSquareOrElse({x: this.x - 1, y: this.y - 1}), this.field.getSquareOrElse({x: this.x - 1, y: this.y - 1}, null),
this.field.getSquareOrElse({x: this.x, y: this.y - 1}), this.field.getSquareOrElse({x: this.x, y: this.y - 1}, null),
this.field.getSquareOrElse({x: this.x + 1, y: this.y - 1}), this.field.getSquareOrElse({x: this.x + 1, y: this.y - 1}, null),
this.field.getSquareOrElse({x: this.x - 1, y: this.y}), this.field.getSquareOrElse({x: this.x - 1, y: this.y}, null),
this.field.getSquareOrElse({x: this.x + 1, y: this.y}), this.field.getSquareOrElse({x: this.x + 1, y: this.y}, null),
this.field.getSquareOrElse({x: this.x - 1, y: this.y + 1}), this.field.getSquareOrElse({x: this.x - 1, y: this.y + 1}, null),
this.field.getSquareOrElse({x: this.x, y: this.y + 1}), this.field.getSquareOrElse({x: this.x, y: this.y + 1}, null),
this.field.getSquareOrElse({x: this.x + 1, y: this.y + 1}), this.field.getSquareOrElse({x: this.x + 1, y: this.y + 1}, null),
].filter(it => it !== null); ].filter((it): it is Square => it !== null);
} }
return this._neighbors!; return this._neighbors!;

View File

@ -1,16 +1,16 @@
// @ts-ignore const {$, stringToHtml} = (window as any).fwdekker;
const {$} = window.fwdekker;
// @ts-ignore // @ts-ignore
import alea from "alea"; import alea from "alea";
import {blurActiveElement, stringToHash} from "./Common"; import {stringToHash} from "./Common";
import {customDifficulty, defaultDifficulty, difficulties} from "./Difficulty"; import {customDifficulty, defaultDifficulty, difficulties} from "./Difficulty";
import {Display} from "./Display"; import {Display} from "./Display";
import {Field} from "./Field"; import {Field} from "./Field";
import {HighScores} from "./HighScores"; import {HighScores} from "./HighScores";
import {ModalDialog} from "./ModalDialog";
import {Preferences} from "./Preferences"; import {Preferences} from "./Preferences";
import {Solver} from "./Solver"; import {Solver} from "./Solver";
import {Statistics} from "./Statistics"; import {Statistics} from "./Statistics";
import {Overlay} from "./UI";
/** /**
@ -22,33 +22,13 @@ export class Game {
private statisticsTimer: number | undefined; private statisticsTimer: number | undefined;
private readonly canvas: HTMLCanvasElement; private readonly canvas: HTMLCanvasElement;
private readonly difficultySelect: HTMLSelectElement; private readonly customDifficultyOverlay: ModalDialog;
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 widthInput: HTMLInputElement; private readonly widthInput: HTMLInputElement;
private readonly heightInput: HTMLInputElement; private readonly heightInput: HTMLInputElement;
private readonly minesInput: HTMLInputElement; private readonly minesInput: HTMLInputElement;
private readonly solvableInput: 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 statisticsDiv: HTMLDivElement;
private readonly statisticsResetForm: HTMLFormElement;
private readonly statisticsOpenForm: HTMLFormElement;
private readonly highScoresOverlay: Overlay;
private readonly highScoresDiv: HTMLDivElement; private readonly highScoresDiv: HTMLDivElement;
private readonly highScoresResetForm: HTMLFormElement;
private readonly highScoresOpenForm: HTMLFormElement;
private readonly rng: any; private readonly rng: any;
private seed: string; private seed: string;
@ -71,7 +51,7 @@ export class Game {
this.field = null; // Placeholder until `initNewField` this.field = null; // Placeholder until `initNewField`
this.display = new Display(this.canvas, this.field, preferences); this.display = new Display(this.canvas, this.field, preferences);
this.display.startDrawLoop(); this.display.startDrawLoop();
this.canvas.classList.remove("invisible"); this.canvas.classList.remove("hidden");
this.rng = alea("" + Date.now()); this.rng = alea("" + Date.now());
this.seed = "" + this.rng.uint32(); this.seed = "" + this.rng.uint32();
@ -83,44 +63,40 @@ export class Game {
// Settings // Settings
this.difficultySelect = $("#difficulty"); const difficultySelect = $("#difficulty");
difficulties.forEach(it => { difficulties.forEach(it => {
const option = document.createElement("option"); const description = `${it.name}${it.description != null ? ` (${it.description})` : ""}`;
option.value = it.name; difficultySelect.appendChild(stringToHtml(`<option value=${it.name}>${description}</option>`));
option.innerHTML = `${it.name}${it.description !== null ? ` (${it.description})` : ""}`;
this.difficultySelect.add(option);
}); });
this.difficultySelect.addEventListener( difficultySelect.addEventListener("click", (event: MouseEvent) => event.stopPropagation());
difficultySelect.addEventListener(
"change", "change",
event => { () => {
event.preventDefault(); const difficulty = difficulties[difficultySelect.selectedIndex - 1];
difficultySelect.selectedIndex = 0;
const difficulty = difficulties[this.difficultySelect.selectedIndex - 1];
this.difficultySelect.selectedIndex = 0;
if (difficulty === undefined) return; if (difficulty === undefined) return;
if (difficulty.name !== customDifficulty.name) { if (difficulty.name === customDifficulty.name)
this.difficultySelect.selectedIndex = 0; this.customDifficultyOverlay.open();
else
this.initNewField(difficulty.width, difficulty.height, difficulty.mineCount, difficulty.solvable); 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.widthInput.value = "" + (this.field?.width ?? defaultDifficulty.width);
this.heightInput.value = "" + (this.field?.height ?? defaultDifficulty.height); this.heightInput.value = "" + (this.field?.height ?? defaultDifficulty.height);
this.minesInput.value = "" + (this.field?.mineCount ?? defaultDifficulty.mineCount); this.minesInput.value = "" + (this.field?.mineCount ?? defaultDifficulty.mineCount);
this.solvableInput.checked = this.field?.isSolvable ?? defaultDifficulty.solvable; this.solvableInput.checked = this.field?.isSolvable ?? defaultDifficulty.solvable;
this.setMineLimit(); this.setMineLimit();
this.widthInput.focus(); },
} form: $("#custom-difficulty-form"),
); closeButton: $("#custom-difficulty-cancel"),
submitButton: $("#custom-difficulty-submit"),
// Custom difficulty onSubmit: () => {
this.customDifficultyOverlay = new Overlay(
$("#customDifficultyOverlay"),
$("#customDifficultyForm"),
$("#customDifficultyCancelForm"),
() => {
this.initNewField( this.initNewField(
+this.widthInput.value, +this.widthInput.value,
+this.heightInput.value, +this.heightInput.value,
@ -128,218 +104,135 @@ export class Game {
this.solvableInput.checked this.solvableInput.checked
); );
} }
); });
this.widthInput = $("#settingsWidth"); this.widthInput = $("#settings-width");
this.widthInput.addEventListener("change", _ => this.setMineLimit()); this.widthInput.addEventListener("change", _ => this.setMineLimit());
this.heightInput = $("#settingsHeight"); this.heightInput = $("#settings-height");
this.heightInput.addEventListener("change", _ => this.setMineLimit()); this.heightInput.addEventListener("change", _ => this.setMineLimit());
this.minesInput = $("#settingsMines"); this.minesInput = $("#settings-mines");
this.solvableInput = $("#settingsSolvable"); this.solvableInput = $("#settings-solvable");
// New game form // New game form
this.newGameForm = $("#newGameForm"); $("#new-game").addEventListener(
this.newGameForm.addEventListener( "click",
"submit", () =>
event => { this.initNewField(this.field?.width, this.field?.height, this.field?.mineCount, this.field?.isSolvable)
event.preventDefault();
this.initNewField(this.field?.width, this.field?.height, this.field?.mineCount, this.field?.isSolvable);
blurActiveElement();
}
); );
// Restart // Restart
this.restartForm = $("#restartForm"); $("#restart").addEventListener("click", () => this.field?.undo()); // Undoes all
this.restartForm.addEventListener(
"submit",
event => {
event.preventDefault();
this.field?.undo(); // Undoes all
blurActiveElement();
}
);
// Seed // Seed
this.seedInput = $("#seed"); const seedInput = $("#seed");
this.seedOpenForm = $("#seedOpenForm"); const seedDialog = new ModalDialog({
this.seedOpenForm.addEventListener( dialog: $("#seed-dialog"),
"submit", openButton: $("#seed-open"),
event => { onOpen: () => seedInput.value = this.seed,
event.preventDefault(); form: $("#seed-form"),
closeButton: $("#seed-cancel"),
this.seedOverlay.show(); submitButton: $("#seed-submit"),
this.seedInput.value = this.seed; onSubmit: () => {
this.seedInput.focus();
setTimeout(() => this.seedInput.select(), 0);
}
);
this.seedOverlay = new Overlay(
$("#seedOverlay"),
$("#seedForm"),
$("#seedCancelForm"),
() => {
this.initNewField( this.initNewField(
this.field?.width, this.field?.width,
this.field?.height, this.field?.height,
this.field?.mineCount, this.field?.mineCount,
this.field?.isSolvable, this.field?.isSolvable,
this.seedInput.value seedInput.value
); );
seedDialog.close();
} }
); });
// Undo // Undo
this.undoForm = $("#undoForm"); $("#undo").addEventListener("click", () => this.field?.undo(1));
this.undoForm.addEventListener(
"submit",
event => {
event.preventDefault();
this.field?.undo(1);
blurActiveElement();
}
);
// Redo // Redo
this.redoForm = $("#redoForm"); $("#redo").addEventListener("click", () => this.field?.redo(1));
this.redoForm.addEventListener(
"submit",
event => {
event.preventDefault();
this.field?.redo(1);
blurActiveElement();
}
);
// Hint // Hint
this.hintForm = $("#hintForm"); $("#hint").addEventListener(
this.hintForm.addEventListener( "click",
"submit", () => {
event => { if (this.field != null) {
event.preventDefault();
if (this.field !== null) {
this.statistics.hintsRequested++; this.statistics.hintsRequested++;
this.display.hintSquare = Solver.getHint(this.field); this.display.hintSquare = Solver.getHint(this.field);
} }
blurActiveElement();
} }
); );
// Solve // Solve
this.solveForm = $("#solveForm"); $("#solve").addEventListener(
this.solveForm.addEventListener( "click",
"submit", () => {
event => { if (this.field != null) {
event.preventDefault();
if (this.field !== null) {
this.statistics.solverUsages++; this.statistics.solverUsages++;
Solver.solve(this.field); Solver.solve(this.field);
} }
blurActiveElement();
} }
); );
// Preferences // Preferences
this.enableMarksInput = $("#preferencesEnableMarks"); const enableMarksInput = $("#preferences-enable-marks");
this.showTooManyFlagsHintsInput = $("#preferencesShowTooManyFlagsHints"); const showTooManyFlagsHintsInput = $("#preferences-show-too-many-flags-hints");
this.preferencesOpenForm = $("#preferencesOpenForm"); const preferencesDialog = new ModalDialog({
this.preferencesOpenForm.addEventListener( dialog: $("#preferences-dialog"),
"submit", openButton: $("#preferences-open"),
event => { onOpen: () => {
event.preventDefault(); 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; preferencesDialog.close();
this.showTooManyFlagsHintsInput.checked = preferences.showTooManyFlagsHints;
this.preferencesOverlay.show();
blurActiveElement();
} }
); });
this.preferencesOverlay = new Overlay(
$("#preferencesOverlay"),
$("#preferencesForm"),
$("#preferencesCancelForm"),
() => {
preferences.marksEnabled = this.enableMarksInput.checked;
preferences.showTooManyFlagsHints = this.showTooManyFlagsHintsInput.checked;
}
);
// Statistics // Statistics
this.statisticsDiv = $("#statisticsDiv"); this.statisticsDiv = $("#statistics-div");
this.statisticsOpenForm = $("#statisticsOpenForm"); new ModalDialog({
this.statisticsOpenForm.addEventListener( dialog: $("#statistics-dialog"),
"submit", openButton: $("#statistics-open"),
event => { onOpen: () => this.updateStatistics(),
event.preventDefault(); closeButton: $("#statistics-close"),
submitButton: $("#statistics-reset"),
this.statisticsOverlay.show(); onSubmit: () => {
blurActiveElement();
}
);
this.statisticsOverlay = new Overlay(
$("#statisticsOverlay"),
null,
$("#statisticsCloseForm")
);
this.statisticsResetForm = $("#statisticsResetForm");
this.statisticsResetForm.addEventListener(
"submit",
event => {
event.preventDefault();
if (!window.confirm("Are you sure you want to reset all statistics? This cannot be undone.")) if (!window.confirm("Are you sure you want to reset all statistics? This cannot be undone."))
return; return;
this.statistics.clear(); this.statistics.clear();
this.updateStatistics();
} }
); });
this.updateStatistics();
// High scores // High scores
this.highScoresDiv = $("#highScoresDiv"); this.highScoresDiv = $("#high-scores-div");
this.highScoresOpenForm = $("#highScoresOpenForm"); new ModalDialog({
this.highScoresOpenForm.addEventListener( dialog: $("#high-scores-dialog"),
"submit", openButton: $("#high-scores-open"),
event => { onOpen: () => this.highScoresDiv.innerHTML = this.highScores.generateHtmlReport(),
event.preventDefault(); closeButton: $("#high-scores-close"),
submitButton: $("#high-scores-reset"),
this.highScoresDiv.innerHTML = this.highScores.generateHtmlReport(); onSubmit: () => {
this.highScoresOverlay.show();
blurActiveElement();
}
);
this.highScoresOverlay = new Overlay(
$("#highScoresOverlay"),
null,
$("#highScoresCloseForm")
);
this.highScoresResetForm = $("#highScoresResetForm");
this.highScoresResetForm.addEventListener(
"submit",
event => {
event.preventDefault();
if (!window.confirm("Are you sure you want to reset all high scores? This cannot be undone.")) if (!window.confirm("Are you sure you want to reset all high scores? This cannot be undone."))
return; return;
this.highScores.clear(); this.highScores.clear();
this.highScoresDiv.innerHTML = this.highScores.generateHtmlReport(); this.highScoresDiv.innerHTML = this.highScores.generateHtmlReport();
} }
); });
// Canvas // Canvas
this.canvas.addEventListener( this.canvas.addEventListener(
"mousemove", "mousemove",
event => { event => {
this.display.mouseSquare = this.field?.getSquareOrElse( const squarePos = this.display.posToSquare({x: event.clientX, y: event.clientY});
this.display.posToSquare({x: event.clientX, y: event.clientY}), this.display.mouseSquare = this.field?.getSquareOrElse(squarePos, null) ?? null;
null
);
} }
); );
this.canvas.addEventListener( this.canvas.addEventListener(
@ -357,11 +250,11 @@ export class Game {
"mousedown", "mousedown",
event => { event => {
event.preventDefault(); event.preventDefault();
if (this.field === null) return; if (this.field == null) return;
this.field.runUndoably(() => { this.field.runUndoably(() => {
const coords = this.display.posToSquare({x: event.clientX, y: event.clientY}); 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) { switch (event.button) {
case 0: case 0:
@ -370,7 +263,7 @@ export class Game {
case 2: case 2:
if (!this.leftDown) { if (!this.leftDown) {
const square = this.field.getSquareOrElse(coords); const square = this.field.getSquareOrElse(coords);
if (square !== null) { if (square != null) {
if (square.hasFlag) { if (square.hasFlag) {
this.field.toggleFlag(coords); this.field.toggleFlag(coords);
if (preferences.marksEnabled) if (preferences.marksEnabled)
@ -397,11 +290,11 @@ export class Game {
"mouseup", "mouseup",
event => { event => {
event.preventDefault(); event.preventDefault();
if (this.field === null) return; if (this.field == null) return;
this.field.runUndoably(() => { this.field.runUndoably(() => {
const coords = this.display.posToSquare({x: event.clientX, y: event.clientY}); 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) { switch (event.button) {
case 0: case 0:
@ -459,7 +352,7 @@ export class Game {
* @param height the height of the field * @param height the height of the field
* @param mineCount the number of mines to place in the field * @param mineCount the number of mines to place in the field
* @param solvable whether the field is guaranteed to be solvable * @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
*/ */
private initNewField( private initNewField(
@ -467,7 +360,7 @@ export class Game {
height: number = defaultDifficulty.height, height: number = defaultDifficulty.height,
mineCount: number = defaultDifficulty.mineCount, mineCount: number = defaultDifficulty.mineCount,
solvable: boolean = defaultDifficulty.solvable, solvable: boolean = defaultDifficulty.solvable,
seed: string | null = null seed?: string
) { ) {
this.seed = seed ?? "" + this.rng.uint32(); this.seed = seed ?? "" + this.rng.uint32();
this.field = new Field( this.field = new Field(
@ -482,13 +375,11 @@ export class Game {
let lastTime: number | null = null; let lastTime: number | null = null;
window.clearInterval(this.statisticsTimer); window.clearInterval(this.statisticsTimer);
this.statisticsTimer = window.setInterval(() => { this.statisticsTimer = window.setInterval(() => {
if (this.field === null) return; if (this.field == null) return;
const elapsedTime = this.field.elapsedTime; const elapsedTime = this.field.elapsedTime;
this.statistics.timeSpent += elapsedTime - (lastTime ?? 0); this.statistics.timeSpent += elapsedTime - (lastTime ?? 0);
lastTime = elapsedTime; lastTime = elapsedTime;
this.updateStatistics();
}, 1000); }, 1000);
} }
} }

View File

@ -1,7 +1,7 @@
const {LocalStorage} = (window as any).fwdekker.storage;
import {formatTime} from "./Common"; import {formatTime} from "./Common";
import {difficulties, Difficulty} from "./Difficulty"; import {difficulties, Difficulty} from "./Difficulty";
// @ts-ignore
const {Storage, LocalStorage} = window.fwdekker.storage;
/** /**

View File

@ -1,5 +1,5 @@
// @ts-ignore const {doAfterLoad} = (window as any).fwdekker;
const {$, doAfterLoad, footer, header, nav} = window.fwdekker;
import {waitForForkAwesome} from "./Common"; import {waitForForkAwesome} from "./Common";
import {BasicIconFont, ForkAwesomeFont} from "./Display"; import {BasicIconFont, ForkAwesomeFont} from "./Display";
import {Game} from "./Game"; import {Game} from "./Game";
@ -7,17 +7,6 @@ import {Preferences} from "./Preferences";
doAfterLoad(() => { 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(); const preferences = new Preferences();
waitForForkAwesome( waitForForkAwesome(
() => { () => {

128
src/main/js/ModalDialog.ts Normal file
View File

@ -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");
}
}

View File

@ -1,6 +1,6 @@
const {LocalStorage} = (window as any).fwdekker.storage;
import {BasicIconFont, IconFont} from "./Display"; import {BasicIconFont, IconFont} from "./Display";
// @ts-ignore
const {Storage, LocalStorage} = window.fwdekker.storage;
/** /**

View File

@ -220,7 +220,8 @@ export class Solver {
if (!adjacentSquaresOnly) if (!adjacentSquaresOnly)
matrix.push(Array(unknowns.length).fill(1).concat(field.mineCount - field.flagCount)); 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]]); .map((it, i) => it === undefined ? undefined : [it, unknowns[i]]);
} }

View File

@ -1,6 +1,6 @@
const {LocalStorage} = (window as any).fwdekker.storage;
import {formatTime} from "./Common"; import {formatTime} from "./Common";
// @ts-ignore
const {Storage, LocalStorage} = window.fwdekker.storage;
/** /**

View File

@ -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");
}
}