flexdesk 0.16.1 → 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 (252) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/LICENSE +0 -0
  3. package/README.md +2217 -42
  4. package/THIRD_PARTY_LICENSES.md +0 -0
  5. package/css/base.css +1834 -75
  6. package/css/flexdesk.css +3146 -164
  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-B5Q53LAJ.js → chunk-UWO43BZU.js} +2 -2
  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 +3146 -164
  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 +657 -234
  61. package/dist/tiles.js.map +4 -4
  62. package/dist/tokens.css +0 -0
  63. package/dist/widgets.js +3252 -2124
  64. package/dist/widgets.js.map +4 -4
  65. package/dist/wm.js +1227 -347
  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 +7 -7
  144. package/src/tiles/scope_picker.js +0 -0
  145. package/src/tiles/tile_base.js +1 -1
  146. package/src/tiles/tile_config_form.js +442 -0
  147. package/src/tiles/tile_grid.js +2 -0
  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 +0 -0
  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-B5Q53LAJ.js.map +0 -7
  241. package/dist/chunk-FBGP5KN5.js.map +0 -7
  242. package/dist/chunk-FL5KFNQH.js.map +0 -7
  243. package/dist/chunk-NLTOY57M.js.map +0 -7
  244. package/dist/chunk-NNFTETK3.js +0 -633
  245. package/dist/chunk-NNFTETK3.js.map +0 -7
  246. package/dist/chunk-O5OHMWBB.js +0 -284
  247. package/dist/chunk-O5OHMWBB.js.map +0 -7
  248. package/dist/chunk-RQM5Z2MU.js.map +0 -7
  249. package/dist/chunk-SN4LSOAR.js.map +0 -7
  250. package/dist/chunk-W6FBJLPW.js +0 -367
  251. package/dist/chunk-W6FBJLPW.js.map +0 -7
  252. package/src/ui/controllers/panel_state_machine.js +0 -634
