@jsenv/navi 0.9.3 → 0.10.0

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.js CHANGED
@@ -107,6 +107,11 @@ export {
107
107
  addCustomMessage,
108
108
  removeCustomMessage,
109
109
  } from "./src/validation/custom_message.js";
110
+ // advanced constraint validation functions
111
+ export {
112
+ forwardActionRequested,
113
+ installCustomConstraintValidation,
114
+ } from "./src/validation/custom_constraint_validation.js";
110
115
 
111
116
  // Other
112
117
  export { useDependenciesDiff } from "./src/components/use_dependencies_diff.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/navi",
3
- "version": "0.9.3",
3
+ "version": "0.10.0",
4
4
  "description": "Library of components including navigation to create frontend applications",
5
5
  "repository": {
6
6
  "type": "git",
@@ -86,7 +86,9 @@ export const openCallout = (
86
86
  addTeardown(onClose);
87
87
  }
88
88
 
89
- const [updateLevel, addLevelEffect] = createValueEffect(undefined);
89
+ const [updateLevel, addLevelEffect, cleanupLevelEffects] =
90
+ createValueEffect(undefined);
91
+ addTeardown(cleanupLevelEffects);
90
92
 
91
93
  // Create and add callout to document
92
94
  const calloutElement = createCalloutElement();
@@ -16,33 +16,33 @@ import { SummaryMarker } from "./summary_marker.jsx";
16
16
 
17
17
  import.meta.css = /* css */ `
18
18
  .navi_details {
19
- display: flex;
20
- flex-direction: column;
21
19
  position: relative;
22
20
  z-index: 1;
21
+ display: flex;
23
22
  flex-shrink: 0;
23
+ flex-direction: column;
24
24
  }
25
25
 
26
26
  .navi_details > summary {
27
- flex-shrink: 0;
28
- cursor: pointer;
29
27
  display: flex;
28
+ flex-shrink: 0;
30
29
  flex-direction: column;
30
+ cursor: pointer;
31
31
  user-select: none;
32
32
  }
33
33
  .summary_body {
34
34
  display: flex;
35
+ width: 100%;
35
36
  flex-direction: row;
36
37
  align-items: center;
37
- width: 100%;
38
38
  gap: 0.2em;
39
39
  }
40
40
  .summary_label {
41
41
  display: flex;
42
+ padding-right: 10px;
42
43
  flex: 1;
43
- gap: 0.2em;
44
44
  align-items: center;
45
- padding-right: 10px;
45
+ gap: 0.2em;
46
46
  }
47
47
 
48
48
  .navi_details > summary:focus {
@@ -230,7 +230,6 @@ const DetailsWithAction = forwardRef((props, ref) => {
230
230
  const isOpen = toggleEvent.newState === "open";
231
231
  if (isOpen) {
232
232
  requestAction(toggleEvent.target, effectiveAction, {
233
- actionOrigin: "action_prop",
234
233
  event: toggleEvent,
235
234
  method: "run",
236
235
  });
@@ -8,7 +8,7 @@ import {
8
8
 
9
9
  import { getActionPrivateProperties } from "../../action_private_properties.js";
10
10
  import { useActionStatus } from "../../use_action_status.js";
11
- import { requestAction } from "../../validation/custom_constraint_validation.js";
11
+ import { forwardActionRequested } from "../../validation/custom_constraint_validation.js";
12
12
  import { useConstraints } from "../../validation/hooks/use_constraints.js";
13
13
  import { FormActionContext } from "../action_execution/form_context.js";
14
14
  import { renderActionableComponent } from "../action_execution/render_actionable_component.jsx";
@@ -285,7 +285,6 @@ const ButtonWithAction = forwardRef((props, ref) => {
285
285
  const {
286
286
  action,
287
287
  loading,
288
- onClick,
289
288
  actionErrorEffect,
290
289
  onActionPrevented,
291
290
  onActionStart,
@@ -302,22 +301,16 @@ const ButtonWithAction = forwardRef((props, ref) => {
302
301
  errorEffect: actionErrorEffect,
303
302
  });
304
303
 
304
+ const innerLoading = loading || actionLoading;
305
+
305
306
  useActionEvents(innerRef, {
306
307
  onPrevented: onActionPrevented,
308
+ onRequested: (e) => forwardActionRequested(e, boundAction),
307
309
  onAction: executeAction,
308
310
  onStart: onActionStart,
309
311
  onError: onActionError,
310
312
  onEnd: onActionEnd,
311
313
  });
312
- const handleClick = (event) => {
313
- event.preventDefault();
314
- const button = innerRef.current;
315
- requestAction(button, boundAction, {
316
- event,
317
- actionOrigin: "action_prop",
318
- });
319
- };
320
- const innerLoading = loading || actionLoading;
321
314
 
322
315
  return (
323
316
  <ButtonBasic
@@ -326,10 +319,6 @@ const ButtonWithAction = forwardRef((props, ref) => {
326
319
  {...rest}
327
320
  ref={innerRef}
328
321
  loading={innerLoading}
329
- onClick={(event) => {
330
- handleClick(event);
331
- onClick?.(event);
332
- }}
333
322
  >
334
323
  {children}
335
324
  </ButtonBasic>
@@ -341,7 +330,6 @@ const ButtonInsideForm = forwardRef((props, ref) => {
341
330
  // eslint-disable-next-line no-unused-vars
342
331
  formContext,
343
332
  type,
344
- onClick,
345
333
  children,
346
334
  loading,
347
335
  readOnly,
@@ -350,40 +338,8 @@ const ButtonInsideForm = forwardRef((props, ref) => {
350
338
  const innerRef = useRef();
351
339
  useImperativeHandle(ref, () => innerRef.current);
352
340
 
353
- const wouldSubmitFormByType = type === "submit" || type === "image";
354
341
  const innerLoading = loading;
355
342
  const innerReadOnly = readOnly;
356
- const handleClick = (event) => {
357
- const buttonElement = innerRef.current;
358
- const { form } = buttonElement;
359
- let wouldSubmitForm = wouldSubmitFormByType;
360
- if (!wouldSubmitForm && type === undefined) {
361
- const formSubmitButton = form.querySelector(
362
- "button[type='submit'], input[type='submit'], input[type='image']",
363
- );
364
- const wouldSubmitFormBecauseSingleButtonWithoutType = !formSubmitButton;
365
- wouldSubmitForm = wouldSubmitFormBecauseSingleButtonWithoutType;
366
- }
367
- if (!wouldSubmitForm) {
368
- if (buttonElement.hasAttribute("data-readonly")) {
369
- event.preventDefault();
370
- }
371
- return;
372
- }
373
- // prevent default behavior that would submit the form
374
- // we want to go through the action execution process (with validation and all)
375
- event.preventDefault();
376
- form.dispatchEvent(
377
- new CustomEvent("actionrequested", {
378
- detail: {
379
- requester: buttonElement,
380
- event,
381
- meta: { isSubmit: true },
382
- actionOrigin: "action_prop",
383
- },
384
- }),
385
- );
386
- };
387
343
 
388
344
  return (
389
345
  <ButtonBasic
@@ -392,10 +348,6 @@ const ButtonInsideForm = forwardRef((props, ref) => {
392
348
  type={type}
393
349
  loading={innerLoading}
394
350
  readOnly={innerReadOnly}
395
- onClick={(event) => {
396
- handleClick(event);
397
- onClick?.(event);
398
- }}
399
351
  >
400
352
  {children}
401
353
  </ButtonBasic>
@@ -411,7 +363,6 @@ const ButtonWithActionInsideForm = forwardRef((props, ref) => {
411
363
  action,
412
364
  loading,
413
365
  children,
414
- onClick,
415
366
  onActionPrevented,
416
367
  onActionStart,
417
368
  onActionAbort,
@@ -468,16 +419,8 @@ const ButtonWithActionInsideForm = forwardRef((props, ref) => {
468
419
  ref={innerRef}
469
420
  type={type}
470
421
  loading={innerLoading}
471
- onClick={(event) => {
472
- const button = innerRef.current;
473
- const form = button.form;
474
- event.preventDefault();
475
- requestAction(form, actionBoundToFormParams, {
476
- event,
477
- requester: button,
478
- actionOrigin: "action_prop",
479
- });
480
- onClick?.(event);
422
+ onactionrequested={(e) => {
423
+ forwardActionRequested(e, actionBoundToFormParams, e.target.form);
481
424
  }}
482
425
  >
483
426
  {children}
@@ -172,7 +172,6 @@ const CheckboxListWithAction = forwardRef((props, ref) => {
172
172
  requestAction(checkboxList, boundAction, {
173
173
  event,
174
174
  requester: checkbox,
175
- actionOrigin: "action_prop",
176
175
  });
177
176
  }}
178
177
  loading={loading || actionLoading}
@@ -16,7 +16,7 @@
16
16
  import { forwardRef } from "preact/compat";
17
17
  import { useContext, useImperativeHandle, useMemo, useRef } from "preact/hooks";
18
18
 
19
- import { requestAction } from "../../validation/custom_constraint_validation.js";
19
+ import { forwardActionRequested } from "../../validation/custom_constraint_validation.js";
20
20
  import { useConstraints } from "../../validation/hooks/use_constraints.js";
21
21
  import {
22
22
  FormActionContext,
@@ -150,13 +150,7 @@ const FormWithAction = forwardRef((props, ref) => {
150
150
  useActionEvents(innerRef, {
151
151
  onPrevented: onActionPrevented,
152
152
  onRequested: (e) => {
153
- const form = innerRef.current;
154
- requestAction(form, actionBoundToUIState, {
155
- requester: e.detail?.requester,
156
- event: e.detail?.event || e,
157
- meta: e.detail?.meta,
158
- actionOrigin: e.detail?.actionOrigin,
159
- });
153
+ forwardActionRequested(e, actionBoundToUIState);
160
154
  },
161
155
  onAction: (e) => {
162
156
  const form = innerRef.current;
@@ -194,15 +188,6 @@ const FormWithAction = forwardRef((props, ref) => {
194
188
  {...rest}
195
189
  ref={innerRef}
196
190
  loading={innerLoading}
197
- onrequestsubmit={(e) => {
198
- // prevent "submit" event that would be dispatched by the browser after form.requestSubmit()
199
- // (not super important because our <form> listen the "action" and do does preventDefault on "submit")
200
- e.preventDefault();
201
- requestAction(e.target, actionBoundToUIState, {
202
- event: e,
203
- actionOrigin: "action_prop",
204
- });
205
- }}
206
191
  >
207
192
  <FormActionContext.Provider value={actionBoundToUIState}>
208
193
  <LoadingElementContext.Provider value={formActionRequester}>
@@ -425,7 +425,6 @@ const InputCheckboxWithAction = forwardRef((props, ref) => {
425
425
  onChange={(e) => {
426
426
  requestAction(e.target, actionBoundToUIState, {
427
427
  event: e,
428
- actionOrigin: "action_prop",
429
428
  });
430
429
  onChange?.(e);
431
430
  }}
@@ -19,14 +19,13 @@
19
19
  import { forwardRef } from "preact/compat";
20
20
  import {
21
21
  useContext,
22
- useEffect,
23
22
  useImperativeHandle,
24
23
  useLayoutEffect,
25
24
  useRef,
26
25
  } from "preact/hooks";
27
26
 
28
27
  import { useActionStatus } from "../../use_action_status.js";
29
- import { requestAction } from "../../validation/custom_constraint_validation.js";
28
+ import { forwardActionRequested } from "../../validation/custom_constraint_validation.js";
30
29
  import { useConstraints } from "../../validation/hooks/use_constraints.js";
31
30
  import { renderActionableComponent } from "../action_execution/render_actionable_component.jsx";
32
31
  import { useActionBoundToOneParam } from "../action_execution/use_action.js";
@@ -274,8 +273,6 @@ const InputTextualWithAction = forwardRef((props, ref) => {
274
273
  cancelOnBlurInvalid,
275
274
  cancelOnEscape,
276
275
  actionErrorEffect,
277
- onInput,
278
- onKeyDown,
279
276
  ...rest
280
277
  } = props;
281
278
  const innerRef = useRef(null);
@@ -285,21 +282,6 @@ const InputTextualWithAction = forwardRef((props, ref) => {
285
282
  const executeAction = useExecuteAction(innerRef, {
286
283
  errorEffect: actionErrorEffect,
287
284
  });
288
- const valueAtInteractionRef = useRef(null);
289
-
290
- useOnInputChange(innerRef, (e) => {
291
- if (
292
- valueAtInteractionRef.current !== null &&
293
- e.target.value === valueAtInteractionRef.current
294
- ) {
295
- valueAtInteractionRef.current = null;
296
- return;
297
- }
298
- requestAction(e.target, boundAction, {
299
- event: e,
300
- actionOrigin: "action_prop",
301
- });
302
- });
303
285
  // here updating the input won't call the associated action
304
286
  // (user have to blur or press enter for this to happen)
305
287
  // so we can keep the ui state on cancel/abort/error and let user decide
@@ -322,16 +304,12 @@ const InputTextualWithAction = forwardRef((props, ref) => {
322
304
  if (!cancelOnEscape) {
323
305
  return;
324
306
  }
325
- /**
326
- * Browser trigger a "change" event right after the escape is pressed
327
- * if the input value has changed.
328
- * We need to prevent the next change event otherwise we would request action when
329
- * we actually want to cancel
330
- */
331
- valueAtInteractionRef.current = e.target.value;
332
307
  }
333
308
  onCancel?.(e, reason);
334
309
  },
310
+ onRequested: (e) => {
311
+ forwardActionRequested(e, boundAction);
312
+ },
335
313
  onPrevented: onActionPrevented,
336
314
  onAction: executeAction,
337
315
  onStart: onActionStart,
@@ -345,135 +323,20 @@ const InputTextualWithAction = forwardRef((props, ref) => {
345
323
  {...rest}
346
324
  ref={innerRef}
347
325
  loading={loading || actionLoading}
348
- onInput={(e) => {
349
- valueAtInteractionRef.current = null;
350
- onInput?.(e);
351
- }}
352
- onKeyDown={(e) => {
353
- if (e.key !== "Enter") {
354
- return;
355
- }
356
- e.preventDefault();
357
- /**
358
- * Browser trigger a "change" event right after the enter is pressed
359
- * if the input value has changed.
360
- * We need to prevent the next change event otherwise we would request action twice
361
- */
362
- valueAtInteractionRef.current = e.target.value;
363
- requestAction(e.target, boundAction, {
364
- event: e,
365
- actionOrigin: "action_prop",
366
- });
367
- onKeyDown?.(e);
368
- }}
369
326
  />
370
327
  );
371
328
  });
372
329
  const InputTextualInsideForm = forwardRef((props, ref) => {
373
330
  const {
374
- onKeyDown,
375
331
  // We destructure formContext to avoid passing it to the underlying input element
376
332
  // eslint-disable-next-line no-unused-vars
377
333
  formContext,
378
334
  ...rest
379
335
  } = props;
380
336
 
381
- return (
382
- <InputTextualBasic
383
- {...rest}
384
- ref={ref}
385
- onKeyDown={(e) => {
386
- if (e.key === "Enter") {
387
- const inputElement = e.target;
388
- const { form } = inputElement;
389
- const formSubmitButton = form.querySelector(
390
- "button[type='submit'], input[type='submit'], input[type='image']",
391
- );
392
- e.preventDefault();
393
- form.dispatchEvent(
394
- new CustomEvent("actionrequested", {
395
- detail: {
396
- requester: formSubmitButton ? formSubmitButton : inputElement,
397
- event: e,
398
- meta: { isSubmit: true },
399
- actionOrigin: "action_prop",
400
- },
401
- }),
402
- );
403
- }
404
- onKeyDown?.(e);
405
- }}
406
- />
407
- );
337
+ return <InputTextualBasic {...rest} ref={ref} />;
408
338
  });
409
339
 
410
- const useOnInputChange = (inputRef, callback) => {
411
- // we must use a custom event listener because preact bind onChange to onInput for compat with react
412
- useEffect(() => {
413
- const input = inputRef.current;
414
- input.addEventListener("change", callback);
415
- return () => {
416
- input.removeEventListener("change", callback);
417
- };
418
- }, [callback]);
419
-
420
- // Handle programmatic value changes that don't trigger browser change events
421
- //
422
- // Problem: When input values are set programmatically (not by user typing),
423
- // browsers don't fire the 'change' event. However, our application logic
424
- // still needs to detect these changes.
425
- //
426
- // Example scenario:
427
- // 1. User starts editing (letter key pressed, value set programmatically)
428
- // 2. User doesn't type anything additional (this is the key part)
429
- // 3. User clicks outside to finish editing
430
- // 4. Without this code, no change event would fire despite the fact that the input value did change from its original state
431
- //
432
- // This distinction is crucial because:
433
- //
434
- // - If the user typed additional text after the initial programmatic value,
435
- // the browser would fire change events normally
436
- // - But when they don't type anything else, the browser considers it as "no user interaction"
437
- // even though the programmatic initial value represents a meaningful change
438
- const valueAtStartRef = useRef();
439
- const interactedRef = useRef(false);
440
- useLayoutEffect(() => {
441
- const input = inputRef.current;
442
- valueAtStartRef.current = input.value;
443
-
444
- const onfocus = () => {
445
- interactedRef.current = false;
446
- valueAtStartRef.current = input.value;
447
- };
448
- const oninput = (e) => {
449
- if (!e.isTrusted) {
450
- // non trusted "input" events will be ignored by the browser when deciding to fire "change" event
451
- // we ignore them too
452
- return;
453
- }
454
- interactedRef.current = true;
455
- };
456
- const onblur = (e) => {
457
- if (interactedRef.current) {
458
- return;
459
- }
460
- if (valueAtStartRef.current === input.value) {
461
- return;
462
- }
463
- callback(e);
464
- };
465
-
466
- input.addEventListener("focus", onfocus);
467
- input.addEventListener("input", oninput);
468
- input.addEventListener("blur", onblur);
469
-
470
- return () => {
471
- input.removeEventListener("focus", onfocus);
472
- input.removeEventListener("input", oninput);
473
- input.removeEventListener("blur", onblur);
474
- };
475
- }, []);
476
- };
477
340
  // As explained in https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/datetime-local#setting_timezones
478
341
  // datetime-local does not support timezones
479
342
  const convertToLocalTimezone = (dateTimeString) => {
@@ -170,7 +170,6 @@ const RadioListWithAction = forwardRef((props, ref) => {
170
170
  requestAction(radioListContainer, boundAction, {
171
171
  event: e,
172
172
  requester: radio,
173
- actionOrigin: "action_prop",
174
173
  });
175
174
  }}
176
175
  loading={loading || actionLoading}
@@ -195,7 +195,6 @@ const SelectWithAction = forwardRef((props, ref) => {
195
195
  requestAction(radioListContainer, boundAction, {
196
196
  event,
197
197
  requester: optionSelected,
198
- actionOrigin: "action_prop",
199
198
  });
200
199
  }}
201
200
  {...rest}
@@ -7,6 +7,7 @@ export const useFormEvents = (
7
7
  elementRef,
8
8
  {
9
9
  onFormReset,
10
+ onFormActionRequested,
10
11
  onFormActionPrevented,
11
12
  onFormActionStart,
12
13
  onFormActionAbort,
@@ -15,6 +16,7 @@ export const useFormEvents = (
15
16
  },
16
17
  ) => {
17
18
  onFormReset = useStableCallback(onFormReset);
19
+ onFormActionRequested = useStableCallback(onFormActionRequested);
18
20
  onFormActionPrevented = useStableCallback(onFormActionPrevented);
19
21
  onFormActionStart = useStableCallback(onFormActionStart);
20
22
  onFormActionAbort = useStableCallback(onFormActionAbort);
@@ -38,6 +40,7 @@ export const useFormEvents = (
38
40
  }
39
41
  return addManyEventListeners(form, {
40
42
  reset: onFormReset,
43
+ actionrequested: onFormActionRequested,
41
44
  actionprevented: onFormActionPrevented,
42
45
  actionstart: onFormActionStart,
43
46
  actionabort: onFormActionAbort,
@@ -46,6 +49,7 @@ export const useFormEvents = (
46
49
  });
47
50
  }, [
48
51
  onFormReset,
52
+ onFormActionRequested,
49
53
  onFormActionPrevented,
50
54
  onFormActionStart,
51
55
  onFormActionAbort,
@@ -165,10 +165,10 @@ export const useKeyboardShortcuts = (
165
165
  }
166
166
  const { action } = shortcutCandidate;
167
167
  return requestAction(element, action, {
168
+ actionOrigin: "keyboard_shortcut",
168
169
  event: keyboardEvent,
169
170
  requester: document.activeElement,
170
171
  confirmMessage: shortcutCandidate.confirmMessage,
171
- actionOrigin: "keyboard_shortcut",
172
172
  meta: {
173
173
  shortcut: shortcutCandidate,
174
174
  },
@@ -71,10 +71,25 @@ export const REQUIRED_CONSTRAINT = {
71
71
  : undefined,
72
72
  };
73
73
  }
74
- if (!element.value) {
75
- return requiredMessage || `Veuillez remplir ce champ.`;
74
+ if (element.value) {
75
+ return null;
76
76
  }
77
- return null;
77
+ if (requiredMessage) {
78
+ return requiredMessage;
79
+ }
80
+ if (element.type === "password") {
81
+ return element.hasAttribute("data-same-as")
82
+ ? `Veuillez confirmer le mot de passe.`
83
+ : `Veuillez saisir un mot de passe.`;
84
+ }
85
+ if (element.type === "email") {
86
+ return element.hasAttribute("data-same-as")
87
+ ? `Veuillez confirmer l'adresse e-mail`
88
+ : `Veuillez saisir une adresse e-mail.`;
89
+ }
90
+ return element.hasAttribute("data-same-as")
91
+ ? `Veuillez confirmer le champ précédent`
92
+ : `Veuillez remplir ce champ.`;
78
93
  },
