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
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Unified, reusable data table component with support for:
|
|
5
5
|
* - Pagination (first/prev/next/last/jump to row)
|
|
6
6
|
* - Row selection (single, range with Shift, toggle with Ctrl/Cmd)
|
|
7
|
-
* - Keyboard shortcuts (Ctrl+A, Ctrl+C, Shift+Ctrl+C, Esc)
|
|
7
|
+
* - Keyboard shortcuts (Ctrl+A, Ctrl+C, Shift+Ctrl+C outside cell mode, Esc)
|
|
8
8
|
* - Context menu (copy as TSV/CSV)
|
|
9
9
|
* - Column sorting (internal or external via callback)
|
|
10
10
|
* - Column drag-to-resize, and double-click-to-auto-size on the same grip
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
import { isRowActivation, isRowControl } from './row_activation.js';
|
|
28
28
|
import { createChipsFilter } from './table_filter_chips.js';
|
|
29
29
|
import { CellGrid } from './cell_grid.js';
|
|
30
|
+
import { toTSV } from './tsv.js';
|
|
30
31
|
import { ColumnReorder, cellOrder, isColumnOrder, moveInOrder, naturalOrder } from './column_order.js';
|
|
31
32
|
import { ColumnGroups } from './column_groups.js';
|
|
32
33
|
import { Crosstab } from './crosstab.js';
|
|
@@ -415,7 +416,10 @@ const AUTOSIZE_MAX_PX = 520;
|
|
|
415
416
|
* ── STORY-0003, every one of them off unless asked for ─────────────────────
|
|
416
417
|
* @property {'rows'|'cells'} [selectionMode='rows'] - 'cells': CELL MODE (cell_grid.js, over cell_selection.js). A click selects one cell and puts the CURSOR on it, Shift+click selects the range from the anchor to it; every cell of the range is drawn `twm-dt-cell--selected` and the cursor `twm-dt-cell--cursor` (rows are not lit). On the focused table the arrows move the cursor (Shift extends), Ctrl+arrow jumps to the edge of the block of filled cells, Home/End go to the row's ends (Ctrl: the page's corners), Tab moves right and wraps to the next row (Shift+Tab back), Ctrl+A selects every cell of the page and Escape collapses the range onto the cursor. The cursor stays on the page drawn and is scrolled into view (under `virtualize` its row is drawn). `getSelection()` still answers ROWS — the original indices the range touches — and Ctrl+C copies those rows as before. A cell is `{row, col}`: `row` the position in the order shown (`getDisplayedRows()`), `col` the data column. Fixed for the table's life; `selectToggle`, `selectMode` and `keyboardNav` beside it are ignored with a console warning. See `getCellSelection`, `setCellSelection`.
|
|
417
418
|
* @property {boolean|number[]|Function} [editable=false] - Cell mode only (a warning, and ignored, without it). Which cells open an EDITOR: `true` every one, an array of data column indices, or `(colIdx, row, rowIdx) => boolean`. F2, Enter, a double-click or a printable key (which the editor starts with) opens it on the cursor cell; in it Enter commits and moves down, Tab commits and moves right, Escape restores the cell and calls nothing, and the focus leaving commits. While it is open a `render()` (a `setData` included) waits, and runs when it closes; its row leaving the virtual window commits it. An editor with no `getValue` (a factory's `{el}` around an element that is not an input, select or textarea) has no value to commit: Enter and Tab do nothing, the focus leaving does nothing, and its row leaving the window CANCELS it — it calls `commit(value)` itself.
|
|
418
|
-
* @property {Object} [editors] - `{[colIdx|type]: factory}`, the editor for a column (by index first, then by its type: `columnTypes`'
|
|
419
|
+
* @property {Object} [editors] - `{[colIdx|type]: factory}`, the editor for a column (by index first, then by its type: `editTypes`', else `columnTypes`', else the detected 'num'/'text' — as named, then as the built-in editors name it, 'num' being 'number' and 'date' 'datetime'). A factory is `({value, row, rowIdx, colIdx, column: {index, header, type, ...editTypes' entry}, locale, utc, messages, initialText, commit(value, move?), cancel()}) => Element | {el, getValue?, parse?, multiline?, focus?, destroy?}`; an input, select or textarea returned as it is commits its `value`. Absent: the built-in editor for the column's type (cell_editors.js `CELL_EDITORS`: text, multitext, number, bool, choice, datetime), else a text box (cell_editor_host.js `createTextEditor`). Every editor commits the value unchanged when left as it was. An injected editor replaces the built-in one for its type (or column) only.
|
|
420
|
+
* @property {Array|Object} [editTypes] - STORY-0087. Each column's EDITOR type, by position (array) or by data column index (object): a name ('text', 'multitext', 'number', 'bool', 'choice', 'datetime', or any name `editors` is keyed by) or `{type, options?, allowOther?, rows?}`. Unset, a column's editor type is its `columnTypes` type, else the detected one. 'choice' offers `options` (values, or `{value, label}`) through an AutocompleteField and refuses other text unless `allowOther`; 'multitext' is a text area of `rows` lines (3). Only editing reads it: drawing, sorting and filtering are as without it.
|
|
421
|
+
* @property {string} [locale] - STORY-0087. The locale the built-in number and datetime editors read and show their text in (Intl's; unset: the runtime's): '1.234,56' is 1234.56 under 'de-DE', '1,234.56' under 'en-US'; '31/12/2026' is a date under 'en-GB'. A datetime also takes ISO 8601 in any locale.
|
|
422
|
+
* @property {Object} [editorMessages] - STORY-0087. Words for the built-in editors' refusals, over `EDITOR_MESSAGES`: `{number, datetime, bool, choice}`.
|
|
419
423
|
* @property {Function} [onCellCommit] - `({rowIdx, colIdx, value, previous}) => void | {ok, error?} | Promise<…>`, once per commit (not for a value left as the cell showed it; `previous` is the value the row holds). Resolving `{ok: false, error}`, rejecting or throwing leaves the typed value in the cell drawn `twm-dt-cell--error` with the message as its `title` and `data-error`; until it settles the cell is `twm-dt-cell--pending`. Otherwise the value is written into the row the table holds for `rowIdx` when it settles (`rows[rowIdx][colIdx]`) and drawn. Committing a refused cell back to the row's value clears the mark without a call. New rows (`setData({rows})`) drop the marks.
|
|
420
424
|
* @property {Function} [onCellSelectionChange] - `(getCellSelection()) => void`, after every change of the cells selected.
|
|
421
425
|
* @property {Function} [headerMenu] - `(colIdx, {colIdx, header, hiddenColumns, table}) => items`, asked on a right-click on a data column's header (or its filter cell). The items (`{label, action, icon?, disabled?, danger?, title?}` or `{separator: true}`) open the table's menu; with `columnChooser` the chooser's items follow behind a separator. `[]` or `null` shows what would show without the hook: the chooser, or the page's own menu.
|
|
@@ -427,6 +431,9 @@ const AUTOSIZE_MAX_PX = 520;
|
|
|
427
431
|
* @property {Function} [onCommit] - With `staged`: `({edits}) => void | {ok?, error?, errors?} | Promise<…>`, called once by `commit()` with every staged cell as `{rowIdx, colIdx, value, previous}` (by row, then column; `previous` is the value the row holds). Resolving `{errors: [{rowIdx, colIdx, error}]}` (non-empty) marks each of those cells `twm-dt-cell--error` with the message as its `title` and `data-error`, and every staged value stays; `{ok: false, error}`, a rejection or a throw marks every staged cell with the message. Anything else is success: the values are written into the rows the table holds, and the staged cells and the ring are cleared. While it is out no editor opens and Ctrl+Z, Ctrl+Y and `discard()` do nothing.
|
|
428
432
|
* @property {Function} [onStagedChange] - With `staged`: `({count, canUndo, canRedo, committing}) => void` after every change of the staged cells.
|
|
429
433
|
* @property {boolean} [fillHandle=false] - STORY-0089. With `staged` (a warning, and ignored, without it): a FILL HANDLE (fill_handle.js) on the bottom-right corner of the cell range (`.twm-dt-fill-handle`, in the corner cell). Dragging it up, down, left or right over the cells beyond the range previews them `twm-dt-cell--fill-preview` and, on release, stages their values as ONE undo entry: each column of the range (each row, filling sideways) is continued by `projectSeries` (series.js) — one cell repeats, numbers, dates and "Item 1"-style texts a constant step apart continue, anything else repeats in a cycle — and Ctrl (Cmd) held at release repeats the block instead. A target cell `editable` refuses is skipped and drawn `twm-dt-cell--fill-skipped` until the selection moves. The range then covers what was filled. Escape during the drag cancels it. See `fill`.
|
|
434
|
+
* @property {boolean} [pasteable=false] - STORY-0090. With `staged` (a warning, and ignored, without it): Ctrl+V on the focused table (the browser's `paste` event; `paste(text)` from code) pastes a tab-separated block — what Excel and Google Sheets copy, read by `fromTSV` (tsv.js) — at the CURSOR cell, filling right over the columns shown and down over the rows in the order shown (cell_paste.js). Each value is read by its column's editor's parse (the built-in editor's for its type, or an injected `editors` factory's own `parse` property), and every cell that reads is staged as ONE undo entry; the range then covers the block, the cursor where it began. Columns past the last shown one are dropped with a warning naming how many (a console warning and a 'warn' toast). A value that does not parse marks its cell `twm-dt-cell--error` (message as `title` and `data-error`, the text kept) and the rest still stages; a cell `editable` refuses is skipped and drawn `twm-dt-cell--paste-skipped` until the selection moves. Rows past the last loaded row ask `onPasteOverrun`.
|
|
435
|
+
* @property {Function} [onPasteOverrun] - With `pasteable`: `({rows}) => boolean | Promise<boolean>`, asked when a paste runs `rows` rows past the last loaded row (the last row in the order shown; for a server's page, the last row it sent). Anything but `true` stages NOTHING; `true` stages the rows that fit and hands the extra ones to `onRowsAppend`. Without it, such a paste stages nothing, with a console warning.
|
|
436
|
+
* @property {Function} [onRowsAppend] - With `pasteable`: `({rows, errors}) => void | Promise`, after `onPasteOverrun` said `true`: `rows` the extra rows as data rows (one array per row, as long as `headers`, every pasted column read by its parse, the others null) and `errors` the values that did not read (`{row, colIdx, text, error}`, `row` an index into `rows`; that cell is null). Adding them is the consumer's; the rows that fit are staged once it settles. Without it, the extra rows are dropped with a warning.
|
|
430
437
|
* @property {string|Function} [stagedRefusal] - With `staged`: the reason a frozen control carries, or `(count) => string`. Absent: "Sorting, filtering and paging are held while N changes are not committed. Commit or discard them first."
|
|
431
438
|
*
|
|
432
439
|
* ── STORY-0066, every one of them off unless asked for. THE CONSUMER OWNS THE DATA: each edit is a call, answered with `setData` ──
|
|
@@ -450,9 +457,16 @@ const AUTOSIZE_MAX_PX = 520;
|
|
|
450
457
|
* it were this tall. A ResizeObserver corrects it the moment a real size exists. */
|
|
451
458
|
const VIRTUAL_FALLBACK_VIEWPORT_PX = 1000;
|
|
452
459
|
|
|
453
|
-
/** The copy menu
|
|
454
|
-
* `contextMenuItems`, and its tail with them (STORY-0010).
|
|
455
|
-
|
|
460
|
+
/** The copy menu — the whole right-click menu of a table without
|
|
461
|
+
* `contextMenuItems`, and its tail with them (STORY-0010). Row mode has two
|
|
462
|
+
* items, "Copy Row(s)" and "Copy as CSV"; cell mode (STORY-0085) has one,
|
|
463
|
+
* "Copy Cells", and no CSV copy. */
|
|
464
|
+
const COPY_CELLS_MENU_HTML = `
|
|
465
|
+
<div class="twm-context-menu-item" data-action="copy-tsv">
|
|
466
|
+
<span class="material-symbols-outlined">content_copy</span>
|
|
467
|
+
<span class="label">Copy Cells</span>
|
|
468
|
+
</div>`;
|
|
469
|
+
const copyMenuHtml = (cells) => cells ? COPY_CELLS_MENU_HTML : `
|
|
456
470
|
<div class="twm-context-menu-item" data-action="copy-tsv">
|
|
457
471
|
<span class="material-symbols-outlined">content_copy</span>
|
|
458
472
|
<span class="label">Copy Row(s)</span>
|
|
@@ -600,6 +614,9 @@ export class DataTable {
|
|
|
600
614
|
selectionMode: 'rows',
|
|
601
615
|
editable: false,
|
|
602
616
|
editors: null,
|
|
617
|
+
editTypes: null,
|
|
618
|
+
locale: null,
|
|
619
|
+
editorMessages: null,
|
|
603
620
|
onCellCommit: null,
|
|
604
621
|
onCellSelectionChange: null,
|
|
605
622
|
headerMenu: null,
|
|
@@ -624,6 +641,10 @@ export class DataTable {
|
|
|
624
641
|
onRowDelete: null,
|
|
625
642
|
// STORY-0089. Off — no fill handle — until set.
|
|
626
643
|
fillHandle: false,
|
|
644
|
+
// STORY-0090. Off — Ctrl+V is not the table's — until set.
|
|
645
|
+
pasteable: false,
|
|
646
|
+
onPasteOverrun: null,
|
|
647
|
+
onRowsAppend: null,
|
|
627
648
|
...config,
|
|
628
649
|
};
|
|
629
650
|
|
|
@@ -652,6 +673,9 @@ export class DataTable {
|
|
|
652
673
|
if (this.config.fillHandle && !this._cellGrid) {
|
|
653
674
|
console.warn('[DataTable] `fillHandle` needs `staged`; ignored.');
|
|
654
675
|
}
|
|
676
|
+
if (this.config.pasteable && !this._cellGrid) {
|
|
677
|
+
console.warn('[DataTable] `pasteable` needs `staged`; ignored.');
|
|
678
|
+
}
|
|
655
679
|
// STORY-0091. A crosstab has no group row: its spans are `headerRows`.
|
|
656
680
|
if (Array.isArray(this.config.columnGroups) && this.config.columnGroups.length && this._crosstab()) {
|
|
657
681
|
console.warn('[DataTable] `columnGroups` cannot be combined with `crosstab` (use crosstab.headerRows); ignored.');
|
|
@@ -1595,6 +1619,25 @@ export class DataTable {
|
|
|
1595
1619
|
return this._cellGrid?.fill ? this._cellGrid.fill.fill(to, { forceCopy }) : null;
|
|
1596
1620
|
}
|
|
1597
1621
|
|
|
1622
|
+
/**
|
|
1623
|
+
* STORY-0090. Paste tab-separated `text` at the cursor cell, as Ctrl+V
|
|
1624
|
+
* does: each value read by its column's parse, staged as ONE undo entry.
|
|
1625
|
+
* Resolves `{rows, cols, staged, skipped, refused, droppedColumns,
|
|
1626
|
+
* appended}` — `staged` the NUMBER of cells that changed, `skipped` the
|
|
1627
|
+
* cells `editable` refused (`{row, col, rowIdx}`), `refused` the values
|
|
1628
|
+
* that did not parse (`{row, col, rowIdx, text, error}`),
|
|
1629
|
+
* `droppedColumns` the columns past the last one, `appended` the rows
|
|
1630
|
+
* handed to `onRowsAppend` — or null when nothing was pasted (no
|
|
1631
|
+
* `pasteable`, empty text, no cursor, an editor open, a commit or another
|
|
1632
|
+
* paste out, an overrun `onPasteOverrun` did not accept).
|
|
1633
|
+
*
|
|
1634
|
+
* @param {string} text
|
|
1635
|
+
* @returns {Promise<object|null>}
|
|
1636
|
+
*/
|
|
1637
|
+
paste(text) {
|
|
1638
|
+
return this._cellGrid?.paste ? this._cellGrid.paste.paste(text) : Promise.resolve(null);
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1598
1641
|
/** Why sort, filters and pages are held — a staged edit — or null; with
|
|
1599
1642
|
* a reason, `what` is refused out loud. */
|
|
1600
1643
|
_stagedRefusal(what) {
|
|
@@ -2145,10 +2188,21 @@ export class DataTable {
|
|
|
2145
2188
|
}
|
|
2146
2189
|
|
|
2147
2190
|
/**
|
|
2148
|
-
* Copy selected rows to clipboard
|
|
2191
|
+
* Copy selected rows to clipboard — in cell mode (STORY-0085) the cells
|
|
2192
|
+
* selected, or the cursor cell alone, as tab-separated raw values with no
|
|
2193
|
+
* header line; cell mode has no CSV copy
|
|
2149
2194
|
* @param {'tsv'|'csv'} [format='tsv'] - Output format
|
|
2150
2195
|
*/
|
|
2151
2196
|
async copyToClipboard(format = 'tsv') {
|
|
2197
|
+
// STORY-0085. Cell mode copies the cells selected, not their rows,
|
|
2198
|
+
// and only as TSV.
|
|
2199
|
+
if (this._cellGrid) {
|
|
2200
|
+
if (format === 'csv') {
|
|
2201
|
+
console.warn('[DataTable] copyToClipboard: cell mode has no CSV copy; nothing copied');
|
|
2202
|
+
return;
|
|
2203
|
+
}
|
|
2204
|
+
return this._copyCells();
|
|
2205
|
+
}
|
|
2152
2206
|
const indices = this.getSelection();
|
|
2153
2207
|
if (indices.length === 0) {
|
|
2154
2208
|
this._notify('Copy', 'No rows selected.', 'warn');
|
|
@@ -2191,6 +2245,36 @@ export class DataTable {
|
|
|
2191
2245
|
this._hideContextMenu();
|
|
2192
2246
|
}
|
|
2193
2247
|
|
|
2248
|
+
/**
|
|
2249
|
+
* STORY-0085. Cell mode's copy: the range (or the cursor cell alone), its
|
|
2250
|
+
* raw values as TSV — Excel's quoting, `tsv.js` — with no header line.
|
|
2251
|
+
*/
|
|
2252
|
+
async _copyCells() {
|
|
2253
|
+
const matrix = this._cellGrid.copyMatrix();
|
|
2254
|
+
if (!matrix || !matrix.length) {
|
|
2255
|
+
this._notify('Copy', 'No cells selected.', 'warn');
|
|
2256
|
+
this._hideContextMenu();
|
|
2257
|
+
return;
|
|
2258
|
+
}
|
|
2259
|
+
const text = toTSV(matrix);
|
|
2260
|
+
let ok = false;
|
|
2261
|
+
try {
|
|
2262
|
+
await navigator.clipboard.writeText(text);
|
|
2263
|
+
ok = true;
|
|
2264
|
+
} catch (_) {
|
|
2265
|
+
ok = this._fallbackCopy(text);
|
|
2266
|
+
}
|
|
2267
|
+
if (ok) {
|
|
2268
|
+
const n = matrix.length;
|
|
2269
|
+
const m = matrix[0].length;
|
|
2270
|
+
const what = n * m === 1 ? '1 cell' : `${n} × ${m} cells`;
|
|
2271
|
+
this._notify('Copy', `Copied ${what}.`, 'info');
|
|
2272
|
+
} else {
|
|
2273
|
+
this._notify('Copy', 'Clipboard unavailable.', 'warn');
|
|
2274
|
+
}
|
|
2275
|
+
this._hideContextMenu();
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2194
2278
|
/**
|
|
2195
2279
|
* Download all data as CSV
|
|
2196
2280
|
* @param {string} [filename='data.csv'] - Filename for download
|
|
@@ -7047,7 +7131,9 @@ export class DataTable {
|
|
|
7047
7131
|
} else if (key === 'a' && selectable) {
|
|
7048
7132
|
event.preventDefault();
|
|
7049
7133
|
this._selectAllShown();
|
|
7050
|
-
} else if (key === 'c') {
|
|
7134
|
+
} else if (key === 'c' && !(event.shiftKey && this._cellGrid)) {
|
|
7135
|
+
// STORY-0085. Cell mode binds Ctrl+C only: it has no
|
|
7136
|
+
// CSV copy, so Ctrl+Shift+C is left to the page.
|
|
7051
7137
|
event.preventDefault();
|
|
7052
7138
|
this.copyToClipboard(event.shiftKey ? 'csv' : 'tsv');
|
|
7053
7139
|
}
|
|
@@ -7198,7 +7284,7 @@ export class DataTable {
|
|
|
7198
7284
|
menu.style.display = 'none';
|
|
7199
7285
|
menu.style.position = 'fixed';
|
|
7200
7286
|
menu.style.zIndex = '10001';
|
|
7201
|
-
menu.innerHTML =
|
|
7287
|
+
menu.innerHTML = copyMenuHtml(!!this._cellGrid);
|
|
7202
7288
|
document.body.appendChild(menu);
|
|
7203
7289
|
|
|
7204
7290
|
menu.addEventListener('click', (event) => {
|
|
@@ -7371,7 +7457,7 @@ export class DataTable {
|
|
|
7371
7457
|
this._contextMenuCtx = custom ? custom.ctx : null;
|
|
7372
7458
|
if (!custom) {
|
|
7373
7459
|
if (this._contextMenuCustom) {
|
|
7374
|
-
menu.innerHTML =
|
|
7460
|
+
menu.innerHTML = copyMenuHtml(!!this._cellGrid);
|
|
7375
7461
|
this._contextMenuCustom = false;
|
|
7376
7462
|
}
|
|
7377
7463
|
return;
|
|
@@ -7416,7 +7502,7 @@ export class DataTable {
|
|
|
7416
7502
|
sep.className = 'twm-context-menu__separator';
|
|
7417
7503
|
menu.appendChild(sep);
|
|
7418
7504
|
}
|
|
7419
|
-
menu.insertAdjacentHTML('beforeend',
|
|
7505
|
+
menu.insertAdjacentHTML('beforeend', copyMenuHtml(!!this._cellGrid));
|
|
7420
7506
|
}
|
|
7421
7507
|
}
|
|
7422
7508
|
|
|
@@ -103,7 +103,12 @@ export class StagedEdits {
|
|
|
103
103
|
* Returns whether anything changed.
|
|
104
104
|
*/
|
|
105
105
|
stage(rowIdx, colIdx, value) {
|
|
106
|
-
if (this.committing
|
|
106
|
+
if (this.committing) return false;
|
|
107
|
+
const refusal = this._dropRefusal(rowIdx, colIdx);
|
|
108
|
+
if (!this._stageOne(rowIdx, colIdx, value)) {
|
|
109
|
+
if (refusal) this.grid._redraw(rowIdx, colIdx);
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
107
112
|
this.grid._redraw(rowIdx, colIdx);
|
|
108
113
|
this._changed();
|
|
109
114
|
return true;
|
|
@@ -121,7 +126,9 @@ export class StagedEdits {
|
|
|
121
126
|
this.history.beginGroup(label);
|
|
122
127
|
try {
|
|
123
128
|
for (const { rowIdx, colIdx, value } of cells) {
|
|
129
|
+
const refusal = this._dropRefusal(rowIdx, colIdx);
|
|
124
130
|
if (this._stageOne(rowIdx, colIdx, value)) { changed++; rows.add(rowIdx); }
|
|
131
|
+
else if (refusal) rows.add(rowIdx);
|
|
125
132
|
}
|
|
126
133
|
} finally {
|
|
127
134
|
this.history.endGroup();
|
|
@@ -131,6 +138,17 @@ export class StagedEdits {
|
|
|
131
138
|
return changed;
|
|
132
139
|
}
|
|
133
140
|
|
|
141
|
+
/** STORY-0087. A text that did not parse stays in its cell, marked, in
|
|
142
|
+
* the grid's own marks; anything staged into the cell ends that refusal,
|
|
143
|
+
* the value its row holds included. Whether there was one. */
|
|
144
|
+
_dropRefusal(rowIdx, colIdx) {
|
|
145
|
+
const k = key(rowIdx, colIdx);
|
|
146
|
+
if (!this.grid.errors.has(k)) return false;
|
|
147
|
+
this.grid.errors.delete(k);
|
|
148
|
+
this.grid.latest.delete(k);
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
|
|
134
152
|
/** One cell's new state and its delta in the ring; whether it changed. */
|
|
135
153
|
_stageOne(rowIdx, colIdx, value) {
|
|
136
154
|
const k = key(rowIdx, colIdx);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tsv.js — STORY-0085 (writing), STORY-0090 (reading). Tab-separated text,
|
|
3
|
+
* the format Excel and Google Sheets put on the clipboard as `text/plain` and
|
|
4
|
+
* read back from it. DataTable's cell mode copies a range with `toTSV` and
|
|
5
|
+
* pastes a block read with `fromTSV`.
|
|
6
|
+
*
|
|
7
|
+
* QUOTING. Excel quotes a cell that holds a tab, a newline (LF or CR) or a
|
|
8
|
+
* double quote, and doubles every quote inside it: `say "hi"` is written
|
|
9
|
+
* `"say ""hi"""`. Any other cell is written as it is, so a plain grid stays
|
|
10
|
+
* plain text. A writer that does not quote turns a cell holding a tab into two
|
|
11
|
+
* cells, and every column after it lands one to the right when pasted.
|
|
12
|
+
*
|
|
13
|
+
* LINES. Rows are joined with `\n` and there is no newline after the last
|
|
14
|
+
* row: one cell copied is that cell's text alone.
|
|
15
|
+
*
|
|
16
|
+
* READING. Character by character, never by splitting: a quoted cell may hold
|
|
17
|
+
* tabs and newlines, and a split that cannot know it shifts every column after
|
|
18
|
+
* such a cell into the wrong place, silently. A quote opens a quoted cell only
|
|
19
|
+
* at the start of a cell; inside one a doubled quote is a quote. A CR outside
|
|
20
|
+
* a quoted cell is dropped, so CRLF lines read as LF ones. Excel ends its text
|
|
21
|
+
* with a newline: the empty line that leaves (and any further empty lines at
|
|
22
|
+
* the end) is dropped, so a paste never carries a row nobody copied.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** One cell's text: `null` and `undefined` are empty, anything else is
|
|
26
|
+
* `String(value)`, quoted the way Excel quotes it when it has to be. */
|
|
27
|
+
export function tsvCell(value) {
|
|
28
|
+
const text = value === null || value === undefined ? '' : String(value);
|
|
29
|
+
if (/[\t\n\r"]/.test(text)) return `"${text.replace(/"/g, '""')}"`;
|
|
30
|
+
return text;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A matrix (an array of rows, each an array of cells) as TSV. */
|
|
34
|
+
export function toTSV(matrix) {
|
|
35
|
+
if (!Array.isArray(matrix)) return '';
|
|
36
|
+
return matrix
|
|
37
|
+
.map((row) => (Array.isArray(row) ? row : [row]).map(tsvCell).join('\t'))
|
|
38
|
+
.join('\n');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Tab-separated text as a matrix of strings, one array per line. Empty text
|
|
43
|
+
* is `[]`; a line with no tab is one cell; empty cells are kept (`'a\t\tb'`
|
|
44
|
+
* is three cells). Lines may differ in length: each is as long as it is.
|
|
45
|
+
*/
|
|
46
|
+
export function fromTSV(text) {
|
|
47
|
+
if (text === null || text === undefined || text === '') return [];
|
|
48
|
+
const src = String(text);
|
|
49
|
+
const rows = [];
|
|
50
|
+
let row = [];
|
|
51
|
+
let cell = '';
|
|
52
|
+
let quoted = false;
|
|
53
|
+
for (let i = 0; i < src.length; i++) {
|
|
54
|
+
const ch = src[i];
|
|
55
|
+
if (quoted) {
|
|
56
|
+
if (ch === '"' && src[i + 1] === '"') { cell += '"'; i++; }
|
|
57
|
+
else if (ch === '"') quoted = false;
|
|
58
|
+
else cell += ch;
|
|
59
|
+
} else if (ch === '"' && cell === '') quoted = true;
|
|
60
|
+
else if (ch === '\t') { row.push(cell); cell = ''; }
|
|
61
|
+
else if (ch === '\r') { /* CRLF reads as LF */ }
|
|
62
|
+
else if (ch === '\n') { row.push(cell); rows.push(row); row = []; cell = ''; }
|
|
63
|
+
else cell += ch;
|
|
64
|
+
}
|
|
65
|
+
row.push(cell);
|
|
66
|
+
rows.push(row);
|
|
67
|
+
// Excel's closing newline leaves an empty line behind: it is no row.
|
|
68
|
+
while (rows.length > 1) {
|
|
69
|
+
const last = rows[rows.length - 1];
|
|
70
|
+
if (last.length === 1 && last[0] === '') rows.pop();
|
|
71
|
+
else break;
|
|
72
|
+
}
|
|
73
|
+
return rows;
|
|
74
|
+
}
|
package/tiles.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @flexdesk/tiles — A draggable, resizable widget grid: the tile base class, the grid, the
|
|
3
|
+
* widget registry, layout persistence, a chart and a KPI tile (0.13.0), a list and a
|
|
4
|
+
* people tile and a board-wide scope picker (0.16.1), the figure tile every
|
|
5
|
+
* Plotly tile draws through and the config form (0.17.0).
|
|
6
|
+
*
|
|
7
|
+
* It stores layouts under keys YOU choose and resolves starting templates
|
|
8
|
+
* through a callback YOU supply. It has no opinion about what a layout is
|
|
9
|
+
* keyed by.
|
|
10
|
+
*
|
|
11
|
+
* GENERATED by scripts/gen_barrels.py. Do not hand-edit. (0.13.0: hand-edited to
|
|
12
|
+
* add chart_tiles.js — the generator is not in this repository; 0.16.1: list_tiles.js
|
|
13
|
+
* and scope_picker.js, the same way; 0.17.0: figure_tile.js and tile_config_form.js.)
|
|
14
|
+
*
|
|
15
|
+
* 10 modules.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export * from './src/tiles/chart_tiles.js';
|
|
19
|
+
export * from './src/tiles/config_schema.js';
|
|
20
|
+
export * from './src/tiles/figure_tile.js';
|
|
21
|
+
export * from './src/tiles/layout_persistence.js';
|
|
22
|
+
export * from './src/tiles/list_tiles.js';
|
|
23
|
+
export * from './src/tiles/scope_picker.js';
|
|
24
|
+
export * from './src/tiles/tile_base.js';
|
|
25
|
+
export * from './src/tiles/tile_config_form.js';
|
|
26
|
+
export * from './src/tiles/tile_grid.js';
|
|
27
|
+
export * from './src/tiles/tile_registry.js';
|
package/widgets.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @flexdesk/widgets — The widget set: DataTable, modals, managed windows, toasts, trees,
|
|
3
|
+
* context menus, menu bars, panels, section stacks, a step wizard, the help modal and the notification centre.
|
|
4
|
+
*
|
|
5
|
+
* GENERATED by scripts/gen_barrels.py. Do not hand-edit.
|
|
6
|
+
*
|
|
7
|
+
* 59 modules.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export * from './src/ui/base/component_base.js';
|
|
11
|
+
export * from './src/ui/base/controller_base.js';
|
|
12
|
+
export * from './src/ui/base/page_base.js';
|
|
13
|
+
export * from './src/ui/components/about_dialog.js';
|
|
14
|
+
export * from './src/ui/components/alert_log.js';
|
|
15
|
+
export * from './src/ui/components/alerts_pane.js';
|
|
16
|
+
export * from './src/ui/components/action_dropdown.js';
|
|
17
|
+
export * from './src/ui/components/attribute_list_editor.js';
|
|
18
|
+
export * from './src/ui/components/autocomplete_field.js';
|
|
19
|
+
export * from './src/ui/components/cell_editor_host.js';
|
|
20
|
+
export * from './src/ui/components/cell_editors.js';
|
|
21
|
+
export * from './src/ui/components/cell_selection.js';
|
|
22
|
+
export * from './src/ui/components/color_field.js';
|
|
23
|
+
export * from './src/ui/components/color_list_field.js';
|
|
24
|
+
export * from './src/ui/components/column_fit.js';
|
|
25
|
+
export * from './src/ui/components/computing_status_window.js';
|
|
26
|
+
export * from './src/ui/components/confirm_dialog.js';
|
|
27
|
+
export * from './src/ui/components/context_menu.js';
|
|
28
|
+
export * from './src/ui/components/conversation_view.js';
|
|
29
|
+
export * from './src/ui/components/data_table.js';
|
|
30
|
+
export * from './src/ui/components/detail_header.js';
|
|
31
|
+
export * from './src/ui/components/document_outline.js';
|
|
32
|
+
export * from './src/ui/components/drag_reorder.js';
|
|
33
|
+
export * from './src/ui/components/fact_table.js';
|
|
34
|
+
export * from './src/ui/components/gallery_picker.js';
|
|
35
|
+
export * from './src/ui/components/inline_renamer.js';
|
|
36
|
+
export * from './src/ui/components/managed_window.js';
|
|
37
|
+
export * from './src/ui/components/menu_bar.js';
|
|
38
|
+
export * from './src/ui/components/modal.js';
|
|
39
|
+
export * from './src/ui/components/notification_history.js';
|
|
40
|
+
export * from './src/ui/components/progress.js';
|
|
41
|
+
export * from './src/ui/components/record_list.js';
|
|
42
|
+
export * from './src/ui/components/section_stack.js';
|
|
43
|
+
export * from './src/ui/components/series.js';
|
|
44
|
+
export * from './src/ui/components/settings_view.js';
|
|
45
|
+
export * from './src/ui/components/slide_out_panel.js';
|
|
46
|
+
export * from './src/ui/components/slider_field.js';
|
|
47
|
+
export * from './src/ui/components/sortable_list.js';
|
|
48
|
+
export * from './src/ui/components/staged_history.js';
|
|
49
|
+
export * from './src/ui/components/table_state_store.js';
|
|
50
|
+
export * from './src/ui/components/tabs.js';
|
|
51
|
+
export * from './src/ui/components/tag_input.js';
|
|
52
|
+
export * from './src/ui/components/toast.js';
|
|
53
|
+
export * from './src/ui/components/toggle_field.js';
|
|
54
|
+
export * from './src/ui/components/tree_view.js';
|
|
55
|
+
export * from './src/ui/components/tsv.js';
|
|
56
|
+
export * from './src/ui/components/window_taskbar.js';
|
|
57
|
+
export * from './src/ui/components/wizard.js';
|
|
58
|
+
export * from './src/ui/controllers/window_chrome_controller.js';
|
|
59
|
+
export * from './src/ui/notification_center.js';
|
|
60
|
+
export * from './src/ui/utils/auto_scrollbars.js';
|
|
61
|
+
export * from './src/ui/utils/markdown.js';
|
|
62
|
+
export * from './src/ui/utils/native_context_menu.js';
|
|
63
|
+
export * from './src/ui/utils/overlay_scrollbar.js';
|
|
64
|
+
export * from './src/ui/utils/raf_resize_observer.js';
|
|
65
|
+
export * from './src/ui/utils/scroll_lock.js';
|
|
66
|
+
export * from './src/ui/utils/strip_scroll.js';
|
|
67
|
+
export * from './src/ui/utils/tooltip_service.js';
|
|
68
|
+
export * from './src/help/help_modal.js';
|
|
69
|
+
export * from './src/help/help_registry.js';
|
package/wm.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @flexdesk/wm — The tiling window manager: tiles, tabs, splits, desktops, the command
|
|
3
|
+
* palette, the taxonomy and entity registries, and createShell().
|
|
4
|
+
*
|
|
5
|
+
* GENERATED by scripts/gen_barrels.py. Do not hand-edit.
|
|
6
|
+
*
|
|
7
|
+
* 19 modules.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export * from './src/tiling/bottom_panel_tabs.js';
|
|
11
|
+
export * from './src/tiling/command_palette.js';
|
|
12
|
+
export * from './src/tiling/content_registry.js';
|
|
13
|
+
export * from './src/tiling/desktops.js';
|
|
14
|
+
export * from './src/tiling/entity_sources.js';
|
|
15
|
+
export * from './src/tiling/keymap.js';
|
|
16
|
+
export * from './src/tiling/kind_taxonomy.js';
|
|
17
|
+
export * from './src/tiling/landing_table.js';
|
|
18
|
+
export * from './src/tiling/loading_overlay.js';
|
|
19
|
+
export * from './src/tiling/nav_panel.js';
|
|
20
|
+
export * from './src/tiling/page_factory.js';
|
|
21
|
+
export * from './src/tiling/panel_keys.js';
|
|
22
|
+
export * from './src/tiling/shell.js';
|
|
23
|
+
export * from './src/tiling/tile_breadcrumb.js';
|
|
24
|
+
export * from './src/tiling/tile_renderer.js';
|
|
25
|
+
export * from './src/tiling/tile_tab_menu.js';
|
|
26
|
+
export * from './src/tiling/tile_tree.js';
|
|
27
|
+
export * from './src/tiling/wm.js';
|
|
28
|
+
export * from './src/tiling/zoom.js';
|