added demo site
This commit is contained in:
		
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| .DS_Store | ||||
| @@ -1,4 +1,8 @@ | ||||
| # Form Validation | ||||
| # Demo | ||||
|  | ||||
| A demo of the form validation class can be found [here](https://demos.danremollino.dev/form-validation/). | ||||
|  | ||||
| # Documentation | ||||
|  | ||||
| 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) | ||||
|   | ||||
							
								
								
									
										94
									
								
								demo/css/custom_properties.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								demo/css/custom_properties.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,94 @@ | ||||
| :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); | ||||
| } | ||||
							
								
								
									
										24
									
								
								demo/css/dialog.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								demo/css/dialog.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| @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; } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										94
									
								
								demo/css/fonts.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								demo/css/fonts.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,94 @@ | ||||
| @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'); | ||||
| } | ||||
							
								
								
									
										105
									
								
								demo/css/grid.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								demo/css/grid.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,105 @@ | ||||
| /* 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
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								demo/css/hamburgers.min.css
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										7
									
								
								demo/css/main.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								demo/css/main.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| @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'; | ||||
							
								
								
									
										114
									
								
								demo/css/reset.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								demo/css/reset.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | ||||
| /*** | ||||
|     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; | ||||
| } | ||||
							
								
								
									
										166
									
								
								demo/css/tags.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								demo/css/tags.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,166 @@ | ||||
| /* 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; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										56
									
								
								demo/css/utility_classes.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								demo/css/utility_classes.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | ||||
| .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; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										144
									
								
								demo/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								demo/index.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,144 @@ | ||||
| <!DOCTYPE html> | ||||
|  | ||||
| <html xmlns="http://www.w3.org/1999/xhtml" lang="en-US" data-color-scheme="dark"> | ||||
|     <head> | ||||
|         <meta charset="UTF-8"> | ||||
|         <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||||
|         <title>Form Validation</title> | ||||
|         <link rel="stylesheet" href="./css/main.css"> | ||||
|         <script src="./js/main.js" defer></script> | ||||
|     </head> | ||||
|  | ||||
|     <body class="no-transition"> | ||||
|         <main> | ||||
|             <section> | ||||
|                 <div style="background: var(--color-02);"> | ||||
|                     <form data-validation='{"onSuccess": "save()"}'> | ||||
|                         <fieldset> | ||||
|                             <legend>Form Validation</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"> | ||||
|                                 <label for="text">Text</label> | ||||
|                                 <input id="text" type="text" name="text"> | ||||
|                                 <figcaption>Input element with <code>type="text"</code> attribute.</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.</figcaption> | ||||
|                             </figure> | ||||
|  | ||||
|                             <figure class="form-group"> | ||||
|                                 <label for="tel">Tel</label> | ||||
|                                 <input id="tel" type="tel" name="tel"> | ||||
|                             </figure> | ||||
|  | ||||
|                             <figure class="form-group"> | ||||
|                                 <label for="email">Email</label> | ||||
|                                 <input id="email" type="email" name="email"> | ||||
|                                 <figcaption>Input element with <code>type="email"</code> attribute.</figcaption> | ||||
|                             </figure> | ||||
|  | ||||
|                             <figure class="form-group"> | ||||
|                                 <label for="password">Password</label> | ||||
|                                 <input id="password" type="password" name="password"> | ||||
|                                 <figcaption>Input element with <code>type="password"</code> attribute. The 'Password Confirm' field is auto-generated.</figcaption> | ||||
|                             </figure> | ||||
|  | ||||
|                             <figure class="form-group"> | ||||
|                                 <label for="not-required">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="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 class="form-group"> | ||||
|                                 <label for="file">File Upload</label> | ||||
|                                 <input id="file" type="file" name="file"> | ||||
|                             </figure> | ||||
|  | ||||
|                             <figure 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> | ||||
|                             </figure> | ||||
|  | ||||
|                             <figure> | ||||
|                                 <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> | ||||
|                                     <span class="form-group__item"> | ||||
|                                         <input id="checkbox-3" type="checkbox" name="checks"> | ||||
|                                         <label for="checkbox-3">Checkbox 3</label> | ||||
|                                     </span> | ||||
|                                 </fieldset> | ||||
|                             </figure> | ||||
|  | ||||
|                             <figure> | ||||
|                                 <fieldset class="form-group"> | ||||
|                                     <legend>Radio Group</legend> | ||||
|                                     <span class="form-group__item"> | ||||
|                                         <input id="radio-1" type="radio" name="radios"> | ||||
|                                         <label for="radio-1">Radio 1</label> | ||||
|                                     </span> | ||||
|                                     <span class="form-group__item"> | ||||
|                                         <input id="radio-2" type="radio" name="radios"> | ||||
|                                         <label for="radio-2">Radio 2</label> | ||||
|                                     </span> | ||||
|                                 </fieldset> | ||||
|                             </figure> | ||||
|  | ||||
|                             <figure class="form-group"> | ||||
|                                 <label for="textarea">Textarea</label> | ||||
|                                 <textarea id="textarea" name="textarea"></textarea> | ||||
|                             </figure> | ||||
|  | ||||
|                             <figure class="form-group"> | ||||
|                                 <button type="submit">Submit</button> | ||||
|                                 <button type="reset">Reset</button> | ||||
|                             </figure> | ||||
|                         </fieldset> | ||||
|                     </form> | ||||
|  | ||||
|                 </div> | ||||
|             </section> | ||||
|         </main> | ||||
|     </body> | ||||
| </html> | ||||
							
								
								
									
										2
									
								
								demo/js/main.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								demo/js/main.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| import('./validation/validation.js') | ||||
| const save = () => alert('Congrats! You successfully filled out the form. Amazing.') | ||||
							
								
								
									
										463
									
								
								demo/js/validation/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										463
									
								
								demo/js/validation/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,463 @@ | ||||
| # 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. | ||||
							
								
								
									
										450
									
								
								demo/js/validation/validation.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										450
									
								
								demo/js/validation/validation.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,450 @@ | ||||
| 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})) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user