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.
- package/CHANGELOG.md +112 -0
- package/LICENSE +0 -0
- package/README.md +2435 -42
- package/THIRD_PARTY_LICENSES.md +0 -0
- package/canvas.js +18 -0
- package/charts.js +21 -0
- package/core.js +14 -0
- package/css/base.css +1834 -75
- package/css/flexdesk.css +3165 -159
- package/css/overrides.css +16 -0
- package/css/reset.css +0 -0
- package/css/tokens.css +0 -0
- package/dist/canvas.js +6 -239
- package/dist/canvas.js.map +3 -3
- package/dist/charts.js +707 -18
- package/dist/charts.js.map +4 -4
- package/dist/chunk-3PHCPZHT.js +0 -0
- package/dist/chunk-3PHCPZHT.js.map +0 -0
- package/dist/chunk-4A7SJC3T.js +503 -0
- package/dist/chunk-4A7SJC3T.js.map +7 -0
- package/dist/{chunk-6JSOVNID.js → chunk-CVMCZZX2.js} +635 -10
- package/dist/chunk-CVMCZZX2.js.map +7 -0
- package/dist/chunk-D3COLZZ2.js +1311 -0
- package/dist/chunk-D3COLZZ2.js.map +7 -0
- package/dist/chunk-FOOS3T5L.js +0 -0
- package/dist/chunk-FOOS3T5L.js.map +0 -0
- package/dist/{chunk-SN4LSOAR.js → chunk-GOQLR5WR.js} +452 -2
- package/dist/chunk-GOQLR5WR.js.map +7 -0
- package/dist/{chunk-FL5KFNQH.js → chunk-HNU6MM2B.js} +52 -17
- package/dist/chunk-HNU6MM2B.js.map +7 -0
- package/dist/chunk-HYJYYEPK.js +375 -0
- package/dist/chunk-HYJYYEPK.js.map +7 -0
- package/dist/chunk-JKZLNKG6.js +2041 -0
- package/dist/chunk-JKZLNKG6.js.map +7 -0
- package/dist/chunk-JYWURG5T.js +0 -0
- package/dist/chunk-JYWURG5T.js.map +0 -0
- package/dist/{chunk-FBGP5KN5.js → chunk-LWV23YVE.js} +5073 -532
- package/dist/chunk-LWV23YVE.js.map +7 -0
- package/dist/chunk-N7HC3ADV.js +30 -0
- package/dist/chunk-N7HC3ADV.js.map +7 -0
- package/dist/{chunk-RQM5Z2MU.js → chunk-RMTRWWEO.js} +633 -311
- package/dist/chunk-RMTRWWEO.js.map +7 -0
- package/dist/{chunk-NLTOY57M.js → chunk-UURZ37SD.js} +28 -48
- package/dist/chunk-UURZ37SD.js.map +7 -0
- package/dist/{chunk-B5Q53LAJ.js → chunk-UWO43BZU.js} +2 -2
- package/dist/chunk-UWO43BZU.js.map +7 -0
- package/dist/chunk-WVFGV5FT.js +0 -0
- package/dist/chunk-WVFGV5FT.js.map +0 -0
- package/dist/core.js +170 -118
- package/dist/core.js.map +2 -2
- package/dist/editor.js +156 -70
- package/dist/editor.js.map +2 -2
- package/dist/flexdesk.css +3165 -159
- package/dist/flow.js +2789 -306
- package/dist/flow.js.map +4 -4
- package/dist/host.js +109 -3
- package/dist/host.js.map +3 -3
- package/dist/reset.css +0 -0
- package/dist/tile_registry-6WZPTUZV.js +0 -0
- package/dist/tile_registry-6WZPTUZV.js.map +0 -0
- package/dist/tiles.js +657 -234
- package/dist/tiles.js.map +4 -4
- package/dist/tokens.css +0 -0
- package/dist/widgets.js +3290 -2124
- package/dist/widgets.js.map +4 -4
- package/dist/wm.js +1227 -347
- package/dist/wm.js.map +4 -4
- package/editor.js +20 -0
- package/flow.js +21 -0
- package/host.js +14 -0
- package/package.json +12 -1
- package/src/canvas/canvas_adapter.js +5 -0
- package/src/canvas/edge_router.js +0 -0
- package/src/canvas/graph/connector_router.js +0 -0
- package/src/canvas/graph/history_service.js +0 -0
- package/src/canvas/graph/node_platform.js +0 -0
- package/src/canvas/index.js +1 -0
- package/src/canvas/layered_layout.js +244 -0
- package/src/charts/chart_types.js +0 -0
- package/src/charts/data_series_plot_window.js +0 -0
- package/src/charts/downsample.js +2 -2
- package/src/charts/plot_config.js +53 -29
- package/src/charts/plot_config_editor.js +627 -0
- package/src/charts/plot_grid.js +449 -0
- package/src/charts/plot_popout_window.js +103 -317
- package/src/charts/plotly_wrapper.js +22 -2
- package/src/charts/series_transforms.js +206 -0
- package/src/core/event_bus.js +0 -0
- package/src/core/logging.js +1 -1
- package/src/core/settings.js +105 -19
- package/src/core/state_guard.js +93 -51
- package/src/core/state_guard_config.js +85 -74
- package/src/core/state_machine.js +0 -0
- package/src/editor/editor_pane.js +3 -3
- package/src/editor/monaco_editor_factory.js +124 -60
- package/src/editor/monaco_loader.js +103 -28
- package/src/editor/notebook_search.js +0 -0
- package/src/editor/notebook_tab_bar.js +38 -52
- package/src/editor/notebook_undo_manager.js +0 -0
- package/src/editor/split_pane_container.js +6 -3
- package/src/flow/canvas_editor/connections.js +90 -0
- package/src/flow/canvas_editor/editor.js +2351 -153
- package/src/flow/canvas_editor/index.js +2 -1
- package/src/flow/canvas_editor/layout.js +119 -12
- package/src/flow/canvas_editor/strings.js +80 -0
- package/src/flow/kit/always_before.js +0 -0
- package/src/flow/kit/catalogue.js +97 -3
- package/src/flow/kit/chip_input.js +0 -0
- package/src/flow/kit/dom.js +0 -0
- package/src/flow/kit/findings.js +0 -0
- package/src/flow/kit/history.js +0 -0
- package/src/flow/kit/index.js +3 -3
- package/src/flow/kit/keys.js +7 -1
- package/src/flow/kit/logic_graph.js +97 -15
- package/src/flow/kit/references.js +0 -0
- package/src/flow/kit/settings_panel.js +0 -0
- package/src/flow/kit/settings_schema.js +2 -1
- package/src/flow/kit/step_picker.js +0 -0
- package/src/flow/kit/strings.js +3 -1
- package/src/flow/kit/value_picker.js +0 -0
- package/src/flow/kit/widgets.js +86 -109
- package/src/flow/lanes/editor.js +136 -14
- package/src/flow/lanes/index.js +3 -1
- package/src/flow/lanes/layout.js +0 -0
- package/src/flow/lanes/pipeline.js +44 -2
- package/src/flow/lanes/preview.js +0 -0
- package/src/flow/lanes/strings.js +1 -0
- package/src/flow/outline/editor.js +0 -0
- package/src/flow/outline/index.js +0 -0
- package/src/flow/outline/mapping.js +0 -0
- package/src/flow/outline/menu.js +0 -0
- package/src/flow/outline/operations.js +0 -0
- package/src/flow/outline/recognise.js +0 -0
- package/src/flow/outline/strings.js +0 -0
- package/src/help/help_modal.js +168 -42
- package/src/help/help_registry.js +12 -1
- package/src/host/external_links.js +0 -0
- package/src/host/file_picker.js +117 -0
- package/src/host/host.js +10 -2
- package/src/host/photino_host.js +9 -0
- package/src/host/photino_host.md +21 -3
- package/src/host/pywebview_host.js +10 -0
- package/src/tiles/chart_tiles.js +53 -245
- package/src/tiles/config_schema.js +8 -0
- package/src/tiles/figure_tile.js +322 -0
- package/src/tiles/layout_persistence.js +3 -3
- package/src/tiles/list_tiles.js +7 -7
- package/src/tiles/scope_picker.js +0 -0
- package/src/tiles/tile_base.js +1 -1
- package/src/tiles/tile_config_form.js +442 -0
- package/src/tiles/tile_grid.js +2 -0
- package/src/tiles/tile_registry.js +0 -0
- package/src/tiling/bottom_panel_tabs.js +67 -99
- package/src/tiling/command_palette.js +64 -19
- package/src/tiling/content_registry.js +4 -1
- package/src/tiling/desktops.js +11 -1
- package/src/tiling/entity_sources.js +0 -0
- package/src/tiling/keymap.js +403 -128
- package/src/tiling/kind_taxonomy.js +78 -0
- package/src/tiling/landing_table.js +0 -0
- package/src/tiling/loading_overlay.js +0 -0
- package/src/tiling/nav_panel.js +59 -10
- package/src/tiling/page_factory.js +5 -5
- package/src/tiling/panel_availability.js +49 -0
- package/src/tiling/panel_keys.js +0 -0
- package/src/tiling/shell.js +114 -12
- package/src/tiling/tab_strip.js +0 -0
- package/src/tiling/tile_breadcrumb.js +0 -0
- package/src/tiling/tile_renderer.js +105 -9
- package/src/tiling/tile_tab_menu.js +0 -0
- package/src/tiling/tile_tree.js +0 -0
- package/src/tiling/wm.js +562 -30
- package/src/tiling/zoom.js +0 -0
- package/src/ui/base/component_base.js +0 -0
- package/src/ui/base/controller_base.js +0 -0
- package/src/ui/base/page_base.js +0 -0
- package/src/ui/components/about_dialog.js +0 -0
- package/src/ui/components/action_dropdown.js +30 -2
- package/src/ui/components/alert_log.js +0 -0
- package/src/ui/components/alerts_pane.js +0 -0
- package/src/ui/components/attribute_list_editor.js +0 -0
- package/src/ui/components/autocomplete_field.js +0 -0
- package/src/ui/components/cell_editor_host.js +293 -0
- package/src/ui/components/cell_editors.js +480 -0
- package/src/ui/components/cell_grid.js +761 -0
- package/src/ui/components/cell_paste.js +243 -0
- package/src/ui/components/cell_selection.js +220 -0
- package/src/ui/components/choice_controls.js +351 -0
- package/src/ui/components/color_field.js +211 -0
- package/src/ui/components/color_list_field.js +210 -0
- package/src/ui/components/column_fit.js +0 -0
- package/src/ui/components/column_groups.js +314 -0
- package/src/ui/components/column_order.js +142 -0
- package/src/ui/components/computing_status_window.js +98 -25
- package/src/ui/components/confirm_dialog.js +81 -38
- package/src/ui/components/context_menu.js +2 -2
- package/src/ui/components/conversation_view.js +440 -0
- package/src/ui/components/crosstab.js +289 -0
- package/src/ui/components/data_table.js +696 -71
- package/src/ui/components/detail_header.js +22 -11
- package/src/ui/components/document_outline.js +397 -0
- package/src/ui/components/drag_reorder.js +0 -0
- package/src/ui/components/fact_table.js +0 -0
- package/src/ui/components/fill_handle.js +322 -0
- package/src/ui/components/gallery_picker.js +19 -8
- package/src/ui/components/header_actions.js +55 -0
- package/src/ui/components/inline_renamer.js +0 -0
- package/src/ui/components/job_progress.js +51 -0
- package/src/ui/components/managed_window.js +27 -4
- package/src/ui/components/menu_bar.js +745 -0
- package/src/ui/components/modal.js +449 -75
- package/src/ui/components/notification_history.js +3 -3
- package/src/ui/components/progress.js +483 -0
- package/src/ui/components/record_list.js +0 -0
- package/src/ui/components/row_activation.js +0 -0
- package/src/ui/components/section_stack.js +298 -0
- package/src/ui/components/series.js +173 -0
- package/src/ui/components/settings_view.js +634 -0
- package/src/ui/components/slide_out_panel.js +43 -6
- package/src/ui/components/slider_field.js +275 -18
- package/src/ui/components/sortable_list.js +0 -0
- package/src/ui/components/staged_edits.js +361 -0
- package/src/ui/components/staged_history.js +172 -0
- package/src/ui/components/table_filter_chips.js +0 -0
- package/src/ui/components/table_state_store.js +0 -0
- package/src/ui/components/table_structure.js +182 -0
- package/src/ui/components/table_values.js +0 -0
- package/src/ui/components/tabs.js +497 -0
- package/src/ui/components/tag_input.js +215 -0
- package/src/ui/components/toast.js +25 -8
- package/src/ui/components/toggle_field.js +175 -0
- package/src/ui/components/tree_view.js +55 -34
- package/src/ui/components/tsv.js +74 -0
- package/src/ui/components/window_taskbar.js +0 -0
- package/src/ui/components/wizard.js +563 -0
- package/src/ui/controllers/window_chrome_controller.js +2 -2
- package/src/ui/notification_center.js +375 -119
- package/src/ui/utils/auto_scrollbars.js +0 -0
- package/src/ui/utils/markdown.js +709 -0
- package/src/ui/utils/native_context_menu.js +0 -0
- package/src/ui/utils/overlay_scrollbar.js +16 -12
- package/src/ui/utils/raf_resize_observer.js +0 -0
- package/src/ui/utils/scroll_lock.js +0 -0
- package/src/ui/utils/strip_scroll.js +55 -0
- package/src/ui/utils/tooltip_service.js +0 -8
- package/tiles.js +27 -0
- package/widgets.js +69 -0
- package/wm.js +28 -0
- package/dist/chunk-6JSOVNID.js.map +0 -7
- package/dist/chunk-B5Q53LAJ.js.map +0 -7
- package/dist/chunk-FBGP5KN5.js.map +0 -7
- package/dist/chunk-FL5KFNQH.js.map +0 -7
- package/dist/chunk-NLTOY57M.js.map +0 -7
- package/dist/chunk-NNFTETK3.js +0 -633
- package/dist/chunk-NNFTETK3.js.map +0 -7
- package/dist/chunk-O5OHMWBB.js +0 -284
- package/dist/chunk-O5OHMWBB.js.map +0 -7
- package/dist/chunk-RQM5Z2MU.js.map +0 -7
- package/dist/chunk-SN4LSOAR.js.map +0 -7
- package/dist/chunk-W6FBJLPW.js +0 -367
- package/dist/chunk-W6FBJLPW.js.map +0 -7
- package/src/ui/controllers/panel_state_machine.js +0 -634
|
@@ -0,0 +1,761 @@
|
|
|
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 { CELL_EDITORS, editorTypeOf } from './cell_editors.js';
|
|
17
|
+
import { StagedEdits } from './staged_edits.js';
|
|
18
|
+
import { FillHandle } from './fill_handle.js';
|
|
19
|
+
import { CellPaste } from './cell_paste.js';
|
|
20
|
+
import { ISO_DATE, formatDate } from './table_values.js';
|
|
21
|
+
|
|
22
|
+
/** Is this a cell somebody filled? What Ctrl+arrow stops at. */
|
|
23
|
+
export function isFilledValue(value) {
|
|
24
|
+
if (value == null) return false;
|
|
25
|
+
if (typeof value === 'string') return value !== '';
|
|
26
|
+
if (typeof value === 'number') return !Number.isNaN(value);
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The keys a printable character is: one character, no Ctrl, Cmd or Alt. */
|
|
31
|
+
function isPrintable(ev) {
|
|
32
|
+
return typeof ev.key === 'string' && [...ev.key].length === 1
|
|
33
|
+
&& !ev.ctrlKey && !ev.metaKey && !ev.altKey;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The arrows, as steps. */
|
|
37
|
+
const ARROWS = {
|
|
38
|
+
ArrowUp: [-1, 0], ArrowDown: [1, 0], ArrowLeft: [0, -1], ArrowRight: [0, 1],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* STORY-0085. A cell's raw value as the text a copy carries — never the text
|
|
43
|
+
* it is drawn as (no `formatValue`, typed-column format, rounding or
|
|
44
|
+
* `nullDisplay`): `null`/`undefined` (and an invalid Date) are empty, a
|
|
45
|
+
* number is `String(n)` (1234.5, not "1,234.50"). Dates are ISO 8601: a
|
|
46
|
+
* `Date` is `toISOString()`, except in a column declared `'date'`, where it is
|
|
47
|
+
* the calendar day `YYYY-MM-DD` in the table's zone (`dateTimeZone`); a number
|
|
48
|
+
* in a column declared `'date'` or `'datetime'` is read as epoch ms the same
|
|
49
|
+
* way. Text is copied as it is.
|
|
50
|
+
*/
|
|
51
|
+
function copyText(value, { kind, utc }) {
|
|
52
|
+
if (value === null || value === undefined) return '';
|
|
53
|
+
const dated = kind === 'date' || kind === 'datetime';
|
|
54
|
+
if (value instanceof Date || (dated && typeof value === 'number')) {
|
|
55
|
+
const t = value instanceof Date ? value.getTime() : value;
|
|
56
|
+
if (!Number.isFinite(t)) return '';
|
|
57
|
+
return kind === 'date' ? formatDate(t, ISO_DATE, { utc }) : new Date(t).toISOString();
|
|
58
|
+
}
|
|
59
|
+
return String(value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* DataTable's cell mode. Built by the table when `selectionMode` is 'cells';
|
|
64
|
+
* it reads the table, never the other way round, except through the few hooks
|
|
65
|
+
* the table calls (`decorateRow`, `install`, `rowLeaving`, `isEditing`).
|
|
66
|
+
*/
|
|
67
|
+
export class CellGrid {
|
|
68
|
+
/** @param {import('./data_table.js').DataTable} table */
|
|
69
|
+
constructor(table) {
|
|
70
|
+
this.table = table;
|
|
71
|
+
this.sel = new CellSelection();
|
|
72
|
+
/** `rowIdx:colIdx` → value typed, while its commit is out. */
|
|
73
|
+
this.pending = new Map();
|
|
74
|
+
/** `rowIdx:colIdx` → {value, message}: a commit that was refused. */
|
|
75
|
+
this.errors = new Map();
|
|
76
|
+
/** A `render()` asked for while an editor was open. */
|
|
77
|
+
this.renderWaiting = false;
|
|
78
|
+
/** `rowIdx`s whose redraw waits for the editor open in the row to
|
|
79
|
+
* close: a settled commit's, or the consumer's `updateRow`. The row
|
|
80
|
+
* is read when it is drawn, so the newest data is what shows. */
|
|
81
|
+
this.rowsWaiting = new Set();
|
|
82
|
+
/** `rowIdx:colIdx` → the token of the cell's newest commit, so an
|
|
83
|
+
* older commit settling late does not take the newer one's marks. */
|
|
84
|
+
this.latest = new Map();
|
|
85
|
+
/** Bumped by every new set of rows, so a commit that outlived its rows
|
|
86
|
+
* marks nothing in the new ones. */
|
|
87
|
+
this.generation = 0;
|
|
88
|
+
this.editor = new CellEditorHost({
|
|
89
|
+
onCommit: (s, value, move, reason) => this._committed(s, value, move, reason),
|
|
90
|
+
onCancel: (s, reason) => this._closed(s, reason),
|
|
91
|
+
onRefuse: (s, error, text, reason) => this._refused(s, error, text, reason),
|
|
92
|
+
});
|
|
93
|
+
/** STORY-0088 `staged`: commits are staged here instead of sent. */
|
|
94
|
+
this.staged = null;
|
|
95
|
+
if (this._cfg().staged) {
|
|
96
|
+
if (this._cfg().editable) this.staged = new StagedEdits(this);
|
|
97
|
+
else console.warn('[DataTable] `staged` needs `editable`; ignored.');
|
|
98
|
+
}
|
|
99
|
+
/** STORY-0089 `fillHandle`: the handle on the range's corner. */
|
|
100
|
+
this.fill = null;
|
|
101
|
+
if (this._cfg().fillHandle) {
|
|
102
|
+
if (this.staged) this.fill = new FillHandle(this);
|
|
103
|
+
else console.warn('[DataTable] `fillHandle` needs `staged`; ignored.');
|
|
104
|
+
}
|
|
105
|
+
/** STORY-0090 `pasteable`: a tab-separated block pasted at the cursor. */
|
|
106
|
+
this.paste = null;
|
|
107
|
+
if (this._cfg().pasteable) {
|
|
108
|
+
if (this.staged) this.paste = new CellPaste(this);
|
|
109
|
+
else console.warn('[DataTable] `pasteable` needs `staged`; ignored.');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── what the table is, read through its own helpers ──────────────────
|
|
114
|
+
|
|
115
|
+
_cfg() { return this.table.config; }
|
|
116
|
+
|
|
117
|
+
/** The data columns shown, in the order shown. */
|
|
118
|
+
columns() {
|
|
119
|
+
return this.table._shownColumns();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The positions of the rows on the page drawn, `[first, last]`. */
|
|
123
|
+
span() {
|
|
124
|
+
const s = this.table._pageSpan || { start: 0, count: 0 };
|
|
125
|
+
return [s.start, s.start + s.count - 1];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
_sync() {
|
|
129
|
+
const [first, last] = this.span();
|
|
130
|
+
this.sel.setBounds(first, last, this.columns());
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** A position's original row index (the `__rowIndex` space). */
|
|
134
|
+
rowIndexAt(pos) {
|
|
135
|
+
const t = this.table;
|
|
136
|
+
if (t._isServerSide()) return (t.config.dataSource ? 0 : (t.config.offset || 0)) + pos;
|
|
137
|
+
t._getProcessedRows();
|
|
138
|
+
return t._processedIndexMap?.[pos] ?? pos;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
rowAt(pos) {
|
|
142
|
+
return this.table._getProcessedRows()[pos] ?? null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The row the table holds NOW for an original row index — the window's
|
|
147
|
+
* row list, else `config.rows` at the place the table draws it from —
|
|
148
|
+
* the one `updateRow` writes. Never a row captured earlier.
|
|
149
|
+
*/
|
|
150
|
+
currentRow(rowIdx) {
|
|
151
|
+
const t = this.table;
|
|
152
|
+
const local = t._virtLocalOf(rowIdx);
|
|
153
|
+
if (local >= 0) return t._virt.rows[local] ?? null;
|
|
154
|
+
const rows = t.config.rows;
|
|
155
|
+
if (!Array.isArray(rows)) return null;
|
|
156
|
+
const at = t._isServerSide() ? rowIdx - (t.config.dataSource ? 0 : (t.config.offset || 0)) : rowIdx;
|
|
157
|
+
return rows[at] ?? null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** What a cell shows: a refused or pending commit's value, else the row's. */
|
|
161
|
+
shownValue(rowIdx, col) {
|
|
162
|
+
const key = `${rowIdx}:${col}`;
|
|
163
|
+
const err = this.errors.get(key);
|
|
164
|
+
if (err) return err.value;
|
|
165
|
+
if (this.pending.has(key)) return this.pending.get(key);
|
|
166
|
+
if (this.staged?.has(key)) return this.staged.value(key);
|
|
167
|
+
const row = this.currentRow(rowIdx);
|
|
168
|
+
return row ? row[col] : undefined;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Whether the open editor sits in this row element. */
|
|
172
|
+
_holds(tr) {
|
|
173
|
+
const s = this.editor.session;
|
|
174
|
+
return !!s && !!tr && tr.contains(s.td);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* `updateRow` on a drawn row: the table has stored the data already. When
|
|
179
|
+
* the row holds the open editor the rebuild — which would take the editor
|
|
180
|
+
* out of the document — waits for that editor to close, the editor's cell
|
|
181
|
+
* left as it is. Returns whether it waits.
|
|
182
|
+
*/
|
|
183
|
+
holdRow(tr, rowIdx) {
|
|
184
|
+
if (!this._holds(tr)) return false;
|
|
185
|
+
this.rowsWaiting.add(rowIdx);
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
valueAt(pos, col) {
|
|
190
|
+
const row = this.rowAt(pos);
|
|
191
|
+
return row ? row[col] : undefined;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** The `<td>` drawing a cell, or null when it is not drawn. */
|
|
195
|
+
cellEl(pos, col) {
|
|
196
|
+
const tr = this.table.getRowElement(this.rowIndexAt(pos));
|
|
197
|
+
if (!tr) return null;
|
|
198
|
+
for (const td of tr.children) if (td.__colIdx === col) return td;
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── the public face (DataTable delegates) ────────────────────────────
|
|
203
|
+
|
|
204
|
+
/** `{anchor, focus, range}`; each cell is `{row, col, rowIdx}`, and the
|
|
205
|
+
* range also lists its `rows` (original indices) and `columns`. */
|
|
206
|
+
get() {
|
|
207
|
+
this._sync();
|
|
208
|
+
const cell = (c) => (c ? { row: c.row, col: c.col, rowIdx: this.rowIndexAt(c.row) } : null);
|
|
209
|
+
const r = this.sel.range();
|
|
210
|
+
let range = null;
|
|
211
|
+
if (r) {
|
|
212
|
+
const rows = [];
|
|
213
|
+
for (let p = r.top; p <= r.bottom; p++) rows.push(this.rowIndexAt(p));
|
|
214
|
+
range = { ...r, rows };
|
|
215
|
+
}
|
|
216
|
+
return { anchor: cell(this.sel.anchor), focus: cell(this.sel.focus), range };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Rows (original indices) the range touches, ascending. */
|
|
220
|
+
rowIndices() {
|
|
221
|
+
const r = this.sel.range();
|
|
222
|
+
if (!r) return [];
|
|
223
|
+
const out = [];
|
|
224
|
+
for (let p = r.top; p <= r.bottom; p++) out.push(this.rowIndexAt(p));
|
|
225
|
+
return out.sort((a, b) => a - b);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Select from `anchor` to `focus` (`{row, col}`, positions and data
|
|
229
|
+
* indices), or put the cursor on `focus` alone. */
|
|
230
|
+
set(focus, anchor = null) {
|
|
231
|
+
this._sync();
|
|
232
|
+
if (!focus) { this.sel.clear(); this._changed(); return; }
|
|
233
|
+
this.sel.setCursor(anchor ? anchor.row : focus.row, anchor ? anchor.col : focus.col);
|
|
234
|
+
if (anchor) this.sel.extendTo(focus.row, focus.col);
|
|
235
|
+
this._changed({ reveal: true });
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** `setSelection(indices)` in cell mode: whole rows, the first to the last
|
|
239
|
+
* of them as shown. */
|
|
240
|
+
selectRows(indices) {
|
|
241
|
+
this._sync();
|
|
242
|
+
const t = this.table;
|
|
243
|
+
t._getProcessedRows();
|
|
244
|
+
const map = t._processedIndexMap || [];
|
|
245
|
+
const positions = indices.map((i) => (t._isServerSide()
|
|
246
|
+
? i - (t.config.dataSource ? 0 : (t.config.offset || 0)) : map.indexOf(i)))
|
|
247
|
+
.filter((p) => p >= 0);
|
|
248
|
+
const cols = this.sel.cols;
|
|
249
|
+
if (!positions.length || !cols.length) { this.sel.clear(); this._changed(); return; }
|
|
250
|
+
this.sel.setCursor(Math.min(...positions), cols[0]);
|
|
251
|
+
this.sel.extendTo(Math.max(...positions), cols[cols.length - 1]);
|
|
252
|
+
this._changed();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
clear() {
|
|
256
|
+
this.sel.clear();
|
|
257
|
+
this._changed();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* STORY-0085. What a copy of the selection puts on the clipboard: the
|
|
262
|
+
* range's cells as text, one array per row in the order SHOWN (after the
|
|
263
|
+
* sort and the filters), its shown columns left to right (a hidden column
|
|
264
|
+
* inside it is not one of them). With no range beyond the cursor, the
|
|
265
|
+
* cursor cell alone. A cell is its RAW value as text (`copyText`), not
|
|
266
|
+
* the text it is drawn as: the value the cell holds now, a staged,
|
|
267
|
+
* pending or refused one included. Null with nothing selected.
|
|
268
|
+
*/
|
|
269
|
+
copyMatrix() {
|
|
270
|
+
this._sync();
|
|
271
|
+
const r = this.sel.range();
|
|
272
|
+
if (!r) return null;
|
|
273
|
+
const t = this.table;
|
|
274
|
+
const utc = t._utc();
|
|
275
|
+
const out = [];
|
|
276
|
+
for (let p = r.top; p <= r.bottom; p++) {
|
|
277
|
+
const rowIdx = this.rowIndexAt(p);
|
|
278
|
+
out.push(r.columns.map((col) =>
|
|
279
|
+
copyText(this.shownValue(rowIdx, col), { kind: t._valueKind(col), utc })));
|
|
280
|
+
}
|
|
281
|
+
return out;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** New rows (`setData({rows})`): the marks were about the old ones. */
|
|
285
|
+
newRows() {
|
|
286
|
+
this.generation++;
|
|
287
|
+
this.pending.clear();
|
|
288
|
+
this.errors.clear();
|
|
289
|
+
this.latest.clear();
|
|
290
|
+
this.rowsWaiting.clear();
|
|
291
|
+
this.staged?.clear();
|
|
292
|
+
this.fill?.clearMarks();
|
|
293
|
+
this.paste?.clearMarks();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
isEditing() {
|
|
297
|
+
return !!this.editor.session;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ── painting ─────────────────────────────────────────────────────────
|
|
301
|
+
|
|
302
|
+
/** Called for every row drawn — on render, on a scroll, on `updateRow` —
|
|
303
|
+
* so a row that arrives later is painted like the rest. */
|
|
304
|
+
decorateRow(tr) {
|
|
305
|
+
const pos = tr.__cellPos;
|
|
306
|
+
if (pos === undefined) return;
|
|
307
|
+
for (const td of tr.children) {
|
|
308
|
+
if (td.__colIdx !== undefined) this._decorateCell(td, pos, tr.__rowIndex, td.__colIdx);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** One cell: the selection, its commit's pending or refused mark, and
|
|
313
|
+
* (STORY-0088) its staged value and a refused batch's mark. */
|
|
314
|
+
_decorateCell(td, pos, rowIdx, col) {
|
|
315
|
+
this._paintCell(td, pos, col);
|
|
316
|
+
const key = `${rowIdx}:${col}`;
|
|
317
|
+
const err = this.errors.get(key);
|
|
318
|
+
const waiting = this.pending.has(key);
|
|
319
|
+
const staged = !!this.staged?.has(key);
|
|
320
|
+
if (err || waiting) {
|
|
321
|
+
td.textContent = this.table._formatValue(err ? err.value : this.pending.get(key), col);
|
|
322
|
+
} else if (staged) {
|
|
323
|
+
td.textContent = this.table._formatValue(this.staged.value(key), col);
|
|
324
|
+
}
|
|
325
|
+
const message = err ? err.message : this.staged?.errors.get(key);
|
|
326
|
+
td.classList.toggle('twm-dt-cell--pending', waiting);
|
|
327
|
+
td.classList.toggle('twm-dt-cell--staged', staged);
|
|
328
|
+
td.classList.toggle('twm-dt-cell--error', message != null);
|
|
329
|
+
if (message != null) {
|
|
330
|
+
td.title = message;
|
|
331
|
+
td.setAttribute('data-error', message);
|
|
332
|
+
} else td.removeAttribute('data-error');
|
|
333
|
+
this.fill?.paintCell(td, pos, col); // STORY-0089, after the text
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
_paintCell(td, pos, col) {
|
|
337
|
+
td.classList.toggle('twm-dt-cell--selected', this.sel.contains(pos, col));
|
|
338
|
+
const cursor = this.sel.isCursor(pos, col);
|
|
339
|
+
td.classList.toggle('twm-dt-cell--cursor', cursor);
|
|
340
|
+
if (cursor) td.setAttribute('aria-selected', 'true');
|
|
341
|
+
else td.removeAttribute('aria-selected');
|
|
342
|
+
this.fill?.paintCell(td, pos, col); // STORY-0089
|
|
343
|
+
this.paste?.paintCell(td, col); // STORY-0090
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
paint() {
|
|
347
|
+
const tbody = this.table._tbodyEl;
|
|
348
|
+
if (!tbody) return;
|
|
349
|
+
for (const tr of tbody.children) {
|
|
350
|
+
const pos = tr.__cellPos;
|
|
351
|
+
if (pos === undefined) continue;
|
|
352
|
+
for (const td of tr.children) {
|
|
353
|
+
if (td.__colIdx !== undefined) this._paintCell(td, pos, td.__colIdx);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** The selection moved: the cells, the rows `getSelection()` answers, and
|
|
359
|
+
* whoever listens. */
|
|
360
|
+
_changed({ reveal = false } = {}) {
|
|
361
|
+
const t = this.table;
|
|
362
|
+
this.fill?.clearMarks(); // STORY-0089: a skipped mark lasts until the selection moves
|
|
363
|
+
this.paste?.clearMarks(); // STORY-0090: and so does a paste's
|
|
364
|
+
if (reveal && this.sel.focus) t.scrollToRow(this.rowIndexAt(this.sel.focus.row));
|
|
365
|
+
this.paint();
|
|
366
|
+
const before = t.getSelection().join();
|
|
367
|
+
const rows = this.rowIndices();
|
|
368
|
+
t._state.selected.clear();
|
|
369
|
+
for (const i of rows) t._state.selected.add(i);
|
|
370
|
+
t._state.anchorIndex = this.sel.anchor ? this.rowIndexAt(this.sel.anchor.row) : null;
|
|
371
|
+
if (rows.join() !== before) t._notifySelectionChange();
|
|
372
|
+
try { t.config.onCellSelectionChange?.(this.get()); } catch (err) {
|
|
373
|
+
console.error('[DataTable] onCellSelectionChange threw', err);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ── the pointer and the keys ─────────────────────────────────────────
|
|
378
|
+
|
|
379
|
+
/** Bound by `_installInteractions` on every render, BEFORE the table's own
|
|
380
|
+
* activation, detail and copy listeners, so a key the cell mode takes
|
|
381
|
+
* goes no further. Listeners go on the table's disposers. */
|
|
382
|
+
install(tbody, table) {
|
|
383
|
+
const t = this.table;
|
|
384
|
+
const on = (el, type, fn) => {
|
|
385
|
+
el.addEventListener(type, fn);
|
|
386
|
+
t._disposers.push(() => el.removeEventListener(type, fn));
|
|
387
|
+
};
|
|
388
|
+
this._sync();
|
|
389
|
+
const cellOf = (target) => {
|
|
390
|
+
const td = target?.closest?.('td');
|
|
391
|
+
if (!td || td.__colIdx === undefined || !tbody.contains(td)) return null;
|
|
392
|
+
const tr = td.parentNode;
|
|
393
|
+
if (tr?.__cellPos === undefined) return null;
|
|
394
|
+
return { td, row: tr.__cellPos, col: td.__colIdx };
|
|
395
|
+
};
|
|
396
|
+
const inEditor = (target) => !!this.editor.session && this.editor.session.td.contains(target);
|
|
397
|
+
|
|
398
|
+
on(tbody, 'click', (ev) => {
|
|
399
|
+
if (ev.button != null && ev.button !== 0) return;
|
|
400
|
+
if (inEditor(ev.target)) return;
|
|
401
|
+
const hit = cellOf(ev.target);
|
|
402
|
+
if (!hit) return;
|
|
403
|
+
this._sync();
|
|
404
|
+
if (ev.shiftKey && this.sel.anchor) this.sel.extendTo(hit.row, hit.col);
|
|
405
|
+
else this.sel.setCursor(hit.row, hit.col);
|
|
406
|
+
this._changed();
|
|
407
|
+
try { table.focus({ preventScroll: true }); } catch (_) { /* detached */ }
|
|
408
|
+
});
|
|
409
|
+
on(tbody, 'dblclick', (ev) => {
|
|
410
|
+
if (inEditor(ev.target)) return;
|
|
411
|
+
const hit = cellOf(ev.target);
|
|
412
|
+
if (!hit || !this.canEdit(hit.row, hit.col)) return;
|
|
413
|
+
this._sync();
|
|
414
|
+
this.sel.setCursor(hit.row, hit.col);
|
|
415
|
+
this._changed();
|
|
416
|
+
if (this.open(hit.row, hit.col)) ev.stopImmediatePropagation();
|
|
417
|
+
});
|
|
418
|
+
// A right-click outside the range moves the cursor there first, so the
|
|
419
|
+
// table's own menu (bound after this) finds its row already selected.
|
|
420
|
+
on(tbody, 'contextmenu', (ev) => {
|
|
421
|
+
if (inEditor(ev.target)) return;
|
|
422
|
+
const hit = cellOf(ev.target);
|
|
423
|
+
if (!hit || this.sel.contains(hit.row, hit.col)) return;
|
|
424
|
+
this._sync();
|
|
425
|
+
this.sel.setCursor(hit.row, hit.col);
|
|
426
|
+
this._changed();
|
|
427
|
+
});
|
|
428
|
+
on(table, 'keydown', (ev) => {
|
|
429
|
+
if (ev.isComposing || inEditor(ev.target)) return;
|
|
430
|
+
if (ev.target !== table && ev.target?.closest?.('input, select, textarea, button, [contenteditable="true"]')) return;
|
|
431
|
+
if (this._onKey(ev)) {
|
|
432
|
+
ev.preventDefault();
|
|
433
|
+
ev.stopImmediatePropagation();
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
// STORY-0090. Ctrl+V on the focused table: the browser's paste event
|
|
437
|
+
// carries the clipboard's text, with no permission to ask for. In an
|
|
438
|
+
// editor (or any field) the paste is the field's.
|
|
439
|
+
if (this.paste) {
|
|
440
|
+
on(table, 'paste', (ev) => {
|
|
441
|
+
if (inEditor(ev.target)) return;
|
|
442
|
+
if (ev.target !== table && ev.target?.closest?.('input, select, textarea, [contenteditable="true"]')) return;
|
|
443
|
+
const text = ev.clipboardData?.getData?.('text/plain');
|
|
444
|
+
if (typeof text !== 'string') return;
|
|
445
|
+
ev.preventDefault();
|
|
446
|
+
this.paste.paste(text);
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
// An editor that was open across a render of its row (an `updateRow`)
|
|
450
|
+
// is still the editor; the paint follows the model.
|
|
451
|
+
this.paint();
|
|
452
|
+
// STORY-0088. The new controls are held while cells are staged.
|
|
453
|
+
this.staged?.paintFreeze();
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** One key on the focused table. Returns whether the cell mode took it. */
|
|
457
|
+
_onKey(ev) {
|
|
458
|
+
this._sync();
|
|
459
|
+
const ctrl = ev.ctrlKey || ev.metaKey;
|
|
460
|
+
const k = ev.key;
|
|
461
|
+
// STORY-0088. Ctrl+Z takes a staged edit back; Ctrl+Y (Ctrl+Shift+Z)
|
|
462
|
+
// puts it back. Taken even with nothing to undo: the browser's own
|
|
463
|
+
// undo has nothing to do on a table either.
|
|
464
|
+
if (this.staged && ctrl && !ev.altKey) {
|
|
465
|
+
const lower = String(k).toLowerCase();
|
|
466
|
+
if (lower === 'z' && !ev.shiftKey) { this.staged.undo(); return true; }
|
|
467
|
+
if (lower === 'y' || (lower === 'z' && ev.shiftKey)) { this.staged.redo(); return true; }
|
|
468
|
+
}
|
|
469
|
+
if (ARROWS[k] && !ev.altKey) {
|
|
470
|
+
const [dr, dc] = ARROWS[k];
|
|
471
|
+
if (ctrl) {
|
|
472
|
+
this.sel.jump(dr, dc, (r, c) => isFilledValue(this.valueAt(r, c)), { extend: ev.shiftKey });
|
|
473
|
+
} else this.sel.move(dr, dc, { extend: ev.shiftKey });
|
|
474
|
+
this._changed({ reveal: true });
|
|
475
|
+
return true;
|
|
476
|
+
}
|
|
477
|
+
if ((k === 'Home' || k === 'End') && !ev.altKey) {
|
|
478
|
+
this.sel.lineEnd(k === 'End', { extend: ev.shiftKey, corner: ctrl });
|
|
479
|
+
this._changed({ reveal: true });
|
|
480
|
+
return true;
|
|
481
|
+
}
|
|
482
|
+
if (k === 'Tab' && !ctrl && !ev.altKey) {
|
|
483
|
+
// On the last cell (the first, going back) Tab is not taken:
|
|
484
|
+
// focus moves on out of the table, as Tab says.
|
|
485
|
+
if (this.sel.tabEnds(ev.shiftKey)) return false;
|
|
486
|
+
this.sel.tab(ev.shiftKey);
|
|
487
|
+
this._changed({ reveal: true });
|
|
488
|
+
return true;
|
|
489
|
+
}
|
|
490
|
+
if (ctrl && !ev.altKey && String(k).toLowerCase() === 'a') {
|
|
491
|
+
this.sel.selectAll();
|
|
492
|
+
this._changed();
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
if (k === 'Escape') {
|
|
496
|
+
this.sel.collapse();
|
|
497
|
+
this._changed();
|
|
498
|
+
this.table._hideContextMenu();
|
|
499
|
+
return true;
|
|
500
|
+
}
|
|
501
|
+
const f = this.sel.focus;
|
|
502
|
+
if (!f) return false;
|
|
503
|
+
if ((k === 'F2' || (k === 'Enter' && !ctrl && !ev.altKey && !ev.shiftKey)) && this.canEdit(f.row, f.col)) {
|
|
504
|
+
return this.open(f.row, f.col);
|
|
505
|
+
}
|
|
506
|
+
if (isPrintable(ev) && this.canEdit(f.row, f.col)) {
|
|
507
|
+
return this.open(f.row, f.col, { initialText: ev.key });
|
|
508
|
+
}
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// ── editing ──────────────────────────────────────────────────────────
|
|
513
|
+
|
|
514
|
+
/** Whether a cell opens an editor: `editable` says so for its column. */
|
|
515
|
+
canEdit(pos, col) {
|
|
516
|
+
const spec = this._cfg().editable;
|
|
517
|
+
if (!spec) return false;
|
|
518
|
+
// STORY-0088. While a staged batch is out, nothing new is typed into it.
|
|
519
|
+
if (this.staged?.committing) return false;
|
|
520
|
+
if (spec === true) return true;
|
|
521
|
+
if (Array.isArray(spec)) return spec.includes(col);
|
|
522
|
+
if (typeof spec === 'function') {
|
|
523
|
+
try { return !!spec(col, this.rowAt(pos), this.rowIndexAt(pos)); } catch (err) {
|
|
524
|
+
console.error('[DataTable] editable threw', err);
|
|
525
|
+
return false;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** STORY-0087. The column's `editTypes` entry, as `{type, ...}`, or null. */
|
|
532
|
+
_editSpec(col) {
|
|
533
|
+
const all = this._cfg().editTypes;
|
|
534
|
+
let spec = all && typeof all === 'object' ? all[col] : null;
|
|
535
|
+
if (typeof spec === 'string') spec = { type: spec };
|
|
536
|
+
return spec && typeof spec === 'object' ? spec : null;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** The column's type as it is named: `editTypes`', else the declared one
|
|
540
|
+
* (`columnTypes`), else the detected one. */
|
|
541
|
+
_rawType(col) {
|
|
542
|
+
const spec = this._editSpec(col);
|
|
543
|
+
if (spec?.type) return String(spec.type);
|
|
544
|
+
const declared = this._cfg().columnTypes?.[col];
|
|
545
|
+
return declared || this.table._columnTypes?.[col] || 'text';
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/** The column's type as `editors` and the built-in editors know it:
|
|
549
|
+
* 'num' is 'number', 'date' is 'datetime'. */
|
|
550
|
+
_typeOf(col) {
|
|
551
|
+
return editorTypeOf(this._rawType(col));
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** By column index, then by type (as named, then as the built-ins name
|
|
555
|
+
* it), then the built-in editor for the type, else the text box. */
|
|
556
|
+
_factoryFor(col) {
|
|
557
|
+
const editors = this._cfg().editors;
|
|
558
|
+
if (editors && typeof editors === 'object') {
|
|
559
|
+
for (const k of [col, this._rawType(col), this._typeOf(col)]) {
|
|
560
|
+
if (typeof editors[k] === 'function') return editors[k];
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
const builtIn = Object.prototype.hasOwnProperty.call(CELL_EDITORS, this._typeOf(col))
|
|
564
|
+
? CELL_EDITORS[this._typeOf(col)] : null;
|
|
565
|
+
return builtIn ? builtIn.create : createTextEditor;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Open the editor on a cell (position, data column). Brings the row into
|
|
570
|
+
* view first. Returns whether an editor opened.
|
|
571
|
+
*/
|
|
572
|
+
open(pos, col, { initialText = null } = {}) {
|
|
573
|
+
if (this.editor.session || !this.canEdit(pos, col)) return false;
|
|
574
|
+
this.table.scrollToRow(this.rowIndexAt(pos));
|
|
575
|
+
const td = this.cellEl(pos, col);
|
|
576
|
+
if (!td) return false;
|
|
577
|
+
const rowIdx = this.rowIndexAt(pos);
|
|
578
|
+
const row = this.currentRow(rowIdx);
|
|
579
|
+
const headers = this._cfg().headers || [];
|
|
580
|
+
// The editor holds what the cell SHOWS — a refused or pending value,
|
|
581
|
+
// not the row's — so it is what the person sees that they edit.
|
|
582
|
+
return this.editor.open({
|
|
583
|
+
td,
|
|
584
|
+
factory: this._factoryFor(col),
|
|
585
|
+
pos, colIdx: col, rowIdx, row,
|
|
586
|
+
generation: this.generation,
|
|
587
|
+
value: this.shownValue(rowIdx, col),
|
|
588
|
+
column: { ...this._editSpec(col), index: col, header: headers[col], type: this._typeOf(col) },
|
|
589
|
+
args: {
|
|
590
|
+
locale: this._cfg().locale ?? undefined,
|
|
591
|
+
utc: this.table._utc(),
|
|
592
|
+
messages: this._cfg().editorMessages ?? undefined,
|
|
593
|
+
},
|
|
594
|
+
initialText,
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/** The virtual window is taking a row out: an editor in it commits, as a
|
|
599
|
+
* click elsewhere would — what was typed is not thrown away. An editor
|
|
600
|
+
* with no `getValue` has nothing to commit, and is cancelled. */
|
|
601
|
+
rowLeaving(tr) {
|
|
602
|
+
const s = this.editor.session;
|
|
603
|
+
if (s && tr.contains(s.td)) this.editor.commit(null, 'scroll');
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
dispose() {
|
|
607
|
+
this.fill?.dispose();
|
|
608
|
+
this.paste?.dispose();
|
|
609
|
+
this.editor.cancel('dispose');
|
|
610
|
+
this.renderWaiting = false;
|
|
611
|
+
this.rowsWaiting.clear();
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/** After every close: focus back on the table when the keyboard closed it,
|
|
615
|
+
* and the render that waited for the editor. */
|
|
616
|
+
_closed(s, reason) {
|
|
617
|
+
const t = this.table;
|
|
618
|
+
if (reason === 'dispose') return;
|
|
619
|
+
if (reason === 'key' || reason === 'editor') {
|
|
620
|
+
try { t._tableEl?.focus({ preventScroll: true }); } catch (_) { /* detached */ }
|
|
621
|
+
}
|
|
622
|
+
if (this.renderWaiting) {
|
|
623
|
+
this.renderWaiting = false;
|
|
624
|
+
this.rowsWaiting.clear(); // the render draws every row
|
|
625
|
+
// A commit made by the virtual window taking the row out is made
|
|
626
|
+
// in the middle of the window's own redraw, which a render would
|
|
627
|
+
// pull out from under it: that one waits for the redraw to end.
|
|
628
|
+
if (reason === 'scroll') queueMicrotask(() => { if (t._wrapperEl) t.render(); });
|
|
629
|
+
else t.render();
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
if (this.rowsWaiting.size) {
|
|
633
|
+
const flush = () => {
|
|
634
|
+
const waiting = [...this.rowsWaiting];
|
|
635
|
+
this.rowsWaiting.clear();
|
|
636
|
+
for (const rowIdx of waiting) this._redraw(rowIdx);
|
|
637
|
+
};
|
|
638
|
+
if (reason === 'scroll') queueMicrotask(() => { if (t._wrapperEl) flush(); });
|
|
639
|
+
else flush();
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Draw a row again after a commit, from the row the table holds now
|
|
645
|
+
* (`currentRow`). `updateRow` rebuilds EVERY cell of the row, so while an
|
|
646
|
+
* editor is open in it (save, Tab, type — the commit settles under the
|
|
647
|
+
* next editor) the row waits for that editor to close (`_closed`), and
|
|
648
|
+
* only the committed cell's marks are painted now.
|
|
649
|
+
*/
|
|
650
|
+
_redraw(rowIdx, col = null) {
|
|
651
|
+
const t = this.table;
|
|
652
|
+
const tr = t.getRowElement(rowIdx);
|
|
653
|
+
const row = this.currentRow(rowIdx);
|
|
654
|
+
if (!tr || !row) return;
|
|
655
|
+
if (this._holds(tr)) {
|
|
656
|
+
this.rowsWaiting.add(rowIdx);
|
|
657
|
+
if (col === null || tr.__cellPos === undefined) return;
|
|
658
|
+
const s = this.editor.session;
|
|
659
|
+
for (const td of tr.children) {
|
|
660
|
+
if (td.__colIdx === col && td !== s.td) this._decorateCell(td, tr.__cellPos, rowIdx, col);
|
|
661
|
+
}
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
this.rowsWaiting.delete(rowIdx);
|
|
665
|
+
t.updateRow(rowIdx, row);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/** STORY-0087. The editor closed holding text that does not parse (the
|
|
669
|
+
* focus left it, or its row left the window): nothing is committed, and
|
|
670
|
+
* the cell keeps the text, marked with the message, as a refused commit's
|
|
671
|
+
* would be. */
|
|
672
|
+
_refused(s, error, text, reason) {
|
|
673
|
+
if (s.generation === this.generation) {
|
|
674
|
+
const key = `${s.rowIdx}:${s.colIdx}`;
|
|
675
|
+
this.errors.set(key, { value: text, message: error });
|
|
676
|
+
// Any older commit of the cell still out no longer owns its marks.
|
|
677
|
+
this.latest.set(key, {});
|
|
678
|
+
this.pending.delete(key);
|
|
679
|
+
this._redraw(s.rowIdx, s.colIdx);
|
|
680
|
+
}
|
|
681
|
+
this._closed(s, reason);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
_committed(s, value, move, reason) {
|
|
685
|
+
if (move) {
|
|
686
|
+
this._sync();
|
|
687
|
+
if (move === 'down' || move === 'up') this.sel.move(move === 'down' ? 1 : -1, 0);
|
|
688
|
+
else this.sel.tab(move === 'left');
|
|
689
|
+
this._changed({ reveal: true });
|
|
690
|
+
}
|
|
691
|
+
const live = s.generation === this.generation;
|
|
692
|
+
const key = `${s.rowIdx}:${s.colIdx}`;
|
|
693
|
+
// Left as it was shown: no edit at all.
|
|
694
|
+
if (Object.is(value, s.previous)) {
|
|
695
|
+
this._closed(s, reason);
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
// STORY-0088 `staged`: the value is staged, not sent.
|
|
699
|
+
if (this.staged) {
|
|
700
|
+
if (live) this.staged.stage(s.rowIdx, s.colIdx, value);
|
|
701
|
+
this._closed(s, reason);
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
// What the row holds: the table's row now, or — for a commit that
|
|
705
|
+
// outlived its rows — the row it was opened on.
|
|
706
|
+
const held = live ? this.currentRow(s.rowIdx) : s.row;
|
|
707
|
+
const stored = held ? held[s.colIdx] : undefined;
|
|
708
|
+
// A refused cell given back the value its row holds: nothing to save,
|
|
709
|
+
// the refusal is over.
|
|
710
|
+
if (live && this.errors.has(key) && !this.pending.has(key) && Object.is(value, stored)) {
|
|
711
|
+
this.errors.delete(key);
|
|
712
|
+
this.latest.delete(key);
|
|
713
|
+
this._redraw(s.rowIdx, s.colIdx);
|
|
714
|
+
this._closed(s, reason);
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
const fn = this._cfg().onCellCommit;
|
|
718
|
+
const token = {};
|
|
719
|
+
if (live) this.latest.set(key, token);
|
|
720
|
+
const finish = (result) => {
|
|
721
|
+
if (!live || s.generation !== this.generation) return;
|
|
722
|
+
const newest = this.latest.get(key) === token;
|
|
723
|
+
const good = !(result && result.ok === false);
|
|
724
|
+
// The value goes into the row the table holds NOW, so a newer row
|
|
725
|
+
// the consumer put in meanwhile (`updateRow`) keeps its other cells.
|
|
726
|
+
if (good) {
|
|
727
|
+
const row = this.currentRow(s.rowIdx);
|
|
728
|
+
if (Array.isArray(row)) row[s.colIdx] = value;
|
|
729
|
+
}
|
|
730
|
+
// A newer commit of the cell is still out: its marks are its own.
|
|
731
|
+
if (newest) {
|
|
732
|
+
this.latest.delete(key);
|
|
733
|
+
this.pending.delete(key);
|
|
734
|
+
if (good) this.errors.delete(key);
|
|
735
|
+
else this.errors.set(key, { value, message: String(result.error ?? 'Not saved') });
|
|
736
|
+
}
|
|
737
|
+
this._redraw(s.rowIdx, s.colIdx);
|
|
738
|
+
};
|
|
739
|
+
const refused = (err) => ({ ok: false, error: err?.message || String(err ?? 'Not saved') });
|
|
740
|
+
let result;
|
|
741
|
+
if (typeof fn === 'function') {
|
|
742
|
+
try {
|
|
743
|
+
result = fn({ rowIdx: s.rowIdx, colIdx: s.colIdx, value, previous: stored });
|
|
744
|
+
} catch (err) {
|
|
745
|
+
result = refused(err);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
if (result && typeof result.then === 'function') {
|
|
749
|
+
if (live) {
|
|
750
|
+
this.pending.set(key, value);
|
|
751
|
+
this.errors.delete(key);
|
|
752
|
+
this._redraw(s.rowIdx, s.colIdx);
|
|
753
|
+
}
|
|
754
|
+
this._closed(s, reason);
|
|
755
|
+
result.then(finish, (err) => finish(refused(err)));
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
finish(result);
|
|
759
|
+
this._closed(s, reason);
|
|
760
|
+
}
|
|
761
|
+
}
|