flexdesk 0.16.1 → 0.17.0
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 +93 -0
- package/LICENSE +0 -0
- package/README.md +2217 -42
- package/THIRD_PARTY_LICENSES.md +0 -0
- package/css/base.css +1834 -75
- package/css/flexdesk.css +3146 -164
- 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 +709 -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-RQM5Z2MU.js → chunk-MCIXFNVF.js} +633 -311
- package/dist/chunk-MCIXFNVF.js.map +7 -0
- package/dist/chunk-N7HC3ADV.js +30 -0
- package/dist/chunk-N7HC3ADV.js.map +7 -0
- package/dist/{chunk-FBGP5KN5.js → chunk-Q36EPT2R.js} +3268 -203
- package/dist/chunk-Q36EPT2R.js.map +7 -0
- package/dist/chunk-QWQJUDPD.js +610 -0
- package/dist/chunk-QWQJUDPD.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 +3146 -164
- 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 +3252 -2124
- package/dist/widgets.js.map +4 -4
- package/dist/wm.js +1227 -347
- package/dist/wm.js.map +4 -4
- package/package.json +3 -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 +211 -0
- package/src/ui/components/cell_grid.js +648 -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 +601 -62
- 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 +343 -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/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/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
|
@@ -8,6 +8,7 @@
|
|
|
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,11 @@ 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 { ColumnReorder, cellOrder, isColumnOrder, moveInOrder, naturalOrder } from './column_order.js';
|
|
31
|
+
import { ColumnGroups } from './column_groups.js';
|
|
32
|
+
import { Crosstab } from './crosstab.js';
|
|
33
|
+
import { STRUCTURE_OPTIONS, TableStructure } from './table_structure.js';
|
|
28
34
|
|
|
29
35
|
// 0.7.0: the built-in chips filter control, for a consumer that wants to build
|
|
30
36
|
// one outside a table (the same control `filterKinds: {kind: 'chips'}` draws).
|
|
@@ -401,8 +407,42 @@ const AUTOSIZE_MAX_PX = 520;
|
|
|
401
407
|
* @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
408
|
* @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
409
|
* @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})`)
|
|
410
|
+
* @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()`.
|
|
411
|
+
* @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.
|
|
412
|
+
* @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
413
|
* @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.
|
|
414
|
+
*
|
|
415
|
+
* ── STORY-0003, every one of them off unless asked for ─────────────────────
|
|
416
|
+
* @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
|
+
* @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 {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
|
+
* @property {Function} [onCellSelectionChange] - `(getCellSelection()) => void`, after every change of the cells selected.
|
|
421
|
+
* @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.
|
|
422
|
+
* @property {Function} [onHeaderMenuAction] - `(action, colIdx) => void`, when one of `headerMenu`'s items is chosen; the menu is hidden first.
|
|
423
|
+
*
|
|
424
|
+
* ── STORY-0088, off unless asked for ──────────────────────────────────────
|
|
425
|
+
* @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.
|
|
426
|
+
* @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.
|
|
427
|
+
* @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
|
+
* @property {Function} [onStagedChange] - With `staged`: `({count, canUndo, canRedo, committing}) => void` after every change of the staged cells.
|
|
429
|
+
* @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`.
|
|
430
|
+
* @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
|
+
*
|
|
432
|
+
* ── STORY-0066, every one of them off unless asked for. THE CONSUMER OWNS THE DATA: each edit is a call, answered with `setData` ──
|
|
433
|
+
* @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.
|
|
434
|
+
* @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)`.
|
|
435
|
+
* @property {Function} [onGroupAction] - `(action, groupKey) => void`.
|
|
436
|
+
* @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.
|
|
437
|
+
* @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.
|
|
438
|
+
* @property {Function} [onColumnAction] - `(action, colIdx) => void`.
|
|
439
|
+
* @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.
|
|
440
|
+
* @property {Function} [onColumnDelete] - `(colIdx) => void`, from "Delete column" in the header's right-click menu.
|
|
441
|
+
* @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.
|
|
442
|
+
* @property {Function} [onRowDelete] - `(rowIdx) => void`, from "Delete row" in a row's right-click menu.
|
|
443
|
+
*
|
|
444
|
+
* ── STORY-0091, off unless asked for ──────────────────────────────────────
|
|
445
|
+
* @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
446
|
*/
|
|
407
447
|
|
|
408
448
|
/** C10. When the scroll box has no layout — mounted hidden, or a DOM with no
|
|
@@ -548,13 +588,75 @@ export class DataTable {
|
|
|
548
588
|
hiddenColumns: null,
|
|
549
589
|
columnChooser: false,
|
|
550
590
|
onColumnsChange: null,
|
|
591
|
+
// STORY-0086. Off — columns in their natural order, as in 0.16.
|
|
592
|
+
reorderable: false,
|
|
593
|
+
columnOrder: null,
|
|
551
594
|
// STORY-0016. Off — no select column, as in 0.11 — until set.
|
|
552
595
|
selectToggle: false,
|
|
553
596
|
selectMode: false,
|
|
554
597
|
onSelectModeChange: null,
|
|
598
|
+
// STORY-0003. Off — rows selected as in 0.16, no editing — until
|
|
599
|
+
// set. The typedef says what each does.
|
|
600
|
+
selectionMode: 'rows',
|
|
601
|
+
editable: false,
|
|
602
|
+
editors: null,
|
|
603
|
+
onCellCommit: null,
|
|
604
|
+
onCellSelectionChange: null,
|
|
605
|
+
headerMenu: null,
|
|
606
|
+
onHeaderMenuAction: null,
|
|
607
|
+
// STORY-0088. Off — commits go to onCellCommit — until set.
|
|
608
|
+
staged: false,
|
|
609
|
+
historyLimit: 100,
|
|
610
|
+
onCommit: null,
|
|
611
|
+
onStagedChange: null,
|
|
612
|
+
stagedRefusal: null,
|
|
613
|
+
// STORY-0066. Off — one header row, no structural edits — until
|
|
614
|
+
// set. The typedef says what each does.
|
|
615
|
+
columnGroups: null,
|
|
616
|
+
groupActions: null,
|
|
617
|
+
onGroupAction: null,
|
|
618
|
+
onColumnAdd: null,
|
|
619
|
+
columnActions: null,
|
|
620
|
+
onColumnAction: null,
|
|
621
|
+
onColumnRename: null,
|
|
622
|
+
onColumnDelete: null,
|
|
623
|
+
onRowAdd: null,
|
|
624
|
+
onRowDelete: null,
|
|
625
|
+
// STORY-0089. Off — no fill handle — until set.
|
|
626
|
+
fillHandle: false,
|
|
555
627
|
...config,
|
|
556
628
|
};
|
|
557
629
|
|
|
630
|
+
// STORY-0003. Cell mode is the table's for its whole life. The row
|
|
631
|
+
// modes it replaces are ignored beside it, out loud.
|
|
632
|
+
this._cellGrid = null;
|
|
633
|
+
// STORY-0066. Built the first time their options are set (see
|
|
634
|
+
// `_colGroups`, `_structure`); a table without them never builds either.
|
|
635
|
+
this._columnGroups = null;
|
|
636
|
+
this._tableStructure = null;
|
|
637
|
+
// STORY-0091. Built the first time `crosstab` is read (`_crosstab`).
|
|
638
|
+
this._crosstabLayout = null;
|
|
639
|
+
if (this.config.selectionMode === 'cells') {
|
|
640
|
+
const clash = ['selectToggle', 'selectMode', 'keyboardNav'].filter((k) => !!this.config[k]);
|
|
641
|
+
if (clash.length) {
|
|
642
|
+
console.warn(`[DataTable] ${clash.join('/')} cannot be combined with selectionMode: 'cells'; ignored.`);
|
|
643
|
+
for (const k of clash) this.config[k] = false;
|
|
644
|
+
}
|
|
645
|
+
this._cellGrid = new CellGrid(this);
|
|
646
|
+
} else if (this.config.editable) {
|
|
647
|
+
console.warn("[DataTable] `editable` needs selectionMode: 'cells'; ignored.");
|
|
648
|
+
}
|
|
649
|
+
if (this.config.staged && !this._cellGrid) {
|
|
650
|
+
console.warn('[DataTable] `staged` needs `editable`; ignored.');
|
|
651
|
+
}
|
|
652
|
+
if (this.config.fillHandle && !this._cellGrid) {
|
|
653
|
+
console.warn('[DataTable] `fillHandle` needs `staged`; ignored.');
|
|
654
|
+
}
|
|
655
|
+
// STORY-0091. A crosstab has no group row: its spans are `headerRows`.
|
|
656
|
+
if (Array.isArray(this.config.columnGroups) && this.config.columnGroups.length && this._crosstab()) {
|
|
657
|
+
console.warn('[DataTable] `columnGroups` cannot be combined with `crosstab` (use crosstab.headerRows); ignored.');
|
|
658
|
+
}
|
|
659
|
+
|
|
558
660
|
// STORY-0016. The select column is drawn for the table's whole life or
|
|
559
661
|
// not at all, so the drawn column indices (and the widths kept by them)
|
|
560
662
|
// never move when the mode flips.
|
|
@@ -584,6 +686,15 @@ export class DataTable {
|
|
|
584
686
|
// below, which may replace them with the remembered set.
|
|
585
687
|
this._hidden = new Set();
|
|
586
688
|
this._setHidden(this.config.hiddenColumns);
|
|
689
|
+
// STORY-0086. The order asked for (null: natural), before the restore
|
|
690
|
+
// below, which may replace it with the remembered one.
|
|
691
|
+
this._order = null;
|
|
692
|
+
this._askedOrder = null;
|
|
693
|
+
this._askedUnchecked = false;
|
|
694
|
+
if (!this._setOrder(this.config.columnOrder, true)) {
|
|
695
|
+
console.warn('[DataTable] columnOrder is not an order of the columns; ignored.');
|
|
696
|
+
}
|
|
697
|
+
this._reorder = this.config.reorderable ? new ColumnReorder(this) : null;
|
|
587
698
|
|
|
588
699
|
// 0.5.0. The open row's key, and the failure on show (`setError`).
|
|
589
700
|
this._activeKey = this.config.activeRow == null ? null : String(this.config.activeRow);
|
|
@@ -723,6 +834,11 @@ export class DataTable {
|
|
|
723
834
|
this._setHidden(blob.hidden);
|
|
724
835
|
applied = true;
|
|
725
836
|
}
|
|
837
|
+
// STORY-0086. The order, before the widths for the same reason. One
|
|
838
|
+
// saved for a different number of columns is no order of these.
|
|
839
|
+
if (Array.isArray(blob.order) && this._setOrder(blob.order)) {
|
|
840
|
+
applied = true;
|
|
841
|
+
}
|
|
726
842
|
if (blob.colWidths && typeof blob.colWidths === 'object') {
|
|
727
843
|
// Stored under string keys (JSON) → coerce back to ints.
|
|
728
844
|
this._colWidths = {};
|
|
@@ -756,7 +872,11 @@ export class DataTable {
|
|
|
756
872
|
+ (this.config.showRowNumbers ? '|#' : '')
|
|
757
873
|
// STORY-0012. Widths are by drawn column, so a different set drawn
|
|
758
874
|
// is a different schema — unless `setColumnVisible` carried them.
|
|
759
|
-
+ (this._hidden?.size ? `|-${this.getHiddenColumns().join(',')}` : '')
|
|
875
|
+
+ (this._hidden?.size ? `|-${this.getHiddenColumns().join(',')}` : '')
|
|
876
|
+
// STORY-0086. And a different order — unless `moveColumn` carried them.
|
|
877
|
+
+ (this._columnOrder() ? `|>${this._columnOrder().join(',')}` : '')
|
|
878
|
+
// STORY-0066. A placeholder column moves the drawn indices too.
|
|
879
|
+
+ (this._colGroups()?.sig() || '');
|
|
760
880
|
}
|
|
761
881
|
|
|
762
882
|
/** Snapshot the persistable slice of state to the project store.
|
|
@@ -777,6 +897,8 @@ export class DataTable {
|
|
|
777
897
|
// STORY-0012. Only for a table that hides columns or lets them be
|
|
778
898
|
// hidden.
|
|
779
899
|
...(this._columnsOn() ? { hidden: this.getHiddenColumns() } : {}),
|
|
900
|
+
// STORY-0086. Only for a table that orders its columns.
|
|
901
|
+
...(this._orderOn() ? { order: this.getColumnOrder() } : {}),
|
|
780
902
|
});
|
|
781
903
|
}
|
|
782
904
|
|
|
@@ -807,7 +929,36 @@ export class DataTable {
|
|
|
807
929
|
/** The cells a drawn row has: the shown columns, and the row number. */
|
|
808
930
|
_domColCount() {
|
|
809
931
|
const n = Math.max(0, (this.config.headers?.length || 0) - this._hidden.size);
|
|
810
|
-
|
|
932
|
+
// STORY-0066: an empty group's placeholder column is drawn too.
|
|
933
|
+
return n + this._lead().all + (this._colGroups()?.placeholderCount() || 0)
|
|
934
|
+
// STORY-0091: an empty axis's one column.
|
|
935
|
+
+ (this._crosstab()?.extraColumns() || 0);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/** STORY-0066. The column groups (column_groups.js) while `columnGroups`
|
|
939
|
+
* holds any; null otherwise. */
|
|
940
|
+
_colGroups() {
|
|
941
|
+
const spec = this.config.columnGroups;
|
|
942
|
+
if (!Array.isArray(spec) || !spec.length) return null;
|
|
943
|
+
// STORY-0091. A crosstab's spanned rows are its `headerRows`; the
|
|
944
|
+
// split layout's group row has no header table to stand in.
|
|
945
|
+
if (this._crosstab()) return null;
|
|
946
|
+
return (this._columnGroups ||= new ColumnGroups(this));
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/** STORY-0091. The crosstab layout (crosstab.js) while `crosstab` is an
|
|
950
|
+
* object; null otherwise. */
|
|
951
|
+
_crosstab() {
|
|
952
|
+
const spec = this.config.crosstab;
|
|
953
|
+
if (!spec || typeof spec !== 'object') return null;
|
|
954
|
+
return (this._crosstabLayout ||= new Crosstab(this));
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/** STORY-0066. The structural edits (table_structure.js) while any of
|
|
958
|
+
* their options is set; null otherwise. */
|
|
959
|
+
_structure() {
|
|
960
|
+
if (!STRUCTURE_OPTIONS.some((k) => typeof this.config[k] === 'function')) return null;
|
|
961
|
+
return (this._tableStructure ||= new TableStructure(this));
|
|
811
962
|
}
|
|
812
963
|
|
|
813
964
|
/** STORY-0016. The drawn columns in front of the data: the select column
|
|
@@ -817,25 +968,36 @@ export class DataTable {
|
|
|
817
968
|
return { select, all: select + (this.config.showRowNumbers ? 1 : 0) };
|
|
818
969
|
}
|
|
819
970
|
|
|
971
|
+
/** The data columns drawn, in the order drawn (STORY-0086): the order
|
|
972
|
+
* without the hidden ones. */
|
|
973
|
+
_shownColumns() {
|
|
974
|
+
return this._rowColumns(this.config.headers?.length || 0);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/** The data indices of a row of `count` cells to draw, in order: the
|
|
978
|
+
* shown columns, and any cell past the headers after them. */
|
|
979
|
+
_rowColumns(count) {
|
|
980
|
+
return cellOrder(this._columnOrder(), count).filter((c) => !this._isHidden(c));
|
|
981
|
+
}
|
|
982
|
+
|
|
820
983
|
/** A data column's drawn index (row number included), or -1 when hidden. */
|
|
821
984
|
_domIdxOf(colIdx) {
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
985
|
+
// STORY-0066. With a placeholder drawn, the groups' layout says.
|
|
986
|
+
const slot = this._colGroups()?.slotOf(colIdx) ?? -1;
|
|
987
|
+
if (slot >= 0) return this._lead().all + slot;
|
|
988
|
+
const at = this._shownColumns().indexOf(colIdx);
|
|
989
|
+
return at < 0 ? -1 : this._lead().all + at;
|
|
826
990
|
}
|
|
827
991
|
|
|
828
992
|
/** A drawn cell's data column, or null for the row number. */
|
|
829
993
|
_dataIdxOfDom(domIdx) {
|
|
830
|
-
|
|
994
|
+
const i = domIdx - this._lead().all;
|
|
831
995
|
if (i < 0) return null;
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
}
|
|
838
|
-
return null;
|
|
996
|
+
// STORY-0066. With a placeholder drawn, the groups' layout says (and
|
|
997
|
+
// a placeholder is no data column).
|
|
998
|
+
const slot = this._colGroups()?.slotAt(i);
|
|
999
|
+
if (slot !== undefined) return slot;
|
|
1000
|
+
return this._shownColumns()[i] ?? null;
|
|
839
1001
|
}
|
|
840
1002
|
|
|
841
1003
|
/** The columns not drawn, by data index, ascending. */
|
|
@@ -868,12 +1030,24 @@ export class DataTable {
|
|
|
868
1030
|
* that stay drawn to their new places, and tell whoever listens. */
|
|
869
1031
|
_applyHidden(next) {
|
|
870
1032
|
const before = this.getHiddenColumns();
|
|
1033
|
+
this._carryWidths(() => this._setHidden([...next]));
|
|
1034
|
+
const after = this.getHiddenColumns();
|
|
1035
|
+
if (before.join() === after.join()) return;
|
|
1036
|
+
this._savePersisted();
|
|
1037
|
+
if (this._wrapperEl) this.render();
|
|
1038
|
+
this._columnsChanged();
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
/** STORY-0012/0086. Run `change` (a new hidden set or order), carrying
|
|
1042
|
+
* the drawn widths of the columns drawn before and after to their new
|
|
1043
|
+
* places. */
|
|
1044
|
+
_carryWidths(change) {
|
|
871
1045
|
const n = this.config.headers?.length || 0;
|
|
872
1046
|
const old = this._colWidths;
|
|
873
1047
|
const kept = this._colWidthsSig === this._colSig();
|
|
874
1048
|
const domBefore = [];
|
|
875
1049
|
for (let c = 0; c < n; c++) domBefore[c] = this._domIdxOf(c);
|
|
876
|
-
|
|
1050
|
+
change();
|
|
877
1051
|
if (kept) {
|
|
878
1052
|
const widths = {};
|
|
879
1053
|
for (let i = 0; i < this._lead().all; i++) if (old[i] != null) widths[i] = old[i];
|
|
@@ -885,21 +1059,109 @@ export class DataTable {
|
|
|
885
1059
|
this._colWidths = widths;
|
|
886
1060
|
this._colWidthsSig = this._colSig();
|
|
887
1061
|
}
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
/** `onColumnsChange(hidden, {hidden, order})`, after a change of either. */
|
|
1065
|
+
_columnsChanged() {
|
|
1066
|
+
const hidden = this.getHiddenColumns();
|
|
1067
|
+
try { this.config.onColumnsChange?.(hidden, { hidden, order: this.getColumnOrder() }); } catch (err) {
|
|
893
1068
|
console.error('[DataTable] onColumnsChange threw', err);
|
|
894
1069
|
}
|
|
895
1070
|
}
|
|
896
1071
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
1072
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1073
|
+
// STORY-0086 — the order the columns are drawn in (column_order.js)
|
|
1074
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1075
|
+
|
|
1076
|
+
/** Whether this table orders its columns at all (and so saves the order). */
|
|
1077
|
+
_orderOn() {
|
|
1078
|
+
return !!this.config.reorderable || Array.isArray(this.config.columnOrder) || !!this._orderTouched;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
/** Take `list` as the order (`asked`: it is `columnOrder`, kept apart as
|
|
1082
|
+
* the fallback). Refused (false, nothing changed) when it is no
|
|
1083
|
+
* permutation of the columns; a table with no headers yet keeps it for
|
|
1084
|
+
* the headers `setData` brings, and `_checkAskedOrder` checks it then. */
|
|
1085
|
+
_setOrder(list, asked = false) {
|
|
1086
|
+
const n = this.config.headers?.length || 0;
|
|
1087
|
+
if (list != null && (!Array.isArray(list) || (n && !isColumnOrder(list, n)))) return false;
|
|
1088
|
+
this._order = list == null ? null : list.map(Number);
|
|
1089
|
+
if (asked) {
|
|
1090
|
+
this._askedOrder = this._order;
|
|
1091
|
+
this._askedUnchecked = !!this._order && !n;
|
|
1092
|
+
}
|
|
1093
|
+
return true;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/** The first headers have come: a `columnOrder` taken before them, unchecked,
|
|
1097
|
+
* that is no order of them is dropped with the same warning a table with
|
|
1098
|
+
* headers gives at once — once, as the check is made once. */
|
|
1099
|
+
_checkAskedOrder() {
|
|
1100
|
+
const n = this.config.headers?.length || 0;
|
|
1101
|
+
if (!this._askedUnchecked || !n) return;
|
|
1102
|
+
this._askedUnchecked = false;
|
|
1103
|
+
if (isColumnOrder(this._askedOrder, n)) return;
|
|
1104
|
+
console.warn('[DataTable] columnOrder is not an order of the columns; ignored.');
|
|
1105
|
+
if (this._order === this._askedOrder) this._order = null;
|
|
1106
|
+
this._askedOrder = null;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
/** The order in force, or null for the natural one. One rule, whether the
|
|
1110
|
+
* headers were known when an order came or not: the first of the live
|
|
1111
|
+
* order (dragged, moved or restored) and `columnOrder` that is an order
|
|
1112
|
+
* of today's columns — so a saved order of the wrong length never
|
|
1113
|
+
* displaces `columnOrder`. */
|
|
1114
|
+
_columnOrder() {
|
|
1115
|
+
const n = this.config.headers?.length || 0;
|
|
1116
|
+
for (const o of [this._order, this._askedOrder]) {
|
|
1117
|
+
if (o && isColumnOrder(o, n)) return o;
|
|
1118
|
+
}
|
|
1119
|
+
return null;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
/**
|
|
1123
|
+
* Every data column's index, hidden ones included, in the order drawn.
|
|
1124
|
+
* @returns {number[]}
|
|
1125
|
+
*/
|
|
1126
|
+
getColumnOrder() {
|
|
1127
|
+
return (this._columnOrder() || naturalOrder(this.config.headers?.length || 0)).slice();
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
/**
|
|
1131
|
+
* Move the column at position `from` of `getColumnOrder()` to position
|
|
1132
|
+
* `to`, the others closing up behind it: from the natural order,
|
|
1133
|
+
* `moveColumn(3, 1)` draws 0, 3, 1, 2. The widths stay with their columns.
|
|
1134
|
+
* Saved under `persistKey`, and `onColumnsChange` is told.
|
|
1135
|
+
*
|
|
1136
|
+
* @param {number} from
|
|
1137
|
+
* @param {number} to
|
|
1138
|
+
* @returns {boolean} whether the order changed
|
|
1139
|
+
*/
|
|
1140
|
+
moveColumn(from, to) {
|
|
1141
|
+
const order = this.getColumnOrder();
|
|
1142
|
+
const n = order.length;
|
|
1143
|
+
if (![from, to].every((i) => Number.isInteger(i) && i >= 0 && i < n) || from === to) return false;
|
|
1144
|
+
this._applyOrder(moveInOrder(order, from, to));
|
|
1145
|
+
return true;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
/** Swap in a new order (a permutation, or null), widths carried, and
|
|
1149
|
+
* tell whoever listens. */
|
|
1150
|
+
_applyOrder(next) {
|
|
1151
|
+
const before = this.getColumnOrder().join();
|
|
1152
|
+
this._orderTouched = true;
|
|
1153
|
+
this._carryWidths(() => this._setOrder(next));
|
|
1154
|
+
if (this.getColumnOrder().join() === before) return;
|
|
1155
|
+
this._savePersisted();
|
|
1156
|
+
if (this._wrapperEl) this.render();
|
|
1157
|
+
this._columnsChanged();
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
/** `columnChooser`'s items: one checkable item a column. */
|
|
1161
|
+
_columnChooserItems() {
|
|
900
1162
|
const headers = this.config.headers || [];
|
|
901
1163
|
const lastShown = headers.length - this._hidden.size <= 1;
|
|
902
|
-
|
|
1164
|
+
return headers.map((h, c) => {
|
|
903
1165
|
const shown = !this._isHidden(c);
|
|
904
1166
|
return {
|
|
905
1167
|
label: String(h ?? ''),
|
|
@@ -910,7 +1172,69 @@ export class DataTable {
|
|
|
910
1172
|
onChoose: () => this.setColumnVisible(c, !shown),
|
|
911
1173
|
};
|
|
912
1174
|
});
|
|
913
|
-
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
/**
|
|
1178
|
+
* STORY-0003. The header's right-click menu for a press on `target`: the
|
|
1179
|
+
* consumer's `headerMenu` items for the data column under it, then — with
|
|
1180
|
+
* `columnChooser` — the chooser's items behind a separator. Empty when
|
|
1181
|
+
* there is nothing to show.
|
|
1182
|
+
*/
|
|
1183
|
+
_headerMenuItems(target) {
|
|
1184
|
+
const items = [];
|
|
1185
|
+
const hook = this.config.headerMenu;
|
|
1186
|
+
const colIdx = this._headerColumnAt(target);
|
|
1187
|
+
if (typeof hook === 'function' && colIdx !== null) {
|
|
1188
|
+
let asked = null;
|
|
1189
|
+
try {
|
|
1190
|
+
asked = hook(colIdx, {
|
|
1191
|
+
colIdx,
|
|
1192
|
+
header: this.config.headers?.[colIdx],
|
|
1193
|
+
hiddenColumns: this.getHiddenColumns(),
|
|
1194
|
+
table: this,
|
|
1195
|
+
});
|
|
1196
|
+
} catch (err) {
|
|
1197
|
+
console.error('[DataTable] headerMenu threw', err);
|
|
1198
|
+
}
|
|
1199
|
+
for (const item of Array.isArray(asked) ? asked.filter(Boolean) : []) {
|
|
1200
|
+
if (item.separator) { items.push({ separator: true }); continue; }
|
|
1201
|
+
items.push({
|
|
1202
|
+
...item,
|
|
1203
|
+
onChoose: () => {
|
|
1204
|
+
try { this.config.onHeaderMenuAction?.(item.action, colIdx); } catch (err) {
|
|
1205
|
+
console.error('[DataTable] onHeaderMenuAction threw', err);
|
|
1206
|
+
}
|
|
1207
|
+
},
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
// STORY-0086. Move left / Move right, for the data column under it.
|
|
1212
|
+
if (this._reorder && colIdx !== null) {
|
|
1213
|
+
const moves = this._reorder.menuItems(colIdx);
|
|
1214
|
+
if (moves.length && items.length) items.push({ separator: true });
|
|
1215
|
+
items.push(...moves);
|
|
1216
|
+
}
|
|
1217
|
+
// STORY-0066. Rename, delete, add a row.
|
|
1218
|
+
const structural = this._structure()?.headerItems(colIdx) || [];
|
|
1219
|
+
if (structural.length) {
|
|
1220
|
+
if (items.length) items.push({ separator: true });
|
|
1221
|
+
items.push(...structural);
|
|
1222
|
+
}
|
|
1223
|
+
if (this.config.columnChooser) {
|
|
1224
|
+
if (items.length) items.push({ separator: true });
|
|
1225
|
+
items.push(...this._columnChooserItems());
|
|
1226
|
+
}
|
|
1227
|
+
return items;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
/** The data column of a header cell (the header row, or the filter row
|
|
1231
|
+
* under it), or null off one — the row number, the select column. */
|
|
1232
|
+
_headerColumnAt(target) {
|
|
1233
|
+
const cell = target?.closest?.('th, td');
|
|
1234
|
+
const tr = cell?.parentNode;
|
|
1235
|
+
if (!tr || tr.parentNode?.tagName !== 'THEAD') return null;
|
|
1236
|
+
if (tr.children.length !== this._domColCount()) return null;
|
|
1237
|
+
return this._dataIdxOfDom(Array.prototype.indexOf.call(tr.children, cell));
|
|
914
1238
|
}
|
|
915
1239
|
|
|
916
1240
|
/** STORY-0010 `persistPage`: the client page changed, so save it. */
|
|
@@ -964,18 +1288,31 @@ export class DataTable {
|
|
|
964
1288
|
this._setHidden(updates.hiddenColumns);
|
|
965
1289
|
if (this.getHiddenColumns().join() !== before) {
|
|
966
1290
|
this._savePersisted();
|
|
967
|
-
|
|
968
|
-
console.error('[DataTable] onColumnsChange threw', err);
|
|
969
|
-
}
|
|
1291
|
+
this._columnsChanged();
|
|
970
1292
|
}
|
|
971
1293
|
} else if (updates.headers && this._hidden.size) {
|
|
972
1294
|
this._setHidden([...this._hidden]);
|
|
973
1295
|
}
|
|
1296
|
+
// STORY-0086. A new order asked for applies at once (refused, with a
|
|
1297
|
+
// warning, when it is no order of the columns); new headers keep the
|
|
1298
|
+
// order while it still fits them.
|
|
1299
|
+
if (Object.prototype.hasOwnProperty.call(updates, 'columnOrder')) {
|
|
1300
|
+
const before = this.getColumnOrder().join();
|
|
1301
|
+
if (!this._setOrder(updates.columnOrder, true)) {
|
|
1302
|
+
console.warn('[DataTable] columnOrder is not an order of the columns; ignored.');
|
|
1303
|
+
} else if (this.getColumnOrder().join() !== before) {
|
|
1304
|
+
this._savePersisted();
|
|
1305
|
+
this._columnsChanged();
|
|
1306
|
+
}
|
|
1307
|
+
} else if (updates.headers) {
|
|
1308
|
+
this._checkAskedOrder();
|
|
1309
|
+
}
|
|
974
1310
|
|
|
975
1311
|
// Reset state if rows changed
|
|
976
1312
|
if (updates.rows) {
|
|
977
1313
|
this._state.selected.clear();
|
|
978
1314
|
this._state.anchorIndex = null;
|
|
1315
|
+
this._cellGrid?.newRows();
|
|
979
1316
|
if (this._state.offset >= updates.rows.length) {
|
|
980
1317
|
this._state.offset = 0;
|
|
981
1318
|
}
|
|
@@ -1061,6 +1398,12 @@ export class DataTable {
|
|
|
1061
1398
|
* @param {number[]} indices - Row indices to select
|
|
1062
1399
|
*/
|
|
1063
1400
|
setSelection(indices) {
|
|
1401
|
+
// STORY-0003. In cell mode, the cells of those rows: one range from the
|
|
1402
|
+
// first of them to the last, as shown, across every column shown.
|
|
1403
|
+
if (this._cellGrid) {
|
|
1404
|
+
this._cellGrid.selectRows(indices);
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1064
1407
|
this._state.selected.clear();
|
|
1065
1408
|
for (const idx of indices) {
|
|
1066
1409
|
if (idx >= 0 && idx < this.config.rows.length) {
|
|
@@ -1151,17 +1494,127 @@ export class DataTable {
|
|
|
1151
1494
|
* Clear selection
|
|
1152
1495
|
*/
|
|
1153
1496
|
clearSelection() {
|
|
1497
|
+
if (this._cellGrid) {
|
|
1498
|
+
this._cellGrid.clear();
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1154
1501
|
this._state.selected.clear();
|
|
1155
1502
|
this._state.anchorIndex = null;
|
|
1156
1503
|
this._updateRowSelection();
|
|
1157
1504
|
this._notifySelectionChange();
|
|
1158
1505
|
}
|
|
1159
1506
|
|
|
1507
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1508
|
+
// STORY-0003 — cell mode (`selectionMode: 'cells'`), see cell_grid.js
|
|
1509
|
+
// ─────────────────────────────────────────────────────────────────
|
|
1510
|
+
|
|
1511
|
+
/**
|
|
1512
|
+
* The cell selection: `{anchor, focus, range}`. `anchor` and `focus` (the
|
|
1513
|
+
* cursor) are `{row, col, rowIdx}` — `row` the position in the order
|
|
1514
|
+
* shown, `col` the data column, `rowIdx` the original row index — and
|
|
1515
|
+
* `range` is `{top, bottom, left, right, rows, columns}`: positions,
|
|
1516
|
+
* data columns at its edges, and the original row indices and data
|
|
1517
|
+
* columns it covers. All null with nothing selected. Null outside cell
|
|
1518
|
+
* mode.
|
|
1519
|
+
*/
|
|
1520
|
+
getCellSelection() {
|
|
1521
|
+
return this._cellGrid ? this._cellGrid.get() : null;
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
/**
|
|
1525
|
+
* Put the cursor on a cell (`row` a position in the order shown, `col` a
|
|
1526
|
+
* data column) — with `anchor`, select the range from there. `null`
|
|
1527
|
+
* clears. The row is brought into view. A no-op outside cell mode.
|
|
1528
|
+
*
|
|
1529
|
+
* @param {{row:number, col:number}|null} focus
|
|
1530
|
+
* @param {{row:number, col:number}} [anchor]
|
|
1531
|
+
*/
|
|
1532
|
+
setCellSelection(focus, anchor = null) {
|
|
1533
|
+
this._cellGrid?.set(focus, anchor);
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
/** Open the editor on a cell (position, data column), as F2 would.
|
|
1537
|
+
* Returns whether one opened. */
|
|
1538
|
+
editCell(row, col) {
|
|
1539
|
+
return !!this._cellGrid?.open(row, col);
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
/** Whether a cell editor is open. */
|
|
1543
|
+
isEditing() {
|
|
1544
|
+
return !!this._cellGrid?.isEditing();
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
// ── STORY-0088 `staged` ──────────────────────────────────────────────
|
|
1548
|
+
|
|
1549
|
+
/**
|
|
1550
|
+
* Send every staged cell to `onCommit({edits})`, in one call. Resolves
|
|
1551
|
+
* `{ok: true, committed}` (the staged cells and the undo ring are
|
|
1552
|
+
* cleared), or `{ok: false, errors}` (the cells refused are marked and
|
|
1553
|
+
* every staged value stays). An open editor's value is staged first.
|
|
1554
|
+
* Without `staged`, resolves `{ok: false, errors: []}`.
|
|
1555
|
+
*
|
|
1556
|
+
* @returns {Promise<{ok: boolean, committed?: number, errors?: Array<{rowIdx:number, colIdx:number, error:string}>}>}
|
|
1557
|
+
*/
|
|
1558
|
+
commit() {
|
|
1559
|
+
const st = this._cellGrid?.staged;
|
|
1560
|
+
return st ? st.commit() : Promise.resolve({ ok: false, errors: [] });
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
/** Drop every staged cell, its marks and the undo ring. Returns whether
|
|
1564
|
+
* there was anything to drop. */
|
|
1565
|
+
discard() {
|
|
1566
|
+
return !!this._cellGrid?.staged?.discard();
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
/** Take the last staged edit back (Ctrl+Z). Returns whether it moved. */
|
|
1570
|
+
undo() {
|
|
1571
|
+
return !!this._cellGrid?.staged?.undo();
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
/** Put the last edit taken back back again (Ctrl+Y). */
|
|
1575
|
+
redo() {
|
|
1576
|
+
return !!this._cellGrid?.staged?.redo();
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
/** Every staged cell, `{rowIdx, colIdx, value, previous}`, by row then
|
|
1580
|
+
* column. Empty without `staged`. */
|
|
1581
|
+
getStagedEdits() {
|
|
1582
|
+
return this._cellGrid?.staged ? this._cellGrid.staged.edits() : [];
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
/**
|
|
1586
|
+
* STORY-0089. Fill from the cell range to the cell `to` (`{row, col}`:
|
|
1587
|
+
* position in the order shown, data column), as dragging the fill handle
|
|
1588
|
+
* there does: staged as ONE undo entry, Ctrl's repeat as `forceCopy`.
|
|
1589
|
+
* Returns `{direction, count, staged, skipped}` —
|
|
1590
|
+
* `staged` the NUMBER of cells that changed, `skipped` the cells `editable` refused
|
|
1591
|
+
* (`{row, col, rowIdx}`) — or null when nothing was filled (no
|
|
1592
|
+
* `fillHandle`, no range, `to` inside it, an editor open, a commit out).
|
|
1593
|
+
*/
|
|
1594
|
+
fill(to, { forceCopy = false } = {}) {
|
|
1595
|
+
return this._cellGrid?.fill ? this._cellGrid.fill.fill(to, { forceCopy }) : null;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
/** Why sort, filters and pages are held — a staged edit — or null; with
|
|
1599
|
+
* a reason, `what` is refused out loud. */
|
|
1600
|
+
_stagedRefusal(what) {
|
|
1601
|
+
const why = this._cellGrid?.staged?.freezeReason() || null;
|
|
1602
|
+
if (why) console.warn(`[DataTable] ${what} refused: ${why}`);
|
|
1603
|
+
return why;
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
/** STORY-0066. Open the inline renamer on a data column's header (needs
|
|
1607
|
+
* `onColumnRename`). Returns whether it opened. */
|
|
1608
|
+
renameColumn(colIdx) {
|
|
1609
|
+
return !!this._structure()?.rename(colIdx);
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1160
1612
|
/**
|
|
1161
1613
|
* Go to specific page
|
|
1162
1614
|
* @param {number} page - Page number (0-indexed)
|
|
1163
1615
|
*/
|
|
1164
1616
|
goToPage(page) {
|
|
1617
|
+
if (this._stagedRefusal('goToPage')) return;
|
|
1165
1618
|
// 0.7.0 `dataSource`: the page is the SOURCE's — asked for, and drawn
|
|
1166
1619
|
// when it answers (the promise). Past a known total, the last page.
|
|
1167
1620
|
// (The release review: this set an offset nothing under a data source
|
|
@@ -1185,6 +1638,7 @@ export class DataTable {
|
|
|
1185
1638
|
* @param {number} rowIndex - Row index
|
|
1186
1639
|
*/
|
|
1187
1640
|
goToRow(rowIndex) {
|
|
1641
|
+
if (this._stagedRefusal('goToRow')) return;
|
|
1188
1642
|
// 0.7.0 `dataSource`: the page that holds the row, from the source.
|
|
1189
1643
|
if (this.config.dataSource) {
|
|
1190
1644
|
const size = this._sourcePageSize();
|
|
@@ -1203,6 +1657,7 @@ export class DataTable {
|
|
|
1203
1657
|
* @param {boolean} [ascending] - Sort direction (toggles if same column)
|
|
1204
1658
|
*/
|
|
1205
1659
|
sortBy(colIndex, ascending) {
|
|
1660
|
+
if (this._stagedRefusal('sortBy')) return;
|
|
1206
1661
|
if (this._state.sortColumn === colIndex && ascending === undefined) {
|
|
1207
1662
|
this._state.sortAscending = !this._state.sortAscending;
|
|
1208
1663
|
} else {
|
|
@@ -1245,6 +1700,7 @@ export class DataTable {
|
|
|
1245
1700
|
* Clear all column filters
|
|
1246
1701
|
*/
|
|
1247
1702
|
clearFilters() {
|
|
1703
|
+
if (this._stagedRefusal('clearFilters')) return;
|
|
1248
1704
|
this._state.filters.clear();
|
|
1249
1705
|
this._processedRows = null;
|
|
1250
1706
|
this._processedIndexMap = null;
|
|
@@ -1310,6 +1766,12 @@ export class DataTable {
|
|
|
1310
1766
|
* Render the table
|
|
1311
1767
|
*/
|
|
1312
1768
|
render() {
|
|
1769
|
+
// STORY-0003. An open cell editor is not torn down by a redraw: the
|
|
1770
|
+
// render waits for its commit or cancel, and runs then.
|
|
1771
|
+
if (this._cellGrid?.isEditing()) {
|
|
1772
|
+
this._cellGrid.renderWaiting = true;
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1313
1775
|
// Capture active filter input before re-render
|
|
1314
1776
|
const activeFilterColIdx = this._getActiveFilterColIdx();
|
|
1315
1777
|
// KEYBOARD FOCUS SURVIVES A RE-RENDER. Rendering replaces the table
|
|
@@ -1433,7 +1895,9 @@ export class DataTable {
|
|
|
1433
1895
|
// empty one must keep the filter row — or a filter that matched nothing
|
|
1434
1896
|
// could never be cleared. It goes through `_createTable` and gets its
|
|
1435
1897
|
// empty row there.
|
|
1436
|
-
|
|
1898
|
+
// STORY-0091: a crosstab keeps its header rows and its corner too.
|
|
1899
|
+
const keepFilterRow = rows.length === 0
|
|
1900
|
+
&& ((!!this.config.dataSource && this.config.filterable) || !!this._crosstab());
|
|
1437
1901
|
if (rows.length === 0 && !keepFilterRow) {
|
|
1438
1902
|
// Empty state — render the same chrome as the data table
|
|
1439
1903
|
// (header row in its own non-scrolling wrap) followed by a
|
|
@@ -1456,13 +1920,12 @@ export class DataTable {
|
|
|
1456
1920
|
th.textContent = '#';
|
|
1457
1921
|
tr.appendChild(th);
|
|
1458
1922
|
}
|
|
1459
|
-
|
|
1460
|
-
if (this._isHidden(colIdx)) return;
|
|
1923
|
+
for (const colIdx of this._shownColumns()) {
|
|
1461
1924
|
const th = document.createElement('th');
|
|
1462
1925
|
th.className = this._columnTypes[colIdx] || 'text';
|
|
1463
|
-
th.textContent =
|
|
1926
|
+
th.textContent = headers[colIdx];
|
|
1464
1927
|
tr.appendChild(th);
|
|
1465
|
-
}
|
|
1928
|
+
}
|
|
1466
1929
|
thead.appendChild(tr);
|
|
1467
1930
|
headerTable.appendChild(thead);
|
|
1468
1931
|
headerWrap.appendChild(headerTable);
|
|
@@ -1489,7 +1952,15 @@ export class DataTable {
|
|
|
1489
1952
|
this._tableEl = fullTable;
|
|
1490
1953
|
// Extract thead → header table in its own non-scrolling
|
|
1491
1954
|
// container. The body table keeps tbody only.
|
|
1492
|
-
|
|
1955
|
+
// STORY-0091. A crosstab keeps its `thead` in the body table: one
|
|
1956
|
+
// table in one scroller, pinned by `position: sticky`.
|
|
1957
|
+
const crosstab = this._crosstab();
|
|
1958
|
+
const thead = crosstab ? null : fullTable.querySelector('thead');
|
|
1959
|
+
if (crosstab) {
|
|
1960
|
+
crosstab.decorate(fullTable);
|
|
1961
|
+
this._headerTableEl = null;
|
|
1962
|
+
this._headerWrapEl = null;
|
|
1963
|
+
}
|
|
1493
1964
|
if (thead) {
|
|
1494
1965
|
const headerWrap = document.createElement('div');
|
|
1495
1966
|
headerWrap.className = 'twm-preview-table-header-wrap';
|
|
@@ -1515,17 +1986,30 @@ export class DataTable {
|
|
|
1515
1986
|
}
|
|
1516
1987
|
|
|
1517
1988
|
this._tableWrapEl = tableWrap;
|
|
1518
|
-
// STORY-0012 `columnChooser`: the header's
|
|
1519
|
-
|
|
1989
|
+
// STORY-0012 `columnChooser` and STORY-0003 `headerMenu`: the header's
|
|
1990
|
+
// right-click. With neither item to show, the page's own menu.
|
|
1991
|
+
// STORY-0066: the groups' row and placeholder columns, the renamer's
|
|
1992
|
+
// labels and the consumer's column actions, on the header just built.
|
|
1993
|
+
this._colGroups()?.decorate(this._headerWrapEl, this._headerTableEl);
|
|
1994
|
+
this._structure()?.decorate(this._headerTableEl);
|
|
1995
|
+
if ((this.config.columnChooser || typeof this.config.headerMenu === 'function' || this._reorder
|
|
1996
|
+
|| this._structure()?.hasHeaderItems()) && this._headerTableEl) {
|
|
1520
1997
|
const head = this._headerTableEl;
|
|
1521
1998
|
const onHeadMenu = (ev) => {
|
|
1999
|
+
const items = this._headerMenuItems(ev.target);
|
|
2000
|
+
if (!items.length) return;
|
|
1522
2001
|
ev.preventDefault();
|
|
1523
2002
|
ev.stopPropagation();
|
|
1524
|
-
setTimeout(() => this.
|
|
2003
|
+
setTimeout(() => this._showContextMenu(ev.clientX, ev.clientY,
|
|
2004
|
+
{ ctx: null, items, copy: false }), 0);
|
|
1525
2005
|
};
|
|
1526
2006
|
head.addEventListener('contextmenu', onHeadMenu);
|
|
1527
2007
|
this._disposers.push(() => head.removeEventListener('contextmenu', onHeadMenu));
|
|
1528
2008
|
}
|
|
2009
|
+
// STORY-0086 `reorderable`: the header's drag.
|
|
2010
|
+
if (this._reorder && this._headerTableEl) {
|
|
2011
|
+
this._reorder.install(this._headerTableEl.querySelector('thead > tr'));
|
|
2012
|
+
}
|
|
1529
2013
|
// 0.7.0 `pagerPosition: 'bottom'`: under the rows.
|
|
1530
2014
|
if (this._paginationEl && this.config.pagerPosition === 'bottom') {
|
|
1531
2015
|
this._paginationEl.classList.add('twm-pagination-controls--bottom');
|
|
@@ -1541,6 +2025,8 @@ export class DataTable {
|
|
|
1541
2025
|
if (this._pagingEl) this._wrapperEl.appendChild(this._pagingEl);
|
|
1542
2026
|
}
|
|
1543
2027
|
this.container.appendChild(this._wrapperEl);
|
|
2028
|
+
// STORY-0091. The crosstab's offsets, measured now the table is in.
|
|
2029
|
+
this._crosstab()?.install();
|
|
1544
2030
|
// 0.6.1 `scrollLock`: THIS render's body box joins the group, and leaves
|
|
1545
2031
|
// it with the next render's cleanup or the dispose.
|
|
1546
2032
|
if (this.config.scrollLock && typeof this.config.scrollLock.attach === 'function') {
|
|
@@ -1618,7 +2104,8 @@ export class DataTable {
|
|
|
1618
2104
|
if (rows.length > 0 && (this.config.selectable || this.config.copyable || !this.config.readonly
|
|
1619
2105
|
|| typeof this.config.onRowActivate === 'function'
|
|
1620
2106
|
|| typeof this.config.rowDetail === 'function'
|
|
1621
|
-
|| typeof this.config.onRowKeyDown === 'function'
|
|
2107
|
+
|| typeof this.config.onRowKeyDown === 'function'
|
|
2108
|
+
|| this._cellGrid)) {
|
|
1622
2109
|
this._installInteractions();
|
|
1623
2110
|
}
|
|
1624
2111
|
|
|
@@ -1669,14 +2156,14 @@ export class DataTable {
|
|
|
1669
2156
|
}
|
|
1670
2157
|
|
|
1671
2158
|
const { headers, rows } = this.config;
|
|
1672
|
-
// STORY-0012. The columns shown — all of them, unless some are hidden
|
|
1673
|
-
|
|
1674
|
-
const matrix = [headers.
|
|
2159
|
+
// STORY-0012. The columns shown — all of them, unless some are hidden —
|
|
2160
|
+
// and (STORY-0086) in the order shown.
|
|
2161
|
+
const matrix = [this._rowColumns(headers.length).map((c) => headers[c])];
|
|
1675
2162
|
|
|
1676
2163
|
for (const idx of indices) {
|
|
1677
2164
|
const row = rows[idx];
|
|
1678
2165
|
if (row) {
|
|
1679
|
-
matrix.push(row.map((
|
|
2166
|
+
matrix.push(this._rowColumns(row.length).map((c) => this._formatValue(row[c], c)));
|
|
1680
2167
|
}
|
|
1681
2168
|
}
|
|
1682
2169
|
|
|
@@ -1715,12 +2202,11 @@ export class DataTable {
|
|
|
1715
2202
|
return;
|
|
1716
2203
|
}
|
|
1717
2204
|
|
|
1718
|
-
const
|
|
1719
|
-
const lines = [headers.filter(shown).join(',')];
|
|
2205
|
+
const lines = [this._rowColumns(headers.length).map((c) => headers[c]).join(',')];
|
|
1720
2206
|
for (const row of rows) {
|
|
1721
|
-
lines.push(row.map((
|
|
1722
|
-
this._csvEscape(this._formatValue(
|
|
1723
|
-
).
|
|
2207
|
+
lines.push(this._rowColumns(row.length).map((c) =>
|
|
2208
|
+
this._csvEscape(this._formatValue(row[c], c))
|
|
2209
|
+
).join(','));
|
|
1724
2210
|
}
|
|
1725
2211
|
|
|
1726
2212
|
const csv = lines.join('\n');
|
|
@@ -1783,6 +2269,10 @@ export class DataTable {
|
|
|
1783
2269
|
}
|
|
1784
2270
|
|
|
1785
2271
|
_teardown(emptyHost) {
|
|
2272
|
+
// STORY-0003. An open editor goes, committing nothing.
|
|
2273
|
+
this._cellGrid?.dispose();
|
|
2274
|
+
// STORY-0066. And an open column renamer.
|
|
2275
|
+
this._tableStructure?.dispose();
|
|
1786
2276
|
autoDisposeUnwatch(this);
|
|
1787
2277
|
this._autoSeen = false;
|
|
1788
2278
|
// 0.7.0. A grace still running, the follow timer, a request still out
|
|
@@ -2073,8 +2563,8 @@ export class DataTable {
|
|
|
2073
2563
|
tr.appendChild(th);
|
|
2074
2564
|
}
|
|
2075
2565
|
|
|
2076
|
-
|
|
2077
|
-
|
|
2566
|
+
this._shownColumns().forEach((colIdx) => {
|
|
2567
|
+
const header = headers[colIdx];
|
|
2078
2568
|
const th = document.createElement('th');
|
|
2079
2569
|
th.className = 'data-table__filter-cell';
|
|
2080
2570
|
|
|
@@ -2450,6 +2940,7 @@ export class DataTable {
|
|
|
2450
2940
|
}
|
|
2451
2941
|
|
|
2452
2942
|
_onFilterInput(colIdx, value) {
|
|
2943
|
+
if (this._stagedRefusal('A filter')) return;
|
|
2453
2944
|
// 0.7.0. A filter value need not be text (a control's array, a range),
|
|
2454
2945
|
// and an empty one of those is no filter. Text is judged as in 0.4.
|
|
2455
2946
|
const empty = typeof value === 'string' ? !value : this._isEmptyFilterValue(value);
|
|
@@ -2653,6 +3144,7 @@ export class DataTable {
|
|
|
2653
3144
|
|
|
2654
3145
|
// Page change handler - supports both client-side and server-side pagination
|
|
2655
3146
|
const handlePageChange = (newOffset) => {
|
|
3147
|
+
if (this._stagedRefusal('Turning the page')) return;
|
|
2656
3148
|
if (isServerSide) {
|
|
2657
3149
|
// Auto-spinner if the consumer returns a Promise.
|
|
2658
3150
|
this._awaitWithSpinner(onPageChange(newOffset, pageSize));
|
|
@@ -2730,8 +3222,11 @@ export class DataTable {
|
|
|
2730
3222
|
_createExportBtn() {
|
|
2731
3223
|
const name = this.config.exportFilename;
|
|
2732
3224
|
const filename = (typeof name === 'function' ? name(this) : name) || 'data.csv';
|
|
2733
|
-
|
|
2734
|
-
|
|
3225
|
+
const btn = this._createPaginationBtn('download', 'Download visible rows as CSV', false,
|
|
3226
|
+
() => this.downloadCSV(filename));
|
|
3227
|
+
// STORY-0088. Moves no rows, so a staged edit's freeze leaves it be.
|
|
3228
|
+
btn.classList.add('twm-pagination-btn--export');
|
|
3229
|
+
return btn;
|
|
2735
3230
|
}
|
|
2736
3231
|
|
|
2737
3232
|
/**
|
|
@@ -2871,6 +3366,7 @@ export class DataTable {
|
|
|
2871
3366
|
this._wrapperEl.appendChild(next);
|
|
2872
3367
|
}
|
|
2873
3368
|
this._pagingEl = next;
|
|
3369
|
+
this._cellGrid?.staged?.paintFreeze(); // STORY-0088
|
|
2874
3370
|
}
|
|
2875
3371
|
|
|
2876
3372
|
/**
|
|
@@ -2881,6 +3377,7 @@ export class DataTable {
|
|
|
2881
3377
|
*/
|
|
2882
3378
|
_pagingCall(kind, fn, ...args) {
|
|
2883
3379
|
if (typeof fn !== 'function' || this._pagingBusy) return;
|
|
3380
|
+
if (this._stagedRefusal(`paging ${kind}`)) return;
|
|
2884
3381
|
let result;
|
|
2885
3382
|
try {
|
|
2886
3383
|
result = fn(...args);
|
|
@@ -2912,6 +3409,9 @@ export class DataTable {
|
|
|
2912
3409
|
_pagingMaybeLoadMore() {
|
|
2913
3410
|
const p = this._pagingConfig();
|
|
2914
3411
|
if (!p || !p.hasMore || this._pagingBusy) return;
|
|
3412
|
+
// STORY-0088. Paging is held while cells are staged — quietly here:
|
|
3413
|
+
// a scroll is not a click on a control that could say why.
|
|
3414
|
+
if (this._cellGrid?.staged?.freezeReason()) return;
|
|
2915
3415
|
// 0.7.0: not while the source is answering another question (a sort, a
|
|
2916
3416
|
// filter, a refresh) — those rows are about to be replaced.
|
|
2917
3417
|
if (this.config.dataSource && this._source?.inFlight) return;
|
|
@@ -3081,6 +3581,11 @@ export class DataTable {
|
|
|
3081
3581
|
// hundred of a thousand.
|
|
3082
3582
|
const pageRows = isServerSide || !this.config.pagination || this._pagingConfig()
|
|
3083
3583
|
? processedRows : processedRows.slice(offset, offset + pageSize);
|
|
3584
|
+
// STORY-0003. Where the rows drawn stand in the order shown.
|
|
3585
|
+
this._pageSpan = {
|
|
3586
|
+
start: pageRows === processedRows ? 0 : offset,
|
|
3587
|
+
count: pageRows.length,
|
|
3588
|
+
};
|
|
3084
3589
|
|
|
3085
3590
|
const table = document.createElement('table');
|
|
3086
3591
|
table.className = this._tableClassName();
|
|
@@ -3098,8 +3603,8 @@ export class DataTable {
|
|
|
3098
3603
|
headerRow.appendChild(th);
|
|
3099
3604
|
}
|
|
3100
3605
|
|
|
3101
|
-
|
|
3102
|
-
|
|
3606
|
+
this._shownColumns().forEach((colIdx) => {
|
|
3607
|
+
const header = headers[colIdx];
|
|
3103
3608
|
const th = document.createElement('th');
|
|
3104
3609
|
th.className = this._columnTypes[colIdx] || 'text';
|
|
3105
3610
|
|
|
@@ -3217,8 +3722,11 @@ export class DataTable {
|
|
|
3217
3722
|
// 0.5.0. The row's place in the order shown (for 'position' numbers).
|
|
3218
3723
|
tr.__position = (this._positionBase || 0) + localIdx;
|
|
3219
3724
|
tr.className = 'data-preview-row';
|
|
3725
|
+
// STORY-0003. The row's position in the order shown, for cell mode.
|
|
3726
|
+
if (this._cellGrid) tr.__cellPos = (this._pageSpan?.start || 0) + localIdx;
|
|
3220
3727
|
|
|
3221
|
-
|
|
3728
|
+
// STORY-0003. In cell mode the CELLS are lit, not the rows.
|
|
3729
|
+
if (this._state.selected.has(globalIdx) && !this._cellGrid) {
|
|
3222
3730
|
tr.classList.add('selected');
|
|
3223
3731
|
}
|
|
3224
3732
|
// 0.5.0. The consumer's row classes and attributes, and the open mark.
|
|
@@ -3532,6 +4040,7 @@ export class DataTable {
|
|
|
3532
4040
|
const wanted = new Set(want);
|
|
3533
4041
|
for (const [i, tr] of v.map) {
|
|
3534
4042
|
if (!wanted.has(i)) {
|
|
4043
|
+
this._cellGrid?.rowLeaving(tr); // STORY-0003: an open editor commits
|
|
3535
4044
|
tr.remove();
|
|
3536
4045
|
this._removeDetailPieces(tr); // 0.7.0: its detail goes with it
|
|
3537
4046
|
v.map.delete(i);
|
|
@@ -3759,10 +4268,10 @@ export class DataTable {
|
|
|
3759
4268
|
const changedCells = diff?.cells
|
|
3760
4269
|
? (diff.cells instanceof Set ? diff.cells : new Set(diff.cells))
|
|
3761
4270
|
: null;
|
|
3762
|
-
for (
|
|
3763
|
-
if (this._isHidden(colIdx)) continue;
|
|
4271
|
+
for (const colIdx of this._rowColumns(row.length)) {
|
|
3764
4272
|
const td = document.createElement('td');
|
|
3765
4273
|
td.className = this._columnTypes[colIdx] || 'text';
|
|
4274
|
+
if (this._cellGrid) td.__colIdx = colIdx; // STORY-0003
|
|
3766
4275
|
|
|
3767
4276
|
// Use custom cell renderer if provided
|
|
3768
4277
|
const value = row[colIdx];
|
|
@@ -3813,6 +4322,12 @@ export class DataTable {
|
|
|
3813
4322
|
|
|
3814
4323
|
tr.appendChild(td);
|
|
3815
4324
|
}
|
|
4325
|
+
// STORY-0066. An empty group's placeholder column.
|
|
4326
|
+
this._colGroups()?.fillRow(tr);
|
|
4327
|
+
// STORY-0091. The crosstab's pinned cells, and an empty axis's cell.
|
|
4328
|
+
this._crosstab()?.fillRow(tr);
|
|
4329
|
+
// STORY-0003. The selection, and a commit's pending or refused mark.
|
|
4330
|
+
this._cellGrid?.decorateRow(tr);
|
|
3816
4331
|
}
|
|
3817
4332
|
|
|
3818
4333
|
/** Re-render ONE row in place, preserving everything around it.
|
|
@@ -3835,6 +4350,9 @@ export class DataTable {
|
|
|
3835
4350
|
* - keyboard focus, because the focused element's position is recorded
|
|
3836
4351
|
* before the replace and restored after.
|
|
3837
4352
|
*
|
|
4353
|
+
* STORY-0003: on the row an open cell editor is in, the data is stored
|
|
4354
|
+
* now and the row is drawn when the editor closes (true is returned).
|
|
4355
|
+
*
|
|
3838
4356
|
* @param {number} index row index as tracked by `tr.__rowIndex`
|
|
3839
4357
|
* @param {any[]} row the new cell values
|
|
3840
4358
|
* @returns {boolean} false when the row is not currently rendered
|
|
@@ -3867,6 +4385,9 @@ export class DataTable {
|
|
|
3867
4385
|
this.config.rows[index] = row;
|
|
3868
4386
|
}
|
|
3869
4387
|
if (this._virt) this._virtSetRow(index, row);
|
|
4388
|
+
// STORY-0003. The row holds an open cell editor: the rebuild would take
|
|
4389
|
+
// it out of the document, so the row is drawn when the editor closes.
|
|
4390
|
+
if (this._cellGrid?.holdRow(tr, index)) return true;
|
|
3870
4391
|
|
|
3871
4392
|
// Record focus BEFORE the replace: `replaceChildren` detaches the
|
|
3872
4393
|
// focused cell and the browser moves focus to <body>.
|
|
@@ -3913,7 +4434,7 @@ export class DataTable {
|
|
|
3913
4434
|
});
|
|
3914
4435
|
}
|
|
3915
4436
|
|
|
3916
|
-
tr.classList.toggle('selected', this._state?.selected?.has(index) === true);
|
|
4437
|
+
tr.classList.toggle('selected', this._state?.selected?.has(index) === true && !this._cellGrid);
|
|
3917
4438
|
// 0.5.0. The row's data changed, so its classes, attributes, key and
|
|
3918
4439
|
// open mark are asked again (a key paused, a delivery that died).
|
|
3919
4440
|
this._decorateRow(tr, row, index);
|
|
@@ -4473,6 +4994,7 @@ export class DataTable {
|
|
|
4473
4994
|
|
|
4474
4995
|
/** Go to the page at `offset` (paging back stops following). */
|
|
4475
4996
|
_sourceGoTo(offset) {
|
|
4997
|
+
if (this._stagedRefusal('Turning the page')) return undefined;
|
|
4476
4998
|
const next = Math.max(0, offset | 0);
|
|
4477
4999
|
// PAGING AWAY TURNS FOLLOWING OFF, and coming back does not turn it on
|
|
4478
5000
|
// again by itself: a reader who left the tail to read an old page is
|
|
@@ -4764,6 +5286,7 @@ export class DataTable {
|
|
|
4764
5286
|
const maxJump = Number.isFinite(c.maxJumpOffset) ? c.maxJumpOffset : null;
|
|
4765
5287
|
|
|
4766
5288
|
const go = (newOffset) => {
|
|
5289
|
+
if (this._stagedRefusal('Turning the page')) return;
|
|
4767
5290
|
if (source) { this._sourceGoTo(newOffset); return; }
|
|
4768
5291
|
if (server) { this._awaitWithSpinner(c.onPageChange(newOffset, pageSize)); return; }
|
|
4769
5292
|
this._state.offset = newOffset;
|
|
@@ -4953,6 +5476,7 @@ export class DataTable {
|
|
|
4953
5476
|
* value is whatever the column's control works in — text for the 0.4 box.
|
|
4954
5477
|
*/
|
|
4955
5478
|
setFilter(colIdx, value) {
|
|
5479
|
+
if (this._stagedRefusal('setFilter')) return;
|
|
4956
5480
|
const textBox = !this._filterKindFor(colIdx, (this.config.headers || [])[colIdx]);
|
|
4957
5481
|
const v = textBox && value != null && typeof value !== 'string' ? String(value) : value;
|
|
4958
5482
|
if (textBox) {
|
|
@@ -5101,6 +5625,8 @@ export class DataTable {
|
|
|
5101
5625
|
// STORY-0016. The select column, and whether the mode is on.
|
|
5102
5626
|
if (this._gutter) names.push('twm-dt--select');
|
|
5103
5627
|
if (this._selecting) names.push('twm-dt--selecting');
|
|
5628
|
+
// STORY-0003.
|
|
5629
|
+
if (this._cellGrid) names.push('twm-dt--cells');
|
|
5104
5630
|
return names.join(' ');
|
|
5105
5631
|
}
|
|
5106
5632
|
|
|
@@ -6056,6 +6582,9 @@ export class DataTable {
|
|
|
6056
6582
|
headerWrap.scrollLeft = x;
|
|
6057
6583
|
const residual = x - headerWrap.scrollLeft;
|
|
6058
6584
|
headerTable.style.transform = residual ? `translateX(${-residual}px)` : '';
|
|
6585
|
+
// STORY-0066. The group row follows the header: every width pass and
|
|
6586
|
+
// drag ends here, and so does every sideways scroll.
|
|
6587
|
+
this._colGroups()?.align();
|
|
6059
6588
|
}
|
|
6060
6589
|
|
|
6061
6590
|
/** Drag-to-resize: hang a thin grab handle off the right edge of
|
|
@@ -6261,11 +6790,17 @@ export class DataTable {
|
|
|
6261
6790
|
|
|
6262
6791
|
const { selectable, copyable } = this.config;
|
|
6263
6792
|
// STORY-0010. The consumer's own items install the menu by themselves.
|
|
6264
|
-
|
|
6793
|
+
// STORY-0066: the structural row items make a menu of their own.
|
|
6794
|
+
const customMenu = this.config.contextMenuItems != null || !!this._structure()?.hasRowItems();
|
|
6265
6795
|
const tbody = this._tbodyEl;
|
|
6266
6796
|
const table = this._tableEl;
|
|
6267
6797
|
|
|
6268
|
-
|
|
6798
|
+
// STORY-0003. Cell mode: the cells' own pointer and keys, bound before
|
|
6799
|
+
// the activation, detail and copy listeners so a key it takes stops.
|
|
6800
|
+
// The row click below is not cell mode's.
|
|
6801
|
+
if (this._cellGrid) this._cellGrid.install(tbody, table);
|
|
6802
|
+
|
|
6803
|
+
if (selectable && !this._cellGrid) {
|
|
6269
6804
|
const handleRowClick = (event) => {
|
|
6270
6805
|
const rowEl = event.target.closest('tr');
|
|
6271
6806
|
if (!rowEl || rowEl.__rowIndex === undefined) return;
|
|
@@ -6537,7 +7072,7 @@ export class DataTable {
|
|
|
6537
7072
|
const idx = tr.__rowIndex;
|
|
6538
7073
|
if (idx !== undefined) {
|
|
6539
7074
|
const on = this._state.selected.has(idx);
|
|
6540
|
-
tr.classList.toggle('selected', on);
|
|
7075
|
+
tr.classList.toggle('selected', on && !this._cellGrid);
|
|
6541
7076
|
// STORY-0016.
|
|
6542
7077
|
const box = this._gutter ? tr.querySelector(':scope > td.twm-dt-select-cell > input') : null;
|
|
6543
7078
|
if (box) box.checked = on;
|
|
@@ -6818,7 +7353,11 @@ export class DataTable {
|
|
|
6818
7353
|
items = null;
|
|
6819
7354
|
}
|
|
6820
7355
|
}
|
|
6821
|
-
|
|
7356
|
+
const list = Array.isArray(items) ? items.filter(Boolean) : [];
|
|
7357
|
+
// STORY-0066. Insert and delete a row, after the consumer's items.
|
|
7358
|
+
const structural = this._structure()?.rowItems(ctx.rowIdx) || [];
|
|
7359
|
+
if (structural.length && list.length) list.push({ separator: true });
|
|
7360
|
+
return list.concat(structural);
|
|
6822
7361
|
}
|
|
6823
7362
|
|
|
6824
7363
|
/**
|