compote-ui 0.84.0 → 0.86.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/dist/components/data-table-v9/toolbar/data-table-export.svelte +146 -146
- package/dist/components/data-table-v9/xlsx/index.d.ts +22 -0
- package/dist/components/data-table-v9/xlsx/index.js +39 -5
- package/dist/components/data-table-v9/xlsx/xlsx-rows.d.ts +31 -0
- package/dist/components/data-table-v9/xlsx/xlsx-rows.js +60 -0
- package/dist/icons/PhDownloadSimple.svelte +18 -18
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +129 -129
- package/skills/data-table/SKILL.md +401 -400
- package/skills/data-table/references/column-options.md +181 -181
- package/skills/data-table/references/export.md +209 -158
|
@@ -1,146 +1,146 @@
|
|
|
1
|
-
<script lang="ts" generics="T extends RowData">
|
|
2
|
-
import type { RowData } from '@tanstack/svelte-table';
|
|
3
|
-
import { Menu as ArkMenu } from '@ark-ui/svelte/menu';
|
|
4
|
-
import * as Menu from '../../menu';
|
|
5
|
-
import * as Tooltip from '../../tooltip';
|
|
6
|
-
import { button } from '../../button/button.variants';
|
|
7
|
-
import { PhDownloadSimple } from '../../../icons';
|
|
8
|
-
import type { DataTableInstance } from '../data-table-utils';
|
|
9
|
-
import { csvExporter } from '../export/csv-exporter';
|
|
10
|
-
import { getExportData, getExportRows, type DataTableExportScope } from '../export/export-data';
|
|
11
|
-
import type { DataTableExporter } from '../export/types';
|
|
12
|
-
|
|
13
|
-
type Props = {
|
|
14
|
-
table: DataTableInstance<T>;
|
|
15
|
-
/** File formats to offer. One format and no selected rows: a plain button; otherwise a menu. */
|
|
16
|
-
exporters?: DataTableExporter[];
|
|
17
|
-
/** File name without extension. */
|
|
18
|
-
filename?: string;
|
|
19
|
-
/** Text label for the trigger. When omitted, a download icon is shown instead. */
|
|
20
|
-
triggerLabel?: string;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
let { table, exporters = [csvExporter], filename = 'export', triggerLabel }: Props = $props();
|
|
24
|
-
|
|
25
|
-
// Menu and tooltip share one trigger element, so both must agree on its id.
|
|
26
|
-
const triggerId = $props.id();
|
|
27
|
-
|
|
28
|
-
let pending = $state(false);
|
|
29
|
-
|
|
30
|
-
// Row model reads aren't reactive on their own: read the slices they depend on first.
|
|
31
|
-
const rowSlices = $derived.by(() => [
|
|
32
|
-
table.options.data,
|
|
33
|
-
table.atoms.columnFilters.get(),
|
|
34
|
-
table.atoms.globalFilter.get(),
|
|
35
|
-
table.atoms.sorting.get(),
|
|
36
|
-
table.atoms.rowSelection.get()
|
|
37
|
-
]);
|
|
38
|
-
const rowCount = $derived.by(() => {
|
|
39
|
-
void rowSlices;
|
|
40
|
-
return getExportRows(table, 'filtered').length;
|
|
41
|
-
});
|
|
42
|
-
// Counted exactly as the export picks them: selected rows the filter keeps.
|
|
43
|
-
const selectedCount = $derived.by(() => {
|
|
44
|
-
void rowSlices;
|
|
45
|
-
return getExportRows(table, 'selected').length;
|
|
46
|
-
});
|
|
47
|
-
const hasSelection = $derived(selectedCount > 0);
|
|
48
|
-
// The menu's choice, reset to all rows whenever the selection empties or starts again, so a
|
|
49
|
-
// "Selected rows" pick from an earlier selection never carries over to a new one.
|
|
50
|
-
let chosenScope = $derived.by<DataTableExportScope>(() => {
|
|
51
|
-
void hasSelection;
|
|
52
|
-
return 'filtered';
|
|
53
|
-
});
|
|
54
|
-
// `selected` with nothing selected would write an empty file: fall back to all rows.
|
|
55
|
-
const scope = $derived<DataTableExportScope>(hasSelection ? chosenScope : 'filtered');
|
|
56
|
-
const showMenu = $derived(exporters.length > 1 || hasSelection);
|
|
57
|
-
|
|
58
|
-
async function runExport(exporter: DataTableExporter) {
|
|
59
|
-
if (pending) return;
|
|
60
|
-
pending = true;
|
|
61
|
-
const exportScope = scope;
|
|
62
|
-
try {
|
|
63
|
-
await exporter.export({
|
|
64
|
-
table,
|
|
65
|
-
scope: exportScope,
|
|
66
|
-
filename,
|
|
67
|
-
getData: () => getExportData(table, exportScope)
|
|
68
|
-
});
|
|
69
|
-
} finally {
|
|
70
|
-
pending = false;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
</script>
|
|
74
|
-
|
|
75
|
-
{#snippet triggerContent()}
|
|
76
|
-
<PhDownloadSimple />
|
|
77
|
-
{triggerLabel}
|
|
78
|
-
{/snippet}
|
|
79
|
-
|
|
80
|
-
{#if showMenu}
|
|
81
|
-
<Menu.Root ids={{ trigger: triggerId }} positioning={{ placement: 'bottom-end' }}>
|
|
82
|
-
<Tooltip.Root ids={{ trigger: triggerId }} disabled={!!triggerLabel}>
|
|
83
|
-
<Tooltip.Trigger>
|
|
84
|
-
{#snippet asChild(tooltipProps)}
|
|
85
|
-
<ArkMenu.Trigger
|
|
86
|
-
{...tooltipProps()}
|
|
87
|
-
aria-label={triggerLabel ? undefined : 'Export'}
|
|
88
|
-
aria-busy={pending}
|
|
89
|
-
class={button({ variant: 'outline', size: triggerLabel ? 'default' : 'icon' })}
|
|
90
|
-
>
|
|
91
|
-
{@render triggerContent()}
|
|
92
|
-
</ArkMenu.Trigger>
|
|
93
|
-
{/snippet}
|
|
94
|
-
</Tooltip.Trigger>
|
|
95
|
-
<Tooltip.Content>Export</Tooltip.Content>
|
|
96
|
-
</Tooltip.Root>
|
|
97
|
-
|
|
98
|
-
<Menu.Content class="min-w-44">
|
|
99
|
-
{#if hasSelection}
|
|
100
|
-
<Menu.RadioItemGroup
|
|
101
|
-
value={scope}
|
|
102
|
-
onValueChange={(details) => (chosenScope = details.value as DataTableExportScope)}
|
|
103
|
-
>
|
|
104
|
-
<Menu.RadioItem value="filtered" closeOnSelect={false}>
|
|
105
|
-
All rows ({rowCount})
|
|
106
|
-
</Menu.RadioItem>
|
|
107
|
-
<Menu.RadioItem value="selected" closeOnSelect={false}>
|
|
108
|
-
Selected rows ({selectedCount})
|
|
109
|
-
</Menu.RadioItem>
|
|
110
|
-
</Menu.RadioItemGroup>
|
|
111
|
-
<Menu.Separator />
|
|
112
|
-
{/if}
|
|
113
|
-
{#each exporters as exporter (exporter.id)}
|
|
114
|
-
{@const Icon = exporter.icon}
|
|
115
|
-
<Menu.Item
|
|
116
|
-
value={`export:${exporter.id}`}
|
|
117
|
-
disabled={pending}
|
|
118
|
-
onSelect={() => runExport(exporter)}
|
|
119
|
-
>
|
|
120
|
-
{#if Icon}<Icon class="text-ink-dim" />{/if}
|
|
121
|
-
{exporter.label}
|
|
122
|
-
</Menu.Item>
|
|
123
|
-
{/each}
|
|
124
|
-
</Menu.Content>
|
|
125
|
-
</Menu.Root>
|
|
126
|
-
{:else}
|
|
127
|
-
{@const exporter = exporters[0]}
|
|
128
|
-
{#if exporter}
|
|
129
|
-
<Tooltip.Root disabled={!!triggerLabel}>
|
|
130
|
-
<!-- aria-disabled instead of disabled so keyboard focus isn't lost mid-export -->
|
|
131
|
-
<Tooltip.Trigger
|
|
132
|
-
type="button"
|
|
133
|
-
variant="outline"
|
|
134
|
-
size={triggerLabel ? 'default' : 'icon'}
|
|
135
|
-
class="aria-disabled:cursor-default"
|
|
136
|
-
aria-label={triggerLabel ? undefined : `Export ${exporter.label}`}
|
|
137
|
-
aria-busy={pending}
|
|
138
|
-
aria-disabled={pending}
|
|
139
|
-
onclick={() => runExport(exporter)}
|
|
140
|
-
>
|
|
141
|
-
{@render triggerContent()}
|
|
142
|
-
</Tooltip.Trigger>
|
|
143
|
-
<Tooltip.Content>Export {exporter.label}</Tooltip.Content>
|
|
144
|
-
</Tooltip.Root>
|
|
145
|
-
{/if}
|
|
146
|
-
{/if}
|
|
1
|
+
<script lang="ts" generics="T extends RowData">
|
|
2
|
+
import type { RowData } from '@tanstack/svelte-table';
|
|
3
|
+
import { Menu as ArkMenu } from '@ark-ui/svelte/menu';
|
|
4
|
+
import * as Menu from '../../menu';
|
|
5
|
+
import * as Tooltip from '../../tooltip';
|
|
6
|
+
import { button } from '../../button/button.variants';
|
|
7
|
+
import { PhDownloadSimple } from '../../../icons';
|
|
8
|
+
import type { DataTableInstance } from '../data-table-utils';
|
|
9
|
+
import { csvExporter } from '../export/csv-exporter';
|
|
10
|
+
import { getExportData, getExportRows, type DataTableExportScope } from '../export/export-data';
|
|
11
|
+
import type { DataTableExporter } from '../export/types';
|
|
12
|
+
|
|
13
|
+
type Props = {
|
|
14
|
+
table: DataTableInstance<T>;
|
|
15
|
+
/** File formats to offer. One format and no selected rows: a plain button; otherwise a menu. */
|
|
16
|
+
exporters?: DataTableExporter[];
|
|
17
|
+
/** File name without extension. */
|
|
18
|
+
filename?: string;
|
|
19
|
+
/** Text label for the trigger. When omitted, a download icon is shown instead. */
|
|
20
|
+
triggerLabel?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
let { table, exporters = [csvExporter], filename = 'export', triggerLabel }: Props = $props();
|
|
24
|
+
|
|
25
|
+
// Menu and tooltip share one trigger element, so both must agree on its id.
|
|
26
|
+
const triggerId = $props.id();
|
|
27
|
+
|
|
28
|
+
let pending = $state(false);
|
|
29
|
+
|
|
30
|
+
// Row model reads aren't reactive on their own: read the slices they depend on first.
|
|
31
|
+
const rowSlices = $derived.by(() => [
|
|
32
|
+
table.options.data,
|
|
33
|
+
table.atoms.columnFilters.get(),
|
|
34
|
+
table.atoms.globalFilter.get(),
|
|
35
|
+
table.atoms.sorting.get(),
|
|
36
|
+
table.atoms.rowSelection.get()
|
|
37
|
+
]);
|
|
38
|
+
const rowCount = $derived.by(() => {
|
|
39
|
+
void rowSlices;
|
|
40
|
+
return getExportRows(table, 'filtered').length;
|
|
41
|
+
});
|
|
42
|
+
// Counted exactly as the export picks them: selected rows the filter keeps.
|
|
43
|
+
const selectedCount = $derived.by(() => {
|
|
44
|
+
void rowSlices;
|
|
45
|
+
return getExportRows(table, 'selected').length;
|
|
46
|
+
});
|
|
47
|
+
const hasSelection = $derived(selectedCount > 0);
|
|
48
|
+
// The menu's choice, reset to all rows whenever the selection empties or starts again, so a
|
|
49
|
+
// "Selected rows" pick from an earlier selection never carries over to a new one.
|
|
50
|
+
let chosenScope = $derived.by<DataTableExportScope>(() => {
|
|
51
|
+
void hasSelection;
|
|
52
|
+
return 'filtered';
|
|
53
|
+
});
|
|
54
|
+
// `selected` with nothing selected would write an empty file: fall back to all rows.
|
|
55
|
+
const scope = $derived<DataTableExportScope>(hasSelection ? chosenScope : 'filtered');
|
|
56
|
+
const showMenu = $derived(exporters.length > 1 || hasSelection);
|
|
57
|
+
|
|
58
|
+
async function runExport(exporter: DataTableExporter) {
|
|
59
|
+
if (pending) return;
|
|
60
|
+
pending = true;
|
|
61
|
+
const exportScope = scope;
|
|
62
|
+
try {
|
|
63
|
+
await exporter.export({
|
|
64
|
+
table,
|
|
65
|
+
scope: exportScope,
|
|
66
|
+
filename,
|
|
67
|
+
getData: () => getExportData(table, exportScope)
|
|
68
|
+
});
|
|
69
|
+
} finally {
|
|
70
|
+
pending = false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
</script>
|
|
74
|
+
|
|
75
|
+
{#snippet triggerContent()}
|
|
76
|
+
<PhDownloadSimple />
|
|
77
|
+
{triggerLabel}
|
|
78
|
+
{/snippet}
|
|
79
|
+
|
|
80
|
+
{#if showMenu}
|
|
81
|
+
<Menu.Root ids={{ trigger: triggerId }} positioning={{ placement: 'bottom-end' }}>
|
|
82
|
+
<Tooltip.Root ids={{ trigger: triggerId }} disabled={!!triggerLabel}>
|
|
83
|
+
<Tooltip.Trigger>
|
|
84
|
+
{#snippet asChild(tooltipProps)}
|
|
85
|
+
<ArkMenu.Trigger
|
|
86
|
+
{...tooltipProps()}
|
|
87
|
+
aria-label={triggerLabel ? undefined : 'Export'}
|
|
88
|
+
aria-busy={pending}
|
|
89
|
+
class={button({ variant: 'outline', size: triggerLabel ? 'default' : 'icon' })}
|
|
90
|
+
>
|
|
91
|
+
{@render triggerContent()}
|
|
92
|
+
</ArkMenu.Trigger>
|
|
93
|
+
{/snippet}
|
|
94
|
+
</Tooltip.Trigger>
|
|
95
|
+
<Tooltip.Content>Export</Tooltip.Content>
|
|
96
|
+
</Tooltip.Root>
|
|
97
|
+
|
|
98
|
+
<Menu.Content class="min-w-44">
|
|
99
|
+
{#if hasSelection}
|
|
100
|
+
<Menu.RadioItemGroup
|
|
101
|
+
value={scope}
|
|
102
|
+
onValueChange={(details) => (chosenScope = details.value as DataTableExportScope)}
|
|
103
|
+
>
|
|
104
|
+
<Menu.RadioItem value="filtered" closeOnSelect={false}>
|
|
105
|
+
All rows ({rowCount})
|
|
106
|
+
</Menu.RadioItem>
|
|
107
|
+
<Menu.RadioItem value="selected" closeOnSelect={false}>
|
|
108
|
+
Selected rows ({selectedCount})
|
|
109
|
+
</Menu.RadioItem>
|
|
110
|
+
</Menu.RadioItemGroup>
|
|
111
|
+
<Menu.Separator />
|
|
112
|
+
{/if}
|
|
113
|
+
{#each exporters as exporter (exporter.id)}
|
|
114
|
+
{@const Icon = exporter.icon}
|
|
115
|
+
<Menu.Item
|
|
116
|
+
value={`export:${exporter.id}`}
|
|
117
|
+
disabled={pending}
|
|
118
|
+
onSelect={() => runExport(exporter)}
|
|
119
|
+
>
|
|
120
|
+
{#if Icon}<Icon class="text-ink-dim" />{/if}
|
|
121
|
+
{exporter.label}
|
|
122
|
+
</Menu.Item>
|
|
123
|
+
{/each}
|
|
124
|
+
</Menu.Content>
|
|
125
|
+
</Menu.Root>
|
|
126
|
+
{:else}
|
|
127
|
+
{@const exporter = exporters[0]}
|
|
128
|
+
{#if exporter}
|
|
129
|
+
<Tooltip.Root disabled={!!triggerLabel}>
|
|
130
|
+
<!-- aria-disabled instead of disabled so keyboard focus isn't lost mid-export -->
|
|
131
|
+
<Tooltip.Trigger
|
|
132
|
+
type="button"
|
|
133
|
+
variant="outline"
|
|
134
|
+
size={triggerLabel ? 'default' : 'icon'}
|
|
135
|
+
class="aria-disabled:cursor-default"
|
|
136
|
+
aria-label={triggerLabel ? undefined : `Export ${exporter.label}`}
|
|
137
|
+
aria-busy={pending}
|
|
138
|
+
aria-disabled={pending}
|
|
139
|
+
onclick={() => runExport(exporter)}
|
|
140
|
+
>
|
|
141
|
+
{@render triggerContent()}
|
|
142
|
+
</Tooltip.Trigger>
|
|
143
|
+
<Tooltip.Content>Export {exporter.label}</Tooltip.Content>
|
|
144
|
+
</Tooltip.Root>
|
|
145
|
+
{/if}
|
|
146
|
+
{/if}
|
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import type { DataTableExportData } from '../export/export-data';
|
|
2
2
|
import type { DataTableExporter } from '../export/types';
|
|
3
3
|
import { type XlsxCell, type XlsxDateFormats } from './xlsx-cell';
|
|
4
|
+
import { type XlsxRowsColumn } from './xlsx-rows';
|
|
4
5
|
export { DEFAULT_XLSX_DATE_FORMATS, toXlsxCell, xlsxNumberFormat } from './xlsx-cell';
|
|
5
6
|
export type { XlsxCell, XlsxDateFormats, XlsxDatePart } from './xlsx-cell';
|
|
7
|
+
export { rowsToExportData } from './xlsx-rows';
|
|
8
|
+
export type { XlsxRowsColumn } from './xlsx-rows';
|
|
6
9
|
export type XlsxExporterOptions = Partial<XlsxDateFormats> & {
|
|
7
10
|
id?: string;
|
|
8
11
|
label?: string;
|
|
9
12
|
/** Sheet name (Excel allows 31 characters, without `[ ] : * ? / \`). */
|
|
10
13
|
sheet?: string;
|
|
11
14
|
};
|
|
15
|
+
/**
|
|
16
|
+
* A width per column that fits its header and the longest cell text (the text the table
|
|
17
|
+
* shows, which is close to what Excel shows), plus padding, between 8 and 60 characters.
|
|
18
|
+
*/
|
|
19
|
+
export declare function xlsxColumnWidths(data: DataTableExportData): number[];
|
|
12
20
|
/** The export data as sheet rows: a bold header row, then typed cells. */
|
|
13
21
|
export declare function exportDataToSheet(data: DataTableExportData, formats?: XlsxDateFormats): XlsxCell[][];
|
|
14
22
|
/**
|
|
@@ -16,6 +24,20 @@ export declare function exportDataToSheet(data: DataTableExportData, formats?: X
|
|
|
16
24
|
* first call, so importing this module costs nothing until an export runs — and stays SSR-safe.
|
|
17
25
|
*/
|
|
18
26
|
export declare function exportDataToXlsxBlob(data: DataTableExportData, options?: XlsxExporterOptions): Promise<Blob>;
|
|
27
|
+
export type XlsxRowsOptions = XlsxExporterOptions & {
|
|
28
|
+
/**
|
|
29
|
+
* Locale the values are formatted in: the text that sets column widths, the text of cells
|
|
30
|
+
* written as text (a number format Excel can't express), and the decimals of a number format.
|
|
31
|
+
* Default `en-US`.
|
|
32
|
+
*/
|
|
33
|
+
locale?: string;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Plain row objects as an .xlsx file, for an export that isn't a DataTable: typed cells, a bold
|
|
37
|
+
* frozen header row and content-fitted column widths, exactly like a table export. Download
|
|
38
|
+
* the result with `downloadBlob(blob, 'name.xlsx')`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function exportRowsToXlsxBlob(rows: ReadonlyArray<Record<string, unknown>>, columns: ReadonlyArray<XlsxRowsColumn>, options?: XlsxRowsOptions): Promise<Blob>;
|
|
19
41
|
/**
|
|
20
42
|
* An Excel exporter: numbers, dates and booleans as typed cells, formatted like the table.
|
|
21
43
|
* Needs `write-excel-file` installed.
|
|
@@ -1,9 +1,37 @@
|
|
|
1
1
|
import { PhMicrosoftExcelLogo } from '../../../icons';
|
|
2
2
|
import { downloadBlob } from '../../../utils/download';
|
|
3
3
|
import { DEFAULT_XLSX_DATE_FORMATS, toXlsxCell } from './xlsx-cell';
|
|
4
|
+
import { rowsToExportData } from './xlsx-rows';
|
|
4
5
|
export { DEFAULT_XLSX_DATE_FORMATS, toXlsxCell, xlsxNumberFormat } from './xlsx-cell';
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
export { rowsToExportData } from './xlsx-rows';
|
|
7
|
+
// Column widths in characters of Excel's default font.
|
|
8
|
+
const MIN_COLUMN_WIDTH = 8;
|
|
9
|
+
const MAX_COLUMN_WIDTH = 60;
|
|
10
|
+
// One character of slack: char counts already overshoot narrow lowercase text, and this keeps
|
|
11
|
+
// wide letters (W, M, capitals) from being cut off.
|
|
12
|
+
const COLUMN_WIDTH_PADDING = 1;
|
|
13
|
+
/** Characters in the longest line of `text` (code points, so č or 🙂 count once). */
|
|
14
|
+
function longestLine(text) {
|
|
15
|
+
let longest = 0;
|
|
16
|
+
for (const line of text.split(/\r\n|\r|\n/))
|
|
17
|
+
longest = Math.max(longest, Array.from(line).length);
|
|
18
|
+
return longest;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A width per column that fits its header and the longest cell text (the text the table
|
|
22
|
+
* shows, which is close to what Excel shows), plus padding, between 8 and 60 characters.
|
|
23
|
+
*/
|
|
24
|
+
export function xlsxColumnWidths(data) {
|
|
25
|
+
return data.columns.map((column, index) => {
|
|
26
|
+
let longest = longestLine(column.header);
|
|
27
|
+
for (const cells of data.rows) {
|
|
28
|
+
const text = cells[index]?.text;
|
|
29
|
+
if (text)
|
|
30
|
+
longest = Math.max(longest, longestLine(text));
|
|
31
|
+
}
|
|
32
|
+
return Math.min(MAX_COLUMN_WIDTH, Math.max(MIN_COLUMN_WIDTH, longest + COLUMN_WIDTH_PADDING));
|
|
33
|
+
});
|
|
34
|
+
}
|
|
7
35
|
/** The export data as sheet rows: a bold header row, then typed cells. */
|
|
8
36
|
export function exportDataToSheet(data, formats = DEFAULT_XLSX_DATE_FORMATS) {
|
|
9
37
|
const header = data.columns.map((column) => ({
|
|
@@ -29,11 +57,17 @@ export async function exportDataToXlsxBlob(data, options = {}) {
|
|
|
29
57
|
return writeXlsxFile(sheetData, {
|
|
30
58
|
sheet: options.sheet,
|
|
31
59
|
stickyRowsCount: 1,
|
|
32
|
-
columns: data.
|
|
33
|
-
width: Math.max(8, Math.round(column.size / PX_PER_CHAR))
|
|
34
|
-
}))
|
|
60
|
+
columns: xlsxColumnWidths(data).map((width) => ({ width }))
|
|
35
61
|
}).toBlob();
|
|
36
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Plain row objects as an .xlsx file, for an export that isn't a DataTable: typed cells, a bold
|
|
65
|
+
* frozen header row and content-fitted column widths, exactly like a table export. Download
|
|
66
|
+
* the result with `downloadBlob(blob, 'name.xlsx')`.
|
|
67
|
+
*/
|
|
68
|
+
export async function exportRowsToXlsxBlob(rows, columns, options = {}) {
|
|
69
|
+
return exportDataToXlsxBlob(rowsToExportData(rows, columns, options.locale), options);
|
|
70
|
+
}
|
|
37
71
|
function pickFormats({ date, time, dateTime }) {
|
|
38
72
|
return {
|
|
39
73
|
...(date !== undefined ? { date } : {}),
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { DataTableExportData } from '../export/export-data';
|
|
2
|
+
import type { DataTableColumnType } from '../types';
|
|
3
|
+
/**
|
|
4
|
+
* A column of plain row objects for `exportRowsToXlsxBlob`: which key to read, and how the
|
|
5
|
+
* values are typed (the same `type` and `formatOptions` a DataTable column has).
|
|
6
|
+
*/
|
|
7
|
+
export type XlsxRowsColumn = {
|
|
8
|
+
/** The key to read from each row object. */
|
|
9
|
+
key: string;
|
|
10
|
+
/** The header cell; defaults to `key`. */
|
|
11
|
+
header?: string;
|
|
12
|
+
/**
|
|
13
|
+
* How the value is written. `number`, `currency` and `percent` become Excel numbers, `date`,
|
|
14
|
+
* `time` and `date-time` Excel dates, `boolean` TRUE/FALSE. Without a type (or `text`) the
|
|
15
|
+
* value is written as text, so a numeric column needs a type to stay computable.
|
|
16
|
+
*/
|
|
17
|
+
type?: DataTableColumnType;
|
|
18
|
+
/**
|
|
19
|
+
* Intl options for the Excel format of a typed column: `{ minimumFractionDigits: 2,
|
|
20
|
+
* maximumFractionDigits: 2 }` for fixed decimals, `{ useGrouping: false,
|
|
21
|
+
* maximumFractionDigits: 0 }` for whole numbers without a thousands separator (`useGrouping:
|
|
22
|
+
* false` alone keeps the decimals a value has). A `number` column with none shows as many
|
|
23
|
+
* decimals as each value has (up to 3).
|
|
24
|
+
*/
|
|
25
|
+
formatOptions?: Intl.NumberFormatOptions | Intl.DateTimeFormatOptions;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Plain row objects as the export data the xlsx exporter takes, so they get the same typed
|
|
29
|
+
* cells, bold frozen header and content-fitted widths as a DataTable export.
|
|
30
|
+
*/
|
|
31
|
+
export declare function rowsToExportData(rows: ReadonlyArray<Record<string, unknown>>, columns: ReadonlyArray<XlsxRowsColumn>, locale?: string): DataTableExportData;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { TYPE_NUMBER_FORMAT_DEFAULTS } from '../data-table-utils';
|
|
2
|
+
import { formatDateCell } from '../date-cell';
|
|
3
|
+
const NUMBER_TYPES = new Set(['number', 'currency', 'percent']);
|
|
4
|
+
const isEmpty = (value) => value === null || value === undefined || value === '';
|
|
5
|
+
/**
|
|
6
|
+
* The text a DataTable cell of this type would show (same rules as the table's own cell text):
|
|
7
|
+
* what text and fallback cells write, and what column widths are measured on. `''` when the
|
|
8
|
+
* table would show `-`.
|
|
9
|
+
*/
|
|
10
|
+
function cellText(column, value, locale) {
|
|
11
|
+
if (isEmpty(value))
|
|
12
|
+
return '';
|
|
13
|
+
const { type } = column;
|
|
14
|
+
try {
|
|
15
|
+
if (type && NUMBER_TYPES.has(type)) {
|
|
16
|
+
// Like the table: numeric strings count as numbers, anything else shows nothing.
|
|
17
|
+
const number = Number(value);
|
|
18
|
+
if (Number.isNaN(number))
|
|
19
|
+
return '';
|
|
20
|
+
return new Intl.NumberFormat(locale, {
|
|
21
|
+
...TYPE_NUMBER_FORMAT_DEFAULTS[type],
|
|
22
|
+
...column.formatOptions
|
|
23
|
+
}).format(number);
|
|
24
|
+
}
|
|
25
|
+
if (type === 'date' || type === 'time' || type === 'date-time') {
|
|
26
|
+
// Reads a Date, an ISO/DB string, an epoch number... the way the table does.
|
|
27
|
+
return (formatDateCell(value, type, locale, column.formatOptions) ??
|
|
28
|
+
'');
|
|
29
|
+
}
|
|
30
|
+
if (type === 'boolean')
|
|
31
|
+
return value ? 'Yes' : 'No';
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Invalid Intl options: fall back to the plain string.
|
|
35
|
+
}
|
|
36
|
+
return String(value);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Plain row objects as the export data the xlsx exporter takes, so they get the same typed
|
|
40
|
+
* cells, bold frozen header and content-fitted widths as a DataTable export.
|
|
41
|
+
*/
|
|
42
|
+
export function rowsToExportData(rows, columns, locale = 'en-US') {
|
|
43
|
+
return {
|
|
44
|
+
columns: columns.map((column) => ({
|
|
45
|
+
id: column.key,
|
|
46
|
+
header: column.header ?? column.key,
|
|
47
|
+
type: column.type,
|
|
48
|
+
formatOptions: column.formatOptions,
|
|
49
|
+
hasCustomCell: false,
|
|
50
|
+
hasExportValue: false,
|
|
51
|
+
locale,
|
|
52
|
+
// Only a table has a width in px; an xlsx export measures the content instead.
|
|
53
|
+
size: 0
|
|
54
|
+
})),
|
|
55
|
+
rows: rows.map((row) => columns.map((column) => {
|
|
56
|
+
const value = row[column.key];
|
|
57
|
+
return { value, text: cellText(column, value, locale) };
|
|
58
|
+
}))
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
let { class: className = '', ...restProps } = $props();
|
|
3
|
-
</script>
|
|
4
|
-
|
|
5
|
-
<svg
|
|
6
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
7
|
-
width="1em"
|
|
8
|
-
height="1em"
|
|
9
|
-
viewBox="0 0 256 256"
|
|
10
|
-
class={className}
|
|
11
|
-
{...restProps}
|
|
12
|
-
>
|
|
13
|
-
<path
|
|
14
|
-
fill="currentColor"
|
|
15
|
-
d="M224 144v64a8 8 0 0 1-8 8H40a8 8 0 0 1-8-8v-64a8 8 0 0 1 16 0v56h160v-56a8 8 0 0 1 16 0m-101.66 5.66a8 8 0 0 0 11.32 0l40-40a8 8 0 0 0-11.32-11.32L136 124.69V32a8 8 0 0 0-16 0v92.69L93.66 98.34a8 8 0 0 0-11.32 11.32Z"
|
|
16
|
-
/>
|
|
17
|
-
</svg>
|
|
18
|
-
<!-- Icon from Phosphor by Phosphor Icons - https://github.com/phosphor-icons/core/blob/main/LICENSE -->
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
let { class: className = '', ...restProps } = $props();
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<svg
|
|
6
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
7
|
+
width="1em"
|
|
8
|
+
height="1em"
|
|
9
|
+
viewBox="0 0 256 256"
|
|
10
|
+
class={className}
|
|
11
|
+
{...restProps}
|
|
12
|
+
>
|
|
13
|
+
<path
|
|
14
|
+
fill="currentColor"
|
|
15
|
+
d="M224 144v64a8 8 0 0 1-8 8H40a8 8 0 0 1-8-8v-64a8 8 0 0 1 16 0v56h160v-56a8 8 0 0 1 16 0m-101.66 5.66a8 8 0 0 0 11.32 0l40-40a8 8 0 0 0-11.32-11.32L136 124.69V32a8 8 0 0 0-16 0v92.69L93.66 98.34a8 8 0 0 0-11.32 11.32Z"
|
|
16
|
+
/>
|
|
17
|
+
</svg>
|
|
18
|
+
<!-- Icon from Phosphor by Phosphor Icons - https://github.com/phosphor-icons/core/blob/main/LICENSE -->
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export * as HoverCard from './components/hover-card';
|
|
|
11
11
|
export * as ScrollArea from './components/scroll-area';
|
|
12
12
|
export { loadImage, fileToDataUrl, cropImage, processImage } from './utils/image-processing';
|
|
13
13
|
export type { ProcessImageOptions, CropRegion } from './utils/image-processing';
|
|
14
|
+
export { downloadBlob } from './utils/download';
|
|
14
15
|
export * as Carousel from './components/carousel';
|
|
15
16
|
export { default as Checkbox } from './components/checkbox/checkbox.svelte';
|
|
16
17
|
export { default as CheckboxGroup } from './components/checkbox/checkbox-group.svelte';
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ export * as Collapsible from './components/collapsible';
|
|
|
9
9
|
export * as HoverCard from './components/hover-card';
|
|
10
10
|
export * as ScrollArea from './components/scroll-area';
|
|
11
11
|
export { loadImage, fileToDataUrl, cropImage, processImage } from './utils/image-processing';
|
|
12
|
+
export { downloadBlob } from './utils/download';
|
|
12
13
|
export * as Carousel from './components/carousel';
|
|
13
14
|
export { default as Checkbox } from './components/checkbox/checkbox.svelte';
|
|
14
15
|
export { default as CheckboxGroup } from './components/checkbox/checkbox-group.svelte';
|