@jsenv/navi 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/index.js +51 -0
  2. package/package.json +38 -0
  3. package/src/action_private_properties.js +11 -0
  4. package/src/action_proxy_test.html +353 -0
  5. package/src/action_run_states.js +5 -0
  6. package/src/actions.js +1377 -0
  7. package/src/browser_integration/browser_integration.js +191 -0
  8. package/src/browser_integration/document_back_and_forward.js +17 -0
  9. package/src/browser_integration/document_loading_signal.js +100 -0
  10. package/src/browser_integration/document_state_signal.js +9 -0
  11. package/src/browser_integration/document_url_signal.js +9 -0
  12. package/src/browser_integration/use_is_visited.js +19 -0
  13. package/src/browser_integration/via_history.js +199 -0
  14. package/src/browser_integration/via_navigation.js +168 -0
  15. package/src/components/action_execution/form_context.js +8 -0
  16. package/src/components/action_execution/render_actionable_component.jsx +27 -0
  17. package/src/components/action_execution/use_action.js +330 -0
  18. package/src/components/action_execution/use_execute_action.js +161 -0
  19. package/src/components/action_renderer.jsx +136 -0
  20. package/src/components/collect_form_element_values.js +79 -0
  21. package/src/components/demos/0_button_demo.html +155 -0
  22. package/src/components/demos/1_checkbox_demo.html +257 -0
  23. package/src/components/demos/2_input_textual_demo.html +354 -0
  24. package/src/components/demos/3_radio_demo.html +222 -0
  25. package/src/components/demos/4_select_demo.html +104 -0
  26. package/src/components/demos/5_list_scrollable_demo.html +153 -0
  27. package/src/components/demos/action/0_button_demo.html +204 -0
  28. package/src/components/demos/action/10_shortcuts_demo.html +189 -0
  29. package/src/components/demos/action/11_nested_shortcuts_demo.html +401 -0
  30. package/src/components/demos/action/1_input_text_demo.html +461 -0
  31. package/src/components/demos/action/2_form_multiple.html +303 -0
  32. package/src/components/demos/action/3_details_demo.html +172 -0
  33. package/src/components/demos/action/4_input_checkbox_demo.html +611 -0
  34. package/src/components/demos/action/6_checkbox_list_demo.html +109 -0
  35. package/src/components/demos/action/7_radio_list_demo.html +217 -0
  36. package/src/components/demos/action/8_editable_text_demo.html +442 -0
  37. package/src/components/demos/action/9_link_demo.html +172 -0
  38. package/src/components/demos/demo.md +0 -0
  39. package/src/components/demos/route/basic/basic.html +14 -0
  40. package/src/components/demos/route/basic/basic_route_demo.jsx +224 -0
  41. package/src/components/demos/route/multi/multi.html +14 -0
  42. package/src/components/demos/route/multi/multi_route_demo.jsx +277 -0
  43. package/src/components/details/details.jsx +248 -0
  44. package/src/components/details/summary_marker.jsx +141 -0
  45. package/src/components/editable_text/editable_text.jsx +96 -0
  46. package/src/components/error_boundary_context.js +9 -0
  47. package/src/components/form.jsx +144 -0
  48. package/src/components/input/button.jsx +333 -0
  49. package/src/components/input/checkbox_list.jsx +294 -0
  50. package/src/components/input/field.jsx +61 -0
  51. package/src/components/input/field_css.js +118 -0
  52. package/src/components/input/input.jsx +15 -0
  53. package/src/components/input/input_checkbox.jsx +370 -0
  54. package/src/components/input/input_radio.jsx +299 -0
  55. package/src/components/input/input_textual.jsx +338 -0
  56. package/src/components/input/radio_list.jsx +283 -0
  57. package/src/components/input/select.jsx +273 -0
  58. package/src/components/input/use_form_event.js +20 -0
  59. package/src/components/input/use_on_change.js +12 -0
  60. package/src/components/link/link.jsx +291 -0
  61. package/src/components/loader/loader_background.jsx +324 -0
  62. package/src/components/loader/loading_spinner.jsx +68 -0
  63. package/src/components/loader/network_speed.js +83 -0
  64. package/src/components/loader/rectangle_loading.jsx +225 -0
  65. package/src/components/route.jsx +15 -0
  66. package/src/components/selection/selection.js +5 -0
  67. package/src/components/selection/selection_context.jsx +262 -0
  68. package/src/components/shortcut/os.js +9 -0
  69. package/src/components/shortcut/shortcut_context.jsx +390 -0
  70. package/src/components/use_action_events.js +37 -0
  71. package/src/components/use_auto_focus.js +43 -0
  72. package/src/components/use_debounce_true.js +31 -0
  73. package/src/components/use_focus_group.js +19 -0
  74. package/src/components/use_initial_value.js +104 -0
  75. package/src/components/use_is_visited.js +19 -0
  76. package/src/components/use_ref_array.js +38 -0
  77. package/src/components/use_signal_sync.js +50 -0
  78. package/src/components/use_state_array.js +40 -0
  79. package/src/docs/actions.md +228 -0
  80. package/src/docs/demos/resource/action_status.jsx +42 -0
  81. package/src/docs/demos/resource/demo.md +1 -0
  82. package/src/docs/demos/resource/resource_demo_0.html +84 -0
  83. package/src/docs/demos/resource/resource_demo_10_post_gc.html +364 -0
  84. package/src/docs/demos/resource/resource_demo_11_describe_many.html +362 -0
  85. package/src/docs/demos/resource/resource_demo_2.html +173 -0
  86. package/src/docs/demos/resource/resource_demo_3_filtered_users.html +415 -0
  87. package/src/docs/demos/resource/resource_demo_4_details.html +284 -0
  88. package/src/docs/demos/resource/resource_demo_5_renderer_lazy.html +115 -0
  89. package/src/docs/demos/resource/resource_demo_6_gc.html +217 -0
  90. package/src/docs/demos/resource/resource_demo_7_child_gc.html +240 -0
  91. package/src/docs/demos/resource/resource_demo_8_proxy_gc.html +319 -0
  92. package/src/docs/demos/resource/resource_demo_9_describe_one.html +472 -0
  93. package/src/docs/demos/resource/tata.jsx +3 -0
  94. package/src/docs/demos/resource/toto.jsx +3 -0
  95. package/src/docs/demos/user_nav/user_nav.html +12 -0
  96. package/src/docs/demos/user_nav/user_nav.jsx +330 -0
  97. package/src/docs/resource_dependencies.md +103 -0
  98. package/src/docs/resource_with_params.md +80 -0
  99. package/src/notes.md +13 -0
  100. package/src/route/route.js +518 -0
  101. package/src/route/route.test.html +228 -0
  102. package/src/store/array_signal_store.js +537 -0
  103. package/src/store/local_storage_signal.js +17 -0
  104. package/src/store/resource_graph.js +1303 -0
  105. package/src/store/tests/resource_graph_autoreload_demo.html +12 -0
  106. package/src/store/tests/resource_graph_autoreload_demo.jsx +964 -0
  107. package/src/store/tests/resource_graph_dependencies.test.js +95 -0
  108. package/src/store/value_in_local_storage.js +187 -0
  109. package/src/symbol_object_signal.js +1 -0
  110. package/src/use_action_data.js +10 -0
  111. package/src/use_action_status.js +47 -0
  112. package/src/utils/add_many_event_listeners.js +15 -0
  113. package/src/utils/array_add_remove.js +61 -0
  114. package/src/utils/array_signal.js +15 -0
  115. package/src/utils/compare_two_js_values.js +172 -0
  116. package/src/utils/execute_with_cleanup.js +21 -0
  117. package/src/utils/get_caller_info.js +85 -0
  118. package/src/utils/iterable_weak_set.js +62 -0
  119. package/src/utils/js_value_weak_map.js +162 -0
  120. package/src/utils/js_value_weak_map_demo.html +690 -0
  121. package/src/utils/merge_two_js_values.js +53 -0
  122. package/src/utils/stringify_for_display.js +150 -0
  123. package/src/utils/weak_effect.js +48 -0
