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
|
@@ -4,10 +4,11 @@
|
|
|
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
|
|
11
|
+
* - Column drag-to-reorder (`reorderable`, `columnOrder`; column_order.js)
|
|
11
12
|
* - Per-column filtering (numeric operators, text substring)
|
|
12
13
|
* - Column type detection (numeric/text alignment)
|
|
13
14
|
* - Header icons
|
|
@@ -25,6 +26,12 @@ import {
|
|
|
25
26
|
} from './table_values.js';
|
|
26
27
|
import { isRowActivation, isRowControl } from './row_activation.js';
|
|
27
28
|
import { createChipsFilter } from './table_filter_chips.js';
|
|
29
|
+
import { CellGrid } from './cell_grid.js';
|
|
30
|
+
import { toTSV } from './tsv.js';
|
|
31
|
+
import { ColumnReorder, cellOrder, isColumnOrder, moveInOrder, naturalOrder } from './column_order.js';
|
|
32
|
+
import { ColumnGroups } from './column_groups.js';
|
|
33
|
+
import { Crosstab } from './crosstab.js';
|
|
34
|
+
import { STRUCTURE_OPTIONS, TableStructure } from './table_structure.js';
|
|
28
35
|
|
|
29
36
|
// 0.7.0: the built-in chips filter control, for a consumer that wants to build
|
|
30
37
|
// one outside a table (the same control `filterKinds: {kind: 'chips'}` draws).
|
|
@@ -401,8 +408,48 @@ const AUTOSIZE_MAX_PX = 520;
|
|
|
401
408
|
* @property {boolean} [selectToggle=false] - STORY-0016. The table offers a SELECT MODE: a narrow leading column whose header holds a toggle button. While the mode is on, every row shows a checkbox there and the header a select-all box; a plain click (or Space) ticks or unticks one row, Shift-click adds the range from the last row ticked, and no gesture opens a row (`onRowActivate` is not called; `onRowClick` still is — a consumer that opens rows from it asks `isSelectMode()`). The arrows move a cursor without changing what is ticked. A right-click keeps the ticks. Escape clears the ticks, then leaves the mode. Needs a multi-row `selectable` (ignored, with a warning, on `false` or 'single'). The column is not resizable, is no data column (not in a copy, a CSV, `ctx.colIdx` or `hiddenColumns`), and remembered widths stay on their columns.
|
|
402
409
|
* @property {boolean} [selectMode=false] - STORY-0016. Start in select mode. Draws the select column even without `selectToggle` (then there is no toggle: the consumer turns the mode off with `setSelectMode(false)`).
|
|
403
410
|
* @property {Function} [onSelectModeChange] - STORY-0016. `(on) => void`, after every change of the mode (the toggle, Escape, `setSelectMode`).
|
|
404
|
-
* @property {Function} [onColumnsChange] - STORY-0012. `(hidden) => void` with the hidden columns' indices, ascending, after every change of the set (the chooser, `setColumnVisible`, `setData({hiddenColumns})`)
|
|
411
|
+
* @property {Function} [onColumnsChange] - STORY-0012. `(hidden, {hidden, order}) => void` with the hidden columns' indices, ascending, after every change of the set (the chooser, `setColumnVisible`, `setData({hiddenColumns})`) — and (STORY-0086) of the order (a drag, `moveColumn`, `setData({columnOrder})`); `order` is `getColumnOrder()`.
|
|
412
|
+
* @property {boolean} [reorderable=false] - STORY-0086. Data columns can be dragged into a new order by their headers (a press on a resize grip resizes instead), and the header's right-click offers Move left / Move right. The row-number and select columns stay first: they do not move and nothing is dropped in front of them. The order is kept by data index, so `hiddenColumns`, the sort and the filters stay on their columns; copies and the CSV follow the order shown. With `persistKey` it is saved, and a saved order wins over `columnOrder` — unless it is no order of today's columns (another column count), when it is ignored and `columnOrder` is drawn, or the natural order when there is none. The same holds when the saved order arrives before the headers do.
|
|
413
|
+
* @property {number[]} [columnOrder] - STORY-0086. Every data index, in the order to draw them (`[0, 3, 1, 2]`). Not a permutation of the columns: ignored with a console warning, natural order (with no headers yet, checked and warned once when `setData` brings them). Works without `reorderable` (then only code moves columns). See `moveColumn`, `getColumnOrder`.
|
|
405
414
|
* @property {boolean} [persistPage=false] - With `persistKey`: the client page is saved with the sort and the filters, and opened again on mount (on the last page, if the rows have shrunk since). OFF by default: a key shared by DIFFERENT row sets (ad-hoc filters, search results) must not reopen on a page of the last list. A sort under `resetPageOnSort` and a new filter save the first page, as they show it. A server's page (`onPageChange`, `dataSource`) is the consumer's and is not kept.
|
|
415
|
+
*
|
|
416
|
+
* ── STORY-0003, every one of them off unless asked for ─────────────────────
|
|
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`.
|
|
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.
|
|
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}`.
|
|
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.
|
|
424
|
+
* @property {Function} [onCellSelectionChange] - `(getCellSelection()) => void`, after every change of the cells selected.
|
|
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.
|
|
426
|
+
* @property {Function} [onHeaderMenuAction] - `(action, colIdx) => void`, when one of `headerMenu`'s items is chosen; the menu is hidden first.
|
|
427
|
+
*
|
|
428
|
+
* ── STORY-0088, off unless asked for ──────────────────────────────────────
|
|
429
|
+
* @property {boolean} [staged=false] - With `editable` (a warning, and ignored, without it): an editor's commit is STAGED instead of sent to `onCellCommit` (staged_edits.js). A staged cell shows its value drawn `twm-dt-cell--staged`; staging the value its row holds is no change. On the focused table Ctrl+Z takes the last staged edit back and Ctrl+Y (or Ctrl+Shift+Z) puts it back — a cell staged as `null` undoes to no change, not to a staged null. `commit()` sends every staged cell to `onCommit` at once; `discard()` drops them. While any cell is staged the sort, the filters and the pages are FROZEN: their controls are disabled with the reason on them (`data-refusal`, `title`), and `sortBy`, `setFilter`, `clearFilters`, `goToPage`, `goToRow` and the paging calls refuse with a console warning. New rows (`setData({rows})`) drop the staged cells and the ring.
|
|
430
|
+
* @property {number} [historyLimit=100] - With `staged`: the undo ring keeps this many entries — the limit counts entries, not cells; an entry is one cell's before/after change or one fill (a group of cell changes, undone together); the oldest fall off.
|
|
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.
|
|
432
|
+
* @property {Function} [onStagedChange] - With `staged`: `({count, canUndo, canRedo, committing}) => void` after every change of the staged cells.
|
|
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.
|
|
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."
|
|
438
|
+
*
|
|
439
|
+
* ── STORY-0066, every one of them off unless asked for. THE CONSUMER OWNS THE DATA: each edit is a call, answered with `setData` ──
|
|
440
|
+
* @property {Array<{key: string|number, label?: string, columns: number[]}>} [columnGroups] - COLUMN GROUPS (column_groups.js). A second header row above the column headers: one cell per run of shown columns of a group, spanning them, and a blank cell over the row number / select column and over columns in no group. It is a table of its own in the header wrap (`table.twm-dt-groups`, cells `th.twm-dt-group`) that takes the header's column widths after every fit, auto-size and drag, and scrolls sideways with it. A column is in the first group that lists it. A group with NO columns keeps one PLACEHOLDER column (`th.twm-dt-col--placeholder`, `td.twm-dt-cell--placeholder` in every row), after the columns of the groups listed before it, so its add control is reachable; it is no data column (not in a copy, the CSV, `ctx.colIdx` or cell mode). `setData({columnGroups})` changes them.
|
|
441
|
+
* @property {Function} [groupActions] - `(group, {key, columns, table}) => items`, buttons in a group's header cell (`{action, icon?, label?, title?, disabled?}`); a click calls `onGroupAction(action, groupKey)`.
|
|
442
|
+
* @property {Function} [onGroupAction] - `(action, groupKey) => void`.
|
|
443
|
+
* @property {Function} [onColumnAdd] - `(groupKey) => void`. Puts an add button (`.twm-dt-group__add`) in every group's header cell and in a placeholder column's header.
|
|
444
|
+
* @property {Function} [columnActions] - `(colIdx, {colIdx, header, table}) => items`, buttons in a data column's header (`.twm-dt-col-action`, same items as `groupActions`); a click calls `onColumnAction(action, colIdx)` and does not sort.
|
|
445
|
+
* @property {Function} [onColumnAction] - `(action, colIdx) => void`.
|
|
446
|
+
* @property {Function} [onColumnRename] - `(colIdx, name) => void | false | {ok: false} | Promise<…>`. RENAMING IN PLACE through `attachInlineRenamer`: "Rename column" in the header's right-click menu, `renameColumn(colIdx)`, or (on a table that is not `sortable`) a double-click on the header's label opens it. Enter, or the focus leaving, with a different non-empty name calls it once; Escape, or the name unchanged, restores the header and calls nothing. The new name shows until the next render; `false` or `{ok: false}` restores the old one.
|
|
447
|
+
* @property {Function} [onColumnDelete] - `(colIdx) => void`, from "Delete column" in the header's right-click menu.
|
|
448
|
+
* @property {Function} [onRowAdd] - `(afterRowIdx|null) => void`, from "Insert row below" in a row's right-click menu (its original index) and "Add row" in the header's (null), so a table with no rows can get its first.
|
|
449
|
+
* @property {Function} [onRowDelete] - `(rowIdx) => void`, from "Delete row" in a row's right-click menu.
|
|
450
|
+
*
|
|
451
|
+
* ── STORY-0091, off unless asked for ──────────────────────────────────────
|
|
452
|
+
* @property {{labelColumns?: number, headerRows?: Array<Array<{label: string, columns: number[], className?: string|string[], title?: string}>>, emptyLabel?: string}} [crosstab] - A CROSSTAB (crosstab.js): one table in one scroller, the `thead` kept in the body table. `headerRows` draws rows of spans above the column headers (top first; a cell spans the run of drawn body columns it lists, a run in no cell is blank), the leading `labelColumns` drawn data columns are row labels, and header rows stick to the top and label columns (behind the select and row-number columns) to the left at offsets MEASURED after layout and again when a header row or pinned header cell changes size; the corner sticks to both. With no body column, one column headed `emptyLabel` ('(none)'). No header table is built, so the column fit, resize grips, header menu (and the header items of `table_structure`), `columnActions`, `reorderable` and `columnGroups` (ignored, with a warning) do not apply. A row's background is `--twm-dt-row-bg` on its `tr`, which the pinned cells lay over their surface, with every cell tint as a layer above it.
|
|
406
453
|
*/
|
|
407
454
|
|
|
408
455
|
/** C10. When the scroll box has no layout — mounted hidden, or a DOM with no
|
|
@@ -410,9 +457,16 @@ const AUTOSIZE_MAX_PX = 520;
|
|
|
410
457
|
* it were this tall. A ResizeObserver corrects it the moment a real size exists. */
|
|
411
458
|
const VIRTUAL_FALLBACK_VIEWPORT_PX = 1000;
|
|
412
459
|
|
|
413
|
-
/** The copy menu
|
|
414
|
-
* `contextMenuItems`, and its tail with them (STORY-0010).
|
|
415
|
-
|
|
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 : `
|
|
416
470
|
<div class="twm-context-menu-item" data-action="copy-tsv">
|
|
417
471
|
<span class="material-symbols-outlined">content_copy</span>
|
|
418
472
|
<span class="label">Copy Row(s)</span>
|
|
@@ -548,13 +602,85 @@ export class DataTable {
|
|
|
548
602
|
hiddenColumns: null,
|
|
549
603
|
columnChooser: false,
|
|
550
604
|
onColumnsChange: null,
|
|
605
|
+
// STORY-0086. Off — columns in their natural order, as in 0.16.
|
|
606
|
+
reorderable: false,
|
|
607
|
+
columnOrder: null,
|
|
551
608
|
// STORY-0016. Off — no select column, as in 0.11 — until set.
|
|
552
609
|
selectToggle: false,
|
|
553
610
|
selectMode: false,
|
|
554
611
|
onSelectModeChange: null,
|
|
612
|
+
// STORY-0003. Off — rows selected as in 0.16, no editing — until
|
|
613
|
+
// set. The typedef says what each does.
|
|
614
|
+
selectionMode: 'rows',
|
|
615
|
+
editable: false,
|
|
616
|
+
editors: null,
|
|
617
|
+
editTypes: null,
|
|
618
|
+
locale: null,
|
|
619
|
+
editorMessages: null,
|
|
620
|
+
onCellCommit: null,
|
|
621
|
+
onCellSelectionChange: null,
|
|
622
|
+
headerMenu: null,
|
|
623
|
+
onHeaderMenuAction: null,
|
|
624
|
+
// STORY-0088. Off — commits go to onCellCommit — until set.
|
|
625
|
+
staged: false,
|
|
626
|
+
historyLimit: 100,
|
|
627
|
+
onCommit: null,
|
|
628
|
+
onStagedChange: null,
|
|
629
|
+
stagedRefusal: null,
|
|
630
|
+
// STORY-0066. Off — one header row, no structural edits — until
|
|
631
|
+
// set. The typedef says what each does.
|
|
632
|
+
columnGroups: null,
|
|
633
|
+
groupActions: null,
|
|
634
|
+
onGroupAction: null,
|
|
635
|
+
onColumnAdd: null,
|
|
636
|
+
columnActions: null,
|
|
637
|
+
onColumnAction: null,
|
|
638
|
+
onColumnRename: null,
|
|
639
|
+
onColumnDelete: null,
|
|
640
|
+
onRowAdd: null,
|
|
641
|
+
onRowDelete: null,
|
|
642
|
+
// STORY-0089. Off — no fill handle — until set.
|
|
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,
|
|
555
648
|
...config,
|
|
556
649
|
};
|
|
557
650
|
|
|
651
|
+
// STORY-0003. Cell mode is the table's for its whole life. The row
|
|
652
|
+
// modes it replaces are ignored beside it, out loud.
|
|
653
|
+
this._cellGrid = null;
|
|
654
|
+
// STORY-0066. Built the first time their options are set (see
|
|
655
|
+
// `_colGroups`, `_structure`); a table without them never builds either.
|
|
656
|
+
this._columnGroups = null;
|
|
657
|
+
this._tableStructure = null;
|
|
658
|
+
// STORY-0091. Built the first time `crosstab` is read (`_crosstab`).
|
|
659
|
+
this._crosstabLayout = null;
|
|
660
|
+
if (this.config.selectionMode === 'cells') {
|
|
661
|
+
const clash = ['selectToggle', 'selectMode', 'keyboardNav'].filter((k) => !!this.config[k]);
|
|
662
|
+
if (clash.length) {
|
|
663
|
+
console.warn(`[DataTable] ${clash.join('/')} cannot be combined with selectionMode: 'cells'; ignored.`);
|
|
664
|
+
for (const k of clash) this.config[k] = false;
|
|
665
|
+
}
|
|
666
|
+
this._cellGrid = new CellGrid(this);
|
|
667
|
+
} else if (this.config.editable) {
|
|
668
|
+
console.warn("[DataTable] `editable` needs selectionMode: 'cells'; ignored.");
|
|
669
|
+
}
|
|
670
|
+
if (this.config.staged && !this._cellGrid) {
|
|
671
|
+
console.warn('[DataTable] `staged` needs `editable`; ignored.');
|
|
672
|
+
}
|
|
673
|
+
if (this.config.fillHandle && !this._cellGrid) {
|
|
674
|
+
console.warn('[DataTable] `fillHandle` needs `staged`; ignored.');
|
|
675
|
+
}
|
|
676
|
+
if (this.config.pasteable && !this._cellGrid) {
|
|
677
|
+
console.warn('[DataTable] `pasteable` needs `staged`; ignored.');
|
|
678
|
+
}
|
|
679
|
+
// STORY-0091. A crosstab has no group row: its spans are `headerRows`.
|
|
680
|
+
if (Array.isArray(this.config.columnGroups) && this.config.columnGroups.length && this._crosstab()) {
|
|
681
|
+
console.warn('[DataTable] `columnGroups` cannot be combined with `crosstab` (use crosstab.headerRows); ignored.');
|
|
682
|
+
}
|
|
683
|
+
|
|
558
684
|
// STORY-0016. The select column is drawn for the table's whole life or
|
|
559
685
|
// not at all, so the drawn column indices (and the widths kept by them)
|
|
560
686
|
// never move when the mode flips.
|
|
@@ -584,6 +710,15 @@ export class DataTable {
|
|
|
584
710
|
// below, which may replace them with the remembered set.
|
|
585
711
|
this._hidden = new Set();
|
|
586
712
|
this._setHidden(this.config.hiddenColumns);
|
|
713
|
+
// STORY-0086. The order asked for (null: natural), before the restore
|
|
714
|
+
// below, which may replace it with the remembered one.
|
|
715
|
+
this._order = null;
|
|
716
|
+
this._askedOrder = null;
|
|
717
|
+
this._askedUnchecked = false;
|
|
718
|
+
if (!this._setOrder(this.config.columnOrder, true)) {
|
|
719
|
+
console.warn('[DataTable] columnOrder is not an order of the columns; ignored.');
|
|
720
|
+
}
|
|
721
|
+
this._reorder = this.config.reorderable ? new ColumnReorder(this) : null;
|
|
587
722
|
|
|
588
723
|
// 0.5.0. The open row's key, and the failure on show (`setError`).
|
|
589
724
|
this._activeKey = this.config.activeRow == null ? null : String(this.config.activeRow);
|
|
@@ -723,6 +858,11 @@ export class DataTable {
|
|
|
723
858
|
this._setHidden(blob.hidden);
|
|
724
859
|
applied = true;
|
|
725
860
|
}
|
|
861
|
+
// STORY-0086. The order, before the widths for the same reason. One
|
|
862
|
+
// saved for a different number of columns is no order of these.
|
|
863
|
+
if (Array.isArray(blob.order) && this._setOrder(blob.order)) {
|
|
864
|
+
applied = true;
|
|
865
|
+
}
|
|
726
866
|
if (blob.colWidths && typeof blob.colWidths === 'object') {
|
|
727
867
|
// Stored under string keys (JSON) → coerce back to ints.
|
|
728
868
|
this._colWidths = {};
|
|
@@ -756,7 +896,11 @@ export class DataTable {
|
|
|
756
896
|
+ (this.config.showRowNumbers ? '|#' : '')
|
|
757
897
|
// STORY-0012. Widths are by drawn column, so a different set drawn
|
|
758
898
|
// is a different schema — unless `setColumnVisible` carried them.
|
|
759
|
-
+ (this._hidden?.size ? `|-${this.getHiddenColumns().join(',')}` : '')
|
|
899
|
+
+ (this._hidden?.size ? `|-${this.getHiddenColumns().join(',')}` : '')
|
|
900
|
+
// STORY-0086. And a different order — unless `moveColumn` carried them.
|
|
901
|
+
+ (this._columnOrder() ? `|>${this._columnOrder().join(',')}` : '')
|
|
902
|
+
// STORY-0066. A placeholder column moves the drawn indices too.
|
|
903
|
+
+ (this._colGroups()?.sig() || '');
|
|
760
904
|
}
|
|
761
905
|
|
|
762
906
|
/** Snapshot the persistable slice of state to the project store.
|
|
@@ -777,6 +921,8 @@ export class DataTable {
|
|
|
777
921
|
// STORY-0012. Only for a table that hides columns or lets them be
|
|
778
922
|
// hidden.
|
|
779
923
|
...(this._columnsOn() ? { hidden: this.getHiddenColumns() } : {}),
|
|
924
|
+
// STORY-0086. Only for a table that orders its columns.
|
|
925
|
+
...(this._orderOn() ? { order: this.getColumnOrder() } : {}),
|
|
780
926
|
});
|
|
781
927
|
}
|
|
782
928
|
|
|
@@ -807,7 +953,36 @@ export class DataTable {
|
|
|
807
953
|
/** The cells a drawn row has: the shown columns, and the row number. */
|
|
808
954
|
_domColCount() {
|
|
809
955
|
const n = Math.max(0, (this.config.headers?.length || 0) - this._hidden.size);
|
|
810
|
-
|
|
956
|
+
// STORY-0066: an empty group's placeholder column is drawn too.
|
|
957
|
+
return n + this._lead().all + (this._colGroups()?.placeholderCount() || 0)
|
|
958
|
+
// STORY-0091: an empty axis's one column.
|
|
959
|
+
+ (this._crosstab()?.extraColumns() || 0);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/** STORY-0066. The column groups (column_groups.js) while `columnGroups`
|
|
963
|
+
* holds any; null otherwise. */
|
|
964
|
+
_colGroups() {
|
|
965
|
+
const spec = this.config.columnGroups;
|
|
966
|
+
if (!Array.isArray(spec) || !spec.length) return null;
|
|
967
|
+
// STORY-0091. A crosstab's spanned rows are its `headerRows`; the
|
|
968
|
+
// split layout's group row has no header table to stand in.
|
|
969
|
+
if (this._crosstab()) return null;
|
|
970
|
+
return (this._columnGroups ||= new ColumnGroups(this));
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/** STORY-0091. The crosstab layout (crosstab.js) while `crosstab` is an
|
|
974
|
+
* object; null otherwise. */
|
|
975
|
+
_crosstab() {
|
|
976
|
+
const spec = this.config.crosstab;
|
|
977
|
+
if (!spec || typeof spec !== 'object') return null;
|
|
978
|
+
return (this._crosstabLayout ||= new Crosstab(this));
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/** STORY-0066. The structural edits (table_structure.js) while any of
|
|
982
|
+
* their options is set; null otherwise. */
|
|
983
|
+
_structure() {
|
|
984
|
+
if (!STRUCTURE_OPTIONS.some((k) => typeof this.config[k] === 'function')) return null;
|
|
985
|
+
return (this._tableStructure ||= new TableStructure(this));
|
|
811
986
|
}
|
|
812
987
|
|
|
813
988
|
/** STORY-0016. The drawn columns in front of the data: the select column
|
|
@@ -817,25 +992,36 @@ export class DataTable {
|
|
|
817
992
|
return { select, all: select + (this.config.showRowNumbers ? 1 : 0) };
|
|
818
993
|
}
|
|
819
994
|
|
|
995
|
+
/** The data columns drawn, in the order drawn (STORY-0086): the order
|
|
996
|
+
* without the hidden ones. */
|
|
997
|
+
_shownColumns() {
|
|
998
|
+
return this._rowColumns(this.config.headers?.length || 0);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/** The data indices of a row of `count` cells to draw, in order: the
|
|
1002
|
+
* shown columns, and any cell past the headers after them. */
|
|
1003
|
+
_rowColumns(count) {
|
|
1004
|
+
return cellOrder(this._columnOrder(), count).filter((c) => !this._isHidden(c));
|
|
1005
|
+
}
|
|
1006
|
+
|
|
820
1007
|
/** A data column's drawn index (row number included), or -1 when hidden. */
|
|
821
1008
|
_domIdxOf(colIdx) {
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
1009
|
+
// STORY-0066. With a placeholder drawn, the groups' layout says.
|
|
1010
|
+
const slot = this._colGroups()?.slotOf(colIdx) ?? -1;
|
|
1011
|
+
if (slot >= 0) return this._lead().all + slot;
|
|
1012
|
+
const at = this._shownColumns().indexOf(colIdx);
|
|
1013
|
+
return at < 0 ? -1 : this._lead().all + at;
|
|
826
1014
|
}
|
|
827
1015
|
|
|
828
1016
|
/** A drawn cell's data column, or null for the row number. */
|
|
829
1017
|
_dataIdxOfDom(domIdx) {
|
|
830
|
-
|
|
1018
|
+
const i = domIdx - this._lead().all;
|
|
831
1019
|
if (i < 0) return null;
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
}
|
|
838
|
-
return null;
|
|
1020
|
+
// STORY-0066. With a placeholder drawn, the groups' layout says (and
|
|
1021
|
+
// a placeholder is no data column).
|
|
1022
|
+
const slot = this._colGroups()?.slotAt(i);
|
|
1023
|
+
if (slot !== undefined) return slot;
|
|
1024
|
+
return this._shownColumns()[i] ?? null;
|
|
839
1025
|
}
|
|
840
1026
|
|
|
841
1027
|
/** The columns not drawn, by data index, ascending. */
|
|
@@ -868,12 +1054,24 @@ export class DataTable {
|
|
|
868
1054
|
* that stay drawn to their new places, and tell whoever listens. */
|
|
869
1055
|
_applyHidden(next) {
|
|
870
1056
|
const before = this.getHiddenColumns();
|
|
1057
|
+
this._carryWidths(() => this._setHidden([...next]));
|
|
1058
|
+
const after = this.getHiddenColumns();
|
|
1059
|
+
if (before.join() === after.join()) return;
|
|
1060
|
+
this._savePersisted();
|
|
1061
|
+
if (this._wrapperEl) this.render();
|
|
1062
|
+
this._columnsChanged();
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/** STORY-0012/0086. Run `change` (a new hidden set or order), carrying
|
|
1066
|
+
* the drawn widths of the columns drawn before and after to their new
|
|
1067
|
+
* places. */
|
|
1068
|
+
_carryWidths(change) {
|
|
871
1069
|
const n = this.config.headers?.length || 0;
|
|
872
1070
|
const old = this._colWidths;
|
|
873
1071
|
const kept = this._colWidthsSig === this._colSig();
|
|
874
1072
|
const domBefore = [];
|
|
875
1073
|
for (let c = 0; c < n; c++) domBefore[c] = this._domIdxOf(c);
|
|
876
|
-
|
|
1074
|
+
change();
|
|
877
1075
|
if (kept) {
|
|
878
1076
|
const widths = {};
|
|
879
1077
|
for (let i = 0; i < this._lead().all; i++) if (old[i] != null) widths[i] = old[i];
|
|
@@ -885,21 +1083,109 @@ export class DataTable {
|
|
|
885
1083
|
this._colWidths = widths;
|
|
886
1084
|
this._colWidthsSig = this._colSig();
|
|
887
1085
|
}
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
/** `onColumnsChange(hidden, {hidden, order})`, after a change of either. */
|
|
1089
|
+
_columnsChanged() {
|
|
1090
|
+
const hidden = this.getHiddenColumns();
|
|
1091
|
+
try { this.config.onColumnsChange?.(hidden, { hidden, order: this.getColumnOrder() }); } catch (err) {
|
|
893
1092
|
console.error('[DataTable] onColumnsChange threw', err);
|
|
894
1093
|
}
|
|
895
1094
|
}
|
|
896
1095
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
1096
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1097
|
+
// STORY-0086 — the order the columns are drawn in (column_order.js)
|
|
1098
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1099
|
+
|
|
1100
|
+
/** Whether this table orders its columns at all (and so saves the order). */
|
|
1101
|
+
_orderOn() {
|
|
1102
|
+
return !!this.config.reorderable || Array.isArray(this.config.columnOrder) || !!this._orderTouched;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/** Take `list` as the order (`asked`: it is `columnOrder`, kept apart as
|
|
1106
|
+
* the fallback). Refused (false, nothing changed) when it is no
|
|
1107
|
+
* permutation of the columns; a table with no headers yet keeps it for
|
|
1108
|
+
* the headers `setData` brings, and `_checkAskedOrder` checks it then. */
|
|
1109
|
+
_setOrder(list, asked = false) {
|
|
1110
|
+
const n = this.config.headers?.length || 0;
|
|
1111
|
+
if (list != null && (!Array.isArray(list) || (n && !isColumnOrder(list, n)))) return false;
|
|
1112
|
+
this._order = list == null ? null : list.map(Number);
|
|
1113
|
+
if (asked) {
|
|
1114
|
+
this._askedOrder = this._order;
|
|
1115
|
+
this._askedUnchecked = !!this._order && !n;
|
|
1116
|
+
}
|
|
1117
|
+
return true;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
/** The first headers have come: a `columnOrder` taken before them, unchecked,
|
|
1121
|
+
* that is no order of them is dropped with the same warning a table with
|
|
1122
|
+
* headers gives at once — once, as the check is made once. */
|
|
1123
|
+
_checkAskedOrder() {
|
|
1124
|
+
const n = this.config.headers?.length || 0;
|
|
1125
|
+
if (!this._askedUnchecked || !n) return;
|
|
1126
|
+
this._askedUnchecked = false;
|
|
1127
|
+
if (isColumnOrder(this._askedOrder, n)) return;
|
|
1128
|
+
console.warn('[DataTable] columnOrder is not an order of the columns; ignored.');
|
|
1129
|
+
if (this._order === this._askedOrder) this._order = null;
|
|
1130
|
+
this._askedOrder = null;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
/** The order in force, or null for the natural one. One rule, whether the
|
|
1134
|
+
* headers were known when an order came or not: the first of the live
|
|
1135
|
+
* order (dragged, moved or restored) and `columnOrder` that is an order
|
|
1136
|
+
* of today's columns — so a saved order of the wrong length never
|
|
1137
|
+
* displaces `columnOrder`. */
|
|
1138
|
+
_columnOrder() {
|
|
1139
|
+
const n = this.config.headers?.length || 0;
|
|
1140
|
+
for (const o of [this._order, this._askedOrder]) {
|
|
1141
|
+
if (o && isColumnOrder(o, n)) return o;
|
|
1142
|
+
}
|
|
1143
|
+
return null;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
/**
|
|
1147
|
+
* Every data column's index, hidden ones included, in the order drawn.
|
|
1148
|
+
* @returns {number[]}
|
|
1149
|
+
*/
|
|
1150
|
+
getColumnOrder() {
|
|
1151
|
+
return (this._columnOrder() || naturalOrder(this.config.headers?.length || 0)).slice();
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
/**
|
|
1155
|
+
* Move the column at position `from` of `getColumnOrder()` to position
|
|
1156
|
+
* `to`, the others closing up behind it: from the natural order,
|
|
1157
|
+
* `moveColumn(3, 1)` draws 0, 3, 1, 2. The widths stay with their columns.
|
|
1158
|
+
* Saved under `persistKey`, and `onColumnsChange` is told.
|
|
1159
|
+
*
|
|
1160
|
+
* @param {number} from
|
|
1161
|
+
* @param {number} to
|
|
1162
|
+
* @returns {boolean} whether the order changed
|
|
1163
|
+
*/
|
|
1164
|
+
moveColumn(from, to) {
|
|
1165
|
+
const order = this.getColumnOrder();
|
|
1166
|
+
const n = order.length;
|
|
1167
|
+
if (![from, to].every((i) => Number.isInteger(i) && i >= 0 && i < n) || from === to) return false;
|
|
1168
|
+
this._applyOrder(moveInOrder(order, from, to));
|
|
1169
|
+
return true;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
/** Swap in a new order (a permutation, or null), widths carried, and
|
|
1173
|
+
* tell whoever listens. */
|
|
1174
|
+
_applyOrder(next) {
|
|
1175
|
+
const before = this.getColumnOrder().join();
|
|
1176
|
+
this._orderTouched = true;
|
|
1177
|
+
this._carryWidths(() => this._setOrder(next));
|
|
1178
|
+
if (this.getColumnOrder().join() === before) return;
|
|
1179
|
+
this._savePersisted();
|
|
1180
|
+
if (this._wrapperEl) this.render();
|
|
1181
|
+
this._columnsChanged();
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
/** `columnChooser`'s items: one checkable item a column. */
|
|
1185
|
+
_columnChooserItems() {
|
|
900
1186
|
const headers = this.config.headers || [];
|
|
901
1187
|
const lastShown = headers.length - this._hidden.size <= 1;
|
|
902
|
-
|
|
1188
|
+
return headers.map((h, c) => {
|
|
903
1189
|
const shown = !this._isHidden(c);
|
|
904
1190
|
return {
|
|
905
1191
|
label: String(h ?? ''),
|
|
@@ -910,7 +1196,69 @@ export class DataTable {
|
|
|
910
1196
|
onChoose: () => this.setColumnVisible(c, !shown),
|
|
911
1197
|
};
|
|
912
1198
|
});
|
|
913
|
-
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
/**
|
|
1202
|
+
* STORY-0003. The header's right-click menu for a press on `target`: the
|
|
1203
|
+
* consumer's `headerMenu` items for the data column under it, then — with
|
|
1204
|
+
* `columnChooser` — the chooser's items behind a separator. Empty when
|
|
1205
|
+
* there is nothing to show.
|
|
1206
|
+
*/
|
|
1207
|
+
_headerMenuItems(target) {
|
|
1208
|
+
const items = [];
|
|
1209
|
+
const hook = this.config.headerMenu;
|
|
1210
|
+
const colIdx = this._headerColumnAt(target);
|
|
1211
|
+
if (typeof hook === 'function' && colIdx !== null) {
|
|
1212
|
+
let asked = null;
|
|
1213
|
+
try {
|
|
1214
|
+
asked = hook(colIdx, {
|
|
1215
|
+
colIdx,
|
|
1216
|
+
header: this.config.headers?.[colIdx],
|
|
1217
|
+
hiddenColumns: this.getHiddenColumns(),
|
|
1218
|
+
table: this,
|
|
1219
|
+
});
|
|
1220
|
+
} catch (err) {
|
|
1221
|
+
console.error('[DataTable] headerMenu threw', err);
|
|
1222
|
+
}
|
|
1223
|
+
for (const item of Array.isArray(asked) ? asked.filter(Boolean) : []) {
|
|
1224
|
+
if (item.separator) { items.push({ separator: true }); continue; }
|
|
1225
|
+
items.push({
|
|
1226
|
+
...item,
|
|
1227
|
+
onChoose: () => {
|
|
1228
|
+
try { this.config.onHeaderMenuAction?.(item.action, colIdx); } catch (err) {
|
|
1229
|
+
console.error('[DataTable] onHeaderMenuAction threw', err);
|
|
1230
|
+
}
|
|
1231
|
+
},
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
// STORY-0086. Move left / Move right, for the data column under it.
|
|
1236
|
+
if (this._reorder && colIdx !== null) {
|
|
1237
|
+
const moves = this._reorder.menuItems(colIdx);
|
|
1238
|
+
if (moves.length && items.length) items.push({ separator: true });
|
|
1239
|
+
items.push(...moves);
|
|
1240
|
+
}
|
|
1241
|
+
// STORY-0066. Rename, delete, add a row.
|
|
1242
|
+
const structural = this._structure()?.headerItems(colIdx) || [];
|
|
1243
|
+
if (structural.length) {
|
|
1244
|
+
if (items.length) items.push({ separator: true });
|
|
1245
|
+
items.push(...structural);
|
|
1246
|
+
}
|
|
1247
|
+
if (this.config.columnChooser) {
|
|
1248
|
+
if (items.length) items.push({ separator: true });
|
|
1249
|
+
items.push(...this._columnChooserItems());
|
|
1250
|
+
}
|
|
1251
|
+
return items;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
/** The data column of a header cell (the header row, or the filter row
|
|
1255
|
+
* under it), or null off one — the row number, the select column. */
|
|
1256
|
+
_headerColumnAt(target) {
|
|
1257
|
+
const cell = target?.closest?.('th, td');
|
|
1258
|
+
const tr = cell?.parentNode;
|
|
1259
|
+
if (!tr || tr.parentNode?.tagName !== 'THEAD') return null;
|
|
1260
|
+
if (tr.children.length !== this._domColCount()) return null;
|
|
1261
|
+
return this._dataIdxOfDom(Array.prototype.indexOf.call(tr.children, cell));
|
|
914
1262
|
}
|
|
915
1263
|
|
|
916
1264
|
/** STORY-0010 `persistPage`: the client page changed, so save it. */
|
|
@@ -964,18 +1312,31 @@ export class DataTable {
|
|
|
964
1312
|
this._setHidden(updates.hiddenColumns);
|
|
965
1313
|
if (this.getHiddenColumns().join() !== before) {
|
|
966
1314
|
this._savePersisted();
|
|
967
|
-
|
|
968
|
-
console.error('[DataTable] onColumnsChange threw', err);
|
|
969
|
-
}
|
|
1315
|
+
this._columnsChanged();
|
|
970
1316
|
}
|
|
971
1317
|
} else if (updates.headers && this._hidden.size) {
|
|
972
1318
|
this._setHidden([...this._hidden]);
|
|
973
1319
|
}
|
|
1320
|
+
// STORY-0086. A new order asked for applies at once (refused, with a
|
|
1321
|
+
// warning, when it is no order of the columns); new headers keep the
|
|
1322
|
+
// order while it still fits them.
|
|
1323
|
+
if (Object.prototype.hasOwnProperty.call(updates, 'columnOrder')) {
|
|
1324
|
+
const before = this.getColumnOrder().join();
|
|
1325
|
+
if (!this._setOrder(updates.columnOrder, true)) {
|
|
1326
|
+
console.warn('[DataTable] columnOrder is not an order of the columns; ignored.');
|
|
1327
|
+
} else if (this.getColumnOrder().join() !== before) {
|
|
1328
|
+
this._savePersisted();
|
|
1329
|
+
this._columnsChanged();
|
|
1330
|
+
}
|
|
1331
|
+
} else if (updates.headers) {
|
|
1332
|
+
this._checkAskedOrder();
|
|
1333
|
+
}
|
|
974
1334
|
|
|
975
1335
|
// Reset state if rows changed
|
|
976
1336
|
if (updates.rows) {
|
|
977
1337
|
this._state.selected.clear();
|
|
978
1338
|
this._state.anchorIndex = null;
|
|
1339
|
+
this._cellGrid?.newRows();
|
|
979
1340
|
if (this._state.offset >= updates.rows.length) {
|
|
980
1341
|
this._state.offset = 0;
|
|
981
1342
|
}
|
|
@@ -1061,6 +1422,12 @@ export class DataTable {
|
|
|
1061
1422
|
* @param {number[]} indices - Row indices to select
|
|
1062
1423
|
*/
|
|
1063
1424
|
setSelection(indices) {
|
|
1425
|
+
// STORY-0003. In cell mode, the cells of those rows: one range from the
|
|
1426
|
+
// first of them to the last, as shown, across every column shown.
|
|
1427
|
+
if (this._cellGrid) {
|
|
1428
|
+
this._cellGrid.selectRows(indices);
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1064
1431
|
this._state.selected.clear();
|
|
1065
1432
|
for (const idx of indices) {
|
|
1066
1433
|
if (idx >= 0 && idx < this.config.rows.length) {
|
|
@@ -1151,17 +1518,146 @@ export class DataTable {
|
|
|
1151
1518
|
* Clear selection
|
|
1152
1519
|
*/
|
|
1153
1520
|
clearSelection() {
|
|
1521
|
+
if (this._cellGrid) {
|
|
1522
|
+
this._cellGrid.clear();
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1154
1525
|
this._state.selected.clear();
|
|
1155
1526
|
this._state.anchorIndex = null;
|
|
1156
1527
|
this._updateRowSelection();
|
|
1157
1528
|
this._notifySelectionChange();
|
|
1158
1529
|
}
|
|
1159
1530
|
|
|
1531
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1532
|
+
// STORY-0003 — cell mode (`selectionMode: 'cells'`), see cell_grid.js
|
|
1533
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1534
|
+
|
|
1535
|
+
/**
|
|
1536
|
+
* The cell selection: `{anchor, focus, range}`. `anchor` and `focus` (the
|
|
1537
|
+
* cursor) are `{row, col, rowIdx}` — `row` the position in the order
|
|
1538
|
+
* shown, `col` the data column, `rowIdx` the original row index — and
|
|
1539
|
+
* `range` is `{top, bottom, left, right, rows, columns}`: positions,
|
|
1540
|
+
* data columns at its edges, and the original row indices and data
|
|
1541
|
+
* columns it covers. All null with nothing selected. Null outside cell
|
|
1542
|
+
* mode.
|
|
1543
|
+
*/
|
|
1544
|
+
getCellSelection() {
|
|
1545
|
+
return this._cellGrid ? this._cellGrid.get() : null;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
/**
|
|
1549
|
+
* Put the cursor on a cell (`row` a position in the order shown, `col` a
|
|
1550
|
+
* data column) — with `anchor`, select the range from there. `null`
|
|
1551
|
+
* clears. The row is brought into view. A no-op outside cell mode.
|
|
1552
|
+
*
|
|
1553
|
+
* @param {{row:number, col:number}|null} focus
|
|
1554
|
+
* @param {{row:number, col:number}} [anchor]
|
|
1555
|
+
*/
|
|
1556
|
+
setCellSelection(focus, anchor = null) {
|
|
1557
|
+
this._cellGrid?.set(focus, anchor);
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
/** Open the editor on a cell (position, data column), as F2 would.
|
|
1561
|
+
* Returns whether one opened. */
|
|
1562
|
+
editCell(row, col) {
|
|
1563
|
+
return !!this._cellGrid?.open(row, col);
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
/** Whether a cell editor is open. */
|
|
1567
|
+
isEditing() {
|
|
1568
|
+
return !!this._cellGrid?.isEditing();
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
// ── STORY-0088 `staged` ──────────────────────────────────────────────
|
|
1572
|
+
|
|
1573
|
+
/**
|
|
1574
|
+
* Send every staged cell to `onCommit({edits})`, in one call. Resolves
|
|
1575
|
+
* `{ok: true, committed}` (the staged cells and the undo ring are
|
|
1576
|
+
* cleared), or `{ok: false, errors}` (the cells refused are marked and
|
|
1577
|
+
* every staged value stays). An open editor's value is staged first.
|
|
1578
|
+
* Without `staged`, resolves `{ok: false, errors: []}`.
|
|
1579
|
+
*
|
|
1580
|
+
* @returns {Promise<{ok: boolean, committed?: number, errors?: Array<{rowIdx:number, colIdx:number, error:string}>}>}
|
|
1581
|
+
*/
|
|
1582
|
+
commit() {
|
|
1583
|
+
const st = this._cellGrid?.staged;
|
|
1584
|
+
return st ? st.commit() : Promise.resolve({ ok: false, errors: [] });
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
/** Drop every staged cell, its marks and the undo ring. Returns whether
|
|
1588
|
+
* there was anything to drop. */
|
|
1589
|
+
discard() {
|
|
1590
|
+
return !!this._cellGrid?.staged?.discard();
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
/** Take the last staged edit back (Ctrl+Z). Returns whether it moved. */
|
|
1594
|
+
undo() {
|
|
1595
|
+
return !!this._cellGrid?.staged?.undo();
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
/** Put the last edit taken back back again (Ctrl+Y). */
|
|
1599
|
+
redo() {
|
|
1600
|
+
return !!this._cellGrid?.staged?.redo();
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
/** Every staged cell, `{rowIdx, colIdx, value, previous}`, by row then
|
|
1604
|
+
* column. Empty without `staged`. */
|
|
1605
|
+
getStagedEdits() {
|
|
1606
|
+
return this._cellGrid?.staged ? this._cellGrid.staged.edits() : [];
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
/**
|
|
1610
|
+
* STORY-0089. Fill from the cell range to the cell `to` (`{row, col}`:
|
|
1611
|
+
* position in the order shown, data column), as dragging the fill handle
|
|
1612
|
+
* there does: staged as ONE undo entry, Ctrl's repeat as `forceCopy`.
|
|
1613
|
+
* Returns `{direction, count, staged, skipped}` —
|
|
1614
|
+
* `staged` the NUMBER of cells that changed, `skipped` the cells `editable` refused
|
|
1615
|
+
* (`{row, col, rowIdx}`) — or null when nothing was filled (no
|
|
1616
|
+
* `fillHandle`, no range, `to` inside it, an editor open, a commit out).
|
|
1617
|
+
*/
|
|
1618
|
+
fill(to, { forceCopy = false } = {}) {
|
|
1619
|
+
return this._cellGrid?.fill ? this._cellGrid.fill.fill(to, { forceCopy }) : null;
|
|
1620
|
+
}
|
|
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
|
+
|
|
1641
|
+
/** Why sort, filters and pages are held — a staged edit — or null; with
|
|
1642
|
+
* a reason, `what` is refused out loud. */
|
|
1643
|
+
_stagedRefusal(what) {
|
|
1644
|
+
const why = this._cellGrid?.staged?.freezeReason() || null;
|
|
1645
|
+
if (why) console.warn(`[DataTable] ${what} refused: ${why}`);
|
|
1646
|
+
return why;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
/** STORY-0066. Open the inline renamer on a data column's header (needs
|
|
1650
|
+
* `onColumnRename`). Returns whether it opened. */
|
|
1651
|
+
renameColumn(colIdx) {
|
|
1652
|
+
return !!this._structure()?.rename(colIdx);
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1160
1655
|
/**
|
|
1161
1656
|
* Go to specific page
|
|
1162
1657
|
* @param {number} page - Page number (0-indexed)
|
|
1163
1658
|
*/
|
|
1164
1659
|
goToPage(page) {
|
|
1660
|
+
if (this._stagedRefusal('goToPage')) return;
|
|
1165
1661
|
// 0.7.0 `dataSource`: the page is the SOURCE's — asked for, and drawn
|
|
1166
1662
|
// when it answers (the promise). Past a known total, the last page.
|
|
1167
1663
|
// (The release review: this set an offset nothing under a data source
|
|
@@ -1185,6 +1681,7 @@ export class DataTable {
|
|
|
1185
1681
|
* @param {number} rowIndex - Row index
|
|
1186
1682
|
*/
|
|
1187
1683
|
goToRow(rowIndex) {
|
|
1684
|
+
if (this._stagedRefusal('goToRow')) return;
|
|
1188
1685
|
// 0.7.0 `dataSource`: the page that holds the row, from the source.
|
|
1189
1686
|
if (this.config.dataSource) {
|
|
1190
1687
|
const size = this._sourcePageSize();
|
|
@@ -1203,6 +1700,7 @@ export class DataTable {
|
|
|
1203
1700
|
* @param {boolean} [ascending] - Sort direction (toggles if same column)
|
|
1204
1701
|
*/
|
|
1205
1702
|
sortBy(colIndex, ascending) {
|
|
1703
|
+
if (this._stagedRefusal('sortBy')) return;
|
|
1206
1704
|
if (this._state.sortColumn === colIndex && ascending === undefined) {
|
|
1207
1705
|
this._state.sortAscending = !this._state.sortAscending;
|
|
1208
1706
|
} else {
|
|
@@ -1245,6 +1743,7 @@ export class DataTable {
|
|
|
1245
1743
|
* Clear all column filters
|
|
1246
1744
|
*/
|
|
1247
1745
|
clearFilters() {
|
|
1746
|
+
if (this._stagedRefusal('clearFilters')) return;
|
|
1248
1747
|
this._state.filters.clear();
|
|
1249
1748
|
this._processedRows = null;
|
|
1250
1749
|
this._processedIndexMap = null;
|
|
@@ -1310,6 +1809,12 @@ export class DataTable {
|
|
|
1310
1809
|
* Render the table
|
|
1311
1810
|
*/
|
|
1312
1811
|
render() {
|
|
1812
|
+
// STORY-0003. An open cell editor is not torn down by a redraw: the
|
|
1813
|
+
// render waits for its commit or cancel, and runs then.
|
|
1814
|
+
if (this._cellGrid?.isEditing()) {
|
|
1815
|
+
this._cellGrid.renderWaiting = true;
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1313
1818
|
// Capture active filter input before re-render
|
|
1314
1819
|
const activeFilterColIdx = this._getActiveFilterColIdx();
|
|
1315
1820
|
// KEYBOARD FOCUS SURVIVES A RE-RENDER. Rendering replaces the table
|
|
@@ -1433,7 +1938,9 @@ export class DataTable {
|
|
|
1433
1938
|
// empty one must keep the filter row — or a filter that matched nothing
|
|
1434
1939
|
// could never be cleared. It goes through `_createTable` and gets its
|
|
1435
1940
|
// empty row there.
|
|
1436
|
-
|
|
1941
|
+
// STORY-0091: a crosstab keeps its header rows and its corner too.
|
|
1942
|
+
const keepFilterRow = rows.length === 0
|
|
1943
|
+
&& ((!!this.config.dataSource && this.config.filterable) || !!this._crosstab());
|
|
1437
1944
|
if (rows.length === 0 && !keepFilterRow) {
|
|
1438
1945
|
// Empty state — render the same chrome as the data table
|
|
1439
1946
|
// (header row in its own non-scrolling wrap) followed by a
|
|
@@ -1456,13 +1963,12 @@ export class DataTable {
|
|
|
1456
1963
|
th.textContent = '#';
|
|
1457
1964
|
tr.appendChild(th);
|
|
1458
1965
|
}
|
|
1459
|
-
|
|
1460
|
-
if (this._isHidden(colIdx)) return;
|
|
1966
|
+
for (const colIdx of this._shownColumns()) {
|
|
1461
1967
|
const th = document.createElement('th');
|
|
1462
1968
|
th.className = this._columnTypes[colIdx] || 'text';
|
|
1463
|
-
th.textContent =
|
|
1969
|
+
th.textContent = headers[colIdx];
|
|
1464
1970
|
tr.appendChild(th);
|
|
1465
|
-
}
|
|
1971
|
+
}
|
|
1466
1972
|
thead.appendChild(tr);
|
|
1467
1973
|
headerTable.appendChild(thead);
|
|
1468
1974
|
headerWrap.appendChild(headerTable);
|
|
@@ -1489,7 +1995,15 @@ export class DataTable {
|
|
|
1489
1995
|
this._tableEl = fullTable;
|
|
1490
1996
|
// Extract thead → header table in its own non-scrolling
|
|
1491
1997
|
// container. The body table keeps tbody only.
|
|
1492
|
-
|
|
1998
|
+
// STORY-0091. A crosstab keeps its `thead` in the body table: one
|
|
1999
|
+
// table in one scroller, pinned by `position: sticky`.
|
|
2000
|
+
const crosstab = this._crosstab();
|
|
2001
|
+
const thead = crosstab ? null : fullTable.querySelector('thead');
|
|
2002
|
+
if (crosstab) {
|
|
2003
|
+
crosstab.decorate(fullTable);
|
|
2004
|
+
this._headerTableEl = null;
|
|
2005
|
+
this._headerWrapEl = null;
|
|
2006
|
+
}
|
|
1493
2007
|
if (thead) {
|
|
1494
2008
|
const headerWrap = document.createElement('div');
|
|
1495
2009
|
headerWrap.className = 'twm-preview-table-header-wrap';
|
|
@@ -1515,17 +2029,30 @@ export class DataTable {
|
|
|
1515
2029
|
}
|
|
1516
2030
|
|
|
1517
2031
|
this._tableWrapEl = tableWrap;
|
|
1518
|
-
// STORY-0012 `columnChooser`: the header's
|
|
1519
|
-
|
|
2032
|
+
// STORY-0012 `columnChooser` and STORY-0003 `headerMenu`: the header's
|
|
2033
|
+
// right-click. With neither item to show, the page's own menu.
|
|
2034
|
+
// STORY-0066: the groups' row and placeholder columns, the renamer's
|
|
2035
|
+
// labels and the consumer's column actions, on the header just built.
|
|
2036
|
+
this._colGroups()?.decorate(this._headerWrapEl, this._headerTableEl);
|
|
2037
|
+
this._structure()?.decorate(this._headerTableEl);
|
|
2038
|
+
if ((this.config.columnChooser || typeof this.config.headerMenu === 'function' || this._reorder
|
|
2039
|
+
|| this._structure()?.hasHeaderItems()) && this._headerTableEl) {
|
|
1520
2040
|
const head = this._headerTableEl;
|
|
1521
2041
|
const onHeadMenu = (ev) => {
|
|
2042
|
+
const items = this._headerMenuItems(ev.target);
|
|
2043
|
+
if (!items.length) return;
|
|
1522
2044
|
ev.preventDefault();
|
|
1523
2045
|
ev.stopPropagation();
|
|
1524
|
-
setTimeout(() => this.
|
|
2046
|
+
setTimeout(() => this._showContextMenu(ev.clientX, ev.clientY,
|
|
2047
|
+
{ ctx: null, items, copy: false }), 0);
|
|
1525
2048
|
};
|
|
1526
2049
|
head.addEventListener('contextmenu', onHeadMenu);
|
|
1527
2050
|
this._disposers.push(() => head.removeEventListener('contextmenu', onHeadMenu));
|
|
1528
2051
|
}
|
|
2052
|
+
// STORY-0086 `reorderable`: the header's drag.
|
|
2053
|
+
if (this._reorder && this._headerTableEl) {
|
|
2054
|
+
this._reorder.install(this._headerTableEl.querySelector('thead > tr'));
|
|
2055
|
+
}
|
|
1529
2056
|
// 0.7.0 `pagerPosition: 'bottom'`: under the rows.
|
|
1530
2057
|
if (this._paginationEl && this.config.pagerPosition === 'bottom') {
|
|
1531
2058
|
this._paginationEl.classList.add('twm-pagination-controls--bottom');
|
|
@@ -1541,6 +2068,8 @@ export class DataTable {
|
|
|
1541
2068
|
if (this._pagingEl) this._wrapperEl.appendChild(this._pagingEl);
|
|
1542
2069
|
}
|
|
1543
2070
|
this.container.appendChild(this._wrapperEl);
|
|
2071
|
+
// STORY-0091. The crosstab's offsets, measured now the table is in.
|
|
2072
|
+
this._crosstab()?.install();
|
|
1544
2073
|
// 0.6.1 `scrollLock`: THIS render's body box joins the group, and leaves
|
|
1545
2074
|
// it with the next render's cleanup or the dispose.
|
|
1546
2075
|
if (this.config.scrollLock && typeof this.config.scrollLock.attach === 'function') {
|
|
@@ -1618,7 +2147,8 @@ export class DataTable {
|
|
|
1618
2147
|
if (rows.length > 0 && (this.config.selectable || this.config.copyable || !this.config.readonly
|
|
1619
2148
|
|| typeof this.config.onRowActivate === 'function'
|
|
1620
2149
|
|| typeof this.config.rowDetail === 'function'
|
|
1621
|
-
|| typeof this.config.onRowKeyDown === 'function'
|
|
2150
|
+
|| typeof this.config.onRowKeyDown === 'function'
|
|
2151
|
+
|| this._cellGrid)) {
|
|
1622
2152
|
this._installInteractions();
|
|
1623
2153
|
}
|
|
1624
2154
|
|
|
@@ -1658,10 +2188,21 @@ export class DataTable {
|
|
|
1658
2188
|
}
|
|
1659
2189
|
|
|
1660
2190
|
/**
|
|
1661
|
-
* 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
|
|
1662
2194
|
* @param {'tsv'|'csv'} [format='tsv'] - Output format
|
|
1663
2195
|
*/
|
|
1664
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
|
+
}
|
|
1665
2206
|
const indices = this.getSelection();
|
|
1666
2207
|
if (indices.length === 0) {
|
|
1667
2208
|
this._notify('Copy', 'No rows selected.', 'warn');
|
|
@@ -1669,14 +2210,14 @@ export class DataTable {
|
|
|
1669
2210
|
}
|
|
1670
2211
|
|
|
1671
2212
|
const { headers, rows } = this.config;
|
|
1672
|
-
// STORY-0012. The columns shown — all of them, unless some are hidden
|
|
1673
|
-
|
|
1674
|
-
const matrix = [headers.
|
|
2213
|
+
// STORY-0012. The columns shown — all of them, unless some are hidden —
|
|
2214
|
+
// and (STORY-0086) in the order shown.
|
|
2215
|
+
const matrix = [this._rowColumns(headers.length).map((c) => headers[c])];
|
|
1675
2216
|
|
|
1676
2217
|
for (const idx of indices) {
|
|
1677
2218
|
const row = rows[idx];
|
|
1678
2219
|
if (row) {
|
|
1679
|
-
matrix.push(row.map((
|
|
2220
|
+
matrix.push(this._rowColumns(row.length).map((c) => this._formatValue(row[c], c)));
|
|
1680
2221
|
}
|
|
1681
2222
|
}
|
|
1682
2223
|
|
|
@@ -1704,6 +2245,36 @@ export class DataTable {
|
|
|
1704
2245
|
this._hideContextMenu();
|
|
1705
2246
|
}
|
|
1706
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
|
+
|
|
1707
2278
|
/**
|
|
1708
2279
|
* Download all data as CSV
|
|
1709
2280
|
* @param {string} [filename='data.csv'] - Filename for download
|
|
@@ -1715,12 +2286,11 @@ export class DataTable {
|
|
|
1715
2286
|
return;
|
|
1716
2287
|
}
|
|
1717
2288
|
|
|
1718
|
-
const
|
|
1719
|
-
const lines = [headers.filter(shown).join(',')];
|
|
2289
|
+
const lines = [this._rowColumns(headers.length).map((c) => headers[c]).join(',')];
|
|
1720
2290
|
for (const row of rows) {
|
|
1721
|
-
lines.push(row.map((
|
|
1722
|
-
this._csvEscape(this._formatValue(
|
|
1723
|
-
).
|
|
2291
|
+
lines.push(this._rowColumns(row.length).map((c) =>
|
|
2292
|
+
this._csvEscape(this._formatValue(row[c], c))
|
|
2293
|
+
).join(','));
|
|
1724
2294
|
}
|
|
1725
2295
|
|
|
1726
2296
|
const csv = lines.join('\n');
|
|
@@ -1783,6 +2353,10 @@ export class DataTable {
|
|
|
1783
2353
|
}
|
|
1784
2354
|
|
|
1785
2355
|
_teardown(emptyHost) {
|
|
2356
|
+
// STORY-0003. An open editor goes, committing nothing.
|
|
2357
|
+
this._cellGrid?.dispose();
|
|
2358
|
+
// STORY-0066. And an open column renamer.
|
|
2359
|
+
this._tableStructure?.dispose();
|
|
1786
2360
|
autoDisposeUnwatch(this);
|
|
1787
2361
|
this._autoSeen = false;
|
|
1788
2362
|
// 0.7.0. A grace still running, the follow timer, a request still out
|
|
@@ -2073,8 +2647,8 @@ export class DataTable {
|
|
|
2073
2647
|
tr.appendChild(th);
|
|
2074
2648
|
}
|
|
2075
2649
|
|
|
2076
|
-
|
|
2077
|
-
|
|
2650
|
+
this._shownColumns().forEach((colIdx) => {
|
|
2651
|
+
const header = headers[colIdx];
|
|
2078
2652
|
const th = document.createElement('th');
|
|
2079
2653
|
th.className = 'data-table__filter-cell';
|
|
2080
2654
|
|
|
@@ -2450,6 +3024,7 @@ export class DataTable {
|
|
|
2450
3024
|
}
|
|
2451
3025
|
|
|
2452
3026
|
_onFilterInput(colIdx, value) {
|
|
3027
|
+
if (this._stagedRefusal('A filter')) return;
|
|
2453
3028
|
// 0.7.0. A filter value need not be text (a control's array, a range),
|
|
2454
3029
|
// and an empty one of those is no filter. Text is judged as in 0.4.
|
|
2455
3030
|
const empty = typeof value === 'string' ? !value : this._isEmptyFilterValue(value);
|
|
@@ -2653,6 +3228,7 @@ export class DataTable {
|
|
|
2653
3228
|
|
|
2654
3229
|
// Page change handler - supports both client-side and server-side pagination
|
|
2655
3230
|
const handlePageChange = (newOffset) => {
|
|
3231
|
+
if (this._stagedRefusal('Turning the page')) return;
|
|
2656
3232
|
if (isServerSide) {
|
|
2657
3233
|
// Auto-spinner if the consumer returns a Promise.
|
|
2658
3234
|
this._awaitWithSpinner(onPageChange(newOffset, pageSize));
|
|
@@ -2730,8 +3306,11 @@ export class DataTable {
|
|
|
2730
3306
|
_createExportBtn() {
|
|
2731
3307
|
const name = this.config.exportFilename;
|
|
2732
3308
|
const filename = (typeof name === 'function' ? name(this) : name) || 'data.csv';
|
|
2733
|
-
|
|
2734
|
-
|
|
3309
|
+
const btn = this._createPaginationBtn('download', 'Download visible rows as CSV', false,
|
|
3310
|
+
() => this.downloadCSV(filename));
|
|
3311
|
+
// STORY-0088. Moves no rows, so a staged edit's freeze leaves it be.
|
|
3312
|
+
btn.classList.add('twm-pagination-btn--export');
|
|
3313
|
+
return btn;
|
|
2735
3314
|
}
|
|
2736
3315
|
|
|
2737
3316
|
/**
|
|
@@ -2871,6 +3450,7 @@ export class DataTable {
|
|
|
2871
3450
|
this._wrapperEl.appendChild(next);
|
|
2872
3451
|
}
|
|
2873
3452
|
this._pagingEl = next;
|
|
3453
|
+
this._cellGrid?.staged?.paintFreeze(); // STORY-0088
|
|
2874
3454
|
}
|
|
2875
3455
|
|
|
2876
3456
|
/**
|
|
@@ -2881,6 +3461,7 @@ export class DataTable {
|
|
|
2881
3461
|
*/
|
|
2882
3462
|
_pagingCall(kind, fn, ...args) {
|
|
2883
3463
|
if (typeof fn !== 'function' || this._pagingBusy) return;
|
|
3464
|
+
if (this._stagedRefusal(`paging ${kind}`)) return;
|
|
2884
3465
|
let result;
|
|
2885
3466
|
try {
|
|
2886
3467
|
result = fn(...args);
|
|
@@ -2912,6 +3493,9 @@ export class DataTable {
|
|
|
2912
3493
|
_pagingMaybeLoadMore() {
|
|
2913
3494
|
const p = this._pagingConfig();
|
|
2914
3495
|
if (!p || !p.hasMore || this._pagingBusy) return;
|
|
3496
|
+
// STORY-0088. Paging is held while cells are staged — quietly here:
|
|
3497
|
+
// a scroll is not a click on a control that could say why.
|
|
3498
|
+
if (this._cellGrid?.staged?.freezeReason()) return;
|
|
2915
3499
|
// 0.7.0: not while the source is answering another question (a sort, a
|
|
2916
3500
|
// filter, a refresh) — those rows are about to be replaced.
|
|
2917
3501
|
if (this.config.dataSource && this._source?.inFlight) return;
|
|
@@ -3081,6 +3665,11 @@ export class DataTable {
|
|
|
3081
3665
|
// hundred of a thousand.
|
|
3082
3666
|
const pageRows = isServerSide || !this.config.pagination || this._pagingConfig()
|
|
3083
3667
|
? processedRows : processedRows.slice(offset, offset + pageSize);
|
|
3668
|
+
// STORY-0003. Where the rows drawn stand in the order shown.
|
|
3669
|
+
this._pageSpan = {
|
|
3670
|
+
start: pageRows === processedRows ? 0 : offset,
|
|
3671
|
+
count: pageRows.length,
|
|
3672
|
+
};
|
|
3084
3673
|
|
|
3085
3674
|
const table = document.createElement('table');
|
|
3086
3675
|
table.className = this._tableClassName();
|
|
@@ -3098,8 +3687,8 @@ export class DataTable {
|
|
|
3098
3687
|
headerRow.appendChild(th);
|
|
3099
3688
|
}
|
|
3100
3689
|
|
|
3101
|
-
|
|
3102
|
-
|
|
3690
|
+
this._shownColumns().forEach((colIdx) => {
|
|
3691
|
+
const header = headers[colIdx];
|
|
3103
3692
|
const th = document.createElement('th');
|
|
3104
3693
|
th.className = this._columnTypes[colIdx] || 'text';
|
|
3105
3694
|
|
|
@@ -3217,8 +3806,11 @@ export class DataTable {
|
|
|
3217
3806
|
// 0.5.0. The row's place in the order shown (for 'position' numbers).
|
|
3218
3807
|
tr.__position = (this._positionBase || 0) + localIdx;
|
|
3219
3808
|
tr.className = 'data-preview-row';
|
|
3809
|
+
// STORY-0003. The row's position in the order shown, for cell mode.
|
|
3810
|
+
if (this._cellGrid) tr.__cellPos = (this._pageSpan?.start || 0) + localIdx;
|
|
3220
3811
|
|
|
3221
|
-
|
|
3812
|
+
// STORY-0003. In cell mode the CELLS are lit, not the rows.
|
|
3813
|
+
if (this._state.selected.has(globalIdx) && !this._cellGrid) {
|
|
3222
3814
|
tr.classList.add('selected');
|
|
3223
3815
|
}
|
|
3224
3816
|
// 0.5.0. The consumer's row classes and attributes, and the open mark.
|
|
@@ -3532,6 +4124,7 @@ export class DataTable {
|
|
|
3532
4124
|
const wanted = new Set(want);
|
|
3533
4125
|
for (const [i, tr] of v.map) {
|
|
3534
4126
|
if (!wanted.has(i)) {
|
|
4127
|
+
this._cellGrid?.rowLeaving(tr); // STORY-0003: an open editor commits
|
|
3535
4128
|
tr.remove();
|
|
3536
4129
|
this._removeDetailPieces(tr); // 0.7.0: its detail goes with it
|
|
3537
4130
|
v.map.delete(i);
|
|
@@ -3759,10 +4352,10 @@ export class DataTable {
|
|
|
3759
4352
|
const changedCells = diff?.cells
|
|
3760
4353
|
? (diff.cells instanceof Set ? diff.cells : new Set(diff.cells))
|
|
3761
4354
|
: null;
|
|
3762
|
-
for (
|
|
3763
|
-
if (this._isHidden(colIdx)) continue;
|
|
4355
|
+
for (const colIdx of this._rowColumns(row.length)) {
|
|
3764
4356
|
const td = document.createElement('td');
|
|
3765
4357
|
td.className = this._columnTypes[colIdx] || 'text';
|
|
4358
|
+
if (this._cellGrid) td.__colIdx = colIdx; // STORY-0003
|
|
3766
4359
|
|
|
3767
4360
|
// Use custom cell renderer if provided
|
|
3768
4361
|
const value = row[colIdx];
|
|
@@ -3813,6 +4406,12 @@ export class DataTable {
|
|
|
3813
4406
|
|
|
3814
4407
|
tr.appendChild(td);
|
|
3815
4408
|
}
|
|
4409
|
+
// STORY-0066. An empty group's placeholder column.
|
|
4410
|
+
this._colGroups()?.fillRow(tr);
|
|
4411
|
+
// STORY-0091. The crosstab's pinned cells, and an empty axis's cell.
|
|
4412
|
+
this._crosstab()?.fillRow(tr);
|
|
4413
|
+
// STORY-0003. The selection, and a commit's pending or refused mark.
|
|
4414
|
+
this._cellGrid?.decorateRow(tr);
|
|
3816
4415
|
}
|
|
3817
4416
|
|
|
3818
4417
|
/** Re-render ONE row in place, preserving everything around it.
|
|
@@ -3835,6 +4434,9 @@ export class DataTable {
|
|
|
3835
4434
|
* - keyboard focus, because the focused element's position is recorded
|
|
3836
4435
|
* before the replace and restored after.
|
|
3837
4436
|
*
|
|
4437
|
+
* STORY-0003: on the row an open cell editor is in, the data is stored
|
|
4438
|
+
* now and the row is drawn when the editor closes (true is returned).
|
|
4439
|
+
*
|
|
3838
4440
|
* @param {number} index row index as tracked by `tr.__rowIndex`
|
|
3839
4441
|
* @param {any[]} row the new cell values
|
|
3840
4442
|
* @returns {boolean} false when the row is not currently rendered
|
|
@@ -3867,6 +4469,9 @@ export class DataTable {
|
|
|
3867
4469
|
this.config.rows[index] = row;
|
|
3868
4470
|
}
|
|
3869
4471
|
if (this._virt) this._virtSetRow(index, row);
|
|
4472
|
+
// STORY-0003. The row holds an open cell editor: the rebuild would take
|
|
4473
|
+
// it out of the document, so the row is drawn when the editor closes.
|
|
4474
|
+
if (this._cellGrid?.holdRow(tr, index)) return true;
|
|
3870
4475
|
|
|
3871
4476
|
// Record focus BEFORE the replace: `replaceChildren` detaches the
|
|
3872
4477
|
// focused cell and the browser moves focus to <body>.
|
|
@@ -3913,7 +4518,7 @@ export class DataTable {
|
|
|
3913
4518
|
});
|
|
3914
4519
|
}
|
|
3915
4520
|
|
|
3916
|
-
tr.classList.toggle('selected', this._state?.selected?.has(index) === true);
|
|
4521
|
+
tr.classList.toggle('selected', this._state?.selected?.has(index) === true && !this._cellGrid);
|
|
3917
4522
|
// 0.5.0. The row's data changed, so its classes, attributes, key and
|
|
3918
4523
|
// open mark are asked again (a key paused, a delivery that died).
|
|
3919
4524
|
this._decorateRow(tr, row, index);
|
|
@@ -4473,6 +5078,7 @@ export class DataTable {
|
|
|
4473
5078
|
|
|
4474
5079
|
/** Go to the page at `offset` (paging back stops following). */
|
|
4475
5080
|
_sourceGoTo(offset) {
|
|
5081
|
+
if (this._stagedRefusal('Turning the page')) return undefined;
|
|
4476
5082
|
const next = Math.max(0, offset | 0);
|
|
4477
5083
|
// PAGING AWAY TURNS FOLLOWING OFF, and coming back does not turn it on
|
|
4478
5084
|
// again by itself: a reader who left the tail to read an old page is
|
|
@@ -4764,6 +5370,7 @@ export class DataTable {
|
|
|
4764
5370
|
const maxJump = Number.isFinite(c.maxJumpOffset) ? c.maxJumpOffset : null;
|
|
4765
5371
|
|
|
4766
5372
|
const go = (newOffset) => {
|
|
5373
|
+
if (this._stagedRefusal('Turning the page')) return;
|
|
4767
5374
|
if (source) { this._sourceGoTo(newOffset); return; }
|
|
4768
5375
|
if (server) { this._awaitWithSpinner(c.onPageChange(newOffset, pageSize)); return; }
|
|
4769
5376
|
this._state.offset = newOffset;
|
|
@@ -4953,6 +5560,7 @@ export class DataTable {
|
|
|
4953
5560
|
* value is whatever the column's control works in — text for the 0.4 box.
|
|
4954
5561
|
*/
|
|
4955
5562
|
setFilter(colIdx, value) {
|
|
5563
|
+
if (this._stagedRefusal('setFilter')) return;
|
|
4956
5564
|
const textBox = !this._filterKindFor(colIdx, (this.config.headers || [])[colIdx]);
|
|
4957
5565
|
const v = textBox && value != null && typeof value !== 'string' ? String(value) : value;
|
|
4958
5566
|
if (textBox) {
|
|
@@ -5101,6 +5709,8 @@ export class DataTable {
|
|
|
5101
5709
|
// STORY-0016. The select column, and whether the mode is on.
|
|
5102
5710
|
if (this._gutter) names.push('twm-dt--select');
|
|
5103
5711
|
if (this._selecting) names.push('twm-dt--selecting');
|
|
5712
|
+
// STORY-0003.
|
|
5713
|
+
if (this._cellGrid) names.push('twm-dt--cells');
|
|
5104
5714
|
return names.join(' ');
|
|
5105
5715
|
}
|
|
5106
5716
|
|
|
@@ -6056,6 +6666,9 @@ export class DataTable {
|
|
|
6056
6666
|
headerWrap.scrollLeft = x;
|
|
6057
6667
|
const residual = x - headerWrap.scrollLeft;
|
|
6058
6668
|
headerTable.style.transform = residual ? `translateX(${-residual}px)` : '';
|
|
6669
|
+
// STORY-0066. The group row follows the header: every width pass and
|
|
6670
|
+
// drag ends here, and so does every sideways scroll.
|
|
6671
|
+
this._colGroups()?.align();
|
|
6059
6672
|
}
|
|
6060
6673
|
|
|
6061
6674
|
/** Drag-to-resize: hang a thin grab handle off the right edge of
|
|
@@ -6261,11 +6874,17 @@ export class DataTable {
|
|
|
6261
6874
|
|
|
6262
6875
|
const { selectable, copyable } = this.config;
|
|
6263
6876
|
// STORY-0010. The consumer's own items install the menu by themselves.
|
|
6264
|
-
|
|
6877
|
+
// STORY-0066: the structural row items make a menu of their own.
|
|
6878
|
+
const customMenu = this.config.contextMenuItems != null || !!this._structure()?.hasRowItems();
|
|
6265
6879
|
const tbody = this._tbodyEl;
|
|
6266
6880
|
const table = this._tableEl;
|
|
6267
6881
|
|
|
6268
|
-
|
|
6882
|
+
// STORY-0003. Cell mode: the cells' own pointer and keys, bound before
|
|
6883
|
+
// the activation, detail and copy listeners so a key it takes stops.
|
|
6884
|
+
// The row click below is not cell mode's.
|
|
6885
|
+
if (this._cellGrid) this._cellGrid.install(tbody, table);
|
|
6886
|
+
|
|
6887
|
+
if (selectable && !this._cellGrid) {
|
|
6269
6888
|
const handleRowClick = (event) => {
|
|
6270
6889
|
const rowEl = event.target.closest('tr');
|
|
6271
6890
|
if (!rowEl || rowEl.__rowIndex === undefined) return;
|
|
@@ -6512,7 +7131,9 @@ export class DataTable {
|
|
|
6512
7131
|
} else if (key === 'a' && selectable) {
|
|
6513
7132
|
event.preventDefault();
|
|
6514
7133
|
this._selectAllShown();
|
|
6515
|
-
} 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.
|
|
6516
7137
|
event.preventDefault();
|
|
6517
7138
|
this.copyToClipboard(event.shiftKey ? 'csv' : 'tsv');
|
|
6518
7139
|
}
|
|
@@ -6537,7 +7158,7 @@ export class DataTable {
|
|
|
6537
7158
|
const idx = tr.__rowIndex;
|
|
6538
7159
|
if (idx !== undefined) {
|
|
6539
7160
|
const on = this._state.selected.has(idx);
|
|
6540
|
-
tr.classList.toggle('selected', on);
|
|
7161
|
+
tr.classList.toggle('selected', on && !this._cellGrid);
|
|
6541
7162
|
// STORY-0016.
|
|
6542
7163
|
const box = this._gutter ? tr.querySelector(':scope > td.twm-dt-select-cell > input') : null;
|
|
6543
7164
|
if (box) box.checked = on;
|
|
@@ -6663,7 +7284,7 @@ export class DataTable {
|
|
|
6663
7284
|
menu.style.display = 'none';
|
|
6664
7285
|
menu.style.position = 'fixed';
|
|
6665
7286
|
menu.style.zIndex = '10001';
|
|
6666
|
-
menu.innerHTML =
|
|
7287
|
+
menu.innerHTML = copyMenuHtml(!!this._cellGrid);
|
|
6667
7288
|
document.body.appendChild(menu);
|
|
6668
7289
|
|
|
6669
7290
|
menu.addEventListener('click', (event) => {
|
|
@@ -6818,7 +7439,11 @@ export class DataTable {
|
|
|
6818
7439
|
items = null;
|
|
6819
7440
|
}
|
|
6820
7441
|
}
|
|
6821
|
-
|
|
7442
|
+
const list = Array.isArray(items) ? items.filter(Boolean) : [];
|
|
7443
|
+
// STORY-0066. Insert and delete a row, after the consumer's items.
|
|
7444
|
+
const structural = this._structure()?.rowItems(ctx.rowIdx) || [];
|
|
7445
|
+
if (structural.length && list.length) list.push({ separator: true });
|
|
7446
|
+
return list.concat(structural);
|
|
6822
7447
|
}
|
|
6823
7448
|
|
|
6824
7449
|
/**
|
|
@@ -6832,7 +7457,7 @@ export class DataTable {
|
|
|
6832
7457
|
this._contextMenuCtx = custom ? custom.ctx : null;
|
|
6833
7458
|
if (!custom) {
|
|
6834
7459
|
if (this._contextMenuCustom) {
|
|
6835
|
-
menu.innerHTML =
|
|
7460
|
+
menu.innerHTML = copyMenuHtml(!!this._cellGrid);
|
|
6836
7461
|
this._contextMenuCustom = false;
|
|
6837
7462
|
}
|
|
6838
7463
|
return;
|
|
@@ -6877,7 +7502,7 @@ export class DataTable {
|
|
|
6877
7502
|
sep.className = 'twm-context-menu__separator';
|
|
6878
7503
|
menu.appendChild(sep);
|
|
6879
7504
|
}
|
|
6880
|
-
menu.insertAdjacentHTML('beforeend',
|
|
7505
|
+
menu.insertAdjacentHTML('beforeend', copyMenuHtml(!!this._cellGrid));
|
|
6881
7506
|
}
|
|
6882
7507
|
}
|
|
6883
7508
|
|