From 00a7df2990e0727a378136c0310a928bd6dc7753 Mon Sep 17 00:00:00 2001 From: lijinke666 Date: Tue, 5 Dec 2023 17:01:00 +0800 Subject: [PATCH 1/3] =?UTF-8?q?refactor:=20frozenRowCell=20=E9=87=8D?= =?UTF-8?q?=E5=91=BD=E5=90=8D=E4=B8=BA=20rowCell?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__tests__/unit/cell/row-cell-spec.ts | 2 +- .../unit/facet/header/frozen-row-spec.ts | 8 +- .../unit/facet/pivot-facet-frozen-spec.ts | 11 +- .../click/data-cell-click-spec.ts | 3 +- .../unit/utils/cell/header-cell-spec.ts | 2 +- .../__tests__/unit/utils/export/copy-spec.ts | 3 +- .../interaction/state-controller-spec.ts | 4 +- packages/s2-core/src/cell/base-row-cell.ts | 501 ++++++++++++++++++ packages/s2-core/src/cell/frozen-row-cell.ts | 80 --- packages/s2-core/src/cell/index.ts | 6 +- packages/s2-core/src/cell/row-cell.ts | 499 ++--------------- .../s2-core/src/cell/series-number-cell.ts | 4 +- .../s2-core/src/common/interface/emitter.ts | 4 +- .../src/common/interface/interaction.ts | 2 + .../src/facet/header/base-frozen-row.ts | 4 +- .../s2-core/src/facet/header/pivot-row.ts | 11 +- .../base-interaction/click/data-cell-click.ts | 5 +- .../s2-react/__tests__/data/strategy-data.ts | 1 + packages/s2-react/playground/config.ts | 2 +- packages/s2-react/playground/index.tsx | 18 + .../manual/basic/sheet-type/pivot-mode.zh.md | 18 +- s2-site/examples/basic/pivot/demo/grid.ts | 3 + s2-site/examples/basic/pivot/demo/tree.ts | 42 +- .../basic/demo/frozen-row-header.ts | 38 ++ .../examples/interaction/basic/demo/meta.json | 12 +- 25 files changed, 684 insertions(+), 599 deletions(-) create mode 100644 packages/s2-core/src/cell/base-row-cell.ts delete mode 100644 packages/s2-core/src/cell/frozen-row-cell.ts create mode 100644 s2-site/examples/interaction/basic/demo/frozen-row-header.ts diff --git a/packages/s2-core/__tests__/unit/cell/row-cell-spec.ts b/packages/s2-core/__tests__/unit/cell/row-cell-spec.ts index 1dca138ff3..9b4c9f3822 100644 --- a/packages/s2-core/__tests__/unit/cell/row-cell-spec.ts +++ b/packages/s2-core/__tests__/unit/cell/row-cell-spec.ts @@ -1,7 +1,7 @@ import { get } from 'lodash'; import { createPivotSheet } from 'tests/util/helpers'; -import type { RowCell } from '@antv/s2'; import type { Group } from '@antv/g-canvas'; +import type { RowCell } from '../../../src/cell'; import type { SpreadSheet } from '@/sheet-type'; import type { TextAlign } from '@/common'; diff --git a/packages/s2-core/__tests__/unit/facet/header/frozen-row-spec.ts b/packages/s2-core/__tests__/unit/facet/header/frozen-row-spec.ts index cf43962bfb..c93bb1d5a5 100644 --- a/packages/s2-core/__tests__/unit/facet/header/frozen-row-spec.ts +++ b/packages/s2-core/__tests__/unit/facet/header/frozen-row-spec.ts @@ -1,8 +1,8 @@ import { createPivotSheet } from 'tests/util/helpers'; import { get } from 'lodash'; +import { RowCell } from '../../../../src/cell/row-cell'; import { DEFAULT_OPTIONS } from '@/common'; - -import { FrozenRowCell, SeriesNumberCell } from '@/cell'; +import { SeriesNumberCell } from '@/cell'; import { PivotRowHeader } from '@/facet/header'; import { SeriesNumberHeader } from '@/facet/header/series-number'; @@ -31,13 +31,13 @@ describe('Frozen Row Header Test', () => { expect(rowHeader.frozenHeadGroup.getChildren()).toHaveLength(1); const frozenRowCell = rowHeader.frozenHeadGroup.getChildren()[0]; - expect(frozenRowCell instanceof FrozenRowCell).toBeTrue(); + expect(frozenRowCell instanceof RowCell).toBeTrue(); expect(get(frozenRowCell, 'meta.height')).toEqual(30); expect(rowHeader.scrollGroup.getChildren()).toHaveLength(10); const scrollCell = rowHeader.scrollGroup.getChildren()[0]; - expect(scrollCell instanceof FrozenRowCell).toBeTrue(); + expect(scrollCell instanceof RowCell).toBeTrue(); expect(get(frozenRowCell, 'meta.height')).toEqual(30); expect(rowHeader.getFrozenFirstRowHeight()).toBe(30); diff --git a/packages/s2-core/__tests__/unit/facet/pivot-facet-frozen-spec.ts b/packages/s2-core/__tests__/unit/facet/pivot-facet-frozen-spec.ts index 913291366b..91d6431543 100644 --- a/packages/s2-core/__tests__/unit/facet/pivot-facet-frozen-spec.ts +++ b/packages/s2-core/__tests__/unit/facet/pivot-facet-frozen-spec.ts @@ -4,13 +4,12 @@ import type { IGroup } from '@antv/g-canvas'; import { get } from 'lodash'; import { createPivotSheet } from 'tests/util/helpers'; - -import type { PivotSheet, S2Options } from '@antv/s2'; -import { FrozenRowCell, SeriesNumberCell } from '@/cell'; +import { type PivotSheet, RowCell, SeriesNumberCell } from '../../../src'; import { FrozenGroup, KEY_GROUP_ROW_HEADER_FROZEN, KEY_GROUP_ROW_SCROLL, + type S2Options, } from '@/common'; import type { FrozenFacet } from '@/facet/frozen-facet'; import { getFrozenRowCfgPivot } from '@/facet/utils'; @@ -371,12 +370,10 @@ describe('test frozen group', () => { scrollHeaderGroup as IGroup ).getChildren(); expect(frozenRowGroupChildren).toHaveLength(1); - expect(frozenRowGroupChildren[0] instanceof FrozenRowCell).toBe(true); + expect(frozenRowGroupChildren[0] instanceof RowCell).toBeTruthy(); expect(get(frozenRowGroupChildren[0], 'meta.value')).toBe('总计'); expect(scrollRowHeaderGroupChildren).toHaveLength(10); - expect(scrollRowHeaderGroupChildren[0] instanceof FrozenRowCell).toBe( - true, - ); + expect(scrollRowHeaderGroupChildren[0] instanceof RowCell).toBeTruthy(); expect(get(scrollRowHeaderGroupChildren[0], 'meta.value')).toBe('浙江省'); // serial number header diff --git a/packages/s2-core/__tests__/unit/interaction/base-interaction/click/data-cell-click-spec.ts b/packages/s2-core/__tests__/unit/interaction/base-interaction/click/data-cell-click-spec.ts index 65827b8434..d23e5608d1 100644 --- a/packages/s2-core/__tests__/unit/interaction/base-interaction/click/data-cell-click-spec.ts +++ b/packages/s2-core/__tests__/unit/interaction/base-interaction/click/data-cell-click-spec.ts @@ -4,8 +4,7 @@ import { sleep, } from 'tests/util/helpers'; import type { Event as GEvent } from '@antv/g-canvas'; -import type { InteractionCellHighlight } from '@antv/s2'; -import type { S2Options } from '@/common/interface'; +import type { InteractionCellHighlight, S2Options } from '@/common/interface'; import type { SpreadSheet } from '@/sheet-type'; import { HOVER_FOCUS_DURATION, diff --git a/packages/s2-core/__tests__/unit/utils/cell/header-cell-spec.ts b/packages/s2-core/__tests__/unit/utils/cell/header-cell-spec.ts index 63498e33c8..a5aa9b6295 100644 --- a/packages/s2-core/__tests__/unit/utils/cell/header-cell-spec.ts +++ b/packages/s2-core/__tests__/unit/utils/cell/header-cell-spec.ts @@ -1,4 +1,4 @@ -import type { Node } from '@antv/s2'; +import type { Node } from '../../../../src'; import { CellTypes, type HeaderActionIcon } from '@/common'; import { getActionIconConfig } from '@/utils/cell/header-cell'; diff --git a/packages/s2-core/__tests__/unit/utils/export/copy-spec.ts b/packages/s2-core/__tests__/unit/utils/export/copy-spec.ts index eb39fc36d9..5ff1a0bb37 100644 --- a/packages/s2-core/__tests__/unit/utils/export/copy-spec.ts +++ b/packages/s2-core/__tests__/unit/utils/export/copy-spec.ts @@ -1,10 +1,9 @@ -import type { S2DataConfig } from '@antv/s2'; import { assembleDataCfg, assembleOptions, TOTALS_OPTIONS } from 'tests/util'; import { getContainer } from 'tests/util/helpers'; import { data as originalData, totalData } from 'tests/data/mock-dataset.json'; import { map } from 'lodash'; +import type { S2DataConfig } from '../../../../src/common'; import { TableSheet, PivotSheet } from '@/sheet-type'; - import { CellTypes, InteractionStateName, diff --git a/packages/s2-core/__tests__/unit/utils/interaction/state-controller-spec.ts b/packages/s2-core/__tests__/unit/utils/interaction/state-controller-spec.ts index 02535a8f4a..f9aae9923d 100644 --- a/packages/s2-core/__tests__/unit/utils/interaction/state-controller-spec.ts +++ b/packages/s2-core/__tests__/unit/utils/interaction/state-controller-spec.ts @@ -1,5 +1,5 @@ import { getCellMeta } from '@/utils/interaction/select-event'; -import type { RowCell } from '@/cell/row-cell'; +import type { BaseRowCell } from '@/cell/base-row-cell'; import { CellTypes, InteractionStateName } from '@/common/constant/interaction'; import type { S2Options } from '@/common/interface'; import { Store } from '@/common/store'; @@ -25,7 +25,7 @@ describe('State Controller Utils Tests', () => { id: `root[&]price`, }; }, - } as unknown as RowCell; + } as unknown as BaseRowCell; let mockInstance: SpreadSheet; diff --git a/packages/s2-core/src/cell/base-row-cell.ts b/packages/s2-core/src/cell/base-row-cell.ts new file mode 100644 index 0000000000..911b0a4475 --- /dev/null +++ b/packages/s2-core/src/cell/base-row-cell.ts @@ -0,0 +1,501 @@ +import type { Point } from '@antv/g-canvas'; +import { GM } from '@antv/g-gesture'; +import { find, get, isEmpty } from 'lodash'; +import type { SimpleBBox } from '@antv/g-canvas'; +import { + CellTypes, + KEY_GROUP_ROW_RESIZE_AREA, + ResizeAreaEffect, + ResizeDirectionType, + S2Event, +} from '../common/constant'; +import { CellBorderPosition, type ViewMeta } from '../common/interface'; +import type { RowHeaderConfig } from '../facet/header/row'; +import { + getBorderPositionAndStyle, + getTextAndFollowingIconPosition, +} from '../utils/cell/cell'; +import { + renderCircle, + renderLine, + renderRect, + renderTreeIcon, +} from '../utils/g-renders'; +import { getAllChildrenNodeHeight } from '../utils/get-all-children-node-height'; +import { + getOrCreateResizeAreaGroupById, + getResizeAreaAttrs, +} from '../utils/interaction/resize'; +import { isMobile } from '../utils/is-mobile'; +import { getAdjustPosition } from '../utils/text-absorption'; +import { shouldAddResizeArea } from '../utils/interaction/resize'; +import { HeaderCell } from './header-cell'; + +export class BaseRowCell extends HeaderCell { + protected declare headerConfig: RowHeaderConfig; + + protected gm: GM; + + public get cellType() { + return CellTypes.ROW_CELL; + } + + public destroy(): void { + super.destroy(); + this.gm?.destroy(); + } + + protected initCell() { + super.initCell(); + // 绘制单元格背景 + this.drawBackgroundShape(); + // 绘制交互背景 + this.drawInteractiveBgShape(); + // 绘制交互边框 + this.drawInteractiveBorderShape(); + // 绘制单元格文本 + this.drawTextShape(); + // 绘制字段标记 -- icon + this.drawConditionIconShapes(); + // 绘制树状模式收起展开的 icon + this.drawTreeIcon(); + // 绘制树状模式下子节点层级占位圆点 + this.drawTreeLeafNodeAlignDot(); + // 绘制单元格边框 + this.drawRectBorder(); + // 绘制 resize 热区 + this.drawResizeAreaInLeaf(); + // 绘制 action icons + this.drawActionIcons(); + this.update(); + } + + public getBackgroundColor() { + const { backgroundColor, backgroundColorOpacity } = + this.getCrossBackgroundColor(this.meta.rowIndex); + return this.getBackgroundColorByCondition( + backgroundColor, + backgroundColorOpacity, + ); + } + + /** + * 绘制hover悬停,刷选的外框 + */ + protected drawInteractiveBorderShape() { + // 往内缩一个像素,避免和外边框重叠 + const margin = 2; + + this.stateShapes.set( + 'interactiveBorderShape', + renderRect(this, this.getInteractiveBorderShapeStyle(margin), { + visible: false, + }), + ); + } + + // 交互使用的背景色 + protected drawInteractiveBgShape() { + this.stateShapes.set( + 'interactiveBgShape', + renderRect( + this, + { + ...this.getCellArea(), + }, + { + visible: false, + }, + ), + ); + } + + protected showTreeIcon() { + return this.spreadsheet.isHierarchyTreeType() && !this.meta.isLeaf; + } + + protected showTreeLeafNodeAlignDot() { + return ( + this.spreadsheet.options.style?.showTreeLeafNodeAlignDot && + this.spreadsheet.isHierarchyTreeType() + ); + } + + // 获取树状模式下叶子节点的父节点收起展开 icon 图形属性 + protected getParentTreeIconCfg() { + if ( + !this.showTreeLeafNodeAlignDot() || + !this.spreadsheet.isHierarchyTreeType() || + !this.meta.isLeaf + ) { + return; + } + + return get(this.meta, 'parent.belongsCell.treeIcon.cfg'); + } + + // draw tree icon + protected drawTreeIcon() { + if (!this.showTreeIcon()) { + return; + } + + const { isCollapsed, id, hierarchy } = this.meta; + const { x } = this.getContentArea(); + const { fill } = this.getTextStyle(); + const { size } = this.getStyle().icon; + + const contentIndent = this.getContentIndent(); + + const iconX = x + contentIndent; + const iconY = this.getIconYPosition(); + + this.treeIcon = renderTreeIcon( + this, + { + x: iconX, + y: iconY, + width: size, + height: size, + }, + fill, + isCollapsed, + () => { + if (isMobile()) { + return; + } + // 折叠行头时因scrollY没变,导致底层出现空白 + if (!isCollapsed) { + const oldScrollY = this.spreadsheet.store.get('scrollY'); + // 可视窗口高度 + const viewportHeight = this.headerConfig.viewportHeight || 0; + // 被折叠项的高度 + const deleteHeight = getAllChildrenNodeHeight(this.meta); + // 折叠后真实高度 + const realHeight = hierarchy.height - deleteHeight; + if (oldScrollY > 0 && oldScrollY + viewportHeight > realHeight) { + const currentScrollY = realHeight - viewportHeight; + this.spreadsheet.store.set( + 'scrollY', + currentScrollY > 0 ? currentScrollY : 0, + ); + } + } + this.spreadsheet.emit(S2Event.ROW_CELL_COLLAPSE_TREE_ROWS, { + id, + isCollapsed: !isCollapsed, + node: this.meta, + }); + }, + ); + + // in mobile, we use this cell + if (isMobile()) { + this.gm = new GM(this, { + gestures: ['Tap'], + }); + this.gm.on('tap', () => { + this.spreadsheet.emit(S2Event.ROW_CELL_COLLAPSE_TREE_ROWS, { + id, + isCollapsed: !isCollapsed, + node: this.meta, + }); + }); + } + } + + protected drawTreeLeafNodeAlignDot() { + const parentTreeIconCfg = this.getParentTreeIconCfg(); + if (!parentTreeIconCfg) { + return; + } + const { size, margin } = this.getStyle().icon; + const x = parentTreeIconCfg.x + size + margin.right; + const textY = this.getTextPosition().y; + + const { fill, fontSize } = this.getTextStyle(); + const r = size / 5; // 半径,暂时先写死,后面看是否有这个点点的定制需求 + this.treeLeafNodeAlignDot = renderCircle(this, { + x: x + size / 2, // 和收起展开 icon 保持居中对齐 + y: textY + (fontSize - r) / 2, + r, + fill, + fillOpacity: 0.3, // 暂时先写死,后面看是否有这个点点的定制需求 + }); + } + + protected isBolderText() { + // 非叶子节点、小计总计,均为粗体 + const { isLeaf, isTotals, level } = this.meta; + return (!isLeaf && level === 0) || isTotals; + } + + // draw text + protected drawTextShape() { + super.drawTextShape(); + this.drawLinkField(this.meta); + } + + protected drawRectBorder() { + const { x } = this.getCellArea(); + + const contentIndent = this.getContentIndent(); + const finalX = this.spreadsheet.isHierarchyTreeType() + ? x + : x + contentIndent; + [CellBorderPosition.BOTTOM, CellBorderPosition.LEFT].forEach((type) => { + const { position, style } = getBorderPositionAndStyle( + type, + { + ...this.getCellArea(), + x: finalX, + }, + this.getStyle().cell, + ); + renderLine(this, position, style); + }); + } + + protected getResizeClipAreaBBox(): SimpleBBox { + const { width, viewportHeight } = this.headerConfig; + return { + x: 0, + y: 0, + width, + height: viewportHeight, + }; + } + + protected drawResizeAreaInLeaf() { + if ( + !this.meta.isLeaf || + !this.shouldDrawResizeAreaByType('rowCellVertical', this) + ) { + return; + } + + const { x, y, width, height } = this.getCellArea(); + const resizeStyle = this.getResizeAreaStyle(); + const resizeArea = getOrCreateResizeAreaGroupById( + this.spreadsheet, + KEY_GROUP_ROW_RESIZE_AREA, + ); + + const { + position, + seriesNumberWidth, + width: headerWidth, + scrollX, + scrollY, + } = this.headerConfig; + + const resizeAreaBBox = { + // fix: When scrolling without the entire frozen header horizontally, the resize area would be removed permanently. + x: x + seriesNumberWidth, + y: y + height - resizeStyle.size / 2, + width, + height: resizeStyle.size, + }; + + const resizeClipAreaBBox = this.getResizeClipAreaBBox(); + + if ( + !shouldAddResizeArea(resizeAreaBBox, resizeClipAreaBBox, { + scrollX, + scrollY, + }) + ) { + return; + } + + const offsetX = position?.x + x - scrollX + seriesNumberWidth; + const offsetY = position?.y + y - scrollY; + + const resizeAreaWidth = this.spreadsheet.isFrozenRowHeader() + ? headerWidth - seriesNumberWidth - (x - scrollX) + : width; + + resizeArea.addShape('rect', { + attrs: { + ...getResizeAreaAttrs({ + id: this.meta.id, + theme: resizeStyle, + type: ResizeDirectionType.Vertical, + effect: ResizeAreaEffect.Cell, + offsetX, + offsetY, + width, + height, + meta: this.meta, + }), + x: offsetX, + y: offsetY + height - resizeStyle.size / 2, + width: resizeAreaWidth, + }, + }); + } + + protected getContentIndent() { + if (!this.spreadsheet.isHierarchyTreeType()) { + return 0; + } + const { icon, cell } = this.getStyle(); + const iconWidth = icon.size + icon.margin.right; + + let parent = this.meta.parent; + let sum = 0; + while (parent) { + if (parent.height !== 0) { + sum += iconWidth; + } + parent = parent.parent; + } + if (this.showTreeLeafNodeAlignDot()) { + sum += this.isTreeLevel() ? 0 : cell.padding.right + icon.margin.right; + } + + return sum; + } + + protected getTextIndent() { + const { size, margin } = this.getStyle().icon; + const contentIndent = this.getContentIndent(); + const treeIconWidth = + this.showTreeIcon() || + (this.isTreeLevel() && this.showTreeLeafNodeAlignDot()) + ? size + margin.right + : 0; + return contentIndent + treeIconWidth; + } + + // 判断当前节点的兄弟节点是否叶子节点 + protected isTreeLevel() { + return find( + get(this.meta, 'parent.children'), + (cell: ViewMeta) => !cell.isLeaf, + ); + } + + protected getIconPosition() { + // 不同 textAlign 下,对应的文字绘制点 x 不同 + const { x, y, textAlign } = this.textShape.cfg.attrs; + const iconMarginLeft = this.getStyle().icon.margin.left; + + if (textAlign === 'left') { + /** + * attrs.x + * | + * v + * +---------+ +----+ + * | text |--|icon| + * +---------+ +----+ + */ + return { + x: x + this.actualTextWidth + iconMarginLeft, + y, + }; + } + if (textAlign === 'right') { + /** + * attrs.x + * | + * v + * +---------+ +----+ + * | text |--|icon| + * +---------+ +----+ + */ + return { + x: x + iconMarginLeft, + y, + }; + } + + /** + * attrs.x + * | + * v + * +---------+ +----+ + * | text |--|icon| + * +---------+ +----+ + */ + return { + x: x + this.actualTextWidth / 2 + iconMarginLeft, + y, + }; + } + + protected getMaxTextWidth(): number { + const { width } = this.getContentArea(); + return width - this.getTextIndent() - this.getActionIconsWidth(); + } + + protected getTextArea(): SimpleBBox { + const content = this.getContentArea(); + const textIndent = this.getTextIndent(); + return { + ...content, + x: content.x + textIndent, + width: content.width - textIndent, + }; + } + + protected getAdjustTextAreaHeight( + textArea: SimpleBBox, + scrollY: number, + viewportHeight: number, + ): number { + let adjustTextAreaHeight = textArea.height; + if ( + !this.spreadsheet.facet.vScrollBar && + textArea.y + textArea.height > scrollY + viewportHeight + ) { + adjustTextAreaHeight = scrollY + viewportHeight - textArea.y; + } + return adjustTextAreaHeight; + } + + protected calculateTextY({ + textArea, + adjustTextAreaHeight, + }: { + textArea: SimpleBBox; + adjustTextAreaHeight: number; + }): number { + const { scrollY, viewportHeight } = this.headerConfig; + const { fontSize } = this.getTextStyle(); + return getAdjustPosition( + textArea.y, + adjustTextAreaHeight, + scrollY, + viewportHeight, + fontSize, + ); + } + + protected getTextPosition(): Point { + const textArea = this.getTextArea(); + const { scrollY, viewportHeight } = this.headerConfig; + + const adjustTextAreaHeight = this.getAdjustTextAreaHeight( + textArea, + scrollY, + viewportHeight, + ); + const textY = this.calculateTextY({ textArea, adjustTextAreaHeight }); + const textX = getTextAndFollowingIconPosition( + textArea, + this.getTextStyle(), + 0, + this.getIconStyle(), + this.getActionIconsCount(), + ).text.x; + return { x: textX, y: textY }; + } + + protected getIconYPosition() { + const textY = this.getTextPosition().y; + const { size } = this.getStyle().icon; + const { fontSize } = this.getTextStyle(); + return textY + (fontSize - size) / 2; + } +} diff --git a/packages/s2-core/src/cell/frozen-row-cell.ts b/packages/s2-core/src/cell/frozen-row-cell.ts deleted file mode 100644 index 3728072c39..0000000000 --- a/packages/s2-core/src/cell/frozen-row-cell.ts +++ /dev/null @@ -1,80 +0,0 @@ -import type { SimpleBBox } from '@antv/g-canvas'; -import { getAdjustPosition } from '../utils/text-absorption'; -import { getFrozenRowCfgPivot } from '../facet/utils'; -import type { BaseHeaderConfig } from '../facet/header/base'; -import { RowCell } from './row-cell'; - -/** - * Adapting the frozen first row for cells pivot table - */ -export class FrozenRowCell extends RowCell { - /** - * To indicate whether the current node is a frozen node - * - * PS: It is a specific config for the cell node, so it should not be extended in the headerConfig. - */ - protected frozenRowCell: boolean; - - protected handleRestOptions( - ...[headerConfig, ...options]: [BaseHeaderConfig, boolean] - ) { - super.handleRestOptions(headerConfig, options); - this.frozenRowCell = options[0]; - } - - protected getAdjustTextAreaHeight( - textArea: SimpleBBox, - scrollY: number, - viewportHeight: number, - ): number { - const correctY = textArea.y - this.getFrozenFirstRowHeight(); - let adjustTextAreaHeight = textArea.height; - if ( - !this.spreadsheet.facet.vScrollBar && - correctY + textArea.height > scrollY + viewportHeight - ) { - adjustTextAreaHeight = scrollY + viewportHeight - correctY; - } - return adjustTextAreaHeight; - } - - protected calculateTextY({ - textArea, - adjustTextAreaHeight, - }: { - textArea: SimpleBBox; - adjustTextAreaHeight: number; - }): number { - const { scrollY, viewportHeight } = this.headerConfig; - const { fontSize } = this.getTextStyle(); - return getAdjustPosition( - textArea.y, - adjustTextAreaHeight, - // viewportLeft: start at the frozen row position - scrollY + this.getFrozenFirstRowHeight(), - viewportHeight, - fontSize, - ); - } - - protected getResizeClipAreaBBox(): SimpleBBox { - return { - ...super.getResizeClipAreaBBox(), - y: this.getFrozenFirstRowHeight(), - }; - } - - private getFrozenFirstRowHeight(): number { - if (this.frozenRowCell) { - // frozen row cell - return 0; - } - const { spreadsheet } = this.headerConfig; - const { facet } = spreadsheet; - const { frozenRowHeight } = getFrozenRowCfgPivot( - spreadsheet.options, - facet?.layoutResult?.rowNodes, - ); - return frozenRowHeight; - } -} diff --git a/packages/s2-core/src/cell/index.ts b/packages/s2-core/src/cell/index.ts index 54eb1eafba..8503b33e5d 100644 --- a/packages/s2-core/src/cell/index.ts +++ b/packages/s2-core/src/cell/index.ts @@ -4,12 +4,12 @@ import { CornerCell } from './corner-cell'; import { DataCell } from './data-cell'; import { HeaderCell } from './header-cell'; import { MergedCell } from './merged-cell'; -import { RowCell } from './row-cell'; import { TableColCell } from './table-col-cell'; import { TableCornerCell } from './table-corner-cell'; import { TableDataCell } from './table-data-cell'; import { TableSeriesCell } from './table-series-cell'; -import { FrozenRowCell } from './frozen-row-cell'; +import { BaseRowCell } from './base-row-cell'; +import { RowCell } from './row-cell'; import { SeriesNumberCell } from './series-number-cell'; export { @@ -18,12 +18,12 @@ export { TableSeriesCell, TableDataCell, RowCell, + BaseRowCell, ColCell, DataCell, MergedCell, CornerCell, BaseCell, HeaderCell, - FrozenRowCell, SeriesNumberCell, }; diff --git a/packages/s2-core/src/cell/row-cell.ts b/packages/s2-core/src/cell/row-cell.ts index f26f76ccb7..2994cf72c0 100644 --- a/packages/s2-core/src/cell/row-cell.ts +++ b/packages/s2-core/src/cell/row-cell.ts @@ -1,442 +1,25 @@ -import type { Point } from '@antv/g-canvas'; -import { GM } from '@antv/g-gesture'; -import { find, get, isEmpty } from 'lodash'; import type { SimpleBBox } from '@antv/g-canvas'; -import { - CellTypes, - KEY_GROUP_ROW_RESIZE_AREA, - ResizeAreaEffect, - ResizeDirectionType, - S2Event, -} from '../common/constant'; -import { CellBorderPosition, type ViewMeta } from '../common/interface'; -import type { RowHeaderConfig } from '../facet/header/row'; -import { - getBorderPositionAndStyle, - getTextAndFollowingIconPosition, -} from '../utils/cell/cell'; -import { - renderCircle, - renderLine, - renderRect, - renderTreeIcon, -} from '../utils/g-renders'; -import { getAllChildrenNodeHeight } from '../utils/get-all-children-node-height'; -import { - getOrCreateResizeAreaGroupById, - getResizeAreaAttrs, -} from '../utils/interaction/resize'; -import { isMobile } from '../utils/is-mobile'; import { getAdjustPosition } from '../utils/text-absorption'; -import { shouldAddResizeArea } from './../utils/interaction/resize'; -import { HeaderCell } from './header-cell'; - -export class RowCell extends HeaderCell { - protected declare headerConfig: RowHeaderConfig; - - protected gm: GM; - - public get cellType() { - return CellTypes.ROW_CELL; - } - - public destroy(): void { - super.destroy(); - this.gm?.destroy(); - } - - protected initCell() { - super.initCell(); - // 绘制单元格背景 - this.drawBackgroundShape(); - // 绘制交互背景 - this.drawInteractiveBgShape(); - // 绘制交互边框 - this.drawInteractiveBorderShape(); - // 绘制单元格文本 - this.drawTextShape(); - // 绘制字段标记 -- icon - this.drawConditionIconShapes(); - // 绘制树状模式收起展开的 icon - this.drawTreeIcon(); - // 绘制树状模式下子节点层级占位圆点 - this.drawTreeLeafNodeAlignDot(); - // 绘制单元格边框 - this.drawRectBorder(); - // 绘制 resize 热区 - this.drawResizeAreaInLeaf(); - // 绘制 action icons - this.drawActionIcons(); - this.update(); - } - - public getBackgroundColor() { - const { backgroundColor, backgroundColorOpacity } = - this.getCrossBackgroundColor(this.meta.rowIndex); - return this.getBackgroundColorByCondition( - backgroundColor, - backgroundColorOpacity, - ); - } - +import { getFrozenRowCfgPivot } from '../facet/utils'; +import type { BaseHeaderConfig } from '../facet/header/base'; +import { BaseRowCell } from './base-row-cell'; + +/** + * Adapting the frozen first row for cells pivot table + */ +export class RowCell extends BaseRowCell { /** - * 绘制hover悬停,刷选的外框 + * To indicate whether the current node is a frozen node + * + * PS: It is a specific config for the cell node, so it should not be extended in the headerConfig. */ - protected drawInteractiveBorderShape() { - // 往内缩一个像素,避免和外边框重叠 - const margin = 2; - - this.stateShapes.set( - 'interactiveBorderShape', - renderRect(this, this.getInteractiveBorderShapeStyle(margin), { - visible: false, - }), - ); - } - - // 交互使用的背景色 - protected drawInteractiveBgShape() { - this.stateShapes.set( - 'interactiveBgShape', - renderRect( - this, - { - ...this.getCellArea(), - }, - { - visible: false, - }, - ), - ); - } - - protected showTreeIcon() { - return this.spreadsheet.isHierarchyTreeType() && !this.meta.isLeaf; - } - - protected showTreeLeafNodeAlignDot() { - return ( - this.spreadsheet.options.style?.showTreeLeafNodeAlignDot && - this.spreadsheet.isHierarchyTreeType() - ); - } + protected frozenRowCell: boolean; - // 获取树状模式下叶子节点的父节点收起展开 icon 图形属性 - protected getParentTreeIconCfg() { - if ( - !this.showTreeLeafNodeAlignDot() || - !this.spreadsheet.isHierarchyTreeType() || - !this.meta.isLeaf - ) { - return; - } - - return get(this.meta, 'parent.belongsCell.treeIcon.cfg'); - } - - // draw tree icon - protected drawTreeIcon() { - if (!this.showTreeIcon()) { - return; - } - - const { isCollapsed, id, hierarchy } = this.meta; - const { x } = this.getContentArea(); - const { fill } = this.getTextStyle(); - const { size } = this.getStyle().icon; - - const contentIndent = this.getContentIndent(); - - const iconX = x + contentIndent; - const iconY = this.getIconYPosition(); - - this.treeIcon = renderTreeIcon( - this, - { - x: iconX, - y: iconY, - width: size, - height: size, - }, - fill, - isCollapsed, - () => { - if (isMobile()) { - return; - } - // 折叠行头时因scrollY没变,导致底层出现空白 - if (!isCollapsed) { - const oldScrollY = this.spreadsheet.store.get('scrollY'); - // 可视窗口高度 - const viewportHeight = this.headerConfig.viewportHeight || 0; - // 被折叠项的高度 - const deleteHeight = getAllChildrenNodeHeight(this.meta); - // 折叠后真实高度 - const realHeight = hierarchy.height - deleteHeight; - if (oldScrollY > 0 && oldScrollY + viewportHeight > realHeight) { - const currentScrollY = realHeight - viewportHeight; - this.spreadsheet.store.set( - 'scrollY', - currentScrollY > 0 ? currentScrollY : 0, - ); - } - } - this.spreadsheet.emit(S2Event.ROW_CELL_COLLAPSE_TREE_ROWS, { - id, - isCollapsed: !isCollapsed, - node: this.meta, - }); - }, - ); - - // in mobile, we use this cell - if (isMobile()) { - this.gm = new GM(this, { - gestures: ['Tap'], - }); - this.gm.on('tap', () => { - this.spreadsheet.emit(S2Event.ROW_CELL_COLLAPSE_TREE_ROWS, { - id, - isCollapsed: !isCollapsed, - node: this.meta, - }); - }); - } - } - - protected drawTreeLeafNodeAlignDot() { - const parentTreeIconCfg = this.getParentTreeIconCfg(); - if (!parentTreeIconCfg) { - return; - } - const { size, margin } = this.getStyle().icon; - const x = parentTreeIconCfg.x + size + margin.right; - const textY = this.getTextPosition().y; - - const { fill, fontSize } = this.getTextStyle(); - const r = size / 5; // 半径,暂时先写死,后面看是否有这个点点的定制需求 - this.treeLeafNodeAlignDot = renderCircle(this, { - x: x + size / 2, // 和收起展开 icon 保持居中对齐 - y: textY + (fontSize - r) / 2, - r, - fill, - fillOpacity: 0.3, // 暂时先写死,后面看是否有这个点点的定制需求 - }); - } - - protected isBolderText() { - // 非叶子节点、小计总计,均为粗体 - const { isLeaf, isTotals, level } = this.meta; - return (!isLeaf && level === 0) || isTotals; - } - - // draw text - protected drawTextShape() { - super.drawTextShape(); - this.drawLinkField(this.meta); - } - - protected drawRectBorder() { - const { x } = this.getCellArea(); - - const contentIndent = this.getContentIndent(); - const finalX = this.spreadsheet.isHierarchyTreeType() - ? x - : x + contentIndent; - [CellBorderPosition.BOTTOM, CellBorderPosition.LEFT].forEach((type) => { - const { position, style } = getBorderPositionAndStyle( - type, - { - ...this.getCellArea(), - x: finalX, - }, - this.getStyle().cell, - ); - renderLine(this, position, style); - }); - } - - protected getResizeClipAreaBBox(): SimpleBBox { - const { width, viewportHeight } = this.headerConfig; - return { - x: 0, - y: 0, - width, - height: viewportHeight, - }; - } - - protected drawResizeAreaInLeaf() { - if ( - !this.meta.isLeaf || - !this.shouldDrawResizeAreaByType('rowCellVertical', this) - ) { - return; - } - - const { x, y, width, height } = this.getCellArea(); - const resizeStyle = this.getResizeAreaStyle(); - const resizeArea = getOrCreateResizeAreaGroupById( - this.spreadsheet, - KEY_GROUP_ROW_RESIZE_AREA, - ); - - const { - position, - seriesNumberWidth, - width: headerWidth, - scrollX, - scrollY, - } = this.headerConfig; - - const resizeAreaBBox = { - // fix: When scrolling without the entire frozen header horizontally, the resize area would be removed permanently. - x: x + seriesNumberWidth, - y: y + height - resizeStyle.size / 2, - width, - height: resizeStyle.size, - }; - - const resizeClipAreaBBox = this.getResizeClipAreaBBox(); - - if ( - !shouldAddResizeArea(resizeAreaBBox, resizeClipAreaBBox, { - scrollX, - scrollY, - }) - ) { - return; - } - - const offsetX = position?.x + x - scrollX + seriesNumberWidth; - const offsetY = position?.y + y - scrollY; - - const resizeAreaWidth = this.spreadsheet.isFrozenRowHeader() - ? headerWidth - seriesNumberWidth - (x - scrollX) - : width; - - resizeArea.addShape('rect', { - attrs: { - ...getResizeAreaAttrs({ - id: this.meta.id, - theme: resizeStyle, - type: ResizeDirectionType.Vertical, - effect: ResizeAreaEffect.Cell, - offsetX, - offsetY, - width, - height, - meta: this.meta, - }), - x: offsetX, - y: offsetY + height - resizeStyle.size / 2, - width: resizeAreaWidth, - }, - }); - } - - protected getContentIndent() { - if (!this.spreadsheet.isHierarchyTreeType()) { - return 0; - } - const { icon, cell } = this.getStyle(); - const iconWidth = icon.size + icon.margin.right; - - let parent = this.meta.parent; - let sum = 0; - while (parent) { - if (parent.height !== 0) { - sum += iconWidth; - } - parent = parent.parent; - } - if (this.showTreeLeafNodeAlignDot()) { - sum += this.isTreeLevel() ? 0 : cell.padding.right + icon.margin.right; - } - - return sum; - } - - protected getTextIndent() { - const { size, margin } = this.getStyle().icon; - const contentIndent = this.getContentIndent(); - const treeIconWidth = - this.showTreeIcon() || - (this.isTreeLevel() && this.showTreeLeafNodeAlignDot()) - ? size + margin.right - : 0; - return contentIndent + treeIconWidth; - } - - // 判断当前节点的兄弟节点是否叶子节点 - protected isTreeLevel() { - return find( - get(this.meta, 'parent.children'), - (cell: ViewMeta) => !cell.isLeaf, - ); - } - - protected getIconPosition() { - // 不同 textAlign 下,对应的文字绘制点 x 不同 - const { x, y, textAlign } = this.textShape.cfg.attrs; - const iconMarginLeft = this.getStyle().icon.margin.left; - - if (textAlign === 'left') { - /** - * attrs.x - * | - * v - * +---------+ +----+ - * | text |--|icon| - * +---------+ +----+ - */ - return { - x: x + this.actualTextWidth + iconMarginLeft, - y, - }; - } - if (textAlign === 'right') { - /** - * attrs.x - * | - * v - * +---------+ +----+ - * | text |--|icon| - * +---------+ +----+ - */ - return { - x: x + iconMarginLeft, - y, - }; - } - - /** - * attrs.x - * | - * v - * +---------+ +----+ - * | text |--|icon| - * +---------+ +----+ - */ - return { - x: x + this.actualTextWidth / 2 + iconMarginLeft, - y, - }; - } - - protected getMaxTextWidth(): number { - const { width } = this.getContentArea(); - return width - this.getTextIndent() - this.getActionIconsWidth(); - } - - protected getTextArea(): SimpleBBox { - const content = this.getContentArea(); - const textIndent = this.getTextIndent(); - return { - ...content, - x: content.x + textIndent, - width: content.width - textIndent, - }; + protected handleRestOptions( + ...[headerConfig, ...options]: [BaseHeaderConfig, boolean] + ) { + super.handleRestOptions(headerConfig, options); + this.frozenRowCell = options[0]; } protected getAdjustTextAreaHeight( @@ -444,12 +27,13 @@ export class RowCell extends HeaderCell { scrollY: number, viewportHeight: number, ): number { + const correctY = textArea.y - this.getFrozenFirstRowHeight(); let adjustTextAreaHeight = textArea.height; if ( !this.spreadsheet.facet.vScrollBar && - textArea.y + textArea.height > scrollY + viewportHeight + correctY + textArea.height > scrollY + viewportHeight ) { - adjustTextAreaHeight = scrollY + viewportHeight - textArea.y; + adjustTextAreaHeight = scrollY + viewportHeight - correctY; } return adjustTextAreaHeight; } @@ -466,36 +50,31 @@ export class RowCell extends HeaderCell { return getAdjustPosition( textArea.y, adjustTextAreaHeight, - scrollY, + // viewportLeft: start at the frozen row position + scrollY + this.getFrozenFirstRowHeight(), viewportHeight, fontSize, ); } - protected getTextPosition(): Point { - const textArea = this.getTextArea(); - const { scrollY, viewportHeight } = this.headerConfig; - - const adjustTextAreaHeight = this.getAdjustTextAreaHeight( - textArea, - scrollY, - viewportHeight, - ); - const textY = this.calculateTextY({ textArea, adjustTextAreaHeight }); - const textX = getTextAndFollowingIconPosition( - textArea, - this.getTextStyle(), - 0, - this.getIconStyle(), - this.getActionIconsCount(), - ).text.x; - return { x: textX, y: textY }; + protected getResizeClipAreaBBox(): SimpleBBox { + return { + ...super.getResizeClipAreaBBox(), + y: this.getFrozenFirstRowHeight(), + }; } - protected getIconYPosition() { - const textY = this.getTextPosition().y; - const { size } = this.getStyle().icon; - const { fontSize } = this.getTextStyle(); - return textY + (fontSize - size) / 2; + private getFrozenFirstRowHeight(): number { + if (this.frozenRowCell) { + // frozen row cell + return 0; + } + const { spreadsheet } = this.headerConfig; + const { facet } = spreadsheet; + const { frozenRowHeight } = getFrozenRowCfgPivot( + spreadsheet.options, + facet?.layoutResult?.rowNodes, + ); + return frozenRowHeight; } } diff --git a/packages/s2-core/src/cell/series-number-cell.ts b/packages/s2-core/src/cell/series-number-cell.ts index 99193ef260..eefc400b7f 100644 --- a/packages/s2-core/src/cell/series-number-cell.ts +++ b/packages/s2-core/src/cell/series-number-cell.ts @@ -1,8 +1,8 @@ import type { Point } from '@antv/g-canvas'; import type { Condition, IconTheme, MappingResult, TextTheme } from '../common'; -import { FrozenRowCell } from './frozen-row-cell'; +import { RowCell } from './row-cell'; -export class SeriesNumberCell extends FrozenRowCell { +export class SeriesNumberCell extends RowCell { protected initCell(): void { this.drawBackgroundShape(); this.drawRectBorder(); diff --git a/packages/s2-core/src/common/interface/emitter.ts b/packages/s2-core/src/common/interface/emitter.ts index 58c42b7079..74b400f36b 100644 --- a/packages/s2-core/src/common/interface/emitter.ts +++ b/packages/s2-core/src/common/interface/emitter.ts @@ -1,7 +1,7 @@ import type { Event as CanvasEvent } from '@antv/g-canvas'; import type { ColCell } from '../../cell/col-cell'; import type { DataCell } from '../../cell/data-cell'; -import type { RowCell } from '../../cell/row-cell'; +import type { BaseRowCell } from '../../cell/base-row-cell'; import type { S2Event } from '../../common/constant'; import type { CellMeta, @@ -106,7 +106,7 @@ export interface EmitterType { data: RowCellCollapseTreeRowsType, ) => void; [S2Event.ROW_CELL_SCROLL]: (position: CellScrollPosition) => void; - [S2Event.ROW_CELL_BRUSH_SELECTION]: (cells: RowCell[]) => void; + [S2Event.ROW_CELL_BRUSH_SELECTION]: (cells: BaseRowCell[]) => void; /** ================ Col Cell ================ */ [S2Event.COL_CELL_MOUSE_DOWN]: CanvasEventHandler; diff --git a/packages/s2-core/src/common/interface/interaction.ts b/packages/s2-core/src/common/interface/interaction.ts index 4bba56f0ef..1f67b44ae2 100644 --- a/packages/s2-core/src/common/interface/interaction.ts +++ b/packages/s2-core/src/common/interface/interaction.ts @@ -1,6 +1,7 @@ import type { SimpleBBox } from '@antv/g-canvas'; import type { BaseCell, + BaseRowCell, ColCell, CornerCell, DataCell, @@ -27,6 +28,7 @@ export type S2CellType = | ColCell | CornerCell | RowCell + | BaseRowCell | MergedCell | BaseCell; diff --git a/packages/s2-core/src/facet/header/base-frozen-row.ts b/packages/s2-core/src/facet/header/base-frozen-row.ts index d7f34ef3a2..e3b2fe6c8b 100644 --- a/packages/s2-core/src/facet/header/base-frozen-row.ts +++ b/packages/s2-core/src/facet/header/base-frozen-row.ts @@ -98,7 +98,7 @@ export class BaseFrozenRowHeader extends RowHeader { }); } - public isFrozenRow(item: Node): boolean { + public isFrozenRow(node: Node): boolean { const { spreadsheet } = this.headerConfig; const { facet } = spreadsheet; const { frozenRowCount } = getFrozenRowCfgPivot( @@ -106,7 +106,7 @@ export class BaseFrozenRowHeader extends RowHeader { facet.layoutResult?.rowNodes, ); return ( - frozenRowCount > 0 && item.rowIndex >= 0 && item.rowIndex < frozenRowCount + frozenRowCount > 0 && node.rowIndex >= 0 && node.rowIndex < frozenRowCount ); } diff --git a/packages/s2-core/src/facet/header/pivot-row.ts b/packages/s2-core/src/facet/header/pivot-row.ts index 493bd13772..8277320fd3 100644 --- a/packages/s2-core/src/facet/header/pivot-row.ts +++ b/packages/s2-core/src/facet/header/pivot-row.ts @@ -1,13 +1,14 @@ import type { Node } from '../layout/node'; -import { FrozenRowCell, RowCell } from '../../cell'; +import { RowCell } from '../../cell/row-cell'; import { BaseFrozenRowHeader } from './base-frozen-row'; export class PivotRowHeader extends BaseFrozenRowHeader { - protected createCellInstance(item: Node): RowCell { + protected createCellInstance(node: Node): RowCell { const { spreadsheet, scrollY } = this.headerConfig; - const frozenRow = this.isFrozenRow(item); - return new FrozenRowCell( - item, + const frozenRow = this.isFrozenRow(node); + + return new RowCell( + node, spreadsheet, { ...this.headerConfig, diff --git a/packages/s2-core/src/interaction/base-interaction/click/data-cell-click.ts b/packages/s2-core/src/interaction/base-interaction/click/data-cell-click.ts index f1b351d15c..d19db80322 100644 --- a/packages/s2-core/src/interaction/base-interaction/click/data-cell-click.ts +++ b/packages/s2-core/src/interaction/base-interaction/click/data-cell-click.ts @@ -1,7 +1,6 @@ import type { Event as CanvasEvent } from '@antv/g-canvas'; import { forEach } from 'lodash'; import type { DataCell } from '../../../cell/data-cell'; -import type { RowCell } from '../../../cell/row-cell'; import { InteractionStateName, InterceptType, @@ -87,8 +86,8 @@ export class DataCellClick extends BaseEvent implements BaseEventImplement { meta, spreadsheet, ); - forEach(allRowHeaderCells, (cell: RowCell) => { - cell.updateByState(InteractionStateName.SELECTED); + forEach(allRowHeaderCells, (rowCell) => { + rowCell.updateByState(InteractionStateName.SELECTED); }); } } diff --git a/packages/s2-react/__tests__/data/strategy-data.ts b/packages/s2-react/__tests__/data/strategy-data.ts index 4a9f7a6d48..e63defac4b 100644 --- a/packages/s2-react/__tests__/data/strategy-data.ts +++ b/packages/s2-react/__tests__/data/strategy-data.ts @@ -389,6 +389,7 @@ export const StrategyOptions: SheetComponentOptions = { width: 800, height: 800, cornerText: '指标', + // frozenFirstRow: true, placeholder: (v) => { const placeholder = v?.fieldValue ? '-' : ''; return placeholder; diff --git a/packages/s2-react/playground/config.ts b/packages/s2-react/playground/config.ts index 8ddb33ad16..4b89787962 100644 --- a/packages/s2-react/playground/config.ts +++ b/packages/s2-react/playground/config.ts @@ -80,7 +80,7 @@ export const s2Options: SheetComponentOptions = { debug: true, width: 600, height: 400, - frozenFirstRow: true, + frozenFirstRow: false, showSeriesNumber: false, interaction: { enableCopy: true, diff --git a/packages/s2-react/playground/index.tsx b/packages/s2-react/playground/index.tsx index e9829dd31e..a5a9c6a4e2 100644 --- a/packages/s2-react/playground/index.tsx +++ b/packages/s2-react/playground/index.tsx @@ -843,6 +843,24 @@ function MainLayout() { }} disabled={sheetType === 'table'} /> + + { + updateOptions({ + frozenFirstRow: checked, + }); + }} + disabled={ + sheetType === 'table' || + (mergedOptions.hierarchyType === 'grid' && + (!mergedOptions?.totals?.row?.showGrandTotals || + !mergedOptions?.totals?.row?.reverseLayout)) + } + /> + + +
```ts const s2Options = { - frozenRowHeader: true, + frozenRowHeader: false, // 默认开启 } ``` -preview +preview ### 冻结首行 @antv/s2@^1.53.0 新增 diff --git a/s2-site/examples/basic/pivot/demo/grid.ts b/s2-site/examples/basic/pivot/demo/grid.ts index bd3947c201..0dfa6ca497 100644 --- a/s2-site/examples/basic/pivot/demo/grid.ts +++ b/s2-site/examples/basic/pivot/demo/grid.ts @@ -10,7 +10,10 @@ fetch( const s2Options = { width: 600, height: 480, + // 冻结行头 + // frozenRowHeader: true }; + const s2 = new PivotSheet(container, dataCfg, s2Options); s2.render(); diff --git a/s2-site/examples/basic/pivot/demo/tree.ts b/s2-site/examples/basic/pivot/demo/tree.ts index 063f99c7f5..35e710662f 100644 --- a/s2-site/examples/basic/pivot/demo/tree.ts +++ b/s2-site/examples/basic/pivot/demo/tree.ts @@ -1,23 +1,31 @@ import { PivotSheet } from '@antv/s2'; fetch( - 'https://gw.alipayobjects.com/os/bmw-prod/2a5dbbc8-d0a7-4d02-b7c9-34f6ca63cff6.json', + 'https://gw.alipayobjects.com/os/bmw-prod/2a5dbbc8-d0a7-4d02-b7c9-34f6ca63cff6.json', ) - .then((res) => res.json()) - .then((dataCfg) => { - const container = document.getElementById('container'); + .then((res) => res.json()) + .then((dataCfg) => { + const container = document.getElementById('container'); - const s2Options = { - width: 600, - height: 480, - hierarchyType: 'tree', - style: { - collapsedRows: { - 'root[&]浙江省': true, // 折叠浙江省下面所有的城市 - } - } - }; - const s2 = new PivotSheet(container, dataCfg, s2Options); + const s2Options = { + width: 600, + height: 480, + hierarchyType: 'tree', + style: { + // 折叠全部 + // hierarchyCollapse: true, - s2.render(); - }); + // 折叠浙江省下面所有的城市 + collapsedRows: { + 'root[&]浙江省': true, + }, + }, + + // 冻结行头 + // frozenRowHeader: true + }; + + const s2 = new PivotSheet(container, dataCfg, s2Options); + + s2.render(); + }); diff --git a/s2-site/examples/interaction/basic/demo/frozen-row-header.ts b/s2-site/examples/interaction/basic/demo/frozen-row-header.ts new file mode 100644 index 0000000000..e51fb43708 --- /dev/null +++ b/s2-site/examples/interaction/basic/demo/frozen-row-header.ts @@ -0,0 +1,38 @@ +import { PivotSheet, S2Event, S2Options } from '@antv/s2'; + +fetch( + 'https://gw.alipayobjects.com/os/bmw-prod/2a5dbbc8-d0a7-4d02-b7c9-34f6ca63cff6.json', +) + .then((res) => res.json()) + .then((dataCfg) => { + const container = document.getElementById('container'); + + const s2Options: S2Options = { + width: 600, + height: 480, + hierarchyType: 'tree', // 'tree' | 'grid' + // 默认开启行头冻结, 关闭后滚动区域为整个表格 + frozenRowHeader: true, + style: { + rowCfg: { + treeRowsWidth: 400, + width: 200, + }, + colCfg: { + width: 200, + }, + }, + }; + + const s2 = new PivotSheet(container, dataCfg, s2Options); + + s2.on(S2Event.GLOBAL_SCROLL, (e) => { + console.log('scroll', e); + }); + + s2.on(S2Event.ROW_CELL_SCROLL, (e) => { + console.log('row cell scroll', e); + }); + + s2.render(); + }); diff --git a/s2-site/examples/interaction/basic/demo/meta.json b/s2-site/examples/interaction/basic/demo/meta.json index f3a6593ce6..3be5a6e3d1 100644 --- a/s2-site/examples/interaction/basic/demo/meta.json +++ b/s2-site/examples/interaction/basic/demo/meta.json @@ -63,11 +63,19 @@ { "filename": "frozen.ts", "title": { - "zh": "行列冻结", - "en": "Freeze Rows And Cols" + "zh": "明细表 - 行列冻结", + "en": "TableSheet - Frozen Rows And Cols" }, "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/%24a16EsOCR8/frozeb.gif" }, + { + "filename": "frozen-row-header.ts", + "title": { + "zh": "透视表 - 行头冻结", + "en": "PivotSheet - Frozen Row Header" + }, + "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*kk0ETbbbnOsAAAAAAAAAAAAADmJ7AQ/original" + }, { "filename": "auto-reset-sheet-style.ts", "title": { From 2c40b87ed407e962ecdeaf48885a1caaa4a6b8ad Mon Sep 17 00:00:00 2001 From: lijinke666 Date: Tue, 5 Dec 2023 17:15:06 +0800 Subject: [PATCH 2/3] =?UTF-8?q?chore:=20=E7=B1=BB=E5=9E=8B=E4=BF=AE?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../unit/utils/interaction/state-controller-spec.ts | 4 ++-- packages/s2-core/src/common/interface/emitter.ts | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/s2-core/__tests__/unit/utils/interaction/state-controller-spec.ts b/packages/s2-core/__tests__/unit/utils/interaction/state-controller-spec.ts index f9aae9923d..a1610f0a9a 100644 --- a/packages/s2-core/__tests__/unit/utils/interaction/state-controller-spec.ts +++ b/packages/s2-core/__tests__/unit/utils/interaction/state-controller-spec.ts @@ -1,5 +1,5 @@ +import type { RowCell } from '../../../../src/cell'; import { getCellMeta } from '@/utils/interaction/select-event'; -import type { BaseRowCell } from '@/cell/base-row-cell'; import { CellTypes, InteractionStateName } from '@/common/constant/interaction'; import type { S2Options } from '@/common/interface'; import { Store } from '@/common/store'; @@ -25,7 +25,7 @@ describe('State Controller Utils Tests', () => { id: `root[&]price`, }; }, - } as unknown as BaseRowCell; + } as unknown as RowCell; let mockInstance: SpreadSheet; diff --git a/packages/s2-core/src/common/interface/emitter.ts b/packages/s2-core/src/common/interface/emitter.ts index 74b400f36b..800f878f95 100644 --- a/packages/s2-core/src/common/interface/emitter.ts +++ b/packages/s2-core/src/common/interface/emitter.ts @@ -1,7 +1,5 @@ import type { Event as CanvasEvent } from '@antv/g-canvas'; -import type { ColCell } from '../../cell/col-cell'; -import type { DataCell } from '../../cell/data-cell'; -import type { BaseRowCell } from '../../cell/base-row-cell'; +import type { ColCell, DataCell, RowCell } from '../../cell'; import type { S2Event } from '../../common/constant'; import type { CellMeta, @@ -106,7 +104,7 @@ export interface EmitterType { data: RowCellCollapseTreeRowsType, ) => void; [S2Event.ROW_CELL_SCROLL]: (position: CellScrollPosition) => void; - [S2Event.ROW_CELL_BRUSH_SELECTION]: (cells: BaseRowCell[]) => void; + [S2Event.ROW_CELL_BRUSH_SELECTION]: (cells: RowCell[]) => void; /** ================ Col Cell ================ */ [S2Event.COL_CELL_MOUSE_DOWN]: CanvasEventHandler; From 3006ab3f966f132553405ca43f27b3317f946361 Mon Sep 17 00:00:00 2001 From: lijinke666 Date: Tue, 5 Dec 2023 17:49:28 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E8=A1=8C=E5=A4=B4?= =?UTF-8?q?=E6=BB=9A=E5=8A=A8=E5=88=B7=E9=80=89=E8=8E=B7=E5=8F=96=E5=88=B0?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E7=9A=84=E5=AE=9E=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/s2-core/src/facet/header/pivot-row.ts | 2 +- packages/s2-core/src/facet/header/row.ts | 6 +++--- packages/s2-core/src/facet/header/series-number.ts | 9 ++++----- .../interaction/brush-selection/row-brush-selection.ts | 4 ++-- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/s2-core/src/facet/header/pivot-row.ts b/packages/s2-core/src/facet/header/pivot-row.ts index 8277320fd3..917f321678 100644 --- a/packages/s2-core/src/facet/header/pivot-row.ts +++ b/packages/s2-core/src/facet/header/pivot-row.ts @@ -3,7 +3,7 @@ import { RowCell } from '../../cell/row-cell'; import { BaseFrozenRowHeader } from './base-frozen-row'; export class PivotRowHeader extends BaseFrozenRowHeader { - protected createCellInstance(node: Node): RowCell { + public createCellInstance(node: Node): RowCell { const { spreadsheet, scrollY } = this.headerConfig; const frozenRow = this.isFrozenRow(node); diff --git a/packages/s2-core/src/facet/header/row.ts b/packages/s2-core/src/facet/header/row.ts index 4e6d2b2523..7c4e2b172a 100644 --- a/packages/s2-core/src/facet/header/row.ts +++ b/packages/s2-core/src/facet/header/row.ts @@ -46,11 +46,11 @@ export class RowHeader extends BaseHeader { ); // right } - protected createCellInstance(item: Node) { - return new RowCell(item, this.headerConfig.spreadsheet, this.headerConfig); + public createCellInstance(node: Node) { + return new RowCell(node, this.headerConfig.spreadsheet, this.headerConfig); } - protected getCellGroup(item: Node): IGroup { + protected getCellGroup(node: Node): IGroup { return this; } diff --git a/packages/s2-core/src/facet/header/series-number.ts b/packages/s2-core/src/facet/header/series-number.ts index b46410ca96..09f541735b 100644 --- a/packages/s2-core/src/facet/header/series-number.ts +++ b/packages/s2-core/src/facet/header/series-number.ts @@ -72,10 +72,10 @@ export class SeriesNumberHeader extends BaseFrozenRowHeader { }); } - protected createCellInstance(item: Node): RowCell { - const frozenRow = this.isFrozenRow(item); - const cell = new SeriesNumberCell( - item, + public createCellInstance(node: Node): RowCell { + const frozenRow = this.isFrozenRow(node); + return new SeriesNumberCell( + node, this.headerConfig.spreadsheet, { ...this.headerConfig, @@ -83,6 +83,5 @@ export class SeriesNumberHeader extends BaseFrozenRowHeader { }, frozenRow, ); - return cell; } } diff --git a/packages/s2-core/src/interaction/brush-selection/row-brush-selection.ts b/packages/s2-core/src/interaction/brush-selection/row-brush-selection.ts index 57e842c95d..91df80b66a 100644 --- a/packages/s2-core/src/interaction/brush-selection/row-brush-selection.ts +++ b/packages/s2-core/src/interaction/brush-selection/row-brush-selection.ts @@ -1,6 +1,6 @@ import type { Point } from '@antv/g-canvas'; import { isNil, last, map } from 'lodash'; -import { RowCell } from '../../cell'; +import { BaseRowCell, RowCell } from '../../cell'; import { InterceptType, S2Event } from '../../common/constant'; import { InteractionBrushSelectionStage, @@ -129,7 +129,7 @@ export class RowBrushSelection extends BaseBrushSelection { } // TODO: 先暂时不考虑自定义单元格的情况, next 分支把这些单元格 (包括自定义单元格) 都放在了 s2.options.rowCell 里 - return new RowCell(node, this.spreadsheet); + return this.spreadsheet.facet.rowHeader.createCellInstance(node); }); }