Migrate to template v3

This commit is contained in:
Florine W. Dekker 2022-11-20 22:26:38 +01:00
parent 8fe82f4a18
commit 69972cd3f1
Signed by: FWDekker
GPG Key ID: D3DCFAA8A4560BE0
6 changed files with 469 additions and 643 deletions

View File

@ -1,6 +1,6 @@
{
"name": "death-notifier",
"version": "0.14.5", "_comment_version": "Also update version in `composer.json`!",
"version": "0.14.6", "_comment_version": "Also update version in `composer.json`!",
"description": "Get notified when a famous person dies.",
"author": "Florine W. Dekker",
"browser": "dist/bundle.js",

View File

@ -1,121 +1,23 @@
:root {
/* Colors taken from https://isabelcastillo.com/error-info-messages-css */
--info-color: #00529b;
--info-bg-color: #bde5f8;
--success-color: #4f8a10;
--success-bg-color: #dff2bf;
--warning-color: #9f6000;
--warning-bg-color: #feefb3;
--error-color: #d8000c;
--error-bg-color: #ffbaba;
/* Color taken from https://fandom.com */
--fandom-redlink: #ba0000;
}
/* General definitions */
a.redLink {
a.red-link {
color: var(--fandom-redlink);
}
.linkButton {
margin-left: 1em;
}
/* Toggleable password */
input.passwordToggle {
display: none;
}
/* Inline form */
.inlineForm {
margin-bottom: unset;
}
.inlineForm button {
margin: 0;
padding: .4rem;
height: unset;
line-height: unset;
}
.inlineForm label {
display: inline;
}
/* Input validation */
.info {
--message-color: var(--info-color);
--message-bg-color: var(--info-bg-color);
color: var(--message-color);
}
.success {
--message-color: var(--success-color);
--message-bg-color: var(--success-bg-color);
color: var(--message-color);
}
.warning {
--message-color: var(--warning-color);
--message-bg-color: var(--warning-bg-color);
color: var(--message-color);
}
.error, .error:focus {
--message-color: var(--error-color);
--message-bg-color: var(--error-bg-color);
color: var(--message-color);
}
.formValidationInfo {
.flex-columns {
display: flex;
border: 1px solid var(--message-color);
background-color: var(--message-bg-color);
align-items: flex-start;
/*noinspection CssUnresolvedCustomProperty*/
grid-column-gap: calc(var(--block-spacing-horizontal) * 3);
}
.formValidationInfo:not(.hasMessage) {
display: none;
}
.formValidationInfo .validationInfo {
.flex-columns > * {
flex: 1;
padding: 1rem;
}
.closeButton {
padding: 0 2rem 0 2rem;
margin: 0;
width: 5rem;
height: unset;
line-height: unset;
border: 0;
font-size: 1em;
}
.closeButton, .closeButton:active, .closeButton:focus, .closeButton:hover {
background-color: unset;
color: unset;
}
.validationInfo.hasMessage, .inputHint {
display: block;
}
label ~ .validationInfo, label ~ .inputHint {
margin-top: -1.5rem;
margin-bottom: 1.5rem;
}
input.hasMessage, input.hasMessage:focus {
border-color: var(--message-color, inherit);
}
@ -124,33 +26,26 @@ input.hasMessage, input.hasMessage:focus {
font-style: italic;
}
#filterTrackingsForm, #filterTrackingsQuery {
margin-bottom: unset;
}
#filterTrackingsQuery {
#filter-trackings-query {
width: unset;
}
#trackingsWrapper {
#trackings-wrapper {
max-height: 54rem;
margin-bottom: 1rem;
margin-bottom: var(--typography-spacing-vertical);
border-bottom: .1rem solid #e1e1e1;
overflow-y: scroll;
}
#trackingsWrapper {
/* Shows scrolling shadows. Adapted from https://css-tricks.com/books/greatest-css-tricks/scroll-shadows/ */
background: linear-gradient(white 30%, rgba(255, 255, 255, 0)) no-repeat local center calc(2.4rem + 1.6em),
background: linear-gradient(white 30%, rgba(255, 255, 255, 0)) no-repeat local center calc(var(--spacing) + 1.6em),
linear-gradient(rgba(255, 255, 255, 0), white 70%) no-repeat local center bottom,
radial-gradient(farthest-side at 50% 0, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0)) no-repeat scroll center calc(2.4rem + 1.6em),
radial-gradient(farthest-side at 50% 0, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0)) no-repeat scroll center calc(var(--spacing) + 1.6em),
radial-gradient(farthest-side at 50% 100%, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0)) no-repeat scroll center bottom;
background-size: 100% 40px, 100% 40px, 100% 14px, 100% 14px;
}
#trackings {
display: table; /* Overrides Milligram. Enables sticky table header */
margin-bottom: 0;
}
@ -165,24 +60,7 @@ input.hasMessage, input.hasMessage:focus {
padding-bottom: 0.6rem;
}
#trackings tr:last-child td {
border-bottom: none;
}
#trackings form, #trackings button {
#trackings form,
#trackings button {
margin-bottom: unset;
}
#addTrackingPersonName {
width: unset;
}
/* Settings */
#settingsRows input {
width: unset;
}
#settingsRows input[type="checkbox"] + label {
display: inline;
}

View File

