|
4 | 4 | * This software may be modified and distributed under the terms |
5 | 5 | * of the MIT license. See the LICENSE file for details. |
6 | 6 | */ |
7 | | -import type { PasswordCollector, Updater } from '@forgerock/davinci-client/types'; |
| 7 | +import type { |
| 8 | + PasswordCollector, |
| 9 | + ValidatedPasswordCollector, |
| 10 | + Updater, |
| 11 | + Validator, |
| 12 | +} from '@forgerock/davinci-client/types'; |
8 | 13 | import { dotToCamelCase } from '../helper.js'; |
9 | 14 |
|
| 15 | +const UPPERCASE_RE = /^[A-Z]+$/; |
| 16 | +const LOWERCASE_RE = /^[a-z]+$/; |
| 17 | +const DIGIT_RE = /^[0-9]+$/; |
| 18 | + |
10 | 19 | export default function passwordComponent( |
11 | 20 | formEl: HTMLFormElement, |
12 | | - collector: PasswordCollector, |
13 | | - updater: Updater<PasswordCollector>, |
| 21 | + collector: PasswordCollector | ValidatedPasswordCollector, |
| 22 | + updater: Updater<PasswordCollector | ValidatedPasswordCollector>, |
| 23 | + validator?: Validator, |
14 | 24 | ) { |
| 25 | + const collectorKey = dotToCamelCase(collector.output.key); |
15 | 26 | const label = document.createElement('label'); |
16 | 27 | const input = document.createElement('input'); |
17 | 28 |
|
18 | | - label.htmlFor = dotToCamelCase(collector.output.key); |
| 29 | + label.htmlFor = collectorKey; |
19 | 30 | label.innerText = collector.output.label; |
20 | 31 | input.type = 'password'; |
21 | | - input.id = dotToCamelCase(collector.output.key); |
22 | | - input.name = dotToCamelCase(collector.output.key); |
| 32 | + input.id = collectorKey; |
| 33 | + input.name = collectorKey; |
23 | 34 |
|
24 | 35 | formEl?.appendChild(label); |
25 | 36 | formEl?.appendChild(input); |
26 | 37 |
|
27 | | - formEl |
28 | | - ?.querySelector(`#${dotToCamelCase(collector.output.key)}`) |
29 | | - ?.addEventListener('blur', (event: Event) => { |
30 | | - const error = updater((event.target as HTMLInputElement).value); |
31 | | - if (error && 'error' in error) { |
32 | | - console.error(error.error.message); |
| 38 | + if (collector.type === 'ValidatedPasswordCollector') { |
| 39 | + const validation = collector.input.validation; |
| 40 | + const requirementsList = document.createElement('ul'); |
| 41 | + requirementsList.className = 'password-requirements'; |
| 42 | + |
| 43 | + if (validation.length) { |
| 44 | + const { min, max } = validation.length; |
| 45 | + let lengthMessage: string | null = null; |
| 46 | + if (min != null && max != null) { |
| 47 | + lengthMessage = `${min}–${max} characters`; |
| 48 | + } else if (min != null) { |
| 49 | + lengthMessage = `At least ${min} characters`; |
| 50 | + } else if (max != null) { |
| 51 | + lengthMessage = `At most ${max} characters`; |
| 52 | + } |
| 53 | + if (lengthMessage) { |
| 54 | + const li = document.createElement('li'); |
| 55 | + li.textContent = lengthMessage; |
| 56 | + requirementsList.appendChild(li); |
| 57 | + } |
| 58 | + } |
| 59 | + |
| 60 | + if (validation.minCharacters) { |
| 61 | + for (const [charset, count] of Object.entries(validation.minCharacters)) { |
| 62 | + const li = document.createElement('li'); |
| 63 | + if (UPPERCASE_RE.test(charset)) { |
| 64 | + li.textContent = `At least ${count} uppercase letter(s)`; |
| 65 | + } else if (LOWERCASE_RE.test(charset)) { |
| 66 | + li.textContent = `At least ${count} lowercase letter(s)`; |
| 67 | + } else if (DIGIT_RE.test(charset)) { |
| 68 | + li.textContent = `At least ${count} number(s)`; |
| 69 | + } else { |
| 70 | + li.textContent = `At least ${count} special character(s)`; |
| 71 | + } |
| 72 | + requirementsList.appendChild(li); |
33 | 73 | } |
34 | | - }); |
| 74 | + } |
| 75 | + |
| 76 | + if (requirementsList.children.length > 0) { |
| 77 | + formEl?.appendChild(requirementsList); |
| 78 | + } |
| 79 | + } |
| 80 | + |
| 81 | + const inputEl = formEl?.querySelector(`#${collectorKey}`); |
| 82 | + const shouldValidate = collector.type === 'ValidatedPasswordCollector' && !!validator; |
| 83 | + |
| 84 | + inputEl?.addEventListener('input', (event: Event) => { |
| 85 | + const value = (event.target as HTMLInputElement).value; |
| 86 | + |
| 87 | + if (shouldValidate) { |
| 88 | + const result = validator(value); |
| 89 | + if (Array.isArray(result) && result.length) { |
| 90 | + let errorEl = formEl?.querySelector<HTMLUListElement>(`.${collectorKey}-error`); |
| 91 | + if (!errorEl) { |
| 92 | + errorEl = document.createElement('ul'); |
| 93 | + errorEl.className = `${collectorKey}-error`; |
| 94 | + inputEl.after(errorEl); |
| 95 | + } |
| 96 | + const items = result.map((msg) => { |
| 97 | + const li = document.createElement('li'); |
| 98 | + li.textContent = msg; |
| 99 | + return li; |
| 100 | + }); |
| 101 | + errorEl.replaceChildren(...items); |
| 102 | + return; |
| 103 | + } |
| 104 | + formEl?.querySelector(`.${collectorKey}-error`)?.remove(); |
| 105 | + } |
| 106 | + |
| 107 | + const error = updater(value); |
| 108 | + if (error && 'error' in error) { |
| 109 | + console.error(error.error.message); |
| 110 | + } |
| 111 | + }); |
35 | 112 | } |
0 commit comments