@@ -0,0 +1,343 @@
1
+ /**
2
+ * staged_edits.js — STORY-0088. DataTable's STAGED edits (`staged: true`).
3
+ * INTERNAL to the table, like `cell_grid.js`: built by the cell grid, and
4
+ * reading the table's private fields.
5
+ *
6
+ * With `staged`, an editor's commit does not go to `onCellCommit`: the value
7
+ * is STAGED here, drawn `twm-dt-cell--staged`, and recorded in the undo ring
8
+ * (`staged_history.js`) as a before/after delta. `commit()` hands every staged
9
+ * cell to the consumer's `onCommit({edits})` in one call; `discard()` drops
10
+ * them. While any cell is staged the table's sort, filters and pages are
11
+ * FROZEN — each such control is disabled with the reason on it
12
+ * (`data-refusal`), and the methods behind them refuse — because a new order
13
+ * or page would move the rows being edited out from under the person editing.
14
+ *
15
+ * A cell has three states: absent (no change), staged as null, staged as a
16
+ * value. Staging the value its row already holds makes it absent again.
17
+ */
18
+
19
+ import { StagedHistory, DEFAULT_HISTORY_LIMIT } from './staged_history.js';
20
+
21
+ const key = (rowIdx, colIdx) => `${rowIdx}:${colIdx}`;
22
+
23
+ /** The controls a freeze holds, under the table's wrapper. The export button
24
+ * moves no rows and is left alone. */
25
+ const FROZEN_SELECTORS = [
26
+ 'th.sortable',
27
+ '.data-table__filter-cell:not(.data-table__filter-cell--empty)',
28
+ '.data-table__filter-cell:not(.data-table__filter-cell--empty) input',
29
+ '.data-table__filter-cell:not(.data-table__filter-cell--empty) button',
30
+ '.data-table__filter-cell:not(.data-table__filter-cell--empty) select',
31
+ '.data-table__filter-cell:not(.data-table__filter-cell--empty) textarea',
32
+ '.twm-pagination-controls button:not(.twm-pagination-btn--export)',
33
+ '.twm-pagination-controls input',
34
+ '.twm-dt-paging button',
35
+ '.twm-dt-paging input',
36
+ ].join(', ');
37
+
38
+ /** The attributes a freeze writes, put back as they were when it lifts. */
39
+ const SAVED_ATTRS = ['title', 'data-refusal', 'data-tooltip', 'aria-disabled'];
40
+
41
+ export class StagedEdits {
42
+ /** @param {import('./cell_grid.js').CellGrid} grid */
43
+ constructor(grid) {
44
+ this.grid = grid;
45
+ this.table = grid.table;
46
+ /** `rowIdx:colIdx` → {rowIdx, colIdx, value}: what is staged. */
47
+ this.cells = new Map();
48
+ /** `rowIdx:colIdx` → message: a cell the last commit refused. */
49
+ this.errors = new Map();
50
+ /** The commit out, or null. */
51
+ this.committing = null;
52
+ /** Bumped by new rows: a commit that outlived its rows marks nothing. */
53
+ this.generation = 0;
54
+ /** Rows touched while the ring applies an entry, drawn after it. */
55
+ this._touched = null;
56
+ /** Element → the attributes it had before the freeze. */
57
+ this._frozen = new Map();
58
+ const limit = this.table.config.historyLimit;
59
+ this.history = new StagedHistory({
60
+ apply: (entry, direction) => this._apply(entry, direction),
61
+ limit: limit == null ? DEFAULT_HISTORY_LIMIT : limit,
62
+ });
63
+ }
64
+
65
+ // ── what is staged ───────────────────────────────────────────────────
66
+
67
+ has(k) { return this.cells.has(k); }
68
+
69
+ value(k) { return this.cells.get(k)?.value; }
70
+
71
+ /** The value the row holds for a cell: what "no change" means. */
72
+ original(rowIdx, colIdx) {
73
+ const row = this.grid.currentRow(rowIdx);
74
+ return row ? row[colIdx] : undefined;
75
+ }
76
+
77
+ /** Every staged cell as `{rowIdx, colIdx, value, previous}`, by row then
78
+ * column; `previous` is the value the row holds. */
79
+ edits() {
80
+ return [...this.cells.values()]
81
+ .sort((a, b) => a.rowIdx - b.rowIdx || a.colIdx - b.colIdx)
82
+ .map(({ rowIdx, colIdx, value }) => ({
83
+ rowIdx, colIdx, value, previous: this.original(rowIdx, colIdx),
84
+ }));
85
+ }
86
+
87
+ _side(k) {
88
+ return this.cells.has(k) ? { has: true, value: this.cells.get(k).value } : { has: false, value: null };
89
+ }
90
+
91
+ /** Write one side of a delta: stage the value, or take the cell out. */
92
+ _write(rowIdx, colIdx, side) {
93
+ const k = key(rowIdx, colIdx);
94
+ if (side.has) this.cells.set(k, { rowIdx, colIdx, value: side.value });
95
+ else this.cells.delete(k);
96
+ // A cell staged again (or taken back) is no longer what a commit refused.
97
+ this.errors.delete(k);
98
+ }
99
+
100
+ /**
101
+ * Stage a value in a cell, as one undo entry (inside an open group, part
102
+ * of it). The value its row holds takes the cell back to no change.
103
+ * Returns whether anything changed.
104
+ */
105
+ stage(rowIdx, colIdx, value) {
106
+ if (this.committing || !this._stageOne(rowIdx, colIdx, value)) return false;
107
+ this.grid._redraw(rowIdx, colIdx);
108
+ this._changed();
109
+ return true;
110
+ }
111
+
112
+ /**
113
+ * STORY-0089. Stage many cells (`[{rowIdx, colIdx, value}]`) as ONE undo
114
+ * entry — a fill, a paste — each as `stage` would; the rows are drawn and
115
+ * the listeners told once, at the end. Returns how many cells changed.
116
+ */
117
+ stageMany(cells, label = '') {
118
+ if (this.committing) return 0;
119
+ const rows = new Set();
120
+ let changed = 0;
121
+ this.history.beginGroup(label);
122
+ try {
123
+ for (const { rowIdx, colIdx, value } of cells) {
124
+ if (this._stageOne(rowIdx, colIdx, value)) { changed++; rows.add(rowIdx); }
125
+ }
126
+ } finally {
127
+ this.history.endGroup();
128
+ }
129
+ for (const rowIdx of rows) this.grid._redraw(rowIdx);
130
+ if (changed) this._changed();
131
+ return changed;
132
+ }
133
+
134
+ /** One cell's new state and its delta in the ring; whether it changed. */
135
+ _stageOne(rowIdx, colIdx, value) {
136
+ const k = key(rowIdx, colIdx);
137
+ const before = this._side(k);
138
+ const after = Object.is(value, this.original(rowIdx, colIdx))
139
+ ? { has: false, value: null } : { has: true, value };
140
+ if (before.has === after.has && Object.is(before.value, after.value)) return false;
141
+ this._write(rowIdx, colIdx, after);
142
+ this.history.record({ action: 'cell:stage', row: rowIdx, col: colIdx, before, after });
143
+ return true;
144
+ }
145
+
146
+ _apply(entry, direction) {
147
+ this._write(entry.row, entry.col, direction === 'undo' ? entry.before : entry.after);
148
+ this._touched?.add(entry.row);
149
+ }
150
+
151
+ _walk(step) {
152
+ if (this.committing) return false;
153
+ this._touched = new Set();
154
+ let moved = false;
155
+ try {
156
+ moved = step();
157
+ } finally {
158
+ const rows = this._touched;
159
+ this._touched = null;
160
+ for (const rowIdx of rows) this.grid._redraw(rowIdx);
161
+ }
162
+ if (moved) this._changed();
163
+ return moved;
164
+ }
165
+
166
+ undo() { return this._walk(() => this.history.undo()); }
167
+
168
+ redo() { return this._walk(() => this.history.redo()); }
169
+
170
+ /** Drop every staged cell, its marks and the ring. */
171
+ discard() {
172
+ if (this.committing) return false;
173
+ const rows = this._rows();
174
+ const had = this.cells.size > 0 || this.errors.size > 0;
175
+ this.cells.clear();
176
+ this.errors.clear();
177
+ this.history.reset();
178
+ for (const rowIdx of rows) this.grid._redraw(rowIdx);
179
+ if (had) this._changed();
180
+ return had;
181
+ }
182
+
183
+ /** New rows (`setData({rows})`): the cells named rows that are gone. */
184
+ clear() {
185
+ this.generation++;
186
+ this.committing = null;
187
+ const had = this.cells.size > 0 || this.errors.size > 0;
188
+ this.cells.clear();
189
+ this.errors.clear();
190
+ this.history.reset();
191
+ if (had) this._changed();
192
+ }
193
+
194
+ _rows() {
195
+ const rows = new Set();
196
+ for (const c of this.cells.values()) rows.add(c.rowIdx);
197
+ for (const k of this.errors.keys()) rows.add(Number(k.split(':')[0]));
198
+ return rows;
199
+ }
200
+
201
+ // ── the commit ───────────────────────────────────────────────────────
202
+
203
+ /**
204
+ * Hand every staged cell to `onCommit({edits})`, once. Resolves
205
+ * `{ok: true, committed}` when it succeeded — the values are written into
206
+ * the rows the table holds, and the staged cells and the ring are gone —
207
+ * or `{ok: false, errors}` when it refused: per-cell errors mark their
208
+ * cells, a refusal with none marks every staged cell, and every staged
209
+ * value stays.
210
+ */
211
+ commit() {
212
+ if (this.committing) return this.committing;
213
+ // An open editor's value is staged first: it is part of the batch.
214
+ if (this.grid.isEditing()) this.grid.editor.commit(null, 'commit');
215
+ const fn = this.table.config.onCommit;
216
+ const edits = this.edits();
217
+ if (!edits.length) return Promise.resolve({ ok: true, committed: 0 });
218
+ if (typeof fn !== 'function') {
219
+ console.warn('[DataTable] commit() needs an `onCommit`; nothing was committed.');
220
+ return Promise.resolve({ ok: false, errors: [] });
221
+ }
222
+ const generation = this.generation;
223
+ const refused = (err) => ({ ok: false, error: err?.message || String(err ?? 'Not saved') });
224
+ let result;
225
+ try {
226
+ result = fn({ edits: edits.map((e) => ({ ...e })) });
227
+ } catch (err) {
228
+ result = refused(err);
229
+ }
230
+ const out = Promise.resolve(result)
231
+ .then((r) => r, refused)
232
+ .then((r) => {
233
+ if (generation !== this.generation) return { ok: false, errors: [] };
234
+ this.committing = null;
235
+ return this._settle(edits, r);
236
+ });
237
+ this.committing = out;
238
+ this._changed();
239
+ return out;
240
+ }
241
+
242
+ _settle(edits, result) {
243
+ const listed = Array.isArray(result?.errors) ? result.errors : [];
244
+ if (listed.length || result?.ok === false) {
245
+ const errors = [];
246
+ const mark = (rowIdx, colIdx, message) => {
247
+ const k = key(rowIdx, colIdx);
248
+ if (!this.cells.has(k)) return;
249
+ const text = String(message ?? 'Not saved');
250
+ this.errors.set(k, text);
251
+ errors.push({ rowIdx, colIdx, error: text });
252
+ };
253
+ if (listed.length) {
254
+ for (const e of listed) mark(e?.rowIdx, e?.colIdx, e?.error ?? e?.message);
255
+ } else {
256
+ for (const e of edits) mark(e.rowIdx, e.colIdx, result.error);
257
+ }
258
+ for (const rowIdx of this._rows()) this.grid._redraw(rowIdx);
259
+ this._changed();
260
+ return { ok: false, errors };
261
+ }
262
+ const rows = this._rows();
263
+ for (const e of edits) {
264
+ const row = this.grid.currentRow(e.rowIdx);
265
+ if (Array.isArray(row)) row[e.colIdx] = e.value;
266
+ }
267
+ this.cells.clear();
268
+ this.errors.clear();
269
+ this.history.reset();
270
+ for (const rowIdx of rows) this.grid._redraw(rowIdx);
271
+ this._changed();
272
+ return { ok: true, committed: edits.length };
273
+ }
274
+
275
+ // ── the freeze ───────────────────────────────────────────────────────
276
+
277
+ /** Why sort, filters and pages are held, or null when nothing is staged. */
278
+ freezeReason() {
279
+ const n = this.cells.size;
280
+ if (!n) return null;
281
+ const own = this.table.config.stagedRefusal;
282
+ if (typeof own === 'function') {
283
+ try { return String(own(n)); } catch (err) {
284
+ console.error('[DataTable] stagedRefusal threw', err);
285
+ }
286
+ } else if (typeof own === 'string' && own) return own;
287
+ return `Sorting, filtering and paging are held while ${n} change${n === 1 ? ' is' : 's are'} `
288
+ + 'not committed. Commit or discard them first.';
289
+ }
290
+
291
+ /** Disable every control the freeze holds with the reason on it, or put
292
+ * back the ones it held. Run after every render and every change. */
293
+ paintFreeze() {
294
+ const root = this.table._wrapperEl;
295
+ const reason = this.freezeReason();
296
+ root?.classList.toggle('twm-dt--frozen', !!reason);
297
+ if (!reason) {
298
+ for (const [el, saved] of this._frozen) this._thaw(el, saved);
299
+ this._frozen.clear();
300
+ return;
301
+ }
302
+ for (const el of this._frozen.keys()) if (!el.isConnected) this._frozen.delete(el);
303
+ if (!root) return;
304
+ for (const el of root.querySelectorAll(FROZEN_SELECTORS)) {
305
+ if (!this._frozen.has(el)) {
306
+ const saved = {};
307
+ for (const a of SAVED_ATTRS) saved[a] = el.getAttribute(a);
308
+ saved.disabled = 'disabled' in el ? el.disabled : null;
309
+ this._frozen.set(el, saved);
310
+ }
311
+ el.setAttribute('data-refusal', reason);
312
+ el.setAttribute('title', reason);
313
+ if (el.hasAttribute('data-tooltip')) el.setAttribute('data-tooltip', reason);
314
+ el.setAttribute('aria-disabled', 'true');
315
+ if ('disabled' in el) el.disabled = true;
316
+ }
317
+ }
318
+
319
+ _thaw(el, saved) {
320
+ for (const a of SAVED_ATTRS) {
321
+ if (saved[a] == null) el.removeAttribute(a);
322
+ else el.setAttribute(a, saved[a]);
323
+ }
324
+ if (saved.disabled !== null) el.disabled = saved.disabled;
325
+ }
326
+
327
+ /** After every change: the freeze, and whoever listens. */
328
+ _changed() {
329
+ this.paintFreeze();
330
+ const fn = this.table.config.onStagedChange;
331
+ if (typeof fn !== 'function') return;
332
+ try {
333
+ fn({
334
+ count: this.cells.size,
335
+ canUndo: this.history.canUndo,
336
+ canRedo: this.history.canRedo,
337
+ committing: !!this.committing,
338
+ });
339
+ } catch (err) {
340
+ console.error('[DataTable] onStagedChange threw', err);
341
+ }
342
+ }
343
+ }
@@ -0,0 +1,172 @@
1
+ /**
2
+ * staged_history.js — STORY-0088. Ctrl+Z and Ctrl+Y over a grid's STAGED
3
+ * edits, and over nothing else. No DOM: the ring decides WHAT to reverse, and
4
+ * the grid that owns the staged cells decides HOW (`apply`).
5
+ *
6
+ * ══ STAGED, NEVER COMMITTED ══════════════════════════════════════════════
7
+ *
8
+ * Ctrl+Z takes back what was typed and not yet committed. It never reaches a
9
+ * commit: once a batch has gone to the consumer, `reset()` empties the ring,
10
+ * so an undo or a redo after a commit does nothing. `ACTIONS` lists the only
11
+ * thing the ring knows how to reverse, and `record()` refuses anything else —
12
+ * an undoable action that writes somewhere cannot be added by accident.
13
+ *
14
+ * ══ A DELTA, NOT A SNAPSHOT ══════════════════════════════════════════════
15
+ *
16
+ * An entry is the change itself: one cell's `before` and `after`. A paste of
17
+ * 20,000 cells is one group of 20,000 deltas, held once — not 20,000 entries'
18
+ * worth of copies of the staged set. `limit` counts ENTRIES (a group is one).
19
+ *
20
+ * Each side is `{has, value}`, not just a value, because a staged cell has
21
+ * three states: absent (no change), staged as null, staged as something. A
22
+ * side that held only the value could not tell the first two apart, and undoing
23
+ * a cell staged as null would leave a staged null behind instead of no change.
24
+ */
25
+
26
+ /** The only action that exists: one cell's staged state changed.
27
+ * `{action: 'cell:stage', row, col, before: {has, value}, after: {has, value}}`. */
28
+ export const ACTIONS = Object.freeze(['cell:stage']);
29
+
30
+ /** Entries kept when no `limit` is given. */
31
+ export const DEFAULT_HISTORY_LIMIT = 100;
32
+
33
+ export class StagedHistory {
34
+ /**
35
+ * @param {object} cfg
36
+ * @param {(entry, direction: 'undo'|'redo') => void} cfg.apply writes one
37
+ * leaf entry's `before` (undo) or `after` (redo) into the staged cells.
38
+ * Called once per leaf; the ring holds no reference to those cells.
39
+ * @param {({canUndo, canRedo, depth}) => void} [cfg.onState] after every
40
+ * change of the stacks.
41
+ * @param {number} [cfg.limit=100] entries kept; the oldest fall off.
42
+ */
43
+ constructor({ apply, onState = null, limit = DEFAULT_HISTORY_LIMIT } = {}) {
44
+ if (typeof apply !== 'function') {
45
+ throw new Error('StagedHistory needs an `apply(entry, direction)`: a ring with no '
46
+ + 'writer would report every undo as done and change nothing.');
47
+ }
48
+ this.apply = apply;
49
+ this.onState = onState;
50
+ this.limit = Number.isFinite(limit) && limit >= 1 ? Math.floor(limit) : DEFAULT_HISTORY_LIMIT;
51
+ this.undoStack = [];
52
+ this.redoStack = [];
53
+ this.group = null;
54
+ /** Groups nest; only the outermost `endGroup` closes one. */
55
+ this.depthOpen = 0;
56
+ /** Set while `apply` runs: a record from inside it is ignored, so an
57
+ * undo that writes through the grid's own staging cannot record its
58
+ * own inverse. */
59
+ this.applying = false;
60
+ }
61
+
62
+ get canUndo() { return this.undoStack.length > 0; }
63
+ get canRedo() { return this.redoStack.length > 0; }
64
+ get depth() { return this.undoStack.length; }
65
+
66
+ /** Open a group: everything recorded until the matching `endGroup` is one
67
+ * undo (a fill, a paste). */
68
+ beginGroup(label = '') {
69
+ this.depthOpen += 1;
70
+ if (this.depthOpen === 1) this.group = { action: 'batch', label, entries: [] };
71
+ }
72
+
73
+ /** Close a group. An empty one is dropped, so it costs no undo step; one
74
+ * holding a single entry is that entry. */
75
+ endGroup() {
76
+ if (!this.depthOpen) return;
77
+ this.depthOpen -= 1;
78
+ if (this.depthOpen) return;
79
+ const group = this.group;
80
+ this.group = null;
81
+ if (!group) return;
82
+ if (group.entries.length === 1) this._push(group.entries[0]);
83
+ else if (group.entries.length) this._push(group);
84
+ }
85
+
86
+ /** Record one delta. Returns whether it was recorded (false while an
87
+ * entry is being applied). Throws on an action that is not in `ACTIONS`. */
88
+ record(entry) {
89
+ if (this.applying) return false;
90
+ if (!entry || !ACTIONS.includes(entry.action)) {
91
+ throw new Error(`${entry?.action} is not an undoable action: the ring reverses `
92
+ + 'staged cells only, never a commit.');
93
+ }
94
+ if (this.group) { this.group.entries.push(entry); return true; }
95
+ this._push(entry);
96
+ return true;
97
+ }
98
+
99
+ undo() {
100
+ this._closeOpenGroup();
101
+ const entry = this.undoStack.pop();
102
+ if (!entry) return false;
103
+ this._run(entry, 'undo');
104
+ this.redoStack.push(entry);
105
+ this._publish();
106
+ return true;
107
+ }
108
+
109
+ redo() {
110
+ this._closeOpenGroup();
111
+ const entry = this.redoStack.pop();
112
+ if (!entry) return false;
113
+ this._run(entry, 'redo');
114
+ this.undoStack.push(entry);
115
+ this._publish();
116
+ return true;
117
+ }
118
+
119
+ /** Forget everything — after a commit or a discard, whose cells the
120
+ * entries no longer describe. An open group is closed with it. */
121
+ reset() {
122
+ this.undoStack = [];
123
+ this.redoStack = [];
124
+ this.group = null;
125
+ this.depthOpen = 0;
126
+ this._publish();
127
+ }
128
+
129
+ /** A group left open (a caller that threw) is closed before an undo, so it
130
+ * cannot swallow every later entry. */
131
+ _closeOpenGroup() {
132
+ if (!this.depthOpen) return;
133
+ this.depthOpen = 1;
134
+ this.endGroup();
135
+ }
136
+
137
+ _push(entry) {
138
+ this.undoStack.push(entry);
139
+ // A new edit after an undo ends the redo branch.
140
+ this.redoStack = [];
141
+ if (this.undoStack.length > this.limit) this.undoStack.splice(0, this.undoStack.length - this.limit);
142
+ this._publish();
143
+ }
144
+
145
+ _publish() {
146
+ try {
147
+ this.onState?.({ canUndo: this.canUndo, canRedo: this.canRedo, depth: this.undoStack.length });
148
+ } catch (err) {
149
+ console.error('[StagedHistory] onState threw', err);
150
+ }
151
+ }
152
+
153
+ _run(entry, direction) {
154
+ this.applying = true;
155
+ try {
156
+ this._applyOne(entry, direction);
157
+ } finally {
158
+ this.applying = false;
159
+ }
160
+ }
161
+
162
+ /** A group replays in REVERSE on the way back (two writes to one cell must
163
+ * unwind last first) and in order on the way forward. */
164
+ _applyOne(entry, direction) {
165
+ if (entry.action === 'batch') {
166
+ const entries = direction === 'undo' ? [...entry.entries].reverse() : entry.entries;
167
+ for (const child of entries) this._applyOne(child, direction);
168
+ return;
169
+ }
170
+ this.apply(entry, direction);
171
+ }
172
+ }
File without changes
File without changes