flexdesk 0.16.0 → 0.17.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 (251) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/LICENSE +0 -0
  3. package/README.md +2261 -42
  4. package/THIRD_PARTY_LICENSES.md +0 -0
  5. package/css/base.css +1834 -75
  6. package/css/flexdesk.css +3373 -238
  7. package/css/overrides.css +16 -0
  8. package/css/reset.css +0 -0
  9. package/css/tokens.css +0 -0
  10. package/dist/canvas.js +6 -239
  11. package/dist/canvas.js.map +3 -3
  12. package/dist/charts.js +709 -18
  13. package/dist/charts.js.map +4 -4
  14. package/dist/chunk-3PHCPZHT.js +0 -0
  15. package/dist/chunk-3PHCPZHT.js.map +0 -0
  16. package/dist/chunk-4A7SJC3T.js +503 -0
  17. package/dist/chunk-4A7SJC3T.js.map +7 -0
  18. package/dist/{chunk-6JSOVNID.js → chunk-CVMCZZX2.js} +635 -10
  19. package/dist/chunk-CVMCZZX2.js.map +7 -0
  20. package/dist/chunk-D3COLZZ2.js +1311 -0
  21. package/dist/chunk-D3COLZZ2.js.map +7 -0
  22. package/dist/chunk-FOOS3T5L.js +0 -0
  23. package/dist/chunk-FOOS3T5L.js.map +0 -0
  24. package/dist/{chunk-SN4LSOAR.js → chunk-GOQLR5WR.js} +452 -2
  25. package/dist/chunk-GOQLR5WR.js.map +7 -0
  26. package/dist/{chunk-FL5KFNQH.js → chunk-HNU6MM2B.js} +52 -17
  27. package/dist/chunk-HNU6MM2B.js.map +7 -0
  28. package/dist/chunk-HYJYYEPK.js +375 -0
  29. package/dist/chunk-HYJYYEPK.js.map +7 -0
  30. package/dist/chunk-JKZLNKG6.js +2041 -0
  31. package/dist/chunk-JKZLNKG6.js.map +7 -0
  32. package/dist/chunk-JYWURG5T.js +0 -0
  33. package/dist/chunk-JYWURG5T.js.map +0 -0
  34. package/dist/{chunk-RQM5Z2MU.js → chunk-MCIXFNVF.js} +633 -311
  35. package/dist/chunk-MCIXFNVF.js.map +7 -0
  36. package/dist/chunk-N7HC3ADV.js +30 -0
  37. package/dist/chunk-N7HC3ADV.js.map +7 -0
  38. package/dist/{chunk-FBGP5KN5.js → chunk-Q36EPT2R.js} +3268 -203
  39. package/dist/chunk-Q36EPT2R.js.map +7 -0
  40. package/dist/chunk-QWQJUDPD.js +610 -0
  41. package/dist/chunk-QWQJUDPD.js.map +7 -0
  42. package/dist/{chunk-NLTOY57M.js → chunk-UURZ37SD.js} +28 -48
  43. package/dist/chunk-UURZ37SD.js.map +7 -0
  44. package/dist/chunk-UWO43BZU.js +111 -0
  45. package/dist/chunk-UWO43BZU.js.map +7 -0
  46. package/dist/chunk-WVFGV5FT.js +0 -0
  47. package/dist/chunk-WVFGV5FT.js.map +0 -0
  48. package/dist/core.js +170 -118
  49. package/dist/core.js.map +2 -2
  50. package/dist/editor.js +156 -70
  51. package/dist/editor.js.map +2 -2
  52. package/dist/flexdesk.css +3373 -238
  53. package/dist/flow.js +2789 -306
  54. package/dist/flow.js.map +4 -4
  55. package/dist/host.js +109 -3
  56. package/dist/host.js.map +3 -3
  57. package/dist/reset.css +0 -0
  58. package/dist/tile_registry-6WZPTUZV.js +0 -0
  59. package/dist/tile_registry-6WZPTUZV.js.map +0 -0
  60. package/dist/tiles.js +1131 -253
  61. package/dist/tiles.js.map +4 -4
  62. package/dist/tokens.css +0 -0
  63. package/dist/widgets.js +3254 -2112
  64. package/dist/widgets.js.map +4 -4
  65. package/dist/wm.js +1227 -345
  66. package/dist/wm.js.map +4 -4
  67. package/package.json +3 -1
  68. package/src/canvas/canvas_adapter.js +5 -0
  69. package/src/canvas/edge_router.js +0 -0
  70. package/src/canvas/graph/connector_router.js +0 -0
  71. package/src/canvas/graph/history_service.js +0 -0
  72. package/src/canvas/graph/node_platform.js +0 -0
  73. package/src/canvas/index.js +1 -0
  74. package/src/canvas/layered_layout.js +244 -0
  75. package/src/charts/chart_types.js +0 -0
  76. package/src/charts/data_series_plot_window.js +0 -0
  77. package/src/charts/downsample.js +2 -2
  78. package/src/charts/plot_config.js +53 -29
  79. package/src/charts/plot_config_editor.js +627 -0
  80. package/src/charts/plot_grid.js +449 -0
  81. package/src/charts/plot_popout_window.js +103 -317
  82. package/src/charts/plotly_wrapper.js +22 -2
  83. package/src/charts/series_transforms.js +206 -0
  84. package/src/core/event_bus.js +0 -0
  85. package/src/core/logging.js +1 -1
  86. package/src/core/settings.js +105 -19
  87. package/src/core/state_guard.js +93 -51
  88. package/src/core/state_guard_config.js +85 -74
  89. package/src/core/state_machine.js +0 -0
  90. package/src/editor/editor_pane.js +3 -3
  91. package/src/editor/monaco_editor_factory.js +124 -60
  92. package/src/editor/monaco_loader.js +103 -28
  93. package/src/editor/notebook_search.js +0 -0
  94. package/src/editor/notebook_tab_bar.js +38 -52
  95. package/src/editor/notebook_undo_manager.js +0 -0
  96. package/src/editor/split_pane_container.js +6 -3
  97. package/src/flow/canvas_editor/connections.js +90 -0
  98. package/src/flow/canvas_editor/editor.js +2351 -153
  99. package/src/flow/canvas_editor/index.js +2 -1
  100. package/src/flow/canvas_editor/layout.js +119 -12
  101. package/src/flow/canvas_editor/strings.js +80 -0
  102. package/src/flow/kit/always_before.js +0 -0
  103. package/src/flow/kit/catalogue.js +97 -3
  104. package/src/flow/kit/chip_input.js +0 -0
  105. package/src/flow/kit/dom.js +0 -0
  106. package/src/flow/kit/findings.js +0 -0
  107. package/src/flow/kit/history.js +0 -0
  108. package/src/flow/kit/index.js +3 -3
  109. package/src/flow/kit/keys.js +7 -1
  110. package/src/flow/kit/logic_graph.js +97 -15
  111. package/src/flow/kit/references.js +0 -0
  112. package/src/flow/kit/settings_panel.js +0 -0
  113. package/src/flow/kit/settings_schema.js +2 -1
  114. package/src/flow/kit/step_picker.js +0 -0
  115. package/src/flow/kit/strings.js +3 -1
  116. package/src/flow/kit/value_picker.js +0 -0
  117. package/src/flow/kit/widgets.js +86 -109
  118. package/src/flow/lanes/editor.js +136 -14
  119. package/src/flow/lanes/index.js +3 -1
  120. package/src/flow/lanes/layout.js +0 -0
  121. package/src/flow/lanes/pipeline.js +44 -2
  122. package/src/flow/lanes/preview.js +0 -0
  123. package/src/flow/lanes/strings.js +1 -0
  124. package/src/flow/outline/editor.js +0 -0
  125. package/src/flow/outline/index.js +0 -0
  126. package/src/flow/outline/mapping.js +0 -0
  127. package/src/flow/outline/menu.js +0 -0
  128. package/src/flow/outline/operations.js +0 -0
  129. package/src/flow/outline/recognise.js +0 -0
  130. package/src/flow/outline/strings.js +0 -0
  131. package/src/help/help_modal.js +168 -42
  132. package/src/help/help_registry.js +12 -1
  133. package/src/host/external_links.js +0 -0
  134. package/src/host/file_picker.js +117 -0
  135. package/src/host/host.js +10 -2
  136. package/src/host/photino_host.js +9 -0
  137. package/src/host/photino_host.md +21 -3
  138. package/src/host/pywebview_host.js +10 -0
  139. package/src/tiles/chart_tiles.js +53 -245
  140. package/src/tiles/config_schema.js +8 -0
  141. package/src/tiles/figure_tile.js +322 -0
  142. package/src/tiles/layout_persistence.js +3 -3
  143. package/src/tiles/list_tiles.js +422 -0
  144. package/src/tiles/scope_picker.js +96 -0
  145. package/src/tiles/tile_base.js +12 -1
  146. package/src/tiles/tile_config_form.js +442 -0
  147. package/src/tiles/tile_grid.js +38 -1
  148. package/src/tiles/tile_registry.js +0 -0
  149. package/src/tiling/bottom_panel_tabs.js +67 -99
  150. package/src/tiling/command_palette.js +64 -19
  151. package/src/tiling/content_registry.js +4 -1
  152. package/src/tiling/desktops.js +11 -1
  153. package/src/tiling/entity_sources.js +0 -0
  154. package/src/tiling/keymap.js +403 -128
  155. package/src/tiling/kind_taxonomy.js +78 -0
  156. package/src/tiling/landing_table.js +0 -0
  157. package/src/tiling/loading_overlay.js +0 -0
  158. package/src/tiling/nav_panel.js +59 -10
  159. package/src/tiling/page_factory.js +5 -5
  160. package/src/tiling/panel_availability.js +49 -0
  161. package/src/tiling/panel_keys.js +0 -0
  162. package/src/tiling/shell.js +114 -12
  163. package/src/tiling/tab_strip.js +0 -0
  164. package/src/tiling/tile_breadcrumb.js +0 -0
  165. package/src/tiling/tile_renderer.js +105 -9
  166. package/src/tiling/tile_tab_menu.js +0 -0
  167. package/src/tiling/tile_tree.js +0 -0
  168. package/src/tiling/wm.js +562 -30
  169. package/src/tiling/zoom.js +0 -0
  170. package/src/ui/base/component_base.js +0 -0
  171. package/src/ui/base/controller_base.js +0 -0
  172. package/src/ui/base/page_base.js +0 -0
  173. package/src/ui/components/about_dialog.js +0 -0
  174. package/src/ui/components/action_dropdown.js +30 -2
  175. package/src/ui/components/alert_log.js +0 -0
  176. package/src/ui/components/alerts_pane.js +0 -0
  177. package/src/ui/components/attribute_list_editor.js +0 -0
  178. package/src/ui/components/autocomplete_field.js +0 -0
  179. package/src/ui/components/cell_editor_host.js +211 -0
  180. package/src/ui/components/cell_grid.js +648 -0
  181. package/src/ui/components/cell_selection.js +220 -0
  182. package/src/ui/components/choice_controls.js +351 -0
  183. package/src/ui/components/color_field.js +211 -0
  184. package/src/ui/components/color_list_field.js +210 -0
  185. package/src/ui/components/column_fit.js +0 -0
  186. package/src/ui/components/column_groups.js +314 -0
  187. package/src/ui/components/column_order.js +142 -0
  188. package/src/ui/components/computing_status_window.js +98 -25
  189. package/src/ui/components/confirm_dialog.js +81 -38
  190. package/src/ui/components/context_menu.js +2 -2
  191. package/src/ui/components/conversation_view.js +440 -0
  192. package/src/ui/components/crosstab.js +289 -0
  193. package/src/ui/components/data_table.js +601 -62
  194. package/src/ui/components/detail_header.js +22 -11
  195. package/src/ui/components/document_outline.js +397 -0
  196. package/src/ui/components/drag_reorder.js +0 -0
  197. package/src/ui/components/fact_table.js +0 -0
  198. package/src/ui/components/fill_handle.js +322 -0
  199. package/src/ui/components/gallery_picker.js +19 -8
  200. package/src/ui/components/header_actions.js +55 -0
  201. package/src/ui/components/inline_renamer.js +0 -0
  202. package/src/ui/components/job_progress.js +51 -0
  203. package/src/ui/components/managed_window.js +27 -4
  204. package/src/ui/components/menu_bar.js +745 -0
  205. package/src/ui/components/modal.js +449 -75
  206. package/src/ui/components/notification_history.js +3 -3
  207. package/src/ui/components/progress.js +483 -0
  208. package/src/ui/components/record_list.js +0 -0
  209. package/src/ui/components/row_activation.js +0 -0
  210. package/src/ui/components/section_stack.js +298 -0
  211. package/src/ui/components/series.js +173 -0
  212. package/src/ui/components/settings_view.js +634 -0
  213. package/src/ui/components/slide_out_panel.js +43 -6
  214. package/src/ui/components/slider_field.js +275 -18
  215. package/src/ui/components/sortable_list.js +26 -4
  216. package/src/ui/components/staged_edits.js +343 -0
  217. package/src/ui/components/staged_history.js +172 -0
  218. package/src/ui/components/table_filter_chips.js +0 -0
  219. package/src/ui/components/table_state_store.js +0 -0
  220. package/src/ui/components/table_structure.js +182 -0
  221. package/src/ui/components/table_values.js +0 -0
  222. package/src/ui/components/tabs.js +497 -0
  223. package/src/ui/components/tag_input.js +215 -0
  224. package/src/ui/components/toast.js +25 -8
  225. package/src/ui/components/toggle_field.js +175 -0
  226. package/src/ui/components/tree_view.js +55 -34
  227. package/src/ui/components/window_taskbar.js +0 -0
  228. package/src/ui/components/wizard.js +563 -0
  229. package/src/ui/controllers/window_chrome_controller.js +2 -2
  230. package/src/ui/notification_center.js +375 -119
  231. package/src/ui/utils/auto_scrollbars.js +0 -0
  232. package/src/ui/utils/markdown.js +709 -0
  233. package/src/ui/utils/native_context_menu.js +0 -0
  234. package/src/ui/utils/overlay_scrollbar.js +16 -12
  235. package/src/ui/utils/raf_resize_observer.js +0 -0
  236. package/src/ui/utils/scroll_lock.js +0 -0
  237. package/src/ui/utils/strip_scroll.js +55 -0
  238. package/src/ui/utils/tooltip_service.js +0 -8
  239. package/dist/chunk-6JSOVNID.js.map +0 -7
  240. package/dist/chunk-CWPMLX67.js +0 -475
  241. package/dist/chunk-CWPMLX67.js.map +0 -7
  242. package/dist/chunk-FBGP5KN5.js.map +0 -7
  243. package/dist/chunk-FL5KFNQH.js.map +0 -7
  244. package/dist/chunk-NLTOY57M.js.map +0 -7
  245. package/dist/chunk-NNFTETK3.js +0 -633
  246. package/dist/chunk-NNFTETK3.js.map +0 -7
  247. package/dist/chunk-O5OHMWBB.js +0 -284
  248. package/dist/chunk-O5OHMWBB.js.map +0 -7
  249. package/dist/chunk-RQM5Z2MU.js.map +0 -7
  250. package/dist/chunk-SN4LSOAR.js.map +0 -7
  251. package/src/ui/controllers/panel_state_machine.js +0 -634
@@ -1,7 +1,368 @@
1
1
  import {
2
2
  isRowActivation,
3
3
  isRowControl
4
- } from "./chunk-6JSOVNID.js";
4
+ } from "./chunk-CVMCZZX2.js";
5
+
6
+ // src/ui/components/cell_editor_host.js
7
+ function createTextEditor({ value, initialText = null }) {
8
+ const input = document.createElement("input");
9
+ input.type = "text";
10
+ input.className = "twm-dt-editor";
11
+ input.spellcheck = false;
12
+ input.setAttribute("autocomplete", "off");
13
+ const start = value == null ? "" : String(value);
14
+ input.value = initialText != null ? String(initialText) : start;
15
+ return {
16
+ el: input,
17
+ getValue: () => initialText == null && input.value === start ? value : input.value,
18
+ focus: () => {
19
+ input.focus({ preventScroll: true });
20
+ const end = input.value.length;
21
+ try {
22
+ input.setSelectionRange(end, end);
23
+ } catch (_) {
24
+ }
25
+ }
26
+ };
27
+ }
28
+ function normalise(made) {
29
+ if (!made) return null;
30
+ const isNode = typeof Node !== "undefined" && made instanceof Node;
31
+ const el = isNode ? made : made.el;
32
+ if (!el || typeof Node !== "undefined" && !(el instanceof Node)) return null;
33
+ const own = isNode ? {} : made;
34
+ let getValue = typeof own.getValue === "function" ? own.getValue : null;
35
+ if (!getValue && el.matches?.("input, select, textarea")) getValue = () => el.value;
36
+ return {
37
+ el,
38
+ getValue,
39
+ focus: typeof own.focus === "function" ? own.focus : null,
40
+ destroy: typeof own.destroy === "function" ? own.destroy : null
41
+ };
42
+ }
43
+ var CellEditorHost = class {
44
+ /**
45
+ * @param {object} o
46
+ * @param {(session, value, move, reason) => void} o.onCommit after the
47
+ * cell is drawn as it was again; `move` is 'down', 'up', 'right', 'left'
48
+ * or null; `reason` is 'key', 'blur', 'editor' or 'scroll'.
49
+ * @param {(session, reason) => void} o.onCancel
50
+ */
51
+ constructor({ onCommit, onCancel }) {
52
+ this._onCommit = onCommit;
53
+ this._onCancel = onCancel;
54
+ this.session = null;
55
+ }
56
+ /**
57
+ * Open an editor in `td`. Returns whether one opened (a factory that
58
+ * throws or returns nothing opens none, and the cell is untouched).
59
+ */
60
+ open({ td, factory, value, row, rowIdx, colIdx, column, initialText = null, ...extra }) {
61
+ if (this.session) return false;
62
+ const s = {
63
+ ...extra,
64
+ td,
65
+ value,
66
+ row,
67
+ rowIdx,
68
+ colIdx,
69
+ column,
70
+ previous: value,
71
+ closed: false,
72
+ offs: [],
73
+ saved: null,
74
+ editor: null
75
+ };
76
+ let made = null;
77
+ try {
78
+ made = factory({
79
+ value,
80
+ row,
81
+ rowIdx,
82
+ colIdx,
83
+ column,
84
+ initialText,
85
+ commit: (v) => this._finish(s, v, null, "editor"),
86
+ cancel: () => this._cancel(s, "editor")
87
+ });
88
+ } catch (err) {
89
+ console.error("[DataTable] cell editor factory threw", err);
90
+ return false;
91
+ }
92
+ const editor = normalise(made);
93
+ if (!editor) return false;
94
+ s.editor = editor;
95
+ s.saved = [...td.childNodes];
96
+ s.savedTitle = td.getAttribute("title");
97
+ this.session = s;
98
+ td.classList.add("twm-dt-cell--editing");
99
+ td.replaceChildren(editor.el);
100
+ const on = (type, fn) => {
101
+ editor.el.addEventListener(type, fn);
102
+ s.offs.push(() => editor.el.removeEventListener(type, fn));
103
+ };
104
+ on("keydown", (ev) => {
105
+ if (ev.isComposing) return;
106
+ ev.stopPropagation();
107
+ if (ev.key === "Escape") {
108
+ ev.preventDefault();
109
+ this._cancel(s, "key");
110
+ } else if (ev.key === "Enter" && !ev.altKey && !ev.ctrlKey && !ev.metaKey && editor.getValue) {
111
+ ev.preventDefault();
112
+ this._finish(s, editor.getValue(), ev.shiftKey ? "up" : "down", "key");
113
+ } else if (ev.key === "Tab" && !ev.altKey && !ev.ctrlKey && !ev.metaKey && editor.getValue) {
114
+ ev.preventDefault();
115
+ this._finish(s, editor.getValue(), ev.shiftKey ? "left" : "right", "key");
116
+ }
117
+ });
118
+ for (const type of ["click", "dblclick", "mousedown", "contextmenu"]) {
119
+ on(type, (ev) => ev.stopPropagation());
120
+ }
121
+ on("focusout", (ev) => {
122
+ if (s.closed || !editor.getValue) return;
123
+ const to = ev.relatedTarget;
124
+ if (to && editor.el.contains(to)) return;
125
+ const doc = editor.el.ownerDocument;
126
+ if (!to && doc && typeof doc.hasFocus === "function" && !doc.hasFocus()) return;
127
+ this._finish(s, editor.getValue(), null, "blur");
128
+ });
129
+ try {
130
+ if (editor.focus) editor.focus();
131
+ else editor.el.focus?.({ preventScroll: true });
132
+ } catch (_) {
133
+ }
134
+ return true;
135
+ }
136
+ /** Commit the open session with what its editor holds. */
137
+ commit(move = null, reason = "editor") {
138
+ const s = this.session;
139
+ if (!s) return false;
140
+ if (!s.editor.getValue) {
141
+ this._cancel(s, reason);
142
+ return false;
143
+ }
144
+ return this._finish(s, s.editor.getValue(), move, reason);
145
+ }
146
+ /** Cancel the open session: the cell is drawn as it was. */
147
+ cancel(reason = "editor") {
148
+ const s = this.session;
149
+ if (!s) return false;
150
+ return this._cancel(s, reason);
151
+ }
152
+ /** Take the editor out and put the cell's own content back. */
153
+ _close(s) {
154
+ s.closed = true;
155
+ if (this.session === s) this.session = null;
156
+ for (const off of s.offs) {
157
+ try {
158
+ off();
159
+ } catch (_) {
160
+ }
161
+ }
162
+ s.offs = [];
163
+ try {
164
+ s.editor.destroy?.();
165
+ } catch (err) {
166
+ console.error("[DataTable] cell editor destroy threw", err);
167
+ }
168
+ s.td.classList.remove("twm-dt-cell--editing");
169
+ s.td.replaceChildren(...s.saved);
170
+ if (s.savedTitle == null) s.td.removeAttribute("title");
171
+ else s.td.setAttribute("title", s.savedTitle);
172
+ }
173
+ _finish(s, value, move, reason) {
174
+ if (s.closed) return false;
175
+ this._close(s);
176
+ this._onCommit?.(s, value, move, reason);
177
+ return true;
178
+ }
179
+ _cancel(s, reason) {
180
+ if (s.closed) return false;
181
+ this._close(s);
182
+ this._onCancel?.(s, reason);
183
+ return true;
184
+ }
185
+ };
186
+
187
+ // src/ui/components/cell_selection.js
188
+ var CellSelection = class {
189
+ constructor() {
190
+ this.anchor = null;
191
+ this.focus = null;
192
+ this.first = 0;
193
+ this.last = -1;
194
+ this.cols = [];
195
+ }
196
+ /** The rows (positions, inclusive) and columns (data indices, shown order)
197
+ * the cursor may stand on. A selection outside them is pulled in. */
198
+ setBounds(first, last, cols) {
199
+ this.first = first;
200
+ this.last = last;
201
+ this.cols = Array.isArray(cols) ? cols.slice() : [];
202
+ if (this.isEmptyGrid()) {
203
+ this.anchor = null;
204
+ this.focus = null;
205
+ return;
206
+ }
207
+ if (this.focus) this.focus = this._clamp(this.focus);
208
+ if (this.anchor) this.anchor = this._clamp(this.anchor);
209
+ }
210
+ isEmptyGrid() {
211
+ return this.last < this.first || this.cols.length === 0;
212
+ }
213
+ _ord(col) {
214
+ const i = this.cols.indexOf(col);
215
+ return i >= 0 ? i : 0;
216
+ }
217
+ _clamp({ row, col }) {
218
+ const r = Math.max(this.first, Math.min(this.last, row));
219
+ const c = this.cols.includes(col) ? col : this.cols[Math.max(0, Math.min(this.cols.length - 1, this._nearestOrd(col)))];
220
+ return { row: r, col: c };
221
+ }
222
+ /** A column that is not shown any more: the shown one nearest its index. */
223
+ _nearestOrd(col) {
224
+ let best = 0;
225
+ for (let i = 0; i < this.cols.length; i++) if (this.cols[i] <= col) best = i;
226
+ return best;
227
+ }
228
+ _inside(row, ord) {
229
+ return row >= this.first && row <= this.last && ord >= 0 && ord < this.cols.length;
230
+ }
231
+ /** Put the cursor on one cell and collapse the range onto it. */
232
+ setCursor(row, col) {
233
+ if (this.isEmptyGrid()) return false;
234
+ this.focus = this._clamp({ row, col });
235
+ this.anchor = { ...this.focus };
236
+ return true;
237
+ }
238
+ /** Move the cursor to a cell and keep the anchor: the range grows. */
239
+ extendTo(row, col) {
240
+ if (!this.anchor) return this.setCursor(row, col);
241
+ if (this.isEmptyGrid()) return false;
242
+ this.focus = this._clamp({ row, col });
243
+ return true;
244
+ }
245
+ _go(cell, extend) {
246
+ return extend ? this.extendTo(cell.row, cell.col) : this.setCursor(cell.row, cell.col);
247
+ }
248
+ /** One step: the arrows. At an edge the cursor stays put. */
249
+ move(dRow, dCol, { extend = false } = {}) {
250
+ if (!this.focus) return this.setCursor(this.first, this.cols[0]);
251
+ const ord = Math.max(0, Math.min(this.cols.length - 1, this._ord(this.focus.col) + dCol));
252
+ const row = Math.max(this.first, Math.min(this.last, this.focus.row + dRow));
253
+ return this._go({ row, col: this.cols[ord] }, extend);
254
+ }
255
+ /**
256
+ * Ctrl+arrow: to the edge of the block of filled cells, as a spreadsheet
257
+ * does. From a filled cell whose neighbour is filled, to the last filled
258
+ * cell of that run; otherwise to the next filled cell along the line, or
259
+ * the edge of the grid when there is none.
260
+ *
261
+ * @param {number} dRow -1, 0 or 1
262
+ * @param {number} dCol -1, 0 or 1
263
+ * @param {(row:number, col:number) => boolean} filled
264
+ */
265
+ jump(dRow, dCol, filled, { extend = false } = {}) {
266
+ if (!this.focus) return this.setCursor(this.first, this.cols[0]);
267
+ let r = this.focus.row;
268
+ let o = this._ord(this.focus.col);
269
+ const isFilled = (row, ord) => !!filled(row, this.cols[ord]);
270
+ if (this._inside(r + dRow, o + dCol)) {
271
+ if (isFilled(r, o) && isFilled(r + dRow, o + dCol)) {
272
+ while (this._inside(r + dRow, o + dCol) && isFilled(r + dRow, o + dCol)) {
273
+ r += dRow;
274
+ o += dCol;
275
+ }
276
+ } else {
277
+ r += dRow;
278
+ o += dCol;
279
+ while (!isFilled(r, o) && this._inside(r + dRow, o + dCol)) {
280
+ r += dRow;
281
+ o += dCol;
282
+ }
283
+ }
284
+ }
285
+ return this._go({ row: r, col: this.cols[o] }, extend);
286
+ }
287
+ /** Home / End: the first or last column of the cursor's row; with
288
+ * `corner` (Ctrl), the first or last cell of the grid. */
289
+ lineEnd(toEnd, { extend = false, corner = false } = {}) {
290
+ if (!this.focus) return this.setCursor(this.first, this.cols[0]);
291
+ const col = toEnd ? this.cols[this.cols.length - 1] : this.cols[0];
292
+ const row = corner ? toEnd ? this.last : this.first : this.focus.row;
293
+ return this._go({ row, col }, extend);
294
+ }
295
+ /** Tab: one cell right, wrapping to the first cell of the next row (Shift:
296
+ * left, wrapping to the last cell of the row before). The last cell of
297
+ * the grid (the first, going back) stays put. */
298
+ tab(back = false) {
299
+ if (!this.focus) return this.setCursor(this.first, this.cols[0]);
300
+ let row = this.focus.row;
301
+ let ord = this._ord(this.focus.col) + (back ? -1 : 1);
302
+ if (ord >= this.cols.length) {
303
+ if (row >= this.last) return this.setCursor(row, this.focus.col);
304
+ row += 1;
305
+ ord = 0;
306
+ } else if (ord < 0) {
307
+ if (row <= this.first) return this.setCursor(row, this.focus.col);
308
+ row -= 1;
309
+ ord = this.cols.length - 1;
310
+ }
311
+ return this.setCursor(row, this.cols[ord]);
312
+ }
313
+ /** Whether Tab (Shift+Tab with `back`) has nowhere to go: the cursor is
314
+ * on the grid's last cell (its first, going back). The grid then lets
315
+ * the key move focus on, out of the table. */
316
+ tabEnds(back = false) {
317
+ if (!this.focus || this.isEmptyGrid()) return false;
318
+ const col = back ? this.cols[0] : this.cols[this.cols.length - 1];
319
+ const row = back ? this.first : this.last;
320
+ return this.focus.row === row && this.focus.col === col;
321
+ }
322
+ /** Every cell of the grid: from its first cell to its last, which is
323
+ * where the cursor goes. */
324
+ selectAll() {
325
+ if (this.isEmptyGrid()) return false;
326
+ this.anchor = { row: this.first, col: this.cols[0] };
327
+ this.focus = { row: this.last, col: this.cols[this.cols.length - 1] };
328
+ return true;
329
+ }
330
+ /** The range collapses onto the cursor. */
331
+ collapse() {
332
+ if (!this.focus) return false;
333
+ const was = this.anchor && (this.anchor.row !== this.focus.row || this.anchor.col !== this.focus.col);
334
+ this.anchor = { ...this.focus };
335
+ return !!was;
336
+ }
337
+ clear() {
338
+ this.anchor = null;
339
+ this.focus = null;
340
+ }
341
+ /** `{top, bottom, left, right, columns}` — rows as positions, `left` and
342
+ * `right` and `columns` as data indices — or null with nothing selected. */
343
+ range() {
344
+ if (!this.focus || !this.anchor) return null;
345
+ const a = this._ord(this.anchor.col);
346
+ const b = this._ord(this.focus.col);
347
+ const lo = Math.min(a, b);
348
+ const hi = Math.max(a, b);
349
+ return {
350
+ top: Math.min(this.anchor.row, this.focus.row),
351
+ bottom: Math.max(this.anchor.row, this.focus.row),
352
+ left: this.cols[lo],
353
+ right: this.cols[hi],
354
+ columns: this.cols.slice(lo, hi + 1)
355
+ };
356
+ }
357
+ contains(row, col) {
358
+ const r = this.range();
359
+ if (!r || row < r.top || row > r.bottom) return false;
360
+ return r.columns.includes(col);
361
+ }
362
+ isCursor(row, col) {
363
+ return !!this.focus && this.focus.row === row && this.focus.col === col;
364
+ }
365
+ };
5
366
 
