flexdesk 0.17.0 → 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 +19 -0
- package/README.md +221 -3
- package/canvas.js +18 -0
- package/charts.js +21 -0
- package/core.js +14 -0
- package/css/flexdesk.css +24 -0
- package/dist/charts.js +3 -5
- package/dist/charts.js.map +1 -1
- package/dist/{chunk-Q36EPT2R.js → chunk-LWV23YVE.js} +1614 -138
- package/dist/chunk-LWV23YVE.js.map +7 -0
- package/dist/{chunk-MCIXFNVF.js → chunk-RMTRWWEO.js} +2 -2
- package/dist/flexdesk.css +24 -0
- package/dist/flow.js +2 -2
- package/dist/flow.js.map +2 -2
- package/dist/tiles.js +2 -2
- package/dist/widgets.js +43 -5
- package/dist/widgets.js.map +1 -1
- package/editor.js +20 -0
- package/flow.js +21 -0
- package/host.js +14 -0
- package/package.json +10 -1
- package/src/flow/canvas_editor/editor.js +1 -1
- package/src/ui/components/cell_editor_host.js +100 -18
- package/src/ui/components/cell_editors.js +480 -0
- package/src/ui/components/cell_grid.js +121 -8
- package/src/ui/components/cell_paste.js +243 -0
- package/src/ui/components/data_table.js +96 -10
- package/src/ui/components/staged_edits.js +19 -1
- package/src/ui/components/tsv.js +74 -0
- package/tiles.js +27 -0
- package/widgets.js +69 -0
- package/wm.js +28 -0
- package/dist/chunk-Q36EPT2R.js.map +0 -7
- package/dist/chunk-QWQJUDPD.js +0 -610
- package/dist/chunk-QWQJUDPD.js.map +0 -7
- /package/dist/{chunk-MCIXFNVF.js.map → chunk-RMTRWWEO.js.map} +0 -0
|
@@ -13,8 +13,11 @@
|
|
|
13
13
|
|
|
14
14
|
import { CellSelection } from './cell_selection.js';
|
|
15
15
|
import { CellEditorHost, createTextEditor } from './cell_editor_host.js';
|
|
16
|
+
import { CELL_EDITORS, editorTypeOf } from './cell_editors.js';
|
|
16
17
|
import { StagedEdits } from './staged_edits.js';
|
|
17
18
|
import { FillHandle } from './fill_handle.js';
|
|
19
|
+
import { CellPaste } from './cell_paste.js';
|
|
20
|
+
import { ISO_DATE, formatDate } from './table_values.js';
|
|
18
21
|
|
|
19
22
|
/** Is this a cell somebody filled? What Ctrl+arrow stops at. */
|
|
20
23
|
export function isFilledValue(value) {
|
|
@@ -35,6 +38,27 @@ const ARROWS = {
|
|
|
35
38
|
ArrowUp: [-1, 0], ArrowDown: [1, 0], ArrowLeft: [0, -1], ArrowRight: [0, 1],
|
|
36
39
|
};
|
|
37
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
|
+
|
|
38
62
|
/**
|
|
39
63
|
* DataTable's cell mode. Built by the table when `selectionMode` is 'cells';
|
|
40
64
|
* it reads the table, never the other way round, except through the few hooks
|
|
@@ -64,6 +88,7 @@ export class CellGrid {
|
|
|
64
88
|
this.editor = new CellEditorHost({
|
|
65
89
|
onCommit: (s, value, move, reason) => this._committed(s, value, move, reason),
|
|
66
90
|
onCancel: (s, reason) => this._closed(s, reason),
|
|
91
|
+
onRefuse: (s, error, text, reason) => this._refused(s, error, text, reason),
|
|
67
92
|
});
|
|
68
93
|
/** STORY-0088 `staged`: commits are staged here instead of sent. */
|
|
69
94
|
this.staged = null;
|
|
@@ -77,6 +102,12 @@ export class CellGrid {
|
|
|
77
102
|
if (this.staged) this.fill = new FillHandle(this);
|
|
78
103
|
else console.warn('[DataTable] `fillHandle` needs `staged`; ignored.');
|
|
79
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
|
+
}
|
|
80
111
|
}
|
|
81
112
|
|
|
82
113
|
// ── what the table is, read through its own helpers ──────────────────
|
|
@@ -226,6 +257,30 @@ export class CellGrid {
|
|
|
226
257
|
this._changed();
|
|
227
258
|
}
|
|
228
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
|
+
|
|
229
284
|
/** New rows (`setData({rows})`): the marks were about the old ones. */
|
|
230
285
|
newRows() {
|
|
231
286
|
this.generation++;
|
|
@@ -235,6 +290,7 @@ export class CellGrid {
|
|
|
235
290
|
this.rowsWaiting.clear();
|
|
236
291
|
this.staged?.clear();
|
|
237
292
|
this.fill?.clearMarks();
|
|
293
|
+
this.paste?.clearMarks();
|
|
238
294
|
}
|
|
239
295
|
|
|
240
296
|
isEditing() {
|
|
@@ -284,6 +340,7 @@ export class CellGrid {
|
|
|
284
340
|
if (cursor) td.setAttribute('aria-selected', 'true');
|
|
285
341
|
else td.removeAttribute('aria-selected');
|
|
286
342
|
this.fill?.paintCell(td, pos, col); // STORY-0089
|
|
343
|
+
this.paste?.paintCell(td, col); // STORY-0090
|
|
287
344
|
}
|
|
288
345
|
|
|
289
346
|
paint() {
|
|
@@ -303,6 +360,7 @@ export class CellGrid {
|
|
|
303
360
|
_changed({ reveal = false } = {}) {
|
|
304
361
|
const t = this.table;
|
|
305
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
|
|
306
364
|
if (reveal && this.sel.focus) t.scrollToRow(this.rowIndexAt(this.sel.focus.row));
|
|
307
365
|
this.paint();
|
|
308
366
|
const before = t.getSelection().join();
|
|
@@ -375,6 +433,19 @@ export class CellGrid {
|
|
|
375
433
|
ev.stopImmediatePropagation();
|
|
376
434
|
}
|
|
377
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
|
+
}
|
|
378
449
|
// An editor that was open across a render of its row (an `updateRow`)
|
|
379
450
|
// is still the editor; the paint follows the model.
|
|
380
451
|
this.paint();
|
|
@@ -457,21 +528,41 @@ export class CellGrid {
|
|
|
457
528
|
return false;
|
|
458
529
|
}
|
|
459
530
|
|
|
460
|
-
/** The column's
|
|
461
|
-
|
|
462
|
-
|
|
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);
|
|
463
544
|
const declared = this._cfg().columnTypes?.[col];
|
|
464
545
|
return declared || this.table._columnTypes?.[col] || 'text';
|
|
465
546
|
}
|
|
466
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. */
|
|
467
556
|
_factoryFor(col) {
|
|
468
557
|
const editors = this._cfg().editors;
|
|
469
558
|
if (editors && typeof editors === 'object') {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
559
|
+
for (const k of [col, this._rawType(col), this._typeOf(col)]) {
|
|
560
|
+
if (typeof editors[k] === 'function') return editors[k];
|
|
561
|
+
}
|
|
473
562
|
}
|
|
474
|
-
|
|
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;
|
|
475
566
|
}
|
|
476
567
|
|
|
477
568
|
/**
|
|
@@ -494,7 +585,12 @@ export class CellGrid {
|
|
|
494
585
|
pos, colIdx: col, rowIdx, row,
|
|
495
586
|
generation: this.generation,
|
|
496
587
|
value: this.shownValue(rowIdx, col),
|
|
497
|
-
column: { index: col, header: headers[col], type: this._typeOf(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
|
+
},
|
|
498
594
|
initialText,
|
|
499
595
|
});
|
|
500
596
|
}
|
|
@@ -509,6 +605,7 @@ export class CellGrid {
|
|
|
509
605
|
|
|
510
606
|
dispose() {
|
|
511
607
|
this.fill?.dispose();
|
|
608
|
+
this.paste?.dispose();
|
|
512
609
|
this.editor.cancel('dispose');
|
|
513
610
|
this.renderWaiting = false;
|
|
514
611
|
this.rowsWaiting.clear();
|
|
@@ -568,6 +665,22 @@ export class CellGrid {
|
|
|
568
665
|
t.updateRow(rowIdx, row);
|
|
569
666
|
}
|
|
570
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
|
+
|
|
571
684
|
_committed(s, value, move, reason) {
|
|
572
685
|
if (move) {
|
|
573
686
|
this._sync();
|
|
@@ -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
|
+
}
|