@jsenv/navi 0.0.1 → 0.1.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 +22959 -0
- package/index.js +66 -16
- package/package.json +23 -11
- package/src/actions.js +50 -26
- package/src/browser_integration/browser_integration.js +31 -6
- package/src/browser_integration/via_history.js +42 -9
- package/src/components/action_execution/render_actionable_component.jsx +6 -4
- package/src/components/action_execution/use_action.js +51 -282
- package/src/components/action_execution/use_execute_action.js +106 -92
- package/src/components/action_execution/use_run_on_mount.js +9 -0
- package/src/components/action_renderer.jsx +21 -32
- package/src/components/demos/0_button_demo.html +574 -103
- package/src/components/demos/10_column_reordering_debug.html +277 -0
- package/src/components/demos/11_table_selection_debug.html +432 -0
- package/src/components/demos/1_checkbox_demo.html +579 -202
- package/src/components/demos/2_input_textual_demo.html +81 -138
- package/src/components/demos/3_radio_demo.html +0 -2
- package/src/components/demos/4_select_demo.html +19 -23
- package/src/components/demos/6_tablist_demo.html +77 -0
- package/src/components/demos/7_table_selection_demo.html +176 -0
- package/src/components/demos/8_table_fixed_headers_demo.html +584 -0
- package/src/components/demos/9_table_column_drag_demo.html +325 -0
- package/src/components/demos/action/0_button_demo.html +2 -4
- package/src/components/demos/action/1_input_text_demo.html +643 -222
- package/src/components/demos/action/3_details_demo.html +146 -115
- package/src/components/demos/action/4_input_checkbox_demo.html +442 -322
- package/src/components/demos/action/5_input_checkbox_state_demo.html +270 -0
- package/src/components/demos/action/6_checkbox_list_demo.html +304 -72
- package/src/components/demos/action/7_radio_list_demo.html +310 -170
- package/src/components/demos/action/{8_editable_text_demo.html → 8_editable_demo.html} +65 -76
- package/src/components/demos/action/9_link_demo.html +84 -62
- package/src/components/demos/ui_transition/0_action_renderer_ui_transition_demo.html +695 -0
- package/src/components/demos/ui_transition/1_nested_ui_transition_demo.html +429 -0
- package/src/components/demos/ui_transition/2_height_transition_test.html +295 -0
- package/src/components/details/details.jsx +62 -64
- package/src/components/edition/editable.jsx +186 -0
- package/src/components/field/README.md +247 -0
- package/src/components/{input → field}/button.jsx +151 -130
- package/src/components/field/checkbox_list.jsx +184 -0
- package/src/components/{collect_form_element_values.js → field/collect_form_element_values.js} +7 -4
- package/src/components/{input → field}/field_css.js +4 -1
- package/src/components/field/form.jsx +211 -0
- package/src/components/{input → field}/input.jsx +1 -0
- package/src/components/{input → field}/input_checkbox.jsx +132 -155
- package/src/components/{input → field}/input_radio.jsx +135 -46
- package/src/components/field/input_textual.jsx +418 -0
- package/src/components/field/label.jsx +32 -0
- package/src/components/field/radio_list.jsx +182 -0
- package/src/components/{input → field}/select.jsx +17 -32
- package/src/components/field/use_action_events.js +132 -0
- package/src/components/field/use_form_events.js +55 -0
- package/src/components/field/use_ui_state_controller.js +506 -0
- package/src/components/item_tracker/README.md +461 -0
- package/src/components/item_tracker/use_isolated_item_tracker.jsx +209 -0
- package/src/components/item_tracker/use_isolated_item_tracker_demo.html +148 -0
- package/src/components/item_tracker/use_isolated_item_tracker_demo.jsx +460 -0
- package/src/components/item_tracker/use_item_tracker.jsx +143 -0
- package/src/components/item_tracker/use_item_tracker_demo.html +207 -0
- package/src/components/item_tracker/use_item_tracker_demo.jsx +216 -0
- package/src/components/keyboard_shortcuts/active_keyboard_shortcuts.jsx +87 -0
- package/src/components/keyboard_shortcuts/aria_key_shortcuts.js +61 -0
- package/src/components/keyboard_shortcuts/keyboard_key_meta.js +17 -0
- package/src/components/keyboard_shortcuts/keyboard_shortcuts.js +371 -0
- package/src/components/link/link.jsx +65 -102
- package/src/components/link/link_with_icon.jsx +52 -0
- package/src/components/loader/loader_background.jsx +85 -64
- package/src/components/loader/rectangle_loading.jsx +38 -19
- package/src/components/route.jsx +8 -4
- package/src/components/selection/selection.jsx +1583 -0
- package/src/components/svg/font_sized_svg.jsx +45 -0
- package/src/components/svg/icon_and_text.jsx +21 -0
- package/src/components/svg/svg_mask_overlay.jsx +105 -0
- package/src/components/table/drag/table_drag.jsx +506 -0
- package/src/components/table/resize/table_resize.jsx +650 -0
- package/src/components/table/resize/table_size.js +43 -0
- package/src/components/table/selection/table_selection.js +106 -0
- package/src/components/table/selection/table_selection.jsx +203 -0
- package/src/components/table/sticky/sticky_group.js +354 -0
- package/src/components/table/sticky/table_sticky.js +25 -0
- package/src/components/table/sticky/table_sticky.jsx +501 -0
- package/src/components/table/table.jsx +721 -0
- package/src/components/table/table_css.js +211 -0
- package/src/components/table/table_ui.jsx +49 -0
- package/src/components/table/use_cells_and_columns.js +90 -0
- package/src/components/table/use_object_array_to_cells.js +46 -0
- package/src/components/table/z_indexes.js +23 -0
- package/src/components/tablist/tablist.jsx +99 -0
- package/src/components/text/overflow.jsx +15 -0
- package/src/components/text/text_and_count.jsx +28 -0
- package/src/components/ui_transition.jsx +128 -0
- package/src/components/use_auto_focus.js +58 -7
- package/src/components/use_batch_during_render.js +33 -0
- package/src/components/use_debounce_true.js +7 -7
- package/src/components/use_dependencies_diff.js +35 -0
- package/src/components/use_focus_group.js +4 -3
- package/src/components/use_initial_value.js +8 -34
- package/src/components/use_signal_sync.js +1 -1
- package/src/components/use_stable_callback.js +68 -0
- package/src/components/use_state_array.js +16 -9
- package/src/docs/actions.md +22 -0
- package/src/notes.md +33 -12
- package/src/route/route.js +97 -47
- package/src/store/resource_graph.js +2 -1
- package/src/store/tests/{resource_graph_dependencies.test.js → resource_graph_dependencies.test_manual.js} +13 -13
- package/src/utils/is_signal.js +20 -0
- package/src/utils/stringify_for_display.js +4 -23
- package/src/validation/constraints/confirm_constraint.js +14 -0
- package/src/validation/constraints/create_unique_value_constraint.js +27 -0
- package/src/validation/constraints/native_constraints.js +313 -0
- package/src/validation/constraints/readonly_constraint.js +36 -0
- package/src/validation/constraints/single_space_constraint.js +13 -0
- package/src/validation/custom_constraint_validation.js +599 -0
- package/src/validation/custom_message.js +18 -0
- package/src/validation/demos/browser_style.png +0 -0
- package/src/validation/demos/form_validation_demo.html +142 -0
- package/src/validation/demos/form_validation_demo_preact.html +87 -0
- package/src/validation/demos/form_validation_native_popover_demo.html +168 -0
- package/src/validation/demos/form_validation_vs_native_demo.html +172 -0
- package/src/validation/demos/validation_message_demo.html +203 -0
- package/src/validation/hooks/use_constraints.js +23 -0
- package/src/validation/hooks/use_custom_validation_ref.js +73 -0
- package/src/validation/hooks/use_validation_message.js +19 -0
- package/src/validation/validation_message.js +741 -0
- package/src/components/editable_text/editable_text.jsx +0 -96
- package/src/components/form.jsx +0 -144
- package/src/components/input/checkbox_list.jsx +0 -294
- package/src/components/input/field.jsx +0 -61
- package/src/components/input/input_textual.jsx +0 -338
- package/src/components/input/radio_list.jsx +0 -283
- package/src/components/input/use_form_event.js +0 -20
- package/src/components/input/use_on_change.js +0 -12
- package/src/components/selection/selection.js +0 -5
- package/src/components/selection/selection_context.jsx +0 -262
- package/src/components/shortcut/shortcut_context.jsx +0 -390
- package/src/components/use_action_events.js +0 -37
- package/src/utils/iterable_weak_set.js +0 -62
- /package/src/components/demos/action/{11_nested_shortcuts_demo.html → 11_nested_shortcuts_demo.xhtml} +0 -0
- /package/src/components/{shortcut → keyboard_shortcuts}/os.js +0 -0
- /package/src/route/{route.test.html → route.xtest.html} +0 -0
|
@@ -0,0 +1,418 @@
|
|
|
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 { forwardRef } from "preact/compat";
|
|
20
|
+
import {
|
|
21
|
+
useContext,
|
|
22
|
+
useEffect,
|
|
23
|
+
useImperativeHandle,
|
|
24
|
+
useLayoutEffect,
|
|
25
|
+
useRef,
|
|
26
|
+
} from "preact/hooks";
|
|
27
|
+
|
|
28
|
+
import { useActionStatus } from "../../use_action_status.js";
|
|
29
|
+
import { requestAction } from "../../validation/custom_constraint_validation.js";
|
|
30
|
+
import { useConstraints } from "../../validation/hooks/use_constraints.js";
|
|
31
|
+
import { renderActionableComponent } from "../action_execution/render_actionable_component.jsx";
|
|
32
|
+
import { useActionBoundToOneParam } from "../action_execution/use_action.js";
|
|
33
|
+
import { useExecuteAction } from "../action_execution/use_execute_action.js";
|
|
34
|
+
import { LoadableInlineElement } from "../loader/loader_background.jsx";
|
|
35
|
+
import { useAutoFocus } from "../use_auto_focus.js";
|
|
36
|
+
import "./field_css.js";
|
|
37
|
+
import { ReportReadOnlyOnLabelContext } from "./label.jsx";
|
|
38
|
+
import { useActionEvents } from "./use_action_events.js";
|
|
39
|
+
import {
|
|
40
|
+
DisabledContext,
|
|
41
|
+
LoadingContext,
|
|
42
|
+
LoadingElementContext,
|
|
43
|
+
ReadOnlyContext,
|
|
44
|
+
UIStateContext,
|
|
45
|
+
UIStateControllerContext,
|
|
46
|
+
useUIState,
|
|
47
|
+
useUIStateController,
|
|
48
|
+
} from "./use_ui_state_controller.js";
|
|
49
|
+
|
|
50
|
+
export const InputTextual = forwardRef((props, ref) => {
|
|
51
|
+
const uiStateController = useUIStateController(props, "input");
|
|
52
|
+
const uiState = useUIState(uiStateController);
|
|
53
|
+
|
|
54
|
+
const input = renderActionableComponent(props, ref, {
|
|
55
|
+
Basic: InputTextualBasic,
|
|
56
|
+
WithAction: InputTextualWithAction,
|
|
57
|
+
InsideForm: InputTextualInsideForm,
|
|
58
|
+
});
|
|
59
|
+
return (
|
|
60
|
+
<UIStateControllerContext.Provider value={uiStateController}>
|
|
61
|
+
<UIStateContext.Provider value={uiState}>{input}</UIStateContext.Provider>
|
|
62
|
+
</UIStateControllerContext.Provider>
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const InputTextualBasic = forwardRef((props, ref) => {
|
|
67
|
+
const contextReadOnly = useContext(ReadOnlyContext);
|
|
68
|
+
const contextDisabled = useContext(DisabledContext);
|
|
69
|
+
const contextLoading = useContext(LoadingContext);
|
|
70
|
+
const contextLoadingElement = useContext(LoadingElementContext);
|
|
71
|
+
const reportReadOnlyOnLabel = useContext(ReportReadOnlyOnLabelContext);
|
|
72
|
+
const uiStateController = useContext(UIStateControllerContext);
|
|
73
|
+
const uiState = useContext(UIStateContext);
|
|
74
|
+
const {
|
|
75
|
+
type,
|
|
76
|
+
onInput,
|
|
77
|
+
|
|
78
|
+
readOnly,
|
|
79
|
+
disabled,
|
|
80
|
+
constraints = [],
|
|
81
|
+
loading,
|
|
82
|
+
|
|
83
|
+
autoFocus,
|
|
84
|
+
autoFocusVisible,
|
|
85
|
+
autoSelect,
|
|
86
|
+
appearance = "custom",
|
|
87
|
+
width,
|
|
88
|
+
height,
|
|
89
|
+
...rest
|
|
90
|
+
} = props;
|
|
91
|
+
const innerRef = useRef();
|
|
92
|
+
useImperativeHandle(ref, () => innerRef.current);
|
|
93
|
+
|
|
94
|
+
const innerValue =
|
|
95
|
+
type === "datetime-local" ? convertToLocalTimezone(uiState) : uiState;
|
|
96
|
+
const innerLoading =
|
|
97
|
+
loading || (contextLoading && contextLoadingElement === innerRef.current);
|
|
98
|
+
const innerReadOnly =
|
|
99
|
+
readOnly || contextReadOnly || innerLoading || uiStateController.readOnly;
|
|
100
|
+
const innerDisabled = disabled || contextDisabled;
|
|
101
|
+
// infom any <label> parent of our readOnly state
|
|
102
|
+
reportReadOnlyOnLabel?.(innerReadOnly);
|
|
103
|
+
useAutoFocus(innerRef, autoFocus, {
|
|
104
|
+
autoFocusVisible,
|
|
105
|
+
autoSelect,
|
|
106
|
+
});
|
|
107
|
+
useConstraints(innerRef, constraints);
|
|
108
|
+
|
|
109
|
+
const inputTextual = (
|
|
110
|
+
<input
|
|
111
|
+
{...rest}
|
|
112
|
+
ref={innerRef}
|
|
113
|
+
type={type}
|
|
114
|
+
data-value={uiState}
|
|
115
|
+
value={innerValue}
|
|
116
|
+
data-field=""
|
|
117
|
+
data-field-with-border=""
|
|
118
|
+
data-custom={appearance === "custom" ? "" : undefined}
|
|
119
|
+
readOnly={innerReadOnly}
|
|
120
|
+
disabled={innerDisabled}
|
|
121
|
+
onInput={(e) => {
|
|
122
|
+
let inputValue;
|
|
123
|
+
if (type === "number") {
|
|
124
|
+
inputValue = e.target.valueAsNumber;
|
|
125
|
+
} else if (type === "datetime-local") {
|
|
126
|
+
inputValue = convertToUTCTimezone(e.target.value);
|
|
127
|
+
} else {
|
|
128
|
+
inputValue = e.target.value;
|
|
129
|
+
}
|
|
130
|
+
uiStateController.setUIState(inputValue, e);
|
|
131
|
+
onInput?.(e);
|
|
132
|
+
}}
|
|
133
|
+
// eslint-disable-next-line react/no-unknown-property
|
|
134
|
+
onresetuistate={(e) => {
|
|
135
|
+
uiStateController.resetUIState(e);
|
|
136
|
+
}}
|
|
137
|
+
// eslint-disable-next-line react/no-unknown-property
|
|
138
|
+
onsetuistate={(e) => {
|
|
139
|
+
uiStateController.setUIState(e.detail.value, e);
|
|
140
|
+
}}
|
|
141
|
+
/>
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<LoadableInlineElement
|
|
146
|
+
loading={innerLoading}
|
|
147
|
+
color="light-dark(#355fcc, #3b82f6)"
|
|
148
|
+
width={width}
|
|
149
|
+
height={height}
|
|
150
|
+
>
|
|
151
|
+
{inputTextual}
|
|
152
|
+
</LoadableInlineElement>
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const InputTextualWithAction = forwardRef((props, ref) => {
|
|
157
|
+
const uiState = useContext(UIStateContext);
|
|
158
|
+
const {
|
|
159
|
+
action,
|
|
160
|
+
loading,
|
|
161
|
+
onCancel,
|
|
162
|
+
onActionPrevented,
|
|
163
|
+
onActionStart,
|
|
164
|
+
onActionError,
|
|
165
|
+
onActionEnd,
|
|
166
|
+
cancelOnBlurInvalid,
|
|
167
|
+
cancelOnEscape,
|
|
168
|
+
actionErrorEffect,
|
|
169
|
+
onInput,
|
|
170
|
+
onKeyDown,
|
|
171
|
+
...rest
|
|
172
|
+
} = props;
|
|
173
|
+
const innerRef = useRef(null);
|
|
174
|
+
useImperativeHandle(ref, () => innerRef.current);
|
|
175
|
+
const [boundAction] = useActionBoundToOneParam(action, uiState);
|
|
176
|
+
const { loading: actionLoading } = useActionStatus(boundAction);
|
|
177
|
+
const executeAction = useExecuteAction(innerRef, {
|
|
178
|
+
errorEffect: actionErrorEffect,
|
|
179
|
+
});
|
|
180
|
+
const valueAtInteractionRef = useRef(null);
|
|
181
|
+
|
|
182
|
+
useOnInputChange(innerRef, (e) => {
|
|
183
|
+
if (
|
|
184
|
+
valueAtInteractionRef.current !== null &&
|
|
185
|
+
e.target.value === valueAtInteractionRef.current
|
|
186
|
+
) {
|
|
187
|
+
valueAtInteractionRef.current = null;
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
requestAction(e.target, boundAction, {
|
|
191
|
+
event: e,
|
|
192
|
+
actionOrigin: "action_prop",
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
// here updating the input won't call the associated action
|
|
196
|
+
// (user have to blur or press enter for this to happen)
|
|
197
|
+
// so we can keep the ui state on cancel/abort/error and let user decide
|
|
198
|
+
// to update ui state or retry via blur/enter as is
|
|
199
|
+
useActionEvents(innerRef, {
|
|
200
|
+
onCancel: (e, reason) => {
|
|
201
|
+
if (reason.startsWith("blur_invalid")) {
|
|
202
|
+
if (!cancelOnBlurInvalid) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (
|
|
206
|
+
// error prevent cancellation until the user closes it (or something closes it)
|
|
207
|
+
e.detail.failedConstraintInfo.level === "error" &&
|
|
208
|
+
e.detail.failedConstraintInfo.reportStatus !== "closed"
|
|
209
|
+
) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (reason === "escape_key") {
|
|
214
|
+
if (!cancelOnEscape) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Browser trigger a "change" event right after the escape is pressed
|
|
219
|
+
* if the input value has changed.
|
|
220
|
+
* We need to prevent the next change event otherwise we would request action when
|
|
221
|
+
* we actually want to cancel
|
|
222
|
+
*/
|
|
223
|
+
valueAtInteractionRef.current = e.target.value;
|
|
224
|
+
}
|
|
225
|
+
onCancel?.(e, reason);
|
|
226
|
+
},
|
|
227
|
+
onPrevented: onActionPrevented,
|
|
228
|
+
onAction: executeAction,
|
|
229
|
+
onStart: onActionStart,
|
|
230
|
+
onError: onActionError,
|
|
231
|
+
onEnd: onActionEnd,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
return (
|
|
235
|
+
<InputTextualBasic
|
|
236
|
+
data-action={boundAction.name}
|
|
237
|
+
{...rest}
|
|
238
|
+
ref={innerRef}
|
|
239
|
+
loading={loading || actionLoading}
|
|
240
|
+
onInput={(e) => {
|
|
241
|
+
valueAtInteractionRef.current = null;
|
|
242
|
+
onInput?.(e);
|
|
243
|
+
}}
|
|
244
|
+
onKeyDown={(e) => {
|
|
245
|
+
if (e.key !== "Enter") {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
e.preventDefault();
|
|
249
|
+
/**
|
|
250
|
+
* Browser trigger a "change" event right after the enter is pressed
|
|
251
|
+
* if the input value has changed.
|
|
252
|
+
* We need to prevent the next change event otherwise we would request action twice
|
|
253
|
+
*/
|
|
254
|
+
valueAtInteractionRef.current = e.target.value;
|
|
255
|
+
requestAction(e.target, boundAction, {
|
|
256
|
+
event: e,
|
|
257
|
+
actionOrigin: "action_prop",
|
|
258
|
+
});
|
|
259
|
+
onKeyDown?.(e);
|
|
260
|
+
}}
|
|
261
|
+
/>
|
|
262
|
+
);
|
|
263
|
+
});
|
|
264
|
+
const InputTextualInsideForm = forwardRef((props, ref) => {
|
|
265
|
+
const {
|
|
266
|
+
onKeyDown,
|
|
267
|
+
// We destructure formContext to avoid passing it to the underlying input element
|
|
268
|
+
// eslint-disable-next-line no-unused-vars
|
|
269
|
+
formContext,
|
|
270
|
+
...rest
|
|
271
|
+
} = props;
|
|
272
|
+
|
|
273
|
+
return (
|
|
274
|
+
<InputTextualBasic
|
|
275
|
+
{...rest}
|
|
276
|
+
ref={ref}
|
|
277
|
+
onKeyDown={(e) => {
|
|
278
|
+
if (e.key === "Enter") {
|
|
279
|
+
const inputElement = e.target;
|
|
280
|
+
const { form } = inputElement;
|
|
281
|
+
const formSubmitButton = form.querySelector(
|
|
282
|
+
"button[type='submit'], input[type='submit'], input[type='image']",
|
|
283
|
+
);
|
|
284
|
+
e.preventDefault();
|
|
285
|
+
form.dispatchEvent(
|
|
286
|
+
new CustomEvent("actionrequested", {
|
|
287
|
+
detail: {
|
|
288
|
+
requester: formSubmitButton ? formSubmitButton : inputElement,
|
|
289
|
+
event: e,
|
|
290
|
+
meta: { isSubmit: true },
|
|
291
|
+
actionOrigin: "action_prop",
|
|
292
|
+
},
|
|
293
|
+
}),
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
onKeyDown?.(e);
|
|
297
|
+
}}
|
|
298
|
+
/>
|
|
299
|
+
);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const useOnInputChange = (inputRef, callback) => {
|
|
303
|
+
// we must use a custom event listener because preact bind onChange to onInput for compat with react
|
|
304
|
+
useEffect(() => {
|
|
305
|
+
const input = inputRef.current;
|
|
306
|
+
input.addEventListener("change", callback);
|
|
307
|
+
return () => {
|
|
308
|
+
input.removeEventListener("change", callback);
|
|
309
|
+
};
|
|
310
|
+
}, [callback]);
|
|
311
|
+
|
|
312
|
+
// Handle programmatic value changes that don't trigger browser change events
|
|
313
|
+
//
|
|
314
|
+
// Problem: When input values are set programmatically (not by user typing),
|
|
315
|
+
// browsers don't fire the 'change' event. However, our application logic
|
|
316
|
+
// still needs to detect these changes.
|
|
317
|
+
//
|
|
318
|
+
// Example scenario:
|
|
319
|
+
// 1. User starts editing (letter key pressed, value set programmatically)
|
|
320
|
+
// 2. User doesn't type anything additional (this is the key part)
|
|
321
|
+
// 3. User clicks outside to finish editing
|
|
322
|
+
// 4. Without this code, no change event would fire despite the fact that the input value did change from its original state
|
|
323
|
+
//
|
|
324
|
+
// This distinction is crucial because:
|
|
325
|
+
//
|
|
326
|
+
// - If the user typed additional text after the initial programmatic value,
|
|
327
|
+
// the browser would fire change events normally
|
|
328
|
+
// - But when they don't type anything else, the browser considers it as "no user interaction"
|
|
329
|
+
// even though the programmatic initial value represents a meaningful change
|
|
330
|
+
const valueAtStartRef = useRef();
|
|
331
|
+
const interactedRef = useRef(false);
|
|
332
|
+
useLayoutEffect(() => {
|
|
333
|
+
const input = inputRef.current;
|
|
334
|
+
valueAtStartRef.current = input.value;
|
|
335
|
+
|
|
336
|
+
const onfocus = () => {
|
|
337
|
+
interactedRef.current = false;
|
|
338
|
+
valueAtStartRef.current = input.value;
|
|
339
|
+
};
|
|
340
|
+
const oninput = (e) => {
|
|
341
|
+
if (!e.isTrusted) {
|
|
342
|
+
// non trusted "input" events will be ignored by the browser when deciding to fire "change" event
|
|
343
|
+
// we ignore them too
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
interactedRef.current = true;
|
|
347
|
+
};
|
|
348
|
+
const onblur = (e) => {
|
|
349
|
+
if (interactedRef.current) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (valueAtStartRef.current === input.value) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
callback(e);
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
input.addEventListener("focus", onfocus);
|
|
359
|
+
input.addEventListener("input", oninput);
|
|
360
|
+
input.addEventListener("blur", onblur);
|
|
361
|
+
|
|
362
|
+
return () => {
|
|
363
|
+
input.removeEventListener("focus", onfocus);
|
|
364
|
+
input.removeEventListener("input", oninput);
|
|
365
|
+
input.removeEventListener("blur", onblur);
|
|
366
|
+
};
|
|
367
|
+
}, []);
|
|
368
|
+
};
|
|
369
|
+
// As explained in https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/datetime-local#setting_timezones
|
|
370
|
+
// datetime-local does not support timezones
|
|
371
|
+
const convertToLocalTimezone = (dateTimeString) => {
|
|
372
|
+
const date = new Date(dateTimeString);
|
|
373
|
+
// Check if the date is valid
|
|
374
|
+
if (isNaN(date.getTime())) {
|
|
375
|
+
return dateTimeString;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Format to YYYY-MM-DDThh:mm:ss
|
|
379
|
+
const year = date.getFullYear();
|
|
380
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
381
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
382
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
383
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
384
|
+
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
385
|
+
|
|
386
|
+
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
|
|
387
|
+
};
|
|
388
|
+
/**
|
|
389
|
+
* Converts a datetime string without timezone (local time) to UTC format with 'Z' notation
|
|
390
|
+
*
|
|
391
|
+
* @param {string} localDateTimeString - Local datetime string without timezone (e.g., "2023-07-15T14:30:00")
|
|
392
|
+
* @returns {string} Datetime string in UTC with 'Z' notation (e.g., "2023-07-15T12:30:00Z")
|
|
393
|
+
*/
|
|
394
|
+
const convertToUTCTimezone = (localDateTimeString) => {
|
|
395
|
+
if (!localDateTimeString) {
|
|
396
|
+
return localDateTimeString;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
try {
|
|
400
|
+
// Create a Date object using the local time string
|
|
401
|
+
// The browser will interpret this as local timezone
|
|
402
|
+
const localDate = new Date(localDateTimeString);
|
|
403
|
+
|
|
404
|
+
// Check if the date is valid
|
|
405
|
+
if (isNaN(localDate.getTime())) {
|
|
406
|
+
return localDateTimeString;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Convert to UTC ISO string
|
|
410
|
+
const utcString = localDate.toISOString();
|
|
411
|
+
|
|
412
|
+
// Return the UTC string (which includes the 'Z' notation)
|
|
413
|
+
return utcString;
|
|
414
|
+
} catch (error) {
|
|
415
|
+
console.error("Error converting local datetime to UTC:", error);
|
|
416
|
+
return localDateTimeString;
|
|
417
|
+
}
|
|
418
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createContext } from "preact";
|
|
2
|
+
import { forwardRef } from "preact/compat";
|
|
3
|
+
import { useImperativeHandle, useRef, useState } from "preact/hooks";
|
|
4
|
+
|
|
5
|
+
import.meta.css = /* css */ `
|
|
6
|
+
label[data-readonly] {
|
|
7
|
+
color: rgba(0, 0, 0, 0.5);
|
|
8
|
+
}
|
|
9
|
+
`;
|
|
10
|
+
|
|
11
|
+
export const ReportReadOnlyOnLabelContext = createContext();
|
|
12
|
+
|
|
13
|
+
export const Label = forwardRef((props, ref) => {
|
|
14
|
+
const { readOnly, children, ...rest } = props;
|
|
15
|
+
const innerRef = useRef();
|
|
16
|
+
useImperativeHandle(ref, () => innerRef.current);
|
|
17
|
+
|
|
18
|
+
const [inputReadOnly, setInputReadOnly] = useState(false);
|
|
19
|
+
const innerReadOnly = readOnly || inputReadOnly;
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<label
|
|
23
|
+
ref={innerRef}
|
|
24
|
+
data-readonly={innerReadOnly ? "" : undefined}
|
|
25
|
+
{...rest}
|
|
26
|
+
>
|
|
27
|
+
<ReportReadOnlyOnLabelContext.Provider value={setInputReadOnly}>
|
|
28
|
+
{children}
|
|
29
|
+
</ReportReadOnlyOnLabelContext.Provider>
|
|
30
|
+
</label>
|
|
31
|
+
);
|
|
32
|
+
});
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { forwardRef } from "preact/compat";
|
|
2
|
+
import {
|
|
3
|
+
useContext,
|
|
4
|
+
useImperativeHandle,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
} from "preact/hooks";
|
|
8
|
+
|
|
9
|
+
import { useActionStatus } from "../../use_action_status.js";
|
|
10
|
+
import { requestAction } from "../../validation/custom_constraint_validation.js";
|
|
11
|
+
import { renderActionableComponent } from "../action_execution/render_actionable_component.jsx";
|
|
12
|
+
import { useActionBoundToOneParam } from "../action_execution/use_action.js";
|
|
13
|
+
import { useExecuteAction } from "../action_execution/use_execute_action.js";
|
|
14
|
+
import { InputRadio } from "./input_radio.jsx";
|
|
15
|
+
import { useActionEvents } from "./use_action_events.js";
|
|
16
|
+
import {
|
|
17
|
+
DisabledContext,
|
|
18
|
+
FieldNameContext,
|
|
19
|
+
LoadingContext,
|
|
20
|
+
LoadingElementContext,
|
|
21
|
+
ParentUIStateControllerContext,
|
|
22
|
+
ReadOnlyContext,
|
|
23
|
+
RequiredContext,
|
|
24
|
+
UIStateContext,
|
|
25
|
+
UIStateControllerContext,
|
|
26
|
+
useUIGroupStateController,
|
|
27
|
+
useUIState,
|
|
28
|
+
} from "./use_ui_state_controller.js";
|
|
29
|
+
|
|
30
|
+
import.meta.css = /* css */ `
|
|
31
|
+
.navi_radio_list {
|
|
32
|
+
display: flex;
|
|
33
|
+
flex-direction: column;
|
|
34
|
+
}
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
export const RadioList = forwardRef((props, ref) => {
|
|
38
|
+
const uiStateController = useUIGroupStateController(props, "radio_list", {
|
|
39
|
+
childComponentType: "radio",
|
|
40
|
+
aggregateChildStates: (childUIStateControllers) => {
|
|
41
|
+
let activeValue;
|
|
42
|
+
for (const childUIStateController of childUIStateControllers) {
|
|
43
|
+
if (childUIStateController.uiState) {
|
|
44
|
+
activeValue = childUIStateController.uiState;
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return activeValue;
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
const uiState = useUIState(uiStateController);
|
|
52
|
+
const radioList = renderActionableComponent(props, ref, {
|
|
53
|
+
Basic: RadioListBasic,
|
|
54
|
+
WithAction: RadioListWithAction,
|
|
55
|
+
InsideForm: RadioListInsideForm,
|
|
56
|
+
});
|
|
57
|
+
return (
|
|
58
|
+
<UIStateControllerContext.Provider value={uiStateController}>
|
|
59
|
+
<UIStateContext.Provider value={uiState}>
|
|
60
|
+
{radioList}
|
|
61
|
+
</UIStateContext.Provider>
|
|
62
|
+
</UIStateControllerContext.Provider>
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
export const Radio = InputRadio;
|
|
66
|
+
|
|
67
|
+
const RadioListBasic = forwardRef((props, ref) => {
|
|
68
|
+
const contextReadOnly = useContext(ReadOnlyContext);
|
|
69
|
+
const contextDisabled = useContext(DisabledContext);
|
|
70
|
+
const contextLoading = useContext(LoadingContext);
|
|
71
|
+
const uiStateController = useContext(UIStateControllerContext);
|
|
72
|
+
const { name, loading, disabled, readOnly, children, required, ...rest } =
|
|
73
|
+
props;
|
|
74
|
+
const innerRef = useRef();
|
|
75
|
+
useImperativeHandle(ref, () => innerRef.current);
|
|
76
|
+
|
|
77
|
+
const innerLoading = loading || contextLoading;
|
|
78
|
+
const innerReadOnly =
|
|
79
|
+
readOnly || contextReadOnly || innerLoading || uiStateController.readOnly;
|
|
80
|
+
const innerDisabled = disabled || contextDisabled;
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div
|
|
84
|
+
data-action={rest["data-action"]}
|
|
85
|
+
{...rest}
|
|
86
|
+
ref={innerRef}
|
|
87
|
+
className="navi_radio_list"
|
|
88
|
+
data-radio-list
|
|
89
|
+
// eslint-disable-next-line react/no-unknown-property
|
|
90
|
+
onresetuistate={(e) => {
|
|
91
|
+
uiStateController.resetUIState(e);
|
|
92
|
+
}}
|
|
93
|
+
>
|
|
94
|
+
<ParentUIStateControllerContext.Provider value={uiStateController}>
|
|
95
|
+
<FieldNameContext.Provider value={name}>
|
|
96
|
+
<ReadOnlyContext.Provider value={innerReadOnly}>
|
|
97
|
+
<DisabledContext.Provider value={innerDisabled}>
|
|
98
|
+
<RequiredContext.Provider value={required}>
|
|
99
|
+
<LoadingContext.Provider value={innerLoading}>
|
|
100
|
+
{children}
|
|
101
|
+
</LoadingContext.Provider>
|
|
102
|
+
</RequiredContext.Provider>
|
|
103
|
+
</DisabledContext.Provider>
|
|
104
|
+
</ReadOnlyContext.Provider>
|
|
105
|
+
</FieldNameContext.Provider>
|
|
106
|
+
</ParentUIStateControllerContext.Provider>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
const RadioListWithAction = forwardRef((props, ref) => {
|
|
111
|
+
const uiStateController = useContext(UIStateControllerContext);
|
|
112
|
+
const uiState = useContext(UIStateContext);
|
|
113
|
+
const {
|
|
114
|
+
action,
|
|
115
|
+
|
|
116
|
+
onCancel,
|
|
117
|
+
onActionPrevented,
|
|
118
|
+
onActionStart,
|
|
119
|
+
onActionAbort,
|
|
120
|
+
onActionError,
|
|
121
|
+
onActionEnd,
|
|
122
|
+
actionErrorEffect,
|
|
123
|
+
loading,
|
|
124
|
+
children,
|
|
125
|
+
...rest
|
|
126
|
+
} = props;
|
|
127
|
+
const innerRef = useRef();
|
|
128
|
+
useImperativeHandle(ref, () => innerRef.current);
|
|
129
|
+
const [boundAction] = useActionBoundToOneParam(action, uiState);
|
|
130
|
+
const { loading: actionLoading } = useActionStatus(boundAction);
|
|
131
|
+
const executeAction = useExecuteAction(innerRef, {
|
|
132
|
+
errorEffect: actionErrorEffect,
|
|
133
|
+
});
|
|
134
|
+
const [actionRequester, setActionRequester] = useState(null);
|
|
135
|
+
|
|
136
|
+
useActionEvents(innerRef, {
|
|
137
|
+
onCancel: (e, reason) => {
|
|
138
|
+
uiStateController.resetUIState(e);
|
|
139
|
+
onCancel?.(e, reason);
|
|
140
|
+
},
|
|
141
|
+
onPrevented: onActionPrevented,
|
|
142
|
+
onAction: (actionEvent) => {
|
|
143
|
+
setActionRequester(actionEvent.detail.requester);
|
|
144
|
+
executeAction(actionEvent);
|
|
145
|
+
},
|
|
146
|
+
onStart: onActionStart,
|
|
147
|
+
onAbort: (e) => {
|
|
148
|
+
uiStateController.resetUIState(e);
|
|
149
|
+
onActionAbort?.(e);
|
|
150
|
+
},
|
|
151
|
+
onError: (e) => {
|
|
152
|
+
uiStateController.resetUIState(e);
|
|
153
|
+
onActionError?.(e);
|
|
154
|
+
},
|
|
155
|
+
onEnd: (e) => {
|
|
156
|
+
onActionEnd?.(e);
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<RadioListBasic
|
|
162
|
+
data-action={boundAction}
|
|
163
|
+
{...rest}
|
|
164
|
+
ref={innerRef}
|
|
165
|
+
onChange={(e) => {
|
|
166
|
+
const radio = e.target;
|
|
167
|
+
const radioListContainer = innerRef.current;
|
|
168
|
+
requestAction(radioListContainer, boundAction, {
|
|
169
|
+
event: e,
|
|
170
|
+
requester: radio,
|
|
171
|
+
actionOrigin: "action_prop",
|
|
172
|
+
});
|
|
173
|
+
}}
|
|
174
|
+
loading={loading || actionLoading}
|
|
175
|
+
>
|
|
176
|
+
<LoadingElementContext.Provider value={actionRequester}>
|
|
177
|
+
{children}
|
|
178
|
+
</LoadingElementContext.Provider>
|
|
179
|
+
</RadioListBasic>
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
const RadioListInsideForm = RadioListBasic;
|