@jsenv/navi 0.9.3 → 0.10.1

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.
@@ -0,0 +1,259 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>Same As Constraint Demo - Password Confirmation</title>
6
+ <style>
7
+ .demo-container {
8
+ max-width: 500px;
9
+ margin-bottom: 30px;
10
+ padding: 20px;
11
+ background: #f8f9fa;
12
+ border-radius: 8px;
13
+ }
14
+
15
+ .form-group {
16
+ margin-bottom: 20px;
17
+ }
18
+
19
+ label {
20
+ display: block;
21
+ margin-bottom: 5px;
22
+ color: #333;
23
+ font-weight: bold;
24
+ }
25
+
26
+ input[type="password"] {
27
+ box-sizing: border-box;
28
+ width: 100%;
29
+ padding: 10px;
30
+ font-size: 14px;
31
+ border: 1px solid #ddd;
32
+ border-radius: 4px;
33
+ }
34
+
35
+ input[type="password"]:focus {
36
+ border-color: #007bff;
37
+ outline: none;
38
+ box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
39
+ }
40
+
41
+ input[type="password"]:invalid {
42
+ border-color: #dc3545;
43
+ }
44
+
45
+ .demo-container button {
46
+ padding: 10px 20px;
47
+ color: white;
48
+ font-size: 14px;
49
+ background: #007bff;
50
+ border: none;
51
+ border-radius: 4px;
52
+ cursor: pointer;
53
+ }
54
+
55
+ .demo-container button:hover {
56
+ background: #0056b3;
57
+ }
58
+
59
+ .demo-container button:disabled {
60
+ background: #6c757d;
61
+ cursor: not-allowed;
62
+ }
63
+
64
+ .validation-info {
65
+ margin-top: 20px;
66
+ padding: 15px;
67
+ font-size: 14px;
68
+ background: #e9ecef;
69
+ border-radius: 4px;
70
+ }
71
+
72
+ .success-message {
73
+ margin-top: 15px;
74
+ padding: 10px;
75
+ color: #155724;
76
+ background: #d4edda;
77
+ border: 1px solid #c3e6cb;
78
+ border-radius: 4px;
79
+ }
80
+
81
+ .form-container {
82
+ padding: 20px;
83
+ background: white;
84
+ border: 1px solid #ddd;
85
+ border-radius: 8px;
86
+ }
87
+
88
+ h3 {
89
+ margin-top: 0;
90
+ color: #495057;
91
+ }
92
+ </style>
93
+ </head>
94
+ <body>
95
+ <div style="padding: 20px; font-family: system-ui, sans-serif">
96
+ <h1>Same As Constraint Demo - Password Confirmation</h1>
97
+ <p>
98
+ This demo shows how the <code>data-same-as</code> attribute validates
99
+ that two password fields contain identical values when submitting a
100
+ form.
101
+ </p>
102
+
103
+ <div class="demo-container">
104
+ <h3>Password Confirmation Form</h3>
105
+ <div class="form-container">
106
+ <form id="passwordForm">
107
+ <input
108
+ name="username"
109
+ value="demo_user"
110
+ autocomplete="username"
111
+ style="display: none"
112
+ />
113
+
114
+ <div class="form-group">
115
+ <label for="password">Password:</label>
116
+ <input
117
+ type="password"
118
+ id="password"
119
+ name="password"
120
+ required
121
+ minlength="6"
122
+ placeholder="Enter your password"
123
+ autocomplete="new-password"
124
+ />
125
+ </div>
126
+
127
+ <div class="form-group">
128
+ <label for="confirmPassword">Confirm Password:</label>
129
+ <input
130
+ type="password"
131
+ id="confirmPassword"
132
+ name="confirmPassword"
133
+ required
134
+ data-same-as="#password"
135
+ placeholder="Confirm your password"
136
+ autocomplete="new-password"
137
+ />
138
+ </div>
139
+
140
+ <button>Create Account</button>
141
+ </form>
142
+
143
+ <div
144
+ id="successMessage"
145
+ class="success-message"
146
+ style="display: none"
147
+ >
148
+ ✅ Form submitted successfully! Passwords match.
149
+ </div>
150
+ </div>
151
+
152
+ <div class="validation-info">
153
+ <h4>How it works:</h4>
154
+ <ul>
155
+ <li>
156
+ <strong>data-same-as="#password"</strong> - Links the confirm
157
+ password field to the original password field
158
+ </li>
159
+ <li>Validation triggers when you try to submit the form</li>
160
+ <li>If passwords don't match, a validation message appears</li>
161
+ <li>
162
+ The default message for password fields is: "Ce mot de passe doit
163
+ être identique au précédent."
164
+ </li>
165
+ <li>Form submission is prevented until passwords match</li>
166
+ </ul>
167
+ </div>
168
+ </div>
169
+
170
+ <div class="demo-container">
171
+ <h3>Test Scenarios</h3>
172
+ <div class="validation-info">
173
+ <h4>Try these test cases:</h4>
174
+ <ol>
175
+ <li>
176
+ <strong>Empty fields</strong>: Click submit with empty passwords
177
+ (required validation triggers first)
178
+ </li>
179
+ <li>
180
+ <strong>Short password</strong>: Enter less than 6 characters
181
+ (minlength validation)
182
+ </li>
183
+ <li>
184
+ <strong>Mismatched passwords</strong>: Enter different passwords
185
+ and click submit
186
+ </li>
187
+ <li>
188
+ <strong>Matching passwords</strong>: Enter identical passwords to
189
+ see successful submission
190
+ </li>
191
+ <li>
192
+ <strong>Real-time feedback</strong>: Type in one field, then the
193
+ other, then submit
194
+ </li>
195
+ </ol>
196
+ </div>
197
+ </div>
198
+ </div>
199
+
200
+ <script type="module">
201
+ import {
202
+ installCustomConstraintValidation,
203
+ forwardActionRequested,
204
+ } from "@jsenv/navi";
205
+
206
+ // Install validation on the form and its inputs
207
+ const form = document.getElementById("passwordForm");
208
+ const passwordInput = document.getElementById("password");
209
+ const confirmPasswordInput = document.getElementById("confirmPassword");
210
+ const successMessage = document.getElementById("successMessage");
211
+ const submitButton = form.querySelector("button");
212
+
213
+ // Install custom constraint validation on the form
214
+ installCustomConstraintValidation(form);
215
+ installCustomConstraintValidation(passwordInput);
216
+ installCustomConstraintValidation(confirmPasswordInput);
217
+ installCustomConstraintValidation(submitButton);
218
+
219
+ form.addEventListener("actionrequested", (e) => {
220
+ forwardActionRequested(e, () => {
221
+ // noop
222
+ console.log("execute form action");
223
+ });
224
+ });
225
+
226
+ // Handle form submission
227
+ form.addEventListener("action", (event) => {
228
+ console.log("Form action event triggered:", event.detail);
229
+
230
+ // Simulate successful form submission
231
+ successMessage.style.display = "block";
232
+ form.reset();
233
+
234
+ // Hide success message after 3 seconds
235
+ setTimeout(() => {
236
+ successMessage.style.display = "none";
237
+ }, 3000);
238
+ });
239
+
240
+ // Handle validation prevention
241
+ form.addEventListener("actionprevented", (event) => {
242
+ console.log(
243
+ "Form submission prevented due to validation:",
244
+ event.detail,
245
+ );
246
+ successMessage.style.display = "none";
247
+ });
248
+
249
+ // Add some debugging
250
+ passwordInput.addEventListener("input", () => {
251
+ console.log("Password value:", passwordInput.value);
252
+ });
253
+
254
+ confirmPasswordInput.addEventListener("input", () => {
255
+ console.log("Confirm password value:", confirmPasswordInput.value);
256
+ });
257
+ </script>
258
+ </body>
259
+ </html>
@@ -0,0 +1,106 @@
1
+ import { createPubSub } from "@jsenv/dom";
2
+
3
+ export const listenInputChange = (input, callback) => {
4
+ const [teardown, addTeardown] = createPubSub();
5
+
6
+ let valueAtInteraction;
7
+ const oninput = () => {
8
+ valueAtInteraction = undefined;
9
+ };
10
+ const onkeydown = (e) => {
11
+ if (e.key === "Enter") {
12
+ /**
13
+ * Browser trigger a "change" event right after the enter is pressed
14
+ * if the input value has changed.
15
+ * We need to prevent the next change event otherwise we would request action twice
16
+ */
17
+ valueAtInteraction = input.value;
18
+ }
19
+ if (e.key === "Escape") {
20
+ /**
21
+ * Browser trigger a "change" event right after the escape is pressed
22
+ * if the input value has changed.
23
+ * We need to prevent the next change event otherwise we would request action when
24
+ * we actually want to cancel
25
+ */
26
+ valueAtInteraction = input.value;
27
+ }
28
+ };
29
+ const onchange = (e) => {
30
+ if (
31
+ valueAtInteraction !== undefined &&
32
+ e.target.value === valueAtInteraction
33
+ ) {
34
+ valueAtInteraction = undefined;
35
+ return;
36
+ }
37
+ callback(e);
38
+ };
39
+ input.addEventListener("input", oninput);
40
+ input.addEventListener("keydown", onkeydown);
41
+ input.addEventListener("change", onchange);
42
+ addTeardown(() => {
43
+ input.removeEventListener("input", oninput);
44
+ input.removeEventListener("keydown", onkeydown);
45
+ input.removeEventListener("change", onchange);
46
+ });
47
+
48
+ programmatic_change: {
49
+ // Handle programmatic value changes that don't trigger browser change events
50
+ //
51
+ // Problem: When input values are set programmatically (not by user typing),
52
+ // browsers don't fire the 'change' event. However, our application logic
53
+ // still needs to detect these changes.
54
+ //
55
+ // Example scenario:
56
+ // 1. User starts editing (letter key pressed, value set programmatically)
57
+ // 2. User doesn't type anything additional (this is the key part)
58
+ // 3. User clicks outside to finish editing
59
+ // 4. Without this code, no change event would fire despite the fact that the input value did change from its original state
60
+ //
61
+ // This distinction is crucial because:
62
+ //
63
+ // - If the user typed additional text after the initial programmatic value,
64
+ // the browser would fire change events normally
65
+ // - But when they don't type anything else, the browser considers it as "no user interaction"
66
+ // even though the programmatic initial value represents a meaningful change
67
+ //
68
+ // We achieve this by checking if the input value has changed between focus and blur without any user interaction
69
+ // if yes we fire the callback because input value did change
70
+ let valueAtStart = input.value;
71
+ let interacted = false;
72
+
73
+ const onfocus = () => {
74
+ interacted = false;
75
+ valueAtStart = input.value;
76
+ };
77
+ const oninput = (e) => {
78
+ if (!e.isTrusted) {
79
+ // non trusted "input" events will be ignored by the browser when deciding to fire "change" event
80
+ // we ignore them too
81
+ return;
82
+ }
83
+ interacted = true;
84
+ };
85
+ const onblur = (e) => {
86
+ if (interacted) {
87
+ return;
88
+ }
89
+ if (valueAtStart === input.value) {
90
+ return;
91
+ }
92
+ callback(e);
93
+ };
94
+
95
+ input.addEventListener("focus", onfocus);
96
+ input.addEventListener("input", oninput);
97
+ input.addEventListener("blur", onblur);
98
+ addTeardown(() => {
99
+ input.removeEventListener("focus", onfocus);
100
+ input.removeEventListener("input", oninput);
101
+ input.removeEventListener("blur", onblur);
102
+ });
103
+ }
104
+
105
+ return teardown;
106
+ };