@rsuci/shared-form-components 1.0.83 → 1.0.84
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.
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Composant DatePicker - Sélecteur de date et date-heure
|
|
3
3
|
* RSU v2 - Moteur de Rendu des Formulaires d'Enquête
|
|
4
|
+
*
|
|
5
|
+
* Supporte la saisie manuelle (dd/MM/yyyy ou dd/MM/yyyy HH:mm)
|
|
6
|
+
* ET la sélection via le calendrier natif du navigateur.
|
|
4
7
|
*/
|
|
5
8
|
import React from 'react';
|
|
6
9
|
import { VariableFormulaire, VariableValue } from '../../types/enquete';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DatePicker.d.ts","sourceRoot":"","sources":["../../../src/components/inputs/DatePicker.tsx"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"DatePicker.d.ts","sourceRoot":"","sources":["../../../src/components/inputs/DatePicker.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAmD,MAAM,OAAO,CAAC;AAExE,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAKxE,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,kBAAkB,GAAG;QAAE,QAAQ,EAAE,MAAM,GAAG,WAAW,CAAA;KAAE,CAAC;IAClE,KAAK,EAAE,aAAa,CAAC;IACrB,QAAQ,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IACzC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAuDD,QAAA,MAAM,UAAU,EAAE,KAAK,CAAC,EAAE,CAAC,eAAe,CA+JzC,CAAC;AAEF,eAAe,UAAU,CAAC"}
|
|
@@ -1,57 +1,178 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Composant DatePicker - Sélecteur de date et date-heure
|
|
3
3
|
* RSU v2 - Moteur de Rendu des Formulaires d'Enquête
|
|
4
|
+
*
|
|
5
|
+
* Supporte la saisie manuelle (dd/MM/yyyy ou dd/MM/yyyy HH:mm)
|
|
6
|
+
* ET la sélection via le calendrier natif du navigateur.
|
|
4
7
|
*/
|
|
5
8
|
'use client';
|
|
6
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
9
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
10
|
+
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
11
|
+
import { Calendar } from 'lucide-react';
|
|
7
12
|
import { VariableValueConverter } from '../../lib/utils/variableValueConverter';
|
|
8
13
|
import { applyComponentStyle } from '../../utils/styleUtils';
|
|
9
14
|
import { isComponentReadonly, readonlyClasses } from '../../utils/componentStateUtils';
|
|
15
|
+
/**
|
|
16
|
+
* Applique le masque de saisie date sur une chaîne de chiffres bruts.
|
|
17
|
+
* DATE: dd/MM/yyyy (max 8 chiffres → 10 chars avec séparateurs)
|
|
18
|
+
* DATEHEURE: dd/MM/yyyy HH:mm (max 12 chiffres → 16 chars avec séparateurs)
|
|
19
|
+
*/
|
|
20
|
+
function applyDateMask(raw, isDateTime) {
|
|
21
|
+
// Ne garder que les chiffres
|
|
22
|
+
const digits = raw.replace(/\D/g, '');
|
|
23
|
+
const maxDigits = isDateTime ? 12 : 8;
|
|
24
|
+
const limited = digits.slice(0, maxDigits);
|
|
25
|
+
let result = '';
|
|
26
|
+
for (let i = 0; i < limited.length; i++) {
|
|
27
|
+
// Séparateurs pour la partie date: après position 2 et 4 (dd/MM/yyyy)
|
|
28
|
+
if (i === 2 || i === 4)
|
|
29
|
+
result += '/';
|
|
30
|
+
// Séparateurs pour la partie heure: espace après position 8, : après position 10
|
|
31
|
+
if (i === 8)
|
|
32
|
+
result += ' ';
|
|
33
|
+
if (i === 10)
|
|
34
|
+
result += ':';
|
|
35
|
+
result += limited[i];
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Convertit une Date JS en chaîne d'affichage dd/MM/yyyy ou dd/MM/yyyy HH:mm
|
|
41
|
+
*/
|
|
42
|
+
function dateToDisplay(date, isDateTime) {
|
|
43
|
+
const d = date.getDate().toString().padStart(2, '0');
|
|
44
|
+
const m = (date.getMonth() + 1).toString().padStart(2, '0');
|
|
45
|
+
const y = date.getFullYear();
|
|
46
|
+
if (isDateTime) {
|
|
47
|
+
const hh = date.getHours().toString().padStart(2, '0');
|
|
48
|
+
const mm = date.getMinutes().toString().padStart(2, '0');
|
|
49
|
+
return `${d}/${m}/${y} ${hh}:${mm}`;
|
|
50
|
+
}
|
|
51
|
+
return `${d}/${m}/${y}`;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Convertit une Date JS en format ISO pour l'input natif caché
|
|
55
|
+
*/
|
|
56
|
+
function dateToIso(date, isDateTime) {
|
|
57
|
+
const y = date.getFullYear();
|
|
58
|
+
const m = (date.getMonth() + 1).toString().padStart(2, '0');
|
|
59
|
+
const d = date.getDate().toString().padStart(2, '0');
|
|
60
|
+
if (isDateTime) {
|
|
61
|
+
const hh = date.getHours().toString().padStart(2, '0');
|
|
62
|
+
const mm = date.getMinutes().toString().padStart(2, '0');
|
|
63
|
+
return `${y}-${m}-${d}T${hh}:${mm}`;
|
|
64
|
+
}
|
|
65
|
+
return `${y}-${m}-${d}`;
|
|
66
|
+
}
|
|
10
67
|
const DatePicker = ({ variable, value, onChange, onBlur, error, disabled, isConsultationMode = false }) => {
|
|
11
68
|
const props = variable.proprietes;
|
|
12
69
|
const { textStyle, containerStyle } = applyComponentStyle(variable.componentStyle);
|
|
13
|
-
|
|
70
|
+
const isDateTime = variable.typeCode === 'DATEHEURE';
|
|
14
71
|
const isReadonly = isComponentReadonly(variable, isConsultationMode);
|
|
15
|
-
|
|
72
|
+
const hiddenInputRef = useRef(null);
|
|
73
|
+
const isTypingRef = useRef(false);
|
|
74
|
+
// État local pour le texte affiché dans l'input
|
|
75
|
+
const [displayValue, setDisplayValue] = useState('');
|
|
76
|
+
const [hasFormatError, setHasFormatError] = useState(false);
|
|
77
|
+
// Synchroniser la prop value → displayValue (quand la valeur change depuis l'extérieur)
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (isTypingRef.current)
|
|
80
|
+
return; // Ne pas écraser pendant la saisie
|
|
81
|
+
if (!value || value === '') {
|
|
82
|
+
setDisplayValue('');
|
|
83
|
+
setHasFormatError(false);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const parsed = VariableValueConverter.parse(value, variable.typeCode);
|
|
87
|
+
if (parsed && !isNaN(parsed.getTime())) {
|
|
88
|
+
setDisplayValue(dateToDisplay(parsed, isDateTime));
|
|
89
|
+
setHasFormatError(false);
|
|
90
|
+
}
|
|
91
|
+
}, [value, variable.typeCode, isDateTime]);
|
|
92
|
+
// Valeur ISO pour l'input natif caché
|
|
93
|
+
const isoValue = (() => {
|
|
94
|
+
const parsed = VariableValueConverter.parse(value, variable.typeCode);
|
|
95
|
+
if (parsed && !isNaN(parsed.getTime())) {
|
|
96
|
+
return dateToIso(parsed, isDateTime);
|
|
97
|
+
}
|
|
98
|
+
return '';
|
|
99
|
+
})();
|
|
100
|
+
const inputType = isDateTime ? 'datetime-local' : 'date';
|
|
101
|
+
const placeholder = isDateTime ? 'jj/MM/aaaa HH:mm' : 'jj/MM/aaaa';
|
|
102
|
+
const maxLength = isDateTime ? 16 : 10;
|
|
103
|
+
// Classes CSS
|
|
16
104
|
const getInputClasses = () => {
|
|
17
|
-
const baseClasses = 'w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent text-gray-900';
|
|
18
|
-
const
|
|
105
|
+
const baseClasses = 'w-full px-3 py-2 pr-10 border rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent text-gray-900';
|
|
106
|
+
const borderClass = (error || hasFormatError) ? 'border-red-500' : 'border-gray-300';
|
|
19
107
|
if (disabled)
|
|
20
|
-
return `${baseClasses} ${
|
|
108
|
+
return `${baseClasses} ${borderClass} bg-gray-100 cursor-not-allowed text-gray-500`;
|
|
21
109
|
if (isReadonly)
|
|
22
|
-
return `${baseClasses} ${
|
|
23
|
-
return `${baseClasses} ${
|
|
110
|
+
return `${baseClasses} ${borderClass} ${readonlyClasses.readonly}`;
|
|
111
|
+
return `${baseClasses} ${borderClass} bg-white`;
|
|
24
112
|
};
|
|
25
|
-
//
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
113
|
+
// Saisie manuelle avec masque
|
|
114
|
+
const handleTextChange = useCallback((e) => {
|
|
115
|
+
isTypingRef.current = true;
|
|
116
|
+
const masked = applyDateMask(e.target.value, isDateTime);
|
|
117
|
+
setDisplayValue(masked);
|
|
118
|
+
setHasFormatError(false);
|
|
119
|
+
}, [isDateTime]);
|
|
120
|
+
// Validation au blur
|
|
121
|
+
const handleBlur = useCallback(() => {
|
|
122
|
+
isTypingRef.current = false;
|
|
123
|
+
if (!displayValue || displayValue.trim() === '') {
|
|
124
|
+
setHasFormatError(false);
|
|
125
|
+
onChange('');
|
|
126
|
+
onBlur?.();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// Vérifier que la longueur est complète
|
|
130
|
+
const expectedLength = isDateTime ? 16 : 10;
|
|
131
|
+
if (displayValue.length < expectedLength) {
|
|
132
|
+
setHasFormatError(true);
|
|
133
|
+
onBlur?.();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
// Tenter de parser
|
|
137
|
+
const parsed = VariableValueConverter.parse(displayValue, variable.typeCode);
|
|
138
|
+
if (parsed && !isNaN(parsed.getTime())) {
|
|
139
|
+
const serialized = VariableValueConverter.serialize(parsed, variable.typeCode);
|
|
140
|
+
setHasFormatError(false);
|
|
141
|
+
onChange(serialized);
|
|
37
142
|
}
|
|
38
143
|
else {
|
|
39
|
-
|
|
144
|
+
setHasFormatError(true);
|
|
40
145
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
146
|
+
onBlur?.();
|
|
147
|
+
}, [displayValue, isDateTime, variable.typeCode, onChange, onBlur]);
|
|
148
|
+
// Sélection via le calendrier natif
|
|
149
|
+
const handlePickerChange = useCallback((e) => {
|
|
44
150
|
if (!e.target.value) {
|
|
45
|
-
|
|
151
|
+
setDisplayValue('');
|
|
152
|
+
onChange('');
|
|
46
153
|
return;
|
|
47
154
|
}
|
|
48
155
|
const newDate = new Date(e.target.value);
|
|
49
156
|
if (!isNaN(newDate.getTime())) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
157
|
+
setDisplayValue(dateToDisplay(newDate, isDateTime));
|
|
158
|
+
setHasFormatError(false);
|
|
159
|
+
const serialized = VariableValueConverter.serialize(newDate, variable.typeCode);
|
|
160
|
+
onChange(serialized);
|
|
53
161
|
}
|
|
54
|
-
};
|
|
55
|
-
|
|
162
|
+
}, [isDateTime, variable.typeCode, onChange]);
|
|
163
|
+
// Ouvrir le picker natif
|
|
164
|
+
const handleCalendarClick = useCallback(() => {
|
|
165
|
+
if (hiddenInputRef.current) {
|
|
166
|
+
try {
|
|
167
|
+
hiddenInputRef.current.showPicker();
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
// Fallback: focus si showPicker() n'est pas supporté
|
|
171
|
+
hiddenInputRef.current.focus();
|
|
172
|
+
hiddenInputRef.current.click();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}, []);
|
|
176
|
+
return (_jsxs("div", { style: containerStyle, className: "relative", children: [_jsx("input", { type: "text", value: displayValue, onChange: handleTextChange, onBlur: handleBlur, placeholder: placeholder, maxLength: maxLength, disabled: disabled, readOnly: isReadonly, style: textStyle, className: getInputClasses() }), !disabled && !isReadonly && (_jsx("button", { type: "button", onClick: handleCalendarClick, className: "absolute right-2 top-1/2 -translate-y-1/2 p-1 text-gray-400 hover:text-gray-600 focus:outline-none", tabIndex: -1, "aria-label": "Ouvrir le calendrier", children: _jsx(Calendar, { className: "h-4 w-4" }) })), _jsx("input", { ref: hiddenInputRef, type: inputType, value: isoValue, onChange: handlePickerChange, min: props?.minDate ? dateToIso(props.minDate, isDateTime) : undefined, max: props?.maxDate ? dateToIso(props.maxDate, isDateTime) : undefined, className: "sr-only", tabIndex: -1, "aria-hidden": "true" })] }));
|
|
56
177
|
};
|
|
57
178
|
export default DatePicker;
|
package/package.json
CHANGED