79
94
  };
80
95
  export const PATTERN_CONSTRAINT = {
@@ -89,19 +104,19 @@ export const PATTERN_CONSTRAINT = {
89
104
  return null;
90
105
  }
91
106
  const regex = new RegExp(pattern);
92
- if (!regex.test(value)) {
93
- const patternMessage = input.getAttribute("data-pattern-message");
94
- if (patternMessage) {
95
- return patternMessage;
96
- }
97
- let message = `Veuillez respecter le format requis.`;
98
- const title = input.title;
99
- if (title) {
100
- message += `<br />${title}`;
101
- }
102
- return message;
107
+ if (regex.test(value)) {
108
+ return null;
103
109
  }
104
- return null;
110
+ const patternMessage = input.getAttribute("data-pattern-message");
111
+ if (patternMessage) {
112
+ return patternMessage;
113
+ }
114
+ let message = `Veuillez respecter le format requis.`;
115
+ const title = input.title;
116
+ if (title) {
117
+ message += `<br />${title}`;
118
+ }
119
+ return message;
105
120
  },
106
121
  };
107
122
  // https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/email#validation
@@ -149,10 +164,11 @@ export const MIN_LENGTH_CONSTRAINT = {
149
164
  return null;
150
165
  }
151
166
  if (valueLength < minLength) {
167
+ const thisField = generateThisFieldText(element);
152
168
  if (valueLength === 1) {
153
- return `Ce champ doit contenir au moins ${minLength} caractère (il contient actuellement un seul caractère).`;
169
+ return `${thisField} doit contenir au moins ${minLength} caractère (il contient actuellement un seul caractère).`;
154
170
  }
155
- return `Ce champ doit contenir au moins ${minLength} caractères (il contient actuellement ${valueLength} caractères).`;
171
+ return `${thisField} doit contenir au moins ${minLength} caractères (il contient actuellement ${valueLength} caractères).`;
156
172
  }
