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
@@ -0,0 +1,648 @@
1
+ /**
2
+ * cell_grid.js — STORY-0003. DataTable's half of the CELL MODE
3
+ * (`selectionMode: 'cells'`). INTERNAL to the table: imported by
4
+ * `data_table.js` only, and not part of the widgets barrel — it reads the
5
+ * table's private fields.
6
+ *
7
+ * It reads the table's rows and columns, paints the cells, listens to the
8
+ * pointer and the keys, moves the selection model (`cell_selection.js`
9
+ * `CellSelection`) and drives the editor (`cell_editor_host.js`).
10
+ *
11
+ * A table without `selectionMode: 'cells'` never builds it.
12
+ */
13
+
14
+ import { CellSelection } from './cell_selection.js';
15
+ import { CellEditorHost, createTextEditor } from './cell_editor_host.js';
16
+ import { StagedEdits } from './staged_edits.js';
17
+ import { FillHandle } from './fill_handle.js';
18
+
19
+ /** Is this a cell somebody filled? What Ctrl+arrow stops at. */
20
+ export function isFilledValue(value) {
21
+ if (value == null) return false;
22
+ if (typeof value === 'string') return value !== '';
23
+ if (typeof value === 'number') return !Number.isNaN(value);
24
+ return true;
25
+ }
26
+
27
+ /** The keys a printable character is: one character, no Ctrl, Cmd or Alt. */
28
+ function isPrintable(ev) {
29
+ return typeof ev.key === 'string' && [...ev.key].length === 1
30
+ && !ev.ctrlKey && !ev.metaKey && !ev.altKey;
31
+ }
32
+
33
+ /** The arrows, as steps. */
34
+ const ARROWS = {
35
+ ArrowUp: [-1, 0], ArrowDown: [1, 0], ArrowLeft: [0, -1], ArrowRight: [0, 1],
36
+ };
37
+
38
+ /**
39
+ * DataTable's cell mode. Built by the table when `selectionMode` is 'cells';
40
+ * it reads the table, never the other way round, except through the few hooks
41
+ * the table calls (`decorateRow`, `install`, `rowLeaving`, `isEditing`).
42
+ */
43
+ export class CellGrid {
44
+ /** @param {import('./data_table.js').DataTable} table */
45
+ constructor(table) {
46
+ this.table = table;
47
+ this.sel = new CellSelection();
48
+ /** `rowIdx:colIdx` → value typed, while its commit is out. */
49
+ this.pending = new Map();
50
+ /** `rowIdx:colIdx` → {value, message}: a commit that was refused. */
51
+ this.errors = new Map();
52
+ /** A `render()` asked for while an editor was open. */
53
+ this.renderWaiting = false;
54
+ /** `rowIdx`s whose redraw waits for the editor open in the row to
55
+ * close: a settled commit's, or the consumer's `updateRow`. The row
56
+ * is read when it is drawn, so the newest data is what shows. */
57
+ this.rowsWaiting = new Set();
58
+ /** `rowIdx:colIdx` → the token of the cell's newest commit, so an
59
+ * older commit settling late does not take the newer one's marks. */
60
+ this.latest = new Map();
61
+ /** Bumped by every new set of rows, so a commit that outlived its rows
62
+ * marks nothing in the new ones. */
63
+ this.generation = 0;
64
+ this.editor = new CellEditorHost({
65
+ onCommit: (s, value, move, reason) => this._committed(s, value, move, reason),
66
+ onCancel: (s, reason) => this._closed(s, reason),
67
+ });
68
+ /** STORY-0088 `staged`: commits are staged here instead of sent. */
69
+ this.staged = null;
70
+ if (this._cfg().staged) {
71
+ if (this._cfg().editable) this.staged = new StagedEdits(this);
72
+ else console.warn('[DataTable] `staged` needs `editable`; ignored.');
73
+ }
74
+ /** STORY-0089 `fillHandle`: the handle on the range's corner. */
75
+ this.fill = null;
76
+ if (this._cfg().fillHandle) {
77
+ if (this.staged) this.fill = new FillHandle(this);
78
+ else console.warn('[DataTable] `fillHandle` needs `staged`; ignored.');
79
+ }
80
+ }
81
+
82
+ // ── what the table is, read through its own helpers ──────────────────
83
+
84
+ _cfg() { return this.table.config; }
85
+
86
+ /** The data columns shown, in the order shown. */
87
+ columns() {
88
+ return this.table._shownColumns();
89
+ }
90
+
91
+ /** The positions of the rows on the page drawn, `[first, last]`. */
92
+ span() {
93
+ const s = this.table._pageSpan || { start: 0, count: 0 };
94
+ return [s.start, s.start + s.count - 1];
95
+ }
96
+
97
+ _sync() {
98
+ const [first, last] = this.span();
99
+ this.sel.setBounds(first, last, this.columns());
100
+ }
101
+
102
+ /** A position's original row index (the `__rowIndex` space). */
103
+ rowIndexAt(pos) {
104
+ const t = this.table;
105
+ if (t._isServerSide()) return (t.config.dataSource ? 0 : (t.config.offset || 0)) + pos;
106
+ t._getProcessedRows();
107
+ return t._processedIndexMap?.[pos] ?? pos;
108
+ }
109
+
110
+ rowAt(pos) {
111
+ return this.table._getProcessedRows()[pos] ?? null;
112
+ }
113
+
114
+ /**
115
+ * The row the table holds NOW for an original row index — the window's
116
+ * row list, else `config.rows` at the place the table draws it from —
117
+ * the one `updateRow` writes. Never a row captured earlier.
118
+ */
119
+ currentRow(rowIdx) {
120
+ const t = this.table;
121
+ const local = t._virtLocalOf(rowIdx);
122
+ if (local >= 0) return t._virt.rows[local] ?? null;
123
+ const rows = t.config.rows;
124
+ if (!Array.isArray(rows)) return null;
125
+ const at = t._isServerSide() ? rowIdx - (t.config.dataSource ? 0 : (t.config.offset || 0)) : rowIdx;
126
+ return rows[at] ?? null;
127
+ }
128
+
129
+ /** What a cell shows: a refused or pending commit's value, else the row's. */
130
+ shownValue(rowIdx, col) {
131
+ const key = `${rowIdx}:${col}`;
132
+ const err = this.errors.get(key);
133
+ if (err) return err.value;
134
+ if (this.pending.has(key)) return this.pending.get(key);
135
+ if (this.staged?.has(key)) return this.staged.value(key);
136
+ const row = this.currentRow(rowIdx);
137
+ return row ? row[col] : undefined;
138
+ }
139
+
140
+ /** Whether the open editor sits in this row element. */
141
+ _holds(tr) {
142
+ const s = this.editor.session;
143
+ return !!s && !!tr && tr.contains(s.td);
144
+ }
145
+
146
+ /**
147
+ * `updateRow` on a drawn row: the table has stored the data already. When
148
+ * the row holds the open editor the rebuild — which would take the editor
149
+ * out of the document — waits for that editor to close, the editor's cell
150
+ * left as it is. Returns whether it waits.
151
+ */
152
+ holdRow(tr, rowIdx) {
153
+ if (!this._holds(tr)) return false;
154
+ this.rowsWaiting.add(rowIdx);
155
+ return true;
156
+ }
157
+
158
+ valueAt(pos, col) {
159
+ const row = this.rowAt(pos);
160
+ return row ? row[col] : undefined;
161
+ }
162
+
163
+ /** The `<td>` drawing a cell, or null when it is not drawn. */
164
+ cellEl(pos, col) {
165
+ const tr = this.table.getRowElement(this.rowIndexAt(pos));
166
+ if (!tr) return null;
167
+ for (const td of tr.children) if (td.__colIdx === col) return td;
168
+ return null;
169
+ }
170
+
171
+ // ── the public face (DataTable delegates) ────────────────────────────
172
+
173
+ /** `{anchor, focus, range}`; each cell is `{row, col, rowIdx}`, and the
174
+ * range also lists its `rows` (original indices) and `columns`. */
175
+ get() {
176
+ this._sync();
177
+ const cell = (c) => (c ? { row: c.row, col: c.col, rowIdx: this.rowIndexAt(c.row) } : null);
178
+ const r = this.sel.range();
179
+ let range = null;
180
+ if (r) {
181
+ const rows = [];
182
+ for (let p = r.top; p <= r.bottom; p++) rows.push(this.rowIndexAt(p));
183
+ range = { ...r, rows };
184
+ }
185
+ return { anchor: cell(this.sel.anchor), focus: cell(this.sel.focus), range };
186
+ }
187
+
188
+ /** Rows (original indices) the range touches, ascending. */
189
+ rowIndices() {
190
+ const r = this.sel.range();
191
+ if (!r) return [];
192
+ const out = [];
193
+ for (let p = r.top; p <= r.bottom; p++) out.push(this.rowIndexAt(p));
194
+ return out.sort((a, b) => a - b);
195
+ }
196
+
197
+ /** Select from `anchor` to `focus` (`{row, col}`, positions and data
198
+ * indices), or put the cursor on `focus` alone. */
199
+ set(focus, anchor = null) {
200
+ this._sync();
201
+ if (!focus) { this.sel.clear(); this._changed(); return; }
202
+ this.sel.setCursor(anchor ? anchor.row : focus.row, anchor ? anchor.col : focus.col);
203
+ if (anchor) this.sel.extendTo(focus.row, focus.col);
204
+ this._changed({ reveal: true });
205
+ }
206
+
207
+ /** `setSelection(indices)` in cell mode: whole rows, the first to the last
208
+ * of them as shown. */
209
+ selectRows(indices) {
210
+ this._sync();
211
+ const t = this.table;
212
+ t._getProcessedRows();
213
+ const map = t._processedIndexMap || [];
214
+ const positions = indices.map((i) => (t._isServerSide()
215
+ ? i - (t.config.dataSource ? 0 : (t.config.offset || 0)) : map.indexOf(i)))
216
+ .filter((p) => p >= 0);
217
+ const cols = this.sel.cols;
218
+ if (!positions.length || !cols.length) { this.sel.clear(); this._changed(); return; }
219
+ this.sel.setCursor(Math.min(...positions), cols[0]);
220
+ this.sel.extendTo(Math.max(...positions), cols[cols.length - 1]);
221
+ this._changed();
222
+ }
223
+
224
+ clear() {
225
+ this.sel.clear();
226
+ this._changed();
227
+ }
228
+
229
+ /** New rows (`setData({rows})`): the marks were about the old ones. */
230
+ newRows() {
231
+ this.generation++;
232
+ this.pending.clear();
233
+ this.errors.clear();
234
+ this.latest.clear();
235
+ this.rowsWaiting.clear();
236
+ this.staged?.clear();
237
+ this.fill?.clearMarks();
238
+ }
239
+
240
+ isEditing() {
241
+ return !!this.editor.session;
242
+ }
243
+
244
+ // ── painting ─────────────────────────────────────────────────────────
245
+
246
+ /** Called for every row drawn — on render, on a scroll, on `updateRow` —
247
+ * so a row that arrives later is painted like the rest. */
248
+ decorateRow(tr) {
249
+ const pos = tr.__cellPos;
250
+ if (pos === undefined) return;
251
+ for (const td of tr.children) {
252
+ if (td.__colIdx !== undefined) this._decorateCell(td, pos, tr.__rowIndex, td.__colIdx);
253
+ }
254
+ }
255
+
256
+ /** One cell: the selection, its commit's pending or refused mark, and
257
+ * (STORY-0088) its staged value and a refused batch's mark. */
258
+ _decorateCell(td, pos, rowIdx, col) {
259
+ this._paintCell(td, pos, col);
260
+ const key = `${rowIdx}:${col}`;
261
+ const err = this.errors.get(key);
262
+ const waiting = this.pending.has(key);
263
+ const staged = !!this.staged?.has(key);
264
+ if (err || waiting) {
265
+ td.textContent = this.table._formatValue(err ? err.value : this.pending.get(key), col);
266
+ } else if (staged) {
267
+ td.textContent = this.table._formatValue(this.staged.value(key), col);
268
+ }
269
+ const message = err ? err.message : this.staged?.errors.get(key);
270
+ td.classList.toggle('twm-dt-cell--pending', waiting);
271
+ td.classList.toggle('twm-dt-cell--staged', staged);
272
+ td.classList.toggle('twm-dt-cell--error', message != null);
273
+ if (message != null) {
274
+ td.title = message;
275
+ td.setAttribute('data-error', message);
276
+ } else td.removeAttribute('data-error');
277
+ this.fill?.paintCell(td, pos, col); // STORY-0089, after the text
278
+ }
279
+
280
+ _paintCell(td, pos, col) {
281
+ td.classList.toggle('twm-dt-cell--selected', this.sel.contains(pos, col));
282
+ const cursor = this.sel.isCursor(pos, col);
283
+ td.classList.toggle('twm-dt-cell--cursor', cursor);
284
+ if (cursor) td.setAttribute('aria-selected', 'true');
285
+ else td.removeAttribute('aria-selected');
286
+ this.fill?.paintCell(td, pos, col); // STORY-0089
287
+ }
288
+
289
+ paint() {
290
+ const tbody = this.table._tbodyEl;
291
+ if (!tbody) return;
292
+ for (const tr of tbody.children) {
293
+ const pos = tr.__cellPos;
294
+ if (pos === undefined) continue;
295
+ for (const td of tr.children) {
296
+ if (td.__colIdx !== undefined) this._paintCell(td, pos, td.__colIdx);
297
+ }
298
+ }
299
+ }
300
+
301
+ /** The selection moved: the cells, the rows `getSelection()` answers, and
302
+ * whoever listens. */
303
+ _changed({ reveal = false } = {}) {
304
+ const t = this.table;
305
+ this.fill?.clearMarks(); // STORY-0089: a skipped mark lasts until the selection moves
306
+ if (reveal && this.sel.focus) t.scrollToRow(this.rowIndexAt(this.sel.focus.row));
307
+ this.paint();
308
+ const before = t.getSelection().join();
309
+ const rows = this.rowIndices();
310
+ t._state.selected.clear();
311
+ for (const i of rows) t._state.selected.add(i);
312
+ t._state.anchorIndex = this.sel.anchor ? this.rowIndexAt(this.sel.anchor.row) : null;
313
+ if (rows.join() !== before) t._notifySelectionChange();
314
+ try { t.config.onCellSelectionChange?.(this.get()); } catch (err) {
315
+ console.error('[DataTable] onCellSelectionChange threw', err);
316
+ }
317
+ }
318
+
319
+ // ── the pointer and the keys ─────────────────────────────────────────
320
+
321
+ /** Bound by `_installInteractions` on every render, BEFORE the table's own
322
+ * activation, detail and copy listeners, so a key the cell mode takes
323
+ * goes no further. Listeners go on the table's disposers. */
324
+ install(tbody, table) {
325
+ const t = this.table;
326
+ const on = (el, type, fn) => {
327
+ el.addEventListener(type, fn);
328
+ t._disposers.push(() => el.removeEventListener(type, fn));
329
+ };
330
+ this._sync();
331
+ const cellOf = (target) => {
332
+ const td = target?.closest?.('td');
333
+ if (!td || td.__colIdx === undefined || !tbody.contains(td)) return null;
334
+ const tr = td.parentNode;
335
+ if (tr?.__cellPos === undefined) return null;
336
+ return { td, row: tr.__cellPos, col: td.__colIdx };
337
+ };
338
+ const inEditor = (target) => !!this.editor.session && this.editor.session.td.contains(target);
339
+
340
+ on(tbody, 'click', (ev) => {
341
+ if (ev.button != null && ev.button !== 0) return;
342
+ if (inEditor(ev.target)) return;
343
+ const hit = cellOf(ev.target);
344
+ if (!hit) return;
345
+ this._sync();
346
+ if (ev.shiftKey && this.sel.anchor) this.sel.extendTo(hit.row, hit.col);
347
+ else this.sel.setCursor(hit.row, hit.col);
348
+ this._changed();
349
+ try { table.focus({ preventScroll: true }); } catch (_) { /* detached */ }
350
+ });
351
+ on(tbody, 'dblclick', (ev) => {
352
+ if (inEditor(ev.target)) return;
353
+ const hit = cellOf(ev.target);
354
+ if (!hit || !this.canEdit(hit.row, hit.col)) return;
355
+ this._sync();
356
+ this.sel.setCursor(hit.row, hit.col);
357
+ this._changed();
358
+ if (this.open(hit.row, hit.col)) ev.stopImmediatePropagation();
359
+ });
360
+ // A right-click outside the range moves the cursor there first, so the
361
+ // table's own menu (bound after this) finds its row already selected.
362
+ on(tbody, 'contextmenu', (ev) => {
363
+ if (inEditor(ev.target)) return;
364
+ const hit = cellOf(ev.target);
365
+ if (!hit || this.sel.contains(hit.row, hit.col)) return;
366
+ this._sync();
367
+ this.sel.setCursor(hit.row, hit.col);
368
+ this._changed();
369
+ });
370
+ on(table, 'keydown', (ev) => {
371
+ if (ev.isComposing || inEditor(ev.target)) return;
372
+ if (ev.target !== table && ev.target?.closest?.('input, select, textarea, button, [contenteditable="true"]')) return;
373
+ if (this._onKey(ev)) {
374
+ ev.preventDefault();
375
+ ev.stopImmediatePropagation();
376
+ }
377
+ });
378
+ // An editor that was open across a render of its row (an `updateRow`)
379
+ // is still the editor; the paint follows the model.
380
+ this.paint();
381
+ // STORY-0088. The new controls are held while cells are staged.
382
+ this.staged?.paintFreeze();
383
+ }
384
+
385
+ /** One key on the focused table. Returns whether the cell mode took it. */
386
+ _onKey(ev) {
387
+ this._sync();
388
+ const ctrl = ev.ctrlKey || ev.metaKey;
389
+ const k = ev.key;
390
+ // STORY-0088. Ctrl+Z takes a staged edit back; Ctrl+Y (Ctrl+Shift+Z)
391
+ // puts it back. Taken even with nothing to undo: the browser's own
392
+ // undo has nothing to do on a table either.
393
+ if (this.staged && ctrl && !ev.altKey) {
394
+ const lower = String(k).toLowerCase();
395
+ if (lower === 'z' && !ev.shiftKey) { this.staged.undo(); return true; }
396
+ if (lower === 'y' || (lower === 'z' && ev.shiftKey)) { this.staged.redo(); return true; }
397
+ }
398
+ if (ARROWS[k] && !ev.altKey) {
399
+ const [dr, dc] = ARROWS[k];
400
+ if (ctrl) {
401
+ this.sel.jump(dr, dc, (r, c) => isFilledValue(this.valueAt(r, c)), { extend: ev.shiftKey });
402
+ } else this.sel.move(dr, dc, { extend: ev.shiftKey });
403
+ this._changed({ reveal: true });
404
+ return true;
405
+ }
406
+ if ((k === 'Home' || k === 'End') && !ev.altKey) {
407
+ this.sel.lineEnd(k === 'End', { extend: ev.shiftKey, corner: ctrl });
408
+ this._changed({ reveal: true });
409
+ return true;
410
+ }
411
+ if (k === 'Tab' && !ctrl && !ev.altKey) {
412
+ // On the last cell (the first, going back) Tab is not taken:
413
+ // focus moves on out of the table, as Tab says.
414
+ if (this.sel.tabEnds(ev.shiftKey)) return false;
415
+ this.sel.tab(ev.shiftKey);
416
+ this._changed({ reveal: true });
417
+ return true;
418
+ }
419
+ if (ctrl && !ev.altKey && String(k).toLowerCase() === 'a') {
420
+ this.sel.selectAll();
421
+ this._changed();
422
+ return true;
423
+ }
424
+ if (k === 'Escape') {
425
+ this.sel.collapse();
426
+ this._changed();
427
+ this.table._hideContextMenu();
428
+ return true;
429
+ }
430
+ const f = this.sel.focus;
431
+ if (!f) return false;
432
+ if ((k === 'F2' || (k === 'Enter' && !ctrl && !ev.altKey && !ev.shiftKey)) && this.canEdit(f.row, f.col)) {
433
+ return this.open(f.row, f.col);
434
+ }
435
+ if (isPrintable(ev) && this.canEdit(f.row, f.col)) {
436
+ return this.open(f.row, f.col, { initialText: ev.key });
437
+ }
438
+ return false;
439
+ }
440
+
441
+ // ── editing ──────────────────────────────────────────────────────────
442
+
443
+ /** Whether a cell opens an editor: `editable` says so for its column. */
444
+ canEdit(pos, col) {
445
+ const spec = this._cfg().editable;
446
+ if (!spec) return false;
447
+ // STORY-0088. While a staged batch is out, nothing new is typed into it.
448
+ if (this.staged?.committing) return false;
449
+ if (spec === true) return true;
450
+ if (Array.isArray(spec)) return spec.includes(col);
451
+ if (typeof spec === 'function') {
452
+ try { return !!spec(col, this.rowAt(pos), this.rowIndexAt(pos)); } catch (err) {
453
+ console.error('[DataTable] editable threw', err);
454
+ return false;
455
+ }
456
+ }
457
+ return false;
458
+ }
459
+
460
+ /** The column's type, as `editors` is keyed by: the declared one, else the
461
+ * detected one. */
462
+ _typeOf(col) {
463
+ const declared = this._cfg().columnTypes?.[col];
464
+ return declared || this.table._columnTypes?.[col] || 'text';
465
+ }
466
+
467
+ _factoryFor(col) {
468
+ const editors = this._cfg().editors;
469
+ if (editors && typeof editors === 'object') {
470
+ if (typeof editors[col] === 'function') return editors[col];
471
+ const type = this._typeOf(col);
472
+ if (typeof editors[type] === 'function') return editors[type];
473
+ }
474
+ return createTextEditor;
475
+ }
476
+
477
+ /**
478
+ * Open the editor on a cell (position, data column). Brings the row into
479
+ * view first. Returns whether an editor opened.
480
+ */
481
+ open(pos, col, { initialText = null } = {}) {
482
+ if (this.editor.session || !this.canEdit(pos, col)) return false;
483
+ this.table.scrollToRow(this.rowIndexAt(pos));
484
+ const td = this.cellEl(pos, col);
485
+ if (!td) return false;
486
+ const rowIdx = this.rowIndexAt(pos);
487
+ const row = this.currentRow(rowIdx);
488
+ const headers = this._cfg().headers || [];
489
+ // The editor holds what the cell SHOWS — a refused or pending value,
490
+ // not the row's — so it is what the person sees that they edit.
491
+ return this.editor.open({
492
+ td,
493
+ factory: this._factoryFor(col),
494
+ pos, colIdx: col, rowIdx, row,
495
+ generation: this.generation,
496
+ value: this.shownValue(rowIdx, col),
497
+ column: { index: col, header: headers[col], type: this._typeOf(col) },
498
+ initialText,
499
+ });
500
+ }
501
+
502
+ /** The virtual window is taking a row out: an editor in it commits, as a
503
+ * click elsewhere would — what was typed is not thrown away. An editor
504
+ * with no `getValue` has nothing to commit, and is cancelled. */
505
+ rowLeaving(tr) {
506
+ const s = this.editor.session;
507
+ if (s && tr.contains(s.td)) this.editor.commit(null, 'scroll');
508
+ }
509
+
510
+ dispose() {
511
+ this.fill?.dispose();
512
+ this.editor.cancel('dispose');
513
+ this.renderWaiting = false;
514
+ this.rowsWaiting.clear();
515
+ }
516
+
517
+ /** After every close: focus back on the table when the keyboard closed it,
518
+ * and the render that waited for the editor. */
519
+ _closed(s, reason) {
520
+ const t = this.table;
521
+ if (reason === 'dispose') return;
522
+ if (reason === 'key' || reason === 'editor') {
523
+ try { t._tableEl?.focus({ preventScroll: true }); } catch (_) { /* detached */ }
524
+ }
525
+ if (this.renderWaiting) {
526
+ this.renderWaiting = false;
527
+ this.rowsWaiting.clear(); // the render draws every row
528
+ // A commit made by the virtual window taking the row out is made
529
+ // in the middle of the window's own redraw, which a render would
530
+ // pull out from under it: that one waits for the redraw to end.
531
+ if (reason === 'scroll') queueMicrotask(() => { if (t._wrapperEl) t.render(); });
532
+ else t.render();
533
+ return;
534
+ }
535
+ if (this.rowsWaiting.size) {
536
+ const flush = () => {
537
+ const waiting = [...this.rowsWaiting];
538
+ this.rowsWaiting.clear();
539
+ for (const rowIdx of waiting) this._redraw(rowIdx);
540
+ };
541
+ if (reason === 'scroll') queueMicrotask(() => { if (t._wrapperEl) flush(); });
542
+ else flush();
543
+ }
544
+ }
545
+
546
+ /**
547
+ * Draw a row again after a commit, from the row the table holds now
548
+ * (`currentRow`). `updateRow` rebuilds EVERY cell of the row, so while an
549
+ * editor is open in it (save, Tab, type — the commit settles under the
550
+ * next editor) the row waits for that editor to close (`_closed`), and
551
+ * only the committed cell's marks are painted now.
552
+ */
553
+ _redraw(rowIdx, col = null) {
554
+ const t = this.table;
555
+ const tr = t.getRowElement(rowIdx);
556
+ const row = this.currentRow(rowIdx);
557
+ if (!tr || !row) return;
558
+ if (this._holds(tr)) {
559
+ this.rowsWaiting.add(rowIdx);
560
+ if (col === null || tr.__cellPos === undefined) return;
561
+ const s = this.editor.session;
562
+ for (const td of tr.children) {
563
+ if (td.__colIdx === col && td !== s.td) this._decorateCell(td, tr.__cellPos, rowIdx, col);
564
+ }
565
+ return;
566
+ }
567
+ this.rowsWaiting.delete(rowIdx);
568
+ t.updateRow(rowIdx, row);
569
+ }
570
+
571
+ _committed(s, value, move, reason) {
572
+ if (move) {
573
+ this._sync();
574
+ if (move === 'down' || move === 'up') this.sel.move(move === 'down' ? 1 : -1, 0);
575
+ else this.sel.tab(move === 'left');
576
+ this._changed({ reveal: true });
577
+ }
578
+ const live = s.generation === this.generation;
579
+ const key = `${s.rowIdx}:${s.colIdx}`;
580
+ // Left as it was shown: no edit at all.
581
+ if (Object.is(value, s.previous)) {
582
+ this._closed(s, reason);
583
+ return;
584
+ }
585
+ // STORY-0088 `staged`: the value is staged, not sent.
586
+ if (this.staged) {
587
+ if (live) this.staged.stage(s.rowIdx, s.colIdx, value);
588
+ this._closed(s, reason);
589
+ return;
590
+ }
591
+ // What the row holds: the table's row now, or — for a commit that
592
+ // outlived its rows — the row it was opened on.
593
+ const held = live ? this.currentRow(s.rowIdx) : s.row;
594
+ const stored = held ? held[s.colIdx] : undefined;
595
+ // A refused cell given back the value its row holds: nothing to save,
596
+ // the refusal is over.
597
+ if (live && this.errors.has(key) && !this.pending.has(key) && Object.is(value, stored)) {
598
+ this.errors.delete(key);
599
+ this.latest.delete(key);
600
+ this._redraw(s.rowIdx, s.colIdx);
601
+ this._closed(s, reason);
602
+ return;
603
+ }
604
+ const fn = this._cfg().onCellCommit;
605
+ const token = {};
606
+ if (live) this.latest.set(key, token);
607
+ const finish = (result) => {
608
+ if (!live || s.generation !== this.generation) return;
609
+ const newest = this.latest.get(key) === token;
610
+ const good = !(result && result.ok === false);
611
+ // The value goes into the row the table holds NOW, so a newer row
612
+ // the consumer put in meanwhile (`updateRow`) keeps its other cells.
613
+ if (good) {
614
+ const row = this.currentRow(s.rowIdx);
615
+ if (Array.isArray(row)) row[s.colIdx] = value;
616
+ }
617
+ // A newer commit of the cell is still out: its marks are its own.
618
+ if (newest) {
619
+ this.latest.delete(key);
620
+ this.pending.delete(key);
621
+ if (good) this.errors.delete(key);
622
+ else this.errors.set(key, { value, message: String(result.error ?? 'Not saved') });
623
+ }
624
+ this._redraw(s.rowIdx, s.colIdx);
625
+ };
626
+ const refused = (err) => ({ ok: false, error: err?.message || String(err ?? 'Not saved') });
627
+ let result;
628
+ if (typeof fn === 'function') {
629
+ try {
630
+ result = fn({ rowIdx: s.rowIdx, colIdx: s.colIdx, value, previous: stored });
631
+ } catch (err) {
632
+ result = refused(err);
633
+ }
634
+ }
635
+ if (result && typeof result.then === 'function') {
636
+ if (live) {
637
+ this.pending.set(key, value);
638
+ this.errors.delete(key);
639
+ this._redraw(s.rowIdx, s.colIdx);
640
+ }
641
+ this._closed(s, reason);
642
+ result.then(finish, (err) => finish(refused(err)));
643
+ return;
644
+ }
645
+ finish(result);
646
+ this._closed(s, reason);
647
+ }
648
+ }