@jsenv/navi 0.0.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/index.js +51 -0
- package/package.json +38 -0
- package/src/action_private_properties.js +11 -0
- package/src/action_proxy_test.html +353 -0
- package/src/action_run_states.js +5 -0
- package/src/actions.js +1377 -0
- package/src/browser_integration/browser_integration.js +191 -0
- package/src/browser_integration/document_back_and_forward.js +17 -0
- package/src/browser_integration/document_loading_signal.js +100 -0
- package/src/browser_integration/document_state_signal.js +9 -0
- package/src/browser_integration/document_url_signal.js +9 -0
- package/src/browser_integration/use_is_visited.js +19 -0
- package/src/browser_integration/via_history.js +199 -0
- package/src/browser_integration/via_navigation.js +168 -0
- package/src/components/action_execution/form_context.js +8 -0
- package/src/components/action_execution/render_actionable_component.jsx +27 -0
- package/src/components/action_execution/use_action.js +330 -0
- package/src/components/action_execution/use_execute_action.js +161 -0
- package/src/components/action_renderer.jsx +136 -0
- package/src/components/collect_form_element_values.js +79 -0
- package/src/components/demos/0_button_demo.html +155 -0
- package/src/components/demos/1_checkbox_demo.html +257 -0
- package/src/components/demos/2_input_textual_demo.html +354 -0
- package/src/components/demos/3_radio_demo.html +222 -0
- package/src/components/demos/4_select_demo.html +104 -0
- package/src/components/demos/5_list_scrollable_demo.html +153 -0
- package/src/components/demos/action/0_button_demo.html +204 -0
- package/src/components/demos/action/10_shortcuts_demo.html +189 -0
- package/src/components/demos/action/11_nested_shortcuts_demo.html +401 -0
- package/src/components/demos/action/1_input_text_demo.html +461 -0
- package/src/components/demos/action/2_form_multiple.html +303 -0
- package/src/components/demos/action/3_details_demo.html +172 -0
- package/src/components/demos/action/4_input_checkbox_demo.html +611 -0
- package/src/components/demos/action/6_checkbox_list_demo.html +109 -0
- package/src/components/demos/action/7_radio_list_demo.html +217 -0
- package/src/components/demos/action/8_editable_text_demo.html +442 -0
- package/src/components/demos/action/9_link_demo.html +172 -0
- package/src/components/demos/demo.md +0 -0
- package/src/components/demos/route/basic/basic.html +14 -0
- package/src/components/demos/route/basic/basic_route_demo.jsx +224 -0
- package/src/components/demos/route/multi/multi.html +14 -0
- package/src/components/demos/route/multi/multi_route_demo.jsx +277 -0
- package/src/components/details/details.jsx +248 -0
- package/src/components/details/summary_marker.jsx +141 -0
- package/src/components/editable_text/editable_text.jsx +96 -0
- package/src/components/error_boundary_context.js +9 -0
- package/src/components/form.jsx +144 -0
- package/src/components/input/button.jsx +333 -0
- package/src/components/input/checkbox_list.jsx +294 -0
- package/src/components/input/field.jsx +61 -0
- package/src/components/input/field_css.js +118 -0
- package/src/components/input/input.jsx +15 -0
- package/src/components/input/input_checkbox.jsx +370 -0
- package/src/components/input/input_radio.jsx +299 -0
- package/src/components/input/input_textual.jsx +338 -0
- package/src/components/input/radio_list.jsx +283 -0
- package/src/components/input/select.jsx +273 -0
- package/src/components/input/use_form_event.js +20 -0
- package/src/components/input/use_on_change.js +12 -0
- package/src/components/link/link.jsx +291 -0
- package/src/components/loader/loader_background.jsx +324 -0
- package/src/components/loader/loading_spinner.jsx +68 -0
- package/src/components/loader/network_speed.js +83 -0
- package/src/components/loader/rectangle_loading.jsx +225 -0
- package/src/components/route.jsx +15 -0
- package/src/components/selection/selection.js +5 -0
- package/src/components/selection/selection_context.jsx +262 -0
- package/src/components/shortcut/os.js +9 -0
- package/src/components/shortcut/shortcut_context.jsx +390 -0
- package/src/components/use_action_events.js +37 -0
- package/src/components/use_auto_focus.js +43 -0
- package/src/components/use_debounce_true.js +31 -0
- package/src/components/use_focus_group.js +19 -0
- package/src/components/use_initial_value.js +104 -0
- package/src/components/use_is_visited.js +19 -0
- package/src/components/use_ref_array.js +38 -0
- package/src/components/use_signal_sync.js +50 -0
- package/src/components/use_state_array.js +40 -0
- package/src/docs/actions.md +228 -0
- package/src/docs/demos/resource/action_status.jsx +42 -0
- package/src/docs/demos/resource/demo.md +1 -0
- package/src/docs/demos/resource/resource_demo_0.html +84 -0
- package/src/docs/demos/resource/resource_demo_10_post_gc.html +364 -0
- package/src/docs/demos/resource/resource_demo_11_describe_many.html +362 -0
- package/src/docs/demos/resource/resource_demo_2.html +173 -0
- package/src/docs/demos/resource/resource_demo_3_filtered_users.html +415 -0
- package/src/docs/demos/resource/resource_demo_4_details.html +284 -0
- package/src/docs/demos/resource/resource_demo_5_renderer_lazy.html +115 -0
- package/src/docs/demos/resource/resource_demo_6_gc.html +217 -0
- package/src/docs/demos/resource/resource_demo_7_child_gc.html +240 -0
- package/src/docs/demos/resource/resource_demo_8_proxy_gc.html +319 -0
- package/src/docs/demos/resource/resource_demo_9_describe_one.html +472 -0
- package/src/docs/demos/resource/tata.jsx +3 -0
- package/src/docs/demos/resource/toto.jsx +3 -0
- package/src/docs/demos/user_nav/user_nav.html +12 -0
- package/src/docs/demos/user_nav/user_nav.jsx +330 -0
- package/src/docs/resource_dependencies.md +103 -0
- package/src/docs/resource_with_params.md +80 -0
- package/src/notes.md +13 -0
- package/src/route/route.js +518 -0
- package/src/route/route.test.html +228 -0
- package/src/store/array_signal_store.js +537 -0
- package/src/store/local_storage_signal.js +17 -0
- package/src/store/resource_graph.js +1303 -0
- package/src/store/tests/resource_graph_autoreload_demo.html +12 -0
- package/src/store/tests/resource_graph_autoreload_demo.jsx +964 -0
- package/src/store/tests/resource_graph_dependencies.test.js +95 -0
- package/src/store/value_in_local_storage.js +187 -0
- package/src/symbol_object_signal.js +1 -0
- package/src/use_action_data.js +10 -0
- package/src/use_action_status.js +47 -0
- package/src/utils/add_many_event_listeners.js +15 -0
- package/src/utils/array_add_remove.js +61 -0
- package/src/utils/array_signal.js +15 -0
- package/src/utils/compare_two_js_values.js +172 -0
- package/src/utils/execute_with_cleanup.js +21 -0
- package/src/utils/get_caller_info.js +85 -0
- package/src/utils/iterable_weak_set.js +62 -0
- package/src/utils/js_value_weak_map.js +162 -0
- package/src/utils/js_value_weak_map_demo.html +690 -0
- package/src/utils/merge_two_js_values.js +53 -0
- package/src/utils/stringify_for_display.js +150 -0
- package/src/utils/weak_effect.js +48 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { useConstraints } from "@jsenv/validation";
|
|
2
|
+
import { forwardRef } from "preact/compat";
|
|
3
|
+
import { useImperativeHandle, useRef, useState } from "preact/hooks";
|
|
4
|
+
import { renderActionableComponent } from "../action_execution/render_actionable_component.jsx";
|
|
5
|
+
import { LoaderBackground } from "../loader/loader_background.jsx";
|
|
6
|
+
import { useAutoFocus } from "../use_auto_focus.js";
|
|
7
|
+
|
|
8
|
+
import.meta.css = /* css */ `
|
|
9
|
+
.custom_radio_wrapper {
|
|
10
|
+
display: inline-flex;
|
|
11
|
+
box-sizing: content-box;
|
|
12
|
+
|
|
13
|
+
--checked-color: #3b82f6;
|
|
14
|
+
--checked-disabled-color: var(--field-disabled-border-color);
|
|
15
|
+
|
|
16
|
+
--checkmark-color: var(--checked-color);
|
|
17
|
+
--checkmark-disabled-color: var(--field-disabled-text-color);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.custom_radio_wrapper input {
|
|
21
|
+
position: absolute;
|
|
22
|
+
opacity: 0;
|
|
23
|
+
inset: 0;
|
|
24
|
+
margin: 0;
|
|
25
|
+
padding: 0;
|
|
26
|
+
border: none;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.custom_radio {
|
|
30
|
+
width: 13px;
|
|
31
|
+
height: 13px;
|
|
32
|
+
background: transparent;
|
|
33
|
+
border-radius: 50%;
|
|
34
|
+
display: inline-flex;
|
|
35
|
+
align-items: center;
|
|
36
|
+
justify-content: center;
|
|
37
|
+
margin-left: 5px;
|
|
38
|
+
margin-top: 3px;
|
|
39
|
+
margin-right: 3px;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.custom_radio svg {
|
|
43
|
+
width: 100%;
|
|
44
|
+
height: 100%;
|
|
45
|
+
transition: all 0.15s ease;
|
|
46
|
+
pointer-events: none;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.custom_radio svg .custom_radio_border {
|
|
50
|
+
transition: all 0.15s ease;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.custom_radio svg .custom_radio_dashed_border {
|
|
54
|
+
display: none;
|
|
55
|
+
transition: all 0.15s ease;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.custom_radio svg .custom_radio_marker {
|
|
59
|
+
fill: var(--checkmark-color);
|
|
60
|
+
opacity: 0;
|
|
61
|
+
transform-origin: center;
|
|
62
|
+
transform: scale(0.3);
|
|
63
|
+
transition: all 0.15s ease;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* États hover */
|
|
67
|
+
.custom_radio_wrapper:hover .custom_radio svg .custom_radio_border {
|
|
68
|
+
stroke: var(--field-hover-border-color);
|
|
69
|
+
}
|
|
70
|
+
.custom_radio_wrapper:hover .custom_radio svg .custom_radio_marker {
|
|
71
|
+
fill: var(--field-strong-color);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.custom_radio_wrapper:hover
|
|
75
|
+
input:checked
|
|
76
|
+
+ .custom_radio
|
|
77
|
+
svg
|
|
78
|
+
.custom_radio_border {
|
|
79
|
+
stroke: var(--field-strong-color);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* État checked */
|
|
83
|
+
.custom_radio_wrapper input:checked + .custom_radio svg .custom_radio_border {
|
|
84
|
+
stroke: var(--field-strong-color);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.custom_radio_wrapper input:checked + .custom_radio svg .custom_radio_marker {
|
|
88
|
+
opacity: 1;
|
|
89
|
+
transform: scale(1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* États disabled */
|
|
93
|
+
.custom_radio_wrapper
|
|
94
|
+
input[disabled]
|
|
95
|
+
+ .custom_radio
|
|
96
|
+
svg
|
|
97
|
+
.custom_radio_border {
|
|
98
|
+
fill: light-dark(rgba(239, 239, 239, 0.3), rgba(59, 59, 59, 0.3));
|
|
99
|
+
stroke: var(--field-disabled-border-color);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.custom_radio_wrapper
|
|
103
|
+
input[disabled]:checked
|
|
104
|
+
+ .custom_radio
|
|
105
|
+
svg
|
|
106
|
+
.custom_radio_border {
|
|
107
|
+
stroke: var(--checked-disabled-color);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.custom_radio_wrapper
|
|
111
|
+
input[disabled]:checked
|
|
112
|
+
+ .custom_radio
|
|
113
|
+
svg
|
|
114
|
+
.custom_radio_marker {
|
|
115
|
+
fill: var(--checkmark-disabled-color);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.custom_radio_wrapper
|
|
119
|
+
input[data-readonly]
|
|
120
|
+
+ .custom_radio
|
|
121
|
+
svg
|
|
122
|
+
.custom_radio_border {
|
|
123
|
+
fill: light-dark(rgba(239, 239, 239, 0.3), rgba(59, 59, 59, 0.3));
|
|
124
|
+
stroke: var(--field-disabled-border-color);
|
|
125
|
+
}
|
|
126
|
+
.custom_radio_wrapper
|
|
127
|
+
input[data-readonly]
|
|
128
|
+
+ .custom_radio
|
|
129
|
+
svg
|
|
130
|
+
.custom_radio_dashed_border {
|
|
131
|
+
display: none;
|
|
132
|
+
}
|
|
133
|
+
.custom_radio_wrapper
|
|
134
|
+
input[data-readonly]:checked
|
|
135
|
+
+ .custom_radio
|
|
136
|
+
svg
|
|
137
|
+
.custom_radio_border {
|
|
138
|
+
stroke: var(--checked-disabled-color);
|
|
139
|
+
}
|
|
140
|
+
.custom_radio_wrapper
|
|
141
|
+
input[data-readonly]:checked
|
|
142
|
+
+ .custom_radio
|
|
143
|
+
svg
|
|
144
|
+
.custom_radio_marker {
|
|
145
|
+
fill: var(--checkmark-disabled-color);
|
|
146
|
+
}
|
|
147
|
+
.custom_radio_wrapper:hover
|
|
148
|
+
input[data-readonly]
|
|
149
|
+
+ .custom_radio
|
|
150
|
+
svg
|
|
151
|
+
.custom_radio_border {
|
|
152
|
+
fill: light-dark(rgba(239, 239, 239, 0.3), rgba(59, 59, 59, 0.3));
|
|
153
|
+
stroke: var(--field-disabled-border-color);
|
|
154
|
+
}
|
|
155
|
+
.custom_radio_wrapper:hover
|
|
156
|
+
input[data-readonly]:checked
|
|
157
|
+
+ .custom_radio
|
|
158
|
+
svg
|
|
159
|
+
.custom_radio_border {
|
|
160
|
+
stroke: var(--checked-disabled-color);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/* Focus state avec outline */
|
|
164
|
+
.custom_radio_wrapper input:focus-visible + .custom_radio {
|
|
165
|
+
outline: 2px solid var(--field-outline-color);
|
|
166
|
+
outline-offset: 1px;
|
|
167
|
+
border-radius: 50%;
|
|
168
|
+
}
|
|
169
|
+
`;
|
|
170
|
+
|
|
171
|
+
export const InputRadio = forwardRef((props, ref) => {
|
|
172
|
+
return renderActionableComponent(props, ref, {
|
|
173
|
+
Basic: InputRadioBasic,
|
|
174
|
+
WithAction: InputRadioWithAction,
|
|
175
|
+
InsideForm: InputRadioInsideForm,
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const InputRadioBasic = forwardRef((props, ref) => {
|
|
180
|
+
const {
|
|
181
|
+
autoFocus,
|
|
182
|
+
constraints = [],
|
|
183
|
+
checked,
|
|
184
|
+
readOnly,
|
|
185
|
+
disabled,
|
|
186
|
+
loading,
|
|
187
|
+
onClick,
|
|
188
|
+
onChange,
|
|
189
|
+
appeareance = "custom", // "custom" or "default"
|
|
190
|
+
...rest
|
|
191
|
+
} = props;
|
|
192
|
+
|
|
193
|
+
const innerRef = useRef(null);
|
|
194
|
+
useImperativeHandle(ref, () => innerRef.current);
|
|
195
|
+
useAutoFocus(innerRef, autoFocus);
|
|
196
|
+
useConstraints(innerRef, constraints);
|
|
197
|
+
|
|
198
|
+
const [innerChecked, setInnerChecked] = useState(checked);
|
|
199
|
+
const checkedRef = useRef(checked);
|
|
200
|
+
if (checkedRef.current !== checked) {
|
|
201
|
+
setInnerChecked(checked);
|
|
202
|
+
checkedRef.current = checked;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const handleChange = (e) => {
|
|
206
|
+
const isChecked = e.target.checked;
|
|
207
|
+
setInnerChecked(isChecked);
|
|
208
|
+
onChange?.(e);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const inputRadio = (
|
|
212
|
+
<input
|
|
213
|
+
ref={innerRef}
|
|
214
|
+
type="radio"
|
|
215
|
+
checked={innerChecked}
|
|
216
|
+
data-readonly={readOnly && !disabled ? "" : undefined}
|
|
217
|
+
disabled={disabled}
|
|
218
|
+
data-validation-message-arrow-x="center"
|
|
219
|
+
onClick={(e) => {
|
|
220
|
+
if (readOnly) {
|
|
221
|
+
e.preventDefault();
|
|
222
|
+
}
|
|
223
|
+
onClick?.(e);
|
|
224
|
+
}}
|
|
225
|
+
onChange={handleChange}
|
|
226
|
+
{...rest}
|
|
227
|
+
/>
|
|
228
|
+
);
|
|
229
|
+
const inputRadioDisplayed =
|
|
230
|
+
appeareance === "custom" ? (
|
|
231
|
+
<CustomRadio>{inputRadio}</CustomRadio>
|
|
232
|
+
) : (
|
|
233
|
+
inputRadio
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const inputRadioWithLoader = (
|
|
237
|
+
<LoaderBackground
|
|
238
|
+
loading={loading}
|
|
239
|
+
targetSelector={appeareance === "custom" ? ".custom_radio" : ""}
|
|
240
|
+
inset={-2}
|
|
241
|
+
color="light-dark(#355fcc, #3b82f6)"
|
|
242
|
+
>
|
|
243
|
+
{inputRadioDisplayed}
|
|
244
|
+
</LoaderBackground>
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
return inputRadioWithLoader;
|
|
248
|
+
});
|
|
249
|
+
const CustomRadio = ({ children }) => {
|
|
250
|
+
return (
|
|
251
|
+
<div className="custom_radio_wrapper" data-field-wrapper="">
|
|
252
|
+
{children}
|
|
253
|
+
<div className="custom_radio">
|
|
254
|
+
<svg
|
|
255
|
+
viewBox="0 0 12 12"
|
|
256
|
+
aria-hidden="true"
|
|
257
|
+
preserveAspectRatio="xMidYMid meet"
|
|
258
|
+
>
|
|
259
|
+
{/* Border circle - always visible */}
|
|
260
|
+
<circle
|
|
261
|
+
className="custom_radio_border"
|
|
262
|
+
cx="6"
|
|
263
|
+
cy="6"
|
|
264
|
+
r="5.5"
|
|
265
|
+
fill="white"
|
|
266
|
+
stroke="var(--field-border-color)"
|
|
267
|
+
strokeWidth="1"
|
|
268
|
+
/>
|
|
269
|
+
{/* Dashed border for readonly - calculated for even distribution */}
|
|
270
|
+
<circle
|
|
271
|
+
className="custom_radio_dashed_border"
|
|
272
|
+
cx="6"
|
|
273
|
+
cy="6"
|
|
274
|
+
r="5.5"
|
|
275
|
+
fill="var(--field-readonly-background-color)"
|
|
276
|
+
stroke="var(--field-border-color)"
|
|
277
|
+
strokeWidth="1"
|
|
278
|
+
strokeDasharray="2.16 2.16"
|
|
279
|
+
strokeDashoffset="0"
|
|
280
|
+
/>
|
|
281
|
+
{/* Inner fill circle - only visible when checked */}
|
|
282
|
+
<circle className="custom_radio_marker" cx="6" cy="6" r="3.5" />
|
|
283
|
+
</svg>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
);
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const InputRadioWithAction = () => {
|
|
290
|
+
throw new Error(
|
|
291
|
+
`Do not use <Input type="radio" />, use <RadioList /> instead`,
|
|
292
|
+
);
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const InputRadioInsideForm = () => {
|
|
296
|
+
throw new Error(
|
|
297
|
+
`Do not use <Input type="radio" />, use <RadioList /> instead`,
|
|
298
|
+
);
|
|
299
|
+
};
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input component for all textual input types.
|
|
3
|
+
*
|
|
4
|
+
* Supports:
|
|
5
|
+
* - text (default)
|
|
6
|
+
* - password
|
|
7
|
+
* - hidden
|
|
8
|
+
* - email
|
|
9
|
+
* - url
|
|
10
|
+
* - search
|
|
11
|
+
* - tel
|
|
12
|
+
* - etc.
|
|
13
|
+
*
|
|
14
|
+
* For non-textual inputs, specialized components will be used:
|
|
15
|
+
* - <InputCheckbox /> for type="checkbox"
|
|
16
|
+
* - <InputRadio /> for type="radio"
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { requestAction, useConstraints } from "@jsenv/validation";
|
|
20
|
+
import { forwardRef } from "preact/compat";
|
|
21
|
+
import { useEffect, useImperativeHandle, useRef } from "preact/hooks";
|
|
22
|
+
import { useNavState } from "../../browser_integration/browser_integration.js";
|
|
23
|
+
import { useActionStatus } from "../../use_action_status.js";
|
|
24
|
+
import { renderActionableComponent } from "../action_execution/render_actionable_component.jsx";
|
|
25
|
+
import {
|
|
26
|
+
useActionBoundToOneParam,
|
|
27
|
+
useOneFormParam,
|
|
28
|
+
} from "../action_execution/use_action.js";
|
|
29
|
+
import { useExecuteAction } from "../action_execution/use_execute_action.js";
|
|
30
|
+
import { LoaderBackground } from "../loader/loader_background.jsx";
|
|
31
|
+
import { useActionEvents } from "../use_action_events.js";
|
|
32
|
+
import { useAutoFocus } from "../use_auto_focus.js";
|
|
33
|
+
import "./field_css.js";
|
|
34
|
+
import { useOnChange } from "./use_on_change.js";
|
|
35
|
+
|
|
36
|
+
export const InputTextual = forwardRef((props, ref) => {
|
|
37
|
+
return renderActionableComponent(props, ref, {
|
|
38
|
+
Basic: InputTextualBasic,
|
|
39
|
+
WithAction: InputTextualWithAction,
|
|
40
|
+
InsideForm: InputTextualInsideForm,
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const InputTextualBasic = forwardRef((props, ref) => {
|
|
45
|
+
let {
|
|
46
|
+
type,
|
|
47
|
+
value,
|
|
48
|
+
autoFocus,
|
|
49
|
+
autoFocusVisible,
|
|
50
|
+
autoSelect,
|
|
51
|
+
constraints = [],
|
|
52
|
+
loading,
|
|
53
|
+
appearance = "custom",
|
|
54
|
+
...rest
|
|
55
|
+
} = props;
|
|
56
|
+
|
|
57
|
+
const innerRef = useRef();
|
|
58
|
+
useImperativeHandle(ref, () => innerRef.current);
|
|
59
|
+
useAutoFocus(innerRef, autoFocus, {
|
|
60
|
+
autoFocusVisible,
|
|
61
|
+
autoSelect,
|
|
62
|
+
});
|
|
63
|
+
useConstraints(innerRef, constraints);
|
|
64
|
+
|
|
65
|
+
if (type === "datetime-local") {
|
|
66
|
+
value = convertToLocalTimezone(value);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const inputTextual = (
|
|
70
|
+
<input
|
|
71
|
+
ref={innerRef}
|
|
72
|
+
type={type}
|
|
73
|
+
value={value}
|
|
74
|
+
data-field=""
|
|
75
|
+
data-field-with-border=""
|
|
76
|
+
data-custom={appearance === "custom" ? "" : undefined}
|
|
77
|
+
{...rest}
|
|
78
|
+
/>
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<LoaderBackground loading={loading} color="light-dark(#355fcc, #3b82f6)">
|
|
83
|
+
{inputTextual}
|
|
84
|
+
</LoaderBackground>
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// As explained in https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/datetime-local#setting_timezones
|
|
89
|
+
// datetime-local does not support timezones
|
|
90
|
+
const convertToLocalTimezone = (dateTimeString) => {
|
|
91
|
+
const date = new Date(dateTimeString);
|
|
92
|
+
// Check if the date is valid
|
|
93
|
+
if (isNaN(date.getTime())) {
|
|
94
|
+
return dateTimeString;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Format to YYYY-MM-DDThh:mm:ss
|
|
98
|
+
const year = date.getFullYear();
|
|
99
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
100
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
101
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
102
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
103
|
+
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
104
|
+
|
|
105
|
+
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Converts a datetime string without timezone (local time) to UTC format with 'Z' notation
|
|
110
|
+
*
|
|
111
|
+
* @param {string} localDateTimeString - Local datetime string without timezone (e.g., "2023-07-15T14:30:00")
|
|
112
|
+
* @returns {string} Datetime string in UTC with 'Z' notation (e.g., "2023-07-15T12:30:00Z")
|
|
113
|
+
*/
|
|
114
|
+
const convertToUTCTimezone = (localDateTimeString) => {
|
|
115
|
+
if (!localDateTimeString) {
|
|
116
|
+
return localDateTimeString;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
// Create a Date object using the local time string
|
|
121
|
+
// The browser will interpret this as local timezone
|
|
122
|
+
const localDate = new Date(localDateTimeString);
|
|
123
|
+
|
|
124
|
+
// Check if the date is valid
|
|
125
|
+
if (isNaN(localDate.getTime())) {
|
|
126
|
+
return localDateTimeString;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Convert to UTC ISO string
|
|
130
|
+
const utcString = localDate.toISOString();
|
|
131
|
+
|
|
132
|
+
// Return the UTC string (which includes the 'Z' notation)
|
|
133
|
+
return utcString;
|
|
134
|
+
} catch (error) {
|
|
135
|
+
console.error("Error converting local datetime to UTC:", error);
|
|
136
|
+
return localDateTimeString;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const InputTextualWithAction = forwardRef((props, ref) => {
|
|
141
|
+
const {
|
|
142
|
+
id,
|
|
143
|
+
type,
|
|
144
|
+
action,
|
|
145
|
+
name,
|
|
146
|
+
value: externalValue,
|
|
147
|
+
valueSignal,
|
|
148
|
+
cancelOnBlurInvalid,
|
|
149
|
+
cancelOnEscape,
|
|
150
|
+
actionErrorEffect,
|
|
151
|
+
readOnly,
|
|
152
|
+
loading,
|
|
153
|
+
onInput,
|
|
154
|
+
onCancel,
|
|
155
|
+
onActionPrevented,
|
|
156
|
+
onActionStart,
|
|
157
|
+
onActionError,
|
|
158
|
+
onActionEnd,
|
|
159
|
+
...rest
|
|
160
|
+
} = props;
|
|
161
|
+
if (import.meta.dev && !name && !valueSignal) {
|
|
162
|
+
console.warn(`InputTextual with action requires a name prop to be set.`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const innerRef = useRef(null);
|
|
166
|
+
useImperativeHandle(ref, () => innerRef.current);
|
|
167
|
+
|
|
168
|
+
const [navState, setNavState] = useNavState(id);
|
|
169
|
+
const [boundAction, value, setValue, resetValue] = useActionBoundToOneParam(
|
|
170
|
+
action,
|
|
171
|
+
name,
|
|
172
|
+
valueSignal ? valueSignal : externalValue,
|
|
173
|
+
navState,
|
|
174
|
+
"",
|
|
175
|
+
);
|
|
176
|
+
const { loading: actionLoading } = useActionStatus(boundAction);
|
|
177
|
+
const executeAction = useExecuteAction(innerRef, {
|
|
178
|
+
errorEffect: actionErrorEffect,
|
|
179
|
+
});
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
setNavState(value);
|
|
182
|
+
}, [value]);
|
|
183
|
+
|
|
184
|
+
const valueAtInteractionRef = useRef(null);
|
|
185
|
+
useOnChange(innerRef, (e) => {
|
|
186
|
+
if (
|
|
187
|
+
valueAtInteractionRef.current !== null &&
|
|
188
|
+
e.target.value === valueAtInteractionRef.current
|
|
189
|
+
) {
|
|
190
|
+
valueAtInteractionRef.current = null;
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
requestAction(boundAction, { event: e });
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
useActionEvents(innerRef, {
|
|
197
|
+
onCancel: (e, reason) => {
|
|
198
|
+
if (reason.startsWith("blur_invalid")) {
|
|
199
|
+
if (!cancelOnBlurInvalid) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (
|
|
203
|
+
// error prevent cancellation until the user closes it (or something closes it)
|
|
204
|
+
e.detail.failedConstraintInfo.level === "error" &&
|
|
205
|
+
e.detail.failedConstraintInfo.reportStatus !== "closed"
|
|
206
|
+
) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (reason === "escape_key") {
|
|
211
|
+
if (!cancelOnEscape) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Browser trigger a "change" event right after the escape is pressed
|
|
216
|
+
* if the input value has changed.
|
|
217
|
+
* We need to prevent the next change event otherwise we would request action when
|
|
218
|
+
* we actually want to cancel
|
|
219
|
+
*/
|
|
220
|
+
valueAtInteractionRef.current = e.target.value;
|
|
221
|
+
}
|
|
222
|
+
resetValue();
|
|
223
|
+
onCancel?.(e, reason);
|
|
224
|
+
},
|
|
225
|
+
onPrevented: onActionPrevented,
|
|
226
|
+
onAction: executeAction,
|
|
227
|
+
onStart: onActionStart,
|
|
228
|
+
onError: onActionError,
|
|
229
|
+
onEnd: (e) => {
|
|
230
|
+
setNavState(undefined);
|
|
231
|
+
onActionEnd?.(e);
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const innerLoading = loading || actionLoading;
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<InputTextualBasic
|
|
239
|
+
{...rest}
|
|
240
|
+
data-action={boundAction}
|
|
241
|
+
ref={innerRef}
|
|
242
|
+
type={type}
|
|
243
|
+
id={id}
|
|
244
|
+
name={name}
|
|
245
|
+
value={value}
|
|
246
|
+
data-form-value={
|
|
247
|
+
type === "datetime-local" ? convertToUTCTimezone(value) : undefined
|
|
248
|
+
}
|
|
249
|
+
loading={innerLoading}
|
|
250
|
+
readOnly={readOnly || innerLoading}
|
|
251
|
+
onInput={(e) => {
|
|
252
|
+
valueAtInteractionRef.current = null;
|
|
253
|
+
const inputValue =
|
|
254
|
+
type === "number" ? e.target.valueAsNumber : e.target.value;
|
|
255
|
+
setValue(
|
|
256
|
+
type === "datetime-local"
|
|
257
|
+
? convertToUTCTimezone(inputValue)
|
|
258
|
+
: inputValue,
|
|
259
|
+
);
|
|
260
|
+
onInput?.(e);
|
|
261
|
+
}}
|
|
262
|
+
onKeyDown={(e) => {
|
|
263
|
+
if (e.key !== "Enter") {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
e.preventDefault();
|
|
267
|
+
/**
|
|
268
|
+
* Browser trigger a "change" event right after the enter is pressed
|
|
269
|
+
* if the input value has changed.
|
|
270
|
+
* We need to prevent the next change event otherwise we would request action twice
|
|
271
|
+
*/
|
|
272
|
+
valueAtInteractionRef.current = e.target.value;
|
|
273
|
+
requestAction(boundAction, { event: e });
|
|
274
|
+
}}
|
|
275
|
+
/>
|
|
276
|
+
);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const InputTextualInsideForm = forwardRef((props, ref) => {
|
|
280
|
+
const {
|
|
281
|
+
formContext,
|
|
282
|
+
id,
|
|
283
|
+
name,
|
|
284
|
+
value: externalValue,
|
|
285
|
+
loading,
|
|
286
|
+
readOnly,
|
|
287
|
+
onInput,
|
|
288
|
+
onKeyDown,
|
|
289
|
+
...rest
|
|
290
|
+
} = props;
|
|
291
|
+
|
|
292
|
+
const innerRef = useRef(null);
|
|
293
|
+
useImperativeHandle(ref, () => innerRef.current);
|
|
294
|
+
|
|
295
|
+
const [navState, setNavState] = useNavState(id);
|
|
296
|
+
const { formAction, formIsBusy, formIsReadOnly, formActionRequester } =
|
|
297
|
+
formContext;
|
|
298
|
+
const [value, setValue] = useOneFormParam(name, externalValue, navState, "");
|
|
299
|
+
useEffect(() => {
|
|
300
|
+
setNavState(value);
|
|
301
|
+
}, [value]);
|
|
302
|
+
|
|
303
|
+
return (
|
|
304
|
+
<InputTextualBasic
|
|
305
|
+
{...rest}
|
|
306
|
+
ref={innerRef}
|
|
307
|
+
id={id}
|
|
308
|
+
name={name}
|
|
309
|
+
value={value}
|
|
310
|
+
data-form-value={convertToUTCTimezone(value)}
|
|
311
|
+
loading={
|
|
312
|
+
loading || (formIsBusy && formActionRequester === innerRef.current)
|
|
313
|
+
}
|
|
314
|
+
readOnly={readOnly || formIsReadOnly}
|
|
315
|
+
onInput={(e) => {
|
|
316
|
+
const inputValue = e.target.value;
|
|
317
|
+
setValue(convertToUTCTimezone(inputValue));
|
|
318
|
+
onInput?.(e);
|
|
319
|
+
}}
|
|
320
|
+
onKeyDown={(e) => {
|
|
321
|
+
if (e.key === "Enter") {
|
|
322
|
+
const inputElement = e.target;
|
|
323
|
+
const { form } = inputElement;
|
|
324
|
+
const formSubmitButton = form.querySelector(
|
|
325
|
+
"button[type='submit'], input[type='submit'], input[type='image']",
|
|
326
|
+
);
|
|
327
|
+
e.preventDefault();
|
|
328
|
+
requestAction(formAction, {
|
|
329
|
+
event: e,
|
|
330
|
+
target: form,
|
|
331
|
+
requester: formSubmitButton ? formSubmitButton : inputElement,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
onKeyDown?.(e);
|
|
335
|
+
}}
|
|
336
|
+
/>
|
|
337
|
+
);
|
|
338
|
+
});
|