From 3c6295cb9226be5cc08d020c11ae2dbc60becf35 Mon Sep 17 00:00:00 2001 From: Jinke Li Date: Wed, 7 Feb 2024 15:42:06 +0800 Subject: [PATCH 1/4] =?UTF-8?q?chore:=20=F0=9F=A4=96=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=20changelog=20=E6=96=87=E4=BB=B6=20(#2547)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/s2-core/CHANGELOG.md | 17 +++++++++++++++++ packages/s2-core/package.json | 2 +- packages/s2-react/CHANGELOG.md | 14 ++++++++++++++ packages/s2-react/package.json | 2 +- packages/s2-vue/CHANGELOG.md | 7 +++++++ packages/s2-vue/package.json | 2 +- 6 files changed, 41 insertions(+), 3 deletions(-) diff --git a/packages/s2-core/CHANGELOG.md b/packages/s2-core/CHANGELOG.md index 3768a8b518..55ca8e3fe1 100644 --- a/packages/s2-core/CHANGELOG.md +++ b/packages/s2-core/CHANGELOG.md @@ -1,3 +1,20 @@ +# [@antv/s2-v1.54.4](https://github.com/antvis/S2/compare/@antv/s2-v1.54.3...@antv/s2-v1.54.4) (2024-02-07) + + +### Bug Fixes + +* 修复表格排序后, 编辑单元格后数据更新错误 close [#2573](https://github.com/antvis/S2/issues/2573) ([#2544](https://github.com/antvis/S2/issues/2544)) ([9075896](https://github.com/antvis/S2/commit/9075896def56eb3841c552da0c5cf5603572023e)) + +# [@antv/s2-v1.54.3](https://github.com/antvis/S2/compare/@antv/s2-v1.54.2...@antv/s2-v1.54.3) (2024-01-26) + + +### Bug Fixes + +* **interaction:** 修复隐藏列头配置更新时未覆盖上一次的配置 close [#2495](https://github.com/antvis/S2/issues/2495) ([#2527](https://github.com/antvis/S2/issues/2527)) ([d68adcc](https://github.com/antvis/S2/commit/d68adcc7219d43da3f336c0cac5f538e7d83de01)) +* **table-sheet:** 修复明细表配置自定义行高后展示异常 close [#2501](https://github.com/antvis/S2/issues/2501) ([#2521](https://github.com/antvis/S2/issues/2521)) ([c772c2a](https://github.com/antvis/S2/commit/c772c2a48e5e805f6def128d8c7ba07258246a29)) +* 增加树状模式自定义宽度的容错 ([#2519](https://github.com/antvis/S2/issues/2519)) ([a28322a](https://github.com/antvis/S2/commit/a28322a8f4ae56f3fedd186663d4194b9d36d342)) +* 自定义 icon 支持跨域 close [#2513](https://github.com/antvis/S2/issues/2513) ([#2524](https://github.com/antvis/S2/issues/2524)) ([d0e512c](https://github.com/antvis/S2/commit/d0e512cc4810879bee36b5329ac83aded6e6bb78)) + # [@antv/s2-v1.54.2](https://github.com/antvis/S2/compare/@antv/s2-v1.54.1...@antv/s2-v1.54.2) (2023-12-22) diff --git a/packages/s2-core/package.json b/packages/s2-core/package.json index b57672de81..06a4b0ddf8 100644 --- a/packages/s2-core/package.json +++ b/packages/s2-core/package.json @@ -1,7 +1,7 @@ { "private": false, "name": "@antv/s2", - "version": "1.54.2", + "version": "1.54.4", "main": "lib/index.js", "unpkg": "dist/index.min.js", "jsdelivr": "dist/index.min.js", diff --git a/packages/s2-react/CHANGELOG.md b/packages/s2-react/CHANGELOG.md index a60b02a6cf..56ea6e2d1e 100644 --- a/packages/s2-react/CHANGELOG.md +++ b/packages/s2-react/CHANGELOG.md @@ -1,3 +1,17 @@ +# [@antv/s2-react-v1.46.2](https://github.com/antvis/S2/compare/@antv/s2-react-v1.46.1...@antv/s2-react-v1.46.2) (2024-02-07) + + +### Bug Fixes + +* 修复表格排序后, 编辑单元格后数据更新错误 close [#2573](https://github.com/antvis/S2/issues/2573) ([#2544](https://github.com/antvis/S2/issues/2544)) ([9075896](https://github.com/antvis/S2/commit/9075896def56eb3841c552da0c5cf5603572023e)) + +# [@antv/s2-react-v1.46.1](https://github.com/antvis/S2/compare/@antv/s2-react-v1.46.0...@antv/s2-react-v1.46.1) (2024-01-26) + + +### Bug Fixes + +* **interaction:** 修复隐藏列头配置更新时未覆盖上一次的配置 close [#2495](https://github.com/antvis/S2/issues/2495) ([#2527](https://github.com/antvis/S2/issues/2527)) ([d68adcc](https://github.com/antvis/S2/commit/d68adcc7219d43da3f336c0cac5f538e7d83de01)) + # [@antv/s2-react-v1.46.0](https://github.com/antvis/S2/compare/@antv/s2-react-v1.45.0...@antv/s2-react-v1.46.0) (2023-12-15) diff --git a/packages/s2-react/package.json b/packages/s2-react/package.json index 690267daa1..3d915f6daf 100644 --- a/packages/s2-react/package.json +++ b/packages/s2-react/package.json @@ -1,7 +1,7 @@ { "private": false, "name": "@antv/s2-react", - "version": "1.46.0", + "version": "1.46.2", "main": "lib/index.js", "unpkg": "dist/index.min.js", "jsdelivr": "dist/index.min.js", diff --git a/packages/s2-vue/CHANGELOG.md b/packages/s2-vue/CHANGELOG.md index 93ea10593a..db813dcc67 100644 --- a/packages/s2-vue/CHANGELOG.md +++ b/packages/s2-vue/CHANGELOG.md @@ -1,3 +1,10 @@ +# [@antv/s2-vue-v1.6.1](https://github.com/antvis/S2/compare/@antv/s2-vue-v1.6.0...@antv/s2-vue-v1.6.1) (2024-01-26) + + +### Bug Fixes + +* **interaction:** 修复隐藏列头配置更新时未覆盖上一次的配置 close [#2495](https://github.com/antvis/S2/issues/2495) ([#2527](https://github.com/antvis/S2/issues/2527)) ([d68adcc](https://github.com/antvis/S2/commit/d68adcc7219d43da3f336c0cac5f538e7d83de01)) + # [@antv/s2-vue-v1.6.0](https://github.com/antvis/S2/compare/@antv/s2-vue-v1.5.0...@antv/s2-vue-v1.6.0) (2023-12-15) diff --git a/packages/s2-vue/package.json b/packages/s2-vue/package.json index 207bcb5a64..0e7a7bfc42 100644 --- a/packages/s2-vue/package.json +++ b/packages/s2-vue/package.json @@ -1,7 +1,7 @@ { "private": false, "name": "@antv/s2-vue", - "version": "1.6.0", + "version": "1.6.1", "main": "lib/index.js", "unpkg": "dist/index.min.js", "jsdelivr": "dist/index.min.js", From cfab9f40b78bcef4c3a4ecb74f9b9f7102a0e422 Mon Sep 17 00:00:00 2001 From: Jinke Li Date: Thu, 22 Feb 2024 17:43:30 +0800 Subject: [PATCH 2/4] =?UTF-8?q?fix(scroll):=20=E4=BF=AE=E5=A4=8D=E7=A7=BB?= =?UTF-8?q?=E5=8A=A8=E7=AB=AF=E6=BB=9A=E5=8A=A8=E8=87=B3=E8=BE=B9=E7=BC=98?= =?UTF-8?q?=E6=97=B6=E6=8A=96=E5=8A=A8=20(#2556)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/s2-core/src/facet/base-facet.ts | 48 +++++++++++++++++++++--- 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/packages/s2-core/src/facet/base-facet.ts b/packages/s2-core/src/facet/base-facet.ts index ccbda879de..fd14be4e2f 100644 --- a/packages/s2-core/src/facet/base-facet.ts +++ b/packages/s2-core/src/facet/base-facet.ts @@ -1,4 +1,4 @@ -import type { IElement, IGroup, Event as GraphEvent } from '@antv/g-canvas'; +import type { Event as GraphEvent, IElement, IGroup } from '@antv/g-canvas'; import { Group } from '@antv/g-canvas'; import { Wheel, type GestureEvent } from '@antv/g-gesture'; import { interpolateArray } from 'd3-interpolate'; @@ -24,6 +24,7 @@ import { KEY_GROUP_ROW_RESIZE_AREA, OriginEventType, S2Event, + ScrollDirection, ScrollbarPositionType, } from '../common/constant'; import { DEFAULT_PAGE_INDEX } from '../common/constant/pagination'; @@ -74,7 +75,6 @@ import { optimizeScrollXY, translateGroup, } from './utils'; -import type { BaseHeader, BaseHeaderConfig } from './header/base'; export abstract class BaseFacet { // spreadsheet instance @@ -133,6 +133,8 @@ export abstract class BaseFacet { layoutResult?: LayoutResult, ): ViewCellHeights; + protected scrollDirection: ScrollDirection; + protected scrollFrameId: ReturnType = null; get scrollBarTheme() { @@ -183,26 +185,57 @@ export abstract class BaseFacet { this.hScrollBar?.show(); }; + onContainerWheelForMobileCompatibility = () => { + const canvas = this.spreadsheet.getCanvasElement(); + let startY: number; + let endY: number; + + canvas.addEventListener('touchstart', (event) => { + startY = event.touches[0].clientY; + }); + + canvas.addEventListener('touchend', (event) => { + endY = event.changedTouches[0].clientY; + if (endY < startY) { + this.scrollDirection = ScrollDirection.SCROLL_UP; + } else if (endY > startY) { + this.scrollDirection = ScrollDirection.SCROLL_DOWN; + } + }); + }; + onContainerWheel = () => { this.onContainerWheelForPc(); this.onContainerWheelForMobile(); }; + // g-gesture@1.0.1 手指快速往上滚动时, deltaY 有时会为负数, 导致向下滚动时然后回弹, 看起来就像表格在抖动, 需要判断滚动方向, 修正一下. + getMobileWheelDeltaY = (deltaY: number) => { + if (this.scrollDirection === ScrollDirection.SCROLL_UP) { + return Math.max(0, deltaY); + } + + if (this.scrollDirection === ScrollDirection.SCROLL_DOWN) { + return Math.min(0, deltaY); + } + + return deltaY; + }; + onContainerWheelForPc = () => { const canvas = this.spreadsheet.getCanvasElement(); canvas?.addEventListener('wheel', this.onWheel); }; onContainerWheelForMobile = () => { - // mock wheel event fo mobile this.mobileWheel = new Wheel(this.spreadsheet.container); this.mobileWheel.on('wheel', (ev: GestureEvent) => { this.spreadsheet.hideTooltip(); const originEvent = ev.event; - const { deltaX, deltaY, x, y } = ev; - // The coordinates of mobile and pc are three times different - // TODO: 手指快速往上滚动时, deltaY 有时会为负数, 导致向下滚动时然后回弹, 看起来就像表格在抖动, 需要判断滚动方向, next 版本未复现 + const { deltaX, deltaY: defaultDeltaY, x, y } = ev; + const deltaY = this.getMobileWheelDeltaY(defaultDeltaY); + this.onWheel({ ...originEvent, deltaX, @@ -211,6 +244,8 @@ export abstract class BaseFacet { offsetY: y, } as unknown as WheelEvent); }); + + this.onContainerWheelForMobileCompatibility(); }; bindEvents = () => { @@ -930,6 +965,7 @@ export abstract class BaseFacet { const { interaction } = this.spreadsheet.options; if (interaction.overscrollBehavior !== 'auto') { + this.cancelScrollFrame(); this.stopScrollChaining(event); } }; From 31cc240a3d4bcf4d83e97bda75ecdca9276cc6d2 Mon Sep 17 00:00:00 2001 From: Jinke Li Date: Fri, 23 Feb 2024 11:14:07 +0800 Subject: [PATCH 3/4] =?UTF-8?q?fix(interaction):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E8=A1=A8=E6=A0=BC=E6=BB=9A=E5=8A=A8=E5=90=8E,=20=E8=A1=8C?= =?UTF-8?q?=E5=88=97=E5=A4=B4=E9=83=A8=E5=88=86=E5=8D=95=E5=85=83=E6=A0=BC?= =?UTF-8?q?=E9=80=89=E4=B8=AD=E9=AB=98=E4=BA=AE=E6=95=88=E6=9E=9C=E4=B8=A2?= =?UTF-8?q?=E5=A4=B1=20close=20#2503=20(#2545)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(interaction): 修复表格滚动后, 行列头部分单元格选中高亮效果丢失 close #2503 * chore: 还原 * test: 修复单测 * test: 修复单测 --------- Co-authored-by: Wenjun Xu <906626481@qq.com> --- .../interaction-multi-selection-spec.ts | 75 +++++++++++++++++-- .../__tests__/unit/utils/tooltip-spec.ts | 6 +- packages/s2-core/src/interaction/root.ts | 4 +- packages/s2-core/src/utils/tooltip.ts | 17 ++++- packages/s2-react/playground/config.ts | 3 +- 5 files changed, 91 insertions(+), 14 deletions(-) diff --git a/packages/s2-core/__tests__/spreadsheet/interaction-multi-selection-spec.ts b/packages/s2-core/__tests__/spreadsheet/interaction-multi-selection-spec.ts index 3eaa290de2..e5d7b98aef 100644 --- a/packages/s2-core/__tests__/spreadsheet/interaction-multi-selection-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/interaction-multi-selection-spec.ts @@ -1,5 +1,10 @@ import * as mockDataConfig from 'tests/data/simple-data.json'; -import { createMockCellInfo, getContainer } from 'tests/util/helpers'; +import { + createMockCellInfo, + createPivotSheet, + getContainer, + sleep, +} from 'tests/util/helpers'; import { size, sumBy } from 'lodash'; import { getTooltipData, mergeCellInfo } from '../../src/utils/tooltip'; import { PivotSheet, SpreadSheet } from '@/sheet-type'; @@ -20,8 +25,8 @@ const s2Options: S2Options = { describe('Interaction Multi Selection Tests', () => { let s2: SpreadSheet; - const expectNodes = (ids: string[] = []) => { - const state = s2.interaction.getState(); + const expectNodes = (ids: string[] = [], instance: SpreadSheet = s2) => { + const state = instance.interaction.getState(); const nodeIds = state.nodes.map((node) => node.id); expect(nodeIds).toEqual(ids); }; @@ -120,7 +125,11 @@ describe('Interaction Multi Selection Tests', () => { cell: rowRootCell, }); - expectNodes(['root[&]中国[&]浙江[&]义乌', 'root[&]中国[&]浙江[&]杭州']); + expectNodes([ + 'root[&]中国[&]浙江', + 'root[&]中国[&]浙江[&]义乌', + 'root[&]中国[&]浙江[&]杭州', + ]); const tooltipData = getTestTooltipData(rowRootCell); @@ -150,11 +159,67 @@ describe('Interaction Multi Selection Tests', () => { cell: colRootCell, }); - expectNodes(['root[&]中国[&]浙江[&]price', 'root[&]中国[&]浙江[&]cost']); + expectNodes([ + 'root[&]中国[&]浙江', + 'root[&]中国[&]浙江[&]price', + 'root[&]中国[&]浙江[&]cost', + ]); const tooltipData = getTestTooltipData(colRootCell); expect(getSelectedCount(tooltipData.summaries)).toEqual(4); expect(getSelectedSum(tooltipData.summaries)).toEqual(6); }); + + // https://github.com/antvis/S2/issues/2503 + test('should keep all cell highlighted after scroll', async () => { + const pivotSheet = createPivotSheet( + { + style: { + // 显示滚动条 + cellCfg: { + width: 200, + }, + }, + }, + { useSimpleData: false }, + ); + + pivotSheet.render(); + + const colRootCell = pivotSheet.interaction.getAllColHeaderCells()[0]; + + pivotSheet.interaction.selectHeaderCell({ + cell: colRootCell, + }); + + await sleep(100); + + pivotSheet.updateScrollOffset({ + offsetX: { + value: 100, + animate: true, + }, + }); + + await sleep(500); + + expectNodes( + [ + 'root[&]家具[&]桌子', + 'root[&]家具[&]桌子[&]number', + 'root[&]家具[&]沙发', + 'root[&]家具[&]沙发[&]number', + ], + pivotSheet, + ); + + const interactedCells = pivotSheet.interaction.getInteractedCells(); + + ['root[&]家具[&]桌子', 'root[&]家具[&]沙发'].forEach((id) => { + expect( + interactedCells.find((cell) => cell.getMeta().id === id), + ).toBeTruthy(); + }); + }); }); diff --git a/packages/s2-core/__tests__/unit/utils/tooltip-spec.ts b/packages/s2-core/__tests__/unit/utils/tooltip-spec.ts index 50913718f1..6c41b74dd6 100644 --- a/packages/s2-core/__tests__/unit/utils/tooltip-spec.ts +++ b/packages/s2-core/__tests__/unit/utils/tooltip-spec.ts @@ -450,9 +450,13 @@ describe('Tooltip Utils Tests', () => { let s2: SpreadSheet; const getMockTooltipData = (cell: S2CellType) => { + const node = cell.getMeta() as Node; + // #getSelectedCellIndexes mock + node.isLeaf = true; + jest.spyOn(s2.interaction, 'getState').mockImplementationOnce(() => ({ cells: [getCellMeta(cell)], - nodes: [cell.getMeta() as Node], + nodes: [node], })); jest diff --git a/packages/s2-core/src/interaction/root.ts b/packages/s2-core/src/interaction/root.ts index b7293ca5c7..e663e501bd 100644 --- a/packages/s2-core/src/interaction/root.ts +++ b/packages/s2-core/src/interaction/root.ts @@ -323,12 +323,10 @@ export class RootInteraction { return; } - // 高亮所有的子节点, 但是只有叶子节点需要参与数据计算 - const needCalcNodes = childrenNodes.filter((node) => node?.isLeaf); // 兼容行列多选 (高亮 行/列头 以及相对应的数值单元格) this.changeState({ cells: selectedCells, - nodes: needCalcNodes, + nodes: childrenNodes, stateName: InteractionStateName.SELECTED, }); diff --git a/packages/s2-core/src/utils/tooltip.ts b/packages/s2-core/src/utils/tooltip.ts index 33b457e94e..38b46ba068 100644 --- a/packages/s2-core/src/utils/tooltip.ts +++ b/packages/s2-core/src/utils/tooltip.ts @@ -63,6 +63,7 @@ import type { import { getLeafColumnsWithKey } from '../facet/utils'; import type { SpreadSheet } from '../sheet-type'; import { getDataSumByField, isNotNumber } from '../utils/number-calculate'; +import type { Node as S2Node } from '../facet/layout/node'; import { handleDataItem } from './cell/data-cell'; import { isMultiDataItem } from './data-item-type-checker'; import { customMerge } from './merge'; @@ -330,7 +331,11 @@ export const getSummaryName = ( return name && name !== 'undefined' ? name : ''; }; -const getRowOrColSelectedIndexes = (nodes, leafNodes, isRow = true) => { +const getRowOrColSelectedIndexes = ( + nodes: S2Node[], + leafNodes: S2Node[], + isRow = true, +) => { const selectedIndexes = []; forEach(leafNodes, (leaf, index) => { forEach(nodes, (item) => { @@ -352,12 +357,15 @@ export const getSelectedCellIndexes = ( const { rowLeafNodes, colLeafNodes } = layoutResult; const { nodes = [], cells = [] } = spreadsheet.interaction.getState(); const cellType = cells?.[0]?.type; + // 高亮所有的子节点, 但是只有叶子节点需要参与数据计算 https://github.com/antvis/S2/pull/1443 + const needCalcNodes = nodes.filter((node) => node?.isLeaf); if (cellType === CellTypes.COL_CELL) { - return getRowOrColSelectedIndexes(nodes, rowLeafNodes, false); + return getRowOrColSelectedIndexes(needCalcNodes, rowLeafNodes, false); } + if (cellType === CellTypes.ROW_CELL) { - return getRowOrColSelectedIndexes(nodes, colLeafNodes); + return getRowOrColSelectedIndexes(needCalcNodes, colLeafNodes); } return []; @@ -375,7 +383,7 @@ export const getSelectedCellsData = ( * 1. [点击列头单元格时], 选中列所对应的数值单元格的数据如果是小计/总计, 则不应该参与计算: * - 1.1 [小计/总计 位于行头]: 点击的都是 (普通列头), 需要去除 (数值单元格) 对应 (行头为小计) 的单元格的数据 * - 1.2 [小计/总计 位于列头]: 点击的是 (普通列头/小计/总计列头), 由于行头没有, 所有数值单元格参与计算即可 - * - 1.3 [小计/总计 同时位于行头/列头]: 和 1.1 处理一致 + * - 1.3 [小计/总计 同时位于行头/列头]: 和 1.1 处理一致 * 2. [点击行头单元格时]: * - 2.1 如果本身就是小计/总计单元格, 且列头无小计/总计, 则当前行所有 (数值单元格) 参与计算 @@ -568,6 +576,7 @@ export const getTooltipData = (params: TooltipDataParam): TooltipData => { details = getTooltipDetailList(spreadsheet, firstCellInfo, options); } const { interpretation, infos, tips, name } = firstCellInfo || {}; + return { summaries, interpretation, diff --git a/packages/s2-react/playground/config.ts b/packages/s2-react/playground/config.ts index 899add0ff7..53cb15349b 100644 --- a/packages/s2-react/playground/config.ts +++ b/packages/s2-react/playground/config.ts @@ -81,7 +81,7 @@ export const s2Options: SheetComponentOptions = { width: 600, height: 400, frozenFirstRow: false, - showSeriesNumber: true, + showSeriesNumber: false, interaction: { enableCopy: true, copyWithHeader: true, @@ -95,6 +95,7 @@ export const s2Options: SheetComponentOptions = { }, }, tooltip: { + showTooltip: true, operation: { trend: true, }, From 8d9c5b29ef5c05ada8fcbc9ea35b297e3e565ce3 Mon Sep 17 00:00:00 2001 From: Jinke Li Date: Fri, 23 Feb 2024 16:10:04 +0800 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E8=A1=A8=E7=9A=84=E8=BE=93=E5=85=A5=E6=A1=86=E6=9C=AA=E5=9B=9E?= =?UTF-8?q?=E5=A1=AB=E6=A0=BC=E5=BC=8F=E5=8C=96=E5=90=8E=E7=9A=84=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=20close=20#2528=20(#2549)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 修复编辑表的输入框未回填格式化后的数据 close #2528 * fix: 修复循环引用 * fix: 编辑后不应该再次格式化 * fix: 移除定义 * test: 修复单测 * test: 增加单测 --- .../s2-core/__tests__/bugs/issue-2528-spec.ts | 57 ++++++++ .../__tests__/unit/facet/pivot-facet-spec.ts | 1 + .../__tests__/unit/facet/table-facet-spec.ts | 1 + packages/s2-core/__tests__/util/helpers.ts | 1 + packages/s2-core/src/cell/base-cell.ts | 3 +- packages/s2-core/src/cell/data-cell.ts | 12 +- packages/s2-core/src/cell/table-data-cell.ts | 1 - .../s2-core/src/data-set/base-data-set.ts | 3 + packages/s2-react/playground/config.ts | 4 +- packages/s2-react/playground/index.tsx | 1 + .../sheets/editable-sheet/edit-cell/index.tsx | 132 +++++++++--------- .../sheets/editable-sheet/index.tsx | 5 +- packages/s2-shared/src/interface.ts | 4 +- s2-site/docs/api/basic-class/base-cell.en.md | 2 +- s2-site/docs/api/basic-class/base-cell.zh.md | 2 +- .../docs/api/basic-class/base-data-set.zh.md | 3 +- .../docs/api/components/sheet-component.zh.md | 3 +- .../manual/basic/analysis/editable-mode.zh.md | 7 +- .../react-component/sheet/demo/editable.tsx | 9 +- 19 files changed, 168 insertions(+), 83 deletions(-) create mode 100644 packages/s2-core/__tests__/bugs/issue-2528-spec.ts diff --git a/packages/s2-core/__tests__/bugs/issue-2528-spec.ts b/packages/s2-core/__tests__/bugs/issue-2528-spec.ts new file mode 100644 index 0000000000..527c9d24cf --- /dev/null +++ b/packages/s2-core/__tests__/bugs/issue-2528-spec.ts @@ -0,0 +1,57 @@ +/** + * @description spec for issue #2528 + * https://github.com/antvis/S2/issues/2528 + */ + +import * as mockDataConfig from '../data/simple-table-data.json'; +import { getContainer } from '../util/helpers'; +import type { SpreadSheet, S2DataConfig, S2Options } from '@/index'; +import { TableSheet } from '@/sheet-type'; + +const s2DataConfig: S2DataConfig = { + ...mockDataConfig, + meta: [{ field: 'cost', formatter: (v) => `${v}-@` }], +}; + +const s2Options: S2Options = { + width: 800, + height: 400, +}; + +describe('Table Sheet Editable Formatter Tests', () => { + let s2: SpreadSheet; + + beforeEach(() => { + s2 = new TableSheet(getContainer(), s2DataConfig, s2Options); + + s2.render(); + }); + + test('should get formatted data', () => { + const costValues = s2.interaction + .getPanelGroupAllDataCells() + .filter((cell) => cell.getMeta().valueField === 'cost') + .map((cell) => cell.getFieldValue()); + + expect(costValues).toEqual(['2-@', '2-@', '2-@']); + }); + + test('should only format data once after data edited', () => { + const id = '0-root[&]cost'; + const inputValue = 'test'; + + // 模拟一次编辑 (更新第一行的 cost) + const displayData = s2.dataSet.getDisplayDataSet(); + displayData[0].cost = inputValue; + s2.dataSet.displayFormattedValueMap?.set(id, inputValue); + + s2.render(); + + const costValues = s2.interaction + .getPanelGroupAllDataCells() + .filter((cell) => cell.getMeta().valueField === 'cost') + .map((cell) => cell.getFieldValue()); + + expect(costValues).toEqual([inputValue, '2-@', '2-@']); + }); +}); diff --git a/packages/s2-core/__tests__/unit/facet/pivot-facet-spec.ts b/packages/s2-core/__tests__/unit/facet/pivot-facet-spec.ts index 4e5e2438bf..62017ca0b9 100644 --- a/packages/s2-core/__tests__/unit/facet/pivot-facet-spec.ts +++ b/packages/s2-core/__tests__/unit/facet/pivot-facet-spec.ts @@ -108,6 +108,7 @@ jest.mock('@/data-set/pivot-data-set', () => { getDimensionValues: actualPivotDataSet.prototype.getDimensionValues, getFieldsAndPivotMetaByField: actualPivotDataSet.prototype.getFieldsAndPivotMetaByField, + displayFormattedValueMap: new Map(), }; }), }; diff --git a/packages/s2-core/__tests__/unit/facet/table-facet-spec.ts b/packages/s2-core/__tests__/unit/facet/table-facet-spec.ts index ad72e6226a..277d771e9d 100644 --- a/packages/s2-core/__tests__/unit/facet/table-facet-spec.ts +++ b/packages/s2-core/__tests__/unit/facet/table-facet-spec.ts @@ -63,6 +63,7 @@ jest.mock('@/sheet-type', () => { }, dataSet: { isEmpty: jest.fn(), + displayFormattedValueMap: new Map(), }, }; }), diff --git a/packages/s2-core/__tests__/util/helpers.ts b/packages/s2-core/__tests__/util/helpers.ts index 7d7d9dc486..c357db885b 100644 --- a/packages/s2-core/__tests__/util/helpers.ts +++ b/packages/s2-core/__tests__/util/helpers.ts @@ -72,6 +72,7 @@ export const createFakeSpreadSheet = () => { getMultiData() { return []; }, + displayFormattedValueMap: new Map(), } as unknown as any; s2.facet = { layoutResult: { diff --git a/packages/s2-core/src/cell/base-cell.ts b/packages/s2-core/src/cell/base-cell.ts index 28467f391f..e80abe2c46 100644 --- a/packages/s2-core/src/cell/base-cell.ts +++ b/packages/s2-core/src/cell/base-cell.ts @@ -53,6 +53,7 @@ import type { GuiIcon } from '../common/icons/gui-icon'; import { checkIsLinkField } from '../utils/interaction/link-field'; import type { Node } from '../facet/layout/node'; import type { ViewMeta } from '../common/interface/basic'; +import { customMerge } from '../utils/merge'; export abstract class BaseCell extends Group { // cell's data meta info @@ -113,7 +114,7 @@ export abstract class BaseCell extends Group { } public setMeta(viewMeta: T) { - this.meta = viewMeta; + this.meta = customMerge(this.meta, viewMeta); } public getIconStyle() { diff --git a/packages/s2-core/src/cell/data-cell.ts b/packages/s2-core/src/cell/data-cell.ts index 29472a49bb..ece1b92141 100644 --- a/packages/s2-core/src/cell/data-cell.ts +++ b/packages/s2-core/src/cell/data-cell.ts @@ -180,8 +180,8 @@ export class DataCell extends BaseCell { } } - public setMeta(viewMeta: ViewMeta) { - super.setMeta(viewMeta); + public setMeta(viewMeta: Partial) { + super.setMeta(viewMeta as ViewMeta); this.initCell(); } @@ -293,12 +293,16 @@ export class DataCell extends BaseCell { formattedValue: EMPTY_PLACEHOLDER, }; } - const { rowId, valueField, fieldValue, data } = this.meta; + + const { rowId, valueField, fieldValue, data, id } = this.meta; + const displayFormattedValue = + this.spreadsheet.dataSet.displayFormattedValueMap?.get(id); const rowMeta = this.spreadsheet.dataSet.getFieldMeta(rowId); const fieldId = rowMeta ? rowId : valueField; const formatter = this.spreadsheet.dataSet.getFieldFormatter(fieldId); // TODO: 这里只用 formatter(fieldValue, this.meta) 即可, 为了保持兼容, 暂时在第三个参入传入 meta 信息 - const formattedValue = formatter(fieldValue, data, this.meta); + const formattedValue = + displayFormattedValue ?? formatter(fieldValue, data, this.meta); return { value: fieldValue, diff --git a/packages/s2-core/src/cell/table-data-cell.ts b/packages/s2-core/src/cell/table-data-cell.ts index 436794cc6f..1bceb807b8 100644 --- a/packages/s2-core/src/cell/table-data-cell.ts +++ b/packages/s2-core/src/cell/table-data-cell.ts @@ -13,7 +13,6 @@ import { getOrCreateResizeAreaGroupById, getResizeAreaAttrs, } from '../utils/interaction/resize'; -import { checkIsLinkField } from '../utils/interaction/link-field'; export class TableDataCell extends DataCell { protected getLinkFieldStyle() { diff --git a/packages/s2-core/src/data-set/base-data-set.ts b/packages/s2-core/src/data-set/base-data-set.ts index 2b1dcfc2e2..335cc770a8 100644 --- a/packages/s2-core/src/data-set/base-data-set.ts +++ b/packages/s2-core/src/data-set/base-data-set.ts @@ -53,6 +53,9 @@ export abstract class BaseDataSet { // 透视表入口对象实例 public spreadsheet: SpreadSheet; + // 单元格所对应格式化后的值(用于编辑表) + public displayFormattedValueMap = new Map(); + public constructor(spreadsheet: SpreadSheet) { this.spreadsheet = spreadsheet; } diff --git a/packages/s2-react/playground/config.ts b/packages/s2-react/playground/config.ts index 53cb15349b..10dda84773 100644 --- a/packages/s2-react/playground/config.ts +++ b/packages/s2-react/playground/config.ts @@ -33,7 +33,7 @@ export const tableSheetMultipleColumns: Columns = [ export const tableSheetDataCfg: S2DataConfig = { data, totalData, - meta, + meta: [{ field: 'number', formatter: (v) => `${v}-@` }, ...meta], fields: { columns: tableSheetSingleColumns, }, @@ -77,7 +77,7 @@ export const pivotSheetDataCfgForCompactMode = customMerge(pivotSheetDataCfg, { }); export const s2Options: SheetComponentOptions = { - debug: false, + debug: true, width: 600, height: 400, frozenFirstRow: false, diff --git a/packages/s2-react/playground/index.tsx b/packages/s2-react/playground/index.tsx index 290dcc68a4..e54366583e 100644 --- a/packages/s2-react/playground/index.tsx +++ b/packages/s2-react/playground/index.tsx @@ -1299,6 +1299,7 @@ function MainLayout() { ref={s2Ref} themeCfg={themeCfg} onMounted={onSheetMounted} + onDataCellEditStart={logHandler('onDataCellEditStart')} onDataCellEditEnd={logHandler('onDataCellEditEnd')} /> diff --git a/packages/s2-react/src/components/sheets/editable-sheet/edit-cell/index.tsx b/packages/s2-react/src/components/sheets/editable-sheet/edit-cell/index.tsx index ee41385145..ce6a67059c 100644 --- a/packages/s2-react/src/components/sheets/editable-sheet/edit-cell/index.tsx +++ b/packages/s2-react/src/components/sheets/editable-sheet/edit-cell/index.tsx @@ -1,22 +1,16 @@ import type { Event as CanvasEvent } from '@antv/g-canvas'; import { - BaseCell, S2Event, SpreadSheet, + customMerge, type DataType, type S2CellType, + type TableDataCell, type ViewMeta, } from '@antv/s2'; import { Input } from 'antd'; -import { merge, pick } from 'lodash'; -import React, { - memo, - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; +import { pick } from 'lodash'; +import React from 'react'; import { useS2Event } from '../../../../hooks'; import { useSpreadSheetRef } from '../../../../utils/SpreadSheetContext'; import { @@ -34,9 +28,15 @@ export interface CustomProps { cell: S2CellType; } +type DateCellEdit = (meta: ViewMeta, cell: TableDataCell) => void; + type EditCellProps = { + /** + * @deprecated use `onDataCellEditEnd` instead. + */ onChange?: (data: DataType[]) => void; - onDataCellEditEnd?: (meta: ViewMeta) => void; + onDataCellEditStart?: DateCellEdit; + onDataCellEditEnd?: DateCellEdit; trigger?: number; CustomComponent?: React.FunctionComponent; }; @@ -46,30 +46,32 @@ function EditCellComponent( ) { const { params, resolver } = props; const spreadsheet = useSpreadSheetRef(); - const { event, onChange, onDataCellEditEnd, CustomComponent } = params; - const cell: BaseCell = event.target.cfg.parent; + const { + event, + onChange, + onDataCellEditStart, + onDataCellEditEnd, + CustomComponent, + } = params; - const { left, top, width, height } = useMemo(() => { - const rect = ( - spreadsheet?.container.cfg.container as HTMLElement - ).getBoundingClientRect(); + const cell = spreadsheet.getCell(event.target); + const { left, top, width, height } = React.useMemo>(() => { + const rect = spreadsheet.getCanvasElement()?.getBoundingClientRect(); - const modified = { + return { left: window.scrollX + rect.left, top: window.scrollY + rect.top, width: rect.width, height: rect.height, }; - - return modified; - }, [spreadsheet?.container.cfg.container]); + }, [spreadsheet]); const { x: cellLeft, y: cellTop, width: cellWidth, height: cellHeight, - } = useMemo(() => { + } = React.useMemo(() => { const scroll = spreadsheet.facet.getScrollOffset(); const cellMeta = pick(cell.getMeta(), ['x', 'y', 'width', 'height']); @@ -81,45 +83,44 @@ function EditCellComponent( return cellMeta; }, [cell, spreadsheet]); - const [inputVal, setInputVal] = useState(cell.getMeta().fieldValue); - const inputRef = useRef(null); - const containerRef = useRef(null); + const [inputVal, setInputVal] = React.useState(() => cell.getFieldValue()); - useEffect(() => { - setTimeout(() => { - // 防止触发表格全选 - containerRef.current?.click(); - // 开启 preventScroll, 防止页面有滚动条时触发滚动 - inputRef.current?.focus({ preventScroll: true }); - }); - }, []); + const inputRef = React.useRef(null); + const containerRef = React.useRef(null); const onSave = () => { - const { rowIndex, valueField } = cell.getMeta(); + const { rowIndex, valueField, id } = cell.getMeta(); const displayData = spreadsheet.dataSet.getDisplayDataSet(); displayData[rowIndex][valueField] = inputVal; - spreadsheet.render(true); - - onDataCellEditEnd?.( - merge(cell.getMeta(), { - fieldValue: inputVal, - data: { - [valueField]: inputVal, - }, - }), - ); + // 编辑后的值作为格式化后的结果, formatter 不再触发, 避免二次格式化 + spreadsheet.dataSet.displayFormattedValueMap?.set(id, inputVal); + spreadsheet.render(); + + const editedMeta = customMerge(cell.getMeta(), { + fieldValue: inputVal, + data: { + [valueField]: inputVal, + }, + }); + onDataCellEditEnd?.(editedMeta, cell); onChange?.(displayData); resolver(true); }; const onKeyDown: React.KeyboardEventHandler = (e) => { - if (e.keyCode === 13) { + if (e.key === 'Enter') { e.preventDefault(); onSave(); } }; + // 让输入框聚焦时光标在文字的末尾 + const onFocus: React.FocusEventHandler = (e) => { + e.target.selectionStart = e.target.value.length; + e.target.selectionEnd = e.target.value.length; + }; + const styleProps = React.useMemo(() => { return { left: cellLeft, @@ -134,6 +135,16 @@ function EditCellComponent( setInputVal(val); }; + React.useEffect(() => { + onDataCellEditStart?.(cell.getMeta(), cell); + setTimeout(() => { + // 防止触发表格全选 + containerRef.current?.click(); + // 开启 preventScroll, 防止页面有滚动条时触发滚动 + inputRef.current?.focus({ preventScroll: true }); + }); + }, []); + return (
)}
); } -export const EditCell: React.FC = memo( - ({ onChange, onDataCellEditEnd, CustomComponent }) => { - const spreadsheet = useSpreadSheetRef(); - - const cb = useCallback( - (event: CanvasEvent) => { - invokeComponent( - EditCellComponent, - { event, onChange, onDataCellEditEnd, CustomComponent }, - spreadsheet, - ); - }, - [spreadsheet], - ); +export const EditCell: React.FC = React.memo((props) => { + const spreadsheet = useSpreadSheetRef(); + + const cb = React.useCallback( + (event: CanvasEvent) => { + invokeComponent(EditCellComponent, { ...props, event }, spreadsheet); + }, + [spreadsheet], + ); - useS2Event(S2Event.DATA_CELL_DOUBLE_CLICK, cb, spreadsheet); + useS2Event(S2Event.DATA_CELL_DOUBLE_CLICK, cb, spreadsheet); - return null; - }, -); + return null; +}); EditCell.displayName = 'EditCell'; diff --git a/packages/s2-react/src/components/sheets/editable-sheet/index.tsx b/packages/s2-react/src/components/sheets/editable-sheet/index.tsx index 75ef726733..c20a924973 100644 --- a/packages/s2-react/src/components/sheets/editable-sheet/index.tsx +++ b/packages/s2-react/src/components/sheets/editable-sheet/index.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react'; +import React from 'react'; import { BaseSheet } from '../base-sheet'; import type { SheetComponentsProps } from '../interface'; import { DragCopyPoint } from './drag-copy'; @@ -6,11 +6,10 @@ import { EditCell } from './edit-cell'; export const EditableSheet: React.FC = React.memo( (props) => { - const onChange = useCallback(() => {}, []); return ( diff --git a/packages/s2-shared/src/interface.ts b/packages/s2-shared/src/interface.ts index 9a6b4e0529..594efd20b8 100644 --- a/packages/s2-shared/src/interface.ts +++ b/packages/s2-shared/src/interface.ts @@ -19,6 +19,7 @@ import type { S2RenderOptions, SortParams, SpreadSheet, + TableDataCell, TargetCellInfo, ThemeCfg, TooltipContentType, @@ -121,7 +122,8 @@ export interface BaseSheetComponentProps< onDataCellTrendIconClick?: (meta: ViewMeta) => void; onDataCellBrushSelection?: (brushRangeDataCells: DataCell[]) => void; onDataCellSelectMove?: (metas: CellMeta[]) => void; - onDataCellEditEnd?: (meta: ViewMeta) => void; + onDataCellEditStart?: (meta: ViewMeta, cell: TableDataCell) => void; + onDataCellEditEnd?: (meta: ViewMeta, cell: TableDataCell) => void; // ============== Corner Cell ==================== onCornerCellHover?: (data: TargetCellInfo) => void; diff --git a/s2-site/docs/api/basic-class/base-cell.en.md b/s2-site/docs/api/basic-class/base-cell.en.md index 1dd9727128..bcc5984333 100644 --- a/s2-site/docs/api/basic-class/base-cell.en.md +++ b/s2-site/docs/api/basic-class/base-cell.en.md @@ -7,7 +7,7 @@ Function description: cell base class. [details](https://github.com/antvis/S2/bl | parameter | illustrate | type | | ---------------------- | -------------------------------------- | ----------------------------------------------------------------------- | | getMeta | Get cell metadata | () => [ViewMeta](#viewmeta) | -| setMeta | Set cell metadata | (vieMeta: [ViewMeta](#viewmeta) ) => void | +| setMeta | Set cell metadata | (vieMeta: [Partial](#viewmeta) ) => void | | getIconStyle | Get cell icon style | () => [IconTheme](/docs/api/general/S2Theme#icontheme) | | getStyle | get cell style | () => [DefaultCellTheme](/docs/api/general/S2Theme#defaultcelltheme) | | getTextAndIconPosition | Get the position of cell text and icon | (iconCount: `number` ) => [TextAndIconPosition](#textandiconposition) | diff --git a/s2-site/docs/api/basic-class/base-cell.zh.md b/s2-site/docs/api/basic-class/base-cell.zh.md index 45753b1589..3bd8e03483 100644 --- a/s2-site/docs/api/basic-class/base-cell.zh.md +++ b/s2-site/docs/api/basic-class/base-cell.zh.md @@ -12,7 +12,7 @@ cell.getActualText() | 参数 | 说明 | 类型 | | --- | --- | --- | | getMeta | 获取单元格元数据 | () => [ViewMeta](#viewmeta) | -| setMeta | 设置单元格元数据 | (vieMeta: [ViewMeta](#viewmeta)) => void | +| setMeta | 设置单元格元数据 | (vieMeta: [Partial](#viewmeta)) => void | | getIconStyle | 获取单元格图标样式 | () => [IconTheme](/docs/api/general/S2Theme#icontheme) | | getStyle | 获取单元格样式 | () => [DefaultCellTheme](/docs/api/general/S2Theme#defaultcelltheme) | | getTextAndIconPosition | 获取单元格文本和图标的位置 | (iconCount: `number`) => [TextAndIconPosition](#textandiconposition) | diff --git a/s2-site/docs/api/basic-class/base-data-set.zh.md b/s2-site/docs/api/basic-class/base-data-set.zh.md index 1d3d304ca1..90511ff21b 100644 --- a/s2-site/docs/api/basic-class/base-data-set.zh.md +++ b/s2-site/docs/api/basic-class/base-data-set.zh.md @@ -27,9 +27,10 @@ s2.dataSet.getFieldName('type') | getDimensionValues | 获取维值 | (filed: string, query?: [DataType](#datatype) ) => string[] | | | getCellData | 获取单个的单元格数据 | (params: [CellDataParams](#celldataparams)) => [DataType[]](#datatype) | | | getMultiData | 获取批量的单元格数据 | (query: [DataType](#datatype),params?: [MultiDataParams](#multidataparams)) => [DataType[]](#datatype) | | -| getMultiData (已废弃) | 获取批量的单元格数据 | (query: [DataType](#datatype), isTotals?: boolean, isRow?: boolean, drillDownFields?: string[], includeTotalData:boolean) => [DataType[]](#datatype) | | +| getMultiData (已废弃) | 获取批量的单元格数据 | (query: [DataType](#datatype), isTotals?: boolean, isRow?: boolean, drillDownFields?: string[], includeTotalData:boolean) => [DataType[]](#datatype) | | | moreThanOneValue | 是否超过 1 个数值 | () => [ViewMeta](#viewmeta) | | | isEmpty | 是否为空数据集 | () => `boolean` | `@antv/s2-v1.51.1` | +| displayFormattedValueMap | 单元格所对应格式化后的值(用于编辑表) | `Map` | `@antv/s2-v1.54.5` | ### DataType diff --git a/s2-site/docs/api/components/sheet-component.zh.md b/s2-site/docs/api/components/sheet-component.zh.md index d114b3a135..9599998ad6 100644 --- a/s2-site/docs/api/components/sheet-component.zh.md +++ b/s2-site/docs/api/components/sheet-component.zh.md @@ -48,7 +48,8 @@ order: 0 | onDataCellTrendIconClick | 数值单元格的趋势图 icon 点击事件 | (meta: [ViewMeta](/docs/api/basic-class/node)) => void | | | | onDataCellBrushSelection | 数值单元格刷选事件 | ( dataCells: [DataCell](/docs/api/basic-class/base-cell)[] ) => void | | | | onDataCellSelectMove | 数值单元格键盘方向键移动事件 | (metas: CellMeta[]) => void | | | -| onDataCellEditEnd | 数值单元格编辑完成(暂只支持编辑表) | (meta: [ViewMeta](/docs/api/basic-class/node)) => void | | | +| onDataCellEditStart | 数值单元格编辑开始(暂只支持编辑表) | (meta: [ViewMeta](/docs/api/basic-class/node), cell: [S2CellType](/docs/api/basic-class/base-cell)) => void | | | +| onDataCellEditEnd | 数值单元格编辑完成(暂只支持编辑表) | (meta: [ViewMeta](/docs/api/basic-class/node), cell: [S2CellType](/docs/api/basic-class/base-cell)) => void | | | | onCornerCellHover | 角头鼠标悬停事件 | (data: [TargetCellInfo](#targetcellinfo)) => void | | | | onCornerCellClick | 角头鼠标单击事件 | (data: [TargetCellInfo](#targetcellinfo)) => void | | | | onCornerCellDoubleClick | 角头鼠标双击事件 | (data: [TargetCellInfo](#targetcellinfo)) => void | | | diff --git a/s2-site/docs/manual/basic/analysis/editable-mode.zh.md b/s2-site/docs/manual/basic/analysis/editable-mode.zh.md index 813be1d588..165468e54b 100644 --- a/s2-site/docs/manual/basic/analysis/editable-mode.zh.md +++ b/s2-site/docs/manual/basic/analysis/editable-mode.zh.md @@ -167,8 +167,11 @@ ReactDOM.render( sheetType="editable" // 此处指定 sheetType 为 editable dataCfg={s2DataCfg} options={s2Options} - onDataCellEditEnd={(meta) => { - console.log('onDataCellEditEnd', meta); + onDataCellEditStart={(meta, cell) => { + console.log('onDataCellEditStart:', meta, cell); + }} + onDataCellEditEnd={(meta, cell) => { + console.log('onDataCellEditEnd:', meta, cell); }} />, document.getElementById('container') diff --git a/s2-site/examples/react-component/sheet/demo/editable.tsx b/s2-site/examples/react-component/sheet/demo/editable.tsx index fd3025d678..eb053dbfb4 100644 --- a/s2-site/examples/react-component/sheet/demo/editable.tsx +++ b/s2-site/examples/react-component/sheet/demo/editable.tsx @@ -44,8 +44,12 @@ fetch('https://assets.antv.antgroup.com/s2/basic-table-mode.json') frozenTrailingColCount: 1, // 列尾冻结数量 }; - const onDataCellEditEnd = (meta) => { - console.log('onDataCellEditEnd', meta); + const onDataCellEditStart = (meta, cell) => { + console.log('onDataCellEditStart:', meta, cell); + }; + + const onDataCellEditEnd = (meta, cell) => { + console.log('onDataCellEditEnd:', meta, cell); }; ReactDOM.render( @@ -53,6 +57,7 @@ fetch('https://assets.antv.antgroup.com/s2/basic-table-mode.json') dataCfg={s2DataConfig} options={s2Options} sheetType="editable" + onDataCellEditStart={onDataCellEditStart} onDataCellEditEnd={onDataCellEditEnd} />, document.getElementById('container'),