From ac4f5ab9d87bf9bf725b2ef872342a597823ebf6 Mon Sep 17 00:00:00 2001 From: Jinke Li Date: Mon, 5 Aug 2024 17:44:53 +0800 Subject: [PATCH] =?UTF-8?q?feat(interaction):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E9=80=89=E4=B8=AD/=E9=AB=98=E4=BA=AE=E5=8D=95=E5=85=83?= =?UTF-8?q?=E6=A0=BC=20&=20=E6=BB=9A=E5=8A=A8=20API=20(#2586)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(interaction): 新增选中/高亮单元格 & 滚动 API * docs: 补充文档 * feat: 角头增加高亮效果 * test: 修复测试 * docs: 更新隐藏列头文档 * docs: 更新隐藏列头文档 * docs: 完善字段标记文档 * docs: 完善文档 * fix: 修复 hover 后分割线被遮挡的问题 * fix: 优化分割线样式问题 * feat: 支持动画和跳过滚动事件参数 & 动态判断交互重置 hook * test: 新增单测 * docs: 更新文档 * test: 修复单测 * test: 更新快照 --------- Co-authored-by: Wenjun Xu <906626481@qq.com> --- .../s2-core/__tests__/bugs/issue-1561-spec.ts | 8 +- .../s2-core/__tests__/bugs/issue-2199-spec.ts | 6 +- .../s2-core/__tests__/bugs/issue-2340-spec.ts | 12 +- .../s2-core/__tests__/bugs/issue-2804-spec.ts | 2 +- .../__snapshots__/theme-spec.ts.snap | 168 ++++ .../__tests__/spreadsheet/corner-spec.ts | 3 + .../__tests__/spreadsheet/custom-grid-spec.ts | 12 +- .../spreadsheet/custom-table-col-spec.ts | 4 +- .../__tests__/spreadsheet/custom-tree-spec.ts | 6 +- .../interaction-multi-selection-spec.ts | 75 +- .../__tests__/spreadsheet/scroll-spec.ts | 152 ++- .../spreadsheet/spread-sheet-frozen-spec.ts | 20 +- .../spreadsheet/spread-sheet-spec.ts | 8 +- .../__snapshots__/root-spec.ts.snap | 125 +++ .../click/row-column-click-spec.ts | 13 +- .../unit/interaction/event-controller-spec.ts | 17 + .../__tests__/unit/interaction/root-spec.ts | 45 +- .../interaction/selected-cell-move-spec.ts | 7 +- .../unit/sheet-type/pivot-sheet-spec.ts | 2 +- packages/s2-core/src/cell/base-cell.ts | 6 +- packages/s2-core/src/cell/corner-cell.ts | 2 + packages/s2-core/src/cell/data-cell.ts | 11 +- packages/s2-core/src/cell/header-cell.ts | 11 +- .../s2-core/src/common/interface/basic.ts | 15 - .../src/common/interface/interaction.ts | 80 +- .../s2-core/src/common/interface/scroll.ts | 12 + packages/s2-core/src/facet/base-facet.ts | 22 +- packages/s2-core/src/facet/header/frame.ts | 18 +- .../click/corner-cell-click.ts | 5 +- .../click/row-column-click.ts | 5 +- .../src/interaction/base-interaction/hover.ts | 10 +- .../brush-selection/base-brush-selection.ts | 6 +- .../src/interaction/event-controller.ts | 14 +- packages/s2-core/src/interaction/root.ts | 431 ++++++++- .../s2-core/src/sheet-type/spread-sheet.ts | 29 - packages/s2-core/src/theme/index.ts | 36 + .../__snapshots__/utils-spec.ts.snap | 196 ++++ packages/s2-react/playground/config.tsx | 19 +- packages/s2-react/playground/index.tsx | 892 ++++++++++-------- s2-site/docs/api/basic-class/base-cell.zh.md | 8 +- s2-site/docs/api/basic-class/base-facet.en.md | 10 +- s2-site/docs/api/basic-class/base-facet.zh.md | 11 +- .../docs/api/basic-class/interaction.en.md | 8 +- .../docs/api/basic-class/interaction.zh.md | 132 ++- .../docs/api/basic-class/spreadsheet.en.md | 5 +- .../docs/api/basic-class/spreadsheet.zh.md | 5 +- s2-site/docs/api/general/S2Options.zh.md | 16 +- s2-site/docs/common/conditions.zh.md | 98 +- s2-site/docs/common/interaction.zh.md | 2 +- .../docs/manual/advanced/chart-in-cell.zh.md | 42 +- .../manual/advanced/custom/cell-align.zh.md | 4 +- .../manual/advanced/custom/custom-icon.zh.md | 4 + .../docs/manual/advanced/get-cell-data.zh.md | 4 +- .../manual/advanced/interaction/basic.zh.md | 24 +- .../advanced/interaction/hide-columns.zh.md | 55 +- .../highlight-and-select-cell.en.md | 5 + .../highlight-and-select-cell.zh.md | 132 +++ .../manual/advanced/interaction/resize.zh.md | 2 +- .../manual/advanced/interaction/scroll.zh.md | 31 + s2-site/docs/manual/basic/conditions.zh.md | 50 +- .../docs/manual/basic/multi-line-text.zh.md | 2 +- s2-site/docs/manual/contribution.zh.md | 18 + s2-site/docs/manual/migration-v2.zh.md | 80 +- .../conditions/demo/icon-with-action.ts | 1 + .../analysis/conditions/demo/meta.json | 12 +- .../analysis/conditions/demo/table-text.ts | 2 +- .../examples/analysis/conditions/demo/text.ts | 42 +- .../analysis/get-data/demo/get-cell-data.ts | 1 + .../demo/custom-g-shape.ts | 27 + .../interaction/advanced/demo/scroll-loop.ts | 4 +- .../advanced/demo/scroll-to-cell.ts | 47 +- .../basic/demo/auto-reset-sheet-style.ts | 41 +- .../examples/interaction/basic/demo/event.ts | 134 ++- .../examples/interaction/basic/demo/meta.json | 5 +- .../examples/interaction/basic/demo/resize.ts | 2 +- 75 files changed, 2792 insertions(+), 779 deletions(-) create mode 100644 packages/s2-core/__tests__/unit/interaction/__snapshots__/root-spec.ts.snap create mode 100644 s2-site/docs/manual/advanced/interaction/highlight-and-select-cell.en.md create mode 100644 s2-site/docs/manual/advanced/interaction/highlight-and-select-cell.zh.md diff --git a/packages/s2-core/__tests__/bugs/issue-1561-spec.ts b/packages/s2-core/__tests__/bugs/issue-1561-spec.ts index 71cb4a8bc0..01b5db1ec0 100644 --- a/packages/s2-core/__tests__/bugs/issue-1561-spec.ts +++ b/packages/s2-core/__tests__/bugs/issue-1561-spec.ts @@ -25,10 +25,10 @@ describe('Grid Border Tests', () => { const gridGroup = (panelScrollGroup as any).gridGroup as Group; const originalLeftBorderBBox = (gridGroup.children[0] as Group).getBBox(); - s2.facet.updateScrollOffset({ offsetX: { value: 100, animate: false } }); - s2.facet.updateScrollOffset({ offsetX: { value: 200, animate: false } }); - s2.facet.updateScrollOffset({ offsetX: { value: 300, animate: false } }); - s2.facet.updateScrollOffset({ offsetX: { value: 0, animate: false } }); + s2.interaction.scrollTo({ offsetX: { value: 100, animate: false } }); + s2.interaction.scrollTo({ offsetX: { value: 200, animate: false } }); + s2.interaction.scrollTo({ offsetX: { value: 300, animate: false } }); + s2.interaction.scrollTo({ offsetX: { value: 0, animate: false } }); const newLeftBorderBBbox = (gridGroup.children[0] as Group).getBBox(); diff --git a/packages/s2-core/__tests__/bugs/issue-2199-spec.ts b/packages/s2-core/__tests__/bugs/issue-2199-spec.ts index f4bad022cc..c2f5820e2a 100644 --- a/packages/s2-core/__tests__/bugs/issue-2199-spec.ts +++ b/packages/s2-core/__tests__/bugs/issue-2199-spec.ts @@ -5,7 +5,7 @@ */ import type { S2Options } from '@/common/interface'; import { TableSheet } from '@/sheet-type'; -import { getContainer } from 'tests/util/helpers'; +import { getContainer, sleep } from 'tests/util/helpers'; import dataCfg from '../data/data-issue-2199.json'; const s2Options: S2Options = { @@ -25,13 +25,15 @@ describe('ColCell Text Center Tests', () => { await s2.render(); - s2.facet.updateScrollOffset({ + s2.interaction.scrollTo({ offsetX: { value: 500, animate: false, }, }); + await sleep(200); + const node = s2.facet.getColNodes(0).slice(-1)?.[0]; const cell = node?.belongsCell; const { width: nodeWidth, x: nodeX } = node; diff --git a/packages/s2-core/__tests__/bugs/issue-2340-spec.ts b/packages/s2-core/__tests__/bugs/issue-2340-spec.ts index c4d095c4c9..083b5145f8 100644 --- a/packages/s2-core/__tests__/bugs/issue-2340-spec.ts +++ b/packages/s2-core/__tests__/bugs/issue-2340-spec.ts @@ -56,17 +56,19 @@ describe('Header Brush Selection Tests', () => { const offsetKey = isRow ? 'offsetY' : 'offsetX'; // 将圈选的单元格滑出可视范围 - s2.facet.updateScrollOffset({ - [offsetKey]: { value: 300 }, + s2.interaction.scrollTo({ + [offsetKey]: { value: 300, animate: false }, }); - await sleep(500); + await sleep(200); // 还原 - s2.facet.updateScrollOffset({ - [offsetKey]: { value: 0 }, + s2.interaction.scrollTo({ + [offsetKey]: { value: 0, animate: false }, }); + await sleep(200); + expect(s2.interaction.getActiveCells()).toHaveLength(1); expect(s2.interaction.getCurrentStateName()).toEqual(stateName); diff --git a/packages/s2-core/__tests__/bugs/issue-2804-spec.ts b/packages/s2-core/__tests__/bugs/issue-2804-spec.ts index ca2831927d..924fea8031 100644 --- a/packages/s2-core/__tests__/bugs/issue-2804-spec.ts +++ b/packages/s2-core/__tests__/bugs/issue-2804-spec.ts @@ -21,7 +21,7 @@ describe('Tree Leaf Node Status Tests', () => { await s2.render(); - const [a1, a2] = s2.facet.getRowNodes(); + const [a1, a2] = await s2.facet.getRowNodes(); expect(a1.isLeaf).toBeTruthy(); expect(a1.isTotals).toBeFalsy(); diff --git a/packages/s2-core/__tests__/spreadsheet/__snapshots__/theme-spec.ts.snap b/packages/s2-core/__tests__/spreadsheet/__snapshots__/theme-spec.ts.snap index 6572cf2782..281de69170 100644 --- a/packages/s2-core/__tests__/spreadsheet/__snapshots__/theme-spec.ts.snap +++ b/packages/s2-core/__tests__/spreadsheet/__snapshots__/theme-spec.ts.snap @@ -110,6 +110,34 @@ Object { "horizontalBorderColor": "#5286FA", "horizontalBorderColorOpacity": 1, "horizontalBorderWidth": 1, + "interactionState": Object { + "highlight": Object { + "backgroundColor": "#87B5FF", + "backgroundOpacity": 1, + }, + "hover": Object { + "backgroundColor": "#2C60D4", + "backgroundOpacity": 0.6, + }, + "prepareSelect": Object { + "borderColor": "#000000", + "borderOpacity": 1, + "borderWidth": 1, + }, + "searchResult": Object { + "backgroundColor": "#F0F7FF", + "backgroundOpacity": 1, + }, + "selected": Object { + "backgroundColor": "#2C60D4", + "backgroundOpacity": 0.6, + }, + "unselected": Object { + "backgroundOpacity": 0.3, + "opacity": 0.3, + "textOpacity": 0.3, + }, + }, "padding": Object { "bottom": 4, "left": 8, @@ -661,6 +689,34 @@ Object { "horizontalBorderColor": "#0647b1", "horizontalBorderColorOpacity": 1, "horizontalBorderWidth": 1, + "interactionState": Object { + "highlight": Object { + "backgroundColor": "#4b91ff", + "backgroundOpacity": 1, + }, + "hover": Object { + "backgroundColor": "#213f94", + "backgroundOpacity": 0.6, + }, + "prepareSelect": Object { + "borderColor": "#dcdcdc", + "borderOpacity": 1, + "borderWidth": 1, + }, + "searchResult": Object { + "backgroundColor": "#213f94", + "backgroundOpacity": 1, + }, + "selected": Object { + "backgroundColor": "#213f94", + "backgroundOpacity": 0.6, + }, + "unselected": Object { + "backgroundOpacity": 0.3, + "opacity": 0.3, + "textOpacity": 0.3, + }, + }, "padding": Object { "bottom": 4, "left": 8, @@ -1212,6 +1268,34 @@ Object { "horizontalBorderColor": "#CCDBFC", "horizontalBorderColorOpacity": 1, "horizontalBorderWidth": 1, + "interactionState": Object { + "highlight": Object { + "backgroundColor": "#87B5FF", + "backgroundOpacity": 1, + }, + "hover": Object { + "backgroundColor": "#CCDBFC", + "backgroundOpacity": 0.6, + }, + "prepareSelect": Object { + "borderColor": "#000000", + "borderOpacity": 1, + "borderWidth": 1, + }, + "searchResult": Object { + "backgroundColor": "#F0F7FF", + "backgroundOpacity": 1, + }, + "selected": Object { + "backgroundColor": "#CCDBFC", + "backgroundOpacity": 0.6, + }, + "unselected": Object { + "backgroundOpacity": 0.3, + "opacity": 0.3, + "textOpacity": 0.3, + }, + }, "padding": Object { "bottom": 4, "left": 8, @@ -1763,6 +1847,34 @@ Object { "horizontalBorderColor": "#E7E9ED", "horizontalBorderColorOpacity": 1, "horizontalBorderWidth": 1, + "interactionState": Object { + "highlight": Object { + "backgroundColor": "#87B5FF", + "backgroundOpacity": 1, + }, + "hover": Object { + "backgroundColor": "#E7E9ED", + "backgroundOpacity": 0.6, + }, + "prepareSelect": Object { + "borderColor": "#000000", + "borderOpacity": 1, + "borderWidth": 1, + }, + "searchResult": Object { + "backgroundColor": "#F0F7FF", + "backgroundOpacity": 1, + }, + "selected": Object { + "backgroundColor": "#E7E9ED", + "backgroundOpacity": 0.6, + }, + "unselected": Object { + "backgroundOpacity": 0.3, + "opacity": 0.3, + "textOpacity": 0.3, + }, + }, "padding": Object { "bottom": 4, "left": 8, @@ -2314,6 +2426,34 @@ Object { "horizontalBorderColor": "#CCDBFC", "horizontalBorderColorOpacity": 1, "horizontalBorderWidth": 1, + "interactionState": Object { + "highlight": Object { + "backgroundColor": "#87B5FF", + "backgroundOpacity": 1, + }, + "hover": Object { + "backgroundColor": "#CCDBFC", + "backgroundOpacity": 0.6, + }, + "prepareSelect": Object { + "borderColor": "#000000", + "borderOpacity": 1, + "borderWidth": 1, + }, + "searchResult": Object { + "backgroundColor": "#F0F7FF", + "backgroundOpacity": 1, + }, + "selected": Object { + "backgroundColor": "#CCDBFC", + "backgroundOpacity": 0.6, + }, + "unselected": Object { + "backgroundOpacity": 0.3, + "opacity": 0.3, + "textOpacity": 0.3, + }, + }, "padding": Object { "bottom": 4, "left": 8, @@ -2865,6 +3005,34 @@ Object { "horizontalBorderColor": "#CCDBFC", "horizontalBorderColorOpacity": 1, "horizontalBorderWidth": 1, + "interactionState": Object { + "highlight": Object { + "backgroundColor": "#87B5FF", + "backgroundOpacity": 1, + }, + "hover": Object { + "backgroundColor": "#CCDBFC", + "backgroundOpacity": 0.6, + }, + "prepareSelect": Object { + "borderColor": "#000000", + "borderOpacity": 1, + "borderWidth": 1, + }, + "searchResult": Object { + "backgroundColor": "#F0F7FF", + "backgroundOpacity": 1, + }, + "selected": Object { + "backgroundColor": "#CCDBFC", + "backgroundOpacity": 0.6, + }, + "unselected": Object { + "backgroundOpacity": 0.3, + "opacity": 0.3, + "textOpacity": 0.3, + }, + }, "padding": Object { "bottom": 4, "left": 8, diff --git a/packages/s2-core/__tests__/spreadsheet/corner-spec.ts b/packages/s2-core/__tests__/spreadsheet/corner-spec.ts index 0b83b0e131..76a6702471 100644 --- a/packages/s2-core/__tests__/spreadsheet/corner-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/corner-spec.ts @@ -255,6 +255,7 @@ describe('PivotSheet Corner Tests', () => { ...node, cornerType: CornerNodeType.Row, }), + updateByState: jest.fn(), } as unknown as S2CellType; }); const selected = jest.fn(); @@ -293,6 +294,7 @@ describe('PivotSheet Corner Tests', () => { ...node, cornerType: CornerNodeType.Row, }), + updateByState: jest.fn(), } as unknown as S2CellType; }); const selected = jest.fn(); @@ -318,6 +320,7 @@ describe('PivotSheet Corner Tests', () => { ...node, cornerType: CornerNodeType.Row, }), + updateByState: jest.fn(), } as unknown as S2CellType; }); diff --git a/packages/s2-core/__tests__/spreadsheet/custom-grid-spec.ts b/packages/s2-core/__tests__/spreadsheet/custom-grid-spec.ts index f664bc8e7b..d28cd7025a 100644 --- a/packages/s2-core/__tests__/spreadsheet/custom-grid-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/custom-grid-spec.ts @@ -140,7 +140,7 @@ describe('SpreadSheet Custom Grid Tests', () => { const rowNode = s2.facet.getRowNodes()[0]; // 选中 a-1 - s2.interaction.selectHeaderCell({ + s2.interaction.changeCell({ cell: rowNode.belongsCell!, }); @@ -155,7 +155,7 @@ describe('SpreadSheet Custom Grid Tests', () => { ]); // 取消选中 a - 1 - s2.interaction.selectHeaderCell({ + s2.interaction.changeCell({ cell: rowNode.belongsCell!, }); expect(s2.interaction.getActiveCells()).toBeEmpty(); @@ -176,7 +176,7 @@ describe('SpreadSheet Custom Grid Tests', () => { .find((node) => node.field === field)!; // 选中 - s2.interaction.selectHeaderCell({ + s2.interaction.changeCell({ cell: rowNode.belongsCell!, }); @@ -338,7 +338,7 @@ describe('SpreadSheet Custom Grid Tests', () => { const colNode = s2.facet.getColNodes()[0]; // 选中 a-1 - s2.interaction.selectHeaderCell({ + s2.interaction.changeCell({ cell: colNode.belongsCell!, }); @@ -353,7 +353,7 @@ describe('SpreadSheet Custom Grid Tests', () => { ]); // 取消选中 a - 1 - s2.interaction.selectHeaderCell({ + s2.interaction.changeCell({ cell: colNode.belongsCell!, }); expect(s2.interaction.getActiveCells()).toBeEmpty(); @@ -374,7 +374,7 @@ describe('SpreadSheet Custom Grid Tests', () => { .find((node) => node.field === field)!; // 选中 - s2.interaction.selectHeaderCell({ + s2.interaction.changeCell({ cell: colNode.belongsCell!, }); diff --git a/packages/s2-core/__tests__/spreadsheet/custom-table-col-spec.ts b/packages/s2-core/__tests__/spreadsheet/custom-table-col-spec.ts index 71e247ad4e..f1ebace5cc 100644 --- a/packages/s2-core/__tests__/spreadsheet/custom-table-col-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/custom-table-col-spec.ts @@ -118,7 +118,7 @@ describe('TableSheet Custom Tests', () => { const colNode = s2.facet.getColNodes()[0]; // 选中地区 - s2.interaction.selectHeaderCell({ + s2.interaction.changeCell({ cell: colNode.belongsCell!, }); @@ -131,7 +131,7 @@ describe('TableSheet Custom Tests', () => { ]); // 取消选中 a - 1 - s2.interaction.selectHeaderCell({ + s2.interaction.changeCell({ cell: colNode.belongsCell!, }); expect(s2.interaction.getActiveCells()).toBeEmpty(); diff --git a/packages/s2-core/__tests__/spreadsheet/custom-tree-spec.ts b/packages/s2-core/__tests__/spreadsheet/custom-tree-spec.ts index f3620ccf6f..2c93df3010 100644 --- a/packages/s2-core/__tests__/spreadsheet/custom-tree-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/custom-tree-spec.ts @@ -118,7 +118,7 @@ describe('SpreadSheet Custom Tree Tests', () => { const rowNode = s2.facet.getRowNodes()[0]; // 选中 a-1 - s2.interaction.selectHeaderCell({ + s2.interaction.changeCell({ cell: rowNode.belongsCell!, }); @@ -128,7 +128,7 @@ describe('SpreadSheet Custom Tree Tests', () => { expectHighlightActiveNodes(s2, ['root[&]a-1']); // 取消选中 a-1 - s2.interaction.selectHeaderCell({ + s2.interaction.changeCell({ cell: rowNode.belongsCell!, }); expect(s2.interaction.getActiveCells()).toBeEmpty(); @@ -149,7 +149,7 @@ describe('SpreadSheet Custom Tree Tests', () => { .find((node) => node.field === field)!; // 选中 - s2.interaction.selectHeaderCell({ + s2.interaction.changeCell({ cell: rowNode.belongsCell!, }); 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 e1653d5431..1f75fc8acb 100644 --- a/packages/s2-core/__tests__/spreadsheet/interaction-multi-selection-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/interaction-multi-selection-spec.ts @@ -1,3 +1,4 @@ +/* eslint-disable jest/expect-expect */ import type { HierarchyType, S2Options } from '@/common/interface'; import { PivotSheet, SpreadSheet } from '@/sheet-type'; import * as mockDataConfig from 'tests/data/simple-data.json'; @@ -7,7 +8,7 @@ import { getContainer, sleep, } from 'tests/util/helpers'; -import { CellType, InteractionStateName } from '../../src'; +import { CellType, InteractionStateName, RootInteraction } from '../../src'; import { expectHighlightActiveNodes, getSelectedCount, @@ -34,6 +35,14 @@ const highlightCellConfig: Array<{ ]; describe('Interaction Multi Selection Tests', () => { + const config: Array<{ + method: keyof typeof RootInteraction.prototype; + stateName: InteractionStateName; + }> = [ + { method: 'selectCell', stateName: InteractionStateName.SELECTED }, + { method: 'highlightCell', stateName: InteractionStateName.HOVER }, + ]; + let s2: SpreadSheet; beforeEach(async () => { @@ -46,7 +55,7 @@ describe('Interaction Multi Selection Tests', () => { }); afterEach(() => { - s2.destroy(); + // s2.destroy(); }); // https://github.com/antvis/S2/issues/1306 @@ -59,7 +68,7 @@ describe('Interaction Multi Selection Tests', () => { const colRootCell = s2.facet.getColCells()[0]; // 选中 - s2.interaction.selectHeaderCell({ + s2.interaction.changeCell({ cell: colRootCell, }); @@ -81,7 +90,7 @@ describe('Interaction Multi Selection Tests', () => { expectHighlightActiveNodes(s2, ['root[&]笔[&]price', 'root[&]笔[&]cost']); // 取消选中 - s2.interaction.selectHeaderCell({ + s2.interaction.changeCell({ cell: colRootCell, }); @@ -108,7 +117,7 @@ describe('Interaction Multi Selection Tests', () => { const rowRootCell = s2.facet.getRowCells()[0]; - s2.interaction.selectHeaderCell({ + s2.interaction.changeCell({ cell: rowRootCell, }); @@ -144,7 +153,7 @@ describe('Interaction Multi Selection Tests', () => { const colRootCell = s2.facet.getColCells()[0]; - s2.interaction.selectHeaderCell({ + s2.interaction.changeCell({ cell: colRootCell, }); @@ -178,13 +187,13 @@ describe('Interaction Multi Selection Tests', () => { const colRootCell = pivotSheet.facet.getColCells()[0]; - pivotSheet.interaction.selectHeaderCell({ + pivotSheet.interaction.changeCell({ cell: colRootCell, }); await sleep(100); - pivotSheet.updateScrollOffset({ + pivotSheet.interaction.scrollTo({ offsetX: { value: 100, animate: true, @@ -209,6 +218,56 @@ describe('Interaction Multi Selection Tests', () => { }); }); + test.each(config)('should %o for root row cell', ({ method, stateName }) => { + // @ts-ignore + s2.interaction[method](s2.facet.getRowCells()[0]); + + expectHighlightActiveNodes(s2, [ + 'root[&]浙江[&]义乌', + 'root[&]浙江[&]杭州', + ]); + + expect(s2.interaction.getCurrentStateName()).toEqual(stateName); + }); + + test.each(config)('should %o for leaf row cell', ({ method, stateName }) => { + // @ts-ignore + s2.interaction[method](s2.facet.getRowLeafCells()[0]); + + expectHighlightActiveNodes(s2, ['root[&]浙江[&]义乌']); + expect(s2.interaction.getCurrentStateName()).toEqual(stateName); + }); + + test.each(config)( + 'should %o for root row cell by tree mode', + async ({ method, stateName }) => { + s2.setOptions({ hierarchyType: 'tree' }); + await s2.render(false); + + // @ts-ignore + s2.interaction[method](s2.facet.getRowCells()[0]); + + expectHighlightActiveNodes(s2, ['root[&]浙江']); + expect(s2.interaction.getCurrentStateName()).toEqual(stateName); + }, + ); + + test.each(config)('should %o for root col cell', ({ method, stateName }) => { + // @ts-ignore + s2.interaction[method](s2.facet.getColCells()[0]); + + expectHighlightActiveNodes(s2, ['root[&]笔[&]price', 'root[&]笔[&]cost']); + expect(s2.interaction.getCurrentStateName()).toEqual(stateName); + }); + + test.each(config)('should %o for leaf col cell', ({ method, stateName }) => { + // @ts-ignore + s2.interaction[method](s2.facet.getColLeafCells()[0]); + + expectHighlightActiveNodes(s2, ['root[&]笔[&]price']); + expect(s2.interaction.getCurrentStateName()).toEqual(stateName); + }); + test.each(highlightCellConfig)( 'should highlight relevancy header cell after selected data cell by %s mode', async ({ hierarchyType, stateName }) => { diff --git a/packages/s2-core/__tests__/spreadsheet/scroll-spec.ts b/packages/s2-core/__tests__/spreadsheet/scroll-spec.ts index 755742254f..8d7fe29277 100644 --- a/packages/s2-core/__tests__/spreadsheet/scroll-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/scroll-spec.ts @@ -14,7 +14,7 @@ import type { S2Options, } from '@/common/interface'; import { PivotSheet, SpreadSheet } from '@/sheet-type'; -import { cloneDeep, get } from 'lodash'; +import { cloneDeep, get, last } from 'lodash'; import * as mockDataConfig from 'tests/data/simple-data.json'; import { createMockCellInfo, getContainer, sleep } from 'tests/util/helpers'; import { ScrollBar, ScrollType } from '../../src/ui/scrollbar'; @@ -73,7 +73,6 @@ describe('Scroll Tests', () => { afterEach(() => { s2.destroy(); - canvas.remove(); }); test('should hide tooltip when start scroll', () => { @@ -534,7 +533,7 @@ describe('Scroll Tests', () => { s2.changeSheetSize(200, 200); // 显示横/竖滚动条 await s2.render(false); - s2.updateScrollOffset({ + s2.interaction.scrollTo({ offsetX: { value: 999, }, @@ -543,6 +542,8 @@ describe('Scroll Tests', () => { }, }); + await sleep(500); + const { hScrollBar, vScrollBar, panelBBox } = s2.facet; expect( @@ -916,7 +917,7 @@ describe('Scroll Tests', () => { const colCell = s2.facet.getColLeafCells()[0]!; s2.on(S2Event.GLOBAL_RESET, reset); - s2.interaction.selectHeaderCell({ + s2.interaction.changeCell({ cell: colCell, }); @@ -950,6 +951,108 @@ describe('Scroll Tests', () => { }, ); + test('should scroll to custom offset', async () => { + const value = 20; + + s2.interaction.scrollTo({ + offsetX: { + value, + animate: false, + }, + offsetY: { + value, + animate: false, + }, + rowHeaderOffsetX: { + value, + animate: false, + }, + }); + + await sleep(500); + + expect(Math.floor(s2.facet.hScrollBar.thumbOffset)).toBeCloseTo(9); + expect(Math.floor(s2.facet.vScrollBar.thumbOffset)).toBeCloseTo(14); + expect(Math.floor(s2.facet.hRowScrollBar.thumbOffset)).toBeCloseTo(10); + }); + + test('should scroll to cell by id', async () => { + s2.interaction.scrollToCellById('root[&]浙江[&]杭州-root[&]笔[&]price'); + + await sleep(500); + + expect(s2.facet.hScrollBar.thumbOffset).toBeCloseTo(0); + expect(Math.floor(s2.facet.vScrollBar.thumbOffset)).toBeCloseTo(20); + }); + + test('should scroll to cell by id when cell outside of viewport', async () => { + s2.interaction.scrollToCellById('root[&]浙江[&]杭州'); + + await sleep(500); + + expect(Math.floor(s2.facet.hRowScrollBar.thumbOffset)).toBeCloseTo(52); + }); + + test('should scroll to cell', async () => { + const dataCell = last(s2.facet.getDataCells()); + + s2.interaction.scrollToCell(dataCell!); + + await sleep(500); + + expect(s2.facet.hScrollBar.thumbOffset).toBeCloseTo(0); + expect(Math.floor(s2.facet.vScrollBar.thumbOffset)).toBeCloseTo(20); + }); + + test('should scroll to node', async () => { + const rowNode = last(s2.facet.getRowNodes()); + + s2.interaction.scrollToNode(rowNode!); + + await sleep(500); + + expect(s2.facet.hScrollBar.thumbOffset).toBeCloseTo(49); + expect(Math.floor(s2.facet.vScrollBar.thumbOffset)).toBeCloseTo(20); + }); + + test('should scroll to top', async () => { + s2.interaction.scrollTo({ offsetY: { value: 10 } }); + await sleep(100); + + s2.interaction.scrollToTop(); + await sleep(500); + expect(s2.facet.hScrollBar.thumbOffset).toBeCloseTo(0); + expect(s2.facet.vScrollBar.thumbOffset).toBeCloseTo(0); + }); + + test('should scroll to left', async () => { + s2.interaction.scrollTo({ offsetX: { value: 10 } }); + await sleep(100); + + s2.interaction.scrollToLeft(); + await sleep(500); + expect(s2.facet.hScrollBar.thumbOffset).toBeCloseTo(0); + expect(s2.facet.vScrollBar.thumbOffset).toBeCloseTo(0); + }); + + test('should scroll to bottom', async () => { + s2.interaction.scrollToBottom(false); + + await sleep(500); + + expect(s2.facet.hScrollBar.thumbOffset).toBeCloseTo(0); + expect(Math.floor(s2.facet.vScrollBar.thumbOffset)).toBeCloseTo(20); + }); + + test('should scroll to right', async () => { + s2.interaction.scrollToRight(false); + + await sleep(500); + + expect(s2.facet.vScrollBar.thumbOffset).toBeCloseTo(0); + expect(Math.floor(s2.facet.hScrollBar.thumbOffset)).toBeCloseTo(49); + }); + test('should not trigger scroll event when first rendered', () => { const expectScroll = getScrollExpect(); @@ -975,4 +1078,45 @@ describe('Scroll Tests', () => { expectScroll(); }); + + test('should skip scroll event after scroll end', async () => { + const onScroll = jest.fn(); + const onRowScroll = jest.fn(); + + s2.on(S2Event.GLOBAL_SCROLL, onScroll); + s2.on(S2Event.ROW_CELL_SCROLL, onRowScroll); + + await s2.render(false); + + s2.interaction.scrollTo({ + skipScrollEvent: true, + rowHeaderOffsetX: { + value: 999, + }, + offsetX: { + value: 999, + }, + offsetY: { + value: 999, + }, + }); + + s2.interaction.scrollToLeft({ + skipScrollEvent: true, + }); + s2.interaction.scrollToTop({ + skipScrollEvent: true, + }); + s2.interaction.scrollToRight({ + skipScrollEvent: true, + }); + s2.interaction.scrollToBottom({ + skipScrollEvent: true, + }); + + await sleep(500); + + expect(onScroll).not.toHaveBeenCalled(); + expect(onRowScroll).not.toHaveBeenCalled(); + }); }); diff --git a/packages/s2-core/__tests__/spreadsheet/spread-sheet-frozen-spec.ts b/packages/s2-core/__tests__/spreadsheet/spread-sheet-frozen-spec.ts index 601fbf1660..052b89013a 100644 --- a/packages/s2-core/__tests__/spreadsheet/spread-sheet-frozen-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/spread-sheet-frozen-spec.ts @@ -8,6 +8,7 @@ import { type FrozenFacet, type S2DataConfig, type S2Options, + type SpreadSheet, } from '../../src'; import { pickMap } from '../util/fp'; @@ -43,6 +44,14 @@ function getFrozenGroupPosition( describe('Spread Sheet Frozen Tests', () => { let container: HTMLElement; + const scrollX = (s2: SpreadSheet, value: number) => { + s2.interaction.scrollTo({ + offsetX: { value, animate: false }, + offsetY: { value, animate: false }, + rowHeaderOffsetX: { value, animate: false }, + }); + }; + beforeEach(() => { container = getContainer(); }); @@ -465,7 +474,7 @@ describe('Spread Sheet Frozen Tests', () => { const prev = getFrozenGroupPosition(s2, 'columnHeader'); - s2.updateScrollOffset({ offsetX: { value: 100, animate: false } }); + scrollX(s2, 100); // 移动后,frozen col 和 trailing col 的位置都不变 expect(getFrozenGroupPosition(s2, 'columnHeader')).toEqual(prev); @@ -502,7 +511,8 @@ describe('Spread Sheet Frozen Tests', () => { expectFrozenGroup(s2, 'columnHeader'); let prev = getFrozenGroupPosition(s2, 'columnHeader'); - s2.updateScrollOffset({ offsetX: { value: 100, animate: false } }); + scrollX(s2, 100); + // 移动后,frozen col 会改变 而 trailing col 的位置不变 let current = getFrozenGroupPosition(s2, 'columnHeader'); @@ -511,10 +521,12 @@ describe('Spread Sheet Frozen Tests', () => { // 移动超过角头宽度 // 移动后,frozen col 和 trailing col 的位置都不变 - s2.updateScrollOffset({ offsetX: { value: 300, animate: false } }); + scrollX(s2, 300); prev = getFrozenGroupPosition(s2, 'columnHeader'); - s2.updateScrollOffset({ offsetX: { value: 300, animate: false } }); + + scrollX(s2, 300); + current = getFrozenGroupPosition(s2, 'columnHeader'); expect(current).toEqual(prev); diff --git a/packages/s2-core/__tests__/spreadsheet/spread-sheet-spec.ts b/packages/s2-core/__tests__/spreadsheet/spread-sheet-spec.ts index 95b7bfcda6..f3d65ec151 100644 --- a/packages/s2-core/__tests__/spreadsheet/spread-sheet-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/spread-sheet-spec.ts @@ -154,7 +154,7 @@ describe('SpreadSheet Tests', () => { expect(s2.facet.hScrollBar.current()).toEqual(0); - s2.updateScrollOffset({ + s2.interaction.scrollTo({ offsetX: { value: 30 }, }); await sleep(500); @@ -181,7 +181,7 @@ describe('SpreadSheet Tests', () => { await s2.render(); - s2.updateScrollOffset({ + s2.interaction.scrollTo({ offsetY: { value: 20 }, }); @@ -218,7 +218,7 @@ describe('SpreadSheet Tests', () => { } `); - s2.updateScrollOffset({ + s2.interaction.scrollTo({ rowHeaderOffsetX: { value: 30 }, }); @@ -248,7 +248,7 @@ describe('SpreadSheet Tests', () => { await s2.render(); - s2.updateScrollOffset({ + s2.interaction.scrollTo({ offsetY: { value: 20 }, offsetX: { value: 30 }, rowHeaderOffsetX: { value: 40 }, diff --git a/packages/s2-core/__tests__/unit/interaction/__snapshots__/root-spec.ts.snap b/packages/s2-core/__tests__/unit/interaction/__snapshots__/root-spec.ts.snap new file mode 100644 index 0000000000..de6f9b9a77 --- /dev/null +++ b/packages/s2-core/__tests__/unit/interaction/__snapshots__/root-spec.ts.snap @@ -0,0 +1,125 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RootInteraction Tests should highlight header cell 1`] = ` +Object { + "cells": Array [ + Object { + "colIndex": 0, + "id": "0-0", + "rowIndex": 1, + "rowQuery": undefined, + "type": "dataCell", + }, + ], + "nodes": Array [ + Object { + "colIndex": 0, + "id": "0-0", + "rowIndex": 1, + "x": 1, + }, + ], + "stateName": "hover", +} +`; + +exports[`RootInteraction Tests should selected header cell 1`] = ` +Object { + "cells": Array [ + Object { + "colIndex": 0, + "id": "0-0", + "rowIndex": 1, + "rowQuery": undefined, + "type": "dataCell", + }, + ], + "nodes": Array [ + Object { + "colIndex": 0, + "id": "0-0", + "rowIndex": 1, + "x": 1, + }, + ], + "stateName": "selected", +} +`; + +exports[`RootInteraction Tests should set all selected interaction state correct 1`] = ` +Object { + "cells": Array [ + Object { + "colIndex": 0, + "id": "0-0", + "rowIndex": 1, + "rowQuery": undefined, + "type": "dataCell", + }, + Object { + "colIndex": 1, + "id": "0-1", + "rowIndex": 1, + "rowQuery": undefined, + "type": "dataCell", + }, + Object { + "colIndex": 2, + "id": "0-2", + "rowIndex": 1, + "rowQuery": undefined, + "type": "dataCell", + }, + Object { + "colIndex": 3, + "id": "0-3", + "rowIndex": 1, + "rowQuery": undefined, + "type": "dataCell", + }, + Object { + "colIndex": 4, + "id": "0-4", + "rowIndex": 1, + "rowQuery": undefined, + "type": "dataCell", + }, + Object { + "colIndex": 5, + "id": "0-5", + "rowIndex": 1, + "rowQuery": undefined, + "type": "dataCell", + }, + Object { + "colIndex": 6, + "id": "0-6", + "rowIndex": 1, + "rowQuery": undefined, + "type": "dataCell", + }, + Object { + "colIndex": 7, + "id": "0-7", + "rowIndex": 1, + "rowQuery": undefined, + "type": "dataCell", + }, + Object { + "colIndex": 8, + "id": "0-8", + "rowIndex": 1, + "rowQuery": undefined, + "type": "dataCell", + }, + Object { + "colIndex": 9, + "id": "0-9", + "rowIndex": 1, + "rowQuery": undefined, + "type": "dataCell", + }, + ], + "stateName": "allSelected", +} +`; diff --git a/packages/s2-core/__tests__/unit/interaction/base-interaction/click/row-column-click-spec.ts b/packages/s2-core/__tests__/unit/interaction/base-interaction/click/row-column-click-spec.ts index 979e948ca6..51a3cf4ade 100644 --- a/packages/s2-core/__tests__/unit/interaction/base-interaction/click/row-column-click-spec.ts +++ b/packages/s2-core/__tests__/unit/interaction/base-interaction/click/row-column-click-spec.ts @@ -192,8 +192,8 @@ describe('Interaction Row & Column Cell Click Tests', () => { s2.on(S2Event.GLOBAL_LINK_FIELD_JUMP, linkFieldJump); - const selectHeaderCellSpy = jest - .spyOn(s2.interaction, 'selectHeaderCell') + const changeCellSpy = jest + .spyOn(s2.interaction, 'changeCell') .mockImplementationOnce(() => undefined); const mockCellData = { @@ -218,7 +218,7 @@ describe('Interaction Row & Column Cell Click Tests', () => { expect(linkFieldJump).toHaveBeenCalledTimes(1); expect(s2.showTooltipWithInfo).not.toHaveBeenCalled(); expect(s2.showTooltip).not.toHaveBeenCalled(); - expect(selectHeaderCellSpy).not.toHaveBeenCalled(); + expect(changeCellSpy).not.toHaveBeenCalled(); }); test.each([S2Event.ROW_CELL_CLICK, S2Event.COL_CELL_CLICK])( @@ -261,8 +261,8 @@ describe('Interaction Row & Column Cell Click Tests', () => { ({ event, enableMultiSelection, result }) => { s2.options.interaction!.multiSelection = enableMultiSelection; - const selectHeaderCellSpy = jest - .spyOn(s2.interaction, 'selectHeaderCell') + const changeCellSpy = jest + .spyOn(s2.interaction, 'changeCell') .mockImplementation(() => true); Object.defineProperty(rowColumnClick, 'isMultiSelection', { @@ -274,9 +274,10 @@ describe('Interaction Row & Column Cell Click Tests', () => { stopPropagation() {}, } as unknown as GEvent); - expect(selectHeaderCellSpy).toHaveBeenCalledWith({ + expect(changeCellSpy).toHaveBeenCalledWith({ cell: expect.anything(), isMultiSelection: result, + scrollIntoView: false, }); }, ); diff --git a/packages/s2-core/__tests__/unit/interaction/event-controller-spec.ts b/packages/s2-core/__tests__/unit/interaction/event-controller-spec.ts index a00201f9c1..b21d5bc354 100644 --- a/packages/s2-core/__tests__/unit/interaction/event-controller-spec.ts +++ b/packages/s2-core/__tests__/unit/interaction/event-controller-spec.ts @@ -766,6 +766,23 @@ describe('Interaction Event Controller Tests', () => { expect(spreadsheet.interaction.reset).not.toHaveBeenCalled(); }); + test('should disable reset if mouse outside the canvas and autoResetSheetStyle func set to false', () => { + spreadsheet.setOptions({ + interaction: { + autoResetSheetStyle: () => false, + }, + }); + + spreadsheet.container.dispatchEvent( + new CustomEvent( + OriginEventType.MOUSE_OUT, + getClientPointOnCanvas(spreadsheet.container, -10, -10), + ), + ); + + expect(spreadsheet.interaction.reset).not.toHaveBeenCalled(); + }); + test('should disable reset if mouse outside the canvas and action tooltip is active', () => { spreadsheet.interaction.addIntercepts([InterceptType.HOVER]); diff --git a/packages/s2-core/__tests__/unit/interaction/root-spec.ts b/packages/s2-core/__tests__/unit/interaction/root-spec.ts index 47c42b6d9e..9f56b7e4cf 100644 --- a/packages/s2-core/__tests__/unit/interaction/root-spec.ts +++ b/packages/s2-core/__tests__/unit/interaction/root-spec.ts @@ -162,13 +162,15 @@ describe('RootInteraction Tests', () => { test('should set all selected interaction state correct', () => { rootInteraction.selectAll(); - expect(rootInteraction.getState()).toEqual({ - stateName: InteractionStateName.ALL_SELECTED, - }); + + expect(rootInteraction.getCurrentStateName()).toEqual( + InteractionStateName.ALL_SELECTED, + ); + expect(rootInteraction.getState()).toMatchSnapshot(); }); test('should set header cell selected interaction state correct', () => { - rootInteraction.selectHeaderCell({ cell: mockCell }); + rootInteraction.changeCell({ cell: mockCell }); const state = rootInteraction.getState(); expect(state.stateName).toEqual(InteractionStateName.SELECTED); @@ -176,6 +178,20 @@ describe('RootInteraction Tests', () => { expect(rootInteraction.hasIntercepts([InterceptType.HOVER])).toBeTruthy(); }); + test('should selected header cell', () => { + rootInteraction.selectCell(mockCell); + + expect(rootInteraction.getState()).toMatchSnapshot(); + expect(rootInteraction.hasIntercepts([InterceptType.HOVER])).toBeTruthy(); + }); + + test('should highlight header cell', () => { + rootInteraction.highlightCell(mockCell); + + expect(rootInteraction.getState()).toMatchSnapshot(); + expect(rootInteraction.hasIntercepts([InterceptType.HOVER])).toBeTruthy(); + }); + // https://github.com/antvis/S2/issues/1243 test('should multi selected header cells', () => { jest @@ -186,7 +202,7 @@ describe('RootInteraction Tests', () => { const mockCellB = createMockCellInfo('test-B').mockCell; // 选中 cellA - rootInteraction.selectHeaderCell({ + rootInteraction.changeCell({ cell: mockCellA, isMultiSelection: true, }); @@ -194,7 +210,7 @@ describe('RootInteraction Tests', () => { expect(rootInteraction.getState().cells).toEqual([getCellMeta(mockCellA)]); // 选中 cellB - rootInteraction.selectHeaderCell({ + rootInteraction.changeCell({ cell: mockCellB, isMultiSelection: true, }); @@ -205,7 +221,7 @@ describe('RootInteraction Tests', () => { ]); // 再次选中 cellB - rootInteraction.selectHeaderCell({ + rootInteraction.changeCell({ cell: mockCellB, isMultiSelection: true, }); @@ -226,14 +242,14 @@ describe('RootInteraction Tests', () => { cellType: CellType.COL_CELL, }).mockCell; - rootInteraction.selectHeaderCell({ + rootInteraction.changeCell({ cell: mockCellA, isMultiSelection: true, }); expect(rootInteraction.getState().cells).toEqual([getCellMeta(mockCellA)]); - rootInteraction.selectHeaderCell({ + rootInteraction.changeCell({ cell: mockCellB, isMultiSelection: true, }); @@ -450,17 +466,18 @@ describe('RootInteraction Tests', () => { [mockNodeA, mockNodeB].forEach((node) => { expect(node.belongsCell?.updateByState).toHaveBeenCalledWith( - InteractionStateName.SELECTED, + InteractionStateName.HOVER, belongsCell, ); }); }); test.each` - stateName | handler - ${InteractionStateName.SELECTED} | ${'isSelectedState'} - ${InteractionStateName.HOVER} | ${'isHoverState'} - ${InteractionStateName.HOVER_FOCUS} | ${'isHoverFocusState'} + stateName | handler + ${InteractionStateName.SELECTED} | ${'isSelectedState'} + ${InteractionStateName.ALL_SELECTED} | ${'isAllSelectedState'} + ${InteractionStateName.HOVER} | ${'isHoverState'} + ${InteractionStateName.HOVER_FOCUS} | ${'isHoverFocusState'} `('should get correctly %s state', ({ stateName, handler }) => { rootInteraction.changeState({ cells: [getCellMeta(mockCell)], diff --git a/packages/s2-core/__tests__/unit/interaction/selected-cell-move-spec.ts b/packages/s2-core/__tests__/unit/interaction/selected-cell-move-spec.ts index d00a09534d..70bca6d135 100644 --- a/packages/s2-core/__tests__/unit/interaction/selected-cell-move-spec.ts +++ b/packages/s2-core/__tests__/unit/interaction/selected-cell-move-spec.ts @@ -3,7 +3,10 @@ import { InteractionKeyboardKey, S2Event, } from '@/common/constant'; -import type { InternalFullyTheme, OffsetConfig } from '@/common/interface'; +import type { + InternalFullyTheme, + ScrollOffsetConfig, +} from '@/common/interface'; import { SelectedCellMove } from '@/interaction/selected-cell-move'; import type { SpreadSheet } from '@/sheet-type'; import { createFakeSpreadSheet, createMockCellInfo } from 'tests/util/helpers'; @@ -48,7 +51,7 @@ describe('Interaction Keyboard Move Tests', () => { { x: 1, id: '1', colIndex: 1 }, ], getTotalHeightForRange: () => 0, - scrollWithAnimation: (data: OffsetConfig) => { + scrollWithAnimation: (data: ScrollOffsetConfig) => { s2.store.set('scrollX', data?.offsetX?.value!); s2.store.set('scrollY', data?.offsetY?.value!); }, diff --git a/packages/s2-core/__tests__/unit/sheet-type/pivot-sheet-spec.ts b/packages/s2-core/__tests__/unit/sheet-type/pivot-sheet-spec.ts index 40543a1ea8..8c6a9bf4d6 100644 --- a/packages/s2-core/__tests__/unit/sheet-type/pivot-sheet-spec.ts +++ b/packages/s2-core/__tests__/unit/sheet-type/pivot-sheet-spec.ts @@ -667,7 +667,7 @@ describe('PivotSheet Tests', () => { .spyOn(s2.facet, 'updateScrollOffset') .mockImplementation(() => {}); - s2.updateScrollOffset({}); + s2.interaction.scrollTo({}); expect(updateScrollOffsetSpy).toHaveReturnedTimes(1); }); diff --git a/packages/s2-core/src/cell/base-cell.ts b/packages/s2-core/src/cell/base-cell.ts index c4f6e1f76d..64c4992e16 100644 --- a/packages/s2-core/src/cell/base-cell.ts +++ b/packages/s2-core/src/cell/base-cell.ts @@ -559,7 +559,7 @@ export abstract class BaseCell extends Group { } // 根据当前 state 来更新 cell 的样式 - public updateByState(stateName: InteractionStateName, cell: S2CellType) { + public updateByState(stateName: `${InteractionStateName}`, cell: S2CellType) { this.spreadsheet.interaction.setInteractedCells(cell); const stateStyles = get( this.theme, @@ -580,7 +580,9 @@ export abstract class BaseCell extends Group { (this[shapeName] as DisplayObject); // 兼容多列文本 (MultiData) - const shapes = !isArray(shapeGroup) ? [shapeGroup] : shapeGroup; + const shapes = ( + !isArray(shapeGroup) ? [shapeGroup] : shapeGroup + ) as DisplayObject[]; // stateShape 默认 visible 为 false if (isStateShape) { diff --git a/packages/s2-core/src/cell/corner-cell.ts b/packages/s2-core/src/cell/corner-cell.ts index a425562990..240be80c55 100644 --- a/packages/s2-core/src/cell/corner-cell.ts +++ b/packages/s2-core/src/cell/corner-cell.ts @@ -45,6 +45,8 @@ export class CornerCell extends HeaderCell { super.initCell(); this.resetTextAndConditionIconShapes(); this.drawBackgroundShape(); + this.drawInteractiveBgShape(); + this.drawInteractiveBorderShape(); this.drawTextShape(); this.drawTreeIcon(); this.drawActionAndConditionIcons(); diff --git a/packages/s2-core/src/cell/data-cell.ts b/packages/s2-core/src/cell/data-cell.ts index 60daabb41e..1d17bc9fa8 100644 --- a/packages/s2-core/src/cell/data-cell.ts +++ b/packages/s2-core/src/cell/data-cell.ts @@ -126,7 +126,7 @@ export class DataCell extends BaseCell { protected handleByStateName( cells: CellMeta[], - stateName: InteractionStateName, + stateName: `${InteractionStateName}`, ) { if (includeCell(cells, this)) { this.updateByState(stateName); @@ -227,8 +227,6 @@ export class DataCell extends BaseCell { public update() { const stateName = this.spreadsheet.interaction.getCurrentStateName(); - // 获取当前 interaction 记录的 Cells 元信息列表,不仅仅是数据单元格,也可能是行头或者列头。 - const cells = this.spreadsheet.interaction.getCells(); if (stateName === InteractionStateName.ALL_SELECTED) { this.updateByState(InteractionStateName.SELECTED); @@ -236,6 +234,9 @@ export class DataCell extends BaseCell { return; } + // 获取当前 interaction 记录的 Cells 元信息列表,不仅仅是数据单元格,也可能是行头或者列头。 + const cells = this.spreadsheet.interaction.getCells(); + if (isEmpty(cells) || !stateName) { return; } @@ -454,7 +455,7 @@ export class DataCell extends BaseCell { // 明细表模式多级表头计算索引换一种策略 if (this.spreadsheet.isTableMode() && nodes.length) { - const leafs = nodes[0].hierarchy.getLeaves(); + const leafs = nodes[0]?.hierarchy?.getLeaves() || []; isEqualIndex = leafs.some((cell, i) => { if (nodes.some((node) => node === cell)) { @@ -518,7 +519,7 @@ export class DataCell extends BaseCell { return condition.mapping(value, rowDataInfo as RawData, this); } - public updateByState(stateName: InteractionStateName) { + public updateByState(stateName: `${InteractionStateName}`) { super.updateByState(stateName, this); if (stateName === InteractionStateName.UNSELECTED) { diff --git a/packages/s2-core/src/cell/header-cell.ts b/packages/s2-core/src/cell/header-cell.ts index e06667c7bc..ffe4864ff7 100644 --- a/packages/s2-core/src/cell/header-cell.ts +++ b/packages/s2-core/src/cell/header-cell.ts @@ -348,7 +348,7 @@ export abstract class HeaderCell< protected handleByStateName( cells: CellMeta[], - stateName: InteractionStateName, + stateName: `${InteractionStateName}`, ) { if (includeCell(cells, this)) { this.updateByState(stateName); @@ -445,6 +445,13 @@ export abstract class HeaderCell< public update() { const { interaction } = this.spreadsheet; const stateInfo = interaction?.getState(); + + if (stateInfo?.stateName === InteractionStateName.ALL_SELECTED) { + this.updateByState(InteractionStateName.SELECTED); + + return; + } + const cells = interaction?.getCells([ CellType.CORNER_CELL, CellType.COL_CELL, @@ -475,7 +482,7 @@ export abstract class HeaderCell< } } - public updateByState(stateName: InteractionStateName) { + public updateByState(stateName: `${InteractionStateName}`) { super.updateByState(stateName, this); } diff --git a/packages/s2-core/src/common/interface/basic.ts b/packages/s2-core/src/common/interface/basic.ts index 5eabb358e2..01a224c8e4 100644 --- a/packages/s2-core/src/common/interface/basic.ts +++ b/packages/s2-core/src/common/interface/basic.ts @@ -486,21 +486,6 @@ export interface ViewMeta { export type ViewMetaIndexType = keyof Pick; -export interface OffsetConfig { - rowHeaderOffsetX?: { - value: number | undefined; - animate?: boolean; - }; - offsetX?: { - value: number | undefined; - animate?: boolean; - }; - offsetY?: { - value: number | undefined; - animate?: boolean; - }; -} - export interface CellAppendInfo extends Partial> { isLinkFieldText?: boolean; diff --git a/packages/s2-core/src/common/interface/interaction.ts b/packages/s2-core/src/common/interface/interaction.ts index 4e19af5d4e..97a36df29c 100644 --- a/packages/s2-core/src/common/interface/interaction.ts +++ b/packages/s2-core/src/common/interface/interaction.ts @@ -1,3 +1,4 @@ +import type { FederatedPointerEvent } from '@antv/g'; import type { BaseCell, ColCell, @@ -25,6 +26,7 @@ import type { import type { ViewMeta } from './basic'; import type { Transformer } from './export'; import type { ResizeInteractionOptions } from './resize'; +import type { CellScrollToOptions } from './scroll'; export type S2CellType = | DataCell @@ -59,7 +61,7 @@ export interface InteractionStateInfo { /** * 交互状态名 */ - stateName?: InteractionStateName; + stateName?: `${InteractionStateName}`; /** * 单元格元数据 (包含不在可视范围内的) @@ -87,9 +89,27 @@ export interface InteractionStateInfo { onUpdateCells?: OnUpdateCells; } -export interface SelectHeaderCellInfo { +export interface ChangeCellOptions extends CellScrollToOptions { + /** + * 目标单元格 + */ cell: S2CellType; + + /** + * 是否是多选 + */ isMultiSelection?: boolean; + + /** + * 状态名 (默认 `selected`) + */ + stateName?: `${InteractionStateName}`; + + /** + * 如果单元格不在可视范围, 是否自动滚动 + * @default true + */ + scrollIntoView?: boolean; } export type InteractionConstructor = new ( @@ -206,10 +226,16 @@ export interface InteractionOptions { }; /** - * 自动重置表格样式 (按下 ESC 键, 点击空白区域时, 关闭 tooltip/交互状态) + * 自动重置表格样式 (按下 ESC 键, 点击空白区域时, 关闭 tooltip/交互状态), 支持根据 event 动态判断 * @see https://s2.antv.antgroup.com/examples/interaction/basic/#auto-reset-sheet-style + * @example autoResetSheetStyle: (event, spreadsheet) => event.target instanceof HTMLDivElement */ - autoResetSheetStyle?: boolean; + autoResetSheetStyle?: + | boolean + | (( + event: Event | FederatedPointerEvent, + spreadsheet: SpreadSheet, + ) => boolean); /** * 隐藏列头配置, 支持维度 (S2DataConfig.fields) 和具体维值 (id) @@ -296,12 +322,50 @@ export interface InteractionOptions { } export interface InteractionCellHighlightOptions { - /** 高亮行头 */ + /** + * 高亮行头 + */ rowHeader?: boolean; - /** 高亮列头 */ + + /** + * 高亮列头 + */ colHeader?: boolean; - /** 高亮选中单元格所在行 */ + + /** + * 高亮选中单元格所在行 + */ currentRow?: boolean; - /** 高亮选中单元格所在列 */ + + /** + * 高亮选中单元格所在列 + */ currentCol?: boolean; } + +export interface ScrollOffsetConfig + extends Pick { + /** + * 行头水平偏移量 + */ + rowHeaderOffsetX?: { + value: number | undefined; + animate?: boolean; + }; + + /** + * 水平偏移量 + */ + offsetX?: { + value: number | undefined; + animate?: boolean; + }; + + /** + * 垂直偏移量 + */ + offsetY?: { + value: number | undefined; + animate?: boolean; + }; +} diff --git a/packages/s2-core/src/common/interface/scroll.ts b/packages/s2-core/src/common/interface/scroll.ts index 5e27e439b0..9a89ba5df1 100644 --- a/packages/s2-core/src/common/interface/scroll.ts +++ b/packages/s2-core/src/common/interface/scroll.ts @@ -18,3 +18,15 @@ export interface CellScrollOffset { offsetX: number; offsetY: number; } + +export interface CellScrollToOptions { + /** + * 是否展示滚动动画 + */ + animate?: boolean; + + /** + * 是否触发滚动事件 + */ + skipScrollEvent?: boolean; +} diff --git a/packages/s2-core/src/facet/base-facet.ts b/packages/s2-core/src/facet/base-facet.ts index c15e21db73..7730fe5ce0 100644 --- a/packages/s2-core/src/facet/base-facet.ts +++ b/packages/s2-core/src/facet/base-facet.ts @@ -72,9 +72,9 @@ import type { GridInfo, HiddenColumnsInfo, LayoutResult, - OffsetConfig, S2CellType, ScrollChangeParams, + ScrollOffsetConfig, ViewMeta, } from '../common/interface'; import type { @@ -643,7 +643,10 @@ export abstract class BaseFacet { return rowsHierarchy.height + colsHierarchy.height; } - public updateScrollOffset(offsetConfig: OffsetConfig) { + /** + * @alias s2.interaction.scrollTo(offsetConfig) + */ + public updateScrollOffset(offsetConfig: ScrollOffsetConfig) { if (offsetConfig.rowHeaderOffsetX?.value !== undefined) { if (offsetConfig.rowHeaderOffsetX?.animate) { this.scrollWithAnimation(offsetConfig); @@ -829,7 +832,7 @@ export abstract class BaseFacet { } scrollWithAnimation = ( - offsetConfig: OffsetConfig = {}, + offsetConfig: ScrollOffsetConfig = {}, duration = 200, cb?: () => void, ) => { @@ -867,7 +870,7 @@ export abstract class BaseFacet { scrollX, scrollY, }); - this.startScroll(); + this.startScroll(offsetConfig?.skipScrollEvent); if (elapsed > duration) { this.timer.stop(); @@ -881,7 +884,7 @@ export abstract class BaseFacet { }); }; - scrollImmediately = (offsetConfig: OffsetConfig = {}) => { + scrollImmediately = (offsetConfig: ScrollOffsetConfig = {}) => { const { scrollX, scrollY, rowHeaderScrollX } = this.getAdjustedScrollOffset( { scrollX: offsetConfig.offsetX?.value || 0, @@ -891,11 +894,10 @@ export abstract class BaseFacet { ); this.setScrollOffset({ scrollX, scrollY, rowHeaderScrollX }); - this.startScroll(); + this.startScroll(offsetConfig?.skipScrollEvent); }; /** - * * @param skipScrollEvent 不触发 S2Event.GLOBAL_SCROLL */ startScroll = (skipScrollEvent = false) => { @@ -1745,10 +1747,7 @@ export abstract class BaseFacet { } /** - * - * @param skipScrollEvent: 如true则不触发GLOBAL_SCROLL事件 - * During scroll behavior, first call to this method fires immediately and then on interval. - * @protected + * @param skipScrollEvent: 不触发 GLOBAL_SCROLL 事件 */ protected dynamicRenderCell(skipScrollEvent?: boolean) { const { @@ -1764,7 +1763,6 @@ export abstract class BaseFacet { this.realDataCellRender(scrollX, scrollY); this.updatePanelScrollGroup(); this.translateRelatedGroups(scrollX, scrollY, rowHeaderScrollX); - this.clip(scrollX, scrollY); if (!skipScrollEvent) { diff --git a/packages/s2-core/src/facet/header/frame.ts b/packages/s2-core/src/facet/header/frame.ts index c0ca9f8915..3389b2a95d 100644 --- a/packages/s2-core/src/facet/header/frame.ts +++ b/packages/s2-core/src/facet/header/frame.ts @@ -1,6 +1,6 @@ -import { Group, Rect } from '@antv/g'; +import { Group, Line, Rect } from '@antv/g'; import type { FrameConfig } from '../../common/interface'; -import type { SpreadSheet } from '../../sheet-type/spread-sheet'; +import type { SpreadSheet } from '../../sheet-type'; import { floor } from '../../utils/math'; import { renderLine } from '.././../utils/g-renders'; import type { FrozenFacet } from '../frozen-facet'; @@ -9,6 +9,10 @@ import { translateGroup } from '../utils'; export class Frame extends Group { declare cfg: FrameConfig; + public cornerRightBorder: Line; + + public cornerBottomBorder: Line; + constructor(cfg: FrameConfig) { super(); this.cfg = cfg; @@ -101,14 +105,14 @@ export class Frame extends Group { const y2 = position.y + cornerHeight + horizontalBorderWidth! + viewportHeight; - renderLine(this, { + this.cornerRightBorder = renderLine(this, { x1: x, y1: position.y, x2: x, y2, stroke: verticalBorderColor, lineWidth: frameVerticalWidth, - opacity: verticalBorderColorOpacity, + strokeOpacity: verticalBorderColorOpacity, }); return; @@ -129,7 +133,7 @@ export class Frame extends Group { y2: position.y + cornerHeight, stroke: headerVerticalBorderColor, lineWidth: frameVerticalWidth, - opacity: headerVerticalBorderColorOpacity, + strokeOpacity: headerVerticalBorderColorOpacity, }); const { @@ -144,7 +148,7 @@ export class Frame extends Group { y2: position.y + cornerHeight + horizontalBorderWidth! + viewportHeight, stroke: cellVerticalBorderColor, lineWidth: frameVerticalWidth, - opacity: cellVerticalBorderColorOpacity, + strokeOpacity: cellVerticalBorderColorOpacity, }); } @@ -172,7 +176,7 @@ export class Frame extends Group { (spreadsheet.isFrozenRowHeader() ? 0 : scrollX); const y = position.y + cornerHeight + horizontalBorderWidth! / 2; - renderLine(this, { + this.cornerBottomBorder = renderLine(this, { x1, y1: y, x2, diff --git a/packages/s2-core/src/interaction/base-interaction/click/corner-cell-click.ts b/packages/s2-core/src/interaction/base-interaction/click/corner-cell-click.ts index 1629732607..ddfcd2ef0a 100644 --- a/packages/s2-core/src/interaction/base-interaction/click/corner-cell-click.ts +++ b/packages/s2-core/src/interaction/base-interaction/click/corner-cell-click.ts @@ -1,5 +1,6 @@ import type { FederatedPointerEvent as CanvasEvent } from '@antv/g'; import { isEmpty } from 'lodash'; +import type { CornerCell } from '../../../cell'; import { CellType, CornerNodeType, @@ -24,7 +25,7 @@ export class CornerCellClick extends BaseEvent implements BaseEventImplement { private bindCornerCellClick() { this.spreadsheet.on(S2Event.CORNER_CELL_CLICK, (event) => { - const cornerCell = this.spreadsheet.getCell(event.target); + const cornerCell = this.spreadsheet.getCell(event.target); if (!cornerCell) { return; @@ -116,6 +117,7 @@ export class CornerCellClick extends BaseEvent implements BaseEventImplement { const { interaction } = this.spreadsheet; const sample = nodes[0]?.belongsCell; const cells = this.getCellMetas(nodes, sample?.cellType!); + const cornerCell = this.spreadsheet.getCell(event.target)!; if (sample && interaction.isSelectedCell(sample)) { interaction.reset(); @@ -137,6 +139,7 @@ export class CornerCellClick extends BaseEvent implements BaseEventImplement { stateName: InteractionStateName.SELECTED, }); interaction.highlightNodes(nodes); + cornerCell?.updateByState(InteractionStateName.SELECTED); this.showTooltip(event); this.spreadsheet.emit( diff --git a/packages/s2-core/src/interaction/base-interaction/click/row-column-click.ts b/packages/s2-core/src/interaction/base-interaction/click/row-column-click.ts index 99d004e5b8..a8de1cd227 100644 --- a/packages/s2-core/src/interaction/base-interaction/click/row-column-click.ts +++ b/packages/s2-core/src/interaction/base-interaction/click/row-column-click.ts @@ -106,9 +106,12 @@ export class RowColumnClick extends BaseEvent implements BaseEventImplement { const { multiSelection: enableMultiSelection } = options.interaction!; // 关闭了多选就算按下了 Ctrl/Commend, 行/列也按单选处理 const isMultiSelection = !!(enableMultiSelection && this.isMultiSelection); - const success = interaction.selectHeaderCell({ + + const success = interaction.changeCell({ cell, isMultiSelection, + // 能主动触发点击一定是在可视范围内, 无需额外触发滚动 + scrollIntoView: false, }); if (success) { diff --git a/packages/s2-core/src/interaction/base-interaction/hover.ts b/packages/s2-core/src/interaction/base-interaction/hover.ts index 7e8bb41256..663af4dad1 100644 --- a/packages/s2-core/src/interaction/base-interaction/hover.ts +++ b/packages/s2-core/src/interaction/base-interaction/hover.ts @@ -25,12 +25,6 @@ export class HoverEvent extends BaseEvent implements BaseEventImplement { this.bindHeaderCellHover(); } - /** - * @description change the data cell state from hover to hover focus - * @param cell - * @param event - * @param meta - */ private changeStateToHoverFocus(cell: S2CellType, event: CanvasEvent) { if (!cell) { return; @@ -98,7 +92,7 @@ export class HoverEvent extends BaseEvent implements BaseEventImplement { return; } - const { interaction } = this.spreadsheet; + const { interaction, facet } = this.spreadsheet; interaction.clearHoverTimer(); @@ -114,6 +108,8 @@ export class HoverEvent extends BaseEvent implements BaseEventImplement { cell.update(); this.showEllipsisTooltip(event, cell); + // 由于绘制的顺序问题, 交互背景图层展示后, 会遮挡边框, 需要让边框展示在前面. + facet.centerFrame?.toFront(); } private showEllipsisTooltip(event: CanvasEvent, cell: S2CellType | null) { diff --git a/packages/s2-core/src/interaction/brush-selection/base-brush-selection.ts b/packages/s2-core/src/interaction/brush-selection/base-brush-selection.ts index 5942661a8c..03c9cdcd6d 100644 --- a/packages/s2-core/src/interaction/brush-selection/base-brush-selection.ts +++ b/packages/s2-core/src/interaction/brush-selection/base-brush-selection.ts @@ -23,9 +23,9 @@ import type { BrushAutoScrollConfig, BrushPoint, BrushRange, - OffsetConfig, OnUpdateCells, S2CellType, + ScrollOffsetConfig, ViewMeta, } from '../../common/interface'; import type { BBox } from '../../engine/interface'; @@ -416,11 +416,11 @@ export class BaseBrushSelection const config = this.autoScrollConfig; const scrollOffset = this.spreadsheet.facet.getScrollOffset(); - const key: keyof OffsetConfig = isRowHeader + const key: keyof ScrollOffsetConfig = isRowHeader ? 'rowHeaderOffsetX' : 'offsetX'; - const offsetCfg: OffsetConfig = { + const offsetCfg: ScrollOffsetConfig = { rowHeaderOffsetX: { value: scrollOffset.rowHeaderScrollX, animate: true, diff --git a/packages/s2-core/src/interaction/event-controller.ts b/packages/s2-core/src/interaction/event-controller.ts index 11470699d2..b8f19514fa 100644 --- a/packages/s2-core/src/interaction/event-controller.ts +++ b/packages/s2-core/src/interaction/event-controller.ts @@ -5,7 +5,7 @@ import { type Group, type PointLike, } from '@antv/g'; -import { each, get, hasIn, isEmpty, isNil } from 'lodash'; +import { each, get, hasIn, isEmpty, isFunction, isNil } from 'lodash'; import { GuiIcon } from '../common'; import { CellType, @@ -66,8 +66,12 @@ export class EventController { return this.spreadsheet.container; } - public get isAutoResetSheetStyle() { - return this.spreadsheet.options.interaction?.autoResetSheetStyle; + public isAutoResetSheetStyle(event: Event | CanvasEvent) { + const { interaction } = this.spreadsheet.options; + + return isFunction(interaction?.autoResetSheetStyle) + ? interaction?.autoResetSheetStyle?.(event, this.spreadsheet) + : interaction?.autoResetSheetStyle; } public bindEvents() { @@ -157,7 +161,7 @@ export class EventController { } private resetSheetStyle(event: Event) { - if (!this.isAutoResetSheetStyle || !this.spreadsheet) { + if (!this.isAutoResetSheetStyle(event) || !this.spreadsheet) { return; } @@ -587,7 +591,7 @@ export class EventController { private onCanvasMouseout = (event: CanvasEvent) => { if ( - !this.isAutoResetSheetStyle || + !this.isAutoResetSheetStyle(event) || this.isMouseOnTheCanvasContainer(event as Event) ) { return; diff --git a/packages/s2-core/src/interaction/root.ts b/packages/s2-core/src/interaction/root.ts index b15e9f1f5b..297409bdc3 100644 --- a/packages/s2-core/src/interaction/root.ts +++ b/packages/s2-core/src/interaction/root.ts @@ -21,17 +21,20 @@ import type { BrushSelectionInfo, BrushSelectionOptions, CellMeta, + CellScrollToOptions, + ChangeCellOptions, CustomInteraction, InteractionCellHighlightOptions, InteractionStateInfo, Intercept, MergedCellInfo, S2CellType, - SelectHeaderCellInfo, + ScrollOffsetConfig, ViewMeta, } from '../common/interface'; import type { Node } from '../facet/layout/node'; import type { SpreadSheet } from '../sheet-type'; +import { customMerge } from '../utils'; import { hideColumnsByThunkGroup } from '../utils/hide-columns'; import { getActiveHoverHeaderCells, @@ -70,9 +73,9 @@ export class RootInteraction { // 用来标记需要拦截的交互,interaction 和本身的 hover 等事件可能会有冲突,有冲突时在此屏蔽 public intercepts = new Set(); - /* + /** * hover有 keep-hover 态,是个计时器,hover后 800毫秒还在当前 cell 的情况下,该 cell 进入 keep-hover 状态 - * 在任何触发点击,或者点击空白区域时,说明已经不是hover了,因此需要取消这个计时器。 + * 在任何触发点击,或者点击空白区域时,说明已经不是 hover了,因此需要取消这个计时器。 */ private hoverTimer: number | null = null; @@ -93,6 +96,10 @@ export class RootInteraction { ); } + /** + * 销毁交互 + * @example s2.interaction.destroy() + */ public destroy() { this.interactions.clear(); this.intercepts.clear(); @@ -105,6 +112,10 @@ export class RootInteraction { ); } + /** + * 重置交互 + * @example s2.interaction.reset() + */ public reset() { this.clearState(); this.clearHoverTimer(); @@ -118,10 +129,27 @@ export class RootInteraction { }); }; + /** + * 设置交互状态 + * @example + s2.interaction.setState({ + stateName: InteractionStateName.SELECTED, + cells: [{ + "id": "root[&]浙江省[&]舟山市", + "colIndex": -1, + "rowIndex": 3, + "type": "rowCell" + }] + }) + */ public setState(interactionStateInfo: InteractionStateInfo) { setState(this.spreadsheet, interactionStateInfo); } + /** + * 获取交互状态 + * @example s2.interaction.getState() + */ public getState() { return ( this.spreadsheet.store.get(INTERACTION_STATE_INFO_KEY) || @@ -129,6 +157,10 @@ export class RootInteraction { ); } + /** + * 设置通过交互触发过更新的单元格 + * @example s2.interaction.setInteractedCells(dataCell) + */ public setInteractedCells(cell: S2CellType) { const interactedCells = this.getInteractedCells().concat([cell]); const state = this.getState(); @@ -138,33 +170,67 @@ export class RootInteraction { this.setState(state); } + /** + * 获取通过交互触发过更新的单元格 + * @example s2.interaction.getInteractedCells() + */ public getInteractedCells() { const currentState = this.getState(); return currentState?.interactedCells || []; } + /** + * 重置交互状态 + * @example s2.interaction.resetState() + */ public resetState() { this.spreadsheet.store.set(INTERACTION_STATE_INFO_KEY, this.defaultState); } + /** + * 获取当前交互状态名 + * @example s2.interaction.getCurrentStateName() + */ public getCurrentStateName() { return this.getState().stateName; } - public isEqualStateName(stateName: InteractionStateName) { + /** + * 是否和当前状态名相同 + * @example s2.interaction.isEqualStateName(InteractionStateName.SELECTED) + */ + public isEqualStateName(stateName: `${InteractionStateName}`) { return this.getCurrentStateName() === stateName; } - private isStateOf(stateName: InteractionStateName) { + private isStateOf(stateName: `${InteractionStateName}`) { const currentState = this.getState(); return currentState?.stateName === stateName; } + /** + * 是否是选中状态 (含单选,多选,圈选,全选) + * @example s2.interaction.isSelectedState() + */ public isSelectedState() { + return ( + this.isBrushSelectedState() || + [InteractionStateName.SELECTED, InteractionStateName.ALL_SELECTED].some( + (stateName) => { + return this.isStateOf(stateName); + }, + ) + ); + } + + /** + * 是否是刷选状态 + * @example s2.interaction.isBrushSelectedState() + */ + public isBrushSelectedState() { return [ - InteractionStateName.SELECTED, InteractionStateName.ROW_CELL_BRUSH_SELECTED, InteractionStateName.COL_CELL_BRUSH_SELECTED, InteractionStateName.DATA_CELL_BRUSH_SELECTED, @@ -173,27 +239,50 @@ export class RootInteraction { }); } + /** + * 是否是全选状态 + * @example s2.interaction.isAllSelectedState() + */ public isAllSelectedState() { return this.isStateOf(InteractionStateName.ALL_SELECTED); } + /** + * 是否是悬停聚焦状态 + * @example s2.interaction.isHoverFocusState() + */ public isHoverFocusState() { return this.isStateOf(InteractionStateName.HOVER_FOCUS); } + /** + * 是否是悬停状态 + * @example s2.interaction.isHoverState() + */ public isHoverState() { return this.isStateOf(InteractionStateName.HOVER); } + /** + * 是否是激活的单元格 + * @example s2.interaction.isActiveCell(cell) + */ public isActiveCell(cell: S2CellType): boolean { return !!this.getCells().find((meta) => cell.getMeta().id === meta.id); } + /** + * 是否是选中的单元格 + * @example s2.interaction.isSelectedCell(cell) + */ public isSelectedCell(cell: S2CellType): boolean { return this.isSelectedState() && this.isActiveCell(cell); } - // 获取当前 interaction 记录的 Cells 元信息列表,包括不在可视区域内的格子 + /** + * 获取当前 interaction 记录的 Cells 元信息列表,包括不在可视区域内的格子 + * @example s2.interaction.getCells(CellType.DATA_CELL) + */ public getCells(cellType?: CellType[]): CellMeta[] { const currentState = this.getState(); const cells = currentState?.cells || []; @@ -205,7 +294,10 @@ export class RootInteraction { return cells.filter((cell) => cellType.includes(cell.type)); } - // 获取 cells 中在可视区域内的实例列表 + /** + * 获取已激活单元格 (不含非可视区域) + * @example s2.interaction.getActiveCells() + */ public getActiveCells(): S2CellType[] { const ids = this.getCells().map((item) => item.id); const allCells = this.spreadsheet.facet?.getCells(); @@ -234,6 +326,10 @@ export class RootInteraction { ); } + /** + * 清除单元格交互样式 + * @example s2.interaction.clearStyleIndependent() + */ public clearStyleIndependent() { if ( !this.isSelectedState() && @@ -248,30 +344,248 @@ export class RootInteraction { }); } + /** + * 获取未选中的单元格 (不含非可视区域) + * @example s2.interaction.getUnSelectedDataCells() + */ public getUnSelectedDataCells() { return this.spreadsheet.facet .getDataCells() .filter((cell) => !this.isActiveCell(cell)); } - public selectAll = () => { + private scrollToCellByMeta( + meta: ViewMeta | Node, + options: CellScrollToOptions = { animate: true }, + ) { + if (!meta) { + return; + } + + const { skipScrollEvent, animate } = options; + const { facet } = this.spreadsheet; + + if (!facet.hRowScrollBar && !facet.hScrollBar && !facet.vScrollBar) { + return; + } + + this.scrollTo({ + skipScrollEvent, + rowHeaderOffsetX: { + value: meta.x, + animate, + }, + offsetX: { + value: meta.x, + animate, + }, + offsetY: { + value: meta.y, + animate, + }, + }); + } + + /** + * 滚动至指定位置 + * @example + s2.interaction.scrollTo({ + skipScrollEvent: false, + offsetX: { value: 100, animate: true }, + offsetY: { value: 100, animate: true }, + }) + */ + public scrollTo(offsetConfig: ScrollOffsetConfig) { + const { facet } = this.spreadsheet; + const { scrollX, scrollY, rowHeaderScrollX } = facet.getScrollOffset(); + + const defaultConfig: ScrollOffsetConfig = { + skipScrollEvent: false, + offsetX: { + value: scrollX, + animate: true, + }, + offsetY: { + value: scrollY, + animate: true, + }, + rowHeaderOffsetX: { + value: rowHeaderScrollX, + animate: true, + }, + }; + + facet.updateScrollOffset( + customMerge(defaultConfig, offsetConfig), + ); + } + + /** + * 滚动至指定单元格节点 + * @example s2.interaction.scrollToNode(rowNode, { animate: true, skipScrollEvent: true }) + */ + public scrollToNode(meta: ViewMeta | Node, options?: CellScrollToOptions) { + this.scrollToCellByMeta(meta, options); + } + + /** + * 滚动至指定单元格 + * @example s2.interaction.scrollToNode(rowCell, { animate: true, skipScrollEvent: true }) + */ + public scrollToCell(cell: S2CellType, options?: CellScrollToOptions) { + this.scrollToCellByMeta(cell.getMeta(), options); + } + + /** + * 滚动至指定单元格 id 对应的位置 + * @example s2.interaction.scrollToCellById('root[&]四川省[&]成都市', { animate: true, skipScrollEvent: true }) + */ + public scrollToCellById(id: string, options?: CellScrollToOptions) { + if (!id) { + return; + } + + // 兼容不在可视区域, 未实例化的行列头单元格 + const headerNodes = this.spreadsheet.facet.getHeaderNodes(); + const viewMetaList = this.spreadsheet.facet + .getDataCells() + .map((cell) => cell.getMeta()); + + const cellMeta = [...headerNodes, ...viewMetaList].find( + (meta) => meta.id === id, + ); + + if (!cellMeta) { + return; + } + + this.scrollToCellByMeta(cellMeta, options); + } + + /** + * 滚动至顶部 + * @example s2.interaction.scrollToTop({ animate: true, skipScrollEvent: true }) + */ + public scrollToTop(options?: CellScrollToOptions) { + this.scrollTo({ + skipScrollEvent: options?.skipScrollEvent, + offsetY: { + value: 0, + animate: options?.animate, + }, + }); + } + + /** + * 滚动至右边 + * @example s2.interaction.scrollToRight({ animate: true, skipScrollEvent: true }) + */ + public scrollToRight(options?: CellScrollToOptions) { + this.scrollTo({ + skipScrollEvent: options?.skipScrollEvent, + offsetX: { + value: this.spreadsheet.facet.panelBBox.maxX, + animate: options?.animate, + }, + }); + } + + /** + * 滚动至底部 + * @example s2.interaction.scrollToBottom({ animate: true, skipScrollEvent: true }) + */ + public scrollToBottom(options?: CellScrollToOptions) { + this.scrollTo({ + skipScrollEvent: options?.skipScrollEvent, + offsetY: { + value: this.spreadsheet.facet.panelBBox.maxY, + animate: options?.animate, + }, + }); + } + + /** + * 滚动至左边 + * @example s2.interaction.scrollToLeft({ animate: true, skipScrollEvent: true }) + */ + public scrollToLeft(options?: CellScrollToOptions) { + this.scrollTo({ + skipScrollEvent: options?.skipScrollEvent, + offsetX: { + value: 0, + animate: options?.animate, + }, + }); + } + + /** + * 全选 + * @example s2.interaction.selectAll() + */ + public selectAll() { + const cells = this.spreadsheet.facet.getCells().map(getCellMeta); + this.changeState({ + cells, stateName: InteractionStateName.ALL_SELECTED, }); - }; - public selectHeaderCell = ( - selectHeaderCellInfo: SelectHeaderCellInfo = {} as SelectHeaderCellInfo, - ) => { - const { cell } = selectHeaderCellInfo; + this.addIntercepts([InterceptType.HOVER]); + this.updateCells(this.spreadsheet.facet.getCells()); + } + + /** + * 高亮指定单元格 (可视范围内) + * @example s2.interaction.highlightCell(dataCell, options) + */ + public highlightCell(cell: S2CellType, options?: CellScrollToOptions) { + this.changeCell({ + ...options, + cell, + stateName: InteractionStateName.HOVER, + }); + } + + /** + * 选中指定单元格 (可视范围内) + * @example s2.interaction.selectCell(dataCell, options) + */ + public selectCell(cell: S2CellType, options?: CellScrollToOptions) { + this.changeCell({ + ...options, + cell, + stateName: InteractionStateName.SELECTED, + }); + } + + /** + * 改变指定单元格状态 (如: 选中/高亮/多选等) (可视范围内) + * @example + s2.interaction.changeCell({ + cell: rowCell, + stateName: InteractionStateName.SELECTED, + isMultiSelection: false, + scrollIntoView: false, + animate: true, + skipScrollEvent: true, + }) + */ + public changeCell(options: ChangeCellOptions = {} as ChangeCellOptions) { + const { + cell, + stateName = InteractionStateName.SELECTED, + scrollIntoView = true, + animate = true, + skipScrollEvent = true, + } = options; if (isEmpty(cell)) { return; } - const currentCellMeta = cell?.getMeta?.() as Node; + const meta = cell?.getMeta?.() as Node; - if (!currentCellMeta || isNil(currentCellMeta?.x)) { + if (!meta || isNil(meta?.x)) { return; } @@ -281,8 +595,7 @@ export class RootInteraction { const isColCell = cell?.cellType === CellType.COL_CELL; const lastState = this.getState(); const isSelectedCell = this.isSelectedCell(cell); - const isMultiSelected = - selectHeaderCellInfo?.isMultiSelection && this.isSelectedState(); + const isMultiSelected = options?.isMultiSelection && this.isSelectedState(); // 如果是已选中的单元格, 则取消选中, 兼容行列多选 (含叶子节点) let childrenNodes = isSelectedCell @@ -295,11 +608,9 @@ export class RootInteraction { childrenNodes = concat(lastState?.nodes || [], childrenNodes); if (isSelectedCell) { - selectedCells = selectedCells.filter( - ({ id }) => id !== currentCellMeta.id, - ); + selectedCells = selectedCells.filter(({ id }) => id !== meta.id); childrenNodes = childrenNodes.filter( - (node) => !node?.id.includes(currentCellMeta.id), + (node) => !node?.id.includes(meta.id), ); } } @@ -324,7 +635,7 @@ export class RootInteraction { this.changeState({ cells: selectedCells, nodes, - stateName: InteractionStateName.SELECTED, + stateName, }); const selectedCellIds = selectedCells.map(({ id }) => id); @@ -336,28 +647,54 @@ export class RootInteraction { this.highlightNodes(childrenNodes); } + // 如果不在可视范围, 自动滚动 + if (scrollIntoView) { + this.scrollToCell(cell, { + skipScrollEvent, + animate, + }); + } + + // 由于绘制的顺序问题, 交互背景图层展示后, 会遮挡边框, 需要让边框展示在前面. + this.spreadsheet.facet.centerFrame?.toFront(); this.spreadsheet.emit(S2Event.GLOBAL_SELECTED, this.getActiveCells()); return true; - }; + } + /** + * 高亮节点对应的单元格 + * @example s2.interaction.highlightNodes([node]) + */ public highlightNodes = (nodes: Node[] = []) => { nodes.forEach((node) => { node?.belongsCell?.updateByState( - InteractionStateName.SELECTED, + InteractionStateName.HOVER, node.belongsCell, ); }); }; + /** + * 合并单元格 + * @example s2.interaction.mergeCells() + */ public mergeCells = (cellsInfo?: MergedCellInfo[], hideData?: boolean) => { mergeCell(this.spreadsheet, cellsInfo, hideData); }; - public unmergeCell = (removedCells: MergedCell) => { - unmergeCell(this.spreadsheet, removedCells); + /** + * 取消合并单元格 + * @example s2.interaction.unmergeCell(mergedCell) + */ + public unmergeCell = (removedCell: MergedCell) => { + unmergeCell(this.spreadsheet, removedCell); }; + /** + * 隐藏列头 + * @example s2.interaction.hideColumns(['city']) + */ public async hideColumns( hiddenColumnFields: string[] = [], forceRender = true, @@ -504,7 +841,15 @@ export class RootInteraction { } } - // 改变 cell 交互状态后,进行了更新和重新绘制 + /** + * 改变单元格交互状态后,进行更新和重新绘制 + * @example + s2.interaction.changeState({ + cells: [{ id: 'city', colIndex: 1, rowIndex : 2, type: 'rowCell' }], + stateName: InteractionStateName.SELECTED, + force: false + }) + */ public changeState(interactionStateInfo: InteractionStateInfo) { const { interaction } = this.spreadsheet; const { @@ -535,7 +880,7 @@ export class RootInteraction { // 更新单元格 const update = () => { - this.updatePanelGroupAllDataCells(); + this.updateAllDataCells(); }; if (onUpdateCells) { @@ -547,29 +892,49 @@ export class RootInteraction { this.draw(); } - public updatePanelGroupAllDataCells() { + /** + * 更新所有数值单元格 + * @example s2.interaction.updateAllDataCells() + */ + public updateAllDataCells() { this.updateCells(this.spreadsheet.facet.getDataCells()); } + /** + * 更新指定单元格 + * @example s2.interaction.updateCells([rowCell, dataCell]) + */ public updateCells(cells: S2CellType[] = []) { cells.forEach((cell) => { cell.update(); }); } - public addIntercepts(interceptTypes: InterceptType[] = []) { + /** + * 添加交互拦截 + * @example s2.interaction.addIntercepts([InterceptType.HOVER]) + */ + public addIntercepts(interceptTypes: `${InterceptType}`[] = []) { interceptTypes.forEach((interceptType) => { this.intercepts.add(interceptType); }); } - public hasIntercepts(interceptTypes: InterceptType[] = []) { + /** + * 是否有指定交互拦截 + * @example s2.interaction.hasIntercepts([InterceptType.HOVER]) + */ + public hasIntercepts(interceptTypes: `${InterceptType}`[] = []) { return interceptTypes.some((interceptType) => this.intercepts.has(interceptType), ); } - public removeIntercepts(interceptTypes: InterceptType[] = []) { + /** + * 移除交互拦截 + * @example s2.interaction.removeIntercepts([InterceptType.HOVER]) + */ + public removeIntercepts(interceptTypes: `${InterceptType}`[] = []) { interceptTypes.forEach((interceptType) => { this.intercepts.delete(interceptType); }); diff --git a/packages/s2-core/src/sheet-type/spread-sheet.ts b/packages/s2-core/src/sheet-type/spread-sheet.ts index acb1c2eff5..47524b7ea6 100644 --- a/packages/s2-core/src/sheet-type/spread-sheet.ts +++ b/packages/s2-core/src/sheet-type/spread-sheet.ts @@ -37,7 +37,6 @@ import type { Fields, InteractionOptions, InternalFullyTheme, - OffsetConfig, Pagination, S2CellType, S2DataConfig, @@ -635,34 +634,6 @@ export abstract class SpreadSheet extends EE { return this.options.style?.layoutWidthType!; } - /** - * Update scroll's offset, the value can be undefined, - * indicate not update current value - * @param offsetConfig - * default offsetX(horizontal scroll need animation) - * but offsetY(vertical scroll don't need animation) - */ - public updateScrollOffset(offsetConfig: OffsetConfig) { - const config: OffsetConfig = { - offsetX: { - value: undefined, - animate: false, - }, - offsetY: { - value: undefined, - animate: false, - }, - rowHeaderOffsetX: { - value: undefined, - animate: false, - }, - }; - - this.facet.updateScrollOffset( - customMerge(config, offsetConfig) as OffsetConfig, - ); - } - protected isCellType(cell?: CellEventTarget) { return cell instanceof BaseCell; } diff --git a/packages/s2-core/src/theme/index.ts b/packages/s2-core/src/theme/index.ts index efa7dc3501..124303bda9 100644 --- a/packages/s2-core/src/theme/index.ts +++ b/packages/s2-core/src/theme/index.ts @@ -206,6 +206,42 @@ export const getTheme = ( bottom: 4, left: 8, }, + + /* ---------- interaction state ----------- */ + interactionState: { + // -------------- hover ------------------- + hover: { + backgroundColor: basicColors[4], + backgroundOpacity: 0.6, + }, + // -------------- selected ------------------- + selected: { + backgroundColor: basicColors[4], + backgroundOpacity: 0.6, + }, + // -------------- unselected ------------------- + unselected: { + backgroundOpacity: 0.3, + textOpacity: 0.3, + opacity: 0.3, + }, + // -------------- prepare select -------------- + prepareSelect: { + borderColor: basicColors[14], + borderOpacity: 1, + borderWidth: 1, + }, + // -------------- searchResult ------------------- + searchResult: { + backgroundColor: otherColors?.results ?? basicColors[2], + backgroundOpacity: 1, + }, + // -------------- highlight ------------------- + highlight: { + backgroundColor: otherColors?.highlight ?? basicColors[6], + backgroundOpacity: 1, + }, + }, }, icon: { fill: basicColors[0], diff --git a/packages/s2-react-components/__tests__/unit/components/config/theme-panel/__snapshots__/utils-spec.ts.snap b/packages/s2-react-components/__tests__/unit/components/config/theme-panel/__snapshots__/utils-spec.ts.snap index 80d1939618..092d142832 100644 --- a/packages/s2-react-components/__tests__/unit/components/config/theme-panel/__snapshots__/utils-spec.ts.snap +++ b/packages/s2-react-components/__tests__/unit/components/config/theme-panel/__snapshots__/utils-spec.ts.snap @@ -110,6 +110,34 @@ Object { "horizontalBorderColor": "#E0E9FD", "horizontalBorderColorOpacity": 1, "horizontalBorderWidth": 1, + "interactionState": Object { + "highlight": Object { + "backgroundColor": "#87B5FF", + "backgroundOpacity": 1, + }, + "hover": Object { + "backgroundColor": "#E0E9FD", + "backgroundOpacity": 0.6, + }, + "prepareSelect": Object { + "borderColor": "#000000", + "borderOpacity": 1, + "borderWidth": 1, + }, + "searchResult": Object { + "backgroundColor": "#F0F7FF", + "backgroundOpacity": 1, + }, + "selected": Object { + "backgroundColor": "#E0E9FD", + "backgroundOpacity": 0.6, + }, + "unselected": Object { + "backgroundOpacity": 0.3, + "opacity": 0.3, + "textOpacity": 0.3, + }, + }, "padding": Object { "bottom": 4, "left": 8, @@ -661,6 +689,34 @@ Object { "horizontalBorderColor": "#5184F6", "horizontalBorderColorOpacity": 1, "horizontalBorderWidth": 1, + "interactionState": Object { + "highlight": Object { + "backgroundColor": "#87B5FF", + "backgroundOpacity": 1, + }, + "hover": Object { + "backgroundColor": "#2B5ECF", + "backgroundOpacity": 0.6, + }, + "prepareSelect": Object { + "borderColor": "#000000", + "borderOpacity": 1, + "borderWidth": 1, + }, + "searchResult": Object { + "backgroundColor": "#F0F7FF", + "backgroundOpacity": 1, + }, + "selected": Object { + "backgroundColor": "#2B5ECF", + "backgroundOpacity": 0.6, + }, + "unselected": Object { + "backgroundOpacity": 0.3, + "opacity": 0.3, + "textOpacity": 0.3, + }, + }, "padding": Object { "bottom": 4, "left": 8, @@ -1212,6 +1268,34 @@ Object { "horizontalBorderColor": "#52A87D", "horizontalBorderColorOpacity": 1, "horizontalBorderWidth": 1, + "interactionState": Object { + "highlight": Object { + "backgroundColor": "#87B5FF", + "backgroundOpacity": 1, + }, + "hover": Object { + "backgroundColor": "#2B8257", + "backgroundOpacity": 0.6, + }, + "prepareSelect": Object { + "borderColor": "#000000", + "borderOpacity": 1, + "borderWidth": 1, + }, + "searchResult": Object { + "backgroundColor": "#F0F7FF", + "backgroundOpacity": 1, + }, + "selected": Object { + "backgroundColor": "#2B8257", + "backgroundOpacity": 0.6, + }, + "unselected": Object { + "backgroundOpacity": 0.3, + "opacity": 0.3, + "textOpacity": 0.3, + }, + }, "padding": Object { "bottom": 4, "left": 8, @@ -1763,6 +1847,34 @@ Object { "horizontalBorderColor": "#CCDBFC", "horizontalBorderColorOpacity": 1, "horizontalBorderWidth": 1, + "interactionState": Object { + "highlight": Object { + "backgroundColor": "#87B5FF", + "backgroundOpacity": 1, + }, + "hover": Object { + "backgroundColor": "#CCDBFC", + "backgroundOpacity": 0.6, + }, + "prepareSelect": Object { + "borderColor": "#000000", + "borderOpacity": 1, + "borderWidth": 1, + }, + "searchResult": Object { + "backgroundColor": "#F0F7FF", + "backgroundOpacity": 1, + }, + "selected": Object { + "backgroundColor": "#CCDBFC", + "backgroundOpacity": 0.6, + }, + "unselected": Object { + "backgroundOpacity": 0.3, + "opacity": 0.3, + "textOpacity": 0.3, + }, + }, "padding": Object { "bottom": 4, "left": 8, @@ -2314,6 +2426,34 @@ Object { "horizontalBorderColor": "#5184F6", "horizontalBorderColorOpacity": 1, "horizontalBorderWidth": 1, + "interactionState": Object { + "highlight": Object { + "backgroundColor": "#87B5FF", + "backgroundOpacity": 1, + }, + "hover": Object { + "backgroundColor": "#2B5ECF", + "backgroundOpacity": 0.6, + }, + "prepareSelect": Object { + "borderColor": "#000000", + "borderOpacity": 1, + "borderWidth": 1, + }, + "searchResult": Object { + "backgroundColor": "#F0F7FF", + "backgroundOpacity": 1, + }, + "selected": Object { + "backgroundColor": "#2B5ECF", + "backgroundOpacity": 0.6, + }, + "unselected": Object { + "backgroundOpacity": 0.3, + "opacity": 0.3, + "textOpacity": 0.3, + }, + }, "padding": Object { "bottom": 4, "left": 8, @@ -2865,6 +3005,34 @@ Object { "horizontalBorderColor": "#B6C0D7", "horizontalBorderColorOpacity": 1, "horizontalBorderWidth": 1, + "interactionState": Object { + "highlight": Object { + "backgroundColor": "#87B5FF", + "backgroundOpacity": 1, + }, + "hover": Object { + "backgroundColor": "#B6C0D7", + "backgroundOpacity": 0.6, + }, + "prepareSelect": Object { + "borderColor": "#000000", + "borderOpacity": 1, + "borderWidth": 1, + }, + "searchResult": Object { + "backgroundColor": "#F0F7FF", + "backgroundOpacity": 1, + }, + "selected": Object { + "backgroundColor": "#B6C0D7", + "backgroundOpacity": 0.6, + }, + "unselected": Object { + "backgroundOpacity": 0.3, + "opacity": 0.3, + "textOpacity": 0.3, + }, + }, "padding": Object { "bottom": 4, "left": 8, @@ -3416,6 +3584,34 @@ Object { "horizontalBorderColor": "#5184F6", "horizontalBorderColorOpacity": 1, "horizontalBorderWidth": 0, + "interactionState": Object { + "highlight": Object { + "backgroundColor": "#87B5FF", + "backgroundOpacity": 1, + }, + "hover": Object { + "backgroundColor": "#2B5ECF", + "backgroundOpacity": 0.6, + }, + "prepareSelect": Object { + "borderColor": "#000000", + "borderOpacity": 1, + "borderWidth": 1, + }, + "searchResult": Object { + "backgroundColor": "#F0F7FF", + "backgroundOpacity": 1, + }, + "selected": Object { + "backgroundColor": "#2B5ECF", + "backgroundOpacity": 0.6, + }, + "unselected": Object { + "backgroundOpacity": 0.3, + "opacity": 0.3, + "textOpacity": 0.3, + }, + }, "padding": Object { "bottom": 4, "left": 8, diff --git a/packages/s2-react/playground/config.tsx b/packages/s2-react/playground/config.tsx index 4dcd012e31..fe443647a4 100644 --- a/packages/s2-react/playground/config.tsx +++ b/packages/s2-react/playground/config.tsx @@ -368,9 +368,19 @@ export const s2Options: SheetComponentOptions = { }, hoverAfterScroll: true, hoverHighlight: true, - selectedCellHighlight: true, + selectedCellHighlight: false, selectedCellMove: true, rangeSelection: true, + autoResetSheetStyle: (event) => { + // 点击配置面板时不自动重置交互 + if (event?.target instanceof HTMLElement) { + return !document + .querySelector('.ant-collapse') + ?.contains(event?.target); + } + + return true; + }, // 防止 mac 触控板横向滚动触发浏览器返回, 和移动端下拉刷新 overscrollBehavior: 'none', brushSelection: { @@ -410,7 +420,12 @@ export const s2Options: SheetComponentOptions = { // ], // ], tooltip: S2TooltipOptions, - style: {}, + style: { + dataCell: { + width: 200, + height: 200, + }, + }, }; export const sliderOptions: SliderSingleProps = { diff --git a/packages/s2-react/playground/index.tsx b/packages/s2-react/playground/index.tsx index c8f6bd0c3a..a9d677d6eb 100644 --- a/packages/s2-react/playground/index.tsx +++ b/packages/s2-react/playground/index.tsx @@ -42,7 +42,7 @@ import { Tooltip, type RadioChangeEvent, } from 'antd'; -import { debounce, isEmpty } from 'lodash'; +import { debounce, isEmpty, random } from 'lodash'; import React from 'react'; import { ChromePicker } from 'react-color'; import reactPkg from '../package.json'; @@ -909,162 +909,6 @@ function MainLayout() { 改变表格大小 (s2.changeSheetSize) - - - 滚动 - - - -
- 水平滚动速率 : - - 垂直滚动速率 : - -
- - } - > - -
- - - - - - - -
折叠 / 展开 @@ -1217,255 +1061,491 @@ function MainLayout() { key: 'interaction', label: '交互配置', children: ( - - - - - - { - updateOptions({ - interaction: { - selectedCellsSpotlight: checked, - }, - }); - }} - /> - - - { - updateOptions({ - interaction: { - hoverHighlight: checked, - }, - }); - }} - /> - - - { - updateOptions({ - interaction: { - selectedCellHighlight: checked, - }, - }); - }} - /> - - - { - updateOptions({ - interaction: { - hoverFocus: checked, - }, - }); - }} - /> - - - { - updateOptions({ - interaction: { - selectedCellMove: checked, - }, - }); - }} - /> - - - { - updateOptions({ - interaction: { - brushSelection: checked, - }, - }); - }} - /> - - - { - updateOptions({ - interaction: { - multiSelection: checked, - }, - }); - }} - /> - - - { - updateOptions({ - interaction: { - rangeSelection: checked, - }, - }); - }} - /> - - - { - updateOptions({ - interaction: { - hoverAfterScroll: checked, - }, - }); - }} - /> - - - { - updateOptions({ - interaction: { - autoResetSheetStyle: checked, - }, - }); - }} - /> - - -

