@jsenv/navi 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/dist/jsenv_navi.js +22954 -0
  2. package/index.js +66 -16
  3. package/package.json +22 -11
  4. package/src/actions.js +50 -26
  5. package/src/browser_integration/browser_integration.js +31 -6
  6. package/src/browser_integration/via_history.js +42 -9
  7. package/src/components/action_execution/render_actionable_component.jsx +6 -4
  8. package/src/components/action_execution/use_action.js +51 -282
  9. package/src/components/action_execution/use_execute_action.js +106 -92
  10. package/src/components/action_execution/use_run_on_mount.js +9 -0
  11. package/src/components/action_renderer.jsx +21 -32
  12. package/src/components/demos/0_button_demo.html +574 -103
  13. package/src/components/demos/10_column_reordering_debug.html +277 -0
  14. package/src/components/demos/11_table_selection_debug.html +432 -0
  15. package/src/components/demos/1_checkbox_demo.html +579 -202
  16. package/src/components/demos/2_input_textual_demo.html +81 -138
  17. package/src/components/demos/3_radio_demo.html +0 -2
  18. package/src/components/demos/4_select_demo.html +19 -23
  19. package/src/components/demos/6_tablist_demo.html +77 -0
  20. package/src/components/demos/7_table_selection_demo.html +176 -0
  21. package/src/components/demos/8_table_fixed_headers_demo.html +584 -0
  22. package/src/components/demos/9_table_column_drag_demo.html +325 -0
  23. package/src/components/demos/action/0_button_demo.html +2 -4
  24. package/src/components/demos/action/1_input_text_demo.html +643 -222
  25. package/src/components/demos/action/3_details_demo.html +146 -115
  26. package/src/components/demos/action/4_input_checkbox_demo.html +442 -322
  27. package/src/components/demos/action/5_input_checkbox_state_demo.html +270 -0
  28. package/src/components/demos/action/6_checkbox_list_demo.html +304 -72
  29. package/src/components/demos/action/7_radio_list_demo.html +310 -170
  30. package/src/components/demos/action/{8_editable_text_demo.html → 8_editable_demo.html} +65 -76
  31. package/src/components/demos/action/9_link_demo.html +84 -62
  32. package/src/components/demos/ui_transition/0_action_renderer_ui_transition_demo.html +695 -0
  33. package/src/components/demos/ui_transition/1_nested_ui_transition_demo.html +429 -0
  34. package/src/components/demos/ui_transition/2_height_transition_test.html +295 -0
  35. package/src/components/details/details.jsx +62 -64
  36. package/src/components/edition/editable.jsx +186 -0
  37. package/src/components/field/README.md +247 -0
  38. package/src/components/{input → field}/button.jsx +151 -130
  39. package/src/components/field/checkbox_list.jsx +184 -0
  40. package/src/components/{collect_form_element_values.js → field/collect_form_element_values.js} +7 -4
  41. package/src/components/{input → field}/field_css.js +4 -1
  42. package/src/components/field/form.jsx +211 -0
  43. package/src/components/{input → field}/input.jsx +1 -0
  44. package/src/components/{input → field}/input_checkbox.jsx +132 -155
  45. package/src/components/{input → field}/input_radio.jsx +135 -46
  46. package/src/components/{input → field}/input_textual.jsx +247 -173
  47. package/src/components/field/label.jsx +32 -0
  48. package/src/components/field/radio_list.jsx +182 -0
  49. package/src/components/{input → field}/select.jsx +17 -32
  50. package/src/components/field/use_action_events.js +132 -0
  51. package/src/components/field/use_form_events.js +55 -0
  52. package/src/components/field/use_ui_state_controller.js +506 -0
  53. package/src/components/item_tracker/README.md +461 -0
  54. package/src/components/item_tracker/use_isolated_item_tracker.jsx +209 -0
  55. package/src/components/item_tracker/use_isolated_item_tracker_demo.html +148 -0
  56. package/src/components/item_tracker/use_isolated_item_tracker_demo.jsx +460 -0
  57. package/src/components/item_tracker/use_item_tracker.jsx +143 -0
  58. package/src/components/item_tracker/use_item_tracker_demo.html +207 -0
  59. package/src/components/item_tracker/use_item_tracker_demo.jsx +216 -0
  60. package/src/components/keyboard_shortcuts/active_keyboard_shortcuts.jsx +87 -0
  61. package/src/components/keyboard_shortcuts/aria_key_shortcuts.js +61 -0
  62. package/src/components/keyboard_shortcuts/keyboard_key_meta.js +17 -0
  63. package/src/components/keyboard_shortcuts/keyboard_shortcuts.js +371 -0
  64. package/src/components/link/link.jsx +65 -102
  65. package/src/components/link/link_with_icon.jsx +52 -0
  66. package/src/components/loader/loader_background.jsx +85 -64
  67. package/src/components/loader/rectangle_loading.jsx +38 -19
  68. package/src/components/route.jsx +8 -4
  69. package/src/components/selection/selection.jsx +1583 -0
  70. package/src/components/svg/font_sized_svg.jsx +45 -0
  71. package/src/components/svg/icon_and_text.jsx +21 -0
  72. package/src/components/svg/svg_mask_overlay.jsx +105 -0
  73. package/src/components/table/drag/table_drag.jsx +506 -0
  74. package/src/components/table/resize/table_resize.jsx +650 -0
  75. package/src/components/table/resize/table_size.js +43 -0
  76. package/src/components/table/selection/table_selection.js +106 -0
  77. package/src/components/table/selection/table_selection.jsx +203 -0
  78. package/src/components/table/sticky/sticky_group.js +354 -0
  79. package/src/components/table/sticky/table_sticky.js +25 -0
  80. package/src/components/table/sticky/table_sticky.jsx +501 -0
  81. package/src/components/table/table.jsx +721 -0
  82. package/src/components/table/table_css.js +211 -0
  83. package/src/components/table/table_ui.jsx +49 -0
  84. package/src/components/table/use_cells_and_columns.js +90 -0
  85. package/src/components/table/use_object_array_to_cells.js +46 -0
  86. package/src/components/table/z_indexes.js +23 -0
  87. package/src/components/tablist/tablist.jsx +99 -0
  88. package/src/components/text/overflow.jsx +15 -0
  89. package/src/components/text/text_and_count.jsx +28 -0
  90. package/src/components/ui_transition.jsx +128 -0
  91. package/src/components/use_auto_focus.js +58 -7
  92. package/src/components/use_batch_during_render.js +33 -0
  93. package/src/components/use_debounce_true.js +7 -7
  94. package/src/components/use_dependencies_diff.js +35 -0
  95. package/src/components/use_focus_group.js +4 -3
  96. package/src/components/use_initial_value.js +8 -34
  97. package/src/components/use_signal_sync.js +1 -1
  98. package/src/components/use_stable_callback.js +68 -0
  99. package/src/components/use_state_array.js +16 -9
  100. package/src/docs/actions.md +22 -0
  101. package/src/notes.md +33 -12
  102. package/src/route/route.js +97 -47
  103. package/src/store/resource_graph.js +2 -1
  104. package/src/store/tests/{resource_graph_dependencies.test.js → resource_graph_dependencies.test_manual.js} +13 -13
  105. package/src/utils/is_signal.js +20 -0
  106. package/src/utils/stringify_for_display.js +4 -23
  107. package/src/validation/constraints/confirm_constraint.js +14 -0
  108. package/src/validation/constraints/create_unique_value_constraint.js +27 -0
  109. package/src/validation/constraints/native_constraints.js +313 -0
  110. package/src/validation/constraints/readonly_constraint.js +36 -0
  111. package/src/validation/constraints/single_space_constraint.js +13 -0
  112. package/src/validation/custom_constraint_validation.js +599 -0
  113. package/src/validation/custom_message.js +18 -0
  114. package/src/validation/demos/browser_style.png +0 -0
  115. package/src/validation/demos/form_validation_demo.html +142 -0
  116. package/src/validation/demos/form_validation_demo_preact.html +87 -0
  117. package/src/validation/demos/form_validation_native_popover_demo.html +168 -0
  118. package/src/validation/demos/form_validation_vs_native_demo.html +172 -0
  119. package/src/validation/demos/validation_message_demo.html +203 -0
  120. package/src/validation/hooks/use_constraints.js +23 -0
  121. package/src/validation/hooks/use_custom_validation_ref.js +73 -0
  122. package/src/validation/hooks/use_validation_message.js +19 -0
  123. package/src/validation/validation_message.js +741 -0
  124. package/src/components/editable_text/editable_text.jsx +0 -96
  125. package/src/components/form.jsx +0 -144
  126. package/src/components/input/checkbox_list.jsx +0 -294
  127. package/src/components/input/field.jsx +0 -61
  128. package/src/components/input/radio_list.jsx +0 -283
  129. package/src/components/input/use_form_event.js +0 -20
  130. package/src/components/input/use_on_change.js +0 -12
  131. package/src/components/selection/selection.js +0 -5
  132. package/src/components/selection/selection_context.jsx +0 -262
  133. package/src/components/shortcut/shortcut_context.jsx +0 -390
  134. package/src/components/use_action_events.js +0 -37
  135. package/src/utils/iterable_weak_set.js +0 -62
  136. /package/src/components/demos/action/{11_nested_shortcuts_demo.html → 11_nested_shortcuts_demo.xhtml} +0 -0
  137. /package/src/components/{shortcut → keyboard_shortcuts}/os.js +0 -0
  138. /package/src/route/{route.test.html → route.xtest.html} +0 -0