6
367
  // src/ui/utils/raf_resize_observer.js
7
368
  function createRafResizeObserver(callback) {
@@ -355,116 +716,2403 @@ function matchDurationFilter(ms, filterText, shown) {
355
716
  return ms !== target;
356
717
  }
357
718
  }
358
- return String(shown ?? "").toLowerCase().includes(text.toLowerCase());
359
- }
719
+ return String(shown ?? "").toLowerCase().includes(text.toLowerCase());
720
+ }
721
+
722
+ // src/ui/components/table_filter_chips.js
723
+ var isAll = (v) => v === "" || v == null;
724
+ function createChipsFilter(ctx = {}, spec = {}) {
725
+ const options = Array.isArray(spec.options) ? spec.options : [];
726
+ const multiple = !!spec.multiple;
727
+ const showCounts = spec.counts !== false;
728
+ const el = document.createElement("div");
729
+ el.className = "twm-dt-filter-chips" + (multiple ? " twm-dt-filter-chips--multiple" : "");
730
+ el.setAttribute("role", "group");
731
+ const name = spec.label || ctx.header;
732
+ if (name) el.setAttribute("aria-label", String(name));
733
+ const valueOf = /* @__PURE__ */ new Map();
734
+ const chips = /* @__PURE__ */ new Map();
735
+ let value = null;
736
+ const keyOf = (v) => String(v ?? "");
737
+ const normalise2 = (v) => {
738
+ if (multiple) {
739
+ const list = Array.isArray(v) ? v : isAll(v) ? [] : [v];
740
+ return list.filter((x) => !isAll(x)).map(keyOf);
741
+ }
742
+ if (Array.isArray(v)) v = v[0];
743
+ return isAll(v) ? null : keyOf(v);
744
+ };
745
+ const chosen = () => multiple ? value.length === 0 : value === null;
746
+ for (const opt of options) {
747
+ const key3 = keyOf(opt.value);
748
+ valueOf.set(key3, opt.value);
749
+ const btn = document.createElement("button");
750
+ btn.type = "button";
751
+ btn.className = "twm-dt-filter-chip";
752
+ btn.dataset.value = key3;
753
+ if (opt.title) btn.title = String(opt.title);
754
+ if (opt.icon) {
755
+ const icon = document.createElement("span");
756
+ icon.className = "material-symbols-outlined twm-dt-filter-chip__icon";
757
+ icon.setAttribute("aria-hidden", "true");
758
+ icon.textContent = String(opt.icon);
759
+ btn.appendChild(icon);
760
+ }
761
+ const label = document.createElement("span");
762
+ label.className = "twm-dt-filter-chip__label";
763
+ label.textContent = String(opt.label ?? key3);
764
+ btn.appendChild(label);
765
+ let count = null;
766
+ if (showCounts) {
767
+ count = document.createElement("span");
768
+ count.className = "twm-dt-filter-chip__count";
769
+ count.textContent = "\u2013";
770
+ btn.appendChild(count);
771
+ }
772
+ btn.addEventListener("click", (ev) => {
773
+ ev.stopPropagation();
774
+ pick(key3);
775
+ });
776
+ chips.set(key3, { btn, count });
777
+ el.appendChild(btn);
778
+ }
779
+ function outward() {
780
+ if (multiple) return value.length ? value.map((k) => valueOf.get(k)) : null;
781
+ return value === null ? null : valueOf.get(value);
782
+ }
783
+ function pick(key3) {
784
+ if (isAll(key3)) value = multiple ? [] : null;
785
+ else if (multiple) {
786
+ value = value.includes(key3) ? value.filter((k) => k !== key3) : [...value, key3];
787
+ } else {
788
+ value = value === key3 ? null : key3;
789
+ }
790
+ paint();
791
+ const out = outward();
792
+ if (typeof ctx.setValue === "function") ctx.setValue(out);
793
+ try {
794
+ spec.onChange?.(out);
795
+ } catch (err) {
796
+ console.error("[chips filter] onChange threw", err);
797
+ }
798
+ }
799
+ function paint() {
800
+ for (const [key3, { btn }] of chips) {
801
+ const on = isAll(key3) ? chosen() : multiple ? value.includes(key3) : value === key3;
802
+ btn.classList.toggle("twm-dt-filter-chip--on", on);
803
+ btn.setAttribute("aria-pressed", on ? "true" : "false");
804
+ }
805
+ }
806
+ function setValue(v) {
807
+ value = normalise2(v);
808
+ paint();
809
+ }
810
+ function setCounts(counts) {
811
+ for (const [key3, { btn, count }] of chips) {
812
+ const n = counts == null ? void 0 : counts[key3];
813
+ const known = typeof n === "number" && Number.isFinite(n);
814
+ if (count) count.textContent = known ? n.toLocaleString() : "\u2013";
815
+ btn.classList.toggle("twm-dt-filter-chip--empty", known && n === 0);
816
+ }
817
+ }
818
+ setValue(ctx.value);
819
+ if (ctx.counts) setCounts(ctx.counts);
820
+ return {
821
+ el,
822
+ setValue,
823
+ setCounts,
824
+ getValue: outward,
825
+ destroy() {
826
+ }
827
+ };
828
+ }
829
+
830
+ // src/ui/components/staged_history.js
831
+ var ACTIONS = Object.freeze(["cell:stage"]);
832
+ var DEFAULT_HISTORY_LIMIT = 100;
833
+ var StagedHistory = class {
834
+ /**
835
+ * @param {object} cfg
836
+ * @param {(entry, direction: 'undo'|'redo') => void} cfg.apply writes one
837
+ * leaf entry's `before` (undo) or `after` (redo) into the staged cells.
838
+ * Called once per leaf; the ring holds no reference to those cells.
839
+ * @param {({canUndo, canRedo, depth}) => void} [cfg.onState] after every
840
+ * change of the stacks.
841
+ * @param {number} [cfg.limit=100] entries kept; the oldest fall off.
842
+ */
843
+ constructor({ apply, onState = null, limit = DEFAULT_HISTORY_LIMIT } = {}) {
844
+ if (typeof apply !== "function") {
845
+ throw new Error("StagedHistory needs an `apply(entry, direction)`: a ring with no writer would report every undo as done and change nothing.");
846
+ }
847
+ this.apply = apply;
848
+ this.onState = onState;
849
+ this.limit = Number.isFinite(limit) && limit >= 1 ? Math.floor(limit) : DEFAULT_HISTORY_LIMIT;
850
+ this.undoStack = [];
851
+ this.redoStack = [];
852
+ this.group = null;
853
+ this.depthOpen = 0;
854
+ this.applying = false;
855
+ }
856
+ get canUndo() {
857
+ return this.undoStack.length > 0;
858
+ }
859
+ get canRedo() {
860
+ return this.redoStack.length > 0;
861
+ }
862
+ get depth() {
863
+ return this.undoStack.length;
864
+ }
865
+ /** Open a group: everything recorded until the matching `endGroup` is one
866
+ * undo (a fill, a paste). */
867
+ beginGroup(label = "") {
868
+ this.depthOpen += 1;
869
+ if (this.depthOpen === 1) this.group = { action: "batch", label, entries: [] };
870
+ }
871
+ /** Close a group. An empty one is dropped, so it costs no undo step; one
872
+ * holding a single entry is that entry. */
873
+ endGroup() {
874
+ if (!this.depthOpen) return;
875
+ this.depthOpen -= 1;
876
+ if (this.depthOpen) return;
877
+ const group = this.group;
878
+ this.group = null;
879
+ if (!group) return;
880
+ if (group.entries.length === 1) this._push(group.entries[0]);
881
+ else if (group.entries.length) this._push(group);
882
+ }
883
+ /** Record one delta. Returns whether it was recorded (false while an
884
+ * entry is being applied). Throws on an action that is not in `ACTIONS`. */
885
+ record(entry) {
886
+ if (this.applying) return false;
887
+ if (!entry || !ACTIONS.includes(entry.action)) {
888
+ throw new Error(`${entry?.action} is not an undoable action: the ring reverses staged cells only, never a commit.`);
889
+ }
890
+ if (this.group) {
891
+ this.group.entries.push(entry);
892
+ return true;
893
+ }
894
+ this._push(entry);
895
+ return true;
896
+ }
897
+ undo() {
898
+ this._closeOpenGroup();
899
+ const entry = this.undoStack.pop();
900
+ if (!entry) return false;
901
+ this._run(entry, "undo");
902
+ this.redoStack.push(entry);
903
+ this._publish();
904
+ return true;
905
+ }
906
+ redo() {
907
+ this._closeOpenGroup();
908
+ const entry = this.redoStack.pop();
909
+ if (!entry) return false;
910
+ this._run(entry, "redo");
911
+ this.undoStack.push(entry);
912
+ this._publish();
913
+ return true;
914
+ }
915
+ /** Forget everything — after a commit or a discard, whose cells the
916
+ * entries no longer describe. An open group is closed with it. */
917
+ reset() {
918
+ this.undoStack = [];
919
+ this.redoStack = [];
920
+ this.group = null;
921
+ this.depthOpen = 0;
922
+ this._publish();
923
+ }
924
+ /** A group left open (a caller that threw) is closed before an undo, so it
925
+ * cannot swallow every later entry. */
926
+ _closeOpenGroup() {
927
+ if (!this.depthOpen) return;
928
+ this.depthOpen = 1;
929
+ this.endGroup();
930
+ }
931
+ _push(entry) {
932
+ this.undoStack.push(entry);
933
+ this.redoStack = [];
934
+ if (this.undoStack.length > this.limit) this.undoStack.splice(0, this.undoStack.length - this.limit);
935
+ this._publish();
936
+ }
937
+ _publish() {
938
+ try {
939
+ this.onState?.({ canUndo: this.canUndo, canRedo: this.canRedo, depth: this.undoStack.length });
940
+ } catch (err) {
941
+ console.error("[StagedHistory] onState threw", err);
942
+ }
943
+ }
944
+ _run(entry, direction) {
945
+ this.applying = true;
946
+ try {
947
+ this._applyOne(entry, direction);
948
+ } finally {
949
+ this.applying = false;
950
+ }
951
+ }
952
+ /** A group replays in REVERSE on the way back (two writes to one cell must
953
+ * unwind last first) and in order on the way forward. */
954
+ _applyOne(entry, direction) {
955
+ if (entry.action === "batch") {
956
+ const entries = direction === "undo" ? [...entry.entries].reverse() : entry.entries;
957
+ for (const child of entries) this._applyOne(child, direction);
958
+ return;
959
+ }
960
+ this.apply(entry, direction);
961
+ }
962
+ };
963
+
964
+ // src/ui/components/series.js
965
+ var ISO_DATE2 = /^\d{4}-\d{2}-\d{2}$/;
966
+ var TRAILING_NUMBER = /^(.*?)(\d+)(\D*)$/;
967
+ var NUMERIC_TEXT = /^[-+]?(\d+\.?\d*|\.\d+)$/;
968
+ var DAY_MS = 864e5;
969
+ var isBlank = (v) => v === null || v === void 0 || v === "";
970
+ function detectSeries(values) {
971
+ if (!Array.isArray(values) || values.length === 0) return null;
972
+ if (values.every(isBlank)) return null;
973
+ if (values.length === 1) return { kind: "copy", step: 0 };
974
+ const numbers = values.map(toNumber);
975
+ if (numbers.every((n) => n !== null)) {
976
+ const step = numbers[1] - numbers[0];
977
+ const constant = numbers.every((n, i) => i === 0 || nearlyEqual(n - numbers[i - 1], step));
978
+ if (constant && step !== 0) return { kind: "number", step };
979
+ if (constant) return { kind: "copy", step: 0 };
980
+ return { kind: "cycle", step: 0 };
981
+ }
982
+ const days = values.map(toDayMs);
983
+ if (days.every((d) => d !== null)) {
984
+ const step = Math.round((days[1] - days[0]) / DAY_MS);
985
+ const constant = days.every((d, i) => i === 0 || Math.round((d - days[i - 1]) / DAY_MS) === step);
986
+ if (constant && step !== 0) return { kind: "date", step };
987
+ if (constant) return { kind: "copy", step: 0 };
988
+ return { kind: "cycle", step: 0 };
989
+ }
990
+ const parts = values.map((v) => typeof v === "string" ? TRAILING_NUMBER.exec(v) : null);
991
+ if (parts.every(Boolean)) {
992
+ const prefix = parts[0][1];
993
+ const suffix = parts[0][3];
994
+ if (parts.every((p) => p[1] === prefix && p[3] === suffix)) {
995
+ const nums = parts.map((p) => Number(p[2]));
996
+ const step = nums[1] - nums[0];
997
+ const constant = nums.every((n, i) => i === 0 || n - nums[i - 1] === step);
998
+ if (constant && step !== 0) {
999
+ return { kind: "text-number", step, prefix, suffix, pad: parts[0][2].length };
1000
+ }
1001
+ }
1002
+ }
1003
+ return { kind: "cycle", step: 0 };
1004
+ }
1005
+ function projectSeries(values, count, { forceCopy = false } = {}) {
1006
+ const out = [];
1007
+ const n = Math.max(0, Math.floor(Number(count) || 0));
1008
+ if (!Array.isArray(values) || values.length === 0 || n === 0) return out;
1009
+ const series = forceCopy ? null : detectSeries(values);
1010
+ const last = values[values.length - 1];
1011
+ if (series?.kind === "number") {
1012
+ const base = toNumber(last);
1013
+ const decimals = Math.min(15, Math.max(...values.map(decimalsOf)));
1014
+ const allNumbers = values.every((v) => typeof v === "number");
1015
+ for (let i = 1; i <= n; i++) {
1016
+ const next = base + series.step * i;
1017
+ out.push(allNumbers ? Number(next.toFixed(decimals)) : next.toFixed(decimals));
1018
+ }
1019
+ return out;
1020
+ }
1021
+ if (series?.kind === "date") {
1022
+ const asDate = last instanceof Date;
1023
+ for (let i = 1; i <= n; i++) {
1024
+ if (asDate) {
1025
+ const next = new Date(last.getTime());
1026
+ next.setDate(next.getDate() + series.step * i);
1027
+ out.push(next);
1028
+ } else {
1029
+ const next = new Date(toDayMs(last));
1030
+ next.setUTCDate(next.getUTCDate() + series.step * i);
1031
+ out.push(next.toISOString().slice(0, 10));
1032
+ }
1033
+ }
1034
+ return out;
1035
+ }
1036
+ if (series?.kind === "text-number") {
1037
+ const base = Number(TRAILING_NUMBER.exec(last)[2]);
1038
+ for (let i = 1; i <= n; i++) {
1039
+ const next = base + series.step * i;
1040
+ const digits = String(Math.abs(next)).padStart(series.pad, "0");
1041
+ out.push(`${series.prefix}${next < 0 ? "-" : ""}${digits}${series.suffix}`);
1042
+ }
1043
+ return out;
1044
+ }
1045
+ for (let i = 0; i < n; i++) out.push(values[i % values.length]);
1046
+ return out;
1047
+ }
1048
+ function toNumber(value) {
1049
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
1050
+ if (typeof value !== "string") return null;
1051
+ const text = value.trim();
1052
+ return NUMERIC_TEXT.test(text) ? Number(text) : null;
1053
+ }
1054
+ function toDayMs(value) {
1055
+ if (value instanceof Date) {
1056
+ const t2 = value.getTime();
1057
+ return Number.isNaN(t2) ? null : t2;
1058
+ }
1059
+ if (typeof value !== "string" || !ISO_DATE2.test(value.trim())) return null;
1060
+ const text = value.trim();
1061
+ const t = (/* @__PURE__ */ new Date(`${text}T00:00:00Z`)).getTime();
1062
+ if (Number.isNaN(t) || new Date(t).toISOString().slice(0, 10) !== text) return null;
1063
+ return t;
1064
+ }
1065
+ function decimalsOf(value) {
1066
+ const text = typeof value === "number" ? String(value) : String(value).trim();
1067
+ if (/e/i.test(text)) return 15;
1068
+ return (text.split(".")[1] || "").length;
1069
+ }
1070
+ function nearlyEqual(a, b) {
1071
+ return Math.abs(a - b) < 1e-9;
1072
+ }
1073
+
1074
+ // src/ui/components/inline_renamer.js
1075
+ function assertLabel(label) {
1076
+ if (!label || typeof label.appendChild !== "function") {
1077
+ throw new Error("[InlineRenamer] options.label must be an HTMLElement");
1078
+ }
1079
+ }
1080
+ function buildInput(label, options) {
1081
+ const input = document.createElement("input");
1082
+ input.type = "text";
1083
+ input.className = ["twm-node-edit-input", options?.inputClass || ""].join(" ").trim();
1084
+ input.placeholder = options?.placeholder || label.textContent || "Name";
1085
+ input.value = options?.initialValue ?? label.textContent ?? "";
1086
+ input.spellcheck = false;
1087
+ input.autocomplete = "off";
1088
+ input.setAttribute("aria-label", "Edit name");
1089
+ return input;
1090
+ }
1091
+ function attachInlineRenamer(options = {}) {
1092
+ const label = options.label;
1093
+ assertLabel(label);
1094
+ let input = null;
1095
+ let editing = false;
1096
+ let destroyed = false;
1097
+ let originalText = label.textContent || "";
1098
+ const cleanup = () => {
1099
+ if (input && input.parentNode === label) {
1100
+ input.remove();
1101
+ }
1102
+ input = null;
1103
+ editing = false;
1104
+ };
1105
+ const cancel = () => {
1106
+ if (destroyed) return;
1107
+ cleanup();
1108
+ label.textContent = originalText;
1109
+ options.onCancel?.({ value: originalText });
1110
+ };
1111
+ const commit = async (reason = "commit") => {
1112
+ if (destroyed || !input) return;
1113
+ const raw = input.value;
1114
+ let next = raw;
1115
+ try {
1116
+ next = options.transformInput ? options.transformInput(raw, { reason }) : raw;
1117
+ } catch (err) {
1118
+ window.logger?.warn("inline-renamer", "transformInput failed", err);
1119
+ }
1120
+ try {
1121
+ const result = await options.onCommit?.(next, { reason }) || {};
1122
+ if (result.success === false) {
1123
+ if (result.keepEditing) {
1124
+ input.focus();
1125
+ input.select();
1126
+ return;
1127
+ }
1128
+ cancel();
1129
+ return;
1130
+ }
1131
+ const finalValue = result.newValue ?? next ?? "";
1132
+ label.textContent = finalValue;
1133
+ cleanup();
1134
+ options.onFinish?.({ value: finalValue });
1135
+ } catch (err) {
1136
+ window.logger?.warn("inline-renamer", "onCommit failed", err);
1137
+ cancel();
1138
+ }
1139
+ };
1140
+ const start = (triggerEvent = null) => {
1141
+ if (destroyed || editing) return;
1142
+ editing = true;
1143
+ originalText = label.textContent || "";
1144
+ label.textContent = "";
1145
+ input = buildInput(label, { ...options, initialValue: originalText });
1146
+ label.appendChild(input);
1147
+ const stopEvents = (ev) => ev.stopPropagation();
1148
+ input.addEventListener("mousedown", stopEvents);
1149
+ input.addEventListener("click", stopEvents);
1150
+ input.addEventListener("keydown", (ev) => {
1151
+ if (ev.key === "Enter") {
1152
+ ev.preventDefault();
1153
+ commit("enter");
1154
+ } else if (ev.key === "Escape") {
1155
+ ev.preventDefault();
1156
+ cancel();
1157
+ }
1158
+ });
1159
+ input.addEventListener("blur", () => commit("blur"));
1160
+ options.onStart?.({ value: originalText, triggerEvent });
1161
+ setTimeout(() => {
1162
+ input?.focus();
1163
+ input?.select?.();
1164
+ }, 0);
1165
+ };
1166
+ const destroy = () => {
1167
+ destroyed = true;
1168
+ cleanup();
1169
+ };
1170
+ return { start, cancel, commit, destroy };
1171
+ }
1172
+
1173
+ // src/ui/components/staged_edits.js
1174
+ var key = (rowIdx, colIdx) => `${rowIdx}:${colIdx}`;
1175
+ var FROZEN_SELECTORS = [
1176
+ "th.sortable",
1177
+ ".data-table__filter-cell:not(.data-table__filter-cell--empty)",
1178
+ ".data-table__filter-cell:not(.data-table__filter-cell--empty) input",
1179
+ ".data-table__filter-cell:not(.data-table__filter-cell--empty) button",
1180
+ ".data-table__filter-cell:not(.data-table__filter-cell--empty) select",
1181
+ ".data-table__filter-cell:not(.data-table__filter-cell--empty) textarea",
1182
+ ".twm-pagination-controls button:not(.twm-pagination-btn--export)",
1183
+ ".twm-pagination-controls input",
1184
+ ".twm-dt-paging button",
1185
+ ".twm-dt-paging input"
1186
+ ].join(", ");
1187
+ var SAVED_ATTRS = ["title", "data-refusal", "data-tooltip", "aria-disabled"];
1188
+ var StagedEdits = class {
1189
+ /** @param {import('./cell_grid.js').CellGrid} grid */
1190
+ constructor(grid) {
1191
+ this.grid = grid;
1192
+ this.table = grid.table;
1193
+ this.cells = /* @__PURE__ */ new Map();
1194
+ this.errors = /* @__PURE__ */ new Map();
1195
+ this.committing = null;
1196
+ this.generation = 0;
1197
+ this._touched = null;
1198
+ this._frozen = /* @__PURE__ */ new Map();
1199
+ const limit = this.table.config.historyLimit;
1200
+ this.history = new StagedHistory({
1201
+ apply: (entry, direction) => this._apply(entry, direction),
1202
+ limit: limit == null ? DEFAULT_HISTORY_LIMIT : limit
1203
+ });
1204
+ }
1205
+ // ── what is staged ───────────────────────────────────────────────────
1206
+ has(k) {
1207
+ return this.cells.has(k);
1208
+ }
1209
+ value(k) {
1210
+ return this.cells.get(k)?.value;
1211
+ }
1212
+ /** The value the row holds for a cell: what "no change" means. */
1213
+ original(rowIdx, colIdx) {
1214
+ const row = this.grid.currentRow(rowIdx);
1215
+ return row ? row[colIdx] : void 0;
1216
+ }
1217
+ /** Every staged cell as `{rowIdx, colIdx, value, previous}`, by row then
1218
+ * column; `previous` is the value the row holds. */
1219
+ edits() {
1220
+ return [...this.cells.values()].sort((a, b) => a.rowIdx - b.rowIdx || a.colIdx - b.colIdx).map(({ rowIdx, colIdx, value }) => ({
1221
+ rowIdx,
1222
+ colIdx,
1223
+ value,
1224
+ previous: this.original(rowIdx, colIdx)
1225
+ }));
1226
+ }
1227
+ _side(k) {
1228
+ return this.cells.has(k) ? { has: true, value: this.cells.get(k).value } : { has: false, value: null };
1229
+ }
1230
+ /** Write one side of a delta: stage the value, or take the cell out. */
1231
+ _write(rowIdx, colIdx, side) {
1232
+ const k = key(rowIdx, colIdx);
1233
+ if (side.has) this.cells.set(k, { rowIdx, colIdx, value: side.value });
1234
+ else this.cells.delete(k);
1235
+ this.errors.delete(k);
1236
+ }
1237
+ /**
1238
+ * Stage a value in a cell, as one undo entry (inside an open group, part
1239
+ * of it). The value its row holds takes the cell back to no change.
1240
+ * Returns whether anything changed.
1241
+ */
1242
+ stage(rowIdx, colIdx, value) {
1243
+ if (this.committing || !this._stageOne(rowIdx, colIdx, value)) return false;
1244
+ this.grid._redraw(rowIdx, colIdx);
1245
+ this._changed();
1246
+ return true;
1247
+ }
1248
+ /**
1249
+ * STORY-0089. Stage many cells (`[{rowIdx, colIdx, value}]`) as ONE undo
1250
+ * entry — a fill, a paste — each as `stage` would; the rows are drawn and
1251
+ * the listeners told once, at the end. Returns how many cells changed.
1252
+ */
1253
+ stageMany(cells, label = "") {
1254
+ if (this.committing) return 0;
1255
+ const rows = /* @__PURE__ */ new Set();
1256
+ let changed = 0;
1257
+ this.history.beginGroup(label);
1258
+ try {
1259
+ for (const { rowIdx, colIdx, value } of cells) {
1260
+ if (this._stageOne(rowIdx, colIdx, value)) {
1261
+ changed++;
1262
+ rows.add(rowIdx);
1263
+ }
1264
+ }
1265
+ } finally {
1266
+ this.history.endGroup();
1267
+ }
1268
+ for (const rowIdx of rows) this.grid._redraw(rowIdx);
1269
+ if (changed) this._changed();
1270
+ return changed;
1271
+ }
1272
+ /** One cell's new state and its delta in the ring; whether it changed. */
1273
+ _stageOne(rowIdx, colIdx, value) {
1274
+ const k = key(rowIdx, colIdx);
1275
+ const before = this._side(k);
1276
+ const after = Object.is(value, this.original(rowIdx, colIdx)) ? { has: false, value: null } : { has: true, value };
1277
+ if (before.has === after.has && Object.is(before.value, after.value)) return false;
1278
+ this._write(rowIdx, colIdx, after);
1279
+ this.history.record({ action: "cell:stage", row: rowIdx, col: colIdx, before, after });
1280
+ return true;
1281
+ }
1282
+ _apply(entry, direction) {
1283
+ this._write(entry.row, entry.col, direction === "undo" ? entry.before : entry.after);
1284
+ this._touched?.add(entry.row);
1285
+ }
1286
+ _walk(step) {
1287
+ if (this.committing) return false;
1288
+ this._touched = /* @__PURE__ */ new Set();
1289
+ let moved = false;
1290
+ try {
1291
+ moved = step();
1292
+ } finally {
1293
+ const rows = this._touched;
1294
+ this._touched = null;
1295
+ for (const rowIdx of rows) this.grid._redraw(rowIdx);
1296
+ }
1297
+ if (moved) this._changed();
1298
+ return moved;
1299
+ }
1300
+ undo() {
1301
+ return this._walk(() => this.history.undo());
1302
+ }
1303
+ redo() {
1304
+ return this._walk(() => this.history.redo());
1305
+ }
1306
+ /** Drop every staged cell, its marks and the ring. */
1307
+ discard() {
1308
+ if (this.committing) return false;
1309
+ const rows = this._rows();
1310
+ const had = this.cells.size > 0 || this.errors.size > 0;
1311
+ this.cells.clear();
1312
+ this.errors.clear();
1313
+ this.history.reset();
1314
+ for (const rowIdx of rows) this.grid._redraw(rowIdx);
1315
+ if (had) this._changed();
1316
+ return had;
1317
+ }
1318
+ /** New rows (`setData({rows})`): the cells named rows that are gone. */
1319
+ clear() {
1320
+ this.generation++;
1321
+ this.committing = null;
1322
+ const had = this.cells.size > 0 || this.errors.size > 0;
1323
+ this.cells.clear();
1324
+ this.errors.clear();
1325
+ this.history.reset();
1326
+ if (had) this._changed();
1327
+ }
1328
+ _rows() {
1329
+ const rows = /* @__PURE__ */ new Set();
1330
+ for (const c of this.cells.values()) rows.add(c.rowIdx);
1331
+ for (const k of this.errors.keys()) rows.add(Number(k.split(":")[0]));
1332
+ return rows;
1333
+ }
1334
+ // ── the commit ───────────────────────────────────────────────────────
1335
+ /**
1336
+ * Hand every staged cell to `onCommit({edits})`, once. Resolves
1337
+ * `{ok: true, committed}` when it succeeded — the values are written into
1338
+ * the rows the table holds, and the staged cells and the ring are gone —
1339
+ * or `{ok: false, errors}` when it refused: per-cell errors mark their
1340
+ * cells, a refusal with none marks every staged cell, and every staged
1341
+ * value stays.
1342
+ */
1343
+ commit() {
1344
+ if (this.committing) return this.committing;
1345
+ if (this.grid.isEditing()) this.grid.editor.commit(null, "commit");
1346
+ const fn = this.table.config.onCommit;
1347
+ const edits = this.edits();
1348
+ if (!edits.length) return Promise.resolve({ ok: true, committed: 0 });
1349
+ if (typeof fn !== "function") {
1350
+ console.warn("[DataTable] commit() needs an `onCommit`; nothing was committed.");
1351
+ return Promise.resolve({ ok: false, errors: [] });
1352
+ }
1353
+ const generation = this.generation;
1354
+ const refused = (err) => ({ ok: false, error: err?.message || String(err ?? "Not saved") });
1355
+ let result;
1356
+ try {
1357
+ result = fn({ edits: edits.map((e) => ({ ...e })) });
1358
+ } catch (err) {
1359
+ result = refused(err);
1360
+ }
1361
+ const out = Promise.resolve(result).then((r) => r, refused).then((r) => {
1362
+ if (generation !== this.generation) return { ok: false, errors: [] };
1363
+ this.committing = null;
1364
+ return this._settle(edits, r);
1365
+ });
1366
+ this.committing = out;
1367
+ this._changed();
1368
+ return out;
1369
+ }
1370
+ _settle(edits, result) {
1371
+ const listed = Array.isArray(result?.errors) ? result.errors : [];
1372
+ if (listed.length || result?.ok === false) {
1373
+ const errors = [];
1374
+ const mark = (rowIdx, colIdx, message) => {
1375
+ const k = key(rowIdx, colIdx);
1376
+ if (!this.cells.has(k)) return;
1377
+ const text = String(message ?? "Not saved");
1378
+ this.errors.set(k, text);
1379
+ errors.push({ rowIdx, colIdx, error: text });
1380
+ };
1381
+ if (listed.length) {
1382
+ for (const e of listed) mark(e?.rowIdx, e?.colIdx, e?.error ?? e?.message);
1383
+ } else {
1384
+ for (const e of edits) mark(e.rowIdx, e.colIdx, result.error);
1385
+ }
1386
+ for (const rowIdx of this._rows()) this.grid._redraw(rowIdx);
1387
+ this._changed();
1388
+ return { ok: false, errors };
1389
+ }
1390
+ const rows = this._rows();
1391
+ for (const e of edits) {
1392
+ const row = this.grid.currentRow(e.rowIdx);
1393
+ if (Array.isArray(row)) row[e.colIdx] = e.value;
1394
+ }
1395
+ this.cells.clear();
1396
+ this.errors.clear();
1397
+ this.history.reset();
1398
+ for (const rowIdx of rows) this.grid._redraw(rowIdx);
1399
+ this._changed();
1400
+ return { ok: true, committed: edits.length };
1401
+ }
1402
+ // ── the freeze ───────────────────────────────────────────────────────
1403
+ /** Why sort, filters and pages are held, or null when nothing is staged. */
1404
+ freezeReason() {
1405
+ const n = this.cells.size;
1406
+ if (!n) return null;
1407
+ const own = this.table.config.stagedRefusal;
1408
+ if (typeof own === "function") {
1409
+ try {
1410
+ return String(own(n));
1411
+ } catch (err) {
1412
+ console.error("[DataTable] stagedRefusal threw", err);
1413
+ }
1414
+ } else if (typeof own === "string" && own) return own;
1415
+ return `Sorting, filtering and paging are held while ${n} change${n === 1 ? " is" : "s are"} not committed. Commit or discard them first.`;
1416
+ }
1417
+ /** Disable every control the freeze holds with the reason on it, or put
1418
+ * back the ones it held. Run after every render and every change. */
1419
+ paintFreeze() {
1420
+ const root = this.table._wrapperEl;
1421
+ const reason = this.freezeReason();
1422
+ root?.classList.toggle("twm-dt--frozen", !!reason);
1423
+ if (!reason) {
1424
+ for (const [el, saved] of this._frozen) this._thaw(el, saved);
1425
+ this._frozen.clear();
1426
+ return;
1427
+ }
1428
+ for (const el of this._frozen.keys()) if (!el.isConnected) this._frozen.delete(el);
1429
+ if (!root) return;
1430
+ for (const el of root.querySelectorAll(FROZEN_SELECTORS)) {
1431
+ if (!this._frozen.has(el)) {
1432
+ const saved = {};
1433
+ for (const a of SAVED_ATTRS) saved[a] = el.getAttribute(a);
1434
+ saved.disabled = "disabled" in el ? el.disabled : null;
1435
+ this._frozen.set(el, saved);
1436
+ }
1437
+ el.setAttribute("data-refusal", reason);
1438
+ el.setAttribute("title", reason);
1439
+ if (el.hasAttribute("data-tooltip")) el.setAttribute("data-tooltip", reason);
1440
+ el.setAttribute("aria-disabled", "true");
1441
+ if ("disabled" in el) el.disabled = true;
1442
+ }
1443
+ }
1444
+ _thaw(el, saved) {
1445
+ for (const a of SAVED_ATTRS) {
1446
+ if (saved[a] == null) el.removeAttribute(a);
1447
+ else el.setAttribute(a, saved[a]);
1448
+ }
1449
+ if (saved.disabled !== null) el.disabled = saved.disabled;
1450
+ }
1451
+ /** After every change: the freeze, and whoever listens. */
1452
+ _changed() {
1453
+ this.paintFreeze();
1454
+ const fn = this.table.config.onStagedChange;
1455
+ if (typeof fn !== "function") return;
1456
+ try {
1457
+ fn({
1458
+ count: this.cells.size,
1459
+ canUndo: this.history.canUndo,
1460
+ canRedo: this.history.canRedo,
1461
+ committing: !!this.committing
1462
+ });
1463
+ } catch (err) {
1464
+ console.error("[DataTable] onStagedChange threw", err);
1465
+ }
1466
+ }
1467
+ };
1468
+
1469
+ // src/ui/components/fill_handle.js
1470
+ var key2 = (rowIdx, colIdx) => `${rowIdx}:${colIdx}`;
1471
+ var FillHandle = class {
1472
+ /** @param {import('./cell_grid.js').CellGrid} grid */
1473
+ constructor(grid) {
1474
+ this.grid = grid;
1475
+ this.el = null;
1476
+ this.corner = null;
1477
+ this.skipped = /* @__PURE__ */ new Set();
1478
+ this.preview = /* @__PURE__ */ new Set();
1479
+ this.drag = null;
1480
+ this._unguard = null;
1481
+ }
1482
+ // ── painting (called by the grid for every cell it paints) ───────────
1483
+ /** One cell: its preview and skipped marks, and the handle when it is
1484
+ * the range's corner. */
1485
+ paintCell(td, pos, col) {
1486
+ const rowIdx = td.parentNode?.__rowIndex;
1487
+ td.classList.toggle("twm-dt-cell--fill-preview", this.preview.has(`${pos}:${col}`));
1488
+ td.classList.toggle("twm-dt-cell--fill-skipped", this.skipped.has(key2(rowIdx, col)));
1489
+ const r = this.grid.sel.range();
1490
+ if (r && r.bottom === pos && r.right === col) this._place(td);
1491
+ else if (td === this.corner) this._unplace();
1492
+ }
1493
+ _handle() {
1494
+ if (this.el) return this.el;
1495
+ const el = document.createElement("div");
1496
+ el.className = "twm-dt-fill-handle";
1497
+ el.setAttribute("aria-hidden", "true");
1498
+ el.title = "Drag to fill; hold Ctrl to repeat instead of continuing a series";
1499
+ el.addEventListener("pointerdown", (ev) => this._begin(ev));
1500
+ this.el = el;
1501
+ return el;
1502
+ }
1503
+ _place(td) {
1504
+ if (this.grid.isEditing() || this.grid.staged.committing) {
1505
+ this._unplace();
1506
+ return;
1507
+ }
1508
+ if (this.corner && this.corner !== td) this.corner.classList.remove("twm-dt-cell--fill-corner");
1509
+ this.corner = td;
1510
+ td.classList.add("twm-dt-cell--fill-corner");
1511
+ const el = this._handle();
1512
+ if (el.parentNode !== td) td.appendChild(el);
1513
+ }
1514
+ _unplace() {
1515
+ this.corner?.classList.remove("twm-dt-cell--fill-corner");
1516
+ this.corner = null;
1517
+ this.el?.remove();
1518
+ }
1519
+ // ── what a fill covers ───────────────────────────────────────────────
1520
+ /**
1521
+ * The fill that reaching cell `to` (`{row, col}`, position and data
1522
+ * column) from `range` makes: `{direction, count}`, or null when `to` is
1523
+ * inside the range's rows and columns. The direction is the one the cell
1524
+ * lies furthest out in; up and down win a tie. Clamped to the page drawn.
1525
+ */
1526
+ plan(range, to) {
1527
+ if (!range || !to) return null;
1528
+ const cols = this.grid.columns();
1529
+ const [first, last] = this.grid.span();
1530
+ const row = Math.max(first, Math.min(last, Number(to.row)));
1531
+ const ordL = cols.indexOf(range.left);
1532
+ const ordR = cols.indexOf(range.right);
1533
+ const ordTo = cols.indexOf(to.col);
1534
+ const out = [
1535
+ ["down", row - range.bottom],
1536
+ ["up", range.top - row],
1537
+ ["right", ordTo < 0 ? 0 : ordTo - ordR],
1538
+ ["left", ordTo < 0 ? 0 : ordL - ordTo]
1539
+ ];
1540
+ let best = null;
1541
+ for (const [direction, count] of out) {
1542
+ if (count > 0 && (!best || count > best.count)) best = { direction, count };
1543
+ }
1544
+ return best;
1545
+ }
1546
+ /** The cells a plan covers, as `{pos, col}`, line by line in fill order:
1547
+ * `[{seed: [{pos, col}], targets: [{pos, col}]}]`. */
1548
+ _lines(range, { direction, count }) {
1549
+ const cols = this.grid.columns();
1550
+ const ordL = cols.indexOf(range.left);
1551
+ const ordR = cols.indexOf(range.right);
1552
+ const lines = [];
1553
+ if (direction === "down" || direction === "up") {
1554
+ const down = direction === "down";
1555
+ for (let o = ordL; o <= ordR; o++) {
1556
+ const col = cols[o];
1557
+ const seed = [];
1558
+ for (let p = range.top; p <= range.bottom; p++) seed.push({ pos: p, col });
1559
+ if (!down) seed.reverse();
1560
+ const targets = [];
1561
+ for (let i = 1; i <= count; i++) {
1562
+ targets.push({ pos: down ? range.bottom + i : range.top - i, col });
1563
+ }
1564
+ lines.push({ seed, targets });
1565
+ }
1566
+ } else {
1567
+ const right = direction === "right";
1568
+ for (let p = range.top; p <= range.bottom; p++) {
1569
+ const seed = [];
1570
+ for (let o = ordL; o <= ordR; o++) seed.push({ pos: p, col: cols[o] });
1571
+ if (!right) seed.reverse();
1572
+ const targets = [];
1573
+ for (let i = 1; i <= count; i++) {
1574
+ const o = right ? ordR + i : ordL - i;
1575
+ if (o >= 0 && o < cols.length) targets.push({ pos: p, col: cols[o] });
1576
+ }
1577
+ lines.push({ seed, targets });
1578
+ }
1579
+ }
1580
+ return lines;
1581
+ }
1582
+ // ── the fill ─────────────────────────────────────────────────────────
1583
+ /**
1584
+ * Fill from the range to cell `to`, staging the values as ONE undo
1585
+ * entry. Returns `{direction, count, staged, skipped}` (`skipped` the
1586
+ * cells `editable` refused, `{row, col, rowIdx}`), or null when nothing
1587
+ * was filled: no range, `to` inside it, an editor open or a commit out.
1588
+ */
1589
+ fill(to, { forceCopy = false } = {}) {
1590
+ const g = this.grid;
1591
+ g._sync();
1592
+ const range = g.sel.range();
1593
+ const plan = this.plan(range, to);
1594
+ if (!plan || g.staged.committing || g.isEditing()) return null;
1595
+ const cells = [];
1596
+ const skipped = [];
1597
+ for (const { seed, targets } of this._lines(range, plan)) {
1598
+ const values = projectSeries(
1599
+ seed.map(({ pos, col }) => g.shownValue(g.rowIndexAt(pos), col)),
1600
+ targets.length,
1601
+ { forceCopy }
1602
+ );
1603
+ targets.forEach(({ pos, col }, i) => {
1604
+ const rowIdx = g.rowIndexAt(pos);
1605
+ if (g.canEdit(pos, col)) cells.push({ rowIdx, colIdx: col, value: values[i] });
1606
+ else skipped.push({ row: pos, col, rowIdx });
1607
+ });
1608
+ }
1609
+ const staged = g.staged.stageMany(cells, `fill ${cells.length} cell(s)`);
1610
+ const cols = g.columns();
1611
+ const ordL = cols.indexOf(range.left);
1612
+ const ordR = cols.indexOf(range.right);
1613
+ const { direction, count } = plan;
1614
+ if (direction === "down") g.set({ row: range.bottom + count, col: range.right }, { row: range.top, col: range.left });
1615
+ else if (direction === "up") g.set({ row: range.top - count, col: range.left }, { row: range.bottom, col: range.right });
1616
+ else if (direction === "right") g.set({ row: range.bottom, col: cols[Math.min(cols.length - 1, ordR + count)] }, { row: range.top, col: range.left });
1617
+ else g.set({ row: range.top, col: cols[Math.max(0, ordL - count)] }, { row: range.bottom, col: range.right });
1618
+ for (const s of skipped) this.skipped.add(key2(s.rowIdx, s.col));
1619
+ if (skipped.length) g.paint();
1620
+ return { direction, count, staged, skipped };
1621
+ }
1622
+ /** The selection moved, or new rows came: the skipped marks are over. */
1623
+ clearMarks() {
1624
+ this.skipped.clear();
1625
+ }
1626
+ // ── the drag ─────────────────────────────────────────────────────────
1627
+ _begin(ev) {
1628
+ if (ev.button != null && ev.button !== 0) return;
1629
+ ev.preventDefault();
1630
+ ev.stopPropagation();
1631
+ const g = this.grid;
1632
+ g._sync();
1633
+ const range = g.sel.range();
1634
+ if (!range || this.drag || g.isEditing() || g.staged.committing) return;
1635
+ const doc = this.el.ownerDocument;
1636
+ this._guardClick(doc);
1637
+ const onMove = (move) => this._move(move);
1638
+ const onUp = (up) => {
1639
+ const plan = this.drag?.plan;
1640
+ this._end();
1641
+ if (plan) this.fill(this._reach(range, plan), { forceCopy: !!(up.ctrlKey || up.metaKey) });
1642
+ try {
1643
+ g.table._tableEl?.focus({ preventScroll: true });
1644
+ } catch (_) {
1645
+ }
1646
+ };
1647
+ const onCancel = () => this._end();
1648
+ const onKey = (k) => {
1649
+ if (k.key === "Escape") {
1650
+ k.preventDefault();
1651
+ k.stopPropagation();
1652
+ this._end();
1653
+ }
1654
+ };
1655
+ doc.addEventListener("pointermove", onMove);
1656
+ doc.addEventListener("pointerup", onUp);
1657
+ doc.addEventListener("pointercancel", onCancel);
1658
+ doc.addEventListener("keydown", onKey, true);
1659
+ this.drag = {
1660
+ range,
1661
+ plan: null,
1662
+ unbind: () => {
1663
+ doc.removeEventListener("pointermove", onMove);
1664
+ doc.removeEventListener("pointerup", onUp);
1665
+ doc.removeEventListener("pointercancel", onCancel);
1666
+ doc.removeEventListener("keydown", onKey, true);
1667
+ }
1668
+ };
1669
+ }
1670
+ /**
1671
+ * A press on the handle and its release make, in a browser, a click (and
1672
+ * on a second press a dblclick) of their own, on the element under both:
1673
+ * the corner cell, the row, the body. Filled, cancelled by Escape or
1674
+ * released where it began, a drag is no click on the table: from the
1675
+ * press, the next click in the document is swallowed in the capture
1676
+ * phase, and so is every dblclick until the task the release is handled
1677
+ * in is over. A cancelled pointer makes no click; the guard ends with it.
1678
+ */
1679
+ _guardClick(doc) {
1680
+ this._unguard?.();
1681
+ const view = doc.defaultView;
1682
+ let timer = null;
1683
+ const swallow = (c) => {
1684
+ c.stopPropagation();
1685
+ c.preventDefault();
1686
+ };
1687
+ const swallowOnce = (c) => {
1688
+ swallow(c);
1689
+ doc.removeEventListener("click", swallowOnce, true);
1690
+ };
1691
+ const released = () => {
1692
+ doc.removeEventListener("pointerup", released, true);
1693
+ doc.removeEventListener("pointercancel", unguard, true);
1694
+ timer = view.setTimeout(unguard, 0);
1695
+ };
1696
+ const unguard = () => {
1697
+ if (timer !== null) view.clearTimeout(timer);
1698
+ doc.removeEventListener("click", swallowOnce, true);
1699
+ doc.removeEventListener("dblclick", swallow, true);
1700
+ doc.removeEventListener("pointerup", released, true);
1701
+ doc.removeEventListener("pointercancel", unguard, true);
1702
+ if (this._unguard === unguard) this._unguard = null;
1703
+ };
1704
+ doc.addEventListener("click", swallowOnce, true);
1705
+ doc.addEventListener("dblclick", swallow, true);
1706
+ doc.addEventListener("pointerup", released, true);
1707
+ doc.addEventListener("pointercancel", unguard, true);
1708
+ this._unguard = unguard;
1709
+ }
1710
+ /** The cell a plan reaches, as `fill` takes it. */
1711
+ _reach(range, { direction, count }) {
1712
+ const cols = this.grid.columns();
1713
+ if (direction === "down") return { row: range.bottom + count, col: range.right };
1714
+ if (direction === "up") return { row: range.top - count, col: range.left };
1715
+ if (direction === "right") return { row: range.bottom, col: cols[cols.indexOf(range.right) + count] };
1716
+ return { row: range.top, col: cols[cols.indexOf(range.left) - count] };
1717
+ }
1718
+ /** The data cell under the pointer: the event's target, or (a touch,
1719
+ * whose events stay on the handle) the element at its point. */
1720
+ _cellUnder(ev) {
1721
+ const tbody = this.grid.table._tbodyEl;
1722
+ const hit = (el) => {
1723
+ const td = el?.closest?.("td");
1724
+ if (!td || td.__colIdx === void 0 || !tbody?.contains(td)) return null;
1725
+ const pos = td.parentNode?.__cellPos;
1726
+ return pos === void 0 ? null : { row: pos, col: td.__colIdx };
1727
+ };
1728
+ let cell = ev.target === this.el ? null : hit(ev.target);
1729
+ const doc = this.el.ownerDocument;
1730
+ if (!cell && typeof doc.elementFromPoint === "function") {
1731
+ cell = hit(doc.elementFromPoint(ev.clientX, ev.clientY));
1732
+ }
1733
+ return cell;
1734
+ }
1735
+ _move(ev) {
1736
+ const d = this.drag;
1737
+ if (!d) return;
1738
+ const cell = this._cellUnder(ev);
1739
+ if (!cell) return;
1740
+ d.plan = this.plan(d.range, cell);
1741
+ this.preview.clear();
1742
+ if (d.plan) {
1743
+ for (const { targets } of this._lines(d.range, d.plan)) {
1744
+ for (const t of targets) this.preview.add(`${t.pos}:${t.col}`);
1745
+ }
1746
+ }
1747
+ this.grid.paint();
1748
+ }
1749
+ _end() {
1750
+ const d = this.drag;
1751
+ if (!d) return;
1752
+ this.drag = null;
1753
+ d.unbind();
1754
+ this.preview.clear();
1755
+ this.grid.paint();
1756
+ }
1757
+ dispose() {
1758
+ this._end();
1759
+ this._unguard?.();
1760
+ this._unplace();
1761
+ this.skipped.clear();
1762
+ }
1763
+ };
1764
+
1765
+ // src/ui/components/cell_grid.js
1766
+ function isFilledValue(value) {
1767
+ if (value == null) return false;
1768
+ if (typeof value === "string") return value !== "";
1769
+ if (typeof value === "number") return !Number.isNaN(value);
1770
+ return true;
1771
+ }
1772
+ function isPrintable(ev) {
1773
+ return typeof ev.key === "string" && [...ev.key].length === 1 && !ev.ctrlKey && !ev.metaKey && !ev.altKey;
1774
+ }
1775
+ var ARROWS = {
1776
+ ArrowUp: [-1, 0],
1777
+ ArrowDown: [1, 0],
1778
+ ArrowLeft: [0, -1],
1779
+ ArrowRight: [0, 1]
1780
+ };
1781
+ var CellGrid = class {
1782
+ /** @param {import('./data_table.js').DataTable} table */
1783
+ constructor(table) {
1784
+ this.table = table;
1785
+ this.sel = new CellSelection();
1786
+ this.pending = /* @__PURE__ */ new Map();
1787
+ this.errors = /* @__PURE__ */ new Map();
1788
+ this.renderWaiting = false;
1789
+ this.rowsWaiting = /* @__PURE__ */ new Set();
1790
+ this.latest = /* @__PURE__ */ new Map();
1791
+ this.generation = 0;
1792
+ this.editor = new CellEditorHost({
1793
+ onCommit: (s, value, move, reason) => this._committed(s, value, move, reason),
1794
+ onCancel: (s, reason) => this._closed(s, reason)
1795
+ });
1796
+ this.staged = null;
1797
+ if (this._cfg().staged) {
1798
+ if (this._cfg().editable) this.staged = new StagedEdits(this);
1799
+ else console.warn("[DataTable] `staged` needs `editable`; ignored.");
1800
+ }
1801
+ this.fill = null;
1802
+ if (this._cfg().fillHandle) {
1803
+ if (this.staged) this.fill = new FillHandle(this);
1804
+ else console.warn("[DataTable] `fillHandle` needs `staged`; ignored.");
1805
+ }
1806
+ }
1807
+ // ── what the table is, read through its own helpers ──────────────────
1808
+ _cfg() {
1809
+ return this.table.config;
1810
+ }
1811
+ /** The data columns shown, in the order shown. */
1812
+ columns() {
1813
+ return this.table._shownColumns();
1814
+ }
1815
+ /** The positions of the rows on the page drawn, `[first, last]`. */
1816
+ span() {
1817
+ const s = this.table._pageSpan || { start: 0, count: 0 };
1818
+ return [s.start, s.start + s.count - 1];
1819
+ }
1820
+ _sync() {
1821
+ const [first, last] = this.span();
1822
+ this.sel.setBounds(first, last, this.columns());
1823
+ }
1824
+ /** A position's original row index (the `__rowIndex` space). */
1825
+ rowIndexAt(pos) {
1826
+ const t = this.table;
1827
+ if (t._isServerSide()) return (t.config.dataSource ? 0 : t.config.offset || 0) + pos;
1828
+ t._getProcessedRows();
1829
+ return t._processedIndexMap?.[pos] ?? pos;
1830
+ }
1831
+ rowAt(pos) {
1832
+ return this.table._getProcessedRows()[pos] ?? null;
1833
+ }
1834
+ /**
1835
+ * The row the table holds NOW for an original row index — the window's
1836
+ * row list, else `config.rows` at the place the table draws it from —
1837
+ * the one `updateRow` writes. Never a row captured earlier.
1838
+ */
1839
+ currentRow(rowIdx) {
1840
+ const t = this.table;
1841
+ const local = t._virtLocalOf(rowIdx);
1842
+ if (local >= 0) return t._virt.rows[local] ?? null;
1843
+ const rows = t.config.rows;
1844
+ if (!Array.isArray(rows)) return null;
1845
+ const at = t._isServerSide() ? rowIdx - (t.config.dataSource ? 0 : t.config.offset || 0) : rowIdx;
1846
+ return rows[at] ?? null;
1847
+ }
1848
+ /** What a cell shows: a refused or pending commit's value, else the row's. */
1849
+ shownValue(rowIdx, col) {
1850
+ const key3 = `${rowIdx}:${col}`;
1851
+ const err = this.errors.get(key3);
1852
+ if (err) return err.value;
1853
+ if (this.pending.has(key3)) return this.pending.get(key3);
1854
+ if (this.staged?.has(key3)) return this.staged.value(key3);
1855
+ const row = this.currentRow(rowIdx);
1856
+ return row ? row[col] : void 0;
1857
+ }
1858
+ /** Whether the open editor sits in this row element. */
1859
+ _holds(tr) {
1860
+ const s = this.editor.session;
1861
+ return !!s && !!tr && tr.contains(s.td);
1862
+ }
1863
+ /**
1864
+ * `updateRow` on a drawn row: the table has stored the data already. When
1865
+ * the row holds the open editor the rebuild — which would take the editor
1866
+ * out of the document — waits for that editor to close, the editor's cell
1867
+ * left as it is. Returns whether it waits.
1868
+ */
1869
+ holdRow(tr, rowIdx) {
1870
+ if (!this._holds(tr)) return false;
1871
+ this.rowsWaiting.add(rowIdx);
1872
+ return true;
1873
+ }
1874
+ valueAt(pos, col) {
1875
+ const row = this.rowAt(pos);
1876
+ return row ? row[col] : void 0;
1877
+ }
1878
+ /** The `<td>` drawing a cell, or null when it is not drawn. */
1879
+ cellEl(pos, col) {
1880
+ const tr = this.table.getRowElement(this.rowIndexAt(pos));
1881
+ if (!tr) return null;
1882
+ for (const td of tr.children) if (td.__colIdx === col) return td;
1883
+ return null;
1884
+ }
1885
+ // ── the public face (DataTable delegates) ────────────────────────────
1886
+ /** `{anchor, focus, range}`; each cell is `{row, col, rowIdx}`, and the
1887
+ * range also lists its `rows` (original indices) and `columns`. */
1888
+ get() {
1889
+ this._sync();
1890
+ const cell = (c) => c ? { row: c.row, col: c.col, rowIdx: this.rowIndexAt(c.row) } : null;
1891
+ const r = this.sel.range();
1892
+ let range = null;
1893
+ if (r) {
1894
+ const rows = [];
1895
+ for (let p = r.top; p <= r.bottom; p++) rows.push(this.rowIndexAt(p));
1896
+ range = { ...r, rows };
1897
+ }
1898
+ return { anchor: cell(this.sel.anchor), focus: cell(this.sel.focus), range };
1899
+ }
1900
+ /** Rows (original indices) the range touches, ascending. */
1901
+ rowIndices() {
1902
+ const r = this.sel.range();
1903
+ if (!r) return [];
1904
+ const out = [];
1905
+ for (let p = r.top; p <= r.bottom; p++) out.push(this.rowIndexAt(p));
1906
+ return out.sort((a, b) => a - b);
1907
+ }
1908
+ /** Select from `anchor` to `focus` (`{row, col}`, positions and data
1909
+ * indices), or put the cursor on `focus` alone. */
1910
+ set(focus, anchor = null) {
1911
+ this._sync();
1912
+ if (!focus) {
1913
+ this.sel.clear();
1914
+ this._changed();
1915
+ return;
1916
+ }
1917
+ this.sel.setCursor(anchor ? anchor.row : focus.row, anchor ? anchor.col : focus.col);
1918
+ if (anchor) this.sel.extendTo(focus.row, focus.col);
1919
+ this._changed({ reveal: true });
1920
+ }
1921
+ /** `setSelection(indices)` in cell mode: whole rows, the first to the last
1922
+ * of them as shown. */
1923
+ selectRows(indices) {
1924
+ this._sync();
1925
+ const t = this.table;
1926
+ t._getProcessedRows();
1927
+ const map = t._processedIndexMap || [];
1928
+ const positions = indices.map((i) => t._isServerSide() ? i - (t.config.dataSource ? 0 : t.config.offset || 0) : map.indexOf(i)).filter((p) => p >= 0);
1929
+ const cols = this.sel.cols;
1930
+ if (!positions.length || !cols.length) {
1931
+ this.sel.clear();
1932
+ this._changed();
1933
+ return;
1934
+ }
1935
+ this.sel.setCursor(Math.min(...positions), cols[0]);
1936
+ this.sel.extendTo(Math.max(...positions), cols[cols.length - 1]);
1937
+ this._changed();
1938
+ }
1939
+ clear() {
1940
+ this.sel.clear();
1941
+ this._changed();
1942
+ }
1943
+ /** New rows (`setData({rows})`): the marks were about the old ones. */
1944
+ newRows() {
1945
+ this.generation++;
1946
+ this.pending.clear();
1947
+ this.errors.clear();
1948
+ this.latest.clear();
1949
+ this.rowsWaiting.clear();
1950
+ this.staged?.clear();
1951
+ this.fill?.clearMarks();
1952
+ }
1953
+ isEditing() {
1954
+ return !!this.editor.session;
1955
+ }
1956
+ // ── painting ─────────────────────────────────────────────────────────
1957
+ /** Called for every row drawn — on render, on a scroll, on `updateRow` —
1958
+ * so a row that arrives later is painted like the rest. */
1959
+ decorateRow(tr) {
1960
+ const pos = tr.__cellPos;
1961
+ if (pos === void 0) return;
1962
+ for (const td of tr.children) {
1963
+ if (td.__colIdx !== void 0) this._decorateCell(td, pos, tr.__rowIndex, td.__colIdx);
1964
+ }
1965
+ }
1966
+ /** One cell: the selection, its commit's pending or refused mark, and
1967
+ * (STORY-0088) its staged value and a refused batch's mark. */
1968
+ _decorateCell(td, pos, rowIdx, col) {
1969
+ this._paintCell(td, pos, col);
1970
+ const key3 = `${rowIdx}:${col}`;
1971
+ const err = this.errors.get(key3);
1972
+ const waiting = this.pending.has(key3);
1973
+ const staged = !!this.staged?.has(key3);
1974
+ if (err || waiting) {
1975
+ td.textContent = this.table._formatValue(err ? err.value : this.pending.get(key3), col);
1976
+ } else if (staged) {
1977
+ td.textContent = this.table._formatValue(this.staged.value(key3), col);
1978
+ }
1979
+ const message = err ? err.message : this.staged?.errors.get(key3);
1980
+ td.classList.toggle("twm-dt-cell--pending", waiting);
1981
+ td.classList.toggle("twm-dt-cell--staged", staged);
1982
+ td.classList.toggle("twm-dt-cell--error", message != null);
1983
+ if (message != null) {
1984
+ td.title = message;
1985
+ td.setAttribute("data-error", message);
1986
+ } else td.removeAttribute("data-error");
1987
+ this.fill?.paintCell(td, pos, col);
1988
+ }
1989
+ _paintCell(td, pos, col) {
1990
+ td.classList.toggle("twm-dt-cell--selected", this.sel.contains(pos, col));
1991
+ const cursor = this.sel.isCursor(pos, col);
1992
+ td.classList.toggle("twm-dt-cell--cursor", cursor);
1993
+ if (cursor) td.setAttribute("aria-selected", "true");
1994
+ else td.removeAttribute("aria-selected");
1995
+ this.fill?.paintCell(td, pos, col);
1996
+ }
1997
+ paint() {
1998
+ const tbody = this.table._tbodyEl;
1999
+ if (!tbody) return;
2000
+ for (const tr of tbody.children) {
2001
+ const pos = tr.__cellPos;
2002
+ if (pos === void 0) continue;
2003
+ for (const td of tr.children) {
2004
+ if (td.__colIdx !== void 0) this._paintCell(td, pos, td.__colIdx);
2005
+ }
2006
+ }
2007
+ }
2008
+ /** The selection moved: the cells, the rows `getSelection()` answers, and
2009
+ * whoever listens. */
2010
+ _changed({ reveal = false } = {}) {
2011
+ const t = this.table;
2012
+ this.fill?.clearMarks();
2013
+ if (reveal && this.sel.focus) t.scrollToRow(this.rowIndexAt(this.sel.focus.row));
2014
+ this.paint();
2015
+ const before = t.getSelection().join();
2016
+ const rows = this.rowIndices();
2017
+ t._state.selected.clear();
2018
+ for (const i of rows) t._state.selected.add(i);
2019
+ t._state.anchorIndex = this.sel.anchor ? this.rowIndexAt(this.sel.anchor.row) : null;
2020
+ if (rows.join() !== before) t._notifySelectionChange();
2021
+ try {
2022
+ t.config.onCellSelectionChange?.(this.get());
2023
+ } catch (err) {
2024
+ console.error("[DataTable] onCellSelectionChange threw", err);
2025
+ }
2026
+ }
2027
+ // ── the pointer and the keys ─────────────────────────────────────────
2028
+ /** Bound by `_installInteractions` on every render, BEFORE the table's own
2029
+ * activation, detail and copy listeners, so a key the cell mode takes
2030
+ * goes no further. Listeners go on the table's disposers. */
2031
+ install(tbody, table) {
2032
+ const t = this.table;
2033
+ const on = (el, type, fn) => {
2034
+ el.addEventListener(type, fn);
2035
+ t._disposers.push(() => el.removeEventListener(type, fn));
2036
+ };
2037
+ this._sync();
2038
+ const cellOf = (target) => {
2039
+ const td = target?.closest?.("td");
2040
+ if (!td || td.__colIdx === void 0 || !tbody.contains(td)) return null;
2041
+ const tr = td.parentNode;
2042
+ if (tr?.__cellPos === void 0) return null;
2043
+ return { td, row: tr.__cellPos, col: td.__colIdx };
2044
+ };
2045
+ const inEditor = (target) => !!this.editor.session && this.editor.session.td.contains(target);
2046
+ on(tbody, "click", (ev) => {
2047
+ if (ev.button != null && ev.button !== 0) return;
2048
+ if (inEditor(ev.target)) return;
2049
+ const hit = cellOf(ev.target);
2050
+ if (!hit) return;
2051
+ this._sync();
2052
+ if (ev.shiftKey && this.sel.anchor) this.sel.extendTo(hit.row, hit.col);
2053
+ else this.sel.setCursor(hit.row, hit.col);
2054
+ this._changed();
2055
+ try {
2056
+ table.focus({ preventScroll: true });
2057
+ } catch (_) {
2058
+ }
2059
+ });
2060
+ on(tbody, "dblclick", (ev) => {
2061
+ if (inEditor(ev.target)) return;
2062
+ const hit = cellOf(ev.target);
2063
+ if (!hit || !this.canEdit(hit.row, hit.col)) return;
2064
+ this._sync();
2065
+ this.sel.setCursor(hit.row, hit.col);
2066
+ this._changed();
2067
+ if (this.open(hit.row, hit.col)) ev.stopImmediatePropagation();
2068
+ });
2069
+ on(tbody, "contextmenu", (ev) => {
2070
+ if (inEditor(ev.target)) return;
2071
+ const hit = cellOf(ev.target);
2072
+ if (!hit || this.sel.contains(hit.row, hit.col)) return;
2073
+ this._sync();
2074
+ this.sel.setCursor(hit.row, hit.col);
2075
+ this._changed();
2076
+ });
2077
+ on(table, "keydown", (ev) => {
2078
+ if (ev.isComposing || inEditor(ev.target)) return;
2079
+ if (ev.target !== table && ev.target?.closest?.('input, select, textarea, button, [contenteditable="true"]')) return;
2080
+ if (this._onKey(ev)) {
2081
+ ev.preventDefault();
2082
+ ev.stopImmediatePropagation();
2083
+ }
2084
+ });
2085
+ this.paint();
2086
+ this.staged?.paintFreeze();
2087
+ }
2088
+ /** One key on the focused table. Returns whether the cell mode took it. */
2089
+ _onKey(ev) {
2090
+ this._sync();
2091
+ const ctrl = ev.ctrlKey || ev.metaKey;
2092
+ const k = ev.key;
2093
+ if (this.staged && ctrl && !ev.altKey) {
2094
+ const lower = String(k).toLowerCase();
2095
+ if (lower === "z" && !ev.shiftKey) {
2096
+ this.staged.undo();
2097
+ return true;
2098
+ }
2099
+ if (lower === "y" || lower === "z" && ev.shiftKey) {
2100
+ this.staged.redo();
2101
+ return true;
2102
+ }
2103
+ }
2104
+ if (ARROWS[k] && !ev.altKey) {
2105
+ const [dr, dc] = ARROWS[k];
2106
+ if (ctrl) {
2107
+ this.sel.jump(dr, dc, (r, c) => isFilledValue(this.valueAt(r, c)), { extend: ev.shiftKey });
2108
+ } else this.sel.move(dr, dc, { extend: ev.shiftKey });
2109
+ this._changed({ reveal: true });
2110
+ return true;
2111
+ }
2112
+ if ((k === "Home" || k === "End") && !ev.altKey) {
2113
+ this.sel.lineEnd(k === "End", { extend: ev.shiftKey, corner: ctrl });
2114
+ this._changed({ reveal: true });
2115
+ return true;
2116
+ }
2117
+ if (k === "Tab" && !ctrl && !ev.altKey) {
2118
+ if (this.sel.tabEnds(ev.shiftKey)) return false;
2119
+ this.sel.tab(ev.shiftKey);
2120
+ this._changed({ reveal: true });
2121
+ return true;
2122
+ }
2123
+ if (ctrl && !ev.altKey && String(k).toLowerCase() === "a") {
2124
+ this.sel.selectAll();
2125
+ this._changed();
2126
+ return true;
2127
+ }
2128
+ if (k === "Escape") {
2129
+ this.sel.collapse();
2130
+ this._changed();
2131
+ this.table._hideContextMenu();
2132
+ return true;
2133
+ }
2134
+ const f = this.sel.focus;
2135
+ if (!f) return false;
2136
+ if ((k === "F2" || k === "Enter" && !ctrl && !ev.altKey && !ev.shiftKey) && this.canEdit(f.row, f.col)) {
2137
+ return this.open(f.row, f.col);
2138
+ }
2139
+ if (isPrintable(ev) && this.canEdit(f.row, f.col)) {
2140
+ return this.open(f.row, f.col, { initialText: ev.key });
2141
+ }
2142
+ return false;
2143
+ }
2144
+ // ── editing ──────────────────────────────────────────────────────────
2145
+ /** Whether a cell opens an editor: `editable` says so for its column. */
2146
+ canEdit(pos, col) {
2147
+ const spec = this._cfg().editable;
2148
+ if (!spec) return false;
2149
+ if (this.staged?.committing) return false;
2150
+ if (spec === true) return true;
2151
+ if (Array.isArray(spec)) return spec.includes(col);
2152
+ if (typeof spec === "function") {
2153
+ try {
2154
+ return !!spec(col, this.rowAt(pos), this.rowIndexAt(pos));
2155
+ } catch (err) {
2156
+ console.error("[DataTable] editable threw", err);
2157
+ return false;
2158
+ }
2159
+ }
2160
+ return false;
2161
+ }
2162
+ /** The column's type, as `editors` is keyed by: the declared one, else the
2163
+ * detected one. */
2164
+ _typeOf(col) {
2165
+ const declared = this._cfg().columnTypes?.[col];
2166
+ return declared || this.table._columnTypes?.[col] || "text";
2167
+ }
2168
+ _factoryFor(col) {
2169
+ const editors = this._cfg().editors;
2170
+ if (editors && typeof editors === "object") {
2171
+ if (typeof editors[col] === "function") return editors[col];
2172
+ const type = this._typeOf(col);
2173
+ if (typeof editors[type] === "function") return editors[type];
2174
+ }
2175
+ return createTextEditor;
2176
+ }
2177
+ /**
2178
+ * Open the editor on a cell (position, data column). Brings the row into
2179
+ * view first. Returns whether an editor opened.
2180
+ */
2181
+ open(pos, col, { initialText = null } = {}) {
2182
+ if (this.editor.session || !this.canEdit(pos, col)) return false;
2183
+ this.table.scrollToRow(this.rowIndexAt(pos));
2184
+ const td = this.cellEl(pos, col);
2185
+ if (!td) return false;
2186
+ const rowIdx = this.rowIndexAt(pos);
2187
+ const row = this.currentRow(rowIdx);
2188
+ const headers = this._cfg().headers || [];
2189
+ return this.editor.open({
2190
+ td,
2191
+ factory: this._factoryFor(col),
2192
+ pos,
2193
+ colIdx: col,
2194
+ rowIdx,
2195
+ row,
2196
+ generation: this.generation,
2197
+ value: this.shownValue(rowIdx, col),
2198
+ column: { index: col, header: headers[col], type: this._typeOf(col) },
2199
+ initialText
2200
+ });
2201
+ }
2202
+ /** The virtual window is taking a row out: an editor in it commits, as a
2203
+ * click elsewhere would — what was typed is not thrown away. An editor
2204
+ * with no `getValue` has nothing to commit, and is cancelled. */
2205
+ rowLeaving(tr) {
2206
+ const s = this.editor.session;
2207
+ if (s && tr.contains(s.td)) this.editor.commit(null, "scroll");
2208
+ }
2209
+ dispose() {
2210
+ this.fill?.dispose();
2211
+ this.editor.cancel("dispose");
2212
+ this.renderWaiting = false;
2213
+ this.rowsWaiting.clear();
2214
+ }
2215
+ /** After every close: focus back on the table when the keyboard closed it,
2216
+ * and the render that waited for the editor. */
2217
+ _closed(s, reason) {
2218
+ const t = this.table;
2219
+ if (reason === "dispose") return;
2220
+ if (reason === "key" || reason === "editor") {
2221
+ try {
2222
+ t._tableEl?.focus({ preventScroll: true });
2223
+ } catch (_) {
2224
+ }
2225
+ }
2226
+ if (this.renderWaiting) {
2227
+ this.renderWaiting = false;
2228
+ this.rowsWaiting.clear();
2229
+ if (reason === "scroll") queueMicrotask(() => {
2230
+ if (t._wrapperEl) t.render();
2231
+ });
2232
+ else t.render();
2233
+ return;
2234
+ }
2235
+ if (this.rowsWaiting.size) {
2236
+ const flush = () => {
2237
+ const waiting = [...this.rowsWaiting];
2238
+ this.rowsWaiting.clear();
2239
+ for (const rowIdx of waiting) this._redraw(rowIdx);
2240
+ };
2241
+ if (reason === "scroll") queueMicrotask(() => {
2242
+ if (t._wrapperEl) flush();
2243
+ });
2244
+ else flush();
2245
+ }
2246
+ }
2247
+ /**
2248
+ * Draw a row again after a commit, from the row the table holds now
2249
+ * (`currentRow`). `updateRow` rebuilds EVERY cell of the row, so while an
2250
+ * editor is open in it (save, Tab, type — the commit settles under the
2251
+ * next editor) the row waits for that editor to close (`_closed`), and
2252
+ * only the committed cell's marks are painted now.
2253
+ */
2254
+ _redraw(rowIdx, col = null) {
2255
+ const t = this.table;
2256
+ const tr = t.getRowElement(rowIdx);
2257
+ const row = this.currentRow(rowIdx);
2258
+ if (!tr || !row) return;
2259
+ if (this._holds(tr)) {
2260
+ this.rowsWaiting.add(rowIdx);
2261
+ if (col === null || tr.__cellPos === void 0) return;
2262
+ const s = this.editor.session;
2263
+ for (const td of tr.children) {
2264
+ if (td.__colIdx === col && td !== s.td) this._decorateCell(td, tr.__cellPos, rowIdx, col);
2265
+ }
2266
+ return;
2267
+ }
2268
+ this.rowsWaiting.delete(rowIdx);
2269
+ t.updateRow(rowIdx, row);
2270
+ }
2271
+ _committed(s, value, move, reason) {
2272
+ if (move) {
2273
+ this._sync();
2274
+ if (move === "down" || move === "up") this.sel.move(move === "down" ? 1 : -1, 0);
2275
+ else this.sel.tab(move === "left");
2276
+ this._changed({ reveal: true });
2277
+ }
2278
+ const live = s.generation === this.generation;
2279
+ const key3 = `${s.rowIdx}:${s.colIdx}`;
2280
+ if (Object.is(value, s.previous)) {
2281
+ this._closed(s, reason);
2282
+ return;
2283
+ }
2284
+ if (this.staged) {
2285
+ if (live) this.staged.stage(s.rowIdx, s.colIdx, value);
2286
+ this._closed(s, reason);
2287
+ return;
2288
+ }
2289
+ const held = live ? this.currentRow(s.rowIdx) : s.row;
2290
+ const stored = held ? held[s.colIdx] : void 0;
2291
+ if (live && this.errors.has(key3) && !this.pending.has(key3) && Object.is(value, stored)) {
2292
+ this.errors.delete(key3);
2293
+ this.latest.delete(key3);
2294
+ this._redraw(s.rowIdx, s.colIdx);
2295
+ this._closed(s, reason);
2296
+ return;
2297
+ }
2298
+ const fn = this._cfg().onCellCommit;
2299
+ const token = {};
2300
+ if (live) this.latest.set(key3, token);
2301
+ const finish = (result2) => {
2302
+ if (!live || s.generation !== this.generation) return;
2303
+ const newest = this.latest.get(key3) === token;
2304
+ const good = !(result2 && result2.ok === false);
2305
+ if (good) {
2306
+ const row = this.currentRow(s.rowIdx);
2307
+ if (Array.isArray(row)) row[s.colIdx] = value;
2308
+ }
2309
+ if (newest) {
2310
+ this.latest.delete(key3);
2311
+ this.pending.delete(key3);
2312
+ if (good) this.errors.delete(key3);
2313
+ else this.errors.set(key3, { value, message: String(result2.error ?? "Not saved") });
2314
+ }
2315
+ this._redraw(s.rowIdx, s.colIdx);
2316
+ };
2317
+ const refused = (err) => ({ ok: false, error: err?.message || String(err ?? "Not saved") });
2318
+ let result;
2319
+ if (typeof fn === "function") {
2320
+ try {
2321
+ result = fn({ rowIdx: s.rowIdx, colIdx: s.colIdx, value, previous: stored });
2322
+ } catch (err) {
2323
+ result = refused(err);
2324
+ }
2325
+ }
2326
+ if (result && typeof result.then === "function") {
2327
+ if (live) {
2328
+ this.pending.set(key3, value);
2329
+ this.errors.delete(key3);
2330
+ this._redraw(s.rowIdx, s.colIdx);
2331
+ }
2332
+ this._closed(s, reason);
2333
+ result.then(finish, (err) => finish(refused(err)));
2334
+ return;
2335
+ }
2336
+ finish(result);
2337
+ this._closed(s, reason);
2338
+ }
2339
+ };
2340
+
2341
+ // src/ui/components/column_order.js
2342
+ function isColumnOrder(order, n) {
2343
+ if (!Array.isArray(order) || order.length !== n) return false;
2344
+ const seen = /* @__PURE__ */ new Set();
2345
+ for (const c of order) {
2346
+ if (!Number.isInteger(c) || c < 0 || c >= n || seen.has(c)) return false;
2347
+ seen.add(c);
2348
+ }
2349
+ return true;
2350
+ }
2351
+ function naturalOrder(n) {
2352
+ return Array.from({ length: Math.max(0, n) }, (_, i) => i);
2353
+ }
2354
+ function cellOrder(order, count) {
2355
+ if (!order) return naturalOrder(count);
2356
+ const out = order.filter((c) => c < count);
2357
+ for (let c = order.length; c < count; c++) out.push(c);
2358
+ return out;
2359
+ }
2360
+ function moveInOrder(order, from, to) {
2361
+ const next = order.slice();
2362
+ const [moved] = next.splice(from, 1);
2363
+ next.splice(to, 0, moved);
2364
+ return next;
2365
+ }
2366
+ var ColumnReorder = class {
2367
+ /** @param {import('./data_table.js').DataTable} table */
2368
+ constructor(table) {
2369
+ this.table = table;
2370
+ this.dragging = null;
2371
+ this.onGrip = false;
2372
+ }
2373
+ /** Wire the header row of a fresh render. Its cells are new each render,
2374
+ * so nothing needs taking off. */
2375
+ install(headRow) {
2376
+ if (!headRow) return;
2377
+ [...headRow.children].forEach((th, domIdx) => {
2378
+ const col = this.table._dataIdxOfDom(domIdx);
2379
+ if (col === null) return;
2380
+ th.draggable = true;
2381
+ th.classList.add("twm-dt-col--movable");
2382
+ th.addEventListener("mousedown", (ev) => {
2383
+ this.onGrip = !!ev.target?.closest?.(".twm-dt-col-resizer");
2384
+ }, true);
2385
+ th.addEventListener("dragstart", (ev) => {
2386
+ if (this.onGrip || ev.target?.closest?.(".twm-dt-col-resizer")) {
2387
+ ev.preventDefault();
2388
+ return;
2389
+ }
2390
+ this.dragging = col;
2391
+ try {
2392
+ ev.dataTransfer?.setData("text/plain", String(col));
2393
+ if (ev.dataTransfer) ev.dataTransfer.effectAllowed = "move";
2394
+ } catch (_) {
2395
+ }
2396
+ th.classList.add("twm-dt-col--dragging");
2397
+ });
2398
+ th.addEventListener("dragover", (ev) => {
2399
+ if (this.dragging === null || this.dragging === col) return;
2400
+ ev.preventDefault();
2401
+ if (ev.dataTransfer) ev.dataTransfer.dropEffect = "move";
2402
+ const order = this.table.getColumnOrder();
2403
+ th.classList.add("twm-dt-col--drop");
2404
+ th.classList.toggle("twm-dt-col--drop-after", order.indexOf(col) > order.indexOf(this.dragging));
2405
+ });
2406
+ th.addEventListener("dragleave", () => th.classList.remove("twm-dt-col--drop", "twm-dt-col--drop-after"));
2407
+ th.addEventListener("drop", (ev) => {
2408
+ th.classList.remove("twm-dt-col--drop", "twm-dt-col--drop-after");
2409
+ const from = this.dragging;
2410
+ this.dragging = null;
2411
+ if (from === null || from === col) return;
2412
+ ev.preventDefault();
2413
+ const order = this.table.getColumnOrder();
2414
+ this.table.moveColumn(order.indexOf(from), order.indexOf(col));
2415
+ });
2416
+ th.addEventListener("dragend", () => {
2417
+ th.classList.remove("twm-dt-col--dragging");
2418
+ this.dragging = null;
2419
+ });
2420
+ });
2421
+ }
2422
+ /** "Move left" / "Move right" for the header menu of data column `col`:
2423
+ * past the shown column next to it. */
2424
+ menuItems(col) {
2425
+ const shown = this.table._shownColumns();
2426
+ const at = shown.indexOf(col);
2427
+ if (at < 0) return [];
2428
+ const order = this.table.getColumnOrder();
2429
+ const step = (to) => () => this.table.moveColumn(order.indexOf(col), order.indexOf(to));
2430
+ return [
2431
+ {
2432
+ label: "Move left",
2433
+ icon: "arrow_back",
2434
+ action: "move-left",
2435
+ disabled: at === 0,
2436
+ onChoose: at === 0 ? void 0 : step(shown[at - 1])
2437
+ },
2438
+ {
2439
+ label: "Move right",
2440
+ icon: "arrow_forward",
2441
+ action: "move-right",
2442
+ disabled: at === shown.length - 1,
2443
+ onChoose: at === shown.length - 1 ? void 0 : step(shown[at + 1])
2444
+ }
2445
+ ];
2446
+ }
2447
+ };
2448
+
2449
+ // src/ui/components/header_actions.js
2450
+ function callOption(config, name, ...args) {
2451
+ const fn = config?.[name];
2452
+ if (typeof fn !== "function") return void 0;
2453
+ try {
2454
+ return fn.apply(config, args);
2455
+ } catch (err) {
2456
+ console.error(`[DataTable] ${name} threw`, err);
2457
+ return void 0;
2458
+ }
2459
+ }
2460
+ function headerActionButton(className, { icon, label, title, disabled } = {}, onClick) {
2461
+ const b = document.createElement("button");
2462
+ b.type = "button";
2463
+ b.className = className;
2464
+ if (icon) {
2465
+ const span = document.createElement("span");
2466
+ span.className = "material-symbols-outlined";
2467
+ span.textContent = String(icon);
2468
+ b.appendChild(span);
2469
+ } else if (label != null) {
2470
+ b.appendChild(document.createTextNode(String(label)));
2471
+ }
2472
+ const name = title ?? label;
2473
+ if (name != null) {
2474
+ b.title = String(name);
2475
+ b.setAttribute("aria-label", String(name));
2476
+ }
2477
+ if (disabled) b.disabled = true;
2478
+ b.addEventListener("mousedown", (ev) => ev.stopPropagation());
2479
+ b.addEventListener("click", (ev) => {
2480
+ ev.preventDefault();
2481
+ ev.stopPropagation();
2482
+ onClick(ev);
2483
+ });
2484
+ return b;
2485
+ }
2486
+
2487
+ // src/ui/components/column_groups.js
2488
+ function normalizeSpans(spec, n, { requireKey = false, defaultLabel = () => "" } = {}) {
2489
+ const taken = /* @__PURE__ */ new Set();
2490
+ const out = [];
2491
+ (Array.isArray(spec) ? spec : []).forEach((g, index) => {
2492
+ if (!g || requireKey && g.key == null) return;
2493
+ const columns = [];
2494
+ for (const c of Array.isArray(g.columns) ? g.columns : []) {
2495
+ const i = Number(c);
2496
+ if (!Number.isInteger(i) || i < 0 || i >= n || taken.has(i)) continue;
2497
+ taken.add(i);
2498
+ columns.push(i);
2499
+ }
2500
+ out.push({ key: g.key, label: g.label == null ? defaultLabel(g) : String(g.label), columns, index });
2501
+ });
2502
+ return out;
2503
+ }
2504
+ function normalize(spec, n) {
2505
+ return normalizeSpans(spec, n, { requireKey: true, defaultLabel: (g) => String(g.key) });
2506
+ }
2507
+ function blankSpanCell(span) {
2508
+ const th = document.createElement("th");
2509
+ th.className = "twm-dt-group twm-dt-group--none";
2510
+ th.colSpan = span;
2511
+ return th;
2512
+ }
2513
+ function labelSpanCell(span, label, title = label) {
2514
+ const th = document.createElement("th");
2515
+ th.className = "twm-dt-group";
2516
+ th.colSpan = span;
2517
+ th.title = title;
2518
+ const el = document.createElement("span");
2519
+ el.className = "twm-dt-group__label";
2520
+ el.textContent = label;
2521
+ th.appendChild(el);
2522
+ return th;
2523
+ }
2524
+ var ColumnGroups = class {
2525
+ /** @param {import('./data_table.js').DataTable} table */
2526
+ constructor(table) {
2527
+ this.table = table;
2528
+ this._memoKey = null;
2529
+ this._memo = null;
2530
+ this._el = null;
2531
+ }
2532
+ _cfg() {
2533
+ return this.table.config;
2534
+ }
2535
+ /**
2536
+ * The drawn columns after the lead, in order: `{col, group}` for a shown
2537
+ * data column (`group` the index in `groups`, or null) and
2538
+ * `{placeholder: true, group}` for an empty group's column. The data
2539
+ * columns come in the order the table draws them (STORY-0086's
2540
+ * `columnOrder`, hidden ones left out). Memoised on the groups' content,
2541
+ * the column count and the drawn columns.
2542
+ */
2543
+ layout() {
2544
+ const cfg = this._cfg();
2545
+ const n = cfg.headers?.length || 0;
2546
+ const shown = this.table._shownColumns();
2547
+ const spec = Array.isArray(cfg.columnGroups) ? cfg.columnGroups : [];
2548
+ const key3 = `${n}|${shown.join(",")}|${spec.map((g) => g ? `${g.key}${g.label}${(g.columns || []).join(",")}` : "").join("")}`;
2549
+ if (key3 === this._memoKey) return this._memo;
2550
+ const groups = normalize(spec, n);
2551
+ const groupOf = new Array(n).fill(null);
2552
+ groups.forEach((g, gi) => {
2553
+ for (const c of g.columns) groupOf[c] = gi;
2554
+ });
2555
+ const empty = groups.map((g) => g.columns.length === 0);
2556
+ const placed = /* @__PURE__ */ new Set();
2557
+ const slots = [];
2558
+ const placeEmptyBefore = (gi) => {
2559
+ for (let e = 0; e < gi; e++) {
2560
+ if (empty[e] && !placed.has(e)) {
2561
+ placed.add(e);
2562
+ slots.push({ placeholder: true, group: e });
2563
+ }
2564
+ }
2565
+ };
2566
+ for (const c of shown) {
2567
+ const gi = groupOf[c];
2568
+ if (gi !== null) placeEmptyBefore(gi);
2569
+ slots.push({ col: c, group: gi });
2570
+ }
2571
+ placeEmptyBefore(groups.length);
2572
+ const slotOfCol = /* @__PURE__ */ new Map();
2573
+ slots.forEach((s, i) => {
2574
+ if (!s.placeholder) slotOfCol.set(s.col, i);
2575
+ });
2576
+ const placeholders = slots.filter((s) => s.placeholder).length;
2577
+ this._memoKey = key3;
2578
+ this._memo = { groups, slots, slotOfCol, placeholders };
2579
+ return this._memo;
2580
+ }
2581
+ /** How many placeholder columns are drawn. */
2582
+ placeholderCount() {
2583
+ return this.layout().placeholders;
2584
+ }
2585
+ /** A shown data column's place after the lead, or -1 when the layout
2586
+ * has no placeholder (the table's own arithmetic then holds). */
2587
+ slotOf(colIdx) {
2588
+ const l = this.layout();
2589
+ if (!l.placeholders) return -1;
2590
+ return l.slotOfCol.has(colIdx) ? l.slotOfCol.get(colIdx) : -1;
2591
+ }
2592
+ /** What the drawn column `i` after the lead is: the data column, null for
2593
+ * a placeholder or past the end, `undefined` when the layout has no
2594
+ * placeholder (the table's own arithmetic then holds). */
2595
+ slotAt(i) {
2596
+ const l = this.layout();
2597
+ if (!l.placeholders) return void 0;
2598
+ const s = l.slots[i];
2599
+ return s && !s.placeholder ? s.col : null;
2600
+ }
2601
+ /** Part of the table's column signature: where the placeholders stand. */
2602
+ sig() {
2603
+ const l = this.layout();
2604
+ if (!l.placeholders) return "";
2605
+ return `|g${l.slots.map((s, i) => s.placeholder ? i : "").filter((x) => x !== "").join(",")}`;
2606
+ }
2607
+ /** The drawn positions (after the lead) of the placeholders, ascending,
2608
+ * with their groups. */
2609
+ _placeholderSlots() {
2610
+ const out = [];
2611
+ this.layout().slots.forEach((s, i) => {
2612
+ if (s.placeholder) out.push({ at: i, group: s.group });
2613
+ });
2614
+ return out;
2615
+ }
2616
+ /** Put the placeholders' cells into a drawn row whose cells are the lead
2617
+ * and the shown data columns, in order. */
2618
+ _insert(tr, make) {
2619
+ const lead = this.table._lead().all;
2620
+ for (const { at, group } of this._placeholderSlots()) {
2621
+ const ref = tr.children[lead + at] || null;
2622
+ tr.insertBefore(make(this.layout().groups[group]), ref);
2623
+ }
2624
+ }
2625
+ /** STORY-0066. A body row gets an empty cell under each placeholder. */
2626
+ fillRow(tr) {
2627
+ if (!this.placeholderCount()) return;
2628
+ this._insert(tr, () => {
2629
+ const td = document.createElement("td");
2630
+ td.className = "twm-dt-cell--placeholder";
2631
+ return td;
2632
+ });
2633
+ }
2634
+ /**
2635
+ * After the header is built and in the header table: the placeholders'
2636
+ * header and filter cells, and the group table above the header table.
2637
+ */
2638
+ decorate(headerWrap, headerTable) {
2639
+ this._el = null;
2640
+ if (!headerWrap || !headerTable) return;
2641
+ const thead = headerTable.querySelector("thead");
2642
+ const labelRow = thead?.querySelector("tr");
2643
+ if (!labelRow) return;
2644
+ if (this.placeholderCount()) {
2645
+ this._insert(labelRow, (g) => this._placeholderHead(g));
2646
+ for (const tr of thead.querySelectorAll("tr.twm-data-table__filter-row")) {
2647
+ this._insert(tr, () => {
2648
+ const th = document.createElement("th");
2649
+ th.className = "data-table__filter-cell data-table__filter-cell--empty twm-dt-col--placeholder";
2650
+ return th;
2651
+ });
2652
+ }
2653
+ }
2654
+ const groupTable = this._buildGroupTable(headerTable);
2655
+ headerWrap.insertBefore(groupTable, headerTable);
2656
+ this._el = groupTable;
2657
+ this.align();
2658
+ }
2659
+ _placeholderHead(group) {
2660
+ const th = document.createElement("th");
2661
+ th.className = "text twm-dt-col--placeholder";
2662
+ if (typeof this._cfg().onColumnAdd === "function") {
2663
+ th.appendChild(this._addButton(group));
2664
+ }
2665
+ return th;
2666
+ }
2667
+ _addButton(group) {
2668
+ return headerActionButton(
2669
+ "twm-dt-group__add",
2670
+ { icon: "add", title: `Add column to ${group.label}` },
2671
+ () => callOption(this._cfg(), "onColumnAdd", group.key)
2672
+ );
2673
+ }
2674
+ _buildGroupTable(headerTable) {
2675
+ const { groups, slots } = this.layout();
2676
+ const lead = this.table._lead().all;
2677
+ const table = document.createElement("table");
2678
+ table.className = `${headerTable.className} twm-dt-groups`;
2679
+ const colgroup = document.createElement("colgroup");
2680
+ for (let i2 = 0; i2 < lead + slots.length; i2++) colgroup.appendChild(document.createElement("col"));
2681
+ table.appendChild(colgroup);
2682
+ const thead = document.createElement("thead");
2683
+ const tr = document.createElement("tr");
2684
+ tr.className = "twm-dt-group-row";
2685
+ if (lead) {
2686
+ const th = document.createElement("th");
2687
+ th.className = "twm-dt-group twm-dt-group--lead";
2688
+ th.colSpan = lead;
2689
+ tr.appendChild(th);
2690
+ }
2691
+ let i = 0;
2692
+ while (i < slots.length) {
2693
+ const gi = slots[i].group;
2694
+ let j = i + 1;
2695
+ while (j < slots.length && slots[j].group === gi) j++;
2696
+ tr.appendChild(gi === null ? blankSpanCell(j - i) : this._groupCell(groups[gi], j - i, slots[i].placeholder));
2697
+ i = j;
2698
+ }
2699
+ thead.appendChild(tr);
2700
+ table.appendChild(thead);
2701
+ return table;
2702
+ }
2703
+ _groupCell(group, span, empty) {
2704
+ const th = labelSpanCell(span, group.label);
2705
+ if (empty) th.classList.add("twm-dt-group--empty");
2706
+ const actions = document.createElement("span");
2707
+ actions.className = "twm-dt-group__actions";
2708
+ const cfg = this._cfg();
2709
+ const items = callOption(cfg, "groupActions", cfg.columnGroups[group.index], { key: group.key, columns: group.columns.slice(), table: this.table });
2710
+ for (const item of Array.isArray(items) ? items.filter(Boolean) : []) {
2711
+ actions.appendChild(headerActionButton(
2712
+ "twm-dt-group__action",
2713
+ item,
2714
+ () => callOption(cfg, "onGroupAction", item.action, group.key)
2715
+ ));
2716
+ }
2717
+ if (typeof cfg.onColumnAdd === "function") actions.appendChild(this._addButton(group));
2718
+ if (actions.childNodes.length) th.appendChild(actions);
2719
+ return th;
2720
+ }
2721
+ /** The group table follows the header table: the same column widths,
2722
+ * table width, layout and sideways offset. */
2723
+ align() {
2724
+ const groupTable = this._el;
2725
+ const headerTable = this.table._headerTableEl;
2726
+ if (!groupTable || !headerTable) return;
2727
+ const labelRow = headerTable.querySelector("thead > tr");
2728
+ const cols = groupTable.querySelector("colgroup")?.children || [];
2729
+ for (let i = 0; i < cols.length; i++) {
2730
+ const th = labelRow?.children[i];
2731
+ const w = th ? parseFloat(th.style.width) : NaN;
2732
+ cols[i].style.width = Number.isFinite(w) ? `${w}px` : "";
2733
+ }
2734
+ groupTable.style.width = headerTable.style.width;
2735
+ groupTable.style.tableLayout = headerTable.style.tableLayout;
2736
+ groupTable.style.transform = headerTable.style.transform;
2737
+ }
2738
+ };
360
2739
 