157
173
  return null;
158
174
  },
@@ -166,6 +182,14 @@ const INPUT_TYPE_SUPPORTING_MIN_LENGTH_SET = new Set([
166
182
  "password",
167
183
  ]);
168
184
 
185
+ const generateThisFieldText = (field) => {
186
+ return field.type === "password"
187
+ ? "Ce mot de passe"
188
+ : field.type === "email"
189
+ ? "Cette adresse e-mail"
190
+ : "Ce champ";
191
+ };
192
+
169
193
  export const MAX_LENGTH_CONSTRAINT = {
170
194
  name: "max_length",
171
195
  check: (element) => {
@@ -185,7 +209,8 @@ export const MAX_LENGTH_CONSTRAINT = {
185
209
  const value = element.value;
186
210
  const valueLength = value.length;
187
211
  if (valueLength > maxLength) {
188
- return `Ce champ doit contenir au maximum ${maxLength} caractères (il contient actuellement ${valueLength} caractères).`;
212
+ const thisField = generateThisFieldText(element);
213
+ return `${thisField} doit contenir au maximum ${maxLength} caractères (il contient actuellement ${valueLength} caractères).`;
189
214
  }
190
215
  return null;
191
216
  },
@@ -0,0 +1,42 @@
1
+ export const SAME_AS_CONSTRAINT = {
2
+ name: "same_as",
3
+ check: (element) => {
4
+ const sameAs = element.getAttribute("data-same-as");
5
+ if (!sameAs) {
6
+ return null;
7
+ }
8
+
9
+ const otherElement = document.querySelector(sameAs);
10
+ if (!otherElement) {
11
+ console.warn(
12
+ `Same as constraint: could not find element for selector ${sameAs}`,
13
+ );
14
+ return null;
15
+ }
16
+
17
+ const value = element.value;
18
+ const otherValue = otherElement.value;
19
+ if (value === "" || otherValue === "") {
20
+ // don't validate if one of the two values is empty
21
+ return null;
22
+ }
23
+
24
+ if (value === otherValue) {
25
+ return null;
26
+ }
27
+
28
+ const message = element.getAttribute("data-same-as-message");
29
+ if (message) {
30
+ return message;
31
+ }
32
+
33
+ const type = element.type;
34
+ if (type === "password") {
35
+ return `Ce mot de passe doit être identique au précédent.`;
36
+ }
37
+ if (type === "email") {
38
+ return `Cette adresse e-mail doit être identique a la précédente.`;
39
+ }
40
+ return `Ce champ doit être identique au précédent.`;
41
+ },
42
+ };