@@ -0,0 +1,45 @@
1
+ /**
2
+ * FontSizedSvg component
3
+ *
4
+ * This component wraps an SVG element to make it inherit the current font size.
5
+ * It creates a container that's exactly 1em × 1em in size, allowing the SVG to scale
6
+ * proportionally with the surrounding text.
7
+ *
8
+ * Usage:
9
+ * ```jsx
10
+ * <FontSizedSvg>
11
+ * <svg width="100%" height="100%" viewBox="...">
12
+ * <path d="..." />
13
+ * </svg>
14
+ * </FontSizedSvg>
15
+ * ```
16
+ *
17
+ * Notes:
18
+ * - The wrapped SVG should use width="100%" and height="100%" to fill the container
19
+ * - This ensures SVG icons match the current text size without additional styling
20
+ * - Useful for inline icons that should respect the parent's font-size
21
+ */
22
+
23
+ export const FontSizedSvg = ({
24
+ width = "1em",
25
+ height = "1em",
26
+ children,
27
+ ...props
28
+ }) => {
29
+ return (
30
+ <span
31
+ {...props}
32
+ style={{
33
+ display: "flex",
34
+ alignItems: "center",
35
+ width,
36
+ height,
37
+ justifySelf: "center",
38
+ lineHeight: "1em",
39
+ flexShrink: 0,
40
+ }}
41
+ >
42
+ {children}
43
+ </span>
44
+ );
45
+ };
@@ -0,0 +1,21 @@
1
+ import { FontSizedSvg } from "./font_sized_svg.jsx";
2
+
3
+ export const IconAndText = ({ icon, children, ...rest }) => {
4
+ if (typeof icon === "function") icon = icon({});
5
+
6
+ return (
7
+ <span
8
+ className="icon_and_text"
9
+ {...rest}
10
+ style={{
11
+ display: "flex",
12
+ alignItems: "center",
13
+ gap: "0.1em",
14
+ ...rest.style,
15
+ }}
16
+ >
17
+ <FontSizedSvg className="icon">{icon}</FontSizedSvg>
18
+ <span className="text">{children}</span>
19
+ </span>
20
+ );
21
+ };
@@ -0,0 +1,105 @@
1
+ /**
2
+ * SVGComposition Component
3
+ *
4
+ * Creates composite SVGs by combining independent SVG elements with masking.
5
+ *
6
+ * This component solves the challenge of combining independently created SVGs into
7
+ * a single visual composition. Each SVG can have its own coordinate system, viewBox,
8
+ * and styling, allowing for maximum reusability of individual icons or graphics.
9
+ *
10
+ * When overlaying SVGs, each subsequent overlay "cuts out" its portion from the base SVG,
11
+ * creating a seamless integration where SVGs appear to interact with each other visually.
12
+ *
13
+ * Key benefits:
14
+ * - Maintains each SVG's independence - use them individually elsewhere
15
+ * - Handles different viewBox dimensions automatically
16
+ * - Works with any SVG components regardless of internal implementation
17
+ * - Supports unlimited overlay elements
18
+ * - Creates proper masking between elements for visual integration
19
+ *
20
+ * Usage example combining two independent icon components:
21
+ * ```jsx
22
+ * <SVGMaskOverlay viewBox="0 0 24 24">
23
+ * <DatabaseSvg />
24
+ * <svg x="12" y="12" width="16" height="16" overflow="visible">
25
+ * <PlusSvg />
26
+ * </svg>
27
+ * </SVGMaskOverlay>
28
+ * ```
29
+ *
30
+ * @param {Object} props - Component properties
31
+ * @param {string} props.viewBox - The main viewBox for the composition (required)
32
+ * @param {ReactNode[]} props.children - SVG elements (first is base, rest are overlays)
33
+ * @returns {ReactElement} A composed SVG with all elements properly masked
34
+ */
35
+
36
+ import { cloneElement } from "preact";
37
+
38
+ import.meta.css = /* css */ `
39
+ .svg_mask_content * {
40
+ fill: black !important;
41
+ stroke: black !important;
42
+ fill-opacity: 1 !important;
43
+ stroke-opacity: 1 !important;
44
+ color: black !important;
45
+ opacity: 1 !important;
46
+ }
47
+ `;
48
+
49
+ export const SVGMaskOverlay = ({ viewBox, children }) => {
50
+ if (!Array.isArray(children)) {
51
+ return children;
52
+ }
53
+ if (children.length === 1) {
54
+ return children[0];
55
+ }
56
+ if (!viewBox) {
57
+ console.error("SVGComposition requires an explicit viewBox");
58
+ return null;
59
+ }
60
+
61
+ // First SVG is the base, all others are overlays
62
+ const [baseSvg, ...overlaySvgs] = children;
63
+
64
+ // Generate unique ID for this instance
65
+ const instanceId = `svgmo-${Math.random().toString(36).slice(2, 9)}`;
66
+
67
+ // Create nested masked elements
68
+ let maskedElement = baseSvg;
69
+
70
+ // Apply each mask in sequence
71
+ overlaySvgs.forEach((overlaySvg, index) => {
72
+ const maskId = `mask-${instanceId}-${index}`;
73
+ maskedElement = <g mask={`url(#${maskId})`}>{maskedElement}</g>;
74
+ });
75
+
76
+ return (
77
+ <svg viewBox={viewBox} width="100%" height="100%">
78
+ <defs>
79
+ {/* Define masks that respect position */}
80
+ {overlaySvgs.map((overlaySvg, index) => {
81
+ const maskId = `mask-${instanceId}-${index}`;
82
+
83
+ // IMPORTANT: clone the overlay SVG exactly as is, just add the mask class
84
+ return (
85
+ <mask id={maskId} key={maskId}>
86
+ {/* White background makes everything visible by default */}
87
+ <rect width="100%" height="100%" fill="white" />
88
+
89
+ {/* EXACT CLONE of the overlay SVG */}
90
+ {cloneElement(overlaySvg, {
91
+ className: "svg_mask_content", // Apply styling to make it black
92
+ })}
93
+ </mask>
94
+ );
95
+ })}
96
+ </defs>
97
+
98
+ {/* Base SVG with all masks applied */}
99
+ {maskedElement}
100
+
101
+ {/* Render all overlays */}
102
+ {overlaySvgs}
103
+ </svg>
104
+ );
105
+ };
@@ -0,0 +1,506 @@
1
+ import {
2
+ createDragToMoveGestureController,
3
+ createPubSub,
4
+ dragAfterThreshold,
5
+ getDropTargetInfo,
6
+ getScrollContainer,
7
+ stickyAsRelativeCoords,
8
+ } from "@jsenv/dom";
9
+ import { createContext } from "preact";
10
+ import { forwardRef } from "preact/compat";
11
+ import { useMemo, useState } from "preact/hooks";
12
+
13
+ import { useStableCallback } from "../../use_stable_callback.js";
14
+ import { Z_INDEX_CELL_FOREGROUND, Z_INDEX_DROP_PREVIEW } from "../z_indexes.js";
15
+
16
+ const DEBUG_VISUAL = false;
17
+
18
+ import.meta.css = /* css */ `
19
+ .navi_table_drag_clone_container {
20
+ position: absolute;
21
+ left: var(--table-visual-left);
22
+ top: var(--table-visual-top);
23
+ width: var(--table-visual-width);
24
+ height: var(--table-visual-height);
25
+ /* background: rgba(0, 0, 0, 0.5); */
26
+ }
27
+
28
+ .navi_table_cell[data-grabbed]::before,
29
+ .navi_table_cell[data-grabbed]::after {
30
+ box-shadow: none !important;
31
+ }
32
+
33
+ /* We preprend ".navi_table_container" to ensure it propertly overrides */
34
+ .navi_table_drag_clone_container .navi_table_cell {
35
+ opacity: ${DEBUG_VISUAL ? 0.5 : 0};
36
+ }
37
+
38
+ .navi_table_drag_clone_container .navi_table_cell[data-grabbed] {
39
+ opacity: 0.7;
40
+ }
41
+
42
+ .navi_table_drag_clone_container .navi_table_cell_sticky_frontier {
43
+ opacity: 0;
44
+ }
45
+
46
+ .navi_table_drag_clone_container .navi_table_cell[data-sticky-left],
47
+ .navi_table_drag_clone_container .navi_table_cell[data-sticky-top] {
48
+ position: relative;
49
+ }
50
+
51
+ .navi_table_cell_foreground {
52
+ pointer-events: none;
53
+ position: absolute;
54
+ inset: 0;
55
+ background: lightgrey;
56
+ opacity: 0;
57
+ z-index: ${Z_INDEX_CELL_FOREGROUND};
58
+ }
59
+ .navi_table_cell[data-first-row] .navi_table_cell_foreground {
60
+ background-color: grey;
61
+ }
62
+ .navi_table_cell_foreground[data-visible] {
63
+ opacity: 1;
64
+ }
65
+
66
+ .navi_table_drag_clone_container .navi_table_cell_foreground {
67
+ opacity: 1;
68
+ background-color: rgba(255, 255, 255, 0.2);
69
+ backdrop-filter: blur(10px);
70
+ }
71
+ .navi_table_drag_clone_container
72
+ .navi_table_cell[data-first-row][data-grabbed] {
73
+ opacity: 1;
74
+ }
75
+ .navi_table_drag_clone_container
76
+ .navi_table_cell[data-first-row]
77
+ .navi_table_cell_foreground {
78
+ opacity: 0;
79
+ }
80
+
81
+ .navi_table_column_drop_preview {
82
+ position: absolute;
83
+ left: var(--column-left);
84
+ top: var(--column-top);
85
+ width: var(--column-width);
86
+ height: var(--column-height);
87
+ pointer-events: none;
88
+ z-index: ${Z_INDEX_DROP_PREVIEW};
89
+ /* Invisible container - just for positioning */
90
+ background: transparent;
91
+ border: none;
92
+ }
93
+
94
+ .navi_table_column_drop_preview_line {
95
+ position: absolute;
96
+ top: 0;
97
+ bottom: 0;
98
+ width: 4px;
99
+ background: rgba(0, 0, 255, 0.5);
100
+ opacity: 0;
101
+ left: 0; /* Default: left edge for dropping before */
102
+ transform: translateX(-50%);
103
+ }
104
+ .navi_table_column_drop_preview[data-after]
105
+ .navi_table_column_drop_preview_line {
106
+ left: 100%; /* Right edge for dropping after */
107
+ }
108
+ .navi_table_column_drop_preview[data-visible]
109
+ .navi_table_column_drop_preview_line {
110
+ opacity: 1;
111
+ }
112
+
113
+ .navi_table_column_drop_preview .arrow_positioner {
114
+ position: absolute;
115
+ left: 0; /* Default: left edge for dropping before */
116
+ display: flex;
117
+ opacity: 0;
118
+ transform: translateX(-50%);
119
+ color: rgba(0, 0, 255, 0.5);
120
+ }
121
+ .navi_table_column_drop_preview[data-after] .arrow_positioner {
122
+ left: 100%; /* Right edge for dropping after */
123
+ }
124
+ .navi_table_column_drop_preview[data-visible] .arrow_positioner {
125
+ opacity: 1;
126
+ }
127
+ .navi_table_column_drop_preview .arrow_positioner[data-top] {
128
+ top: -10px;
129
+ }
130
+ .navi_table_column_drop_preview .arrow_positioner[data-bottom] {
131
+ bottom: -10px;
132
+ }
133
+ .arrow_positioner svg {
134
+ width: 10px;
135
+ height: 10px;
136
+ }
137
+ `;
138
+
139
+ export const TableDragContext = createContext();
140
+ export const useTableDragContextValue = ({
141
+ tableDragCloneContainerRef,
142
+ tableColumnDropPreviewRef,
143
+ columns,
144
+ setColumnOrder,
145
+ canChangeColumnOrder,
146
+ }) => {
147
+ setColumnOrder = useStableCallback(setColumnOrder);
148
+
149
+ const [grabTarget, setGrabTarget] = useState(null);
150
+ const grabColumn = (columnIndex) => {
151
+ setGrabTarget(`column:${columnIndex}`);
152
+ };
153
+ const releaseColumn = (columnIndex, newColumnIndex) => {
154
+ setGrabTarget(null);
155
+ if (columnIndex === newColumnIndex) {
156
+ return;
157
+ }
158
+ const columnIds = columns.map((col) => col.id);
159
+ const columnIdsWithNewOrder = moveItem(
160
+ columnIds,
161
+ columnIndex,
162
+ newColumnIndex,
163
+ );
164
+ setColumnOrder(columnIdsWithNewOrder);
165
+ };
166
+
167
+ return useMemo(() => {
168
+ return {
169
+ tableDragCloneContainerRef,
170
+ tableColumnDropPreviewRef,
171
+ grabTarget,
172
+ grabColumn,
173
+ releaseColumn,
174
+ setColumnOrder,
175
+ canChangeColumnOrder,
176
+ };
177
+ }, [grabTarget, canChangeColumnOrder]);
178
+ };
179
+ const moveItem = (array, indexA, indexB) => {
180
+ const newArray = [];
181
+ const movedItem = array[indexA];
182
+ const movingRight = indexA < indexB;
183
+
184
+ for (let i = 0; i < array.length; i++) {
185
+ if (movingRight) {
186
+ // Moving right: add target first, then moved item after
187
+ if (i !== indexA) {
188
+ newArray.push(array[i]);
189
+ }
190
+ if (i === indexB) {
191
+ newArray.push(movedItem);
192
+ }
193
+ } else {
194
+ // Moving left: add moved item first, then target after
195
+ if (i === indexB) {
196
+ newArray.push(movedItem);
197
+ }
198
+ if (i !== indexA) {
199
+ newArray.push(array[i]);
200
+ }
201
+ }
202
+ }
203
+ return newArray;
204
+ };
205
+ export const swapItem = (array, indexA, indexB) => {
206
+ const newArray = [];
207
+ const itemAtPositionA = array[indexA];
208
+ const itemAtPositionB = array[indexB];
209
+ for (let i = 0; i < array.length; i++) {
210
+ if (i === indexB) {
211
+ // At the new position, put the dragged column
212
+ newArray.push(itemAtPositionA);
213
+ continue;
214
+ }
215
+ if (i === indexA) {
216
+ // At the old position, put what was at the new position
217
+ newArray.push(itemAtPositionB);
218
+ continue;
219
+ }
220
+ // Everything else stays the same
221
+ newArray.push(array[i]);
222
+ }
223
+ return newArray;
224
+ };
225
+
226
+ export const TableDragCloneContainer = forwardRef((props, ref) => {
227
+ const { tableId } = props;
228
+
229
+ return (
230
+ <div
231
+ ref={ref}
232
+ className="navi_table_drag_clone_container"
233
+ data-overlay-for={tableId}
234
+ ></div>
235
+ );
236
+ });
237
+ export const TableColumnDropPreview = forwardRef((props, ref) => {
238
+ return (
239
+ <div ref={ref} className="navi_table_column_drop_preview">
240
+ <div className="arrow_positioner" data-top="">
241
+ {/* This is an arrow pointing down */}
242
+ <svg fill="currentColor" viewBox="0 0 30.727 30.727">
243
+ <path
244
+ d="M29.994,10.183L15.363,24.812L0.733,10.184c-0.977-0.978-0.977-2.561,0-3.536c0.977-0.977,2.559-0.976,3.536,0
245
+ l11.095,11.093L26.461,6.647c0.977-0.976,2.559-0.976,3.535,0C30.971,7.624,30.971,9.206,29.994,10.183z"
246
+ />
247
+ </svg>
248
+ </div>
249
+ <div className="navi_table_column_drop_preview_line"></div>
250
+ <div className="arrow_positioner" data-bottom="">
251
+ {/* This is an arrow pointing up */}
252
+ <svg fill="currentColor" viewBox="0 0 30.727 30.727">
253
+ <path
254
+ d="M29.994,20.544L15.363,5.915L0.733,20.543c-0.977,0.978-0.977,2.561,0,3.536c0.977,0.977,2.559,0.976,3.536,0
255
+ l11.095-11.093L26.461,24.08c0.977,0.976,2.559,0.976,3.535,0C30.971,23.103,30.971,21.521,29.994,20.544z"
256
+ />
257
+ </svg>
258
+ </div>
259
+ </div>
260
+ );
261
+ });
262
+
263
+ export const initDragTableColumnViaPointer = (
264
+ pointerdownEvent,
265
+ { tableDragCloneContainer, dropPreview, onGrab, onDrag, onRelease },
266
+ ) => {
267
+ dragAfterThreshold(pointerdownEvent, () => {
268
+ const [teardown, addTeardown] = createPubSub();
269
+
270
+ const tableCell = pointerdownEvent.target.closest(".navi_table_cell");
271
+ const table = tableCell.closest(".navi_table");
272
+ const columnIndex = Array.from(tableCell.parentNode.children).indexOf(
273
+ tableCell,
274
+ );
275
+
276
+ // Track the drop target column index (starts as current column)
277
+ let dropColumnIndex = columnIndex;
278
+
279
+ const tableClone = table.cloneNode(true);
280
+ // ensure [data-drag-obstacle] inside the table clone are ignored
281
+ tableClone.setAttribute("data-drag-ignore", "");
282
+
283
+ // Scale down the table clone and set transform origin to mouse grab point
284
+ // const tableRect = table.getBoundingClientRect();
285
+ // const mouseX = mousedownEvent.clientX - tableRect.left;
286
+ // const mouseY = mousedownEvent.clientY - tableRect.top;
287
+ // tableClone.style.transform = "scale(1.2)";
288
+ // tableClone.style.transformOrigin = `${mouseX}px ${mouseY}px`;
289
+
290
+ update_sticky_elements: {
291
+ // In the table clone we need to convert sticky elements to position: relative
292
+ // with calculated offsets that match their appearance in the original context
293
+ const scrollContainer = getScrollContainer(table);
294
+
295
+ // important: only on cells, not on <col> nor <tr>
296
+ const originalStickyCells = table.querySelectorAll(
297
+ ".navi_table_cell[data-sticky-left], .navi_table_cell[data-sticky-top]",
298
+ );
299
+ const cloneStickyCells = tableClone.querySelectorAll(
300
+ ".navi_table_cell[data-sticky-left], .navi_table_cell[data-sticky-top]",
301
+ );
302
+
303
+ originalStickyCells.forEach((originalCell, index) => {
304
+ const cloneCell = cloneStickyCells[index];
305
+ const relativePosition = stickyAsRelativeCoords(
306
+ originalCell,
307
+ // Our clone is absolutely positioned on top of <table />
308
+ // So we need the sticky position relative to <table />
309
+ table,
310
+ {
311
+ scrollContainer,
312
+ },
313
+ );
314
+ if (relativePosition) {
315
+ const [relativeLeft, relativeTop] = relativePosition;
316
+ cloneCell.style.position = "relative";
317
+ if (relativeLeft !== undefined) {
318
+ cloneCell.style.left = `${relativeLeft}px`;
319
+ }
320
+ if (relativeTop !== undefined) {
321
+ cloneCell.style.top = `${relativeTop}px`;
322
+ }
323
+ }
324
+ });
325
+ }
326
+
327
+ sync_data_grabbed: {
328
+ // ensure [data-grabbed] are present in the table clone
329
+ // we could retry on "sync_attributes" but we want to be sure it's done asap to prevent table from being displayed at all
330
+ // I fear without this we might have an intermediate step where the table column clone is not visible
331
+ // as [data-grabbed] are not set
332
+ // Would not be a problem but this ensure we see exactly the table clone right away preventing any possibility
333
+ // of visual glitches
334
+ const tableCloneCells = tableClone.querySelectorAll(".navi_table_cell");
335
+ tableCloneCells.forEach((cellClone) => {
336
+ const cellColumnIndex = Array.from(
337
+ cellClone.parentNode.children,
338
+ ).indexOf(cellClone);
339
+ if (cellColumnIndex === columnIndex) {
340
+ cellClone.setAttribute("data-grabbed", "");
341
+ }
342
+ });
343
+ }
344
+
345
+ append_in_dom: {
346
+ tableDragCloneContainer.appendChild(tableClone);
347
+ addTeardown(() => {
348
+ tableClone.remove();
349
+ });
350
+ }
351
+
352
+ sync_attributes: {
353
+ // Sync attribute changes from original table to clone
354
+ // This is used to:
355
+ // - handle table cells being selected as result of mousedown on the <th />
356
+ // - nothing else is supposed to change in the original <table /> during the drag gesture
357
+ const syncTableAttributes = createTableAttributeSync(table, tableClone);
358
+ addTeardown(() => {
359
+ syncTableAttributes.disconnect();
360
+ });
361
+ }
362
+
363
+ const colgroup = table.querySelector(".navi_colgroup");
364
+ const colElements = Array.from(colgroup.children);
365
+
366
+ const col = colElements[columnIndex];
367
+ const colgroupClone = tableClone.querySelector(".navi_colgroup");
368
+ const colClone = colgroupClone.children[columnIndex];
369
+ const dragToMoveGestureController = createDragToMoveGestureController({
370
+ name: "move-column",
371
+ direction: { x: true },
372
+ threshold: 0,
373
+ onGrab,
374
+ onDrag: (gestureInfo) => {
375
+ onDrag?.(gestureInfo, dropColumnIndex);
376
+ },
377
+ resetPositionAfterRelease: !DEBUG_VISUAL,
378
+ onRelease: (gestureInfo) => {
379
+ if (!DEBUG_VISUAL) {
380
+ teardown();
381
+ }
382
+ onRelease?.(gestureInfo, dropColumnIndex);
383
+ },
384
+ });
385
+ const dragToMoveGesture = dragToMoveGestureController.grabViaPointer(
386
+ pointerdownEvent,
387
+ {
388
+ element: colClone,
389
+ referenceElement: col,
390
+ elementToMove: tableClone,
391
+ },
392
+ );
393
+
394
+ drop_preview: {
395
+ // Get all column elements for drop target detection
396
+ const dropCandidateElements = colElements.filter(
397
+ (col) =>
398
+ !(col.getAttribute("data-drag-obstacle") || "").includes(
399
+ "move-column",
400
+ ),
401
+ );
402
+
403
+ const updateDropTarget = (dropTargetInfo) => {
404
+ const targetColumn = dropTargetInfo.element;
405
+ const targetColumnIndex = colElements.indexOf(targetColumn);
406
+ dropColumnIndex = targetColumnIndex;
407
+ if (dropColumnIndex === columnIndex) {
408
+ dropPreview.removeAttribute("data-visible");
409
+ return;
410
+ }
411
+ // Position the invisible container to match the target column
412
+ const { left, top, width, height } =
413
+ targetColumn.getBoundingClientRect();
414
+ dropPreview.style.setProperty("--column-left", `${left}px`);
415
+ dropPreview.style.setProperty("--column-top", `${top}px`);
416
+ dropPreview.style.setProperty("--column-width", `${width}px`);
417
+ dropPreview.style.setProperty("--column-height", `${height}px`);
418
+ // Set data-after attribute to control line position via CSS
419
+ if (dropColumnIndex > columnIndex) {
420
+ // Dropping after: CSS will position line at right edge (100%)
421
+ dropPreview.setAttribute("data-after", "");
422
+ } else {
423
+ // Dropping before: CSS will position line at left edge (0%)
424
+ dropPreview.removeAttribute("data-after");
425
+ }
426
+ dropPreview.setAttribute("data-drop-column-index", dropColumnIndex);
427
+ dropPreview.setAttribute("data-visible", "");
428
+ };
429
+
430
+ dragToMoveGesture.addDragCallback((gestureInfo) => {
431
+ const dropTargetInfo = getDropTargetInfo(
432
+ gestureInfo,
433
+ dropCandidateElements,
434
+ );
435
+ if (!dropTargetInfo) {
436
+ dropPreview.removeAttribute("data-visible");
437
+ return;
438
+ }
439
+ updateDropTarget(dropTargetInfo);
440
+ });
441
+ dragToMoveGesture.addReleaseCallback(() => {
442
+ dropPreview.removeAttribute("data-visible");
443
+ });
444
+ }
445
+
446
+ return dragToMoveGesture;
447
+ });
448
+ };
449
+
450
+ /**
451
+ * Creates a MutationObserver that syncs attribute changes from original table to clone
452
+ * @param {HTMLElement} table - The original table element
453
+ * @param {HTMLElement} cloneTable - The cloned table element
454
+ * @returns {MutationObserver} The observer instance with disconnect method
455
+ */
456
+ const createTableAttributeSync = (table, tableClone) => {
457
+ // Create a map to quickly find corresponding elements in the clone
458
+ const createElementMap = () => {
459
+ const map = new Map();
460
+ const cells = table.querySelectorAll(".navi_table_cell");
461
+ const cellClones = tableClone.querySelectorAll(".navi_table_cell");
462
+ for (let i = 0; i < cells.length; i++) {
463
+ if (cellClones[i]) {
464
+ map.set(cells[i], cellClones[i]);
465
+ }
466
+ }
467
+ return map;
468
+ };
469
+
470
+ const elementMap = createElementMap();
471
+ const observer = new MutationObserver((mutations) => {
472
+ mutations.forEach((mutation) => {
473
+ if (mutation.type === "attributes") {
474
+ const originalElement = mutation.target;
475
+ const cloneElement = elementMap.get(originalElement);
476
+
477
+ if (cloneElement) {
478
+ const attributeName = mutation.attributeName;
479
+ if (attributeName === "style") {
480
+ return;
481
+ }
482
+
483
+ // Sync the attribute change to the clone
484
+ if (originalElement.hasAttribute(attributeName)) {
485
+ const attributeValue = originalElement.getAttribute(attributeName);
486
+ cloneElement.setAttribute(attributeName, attributeValue);
487
+ } else {
488
+ cloneElement.removeAttribute(attributeName);
489
+ }
490
+ }
491
+ }
492
+ });
493
+ });
494
+
495
+ // Observe attribute changes on all table cells
496
+ const cellsToObserve = table.querySelectorAll(".navi_table_cell");
497
+ cellsToObserve.forEach((cell) => {
498
+ observer.observe(cell, {
499
+ attributes: true,
500
+ attributeOldValue: false,
501
+ subtree: false,
502
+ });
503
+ });
504
+
505
+ return observer;
506
+ };