默认隐藏列

-

明细表: 列头指定 field: number

-

- 透视表: 列头指定id: - root[&]家具[&]沙发[&]number -

- - } - > - -
-
+ <> + + + 配置 + + + + { + updateOptions({ + interaction: { + selectedCellsSpotlight: checked, + }, + }); + }} + /> + + + { + updateOptions({ + interaction: { + hoverHighlight: checked, + }, + }); + }} + /> + + + { + updateOptions({ + interaction: { + selectedCellHighlight: checked, + }, + }); + }} + /> + + + { + updateOptions({ + interaction: { + hoverFocus: checked, + }, + }); + }} + /> + + + { + updateOptions({ + interaction: { + selectedCellMove: checked, + }, + }); + }} + /> + + + { + updateOptions({ + interaction: { + brushSelection: checked, + }, + }); + }} + /> + + + { + updateOptions({ + interaction: { + multiSelection: checked, + }, + }); + }} + /> + + + { + updateOptions({ + interaction: { + rangeSelection: checked, + }, + }); + }} + /> + + + { + updateOptions({ + interaction: { + hoverAfterScroll: checked, + }, + }); + }} + /> + + + { + updateOptions({ + interaction: { + autoResetSheetStyle: checked, + }, + }); + }} + /> + + +

默认隐藏列

