diff --git a/packages/vtable/__tests__/plugins/custom-cell-style.test.ts b/packages/vtable/__tests__/plugins/custom-cell-style.test.ts new file mode 100644 index 0000000000..7781a8b1a6 --- /dev/null +++ b/packages/vtable/__tests__/plugins/custom-cell-style.test.ts @@ -0,0 +1,127 @@ +// @ts-nocheck +import { CustomCellStylePlugin } from '../../src/plugins/custom-cell-style'; + +function createMockTable(colCount = 5000, rowCount = 5000) { + return { + colCount, + rowCount, + getCellRange: (col: number, row: number) => ({ + start: { col, row }, + end: { col, row } + }), + getCellValue: jest.fn(), + getCellOriginValue: jest.fn(), + getCellHeaderPaths: jest.fn(), + scenegraph: { + updateCellContent: jest.fn(), + updateNextFrame: jest.fn() + } + }; +} + +describe('CustomCellStylePlugin', () => { + test('apply and clear single cell style without shrinking array', () => { + const table = createMockTable(); + const plugin = new CustomCellStylePlugin( + table as any, + [ + { + id: 's1', + style: { bgColor: 'red' } + } + ] as any, + [] as any + ); + + plugin.arrangeCustomCellStyle({ col: 1, row: 2 }, 's1'); + expect(plugin.getCustomCellStyleIds(1, 2)).toEqual(['s1']); + expect(plugin.getCustomCellStyle(1, 2)).toEqual({ bgColor: 'red' }); + + const beforeClearLength = plugin.customCellStyleArrangement.length; + plugin.arrangeCustomCellStyle({ col: 1, row: 2 }, null); + expect(plugin.getCustomCellStyleIds(1, 2)).toEqual([]); + expect(plugin.getCustomCellStyle(1, 2)).toBeUndefined(); + expect(plugin.customCellStyleArrangement.length).toBe(beforeClearLength); + expect((plugin as any)._customCellStyleArrangementTombstoneCount).toBe(1); + }); + + test('does not delete wrong cell when index map is stale', () => { + const table = createMockTable(); + const plugin = new CustomCellStylePlugin( + table as any, + [ + { id: 'a', style: { bgColor: 'red' } }, + { id: 'b', style: { bgColor: 'blue' } } + ] as any, + [] as any + ); + + plugin.arrangeCustomCellStyle({ col: 1, row: 1 }, 'a'); + plugin.arrangeCustomCellStyle({ col: 2, row: 2 }, 'b'); + + const arr = plugin.customCellStyleArrangement; + const tmp = arr[0]; + arr[0] = arr[1]; + arr[1] = tmp; + + plugin.arrangeCustomCellStyle({ col: 1, row: 1 }, null); + + expect(plugin.getCustomCellStyleIds(1, 1)).toEqual([]); + expect(plugin.getCustomCellStyleIds(2, 2)).toEqual(['b']); + }); + + test('compacts tombstones during massive clears and keeps index consistent', () => { + const table = createMockTable(10000, 2); + const plugin = new CustomCellStylePlugin( + table as any, + [ + { + id: 's', + style: { bgColor: 'yellow' } + } + ] as any, + [] as any + ); + + const total = 3000; + const removed = 2500; + for (let i = 0; i < total; i++) { + plugin.arrangeCustomCellStyle({ col: i, row: 0 }, 's'); + } + for (let i = 0; i < removed; i++) { + plugin.arrangeCustomCellStyle({ col: i, row: 0 }, null); + } + + expect((plugin as any)._customCellStyleArrangementTombstoneCount).toBeLessThan(2048); + expect((plugin as any)._customCellStyleArrangementIndex.size).toBe(total - removed); + + for (let i = 0; i < removed; i++) { + expect(plugin.getCustomCellStyleIds(i, 0)).toEqual([]); + } + for (let i = removed; i < total; i++) { + expect(plugin.getCustomCellStyleIds(i, 0)).toEqual(['s']); + } + }); + + test('uses fast update when style only touches cellStyleKeys', () => { + const table = createMockTable(); + const plugin = new CustomCellStylePlugin( + table as any, + [ + { + id: 's', + style: { bgColor: 'red', color: '#000' } + } + ] as any, + [] as any + ); + + plugin.arrangeCustomCellStyle({ col: 3, row: 4 }, 's'); + const calls = (table as any).scenegraph.updateCellContent.mock.calls; + expect(calls.length).toBeGreaterThan(0); + const lastCall = calls[calls.length - 1]; + expect(lastCall[0]).toBe(3); + expect(lastCall[1]).toBe(4); + expect(lastCall[2]).toBe(true); + }); +}); diff --git a/packages/vtable/src/PivotTable-all.ts b/packages/vtable/src/PivotTable-all.ts index 8194ecf2ef..1caf13a88b 100644 --- a/packages/vtable/src/PivotTable-all.ts +++ b/packages/vtable/src/PivotTable-all.ts @@ -8,7 +8,7 @@ import { registerTooltip, registerAnimation } from './components'; -import { registerCustomCellStylePlugin } from './plugins/custom-cell-style' +import { registerCustomCellStylePlugin } from './plugins/custom-cell-style'; import { registerChartCell, registerCheckboxCell, diff --git a/packages/vtable/src/edit/edit-manager.ts b/packages/vtable/src/edit/edit-manager.ts index 117f60802a..7d973843b7 100644 --- a/packages/vtable/src/edit/edit-manager.ts +++ b/packages/vtable/src/edit/edit-manager.ts @@ -120,7 +120,7 @@ export class EditManager { // 如果这里不加延时,会导致鼠标抬起pointerup的时候将table.getElement()元素设置成焦点,从而导致编辑器失去焦点(因为prepareEdit只是将editor的element设置pointerEvents为none) if (editor && this.editingEditor !== editor) { // 判断当前编辑器如果是当前需要准备的编辑器,则不进行准备编辑。这个是为了container-dom文件moveEditCellOnArrowKeys前后逻辑问题,前面有个selectCell会触发这个事件,后面有startEdit了,所以这个prepare就没必要了,触发的话反而有问题 - editor.prepareEdit?.({ + (editor as any).prepareEdit?.({ referencePosition, container: this.table.getElement(), table: this.table, @@ -149,7 +149,7 @@ export class EditManager { } const editor = (this.table as ListTableAPI).getEditor(col, row); if (editor) { - editElement && editor.setElement(editElement); + editElement && (editor as any).setElement?.(editElement); // //自定义内容单元格不允许编辑 // if (this.table.getCustomRender(col, row) || this.table.getCustomLayout(col, row)) { // console.warn("VTable Warn: cell has config custom render or layout, can't be edited"); diff --git a/packages/vtable/src/event/event.ts b/packages/vtable/src/event/event.ts index 13126cca1d..465f962853 100644 --- a/packages/vtable/src/event/event.ts +++ b/packages/vtable/src/event/event.ts @@ -284,7 +284,7 @@ export class EventManager { eventArgs.event.shiftKey && shiftMultiSelect, (eventArgs.event.ctrlKey || eventArgs.event.metaKey) && ctrlMultiSelect, false, - isSelectMoving ? false : (this.table.options.select?.makeSelectCellVisible ?? true) + isSelectMoving ? false : this.table.options.select?.makeSelectCellVisible ?? true ); return true; diff --git a/packages/vtable/src/plugins/custom-cell-style.ts b/packages/vtable/src/plugins/custom-cell-style.ts index 2979ad9d2c..4bf487d532 100644 --- a/packages/vtable/src/plugins/custom-cell-style.ts +++ b/packages/vtable/src/plugins/custom-cell-style.ts @@ -22,6 +22,8 @@ export class CustomCellStylePlugin { table: BaseTableAPI; customCellStyle: CustomCellStyle[]; customCellStyleArrangement: CustomCellStyleArrangement[]; + private _customCellStyleArrangementIndex: Map; + private _customCellStyleArrangementTombstoneCount: number; constructor( table: BaseTableAPI, @@ -31,6 +33,53 @@ export class CustomCellStylePlugin { this.table = table; this.customCellStyle = customCellStyle; this.customCellStyleArrangement = customCellStyleArrangement; + this._customCellStyleArrangementIndex = new Map(); + this._customCellStyleArrangementTombstoneCount = 0; + this._rebuildCustomCellStyleArrangementIndex(); + } + + private _getCustomCellStyleArrangementKey(cellPos: { col?: number; row?: number; range?: CellRange }) { + if (cellPos.range) { + const { start, end } = cellPos.range; + return `range:${start.col},${start.row},${end.col},${end.row}`; + } + if (cellPos.col === undefined || cellPos.row === undefined) { + return undefined; + } + return `cell:${cellPos.col},${cellPos.row}`; + } + + private _rebuildCustomCellStyleArrangementIndex() { + this._customCellStyleArrangementIndex.clear(); + this._customCellStyleArrangementTombstoneCount = 0; + for (let i = 0; i < this.customCellStyleArrangement.length; i++) { + if (!isValid((this.customCellStyleArrangement[i] as any).customStyleId)) { + this._customCellStyleArrangementTombstoneCount++; + continue; + } + const key = this._getCustomCellStyleArrangementKey(this.customCellStyleArrangement[i].cellPosition); + if (key) { + this._customCellStyleArrangementIndex.set(key, i); + } + } + } + + private _compactCustomCellStyleArrangementIfNeeded() { + const length = this.customCellStyleArrangement.length; + if (this._customCellStyleArrangementTombstoneCount < 2048) { + return; + } + if (this._customCellStyleArrangementTombstoneCount * 4 < length) { + return; + } + const compacted = this.customCellStyleArrangement.filter(style => isValid((style as any).customStyleId)); + if (compacted.length === this.customCellStyleArrangement.length) { + this._customCellStyleArrangementTombstoneCount = 0; + return; + } + this.customCellStyleArrangement.length = 0; + this.customCellStyleArrangement.push(...compacted); + this._rebuildCustomCellStyleArrangementIndex(); } getCustomCellStyle(col: number, row: number) { @@ -55,6 +104,9 @@ export class CustomCellStylePlugin { } }); + if (!styles.length) { + return undefined; + } return merge({}, ...styles); // const styleOption = this.getCustomCellStyleOption(customStyleId); // return styleOption?.style; @@ -71,6 +123,9 @@ export class CustomCellStylePlugin { for (let r = range.start.row; r <= range.end.row; r++) { // eslint-disable-next-line no-loop-func this.customCellStyleArrangement.forEach(style => { + if (!isValid(style.customStyleId)) { + return; + } if (style.cellPosition.range) { if ( style.cellPosition.range.start.col <= c && @@ -79,11 +134,11 @@ export class CustomCellStylePlugin { style.cellPosition.range.end.row >= r ) { // customStyleId = style.customStyleId; - customStyleIds.push(style.customStyleId); + customStyleIds.push(style.customStyleId as string); } } else if (style.cellPosition.col === c && style.cellPosition.row === r) { // customStyleId = style.customStyleId; - customStyleIds.push(style.customStyleId); + customStyleIds.push(style.customStyleId as string); } }); } @@ -147,17 +202,41 @@ export class CustomCellStylePlugin { customStyleId: string | undefined | null, forceFastUpdate?: boolean ) { - const index = this.customCellStyleArrangement.findIndex(style => { - if (style.cellPosition.range && cellPos.range) { - return ( - style.cellPosition.range.start.col === cellPos.range.start.col && - style.cellPosition.range.start.row === cellPos.range.start.row && - style.cellPosition.range.end.col === cellPos.range.end.col && - style.cellPosition.range.end.row === cellPos.range.end.row - ); + const inputKey = this._getCustomCellStyleArrangementKey(cellPos); + let index = inputKey ? this._customCellStyleArrangementIndex.get(inputKey) ?? -1 : -1; + if (inputKey && index !== -1) { + const item = this.customCellStyleArrangement[index]; + const itemKey = item ? this._getCustomCellStyleArrangementKey(item.cellPosition) : undefined; + if (!item || !isValid((item as any).customStyleId) || itemKey !== inputKey) { + index = this.customCellStyleArrangement.findIndex(style => { + if (!isValid((style as any).customStyleId)) { + return false; + } + return this._getCustomCellStyleArrangementKey(style.cellPosition) === inputKey; + }); + if (index !== -1) { + this._customCellStyleArrangementIndex.set(inputKey, index); + } else { + this._customCellStyleArrangementIndex.delete(inputKey); + } } - return style.cellPosition.col === cellPos.col && style.cellPosition.row === cellPos.row; - }); + } + if (index === -1 && !inputKey) { + index = this.customCellStyleArrangement.findIndex(style => { + if (!isValid((style as any).customStyleId)) { + return false; + } + if (style.cellPosition.range && cellPos.range) { + return ( + style.cellPosition.range.start.col === cellPos.range.start.col && + style.cellPosition.range.start.row === cellPos.range.start.row && + style.cellPosition.range.end.col === cellPos.range.end.col && + style.cellPosition.range.end.row === cellPos.range.end.row + ); + } + return style.cellPosition.col === cellPos.col && style.cellPosition.row === cellPos.row; + }); + } if (index === -1 && !customStyleId) { // do nothing @@ -172,6 +251,13 @@ export class CustomCellStylePlugin { }, customStyleId: customStyleId }); + const pushedIndex = this.customCellStyleArrangement.length - 1; + const pushedKey = this._getCustomCellStyleArrangementKey( + this.customCellStyleArrangement[pushedIndex].cellPosition + ); + if (pushedKey) { + this._customCellStyleArrangementIndex.set(pushedKey, pushedIndex); + } } else if (this.customCellStyleArrangement[index].customStyleId === customStyleId) { // same style return; @@ -180,10 +266,18 @@ export class CustomCellStylePlugin { this.customCellStyleArrangement[index].customStyleId = customStyleId; } else { // delete useless style - this.customCellStyleArrangement.splice(index, 1); + const existedKey = this._getCustomCellStyleArrangementKey(this.customCellStyleArrangement[index].cellPosition); + if (isValid((this.customCellStyleArrangement[index] as any).customStyleId)) { + this._customCellStyleArrangementTombstoneCount++; + } + (this.customCellStyleArrangement[index] as any).customStyleId = null; + if (existedKey) { + this._customCellStyleArrangementIndex.delete(existedKey); + } + this._compactCustomCellStyleArrangementIfNeeded(); } - const style = this.getCustomCellStyleOption(customStyleId)?.style; + const style = customStyleId ? this.getCustomCellStyleOption(customStyleId)?.style : undefined; // let forceFastUpdate; if (style) { forceFastUpdate = true; @@ -226,6 +320,8 @@ export class CustomCellStylePlugin { updateCustomCell(customCellStyle: CustomCellStyle[], customCellStyleArrangement: CustomCellStyleArrangement[]) { this.customCellStyle.length = 0; this.customCellStyleArrangement.length = 0; + this._customCellStyleArrangementIndex.clear(); + this._customCellStyleArrangementTombstoneCount = 0; customCellStyle.forEach((cellStyle: CustomCellStyle) => { this.registerCustomCellStyle(cellStyle.id, cellStyle.style); }); diff --git a/packages/vtable/src/scenegraph/group-creater/cell-type/progress-bar-cell.ts b/packages/vtable/src/scenegraph/group-creater/cell-type/progress-bar-cell.ts index 9bee9db65a..743713f667 100644 --- a/packages/vtable/src/scenegraph/group-creater/cell-type/progress-bar-cell.ts +++ b/packages/vtable/src/scenegraph/group-creater/cell-type/progress-bar-cell.ts @@ -73,7 +73,7 @@ export function createProgressBarCell( } else { height = table.getRowHeight(row); } - + // 检查是否有主从表插件,如果有则使用原始高度 if ((table as any).pluginManager) { const masterDetailPlugin = (table as any).pluginManager.getPluginByName('Master Detail Plugin'); diff --git a/packages/vtable/src/scenegraph/select/update-select-border.ts b/packages/vtable/src/scenegraph/select/update-select-border.ts index 6acc429450..f840986b45 100644 --- a/packages/vtable/src/scenegraph/select/update-select-border.ts +++ b/packages/vtable/src/scenegraph/select/update-select-border.ts @@ -92,7 +92,7 @@ function updateComponent( // const rowsHeight = table.getRowsHeight(cellRange.start.row, endRow); const colsWidth = table.getColsWidth(computeRectCellRangeStartCol, computeRectCellRangeEndCol); const rowsHeight = table.getRowsHeight(computeRectCellRangeStartRow, computeRectCellRangeEndRow); - + const firstCellBound = scene.highPerformanceGetCell( computeRectCellRangeStartCol, computeRectCellRangeStartRow