@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.
- package/dist/jsenv_navi.js +470 -309
- package/index.js +5 -0
- package/package.json +1 -1
- package/src/components/callout/callout.js +3 -1
- package/src/components/details/details.jsx +7 -8
- package/src/components/field/button.jsx +6 -63
- package/src/components/field/checkbox_list.jsx +0 -1
- package/src/components/field/form.jsx +2 -17
- package/src/components/field/input_checkbox.jsx +0 -1
- package/src/components/field/input_textual.jsx +5 -142
- package/src/components/field/radio_list.jsx +0 -1
- package/src/components/field/select.jsx +0 -1
- package/src/components/field/use_form_events.js +4 -0
- package/src/components/keyboard_shortcuts/keyboard_shortcuts.js +1 -1
- package/src/validation/constraints/native_constraints.js +43 -18
- package/src/validation/constraints/same_as_constraint.js +42 -0
- package/src/validation/custom_constraint_validation.js +254 -59
- package/src/validation/demos/demo_same_as_constraint.html +259 -0
- package/src/validation/input_change_effect.js +106 -0
|
@@ -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
|
+
};
|