361
- // src/ui/components/table_filter_chips.js
362
- var isAll = (v) => v === "" || v == null;
363
- function createChipsFilter(ctx = {}, spec = {}) {
364
- const options = Array.isArray(spec.options) ? spec.options : [];
365
- const multiple = !!spec.multiple;
366
- const showCounts = spec.counts !== false;
367
- const el = document.createElement("div");
368
- el.className = "twm-dt-filter-chips" + (multiple ? " twm-dt-filter-chips--multiple" : "");
369
- el.setAttribute("role", "group");
370
- const name = spec.label || ctx.header;
371
- if (name) el.setAttribute("aria-label", String(name));
372
- const valueOf = /* @__PURE__ */ new Map();
373
- const chips = /* @__PURE__ */ new Map();
374
- let value = null;
375
- const keyOf = (v) => String(v ?? "");
376
- const normalise = (v) => {
377
- if (multiple) {
378
- const list = Array.isArray(v) ? v : isAll(v) ? [] : [v];
379
- return list.filter((x) => !isAll(x)).map(keyOf);
2740
+ // src/ui/components/crosstab.js
2741
+ var DEFAULT_EMPTY_LABEL = "(none)";
2742
+ function unzoom(el) {
2743
+ return 1 / (el?.currentCSSZoom || 1);
2744
+ }
2745
+ var Crosstab = class {
2746
+ /** @param {import('./data_table.js').DataTable} table */
2747
+ constructor(table) {
2748
+ this.table = table;
2749
+ this._lefts = [];
2750
+ this._leafRow = null;
2751
+ }
2752
+ _spec() {
2753
+ const s = this.table.config.crosstab;
2754
+ return s && typeof s === "object" ? s : {};
2755
+ }
2756
+ /** How many of the drawn data columns are row labels: the leading ones. */
2757
+ labelCount() {
2758
+ const k = Math.floor(Number(this._spec().labelColumns) || 0);
2759
+ return Math.max(0, Math.min(k, this.table._shownColumns().length));
2760
+ }
2761
+ /** The drawn columns that pin to the left: the select and row-number
2762
+ * columns, and the label columns. */
2763
+ pinnedCount() {
2764
+ return this.table._lead().all + this.labelCount();
2765
+ }
2766
+ /** The drawn data columns after the label columns, in order. */
2767
+ bodyColumns() {
2768
+ return this.table._shownColumns().slice(this.labelCount());
2769
+ }
2770
+ /** Columns drawn that are no data column: the empty axis's one. */
2771
+ extraColumns() {
2772
+ return this.bodyColumns().length ? 0 : 1;
2773
+ }
2774
+ /** The header rows as drawn: per row, `{cell, span}` runs over the body
2775
+ * columns (`cell` null for a run in no cell of that row). None for an
2776
+ * empty axis. */
2777
+ headerRuns() {
2778
+ const body = this.bodyColumns();
2779
+ if (!body.length) return [];
2780
+ const spec = this._spec().headerRows;
2781
+ const n = this.table.config.headers?.length || 0;
2782
+ return (Array.isArray(spec) ? spec : []).map((rowSpec) => {
2783
+ const cells = normalizeSpans(rowSpec, n);
2784
+ const cellOf = /* @__PURE__ */ new Map();
2785
+ cells.forEach((c) => {
2786
+ for (const col of c.columns) cellOf.set(col, c);
2787
+ });
2788
+ const runs = [];
2789
+ for (const col of body) {
2790
+ const cell = cellOf.get(col) || null;
2791
+ const last = runs[runs.length - 1];
2792
+ if (last && last.cell === cell) last.span++;
2793
+ else runs.push({ cell, span: 1, rowSpec });
2794
+ }
2795
+ return runs;
2796
+ });
2797
+ }
2798
+ /** STORY-0091. A body row: its pinned cells marked and put at the measured
2799
+ * offsets (a row the virtual window draws later lines up at once), and
2800
+ * the empty axis's cell. */
2801
+ fillRow(tr) {
2802
+ if (this.extraColumns()) {
2803
+ const td = document.createElement("td");
2804
+ td.className = "twm-dt-crosstab__none";
2805
+ tr.appendChild(td);
380
2806
  }
381
- if (Array.isArray(v)) v = v[0];
382
- return isAll(v) ? null : keyOf(v);
383
- };
384
- const chosen = () => multiple ? value.length === 0 : value === null;
385
- for (const opt of options) {
386
- const key = keyOf(opt.value);
387
- valueOf.set(key, opt.value);
388
- const btn = document.createElement("button");
389
- btn.type = "button";
390
- btn.className = "twm-dt-filter-chip";
391
- btn.dataset.value = key;
392
- if (opt.title) btn.title = String(opt.title);
393
- if (opt.icon) {
394
- const icon = document.createElement("span");
395
- icon.className = "material-symbols-outlined twm-dt-filter-chip__icon";
396
- icon.setAttribute("aria-hidden", "true");
397
- icon.textContent = String(opt.icon);
398
- btn.appendChild(icon);
2807
+ this._pinRowCells(tr);
2808
+ }
2809
+ _pinRowCells(tr) {
2810
+ const p = this.pinnedCount();
2811
+ for (let j = 0; j < p && j < tr.children.length; j++) {
2812
+ const cell = tr.children[j];
2813
+ cell.classList.add("twm-dt-pin");
2814
+ if (j === p - 1) cell.classList.add("twm-dt-pin--last");
2815
+ cell.__pinCol = j;
2816
+ if (this._lefts[j] != null) cell.style.left = `${this._lefts[j]}px`;
399
2817
  }
400
- const label = document.createElement("span");
401
- label.className = "twm-dt-filter-chip__label";
402
- label.textContent = String(opt.label ?? key);
403
- btn.appendChild(label);
404
- let count = null;
405
- if (showCounts) {
406
- count = document.createElement("span");
407
- count.className = "twm-dt-filter-chip__count";
408
- count.textContent = "\u2013";
409
- btn.appendChild(count);
2818
+ }
2819
+ /**
2820
+ * After `_createTable`: the header rows above the column headers, the
2821
+ * corner, the pinned header cells and the empty axis's header — all in the
2822
+ * body table's own `<thead>`.
2823
+ */
2824
+ decorate(tableEl) {
2825
+ this._leafRow = null;
2826
+ const thead = tableEl?.querySelector("thead");
2827
+ const leaf = thead?.querySelector("tr");
2828
+ if (!leaf) return;
2829
+ tableEl.classList.add("twm-dt-crosstab");
2830
+ this._leafRow = leaf;
2831
+ leaf.classList.add("twm-dt-crosstab__leaf");
2832
+ const filters = [...thead.querySelectorAll("tr.twm-data-table__filter-row")];
2833
+ if (this.extraColumns()) {
2834
+ const th = document.createElement("th");
2835
+ th.className = "text twm-dt-crosstab__none";
2836
+ const label = this._spec().emptyLabel;
2837
+ th.textContent = label == null ? DEFAULT_EMPTY_LABEL : String(label);
2838
+ leaf.appendChild(th);
2839
+ for (const tr of filters) {
2840
+ const cell = document.createElement("th");
2841
+ cell.className = "data-table__filter-cell data-table__filter-cell--empty twm-dt-crosstab__none";
2842
+ tr.appendChild(cell);
2843
+ }
410
2844
  }
411
- btn.addEventListener("click", (ev) => {
412
- ev.stopPropagation();
413
- pick(key);
2845
+ for (const tr of [leaf, ...filters]) this._pinRowCells(tr);
2846
+ const levels = this.headerRuns();
2847
+ const p = this.pinnedCount();
2848
+ levels.forEach((runs, r) => {
2849
+ const tr = document.createElement("tr");
2850
+ tr.className = "twm-dt-crosstab__band";
2851
+ if (r === 0) {
2852
+ for (let j = 0; j < p; j++) {
2853
+ const th = document.createElement("th");
2854
+ th.className = "twm-dt-crosstab__corner twm-dt-pin";
2855
+ if (j === p - 1) th.classList.add("twm-dt-pin--last");
2856
+ th.rowSpan = levels.length;
2857
+ th.__pinCol = j;
2858
+ tr.appendChild(th);
2859
+ }
2860
+ }
2861
+ for (const run of runs) tr.appendChild(this._bandCell(run));
2862
+ thead.insertBefore(tr, leaf);
414
2863
  });
415
- chips.set(key, { btn, count });
416
- el.appendChild(btn);
417
2864
  }
418
- function outward() {
419
- if (multiple) return value.length ? value.map((k) => valueOf.get(k)) : null;
420
- return value === null ? null : valueOf.get(value);
2865
+ /** A header row's cell: STORY-0066's spanning cells, the consumer's
2866
+ * `className` and `title` on a labelled one. */
2867
+ _bandCell({ cell, span, rowSpec }) {
2868
+ if (!cell) return blankSpanCell(span);
2869
+ const own = rowSpec[cell.index];
2870
+ const th = labelSpanCell(span, cell.label, own?.title == null ? cell.label : String(own.title));
2871
+ if (own?.className) this.table._addClasses(th, own.className);
2872
+ return th;
421
2873
  }
422
- function pick(key) {
423
- if (isAll(key)) value = multiple ? [] : null;
424
- else if (multiple) {
425
- value = value.includes(key) ? value.filter((k) => k !== key) : [...value, key];
426
- } else {
427
- value = value === key ? null : key;
2874
+ /**
2875
+ * Once the table is in the document: pin now (a layout already there is
2876
+ * used at once), on the next frame (when it has settled), and whenever a
2877
+ * header row, a pinned header cell or the table's box changes size. The
2878
+ * observer goes with the render (`_disposers`).
2879
+ *
2880
+ * The split layout's own resize observer and settle frame hang off its
2881
+ * header table, which a crosstab has none of; so the virtual window's
2882
+ * re-measure and resize (`_virtRemeasure`, `_virtUpdate`) run from here.
2883
+ */
2884
+ install() {
2885
+ const table = this.table;
2886
+ const tableEl = table._tableEl;
2887
+ if (!tableEl || !this._leafRow) return;
2888
+ this.pin();
2889
+ requestAnimationFrame(() => {
2890
+ if (table._tableEl !== tableEl) return;
2891
+ this.pin();
2892
+ table._virtRemeasure();
2893
+ });
2894
+ if (typeof ResizeObserver === "undefined") return;
2895
+ const observer = createRafResizeObserver(() => {
2896
+ if (table._tableEl !== tableEl) return;
2897
+ this.pin();
2898
+ table._virtUpdate();
2899
+ });
2900
+ if (table._wrapperEl) observer.observe(table._wrapperEl);
2901
+ const thead = tableEl.querySelector("thead");
2902
+ for (const tr of thead.querySelectorAll("tr")) observer.observe(tr);
2903
+ for (let j = 0; j < this.pinnedCount(); j++) {
2904
+ const cell = this._leafRow.children[j];
2905
+ if (cell) observer.observe(cell);
2906
+ }
2907
+ table._disposers.push(() => {
2908
+ try {
2909
+ observer.disconnect();
2910
+ } catch (_) {
2911
+ }
2912
+ });
2913
+ }
2914
+ /** Measure and write every offset: each header row's `top`, each pinned
2915
+ * column's `left`. */
2916
+ pin() {
2917
+ const tableEl = this.table._tableEl;
2918
+ const thead = tableEl?.querySelector("thead");
2919
+ if (!thead || !this._leafRow) return;
2920
+ const rows = [...thead.rows];
2921
+ const scale = unzoom(thead);
2922
+ const origin = rows[0].getBoundingClientRect().top;
2923
+ for (const tr of rows) {
2924
+ const top = Math.max(0, (tr.getBoundingClientRect().top - origin) * scale);
2925
+ const css = `${+top.toFixed(2)}px`;
2926
+ for (const th of tr.cells) th.style.top = css;
2927
+ }
2928
+ const p = this.pinnedCount();
2929
+ const lefts = [0];
2930
+ for (let j = 1; j < p; j++) {
2931
+ const cell = this._leafRow.children[j - 1];
2932
+ const w = cell ? cell.getBoundingClientRect().width * scale : 0;
2933
+ lefts[j] = +(lefts[j - 1] + w).toFixed(2);
2934
+ }
2935
+ this._lefts = p ? lefts : [];
2936
+ for (const cell of tableEl.querySelectorAll(".twm-dt-pin")) {
2937
+ const left = this._lefts[cell.__pinCol];
2938
+ if (left != null) cell.style.left = `${left}px`;
2939
+ }
2940
+ this._lockColumns(scale);
2941
+ }
2942
+ /**
2943
+ * Under `virtualize` the rows drawn change with every scroll, and with
2944
+ * them the widths an automatic layout gives the columns. The first pass
2945
+ * that sees a layout writes the widths it measured on the column headers
2946
+ * into the `<colgroup>` and fixes the layout, so the columns hold still.
2947
+ * No layout (every width 0) is no measurement, and nothing is written.
2948
+ */
2949
+ _lockColumns(scale) {
2950
+ const cols = this.table._cols();
2951
+ const tableEl = this.table._tableEl;
2952
+ if (!cols || !tableEl || tableEl.__crosstabLocked) return;
2953
+ const cells = this._leafRow.children;
2954
+ if (cells.length !== cols.length) return;
2955
+ const widths = [...cells].map((c) => c.getBoundingClientRect().width * scale);
2956
+ if (!widths.every((w) => w > 0)) return;
2957
+ widths.forEach((w, i) => {
2958
+ cols[i].style.width = `${+w.toFixed(2)}px`;
2959
+ });
2960
+ tableEl.style.tableLayout = "fixed";
2961
+ tableEl.style.width = `${+widths.reduce((a, b) => a + b, 0).toFixed(2)}px`;
2962
+ tableEl.__crosstabLocked = true;
2963
+ }
2964
+ };
2965
+
2966
+ // src/ui/components/table_structure.js
2967
+ var STRUCTURE_OPTIONS = ["columnActions", "onColumnRename", "onColumnDelete", "onRowAdd", "onRowDelete"];
2968
+ var TableStructure = class {
2969
+ /** @param {import('./data_table.js').DataTable} table */
2970
+ constructor(table) {
2971
+ this.table = table;
2972
+ this._renamer = null;
2973
+ }
2974
+ _cfg() {
2975
+ return this.table.config;
2976
+ }
2977
+ _has(name) {
2978
+ return typeof this._cfg()[name] === "function";
2979
+ }
2980
+ _call(name, ...args) {
2981
+ return callOption(this._cfg(), name, ...args);
2982
+ }
2983
+ /** Whether the header's right-click has items of this module's. */
2984
+ hasHeaderItems() {
2985
+ return this._has("onColumnRename") || this._has("onColumnDelete") || this._has("onRowAdd");
2986
+ }
2987
+ /** Whether a row's right-click has items of this module's. */
2988
+ hasRowItems() {
2989
+ return this._has("onRowAdd") || this._has("onRowDelete");
2990
+ }
2991
+ /** The header menu's items for the data column `colIdx` (null off one). */
2992
+ headerItems(colIdx) {
2993
+ const items = [];
2994
+ if (colIdx !== null && this._has("onColumnRename")) {
2995
+ items.push({ label: "Rename column", icon: "edit", onChoose: () => this.rename(colIdx) });
2996
+ }
2997
+ if (colIdx !== null && this._has("onColumnDelete")) {
2998
+ items.push({
2999
+ label: "Delete column",
3000
+ icon: "delete",
3001
+ danger: true,
3002
+ onChoose: () => this._call("onColumnDelete", colIdx)
3003
+ });
428
3004
  }
429
- paint();
430
- const out = outward();
431
- if (typeof ctx.setValue === "function") ctx.setValue(out);
432
- try {
433
- spec.onChange?.(out);
434
- } catch (err) {
435
- console.error("[chips filter] onChange threw", err);
3005
+ if (this._has("onRowAdd")) {
3006
+ items.push({ label: "Add row", icon: "add", onChoose: () => this._call("onRowAdd", null) });
436
3007
  }
3008
+ return items;
437
3009
  }
438
- function paint() {
439
- for (const [key, { btn }] of chips) {
440
- const on = isAll(key) ? chosen() : multiple ? value.includes(key) : value === key;
441
- btn.classList.toggle("twm-dt-filter-chip--on", on);
442
- btn.setAttribute("aria-pressed", on ? "true" : "false");
3010
+ /** The body menu's items for the row `rowIdx` (its original index). */
3011
+ rowItems(rowIdx) {
3012
+ const items = [];
3013
+ if (this._has("onRowAdd")) {
3014
+ items.push({ label: "Insert row below", icon: "add", onChoose: () => this._call("onRowAdd", rowIdx) });
443
3015
  }
3016
+ if (this._has("onRowDelete")) {
3017
+ items.push({
3018
+ label: "Delete row",
3019
+ icon: "delete",
3020
+ danger: true,
3021
+ onChoose: () => this._call("onRowDelete", rowIdx)
3022
+ });
3023
+ }
3024
+ return items;
444
3025
  }
445
- function setValue(v) {
446
- value = normalise(v);
447
- paint();
3026
+ /**
3027
+ * After the header is built: each data column's label in a span the
3028
+ * renamer can take over, and the consumer's action buttons.
3029
+ */
3030
+ decorate(headerTable) {
3031
+ this._renamer?.destroy();
3032
+ this._renamer = null;
3033
+ const labelRow = headerTable?.querySelector("thead > tr");
3034
+ if (!labelRow) return;
3035
+ const headers = this._cfg().headers || [];
3036
+ const rename = this._has("onColumnRename");
3037
+ const actions = this._has("columnActions");
3038
+ if (!rename && !actions) return;
3039
+ for (let colIdx = 0; colIdx < headers.length; colIdx++) {
3040
+ const th = labelRow.children[this.table._domIdxOf(colIdx)];
3041
+ if (!th) continue;
3042
+ if (rename) this._wrapLabel(th, colIdx);
3043
+ if (actions) this._addActions(th, colIdx, headers[colIdx]);
3044
+ }
448
3045
  }
449
- function setCounts(counts) {
450
- for (const [key, { btn, count }] of chips) {
451
- const n = counts == null ? void 0 : counts[key];
452
- const known = typeof n === "number" && Number.isFinite(n);
453
- if (count) count.textContent = known ? n.toLocaleString() : "\u2013";
454
- btn.classList.toggle("twm-dt-filter-chip--empty", known && n === 0);
3046
+ _wrapLabel(th, colIdx) {
3047
+ const text = [...th.childNodes].find((n) => n.nodeType === 3);
3048
+ if (!text) return;
3049
+ const label = document.createElement("span");
3050
+ label.className = "twm-dt-col-label";
3051
+ th.insertBefore(label, text);
3052
+ label.appendChild(text);
3053
+ label.addEventListener("keydown", (ev) => ev.stopPropagation());
3054
+ if (!this._cfg().sortable) {
3055
+ label.addEventListener("dblclick", (ev) => {
3056
+ ev.preventDefault();
3057
+ ev.stopPropagation();
3058
+ this.rename(colIdx);
3059
+ });
455
3060
  }
456
3061
  }
457
- setValue(ctx.value);
458
- if (ctx.counts) setCounts(ctx.counts);
459
- return {
460
- el,
461
- setValue,
462
- setCounts,
463
- getValue: outward,
464
- destroy() {
3062
+ _addActions(th, colIdx, header) {
3063
+ const items = this._call("columnActions", colIdx, { colIdx, header, table: this.table });
3064
+ const list = Array.isArray(items) ? items.filter(Boolean) : [];
3065
+ if (!list.length) return;
3066
+ const box = document.createElement("span");
3067
+ box.className = "twm-dt-col-actions";
3068
+ for (const item of list) {
3069
+ box.appendChild(headerActionButton(
3070
+ "twm-dt-col-action",
3071
+ item,
3072
+ () => this._call("onColumnAction", item.action, colIdx)
3073
+ ));
465
3074
  }
466
- };
467
- }
3075
+ th.appendChild(box);
3076
+ }
3077
+ /**
3078
+ * Open the renamer on the data column's header. Enter (or the focus
3079
+ * leaving) with a different, non-empty name calls
3080
+ * `onColumnRename(colIdx, name)`; Escape, or the name left as it was,
3081
+ * restores the header and calls nothing. Returns whether it opened.
3082
+ */
3083
+ rename(colIdx) {
3084
+ if (!this._has("onColumnRename")) return false;
3085
+ const labelRow = this.table._headerTableEl?.querySelector("thead > tr");
3086
+ const dom = this.table._domIdxOf(colIdx);
3087
+ const label = dom < 0 ? null : labelRow?.children[dom]?.querySelector(".twm-dt-col-label");
3088
+ if (!label) return false;
3089
+ this._renamer?.cancel();
3090
+ const before = label.textContent || "";
3091
+ const done = () => {
3092
+ if (this._renamer === renamer) this._renamer = null;
3093
+ };
3094
+ const renamer = attachInlineRenamer({
3095
+ label,
3096
+ inputClass: "twm-dt-col-rename",
3097
+ onCommit: async (raw) => {
3098
+ const name = String(raw ?? "").trim();
3099
+ if (!name || name === before) return { success: false };
3100
+ const answer = await this._call("onColumnRename", colIdx, name);
3101
+ if (answer === false || answer && answer.ok === false) return { success: false };
3102
+ return { newValue: name };
3103
+ },
3104
+ onCancel: done,
3105
+ onFinish: done
3106
+ });
3107
+ this._renamer = renamer;
3108
+ renamer.start();
3109
+ return true;
3110
+ }
3111
+ dispose() {
3112
+ this._renamer?.destroy();
3113
+ this._renamer = null;
3114
+ }
3115
+ };
468
3116
 
469
3117
  // src/ui/components/data_table.js
470
3118
  var DEFAULT_PAGE_SIZE = 100;
@@ -764,12 +3412,67 @@ var DataTable = class {
764
3412
  hiddenColumns: null,
765
3413
  columnChooser: false,
766
3414
  onColumnsChange: null,
3415
+ // STORY-0086. Off — columns in their natural order, as in 0.16.
3416
+ reorderable: false,
3417
+ columnOrder: null,
767
3418
  // STORY-0016. Off — no select column, as in 0.11 — until set.
768
3419
  selectToggle: false,
769
3420
  selectMode: false,
770
3421
  onSelectModeChange: null,
3422
+ // STORY-0003. Off — rows selected as in 0.16, no editing — until
3423
+ // set. The typedef says what each does.
3424
+ selectionMode: "rows",
3425
+ editable: false,
3426
+ editors: null,
3427
+ onCellCommit: null,
3428
+ onCellSelectionChange: null,
3429
+ headerMenu: null,
3430
+ onHeaderMenuAction: null,
3431
+ // STORY-0088. Off — commits go to onCellCommit — until set.
3432
+ staged: false,
3433
+ historyLimit: 100,
3434
+ onCommit: null,
3435
+ onStagedChange: null,
3436
+ stagedRefusal: null,
3437
+ // STORY-0066. Off — one header row, no structural edits — until
3438
+ // set. The typedef says what each does.
3439
+ columnGroups: null,
3440
+ groupActions: null,
3441
+ onGroupAction: null,
3442
+ onColumnAdd: null,
3443
+ columnActions: null,
3444
+ onColumnAction: null,
3445
+ onColumnRename: null,
3446
+ onColumnDelete: null,
3447
+ onRowAdd: null,
3448
+ onRowDelete: null,
3449
+ // STORY-0089. Off — no fill handle — until set.
3450
+ fillHandle: false,
771
3451
  ...config
772
3452
  };
3453
+ this._cellGrid = null;
3454
+ this._columnGroups = null;
3455
+ this._tableStructure = null;
3456
+ this._crosstabLayout = null;
3457
+ if (this.config.selectionMode === "cells") {
3458
+ const clash = ["selectToggle", "selectMode", "keyboardNav"].filter((k) => !!this.config[k]);
3459
+ if (clash.length) {
3460
+ console.warn(`[DataTable] ${clash.join("/")} cannot be combined with selectionMode: 'cells'; ignored.`);
3461
+ for (const k of clash) this.config[k] = false;
3462
+ }
3463
+ this._cellGrid = new CellGrid(this);
3464
+ } else if (this.config.editable) {
3465
+ console.warn("[DataTable] `editable` needs selectionMode: 'cells'; ignored.");
3466
+ }
3467
+ if (this.config.staged && !this._cellGrid) {
3468
+ console.warn("[DataTable] `staged` needs `editable`; ignored.");
3469
+ }
3470
+ if (this.config.fillHandle && !this._cellGrid) {
3471
+ console.warn("[DataTable] `fillHandle` needs `staged`; ignored.");
3472
+ }
3473
+ if (Array.isArray(this.config.columnGroups) && this.config.columnGroups.length && this._crosstab()) {
3474
+ console.warn("[DataTable] `columnGroups` cannot be combined with `crosstab` (use crosstab.headerRows); ignored.");
3475
+ }
773
3476
  {
774
3477
  const sel = this.config.selectable;
775
3478
  const asked = !!this.config.selectToggle || !!this.config.selectMode;
@@ -790,6 +3493,13 @@ var DataTable = class {
790
3493
  this._pendingFilter = null;
791
3494
  this._hidden = /* @__PURE__ */ new Set();
792
3495
  this._setHidden(this.config.hiddenColumns);
3496
+ this._order = null;
3497
+ this._askedOrder = null;
3498
+ this._askedUnchecked = false;
3499
+ if (!this._setOrder(this.config.columnOrder, true)) {
3500
+ console.warn("[DataTable] columnOrder is not an order of the columns; ignored.");
3501
+ }
3502
+ this._reorder = this.config.reorderable ? new ColumnReorder(this) : null;
793
3503
  this._activeKey = this.config.activeRow == null ? null : String(this.config.activeRow);
794
3504
  this._error = null;
795
3505
  this._virt = null;
@@ -891,6 +3601,9 @@ var DataTable = class {
891
3601
  this._setHidden(blob.hidden);
892
3602
  applied = true;
893
3603
  }
3604
+ if (Array.isArray(blob.order) && this._setOrder(blob.order)) {
3605
+ applied = true;
3606
+ }
894
3607
  if (blob.colWidths && typeof blob.colWidths === "object") {
895
3608
  this._colWidths = {};
896
3609
  const shift = this._lead().select - (Number(blob.colWidthsLead) || 0);
@@ -911,7 +3624,7 @@ var DataTable = class {
911
3624
  /** Identity of the current column set — restored widths/sort are
912
3625
  * keyed by position, so a schema change invalidates them. */
913
3626
  _colSig() {
914
- return (this.config.headers || []).join("") + (this.config.showRowNumbers ? "|#" : "") + (this._hidden?.size ? `|-${this.getHiddenColumns().join(",")}` : "");
3627
+ return (this.config.headers || []).join("") + (this.config.showRowNumbers ? "|#" : "") + (this._hidden?.size ? `|-${this.getHiddenColumns().join(",")}` : "") + (this._columnOrder() ? `|>${this._columnOrder().join(",")}` : "") + (this._colGroups()?.sig() || "");
915
3628
  }
916
3629
  /** Snapshot the persistable slice of state to the project store.
917
3630
  * No-op unless `persistKey` is set. Debounced inside the store. */
@@ -930,7 +3643,9 @@ var DataTable = class {
930
3643
  ...this.config.persistPage ? { offset: this._state.offset } : {},
931
3644
  // STORY-0012. Only for a table that hides columns or lets them be
932
3645
  // hidden.
933
- ...this._columnsOn() ? { hidden: this.getHiddenColumns() } : {}
3646
+ ...this._columnsOn() ? { hidden: this.getHiddenColumns() } : {},
3647
+ // STORY-0086. Only for a table that orders its columns.
3648
+ ...this._orderOn() ? { order: this.getColumnOrder() } : {}
934
3649
  });
935
3650
  }
936
3651
  // ─────────────────────────────────────────────────────────────────
@@ -954,7 +3669,28 @@ var DataTable = class {
954
3669
  /** The cells a drawn row has: the shown columns, and the row number. */
955
3670
  _domColCount() {
956
3671
  const n = Math.max(0, (this.config.headers?.length || 0) - this._hidden.size);
957
- return n + this._lead().all;
3672
+ return n + this._lead().all + (this._colGroups()?.placeholderCount() || 0) + (this._crosstab()?.extraColumns() || 0);
3673
+ }
3674
+ /** STORY-0066. The column groups (column_groups.js) while `columnGroups`
3675
+ * holds any; null otherwise. */
3676
+ _colGroups() {
3677
+ const spec = this.config.columnGroups;
3678
+ if (!Array.isArray(spec) || !spec.length) return null;
3679
+ if (this._crosstab()) return null;
3680
+ return this._columnGroups ||= new ColumnGroups(this);
3681
+ }
3682
+ /** STORY-0091. The crosstab layout (crosstab.js) while `crosstab` is an
3683
+ * object; null otherwise. */
3684
+ _crosstab() {
3685
+ const spec = this.config.crosstab;
3686
+ if (!spec || typeof spec !== "object") return null;
3687
+ return this._crosstabLayout ||= new Crosstab(this);
3688
+ }
3689
+ /** STORY-0066. The structural edits (table_structure.js) while any of
3690
+ * their options is set; null otherwise. */
3691
+ _structure() {
3692
+ if (!STRUCTURE_OPTIONS.some((k) => typeof this.config[k] === "function")) return null;
3693
+ return this._tableStructure ||= new TableStructure(this);
958
3694
  }
959
3695
  /** STORY-0016. The drawn columns in front of the data: the select column
960
3696
  * (`select`, 0 or 1, always first) and the row number. */
@@ -962,24 +3698,30 @@ var DataTable = class {
962
3698
  const select = this._gutter ? 1 : 0;
963
3699
  return { select, all: select + (this.config.showRowNumbers ? 1 : 0) };
964
3700
  }
3701
+ /** The data columns drawn, in the order drawn (STORY-0086): the order
3702
+ * without the hidden ones. */
3703
+ _shownColumns() {
3704
+ return this._rowColumns(this.config.headers?.length || 0);
3705
+ }
3706
+ /** The data indices of a row of `count` cells to draw, in order: the
3707
+ * shown columns, and any cell past the headers after them. */
3708
+ _rowColumns(count) {
3709
+ return cellOrder(this._columnOrder(), count).filter((c) => !this._isHidden(c));
3710
+ }
965
3711
  /** A data column's drawn index (row number included), or -1 when hidden. */
966
3712
  _domIdxOf(colIdx) {
967
- if (this._isHidden(colIdx)) return -1;
968
- let dom = this._lead().all;
969
- for (const h of this._hidden) if (h < colIdx) dom--;
970
- return dom + colIdx;
3713
+ const slot = this._colGroups()?.slotOf(colIdx) ?? -1;
3714
+ if (slot >= 0) return this._lead().all + slot;
3715
+ const at = this._shownColumns().indexOf(colIdx);
3716
+ return at < 0 ? -1 : this._lead().all + at;
971
3717
  }
972
3718
  /** A drawn cell's data column, or null for the row number. */
973
3719
  _dataIdxOfDom(domIdx) {
974
- let i = domIdx - this._lead().all;
3720
+ const i = domIdx - this._lead().all;
975
3721
  if (i < 0) return null;
976
- const n = this.config.headers?.length || 0;
977
- for (let c = 0; c < n; c++) {
978
- if (this._isHidden(c)) continue;
979
- if (i === 0) return c;
980
- i--;
981
- }
982
- return null;
3722
+ const slot = this._colGroups()?.slotAt(i);
3723
+ if (slot !== void 0) return slot;
3724
+ return this._shownColumns()[i] ?? null;
983
3725
  }
984
3726
  /** The columns not drawn, by data index, ascending. */
985
3727
  getHiddenColumns() {
@@ -1010,12 +3752,23 @@ var DataTable = class {
1010
3752
  * that stay drawn to their new places, and tell whoever listens. */
1011
3753
  _applyHidden(next) {
1012
3754
  const before = this.getHiddenColumns();
3755
+ this._carryWidths(() => this._setHidden([...next]));
3756
+ const after = this.getHiddenColumns();
3757
+ if (before.join() === after.join()) return;
3758
+ this._savePersisted();
3759
+ if (this._wrapperEl) this.render();
3760
+ this._columnsChanged();
3761
+ }
3762
+ /** STORY-0012/0086. Run `change` (a new hidden set or order), carrying
3763
+ * the drawn widths of the columns drawn before and after to their new
3764
+ * places. */
3765
+ _carryWidths(change) {
1013
3766
  const n = this.config.headers?.length || 0;
1014
3767
  const old = this._colWidths;
1015
3768
  const kept = this._colWidthsSig === this._colSig();
1016
3769
  const domBefore = [];
1017
3770
  for (let c = 0; c < n; c++) domBefore[c] = this._domIdxOf(c);
1018
- this._setHidden([...next]);
3771
+ change();
1019
3772
  if (kept) {
1020
3773
  const widths = {};
1021
3774
  for (let i = 0; i < this._lead().all; i++) if (old[i] != null) widths[i] = old[i];
@@ -1027,22 +3780,101 @@ var DataTable = class {
1027
3780
  this._colWidths = widths;
1028
3781
  this._colWidthsSig = this._colSig();
1029
3782
  }
1030
- const after = this.getHiddenColumns();
1031
- if (before.join() === after.join()) return;
1032
- this._savePersisted();
1033
- if (this._wrapperEl) this.render();
3783
+ }
3784
+ /** `onColumnsChange(hidden, {hidden, order})`, after a change of either. */
3785
+ _columnsChanged() {
3786
+ const hidden = this.getHiddenColumns();
1034
3787
  try {
1035
- this.config.onColumnsChange?.(after);
3788
+ this.config.onColumnsChange?.(hidden, { hidden, order: this.getColumnOrder() });
1036
3789
  } catch (err) {
1037
3790
  console.error("[DataTable] onColumnsChange threw", err);
1038
3791
  }
1039
3792
  }
1040
- /** `columnChooser`: the header's right-click menu, one checkable item a
1041
- * column. */
1042
- _showColumnChooser(clientX, clientY) {
3793
+ // ─────────────────────────────────────────────────────────────────
3794
+ // STORY-0086 — the order the columns are drawn in (column_order.js)
3795
+ // ─────────────────────────────────────────────────────────────────
3796
+ /** Whether this table orders its columns at all (and so saves the order). */
3797
+ _orderOn() {
3798
+ return !!this.config.reorderable || Array.isArray(this.config.columnOrder) || !!this._orderTouched;
3799
+ }
3800
+ /** Take `list` as the order (`asked`: it is `columnOrder`, kept apart as
3801
+ * the fallback). Refused (false, nothing changed) when it is no
3802
+ * permutation of the columns; a table with no headers yet keeps it for
3803
+ * the headers `setData` brings, and `_checkAskedOrder` checks it then. */
3804
+ _setOrder(list, asked = false) {
3805
+ const n = this.config.headers?.length || 0;
3806
+ if (list != null && (!Array.isArray(list) || n && !isColumnOrder(list, n))) return false;
3807
+ this._order = list == null ? null : list.map(Number);
3808
+ if (asked) {
3809
+ this._askedOrder = this._order;
3810
+ this._askedUnchecked = !!this._order && !n;
3811
+ }
3812
+ return true;
3813
+ }
3814
+ /** The first headers have come: a `columnOrder` taken before them, unchecked,
3815
+ * that is no order of them is dropped with the same warning a table with
3816
+ * headers gives at once — once, as the check is made once. */
3817
+ _checkAskedOrder() {
3818
+ const n = this.config.headers?.length || 0;
3819
+ if (!this._askedUnchecked || !n) return;
3820
+ this._askedUnchecked = false;
3821
+ if (isColumnOrder(this._askedOrder, n)) return;
3822
+ console.warn("[DataTable] columnOrder is not an order of the columns; ignored.");
3823
+ if (this._order === this._askedOrder) this._order = null;
3824
+ this._askedOrder = null;
3825
+ }
3826
+ /** The order in force, or null for the natural one. One rule, whether the
3827
+ * headers were known when an order came or not: the first of the live
3828
+ * order (dragged, moved or restored) and `columnOrder` that is an order
3829
+ * of today's columns — so a saved order of the wrong length never
3830
+ * displaces `columnOrder`. */
3831
+ _columnOrder() {
3832
+ const n = this.config.headers?.length || 0;
3833
+ for (const o of [this._order, this._askedOrder]) {
3834
+ if (o && isColumnOrder(o, n)) return o;
3835
+ }
3836
+ return null;
3837
+ }
3838
+ /**
3839
+ * Every data column's index, hidden ones included, in the order drawn.
3840
+ * @returns {number[]}
3841
+ */
3842
+ getColumnOrder() {
3843
+ return (this._columnOrder() || naturalOrder(this.config.headers?.length || 0)).slice();
3844
+ }
3845
+ /**
3846
+ * Move the column at position `from` of `getColumnOrder()` to position
3847
+ * `to`, the others closing up behind it: from the natural order,
3848
+ * `moveColumn(3, 1)` draws 0, 3, 1, 2. The widths stay with their columns.
3849
+ * Saved under `persistKey`, and `onColumnsChange` is told.
3850
+ *
3851
+ * @param {number} from
3852
+ * @param {number} to
3853
+ * @returns {boolean} whether the order changed
3854
+ */
3855
+ moveColumn(from, to) {
3856
+ const order = this.getColumnOrder();
3857
+ const n = order.length;
3858
+ if (![from, to].every((i) => Number.isInteger(i) && i >= 0 && i < n) || from === to) return false;
3859
+ this._applyOrder(moveInOrder(order, from, to));
3860
+ return true;
3861
+ }
3862
+ /** Swap in a new order (a permutation, or null), widths carried, and
3863
+ * tell whoever listens. */
3864
+ _applyOrder(next) {
3865
+ const before = this.getColumnOrder().join();
3866
+ this._orderTouched = true;
3867
+ this._carryWidths(() => this._setOrder(next));
3868
+ if (this.getColumnOrder().join() === before) return;
3869
+ this._savePersisted();
3870
+ if (this._wrapperEl) this.render();
3871
+ this._columnsChanged();
3872
+ }
3873
+ /** `columnChooser`'s items: one checkable item a column. */
3874
+ _columnChooserItems() {
1043
3875
  const headers = this.config.headers || [];
1044
3876
  const lastShown = headers.length - this._hidden.size <= 1;
1045
- const items = headers.map((h, c) => {
3877
+ return headers.map((h, c) => {
1046
3878
  const shown = !this._isHidden(c);
1047
3879
  return {
1048
3880
  label: String(h ?? ""),
@@ -1053,7 +3885,70 @@ var DataTable = class {
1053
3885
  onChoose: () => this.setColumnVisible(c, !shown)
1054
3886
  };
1055
3887
  });
1056
- this._showContextMenu(clientX, clientY, { ctx: null, items, copy: false });
3888
+ }
3889
+ /**
3890
+ * STORY-0003. The header's right-click menu for a press on `target`: the
3891
+ * consumer's `headerMenu` items for the data column under it, then — with
3892
+ * `columnChooser` — the chooser's items behind a separator. Empty when
3893
+ * there is nothing to show.
3894
+ */
3895
+ _headerMenuItems(target) {
3896
+ const items = [];
3897
+ const hook = this.config.headerMenu;
3898
+ const colIdx = this._headerColumnAt(target);
3899
+ if (typeof hook === "function" && colIdx !== null) {
3900
+ let asked = null;
3901
+ try {
3902
+ asked = hook(colIdx, {
3903
+ colIdx,
3904
+ header: this.config.headers?.[colIdx],
3905
+ hiddenColumns: this.getHiddenColumns(),
3906
+ table: this
3907
+ });
3908
+ } catch (err) {
3909
+ console.error("[DataTable] headerMenu threw", err);
3910
+ }
3911
+ for (const item of Array.isArray(asked) ? asked.filter(Boolean) : []) {
3912
+ if (item.separator) {
3913
+ items.push({ separator: true });
3914
+ continue;
3915
+ }
3916
+ items.push({
3917
+ ...item,
3918
+ onChoose: () => {
3919
+ try {
3920
+ this.config.onHeaderMenuAction?.(item.action, colIdx);
3921
+ } catch (err) {
3922
+ console.error("[DataTable] onHeaderMenuAction threw", err);
3923
+ }
3924
+ }
3925
+ });
3926
+ }
3927
+ }
3928
+ if (this._reorder && colIdx !== null) {
3929
+ const moves = this._reorder.menuItems(colIdx);
3930
+ if (moves.length && items.length) items.push({ separator: true });
3931
+ items.push(...moves);
3932
+ }
3933
+ const structural = this._structure()?.headerItems(colIdx) || [];
3934
+ if (structural.length) {
3935
+ if (items.length) items.push({ separator: true });
3936
+ items.push(...structural);
3937
+ }
3938
+ if (this.config.columnChooser) {
3939
+ if (items.length) items.push({ separator: true });
3940
+ items.push(...this._columnChooserItems());
3941
+ }
3942
+ return items;
3943
+ }
3944
+ /** The data column of a header cell (the header row, or the filter row
3945
+ * under it), or null off one — the row number, the select column. */
3946
+ _headerColumnAt(target) {
3947
+ const cell = target?.closest?.("th, td");
3948
+ const tr = cell?.parentNode;
3949
+ if (!tr || tr.parentNode?.tagName !== "THEAD") return null;
3950
+ if (tr.children.length !== this._domColCount()) return null;
3951
+ return this._dataIdxOfDom(Array.prototype.indexOf.call(tr.children, cell));
1057
3952
  }
1058
3953
  /** STORY-0010 `persistPage`: the client page changed, so save it. */
1059
3954
  _savePage() {
@@ -1089,18 +3984,26 @@ var DataTable = class {
1089
3984
  this._setHidden(updates.hiddenColumns);
1090
3985
  if (this.getHiddenColumns().join() !== before) {
1091
3986
  this._savePersisted();
1092
- try {
1093
- this.config.onColumnsChange?.(this.getHiddenColumns());
1094
- } catch (err) {
1095
- console.error("[DataTable] onColumnsChange threw", err);
1096
- }
3987
+ this._columnsChanged();
1097
3988
  }
1098
3989
  } else if (updates.headers && this._hidden.size) {
1099
3990
  this._setHidden([...this._hidden]);
1100
3991
  }
3992
+ if (Object.prototype.hasOwnProperty.call(updates, "columnOrder")) {
3993
+ const before = this.getColumnOrder().join();
3994
+ if (!this._setOrder(updates.columnOrder, true)) {
3995
+ console.warn("[DataTable] columnOrder is not an order of the columns; ignored.");
3996
+ } else if (this.getColumnOrder().join() !== before) {
3997
+ this._savePersisted();
3998
+ this._columnsChanged();
3999
+ }
4000
+ } else if (updates.headers) {
4001
+ this._checkAskedOrder();
4002
+ }
1101
4003
  if (updates.rows) {
1102
4004
  this._state.selected.clear();
1103
4005
  this._state.anchorIndex = null;
4006
+ this._cellGrid?.newRows();
1104
4007
  if (this._state.offset >= updates.rows.length) {
1105
4008
  this._state.offset = 0;
1106
4009
  }
@@ -1167,6 +4070,10 @@ var DataTable = class {
1167
4070
  * @param {number[]} indices - Row indices to select
1168
4071
  */
1169
4072
  setSelection(indices) {
4073
+ if (this._cellGrid) {
4074
+ this._cellGrid.selectRows(indices);
4075
+ return;
4076
+ }
1170
4077
  this._state.selected.clear();
1171
4078
  for (const idx of indices) {
1172
4079
  if (idx >= 0 && idx < this.config.rows.length) {
@@ -1194,9 +4101,9 @@ var DataTable = class {
1194
4101
  *
1195
4102
  * @param {*} key
1196
4103
  */
1197
- setActiveRow(key) {
1198
- this._activeKey = key == null ? null : String(key);
1199
- this.config.activeRow = key ?? null;
4104
+ setActiveRow(key3) {
4105
+ this._activeKey = key3 == null ? null : String(key3);
4106
+ this.config.activeRow = key3 ?? null;
1200
4107
  const tbody = this._tbodyEl;
1201
4108
  if (!tbody) return;
1202
4109
  for (const tr of tbody.children) {
@@ -1247,16 +4154,112 @@ var DataTable = class {
1247
4154
  * Clear selection
1248
4155
  */
1249
4156
  clearSelection() {
4157
+ if (this._cellGrid) {
4158
+ this._cellGrid.clear();
4159
+ return;
4160
+ }
1250
4161
  this._state.selected.clear();
1251
4162
  this._state.anchorIndex = null;
1252
4163
  this._updateRowSelection();
1253
4164
  this._notifySelectionChange();
1254
4165
  }
4166
+ // ─────────────────────────────────────────────────────────────────
4167
+ // STORY-0003 — cell mode (`selectionMode: 'cells'`), see cell_grid.js
4168
+ // ─────────────────────────────────────────────────────────────────
4169
+ /**
4170
+ * The cell selection: `{anchor, focus, range}`. `anchor` and `focus` (the
4171
+ * cursor) are `{row, col, rowIdx}` — `row` the position in the order
4172
+ * shown, `col` the data column, `rowIdx` the original row index — and
4173
+ * `range` is `{top, bottom, left, right, rows, columns}`: positions,
4174
+ * data columns at its edges, and the original row indices and data
4175
+ * columns it covers. All null with nothing selected. Null outside cell
4176
+ * mode.
4177
+ */
4178
+ getCellSelection() {
4179
+ return this._cellGrid ? this._cellGrid.get() : null;
4180
+ }
4181
+ /**
4182
+ * Put the cursor on a cell (`row` a position in the order shown, `col` a
4183
+ * data column) — with `anchor`, select the range from there. `null`
4184
+ * clears. The row is brought into view. A no-op outside cell mode.
4185
+ *
4186
+ * @param {{row:number, col:number}|null} focus
4187
+ * @param {{row:number, col:number}} [anchor]
4188
+ */
4189
+ setCellSelection(focus, anchor = null) {
4190
+ this._cellGrid?.set(focus, anchor);
4191
+ }
4192
+ /** Open the editor on a cell (position, data column), as F2 would.
4193
+ * Returns whether one opened. */
4194
+ editCell(row, col) {
4195
+ return !!this._cellGrid?.open(row, col);
4196
+ }
4197
+ /** Whether a cell editor is open. */
4198
+ isEditing() {
4199
+ return !!this._cellGrid?.isEditing();
4200
+ }
4201
+ // ── STORY-0088 `staged` ──────────────────────────────────────────────
4202
+ /**
4203
+ * Send every staged cell to `onCommit({edits})`, in one call. Resolves
4204
+ * `{ok: true, committed}` (the staged cells and the undo ring are
4205
+ * cleared), or `{ok: false, errors}` (the cells refused are marked and
4206
+ * every staged value stays). An open editor's value is staged first.
4207
+ * Without `staged`, resolves `{ok: false, errors: []}`.
4208
+ *
4209
+ * @returns {Promise<{ok: boolean, committed?: number, errors?: Array<{rowIdx:number, colIdx:number, error:string}>}>}
4210
+ */
4211
+ commit() {
4212
+ const st = this._cellGrid?.staged;
4213
+ return st ? st.commit() : Promise.resolve({ ok: false, errors: [] });
4214
+ }
4215
+ /** Drop every staged cell, its marks and the undo ring. Returns whether
4216
+ * there was anything to drop. */
4217
+ discard() {
4218
+ return !!this._cellGrid?.staged?.discard();
4219
+ }
4220
+ /** Take the last staged edit back (Ctrl+Z). Returns whether it moved. */
4221
+ undo() {
4222
+ return !!this._cellGrid?.staged?.undo();
4223
+ }
4224
+ /** Put the last edit taken back back again (Ctrl+Y). */
4225
+ redo() {
4226
+ return !!this._cellGrid?.staged?.redo();
4227
+ }
4228
+ /** Every staged cell, `{rowIdx, colIdx, value, previous}`, by row then
4229
+ * column. Empty without `staged`. */
4230
+ getStagedEdits() {
4231
+ return this._cellGrid?.staged ? this._cellGrid.staged.edits() : [];
4232
+ }
4233
+ /**
4234
+ * STORY-0089. Fill from the cell range to the cell `to` (`{row, col}`:
4235
+ * position in the order shown, data column), as dragging the fill handle
4236
+ * there does: staged as ONE undo entry, Ctrl's repeat as `forceCopy`.
4237
+ * Returns `{direction, count, staged, skipped}` —
4238
+ * `staged` the NUMBER of cells that changed, `skipped` the cells `editable` refused
4239
+ * (`{row, col, rowIdx}`) — or null when nothing was filled (no
4240
+ * `fillHandle`, no range, `to` inside it, an editor open, a commit out).
4241
+ */
4242
+ fill(to, { forceCopy = false } = {}) {
4243
+ return this._cellGrid?.fill ? this._cellGrid.fill.fill(to, { forceCopy }) : null;
4244
+ }
4245
+ /** Why sort, filters and pages are held — a staged edit — or null; with
4246
+ * a reason, `what` is refused out loud. */
4247
+ _stagedRefusal(what) {
4248
+ const why = this._cellGrid?.staged?.freezeReason() || null;
4249
+ if (why) console.warn(`[DataTable] ${what} refused: ${why}`);
4250
+ return why;
4251
+ }
4252
+ /** STORY-0066. Open the inline renamer on a data column's header (needs
4253
+ * `onColumnRename`). Returns whether it opened. */
4254
+ renameColumn(colIdx) {
4255
+ return !!this._structure()?.rename(colIdx);
4256
+ }
1255
4257
  /**
1256
4258
  * Go to specific page
1257
4259
  * @param {number} page - Page number (0-indexed)
1258
4260
  */
1259
4261
  goToPage(page) {
4262
+ if (this._stagedRefusal("goToPage")) return;
1260
4263
  if (this.config.dataSource) {
1261
4264
  const size = this._sourcePageSize();
1262
4265
  let p = Math.max(0, Math.floor(Number(page)) || 0);
@@ -1275,6 +4278,7 @@ var DataTable = class {
1275
4278
  * @param {number} rowIndex - Row index
1276
4279
  */
1277
4280
  goToRow(rowIndex) {
4281
+ if (this._stagedRefusal("goToRow")) return;
1278
4282
  if (this.config.dataSource) {
1279
4283
  const size = this._sourcePageSize();
1280
4284
  return this.goToPage(Math.floor(Math.max(0, Math.floor(Number(rowIndex)) || 0) / size));
@@ -1291,6 +4295,7 @@ var DataTable = class {
1291
4295
  * @param {boolean} [ascending] - Sort direction (toggles if same column)
1292
4296
  */
1293
4297
  sortBy(colIndex, ascending) {
4298
+ if (this._stagedRefusal("sortBy")) return;
1294
4299
  if (this._state.sortColumn === colIndex && ascending === void 0) {
1295
4300
  this._state.sortAscending = !this._state.sortAscending;
1296
4301
  } else {
@@ -1317,6 +4322,7 @@ var DataTable = class {
1317
4322
  * Clear all column filters
1318
4323
  */
1319
4324
  clearFilters() {
4325
+ if (this._stagedRefusal("clearFilters")) return;
1320
4326
  this._state.filters.clear();
1321
4327
  this._processedRows = null;
1322
4328
  this._processedIndexMap = null;
@@ -1384,6 +4390,10 @@ var DataTable = class {
1384
4390
  * Render the table
1385
4391
  */
1386
4392
  render() {
4393
+ if (this._cellGrid?.isEditing()) {
4394
+ this._cellGrid.renderWaiting = true;
4395
+ return;
4396
+ }
1387
4397
  const activeFilterColIdx = this._getActiveFilterColIdx();
1388
4398
  const doc = this.container.ownerDocument;
1389
4399
  const hadTableFocus = !!this._tableEl && doc?.activeElement === this._tableEl;
@@ -1436,7 +4446,7 @@ var DataTable = class {
1436
4446
  if (this._error && rows.length > 0) {
1437
4447
  this._wrapperEl.appendChild(this._createErrorBanner());
1438
4448
  }
1439
- const keepFilterRow = rows.length === 0 && !!this.config.dataSource && this.config.filterable;
4449
+ const keepFilterRow = rows.length === 0 && (!!this.config.dataSource && this.config.filterable || !!this._crosstab());
1440
4450
  if (rows.length === 0 && !keepFilterRow) {
1441
4451
  const { headers, showRowNumbers } = this.config;
1442
4452
  const headerWrap = document.createElement("div");
@@ -1453,13 +4463,12 @@ var DataTable = class {
1453
4463
  th.textContent = "#";
1454
4464
  tr.appendChild(th);
1455
4465
  }
1456
- headers.forEach((h, colIdx) => {
1457
- if (this._isHidden(colIdx)) return;
4466
+ for (const colIdx of this._shownColumns()) {
1458
4467
  const th = document.createElement("th");
1459
4468
  th.className = this._columnTypes[colIdx] || "text";
1460
- th.textContent = h;
4469
+ th.textContent = headers[colIdx];
1461
4470
  tr.appendChild(th);
1462
- });
4471
+ }
1463
4472
  thead.appendChild(tr);
1464
4473
  headerTable.appendChild(thead);
1465
4474
  headerWrap.appendChild(headerTable);
@@ -1478,7 +4487,13 @@ var DataTable = class {
1478
4487
  } else {
1479
4488
  const fullTable = this._createTable();
1480
4489
  this._tableEl = fullTable;
1481
- const thead = fullTable.querySelector("thead");
4490
+ const crosstab = this._crosstab();
4491
+ const thead = crosstab ? null : fullTable.querySelector("thead");
4492
+ if (crosstab) {
4493
+ crosstab.decorate(fullTable);
4494
+ this._headerTableEl = null;
4495
+ this._headerWrapEl = null;
4496
+ }
1482
4497
  if (thead) {
1483
4498
  const headerWrap = document.createElement("div");
1484
4499
  headerWrap.className = "twm-preview-table-header-wrap";
@@ -1500,16 +4515,27 @@ var DataTable = class {
1500
4515
  this._wrapperEl.appendChild(tableWrap);
1501
4516
  }
1502
4517
  this._tableWrapEl = tableWrap;
1503
- if (this.config.columnChooser && this._headerTableEl) {
4518
+ this._colGroups()?.decorate(this._headerWrapEl, this._headerTableEl);
4519
+ this._structure()?.decorate(this._headerTableEl);
4520
+ if ((this.config.columnChooser || typeof this.config.headerMenu === "function" || this._reorder || this._structure()?.hasHeaderItems()) && this._headerTableEl) {
1504
4521
  const head = this._headerTableEl;
1505
4522
  const onHeadMenu = (ev) => {
4523
+ const items = this._headerMenuItems(ev.target);
4524
+ if (!items.length) return;
1506
4525
  ev.preventDefault();
1507
4526
  ev.stopPropagation();
1508
- setTimeout(() => this._showColumnChooser(ev.clientX, ev.clientY), 0);
4527
+ setTimeout(() => this._showContextMenu(
4528
+ ev.clientX,
4529
+ ev.clientY,
4530
+ { ctx: null, items, copy: false }
4531
+ ), 0);
1509
4532
  };
1510
4533
  head.addEventListener("contextmenu", onHeadMenu);
1511
4534
  this._disposers.push(() => head.removeEventListener("contextmenu", onHeadMenu));
1512
4535
  }
4536
+ if (this._reorder && this._headerTableEl) {
4537
+ this._reorder.install(this._headerTableEl.querySelector("thead > tr"));
4538
+ }
1513
4539
  if (this._paginationEl && this.config.pagerPosition === "bottom") {
1514
4540
  this._paginationEl.classList.add("twm-pagination-controls--bottom");
1515
4541
  this._paginationEl.style.borderBottom = "0";
@@ -1522,6 +4548,7 @@ var DataTable = class {
1522
4548
  if (this._pagingEl) this._wrapperEl.appendChild(this._pagingEl);
1523
4549
  }
1524
4550
  this.container.appendChild(this._wrapperEl);
4551
+ this._crosstab()?.install();
1525
4552
  if (this.config.scrollLock && typeof this.config.scrollLock.attach === "function") {
1526
4553
  this._disposers.push(this.config.scrollLock.attach(tableWrap));
1527
4554
  }
@@ -1567,7 +4594,7 @@ var DataTable = class {
1567
4594
  this._installHeaderScrollSync();
1568
4595
  if (rows.length > 0) this._installColumnResizers();
1569
4596
  }
1570
- if (rows.length > 0 && (this.config.selectable || this.config.copyable || !this.config.readonly || typeof this.config.onRowActivate === "function" || typeof this.config.rowDetail === "function" || typeof this.config.onRowKeyDown === "function")) {
4597
+ if (rows.length > 0 && (this.config.selectable || this.config.copyable || !this.config.readonly || typeof this.config.onRowActivate === "function" || typeof this.config.rowDetail === "function" || typeof this.config.onRowKeyDown === "function" || this._cellGrid)) {
1571
4598
  this._installInteractions();
1572
4599
  }
1573
4600
  if (activeFilterColIdx !== null) {
@@ -1609,12 +4636,11 @@ var DataTable = class {
1609
4636
  return;
1610
4637
  }
1611
4638
  const { headers, rows } = this.config;
1612
- const shown = (_, colIdx) => !this._isHidden(colIdx);
1613
- const matrix = [headers.filter(shown)];
4639
+ const matrix = [this._rowColumns(headers.length).map((c) => headers[c])];
1614
4640
  for (const idx of indices) {
1615
4641
  const row = rows[idx];
1616
4642
  if (row) {
1617
- matrix.push(row.map((val, colIdx) => this._formatValue(val, colIdx)).filter(shown));
4643
+ matrix.push(this._rowColumns(row.length).map((c) => this._formatValue(row[c], c)));
1618
4644
  }
1619
4645
  }
1620
4646
  const separator = format === "csv" ? "," : " ";
@@ -1645,12 +4671,11 @@ var DataTable = class {
1645
4671
  this._notify("Download", "No data to download.", "warn");
1646
4672
  return;
1647
4673
  }
1648
- const shown = (_, colIdx) => !this._isHidden(colIdx);
1649
- const lines = [headers.filter(shown).join(",")];
4674
+ const lines = [this._rowColumns(headers.length).map((c) => headers[c]).join(",")];
1650
4675
  for (const row of rows) {
1651
- lines.push(row.map(
1652
- (val, colIdx) => this._csvEscape(this._formatValue(val, colIdx))
1653
- ).filter(shown).join(","));
4676
+ lines.push(this._rowColumns(row.length).map(
4677
+ (c) => this._csvEscape(this._formatValue(row[c], c))
4678
+ ).join(","));
1654
4679
  }
1655
4680
  const csv = lines.join("\n");
1656
4681
  const dialogs = this.config.host?.dialogs;
@@ -1698,6 +4723,8 @@ var DataTable = class {
1698
4723
  this._teardown(false);
1699
4724
  }
1700
4725
  _teardown(emptyHost) {
4726
+ this._cellGrid?.dispose();
4727
+ this._tableStructure?.dispose();
1701
4728
  autoDisposeUnwatch(this);
1702
4729
  this._autoSeen = false;
1703
4730
  if (this._autoTimer) {
@@ -1965,8 +4992,8 @@ var DataTable = class {
1965
4992
  th.className = "data-table__filter-cell data-table__filter-cell--empty";
1966
4993
  tr.appendChild(th);
1967
4994
  }
1968
- headers.forEach((header, colIdx) => {
1969
- if (this._isHidden(colIdx)) return;
4995
+ this._shownColumns().forEach((colIdx) => {
4996
+ const header = headers[colIdx];
1970
4997
  const th = document.createElement("th");
1971
4998
  th.className = "data-table__filter-cell";
1972
4999
  const control = this._filterControlFor(colIdx, header);
@@ -2255,6 +5282,7 @@ var DataTable = class {
2255
5282
  }, 200);
2256
5283
  }
2257
5284
  _onFilterInput(colIdx, value) {
5285
+ if (this._stagedRefusal("A filter")) return;
2258
5286
  const empty = typeof value === "string" ? !value : this._isEmptyFilterValue(value);
2259
5287
  if (!empty) {
2260
5288
  this._state.filters.set(colIdx, value);
@@ -2420,6 +5448,7 @@ var DataTable = class {
2420
5448
  const spacer = document.createElement("div");
2421
5449
  spacer.style.flex = "1";
2422
5450
  const handlePageChange = (newOffset) => {
5451
+ if (this._stagedRefusal("Turning the page")) return;
2423
5452
  if (isServerSide) {
2424
5453
  this._awaitWithSpinner(onPageChange(newOffset, pageSize));
2425
5454
  } else {
@@ -2479,12 +5508,14 @@ var DataTable = class {
2479
5508
  _createExportBtn() {
2480
5509
  const name = this.config.exportFilename;
2481
5510
  const filename = (typeof name === "function" ? name(this) : name) || "data.csv";
2482
- return this._createPaginationBtn(
5511
+ const btn = this._createPaginationBtn(
2483
5512
  "download",
2484
5513
  "Download visible rows as CSV",
2485
5514
  false,
2486
5515
  () => this.downloadCSV(filename)
2487
5516
  );
5517
+ btn.classList.add("twm-pagination-btn--export");
5518
+ return btn;
2488
5519
  }
2489
5520
  /**
2490
5521
  * The strip on a table with one page: nothing, as always, unless the export
@@ -2606,6 +5637,7 @@ var DataTable = class {
2606
5637
  this._wrapperEl.appendChild(next);
2607
5638
  }
2608
5639
  this._pagingEl = next;
5640
+ this._cellGrid?.staged?.paintFreeze();
2609
5641
  }
2610
5642
  /**
2611
5643
  * Call one of the consumer's paging callbacks. A Promise holds every other
@@ -2615,6 +5647,7 @@ var DataTable = class {
2615
5647
  */
2616
5648
  _pagingCall(kind, fn, ...args) {
2617
5649
  if (typeof fn !== "function" || this._pagingBusy) return;
5650
+ if (this._stagedRefusal(`paging ${kind}`)) return;
2618
5651
  let result;
2619
5652
  try {
2620
5653
  result = fn(...args);
@@ -2645,6 +5678,7 @@ var DataTable = class {
2645
5678
  _pagingMaybeLoadMore() {
2646
5679
  const p = this._pagingConfig();
2647
5680
  if (!p || !p.hasMore || this._pagingBusy) return;
5681
+ if (this._cellGrid?.staged?.freezeReason()) return;
2648
5682
  if (this.config.dataSource && this._source?.inFlight) return;
2649
5683
  const state = this.pagingState;
2650
5684
  if (state.mode !== "scroll" || p.held >= state.hardLimit) return;
@@ -2791,6 +5825,10 @@ var DataTable = class {
2791
5825
  const processedRows = isServerSide ? rows : this._getProcessedRows();
2792
5826
  const offset = isServerSide ? configOffset || 0 : this._state.offset;
2793
5827
  const pageRows = isServerSide || !this.config.pagination || this._pagingConfig() ? processedRows : processedRows.slice(offset, offset + pageSize);
5828
+ this._pageSpan = {
5829
+ start: pageRows === processedRows ? 0 : offset,
5830
+ count: pageRows.length
5831
+ };
2794
5832
  const table = document.createElement("table");
2795
5833
  table.className = this._tableClassName();
2796
5834
  table.tabIndex = 0;
@@ -2803,8 +5841,8 @@ var DataTable = class {
2803
5841
  th.textContent = "#";
2804
5842
  headerRow.appendChild(th);
2805
5843
  }
2806
- headers.forEach((header, colIdx) => {
2807
- if (this._isHidden(colIdx)) return;
5844
+ this._shownColumns().forEach((colIdx) => {
5845
+ const header = headers[colIdx];
2808
5846
  const th = document.createElement("th");
2809
5847
  th.className = this._columnTypes[colIdx] || "text";
2810
5848
  if (sortable) {
@@ -2884,7 +5922,8 @@ var DataTable = class {
2884
5922
  tr.__rowIndex = globalIdx;
2885
5923
  tr.__position = (this._positionBase || 0) + localIdx;
2886
5924
  tr.className = "data-preview-row";
2887
- if (this._state.selected.has(globalIdx)) {
5925
+ if (this._cellGrid) tr.__cellPos = (this._pageSpan?.start || 0) + localIdx;
5926
+ if (this._state.selected.has(globalIdx) && !this._cellGrid) {
2888
5927
  tr.classList.add("selected");
2889
5928
  }
2890
5929
  this._decorateRow(tr, row, globalIdx);
@@ -3176,6 +6215,7 @@ var DataTable = class {
3176
6215
  const wanted = new Set(want);
3177
6216
  for (const [i, tr] of v.map) {
3178
6217
  if (!wanted.has(i)) {
6218
+ this._cellGrid?.rowLeaving(tr);
3179
6219
  tr.remove();
3180
6220
  this._removeDetailPieces(tr);
3181
6221
  v.map.delete(i);
@@ -3386,10 +6426,10 @@ var DataTable = class {
3386
6426
  const diff = this._diffOf(row, globalIdx);
3387
6427
  const gap = diff?.status === "gap";
3388
6428
  const changedCells = diff?.cells ? diff.cells instanceof Set ? diff.cells : new Set(diff.cells) : null;
3389
- for (let colIdx = 0; colIdx < row.length; colIdx++) {
3390
- if (this._isHidden(colIdx)) continue;
6429
+ for (const colIdx of this._rowColumns(row.length)) {
3391
6430
  const td = document.createElement("td");
3392
6431
  td.className = this._columnTypes[colIdx] || "text";
6432
+ if (this._cellGrid) td.__colIdx = colIdx;
3393
6433
  const value = row[colIdx];
3394
6434
  let handled = false;
3395
6435
  if (gap) {
@@ -3430,6 +6470,9 @@ var DataTable = class {
3430
6470
  }
3431
6471
  tr.appendChild(td);
3432
6472
  }
6473
+ this._colGroups()?.fillRow(tr);
6474
+ this._crosstab()?.fillRow(tr);
6475
+ this._cellGrid?.decorateRow(tr);
3433
6476
  }
3434
6477
  /** Re-render ONE row in place, preserving everything around it.
3435
6478
  *
@@ -3451,6 +6494,9 @@ var DataTable = class {
3451
6494
  * - keyboard focus, because the focused element's position is recorded
3452
6495
  * before the replace and restored after.
3453
6496
  *
6497
+ * STORY-0003: on the row an open cell editor is in, the data is stored
6498
+ * now and the row is drawn when the editor closes (true is returned).
6499
+ *
3454
6500
  * @param {number} index row index as tracked by `tr.__rowIndex`
3455
6501
  * @param {any[]} row the new cell values
3456
6502
  * @returns {boolean} false when the row is not currently rendered
@@ -3476,6 +6522,7 @@ var DataTable = class {
3476
6522
  this.config.rows[index] = row;
3477
6523
  }
3478
6524
  if (this._virt) this._virtSetRow(index, row);
6525
+ if (this._cellGrid?.holdRow(tr, index)) return true;
3479
6526
  const active = document.activeElement;
3480
6527
  let focusedCol = -1;
3481
6528
  if (active && tr.contains(active)) {
@@ -3498,7 +6545,7 @@ var DataTable = class {
3498
6545
  cell.style.maxWidth = saved.maxWidth;
3499
6546
  });
3500
6547
  }
3501
- tr.classList.toggle("selected", this._state?.selected?.has(index) === true);
6548
+ tr.classList.toggle("selected", this._state?.selected?.has(index) === true && !this._cellGrid);
3502
6549
  this._decorateRow(tr, row, index);
3503
6550
  if (this._detailOn()) this._redrawDetail(tr);
3504
6551
  if (focusedCol >= 0 && tr.children[focusedCol]) {
@@ -3544,8 +6591,8 @@ var DataTable = class {
3544
6591
  td.insertBefore(gap, td.firstChild);
3545
6592
  return;
3546
6593
  }
3547
- const key = this._rowKey(row, rowIdx);
3548
- const open = key !== null && this._expanded.has(key);
6594
+ const key3 = this._rowKey(row, rowIdx);
6595
+ const open = key3 !== null && this._expanded.has(key3);
3549
6596
  const btn = document.createElement("button");
3550
6597
  btn.type = "button";
3551
6598
  btn.className = "material-symbols-outlined twm-dt-detail-toggle";
@@ -3567,14 +6614,14 @@ var DataTable = class {
3567
6614
  const row = tr.__row;
3568
6615
  const idx = tr.__rowIndex;
3569
6616
  if (!this._rowHasDetail(row, idx)) return;
3570
- const key = this._rowKey(row, idx);
3571
- if (key === null || !this._expanded.has(key)) return;
3572
- const pieces = this._buildDetailPieces(tr, key);
6617
+ const key3 = this._rowKey(row, idx);
6618
+ if (key3 === null || !this._expanded.has(key3)) return;
6619
+ const pieces = this._buildDetailPieces(tr, key3);
3573
6620
  const parent = tr.parentNode;
3574
6621
  if (!parent) return;
3575
6622
  for (const p of pieces) parent.insertBefore(p, ref);
3576
6623
  }
3577
- _buildDetailPieces(tr, key) {
6624
+ _buildDetailPieces(tr, key3) {
3578
6625
  const n = this._domColCount();
3579
6626
  const detailTr = document.createElement("tr");
3580
6627
  detailTr.className = "twm-dt-detail-row";
@@ -3593,16 +6640,16 @@ var DataTable = class {
3593
6640
  filler.setAttribute("aria-hidden", "true");
3594
6641
  detailTr.__detailOf = tr;
3595
6642
  tr.__detail = [detailTr, filler];
3596
- this._drawDetail(tr, host, key);
6643
+ this._drawDetail(tr, host, key3);
3597
6644
  const btn = tr.querySelector(":scope > td > .twm-dt-detail-toggle");
3598
6645
  if (btn) btn.setAttribute("aria-controls", host.id);
3599
6646
  return tr.__detail;
3600
6647
  }
3601
- _drawDetail(tr, host, key) {
6648
+ _drawDetail(tr, host, key3) {
3602
6649
  host.replaceChildren();
3603
6650
  let out;
3604
6651
  try {
3605
- out = this.config.rowDetail(tr.__row, tr.__rowIndex, { host, key, table: this });
6652
+ out = this.config.rowDetail(tr.__row, tr.__rowIndex, { host, key: key3, table: this });
3606
6653
  } catch (err) {
3607
6654
  console.error("[DataTable] rowDetail threw", err);
3608
6655
  out = err?.message || String(err);
@@ -3621,8 +6668,8 @@ var DataTable = class {
3621
6668
  }
3622
6669
  /** `updateRow`'s half: the detail follows the row's new data. */
3623
6670
  _redrawDetail(tr) {
3624
- const key = this._rowKey(tr.__row, tr.__rowIndex);
3625
- const want = this._rowHasDetail(tr.__row, tr.__rowIndex) && key !== null && this._expanded.has(key);
6671
+ const key3 = this._rowKey(tr.__row, tr.__rowIndex);
6672
+ const want = this._rowHasDetail(tr.__row, tr.__rowIndex) && key3 !== null && this._expanded.has(key3);
3626
6673
  const pieces = tr.__detail;
3627
6674
  if (!want) {
3628
6675
  if (pieces) this._removeDetailPieces(tr);
@@ -3631,7 +6678,7 @@ var DataTable = class {
3631
6678
  if (pieces) {
3632
6679
  const host = pieces[0].querySelector(".twm-dt-detail");
3633
6680
  if (host) {
3634
- this._drawDetail(tr, host, key);
6681
+ this._drawDetail(tr, host, key3);
3635
6682
  const btn = tr.querySelector(":scope > td > .twm-dt-detail-toggle");
3636
6683
  if (btn) btn.setAttribute("aria-controls", host.id);
3637
6684
  }
@@ -3644,27 +6691,27 @@ var DataTable = class {
3644
6691
  _setRowElExpanded(tr, open, ev = null) {
3645
6692
  if (!this._detailOn() || tr.__rowIndex === void 0) return false;
3646
6693
  if (!this._rowHasDetail(tr.__row, tr.__rowIndex)) return false;
3647
- const key = this._rowKey(tr.__row, tr.__rowIndex);
3648
- if (key === null || this._expanded.has(key) === !!open) return false;
3649
- if (open) this._expanded.add(key);
3650
- else this._expanded.delete(key);
3651
- this._syncDetailOnScreen(tr, key);
6694
+ const key3 = this._rowKey(tr.__row, tr.__rowIndex);
6695
+ if (key3 === null || this._expanded.has(key3) === !!open) return false;
6696
+ if (open) this._expanded.add(key3);
6697
+ else this._expanded.delete(key3);
6698
+ this._syncDetailOnScreen(tr, key3);
3652
6699
  this._afterDetailChange();
3653
6700
  try {
3654
- this.config.onRowDetailToggle?.(tr.__rowIndex, tr.__row, !!open, key, ev);
6701
+ this.config.onRowDetailToggle?.(tr.__rowIndex, tr.__row, !!open, key3, ev);
3655
6702
  } catch (err) {
3656
6703
  console.error("[DataTable] onRowDetailToggle threw", err);
3657
6704
  }
3658
6705
  return true;
3659
6706
  }
3660
6707
  /** Make the row on screen agree with `_expanded` for its key. */
3661
- _syncDetailOnScreen(tr, key) {
3662
- const open = this._expanded.has(key);
6708
+ _syncDetailOnScreen(tr, key3) {
6709
+ const open = this._expanded.has(key3);
3663
6710
  const btn = tr.querySelector(":scope > td > .twm-dt-detail-toggle");
3664
6711
  if (btn) btn.setAttribute("aria-expanded", open ? "true" : "false");
3665
6712
  if (open && !tr.__detail && tr.parentNode) {
3666
6713
  const ref = tr.nextSibling;
3667
- for (const p of this._buildDetailPieces(tr, key)) tr.parentNode.insertBefore(p, ref);
6714
+ for (const p of this._buildDetailPieces(tr, key3)) tr.parentNode.insertBefore(p, ref);
3668
6715
  } else if (!open && tr.__detail) {
3669
6716
  this._removeDetailPieces(tr);
3670
6717
  }
@@ -3678,9 +6725,9 @@ var DataTable = class {
3678
6725
  * measured height open, three rows until it has been measured. */
3679
6726
  _detailHeightOf(row, rowIdx, base) {
3680
6727
  if (!this._rowHasDetail(row, rowIdx)) return 0;
3681
- const key = this._rowKey(row, rowIdx);
3682
- if (key === null || !this._expanded.has(key)) return 0;
3683
- const h = this._detailHeights.get(key);
6728
+ const key3 = this._rowKey(row, rowIdx);
6729
+ if (key3 === null || !this._expanded.has(key3)) return 0;
6730
+ const h = this._detailHeights.get(key3);
3684
6731
  return h > 0 ? h : base * 3;
3685
6732
  }
3686
6733
  /** Measure the open details among `trs` (client pixels, the zoom divided
@@ -3703,9 +6750,9 @@ var DataTable = class {
3703
6750
  if (!d || !d.isConnected) continue;
3704
6751
  const h = d.getBoundingClientRect().height / zoom;
3705
6752
  if (!(h > 0)) continue;
3706
- const key = this._rowKey(tr.__row, tr.__rowIndex);
3707
- if (Math.abs((this._detailHeights.get(key) || 0) - h) > 0.5) {
3708
- this._detailHeights.set(key, h);
6753
+ const key3 = this._rowKey(tr.__row, tr.__rowIndex);
6754
+ if (Math.abs((this._detailHeights.get(key3) || 0) - h) > 0.5) {
6755
+ this._detailHeights.set(key3, h);
3709
6756
  changed = true;
3710
6757
  }
3711
6758
  }
@@ -3720,9 +6767,9 @@ var DataTable = class {
3720
6767
  * @param {boolean} [open=true]
3721
6768
  * @returns {boolean} whether anything changed
3722
6769
  */
3723
- setRowExpanded(key, open = true) {
3724
- if (key == null) return false;
3725
- const k = String(key);
6770
+ setRowExpanded(key3, open = true) {
6771
+ if (key3 == null) return false;
6772
+ const k = String(key3);
3726
6773
  if (this._expanded.has(k) === !!open) return false;
3727
6774
  if (open) this._expanded.add(k);
3728
6775
  else this._expanded.delete(k);
@@ -3743,8 +6790,8 @@ var DataTable = class {
3743
6790
  return true;
3744
6791
  }
3745
6792
  /** Whether the row whose key is `key` has its detail open. */
3746
- isRowExpanded(key) {
3747
- return key != null && this._expanded.has(String(key));
6793
+ isRowExpanded(key3) {
6794
+ return key3 != null && this._expanded.has(String(key3));
3748
6795
  }
3749
6796
  /** The keys of the rows whose detail is open, as text. */
3750
6797
  getExpandedRows() {
@@ -3755,12 +6802,12 @@ var DataTable = class {
3755
6802
  for (const k of [...this._expanded]) this.setRowExpanded(k, false);
3756
6803
  }
3757
6804
  /** The `<tr>` on screen for a key, or null. */
3758
- _rowElByKey(key) {
6805
+ _rowElByKey(key3) {
3759
6806
  const tbody = this._tbodyEl;
3760
6807
  if (!tbody) return null;
3761
6808
  for (const tr of tbody.children) {
3762
6809
  if (tr.__rowIndex === void 0) continue;
3763
- if (this._rowKey(tr.__row, tr.__rowIndex) === key) return tr;
6810
+ if (this._rowKey(tr.__row, tr.__rowIndex) === key3) return tr;
3764
6811
  }
3765
6812
  return null;
3766
6813
  }
@@ -4027,6 +7074,7 @@ var DataTable = class {
4027
7074
  }
4028
7075
  /** Go to the page at `offset` (paging back stops following). */
4029
7076
  _sourceGoTo(offset) {
7077
+ if (this._stagedRefusal("Turning the page")) return void 0;
4030
7078
  const next = Math.max(0, offset | 0);
4031
7079
  if (this._following && next > 0) this.setFollow(false);
4032
7080
  return this._sourceLoad({ offset: next, reason: "page" });
@@ -4300,6 +7348,7 @@ var DataTable = class {
4300
7348
  if (!known && !hasPrev && !hasNext) return this._createExportOnlyStrip();
4301
7349
  const maxJump = Number.isFinite(c.maxJumpOffset) ? c.maxJumpOffset : null;
4302
7350
  const go = (newOffset) => {
7351
+ if (this._stagedRefusal("Turning the page")) return;
4303
7352
  if (source) {
4304
7353
  this._sourceGoTo(newOffset);
4305
7354
  return;
@@ -4504,6 +7553,7 @@ var DataTable = class {
4504
7553
  * value is whatever the column's control works in — text for the 0.4 box.
4505
7554
  */
4506
7555
  setFilter(colIdx, value) {
7556
+ if (this._stagedRefusal("setFilter")) return;
4507
7557
  const textBox = !this._filterKindFor(colIdx, (this.config.headers || [])[colIdx]);
4508
7558
  const v = textBox && value != null && typeof value !== "string" ? String(value) : value;
4509
7559
  if (textBox) {
@@ -4639,6 +7689,7 @@ var DataTable = class {
4639
7689
  if (this._isClickable()) names.push("twm-dt--clickable");
4640
7690
  if (this._gutter) names.push("twm-dt--select");
4641
7691
  if (this._selecting) names.push("twm-dt--selecting");
7692
+ if (this._cellGrid) names.push("twm-dt--cells");
4642
7693
  return names.join(" ");
4643
7694
  }
4644
7695
  /** `clickable` when it is said; otherwise exactly when rows open. */
@@ -4685,18 +7736,18 @@ var DataTable = class {
4685
7736
  /** A row's key, as text, or null: `getRowKey`, else its original index. */
4686
7737
  _rowKey(row, rowIdx) {
4687
7738
  const fn = this.config.getRowKey;
4688
- let key;
7739
+ let key3;
4689
7740
  if (typeof fn === "function") {
4690
7741
  try {
4691
- key = fn(row, rowIdx);
7742
+ key3 = fn(row, rowIdx);
4692
7743
  } catch (err) {
4693
7744
  console.error("[DataTable] getRowKey threw", err);
4694
- key = null;
7745
+ key3 = null;
4695
7746
  }
4696
7747
  } else {
4697
- key = rowIdx;
7748
+ key3 = rowIdx;
4698
7749
  }
4699
- return key == null ? null : String(key);
7750
+ return key3 == null ? null : String(key3);
4700
7751
  }
4701
7752
  /** The gestures `activateOn` names, as a set. */
4702
7753
  _activateOn() {
@@ -5392,6 +8443,7 @@ var DataTable = class {
5392
8443
  headerWrap.scrollLeft = x;
5393
8444
  const residual = x - headerWrap.scrollLeft;
5394
8445
  headerTable.style.transform = residual ? `translateX(${-residual}px)` : "";
8446
+ this._colGroups()?.align();
5395
8447
  }
5396
8448
  /** Drag-to-resize: hang a thin grab handle off the right edge of
5397
8449
  * every header cell. Dragging it writes a per-column width override
@@ -5561,10 +8613,11 @@ var DataTable = class {
5561
8613
  _installInteractions() {
5562
8614
  if (!this._tbodyEl || !this._tableEl) return;
5563
8615
  const { selectable, copyable } = this.config;
5564
- const customMenu = this.config.contextMenuItems != null;
8616
+ const customMenu = this.config.contextMenuItems != null || !!this._structure()?.hasRowItems();
5565
8617
  const tbody = this._tbodyEl;
5566
8618
  const table = this._tableEl;
5567
- if (selectable) {
8619
+ if (this._cellGrid) this._cellGrid.install(tbody, table);
8620
+ if (selectable && !this._cellGrid) {
5568
8621
  const handleRowClick = (event) => {
5569
8622
  const rowEl = event.target.closest("tr");
5570
8623
  if (!rowEl || rowEl.__rowIndex === void 0) return;
@@ -5759,13 +8812,13 @@ var DataTable = class {
5759
8812
  const handleKeyDown = (event) => {
5760
8813
  const ctrlLike = event.ctrlKey || event.metaKey;
5761
8814
  if (ctrlLike && !event.altKey) {
5762
- const key = String(event.key || "").toLowerCase();
5763
- if (key === "a" && selectable === "single") {
8815
+ const key3 = String(event.key || "").toLowerCase();
8816
+ if (key3 === "a" && selectable === "single") {
5764
8817
  event.preventDefault();
5765
- } else if (key === "a" && selectable) {
8818
+ } else if (key3 === "a" && selectable) {
5766
8819
  event.preventDefault();
5767
8820
  this._selectAllShown();
5768
- } else if (key === "c") {
8821
+ } else if (key3 === "c") {
5769
8822
  event.preventDefault();
5770
8823
  this.copyToClipboard(event.shiftKey ? "csv" : "tsv");
5771
8824
  }
@@ -5788,7 +8841,7 @@ var DataTable = class {
5788
8841
  const idx = tr.__rowIndex;
5789
8842
  if (idx !== void 0) {
5790
8843
  const on = this._state.selected.has(idx);
5791
- tr.classList.toggle("selected", on);
8844
+ tr.classList.toggle("selected", on && !this._cellGrid);
5792
8845
  const box = this._gutter ? tr.querySelector(":scope > td.twm-dt-select-cell > input") : null;
5793
8846
  if (box) box.checked = on;
5794
8847
  }
@@ -6045,7 +9098,10 @@ var DataTable = class {
6045
9098
  items = null;
6046
9099
  }
6047
9100
  }
6048
- return Array.isArray(items) ? items.filter(Boolean) : [];
9101
+ const list = Array.isArray(items) ? items.filter(Boolean) : [];
9102
+ const structural = this._structure()?.rowItems(ctx.rowIdx) || [];
9103
+ if (structural.length && list.length) list.push({ separator: true });
9104
+ return list.concat(structural);
6049
9105
  }
6050
9106
  /**
6051
9107
  * STORY-0010. Fill the menu for this right-click: the consumer's items,
@@ -6164,6 +9220,9 @@ var DataTable = class {
6164
9220
  };
6165
9221
 
6166
9222
  export {
9223
+ createTextEditor,
9224
+ CellEditorHost,
9225
+ CellSelection,
6167
9226
  createRafResizeObserver,
6168
9227
  ISO_DATE,
6169
9228
  ISO_DATETIME,
@@ -6175,9 +9234,15 @@ export {
6175
9234
  matchDateFilter,
6176
9235
  matchDurationFilter,
6177
9236
  createChipsFilter,
9237
+ ACTIONS,
9238
+ DEFAULT_HISTORY_LIMIT,
9239
+ StagedHistory,
9240
+ detectSeries,
9241
+ projectSeries,
9242
+ attachInlineRenamer,
6178
9243
  PAGING_HARD_LIMIT,
6179
9244
  PAGING_SOFT_LIMIT,
6180
9245
  resolvePaging,
6181
9246
  DataTable
6182
9247
  };
6183
- //# sourceMappingURL=chunk-FBGP5KN5.js.map
9248
+ //# sourceMappingURL=chunk-Q36EPT2R.js.map