@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.
Files changed (139) hide show
  1. package/dist/jsenv_navi.js +22959 -0
  2. package/index.js +66 -16
  3. package/package.json +23 -11
  4. package/src/actions.js +50 -26
  5. package/src/browser_integration/browser_integration.js +31 -6
  6. package/src/browser_integration/via_history.js +42 -9
  7. package/src/components/action_execution/render_actionable_component.jsx +6 -4
  8. package/src/components/action_execution/use_action.js +51 -282
  9. package/src/components/action_execution/use_execute_action.js +106 -92
  10. package/src/components/action_execution/use_run_on_mount.js +9 -0
  11. package/src/components/action_renderer.jsx +21 -32
  12. package/src/components/demos/0_button_demo.html +574 -103
  13. package/src/components/demos/10_column_reordering_debug.html +277 -0
  14. package/src/components/demos/11_table_selection_debug.html +432 -0
  15. package/src/components/demos/1_checkbox_demo.html +579 -202
  16. package/src/components/demos/2_input_textual_demo.html +81 -138
  17. package/src/components/demos/3_radio_demo.html +0 -2
  18. package/src/components/demos/4_select_demo.html +19 -23
  19. package/src/components/demos/6_tablist_demo.html +77 -0
  20. package/src/components/demos/7_table_selection_demo.html +176 -0
  21. package/src/components/demos/8_table_fixed_headers_demo.html +584 -0
  22. package/src/components/demos/9_table_column_drag_demo.html +325 -0
  23. package/src/components/demos/action/0_button_demo.html +2 -4
  24. package/src/components/demos/action/1_input_text_demo.html +643 -222
  25. package/src/components/demos/action/3_details_demo.html +146 -115
  26. package/src/components/demos/action/4_input_checkbox_demo.html +442 -322
  27. package/src/components/demos/action/5_input_checkbox_state_demo.html +270 -0
  28. package/src/components/demos/action/6_checkbox_list_demo.html +304 -72
  29. package/src/components/demos/action/7_radio_list_demo.html +310 -170
  30. package/src/components/demos/action/{8_editable_text_demo.html → 8_editable_demo.html} +65 -76
  31. package/src/components/demos/action/9_link_demo.html +84 -62
  32. package/src/components/demos/ui_transition/0_action_renderer_ui_transition_demo.html +695 -0
  33. package/src/components/demos/ui_transition/1_nested_ui_transition_demo.html +429 -0
  34. package/src/components/demos/ui_transition/2_height_transition_test.html +295 -0
  35. package/src/components/details/details.jsx +62 -64
  36. package/src/components/edition/editable.jsx +186 -0
  37. package/src/components/field/README.md +247 -0
  38. package/src/components/{input → field}/button.jsx +151 -130
  39. package/src/components/field/checkbox_list.jsx +184 -0
  40. package/src/components/{collect_form_element_values.js → field/collect_form_element_values.js} +7 -4
  41. package/src/components/{input → field}/field_css.js +4 -1
  42. package/src/components/field/form.jsx +211 -0
  43. package/src/components/{input → field}/input.jsx +1 -0
  44. package/src/components/{input → field}/input_checkbox.jsx +132 -155
  45. package/src/components/{input → field}/input_radio.jsx +135 -46
  46. package/src/components/field/input_textual.jsx +418 -0
  47. package/src/components/field/label.jsx +32 -0
  48. package/src/components/field/radio_list.jsx +182 -0
  49. package/src/components/{input → field}/select.jsx +17 -32
  50. package/src/components/field/use_action_events.js +132 -0
  51. package/src/components/field/use_form_events.js +55 -0
  52. package/src/components/field/use_ui_state_controller.js +506 -0
  53. package/src/components/item_tracker/README.md +461 -0
  54. package/src/components/item_tracker/use_isolated_item_tracker.jsx +209 -0
  55. package/src/components/item_tracker/use_isolated_item_tracker_demo.html +148 -0
  56. package/src/components/item_tracker/use_isolated_item_tracker_demo.jsx +460 -0
  57. package/src/components/item_tracker/use_item_tracker.jsx +143 -0
  58. package/src/components/item_tracker/use_item_tracker_demo.html +207 -0
  59. package/src/components/item_tracker/use_item_tracker_demo.jsx +216 -0
  60. package/src/components/keyboard_shortcuts/active_keyboard_shortcuts.jsx +87 -0
  61. package/src/components/keyboard_shortcuts/aria_key_shortcuts.js +61 -0
  62. package/src/components/keyboard_shortcuts/keyboard_key_meta.js +17 -0
  63. package/src/components/keyboard_shortcuts/keyboard_shortcuts.js +371 -0
  64. package/src/components/link/link.jsx +65 -102
  65. package/src/components/link/link_with_icon.jsx +52 -0
  66. package/src/components/loader/loader_background.jsx +85 -64
  67. package/src/components/loader/rectangle_loading.jsx +38 -19
  68. package/src/components/route.jsx +8 -4
  69. package/src/components/selection/selection.jsx +1583 -0
  70. package/src/components/svg/font_sized_svg.jsx +45 -0
  71. package/src/components/svg/icon_and_text.jsx +21 -0
  72. package/src/components/svg/svg_mask_overlay.jsx +105 -0
  73. package/src/components/table/drag/table_drag.jsx +506 -0
  74. package/src/components/table/resize/table_resize.jsx +650 -0
  75. package/src/components/table/resize/table_size.js +43 -0
  76. package/src/components/table/selection/table_selection.js +106 -0
  77. package/src/components/table/selection/table_selection.jsx +203 -0
  78. package/src/components/table/sticky/sticky_group.js +354 -0
  79. package/src/components/table/sticky/table_sticky.js +25 -0
  80. package/src/components/table/sticky/table_sticky.jsx +501 -0
  81. package/src/components/table/table.jsx +721 -0
  82. package/src/components/table/table_css.js +211 -0
  83. package/src/components/table/table_ui.jsx +49 -0
  84. package/src/components/table/use_cells_and_columns.js +90 -0
  85. package/src/components/table/use_object_array_to_cells.js +46 -0
  86. package/src/components/table/z_indexes.js +23 -0
  87. package/src/components/tablist/tablist.jsx +99 -0
  88. package/src/components/text/overflow.jsx +15 -0
  89. package/src/components/text/text_and_count.jsx +28 -0
  90. package/src/components/ui_transition.jsx +128 -0
  91. package/src/components/use_auto_focus.js +58 -7
  92. package/src/components/use_batch_during_render.js +33 -0
  93. package/src/components/use_debounce_true.js +7 -7
  94. package/src/components/use_dependencies_diff.js +35 -0
  95. package/src/components/use_focus_group.js +4 -3
  96. package/src/components/use_initial_value.js +8 -34
  97. package/src/components/use_signal_sync.js +1 -1
  98. package/src/components/use_stable_callback.js +68 -0
  99. package/src/components/use_state_array.js +16 -9
  100. package/src/docs/actions.md +22 -0
  101. package/src/notes.md +33 -12
  102. package/src/route/route.js +97 -47
  103. package/src/store/resource_graph.js +2 -1
  104. package/src/store/tests/{resource_graph_dependencies.test.js → resource_graph_dependencies.test_manual.js} +13 -13
  105. package/src/utils/is_signal.js +20 -0
  106. package/src/utils/stringify_for_display.js +4 -23
  107. package/src/validation/constraints/confirm_constraint.js +14 -0
  108. package/src/validation/constraints/create_unique_value_constraint.js +27 -0
  109. package/src/validation/constraints/native_constraints.js +313 -0
  110. package/src/validation/constraints/readonly_constraint.js +36 -0
  111. package/src/validation/constraints/single_space_constraint.js +13 -0
  112. package/src/validation/custom_constraint_validation.js +599 -0
  113. package/src/validation/custom_message.js +18 -0
  114. package/src/validation/demos/browser_style.png +0 -0
  115. package/src/validation/demos/form_validation_demo.html +142 -0
  116. package/src/validation/demos/form_validation_demo_preact.html +87 -0
  117. package/src/validation/demos/form_validation_native_popover_demo.html +168 -0
  118. package/src/validation/demos/form_validation_vs_native_demo.html +172 -0
  119. package/src/validation/demos/validation_message_demo.html +203 -0
  120. package/src/validation/hooks/use_constraints.js +23 -0
  121. package/src/validation/hooks/use_custom_validation_ref.js +73 -0
  122. package/src/validation/hooks/use_validation_message.js +19 -0
  123. package/src/validation/validation_message.js +741 -0
  124. package/src/components/editable_text/editable_text.jsx +0 -96
  125. package/src/components/form.jsx +0 -144
  126. package/src/components/input/checkbox_list.jsx +0 -294
  127. package/src/components/input/field.jsx +0 -61
  128. package/src/components/input/input_textual.jsx +0 -338
  129. package/src/components/input/radio_list.jsx +0 -283
  130. package/src/components/input/use_form_event.js +0 -20
  131. package/src/components/input/use_on_change.js +0 -12
  132. package/src/components/selection/selection.js +0 -5
  133. package/src/components/selection/selection_context.jsx +0 -262
  134. package/src/components/shortcut/shortcut_context.jsx +0 -390
  135. package/src/components/use_action_events.js +0 -37
  136. package/src/utils/iterable_weak_set.js +0 -62
  137. /package/src/components/demos/action/{11_nested_shortcuts_demo.html → 11_nested_shortcuts_demo.xhtml} +0 -0
  138. /package/src/components/{shortcut → keyboard_shortcuts}/os.js +0 -0
  139. /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;