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,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
|
+
}
|