Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 复制支持html格式 #1647

Merged
merged 10 commits into from
Aug 5, 2022
9 changes: 9 additions & 0 deletions packages/s2-core/__tests__/unit/utils/export/copy-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from '@/common/constant/interaction';
import {
convertString,
CopyMIMEType,
getCopyData,
getSelectedData,
} from '@/utils/export/copy';
Expand Down Expand Up @@ -687,4 +688,12 @@ describe('List Table getCopyData', () => {
expect(data.split('\n').length).toBe(2);
expect(data.split('\t').length).toBe(5);
});

it('should copy in multiple format', () => {
const data = getCopyData(s2, CopyType.ROW, [
CopyMIMEType.PLAIN,
CopyMIMEType.HTML,
]) as string[];
expect(data.length).toBe(2);
});
});
188 changes: 141 additions & 47 deletions packages/s2-core/src/utils/export/copy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { fill, forEach, map, zip } from 'lodash';
import { map, zip, escape } from 'lodash';
import {
type CellMeta,
CellTypes,
Expand Down Expand Up @@ -111,17 +111,82 @@ const getHeaderList = (headerId: string) => {
return headerList;
};

// 把 string[][] 矩阵转换成字符串格式
const transformDataMatrixToStr = (dataMatrix: string[][]) => {
return map(dataMatrix, (line) => line.join(newTab)).join(newLine);
type MatrixTransformer = (data: string[][]) => CopyableItem;

export enum CopyMIMEType {
PLAIN = 'text/plain',
HTML = 'text/html',
}

export type CopyableItem = {
type: CopyMIMEType;
content: string;
};

export type Copyable = CopyableItem | CopyableItem[];

function pickDataFromCopyable(
copyable: Copyable,
type: CopyMIMEType[],
): string[];
function pickDataFromCopyable(copyable: Copyable, type: CopyMIMEType): string;
function pickDataFromCopyable(
copyable: Copyable,
type: CopyMIMEType | CopyMIMEType[],
): string | string[];
function pickDataFromCopyable(
copyable: Copyable,
type: CopyMIMEType[] | CopyMIMEType = CopyMIMEType.PLAIN,
): string[] | string {
if (Array.isArray(type)) {
return ([].concat(copyable) as CopyableItem[])
.filter((item) => type.includes(item.type))
.map((item) => item.content);
}
return (
([].concat(copyable) as CopyableItem[])
.filter((item) => item.type === type)
.map((item) => item.content)[0] || ''
);
}

// 把 string[][] 矩阵转换成 CopyableItem
const matrixPlainTextTransformer: MatrixTransformer = (dataMatrix) => {
return {
type: CopyMIMEType.PLAIN,
content: map(dataMatrix, (line) => line.join(newTab)).join(newLine),
};
};

// 把 string[][] 矩阵转换成 CopyableItem
const matrixHtmlTransformer: MatrixTransformer = (dataMatrix) => {
function createTableData(data: string[], tagName: string) {
return data
.map((cell) => `<${tagName}>${escape(cell)}</${tagName}>`)
.join('');
}

function createBody(data: string[][], tagName: string) {
return data
.map((row) => `<${tagName}>${createTableData(row, 'td')}</${tagName}>`)
.join('');
}

return {
type: CopyMIMEType.HTML,
content: `<meta charset="utf-8"><table><tbody>${createBody(
dataMatrix,
'tr',
)}</tbody></table>`,
};
};

// 生成矩阵:https://gw.alipayobjects.com/zos/antfincdn/bxBVt0nXx/a182c1d4-81bf-469f-b868-8b2e29acfc5f.png
const assembleMatrix = (
rowMatrix: string[][],
colMatrix: string[][],
dataMatrix: string[][],
) => {
): Copyable => {
const rowWidth = rowMatrix[0]?.length ?? 0;
const colHeight = colMatrix?.length ?? 0;
const dataWidth = dataMatrix[0]?.length ?? 0;
Expand Down Expand Up @@ -154,21 +219,19 @@ const assembleMatrix = (
});
}) as string[][];

return transformDataMatrixToStr(matrix);
return [matrixPlainTextTransformer(matrix), matrixHtmlTransformer(matrix)];
};

