flexdesk 0.16.1 → 0.17.1

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 (262) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/LICENSE +0 -0
  3. package/README.md +2435 -42
  4. package/THIRD_PARTY_LICENSES.md +0 -0
  5. package/canvas.js +18 -0
  6. package/charts.js +21 -0
  7. package/core.js +14 -0
  8. package/css/base.css +1834 -75
  9. package/css/flexdesk.css +3165 -159
  10. package/css/overrides.css +16 -0
  11. package/css/reset.css +0 -0
  12. package/css/tokens.css +0 -0
  13. package/dist/canvas.js +6 -239
  14. package/dist/canvas.js.map +3 -3
  15. package/dist/charts.js +707 -18
  16. package/dist/charts.js.map +4 -4
  17. package/dist/chunk-3PHCPZHT.js +0 -0
  18. package/dist/chunk-3PHCPZHT.js.map +0 -0
  19. package/dist/chunk-4A7SJC3T.js +503 -0
  20. package/dist/chunk-4A7SJC3T.js.map +7 -0
  21. package/dist/{chunk-6JSOVNID.js → chunk-CVMCZZX2.js} +635 -10
  22. package/dist/chunk-CVMCZZX2.js.map +7 -0
  23. package/dist/chunk-D3COLZZ2.js +1311 -0
  24. package/dist/chunk-D3COLZZ2.js.map +7 -0
  25. package/dist/chunk-FOOS3T5L.js +0 -0
  26. package/dist/chunk-FOOS3T5L.js.map +0 -0
  27. package/dist/{chunk-SN4LSOAR.js → chunk-GOQLR5WR.js} +452 -2
  28. package/dist/chunk-GOQLR5WR.js.map +7 -0
  29. package/dist/{chunk-FL5KFNQH.js → chunk-HNU6MM2B.js} +52 -17
  30. package/dist/chunk-HNU6MM2B.js.map +7 -0
  31. package/dist/chunk-HYJYYEPK.js +375 -0
  32. package/dist/chunk-HYJYYEPK.js.map +7 -0
  33. package/dist/chunk-JKZLNKG6.js +2041 -0
  34. package/dist/chunk-JKZLNKG6.js.map +7 -0
  35. package/dist/chunk-JYWURG5T.js +0 -0
  36. package/dist/chunk-JYWURG5T.js.map +0 -0
  37. package/dist/{chunk-FBGP5KN5.js → chunk-LWV23YVE.js} +5073 -532
  38. package/dist/chunk-LWV23YVE.js.map +7 -0
  39. package/dist/chunk-N7HC3ADV.js +30 -0
  40. package/dist/chunk-N7HC3ADV.js.map +7 -0
  41. package/dist/{chunk-RQM5Z2MU.js → chunk-RMTRWWEO.js} +633 -311
  42. package/dist/chunk-RMTRWWEO.js.map +7 -0
  43. package/dist/{chunk-NLTOY57M.js → chunk-UURZ37SD.js} +28 -48
  44. package/dist/chunk-UURZ37SD.js.map +7 -0
  45. package/dist/{chunk-B5Q53LAJ.js → chunk-UWO43BZU.js} +2 -2
  46. package/dist/chunk-UWO43BZU.js.map +7 -0
  47. package/dist/chunk-WVFGV5FT.js +0 -0
  48. package/dist/chunk-WVFGV5FT.js.map +0 -0
  49. package/dist/core.js +170 -118
  50. package/dist/core.js.map +2 -2
  51. package/dist/editor.js +156 -70
  52. package/dist/editor.js.map +2 -2
  53. package/dist/flexdesk.css +3165 -159
  54. package/dist/flow.js +2789 -306
  55. package/dist/flow.js.map +4 -4
  56. package/dist/host.js +109 -3
  57. package/dist/host.js.map +3 -3
  58. package/dist/reset.css +0 -0
  59. package/dist/tile_registry-6WZPTUZV.js +0 -0
  60. package/dist/tile_registry-6WZPTUZV.js.map +0 -0
  61. package/dist/tiles.js +657 -234
  62. package/dist/tiles.js.map +4 -4
  63. package/dist/tokens.css +0 -0
  64. package/dist/widgets.js +3290 -2124
  65. package/dist/widgets.js.map +4 -4
  66. package/dist/wm.js +1227 -347
  67. package/dist/wm.js.map +4 -4
  68. package/editor.js +20 -0
  69. package/flow.js +21 -0
  70. package/host.js +14 -0
  71. package/package.json +12 -1
  72. package/src/canvas/canvas_adapter.js +5 -0
  73. package/src/canvas/edge_router.js +0 -0
  74. package/src/canvas/graph/connector_router.js +0 -0
  75. package/src/canvas/graph/history_service.js +0 -0
  76. package/src/canvas/graph/node_platform.js +0 -0
  77. package/src/canvas/index.js +1 -0
  78. package/src/canvas/layered_layout.js +244 -0
  79. package/src/charts/chart_types.js +0 -0
  80. package/src/charts/data_series_plot_window.js +0 -0
  81. package/src/charts/downsample.js +2 -2
  82. package/src/charts/plot_config.js +53 -29
  83. package/src/charts/plot_config_editor.js +627 -0
  84. package/src/charts/plot_grid.js +449 -0
  85. package/src/charts/plot_popout_window.js +103 -317
  86. package/src/charts/plotly_wrapper.js +22 -2
  87. package/src/charts/series_transforms.js +206 -0
  88. package/src/core/event_bus.js +0 -0
  89. package/src/core/logging.js +1 -1
  90. package/src/core/settings.js +105 -19
  91. package/src/core/state_guard.js +93 -51
  92. package/src/core/state_guard_config.js +85 -74
  93. package/src/core/state_machine.js +0 -0
  94. package/src/editor/editor_pane.js +3 -3
  95. package/src/editor/monaco_editor_factory.js +124 -60
  96. package/src/editor/monaco_loader.js +103 -28
  97. package/src/editor/notebook_search.js +0 -0
  98. package/src/editor/notebook_tab_bar.js +38 -52
  99. package/src/editor/notebook_undo_manager.js +0 -0
  100. package/src/editor/split_pane_container.js +6 -3
  101. package/src/flow/canvas_editor/connections.js +90 -0
  102. package/src/flow/canvas_editor/editor.js +2351 -153
  103. package/src/flow/canvas_editor/index.js +2 -1
  104. package/src/flow/canvas_editor/layout.js +119 -12
  105. package/src/flow/canvas_editor/strings.js +80 -0
  106. package/src/flow/kit/always_before.js +0 -0
  107. package/src/flow/kit/catalogue.js +97 -3
  108. package/src/flow/kit/chip_input.js +0 -0
  109. package/src/flow/kit/dom.js +0 -0
  110. package/src/flow/kit/findings.js +0 -0
  111. package/src/flow/kit/history.js +0 -0
  112. package/src/flow/kit/index.js +3 -3
  113. package/src/flow/kit/keys.js +7 -1
  114. package/src/flow/kit/logic_graph.js +97 -15
  115. package/src/flow/kit/references.js +0 -0
  116. package/src/flow/kit/settings_panel.js +0 -0
  117. package/src/flow/kit/settings_schema.js +2 -1
  118. package/src/flow/kit/step_picker.js +0 -0
  119. package/src/flow/kit/strings.js +3 -1
  120. package/src/flow/kit/value_picker.js +0 -0
  121. package/src/flow/kit/widgets.js +86 -109
  122. package/src/flow/lanes/editor.js +136 -14
  123. package/src/flow/lanes/index.js +3 -1
  124. package/src/flow/lanes/layout.js +0 -0
  125. package/src/flow/lanes/pipeline.js +44 -2
  126. package/src/flow/lanes/preview.js +0 -0
  127. package/src/flow/lanes/strings.js +1 -0
  128. package/src/flow/outline/editor.js +0 -0
  129. package/src/flow/outline/index.js +0 -0
  130. package/src/flow/outline/mapping.js +0 -0
  131. package/src/flow/outline/menu.js +0 -0
  132. package/src/flow/outline/operations.js +0 -0
  133. package/src/flow/outline/recognise.js +0 -0
  134. package/src/flow/outline/strings.js +0 -0
  135. package/src/help/help_modal.js +168 -42
  136. package/src/help/help_registry.js +12 -1
  137. package/src/host/external_links.js +0 -0
  138. package/src/host/file_picker.js +117 -0
  139. package/src/host/host.js +10 -2
  140. package/src/host/photino_host.js +9 -0
  141. package/src/host/photino_host.md +21 -3
  142. package/src/host/pywebview_host.js +10 -0
  143. package/src/tiles/chart_tiles.js +53 -245
  144. package/src/tiles/config_schema.js +8 -0
  145. package/src/tiles/figure_tile.js +322 -0
  146. package/src/tiles/layout_persistence.js +3 -3
  147. package/src/tiles/list_tiles.js +7 -7
  148. package/src/tiles/scope_picker.js +0 -0
  149. package/src/tiles/tile_base.js +1 -1
  150. package/src/tiles/tile_config_form.js +442 -0
  151. package/src/tiles/tile_grid.js +2 -0
  152. package/src/tiles/tile_registry.js +0 -0
  153. package/src/tiling/bottom_panel_tabs.js +67 -99
  154. package/src/tiling/command_palette.js +64 -19
  155. package/src/tiling/content_registry.js +4 -1
  156. package/src/tiling/desktops.js +11 -1
  157. package/src/tiling/entity_sources.js +0 -0
  158. package/src/tiling/keymap.js +403 -128
  159. package/src/tiling/kind_taxonomy.js +78 -0
  160. package/src/tiling/landing_table.js +0 -0
  161. package/src/tiling/loading_overlay.js +0 -0
  162. package/src/tiling/nav_panel.js +59 -10
  163. package/src/tiling/page_factory.js +5 -5
  164. package/src/tiling/panel_availability.js +49 -0
  165. package/src/tiling/panel_keys.js +0 -0
  166. package/src/tiling/shell.js +114 -12
  167. package/src/tiling/tab_strip.js +0 -0
  168. package/src/tiling/tile_breadcrumb.js +0 -0
  169. package/src/tiling/tile_renderer.js +105 -9
  170. package/src/tiling/tile_tab_menu.js +0 -0
  171. package/src/tiling/tile_tree.js +0 -0
  172. package/src/tiling/wm.js +562 -30
  173. package/src/tiling/zoom.js +0 -0
  174. package/src/ui/base/component_base.js +0 -0
  175. package/src/ui/base/controller_base.js +0 -0
  176. package/src/ui/base/page_base.js +0 -0
  177. package/src/ui/components/about_dialog.js +0 -0
  178. package/src/ui/components/action_dropdown.js +30 -2
  179. package/src/ui/components/alert_log.js +0 -0
  180. package/src/ui/components/alerts_pane.js +0 -0
  181. package/src/ui/components/attribute_list_editor.js +0 -0
  182. package/src/ui/components/autocomplete_field.js +0 -0
  183. package/src/ui/components/cell_editor_host.js +293 -0
  184. package/src/ui/components/cell_editors.js +480 -0
  185. package/src/ui/components/cell_grid.js +761 -0
  186. package/src/ui/components/cell_paste.js +243 -0
  187. package/src/ui/components/cell_selection.js +220 -0
  188. package/src/ui/components/choice_controls.js +351 -0
  189. package/src/ui/components/color_field.js +211 -0
  190. package/src/ui/components/color_list_field.js +210 -0
  191. package/src/ui/components/column_fit.js +0 -0
  192. package/src/ui/components/column_groups.js +314 -0
  193. package/src/ui/components/column_order.js +142 -0
  194. package/src/ui/components/computing_status_window.js +98 -25
  195. package/src/ui/components/confirm_dialog.js +81 -38
  196. package/src/ui/components/context_menu.js +2 -2
  197. package/src/ui/components/conversation_view.js +440 -0
  198. package/src/ui/components/crosstab.js +289 -0
  199. package/src/ui/components/data_table.js +696 -71
  200. package/src/ui/components/detail_header.js +22 -11
  201. package/src/ui/components/document_outline.js +397 -0
  202. package/src/ui/components/drag_reorder.js +0 -0
  203. package/src/ui/components/fact_table.js +0 -0
  204. package/src/ui/components/fill_handle.js +322 -0
  205. package/src/ui/components/gallery_picker.js +19 -8
  206. package/src/ui/components/header_actions.js +55 -0
  207. package/src/ui/components/inline_renamer.js +0 -0
  208. package/src/ui/components/job_progress.js +51 -0
  209. package/src/ui/components/managed_window.js +27 -4
  210. package/src/ui/components/menu_bar.js +745 -0
  211. package/src/ui/components/modal.js +449 -75
  212. package/src/ui/components/notification_history.js +3 -3
  213. package/src/ui/components/progress.js +483 -0
  214. package/src/ui/components/record_list.js +0 -0
  215. package/src/ui/components/row_activation.js +0 -0
  216. package/src/ui/components/section_stack.js +298 -0
  217. package/src/ui/components/series.js +173 -0
  218. package/src/ui/components/settings_view.js +634 -0
  219. package/src/ui/components/slide_out_panel.js +43 -6
  220. package/src/ui/components/slider_field.js +275 -18
  221. package/src/ui/components/sortable_list.js +0 -0
  222. package/src/ui/components/staged_edits.js +361 -0
  223. package/src/ui/components/staged_history.js +172 -0
  224. package/src/ui/components/table_filter_chips.js +0 -0
  225. package/src/ui/components/table_state_store.js +0 -0
  226. package/src/ui/components/table_structure.js +182 -0
  227. package/src/ui/components/table_values.js +0 -0
  228. package/src/ui/components/tabs.js +497 -0
  229. package/src/ui/components/tag_input.js +215 -0
  230. package/src/ui/components/toast.js +25 -8
  231. package/src/ui/components/toggle_field.js +175 -0
  232. package/src/ui/components/tree_view.js +55 -34
  233. package/src/ui/components/tsv.js +74 -0
  234. package/src/ui/components/window_taskbar.js +0 -0
  235. package/src/ui/components/wizard.js +563 -0
  236. package/src/ui/controllers/window_chrome_controller.js +2 -2
  237. package/src/ui/notification_center.js +375 -119
  238. package/src/ui/utils/auto_scrollbars.js +0 -0
  239. package/src/ui/utils/markdown.js +709 -0
  240. package/src/ui/utils/native_context_menu.js +0 -0
  241. package/src/ui/utils/overlay_scrollbar.js +16 -12
  242. package/src/ui/utils/raf_resize_observer.js +0 -0
  243. package/src/ui/utils/scroll_lock.js +0 -0
  244. package/src/ui/utils/strip_scroll.js +55 -0
  245. package/src/ui/utils/tooltip_service.js +0 -8
  246. package/tiles.js +27 -0
  247. package/widgets.js +69 -0
  248. package/wm.js +28 -0
  249. package/dist/chunk-6JSOVNID.js.map +0 -7
  250. package/dist/chunk-B5Q53LAJ.js.map +0 -7
  251. package/dist/chunk-FBGP5KN5.js.map +0 -7
  252. package/dist/chunk-FL5KFNQH.js.map +0 -7
  253. package/dist/chunk-NLTOY57M.js.map +0 -7
  254. package/dist/chunk-NNFTETK3.js +0 -633
  255. package/dist/chunk-NNFTETK3.js.map +0 -7
  256. package/dist/chunk-O5OHMWBB.js +0 -284
  257. package/dist/chunk-O5OHMWBB.js.map +0 -7
  258. package/dist/chunk-RQM5Z2MU.js.map +0 -7
  259. package/dist/chunk-SN4LSOAR.js.map +0 -7
  260. package/dist/chunk-W6FBJLPW.js +0 -367
  261. package/dist/chunk-W6FBJLPW.js.map +0 -7
  262. package/src/ui/controllers/panel_state_machine.js +0 -634
