@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
|
@@ -1,283 +0,0 @@
|
|
|
1
|
-
import { requestAction } from "@jsenv/validation";
|
|
2
|
-
import { forwardRef } from "preact/compat";
|
|
3
|
-
import { useEffect, useImperativeHandle, useRef, useState } 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
|
-
useActionBoundToOneParam,
|
|
9
|
-
useOneFormParam,
|
|
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 { Field } from "./field.jsx";
|
|
15
|
-
import { InputRadio } from "./input_radio.jsx";
|
|
16
|
-
import { useFormEvents } from "./use_form_event.js";
|
|
17
|
-
|
|
18
|
-
import.meta.css = /* css */ `
|
|
19
|
-
.radio_list {
|
|
20
|
-
display: flex;
|
|
21
|
-
flex-direction: column;
|
|
22
|
-
}
|
|
23
|
-
`;
|
|
24
|
-
|
|
25
|
-
export const RadioList = forwardRef((props, ref) => {
|
|
26
|
-
return renderActionableComponent(props, ref, {
|
|
27
|
-
Basic: RadioListBasic,
|
|
28
|
-
WithAction: RadioListWithAction,
|
|
29
|
-
InsideForm: RadioListInsideForm,
|
|
30
|
-
});
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
const RadioListControlled = forwardRef((props, ref) => {
|
|
34
|
-
const {
|
|
35
|
-
name,
|
|
36
|
-
value,
|
|
37
|
-
label,
|
|
38
|
-
loading,
|
|
39
|
-
disabled,
|
|
40
|
-
readOnly,
|
|
41
|
-
children,
|
|
42
|
-
onChange,
|
|
43
|
-
required,
|
|
44
|
-
...rest
|
|
45
|
-
} = props;
|
|
46
|
-
|
|
47
|
-
const innerRef = useRef();
|
|
48
|
-
useImperativeHandle(ref, () => innerRef.current);
|
|
49
|
-
|
|
50
|
-
return (
|
|
51
|
-
<fieldset className="radio_list" ref={innerRef} {...rest}>
|
|
52
|
-
{label ? <legend>{label}</legend> : null}
|
|
53
|
-
{children.map((child) => {
|
|
54
|
-
const {
|
|
55
|
-
label,
|
|
56
|
-
readOnly: childReadOnly,
|
|
57
|
-
disabled: childDisabled,
|
|
58
|
-
loading: childLoading,
|
|
59
|
-
onChange: childOnChange,
|
|
60
|
-
value: childValue,
|
|
61
|
-
...childRest
|
|
62
|
-
} = child;
|
|
63
|
-
|
|
64
|
-
const radio = (
|
|
65
|
-
<InputRadio
|
|
66
|
-
{...childRest}
|
|
67
|
-
// ignoreForm: each input is controller by this list
|
|
68
|
-
// we don't want the input to try to update the form because it's already done here
|
|
69
|
-
ignoreForm
|
|
70
|
-
name={name}
|
|
71
|
-
value={childValue}
|
|
72
|
-
checked={childValue === value}
|
|
73
|
-
readOnly={readOnly || childReadOnly}
|
|
74
|
-
disabled={disabled || childDisabled}
|
|
75
|
-
loading={loading || childLoading}
|
|
76
|
-
required={required}
|
|
77
|
-
onChange={(event) => {
|
|
78
|
-
onChange(event);
|
|
79
|
-
childOnChange?.(event);
|
|
80
|
-
}}
|
|
81
|
-
/>
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
return <Field key={childValue} input={radio} label={label} />;
|
|
85
|
-
})}
|
|
86
|
-
</fieldset>
|
|
87
|
-
);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
const RadioListBasic = forwardRef((props, ref) => {
|
|
91
|
-
const { value: initialValue, id, children, ...rest } = props;
|
|
92
|
-
|
|
93
|
-
const innerRef = useRef();
|
|
94
|
-
useImperativeHandle(ref, () => innerRef.current);
|
|
95
|
-
|
|
96
|
-
const [navState, setNavState] = useNavState(id);
|
|
97
|
-
const valueAtStart = navState === undefined ? initialValue : navState;
|
|
98
|
-
const [value, setValue] = useState(valueAtStart);
|
|
99
|
-
useEffect(() => {
|
|
100
|
-
setNavState(value);
|
|
101
|
-
}, [value]);
|
|
102
|
-
|
|
103
|
-
return (
|
|
104
|
-
<RadioListControlled
|
|
105
|
-
ref={innerRef}
|
|
106
|
-
value={value}
|
|
107
|
-
onChange={(event) => {
|
|
108
|
-
const radio = event.target;
|
|
109
|
-
const radioIsChecked = radio.checked;
|
|
110
|
-
if (!radioIsChecked) {
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
const value = radio.value;
|
|
114
|
-
setValue(value);
|
|
115
|
-
}}
|
|
116
|
-
{...rest}
|
|
117
|
-
>
|
|
118
|
-
{children}
|
|
119
|
-
</RadioListControlled>
|
|
120
|
-
);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
const RadioListWithAction = forwardRef((props, ref) => {
|
|
124
|
-
const {
|
|
125
|
-
id,
|
|
126
|
-
name,
|
|
127
|
-
value: externalValue,
|
|
128
|
-
valueSignal,
|
|
129
|
-
action,
|
|
130
|
-
children,
|
|
131
|
-
onCancel,
|
|
132
|
-
onActionPrevented,
|
|
133
|
-
onActionStart,
|
|
134
|
-
onActionAbort,
|
|
135
|
-
onActionError,
|
|
136
|
-
onActionEnd,
|
|
137
|
-
actionErrorEffect,
|
|
138
|
-
...rest
|
|
139
|
-
} = props;
|
|
140
|
-
|
|
141
|
-
const innerRef = useRef();
|
|
142
|
-
useImperativeHandle(ref, () => innerRef.current);
|
|
143
|
-
|
|
144
|
-
const [navState, setNavState, resetNavState] = useNavState(id);
|
|
145
|
-
const [boundAction, value, setValue, resetValue] = useActionBoundToOneParam(
|
|
146
|
-
action,
|
|
147
|
-
name,
|
|
148
|
-
valueSignal ? valueSignal : externalValue,
|
|
149
|
-
navState,
|
|
150
|
-
);
|
|
151
|
-
const { loading: actionLoading } = useActionStatus(boundAction);
|
|
152
|
-
const executeAction = useExecuteAction(innerRef, {
|
|
153
|
-
errorEffect: actionErrorEffect,
|
|
154
|
-
});
|
|
155
|
-
const actionRequesterRef = useRef(null);
|
|
156
|
-
useEffect(() => {
|
|
157
|
-
setNavState(value);
|
|
158
|
-
}, [value]);
|
|
159
|
-
|
|
160
|
-
useActionEvents(innerRef, {
|
|
161
|
-
onCancel: (e, reason) => {
|
|
162
|
-
resetNavState();
|
|
163
|
-
resetValue();
|
|
164
|
-
onCancel?.(e, reason);
|
|
165
|
-
},
|
|
166
|
-
onPrevented: onActionPrevented,
|
|
167
|
-
onAction: (actionEvent) => {
|
|
168
|
-
actionRequesterRef.current = actionEvent.detail.requester;
|
|
169
|
-
executeAction(actionEvent);
|
|
170
|
-
},
|
|
171
|
-
onStart: onActionStart,
|
|
172
|
-
onAbort: (e) => {
|
|
173
|
-
resetValue();
|
|
174
|
-
onActionAbort?.(e);
|
|
175
|
-
},
|
|
176
|
-
onError: (error) => {
|
|
177
|
-
resetValue();
|
|
178
|
-
onActionError?.(error);
|
|
179
|
-
},
|
|
180
|
-
onEnd: (e) => {
|
|
181
|
-
resetNavState();
|
|
182
|
-
onActionEnd?.(e);
|
|
183
|
-
},
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
const childRefArray = useRefArray(children, (child) => child.value);
|
|
187
|
-
|
|
188
|
-
return (
|
|
189
|
-
<RadioListControlled
|
|
190
|
-
ref={innerRef}
|
|
191
|
-
name={name}
|
|
192
|
-
value={value}
|
|
193
|
-
data-action={boundAction}
|
|
194
|
-
onChange={(event) => {
|
|
195
|
-
const radio = event.target;
|
|
196
|
-
const radioIsChecked = radio.checked;
|
|
197
|
-
if (!radioIsChecked) {
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
const value = radio.value;
|
|
201
|
-
setValue(value);
|
|
202
|
-
const radioListContainer = innerRef.current;
|
|
203
|
-
requestAction(boundAction, {
|
|
204
|
-
event,
|
|
205
|
-
target: radioListContainer,
|
|
206
|
-
requester: radio,
|
|
207
|
-
});
|
|
208
|
-
}}
|
|
209
|
-
{...rest}
|
|
210
|
-
>
|
|
211
|
-
{children.map((child, i) => {
|
|
212
|
-
const childRef = childRefArray[i];
|
|
213
|
-
return {
|
|
214
|
-
...child,
|
|
215
|
-
ref: childRef,
|
|
216
|
-
loading:
|
|
217
|
-
child.loading ||
|
|
218
|
-
(actionLoading && actionRequesterRef.current === childRef.current),
|
|
219
|
-
readOnly: child.readOnly || actionLoading,
|
|
220
|
-
};
|
|
221
|
-
})}
|
|
222
|
-
</RadioListControlled>
|
|
223
|
-
);
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
const RadioListInsideForm = forwardRef((props, ref) => {
|
|
227
|
-
const {
|
|
228
|
-
formContext,
|
|
229
|
-
id,
|
|
230
|
-
name,
|
|
231
|
-
readOnly,
|
|
232
|
-
value: externalValue,
|
|
233
|
-
children,
|
|
234
|
-
...rest
|
|
235
|
-
} = props;
|
|
236
|
-
const { formIsReadOnly } = formContext;
|
|
237
|
-
|
|
238
|
-
const innerRef = useRef();
|
|
239
|
-
useImperativeHandle(ref, () => innerRef.current);
|
|
240
|
-
|
|
241
|
-
const [navState, setNavState] = useNavState(id);
|
|
242
|
-
const [value, setValue, resetValue] = useOneFormParam(
|
|
243
|
-
name,
|
|
244
|
-
externalValue,
|
|
245
|
-
navState,
|
|
246
|
-
);
|
|
247
|
-
useEffect(() => {
|
|
248
|
-
setNavState(value);
|
|
249
|
-
}, [value]);
|
|
250
|
-
|
|
251
|
-
useFormEvents(innerRef, {
|
|
252
|
-
onFormReset: () => {
|
|
253
|
-
setValue(undefined);
|
|
254
|
-
},
|
|
255
|
-
onFormActionAbort: () => {
|
|
256
|
-
resetValue();
|
|
257
|
-
},
|
|
258
|
-
onFormActionError: () => {
|
|
259
|
-
resetValue();
|
|
260
|
-
},
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
return (
|
|
264
|
-
<RadioListControlled
|
|
265
|
-
ref={innerRef}
|
|
266
|
-
name={name}
|
|
267
|
-
value={value}
|
|
268
|
-
readOnly={readOnly || formIsReadOnly}
|
|
269
|
-
onChange={(event) => {
|
|
270
|
-
const radio = event.target;
|
|
271
|
-
const radioIsChecked = radio.checked;
|
|
272
|
-
if (!radioIsChecked) {
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
const value = radio.value;
|
|
276
|
-
setValue(value);
|
|
277
|
-
}}
|
|
278
|
-
{...rest}
|
|
279
|
-
>
|
|
280
|
-
{children}
|
|
281
|
-
</RadioListControlled>
|
|
282
|
-
);
|
|
283
|
-
});
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { useLayoutEffect } from "preact/hooks";
|
|
2
|
-
import { addManyEventListeners } from "../../utils/add_many_event_listeners.js";
|
|
3
|
-
|
|
4
|
-
export const useFormEvents = (
|
|
5
|
-
elementRef,
|
|
6
|
-
{ onFormReset, onFormActionAbort, onFormActionError },
|
|
7
|
-
) => {
|
|
8
|
-
useLayoutEffect(() => {
|
|
9
|
-
const element = elementRef.current;
|
|
10
|
-
const form = element.form;
|
|
11
|
-
|
|
12
|
-
return addManyEventListeners(form, {
|
|
13
|
-
reset: onFormReset,
|
|
14
|
-
actionabort: onFormActionAbort,
|
|
15
|
-
actionerror: (e) => {
|
|
16
|
-
onFormActionError?.(e.detail.error);
|
|
17
|
-
},
|
|
18
|
-
});
|
|
19
|
-
}, [onFormReset, onFormActionAbort, onFormActionError]);
|
|
20
|
-
};
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { useLayoutEffect } from "preact/hooks";
|
|
2
|
-
|
|
3
|
-
export const useOnChange = (innerRef, callback) => {
|
|
4
|
-
// we must use a custom event listener because preact bind onChange to onInput for compat with react
|
|
5
|
-
useLayoutEffect(() => {
|
|
6
|
-
const input = innerRef.current;
|
|
7
|
-
input.addEventListener("change", callback);
|
|
8
|
-
return () => {
|
|
9
|
-
input.removeEventListener("change", callback);
|
|
10
|
-
};
|
|
11
|
-
}, [callback]);
|
|
12
|
-
};
|
|
@@ -1,262 +0,0 @@
|
|
|
1
|
-
import { canInterceptKeys } from "@jsenv/dom";
|
|
2
|
-
import { createContext } from "preact";
|
|
3
|
-
import { useContext, useLayoutEffect, useRef } from "preact/hooks";
|
|
4
|
-
|
|
5
|
-
const SelectionContext = createContext(null);
|
|
6
|
-
|
|
7
|
-
export const SelectionProvider = ({ value = [], onChange, children }) => {
|
|
8
|
-
const selection = value || [];
|
|
9
|
-
const registryRef = useRef([]); // Array<value>
|
|
10
|
-
const anchorRef = useRef(null);
|
|
11
|
-
|
|
12
|
-
const contextValue = {
|
|
13
|
-
selection,
|
|
14
|
-
|
|
15
|
-
register: (value) => {
|
|
16
|
-
const registry = registryRef.current;
|
|
17
|
-
const existingIndex = registry.indexOf(value);
|
|
18
|
-
if (existingIndex >= 0) {
|
|
19
|
-
console.warn(
|
|
20
|
-
`SelectionContext: Attempted to register an already registered value: ${value}. All values must be unique.`,
|
|
21
|
-
);
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
registry.push(value);
|
|
25
|
-
},
|
|
26
|
-
unregister: (value) => {
|
|
27
|
-
const registry = registryRef.current;
|
|
28
|
-
const index = registry.indexOf(value);
|
|
29
|
-
if (index >= 0) {
|
|
30
|
-
registry.splice(index, 1);
|
|
31
|
-
}
|
|
32
|
-
},
|
|
33
|
-
setAnchor: (value) => {
|
|
34
|
-
anchorRef.current = value;
|
|
35
|
-
},
|
|
36
|
-
isSelected: (itemValue) => {
|
|
37
|
-
return selection.includes(itemValue);
|
|
38
|
-
},
|
|
39
|
-
getAllItems: () => {
|
|
40
|
-
return registryRef.current;
|
|
41
|
-
},
|
|
42
|
-
getRange: (fromValue, toValue) => {
|
|
43
|
-
const registry = registryRef.current;
|
|
44
|
-
|
|
45
|
-
// Find indices of fromValue and toValue
|
|
46
|
-
let fromIndex = -1;
|
|
47
|
-
let toIndex = -1;
|
|
48
|
-
let index = 0;
|
|
49
|
-
for (const valueCandidate of registry) {
|
|
50
|
-
if (valueCandidate === fromValue) {
|
|
51
|
-
fromIndex = index;
|
|
52
|
-
}
|
|
53
|
-
if (valueCandidate === toValue) {
|
|
54
|
-
toIndex = index;
|
|
55
|
-
}
|
|
56
|
-
index++;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (fromIndex >= 0 && toIndex >= 0) {
|
|
60
|
-
// Select all items between fromIndex and toIndex (inclusive)
|
|
61
|
-
const start = Math.min(fromIndex, toIndex);
|
|
62
|
-
const end = Math.max(fromIndex, toIndex);
|
|
63
|
-
const valueInRangeArray = registry.slice(start, end + 1);
|
|
64
|
-
return valueInRangeArray;
|
|
65
|
-
}
|
|
66
|
-
return [];
|
|
67
|
-
},
|
|
68
|
-
|
|
69
|
-
// basic methods to manipulate selection
|
|
70
|
-
set: (newSelection, event = null) => {
|
|
71
|
-
if (
|
|
72
|
-
newSelection.length === selection.length &&
|
|
73
|
-
newSelection.every((value, index) => value === selection[index])
|
|
74
|
-
) {
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
onChange?.(newSelection, event);
|
|
78
|
-
},
|
|
79
|
-
add: (arrayOfValueToAddToSelection, event = null) => {
|
|
80
|
-
const selectionWithValues = [];
|
|
81
|
-
for (const value of selection) {
|
|
82
|
-
selectionWithValues.push(value);
|
|
83
|
-
}
|
|
84
|
-
let modified = false;
|
|
85
|
-
for (const valueToAdd of arrayOfValueToAddToSelection) {
|
|
86
|
-
if (selectionWithValues.includes(valueToAdd)) {
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
modified = true;
|
|
90
|
-
selectionWithValues.push(valueToAdd);
|
|
91
|
-
}
|
|
92
|
-
if (modified) {
|
|
93
|
-
onChange?.(selectionWithValues, event);
|
|
94
|
-
}
|
|
95
|
-
},
|
|
96
|
-
remove: (arrayOfValueToRemoveFromSelection, event = null) => {
|
|
97
|
-
let modified = false;
|
|
98
|
-
const selectionWithoutValues = [];
|
|
99
|
-
for (const value of selection) {
|
|
100
|
-
if (arrayOfValueToRemoveFromSelection.includes(value)) {
|
|
101
|
-
modified = true;
|
|
102
|
-
// If we're removing the last selected value, clear it
|
|
103
|
-
if (value === anchorRef.current) {
|
|
104
|
-
anchorRef.current = null;
|
|
105
|
-
}
|
|
106
|
-
} else {
|
|
107
|
-
selectionWithoutValues.push(value);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (modified) {
|
|
112
|
-
onChange?.(selectionWithoutValues, event);
|
|
113
|
-
}
|
|
114
|
-
},
|
|
115
|
-
|
|
116
|
-
// Convenience method for multi-select: toggle, addFromLastSelectedTo
|
|
117
|
-
toggle: (value, event = null) => {
|
|
118
|
-
if (selection.includes(value)) {
|
|
119
|
-
contextValue.remove([value], event);
|
|
120
|
-
} else {
|
|
121
|
-
contextValue.add([value], event);
|
|
122
|
-
}
|
|
123
|
-
},
|
|
124
|
-
// Convenience method for shift-click: add range from last selected to target value
|
|
125
|
-
setFromAnchorTo: (value, event = null) => {
|
|
126
|
-
const anchorValue = anchorRef.current;
|
|
127
|
-
|
|
128
|
-
// Make sure the last selected value is still in the current selection
|
|
129
|
-
if (anchorValue && selection.includes(anchorValue)) {
|
|
130
|
-
const range = contextValue.getRange(anchorValue, value);
|
|
131
|
-
contextValue.set(range, event);
|
|
132
|
-
} else {
|
|
133
|
-
// No valid previous selection, just select this one
|
|
134
|
-
contextValue.set([value], event);
|
|
135
|
-
}
|
|
136
|
-
},
|
|
137
|
-
|
|
138
|
-
getValueAfter: (value) => {
|
|
139
|
-
const registry = registryRef.current;
|
|
140
|
-
const index = registry.indexOf(value);
|
|
141
|
-
if (index < 0 || index >= registry.length - 1) {
|
|
142
|
-
return null; // No next value
|
|
143
|
-
}
|
|
144
|
-
return registry[index + 1];
|
|
145
|
-
},
|
|
146
|
-
getValueBefore: (value) => {
|
|
147
|
-
const registry = registryRef.current;
|
|
148
|
-
const index = registry.indexOf(value);
|
|
149
|
-
if (index <= 0) {
|
|
150
|
-
return null; // No previous value
|
|
151
|
-
}
|
|
152
|
-
return registry[index - 1];
|
|
153
|
-
},
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
return (
|
|
157
|
-
<SelectionContext.Provider value={contextValue}>
|
|
158
|
-
{children}
|
|
159
|
-
</SelectionContext.Provider>
|
|
160
|
-
);
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
export const useSelectionContext = () => {
|
|
164
|
-
return useContext(SelectionContext);
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
export const useRegisterSelectionValue = (value) => {
|
|
168
|
-
const selectionContext = useSelectionContext();
|
|
169
|
-
|
|
170
|
-
useLayoutEffect(() => {
|
|
171
|
-
if (selectionContext) {
|
|
172
|
-
selectionContext.register(value);
|
|
173
|
-
return () => selectionContext.unregister(value);
|
|
174
|
-
}
|
|
175
|
-
return undefined;
|
|
176
|
-
}, [selectionContext, value]);
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
export const clickToSelect = (clickEvent, { selectionContext, value }) => {
|
|
180
|
-
if (clickEvent.defaultPrevented) {
|
|
181
|
-
// If the click was prevented by another handler, do not interfere
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const isMultiSelect = clickEvent.metaKey || clickEvent.ctrlKey;
|
|
186
|
-
const isShiftSelect = clickEvent.shiftKey;
|
|
187
|
-
const isSingleSelect = !isMultiSelect && !isShiftSelect;
|
|
188
|
-
|
|
189
|
-
if (isSingleSelect) {
|
|
190
|
-
// Single select - replace entire selection with just this item
|
|
191
|
-
selectionContext.set([value], clickEvent);
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
if (isMultiSelect) {
|
|
195
|
-
// here no need to prevent nav on <a> but it means cmd + click will both multi select
|
|
196
|
-
// and open in a new tab
|
|
197
|
-
selectionContext.toggle(value, clickEvent);
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
if (isShiftSelect) {
|
|
201
|
-
clickEvent.preventDefault(); // Prevent navigation
|
|
202
|
-
selectionContext.setFromAnchorTo(value, clickEvent);
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
export const keydownToSelect = (keydownEvent, { selectionContext, value }) => {
|
|
208
|
-
if (!canInterceptKeys(keydownEvent)) {
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (keydownEvent.key === "Shift") {
|
|
213
|
-
selectionContext.setAnchor(value);
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const isMultiSelect = keydownEvent.metaKey || keydownEvent.ctrlKey;
|
|
218
|
-
const isShiftSelect = keydownEvent.shiftKey;
|
|
219
|
-
const { key } = keydownEvent;
|
|
220
|
-
if (key === "a") {
|
|
221
|
-
if (!isMultiSelect) {
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
keydownEvent.preventDefault(); // prevent default select all text behavior
|
|
225
|
-
selectionContext.set(selectionContext.getAllItems(), keydownEvent);
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
if (key === "ArrowDown") {
|
|
229
|
-
const nextValue = selectionContext.getValueAfter(value);
|
|
230
|
-
if (!nextValue) {
|
|
231
|
-
return; // No next value to select
|
|
232
|
-
}
|
|
233
|
-
keydownEvent.preventDefault(); // Prevent default scrolling behavior
|
|
234
|
-
if (isShiftSelect) {
|
|
235
|
-
selectionContext.setFromAnchorTo(nextValue, keydownEvent);
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
if (isMultiSelect) {
|
|
239
|
-
selectionContext.add([nextValue], keydownEvent);
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
selectionContext.set([nextValue], keydownEvent);
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
if (key === "ArrowUp") {
|
|
246
|
-
const previousValue = selectionContext.getValueBefore(value);
|
|
247
|
-
if (!previousValue) {
|
|
248
|
-
return; // No previous value to select
|
|
249
|
-
}
|
|
250
|
-
keydownEvent.preventDefault(); // Prevent default scrolling behavior
|
|
251
|
-
if (isShiftSelect) {
|
|
252
|
-
selectionContext.setFromAnchorTo(previousValue, keydownEvent);
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
if (isMultiSelect) {
|
|
256
|
-
selectionContext.add([previousValue], keydownEvent);
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
selectionContext.set([previousValue], keydownEvent);
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
};
|