fixed optional fields not validating while having a value
This commit is contained in:
@ -1,3 +1,7 @@
|
|||||||
|
# Overview
|
||||||
|
|
||||||
|
This class easily adds front-end validation to HTML forms.
|
||||||
|
|
||||||
# Demo
|
# Demo
|
||||||
|
|
||||||
A demo of the form validation class can be found [here](https://demos.danremollino.dev/form-validation/).
|
A demo of the form validation class can be found [here](https://demos.danremollino.dev/form-validation/).
|
||||||
|
@ -1,94 +0,0 @@
|
|||||||
:root {
|
|
||||||
|
|
||||||
/* ###### ##### ## ##### ###### */
|
|
||||||
/* ### ## ## ## ## ## ## ## */
|
|
||||||
/* ## ## ## ## ## ## ###### */
|
|
||||||
/* ### ## ## ## ## ## ## ## */
|
|
||||||
/* ###### ##### ####### ##### ## ## */
|
|
||||||
/*
|
|
||||||
use color-mix() with transparent to add transparency
|
|
||||||
https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/color-mix#adding_transparency
|
|
||||||
*/
|
|
||||||
|
|
||||||
&[data-color-scheme="light"] {
|
|
||||||
/* light gray */
|
|
||||||
--color-01-l: 90%;
|
|
||||||
--color-01-a: 0%;
|
|
||||||
--color-01-b: 0%;
|
|
||||||
|
|
||||||
/* black */
|
|
||||||
--color-02-l: 0%;
|
|
||||||
--color-02-a: 0%;
|
|
||||||
--color-02-b: 0%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-color-scheme="dark"] {
|
|
||||||
/* black */
|
|
||||||
--color-01-l: 0%;
|
|
||||||
--color-01-a: 0%;
|
|
||||||
--color-01-b: 0%;
|
|
||||||
|
|
||||||
/* light gray */
|
|
||||||
--color-02-l: 90%;
|
|
||||||
--color-02-a: 0%;
|
|
||||||
--color-02-b: 0%;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
--color-01: oklab(var(--color-01-l) var(--color-01-a) var(--color-01-b));
|
|
||||||
--color-01-light: oklab(calc(var(--color-01-l) + 25%) var(--color-01-a) var(--color-01-b));
|
|
||||||
--color-01-dark: oklab(calc(var(--color-01-l) - 25%) var(--color-01-a) var(--color-01-b));
|
|
||||||
|
|
||||||
--color-02: oklab(var(--color-02-l) var(--color-02-a) var(--color-02-b));
|
|
||||||
--color-02-light: oklab(calc(var(--color-02-l) + 25%) var(--color-02-a) var(--color-02-b));
|
|
||||||
--color-02-dark: oklab(calc(var(--color-02-l) - 25%) var(--color-02-a) var(--color-02-b));
|
|
||||||
|
|
||||||
--link-color-01: var(--text-color-01);
|
|
||||||
--link-decoration-01: none;
|
|
||||||
--link-hover-color-01: var(--text-color-01);
|
|
||||||
--link-hover-decoration-01: underline;
|
|
||||||
|
|
||||||
--success-color: oklab(60.735% -40.534% 31.334%);
|
|
||||||
--info-color: oklab(61.465% -23.829% -17.445%);
|
|
||||||
--warning-color: oklab(82.103% 10.93% 66.106%);
|
|
||||||
--danger-color: oklab(50.864% 51.073% 25.414%);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* ###### ###### ##### ###### ###### ## ## ###### */
|
|
||||||
/* ## ## ## ## ## ### ## ### ## ## */
|
|
||||||
/* ##### ###### ####### ## ## ## # ## ## ### */
|
|
||||||
/* ## ## ## ## ### ## ## ### ## ## */
|
|
||||||
/* ###### ## ## ## ###### ###### ## ## ##### */
|
|
||||||
|
|
||||||
|
|
||||||
--space: 1lh;
|
|
||||||
--eighth-space: calc(var(--space) * .125);
|
|
||||||
--quarter-space: calc(var(--space) * .25);
|
|
||||||
--half-space: calc(var(--space) * .5);
|
|
||||||
--three-quarter-space: calc(var(--space) * .75);
|
|
||||||
--one-and-quarter-space: calc(var(--space) * 1.25);
|
|
||||||
--one-and-half-space: calc(var(--space) * 1.5);
|
|
||||||
--one-and-three-quarter-space: calc(var(--space) * 1.75);
|
|
||||||
--double-space: calc(var(--space) * 2);
|
|
||||||
--tripple-space: calc(var(--space) * 3);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* ###### ###### ####### ###### ## ## ###### */
|
|
||||||
/* ## ## ## ## ### ## ## */
|
|
||||||
/* ##### ## # ## ## # ## ## ### */
|
|
||||||
/* ## ## ## ## ## ### ## ## */
|
|
||||||
/* ###### ###### ####### ###### ## ## ##### */
|
|
||||||
|
|
||||||
|
|
||||||
--site-max-width: 1440px;
|
|
||||||
|
|
||||||
/* grid.css */
|
|
||||||
--content-gap: var(--double-space);
|
|
||||||
--content-padding: clamp(var(--space), 5dvw, var(--tripple-space));
|
|
||||||
--site-padding: max(5dvw, 10px);
|
|
||||||
--max-content-width: calc(var(--site-max-width) - var(--content-padding) * 2);
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
@layer dialog {
|
|
||||||
[data-open-dialog]:hover { cursor: pointer; }
|
|
||||||
|
|
||||||
dialog {
|
|
||||||
border: 0;
|
|
||||||
padding: var(--half-space);
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&::backdrop {
|
|
||||||
background-color: rgb(0 0 0 / .75);
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-btn:hover { cursor: pointer; }
|
|
||||||
|
|
||||||
.close-icon {
|
|
||||||
height: 20px;
|
|
||||||
inset: var(--quarter-space) var(--quarter-space) auto auto;
|
|
||||||
position: absolute;
|
|
||||||
width: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
body:has(&[open]) { overflow: hidden; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,94 +0,0 @@
|
|||||||
@font-face {
|
|
||||||
font-family: 'Kommon Grotesk';
|
|
||||||
font-weight: 100;
|
|
||||||
src: url('../fonts/kommon_grotesk_thin.woff') format('woff'),
|
|
||||||
url('../fonts/kommon_grotesk_thin.woff2') format('woff2');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Kommon Grotesk';
|
|
||||||
font-weight: 100;
|
|
||||||
font-style: italic;
|
|
||||||
src: url('../fonts/kommon_grotesk_thin_italic.woff') format('woff'),
|
|
||||||
url('../fonts/kommon_grotesk_thin_italic.woff2') format('woff2');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Kommon Grotesk';
|
|
||||||
font-weight: 200;
|
|
||||||
src: url('../fonts/kommon_grotesk_extra_light.woff') format('woff'),
|
|
||||||
url('../fonts/kommon_grotesk_extra_light.woff2') format('woff2');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Kommon Grotesk';
|
|
||||||
font-weight: 200;
|
|
||||||
font-style: italic;
|
|
||||||
src: url('../fonts/kommon_grotesk_extra_light_italic.woff') format('woff'),
|
|
||||||
url('../fonts/kommon_grotesk_extra_light_italic.woff2') format('woff2');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Kommon Grotesk';
|
|
||||||
font-weight: 300;
|
|
||||||
src: url('../fonts/kommon_grotesk_light.woff') format('woff'),
|
|
||||||
url('../fonts/kommon_grotesk_light.woff2') format('woff2');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Kommon Grotesk';
|
|
||||||
font-weight: 300;
|
|
||||||
font-style: italic;
|
|
||||||
src: url('../fonts/kommon_grotesk_light_italic.woff') format('woff'),
|
|
||||||
url('../fonts/kommon_grotesk_light_italic.woff2') format('woff2');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Kommon Grotesk';
|
|
||||||
font-weight: 400;
|
|
||||||
src: url('../fonts/kommon_grotesk_normal.woff') format('woff'),
|
|
||||||
url('../fonts/kommon_grotesk_normal.woff2') format('woff2');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Kommon Grotesk';
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: italic;
|
|
||||||
src: url('../fonts/kommon_grotesk_italic.woff') format('woff'),
|
|
||||||
url('../fonts/kommon_grotesk_italic.woff2') format('woff2');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Kommon Grotesk';
|
|
||||||
font-weight: 500;
|
|
||||||
src: url('../fonts/kommon_grotesk_medium.woff') format('woff'),
|
|
||||||
url('../fonts/kommon_grotesk_medium.woff2') format('woff2');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Kommon Grotesk';
|
|
||||||
font-weight: 600;
|
|
||||||
src: url('../fonts/kommon_grotesk_semi_bold.woff') format('woff'),
|
|
||||||
url('../fonts/kommon_grotesk_semi_bold.woff2') format('woff2');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Kommon Grotesk';
|
|
||||||
font-weight: 700;
|
|
||||||
src: url('../fonts/kommon_grotesk_bold.woff') format('woff'),
|
|
||||||
url('../fonts/kommon_grotesk_bold.woff2') format('woff2');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Kommon Grotesk';
|
|
||||||
font-weight: 800;
|
|
||||||
src: url('../fonts/kommon_grotesk_extra_bold.woff') format('woff'),
|
|
||||||
url('../fonts/kommon_grotesk_extra_bold.woff2') format('woff2');
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Kommon Grotesk';
|
|
||||||
font-weight: 900;
|
|
||||||
src: url('../fonts/kommon_grotesk_black.woff') format('woff'),
|
|
||||||
url('../fonts/kommon_grotesk_black.woff2') format('woff2');
|
|
||||||
}
|
|
@ -1,105 +0,0 @@
|
|||||||
/* temp */
|
|
||||||
section {
|
|
||||||
/* background: gray; */
|
|
||||||
margin-block: var(--space);
|
|
||||||
}
|
|
||||||
|
|
||||||
:where(header, section, footer) > * {
|
|
||||||
background-image: url('https://res.cloudinary.com/demo/image/upload/cashew_chicken.jpg');
|
|
||||||
background-size: cover;
|
|
||||||
color: var(--color-01);
|
|
||||||
/* text-shadow: 1px 1px 3px var(--color-02); */
|
|
||||||
}
|
|
||||||
|
|
||||||
:where(header, section, footer) {
|
|
||||||
display: grid;
|
|
||||||
row-gap: var(--content-gap);
|
|
||||||
grid-template-columns:
|
|
||||||
[viewport-start] 1fr
|
|
||||||
[site-padding-start] var(--site-padding)
|
|
||||||
[content-padding-start] var(--content-padding)
|
|
||||||
[content-start]
|
|
||||||
minmax(0, calc(var(--max-content-width) / 12))
|
|
||||||
[one-twelfth]
|
|
||||||
minmax(0, calc(var(--max-content-width) / 12))
|
|
||||||
[two-twelfths one-sixth]
|
|
||||||
minmax(0, calc(var(--max-content-width) / 12))
|
|
||||||
[three-twelfths one-fourth fourth one-quarter]
|
|
||||||
minmax(0, calc(var(--max-content-width) / 12))
|
|
||||||
[four-twelfths one-third]
|
|
||||||
minmax(0, calc(var(--max-content-width) / 12))
|
|
||||||
[five-twelfths]
|
|
||||||
minmax(0, calc(var(--max-content-width) / 12))
|
|
||||||
[six-twelfths one-half half]
|
|
||||||
minmax(0, calc(var(--max-content-width) / 12))
|
|
||||||
[seven-twelfths]
|
|
||||||
minmax(0, calc(var(--max-content-width) / 12))
|
|
||||||
[eight-twelfths two-thirds]
|
|
||||||
minmax(0, calc(var(--max-content-width) / 12))
|
|
||||||
[nine-twelfths three-fourths third three-quarters]
|
|
||||||
minmax(0, calc(var(--max-content-width) / 12))
|
|
||||||
[ten-twelfths five-sixths]
|
|
||||||
minmax(0, calc(var(--max-content-width) / 12))
|
|
||||||
[eleven-twelfths]
|
|
||||||
minmax(0, calc(var(--max-content-width) / 12))
|
|
||||||
[content-end twelve-twelfths]
|
|
||||||
var(--content-padding) [content-padding-end]
|
|
||||||
var(--site-padding) [site-padding-end]
|
|
||||||
1fr [viewport-end];
|
|
||||||
|
|
||||||
> * {
|
|
||||||
display: grid;
|
|
||||||
grid-column: content-padding;
|
|
||||||
grid-template-columns: subgrid;
|
|
||||||
padding: var(--content-padding) 0;
|
|
||||||
|
|
||||||
> * { grid-column: content; }
|
|
||||||
|
|
||||||
&.full-viewport { grid-column: viewport !important; }
|
|
||||||
|
|
||||||
&.half-content-left {
|
|
||||||
grid-column-end: half !important;
|
|
||||||
padding: var(--content-padding) var(--content-padding) var(--content-padding) 0;
|
|
||||||
}
|
|
||||||
&.half-content-right {
|
|
||||||
grid-column-start: half !important;
|
|
||||||
padding: var(--content-padding) 0 var(--content-padding) var(--content-padding);
|
|
||||||
> * { grid-column-start: inherit; }
|
|
||||||
}
|
|
||||||
|
|
||||||
&.half-viewport-left {
|
|
||||||
grid-column: viewport-start / half !important;
|
|
||||||
padding: var(--content-padding) var(--content-padding) var(--content-padding) 0;
|
|
||||||
}
|
|
||||||
&.half-viewport-right {
|
|
||||||
grid-column: half / viewport-end !important;
|
|
||||||
padding: var(--content-padding) 0 var(--content-padding) var(--content-padding);
|
|
||||||
> * { grid-column-start: inherit; }
|
|
||||||
}
|
|
||||||
|
|
||||||
&.remove-padding {
|
|
||||||
grid-column: content;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.remove-inline-padding {
|
|
||||||
grid-column: content;
|
|
||||||
padding: var(--content-padding) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1240px) {
|
|
||||||
&[class*="-left"] { grid-column-end: three-quarters !important; }
|
|
||||||
&[class*="-right"] { grid-column-start: one-quarter !important; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 940px) {
|
|
||||||
&[class*="-left"] { grid-column-end: five-sixths !important; }
|
|
||||||
&[class*="-right"] { grid-column-start: one-sixth !important; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
&[class*="-left"] { grid-column-end: content-end !important; }
|
|
||||||
&[class*="-right"] { grid-column-start: content-start !important; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
7
demo/css/hamburgers.min.css
vendored
7
demo/css/hamburgers.min.css
vendored
File diff suppressed because one or more lines are too long
@ -1,7 +0,0 @@
|
|||||||
@import './reset.css' layer(reset);
|
|
||||||
@import './custom_properties.css' layer(custom-properties);
|
|
||||||
@import './tags.css' layer(tags);
|
|
||||||
@import './grid.css' layer(grid);
|
|
||||||
@import './utility_classes.css' layer(classes);
|
|
||||||
|
|
||||||
@import './fonts.css';
|
|
@ -1,114 +0,0 @@
|
|||||||
/***
|
|
||||||
The new CSS reset - version 1.11.3 (last updated 25.08.2024)
|
|
||||||
GitHub page: https://github.com/elad2412/the-new-css-reset
|
|
||||||
***/
|
|
||||||
|
|
||||||
/*
|
|
||||||
Remove all the styles of the "User-Agent-Stylesheet", except for the 'display' property
|
|
||||||
- The "symbol *" part is to solve Firefox SVG sprite bug
|
|
||||||
- The "html" element is excluded, otherwise a bug in Chrome breaks the CSS hyphens property (https://github.com/elad2412/the-new-css-reset/issues/36)
|
|
||||||
*/
|
|
||||||
*:where(:not(html, iframe, canvas, img, svg, video, audio):not(svg *, symbol *)) {
|
|
||||||
all: unset;
|
|
||||||
display: revert;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Preferred box-sizing value */
|
|
||||||
*,
|
|
||||||
*::before,
|
|
||||||
*::after {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fix mobile Safari increase font-size on landscape mode */
|
|
||||||
html {
|
|
||||||
-moz-text-size-adjust: none;
|
|
||||||
-webkit-text-size-adjust: none;
|
|
||||||
text-size-adjust: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Reapply the pointer cursor for anchor tags */
|
|
||||||
a, button {
|
|
||||||
cursor: revert;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Remove list styles (bullets/numbers) */
|
|
||||||
ol, ul, menu, summary {
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Firefox: solve issue where nested ordered lists continue numbering from parent (https://bugzilla.mozilla.org/show_bug.cgi?id=1881517) */
|
|
||||||
ol {
|
|
||||||
counter-reset: revert;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* For images to not be able to exceed their container */
|
|
||||||
img {
|
|
||||||
max-inline-size: 100%;
|
|
||||||
max-block-size: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* removes spacing between cells in tables */
|
|
||||||
table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Safari - solving issue when using user-select:none on the <body> text input doesn't working */
|
|
||||||
input, textarea {
|
|
||||||
-webkit-user-select: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* revert the 'white-space' property for textarea elements on Safari */
|
|
||||||
textarea {
|
|
||||||
white-space: revert;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* minimum style to allow to style meter element */
|
|
||||||
meter {
|
|
||||||
-webkit-appearance: revert;
|
|
||||||
appearance: revert;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* preformatted text - use only for this feature */
|
|
||||||
:where(pre) {
|
|
||||||
all: revert;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* reset default text opacity of input placeholder */
|
|
||||||
::placeholder {
|
|
||||||
color: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* fix the feature of 'hidden' attribute.
|
|
||||||
display:revert; revert to element instead of attribute */
|
|
||||||
:where([hidden]) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* revert for bug in Chromium browsers
|
|
||||||
- fix for the content editable attribute will work properly.
|
|
||||||
- webkit-user-select: auto; added for Safari in case of using user-select:none on wrapper element*/
|
|
||||||
:where([contenteditable]:not([contenteditable="false"])) {
|
|
||||||
-moz-user-modify: read-write;
|
|
||||||
-webkit-user-modify: read-write;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
-webkit-line-break: after-white-space;
|
|
||||||
-webkit-user-select: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* apply back the draggable feature - exist only in Chromium and Safari */
|
|
||||||
:where([draggable="true"]) {
|
|
||||||
-webkit-user-drag: element;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Revert Modal native behavior */
|
|
||||||
:where(dialog:modal) {
|
|
||||||
all: revert;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Remove details summary webkit styles */
|
|
||||||
::-webkit-details-marker {
|
|
||||||
display: none;
|
|
||||||
}
|
|
@ -1,166 +0,0 @@
|
|||||||
/* debug */
|
|
||||||
/* *, *::before, *::after {
|
|
||||||
outline: 2px solid lime;
|
|
||||||
} */
|
|
||||||
|
|
||||||
:root {
|
|
||||||
interpolate-size: allow-keywords;
|
|
||||||
scrollbar-color: color-mix(in oklab, var(--color-02) 40%, transparent)
|
|
||||||
color-mix(in oklab, var(--color-01) 40%, transparent);
|
|
||||||
scrollbar-gutter: stable;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: var(--color-01);
|
|
||||||
color: var(--color-02);
|
|
||||||
font: normal normal 300 clamp(1.6rem, 1.4dvw, 2.0rem)/1.25 'Helvetica', sans-serif;
|
|
||||||
letter-spacing: .25px;
|
|
||||||
overflow-x: clip;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
background: var(--color-01);
|
|
||||||
border: 1px solid var(--color-02);
|
|
||||||
color: var(--color-02);
|
|
||||||
padding: var(--quarter-space);
|
|
||||||
&:not(:last-child) { margin-bottom: var(--half-space); }
|
|
||||||
&:hover { cursor: pointer; }
|
|
||||||
}
|
|
||||||
|
|
||||||
em {
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: bolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
figcaption {
|
|
||||||
font-size: smaller;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6 { text-wrap: balance; }
|
|
||||||
|
|
||||||
html {
|
|
||||||
font-size: 62.5%;
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
}
|
|
||||||
|
|
||||||
img { display: block; }
|
|
||||||
|
|
||||||
small { font-size: .35lh; }
|
|
||||||
|
|
||||||
strong { font-weight: 600; }
|
|
||||||
|
|
||||||
svg { display: block; }
|
|
||||||
|
|
||||||
figcaption, li, ol, p {
|
|
||||||
text-wrap: balance;
|
|
||||||
text-wrap: pretty;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* ####### ##### ###### ## ## ###### */
|
|
||||||
/* ## ## ## ## ## ### ### ## */
|
|
||||||
/* ###### ## ## ###### ## # ## ##### */
|
|
||||||
/* ## ## ## ## ## ## ## ## */
|
|
||||||
/* ## ##### ## ## ## ## ###### */
|
|
||||||
|
|
||||||
form {
|
|
||||||
container: form / inline-size;
|
|
||||||
|
|
||||||
::placeholder { opacity: .65; }
|
|
||||||
|
|
||||||
fieldset {
|
|
||||||
form > & {
|
|
||||||
column-gap: clamp(var(--three-quarter-space), 3dvw, var(--space));
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
row-gap: var(--one-and-three-quarter-space);
|
|
||||||
|
|
||||||
@container form (width < 400px) {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
margin-top: -.5lh;
|
|
||||||
padding-top: .75lh;
|
|
||||||
|
|
||||||
legend {
|
|
||||||
font-weight: bolder;
|
|
||||||
position: relative;
|
|
||||||
top: .5lh;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
position: relative;
|
|
||||||
transition: background .25s linear;
|
|
||||||
|
|
||||||
input, select, textarea {
|
|
||||||
border: 1px solid var(--color-01);
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
&:not(:last-child, input[type="checkbox"], input[type="radio"]) {
|
|
||||||
margin-bottom: var(--half-space);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="checkbox"],
|
|
||||||
input[type="radio"] {
|
|
||||||
aspect-ratio: 1;
|
|
||||||
background: var(--color-02);
|
|
||||||
height: .75lh;
|
|
||||||
width: .75lh;
|
|
||||||
|
|
||||||
&:checked { background: var(--color-01); }
|
|
||||||
|
|
||||||
.form-group:has(&) {
|
|
||||||
align-content: start;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: .5lh 2ch;
|
|
||||||
|
|
||||||
& > :where(label, legend) { flex: 1 1 100%; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
&:not(:where(input[type="checkbox"], input[type="radio"]) + &,
|
|
||||||
&:has(+ :where([type="checkbox"], [type="radio"]))
|
|
||||||
) { font-weight: bolder; }
|
|
||||||
|
|
||||||
&:hover { cursor: pointer; }
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea { height: 3lh; }
|
|
||||||
|
|
||||||
:disabled, label:has(:disabled, ~ :disabled), :disabled ~ label {
|
|
||||||
border-color: color-mix(in oklab, var(--color-01) 40%, transparent);
|
|
||||||
color: color-mix(in oklab, var(--color-01) 40%, transparent);
|
|
||||||
&:hover { cursor: not-allowed; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* wrapper for checkbox/radio input and label elements */
|
|
||||||
.form-group__item {
|
|
||||||
align-items: center;
|
|
||||||
display: inline-flex;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
gap: .5ch;
|
|
||||||
|
|
||||||
& *:hover { cursor: pointer; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.required__indicator { color: var(--danger-color); }
|
|
||||||
|
|
||||||
&.invalid {
|
|
||||||
background: color-mix(in oklab, var(--danger-color) 5%, transparent);
|
|
||||||
|
|
||||||
.invalid__message {
|
|
||||||
color: var(--danger-color);
|
|
||||||
font-size: 1.0rem;
|
|
||||||
inset: calc(100% + .5lh) auto auto 0;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,56 +0,0 @@
|
|||||||
.align-center {
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.align-left {
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: auto;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.align-right {
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: 0;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.keep { white-space: nowrap; }
|
|
||||||
|
|
||||||
/* https://fajarwz.com/blog/how-to-solve-unwanted-css-transitions-on-page-load/ */
|
|
||||||
.no-transition,
|
|
||||||
.no-transition * {
|
|
||||||
-webkit-transition: none !important;
|
|
||||||
-moz-transition: none !important;
|
|
||||||
-ms-transition: none !important;
|
|
||||||
-o-transition: none !important;
|
|
||||||
transition: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@keyframes parallax-background {
|
|
||||||
to { background-position-y: -200px; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.parallax-background {
|
|
||||||
position: relative;
|
|
||||||
overflow: clip;
|
|
||||||
background: none;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
animation: parallax-background linear;
|
|
||||||
animation-timeline: view();
|
|
||||||
background-image: var(--parallax-background); /* define --parallax-background in element's style attribute */
|
|
||||||
background-position-x: center;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: cover;
|
|
||||||
content: '';
|
|
||||||
height: calc(100% + 200px);
|
|
||||||
inset: 0;
|
|
||||||
position: absolute;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,2 +0,0 @@
|
|||||||
import('./validation/validation.js')
|
|
||||||
const save = () => alert('Congrats! You successfully filled out the form. Amazing.')
|
|
@ -1,463 +0,0 @@
|
|||||||
# Form Validation
|
|
||||||
|
|
||||||
Importing the `Validation` class queries the current page for `<form>` elements
|
|
||||||
and disables the default browser [constraint validation](https://developer.mozilla.org/en-US/docs/Web/HTML/Constraint_validation)
|
|
||||||
by adding a `novalidate` attribute to that element.
|
|
||||||
|
|
||||||
The default behavior is to validate each field when their value is updated,
|
|
||||||
and validate the entire form when it is submitted. This can be changed to only
|
|
||||||
validate on form submission by setting [`onlyOnSubmit`](#onlyonsubmit) to `true` on the
|
|
||||||
instance.
|
|
||||||
|
|
||||||
Once a field is marked invalid, it will always re-validate when its value is
|
|
||||||
changed regardless of the [`onlyOnSubmit`](#onlyOnSubmit) value.
|
|
||||||
|
|
||||||
> `required` fields are tested for blank first. If this check is passed, the
|
|
||||||
field will then test for [custom rules](#custom-validation) if present. If
|
|
||||||
there are no custom rules, some fields are also tested further based on their
|
|
||||||
`type` attribute value. These are described [below](#markup).
|
|
||||||
|
|
||||||
## Required Fields
|
|
||||||
|
|
||||||
Usually, the majority of fields in a form need validation. To make setup
|
|
||||||
easier, instances of the *Validation* class have a default [`reqAll`](#reqall)
|
|
||||||
value of `true`. This marks all fields that meet the following criteria
|
|
||||||
required and needing validation:
|
|
||||||
|
|
||||||
* Have a `name` attribute.
|
|
||||||
* Not [`disabled`](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/disabled).
|
|
||||||
|
|
||||||
Indivudual fields can then be excluded from validation as described [below](#reqall).
|
|
||||||
|
|
||||||
This functionality can be inverted by setting [`reqAll`](#reqall) to `false`,
|
|
||||||
and manually giving your required fields a [`required`](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/required)
|
|
||||||
attribute.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="text">Text</label>
|
|
||||||
<input required id="text" type="text" name="text">
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
`<input>` `type` attribute values of `checkbox` and `radio` only need one option to have the
|
|
||||||
`required` attribute, however, it is recommended to add it to all options for
|
|
||||||
consistency. More information on checkbox and radio fields can be found [below](#checkbox-and-radio-fields).
|
|
||||||
|
|
||||||
### Required Indicator
|
|
||||||
|
|
||||||
A `<span>` tag with class of `required__indicator` containing an asterisk(*)
|
|
||||||
character will be inserted [`beforeend`](https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML#beforeend)
|
|
||||||
of a required field's `<label>` element.
|
|
||||||
|
|
||||||
This indicator will be inserted to the `<legend>` element of `checkbox` and
|
|
||||||
`radio` `<input>` types if present as described [below](#checkbox-and-radio-fields).
|
|
||||||
|
|
||||||
The indicator can be disabled or customized as described in [Form Options](#form-options).
|
|
||||||
|
|
||||||
## Markup
|
|
||||||
|
|
||||||
The `Validation` class requires the following for each required field:
|
|
||||||
- All form fields to be wrapped in an element with the `form-group` class.
|
|
||||||
- Do **NOT** nest the required field in a `<label>` element.
|
|
||||||
- Unique, matching values of the required field's `id` and its `<label>`'s `for`
|
|
||||||
attribute.
|
|
||||||
- A unique `name` attribute on the required field. `checkbox` and `radio`
|
|
||||||
`<input>` types should share the same unique `name` attribute.
|
|
||||||
|
|
||||||
> When a field fails validation the wrapper `.form-group` element will receive
|
|
||||||
an `invalid` CSS class. Use this to write your own CSS to indicate an invalid
|
|
||||||
state.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="text">Text</label>
|
|
||||||
<input id="text" type="text" name="text">
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Checkbox and Radio Fields
|
|
||||||
|
|
||||||
- Wrap all options in a `<fieldset>` element and add the `form-group` class to
|
|
||||||
it instead of the individual options.
|
|
||||||
- The `<legend>` tag may be used to give the option set a heading.
|
|
||||||
- Each `checkbox` or `radio` `<input>` type should be wrapped in a
|
|
||||||
`form-group__item` class. This class is not used programattically, and is only
|
|
||||||
used for styling.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<fieldset class="form-group">
|
|
||||||
<legend>Check Group</legend>
|
|
||||||
<span class="form-group__item">
|
|
||||||
<input id="checkbox-1" type="checkbox" name="checks">
|
|
||||||
<label for="checkbox-1">Checkbox 1</label>
|
|
||||||
</span>
|
|
||||||
<span class="form-group__item">
|
|
||||||
<input id="checkbox-2" type="checkbox" name="checks">
|
|
||||||
<label for="checkbox-2">Checkbox 2</label>
|
|
||||||
</span>
|
|
||||||
</fieldset>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Select Fields
|
|
||||||
|
|
||||||
Required `<select>` elements should contain an `<option>` with the `selected`
|
|
||||||
attribute and an empty `value` attribute. If not present the element will pass
|
|
||||||
validation even while being required.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="select">Select</label>
|
|
||||||
<select id="select" name="select">
|
|
||||||
<option selected value="">Choose an Item</option>
|
|
||||||
<option>1</option>
|
|
||||||
<option>2</option>
|
|
||||||
<option>3</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Password Fields
|
|
||||||
|
|
||||||
When an `<input>` element with a `type` attribute of `password` is detected on the
|
|
||||||
page, a required *Password Confirm* field will auto generate after its parent
|
|
||||||
`.form-group` wrapper.
|
|
||||||
|
|
||||||
The markup for this field is a duplicate of the original input's parent
|
|
||||||
`.form-group` wrapper. The `id` and `name` attributes of the `<input>` element,
|
|
||||||
and the `for` attribute of the `<label>` element will have their values set to
|
|
||||||
`password-confirm`.
|
|
||||||
|
|
||||||
This functionality can be disabled by settting [`passConfirm`](#passConfirm) to `false`.
|
|
||||||
|
|
||||||
A *Password* field with the markup of
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="password">Password</label>
|
|
||||||
<input id="password" type="password" name="password">
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
will generate a *Password Confirm* field with the markup of
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="password-confirm">Password Confirm<span class="required__indicator">*</span></label>
|
|
||||||
<input id="password-confirm" type="password" name="password-confirm">
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
> The *Password* field is tested that it matches its corresponding
|
|
||||||
*Password Confirm* field when the *Password Confirm* field contains a non-blank
|
|
||||||
value.
|
|
||||||
<br><br>
|
|
||||||
The *Password Confirm* field is always tested that it matches its corresponding
|
|
||||||
*Password* field.
|
|
||||||
|
|
||||||
## Custom Form Controls
|
|
||||||
|
|
||||||
[Custom form controls](https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Forms/How_to_build_custom_form_controls)
|
|
||||||
can be validated by informing the ValidationField class what you want to check
|
|
||||||
against as the value for your control. You can determine validity based on:
|
|
||||||
|
|
||||||
1. [Existence of an element.](#existence-of-an-element)
|
|
||||||
2. [TextContent of an element.](#textcontent-of-an-element)
|
|
||||||
3. [Existence of an attribute.](#existence-of-an-attribute)
|
|
||||||
4. [Value of an attribute.](#value-of-an-attribute)
|
|
||||||
|
|
||||||
To validate a custom form control, add a `data-validation` attribute to an
|
|
||||||
element of the control. The value should be a single quoted JSON object with
|
|
||||||
`valueSrc` as a property.
|
|
||||||
|
|
||||||
> The element that the `data-validation` attribute is added to **MUST** have a
|
|
||||||
unique `id` and `name` attribute. Having an associated `label`/`legend` tag
|
|
||||||
with a `for` attribute is optional, but recommended.
|
|
||||||
|
|
||||||
The value of the property should be an array with up to three items.
|
|
||||||
|
|
||||||
`valueSrc[0]:` A CSS selector to target the element to reference for value.<br>
|
|
||||||
`valueSrc[1]:` The keyword `attribute`, `attributeValue`, `exists`, or
|
|
||||||
`textContent`.<br>
|
|
||||||
`valueSrc[2]:` If using `attribute` or `attributeValue`, the attribute to
|
|
||||||
reference. If using `exists`, a CSS selector relative to `valueSrc[0]` as the
|
|
||||||
parent.
|
|
||||||
|
|
||||||
`valueSrc` can be combined with [`pattern`](#pattern) and [`patternMessage`](#patternmessage)
|
|
||||||
field options to check for specific values and output custom error messages.
|
|
||||||
See the examples below.
|
|
||||||
|
|
||||||
### Existence of an Element
|
|
||||||
|
|
||||||
Determine validity based on whether an element exists or not.
|
|
||||||
|
|
||||||
*Valid if...* the target element exists.
|
|
||||||
|
|
||||||
The following example checks for the presence of in image element that is a
|
|
||||||
child of the element with an id of 'custom-control'.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="custom-control">Custom Form Control</label>
|
|
||||||
<div data-validation='{"valueSrc": ["#custom-control", "exists", "img"]}' id="custom-control" name="custom_control"></div>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
> The third item in the `valueSrc` array can be any valid CSS selector string
|
|
||||||
that [`querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)
|
|
||||||
can parse. e.x. `:scope > img`
|
|
||||||
|
|
||||||
### TextContent of an Element
|
|
||||||
|
|
||||||
Use the target element's [`textContent`](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent)
|
|
||||||
to determine validity. Useful when using [`contenteditable`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/contenteditable).
|
|
||||||
|
|
||||||
*Valid if...* the target element's textContent property is a non-empty string.
|
|
||||||
|
|
||||||
The following example checks if the element with an id of 'custom-control' has
|
|
||||||
a non-empty string [`textContent`](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent)
|
|
||||||
property.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="custom-control">Custom Form Control</label>
|
|
||||||
<div data-validation='{"valueSrc": ["#custom-control", "textContent"]}' id="custom-control" name="custom_control" contenteditable></div>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Existence of an Attribute
|
|
||||||
|
|
||||||
Use the target element's specified attribute existence to determine validity.
|
|
||||||
|
|
||||||
*Valid if...* the target element's specified attribute exists.
|
|
||||||
|
|
||||||
The following example checks if the element with an id of 'custom-control' has
|
|
||||||
a `title` attribute.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="custom-control">Custom Form Control</label>
|
|
||||||
<div data-validation='{"valueSrc": ["#custom-control", "attribute", "title"]}' id="custom-control" name="custom_control"></div>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
> The value of a [boolean attribute](https://developer.mozilla.org/en-US/docs/Glossary/Boolean/HTML)
|
|
||||||
will evaluate as `false` and invalid unless it is a non-empty string. When
|
|
||||||
using this option, the attribute must use the `checked="checked"` (true, valid)
|
|
||||||
and `checked=""` (false, invalid) form and not `checked`. `checked` will
|
|
||||||
evaluate as false and invalid whether present or not.
|
|
||||||
|
|
||||||
### Value of an Attribute
|
|
||||||
|
|
||||||
Use the target element's specified attribute value to determine validity.
|
|
||||||
|
|
||||||
*Valid if...* the value of the target's specified attribute is a non-empty string.
|
|
||||||
|
|
||||||
The following example checks if the element with an id of 'custom-control' has
|
|
||||||
a `title` attribute with a non-empty string value.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="custom-control">Custom Form Control</label>
|
|
||||||
<div data-validation='{"valueSrc": ["#custom-control", "attributeValue", "title"]}' id="custom-control" name="custom_control"></div>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
> The value of a [boolean attribute](https://developer.mozilla.org/en-US/docs/Glossary/Boolean/HTML)
|
|
||||||
will evaluate as `false` and invalid unless it is a non-empty string. When
|
|
||||||
using this option, the attribute must use the `checked="checked"` (true, valid)
|
|
||||||
and `checked=""` (false, invalid) syntax. If you need to check a boolean
|
|
||||||
attribute use [`attribute`](#existence-of-an-attribute).
|
|
||||||
|
|
||||||
## Form Options
|
|
||||||
|
|
||||||
* [invalidClass](#invalidclass)
|
|
||||||
* [messageClass](#messageclass)
|
|
||||||
* [onlyOnSubmit](#onlyonsubmit)
|
|
||||||
* [onSuccess](#onsuccess)
|
|
||||||
* [passConfirm](#passconfirm)
|
|
||||||
* [reqAll](#reqall)
|
|
||||||
* [reqIndicator](#reqindicator)
|
|
||||||
* [reqIndicators](#reqindicators)
|
|
||||||
* [reqIndicatorClass](#reqindicatorclass)
|
|
||||||
* [valFieldsOn](#valfieldson)
|
|
||||||
|
|
||||||
Default instance options can be overwritten by adding a single quoted
|
|
||||||
`data-validation` attribute on the `<form>` being validated. Its value must be
|
|
||||||
a properly formatted JSON object.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<form data-validation='{"onlyOnSubmit": true, "reqIndicator": "❗️"}'>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="text">Text</label>
|
|
||||||
<input id="text" type="text" name="text">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<button type="submit">Submit</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
```
|
|
||||||
|
|
||||||
### invalidClass
|
|
||||||
|
|
||||||
**Default:** `invalid`
|
|
||||||
|
|
||||||
The class assigned to the parent `<div class="form-group" />` wrapper of the
|
|
||||||
invalid field.
|
|
||||||
|
|
||||||
### messageClass
|
|
||||||
|
|
||||||
**Default:** `invalid__message`
|
|
||||||
|
|
||||||
The class assigned output message of the invalid field.
|
|
||||||
|
|
||||||
### onlyOnSubmit
|
|
||||||
|
|
||||||
**Default:** `false`
|
|
||||||
|
|
||||||
Only validate the form on submission. A setting of `true` will disable
|
|
||||||
individual field validation. Regardless of this setting fields will always
|
|
||||||
validate on the [`valFieldsOn`](#valFieldsOn) value while they are invalid.
|
|
||||||
|
|
||||||
### onSuccess
|
|
||||||
|
|
||||||
**Default:** `null`
|
|
||||||
|
|
||||||
Run a script when the form is submited and all fields pass validation. A non
|
|
||||||
truthy value will cancel the form's default action.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<form data-validation='{"onSuccess": "save()"}'>...</form>
|
|
||||||
```
|
|
||||||
|
|
||||||
### passConfirm
|
|
||||||
|
|
||||||
**Default:** `true`
|
|
||||||
|
|
||||||
Automatically create a confirm password field. The markup from the user created
|
|
||||||
field with a `type="password"` attribute will be duplicated and modifed, then
|
|
||||||
inserted after the original field.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<form data-validation='{"passConfirm": true}'>...</form>
|
|
||||||
```
|
|
||||||
|
|
||||||
### reqAll
|
|
||||||
|
|
||||||
**Default:** `true`
|
|
||||||
|
|
||||||
Automatically add a `required` attribute to all `<input>`, `<select>`, and
|
|
||||||
`<textarea>` tags in the `<form>`. This allows you to skip marking individual
|
|
||||||
fields as `required` in your markup.
|
|
||||||
|
|
||||||
> Individual fields can be excluded from validation by adding a
|
|
||||||
`data-validation` attribute to a field that's value is a JSON object with an
|
|
||||||
[`optional`](#optional) property value set to `true`.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="not-required">Not Required</label>
|
|
||||||
<input data-validation='{"optional": true}' id="not-required" type="text" name="not_required">
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
### reqIndicator
|
|
||||||
|
|
||||||
**Default:** `*`
|
|
||||||
|
|
||||||
The string to render after the label text to indicate to the user that the
|
|
||||||
field is required.
|
|
||||||
|
|
||||||
### reqIndicators
|
|
||||||
|
|
||||||
**Default:** `true`
|
|
||||||
|
|
||||||
Enable the output of `reqIndicator` strings.
|
|
||||||
|
|
||||||
If set to `false`, you can target all field labels with the CSS selector below
|
|
||||||
to apply a custom style.
|
|
||||||
|
|
||||||
```css
|
|
||||||
.form-group:has([required])
|
|
||||||
:where(label:has(:not(.form-group__item)), legend) {
|
|
||||||
/* Your styles here. */
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### reqIndicatorClass
|
|
||||||
|
|
||||||
**Default:** `required__indicator`
|
|
||||||
|
|
||||||
The class assigned to the `<span>` wrapper of the outputted `reqIndicator`.
|
|
||||||
|
|
||||||
### valFieldsOn
|
|
||||||
|
|
||||||
**Default:** `input`
|
|
||||||
|
|
||||||
The event that triggeers individual field validation. Value can be any
|
|
||||||
JavaScript event, but it is recommended to try
|
|
||||||
[`blur`](https://developer.mozilla.org/en-US/docs/Web/API/Element/blur_event),
|
|
||||||
[`change`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/change_event),
|
|
||||||
or [`input`](https://developer.mozilla.org/en-US/docs/Web/API/Element/input_event).
|
|
||||||
|
|
||||||
> Individual field validation can be disable by setting
|
|
||||||
[`onlyOnSubmit`](#onlyOnSubmit) to true.
|
|
||||||
|
|
||||||
## Field Options
|
|
||||||
|
|
||||||
* [optional](#optional)
|
|
||||||
* [pattern](#pattern)
|
|
||||||
* [patternMessage](#patternmessage)
|
|
||||||
* [valueSrc](#valuesrc)
|
|
||||||
|
|
||||||
Individual fields can be passed options to customize their behavior by adding a
|
|
||||||
single quoted `data-validation` attribute to a field. Its value must be a
|
|
||||||
properly formatted JSON object with the properties and values below.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="custom">Custom Regex</label>
|
|
||||||
<input data-validation='{"optional": true,
|
|
||||||
"pattern": "^[0-9]*$",
|
|
||||||
"patternMessage": "This field must only contain numbers."
|
|
||||||
}'
|
|
||||||
id="custom" type="text" inputmode="numeric" name="custom">
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
### optional
|
|
||||||
|
|
||||||
**Default:** `false`
|
|
||||||
|
|
||||||
Fields with the `optional` property set to `true` will pass vaidation when
|
|
||||||
blank. They still will be processed for validation when they contain a value
|
|
||||||
based on the `input` `type` attribute, or if a user [`pattern`](#pattern) is specified.
|
|
||||||
|
|
||||||
### pattern
|
|
||||||
|
|
||||||
**Default:** `null`
|
|
||||||
|
|
||||||
A regular expression to test the field's value against. This string is also
|
|
||||||
copied and added to the field as a native [`pattern`](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/pattern)
|
|
||||||
attribute. It is recommended to also provide an error message to display when
|
|
||||||
the value does not match `pattern` by providing a [`patternMessage`](#patternmessage).
|
|
||||||
|
|
||||||
> The pattern is checked when input is present. A required field will always
|
|
||||||
first test for blank, then for the provided pattern.
|
|
||||||
|
|
||||||
### patternMessage
|
|
||||||
|
|
||||||
**Default:** `null`
|
|
||||||
|
|
||||||
An error message to display when the field fails the custom validation test
|
|
||||||
provided by [`pattern`](#pattern).
|
|
||||||
|
|
||||||
> If not provided with [`pattern`](#pattern) the field will be marked as
|
|
||||||
invalid, but no message will output when the pattern does not match.
|
|
||||||
|
|
||||||
### valueSrc
|
|
||||||
|
|
||||||
**Default:** `null`
|
|
||||||
|
|
||||||
Used to validate [custom form controls](https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Forms/How_to_build_custom_form_controls).
|
|
||||||
See [here](#custom-form-controls) for more information.
|
|
@ -1,450 +0,0 @@
|
|||||||
class ValidationField {
|
|
||||||
el = null
|
|
||||||
formGroupEl = null
|
|
||||||
isValid = null
|
|
||||||
labelEl = null
|
|
||||||
labelText = null
|
|
||||||
message = null
|
|
||||||
messageOutputted = null
|
|
||||||
optional = false
|
|
||||||
pattern = null
|
|
||||||
patternMessage = null
|
|
||||||
validationInstance = null
|
|
||||||
valueSrc = [] // [css_selector, 'attribute|attributeValue'|'exists'|'textContent', attribute_name|element]
|
|
||||||
|
|
||||||
constructor(obj) {
|
|
||||||
try {
|
|
||||||
Object.assign(this, obj)
|
|
||||||
|
|
||||||
this.formGroupEl = this.el.closest('.form-group')
|
|
||||||
this.labelEl = this.formGroupEl?.querySelector('legend') ??
|
|
||||||
this.validationInstance.form.querySelector(
|
|
||||||
`[name="${this.el.getAttribute('name')}"]:not([type="hidden"])`
|
|
||||||
).closest('.form-group').querySelector('label')
|
|
||||||
this.labelText = this.labelEl?.textContent.replace(this.validationInstance.reqIndicator, '')
|
|
||||||
|
|
||||||
// user field config
|
|
||||||
try {
|
|
||||||
Object.assign(this, this.el.dataset.validation ? JSON.parse(this.el.dataset.validation) : {})
|
|
||||||
} catch(error) {
|
|
||||||
if (error instanceof SyntaxError) {
|
|
||||||
console.warn(`There was a problem configuring your field options. Using defaults. Are you using a properly formatted JSON object? (e.g. data-validation='{"optional": true}')\n\nElement: ${this.el.outerHTML}`)
|
|
||||||
}
|
|
||||||
delete this.el.dataset.validation
|
|
||||||
}
|
|
||||||
|
|
||||||
// custom form controls
|
|
||||||
if (this.valueSrc.length > 0) {
|
|
||||||
const valueProps = {
|
|
||||||
valueRef: this.validationInstance.form.querySelector(this.valueSrc[0]),
|
|
||||||
kind: this.valueSrc[1],
|
|
||||||
attr: this.valueSrc[2]
|
|
||||||
}
|
|
||||||
|
|
||||||
let observerOpts
|
|
||||||
const updateValue = (valueProps) => {
|
|
||||||
switch (valueProps.kind) {
|
|
||||||
case 'attribute':
|
|
||||||
valueProps.valueRef.hasAttribute(valueProps.attr)
|
|
||||||
? this.el.setAttribute('value', '1')
|
|
||||||
: this.el.setAttribute('value', '')
|
|
||||||
observerOpts = {attributeFilter: [valueProps.attr]}
|
|
||||||
break
|
|
||||||
case 'attributeValue':
|
|
||||||
valueProps.valueRef.getAttribute(valueProps.attr)
|
|
||||||
? this.el.setAttribute('value', valueProps.valueRef.getAttribute(valueProps.attr))
|
|
||||||
: this.el.setAttribute('value', '')
|
|
||||||
observerOpts = {attributeFilter: [valueProps.attr]}
|
|
||||||
break
|
|
||||||
case 'exists':
|
|
||||||
valueProps.valueRef.querySelector(valueProps.attr)
|
|
||||||
? this.el.setAttribute('value', '1')
|
|
||||||
: this.el.setAttribute('value', '')
|
|
||||||
observerOpts = {childList: true}
|
|
||||||
break
|
|
||||||
case 'textContent':
|
|
||||||
this.el.setAttribute('value', valueProps.valueRef?.textContent.trim())
|
|
||||||
observerOpts = {characterData: true, childList: true, subtree: true}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateValue(valueProps)
|
|
||||||
|
|
||||||
const custConObserver = new MutationObserver((mutationList, observer) => {
|
|
||||||
updateValue(valueProps)
|
|
||||||
this.validationInstance.validate(this)
|
|
||||||
})
|
|
||||||
|
|
||||||
custConObserver.observe(valueProps.valueRef, observerOpts)
|
|
||||||
|
|
||||||
// focus on the value target when the label is clicked
|
|
||||||
this.labelEl.addEventListener('click', () => {
|
|
||||||
valueProps.valueRef.focus()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate individual fields according to the parent Validation
|
|
||||||
// instance `valFieldsOn` value
|
|
||||||
this.el.addEventListener(this.validationInstance.valFieldsOn, () => {
|
|
||||||
this.validationInstance.validate(this)
|
|
||||||
})
|
|
||||||
|
|
||||||
// add a native pattern attr if pattern property is present
|
|
||||||
if (this.pattern) this.el.setAttribute('pattern', this.pattern)
|
|
||||||
|
|
||||||
// add required indicator
|
|
||||||
if (this.validationInstance.reqIndicators) {
|
|
||||||
const hasIndicator = this.formGroupEl?.querySelector(`.${this.validationInstance.reqIndicatorClass}`)
|
|
||||||
if (!hasIndicator && !this.optional) {
|
|
||||||
this.labelEl?.insertAdjacentHTML(
|
|
||||||
'beforeend',
|
|
||||||
`<span class="${this.validationInstance.reqIndicatorClass}">${this.validationInstance.reqIndicator}</span>`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate the field without outputting errors to set the default state
|
|
||||||
this.validationInstance.validate(this, false, true)
|
|
||||||
|
|
||||||
// console.log(this)
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`Something when wrong setting up a ValidationField instance.`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Validation {
|
|
||||||
static forms = []
|
|
||||||
|
|
||||||
errors = []
|
|
||||||
form = null
|
|
||||||
invalidClass = 'invalid'
|
|
||||||
messageClass = 'invalid__message'
|
|
||||||
onlyOnSubmit = false
|
|
||||||
onSuccess = null
|
|
||||||
passConfirm = true
|
|
||||||
passConfirmField = null
|
|
||||||
passwordField = null
|
|
||||||
reqAll = true
|
|
||||||
reqIndicator = '*'
|
|
||||||
reqIndicators = true
|
|
||||||
reqIndicatorClass = 'required__indicator'
|
|
||||||
reqData = new Set()
|
|
||||||
reqFields = []
|
|
||||||
valFieldsOn = 'input' // blur, change, input
|
|
||||||
|
|
||||||
constructor(obj) {
|
|
||||||
Object.assign(this, obj)
|
|
||||||
|
|
||||||
// check for custom form controls
|
|
||||||
const custFormControls = this.form.querySelectorAll('[data-validation]:not(form, input, select, textarea)')
|
|
||||||
if (custFormControls.length > 0) {
|
|
||||||
for (const el of custFormControls) {
|
|
||||||
try {
|
|
||||||
const fieldOptions = JSON.parse(el.dataset.validation)
|
|
||||||
if (!this.form.querySelector(fieldOptions.valueSrc[0])) {
|
|
||||||
throw new Error(`No element matched with the supplied selector – ${fieldOptions.valueSrc[0]}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
let inputEl = document.createElement('input')
|
|
||||||
inputEl.dataset.validation = el.dataset.validation
|
|
||||||
let inputAttrs = {name: el.getAttribute('name'), type: 'hidden', value: ''}
|
|
||||||
for (const [key, value] of Object.entries(inputAttrs)) {
|
|
||||||
inputEl[key] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
el.parentElement.prepend(inputEl)
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`There was a problem configuring your custom form control. Aborted.\n\nError: ${error.message}.\n\nElement: ${el.outerHTML}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// add confirm password field if a password field is present
|
|
||||||
if (this.passConfirm) {
|
|
||||||
const pwField = this.form.querySelector('input[type="password"]')
|
|
||||||
if (pwField) {
|
|
||||||
this.passwordField = new ValidationField({el: pwField, validationInstance: this})
|
|
||||||
const passConfirmGroup = this.passwordField.formGroupEl.cloneNode(true)
|
|
||||||
for (const child of passConfirmGroup.children) {
|
|
||||||
switch (child.tagName) {
|
|
||||||
case 'LABEL':
|
|
||||||
child.innerText = 'Password Confirm'
|
|
||||||
child.htmlFor = 'password-confirm'
|
|
||||||
break
|
|
||||||
case 'INPUT':
|
|
||||||
child.setAttribute('id', 'password-confirm')
|
|
||||||
child.setAttribute('name', 'password-confirm')
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.passwordField.formGroupEl.insertAdjacentHTML('afterend', passConfirmGroup.outerHTML)
|
|
||||||
this.passConfirmField = new ValidationField({el: this.form.querySelector('input[name="password-confirm"]'), validationInstance: this})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// add required attr to fields if reqAll == true
|
|
||||||
if (this.reqAll) {
|
|
||||||
for (const field of this.form.querySelectorAll('input, select, textarea')) {
|
|
||||||
field.setAttribute('required', 'required')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check for fields that are elidgeable for validation and make a set of unique name attr values
|
|
||||||
for (const field of this.form.querySelectorAll(':where(input, select, textarea)[required][name]:not([disabled])')) {
|
|
||||||
this.reqData.add(field.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// create a list of all the field elements that were specified for validation
|
|
||||||
for (const nameAttr of this.reqData) {
|
|
||||||
const fields = this.form.querySelectorAll(`[name="${nameAttr}"]:where(input, select, textarea)`)
|
|
||||||
for (const el of fields) {
|
|
||||||
this.reqFields.push(new ValidationField({el: el, validationInstance: this}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.form.addEventListener('submit', e => {
|
|
||||||
e.preventDefault()
|
|
||||||
this.validate()
|
|
||||||
})
|
|
||||||
|
|
||||||
this.form.addEventListener('reset', () => {
|
|
||||||
this.removeErrors()
|
|
||||||
})
|
|
||||||
|
|
||||||
// add/remove a field when disabled attr is dynamically toggled
|
|
||||||
const observeEls = this.form.querySelectorAll('input, select, textarea, [data-validation]:not(form, input, select, textarea)')
|
|
||||||
const disabledObserver = new MutationObserver((mutationList, observer) => {
|
|
||||||
for (const item of mutationList) {
|
|
||||||
let updateEl = item.target
|
|
||||||
|
|
||||||
if (item.target.dataset.validation?.includes('valueSrc')) {
|
|
||||||
// custom form control – update the hidden input instead
|
|
||||||
updateEl = this.form.querySelector(
|
|
||||||
`input[name="${item.target.getAttribute('name')}"][type="hidden"]`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateEl.hasAttribute('disabled')
|
|
||||||
? this.removeField(updateEl)
|
|
||||||
: this.addField(updateEl)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
for (const el of observeEls) {
|
|
||||||
disabledObserver.observe(el, {
|
|
||||||
attributes: true,
|
|
||||||
attributeFilter: ['disabled'],
|
|
||||||
attributeOldValue: true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.log(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
// add a field after initialization
|
|
||||||
addField(el) {
|
|
||||||
this.reqData.add(el.name)
|
|
||||||
this.reqFields.push(new ValidationField({el: el, validationInstance: this}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove a field after initialization
|
|
||||||
removeField(el) {
|
|
||||||
this.reqData.delete(el.name)
|
|
||||||
for (const obj of this.reqFields) {
|
|
||||||
if (el === obj.el) {
|
|
||||||
obj.formGroupEl.querySelector(`.${this.reqIndicatorClass}`)?.remove()
|
|
||||||
this.reqFields.pop(obj)
|
|
||||||
this.removeErrors(obj)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parseField(fieldInstance) {
|
|
||||||
const nameAttrVal = fieldInstance.el.name
|
|
||||||
|
|
||||||
if (!fieldInstance.optional) {
|
|
||||||
if (!fieldInstance.el.value) {
|
|
||||||
this.validateForBlank(fieldInstance)
|
|
||||||
} else {
|
|
||||||
if (fieldInstance.pattern) {
|
|
||||||
this.validatePattern(fieldInstance)
|
|
||||||
} else {
|
|
||||||
switch (fieldInstance.el.type) {
|
|
||||||
case 'checkbox':
|
|
||||||
this.validateCheckbox(fieldInstance, nameAttrVal)
|
|
||||||
break
|
|
||||||
case 'email':
|
|
||||||
this.validateEmail(fieldInstance)
|
|
||||||
break
|
|
||||||
case 'password':
|
|
||||||
if (fieldInstance.el.name === 'password') this.validatePassword(fieldInstance)
|
|
||||||
if (fieldInstance.el.name === 'password-confirm') this.validateConfirmPass(fieldInstance)
|
|
||||||
break
|
|
||||||
case 'radio':
|
|
||||||
this.validateRadio(fieldInstance, nameAttrVal)
|
|
||||||
break
|
|
||||||
case 'tel':
|
|
||||||
this.validateTel(fieldInstance)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
validateForBlank(fieldInstance) {
|
|
||||||
this.addError(
|
|
||||||
fieldInstance,
|
|
||||||
(fieldInstance.labelText ? fieldInstance.labelText : `Field`) + ` is required.`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
validatePattern(fieldInstance) {
|
|
||||||
const regex = new RegExp(fieldInstance.pattern)
|
|
||||||
if (!regex.test(fieldInstance.el.value)) {
|
|
||||||
this.addError(fieldInstance, fieldInstance.patternMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
validateCheckbox(fieldInstance, nameAttrVal) {
|
|
||||||
if (!this.form.querySelector(`[name="${nameAttrVal}"]:checked`)) {
|
|
||||||
this.addError(fieldInstance, `At least one choice is required.`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
validateEmail(fieldInstance) {
|
|
||||||
const regex = new RegExp(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/)
|
|
||||||
if (!regex.test(fieldInstance.el.value)) {
|
|
||||||
this.addError(fieldInstance, `'${fieldInstance.el.value}' is not a valid email address.`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
validatePassword(fieldInstance) {
|
|
||||||
if (this.passConfirmField.el.value) {
|
|
||||||
if (this.passConfirmField.el.value !== this.passwordField.el.value) {
|
|
||||||
this.validate(this.passConfirmField)
|
|
||||||
} else {
|
|
||||||
this.removeErrors(this.passConfirmField)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// minimum eight characters, at least one upper case English letter,
|
|
||||||
// one lower case English letter, one number and one special character
|
|
||||||
const regex = new RegExp(/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$ %^&*-]).{8,}$/)
|
|
||||||
if (!regex.test(fieldInstance.el.value)) {
|
|
||||||
this.addError(fieldInstance, `Password is not secure.`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
validateConfirmPass(fieldInstance) {
|
|
||||||
if (fieldInstance.el.value !== this.passwordField.el.value) {
|
|
||||||
this.removeErrors(fieldInstance)
|
|
||||||
this.addError(fieldInstance, `Does not match Password field.`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
validateRadio(fieldInstance, nameAttrVal) {
|
|
||||||
if (!this.form.querySelector(`[name="${nameAttrVal}"]:checked`)) {
|
|
||||||
this.addError(fieldInstance, `A choice is required.`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
validateTel(fieldInstance) {
|
|
||||||
// 1234567890, 123-456-7890, (123)456-7890, (123) 456-7890
|
|
||||||
const regex = new RegExp(/^\(?(\d{3})\)?[- ]?(\d{3})[- ]?(\d{4})$/)
|
|
||||||
if (!regex.test(fieldInstance.el.value)) {
|
|
||||||
this.addError(fieldInstance, `'${fieldInstance.el.value}' is not a valid telephone number.`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addError(fieldInstance, message) {
|
|
||||||
fieldInstance.isValid = false
|
|
||||||
fieldInstance.message = message
|
|
||||||
this.errors.push(fieldInstance)
|
|
||||||
}
|
|
||||||
|
|
||||||
outputErrors(fieldInstance) {
|
|
||||||
for (const error of this.errors) {
|
|
||||||
if (!fieldInstance || error.el === fieldInstance.el) {
|
|
||||||
error.formGroupEl.classList.add(`${this.invalidClass}`)
|
|
||||||
if (error.message) {
|
|
||||||
error.el.insertAdjacentHTML(
|
|
||||||
'beforeBegin',
|
|
||||||
`<div class="${this.messageClass}">${error.message}</div>`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
error.messageOutputted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removeErrors(fieldInstance) {
|
|
||||||
const updateMarkup = valField => {
|
|
||||||
valField.formGroupEl.classList.remove(`${this.invalidClass}`)
|
|
||||||
valField.formGroupEl.querySelector(`.${this.messageClass}`)?.remove()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fieldInstance) {
|
|
||||||
for (const [index, error] of this.errors.entries()) {
|
|
||||||
if (error.el.name === fieldInstance.el.name) {
|
|
||||||
this.errors.splice(index, 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
updateMarkup(fieldInstance)
|
|
||||||
fieldInstance.isValid = true
|
|
||||||
fieldInstance.messageOutputted = false
|
|
||||||
} else {
|
|
||||||
for (const error of this.errors) {
|
|
||||||
updateMarkup(error)
|
|
||||||
}
|
|
||||||
this.errors.length = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
validate(fieldInstance, outputErrors = true, force = false) {
|
|
||||||
if (fieldInstance) {
|
|
||||||
if (!this.onlyOnSubmit || (this.onlyOnSubmit && fieldInstance.messageOutputted) || force) {
|
|
||||||
this.removeErrors(fieldInstance)
|
|
||||||
this.parseField(fieldInstance)
|
|
||||||
if (outputErrors) this.outputErrors(fieldInstance)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (this.errors.length > 0) this.removeErrors()
|
|
||||||
for (const nameAttrVal of this.reqData) {
|
|
||||||
for (const field of this.reqFields) {
|
|
||||||
if (field.el.name === nameAttrVal) {
|
|
||||||
this.parseField(field)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.outputErrors()
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.log(this); return;
|
|
||||||
|
|
||||||
// form was sbmitted and all fields pass validation
|
|
||||||
if (!fieldInstance && this.errors.length === 0) {
|
|
||||||
this.onSuccess ? Function(this.onSuccess)() : this.form.submit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static {
|
|
||||||
const forms = document.getElementsByTagName('form')
|
|
||||||
for (let form of forms) {
|
|
||||||
form.noValidate = true
|
|
||||||
try {
|
|
||||||
const config = Object.assign(
|
|
||||||
{form: form},
|
|
||||||
form.dataset.validation ? JSON.parse(form.dataset.validation) : {}
|
|
||||||
)
|
|
||||||
Validation.forms.push(new Validation(config))
|
|
||||||
} catch(error) {
|
|
||||||
if (error instanceof SyntaxError) console.warn(`There was a problem configuring your custom validation options. Using defaults. Are you using a properly formatted JSON object? (e.g. data-validation='{"reqIndicators": false}')`)
|
|
||||||
delete form.dataset.validation
|
|
||||||
Validation.forms.push(new Validation({form: form}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,8 +5,8 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Form Validation</title>
|
<title>Form Validation</title>
|
||||||
<link rel="stylesheet" href="./css/main.css">
|
<link rel="stylesheet" href="../../css/main.css">
|
||||||
<script src="./js/main.js" defer></script>
|
<script src="./validation.js" defer></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="no-transition">
|
<body class="no-transition">
|
||||||
@ -15,17 +15,7 @@
|
|||||||
<div style="background: var(--color-02);">
|
<div style="background: var(--color-02);">
|
||||||
<form data-validation='{"onSuccess": "save()"}'>
|
<form data-validation='{"onSuccess": "save()"}'>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Form Validation</legend>
|
<legend>Native HTML Form Controls</legend>
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="exists">Custom Form Control</label>
|
|
||||||
<div data-validation='{"valueSrc": ["#exists", "exists", ":scope > img"]}' id="exists" name="exists"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<figure class="form-group">
|
|
||||||
<label for="date">Date</label>
|
|
||||||
<input id="date" type="date" name="date" min="2025-01-01" max="2025-12-31">
|
|
||||||
</figure>
|
|
||||||
|
|
||||||
<figure class="form-group">
|
<figure class="form-group">
|
||||||
<label for="text">Text</label>
|
<label for="text">Text</label>
|
||||||
@ -33,17 +23,6 @@
|
|||||||
<figcaption>Input element with <code>type="text"</code> attribute.</figcaption>
|
<figcaption>Input element with <code>type="text"</code> attribute.</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
<figure class="form-group">
|
|
||||||
<label for="custom">Regex Pattern Test</label> <small>(Numbers only.)</small>
|
|
||||||
<input data-validation='{"pattern": "^[0-9]*$", "patternMessage": "This field must only contain numbers."}' id="custom" type="text" inputmode="numeric" name="custom">
|
|
||||||
<figcaption>This field uses a user supplied regular expression to test against.</figcaption>
|
|
||||||
</figure>
|
|
||||||
|
|
||||||
<figure class="form-group">
|
|
||||||
<label for="tel">Tel</label>
|
|
||||||
<input id="tel" type="tel" name="tel">
|
|
||||||
</figure>
|
|
||||||
|
|
||||||
<figure class="form-group">
|
<figure class="form-group">
|
||||||
<label for="email">Email</label>
|
<label for="email">Email</label>
|
||||||
<input id="email" type="email" name="email">
|
<input id="email" type="email" name="email">
|
||||||
@ -51,31 +30,13 @@
|
|||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
<figure class="form-group">
|
<figure class="form-group">
|
||||||
<label for="password">Password</label>
|
<label for="tel">Tel</label>
|
||||||
<input id="password" type="password" name="password">
|
<input id="tel" type="tel" name="tel">
|
||||||
<figcaption>Input element with <code>type="password"</code> attribute. The 'Password Confirm' field is auto-generated.</figcaption>
|
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
<figure class="form-group">
|
<figure class="form-group">
|
||||||
<label for="not-required">Not Required</label>
|
<label for="date">Date</label>
|
||||||
<input data-validation='{"optional": true}' id="not-required" type="text" name="not_required">
|
<input id="date" type="date" name="date" min="2025-01-01" max="2025-12-31">
|
||||||
<figcaption>Exclude fields.</figcaption>
|
|
||||||
</figure>
|
|
||||||
|
|
||||||
<figure class="form-group">
|
|
||||||
<label for="custom-control">Custom Form Control</label>
|
|
||||||
<div data-validation='{"valueSrc": ["#custom-control", "textContent"]}'
|
|
||||||
id="custom-control" name="custom_control" contenteditable
|
|
||||||
style="background: green;">
|
|
||||||
</div>
|
|
||||||
<figcaption>This field is a <code>div</code> element, not an <code>input</code> element.</figcaption>
|
|
||||||
</figure>
|
|
||||||
|
|
||||||
<figure class="form-group">
|
|
||||||
<label for="disabled">Dynamic Disabling</label>
|
|
||||||
<input id="disabled" type="text" name="disabled" disabled>
|
|
||||||
<button type="button" onclick="document.querySelector('#disabled').toggleAttribute('disabled')">Toggle Disabled Attribute</button>
|
|
||||||
<figcaption>Fields can be dynamically disabled/enabled.</figcaption>
|
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
<figure class="form-group">
|
<figure class="form-group">
|
||||||
@ -129,7 +90,70 @@
|
|||||||
<label for="textarea">Textarea</label>
|
<label for="textarea">Textarea</label>
|
||||||
<textarea id="textarea" name="textarea"></textarea>
|
<textarea id="textarea" name="textarea"></textarea>
|
||||||
</figure>
|
</figure>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Passwords</legend>
|
||||||
|
<p style="grid-column: -1 / 1;">The Password Confirm field below is automatically generated based on the presence of a Password field.</p>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input id="password" type="password" name="password">
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Custom Form Controls</legend>
|
||||||
|
|
||||||
|
<figure class="form-group">
|
||||||
|
<label for="custom-control">Custom Form Control</label>
|
||||||
|
<div data-validation='{"valueSrc": ["#custom-control", "textContent"]}'
|
||||||
|
id="custom-control" name="custom_control" contenteditable
|
||||||
|
style="background: green;">
|
||||||
|
</div>
|
||||||
|
<figcaption>This custom form control is a <code>div</code> element, not an <code>input</code> element.</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<figure class="form-group">
|
||||||
|
<label for="exists">Custom Form Control</label>
|
||||||
|
<div data-validation='{"valueSrc": ["#exists", "exists", ":scope > img"]}' id="exists" name="exists"></div>
|
||||||
|
<button type="button" onclick="document.querySelector('#exists').insertAdjacentHTML('afterbegin', '<img src="https://raw.githubusercontent.com/tailwindlabs/heroicons/refs/heads/master/src/24/solid/hand-thumb-up.svg" alt="Ya good." />')">Add Image</button>
|
||||||
|
<button type="button" onclick="document.querySelector('#exists').innerHTML = ''">Remove Image</button>
|
||||||
|
<figcaption>This custom form control checks if a the <code>div#exists</code> contains an image.</figcaption>
|
||||||
|
</figure>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Other Options</legend>
|
||||||
|
<p style="grid-column: -1 / 1;">These options can be applied to the fields above.</p>
|
||||||
|
|
||||||
|
<figure class="form-group">
|
||||||
|
<label for="not-required">Text (Not Required)</label>
|
||||||
|
<input data-validation='{"optional": true}' id="not-required" type="text" name="not_required">
|
||||||
|
<figcaption>Exclude fields.</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<figure class="form-group">
|
||||||
|
<label for="not-required-email">Email (Not Required)</label>
|
||||||
|
<input data-validation='{"optional": true}' id="not-required-email" type="email" name="not_required_email">
|
||||||
|
<figcaption>Exclude fields.</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<figure class="form-group">
|
||||||
|
<label for="custom">Regex Pattern Test</label> <small>(Numbers only.)</small>
|
||||||
|
<input data-validation='{"pattern": "^[0-9]*$", "patternMessage": "This field must only contain numbers."}' id="custom" type="text" inputmode="numeric" name="custom">
|
||||||
|
<figcaption>This field uses a user supplied regular expression to test against and can output a custom error message.</figcaption>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<figure class="form-group">
|
||||||
|
<label for="disabled">Dynamic Disabling</label>
|
||||||
|
<input id="disabled" type="text" name="disabled" disabled>
|
||||||
|
<button type="button" onclick="document.querySelector('#disabled').toggleAttribute('disabled')">Toggle Disabled Attribute</button>
|
||||||
|
<figcaption>Required fields can be dynamically disabled/enabled.</figcaption>
|
||||||
|
</figure>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
<figure class="form-group">
|
<figure class="form-group">
|
||||||
<button type="submit">Submit</button>
|
<button type="submit">Submit</button>
|
||||||
<button type="reset">Reset</button>
|
<button type="reset">Reset</button>
|
@ -263,41 +263,41 @@ class Validation {
|
|||||||
parseField(fieldInstance) {
|
parseField(fieldInstance) {
|
||||||
const nameAttrVal = fieldInstance.el.name
|
const nameAttrVal = fieldInstance.el.name
|
||||||
|
|
||||||
if (!fieldInstance.optional) {
|
this.validateForBlank(fieldInstance)
|
||||||
if (!fieldInstance.el.value) {
|
|
||||||
this.validateForBlank(fieldInstance)
|
if (fieldInstance.el.value) {
|
||||||
|
if (fieldInstance.pattern) {
|
||||||
|
this.validatePattern(fieldInstance)
|
||||||
} else {
|
} else {
|
||||||
if (fieldInstance.pattern) {
|
switch (fieldInstance.el.type) {
|
||||||
this.validatePattern(fieldInstance)
|
case 'checkbox':
|
||||||
} else {
|
this.validateCheckbox(fieldInstance, nameAttrVal)
|
||||||
switch (fieldInstance.el.type) {
|
break
|
||||||
case 'checkbox':
|
case 'email':
|
||||||
this.validateCheckbox(fieldInstance, nameAttrVal)
|
this.validateEmail(fieldInstance)
|
||||||
break
|
break
|
||||||
case 'email':
|
case 'password':
|
||||||
this.validateEmail(fieldInstance)
|
if (fieldInstance.el.name === 'password') this.validatePassword(fieldInstance)
|
||||||
break
|
if (fieldInstance.el.name === 'password-confirm') this.validateConfirmPass(fieldInstance)
|
||||||
case 'password':
|
break
|
||||||
if (fieldInstance.el.name === 'password') this.validatePassword(fieldInstance)
|
case 'radio':
|
||||||
if (fieldInstance.el.name === 'password-confirm') this.validateConfirmPass(fieldInstance)
|
this.validateRadio(fieldInstance, nameAttrVal)
|
||||||
break
|
break
|
||||||
case 'radio':
|
case 'tel':
|
||||||
this.validateRadio(fieldInstance, nameAttrVal)
|
this.validateTel(fieldInstance)
|
||||||
break
|
break
|
||||||
case 'tel':
|
|
||||||
this.validateTel(fieldInstance)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
validateForBlank(fieldInstance) {
|
validateForBlank(fieldInstance) {
|
||||||
this.addError(
|
if (!fieldInstance.el.value && !fieldInstance.optional) {
|
||||||
fieldInstance,
|
this.addError(
|
||||||
(fieldInstance.labelText ? fieldInstance.labelText : `Field`) + ` is required.`
|
fieldInstance,
|
||||||
)
|
(fieldInstance.labelText ? fieldInstance.labelText : `Field`) + ` is required.`
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
validatePattern(fieldInstance) {
|
validatePattern(fieldInstance) {
|
||||||
|
Reference in New Issue
Block a user