@ -8,17 +8,23 @@
<meta name="description" content="Get notified when a famous person dies." />
<meta name="theme-color" content="#0033cc" />
<meta name="fwd:nav:target" content="#nav" />
<meta name="fwd:nav:highlight-path" content="/Tools/Death-Notifier/" />
<meta name="fwd:footer:target" content="#footer" />
<meta name="fwd:footer:vcs-url" content="https://git.fwdekker.com/tools/death-notifier/" />
<meta name="fwd:footer:version" content="%%VERSION_NUMBER%%" />
<meta name="fwd:validation:load-forms" />
<title>Death Notifier | FWDekker</title>
<link rel="stylesheet" href="https://static.fwdekker.com/fonts/roboto/roboto.css" />
<link rel="stylesheet" href="https://static.fwdekker.com/lib/template/2.x.x/template.css?v=%%VERSION_NUMBER%%" />
<link rel="stylesheet" href="https://static.fwdekker.com/lib/template/3.x.x/template.css?v=%%VERSION_NUMBER%%" />
<!--suppress HtmlUnknownTarget -->
<link rel="stylesheet" href="main.css?v=%%VERSION_NUMBER%%" />
<script async src="https://stats.fwdekker.com/count.js"
data-goatcounter="https://stats.fwdekker.com/count"></script>
</head>
<body>
<noscript>
<noscript class="fwd-js-notice">
<img src="https://stats.fwdekker.com/count?p=/tools/death-notifier/" alt="Counting pixel" />
<p>
@ -27,292 +33,367 @@
<a href="https://www.enable-javascript.com/">instructions on how to enable JavaScript in your web browser</a>.
</p>
</noscript>
<main class="hidden">
<div id="nav"></div>
<div id="contents">
<div id="header"></div>
<nav id="nav"></nav>
<main class="container hidden">
<div role="document">
<section>
<header class="fwd-header">
<hgroup>
<h1><a href=".">Death Notifier</a></h1>
<h2>Get notified when a famous person dies.</h2>
</hgroup>
</header>
<section class="container">
<div class="row">
<div class="column">
<p id="globalMessage" class="formValidationInfo">
<output class="validationInfo" for="globalMessage"></output>
</p>
<p id="sharedValidationInfo" class="formValidationInfo">
<output class="validationInfo" for="sharedValidationInfo"></output>
<button type="button" class="closeButton">&times;</button>
</p>
<p id="sharedHomeLink" class="hidden">
<a href="./">Click here to return to the main page</a>
</p>
</div>
</div>
<article id="global-message" class="status-card hidden">
<output></output>
<a class="close" href="#" aria-label="Close"></a>
</article>
<article id="shared-status-card" class="status-card hidden">
<output></output>
<a class="close" href="#" aria-label="Close"></a>
</article>
<p id="shared-home-link" class="hidden">
<a href="./">Click here to return to the main page</a>
</p>
<div class="row hidden" id="loginRow">
<div class="column">
<h2>Log in</h2>
<p>Already have an account? Welcome back!</p>
<form id="loginForm" novalidate>
<p id="loginFormValidationInfo" class="formValidationInfo">
<output class="validationInfo" for="loginFormValidationInfo"></output>
<button type="button" class="closeButton">&times;</button>
</p>
<div id="welcome-part" class="flex-columns">
<article>
<header>
<hgroup>
<h2>Log in</h2>
<h3>Already have an account? Welcome back!</h3>
</hgroup>
</header>
<div class="article-contents">
<form id="login-form" novalidate>
<article class="status-card hidden" data-status-for="login-form">
<output></output>
<a class="close" href="#" aria-label="Close"></a>
</article>
<label for="loginEmail">Email</label>
<input id="loginEmail" type="email" name="email" autocomplete="on" />
<output class="validationInfo" for="loginEmail"></output>
<fieldset>
<label for="login-email">Email</label>
<input id="login-email" type="email" name="email" autocomplete="on" />
<small id="login-email-hint" data-hint-for="login-email"></small>
<label for="loginPassword">Password</label>
<div class="inputWithButton">
<input id="loginPassword" type="password" name="password" />
<button type="button" class="passwordToggle" data-toggles="loginPassword">Show</button>
</div>
<output class="validationInfo" for="loginPassword"></output>
<label for="login-password">Password</label>
<input id="login-password" type="password" name="password" />
<small id="login-password-hint" data-hint-for="login-password"></small>
<button id="loginButton">Log in</button>
<a id="forgotPasswordGoTo" class="linkButton" href="#">Forgot password?</a>
</form>
</div>
<div class="column">
<h2>Register</h2>
<p>
New user?
Create an account!
You can always delete your account and associated data.
Check the <a href="https://fwdekker.com/privacy/">privacy policy</a> for more information.
</p>
<form id="registerForm" novalidate>
<p id="registerFormValidationInfo" class="formValidationInfo">
<output class="validationInfo" for="registerFormValidationInfo"></output>
<button type="button" class="closeButton">&times;</button>
</p>
<input type="checkbox" role="switch" id="login-password-toggle" class="password-toggle"
data-toggles="login-password" />
<label for="login-password-toggle">Show password</label>
</fieldset>
<label for="registerEmail">Email</label>
<input id="registerEmail" type="email" name="email" autocomplete="on" />
<output class="validationInfo" for="registerEmail"></output>
<label for="registerPassword">Password</label>
<div class="inputWithButton">
<input id="registerPassword" type="password" name="password" />
<button type="button" class="passwordToggle" data-toggles="registerPassword">Show</button>
</div>
<fieldset>
<button id="login-submit">Log in</button>
<a role="button" id="forgot-password-go-to" class="outline" href="#">
Forgot password?
</a>
</fieldset>
</form>
</div>
</article>
<article>
<header>
<hgroup>
<h2>Register</h2>
<h3>
New user?
Create an account!
You can always delete your account and associated data.
Check the <a href="https://fwdekker.com/privacy/">privacy policy</a> for more
information.
</h3>
</hgroup>
</header>
<div class="article-contents">
<!-- TODO: Receive requirements from server(?) (combine this with client-side validation) -->
<span class="inputHint" data-for="registerPassword">Use at least 8 characters.</span>
<output class="validationInfo" for="registerPassword"></output>
<form id="register-form" novalidate>
<article class="status-card hidden" data-status-for="register-form">
<output></output>
<a class="close" href="#" aria-label="Close"></a>
</article>
<button id="registerButton">Create account</button>
</form>
</div>
<fieldset>
<label for="register-email">Email</label>
<input id="register-email" type="email" name="email" autocomplete="on" />
<small id="register-email-hint" data-hint-for="register-email"></small>
<label for="register-password">Password</label>
<input id="register-password" type="password" name="password" />
<small id="register-password-hint" data-hint-for="register-password"
data-hint="Use at least 8 characters."></small>
<input type="checkbox" role="switch" id="register-password-toggle"
class="password-toggle"
data-toggles="register-password" />
<label for="register-password-toggle">Show password</label>
</fieldset>
<fieldset>
<button id="register-submit">Create account</button>
</fieldset>
</form>
</div>
</article>
</div>
<div class="row hidden" id="sendForgotPasswordRow">
<div class="column column-50">
<h2>Forgot password</h2>
<p>Send an email to help reset your password.</p>
<form id="sendPasswordResetForm" novalidate>
<p id="sendPasswordResetFormValidationInfo" class="formValidationInfo">
<output class="validationInfo" for="sendPasswordResetFormValidationInfo"></output>
<button type="button" class="closeButton">&times;</button>
</p>
<div id="send-forgot-password-part" class="flex-columns hidden">
<article>
<header>
<hgroup>
<h2>Forgot password</h2>
<h3>Send an email to help reset your password.</h3>
</hgroup>
</header>
<div class="article-contents">
<form id="send-password-reset-form" novalidate>
<article class="status-card hidden" data-status-for="send-password-reset-form">
<output></output>
<a class="close" href="#" aria-label="Close"></a>
</article>
<label for="sendPasswordResetEmail">Email</label>
<input id="sendPasswordResetEmail" type="email" name="email" autocomplete="on" />
<output class="validationInfo" for="sendPasswordResetEmail"></output>
<fieldset>
<label for="send-password-reset-email">Email</label>
<input id="send-password-reset-email" type="email" name="email" autocomplete="on" />
<small id="send-password-reset-email-hint"
data-hint-for="send-password-reset-email"></small>
</fieldset>
<button id="sendPasswordResetButton">Send email</button>
<a id="forgotPasswordGoBack" class="linkButton" href="#">Return to log in form</a>
</form>
</div>
<fieldset>
<button id="send-password-reset-submit">Send email</button>
</fieldset>
</form>
<a role="button" id="forgot-password-go-back" class="outline" href="#">Return to log in form</a>
</div>
</article>
<div></div>
</div>
<div class="row hidden" id="resetPasswordRow">
<div class="column column-50">
<h2>Reset password</h2>
<p>Set a new password for your account.</p>
<form id="resetPasswordForm" novalidate>
<p id="resetPasswordFormValidationInfo" class="formValidationInfo">
<output class="validationInfo" for="resetPasswordFormValidationInfo"></output>
<button type="button" class="closeButton">&times;</button>
</p>
<div id="reset-password-part" class="flex-columns hidden">
<article>
<header>
<hgroup>
<h2>Reset password</h2>
<h3>Set a new password for your account.</h3>
</hgroup>
</header>
<input id="resetPasswordToken" type="hidden" name="token" />
<form id="reset-password-form" novalidate>
<article class="status-card hidden" data-status-for="reset-password-form">
<output></output>
<a class="close" href="#" aria-label="Close"></a>
</article>
<label for="resetPasswordEmail">Email</label>
<input id="resetPasswordEmail" type="email" name="email" disabled />
<output class="validationInfo" for="resetPasswordEmail"></output>
<fieldset>
<input id="reset-password-token" type="hidden" name="token" />
<label for="resetPasswordPassword">Password</label>
<div class="inputWithButton">
<input id="resetPasswordPassword" type="password" name="password" />
<button type="button" class="passwordToggle" data-toggles="resetPasswordPassword">
Show
</button>
</div>
<span class="inputHint" data-for="resetPasswordPassword">Use at least 8 characters.</span>
<output class="validationInfo" for="resetPasswordPassword"></output>
<label for="reset-password-email">Email</label>
<input id="reset-password-email" type="email" name="email" disabled />
<small id="reset-password-email-hint" data-hint-for="reset-password-email"></small>
<button id="resetPasswordButton">Set password</button>
<a id="resetPasswordGoBack" class="linkButton" href="./">Return to log in form</a>
<label for="reset-password-password">Password</label>
<input id="reset-password-password" type="password" name="password" />
<small id="reset-password-password-hint" data-hint-for="reset-password-password"
data-hint="Use at least 8 characters."></small>
<input type="checkbox" role="switch" id="reset-password-password-toggle"
class="password-toggle"
data-toggles="reset-password-password" />
<label for="reset-password-password-toggle">Show password</label>
</fieldset>
<fieldset>
<button id="reset-password-submit">Set password</button>
</fieldset>
</form>
</div>
<a role="button" id="reset-password-go-back" class="outline" href="./">Return to log in form</a>
</article>
<div></div>
</div>
<div class="row hidden" id="trackingRow">
<div class="column">
<article id="tracking-part" class="hidden">
<header>
<h2>Tracked articles</h2>
<p id="removeTrackingValidationInfo" class="formValidationInfo">
<output class="validationInfo" for="removeTrackingValidationInfo"></output>
<button type="button" class="closeButton">&times;</button>
</p>
<form id="filterTrackingsForm" novalidate>
<label for="filterTrackingsQuery">Filter articles</label>
<div class="inputWithButton">
<input id="filterTrackingsQuery" type="text" name="query" />
<button id="filterTrackingsClear" type="button">Clear</button>
</div>
</form>
<div id="trackingsWrapper">
<table id="trackings">
<thead>
<tr>
<th>Name</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<form id="addTrackingForm" novalidate>
<p id="addTrackingFormValidationInfo" class="formValidationInfo">
<output class="validationInfo" for="addTrackingFormValidationInfo"></output>
<button type="button" class="closeButton">&times;</button>
</p>
<label for="addTrackingPersonName">Track another article</label>
<div class="inputWithButton">
<!-- TODO: Show random suggestions on who to track -->
<input id="addTrackingPersonName" type="text" name="person_name"
autocomplete="on" />
<button id="addTrackingButton">Add</button>
</div>
<output class="validationInfo" for="addTrackingPersonName"></output>
</form>
</header>
<article class="status-card hidden" id="remove-trackings-status-card">
<output></output>
<a class="close" href="#" aria-label="Close"></a>
</article>
<form id="filter-trackings-form" novalidate>
<fieldset>
<!--suppress HtmlFormInputWithoutLabel -->
<input id="filter-trackings-query" type="search" name="query" />
</fieldset>
</form>
<div id="trackings-wrapper">
<table id="trackings">
<thead>
<tr>
<th>Name</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<form id="add-tracking-form" novalidate>
<article class="status-card hidden" data-status-for="add-tracking-form">
<output></output>
<a class="close" href="#" aria-label="Close"></a>
</article>
<div id="settingsRows" class="hidden">
<div class="row">
<div class="column">
<fieldset>
<!-- TODO: Show random suggestions on who to track -->
<label for="add-tracking-name">Track another article</label>
<input id="add-tracking-name" type="text" name="person_name" autocomplete="on" />
<small id="add-tracking-name-hint" data-hint-for="add-tracking-name"></small>
<button id="add-tracking-submit">Add</button>
</fieldset>
</form>
</article>
<div id="settings-part" class="hidden">
<article>
<header>
<h2>Account settings</h2>
<form id="logoutForm" novalidate>
<p id="logoutFormValidationInfo" class="formValidationInfo">
<output class="validationInfo" for="logoutFormValidationInfo"></output>
<button type="button" class="closeButton">&times;</button>
</p>
</header>
<form id="logout-form" novalidate>
<article class="status-card hidden" data-status-for="logout-form">
<output></output>
<a class="close" href="#" aria-label="Close"></a>
</article>
<fieldset>
<button id="logoutButton">Log out</button>
</form>
</div>
</div>
<div class="row">
<div class="column">
<h3>Email</h3>
<p>
You use your email address to log in.
If you have verified your email address, you also receive notifications about tracked
articles at this address.
You can always disable notifications.
</p>
<form id="updateEmailForm" novalidate>
<p id="updateEmailFormValidationInfo" class="formValidationInfo">
<output class="validationInfo" for="updateEmailFormValidationInfo"></output>
<button type="button" class="closeButton">&times;</button>
</p>
</fieldset>
</form>
</article>
<label for="updateEmailEmail">Email address</label>
<div class="inputWithButton">
<input id="updateEmailEmail" type="email" name="email" autocomplete="on" />
<button id="updateEmailButton">Change email</button>
</div>
<output class="validationInfo" for="updateEmailEmail"></output>
</form>
<form id="resendEmailVerificationForm" novalidate>
<p id="resendEmailVerificationFormValidationInfo" class="formValidationInfo">
<output class="validationInfo" for="resendEmailVerificationFormValidationInfo"></output>
<button type="button" class="closeButton">&times;</button>
</p>
<article>
<header>
<hgroup>
<h3>Email address</h3>
<h4>
You use your email address to log in.
If you have verified your email address, you also receive notifications about tracked
articles at this address.
You can always disable notifications.
</h4>
</hgroup>
</header>
<form id="update-email-form" novalidate>
<article class="status-card hidden" data-status-for="update-email-form">
<output></output>
<a class="close" href="#" aria-label="Close"></a>
</article>
<input type="checkbox" id="emailVerifiedCheckbox" disabled />
<label for="emailVerifiedCheckbox">Verified</label>
<button id="resendEmailVerificationButton" class="hidden">resend link</button>
</form>
<form id="toggleNotificationsForm" novalidate>
<p id="toggleNotificationsFormValidationInfo" class="formValidationInfo">
<output class="validationInfo" for="toggleNotificationsFormValidationInfo"></output>
<button type="button" class="closeButton">&times;</button>
</p>
<fieldset>
<label for="update-email-email">Email address</label>
<input id="update-email-email" type="email" name="email" autocomplete="on" />
<small id="update-email-email-hint" data-hint-for="update-email-email"></small>
<input type="checkbox" id="notificationsEnabledCheckbox" />
<label for="notificationsEnabledCheckbox">Notifications</label>
</form>
</div>
</div>
<div class="row">
<div class="column">
<button id="updateEmailButton">Change email</button>
</fieldset>
</form>
<form id="resend-email-verification-form" novalidate>
<article class="status-card hidden" data-status-for="resend-email-verification-form">
<output></output>
<a class="close" href="#" aria-label="Close"></a>
</article>
<fieldset>
<input type="checkbox" id="email-verified-checkbox" disabled />
<label for="email-verified-checkbox">Verified</label>
</fieldset>
<fieldset>
<button id="resend-email-verification-submit" class="hidden">resend link</button>
</fieldset>
</form>
<form id="toggle-notifications-form" novalidate>
<article class="status-card hidden" data-status-for="toggle-notifications-form">
<output></output>
<a class="close" href="#" aria-label="Close"></a>
</article>
<fieldset>
<input type="checkbox" id="notifications-enabled-checkbox" />
<label for="notifications-enabled-checkbox">Notifications</label>
</fieldset>
</form>
</article>
<article>
<header>
<h3>Password</h3>
<form><b>Last changed:</b> <span id="passwordLastChanged">...</span></form>
<form id="updatePasswordForm" novalidate>
<p id="updatePasswordFormValidationInfo" class="formValidationInfo">
<output class="validationInfo" for="updatePasswordFormValidationInfo"></output>
<button type="button" class="closeButton">&times;</button>
</p>
</header>
<form><b>Last changed:</b> <span id="password-last-changed">...</span></form>
<form id="update-password-form" novalidate>
<article class="status-card hidden" data-status-for="update-password-form">
<output></output>
<a class="close" href="#" aria-label="Close"></a>
</article>
<label for="updatePasswordPasswordOld">Old password</label>
<div class="inputWithButton">
<input id="updatePasswordPasswordOld" type="password" name="password_old" />
<button type="button" class="passwordToggle" data-toggles="updatePasswordPasswordOld">
Show
</button>
</div>
<output class="validationInfo" for="updatePasswordPasswordOld"></output>
<fieldset>
<label for="update-password-password-old">Old password</label>
<input id="update-password-password-old" type="password" name="password" />
<small id="update-password-password-old-hint"
data-hint-for="update-password-password-old"></small>
<label for="updatePasswordPasswordNew">New password</label>
<div class="inputWithButton">
<input id="updatePasswordPasswordNew" type="password" name="password_new" />
<button type="button" class="passwordToggle" data-toggles="updatePasswordPasswordNew">
Show
</button>
</div>
<span class="inputHint"
data-for="updatePasswordPasswordNew">Use at least 8 characters.</span>
<output class="validationInfo" for="updatePasswordPasswordNew"></output>
<input type="checkbox" role="switch" id="update-password-password-old-toggle"
class="password-toggle"
data-toggles="update-password-password-old" />
<label for="update-password-password-old-toggle">Show password</label>
</fieldset>
<fieldset>
<label for="update-password-password-new">New password</label>
<input id="update-password-password-new" type="password" name="password" />
<small id="update-password-password-new-hint" data-hint-for="update-password-password-new"
data-hint="Use at least 8 characters."></small>
<input type="checkbox" role="switch" id="update-password-password-new-toggle"
class="password-toggle"
data-toggles="update-password-password-new" />
<label for="update-password-password-new-toggle">Show password</label>
</fieldset>
<fieldset>
<button id="updatePasswordButton">Change password</button>
</form>
<!-- TODO: Add forgot password button after logging in -->
</div>
</div>
<div class="row">
<div class="column">
<h3>Delete account</h3>
<p>
If you no longer want to use Death Notifier, you can permanently delete your account.
</p>
<form id="deleteForm" novalidate>
<p id="deleteFormValidationInfo" class="formValidationInfo">
<output class="validationInfo" for="deleteFormValidationInfo"></output>
<button type="button" class="closeButton">&times;</button>
</p>
<input id="deleteEmail" type="hidden" name="email" />
<button id="deleteButton">Delete account</button>
</form>
</div>
</div>
</fieldset>
</form>
<!-- TODO: Add forgot password button after logging in -->
</article>
<article>
<header>
<hgroup>
<h3>Delete account</h3>
<h4>If you no longer want to use Death Notifier, you can permanently delete your
account.</h4>
</hgroup>
</header>
<form id="delete-form" novalidate>
<article class="status-card hidden" data-status-for="delete-form">
<output></output>
<a class="close" href="#" aria-label="Close"></a>
</article>
<fieldset>
<input id="delete-email" type="hidden" name="email" />
<button id="delete-button">Delete account</button>
</fieldset>
</form>
</article>
</div>
</section>
<footer id="footer"></footer>
</div>
<div id="footer"></div>
</main>
<script src="https://static.fwdekker.com/lib/template/2.x.x/template.js?v=%%VERSION_NUMBER%%"></script>
<script src="https://static.fwdekker.com/lib/template/3.x.x/template.js?v=%%VERSION_NUMBER%%"></script>
<!--suppress HtmlUnknownTarget -->
<script src="bundle.js?v=%%VERSION_NUMBER%%"></script>
</body>

