@jsenv/navi 0.10.1 → 0.11.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.
Files changed (207) hide show
  1. package/dist/jsenv_navi.js +13858 -23295
  2. package/dist/jsenv_navi.js.map +1281 -0
  3. package/package.json +5 -7
  4. package/index.js +0 -122
  5. package/src/action_private_properties.js +0 -11
  6. package/src/action_proxy_test.html +0 -353
  7. package/src/action_run_states.js +0 -5
  8. package/src/actions.js +0 -1401
  9. package/src/browser_integration/browser_integration.js +0 -216
  10. package/src/browser_integration/document_back_and_forward.js +0 -17
  11. package/src/browser_integration/document_loading_signal.js +0 -100
  12. package/src/browser_integration/document_state_signal.js +0 -9
  13. package/src/browser_integration/document_url_signal.js +0 -9
  14. package/src/browser_integration/use_is_visited.js +0 -19
  15. package/src/browser_integration/via_history.js +0 -232
  16. package/src/browser_integration/via_navigation.js +0 -168
  17. package/src/components/action_execution/form_context.js +0 -5
  18. package/src/components/action_execution/render_actionable_component.jsx +0 -29
  19. package/src/components/action_execution/use_action.js +0 -99
  20. package/src/components/action_execution/use_execute_action.js +0 -177
  21. package/src/components/action_execution/use_run_on_mount.js +0 -9
  22. package/src/components/action_renderer.jsx +0 -125
  23. package/src/components/callout/callout.js +0 -990
  24. package/src/components/callout/callout_demo.html +0 -201
  25. package/src/components/callout/test_dynamic_positioning.html +0 -161
  26. package/src/components/callout/test_html_document_iframe.html +0 -182
  27. package/src/components/demos/0_button_demo.html +0 -707
  28. package/src/components/demos/10_column_reordering_debug.html +0 -277
  29. package/src/components/demos/11_table_selection_debug.html +0 -432
  30. package/src/components/demos/1_checkbox_demo.html +0 -754
  31. package/src/components/demos/2_input_textual_demo.html +0 -286
  32. package/src/components/demos/3_radio_demo.html +0 -874
  33. package/src/components/demos/4_select_demo.html +0 -100
  34. package/src/components/demos/5_list_scrollable_demo.html +0 -153
  35. package/src/components/demos/6_tablist_demo.html +0 -77
  36. package/src/components/demos/7_table_selection_demo.html +0 -176
  37. package/src/components/demos/8_table_fixed_headers_demo.html +0 -584
  38. package/src/components/demos/9_table_column_drag_demo.html +0 -325
  39. package/src/components/demos/action/0_button_demo.html +0 -204
  40. package/src/components/demos/action/10_shortcuts_demo.html +0 -189
  41. package/src/components/demos/action/11_nested_shortcuts_demo.xhtml +0 -401
  42. package/src/components/demos/action/1_input_text_demo.html +0 -876
  43. package/src/components/demos/action/2_form_multiple.html +0 -303
  44. package/src/components/demos/action/3_details_demo.html +0 -203
  45. package/src/components/demos/action/4_input_checkbox_demo.html +0 -731
  46. package/src/components/demos/action/5_input_checkbox_state_demo.html +0 -270
  47. package/src/components/demos/action/6_checkbox_list_demo.html +0 -341
  48. package/src/components/demos/action/7_radio_list_demo.html +0 -357
  49. package/src/components/demos/action/8_editable_demo.html +0 -431
  50. package/src/components/demos/action/9_link_demo.html +0 -194
  51. package/src/components/demos/demo.md +0 -0
  52. package/src/components/demos/route/basic/basic.html +0 -14
  53. package/src/components/demos/route/basic/basic_route_demo.jsx +0 -224
  54. package/src/components/demos/route/multi/multi.html +0 -14
  55. package/src/components/demos/route/multi/multi_route_demo.jsx +0 -277
  56. package/src/components/demos/ui_transition/0_action_renderer_ui_transition_demo.html +0 -695
  57. package/src/components/demos/ui_transition/1_nested_ui_transition_demo.html +0 -429
  58. package/src/components/demos/ui_transition/2_height_transition_test.html +0 -295
  59. package/src/components/details/details.jsx +0 -245
  60. package/src/components/details/summary_marker.jsx +0 -141
  61. package/src/components/edition/editable.jsx +0 -186
  62. package/src/components/error_boundary_context.js +0 -9
  63. package/src/components/field/README.md +0 -247
  64. package/src/components/field/button.jsx +0 -429
  65. package/src/components/field/checkbox_list.jsx +0 -185
  66. package/src/components/field/collect_form_element_values.js +0 -82
  67. package/src/components/field/custom_field.js +0 -106
  68. package/src/components/field/form.jsx +0 -209
  69. package/src/components/field/input.jsx +0 -16
  70. package/src/components/field/input_checkbox.jsx +0 -434
  71. package/src/components/field/input_radio.jsx +0 -432
  72. package/src/components/field/input_textual.jsx +0 -389
  73. package/src/components/field/label.jsx +0 -46
  74. package/src/components/field/radio_list.jsx +0 -183
  75. package/src/components/field/select.jsx +0 -256
  76. package/src/components/field/use_action_events.js +0 -132
  77. package/src/components/field/use_form_events.js +0 -59
  78. package/src/components/field/use_ui_state_controller.js +0 -506
  79. package/src/components/item_tracker/README.md +0 -461
  80. package/src/components/item_tracker/use_isolated_item_tracker.jsx +0 -209
  81. package/src/components/item_tracker/use_isolated_item_tracker_demo.html +0 -148
  82. package/src/components/item_tracker/use_isolated_item_tracker_demo.jsx +0 -460
  83. package/src/components/item_tracker/use_item_tracker.jsx +0 -143
  84. package/src/components/item_tracker/use_item_tracker_demo.html +0 -207
  85. package/src/components/item_tracker/use_item_tracker_demo.jsx +0 -216
  86. package/src/components/keyboard_shortcuts/active_keyboard_shortcuts.jsx +0 -87
  87. package/src/components/keyboard_shortcuts/aria_key_shortcuts.js +0 -61
  88. package/src/components/keyboard_shortcuts/keyboard_key_meta.js +0 -17
  89. package/src/components/keyboard_shortcuts/keyboard_shortcuts.js +0 -371
  90. package/src/components/keyboard_shortcuts/os.js +0 -9
  91. package/src/components/layout/demos/demo_flex.html +0 -638
  92. package/src/components/layout/demos/demo_layout_style_buttons.html +0 -351
  93. package/src/components/layout/demos/demo_layout_style_input.html +0 -226
  94. package/src/components/layout/demos/demo_layout_style_text.html +0 -514
  95. package/src/components/layout/flex.jsx +0 -109
  96. package/src/components/layout/layout_context.jsx +0 -3
  97. package/src/components/layout/spacing.jsx +0 -20
  98. package/src/components/layout/use_layout_style.js +0 -249
  99. package/src/components/link/link.jsx +0 -267
  100. package/src/components/link/link_with_icon.jsx +0 -52
  101. package/src/components/loader/loader_background.jsx +0 -372
  102. package/src/components/loader/loading_spinner.jsx +0 -68
  103. package/src/components/loader/network_speed.js +0 -83
  104. package/src/components/loader/rectangle_loading.jsx +0 -244
  105. package/src/components/props_composition/demos/demo_with_props_style.html +0 -81
  106. package/src/components/props_composition/with_props_class_name.js +0 -37
  107. package/src/components/props_composition/with_props_style.js +0 -26
  108. package/src/components/route.jsx +0 -19
  109. package/src/components/selection/selection.jsx +0 -1583
  110. package/src/components/svg/font_sized_svg.jsx +0 -59
  111. package/src/components/svg/icon_and_text.jsx +0 -21
  112. package/src/components/svg/svg_mask_overlay.jsx +0 -105
  113. package/src/components/table/drag/table_drag.jsx +0 -506
  114. package/src/components/table/resize/table_resize.jsx +0 -650
  115. package/src/components/table/resize/table_size.js +0 -43
  116. package/src/components/table/selection/table_selection.js +0 -106
  117. package/src/components/table/selection/table_selection.jsx +0 -203
  118. package/src/components/table/sticky/sticky_group.js +0 -354
  119. package/src/components/table/sticky/table_sticky.js +0 -25
  120. package/src/components/table/sticky/table_sticky.jsx +0 -501
  121. package/src/components/table/table.jsx +0 -721
  122. package/src/components/table/table_css.js +0 -211
  123. package/src/components/table/table_ui.jsx +0 -49
  124. package/src/components/table/use_cells_and_columns.js +0 -90
  125. package/src/components/table/use_object_array_to_cells.js +0 -46
  126. package/src/components/table/z_indexes.js +0 -23
  127. package/src/components/tablist/tablist.jsx +0 -99
  128. package/src/components/text/demos/demo_text_and_icon.html +0 -421
  129. package/src/components/text/overflow.jsx +0 -15
  130. package/src/components/text/text.jsx +0 -83
  131. package/src/components/text/text_and_count.jsx +0 -28
  132. package/src/components/ui_transition.jsx +0 -128
  133. package/src/components/use_auto_focus.js +0 -94
  134. package/src/components/use_batch_during_render.js +0 -33
  135. package/src/components/use_debounce_true.js +0 -31
  136. package/src/components/use_dependencies_diff.js +0 -35
  137. package/src/components/use_focus_group.js +0 -20
  138. package/src/components/use_initial_value.js +0 -78
  139. package/src/components/use_is_visited.js +0 -19
  140. package/src/components/use_ref_array.js +0 -38
  141. package/src/components/use_signal_sync.js +0 -50
  142. package/src/components/use_stable_callback.js +0 -68
  143. package/src/components/use_state_array.js +0 -47
  144. package/src/docs/actions.md +0 -250
  145. package/src/docs/demos/resource/action_status.jsx +0 -42
  146. package/src/docs/demos/resource/demo.md +0 -1
  147. package/src/docs/demos/resource/resource_demo_0.html +0 -84
  148. package/src/docs/demos/resource/resource_demo_10_post_gc.html +0 -364
  149. package/src/docs/demos/resource/resource_demo_11_describe_many.html +0 -362
  150. package/src/docs/demos/resource/resource_demo_2.html +0 -173
  151. package/src/docs/demos/resource/resource_demo_3_filtered_users.html +0 -415
  152. package/src/docs/demos/resource/resource_demo_4_details.html +0 -284
  153. package/src/docs/demos/resource/resource_demo_5_renderer_lazy.html +0 -115
  154. package/src/docs/demos/resource/resource_demo_6_gc.html +0 -217
  155. package/src/docs/demos/resource/resource_demo_7_child_gc.html +0 -240
  156. package/src/docs/demos/resource/resource_demo_8_proxy_gc.html +0 -319
  157. package/src/docs/demos/resource/resource_demo_9_describe_one.html +0 -472
  158. package/src/docs/demos/resource/tata.jsx +0 -3
  159. package/src/docs/demos/resource/toto.jsx +0 -3
  160. package/src/docs/demos/user_nav/user_nav.html +0 -12
  161. package/src/docs/demos/user_nav/user_nav.jsx +0 -330
  162. package/src/docs/resource_dependencies.md +0 -103
  163. package/src/docs/resource_with_params.md +0 -80
  164. package/src/navi_css_vars.js +0 -14
  165. package/src/notes.md +0 -34
  166. package/src/route/route.js +0 -596
  167. package/src/route/route.xtest.html +0 -228
  168. package/src/store/array_signal_store.js +0 -537
  169. package/src/store/local_storage_signal.js +0 -17
  170. package/src/store/resource_graph.js +0 -1304
  171. package/src/store/tests/resource_graph_autoreload_demo.html +0 -12
  172. package/src/store/tests/resource_graph_autoreload_demo.jsx +0 -964
  173. package/src/store/tests/resource_graph_dependencies.test_manual.js +0 -95
  174. package/src/store/value_in_local_storage.js +0 -187
  175. package/src/symbol_object_signal.js +0 -1
  176. package/src/use_action_data.js +0 -10
  177. package/src/use_action_status.js +0 -47
  178. package/src/utils/add_many_event_listeners.js +0 -15
  179. package/src/utils/array_add_remove.js +0 -61
  180. package/src/utils/array_signal.js +0 -15
  181. package/src/utils/compare_two_js_values.js +0 -172
  182. package/src/utils/execute_with_cleanup.js +0 -21
  183. package/src/utils/get_caller_info.js +0 -85
  184. package/src/utils/is_signal.js +0 -20
  185. package/src/utils/js_value_weak_map.js +0 -162
  186. package/src/utils/js_value_weak_map_demo.html +0 -690
  187. package/src/utils/merge_two_js_values.js +0 -53
  188. package/src/utils/stringify_for_display.js +0 -131
  189. package/src/utils/weak_effect.js +0 -48
  190. package/src/validation/constraints/confirm_constraint.js +0 -14
  191. package/src/validation/constraints/create_unique_value_constraint.js +0 -27
  192. package/src/validation/constraints/native_constraints.js +0 -338
  193. package/src/validation/constraints/readonly_constraint.js +0 -41
  194. package/src/validation/constraints/same_as_constraint.js +0 -42
  195. package/src/validation/constraints/single_space_constraint.js +0 -13
  196. package/src/validation/custom_constraint_validation.js +0 -793
  197. package/src/validation/custom_message.js +0 -18
  198. package/src/validation/demos/browser_style.png +0 -0
  199. package/src/validation/demos/demo_same_as_constraint.html +0 -259
  200. package/src/validation/demos/form_validation_demo.html +0 -142
  201. package/src/validation/demos/form_validation_demo_preact.html +0 -87
  202. package/src/validation/demos/form_validation_native_popover_demo.html +0 -168
  203. package/src/validation/demos/form_validation_vs_native_demo.html +0 -172
  204. package/src/validation/hooks/use_constraints.js +0 -23
  205. package/src/validation/hooks/use_custom_validation_ref.js +0 -73
  206. package/src/validation/hooks/use_validation_message.js +0 -19
  207. package/src/validation/input_change_effect.js +0 -106
