@ui-organized/svelte-table 0.0.0-stage → 0.1.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/LICENSE +201 -0
- package/README.md +99 -2
- package/dist/DataTable/DataTable.svelte +62 -0
- package/dist/DataTable/DataTable.svelte.d.ts +29 -0
- package/dist/DataTable/DataTable.svelte.d.ts.map +1 -0
- package/dist/DataTable/DataTable.test.svelte.d.ts +2 -0
- package/dist/DataTable/DataTable.test.svelte.d.ts.map +1 -0
- package/dist/DataTable/DataTable.test.svelte.js +360 -0
- package/dist/DataTable/index.d.ts +2 -0
- package/dist/DataTable/index.d.ts.map +1 -0
- package/dist/DataTable/index.js +1 -0
- package/dist/components/Table/CellEditor.svelte +29 -0
- package/dist/components/Table/CellEditor.svelte.d.ts +8 -0
- package/dist/components/Table/CellEditor.svelte.d.ts.map +1 -0
- package/dist/components/Table/MoveButtons.svelte +60 -0
- package/dist/components/Table/MoveButtons.svelte.d.ts +8 -0
- package/dist/components/Table/MoveButtons.svelte.d.ts.map +1 -0
- package/dist/components/Table/ResizeHandle.svelte +82 -0
- package/dist/components/Table/ResizeHandle.svelte.d.ts +28 -0
- package/dist/components/Table/ResizeHandle.svelte.d.ts.map +1 -0
- package/dist/components/Table/Table.svelte +17 -0
- package/dist/components/Table/Table.svelte.d.ts +9 -0
- package/dist/components/Table/Table.svelte.d.ts.map +1 -0
- package/dist/components/Table/TableBody.svelte +78 -0
- package/dist/components/Table/TableBody.svelte.d.ts +30 -0
- package/dist/components/Table/TableBody.svelte.d.ts.map +1 -0
- package/dist/components/Table/TableCell.svelte +106 -0
- package/dist/components/Table/TableCell.svelte.d.ts +34 -0
- package/dist/components/Table/TableCell.svelte.d.ts.map +1 -0
- package/dist/components/Table/TableFooter.svelte +34 -0
- package/dist/components/Table/TableFooter.svelte.d.ts +9 -0
- package/dist/components/Table/TableFooter.svelte.d.ts.map +1 -0
- package/dist/components/Table/TableHeadCell.svelte +157 -0
- package/dist/components/Table/TableHeadCell.svelte.d.ts +29 -0
- package/dist/components/Table/TableHeadCell.svelte.d.ts.map +1 -0
- package/dist/components/Table/TableHeader.svelte +22 -0
- package/dist/components/Table/TableHeader.svelte.d.ts +7 -0
- package/dist/components/Table/TableHeader.svelte.d.ts.map +1 -0
- package/dist/components/Table/TableRow.svelte +84 -0
- package/dist/components/Table/TableRow.svelte.d.ts +32 -0
- package/dist/components/Table/TableRow.svelte.d.ts.map +1 -0
- package/dist/components/Table/TableViewport.svelte +69 -0
- package/dist/components/Table/TableViewport.svelte.d.ts +10 -0
- package/dist/components/Table/TableViewport.svelte.d.ts.map +1 -0
- package/dist/components/Table/index.d.ts +23 -0
- package/dist/components/Table/index.d.ts.map +1 -0
- package/dist/components/Table/index.js +22 -0
- package/dist/components/TableCards/TableCard.svelte +96 -0
- package/dist/components/TableCards/TableCard.svelte.d.ts +31 -0
- package/dist/components/TableCards/TableCard.svelte.d.ts.map +1 -0
- package/dist/components/TableCards/TableCards.svelte +61 -0
- package/dist/components/TableCards/TableCards.svelte.d.ts +7 -0
- package/dist/components/TableCards/TableCards.svelte.d.ts.map +1 -0
- package/dist/components/TableCards/index.d.ts +3 -0
- package/dist/components/TableCards/index.d.ts.map +1 -0
- package/dist/components/TableCards/index.js +2 -0
- package/dist/components/TableDetailSheet/TableDetailSheet.svelte +130 -0
- package/dist/components/TableDetailSheet/TableDetailSheet.svelte.d.ts +8 -0
- package/dist/components/TableDetailSheet/TableDetailSheet.svelte.d.ts.map +1 -0
- package/dist/components/TableDetailSheet/index.d.ts +2 -0
- package/dist/components/TableDetailSheet/index.d.ts.map +1 -0
- package/dist/components/TableDetailSheet/index.js +1 -0
- package/dist/components/TableFilters/TableFilterAdd.svelte +89 -0
- package/dist/components/TableFilters/TableFilterAdd.svelte.d.ts +15 -0
- package/dist/components/TableFilters/TableFilterAdd.svelte.d.ts.map +1 -0
- package/dist/components/TableFilters/TableFilterChip.svelte +164 -0
- package/dist/components/TableFilters/TableFilterChip.svelte.d.ts +9 -0
- package/dist/components/TableFilters/TableFilterChip.svelte.d.ts.map +1 -0
- package/dist/components/TableFilters/TableFilterEditor.svelte +40 -0
- package/dist/components/TableFilters/TableFilterEditor.svelte.d.ts +9 -0
- package/dist/components/TableFilters/TableFilterEditor.svelte.d.ts.map +1 -0
- package/dist/components/TableFilters/TableFilterValueEditor.svelte +209 -0
- package/dist/components/TableFilters/TableFilterValueEditor.svelte.d.ts +10 -0
- package/dist/components/TableFilters/TableFilterValueEditor.svelte.d.ts.map +1 -0
- package/dist/components/TableFilters/TableFilters.svelte +51 -0
- package/dist/components/TableFilters/TableFilters.svelte.d.ts +7 -0
- package/dist/components/TableFilters/TableFilters.svelte.d.ts.map +1 -0
- package/dist/components/TableFilters/index.d.ts +7 -0
- package/dist/components/TableFilters/index.d.ts.map +1 -0
- package/dist/components/TableFilters/index.js +6 -0
- package/dist/components/TableFilters/useAutoFocusField.d.ts +30 -0
- package/dist/components/TableFilters/useAutoFocusField.d.ts.map +1 -0
- package/dist/components/TableFilters/useAutoFocusField.js +59 -0
- package/dist/components/TablePagination/TablePagination.svelte +61 -0
- package/dist/components/TablePagination/TablePagination.svelte.d.ts +9 -0
- package/dist/components/TablePagination/TablePagination.svelte.d.ts.map +1 -0
- package/dist/components/TablePagination/index.d.ts +2 -0
- package/dist/components/TablePagination/index.d.ts.map +1 -0
- package/dist/components/TablePagination/index.js +1 -0
- package/dist/components/TableRowActions/TableRowActions.svelte +68 -0
- package/dist/components/TableRowActions/TableRowActions.svelte.d.ts +37 -0
- package/dist/components/TableRowActions/TableRowActions.svelte.d.ts.map +1 -0
- package/dist/components/TableRowActions/index.d.ts +2 -0
- package/dist/components/TableRowActions/index.d.ts.map +1 -0
- package/dist/components/TableRowActions/index.js +1 -0
- package/dist/components/TableSelectionBar/TableSelectionBar.svelte +88 -0
- package/dist/components/TableSelectionBar/TableSelectionBar.svelte.d.ts +30 -0
- package/dist/components/TableSelectionBar/TableSelectionBar.svelte.d.ts.map +1 -0
- package/dist/components/TableSelectionBar/index.d.ts +2 -0
- package/dist/components/TableSelectionBar/index.d.ts.map +1 -0
- package/dist/components/TableSelectionBar/index.js +1 -0
- package/dist/components/TableSelectionFooter/TableSelectionFooter.svelte +38 -0
- package/dist/components/TableSelectionFooter/TableSelectionFooter.svelte.d.ts +7 -0
- package/dist/components/TableSelectionFooter/TableSelectionFooter.svelte.d.ts.map +1 -0
- package/dist/components/TableSelectionFooter/index.d.ts +2 -0
- package/dist/components/TableSelectionFooter/index.d.ts.map +1 -0
- package/dist/components/TableSelectionFooter/index.js +1 -0
- package/dist/components/TableSheets/TableFilterSheet.svelte +150 -0
- package/dist/components/TableSheets/TableFilterSheet.svelte.d.ts +8 -0
- package/dist/components/TableSheets/TableFilterSheet.svelte.d.ts.map +1 -0
- package/dist/components/TableSheets/TableFilterSheetCondition.svelte +53 -0
- package/dist/components/TableSheets/TableFilterSheetCondition.svelte.d.ts +9 -0
- package/dist/components/TableSheets/TableFilterSheetCondition.svelte.d.ts.map +1 -0
- package/dist/components/TableSheets/TableSheetButton.svelte +44 -0
- package/dist/components/TableSheets/TableSheetButton.svelte.d.ts +15 -0
- package/dist/components/TableSheets/TableSheetButton.svelte.d.ts.map +1 -0
- package/dist/components/TableSheets/TableSortSheet.svelte +100 -0
- package/dist/components/TableSheets/TableSortSheet.svelte.d.ts +8 -0
- package/dist/components/TableSheets/TableSortSheet.svelte.d.ts.map +1 -0
- package/dist/components/TableSheets/index.d.ts +4 -0
- package/dist/components/TableSheets/index.d.ts.map +1 -0
- package/dist/components/TableSheets/index.js +3 -0
- package/dist/components/TableStates/TableEmpty.svelte +35 -0
- package/dist/components/TableStates/TableEmpty.svelte.d.ts +11 -0
- package/dist/components/TableStates/TableEmpty.svelte.d.ts.map +1 -0
- package/dist/components/TableStates/TableError.svelte +26 -0
- package/dist/components/TableStates/TableError.svelte.d.ts +10 -0
- package/dist/components/TableStates/TableError.svelte.d.ts.map +1 -0
- package/dist/components/TableStates/TableLoading.svelte +33 -0
- package/dist/components/TableStates/TableLoading.svelte.d.ts +8 -0
- package/dist/components/TableStates/TableLoading.svelte.d.ts.map +1 -0
- package/dist/components/TableStates/index.d.ts +4 -0
- package/dist/components/TableStates/index.d.ts.map +1 -0
- package/dist/components/TableStates/index.js +3 -0
- package/dist/components/TableSubBar/TableSubBar.svelte +34 -0
- package/dist/components/TableSubBar/TableSubBar.svelte.d.ts +7 -0
- package/dist/components/TableSubBar/TableSubBar.svelte.d.ts.map +1 -0
- package/dist/components/TableSubBar/index.d.ts +2 -0
- package/dist/components/TableSubBar/index.d.ts.map +1 -0
- package/dist/components/TableSubBar/index.js +1 -0
- package/dist/components/TableToolbar/TableActions.svelte +40 -0
- package/dist/components/TableToolbar/TableActions.svelte.d.ts +9 -0
- package/dist/components/TableToolbar/TableActions.svelte.d.ts.map +1 -0
- package/dist/components/TableToolbar/TableExportMenu.svelte +60 -0
- package/dist/components/TableToolbar/TableExportMenu.svelte.d.ts +8 -0
- package/dist/components/TableToolbar/TableExportMenu.svelte.d.ts.map +1 -0
- package/dist/components/TableToolbar/TableReorderToggle.svelte +27 -0
- package/dist/components/TableToolbar/TableReorderToggle.svelte.d.ts +7 -0
- package/dist/components/TableToolbar/TableReorderToggle.svelte.d.ts.map +1 -0
- package/dist/components/TableToolbar/TableScrollButtons.svelte +54 -0
- package/dist/components/TableToolbar/TableScrollButtons.svelte.d.ts +9 -0
- package/dist/components/TableToolbar/TableScrollButtons.svelte.d.ts.map +1 -0
- package/dist/components/TableToolbar/TableSearch.svelte +24 -0
- package/dist/components/TableToolbar/TableSearch.svelte.d.ts +9 -0
- package/dist/components/TableToolbar/TableSearch.svelte.d.ts.map +1 -0
- package/dist/components/TableToolbar/TableSortMenu.svelte +97 -0
- package/dist/components/TableToolbar/TableSortMenu.svelte.d.ts +8 -0
- package/dist/components/TableToolbar/TableSortMenu.svelte.d.ts.map +1 -0
- package/dist/components/TableToolbar/TableToolbar.svelte +113 -0
- package/dist/components/TableToolbar/TableToolbar.svelte.d.ts +9 -0
- package/dist/components/TableToolbar/TableToolbar.svelte.d.ts.map +1 -0
- package/dist/components/TableToolbar/TableViewOptions.svelte +69 -0
- package/dist/components/TableToolbar/TableViewOptions.svelte.d.ts +8 -0
- package/dist/components/TableToolbar/TableViewOptions.svelte.d.ts.map +1 -0
- package/dist/components/TableToolbar/index.d.ts +9 -0
- package/dist/components/TableToolbar/index.d.ts.map +1 -0
- package/dist/components/TableToolbar/index.js +8 -0
- package/dist/core/ActionsHeader.svelte +8 -0
- package/dist/core/ActionsHeader.svelte.d.ts +27 -0
- package/dist/core/ActionsHeader.svelte.d.ts.map +1 -0
- package/dist/core/SelectAllCell.svelte +21 -0
- package/dist/core/SelectAllCell.svelte.d.ts +19 -0
- package/dist/core/SelectAllCell.svelte.d.ts.map +1 -0
- package/dist/core/SelectCell.svelte +37 -0
- package/dist/core/SelectCell.svelte.d.ts +8 -0
- package/dist/core/SelectCell.svelte.d.ts.map +1 -0
- package/dist/core/createDataTable.svelte.d.ts +4 -0
- package/dist/core/createDataTable.svelte.d.ts.map +1 -0
- package/dist/core/createDataTable.svelte.js +1152 -0
- package/dist/core/createDataTable.test.svelte.d.ts +2 -0
- package/dist/core/createDataTable.test.svelte.d.ts.map +1 -0
- package/dist/core/createDataTable.test.svelte.js +156 -0
- package/dist/core/createDeferred.svelte.d.ts +38 -0
- package/dist/core/createDeferred.svelte.d.ts.map +1 -0
- package/dist/core/createDeferred.svelte.js +81 -0
- package/dist/core/elementRef.d.ts +17 -0
- package/dist/core/elementRef.d.ts.map +1 -0
- package/dist/core/elementRef.js +20 -0
- package/dist/core/props.d.ts +3 -0
- package/dist/core/props.d.ts.map +1 -0
- package/dist/core/props.js +46 -0
- package/dist/core/systemColumns.d.ts +16 -0
- package/dist/core/systemColumns.d.ts.map +1 -0
- package/dist/core/systemColumns.js +53 -0
- package/dist/core/tableContext.d.ts +7 -0
- package/dist/core/tableContext.d.ts.map +1 -0
- package/dist/core/tableContext.js +30 -0
- package/dist/core/trackStore.svelte.d.ts +24 -0
- package/dist/core/trackStore.svelte.d.ts.map +1 -0
- package/dist/core/trackStore.svelte.js +28 -0
- package/dist/core/types.d.ts +397 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/styles.css +895 -0
- package/package.json +77 -3
|
@@ -0,0 +1,1152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The headless layer: core's stores and TanStack's Svelte adapter, bound
|
|
3
|
+
* together.
|
|
4
|
+
*
|
|
5
|
+
* Everything decided here is a *binding* decision — which rune holds what, when
|
|
6
|
+
* a value is deferred, how focus is restored. Every behavioural decision (what
|
|
7
|
+
* Tab does, what a shift-range covers, when to virtualize) lives in
|
|
8
|
+
* `@ui-organized/table-core` and is reached from here, which is what keeps a
|
|
9
|
+
* third framework adapter to this file plus the components.
|
|
10
|
+
*
|
|
11
|
+
* ── The shape, and why it is a class ────────────────────────────────────────
|
|
12
|
+
*
|
|
13
|
+
* React's hook returns values, rebuilt per render. Vue's composable returns
|
|
14
|
+
* refs, read with `.value`. Svelte's returns **an object of getters over
|
|
15
|
+
* `$derived`**, so a component writes `table.rows` and the read is tracked — the
|
|
16
|
+
* same property access React has, with none of Vue's ceremony.
|
|
17
|
+
*
|
|
18
|
+
* A class rather than an object literal because `$state` and `$derived` are
|
|
19
|
+
* legal in class fields and the getters come for free, and because the field
|
|
20
|
+
* order *is* the dependency order: a `$derived` may read a field declared above
|
|
21
|
+
* it and not one below.
|
|
22
|
+
*
|
|
23
|
+
* ── The one thing Svelte does not have ──────────────────────────────────────
|
|
24
|
+
*
|
|
25
|
+
* `useDeferredValue`. See `./createDeferred.svelte.ts`, which is deliberately the
|
|
26
|
+
* same answer `@ui-organized/vue-table` gives — two designs for one problem in
|
|
27
|
+
* one design system would be a defect in itself.
|
|
28
|
+
*/
|
|
29
|
+
import { untrack } from "svelte";
|
|
30
|
+
import { createTable, } from "@tanstack/svelte-table";
|
|
31
|
+
import { createVirtualizer } from "@tanstack/svelte-virtual";
|
|
32
|
+
import { CARD_HEIGHT, DEFAULT_PAGE_SIZE, EMPTY_SELECTION, FACET_ROW_LIMIT, columnId as columnIdOf, copyRowsToClipboard, coreTableOptions, createDetailStore, createEditStore, createFilterReducer, createOperatorCatalogue, defaultOperatorFor, describeCondition, estimateRowHeight, exportRowsToCsv, facetOptions, filterAnnouncement, fromColumnFilters, gridKeyDown, headerCheckboxState, horizontalScrollState, horizontalScrollTarget, inferFilterType, INITIAL_FILTER_STATE, isEditing as isEditingCell, isNearEnd, isSelected as isRowSelected, layoutColumns, metaOf, NO_HORIZONTAL_SCROLL, normalizeFilterDef, operatorLabel, operatorsFor, retargetCondition, orderFilterableFields, overscanFor, primaryColumnId, rangeSelect, resolveMode, selectAllMatching as selectAllMatchingCore, selectPage, selectionCount as selectionCountCore, shouldVirtualize, spacerHeights, stepRow, toBulkSelection, toColumnFilters, watchWidth, withDateColumns, canSelectHiddenRows, canStepColumn, currentColumnOrder, dropColumn, isMovableColumn, reorderModeOf, stepColumn, } from "@ui-organized/table-core";
|
|
33
|
+
import { trackStore } from "./trackStore.svelte.js";
|
|
34
|
+
import { createDeferred } from "./createDeferred.svelte.js";
|
|
35
|
+
import { actionsColumn, selectionColumn } from "./systemColumns.js";
|
|
36
|
+
/** Stable identity for the un-virtualized path. */
|
|
37
|
+
const NO_SPACERS = { top: 0, bottom: 0 };
|
|
38
|
+
/**
|
|
39
|
+
* Ids are assigned here rather than accepted, so they are deterministic —
|
|
40
|
+
* which is what keeps visual baselines and test assertions stable across runs.
|
|
41
|
+
*/
|
|
42
|
+
function seedFilterState(defaults) {
|
|
43
|
+
if (!defaults?.length)
|
|
44
|
+
return INITIAL_FILTER_STATE;
|
|
45
|
+
const conditions = defaults.map((condition, index) => ({
|
|
46
|
+
...condition,
|
|
47
|
+
id: `f${index + 1}`,
|
|
48
|
+
}));
|
|
49
|
+
return { ...INITIAL_FILTER_STATE, conditions, nextId: conditions.length + 1 };
|
|
50
|
+
}
|
|
51
|
+
/** Follows the same dot path `accessorKey` does, so nested data is reachable. */
|
|
52
|
+
function readPath(row, path) {
|
|
53
|
+
if (!path.includes("."))
|
|
54
|
+
return row?.[path];
|
|
55
|
+
let value = row;
|
|
56
|
+
for (const key of path.split(".")) {
|
|
57
|
+
value = value?.[key];
|
|
58
|
+
}
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
/** A few real values from a column, for type inference. */
|
|
62
|
+
function sampleColumn(data, def, id) {
|
|
63
|
+
const key = def.accessorKey ?? id;
|
|
64
|
+
// Twenty, not one: a leading null should not get to decide the type.
|
|
65
|
+
return data.slice(0, 20).map((row) => readPath(row, key));
|
|
66
|
+
}
|
|
67
|
+
/** TanStack's logical pinning positions, in this design system's physical ones. */
|
|
68
|
+
const PINNED_SIDE = {
|
|
69
|
+
start: "left",
|
|
70
|
+
end: "right",
|
|
71
|
+
false: false,
|
|
72
|
+
undefined: false,
|
|
73
|
+
};
|
|
74
|
+
function applyUpdater(updater, previous) {
|
|
75
|
+
return typeof updater === "function" ? updater(previous) : updater;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* `meta.sticky` is our vocabulary; `columnPinning` is TanStack's.
|
|
79
|
+
*
|
|
80
|
+
* The two disagree on more than the key name since v9: TanStack pins to
|
|
81
|
+
* `start`/`end`, which is the correct call for a library that has to work in
|
|
82
|
+
* both writing directions, while this design system says `left`/`right` because
|
|
83
|
+
* its sticky offsets are physical CSS properties.
|
|
84
|
+
*/
|
|
85
|
+
function pinningFromMeta(columns) {
|
|
86
|
+
const start = [];
|
|
87
|
+
const end = [];
|
|
88
|
+
for (const def of columns) {
|
|
89
|
+
const sticky = metaOf(def)?.sticky;
|
|
90
|
+
if (sticky === "left")
|
|
91
|
+
start.push(columnIdOf(def));
|
|
92
|
+
else if (sticky === "right")
|
|
93
|
+
end.push(columnIdOf(def));
|
|
94
|
+
}
|
|
95
|
+
return { start, end };
|
|
96
|
+
}
|
|
97
|
+
function prefersReducedMotion() {
|
|
98
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function")
|
|
99
|
+
return false;
|
|
100
|
+
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
101
|
+
}
|
|
102
|
+
export function createDataTable(options) {
|
|
103
|
+
// ── Options, read reactively ──────────────────────────────────────────────
|
|
104
|
+
const label = $derived(options().label);
|
|
105
|
+
const captionVisible = $derived(options().captionVisible ?? false);
|
|
106
|
+
const size = $derived(options().size ?? "md");
|
|
107
|
+
const variant = $derived(options().variant ?? "default");
|
|
108
|
+
const selectionMode = $derived(options().selection ?? "none");
|
|
109
|
+
const sortable = $derived(options().sortable ?? true);
|
|
110
|
+
const filterable = $derived(options().filterable ?? true);
|
|
111
|
+
const paginated = $derived(options().paginated ?? false);
|
|
112
|
+
const resizable = $derived(options().resizable ?? false);
|
|
113
|
+
const virtualOption = $derived(options().virtual ?? true);
|
|
114
|
+
const loading = $derived(options().loading ?? false);
|
|
115
|
+
const manual = $derived(options().manual ?? false);
|
|
116
|
+
// ── DOM handles ───────────────────────────────────────────────────────────
|
|
117
|
+
// `$state` written by a callback ref rather than `bind:this`: a plain variable
|
|
118
|
+
// assignment is not something a `$derived` elsewhere can watch, and the resize
|
|
119
|
+
// and scroll observers below have to re-run when the node arrives.
|
|
120
|
+
let rootEl = $state(null);
|
|
121
|
+
// `HTMLElement`, not `HTMLDivElement`: in card mode the scroll container is a
|
|
122
|
+
// `<ul>`, and the virtualizer attaches to whichever one is rendered.
|
|
123
|
+
let viewportEl = $state(null);
|
|
124
|
+
// ── Table state ───────────────────────────────────────────────────────────
|
|
125
|
+
let sorting = $state(options().defaultSorting ?? []);
|
|
126
|
+
const catalogue = $derived(createOperatorCatalogue(options().filterOperators));
|
|
127
|
+
let filterState = $state(seedFilterState(options().defaultFilters));
|
|
128
|
+
const dispatchFilter = (action) => {
|
|
129
|
+
filterState = createFilterReducer(catalogue)(filterState, action);
|
|
130
|
+
};
|
|
131
|
+
const conditions = $derived(filterState.conditions);
|
|
132
|
+
/**
|
|
133
|
+
* Conditions update **urgently**; only the filtering they drive is deferred.
|
|
134
|
+
* See `./createDeferred.svelte.ts`.
|
|
135
|
+
*/
|
|
136
|
+
const deferredConditions = createDeferred(() => conditions);
|
|
137
|
+
const columnFilters = $derived(toColumnFilters(deferredConditions.current));
|
|
138
|
+
let search = $state("");
|
|
139
|
+
const deferredSearch = createDeferred(() => search);
|
|
140
|
+
let pagination = $state({
|
|
141
|
+
pageIndex: 0,
|
|
142
|
+
pageSize: options().pageSize ?? DEFAULT_PAGE_SIZE,
|
|
143
|
+
});
|
|
144
|
+
let columnVisibility = $state(options().defaultColumnVisibility ?? {});
|
|
145
|
+
let columnOrder = $state([]);
|
|
146
|
+
let columnSizing = $state({});
|
|
147
|
+
let selection = $state(EMPTY_SELECTION);
|
|
148
|
+
// ── Measurement ───────────────────────────────────────────────────────────
|
|
149
|
+
// Two widths, because they answer different questions. The root's width picks
|
|
150
|
+
// the responsive mode; the viewport's content width decides how much room the
|
|
151
|
+
// columns actually have.
|
|
152
|
+
let width = $state(null);
|
|
153
|
+
let viewportWidth = $state(null);
|
|
154
|
+
$effect(() => watchWidth(rootEl, (value) => (width = value)));
|
|
155
|
+
$effect(() => watchWidth(viewportEl, (value) => (viewportWidth = value)));
|
|
156
|
+
const mode = $derived(resolveMode(options().responsive, width));
|
|
157
|
+
// ── Horizontal scroll ─────────────────────────────────────────────────────
|
|
158
|
+
let scrollState = $state(NO_HORIZONTAL_SCROLL);
|
|
159
|
+
/**
|
|
160
|
+
* Three signals, because three different things change the answer: the user
|
|
161
|
+
* scrolls, the viewport is resized, and the content is re-laid out underneath
|
|
162
|
+
* it. That last one moves neither the scroll offset nor the viewport's own
|
|
163
|
+
* box, which is why the scrolled content is observed and not just its
|
|
164
|
+
* container.
|
|
165
|
+
*
|
|
166
|
+
* Every update goes through a bail-out compare. The numbers change on every
|
|
167
|
+
* frame of a scroll; the three booleans they answer change only at the ends.
|
|
168
|
+
*/
|
|
169
|
+
$effect(() => {
|
|
170
|
+
const element = viewportEl;
|
|
171
|
+
if (!element) {
|
|
172
|
+
scrollState = NO_HORIZONTAL_SCROLL;
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const read = () => {
|
|
176
|
+
const next = horizontalScrollState(element);
|
|
177
|
+
if (next.overflowing === scrollState.overflowing &&
|
|
178
|
+
next.canScrollLeft === scrollState.canScrollLeft &&
|
|
179
|
+
next.canScrollRight === scrollState.canScrollRight) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
scrollState = next;
|
|
183
|
+
};
|
|
184
|
+
read();
|
|
185
|
+
element.addEventListener("scroll", read, { passive: true });
|
|
186
|
+
// Guarded for jsdom and SSR, exactly as `watchWidth` is.
|
|
187
|
+
const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(read);
|
|
188
|
+
observer?.observe(element);
|
|
189
|
+
const content = element.firstElementChild;
|
|
190
|
+
if (content)
|
|
191
|
+
observer?.observe(content);
|
|
192
|
+
return () => {
|
|
193
|
+
element.removeEventListener("scroll", read);
|
|
194
|
+
observer?.disconnect();
|
|
195
|
+
};
|
|
196
|
+
});
|
|
197
|
+
// ── Columns ───────────────────────────────────────────────────────────────
|
|
198
|
+
// Date columns rewritten once, up front: everything below — filter types,
|
|
199
|
+
// chips, sorting, export — then sees ISO days and the column's own format.
|
|
200
|
+
// Same array back when no column declares `meta.date`.
|
|
201
|
+
const userColumns = $derived(withDateColumns(options().columns));
|
|
202
|
+
const allColumns = $derived.by(() => {
|
|
203
|
+
const out = [...userColumns];
|
|
204
|
+
if (selectionMode !== "none")
|
|
205
|
+
out.unshift(selectionColumn());
|
|
206
|
+
if (options().rowActions?.length)
|
|
207
|
+
out.push(actionsColumn());
|
|
208
|
+
return out;
|
|
209
|
+
});
|
|
210
|
+
const primaryId = $derived(primaryColumnId(userColumns));
|
|
211
|
+
const basePinning = $derived(pinningFromMeta(allColumns));
|
|
212
|
+
// Created once, outside any rune: these are stores, not derived values, and
|
|
213
|
+
// rebuilding one would discard an open editor mid-keystroke.
|
|
214
|
+
const stores = { edit: createEditStore(), detail: createDetailStore() };
|
|
215
|
+
const editState = trackStore(stores.edit);
|
|
216
|
+
const detailState = trackStore(stores.detail);
|
|
217
|
+
const editableColumns = $derived(new Set(options()
|
|
218
|
+
.columns.filter((def) => metaOf(def)?.edit)
|
|
219
|
+
.map((def) => columnIdOf(def))));
|
|
220
|
+
const interactive = $derived(selectionMode !== "none" ||
|
|
221
|
+
editableColumns.size > 0 ||
|
|
222
|
+
Boolean(options().onRowClick) ||
|
|
223
|
+
Boolean(options().detail) ||
|
|
224
|
+
Boolean(options().rowActions?.length));
|
|
225
|
+
/**
|
|
226
|
+
* The filter type per filterable column, from its declaration or inferred from
|
|
227
|
+
* the data. Read off the raw rows rather than the table's, because this is an
|
|
228
|
+
* *input* to building the table.
|
|
229
|
+
*/
|
|
230
|
+
const filterTypes = $derived.by(() => {
|
|
231
|
+
const types = {};
|
|
232
|
+
for (const def of userColumns) {
|
|
233
|
+
const filter = normalizeFilterDef(metaOf(def)?.filter);
|
|
234
|
+
if (!filter)
|
|
235
|
+
continue;
|
|
236
|
+
const id = columnIdOf(def);
|
|
237
|
+
types[id] =
|
|
238
|
+
filter.type ?? inferFilterType(sampleColumn(options().data, def, id), filter.options);
|
|
239
|
+
}
|
|
240
|
+
return types;
|
|
241
|
+
});
|
|
242
|
+
const tableOptions = $derived(coreTableOptions({
|
|
243
|
+
data: options().data,
|
|
244
|
+
columns: allColumns,
|
|
245
|
+
getRowId: options().getRowId,
|
|
246
|
+
selection: selectionMode,
|
|
247
|
+
sortable,
|
|
248
|
+
filterable,
|
|
249
|
+
paginated,
|
|
250
|
+
resizable,
|
|
251
|
+
manual,
|
|
252
|
+
rowCount: options().rowCount,
|
|
253
|
+
filterTypes,
|
|
254
|
+
catalogue,
|
|
255
|
+
}));
|
|
256
|
+
/**
|
|
257
|
+
* The controlled state, in one place — and handed back on the api, because
|
|
258
|
+
* `table.state` is a *React*-adapter convenience. The Svelte adapter exposes
|
|
259
|
+
* rune-aware `atoms` instead, so the parts would have no equivalent to read.
|
|
260
|
+
*/
|
|
261
|
+
const state = $derived({
|
|
262
|
+
sorting,
|
|
263
|
+
columnFilters,
|
|
264
|
+
globalFilter: deferredSearch.current,
|
|
265
|
+
pagination,
|
|
266
|
+
columnVisibility,
|
|
267
|
+
columnOrder,
|
|
268
|
+
columnSizing,
|
|
269
|
+
columnPinning: basePinning,
|
|
270
|
+
rowSelection: selection.rows,
|
|
271
|
+
});
|
|
272
|
+
/**
|
|
273
|
+
* Options as **getters**, which is the Svelte adapter's reactivity contract:
|
|
274
|
+
* it syncs in `$effect.pre`, reading whatever the getters return, so a getter
|
|
275
|
+
* that reads a `$derived` keeps the table in step without a watcher.
|
|
276
|
+
*/
|
|
277
|
+
const table = createTable({
|
|
278
|
+
get data() {
|
|
279
|
+
return tableOptions.data;
|
|
280
|
+
},
|
|
281
|
+
get columns() {
|
|
282
|
+
return tableOptions.columns;
|
|
283
|
+
},
|
|
284
|
+
get features() {
|
|
285
|
+
return tableOptions.features;
|
|
286
|
+
},
|
|
287
|
+
get defaultColumn() {
|
|
288
|
+
return tableOptions.defaultColumn;
|
|
289
|
+
},
|
|
290
|
+
get getRowId() {
|
|
291
|
+
return tableOptions.getRowId;
|
|
292
|
+
},
|
|
293
|
+
get enableSorting() {
|
|
294
|
+
return tableOptions.enableSorting;
|
|
295
|
+
},
|
|
296
|
+
get enableFilters() {
|
|
297
|
+
return tableOptions.enableFilters;
|
|
298
|
+
},
|
|
299
|
+
get enableGlobalFilter() {
|
|
300
|
+
return tableOptions.enableGlobalFilter;
|
|
301
|
+
},
|
|
302
|
+
get globalFilterFn() {
|
|
303
|
+
return tableOptions.globalFilterFn;
|
|
304
|
+
},
|
|
305
|
+
get enableRowSelection() {
|
|
306
|
+
return tableOptions.enableRowSelection;
|
|
307
|
+
},
|
|
308
|
+
get enableMultiRowSelection() {
|
|
309
|
+
return tableOptions.enableMultiRowSelection;
|
|
310
|
+
},
|
|
311
|
+
get enableColumnResizing() {
|
|
312
|
+
return tableOptions.enableColumnResizing;
|
|
313
|
+
},
|
|
314
|
+
get columnResizeMode() {
|
|
315
|
+
return tableOptions.columnResizeMode;
|
|
316
|
+
},
|
|
317
|
+
get manualSorting() {
|
|
318
|
+
return tableOptions.manualSorting;
|
|
319
|
+
},
|
|
320
|
+
get manualFiltering() {
|
|
321
|
+
return tableOptions.manualFiltering;
|
|
322
|
+
},
|
|
323
|
+
get manualPagination() {
|
|
324
|
+
return tableOptions.manualPagination;
|
|
325
|
+
},
|
|
326
|
+
get rowCount() {
|
|
327
|
+
return tableOptions.rowCount;
|
|
328
|
+
},
|
|
329
|
+
/**
|
|
330
|
+
* TanStack's own page reset is turned **off**, and replaced below. This is
|
|
331
|
+
* the one behavioural workaround in this package and it is forced by the
|
|
332
|
+
* adapter rather than chosen.
|
|
333
|
+
*
|
|
334
|
+
* ── What goes wrong ─────────────────────────────────────────────────────
|
|
335
|
+
*
|
|
336
|
+
* `@tanstack/svelte-table@9.2.4` backs its options store with `$state`,
|
|
337
|
+
* which **deep-proxies**. Every option sync replaces the options object, so
|
|
338
|
+
* `table.options.data` comes back as a *fresh proxy of the same array* —
|
|
339
|
+
* a new identity every time. The core row model memoizes on
|
|
340
|
+
* `[table.options.data]`, so it rebuilds on every state change, and its
|
|
341
|
+
* `onAfterUpdate` calls `table_autoResetPageIndex`.
|
|
342
|
+
*
|
|
343
|
+
* The result: clicking "next page" set the page to 1, the sync fired, the
|
|
344
|
+
* row model rebuilt, and the page was reset to 0 before anything rendered.
|
|
345
|
+
* Paging simply did nothing, with no error.
|
|
346
|
+
*
|
|
347
|
+
* `@tanstack/vue-table` backs the same store with `shallowRef`, which does
|
|
348
|
+
* not proxy, so identity survives and neither Vue nor React sees this.
|
|
349
|
+
*
|
|
350
|
+
* ── Why this is the workaround ──────────────────────────────────────────
|
|
351
|
+
*
|
|
352
|
+
* `autoResetPageIndex` is the only *behaviour* the spurious rebuild breaks;
|
|
353
|
+
* the rebuilds themselves are wasteful but correct. So the flag goes off and
|
|
354
|
+
* the effect below does the reset explicitly, on the inputs TanStack itself
|
|
355
|
+
* resets for — data, filters, search, sorting. Remove both when the adapter
|
|
356
|
+
* stops proxying its options.
|
|
357
|
+
*/
|
|
358
|
+
autoResetPageIndex: false,
|
|
359
|
+
get state() {
|
|
360
|
+
return state;
|
|
361
|
+
},
|
|
362
|
+
onSortingChange: (updater) => {
|
|
363
|
+
sorting = applyUpdater(updater, sorting);
|
|
364
|
+
},
|
|
365
|
+
// The raw TanStack instance is a documented escape hatch, so
|
|
366
|
+
// `table.resetColumnFilters()` still has to work. It lands here and is
|
|
367
|
+
// translated back into conditions — one direction of truth.
|
|
368
|
+
onColumnFiltersChange: (updater) => dispatchFilter({
|
|
369
|
+
type: "replace",
|
|
370
|
+
conditions: fromColumnFilters(applyUpdater(updater, columnFilters)),
|
|
371
|
+
}),
|
|
372
|
+
onGlobalFilterChange: (updater) => {
|
|
373
|
+
search = applyUpdater(updater, search);
|
|
374
|
+
},
|
|
375
|
+
onPaginationChange: (updater) => {
|
|
376
|
+
pagination = applyUpdater(updater, pagination);
|
|
377
|
+
},
|
|
378
|
+
onColumnVisibilityChange: (updater) => {
|
|
379
|
+
columnVisibility = applyUpdater(updater, columnVisibility);
|
|
380
|
+
},
|
|
381
|
+
onColumnOrderChange: (updater) => {
|
|
382
|
+
columnOrder = applyUpdater(updater, columnOrder);
|
|
383
|
+
},
|
|
384
|
+
onColumnSizingChange: (updater) => {
|
|
385
|
+
columnSizing = applyUpdater(updater, columnSizing);
|
|
386
|
+
},
|
|
387
|
+
onRowSelectionChange: (updater) => {
|
|
388
|
+
selection = { ...selection, rows: applyUpdater(updater, selection.rows) };
|
|
389
|
+
},
|
|
390
|
+
});
|
|
391
|
+
/**
|
|
392
|
+
* The page reset TanStack would do for us, done here. See `autoResetPageIndex`
|
|
393
|
+
* above for why it cannot.
|
|
394
|
+
*
|
|
395
|
+
* `skipFirstRun` in spirit: a table seeded with `defaultFilters` must not have
|
|
396
|
+
* its page reset on mount, which is why the first pass only records.
|
|
397
|
+
*/
|
|
398
|
+
let resetSeen = false;
|
|
399
|
+
$effect(() => {
|
|
400
|
+
// Read every input TanStack resets on, so this effect depends on them.
|
|
401
|
+
void options().data;
|
|
402
|
+
void columnFilters;
|
|
403
|
+
void deferredSearch.current;
|
|
404
|
+
void sorting;
|
|
405
|
+
if (!resetSeen) {
|
|
406
|
+
resetSeen = true;
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
untrack(() => {
|
|
410
|
+
if (pagination.pageIndex !== 0)
|
|
411
|
+
pagination = { ...pagination, pageIndex: 0 };
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
const rows = $derived(table.getRowModel().rows);
|
|
415
|
+
const visibleColumns = $derived(table.getVisibleLeafColumns());
|
|
416
|
+
// A table narrower than its container would otherwise leave a bare strip where
|
|
417
|
+
// the header band stops. The surplus goes to one column rather than being
|
|
418
|
+
// spread across all of them — see `layoutColumns`.
|
|
419
|
+
const layout = $derived(layoutColumns(visibleColumns.map((column) => ({
|
|
420
|
+
id: column.id,
|
|
421
|
+
size: column.getSize(),
|
|
422
|
+
pinned: PINNED_SIDE[String(column.getIsPinned())] ?? false,
|
|
423
|
+
})), mode === "table" ? viewportWidth : null));
|
|
424
|
+
// Post-filter, pre-pagination: what "1–25 of 312" counts.
|
|
425
|
+
const totalMatching = $derived(manual ? (options().rowCount ?? rows.length) : table.getPrePaginatedRowModel().rows.length);
|
|
426
|
+
const rowOffset = $derived(paginated && !manual ? pagination.pageIndex * pagination.pageSize : 0);
|
|
427
|
+
// ── Virtualization ────────────────────────────────────────────────────────
|
|
428
|
+
const virtualizing = $derived(shouldVirtualize(rows.length, virtualOption));
|
|
429
|
+
// Always constructed — tearing a virtualizer down when a filter drops the row
|
|
430
|
+
// count below the threshold would lose the scroll position. An idle one costs
|
|
431
|
+
// one scroll listener.
|
|
432
|
+
const virtualizer = createVirtualizer({
|
|
433
|
+
get count() {
|
|
434
|
+
return virtualizing ? rows.length : 0;
|
|
435
|
+
},
|
|
436
|
+
getScrollElement: () => viewportEl,
|
|
437
|
+
// Cards are much taller than rows and vary far more, so the first guess
|
|
438
|
+
// differs by mode. Both are corrected by `measureElement` on the first frame.
|
|
439
|
+
estimateSize: () => (mode === "cards" ? CARD_HEIGHT : estimateRowHeight(size)),
|
|
440
|
+
get overscan() {
|
|
441
|
+
return overscanFor(virtualOption);
|
|
442
|
+
},
|
|
443
|
+
// Rows are measured rather than trusted: the estimate is derived from
|
|
444
|
+
// tokens, and a re-themed table would otherwise mis-position its content.
|
|
445
|
+
measureElement: (element) => element.getBoundingClientRect().height,
|
|
446
|
+
});
|
|
447
|
+
const spacers = $derived(virtualizing
|
|
448
|
+
? spacerHeights(virtualizer.current.getVirtualItems(), virtualizer.current.getTotalSize())
|
|
449
|
+
: NO_SPACERS);
|
|
450
|
+
const renderRows = $derived.by(() => {
|
|
451
|
+
const build = (row, index) => ({
|
|
452
|
+
row,
|
|
453
|
+
index,
|
|
454
|
+
// What `aria-rowindex` reports: where it sits in the whole dataset.
|
|
455
|
+
absoluteIndex: index + rowOffset,
|
|
456
|
+
});
|
|
457
|
+
if (!virtualizing)
|
|
458
|
+
return rows.map(build);
|
|
459
|
+
const out = [];
|
|
460
|
+
for (const item of virtualizer.current.getVirtualItems()) {
|
|
461
|
+
const row = rows[item.index];
|
|
462
|
+
if (row)
|
|
463
|
+
out.push(build(row, item.index));
|
|
464
|
+
}
|
|
465
|
+
return out;
|
|
466
|
+
});
|
|
467
|
+
// ── Grid focus ────────────────────────────────────────────────────────────
|
|
468
|
+
let cursorState = $state({ row: 0, col: 0 });
|
|
469
|
+
// A counter, not a boolean: the same cell can be re-focused (after an editor
|
|
470
|
+
// closes) without the cursor changing.
|
|
471
|
+
let focusRequest = $state(0);
|
|
472
|
+
// With no rows there is nothing in the body to hold the tab stop, so it moves
|
|
473
|
+
// to the header — still operable (sorting), still one tab stop.
|
|
474
|
+
const effectiveCursor = $derived(rows.length === 0 ? { row: -1, col: cursorState.col } : cursorState);
|
|
475
|
+
const moveCursor = (next) => {
|
|
476
|
+
if (cursorState.row === next.row && cursorState.col === next.col)
|
|
477
|
+
return;
|
|
478
|
+
cursorState = next;
|
|
479
|
+
};
|
|
480
|
+
/**
|
|
481
|
+
* Follow focus that has already moved — a click, or Tab landing on a cell.
|
|
482
|
+
* Deliberately does NOT take focus.
|
|
483
|
+
*
|
|
484
|
+
* The two used to be one function in the React adapter, and it broke inline
|
|
485
|
+
* editing outright: the editor's input took focus, its cell's `focus` event
|
|
486
|
+
* bubbled, the cursor was "set", and the effect below focused the cell again —
|
|
487
|
+
* pulling focus straight back out of the editor the user had just opened.
|
|
488
|
+
*/
|
|
489
|
+
const setCursor = moveCursor;
|
|
490
|
+
/** Move the cursor AND take DOM focus. Keyboard actions only. */
|
|
491
|
+
const focusCell = (next) => {
|
|
492
|
+
moveCursor(next);
|
|
493
|
+
focusRequest += 1;
|
|
494
|
+
};
|
|
495
|
+
$effect(() => {
|
|
496
|
+
const count = focusRequest;
|
|
497
|
+
const viewport = viewportEl;
|
|
498
|
+
if (count === 0 || !viewport)
|
|
499
|
+
return;
|
|
500
|
+
let attempts = 0;
|
|
501
|
+
const tryFocus = () => {
|
|
502
|
+
const { row, col } = effectiveCursor;
|
|
503
|
+
const element = viewport.querySelector(`[data-cell="${row}:${col}"]`);
|
|
504
|
+
if (element) {
|
|
505
|
+
element.focus();
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
// A PageDown into un-rendered territory scrolls first; the row exists one
|
|
509
|
+
// frame later. Two attempts, then give up rather than spin.
|
|
510
|
+
if (attempts++ < 2 && typeof requestAnimationFrame === "function") {
|
|
511
|
+
requestAnimationFrame(tryFocus);
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
tryFocus();
|
|
515
|
+
});
|
|
516
|
+
// ── Selection ─────────────────────────────────────────────────────────────
|
|
517
|
+
const pageRowIds = $derived(rows.map((row) => row.id));
|
|
518
|
+
/**
|
|
519
|
+
* The rows the header checkbox governs: what is **on screen**.
|
|
520
|
+
*
|
|
521
|
+
* For a paginated table that is the page. For a virtualized one it is the
|
|
522
|
+
* rendered window, and that distinction is the whole point — `rows` on the
|
|
523
|
+
* 100,000-row table is all 100,000, so a header checkbox scoped to it answers
|
|
524
|
+
* one click by enumerating a hundred thousand ids.
|
|
525
|
+
*/
|
|
526
|
+
const visibleRowIds = $derived(renderRows.map((entry) => entry.row.id));
|
|
527
|
+
const headerState = $derived(headerCheckboxState(selection, visibleRowIds));
|
|
528
|
+
const toggleRow = (rowId, checked, shift = false) => {
|
|
529
|
+
selection =
|
|
530
|
+
selectionMode === "single"
|
|
531
|
+
? { ...EMPTY_SELECTION, rows: checked ? { [rowId]: true } : {}, anchor: rowId }
|
|
532
|
+
: rangeSelect(selection, pageRowIds, rowId, shift, checked);
|
|
533
|
+
};
|
|
534
|
+
const togglePage = (checked) => {
|
|
535
|
+
selection = selectPage(selection, visibleRowIds, checked);
|
|
536
|
+
};
|
|
537
|
+
const selectEverythingMatching = () => {
|
|
538
|
+
const next = selectAllMatchingCore(selection);
|
|
539
|
+
// The loaded rows are materialised as well as the flag, so
|
|
540
|
+
// `getSelectedRowModel()` — and therefore CSV export and any bulk action
|
|
541
|
+
// over real row objects — still works for the rows the client has.
|
|
542
|
+
const rowsState = {};
|
|
543
|
+
for (const row of table.getPrePaginatedRowModel().rows)
|
|
544
|
+
rowsState[row.id] = true;
|
|
545
|
+
selection = { ...next, rows: rowsState };
|
|
546
|
+
};
|
|
547
|
+
const selectedRows = $derived.by(() => {
|
|
548
|
+
// `selection` is read explicitly even though `getSelectedRowModel` depends on
|
|
549
|
+
// it: the table instance is stable and its row-model call is memoized, so
|
|
550
|
+
// without this read the derived would have no dependency at all.
|
|
551
|
+
void selection;
|
|
552
|
+
return table.getSelectedRowModel().rows.map((row) => row.original);
|
|
553
|
+
});
|
|
554
|
+
// ── Inline edit ───────────────────────────────────────────────────────────
|
|
555
|
+
const startEdit = (row, colId) => {
|
|
556
|
+
if (!editableColumns.has(colId))
|
|
557
|
+
return;
|
|
558
|
+
stores.edit.dispatch({
|
|
559
|
+
type: "start",
|
|
560
|
+
target: { rowId: row.id, columnId: colId },
|
|
561
|
+
value: row.getValue(colId),
|
|
562
|
+
});
|
|
563
|
+
};
|
|
564
|
+
const commitEdit = () => {
|
|
565
|
+
const editing = stores.edit.getState();
|
|
566
|
+
const target = editing.target;
|
|
567
|
+
// `pending` is the re-entry guard. A commit is reachable from three places
|
|
568
|
+
// at once — Enter, Tab, and the editor's own blur as focus returns to the
|
|
569
|
+
// cell — and without this an edit is written twice.
|
|
570
|
+
if (!target || editing.invalid || editing.pending)
|
|
571
|
+
return;
|
|
572
|
+
const row = table.getRow(target.rowId);
|
|
573
|
+
const column = allColumns.find((def) => columnIdOf(def) === target.columnId);
|
|
574
|
+
const edit = metaOf(column)?.edit;
|
|
575
|
+
const value = edit?.parse ? edit.parse(editing.draft) : editing.draft;
|
|
576
|
+
const message = edit?.validate?.(value, row.original) ?? null;
|
|
577
|
+
if (message) {
|
|
578
|
+
stores.edit.dispatch({ type: "invalid", message });
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
const patch = {
|
|
582
|
+
rowId: target.rowId,
|
|
583
|
+
columnId: target.columnId,
|
|
584
|
+
row: row.original,
|
|
585
|
+
value,
|
|
586
|
+
previousValue: editing.original,
|
|
587
|
+
};
|
|
588
|
+
stores.edit.dispatch({ type: "commit-start" });
|
|
589
|
+
Promise.resolve(options().onEdit?.(patch)).then(() => stores.edit.dispatch({ type: "commit-done" }), (error) => stores.edit.dispatch({
|
|
590
|
+
type: "commit-failed",
|
|
591
|
+
message: error instanceof Error ? error.message : "Could not save the change",
|
|
592
|
+
}));
|
|
593
|
+
};
|
|
594
|
+
const editContextFor = (row, colId) => {
|
|
595
|
+
if (!isEditingCell(editState.current, row.id, colId))
|
|
596
|
+
return null;
|
|
597
|
+
return {
|
|
598
|
+
row: row.original,
|
|
599
|
+
rowId: row.id,
|
|
600
|
+
columnId: colId,
|
|
601
|
+
value: editState.current.draft,
|
|
602
|
+
size,
|
|
603
|
+
invalid: editState.current.invalid,
|
|
604
|
+
setValue: (value) => {
|
|
605
|
+
const column = allColumns.find((def) => columnIdOf(def) === colId);
|
|
606
|
+
const validate = metaOf(column)?.edit?.validate;
|
|
607
|
+
stores.edit.dispatch({
|
|
608
|
+
type: "change",
|
|
609
|
+
value,
|
|
610
|
+
invalid: validate?.(value, row.original) ?? null,
|
|
611
|
+
});
|
|
612
|
+
},
|
|
613
|
+
commit: commitEdit,
|
|
614
|
+
cancel: () => stores.edit.dispatch({ type: "cancel" }),
|
|
615
|
+
};
|
|
616
|
+
};
|
|
617
|
+
// ── Row detail ────────────────────────────────────────────────────────────
|
|
618
|
+
const openDetail = (rowId) => {
|
|
619
|
+
stores.detail.dispatch({ type: "open", rowId, index: pageRowIds.indexOf(rowId) });
|
|
620
|
+
};
|
|
621
|
+
const stepDetail = (direction) => {
|
|
622
|
+
const detail = stores.detail.getState();
|
|
623
|
+
if (detail.dirty) {
|
|
624
|
+
stores.detail.dispatch({ type: "confirm", intent: direction === -1 ? "prev" : "next" });
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
const next = stepRow(pageRowIds, detail.rowId, direction);
|
|
628
|
+
if (next)
|
|
629
|
+
stores.detail.dispatch({ type: "go", ...next });
|
|
630
|
+
};
|
|
631
|
+
const closeDetail = () => {
|
|
632
|
+
if (stores.detail.getState().dirty) {
|
|
633
|
+
stores.detail.dispatch({ type: "confirm", intent: "close" });
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
stores.detail.dispatch({ type: "close" });
|
|
637
|
+
};
|
|
638
|
+
const resolveDetailConfirm = (discard) => {
|
|
639
|
+
const detail = stores.detail.getState();
|
|
640
|
+
const intent = detail.confirming;
|
|
641
|
+
stores.detail.dispatch({ type: "dismiss" });
|
|
642
|
+
if (!discard || !intent)
|
|
643
|
+
return;
|
|
644
|
+
if (intent === "close") {
|
|
645
|
+
stores.detail.dispatch({ type: "close" });
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
const next = stepRow(pageRowIds, detail.rowId, intent === "prev" ? -1 : 1);
|
|
649
|
+
if (next)
|
|
650
|
+
stores.detail.dispatch({ type: "go", ...next });
|
|
651
|
+
};
|
|
652
|
+
const detailRow = $derived.by(() => {
|
|
653
|
+
const rowId = detailState.current.rowId;
|
|
654
|
+
if (!rowId)
|
|
655
|
+
return null;
|
|
656
|
+
const row = rows.find((candidate) => candidate.id === rowId);
|
|
657
|
+
return row ? row.original : null;
|
|
658
|
+
});
|
|
659
|
+
// ── Export ────────────────────────────────────────────────────────────────
|
|
660
|
+
const exportCsv = (scope = "view") => {
|
|
661
|
+
exportRowsToCsv(table, {
|
|
662
|
+
scope,
|
|
663
|
+
filename: `${label.toLowerCase().replaceAll(/\s+/g, "-")}.csv`,
|
|
664
|
+
});
|
|
665
|
+
};
|
|
666
|
+
const copySelection = (scope = "selected") => copyRowsToClipboard(table, { scope });
|
|
667
|
+
// ── Column reordering ───────────────────────────────────────────────────────
|
|
668
|
+
// `reorderable: true | "always"` keeps headers movable all the time;
|
|
669
|
+
// `"toggle"` only inside a reorder mode the toolbar switches. Card mode has
|
|
670
|
+
// no headers to move, so it is never live there.
|
|
671
|
+
const reorderMode = $derived(reorderModeOf(options().reorderable));
|
|
672
|
+
let reorderOn = $state(false);
|
|
673
|
+
const reorderActive = $derived(mode !== "cards" && (reorderMode === "always" || (reorderMode === "toggle" && reorderOn)));
|
|
674
|
+
let drag = $state(null);
|
|
675
|
+
const isMovable = (id) => {
|
|
676
|
+
const column = table.getColumn(id);
|
|
677
|
+
return Boolean(column?.getIsVisible()) && isMovableColumn(id, column.getIsPinned() !== false);
|
|
678
|
+
};
|
|
679
|
+
const orderNow = () => currentColumnOrder(columnOrder, table.getAllLeafColumns().map((column) => column.id));
|
|
680
|
+
const canMoveColumn = (id, direction) => reorderActive && canStepColumn(orderNow(), id, direction, isMovable);
|
|
681
|
+
const moveColumnBy = (id, direction) => {
|
|
682
|
+
if (!canMoveColumn(id, direction))
|
|
683
|
+
return;
|
|
684
|
+
table.setColumnOrder(stepColumn(orderNow(), id, direction, isMovable));
|
|
685
|
+
};
|
|
686
|
+
const dropColumnOn = (source, target, side) => {
|
|
687
|
+
if (!reorderActive || !isMovable(source) || !isMovable(target))
|
|
688
|
+
return;
|
|
689
|
+
table.setColumnOrder(dropColumn(orderNow(), source, target, side));
|
|
690
|
+
};
|
|
691
|
+
// ── Keyboard ──────────────────────────────────────────────────────────────
|
|
692
|
+
const onGridKeyDown = (event) => {
|
|
693
|
+
if (!interactive)
|
|
694
|
+
return;
|
|
695
|
+
const cursor = effectiveCursor;
|
|
696
|
+
const column = visibleColumns[cursor.col];
|
|
697
|
+
const pageRows = Math.max(1, Math.floor((viewportEl?.clientHeight ?? 0) / estimateRowHeight(size)) - 1);
|
|
698
|
+
const action = gridKeyDown({
|
|
699
|
+
cursor,
|
|
700
|
+
rowCount: rows.length,
|
|
701
|
+
colCount: visibleColumns.length,
|
|
702
|
+
pageRows,
|
|
703
|
+
editing: Boolean(editState.current.target),
|
|
704
|
+
editable: column ? editableColumns.has(column.id) : false,
|
|
705
|
+
selectable: selectionMode !== "none",
|
|
706
|
+
multiSelect: selectionMode === "multiple",
|
|
707
|
+
reorderable: Boolean(column) && cursor.row < 0 && reorderActive && isMovable(column.id),
|
|
708
|
+
}, {
|
|
709
|
+
key: event.key,
|
|
710
|
+
shift: event.shiftKey,
|
|
711
|
+
ctrl: event.ctrlKey,
|
|
712
|
+
meta: event.metaKey,
|
|
713
|
+
alt: event.altKey,
|
|
714
|
+
});
|
|
715
|
+
if (action.type === "none")
|
|
716
|
+
return;
|
|
717
|
+
event.preventDefault();
|
|
718
|
+
switch (action.type) {
|
|
719
|
+
case "move":
|
|
720
|
+
focusCell(action.cursor);
|
|
721
|
+
break;
|
|
722
|
+
case "move-column":
|
|
723
|
+
// Stepping trades places with the next movable visible column, which
|
|
724
|
+
// is the neighbouring header, so the column is now one index over.
|
|
725
|
+
// Focus follows it there once the new order has rendered.
|
|
726
|
+
if (column && canMoveColumn(column.id, action.direction)) {
|
|
727
|
+
moveColumnBy(column.id, action.direction);
|
|
728
|
+
focusCell({ row: -1, col: cursor.col + action.direction });
|
|
729
|
+
}
|
|
730
|
+
break;
|
|
731
|
+
case "page":
|
|
732
|
+
if (virtualizing)
|
|
733
|
+
virtualizer.current.scrollToIndex(action.cursor.row, { align: "center" });
|
|
734
|
+
focusCell(action.cursor);
|
|
735
|
+
break;
|
|
736
|
+
case "extend": {
|
|
737
|
+
const anchorRow = rows[cursor.row];
|
|
738
|
+
const targetRow = rows[action.cursor.row];
|
|
739
|
+
if (anchorRow && targetRow) {
|
|
740
|
+
const seeded = selection.anchor ? selection : { ...selection, anchor: anchorRow.id };
|
|
741
|
+
selection = rangeSelect(seeded, pageRowIds, targetRow.id, true, true);
|
|
742
|
+
}
|
|
743
|
+
focusCell(action.cursor);
|
|
744
|
+
break;
|
|
745
|
+
}
|
|
746
|
+
case "toggle-select": {
|
|
747
|
+
const row = rows[cursor.row];
|
|
748
|
+
if (row)
|
|
749
|
+
toggleRow(row.id, !isRowSelected(selection, row.id));
|
|
750
|
+
break;
|
|
751
|
+
}
|
|
752
|
+
case "select-all":
|
|
753
|
+
togglePage(true);
|
|
754
|
+
break;
|
|
755
|
+
case "copy":
|
|
756
|
+
void copySelection(action.scope);
|
|
757
|
+
break;
|
|
758
|
+
case "edit": {
|
|
759
|
+
const row = rows[cursor.row];
|
|
760
|
+
if (row && column)
|
|
761
|
+
startEdit(row, column.id);
|
|
762
|
+
break;
|
|
763
|
+
}
|
|
764
|
+
case "commit":
|
|
765
|
+
commitEdit();
|
|
766
|
+
// The editor is about to unmount; focus has to be given somewhere
|
|
767
|
+
// deliberate, or it falls to <body> and the grid loses the user.
|
|
768
|
+
focusCell(cursor);
|
|
769
|
+
break;
|
|
770
|
+
case "commit-and-move":
|
|
771
|
+
commitEdit();
|
|
772
|
+
focusCell(action.cursor);
|
|
773
|
+
break;
|
|
774
|
+
case "cancel":
|
|
775
|
+
stores.edit.dispatch({ type: "cancel" });
|
|
776
|
+
focusCell(cursor);
|
|
777
|
+
break;
|
|
778
|
+
case "activate": {
|
|
779
|
+
if (cursor.row < 0) {
|
|
780
|
+
if (column?.getCanSort())
|
|
781
|
+
column.toggleSorting();
|
|
782
|
+
break;
|
|
783
|
+
}
|
|
784
|
+
const row = rows[cursor.row];
|
|
785
|
+
if (!row)
|
|
786
|
+
break;
|
|
787
|
+
if (options().detail)
|
|
788
|
+
openDetail(row.id);
|
|
789
|
+
else
|
|
790
|
+
options().onRowClick?.(row.original);
|
|
791
|
+
break;
|
|
792
|
+
}
|
|
793
|
+
default:
|
|
794
|
+
break;
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
// ── Server mode ───────────────────────────────────────────────────────────
|
|
798
|
+
$effect(() => {
|
|
799
|
+
if (!manual)
|
|
800
|
+
return;
|
|
801
|
+
const onQueryChange = options().onQueryChange;
|
|
802
|
+
if (!onQueryChange)
|
|
803
|
+
return;
|
|
804
|
+
onQueryChange({
|
|
805
|
+
sorting: sorting.map((entry) => ({ id: entry.id, desc: entry.desc })),
|
|
806
|
+
filters: conditions,
|
|
807
|
+
search: deferredSearch.current,
|
|
808
|
+
pageIndex: pagination.pageIndex,
|
|
809
|
+
pageSize: pagination.pageSize,
|
|
810
|
+
});
|
|
811
|
+
});
|
|
812
|
+
// Infinite scroll. Attached to the viewport rather than to the virtualizer so
|
|
813
|
+
// it works in the un-virtualized case too.
|
|
814
|
+
$effect(() => {
|
|
815
|
+
const viewport = viewportEl;
|
|
816
|
+
const onLoadMore = options().onLoadMore;
|
|
817
|
+
if (!viewport || !onLoadMore)
|
|
818
|
+
return;
|
|
819
|
+
const rowHeight = estimateRowHeight(size);
|
|
820
|
+
const onScroll = () => {
|
|
821
|
+
if (isNearEnd({
|
|
822
|
+
scrollTop: viewport.scrollTop,
|
|
823
|
+
scrollHeight: viewport.scrollHeight,
|
|
824
|
+
clientHeight: viewport.clientHeight,
|
|
825
|
+
}, rowHeight)) {
|
|
826
|
+
onLoadMore();
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
viewport.addEventListener("scroll", onScroll, { passive: true });
|
|
830
|
+
return () => viewport.removeEventListener("scroll", onScroll);
|
|
831
|
+
});
|
|
832
|
+
// ── Filters ───────────────────────────────────────────────────────────────
|
|
833
|
+
let announcement = $state("");
|
|
834
|
+
const filterDefs = $derived.by(() => {
|
|
835
|
+
const defs = new Map();
|
|
836
|
+
for (const def of userColumns) {
|
|
837
|
+
const filter = normalizeFilterDef(metaOf(def)?.filter);
|
|
838
|
+
if (filter)
|
|
839
|
+
defs.set(columnIdOf(def), filter);
|
|
840
|
+
}
|
|
841
|
+
return defs;
|
|
842
|
+
});
|
|
843
|
+
const columnLabel = (id) => {
|
|
844
|
+
const column = table.getColumn(id);
|
|
845
|
+
const header = column?.columnDef.header;
|
|
846
|
+
return filterDefs.get(id)?.label ?? (typeof header === "string" ? header : id);
|
|
847
|
+
};
|
|
848
|
+
const typeOfColumn = (id) => filterTypes[id] ?? "text";
|
|
849
|
+
const fields = $derived.by(() => {
|
|
850
|
+
const counts = new Map();
|
|
851
|
+
for (const condition of conditions) {
|
|
852
|
+
counts.set(condition.columnId, (counts.get(condition.columnId) ?? 0) + 1);
|
|
853
|
+
}
|
|
854
|
+
return orderFilterableFields([...filterDefs.entries()].map(([columnId, def], index) => ({
|
|
855
|
+
columnId,
|
|
856
|
+
label: columnLabel(columnId),
|
|
857
|
+
type: typeOfColumn(columnId),
|
|
858
|
+
count: counts.get(columnId) ?? 0,
|
|
859
|
+
priority: def?.priority,
|
|
860
|
+
index,
|
|
861
|
+
})));
|
|
862
|
+
});
|
|
863
|
+
const describe = (condition) => describeCondition(condition, {
|
|
864
|
+
type: typeOfColumn(condition.columnId),
|
|
865
|
+
columnLabel: columnLabel(condition.columnId),
|
|
866
|
+
catalogue,
|
|
867
|
+
options: filterDefs.get(condition.columnId)?.options,
|
|
868
|
+
formatValue: filterDefs.get(condition.columnId)?.formatValue,
|
|
869
|
+
});
|
|
870
|
+
/** Announce discrete changes only — a per-keystroke live region is a firehose. */
|
|
871
|
+
const announce = (kind, condition) => {
|
|
872
|
+
announcement = filterAnnouncement({ kind, description: condition ? describe(condition).text : undefined }, totalMatching);
|
|
873
|
+
};
|
|
874
|
+
const addFilter = (columnId) => {
|
|
875
|
+
const def = filterDefs.get(columnId);
|
|
876
|
+
const type = typeOfColumn(columnId);
|
|
877
|
+
// The id the reducer is about to assign, returned so the caller can open the
|
|
878
|
+
// new chip's editor without waiting for a render to find it.
|
|
879
|
+
const id = `f${filterState.nextId}`;
|
|
880
|
+
dispatchFilter({
|
|
881
|
+
type: "add",
|
|
882
|
+
columnId,
|
|
883
|
+
operator: defaultOperatorFor(type, catalogue, def),
|
|
884
|
+
single: def?.single,
|
|
885
|
+
});
|
|
886
|
+
return id;
|
|
887
|
+
};
|
|
888
|
+
/** The chip's field segment: move a condition to another column. */
|
|
889
|
+
const changeFilterField = (id, columnId) => {
|
|
890
|
+
const condition = conditions.find((entry) => entry.id === id);
|
|
891
|
+
if (!condition || condition.columnId === columnId)
|
|
892
|
+
return;
|
|
893
|
+
dispatchFilter(retargetCondition(condition, { type: typeOfColumn(condition.columnId) }, { columnId, type: typeOfColumn(columnId), def: filterDefs.get(columnId) }, catalogue));
|
|
894
|
+
};
|
|
895
|
+
const removeFilter = (id) => {
|
|
896
|
+
const condition = conditions.find((entry) => entry.id === id);
|
|
897
|
+
if (condition)
|
|
898
|
+
announce("removed", condition);
|
|
899
|
+
dispatchFilter({ type: "remove", id });
|
|
900
|
+
};
|
|
901
|
+
const clearFilters = () => {
|
|
902
|
+
announce("cleared");
|
|
903
|
+
dispatchFilter({ type: "clear" });
|
|
904
|
+
};
|
|
905
|
+
/**
|
|
906
|
+
* `getFacetedUniqueValues` allocates an array per row, so it is capped: above
|
|
907
|
+
* `FACET_ROW_LIMIT` the options render without counts and nothing is disabled.
|
|
908
|
+
*/
|
|
909
|
+
const optionsForCondition = (condition) => {
|
|
910
|
+
const def = filterDefs.get(condition.columnId);
|
|
911
|
+
const column = table.getColumn(condition.columnId);
|
|
912
|
+
const showCounts = def?.counts ?? rows.length <= FACET_ROW_LIMIT;
|
|
913
|
+
return facetOptions(showCounts ? column?.getFacetedUniqueValues() : undefined, def?.options, condition.values, showCounts);
|
|
914
|
+
};
|
|
915
|
+
const operatorsForCondition = (condition) => {
|
|
916
|
+
const type = typeOfColumn(condition.columnId);
|
|
917
|
+
return operatorsFor(type, catalogue, filterDefs.get(condition.columnId)?.operators).map((op) => ({ id: op.id, label: operatorLabel(op, type) }));
|
|
918
|
+
};
|
|
919
|
+
const activate = (row) => {
|
|
920
|
+
if (options().detail)
|
|
921
|
+
openDetail(row.id);
|
|
922
|
+
else
|
|
923
|
+
options().onRowClick?.(row.original);
|
|
924
|
+
};
|
|
925
|
+
const chrome = $derived({
|
|
926
|
+
size,
|
|
927
|
+
variant,
|
|
928
|
+
interactive,
|
|
929
|
+
totalWidth: layout.totalWidth,
|
|
930
|
+
rowCount: totalMatching,
|
|
931
|
+
colCount: visibleColumns.length,
|
|
932
|
+
label,
|
|
933
|
+
multiSelectable: selectionMode === "multiple",
|
|
934
|
+
});
|
|
935
|
+
const scrollBy = (direction) => {
|
|
936
|
+
const element = viewportEl;
|
|
937
|
+
if (!element)
|
|
938
|
+
return;
|
|
939
|
+
const left = horizontalScrollTarget(element, direction);
|
|
940
|
+
// Smooth unless the user asked for less motion — and unless the engine has
|
|
941
|
+
// no options-taking `scrollTo` at all (jsdom), where the assignment is the
|
|
942
|
+
// same scroll without the animation.
|
|
943
|
+
if (typeof element.scrollTo === "function" && !prefersReducedMotion()) {
|
|
944
|
+
element.scrollTo({ left, behavior: "smooth" });
|
|
945
|
+
}
|
|
946
|
+
else {
|
|
947
|
+
element.scrollLeft = left;
|
|
948
|
+
}
|
|
949
|
+
};
|
|
950
|
+
return {
|
|
951
|
+
table,
|
|
952
|
+
get state() {
|
|
953
|
+
return state;
|
|
954
|
+
},
|
|
955
|
+
get rows() {
|
|
956
|
+
return rows;
|
|
957
|
+
},
|
|
958
|
+
get renderRows() {
|
|
959
|
+
return renderRows;
|
|
960
|
+
},
|
|
961
|
+
get chrome() {
|
|
962
|
+
return chrome;
|
|
963
|
+
},
|
|
964
|
+
get options() {
|
|
965
|
+
return options();
|
|
966
|
+
},
|
|
967
|
+
get size() {
|
|
968
|
+
return size;
|
|
969
|
+
},
|
|
970
|
+
get variant() {
|
|
971
|
+
return variant;
|
|
972
|
+
},
|
|
973
|
+
get label() {
|
|
974
|
+
return label;
|
|
975
|
+
},
|
|
976
|
+
get captionVisible() {
|
|
977
|
+
return captionVisible;
|
|
978
|
+
},
|
|
979
|
+
get interactive() {
|
|
980
|
+
return interactive;
|
|
981
|
+
},
|
|
982
|
+
get mode() {
|
|
983
|
+
return mode;
|
|
984
|
+
},
|
|
985
|
+
get loading() {
|
|
986
|
+
return loading;
|
|
987
|
+
},
|
|
988
|
+
get search() {
|
|
989
|
+
return search;
|
|
990
|
+
},
|
|
991
|
+
setSearch: (value) => {
|
|
992
|
+
search = value;
|
|
993
|
+
},
|
|
994
|
+
selection: {
|
|
995
|
+
get mode() {
|
|
996
|
+
return selectionMode;
|
|
997
|
+
},
|
|
998
|
+
get state() {
|
|
999
|
+
return selection;
|
|
1000
|
+
},
|
|
1001
|
+
get count() {
|
|
1002
|
+
return selectionCountCore(selection, totalMatching);
|
|
1003
|
+
},
|
|
1004
|
+
get rows() {
|
|
1005
|
+
return selectedRows;
|
|
1006
|
+
},
|
|
1007
|
+
isSelected: (rowId) => isRowSelected(selection, rowId),
|
|
1008
|
+
toggle: toggleRow,
|
|
1009
|
+
togglePage,
|
|
1010
|
+
selectAllMatching: selectEverythingMatching,
|
|
1011
|
+
clear: () => {
|
|
1012
|
+
selection = EMPTY_SELECTION;
|
|
1013
|
+
},
|
|
1014
|
+
get header() {
|
|
1015
|
+
return headerState;
|
|
1016
|
+
},
|
|
1017
|
+
get canSelectAllMatching() {
|
|
1018
|
+
return canSelectHiddenRows(selection, headerState, totalMatching, selectionMode);
|
|
1019
|
+
},
|
|
1020
|
+
get totalMatching() {
|
|
1021
|
+
return totalMatching;
|
|
1022
|
+
},
|
|
1023
|
+
asBulk: () => toBulkSelection(selection),
|
|
1024
|
+
},
|
|
1025
|
+
filters: {
|
|
1026
|
+
get conditions() {
|
|
1027
|
+
return conditions;
|
|
1028
|
+
},
|
|
1029
|
+
get fields() {
|
|
1030
|
+
return fields;
|
|
1031
|
+
},
|
|
1032
|
+
add: addFilter,
|
|
1033
|
+
update: (id, patch) => dispatchFilter({ type: "update", id, ...patch }),
|
|
1034
|
+
changeField: changeFilterField,
|
|
1035
|
+
restore: (condition) => dispatchFilter({ type: "restore", condition }),
|
|
1036
|
+
remove: removeFilter,
|
|
1037
|
+
clear: clearFilters,
|
|
1038
|
+
describe,
|
|
1039
|
+
operatorsFor: operatorsForCondition,
|
|
1040
|
+
optionsFor: optionsForCondition,
|
|
1041
|
+
typeOf: (condition) => typeOfColumn(condition.columnId),
|
|
1042
|
+
arityOf: (condition) => catalogue.get(condition.operator)?.arity ?? "one",
|
|
1043
|
+
get expanded() {
|
|
1044
|
+
return filterState.expanded;
|
|
1045
|
+
},
|
|
1046
|
+
setExpanded: (expanded) => dispatchFilter({ type: "set-expanded", expanded }),
|
|
1047
|
+
get editing() {
|
|
1048
|
+
return filterState.editingId;
|
|
1049
|
+
},
|
|
1050
|
+
setEditing: (id) => dispatchFilter({ type: "set-editing", id }),
|
|
1051
|
+
get announcement() {
|
|
1052
|
+
return announcement;
|
|
1053
|
+
},
|
|
1054
|
+
},
|
|
1055
|
+
edit: {
|
|
1056
|
+
get enabled() {
|
|
1057
|
+
return editableColumns.size > 0;
|
|
1058
|
+
},
|
|
1059
|
+
get state() {
|
|
1060
|
+
return editState.current;
|
|
1061
|
+
},
|
|
1062
|
+
isEditing: (rowId, colId) => isEditingCell(editState.current, rowId, colId),
|
|
1063
|
+
start: startEdit,
|
|
1064
|
+
setValue: (value) => stores.edit.dispatch({ type: "change", value }),
|
|
1065
|
+
commit: commitEdit,
|
|
1066
|
+
cancel: () => stores.edit.dispatch({ type: "cancel" }),
|
|
1067
|
+
contextFor: editContextFor,
|
|
1068
|
+
},
|
|
1069
|
+
detail: {
|
|
1070
|
+
get enabled() {
|
|
1071
|
+
return Boolean(options().detail);
|
|
1072
|
+
},
|
|
1073
|
+
get state() {
|
|
1074
|
+
return detailState.current;
|
|
1075
|
+
},
|
|
1076
|
+
get row() {
|
|
1077
|
+
return detailRow;
|
|
1078
|
+
},
|
|
1079
|
+
get total() {
|
|
1080
|
+
return totalMatching;
|
|
1081
|
+
},
|
|
1082
|
+
open: openDetail,
|
|
1083
|
+
close: closeDetail,
|
|
1084
|
+
step: stepDetail,
|
|
1085
|
+
canStep: (direction) => stepRow(pageRowIds, detailState.current.rowId, direction) !== null,
|
|
1086
|
+
setDirty: (dirty) => stores.detail.dispatch({ type: "dirty", dirty }),
|
|
1087
|
+
resolveConfirm: resolveDetailConfirm,
|
|
1088
|
+
},
|
|
1089
|
+
focus: {
|
|
1090
|
+
get cursor() {
|
|
1091
|
+
return effectiveCursor;
|
|
1092
|
+
},
|
|
1093
|
+
setCursor,
|
|
1094
|
+
focusCell,
|
|
1095
|
+
isFocused: (rowIndex, colIndex) => effectiveCursor.row === rowIndex && effectiveCursor.col === colIndex,
|
|
1096
|
+
},
|
|
1097
|
+
virtual: {
|
|
1098
|
+
get enabled() {
|
|
1099
|
+
return virtualizing;
|
|
1100
|
+
},
|
|
1101
|
+
get virtualizer() {
|
|
1102
|
+
return virtualizer.current;
|
|
1103
|
+
},
|
|
1104
|
+
get spacers() {
|
|
1105
|
+
return spacers;
|
|
1106
|
+
},
|
|
1107
|
+
},
|
|
1108
|
+
scroll: {
|
|
1109
|
+
get state() {
|
|
1110
|
+
return scrollState;
|
|
1111
|
+
},
|
|
1112
|
+
by: scrollBy,
|
|
1113
|
+
},
|
|
1114
|
+
reorder: {
|
|
1115
|
+
get mode() {
|
|
1116
|
+
return reorderMode;
|
|
1117
|
+
},
|
|
1118
|
+
get active() {
|
|
1119
|
+
return reorderActive;
|
|
1120
|
+
},
|
|
1121
|
+
setActive: (active) => {
|
|
1122
|
+
reorderOn = active;
|
|
1123
|
+
},
|
|
1124
|
+
isMovable,
|
|
1125
|
+
canMove: canMoveColumn,
|
|
1126
|
+
move: moveColumnBy,
|
|
1127
|
+
drop: dropColumnOn,
|
|
1128
|
+
get drag() {
|
|
1129
|
+
return drag;
|
|
1130
|
+
},
|
|
1131
|
+
setDrag: (next) => {
|
|
1132
|
+
drag = next;
|
|
1133
|
+
},
|
|
1134
|
+
},
|
|
1135
|
+
get columnWidths() {
|
|
1136
|
+
return layout.widths;
|
|
1137
|
+
},
|
|
1138
|
+
rootRef: (element) => {
|
|
1139
|
+
rootEl = element;
|
|
1140
|
+
},
|
|
1141
|
+
viewportRef: (element) => {
|
|
1142
|
+
viewportEl = element;
|
|
1143
|
+
},
|
|
1144
|
+
exportCsv,
|
|
1145
|
+
copySelection,
|
|
1146
|
+
onGridKeyDown,
|
|
1147
|
+
activate,
|
|
1148
|
+
get primaryColumnId() {
|
|
1149
|
+
return primaryId;
|
|
1150
|
+
},
|
|
1151
|
+
};
|
|
1152
|
+
}
|