beautiful-grid 1.0.11 → 1.0.13
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 +153 -117
- package/cjs/BGrid.js +193 -39
- package/cjs/components/Table.js +37 -2
- package/cjs/components/TableBodyCell.js +2 -1
- package/cjs/components/TableBodyRow.js +5 -1
- package/cjs/components/TreeCell.js +88 -0
- package/cjs/components/TreeContext.js +42 -0
- package/cjs/utils/exportCsv.js +129 -0
- package/cjs/utils/exportData.js +230 -0
- package/cjs/utils/exportExcel.js +317 -0
- package/cjs/utils/index.js +4 -0
- package/cjs/utils/treeData.js +205 -0
- package/esm/BGrid.js +221 -66
- package/esm/components/Table.js +36 -4
- package/esm/components/TableBodyCell.js +2 -1
- package/esm/components/TableBodyRow.js +5 -1
- package/esm/components/TreeCell.js +49 -0
- package/esm/components/TreeContext.js +5 -0
- package/esm/utils/exportCsv.js +97 -0
- package/esm/utils/exportData.js +154 -0
- package/esm/utils/exportExcel.js +246 -0
- package/esm/utils/index.js +4 -0
- package/esm/utils/treeData.js +174 -0
- package/package.json +1 -1
- package/style.css +125 -43
- package/types/BGrid.d.ts +1 -1
- package/types/components/Table.d.ts +3 -1
- package/types/components/TableBodyCell.d.ts +2 -2
- package/types/components/TreeCell.d.ts +9 -0
- package/types/components/TreeContext.d.ts +16 -0
- package/types/types.d.ts +121 -2
- package/types/utils/exportCsv.d.ts +5 -0
- package/types/utils/exportData.d.ts +16 -0
- package/types/utils/exportExcel.d.ts +5 -0
- package/types/utils/index.d.ts +4 -0
- package/types/utils/treeData.d.ts +22 -0
|
@@ -2,6 +2,7 @@ import * as React from 'react';
|
|
|
2
2
|
import { PluginCellEditor } from './PluginCellEditor';
|
|
3
3
|
import { CellEditorIcon } from './CellEditorIcon';
|
|
4
4
|
import { CheckboxCellEditor } from './CheckboxCellEditor';
|
|
5
|
+
import { TreeCell } from './TreeCell';
|
|
5
6
|
function Cell({ index, hostIndex = index, columnIndex, column, item, valueByRowKey, handleSave, handleCancel, handleMove, editable, cellEditable, interactionEditing = false, editSession, }) {
|
|
6
7
|
const isPluginEditorActive = Boolean(editable && editSession && column.editor?.type === 'plugin');
|
|
7
8
|
let content;
|
|
@@ -36,7 +37,7 @@ function Cell({ index, hostIndex = index, columnIndex, column, item, valueByRowK
|
|
|
36
37
|
content = renderSafeCellValue(valueByRowKey);
|
|
37
38
|
}
|
|
38
39
|
return (React.createElement("div", { className: isPluginEditorActive ? 'bgrid-cell-content bgrid-cell-content-plugin-editor' : 'bgrid-cell-content' },
|
|
39
|
-
React.createElement("div", { className: 'bgrid-cell-value' }, content),
|
|
40
|
+
React.createElement("div", { className: 'bgrid-cell-value' }, column.treeCell ? (React.createElement(TreeCell, { column: column, item: item }, content)) : (content)),
|
|
40
41
|
column.editorIcon && column.editor?.type !== 'checkbox' && cellEditable && (React.createElement(CellEditorIcon, { hostCell: { rowIndex: hostIndex, columnIndex }, index: index, columnIndex: columnIndex, column: column, item: item, value: valueByRowKey, editing: interactionEditing }))));
|
|
41
42
|
}
|
|
42
43
|
function renderSafeCellValue(value, preserveRenderableArrays = false) {
|
|
@@ -4,6 +4,7 @@ import { getCellValueByRowKey, isCellEdited, isCellValueChanged, resolveLogicalC
|
|
|
4
4
|
import { TableBodyCell } from './TableBodyCell';
|
|
5
5
|
import RowSelector from './RowSelector';
|
|
6
6
|
import { GripVertical } from './GripVertical';
|
|
7
|
+
import { useTreeContext } from './TreeContext';
|
|
7
8
|
function TableBodyCellItemInner({ ri, columnIndex, column, canonicalIndex, canonicalItem, valueByRowKey, editable, disabled, editTrigger, rowSpan, isMerged, tdEditable, cellEditable, isLogicalEditing, isCheckboxEditor, isSearchMatch, isCurrentSearchMatch, cellInteractionSession, handleClick, setEditItem, setItemValue, handleMoveEditFocus, }) {
|
|
8
9
|
const resolvedEditTrigger = column.editTrigger ?? editTrigger ?? 'dblclick';
|
|
9
10
|
const edited = isCellEdited(canonicalItem, column);
|
|
@@ -55,9 +56,12 @@ function TableBodyCellItemInner({ ri, columnIndex, column, canonicalIndex, canon
|
|
|
55
56
|
}
|
|
56
57
|
const TableBodyCellItem = React.memo(TableBodyCellItemInner);
|
|
57
58
|
function TableBodyRowInner({ item, data, ri, sourceIndex, columns, startCIdx, endCIdx, frozenColumnIndex, isLeftRegion, showLineNumber, hasRowChecked, isRadio, checked, rowCheckedDisabled, rowStatus, active, className, resolvedRowHeight, itemHeight, itemPadding, editable, disabled, editTrigger, hasOnClick, cellMergeOptions, mergeColumns, odd, variant, rowReorderEnabled, rowReorderRole, rowReorderPhase, rowReorderDirection, rowReorderOffset, reorder, reorderingInfo, searchMatchTokens, currentSearchToken, isRowEditing, cellInteractionSession, handleClick, setEditItem, setItemValue, handleMoveEditFocus, handleChangeChecked, handleChangeCheckedRadio, getRowSpan, onRowReorderPointerDown, onRowReorderKeyDown, }) {
|
|
59
|
+
const tree = useTreeContext();
|
|
60
|
+
const treeRowKey = tree?.getRowKey(item.values);
|
|
61
|
+
const treeMeta = treeRowKey === undefined ? undefined : tree?.metaByRowKey.get(treeRowKey);
|
|
58
62
|
const clickable = !editable && hasOnClick;
|
|
59
63
|
const rowClassName = ['bgrid-body-row', active ? 'bgrid-row-active' : '', className].filter(Boolean).join(' ');
|
|
60
|
-
return (React.createElement("tr", { className: rowClassName, "data-ri": ri, "data-odd": !mergeColumns && odd ? 'true' : undefined, "data-clickable": clickable ? 'true' : undefined, "data-bgrid-row-reorder-role": rowReorderRole, "data-bgrid-row-reorder-phase": rowReorderRole ? rowReorderPhase : undefined, "data-bgrid-row-reorder-direction": rowReorderRole ? rowReorderDirection : undefined, style: {
|
|
64
|
+
return (React.createElement("tr", { className: rowClassName, "data-ri": ri, "data-bgrid-tree-row": treeMeta ? 'true' : undefined, "aria-level": treeMeta ? treeMeta.depth + 1 : undefined, "aria-posinset": treeMeta ? treeMeta.siblingIndex + 1 : undefined, "aria-setsize": treeMeta?.siblingCount, "aria-expanded": treeMeta?.hasChildren ? treeMeta.expanded : undefined, "data-odd": !mergeColumns && odd ? 'true' : undefined, "data-clickable": clickable ? 'true' : undefined, "data-bgrid-row-reorder-role": rowReorderRole, "data-bgrid-row-reorder-phase": rowReorderRole ? rowReorderPhase : undefined, "data-bgrid-row-reorder-direction": rowReorderRole ? rowReorderDirection : undefined, style: {
|
|
61
65
|
['--bgrid-item-line-height']: `${itemHeight}px`,
|
|
62
66
|
['--bgrid-item-cell-height']: `${resolvedRowHeight}px`,
|
|
63
67
|
...(rowReorderRole && rowReorderRole !== 'source'
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { useAppStoreApi } from '../store';
|
|
3
|
+
import { getColumnId } from '../utils';
|
|
4
|
+
import { useTreeContext } from './TreeContext';
|
|
5
|
+
export function TreeCell({ column, item, children }) {
|
|
6
|
+
const tree = useTreeContext();
|
|
7
|
+
const store = useAppStoreApi();
|
|
8
|
+
if (!tree || getColumnId(column) !== tree.treeColumnId)
|
|
9
|
+
return React.createElement(React.Fragment, null, children);
|
|
10
|
+
const key = tree.getRowKey(item.values);
|
|
11
|
+
const meta = key === undefined ? undefined : tree.metaByRowKey.get(key);
|
|
12
|
+
if (!meta)
|
|
13
|
+
return React.createElement(React.Fragment, null, children);
|
|
14
|
+
return (React.createElement("span", { className: 'bgrid-tree-cell', style: {
|
|
15
|
+
'--bgrid-tree-depth': meta.depth,
|
|
16
|
+
'--bgrid-tree-indent-size': `${tree.indentSize}px`,
|
|
17
|
+
} },
|
|
18
|
+
meta.hasChildren ? (React.createElement("button", { type: 'button', className: 'bgrid-tree-toggle', "aria-expanded": meta.expanded, "aria-label": meta.expanded ? tree.collapseAriaLabel : tree.expandAriaLabel, disabled: tree.disabled, onClick: event => {
|
|
19
|
+
event.stopPropagation();
|
|
20
|
+
if (meta.expanded) {
|
|
21
|
+
const state = store.getState();
|
|
22
|
+
const activeCell = state.activeCell;
|
|
23
|
+
const activeSourceIndex = activeCell === undefined
|
|
24
|
+
? undefined
|
|
25
|
+
: state.sourceIndexByVisibleIndex?.[activeCell.rowIndex] ?? activeCell.rowIndex;
|
|
26
|
+
let activeMeta = activeSourceIndex === undefined ? undefined : tree.metaBySourceIndex.get(activeSourceIndex);
|
|
27
|
+
let activeIsDescendant = false;
|
|
28
|
+
while (activeMeta?.parentSourceIndex !== undefined) {
|
|
29
|
+
if (activeMeta.parentSourceIndex === meta.sourceIndex) {
|
|
30
|
+
activeIsDescendant = true;
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
activeMeta = tree.metaBySourceIndex.get(activeMeta.parentSourceIndex);
|
|
34
|
+
}
|
|
35
|
+
state.clearCellSelection();
|
|
36
|
+
if (activeCell && activeIsDescendant) {
|
|
37
|
+
state.endCellEdit();
|
|
38
|
+
state.setActiveCell({
|
|
39
|
+
rowIndex: state.visibleIndexBySourceIndex?.get(meta.sourceIndex) ?? 0,
|
|
40
|
+
columnIndex: activeCell.columnIndex,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
tree.toggle(meta);
|
|
45
|
+
} },
|
|
46
|
+
React.createElement("span", { className: 'bgrid-tree-icon', "aria-hidden": 'true' }, meta.expanded ? tree.icons?.expanded ?? '⌄' : tree.icons?.collapsed ?? '›'))) : (React.createElement("span", { className: 'bgrid-tree-toggle-spacer', "aria-hidden": 'true' },
|
|
47
|
+
React.createElement("span", { className: 'bgrid-tree-icon' }, tree.icons?.leaf))),
|
|
48
|
+
React.createElement("span", { className: 'bgrid-tree-cell-content' }, children)));
|
|
49
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
export function formatCsvCellValue(value, preventFormulaInjection = true) {
|
|
2
|
+
if (value === null || value === undefined) {
|
|
3
|
+
return '';
|
|
4
|
+
}
|
|
5
|
+
let formatted;
|
|
6
|
+
if (typeof value === 'string') {
|
|
7
|
+
formatted = value;
|
|
8
|
+
const startsWithFormula = /^[=+\-@]/.test(formatted);
|
|
9
|
+
const startsWithDangerousControl = /^[\t\r\n]/.test(formatted);
|
|
10
|
+
const hasFormulaAfterLeadingWhitespace = /^\s+[=+\-@]/.test(formatted);
|
|
11
|
+
if (preventFormulaInjection &&
|
|
12
|
+
(startsWithFormula || startsWithDangerousControl || hasFormulaAfterLeadingWhitespace)) {
|
|
13
|
+
formatted = `'${formatted}`;
|
|
14
|
+
}
|
|
15
|
+
return formatted;
|
|
16
|
+
}
|
|
17
|
+
if (typeof value === 'number') {
|
|
18
|
+
return String(value);
|
|
19
|
+
}
|
|
20
|
+
if (typeof value === 'boolean') {
|
|
21
|
+
return value ? 'true' : 'false';
|
|
22
|
+
}
|
|
23
|
+
if (typeof value === 'bigint') {
|
|
24
|
+
return value.toString();
|
|
25
|
+
}
|
|
26
|
+
if (value instanceof Date) {
|
|
27
|
+
return isNaN(value.getTime()) ? 'Invalid Date' : value.toISOString();
|
|
28
|
+
}
|
|
29
|
+
if (typeof value === 'object') {
|
|
30
|
+
try {
|
|
31
|
+
formatted = JSON.stringify(value);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
formatted = String(value);
|
|
35
|
+
}
|
|
36
|
+
return formatted;
|
|
37
|
+
}
|
|
38
|
+
return String(value);
|
|
39
|
+
}
|
|
40
|
+
export function escapeCsvCell(cell, delimiter = ',') {
|
|
41
|
+
const needsQuotes = cell.includes(delimiter) ||
|
|
42
|
+
cell.includes('"') ||
|
|
43
|
+
cell.includes('\r') ||
|
|
44
|
+
cell.includes('\n');
|
|
45
|
+
if (needsQuotes) {
|
|
46
|
+
return `"${cell.replace(/"/g, '""')}"`;
|
|
47
|
+
}
|
|
48
|
+
return cell;
|
|
49
|
+
}
|
|
50
|
+
export function serializeGridExportDataToCsv(data, options) {
|
|
51
|
+
const delimiter = options?.delimiter ?? ',';
|
|
52
|
+
const newline = options?.newline ?? '\r\n';
|
|
53
|
+
const includeHeader = options?.includeHeader ?? true;
|
|
54
|
+
const bom = options?.bom ?? true;
|
|
55
|
+
const preventFormulaInjection = options?.preventFormulaInjection ?? true;
|
|
56
|
+
const lines = [];
|
|
57
|
+
if (includeHeader) {
|
|
58
|
+
const headerLine = data.columns
|
|
59
|
+
.map(col => escapeCsvCell(formatCsvCellValue(col.header, preventFormulaInjection), delimiter))
|
|
60
|
+
.join(delimiter);
|
|
61
|
+
lines.push(headerLine);
|
|
62
|
+
}
|
|
63
|
+
for (const row of data.rows) {
|
|
64
|
+
const rowLine = row.cells
|
|
65
|
+
.map(cell => escapeCsvCell(formatCsvCellValue(cell, preventFormulaInjection), delimiter))
|
|
66
|
+
.join(delimiter);
|
|
67
|
+
lines.push(rowLine);
|
|
68
|
+
}
|
|
69
|
+
const csvText = lines.join(newline);
|
|
70
|
+
return (bom ? '\uFEFF' : '') + csvText;
|
|
71
|
+
}
|
|
72
|
+
export function downloadCsv(csv, fileName = 'export.csv') {
|
|
73
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
let resolvedName = (fileName || '').trim();
|
|
77
|
+
if (!resolvedName) {
|
|
78
|
+
resolvedName = 'export.csv';
|
|
79
|
+
}
|
|
80
|
+
else if (!resolvedName.toLowerCase().endsWith('.csv')) {
|
|
81
|
+
resolvedName += '.csv';
|
|
82
|
+
}
|
|
83
|
+
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
|
84
|
+
if (typeof URL !== 'undefined' && typeof URL.createObjectURL === 'function') {
|
|
85
|
+
const url = URL.createObjectURL(blob);
|
|
86
|
+
const link = document.createElement('a');
|
|
87
|
+
link.href = url;
|
|
88
|
+
link.setAttribute('download', resolvedName);
|
|
89
|
+
link.style.display = 'none';
|
|
90
|
+
document.body.appendChild(link);
|
|
91
|
+
link.click();
|
|
92
|
+
document.body.removeChild(link);
|
|
93
|
+
if (typeof URL.revokeObjectURL === 'function') {
|
|
94
|
+
URL.revokeObjectURL(url);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { getColumnId } from './getColumnId';
|
|
2
|
+
import { getCellValueByRowKey } from './getCellValue';
|
|
3
|
+
export function resolveExportColumns(columns, sourceColumns, scope = 'visible') {
|
|
4
|
+
let selectedColumns;
|
|
5
|
+
if (Array.isArray(scope)) {
|
|
6
|
+
const candidatePool = sourceColumns && sourceColumns.length > 0 ? sourceColumns : columns;
|
|
7
|
+
const columnMap = new Map();
|
|
8
|
+
for (const col of candidatePool) {
|
|
9
|
+
columnMap.set(getColumnId(col), col);
|
|
10
|
+
}
|
|
11
|
+
selectedColumns = [];
|
|
12
|
+
for (const id of scope) {
|
|
13
|
+
const col = columnMap.get(id);
|
|
14
|
+
if (col) {
|
|
15
|
+
selectedColumns.push(col);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
else if (scope === 'all') {
|
|
20
|
+
selectedColumns = sourceColumns && sourceColumns.length > 0 ? [...sourceColumns] : [...columns];
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
// 'visible'
|
|
24
|
+
selectedColumns = [...columns];
|
|
25
|
+
}
|
|
26
|
+
const exportableColumns = selectedColumns.filter(col => col.exportable !== false);
|
|
27
|
+
return exportableColumns.map((column, columnIndex) => {
|
|
28
|
+
const columnId = getColumnId(column);
|
|
29
|
+
let header;
|
|
30
|
+
if (typeof column.exportHeader === 'function') {
|
|
31
|
+
header = column.exportHeader(column);
|
|
32
|
+
}
|
|
33
|
+
else if (typeof column.exportHeader === 'string') {
|
|
34
|
+
header = column.exportHeader;
|
|
35
|
+
}
|
|
36
|
+
else if (typeof column.label === 'string' || typeof column.label === 'number') {
|
|
37
|
+
header = String(column.label);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
header = columnId;
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
columnId,
|
|
44
|
+
header,
|
|
45
|
+
column,
|
|
46
|
+
columnIndex,
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
export function resolveExportRows(exportColumns, params) {
|
|
51
|
+
const rowScope = params.options?.rows ?? 'displayed';
|
|
52
|
+
const rows = [];
|
|
53
|
+
const sourcePool = params.sourceData && params.sourceData.length > 0 ? params.sourceData : params.data;
|
|
54
|
+
const buildCells = (item, values, sourceIndex, visibleIndex) => {
|
|
55
|
+
const cells = [];
|
|
56
|
+
for (const expCol of exportColumns) {
|
|
57
|
+
const rawValue = getCellValueByRowKey(expCol.column.key, values);
|
|
58
|
+
if (expCol.column.getExportValue) {
|
|
59
|
+
cells.push(expCol.column.getExportValue({
|
|
60
|
+
column: expCol.column,
|
|
61
|
+
columnId: expCol.columnId,
|
|
62
|
+
visibleIndex,
|
|
63
|
+
sourceIndex,
|
|
64
|
+
item,
|
|
65
|
+
values,
|
|
66
|
+
value: rawValue,
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
cells.push(rawValue);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return cells;
|
|
74
|
+
};
|
|
75
|
+
const resolveRowKey = (item, values) => {
|
|
76
|
+
let rowKey;
|
|
77
|
+
if (params.rowKey !== undefined && values) {
|
|
78
|
+
const rawKey = getCellValueByRowKey(params.rowKey, values);
|
|
79
|
+
if (rawKey !== undefined && rawKey !== null) {
|
|
80
|
+
rowKey = rawKey;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (rowKey === undefined && item?.key !== undefined) {
|
|
84
|
+
rowKey = item.key;
|
|
85
|
+
}
|
|
86
|
+
return rowKey;
|
|
87
|
+
};
|
|
88
|
+
if (rowScope === 'displayed') {
|
|
89
|
+
const displayData = params.data;
|
|
90
|
+
for (let visibleIndex = 0; visibleIndex < displayData.length; visibleIndex++) {
|
|
91
|
+
const item = displayData[visibleIndex] ?? { values: {} };
|
|
92
|
+
const sourceIndex = params.sourceIndexByVisibleIndex?.[visibleIndex] ?? visibleIndex;
|
|
93
|
+
const values = item.values ?? {};
|
|
94
|
+
const rowKey = resolveRowKey(item, values);
|
|
95
|
+
const cells = buildCells(item, values, sourceIndex, visibleIndex);
|
|
96
|
+
rows.push({
|
|
97
|
+
visibleIndex,
|
|
98
|
+
sourceIndex,
|
|
99
|
+
rowKey,
|
|
100
|
+
item,
|
|
101
|
+
values,
|
|
102
|
+
cells,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else if (rowScope === 'checked') {
|
|
107
|
+
const checkedMap = params.checkedIndexesMap;
|
|
108
|
+
for (let sourceIndex = 0; sourceIndex < sourcePool.length; sourceIndex++) {
|
|
109
|
+
if (!checkedMap?.has(sourceIndex))
|
|
110
|
+
continue;
|
|
111
|
+
const item = sourcePool[sourceIndex] ?? { values: {} };
|
|
112
|
+
const visibleIndex = params.visibleIndexBySourceIndex?.get(sourceIndex);
|
|
113
|
+
const values = item.values ?? {};
|
|
114
|
+
const rowKey = resolveRowKey(item, values);
|
|
115
|
+
const cells = buildCells(item, values, sourceIndex, visibleIndex);
|
|
116
|
+
rows.push({
|
|
117
|
+
visibleIndex,
|
|
118
|
+
sourceIndex,
|
|
119
|
+
rowKey,
|
|
120
|
+
item,
|
|
121
|
+
values,
|
|
122
|
+
cells,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
// 'source'
|
|
128
|
+
for (let sourceIndex = 0; sourceIndex < sourcePool.length; sourceIndex++) {
|
|
129
|
+
const item = sourcePool[sourceIndex] ?? { values: {} };
|
|
130
|
+
const visibleIndex = params.visibleIndexBySourceIndex?.get(sourceIndex);
|
|
131
|
+
const values = item.values ?? {};
|
|
132
|
+
const rowKey = resolveRowKey(item, values);
|
|
133
|
+
const cells = buildCells(item, values, sourceIndex, visibleIndex);
|
|
134
|
+
rows.push({
|
|
135
|
+
visibleIndex,
|
|
136
|
+
sourceIndex,
|
|
137
|
+
rowKey,
|
|
138
|
+
item,
|
|
139
|
+
values,
|
|
140
|
+
cells,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return rows;
|
|
145
|
+
}
|
|
146
|
+
export function createGridExportData(params) {
|
|
147
|
+
const columnScope = params.options?.columns ?? 'visible';
|
|
148
|
+
const columns = resolveExportColumns(params.columns, params.sourceColumns, columnScope);
|
|
149
|
+
const rows = resolveExportRows(columns, params);
|
|
150
|
+
return {
|
|
151
|
+
columns,
|
|
152
|
+
rows,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
function makeCrc32Table() {
|
|
2
|
+
const table = new Uint32Array(256);
|
|
3
|
+
for (let i = 0; i < 256; i++) {
|
|
4
|
+
let c = i;
|
|
5
|
+
for (let k = 0; k < 8; k++) {
|
|
6
|
+
c = (c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1);
|
|
7
|
+
}
|
|
8
|
+
table[i] = c;
|
|
9
|
+
}
|
|
10
|
+
return table;
|
|
11
|
+
}
|
|
12
|
+
const crcTable = makeCrc32Table();
|
|
13
|
+
function calculateCrc32(bytes) {
|
|
14
|
+
let crc = 0xffffffff;
|
|
15
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
16
|
+
crc = (crc >>> 8) ^ crcTable[(crc ^ bytes[i]) & 0xff];
|
|
17
|
+
}
|
|
18
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
19
|
+
}
|
|
20
|
+
export function escapeXml(value) {
|
|
21
|
+
return value
|
|
22
|
+
.replace(/&/g, '&')
|
|
23
|
+
.replace(/</g, '<')
|
|
24
|
+
.replace(/>/g, '>')
|
|
25
|
+
.replace(/"/g, '"')
|
|
26
|
+
.replace(/'/g, ''')
|
|
27
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, '');
|
|
28
|
+
}
|
|
29
|
+
export function colIndexToExcelColumn(colIndex) {
|
|
30
|
+
let name = '';
|
|
31
|
+
let n = colIndex;
|
|
32
|
+
while (n >= 0) {
|
|
33
|
+
name = String.fromCharCode((n % 26) + 65) + name;
|
|
34
|
+
n = Math.floor(n / 26) - 1;
|
|
35
|
+
}
|
|
36
|
+
return name;
|
|
37
|
+
}
|
|
38
|
+
function createZipArchive(files) {
|
|
39
|
+
const encoder = new TextEncoder();
|
|
40
|
+
const fileEntries = [];
|
|
41
|
+
let offset = 0;
|
|
42
|
+
for (const file of files) {
|
|
43
|
+
const nameBytes = encoder.encode(file.name);
|
|
44
|
+
const dataBytes = typeof file.data === 'string' ? encoder.encode(file.data) : file.data;
|
|
45
|
+
const crc = calculateCrc32(dataBytes);
|
|
46
|
+
const localHeader = new Uint8Array(30 + nameBytes.length);
|
|
47
|
+
const dv = new DataView(localHeader.buffer);
|
|
48
|
+
dv.setUint32(0, 0x04034b50, true); // Local file header signature
|
|
49
|
+
dv.setUint16(4, 20, true); // Version needed to extract (2.0)
|
|
50
|
+
dv.setUint16(6, 0x0800, true); // Bit flag: UTF-8 filename (bit 11)
|
|
51
|
+
dv.setUint16(8, 0, true); // Compression method: STORE (0)
|
|
52
|
+
dv.setUint16(10, 0, true); // File last mod time
|
|
53
|
+
dv.setUint16(12, 0, true); // File last mod date
|
|
54
|
+
dv.setUint32(14, crc, true); // CRC-32
|
|
55
|
+
dv.setUint32(18, dataBytes.length, true); // Compressed size
|
|
56
|
+
dv.setUint32(22, dataBytes.length, true); // Uncompressed size
|
|
57
|
+
dv.setUint16(26, nameBytes.length, true); // Filename length
|
|
58
|
+
dv.setUint16(28, 0, true); // Extra field length
|
|
59
|
+
localHeader.set(nameBytes, 30);
|
|
60
|
+
fileEntries.push({
|
|
61
|
+
nameBytes,
|
|
62
|
+
dataBytes,
|
|
63
|
+
crc,
|
|
64
|
+
localHeader,
|
|
65
|
+
offset,
|
|
66
|
+
});
|
|
67
|
+
offset += localHeader.length + dataBytes.length;
|
|
68
|
+
}
|
|
69
|
+
const centralDirEntries = [];
|
|
70
|
+
let centralDirSize = 0;
|
|
71
|
+
for (const entry of fileEntries) {
|
|
72
|
+
const cd = new Uint8Array(46 + entry.nameBytes.length);
|
|
73
|
+
const dv = new DataView(cd.buffer);
|
|
74
|
+
dv.setUint32(0, 0x02014b50, true); // Central file header signature
|
|
75
|
+
dv.setUint16(4, 20, true); // Version made by
|
|
76
|
+
dv.setUint16(6, 20, true); // Version needed to extract
|
|
77
|
+
dv.setUint16(8, 0x0800, true); // Bit flag: UTF-8
|
|
78
|
+
dv.setUint16(10, 0, true); // Compression method: STORE
|
|
79
|
+
dv.setUint16(12, 0, true); // File last mod time
|
|
80
|
+
dv.setUint16(14, 0, true); // File last mod date
|
|
81
|
+
dv.setUint32(16, entry.crc, true); // CRC-32
|
|
82
|
+
dv.setUint32(20, entry.dataBytes.length, true); // Compressed size
|
|
83
|
+
dv.setUint32(24, entry.dataBytes.length, true); // Uncompressed size
|
|
84
|
+
dv.setUint16(28, entry.nameBytes.length, true); // Filename length
|
|
85
|
+
dv.setUint16(30, 0, true); // Extra field length
|
|
86
|
+
dv.setUint16(32, 0, true); // File comment length
|
|
87
|
+
dv.setUint16(34, 0, true); // Disk number start
|
|
88
|
+
dv.setUint16(36, 0, true); // Internal file attributes
|
|
89
|
+
dv.setUint32(38, 0, true); // External file attributes
|
|
90
|
+
dv.setUint32(42, entry.offset, true); // Relative offset of local header
|
|
91
|
+
cd.set(entry.nameBytes, 46);
|
|
92
|
+
centralDirEntries.push(cd);
|
|
93
|
+
centralDirSize += cd.length;
|
|
94
|
+
}
|
|
95
|
+
const eocd = new Uint8Array(22);
|
|
96
|
+
const dvEocd = new DataView(eocd.buffer);
|
|
97
|
+
dvEocd.setUint32(0, 0x06054b50, true); // End of central dir signature
|
|
98
|
+
dvEocd.setUint16(4, 0, true); // Number of this disk
|
|
99
|
+
dvEocd.setUint16(6, 0, true); // Number of disk with start of central directory
|
|
100
|
+
dvEocd.setUint16(8, fileEntries.length, true); // Total entries in this disk
|
|
101
|
+
dvEocd.setUint16(10, fileEntries.length, true); // Total entries
|
|
102
|
+
dvEocd.setUint32(12, centralDirSize, true); // Size of the central directory
|
|
103
|
+
dvEocd.setUint32(16, offset, true); // Offset of start of central directory
|
|
104
|
+
dvEocd.setUint16(20, 0, true); // ZIP file comment length
|
|
105
|
+
const totalLength = offset + centralDirSize + 22;
|
|
106
|
+
const result = new Uint8Array(totalLength);
|
|
107
|
+
let pos = 0;
|
|
108
|
+
for (const entry of fileEntries) {
|
|
109
|
+
result.set(entry.localHeader, pos);
|
|
110
|
+
pos += entry.localHeader.length;
|
|
111
|
+
result.set(entry.dataBytes, pos);
|
|
112
|
+
pos += entry.dataBytes.length;
|
|
113
|
+
}
|
|
114
|
+
for (const cd of centralDirEntries) {
|
|
115
|
+
result.set(cd, pos);
|
|
116
|
+
pos += cd.length;
|
|
117
|
+
}
|
|
118
|
+
result.set(eocd, pos);
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
function formatCellValueAsXml(cellValue, cellRef) {
|
|
122
|
+
if (cellValue === null || cellValue === undefined || cellValue === '') {
|
|
123
|
+
return `<c r="${cellRef}"/>`;
|
|
124
|
+
}
|
|
125
|
+
if (typeof cellValue === 'number') {
|
|
126
|
+
if (Number.isFinite(cellValue)) {
|
|
127
|
+
return `<c r="${cellRef}"><v>${cellValue}</v></c>`;
|
|
128
|
+
}
|
|
129
|
+
return `<c r="${cellRef}" t="inlineStr"><is><t>${escapeXml(String(cellValue))}</t></is></c>`;
|
|
130
|
+
}
|
|
131
|
+
if (typeof cellValue === 'boolean') {
|
|
132
|
+
return `<c r="${cellRef}" t="b"><v>${cellValue ? 1 : 0}</v></c>`;
|
|
133
|
+
}
|
|
134
|
+
if (typeof cellValue === 'bigint') {
|
|
135
|
+
return `<c r="${cellRef}" t="inlineStr"><is><t>${cellValue.toString()}</t></is></c>`;
|
|
136
|
+
}
|
|
137
|
+
if (cellValue instanceof Date) {
|
|
138
|
+
const dateStr = Number.isNaN(cellValue.getTime()) ? 'Invalid Date' : cellValue.toISOString();
|
|
139
|
+
return `<c r="${cellRef}" t="inlineStr"><is><t>${escapeXml(dateStr)}</t></is></c>`;
|
|
140
|
+
}
|
|
141
|
+
let text;
|
|
142
|
+
if (typeof cellValue === 'object') {
|
|
143
|
+
try {
|
|
144
|
+
text = JSON.stringify(cellValue);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
text = String(cellValue);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
text = String(cellValue);
|
|
152
|
+
}
|
|
153
|
+
return `<c r="${cellRef}" t="inlineStr"><is><t xml:space="preserve">${escapeXml(text)}</t></is></c>`;
|
|
154
|
+
}
|
|
155
|
+
export function serializeGridExportDataToExcel(data, options) {
|
|
156
|
+
const includeHeader = options?.includeHeader ?? true;
|
|
157
|
+
let rawSheetName = (options?.sheetName || 'Sheet1').trim().replace(/[:\\/?*\[\]]/g, '');
|
|
158
|
+
if (!rawSheetName) {
|
|
159
|
+
rawSheetName = 'Sheet1';
|
|
160
|
+
}
|
|
161
|
+
else if (rawSheetName.length > 31) {
|
|
162
|
+
rawSheetName = rawSheetName.slice(0, 31);
|
|
163
|
+
}
|
|
164
|
+
const sheetRows = [];
|
|
165
|
+
let rowIndex = 1;
|
|
166
|
+
if (includeHeader) {
|
|
167
|
+
const headerCells = data.columns
|
|
168
|
+
.map((col, colIdx) => {
|
|
169
|
+
const cellRef = `${colIndexToExcelColumn(colIdx)}${rowIndex}`;
|
|
170
|
+
return formatCellValueAsXml(col.header, cellRef);
|
|
171
|
+
})
|
|
172
|
+
.join('');
|
|
173
|
+
sheetRows.push(`<row r="${rowIndex}">${headerCells}</row>`);
|
|
174
|
+
rowIndex++;
|
|
175
|
+
}
|
|
176
|
+
for (const row of data.rows) {
|
|
177
|
+
const rowCells = row.cells
|
|
178
|
+
.map((cell, colIdx) => {
|
|
179
|
+
const cellRef = `${colIndexToExcelColumn(colIdx)}${rowIndex}`;
|
|
180
|
+
return formatCellValueAsXml(cell, cellRef);
|
|
181
|
+
})
|
|
182
|
+
.join('');
|
|
183
|
+
sheetRows.push(`<row r="${rowIndex}">${rowCells}</row>`);
|
|
184
|
+
rowIndex++;
|
|
185
|
+
}
|
|
186
|
+
const contentTypesXml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' +
|
|
187
|
+
'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">' +
|
|
188
|
+
'<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>' +
|
|
189
|
+
'<Default Extension="xml" ContentType="application/xml"/>' +
|
|
190
|
+
'<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>' +
|
|
191
|
+
'<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>' +
|
|
192
|
+
'</Types>';
|
|
193
|
+
const rootRelsXml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' +
|
|
194
|
+
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
|
|
195
|
+
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>' +
|
|
196
|
+
'</Relationships>';
|
|
197
|
+
const workbookRelsXml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' +
|
|
198
|
+
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' +
|
|
199
|
+
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>' +
|
|
200
|
+
'</Relationships>';
|
|
201
|
+
const workbookXml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' +
|
|
202
|
+
'<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">' +
|
|
203
|
+
`<sheets><sheet name="${escapeXml(rawSheetName)}" sheetId="1" r:id="rId1"/></sheets>` +
|
|
204
|
+
'</workbook>';
|
|
205
|
+
const worksheetXml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' +
|
|
206
|
+
'<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">' +
|
|
207
|
+
`<sheetData>${sheetRows.join('')}</sheetData>` +
|
|
208
|
+
'</worksheet>';
|
|
209
|
+
return createZipArchive([
|
|
210
|
+
{ name: '[Content_Types].xml', data: contentTypesXml },
|
|
211
|
+
{ name: '_rels/.rels', data: rootRelsXml },
|
|
212
|
+
{ name: 'xl/_rels/workbook.xml.rels', data: workbookRelsXml },
|
|
213
|
+
{ name: 'xl/workbook.xml', data: workbookXml },
|
|
214
|
+
{ name: 'xl/worksheets/sheet1.xml', data: worksheetXml },
|
|
215
|
+
]);
|
|
216
|
+
}
|
|
217
|
+
export function downloadExcel(blobOrBytes, fileName = 'export.xlsx') {
|
|
218
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
let resolvedName = (fileName || '').trim();
|
|
222
|
+
if (!resolvedName) {
|
|
223
|
+
resolvedName = 'export.xlsx';
|
|
224
|
+
}
|
|
225
|
+
else if (!resolvedName.toLowerCase().endsWith('.xlsx')) {
|
|
226
|
+
resolvedName += '.xlsx';
|
|
227
|
+
}
|
|
228
|
+
const blob = blobOrBytes instanceof Blob
|
|
229
|
+
? blobOrBytes
|
|
230
|
+
: new Blob([blobOrBytes], {
|
|
231
|
+
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
232
|
+
});
|
|
233
|
+
if (typeof URL !== 'undefined' && typeof URL.createObjectURL === 'function') {
|
|
234
|
+
const url = URL.createObjectURL(blob);
|
|
235
|
+
const link = document.createElement('a');
|
|
236
|
+
link.href = url;
|
|
237
|
+
link.setAttribute('download', resolvedName);
|
|
238
|
+
link.style.display = 'none';
|
|
239
|
+
document.body.appendChild(link);
|
|
240
|
+
link.click();
|
|
241
|
+
document.body.removeChild(link);
|
|
242
|
+
if (typeof URL.revokeObjectURL === 'function') {
|
|
243
|
+
URL.revokeObjectURL(url);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
package/esm/utils/index.js
CHANGED
|
@@ -16,6 +16,7 @@ export * from './number';
|
|
|
16
16
|
export * from './buildHeaderMatrix';
|
|
17
17
|
export * from './getVisibleScrollableRowRange';
|
|
18
18
|
export * from './virtualScrollWindow';
|
|
19
|
+
export * from './treeData';
|
|
19
20
|
export * from './rowHeightMetrics';
|
|
20
21
|
export * from './cellEditState';
|
|
21
22
|
export * from './getCellSelectionAxisState';
|
|
@@ -26,3 +27,6 @@ export * from './rowReorder';
|
|
|
26
27
|
export * from './checkboxEditor';
|
|
27
28
|
export * from './columnVisibility';
|
|
28
29
|
export * from './checkedIndexes';
|
|
30
|
+
export * from './exportData';
|
|
31
|
+
export * from './exportCsv';
|
|
32
|
+
export * from './exportExcel';
|