@jdlien/validator 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ import Validator from './src/Validator'
2
+ import * as validatorUtils from './src/validator-utils'
3
+
4
+ export default Validator
5
+ export { validatorUtils }
package/package.json CHANGED
@@ -1,17 +1,8 @@
1
1
  {
2
2
  "name": "@jdlien/validator",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "type": "module",
5
- "files": [
6
- "dist"
7
- ],
8
5
  "module": "dist/validator.js",
9
- "exports": {
10
- ".": {
11
- "import": "./dist/validator.js"
12
- },
13
- "./package.json": "./package.json"
14
- },
15
6
  "description": "Validates and sanitizes the inputs in a form using native html attributes.",
16
7
  "scripts": {
17
8
  "dev": "vite",
@@ -0,0 +1,628 @@
1
+ /**
2
+ * Form Validator used by EPL apps and www2.
3
+ * © 2023 JD Lien
4
+ *
5
+ * @format
6
+ */
7
+
8
+ // Import the validator utility functions
9
+ import * as utils from './validator-utils'
10
+ import { FormControl, ValidatorOptions, InputHandlers } from './types'
11
+
12
+ export class ValidationSuccessEvent extends Event {
13
+ submitEvent: Event
14
+ constructor(submitEvent: Event) {
15
+ super('validationSuccess', { cancelable: true })
16
+ this.submitEvent = submitEvent
17
+ }
18
+ }
19
+
20
+ export class ValidationErrorEvent extends Event {
21
+ submitEvent: Event
22
+ constructor(submitEvent: Event) {
23
+ super('validationError', { cancelable: true })
24
+ this.submitEvent = submitEvent
25
+ }
26
+ }
27
+
28
+ export default class Validator {
29
+ form: HTMLFormElement
30
+ inputs: FormControl[] = []
31
+ // Keeps track of error messages accumulated for each input
32
+ inputErrors: { [key: string]: string[] } = {}
33
+
34
+ // Default error messages.
35
+ messages = {
36
+ ERROR_MAIN: 'There is a problem with your submission.',
37
+ ERROR_GENERIC: 'Enter a valid value.',
38
+ ERROR_REQUIRED: 'This field is required.',
39
+ OPTION_REQUIRED: 'An option must be selected.',
40
+ CHECKED_REQUIRED: 'This must be checked.',
41
+ ERROR_MAXLENGTH: 'This must be ${val} characters or fewer.',
42
+ ERROR_MINLENGTH: 'This must be at least ${val} characters.',
43
+ ERROR_NUMBER: 'This must be a number.',
44
+ ERROR_INTEGER: 'This must be a whole number.',
45
+ ERROR_TEL: 'This is not a valid telephone number.',
46
+ ERROR_EMAIL: 'This is not a valid email address.',
47
+ ERROR_ZIP: 'This is not a valid zip code.',
48
+ ERROR_POSTAL: 'This is not a valid postal code.',
49
+ ERROR_DATE: 'This is not a valid date.',
50
+ ERROR_DATE_PAST: 'The date must be in the past.',
51
+ ERROR_DATE_FUTURE: 'The date must be in the future.',
52
+ ERROR_DATE_RANGE: 'The date is outside the allowed range.',
53
+ ERROR_TIME: 'This is not a valid time.',
54
+ ERROR_TIME_RANGE: 'The time is outside the allowed range.',
55
+ ERROR_URL: 'This is not a valid URL.',
56
+ ERROR_COLOR: 'This is not a valid CSS colour.',
57
+ ERROR_CUSTOM_VALIDATION: 'There was a problem validating this field.',
58
+ }
59
+ // Show debug messages in the console
60
+ debug: boolean
61
+ // Whether validation should be performed immediately on instantiation
62
+ autoInit: boolean
63
+ // Whether to prevent the form from submitting if validation is successful
64
+ preventSubmit: boolean = false
65
+ // Class toggled hide an element (eg display:none)
66
+ hiddenClasses: string
67
+
68
+ // Classes to apply to the main error message (space-separated)
69
+ errorMainClasses: string
70
+ // Classes added to an invalid input (space-separated)
71
+ errorInputClasses: string
72
+ // Timeout for dispatching events on input (used by syncColorInput)
73
+ private dispatchTimeout: number = 0
74
+
75
+ // Whether the original form has a novalidate attribute
76
+ private originalNoValidate: boolean = false
77
+
78
+ private validationSuccessCallback: (event: Event) => void
79
+ private validationErrorCallback: (event: Event) => void
80
+
81
+ // Sets defaults and adds event listeners
82
+ constructor(form: HTMLFormElement, options: ValidatorOptions = {}) {
83
+ if (!form) throw new Error('Validator requires a form to be passed as the first argument.')
84
+ if (!(form instanceof HTMLFormElement)) {
85
+ throw new Error('form argument must be an instance of HTMLFormElement')
86
+ }
87
+
88
+ this.form = form
89
+
90
+ // If the form has a data-prevent-submit attribute, set preventSubmit to true
91
+ // Can be overridden with the preventSubmit option
92
+ if (form.dataset.preventSubmit === '' || form.dataset.preventSubmit) this.preventSubmit = true
93
+
94
+ // Merge options with defaults
95
+ Object.assign(this.messages, options.messages || {})
96
+ this.debug = options.debug || false
97
+ this.autoInit = options.autoInit === false ? false : true
98
+ this.preventSubmit = options.preventSubmit === false ? false : this.preventSubmit
99
+ this.hiddenClasses = options.hiddenClasses || 'hidden opacity-0'
100
+ this.errorMainClasses =
101
+ options.errorMainClasses ||
102
+ 'm-2 border border-red-500 bg-red-100 p-3 dark:bg-red-900/80 text-center'
103
+
104
+ this.errorInputClasses = options.errorInputClasses || 'border-red-600 dark:border-red-500'
105
+ this.validationSuccessCallback = options.validationSuccessCallback || (() => {})
106
+ this.validationErrorCallback = options.validationErrorCallback || (() => {})
107
+
108
+ if (this.autoInit) this.init()
109
+
110
+ // Re-initialize the form if it altered in the DOM
111
+ new MutationObserver(() => this.autoInit && this.init()).observe(form, { childList: true })
112
+ }
113
+
114
+ // Event handler references
115
+ private submitHandlerRef = this.submitHandler.bind(this)
116
+ private inputInputHandlerRef = this.inputInputHandler.bind(this)
117
+ private inputChangeHandlerRef = this.inputChangeHandler.bind(this)
118
+ private inputKeydownHandlerRef = this.inputKeydownHandler.bind(this)
119
+
120
+ public addEventListeners(): void {
121
+ this.form.addEventListener('submit', this.submitHandlerRef)
122
+ this.form.addEventListener('input', this.inputInputHandlerRef)
123
+ this.form.addEventListener('change', this.inputChangeHandlerRef)
124
+ this.form.addEventListener('keydown', this.inputKeydownHandlerRef)
125
+ // This doesn't seem to be very useful
126
+ this.form.addEventListener('remove', this.destroy, { once: true })
127
+ }
128
+
129
+ public removeEventListeners(): void {
130
+ this.form.removeEventListener('submit', this.submitHandlerRef)
131
+ this.form.removeEventListener('input', this.inputInputHandlerRef)
132
+ this.form.removeEventListener('change', this.inputChangeHandlerRef)
133
+ this.form.removeEventListener('keydown', this.inputKeydownHandlerRef)
134
+ this.form.removeEventListener('remove', this.destroy)
135
+ }
136
+
137
+ // Adds event listeners to all formFields in a specified form
138
+ init(): void {
139
+ this.inputs = Array.from(this.form.elements) as FormControl[]
140
+ // Ensure each input has a unique ID and an empty array in inputErrors
141
+ this.inputs.forEach((input) => {
142
+ if (!input.name && !input.id) input.id = `vl-input-${Math.random().toString(36).slice(2)}`
143
+ this.inputErrors[input.name || input.id] = []
144
+ })
145
+
146
+ // Check that the original form has a novalidate attribute
147
+ this.originalNoValidate = this.form.hasAttribute('novalidate')
148
+
149
+ // Disable the browser's built-in validation
150
+ this.form.setAttribute('novalidate', 'novalidate')
151
+
152
+ this.removeEventListeners()
153
+ this.addEventListeners()
154
+ } // end init()
155
+
156
+ private getErrorEl(input: FormControl): HTMLElement | null {
157
+ const errorEl = document.getElementById(input.name + '-error')
158
+ if (errorEl) return errorEl
159
+
160
+ return document.getElementById(input.id + '-error') || null
161
+ }
162
+
163
+ private addErrorMain(message?: string): void {
164
+ const errorEl = document.createElement('div')
165
+ errorEl.id = 'form-error-main'
166
+ this.errorMainClasses.split(' ').forEach((className) => {
167
+ errorEl.classList.add(className)
168
+ })
169
+
170
+ if (message) errorEl.innerHTML = message
171
+ else errorEl.innerHTML = this.messages.ERROR_MAIN
172
+ // Add the error message to the bottom of the form
173
+ this.form.appendChild(errorEl)
174
+ }
175
+
176
+ // Adds an error to the array of strings to be displayed by an input that failed
177
+ private addInputError(
178
+ el: FormControl,
179
+ message = el.dataset.errorDefault || this.messages.ERROR_GENERIC
180
+ ): void {
181
+ const name = el.name || el.id
182
+
183
+ if (this.debug) console.log('Invalid value for ' + name + ': ' + message)
184
+
185
+ // init already does this, but ensure the input has an array
186
+ if (!(name in this.inputErrors)) this.inputErrors[name] = []
187
+ // Add the message if it isn't already in the array
188
+ if (!this.inputErrors[name].includes(message)) this.inputErrors[name].push(message)
189
+ }
190
+
191
+ // Shows an error message in a container with the input's id suffixed with -error.
192
+ // A future version of this could inject an error element into the DOM if it doesn't exist
193
+ private showInputErrors(el: FormControl): void {
194
+ if (!el || (!el.name && !el.id)) return
195
+
196
+ // Check if the input has any error messages
197
+ const name = el.name || el.id
198
+ const errors = name in this.inputErrors ? this.inputErrors[name] : []
199
+ // If there are no errors, don't do anything
200
+ if (!errors.length) return
201
+
202
+ el.setAttribute('aria-invalid', 'true')
203
+
204
+ // Apply input classes to indicate an error on the input itself
205
+ this.errorInputClasses.split(' ').forEach((className) => {
206
+ el.classList.add(className)
207
+ })
208
+
209
+ // Add the error messages to the error element and show it
210
+ let errorEl = this.getErrorEl(el)
211
+ if (!errorEl) return
212
+
213
+ errorEl.innerHTML = errors.join('<br>')
214
+
215
+ this.hiddenClasses.split(' ').forEach((className) => {
216
+ if (errorEl) errorEl.classList.remove(className)
217
+ })
218
+ }
219
+
220
+ // Shows all the error messages for all the inputs of the form, and a main error message
221
+ // TODO: Consider (optionally) scrolling to the first error message
222
+ private showFormErrors(): void {
223
+ // Show any errors from validation
224
+ this.inputs.forEach((el) => this.showInputErrors(el))
225
+
226
+ // If if any of the inputs have error messages, show the main error message
227
+ // One could show all the errors in the main message, but it might get long
228
+ // Should this be in the same branch of code that does the dispatch and callback?
229
+ if (Object.values(this.inputErrors).some((el) => Array.isArray(el) && el.length)) {
230
+ const mainErrorEl = this.form.querySelectorAll('#form-error-main')
231
+ if (mainErrorEl.length) {
232
+ mainErrorEl.forEach((el) => {
233
+ // If there are no contents, add the default message
234
+ if (!el.innerHTML) el.innerHTML = this.messages.ERROR_MAIN
235
+ this.hiddenClasses.split(' ').forEach((className) => {
236
+ el.classList.remove(className)
237
+ })
238
+ })
239
+ } else this.addErrorMain()
240
+ }
241
+ }
242
+
243
+ // Clears error messages from an input and removes its errors from the inputErrors array
244
+ private clearInputErrors(el: FormControl): void {
245
+ this.inputErrors[el.name || el.id] = []
246
+
247
+ // Remove the aria-invalid attribute from the input
248
+ el.removeAttribute('aria-invalid')
249
+
250
+ let errorEl = this.getErrorEl(el)
251
+ if (!errorEl) return
252
+
253
+ // Remove the error style
254
+ this.errorInputClasses.split(' ').forEach((className) => {
255
+ el.classList.remove(className)
256
+ })
257
+
258
+ // Hide the error element
259
+ this.hiddenClasses.split(' ').forEach((className) => {
260
+ if (errorEl) errorEl.classList.add(className)
261
+ })
262
+
263
+ // Clear the error message
264
+ // TODO: This needs to happen on transitionend if we want to animate the error message out
265
+ errorEl.textContent = ''
266
+ }
267
+
268
+ private clearFormErrors(): void {
269
+ // If there's a big error message, hide it
270
+ this.form.querySelectorAll('#form-error-main').forEach((el) => {
271
+ this.hiddenClasses.split(' ').forEach((className) => {
272
+ el.classList.add(className)
273
+ })
274
+ })
275
+
276
+ // Clear any previous errors
277
+ this.inputs.forEach((el) => this.clearInputErrors(el))
278
+ }
279
+
280
+ // Validates a required input and returns true if it's valid.
281
+ // Shows an error if the input is required and empty.
282
+ private validateRequired(el: FormControl): boolean {
283
+ let valid = true
284
+ if (
285
+ el.required &&
286
+ (el.value === '' ||
287
+ (el instanceof HTMLInputElement && ['checkbox', 'radio'].includes(el.type) && !el.checked))
288
+ ) {
289
+ // Handle checkboxes and radio buttons. Check that at least one of any name group is checked
290
+ // Check that any checkbox of a group of checkboxes is checked
291
+ // This assumes the checkbox or radio button is in a group... if it's not,
292
+ // we can specify a default error message with error=
293
+ if (el instanceof HTMLInputElement && ['checkbox', 'radio'].includes(el.type)) {
294
+ let groupChecked = false
295
+ let groupName = el.name
296
+ const groupInputs = this.form.querySelectorAll(`input[name="${groupName}"]`)
297
+ groupInputs.forEach((input) => {
298
+ if (input instanceof HTMLInputElement && input.checked === true) {
299
+ groupChecked = true
300
+ return
301
+ }
302
+ })
303
+
304
+ if (groupChecked === false) {
305
+ valid = false
306
+
307
+ let message =
308
+ groupInputs.length > 1 ? this.messages.OPTION_REQUIRED : this.messages.CHECKED_REQUIRED
309
+
310
+ // If there's a data-error-default attribute, use that as the error message
311
+ if (el.dataset.errorDefault) message = el.dataset.errorDefault
312
+ this.addInputError(el, message)
313
+ }
314
+ } else if (utils.isFormControl(el)) {
315
+ valid = false
316
+ this.addInputError(el, el.dataset.errorDefault || this.messages.ERROR_REQUIRED)
317
+ }
318
+ }
319
+ return valid
320
+ } // end validateRequired
321
+
322
+ // Validates a min and max length
323
+ private validateLength(el: FormControl): boolean {
324
+ let valid = true
325
+
326
+ if ((el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) && el.value.length) {
327
+ // prettier-ignore
328
+ let minLength = el.minLength > 0 ? el.minLength
329
+ : el.dataset.minLength ? parseInt(el.dataset.minLength as string) : 0
330
+
331
+ // prettier-ignore
332
+ let maxLength = el.maxLength > 0 && el.maxLength < 500_000 ? el.maxLength
333
+ : el.dataset.maxLength ? parseInt(el.dataset.maxLength as string) : Infinity
334
+
335
+ if (minLength > 0 && el.value.length < minLength) {
336
+ valid = false
337
+ this.addInputError(
338
+ el,
339
+ this.messages.ERROR_MINLENGTH.replace('${val}', minLength.toString())
340
+ )
341
+ }
342
+
343
+ if (el.value.length > maxLength) {
344
+ valid = false
345
+ this.addInputError(
346
+ el,
347
+ this.messages.ERROR_MAXLENGTH.replace('${val}', maxLength.toString())
348
+ )
349
+ }
350
+ }
351
+
352
+ return valid
353
+ }
354
+
355
+ // A map of input handlers that can be used for each type of input.
356
+ private inputHandlers: InputHandlers = {
357
+ number: {
358
+ parse: utils.parseNumber,
359
+ isValid: utils.isNumber,
360
+ error: this.messages.ERROR_NUMBER,
361
+ },
362
+ integer: {
363
+ parse: utils.parseInteger,
364
+ isValid: utils.isInteger,
365
+ error: this.messages.ERROR_INTEGER,
366
+ },
367
+ tel: {
368
+ parse: utils.parseNANPTel,
369
+ isValid: utils.isNANPTel,
370
+ error: this.messages.ERROR_TEL,
371
+ },
372
+ email: {
373
+ parse: (value: string) => value.trim(),
374
+ isValid: utils.isEmail,
375
+ error: this.messages.ERROR_EMAIL,
376
+ },
377
+ zip: {
378
+ parse: utils.parseZip,
379
+ isValid: utils.isZip,
380
+ error: this.messages.ERROR_ZIP,
381
+ },
382
+ postal: {
383
+ parse: utils.parsePostalCA,
384
+ isValid: utils.isPostalCA,
385
+ error: this.messages.ERROR_POSTAL,
386
+ },
387
+ url: {
388
+ parse: utils.parseUrl,
389
+ isValid: utils.isUrl,
390
+ error: this.messages.ERROR_URL,
391
+ },
392
+ date: {
393
+ parse: utils.parseDateToString,
394
+ isValid: utils.isDate,
395
+ error: this.messages.ERROR_DATE,
396
+ },
397
+ time: {
398
+ parse: utils.parseTimeToString,
399
+ isValid: utils.isTime,
400
+ error: this.messages.ERROR_TIME,
401
+ },
402
+ color: {
403
+ parse: (value: string) => value.trim().toLowerCase(),
404
+ isValid: utils.isColor,
405
+ error: this.messages.ERROR_COLOR,
406
+ },
407
+ }
408
+
409
+ private validateInputType(el: FormControl): boolean {
410
+ const dataType = el.dataset.type || el.type
411
+ const inputHandler = this.inputHandlers[el.type] || this.inputHandlers[dataType]
412
+
413
+ if (inputHandler) {
414
+ const dateFormat = el.dataset.dateFormat || el.dataset.timeFormat
415
+ const parsedValue = inputHandler.parse(el.value, dateFormat)
416
+
417
+ // Do not update the value if the input is one of these input types
418
+ const nonUpdateableTypes = ['date', 'time', 'datetime-local', 'month', 'week']
419
+ if (parsedValue.length && !nonUpdateableTypes.includes(el.type)) el.value = parsedValue
420
+
421
+ if (!inputHandler.isValid(el.value)) {
422
+ this.addInputError(el, inputHandler.error)
423
+ return false
424
+ }
425
+ }
426
+
427
+ return true
428
+ }
429
+
430
+ private validateDateRange(el: FormControl): boolean {
431
+ if (el.dataset.dateRange) {
432
+ const range = el.dataset.dateRange
433
+ const date = utils.parseDate(el.value)
434
+ // only validate the date range if it's a valid date
435
+ if (!isNaN(date.getTime()) && !utils.isDateInRange(date, range)) {
436
+ let msg = el.dataset.errorDefault || this.messages.ERROR_DATE_RANGE
437
+ if (range === 'past') msg = this.messages.ERROR_DATE_PAST
438
+ else if (range === 'future') msg = this.messages.ERROR_DATE_FUTURE
439
+ this.addInputError(el, msg)
440
+ return false
441
+ }
442
+ }
443
+
444
+ return true
445
+ }
446
+
447
+ // Validates a pattern from data-pattern or pattern; data-pattern takes precedence
448
+ private validatePattern(el: FormControl): boolean {
449
+ const pattern = el.dataset.pattern || (el instanceof HTMLInputElement && el.pattern) || null
450
+ if (pattern && !new RegExp(pattern).test(el.value)) {
451
+ this.addInputError(el) // Use the default error message
452
+ return false
453
+ }
454
+
455
+ return true
456
+ }
457
+
458
+ /**
459
+ * Specify a custom function in data-validation and it gets called to validate the input
460
+ * The custom function can return
461
+ * - a boolean
462
+ * - a Promise that resolves to a boolean
463
+ * - an object with a valid property that is a boolean
464
+ * - a Promise that resolves to an object with a valid property that is a boolean
465
+ * - and optionally a messages property that is a string or array of strings
466
+ * - OR optionally, a message property that is a string
467
+ * - optionaly, a boolean error property that is true if something went wrong
468
+ */
469
+ private async validateCustom(el: FormControl): Promise<boolean> {
470
+ const validation = el.dataset.validation
471
+ if (!validation || typeof validation !== 'string') return true
472
+ const validationFn: Function = window[validation as keyof Window] as Function
473
+ if (!validationFn || typeof validationFn !== 'function') return true
474
+
475
+ let result: any
476
+ try {
477
+ result = await Promise.resolve(validationFn(el.value))
478
+ result = utils.normalizeValidationResult(result)
479
+ } catch (err) {
480
+ this.addInputError(el, this.messages.ERROR_CUSTOM_VALIDATION)
481
+ return false
482
+ }
483
+
484
+ const message = result.messages.join('<br>') || this.messages.ERROR_CUSTOM_VALIDATION
485
+ if (!result.valid) this.addInputError(el, message)
486
+
487
+ return result.valid
488
+ }
489
+
490
+ // Validates an input with a value and returns true if it's valid
491
+ // Checks inputs defined in the inputHandlers map, pattern, and date range,
492
+ private async validateInput(el: FormControl): Promise<boolean> {
493
+ if (!(el instanceof HTMLInputElement) || !el.value.length) return true
494
+
495
+ let valid = true
496
+ valid = this.validateInputType(el) && valid
497
+ valid = this.validateDateRange(el) && valid
498
+ valid = this.validatePattern(el) && valid
499
+ valid = (await this.validateCustom(el)) && valid
500
+
501
+ return valid
502
+ }
503
+
504
+ // Validates all the fields in the form. It will show an error message
505
+ // in all invalid fields and return false if any are invalid.
506
+ async validate(_e?: Event): Promise<boolean> {
507
+ let valid = true
508
+
509
+ for (const el of this.inputs) {
510
+ valid = this.validateRequired(el) && valid
511
+ valid = this.validateLength(el) && valid
512
+ valid = (await this.validateInput(el)) && valid
513
+ }
514
+
515
+ return valid
516
+ } //end validate()
517
+
518
+ private isSubmitting = false
519
+ private async submitHandler(e: Event): Promise<void> {
520
+ if (this.isSubmitting) return
521
+ e.preventDefault()
522
+
523
+ // Clear any error messages
524
+ this.clearFormErrors()
525
+ let valid = await this.validate(e)
526
+ // Show messages for any invalid inputs and show a large error message
527
+ this.showFormErrors()
528
+
529
+ // External functions can prevent the form from submitting
530
+ // by calling e.preventDefault() in the validationSuccess event
531
+ const validationSuccessEvent = new ValidationSuccessEvent(e)
532
+ const validationErrorEvent = new ValidationErrorEvent(e)
533
+
534
+ if (valid) {
535
+ this.form.dispatchEvent(validationSuccessEvent)
536
+ if (this.validationSuccessCallback) this.validationSuccessCallback(e)
537
+ } else {
538
+ this.form.dispatchEvent(validationErrorEvent)
539
+ if (this.validationErrorCallback) this.validationErrorCallback(e)
540
+ }
541
+
542
+ if (valid && !this.preventSubmit) {
543
+ this.isSubmitting = true
544
+ if (!validationSuccessEvent.defaultPrevented) this.form.submit()
545
+ this.isSubmitting = false
546
+ }
547
+ }
548
+
549
+ private async inputChangeHandler(e: Event): Promise<void> {
550
+ if (!(e.target instanceof HTMLInputElement)) return
551
+
552
+ // Clear and reset error messages for the input
553
+ this.clearInputErrors(e.target)
554
+ await this.validateInput(e.target)
555
+ // Show any error messages for the input after validation
556
+ this.showInputErrors(e.target)
557
+ }
558
+
559
+ private inputInputHandler(e: Event) {
560
+ const input = e.target as HTMLInputElement
561
+
562
+ // Ensure that a user cannot type non-numerics into an integer input
563
+ if (utils.isType(input, 'integer')) input.value = utils.parseInteger(input.value)
564
+
565
+ // We don't filter native number inputs because it causes issues in Chrome
566
+ if (input.type !== 'number' && utils.isType(input, ['number', 'float', 'decimal'])) {
567
+ input.value = utils.parseNumber(input.value)
568
+ }
569
+
570
+ if (utils.isType(input, 'color')) this.syncColorInput(e)
571
+ }
572
+
573
+ // Sync color inputs (data-type="color") with an associated native color input type
574
+ private syncColorInput(e: Event): void {
575
+ let input = e.target as HTMLInputElement
576
+ let colorInput = input
577
+
578
+ if (input.type === 'color')
579
+ colorInput = this.form.querySelector(`#${input.id.replace(/-color/, '')}`) as HTMLInputElement
580
+
581
+ let colorLabel = this.form.querySelector(`#${colorInput.id}-color-label`) as HTMLInputElement
582
+
583
+ // Update the HTML color picker input and its label background when color input changes
584
+ if ((input.dataset.type || '') === 'color') {
585
+ let colorPicker = this.form.querySelector(`input#${input.id}-color`) as HTMLInputElement
586
+
587
+ if (!colorPicker || !utils.isColor(input.value)) return
588
+ colorPicker.value = utils.parseColor(input.value)
589
+ }
590
+
591
+ // Update the color input and label background when the HTML color picker is changed
592
+ if (input.type === 'color') colorInput.value = input.value
593
+
594
+ if (colorLabel) colorLabel.style.backgroundColor = input.value
595
+
596
+ // Dispatch a change event so the color picker's error message updates
597
+ // Debounce so it doesn't get called rapidly when selecting a color
598
+ clearTimeout(this.dispatchTimeout)
599
+ this.dispatchTimeout = window.setTimeout(() => {
600
+ colorInput.dispatchEvent(new Event('change', { bubbles: true }))
601
+ }, 200)
602
+ }
603
+
604
+ // Support using arrow keys to cycle through numbers.
605
+ // Other handling for keyboard events can be done here
606
+ private inputKeydownHandler(e: KeyboardEvent) {
607
+ if (!(e.target instanceof HTMLInputElement)) return
608
+
609
+ if (utils.isType(e.target, 'integer')) {
610
+ if (e.key === 'ArrowUp') {
611
+ // Prevent the cursor from moving to the beginning of the input
612
+ e.preventDefault()
613
+ if (e.target.value === '') e.target.value = '0'
614
+ e.target.value = (parseInt(e.target.value) + 1).toString()
615
+ } else if (e.key === 'ArrowDown') {
616
+ if (parseInt(e.target.value) > 0) e.target.value = (parseInt(e.target.value) - 1).toString()
617
+ else e.target.value = '0'
618
+ }
619
+ }
620
+ }
621
+
622
+ public destroy() {
623
+ this.removeEventListeners()
624
+
625
+ // Perform other cleanup actions here
626
+ if (!this.originalNoValidate) this.form.removeAttribute('novalidate')
627
+ }
628
+ }
package/src/types.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ /** @format */
2
+
3
+ export type FormControl = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
4
+
5
+ export interface ValidatorOptions {
6
+ messages?: object
7
+ debug?: boolean
8
+ autoInit?: boolean
9
+ preventSubmit?: boolean
10
+ hiddenClasses?: string
11
+ errorMainClasses?: string
12
+ errorInputClasses?: string
13
+ validationSuccessCallback?: (event: Event) => void
14
+ validationErrorCallback?: (event: Event) => void
15
+ }
16
+
17
+ export interface InputHandlers {
18
+ [key: string]: {
19
+ parse: (value: string, dateFormat?: string) => string
20
+ isValid: (value: string) => boolean
21
+ error: string
22
+ }
23
+ }