@jsenv/navi 0.10.2 → 0.11.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.
- package/dist/jsenv_navi.js +13838 -23291
- package/dist/jsenv_navi.js.map +1281 -0
- package/package.json +6 -8
- package/index.js +0 -122
- package/src/action_private_properties.js +0 -11
- package/src/action_proxy_test.html +0 -353
- package/src/action_run_states.js +0 -5
- package/src/actions.js +0 -1401
- package/src/browser_integration/browser_integration.js +0 -216
- package/src/browser_integration/document_back_and_forward.js +0 -17
- package/src/browser_integration/document_loading_signal.js +0 -100
- package/src/browser_integration/document_state_signal.js +0 -9
- package/src/browser_integration/document_url_signal.js +0 -9
- package/src/browser_integration/use_is_visited.js +0 -19
- package/src/browser_integration/via_history.js +0 -232
- package/src/browser_integration/via_navigation.js +0 -168
- package/src/components/action_execution/form_context.js +0 -5
- package/src/components/action_execution/render_actionable_component.jsx +0 -29
- package/src/components/action_execution/use_action.js +0 -99
- package/src/components/action_execution/use_execute_action.js +0 -193
- package/src/components/action_execution/use_run_on_mount.js +0 -9
- package/src/components/action_renderer.jsx +0 -125
- package/src/components/callout/callout.js +0 -990
- package/src/components/callout/callout_demo.html +0 -201
- package/src/components/callout/test_dynamic_positioning.html +0 -161
- package/src/components/callout/test_html_document_iframe.html +0 -182
- package/src/components/demos/0_button_demo.html +0 -707
- package/src/components/demos/10_column_reordering_debug.html +0 -277
- package/src/components/demos/11_table_selection_debug.html +0 -432
- package/src/components/demos/1_checkbox_demo.html +0 -754
- package/src/components/demos/2_input_textual_demo.html +0 -286
- package/src/components/demos/3_radio_demo.html +0 -874
- package/src/components/demos/4_select_demo.html +0 -100
- package/src/components/demos/5_list_scrollable_demo.html +0 -153
- package/src/components/demos/6_tablist_demo.html +0 -77
- package/src/components/demos/7_table_selection_demo.html +0 -176
- package/src/components/demos/8_table_fixed_headers_demo.html +0 -584
- package/src/components/demos/9_table_column_drag_demo.html +0 -325
- package/src/components/demos/action/0_button_demo.html +0 -204
- package/src/components/demos/action/10_shortcuts_demo.html +0 -189
- package/src/components/demos/action/11_nested_shortcuts_demo.xhtml +0 -401
- package/src/components/demos/action/1_input_text_demo.html +0 -876
- package/src/components/demos/action/2_form_multiple.html +0 -303
- package/src/components/demos/action/3_details_demo.html +0 -203
- package/src/components/demos/action/4_input_checkbox_demo.html +0 -731
- package/src/components/demos/action/5_input_checkbox_state_demo.html +0 -270
- package/src/components/demos/action/6_checkbox_list_demo.html +0 -341
- package/src/components/demos/action/7_radio_list_demo.html +0 -357
- package/src/components/demos/action/8_editable_demo.html +0 -431
- package/src/components/demos/action/9_link_demo.html +0 -194
- package/src/components/demos/demo.md +0 -0
- package/src/components/demos/route/basic/basic.html +0 -14
- package/src/components/demos/route/basic/basic_route_demo.jsx +0 -224
- package/src/components/demos/route/multi/multi.html +0 -14
- package/src/components/demos/route/multi/multi_route_demo.jsx +0 -277
- package/src/components/demos/ui_transition/0_action_renderer_ui_transition_demo.html +0 -695
- package/src/components/demos/ui_transition/1_nested_ui_transition_demo.html +0 -429
- package/src/components/demos/ui_transition/2_height_transition_test.html +0 -295
- package/src/components/details/details.jsx +0 -245
- package/src/components/details/summary_marker.jsx +0 -141
- package/src/components/edition/editable.jsx +0 -186
- package/src/components/error_boundary_context.js +0 -9
- package/src/components/field/README.md +0 -247
- package/src/components/field/button.jsx +0 -429
- package/src/components/field/checkbox_list.jsx +0 -185
- package/src/components/field/collect_form_element_values.js +0 -82
- package/src/components/field/custom_field.js +0 -106
- package/src/components/field/form.jsx +0 -209
- package/src/components/field/input.jsx +0 -16
- package/src/components/field/input_checkbox.jsx +0 -434
- package/src/components/field/input_radio.jsx +0 -432
- package/src/components/field/input_textual.jsx +0 -389
- package/src/components/field/label.jsx +0 -46
- package/src/components/field/radio_list.jsx +0 -183
- package/src/components/field/select.jsx +0 -256
- package/src/components/field/use_action_events.js +0 -132
- package/src/components/field/use_form_events.js +0 -59
- package/src/components/field/use_ui_state_controller.js +0 -506
- package/src/components/item_tracker/README.md +0 -461
- package/src/components/item_tracker/use_isolated_item_tracker.jsx +0 -209
- package/src/components/item_tracker/use_isolated_item_tracker_demo.html +0 -148
- package/src/components/item_tracker/use_isolated_item_tracker_demo.jsx +0 -460
- package/src/components/item_tracker/use_item_tracker.jsx +0 -143
- package/src/components/item_tracker/use_item_tracker_demo.html +0 -207
- package/src/components/item_tracker/use_item_tracker_demo.jsx +0 -216
- package/src/components/keyboard_shortcuts/active_keyboard_shortcuts.jsx +0 -87
- package/src/components/keyboard_shortcuts/aria_key_shortcuts.js +0 -61
- package/src/components/keyboard_shortcuts/keyboard_key_meta.js +0 -17
- package/src/components/keyboard_shortcuts/keyboard_shortcuts.js +0 -371
- package/src/components/keyboard_shortcuts/os.js +0 -9
- package/src/components/layout/demos/demo_flex.html +0 -638
- package/src/components/layout/demos/demo_layout_style_buttons.html +0 -351
- package/src/components/layout/demos/demo_layout_style_input.html +0 -226
- package/src/components/layout/demos/demo_layout_style_text.html +0 -514
- package/src/components/layout/flex.jsx +0 -109
- package/src/components/layout/layout_context.jsx +0 -3
- package/src/components/layout/spacing.jsx +0 -20
- package/src/components/layout/use_layout_style.js +0 -249
- package/src/components/link/link.jsx +0 -267
- package/src/components/link/link_with_icon.jsx +0 -52
- package/src/components/loader/loader_background.jsx +0 -372
- package/src/components/loader/loading_spinner.jsx +0 -68
- package/src/components/loader/network_speed.js +0 -83
- package/src/components/loader/rectangle_loading.jsx +0 -244
- package/src/components/props_composition/demos/demo_with_props_style.html +0 -81
- package/src/components/props_composition/with_props_class_name.js +0 -37
- package/src/components/props_composition/with_props_style.js +0 -26
- package/src/components/route.jsx +0 -19
- package/src/components/selection/selection.jsx +0 -1583
- package/src/components/svg/font_sized_svg.jsx +0 -59
- package/src/components/svg/icon_and_text.jsx +0 -21
- package/src/components/svg/svg_mask_overlay.jsx +0 -105
- package/src/components/table/drag/table_drag.jsx +0 -506
- package/src/components/table/resize/table_resize.jsx +0 -650
- package/src/components/table/resize/table_size.js +0 -43
- package/src/components/table/selection/table_selection.js +0 -106
- package/src/components/table/selection/table_selection.jsx +0 -203
- package/src/components/table/sticky/sticky_group.js +0 -354
- package/src/components/table/sticky/table_sticky.js +0 -25
- package/src/components/table/sticky/table_sticky.jsx +0 -501
- package/src/components/table/table.jsx +0 -721
- package/src/components/table/table_css.js +0 -211
- package/src/components/table/table_ui.jsx +0 -49
- package/src/components/table/use_cells_and_columns.js +0 -90
- package/src/components/table/use_object_array_to_cells.js +0 -46
- package/src/components/table/z_indexes.js +0 -23
- package/src/components/tablist/tablist.jsx +0 -99
- package/src/components/text/demos/demo_text_and_icon.html +0 -421
- package/src/components/text/overflow.jsx +0 -15
- package/src/components/text/text.jsx +0 -83
- package/src/components/text/text_and_count.jsx +0 -28
- package/src/components/ui_transition.jsx +0 -128
- package/src/components/use_auto_focus.js +0 -94
- package/src/components/use_batch_during_render.js +0 -33
- package/src/components/use_debounce_true.js +0 -31
- package/src/components/use_dependencies_diff.js +0 -35
- package/src/components/use_focus_group.js +0 -20
- package/src/components/use_initial_value.js +0 -78
- package/src/components/use_is_visited.js +0 -19
- package/src/components/use_ref_array.js +0 -38
- package/src/components/use_signal_sync.js +0 -50
- package/src/components/use_stable_callback.js +0 -68
- package/src/components/use_state_array.js +0 -47
- package/src/docs/actions.md +0 -250
- package/src/docs/demos/resource/action_status.jsx +0 -42
- package/src/docs/demos/resource/demo.md +0 -1
- package/src/docs/demos/resource/resource_demo_0.html +0 -84
- package/src/docs/demos/resource/resource_demo_10_post_gc.html +0 -364
- package/src/docs/demos/resource/resource_demo_11_describe_many.html +0 -362
- package/src/docs/demos/resource/resource_demo_2.html +0 -173
- package/src/docs/demos/resource/resource_demo_3_filtered_users.html +0 -415
- package/src/docs/demos/resource/resource_demo_4_details.html +0 -284
- package/src/docs/demos/resource/resource_demo_5_renderer_lazy.html +0 -115
- package/src/docs/demos/resource/resource_demo_6_gc.html +0 -217
- package/src/docs/demos/resource/resource_demo_7_child_gc.html +0 -240
- package/src/docs/demos/resource/resource_demo_8_proxy_gc.html +0 -319
- package/src/docs/demos/resource/resource_demo_9_describe_one.html +0 -472
- package/src/docs/demos/resource/tata.jsx +0 -3
- package/src/docs/demos/resource/toto.jsx +0 -3
- package/src/docs/demos/user_nav/user_nav.html +0 -12
- package/src/docs/demos/user_nav/user_nav.jsx +0 -330
- package/src/docs/resource_dependencies.md +0 -103
- package/src/docs/resource_with_params.md +0 -80
- package/src/navi_css_vars.js +0 -14
- package/src/notes.md +0 -34
- package/src/route/route.js +0 -596
- package/src/route/route.xtest.html +0 -228
- package/src/store/array_signal_store.js +0 -537
- package/src/store/local_storage_signal.js +0 -17
- package/src/store/resource_graph.js +0 -1304
- package/src/store/tests/resource_graph_autoreload_demo.html +0 -12
- package/src/store/tests/resource_graph_autoreload_demo.jsx +0 -964
- package/src/store/tests/resource_graph_dependencies.test_manual.js +0 -95
- package/src/store/value_in_local_storage.js +0 -187
- package/src/symbol_object_signal.js +0 -1
- package/src/use_action_data.js +0 -10
- package/src/use_action_status.js +0 -47
- package/src/utils/add_many_event_listeners.js +0 -15
- package/src/utils/array_add_remove.js +0 -61
- package/src/utils/array_signal.js +0 -15
- package/src/utils/compare_two_js_values.js +0 -172
- package/src/utils/execute_with_cleanup.js +0 -21
- package/src/utils/get_caller_info.js +0 -85
- package/src/utils/is_signal.js +0 -20
- package/src/utils/js_value_weak_map.js +0 -162
- package/src/utils/js_value_weak_map_demo.html +0 -690
- package/src/utils/merge_two_js_values.js +0 -53
- package/src/utils/stringify_for_display.js +0 -131
- package/src/utils/weak_effect.js +0 -48
- package/src/validation/constraints/confirm_constraint.js +0 -14
- package/src/validation/constraints/create_unique_value_constraint.js +0 -27
- package/src/validation/constraints/native_constraints.js +0 -338
- package/src/validation/constraints/readonly_constraint.js +0 -41
- package/src/validation/constraints/same_as_constraint.js +0 -42
- package/src/validation/constraints/single_space_constraint.js +0 -13
- package/src/validation/custom_constraint_validation.js +0 -793
- package/src/validation/custom_message.js +0 -18
- package/src/validation/demos/browser_style.png +0 -0
- package/src/validation/demos/demo_same_as_constraint.html +0 -259
- package/src/validation/demos/form_validation_demo.html +0 -142
- package/src/validation/demos/form_validation_demo_preact.html +0 -87
- package/src/validation/demos/form_validation_native_popover_demo.html +0 -168
- package/src/validation/demos/form_validation_vs_native_demo.html +0 -172
- package/src/validation/hooks/use_constraints.js +0 -23
- package/src/validation/hooks/use_custom_validation_ref.js +0 -73
- package/src/validation/hooks/use_validation_message.js +0 -19
- 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
|
-
};
|