@@ -0,0 +1,333 @@
1
+ import { resolveCSSSize } from "@jsenv/dom";
2
+ import { requestAction, useConstraints } from "@jsenv/validation";
3
+ import { forwardRef } from "preact/compat";
4
+ import { useImperativeHandle, useRef } from "preact/hooks";
5
+ import { useActionStatus } from "../../use_action_status.js";
6
+ import { renderActionableComponent } from "../action_execution/render_actionable_component.jsx";
7
+ import { useAction } from "../action_execution/use_action.js";
8
+ import { useExecuteAction } from "../action_execution/use_execute_action.js";
9
+ import { LoaderBackground } from "../loader/loader_background.jsx";
10
+ import { useActionEvents } from "../use_action_events.js";
11
+ import { useAutoFocus } from "../use_auto_focus.js";
12
+ import "./field_css.js";
13
+
14
+ /**
15
+ * We have to re-define the CSS of button because getComputedStyle(button).borderColor returns
16
+ * rgb(0, 0, 0) while being visually grey in chrome
17
+ * So we redefine chrome styles so that loader can keep up with the actual color visible to the user
18
+ *
19
+ */
20
+ import.meta.css = /* css */ `
21
+ button[data-custom] {
22
+ transition-duration: 0.15s;
23
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
24
+ transition-property: transform;
25
+ }
26
+ button[data-custom]:active {
27
+ transform: scale(0.9);
28
+ }
29
+ button[data-custom]:disabled {
30
+ transform: none;
31
+ }
32
+
33
+ button[data-custom] > .shadow {
34
+ position: absolute;
35
+ inset: calc(
36
+ -1 * (var(--button-border-width) + var(--button-outline-width))
37
+ );
38
+ pointer-events: none;
39
+ border-radius: inherit;
40
+ }
41
+ button[data-custom]:active > .shadow {
42
+ box-shadow:
43
+ inset 0 3px 6px rgba(0, 0, 0, 0.2),
44
+ inset 0 1px 2px rgba(0, 0, 0, 0.3),
45
+ inset 0 0 0 1px rgba(0, 0, 0, 0.1),
46
+ inset 2px 0 4px rgba(0, 0, 0, 0.1),
47
+ inset -2px 0 4px rgba(0, 0, 0, 0.1);
48
+ }
49
+ button[data-custom]:disabled > .shadow {
50
+ box-shadow: none;
51
+ }
52
+ `;
53
+ export const Button = forwardRef((props, ref) => {
54
+ return renderActionableComponent(props, ref, {
55
+ Basic: ButtonBasic,
56
+ WithAction: ButtonWithAction,
57
+ InsideForm: ButtonInsideForm,
58
+ InsideFormWithAction: ButtonWithActionInsideForm,
59
+ });
60
+ });
61
+
62
+ const ButtonBasic = forwardRef((props, ref) => {
63
+ const {
64
+ autoFocus,
65
+ constraints = [],
66
+ loading,
67
+ readOnly,
68
+ children,
69
+ appearance = "custom",
70
+ discrete,
71
+ style = {},
72
+ ...rest
73
+ } = props;
74
+
75
+ const innerRef = useRef();
76
+ useImperativeHandle(ref, () => innerRef.current);
77
+ useAutoFocus(innerRef, autoFocus);
78
+ useConstraints(innerRef, constraints);
79
+
80
+ let {
81
+ border,
82
+ borderWidth = border === "none" || discrete ? 0 : 1,
83
+ outlineWidth = discrete ? 0 : 1,
84
+ borderColor = "light-dark(#767676, #8e8e93)",
85
+ ...restStyle
86
+ } = style;
87
+ borderWidth = resolveCSSSize(borderWidth);
88
+ outlineWidth = resolveCSSSize(outlineWidth);
89
+
90
+ return (
91
+ <LoaderBackground
92
+ loading={loading}
93
+ inset={
94
+ borderWidth -
95
+ // -1 is the outline offset thing
96
+ 1
97
+ }
98
+ color="light-dark(#355fcc, #3b82f6)"
99
+ >
100
+ <button
101
+ ref={innerRef}
102
+ {...rest}
103
+ data-field=""
104
+ data-field-with-background=""
105
+ data-field-with-hover=""
106
+ data-field-with-border={borderWidth ? "" : undefined}
107
+ data-field-with-border-hover={discrete ? "" : undefined}
108
+ data-field-with-background-hover={discrete ? "" : undefined}
109
+ data-custom={appearance === "custom" ? "" : undefined}
110
+ data-validation-message-arrow-x="center"
111
+ data-readonly={readOnly ? "" : undefined}
112
+ aria-busy={loading}
113
+ style={{
114
+ ...restStyle,
115
+ "--button-border-width": `${borderWidth}px`,
116
+ "--button-outline-width": `${outlineWidth}px`,
117
+ "--button-border-color": borderColor,
118
+ "position": "relative",
119
+ }}
120
+ >
121
+ {children}
122
+ <div className="shadow"></div>
123
+ </button>
124
+ </LoaderBackground>
125
+ );
126
+ });
127
+
128
+ const ButtonWithAction = forwardRef((props, ref) => {
129
+ const {
130
+ action,
131
+ loading,
132
+ readOnly,
133
+ children,
134
+ onClick,
135
+ actionErrorEffect,
136
+ onActionPrevented,
137
+ onActionStart,
138
+ onActionError,
139
+ onActionEnd,
140
+ ...rest
141
+ } = props;
142
+
143
+ const innerRef = useRef();
144
+ useImperativeHandle(ref, () => innerRef.current);
145
+
146
+ const boundAction = useAction(action);
147
+ const { loading: actionLoading } = useActionStatus(boundAction);
148
+ const executeAction = useExecuteAction(innerRef, {
149
+ errorEffect: actionErrorEffect,
150
+ });
151
+
152
+ useActionEvents(innerRef, {
153
+ onPrevented: onActionPrevented,
154
+ onAction: executeAction,
155
+ onStart: onActionStart,
156
+ onError: onActionError,
157
+ onEnd: onActionEnd,
158
+ });
159
+
160
+ const handleClick = (event) => {
161
+ event.preventDefault();
162
+ requestAction(boundAction, { event });
163
+ };
164
+ const innerLoading = loading || actionLoading;
165
+
166
+ return (
167
+ <ButtonBasic
168
+ data-action={boundAction.name}
169
+ ref={innerRef}
170
+ {...rest}
171
+ loading={innerLoading}
172
+ readOnly={readOnly || innerLoading}
173
+ onClick={(event) => {
174
+ handleClick(event);
175
+ onClick?.(event);
176
+ }}
177
+ >
178
+ {children}
179
+ </ButtonBasic>
180
+ );
181
+ });
182
+
183
+ const ButtonInsideForm = forwardRef((props, ref) => {
184
+ const { formContext, type, loading, readOnly, onClick, children, ...rest } =
185
+ props;
186
+ const { formAction, formIsBusy, formIsReadOnly, formActionRequester } =
187
+ formContext;
188
+
189
+ const innerRef = useRef();
190
+ useImperativeHandle(ref, () => innerRef.current);
191
+
192
+ const wouldSubmitFormByType = type === "submit" || type === "image";
193
+ const innerReadOnly = readOnly || formIsReadOnly;
194
+
195
+ const handleClick = (event) => {
196
+ const buttonElement = event.target;
197
+ const { form } = buttonElement;
198
+ let wouldSubmitForm = wouldSubmitFormByType;
199
+ if (!wouldSubmitForm) {
200
+ const formSubmitButton = form.querySelector(
201
+ "button[type='submit'], input[type='submit'], input[type='image']",
202
+ );
203
+ const wouldSubmitFormBecauseSingleButton = !formSubmitButton;
204
+ wouldSubmitForm = wouldSubmitFormBecauseSingleButton;
205
+ }
206
+ if (!wouldSubmitForm) {
207
+ if (innerReadOnly) {
208
+ event.preventDefault();
209
+ }
210
+ return;
211
+ }
212
+ // prevent default behavior that would submit the form
213
+ // we want to go through the action execution process (with validation and all)
214
+ event.preventDefault();
215
+ requestAction(formAction, {
216
+ event,
217
+ target: form,
218
+ requester: buttonElement,
219
+ meta: { isSubmit: true },
220
+ });
221
+ };
222
+
223
+ return (
224
+ <ButtonBasic
225
+ ref={innerRef}
226
+ {...rest}
227
+ type={type}
228
+ loading={
229
+ loading || (formIsBusy && formActionRequester === innerRef.current)
230
+ }
231
+ readOnly={innerReadOnly}
232
+ data-readonly-silent={formIsReadOnly ? "" : undefined}
233
+ onClick={(event) => {
234
+ handleClick(event);
235
+ onClick?.(event);
236
+ }}
237
+ >
238
+ {children}
239
+ </ButtonBasic>
240
+ );
241
+ });
242
+
243
+ const ButtonWithActionInsideForm = forwardRef((props, ref) => {
244
+ const {
245
+ formContext,
246
+ type,
247
+ action,
248
+ loading,
249
+ readOnly,
250
+ children,
251
+ onClick,
252
+ actionErrorEffect,
253
+ onActionPrevented,
254
+ onActionStart,
255
+ onActionAbort,
256
+ onActionError,
257
+ onActionEnd,
258
+ ...rest
259
+ } = props;
260
+ const hasEffectOnForm =
261
+ type === "submit" || type === "reset" || type === "image";
262
+ if (import.meta.dev && hasEffectOnForm) {
263
+ throw new Error(
264
+ "Button with type submit/reset/image should not have their own action",
265
+ );
266
+ }
267
+
268
+ const innerRef = useRef();
269
+ useImperativeHandle(ref, () => innerRef.current);
270
+
271
+ const { formIsReadOnly, formParamsSignal } = formContext;
272
+ const actionBoundToFormParams = useAction(action, formParamsSignal);
273
+ const { loading: actionLoading } = useActionStatus(actionBoundToFormParams);
274
+ const executeAction = useExecuteAction(innerRef, {
275
+ errorEffect: actionErrorEffect,
276
+ });
277
+
278
+ useActionEvents(innerRef, {
279
+ onPrevented: onActionPrevented,
280
+ onAction: executeAction,
281
+ onStart: (e) => {
282
+ e.target.form.dispatchEvent(
283
+ new CustomEvent("actionstart", { detail: e.detail }),
284
+ );
285
+ onActionStart?.(e);
286
+ },
287
+ onAbort: (e) => {
288
+ e.target.form.dispatchEvent(
289
+ new CustomEvent("actionabort", { detail: e.detail }),
290
+ );
291
+ onActionAbort?.(e);
292
+ },
293
+ onError: (e) => {
294
+ e.target.form.dispatchEvent(
295
+ new CustomEvent("actionerror", { detail: e.detail }),
296
+ );
297
+ onActionError?.(e);
298
+ },
299
+ onEnd: (e) => {
300
+ e.target.form.dispatchEvent(
301
+ new CustomEvent("actionend", { detail: e.detail }),
302
+ );
303
+ onActionEnd?.(e);
304
+ },
305
+ });
306
+
307
+ const handleClick = (event) => {
308
+ event.preventDefault();
309
+ // lorsque cette action s'éxecute elle doit mettre le form en mode busy
310
+ // je vois pas encore comment je vais faire ca mais a priori
311
+ // on va juste le faire "manuellement"
312
+ // en utilisnt un truc du formContext
313
+ requestAction(actionBoundToFormParams, { event });
314
+ };
315
+
316
+ return (
317
+ <ButtonBasic
318
+ data-action={actionBoundToFormParams.name}
319
+ ref={innerRef}
320
+ {...rest}
321
+ type={type}
322
+ loading={loading || actionLoading}
323
+ readOnly={readOnly || formIsReadOnly}
324
+ data-readonly-silent={!readOnly && formIsReadOnly ? "" : undefined}
325
+ onClick={(event) => {
326
+ handleClick(event);
327
+ onClick?.(event);
328
+ }}
329
+ >
330
+ {children}
331
+ </ButtonBasic>
332
+ );
333
+ });
@@ -0,0 +1,294 @@
1
+ import { requestAction } from "@jsenv/validation";
2
+ import { forwardRef } from "preact/compat";
3
+ import { useEffect, useImperativeHandle, useRef } from "preact/hooks";
4
+ import { useNavState } from "../../browser_integration/browser_integration.js";
5
+ import { useActionStatus } from "../../use_action_status.js";
6
+ import { renderActionableComponent } from "../action_execution/render_actionable_component.jsx";
7
+ import {
8
+ useActionBoundToOneArrayParam,
9
+ useOneFormArrayParam,
10
+ } from "../action_execution/use_action.js";
11
+ import { useExecuteAction } from "../action_execution/use_execute_action.js";
12
+ import { useActionEvents } from "../use_action_events.js";
13
+ import { useRefArray } from "../use_ref_array.js";
14
+ import { useStateArray } from "../use_state_array.js";
15
+ import { Field } from "./field.jsx";
16
+ import { InputCheckbox } from "./input_checkbox.jsx";
17
+ import { useFormEvents } from "./use_form_event.js";
18
+
19
+ import.meta.css = /* css */ `
20
+ .checkbox_list {
21
+ display: flex;
22
+ flex-direction: column;
23
+ }
24
+ `;
25
+
26
+ export const CheckboxList = forwardRef((props, ref) => {
27
+ return renderActionableComponent(props, ref, {
28
+ Basic: CheckboxListBasic,
29
+ WithAction: CheckboxListWithAction,
30
+ InsideForm: CheckboxListInsideForm,
31
+ });
32
+ });
33
+
34
+ const CheckboxListControlled = forwardRef((props, ref) => {
35
+ const {
36
+ name,
37
+ value,
38
+ label,
39
+ loading,
40
+ disabled,
41
+ readOnly,
42
+ children,
43
+ onChange,
44
+ ...rest
45
+ } = props;
46
+
47
+ const innerRef = useRef();
48
+ useImperativeHandle(ref, () => innerRef.current);
49
+
50
+ return (
51
+ <fieldset
52
+ {...rest}
53
+ className="checkbox_list"
54
+ ref={innerRef}
55
+ data-checkbox-list
56
+ >
57
+ {label ? <legend>{label}</legend> : null}
58
+ {children.map((child) => {
59
+ const {
60
+ label: childLabel,
61
+ readOnly: childReadOnly,
62
+ disabled: childDisabled,
63
+ loading: childLoading,
64
+ onChange: childOnChange,
65
+ value: childValue,
66
+ ...childRest
67
+ } = child;
68
+
69
+ const checkbox = (
70
+ <InputCheckbox
71
+ {...childRest}
72
+ // ignoreForm: each input is controller by this list
73
+ // we don't want the input to try to update the form because it's already done here
74
+ ignoreForm
75
+ name={name}
76
+ value={childValue}
77
+ checked={value.includes(childValue)}
78
+ readOnly={readOnly || childReadOnly}
79
+ disabled={disabled || childDisabled}
80
+ loading={loading || childLoading}
81
+ onChange={(event) => {
82
+ onChange(event, child);
83
+ childOnChange?.(event);
84
+ }}
85
+ />
86
+ );
87
+
88
+ return <Field key={childValue} input={checkbox} label={childLabel} />;
89
+ })}
90
+ </fieldset>
91
+ );
92
+ });
93
+
94
+ const CheckboxListBasic = forwardRef((props, ref) => {
95
+ const { value: externalValue, id, children, ...rest } = props;
96
+
97
+ const innerRef = useRef();
98
+ useImperativeHandle(ref, () => innerRef.current);
99
+
100
+ const [navState, setNavState] = useNavState(id);
101
+ const [valueArray, addValue, removeValue] = useStateArray(
102
+ externalValue,
103
+ navState,
104
+ [],
105
+ );
106
+ useEffect(() => {
107
+ setNavState(valueArray);
108
+ }, [valueArray]);
109
+
110
+ return (
111
+ <CheckboxListControlled
112
+ ref={innerRef}
113
+ value={valueArray}
114
+ onChange={(event) => {
115
+ const checkbox = event.target;
116
+ const checkboxIsChecked = checkbox.checked;
117
+ const checkboxValue = checkbox.value;
118
+ if (checkboxIsChecked) {
119
+ addValue(checkboxValue);
120
+ } else {
121
+ removeValue(checkboxValue);
122
+ }
123
+ }}
124
+ {...rest}
125
+ >
126
+ {children}
127
+ </CheckboxListControlled>
128
+ );
129
+ });
130
+
131
+ const CheckboxListWithAction = forwardRef((props, ref) => {
132
+ const {
133
+ id,
134
+ name,
135
+ value: externalValue,
136
+ valueSignal,
137
+ action,
138
+ children,
139
+ actionErrorEffect,
140
+ onCancel,
141
+ onActionPrevented,
142
+ onActionStart,
143
+ onActionAbort,
144
+ onActionError,
145
+ onActionEnd,
146
+ ...rest
147
+ } = props;
148
+
149
+ const innerRef = useRef();
150
+ useImperativeHandle(ref, () => innerRef.current);
151
+
152
+ const [navState, setNavState, resetNavState] = useNavState(id);
153
+ const [boundAction, valueArray, addValue, removeValue, resetValueArray] =
154
+ useActionBoundToOneArrayParam(
155
+ action,
156
+ name,
157
+ valueSignal ? valueSignal : externalValue,
158
+ navState,
159
+ [],
160
+ );
161
+ const { loading: actionLoading } = useActionStatus(boundAction);
162
+ const executeAction = useExecuteAction(innerRef, {
163
+ errorEffect: actionErrorEffect,
164
+ });
165
+ useEffect(() => {
166
+ setNavState(valueArray);
167
+ }, [valueArray]);
168
+
169
+ const actionRequesterRef = useRef(null);
170
+ useActionEvents(innerRef, {
171
+ onCancel: (e, reason) => {
172
+ resetNavState();
173
+ resetValueArray();
174
+ onCancel?.(e, reason);
175
+ },
176
+ onPrevented: onActionPrevented,
177
+ onAction: (actionEvent) => {
178
+ actionRequesterRef.current = actionEvent.detail.requester;
179
+ executeAction(actionEvent);
180
+ },
181
+ onStart: onActionStart,
182
+ onAbort: (e) => {
183
+ resetValueArray();
184
+ onActionAbort?.(e);
185
+ },
186
+ onError: (e) => {
187
+ resetValueArray();
188
+ onActionError?.(e);
189
+ },
190
+ onEnd: (e) => {
191
+ resetNavState();
192
+ onActionEnd?.(e);
193
+ },
194
+ });
195
+
196
+ const childRefArray = useRefArray(children, (child) => child.value);
197
+
198
+ return (
199
+ <CheckboxListControlled
200
+ {...rest}
201
+ name={name}
202
+ value={valueArray}
203
+ data-action={boundAction}
204
+ ref={innerRef}
205
+ onChange={(event) => {
206
+ const checkbox = event.target;
207
+ const checkboxIsChecked = checkbox.checked;
208
+ const checkboxValue = checkbox.value;
209
+ if (checkboxIsChecked) {
210
+ addValue(checkboxValue, valueArray);
211
+ } else {
212
+ removeValue(checkboxValue, valueArray);
213
+ }
214
+ const checkboxListContainer = innerRef.current;
215
+ requestAction(boundAction, {
216
+ event,
217
+ target: checkboxListContainer,
218
+ requester: checkbox,
219
+ });
220
+ }}
221
+ >
222
+ {children.map((child, i) => {
223
+ const childRef = childRefArray[i];
224
+ const loading =
225
+ child.loading ||
226
+ (actionLoading && actionRequesterRef.current === childRef.current);
227
+
228
+ return {
229
+ ...child,
230
+ ref: childRef,
231
+ loading,
232
+ readOnly: child.readOnly || actionLoading,
233
+ };
234
+ })}
235
+ </CheckboxListControlled>
236
+ );
237
+ });
238
+
239
+ const CheckboxListInsideForm = forwardRef((props, ref) => {
240
+ const {
241
+ formContext,
242
+ id,
243
+ name,
244
+ readOnly,
245
+ value: initialValue,
246
+ children,
247
+ ...rest
248
+ } = props;
249
+ const { formIsReadOnly } = formContext;
250
+
251
+ const innerRef = useRef();
252
+ useImperativeHandle(ref, () => innerRef.current);
253
+
254
+ const [navState, setNavState] = useNavState(id);
255
+ const [valueArray, addValue, removeValue, resetValueArray] =
256
+ useOneFormArrayParam(name, initialValue, navState, []);
257
+ useEffect(() => {
258
+ setNavState(valueArray);
259
+ }, [valueArray]);
260
+
261
+ useFormEvents(innerRef, {
262
+ onFormReset: () => {
263
+ resetValueArray();
264
+ },
265
+ onFormActionAbort: () => {
266
+ resetValueArray();
267
+ },
268
+ onFormActionError: () => {
269
+ resetValueArray();
270
+ },
271
+ });
272
+
273
+ return (
274
+ <CheckboxListControlled
275
+ ref={innerRef}
276
+ name={name}
277
+ value={valueArray}
278
+ readOnly={readOnly || formIsReadOnly}
279
+ onChange={(event) => {
280
+ const checkbox = event.target;
281
+ const checkboxIsChecked = checkbox.checked;
282
+ const checkboxValue = checkbox.value;
283
+ if (checkboxIsChecked) {
284
+ addValue(checkboxValue, valueArray);
285
+ } else {
286
+ removeValue(checkboxValue, valueArray);
287
+ }
288
+ }}
289
+ {...rest}
290
+ >
291
+ {children}
292
+ </CheckboxListControlled>
293
+ );
294
+ });
@@ -0,0 +1,61 @@
1
+ import { useLayoutEffect, useRef, useState } from "preact/hooks";
2
+
3
+ import.meta.css = /* css */ `
4
+ .label_wrapper_for_opacity {
5
+ display: inline-flex;
6
+ }
7
+
8
+ label[data-readonly] .label_wrapper_for_opacity {
9
+ opacity: 0.5;
10
+ }
11
+ `;
12
+
13
+ export const Field = (props) => {
14
+ const { label, input, readOnly, ...rest } = props;
15
+
16
+ const keys = Object.keys(props);
17
+ const labelIndex = keys.indexOf("label");
18
+ const inputIndex = keys.indexOf("input");
19
+ const labelBeforeInput = labelIndex < inputIndex;
20
+
21
+ const labelWrapped = <div className="label_wrapper_for_opacity">{label}</div>;
22
+
23
+ const children = labelBeforeInput
24
+ ? [labelWrapped, input]
25
+ : [input, labelWrapped];
26
+
27
+ const [inputReadOnly, setInputReadOnly] = useState(false);
28
+ const innerReadOnly = readOnly || inputReadOnly;
29
+ const labelRef = useRef();
30
+ useLayoutEffect(() => {
31
+ if (readOnly) {
32
+ return null;
33
+ }
34
+ let animationFrame;
35
+ const updateInputDisabled = () => {
36
+ const label = labelRef.current;
37
+ const input = label.querySelector("input");
38
+ if (!input) {
39
+ setInputReadOnly(false);
40
+ } else {
41
+ setInputReadOnly(input.readonly || input.hasAttribute("data-readonly"));
42
+ }
43
+ animationFrame = requestAnimationFrame(updateInputDisabled);
44
+ };
45
+ updateInputDisabled();
46
+
47
+ return () => {
48
+ cancelAnimationFrame(animationFrame);
49
+ };
50
+ }, [readOnly]);
51
+
52
+ return (
53
+ <label
54
+ ref={labelRef}
55
+ data-readonly={innerReadOnly ? "" : undefined}
56
+ {...rest}
57
+ >
58
+ {children}
59
+ </label>
60
+ );
61
+ };