@@ -1,1583 +0,0 @@
1
- import { createPubSub, findAfter, findBefore } from "@jsenv/dom";
2
- import { createContext } from "preact";
3
- import {
4
- useEffect,
5
- useLayoutEffect,
6
- useMemo,
7
- useRef,
8
- useState,
9
- } from "preact/hooks";
10
-
11
- import { compareTwoJsValues } from "../../utils/compare_two_js_values.js";
12
- import { useStableCallback } from "../use_stable_callback.js";
13
-
14
- const DEBUG = {
15
- registration: false, // Element registration/unregistration
16
- interaction: false, // Click and keyboard interactions
17
- selection: false, // Selection state changes (set, add, remove, toggle)
18
- navigation: false, // Arrow key navigation and element finding
19
- valueExtraction: false, // Value extraction from elements
20
- };
21
-
22
- const debug = (category, ...args) => {
23
- if (DEBUG[category]) {
24
- console.debug(`[selection:${category}]`, ...args);
25
- }
26
- };
27
-
28
- export const SelectionContext = createContext();
29
-
30
- export const useSelectionController = ({
31
- elementRef,
32
- layout,
33
- value,
34
- onChange,
35
- multiple,
36
- selectAllName,
37
- }) => {
38
- if (!elementRef) {
39
- throw new Error("useSelectionController: elementRef is required");
40
- }
41
-
42
- onChange = useStableCallback(onChange);
43
-
44
- const currentValueRef = useRef(value);
45
- currentValueRef.current = value;
46
-
47
- const lastInternalValueRef = useRef(null);
48
-
49
- const selectionController = useMemo(() => {
50
- const innerOnChange = (newValue, ...args) => {
51
- lastInternalValueRef.current = newValue;
52
- onChange?.(newValue, ...args);
53
- };
54
-
55
- const getCurrentValue = () => currentValueRef.current;
56
-
57
- if (layout === "grid") {
58
- return createGridSelectionController({
59
- getCurrentValue,
60
- onChange: innerOnChange,
61
- enabled: Boolean(onChange),
62
- multiple,
63
- selectAllName,
64
- });
65
- }
66
- return createLinearSelectionController({
67
- getCurrentValue,
68
- onChange: innerOnChange,
69
- layout,
70
- elementRef,
71
- multiple,
72
- enabled: Boolean(onChange),
73
- selectAllName,
74
- });
75
- }, [layout, multiple, elementRef]);
76
-
77
- useEffect(() => {
78
- selectionController.element = elementRef.current;
79
- }, [selectionController]);
80
-
81
- useLayoutEffect(() => {
82
- selectionController.enabled = Boolean(onChange);
83
- }, [selectionController, onChange]);
84
-
85
- // Smart sync: only update selection when value changes externally
86
- useEffect(() => {
87
- // Check if this is an external change (not from our internal onChange)
88
- const isExternalChange = !compareTwoJsValues(
89
- value,
90
- lastInternalValueRef.current,
91
- );
92
- if (isExternalChange) {
93
- selectionController.update(value);
94
- }
95
- }, [value, selectionController]);
96
-
97
- return selectionController;
98
- };
99
- // Base Selection - shared functionality between grid and linear
100
- const createBaseSelectionController = ({
101
- getCurrentValue,
102
- registry,
103
- onChange,
104
- type,
105
- enabled,
106
- multiple,
107
- selectAllName,
108
- navigationMethods: {
109
- getElementRange,
110
- getElementAfter,
111
- getElementBefore,
112
- getElementBelow,
113
- getElementAbove,
114
- },
115
- }) => {
116
- const [publishChange, subscribeChange] = createPubSub();
117
-
118
- const getElementByValue = (valueToFind) => {
119
- for (const element of registry) {
120
- if (getElementValue(element) === valueToFind) {
121
- return element;
122
- }
123
- }
124
- return null;
125
- };
126
-
127
- const update = (newValue, event) => {
128
- if (!baseSelection.enabled) {
129
- console.warn("cannot change selection: no onChange provided");
130
- return;
131
- }
132
-
133
- const currentValue = getCurrentValue();
134
- if (compareTwoJsValues(newValue, currentValue)) {
135
- return;
136
- }
137
-
138
- const allValues = [];
139
- for (const element of registry) {
140
- const value = getElementValue(element);
141
- allValues.push(value);
142
- }
143
-
144
- const oldSelectedSet = new Set(currentValue);
145
- const newSelectedSet = new Set(newValue);
146
- const willBeUnselectedSet = new Set();
147
- for (const item of oldSelectedSet) {
148
- if (!newSelectedSet.has(item)) {
149
- willBeUnselectedSet.add(item);
150
- }
151
- }
152
- const selectionSet = new Set(newValue);
153
- for (const newSelected of newSelectedSet) {
154
- const element = getElementByValue(newSelected);
155
- if (element._selectionImpact) {
156
- const impactedValues = element._selectionImpact(allValues);
157
- for (const impactedValue of impactedValues) {
158
- selectionSet.add(impactedValue);
159
- }
160
- }
161
- }
162
- for (const willBeUnselected of willBeUnselectedSet) {
163
- const element = getElementByValue(willBeUnselected);
164
- if (element._selectionImpact) {
165
- const impactedValues = element._selectionImpact(allValues);
166
- for (const impactedValue of impactedValues) {
167
- if (selectionSet.has(impactedValue)) {
168
- // want to be selected -> keep it
169
- // - might be explicit : initially part of newValue/selectionSet)
170
- // - or implicit: added to selectionSet by selectionImpact
171
- continue;
172
- }
173
- selectionSet.delete(impactedValue);
174
- }
175
- }
176
- }
177
-
178
- const finalValue = Array.from(selectionSet);
179
- debug(
180
- "selection",
181
- `${type} setSelection: calling onChange with:`,
182
- finalValue,
183
- );
184
- onChange(finalValue, event);
185
- publishChange(finalValue, event);
186
- };
187
- let anchorElement = null;
188
- let activeElement = null;
189
-
190
- const registerElement = (element, options = {}) => {
191
- const elementValue = getElementValue(element);
192
- debug(
193
- "registration",
194
- `${type} registerElement:`,
195
- element,
196
- "value:",
197
- elementValue,
198
- "registry size before:",
199
- registry.size,
200
- );
201
- registry.add(element);
202
- // Store the selectionImpact callback if provided
203
- if (options.selectionImpact) {
204
- element._selectionImpact = options.selectionImpact;
205
- }
206
- debug(
207
- "registration",
208
- `${type} registerElement: registry size after:`,
209
- registry.size,
210
- );
211
- };
212
- const unregisterElement = (element) => {
213
- const elementValue = getElementValue(element);
214
- debug(
215
- "registration",
216
- `${type} unregisterElement:`,
217
- element,
218
- "value:",
219
- elementValue,
220
- "registry size before:",
221
- registry.size,
222
- );
223
- registry.delete(element);
224
- debug(
225
- "registration",
226
- `${type} unregisterElement: registry size after:`,
227
- registry.size,
228
- );
229
- };
230
- const setActiveElement = (element) => {
231
- activeElement = element;
232
- };
233
- const setAnchorElement = (element) => {
234
- const elementValue = getElementValue(element);
235
- debug(
236
- "selection",
237
- `${type} setAnchorElement:`,
238
- element,
239
- "value:",
240
- elementValue,
241
- );
242
- anchorElement = element;
243
- };
244
- const isElementSelected = (element) => {
245
- const elementValue = getElementValue(element);
246
- const isSelected = baseSelection.value.includes(elementValue);
247
- return isSelected;
248
- };
249
- const isValueSelected = (value) => {
250
- const isSelected = baseSelection.value.includes(value);
251
- return isSelected;
252
- };
253
- // Selection manipulation methods
254
- const setSelection = (newSelection, event = null) => {
255
- debug(
256
- "selection",
257
- `${type} setSelection called with:`,
258
- newSelection,
259
- "current selection:",
260
- baseSelection.value,
261
- );
262
- if (
263
- newSelection.length === baseSelection.value.length &&
264
- newSelection.every((value, index) => value === baseSelection.value[index])
265
- ) {
266
- debug("selection", `${type} setSelection: no change, returning early`);
267
- return;
268
- }
269
- update(newSelection, event);
270
- };
271
- const addToSelection = (arrayOfValuesToAdd, event = null) => {
272
- debug(
273
- "selection",
274
- `${type} addToSelection called with:`,
275
- arrayOfValuesToAdd,
276
- "current selection:",
277
- baseSelection.value,
278
- );
279
- const selectionWithValues = [...baseSelection.value];
280
- let modified = false;
281
-
282
- for (const valueToAdd of arrayOfValuesToAdd) {
283
- if (!selectionWithValues.includes(valueToAdd)) {
284
- modified = true;
285
- selectionWithValues.push(valueToAdd);
286
- debug("selection", `${type} addToSelection: adding value:`, valueToAdd);
287
- }
288
- }
289
-
290
- if (modified) {
291
- update(selectionWithValues, event);
292
- } else {
293
- debug("selection", `${type} addToSelection: no changes made`);
294
- }
295
- };
296
- const removeFromSelection = (arrayOfValuesToRemove, event = null) => {
297
- let modified = false;
298
- const selectionWithoutValues = [];
299
-
300
- for (const elementValue of baseSelection.value) {
301
- if (arrayOfValuesToRemove.includes(elementValue)) {
302
- modified = true;
303
- } else {
304
- selectionWithoutValues.push(elementValue);
305
- }
306
- }
307
-
308
- if (modified) {
309
- update(selectionWithoutValues, event);
310
- }
311
- };
312
- const toggleElement = (element, event = null) => {
313
- const elementValue = getElementValue(element);
314
- if (baseSelection.value.includes(elementValue)) {
315
- baseSelection.removeFromSelection([elementValue], event);
316
- } else {
317
- baseSelection.addToSelection([elementValue], event);
318
- }
319
- };
320
- const selectFromAnchorTo = (element, event = null) => {
321
- if (anchorElement) {
322
- const currentAnchor = anchorElement; // Preserve the current anchor
323
- const range = getElementRange(anchorElement, element);
324
- baseSelection.setSelection(range, event);
325
- // Restore the original anchor (setSelection changes it to the last element)
326
- anchorElement = currentAnchor;
327
- } else {
328
- baseSelection.setSelection([getElementValue(element)], event);
329
- }
330
- };
331
- const selectAll = (event) => {
332
- const allValues = [];
333
- for (const element of registry) {
334
- if (selectAllName && getElementSelectionName(element) !== selectAllName) {
335
- continue;
336
- }
337
- const value = getElementValue(element);
338
- allValues.push(value);
339
- }
340
- debug(
341
- "interaction",
342
- "Select All - setting selection to all values:",
343
- allValues,
344
- );
345
- baseSelection.setSelection(allValues, event);
346
- };
347
-
348
- const baseSelection = {
349
- type,
350
- multiple,
351
- enabled,
352
- get value() {
353
- return getCurrentValue();
354
- },
355
- registry,
356
- get anchorElement() {
357
- return anchorElement;
358
- },
359
- get activeElement() {
360
- return activeElement;
361
- },
362
- channels: {
363
- change: {
364
- add: subscribeChange,
365
- },
366
- },
367
- update,
368
-
369
- registerElement,
370
- unregisterElement,
371
- setAnchorElement,
372
- setActiveElement,
373
- isElementSelected,
374
- isValueSelected,
375
- setSelection,
376
- addToSelection,
377
- removeFromSelection,
378
- toggleElement,
379
- selectFromAnchorTo,
380
- selectAll,
381
-
382
- // Navigation methods (will be overridden by specific implementations)
383
- getElementRange,
384
- getElementAfter,
385
- getElementBefore,
386
- getElementBelow,
387
- getElementAbove,
388
- };
389
-
390
- return baseSelection;
391
- };
392
- // Grid Selection Provider - for 2D layouts like tables
393
- const createGridSelectionController = ({ ...options }) => {
394
- const registry = new Set();
395
- const navigationMethods = {
396
- getElementRange: (fromElement, toElement) => {
397
- const fromPos = getElementPosition(fromElement);
398
- const toPos = getElementPosition(toElement);
399
-
400
- if (!fromPos || !toPos) {
401
- return [];
402
- }
403
-
404
- // Check selection types to ensure we only select compatible elements
405
- const fromSelectionName = getElementSelectionName(fromElement);
406
- const toSelectionName = getElementSelectionName(toElement);
407
-
408
- // Calculate rectangular selection area
409
- const { x: fromX, y: fromY } = fromPos;
410
- const { x: toX, y: toY } = toPos;
411
- const minX = Math.min(fromX, toX);
412
- const maxX = Math.max(fromX, toX);
413
- const minY = Math.min(fromY, toY);
414
- const maxY = Math.max(fromY, toY);
415
-
416
- // Find all registered elements within the rectangular area
417
- const valuesInRange = [];
418
- for (const element of registry) {
419
- const pos = getElementPosition(element);
420
- if (
421
- pos &&
422
- pos.x >= minX &&
423
- pos.x <= maxX &&
424
- pos.y >= minY &&
425
- pos.y <= maxY
426
- ) {
427
- const elementSelectionName = getElementSelectionName(element);
428
- // Only include elements with matching selection type
429
- if (
430
- elementSelectionName === fromSelectionName &&
431
- elementSelectionName === toSelectionName
432
- ) {
433
- valuesInRange.push(getElementValue(element));
434
- }
435
- }
436
- }
437
-
438
- return valuesInRange;
439
- },
440
-
441
- getElementAfter: (element) => {
442
- const currentPos = getElementPosition(element);
443
- if (!currentPos) {
444
- return null;
445
- }
446
-
447
- const { x, y } = currentPos;
448
- const nextX = x + 1;
449
- const currentSelectionName = getElementSelectionName(element);
450
- let fallbackElement = null;
451
- // Single loop: prioritize same selection name
452
- for (const candidateElement of registry) {
453
- const pos = getElementPosition(candidateElement);
454
- const candidateSelectionName =
455
- getElementSelectionName(candidateElement);
456
-
457
- if (pos && pos.x === nextX && pos.y === y) {
458
- if (candidateSelectionName === currentSelectionName) {
459
- return candidateElement;
460
- }
461
- if (!fallbackElement) {
462
- fallbackElement = candidateElement;
463
- }
464
- }
465
- }
466
- return fallbackElement;
467
- },
468
-
469
- getElementBefore: (element) => {
470
- const currentPos = getElementPosition(element);
471
- if (!currentPos) {
472
- return null;
473
- }
474
-
475
- const { x, y } = currentPos;
476
- const prevX = x - 1;
477
- const currentSelectionName = getElementSelectionName(element);
478
- let fallbackElement = null;
479
- // Single loop: prioritize same selection name
480
- for (const candidateElement of registry) {
481
- const pos = getElementPosition(candidateElement);
482
- const candidateSelectionName =
483
- getElementSelectionName(candidateElement);
484
-
485
- if (pos && pos.x === prevX && pos.y === y) {
486
- if (candidateSelectionName === currentSelectionName) {
487
- return candidateElement;
488
- }
489
- if (!fallbackElement) {
490
- fallbackElement = candidateElement;
491
- }
492
- }
493
- }
494
- return fallbackElement;
495
- },
496
-
497
- getElementBelow: (element) => {
498
- const currentPos = getElementPosition(element);
499
- if (!currentPos) {
500
- return null;
501
- }
502
-
503
- const { x, y } = currentPos;
504
- const nextY = y + 1;
505
- const currentSelectionName = getElementSelectionName(element);
506
-
507
- let fallbackElement = null;
508
- // Single loop: prioritize same selection name
509
- for (const candidateElement of registry) {
510
- const pos = getElementPosition(candidateElement);
511
- const candidateSelectionName =
512
- getElementSelectionName(candidateElement);
513
-
514
- if (pos && pos.x === x && pos.y === nextY) {
515
- if (candidateSelectionName === currentSelectionName) {
516
- return candidateElement;
517
- }
518
- if (!fallbackElement) {
519
- fallbackElement = candidateElement;
520
- }
521
- }
522
- }
523
- return fallbackElement;
524
- },
525
-
526
- getElementAbove: (element) => {
527
- const currentPos = getElementPosition(element);
528
- if (!currentPos) {
529
- return null;
530
- }
531
-
532
- const { x, y } = currentPos;
533
- const prevY = y - 1;
534
- const currentSelectionName = getElementSelectionName(element);
535
- let fallbackElement = null;
536
- // Single loop: prioritize same selection name
537
- for (const candidateElement of registry) {
538
- const pos = getElementPosition(candidateElement);
539
- const candidateSelectionName =
540
- getElementSelectionName(candidateElement);
541
-
542
- if (pos && pos.x === x && pos.y === prevY) {
543
- if (candidateSelectionName === currentSelectionName) {
544
- return candidateElement;
545
- }
546
- if (!fallbackElement) {
547
- fallbackElement = candidateElement;
548
- }
549
- }
550
- }
551
- return fallbackElement;
552
- },
553
- };
554
- const gridSelectionController = createBaseSelectionController({
555
- ...options,
556
- registry,
557
- type: "grid",
558
- navigationMethods,
559
- });
560
- gridSelectionController.axis = { x: true, y: true };
561
-
562
- return gridSelectionController;
563
- };
564
- // Linear Selection Provider - for 1D layouts like lists
565
- const createLinearSelectionController = ({
566
- layout = "vertical", // "horizontal" or "vertical"
567
- elementRef, // Root element to scope DOM traversal
568
- ...options
569
- }) => {
570
- if (!["horizontal", "vertical"].includes(layout)) {
571
- throw new Error(
572
- `useLinearSelection: Invalid axis "${layout}". Must be "horizontal" or "vertical".`,
573
- );
574
- }
575
-
576
- const registry = new Set();
577
-
578
- // Define navigation methods that need access to registry
579
- const navigationMethods = {
580
- getElementRange: (fromElement, toElement) => {
581
- if (!registry.has(fromElement) || !registry.has(toElement)) {
582
- return [];
583
- }
584
-
585
- // Check selection types to ensure we only select compatible elements
586
- const fromSelectionName = getElementSelectionName(fromElement);
587
- const toSelectionName = getElementSelectionName(toElement);
588
-
589
- // Use compareDocumentPosition to determine order
590
- const comparison = fromElement.compareDocumentPosition(toElement);
591
- let startElement;
592
- let endElement;
593
-
594
- if (comparison & Node.DOCUMENT_POSITION_FOLLOWING) {
595
- // toElement comes after fromElement
596
- startElement = fromElement;
597
- endElement = toElement;
598
- } else if (comparison & Node.DOCUMENT_POSITION_PRECEDING) {
599
- // toElement comes before fromElement
600
- startElement = toElement;
601
- endElement = fromElement;
602
- } else {
603
- // Same element
604
- return [getElementValue(fromElement)];
605
- }
606
-
607
- const valuesInRange = [];
608
-
609
- // Check all registered elements to see if they're in the range
610
- for (const element of registry) {
611
- // Check if element is between startElement and endElement
612
- const afterStart =
613
- startElement.compareDocumentPosition(element) &
614
- Node.DOCUMENT_POSITION_FOLLOWING;
615
- const beforeEnd =
616
- element.compareDocumentPosition(endElement) &
617
- Node.DOCUMENT_POSITION_FOLLOWING;
618
-
619
- if (
620
- element === startElement ||
621
- element === endElement ||
622
- (afterStart && beforeEnd)
623
- ) {
624
- const elementSelectionName = getElementSelectionName(element);
625
-
626
- // Only include elements with matching selection type
627
- if (
628
- elementSelectionName === fromSelectionName &&
629
- elementSelectionName === toSelectionName
630
- ) {
631
- valuesInRange.push(getElementValue(element));
632
- }
633
- }
634
- }
635
-
636
- return valuesInRange;
637
- },
638
- getElementAfter: (element) => {
639
- if (!registry.has(element)) {
640
- return null;
641
- }
642
-
643
- const currentSelectionName = getElementSelectionName(element);
644
- let fallbackElement = null;
645
-
646
- const sameTypeElement = findAfter(
647
- element,
648
- (candidate) => {
649
- if (!registry.has(candidate)) {
650
- return false;
651
- }
652
- const candidateSelectionName = getElementSelectionName(candidate);
653
- // If same selection name, this is our preferred result
654
- if (candidateSelectionName === currentSelectionName) {
655
- return true;
656
- }
657
- // Different selection name - store as fallback but keep searching
658
- if (!fallbackElement) {
659
- fallbackElement = candidate;
660
- }
661
- return false;
662
- },
663
- {
664
- root: elementRef.current || document.body,
665
- },
666
- );
667
-
668
- return sameTypeElement || fallbackElement;
669
- },
670
- getElementBefore: (element) => {
671
- if (!registry.has(element)) {
672
- return null;
673
- }
674
-
675
- const currentSelectionName = getElementSelectionName(element);
676
-
677
- let fallbackElement = null;
678
- const sameTypeElement = findBefore(
679
- element,
680
- (candidate) => {
681
- if (!registry.has(candidate)) {
682
- return false;
683
- }
684
- const candidateSelectionName = getElementSelectionName(candidate);
685
- // If same selection name, this is our preferred result
686
- if (candidateSelectionName === currentSelectionName) {
687
- return true;
688
- }
689
- // Different selection name - store as fallback but keep searching
690
- if (!fallbackElement) {
691
- fallbackElement = candidate;
692
- }
693
- return false;
694
- },
695
- {
696
- root: elementRef.current || document.body,
697
- },
698
- );
699
-
700
- return sameTypeElement || fallbackElement;
701
- },
702
- // Add axis-dependent methods
703
- getElementBelow: (element) => {
704
- if (layout === "vertical") {
705
- return navigationMethods.getElementAfter(element);
706
- }
707
- return null;
708
- },
709
- getElementAbove: (element) => {
710
- if (layout === "vertical") {
711
- return navigationMethods.getElementBefore(element);
712
- }
713
- return null;
714
- },
715
- };
716
-
717
- // Create base selection with navigation methods
718
- const linearSelectionController = createBaseSelectionController({
719
- ...options,
720
- registry,
721
- type: "linear",
722
- navigationMethods,
723
- });
724
- linearSelectionController.axis = {
725
- x: layout === "horizontal",
726
- y: layout === "vertical",
727
- };
728
-
729
- return linearSelectionController;
730
- };
731
- // Helper function to extract value from an element
732
- const getElementValue = (element) => {
733
- let value;
734
- if (element.value !== undefined) {
735
- value = element.value;
736
- } else if (element.hasAttribute("data-value")) {
737
- value = element.getAttribute("data-value");
738
- } else {
739
- value = undefined;
740
- }
741
- debug("valueExtraction", "getElementValue:", element, "->", value);
742
- return value;
743
- };
744
- const getElementSelectionName = (element) => {
745
- return element.getAttribute("data-selection-name");
746
- };
747
-
748
- // Helper functions to find end elements for jump to end functionality
749
- const getJumpToEndElement = (selection, element, keydownEvent) => {
750
- if (selection.type === "grid") {
751
- return getJumpToEndElementGrid(selection, element, keydownEvent);
752
- } else if (selection.type === "linear") {
753
- return getJumpToEndElementLinear(selection, element, keydownEvent);
754
- }
755
- return null;
756
- };
757
- const getJumpToEndElementGrid = (selection, element, keydownEvent) => {
758
- const currentPos = getElementPosition(element);
759
- if (!currentPos) {
760
- return null;
761
- }
762
- const { key } = keydownEvent;
763
-
764
- const { x, y } = currentPos;
765
- const currentSelectionName = getElementSelectionName(element);
766
-
767
- if (key === "ArrowRight") {
768
- // Jump to last element in current row with matching selection name
769
- let lastInRow = null;
770
- let fallbackElement = null;
771
- let maxX = -1;
772
- let fallbackMaxX = -1;
773
-
774
- for (const candidateElement of selection.registry) {
775
- const candidateSelectionName = getElementSelectionName(candidateElement);
776
- const pos = getElementPosition(candidateElement);
777
-
778
- if (pos && pos.y === y) {
779
- if (candidateSelectionName === currentSelectionName && pos.x > maxX) {
780
- maxX = pos.x;
781
- lastInRow = candidateElement;
782
- } else if (
783
- candidateSelectionName !== currentSelectionName &&
784
- pos.x > fallbackMaxX
785
- ) {
786
- fallbackMaxX = pos.x;
787
- fallbackElement = candidateElement;
788
- }
789
- }
790
- }
791
- return lastInRow || fallbackElement;
792
- }
793
-
794
- if (key === "ArrowLeft") {
795
- // Jump to first element in current row with matching selection name
796
- let firstInRow = null;
797
- let fallbackElement = null;
798
- let minX = Infinity;
799
- let fallbackMinX = Infinity;
800
-
801
- for (const candidateElement of selection.registry) {
802
- const candidateSelectionName = getElementSelectionName(candidateElement);
803
- const pos = getElementPosition(candidateElement);
804
-
805
- if (pos && pos.y === y) {
806
- if (candidateSelectionName === currentSelectionName && pos.x < minX) {
807
- minX = pos.x;
808
- firstInRow = candidateElement;
809
- } else if (
810
- candidateSelectionName !== currentSelectionName &&
811
- pos.x < fallbackMinX
812
- ) {
813
- fallbackMinX = pos.x;
814
- fallbackElement = candidateElement;
815
- }
816
- }
817
- }
818
- return firstInRow || fallbackElement;
819
- }
820
-
821
- if (key === "ArrowDown") {
822
- // Jump to last element in current column with matching selection name
823
- let lastInColumn = null;
824
- let fallbackElement = null;
825
- let maxY = -1;
826
- let fallbackMaxY = -1;
827
-
828
- for (const candidateElement of selection.registry) {
829
- const candidateSelectionName = getElementSelectionName(candidateElement);
830
- const pos = getElementPosition(candidateElement);
831
-
832
- if (pos && pos.x === x) {
833
- if (candidateSelectionName === currentSelectionName && pos.y > maxY) {
834
- maxY = pos.y;
835
- lastInColumn = candidateElement;
836
- } else if (
837
- candidateSelectionName !== currentSelectionName &&
838
- pos.y > fallbackMaxY
839
- ) {
840
- fallbackMaxY = pos.y;
841
- fallbackElement = candidateElement;
842
- }
843
- }
844
- }
845
- return lastInColumn || fallbackElement;
846
- }
847
-
848
- if (key === "ArrowUp") {
849
- // Jump to first element in current column with matching selection name
850
- let firstInColumn = null;
851
- let fallbackElement = null;
852
- let minY = Infinity;
853
- let fallbackMinY = Infinity;
854
-
855
- for (const candidateElement of selection.registry) {
856
- const candidateSelectionName = getElementSelectionName(candidateElement);
857
- const pos = getElementPosition(candidateElement);
858
-
859
- if (pos && pos.x === x) {
860
- if (candidateSelectionName === currentSelectionName && pos.y < minY) {
861
- minY = pos.y;
862
- firstInColumn = candidateElement;
863
- } else if (
864
- candidateSelectionName !== currentSelectionName &&
865
- pos.y < fallbackMinY
866
- ) {
867
- fallbackMinY = pos.y;
868
- fallbackElement = candidateElement;
869
- }
870
- }
871
- }
872
- return firstInColumn || fallbackElement;
873
- }
874
-
875
- return null;
876
- };
877
- const getJumpToEndElementLinear = (selection, element, direction) => {
878
- const currentSelectionName = getElementSelectionName(element);
879
-
880
- if (direction === "ArrowDown" || direction === "ArrowRight") {
881
- // Jump to last element in the registry with matching selection name
882
- let lastElement = null;
883
- let fallbackElement = null;
884
-
885
- for (const candidateElement of selection.registry) {
886
- const candidateSelectionName = getElementSelectionName(candidateElement);
887
-
888
- if (candidateSelectionName === currentSelectionName) {
889
- if (
890
- !lastElement ||
891
- candidateElement.compareDocumentPosition(lastElement) &
892
- Node.DOCUMENT_POSITION_FOLLOWING
893
- ) {
894
- lastElement = candidateElement;
895
- }
896
- } else if (!fallbackElement) {
897
- if (
898
- !fallbackElement ||
899
- candidateElement.compareDocumentPosition(fallbackElement) &
900
- Node.DOCUMENT_POSITION_FOLLOWING
901
- ) {
902
- fallbackElement = candidateElement;
903
- }
904
- }
905
- }
906
- return lastElement || fallbackElement;
907
- }
908
-
909
- if (direction === "ArrowUp" || direction === "ArrowLeft") {
910
- // Jump to first element in the registry with matching selection name
911
- let firstElement = null;
912
- let fallbackElement = null;
913
-
914
- for (const candidateElement of selection.registry) {
915
- const candidateSelectionName = getElementSelectionName(candidateElement);
916
-
917
- if (candidateSelectionName === currentSelectionName) {
918
- if (
919
- !firstElement ||
920
- firstElement.compareDocumentPosition(candidateElement) &
921
- Node.DOCUMENT_POSITION_FOLLOWING
922
- ) {
923
- firstElement = candidateElement;
924
- }
925
- } else if (!fallbackElement) {
926
- if (
927
- !fallbackElement ||
928
- fallbackElement.compareDocumentPosition(candidateElement) &
929
- Node.DOCUMENT_POSITION_FOLLOWING
930
- ) {
931
- fallbackElement = candidateElement;
932
- }
933
- }
934
- }
935
- return firstElement || fallbackElement;
936
- }
937
-
938
- return null;
939
- };
940
-
941
- // Helper function for grid positioning (moved here from createGridSelection)
942
- const getElementPosition = (element) => {
943
- // Get position by checking element's position in table structure
944
- const cell = element.closest("td, th");
945
- if (!cell) return null;
946
-
947
- const row = cell.closest("tr");
948
- if (!row) return null;
949
-
950
- const table = row.closest("table");
951
- if (!table) return null;
952
-
953
- const rows = Array.from(table.rows);
954
- const cells = Array.from(row.cells);
955
-
956
- return {
957
- x: cells.indexOf(cell),
958
- y: rows.indexOf(row),
959
- };
960
- };
961
-
962
- export const useSelectableElement = (
963
- elementRef,
964
- { selection, selectionController, selectionImpact },
965
- ) => {
966
- if (!selectionController) {
967
- throw new Error("useSelectableElement needs a selectionController");
968
- }
969
-
970
- useLayoutEffect(() => {
971
- const element = elementRef.current;
972
- if (!element) {
973
- return null;
974
- }
975
- const value = getElementValue(element);
976
- const selectionName = getElementSelectionName(element);
977
- debug(
978
- "registration",
979
- "useSelectableElement: registering element:",
980
- element,
981
- "value:",
982
- value,
983
- "selectionName:",
984
- selectionName,
985
- );
986
-
987
- selectionController.registerElement(element, { selectionImpact });
988
- element.setAttribute("data-selectable", "");
989
- return () => {
990
- debug(
991
- "registration",
992
- "useSelectableElement: unregistering element:",
993
- element,
994
- "value:",
995
- value,
996
- );
997
- selectionController.unregisterElement(element);
998
- element.removeAttribute("data-selectable");
999
- };
1000
- }, [selectionController, selectionImpact]);
1001
-
1002
- const [selected, setSelected] = useState(false);
1003
- debug("selection", "useSelectableElement: initial selected state:", selected);
1004
- // Update selected state when selection value changes
1005
- useLayoutEffect(() => {
1006
- const element = elementRef.current;
1007
- if (!element) {
1008
- debug(
1009
- "selection",
1010
- "useSelectableElement: no element, setting selected to false",
1011
- );
1012
- setSelected(false);
1013
- return;
1014
- }
1015
- // Use selection values directly for better performance
1016
- const elementValue = getElementValue(element);
1017
- const isSelected = selection.includes(elementValue);
1018
- debug(
1019
- "selection",
1020
- "useSelectableElement: updating selected state",
1021
- element,
1022
- "isSelected:",
1023
- isSelected,
1024
- );
1025
- setSelected(isSelected);
1026
- }, [selection]);
1027
-
1028
- // Add event listeners directly to the element
1029
- useLayoutEffect(() => {
1030
- const element = elementRef.current;
1031
- if (!element) {
1032
- return null;
1033
- }
1034
-
1035
- let isDragging = false;
1036
- let dragStartElement = null;
1037
- let cleanup = () => {};
1038
-
1039
- const handleMouseDown = (e) => {
1040
- if (!selectionController.enabled) {
1041
- return;
1042
- }
1043
- if (e.button !== 0) {
1044
- // Only handle left mouse button
1045
- return;
1046
- }
1047
-
1048
- // if (e.defaultPrevented) {
1049
- // // If the event was prevented by another handler, do not interfere
1050
- // debug("interaction", "mousedown: event already prevented, skipping");
1051
- // return;
1052
- // }
1053
- const isMultiSelect = e.metaKey || e.ctrlKey;
1054
- const isShiftSelect = e.shiftKey;
1055
- const isSingleSelect = !isMultiSelect && !isShiftSelect;
1056
- const value = getElementValue(element);
1057
-
1058
- debug("interaction", "mousedown:", {
1059
- element,
1060
- value,
1061
- isMultiSelect,
1062
- isShiftSelect,
1063
- isSingleSelect,
1064
- currentSelection: selectionController.value,
1065
- });
1066
-
1067
- // Handle immediate selection based on modifier keys
1068
- if (isSingleSelect) {
1069
- // Single select - replace entire selection with just this item
1070
- debug(
1071
- "interaction",
1072
- "mousedown: single select, setting selection to:",
1073
- [value],
1074
- );
1075
- selectionController.setSelection([value], e);
1076
- } else if (isMultiSelect && !isShiftSelect) {
1077
- // Multi select without shift - toggle element
1078
- debug("interaction", "mousedown: multi select, toggling element");
1079
- selectionController.toggleElement(element, e);
1080
- } else if (isShiftSelect) {
1081
- e.preventDefault(); // Prevent navigation
1082
- debug(
1083
- "interaction",
1084
- "mousedown: shift select, selecting from anchor to element",
1085
- );
1086
- selectionController.selectFromAnchorTo(element, e);
1087
- }
1088
-
1089
- if (!selectionController.dragToSelect) {
1090
- return;
1091
- }
1092
-
1093
- // Set up for potential drag selection (now works with all modifier combinations)
1094
- dragStartElement = element;
1095
- isDragging = false; // Will be set to true if mouse moves beyond threshold
1096
-
1097
- // Store initial mouse position for drag threshold
1098
- const startX = e.clientX;
1099
- const startY = e.clientY;
1100
- const dragThreshold = 5; // pixels
1101
-
1102
- const handleMouseMove = (e) => {
1103
- if (!dragStartElement) {
1104
- return;
1105
- }
1106
-
1107
- if (!isDragging) {
1108
- // Check if we've exceeded the drag threshold
1109
- const deltaX = Math.abs(e.clientX - startX);
1110
- const deltaY = Math.abs(e.clientY - startY);
1111
- const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
1112
-
1113
- if (distance < dragThreshold) {
1114
- return; // Don't start dragging yet
1115
- }
1116
-
1117
- isDragging = true;
1118
- // mark it as drag-selecting
1119
- selectionController.element.setAttribute("data-drag-selecting", "");
1120
- }
1121
-
1122
- // Find the element under the current mouse position
1123
- const elementUnderMouse = document.elementFromPoint(
1124
- e.clientX,
1125
- e.clientY,
1126
- );
1127
- if (!elementUnderMouse) {
1128
- return;
1129
- }
1130
- // Find the closest selectable element (look for element with data-value or in registry)
1131
- let targetElement = elementUnderMouse;
1132
- while (true) {
1133
- if (selectionController.registry.has(targetElement)) {
1134
- break;
1135
- }
1136
- if (
1137
- targetElement.hasAttribute("data-value") ||
1138
- targetElement.hasAttribute("aria-selected")
1139
- ) {
1140
- break;
1141
- }
1142
- targetElement = targetElement.parentElement;
1143
- if (!targetElement) {
1144
- return;
1145
- }
1146
- }
1147
- if (!selectionController.registry.has(targetElement)) {
1148
- return;
1149
- }
1150
- // Check if we're mixing selection types (like row and cell selections)
1151
- const dragStartSelectionName =
1152
- getElementSelectionName(dragStartElement);
1153
- const targetSelectionName = getElementSelectionName(targetElement);
1154
- // Only allow drag between elements of the same selection type
1155
- if (dragStartSelectionName !== targetSelectionName) {
1156
- debug("interaction", "drag select: skipping mixed selection types", {
1157
- dragStartSelectionName,
1158
- targetSelectionName,
1159
- });
1160
- return;
1161
- }
1162
-
1163
- // Get the range from anchor to current target
1164
- const rangeValues = selectionController.getElementRange(
1165
- dragStartElement,
1166
- targetElement,
1167
- );
1168
-
1169
- // Handle different drag behaviors based on modifier keys
1170
- const isShiftSelect = e.shiftKey;
1171
- const isMultiSelect = e.metaKey || e.ctrlKey;
1172
-
1173
- if (isShiftSelect) {
1174
- // For shift drag, use selectFromAnchorTo behavior (replace selection with range from anchor)
1175
- debug(
1176
- "interaction",
1177
- "shift drag select: selecting from anchor to target",
1178
- rangeValues,
1179
- );
1180
- selectionController.selectFromAnchorTo(targetElement, e);
1181
- return;
1182
- }
1183
- if (isMultiSelect) {
1184
- // For multi-select drag, add to existing selection
1185
- debug(
1186
- "interaction",
1187
- "multi-select drag: adding range to selection",
1188
- rangeValues,
1189
- );
1190
- const currentSelection = [...selectionController.value];
1191
- const newSelection = [
1192
- ...new Set([...currentSelection, ...rangeValues]),
1193
- ];
1194
- selectionController.setSelection(newSelection, e);
1195
- return;
1196
- }
1197
- // For normal drag, replace selection
1198
- debug(
1199
- "interaction",
1200
- "drag select: setting selection to range",
1201
- rangeValues,
1202
- );
1203
- selectionController.setSelection(rangeValues, e);
1204
- };
1205
-
1206
- const handleMouseUp = () => {
1207
- document.removeEventListener("mousemove", handleMouseMove);
1208
- document.removeEventListener("mouseup", handleMouseUp);
1209
-
1210
- // Remove drag-selecting state from table
1211
- if (isDragging) {
1212
- selectionController.element.removeAttribute("data-drag-selecting");
1213
- }
1214
-
1215
- // Reset drag state
1216
- dragStartElement = null;
1217
- isDragging = false;
1218
- };
1219
-
1220
- document.addEventListener("mousemove", handleMouseMove);
1221
- document.addEventListener("mouseup", handleMouseUp);
1222
- cleanup = () => {
1223
- document.removeEventListener("mousemove", handleMouseMove);
1224
- document.removeEventListener("mouseup", handleMouseUp);
1225
- };
1226
- };
1227
-
1228
- element.addEventListener("mousedown", handleMouseDown);
1229
- return () => {
1230
- element.removeEventListener("mousedown", handleMouseDown);
1231
- cleanup();
1232
- };
1233
- }, [selectionController]);
1234
-
1235
- return {
1236
- selected,
1237
- };
1238
- };
1239
-
1240
- // Helper function to handle cross-type navigation
1241
- const handleCrossTypeNavigation = (
1242
- currentElement,
1243
- targetElement,
1244
- isMultiSelect,
1245
- ) => {
1246
- const currentSelectionName = getElementSelectionName(currentElement);
1247
- const targetSelectionName = getElementSelectionName(targetElement);
1248
-
1249
- // Check if we're switching between different selection types
1250
- if (currentSelectionName !== targetSelectionName) {
1251
- debug(
1252
- "navigation",
1253
- "Cross-type navigation detected:",
1254
- currentSelectionName,
1255
- "->",
1256
- targetSelectionName,
1257
- );
1258
-
1259
- // Return info about cross-type navigation for caller to handle
1260
- return {
1261
- isCrossType: true,
1262
- shouldClearPreviousSelection: !isMultiSelect,
1263
- };
1264
- }
1265
-
1266
- return {
1267
- isCrossType: false,
1268
- shouldClearPreviousSelection: false,
1269
- };
1270
- };
1271
-
1272
- export const createSelectionKeyboardShortcuts = (
1273
- selectionController,
1274
- { toggleEnabled, enabled, toggleKey = "space" } = {},
1275
- ) => {
1276
- const getSelectableElement = (keydownEvent) => {
1277
- return keydownEvent.target.closest("[data-selectable]");
1278
- };
1279
- const moveSelection = (keyboardEvent, getElementToSelect) => {
1280
- const selectableElement = getSelectableElement(keyboardEvent);
1281
- const elementToSelect = getElementToSelect(
1282
- selectableElement,
1283
- keyboardEvent,
1284
- );
1285
-
1286
- if (!elementToSelect) {
1287
- return false;
1288
- }
1289
-
1290
- const { key } = keyboardEvent;
1291
- const isMetaOrCtrlPressed = keyboardEvent.metaKey || keyboardEvent.ctrlKey;
1292
- const isShiftSelect = keyboardEvent.shiftKey;
1293
- const isMultiSelect = isMetaOrCtrlPressed && isShiftSelect; // Only add to selection when BOTH are pressed
1294
- const targetValue = getElementValue(elementToSelect);
1295
- const { isCrossType, shouldClearPreviousSelection } =
1296
- handleCrossTypeNavigation(
1297
- selectableElement,
1298
- elementToSelect,
1299
- isMultiSelect,
1300
- );
1301
-
1302
- if (isShiftSelect) {
1303
- debug(
1304
- "interaction",
1305
- `keydownToSelect: ${key} with Shift - selecting from anchor to target element`,
1306
- );
1307
- selectionController.setActiveElement(elementToSelect);
1308
- selectionController.selectFromAnchorTo(elementToSelect, keyboardEvent);
1309
- return true;
1310
- }
1311
- if (isMultiSelect && !isCrossType) {
1312
- debug(
1313
- "interaction",
1314
- `keydownToSelect: ${key} with multi-select - adding to selection`,
1315
- );
1316
- selectionController.addToSelection([targetValue], keyboardEvent);
1317
- return true;
1318
- }
1319
- // Handle cross-type navigation
1320
- if (shouldClearPreviousSelection) {
1321
- debug(
1322
- "interaction",
1323
- `keydownToSelect: ${key} - cross-type navigation, clearing and setting new selection`,
1324
- );
1325
- selectionController.setSelection([targetValue], keyboardEvent);
1326
- return true;
1327
- }
1328
- if (isCrossType && !shouldClearPreviousSelection) {
1329
- debug(
1330
- "interaction",
1331
- `keydownToSelect: ${key} - cross-type navigation with Cmd, adding to selection`,
1332
- );
1333
- selectionController.addToSelection([targetValue], keyboardEvent);
1334
- return true;
1335
- }
1336
- debug(
1337
- "interaction",
1338
- `keydownToSelect: ${key} - setting selection to target element`,
1339
- );
1340
- selectionController.setSelection([targetValue], keyboardEvent);
1341
- return true;
1342
- };
1343
-
1344
- if (enabled !== undefined && typeof enabled !== "function") {
1345
- const v = enabled;
1346
- enabled = () => v;
1347
- }
1348
-
1349
- return [
1350
- {
1351
- description: "Add element above to selection",
1352
- key: "command+shift+up",
1353
- enabled: () => {
1354
- if (!selectionController.enabled) {
1355
- return false;
1356
- }
1357
- if (!selectionController.axis.y) {
1358
- return false;
1359
- }
1360
- if (enabled && !enabled()) {
1361
- return false;
1362
- }
1363
- return true;
1364
- },
1365
- handler: (keyboardEvent) => {
1366
- return moveSelection(keyboardEvent, getJumpToEndElement);
1367
- },
1368
- },
1369
- {
1370
- description: "Select element above",
1371
- key: "up",
1372
- enabled: () => {
1373
- if (!selectionController.enabled) {
1374
- return false;
1375
- }
1376
- if (!selectionController.axis.y) {
1377
- return false;
1378
- }
1379
- if (enabled && !enabled()) {
1380
- return false;
1381
- }
1382
- return true;
1383
- },
1384
- handler: (keyboardEvent) => {
1385
- return moveSelection(keyboardEvent, (selectableElement) =>
1386
- selectionController.getElementAbove(selectableElement),
1387
- );
1388
- },
1389
- },
1390
- {
1391
- description: "Add element below to selection",
1392
- key: "command+shift+down",
1393
- enabled: () => {
1394
- if (!selectionController.enabled) {
1395
- return false;
1396
- }
1397
- if (!selectionController.axis.y) {
1398
- return false;
1399
- }
1400
- if (enabled && !enabled()) {
1401
- return false;
1402
- }
1403
- return true;
1404
- },
1405
- handler: (keyboardEvent) => {
1406
- return moveSelection(keyboardEvent, getJumpToEndElement);
1407
- },
1408
- },
1409
- {
1410
- description: "Select element below",
1411
- key: "down",
1412
- enabled: () => {
1413
- if (!selectionController.enabled) {
1414
- return false;
1415
- }
1416
- if (!selectionController.axis.y) {
1417
- return false;
1418
- }
1419
- if (enabled && !enabled()) {
1420
- return false;
1421
- }
1422
- return true;
1423
- },
1424
- handler: (keyboardEvent) => {
1425
- return moveSelection(keyboardEvent, (selectableElement) => {
1426
- return selectionController.getElementBelow(selectableElement);
1427
- });
1428
- },
1429
- },
1430
- {
1431
- description: "Add left element to selection",
1432
- key: "command+shift+left",
1433
- enabled: () => {
1434
- if (!selectionController.enabled) {
1435
- return false;
1436
- }
1437
- if (!selectionController.axis.x) {
1438
- return false;
1439
- }
1440
- if (enabled && !enabled()) {
1441
- return false;
1442
- }
1443
- return true;
1444
- },
1445
- handler: (keyboardEvent) => {
1446
- return moveSelection(keyboardEvent, getJumpToEndElement);
1447
- },
1448
- },
1449
- {
1450
- description: "Select left element",
1451
- key: "left",
1452
- enabled: () => {
1453
- if (!selectionController.enabled) {
1454
- return false;
1455
- }
1456
- if (!selectionController.axis.x) {
1457
- return false;
1458
- }
1459
- if (enabled && !enabled()) {
1460
- return false;
1461
- }
1462
- return true;
1463
- },
1464
- handler: (keyboardEvent) => {
1465
- return moveSelection(keyboardEvent, (selectableElement) => {
1466
- return selectionController.getElementBefore(selectableElement);
1467
- });
1468
- },
1469
- },
1470
- {
1471
- description: "Add right element to selection",
1472
- key: "command+shift+right",
1473
- enabled: () => {
1474
- if (!selectionController.enabled) {
1475
- return false;
1476
- }
1477
- if (!selectionController.axis.x) {
1478
- return false;
1479
- }
1480
- if (enabled && !enabled()) {
1481
- return false;
1482
- }
1483
- return true;
1484
- },
1485
- handler: (keyboardEvent) => {
1486
- return moveSelection(keyboardEvent, getJumpToEndElement);
1487
- },
1488
- },
1489
- {
1490
- description: "Select right element",
1491
- key: "right",
1492
- enabled: () => {
1493
- if (!selectionController.enabled) {
1494
- return false;
1495
- }
1496
- if (!selectionController.axis.x) {
1497
- return false;
1498
- }
1499
- if (enabled && !enabled()) {
1500
- return false;
1501
- }
1502
- return true;
1503
- },
1504
- handler: (keyboardEvent) => {
1505
- return moveSelection(keyboardEvent, (selectableElement) => {
1506
- return selectionController.getElementAfter(selectableElement);
1507
- });
1508
- },
1509
- },
1510
- {
1511
- description: "Set element as anchor for shift selections",
1512
- key: "shift",
1513
- enabled: () => {
1514
- if (!selectionController.enabled) {
1515
- return false;
1516
- }
1517
- if (enabled && !enabled()) {
1518
- return false;
1519
- }
1520
- return true;
1521
- },
1522
- handler: (keyboardEvent) => {
1523
- const element = getSelectableElement(keyboardEvent);
1524
- selectionController.setAnchorElement(element);
1525
- return true;
1526
- },
1527
- },
1528
- {
1529
- description: "Select all",
1530
- key: "command+a",
1531
- enabled: () => {
1532
- if (!selectionController.enabled) {
1533
- return false;
1534
- }
1535
- if (enabled && !enabled()) {
1536
- return false;
1537
- }
1538
- return true;
1539
- },
1540
- handler: (keyboardEvent) => {
1541
- selectionController.selectAll(keyboardEvent);
1542
- return true;
1543
- },
1544
- },
1545
- {
1546
- description: "Toggle element selected state",
1547
- enabled: (keyboardEvent) => {
1548
- if (!selectionController.enabled) {
1549
- return false;
1550
- }
1551
- if (!toggleEnabled) {
1552
- return false;
1553
- }
1554
- const elementWithToggleShortcut = keyboardEvent.target.closest(
1555
- "[data-selection-keyboard-toggle]",
1556
- );
1557
- if (!elementWithToggleShortcut) {
1558
- return false;
1559
- }
1560
- if (enabled && !enabled()) {
1561
- return false;
1562
- }
1563
- return true;
1564
- },
1565
- key: toggleKey,
1566
- handler: (keyboardEvent) => {
1567
- const element = getSelectableElement(keyboardEvent);
1568
- const elementValue = getElementValue(element);
1569
- const isCurrentlySelected =
1570
- selectionController.isElementSelected(element);
1571
- if (isCurrentlySelected) {
1572
- selectionController.removeFromSelection(
1573
- [elementValue],
1574
- keyboardEvent,
1575
- );
1576
- return true;
1577
- }
1578
- selectionController.addToSelection([elementValue], keyboardEvent);
1579
- return true;
1580
- },
1581
- },
1582
- ];
1583
- };