@@ -0,0 +1,243 @@
1
+ /**
2
+ * cell_paste.js — STORY-0090. DataTable's PASTE (`pasteable: true`, with
3
+ * `staged`). INTERNAL to the table, like `cell_grid.js` and `fill_handle.js`:
4
+ * built by the cell grid, and reading the table through it.
5
+ *
6
+ * A block of tab-separated text (`tsv.js` `fromTSV`, what Excel and Google
7
+ * Sheets put on the clipboard) lands at the CURSOR cell and fills right, over
8
+ * the columns shown, and down, over the rows in the order shown. Each value is
9
+ * read by its column's editor's parse (`cell_editors.js`, or an injected
10
+ * editor's own `parse`), and every cell that reads is STAGED
11
+ * (`staged_edits.js`) as ONE undo entry.
12
+ *
13
+ * - Columns past the last one shown are dropped, with a warning naming how
14
+ * many.
15
+ * - Rows past the last loaded row ask `onPasteOverrun({rows: n})` first:
16
+ * anything but `true` (or a Promise of it) stages nothing; `true` stages
17
+ * the rows that fit and hands the extra ones to `onRowsAppend`.
18
+ * - A value that does not parse marks its cell (`twm-dt-cell--error`, the
19
+ * message as `title` and `data-error`, the text kept in the cell, as an
20
+ * editor's refusal is) and the rest of the block still stages.
21
+ * - A cell `editable` refuses is skipped — nothing is staged in it — and
22
+ * drawn `twm-dt-cell--paste-skipped` until the selection moves, the next
23
+ * paste or fill, or new rows.
24
+ */
25
+
26
+ import { fromTSV } from './tsv.js';
27
+ import { CELL_EDITORS } from './cell_editors.js';
28
+
29
+ const key = (rowIdx, colIdx) => `${rowIdx}:${colIdx}`;
30
+
31
+ export class CellPaste {
32
+ /** @param {import('./cell_grid.js').CellGrid} grid */
33
+ constructor(grid) {
34
+ this.grid = grid;
35
+ /** `rowIdx:colIdx` of the cells the last paste skipped. */
36
+ this.skipped = new Set();
37
+ /** True while a paste waits for `onPasteOverrun` or `onRowsAppend`. */
38
+ this.busy = false;
39
+ }
40
+
41
+ // ── painting (called by the grid for every cell it paints) ───────────
42
+
43
+ paintCell(td, col) {
44
+ const rowIdx = td.parentNode?.__rowIndex;
45
+ td.classList.toggle('twm-dt-cell--paste-skipped', this.skipped.has(key(rowIdx, col)));
46
+ }
47
+
48
+ /** The selection moved, or new rows came: the skipped marks are over. */
49
+ clearMarks() {
50
+ this.skipped.clear();
51
+ }
52
+
53
+ // ── reading a value ──────────────────────────────────────────────────
54
+
55
+ /**
56
+ * The parse a column's pasted text is read with: an injected editor's
57
+ * own (`editors[k].parse`, looked up as the editor is), else the built-in
58
+ * editor's for the column's type, else the text as it is.
59
+ */
60
+ _parserFor(col) {
61
+ const g = this.grid;
62
+ const editors = g._cfg().editors;
63
+ if (editors && typeof editors === 'object') {
64
+ for (const k of [col, g._rawType(col), g._typeOf(col)]) {
65
+ const f = editors[k];
66
+ if (typeof f === 'function') {
67
+ if (typeof f.parse === 'function') return f.parse;
68
+ break;
69
+ }
70
+ }
71
+ }
72
+ const type = g._typeOf(col);
73
+ return Object.prototype.hasOwnProperty.call(CELL_EDITORS, type)
74
+ ? CELL_EDITORS[type].parse : CELL_EDITORS.text.parse;
75
+ }
76
+
77
+ /** One column's reader: `(text) => {ok: true, value} | {ok: false, error}`. */
78
+ _reader(col) {
79
+ const g = this.grid;
80
+ const cfg = g._cfg();
81
+ const spec = g._editSpec(col) || {};
82
+ const parse = this._parserFor(col);
83
+ const ctx = {
84
+ locale: cfg.locale ?? undefined,
85
+ utc: g.table._utc(),
86
+ messages: cfg.editorMessages ?? undefined,
87
+ options: spec.options,
88
+ allowOther: !!spec.allowOther,
89
+ };
90
+ return (text) => {
91
+ let r;
92
+ try { r = parse(text, ctx); } catch (err) {
93
+ return { ok: false, error: err?.message || String(err ?? 'Not pasted') };
94
+ }
95
+ if (r && r.ok === true) return { ok: true, value: r.value };
96
+ if (r && r.ok === false) return { ok: false, error: String(r.error ?? 'Not pasted') };
97
+ return { ok: false, error: 'Not pasted' };
98
+ };
99
+ }
100
+
101
+ /** The position of the last loaded row: the rows held in the order shown
102
+ * (all of them filtered and sorted; a server's page, the rows it sent). */
103
+ _lastLoaded() {
104
+ const t = this.grid.table;
105
+ if (t._isServerSide()) return this.grid.span()[1];
106
+ return t._getProcessedRows().length - 1;
107
+ }
108
+
109
+ _warn(message) {
110
+ console.warn(`[DataTable] paste: ${message}`);
111
+ this.grid.table._notify('Paste', message, 'warn');
112
+ }
113
+
114
+ // ── the paste ────────────────────────────────────────────────────────
115
+
116
+ /**
117
+ * Paste tab-separated `text` at the cursor. Resolves `{rows, cols, staged,
118
+ * skipped, refused, droppedColumns, appended}` — `staged` the NUMBER of
119
+ * cells that changed, `skipped` the cells `editable` refused and `refused`
120
+ * the ones whose text did not parse (`{row, col, rowIdx}`, `refused` with
121
+ * its `text` and `error`), `droppedColumns` the columns past the last one,
122
+ * `appended` the rows handed to `onRowsAppend` — or null when nothing was
123
+ * pasted: empty text, no cursor, an editor open, a commit or another paste
124
+ * out, an overrun declined, or new rows while `onPasteOverrun` was asked.
125
+ */
126
+ async paste(text) {
127
+ const g = this.grid;
128
+ const matrix = fromTSV(text);
129
+ if (!matrix.length || this.busy) return null;
130
+ g._sync();
131
+ const at = g.sel.focus ? { ...g.sel.focus } : null;
132
+ if (!at || g.isEditing() || g.staged.committing) return null;
133
+ const cfg = g._cfg();
134
+ const cols = g.columns();
135
+ const width = Math.max(...matrix.map((line) => line.length));
136
+ const targetCols = cols.slice(cols.indexOf(at.col), cols.indexOf(at.col) + width);
137
+ const droppedColumns = width - targetCols.length;
138
+ const last = this._lastLoaded();
139
+ const fit = Math.max(0, Math.min(matrix.length, last - at.row + 1));
140
+ const extra = matrix.length - fit;
141
+ const generation = g.generation;
142
+
143
+ if (extra > 0) {
144
+ const ask = cfg.onPasteOverrun;
145
+ let answer = false;
146
+ if (typeof ask === 'function') {
147
+ this.busy = true;
148
+ try { answer = await ask({ rows: extra }); } catch (err) {
149
+ console.error('[DataTable] onPasteOverrun threw', err);
150
+ answer = false;
151
+ } finally {
152
+ this.busy = false;
153
+ }
154
+ } else {
155
+ console.warn(`[DataTable] paste: ${extra} row(s) past the last loaded row and no \`onPasteOverrun\`; nothing pasted.`);
156
+ }
157
+ if (answer !== true) return null;
158
+ if (generation !== g.generation || g.isEditing() || g.staged.committing) return null;
159
+ }
160
+
161
+ // What lands in the loaded rows: each cell read, or skipped.
162
+ const cells = [];
163
+ const skipped = [];
164
+ const refused = [];
165
+ const readers = new Map(targetCols.map((col) => [col, this._reader(col)]));
166
+ for (let r = 0; r < fit; r++) {
167
+ const pos = at.row + r;
168
+ const rowIdx = g.rowIndexAt(pos);
169
+ targetCols.forEach((col, c) => {
170
+ if (c >= matrix[r].length) return; // a short line: nothing there
171
+ if (!g.canEdit(pos, col)) { skipped.push({ row: pos, col, rowIdx }); return; }
172
+ const text = matrix[r][c];
173
+ const read = readers.get(col)(text);
174
+ if (read.ok) cells.push({ rowIdx, colIdx: col, value: read.value });
175
+ else refused.push({ row: pos, col, rowIdx, text, error: read.error });
176
+ });
177
+ }
178
+
179
+ // The rows past the last loaded one, as data rows: every pasted
180
+ // column read; one that does not read is null, and listed.
181
+ let appended = 0;
182
+ if (extra > 0) {
183
+ const n = (cfg.headers || []).length;
184
+ const rows = [];
185
+ const errors = [];
186
+ for (let r = fit; r < matrix.length; r++) {
187
+ const row = new Array(n).fill(null);
188
+ targetCols.forEach((col, c) => {
189
+ if (c >= matrix[r].length) return;
190
+ const text = matrix[r][c];
191
+ const read = readers.get(col)(text);
192
+ if (read.ok) row[col] = read.value;
193
+ else errors.push({ row: rows.length, colIdx: col, text, error: read.error });
194
+ });
195
+ rows.push(row);
196
+ }
197
+ const append = cfg.onRowsAppend;
198
+ if (typeof append === 'function') {
199
+ appended = rows.length;
200
+ this.busy = true;
201
+ try { await append({ rows, errors }); } catch (err) {
202
+ console.error('[DataTable] onRowsAppend threw', err);
203
+ } finally {
204
+ this.busy = false;
205
+ }
206
+ } else {
207
+ this._warn(`${rows.length} row(s) past the last loaded row were not pasted (no \`onRowsAppend\`).`);
208
+ }
209
+ }
210
+
211
+ const staged = g.staged.stageMany(cells, `paste ${cells.length} cell(s)`);
212
+
213
+ // The range covers the block that landed, the cursor where it began.
214
+ const lastCol = targetCols[targetCols.length - 1] ?? at.col;
215
+ const lastRow = at.row + Math.max(0, fit - 1);
216
+ g.set({ row: at.row, col: at.col }, { row: lastRow, col: lastCol });
217
+
218
+ // Marked after the selection moved, which clears the last marks.
219
+ for (const s of skipped) this.skipped.add(key(s.rowIdx, s.col));
220
+ const redraw = new Set();
221
+ for (const f of refused) {
222
+ const k = key(f.rowIdx, f.col);
223
+ g.errors.set(k, { value: f.text, message: f.error });
224
+ // Any older commit of the cell still out no longer owns its marks.
225
+ g.latest.set(k, {});
226
+ g.pending.delete(k);
227
+ redraw.add(f.rowIdx);
228
+ }
229
+ for (const rowIdx of redraw) g._redraw(rowIdx);
230
+ if (skipped.length) g.paint();
231
+
232
+ if (droppedColumns > 0) {
233
+ this._warn(`${droppedColumns} column(s) past the last column were not pasted.`);
234
+ }
235
+ return {
236
+ rows: matrix.length, cols: width, staged, skipped, refused, droppedColumns, appended,
237
+ };
238
+ }
239
+
240
+ dispose() {
241
+ this.skipped.clear();
242
+ }
243
+ }
@@ -0,0 +1,220 @@
1
+ /**
2
+ * cell_selection.js — STORY-0003. The model under DataTable's CELL MODE
3
+ * (`selectionMode: 'cells'`): a cursor cell, a range from an anchor to that
4
+ * cursor, and every move the keys make over them. No DOM, so the rules can be
5
+ * tested — or drive another grid — without a table. DataTable's own half (the
6
+ * painting, the pointer, the keys, the editor) is `cell_grid.js`, which is
7
+ * internal to the table.
8
+ *
9
+ * ══ COORDINATES ═════════════════════════════════════════════════════════
10
+ *
11
+ * A cell is `{row, col}`. `row` is the row's POSITION in the order shown — the
12
+ * index into `getDisplayedRows()`, after the sort and the filters — because a
13
+ * range is a rectangle on the screen, not in the data. `col` is the DATA column
14
+ * index (the one `headers`, `renderCell` and `hiddenColumns` use); a range
15
+ * spans the columns SHOWN between its two edges, so a hidden column inside it
16
+ * is not part of it. The cursor moves only over the rows of the page drawn.
17
+ */
18
+
19
+ /**
20
+ * The selection model. Rows are positions `first..last`; columns are the data
21
+ * indices in `cols`, in the order they are shown. Both are handed in on every
22
+ * move (`setBounds`), because a filter, a page or a hidden column changes them
23
+ * under a selection that has to survive.
24
+ */
25
+ export class CellSelection {
26
+ constructor() {
27
+ /** @type {{row:number, col:number}|null} */
28
+ this.anchor = null;
29
+ /** @type {{row:number, col:number}|null} the cursor */
30
+ this.focus = null;
31
+ this.first = 0;
32
+ this.last = -1;
33
+ this.cols = [];
34
+ }
35
+
36
+ /** The rows (positions, inclusive) and columns (data indices, shown order)
37
+ * the cursor may stand on. A selection outside them is pulled in. */
38
+ setBounds(first, last, cols) {
39
+ this.first = first;
40
+ this.last = last;
41
+ this.cols = Array.isArray(cols) ? cols.slice() : [];
42
+ if (this.isEmptyGrid()) {
43
+ this.anchor = null;
44
+ this.focus = null;
45
+ return;
46
+ }
47
+ if (this.focus) this.focus = this._clamp(this.focus);
48
+ if (this.anchor) this.anchor = this._clamp(this.anchor);
49
+ }
50
+
51
+ isEmptyGrid() {
52
+ return this.last < this.first || this.cols.length === 0;
53
+ }
54
+
55
+ _ord(col) {
56
+ const i = this.cols.indexOf(col);
57
+ return i >= 0 ? i : 0;
58
+ }
59
+
60
+ _clamp({ row, col }) {
61
+ const r = Math.max(this.first, Math.min(this.last, row));
62
+ const c = this.cols.includes(col) ? col
63
+ : this.cols[Math.max(0, Math.min(this.cols.length - 1, this._nearestOrd(col)))];
64
+ return { row: r, col: c };
65
+ }
66
+
67
+ /** A column that is not shown any more: the shown one nearest its index. */
68
+ _nearestOrd(col) {
69
+ let best = 0;
70
+ for (let i = 0; i < this.cols.length; i++) if (this.cols[i] <= col) best = i;
71
+ return best;
72
+ }
73
+
74
+ _inside(row, ord) {
75
+ return row >= this.first && row <= this.last && ord >= 0 && ord < this.cols.length;
76
+ }
77
+
78
+ /** Put the cursor on one cell and collapse the range onto it. */
79
+ setCursor(row, col) {
80
+ if (this.isEmptyGrid()) return false;
81
+ this.focus = this._clamp({ row, col });
82
+ this.anchor = { ...this.focus };
83
+ return true;
84
+ }
85
+
86
+ /** Move the cursor to a cell and keep the anchor: the range grows. */
87
+ extendTo(row, col) {
88
+ if (!this.anchor) return this.setCursor(row, col);
89
+ if (this.isEmptyGrid()) return false;
90
+ this.focus = this._clamp({ row, col });
91
+ return true;
92
+ }
93
+
94
+ _go(cell, extend) {
95
+ return extend ? this.extendTo(cell.row, cell.col) : this.setCursor(cell.row, cell.col);
96
+ }
97
+
98
+ /** One step: the arrows. At an edge the cursor stays put. */
99
+ move(dRow, dCol, { extend = false } = {}) {
100
+ if (!this.focus) return this.setCursor(this.first, this.cols[0]);
101
+ const ord = Math.max(0, Math.min(this.cols.length - 1, this._ord(this.focus.col) + dCol));
102
+ const row = Math.max(this.first, Math.min(this.last, this.focus.row + dRow));
103
+ return this._go({ row, col: this.cols[ord] }, extend);
104
+ }
105
+
106
+ /**
107
+ * Ctrl+arrow: to the edge of the block of filled cells, as a spreadsheet
108
+ * does. From a filled cell whose neighbour is filled, to the last filled
109
+ * cell of that run; otherwise to the next filled cell along the line, or
110
+ * the edge of the grid when there is none.
111
+ *
112
+ * @param {number} dRow -1, 0 or 1
113
+ * @param {number} dCol -1, 0 or 1
114
+ * @param {(row:number, col:number) => boolean} filled
115
+ */
116
+ jump(dRow, dCol, filled, { extend = false } = {}) {
117
+ if (!this.focus) return this.setCursor(this.first, this.cols[0]);
118
+ let r = this.focus.row;
119
+ let o = this._ord(this.focus.col);
120
+ const isFilled = (row, ord) => !!filled(row, this.cols[ord]);
121
+ if (this._inside(r + dRow, o + dCol)) {
122
+ if (isFilled(r, o) && isFilled(r + dRow, o + dCol)) {
123
+ while (this._inside(r + dRow, o + dCol) && isFilled(r + dRow, o + dCol)) {
124
+ r += dRow; o += dCol;
125
+ }
126
+ } else {
127
+ r += dRow; o += dCol;
128
+ while (!isFilled(r, o) && this._inside(r + dRow, o + dCol)) {
129
+ r += dRow; o += dCol;
130
+ }
131
+ }
132
+ }
133
+ return this._go({ row: r, col: this.cols[o] }, extend);
134
+ }
135
+
136
+ /** Home / End: the first or last column of the cursor's row; with
137
+ * `corner` (Ctrl), the first or last cell of the grid. */
138
+ lineEnd(toEnd, { extend = false, corner = false } = {}) {
139
+ if (!this.focus) return this.setCursor(this.first, this.cols[0]);
140
+ const col = toEnd ? this.cols[this.cols.length - 1] : this.cols[0];
141
+ const row = corner ? (toEnd ? this.last : this.first) : this.focus.row;
142
+ return this._go({ row, col }, extend);
143
+ }
144
+
145
+ /** Tab: one cell right, wrapping to the first cell of the next row (Shift:
146
+ * left, wrapping to the last cell of the row before). The last cell of
147
+ * the grid (the first, going back) stays put. */
148
+ tab(back = false) {
149
+ if (!this.focus) return this.setCursor(this.first, this.cols[0]);
150
+ let row = this.focus.row;
151
+ let ord = this._ord(this.focus.col) + (back ? -1 : 1);
152
+ if (ord >= this.cols.length) {
153
+ if (row >= this.last) return this.setCursor(row, this.focus.col);
154
+ row += 1; ord = 0;
155
+ } else if (ord < 0) {
156
+ if (row <= this.first) return this.setCursor(row, this.focus.col);
157
+ row -= 1; ord = this.cols.length - 1;
158
+ }
159
+ return this.setCursor(row, this.cols[ord]);
160
+ }
161
+
162
+ /** Whether Tab (Shift+Tab with `back`) has nowhere to go: the cursor is
163
+ * on the grid's last cell (its first, going back). The grid then lets
164
+ * the key move focus on, out of the table. */
165
+ tabEnds(back = false) {
166
+ if (!this.focus || this.isEmptyGrid()) return false;
167
+ const col = back ? this.cols[0] : this.cols[this.cols.length - 1];
168
+ const row = back ? this.first : this.last;
169
+ return this.focus.row === row && this.focus.col === col;
170
+ }
171
+
172
+ /** Every cell of the grid: from its first cell to its last, which is
173
+ * where the cursor goes. */
174
+ selectAll() {
175
+ if (this.isEmptyGrid()) return false;
176
+ this.anchor = { row: this.first, col: this.cols[0] };
177
+ this.focus = { row: this.last, col: this.cols[this.cols.length - 1] };
178
+ return true;
179
+ }
180
+
181
+ /** The range collapses onto the cursor. */
182
+ collapse() {
183
+ if (!this.focus) return false;
184
+ const was = this.anchor && (this.anchor.row !== this.focus.row || this.anchor.col !== this.focus.col);
185
+ this.anchor = { ...this.focus };
186
+ return !!was;
187
+ }
188
+
189
+ clear() {
190
+ this.anchor = null;
191
+ this.focus = null;
192
+ }
193
+
194
+ /** `{top, bottom, left, right, columns}` — rows as positions, `left` and
195
+ * `right` and `columns` as data indices — or null with nothing selected. */
196
+ range() {
197
+ if (!this.focus || !this.anchor) return null;
198
+ const a = this._ord(this.anchor.col);
199
+ const b = this._ord(this.focus.col);
200
+ const lo = Math.min(a, b);
201
+ const hi = Math.max(a, b);
202
+ return {
203
+ top: Math.min(this.anchor.row, this.focus.row),
204
+ bottom: Math.max(this.anchor.row, this.focus.row),
205
+ left: this.cols[lo],
206
+ right: this.cols[hi],
207
+ columns: this.cols.slice(lo, hi + 1),
208
+ };
209
+ }
210
+
211
+ contains(row, col) {
212
+ const r = this.range();
213
+ if (!r || row < r.top || row > r.bottom) return false;
214
+ return r.columns.includes(col);
215
+ }
216
+
217
+ isCursor(row, col) {
218
+ return !!this.focus && this.focus.row === row && this.focus.col === col;
219
+ }
220
+ }