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.
@@ -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`' or the detected 'num'/'text'). A factory is `({value, row, rowIdx, colIdx, column: {index, header, type}, initialText, commit(value), cancel()}) => Element | {el, getValue?, focus?, destroy?}`; an input, select or textarea returned as it is commits its `value`. Absent: a text box (cell_editor_host.js `createTextEditor`), which commits the value unchanged when left as it was.
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's two items — the whole right-click menu of a table without
454
- * `contextMenuItems`, and its tail with them (STORY-0010). */
455
- const COPY_MENU_HTML = `
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 = COPY_MENU_HTML;
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 = COPY_MENU_HTML;
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', COPY_MENU_HTML);
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 || !this._stageOne(rowIdx, colIdx, value)) return false;
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';