Skip to content

Commit 00eb6d4

Browse files
committed
refactor: add copyTypes and remove showCopyWithHeader
1 parent 798e662 commit 00eb6d4

File tree

5 files changed

+252
-72
lines changed

5 files changed

+252
-72
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,82 @@
11
import React from 'react';
2-
import SpreadSheet from '../index';
3-
import { render } from '@testing-library/react';
2+
import { fireEvent, render } from '@testing-library/react';
43
import '@testing-library/jest-dom/extend-expect';
54

5+
import SpreadSheet from '../index';
6+
7+
// mock the CopyUtils
8+
jest.mock('../../utils/copy', () => {
9+
class CopyUtilsMock {
10+
copy = jest.fn();
11+
}
12+
return CopyUtilsMock;
13+
});
14+
615
describe('test spreadSheet ', () => {
16+
const columns = ['name', 'gender', 'age', 'address'];
17+
const data = [
18+
['zhangsan', 'male', '20', 'xihu'],
19+
['lisi', 'male', '18', 'yuhang'],
20+
];
721
test('should render SpreadSheet custom className', () => {
8-
const { container } = render(
22+
const { container, getByText, unmount } = render(
23+
<SpreadSheet columns={columns} data={data} className="testSpreadSheet" />
24+
);
25+
expect(container.firstChild).toHaveClass('testSpreadSheet');
26+
expect(getByText('zhangsan')).toBeInTheDocument();
27+
unmount();
28+
});
29+
30+
test('renders without data', () => {
31+
const { getByText, unmount } = render(<SpreadSheet data={[]} columns={columns} />);
32+
expect(getByText('暂无数据')).toBeInTheDocument();
33+
unmount();
34+
});
35+
36+
test('copy value without header', () => {
37+
const { getByText, unmount } = render(<SpreadSheet data={data} columns={columns} />);
38+
const cell = getByText('zhangsan');
39+
fireEvent.contextMenu(cell);
40+
const copyBtn = getByText('复制值');
41+
expect(copyBtn).toBeInTheDocument();
42+
fireEvent.click(copyBtn);
43+
unmount();
44+
});
45+
46+
test('copy value with header', () => {
47+
const { getByText, unmount } = render(
48+
<SpreadSheet data={data} columns={columns} options={{ copyTypes: ['copyData'] }} />
49+
);
50+
const cell = getByText('zhangsan');
51+
fireEvent.contextMenu(cell);
52+
const copyBtn = getByText('复制值');
53+
expect(copyBtn).toBeInTheDocument();
54+
fireEvent.click(copyBtn);
55+
unmount();
56+
});
57+
58+
test('copy value with header', () => {
59+
const { getByText, unmount } = render(
960
<SpreadSheet
10-
columns={['name', 'gender', 'age', 'address']}
11-
data={[
12-
['zhangsan', 'male', '20', 'xihu'],
13-
['lisi', 'male', '18', 'yuhang'],
14-
]}
15-
className="testSpreadSheet"
61+
data={data}
62+
columns={columns}
63+
options={{ copyTypes: ['copyData', 'copyHeadersAndData'] }}
1664
/>
1765
);
18-
expect(container.firstChild).toHaveClass('testSpreadSheet');
66+
const cell = getByText('zhangsan');
67+
fireEvent.contextMenu(cell);
68+
const copyBtn = getByText('复制列名和值');
69+
expect(copyBtn).toBeInTheDocument();
70+
fireEvent.click(copyBtn);
71+
unmount();
72+
});
73+
74+
test('should call componentDidUpdate when props are updated', () => {
75+
const rerenderData = [['wangwu', 'male', '18', 'yuhang']];
76+
jest.useFakeTimers();
77+
const { rerender, getByText } = render(<SpreadSheet data={data} columns={columns} />);
78+
rerender(<SpreadSheet data={rerenderData} columns={columns} />);
79+
jest.runAllTimers();
80+
expect(getByText('wangwu')).toBeInTheDocument();
1981
});
2082
});

src/components/spreadSheet/index.tsx

+98-49
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
import React from 'react';
2-
3-
import CopyUtils from '../utils/copy';
4-
import { HotTable } from '@handsontable/react';
52
import type { HotTableProps } from '@handsontable/react';
3+
import { HotTable } from '@handsontable/react';
64
import classNames from 'classnames';
7-
import 'handsontable/dist/handsontable.full.css';
85
import 'handsontable/languages/zh-CN.js';
96

7+
import CopyUtils from '../utils/copy';
8+
import 'handsontable/dist/handsontable.full.css';
9+
1010
type IOptions = HotTableProps & {
11-
/** 是否展示复制值以及列名 */
12-
showCopyWithHeader?: boolean;
11+
// 右击右键菜单中展示的选项 复制值/复制列名/复制列名和值 按钮 */
12+
copyTypes?: Array<'copyData' | 'copyHeaders' | 'copyHeadersAndData'>;
1313
};
1414

15-
export interface SpreadSheetProps {
15+
export interface ISpreadSheetProps {
1616
data: Array<Array<string | null | number>>;
1717
columns: any;
1818
className?: string;
@@ -27,7 +27,7 @@ export interface SpreadSheetProps {
2727
hotTableInstanceRef?: (instance: any) => void;
2828
}
2929

30-
class SpreadSheet extends React.PureComponent<SpreadSheetProps, any> {
30+
class SpreadSheet extends React.PureComponent<ISpreadSheetProps, any> {
3131
tableRef: any = React.createRef();
3232
copyUtils = new CopyUtils();
3333
_renderTimer: any;
@@ -54,7 +54,7 @@ class SpreadSheet extends React.PureComponent<SpreadSheetProps, any> {
5454
componentWillUnmount() {
5555
this.removeRenderClock();
5656
}
57-
getData() {
57+
getShowData() {
5858
const { data, columns = [] } = this.props;
5959
let showData = data;
6060
if (!showData || !showData.length) {
@@ -78,63 +78,110 @@ class SpreadSheet extends React.PureComponent<SpreadSheetProps, any> {
7878
}
7979
return null;
8080
}
81-
beforeCopy(arr: any, _arr2?: any) {
82-
/**
83-
* 去除格式化
84-
*/
81+
82+
/**
83+
* 去除格式化
84+
*/
85+
beforeCopy(arr: Array<Array<any>>) {
8586
const value = arr
8687
.map((row: any) => {
8788
return row.join('\t');
8889
})
8990
.join('\n');
91+
9092
this.copyUtils.copy(value);
9193
return false;
9294
}
9395
getContextMenu() {
9496
const that = this;
9597
const { columns = [], options } = this.props;
96-
const items = {
97-
copy: {
98-
name: '复制',
99-
callback: function (_key) {
100-
const indexArr = this.getSelected();
101-
// eslint-disable-next-line prefer-spread
102-
const copyDataArr = this.getData.apply(this, indexArr[0]);
103-
that.beforeCopy(copyDataArr);
104-
},
105-
},
98+
const { copyTypes = [] } = options || {};
99+
100+
// 获取值
101+
const getCopyData = (_that) => {
102+
// _that 调用的是 handsontable 的方法(在 handsontable.d.ts), this/that 调用的是当前文件的方法
103+
const selectedIndexArr = _that.getSelected();
104+
let dataArr = [];
105+
106+
if (Array.isArray(selectedIndexArr)) {
107+
selectedIndexArr.forEach((arr, index) => {
108+
const [r, c, r2, c2] = arr || [];
109+
const colData: [] = _that.getData(r, c, r2, c2) || [];
110+
if (index === 0) {
111+
dataArr.push(...colData);
112+
} else {
113+
dataArr = dataArr.map((item: any[], index: number) => {
114+
return item.concat(colData[index]);
115+
});
116+
}
117+
});
118+
}
119+
return dataArr;
106120
};
107-
if (options?.showCopyWithHeader) {
108-
const copyWithHeaderItem = {
109-
name: '复制值以及列名',
110-
callback: function (_key, selection) {
111-
const indexArr = this.getSelected();
112-
// eslint-disable-next-line prefer-spread
113-
let copyDataArr = this.getData.apply(this, indexArr[0]);
114-
const columnStart = selection?.[0]?.start?.col;
115-
const columnEnd = selection?.[0]?.end?.col;
116-
let columnArr;
121+
// 获取列名
122+
const getCopyHeaders = (selection) => {
123+
// _that 调用的是 handsontable 的方法(在 handsontable.d.ts), this/that 调用的是当前文件的方法
124+
let headerArr = [];
125+
if (Array.isArray(selection)) {
126+
selection.forEach((it) => {
127+
const columnStart = it.start?.col;
128+
const columnEnd = it.end?.col;
117129
if (columnStart !== undefined && columnEnd !== undefined) {
118-
columnArr = columns.slice(columnStart, columnEnd + 1);
130+
headerArr = headerArr.concat(columns.slice(columnStart, columnEnd + 1));
119131
}
120-
if (columnArr) {
121-
copyDataArr = [columnArr, ...copyDataArr];
122-
}
123-
that.beforeCopy(copyDataArr);
124-
},
125-
};
126-
// 目前版本不支持 copy_with_column_headers 暂时用 cut 代替,以达到与copy类似的表现
127-
items['cut'] = copyWithHeaderItem;
132+
});
133+
}
134+
return headerArr;
135+
};
136+
137+
const copyDataItem = {
138+
name: '复制值',
139+
callback: function (_key) {
140+
const copyDataArr = getCopyData(this);
141+
that.beforeCopy(copyDataArr);
142+
},
143+
};
144+
const copyHeadersItem = {
145+
name: '复制列名',
146+
callback: function (_key, selection) {
147+
const copyHeaders = getCopyHeaders(selection);
148+
that.beforeCopy([copyHeaders]);
149+
},
150+
};
151+
const copyHeadersAndDataItem = {
152+
name: '复制列名和值',
153+
callback: function (_key, selection) {
154+
const copyDataArr = getCopyData(this);
155+
const copyHeaders = getCopyHeaders(selection);
156+
that.beforeCopy([copyHeaders, ...copyDataArr]);
157+
},
158+
};
159+
160+
// 目前 items 在 https://github.com/handsontable/handsontable/blob/6.2.2/handsontable.d.ts#L779,自定义方法也可以被执行
161+
const items = {};
162+
if (Array.isArray(copyTypes) && copyTypes?.length) {
163+
// 复制值
164+
if (copyTypes.includes('copyData')) {
165+
items['copyData'] = copyDataItem;
166+
}
167+
// 复制列名
168+
if (copyTypes.includes('copyHeaders')) {
169+
items['copyHeaders'] = copyHeadersItem;
170+
}
171+
// 复制列名和值
172+
if (copyTypes.includes('copyHeadersAndData')) {
173+
items['copyHeadersAndData'] = copyHeadersAndDataItem;
174+
}
175+
} else {
176+
items['copyData'] = copyDataItem;
128177
}
129-
return {
130-
items,
131-
} as any;
178+
179+
return { items } as any;
132180
}
133181
render() {
134182
const { columns = [], className = '', options, columnTypes = [] } = this.props;
135-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
136-
const { trimWhitespace = true, showCopyWithHeader, ...restOptions } = options || {};
137-
const showData = this.getData();
183+
const { trimWhitespace = true, ...restOptions } = options || {};
184+
const showData = this.getShowData();
138185
// 空数组情况,不显示colHeaders,否则colHeaders默认会按照 A、B...显示
139186
// 具体可见 https://handsontable.com/docs/7.1.1/Options.html#colHeaders
140187
let isShowColHeaders = false;
@@ -153,7 +200,9 @@ class SpreadSheet extends React.PureComponent<SpreadSheetProps, any> {
153200
if (!isShowColHeaders) return false;
154201
// handsontable 不支持 renderCustomHeader,所以只能用 html string 实现 tooltip
155202
const fieldTypeStr = columnTypes?.[index]?.type;
156-
const title = fieldTypeStr ? `${columns?.[index]}: ${fieldTypeStr}` : columns?.[index];
203+
const title = fieldTypeStr
204+
? `${columns?.[index]}: ${fieldTypeStr}`
205+
: columns?.[index];
157206
return `<span title="${title}">${title}</span>`;
158207
}}
159208
data={showData}

src/components/spreadSheet/style.scss

+11
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
11
.dtc-handsontable-no-border {
2+
width: 100%;
23
.handsontable {
34
thead tr:first-child th {
45
border-top: 0;
56
}
67
th:first-child {
78
border-left: 0;
89
}
10+
th:last-child {
11+
border-right: 0;
12+
}
13+
td:last-child {
14+
border-right: 0;
15+
}
16+
// 最后一行
17+
tr:last-child th, tr:last-child td {
18+
border-bottom: 0;
19+
}
920
}
1021
}

src/components/utils/copy.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,10 @@ export default class CopyUtils {
6060
let succeeded;
6161

6262
try {
63-
succeeded = document.execCommand('copy');
63+
// 浏览器兼容性处理,当前语法已废弃,延迟处理可以保证复制成功
64+
setTimeout(() => {
65+
succeeded = document.execCommand('copy');
66+
});
6467
} catch (err) {
6568
succeeded = false;
6669
}

0 commit comments

Comments
 (0)