View File

@ -1,7 +1,7 @@
// @ts-ignore
const {$} = window.fwdekker;
import {clearMessages, showError} from "./Message";
// @ts-ignore
const {clearFormValidity, showInputInvalid, showMessageError} = window.fwdekker.validation;
/**
@ -11,7 +11,7 @@ export let csrfToken: string | null = null;
/**
* A shared element to place global messages in.
*/
export const sharedMessageElement: HTMLFormElement = $("#sharedValidationInfo");
export const sharedMessageElement: HTMLFormElement = $("#shared-status-card");
/**
@ -57,7 +57,7 @@ export function getApi(
onSatisfied: (response: ServerResponse) => void = emptyFunction,
onUnsatisfied: (response: ServerResponse) => void = emptyFunction,
onError: (error: any) => void = emptyFunction,
onAlways: (response: ServerResponse|undefined) => void = emptyFunction
onAlways: (response: ServerResponse | undefined) => void = emptyFunction
): void {
interactWithApi(
"api.php?" + new URLSearchParams(params), undefined, form,
@ -81,7 +81,7 @@ export function postApi(
onSatisfied: (response: ServerResponse) => void = emptyFunction,
onUnsatisfied: (response: ServerResponse) => void = emptyFunction,
onError: (error: any) => void = emptyFunction,
onAlways: (response: ServerResponse|undefined) => void = emptyFunction
onAlways: (response: ServerResponse | undefined) => void = emptyFunction
): void {
interactWithApi("api.php",
{
@ -115,10 +115,9 @@ function interactWithApi(
onSatisfied: (response: ServerResponse) => void = emptyFunction,
onUnsatisfied: (response: ServerResponse) => void = emptyFunction,
onError: (error: any) => void = emptyFunction,
onAlways: (response: ServerResponse|undefined) => void = emptyFunction
onAlways: (response: ServerResponse | undefined) => void = emptyFunction
): void {
clearMessages(form);
const topErrorElement = $(`#${form.id}ValidationInfo`) ?? sharedMessageElement;
clearFormValidity(form);
fetch(url, options)
.then(it => it.json())
@ -128,11 +127,14 @@ function interactWithApi(
if (!it.satisfied) {
if (it.payload.message != null) {
if (it.payload.target == null) {
showError(topErrorElement, it.payload.message);
showMessageError(form, it.payload.message);
} else {
const target = $(`input[name=${it.payload.target}]`, form);
showError(target ?? topErrorElement, it.payload.message);
target?.focus();
const target = $(`input[name="${it.payload.target}"]`, form);
if (target == null) {
showMessageError(form, it.payload.message);
} else {
showInputInvalid(target, it.payload.message);
}
}
}
@ -144,7 +146,7 @@ function interactWithApi(
})
.catch((error) => {
console.error(error);
showError(topErrorElement, "Unexpected error. Please try again later.");
showMessageError(form, "Unexpected error. Please try again later.");
onError(error);
onAlways(undefined);
});

View File

@ -1,9 +1,12 @@
// @ts-ignore
const {$, $a, doAfterLoad, footer, header, nav} = window.fwdekker;
const {$, $a, doAfterLoad} = window.fwdekker;
const {
clearFormValidity, clearMessageStatus, showMessageInfo, showMessageError, showMessageSuccess, showMessageWarning
// @ts-ignore
} = window.fwdekker.validation;
import {csrfToken, emptyFunction, getApi, postApi, ServerResponse, sharedMessageElement} from "./API";
import {CustomEventHandler} from "./CustomEventHandler";
import {clearMessage, clearMessages, showError, showInfo, showSuccess, showWarning} from "./Message";
/**
@ -37,7 +40,7 @@ function refreshTrackings(): void {
nameLink.href = "https://en.wikipedia.org/wiki/" + tracking.name;
nameLink.innerText = tracking.name;
if (tracking.deleted)
nameLink.classList.add("redLink");
nameLink.classList.add("red-link");
nameCell.append(nameLink);
row.append(nameCell);
@ -74,8 +77,8 @@ function refreshTrackings(): void {
deleteForm,
() => {
refreshTrackings();
showSuccess(
$("#removeTrackingValidationInfo"),
showMessageSuccess(
$("#remove-trackings-status-card"),
`Successfully removed <b>${tracking.name}</b>.`
);
}
@ -83,6 +86,7 @@ function refreshTrackings(): void {
});
const deleteButton = document.createElement("button");
deleteButton.innerText = "remove";
deleteButton.classList.add("outline");
deleteForm.append(deleteButton);
deleteCell.append(deleteForm);
row.append(deleteCell);
@ -100,8 +104,8 @@ function refreshTrackings(): void {
}
// Scroll to top, re-apply filter
$("#trackingsWrapper").scrollTop = 0;
$("#filterTrackingsQuery").dispatchEvent(new InputEvent("input"));
$("#trackings-wrapper").scrollTop = 0;
$("#filter-trackings-query").dispatchEvent(new InputEvent("input"));
}
);
}
@ -117,23 +121,23 @@ function refreshUserData(): void {
const userData = response.payload;
// Account deletion
$("#deleteEmail").value = userData.email;
$("#delete-email").value = userData.email;
// Email
$("#updateEmailEmail").value = userData.email;
$("#emailVerifiedCheckbox").checked = userData.email_verified;
$("#update-email-email").value = userData.email;
$("#email-verified-checkbox").checked = userData.email_verified;
if (!userData.email_verified)
showWarning(
showMessageWarning(
sharedMessageElement,
"You will not receive any email notifications until you verify your email address. " +
"Check your inbox for further instructions."
);
else
clearMessage(sharedMessageElement);
$("#resendEmailVerificationButton").classList.toggle("hidden", userData.email_verified);
clearMessageStatus(sharedMessageElement);
$("#resend-email-verification-submit").classList.toggle("hidden", userData.email_verified);
// Notifications
const notificationsCheckbox = $("#notificationsEnabledCheckbox");
const notificationsCheckbox = $("#notifications-enabled-checkbox");
notificationsCheckbox.disabled = !userData.email_verified;
notificationsCheckbox.checked = userData.email_verified && userData.email_notifications_enabled;
@ -143,7 +147,7 @@ function refreshUserData(): void {
const updateTime = new Date(userData.password_last_change * 1000);
updateTime.setHours(0, 0, 0, 0);
const diff = (+today - +updateTime) / 86400000;
$("#passwordLastChanged").innerText = diff === 0 ? "today" : diff + " days ago";
$("#password-last-changed").innerText = diff === 0 ? "today" : diff + " days ago";
}
)
}
@ -154,7 +158,7 @@ function refreshUserData(): void {
* @param params the URL's get parameters
*/
function handleAction(params: URLSearchParams): void {
const sharedHomeLink = $("#sharedHomeLink");
const sharedHomeLink = $("#shared-home-link");
let params_are_valid = true;
switch (params.get("action")) {
@ -176,7 +180,7 @@ function handleAction(params: URLSearchParams): void {
sharedHomeLink.classList.remove("hidden");
redirectWithTimeout(
"./", 3, (secondsLeft) => {
showSuccess(
showMessageSuccess(
sharedMessageElement,
`Your email address has been verified. ` +
`You will be redirected after ${secondsLeft} second(s).`
@ -201,10 +205,10 @@ function handleAction(params: URLSearchParams): void {
},
sharedMessageElement,
() => {
$("#resetPasswordToken").value = params.get("token");
$("#resetPasswordEmail").value = params.get("email");
$("#resetPasswordRow").classList.remove("hidden");
$("#resetPasswordPassword").focus();
$("#reset-password-token").value = params.get("token");
$("#reset-password-email").value = params.get("email");
$("#reset-password-part").classList.remove("hidden");
$("#reset-password-password").focus();
},
() => sharedHomeLink.classList.remove("hidden")
);
@ -218,10 +222,7 @@ function handleAction(params: URLSearchParams): void {
if (!params_are_valid) {
sharedHomeLink.classList.remove("hidden");
showError(
sharedMessageElement,
`Invalid URL.`
);
showMessageError(sharedMessageElement, `Invalid URL.`);
}
}
@ -245,66 +246,41 @@ function redirectWithTimeout(target: string, seconds: number, doEachSecond: (sec
}
// Initialize template
doAfterLoad(() => {
$("#nav").appendChild(nav("/Tools/Death-Notifier/"));
$("#header").appendChild(header({
title: "Death Notifier",
description: "Get notified when a famous person dies"
}));
$("#footer").appendChild(footer({
vcsURL: "https://git.fwdekker.com/tools/death-notifier/",
version: "v%%VERSION_NUMBER%%"
}));
$("main").classList.remove("hidden");
});
// Register event handlers
doAfterLoad(() => {
// Switch between logged-out and logged-in views
loginHandler.addListener(() => {
clearMessage(sharedMessageElement);
clearMessageStatus(sharedMessageElement);
refreshUserData();
refreshTrackings();
$("#loginRow").classList.add("hidden")
$("#trackingRow").classList.remove("hidden");
$("#settingsRows").classList.remove("hidden");
$("#welcome-part").classList.add("hidden")
$("#tracking-part").classList.remove("hidden");
$("#settings-part").classList.remove("hidden");
});
logoutHandler.addListener(() => {
clearMessage(sharedMessageElement);
clearMessageStatus(sharedMessageElement);
$("#loginRow").classList.remove("hidden")
$("#trackingRow").classList.add("hidden");
$("#settingsRows").classList.add("hidden");
$("#loginEmail").focus();
$("#welcome-part").classList.remove("hidden")
$("#tracking-part").classList.add("hidden");
$("#settings-part").classList.add("hidden");
$("#login-email").focus();
});
// Password visibility toggling
$a(".passwordToggle").forEach((toggle: HTMLElement) => {
$a(".password-toggle").forEach((toggle: HTMLElement) => {
const passwordField = $(`#${toggle.dataset.toggles}`);
const setState = (showPassword: boolean) => {
toggle.innerText = showPassword ? "Hide" : "Show";
passwordField.type = showPassword ? "text" : "password";
};
const setState = (showPassword: boolean) => passwordField.type = showPassword ? "text" : "password";
passwordField.form.addEventListener("reset", () => setState(false));
toggle.addEventListener("click", () => setState(passwordField.type === "password"));
});
// Message closing
$a(".formValidationInfo .closeButton").forEach((button: HTMLElement) => {
const parent = button.parentElement;
if (parent == null) return;
button.addEventListener("click", () => clearMessage(parent));
});
// Login
const loginForm = $("#loginForm");
const loginForm = $("#login-form");
loginForm.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
@ -312,8 +288,8 @@ doAfterLoad(() => {
{
action: "login",
token: csrfToken,
email: $("#loginEmail").value,
password: $("#loginPassword").value,
email: $("#login-email").value,
password: $("#login-password").value,
},
event.target as HTMLFormElement,
() => loginHandler.invokeListeners()
@ -321,10 +297,10 @@ doAfterLoad(() => {
});
loginHandler.addListener(() => {
loginForm.reset();
clearMessages(loginForm);
clearMessageStatus(loginForm);
});
const registerForm = $("#registerForm");
const registerForm = $("#register-form");
registerForm.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
@ -332,27 +308,24 @@ doAfterLoad(() => {
{
action: "register",
token: csrfToken,
email: $("#registerEmail").value,
password: $("#registerPassword").value
email: $("#register-email").value,
password: $("#register-password").value
},
registerForm,
() => {
// TODO: Add client-side form validation
registerForm.reset();
showSuccess(
$("#registerFormValidationInfo"),
"Account created successfully! You may now log in."
);
$("#loginEmail").focus();
showMessageSuccess(registerForm, "Account created successfully! You may now log in.");
$("#login-email").focus();
}
);
});
loginHandler.addListener(() => {
registerForm.reset();
clearMessages(registerForm);
clearFormValidity(registerForm);
});
const logoutForm = $("#logoutForm");
const logoutForm = $("#logout-form");
logoutForm.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
@ -371,7 +344,7 @@ doAfterLoad(() => {
// Forgot password
const sendPasswordResetForm = $("#sendPasswordResetForm");
const sendPasswordResetForm = $("#send-password-reset-form");
sendPasswordResetForm.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
@ -379,24 +352,21 @@ doAfterLoad(() => {
{
action: "send-password-reset",
token: csrfToken,
email: $("#sendPasswordResetEmail").value,
email: $("#send-password-reset-email").value,
},
sendPasswordResetForm,
() => {
sendPasswordResetForm.reset();
showSuccess(
$("#sendPasswordResetFormValidationInfo"),
"Password reset email sent successfully!"
);
showMessageSuccess(sendPasswordResetForm, "Password reset email sent successfully!");
}
);
});
loginHandler.addListener(() => {
sendPasswordResetForm.reset();
clearMessages(sendPasswordResetForm);
clearFormValidity(sendPasswordResetForm);
});
const resetPasswordForm = $("#resetPasswordForm");
const resetPasswordForm = $("#reset-password-form");
resetPasswordForm.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
@ -404,17 +374,17 @@ doAfterLoad(() => {
{
action: "reset-password",
token: csrfToken,
email: $("#resetPasswordEmail").value,
reset_token: $("#resetPasswordToken").value,
password: $("#resetPasswordPassword").value,
email: $("#reset-password-email").value,
reset_token: $("#reset-password-token").value,
password: $("#reset-password-password").value,
},
resetPasswordForm,
() => {
$("#resetPasswordForm").reset();
resetPasswordForm.reset();
redirectWithTimeout(
"./", 3, (secondsLeft) => {
showSuccess(
$("#resetPasswordFormValidationInfo"),
showMessageSuccess(
resetPasswordForm,
`Your password has been updated. You will be redirected after ${secondsLeft} second(s).`
);
}
@ -423,35 +393,35 @@ doAfterLoad(() => {
);
});
$("#forgotPasswordGoTo").addEventListener("click", (event: MouseEvent) => {
$("#forgot-password-go-to").addEventListener("click", (event: MouseEvent) => {
event.preventDefault();
$("#loginRow").classList.add("hidden");
$("#sendForgotPasswordRow").classList.remove("hidden");
$("#welcome-part").classList.add("hidden");
$("#send-forgot-password-part").classList.remove("hidden");
const resetEmail = $("#sendPasswordResetEmail");
resetEmail.value = $("#loginEmail").value;
const resetEmail = $("#send-password-reset-email");
resetEmail.value = $("#login-email").value;
resetEmail.focus();
});
$("#forgotPasswordGoBack").addEventListener("click", (event: MouseEvent) => {
$("#forgot-password-go-back").addEventListener("click", (event: MouseEvent) => {
event.preventDefault();
$("#sendPasswordResetForm").reset();
$("#sendForgotPasswordRow").classList.add("hidden");
$("#loginRow").classList.remove("hidden");
$("#send-password-reset-form").reset();
$("#send-forgot-password-part").classList.add("hidden");
$("#welcome-part").classList.remove("hidden");
const loginEmail = $("#loginEmail");
loginEmail.value = $("#sendPasswordResetEmail").value;
const loginEmail = $("#login-email");
loginEmail.value = $("#send-password-reset-email").value;
loginEmail.focus();
});
// Account management
const deleteForm = $("#deleteForm");
const deleteForm = $("#delete-form");
deleteForm.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
const actual_email = $("#deleteEmail").value;
const actual_email = $("#delete-email").value;
const entered_email = window.prompt(
`Are you sure you want to delete your account? ` +
`This action cannot be undone. ` +
@ -460,7 +430,7 @@ doAfterLoad(() => {
if (entered_email === null) {
return;
} else if (entered_email !== actual_email) {
showError($("#deleteFormValidationInfo"), "Incorrect email address.");
showMessageError(deleteForm, "Incorrect email address.");
return;
}
@ -469,13 +439,13 @@ doAfterLoad(() => {
deleteForm,
() => {
logoutHandler.invokeListeners();
showSuccess(sharedMessageElement, "Your account has been deleted.");
showMessageSuccess(sharedMessageElement, "Your account has been deleted.");
}
);
});
logoutHandler.addListener(() => clearMessages(deleteForm));
logoutHandler.addListener(() => clearFormValidity(deleteForm));
const resendEmailVerificationForm = $("#resendEmailVerificationForm");
const resendEmailVerificationForm = $("#resend-email-verification-form");
resendEmailVerificationForm.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
@ -484,17 +454,14 @@ doAfterLoad(() => {
resendEmailVerificationForm,
() => {
refreshUserData();
showSuccess(
$("#resendEmailVerificationFormValidationInfo"),
"Email verification resent successfully!"
);
showMessageSuccess(resendEmailVerificationForm, "Email verification resent successfully!");
}
);
});
logoutHandler.addListener(() => clearMessages(resendEmailVerificationForm));
logoutHandler.addListener(() => clearFormValidity(resendEmailVerificationForm));
const toggleNotificationsForm = $("#toggleNotificationsForm");
const notificationsEnabledCheckbox = $("#notificationsEnabledCheckbox");
const toggleNotificationsForm = $("#toggle-notifications-form");
const notificationsEnabledCheckbox = $("#notifications-enabled-checkbox");
notificationsEnabledCheckbox.addEventListener("change", (event: Event) => {
event.preventDefault();
const enableNotifications = notificationsEnabledCheckbox.checked;
@ -510,19 +477,19 @@ doAfterLoad(() => {
refreshUserData();
if (enableNotifications)
showSuccess($("#toggleNotificationsFormValidationInfo"), "Notifications have been enabled.");
showMessageSuccess(toggleNotificationsForm, "Notifications have been enabled.");
else
showSuccess(
$("#toggleNotificationsFormValidationInfo"),
showMessageSuccess(
toggleNotificationsForm,
"Notifications have been disabled. " +
"You will still receive security notifications, for example if you change your email address " +
"or password.");
}
);
});
logoutHandler.addListener(() => clearMessages(toggleNotificationsForm));
logoutHandler.addListener(() => clearFormValidity(toggleNotificationsForm));
const updateEmailForm = $("#updateEmailForm");
const updateEmailForm = $("#update-email-form");
updateEmailForm.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
@ -530,14 +497,14 @@ doAfterLoad(() => {
{
action: "update-email",
token: csrfToken,
email: $("#updateEmailEmail").value,
email: $("#update-email-email").value,
},
updateEmailForm,
() => {
updateEmailForm.reset();
refreshUserData();
showSuccess(
$("#updateEmailFormValidationInfo"),
showMessageSuccess(
updateEmailForm,
"Email address updated successfully! " +
"Check your inbox for the verification email. " +
"You will not receive notifications until you verify your email address."
@ -547,10 +514,10 @@ doAfterLoad(() => {
});
logoutHandler.addListener(() => {
updateEmailForm.reset();
clearMessages(updateEmailForm);
clearFormValidity(updateEmailForm);
});
const updatePasswordForm = $("#updatePasswordForm");
const updatePasswordForm = $("#update-password-form");
updatePasswordForm.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
@ -558,32 +525,32 @@ doAfterLoad(() => {
{
action: "update-password",
token: csrfToken,
password_old: $("#updatePasswordPasswordOld").value,
password_new: $("#updatePasswordPasswordNew").value,
password_old: $("#update-password-password-old").value,
password_new: $("#update-password-password-new").value,
},
event.target as HTMLFormElement,
() => {
updatePasswordForm.reset();
refreshUserData();
showSuccess($("#updatePasswordFormValidationInfo"), "Password updated successfully!");
showMessageSuccess(updatePasswordForm, "Password updated successfully!");
}
);
});
logoutHandler.addListener(() => {
updatePasswordForm.reset();
clearMessages(updatePasswordForm);
clearFormValidity(updatePasswordForm);
});
// Tracking management
const queryInput = $("#filterTrackingsQuery");
$("#filterTrackingsForm").addEventListener("submit", (event: SubmitEvent) => event.preventDefault());
const queryInput = $("#filter-trackings-query");
$("#filter-trackings-form").addEventListener("submit", (event: SubmitEvent) => event.preventDefault());
queryInput.addEventListener("input", (event: InputEvent) => {
event.preventDefault();
$("#trackingsNoMatches")?.remove();
$("#trackings-no-matches")?.remove();
const queryWords = $("#filterTrackingsQuery").value.trim().toLowerCase().split(" ");
const queryWords = queryInput.value.trim().toLowerCase().split(" ");
let foundMatches = false;
$a("#trackings tbody tr").forEach((row: HTMLTableRowElement) => {
const rowText = row.innerText.toLowerCase();
@ -596,7 +563,7 @@ doAfterLoad(() => {
if (!foundMatches) {
const row = document.createElement("tr");
row.id = "trackingsNoMatches";
row.id = "trackings-no-matches";
row.classList.add("placeholder");
const cell = document.createElement("td");
cell.colSpan = 3;
@ -605,12 +572,8 @@ doAfterLoad(() => {
$("#trackings tbody").appendChild(row);
}
});
$("#filterTrackingsClear").addEventListener("click", () => {
queryInput.value = "";
queryInput.dispatchEvent(new InputEvent("input"));
});
const addTrackingForm = $("#addTrackingForm");
const addTrackingForm = $("#add-tracking-form");
addTrackingForm.addEventListener("submit", (event: SubmitEvent) => {
event.preventDefault();
@ -618,15 +581,15 @@ doAfterLoad(() => {
{
action: "add-tracking",
token: csrfToken,
person_name: $("#addTrackingPersonName").value,
person_name: $("#add-tracking-name").value,
},
addTrackingForm,
(response: ServerResponse) => {
addTrackingForm.reset();
refreshTrackings();
showSuccess(
$("#addTrackingFormValidationInfo"),
showMessageSuccess(
addTrackingForm,
response.payload["renamed"]
? (
`Successfully added <b>${response.payload["input"]}</b> as ` +
@ -639,7 +602,7 @@ doAfterLoad(() => {
});
logoutHandler.addListener(() => {
addTrackingForm.reset();
clearMessages(addTrackingForm);
clearFormValidity(addTrackingForm);
});
});
@ -661,9 +624,11 @@ doAfterLoad(() => {
emptyFunction,
(response) => {
// Always execute the following
$("main").classList.remove("hidden");
const message = (response?.payload ?? {})["global_message"];
if (message != null)
showInfo($("#globalMessage"), message);
showMessageInfo($("#global-message"), message);
handleAction(params);
}

View File

@ -1,100 +0,0 @@
// @ts-ignore
const {$, $a} = window.fwdekker;
/**
* Shows a message of the given type.
*
* @param type the type of message to show, or `null` if the message should be hidden
* @param element the element to show the message at. This is typically an `input` element or an element with CSS class
* `formValidationInfo`. If a `label` exists with `for` attribute referring to `input`, the `label` is also
* appropriately styled
* @param message the message to display, or `undefined` if the message should be hidden. The message is shown in the
* `span` with attribute `data-for` containing the ID of `element`
*/
function showMessage(type: "info" | "success" | "warning" | "error" | null, element: HTMLElement,
message?: string): void {
element.classList.remove("hasMessage", "info", "success", "warning", "error");
if (type != null && message != null) element.classList.add("hasMessage", type);
if (element.id == null) return;
const label = $(`label[for="${element.id}"]`);
if (label != null) {
label.classList.remove("hasMessage", "info", "success", "warning", "error");
if (type != null && message != null) label.classList.add("hasMessage", type);
}
const info = $(`output[for="${element.id}"]`);
if (info != null) {
info.classList.remove("hasMessage", "info", "success", "warning", "error");
if (type != null && message != null) info.classList.add("hasMessage", type);
info.innerHTML = message ?? "";
}
const hint = $(`span[data-for="${element.id}"]`);
if (hint != null)
hint.classList.toggle("hidden", type != null && message != null);
}
/**
* Clears all validation messages from the given form.
*
* @param form the form to clear all validation messages in
*/
export function clearMessages(form: HTMLFormElement): void {
const formValidationInfo = $(`#${form.id}ValidationInfo`);
if (formValidationInfo != null) clearMessage(formValidationInfo);
$a("input", form).forEach(clearMessage);
}
/**
* Clears the validation message from the given element.
*
* @param element the element to clear the validation message from
*/
export function clearMessage(element: HTMLElement): void {
showMessage(null, element, undefined);
}
/**
* Shows an info message at the given element.
*
* @param element the element to show the info message at
* @param message the info message to display
*/
export function showInfo(element: HTMLElement, message?: string): void {
showMessage("info", element, message);
}
/**
* Shows a success message at the given element.
*
* @param element the element to show the success message at
* @param message the success message to display
*/
export function showSuccess(element: HTMLElement, message?: string): void {
showMessage("success", element, message);
}
/**
* Shows a warning message at the given element.
*
* @param element the element to show the warning message at
* @param message the warning message to display
*/
export function showWarning(element: HTMLElement, message?: string): void {
showMessage("warning", element, message);
}
/**
* Shows an error message at the given element.
*
* @param element the element to show the error message at
* @param message the error message to display
*/
export function showError(element: HTMLElement, message?: string): void {
showMessage("error", element, message);
}