From 4dcb1a2344783c8df283071bee1f8b07988b9b01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=98=A4=E5=98=A4=E5=98=A4?= Date: Thu, 23 Sep 2021 21:45:53 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E2=9C=A8=20add=20custom=20header=20act?= =?UTF-8?q?ion=20icons=20(#331)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: :recycle: rename the icon param from "type" to "row" * chore: :pencil2: fix lint * feat: :sparkles: add drilldown action icon * feat: :sparkles: allow to register custom svg icons * feat: :art: set default sortIcons * fix: :bug: tweak the position of custom header action icons * style: :lipstick: add fill for the action icon * chore: :pencil2: fix lint * style: :lipstick: tweak the row header icon * chore: delete useless husky * chore: delete useless code * refactor: replace the display and customDisplayByLabelName by displayCondition of HeaderActionIcon * fix: fix the test * fix: solve the test * fix: sovle the test --- .../spreadsheet/multiple-values-cell-spec.tsx | 30 +-- .../spreadsheet/spread-sheet-spec.tsx | 71 +++++- packages/s2-core/src/cell/base-cell.ts | 24 +- packages/s2-core/src/cell/col-cell.ts | 52 ----- packages/s2-core/src/cell/corner-cell.ts | 27 ++- packages/s2-core/src/cell/data-cell.ts | 24 +- packages/s2-core/src/cell/header-cell.ts | 208 ++++++++++++++---- packages/s2-core/src/cell/row-cell.ts | 94 +------- packages/s2-core/src/cell/table-col-cell.ts | 18 +- .../src/common/constant/events/basic.ts | 3 + packages/s2-core/src/common/icons/factory.ts | 8 +- packages/s2-core/src/common/icons/gui-icon.ts | 14 +- .../s2-core/src/common/icons/html-icon.tsx | 6 +- .../s2-core/src/common/interface/basic.ts | 46 ++-- .../s2-core/src/common/interface/emitter.ts | 2 + .../s2-core/src/common/interface/s2Options.ts | 16 +- .../src/components/drill-down/index.less | 6 + .../components/sheets/base-sheet/index.tsx | 14 +- .../src/components/sheets/interface.ts | 4 +- .../src/interaction/base-interaction/hover.ts | 2 +- .../src/interaction/event-controller.ts | 8 + .../s2-core/src/sheet-type/pivot-sheet.ts | 48 +++- .../s2-core/src/sheet-type/spread-sheet.ts | 31 ++- packages/s2-core/src/theme/index.ts | 13 +- .../src/ui/tooltip/components/icon.tsx | 4 +- .../s2-core/src/utils/drill-down/helper.ts | 45 ++-- packages/s2-core/src/utils/g-renders.ts | 2 +- .../utils/layout/add-detail-type-sort-icon.ts | 24 +- .../s2-core/src/utils/layout/generate-id.ts | 3 +- packages/s2-core/src/utils/merge.ts | 4 +- 30 files changed, 530 insertions(+), 321 deletions(-) diff --git a/packages/s2-core/__tests__/spreadsheet/multiple-values-cell-spec.tsx b/packages/s2-core/__tests__/spreadsheet/multiple-values-cell-spec.tsx index 820679ad24..9ba407f9a4 100644 --- a/packages/s2-core/__tests__/spreadsheet/multiple-values-cell-spec.tsx +++ b/packages/s2-core/__tests__/spreadsheet/multiple-values-cell-spec.tsx @@ -4,19 +4,20 @@ import { cloneDeep, merge } from 'lodash'; import React from 'react'; import ReactDOM from 'react-dom'; import { act } from 'react-dom/test-utils'; -import { - S2DataConfig, - S2Options, - SheetComponent, - SpreadSheet, - PivotSheet, -} from '../../src'; import { multipleDataWithBottom, multipleDataWithCombine, multipleDataWithNormal, } from '../data/multiple-values-cell-mock-data'; import { getContainer } from '../util/helpers'; +import { + S2DataConfig, + S2Options, + SheetComponent, + SpreadSheet, + PivotSheet, + Node, +} from '@/index'; let sheet: SpreadSheet; const getSpreadSheet = ( @@ -116,15 +117,14 @@ const getOptions = (): S2Options => { }, ], }, - rowActionIcons: { - iconTypes: ['SortDown', 'SortUp'], - display: { - level: 0, - operator: '>=', + headerActionIcons: [ + { + iconNames: ['SortDown', 'SortUp'], + belongsCell: 'colCell', + display: (meta: Node) => meta.level >= 0, + action() {}, }, - action(type, node) {}, - }, - + ], selectedCellsSpotlight: true, hoverHighlight: true, tooltip: { diff --git a/packages/s2-core/__tests__/spreadsheet/spread-sheet-spec.tsx b/packages/s2-core/__tests__/spreadsheet/spread-sheet-spec.tsx index d75b2b3d15..0049cc9dcb 100644 --- a/packages/s2-core/__tests__/spreadsheet/spread-sheet-spec.tsx +++ b/packages/s2-core/__tests__/spreadsheet/spread-sheet-spec.tsx @@ -6,7 +6,12 @@ import { getContainer } from '../util/helpers'; import { SheetEntry, assembleDataCfg } from '../util/sheet-entry'; // import * as tableData from '../data/mock-dataset.json'; import { CustomTooltip } from './custom/custom-tooltip'; -import { S2Options, SheetType, ThemeName } from '@/index'; +import { + HeaderActionIconProps, + S2Options, + SheetType, + ThemeName, +} from '@/index'; const tableDataFields = { fields: { @@ -24,9 +29,16 @@ function MainLayout() { const [hoverHighlight, setHoverHighlight] = React.useState(true); const [showSeriesNumber, setShowSeriesNumber] = React.useState(false); const [showPagination, setShowPagination] = React.useState(false); - const [showTooltip, setShowTooltip] = React.useState(true); + const [showDefaultActionIcons, setShowDefaultActionIcons] = + React.useState(true); const [themeName, setThemeName] = React.useState('default'); + const cornerTooltip =
cornerHeader
; + + const rowTooltip =
rowHeader
; + + const colTooltip =
colHeader
; + const onToggleRender = () => { setRender(!render); }; @@ -40,7 +52,7 @@ function MainLayout() { current: 1, }, tooltip: { - showTooltip: showTooltip, + showTooltip: true, renderTooltip: (spreadsheet) => { return new CustomTooltip(spreadsheet); }, @@ -51,6 +63,51 @@ function MainLayout() { showSeriesNumber: showSeriesNumber, selectedCellsSpotlight: spotLight, hoverHighlight: hoverHighlight, + customSVGIcons: !showDefaultActionIcons && [ + { + name: 'Filter', + svg: 'https://gw.alipayobjects.com/zos/antfincdn/gu1Fsz3fw0/filter%26sort_filter.svg', + }, + { + name: 'FilterAsc', + svg: 'https://gw.alipayobjects.com/zos/antfincdn/UxDm6TCYP3/filter%26sort_asc%2Bfilter.svg', + }, + ], + headerActionIcons: !showDefaultActionIcons && [ + { + iconNames: ['Filter'], + belongsCell: 'colCell', + action: (props: HeaderActionIconProps) => { + const { meta, event } = props; + meta.spreadsheet.tooltip.show({ + position: { x: event.clientX, y: event.clientY }, + element: colTooltip, + }); + }, + }, + { + iconNames: ['FilterAsc'], + belongsCell: 'cornerCell', + action: (props: HeaderActionIconProps) => { + const { meta, event } = props; + meta.spreadsheet.tooltip.show({ + position: { x: event.clientX, y: event.clientY }, + element: cornerTooltip, + }); + }, + }, + { + iconNames: ['SortDown', 'Filter'], + belongsCell: 'rowCell', + action: (props: HeaderActionIconProps) => { + const { meta, event } = props; + meta.spreadsheet.tooltip.show({ + position: { x: event.clientX, y: event.clientY }, + element: rowTooltip, + }); + }, + }, + ], }; const onSheetTypeChange = (checked) => { @@ -116,10 +173,10 @@ function MainLayout() { /> extends Group { this.meta = viewMeta; } + public getIconStyle() { + return this.theme[this.cellType].icon; + } + + public getTextAndIconPosition() { + const textStyle = this.getTextStyle(); + const iconCfg = this.getIconStyle(); + return getTextAndFollowingIconPosition( + this.getContentArea(), + textStyle, + this.actualTextWidth, + iconCfg, + ); + } + /** * in case there are more params to be handled * @param options any type's rest params @@ -119,6 +137,10 @@ export abstract class BaseCell extends Group { return getContentArea(this.getCellArea(), padding); } + protected getIconPosition() { + return this.getTextAndIconPosition().icon; + } + protected drawTextShape() { const { formattedValue } = this.getFormattedFieldValue(); const maxTextWidth = this.getMaxTextWidth(); diff --git a/packages/s2-core/src/cell/col-cell.ts b/packages/s2-core/src/cell/col-cell.ts index 8fc339b9f2..31b7a765ac 100644 --- a/packages/s2-core/src/cell/col-cell.ts +++ b/packages/s2-core/src/cell/col-cell.ts @@ -1,12 +1,10 @@ import { Group, Point } from '@antv/g-canvas'; -import { get, isEqual } from 'lodash'; import { HeaderCell } from './header-cell'; import { CellTypes, KEY_GROUP_COL_RESIZE_AREA, HORIZONTAL_RESIZE_AREA_KEY_PRE, } from '@/common/constant'; -import { GuiIcon } from '@/common/icons'; import { FormatResult, TextAlign, @@ -135,56 +133,6 @@ export class ColCell extends HeaderCell { return { x: textX, y: textY }; } - private showSortIcon() { - const { sortParam } = this.headerConfig; - const query = this.meta.query; - return ( - isEqual(get(sortParam, 'query'), query) && - get(sortParam, 'type') !== 'none' - ); - } - - private getActionIconsWidth() { - const { icon } = this.getStyle(); - return this.showSortIcon() ? icon.size + icon.margin.left : 0; - } - - protected getActionIconPosition(): Point { - const { textBaseline } = this.getTextStyle(); - const { size } = this.getStyle().icon; - const { x, width } = this.getContentArea(); - - const iconX = x + width - size; - const iconY = getVerticalPosition( - this.getContentArea(), - textBaseline, - size, - ); - - return { x: iconX, y: iconY }; - } - - // 绘制排序icon - private drawActionIcons() { - const { icon } = this.getStyle(); - if (this.showSortIcon()) { - const { sortParam } = this.headerConfig; - const position = this.getActionIconPosition(); - const sortIcon = new GuiIcon({ - type: get(sortParam, 'type', 'none'), - ...position, - width: icon.size, - height: icon.size, - }); - // TODO:和row-cell统一icon之后需更改 - sortIcon.on('click', (event) => { - this.handleGroupSort(event, this.meta); - }); - this.add(sortIcon); - this.actionIcons.push(sortIcon); - } - } - protected getColResizeAreaKey() { return this.meta.key; } diff --git a/packages/s2-core/src/cell/corner-cell.ts b/packages/s2-core/src/cell/corner-cell.ts index 389a0d2d82..8c6efc1b22 100644 --- a/packages/s2-core/src/cell/corner-cell.ts +++ b/packages/s2-core/src/cell/corner-cell.ts @@ -1,5 +1,5 @@ import { Group, IShape, Point, ShapeAttrs } from '@antv/g-canvas'; -import { isEmpty, isEqual } from 'lodash'; +import { isEmpty, isEqual, max } from 'lodash'; import { HeaderCell } from './header-cell'; import { CellTypes, @@ -18,7 +18,7 @@ import { renderTreeIcon, } from '@/utils/g-renders'; import { isIPhoneX } from '@/utils/is-mobile'; -import { getEllipsisText } from '@/utils/text'; +import { getEllipsisText, measureTextWidth } from '@/utils/text'; export class CornerCell extends HeaderCell { protected headerConfig: CornerHeaderConfig; @@ -35,10 +35,12 @@ export class CornerCell extends HeaderCell { public update() {} protected initCell() { + super.initCell(); this.textShapes = []; this.drawBackgroundShape(); this.drawTreeIcon(); this.drawCellText(); + this.drawActionIcons(); this.drawBorderShape(); this.drawResizeArea(); } @@ -110,6 +112,11 @@ export class CornerCell extends HeaderCell { ), ); } + + this.actualTextWidth = max([ + measureTextWidth(firstLine, textStyle), + measureTextWidth(secondLine, textStyle), + ]); } /** @@ -230,6 +237,20 @@ export class CornerCell extends HeaderCell { ); } + protected getIconPosition(): Point { + const textCfg = this.textShapes?.[0]?.cfg.attrs; + const { textBaseline } = this.getTextStyle(); + const { size, margin } = this.getStyle().icon; + const iconX = textCfg?.x + this.actualTextWidth + margin.left; + const iconY = getVerticalPosition( + this.getContentArea(), + textBaseline, + size, + ); + + return { x: iconX, y: iconY }; + } + private getTreeIconWidth() { const { size, margin } = this.getStyle().icon; return this.showTreeIcon() ? size + margin.right : 0; @@ -250,7 +271,7 @@ export class CornerCell extends HeaderCell { protected getMaxTextWidth(): number { const { width } = this.getCellArea(); - return width - this.getTreeIconWidth(); + return width - this.getTreeIconWidth() - this.getActionIconsWidth(); } protected getTextPosition(): Point { diff --git a/packages/s2-core/src/cell/data-cell.ts b/packages/s2-core/src/cell/data-cell.ts index 562eb2f378..dafb5e2bae 100644 --- a/packages/s2-core/src/cell/data-cell.ts +++ b/packages/s2-core/src/cell/data-cell.ts @@ -22,10 +22,7 @@ import { ViewMeta, ViewMetaIndexType, } from '@/common/interface'; -import { - getMaxTextWidth, - getTextAndFollowingIconPosition, -} from '@/utils/cell/cell'; +import { getMaxTextWidth } from '@/utils/cell/cell'; import { includeCell } from '@/utils/cell/data-cell'; import { getIconPositionCfg } from '@/utils/condition/condition'; import { @@ -175,7 +172,7 @@ export class DataCell extends BaseCell { return { ...textStyle, fill }; } - protected getIconStyle(): IconCfg | undefined { + public getIconStyle(): IconCfg | undefined { const { size, margin } = this.theme.dataCell.icon; const iconCondition: IconCondition = this.findFieldCondition( this.conditions?.icon, @@ -215,25 +212,10 @@ export class DataCell extends BaseCell { return getMaxTextWidth(width, this.getIconStyle()); } - private getTextAndIconPosition() { - const textStyle = this.getTextStyle(); - const iconCfg = this.getIconStyle(); - return getTextAndFollowingIconPosition( - this.getContentArea(), - textStyle, - this.actualTextWidth, - iconCfg, - ); - } - protected getTextPosition(): Point { return this.getTextAndIconPosition().text; } - protected getIconPosition() { - return this.getTextAndIconPosition().icon; - } - protected drawConditionIconShapes() { const iconCondition: IconCondition = this.findFieldCondition( this.conditions?.icon, @@ -246,7 +228,7 @@ export class DataCell extends BaseCell { if (!isEmpty(attrs?.icon) && formattedValue) { this.conditionIconShape = renderIcon(this, { ...position, - type: attrs.icon, + name: attrs.icon, width: size, height: size, fill: attrs.fill, diff --git a/packages/s2-core/src/cell/header-cell.ts b/packages/s2-core/src/cell/header-cell.ts index 4a565c3333..e7d90dcae9 100644 --- a/packages/s2-core/src/cell/header-cell.ts +++ b/packages/s2-core/src/cell/header-cell.ts @@ -1,18 +1,30 @@ -import { first, map, includes, find, isEqual, get, last } from 'lodash'; +import { Event } from '@antv/g-canvas'; +import { + first, + map, + includes, + find, + isEqual, + get, + isEmpty, + forEach, + filter, +} from 'lodash'; import { BaseCell } from '@/cell/base-cell'; import { InteractionStateName } from '@/common/constant/interaction'; import { GuiIcon } from '@/common/icons'; -import { CellMeta } from '@/common/interface'; +import { + S2CellType, + HeaderActionIcon, + HeaderActionIconProps, + CellMeta, +} from '@/common/interface'; import { BaseHeaderConfig } from '@/facet/header/base'; import { Node } from '@/facet/layout/node'; import { includeCell } from '@/utils/cell/data-cell'; -import { - EXTRA_FIELD, - InterceptType, - TOOLTIP_OPERATOR_MENUS, -} from '@/common/constant'; +import { EXTRA_FIELD, S2Event } from '@/common/constant'; import { getSortTypeIcon } from '@/utils/sort-action'; -import { TooltipOperatorOptions, SortParam } from '@/common/interface'; +import { SortParam } from '@/common/interface'; export abstract class HeaderCell extends BaseCell { protected headerConfig: BaseHeaderConfig; @@ -31,6 +43,7 @@ export abstract class HeaderCell extends BaseCell { ? item?.sortByMeasure === value && isEqual(get(item, 'query'), query) : isEqual(get(item, 'query'), query), ); + const type = getSortTypeIcon(sortParam, isValueCell); this.headerConfig.sortParam = { ...this.headerConfig.sortParam, @@ -43,57 +56,158 @@ export abstract class HeaderCell extends BaseCell { this.actionIcons = []; } - protected isValueCell() { - return this.meta.key === EXTRA_FIELD; + protected getActionIconCfg() { + // 每种 header cell 类型只取第一个配置 + return filter( + this.spreadsheet.options.headerActionIcons, + (headerActionIcon: HeaderActionIcon) => + headerActionIcon?.belongsCell === this.cellType, + )[0]; } - protected handleGroupSort(event, meta) { - event.stopPropagation(); - this.spreadsheet.interaction.addIntercepts([InterceptType.HOVER]); - const operator: TooltipOperatorOptions = { - onClick: ({ key }) => { - const { rows, columns } = this.spreadsheet.dataCfg.fields; - const sortFieldId = this.spreadsheet.isValueInCols() - ? last(rows) - : last(columns); - const { query, value } = meta; - const sortParam: SortParam = { - sortFieldId, - sortMethod: key as SortParam['sortMethod'], - sortByMeasure: value, - query, - }; - const prevSortParams = this.spreadsheet.dataCfg.sortParams.filter( - (item) => item?.sortFieldId !== sortFieldId, - ); - this.spreadsheet.setDataCfg({ - ...this.spreadsheet.dataCfg, - sortParams: [...prevSortParams, sortParam], - }); - this.spreadsheet.render(); - }, - menus: TOOLTIP_OPERATOR_MENUS.Sort, - }; + protected showActionIcons() { + const actionIcons = this.getActionIconCfg(); + + if (!actionIcons) return false; + const { iconNames, displayCondition } = actionIcons; + if (isEmpty(iconNames)) return false; + // 没有展示条件参数默认全展示 + if (!displayCondition) return true; + return displayCondition(this.meta); + } + + private showSortIcon() { + if (isEmpty(this.spreadsheet.options.headerActionIcons)) { + const { sortParam } = this.headerConfig; + const query = this.meta.query; + return ( + isEqual(get(sortParam, 'query'), query) && + get(sortParam, 'type') !== 'none' + ); + } + return false; + } + + protected getActionIconsWidth() { + if (this.showSortIcon()) { + const { icon } = this.getStyle(); + return this.showSortIcon() ? icon.size + icon.margin.left : 0; + } + + if (this.showActionIcons()) { + const iconNames = this.getActionIconCfg()?.iconNames; + const { size, margin } = this.getStyle().icon; + return ( + size * iconNames.length + + margin.left + + margin.right * (iconNames.length - 1) + ); + } + } + + // 绘制排序icon + protected drawSortIcons() { + const { icon, text } = this.getStyle(); + if (this.showSortIcon()) { + const { sortParam } = this.headerConfig; + const position = this.getIconPosition(); + const sortIcon = new GuiIcon({ + name: get(sortParam, 'type', 'none'), + ...position, + width: icon.size, + height: icon.size, + fill: text.fill, + }); + sortIcon.on('click', (event) => { + this.spreadsheet.handleGroupSort(event, this.meta); + }); + this.add(sortIcon); + this.actionIcons.push(sortIcon); + } + } - this.spreadsheet.showTooltipWithInfo(event, [], { - operator, - onlyMenu: true, + protected addActionIcon( + iconName: string, + x: number, + y: number, + size: number, + action: (prop: HeaderActionIconProps) => void, + defaultHide?: boolean, + ) { + const { text } = this.getStyle(); + const icon = new GuiIcon({ + name: iconName, + x, + y, + width: size, + height: size, + fill: text.fill, }); + icon.set('visible', !defaultHide); + icon.on('mouseover', (event: Event) => { + this.spreadsheet.emit(S2Event.GLOBAL_ACTION_ICON_HOVER, event); + }); + icon.on('click', (event: Event) => { + this.spreadsheet.emit(S2Event.GLOBAL_ACTION_ICON_CLICK, event); + action({ + iconName: iconName, + meta: this.meta, + event: event, + }); + }); + + this.actionIcons.push(icon); + this.add(icon); + } + + protected drawActionIcons() { + this.drawSortIcons(); + const actionIcons = this.getActionIconCfg(); + + if (!actionIcons) return; + const { iconNames, action, defaultHide } = actionIcons; + + if (this.showActionIcons()) { + const position = this.getIconPosition(); + const { size, margin } = this.getStyle().icon; + for (let i = 0; i < iconNames.length; i++) { + const x = position.x + i * size + i * margin.left; + this.addActionIcon( + iconNames[i], + x, + position.y, + size, + action, + defaultHide, + ); + } + } + } + + protected isValueCell() { + return this.meta.key === EXTRA_FIELD; } private handleHover(cells: CellMeta[]) { if (includeCell(cells, this)) { - this.updateByState(InteractionStateName.HOVER, this); + this.updateByState(InteractionStateName.HOVER); + this.toggleActionIcon(true); } } private handleSelect(cells: CellMeta[], nodes: Node[]) { if (includeCell(cells, this)) { - this.updateByState(InteractionStateName.SELECTED, this); + this.updateByState(InteractionStateName.SELECTED); } const selectedNodeIds = map(nodes, 'id'); if (includes(selectedNodeIds, this.meta.id)) { - this.updateByState(InteractionStateName.SELECTED, this); + this.updateByState(InteractionStateName.SELECTED); + } + } + + public toggleActionIcon(visible: boolean) { + if (this.getActionIconCfg()?.defaultHide) { + forEach(this.actionIcons, (icon) => icon.set('visible', visible)); } } @@ -116,4 +230,12 @@ export abstract class HeaderCell extends BaseCell { break; } } + + updateByState(stateName: InteractionStateName) { + super.updateByState(stateName, this); + } + + public hideInteractionShape() { + super.hideInteractionShape(); + } } diff --git a/packages/s2-core/src/cell/row-cell.ts b/packages/s2-core/src/cell/row-cell.ts index a50eda9e9b..a96a56e911 100644 --- a/packages/s2-core/src/cell/row-cell.ts +++ b/packages/s2-core/src/cell/row-cell.ts @@ -1,15 +1,11 @@ -import { Event, Group, Point } from '@antv/g-canvas'; +import { Group, Point } from '@antv/g-canvas'; import { GM } from '@antv/g-gesture'; -import { each, forEach } from 'lodash'; import { HeaderCell } from './header-cell'; import { CellTypes, - ID_SEPARATOR, KEY_GROUP_ROW_RESIZE_AREA, S2Event, } from '@/common/constant'; -import { InteractionStateName } from '@/common/constant/interaction'; -import { GuiIcon } from '@/common/icons'; import { FormatResult, TextTheme } from '@/common/interface'; import { ResizeInfo } from '@/facet/header/interface'; import { RowHeaderConfig } from '@/facet/header/row'; @@ -42,8 +38,6 @@ export class RowCell extends HeaderCell { this.drawTreeIcon(); // draw text this.drawTextShape(); - // draw action icon shapes: trend icon, drill-down icon ... - this.drawActionIcons(); // draw bottom border this.drawRectBorder(); @@ -239,70 +233,6 @@ export class RowCell extends HeaderCell { } } - protected drawActionIcons() { - const rowActionIcons = this.spreadsheet.options.rowActionIcons; - - if (!rowActionIcons) return; - const { iconTypes, display, action, customDisplayByRowName } = - rowActionIcons; - if (customDisplayByRowName) { - const { rowNames, mode } = customDisplayByRowName; - const rowIds = rowNames.map((rowName) => `root${ID_SEPARATOR}${rowName}`); - - if ( - (mode === 'omit' && rowIds.includes(this.meta.id)) || - (mode === 'pick' && !rowIds.includes(this.meta.id)) - ) - return; - } - - const showIcon = () => { - const level = this.meta.level; - const rowLevel = display?.level; - switch (display?.operator) { - case '<': - return level < rowLevel; - case '<=': - return level <= rowLevel; - case '=': - return level === rowLevel; - case '>': - return level > rowLevel; - case '>=': - return level >= rowLevel; - default: - break; - } - }; - - if ( - showIcon() && - this.spreadsheet.isHierarchyTreeType() && - this.spreadsheet.isPivotMode() - ) { - const { x, width } = this.getContentArea(); - const { size } = this.getStyle().icon; - - for (let i = 0; i < iconTypes.length; i++) { - const iconRight = size * (iconTypes.length - i); - const icon = new GuiIcon({ - type: iconTypes[i], - x: x + width - iconRight, - y: this.getIconYPosition(), - width: size, - height: size, - }); - icon.set('visible', false); - icon.on('click', (e: Event) => { - action(iconTypes[i], this.meta, e); - }); - - this.actionIcons.push(icon); - this.add(icon); - } - } - } - protected getContentIndent() { if (!this.spreadsheet.isHierarchyTreeType()) { return 0; @@ -356,9 +286,17 @@ export class RowCell extends HeaderCell { }; } + protected getIconPosition() { + const textCfg = this.textShape.cfg.attrs; + return { + x: textCfg.x + this.actualTextWidth + this.getStyle().icon.margin.left, + y: textCfg.y, + }; + } + protected getMaxTextWidth(): number { const { width } = this.getContentArea(); - return width - this.getTextIndent(); + return width - this.getTextIndent() - this.getActionIconsWidth(); } protected getTextPosition(): Point { @@ -380,16 +318,4 @@ export class RowCell extends HeaderCell { const { fontSize } = this.getTextStyle(); return textY + (fontSize - size) / 2; } - - updateByState(stateName: InteractionStateName) { - super.updateByState(stateName, this); - each(this.actionIcons, (icon) => - icon.set('visible', stateName === InteractionStateName.HOVER), - ); - } - - public hideInteractionShape() { - super.hideInteractionShape(); - forEach(this.actionIcons, (icon) => icon.set('visible', false)); - } } diff --git a/packages/s2-core/src/cell/table-col-cell.ts b/packages/s2-core/src/cell/table-col-cell.ts index 82db5cab3d..274d79caf7 100644 --- a/packages/s2-core/src/cell/table-col-cell.ts +++ b/packages/s2-core/src/cell/table-col-cell.ts @@ -110,13 +110,15 @@ export class TableColCell extends ColCell { { cursor: 'pointer' }, ); - renderDetailTypeSortIcon( - this, - spreadsheet, - x + cellWidth - iconSize - iconMarginRight, - textY, - key, - ); + if (!spreadsheet.options.headerActionIcons) { + renderDetailTypeSortIcon( + this, + spreadsheet, + x + cellWidth - iconSize - iconMarginRight, + textY, + key, + ); + } } protected initCell() { @@ -184,7 +186,7 @@ export class TableColCell extends ColCell { const iconConfig = this.getExpandColumnIconConfig(); const icon = renderIcon(this, { ...iconConfig, - type: 'ExpandColIcon', + name: 'ExpandColIcon', cursor: 'pointer', }); icon.on('click', () => { diff --git a/packages/s2-core/src/common/constant/events/basic.ts b/packages/s2-core/src/common/constant/events/basic.ts index f4d47e392b..0c98c8f218 100644 --- a/packages/s2-core/src/common/constant/events/basic.ts +++ b/packages/s2-core/src/common/constant/events/basic.ts @@ -76,6 +76,9 @@ export enum S2Event { /** ================ Global Mouse ================ */ GLOBAL_MOUSE_UP = 'global:mouse-up', + /** ================ Global Action Icon ================ */ + GLOBAL_ACTION_ICON_CLICK = 'global:action-icon-click', + GLOBAL_ACTION_ICON_HOVER = 'global:action-icon-hover', /** ================ Global Context Menu ================ */ GLOBAL_CONTEXT_MENU = 'global:context-menu', } diff --git a/packages/s2-core/src/common/icons/factory.ts b/packages/s2-core/src/common/icons/factory.ts index 1d925a1491..4de795a289 100644 --- a/packages/s2-core/src/common/icons/factory.ts +++ b/packages/s2-core/src/common/icons/factory.ts @@ -3,10 +3,10 @@ import { lowerCase } from 'lodash'; // 所有的 Icon 缓存 const SVGMap: Record = {}; -export const registerIcon = (type: string, svg: string) => { - SVGMap[lowerCase(type)] = svg; +export const registerIcon = (name: string, svg: string) => { + SVGMap[lowerCase(name)] = svg; }; -export const getIcon = (type: string): string => { - return SVGMap[lowerCase(type)]; +export const getIcon = (name: string): string => { + return SVGMap[lowerCase(name)]; }; diff --git a/packages/s2-core/src/common/icons/gui-icon.ts b/packages/s2-core/src/common/icons/gui-icon.ts index 807deb16f6..c52336078f 100644 --- a/packages/s2-core/src/common/icons/gui-icon.ts +++ b/packages/s2-core/src/common/icons/gui-icon.ts @@ -10,7 +10,7 @@ const STYLE_PLACEHOLDER = ' = {}; export interface GuiIconCfg extends ShapeAttrs { - readonly type: string; + readonly name: string; } /** @@ -28,7 +28,7 @@ export class GuiIcon extends Group { // 获取 Image 实例,使用缓存,以避免滚动时因重复的 new Image() 耗时导致的闪烁问题 /* 异步获取 image 实例 */ private getImage( - type: string, + name: string, cacheKey: string, fill?: string, ): Promise { @@ -43,7 +43,7 @@ export class GuiIcon extends Group { img.onerror = (e) => { reject(e); }; - let svg = getIcon(type); + let svg = getIcon(name); // 兼容三种情况 // 1、base 64 @@ -82,27 +82,27 @@ export class GuiIcon extends Group { }; private render() { - const { type, fill } = this.cfg; + const { name, fill } = this.cfg; const attrs = clone(this.cfg); const image = new Shape.Image({ attrs: omit(attrs, 'fill'), }); - const cacheKey = `${type}-${fill}`; + const cacheKey = `${name}-${fill}`; const img = ImageCache[cacheKey]; if (img) { // already in cache image.attr('img', img); this.addShape('image', image); } else { - this.getImage(type, cacheKey, fill) + this.getImage(name, cacheKey, fill) .then((value: HTMLImageElement) => { image.attr('img', value); this.addShape('image', image); }) .catch((err: Event) => { // eslint-disable-next-line no-console - console.warn(`GuiIcon ${type} load error`, err); + console.warn(`GuiIcon ${name} load error`, err); }); } this.image = image; diff --git a/packages/s2-core/src/common/icons/html-icon.tsx b/packages/s2-core/src/common/icons/html-icon.tsx index ff97e29c34..5d6bffcdc9 100644 --- a/packages/s2-core/src/common/icons/html-icon.tsx +++ b/packages/s2-core/src/common/icons/html-icon.tsx @@ -5,7 +5,7 @@ import { getIcon } from './factory'; import './html-icon.less'; interface Props { - type: string; // 'globalAsc' | 'globalDesc' | 'groupAsc' | 'groupDesc' | 'none'; + name: string; // 'globalAsc' | 'globalDesc' | 'groupAsc' | 'groupDesc' | 'none'; style?: any; width?: number; height?: number; @@ -13,8 +13,8 @@ interface Props { } export class HtmlIcon extends React.PureComponent { render() { - const { style = {}, width, height, className, type } = this.props; - const svgIcon = () => getIcon(type); + const { style = {}, width, height, className, name } = this.props; + const svgIcon = () => getIcon(name); // fix: Uncaught TypeError: Cannot assign to read only property 'width' of object '#' const newStyle = { ...style }; diff --git a/packages/s2-core/src/common/interface/basic.ts b/packages/s2-core/src/common/interface/basic.ts index 07aeceacb8..4900becdbe 100644 --- a/packages/s2-core/src/common/interface/basic.ts +++ b/packages/s2-core/src/common/interface/basic.ts @@ -8,6 +8,7 @@ import { S2PartialOptions } from '@/common/interface/s2Options'; import { BaseDataSet } from '@/data-set'; import { Frame } from '@/facet/header'; import { + CellTypes, FrameConfig, Hierarchy, Node, @@ -159,22 +160,35 @@ export interface NodeField { colField?: string[]; } -export interface RowActionIcons { - iconTypes: string[]; - // 需要展示的层级(行头) - display: { - level: number; // 层级 - operator: '>' | '=' | '<' | '>=' | '<='; // 层级关系 - }; - // 根据行头名自定义展示 - customDisplayByRowName?: { - // Row headers, using the ID_SEPARATOR('[&]') to join two labels when there are hierarchical relations between them. - rowNames: string[]; - // 指定行头名称是否展示icon - mode: 'pick' | 'omit'; - }; - // 具体的动作 - action: (iconType: string, meta: Node, event: Event) => void; +export interface CustomSVGIcon { + // icon 类型名 + name: string; + // 1、base 64 + // 2、svg本地文件(兼容老方式,可以改颜色) + // 3、线上支持的图片地址 TODO 🤔 是否存在安全问题 + svg: string; +} + +export interface HeaderActionIconProps { + iconName: string; + meta: Node; + event: Event; +} + +export interface HeaderActionIcon { + // 已注册的 icon 类型或自定义的 icon 类型名 + iconNames: string[]; + + // 所属的 cell 类型 + belongsCell: Omit; + // 是否默认隐藏, true 为 hover后显示, false 为一直显示 + defaultHide?: boolean; + + // 需要展示的层级(行头/列头) 如果没有改配置则默认全部打开 + displayCondition?: (mete: Node) => void; + + // 点击后的执行函数 + action: (headerActionIconProps: HeaderActionIconProps) => void; } // Hook 渲染和布局相关的函数类型定义 diff --git a/packages/s2-core/src/common/interface/emitter.ts b/packages/s2-core/src/common/interface/emitter.ts index 32cd701d65..2a71a9c2b5 100644 --- a/packages/s2-core/src/common/interface/emitter.ts +++ b/packages/s2-core/src/common/interface/emitter.ts @@ -39,6 +39,8 @@ type ResizeHandler = (style: Style) => void; export interface EmitterType { /** ================ Global ================ */ + [S2Event.GLOBAL_ACTION_ICON_CLICK]: CanvasEventHandler; + [S2Event.GLOBAL_ACTION_ICON_HOVER]: CanvasEventHandler; [S2Event.GLOBAL_COPIED]: (data: string) => void; [S2Event.GLOBAL_KEYBOARD_DOWN]: KeyboardEventHandler; [S2Event.GLOBAL_KEYBOARD_UP]: KeyboardEventHandler; diff --git a/packages/s2-core/src/common/interface/s2Options.ts b/packages/s2-core/src/common/interface/s2Options.ts index dad8709c1d..d647cafa82 100644 --- a/packages/s2-core/src/common/interface/s2Options.ts +++ b/packages/s2-core/src/common/interface/s2Options.ts @@ -1,7 +1,11 @@ import { merge } from 'lodash'; import { CustomInteraction } from './interaction'; import { Conditions } from './condition'; -import { FilterDataItemCallback } from './basic'; +import { + FilterDataItemCallback, + HeaderActionIcon, + CustomSVGIcon, +} from './basic'; import { Tooltip } from './tooltip'; import { CellCallback, @@ -13,7 +17,6 @@ import { MergedCellInfo, NodeField, Pagination, - RowActionIcons, Style, Totals, } from '@/common/interface/basic'; @@ -55,8 +58,10 @@ export interface S2PartialOptions { readonly scrollReachNodeField?: NodeField; // custom config of showing columns and rows readonly customHeaderCells?: CustomHeaderCells; - // row header action icon's config - readonly rowActionIcons?: RowActionIcons; + // header cells including ColCell, RowCell, CornerCell action icon's config + readonly headerActionIcons?: HeaderActionIcon[]; + // register custom svg icons + readonly customSVGIcons?: CustomSVGIcon[]; // extra styles readonly style?: Partial