@jsenv/navi 0.0.1 → 0.1.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 +22954 -0
- package/index.js +66 -16
- package/package.json +22 -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/{input → field}/input_textual.jsx +247 -173
- 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/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
|
@@ -16,173 +16,170 @@
|
|
|
16
16
|
* - <InputRadio /> for type="radio"
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { requestAction, useConstraints } from "@jsenv/validation";
|
|
20
19
|
import { forwardRef } from "preact/compat";
|
|
21
|
-
import {
|
|
22
|
-
|
|
20
|
+
import {
|
|
21
|
+
useContext,
|
|
22
|
+
useEffect,
|
|
23
|
+
useImperativeHandle,
|
|
24
|
+
useLayoutEffect,
|
|
25
|
+
useRef,
|
|
26
|
+
} from "preact/hooks";
|
|
27
|
+
|
|
23
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";
|
|
24
31
|
import { renderActionableComponent } from "../action_execution/render_actionable_component.jsx";
|
|
25
|
-
import {
|
|
26
|
-
useActionBoundToOneParam,
|
|
27
|
-
useOneFormParam,
|
|
28
|
-
} from "../action_execution/use_action.js";
|
|
32
|
+
import { useActionBoundToOneParam } from "../action_execution/use_action.js";
|
|
29
33
|
import { useExecuteAction } from "../action_execution/use_execute_action.js";
|
|
30
|
-
import {
|
|
31
|
-
import { useActionEvents } from "../use_action_events.js";
|
|
34
|
+
import { LoadableInlineElement } from "../loader/loader_background.jsx";
|
|
32
35
|
import { useAutoFocus } from "../use_auto_focus.js";
|
|
33
36
|
import "./field_css.js";
|
|
34
|
-
import {
|
|
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";
|
|
35
49
|
|
|
36
50
|
export const InputTextual = forwardRef((props, ref) => {
|
|
37
|
-
|
|
51
|
+
const uiStateController = useUIStateController(props, "input");
|
|
52
|
+
const uiState = useUIState(uiStateController);
|
|
53
|
+
|
|
54
|
+
const input = renderActionableComponent(props, ref, {
|
|
38
55
|
Basic: InputTextualBasic,
|
|
39
56
|
WithAction: InputTextualWithAction,
|
|
40
57
|
InsideForm: InputTextualInsideForm,
|
|
41
58
|
});
|
|
59
|
+
return (
|
|
60
|
+
<UIStateControllerContext.Provider value={uiStateController}>
|
|
61
|
+
<UIStateContext.Provider value={uiState}>{input}</UIStateContext.Provider>
|
|
62
|
+
</UIStateControllerContext.Provider>
|
|
63
|
+
);
|
|
42
64
|
});
|
|
43
65
|
|
|
44
66
|
const InputTextualBasic = forwardRef((props, ref) => {
|
|
45
|
-
|
|
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 {
|
|
46
75
|
type,
|
|
47
|
-
|
|
76
|
+
onInput,
|
|
77
|
+
|
|
78
|
+
readOnly,
|
|
79
|
+
disabled,
|
|
80
|
+
constraints = [],
|
|
81
|
+
loading,
|
|
82
|
+
|
|
48
83
|
autoFocus,
|
|
49
84
|
autoFocusVisible,
|
|
50
85
|
autoSelect,
|
|
51
|
-
constraints = [],
|
|
52
|
-
loading,
|
|
53
86
|
appearance = "custom",
|
|
87
|
+
width,
|
|
88
|
+
height,
|
|
54
89
|
...rest
|
|
55
90
|
} = props;
|
|
56
|
-
|
|
57
91
|
const innerRef = useRef();
|
|
58
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);
|
|
59
103
|
useAutoFocus(innerRef, autoFocus, {
|
|
60
104
|
autoFocusVisible,
|
|
61
105
|
autoSelect,
|
|
62
106
|
});
|
|
63
107
|
useConstraints(innerRef, constraints);
|
|
64
108
|
|
|
65
|
-
if (type === "datetime-local") {
|
|
66
|
-
value = convertToLocalTimezone(value);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
109
|
const inputTextual = (
|
|
70
110
|
<input
|
|
111
|
+
{...rest}
|
|
71
112
|
ref={innerRef}
|
|
72
113
|
type={type}
|
|
73
|
-
value={
|
|
114
|
+
data-value={uiState}
|
|
115
|
+
value={innerValue}
|
|
74
116
|
data-field=""
|
|
75
117
|
data-field-with-border=""
|
|
76
118
|
data-custom={appearance === "custom" ? "" : undefined}
|
|
77
|
-
{
|
|
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
|
+
}}
|
|
78
141
|
/>
|
|
79
142
|
);
|
|
80
143
|
|
|
81
144
|
return (
|
|
82
|
-
<
|
|
145
|
+
<LoadableInlineElement
|
|
146
|
+
loading={innerLoading}
|
|
147
|
+
color="light-dark(#355fcc, #3b82f6)"
|
|
148
|
+
width={width}
|
|
149
|
+
height={height}
|
|
150
|
+
>
|
|
83
151
|
{inputTextual}
|
|
84
|
-
</
|
|
152
|
+
</LoadableInlineElement>
|
|
85
153
|
);
|
|
86
154
|
});
|
|
87
155
|
|
|
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
156
|
const InputTextualWithAction = forwardRef((props, ref) => {
|
|
157
|
+
const uiState = useContext(UIStateContext);
|
|
141
158
|
const {
|
|
142
|
-
id,
|
|
143
|
-
type,
|
|
144
159
|
action,
|
|
145
|
-
name,
|
|
146
|
-
value: externalValue,
|
|
147
|
-
valueSignal,
|
|
148
|
-
cancelOnBlurInvalid,
|
|
149
|
-
cancelOnEscape,
|
|
150
|
-
actionErrorEffect,
|
|
151
|
-
readOnly,
|
|
152
160
|
loading,
|
|
153
|
-
onInput,
|
|
154
161
|
onCancel,
|
|
155
162
|
onActionPrevented,
|
|
156
163
|
onActionStart,
|
|
157
164
|
onActionError,
|
|
158
165
|
onActionEnd,
|
|
166
|
+
cancelOnBlurInvalid,
|
|
167
|
+
cancelOnEscape,
|
|
168
|
+
actionErrorEffect,
|
|
169
|
+
onInput,
|
|
170
|
+
onKeyDown,
|
|
159
171
|
...rest
|
|
160
172
|
} = props;
|
|
161
|
-
if (import.meta.dev && !name && !valueSignal) {
|
|
162
|
-
console.warn(`InputTextual with action requires a name prop to be set.`);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
173
|
const innerRef = useRef(null);
|
|
166
174
|
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
|
-
);
|
|
175
|
+
const [boundAction] = useActionBoundToOneParam(action, uiState);
|
|
176
176
|
const { loading: actionLoading } = useActionStatus(boundAction);
|
|
177
177
|
const executeAction = useExecuteAction(innerRef, {
|
|
178
178
|
errorEffect: actionErrorEffect,
|
|
179
179
|
});
|
|
180
|
-
useEffect(() => {
|
|
181
|
-
setNavState(value);
|
|
182
|
-
}, [value]);
|
|
183
|
-
|
|
184
180
|
const valueAtInteractionRef = useRef(null);
|
|
185
|
-
|
|
181
|
+
|
|
182
|
+
useOnInputChange(innerRef, (e) => {
|
|
186
183
|
if (
|
|
187
184
|
valueAtInteractionRef.current !== null &&
|
|
188
185
|
e.target.value === valueAtInteractionRef.current
|
|
@@ -190,9 +187,15 @@ const InputTextualWithAction = forwardRef((props, ref) => {
|
|
|
190
187
|
valueAtInteractionRef.current = null;
|
|
191
188
|
return;
|
|
192
189
|
}
|
|
193
|
-
requestAction(boundAction, {
|
|
190
|
+
requestAction(e.target, boundAction, {
|
|
191
|
+
event: e,
|
|
192
|
+
actionOrigin: "action_prop",
|
|
193
|
+
});
|
|
194
194
|
});
|
|
195
|
-
|
|
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
|
|
196
199
|
useActionEvents(innerRef, {
|
|
197
200
|
onCancel: (e, reason) => {
|
|
198
201
|
if (reason.startsWith("blur_invalid")) {
|
|
@@ -219,44 +222,23 @@ const InputTextualWithAction = forwardRef((props, ref) => {
|
|
|
219
222
|
*/
|
|
220
223
|
valueAtInteractionRef.current = e.target.value;
|
|
221
224
|
}
|
|
222
|
-
resetValue();
|
|
223
225
|
onCancel?.(e, reason);
|
|
224
226
|
},
|
|
225
227
|
onPrevented: onActionPrevented,
|
|
226
228
|
onAction: executeAction,
|
|
227
229
|
onStart: onActionStart,
|
|
228
230
|
onError: onActionError,
|
|
229
|
-
onEnd:
|
|
230
|
-
setNavState(undefined);
|
|
231
|
-
onActionEnd?.(e);
|
|
232
|
-
},
|
|
231
|
+
onEnd: onActionEnd,
|
|
233
232
|
});
|
|
234
233
|
|
|
235
|
-
const innerLoading = loading || actionLoading;
|
|
236
|
-
|
|
237
234
|
return (
|
|
238
235
|
<InputTextualBasic
|
|
236
|
+
data-action={boundAction.name}
|
|
239
237
|
{...rest}
|
|
240
|
-
data-action={boundAction}
|
|
241
238
|
ref={innerRef}
|
|
242
|
-
|
|
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}
|
|
239
|
+
loading={loading || actionLoading}
|
|
251
240
|
onInput={(e) => {
|
|
252
241
|
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
242
|
onInput?.(e);
|
|
261
243
|
}}
|
|
262
244
|
onKeyDown={(e) => {
|
|
@@ -270,53 +252,22 @@ const InputTextualWithAction = forwardRef((props, ref) => {
|
|
|
270
252
|
* We need to prevent the next change event otherwise we would request action twice
|
|
271
253
|
*/
|
|
272
254
|
valueAtInteractionRef.current = e.target.value;
|
|
273
|
-
requestAction(boundAction, {
|
|
255
|
+
requestAction(e.target, boundAction, {
|
|
256
|
+
event: e,
|
|
257
|
+
actionOrigin: "action_prop",
|
|
258
|
+
});
|
|
259
|
+
onKeyDown?.(e);
|
|
274
260
|
}}
|
|
275
261
|
/>
|
|
276
262
|
);
|
|
277
263
|
});
|
|
278
|
-
|
|
279
264
|
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]);
|
|
265
|
+
const { onKeyDown, ...rest } = props;
|
|
302
266
|
|
|
303
267
|
return (
|
|
304
268
|
<InputTextualBasic
|
|
305
269
|
{...rest}
|
|
306
|
-
ref={
|
|
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
|
-
}}
|
|
270
|
+
ref={ref}
|
|
320
271
|
onKeyDown={(e) => {
|
|
321
272
|
if (e.key === "Enter") {
|
|
322
273
|
const inputElement = e.target;
|
|
@@ -325,14 +276,137 @@ const InputTextualInsideForm = forwardRef((props, ref) => {
|
|
|
325
276
|
"button[type='submit'], input[type='submit'], input[type='image']",
|
|
326
277
|
);
|
|
327
278
|
e.preventDefault();
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
279
|
+
form.dispatchEvent(
|
|
280
|
+
new CustomEvent("actionrequested", {
|
|
281
|
+
detail: {
|
|
282
|
+
requester: formSubmitButton ? formSubmitButton : inputElement,
|
|
283
|
+
event: e,
|
|
284
|
+
meta: { isSubmit: true },
|
|
285
|
+
actionOrigin: "action_prop",
|
|
286
|
+
},
|
|
287
|
+
}),
|
|
288
|
+
);
|
|
333
289
|
}
|
|
334
290
|
onKeyDown?.(e);
|
|
335
291
|
}}
|
|
336
292
|
/>
|
|
337
293
|
);
|
|
338
294
|
});
|
|
295
|
+
|
|
296
|
+
const useOnInputChange = (inputRef, callback) => {
|
|
297
|
+
// we must use a custom event listener because preact bind onChange to onInput for compat with react
|
|
298
|
+
useEffect(() => {
|
|
299
|
+
const input = inputRef.current;
|
|
300
|
+
input.addEventListener("change", callback);
|
|
301
|
+
return () => {
|
|
302
|
+
input.removeEventListener("change", callback);
|
|
303
|
+
};
|
|
304
|
+
}, [callback]);
|
|
305
|
+
|
|
306
|
+
// Handle programmatic value changes that don't trigger browser change events
|
|
307
|
+
//
|
|
308
|
+
// Problem: When input values are set programmatically (not by user typing),
|
|
309
|
+
// browsers don't fire the 'change' event. However, our application logic
|
|
310
|
+
// still needs to detect these changes.
|
|
311
|
+
//
|
|
312
|
+
// Example scenario:
|
|
313
|
+
// 1. User starts editing (letter key pressed, value set programmatically)
|
|
314
|
+
// 2. User doesn't type anything additional (this is the key part)
|
|
315
|
+
// 3. User clicks outside to finish editing
|
|
316
|
+
// 4. Without this code, no change event would fire despite the fact that the input value did change from its original state
|
|
317
|
+
//
|
|
318
|
+
// This distinction is crucial because:
|
|
319
|
+
//
|
|
320
|
+
// - If the user typed additional text after the initial programmatic value,
|
|
321
|
+
// the browser would fire change events normally
|
|
322
|
+
// - But when they don't type anything else, the browser considers it as "no user interaction"
|
|
323
|
+
// even though the programmatic initial value represents a meaningful change
|
|
324
|
+
const valueAtStartRef = useRef();
|
|
325
|
+
const interactedRef = useRef(false);
|
|
326
|
+
useLayoutEffect(() => {
|
|
327
|
+
const input = inputRef.current;
|
|
328
|
+
valueAtStartRef.current = input.value;
|
|
329
|
+
|
|
330
|
+
const onfocus = () => {
|
|
331
|
+
interactedRef.current = false;
|
|
332
|
+
valueAtStartRef.current = input.value;
|
|
333
|
+
};
|
|
334
|
+
const oninput = (e) => {
|
|
335
|
+
if (!e.isTrusted) {
|
|
336
|
+
// non trusted "input" events will be ignored by the browser when deciding to fire "change" event
|
|
337
|
+
// we ignore them too
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
interactedRef.current = true;
|
|
341
|
+
};
|
|
342
|
+
const onblur = (e) => {
|
|
343
|
+
if (interactedRef.current) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (valueAtStartRef.current === input.value) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
callback(e);
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
input.addEventListener("focus", onfocus);
|
|
353
|
+
input.addEventListener("input", oninput);
|
|
354
|
+
input.addEventListener("blur", onblur);
|
|
355
|
+
|
|
356
|
+
return () => {
|
|
357
|
+
input.removeEventListener("focus", onfocus);
|
|
358
|
+
input.removeEventListener("input", oninput);
|
|
359
|
+
input.removeEventListener("blur", onblur);
|
|
360
|
+
};
|
|
361
|
+
}, []);
|
|
362
|
+
};
|
|
363
|
+
// As explained in https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/datetime-local#setting_timezones
|
|
364
|
+
// datetime-local does not support timezones
|
|
365
|
+
const convertToLocalTimezone = (dateTimeString) => {
|
|
366
|
+
const date = new Date(dateTimeString);
|
|
367
|
+
// Check if the date is valid
|
|
368
|
+
if (isNaN(date.getTime())) {
|
|
369
|
+
return dateTimeString;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Format to YYYY-MM-DDThh:mm:ss
|
|
373
|
+
const year = date.getFullYear();
|
|
374
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
375
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
376
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
377
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
378
|
+
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
379
|
+
|
|
380
|
+
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
|
|
381
|
+
};
|
|
382
|
+
/**
|
|
383
|
+
* Converts a datetime string without timezone (local time) to UTC format with 'Z' notation
|
|
384
|
+
*
|
|
385
|
+
* @param {string} localDateTimeString - Local datetime string without timezone (e.g., "2023-07-15T14:30:00")
|
|
386
|
+
* @returns {string} Datetime string in UTC with 'Z' notation (e.g., "2023-07-15T12:30:00Z")
|
|
387
|
+
*/
|
|
388
|
+
const convertToUTCTimezone = (localDateTimeString) => {
|
|
389
|
+
if (!localDateTimeString) {
|
|
390
|
+
return localDateTimeString;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
try {
|
|
394
|
+
// Create a Date object using the local time string
|
|
395
|
+
// The browser will interpret this as local timezone
|
|
396
|
+
const localDate = new Date(localDateTimeString);
|
|
397
|
+
|
|
398
|
+
// Check if the date is valid
|
|
399
|
+
if (isNaN(localDate.getTime())) {
|
|
400
|
+
return localDateTimeString;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Convert to UTC ISO string
|
|
404
|
+
const utcString = localDate.toISOString();
|
|
405
|
+
|
|
406
|
+
// Return the UTC string (which includes the 'Z' notation)
|
|
407
|
+
return utcString;
|
|
408
|
+
} catch (error) {
|
|
409
|
+
console.error("Error converting local datetime to UTC:", error);
|
|
410
|
+
return localDateTimeString;
|
|
411
|
+
}
|
|
412
|
+
};
|
|
@@ -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
|
+
});
|