+

明细表: 列头指定 field: number

+

+ 透视表: 列头指定id: + root[&]家具[&]沙发[&]number +

+ + } + > + +
+ + + +
+ + + 滚动 + + + +
+ 水平滚动速率 : + + 垂直滚动速率 : + +
+ + } + > + +
+ + + + + + + + + + +
+ + + 高亮 / 选中 + + + + + + + + + + + + + + + + + ), }, { 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 7e9ea02c0d..3372698441 100644 --- a/s2-site/docs/api/basic-class/base-cell.zh.md +++ b/s2-site/docs/api/basic-class/base-cell.zh.md @@ -84,10 +84,8 @@ export enum CellType { ### S2CellType -```ts -import type { SimpleBBox } from '@antv/g-canvas'; - -export type S2CellType = +```ts | pure +export type S2CellType = | DataCell | HeaderCell | ColCell @@ -95,5 +93,5 @@ export type S2CellType = | RowCell | MergedCell | SeriesNumberCell - | BaseCell; + | BaseCell; ``` diff --git a/s2-site/docs/api/basic-class/base-facet.en.md b/s2-site/docs/api/basic-class/base-facet.en.md index 3dc3293738..0f614967ef 100644 --- a/s2-site/docs/api/basic-class/base-facet.en.md +++ b/s2-site/docs/api/basic-class/base-facet.en.md @@ -41,9 +41,9 @@ s2.facet.getRealWidth() | getSeriesNumberWidth | Get the serial number width | () => number | | | getContentHeight | Get the height of the currently rendered area | () => number | | | getPaginationScrollY | x | () => number | | -| updateScrollOffset | scroll | (offsetConfig: [OffsetConfig](#offsetconfig) ) => void | | -| scrollWithAnimation | Scrolling (with easing animation) | (offsetConfig: [OffsetConfig](#offsetconfig) , duration?: number, callback?: () => void) => void | | -| scroll Immediately | scrolling (no animation) | (offsetConfig: [OffsetConfig](#offsetconfig) ) => void | | +| updateScrollOffset | scroll | (offsetConfig: [ScrollOffsetConfig](#offsetconfig) ) => void | | +| scrollWithAnimation | Scrolling (with easing animation) | (offsetConfig: [ScrollOffsetConfig](#offsetconfig) , duration?: number, callback?: () => void) => void | | +| scroll Immediately | scrolling (no animation) | (offsetConfig: [ScrollOffsetConfig](#offsetconfig) ) => void | | | destroy | uninstall | () => void | | | getScrollOffset | Get the current scroll offset | () => [ScrollOffset](#scrolloffset) | | | setScrollOffset | Set the current scroll offset | (scrollOffset: [ScrollOffset](#scrolloffset) ) => void | | @@ -158,10 +158,10 @@ export interface ViewCellHeights { } ``` -### OffsetConfig +### ScrollOffsetConfig ```ts -export interface OffsetConfig { +export interface ScrollOffsetConfig { offsetX?: { value: number | undefined; animate?: boolean; diff --git a/s2-site/docs/api/basic-class/base-facet.zh.md b/s2-site/docs/api/basic-class/base-facet.zh.md index 12557e0526..b3c6f32e70 100644 --- a/s2-site/docs/api/basic-class/base-facet.zh.md +++ b/s2-site/docs/api/basic-class/base-facet.zh.md @@ -41,9 +41,9 @@ s2.facet.getRealWidth() | getSeriesNumberWidth | 获取序号宽度 | () => number | | getContentHeight | 获取当前渲染的区域高度 | () => number | | getPaginationScrollY | x | () => number | -| updateScrollOffset | 滚动 | (offsetConfig: [OffsetConfig](#offsetconfig)) => void | -| scrollWithAnimation | 滚动 (带缓动动画) | (offsetConfig: [OffsetConfig](#offsetconfig), duration?: number, callback?: () => void) => void | -| scrollImmediately | 滚动 (无动画) | (offsetConfig: [OffsetConfig](#offsetconfig)) => void | +| updateScrollOffset | 滚动 | (offsetConfig: [ScrollOffsetConfig](#offsetconfig)) => void | +| scrollWithAnimation | 滚动 (带缓动动画) | (offsetConfig: [ScrollOffsetConfig](#offsetconfig), duration?: number, callback?: () => void) => void | +| scrollImmediately | 滚动 (无动画) | (offsetConfig: [ScrollOffsetConfig](#offsetconfig)) => void | | destroy | 卸载 | () => void | | getScrollOffset | 获取当前滚动偏移 | () => [ScrollOffset](#scrolloffset) | | setScrollOffset | 设置当前滚动偏移 | (scrollOffset: [ScrollOffset](#scrolloffset)) => void | @@ -190,10 +190,11 @@ export interface ViewCellHeights { } ``` -### OffsetConfig +### ScrollOffsetConfig ```ts -export interface OffsetConfig { +export interface ScrollOffsetConfig { + skipScrollEvent?: boolean; rowHeaderOffsetX?: { value: number | undefined; animate?: boolean; diff --git a/s2-site/docs/api/basic-class/interaction.en.md b/s2-site/docs/api/basic-class/interaction.en.md index e26f869d57..dcc8f44651 100644 --- a/s2-site/docs/api/basic-class/interaction.en.md +++ b/s2-site/docs/api/basic-class/interaction.en.md @@ -41,12 +41,12 @@ s2.interaction.reset() | getRowColActiveCells | Get the active cell of row header and column header | `() => RowCell[] \| ColCell[]` | | getAllCells | Get all cells in the visible area | () => [S2CellType](#s2celltype) \[] | | selectAll | select all cells | `() => void` | -| selectHeaderCell | Select the specified row and column header cell | (selectHeaderCellInfo: [SelectHeaderCellInfo](#selectheadercellinfo) ) => boolean | +| changeCell | Select the specified row and column header cell | (changeCellInfo: [ChangeCellOptions](#selectheadercellinfo) ) => boolean | | getCellChildrenNodes | Get all child nodes of the current cell | (cell: [S2CellType](#s2celltype) ) => [Node](\(/docs/api/basic-class/node\)) \[] | | hideColumns | Hidden column (when forceRender is `false` , if the hidden column is empty, the table update will no longer be triggered) | `(hiddenColumnFields: string[], forceRender?: boolean = true) => void` | | mergeCells | Merge Cells | (cellsInfo?: [MergedCellInfo](#mergedcellinfo) \[], hideData?: boolean) => void | | unmergeCells | unmerge cells | `(removedCells: MergedCell[]) => void` | -| updatePanelGroupAllDataCells | update all value cells | `() => void` | +| updateAllDataCells | update all value cells | `() => void` | | updateCells | Update the specified cell | (cells: [S2CellType](#s2celltype) \[]) => void | | addIntercepts | Added interactive interception | (interceptTypes: [InterceptType](#intercepttype) \[]) => void | | hasIntercepts | Whether there is an interaction specified for interception | (interceptTypes: [InterceptType](#intercepttype) \[]) => boolean | @@ -105,10 +105,10 @@ type S2CellType = | BaseCell; ``` -### SelectHeaderCellInfo +### ChangeCellOptions ```ts -interface SelectHeaderCellInfo { +interface ChangeCellOptions { cell: S2CellType; // 目标单元格 isMultiSelection?: boolean; // 是否是多选 } diff --git a/s2-site/docs/api/basic-class/interaction.zh.md b/s2-site/docs/api/basic-class/interaction.zh.md index d62bcec473..419e3a18d1 100644 --- a/s2-site/docs/api/basic-class/interaction.zh.md +++ b/s2-site/docs/api/basic-class/interaction.zh.md @@ -26,14 +26,16 @@ s2.interaction.reset() | setInteractedCells | 设置当前发生改变的单元格 | (cell: [S2CellType](#s2celltype)) => void | | getInteractedCells | 获取当前发生改变的单元格 | () => [S2CellType](#s2celltype)[] | | getCurrentStateName | 获取当前状态名 | `() => void` | -| isEqualStateName | 是否是相同的状态名 | (name: [InteractionStateName](#interactionstatename)) => void | -| isSelectedState | 是否是选中状态 | `() => void` | -| isHoverState | 是否是悬停状态 | `() => void` | -| isHoverFocusState | 是否是悬停聚焦状态 (悬停在单元格 `focusTime`: 默认 800ms 后) | `() => void` | -| isSelectedCell | 是否是选中的单元格 | (cell: [S2CellType](#s2celltype)) => void | -| isActiveCell | 是否是激活的单元格 | (cell: [S2CellType](#s2celltype)) => void | +| isEqualStateName | 是否是相同的状态名 | (name: [InteractionStateName](#interactionstatename)) => boolean | +| isSelectedState | 是否是选中状态 | `() => boolean` | +| isBrushSelectedState | 是否是刷选状态 | `() => boolean` | +| isHoverState | 是否是悬停状态 | `() => boolean` | +| isHoverFocusState | 是否是悬停聚焦状态 (悬停在单元格 `focusTime`: 默认 800ms 后) | `() => boolean` | +| isSelectedCell | 是否是选中的单元格 | (cell: [S2CellType](#s2celltype)) => boolean | +| isActiveCell | 是否是激活的单元格 | (cell: [S2CellType](#s2celltype)) => boolean | | getCells | 获取当前 interaction 记录的 Cells 元信息列表,包括不在可视范围内的单元格 | () => Partial<[ViewMeta](#viewmeta)>[] | | getActiveCells | 获取当前在可视区域的单元格实例 | `() => S2CellType[]` | +| clearStyleIndependent | 清除单元格交互样式 | `() => void` | | getActiveDataCells | 获取当前在可视区域的数值单元格实例 | `() => S2CellType[]` | | getActiveRowCells | 获取当前在可视区域的行头单元格实例 | `() => S2CellType[]` | | getActiveColCells | 获取当前在可视区域的列头单元格实例 | `() => S2CellType[]` | @@ -41,22 +43,105 @@ s2.interaction.reset() | getUnSelectedDataCells | 获取可视区域内选中的数值单元格 | `() => DataCell[]` | | getAllCells | 获取所有可视区域内的单元格 | () => [S2CellType](#s2celltype)[] | | selectAll | 选中所有单元格 | `() => void` | -| selectHeaderCell | 选中指定行列头单元格 | (selectHeaderCellInfo: [SelectHeaderCellInfo](#selectheadercellinfo)) => boolean | | hideColumns | 隐藏列 (forceRender 为 `false` 时,隐藏列为空的情况下,不再触发表格更新) | `(hiddenColumnFields: string[], forceRender?: boolean = true) => void` | | mergeCells | 合并单元格 | (cellsInfo?: [MergedCellInfo](#mergedcellinfo)[], hideData?: boolean) => void | -| unmergeCells | 取消合并单元格 | `(removedCells: MergedCell[]) => void` | -| updatePanelGroupAllDataCells | 更新所有数值单元格 | `() => void` | +| unmergeCell | 取消合并单元格 | `(removedCell: MergedCell) => void` | +| updateAllDataCells | 更新所有数值单元格 | `() => void` | | updateCells | 更新指定单元格 | (cells: [S2CellType](#s2celltype)[]) => void | | addIntercepts | 新增交互拦截 | (interceptTypes: [InterceptType](#intercepttype)[]) => void | | hasIntercepts | 是否有指定拦截的交互 | (interceptTypes: [InterceptType](#intercepttype)[]) => boolean | | removeIntercepts | 移除指定交互拦截 | (interceptTypes: [InterceptType](#intercepttype)[]) => void | | highlightNodes | 高亮节点对应的单元格 | (nodes: [Node](/docs/api/basic-class/node)[]) => void | +| scrollTo | 滚动至指定位置 | (offsetConfig: [ScrollOffsetConfig](#offsetconfig)) => void | | +| scrollToNode | 滚动至指定单元格节点 | (node: [Node](/docs/api/basic-class/node), options?: [CellScrollToOptions](#cellscrolltooptions)) => void | | +| scrollToCell | 滚动至指定单元格 | (cell: [S2CellType](#s2celltype), options?: [CellScrollToOptions](#cellscrolltooptions)) => void | | +| scrollToCellById | 滚动至指定单元格 id 对应的位置,如果不在可视化范围内,则会自动滚动 | (id: string, options?: [CellScrollToOptions](#cellscrolltooptions)) => void | | +| scrollToTop | 滚动至顶部 | (options?: [CellScrollToOptions](#cellscrolltooptions)) => void | | +| scrollToRight | 滚动至右边 | (options?: [CellScrollToOptions](#cellscrolltooptions)) => void | | +| scrollToBottom | 滚动至底部 | (options?: [CellScrollToOptions](#cellscrolltooptions)) => void | | +| scrollToLeft | 滚动至左边 | (options?: [CellScrollToOptions](#cellscrolltooptions)) => void | | +| highlightCell | 高亮指定单元格(可视范围内)| (cell: [S2CellType](#s2celltype)) => void | | +| selectCell | 选中指定单元格(可视范围内)| (cell: [S2CellType](#s2celltype), options: [ChangeCellOptions](#changecelloptions)) => void | | +| changeCell | 改变指定单元格状态(可视范围内)(如:选中/高亮/多选等) | (cell: [S2CellType](#s2celltype), options: [ChangeCellOptions](#changecelloptions)) => void | | | updateDataCellRelevantHeaderCells | 高亮数值单元格和所对应行列单元格 | (stateName: [InteractionStateName](#interactionstatename), meta: [ViewMeta](#viewmeta)) => void | | updateDataCellRelevantRowCells | 高亮数值单元格和所对应行头单元格 | (stateName: [InteractionStateName](#interactionstatename), meta: [ViewMeta](#viewmeta)) => void | | updateDataCellRelevantColCells | 高亮数值单元格和所对应列头单元格 | (stateName: [InteractionStateName](#interactionstatename), meta: [ViewMeta](#viewmeta)) => void | +### CellScrollToOptions + +```ts +export interface CellScrollToOptions { + /** + * 是否展示滚动动画 + */ + animate?: boolean; + + /** + * 是否触发滚动事件 + */ + skipScrollEvent?: boolean; +} +``` + +### ChangeCellOptions + +```ts + +export interface ChangeCellOptions { + /** + * 目标单元格 + */ + cell: S2CellType; + + /** + * 是否是多选 + */ + isMultiSelection?: boolean; + + /** + * 状态名 (默认 `selected`) + */ + stateName?: InteractionStateName; + + /** + * 如果单元格不在可视范围,是否自动滚动 + */ + scrollIntoView?: boolean; +} +``` + +### ScrollOffsetConfig + +```ts +export interface ScrollOffsetConfig { + skipScrollEvent?: boolean; + rowHeaderOffsetX?: { + value: number | undefined; + animate?: boolean; + }; + offsetX?: { + value: number | undefined; + animate?: boolean; + }; + offsetY?: { + value: number | undefined; + animate?: boolean; + }; +} +``` + +### ScrollOffset + +```ts +export interface ScrollOffset { + scrollX?: number; + scrollY?: number; + rowHeaderScrollX?: number; +} +``` + ### InteractionConstructor ```ts @@ -112,12 +197,29 @@ type S2CellType = | BaseCell; ``` -### SelectHeaderCellInfo +### ChangeCellOptions ```ts -interface SelectHeaderCellInfo { - cell: S2CellType; // 目标单元格 - isMultiSelection?: boolean; // 是否是多选 +interface ChangeCellOptions { + /** + * 目标单元格 + */ + cell: S2CellType; + + /** + * 是否是多选 + */ + isMultiSelection?: boolean; + + /** + * 状态名 + */ + stateName?: InteractionStateName; + + /** + * 如果单元格不在可视范围,是否自动滚动 + */ + scrollIntoView?: boolean; } ``` @@ -197,8 +299,8 @@ interface InteractionStateInfo { | domEventListeners | 当前已注册的交互 | [EventHandler](#eventhandler)[] | | isCanvasEffect | 是否是图表内部引起的事件 | boolean | | canvasMousemoveEvent | 表格鼠标移动事件 | CanvasEvent | -| isMatchElement | 是否是表格内部的元素 | (event: MouseEvent) => void | -| isMatchPoint | 是否是表格内部的坐标 | (event: MouseEvent) => void | +| isMatchElement | 是否是表格内部的元素 | (event: MouseEvent) => boolean | +| isMatchPoint | 是否是表格内部的坐标 | (event: MouseEvent) => boolean | | bindEvents | 绑定交互事件 | `() => void`) | | clear | 清空交互事件 | `() => void`) | | getViewportPoint | 获取表格内的鼠标坐标 (兼容 `supportsCSSTransform`) | `(event: MouseEvent \| PointerEvent \| WheelEvent) => PointLike` | diff --git a/s2-site/docs/api/basic-class/spreadsheet.en.md b/s2-site/docs/api/basic-class/spreadsheet.en.md index 7ea09c2b0d..603db7d4eb 100644 --- a/s2-site/docs/api/basic-class/spreadsheet.en.md +++ b/s2-site/docs/api/basic-class/spreadsheet.en.md @@ -52,7 +52,6 @@ s2.isPivotMode() | getRowLeafNodes | Get the row header leaf node | () => [Node\[\]](/docs/api/basic-class/node/) | | | facet.getColNodes | Get column head node | (level: number) => [Node\[\]](/docs/api/basic-class/node/) | | | facet.getColLeafNodes | Get the column head leaf node | () => [Node\[\]](/docs/api/basic-class/node/) | | -| updateScrollOffset | update scroll offset | (config: [OffsetConfig](#offsetconfig) ) => void | | | getCell | Get the current cell according to event.target | (target: [EventTarget](https://developer.mozilla.org/zh-CN/docs/Web/API/Event/target) ) => [S2CellType](/docs/api/basic-class/base-cell#s2celltype) | | | getCellType | Get the current cell type according to event.target | (target: [EventTarget](https://developer.mozilla.org/zh-CN/docs/Web/API/Event/target) ) => [CellType](/docs/api/basic-class/base-cell#celltypes) | | | getTotalsConfig | Get Total Subtotal Configuration | (dimension: string) => [Total](/docs/api/general/S2Options#totals) | | @@ -69,12 +68,12 @@ s2.isPivotMode() type S2MountContainer = string | HTMLElement; ``` -### OffsetConfig +### ScrollOffsetConfig Function description: Scroll offset configuration ```ts -interface OffsetConfig { +interface ScrollOffsetConfig { offsetX?: { value: number | undefined; animate?: boolean; diff --git a/s2-site/docs/api/basic-class/spreadsheet.zh.md b/s2-site/docs/api/basic-class/spreadsheet.zh.md index a19dc4b525..734fb0d427 100644 --- a/s2-site/docs/api/basic-class/spreadsheet.zh.md +++ b/s2-site/docs/api/basic-class/spreadsheet.zh.md @@ -53,7 +53,6 @@ s2.isPivotMode() | getContentHeight | 获取当前表格实际内容高度 | `() => number` | | | changeSheetSize | 修改表格画布大小,不用重新加载数据 | `(width?: number, height?: number) => void` | | | getLayoutWidthType | 获取单元格宽度布局类型(LayoutWidthType: `adaptive(自适应)` \| `colAdaptive(列自适应)` \| `compact(紧凑)`) | () => `LayoutWidthType`| | -| updateScrollOffset | 更新滚动偏移 | (config: [OffsetConfig](#offsetconfig)) => void | | | getCell | 根据 event.target 获取当前 单元格 | (target: [EventTarget](https://developer.mozilla.org/zh-CN/docs/Web/API/Event/target)) => [S2CellType](/docs/api/basic-class/base-cell#s2celltype) | | | getCellType | 根据 event.target 获取当前 单元格类型 | (target: [EventTarget](https://developer.mozilla.org/zh-CN/docs/Web/API/Event/target)) => [CellType](/docs/api/basic-class/base-cell#celltypes) | | | getTotalsConfig | 获取总计小计配置 | (dimension: string) => [Total](/docs/api/general/S2Options#totals) | | @@ -74,12 +73,12 @@ s2.isPivotMode() type S2MountContainer = string | HTMLElement; ``` -### OffsetConfig +### ScrollOffsetConfig 功能描述:滚动偏移配置 ```ts -interface OffsetConfig { +interface ScrollOffsetConfig { rowHeaderOffsetX?: { value: number | undefined; animate?: boolean; diff --git a/s2-site/docs/api/general/S2Options.zh.md b/s2-site/docs/api/general/S2Options.zh.md index c4048cab0d..823a2faef8 100644 --- a/s2-site/docs/api/general/S2Options.zh.md +++ b/s2-site/docs/api/general/S2Options.zh.md @@ -52,6 +52,8 @@ const s2Options = { | facet | (spreadsheet: [SpreadSheet](/api/basic-class/spreadsheet)) => [BaseFacet](/api/basic-class/base-facet) | | | 自定义分面 | | transformCanvasConfig | (renderer: [Renderer](https://g.antv.antgroup.com/api/canvas/options#renderer), spreadsheet: [SpreadSheet](/api/basic-class/spreadsheet)) => Partial<[CanvasConfig](https://g.antv.antgroup.com/api/canvas/options)> \| void | | `-` | 自定义 AntV/G 渲染引擎 [配置参数](https://g.antv.antgroup.com/api/canvas/options) & [插件注册](https://g.antv.antgroup.com/plugins/intro) | + + @@ -70,8 +72,8 @@ const s2Options = { ## DataCellCallback -```js -DataCellCallback = (viewMeta: ViewMeta, spreadsheet: SpreadSheet) => G.Group; +```js | pure +DataCellCallback = (viewMeta: ViewMeta, s2: Spreadsheet) => G.Group; ``` 功能描述:自定义数值单元格。[查看示例](/examples/custom/custom-cell#data-cell) @@ -80,7 +82,7 @@ DataCellCallback = (viewMeta: ViewMeta, spreadsheet: SpreadSheet) => G.Group; ## CellCallback -```js +```js | pure CellCallback = (node: Node, spreadsheet: SpreadSheet, ...restOptions: unknown[]) => G.Group; ``` @@ -94,8 +96,8 @@ CellCallback = (node: Node, spreadsheet: SpreadSheet, ...restOptions: unknown[]) ## MergedCellCallback -```js -DataCellCallback = (s2: Spreadsheet, cells: S2CellType[],viewMeta: ViewMeta) => MergedCell; +```js | pure +DataCellCallback = (s2: Spreadsheet, cells: S2CellType[], viewMeta: ViewMeta) => MergedCell; ``` 功能描述:自定义合并单元格。[查看示例](/examples/custom/custom-cell/#custom-merged-cell) @@ -104,7 +106,7 @@ DataCellCallback = (s2: Spreadsheet, cells: S2CellType[],viewMeta: ViewMeta) => ## CornerHeaderCallback -```js +```js | pure CornerHeaderCallback = (parent: S2CellType, spreadsheet: SpreadSheet, ...restOptions: unknown[]) => void; ``` @@ -223,7 +225,7 @@ export type Data = RawData & ExtraData; 功能描述:自定义数据集。[查看示例](/examples/custom/custom-dataset/#custom-strategy-sheet-dataset) -```js +```js | pure DataSet = (spreadsheet: SpreadSheet) => BaseDataSet; ``` diff --git a/s2-site/docs/common/conditions.zh.md b/s2-site/docs/common/conditions.zh.md index bc177f9aa1..d1d3169bc9 100644 --- a/s2-site/docs/common/conditions.zh.md +++ b/s2-site/docs/common/conditions.zh.md @@ -9,14 +9,14 @@ order: 2 | 参数 | 说明 | 类型 | 默认值 | 必选 | | ---------- | -------------- | --------------------------------------------- | ------ | ---- | -| text | 文本字段标记 | [TextCondition](#textcondition)[] | - | | -| background | 背景字段标记 | [BackgroundCondition](#backgroundcondition)[] | - | | -| interval | 柱状图字段标记 | [IntervalCondition](#intervalcondition)[] | - | | -| icon | 图标字段标记 | [IconCondition](#iconcondition)[] | - | | +| text | 文本字段标记 ([查看示例](/examples/analysis/conditions/#text)) | [TextCondition](#textcondition)[] | - | | +| background | 背景字段标记 ([查看示例](/examples/analysis/conditions/#background)) | [BackgroundCondition](#backgroundcondition)[] | - | | +| interval | 柱状图字段标记 ([查看示例](/examples/analysis/conditions/#interval)) | [IntervalCondition](#intervalcondition)[] | - | | +| icon | 图标字段标记 ([查看示例](/examples/analysis/conditions/#icon)) | [IconCondition](#iconcondition)[] | - | | ### Condition -功能描述: 配置条件格式。TextCondition,BackgroundCondition,IntervalCondition,IconCondition 具继承于 Condition。 +功能描述: 配置条件格式。TextCondition,BackgroundCondition,IntervalCondition,IconCondition 均继承于 Condition。 | 参数 | 说明 | 类型 | 默认值 | 必选 | | ------- | ---------------------------------------------- | ------------------------------------- | ------ | ---- | @@ -45,17 +45,25 @@ const options = { text: [ { field: "province", - mapping: () => ({ - fill: "rgba(0, 0, 0, .65)", - }), + mapping: (fieldValue, data, cell) => { + return { + fill: "green", + fontSize: 16, + opacity: 0.2, + textAlign: 'right' + }; + }, }, ], interval: [ { field: "sub_type", - mapping: () => { + mapping: (fieldValue, data, cell) => { return { fill: "green", + isCompare: true, + maxValue: 8000, + minValue: 300, }; }, }, @@ -63,9 +71,24 @@ const options = { background: [ { field: "count", - mapping: () => ({ - fill: "#ff00ff", - }), + mapping: (fieldValue, data, cell) => { + return { + fill: "green", + intelligentReverseTextColor: true, + }; + }, + }, + ], + icon: [ + { + field: "number", + position: 'left', + mapping: (fieldValue, data, cell) => { + return { + icon: "InfoCircle", + fill: "green", + }; + }, }, ], }, @@ -73,9 +96,47 @@ const options = { ``` +### TextCondition + +同 [Condition](#condition) 一致,`ConditionMappingResult` 配置和 [文本主题配置一致(部分生效)](/api/general/s2-theme#texttheme), 也就意味着可以控制不同文本的颜色,透明度,对齐方式,字体等配置。 + +```ts +export type TextConditionMappingResult = TextTheme; +``` + +[查看示例](/examples/analysis/conditions/#text) + +### BackgroundCondition + +同 [Condition](#condition) 一致,`ConditionMappingResult` 配置为: + +```ts +export type BackgroundConditionMappingResult = { + fill: string; + intelligentReverseTextColor?: boolean; +}; +``` + +[查看示例](/examples/analysis/conditions/#background) + +### IntervalCondition + +同 [Condition](#condition) 一致,`ConditionMappingResult` 配置为: + +```ts +export type IntervalConditionMappingResult = { + fill?: string; + isCompare?: boolean; + minValue?: number; + maxValue?: number; +} +``` + +[查看示例](/examples/analysis/conditions/#interval) + ### IconCondition -功能描述: 配置图标 (icon) 条件格式,和其他 Condition 的唯一区别在于多了 position 参数用于自定义 icon 相对于文本的位置。查看 [文档](/manual/basic/conditions) 和 [示例](/examples/analysis/conditions/#icon) +功能描述: 配置图标 (icon) 条件格式,和其他 [Condition](#condition) 的唯一区别在于多了 position 参数用于自定义 icon 相对于文本的位置。查看 [文档](/manual/basic/conditions) 和 [示例](/examples/analysis/conditions/#icon) | 参数 | 说明 | 类型 | 默认值 | 必选 | | -------- | --------------------- | --------------- | ------- | ---- | @@ -90,7 +151,7 @@ const options = { { field: "profit", position: "left", - mapping: () => { + mapping: (fieldValue, data, cell) => { return { icon: "InfoCircle", fill: "red", @@ -103,4 +164,13 @@ const options = { ``` +`ConditionMappingResult` 配置为: + +```ts +export type IconConditionMappingResult = { + fill: string; + icon: string; +}; +``` + ​ diff --git a/s2-site/docs/common/interaction.zh.md b/s2-site/docs/common/interaction.zh.md index 241f31d3a4..e210ad6901 100644 --- a/s2-site/docs/common/interaction.zh.md +++ b/s2-site/docs/common/interaction.zh.md @@ -17,7 +17,7 @@ order: 5 | copy | 单元格复制配置 | [Copy](#copy) | | | | customInteractions | 自定义交互 [详情](/docs/manual/advanced/interaction/custom) | [CustomInteraction[]](#custominteraction) | | | | scrollSpeedRatio | 用于控制滚动速率,分水平和垂直两个方向,默认为 1 | [ScrollSpeedRatio](#scrollspeedratio) | | | -| autoResetSheetStyle | 用于控制点击表格外区域和按下 esc 键时是否重置交互状态 | `boolean` | `true` | | +| autoResetSheetStyle | 用于控制点击表格外区域和按下 `ESC` 键时是否重置交互状态和关闭 Tooltip, 支持根据 event 动态判断 | `boolean \| (event: Event \| FederatedPointerEvent, spreadsheet: SpreadSheet) => boolean` | `true` | | | resize | 用于控制 resize 热区是否显示 | `boolean` \| [ResizeInteractionOptions](#resizeinteractionoptions) | `true` | | | brushSelection | 是否允许单元格(包含行头,列头,数值单元格)刷选。行头,列头刷选只支持透视表 | `boolean` \| [BrushSelection](#brushSelection) | `true` | | 1.29.0 后支持 [BrushSelection](#brushSelection) | | multiSelection | 是否允许多选 (包含行头,列头,数值单元格) | `boolean` | `true` | | diff --git a/s2-site/docs/manual/advanced/chart-in-cell.zh.md b/s2-site/docs/manual/advanced/chart-in-cell.zh.md index 5ef2bde2d9..5bdc8baa68 100644 --- a/s2-site/docs/manual/advanced/chart-in-cell.zh.md +++ b/s2-site/docs/manual/advanced/chart-in-cell.zh.md @@ -433,6 +433,13 @@ import { Image as GImage } from '@antv/g'; import { CornerCell } from '@antv/s2'; class CustomCornerCell extends CornerCell { + initCell() + super.initCell() + + // 绘制任意图形 + this.appendChild(...) + } + drawBackgroundShape() { const url = 'https://gw.alipayobjects.com/zos/antfincdn/og1XQOMyyj/1e3a8de1-3b42-405d-9f82-f92cb1c10413.png'; @@ -513,8 +520,39 @@ targetCell?.appendChild( ); ``` -#### 3.4 效果 +#### 3.4 手动获取指定单元格实例 (Group) 后绘制 icon + +表格内的 `Icon` 也是一种特殊图形,可以通过 `GuiIcon` 生成图标实例,然后绘制。 + +```ts +import { GuiIcon } from '@antv/s2'; + +await s2.render(); + +const targetCell = s2.facet.getDataCells()[0]; + +const size = 12; +const meta = targetCell.getMeta(); + +// 例:绘制在右下角 +const icon = new GuiIcon({ + x: meta.x + meta.width - size, + y: meta.y + meta.height - size, + name: 'Trend', + width: size, + height: size, + fill: 'red', +}); + +icon.addEventListener('click', (e) => { + console.log('trend icon click:', e); +}); + +targetCell.appendChild(icon); +``` + +#### 3.5 效果 -preview +preview [查看示例](/examples/custom/custom-shape-and-chart/#custom-g-shape) diff --git a/s2-site/docs/manual/advanced/custom/cell-align.zh.md b/s2-site/docs/manual/advanced/custom/cell-align.zh.md index f21c1d5de2..837bbfd8c2 100644 --- a/s2-site/docs/manual/advanced/custom/cell-align.zh.md +++ b/s2-site/docs/manual/advanced/custom/cell-align.zh.md @@ -5,7 +5,9 @@ tag: Updated --- -> **在阅读本节内容前,请确保你已经阅读 [主题配置](/docs/manual/basic/theme) 文档** +:::warning{title='提示'} +在阅读本节内容前,请确保你已经阅读 [主题配置](/docs/manual/basic/theme) 文档 +::: 为方便用户查看数据,S2 交叉表会在滑动过程中,保证行头和列头的最大可见性。因此,S2 在行头、列头对齐方式所对应的的范围是当前格子的可视区域,而角头,数据单元格所对应的范围是当前格子的实际尺寸区域。 diff --git a/s2-site/docs/manual/advanced/custom/custom-icon.zh.md b/s2-site/docs/manual/advanced/custom/custom-icon.zh.md index 1af64952fe..ea4f596817 100644 --- a/s2-site/docs/manual/advanced/custom/custom-icon.zh.md +++ b/s2-site/docs/manual/advanced/custom/custom-icon.zh.md @@ -177,4 +177,8 @@ const s2Options = { +### 自定义绘制 icon + +S2 表格是一个 Canvas 画布,所以你可以绘制任意的图形在表格里,比如 `icon`, 可以查看 [单元格内绘制图标和图形](/manual/advanced/chart-in-cell#3-%E7%BB%98%E5%88%B6-g-%E8%87%AA%E5%AE%9A%E4%B9%89%E5%9B%BE%E5%BD%A2) 章节了解更多。 + diff --git a/s2-site/docs/manual/advanced/get-cell-data.zh.md b/s2-site/docs/manual/advanced/get-cell-data.zh.md index 0331a6d991..a4b58f9cf2 100644 --- a/s2-site/docs/manual/advanced/get-cell-data.zh.md +++ b/s2-site/docs/manual/advanced/get-cell-data.zh.md @@ -184,6 +184,8 @@ s2.interaction.getCells(); s2.interaction.getActiveCells(); // 是否是选中状态 s2.interaction.isSelectedState(); +// 是否是刷选状态 +s2.interaction.isBrushSelectedState(); // 获取当前交互状态 s2.interaction.getCurrentStateName(); // 获取当前发生过交互的单元格 @@ -351,7 +353,7 @@ s2.on(S2Event.DATA_CELL_CLICK, (event) => { // 找到 "舟山市" 对应的行头单元格节点 const rowCellNode = s2.facet.getRowNodes().find((node) => node.id === 'root[&]浙江省[&]舟山市') // 找到 "办公用品" 下 "纸张" 对应的 "数量"列头单元格节点 -const colCellNode = s2.facet.getColNodes().find((node) => node.id === 'root[&]办公用品[&]纸张[&]数量') +const colCellNode = s2.facet.getColNodes().find((node) => node.id === 'root[&]办公用品[&]纸张[&]number') const data = s2.dataSet.getCellMultiData({ query: { diff --git a/s2-site/docs/manual/advanced/interaction/basic.zh.md b/s2-site/docs/manual/advanced/interaction/basic.zh.md index 87fda41c5d..0032279b10 100644 --- a/s2-site/docs/manual/advanced/interaction/basic.zh.md +++ b/s2-site/docs/manual/advanced/interaction/basic.zh.md @@ -428,6 +428,24 @@ const s2Options = { }; ``` +也可以根据当前 `event` 动态判断是否重置,如:点击指定容器或按钮时不自动重置交互。 + +```ts | pure +const s2Options = { + interaction: { + autoResetSheetStyle: (event, spreadsheet) => { + if (event?.target instanceof HTMLElement) { + return !document + .querySelector('.container') + ?.contains(event?.target); + } + + return true; + }, + } +}; +``` + [查看示例](/examples/interaction/basic/#auto-reset-sheet-style) ## 调整交互主题 @@ -456,11 +474,15 @@ s2.interaction.removeIntercepts([InterceptType.HOVER, InterceptType.CLICK]); ## 调用 API -`S2` 内置了一些交互相关的 `API`,统一挂载在 `s2.interaction` 命名空间下,你可以在拿到 [SpreadSheet 实例](/docs/api/basic-class/spreadsheet) 后调用它们来实现你的效果,比如 `选中所有单元格`, `获取列头单元格` 等常用方法,具体请查看 [Interaction 实例类](/docs/api/basic-class/interaction) 和 [示例](/examples/analysis/get-data/#get-cell-data) +`S2` 内置了一些交互相关的 `API`,统一挂载在 `s2.interaction` 命名空间下,你可以在拿到 [SpreadSheet 实例](/docs/api/basic-class/spreadsheet) 后调用它们来实现你的效果,比如 `选中所有单元格`, `获取列头单元格` 等常用方法,具体请查看 [Interaction 实例类](/docs/api/basic-class/interaction) 和 [示例](/examples/interaction/basic/#event) ```ts const s2 = new PivotSheet() + s2.interaction.selectAll() +s2.interaction.selectCell() +s2.interaction.highlightCell() +s2.interaction.changeCell() ``` [查看示例](/examples/interaction/basic/#event) diff --git a/s2-site/docs/manual/advanced/interaction/hide-columns.zh.md b/s2-site/docs/manual/advanced/interaction/hide-columns.zh.md index c03a468396..697bd97d21 100644 --- a/s2-site/docs/manual/advanced/interaction/hide-columns.zh.md +++ b/s2-site/docs/manual/advanced/interaction/hide-columns.zh.md @@ -1,19 +1,20 @@ --- title: 隐藏列头 order: 2 +tag: Updated --- -当你想降低不重要信息干扰时,可以隐藏列头,方便你更直观的查看数据,有三种方式隐藏列头 +当你想降低不重要信息干扰时,可以隐藏列头,方便你更直观的查看数据,有三种方式隐藏列头。 ## 1. 手动隐藏 - 通过点击 -点击列头在弹出的 `tooltip` 里,点击 `隐藏` 按钮即可 +在 `@antv/s2` 中,可以通过 [自定义 tooltip 内容](/examples/interaction/advanced/#pivot-hide-columns) 的方式添加 `隐藏按钮`, `@antv/s2-react` 中已经内置,点击列头 `tooltip` 内的 `隐藏` 按钮即可。 preview -关闭交互式隐藏 +关闭交互式隐藏: ```ts const s2Options = { @@ -27,11 +28,11 @@ const s2Options = { ## 2. 自动隐藏 - 通过配置 -可配置默认隐藏的列头,透视表和明细表 +可配置默认隐藏的列头,支持透视表和明细表。 ### 1. 明细表 -明细表不存在多列头,指定 `fields` 的 `columns` 里面任意字段即可 +如果是单列头的明细表,指定 `s2DataConfig.fields.columns` 的任意字段即可。 ```ts const s2DataConfig = { @@ -49,17 +50,51 @@ const s2Options = { ![preview](https://gw.alipayobjects.com/zos/antfincdn/GHizMg2ok/f8d667c9-910a-40da-a6e3-74c238e7afa8.png) +对于 [自定义列头](/manual/advanced/custom/custom-header#21-%E8%87%AA%E5%AE%9A%E4%B9%89%E5%88%97%E5%A4%B4) 的明细表,指定 `field` 字段。 + +```ts +const s2DataConfig = { + fields: { + columns: [ + { + field: 'a-1', + title: '自定义节点 a-1', + children: [ + { + field: 'a-1-1', + title: '自定义节点 a-1-1', + }, + ], + }, + { + field: 'a-2', + title: '自定义节点 a-2', + children: [], + } + ] + } +}; + +const s2Options = { + interaction: { + hiddenColumnFields: ['a-1-1'] + } +} +``` + ### 2. 透视表 -透视表存在多列头,需要指定列头对应的 [节点 id](/docs/api/basic-class/node) +透视表存在多列头,需要指定列头对应的 [节点 id](/docs/api/basic-class/node), 如果是 [自定义列头](/manual/advanced/custom/custom-header#12-%E8%87%AA%E5%AE%9A%E4%B9%89%E5%88%97%E5%A4%B4) , 那么和明细表相同,指定 `field` 字段即可,这里不再赘述。
- 如何获取列头 Id? + 如何获取列头 ID? -```ts -// /docs/api/basic-class/spreadsheet +```ts | pure const s2 = new PivotSheet() -console.log(s2.facet.getColCellNodes()) + +await s2.render() + +console.log(s2.facet.getColNodes()) ```
diff --git a/s2-site/docs/manual/advanced/interaction/highlight-and-select-cell.en.md b/s2-site/docs/manual/advanced/interaction/highlight-and-select-cell.en.md new file mode 100644 index 0000000000..caacfc04d5 --- /dev/null +++ b/s2-site/docs/manual/advanced/interaction/highlight-and-select-cell.en.md @@ -0,0 +1,5 @@ +--- +title: Highlight and select cell +order: 8 +tag: New +--- diff --git a/s2-site/docs/manual/advanced/interaction/highlight-and-select-cell.zh.md b/s2-site/docs/manual/advanced/interaction/highlight-and-select-cell.zh.md new file mode 100644 index 0000000000..ac0735638b --- /dev/null +++ b/s2-site/docs/manual/advanced/interaction/highlight-and-select-cell.zh.md @@ -0,0 +1,132 @@ +--- +title: 高亮/选中单元格 +order: 8 +tag: New +--- + +:::warning{title="注意"} +阅读本章前,请确保已经阅读了 [基础交互](/manual/advanced/interaction/basic) 和 [获取单元格数据](/manual/advanced/get-cell-data) 等章节,并对 [布局流程](/manual/extended-reading/layout/pivot) 有所了解。 +::: + +我们通过鼠标悬停 (hover) 和点击 (click) 可以触发表格单元格的 `高亮` 和 `选中`,在一些特定场景下,如果希望主动触发,可以通过内置的 [交互 API](/api/basic-class/interaction) 来实现。 + + + +## 高亮单元格 + +高亮的效果和默认的 [主题配置](/manual/advanced/interaction/basic#%E8%B0%83%E6%95%B4%E4%BA%A4%E4%BA%92%E4%B8%BB%E9%A2%98) 一致,内部状态为 `"hover"` + +```ts | pure +const targetCell = s2.facet.getRowCell()[0] +s2.interaction.highlightCell(targetCell) + +s2.interaction.getCurrentStateName() // "hover" +``` + +preview + +### 高亮子节点 + +将一组子节点对应的单元格设置为高亮状态。 + +```ts | pure +const targetNodes = s2.facet.getRowNodes() + +s2.interaction.highlightNodes(targetNodes) +``` + +## 选中单元格 + +选中的效果和默认的 [主题配置](/manual/advanced/interaction/basic#%E8%B0%83%E6%95%B4%E4%BA%A4%E4%BA%92%E4%B8%BB%E9%A2%98) 一致,内部状态为 `"selected"` + +```ts | pure +const targetCell = s2.facet.getRowCell()[0] +s2.interaction.selectCell(targetCell) + +s2.interaction.getCurrentStateName() // "selected" +``` + +preview + +### 全选 + +全部的单元格都会被更新为选中的样式,内部状态为 `"allSelected"` + +```ts | pure +s2.interaction.selectAll() + +s2.interaction.getCurrentStateName() // "allSelected" +``` + +### 获取选中数据 + +具体请查看 [获取单元格数据 - 选中单元格](/manual/advanced/get-cell-data#%E8%8E%B7%E5%8F%96%E9%80%89%E4%B8%AD%E7%9A%84%E5%8D%95%E5%85%83%E6%A0%BC) + +## 改变单元格状态 + +本质上 `highlightCell` 和 `selectCell` 内部都是基于 `changeCell` 实现的语法糖,所以你也可以直接使用 `changeCell` 实现更细致的状态更新,如 S2 内置的行列头选中也是基于该 API 实现。 + +```ts | pure +import { InteractionStateName } from '@antv/s2' + +const targetCell = s2.facet.getRowCell()[0] + +s2.interaction.changeCell({ + cell: targetCell, + stateName: InteractionStateName.SELECTED, + isMultiSelection: false, + scrollIntoView: false, +}) +``` + +默认会滚动至可视范围内,可以通过 `scrollIntoView` 禁用。 + +也可以直接使用 `changeState` 直接更新指定单元格的状态 + +```ts | pure +import { InteractionStateName, getCellMeta } from '@antv/s2' + +const targetCell = s2.facet.getRowCell()[0] + +// 选中 +s2.interaction.changeState({ + cells: [getCellMeta(targetCell)], + stateName: InteractionStateName.SELECTED, +}); + +// 取消选中 +s2.interaction.changeState({ + cells: [], + stateName: InteractionStateName.UNSELECTED, +}); +``` + +## 更新单元格 + +每一个 [单元格实例](/api/basic-class/base-cell) 都有一个 `update` 方法,调用它可以进行重绘,从而实现单元格的手动更新。 + +```ts | pure +const targetCell = s2.facet.getRowCell()[0] + +targetCell.update() +``` + +## 关闭滚动动画 + +当选中或高亮单元格时,会自动滚动,可以通过 `animate: false` 关闭动画。 + +```ts | pure +s2.interaction.selectCell(targetCell, { + animate: false +}) +``` + +## 触发滚动事件 + +自动滚动时,默认不会触发内部滚动事件,即 `S2Event.GLOBAL_SCROLL`, 可以通过 `skipScrollEvent: false` 禁用。 + +```ts | pure +s2.interaction.selectCell(targetCell, { + skipScrollEvent: false +}) +``` diff --git a/s2-site/docs/manual/advanced/interaction/resize.zh.md b/s2-site/docs/manual/advanced/interaction/resize.zh.md index 4587e4e9c5..30ba763f39 100644 --- a/s2-site/docs/manual/advanced/interaction/resize.zh.md +++ b/s2-site/docs/manual/advanced/interaction/resize.zh.md @@ -108,7 +108,7 @@ const s2Options = { resize: { visible: (cell) => { const meta = cell.getMeta(); - return meta.id === 'root[&]家具[&]桌子[&]数量' + return meta.id === 'root[&]家具[&]桌子[&]number' } } }, diff --git a/s2-site/docs/manual/advanced/interaction/scroll.zh.md b/s2-site/docs/manual/advanced/interaction/scroll.zh.md index 898797830a..50b5d83b7c 100644 --- a/s2-site/docs/manual/advanced/interaction/scroll.zh.md +++ b/s2-site/docs/manual/advanced/interaction/scroll.zh.md @@ -1,6 +1,7 @@ --- title: 滚动 order: 6 +tag: Updated --- ## 虚拟滚动 @@ -104,3 +105,33 @@ s2.on(S2Event.ROW_CELL_SCROLL, (position) => { - [循环滚动](/examples/interaction/advanced#scroll-loop) + +```ts +// 滚动至指定位置 +s2.interaction.scrollTo({ + offsetX: { value: 100, animate: true }, + offsetY: { value: 100, animate: true }, +}) + +// 滚动至顶部 +s2.interaction.scrollToTop({ animate: true }) +``` + +## 不触发滚动事件 + +在手动触发表格滚动时,如果不希望触发内部滚动事件,即 `S2Event.GLOBAL_SCROLL`, 可以通过 `skipScrollEvent: true` 禁用。 + +```diff | pure +s2.interaction.scrollTo({ + offsetX: { value: 100, animate: true }, + offsetY: { value: 100, animate: true }, ++ skipScrollEvent: true +}) + +s2.interaction.scrollToTop({ + animate: true, ++ skipScrollEvent: true +}) +``` + +查看更多 [API](/api/basic-class/interaction) diff --git a/s2-site/docs/manual/basic/conditions.zh.md b/s2-site/docs/manual/basic/conditions.zh.md index 3f326c1d09..9f1e6c88f4 100644 --- a/s2-site/docs/manual/basic/conditions.zh.md +++ b/s2-site/docs/manual/basic/conditions.zh.md @@ -19,10 +19,9 @@ tag: Updated ## 快速上手 -`S2` 字段标记特性通过配置 `s2Options` 中 [`Conditions`](/docs/api/general/S2Options#conditions) 属性。 +`S2` 字段标记特性通过配置 `s2Options` 中 [`Conditions`](/api/general/S2Options#conditions) 属性。 ```ts -// 构建 options const s2Options = { width: 600, height: 600, @@ -30,6 +29,7 @@ const s2Options = { conditions: { text: [ { + // 维度字段,支持正则 /^price+$/ field: "price", mapping(fieldValue, data) { return { @@ -45,39 +45,39 @@ const s2Options = { ## 配置解释 -[Conditions 属性](/docs/api/general/S2Options#conditions) 可配置四种不同的字段,分别对应四种不同的字段标记。 +[Conditions 属性](/api/general/S2Options#conditions) 可配置四种不同的字段,分别对应四种不同的字段标记。 -* `text`,`background` 和 `interval` ,`icon` 均是继承自 [Condition](/docs/api/general/S2Options#condition) 数组类型 - * 包含 `field` 和 `mapping` 两个字段 - * 一个字段 ID 多次匹配到同一范围的字段标记规则,以最后一个规则为准 -* `icon` 稍有不同,为 [IconCondition](/docs/api/general/S2Options#iconcondition) 数组类型 - * 多一个额外的`position` 字段用于指定图标相对于文字的位置,定义图标相对于单元格文本的位置。这个位置可以是文本的左侧、右侧 +* `text`,`background` 和 `interval` ,`icon` 均是继承自 [Condition](/api/general/S2Options#condition) 数组类型 + * 包含 `field` 和 `mapping` 两个字段。 + * 一个字段 ID 多次匹配到同一范围的字段标记规则,**以最后一个规则为准**. +* `icon` 稍有不同,为 [IconCondition](/api/general/S2Options#iconcondition) 数组类型。 + * 多一个额外的 `position` 字段用于指定图标相对于文字的位置,定义图标相对于单元格文本的位置。这个位置可以是文本的左侧、右侧。 ### field `field` 用于指定将字段标记应用于哪些字段上,其取值范围会因表的形态不同而不同: -* 对于透视表,`field` 取值范围或正则匹配范围是 `rows`,`columns`,`values`,作用范围为行头、列头、角头和数据单元格 -* 对于明细表,`field` 取值范围或正则匹配范围是 `columns`,作用范围为数据单元格 +* 对于透视表,`field` 取值或正则匹配范围是 `rows`,`columns`,`values`,作用范围为行头、列头、角头和数据单元格。 +* 对于明细表,`field` 取值或正则匹配范围是 `columns`,作用范围为数据单元格。 - + - + - @@ -91,23 +91,23 @@ const s2Options = { export type ConditionMapping = ( fieldValue: number | string, data: RawData, - cell?: DataCell | HeaderCell, + cell?: S2CellType, ) => ConditionMappingResult; ``` `mapping` 接收三个参数,分别是: -* fieldValue: 当前单元格的值 -* data: 如果是数据单元格,则是格子对应的数据;如果是角头、行头、列头,则是格子的 meta 信息 -* cell: 对应当前格子的实例,如果前两个参数不满足业务需求,可以通过这个参数获取任意你想要的数据 +* `fieldValue`: 当前单元格的值。 +* `data`: 如果是数据单元格,则是单元格对应的数据;如果是角头、行头、列头,则是单元格的 [Node](/api/basic-class/node) 信息。 +* `cell`: 对应当前单元格的实例,如果前两个参数不满足业务需求,可以通过这个参数获取任意你想要的数据。 -不同的字段标记类型所需的返回值类型 `ConditionMappingResult` 有所不同,主要是泛型 `T`不同。S2 提供了完备的类型提示: +不同的字段标记类型所需的返回值类型 `ConditionMappingResult` 有所不同,主要是泛型 `T` 不同。S2 提供了完备的类型提示 -![类型提示](https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*wgC1QoXRWkAAAAAAAAAAAAAADmJ7AQ/original) +类型提示 > 也可以通过 [condition.ts](https://github.com/antvis/S2/blob/next/packages/s2-core/src/common/interface/condition.ts) 查看具体的类型定义。 -🎨 字段标记详细的配置参考 [Conditions API](/docs/api/general/S2Options#conditions) 文档。 +🎨 字段标记详细的配置参考 [Conditions API](/api/general/S2Options#conditions) 文档。 ## 特性 @@ -145,7 +145,7 @@ const s2Options = { -> 自定义 Icon 详情,可查看 [自定义 Icon](/docs/manual/advanced/custom/custom-icon) 章节 +> 自定义 Icon 详情,可查看 [自定义 Icon](/manual/advanced/custom/custom-icon) 章节 ### 自定义柱状图范围 diff --git a/s2-site/docs/manual/basic/multi-line-text.zh.md b/s2-site/docs/manual/basic/multi-line-text.zh.md index b78852e9da..460d48ef74 100644 --- a/s2-site/docs/manual/basic/multi-line-text.zh.md +++ b/s2-site/docs/manual/basic/multi-line-text.zh.md @@ -5,7 +5,7 @@ tag: New --- :::warning{title="注意"} -请确保已经阅读了 [基础教程](/manual/basic/base-concept) 和 [主题配置](/manual/basic/theme) 等章节,并对 [AntV/G](https://g.antv.antgroup.com/) 渲染引擎有所了解。 +阅读本章前,请确保已经阅读了 [基础教程](/manual/basic/base-concept) 和 [主题配置](/manual/basic/theme) 等章节,并对 [AntV/G](https://g.antv.antgroup.com/) 渲染引擎有所了解。 ::: 在基于 `DOM` 的 表格中,我们可以写一些简单的 [CSS 属性](https://developer.mozilla.org/en-US/docs/Web/CSS/text-overflow), 就可以实现文本的`自动换行`, `溢出隐藏`等特性,因为浏览器已经帮我们计算好了,而在 `Canvas` 中,`文本是否溢出`, `文字换行坐标计算`, `多行文本高度自适应` 等特性都需要自行实现。 diff --git a/s2-site/docs/manual/contribution.zh.md b/s2-site/docs/manual/contribution.zh.md index 4e55ea44c1..206ed0ba7e 100644 --- a/s2-site/docs/manual/contribution.zh.md +++ b/s2-site/docs/manual/contribution.zh.md @@ -94,6 +94,24 @@ pnpm react:start packages/s2-react/__tests__/spreadsheet/spread-sheet-spec.tsx preview +### 调试文档 + +如果有涉及到官网文档的改动,可以在本地调试运行,便于更直观的看到改动效果。 + +- `使用文档` - `s2-site/docs/manual` +- `API 文档` - `s2-site/docs/api` +- `图表示例` - `s2-site/examples` + +```bash +# 开发 +pnpm site:start + +# 预览 +pnpm site:preview +``` + +preview + ## 📦 版本 diff --git a/s2-site/docs/manual/migration-v2.zh.md b/s2-site/docs/manual/migration-v2.zh.md index 6e8abfc900..a991d88aab 100644 --- a/s2-site/docs/manual/migration-v2.zh.md +++ b/s2-site/docs/manual/migration-v2.zh.md @@ -34,7 +34,7 @@ tag: New 目前 `next` 版本会持续内测一段时间, 会作为相应的文档网站。 -在此期间,会持续根据用户的反馈进行 Bug fix 和代码调整,在 `@antv/s2@next` 版本稳定后,会发布正式版本,`latest` 将默认指向 `2.x` 版本,去除 `next` 标识。 +在此期间,会持续根据用户的反馈进行 Bug fix 和代码调整,在 `@antv/s2@next` 版本稳定后,会发布正式版本(时间待定),`latest` 将默认指向 `2.x` 版本,去除 `next` 标识。 ## 🗓️ v1 版本维护期 @@ -447,6 +447,32 @@ const s2Options = { } ``` +#### 字段标记 + +文本字段标记能力和 [文本主题配置](/api/general/s2-theme#texttheme) 保持一致,支持字体大小,透明度,对齐方式等配置。 + +```diff +const s2Options = { + conditions: { + text: [ + { + field: 'city', + mapping() { + return { + fill: '#DB6BCF', ++ fontSize: 16, ++ opacity: 0.8, ++ textAlign: 'right', + }; + }, + }, + ] + }, +} +``` + +具体请查看 [字段标记](/manual/basic/conditions) 相关文档和 [文本标记示例](/examples/analysis/conditions#text)。 + #### 序号配置变更 序号相关配置统一收拢在 `seriesNumber`. @@ -715,6 +741,58 @@ s2.interaction.getState() + stateName: "dataCellBrushSelected" ``` +#### 全选 API 逻辑调整 + +在 `1.x`, 全选本质上是高亮,只有样式更新,并不是真的选中,在 `2.x` 中切换为正确的语义,并且能获取到选中的单元格。 + +```diff +s2.interaction.selectAll() +- s2.interaction.getActiveCells() // [] ++ s2.interaction.getActiveCells() // [CellA, CellB] +``` + +#### 选中单元格 API 调整 + +`selectHeaderCell` 变更为 `changeCell`, 支持所有类型单元格的选中。同时支持 `选中 (selectCell)` 和 `高亮 (highlightCell)` 等语法糖。 + +```diff +- s2.interaction.selectHeaderCell(selectHeaderCellInfo: SelectHeaderCellInfo) ++ s2.interaction.changeCell(options: ChangeCellOptions) + ++ s2.interaction.selectCell(cell: S2CellType) ++ s2.interaction.highlightCell(cell: S2CellType) +``` + +同时支持 `animate (是否展示滚动动画` 和 `skipScrollEvent (是否触发滚动事件)` 配置。 + +```ts | pure +s2.interaction.selectCell(cell, { + animate: true, + skipScrollEvent: true +}) +``` + +具体请查看 [高亮/选中单元格](/manual/advanced/interaction/highlight-and-select-cell) 相关文档。 + +#### 滚动 API 调整 + +滚动 API `s2.updateScrollOffset` 移除,统一至 `s2.interaction` 命名空间下。同时支持 `scrollToCell` 和 `scrollToTop` 等语法糖。 + +```diff +- s2.updateScrollOffset(offsetConfig: ScrollOffsetConfig) ++ s2.interaction.scrollTo(offsetConfig: ScrollOffsetConfig) +``` + +同时支持 `skipScrollEvent(是否触发滚动事件)` 配置。 + +```diff +s2.interaction.scrollTo({ ++ skipScrollEvent: false +}) +``` + +具体请查看 [滚动](/manual/advanced/interaction/scroll) 相关文档。 + ##### 配置预处理 API 变更 ```diff diff --git a/s2-site/examples/analysis/conditions/demo/icon-with-action.ts b/s2-site/examples/analysis/conditions/demo/icon-with-action.ts index abc824342f..feb564a8bd 100644 --- a/s2-site/examples/analysis/conditions/demo/icon-with-action.ts +++ b/s2-site/examples/analysis/conditions/demo/icon-with-action.ts @@ -31,6 +31,7 @@ fetch( icon: [ { field: 'city', + position: 'right', mapping() { return { icon: 'Trend', diff --git a/s2-site/examples/analysis/conditions/demo/meta.json b/s2-site/examples/analysis/conditions/demo/meta.json index e897cbc3aa..73dea739cf 100644 --- a/s2-site/examples/analysis/conditions/demo/meta.json +++ b/s2-site/examples/analysis/conditions/demo/meta.json @@ -10,7 +10,8 @@ "zh": "文本标记", "en": "Text condition" }, - "screenshot": "https://gw.alipayobjects.com/mdn/rms_56cbb2/afts/img/A*50NlSIHAly0AAAAAAAAAAAAAARQnAQ" + "screenshot": "https://gw.alipayobjects.com/mdn/rms_56cbb2/afts/img/A*50NlSIHAly0AAAAAAAAAAAAAARQnAQ", + "new": true }, { "filename": "background.ts", @@ -42,7 +43,8 @@ "zh": "图标标记混合自定义 Action Icons", "en": "Icon condition with action icons" }, - "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*wClMR4kLBaoAAAAAAAAAAAAADmJ7AQ/original" + "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*wClMR4kLBaoAAAAAAAAAAAAADmJ7AQ/original", + "new": true }, { "filename": "multi-background.ts", @@ -92,7 +94,8 @@ "zh": "区分行列角头和数据单元格", "en": "Distinguish different cell" }, - "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*pxw2S5JFzUcAAAAAAAAAAAAADmJ7AQ/original" + "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*pxw2S5JFzUcAAAAAAAAAAAAADmJ7AQ/original", + "new": true }, { "filename": "table-text.ts", @@ -100,7 +103,8 @@ "zh": "明细表文本标记", "en": "Table text condition" }, - "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/jdTsPFc09l/4795d9f3-f55b-4575-99de-829e0e3ab3db.png" + "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/jdTsPFc09l/4795d9f3-f55b-4575-99de-829e0e3ab3db.png", + "new": true } ] } diff --git a/s2-site/examples/analysis/conditions/demo/table-text.ts b/s2-site/examples/analysis/conditions/demo/table-text.ts index 11e1bb55e2..896f66e12f 100644 --- a/s2-site/examples/analysis/conditions/demo/table-text.ts +++ b/s2-site/examples/analysis/conditions/demo/table-text.ts @@ -52,7 +52,7 @@ fetch( }, }, { - field: new RegExp('co*'), + field: /co*/, mapping(fieldValue, data) { return { fill: '#F4664A', diff --git a/s2-site/examples/analysis/conditions/demo/text.ts b/s2-site/examples/analysis/conditions/demo/text.ts index 3bda6b622c..70a1965321 100644 --- a/s2-site/examples/analysis/conditions/demo/text.ts +++ b/s2-site/examples/analysis/conditions/demo/text.ts @@ -17,18 +17,41 @@ fetch( text: [ // 行头 { + // 支持正则: /^city+$/, field: 'city', - mapping() { + mapping(fieldValue, data, cell) { + const meta = cell.getMeta(); + + // 根据单元格信息动态标记 + if (meta.rowIndex === 2) { + return { + textAlign: 'left', + }; + } + + // 根据维值动态标记 + if (fieldValue === '成都市') { + return { + fill: '#327039', + fontSize: 14, + textAlign: 'left', + }; + } + return { // fill 是文本字段标记下唯一必须的字段,用于指定文本颜色 fill: '#DB6BCF', + // 其他配置同文本主题: https://s2.antv.antgroup.com/api/general/s2-theme#texttheme + fontSize: 16, + opacity: 0.8, + textAlign: 'right', }; }, }, // 列头 { field: 'sub_type', - mapping() { + mapping(fieldValue, data) { return { fill: '#025DF4', }; @@ -37,8 +60,8 @@ fetch( // 单独控制角头 { field: 'type', - mapping(field) { - if (field === '类别') { + mapping(fieldValue, data) { + if (fieldValue === '类别') { return { fill: '#327039', }; @@ -48,7 +71,7 @@ fetch( // 单独配置指标名 { field: EXTRA_FIELD, - mapping() { + mapping(fieldValue, data) { return { fill: '#5B8FF9', }; @@ -57,7 +80,14 @@ fetch( // 配置数据单元格 { field: 'number', - mapping() { + mapping(fieldValue, data) { + // 根据单元格数据动态标记 + if (data.number < 1000) { + return { + fill: '#cf2f2fd9', + }; + } + return { fill: '#2498D1', }; diff --git a/s2-site/examples/analysis/get-data/demo/get-cell-data.ts b/s2-site/examples/analysis/get-data/demo/get-cell-data.ts index bcc9eb0df5..dbbaa7ea54 100644 --- a/s2-site/examples/analysis/get-data/demo/get-cell-data.ts +++ b/s2-site/examples/analysis/get-data/demo/get-cell-data.ts @@ -230,6 +230,7 @@ fetch( enable: true, }, interaction: { + autoResetSheetStyle: false, selectedCellsSpotlight: true, hoverHighlight: true, copy: { enable: true }, diff --git a/s2-site/examples/custom/custom-shape-and-chart/demo/custom-g-shape.ts b/s2-site/examples/custom/custom-shape-and-chart/demo/custom-g-shape.ts index 74714eadc5..1569485e86 100644 --- a/s2-site/examples/custom/custom-shape-and-chart/demo/custom-g-shape.ts +++ b/s2-site/examples/custom/custom-shape-and-chart/demo/custom-g-shape.ts @@ -1,9 +1,11 @@ +/* eslint-disable no-console */ /* eslint-disable max-classes-per-file */ import { Image as GImage, Polygon, Polyline, Rect } from '@antv/g'; import { ColCell, CornerCell, DataCell, + GuiIcon, PivotSheet, RowCell, S2DataConfig, @@ -100,6 +102,13 @@ class CustomDataCell extends DataCell { * 自定义 Polyline 折线: https://g.antv.antgroup.com/api/basic/polyline */ class CustomRowCell extends RowCell { + initCell() { + super.initCell(); + + // 绘制任意图形... + // this.appendChild(...) + } + drawBackgroundShape() { if (this.meta.rowIndex > 0) { return super.drawBackgroundShape(); @@ -215,4 +224,22 @@ fetch( }, }), ); + + // 4. 手动获取指定单元格实例 (Group) 后绘制任意图标 + const size = 12; + const meta = targetCell.getMeta(); + const icon = new GuiIcon({ + x: meta.x + meta.width - size, + y: meta.y + meta.height - size, + name: 'Trend', + width: size, + height: size, + fill: 'red', + }); + + icon.addEventListener('click', (e) => { + console.log('trend icon click:', e); + }); + + targetCell.appendChild(icon); }); diff --git a/s2-site/examples/interaction/advanced/demo/scroll-loop.ts b/s2-site/examples/interaction/advanced/demo/scroll-loop.ts index fe6558fd78..e785c9a668 100644 --- a/s2-site/examples/interaction/advanced/demo/scroll-loop.ts +++ b/s2-site/examples/interaction/advanced/demo/scroll-loop.ts @@ -41,7 +41,7 @@ function addScrollButton(s2) { if (s2.facet.isScrollToBottom(scrollY)) { console.log('滚动到底部'); - s2.updateScrollOffset({ + s2.interaction.scrollTo({ offsetY: { value: 0, animate: false, @@ -52,7 +52,7 @@ function addScrollButton(s2) { } console.log('开始滚动, 当前 scrollY:', scrollY); - s2.updateScrollOffset({ + s2.interaction.scrollTo({ offsetY: { value: scrollY + STEP, animate: true, diff --git a/s2-site/examples/interaction/advanced/demo/scroll-to-cell.ts b/s2-site/examples/interaction/advanced/demo/scroll-to-cell.ts index cba82ce13b..b0cf3ea3df 100644 --- a/s2-site/examples/interaction/advanced/demo/scroll-to-cell.ts +++ b/s2-site/examples/interaction/advanced/demo/scroll-to-cell.ts @@ -8,7 +8,7 @@ function addScrollRowHeaderButton(s2: SpreadSheet) { btn.innerHTML = '滚动行头'; btn.addEventListener('click', () => { - s2.updateScrollOffset({ + s2.interaction.scrollTo({ rowHeaderOffsetX: { value: 50, animate: true, @@ -25,17 +25,23 @@ function addScrollToCellButton(s2: SpreadSheet) { btn.innerHTML = '滚动至成都市'; btn.addEventListener('click', () => { - // 获取行头是成都市对应的单元格 const rowNode = s2.facet.getRowNodeById('root[&]四川省[&]成都市'); + // 方式 1: 获取行头是成都市对应的单元格, 使用 scrollTo, 只会改变纵向滚动条 if (rowNode) { - s2.updateScrollOffset({ + s2.interaction.scrollTo({ offsetY: { value: rowNode.y, animate: true, }, }); } + + // 方式 2: 使用 scrollToCellById + // s2.interaction.scrollToCellById('root[&]四川省[&]成都市'); + + // 方式 3: 使用 scrollToNode + // s2.interaction.scrollToNode(rowNode); }); document.querySelector('#container > canvas')?.before(btn); } @@ -47,12 +53,34 @@ function addScrollToTopButton(s2: SpreadSheet) { btn.innerHTML = '滚动至顶部'; btn.addEventListener('click', () => { - s2.updateScrollOffset({ - offsetY: { - value: 0, - animate: true, - }, - }); + s2.interaction.scrollToTop(); + + // 等价于: + // s2.interaction.scrollTo({ + // offsetY: { + // value: 0, + // animate: true, + // }, + // }); + }); + document.querySelector('#container > canvas')?.before(btn); +} + +function addScrollToBottomButton(s2: SpreadSheet) { + const btn = document.createElement('button'); + + btn.className = 'ant-btn ant-btn-default'; + btn.innerHTML = '滚动至底部'; + + btn.addEventListener('click', () => { + s2.interaction.scrollToBottom(); + // 等价于: + // s2.interaction.scrollTo({ + // offsetY: { + // value: 0, + // animate: true, + // }, + // }); }); document.querySelector('#container > canvas')?.before(btn); } @@ -88,6 +116,7 @@ fetch( addScrollToCellButton(s2); addScrollToTopButton(s2); + addScrollToBottomButton(s2); addScrollRowHeaderButton(s2); }); diff --git a/s2-site/examples/interaction/basic/demo/auto-reset-sheet-style.ts b/s2-site/examples/interaction/basic/demo/auto-reset-sheet-style.ts index 22b2f6e44f..ce8f6dcf7b 100644 --- a/s2-site/examples/interaction/basic/demo/auto-reset-sheet-style.ts +++ b/s2-site/examples/interaction/basic/demo/auto-reset-sheet-style.ts @@ -1,4 +1,30 @@ -import { S2DataConfig, S2Options, TableSheet } from '@antv/s2'; +import { + S2DataConfig, + S2Options, + TableSheet, + type SpreadSheet, +} from '@antv/s2'; + +function addButton(s2: SpreadSheet) { + const btn = document.createElement('button'); + + btn.className = 'ant-btn ant-btn-default'; + btn.innerHTML = '点我选中列头, 但是不会重置交互状态'; + + btn.addEventListener('click', () => { + const colCell = s2.facet.getColCells()[0]; + + s2.interaction.selectCell(colCell); + }); + + const canvas = document.querySelector('#container > canvas'); + + if (canvas) { + canvas.style.marginTop = '10px'; + + canvas.before(btn); + } +} fetch('https://assets.antv.antgroup.com/s2/basic-table-mode.json') .then((res) => res.json()) @@ -38,11 +64,22 @@ fetch('https://assets.antv.antgroup.com/s2/basic-table-mode.json') height: 480, interaction: { // 在按下 ESC 键或者鼠标移出表格区域后,自动重置单元格选中,高亮状态 - autoResetSheetStyle: true, + // autoResetSheetStyle: true, + autoResetSheetStyle: (event) => { + // 支持根据 event 动态判断, 如: 点击操作按钮时不自动重置交互 + if (event?.target instanceof HTMLElement) { + return !event.target.classList.contains('ant-btn'); + } + + // 其他情况正常重置 + return true; + }, }, }; const s2 = new TableSheet(container, s2DataConfig, s2Options); await s2.render(); + + addButton(s2); }); diff --git a/s2-site/examples/interaction/basic/demo/event.ts b/s2-site/examples/interaction/basic/demo/event.ts index f9eb2f07f9..98fb3dfd88 100644 --- a/s2-site/examples/interaction/basic/demo/event.ts +++ b/s2-site/examples/interaction/basic/demo/event.ts @@ -1,21 +1,25 @@ /* eslint-disable no-console */ import { + InteractionStateName, PivotSheet, - S2Options, S2Event, + S2Options, SpreadSheet, - InteractionStateName, } from '@antv/s2'; +import { random } from 'lodash'; function addButtons(s2: SpreadSheet) { const [ selectAllBtn, - selectHeaderCellBtn, + selectCornerCellBtn, + selectRowCellBtn, + selectColCellBtn, selectDataCellBtn, + highlightCellBtn, hideColumnsBtn, highlightHeaderBtn, resetBtn, - ] = Array.from({ length: 6 }).map(() => { + ] = Array.from({ length: 9 }).map(() => { const btn = document.createElement('button'); btn.className = 'ant-btn ant-btn-default'; @@ -24,8 +28,11 @@ function addButtons(s2: SpreadSheet) { }); selectAllBtn.innerHTML = '选中全部'; - selectHeaderCellBtn.innerHTML = '选中指定行列头单元格'; + selectCornerCellBtn.innerHTML = '选中指定角头单元格'; + selectRowCellBtn.innerHTML = '选中指定行头单元格'; + selectColCellBtn.innerHTML = '选中指定列头单元格'; selectDataCellBtn.innerHTML = '选中指定数值单元格'; + highlightCellBtn.innerHTML = '高亮指定单元格'; hideColumnsBtn.innerHTML = '隐藏指定列头'; highlightHeaderBtn.innerHTML = '高亮数值和对应的行列头单元格'; resetBtn.innerHTML = '重置'; @@ -35,49 +42,78 @@ function addButtons(s2: SpreadSheet) { s2.interaction.selectAll(); }); - selectHeaderCellBtn.addEventListener('click', () => { - const rowNode = s2.facet.getRowNodeById('root[&]浙江省[&]杭州市'); + selectCornerCellBtn.addEventListener('click', () => { + const cornerCell = + s2.facet.getCornerCells()[ + random(0, s2.facet.getCornerCells().length - 1) + ]; - console.log( - '🚀 ~ selectHeaderCellBtn.addEventListener ~ rowNode:', - rowNode, - ); + console.log('cornerCell: ', cornerCell); - s2.interaction.selectHeaderCell({ - cell: rowNode?.belongsCell, - }); + s2.interaction.selectCell(cornerCell); + }); + + selectRowCellBtn.addEventListener('click', () => { + const rowCell = + s2.facet.getRowCells()[random(0, s2.facet.getRowCells().length - 1)]; + + console.log('rowCell: ', rowCell); + + s2.interaction.selectCell(rowCell); + }); + + selectColCellBtn.addEventListener('click', () => { + const colCell = + s2.facet.getColCells()[random(0, s2.facet.getColCells().length - 1)]; + + console.log('colCell: ', colCell); + + s2.interaction.selectCell(colCell); }); selectDataCellBtn.addEventListener('click', () => { - const dataCells = s2.facet - .getDataCells() - .slice(0, 4) - .map((cell) => { - const meta = cell.getMeta(); - - return { - id: meta.id, - rowIndex: meta.rowIndex, - colIndex: meta.colIndex, - type: cell.cellType, - }; - }); + const dataCell = + s2.facet.getDataCells()[random(0, s2.facet.getDataCells().length - 1)]; + + console.log('dataCell:', dataCell); + + // 第二个参数可选 + s2.interaction.selectCell(dataCell, { + /** + * 是否展示滚动动画 + */ + animate: true, + + /** + * 是否触发滚动事件 + */ + skipScrollEvent: false, + }); + }); - console.log( - '🚀 ~ selectDataCellBtn.addEventListener ~ dataCells:', - dataCells, - ); + highlightCellBtn.addEventListener('click', () => { + const cell = s2.facet.getCells()[random(0, s2.facet.getCells().length - 1)]; - s2.interaction.setState({ - stateName: InteractionStateName.SELECTED, - cells: dataCells, + console.log('highlightCell:', cell); + + // 第二个参数可选 + s2.interaction.highlightCell(cell, { + /** + * 是否展示滚动动画 + */ + animate: true, + + /** + * 是否触发滚动事件 + */ + skipScrollEvent: false, }); }); hideColumnsBtn.addEventListener('click', () => { s2.interaction.hideColumns([ - 'root[&]家具[&]桌子[&]数量', - 'root[&]办公用品[&]笔[&]数量', + 'root[&]家具[&]桌子[&]number', + 'root[&]办公用品[&]笔[&]number', ]); }); @@ -85,18 +121,18 @@ function addButtons(s2: SpreadSheet) { const dataCellViewMeta = s2.facet.getCellMeta(1, 1); s2.interaction.updateDataCellRelevantHeaderCells( - dataCellViewMeta, InteractionStateName.HOVER, + dataCellViewMeta, ); // s2.interaction.updateDataCellRelevantRowCells( - // dataCellViewMeta, // InteractionStateName.HOVER, + // dataCellViewMeta, // ); // s2.interaction.updateDataCellRelevantColCells( - // dataCellViewMeta, // InteractionStateName.HOVER, + // dataCellViewMeta, // ); }); @@ -107,7 +143,7 @@ function addButtons(s2: SpreadSheet) { console.log('当前未选中的单元格:', s2.interaction.getUnSelectedDataCells()); s2.interaction.reset(); - s2.interaction.resetState(); + // s2.interaction.resetState(); s2.interaction.hideColumns([]); }); @@ -117,8 +153,11 @@ function addButtons(s2: SpreadSheet) { canvas.style.marginTop = '10px'; canvas.before(selectAllBtn); - canvas.before(selectHeaderCellBtn); + canvas.before(selectCornerCellBtn); + canvas.before(selectRowCellBtn); + canvas.before(selectColCellBtn); canvas.before(selectDataCellBtn); + canvas.before(highlightCellBtn); canvas.before(hideColumnsBtn); canvas.before(highlightHeaderBtn); canvas.before(resetBtn); @@ -137,7 +176,7 @@ fetch( height: 480, style: { rowCell: { - width: 200, + width: 80, }, dataCell: { width: 100, @@ -149,10 +188,19 @@ fetch( hoverHighlight: true, brushSelection: true, multiSelection: true, - selectedCellHighlight: true, + selectedCellHighlight: false, selectedCellsSpotlight: true, selectedCellMove: true, overscrollBehavior: 'none', + autoResetSheetStyle: (event, spreadsheet) => { + // 点击操作按钮时不自动重置交互 + if (event?.target instanceof HTMLElement) { + return !event.target.classList.contains('ant-btn'); + } + + // 其他情况正常重置 (如: 点击空白处, 按下 ESC) + return true; + }, /** * 透传底层 Event Listener 属性的可选参数对象 diff --git a/s2-site/examples/interaction/basic/demo/meta.json b/s2-site/examples/interaction/basic/demo/meta.json index d84ddaad2c..8dba1c15cf 100644 --- a/s2-site/examples/interaction/basic/demo/meta.json +++ b/s2-site/examples/interaction/basic/demo/meta.json @@ -81,10 +81,11 @@ { "filename": "auto-reset-sheet-style.ts", "title": { - "zh": "自动交互状态重置", + "zh": "自动重置交互状态", "en": "Disable auto reset sheet style" }, - "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/Lb0%26u6LtAu/reset.gif" + "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/Lb0%26u6LtAu/reset.gif", + "new": true }, { "filename": "hover-after-scroll.ts", diff --git a/s2-site/examples/interaction/basic/demo/resize.ts b/s2-site/examples/interaction/basic/demo/resize.ts index 543e3c6c6b..c3add6c7ef 100644 --- a/s2-site/examples/interaction/basic/demo/resize.ts +++ b/s2-site/examples/interaction/basic/demo/resize.ts @@ -18,7 +18,7 @@ fetch( visible: (cell) => true, // 是否禁用拖拽 disable: (resizeInfo) => { - return resizeInfo.meta.id === 'root[&]家具[&]桌子[&]数量'; + return resizeInfo.meta.id === 'root[&]家具[&]桌子[&]number'; }, // 行高调整时,影响全部行 (可选 'all' | 'current' | 'selected') rowResizeType: ResizeType.ALL,
- 透视表 - - + 透视表 + +
明细表 - + +