@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/dist/jsenv_navi.js +478 -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 +262 -59
- package/src/validation/demos/demo_same_as_constraint.html +259 -0
- package/src/validation/input_change_effect.js +106 -0
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
|
@@ -86,7 +86,9 @@ export const openCallout = (
|
|
|
86
86
|
addTeardown(onClose);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
const [updateLevel, addLevelEffect] =
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
472
|
-
|
|
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}
|
|
@@ -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 {
|
|
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
|
-
|
|
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}>
|
|
@@ -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 {
|
|
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) => {
|
|
@@ -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 (
|
|
75
|
-
return
|
|
74
|
+
if (element.value) {
|
|
75
|
+
return null;
|
|
76
76
|
}
|
|
77
|
-
|
|
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 (
|
|
93
|
-
|
|
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
|
-
|
|
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
|
|
169
|
+
return `${thisField} doit contenir au moins ${minLength} caractère (il contient actuellement un seul caractère).`;
|
|
154
170
|
}
|
|
155
|
-
return
|
|
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
|
-
|
|
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
|
+
};
|