Upgrade template to v3
This commit is contained in:
parent
0e0238687a
commit
b4f2586aa8
Binary file not shown.
18
package.json
18
package.json
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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> Preferences</a>
|
||||||
<!-- Preferences -->
|
<a role="button" href="#" id="statistics-open"><i class="fa fa-tachometer"></i> Statistics</a>
|
||||||
<form id="preferencesOpenForm">
|
<a role="button" href="#" id="high-scores-open">
|
||||||
<button><i class="fa fa-cogs"></i> Preferences</button>
|
<i class="fa fa-trophy"></i> High scores</a>
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- Statistics -->
|
|
||||||
<form id="statisticsOpenForm">
|
|
||||||
<button><i class="fa fa-tachometer"></i> Statistics</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- High scores -->
|
|
||||||
<form id="highScoresOpenForm">
|
|
||||||
<button><i class="fa fa-trophy"></i> 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> New game</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- Restart -->
|
|
||||||
<form id="restartForm">
|
|
||||||
<button><i class="fa fa-sync"></i> Restart game</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- Seed -->
|
|
||||||
<form id="seedOpenForm">
|
|
||||||
<button><i class="fa fa-tree"></i> Enter seed</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row controls">
|
|
||||||
<div class="column">
|
|
||||||
<!-- Undo -->
|
|
||||||
<form id="undoForm">
|
|
||||||
<button><i class="fa fa-undo"></i> Undo</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- Redo -->
|
|
||||||
<form id="redoForm">
|
|
||||||
<button><i class="fa fa-repeat"></i> Redo</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- Hint -->
|
|
||||||
<form id="hintForm">
|
|
||||||
<button><i class="fa fa-lightbulb-o"></i> Hint</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- Solver -->
|
|
||||||
<form id="solveForm">
|
|
||||||
<button><i class="fa fa-key"></i> 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 <canvas> element to run this game.
|
|
||||||
</canvas>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="controls">
|
||||||
|
<a role="button" href="#" id="new-game"><i class="fa fa-random"></i> New game</a>
|
||||||
|
<a role="button" href="#" id="restart"><i class="fa fa-sync"></i> Restart game</a>
|
||||||
|
<a role="button" href="#" id="seed-open"><i class="fa fa-tree"></i> 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 <canvas> element to run this game.
|
||||||
|
</canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<footer class="controls">
|
||||||
|
<a role="button" href="#" id="undo"><i class="fa fa-undo"></i> Undo</a>
|
||||||
|
<a role="button" href="#" id="redo"><i class="fa fa-repeat"></i> Redo</a>
|
||||||
|
<a role="button" href="#" id="hint"><i class="fa fa-lightbulb-o"></i> Hint</a>
|
||||||
|
<a role="button" href="#" id="solve"><i class="fa fa-key"></i> 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>
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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!;
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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(
|
||||||
() => {
|
() => {
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue