@jielga/tmdatagrid 2.0.0-beta.2 → 2.0.0-beta.21
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/README.md +5 -212
- package/dist/index.d.ts +1664 -632
- package/dist/index.js +5226 -3223
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/docs/anatomy.md +102 -0
- package/docs/cell-selection.md +154 -0
- package/docs/column-layout.md +204 -0
- package/docs/columns.md +262 -0
- package/docs/components.md +304 -0
- package/docs/editing.md +603 -0
- package/docs/editors.md +250 -0
- package/docs/export.md +326 -0
- package/docs/filtering.md +358 -0
- package/docs/getting-started.md +123 -0
- package/docs/grouping.md +165 -0
- package/docs/loading-and-empty.md +92 -0
- package/docs/localization.md +79 -0
- package/docs/menu.md +143 -0
- package/docs/pagination.md +144 -0
- package/docs/persistence.md +111 -0
- package/docs/portfolio-rebalancer.md +94 -0
- package/docs/query-builder.md +175 -0
- package/docs/quick-search.md +83 -0
- package/docs/row-details.md +113 -0
- package/docs/row-interaction.md +148 -0
- package/docs/row-pinning.md +132 -0
- package/docs/row-selection.md +134 -0
- package/docs/row-styling.md +133 -0
- package/docs/scrolling.md +111 -0
- package/docs/server-query.md +246 -0
- package/docs/server-side.md +206 -0
- package/docs/sorting.md +101 -0
- package/docs/styling.md +126 -0
- package/docs/summary-row.md +76 -0
- package/docs/testing.md +309 -0
- package/docs/toolbar.md +161 -0
- package/docs/use-tm-data-grid.md +361 -0
- package/package.json +21 -45
- package/skills/appearance/SKILL.md +70 -17
- package/skills/cell-selection/SKILL.md +70 -76
- package/skills/columns/SKILL.md +131 -32
- package/skills/data/SKILL.md +100 -23
- package/skills/editing/SKILL.md +217 -96
- package/skills/editing/references/common-mistakes.md +111 -24
- package/skills/editing/references/editing-api.md +63 -39
- package/skills/editing/references/editors-and-validation.md +77 -19
- package/skills/filtering/SKILL.md +148 -40
- package/skills/getting-started/SKILL.md +18 -16
- package/skills/grouping/SKILL.md +32 -15
- package/skills/options/SKILL.md +39 -9
- package/skills/rows/SKILL.md +22 -18
- package/skills/server-side/SKILL.md +170 -17
- package/skills/testing/SKILL.md +10 -7
- package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +7 -19
- package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +7 -1
- package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +39 -23
- package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +106 -38
- package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
- package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +47 -55
- package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +4 -4
- package/src/components/TMDataGridDraftActions.tsx +307 -0
- package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +58 -50
- package/src/{tmdatagrid/components → components}/TMDataGridEntryRows.tsx +150 -115
- package/src/components/TMDataGridExportPicker.module.css +77 -0
- package/src/components/TMDataGridExportPicker.tsx +234 -0
- package/src/components/TMDataGridFilterPanel.module.css +54 -0
- package/src/components/TMDataGridFilterPanel.tsx +348 -0
- package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +7 -5
- package/src/components/TMDataGridFilterSurface.module.css +54 -0
- package/src/components/TMDataGridFilterSurface.tsx +167 -0
- package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -13
- package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +4 -3
- package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +10 -0
- package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +100 -28
- package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
- package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
- package/src/components/TMDataGridMenu.tsx +354 -0
- package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +12 -7
- package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +90 -67
- package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +678 -156
- package/src/components/TMDataGridToolbar.module.css +21 -0
- package/src/components/TMDataGridToolbar.tsx +181 -0
- package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +3 -3
- package/src/components/editors/TMDataGridNumberEditor.tsx +70 -0
- package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +3 -3
- package/src/{tmdatagrid/components → components}/editors/editorShared.ts +17 -31
- package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +5 -4
- package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +22 -5
- package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +5 -1
- package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +5 -1
- package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +44 -28
- package/src/components/filters/controlLayout.ts +32 -0
- package/src/components/filters/filterControlFor.ts +65 -0
- package/src/{tmdatagrid/components → components}/icons.ts +1 -0
- package/src/{tmdatagrid/components → components}/sticky.module.css +44 -0
- package/src/components/useHideableColumns.ts +52 -0
- package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
- package/src/{tmdatagrid/core → core}/capabilities.ts +14 -6
- package/src/{tmdatagrid/core → core}/columnOptions.ts +46 -0
- package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
- package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
- package/src/core/controlledState.ts +179 -0
- package/src/core/controlledStateSync.ts +108 -0
- package/src/core/deletedRows.ts +34 -0
- package/src/core/dom.ts +74 -0
- package/src/core/editEngine.ts +2476 -0
- package/src/{tmdatagrid/core → core}/editorFocus.ts +8 -4
- package/src/core/export.ts +843 -0
- package/src/{tmdatagrid/core → core}/filterControls.ts +38 -1
- package/src/{tmdatagrid/core → core}/filterOperators.ts +64 -1
- package/src/core/filterSurface.ts +99 -0
- package/src/{tmdatagrid/core → core}/labels.ts +66 -8
- package/src/{tmdatagrid/core → core}/labelsSv.ts +26 -3
- package/src/core/pageReset.ts +120 -0
- package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
- package/src/core/resizePreview.ts +141 -0
- package/src/core/summary.ts +59 -0
- package/src/core/useSettledTableState.ts +36 -0
- package/src/{tmdatagrid/index.ts → index.ts} +75 -12
- package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +734 -135
- package/src/useTMDataGridExport.ts +78 -0
- package/src/tmdatagrid/components/TMDataGridEditActions.tsx +0 -162
- package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
- package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
- package/src/tmdatagrid/components/TMDataGridToolbar.module.css +0 -12
- package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -162
- package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +0 -40
- package/src/tmdatagrid/core/cellExport.ts +0 -320
- package/src/tmdatagrid/core/editEngine.ts +0 -1006
- package/src/tmdatagrid/core/summary.ts +0 -35
- /package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridFooter.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.module.css +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridRowNumberColumn.tsx +0 -0
- /package/src/{tmdatagrid/components → components}/TMDataGridSearch.tsx +0 -0
- /package/src/{tmdatagrid/core → core}/cellNavigation.ts +0 -0
- /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
- /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
- /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
- /package/src/{tmdatagrid/core → core}/grouping.ts +0 -0
- /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
- /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
- /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
- /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
- /package/src/{tmdatagrid/core → core}/sizes.ts +0 -0
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
import { ActionIcon, Loader, Popover, Text, Tooltip } from "@mantine/core";
|
|
2
|
-
import { useSelector } from "@tanstack/react-store";
|
|
3
|
-
import type { ReactNode } from "react";
|
|
4
|
-
import classes from "./TMDataGridToolbar.module.css";
|
|
5
|
-
import { TMDataGridColumnsPanel } from "./TMDataGridColumnsPanel";
|
|
6
|
-
import { useTMDataGridContext } from "../TMDataGridContext";
|
|
7
|
-
import { getGridCapabilities } from "../core/capabilities";
|
|
8
|
-
import { isFilterActive } from "../core/filterOperators";
|
|
9
|
-
import { BurgerIcon, FilterIcon } from "./icons";
|
|
10
|
-
import { openColumnFilter } from "../useTMDataGrid";
|
|
11
|
-
|
|
12
|
-
/** Row above the grid. Compose it from the pieces below, or anything else. */
|
|
13
|
-
export function TMDataGridToolbar({ children }: { children?: ReactNode }) {
|
|
14
|
-
return (
|
|
15
|
-
<div data-dg-part="toolbar" className={classes.toolbar}>
|
|
16
|
-
{children}
|
|
17
|
-
</div>
|
|
18
|
-
);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** Pushes the following toolbar items to the right. */
|
|
22
|
-
export function TMDataGridToolbarSpacer() {
|
|
23
|
-
return <div className={classes.toolbarSpacer} />;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* A small spinner shown while `meta.loading` is true, and nothing otherwise.
|
|
28
|
-
*
|
|
29
|
-
* The body only shows its loading state while the grid is *empty* - a
|
|
30
|
-
* server-driven grid refetching with rows on screen keeps showing them, which
|
|
31
|
-
* is right, but leaves nothing saying a fetch is running. This is that signal,
|
|
32
|
-
* and the consumer decides where it sits by placing it in the toolbar:
|
|
33
|
-
*
|
|
34
|
-
* ```tsx
|
|
35
|
-
* <TMDataGrid.Toolbar>
|
|
36
|
-
* <TMDataGrid.SummaryCount />
|
|
37
|
-
* <TMDataGrid.Spacer />
|
|
38
|
-
* <TMDataGrid.LoadingIndicator />
|
|
39
|
-
* <TMDataGrid.FilterButton />
|
|
40
|
-
* </TMDataGrid.Toolbar>
|
|
41
|
-
* ```
|
|
42
|
-
*/
|
|
43
|
-
export function TMDataGridLoadingIndicator() {
|
|
44
|
-
const { table, labels } = useTMDataGridContext();
|
|
45
|
-
if (table.options.meta?.loading !== true) return null;
|
|
46
|
-
return <Loader size="xs" data-dg-part="loading" aria-label={labels.loading} />;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Visible rows over total rows. On a server-driven grid, set
|
|
51
|
-
* `meta.totalRowCount` for the denominator - the client never sees every row.
|
|
52
|
-
*/
|
|
53
|
-
export function TMDataGridSummaryCount({ children }: { children?: ReactNode }) {
|
|
54
|
-
const { table, controlSize } = useTMDataGridContext();
|
|
55
|
-
useSelector(table.store);
|
|
56
|
-
|
|
57
|
-
if (children !== undefined) {
|
|
58
|
-
return (
|
|
59
|
-
<Text span size={controlSize} c="dimmed" data-dg-part="summary-count">
|
|
60
|
-
{children}
|
|
61
|
-
</Text>
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const shown = table.getRowCount();
|
|
66
|
-
const total =
|
|
67
|
-
table.options.meta?.totalRowCount ??
|
|
68
|
-
table.getPreFilteredRowModel().rows.length;
|
|
69
|
-
|
|
70
|
-
return (
|
|
71
|
-
<Text span size={controlSize} c="dimmed" data-dg-part="summary-count">
|
|
72
|
-
{shown} / {total}
|
|
73
|
-
</Text>
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Burger menu in the grid's top-right corner - opens "Manage columns".
|
|
79
|
-
* Renders nothing when no column can be hidden (`enableHiding: false`).
|
|
80
|
-
*/
|
|
81
|
-
export function TMDataGridColumnsButton() {
|
|
82
|
-
const { table, ui, features, labels, controlSize } = useTMDataGridContext();
|
|
83
|
-
const opened = useSelector(ui, (state) => state.columnsPanelOpen);
|
|
84
|
-
|
|
85
|
-
if (!getGridCapabilities(table, features).canHideAny) return null;
|
|
86
|
-
|
|
87
|
-
return (
|
|
88
|
-
<Popover
|
|
89
|
-
opened={opened}
|
|
90
|
-
onChange={ui.actions.setColumnsPanelOpen}
|
|
91
|
-
position="bottom-end"
|
|
92
|
-
shadow="md"
|
|
93
|
-
radius="md"
|
|
94
|
-
withinPortal
|
|
95
|
-
trapFocus
|
|
96
|
-
>
|
|
97
|
-
<Popover.Target>
|
|
98
|
-
<Tooltip label={labels.manageColumns} openDelay={400}>
|
|
99
|
-
<ActionIcon
|
|
100
|
-
variant="subtle"
|
|
101
|
-
color="gray"
|
|
102
|
-
size={controlSize}
|
|
103
|
-
aria-label={labels.manageColumns}
|
|
104
|
-
data-dg-part="columns-button"
|
|
105
|
-
onClick={ui.actions.toggleColumnsPanel}
|
|
106
|
-
>
|
|
107
|
-
<BurgerIcon size={18} stroke={1.6} />
|
|
108
|
-
</ActionIcon>
|
|
109
|
-
</Tooltip>
|
|
110
|
-
</Popover.Target>
|
|
111
|
-
<Popover.Dropdown p={0}>
|
|
112
|
-
<TMDataGridColumnsPanel />
|
|
113
|
-
</Popover.Dropdown>
|
|
114
|
-
</Popover>
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Toggles the filter panel, seeding a filter row on the first filterable column.
|
|
120
|
-
* Renders nothing when no column can be filtered (`enableColumnFilters: false`).
|
|
121
|
-
*/
|
|
122
|
-
export function TMDataGridFilterButton() {
|
|
123
|
-
const api = useTMDataGridContext();
|
|
124
|
-
const { table, ui, features, labels, controlSize } = api;
|
|
125
|
-
const opened = useSelector(ui, (state) => state.filterPanelOpen);
|
|
126
|
-
const columnFilters = useSelector(
|
|
127
|
-
table.store,
|
|
128
|
-
(state) => state.columnFilters,
|
|
129
|
-
);
|
|
130
|
-
const activeCount = columnFilters.filter((filter) =>
|
|
131
|
-
isFilterActive(filter.value),
|
|
132
|
-
).length;
|
|
133
|
-
|
|
134
|
-
if (!getGridCapabilities(table, features).canFilterAny) return null;
|
|
135
|
-
|
|
136
|
-
return (
|
|
137
|
-
<Tooltip label={labels.filters} openDelay={400}>
|
|
138
|
-
<ActionIcon
|
|
139
|
-
variant={opened || activeCount > 0 ? "light" : "subtle"}
|
|
140
|
-
color={activeCount > 0 ? undefined : "gray"}
|
|
141
|
-
size={controlSize}
|
|
142
|
-
aria-label={labels.filters}
|
|
143
|
-
// Also what the panel's click-away handler tests for, so that a click
|
|
144
|
-
// here reads as a toggle rather than as a click outside.
|
|
145
|
-
data-dg-part="filter-button"
|
|
146
|
-
onClick={() => {
|
|
147
|
-
if (opened) {
|
|
148
|
-
ui.actions.closeFilterPanel();
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
const firstFilterable = table
|
|
152
|
-
.getAllLeafColumns()
|
|
153
|
-
.find((column) => column.getCanFilter());
|
|
154
|
-
if (firstFilterable) openColumnFilter(api, firstFilterable.id);
|
|
155
|
-
else ui.actions.openFilterPanel();
|
|
156
|
-
}}
|
|
157
|
-
>
|
|
158
|
-
<FilterIcon size={16} stroke={1.6} />
|
|
159
|
-
</ActionIcon>
|
|
160
|
-
</Tooltip>
|
|
161
|
-
);
|
|
162
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { NumberInput } from "@mantine/core";
|
|
2
|
-
import type { TMDataGridEditorArgs } from "../../core/editEngine";
|
|
3
|
-
import { getColumnLabel } from "../../core/columnUtils";
|
|
4
|
-
import { useTMDataGridContext } from "../../TMDataGridContext";
|
|
5
|
-
import {
|
|
6
|
-
selectAllOnFocus,
|
|
7
|
-
useCaretKeeper,
|
|
8
|
-
useFieldError,
|
|
9
|
-
useFieldValue,
|
|
10
|
-
} from "./editorShared";
|
|
11
|
-
|
|
12
|
-
/** The built-in editor for `meta.type: "number"`. */
|
|
13
|
-
export function TMDataGridNumberEditor({ field, column, size }: TMDataGridEditorArgs) {
|
|
14
|
-
const { labels } = useTMDataGridContext();
|
|
15
|
-
const value = useFieldValue(field);
|
|
16
|
-
const error = useFieldError(field);
|
|
17
|
-
const { inputRef, remember } = useCaretKeeper();
|
|
18
|
-
return (
|
|
19
|
-
<NumberInput
|
|
20
|
-
ref={inputRef}
|
|
21
|
-
size={size}
|
|
22
|
-
w="100%"
|
|
23
|
-
onFocus={selectAllOnFocus}
|
|
24
|
-
hideControls
|
|
25
|
-
aria-label={labels.editCell(getColumnLabel(column))}
|
|
26
|
-
data-dg-part="editor-input"
|
|
27
|
-
value={typeof value === "number" ? value : ""}
|
|
28
|
-
onChange={(next) => {
|
|
29
|
-
if (inputRef.current !== null) remember(inputRef.current);
|
|
30
|
-
// Mantine reports a number once one can be parsed, and a string while
|
|
31
|
-
// it cannot. An empty cell is `null`, meaning no value.
|
|
32
|
-
field.handleChange(
|
|
33
|
-
typeof next === "number" ? next : next === "" ? null : Number(next),
|
|
34
|
-
);
|
|
35
|
-
}}
|
|
36
|
-
onBlur={field.handleBlur}
|
|
37
|
-
error={error}
|
|
38
|
-
/>
|
|
39
|
-
);
|
|
40
|
-
}
|
|
@@ -1,320 +0,0 @@
|
|
|
1
|
-
import type { Column, Row, RowData } from "@tanstack/react-table";
|
|
2
|
-
import type { TMDataGridRowData } from "../TMDataGridContext";
|
|
3
|
-
import type { TMDataGridFeatures, TMDataGridTable } from "../useTMDataGrid";
|
|
4
|
-
import type { TMDataGridRangeBounds } from "./cellRange";
|
|
5
|
-
import { getColumnLabel, isControlColumn } from "./columnUtils";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* The byte order mark Excel looks for before it will read a file as UTF-8.
|
|
9
|
-
* Built from its code point rather than pasted in, because a literal BOM is
|
|
10
|
-
* invisible in a source file and easily deleted by accident.
|
|
11
|
-
*/
|
|
12
|
-
const UTF8_BOM = String.fromCharCode(0xfeff);
|
|
13
|
-
|
|
14
|
-
/** The selected block, flattened to text. One inner array per row. */
|
|
15
|
-
export type TMDataGridCellMatrix = Array<Array<string>>;
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* How values are written out.
|
|
19
|
-
*
|
|
20
|
-
* The defaults are the Nordic ones, because they are the ones that need
|
|
21
|
-
* choosing: an Excel running a Swedish, Norwegian, Danish or Finnish locale
|
|
22
|
-
* reads `;` as its list separator and `,` as its decimal mark, and a file
|
|
23
|
-
* written the other way opens as one column of text. Both are settable for a
|
|
24
|
-
* grid whose users run something else.
|
|
25
|
-
*/
|
|
26
|
-
export type TMDataGridCellExportOptions = {
|
|
27
|
-
/** CSV field separator. Defaults to `";"`. */
|
|
28
|
-
separator?: string;
|
|
29
|
-
/** Write numbers as `1,5` rather than `1.5`. Defaults to `true`. */
|
|
30
|
-
decimalComma?: boolean;
|
|
31
|
-
/** Column labels as the first row. Defaults to `true`. */
|
|
32
|
-
includeHeaders?: boolean;
|
|
33
|
-
/** Without extension. Defaults to `"export"`. */
|
|
34
|
-
fileName?: string;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
type ResolvedExportOptions = Required<TMDataGridCellExportOptions>;
|
|
38
|
-
|
|
39
|
-
export const DEFAULT_CELL_EXPORT_OPTIONS: ResolvedExportOptions = {
|
|
40
|
-
separator: ";",
|
|
41
|
-
decimalComma: true,
|
|
42
|
-
includeHeaders: true,
|
|
43
|
-
fileName: "export",
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* One value as text.
|
|
48
|
-
*
|
|
49
|
-
* Deliberately not the rendered cell: what a cell renders is React, and often a
|
|
50
|
-
* badge, a link or an icon rather than the value. The value is what a
|
|
51
|
-
* spreadsheet wants, and it is the one thing every column is guaranteed to
|
|
52
|
-
* have.
|
|
53
|
-
*/
|
|
54
|
-
export function formatExportValue(
|
|
55
|
-
value: unknown,
|
|
56
|
-
{ decimalComma }: { decimalComma: boolean },
|
|
57
|
-
): string {
|
|
58
|
-
if (value === null || value === undefined) return "";
|
|
59
|
-
if (typeof value === "number") {
|
|
60
|
-
if (!Number.isFinite(value)) return "";
|
|
61
|
-
const text = String(value);
|
|
62
|
-
return decimalComma ? text.replace(".", ",") : text;
|
|
63
|
-
}
|
|
64
|
-
if (typeof value === "string") return value;
|
|
65
|
-
if (typeof value === "boolean") return value ? "true" : "false";
|
|
66
|
-
// A multiSelect cell holds an array of values; a spreadsheet cell holds one
|
|
67
|
-
// string, so the elements are joined the way they read on screen.
|
|
68
|
-
if (Array.isArray(value)) {
|
|
69
|
-
return value
|
|
70
|
-
.map((entry) => formatExportValue(entry, { decimalComma }))
|
|
71
|
-
.join(", ");
|
|
72
|
-
}
|
|
73
|
-
// `sv-SE` is ISO-shaped (2026-07-31, 2026-07-31 14:05:00), which is both the
|
|
74
|
-
// Nordic form and the one Excel parses as a date rather than as text.
|
|
75
|
-
if (value instanceof Date) {
|
|
76
|
-
return Number.isNaN(value.getTime()) ? "" : value.toLocaleString("sv-SE");
|
|
77
|
-
}
|
|
78
|
-
// An object in a cell is a shape the grid cannot know. JSON at least keeps
|
|
79
|
-
// what was there, where `String(value)` would write "[object Object]".
|
|
80
|
-
if (typeof value === "object") {
|
|
81
|
-
try {
|
|
82
|
-
return JSON.stringify(value);
|
|
83
|
-
} catch {
|
|
84
|
-
return "";
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return String(value);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export type BuildCellMatrixArgs = {
|
|
91
|
-
rows: ReadonlyArray<Row<TMDataGridFeatures, TMDataGridRowData>>;
|
|
92
|
-
/** Every visible column, in render order - the same list the bounds index into. */
|
|
93
|
-
columns: ReadonlyArray<Column<TMDataGridFeatures, TMDataGridRowData, unknown>>;
|
|
94
|
-
bounds: TMDataGridRangeBounds;
|
|
95
|
-
includeHeaders: boolean;
|
|
96
|
-
decimalComma: boolean;
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* The selected rectangle as rows of text.
|
|
101
|
-
*
|
|
102
|
-
* The generated lanes - the checkbox, the tree chevron, the details chevron -
|
|
103
|
-
* are dropped even when the rectangle covers them. They hold controls rather
|
|
104
|
-
* than data, so a column of empty strings is all they could contribute, and
|
|
105
|
-
* pasting one into a spreadsheet only shifts everything to its right.
|
|
106
|
-
*/
|
|
107
|
-
export function buildCellMatrix({
|
|
108
|
-
rows,
|
|
109
|
-
columns,
|
|
110
|
-
bounds,
|
|
111
|
-
includeHeaders,
|
|
112
|
-
decimalComma,
|
|
113
|
-
}: BuildCellMatrixArgs): TMDataGridCellMatrix {
|
|
114
|
-
const selectedColumns = columns
|
|
115
|
-
.slice(bounds.left, bounds.right + 1)
|
|
116
|
-
.filter((column) => !isControlColumn(column.id));
|
|
117
|
-
if (selectedColumns.length === 0) return [];
|
|
118
|
-
|
|
119
|
-
const matrix: TMDataGridCellMatrix = [];
|
|
120
|
-
if (includeHeaders) {
|
|
121
|
-
matrix.push(selectedColumns.map((column) => getColumnLabel(column)));
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
for (let index = bounds.top; index <= bounds.bottom; index += 1) {
|
|
125
|
-
const row = rows[index];
|
|
126
|
-
if (row === undefined) continue;
|
|
127
|
-
matrix.push(
|
|
128
|
-
selectedColumns.map((column) =>
|
|
129
|
-
formatExportValue(row.getValue(column.id), { decimalComma }),
|
|
130
|
-
),
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
return matrix;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* The whole grid as rows of text: every filtered and sorted data row - all
|
|
138
|
-
* pages, not the one on screen - by every visible non-control column, in
|
|
139
|
-
* render order.
|
|
140
|
-
*
|
|
141
|
-
* The values are the same ones a cell-range export writes; only the bounds
|
|
142
|
-
* differ. Group rows are left out (their records are the rows themselves),
|
|
143
|
-
* and hidden columns are not exported - what you see is what you get, minus
|
|
144
|
-
* paging.
|
|
145
|
-
*/
|
|
146
|
-
export function buildGridCellMatrix<TData extends RowData>({
|
|
147
|
-
table,
|
|
148
|
-
includeHeaders = DEFAULT_CELL_EXPORT_OPTIONS.includeHeaders,
|
|
149
|
-
decimalComma = DEFAULT_CELL_EXPORT_OPTIONS.decimalComma,
|
|
150
|
-
}: {
|
|
151
|
-
table: TMDataGridTable<TData>;
|
|
152
|
-
includeHeaders?: boolean;
|
|
153
|
-
decimalComma?: boolean;
|
|
154
|
-
}): TMDataGridCellMatrix {
|
|
155
|
-
// The same erasure the context provider performs: the matrix only ever reads
|
|
156
|
-
// generic row and column APIs.
|
|
157
|
-
const erased = table as unknown as TMDataGridTable<TMDataGridRowData>;
|
|
158
|
-
const columns = [
|
|
159
|
-
...erased.getLeftVisibleLeafColumns(),
|
|
160
|
-
...erased.getCenterVisibleLeafColumns(),
|
|
161
|
-
...erased.getRightVisibleLeafColumns(),
|
|
162
|
-
];
|
|
163
|
-
// The sorted model: after filtering, grouping and sorting, before expansion
|
|
164
|
-
// and paging - so a paged grid exports the whole filtered set, and a grouped
|
|
165
|
-
// grid exports the records under every group whether or not it is open.
|
|
166
|
-
//
|
|
167
|
-
// Walked by hand rather than taken from `flatRows`: in table-core
|
|
168
|
-
// 9.0.0-beta.21 the grouped model lists every leaf twice there (once as a
|
|
169
|
-
// leaf row, once re-parented under its group), and its order puts the
|
|
170
|
-
// groups last. Depth-first over `rows` is exactly render order, once each.
|
|
171
|
-
const rows: Array<Row<TMDataGridFeatures, TMDataGridRowData>> = [];
|
|
172
|
-
const walk = (
|
|
173
|
-
list: ReadonlyArray<Row<TMDataGridFeatures, TMDataGridRowData>>,
|
|
174
|
-
) => {
|
|
175
|
-
for (const row of list) {
|
|
176
|
-
if (row.getIsGrouped()) walk(row.subRows);
|
|
177
|
-
else rows.push(row);
|
|
178
|
-
}
|
|
179
|
-
};
|
|
180
|
-
walk(erased.getSortedRowModel().rows);
|
|
181
|
-
|
|
182
|
-
return buildCellMatrix({
|
|
183
|
-
rows,
|
|
184
|
-
columns,
|
|
185
|
-
bounds: {
|
|
186
|
-
top: 0,
|
|
187
|
-
bottom: rows.length - 1,
|
|
188
|
-
left: 0,
|
|
189
|
-
right: columns.length - 1,
|
|
190
|
-
},
|
|
191
|
-
includeHeaders,
|
|
192
|
-
decimalComma,
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Downloads the whole grid as a CSV for Excel - {@link buildGridCellMatrix}
|
|
198
|
-
* through {@link toExcelCsv}, with the same Nordic defaults and overrides as
|
|
199
|
-
* the cell-range export.
|
|
200
|
-
*
|
|
201
|
-
* No built-in button: wire it to your own toolbar.
|
|
202
|
-
*
|
|
203
|
-
* ```tsx
|
|
204
|
-
* <Button onClick={() => exportGridToCsv({ table: grid.table })}>Export</Button>
|
|
205
|
-
* ```
|
|
206
|
-
*/
|
|
207
|
-
export function exportGridToCsv<TData extends RowData>({
|
|
208
|
-
table,
|
|
209
|
-
options,
|
|
210
|
-
}: {
|
|
211
|
-
table: TMDataGridTable<TData>;
|
|
212
|
-
options?: TMDataGridCellExportOptions;
|
|
213
|
-
}): void {
|
|
214
|
-
const resolved = { ...DEFAULT_CELL_EXPORT_OPTIONS, ...options };
|
|
215
|
-
const matrix = buildGridCellMatrix({
|
|
216
|
-
table,
|
|
217
|
-
includeHeaders: resolved.includeHeaders,
|
|
218
|
-
decimalComma: resolved.decimalComma,
|
|
219
|
-
});
|
|
220
|
-
if (matrix.length === 0) return;
|
|
221
|
-
downloadTextFile({
|
|
222
|
-
fileName: `${resolved.fileName}.csv`,
|
|
223
|
-
text: toExcelCsv(matrix, { separator: resolved.separator }),
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Quotes a field when it holds something that would otherwise end it early.
|
|
229
|
-
* Doubling the quote is how both CSV and Excel's own clipboard format escape
|
|
230
|
-
* one, so the same rule serves both.
|
|
231
|
-
*/
|
|
232
|
-
function escapeField(value: string, separator: string): string {
|
|
233
|
-
const needsQuotes =
|
|
234
|
-
value.includes(separator) ||
|
|
235
|
-
value.includes('"') ||
|
|
236
|
-
value.includes("\n") ||
|
|
237
|
-
value.includes("\r");
|
|
238
|
-
return needsQuotes ? `"${value.replaceAll('"', '""')}"` : value;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* The clipboard format spreadsheets read: tab between cells, CRLF between rows.
|
|
243
|
-
*
|
|
244
|
-
* Tabs rather than commas because that is what Excel, Sheets and Numbers all
|
|
245
|
-
* put on the clipboard themselves - paste it and the cells land in cells. A
|
|
246
|
-
* comma-separated string pastes into a single column, which is the thing this
|
|
247
|
-
* exists to avoid.
|
|
248
|
-
*/
|
|
249
|
-
export function toClipboardText(matrix: TMDataGridCellMatrix): string {
|
|
250
|
-
return matrix
|
|
251
|
-
.map((row) => row.map((value) => escapeField(value, "\t")).join("\t"))
|
|
252
|
-
.join("\r\n");
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* A CSV that opens straight into columns in Excel.
|
|
257
|
-
*
|
|
258
|
-
* Three things make that true, and all three are needed:
|
|
259
|
-
*
|
|
260
|
-
* | Part | Why |
|
|
261
|
-
* | ---- | --- |
|
|
262
|
-
* | `sep=;` first line | Excel's own directive - it stops guessing and uses this |
|
|
263
|
-
* | UTF-8 BOM | without it Excel reads the file as ANSI, and å ä ö arrive broken |
|
|
264
|
-
* | CRLF line endings | what Excel writes, and what its importer is happiest with |
|
|
265
|
-
*
|
|
266
|
-
* The `sep=` line is Excel's alone; other readers show it as a first row. This
|
|
267
|
-
* export targets Excel, so opening correctly there takes priority over strict
|
|
268
|
-
* CSV.
|
|
269
|
-
*/
|
|
270
|
-
export function toExcelCsv(
|
|
271
|
-
matrix: TMDataGridCellMatrix,
|
|
272
|
-
{ separator }: { separator: string },
|
|
273
|
-
): string {
|
|
274
|
-
const body = matrix
|
|
275
|
-
.map((row) => row.map((value) => escapeField(value, separator)).join(separator))
|
|
276
|
-
.join("\r\n");
|
|
277
|
-
return `${UTF8_BOM}sep=${separator}\r\n${body}\r\n`;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Puts text on the clipboard, reporting whether it landed.
|
|
282
|
-
*
|
|
283
|
-
* The async clipboard API only resolves for a document that has the focus and a
|
|
284
|
-
* user gesture behind it - both true when this runs off Ctrl+C or a menu item.
|
|
285
|
-
* It is still allowed to reject (a permissions policy, a page that lost focus
|
|
286
|
-
* mid-copy), so the result is a boolean the caller can act on.
|
|
287
|
-
*/
|
|
288
|
-
export async function writeClipboardText(text: string): Promise<boolean> {
|
|
289
|
-
try {
|
|
290
|
-
await navigator.clipboard.writeText(text);
|
|
291
|
-
return true;
|
|
292
|
-
} catch {
|
|
293
|
-
return false;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Downloads text as a file, through the one mechanism a library can use: an
|
|
299
|
-
* anchor with an object URL behind it, clicked. Revoked on the next frame -
|
|
300
|
-
* immediately would race the browser's own read of it.
|
|
301
|
-
*/
|
|
302
|
-
export function downloadTextFile({
|
|
303
|
-
fileName,
|
|
304
|
-
text,
|
|
305
|
-
mimeType = "text/csv;charset=utf-8",
|
|
306
|
-
}: {
|
|
307
|
-
fileName: string;
|
|
308
|
-
text: string;
|
|
309
|
-
mimeType?: string;
|
|
310
|
-
}): void {
|
|
311
|
-
const url = URL.createObjectURL(new Blob([text], { type: mimeType }));
|
|
312
|
-
const anchor = document.createElement("a");
|
|
313
|
-
anchor.href = url;
|
|
314
|
-
anchor.download = fileName;
|
|
315
|
-
anchor.style.display = "none";
|
|
316
|
-
document.body.append(anchor);
|
|
317
|
-
anchor.click();
|
|
318
|
-
anchor.remove();
|
|
319
|
-
setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
320
|
-
}
|