@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,106 @@
1
+ import { createContext } from "preact";
2
+ import { useMemo } from "preact/hooks";
3
+
4
+ export const TableSelectionContext = createContext();
5
+ export const useTableSelectionContextValue = (
6
+ selection,
7
+ selectionController,
8
+ ) => {
9
+ const selectionContextValue = useMemo(() => {
10
+ const selectedColumnIds = [];
11
+ const selectedRowIds = [];
12
+ const selectedCellIds = [];
13
+ const columnIdWithSomeSelectedCellSet = new Set();
14
+ const rowIdWithSomeSelectedCellSet = new Set();
15
+ for (const item of selection) {
16
+ const selectionValueInfo = parseTableSelectionValue(item);
17
+ if (selectionValueInfo.type === "row") {
18
+ const { rowId } = selectionValueInfo;
19
+ selectedRowIds.push(rowId);
20
+ continue;
21
+ }
22
+ if (selectionValueInfo.type === "column") {
23
+ const { columnId } = selectionValueInfo;
24
+ selectedColumnIds.push(columnId);
25
+ continue;
26
+ }
27
+ if (selectionValueInfo.type === "cell") {
28
+ const { cellId, columnId, rowId } = selectionValueInfo;
29
+ selectedCellIds.push(cellId);
30
+ columnIdWithSomeSelectedCellSet.add(columnId);
31
+ rowIdWithSomeSelectedCellSet.add(rowId);
32
+ continue;
33
+ }
34
+ }
35
+ return {
36
+ selection,
37
+ selectionController,
38
+ selectedColumnIds,
39
+ selectedRowIds,
40
+ columnIdWithSomeSelectedCellSet,
41
+ rowIdWithSomeSelectedCellSet,
42
+ };
43
+ }, [selection]);
44
+
45
+ return selectionContextValue;
46
+ };
47
+
48
+ export const parseTableSelectionValue = (selectionValue) => {
49
+ if (selectionValue.startsWith("column:")) {
50
+ const columnId = selectionValue.slice("column:".length);
51
+ return { type: "column", columnId };
52
+ }
53
+ if (selectionValue.startsWith("row:")) {
54
+ const rowId = selectionValue.slice("row:".length);
55
+ return { type: "row", rowId };
56
+ }
57
+ const cellId = selectionValue.slice("cell:".length);
58
+ const [columnId, rowId] = cellId.split("-");
59
+ return { type: "cell", cellId, columnId, rowId };
60
+ };
61
+ export const stringifyTableSelectionValue = (type, value) => {
62
+ if (type === "cell") {
63
+ const { columnId, rowId } = value;
64
+ return `cell:${columnId}-${rowId}`;
65
+ }
66
+ if (type === "column") {
67
+ return `column:${value}`;
68
+ }
69
+ if (type === "row") {
70
+ return `row:${value}`;
71
+ }
72
+ return "";
73
+ };
74
+
75
+ /**
76
+ * Check if a specific cell is selected
77
+ * @param {Array} selection - The selection set or array
78
+ * @param {{rowIndex: number, columnIndex: number}} cellPosition - Cell coordinates
79
+ * @returns {boolean} True if the cell is selected
80
+ */
81
+ export const isCellSelected = (selection, cellId) => {
82
+ const cellSelectionValue = stringifyTableSelectionValue("cell", cellId);
83
+ return selection.includes(cellSelectionValue);
84
+ };
85
+
86
+ /**
87
+ * Check if a specific row is selected
88
+ * @param {Array} selection - The selection set or array
89
+ * @param {number} rowIndex - Row index
90
+ * @returns {boolean} True if the row is selected
91
+ */
92
+ export const isRowSelected = (selection, rowId) => {
93
+ const rowSelectionValue = stringifyTableSelectionValue("row", rowId);
94
+ return selection.includes(rowSelectionValue);
95
+ };
96
+
97
+ /**
98
+ * Check if a specific column is selected
99
+ * @param {Array} selection - The selection set or array
100
+ * @param {number} columnIndex - Column index
101
+ * @returns {boolean} True if the column is selected
102
+ */
103
+ export const isColumnSelected = (selection, columnId) => {
104
+ const columnSelectionValue = stringifyTableSelectionValue("column", columnId);
105
+ return selection.has(columnSelectionValue);
106
+ };
@@ -0,0 +1,203 @@
1
+ import { useLayoutEffect } from "preact/hooks";
2
+
3
+ import { useSelectionController } from "../../selection/selection.jsx";
4
+
5
+ import.meta.css = /* css */ `
6
+ body {
7
+ --selection-border-color: #0078d4;
8
+ --selection-background-color: #eaf1fd;
9
+ }
10
+
11
+ .navi_table_cell[aria-selected="true"] {
12
+ background-color: var(--selection-background-color);
13
+ }
14
+
15
+ /* One border */
16
+ .navi_table_cell[data-selection-border-top]::after {
17
+ box-shadow: inset 0 1px 0 0 var(--selection-border-color);
18
+ }
19
+ .navi_table_cell[data-selection-border-right]::after {
20
+ box-shadow: inset -1px 0 0 0 var(--selection-border-color);
21
+ }
22
+ .navi_table_cell[data-selection-border-bottom]::after {
23
+ box-shadow: inset 0 -1px 0 0 var(--selection-border-color);
24
+ }
25
+ .navi_table_cell[data-selection-border-left]::after {
26
+ box-shadow: inset 1px 0 0 0 var(--selection-border-color);
27
+ }
28
+
29
+ /* Two border combinations */
30
+ .navi_table_cell[data-selection-border-top][data-selection-border-right]::after {
31
+ box-shadow:
32
+ inset 0 1px 0 0 var(--selection-border-color),
33
+ inset -1px 0 0 0 var(--selection-border-color);
34
+ }
35
+ .navi_table_cell[data-selection-border-top][data-selection-border-bottom]::after {
36
+ box-shadow:
37
+ inset 0 1px 0 0 var(--selection-border-color),
38
+ inset 0 -1px 0 0 var(--selection-border-color);
39
+ }
40
+ .navi_table_cell[data-selection-border-top][data-selection-border-left]::after {
41
+ box-shadow:
42
+ inset 0 1px 0 0 var(--selection-border-color),
43
+ inset 1px 0 0 0 var(--selection-border-color);
44
+ }
45
+ .navi_table_cell[data-selection-border-right][data-selection-border-bottom]::after {
46
+ box-shadow:
47
+ inset -1px 0 0 0 var(--selection-border-color),
48
+ inset 0 -1px 0 0 var(--selection-border-color);
49
+ }
50
+ .navi_table_cell[data-selection-border-right][data-selection-border-left]::after {
51
+ box-shadow:
52
+ inset -1px 0 0 0 var(--selection-border-color),
53
+ inset 1px 0 0 0 var(--selection-border-color);
54
+ }
55
+ .navi_table_cell[data-selection-border-bottom][data-selection-border-left]::after {
56
+ box-shadow:
57
+ inset 0 -1px 0 0 var(--selection-border-color),
58
+ inset 1px 0 0 0 var(--selection-border-color);
59
+ }
60
+
61
+ /* Three border combinations */
62
+ .navi_table_cell[data-selection-border-top][data-selection-border-right][data-selection-border-bottom]::after {
63
+ box-shadow:
64
+ inset 0 1px 0 0 var(--selection-border-color),
65
+ inset -1px 0 0 0 var(--selection-border-color),
66
+ inset 0 -1px 0 0 var(--selection-border-color);
67
+ }
68
+ .navi_table_cell[data-selection-border-top][data-selection-border-bottom][data-selection-border-left]::after {
69
+ box-shadow:
70
+ inset 0 1px 0 0 var(--selection-border-color),
71
+ inset 0 -1px 0 0 var(--selection-border-color),
72
+ inset 1px 0 0 0 var(--selection-border-color);
73
+ }
74
+ .navi_table_cell[data-selection-border-top][data-selection-border-right][data-selection-border-left]::after {
75
+ box-shadow:
76
+ inset 0 1px 0 0 var(--selection-border-color),
77
+ inset -1px 0 0 0 var(--selection-border-color),
78
+ inset 1px 0 0 0 var(--selection-border-color);
79
+ }
80
+ .navi_table_cell[data-selection-border-right][data-selection-border-bottom][data-selection-border-left]::after {
81
+ box-shadow:
82
+ inset -1px 0 0 0 var(--selection-border-color),
83
+ inset 0 -1px 0 0 var(--selection-border-color),
84
+ inset 1px 0 0 0 var(--selection-border-color);
85
+ }
86
+
87
+ /* Four border combinations (full selection) */
88
+ .navi_table_cell[data-selection-border-top][data-selection-border-right][data-selection-border-bottom][data-selection-border-left]::after {
89
+ box-shadow:
90
+ inset 0 1px 0 0 var(--selection-border-color),
91
+ inset -1px 0 0 0 var(--selection-border-color),
92
+ inset 0 -1px 0 0 var(--selection-border-color),
93
+ inset 1px 0 0 0 var(--selection-border-color);
94
+ }
95
+ `;
96
+
97
+ export const useTableSelectionController = ({
98
+ tableRef,
99
+ selection,
100
+ onSelectionChange,
101
+ selectionColor,
102
+ }) => {
103
+ const selectionController = useSelectionController({
104
+ elementRef: tableRef,
105
+ layout: "grid",
106
+ value: selection,
107
+ onChange: onSelectionChange,
108
+ selectAllName: "cell",
109
+ });
110
+
111
+ useLayoutEffect(() => {
112
+ const table = tableRef.current;
113
+ if (!table) {
114
+ return;
115
+ }
116
+ updateSelectionBorders(table, selectionController);
117
+ }, [selectionController.value]);
118
+
119
+ useLayoutEffect(() => {
120
+ const table = tableRef.current;
121
+ if (!table) {
122
+ return;
123
+ }
124
+ if (selectionColor) {
125
+ table.style.setProperty("--selection-border-color", selectionColor);
126
+ } else {
127
+ table.style.removeProperty("--selection-border-color");
128
+ }
129
+ }, [selectionColor]);
130
+
131
+ return selectionController;
132
+ };
133
+
134
+ const updateSelectionBorders = (tableElement, selectionController) => {
135
+ // Find all selected cells
136
+ const cells = Array.from(tableElement.querySelectorAll(".navi_table_cell"));
137
+ const selectedCells = [];
138
+ for (const cell of cells) {
139
+ if (selectionController.isElementSelected(cell)) {
140
+ selectedCells.push(cell);
141
+ }
142
+ }
143
+
144
+ // Clear all existing selection border attributes
145
+ tableElement
146
+ .querySelectorAll(
147
+ "[data-selection-border-top], [data-selection-border-right], [data-selection-border-bottom], [data-selection-border-left]",
148
+ )
149
+ .forEach((cell) => {
150
+ cell.removeAttribute("data-selection-border-top");
151
+ cell.removeAttribute("data-selection-border-right");
152
+ cell.removeAttribute("data-selection-border-bottom");
153
+ cell.removeAttribute("data-selection-border-left");
154
+ });
155
+
156
+ if (selectedCells.length === 0) {
157
+ return;
158
+ }
159
+
160
+ // Convert NodeList to array and get cell positions
161
+
162
+ const cellPositions = selectedCells.map((cell) => {
163
+ const row = cell.parentElement;
164
+ const allRows = Array.from(tableElement.querySelectorAll(".navi_tr"));
165
+ return {
166
+ element: cell,
167
+ rowIndex: allRows.indexOf(row),
168
+ columnIndex: Array.from(row.children).indexOf(cell),
169
+ };
170
+ });
171
+
172
+ // Create a set for fast lookup of selected cell positions
173
+ const selectedPositions = new Set(
174
+ cellPositions.map((pos) => `${pos.rowIndex},${pos.columnIndex}`),
175
+ );
176
+
177
+ // Apply selection borders based on actual neighbors (for proper L-shaped selection support)
178
+ cellPositions.forEach(({ element, rowIndex, columnIndex }) => {
179
+ // Top border: if cell above is NOT selected or doesn't exist
180
+ const cellAbove = `${rowIndex - 1},${columnIndex}`;
181
+ if (!selectedPositions.has(cellAbove)) {
182
+ element.setAttribute("data-selection-border-top", "");
183
+ }
184
+
185
+ // Bottom border: if cell below is NOT selected or doesn't exist
186
+ const cellBelow = `${rowIndex + 1},${columnIndex}`;
187
+ if (!selectedPositions.has(cellBelow)) {
188
+ element.setAttribute("data-selection-border-bottom", "");
189
+ }
190
+
191
+ // Left border: if cell to the left is NOT selected or doesn't exist
192
+ const cellLeft = `${rowIndex},${columnIndex - 1}`;
193
+ if (!selectedPositions.has(cellLeft)) {
194
+ element.setAttribute("data-selection-border-left", "");
195
+ }
196
+
197
+ // Right border: if cell to the right is NOT selected or doesn't exist
198
+ const cellRight = `${rowIndex},${columnIndex + 1}`;
199
+ if (!selectedPositions.has(cellRight)) {
200
+ element.setAttribute("data-selection-border-right", "");
201
+ }
202
+ });
203
+ };
@@ -0,0 +1,354 @@
1
+ // TODO: move this to @jsenv/dom (the initStickyGroup part, not the useLayoutEffect)
2
+
3
+ import { createPubSub, setStyles } from "@jsenv/dom";
4
+ import { useLayoutEffect } from "preact/hooks";
5
+
6
+ // React hook version for easy integration
7
+ export const useStickyGroup = (
8
+ elementRef,
9
+ { elementReceivingCumulativeStickyPositionRef, elementSelector } = {},
10
+ ) => {
11
+ useLayoutEffect(() => {
12
+ const element = elementRef.current;
13
+ if (!element) {
14
+ return undefined;
15
+ }
16
+ return initStickyGroup(element, {
17
+ elementSelector,
18
+ elementReceivingCumulativeStickyPosition:
19
+ elementReceivingCumulativeStickyPositionRef.current,
20
+ });
21
+ }, [elementSelector]);
22
+ };
23
+
24
+ const ITEM_LEFT_VAR = "--sticky-group-item-left";
25
+ const ITEM_TOP_VAR = "--sticky-group-item-top";
26
+ const FRONTIER_LEFT_VAR = "--sticky-group-left";
27
+ const FRONTIER_TOP_VAR = "--sticky-group-top";
28
+ // const FRONTIER_LEFT_VIEWPORT_VAR = "--sticky-group-left-viewport";
29
+ // const FRONTIER_TOP_VIEWPORT_VAR = "--sticky-group-top-viewport";
30
+
31
+ /**
32
+ * Creates a sticky group that manages positioning for multiple sticky elements
33
+ * that need to be aware of each other's dimensions.
34
+ * Always uses CSS variables for positioning.
35
+ *
36
+ * @param {HTMLElement} container The container element
37
+ * @returns {Function} Cleanup function
38
+ */
39
+ const initStickyGroup = (
40
+ container,
41
+ { elementSelector, elementReceivingCumulativeStickyPosition } = {},
42
+ ) => {
43
+ if (!container) {
44
+ throw new Error("initStickyGroup: container is required");
45
+ }
46
+
47
+ const [teardown, addTeardown] = createPubSub();
48
+ const [cleanup, addCleanup, clearCleanup] = createPubSub();
49
+ addTeardown(cleanup);
50
+
51
+ const element = elementSelector
52
+ ? container.querySelector(elementSelector)
53
+ : container;
54
+ const isGrid =
55
+ element.tagName === "TABLE" || element.classList.contains("navi_table");
56
+ const updatePositions = () => {
57
+ // Clear all previous CSS variable cleanups before setting new ones
58
+ cleanup();
59
+ clearCleanup();
60
+
61
+ if (isGrid) {
62
+ updateGridPositions();
63
+ } else {
64
+ updateLinearPositions();
65
+ }
66
+ };
67
+
68
+ const updateGridPositions = () => {
69
+ // Handle table grid - update both horizontal and vertical sticky elements
70
+ updateTableColumns();
71
+ updateTableRows();
72
+ };
73
+ const updateTableColumns = () => {
74
+ // Find all sticky columns by checking all rows to identify which columns have sticky cells
75
+ const allStickyColumnCells = element.querySelectorAll(
76
+ ".navi_table_cell[data-sticky-left]",
77
+ );
78
+ if (allStickyColumnCells.length === 0) {
79
+ return;
80
+ }
81
+
82
+ // Get the first row to determine column indices (use any row that exists)
83
+ const firstRow = element.querySelector(".navi_tr");
84
+ if (!firstRow) {
85
+ return;
86
+ }
87
+
88
+ // Group sticky cells by column index
89
+ const stickyColumnsByIndex = new Map();
90
+ allStickyColumnCells.forEach((cell) => {
91
+ const row = cell.closest(".navi_tr");
92
+ const columnIndex = Array.from(row.children).indexOf(cell);
93
+ if (!stickyColumnsByIndex.has(columnIndex)) {
94
+ stickyColumnsByIndex.set(columnIndex, []);
95
+ }
96
+ stickyColumnsByIndex.get(columnIndex).push(cell);
97
+ });
98
+
99
+ // Sort columns by index and process them
100
+ const sortedColumnIndices = Array.from(stickyColumnsByIndex.keys()).sort(
101
+ (a, b) => a - b,
102
+ );
103
+ let cumulativeWidth = 0;
104
+
105
+ sortedColumnIndices.forEach((columnIndex, stickyIndex) => {
106
+ const cellsInColumn = stickyColumnsByIndex.get(columnIndex);
107
+ const leftPosition = stickyIndex === 0 ? 0 : cumulativeWidth;
108
+
109
+ // Set CSS variable on all sticky cells in this column using setStyles for proper cleanup
110
+ cellsInColumn.forEach((cell) => {
111
+ const restoreStyles = setStyles(cell, {
112
+ [ITEM_LEFT_VAR]: `${leftPosition}px`,
113
+ });
114
+ addCleanup(restoreStyles);
115
+ });
116
+
117
+ // Also set CSS variable on corresponding <col> element if it exists
118
+ const colgroup = element.querySelector(".navi_colgroup");
119
+ if (colgroup) {
120
+ const colElements = Array.from(colgroup.querySelectorAll(".navi_col"));
121
+ const correspondingCol = colElements[columnIndex];
122
+ if (correspondingCol) {
123
+ const restoreStyles = setStyles(correspondingCol, {
124
+ [ITEM_LEFT_VAR]: `${leftPosition}px`,
125
+ });
126
+ addCleanup(restoreStyles);
127
+ }
128
+ }
129
+
130
+ // Update cumulative width for next column using the first cell in this column as reference
131
+ const referenceCell = cellsInColumn[0];
132
+ const columnWidth = referenceCell.getBoundingClientRect().width;
133
+ if (stickyIndex === 0) {
134
+ cumulativeWidth = columnWidth;
135
+ } else {
136
+ cumulativeWidth += columnWidth;
137
+ }
138
+ });
139
+
140
+ // Set frontier variables with proper cleanup tracking
141
+ const restoreContainerStyles = setStyles(container, {
142
+ [FRONTIER_LEFT_VAR]: `${cumulativeWidth}px`,
143
+ });
144
+ addCleanup(restoreContainerStyles);
145
+
146
+ if (elementReceivingCumulativeStickyPosition) {
147
+ const restoreCumulativeStyles = setStyles(
148
+ elementReceivingCumulativeStickyPosition,
149
+ {
150
+ [FRONTIER_LEFT_VAR]: `${cumulativeWidth}px`,
151
+ },
152
+ );
153
+ addCleanup(restoreCumulativeStyles);
154
+ }
155
+ };
156
+ const updateTableRows = () => {
157
+ // Handle sticky rows by finding cells with data-sticky-top and grouping by row
158
+ const stickyCells = element.querySelectorAll(
159
+ ".navi_table_cell[data-sticky-top]",
160
+ );
161
+ if (stickyCells.length === 0) {
162
+ return;
163
+ }
164
+
165
+ // Group cells by their parent row
166
+ const rowsWithStickyCells = new Map();
167
+ stickyCells.forEach((cell) => {
168
+ const row = cell.parentElement;
169
+ if (!rowsWithStickyCells.has(row)) {
170
+ rowsWithStickyCells.set(row, []);
171
+ }
172
+ rowsWithStickyCells.get(row).push(cell);
173
+ });
174
+
175
+ // Convert to array and sort by row position in DOM
176
+ const allRows = Array.from(element.querySelectorAll(".navi_tr"));
177
+ const stickyRows = Array.from(rowsWithStickyCells.keys()).sort((a, b) => {
178
+ const aIndex = allRows.indexOf(a);
179
+ const bIndex = allRows.indexOf(b);
180
+ return aIndex - bIndex;
181
+ });
182
+
183
+ let cumulativeHeight = 0;
184
+ stickyRows.forEach((row, index) => {
185
+ const rowCells = rowsWithStickyCells.get(row);
186
+ const topPosition = index === 0 ? 0 : cumulativeHeight;
187
+
188
+ // Set CSS variable on all sticky cells in this row using setStyles for proper cleanup
189
+ rowCells.forEach((cell) => {
190
+ const restoreStyles = setStyles(cell, {
191
+ [ITEM_TOP_VAR]: `${topPosition}px`,
192
+ });
193
+ addCleanup(restoreStyles);
194
+ });
195
+
196
+ // Also set CSS variable on the <tr> element itself
197
+ const restoreRowStyles = setStyles(row, {
198
+ [ITEM_TOP_VAR]: `${topPosition}px`,
199
+ });
200
+ addCleanup(restoreRowStyles);
201
+
202
+ // Update cumulative height for next row
203
+ const rowHeight = row.getBoundingClientRect().height;
204
+ if (index === 0) {
205
+ cumulativeHeight = rowHeight;
206
+ } else {
207
+ cumulativeHeight += rowHeight;
208
+ }
209
+ });
210
+
211
+ // Set frontier variables with proper cleanup tracking
212
+ const restoreContainerStyles = setStyles(container, {
213
+ [FRONTIER_TOP_VAR]: `${cumulativeHeight}px`,
214
+ });
215
+ addCleanup(restoreContainerStyles);
216
+
217
+ if (elementReceivingCumulativeStickyPosition) {
218
+ const restoreCumulativeStyles = setStyles(
219
+ elementReceivingCumulativeStickyPosition,
220
+ {
221
+ [FRONTIER_TOP_VAR]: `${cumulativeHeight}px`,
222
+ },
223
+ );
224
+ addCleanup(restoreCumulativeStyles);
225
+ }
226
+ };
227
+
228
+ const updateLinearPositions = () => {
229
+ // Handle linear container - detect direction from first sticky element
230
+ const stickyElements = element.querySelectorAll(
231
+ "[data-sticky-left], [data-sticky-top]",
232
+ );
233
+ if (stickyElements.length <= 1) return;
234
+
235
+ const firstElement = stickyElements[0];
236
+ const isHorizontal = firstElement.hasAttribute("data-sticky-left");
237
+ const dimensionProperty = isHorizontal ? "width" : "height";
238
+ const cssVariableName = isHorizontal ? ITEM_LEFT_VAR : ITEM_TOP_VAR;
239
+
240
+ let cumulativeSize = 0;
241
+ stickyElements.forEach((element, index) => {
242
+ if (index === 0) {
243
+ // First element stays at position 0
244
+ const restoreStyles = setStyles(element, {
245
+ [cssVariableName]: "0px",
246
+ });
247
+ addCleanup(restoreStyles);
248
+ cumulativeSize = element.getBoundingClientRect()[dimensionProperty];
249
+ } else {
250
+ // Subsequent elements use cumulative positioning
251
+ const position = cumulativeSize;
252
+ const restoreStyles = setStyles(element, {
253
+ [cssVariableName]: `${position}px`,
254
+ });
255
+ addCleanup(restoreStyles);
256
+ cumulativeSize += element.getBoundingClientRect()[dimensionProperty];
257
+ }
258
+ });
259
+
260
+ // Set frontier variables with proper cleanup tracking
261
+ const frontierVar = isHorizontal ? FRONTIER_LEFT_VAR : FRONTIER_TOP_VAR;
262
+ const restoreContainerStyles = setStyles(container, {
263
+ [frontierVar]: `${cumulativeSize}px`,
264
+ });
265
+ addCleanup(restoreContainerStyles);
266
+
267
+ if (elementReceivingCumulativeStickyPosition) {
268
+ const restoreCumulativeStyles = setStyles(
269
+ elementReceivingCumulativeStickyPosition,
270
+ {
271
+ [frontierVar]: `${cumulativeSize}px`,
272
+ },
273
+ );
274
+ addCleanup(restoreCumulativeStyles);
275
+ }
276
+ };
277
+
278
+ // Initial positioning
279
+ updatePositions();
280
+
281
+ // Set up ResizeObserver to handle size changes
282
+ const resizeObserver = new ResizeObserver(() => {
283
+ updatePositions();
284
+ });
285
+
286
+ // Set up MutationObserver to handle DOM changes
287
+ const mutationObserver = new MutationObserver((mutations) => {
288
+ let shouldUpdate = false;
289
+
290
+ mutations.forEach((mutation) => {
291
+ // Check if sticky elements were added/removed or attributes changed
292
+ if (mutation.type === "childList") {
293
+ shouldUpdate = true;
294
+ }
295
+ if (mutation.type === "attributes") {
296
+ // Check if the mutation affects sticky attributes
297
+ if (
298
+ mutation.attributeName === "data-sticky-left" ||
299
+ mutation.attributeName === "data-sticky-top"
300
+ ) {
301
+ shouldUpdate = true;
302
+ }
303
+ }
304
+ });
305
+
306
+ if (shouldUpdate) {
307
+ updatePositions();
308
+ }
309
+ });
310
+
311
+ // Start observing
312
+ resizeObserver.observe(element);
313
+ addTeardown(() => {
314
+ resizeObserver.disconnect();
315
+ });
316
+
317
+ mutationObserver.observe(element, {
318
+ attributes: true,
319
+ childList: true,
320
+ subtree: true,
321
+ attributeFilter: ["data-sticky-left", "data-sticky-top"],
322
+ });
323
+ addTeardown(() => {
324
+ mutationObserver.disconnect();
325
+ });
326
+
327
+ // Return cleanup function
328
+ return () => {
329
+ teardown();
330
+ };
331
+ };
332
+
333
+ // const visualPositionEffect = (element, callback) => {
334
+ // const updatePosition = () => {
335
+ // const { left, top } = getVisualRect(element, document.body, {
336
+ // isStickyLeft: false,
337
+ // isStickyTop: false,
338
+ // });
339
+ // callback({ left, top });
340
+ // };
341
+ // updatePosition();
342
+
343
+ // window.addEventListener("scroll", updatePosition, { passive: true });
344
+ // window.addEventListener("resize", updatePosition);
345
+ // window.addEventListener("touchmove", updatePosition);
346
+
347
+ // return () => {
348
+ // window.removeEventListener("scroll", updatePosition, {
349
+ // passive: true,
350
+ // });
351
+ // window.removeEventListener("resize", updatePosition);
352
+ // window.removeEventListener("touchmove", updatePosition);
353
+ // };
354
+ // };
@@ -0,0 +1,25 @@
1
+ import { createContext } from "preact";
2
+ import { useMemo } from "preact/hooks";
3
+
4
+ import { useStableCallback } from "../../use_stable_callback.js";
5
+
6
+ export const TableStickyContext = createContext();
7
+
8
+ export const useTableStickyContextValue = ({
9
+ stickyLeftFrontierColumnIndex,
10
+ stickyTopFrontierRowIndex,
11
+ onStickyLeftFrontierChange,
12
+ onStickyTopFrontierChange,
13
+ }) => {
14
+ onStickyLeftFrontierChange = useStableCallback(onStickyLeftFrontierChange);
15
+ onStickyTopFrontierChange = useStableCallback(onStickyTopFrontierChange);
16
+
17
+ return useMemo(() => {
18
+ return {
19
+ stickyLeftFrontierColumnIndex,
20
+ stickyTopFrontierRowIndex,
21
+ onStickyLeftFrontierChange,
22
+ onStickyTopFrontierChange,
23
+ };
24
+ }, [stickyLeftFrontierColumnIndex, stickyTopFrontierRowIndex]);
25
+ };