diff --git a/common/changes/@visactor/vtable/feat-fs-bs_2024-10-17-09-01.json b/common/changes/@visactor/vtable/feat-fs-bs_2024-10-17-09-01.json new file mode 100644 index 000000000..30ab08dc0 --- /dev/null +++ b/common/changes/@visactor/vtable/feat-fs-bs_2024-10-17-09-01.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vtable", + "comment": "feat: add InvertHighlightPlugin ", + "type": "none" + } + ], + "packageName": "@visactor/vtable" +} \ No newline at end of file diff --git a/common/changes/@visactor/vtable/feat-fs-bs_2024-10-17-09-02.json b/common/changes/@visactor/vtable/feat-fs-bs_2024-10-17-09-02.json new file mode 100644 index 000000000..71ced78d6 --- /dev/null +++ b/common/changes/@visactor/vtable/feat-fs-bs_2024-10-17-09-02.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vtable", + "comment": "fix: add CarouselAnimationPlugin", + "type": "none" + } + ], + "packageName": "@visactor/vtable" +} \ No newline at end of file diff --git a/packages/vtable/examples/interactive/highlight-color.ts b/packages/vtable/examples/interactive/highlight-color.ts new file mode 100644 index 000000000..421cb1059 --- /dev/null +++ b/packages/vtable/examples/interactive/highlight-color.ts @@ -0,0 +1,82 @@ +import * as VTable from '../../src'; +import { bindDebugTool } from '../../src/scenegraph/debug-tool'; +const CONTAINER_ID = 'vTable'; +const generatePersons = count => { + return Array.from(new Array(count)).map((_, i) => ({ + id: i + 1, + email1: `${i + 1}@xxx.com`, + name: `小明${i + 1}`, + lastName: '王', + date1: '2022年9月1日', + tel: '000-0000-0000', + sex: i % 2 === 0 ? 'boy' : 'girl', + work: i % 2 === 0 ? 'back-end engineer' + (i + 1) : 'front-end engineer' + (i + 1), + city: 'beijing', + image: + '' + })); +}; + +export function createTable() { + const records = generatePersons(20); + const columns: VTable.ColumnsDefine = [ + { + field: 'id', + title: 'ID', + width: 'auto', + minWidth: 50, + sort: true + }, + { + field: 'email1', + title: 'email', + width: 200, + sort: true, + style: { + underline: true, + underlineDash: [2, 0], + underlineOffset: 3 + } + }, + { + title: 'full name', + columns: [ + { + field: 'name', + title: 'First Name', + width: 200 + }, + { + field: 'name', + title: 'Last Name', + width: 200 + } + ] + }, + { + field: 'date1', + title: 'birthday', + width: 200 + }, + { + field: 'sex', + title: 'sex', + width: 100 + } + ]; + const option: VTable.ListTableConstructorOptions = { + container: document.getElementById(CONTAINER_ID), + records, + columns, + rowSeriesNumber: {} + }; + const tableInstance = new VTable.ListTable(option); + window.tableInstance = tableInstance; + + bindDebugTool(tableInstance.scenegraph.stage, { + customGrapicKeys: ['col', 'row'] + }); + + const highlightPlugin = new VTable.HeaderHighlightPlugin(tableInstance); + window.highlightPlugin = highlightPlugin; +} diff --git a/packages/vtable/examples/interactive/mask.ts b/packages/vtable/examples/interactive/mask.ts new file mode 100644 index 000000000..49eba6c4c --- /dev/null +++ b/packages/vtable/examples/interactive/mask.ts @@ -0,0 +1,132 @@ +import * as VTable from '../../src'; +import { bindDebugTool } from '../../src/scenegraph/debug-tool'; +const CONTAINER_ID = 'vTable'; +const generatePersons = count => { + return Array.from(new Array(count)).map((_, i) => ({ + id: i + 1, + email1: `${i + 1}@xxx.com`, + name: `小明${i + 1}`, + lastName: '王', + date1: '2022年9月1日', + tel: '000-0000-0000', + sex: i % 2 === 0 ? 'boy' : 'girl', + work: i % 2 === 0 ? 'back-end engineer' + (i + 1) : 'front-end engineer' + (i + 1), + city: 'beijing', + image: + '' + })); +}; + +export function createTable() { + const records = generatePersons(20); + const columns: VTable.ColumnsDefine = [ + { + field: 'image', + title: '行号', + width: 80, + cellType: 'image', + keepAspectRatio: true + }, + { + field: 'id', + title: 'ID', + width: 'auto', + minWidth: 50, + sort: true + }, + { + field: 'email1', + title: 'email', + width: 200, + sort: true, + style: { + underline: true, + underlineDash: [2, 0], + underlineOffset: 3 + } + }, + { + title: 'full name', + columns: [ + { + field: 'name', + title: 'First Name', + width: 200 + }, + { + field: 'name', + title: 'Last Name', + width: 200 + } + ] + }, + { + field: 'date1', + title: 'birthday', + width: 200 + }, + { + field: 'sex', + title: 'sex', + width: 100 + } + ]; + const option: VTable.ListTableConstructorOptions = { + container: document.getElementById(CONTAINER_ID), + records, + columns, + theme: VTable.themes.DARK, + // heightMode: 'adaptive', + select: { + disableSelect: true + } + }; + const tableInstance = new VTable.ListTable(option); + window.tableInstance = tableInstance; + + bindDebugTool(tableInstance.scenegraph.stage, { + customGrapicKeys: ['col', 'row'] + }); + + const highlightPlugin = new VTable.InvertHighlightPlugin(tableInstance); + // highlightPlugin.setInvertHighlightRange({ + // start: { + // col: 0, + // row: 6 + // }, + // end: { + // col: 6, + // row: 6 + // } + // }); + + tableInstance.on('click_cell', event => { + const { col, row } = event; + if (tableInstance.isHeader(col, row)) { + highlightPlugin.setInvertHighlightRange(undefined); + } else { + highlightPlugin.setInvertHighlightRange({ + start: { + col: 0, + row + }, + end: { + col: tableInstance.colCount - 1, + row + } + }); + } + }); + + const ca = new VTable.CarouselAnimationPlugin(tableInstance, { + rowCount: 2, + replaceScrollAction: true + }); + + // ca.play(); + + // setInterval(() => { + // row += 2; + // tableInstance.scrollToRow(row, { duration: 500 }); + // }, 2000); +} diff --git a/packages/vtable/examples/menu.ts b/packages/vtable/examples/menu.ts index 53f95f780..bb1769086 100644 --- a/packages/vtable/examples/menu.ts +++ b/packages/vtable/examples/menu.ts @@ -641,6 +641,14 @@ export const menus = [ { path: 'interactive', name: 'pre-sort' + }, + { + path: 'interactive', + name: 'mask' + }, + { + path: 'interactive', + name: 'highlight-color' } ] }, diff --git a/packages/vtable/src/index.ts b/packages/vtable/src/index.ts index 0ce1ec896..982870f04 100644 --- a/packages/vtable/src/index.ts +++ b/packages/vtable/src/index.ts @@ -129,3 +129,7 @@ export * from './scenegraph/group-creater/cell-type'; export { TABLE_EVENT_TYPE } from './core/TABLE_EVENT_TYPE'; export { PIVOT_CHART_EVENT_TYPE, PIVOT_TABLE_EVENT_TYPE } from './ts-types/pivot-table/PIVOT_TABLE_EVENT_TYPE'; + +export * from './plugins/invert-highlight'; +export * from './plugins/carousel-animation'; +export * from './plugins/header-highlight'; diff --git a/packages/vtable/src/plugins/carousel-animation.ts b/packages/vtable/src/plugins/carousel-animation.ts new file mode 100644 index 000000000..35fff969d --- /dev/null +++ b/packages/vtable/src/plugins/carousel-animation.ts @@ -0,0 +1,147 @@ +import type { EasingType } from '@src/vrender'; +import type { BaseTableAPI } from '../ts-types/base-table'; + +export interface ICarouselAnimationPluginOptions { + rowCount?: number; + colCount?: number; + animationDuration?: number; + animationDelay?: number; + animationEasing?: EasingType; + replaceScrollAction?: boolean; +} + +export class CarouselAnimationPlugin { + table: BaseTableAPI; + + rowCount: number; + colCount: number; + animationDuration: number; + animationDelay: number; + animationEasing: EasingType; + replaceScrollAction: boolean; + + playing: boolean; + row: number; + col: number; + willUpdateRow: boolean = false; + willUpdateCol: boolean = false; + constructor(table: BaseTableAPI, options?: ICarouselAnimationPluginOptions) { + this.table = table; + + this.rowCount = options?.rowCount ?? undefined; + this.colCount = options?.colCount ?? undefined; + this.animationDuration = options?.animationDuration ?? 500; + this.animationDelay = options?.animationDelay ?? 1000; + this.animationEasing = options?.animationEasing ?? 'linear'; + this.replaceScrollAction = options?.replaceScrollAction ?? false; + + this.reset(); + this.init(); + } + + init() { + if (this.replaceScrollAction) { + this.table.disableScroll(); + + this.table.scenegraph.stage.addEventListener('wheel', this.onScrollEnd.bind(this)); + } + } + + reset() { + this.playing = false; + this.row = this.table.frozenRowCount; + this.col = this.table.frozenColCount; + } + + onScrollEnd(e: Event) { + if (this.rowCount) { + if ((e as any).deltaY > 0) { + this.row += this.rowCount; + this.row = Math.min(this.row, this.table.rowCount - this.table.frozenRowCount); + } else if ((e as any).deltaY < 0) { + this.row -= this.rowCount; + this.row = Math.max(this.row, this.table.frozenRowCount); + } + this.table.scrollToRow(this.row, { duration: this.animationDuration, easing: this.animationEasing }); + } else if (this.colCount) { + if ((e as any).deltaX > 0) { + this.col += this.colCount; + this.col = Math.min(this.col, this.table.colCount - this.table.frozenColCount); + } else if ((e as any).deltaX < 0) { + this.col -= this.colCount; + this.col = Math.max(this.col, this.table.frozenColCount); + } + this.table.scrollToCol(this.col, { duration: this.animationDuration, easing: this.animationEasing }); + } + } + + play() { + this.playing = true; + + if (this.rowCount && !this.willUpdateRow) { + this.updateRow(); + } else if (this.colCount && !this.willUpdateCol) { + this.updateCol(); + } + } + + pause() { + this.playing = false; + } + + updateRow() { + if (!this.playing) { + return; + } + + let animation = true; + if (this.table.scenegraph.proxy.screenTopRow !== this.row) { + this.row = this.table.frozenRowCount; + animation = false; + } else { + this.row += this.rowCount; + } + this.table.scrollToRow( + this.row, + animation ? { duration: this.animationDuration, easing: this.animationEasing } : undefined + ); + this.willUpdateRow = true; + setTimeout( + () => { + this.willUpdateRow = false; + this.updateRow(); + }, + // animation ? this.animationDuration + this.animationDelay : 0 + this.animationDuration + this.animationDelay + ); + } + + updateCol() { + if (!this.playing) { + return; + } + + let animation = true; + if (this.table.scenegraph.proxy.screenLeftCol !== this.col) { + this.col = this.table.frozenColCount; + animation = false; + } else { + this.col += this.colCount; + } + + this.table.scrollToCol( + this.col, + animation ? { duration: this.animationDuration, easing: this.animationEasing } : undefined + ); + + this.willUpdateCol = true; + setTimeout( + () => { + this.willUpdateCol = false; + this.updateCol(); + }, + // animation ? this.animationDuration + this.animationDelay : 0 + this.animationDuration + this.animationDelay + ); + } +} diff --git a/packages/vtable/src/plugins/custom-cell-style.ts b/packages/vtable/src/plugins/custom-cell-style.ts index 210204dc9..5174295c8 100644 --- a/packages/vtable/src/plugins/custom-cell-style.ts +++ b/packages/vtable/src/plugins/custom-cell-style.ts @@ -34,20 +34,28 @@ export class CustomCellStylePlugin { getCustomCellStyleId(col: number, row: number) { let customStyleId; - this.customCellStyleArrangement.forEach(style => { - if (style.cellPosition.range) { - if ( - style.cellPosition.range.start.col <= col && - style.cellPosition.range.end.col >= col && - style.cellPosition.range.start.row <= row && - style.cellPosition.range.end.row >= row - ) { - customStyleId = style.customStyleId; - } - } else if (style.cellPosition.col === col && style.cellPosition.row === row) { - customStyleId = style.customStyleId; + + const range = this.table.getCellRange(col, row); + for (let c = range.start.col; c <= range.end.col; c++) { + for (let r = range.start.row; r <= range.end.row; r++) { + // eslint-disable-next-line no-loop-func + this.customCellStyleArrangement.forEach(style => { + if (style.cellPosition.range) { + if ( + style.cellPosition.range.start.col <= c && + style.cellPosition.range.end.col >= c && + style.cellPosition.range.start.row <= r && + style.cellPosition.range.end.row >= r + ) { + customStyleId = style.customStyleId; + } + } else if (style.cellPosition.col === c && style.cellPosition.row === r) { + customStyleId = style.customStyleId; + } + }); } - }); + } + return customStyleId; } @@ -106,7 +114,11 @@ export class CustomCellStylePlugin { return style.cellPosition.col === cellPos.col && style.cellPosition.row === cellPos.row; }); - if (index === -1) { + if (index === -1 && !customStyleId) { + // do nothing + return; + } else if (index === -1 && customStyleId) { + // add new style this.customCellStyleArrangement.push({ cellPosition: { col: cellPos.col, @@ -116,16 +128,27 @@ export class CustomCellStylePlugin { customStyleId: customStyleId }); } else if (this.customCellStyleArrangement[index].customStyleId === customStyleId) { + // same style return; - } else { + } else if (customStyleId) { + // update style this.customCellStyleArrangement[index].customStyleId = customStyleId; + } else { + // delete useless style + this.customCellStyleArrangement.splice(index, 1); } // update cell group if (cellPos.range) { for (let col = cellPos.range.start.col; col <= cellPos.range.end.col; col++) { for (let row = cellPos.range.start.row; row <= cellPos.range.end.row; row++) { - this.table.scenegraph.updateCellContent(col, row); + const range = this.table.getCellRange(col, row); + for (let c = range.start.col; c <= range.end.col; c++) { + for (let r = range.start.row; r <= range.end.row; r++) { + this.table.scenegraph.updateCellContent(c, r); + } + } + // this.table.scenegraph.updateCellContent(col, row); } } } else { @@ -154,9 +177,9 @@ export function mergeStyle(cacheStyle: Style, customCellStyle: ColumnStyleOption cacheStyle = cacheStyle.clone(); for (const key in customCellStyle) { - const value = customCellStyle[key]; + const value = (customCellStyle as any)[key]; if (value) { - cacheStyle[`_${key}`] = value; + (cacheStyle as any)[`_${key}`] = value; } } diff --git a/packages/vtable/src/plugins/header-highlight.ts b/packages/vtable/src/plugins/header-highlight.ts new file mode 100644 index 000000000..371a7315f --- /dev/null +++ b/packages/vtable/src/plugins/header-highlight.ts @@ -0,0 +1,158 @@ +import type { CellRange } from '../ts-types'; +import type { BaseTableAPI } from '../ts-types/base-table'; + +export interface IHeaderHighlightPluginOptions { + rowHighlight?: boolean; + colHighlight?: boolean; + colHighlightBGColor?: string; + colHighlightColor?: string; + rowHighlightBGColor?: string; + rowHighlightColor?: string; +} + +export class HeaderHighlightPlugin { + table: BaseTableAPI; + options: IHeaderHighlightPluginOptions; + colHeaderRange?: CellRange; + rowHeaderRange?: CellRange; + constructor(table: BaseTableAPI, options?: IHeaderHighlightPluginOptions) { + this.table = table; + this.options = options; + + this.registerStyle(); + this.bindEvent(); + } + + registerStyle() { + this.table.registerCustomCellStyle('col-highlight', { + bgColor: this.options?.colHighlightBGColor ?? '#82b2f5', + color: this.options?.colHighlightColor ?? '#FFF' + }); + + this.table.registerCustomCellStyle('row-highlight', { + bgColor: this.options?.rowHighlightBGColor ?? '#82b2f5', + color: this.options?.rowHighlightColor ?? '#FFF' + }); + } + + bindEvent() { + this.table.on('selected_cell', e => { + this.updateHighlight(); + }); + + this.table.on('selected_clear', () => { + this.clearHighlight(); + }); + } + + clearHighlight() { + this.colHeaderRange && this.table.arrangeCustomCellStyle({ range: this.colHeaderRange }, undefined); + this.rowHeaderRange && this.table.arrangeCustomCellStyle({ range: this.rowHeaderRange }, undefined); + + // clear range + this.colHeaderRange = undefined; + this.rowHeaderRange = undefined; + } + + updateHighlight() { + if (this.options?.colHighlight === false && this.options?.rowHighlight === false) { + return; + } + const selectRanges = this.table.getSelectedCellRanges(); + if (selectRanges.length === 0) { + this.clearHighlight(); + return; + } + + const selectRange = selectRanges[0]; + const rowSelectRange = [selectRange.start.row, selectRange.end.row]; + rowSelectRange.sort((a, b) => a - b); // sort + const colSelectRange = [selectRange.start.col, selectRange.end.col]; + colSelectRange.sort((a, b) => a - b); // sort + + let colHeaderRange: CellRange; + let rowHeaderRange: CellRange; + if (this.table.isPivotTable()) { + colHeaderRange = { + start: { + col: colSelectRange[0], + row: 0 + }, + end: { + col: colSelectRange[1], + row: this.table.columnHeaderLevelCount - 1 + } + }; + rowHeaderRange = { + start: { + col: 0, + row: rowSelectRange[0] + }, + end: { + col: this.table.rowHeaderLevelCount - 1, + row: rowSelectRange[1] + } + }; + } else if (this.table.internalProps.transpose) { + rowHeaderRange = { + start: { + col: 0, + row: rowSelectRange[0] + }, + end: { + col: this.table.rowHeaderLevelCount - 1, + row: rowSelectRange[1] + } + }; + } else { + colHeaderRange = { + start: { + col: colSelectRange[0], + row: 0 + }, + end: { + col: colSelectRange[1], + row: this.table.columnHeaderLevelCount - 1 + } + }; + if (this.table.internalProps.rowSeriesNumber) { + rowHeaderRange = { + start: { + col: 0, + row: rowSelectRange[0] + }, + end: { + col: 0, + row: rowSelectRange[1] + } + }; + } + } + + if (this.options?.colHighlight !== false && !isSameRange(this.colHeaderRange, colHeaderRange)) { + this.colHeaderRange && this.table.arrangeCustomCellStyle({ range: this.colHeaderRange }, undefined); + colHeaderRange && this.table.arrangeCustomCellStyle({ range: colHeaderRange }, 'col-highlight'); + this.colHeaderRange = colHeaderRange; + } + + if (this.options?.rowHighlight !== false && !isSameRange(this.rowHeaderRange, rowHeaderRange)) { + this.rowHeaderRange && this.table.arrangeCustomCellStyle({ range: this.rowHeaderRange }, undefined); + rowHeaderRange && this.table.arrangeCustomCellStyle({ range: rowHeaderRange }, 'row-highlight'); + this.rowHeaderRange = rowHeaderRange; + } + } +} + +function isSameRange(a: CellRange | undefined, b: CellRange | undefined) { + if (a === undefined && b === undefined) { + return true; + } + + if (a === undefined || b === undefined) { + return false; + } + + return ( + a.start.col === b.start.col && a.start.row === b.start.row && a.end.col === b.end.col && a.end.row === b.end.row + ); +} diff --git a/packages/vtable/src/plugins/invert-highlight.ts b/packages/vtable/src/plugins/invert-highlight.ts new file mode 100644 index 000000000..2dcabf4b2 --- /dev/null +++ b/packages/vtable/src/plugins/invert-highlight.ts @@ -0,0 +1,112 @@ +import type { Rect } from '@src/vrender'; +import { createRect } from '@src/vrender'; +import type { Group } from '../scenegraph/graphic/group'; +import { isSameRange } from '../tools/cell-range'; +import type { CellRange } from '../ts-types'; +import type { BaseTableAPI } from '../ts-types/base-table'; +import { cellInRange } from '../tools/helper'; +import { isValid } from '@visactor/vutils'; + +export interface InvertHighlightPluginOptions { + fill?: string; + opacity?: number; +} + +export class InvertHighlightPlugin { + table: BaseTableAPI; + range?: CellRange; + _fill: string; + _opacity: number; + + constructor(table: BaseTableAPI, options?: InvertHighlightPluginOptions) { + this.table = table; + + this._fill = options?.fill ?? '#000'; + this._opacity = options?.opacity ?? 0.5; + } + + setInvertHighlightRange(range?: CellRange) { + if (isSameRange(this.range, range)) { + return; + } + + this.range = range; + if (!range) { + // reset highlight + this.deleteAllCellGroupShadow(); + } else { + // update highlight + this.updateCellGroupShadow(); + } + + this.table.scenegraph.updateNextFrame(); + } + + deleteAllCellGroupShadow() { + if (!this.table.isPivotTable()) { + this.updateCellGroupShadowInContainer(this.table.scenegraph.rowHeaderGroup); + this.updateCellGroupShadowInContainer(this.table.scenegraph.leftBottomCornerGroup); + } + this.updateCellGroupShadowInContainer(this.table.scenegraph.bodyGroup); + this.updateCellGroupShadowInContainer(this.table.scenegraph.rightFrozenGroup); + this.updateCellGroupShadowInContainer(this.table.scenegraph.bottomFrozenGroup); + this.updateCellGroupShadowInContainer(this.table.scenegraph.rightBottomCornerGroup); + } + + updateCellGroupShadow() { + if (!this.table.isPivotTable()) { + this.updateCellGroupShadowInContainer(this.table.scenegraph.rowHeaderGroup, this.range); + this.updateCellGroupShadowInContainer(this.table.scenegraph.leftBottomCornerGroup, this.range); + } + this.updateCellGroupShadowInContainer(this.table.scenegraph.bodyGroup, this.range); + this.updateCellGroupShadowInContainer(this.table.scenegraph.rightFrozenGroup, this.range); + this.updateCellGroupShadowInContainer(this.table.scenegraph.bottomFrozenGroup), this.range; + this.updateCellGroupShadowInContainer(this.table.scenegraph.rightBottomCornerGroup, this.range); + } + updateCellGroupShadowInContainer(container: Group, range?: CellRange) { + container.forEachChildrenSkipChild((column: Group) => { + if (column.role === 'column') { + column.forEachChildrenSkipChild((cell: Group) => { + if (cell.role !== 'cell') { + return; + } + cell.attachShadow(cell.shadowRoot); + const shadowGroup = cell.shadowRoot; + if (!range) { + // no highlight + shadowGroup.removeAllChild(); + } else if (cellInRange(range, cell.col, cell.row)) { + // inside highlight + shadowGroup.removeAllChild(); + } else if (!shadowGroup.firstChild) { + // outside highlight + const shadowRect = createRect({ + x: 0, + y: 0, + width: cell.attribute.width, + height: cell.attribute.height, + fill: this._fill, + opacity: this._opacity + }); + shadowRect.name = 'shadow-rect'; + shadowGroup.appendChild(shadowRect); + } + }); + } + }); + } +} + +export function onBeforeAttributeUpdateForInvertHighlight(val: Record, attribute: any) { + // @ts-ignore + const graphic = this as any; + if (graphic.shadowRoot && graphic.shadowRoot.childrenCount && (isValid(val.width) || isValid(val.height))) { + const shadowRect = (graphic.shadowRoot as Group).findChildrenByName('shadow-rect')[0] as Rect; + if (shadowRect) { + shadowRect.setAttributes({ + width: val.width ?? shadowRect.attribute.width, + height: val.height ?? shadowRect.attribute.height + }); + } + } +} diff --git a/packages/vtable/src/scenegraph/graphic/group.ts b/packages/vtable/src/scenegraph/graphic/group.ts index 6b75bb433..fd436b5e5 100644 --- a/packages/vtable/src/scenegraph/graphic/group.ts +++ b/packages/vtable/src/scenegraph/graphic/group.ts @@ -307,6 +307,10 @@ export class Group extends VRenderGroup { // 更新bounds之后需要设置父节点,否则tag丢失 this.parent && this.parent.addChildUpdateBoundTag(); this.clearUpdateBoundTag(); + if (this.shadowRoot) { + // this.shadowRoot.clearUpdateBoundTag(); + this.shadowRoot.tryUpdateAABBBounds(); + } return this._AABBBounds; } else if ( this.role === 'body' || diff --git a/packages/vtable/src/scenegraph/group-creater/cell-helper.ts b/packages/vtable/src/scenegraph/group-creater/cell-helper.ts index 36e20624a..d447d83e1 100644 --- a/packages/vtable/src/scenegraph/group-creater/cell-helper.ts +++ b/packages/vtable/src/scenegraph/group-creater/cell-helper.ts @@ -1,4 +1,4 @@ -import type { Cursor, IGraphic, IThemeSpec, Group as VGroup } from '@src/vrender'; +import type { Cursor, IGraphic, IThemeSpec, Rect, Group as VGroup } from '@src/vrender'; import type { ProgressBarStyle } from '../../body-helper/style/ProgressBarStyle'; import { regUrl } from '../../tools/global'; import type { @@ -33,9 +33,10 @@ import type { CreateCheckboxCellGroup } from './cell-type/checkbox-cell'; import { getHierarchyOffset } from '../utils/get-hierarchy-offset'; import { getQuadProps } from '../utils/padding'; import { updateCellContentHeight, updateCellContentWidth } from '../utils/text-icon-layout'; -import { isArray } from '@visactor/vutils'; +import { isArray, isValid } from '@visactor/vutils'; import { breakString } from '../utils/break-string'; import type { CreateRadioCellGroup } from './cell-type/radio-cell'; +import { onBeforeAttributeUpdateForInvertHighlight } from '../../plugins/invert-highlight'; export function createCell( type: ColumnTypeOption, @@ -386,6 +387,7 @@ export function createCell( ); } + cellGroup.onBeforeAttributeUpdate = onBeforeAttributeUpdateForInvertHighlight as any; return cellGroup; } @@ -902,7 +904,8 @@ export function dealWithMergeCellSize( table.isAutoRowHeight(row), padding, textAlign, - textBaseline + textBaseline, + table // 'middle' ); } @@ -952,7 +955,8 @@ export function dealWithMergeCellSizeForShadow( table.isAutoRowHeight(row), padding, textAlign, - textBaseline + textBaseline, + table // 'middle' ); } diff --git a/packages/vtable/src/scenegraph/layout/compute-row-height.ts b/packages/vtable/src/scenegraph/layout/compute-row-height.ts index 2924b51aa..c12b392c8 100644 --- a/packages/vtable/src/scenegraph/layout/compute-row-height.ts +++ b/packages/vtable/src/scenegraph/layout/compute-row-height.ts @@ -57,7 +57,9 @@ export function computeRowsHeight( const isDefaultHeaderHasAuto = table.defaultHeaderRowHeight === 'auto' || (isArray(table.defaultHeaderRowHeight) && table.defaultHeaderRowHeight.some(item => item === 'auto')); - const isAllRowsAuto = table.heightMode === 'autoHeight' || table.heightMode === 'adaptive'; + const isAllRowsAuto = + table.heightMode === 'autoHeight' || + (table.heightMode === 'adaptive' && table.options.autoHeightInAdaptiveMode !== false); if (isAllRowsAuto || isDefaultHeaderHasAuto) { rowStart = rowStart ?? 0; @@ -91,7 +93,9 @@ export function computeRowsHeight( const height = computeRowHeight(row, startCol, endCol, table); newHeights[row] = Math.round(height); //表头部分需要马上设置到缓存中 因为adaptive不会调整表头的高度 另外后面adaptive处理过程中有取值 table.getRowsHeight(0, table.columnHeaderLevelCount - 1); - table._setRowHeight(row, height); + if (table.heightAdaptiveMode === 'only-body' || !update) { + table._setRowHeight(row, height); + } } } @@ -293,7 +297,10 @@ export function computeRowsHeight( if (update) { for (let row = rowStart; row <= rowEnd; row++) { const newRowHeight = newHeights[row] ?? table.getRowHeight(row); - if (newRowHeight !== (oldRowHeights[row] ?? table.getRowHeight(row))) { + // if (newRowHeight !== (oldRowHeights[row] ?? table.getRowHeight(row))) { + // table._setRowHeight(row, newRowHeight); + // } + if (isValid(newRowHeight)) { table._setRowHeight(row, newRowHeight); } } diff --git a/packages/vtable/src/scenegraph/layout/update-height.ts b/packages/vtable/src/scenegraph/layout/update-height.ts index 3de0e22c3..148441516 100644 --- a/packages/vtable/src/scenegraph/layout/update-height.ts +++ b/packages/vtable/src/scenegraph/layout/update-height.ts @@ -361,7 +361,8 @@ function updateMergeCellContentHeight( autoRowHeight, getQuadProps(style.padding as number), style.textAlign, - style.textBaseline + style.textBaseline, + table ); } @@ -402,7 +403,8 @@ function updateMergeCellContentHeight( autoRowHeight, getQuadProps(style.padding as number), style.textAlign, - style.textBaseline + style.textBaseline, + table ); } } diff --git a/packages/vtable/src/scenegraph/utils/text-icon-layout.ts b/packages/vtable/src/scenegraph/utils/text-icon-layout.ts index e89c3680b..652929b71 100644 --- a/packages/vtable/src/scenegraph/utils/text-icon-layout.ts +++ b/packages/vtable/src/scenegraph/utils/text-icon-layout.ts @@ -103,12 +103,19 @@ export function createCellContent( wordBreak: 'break-word', // widthLimit: autoColWidth ? -1 : colWidth - (padding[1] + padding[3]), heightLimit: - autoRowHeight && !table.options.customConfig?.multilinesForXTable + table.options.customConfig?.limitContentHeight === false + ? -1 + : autoRowHeight && !table.options.customConfig?.multilinesForXTable ? -1 : cellHeight - Math.floor(padding[0] + padding[2]), pickable: false, dx: (textAlign === 'left' ? hierarchyOffset : 0) + _contentOffset, - whiteSpace: text.length === 1 && !autoWrapText ? 'no-wrap' : 'normal' + whiteSpace: + table.options.customConfig?.limitContentHeight === false + ? 'normal' + : text.length === 1 && !autoWrapText + ? 'no-wrap' + : 'normal' }; const wrapText = new Text(cellTheme.text ? (Object.assign({}, cellTheme.text, attribute) as any) : attribute); wrapText.name = 'text'; @@ -241,14 +248,21 @@ export function createCellContent( textBaseline: 'top', // widthLimit: autoColWidth ? -1 : colWidth - (padding[1] + padding[3]), heightLimit: - autoRowHeight && !table.options.customConfig?.multilinesForXTable + table.options.customConfig?.limitContentHeight === false + ? -1 + : autoRowHeight && !table.options.customConfig?.multilinesForXTable ? -1 : cellHeight - Math.floor(padding[0] + padding[2]), pickable: false, autoWrapText, lineClamp, wordBreak: 'break-word', - whiteSpace: text.length === 1 && !autoWrapText ? 'no-wrap' : 'normal', + whiteSpace: + table.options.customConfig?.limitContentHeight === false + ? 'normal' + : text.length === 1 && !autoWrapText + ? 'no-wrap' + : 'normal', dx: (textAlign === 'left' ? (!contentLeftIcons.length ? hierarchyOffset : 0) : 0) + _contentOffset }; const wrapText = new Text(cellTheme.text ? (Object.assign({}, cellTheme.text, attribute) as any) : attribute); @@ -702,7 +716,8 @@ export function updateCellContentHeight( autoRowHeight: boolean, padding: [number, number, number, number], textAlign: CanvasTextAlign, - textBaseline: CanvasTextBaseline + textBaseline: CanvasTextBaseline, + table: BaseTableAPI ) { const newHeight = distHeight - Math.floor(padding[0] + padding[2]); @@ -710,7 +725,7 @@ export function updateCellContentHeight( if (textMark instanceof Text && !autoRowHeight) { textMark.setAttributes({ - heightLimit: newHeight + heightLimit: table.options.customConfig?.limitContentHeight === false ? -1 : newHeight } as any); } else if (textMark instanceof RichText && !autoRowHeight) { textMark.setAttributes({ diff --git a/packages/vtable/src/tools/cell-range.ts b/packages/vtable/src/tools/cell-range.ts new file mode 100644 index 000000000..87e137b89 --- /dev/null +++ b/packages/vtable/src/tools/cell-range.ts @@ -0,0 +1,18 @@ +import type { CellRange } from '../ts-types'; + +export function isSameRange(range1: CellRange | undefined | null, range2: CellRange | undefined | null) { + if (!range1 && !range2) { + return true; + } + + if (!range1 || !range2) { + return false; + } + + return ( + range1.start.col === range2.start.col && + range1.start.row === range2.start.row && + range1.end.col === range2.end.col && + range1.end.row === range2.end.row + ); +} diff --git a/packages/vtable/src/ts-types/base-table.ts b/packages/vtable/src/ts-types/base-table.ts index 8fb7621c0..00a0e3480 100644 --- a/packages/vtable/src/ts-types/base-table.ts +++ b/packages/vtable/src/ts-types/base-table.ts @@ -101,7 +101,7 @@ import type { IEmptyTip } from './component/empty-tip'; import type { EmptyTip } from '../components/empty-tip/empty-tip'; import type { CustomCellStylePlugin } from '../plugins/custom-cell-style'; import type { EditManeger } from '../edit/edit-manager'; -import type { TableAnimationManager } from '../core/animation'; +import type { ITableAnimationOption, TableAnimationManager } from '../core/animation'; export interface IBaseTableProtected { element: HTMLElement; @@ -419,6 +419,8 @@ export interface BaseTableConstructorOptions { /** adaptive 模式下高度的适应策略 **/ heightAdaptiveMode?: HeightAdaptiveModeDef; + autoHeightInAdaptiveMode?: boolean; + // /** 行高是否根据内容来计算 */ // autoRowHeight?: boolean; /** 设备的像素比 不配的话默认获取window.devicePixelRatio */ @@ -504,6 +506,9 @@ export interface BaseTableConstructorOptions { // 行列移动不更新表格 notUpdateInColumnRowMove?: boolean; + + // 表格是否限制内容高度 + limitContentHeight?: boolean; }; // 部分特殊配置,兼容xTable等作用 animationAppear?: boolean | IAnimationAppear; @@ -857,7 +862,9 @@ export interface BaseTableAPI { * 滚动到具体某个单元格位置 * @param cellAddr 要滚动到的单元格位置 */ - scrollToCell: (cellAddr: { col?: number; row?: number }) => void; + scrollToCell: (cellAddr: { col?: number; row?: number }, animationOption?: ITableAnimationOption | boolean) => void; + scrollToRow: (row: number, animationOption?: ITableAnimationOption | boolean) => void; + scrollToCol: (col: number, animationOption?: ITableAnimationOption | boolean) => void; registerCustomCellStyle: (customStyleId: string, customStyle: ColumnStyleOption | undefined | null) => void; arrangeCustomCellStyle: (cellPos: { col?: number; row?: number; range?: CellRange }, customStyleId: string) => void; diff --git a/packages/vue-vtable/src/tables/base-table.vue b/packages/vue-vtable/src/tables/base-table.vue index bc5631ce4..ef26edfc1 100644 --- a/packages/vue-vtable/src/tables/base-table.vue +++ b/packages/vue-vtable/src/tables/base-table.vue @@ -1,5 +1,6 @@