diff --git a/src/components/spreadSheet/__tests__/index.test.tsx b/src/components/spreadSheet/__tests__/index.test.tsx index dcb78daaa..83f304ab9 100644 --- a/src/components/spreadSheet/__tests__/index.test.tsx +++ b/src/components/spreadSheet/__tests__/index.test.tsx @@ -1,20 +1,82 @@ import React from 'react'; -import SpreadSheet from '../index'; -import { render } from '@testing-library/react'; +import { fireEvent, render } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; +import SpreadSheet from '../index'; + +// mock the CopyUtils +jest.mock('../../utils/copy', () => { + class CopyUtilsMock { + copy = jest.fn(); + } + return CopyUtilsMock; +}); + describe('test spreadSheet ', () => { + const columns = ['name', 'gender', 'age', 'address']; + const data = [ + ['zhangsan', 'male', '20', 'xihu'], + ['lisi', 'male', '18', 'yuhang'], + ]; test('should render SpreadSheet custom className', () => { - const { container } = render( + const { container, getByText, unmount } = render( + + ); + expect(container.firstChild).toHaveClass('testSpreadSheet'); + expect(getByText('zhangsan')).toBeInTheDocument(); + unmount(); + }); + + test('renders without data', () => { + const { getByText, unmount } = render(); + expect(getByText('暂无数据')).toBeInTheDocument(); + unmount(); + }); + + test('copy value without header', () => { + const { getByText, unmount } = render(); + const cell = getByText('zhangsan'); + fireEvent.contextMenu(cell); + const copyBtn = getByText('复制值'); + expect(copyBtn).toBeInTheDocument(); + fireEvent.click(copyBtn); + unmount(); + }); + + test('copy value with header', () => { + const { getByText, unmount } = render( + + ); + const cell = getByText('zhangsan'); + fireEvent.contextMenu(cell); + const copyBtn = getByText('复制值'); + expect(copyBtn).toBeInTheDocument(); + fireEvent.click(copyBtn); + unmount(); + }); + + test('copy value with header', () => { + const { getByText, unmount } = render( ); - expect(container.firstChild).toHaveClass('testSpreadSheet'); + const cell = getByText('zhangsan'); + fireEvent.contextMenu(cell); + const copyBtn = getByText('复制列名和值'); + expect(copyBtn).toBeInTheDocument(); + fireEvent.click(copyBtn); + unmount(); + }); + + test('should call componentDidUpdate when props are updated', () => { + const rerenderData = [['wangwu', 'male', '18', 'yuhang']]; + jest.useFakeTimers(); + const { rerender, getByText } = render(); + rerender(); + jest.runAllTimers(); + expect(getByText('wangwu')).toBeInTheDocument(); }); }); diff --git a/src/components/spreadSheet/index.tsx b/src/components/spreadSheet/index.tsx index ac8569b03..3d6622123 100644 --- a/src/components/spreadSheet/index.tsx +++ b/src/components/spreadSheet/index.tsx @@ -1,18 +1,18 @@ import React from 'react'; - -import CopyUtils from '../utils/copy'; -import { HotTable } from '@handsontable/react'; import type { HotTableProps } from '@handsontable/react'; +import { HotTable } from '@handsontable/react'; import classNames from 'classnames'; -import 'handsontable/dist/handsontable.full.css'; import 'handsontable/languages/zh-CN.js'; +import CopyUtils from '../utils/copy'; +import 'handsontable/dist/handsontable.full.css'; + type IOptions = HotTableProps & { - /** 是否展示复制值以及列名 */ - showCopyWithHeader?: boolean; + // 右击右键菜单中展示的选项 复制值/复制列名/复制列名和值 按钮 */ + copyTypes?: Array<'copyData' | 'copyHeaders' | 'copyHeadersAndData'>; }; -export interface SpreadSheetProps { +export interface ISpreadSheetProps { data: Array>; columns: any; className?: string; @@ -27,7 +27,7 @@ export interface SpreadSheetProps { hotTableInstanceRef?: (instance: any) => void; } -class SpreadSheet extends React.PureComponent { +class SpreadSheet extends React.PureComponent { tableRef: any = React.createRef(); copyUtils = new CopyUtils(); _renderTimer: any; @@ -54,7 +54,7 @@ class SpreadSheet extends React.PureComponent { componentWillUnmount() { this.removeRenderClock(); } - getData() { + getShowData() { const { data, columns = [] } = this.props; let showData = data; if (!showData || !showData.length) { @@ -78,63 +78,110 @@ class SpreadSheet extends React.PureComponent { } return null; } - beforeCopy(arr: any, _arr2?: any) { - /** - * 去除格式化 - */ + + /** + * 去除格式化 + */ + beforeCopy(arr: Array>) { const value = arr .map((row: any) => { return row.join('\t'); }) .join('\n'); + this.copyUtils.copy(value); return false; } getContextMenu() { const that = this; const { columns = [], options } = this.props; - const items = { - copy: { - name: '复制', - callback: function (_key) { - const indexArr = this.getSelected(); - // eslint-disable-next-line prefer-spread - const copyDataArr = this.getData.apply(this, indexArr[0]); - that.beforeCopy(copyDataArr); - }, - }, + const { copyTypes = [] } = options || {}; + + // 获取值 + const getCopyData = (_that) => { + // _that 调用的是 handsontable 的方法(在 handsontable.d.ts), this/that 调用的是当前文件的方法 + const selectedIndexArr = _that.getSelected(); + let dataArr = []; + + if (Array.isArray(selectedIndexArr)) { + selectedIndexArr.forEach((arr, index) => { + const [r, c, r2, c2] = arr || []; + const colData: [] = _that.getData(r, c, r2, c2) || []; + if (index === 0) { + dataArr.push(...colData); + } else { + dataArr = dataArr.map((item: any[], index: number) => { + return item.concat(colData[index]); + }); + } + }); + } + return dataArr; }; - if (options?.showCopyWithHeader) { - const copyWithHeaderItem = { - name: '复制值以及列名', - callback: function (_key, selection) { - const indexArr = this.getSelected(); - // eslint-disable-next-line prefer-spread - let copyDataArr = this.getData.apply(this, indexArr[0]); - const columnStart = selection?.[0]?.start?.col; - const columnEnd = selection?.[0]?.end?.col; - let columnArr; + // 获取列名 + const getCopyHeaders = (selection) => { + // _that 调用的是 handsontable 的方法(在 handsontable.d.ts), this/that 调用的是当前文件的方法 + let headerArr = []; + if (Array.isArray(selection)) { + selection.forEach((it) => { + const columnStart = it.start?.col; + const columnEnd = it.end?.col; if (columnStart !== undefined && columnEnd !== undefined) { - columnArr = columns.slice(columnStart, columnEnd + 1); + headerArr = headerArr.concat(columns.slice(columnStart, columnEnd + 1)); } - if (columnArr) { - copyDataArr = [columnArr, ...copyDataArr]; - } - that.beforeCopy(copyDataArr); - }, - }; - // 目前版本不支持 copy_with_column_headers 暂时用 cut 代替,以达到与copy类似的表现 - items['cut'] = copyWithHeaderItem; + }); + } + return headerArr; + }; + + const copyDataItem = { + name: '复制值', + callback: function (_key) { + const copyDataArr = getCopyData(this); + that.beforeCopy(copyDataArr); + }, + }; + const copyHeadersItem = { + name: '复制列名', + callback: function (_key, selection) { + const copyHeaders = getCopyHeaders(selection); + that.beforeCopy([copyHeaders]); + }, + }; + const copyHeadersAndDataItem = { + name: '复制列名和值', + callback: function (_key, selection) { + const copyDataArr = getCopyData(this); + const copyHeaders = getCopyHeaders(selection); + that.beforeCopy([copyHeaders, ...copyDataArr]); + }, + }; + + // 目前 items 在 https://github.com/handsontable/handsontable/blob/6.2.2/handsontable.d.ts#L779,自定义方法也可以被执行 + const items = {}; + if (Array.isArray(copyTypes) && copyTypes?.length) { + // 复制值 + if (copyTypes.includes('copyData')) { + items['copyData'] = copyDataItem; + } + // 复制列名 + if (copyTypes.includes('copyHeaders')) { + items['copyHeaders'] = copyHeadersItem; + } + // 复制列名和值 + if (copyTypes.includes('copyHeadersAndData')) { + items['copyHeadersAndData'] = copyHeadersAndDataItem; + } + } else { + items['copyData'] = copyDataItem; } - return { - items, - } as any; + + return { items } as any; } render() { const { columns = [], className = '', options, columnTypes = [] } = this.props; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { trimWhitespace = true, showCopyWithHeader, ...restOptions } = options || {}; - const showData = this.getData(); + const { trimWhitespace = true, ...restOptions } = options || {}; + const showData = this.getShowData(); // 空数组情况,不显示colHeaders,否则colHeaders默认会按照 A、B...显示 // 具体可见 https://handsontable.com/docs/7.1.1/Options.html#colHeaders let isShowColHeaders = false; @@ -153,7 +200,9 @@ class SpreadSheet extends React.PureComponent { if (!isShowColHeaders) return false; // handsontable 不支持 renderCustomHeader,所以只能用 html string 实现 tooltip const fieldTypeStr = columnTypes?.[index]?.type; - const title = fieldTypeStr ? `${columns?.[index]}: ${fieldTypeStr}` : columns?.[index]; + const title = fieldTypeStr + ? `${columns?.[index]}: ${fieldTypeStr}` + : columns?.[index]; return `${title}`; }} data={showData} diff --git a/src/components/spreadSheet/style.scss b/src/components/spreadSheet/style.scss index 1ac8d861e..48195d842 100644 --- a/src/components/spreadSheet/style.scss +++ b/src/components/spreadSheet/style.scss @@ -1,4 +1,5 @@ .dtc-handsontable-no-border { + width: 100%; .handsontable { thead tr:first-child th { border-top: 0; @@ -6,5 +7,15 @@ th:first-child { border-left: 0; } + th:last-child { + border-right: 0; + } + td:last-child { + border-right: 0; + } + // 最后一行 + tr:last-child th, tr:last-child td { + border-bottom: 0; + } } } diff --git a/src/components/utils/copy.tsx b/src/components/utils/copy.tsx index 13a7aa3e0..476361c95 100644 --- a/src/components/utils/copy.tsx +++ b/src/components/utils/copy.tsx @@ -60,7 +60,10 @@ export default class CopyUtils { let succeeded; try { - succeeded = document.execCommand('copy'); + // 浏览器兼容性处理,当前语法已废弃,延迟处理可以保证复制成功 + setTimeout(() => { + succeeded = document.execCommand('copy'); + }); } catch (err) { succeeded = false; } diff --git a/src/stories/spreadSheert.stories.tsx b/src/stories/spreadSheert.stories.tsx index 739628437..ffbc8ca1c 100644 --- a/src/stories/spreadSheert.stories.tsx +++ b/src/stories/spreadSheert.stories.tsx @@ -22,11 +22,11 @@ const propDefinitions = [ defaultValue: '', }, { - property: 'options.showCopyWithHeader', - propType: 'Boolean', + property: 'options.copyTypes', + propType: 'Array<"copyData" \| "copyHeaders" \| "copyHeadersAndData">', required: false, - description: '右键菜单中是否展示“复制值以及列名”按钮', - defaultValue: '', + description: '右键菜单中展示的选项 复制值/复制列名/复制列名和值 按钮', + defaultValue: '["copyData"]', }, { property: 'options.trimWhitespace', @@ -41,11 +41,31 @@ const otherDependencies = `import { SpreadSheet } from 'dt-react-component';`; const functionCode = ''; -const code = ``; +const code1 = ``; + +const code2 = ``; const stories = storiesOf('SpreadSheet 多功能表', module); stories.add( @@ -56,8 +76,9 @@ stories.add(

何时使用

表格内容右键可复制,表格大小可拖动

示例

+ 右键菜单:复制值、复制列名 + + +
+ 右键菜单:复制值、复制列名、复制列名和值 + + @@ -81,9 +127,18 @@ stories.add( 代码示例: ~~~js import { SpreadSheet } from 'dt-react-component' + ~~~ `,