export const processCopyData = (
displayData: DataType[],
cells: CellMeta[][],
spreadsheet: SpreadSheet,
): string => {
const getRowString = (pre: string, cur: CellMeta) =>
pre +
(cur ? convertString(format(cur, displayData, spreadsheet)) : '') +
newTab;
const getColString = (pre: string, cur: CellMeta[]) =>
pre + cur.reduce(getRowString, '').slice(0, -1) + newLine;
return cells.reduce(getColString, '').slice(0, -2);
): Copyable => {
const matrix = cells.map((cols) =>
cols.map((item) => convertString(format(item, displayData, spreadsheet))),
);

return [matrixPlainTextTransformer(matrix), matrixHtmlTransformer(matrix)];
};

/**
Expand Down Expand Up @@ -210,17 +273,19 @@ const processTableColSelected = (
displayData: DataType[],
spreadsheet: SpreadSheet,
selectedCols: CellMeta[],
) => {
): Copyable => {
const selectedFiled = selectedCols.length
? selectedCols.map((e) => getColNodeField(spreadsheet, e.id))
: spreadsheet.dataCfg.fields.columns;
return displayData
.map((row) => {
return selectedFiled
.map((filed) => convertString(row[filed]))
.join(newTab);
})
.join(newLine);

const dataMatrix = displayData.map((row) => {
return selectedFiled.map((filed) => convertString(row[filed]));
});

return [
matrixPlainTextTransformer(dataMatrix),
matrixHtmlTransformer(dataMatrix),
];
};

const getDataMatrix = (
Expand Down Expand Up @@ -251,16 +316,19 @@ const getPivotWithoutHeaderCopyData = (
spreadsheet: SpreadSheet,
leafRows: Node[],
leafCols: Node[],
) => {
): Copyable => {
const dataMatrix = getDataMatrix(leafRows, leafCols, spreadsheet);
return transformDataMatrixToStr(dataMatrix);
return [
matrixPlainTextTransformer(dataMatrix),
matrixHtmlTransformer(dataMatrix),
];
};

const getPivotWithHeaderCopyData = (
spreadsheet: SpreadSheet,
leafRowNodes: Node[],
leafColNodes: Node[],
) => {
): Copyable => {
const rowMatrix = map(leafRowNodes, (n) => getHeaderList(n.id));
const colMatrix = zip(...map(leafColNodes, (n) => getHeaderList(n.id)));
const dataMatrix = getDataMatrix(leafRowNodes, leafColNodes, spreadsheet);
Expand All @@ -271,7 +339,7 @@ function getPivotCopyData(
spreadsheet: SpreadSheet,
allRowLeafNodes: Node[],
colNodes: Node[],
) {
): Copyable {
const { copyWithHeader } = spreadsheet.options.interaction;

return copyWithHeader
Expand All @@ -282,7 +350,7 @@ function getPivotCopyData(
const processPivotColSelected = (
spreadsheet: SpreadSheet,
selectedCols: CellMeta[],
) => {
): Copyable => {
const allRowLeafNodes = spreadsheet
.getRowNodes()
.filter((node) => node.isLeaf);
Expand All @@ -303,7 +371,7 @@ const processColSelected = (
displayData: DataType[],
spreadsheet: SpreadSheet,
selectedCols: CellMeta[],
) => {
): Copyable => {
if (spreadsheet.isPivotMode()) {
return processPivotColSelected(spreadsheet, selectedCols);
}
Expand All @@ -313,22 +381,18 @@ const processColSelected = (
const processTableRowSelected = (
displayData: DataType[],
selectedRows: CellMeta[],
) => {
): Copyable => {
const selectedIndex = selectedRows.map((e) => e.rowIndex);
return displayData
const matrix = displayData
.filter((e, i) => selectedIndex.includes(i))
.map((e) =>
Object.keys(e)
.map((key) => convertString(e[key]))
.join(newTab),
)
.join(newLine);
.map((e) => Object.keys(e).map((key) => convertString(e[key])));
return [matrixPlainTextTransformer(matrix), matrixHtmlTransformer(matrix)];
};

const processPivotRowSelected = (
spreadsheet: SpreadSheet,
selectedRows: CellMeta[],
) => {
): Copyable => {
const allRowLeafNodes = spreadsheet
.getRowNodes()
.filter((node) => node.isLeaf);
Expand All @@ -346,18 +410,42 @@ const processRowSelected = (
displayData: DataType[],
spreadsheet: SpreadSheet,
selectedRows: CellMeta[],
) => {
): Copyable => {
if (spreadsheet.isPivotMode()) {
return processPivotRowSelected(spreadsheet, selectedRows);
}
return processTableRowSelected(displayData, selectedRows);
};

export const getCopyData = (spreadsheet: SpreadSheet, copyType: CopyType) => {
YardWill marked this conversation as resolved.
Show resolved Hide resolved
export function getCopyData(
spreadsheet: SpreadSheet,
copyType: CopyType,
copyFormat: CopyMIMEType,
): string;

export function getCopyData(
spreadsheet: SpreadSheet,
copyType: CopyType,
copyFormat: CopyMIMEType[],
): string[];

export function getCopyData(
spreadsheet: SpreadSheet,
copyType: CopyType,
): string;

export function getCopyData(
spreadsheet: SpreadSheet,
copyType: CopyType,
copyFormat: CopyMIMEType[] | CopyMIMEType = CopyMIMEType.PLAIN,
): string[] | string {
const displayData = spreadsheet.dataSet.getDisplayDataSet();
const cells = spreadsheet.interaction.getState().cells || [];
if (copyType === CopyType.ALL) {
return processColSelected(displayData, spreadsheet, []);
return pickDataFromCopyable(
processColSelected(displayData, spreadsheet, []),
copyFormat,
);
}
if (copyType === CopyType.COL) {
const colIndexes = cells.reduce<number[]>((pre, cur) => {
Expand All @@ -374,7 +462,10 @@ export const getCopyData = (spreadsheet: SpreadSheet, copyType: CopyType) => {
rowIndex: node.rowIndex,
type: CellTypes.COL_CELL,
}));
return processColSelected(displayData, spreadsheet, colNodes);
return pickDataFromCopyable(
processColSelected(displayData, spreadsheet, colNodes),
copyFormat,
);
}
if (copyType === CopyType.ROW) {
const rowIndexes = cells.reduce<number[]>((pre, cur) => {
Expand All @@ -391,9 +482,12 @@ export const getCopyData = (spreadsheet: SpreadSheet, copyType: CopyType) => {
type: CellTypes.ROW_CELL,
};
});
return processRowSelected(displayData, spreadsheet, rowNodes);
return pickDataFromCopyable(
processRowSelected(displayData, spreadsheet, rowNodes),
copyFormat,
);
}
};
}

/**
* 生成包含行列头的导出数据。查看👇🏻图效果展示,更容易理解代码:
Expand All @@ -406,7 +500,7 @@ const getDataWithHeaderMatrix = (
cellMetaMatrix: CellMeta[][],
displayData: DataType[],
spreadsheet: SpreadSheet,
) => {
): Copyable => {
const colMatrix = zip(
...map(cellMetaMatrix[0], (cellMeta) => {
const colId = cellMeta.id.split(EMPTY_PLACEHOLDER)?.[1] ?? '';
Expand All @@ -426,12 +520,12 @@ const getDataWithHeaderMatrix = (
return assembleMatrix(rowMatrix, colMatrix, dataMatrix);
};

export const getSelectedData = (spreadsheet: SpreadSheet) => {
export const getSelectedData = (spreadsheet: SpreadSheet): string => {
const interaction = spreadsheet.interaction;
const { copyWithHeader } = spreadsheet.options.interaction;

const cells = interaction.getState().cells || [];
let data: string;
let data: Copyable;
const selectedCols = cells.filter(({ type }) => type === CellTypes.COL_CELL);
const selectedRows = cells.filter(({ type }) => type === CellTypes.ROW_CELL);

Expand Down Expand Up @@ -468,5 +562,5 @@ export const getSelectedData = (spreadsheet: SpreadSheet) => {
if (data) {
copyToClipboard(data);
}
return data;
return pickDataFromCopyable(data, CopyMIMEType.PLAIN);
};
Loading