react-merge-table
Version:
React component for auto-merging rowspan/colspan in HTML tables
226 lines (218 loc) • 6.31 kB
JavaScript
// src/components/AutoMergeTable.tsx
import { jsx } from "react/jsx-runtime";
var AutoMergeTable = ({ children, className, style, ...rest }) => {
const tableStyle = {
borderCollapse: "collapse",
width: "100%"
};
const mergedStyle = { ...tableStyle, ...style };
return /* @__PURE__ */ jsx("table", { className, style: mergedStyle, ...rest, children });
};
// src/utils/normalizeHeaderCell.ts
function normalizeHeaderCell(input, index) {
if (typeof input === "string" || typeof input === "number") {
return { key: `$HEADER-${index}`, label: input };
}
return input;
}
// src/components/TableHeader.tsx
import { jsx as jsx2 } from "react/jsx-runtime";
var TableHeader = ({ headers, defaultStyle = true, style, ...rest }) => {
const thStyle = {
border: "1px solid #ccc",
padding: "8px",
textAlign: "center",
fontWeight: "bold"
};
const mergedStyle = defaultStyle ? { ...thStyle, ...style } : style;
return /* @__PURE__ */ jsx2("thead", { ...rest, children: /* @__PURE__ */ jsx2("tr", { children: headers.map((header, index) => {
const normalized = normalizeHeaderCell(header, index);
return /* @__PURE__ */ jsx2("th", { style: mergedStyle, children: normalized.label }, normalized.key);
}) }) });
};
// src/components/TableCell.tsx
import { isValidElement, cloneElement } from "react";
import { jsx as jsx3 } from "react/jsx-runtime";
var TableCell = ({
cell,
rowIndex,
colIndex,
columnRenderers,
defaultStyle = true,
style,
...rest
}) => {
const renderer = columnRenderers?.[cell.colIndex];
const renderResult = renderer?.(cell);
const isRawElement = isValidElement(renderResult) && (renderResult.type === "td" || renderResult.type === "th");
if (isRawElement) {
const element = renderResult;
return cloneElement(element, {
rowSpan: cell.rowspan,
colSpan: cell.colspan,
...element.props
});
}
const tdStyle = {
border: "1px solid #ccc",
padding: "8px",
textAlign: "center",
verticalAlign: "middle"
};
const mergedStyle = defaultStyle ? { ...tdStyle, ...style } : style;
return /* @__PURE__ */ jsx3(
"td",
{
rowSpan: cell.rowspan,
colSpan: cell.colspan,
style: mergedStyle,
...rest,
children: renderResult ?? renderContent(cell.content)
}
);
};
function renderContent(value) {
if (Array.isArray(value)) {
console.log(value);
return /* @__PURE__ */ jsx3("div", { children: value.map((v) => /* @__PURE__ */ jsx3("p", { children: v.label }, v.key)) });
}
return /* @__PURE__ */ jsx3("span", { children: value.label });
}
// src/components/TableRow.tsx
import { jsx as jsx4 } from "react/jsx-runtime";
var TableRow = ({ row, rowIndex, columnRenderers, defaultStyle = true, ...rest }) => {
return /* @__PURE__ */ jsx4("tr", { ...rest, children: row.map(
(cell, colIndex) => cell.render ? /* @__PURE__ */ jsx4(
TableCell,
{
cell,
rowIndex,
colIndex,
defaultStyle,
columnRenderers
},
colIndex
) : null
) });
};
// src/utils/normalizeCellContent.ts
var globalKeyId = 0;
function toKeyed(label) {
return {
key: `$CELL-${globalKeyId++}`,
label
};
}
function isKeyedValueArray(value) {
return value.length > 0 && typeof value[0] === "object" && value[0] !== null && "key" in value[0] && "label" in value[0];
}
function isKeyedValue(value) {
return typeof value === "object" && value !== null && "key" in value && "label" in value;
}
function interpretLabel(label) {
if (label === "$$")
return "$";
if (label === "~~")
return "~";
return label;
}
function normalizeCellContent(input) {
if (Array.isArray(input)) {
if (isKeyedValueArray(input)) {
return input.map(({ key, label }) => ({
key,
label: interpretLabel(label)
}));
} else {
return input.map((v) => toKeyed(interpretLabel(v)));
}
}
if (isKeyedValue(input)) {
return [{
key: input.key,
label: interpretLabel(input.label)
}];
}
return [toKeyed(interpretLabel(input))];
}
// src/utils/parseRowsToMatrix.ts
function parseRowsToMatrix(rows) {
const raw = rows.map(
(row) => row.data.map(normalizeCellContent)
);
const matrix = raw.map(
(row, rowIndex) => row.map((contents, colIndex) => ({
value: rows[rowIndex].data[colIndex],
// 원래의 셀 값
content: contents[0],
// 대표 값
contents,
// 전체 값 배열
hasMultiple: contents.length > 1,
// 다중 값 여부
rowspan: 1,
colspan: 1,
render: true,
rowIndex,
colIndex
}))
);
for (let rowIndex = 0; rowIndex < matrix.length; rowIndex++) {
for (let colIndex = 0; colIndex < matrix[rowIndex].length; colIndex++) {
const cell = matrix[rowIndex][colIndex];
if (!cell.render)
continue;
const first = cell.content.label;
if (first === "$" && rowIndex > 0) {
for (let i = rowIndex - 1; i >= 0; i--) {
const target = matrix[i][colIndex];
const tFirst = target.content.label;
if (target.render && tFirst !== "$") {
target.rowspan++;
cell.render = false;
break;
}
}
} else if (first === "~" && colIndex > 0) {
for (let j = colIndex - 1; j >= 0; j--) {
const target = matrix[rowIndex][j];
const tFirst = target.content.label;
if (target.render && tFirst !== "~") {
target.colspan++;
cell.render = false;
break;
}
}
}
}
}
return matrix;
}
// src/components/TableBody.tsx
import { jsx as jsx5 } from "react/jsx-runtime";
var TableBody = ({
rows,
columnRenderers,
defaultStyle = true,
...rest
}) => {
const normalizedRows = rows.map(
(row, i) => Array.isArray(row) ? { key: `row-${i}`, data: row } : row
);
const matrix = parseRowsToMatrix(normalizedRows);
return /* @__PURE__ */ jsx5("tbody", { ...rest, children: matrix.map((row, rowIndex) => /* @__PURE__ */ jsx5(
TableRow,
{
row,
rowIndex,
defaultStyle,
columnRenderers
},
normalizedRows[rowIndex].key
)) });
};
export {
AutoMergeTable as MergeTable,
TableBody,
TableHeader
};