From fd4ce737683da8a6e916ab29d973a68a445a8a80 Mon Sep 17 00:00:00 2001 From: biubiukam Date: Wed, 15 Jul 2026 04:48:52 +0800 Subject: [PATCH 01/77] fix: hide row series number in aggregation rows --- ...able-row-series-number-aggregation.test.ts | 39 +++++++++++++++++++ packages/vtable/src/ListTable.ts | 6 +++ 2 files changed, 45 insertions(+) create mode 100644 packages/vtable/__tests__/options/listTable-row-series-number-aggregation.test.ts diff --git a/packages/vtable/__tests__/options/listTable-row-series-number-aggregation.test.ts b/packages/vtable/__tests__/options/listTable-row-series-number-aggregation.test.ts new file mode 100644 index 0000000000..188afc9223 --- /dev/null +++ b/packages/vtable/__tests__/options/listTable-row-series-number-aggregation.test.ts @@ -0,0 +1,39 @@ +// @ts-nocheck +import { ListTable, TYPES } from '../../src'; +import { createDiv } from '../dom'; + +global.__VERSION__ = 'none'; + +describe('ListTable row series number with aggregation', () => { + const container = createDiv(); + container.style.width = '600px'; + container.style.height = '400px'; + + const table = new ListTable(container, { + records: [{ value: 1 }, { value: 2 }], + columns: [ + { + field: 'value', + title: 'Value', + aggregation: { + aggregationType: TYPES.AggregationType.SUM + } + } + ], + rowSeriesNumber: { + title: 'No.' + } + }); + + afterAll(() => { + table.release(); + }); + + test('does not display a series number in the aggregation row', () => { + const aggregationRow = table.rowCount - 1; + + expect(table.internalProps.layoutMap.isAggregation(0, aggregationRow)).toBe(true); + expect(table.getCellValue(0, aggregationRow)).toBe(''); + expect(table.getCellOriginValue(0, aggregationRow)).toBe(''); + }); +}); diff --git a/packages/vtable/src/ListTable.ts b/packages/vtable/src/ListTable.ts index 070c74ee65..e029b739cf 100644 --- a/packages/vtable/src/ListTable.ts +++ b/packages/vtable/src/ListTable.ts @@ -579,6 +579,9 @@ export class ListTable extends BaseTable implements ListTableAPI { const { title } = table.internalProps.layoutMap.getSeriesNumberHeader(col, row); return title; } + if (table.internalProps.layoutMap.isAggregation(col, row)) { + return ''; + } let value; if ((this.internalProps as ListTableProtected).groupBy) { const record = table.getCellRawRecord(col, row); @@ -629,6 +632,9 @@ export class ListTable extends BaseTable implements ListTableAPI { const { title } = table.internalProps.layoutMap.getSeriesNumberHeader(col, row); return title; } + if (table.internalProps.layoutMap.isAggregation(col, row)) { + return ''; + } const { format } = table.internalProps.layoutMap.getSeriesNumberBody(col, row); return typeof format === 'function' ? format(col, row, this) : row - this.columnHeaderLevelCount; } else if (table.internalProps.layoutMap.isHeader(col, row)) { From 993a8d1ff7dc69f5b37d218619a5da07f9952349 Mon Sep 17 00:00:00 2001 From: biubiukam Date: Wed, 15 Jul 2026 04:50:35 +0800 Subject: [PATCH 02/77] docs: update changlog of rush --- ...13-aggregation-series-number_2026-07-14-20-50.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 common/changes/@visactor/vtable/fix-issue-5213-aggregation-series-number_2026-07-14-20-50.json diff --git a/common/changes/@visactor/vtable/fix-issue-5213-aggregation-series-number_2026-07-14-20-50.json b/common/changes/@visactor/vtable/fix-issue-5213-aggregation-series-number_2026-07-14-20-50.json new file mode 100644 index 0000000000..13a5b655a4 --- /dev/null +++ b/common/changes/@visactor/vtable/fix-issue-5213-aggregation-series-number_2026-07-14-20-50.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "fix: hide row series number in aggregation rows", + "type": "none", + "packageName": "@visactor/vtable" + } + ], + "packageName": "@visactor/vtable", + "email": "biukam.w@gmail.com" +} \ No newline at end of file From 28d5709035ecded03c43c4f36436182bc48d51d4 Mon Sep 17 00:00:00 2001 From: biubiukam Date: Wed, 15 Jul 2026 05:18:14 +0800 Subject: [PATCH 03/77] fix: delete reverse-selected rows from context menu --- .../context-menu/handle-menu-helper.test.ts | 65 +++++++++++++++++++ .../src/contextmenu/handle-menu-helper.ts | 4 +- .../vtable-plugins/src/table-series-number.ts | 6 +- 3 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 packages/vtable-plugins/__tests__/context-menu/handle-menu-helper.test.ts diff --git a/packages/vtable-plugins/__tests__/context-menu/handle-menu-helper.test.ts b/packages/vtable-plugins/__tests__/context-menu/handle-menu-helper.test.ts new file mode 100644 index 0000000000..1c777b352a --- /dev/null +++ b/packages/vtable-plugins/__tests__/context-menu/handle-menu-helper.test.ts @@ -0,0 +1,65 @@ +// @ts-nocheck +import { ListTable } from '@visactor/vtable'; +import { createDiv } from '../../../vtable/__tests__/dom'; +import { MenuHandler } from '../../src/contextmenu/handle-menu-helper'; +import { TableSeriesNumber } from '../../src/table-series-number'; + +global.__VERSION__ = 'none'; + +describe('Context menu row deletion', () => { + let table: ListTable; + + afterEach(() => { + table?.release(); + document.body.innerHTML = ''; + }); + + test('deletes all rows in a reverse-dragged row selection', () => { + const container = createDiv(); + container.style.width = '600px'; + container.style.height = '400px'; + + const seriesNumberPlugin = new TableSeriesNumber({ + rowCount: 5, + colCount: 2 + }); + table = new ListTable({ + container, + showHeader: false, + columns: [ + { field: 'id', title: 'ID' }, + { field: 'name', title: 'Name' } + ], + records: [ + { id: 0, name: 'A' }, + { id: 1, name: 'B' }, + { id: 2, name: 'C' }, + { id: 3, name: 'D' }, + { id: 4, name: 'E' } + ], + syncRecordOperationsToSourceRecords: true, + plugins: [seriesNumberPlugin] + }); + + table.stateManager.select.ranges = [ + { + start: { col: 0, row: 3 }, + end: { col: table.colCount - 1, row: 1 } + } + ]; + + const selectCells = jest.spyOn(table, 'selectCells'); + seriesNumberPlugin['handleSeriesNumberCellRightClick']({ + detail: { + seriesNumberCell: { id: 2, name: 'row-series-number-cell' }, + event: new MouseEvent('contextmenu') + } + }); + + expect(selectCells).not.toHaveBeenCalled(); + + new MenuHandler().handleDeleteRow(table); + + expect(table.records.map(record => record.id)).toEqual([0, 4]); + }); +}); diff --git a/packages/vtable-plugins/src/contextmenu/handle-menu-helper.ts b/packages/vtable-plugins/src/contextmenu/handle-menu-helper.ts index 1528a775e9..94fdf081e4 100644 --- a/packages/vtable-plugins/src/contextmenu/handle-menu-helper.ts +++ b/packages/vtable-plugins/src/contextmenu/handle-menu-helper.ts @@ -105,7 +105,9 @@ export class MenuHandler { const deleteRowIndexs: number[] = []; for (let i = 0; i < selectRanges.length; i++) { const range = selectRanges[i]; - for (let j = range.start.row; j <= range.end.row; j++) { + const startRow = Math.min(range.start.row, range.end.row); + const endRow = Math.max(range.start.row, range.end.row); + for (let j = startRow; j <= endRow; j++) { if (!deleteRowIndexs.includes(j)) { deleteRowIndexs.push(j); } diff --git a/packages/vtable-plugins/src/table-series-number.ts b/packages/vtable-plugins/src/table-series-number.ts index 8919a1cded..4d581d8c9b 100644 --- a/packages/vtable-plugins/src/table-series-number.ts +++ b/packages/vtable-plugins/src/table-series-number.ts @@ -289,9 +289,11 @@ export class TableSeriesNumber implements pluginsDefinition.IVTablePlugin { const rowIndex = seriesNumberCell.id; //判断rowIndex整行是否被选中 const isRowSelected = this.table.stateManager.select.ranges.some(range => { + const startRow = Math.min(range.start.row, range.end.row); + const endRow = Math.max(range.start.row, range.end.row); return ( - range.start.row <= rowIndex && - rowIndex <= range.end.row && + startRow <= rowIndex && + rowIndex <= endRow && range.start.col === 0 && range.end.col === this.table.colCount - 1 ); From 8fcb48adb4be26fbab793e0524340d6a89a79e30 Mon Sep 17 00:00:00 2001 From: biubiukam Date: Wed, 15 Jul 2026 05:22:29 +0800 Subject: [PATCH 04/77] docs: update changlog of rush --- ...ue-5214-delete-selected-rows_2026-07-14-21-21.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 common/changes/@visactor/vtable/fix-issue-5214-delete-selected-rows_2026-07-14-21-21.json diff --git a/common/changes/@visactor/vtable/fix-issue-5214-delete-selected-rows_2026-07-14-21-21.json b/common/changes/@visactor/vtable/fix-issue-5214-delete-selected-rows_2026-07-14-21-21.json new file mode 100644 index 0000000000..3857992a67 --- /dev/null +++ b/common/changes/@visactor/vtable/fix-issue-5214-delete-selected-rows_2026-07-14-21-21.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "fix: delete reverse-selected rows from context menu", + "type": "none", + "packageName": "@visactor/vtable" + } + ], + "packageName": "@visactor/vtable", + "email": "biukam.w@gmail.com" +} \ No newline at end of file From 881d59d42af9ce2fb083cbf9331ce8c42c91eb42 Mon Sep 17 00:00:00 2001 From: biubiukam Date: Wed, 15 Jul 2026 17:34:36 +0800 Subject: [PATCH 05/77] fix: preserve blank rows when importing excel files --- .../__tests__/excel-import/excel.test.ts | 22 +++++++++++++++++++ .../vtable-plugins/src/excel-import/excel.ts | 13 ++++++++--- 2 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 packages/vtable-plugins/__tests__/excel-import/excel.test.ts diff --git a/packages/vtable-plugins/__tests__/excel-import/excel.test.ts b/packages/vtable-plugins/__tests__/excel-import/excel.test.ts new file mode 100644 index 0000000000..2535e43d7a --- /dev/null +++ b/packages/vtable-plugins/__tests__/excel-import/excel.test.ts @@ -0,0 +1,22 @@ +import ExcelJS from 'exceljs'; +import { parseWorksheetToSheetData } from '../../src/excel-import/excel'; + +describe('Excel worksheet import', () => { + test('preserves data after a blank first row', async () => { + const workbook = new ExcelJS.Workbook(); + const worksheet = workbook.addWorksheet('Sheet1'); + worksheet.getCell('A2').value = 'Name'; + worksheet.getCell('B2').value = 'Age'; + worksheet.getCell('A3').value = 'Alice'; + worksheet.getCell('B3').value = 30; + + const result = await parseWorksheetToSheetData(worksheet, 0); + + expect(result.rowCount).toBe(3); + expect(result.data).toEqual([ + [null, null], + ['Name', 'Age'], + ['Alice', 30] + ]); + }); +}); diff --git a/packages/vtable-plugins/src/excel-import/excel.ts b/packages/vtable-plugins/src/excel-import/excel.ts index 3e64674de5..6d7de56b0a 100644 --- a/packages/vtable-plugins/src/excel-import/excel.ts +++ b/packages/vtable-plugins/src/excel-import/excel.ts @@ -150,9 +150,16 @@ export async function parseWorksheetToSheetData( const sheetTitle = worksheet.name || `Sheet${sheetIndex + 1}`; const sheetKey = `sheet_${Date.now()}_${sheetIndex}`; - // 获取实际数据范围 - const rowCount = worksheet.actualRowCount || 0; - const columnCount = worksheet.actualColumnCount || 0; + // 获取实际数据范围。actualRowCount/actualColumnCount 只统计非空行列的数量, + // 当数据前面或中间存在空白行列时,不能作为最后一个数据单元格的索引。 + let rowCount = 0; + let columnCount = 0; + worksheet.eachRow((row, rowNumber) => { + rowCount = Math.max(rowCount, rowNumber); + row.eachCell((_cell, colNumber) => { + columnCount = Math.max(columnCount, colNumber); + }); + }); if (rowCount === 0 || columnCount === 0) { // 空 sheet,但仍需要解析合并单元格信息(可能只有合并单元格没有数据) From c05ba239fe5af78f897da2034d44f04c18aaf19a Mon Sep 17 00:00:00 2001 From: biubiukam Date: Wed, 15 Jul 2026 17:34:59 +0800 Subject: [PATCH 06/77] docs: update changlog of rush --- ...5227-excel-leading-blank-row_2026-07-15-09-34.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 common/changes/@visactor/vtable-plugins/fix-issue-5227-excel-leading-blank-row_2026-07-15-09-34.json diff --git a/common/changes/@visactor/vtable-plugins/fix-issue-5227-excel-leading-blank-row_2026-07-15-09-34.json b/common/changes/@visactor/vtable-plugins/fix-issue-5227-excel-leading-blank-row_2026-07-15-09-34.json new file mode 100644 index 0000000000..d4f9d08cf9 --- /dev/null +++ b/common/changes/@visactor/vtable-plugins/fix-issue-5227-excel-leading-blank-row_2026-07-15-09-34.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "fix: preserve blank rows when importing excel files (GitHub #5227)", + "type": "patch", + "packageName": "@visactor/vtable-plugins" + } + ], + "packageName": "@visactor/vtable-plugins", + "email": "biukam.w@gmail.com" +} From 6ec78aee30a909fe3a2838747240190ffd012834 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Mon, 20 Jul 2026 11:58:40 +0800 Subject: [PATCH 07/77] demo: add issue 5227 reproduction --- .../issue-5227-leading-blank-row.ts | 107 ++++++++++++++++++ packages/vtable-plugins/demo/menu.ts | 4 + 2 files changed, 111 insertions(+) create mode 100644 packages/vtable-plugins/demo/excel-import/issue-5227-leading-blank-row.ts diff --git a/packages/vtable-plugins/demo/excel-import/issue-5227-leading-blank-row.ts b/packages/vtable-plugins/demo/excel-import/issue-5227-leading-blank-row.ts new file mode 100644 index 0000000000..0958dea612 --- /dev/null +++ b/packages/vtable-plugins/demo/excel-import/issue-5227-leading-blank-row.ts @@ -0,0 +1,107 @@ +import ExcelJS from 'exceljs'; +import * as VTable from '@visactor/vtable'; +import { parseWorksheetToSheetData } from '../../src/excel-import/excel'; + +const CONTAINER_ID = 'vTable'; + +const removeDemoToolbar = () => { + document.getElementById('issue5227Toolbar')?.remove(); +}; + +const setStatus = (message: string, pass: boolean) => { + const statusNode = document.getElementById('issue5227Status'); + if (!statusNode) { + return; + } + statusNode.textContent = message; + statusNode.style.color = pass ? '#237804' : '#cf1322'; +}; + +const createWorksheetWithLeadingBlankRow = () => { + const workbook = new ExcelJS.Workbook(); + const worksheet = workbook.addWorksheet('Sheet1'); + worksheet.getCell('A2').value = 'Name'; + worksheet.getCell('B2').value = 'Age'; + worksheet.getCell('A3').value = 'Alice'; + worksheet.getCell('B3').value = 30; + return worksheet; +}; + +const renderImportedData = (container: HTMLElement, data: unknown[][]) => { + if ((window as any).tableInstance) { + (window as any).tableInstance.release(); + } + + const maxColumnCount = data.reduce((max, row) => Math.max(max, row.length), 0); + const records = data.map((row, index) => { + const record: Record = { rowIndex: index + 1 }; + for (let col = 0; col < maxColumnCount; col++) { + record[`col${col}`] = row[col] ?? null; + } + return record; + }); + + const columns: VTable.ColumnsDefine = [ + { field: 'rowIndex', title: 'Excel Row', width: 100 }, + ...Array.from({ length: maxColumnCount }, (_, col) => ({ + field: `col${col}`, + title: String.fromCharCode(65 + col), + width: 140 + })) + ]; + + const tableInstance = new VTable.ListTable({ + container, + columns, + records, + defaultRowHeight: 36 + }); + + (window as any).tableInstance = tableInstance; +}; + +export function createTable() { + removeDemoToolbar(); + + const container = document.getElementById(CONTAINER_ID)!; + container.style.width = '640px'; + container.style.height = '360px'; + container.innerHTML = ''; + + const toolbar = document.createElement('div'); + toolbar.id = 'issue5227Toolbar'; + toolbar.style.cssText = [ + 'display: flex', + 'gap: 8px', + 'align-items: center', + 'height: 48px', + 'font-size: 12px' + ].join(';'); + toolbar.innerHTML = ` + + 预期:保留空白第 1 行,rowCount=3。 + + `; + container.before(toolbar); + + document.getElementById('issue5227Import')?.addEventListener('click', async () => { + const worksheet = createWorksheetWithLeadingBlankRow(); + const result = await parseWorksheetToSheetData(worksheet, 0); + const pass = + result.rowCount === 3 && + result.columnCount === 2 && + result.data[0]?.[0] === null && + result.data[1]?.[0] === 'Name' && + result.data[2]?.[0] === 'Alice'; + + renderImportedData(container, result.data); + setStatus( + `${pass ? 'PASS' : 'FAIL'} | rowCount=${result.rowCount}, columnCount=${result.columnCount}, data=${JSON.stringify( + result.data + )}`, + pass + ); + }); + + setStatus('READY', true); +} diff --git a/packages/vtable-plugins/demo/menu.ts b/packages/vtable-plugins/demo/menu.ts index f5e19298ca..c5da594533 100644 --- a/packages/vtable-plugins/demo/menu.ts +++ b/packages/vtable-plugins/demo/menu.ts @@ -11,6 +11,10 @@ export const menus = [ path: 'excel-import', name: 'excel-import' }, + { + path: 'excel-import', + name: 'issue-5227-leading-blank-row' + }, { path: 'filter', name: 'filter' From 078bee6c21be1da423799db82e9dfab3e1a05143 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Mon, 20 Jul 2026 11:58:40 +0800 Subject: [PATCH 08/77] demo: add issue 5214 reproduction --- .../issue-5214-reverse-selected-row-delete.ts | 111 ++++++++++++++++++ packages/vtable-plugins/demo/menu.ts | 4 + 2 files changed, 115 insertions(+) create mode 100644 packages/vtable-plugins/demo/context-menu/issue-5214-reverse-selected-row-delete.ts diff --git a/packages/vtable-plugins/demo/context-menu/issue-5214-reverse-selected-row-delete.ts b/packages/vtable-plugins/demo/context-menu/issue-5214-reverse-selected-row-delete.ts new file mode 100644 index 0000000000..b0cdfcc0d7 --- /dev/null +++ b/packages/vtable-plugins/demo/context-menu/issue-5214-reverse-selected-row-delete.ts @@ -0,0 +1,111 @@ +import * as VTable from '@visactor/vtable'; +import { MenuHandler } from '../../src/contextmenu/handle-menu-helper'; +import { TableSeriesNumber } from '../../src/table-series-number'; + +const CONTAINER_ID = 'vTable'; + +const initialRecords = [ + { id: 0, name: 'A' }, + { id: 1, name: 'B' }, + { id: 2, name: 'C' }, + { id: 3, name: 'D' }, + { id: 4, name: 'E' } +]; + +const removeDemoToolbar = () => { + document.getElementById('issue5214Toolbar')?.remove(); +}; + +const setStatus = (message: string, pass: boolean) => { + const statusNode = document.getElementById('issue5214Status'); + if (!statusNode) { + return; + } + statusNode.textContent = message; + statusNode.style.color = pass ? '#237804' : '#cf1322'; +}; + +export function createTable() { + removeDemoToolbar(); + + const container = document.getElementById(CONTAINER_ID)!; + container.style.width = '640px'; + container.style.height = '360px'; + + const toolbar = document.createElement('div'); + toolbar.id = 'issue5214Toolbar'; + toolbar.style.cssText = [ + 'display: flex', + 'gap: 8px', + 'align-items: center', + 'height: 48px', + 'font-size: 12px' + ].join(';'); + toolbar.innerHTML = ` + + + 预期:删除 id=1,2,3,仅保留 0 和 4。 + + `; + container.before(toolbar); + + const seriesNumberPlugin = new TableSeriesNumber({ + rowCount: 5, + colCount: 2 + }); + + const tableInstance = new VTable.ListTable({ + container, + showHeader: false, + columns: [ + { field: 'id', title: 'ID', width: 120 }, + { field: 'name', title: 'Name', width: 160 } + ], + records: initialRecords.map(record => ({ ...record })), + syncRecordOperationsToSourceRecords: true, + plugins: [seriesNumberPlugin], + defaultRowHeight: 36 + }); + + const getRecordIds = () => tableInstance.records.map((record: { id: number }) => record.id); + const updateStatusFromRecords = (prefix: string, pass: boolean) => { + setStatus(`${prefix} | records=[${getRecordIds().join(',')}]`, pass); + }; + + document.getElementById('issue5214Reset')?.addEventListener('click', () => { + tableInstance.setRecords(initialRecords.map(record => ({ ...record }))); + updateStatusFromRecords('RESET', true); + }); + + document.getElementById('issue5214Reproduce')?.addEventListener('click', () => { + tableInstance.stateManager.select.ranges = [ + { + start: { col: 0, row: 3 }, + end: { col: tableInstance.colCount - 1, row: 1 } + } + ]; + + seriesNumberPlugin['handleSeriesNumberCellRightClick']({ + detail: { + seriesNumberCell: { id: 2, name: 'row-series-number-cell' }, + event: new MouseEvent('contextmenu') + } + }); + + new MenuHandler().handleDeleteRow(tableInstance); + + const ids = getRecordIds(); + const pass = ids.length === 2 && ids[0] === 0 && ids[1] === 4; + updateStatusFromRecords(pass ? 'PASS' : 'FAIL', pass); + }); + + updateStatusFromRecords('READY', true); + + const release = tableInstance.release.bind(tableInstance); + tableInstance.release = () => { + removeDemoToolbar(); + release(); + }; + + (window as any).tableInstance = tableInstance; +} diff --git a/packages/vtable-plugins/demo/menu.ts b/packages/vtable-plugins/demo/menu.ts index f5e19298ca..91bd566855 100644 --- a/packages/vtable-plugins/demo/menu.ts +++ b/packages/vtable-plugins/demo/menu.ts @@ -140,6 +140,10 @@ export const menus = [ path: 'context-menu', name: 'context-menu' }, + { + path: 'context-menu', + name: 'issue-5214-reverse-selected-row-delete' + }, { path: 'table-export', name: 'table-export' From 808ac423ff85612da98a1cc8a99a8ba3d10f50bb Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Mon, 20 Jul 2026 11:58:40 +0800 Subject: [PATCH 09/77] demo: add issue 5213 reproduction --- ...ssue-5213-row-series-number-aggregation.ts | 84 +++++++++++++++++++ packages/vtable/examples/menu.ts | 4 + 2 files changed, 88 insertions(+) create mode 100644 packages/vtable/examples/debug/issue-5213-row-series-number-aggregation.ts diff --git a/packages/vtable/examples/debug/issue-5213-row-series-number-aggregation.ts b/packages/vtable/examples/debug/issue-5213-row-series-number-aggregation.ts new file mode 100644 index 0000000000..42efd2690d --- /dev/null +++ b/packages/vtable/examples/debug/issue-5213-row-series-number-aggregation.ts @@ -0,0 +1,84 @@ +import * as VTable from '../../src'; + +const CONTAINER_ID = 'vTable'; + +const removeDemoToolbar = () => { + document.getElementById('issue5213Toolbar')?.remove(); +}; + +const setStatus = (message: string, pass: boolean) => { + const statusNode = document.getElementById('issue5213Status'); + if (!statusNode) { + return; + } + statusNode.textContent = message; + statusNode.style.color = pass ? '#237804' : '#cf1322'; +}; + +export function createTable() { + removeDemoToolbar(); + + const container = document.getElementById(CONTAINER_ID)!; + container.style.width = '640px'; + container.style.height = '360px'; + + const toolbar = document.createElement('div'); + toolbar.id = 'issue5213Toolbar'; + toolbar.style.cssText = [ + 'display: flex', + 'gap: 8px', + 'align-items: center', + 'height: 40px', + 'font-size: 12px' + ].join(';'); + toolbar.innerHTML = ` + + 预期:聚合行序号列为空;旧逻辑会显示行号。 + + `; + container.before(toolbar); + + const tableInstance = new VTable.ListTable(container, { + records: [{ value: 1 }, { value: 2 }], + columns: [ + { + field: 'value', + title: 'Value', + width: 160, + aggregation: { + aggregationType: VTable.TYPES.AggregationType.SUM + } + } + ], + rowSeriesNumber: { + title: 'No.' + }, + defaultRowHeight: 36 + }); + + const checkAggregationSeriesNumber = () => { + const aggregationRow = tableInstance.rowCount - 1; + const displayValue = tableInstance.getCellValue(0, aggregationRow); + const originValue = tableInstance.getCellOriginValue(0, aggregationRow); + const isAggregation = tableInstance.internalProps.layoutMap.isAggregation(0, aggregationRow); + const pass = isAggregation && displayValue === '' && originValue === ''; + + setStatus( + `${pass ? 'PASS' : 'FAIL'} | aggregationRow=${aggregationRow}, display=${String(displayValue)}, origin=${String( + originValue + )}`, + pass + ); + }; + + document.getElementById('issue5213Check')?.addEventListener('click', checkAggregationSeriesNumber); + requestAnimationFrame(checkAggregationSeriesNumber); + + const release = tableInstance.release.bind(tableInstance); + tableInstance.release = () => { + removeDemoToolbar(); + release(); + }; + + (window as any).tableInstance = tableInstance; +} diff --git a/packages/vtable/examples/menu.ts b/packages/vtable/examples/menu.ts index 5f040cb70a..2474d4e3ae 100644 --- a/packages/vtable/examples/menu.ts +++ b/packages/vtable/examples/menu.ts @@ -50,6 +50,10 @@ export const menus = [ path: 'debug', name: 'issue-5187-max-frozen-width' }, + { + path: 'debug', + name: 'issue-5213-row-series-number-aggregation' + }, { path: 'debug', name: 'header-frame-border-null-color' From 69fe924a400283cfcbd58a7e999309dc19d92654 Mon Sep 17 00:00:00 2001 From: biubiukam Date: Sun, 26 Jul 2026 15:54:33 +0800 Subject: [PATCH 10/77] fix: prevent page scroll after canceling table rotation --- ...ue-5235-rotate-wheel_2026-07-25-12-00.json | 11 +++ .../__tests__/rotate-table-plugin.test.ts | 88 +++++++++++++++++++ packages/vtable-plugins/src/rotate-table.ts | 1 - 3 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 common/changes/@visactor/vtable-plugins/fix-issue-5235-rotate-wheel_2026-07-25-12-00.json create mode 100644 packages/vtable-plugins/__tests__/rotate-table-plugin.test.ts diff --git a/common/changes/@visactor/vtable-plugins/fix-issue-5235-rotate-wheel_2026-07-25-12-00.json b/common/changes/@visactor/vtable-plugins/fix-issue-5235-rotate-wheel_2026-07-25-12-00.json new file mode 100644 index 0000000000..3096575108 --- /dev/null +++ b/common/changes/@visactor/vtable-plugins/fix-issue-5235-rotate-wheel_2026-07-25-12-00.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@visactor/vtable-plugins", + "comment": "fix: keep wheel events cancelable after restoring a rotated table (GitHub #5235)", + "type": "patch" + } + ], + "packageName": "@visactor/vtable-plugins", + "email": "biukam.w@gmail.com" +} diff --git a/packages/vtable-plugins/__tests__/rotate-table-plugin.test.ts b/packages/vtable-plugins/__tests__/rotate-table-plugin.test.ts new file mode 100644 index 0000000000..3fd7f61bda --- /dev/null +++ b/packages/vtable-plugins/__tests__/rotate-table-plugin.test.ts @@ -0,0 +1,88 @@ +// @ts-nocheck +jest.mock('@visactor/vtable', () => { + const vrender = jest.requireActual('../../vtable/src/vrender'); + + return { + matrixAllocate: vrender.matrixAllocate, + transformPointForCanvas: vrender.transformPointForCanvas, + mapToCanvasPointForCanvas: vrender.mapToCanvasPointForCanvas, + registerGlobalEventTransformer: vrender.registerGlobalEventTransformer, + registerWindowEventTransformer: vrender.registerWindowEventTransformer, + vglobal: { + mapToCanvasPoint: jest.fn(), + setEventListenerTransformer: jest.fn() + }, + TABLE_EVENT_TYPE: { + INITIALIZED: 'initialized' + } + }; +}); + +import { vglobal } from '@visactor/vtable'; +import { cancelTransform } from '../src/rotate-table'; + +global.__VERSION__ = 'none'; + +describe('RotateTablePlugin cancel transform - issue #5235', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + test('keeps wheel preventDefault connected to the native event after canceling rotation', () => { + const tableElement = document.createElement('div'); + jest.spyOn(tableElement, 'getBoundingClientRect').mockReturnValue({ + x: 40, + y: 80, + left: 40, + top: 80, + right: 640, + bottom: 480, + width: 600, + height: 400, + toJSON: () => ({}) + }); + + const originalMapToCanvasPoint = vglobal.mapToCanvasPoint; + const windowSetEventListenerTransformer = jest.fn(); + let globalTransformer; + + jest.spyOn(vglobal, 'setEventListenerTransformer').mockImplementation(transformer => { + globalTransformer = transformer; + }); + + const table = { + rotateDegree: 90, + getElement: () => tableElement, + scenegraph: { + stage: { + window: { + setEventListenerTransformer: windowSetEventListenerTransformer + } + } + }, + pluginManager: { + getPluginByName: () => ({ + vglobal_mapToCanvasPoint: originalMapToCanvasPoint + }) + } + }; + + cancelTransform.call(table, document.createElement('div')); + + const nativeWheelEvent = new WheelEvent('wheel', { + cancelable: true, + clientX: 120, + clientY: 160, + deltaY: 40 + }); + const transformedWheelEvent = globalTransformer(nativeWheelEvent); + + transformedWheelEvent.preventDefault(); + + expect(transformedWheelEvent).toBe(nativeWheelEvent); + expect(nativeWheelEvent.defaultPrevented).toBe(true); + expect(windowSetEventListenerTransformer).toHaveBeenCalledTimes(1); + expect(table.rotateDegree).toBe(0); + expect(vglobal.mapToCanvasPoint).toBe(originalMapToCanvasPoint); + }); +}); diff --git a/packages/vtable-plugins/src/rotate-table.ts b/packages/vtable-plugins/src/rotate-table.ts index 95955bc00d..582c154a2d 100644 --- a/packages/vtable-plugins/src/rotate-table.ts +++ b/packages/vtable-plugins/src/rotate-table.ts @@ -154,7 +154,6 @@ export function cancelTransform(this: ListTable, rotateDom: HTMLElement) { }; const getMatrix = () => { const matrix = matrixAllocate.allocate(1, 0, 0, 1, 0, 0); - matrix.translate(x1, y1); return matrix; }; registerGlobalEventTransformer(vglobal, this.getElement(), getMatrix, getRect as any, transformPointForCanvas); From 4e15ee6eb27224f167667c440d73d7d0354572ff Mon Sep 17 00:00:00 2001 From: biubiukam Date: Mon, 27 Jul 2026 15:57:32 +0800 Subject: [PATCH 11/77] fix: refresh functional icons after theme updates --- ...unctional-icon-theme_2026-07-27-15-36.json | 11 +++ .../__tests__/functional-icons-theme.test.ts | 89 +++++++++++++++++++ .../vtable/src/body-helper/body-helper.ts | 4 + packages/vtable/src/core/BaseTable.ts | 14 ++- .../src/core/row-series-number-helper.ts | 4 + .../vtable/src/header-helper/header-helper.ts | 4 + 6 files changed, 122 insertions(+), 4 deletions(-) create mode 100644 common/changes/@visactor/vtable/fix-issue-4816-functional-icon-theme_2026-07-27-15-36.json create mode 100644 packages/vtable/__tests__/functional-icons-theme.test.ts diff --git a/common/changes/@visactor/vtable/fix-issue-4816-functional-icon-theme_2026-07-27-15-36.json b/common/changes/@visactor/vtable/fix-issue-4816-functional-icon-theme_2026-07-27-15-36.json new file mode 100644 index 0000000000..7db46a9b5c --- /dev/null +++ b/common/changes/@visactor/vtable/fix-issue-4816-functional-icon-theme_2026-07-27-15-36.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "fix: refresh functional icons after theme updates", + "type": "none", + "packageName": "@visactor/vtable" + } + ], + "packageName": "@visactor/vtable", + "email": "biukam.w@gmail.com" +} diff --git a/packages/vtable/__tests__/functional-icons-theme.test.ts b/packages/vtable/__tests__/functional-icons-theme.test.ts new file mode 100644 index 0000000000..97dba2dc48 --- /dev/null +++ b/packages/vtable/__tests__/functional-icons-theme.test.ts @@ -0,0 +1,89 @@ +// @ts-nocheck +import * as VTable from '../src'; +import { createDiv, removeDom } from './dom'; + +global.__VERSION__ = 'none'; + +describe('functional icon theme updates', () => { + let container: HTMLElement; + let table: VTable.ListTable; + + const columns = [{ field: 'name', title: 'Name', sort: true, tree: true }]; + const records = [{ name: 'Parent', children: [{ name: 'Child' }] }]; + + const createTable = () => { + container = createDiv(); + container.style.width = '600px'; + container.style.height = '400px'; + table = new VTable.ListTable(container, { + columns, + records, + rowSeriesNumber: { dragOrder: true }, + theme: { + functionalIconsStyle: { + sort_color: '#111111', + collapse_color: '#222222', + dragReorder_color: '#333333' + } + } + }); + }; + + afterEach(() => { + table?.release(); + removeDom(container); + }); + + test('refreshes functional icons after updateOption', () => { + createTable(); + + table.updateOption({ + columns, + records, + rowSeriesNumber: { dragOrder: true }, + theme: { + functionalIconsStyle: { + sort_color: '#123456', + collapse_color: '#234567', + dragReorder_color: '#345678' + } + } + }); + + expect(table.internalProps.headerHelper.normalIcon.svg).toContain('#123456'); + expect(table.internalProps.bodyHelper.collapseIcon.svg).toContain('#234567'); + expect(table.internalProps.rowSeriesNumberHelper.dragReorderIconName.svg).toContain('#345678'); + }); + + test('refreshes functional icons after updateTheme', () => { + createTable(); + + table.updateTheme({ + functionalIconsStyle: { + sort_color: '#456789', + collapse_color: '#56789a', + dragReorder_color: '#6789ab' + } + }); + + expect(table.internalProps.headerHelper.normalIcon.svg).toContain('#456789'); + expect(table.internalProps.bodyHelper.collapseIcon.svg).toContain('#56789a'); + expect(table.internalProps.rowSeriesNumberHelper.dragReorderIconName.svg).toContain('#6789ab'); + }); + + test('refreshes functional icons after setting theme', () => { + createTable(); + + table.theme = { + functionalIconsStyle: { + sort_color: '#789abc', + collapse_color: '#89abcd', + dragReorder_color: '#9abcde' + } + }; + + expect(table.internalProps.headerHelper.normalIcon.svg).toContain('#789abc'); + expect(table.internalProps.bodyHelper.collapseIcon.svg).toContain('#89abcd'); + expect(table.internalProps.rowSeriesNumberHelper.dragReorderIconName.svg).toContain('#9abcde'); + }); +}); diff --git a/packages/vtable/src/body-helper/body-helper.ts b/packages/vtable/src/body-helper/body-helper.ts index 9047a93878..9a5ab4e6b9 100644 --- a/packages/vtable/src/body-helper/body-helper.ts +++ b/packages/vtable/src/body-helper/body-helper.ts @@ -19,6 +19,10 @@ export class BodyHelper { _table: BaseTableAPI; constructor(_table: BaseTableAPI) { this._table = _table; + this.updateIcons(); + } + + updateIcons() { const regedIcons = registerIcons.get(); //展开折叠按钮 this.expandIcon = regedIcons[InternalIconName.expandIconName] as SvgIcon; diff --git a/packages/vtable/src/core/BaseTable.ts b/packages/vtable/src/core/BaseTable.ts index 7230e26571..587f126314 100644 --- a/packages/vtable/src/core/BaseTable.ts +++ b/packages/vtable/src/core/BaseTable.ts @@ -503,7 +503,7 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { internalProps.focusedTable = false; internalProps.theme = themes.of(options.theme ?? themes.DEFAULT); //原来在listTable文件中 internalProps.theme.isPivot = this.isPivotTable(); - setIconColor(internalProps.theme.functionalIconsStyle); + this._updateFunctionalIcons(); if (container) { // 先清空 if (clearDOM) { @@ -2938,7 +2938,7 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { internalProps.theme = themes.of(options.theme ?? themes.DEFAULT); internalProps.theme.isPivot = this.isPivotTable(); - setIconColor(internalProps.theme.functionalIconsStyle); + this._updateFunctionalIcons(); this.scenegraph.updateStageBackground(); // this._updateSize(); //设置是否自动撑开的配置 @@ -3783,6 +3783,12 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { /** * 获取当前使用的主题 */ + private _updateFunctionalIcons() { + setIconColor(this.internalProps.theme.functionalIconsStyle); + this.internalProps.headerHelper?.updateIcons(); + this.internalProps.bodyHelper?.updateIcons(); + this.internalProps.rowSeriesNumberHelper?.updateIcons(); + } get theme(): TableTheme { return this.internalProps.theme; } @@ -3790,7 +3796,7 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { this.internalProps.theme = themes.of(theme ?? themes.DEFAULT); this.internalProps.theme.isPivot = this.isPivotTable(); this.options.theme = theme; - setIconColor(this.internalProps.theme.functionalIconsStyle); + this._updateFunctionalIcons(); } /** * 设置主题 @@ -3799,7 +3805,7 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { const oldHoverState = { col: this.stateManager.hover.cellPos.col, row: this.stateManager.hover.cellPos.row }; this.internalProps.theme = themes.of(theme ?? themes.DEFAULT); this.internalProps.theme.isPivot = this.isPivotTable(); - setIconColor(this.internalProps.theme.functionalIconsStyle); + this._updateFunctionalIcons(); this.options.theme = theme; this.scenegraph.updateComponent(); this.scenegraph.updateStageBackground(); diff --git a/packages/vtable/src/core/row-series-number-helper.ts b/packages/vtable/src/core/row-series-number-helper.ts index da83ab292b..3975a5d3e6 100644 --- a/packages/vtable/src/core/row-series-number-helper.ts +++ b/packages/vtable/src/core/row-series-number-helper.ts @@ -17,6 +17,10 @@ export class RowSeriesNumberHelper { _table: BaseTableAPI; constructor(_table: BaseTableAPI) { this._table = _table; + this.updateIcons(); + } + + updateIcons() { const regedIcons = registerIcons.get(); this.dragReorderIconName = regedIcons[InternalIconName.dragReorderIconName] as SvgIcon; diff --git a/packages/vtable/src/header-helper/header-helper.ts b/packages/vtable/src/header-helper/header-helper.ts index a448fe0033..4c883780f4 100644 --- a/packages/vtable/src/header-helper/header-helper.ts +++ b/packages/vtable/src/header-helper/header-helper.ts @@ -38,6 +38,10 @@ export class HeaderHelper { _table: BaseTableAPI; constructor(_table: BaseTableAPI) { this._table = _table; + this.updateIcons(); + } + + updateIcons() { const regedIcons = registerIcons.get(); //pin默认值 this.freezeIcon = regedIcons[InternalIconName.freezeIconName] as SvgIcon; From bb335cfad608bef3fa18432c9913246c2f15d2d0 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Mon, 27 Jul 2026 15:59:34 +0800 Subject: [PATCH 12/77] fix: recalculate formula chains --- ...e-5234-formula-chain_2026-07-27-15-55.json | 11 ++ .../recalculation-issue-test.test.ts | 18 ++ packages/vtable-sheet/examples/menu.ts | 4 + .../sheet/issue-5234-formula-chain.ts | 186 ++++++++++++++++++ .../src/formula/formula-engine.ts | 8 +- 5 files changed, 222 insertions(+), 5 deletions(-) create mode 100644 common/changes/@visactor/vtable-sheet/fix-issue-5234-formula-chain_2026-07-27-15-55.json create mode 100644 packages/vtable-sheet/examples/sheet/issue-5234-formula-chain.ts diff --git a/common/changes/@visactor/vtable-sheet/fix-issue-5234-formula-chain_2026-07-27-15-55.json b/common/changes/@visactor/vtable-sheet/fix-issue-5234-formula-chain_2026-07-27-15-55.json new file mode 100644 index 0000000000..ab121f9786 --- /dev/null +++ b/common/changes/@visactor/vtable-sheet/fix-issue-5234-formula-chain_2026-07-27-15-55.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@visactor/vtable-sheet", + "comment": "fix: recalculate multi-level formula dependents", + "type": "patch" + } + ], + "packageName": "@visactor/vtable-sheet", + "email": "892739385@qq.com" +} diff --git a/packages/vtable-sheet/__tests__/recalculation-issue-test.test.ts b/packages/vtable-sheet/__tests__/recalculation-issue-test.test.ts index 74519b9fbd..2e6f13c6d0 100644 --- a/packages/vtable-sheet/__tests__/recalculation-issue-test.test.ts +++ b/packages/vtable-sheet/__tests__/recalculation-issue-test.test.ts @@ -71,6 +71,24 @@ describe('Recalculation Chain Issue Test', () => { } }); + test('should recalculate downstream formulas after an intermediate formula has a cached value', () => { + engine.setCellContent({ sheet: 'Sheet1', row: 1, col: 1 }, 1); // B2 + engine.setCellContent({ sheet: 'Sheet1', row: 2, col: 1 }, 10); // B3 + engine.setCellContent({ sheet: 'Sheet1', row: 1, col: 2 }, '=B2'); // C2 + engine.setCellContent({ sheet: 'Sheet1', row: 1, col: 3 }, '=C2+B3'); // D2 + + expect(engine.getCellValue({ sheet: 'Sheet1', row: 1, col: 2 })).toEqual({ value: 1, error: undefined }); + expect(engine.getCellValue({ sheet: 'Sheet1', row: 1, col: 3 })).toEqual({ value: 11, error: undefined }); + + engine.setCellContent({ sheet: 'Sheet1', row: 2, col: 1 }, 20); + expect(engine.getCellValue({ sheet: 'Sheet1', row: 1, col: 3 })).toEqual({ value: 21, error: undefined }); + + engine.setCellContent({ sheet: 'Sheet1', row: 1, col: 1 }, 5); + + expect(engine.getCellValue({ sheet: 'Sheet1', row: 1, col: 2 })).toEqual({ value: 5, error: undefined }); + expect(engine.getCellValue({ sheet: 'Sheet1', row: 1, col: 3 })).toEqual({ value: 25, error: undefined }); + }); + test('测试重新计算过程中的中间状态', () => { console.log('\n=== 测试重新计算过程中的中间状态 ==='); diff --git a/packages/vtable-sheet/examples/menu.ts b/packages/vtable-sheet/examples/menu.ts index 2265304622..2ed8ae521c 100644 --- a/packages/vtable-sheet/examples/menu.ts +++ b/packages/vtable-sheet/examples/menu.ts @@ -30,5 +30,9 @@ export const menus = [ { path: 'sheet', name: 'issue-5204-formula-manager' + }, + { + path: 'sheet', + name: 'issue-5234-formula-chain' } ]; diff --git a/packages/vtable-sheet/examples/sheet/issue-5234-formula-chain.ts b/packages/vtable-sheet/examples/sheet/issue-5234-formula-chain.ts new file mode 100644 index 0000000000..6c65222ec3 --- /dev/null +++ b/packages/vtable-sheet/examples/sheet/issue-5234-formula-chain.ts @@ -0,0 +1,186 @@ +import { VTableSheet } from '../../src/index'; + +const CONTAINER_ID = 'vTable'; +const SHEET_KEY = 'issue5234'; + +const removeDemoToolbar = () => { + document.getElementById('issue5234Toolbar')?.remove(); +}; + +const setStatus = (message: string, pass: boolean) => { + const statusNode = document.getElementById('issue5234Status'); + if (!statusNode) { + return; + } + statusNode.textContent = message; + statusNode.style.color = pass ? '#237804' : '#cf1322'; +}; + +const getActiveWorkSheet = (sheetInstance: VTableSheet) => sheetInstance.getActiveSheet(); + +const getCellValue = (sheetInstance: VTableSheet, col: number, row: number) => { + const worksheet = getActiveWorkSheet(sheetInstance); + return worksheet.tableInstance?.getCellValue(col, row); +}; + +const getFormulaValue = (sheetInstance: VTableSheet, col: number, row: number) => + sheetInstance.formulaManager.getCellValue({ + sheet: SHEET_KEY, + row, + col + }).value; + +const collectDependents = ( + sheetInstance: VTableSheet, + cell: { sheet: string; row: number; col: number }, + visited = new Set() +): Array<{ sheet: string; row: number; col: number }> => { + const cellKey = `${cell.sheet}!${cell.row},${cell.col}`; + if (visited.has(cellKey)) { + return []; + } + visited.add(cellKey); + + const directDependents = sheetInstance.formulaManager.getCellDependents(cell); + return directDependents.reduce>( + (dependents, dependent) => dependents.concat(dependent, collectDependents(sheetInstance, dependent, visited)), + [] + ); +}; + +const syncDependentsToTable = (sheetInstance: VTableSheet, col: number, row: number) => { + const worksheet = getActiveWorkSheet(sheetInstance); + const dependents = collectDependents(sheetInstance, { + sheet: SHEET_KEY, + row, + col + }); + + dependents.forEach(dependent => { + const result = sheetInstance.formulaManager.getCellValue(dependent); + worksheet.tableInstance?.changeCellValue( + dependent.col, + dependent.row, + result.error ? '#ERROR!' : result.value, + false, + false + ); + }); +}; + +const setCell = (sheetInstance: VTableSheet, col: number, row: number, value: unknown) => { + const worksheet = getActiveWorkSheet(sheetInstance); + sheetInstance.formulaManager.setCellContent( + { + sheet: SHEET_KEY, + row, + col + }, + value + ); + + if (typeof value === 'string' && value.startsWith('=')) { + const result = sheetInstance.formulaManager.getCellValue({ + sheet: SHEET_KEY, + row, + col + }); + worksheet.tableInstance?.changeCellValue(col, row, result.error ? '#ERROR!' : result.value, false, false); + } else { + worksheet.tableInstance?.changeCellValue(col, row, value, false, false); + } + + syncDependentsToTable(sheetInstance, col, row); +}; + +export function createTable() { + removeDemoToolbar(); + + const container = document.getElementById(CONTAINER_ID)!; + container.style.width = '760px'; + container.style.height = '420px'; + + const toolbar = document.createElement('div'); + toolbar.id = 'issue5234Toolbar'; + toolbar.style.cssText = ['display: flex', 'gap: 8px', 'align-items: center', 'height: 48px', 'font-size: 12px'].join( + ';' + ); + toolbar.innerHTML = ` + + + + 公式:C2=B2,D2=C2+B3;预期最终 C2=5、D2=25。 + + `; + container.before(toolbar); + + const sheetInstance = new VTableSheet(container, { + showFormulaBar: true, + showSheetTab: true, + defaultRowHeight: 36, + defaultColWidth: 120, + sheets: [ + { + sheetKey: SHEET_KEY, + sheetTitle: 'Issue 5234', + active: true, + rowCount: 12, + columnCount: 8, + showHeader: false, + data: [ + ['A', 'B', 'C', 'D'], + ['', 1, '', ''], + ['', 10, '', ''] + ] + } + ] + }); + + const resetFormulaChain = () => { + setCell(sheetInstance, 1, 1, 1); // B2 + setCell(sheetInstance, 1, 2, 10); // B3 + setCell(sheetInstance, 2, 1, '=B2'); // C2 + setCell(sheetInstance, 3, 1, '=C2+B3'); // D2 + + const c2 = getCellValue(sheetInstance, 2, 1); + const d2 = getCellValue(sheetInstance, 3, 1); + setStatus(`READY | C2=${String(c2)}, D2=${String(d2)}`, true); + }; + + const changeB3 = () => { + setCell(sheetInstance, 1, 2, 20); + const d2 = getCellValue(sheetInstance, 3, 1); + setStatus(`STEP1 | B3=20, D2=${String(d2)},预期 21`, d2 === 21); + }; + + const changeB2AndCheck = () => { + setCell(sheetInstance, 1, 1, 5); + + const c2 = getCellValue(sheetInstance, 2, 1); + const d2 = getCellValue(sheetInstance, 3, 1); + const engineC2 = getFormulaValue(sheetInstance, 2, 1); + const engineD2 = getFormulaValue(sheetInstance, 3, 1); + const pass = c2 === 5 && d2 === 25 && engineC2 === 5 && engineD2 === 25; + + setStatus( + `${pass ? 'PASS' : 'FAIL'} | table C2=${String(c2)}, D2=${String(d2)}; engine C2=${String(engineC2)}, D2=${String( + engineD2 + )}; 预期 C2=5, D2=25`, + pass + ); + }; + + document.getElementById('issue5234Reset')?.addEventListener('click', resetFormulaChain); + document.getElementById('issue5234ChangeB3')?.addEventListener('click', changeB3); + document.getElementById('issue5234ChangeB2')?.addEventListener('click', changeB2AndCheck); + + resetFormulaChain(); + + const release = sheetInstance.release.bind(sheetInstance); + sheetInstance.release = () => { + removeDemoToolbar(); + release(); + }; + + (window as any).sheetInstance = sheetInstance; +} diff --git a/packages/vtable-sheet/src/formula/formula-engine.ts b/packages/vtable-sheet/src/formula/formula-engine.ts index 52e17f9b97..b0a470651a 100644 --- a/packages/vtable-sheet/src/formula/formula-engine.ts +++ b/packages/vtable-sheet/src/formula/formula-engine.ts @@ -2487,11 +2487,6 @@ export class FormulaEngine { private recalculateDependentsWithTracking(changedCell: FormulaCell, visited: Set): void { const cellKey = this.getCellKey(changedCell); - // 防止循环依赖导致的无限递归 - if (visited.has(cellKey)) { - return; - } - const dependents = this.dependents.get(cellKey); if (!dependents || dependents.size === 0) { @@ -2502,6 +2497,9 @@ export class FormulaEngine { const sortedDependents = this.sortCellsByDependency([...dependents]); for (const dependentKey of sortedDependents) { + if (visited.has(dependentKey)) { + continue; + } this.recalculateSingleCellWithTracking(dependentKey, visited); } } From 2231a6f849af75f7e91f889e92484c5a4ded1f7b Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Mon, 27 Jul 2026 16:49:55 +0800 Subject: [PATCH 13/77] test: add issue 4816 functional icon reproduction --- ...unctional-icon-theme_2026-07-27-15-36.json | 2 +- .../issue-4816-functional-icons-theme.ts | 133 ++++++++++++++++++ packages/vtable/examples/menu.ts | 4 + 3 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 packages/vtable/examples/debug/issue-4816-functional-icons-theme.ts diff --git a/common/changes/@visactor/vtable/fix-issue-4816-functional-icon-theme_2026-07-27-15-36.json b/common/changes/@visactor/vtable/fix-issue-4816-functional-icon-theme_2026-07-27-15-36.json index 7db46a9b5c..0f68b64d76 100644 --- a/common/changes/@visactor/vtable/fix-issue-4816-functional-icon-theme_2026-07-27-15-36.json +++ b/common/changes/@visactor/vtable/fix-issue-4816-functional-icon-theme_2026-07-27-15-36.json @@ -2,7 +2,7 @@ "changes": [ { "comment": "fix: refresh functional icons after theme updates", - "type": "none", + "type": "patch", "packageName": "@visactor/vtable" } ], diff --git a/packages/vtable/examples/debug/issue-4816-functional-icons-theme.ts b/packages/vtable/examples/debug/issue-4816-functional-icons-theme.ts new file mode 100644 index 0000000000..64c49a8eea --- /dev/null +++ b/packages/vtable/examples/debug/issue-4816-functional-icons-theme.ts @@ -0,0 +1,133 @@ +import * as VTable from '../../src'; + +const CONTAINER_ID = 'vTable'; + +const columns: VTable.ColumnsDefine = [ + { + field: 'name', + title: 'Name', + width: 240, + sort: true, + tree: true + } +]; + +const records = [ + { + name: 'Parent', + children: [{ name: 'Child' }] + } +]; + +const initialIconColors = { + sort_color: '#111111', + collapse_color: '#222222', + dragReorder_color: '#333333' +}; + +const updatedIconColors = { + sort_color: '#123456', + collapse_color: '#234567', + dragReorder_color: '#345678' +}; + +const removeToolbar = () => { + document.getElementById('issue4816Toolbar')?.remove(); +}; + +const setStatus = (message: string, pass: boolean) => { + const status = document.getElementById('issue4816Status'); + if (!status) { + return; + } + status.textContent = message; + status.style.color = pass ? '#237804' : '#cf1322'; +}; + +const getIconSvgState = (tableInstance: VTable.ListTable) => { + const internalProps = (tableInstance as any).internalProps; + return { + sort: internalProps.headerHelper.normalIcon.svg, + collapse: internalProps.bodyHelper.collapseIcon.svg, + drag: internalProps.rowSeriesNumberHelper.dragReorderIconName.svg + }; +}; + +const includesColors = (iconState: ReturnType, colors: typeof updatedIconColors) => + iconState.sort.includes(colors.sort_color) && + iconState.collapse.includes(colors.collapse_color) && + iconState.drag.includes(colors.dragReorder_color); + +export function createTable() { + removeToolbar(); + + const container = document.getElementById(CONTAINER_ID)!; + container.style.width = '640px'; + container.style.height = '360px'; + + const toolbar = document.createElement('div'); + toolbar.id = 'issue4816Toolbar'; + toolbar.style.cssText = 'display:flex;gap:8px;align-items:center;height:48px;font-size:12px;'; + toolbar.innerHTML = ` + + + 预期:updateOption 后 sort/collapse/drag 图标 SVG 都使用新颜色。 + + `; + container.before(toolbar); + + const createOption = (functionalIconsStyle: typeof initialIconColors): VTable.ListTableConstructorOptions => ({ + columns, + records, + rowSeriesNumber: { dragOrder: true }, + theme: { + functionalIconsStyle + } + }); + + const tableInstance = new VTable.ListTable(container, createOption(initialIconColors)); + + const checkInitial = () => { + const iconState = getIconSvgState(tableInstance); + const pass = includesColors(iconState, initialIconColors); + const sortMatched = iconState.sort.includes(initialIconColors.sort_color); + const collapseMatched = iconState.collapse.includes(initialIconColors.collapse_color); + const dragMatched = iconState.drag.includes(initialIconColors.dragReorder_color); + + setStatus( + `${pass ? 'PASS' : 'FAIL'} | initial sort=${sortMatched}, collapse=${collapseMatched}, drag=${dragMatched}`, + pass + ); + }; + + const updateAndCheck = () => { + tableInstance.updateOption(createOption(updatedIconColors)); + const iconState = getIconSvgState(tableInstance); + const pass = includesColors(iconState, updatedIconColors); + const sortMatched = iconState.sort.includes(updatedIconColors.sort_color); + const collapseMatched = iconState.collapse.includes(updatedIconColors.collapse_color); + const dragMatched = iconState.drag.includes(updatedIconColors.dragReorder_color); + + setStatus( + `${pass ? 'PASS' : 'FAIL'} | updated sort=${sortMatched}, collapse=${collapseMatched}, drag=${dragMatched}`, + pass + ); + }; + + document.getElementById('issue4816CheckInitial')?.addEventListener('click', checkInitial); + document.getElementById('issue4816Update')?.addEventListener('click', updateAndCheck); + + checkInitial(); + + (window as any).tableInstance = tableInstance; + (window as any).issue4816Run = () => { + updateAndCheck(); + return document.getElementById('issue4816Status')?.textContent; + }; + + const release = tableInstance.release.bind(tableInstance); + tableInstance.release = () => { + removeToolbar(); + release(); + }; +} diff --git a/packages/vtable/examples/menu.ts b/packages/vtable/examples/menu.ts index 2474d4e3ae..f0eacdaba4 100644 --- a/packages/vtable/examples/menu.ts +++ b/packages/vtable/examples/menu.ts @@ -54,6 +54,10 @@ export const menus = [ path: 'debug', name: 'issue-5213-row-series-number-aggregation' }, + { + path: 'debug', + name: 'issue-4816-functional-icons-theme' + }, { path: 'debug', name: 'header-frame-border-null-color' From bc65c01157b4b31a43b13d1b3508dc4f4838d992 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Mon, 27 Jul 2026 16:49:55 +0800 Subject: [PATCH 14/77] test: add issue 5235 rotate wheel reproduction --- packages/vtable-plugins/demo/menu.ts | 4 + .../rotate-table/issue-5235-rotate-wheel.ts | 132 ++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 packages/vtable-plugins/demo/rotate-table/issue-5235-rotate-wheel.ts diff --git a/packages/vtable-plugins/demo/menu.ts b/packages/vtable-plugins/demo/menu.ts index 8c16ad6b42..0d522dd098 100644 --- a/packages/vtable-plugins/demo/menu.ts +++ b/packages/vtable-plugins/demo/menu.ts @@ -87,6 +87,10 @@ export const menus = [ path: 'rotate-table', name: 'rotate-table' }, + { + path: 'rotate-table', + name: 'issue-5235-rotate-wheel' + }, { path: 'table-series-number', name: 'table-series-number' diff --git a/packages/vtable-plugins/demo/rotate-table/issue-5235-rotate-wheel.ts b/packages/vtable-plugins/demo/rotate-table/issue-5235-rotate-wheel.ts new file mode 100644 index 0000000000..1858232ed1 --- /dev/null +++ b/packages/vtable-plugins/demo/rotate-table/issue-5235-rotate-wheel.ts @@ -0,0 +1,132 @@ +import * as VTable from '@visactor/vtable'; +import { RotateTablePlugin } from '../../src'; + +const CONTAINER_ID = 'vTable'; + +const records = Array.from({ length: 80 }, (_, index) => ({ + id: index + 1, + name: `Name ${index + 1}`, + email: `${index + 1}@example.com`, + city: `City ${index + 1}` +})); + +const columns: VTable.ColumnsDefine = [ + { field: 'id', title: 'ID', width: 80, sort: true }, + { field: 'name', title: 'Name', width: 180 }, + { field: 'email', title: 'Email', width: 220 }, + { field: 'city', title: 'City', width: 180 } +]; + +const removeToolbar = () => { + document.getElementById('issue5235Toolbar')?.remove(); + document.getElementById('issue5235Spacer')?.remove(); +}; + +const setStatus = (message: string, pass: boolean) => { + const status = document.getElementById('issue5235Status'); + if (!status) { + return; + } + status.textContent = message; + status.style.color = pass ? '#237804' : '#cf1322'; +}; + +const getRotateDom = () => document.getElementById(CONTAINER_ID) as HTMLElement; + +const dispatchWheelAndCheckPrevented = () => { + const canvas = document.querySelector(`#${CONTAINER_ID} canvas`); + if (!canvas) { + return false; + } + + const rect = canvas.getBoundingClientRect(); + const wheelEvent = new WheelEvent('wheel', { + bubbles: true, + cancelable: true, + clientX: rect.left + rect.width / 2, + clientY: rect.top + rect.height / 2, + deltaY: 80 + }); + + canvas.dispatchEvent(wheelEvent); + return wheelEvent.defaultPrevented; +}; + +export function createTable() { + removeToolbar(); + + const container = document.getElementById(CONTAINER_ID)!; + container.style.width = '720px'; + container.style.height = '360px'; + container.style.margin = '24px auto'; + container.style.border = '1px solid #d9d9d9'; + + const toolbar = document.createElement('div'); + toolbar.id = 'issue5235Toolbar'; + toolbar.style.cssText = 'display:flex;gap:8px;align-items:center;height:48px;font-size:12px;'; + toolbar.innerHTML = ` + + + + 预期:旋转再还原后,表格内 wheel 的原始事件仍会被 preventDefault。 + + `; + container.before(toolbar); + + const spacer = document.createElement('div'); + spacer.id = 'issue5235Spacer'; + spacer.style.cssText = 'height:900px;background:linear-gradient(#fff,#f5f5f5);'; + spacer.textContent = '页面滚动占位:用于观察表格内滚轮是否带动外层页面滚动。'; + container.after(spacer); + + const rotatePlugin = new RotateTablePlugin(); + const tableInstance = new VTable.ListTable(container, { + records, + columns, + widthMode: 'standard', + rowSeriesNumber: {}, + overscrollBehavior: 'none', + plugins: [rotatePlugin] + }); + + const rotateTable = () => { + const rotateDom = getRotateDom(); + const { width, height } = rotateDom.getBoundingClientRect(); + rotateDom.style.width = `${height}px`; + rotateDom.style.height = `${width}px`; + tableInstance.rotate90WithTransform?.(rotateDom); + setStatus('READY | 已旋转,请点击还原后检查滚轮。', true); + }; + + const restoreTable = () => { + const rotateDom = getRotateDom(); + const { width, height } = rotateDom.getBoundingClientRect(); + rotateDom.style.width = `${height}px`; + rotateDom.style.height = `${width}px`; + tableInstance.cancelTransform?.(rotateDom); + setStatus('READY | 已还原,请点击滚轮检查。', true); + }; + + const checkWheel = () => { + const prevented = dispatchWheelAndCheckPrevented(); + setStatus(`${prevented ? 'PASS' : 'FAIL'} | wheel.defaultPrevented=${prevented}`, prevented); + }; + + document.getElementById('issue5235Rotate')?.addEventListener('click', rotateTable); + document.getElementById('issue5235Restore')?.addEventListener('click', restoreTable); + document.getElementById('issue5235Check')?.addEventListener('click', checkWheel); + + (window as any).tableInstance = tableInstance; + (window as any).issue5235Run = () => { + rotateTable(); + restoreTable(); + checkWheel(); + return document.getElementById('issue5235Status')?.textContent; + }; + + const release = tableInstance.release.bind(tableInstance); + tableInstance.release = () => { + removeToolbar(); + release(); + }; +} From d0564a89443c6fa991955bc1625f63eb191c9cd3 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Mon, 27 Jul 2026 18:21:58 +0800 Subject: [PATCH 15/77] fix: handle missing gantt task bar rect --- ...askbar-relative-rect_2026-07-27-18-45.json | 11 ++ docs/assets/api/en/GanttAPI.md | 4 +- docs/assets/api/zh/GanttAPI.md | 4 +- .../gantt/issue-4778-taskbar-relative-rect.ts | 117 ++++++++++++++++++ packages/vtable-gantt/examples/menu.ts | 4 + packages/vtable-gantt/src/Gantt.ts | 7 +- 6 files changed, 141 insertions(+), 6 deletions(-) create mode 100644 common/changes/@visactor/vtable-gantt/fix-issue-4778-taskbar-relative-rect_2026-07-27-18-45.json create mode 100644 packages/vtable-gantt/examples/gantt/issue-4778-taskbar-relative-rect.ts diff --git a/common/changes/@visactor/vtable-gantt/fix-issue-4778-taskbar-relative-rect_2026-07-27-18-45.json b/common/changes/@visactor/vtable-gantt/fix-issue-4778-taskbar-relative-rect_2026-07-27-18-45.json new file mode 100644 index 0000000000..b9ca3c5c70 --- /dev/null +++ b/common/changes/@visactor/vtable-gantt/fix-issue-4778-taskbar-relative-rect_2026-07-27-18-45.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@visactor/vtable-gantt", + "comment": "fix: return null for missing gantt task bars and support sub task rect lookup", + "type": "patch" + } + ], + "packageName": "@visactor/vtable-gantt", + "email": "892739385@qq.com" +} diff --git a/docs/assets/api/en/GanttAPI.md b/docs/assets/api/en/GanttAPI.md index 1b7ab7b384..6bb5f5e492 100644 --- a/docs/assets/api/en/GanttAPI.md +++ b/docs/assets/api/en/GanttAPI.md @@ -149,12 +149,12 @@ Get or set the horizontal scroll value to a specified position. Get the position of the task bar. The position relative to the top-left corner of the Gantt chart. ``` - getTaskBarRelativeRect:(index: number) =>{ + getTaskBarRelativeRect:(index: number, sub_task_index?: number) =>{ left: number; top: number; width: number; height: number; - } + } | null ``` ## Events diff --git a/docs/assets/api/zh/GanttAPI.md b/docs/assets/api/zh/GanttAPI.md index be2c0b889f..13d90f4c1b 100644 --- a/docs/assets/api/zh/GanttAPI.md +++ b/docs/assets/api/zh/GanttAPI.md @@ -150,12 +150,12 @@ const info = ganttInstance.getBaselineInfoByTaskListIndex(0); 获取任务条的位置。相对应甘特图表左上角的位置。 ``` - getTaskBarRelativeRect:(index: number) =>{ + getTaskBarRelativeRect:(index: number, sub_task_index?: number) =>{ left: number; top: number; width: number; height: number; - } + } | null ``` ## Events diff --git a/packages/vtable-gantt/examples/gantt/issue-4778-taskbar-relative-rect.ts b/packages/vtable-gantt/examples/gantt/issue-4778-taskbar-relative-rect.ts new file mode 100644 index 0000000000..3cbc9ec1a3 --- /dev/null +++ b/packages/vtable-gantt/examples/gantt/issue-4778-taskbar-relative-rect.ts @@ -0,0 +1,117 @@ +import type { ColumnsDefine } from '@visactor/vtable'; +import type { GanttConstructorOptions } from '../../src/index'; +import { Gantt } from '../../src/index'; +import { TasksShowMode } from '../../src/ts-types'; + +const CONTAINER_ID = 'vTable'; + +const createStatusBar = () => { + const container = document.getElementById(CONTAINER_ID)!; + const status = document.createElement('div'); + status.id = 'issue4778Status'; + status.style.cssText = 'height: 32px; line-height: 32px; font-size: 13px; color: #333;'; + status.textContent = 'Click "Check getTaskBarRelativeRect" to verify issue #4778.'; + + const button = document.createElement('button'); + button.textContent = 'Check getTaskBarRelativeRect'; + button.style.cssText = 'margin: 0 0 8px 8px;'; + button.onclick = () => checkRects(); + + container.parentElement?.insertBefore(status, container); + status.appendChild(button); +}; + +const isValidRect = (rect: any) => + rect && + Number.isFinite(rect.left) && + Number.isFinite(rect.top) && + Number.isFinite(rect.width) && + Number.isFinite(rect.height); + +const checkRects = () => { + const ganttInstance = (window as any).ganttInstance as Gantt; + const status = document.getElementById('issue4778Status')!; + let emptyRect: any; + let firstChildRect: any; + let secondChildRect: any; + + try { + emptyRect = ganttInstance.getTaskBarRelativeRect(1); + firstChildRect = ganttInstance.getTaskBarRelativeRect(0, 0); + secondChildRect = ganttInstance.getTaskBarRelativeRect(0, 1); + } catch (err) { + status.textContent = `FAIL | ${(err as Error).message}`; + return status.textContent; + } + + const pass = + emptyRect === null && + isValidRect(firstChildRect) && + isValidRect(secondChildRect) && + firstChildRect.left !== secondChildRect.left; + + status.textContent = `${pass ? 'PASS' : 'FAIL'} | empty=${JSON.stringify(emptyRect)}, first=${JSON.stringify( + firstChildRect + )}, second=${JSON.stringify(secondChildRect)}`; + return status.textContent; +}; + +export function createTable() { + const records = [ + { + id: 1, + title: 'Parent with compact subtasks', + children: [ + { id: 11, title: 'Sub task A', start: '2024-07-01', end: '2024-07-04', progress: 30 }, + { id: 12, title: 'Sub task B', start: '2024-07-08', end: '2024-07-12', progress: 60 } + ] + }, + { + id: 2, + title: 'Empty task row' + } + ]; + + const columns: ColumnsDefine = [ + { field: 'title', title: 'title', width: 220 }, + { field: 'start', title: 'start', width: 120 }, + { field: 'end', title: 'end', width: 120 }, + { field: 'progress', title: 'progress', width: 100 } + ]; + + const option: GanttConstructorOptions = { + records, + taskListTable: { + columns, + tableWidth: 260 + }, + taskKeyField: 'id', + tasksShowMode: TasksShowMode.Sub_Tasks_Compact, + taskBar: { + startDateField: 'start', + endDateField: 'end', + progressField: 'progress' + }, + minDate: '2024-07-01', + maxDate: '2024-07-20', + timelineHeader: { + colWidth: 36, + scales: [{ unit: 'day', step: 1 }] + }, + grid: { + verticalLine: { + lineWidth: 1, + lineColor: '#e1e4e8' + }, + horizontalLine: { + lineWidth: 1, + lineColor: '#e1e4e8' + } + } + }; + + createStatusBar(); + const ganttInstance = new Gantt(document.getElementById(CONTAINER_ID)!, option); + (window as any).ganttInstance = ganttInstance; + (window as any).issue4778Run = checkRects; +} diff --git a/packages/vtable-gantt/examples/menu.ts b/packages/vtable-gantt/examples/menu.ts index c5e88f7d7a..1bcfe17a99 100644 --- a/packages/vtable-gantt/examples/menu.ts +++ b/packages/vtable-gantt/examples/menu.ts @@ -178,6 +178,10 @@ export const menus = [ { path: 'gantt', name: 'gantt-issue-5162-sort-drag' + }, + { + path: 'gantt', + name: 'issue-4778-taskbar-relative-rect' } // ] // } diff --git a/packages/vtable-gantt/src/Gantt.ts b/packages/vtable-gantt/src/Gantt.ts index bf77edc652..cbde30c845 100644 --- a/packages/vtable-gantt/src/Gantt.ts +++ b/packages/vtable-gantt/src/Gantt.ts @@ -1637,8 +1637,11 @@ export class Gantt extends EventTarget { this.stateManager.setScrollLeft(value); } /** 获取任务条的位置。相对应甘特图表左上角的位置。 */ - getTaskBarRelativeRect(index: number) { - const taskBarNode = this.scenegraph.taskBar.getTaskBarNodeByIndex(index); + getTaskBarRelativeRect(index: number, sub_task_index?: number) { + const taskBarNode = this.scenegraph.taskBar.getTaskBarNodeByIndex(index, sub_task_index); + if (!taskBarNode) { + return null; + } const left = taskBarNode.attribute.x + this.taskListTableInstance.tableNoFrameWidth + From f67f76b241894d4743cdc045d169e8419434f883 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Mon, 27 Jul 2026 18:26:58 +0800 Subject: [PATCH 16/77] fix: keep master detail expandable after setRecords --- ...r-detail-set-records_2026-07-27-19-20.json | 11 +++ .../issue-5185-set-records-expand.ts | 97 +++++++++++++++++++ packages/vtable-plugins/demo/menu.ts | 4 + .../src/master-detail-plugin/config.ts | 6 +- .../src/master-detail-plugin/core.ts | 28 +++++- .../table-api-extensions.ts | 23 +++++ 6 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 common/changes/@visactor/vtable-plugins/fix-issue-5185-master-detail-set-records_2026-07-27-19-20.json create mode 100644 packages/vtable-plugins/demo/master-detail-plugin/issue-5185-set-records-expand.ts diff --git a/common/changes/@visactor/vtable-plugins/fix-issue-5185-master-detail-set-records_2026-07-27-19-20.json b/common/changes/@visactor/vtable-plugins/fix-issue-5185-master-detail-set-records_2026-07-27-19-20.json new file mode 100644 index 0000000000..f128fbcd56 --- /dev/null +++ b/common/changes/@visactor/vtable-plugins/fix-issue-5185-master-detail-set-records_2026-07-27-19-20.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@visactor/vtable-plugins", + "comment": "fix: keep master detail expandable after list table setRecords", + "type": "patch" + } + ], + "packageName": "@visactor/vtable-plugins", + "email": "892739385@qq.com" +} diff --git a/packages/vtable-plugins/demo/master-detail-plugin/issue-5185-set-records-expand.ts b/packages/vtable-plugins/demo/master-detail-plugin/issue-5185-set-records-expand.ts new file mode 100644 index 0000000000..86c27fe46a --- /dev/null +++ b/packages/vtable-plugins/demo/master-detail-plugin/issue-5185-set-records-expand.ts @@ -0,0 +1,97 @@ +import * as VTable from '@visactor/vtable'; +import { MasterDetailPlugin } from '../../src'; + +const CONTAINER_ID = 'vTable'; + +const columns: VTable.ColumnsDefine = [ + { field: 'name', title: 'Name', width: 180 }, + { field: 'department', title: 'Department', width: 160 }, + { field: 'status', title: 'Status', width: 120 } +]; + +const detailColumns: VTable.ColumnsDefine = [ + { field: 'project', title: 'Project', width: 180 }, + { field: 'role', title: 'Role', width: 140 } +]; + +const createRecords = (prefix: string) => [ + { + id: `${prefix}-1`, + name: `${prefix} Employee 1`, + department: 'Engineering', + status: 'Active', + children: [ + { project: `${prefix} Project A`, role: 'Owner' }, + { project: `${prefix} Project B`, role: 'Reviewer' } + ] + }, + { + id: `${prefix}-2`, + name: `${prefix} Employee 2`, + department: 'Design', + status: 'Active', + children: [{ project: `${prefix} Project C`, role: 'Designer' }] + } +]; + +const createStatusBar = () => { + const container = document.getElementById(CONTAINER_ID)!; + const status = document.createElement('div'); + status.id = 'issue5185Status'; + status.style.cssText = 'height: 32px; line-height: 32px; font-size: 13px; color: #333;'; + status.textContent = 'Click "Check setRecords expand" to verify issue #5185.'; + + const button = document.createElement('button'); + button.textContent = 'Check setRecords expand'; + button.style.cssText = 'margin: 0 0 8px 8px;'; + button.onclick = () => checkSetRecordsExpand(); + + container.parentElement?.insertBefore(status, container); + status.appendChild(button); +}; + +const getSubTableCount = (tableInstance: VTable.ListTable) => + ((tableInstance as any).internalProps.subTableInstances as Map)?.size ?? 0; + +const checkSetRecordsExpand = () => { + const tableInstance = (window as any).tableInstance as VTable.ListTable; + const status = document.getElementById('issue5185Status')!; + + tableInstance.setRecords(createRecords('After')); + tableInstance.toggleHierarchyState(0, tableInstance.columnHeaderLevelCount); + + const subTableCount = getSubTableCount(tableInstance); + const firstRecord = tableInstance.records?.[0] as any; + const pass = subTableCount > 0 && firstRecord?.hierarchyState === VTable.TYPES.HierarchyState.expand; + + status.textContent = `${pass ? 'PASS' : 'FAIL'} | subTableCount=${subTableCount}, hierarchyState=${ + firstRecord?.hierarchyState + }`; + return status.textContent; +}; + +export function createTable() { + const option: VTable.ListTableConstructorOptions = { + records: createRecords('Initial'), + columns, + widthMode: 'standard', + defaultRowHeight: 36, + plugins: [ + new MasterDetailPlugin({ + detailTableOptions: { + columns: detailColumns, + heightMode: 'autoHeight', + defaultRowHeight: 30, + style: { + height: 90 + } + } + }) + ] + }; + + createStatusBar(); + const tableInstance = new VTable.ListTable(document.getElementById(CONTAINER_ID)!, option); + (window as any).tableInstance = tableInstance; + (window as any).issue5185Run = checkSetRecordsExpand; +} diff --git a/packages/vtable-plugins/demo/menu.ts b/packages/vtable-plugins/demo/menu.ts index 8c16ad6b42..2c641801de 100644 --- a/packages/vtable-plugins/demo/menu.ts +++ b/packages/vtable-plugins/demo/menu.ts @@ -131,6 +131,10 @@ export const menus = [ path: 'master-detail-plugin', name: 'master-detail-plugin9' }, + { + path: 'master-detail-plugin', + name: 'issue-5185-set-records-expand' + }, { menu: 'pivot-plugin', children: [ diff --git a/packages/vtable-plugins/src/master-detail-plugin/config.ts b/packages/vtable-plugins/src/master-detail-plugin/config.ts index fe37a9d5b0..3f5552ed7e 100644 --- a/packages/vtable-plugins/src/master-detail-plugin/config.ts +++ b/packages/vtable-plugins/src/master-detail-plugin/config.ts @@ -161,7 +161,7 @@ export class ConfigManager { /** * 处理记录的层级状态 */ - private processRecordsHierarchyStates(records: unknown[]): void { + processRecordsHierarchyStates(records: unknown[], expandInitialRows: boolean = true): void { const HierarchyState = VTable.TYPES.HierarchyState; // 兼容处理headerExpandLevel const hierarchyExpandLevel = this.table.options.hierarchyExpandLevel || this.table.options.headerExpandLevel; @@ -193,7 +193,9 @@ export class ConfigManager { }); }; processRecords(records); - this.performInitialExpansion(); + if (expandInitialRows) { + this.performInitialExpansion(); + } } /** diff --git a/packages/vtable-plugins/src/master-detail-plugin/core.ts b/packages/vtable-plugins/src/master-detail-plugin/core.ts index cea11d6ff5..1a51eb54d7 100644 --- a/packages/vtable-plugins/src/master-detail-plugin/core.ts +++ b/packages/vtable-plugins/src/master-detail-plugin/core.ts @@ -171,13 +171,39 @@ export class MasterDetailPlugin implements pluginsDefinition.IVTablePlugin { collapseRow: (rowIndex: number) => this.collapseRow(rowIndex), updateSubTablePositions: () => this.subTableManager.recalculateAllSubTablePositions(), updateRowHeightForExpand: (rowIndex: number, deltaHeight: number) => - this.updateRowHeightForExpand(rowIndex, deltaHeight) + this.updateRowHeightForExpand(rowIndex, deltaHeight), + resetMasterDetailStateBeforeSetRecords: () => this.resetMasterDetailStateBeforeSetRecords() }); // 执行API扩展 this.tableAPIExtensions.extendTableAPI(); } + /** + * setRecords 前清理旧主从表状态,避免新数据复用旧展开行和子表实例 + */ + private resetMasterDetailStateBeforeSetRecords(): void { + const internalProps = getInternalProps(this.table); + const expandedRows = [...this.eventManager.getExpandedRows()]; + expandedRows.forEach(rowIndex => { + try { + this.collapseRowToNoRealRecordIndex(rowIndex); + } catch (error) { + console.warn(`Failed to collapse master detail row ${rowIndex} before setRecords:`, error); + } + }); + + const subTableRowIndices = Array.from(internalProps.subTableInstances?.keys() ?? []); + subTableRowIndices.forEach(bodyRowIndex => { + this.subTableManager.removeSubTable(bodyRowIndex); + }); + + internalProps.expandedRecordIndices?.splice(0); + internalProps.originalRowHeights?.clear(); + internalProps.subTableCheckboxStates?.clear(); + this.eventManager.setExpandedRows([]); + } + /** * 在 adaptive 处理后更新原始高度缓存 */ diff --git a/packages/vtable-plugins/src/master-detail-plugin/table-api-extensions.ts b/packages/vtable-plugins/src/master-detail-plugin/table-api-extensions.ts index 33b1ef3fb2..4d73c6b406 100644 --- a/packages/vtable-plugins/src/master-detail-plugin/table-api-extensions.ts +++ b/packages/vtable-plugins/src/master-detail-plugin/table-api-extensions.ts @@ -23,6 +23,7 @@ export class TableAPIExtensions { private originalUpdateChartSizeForResizeColWidth?: (col: number) => void; private originalUpdateChartSizeForResizeRowHeight?: (row: number) => void; private originalUpdateRowHeight?: (row: number, detaY: number, skipTableHeightMap?: boolean) => void; + private originalSetRecords?: (records: Array, option?: Parameters[1]) => void; private originalGetResizeColAt?: ( abstractX: number, abstractY: number, @@ -50,6 +51,7 @@ export class TableAPIExtensions { collapseRow: (rowIndex: number) => void; updateSubTablePositions: () => void; updateRowHeightForExpand: (rowIndex: number, deltaHeight: number) => void; + resetMasterDetailStateBeforeSetRecords: () => void; }; constructor( @@ -67,6 +69,7 @@ export class TableAPIExtensions { collapseRow: (rowIndex: number) => void; updateSubTablePositions: () => void; updateRowHeightForExpand: (rowIndex: number, deltaHeight: number) => void; + resetMasterDetailStateBeforeSetRecords: () => void; } ) { this.table = table; @@ -140,6 +143,8 @@ export class TableAPIExtensions { this.extendUpdateRowHeight(); // 处理展开行的列宽调整检测 this.extendGetResizeColAt(); + // 处理 setRecords 后主从表层级状态重建 + this.extendSetRecords(); } /** @@ -197,6 +202,24 @@ export class TableAPIExtensions { }; } + /** + * 扩展 setRecords 方法 + */ + private extendSetRecords(): void { + const table = this.table; + this.originalSetRecords = table.setRecords.bind(table); + table.setRecords = (records: Array, option?: Parameters[1]) => { + this.callbacks.resetMasterDetailStateBeforeSetRecords(); + if (Array.isArray(records)) { + this.configManager.processRecordsHierarchyStates(records, false); + } + this.originalSetRecords?.(records, option); + if (Array.isArray(records)) { + this.configManager.processRecordsHierarchyStates(records); + } + }; + } + /** * 扩展 updateResizeRow 方法 */ From 2b1f6bd81f0f188bd9e27527dc2859173f22e422 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Mon, 27 Jul 2026 18:31:21 +0800 Subject: [PATCH 17/77] fix: update records without explicit indexes --- ...pdate-records-render_2026-07-27-19-55.json | 11 +++ docs/assets/api/en/methods.md | 4 +- docs/assets/api/zh/methods.md | 4 +- .../issue-4761-update-records-edit-render.ts | 75 +++++++++++++++++++ packages/vtable/examples/menu.ts | 4 + packages/vtable/src/ListTable.ts | 7 +- packages/vtable/src/ts-types/table-engine.ts | 2 +- 7 files changed, 99 insertions(+), 8 deletions(-) create mode 100644 common/changes/@visactor/vtable/fix-issue-4761-update-records-render_2026-07-27-19-55.json create mode 100644 packages/vtable/examples/debug/issue-4761-update-records-edit-render.ts diff --git a/common/changes/@visactor/vtable/fix-issue-4761-update-records-render_2026-07-27-19-55.json b/common/changes/@visactor/vtable/fix-issue-4761-update-records-render_2026-07-27-19-55.json new file mode 100644 index 0000000000..e61242adf9 --- /dev/null +++ b/common/changes/@visactor/vtable/fix-issue-4761-update-records-render_2026-07-27-19-55.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@visactor/vtable", + "comment": "fix: refresh rows when updateRecords is called without record indexes", + "type": "patch" + } + ], + "packageName": "@visactor/vtable", + "email": "892739385@qq.com" +} diff --git a/docs/assets/api/en/methods.md b/docs/assets/api/en/methods.md index 9bbbf11349..f41c4a192c 100644 --- a/docs/assets/api/en/methods.md +++ b/docs/assets/api/en/methods.md @@ -2016,9 +2016,9 @@ Modify data, supports multiple data items /** * Modify data, supports multiple data items * @param records Modified data items - * @param recordIndexs Corresponding index of modified data (index displayed in body, i.e., which row of data in the body part to modify), in tree (grouping) structures, recordIndex may be an array, representing the index position of each level from the root node for that node. + * @param recordIndexs Corresponding index of modified data (index displayed in body, i.e., which row of data in the body part to modify), in tree (grouping) structures, recordIndex may be an array, representing the index position of each level from the root node for that node. When omitted, the records are updated by their array order. */ - updateRecords(records: any[], recordIndexs: number[]|number[][]) + updateRecords(records: any[], recordIndexs?: number[]|number[][]) ``` ## getBodyVisibleCellRange(Function) diff --git a/docs/assets/api/zh/methods.md b/docs/assets/api/zh/methods.md index 0d64ac15c9..e10c0b8330 100644 --- a/docs/assets/api/zh/methods.md +++ b/docs/assets/api/zh/methods.md @@ -2015,9 +2015,9 @@ changeCellValuesByRecords 的别名形式(位置参数)。 /** * 修改数据 支持多条数据 * @param records 修改数据条目 - * @param recordIndexs 对应修改数据的索引(显示在body中的索引,即要修改的是body部分的第几行数据),在树形(分组)结构中,recordIndex可能是一个数组,代表改节点从根节点开始的每级索引位置。 + * @param recordIndexs 对应修改数据的索引(显示在body中的索引,即要修改的是body部分的第几行数据),在树形(分组)结构中,recordIndex可能是一个数组,代表改节点从根节点开始的每级索引位置。省略时会按 records 顺序更新对应索引。 */ - updateRecords(records: any[], recordIndexs: number[]|number[][]) + updateRecords(records: any[], recordIndexs?: number[]|number[][]) ``` ## getBodyVisibleCellRange(Function) diff --git a/packages/vtable/examples/debug/issue-4761-update-records-edit-render.ts b/packages/vtable/examples/debug/issue-4761-update-records-edit-render.ts new file mode 100644 index 0000000000..372b176081 --- /dev/null +++ b/packages/vtable/examples/debug/issue-4761-update-records-edit-render.ts @@ -0,0 +1,75 @@ +import * as VTable from '../../src'; +import { InputEditor } from '@visactor/vtable-editors'; + +const CONTAINER_ID = 'vTable'; +const inputEditor = new InputEditor({}); +VTable.register.editor('input', inputEditor); + +const records = [ + { name: 'John', age: 20 }, + { name: 'Jane', age: 21 } +]; + +const createStatusBar = () => { + const container = document.getElementById(CONTAINER_ID)!; + const status = document.createElement('div'); + status.id = 'issue4761Status'; + status.style.cssText = 'height: 32px; line-height: 32px; font-size: 13px; color: #333;'; + status.textContent = 'Click "Check updateRecords render" to verify issue #4761.'; + + const button = document.createElement('button'); + button.textContent = 'Check updateRecords render'; + button.style.cssText = 'margin: 0 0 8px 8px;'; + button.onclick = () => checkUpdateRecordsRender(); + + container.parentElement?.insertBefore(status, container); + status.appendChild(button); +}; + +const getRenderedCellText = (tableInstance: VTable.ListTable, col: number, row: number) => { + const cellGroup = tableInstance.scenegraph.getCell(col, row); + const texts: string[] = []; + cellGroup?.forEachChildren((child: any) => { + const text = child?.attribute?.text; + if (typeof text === 'string') { + texts.push(text); + } else if (Array.isArray(text)) { + texts.push(text.join('')); + } + }); + return texts.join(''); +}; + +const checkUpdateRecordsRender = async () => { + const tableInstance = (window as any).tableInstance as VTable.ListTable; + const status = document.getElementById('issue4761Status')!; + records[0].name = 'aaa'; + tableInstance.updateRecords(records); + + await new Promise(resolve => requestAnimationFrame(resolve)); + + const renderedText = getRenderedCellText(tableInstance, 0, tableInstance.columnHeaderLevelCount); + const dataValue = tableInstance.getCellValue(0, tableInstance.columnHeaderLevelCount); + const pass = renderedText.includes('aaa') && dataValue === 'aaa'; + status.textContent = `${pass ? 'PASS' : 'FAIL'} | rendered=${renderedText}, data=${dataValue}`; + return status.textContent; +}; + +export function createTable() { + const option: VTable.ListTableConstructorOptions = { + records, + columns: [ + { field: 'name', title: 'First Name', width: 180, editor: 'input' }, + { field: 'age', title: 'Age', width: 120, editor: 'input' } + ], + editCellTrigger: 'doubleclick', + editor: 'input', + widthMode: 'standard', + defaultRowHeight: 36 + }; + + createStatusBar(); + const tableInstance = new VTable.ListTable(document.getElementById(CONTAINER_ID)!, option); + (window as any).tableInstance = tableInstance; + (window as any).issue4761Run = checkUpdateRecordsRender; +} diff --git a/packages/vtable/examples/menu.ts b/packages/vtable/examples/menu.ts index 2474d4e3ae..30e719ff1b 100644 --- a/packages/vtable/examples/menu.ts +++ b/packages/vtable/examples/menu.ts @@ -54,6 +54,10 @@ export const menus = [ path: 'debug', name: 'issue-5213-row-series-number-aggregation' }, + { + path: 'debug', + name: 'issue-4761-update-records-edit-render' + }, { path: 'debug', name: 'header-frame-border-null-color' diff --git a/packages/vtable/src/ListTable.ts b/packages/vtable/src/ListTable.ts index c72ae28f23..7374b48034 100644 --- a/packages/vtable/src/ListTable.ts +++ b/packages/vtable/src/ListTable.ts @@ -2422,14 +2422,15 @@ export class ListTable extends BaseTable implements ListTableAPI { * 基本表格中显示在body中的索引,即要修改的是body部分的第几行数据; * 如果是树形结构的话 recordIndexs 为数组,数组中每个元素为data的原始数据索引; */ - updateRecords(records: any[], recordIndexs: (number | number[])[], triggerEvent = true) { - listTableUpdateRecords(records, recordIndexs, this); + updateRecords(records: any[], recordIndexs?: (number | number[])[], triggerEvent = true) { + const updateRecordIndexs = recordIndexs?.length ? recordIndexs : records?.map((_, index) => index) ?? []; + listTableUpdateRecords(records, updateRecordIndexs, this); // 触发更新数据记录事件 - 假设操作成功 if (triggerEvent) { this.fireListeners(TABLE_EVENT_TYPE.UPDATE_RECORD, { records, - recordIndexs, + recordIndexs: updateRecordIndexs, updateCount: records.length }); } diff --git a/packages/vtable/src/ts-types/table-engine.ts b/packages/vtable/src/ts-types/table-engine.ts index 25021a6e07..5655c12ad8 100644 --- a/packages/vtable/src/ts-types/table-engine.ts +++ b/packages/vtable/src/ts-types/table-engine.ts @@ -478,7 +478,7 @@ export interface ListTableAPI extends BaseTableAPI { addRecord: (record: any, recordIndex?: number | number[], triggerEvent?: boolean) => void; addRecords: (records: any[], recordIndex?: number | number[], triggerEvent?: boolean) => void; deleteRecords: (recordIndexs: number[] | number[][], triggerEvent?: boolean) => void; - updateRecords: (records: any[], recordIndexs: (number | number[])[], triggerEvent?: boolean) => void; + updateRecords: (records: any[], recordIndexs?: (number | number[])[], triggerEvent?: boolean) => void; updateFilterRules: (filterRules: FilterRules, options: { clearRowHeightCache?: boolean }) => void; getAggregateValuesByField: (field: string | number) => { col: number; From 497d7465c5291e7f2fb18a0801b4a11fb12e35d7 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Mon, 27 Jul 2026 18:34:58 +0800 Subject: [PATCH 18/77] fix: remount input editor safely --- ...input-editor-remount_2026-07-27-20-25.json | 11 +++ packages/vtable-editors/src/input-editor.ts | 36 +++++---- ...issue-4810-edit-cell-double-click-blank.ts | 73 +++++++++++++++++++ packages/vtable/examples/menu.ts | 4 + 4 files changed, 105 insertions(+), 19 deletions(-) create mode 100644 common/changes/@visactor/vtable-editors/fix-issue-4810-input-editor-remount_2026-07-27-20-25.json create mode 100644 packages/vtable/examples/debug/issue-4810-edit-cell-double-click-blank.ts diff --git a/common/changes/@visactor/vtable-editors/fix-issue-4810-input-editor-remount_2026-07-27-20-25.json b/common/changes/@visactor/vtable-editors/fix-issue-4810-input-editor-remount_2026-07-27-20-25.json new file mode 100644 index 0000000000..cb398d7203 --- /dev/null +++ b/common/changes/@visactor/vtable-editors/fix-issue-4810-input-editor-remount_2026-07-27-20-25.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@visactor/vtable-editors", + "comment": "fix: safely remount and reposition input editor when editing restarts", + "type": "patch" + } + ], + "packageName": "@visactor/vtable-editors", + "email": "892739385@qq.com" +} diff --git a/packages/vtable-editors/src/input-editor.ts b/packages/vtable-editors/src/input-editor.ts index ece3c60398..b67cea442a 100644 --- a/packages/vtable-editors/src/input-editor.ts +++ b/packages/vtable-editors/src/input-editor.ts @@ -98,6 +98,18 @@ export class InputEditor implements IEditor { this.eventHandlers.push({ type: 'paste', handler: pasteHandler }); } + protected ensureElementMounted(container: HTMLElement) { + if (!this.element) { + this.createElement(); + return; + } + + if (!container.contains(this.element)) { + this.element.parentElement?.removeChild(this.element); + this.container.appendChild(this.element); + } + } + setValue(value: string) { this.element.value = typeof value !== 'undefined' ? value : ''; } @@ -118,14 +130,7 @@ export class InputEditor implements IEditor { if (selectCell.col !== this.col || selectCell.row !== this.row) { return; } - if (!this.element) { - this.createElement(); - } else { - if (!container.contains(this.element)) { - this.element.parentElement.removeChild(this.element); - this.container.appendChild(this.element); - } - } + this.ensureElementMounted(container); this.element.style.opacity = '0'; //这个pointerEvents = 'none'很重要,如果没有的话会引起vtable.getElement()元素和这里的element元素的focus和blur的切换, //也会引起mouseleave_table mouseleave_cell和mouseenter的切换 @@ -141,20 +146,13 @@ export class InputEditor implements IEditor { this.table = table; this.col = col; this.row = row; - if (!this.element) { - this.createElement(); - if (referencePosition?.rect) { - this.adjustPosition(referencePosition.rect); - } - } else { - if (!container.contains(this.element)) { - this.element.parentElement.removeChild(this.element); - this.container.appendChild(this.element); - } - } + this.ensureElementMounted(container); if (value !== undefined && value !== null) { this.setValue(value); } + if (referencePosition?.rect) { + this.adjustPosition(referencePosition.rect); + } //防止调用过prepareEdit 后,元素的显示和可操作性被影响 this.element.style.opacity = '1'; this.element.style.pointerEvents = 'auto'; diff --git a/packages/vtable/examples/debug/issue-4810-edit-cell-double-click-blank.ts b/packages/vtable/examples/debug/issue-4810-edit-cell-double-click-blank.ts new file mode 100644 index 0000000000..0ca7934ad5 --- /dev/null +++ b/packages/vtable/examples/debug/issue-4810-edit-cell-double-click-blank.ts @@ -0,0 +1,73 @@ +import * as VTable from '../../src'; +import { InputEditor } from '@visactor/vtable-editors'; + +const CONTAINER_ID = 'vTable'; +const inputEditor = new InputEditor({}); +VTable.register.editor('issue4810-input', inputEditor); + +const createStatusBar = () => { + const container = document.getElementById(CONTAINER_ID)!; + const status = document.createElement('div'); + status.id = 'issue4810Status'; + status.style.cssText = 'height: 32px; line-height: 32px; font-size: 13px; color: #333;'; + status.textContent = 'Click "Check double edit" to verify issue #4810.'; + + const button = document.createElement('button'); + button.textContent = 'Check double edit'; + button.style.cssText = 'margin: 0 0 8px 8px;'; + button.onclick = () => checkDoubleEdit(); + + container.parentElement?.insertBefore(status, container); + status.appendChild(button); +}; + +const checkDoubleEdit = () => { + const tableInstance = (window as any).tableInstance as VTable.ListTable; + const status = document.getElementById('issue4810Status')!; + + try { + tableInstance.startEditCell(0, tableInstance.columnHeaderLevelCount); + inputEditor.getInputElement()?.remove(); + (tableInstance.editorManager as any).editingEditor = null; + tableInstance.startEditCell(1, tableInstance.columnHeaderLevelCount); + } catch (err) { + status.textContent = `FAIL | ${(err as Error).message}`; + return status.textContent; + } + + const inputElement = inputEditor.getInputElement(); + const tableElement = tableInstance.getElement(); + const pass = + !!inputElement && + tableElement.contains(inputElement) && + inputElement.style.opacity === '1' && + inputElement.style.pointerEvents === 'auto' && + inputElement.style.left !== ''; + + status.textContent = `${pass ? 'PASS' : 'FAIL'} | mounted=${ + !!inputElement && tableElement.contains(inputElement) + }, left=${inputElement?.style.left}, top=${inputElement?.style.top}`; + return status.textContent; +}; + +export function createTable() { + const option: VTable.ListTableConstructorOptions = { + records: [ + { name: 'Alice', age: 20 }, + { name: 'Bob', age: 21 } + ], + columns: [ + { field: 'name', title: 'Name', width: 180, editor: 'issue4810-input' }, + { field: 'age', title: 'Age', width: 120, editor: 'issue4810-input' } + ], + editCellTrigger: 'doubleclick', + editor: 'issue4810-input', + widthMode: 'standard', + defaultRowHeight: 36 + }; + + createStatusBar(); + const tableInstance = new VTable.ListTable(document.getElementById(CONTAINER_ID)!, option); + (window as any).tableInstance = tableInstance; + (window as any).issue4810Run = checkDoubleEdit; +} diff --git a/packages/vtable/examples/menu.ts b/packages/vtable/examples/menu.ts index 2474d4e3ae..f51c4e2d48 100644 --- a/packages/vtable/examples/menu.ts +++ b/packages/vtable/examples/menu.ts @@ -54,6 +54,10 @@ export const menus = [ path: 'debug', name: 'issue-5213-row-series-number-aggregation' }, + { + path: 'debug', + name: 'issue-4810-edit-cell-double-click-blank' + }, { path: 'debug', name: 'header-frame-border-null-color' From 935f3fa2f2540ddf4fea493ebe35ec0d39392872 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Tue, 28 Jul 2026 10:56:53 +0800 Subject: [PATCH 19/77] fix(gantt): support inline subtask path for taskbar rect --- .../gantt/issue-4778-taskbar-relative-rect.ts | 11 ++++--- packages/vtable-gantt/src/Gantt.ts | 18 +++++------ .../vtable-gantt/src/scenegraph/gantt-node.ts | 2 +- .../vtable-gantt/src/scenegraph/scenegraph.ts | 7 +++- .../vtable-gantt/src/scenegraph/task-bar.ts | 26 +++++++++++---- .../vtable-gantt/src/state/state-manager.ts | 32 ++++++++++--------- packages/vtable-gantt/src/ts-types/events.ts | 20 ++++++------ 7 files changed, 70 insertions(+), 46 deletions(-) diff --git a/packages/vtable-gantt/examples/gantt/issue-4778-taskbar-relative-rect.ts b/packages/vtable-gantt/examples/gantt/issue-4778-taskbar-relative-rect.ts index 3cbc9ec1a3..c580d86d64 100644 --- a/packages/vtable-gantt/examples/gantt/issue-4778-taskbar-relative-rect.ts +++ b/packages/vtable-gantt/examples/gantt/issue-4778-taskbar-relative-rect.ts @@ -1,7 +1,7 @@ import type { ColumnsDefine } from '@visactor/vtable'; import type { GanttConstructorOptions } from '../../src/index'; import { Gantt } from '../../src/index'; -import { TasksShowMode } from '../../src/ts-types'; +import { TasksShowMode, TaskType } from '../../src/ts-types'; const CONTAINER_ID = 'vTable'; @@ -37,8 +37,8 @@ const checkRects = () => { try { emptyRect = ganttInstance.getTaskBarRelativeRect(1); - firstChildRect = ganttInstance.getTaskBarRelativeRect(0, 0); - secondChildRect = ganttInstance.getTaskBarRelativeRect(0, 1); + firstChildRect = ganttInstance.getTaskBarRelativeRect(0, [0, 0]); + secondChildRect = ganttInstance.getTaskBarRelativeRect(0, [0, 1]); } catch (err) { status.textContent = `FAIL | ${(err as Error).message}`; return status.textContent; @@ -60,7 +60,8 @@ export function createTable() { const records = [ { id: 1, - title: 'Parent with compact subtasks', + title: 'Collapsed project with inline subtasks', + type: TaskType.PROJECT, children: [ { id: 11, title: 'Sub task A', start: '2024-07-01', end: '2024-07-04', progress: 30 }, { id: 12, title: 'Sub task B', start: '2024-07-08', end: '2024-07-12', progress: 60 } @@ -86,7 +87,7 @@ export function createTable() { tableWidth: 260 }, taskKeyField: 'id', - tasksShowMode: TasksShowMode.Sub_Tasks_Compact, + tasksShowMode: TasksShowMode.Project_Sub_Tasks_Inline, taskBar: { startDateField: 'start', endDateField: 'end', diff --git a/packages/vtable-gantt/src/Gantt.ts b/packages/vtable-gantt/src/Gantt.ts index cbde30c845..0cc5482e3d 100644 --- a/packages/vtable-gantt/src/Gantt.ts +++ b/packages/vtable-gantt/src/Gantt.ts @@ -1027,12 +1027,12 @@ export class Gantt extends EventTarget { return this.records[taskShowIndex]; } - _refreshTaskBar(taskShowIndex: number, sub_task_index?: number) { + _refreshTaskBar(taskShowIndex: number, sub_task_index?: number | number[]) { // this.taskListTableInstance.updateRecords([record], [index]); this.scenegraph.taskBar.updateTaskBarNode(taskShowIndex, sub_task_index); this.scenegraph.refreshRecordLinkNodes( taskShowIndex, - undefined, + sub_task_index, this.scenegraph.taskBar.getTaskBarNodeByIndex(taskShowIndex, sub_task_index) as GanttTaskBarNode ); this.scenegraph.updateNextFrame(); @@ -1267,7 +1267,7 @@ export class Gantt extends EventTarget { } } - _updateEndDateToTaskRecord(endDate: Date, index: number, sub_task_index?: number) { + _updateEndDateToTaskRecord(endDate: Date, index: number, sub_task_index?: number | number[]) { const taskRecord = this.getRecordByIndex(index, sub_task_index); const endDateField = this.parsedOptions.endDateField; const dateFormat = this.parsedOptions.dateFormat ?? parseDateFormat(taskRecord[endDateField]); @@ -1289,7 +1289,7 @@ export class Gantt extends EventTarget { } } - _updateStartEndDateToTaskRecord(startDate: Date, endDate: Date, index: number, sub_task_index?: number) { + _updateStartEndDateToTaskRecord(startDate: Date, endDate: Date, index: number, sub_task_index?: number | number[]) { const taskRecord = this.getRecordByIndex(index, sub_task_index); const startDateField = this.parsedOptions.startDateField; const endDateField = this.parsedOptions.endDateField; @@ -1319,7 +1319,7 @@ export class Gantt extends EventTarget { * @param index 对应的一定是左侧表格body的index * @param sub_task_index 子任务的index, 当taskShowMode是sub_tasks_*模式时,会传入sub_task_index。如果是tasks_separate模式,sub_task_index传入undefined。 */ - _updateProgressToTaskRecord(progress: number, index: number, sub_task_index?: number) { + _updateProgressToTaskRecord(progress: number, index: number, sub_task_index?: number | number[]) { const taskRecord = this.getRecordByIndex(index, sub_task_index); const progressField = this.parsedOptions.progressField; if (progressField) { @@ -1353,11 +1353,11 @@ export class Gantt extends EventTarget { * 如果TasksShowModes是 sub_tasks_*** 模式 则需要传入task_index和sub_task_index */ updateTaskRecord(record: any, task_index: number | number[]): void; - updateTaskRecord(record: any, task_index: number, sub_task_index: number): void; - updateTaskRecord(record: any, task_index: number | number[], sub_task_index?: number) { + updateTaskRecord(record: any, task_index: number, sub_task_index: number | number[]): void; + updateTaskRecord(record: any, task_index: number | number[], sub_task_index?: number | number[]) { if (isValid(sub_task_index)) { const index = typeof task_index === 'number' ? task_index : task_index[0]; - this._updateRecordToListTable(record, [index, sub_task_index]); + this._updateRecordToListTable(record, Array.isArray(sub_task_index) ? sub_task_index : [index, sub_task_index]); this._refreshTaskBar(index, sub_task_index); return; } @@ -1637,7 +1637,7 @@ export class Gantt extends EventTarget { this.stateManager.setScrollLeft(value); } /** 获取任务条的位置。相对应甘特图表左上角的位置。 */ - getTaskBarRelativeRect(index: number, sub_task_index?: number) { + getTaskBarRelativeRect(index: number, sub_task_index?: number | number[]) { const taskBarNode = this.scenegraph.taskBar.getTaskBarNodeByIndex(index, sub_task_index); if (!taskBarNode) { return null; diff --git a/packages/vtable-gantt/src/scenegraph/gantt-node.ts b/packages/vtable-gantt/src/scenegraph/gantt-node.ts index e92027d0f0..6a37d207e6 100644 --- a/packages/vtable-gantt/src/scenegraph/gantt-node.ts +++ b/packages/vtable-gantt/src/scenegraph/gantt-node.ts @@ -15,7 +15,7 @@ export class GanttTaskBarNode extends Group { textLabel?: IText; declare name: string; task_index: number; - sub_task_index?: number; + sub_task_index?: number | number[]; record?: any; labelStyle?: ITaskBarLabelTextStyle; diff --git a/packages/vtable-gantt/src/scenegraph/scenegraph.ts b/packages/vtable-gantt/src/scenegraph/scenegraph.ts index d68465655d..88c98dbc29 100644 --- a/packages/vtable-gantt/src/scenegraph/scenegraph.ts +++ b/packages/vtable-gantt/src/scenegraph/scenegraph.ts @@ -338,7 +338,12 @@ export class Scenegraph { this.toolTip.hide(); } - refreshRecordLinkNodes(taskIndex: number, sub_task_index: number, target: GanttTaskBarNode, dy: number = 0) { + refreshRecordLinkNodes( + taskIndex: number, + sub_task_index: number | number[], + target: GanttTaskBarNode, + dy: number = 0 + ) { const gantt: Gantt = this._gantt; const record = gantt.getRecordByIndex(taskIndex, sub_task_index); const vtable_gantt_linkedTo = record.vtable_gantt_linkedTo; diff --git a/packages/vtable-gantt/src/scenegraph/task-bar.ts b/packages/vtable-gantt/src/scenegraph/task-bar.ts index 244f477370..850632174b 100644 --- a/packages/vtable-gantt/src/scenegraph/task-bar.ts +++ b/packages/vtable-gantt/src/scenegraph/task-bar.ts @@ -19,6 +19,23 @@ const LOCATE_ICON_BG_HOVER = '#4080ff'; const LOCATE_ICON_ARROW = '#4e5969'; const LOCATE_ICON_ARROW_HOVER = '#ffffff'; +const isSameSubTaskIndex = (source?: number | number[], target?: number | number[]) => { + if (!isValid(target)) { + return true; + } + + if (Array.isArray(source) || Array.isArray(target)) { + return ( + Array.isArray(source) && + Array.isArray(target) && + source.length === target.length && + source.every((value, index) => value === target[index]) + ); + } + + return source === target; +}; + export class TaskBar { formatMilestoneText(text: string, record: any): string { if (!text) { @@ -524,7 +541,7 @@ export class TaskBar { } return { barGroupBox, baselineBar }; } - updateTaskBarNode(index: number, sub_task_index?: number) { + updateTaskBarNode(index: number, sub_task_index?: number | number[]) { const taskbarGroup = this.getTaskBarNodeByIndex(index, sub_task_index); if (taskbarGroup) { this.barContainer.removeChild(taskbarGroup); @@ -1054,16 +1071,13 @@ export class TaskBar { this.selectedBorders[0].appendChild(line); } - getTaskBarNodeByIndex(index: number, sub_task_index?: number): GanttTaskBarNode { + getTaskBarNodeByIndex(index: number, sub_task_index?: number | number[]): GanttTaskBarNode { let c = this.barContainer.firstChild as GanttTaskBarNode; if (!c) { return null; } for (let i = 0; i < this.barContainer.childrenCount; i++) { - if ( - c.task_index === index && - (!isValid(sub_task_index) || (isValid(sub_task_index) && c.sub_task_index === sub_task_index)) - ) { + if (c.task_index === index && isSameSubTaskIndex(c.sub_task_index, sub_task_index)) { return c; } c = c._next as GanttTaskBarNode; diff --git a/packages/vtable-gantt/src/state/state-manager.ts b/packages/vtable-gantt/src/state/state-manager.ts index f2255b642b..ff7780962b 100644 --- a/packages/vtable-gantt/src/state/state-manager.ts +++ b/packages/vtable-gantt/src/state/state-manager.ts @@ -479,9 +479,9 @@ export class StateManager { const indexs = getTaskIndexsByTaskY(targetEndY, this._gantt); this._gantt._dragOrderTaskRecord( target.task_index, - target.sub_task_index, + target.sub_task_index as number, indexs.task_index, - indexs.sub_task_index + indexs.sub_task_index as number ); clearRecordShowIndex(this._gantt.records); this._gantt.taskListTableInstance.renderWithRecreateCells(); @@ -494,18 +494,20 @@ export class StateManager { Math.abs(Math.round(deltaY / this._gantt.parsedOptions.rowHeight)) >= 1 ) { const indexs = getTaskIndexsByTaskY(targetEndY, this._gantt); - this._gantt._dragOrderTaskRecord( - target.task_index, - target.sub_task_index, - indexs.task_index, - indexs.sub_task_index - ); - if (this._gantt.parsedOptions.tasksShowMode === TasksShowMode.Sub_Tasks_Separate) { - this._gantt.taskListTableInstance.renderWithRecreateCells(); - this._gantt.scenegraph.refreshTaskBarsAndGrid(); - } else { - this._gantt.scenegraph.taskBar.refresh(); - this._gantt.scenegraph.dependencyLink.refresh(); + if (!Array.isArray(target.sub_task_index) && !Array.isArray(indexs.sub_task_index)) { + this._gantt._dragOrderTaskRecord( + target.task_index, + target.sub_task_index, + indexs.task_index, + indexs.sub_task_index + ); + if (this._gantt.parsedOptions.tasksShowMode === TasksShowMode.Sub_Tasks_Separate) { + this._gantt.taskListTableInstance.renderWithRecreateCells(); + this._gantt.scenegraph.refreshTaskBarsAndGrid(); + } else { + this._gantt.scenegraph.taskBar.refresh(); + this._gantt.scenegraph.dependencyLink.refresh(); + } } // target = this._gantt.scenegraph.taskBar.getTaskBarNodeByIndex(indexs.task_index, indexs.sub_task_index); } else { @@ -1377,7 +1379,7 @@ export class StateManager { } } -function reCreateCustomNode(gantt: Gantt, taskBarGroup: Group, taskIndex: number, sub_task_index?: number) { +function reCreateCustomNode(gantt: Gantt, taskBarGroup: Group, taskIndex: number, sub_task_index?: number | number[]) { const taskBarCustomLayout = gantt.parsedOptions.taskBarCustomLayout; if (taskBarCustomLayout) { let customLayoutObj; diff --git a/packages/vtable-gantt/src/ts-types/events.ts b/packages/vtable-gantt/src/ts-types/events.ts index 55f33ecc80..6f75fe741a 100644 --- a/packages/vtable-gantt/src/ts-types/events.ts +++ b/packages/vtable-gantt/src/ts-types/events.ts @@ -3,6 +3,8 @@ import type { IMarkLine, ITaskLink, ITimelineDateInfo } from './gantt-engine'; import type { IPosition } from './common'; import type { IZoomEventArgs } from './zoom-scale'; +type SubTaskIndex = number | number[]; + export type TableEventListener = ( args: TableEventHandlersEventArgumentMap[TYPE] ) => TableEventHandlersReturnMap[TYPE]; //AnyFunction; @@ -19,7 +21,7 @@ export interface TableEventHandlersEventArgumentMap { mouseenter_task_bar: { /** 第几条数据 */ index: number; - sub_task_index?: number; + sub_task_index?: SubTaskIndex; record: any; event: Event; federatedEvent: FederatedPointerEvent; @@ -27,7 +29,7 @@ export interface TableEventHandlersEventArgumentMap { mouseleave_task_bar: { /** 第几条数据 */ index: number; - sub_task_index?: number; + sub_task_index?: SubTaskIndex; record: any; event: Event; federatedEvent: FederatedPointerEvent; @@ -35,7 +37,7 @@ export interface TableEventHandlersEventArgumentMap { click_task_bar: { /** 第几条数据 */ index: number; - sub_task_index?: number; + sub_task_index?: SubTaskIndex; record: any; event: Event; federatedEvent: FederatedPointerEvent; @@ -43,7 +45,7 @@ export interface TableEventHandlersEventArgumentMap { contextmenu_task_bar: { /** 第几条数据 */ index: number; - sub_task_index?: number; + sub_task_index?: SubTaskIndex; record: any; event: Event; federatedEvent: FederatedPointerEvent; @@ -51,7 +53,7 @@ export interface TableEventHandlersEventArgumentMap { change_date_range: { /** 第几条数据 */ index: number; - sub_task_index?: number; + sub_task_index?: SubTaskIndex; /** 改变后的起始日期 */ startDate: Date; /** 改变后的结束日期 */ @@ -66,7 +68,7 @@ export interface TableEventHandlersEventArgumentMap { move_end_task_bar: { /** 第几条数据 */ index: number; - sub_task_index?: number; + sub_task_index?: SubTaskIndex; /** 改变后的起始日期 */ startDate: Date; /** 改变后的结束日期 */ @@ -87,7 +89,7 @@ export interface TableEventHandlersEventArgumentMap { event: Event; /** 第几条数据 */ index: number; - sub_task_index?: number; + sub_task_index?: SubTaskIndex; /** 改变后的起始日期 */ startDate: string; /** 改变后的结束日期 */ @@ -114,7 +116,7 @@ export interface TableEventHandlersEventArgumentMap { point: 'start' | 'end'; /** 第几条数据 */ index: number; - sub_task_index?: number; + sub_task_index?: SubTaskIndex; record: any; }; contextmenu_dependency_link: { @@ -138,7 +140,7 @@ export interface TableEventHandlersEventArgumentMap { event: Event; /** 第几条数据 */ index: number; - sub_task_index?: number; + sub_task_index?: SubTaskIndex; /** 新的进度值 */ progress: number; /** 原来的进度值 */ From 7fc98269ac1e7c76c135052980c31724b185dae2 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Tue, 28 Jul 2026 10:57:27 +0800 Subject: [PATCH 20/77] fix(plugins): cancel stale master detail expansion --- .../src/master-detail-plugin/config.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/vtable-plugins/src/master-detail-plugin/config.ts b/packages/vtable-plugins/src/master-detail-plugin/config.ts index 3f5552ed7e..cef00ebb1a 100644 --- a/packages/vtable-plugins/src/master-detail-plugin/config.ts +++ b/packages/vtable-plugins/src/master-detail-plugin/config.ts @@ -7,6 +7,7 @@ import type { DetailTableOptions, MasterDetailPluginOptions } from './types'; export class ConfigManager { private expandRowCallback?: (rowIndex: number) => void; private childrenKey: string; + private expansionVersion = 0; constructor(private pluginOptions: MasterDetailPluginOptions, private table: VTable.ListTable) { this.childrenKey = pluginOptions.childrenKey || 'children'; @@ -162,6 +163,7 @@ export class ConfigManager { * 处理记录的层级状态 */ processRecordsHierarchyStates(records: unknown[], expandInitialRows: boolean = true): void { + const expansionVersion = ++this.expansionVersion; const HierarchyState = VTable.TYPES.HierarchyState; // 兼容处理headerExpandLevel const hierarchyExpandLevel = this.table.options.hierarchyExpandLevel || this.table.options.headerExpandLevel; @@ -194,7 +196,7 @@ export class ConfigManager { }; processRecords(records); if (expandInitialRows) { - this.performInitialExpansion(); + this.performInitialExpansion(expansionVersion); } } @@ -202,7 +204,7 @@ export class ConfigManager { * 遍历所有记录,根据 hierarchyState 状态执行初始展开 * 与VTable的异步CellGroup创建过程同步,在每个CellGroup创建后检查是否需要展开 */ - private performInitialExpansion(): void { + private performInitialExpansion(expansionVersion: number): void { // 获取需要展开的记录索引列表 const expandableRecords = this.getExpandableRecords(); if (expandableRecords.length === 0) { @@ -210,7 +212,7 @@ export class ConfigManager { } // 开始异步展开过程,与VTable的渲染频率同步 - this.startAsyncExpansion(expandableRecords); + this.startAsyncExpansion(expandableRecords, expansionVersion); } /** @@ -270,11 +272,16 @@ export class ConfigManager { * 开始异步展开过程,与VTable的异步渲染同步 */ private startAsyncExpansion( - expandableRecords: Array<{ recordIndex: number; actualRowIndex: number; record: unknown }> + expandableRecords: Array<{ recordIndex: number; actualRowIndex: number; record: unknown }>, + expansionVersion: number ): void { let currentIndex = 0; const processNextExpansion = (): void => { + if (expansionVersion !== this.expansionVersion) { + return; + } + if (currentIndex >= expandableRecords.length) { return; // 所有展开操作完成 } From 23c35daef698e0cfa2cd528964e173d1d663df13 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Tue, 28 Jul 2026 10:57:43 +0800 Subject: [PATCH 21/77] fix(vtable): guard grouped update records indexes --- packages/vtable/src/ListTable.ts | 3 +-- packages/vtable/src/data/CachedDataSource.ts | 6 ++++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/vtable/src/ListTable.ts b/packages/vtable/src/ListTable.ts index 7374b48034..8366170c65 100644 --- a/packages/vtable/src/ListTable.ts +++ b/packages/vtable/src/ListTable.ts @@ -2423,10 +2423,9 @@ export class ListTable extends BaseTable implements ListTableAPI { * 如果是树形结构的话 recordIndexs 为数组,数组中每个元素为data的原始数据索引; */ updateRecords(records: any[], recordIndexs?: (number | number[])[], triggerEvent = true) { - const updateRecordIndexs = recordIndexs?.length ? recordIndexs : records?.map((_, index) => index) ?? []; + const updateRecordIndexs = recordIndexs ?? records?.map((_, index) => index) ?? []; listTableUpdateRecords(records, updateRecordIndexs, this); - // 触发更新数据记录事件 - 假设操作成功 if (triggerEvent) { this.fireListeners(TABLE_EVENT_TYPE.UPDATE_RECORD, { records, diff --git a/packages/vtable/src/data/CachedDataSource.ts b/packages/vtable/src/data/CachedDataSource.ts index 03e0552055..2dec3e5885 100644 --- a/packages/vtable/src/data/CachedDataSource.ts +++ b/packages/vtable/src/data/CachedDataSource.ts @@ -274,6 +274,9 @@ export class CachedDataSource extends DataSource { continue; } const originRecordIndex = this.getOriginRecordIndexForGroup(recordIndex); + if (!isValid(originRecordIndex)) { + continue; + } this.beforeChangedRecordsMap.delete(originRecordIndex.toString()); this.dataSourceObj.records.splice(originRecordIndex, 1); @@ -292,6 +295,9 @@ export class CachedDataSource extends DataSource { continue; } const originRecordIndex = this.getOriginRecordIndexForGroup(recordIndex); + if (!isValid(originRecordIndex)) { + continue; + } this.beforeChangedRecordsMap.delete(originRecordIndex.toString()); this.dataSourceObj.records[originRecordIndex] = records[index]; } From 7317d702599924ec6ccdfdc1aada7e2d9f649781 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Tue, 28 Jul 2026 10:58:11 +0800 Subject: [PATCH 22/77] fix(editors): refresh reused textarea editor --- packages/vtable-editors/src/input-editor.ts | 2 +- .../vtable-editors/src/textArea-editor.ts | 27 ++++++++++++------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/packages/vtable-editors/src/input-editor.ts b/packages/vtable-editors/src/input-editor.ts index b67cea442a..805b3b31a1 100644 --- a/packages/vtable-editors/src/input-editor.ts +++ b/packages/vtable-editors/src/input-editor.ts @@ -106,7 +106,7 @@ export class InputEditor implements IEditor { if (!container.contains(this.element)) { this.element.parentElement?.removeChild(this.element); - this.container.appendChild(this.element); + container.appendChild(this.element); } } diff --git a/packages/vtable-editors/src/textArea-editor.ts b/packages/vtable-editors/src/textArea-editor.ts index 4eede430ca..5947e0c0f2 100644 --- a/packages/vtable-editors/src/textArea-editor.ts +++ b/packages/vtable-editors/src/textArea-editor.ts @@ -65,18 +65,27 @@ export class TextAreaEditor implements IEditor { return this.element?.value; } - onStart({ value, referencePosition, container, endEdit }: EditContext) { - this.container = container; - this.successCallback = endEdit; + protected ensureElementMounted(container: HTMLElement) { if (!this.element) { this.createElement(); + return; + } - if (value !== undefined && value !== null) { - this.setValue(value); - } - if (referencePosition?.rect) { - this.adjustPosition(referencePosition.rect); - } + if (!container.contains(this.element)) { + this.element.parentElement?.removeChild(this.element); + container.appendChild(this.element); + } + } + + onStart({ value, referencePosition, container, endEdit }: EditContext) { + this.container = container; + this.successCallback = endEdit; + this.ensureElementMounted(container); + if (value !== undefined && value !== null) { + this.setValue(value); + } + if (referencePosition?.rect) { + this.adjustPosition(referencePosition.rect); } this.element.focus(); // do nothing From 448d79f3dc24398c68aeacb54a06d993034ad576 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Tue, 28 Jul 2026 11:47:10 +0800 Subject: [PATCH 23/77] fix(gantt): sync project records after subtask updates --- packages/vtable-gantt/src/state/state-manager.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/vtable-gantt/src/state/state-manager.ts b/packages/vtable-gantt/src/state/state-manager.ts index ff7780962b..47f5560c3e 100644 --- a/packages/vtable-gantt/src/state/state-manager.ts +++ b/packages/vtable-gantt/src/state/state-manager.ts @@ -1366,6 +1366,8 @@ export class StateManager { // 更新父任务的时间范围 parent[startDateField] = formatDateValue(earliestStart); parent[endDateField] = formatDateValue(latestEnd); + const parentRecordIndex = parentPath.length === 0 ? childIndex : [...parentPath, childIndex]; + this._gantt._updateRecordToListTable(parent, parentRecordIndex); } } } From a95261f8cbdc71128cbd8a0b86ec77639f4b8a2e Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Tue, 28 Jul 2026 11:47:26 +0800 Subject: [PATCH 24/77] fix(plugins): invalidate expansion on release --- packages/vtable-plugins/src/master-detail-plugin/config.ts | 1 + packages/vtable-plugins/src/master-detail-plugin/core.ts | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/vtable-plugins/src/master-detail-plugin/config.ts b/packages/vtable-plugins/src/master-detail-plugin/config.ts index cef00ebb1a..36d98c12f7 100644 --- a/packages/vtable-plugins/src/master-detail-plugin/config.ts +++ b/packages/vtable-plugins/src/master-detail-plugin/config.ts @@ -346,6 +346,7 @@ export class ConfigManager { * 释放所有资源和引用 */ release(): void { + this.expansionVersion++; this.isRowExpanded = () => false; // 清理对表格的引用 (this as unknown as { table: VTable.ListTable | null }).table = null; diff --git a/packages/vtable-plugins/src/master-detail-plugin/core.ts b/packages/vtable-plugins/src/master-detail-plugin/core.ts index 1a51eb54d7..2d37acdac3 100644 --- a/packages/vtable-plugins/src/master-detail-plugin/core.ts +++ b/packages/vtable-plugins/src/master-detail-plugin/core.ts @@ -292,9 +292,6 @@ export class MasterDetailPlugin implements pluginsDefinition.IVTablePlugin { this.updateRowHeightForExpand(rowIndex, deltaHeight); this.table.scenegraph.updateContainerHeight(rowIndex, deltaHeight); internalProps._heightResizedRowMap.add(rowIndex); - if (rowIndex === 96) { - console.log('wokk'); - } this.subTableManager.renderSubTable(bodyRowIndex, childrenData, (record, bodyRowIndex) => this.configManager.getDetailConfigForRecord(record, bodyRowIndex) ); From 637a1d766428cb7a3a26bf9ed94c952b1223e5d4 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Tue, 28 Jul 2026 11:47:56 +0800 Subject: [PATCH 25/77] fix(editors): clear reused editor empty values --- packages/vtable-editors/src/input-editor.ts | 8 +++----- packages/vtable-editors/src/textArea-editor.ts | 8 +++----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/vtable-editors/src/input-editor.ts b/packages/vtable-editors/src/input-editor.ts index 805b3b31a1..9d190491bd 100644 --- a/packages/vtable-editors/src/input-editor.ts +++ b/packages/vtable-editors/src/input-editor.ts @@ -110,8 +110,8 @@ export class InputEditor implements IEditor { } } - setValue(value: string) { - this.element.value = typeof value !== 'undefined' ? value : ''; + setValue(value: string | null | undefined) { + this.element.value = value ?? ''; } getValue() { @@ -147,9 +147,7 @@ export class InputEditor implements IEditor { this.col = col; this.row = row; this.ensureElementMounted(container); - if (value !== undefined && value !== null) { - this.setValue(value); - } + this.setValue(value); if (referencePosition?.rect) { this.adjustPosition(referencePosition.rect); } diff --git a/packages/vtable-editors/src/textArea-editor.ts b/packages/vtable-editors/src/textArea-editor.ts index 5947e0c0f2..78612cc6ab 100644 --- a/packages/vtable-editors/src/textArea-editor.ts +++ b/packages/vtable-editors/src/textArea-editor.ts @@ -57,8 +57,8 @@ export class TextAreaEditor implements IEditor { }); } - setValue(value: string) { - this.element.value = typeof value !== 'undefined' ? value : ''; + setValue(value: string | null | undefined) { + this.element.value = value ?? ''; } getValue() { @@ -81,9 +81,7 @@ export class TextAreaEditor implements IEditor { this.container = container; this.successCallback = endEdit; this.ensureElementMounted(container); - if (value !== undefined && value !== null) { - this.setValue(value); - } + this.setValue(value); if (referencePosition?.rect) { this.adjustPosition(referencePosition.rect); } From 1e610c9510c75aa6123b8ee623b9b070c8141f5f Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Tue, 28 Jul 2026 14:46:11 +0800 Subject: [PATCH 26/77] fix(gantt): sync inline subtask record updates --- packages/vtable-gantt/src/Gantt.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/vtable-gantt/src/Gantt.ts b/packages/vtable-gantt/src/Gantt.ts index 0cc5482e3d..0cfcca00c1 100644 --- a/packages/vtable-gantt/src/Gantt.ts +++ b/packages/vtable-gantt/src/Gantt.ts @@ -1262,6 +1262,7 @@ export class Gantt extends EventTarget { } this._refreshSortedTaskBarsAfterRecordUpdate(recordIndex, index); } else if (Array.isArray(sub_task_index)) { + this._updateRecordToListTable(taskRecord, sub_task_index); // 递归更新父级project任务的时间范围 this.stateManager.updateProjectTaskTimes(sub_task_index); } @@ -1284,6 +1285,7 @@ export class Gantt extends EventTarget { } this._refreshSortedTaskBarsAfterRecordUpdate(recordIndex, index); } else if (Array.isArray(sub_task_index)) { + this._updateRecordToListTable(taskRecord, sub_task_index); // 递归更新父级project任务的时间范围 this.stateManager.updateProjectTaskTimes(sub_task_index); } @@ -1308,6 +1310,7 @@ export class Gantt extends EventTarget { } this._refreshSortedTaskBarsAfterRecordUpdate(recordIndex, index); } else if (Array.isArray(sub_task_index)) { + this._updateRecordToListTable(taskRecord, sub_task_index); // 递归更新父级project任务的时间范围 this.stateManager.updateProjectTaskTimes(sub_task_index); } @@ -1324,6 +1327,11 @@ export class Gantt extends EventTarget { const progressField = this.parsedOptions.progressField; if (progressField) { taskRecord[progressField] = progress; + if (Array.isArray(sub_task_index)) { + this._updateRecordToListTable(taskRecord, sub_task_index); + this._refreshTaskBar(index, sub_task_index); + return; + } const recordIndex = this.getRecordIndexByTaskShowIndex(index); this._updateRecordToListTable(taskRecord, Array.isArray(recordIndex) ? recordIndex : index); if (!this._refreshSortedTaskBarsAfterRecordUpdate(recordIndex, index)) { From a6ffe25162b2ee7f6a4be7f6d783fd4daedb70e3 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Tue, 28 Jul 2026 16:19:47 +0800 Subject: [PATCH 27/77] fix(sheet): infer add record rule --- packages/vtable-sheet/src/core/WorkSheet.ts | 22 ++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/vtable-sheet/src/core/WorkSheet.ts b/packages/vtable-sheet/src/core/WorkSheet.ts index 0473123dea..97d28a07a7 100644 --- a/packages/vtable-sheet/src/core/WorkSheet.ts +++ b/packages/vtable-sheet/src/core/WorkSheet.ts @@ -76,6 +76,25 @@ const normalizeColumnsField = (columns: IWorkSheetOptions['columns'], startField return fieldIndex; }; +const hasObjectFieldColumn = (columns: IWorkSheetOptions['columns'] = []): boolean => + columns.some(column => { + const childColumns = (column as IWorkSheetOptions['columns'][number] & { columns?: IWorkSheetOptions['columns'] }) + .columns; + if (childColumns?.length) { + return hasObjectFieldColumn(childColumns); + } + return isValid(column.field) && typeof column.field !== 'number'; + }); + +const getAddRecordRule = (options: IWorkSheetOptions): ListTableConstructorOptions['addRecordRule'] => { + if (options.addRecordRule) { + return options.addRecordRule; + } + + const hasObjectRecord = options.data?.some(record => record && typeof record === 'object' && !Array.isArray(record)); + return hasObjectRecord || hasObjectFieldColumn(options.columns) ? 'Object' : 'Array'; +}; + export class WorkSheet implements IWorkSheetAPI, IWorksheetEventSource { /** 选项 */ options: IWorkSheetOptions; @@ -333,12 +352,13 @@ export class WorkSheet implements IWorkSheetAPI, IWorksheetEventSource { showCopyCellBorder: true, cutSelected: true }; + const addRecordRule = getAddRecordRule(this.options); return { ...(this.options as any), dragOrder: { maintainArrayDataOrder: true }, - addRecordRule: 'Array', + addRecordRule, syncRecordOperationsToSourceRecords: true, defaultCursor: 'cell', records: this.options.data, From eb8da9783266751db71c62f74319a8c6772cbc85 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Tue, 28 Jul 2026 16:56:19 +0800 Subject: [PATCH 28/77] fix(sheet): complete object record support --- packages/vtable-sheet/src/core/WorkSheet.ts | 163 ++++++++++++++++-- .../vtable-sheet/src/core/table-plugins.ts | 3 +- 2 files changed, 146 insertions(+), 20 deletions(-) diff --git a/packages/vtable-sheet/src/core/WorkSheet.ts b/packages/vtable-sheet/src/core/WorkSheet.ts index 97d28a07a7..e5c8c16a33 100644 --- a/packages/vtable-sheet/src/core/WorkSheet.ts +++ b/packages/vtable-sheet/src/core/WorkSheet.ts @@ -48,6 +48,14 @@ type WorkSheetUpdateOptions = Pick< theme?: TYPES.VTableThemes.ITableThemeDefine; }; +type WorkSheetColumn = IWorkSheetOptions['columns'][number] & { + columns?: IWorkSheetOptions['columns']; + children?: IWorkSheetOptions['columns']; +}; + +const getChildColumns = (column: IWorkSheetOptions['columns'][number]): IWorkSheetOptions['columns'] | undefined => + (column as WorkSheetColumn).columns ?? (column as WorkSheetColumn).children; + const normalizeColumnsField = (columns: IWorkSheetOptions['columns'], startFieldIndex = 0): number => { if (!columns?.length) { return startFieldIndex; @@ -56,8 +64,7 @@ const normalizeColumnsField = (columns: IWorkSheetOptions['columns'], startField let fieldIndex = startFieldIndex; columns.forEach(column => { - const childColumns = (column as IWorkSheetOptions['columns'][number] & { columns?: IWorkSheetOptions['columns'] }) - .columns; + const childColumns = getChildColumns(column); if (childColumns?.length) { fieldIndex = normalizeColumnsField(childColumns, fieldIndex); @@ -76,10 +83,24 @@ const normalizeColumnsField = (columns: IWorkSheetOptions['columns'], startField return fieldIndex; }; +const collectLeafColumns = (columns: IWorkSheetOptions['columns'] = []): IWorkSheetOptions['columns'] => { + const leafColumns: IWorkSheetOptions['columns'] = []; + + columns.forEach(column => { + const childColumns = getChildColumns(column); + if (childColumns?.length) { + leafColumns.push(...collectLeafColumns(childColumns)); + return; + } + leafColumns.push(column); + }); + + return leafColumns; +}; + const hasObjectFieldColumn = (columns: IWorkSheetOptions['columns'] = []): boolean => columns.some(column => { - const childColumns = (column as IWorkSheetOptions['columns'][number] & { columns?: IWorkSheetOptions['columns'] }) - .columns; + const childColumns = getChildColumns(column); if (childColumns?.length) { return hasObjectFieldColumn(childColumns); } @@ -95,6 +116,54 @@ const getAddRecordRule = (options: IWorkSheetOptions): ListTableConstructorOptio return hasObjectRecord || hasObjectFieldColumn(options.columns) ? 'Object' : 'Array'; }; +const getValueByField = (record: Record, field: any): any => { + if (Array.isArray(field)) { + return field.reduce((value, key) => value?.[key], record); + } + if (typeof field === 'string') { + if (Object.prototype.hasOwnProperty.call(record, field)) { + return record[field]; + } + return field.split('.').reduce((value, key) => value?.[key], record); + } + return record[field]; +}; + +const setValueByField = (record: Record, field: any, value: any): void => { + if (Array.isArray(field)) { + let target = record; + field.forEach((key, index) => { + if (index === field.length - 1) { + target[key] = value; + return; + } + if (typeof target[key] !== 'object' || target[key] === null) { + target[key] = {}; + } + target = target[key]; + }); + return; + } + + if (typeof field === 'string' && !Object.prototype.hasOwnProperty.call(record, field) && field.includes('.')) { + const keys = field.split('.'); + let target = record; + keys.forEach((key, index) => { + if (index === keys.length - 1) { + target[key] = value; + return; + } + if (typeof target[key] !== 'object' || target[key] === null) { + target[key] = {}; + } + target = target[key]; + }); + return; + } + + record[field] = value; +}; + export class WorkSheet implements IWorkSheetAPI, IWorksheetEventSource { /** 选项 */ options: IWorkSheetOptions; @@ -807,9 +876,66 @@ export class WorkSheet implements IWorkSheetAPI, IWorksheetEventSource { return this.options.data || []; } + private getLeafColumns(): IWorkSheetOptions['columns'] { + return collectLeafColumns(this.options.columns); + } + + private getDataCellByTableCell(col: number, row: number): { col: number; row: number } | null { + const bodyIndex = this.tableInstance?.getBodyIndexByTableIndex?.(col, row) ?? { col, row }; + if (bodyIndex.col < 0 || bodyIndex.row < 0) { + return null; + } + return bodyIndex; + } + + private getDataCellValue(col: number, row: number): any { + const rowData = this.getData()[row]; + if (Array.isArray(rowData)) { + return rowData[col]; + } + if (rowData && typeof rowData === 'object') { + const field = this.getLeafColumns()[col]?.field; + if (isValid(field)) { + return getValueByField(rowData as Record, field); + } + } + return undefined; + } + + private getDataColumnCount(rowData: any): number { + return Array.isArray(rowData) ? rowData.length : this.getLeafColumns().length; + } + + private setDataCellValue(col: number, row: number, value: any, tableCol?: number, tableRow?: number): void { + const rowData = this.getData()[row]; + if (Array.isArray(rowData)) { + rowData[col] = value; + } else if (rowData && typeof rowData === 'object') { + const field = this.getLeafColumns()[col]?.field; + if (isValid(field)) { + setValueByField(rowData as Record, field, value); + } + } + + if (this.tableInstance) { + const tableIndex = + isValid(tableCol) && isValid(tableRow) + ? { col: tableCol, row: tableRow } + : this.tableInstance.getTableIndexByBodyIndex(col, row); + this.tableInstance.changeCellValue(tableIndex.col, tableIndex.row, value); + } + } + getCopiedData(): any[][] { // 为了避免影响当前数据,所以需要复制一份数据 - return this.getData().map(row => (Array.isArray(row) ? row.slice() : [])); + const leafColumns = this.getLeafColumns(); + return this.getData().map(row => + Array.isArray(row) + ? row.slice() + : leafColumns.map(column => + row && typeof row === 'object' ? getValueByField(row as Record, column.field) : undefined + ) + ); } /** * 获取指定坐标的单元格值 @@ -830,7 +956,8 @@ export class WorkSheet implements IWorkSheetAPI, IWorksheetEventSource { if (Array.isArray(rowData) && rowData[col] !== undefined) { return rowData[col]; } - return null; + const dataCell = this.getDataCellByTableCell(col, row); + return dataCell ? this.getDataCellValue(dataCell.col, dataCell.row) ?? null : null; } /** * 获取指定坐标的单元格值 @@ -868,14 +995,9 @@ export class WorkSheet implements IWorkSheetAPI, IWorksheetEventSource { * @param value 新值 */ setCellValue(col: number, row: number, value: any): void { - const rowData = this.getData()[row]; - if (Array.isArray(rowData)) { - rowData[col] = value; - - // 更新表格实例 - if (this.tableInstance) { - this.tableInstance.changeCellValue(col, row, value); - } + const dataCell = this.getDataCellByTableCell(col, row); + if (dataCell) { + this.setDataCellValue(dataCell.col, dataCell.row, value, col, row); } } @@ -1094,7 +1216,6 @@ export class WorkSheet implements IWorkSheetAPI, IWorksheetEventSource { return []; } - const data = this.getData(); const result: string[][] = []; // 获取第一个选择范围 @@ -1107,8 +1228,9 @@ export class WorkSheet implements IWorkSheetAPI, IWorksheetEventSource { for (let col = 0; col < cols; col++) { const actualRow = selection.startRow + row; const actualCol = selection.startCol + col; + const dataCell = this.getDataCellByTableCell(actualCol, actualRow); - if (data[actualRow] && data[actualRow][actualCol] !== undefined) { + if (dataCell && this.getData()[dataCell.row]) { // 如果是公式,返回公式字符串;否则返回值 if ( this.vtableSheet.formulaManager.isCellFormula({ @@ -1124,7 +1246,7 @@ export class WorkSheet implements IWorkSheetAPI, IWorksheetEventSource { }); rowData.push(formula); } else { - rowData.push(data[actualRow][actualCol]); + rowData.push(this.getDataCellValue(dataCell.col, dataCell.row) ?? ''); } } else { rowData.push(''); @@ -1169,15 +1291,18 @@ export class WorkSheet implements IWorkSheetAPI, IWorksheetEventSource { for (let col = 0; col < processedData[row].length; col++) { const targetRow = targetStartRow + row; const targetCol = targetStartCol + col; + const dataCell = this.getDataCellByTableCell(targetCol, targetRow); + const rowData = dataCell ? dataArray[dataCell.row] : undefined; + const columnCount = this.getDataColumnCount(rowData); - if (targetRow < dataArray.length && targetCol < dataArray[targetRow].length) { + if (rowData && dataCell && dataCell.col < columnCount) { const value = processedData[row][col]; // 如果是公式,设置公式;否则设置普通值 if (FormulaPasteProcessor.needsFormulaAdjustment(value)) { this.setCellFormula(targetRow, targetCol, value as string); } else { - this.setCellValue(targetRow, targetCol, value); + this.setDataCellValue(dataCell.col, dataCell.row, value, targetCol, targetRow); } } } diff --git a/packages/vtable-sheet/src/core/table-plugins.ts b/packages/vtable-sheet/src/core/table-plugins.ts index 6b1fca355b..322d6470c9 100644 --- a/packages/vtable-sheet/src/core/table-plugins.ts +++ b/packages/vtable-sheet/src/core/table-plugins.ts @@ -77,7 +77,8 @@ export function getTablePlugins( } else { const addRowColumnPlugin = new AddRowColumnPlugin({ addRowCallback: (row: number, tableInstance: VTable.ListTable) => { - tableInstance.addRecord([], row - tableInstance.columnHeaderLevelCount); + const record = tableInstance.options.addRecordRule === 'Object' ? {} : []; + tableInstance.addRecord(record, row - tableInstance.columnHeaderLevelCount); }, ...userPluginOptions }); From e2208e4f5f8111d952604fb3de0e26d0f63e04d9 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Tue, 28 Jul 2026 17:00:46 +0800 Subject: [PATCH 29/77] fix: support context menu on canvas blank area --- ...-context-menu-canvas_2026-07-28-00-00.json | 11 +++++ ...sue-5215-plugin-init_2026-07-28-00-00.json | 11 +++++ .../context-menu/handle-menu-helper.test.ts | 27 ++++++++++++ .../issue-5215-context-menu-canvas.ts | 44 +++++++++++++++++++ packages/vtable-plugins/demo/menu.ts | 4 ++ packages/vtable-plugins/src/context-menu.ts | 31 ++++++++++++- packages/vtable/src/plugins/interface.ts | 6 ++- packages/vtable/src/plugins/plugin-manager.ts | 2 + 8 files changed, 133 insertions(+), 3 deletions(-) create mode 100644 common/changes/@visactor/vtable-plugins/fix-issue-5215-context-menu-canvas_2026-07-28-00-00.json create mode 100644 common/changes/@visactor/vtable/fix-issue-5215-plugin-init_2026-07-28-00-00.json create mode 100644 packages/vtable-plugins/demo/context-menu/issue-5215-context-menu-canvas.ts diff --git a/common/changes/@visactor/vtable-plugins/fix-issue-5215-context-menu-canvas_2026-07-28-00-00.json b/common/changes/@visactor/vtable-plugins/fix-issue-5215-context-menu-canvas_2026-07-28-00-00.json new file mode 100644 index 0000000000..54808331df --- /dev/null +++ b/common/changes/@visactor/vtable-plugins/fix-issue-5215-context-menu-canvas_2026-07-28-00-00.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "fix: support context menu plugin on canvas blank area", + "type": "patch", + "packageName": "@visactor/vtable-plugins" + } + ], + "packageName": "@visactor/vtable-plugins", + "email": "github@visactor.io" +} diff --git a/common/changes/@visactor/vtable/fix-issue-5215-plugin-init_2026-07-28-00-00.json b/common/changes/@visactor/vtable/fix-issue-5215-plugin-init_2026-07-28-00-00.json new file mode 100644 index 0000000000..674f58f075 --- /dev/null +++ b/common/changes/@visactor/vtable/fix-issue-5215-plugin-init_2026-07-28-00-00.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "fix: allow plugins to initialize before first render", + "type": "patch", + "packageName": "@visactor/vtable" + } + ], + "packageName": "@visactor/vtable", + "email": "github@visactor.io" +} diff --git a/packages/vtable-plugins/__tests__/context-menu/handle-menu-helper.test.ts b/packages/vtable-plugins/__tests__/context-menu/handle-menu-helper.test.ts index 1c777b352a..9ef542b030 100644 --- a/packages/vtable-plugins/__tests__/context-menu/handle-menu-helper.test.ts +++ b/packages/vtable-plugins/__tests__/context-menu/handle-menu-helper.test.ts @@ -1,6 +1,7 @@ // @ts-nocheck import { ListTable } from '@visactor/vtable'; import { createDiv } from '../../../vtable/__tests__/dom'; +import { ContextMenuPlugin } from '../../src/context-menu'; import { MenuHandler } from '../../src/contextmenu/handle-menu-helper'; import { TableSeriesNumber } from '../../src/table-series-number'; @@ -63,3 +64,29 @@ describe('Context menu row deletion', () => { expect(table.records.map(record => record.id)).toEqual([0, 4]); }); }); + +describe('Context menu canvas option', () => { + let table: ListTable; + + afterEach(() => { + table?.release(); + document.body.innerHTML = ''; + }); + + test('ContextMenuPlugin enables canvas context menu through contextMenuWorkOnlyCell', () => { + const container = createDiv(); + const contextMenuPlugin = new ContextMenuPlugin({ + contextMenuWorkOnlyCell: false + }); + + table = new ListTable({ + container, + columns: [{ field: 'id', title: 'ID' }], + records: [{ id: 1 }], + plugins: [contextMenuPlugin] + }); + + expect(table.options.menu.contextMenuWorkOnlyCell).toBe(false); + expect(contextMenuPlugin.runTime).toContain(ListTable.EVENT_TYPE.CONTEXTMENU_CANVAS); + }); +}); diff --git a/packages/vtable-plugins/demo/context-menu/issue-5215-context-menu-canvas.ts b/packages/vtable-plugins/demo/context-menu/issue-5215-context-menu-canvas.ts new file mode 100644 index 0000000000..9b14e1f8d1 --- /dev/null +++ b/packages/vtable-plugins/demo/context-menu/issue-5215-context-menu-canvas.ts @@ -0,0 +1,44 @@ +import * as VTable from '@visactor/vtable'; +import { ContextMenuPlugin } from '../../src/context-menu'; + +const CONTAINER_ID = 'vTable'; + +export function createTableInstance() { + const records = [ + { id: 1, name: 'alpha', value: 12 }, + { id: 2, name: 'beta', value: 34 } + ]; + + const plugin = new ContextMenuPlugin({ + contextMenuWorkOnlyCell: false, + bodyCellMenuItems: [ + { text: 'Copy from plugin', menuKey: 'copy' }, + { text: 'Canvas menu item', menuKey: 'canvas_menu_item' } + ], + beforeShowAdjustMenuItems: (menuItems, _table, col, row) => { + if (col === -1 && row === -1) { + return [{ text: 'Canvas blank area', menuKey: 'canvas_blank_area' }, ...menuItems]; + } + return menuItems; + } + }); + + const option: VTable.ListTableConstructorOptions = { + container: document.getElementById(CONTAINER_ID), + records, + columns: [ + { field: 'id', title: 'ID', width: 100 }, + { field: 'name', title: 'Name', width: 160 }, + { field: 'value', title: 'Value', width: 120 } + ], + defaultRowHeight: 40, + defaultHeaderRowHeight: 40, + widthMode: 'standard', + heightMode: 'standard', + plugins: [plugin] + }; + + const tableInstance = new VTable.ListTable(option); + window.tableInstance = tableInstance; + return tableInstance; +} diff --git a/packages/vtable-plugins/demo/menu.ts b/packages/vtable-plugins/demo/menu.ts index 655f873fee..b252dd55ad 100644 --- a/packages/vtable-plugins/demo/menu.ts +++ b/packages/vtable-plugins/demo/menu.ts @@ -152,6 +152,10 @@ export const menus = [ path: 'context-menu', name: 'context-menu' }, + { + path: 'context-menu', + name: 'issue-5215-context-menu-canvas' + }, { path: 'context-menu', name: 'issue-5214-reverse-selected-row-delete' diff --git a/packages/vtable-plugins/src/context-menu.ts b/packages/vtable-plugins/src/context-menu.ts index 6ecc46b94f..9dcbd8abfd 100644 --- a/packages/vtable-plugins/src/context-menu.ts +++ b/packages/vtable-plugins/src/context-menu.ts @@ -29,6 +29,8 @@ export interface ContextMenuOptions { headerCellMenuItems?: MenuItemOrSeparator[]; /** 表体菜单项 */ bodyCellMenuItems?: MenuItemOrSeparator[]; + /** 右键菜单是否只工作在单元格上。默认 true;配置 false 时空白画布区域也弹出菜单。 */ + contextMenuWorkOnlyCell?: boolean; /** 自定义菜单样式 */ customMenuAttributions?: MenuAttributions; /** 菜单点击回调。如果设置是函数,则忽略内部默认的菜单项处理逻辑。如果这里配置的是个对象(对象的key为menuKey),则有匹配的menuKey时忽略内部默认的菜单项处理逻辑, @@ -56,7 +58,7 @@ export type MenuClickCallback = (args: MenuClickEventArgs, table: ListTable) => export class ContextMenuPlugin implements pluginsDefinition.IVTablePlugin { id = `context-menu`; name = 'Context Menu'; - runTime = [TABLE_EVENT_TYPE.CONTEXTMENU_CELL, TABLE_EVENT_TYPE.PLUGIN_EVENT]; + runTime = [TABLE_EVENT_TYPE.CONTEXTMENU_CELL, TABLE_EVENT_TYPE.CONTEXTMENU_CANVAS, TABLE_EVENT_TYPE.PLUGIN_EVENT]; pluginOptions: ContextMenuOptions; table: ListTable; /** 菜单管理器 */ @@ -156,6 +158,22 @@ export class ContextMenuPlugin implements pluginsDefinition.IVTablePlugin { } }; + /** + * 处理空白画布右键菜单事件 + */ + private handleContextMenuCanvas = (eventArgs: any, table: BaseTableAPI): void => { + let menuItems = this.pluginOptions.bodyCellMenuItems || []; + const { col = -1, row = -1 } = eventArgs; + + if (this.pluginOptions.beforeShowAdjustMenuItems) { + menuItems = this.pluginOptions.beforeShowAdjustMenuItems(menuItems, table as ListTable, col, row); + } + + if (menuItems.length > 0) { + this.showContextMenu(menuItems, eventArgs.event.clientX, eventArgs.event.clientY, col, row); + } + }; + /** * 处理插件事件 */ @@ -191,6 +209,15 @@ export class ContextMenuPlugin implements pluginsDefinition.IVTablePlugin { /** * 运行插件 */ + init(_table: BaseTableAPI, options: BaseTableAPI['options']) { + if (this.pluginOptions.contextMenuWorkOnlyCell === false) { + options.menu = { + ...options.menu, + contextMenuWorkOnlyCell: false + }; + } + } + run(...args: any[]) { const eventArgs = args[0]; const runTime = args[1]; @@ -203,6 +230,8 @@ export class ContextMenuPlugin implements pluginsDefinition.IVTablePlugin { // 根据事件类型处理不同的右键菜单 if (runTime === TABLE_EVENT_TYPE.CONTEXTMENU_CELL) { this.handleContextMenuCell(eventArgs, table); + } else if (runTime === TABLE_EVENT_TYPE.CONTEXTMENU_CANVAS) { + this.handleContextMenuCanvas(eventArgs, table); } else if (runTime === TABLE_EVENT_TYPE.PLUGIN_EVENT) { this.handlePluginEvent(eventArgs, table); } diff --git a/packages/vtable/src/plugins/interface.ts b/packages/vtable/src/plugins/interface.ts index 8119b3dfb7..2f7adb9998 100644 --- a/packages/vtable/src/plugins/interface.ts +++ b/packages/vtable/src/plugins/interface.ts @@ -1,5 +1,5 @@ import type { TableEvents } from '../core/TABLE_EVENT_TYPE'; -import type { BaseTableAPI } from '../ts-types/base-table'; +import type { BaseTableAPI, BaseTableConstructorOptions } from '../ts-types/base-table'; // 插件生命周期接口 export interface IVTablePlugin { @@ -16,7 +16,9 @@ export interface IVTablePlugin { runTime: TableEvents[keyof TableEvents][]; // // 插件依赖 // dependencies?: string[]; - // 初始化方法,在VTable实例创建后、首次渲染前调用 + // 初始化方法,在 VTable 实例创建后、首次渲染前调用 + init?: (table: BaseTableAPI, options: BaseTableConstructorOptions) => void; + // 运行方法,在订阅的表格事件触发时调用 run: (...args: any[]) => void; // 更新方法,当表格数据或配置更新时调用 update?: () => void; diff --git a/packages/vtable/src/plugins/plugin-manager.ts b/packages/vtable/src/plugins/plugin-manager.ts index e984eac6df..4c2838ad93 100644 --- a/packages/vtable/src/plugins/plugin-manager.ts +++ b/packages/vtable/src/plugins/plugin-manager.ts @@ -12,6 +12,7 @@ export class PluginManager { this.table = table; options.plugins?.map(plugin => { this.register(plugin); + plugin.init?.(this.table, options); this._bindTableEventForPlugin(plugin); }); } @@ -59,6 +60,7 @@ export class PluginManager { const addedPlugins = plugins?.filter(plugin => !this.plugins.has(plugin.id)); addedPlugins?.forEach(plugin => { this.register(plugin); + plugin.init?.(this.table, this.table.options); this._bindTableEventForPlugin(plugin); }); } From 1befd95053078cf3b15a3bceb1846a5f17c3eadd Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Tue, 28 Jul 2026 17:01:19 +0800 Subject: [PATCH 30/77] fix: support checkbox state by record index --- ...heckbox-record-index_2026-07-28-00-00.json | 11 ++ docs/assets/api/en/methods.md | 22 +++ docs/assets/api/zh/methods.md | 22 +++ .../listTable-checkbox-record-index.test.ts | 72 +++++++++ .../issue-5211-tree-checkbox-record-index.ts | 79 ++++++++++ packages/vtable/examples/menu.ts | 4 + packages/vtable/src/ListTable.ts | 20 ++- .../vtable/src/state/checkbox/checkbox.ts | 137 +++++++++++++++++- packages/vtable/src/ts-types/table-engine.ts | 19 +++ 9 files changed, 384 insertions(+), 2 deletions(-) create mode 100644 common/changes/@visactor/vtable/fix-issues-5211-checkbox-record-index_2026-07-28-00-00.json create mode 100644 packages/vtable/__tests__/listTable-checkbox-record-index.test.ts create mode 100644 packages/vtable/examples/list/issue-5211-tree-checkbox-record-index.ts diff --git a/common/changes/@visactor/vtable/fix-issues-5211-checkbox-record-index_2026-07-28-00-00.json b/common/changes/@visactor/vtable/fix-issues-5211-checkbox-record-index_2026-07-28-00-00.json new file mode 100644 index 0000000000..bf7b04563f --- /dev/null +++ b/common/changes/@visactor/vtable/fix-issues-5211-checkbox-record-index_2026-07-28-00-00.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "fix: support updating checkbox state by record index", + "type": "patch", + "packageName": "@visactor/vtable" + } + ], + "packageName": "@visactor/vtable", + "email": "github@visactor.io" +} diff --git a/docs/assets/api/en/methods.md b/docs/assets/api/en/methods.md index f41c4a192c..2baced55dc 100644 --- a/docs/assets/api/en/methods.md +++ b/docs/assets/api/en/methods.md @@ -2143,6 +2143,28 @@ setCellCheckboxState(col: number, row: number, checked: boolean) => void - row: Row number - checked: Whether selected +## setCellCheckboxStateByRecordIndex(Function) + +Set the checkbox state by source records index and field. For tree tables, pass a children path such as `[0, 1]` for the second child of the first root record. The state is updated even when the target node is collapsed and not currently visible. + +``` +setCellCheckboxStateByRecordIndex(recordIndex: number | number[], field: string | number, checked: boolean | 'indeterminate') => void +``` + +- recordIndex: Source data index; number for normal tables, number[] for tree tables +- field: Field of the checkbox column +- checked: Checkbox state, including `'indeterminate'` + +## clearCheckboxState(Function) + +Clear all checkbox checked states under the specified field. `clearAllCheckboxState(field)` is an alias of this method. + +``` +clearCheckboxState(field: string | number) => void +``` + +- field: Field of the checkbox column + ## setCellRadioState(Function) Set the radio state of the cell to selected state diff --git a/docs/assets/api/zh/methods.md b/docs/assets/api/zh/methods.md index e10c0b8330..f7557c9c82 100644 --- a/docs/assets/api/zh/methods.md +++ b/docs/assets/api/zh/methods.md @@ -2142,6 +2142,28 @@ setCellCheckboxState(col: number, row: number, checked: boolean) => void - row: 行号 - checked: 是否选中 +## setCellCheckboxStateByRecordIndex(Function) + +根据源数据 records 的 index 和 field 设置 checkbox 状态。树形表格可传入 children 路径,例如 `[0, 1]` 表示第 1 条根节点下第 2 条子节点;即使该节点当前处于折叠不可见状态,也会更新其 checkbox 状态。 + +``` +setCellCheckboxStateByRecordIndex(recordIndex: number | number[], field: string | number, checked: boolean | 'indeterminate') => void +``` + +- recordIndex: 源数据索引;普通表格为 number,树形表格为 number[] +- field: checkbox 所在字段 +- checked: 是否选中,支持半选状态 `'indeterminate'` + +## clearCheckboxState(Function) + +清除指定 field 下所有 checkbox 的选中状态。`clearAllCheckboxState(field)` 是该方法的别名。 + +``` +clearCheckboxState(field: string | number) => void +``` + +- field: checkbox 所在字段 + ## setCellRadioState(Function) 将单元格的 radio 状态设置为选中状态 diff --git a/packages/vtable/__tests__/listTable-checkbox-record-index.test.ts b/packages/vtable/__tests__/listTable-checkbox-record-index.test.ts new file mode 100644 index 0000000000..594cd6042d --- /dev/null +++ b/packages/vtable/__tests__/listTable-checkbox-record-index.test.ts @@ -0,0 +1,72 @@ +// @ts-nocheck +import { ListTable, TYPES } from '../src'; +import { createDiv } from './dom'; + +global.__VERSION__ = 'none'; + +describe('ListTable checkbox record index api', () => { + let table: ListTable; + + afterEach(() => { + table?.release(); + document.body.innerHTML = ''; + }); + + test('sets collapsed tree child checkbox state by record index path', () => { + table = new ListTable({ + container: createDiv(), + columns: [ + { + field: 'task', + title: 'Task', + tree: true, + cellType: 'checkbox', + headerType: 'checkbox' + } + ], + records: [ + { + task: { text: 'parent', checked: true }, + hierarchyState: TYPES.HierarchyState.collapse, + children: [{ task: { text: 'child', checked: true } }] + } + ], + enableCheckboxCascade: false + }); + + table.setCellCheckboxStateByRecordIndex([0, 0], 'task', false); + + expect(table.stateManager.checkedState.get('0,0').task).toBe(false); + }); + + test('clears all checkbox states for a field', () => { + table = new ListTable({ + container: createDiv(), + columns: [ + { + field: 'task', + title: 'Task', + tree: true, + cellType: 'checkbox', + headerType: 'checkbox' + } + ], + records: [ + { + task: { text: 'parent', checked: true }, + hierarchyState: TYPES.HierarchyState.collapse, + children: [{ task: { text: 'child', checked: true } }] + }, + { task: { text: 'sibling', checked: true } } + ], + enableCheckboxCascade: false + }); + + table.clearAllCheckboxState('task'); + + expect(table.stateManager.checkedState.get('0').task).toBe(false); + expect(table.stateManager.checkedState.get('0,0').task).toBe(false); + expect(table.stateManager.checkedState.get('1').task).toBe(false); + expect(table.stateManager.headerCheckedState.task).toBe(false); + }); +}); diff --git a/packages/vtable/examples/list/issue-5211-tree-checkbox-record-index.ts b/packages/vtable/examples/list/issue-5211-tree-checkbox-record-index.ts new file mode 100644 index 0000000000..5c8de24968 --- /dev/null +++ b/packages/vtable/examples/list/issue-5211-tree-checkbox-record-index.ts @@ -0,0 +1,79 @@ +import * as VTable from '../../src'; + +const CONTAINER_ID = 'vTable'; + +export function createTable() { + const records = [ + { + task: { text: 'Project A', checked: true }, + owner: 'Alice', + status: 'collapsed parent', + hierarchyState: VTable.TYPES.HierarchyState.collapse, + children: [ + { + task: { text: 'Hidden task A-1', checked: true }, + owner: 'Bob', + status: 'hidden child' + }, + { + task: { text: 'Hidden task A-2', checked: true }, + owner: 'Cindy', + status: 'hidden child' + } + ] + }, + { + task: { text: 'Project B', checked: true }, + owner: 'David', + status: 'root' + } + ]; + + const option: VTable.ListTableConstructorOptions = { + container: document.getElementById(CONTAINER_ID), + records, + columns: [ + { + field: 'task', + title: 'Task', + tree: true, + cellType: 'checkbox', + headerType: 'checkbox', + width: 260 + }, + { field: 'owner', title: 'Owner', width: 120 }, + { field: 'status', title: 'Status', width: 180 } + ], + defaultRowHeight: 38, + hierarchyIndent: 20, + enableCheckboxCascade: false, + enableHeaderCheckboxCascade: false, + theme: VTable.themes.BRIGHT + }; + + const tableInstance = new VTable.ListTable(option); + window.tableInstance = tableInstance; + + const toolbar = document.createElement('div'); + toolbar.style.cssText = 'position:absolute;top:8px;left:8px;z-index:10;display:flex;gap:8px;'; + + const setHiddenChildButton = document.createElement('button'); + setHiddenChildButton.textContent = 'Uncheck hidden child [0,0]'; + setHiddenChildButton.onclick = () => { + tableInstance.setCellCheckboxStateByRecordIndex([0, 0], 'task', false); + console.log('checkbox state after set hidden child:', tableInstance.getCheckboxState('task')); + }; + + const clearAllButton = document.createElement('button'); + clearAllButton.textContent = 'Clear task checkbox field'; + clearAllButton.onclick = () => { + tableInstance.clearAllCheckboxState('task'); + console.log('checkbox state after clear:', tableInstance.getCheckboxState('task')); + }; + + toolbar.appendChild(setHiddenChildButton); + toolbar.appendChild(clearAllButton); + document.body.appendChild(toolbar); + + return tableInstance; +} diff --git a/packages/vtable/examples/menu.ts b/packages/vtable/examples/menu.ts index 13b2c16107..34dbb96a68 100644 --- a/packages/vtable/examples/menu.ts +++ b/packages/vtable/examples/menu.ts @@ -123,6 +123,10 @@ export const menus = [ path: 'list', name: 'list-checkbox-tree' }, + { + path: 'list', + name: 'issue-5211-tree-checkbox-record-index' + }, { path: 'list', name: 'list-tree-20000' diff --git a/packages/vtable/src/ListTable.ts b/packages/vtable/src/ListTable.ts index 8366170c65..0dda560261 100644 --- a/packages/vtable/src/ListTable.ts +++ b/packages/vtable/src/ListTable.ts @@ -39,7 +39,12 @@ import type { IEditor } from '@visactor/vtable-editors'; import type { ColumnData, ColumnDefine, HeaderData } from './ts-types/list-table/layout-map/api'; import { getCellRadioState, setCellRadioState } from './state/radio/radio'; import { cloneDeepSpec } from '@visactor/vutils-extension'; -import { getGroupCheckboxState, setCellCheckboxState } from './state/checkbox/checkbox'; +import { + clearCheckboxState, + getGroupCheckboxState, + setCellCheckboxState, + setCheckboxStateByRecordIndex +} from './state/checkbox/checkbox'; import type { IEmptyTipComponent } from './components/empty-tip/empty-tip'; import { Factory } from './core/factory'; import { getGroupByDataConfig } from './core/group-helper'; @@ -1568,6 +1573,19 @@ export class ListTable extends BaseTable implements ListTableAPI { setCellCheckboxState(col: number, row: number, checked: boolean | 'indeterminate') { setCellCheckboxState(col, row, checked, this); } + setCellCheckboxStateByRecordIndex( + recordIndex: number | number[], + field: FieldDef, + checked: boolean | 'indeterminate' + ) { + setCheckboxStateByRecordIndex(recordIndex, field, checked, this); + } + clearCheckboxState(field: FieldDef) { + clearCheckboxState(field, this); + } + clearAllCheckboxState(field: FieldDef) { + this.clearCheckboxState(field); + } setCellRadioState(col: number, row: number, index?: number) { setCellRadioState(col, row, index, this); } diff --git a/packages/vtable/src/state/checkbox/checkbox.ts b/packages/vtable/src/state/checkbox/checkbox.ts index 9ac8d7eef0..93b5301a82 100644 --- a/packages/vtable/src/state/checkbox/checkbox.ts +++ b/packages/vtable/src/state/checkbox/checkbox.ts @@ -1,6 +1,6 @@ import { isArray, isFunction, isNumber, isObject, isValid } from '@visactor/vutils'; import type { StateManager } from '../state'; -import type { CheckboxColumnDefine, ListTableAPI } from '../../ts-types'; +import type { CheckboxColumnDefine, FieldDef, ListTableAPI } from '../../ts-types'; import { getOrApply } from '../../tools/helper'; import type { BaseTableAPI } from '../../ts-types/base-table'; import type { CachedDataSource } from '../../data'; @@ -304,6 +304,34 @@ export function setCellCheckboxState( } } +export function setCheckboxStateByRecordIndex( + recordIndex: number | number[], + field: FieldDef, + checked: boolean | 'indeterminate', + table: BaseTableAPI +) { + const dataIndex = normalizeRecordIndex(recordIndex).toString(); + if (table.stateManager.checkedState.get(dataIndex)) { + table.stateManager.checkedState.get(dataIndex)[field as string | number] = checked; + } else { + table.stateManager.checkedState.set(dataIndex, { + [field as string | number]: checked + }); + } + + updateVisibleCheckboxCellByRecordIndex(recordIndex, field, checked, table); + updateHeaderCheckboxStateByField(field, table); +} + +export function clearCheckboxState(field: FieldDef, table: BaseTableAPI) { + if (!isValid(field)) { + return; + } + updateAllRecordCheckboxState((table as any).records, field, false, table.stateManager.checkedState); + updateVisibleCheckboxCellsByField(field, false, table); + updateHeaderCheckboxStateByField(field, table); +} + export function setCellCheckboxStateByAttribute( col: number, row: number, @@ -404,6 +432,113 @@ export function getGroupCheckboxState(table: BaseTableAPI) { return result; } +function normalizeRecordIndex(recordIndex: number | number[]): number | number[] { + if (isArray(recordIndex) && recordIndex.length === 1) { + return recordIndex[0]; + } + return recordIndex; +} + +function updateAllRecordCheckboxState( + records: any[], + field: FieldDef, + checked: boolean | 'indeterminate', + checkedState: Map, + parentIndex: number[] = [] +) { + if (!isArray(records)) { + return; + } + + records.forEach((record, index) => { + const recordIndex = parentIndex.length ? parentIndex.concat(index) : [index]; + const dataIndex = normalizeRecordIndex(recordIndex).toString(); + if (checkedState.get(dataIndex)) { + checkedState.get(dataIndex)[field as string | number] = checked; + } else { + checkedState.set(dataIndex, { + [field as string | number]: checked + }); + } + + if (isArray(record?.children)) { + updateAllRecordCheckboxState(record.children, field, checked, checkedState, recordIndex); + } + }); +} + +function updateVisibleCheckboxCellByRecordIndex( + recordIndex: number | number[], + field: FieldDef, + checked: boolean | 'indeterminate', + table: BaseTableAPI +) { + const bodyRowIndex = (table as ListTableAPI).getBodyRowIndexByRecordIndex?.(recordIndex); + if (!isNumber(bodyRowIndex) || bodyRowIndex < 0) { + return; + } + + const row = bodyRowIndex + table.columnHeaderLevelCount; + const cols = getCheckboxColsByField(field, row, table); + cols.forEach(col => { + setCellCheckboxStateByAttribute(col, row, checked, table); + }); +} + +function updateVisibleCheckboxCellsByField(field: FieldDef, checked: boolean | 'indeterminate', table: BaseTableAPI) { + for (let row = table.columnHeaderLevelCount; row < table.rowCount; row++) { + getCheckboxColsByField(field, row, table).forEach(col => { + setCellCheckboxStateByAttribute(col, row, checked, table); + }); + } +} + +function updateHeaderCheckboxStateByField(field: FieldDef, table: BaseTableAPI) { + const fieldKey = field as string | number; + let hasChecked = false; + let hasUnchecked = false; + + table.stateManager.checkedState.forEach(checkState => { + if (checkState?.[fieldKey] === true) { + hasChecked = true; + } else { + hasUnchecked = true; + } + }); + + const checked = hasChecked && !hasUnchecked ? true : hasChecked ? 'indeterminate' : false; + table.stateManager.headerCheckedState[fieldKey] = checked; + updateVisibleHeaderCheckboxCellByField(field, checked, table); +} + +function updateVisibleHeaderCheckboxCellByField( + field: FieldDef, + checked: boolean | 'indeterminate', + table: BaseTableAPI +) { + for (let row = 0; row < table.columnHeaderLevelCount; row++) { + for (let col = 0; col < table.colCount; col++) { + if (table.getHeaderField(col, row) === field && table.getCellType(col, row) === 'checkbox') { + table.scenegraph.updateHeaderCheckboxCellState(col, row, checked); + } + } + } +} + +function getCheckboxColsByField(field: FieldDef, row: number, table: BaseTableAPI): number[] { + const cols: number[] = []; + for (let col = 0; col < table.colCount; col++) { + if (table.isHeader(col, row)) { + continue; + } + const define = table.getBodyColumnDefine(col, row); + if (define?.field === field && table.getCellType(col, row) === 'checkbox') { + cols.push(col); + } + } + return cols; +} + function initRecordCheckState(records: any[], state: StateManager) { const table = state.table; const start = table.isPivotTable() diff --git a/packages/vtable/src/ts-types/table-engine.ts b/packages/vtable/src/ts-types/table-engine.ts index 5655c12ad8..00f45593ec 100644 --- a/packages/vtable/src/ts-types/table-engine.ts +++ b/packages/vtable/src/ts-types/table-engine.ts @@ -458,6 +458,25 @@ export interface ListTableAPI extends BaseTableAPI { reapplySort?: boolean; clearRowHeightCache?: boolean; }) => void; + /** 获取某个字段下 checkbox 全部数据的选中状态,顺序对应原始 records。 */ + getCheckboxState: (field?: string | number) => any[]; + /** 获取某个单元格 checkbox 的状态。 */ + getCellCheckboxState: (col: number, row: number) => boolean | 'indeterminate' | undefined; + /** 设置某个可见单元格 checkbox 的状态。 */ + setCellCheckboxState: (col: number, row: number, checked: boolean | 'indeterminate') => void; + /** + * 根据源数据 records 的 index + field 设置 checkbox 状态。 + * recordIndex 为源数据中的索引:普通表格为 number;树形表格为 number[](children 路径)。 + */ + setCellCheckboxStateByRecordIndex: ( + recordIndex: number | number[], + field: FieldDef, + checked: boolean | 'indeterminate' + ) => void; + /** 清除指定 field 的全部 checkbox 选中状态。 */ + clearCheckboxState: (field: FieldDef) => void; + /** clearCheckboxState 的别名,兼容 issue 中提出的 API 命名。 */ + clearAllCheckboxState: (field: FieldDef) => void; getFieldData: (field: FieldDef | FieldFormat | undefined, col: number, row: number) => FieldData; //#region 编辑器相关demo /** 获取单元格配置的编辑器 */ From 902aca8b3266df8c8296dc0814111860c179f7f7 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Tue, 28 Jul 2026 17:01:56 +0800 Subject: [PATCH 31/77] fix: refresh vue custom layout after sort --- ...e-custom-layout-sort_2026-07-28-00-00.json | 11 +++ packages/vue-vtable/demo/src/App.vue | 2 + .../composition/Issue5150CustomLayoutSort.vue | 69 +++++++++++++++++++ .../custom/vtable-vue-attribute-plugin.ts | 5 +- 4 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 common/changes/@visactor/vue-vtable/fix-issue-5150-vue-custom-layout-sort_2026-07-28-00-00.json create mode 100644 packages/vue-vtable/demo/src/table/gramatical/composition/Issue5150CustomLayoutSort.vue diff --git a/common/changes/@visactor/vue-vtable/fix-issue-5150-vue-custom-layout-sort_2026-07-28-00-00.json b/common/changes/@visactor/vue-vtable/fix-issue-5150-vue-custom-layout-sort_2026-07-28-00-00.json new file mode 100644 index 0000000000..e3bf74b7f8 --- /dev/null +++ b/common/changes/@visactor/vue-vtable/fix-issue-5150-vue-custom-layout-sort_2026-07-28-00-00.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "fix: refresh vue custom layout dom content after sort", + "type": "patch", + "packageName": "@visactor/vue-vtable" + } + ], + "packageName": "@visactor/vue-vtable", + "email": "github@visactor.io" +} diff --git a/packages/vue-vtable/demo/src/App.vue b/packages/vue-vtable/demo/src/App.vue index a54581281b..e49cb43abc 100644 --- a/packages/vue-vtable/demo/src/App.vue +++ b/packages/vue-vtable/demo/src/App.vue @@ -19,6 +19,7 @@ import ListTableEditorRender from './table/gramatical/composition/ListTable-edit import ListTableDes from './table/gramatical/composition/ListTable-destruction.vue'; import ListTableCustom from './table/gramatical/composition/ListTable-custom.vue'; import ListTableCustomHover from './table/gramatical/composition/ListTable-custom-hover.vue'; +import Issue5150CustomLayoutSort from './table/gramatical/composition/Issue5150CustomLayoutSort.vue'; import ListTableVFor from './table/gramatical/options/ListTable-v-for.vue'; import PivotTable from './table/gramatical/options/PivotTable.vue'; @@ -51,6 +52,7 @@ import singleRadio from './table/single/single-radio.vue'; + diff --git a/packages/vue-vtable/demo/src/table/gramatical/composition/Issue5150CustomLayoutSort.vue b/packages/vue-vtable/demo/src/table/gramatical/composition/Issue5150CustomLayoutSort.vue new file mode 100644 index 0000000000..4f9d35e6ef --- /dev/null +++ b/packages/vue-vtable/demo/src/table/gramatical/composition/Issue5150CustomLayoutSort.vue @@ -0,0 +1,69 @@ + + + + + diff --git a/packages/vue-vtable/src/components/custom/vtable-vue-attribute-plugin.ts b/packages/vue-vtable/src/components/custom/vtable-vue-attribute-plugin.ts index 23656f05f9..579dd52474 100644 --- a/packages/vue-vtable/src/components/custom/vtable-vue-attribute-plugin.ts +++ b/packages/vue-vtable/src/components/custom/vtable-vue-attribute-plugin.ts @@ -158,10 +158,6 @@ export class VTableVueAttributePlugin extends HtmlAttributePlugin implements IPl wrapContainer.setAttribute('data-vue-renderId', dataRenderId); // 先隐藏 wrapContainer.style.display = 'none'; - if (!reuse) { - // 仅在非复用时需要重新渲染 - render(element, wrapContainer); - } targetMap = { wrapContainer, nativeContainer, @@ -181,6 +177,7 @@ export class VTableVueAttributePlugin extends HtmlAttributePlugin implements IPl targetMap.renderId = this.renderId; targetMap.graphic = graphic; targetMap.lastAccessed = Date.now(); + render(element, targetMap.wrapContainer); this.updateAccessQueue(id); this.updateStyleOfWrapContainer(graphic, stage, targetMap.wrapContainer, targetMap.nativeContainer); } From 257172269880e2328e861efd9c69e8bc3ae7eaf2 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Tue, 28 Jul 2026 18:13:52 +0800 Subject: [PATCH 32/77] fix: refine canvas context menu items --- .../context-menu/handle-menu-helper.test.ts | 44 +++++++++++++++++++ .../issue-5215-context-menu-canvas.ts | 7 ++- packages/vtable-plugins/src/context-menu.ts | 4 +- packages/vtable/src/plugins/plugin-manager.ts | 2 +- 4 files changed, 53 insertions(+), 4 deletions(-) diff --git a/packages/vtable-plugins/__tests__/context-menu/handle-menu-helper.test.ts b/packages/vtable-plugins/__tests__/context-menu/handle-menu-helper.test.ts index 9ef542b030..64a16340d3 100644 --- a/packages/vtable-plugins/__tests__/context-menu/handle-menu-helper.test.ts +++ b/packages/vtable-plugins/__tests__/context-menu/handle-menu-helper.test.ts @@ -89,4 +89,48 @@ describe('Context menu canvas option', () => { expect(table.options.menu.contextMenuWorkOnlyCell).toBe(false); expect(contextMenuPlugin.runTime).toContain(ListTable.EVENT_TYPE.CONTEXTMENU_CANVAS); }); + + test('ContextMenuPlugin shows canvasMenuItems on CONTEXTMENU_CANVAS event', () => { + const container = createDiv(); + const contextMenuPlugin = new ContextMenuPlugin({ + contextMenuWorkOnlyCell: false, + bodyCellMenuItems: [{ text: 'Body Item', menuKey: 'body_item' }], + canvasMenuItems: [{ text: 'Canvas Item', menuKey: 'canvas_item' }] + }); + const showMenu = jest.spyOn(contextMenuPlugin['menuManager'], 'showMenu'); + const preventDefault = jest.fn(); + + table = new ListTable({ + container, + columns: [{ field: 'id', title: 'ID' }], + records: [{ id: 1 }], + plugins: [contextMenuPlugin] + }); + + contextMenuPlugin.run( + { + col: -1, + row: -1, + event: { + clientX: 10, + clientY: 20, + preventDefault + } + }, + ListTable.EVENT_TYPE.CONTEXTMENU_CANVAS, + table + ); + + expect(preventDefault).toHaveBeenCalled(); + expect(showMenu).toHaveBeenCalledWith( + [{ text: 'Canvas Item', menuKey: 'canvas_item' }], + 10, + 20, + { + rowIndex: -1, + colIndex: -1 + }, + table + ); + }); }); diff --git a/packages/vtable-plugins/demo/context-menu/issue-5215-context-menu-canvas.ts b/packages/vtable-plugins/demo/context-menu/issue-5215-context-menu-canvas.ts index 9b14e1f8d1..30dbe7d01a 100644 --- a/packages/vtable-plugins/demo/context-menu/issue-5215-context-menu-canvas.ts +++ b/packages/vtable-plugins/demo/context-menu/issue-5215-context-menu-canvas.ts @@ -12,12 +12,15 @@ export function createTableInstance() { const plugin = new ContextMenuPlugin({ contextMenuWorkOnlyCell: false, bodyCellMenuItems: [ - { text: 'Copy from plugin', menuKey: 'copy' }, + { text: 'Copy from plugin', menuKey: 'copy' } + ], + canvasMenuItems: [ + { text: 'Canvas blank area', menuKey: 'canvas_blank_area' }, { text: 'Canvas menu item', menuKey: 'canvas_menu_item' } ], beforeShowAdjustMenuItems: (menuItems, _table, col, row) => { if (col === -1 && row === -1) { - return [{ text: 'Canvas blank area', menuKey: 'canvas_blank_area' }, ...menuItems]; + return menuItems; } return menuItems; } diff --git a/packages/vtable-plugins/src/context-menu.ts b/packages/vtable-plugins/src/context-menu.ts index 9dcbd8abfd..47f9cf06bc 100644 --- a/packages/vtable-plugins/src/context-menu.ts +++ b/packages/vtable-plugins/src/context-menu.ts @@ -29,6 +29,8 @@ export interface ContextMenuOptions { headerCellMenuItems?: MenuItemOrSeparator[]; /** 表体菜单项 */ bodyCellMenuItems?: MenuItemOrSeparator[]; + /** 空白画布区域菜单项。仅 contextMenuWorkOnlyCell 为 false 时生效。 */ + canvasMenuItems?: MenuItemOrSeparator[]; /** 右键菜单是否只工作在单元格上。默认 true;配置 false 时空白画布区域也弹出菜单。 */ contextMenuWorkOnlyCell?: boolean; /** 自定义菜单样式 */ @@ -162,7 +164,7 @@ export class ContextMenuPlugin implements pluginsDefinition.IVTablePlugin { * 处理空白画布右键菜单事件 */ private handleContextMenuCanvas = (eventArgs: any, table: BaseTableAPI): void => { - let menuItems = this.pluginOptions.bodyCellMenuItems || []; + let menuItems = this.pluginOptions.canvasMenuItems || []; const { col = -1, row = -1 } = eventArgs; if (this.pluginOptions.beforeShowAdjustMenuItems) { diff --git a/packages/vtable/src/plugins/plugin-manager.ts b/packages/vtable/src/plugins/plugin-manager.ts index 4c2838ad93..7f4f7f1773 100644 --- a/packages/vtable/src/plugins/plugin-manager.ts +++ b/packages/vtable/src/plugins/plugin-manager.ts @@ -10,7 +10,7 @@ export class PluginManager { constructor(table: BaseTableAPI, options: BaseTableConstructorOptions) { this.table = table; - options.plugins?.map(plugin => { + options.plugins?.forEach(plugin => { this.register(plugin); plugin.init?.(this.table, options); this._bindTableEventForPlugin(plugin); From 431b10295906bc9e2695632a39c2f237f593ff5a Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Tue, 28 Jul 2026 18:16:24 +0800 Subject: [PATCH 33/77] fix: refine checkbox record index state updates --- docs/assets/api/en/methods.md | 19 ++- docs/assets/api/zh/methods.md | 19 ++- .../listTable-checkbox-record-index.test.ts | 90 +++++++++++ .../vtable/src/state/checkbox/checkbox.ts | 151 +++++++++++++++--- 4 files changed, 253 insertions(+), 26 deletions(-) diff --git a/docs/assets/api/en/methods.md b/docs/assets/api/en/methods.md index 2baced55dc..d24cc48048 100644 --- a/docs/assets/api/en/methods.md +++ b/docs/assets/api/en/methods.md @@ -2107,14 +2107,21 @@ Get the selection state of all checkbox data under a field, the order correspond getCheckboxState(field?: string | number): Array ``` +- field: Optional checkbox field. If omitted, returns checkbox states for all fields +- return: Checkbox state array. Tree data keeps state by children path + ## getCellCheckboxState(Function) Get the state of a checkbox in a specific cell ``` -getCellCheckboxState(col: number, row: number): Array +getCellCheckboxState(col: number, row: number): boolean | 'indeterminate' | undefined ``` +- col: Column number +- row: Row number +- return: Checkbox state of the cell + ## getRadioState(Function) Get the selection state of all radio data under a field, the order corresponds to the original input data records, not the state value of the table display row @@ -2165,6 +2172,16 @@ clearCheckboxState(field: string | number) => void - field: Field of the checkbox column +## clearAllCheckboxState(Function) + +Alias of `clearCheckboxState(field)`. Clear all checkbox checked states under the specified field. + +``` +clearAllCheckboxState(field: string | number) => void +``` + +- field: Field of the checkbox column + ## setCellRadioState(Function) Set the radio state of the cell to selected state diff --git a/docs/assets/api/zh/methods.md b/docs/assets/api/zh/methods.md index f7557c9c82..0168642727 100644 --- a/docs/assets/api/zh/methods.md +++ b/docs/assets/api/zh/methods.md @@ -2106,14 +2106,21 @@ arrangeCustomCellStyle: (cellPosition: { col?: number; row?: number; range?: Cel getCheckboxState(field?: string | number): Array ``` +- field: 可选,checkbox 所在字段;不传时返回所有字段的 checkbox 状态 +- 返回值: checkbox 状态数组,树形数据会按 children 路径组织状态 + ## getCellCheckboxState(Function) 获取某个单元格 checkbox 的状态 ``` -getCellCheckboxState(col: number, row: number): Array +getCellCheckboxState(col: number, row: number): boolean | 'indeterminate' | undefined ``` +- col: 列号 +- row: 行号 +- 返回值: 当前单元格 checkbox 状态 + ## getRadioState(Function) 获取某个字段下 radio 全部数据的选中状态 顺序对应原始传入数据 records 不是对应表格展示 row 的状态值 @@ -2164,6 +2171,16 @@ clearCheckboxState(field: string | number) => void - field: checkbox 所在字段 +## clearAllCheckboxState(Function) + +`clearCheckboxState(field)` 的别名,用于清除指定 field 下所有 checkbox 的选中状态。 + +``` +clearAllCheckboxState(field: string | number) => void +``` + +- field: checkbox 所在字段 + ## setCellRadioState(Function) 将单元格的 radio 状态设置为选中状态 diff --git a/packages/vtable/__tests__/listTable-checkbox-record-index.test.ts b/packages/vtable/__tests__/listTable-checkbox-record-index.test.ts index 594cd6042d..b90bab3bff 100644 --- a/packages/vtable/__tests__/listTable-checkbox-record-index.test.ts +++ b/packages/vtable/__tests__/listTable-checkbox-record-index.test.ts @@ -69,4 +69,94 @@ describe('ListTable checkbox record index api', () => { expect(table.stateManager.checkedState.get('1').task).toBe(false); expect(table.stateManager.headerCheckedState.task).toBe(false); }); + + test('keeps header indeterminate when checkedState is sparse', () => { + table = new ListTable({ + container: createDiv(), + columns: [ + { + field: 'task', + title: 'Task', + cellType: 'checkbox', + headerType: 'checkbox' + } + ], + records: [{ task: { text: 'checked', checked: true } }, { task: { text: 'unchecked' } }], + enableCheckboxCascade: false + }); + table.stateManager.checkedState.delete('1'); + + table.setCellCheckboxStateByRecordIndex(0, 'task', true); + + expect(table.stateManager.headerCheckedState.task).toBe('indeterminate'); + }); + + test('does not create false state for records that are already unchecked when clearing', () => { + table = new ListTable({ + container: createDiv(), + columns: [ + { + field: 'task', + title: 'Task', + tree: true, + cellType: 'checkbox', + headerType: 'checkbox' + } + ], + records: [ + { + task: { text: 'parent', checked: true }, + hierarchyState: TYPES.HierarchyState.collapse, + children: [{ task: { text: 'child', checked: true } }] + }, + { task: { text: 'already unchecked', checked: false } } + ], + enableCheckboxCascade: false + }); + table.stateManager.checkedState.delete('1'); + + table.clearAllCheckboxState('task'); + + expect(table.stateManager.checkedState.get('0').task).toBe(false); + expect(table.stateManager.checkedState.get('0,0').task).toBe(false); + expect(table.stateManager.checkedState.has('1')).toBe(false); + }); + + test('updates children and parent state when cascade is enabled', () => { + table = new ListTable({ + container: createDiv(), + columns: [ + { + field: 'task', + title: 'Task', + tree: true, + cellType: 'checkbox', + headerType: 'checkbox' + } + ], + records: [ + { + task: { text: 'parent', checked: true }, + hierarchyState: TYPES.HierarchyState.collapse, + children: [ + { task: { text: 'child 1', checked: true } }, + { task: { text: 'child 2', checked: true } } + ] + } + ], + enableCheckboxCascade: true + }); + + table.setCellCheckboxStateByRecordIndex([0, 0], 'task', false); + + expect(table.stateManager.checkedState.get('0').task).toBe('indeterminate'); + expect(table.stateManager.checkedState.get('0,0').task).toBe(false); + expect(table.stateManager.checkedState.get('0,1').task).toBe(true); + + table.setCellCheckboxStateByRecordIndex(0, 'task', false); + + expect(table.stateManager.checkedState.get('0').task).toBe(false); + expect(table.stateManager.checkedState.get('0,0').task).toBe(false); + expect(table.stateManager.checkedState.get('0,1').task).toBe(false); + }); }); diff --git a/packages/vtable/src/state/checkbox/checkbox.ts b/packages/vtable/src/state/checkbox/checkbox.ts index 93b5301a82..44ec152f39 100644 --- a/packages/vtable/src/state/checkbox/checkbox.ts +++ b/packages/vtable/src/state/checkbox/checkbox.ts @@ -310,13 +310,11 @@ export function setCheckboxStateByRecordIndex( checked: boolean | 'indeterminate', table: BaseTableAPI ) { - const dataIndex = normalizeRecordIndex(recordIndex).toString(); - if (table.stateManager.checkedState.get(dataIndex)) { - table.stateManager.checkedState.get(dataIndex)[field as string | number] = checked; - } else { - table.stateManager.checkedState.set(dataIndex, { - [field as string | number]: checked - }); + setRecordCheckboxState(recordIndex, field, checked, table.stateManager.checkedState); + + if (table.internalProps.enableCheckboxCascade && checked !== 'indeterminate') { + updateChildrenCheckboxStateByRecordIndex(recordIndex, field, checked, table); + updateParentCheckboxStateByRecordIndex(recordIndex, field, table); } updateVisibleCheckboxCellByRecordIndex(recordIndex, field, checked, table); @@ -327,7 +325,7 @@ export function clearCheckboxState(field: FieldDef, table: BaseTableAPI) { if (!isValid(field)) { return; } - updateAllRecordCheckboxState((table as any).records, field, false, table.stateManager.checkedState); + clearRecordCheckboxState((table as any).records, field, table); updateVisibleCheckboxCellsByField(field, false, table); updateHeaderCheckboxStateByField(field, table); } @@ -439,30 +437,36 @@ function normalizeRecordIndex(recordIndex: number | number[]): number | number[] return recordIndex; } -function updateAllRecordCheckboxState( - records: any[], +function setRecordCheckboxState( + recordIndex: number | number[], field: FieldDef, checked: boolean | 'indeterminate', - checkedState: Map, - parentIndex: number[] = [] + checkedState: Map ) { + const dataIndex = normalizeRecordIndex(recordIndex).toString(); + if (checkedState.get(dataIndex)) { + checkedState.get(dataIndex)[field as string | number] = checked; + } else { + checkedState.set(dataIndex, { + [field as string | number]: checked + }); + } +} + +function clearRecordCheckboxState(records: any[], field: FieldDef, table: BaseTableAPI, parentIndex: number[] = []) { if (!isArray(records)) { return; } records.forEach((record, index) => { const recordIndex = parentIndex.length ? parentIndex.concat(index) : [index]; - const dataIndex = normalizeRecordIndex(recordIndex).toString(); - if (checkedState.get(dataIndex)) { - checkedState.get(dataIndex)[field as string | number] = checked; - } else { - checkedState.set(dataIndex, { - [field as string | number]: checked - }); + const currentState = getRecordCheckboxState(recordIndex, record, field, table); + if (currentState !== false) { + setRecordCheckboxState(recordIndex, field, false, table.stateManager.checkedState); } if (isArray(record?.children)) { - updateAllRecordCheckboxState(record.children, field, checked, checkedState, recordIndex); + clearRecordCheckboxState(record.children, field, table, recordIndex); } }); } @@ -486,8 +490,10 @@ function updateVisibleCheckboxCellByRecordIndex( } function updateVisibleCheckboxCellsByField(field: FieldDef, checked: boolean | 'indeterminate', table: BaseTableAPI) { + const bodyStartRow = table.columnHeaderLevelCount; + const cols = getCheckboxColsByField(field, bodyStartRow, table); for (let row = table.columnHeaderLevelCount; row < table.rowCount; row++) { - getCheckboxColsByField(field, row, table).forEach(col => { + cols.forEach(col => { setCellCheckboxStateByAttribute(col, row, checked, table); }); } @@ -497,16 +503,22 @@ function updateHeaderCheckboxStateByField(field: FieldDef, table: BaseTableAPI) const fieldKey = field as string | number; let hasChecked = false; let hasUnchecked = false; + let hasRecord = false; - table.stateManager.checkedState.forEach(checkState => { - if (checkState?.[fieldKey] === true) { + traverseRecords((table as any).records, (record, recordIndex) => { + const state = getRecordCheckboxState(recordIndex, record, field, table); + hasRecord = true; + if (state === true) { + hasChecked = true; + } else if (state === 'indeterminate') { hasChecked = true; + hasUnchecked = true; } else { hasUnchecked = true; } }); - const checked = hasChecked && !hasUnchecked ? true : hasChecked ? 'indeterminate' : false; + const checked = hasRecord && hasChecked && !hasUnchecked ? true : hasChecked ? 'indeterminate' : false; table.stateManager.headerCheckedState[fieldKey] = checked; updateVisibleHeaderCheckboxCellByField(field, checked, table); } @@ -539,6 +551,97 @@ function getCheckboxColsByField(field: FieldDef, row: number, table: BaseTableAP return cols; } +function updateChildrenCheckboxStateByRecordIndex( + recordIndex: number | number[], + field: FieldDef, + checked: boolean, + table: BaseTableAPI +) { + const record = getRecordByRecordIndex((table as any).records, recordIndex); + if (!record?.children?.length) { + return; + } + + record.children.forEach((child: any, childIndex: number) => { + const childRecordIndex = normalizeRecordIndexToArray(recordIndex).concat(childIndex); + setRecordCheckboxState(childRecordIndex, field, checked, table.stateManager.checkedState); + updateVisibleCheckboxCellByRecordIndex(childRecordIndex, field, checked, table); + updateChildrenCheckboxStateByRecordIndex(childRecordIndex, field, checked, table); + }); +} + +function updateParentCheckboxStateByRecordIndex(recordIndex: number | number[], field: FieldDef, table: BaseTableAPI) { + const recordIndexPath = normalizeRecordIndexToArray(recordIndex); + for (let level = recordIndexPath.length - 1; level > 0; level--) { + const parentIndex = recordIndexPath.slice(0, level); + const parentRecord = getRecordByRecordIndex((table as any).records, parentIndex); + if (!parentRecord?.children?.length) { + continue; + } + + const childStates = parentRecord.children.map((child: any, childIndex: number) => + getRecordCheckboxState(parentIndex.concat(childIndex), child, field, table) + ); + const allChecked = childStates.every(state => state === true); + const allUnchecked = childStates.every(state => state !== true && state !== 'indeterminate'); + const parentState = allChecked ? true : allUnchecked ? false : 'indeterminate'; + setRecordCheckboxState(parentIndex, field, parentState, table.stateManager.checkedState); + updateVisibleCheckboxCellByRecordIndex(parentIndex, field, parentState, table); + } +} + +function getRecordCheckboxState( + recordIndex: number | number[], + record: any, + field: FieldDef, + table: BaseTableAPI +): boolean | 'indeterminate' { + const fieldKey = field as string | number; + const dataIndex = normalizeRecordIndex(recordIndex).toString(); + const cachedState = table.stateManager.checkedState.get(dataIndex)?.[fieldKey]; + if (isValid(cachedState)) { + return cachedState; + } + + const value = record?.[fieldKey]; + if (isObject(value) && isValid(value.checked)) { + return value.checked; + } + if (typeof value === 'boolean') { + return value; + } + return false; +} + +function getRecordByRecordIndex(records: any[], recordIndex: number | number[]): any { + const recordIndexPath = normalizeRecordIndexToArray(recordIndex); + let currentRecords = records; + let record; + for (let i = 0; i < recordIndexPath.length; i++) { + record = currentRecords?.[recordIndexPath[i]]; + currentRecords = record?.children; + } + return record; +} + +function normalizeRecordIndexToArray(recordIndex: number | number[]): number[] { + return isArray(recordIndex) ? recordIndex : [recordIndex]; +} + +function traverseRecords(records: any[], handler: (record: any, recordIndex: number | number[]) => void, parentIndex: number[] = []) { + if (!isArray(records)) { + return; + } + + records.forEach((record, index) => { + const recordIndex = parentIndex.length ? parentIndex.concat(index) : index; + handler(record, recordIndex); + if (isArray(record?.children)) { + traverseRecords(record.children, handler, isArray(recordIndex) ? recordIndex : [recordIndex]); + } + }); +} + function initRecordCheckState(records: any[], state: StateManager) { const table = state.table; const start = table.isPivotTable() From 52cf22ce352aa5c5d23da639b6fcf2013eee51b1 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Tue, 28 Jul 2026 19:01:05 +0800 Subject: [PATCH 34/77] fix: pass latest options when adding plugins --- .../context-menu/handle-menu-helper.test.ts | 22 +++++++++++++++++++ packages/vtable/src/ListTable.ts | 2 +- packages/vtable/src/plugins/plugin-manager.ts | 4 ++-- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/packages/vtable-plugins/__tests__/context-menu/handle-menu-helper.test.ts b/packages/vtable-plugins/__tests__/context-menu/handle-menu-helper.test.ts index 64a16340d3..690e247de1 100644 --- a/packages/vtable-plugins/__tests__/context-menu/handle-menu-helper.test.ts +++ b/packages/vtable-plugins/__tests__/context-menu/handle-menu-helper.test.ts @@ -133,4 +133,26 @@ describe('Context menu canvas option', () => { table ); }); + + test('ContextMenuPlugin init uses new options when added through updateOption', () => { + const container = createDiv(); + const contextMenuPlugin = new ContextMenuPlugin({ + contextMenuWorkOnlyCell: false + }); + + table = new ListTable({ + container, + columns: [{ field: 'id', title: 'ID' }], + records: [{ id: 1 }] + }); + + table.updateOption({ + container, + columns: [{ field: 'id', title: 'ID' }], + records: [{ id: 1 }], + plugins: [contextMenuPlugin] + }); + + expect(table.options.menu.contextMenuWorkOnlyCell).toBe(false); + }); }); diff --git a/packages/vtable/src/ListTable.ts b/packages/vtable/src/ListTable.ts index 8366170c65..42083858d9 100644 --- a/packages/vtable/src/ListTable.ts +++ b/packages/vtable/src/ListTable.ts @@ -778,7 +778,7 @@ export class ListTable extends BaseTable implements ListTableAPI { ) { const internalProps = this.internalProps; - this.pluginManager.removeOrAddPlugins(options.plugins); + this.pluginManager.removeOrAddPlugins(options.plugins, options); super.updateOption(options, updateConfig); internalProps.frozenColDragHeaderMode = options.dragOrder?.frozenColDragHeaderMode ?? options.frozenColDragHeaderMode; diff --git a/packages/vtable/src/plugins/plugin-manager.ts b/packages/vtable/src/plugins/plugin-manager.ts index 7f4f7f1773..1b3c53ef5d 100644 --- a/packages/vtable/src/plugins/plugin-manager.ts +++ b/packages/vtable/src/plugins/plugin-manager.ts @@ -45,7 +45,7 @@ export class PluginManager { } // 移除或添加插件 - removeOrAddPlugins(plugins?: IVTablePlugin[]): void { + removeOrAddPlugins(plugins?: IVTablePlugin[], options: BaseTableConstructorOptions = this.table.options): void { // 先找到plugins中没有,但this.plugins中有,也就是已经被移除的插件 const removedPlugins = Array.from(this.plugins.values()).filter(plugin => !plugins?.some(p => p.id === plugin.id)); removedPlugins.forEach(plugin => { @@ -60,7 +60,7 @@ export class PluginManager { const addedPlugins = plugins?.filter(plugin => !this.plugins.has(plugin.id)); addedPlugins?.forEach(plugin => { this.register(plugin); - plugin.init?.(this.table, this.table.options); + plugin.init?.(this.table, options); this._bindTableEventForPlugin(plugin); }); } From 477db10430d86f4aff9e15c8348b1750b0e27d30 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Tue, 28 Jul 2026 19:24:28 +0800 Subject: [PATCH 35/77] fix: refine context menu plugin demo and cleanup --- .../context-menu/handle-menu-helper.test.ts | 35 ++++++++++++++++++ .../issue-5215-context-menu-canvas.ts | 37 +++++++++++++++++-- packages/vtable/src/plugins/plugin-manager.ts | 3 +- 3 files changed, 71 insertions(+), 4 deletions(-) diff --git a/packages/vtable-plugins/__tests__/context-menu/handle-menu-helper.test.ts b/packages/vtable-plugins/__tests__/context-menu/handle-menu-helper.test.ts index 690e247de1..b9579f9562 100644 --- a/packages/vtable-plugins/__tests__/context-menu/handle-menu-helper.test.ts +++ b/packages/vtable-plugins/__tests__/context-menu/handle-menu-helper.test.ts @@ -155,4 +155,39 @@ describe('Context menu canvas option', () => { expect(table.options.menu.contextMenuWorkOnlyCell).toBe(false); }); + + test('PluginManager only releases plugins removed through updateOption', () => { + const container = createDiv(); + const removedPlugin = { + id: 'removed-plugin', + name: 'removed-plugin', + runTime: [], + run: jest.fn(), + release: jest.fn() + }; + const keptPlugin = { + id: 'kept-plugin', + name: 'kept-plugin', + runTime: [], + run: jest.fn(), + release: jest.fn() + }; + + table = new ListTable({ + container, + columns: [{ field: 'id', title: 'ID' }], + records: [{ id: 1 }], + plugins: [removedPlugin, keptPlugin] + }); + + table.updateOption({ + container, + columns: [{ field: 'id', title: 'ID' }], + records: [{ id: 1 }], + plugins: [keptPlugin] + }); + + expect(removedPlugin.release).toHaveBeenCalledTimes(1); + expect(keptPlugin.release).not.toHaveBeenCalled(); + }); }); diff --git a/packages/vtable-plugins/demo/context-menu/issue-5215-context-menu-canvas.ts b/packages/vtable-plugins/demo/context-menu/issue-5215-context-menu-canvas.ts index 30dbe7d01a..30e960df6a 100644 --- a/packages/vtable-plugins/demo/context-menu/issue-5215-context-menu-canvas.ts +++ b/packages/vtable-plugins/demo/context-menu/issue-5215-context-menu-canvas.ts @@ -12,7 +12,9 @@ export function createTableInstance() { const plugin = new ContextMenuPlugin({ contextMenuWorkOnlyCell: false, bodyCellMenuItems: [ - { text: 'Copy from plugin', menuKey: 'copy' } + { text: 'Copy from plugin', menuKey: 'copy' }, + '---', + { text: 'Body cell item', menuKey: 'body_cell_item' } ], canvasMenuItems: [ { text: 'Canvas blank area', menuKey: 'canvas_blank_area' }, @@ -20,9 +22,15 @@ export function createTableInstance() { ], beforeShowAdjustMenuItems: (menuItems, _table, col, row) => { if (col === -1 && row === -1) { - return menuItems; + return [ + ...menuItems, + { + text: `Blank canvas: col ${col}, row ${row}`, + menuKey: 'canvas_position' + } + ]; } - return menuItems; + return menuItems.filter(item => item !== '---'); } }); @@ -45,3 +53,26 @@ export function createTableInstance() { window.tableInstance = tableInstance; return tableInstance; } + +export function createTable() { + const container = document.createElement('div'); + container.id = CONTAINER_ID; + container.style.width = '100%'; + container.style.height = '420px'; + document.body.appendChild(container); + + const info = document.createElement('div'); + info.style.margin = '10px'; + info.style.padding = '10px'; + info.style.border = '1px solid #ddd'; + info.style.borderRadius = '4px'; + info.style.backgroundColor = '#f9f9f9'; + info.innerHTML = ` +

Issue 5215: Canvas context menu

+

Right-click the blank canvas area outside the table cells. The context menu should show canvas-specific items.

+

Right-click body cells to verify cell menu items are still independent from blank canvas menu items.

+ `; + + document.body.insertBefore(info, container); + return createTableInstance(); +} diff --git a/packages/vtable/src/plugins/plugin-manager.ts b/packages/vtable/src/plugins/plugin-manager.ts index 1b3c53ef5d..e6d197ab85 100644 --- a/packages/vtable/src/plugins/plugin-manager.ts +++ b/packages/vtable/src/plugins/plugin-manager.ts @@ -52,7 +52,8 @@ export class PluginManager { this.pluginEventMap.get(plugin.id)?.forEach(id => { this.table.off(id); }); - this.release(); + this.pluginEventMap.delete(plugin.id); + plugin.release?.(this.table); this.plugins.delete(plugin.id); }); From 28e51f08cacb9a14f461e3836457d8f09a479955 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Tue, 28 Jul 2026 19:51:06 +0800 Subject: [PATCH 36/77] fix: render checkbox record index updates --- .../__tests__/listTable-checkbox-record-index.test.ts | 4 ++++ packages/vtable/src/state/checkbox/checkbox.ts | 11 +++++++++++ 2 files changed, 15 insertions(+) diff --git a/packages/vtable/__tests__/listTable-checkbox-record-index.test.ts b/packages/vtable/__tests__/listTable-checkbox-record-index.test.ts index b90bab3bff..5ecd791266 100644 --- a/packages/vtable/__tests__/listTable-checkbox-record-index.test.ts +++ b/packages/vtable/__tests__/listTable-checkbox-record-index.test.ts @@ -33,10 +33,12 @@ describe('ListTable checkbox record index api', () => { ], enableCheckboxCascade: false }); + const renderAsync = jest.spyOn(table, 'renderAsync').mockImplementation(() => Promise.resolve()); table.setCellCheckboxStateByRecordIndex([0, 0], 'task', false); expect(table.stateManager.checkedState.get('0,0').task).toBe(false); + expect(renderAsync).toHaveBeenCalledTimes(1); }); test('clears all checkbox states for a field', () => { @@ -61,6 +63,7 @@ describe('ListTable checkbox record index api', () => { ], enableCheckboxCascade: false }); + const renderAsync = jest.spyOn(table, 'renderAsync').mockImplementation(() => Promise.resolve()); table.clearAllCheckboxState('task'); @@ -68,6 +71,7 @@ describe('ListTable checkbox record index api', () => { expect(table.stateManager.checkedState.get('0,0').task).toBe(false); expect(table.stateManager.checkedState.get('1').task).toBe(false); expect(table.stateManager.headerCheckedState.task).toBe(false); + expect(renderAsync).toHaveBeenCalledTimes(1); }); test('keeps header indeterminate when checkedState is sparse', () => { diff --git a/packages/vtable/src/state/checkbox/checkbox.ts b/packages/vtable/src/state/checkbox/checkbox.ts index 44ec152f39..9b7d676f07 100644 --- a/packages/vtable/src/state/checkbox/checkbox.ts +++ b/packages/vtable/src/state/checkbox/checkbox.ts @@ -319,6 +319,7 @@ export function setCheckboxStateByRecordIndex( updateVisibleCheckboxCellByRecordIndex(recordIndex, field, checked, table); updateHeaderCheckboxStateByField(field, table); + renderCheckboxStateUpdate(table); } export function clearCheckboxState(field: FieldDef, table: BaseTableAPI) { @@ -328,6 +329,7 @@ export function clearCheckboxState(field: FieldDef, table: BaseTableAPI) { clearRecordCheckboxState((table as any).records, field, table); updateVisibleCheckboxCellsByField(field, false, table); updateHeaderCheckboxStateByField(field, table); + renderCheckboxStateUpdate(table); } export function setCellCheckboxStateByAttribute( @@ -499,6 +501,15 @@ function updateVisibleCheckboxCellsByField(field: FieldDef, checked: boolean | ' } } +function renderCheckboxStateUpdate(table: BaseTableAPI) { + const renderAsync = (table as any).renderAsync; + if (isFunction(renderAsync)) { + renderAsync.call(table); + } else { + table.render(); + } +} + function updateHeaderCheckboxStateByField(field: FieldDef, table: BaseTableAPI) { const fieldKey = field as string | number; let hasChecked = false; From 28d3484f073a52c83b102cdf18f76ab33246ac73 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Tue, 28 Jul 2026 21:06:08 +0800 Subject: [PATCH 37/77] fix(vtable): expose auto row real height --- .../issue-5117-auto-height-real-height.ts | 70 +++++++++++++++++++ packages/vtable/examples/menu.ts | 4 ++ .../scenegraph/layout/compute-row-height.ts | 28 ++++++-- packages/vtable/src/ts-types/base-table.ts | 6 +- 4 files changed, 102 insertions(+), 6 deletions(-) create mode 100644 packages/vtable/examples/debug/issue-5117-auto-height-real-height.ts diff --git a/packages/vtable/examples/debug/issue-5117-auto-height-real-height.ts b/packages/vtable/examples/debug/issue-5117-auto-height-real-height.ts new file mode 100644 index 0000000000..77e3099a91 --- /dev/null +++ b/packages/vtable/examples/debug/issue-5117-auto-height-real-height.ts @@ -0,0 +1,70 @@ +import * as VTable from '../../src'; + +const CONTAINER_ID = 'vTable'; + +const records = Array.from({ length: 60 }, (_, index) => ({ + id: index + 1, + name: `row-${index + 1}`, + desc: + index % 3 === 0 + ? 'long text long text long text long text long text long text long text long text long text' + : 'short' +})); + +export function createTable() { + const container = document.getElementById(CONTAINER_ID)!; + document.getElementById('issue5117Toolbar')?.remove(); + container.style.width = '760px'; + container.style.height = '420px'; + + const toolbar = document.createElement('div'); + toolbar.id = 'issue5117Toolbar'; + toolbar.style.cssText = 'height: 48px; font-size: 12px; display: flex; gap: 12px; align-items: center;'; + toolbar.innerHTML = ` + + + `; + container.before(toolbar); + + const realHeights: Array = []; + const tableInstance = new VTable.ListTable({ + container, + records, + columns: [ + { field: 'id', title: 'ID', width: 80 }, + { field: 'name', title: 'Name', width: 120 }, + { + field: 'desc', + title: 'Description', + width: 280, + style: { + autoWrapText: true + } + } + ], + widthMode: 'standard', + heightMode: 'autoHeight', + defaultRowHeight: 64, + customComputeRowHeight: ({ row, realHeight }) => { + realHeights[row] = realHeight; + return 'auto'; + } + }); + + const check = () => { + const bodyRow = tableInstance.columnHeaderLevelCount + 1; + const realHeight = realHeights[bodyRow]; + const rowHeight = tableInstance.getRowHeight(bodyRow); + const pass = typeof realHeight === 'number' && realHeight >= 64 && rowHeight >= 64; + const state = document.getElementById('issue5117State')!; + state.textContent = `${pass ? 'PASS' : 'FAIL'} | realHeight=${realHeight} rowHeight=${rowHeight}`; + return { pass, realHeight, rowHeight }; + }; + + document.getElementById('issue5117Check')!.addEventListener('click', check); + + window.tableInstance = tableInstance; + (window as any).issue5117Check = check; + + setTimeout(check, 0); +} diff --git a/packages/vtable/examples/menu.ts b/packages/vtable/examples/menu.ts index 13b2c16107..0287f9b0ec 100644 --- a/packages/vtable/examples/menu.ts +++ b/packages/vtable/examples/menu.ts @@ -34,6 +34,10 @@ export const menus = [ path: 'debug', name: 'issue-5114' }, + { + path: 'debug', + name: 'issue-5117-auto-height-real-height' + }, { path: 'debug', name: 'issue-5146' diff --git a/packages/vtable/src/scenegraph/layout/compute-row-height.ts b/packages/vtable/src/scenegraph/layout/compute-row-height.ts index 067ce3f2fb..7d51230ad3 100644 --- a/packages/vtable/src/scenegraph/layout/compute-row-height.ts +++ b/packages/vtable/src/scenegraph/layout/compute-row-height.ts @@ -351,7 +351,22 @@ export function computeRowsHeight( } } +const limitMinRowHeight = (height: number, row: number, table: BaseTableAPI): number => { + const defaultHeight = table.getDefaultRowHeight(row); + return isNumber(defaultHeight) ? Math.max(height, defaultHeight) : height; +}; + export function computeRowHeight(row: number, startCol: number, endCol: number, table: BaseTableAPI): number { + return computeRowHeightInternal(row, startCol, endCol, table, true); +} + +function computeRowHeightInternal( + row: number, + startCol: number, + endCol: number, + table: BaseTableAPI, + enableCustomCompute: boolean +): number { const isAllRowsAuto = table.isAutoRowHeight(row) || (table.heightMode === 'adaptive' && table.options.autoHeightInAdaptiveMode !== false); if (!isAllRowsAuto && table.getDefaultRowHeight(row) !== 'auto') { @@ -359,16 +374,19 @@ export function computeRowHeight(row: number, startCol: number, endCol: number, } let maxHeight; - if (table.options.customComputeRowHeight) { + if (enableCustomCompute && table.options.customComputeRowHeight) { + const realHeight = computeRowHeightInternal(row, startCol, endCol, table, false); const customRowHeight = table.options.customComputeRowHeight({ row, - table + table, + realHeight }); if (typeof customRowHeight === 'number') { return customRowHeight; - } else if (customRowHeight !== 'auto') { - return table.getDefaultRowHeight(row) as number; + } else if (customRowHeight === 'auto' || customRowHeight === undefined) { + return realHeight; } + return table.getDefaultRowHeight(row) as number; } if (table.internalProps.rowHeightConfig) { const rowHeightConfig = table.internalProps.rowHeightConfig.find((item: { key: number }) => item.key === row); @@ -462,7 +480,7 @@ export function computeRowHeight(row: number, startCol: number, endCol: number, maxHeight = isValid(maxHeight) ? Math.max(textHeight, maxHeight) : textHeight; } if (isValid(maxHeight)) { - return maxHeight; + return limitMinRowHeight(maxHeight, row, table); } const defaultHeight = table.getDefaultRowHeight(row); diff --git a/packages/vtable/src/ts-types/base-table.ts b/packages/vtable/src/ts-types/base-table.ts index 5433438e5c..f898856f68 100644 --- a/packages/vtable/src/ts-types/base-table.ts +++ b/packages/vtable/src/ts-types/base-table.ts @@ -694,7 +694,11 @@ export interface BaseTableConstructorOptions { renderOption?: any; formatCopyValue?: (value: string) => string; - customComputeRowHeight?: (computeArgs: { row: number; table: BaseTableAPI }) => number | 'auto' | undefined; + customComputeRowHeight?: (computeArgs: { + row: number; + table: BaseTableAPI; + realHeight?: number; + }) => number | 'auto' | undefined; /** 当表格出现抖动情况,请排查是否上层dom容器的宽高是小数引起的。如果不能保证是整数,请配置这个配置项为true */ tableSizeAntiJitter?: boolean; From 5b8056f24ac608a9092db7f8b03e2fb2e99101c8 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Tue, 28 Jul 2026 21:07:40 +0800 Subject: [PATCH 38/77] fix(vtable): fill auto height zero rows --- .../debug/issue-5115-auto-height-zero-row.ts | 65 +++++++++++++++++++ packages/vtable/examples/menu.ts | 4 ++ .../progress/create-group-for-first-screen.ts | 16 +++++ 3 files changed, 85 insertions(+) create mode 100644 packages/vtable/examples/debug/issue-5115-auto-height-zero-row.ts diff --git a/packages/vtable/examples/debug/issue-5115-auto-height-zero-row.ts b/packages/vtable/examples/debug/issue-5115-auto-height-zero-row.ts new file mode 100644 index 0000000000..3f69b13d0f --- /dev/null +++ b/packages/vtable/examples/debug/issue-5115-auto-height-zero-row.ts @@ -0,0 +1,65 @@ +import * as VTable from '../../src'; + +const CONTAINER_ID = 'vTable'; + +const records = Array.from({ length: 120 }, (_, index) => ({ + id: index + 1, + status: index % 2 === 0 ? 'hidden-height-0' : 'visible', + text: `row ${index + 1} visible content` +})); + +export function createTable() { + const container = document.getElementById(CONTAINER_ID)!; + document.getElementById('issue5115Toolbar')?.remove(); + container.style.width = '720px'; + container.style.height = '420px'; + + const toolbar = document.createElement('div'); + toolbar.id = 'issue5115Toolbar'; + toolbar.style.cssText = 'height: 48px; font-size: 12px; display: flex; gap: 12px; align-items: center;'; + toolbar.innerHTML = ` + + + `; + container.before(toolbar); + + const tableInstance = new VTable.ListTable({ + container, + records, + columns: [ + { field: 'id', title: 'ID', width: 80 }, + { field: 'status', title: 'Status', width: 160 }, + { field: 'text', title: 'Text', width: 260 } + ], + widthMode: 'standard', + heightMode: 'autoHeight', + defaultRowHeight: 40, + customComputeRowHeight: ({ row }) => { + if (row > 0 && (row - 1) % 2 === 0) { + return 0; + } + return 'auto'; + } + }); + + const check = () => { + const proxy = tableInstance.scenegraph.proxy; + const bodyStart = tableInstance.frozenRowCount; + const bodyHeight = tableInstance.tableNoFrameHeight - tableInstance.getFrozenRowsHeight(); + const renderedHeight = tableInstance.getRowsHeight(bodyStart, proxy.rowEnd); + const zeroHeight = tableInstance.getRowHeight(tableInstance.columnHeaderLevelCount) === 0; + const pass = zeroHeight && renderedHeight >= bodyHeight; + const state = document.getElementById('issue5115State')!; + state.textContent = + `${pass ? 'PASS' : 'FAIL'} | zeroHeight=${zeroHeight} renderedHeight=${renderedHeight} ` + + `bodyHeight=${bodyHeight} rowEnd=${proxy.rowEnd}`; + return { pass, zeroHeight, renderedHeight, bodyHeight, rowEnd: proxy.rowEnd }; + }; + + document.getElementById('issue5115Check')!.addEventListener('click', check); + + window.tableInstance = tableInstance; + (window as any).issue5115Check = check; + + setTimeout(check, 0); +} diff --git a/packages/vtable/examples/menu.ts b/packages/vtable/examples/menu.ts index 13b2c16107..f435bad181 100644 --- a/packages/vtable/examples/menu.ts +++ b/packages/vtable/examples/menu.ts @@ -34,6 +34,10 @@ export const menus = [ path: 'debug', name: 'issue-5114' }, + { + path: 'debug', + name: 'issue-5115-auto-height-zero-row' + }, { path: 'debug', name: 'issue-5146' diff --git a/packages/vtable/src/scenegraph/group-creater/progress/create-group-for-first-screen.ts b/packages/vtable/src/scenegraph/group-creater/progress/create-group-for-first-screen.ts index 4fcf6d1baa..8927530a19 100644 --- a/packages/vtable/src/scenegraph/group-creater/progress/create-group-for-first-screen.ts +++ b/packages/vtable/src/scenegraph/group-creater/progress/create-group-for-first-screen.ts @@ -6,6 +6,21 @@ import { computeRowsHeight } from '../../layout/compute-row-height'; import { createColGroup } from '../column'; import type { SceneProxy } from './proxy'; +function fillVisibleBodyRows(proxy: SceneProxy, distRow: number): number { + const { table } = proxy; + const bodyBottomRow = table.rowCount - 1 - table.bottomFrozenRowCount; + const visibleBodyHeight = table.tableNoFrameHeight - table.getFrozenRowsHeight() - table.getBottomFrozenRowsHeight(); + let targetRow = distRow; + + while (targetRow < bodyBottomRow && table.getRowsHeight(table.frozenRowCount, targetRow) < visibleBodyHeight) { + const nextRow = targetRow + 1; + computeRowsHeight(table, targetRow + 1, nextRow); + targetRow = nextRow; + } + + return targetRow; +} + export function createGroupForFirstScreen( cornerHeaderGroup: Group, colHeaderGroup: Group, @@ -64,6 +79,7 @@ export function createGroupForFirstScreen( ? table.rowCount - 1 : distRowForCompute ?? distRow ); //如果配置了 canvasHeight为 'auto', 则一次性将所有行高都计算出来才能满足后续赋值表格高度的使用 + distRow = fillVisibleBodyRows(proxy, distRow); } if (distCol < table.colCount - table.rightFrozenColCount) { From 0fbd0d47dfbd6aacea82fcbd5bc85e599c535ed8 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Tue, 28 Jul 2026 21:08:00 +0800 Subject: [PATCH 39/77] fix(vue-vtable): keep custom layout off scrollbars --- packages/vue-vtable/demo/src/App.vue | 4 +- .../Issue5157CustomLayoutScrollbar.vue | 121 ++++++++++++++++++ .../custom/vtable-vue-attribute-plugin.ts | 41 +++++- 3 files changed, 159 insertions(+), 7 deletions(-) create mode 100644 packages/vue-vtable/demo/src/table/gramatical/composition/Issue5157CustomLayoutScrollbar.vue diff --git a/packages/vue-vtable/demo/src/App.vue b/packages/vue-vtable/demo/src/App.vue index e49cb43abc..da28d5432f 100644 --- a/packages/vue-vtable/demo/src/App.vue +++ b/packages/vue-vtable/demo/src/App.vue @@ -20,6 +20,7 @@ import ListTableDes from './table/gramatical/composition/ListTable-destruction.v import ListTableCustom from './table/gramatical/composition/ListTable-custom.vue'; import ListTableCustomHover from './table/gramatical/composition/ListTable-custom-hover.vue'; import Issue5150CustomLayoutSort from './table/gramatical/composition/Issue5150CustomLayoutSort.vue'; +import Issue5157CustomLayoutScrollbar from './table/gramatical/composition/Issue5157CustomLayoutScrollbar.vue'; import ListTableVFor from './table/gramatical/options/ListTable-v-for.vue'; import PivotTable from './table/gramatical/options/PivotTable.vue'; @@ -51,8 +52,9 @@ import singleRadio from './table/single/single-radio.vue'; - + + diff --git a/packages/vue-vtable/demo/src/table/gramatical/composition/Issue5157CustomLayoutScrollbar.vue b/packages/vue-vtable/demo/src/table/gramatical/composition/Issue5157CustomLayoutScrollbar.vue new file mode 100644 index 0000000000..baaa7ba7e8 --- /dev/null +++ b/packages/vue-vtable/demo/src/table/gramatical/composition/Issue5157CustomLayoutScrollbar.vue @@ -0,0 +1,121 @@ + + + + + diff --git a/packages/vue-vtable/src/components/custom/vtable-vue-attribute-plugin.ts b/packages/vue-vtable/src/components/custom/vtable-vue-attribute-plugin.ts index 579dd52474..f48c54d509 100644 --- a/packages/vue-vtable/src/components/custom/vtable-vue-attribute-plugin.ts +++ b/packages/vue-vtable/src/components/custom/vtable-vue-attribute-plugin.ts @@ -489,6 +489,7 @@ export class VTableVueAttributePlugin extends HtmlAttributePlugin implements IPl const { vue: options, width, height, visible, display, ...rest } = attribute || {}; const { x: left, y: top } = this.calculatePosition(graphic, options.anchorType); const { left: offsetX, top: offsetTop } = this.calculateOffset(stage, nativeContainer, left, top); + const { safeWidth, safeHeight } = this.getScrollbarSafeSize(graphic, offsetX, offsetTop, width, height, options); const { id } = this.getGraphicOptions(graphic) || {}; const record = id ? this.htmlMap[id] : null; @@ -499,10 +500,6 @@ export class VTableVueAttributePlugin extends HtmlAttributePlugin implements IPl // 位置变化检查 const positionChanged = !record.lastPosition || record.lastPosition.x !== offsetX || record.lastPosition.y !== offsetTop; - if (!positionChanged) { - // 位置没有变化,无需更新样式 - return; - } // 默认自定义区域内也可带动表格画布滚动 const { pointerEvents } = options; @@ -510,8 +507,8 @@ export class VTableVueAttributePlugin extends HtmlAttributePlugin implements IPl // 单元格样式 const style = this.convertCellStyle(graphic); Object.assign(calculateStyle, { - width: `${width}px`, - height: `${height}px`, + width: `${safeWidth}px`, + height: `${safeHeight}px`, overflow: 'hidden', ...(style || {}), ...(rest || {}), @@ -546,6 +543,38 @@ export class VTableVueAttributePlugin extends HtmlAttributePlugin implements IPl record.lastStyle = calculateStyle; } + record.lastPosition = positionChanged ? { x: offsetX, y: offsetTop } : record.lastPosition; + } + + private getScrollbarSafeSize( + graphic: IGraphic, + offsetX: number, + offsetTop: number, + width: number, + height: number, + options: any + ) { + const pointerEvents = options?.pointerEvents === true ? 'all' : options?.pointerEvents || 'none'; + const table = getTargetGroup(graphic)?.stage?.table; + const scrollStyle = table?.theme?.scrollStyle; + const barToSide = scrollStyle?.barToSide ?? false; + let safeWidth = width; + let safeHeight = height; + + if (pointerEvents !== 'none' && table && !barToSide) { + const verticalVisible = scrollStyle?.verticalVisible ?? scrollStyle?.visible; + const horizontalVisible = scrollStyle?.horizontalVisible ?? scrollStyle?.visible; + const hasVerticalScrollBar = verticalVisible !== 'none' && table.getAllRowsHeight() > table.tableNoFrameHeight; + const hasHorizontalScrollBar = horizontalVisible !== 'none' && table.getAllColsWidth() > table.tableNoFrameWidth; + const scrollBarSize = scrollStyle?.width ?? 7; + const maxRight = table.tableNoFrameWidth - (hasVerticalScrollBar ? scrollBarSize : 0); + const maxBottom = table.tableNoFrameHeight - (hasHorizontalScrollBar ? scrollBarSize : 0); + + safeWidth = Math.max(0, Math.min(width, maxRight - offsetX)); + safeHeight = Math.max(0, Math.min(height, maxBottom - offsetTop)); + } + + return { safeWidth, safeHeight }; } /** From 5146a6d45e20ac32fce7b9cf7e762985801ddb90 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Wed, 29 Jul 2026 10:57:19 +0800 Subject: [PATCH 40/77] fix(vtable): preserve custom row height semantics --- .../scenegraph/layout/compute-row-height.ts | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/packages/vtable/src/scenegraph/layout/compute-row-height.ts b/packages/vtable/src/scenegraph/layout/compute-row-height.ts index 7d51230ad3..81feacf957 100644 --- a/packages/vtable/src/scenegraph/layout/compute-row-height.ts +++ b/packages/vtable/src/scenegraph/layout/compute-row-height.ts @@ -375,16 +375,29 @@ function computeRowHeightInternal( let maxHeight; if (enableCustomCompute && table.options.customComputeRowHeight) { - const realHeight = computeRowHeightInternal(row, startCol, endCol, table, false); - const customRowHeight = table.options.customComputeRowHeight({ + let realHeight: number; + let hasRealHeight = false; + const getRealHeight = () => { + if (!hasRealHeight) { + realHeight = computeRowHeightInternal(row, startCol, endCol, table, false); + hasRealHeight = true; + } + return realHeight; + }; + const computeArgs = { row, - table, - realHeight + table + } as Parameters>[0]; + Object.defineProperty(computeArgs, 'realHeight', { + get: getRealHeight, + enumerable: true, + configurable: true }); + const customRowHeight = table.options.customComputeRowHeight(computeArgs); if (typeof customRowHeight === 'number') { return customRowHeight; - } else if (customRowHeight === 'auto' || customRowHeight === undefined) { - return realHeight; + } else if (customRowHeight === 'auto') { + return getRealHeight(); } return table.getDefaultRowHeight(row) as number; } From 06cc55372be522928b2929afde28b407be2eb917 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Wed, 29 Jul 2026 10:57:37 +0800 Subject: [PATCH 41/77] fix(vtable): limit zero-row fill to auto height --- .../group-creater/progress/create-group-for-first-screen.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/vtable/src/scenegraph/group-creater/progress/create-group-for-first-screen.ts b/packages/vtable/src/scenegraph/group-creater/progress/create-group-for-first-screen.ts index 8927530a19..ea0af67515 100644 --- a/packages/vtable/src/scenegraph/group-creater/progress/create-group-for-first-screen.ts +++ b/packages/vtable/src/scenegraph/group-creater/progress/create-group-for-first-screen.ts @@ -79,7 +79,9 @@ export function createGroupForFirstScreen( ? table.rowCount - 1 : distRowForCompute ?? distRow ); //如果配置了 canvasHeight为 'auto', 则一次性将所有行高都计算出来才能满足后续赋值表格高度的使用 - distRow = fillVisibleBodyRows(proxy, distRow); + if (table.heightMode === 'autoHeight') { + distRow = fillVisibleBodyRows(proxy, distRow); + } } if (distCol < table.colCount - table.rightFrozenColCount) { From bc032ed9f356f35b9e111ff6142719109839747a Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Wed, 29 Jul 2026 10:57:53 +0800 Subject: [PATCH 42/77] fix(vue-vtable): clip custom layout by scrollbar bounds --- .../custom/vtable-vue-attribute-plugin.ts | 49 +++++++++++++++++-- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/packages/vue-vtable/src/components/custom/vtable-vue-attribute-plugin.ts b/packages/vue-vtable/src/components/custom/vtable-vue-attribute-plugin.ts index f48c54d509..d2d8e6eb09 100644 --- a/packages/vue-vtable/src/components/custom/vtable-vue-attribute-plugin.ts +++ b/packages/vue-vtable/src/components/custom/vtable-vue-attribute-plugin.ts @@ -567,11 +567,52 @@ export class VTableVueAttributePlugin extends HtmlAttributePlugin implements IPl const hasVerticalScrollBar = verticalVisible !== 'none' && table.getAllRowsHeight() > table.tableNoFrameHeight; const hasHorizontalScrollBar = horizontalVisible !== 'none' && table.getAllColsWidth() > table.tableNoFrameWidth; const scrollBarSize = scrollStyle?.width ?? 7; - const maxRight = table.tableNoFrameWidth - (hasVerticalScrollBar ? scrollBarSize : 0); - const maxBottom = table.tableNoFrameHeight - (hasHorizontalScrollBar ? scrollBarSize : 0); + const groupX = table.scenegraph?.tableGroup?.attribute?.x ?? 0; + const groupY = table.scenegraph?.tableGroup?.attribute?.y ?? 0; + const hoverOn = scrollStyle?.hoverOn; + const domRight = offsetX + width; + const domBottom = offsetTop + height; + + if (hasVerticalScrollBar) { + const verticalLeft = + Math.min(table.tableNoFrameWidth, table.getAllColsWidth()) - (hoverOn ? scrollBarSize : -groupX); + const verticalTop = table.getFrozenRowsHeight() + (!hoverOn ? groupY : 0); + const verticalBottom = + verticalTop + table.tableNoFrameHeight - table.getFrozenRowsHeight() - table.getBottomFrozenRowsHeight(); + + if ( + offsetTop < verticalBottom && + domBottom > verticalTop && + offsetX < verticalLeft + scrollBarSize && + domRight > verticalLeft + ) { + safeWidth = Math.max(0, Math.min(width, verticalLeft - offsetX)); + } + } - safeWidth = Math.max(0, Math.min(width, maxRight - offsetX)); - safeHeight = Math.max(0, Math.min(height, maxBottom - offsetTop)); + if (hasHorizontalScrollBar) { + const ignoreFrozenCols = scrollStyle?.ignoreFrozenCols ?? false; + const horizontalLeft = ignoreFrozenCols + ? !hoverOn + ? groupX + : 0 + : table.getFrozenColsWidth() + (!hoverOn ? groupX : 0); + const horizontalWidth = ignoreFrozenCols + ? table.tableNoFrameWidth + : table.tableNoFrameWidth - table.getFrozenColsWidth() - table.getRightFrozenColsWidth(); + const horizontalTop = + Math.min(table.tableNoFrameHeight, table.getAllRowsHeight()) - (hoverOn ? scrollBarSize : -groupY); + const horizontalRight = horizontalLeft + horizontalWidth; + + if ( + offsetX < horizontalRight && + domRight > horizontalLeft && + offsetTop < horizontalTop + scrollBarSize && + domBottom > horizontalTop + ) { + safeHeight = Math.max(0, Math.min(height, horizontalTop - offsetTop)); + } + } } return { safeWidth, safeHeight }; From 17c163cae36d270b6ba2f6f15c6b235caeb69194 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Wed, 29 Jul 2026 11:10:44 +0800 Subject: [PATCH 43/77] fix(vtable): keep row height cache during zero fill --- .../group-creater/progress/create-group-for-first-screen.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vtable/src/scenegraph/group-creater/progress/create-group-for-first-screen.ts b/packages/vtable/src/scenegraph/group-creater/progress/create-group-for-first-screen.ts index ea0af67515..f552cee718 100644 --- a/packages/vtable/src/scenegraph/group-creater/progress/create-group-for-first-screen.ts +++ b/packages/vtable/src/scenegraph/group-creater/progress/create-group-for-first-screen.ts @@ -14,7 +14,7 @@ function fillVisibleBodyRows(proxy: SceneProxy, distRow: number): number { while (targetRow < bodyBottomRow && table.getRowsHeight(table.frozenRowCount, targetRow) < visibleBodyHeight) { const nextRow = targetRow + 1; - computeRowsHeight(table, targetRow + 1, nextRow); + computeRowsHeight(table, nextRow, nextRow, false); targetRow = nextRow; } From 828cb661c2f4eca5867cc97ac90572f206c52325 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Wed, 29 Jul 2026 11:11:07 +0800 Subject: [PATCH 44/77] fix(vue-vtable): align scrollbar clip coordinates --- .../custom/vtable-vue-attribute-plugin.ts | 47 ++++++++++++++----- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/packages/vue-vtable/src/components/custom/vtable-vue-attribute-plugin.ts b/packages/vue-vtable/src/components/custom/vtable-vue-attribute-plugin.ts index d2d8e6eb09..6de5c7cd6a 100644 --- a/packages/vue-vtable/src/components/custom/vtable-vue-attribute-plugin.ts +++ b/packages/vue-vtable/src/components/custom/vtable-vue-attribute-plugin.ts @@ -489,7 +489,16 @@ export class VTableVueAttributePlugin extends HtmlAttributePlugin implements IPl const { vue: options, width, height, visible, display, ...rest } = attribute || {}; const { x: left, y: top } = this.calculatePosition(graphic, options.anchorType); const { left: offsetX, top: offsetTop } = this.calculateOffset(stage, nativeContainer, left, top); - const { safeWidth, safeHeight } = this.getScrollbarSafeSize(graphic, offsetX, offsetTop, width, height, options); + const { safeWidth, safeHeight } = this.getScrollbarSafeSize( + graphic, + stage, + nativeContainer, + offsetX, + offsetTop, + width, + height, + options + ); const { id } = this.getGraphicOptions(graphic) || {}; const record = id ? this.htmlMap[id] : null; @@ -548,6 +557,8 @@ export class VTableVueAttributePlugin extends HtmlAttributePlugin implements IPl private getScrollbarSafeSize( graphic: IGraphic, + stage: IStage, + nativeContainer: HTMLElement, offsetX: number, offsetTop: number, width: number, @@ -579,14 +590,21 @@ export class VTableVueAttributePlugin extends HtmlAttributePlugin implements IPl const verticalTop = table.getFrozenRowsHeight() + (!hoverOn ? groupY : 0); const verticalBottom = verticalTop + table.tableNoFrameHeight - table.getFrozenRowsHeight() - table.getBottomFrozenRowsHeight(); + const { left: verticalDomLeft, top: verticalDomTop } = this.calculateOffset( + stage, + nativeContainer, + verticalLeft, + verticalTop + ); + const verticalDomBottom = verticalDomTop + verticalBottom - verticalTop; if ( - offsetTop < verticalBottom && - domBottom > verticalTop && - offsetX < verticalLeft + scrollBarSize && - domRight > verticalLeft + offsetTop < verticalDomBottom && + domBottom > verticalDomTop && + offsetX < verticalDomLeft + scrollBarSize && + domRight > verticalDomLeft ) { - safeWidth = Math.max(0, Math.min(width, verticalLeft - offsetX)); + safeWidth = Math.max(0, Math.min(width, verticalDomLeft - offsetX)); } } @@ -603,14 +621,21 @@ export class VTableVueAttributePlugin extends HtmlAttributePlugin implements IPl const horizontalTop = Math.min(table.tableNoFrameHeight, table.getAllRowsHeight()) - (hoverOn ? scrollBarSize : -groupY); const horizontalRight = horizontalLeft + horizontalWidth; + const { left: horizontalDomLeft, top: horizontalDomTop } = this.calculateOffset( + stage, + nativeContainer, + horizontalLeft, + horizontalTop + ); + const horizontalDomRight = horizontalDomLeft + horizontalRight - horizontalLeft; if ( - offsetX < horizontalRight && - domRight > horizontalLeft && - offsetTop < horizontalTop + scrollBarSize && - domBottom > horizontalTop + offsetX < horizontalDomRight && + domRight > horizontalDomLeft && + offsetTop < horizontalDomTop + scrollBarSize && + domBottom > horizontalDomTop ) { - safeHeight = Math.max(0, Math.min(height, horizontalTop - offsetTop)); + safeHeight = Math.max(0, Math.min(height, horizontalDomTop - offsetTop)); } } } From 8711969f9e3bdcb7cad7f76d11332f3474efd4b0 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Wed, 29 Jul 2026 11:35:29 +0800 Subject: [PATCH 45/77] fix(vtable): avoid global auto row min height --- .../examples/debug/issue-5117-auto-height-real-height.ts | 8 ++++---- .../vtable/src/scenegraph/layout/compute-row-height.ts | 7 +------ 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/packages/vtable/examples/debug/issue-5117-auto-height-real-height.ts b/packages/vtable/examples/debug/issue-5117-auto-height-real-height.ts index 77e3099a91..c9384317c9 100644 --- a/packages/vtable/examples/debug/issue-5117-auto-height-real-height.ts +++ b/packages/vtable/examples/debug/issue-5117-auto-height-real-height.ts @@ -27,6 +27,7 @@ export function createTable() { container.before(toolbar); const realHeights: Array = []; + const minRowHeight = 64; const tableInstance = new VTable.ListTable({ container, records, @@ -44,18 +45,17 @@ export function createTable() { ], widthMode: 'standard', heightMode: 'autoHeight', - defaultRowHeight: 64, customComputeRowHeight: ({ row, realHeight }) => { realHeights[row] = realHeight; - return 'auto'; + return Math.max(realHeight ?? 0, minRowHeight); } }); const check = () => { - const bodyRow = tableInstance.columnHeaderLevelCount + 1; + const bodyRow = tableInstance.columnHeaderLevelCount; const realHeight = realHeights[bodyRow]; const rowHeight = tableInstance.getRowHeight(bodyRow); - const pass = typeof realHeight === 'number' && realHeight >= 64 && rowHeight >= 64; + const pass = typeof realHeight === 'number' && rowHeight === Math.max(realHeight, minRowHeight); const state = document.getElementById('issue5117State')!; state.textContent = `${pass ? 'PASS' : 'FAIL'} | realHeight=${realHeight} rowHeight=${rowHeight}`; return { pass, realHeight, rowHeight }; diff --git a/packages/vtable/src/scenegraph/layout/compute-row-height.ts b/packages/vtable/src/scenegraph/layout/compute-row-height.ts index 81feacf957..cf495d6e68 100644 --- a/packages/vtable/src/scenegraph/layout/compute-row-height.ts +++ b/packages/vtable/src/scenegraph/layout/compute-row-height.ts @@ -351,11 +351,6 @@ export function computeRowsHeight( } } -const limitMinRowHeight = (height: number, row: number, table: BaseTableAPI): number => { - const defaultHeight = table.getDefaultRowHeight(row); - return isNumber(defaultHeight) ? Math.max(height, defaultHeight) : height; -}; - export function computeRowHeight(row: number, startCol: number, endCol: number, table: BaseTableAPI): number { return computeRowHeightInternal(row, startCol, endCol, table, true); } @@ -493,7 +488,7 @@ function computeRowHeightInternal( maxHeight = isValid(maxHeight) ? Math.max(textHeight, maxHeight) : textHeight; } if (isValid(maxHeight)) { - return limitMinRowHeight(maxHeight, row, table); + return maxHeight; } const defaultHeight = table.getDefaultRowHeight(row); From 1eb4d2891dd584ee2f6d24124e7f6033547a9514 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Wed, 29 Jul 2026 12:48:03 +0800 Subject: [PATCH 46/77] fix(vue-vtable): match horizontal scrollbar range --- .../custom/vtable-vue-attribute-plugin.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/vue-vtable/src/components/custom/vtable-vue-attribute-plugin.ts b/packages/vue-vtable/src/components/custom/vtable-vue-attribute-plugin.ts index 6de5c7cd6a..15d2cdd0f2 100644 --- a/packages/vue-vtable/src/components/custom/vtable-vue-attribute-plugin.ts +++ b/packages/vue-vtable/src/components/custom/vtable-vue-attribute-plugin.ts @@ -576,7 +576,8 @@ export class VTableVueAttributePlugin extends HtmlAttributePlugin implements IPl const verticalVisible = scrollStyle?.verticalVisible ?? scrollStyle?.visible; const horizontalVisible = scrollStyle?.horizontalVisible ?? scrollStyle?.visible; const hasVerticalScrollBar = verticalVisible !== 'none' && table.getAllRowsHeight() > table.tableNoFrameHeight; - const hasHorizontalScrollBar = horizontalVisible !== 'none' && table.getAllColsWidth() > table.tableNoFrameWidth; + const hasHorizontalScrollBar = + horizontalVisible !== 'none' && this.getBodyHorizontalScrollRange(table) > this.getScrollSizeTolerance(table); const scrollBarSize = scrollStyle?.width ?? 7; const groupX = table.scenegraph?.tableGroup?.attribute?.x ?? 0; const groupY = table.scenegraph?.tableGroup?.attribute?.y ?? 0; @@ -643,6 +644,22 @@ export class VTableVueAttributePlugin extends HtmlAttributePlugin implements IPl return { safeWidth, safeHeight }; } + private getBodyHorizontalScrollRange(table: any) { + const totalWidth = table.getAllColsWidth(); + const frozenColsWidth = table.getFrozenColsWidth(); + const rightFrozenColsWidth = table.getRightFrozenColsWidth(); + const frozenColsContentWidth = table.getFrozenColsContentWidth?.() ?? frozenColsWidth; + const rightFrozenColsContentWidth = table.getRightFrozenColsContentWidth?.() ?? rightFrozenColsWidth; + const bodyViewportWidth = table.tableNoFrameWidth - frozenColsWidth - rightFrozenColsWidth; + const bodyContentWidth = totalWidth - frozenColsContentWidth - rightFrozenColsContentWidth; + + return Math.max(0, bodyContentWidth - bodyViewportWidth); + } + + private getScrollSizeTolerance(table: any) { + return table.options?.customConfig?._disableColumnAndRowSizeRound ? 1 : 0; + } + /** * @description: 事件监听器管理 * @param {HTMLElement} wrapContainer From eac5791fec86ad7b287b55a8bcf2f97f9bdee3d5 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Wed, 29 Jul 2026 12:48:04 +0800 Subject: [PATCH 47/77] fix(vtable): align first screen body row range --- .../debug/issue-5115-auto-height-zero-row.ts | 4 +++- .../progress/create-group-for-first-screen.ts | 15 ++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/vtable/examples/debug/issue-5115-auto-height-zero-row.ts b/packages/vtable/examples/debug/issue-5115-auto-height-zero-row.ts index 3f69b13d0f..92e1bfb0b6 100644 --- a/packages/vtable/examples/debug/issue-5115-auto-height-zero-row.ts +++ b/packages/vtable/examples/debug/issue-5115-auto-height-zero-row.ts @@ -34,6 +34,7 @@ export function createTable() { widthMode: 'standard', heightMode: 'autoHeight', defaultRowHeight: 40, + bottomFrozenRowCount: 2, customComputeRowHeight: ({ row }) => { if (row > 0 && (row - 1) % 2 === 0) { return 0; @@ -45,7 +46,8 @@ export function createTable() { const check = () => { const proxy = tableInstance.scenegraph.proxy; const bodyStart = tableInstance.frozenRowCount; - const bodyHeight = tableInstance.tableNoFrameHeight - tableInstance.getFrozenRowsHeight(); + const bodyHeight = + tableInstance.tableNoFrameHeight - tableInstance.getFrozenRowsHeight() - tableInstance.getBottomFrozenRowsHeight(); const renderedHeight = tableInstance.getRowsHeight(bodyStart, proxy.rowEnd); const zeroHeight = tableInstance.getRowHeight(tableInstance.columnHeaderLevelCount) === 0; const pass = zeroHeight && renderedHeight >= bodyHeight; diff --git a/packages/vtable/src/scenegraph/group-creater/progress/create-group-for-first-screen.ts b/packages/vtable/src/scenegraph/group-creater/progress/create-group-for-first-screen.ts index f552cee718..0bf78f4ba5 100644 --- a/packages/vtable/src/scenegraph/group-creater/progress/create-group-for-first-screen.ts +++ b/packages/vtable/src/scenegraph/group-creater/progress/create-group-for-first-screen.ts @@ -65,6 +65,7 @@ export function createGroupForFirstScreen( } else { distRow = Math.min(proxy.firstScreenRowLimit - 1, table.rowCount - 1); } + let bodyDistRow = Math.min(proxy.bodyBottomRow, distRow - table.bottomFrozenRowCount); if (table.internalProps._widthResizedColMap.size === 0) { // compute colums width in first screen computeColsWidth(table, 0, distColForCompute ?? distCol); @@ -80,7 +81,7 @@ export function createGroupForFirstScreen( : distRowForCompute ?? distRow ); //如果配置了 canvasHeight为 'auto', 则一次性将所有行高都计算出来才能满足后续赋值表格高度的使用 if (table.heightMode === 'autoHeight') { - distRow = fillVisibleBodyRows(proxy, distRow); + bodyDistRow = fillVisibleBodyRows(proxy, bodyDistRow); } } @@ -90,7 +91,7 @@ export function createGroupForFirstScreen( computeColsWidth(table, table.colCount - table.rightFrozenColCount, table.colCount - 1); } } - if (distRow < table.rowCount - table.bottomFrozenRowCount) { + if (bodyDistRow < table.rowCount - table.bottomFrozenRowCount) { // compute bottom frozen row height if (table.rowCount - table.bottomFrozenRowCount <= table.rowCount - 1) { computeRowsHeight(table, table.rowCount - table.bottomFrozenRowCount, table.rowCount - 1); @@ -146,7 +147,7 @@ export function createGroupForFirstScreen( table.leftRowSeriesNumberCount - 1, // colEnd table.frozenRowCount, // rowStart // Math.min(proxy.firstScreenRowLimit, table.rowCount - 1 - table.bottomFrozenRowCount), // rowEnd - distRow - table.bottomFrozenRowCount, + bodyDistRow, 'rowHeader', // isHeader table ); @@ -160,7 +161,7 @@ export function createGroupForFirstScreen( Math.min(table.frozenColCount - 1, table.rowHeaderLevelCount + table.leftRowSeriesNumberCount - 1), // colEnd table.frozenRowCount, // rowStart // Math.min(proxy.firstScreenRowLimit, table.rowCount - 1 - table.bottomFrozenRowCount), // rowEnd - distRow - table.bottomFrozenRowCount, + bodyDistRow, 'rowHeader', // isHeader table ); @@ -174,7 +175,7 @@ export function createGroupForFirstScreen( table.frozenColCount - 1, // colEnd table.frozenRowCount, // rowStart // Math.min(proxy.firstScreenRowLimit, table.rowCount - 1 - table.bottomFrozenRowCount), // rowEnd - distRow - table.bottomFrozenRowCount, + bodyDistRow, 'body', table ); @@ -293,7 +294,7 @@ export function createGroupForFirstScreen( table.colCount - 1, // colEnd table.frozenRowCount, // rowStart // Math.min(proxy.firstScreenRowLimit, table.rowCount - 1 - table.bottomFrozenRowCount), // rowEnd - distRow - table.bottomFrozenRowCount, + bodyDistRow, table.isPivotChart() ? 'rowHeader' : 'body', // isHeader table ); @@ -325,7 +326,7 @@ export function createGroupForFirstScreen( distCol - table.rightFrozenColCount, table.frozenRowCount, // rowStart // Math.min(proxy.firstScreenRowLimit, table.rowCount - 1 - table.bottomFrozenRowCount), // rowEnd - distRow - table.bottomFrozenRowCount, + bodyDistRow, 'body', // isHeader table ); From 2d21bb1fc391d6026b7b38a0251ab79cb5fe2d77 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Wed, 29 Jul 2026 14:06:45 +0800 Subject: [PATCH 48/77] fix(vue-vtable): align vertical scrollbar tolerance --- .../src/components/custom/vtable-vue-attribute-plugin.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/vue-vtable/src/components/custom/vtable-vue-attribute-plugin.ts b/packages/vue-vtable/src/components/custom/vtable-vue-attribute-plugin.ts index 15d2cdd0f2..791698aec5 100644 --- a/packages/vue-vtable/src/components/custom/vtable-vue-attribute-plugin.ts +++ b/packages/vue-vtable/src/components/custom/vtable-vue-attribute-plugin.ts @@ -575,9 +575,11 @@ export class VTableVueAttributePlugin extends HtmlAttributePlugin implements IPl if (pointerEvents !== 'none' && table && !barToSide) { const verticalVisible = scrollStyle?.verticalVisible ?? scrollStyle?.visible; const horizontalVisible = scrollStyle?.horizontalVisible ?? scrollStyle?.visible; - const hasVerticalScrollBar = verticalVisible !== 'none' && table.getAllRowsHeight() > table.tableNoFrameHeight; + const sizeTolerance = this.getScrollSizeTolerance(table); + const hasVerticalScrollBar = + verticalVisible !== 'none' && table.getAllRowsHeight() > table.tableNoFrameHeight + sizeTolerance; const hasHorizontalScrollBar = - horizontalVisible !== 'none' && this.getBodyHorizontalScrollRange(table) > this.getScrollSizeTolerance(table); + horizontalVisible !== 'none' && this.getBodyHorizontalScrollRange(table) > sizeTolerance; const scrollBarSize = scrollStyle?.width ?? 7; const groupX = table.scenegraph?.tableGroup?.attribute?.x ?? 0; const groupY = table.scenegraph?.tableGroup?.attribute?.y ?? 0; From de79c407c71b0d9ef97a13f76bf6bd8ff9d83356 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Wed, 29 Jul 2026 14:06:46 +0800 Subject: [PATCH 49/77] fix(vtable): keep real height getter non-enumerable --- packages/vtable/src/scenegraph/layout/compute-row-height.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vtable/src/scenegraph/layout/compute-row-height.ts b/packages/vtable/src/scenegraph/layout/compute-row-height.ts index cf495d6e68..dade83a49b 100644 --- a/packages/vtable/src/scenegraph/layout/compute-row-height.ts +++ b/packages/vtable/src/scenegraph/layout/compute-row-height.ts @@ -385,7 +385,7 @@ function computeRowHeightInternal( } as Parameters>[0]; Object.defineProperty(computeArgs, 'realHeight', { get: getRealHeight, - enumerable: true, + enumerable: false, configurable: true }); const customRowHeight = table.options.customComputeRowHeight(computeArgs); From ebd2af57b5c4748295605f9835990d3af7414d4c Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Wed, 29 Jul 2026 14:24:13 +0800 Subject: [PATCH 50/77] fix(vue-vtable): clip frozen horizontal scrollbars --- .../Issue5157CustomLayoutScrollbar.vue | 51 ++++++++++++++----- .../custom/vtable-vue-attribute-plugin.ts | 45 ++++++++++++---- 2 files changed, 73 insertions(+), 23 deletions(-) diff --git a/packages/vue-vtable/demo/src/table/gramatical/composition/Issue5157CustomLayoutScrollbar.vue b/packages/vue-vtable/demo/src/table/gramatical/composition/Issue5157CustomLayoutScrollbar.vue index baaa7ba7e8..ff6302d2a3 100644 --- a/packages/vue-vtable/demo/src/table/gramatical/composition/Issue5157CustomLayoutScrollbar.vue +++ b/packages/vue-vtable/demo/src/table/gramatical/composition/Issue5157CustomLayoutScrollbar.vue @@ -4,7 +4,19 @@ {{ state }} - + + + @@ -43,6 +55,9 @@ const tableOptions = ref({ heightMode: 'standard', defaultRowHeight: 44, defaultHeaderRowHeight: 40, + frozenColCount: 1, + scrollFrozenCols: true, + maxFrozenWidth: 80, theme: { ...VTable.themes.DEFAULT, scrollStyle: { @@ -56,7 +71,7 @@ const tableOptions = ref({ } }); -function renderAction(record: any, row: number) { +function renderAction(record: any, row: number, label: string) { return h( 'button', { @@ -66,26 +81,36 @@ function renderAction(record: any, row: number) { state.value = `clicked ${record.id}`; } }, - `Action ${record.id}` + `${label} ${record.id}` ); } function check() { const table = tableRef.value?.vTableInstance; - const dom = document.querySelector('[id^="vue_issue5157-"]'); - if (!table || !dom) { + const bodyDom = document.querySelector('[id^="vue_issue5157-body-"]'); + const frozenDom = document.querySelector('[id^="vue_issue5157-frozen-"]'); + if (!table || !bodyDom || !frozenDom) { state.value = 'FAIL | missing table or dom'; return { pass: false }; } - const domRect = dom.getBoundingClientRect(); + const bodyRect = bodyDom.getBoundingClientRect(); + const frozenRect = frozenDom.getBoundingClientRect(); const tableRect = table.getElement().getBoundingClientRect(); const scrollbarLeft = tableRect.left + table.tableNoFrameWidth - 16; - const pass = domRect.right <= scrollbarLeft + 0.5; - state.value = `${pass ? 'PASS' : 'FAIL'} | domRight=${Math.round(domRect.right)} scrollbarLeft=${Math.round( - scrollbarLeft - )}`; - return { pass, domRight: domRect.right, scrollbarLeft }; + const frozenScrollbarTop = tableRect.top + Math.min(table.tableNoFrameHeight, table.getAllRowsHeight()) - 16; + const pass = bodyRect.right <= scrollbarLeft + 0.5 && frozenRect.bottom <= frozenScrollbarTop + 0.5; + state.value = + `${pass ? 'PASS' : 'FAIL'} | bodyRight=${Math.round(bodyRect.right)} scrollbarLeft=${Math.round(scrollbarLeft)} ` + + `frozenBottom=${Math.round(frozenRect.bottom)} frozenScrollbarTop=${Math.round(frozenScrollbarTop)}`; + return { + pass, + bodyRight: bodyRect.right, + scrollbarLeft, + frozenBottom: frozenRect.bottom, + frozenScrollbarTop, + frozenOffset: table.getFrozenColsOffset?.() + }; } nextTick(() => { diff --git a/packages/vue-vtable/src/components/custom/vtable-vue-attribute-plugin.ts b/packages/vue-vtable/src/components/custom/vtable-vue-attribute-plugin.ts index 791698aec5..86898c055c 100644 --- a/packages/vue-vtable/src/components/custom/vtable-vue-attribute-plugin.ts +++ b/packages/vue-vtable/src/components/custom/vtable-vue-attribute-plugin.ts @@ -586,6 +586,7 @@ export class VTableVueAttributePlugin extends HtmlAttributePlugin implements IPl const hoverOn = scrollStyle?.hoverOn; const domRight = offsetX + width; const domBottom = offsetTop + height; + const ignoreFrozenCols = scrollStyle?.ignoreFrozenCols ?? false; if (hasVerticalScrollBar) { const verticalLeft = @@ -611,16 +612,7 @@ export class VTableVueAttributePlugin extends HtmlAttributePlugin implements IPl } } - if (hasHorizontalScrollBar) { - const ignoreFrozenCols = scrollStyle?.ignoreFrozenCols ?? false; - const horizontalLeft = ignoreFrozenCols - ? !hoverOn - ? groupX - : 0 - : table.getFrozenColsWidth() + (!hoverOn ? groupX : 0); - const horizontalWidth = ignoreFrozenCols - ? table.tableNoFrameWidth - : table.tableNoFrameWidth - table.getFrozenColsWidth() - table.getRightFrozenColsWidth(); + const clipHorizontalScrollbar = (horizontalLeft: number, horizontalWidth: number) => { const horizontalTop = Math.min(table.tableNoFrameHeight, table.getAllRowsHeight()) - (hoverOn ? scrollBarSize : -groupY); const horizontalRight = horizontalLeft + horizontalWidth; @@ -640,6 +632,39 @@ export class VTableVueAttributePlugin extends HtmlAttributePlugin implements IPl ) { safeHeight = Math.max(0, Math.min(height, horizontalDomTop - offsetTop)); } + }; + + if (hasHorizontalScrollBar) { + const horizontalLeft = ignoreFrozenCols + ? !hoverOn + ? groupX + : 0 + : table.getFrozenColsWidth() + (!hoverOn ? groupX : 0); + const horizontalWidth = ignoreFrozenCols + ? table.tableNoFrameWidth + : table.tableNoFrameWidth - table.getFrozenColsWidth() - table.getRightFrozenColsWidth(); + clipHorizontalScrollbar(horizontalLeft, horizontalWidth); + } + + if ( + horizontalVisible !== 'none' && + !ignoreFrozenCols && + table.options?.scrollFrozenCols && + table.getFrozenColsOffset?.() > 0 + ) { + clipHorizontalScrollbar(!hoverOn ? groupX : 0, table.getFrozenColsWidth()); + } + + if ( + horizontalVisible !== 'none' && + !ignoreFrozenCols && + table.options?.scrollRightFrozenCols && + table.getRightFrozenColsOffset?.() > 0 + ) { + clipHorizontalScrollbar( + table.tableNoFrameWidth - table.getRightFrozenColsWidth() + (!hoverOn ? groupX : 0), + table.getRightFrozenColsWidth() + ); } } From 7ce351ef94743b839874099176ac7d2c3509ee30 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Wed, 29 Jul 2026 15:49:27 +0800 Subject: [PATCH 51/77] fix(vtable): fill auto height rows after tree toggle --- .../debug/issue-5115-auto-height-zero-row.ts | 87 ++++++++++++++----- .../src/scenegraph/layout/update-row.ts | 40 +++++++++ 2 files changed, 107 insertions(+), 20 deletions(-) diff --git a/packages/vtable/examples/debug/issue-5115-auto-height-zero-row.ts b/packages/vtable/examples/debug/issue-5115-auto-height-zero-row.ts index 92e1bfb0b6..ea5d5efcfd 100644 --- a/packages/vtable/examples/debug/issue-5115-auto-height-zero-row.ts +++ b/packages/vtable/examples/debug/issue-5115-auto-height-zero-row.ts @@ -2,11 +2,36 @@ import * as VTable from '../../src'; const CONTAINER_ID = 'vTable'; -const records = Array.from({ length: 120 }, (_, index) => ({ - id: index + 1, - status: index % 2 === 0 ? 'hidden-height-0' : 'visible', - text: `row ${index + 1} visible content` +const filteredChildren = Array.from({ length: 300 }, (_, index) => ({ + name: `filtered child ${index + 1}`, + status: 'visible before filter', + desc: 'This row is hidden after filter interaction', + _show: true })); +const visibleChildren = Array.from({ length: 40 }, (_, index) => ({ + name: `visible child ${index + 1}`, + status: 'visible after filter', + desc: `Visible tree row ${index + 1} should fill the viewport after filtered zero-height rows`, + _show: true +})); +const records = [ + { + name: 'expanded group filtered by interaction', + status: 'group', + desc: 'Children below become _show=false after simulated filter interaction', + _show: true, + hierarchyState: 'expand', + children: filteredChildren + }, + { + name: 'expanded group with visible children', + status: 'group', + desc: 'These rows should be pulled into first screen after zero-height rows', + _show: true, + hierarchyState: 'expand', + children: visibleChildren + } +]; export function createTable() { const container = document.getElementById(CONTAINER_ID)!; @@ -19,29 +44,30 @@ export function createTable() { toolbar.style.cssText = 'height: 48px; font-size: 12px; display: flex; gap: 12px; align-items: center;'; toolbar.innerHTML = ` + `; container.before(toolbar); - const tableInstance = new VTable.ListTable({ + const option: VTable.ListTableConstructorOptions = { container, records, columns: [ - { field: 'id', title: 'ID', width: 80 }, - { field: 'status', title: 'Status', width: 160 }, - { field: 'text', title: 'Text', width: 260 } + { field: 'name', title: 'Name', tree: true, width: 260 }, + { field: 'status', title: 'Status', width: 180 }, + { field: 'desc', title: 'Description', width: 260 } ], widthMode: 'standard', heightMode: 'autoHeight', defaultRowHeight: 40, - bottomFrozenRowCount: 2, - customComputeRowHeight: ({ row }) => { - if (row > 0 && (row - 1) % 2 === 0) { - return 0; - } - return 'auto'; + hierarchyIndent: 20, + hierarchyExpandLevel: 2, + customComputeRowHeight: ({ row, table }) => { + const record = table.getCellOriginRecord(0, row); + return record && record._show === false ? 0 : 'auto'; } - }); + }; + const tableInstance = new VTable.ListTable(option); const check = () => { const proxy = tableInstance.scenegraph.proxy; @@ -49,19 +75,40 @@ export function createTable() { const bodyHeight = tableInstance.tableNoFrameHeight - tableInstance.getFrozenRowsHeight() - tableInstance.getBottomFrozenRowsHeight(); const renderedHeight = tableInstance.getRowsHeight(bodyStart, proxy.rowEnd); - const zeroHeight = tableInstance.getRowHeight(tableInstance.columnHeaderLevelCount) === 0; - const pass = zeroHeight && renderedHeight >= bodyHeight; + const firstFilteredRowHeight = tableInstance.getRowHeight(tableInstance.columnHeaderLevelCount + 1); + const pass = firstFilteredRowHeight === 0 && renderedHeight >= bodyHeight; const state = document.getElementById('issue5115State')!; state.textContent = - `${pass ? 'PASS' : 'FAIL'} | zeroHeight=${zeroHeight} renderedHeight=${renderedHeight} ` + + `${pass ? 'PASS' : 'FAIL'} | firstFilteredRowHeight=${firstFilteredRowHeight} renderedHeight=${renderedHeight} ` + `bodyHeight=${bodyHeight} rowEnd=${proxy.rowEnd}`; - return { pass, zeroHeight, renderedHeight, bodyHeight, rowEnd: proxy.rowEnd }; + return { pass, firstFilteredRowHeight, renderedHeight, bodyHeight, rowEnd: proxy.rowEnd }; + }; + + const filterRows = () => { + filteredChildren.forEach(record => { + record._show = false; + record.status = 'hidden by _show=false'; + }); + tableInstance.updateOption(option, { clearRowHeightCache: true, clearColWidthCache: false }); + }; + + const toggleCheck = async () => { + tableInstance.toggleHierarchyState(0, 1, false); + await new Promise(resolve => setTimeout(resolve, 60)); + tableInstance.toggleHierarchyState(0, 1, false); + await new Promise(resolve => setTimeout(resolve, 120)); + return check(); }; document.getElementById('issue5115Check')!.addEventListener('click', check); + document.getElementById('issue5115ToggleCheck')!.addEventListener('click', toggleCheck); window.tableInstance = tableInstance; (window as any).issue5115Check = check; + (window as any).issue5115ToggleCheck = toggleCheck; - setTimeout(check, 0); + setTimeout(() => { + filterRows(); + setTimeout(check, 0); + }, 0); } diff --git a/packages/vtable/src/scenegraph/layout/update-row.ts b/packages/vtable/src/scenegraph/layout/update-row.ts index a2175ff157..610bbcefa5 100644 --- a/packages/vtable/src/scenegraph/layout/update-row.ts +++ b/packages/vtable/src/scenegraph/layout/update-row.ts @@ -7,6 +7,7 @@ import type { Scenegraph } from '../scenegraph'; import { getCellMergeInfo } from '../utils/get-cell-merge'; import { deduplication } from '../../tools/util'; import { checkHaveTextStick, resetTextStick } from '../stick-text'; +import { computeRowsHeight } from './compute-row-height'; /** * add and remove rows in scenegraph @@ -78,6 +79,11 @@ export function updateRow( updateAfter = updateAfter ?? needUpdateAfter; rowHeightsMap.insert(row); }); + const filledVisibleRowStart = fillVisibleBodyRows(scene); + if (isNumber(filledVisibleRowStart)) { + updateAfter = updateAfter ?? filledVisibleRowStart; + rowUpdatePos = isValid(rowUpdatePos) ? Math.min(rowUpdatePos, filledVisibleRowStart) : filledVisibleRowStart; + } // reset attribute y and row number in CellGroup // const newTotalHeight = resetRowNumberAndY(scene); @@ -277,6 +283,40 @@ function addRow(row: number, scene: Scenegraph, skipUpdateProxy?: boolean) { // scene.proxy.rowEnd++; // scene.proxy.currentRow++; } + +function fillVisibleBodyRows(scene: Scenegraph): number | undefined { + const { table, proxy } = scene; + if (table.heightMode !== 'autoHeight') { + return undefined; + } + const bodyBottomRow = table.rowCount - 1 - table.bottomFrozenRowCount; + const visibleBodyHeight = table.tableNoFrameHeight - table.getFrozenRowsHeight() - table.getBottomFrozenRowsHeight(); + let targetRow = Math.min(proxy.rowEnd, bodyBottomRow); + + computeRowsHeight(table, proxy.rowStart, targetRow, false); + while (targetRow < bodyBottomRow && table.getRowsHeight(table.frozenRowCount, targetRow) < visibleBodyHeight) { + const nextRow = targetRow + 1; + computeRowsHeight(table, nextRow, nextRow, false); + targetRow = nextRow; + } + + if (targetRow <= proxy.rowEnd) { + return undefined; + } + + const startRow = proxy.rowEnd + 1; + for (let row = startRow; row <= targetRow; row++) { + addRowCellGroup(row, scene); + } + proxy.rowEnd = targetRow; + proxy.currentRow = Math.max(proxy.currentRow, targetRow); + proxy.totalRow = Math.max(proxy.totalRow, targetRow); + proxy.totalActualBodyRowCount = Math.max(proxy.totalActualBodyRowCount, targetRow - proxy.rowStart + 1); + proxy.rowUpdatePos = Math.min(proxy.rowUpdatePos, startRow); + + return startRow; +} + function resetRowNumber(scene: Scenegraph) { scene.bodyGroup.forEachChildren((colGroup: Group) => { let rowIndex = scene.bodyRowStart; From 1f24bda8eda55aa9647d1951de1e651015505586 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Wed, 29 Jul 2026 16:15:32 +0800 Subject: [PATCH 52/77] fix(vue-vtable): clip scrollbar hit area with padding --- .../Issue5157CustomLayoutScrollbar.vue | 96 +++++++++++-------- .../custom/vtable-vue-attribute-plugin.ts | 15 +++ 2 files changed, 69 insertions(+), 42 deletions(-) diff --git a/packages/vue-vtable/demo/src/table/gramatical/composition/Issue5157CustomLayoutScrollbar.vue b/packages/vue-vtable/demo/src/table/gramatical/composition/Issue5157CustomLayoutScrollbar.vue index ff6302d2a3..7da2522d7b 100644 --- a/packages/vue-vtable/demo/src/table/gramatical/composition/Issue5157CustomLayoutScrollbar.vue +++ b/packages/vue-vtable/demo/src/table/gramatical/composition/Issue5157CustomLayoutScrollbar.vue @@ -3,35 +3,37 @@ {{ state }} - - - - - - - - - +
+ + + + + + + + + +
+ + diff --git a/packages/vue-vtable/src/edit/editor.ts b/packages/vue-vtable/src/edit/editor.ts index ce41464259..256ec9805e 100644 --- a/packages/vue-vtable/src/edit/editor.ts +++ b/packages/vue-vtable/src/edit/editor.ts @@ -182,8 +182,6 @@ export class DynamicRenderEditor { wrapContainer.style.position = 'absolute'; wrapContainer.style.width = '100%'; wrapContainer.style.boxSizing = 'border-box'; - const { bgColor } = table.getCellStyle(col, row) || {}; - wrapContainer.style.backgroundColor = bgColor || '#FFFFFF'; this.wrapContainer = wrapContainer; this.tableContainer = container; this.tableContainer.appendChild(wrapContainer); From 81531f39bf26be459c55d83c6689bcfee8aba0db Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Thu, 30 Jul 2026 13:19:43 +0800 Subject: [PATCH 60/77] test(vtable-plugins): add filter popup position regression demo --- .../issue-4625-filter-popup-position.ts | 61 +++++++++++++++++++ packages/vtable-plugins/demo/menu.ts | 4 ++ 2 files changed, 65 insertions(+) create mode 100644 packages/vtable-plugins/demo/filter/issue-4625-filter-popup-position.ts diff --git a/packages/vtable-plugins/demo/filter/issue-4625-filter-popup-position.ts b/packages/vtable-plugins/demo/filter/issue-4625-filter-popup-position.ts new file mode 100644 index 0000000000..dc1658e3d0 --- /dev/null +++ b/packages/vtable-plugins/demo/filter/issue-4625-filter-popup-position.ts @@ -0,0 +1,61 @@ +import * as VTable from '@visactor/vtable'; +import { FilterPlugin } from '../../src/filter'; + +const CONTAINER_ID = 'vTable'; + +const generateDemoData = (count: number) => { + const departments = ['研发部', '市场部', '销售部', '人事部', '财务部']; + const statuses = ['在职', '请假', '离职']; + + return Array.from(new Array(count)).map((_, i) => ({ + id: i + 1, + name: `员工${i + 1}`, + age: 22 + (i % 20), + department: departments[i % departments.length], + salary: 5000 + i * 300, + status: statuses[i % statuses.length], + isFullTime: i % 3 !== 0, + status1: statuses[(i + 1) % statuses.length], + status2: statuses[(i + 2) % statuses.length], + status3: statuses[i % statuses.length], + status4: statuses[(i + 1) % statuses.length], + status5: statuses[(i + 2) % statuses.length] + })); +}; + +export function createTable() { + const container = document.getElementById(CONTAINER_ID)!; + container.style.width = '800px'; + container.style.height = '500px'; + + const filterPlugin = new FilterPlugin({ + filterModes: ['byValue', 'byCondition'] + }); + + const tableInstance = new VTable.ListTable({ + container, + records: generateDemoData(50), + columns: [ + { field: 'id', title: 'ID', width: 60 }, + { field: 'name', title: '姓名', width: 120 }, + { field: 'age', title: '年龄', width: 100 }, + { field: 'department', title: '部门', width: 120 }, + { + field: 'salary', + title: '薪资', + width: 120, + fieldFormat: record => `¥${record.salary}` + }, + { field: 'status', title: '状态', width: 100 }, + { field: 'isFullTime', title: '全职', width: 80, cellType: 'checkbox' }, + { field: 'status1', title: '状态1', width: 100 }, + { field: 'status2', title: '状态2', width: 100 }, + { field: 'status3', title: '状态3', width: 100 }, + { field: 'status4', title: '状态4', width: 100 }, + { field: 'status5', title: '状态5', width: 100 } + ], + plugins: [filterPlugin] + }); + + window.tableInstance = tableInstance; +} diff --git a/packages/vtable-plugins/demo/menu.ts b/packages/vtable-plugins/demo/menu.ts index b252dd55ad..f110a5cb4b 100644 --- a/packages/vtable-plugins/demo/menu.ts +++ b/packages/vtable-plugins/demo/menu.ts @@ -27,6 +27,10 @@ export const menus = [ path: 'filter', name: 'issue-5137' }, + { + path: 'filter', + name: 'issue-4625-filter-popup-position' + }, { path: 'filter', name: 'value-filter' From 9915ee4327b223d5e713cc3ddd472c355414a87e Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Thu, 30 Jul 2026 14:07:16 +0800 Subject: [PATCH 61/77] fix(vtable): update reused text icon attributes --- .../debug/issue-4798-sort-icon-visible-time.ts | 2 ++ .../src/scenegraph/utils/text-icon-layout.ts | 18 +++++++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/vtable/examples/debug/issue-4798-sort-icon-visible-time.ts b/packages/vtable/examples/debug/issue-4798-sort-icon-visible-time.ts index 8bb1e25f5a..3faf1d3c3a 100644 --- a/packages/vtable/examples/debug/issue-4798-sort-icon-visible-time.ts +++ b/packages/vtable/examples/debug/issue-4798-sort-icon-visible-time.ts @@ -48,6 +48,8 @@ function getSortIconState(tableInstance: VTable.ListTable) { if (mark.attribute?.funcType === VTable.TYPES.IconFuncTypeEnum.sort) { state = { name: mark.name, + text: mark.attribute.text, + fill: mark.attribute.fill, visibleTime: mark.attribute.visibleTime, opacity: mark.attribute.opacity }; diff --git a/packages/vtable/src/scenegraph/utils/text-icon-layout.ts b/packages/vtable/src/scenegraph/utils/text-icon-layout.ts index 82d1903c88..8920f0b7d8 100644 --- a/packages/vtable/src/scenegraph/utils/text-icon-layout.ts +++ b/packages/vtable/src/scenegraph/utils/text-icon-layout.ts @@ -539,8 +539,6 @@ export function dealWithIcon( iconAttribute.funcType = icon.funcType; iconAttribute.interactive = icon.interactive; iconAttribute.isGif = (icon as any).isGif; - iconAttribute.opacity = - iconAttribute.visibleTime === 'mouseenter_cell' || iconAttribute.visibleTime === 'click_cell' ? 0 : 1; let hierarchyOffset = 0; if ( @@ -585,9 +583,21 @@ export function dealWithIcon( iconAttribute.shape = icon.shape; } + if (icon.type === 'text') { + iconAttribute.text = icon.content; + merge(iconAttribute, icon.style); + } + + if (isNil(iconAttribute.opacity)) { + iconAttribute.opacity = + iconAttribute.visibleTime === 'mouseenter_cell' || iconAttribute.visibleTime === 'click_cell' ? 0 : 1; + } + if (mark) { mark.setAttributes(iconAttribute); - mark.loadImage(iconAttribute.image); + if (iconAttribute.image) { + mark.loadImage(iconAttribute.image); + } mark.tooltip = icon.tooltip; mark.name = icon.name; return mark; @@ -596,8 +606,6 @@ export function dealWithIcon( let iconMark: Icon | TextIcon; if (icon.type === 'text') { - iconAttribute.text = icon.content; - merge(iconAttribute, icon.style); iconMark = new TextIcon(iconAttribute); iconMark.tooltip = icon.tooltip; iconMark.name = icon.name; From 550b23d67dd4dede4a1e453fd76048de43a74c44 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Thu, 30 Jul 2026 14:18:06 +0800 Subject: [PATCH 62/77] fix(vtable): align issue 4904 demo last name field --- packages/vtable/examples/debug/issue-4904-frozen-row-gap.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vtable/examples/debug/issue-4904-frozen-row-gap.ts b/packages/vtable/examples/debug/issue-4904-frozen-row-gap.ts index 34bd29c2b0..22884137d5 100644 --- a/packages/vtable/examples/debug/issue-4904-frozen-row-gap.ts +++ b/packages/vtable/examples/debug/issue-4904-frozen-row-gap.ts @@ -44,7 +44,7 @@ export function createTable() { width: 200 }, { - field: 'name', + field: 'lastName', title: 'Last Name', width: 200 } From 10307a352bf0536fffa1ac23f162442f757b0213 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Thu, 30 Jul 2026 14:48:00 +0800 Subject: [PATCH 63/77] fix(vtable): keep bottom frozen rows connected for short content --- .../options/listTable-api-with-frozen.test.ts | 23 +++++++++++++++++++ packages/vtable/src/scenegraph/scenegraph.ts | 16 +++++++------ 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts b/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts index 4a243da287..ddf5d86c9d 100644 --- a/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts +++ b/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts @@ -143,6 +143,29 @@ describe('listTable init test', () => { }); }); + test('listTable bottom frozen rows should stay connected after short body content', () => { + const optionWithBottomFrozenRows = { + ...option, + frozenRowCount: 5, + rightFrozenColCount: 0, + bottomFrozenRowCount: 2, + container: createDiv(), + records: records.slice(0, 10) + }; + optionWithBottomFrozenRows.container.style.position = 'relative'; + optionWithBottomFrozenRows.container.style.width = '1000px'; + optionWithBottomFrozenRows.container.style.height = '800px'; + + const frozenTable = new ListTable(optionWithBottomFrozenRows); + const { scenegraph } = frozenTable; + + expect(scenegraph.bottomFrozenGroup.attribute.y).toBe( + scenegraph.bodyGroup.attribute.y + scenegraph.bodyGroup.attribute.height + ); + + frozenTable.release(); + }); + test('listTable should support decreasing rightFrozenColCount by setter with row series number', () => { const optionWithRightFrozen = { ...option, diff --git a/packages/vtable/src/scenegraph/scenegraph.ts b/packages/vtable/src/scenegraph/scenegraph.ts index 0330409fa0..6c28b10fbc 100644 --- a/packages/vtable/src/scenegraph/scenegraph.ts +++ b/packages/vtable/src/scenegraph/scenegraph.ts @@ -1436,20 +1436,22 @@ export class Scenegraph { } if (this.table.bottomFrozenRowCount > 0) { - this.bottomFrozenGroup.setAttribute( - 'y', - this.tableGroup.attribute.height - this.table.getBottomFrozenRowsHeight() + const bottomFrozenRowsHeight = this.table.getBottomFrozenRowsHeight(); + const bottomFrozenY = Math.min( + this.tableGroup.attribute.height - bottomFrozenRowsHeight, + this.bodyGroup.attribute.y + this.bodyGroup.attribute.height ); + this.bottomFrozenGroup.setAttribute('y', bottomFrozenY); this.leftBottomCornerGroup.setAttributes({ visible: true, - y: this.tableGroup.attribute.height - this.table.getBottomFrozenRowsHeight(), - height: this.table.getBottomFrozenRowsHeight(), + y: bottomFrozenY, + height: bottomFrozenRowsHeight, width: this.table.getFrozenColsWidth() }); this.rightBottomCornerGroup.setAttributes({ visible: true, - y: this.tableGroup.attribute.height - this.table.getBottomFrozenRowsHeight(), - height: this.table.getBottomFrozenRowsHeight() + y: bottomFrozenY, + height: bottomFrozenRowsHeight }); } From 90c39098c46162a71fa36cc313eb04336f3dce38 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Thu, 30 Jul 2026 15:27:13 +0800 Subject: [PATCH 64/77] test(vtable): align sort icon visibility demo --- .../issue-4798-sort-icon-visible-time.ts | 68 +++++++++++-------- 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/packages/vtable/examples/debug/issue-4798-sort-icon-visible-time.ts b/packages/vtable/examples/debug/issue-4798-sort-icon-visible-time.ts index 3faf1d3c3a..ae2ceb79ac 100644 --- a/packages/vtable/examples/debug/issue-4798-sort-icon-visible-time.ts +++ b/packages/vtable/examples/debug/issue-4798-sort-icon-visible-time.ts @@ -16,35 +16,38 @@ function registerSortIcons() { VTable.register.icon('sort_normal', { ...sortIconBase, name: 'sort_normal', - content: 'N', + content: '-', visibleTime: 'mouseenter_cell', style: { - fill: '#999' + fill: '#999', + fontWeight: 'bold' } }); VTable.register.icon('sort_upward', { ...sortIconBase, name: 'sort_upward', - content: 'A', + content: '^', visibleTime: 'always', style: { - fill: '#1677ff' + fill: '#1677ff', + fontWeight: 'bold' } }); VTable.register.icon('sort_downward', { ...sortIconBase, name: 'sort_downward', - content: 'D', + content: 'v', visibleTime: 'always', style: { - fill: '#1677ff' + fill: '#f5222d', + fontWeight: 'bold' } }); } -function getSortIconState(tableInstance: VTable.ListTable) { +function getSortIconState(tableInstance: VTable.ListTable, col: number) { let state: any = null; - tableInstance.scenegraph.getCell(0, 0).forEachChildren((mark: any) => { + tableInstance.scenegraph.getCell(col, 0).forEachChildren((mark: any) => { if (mark.attribute?.funcType === VTable.TYPES.IconFuncTypeEnum.sort) { state = { name: mark.name, @@ -58,14 +61,6 @@ function getSortIconState(tableInstance: VTable.ListTable) { return state; } -function showCurrentSortIcon(tableInstance: VTable.ListTable) { - tableInstance.scenegraph.getCell(0, 0).forEachChildren((mark: any) => { - if (mark.attribute?.funcType === VTable.TYPES.IconFuncTypeEnum.sort) { - mark.setAttribute('opacity', 1); - } - }); -} - export function createTable() { registerSortIcons(); @@ -76,27 +71,40 @@ export function createTable() { const tableInstance = new VTable.ListTable({ container, records: [ - { id: 1, name: 'Alice' }, - { id: 2, name: 'Bob' }, - { id: 3, name: 'Carol' } + { id: 1, name: 'Alice', score: 91 }, + { id: 2, name: 'Bob', score: 85 }, + { id: 3, name: 'Carol', score: 96 } ], columns: [ { field: 'id', title: 'ID', width: 120, sort: true }, - { field: 'name', title: 'Name', width: 200 } + { field: 'name', title: 'Name', width: 200, sort: true }, + { field: 'score', title: 'Score', width: 120 } ] }); window.tableInstance = tableInstance; - (window as any).issue4798GetSortIconState = () => getSortIconState(tableInstance); - (window as any).issue4798CycleSort = () => { - showCurrentSortIcon(tableInstance); - const shownNormal = getSortIconState(tableInstance); + (window as any).issue4798GetSortIconState = (col = 0) => getSortIconState(tableInstance, col); + (window as any).issue4798Run = () => { tableInstance.updateSortState({ field: 'id', order: 'asc' }); - const asc = getSortIconState(tableInstance); - tableInstance.updateSortState({ field: 'id', order: 'desc' }); - const desc = getSortIconState(tableInstance); - tableInstance.updateSortState(null); - const normal = getSortIconState(tableInstance); - return { shownNormal, asc, desc, normal }; + const firstAsc = getSortIconState(tableInstance, 0); + tableInstance.updateSortState({ field: 'name', order: 'asc' }); + const firstNormalAfterSecondSort = getSortIconState(tableInstance, 0); + const secondAsc = getSortIconState(tableInstance, 1); + return { + firstAsc, + firstNormalAfterSecondSort, + secondAsc, + fixed: + firstNormalAfterSecondSort?.name === 'sort_normal' && + firstNormalAfterSecondSort?.text === '-' && + firstNormalAfterSecondSort?.fill === '#999' && + firstNormalAfterSecondSort?.visibleTime === 'mouseenter_cell' && + firstNormalAfterSecondSort?.opacity === 0 && + secondAsc?.name === 'sort_upward' && + secondAsc?.text === '^' && + secondAsc?.fill === '#1677ff' && + secondAsc?.visibleTime === 'always' && + secondAsc?.opacity === 1 + }; }; } From 6ded357b2d800e7ba54302c1c743cf2282312d24 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Fri, 31 Jul 2026 10:20:25 +0800 Subject: [PATCH 65/77] fix(vtable): align right frozen area for short content --- .../options/listTable-api-with-frozen.test.ts | 35 +++++++++++++++++++ packages/vtable/src/scenegraph/scenegraph.ts | 15 +++++--- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts b/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts index ddf5d86c9d..9fc2643e5c 100644 --- a/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts +++ b/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts @@ -166,6 +166,41 @@ describe('listTable init test', () => { frozenTable.release(); }); + test('listTable bottom right corner should stay connected after short content', () => { + const shortColumns = columns.slice(0, 5).map(column => ({ + ...column, + width: 100 + })); + const optionWithBottomRightFrozen = { + ...option, + columns: shortColumns, + defaultColWidth: 100, + frozenColCount: 2, + frozenRowCount: 5, + rightFrozenColCount: 2, + bottomFrozenRowCount: 2, + containerFit: true, + container: createDiv(), + records: records.slice(0, 10) + }; + optionWithBottomRightFrozen.container.style.position = 'relative'; + optionWithBottomRightFrozen.container.style.width = '1000px'; + optionWithBottomRightFrozen.container.style.height = '800px'; + + const frozenTable = new ListTable(optionWithBottomRightFrozen); + const { scenegraph } = frozenTable; + + expect(scenegraph.rightFrozenGroup.attribute.x).toBe( + scenegraph.bodyGroup.attribute.x + scenegraph.bodyGroup.attribute.width + ); + expect(scenegraph.rightBottomCornerGroup.attribute.x).toBe(scenegraph.rightFrozenGroup.attribute.x); + expect(scenegraph.rightBottomCornerGroup.attribute.y).toBe( + scenegraph.bodyGroup.attribute.y + scenegraph.bodyGroup.attribute.height + ); + + frozenTable.release(); + }); + test('listTable should support decreasing rightFrozenColCount by setter with row series number', () => { const optionWithRightFrozen = { ...option, diff --git a/packages/vtable/src/scenegraph/scenegraph.ts b/packages/vtable/src/scenegraph/scenegraph.ts index 6c28b10fbc..7158672daa 100644 --- a/packages/vtable/src/scenegraph/scenegraph.ts +++ b/packages/vtable/src/scenegraph/scenegraph.ts @@ -1456,17 +1456,22 @@ export class Scenegraph { } if (this.table.rightFrozenColCount > 0) { - this.rightFrozenGroup.setAttribute('x', this.tableGroup.attribute.width - this.table.getRightFrozenColsWidth()); + const rightFrozenColsWidth = this.table.getRightFrozenColsWidth(); + const rightFrozenX = Math.min( + this.tableGroup.attribute.width - rightFrozenColsWidth, + this.bodyGroup.attribute.x + this.bodyGroup.attribute.width + ); + this.rightFrozenGroup.setAttribute('x', rightFrozenX); this.rightTopCornerGroup.setAttributes({ visible: true, - x: this.tableGroup.attribute.width - this.table.getRightFrozenColsWidth(), - width: this.table.getRightFrozenColsWidth(), + x: rightFrozenX, + width: rightFrozenColsWidth, height: this.table.getFrozenRowsHeight() }); this.rightBottomCornerGroup.setAttributes({ visible: true, - x: this.tableGroup.attribute.width - this.table.getRightFrozenColsWidth(), - width: this.table.getRightFrozenColsWidth() + x: rightFrozenX, + width: rightFrozenColsWidth }); } From a62d79e70e8d904deb78e2180dc37c5df90105d2 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Fri, 31 Jul 2026 11:16:07 +0800 Subject: [PATCH 66/77] fix(vtable): guard bottom right corner visibility --- .../__tests__/options/listTable-api-with-frozen.test.ts | 1 + packages/vtable/src/scenegraph/scenegraph.ts | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts b/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts index 9fc2643e5c..c2a052e9f1 100644 --- a/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts +++ b/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts @@ -162,6 +162,7 @@ describe('listTable init test', () => { expect(scenegraph.bottomFrozenGroup.attribute.y).toBe( scenegraph.bodyGroup.attribute.y + scenegraph.bodyGroup.attribute.height ); + expect(scenegraph.rightBottomCornerGroup.attribute.visible).toBe(false); frozenTable.release(); }); diff --git a/packages/vtable/src/scenegraph/scenegraph.ts b/packages/vtable/src/scenegraph/scenegraph.ts index 7158672daa..6e8f29373b 100644 --- a/packages/vtable/src/scenegraph/scenegraph.ts +++ b/packages/vtable/src/scenegraph/scenegraph.ts @@ -1449,8 +1449,10 @@ export class Scenegraph { width: this.table.getFrozenColsWidth() }); this.rightBottomCornerGroup.setAttributes({ - visible: true, + visible: this.table.rightFrozenColCount > 0, + x: 0, y: bottomFrozenY, + width: 0, height: bottomFrozenRowsHeight }); } From fcad11f70e141399a56d1fa4d5f20b6dd320dce0 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Fri, 31 Jul 2026 12:00:03 +0800 Subject: [PATCH 67/77] fix(vtable): hide bottom right corner without bottom frozen rows --- .../vtable/__tests__/options/listTable-api-with-frozen.test.ts | 1 + packages/vtable/src/scenegraph/scenegraph.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts b/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts index c2a052e9f1..a242a72292 100644 --- a/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts +++ b/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts @@ -224,5 +224,6 @@ describe('listTable init test', () => { rightFrozenTable.rightFrozenColCount = 1; }).not.toThrow(); expect(rightFrozenTable.rightFrozenColCount).toBe(1); + expect(rightFrozenTable.scenegraph.rightBottomCornerGroup.attribute.visible).toBe(false); }); }); diff --git a/packages/vtable/src/scenegraph/scenegraph.ts b/packages/vtable/src/scenegraph/scenegraph.ts index 6e8f29373b..58b4a387d2 100644 --- a/packages/vtable/src/scenegraph/scenegraph.ts +++ b/packages/vtable/src/scenegraph/scenegraph.ts @@ -1471,7 +1471,7 @@ export class Scenegraph { height: this.table.getFrozenRowsHeight() }); this.rightBottomCornerGroup.setAttributes({ - visible: true, + visible: this.table.bottomFrozenRowCount > 0, x: rightFrozenX, width: rightFrozenColsWidth }); From a2dd44e7eef83ca08f0c004208a00edb553bd807 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Fri, 31 Jul 2026 13:08:13 +0800 Subject: [PATCH 68/77] fix(vtable): hide bottom left corner without frozen columns --- .../options/listTable-api-with-frozen.test.ts | 22 +++++++++++++++++++ packages/vtable/src/scenegraph/scenegraph.ts | 4 ++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts b/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts index a242a72292..2eef2ac6c8 100644 --- a/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts +++ b/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts @@ -167,6 +167,28 @@ describe('listTable init test', () => { frozenTable.release(); }); + test('listTable bottom left corner should be hidden without frozen columns', () => { + const optionWithOnlyBottomFrozenRows = { + ...option, + frozenColCount: 0, + rightFrozenColCount: 0, + bottomFrozenRowCount: 2, + container: createDiv(), + records: records.slice(0, 10) + }; + optionWithOnlyBottomFrozenRows.container.style.position = 'relative'; + optionWithOnlyBottomFrozenRows.container.style.width = '1000px'; + optionWithOnlyBottomFrozenRows.container.style.height = '800px'; + + const frozenTable = new ListTable(optionWithOnlyBottomFrozenRows); + + expect(frozenTable.scenegraph.leftBottomCornerGroup.attribute.visible).toBe(false); + expect(frozenTable.scenegraph.leftBottomCornerGroup.attribute.width).toBe(0); + expect(frozenTable.scenegraph.rightBottomCornerGroup.attribute.visible).toBe(false); + + frozenTable.release(); + }); + test('listTable bottom right corner should stay connected after short content', () => { const shortColumns = columns.slice(0, 5).map(column => ({ ...column, diff --git a/packages/vtable/src/scenegraph/scenegraph.ts b/packages/vtable/src/scenegraph/scenegraph.ts index 58b4a387d2..5ae6de3269 100644 --- a/packages/vtable/src/scenegraph/scenegraph.ts +++ b/packages/vtable/src/scenegraph/scenegraph.ts @@ -1443,10 +1443,10 @@ export class Scenegraph { ); this.bottomFrozenGroup.setAttribute('y', bottomFrozenY); this.leftBottomCornerGroup.setAttributes({ - visible: true, + visible: this.table.frozenColCount > 0, y: bottomFrozenY, height: bottomFrozenRowsHeight, - width: this.table.getFrozenColsWidth() + width: this.table.frozenColCount > 0 ? this.table.getFrozenColsWidth() : 0 }); this.rightBottomCornerGroup.setAttributes({ visible: this.table.rightFrozenColCount > 0, From 2d448729974665c73462756b521d19da3c4b2e26 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Fri, 31 Jul 2026 15:10:36 +0800 Subject: [PATCH 69/77] fix(vtable): reset frozen corner visibility --- .../options/listTable-api-with-frozen.test.ts | 56 +++++++++++++++++++ packages/vtable/src/scenegraph/scenegraph.ts | 43 ++++++++++---- 2 files changed, 89 insertions(+), 10 deletions(-) diff --git a/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts b/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts index 2eef2ac6c8..08f9ae1c9b 100644 --- a/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts +++ b/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts @@ -247,5 +247,61 @@ describe('listTable init test', () => { }).not.toThrow(); expect(rightFrozenTable.rightFrozenColCount).toBe(1); expect(rightFrozenTable.scenegraph.rightBottomCornerGroup.attribute.visible).toBe(false); + expect(rightFrozenTable.scenegraph.rightBottomCornerGroup.attribute.width).toBe(0); + }); + + test('listTable bottom corner groups should be reset after clearing bottom frozen rows', () => { + const optionWithBottomFrozenRows = { + ...option, + frozenColCount: 2, + rightFrozenColCount: 2, + bottomFrozenRowCount: 2, + container: createDiv(), + records: records.slice(0, 10) + }; + optionWithBottomFrozenRows.container.style.position = 'relative'; + optionWithBottomFrozenRows.container.style.width = '1000px'; + optionWithBottomFrozenRows.container.style.height = '800px'; + + const frozenTable = new ListTable(optionWithBottomFrozenRows); + + expect(frozenTable.scenegraph.leftBottomCornerGroup.attribute.visible).toBe(true); + expect(frozenTable.scenegraph.rightBottomCornerGroup.attribute.visible).toBe(true); + + frozenTable.bottomFrozenRowCount = 0; + + expect(frozenTable.scenegraph.leftBottomCornerGroup.attribute.visible).toBe(false); + expect(frozenTable.scenegraph.leftBottomCornerGroup.attribute.height).toBe(0); + expect(frozenTable.scenegraph.rightBottomCornerGroup.attribute.visible).toBe(false); + expect(frozenTable.scenegraph.rightBottomCornerGroup.attribute.width).toBe(0); + expect(frozenTable.scenegraph.rightBottomCornerGroup.attribute.height).toBe(0); + + frozenTable.release(); + }); + + test('listTable right top corner group should be reset after clearing frozen rows', () => { + const optionWithRightFrozenRows = { + ...option, + frozenRowCount: 5, + bottomFrozenRowCount: 0, + rightFrozenColCount: 2, + container: createDiv(), + records + }; + optionWithRightFrozenRows.container.style.position = 'relative'; + optionWithRightFrozenRows.container.style.width = '1000px'; + optionWithRightFrozenRows.container.style.height = '800px'; + + const frozenTable = new ListTable(optionWithRightFrozenRows); + + expect(frozenTable.scenegraph.rightTopCornerGroup.attribute.visible).toBe(true); + + frozenTable.frozenRowCount = 0; + + expect(frozenTable.scenegraph.rightTopCornerGroup.attribute.visible).toBe(false); + expect(frozenTable.scenegraph.rightTopCornerGroup.attribute.width).toBe(0); + expect(frozenTable.scenegraph.rightTopCornerGroup.attribute.height).toBe(0); + + frozenTable.release(); }); }); diff --git a/packages/vtable/src/scenegraph/scenegraph.ts b/packages/vtable/src/scenegraph/scenegraph.ts index 5ae6de3269..837fde2511 100644 --- a/packages/vtable/src/scenegraph/scenegraph.ts +++ b/packages/vtable/src/scenegraph/scenegraph.ts @@ -1435,7 +1435,12 @@ export class Scenegraph { } } - if (this.table.bottomFrozenRowCount > 0) { + const hasFrozenCols = this.table.frozenColCount > 0; + const hasRightFrozenCols = this.table.rightFrozenColCount > 0; + const hasFrozenRows = this.table.frozenRowCount > 0; + const hasBottomFrozenRows = this.table.bottomFrozenRowCount > 0; + + if (hasBottomFrozenRows) { const bottomFrozenRowsHeight = this.table.getBottomFrozenRowsHeight(); const bottomFrozenY = Math.min( this.tableGroup.attribute.height - bottomFrozenRowsHeight, @@ -1443,21 +1448,32 @@ export class Scenegraph { ); this.bottomFrozenGroup.setAttribute('y', bottomFrozenY); this.leftBottomCornerGroup.setAttributes({ - visible: this.table.frozenColCount > 0, + visible: hasFrozenCols, y: bottomFrozenY, height: bottomFrozenRowsHeight, - width: this.table.frozenColCount > 0 ? this.table.getFrozenColsWidth() : 0 + width: hasFrozenCols ? this.table.getFrozenColsWidth() : 0 }); this.rightBottomCornerGroup.setAttributes({ - visible: this.table.rightFrozenColCount > 0, + visible: hasRightFrozenCols, x: 0, y: bottomFrozenY, width: 0, height: bottomFrozenRowsHeight }); + } else { + this.leftBottomCornerGroup.setAttributes({ + visible: false, + width: 0, + height: 0 + }); + this.rightBottomCornerGroup.setAttributes({ + visible: false, + width: 0, + height: 0 + }); } - if (this.table.rightFrozenColCount > 0) { + if (hasRightFrozenCols) { const rightFrozenColsWidth = this.table.getRightFrozenColsWidth(); const rightFrozenX = Math.min( this.tableGroup.attribute.width - rightFrozenColsWidth, @@ -1465,15 +1481,22 @@ export class Scenegraph { ); this.rightFrozenGroup.setAttribute('x', rightFrozenX); this.rightTopCornerGroup.setAttributes({ - visible: true, + visible: hasFrozenRows, x: rightFrozenX, - width: rightFrozenColsWidth, - height: this.table.getFrozenRowsHeight() + width: hasFrozenRows ? rightFrozenColsWidth : 0, + height: hasFrozenRows ? this.table.getFrozenRowsHeight() : 0 }); this.rightBottomCornerGroup.setAttributes({ - visible: this.table.bottomFrozenRowCount > 0, + visible: hasBottomFrozenRows, x: rightFrozenX, - width: rightFrozenColsWidth + width: hasBottomFrozenRows ? rightFrozenColsWidth : 0, + height: hasBottomFrozenRows ? this.table.getBottomFrozenRowsHeight() : 0 + }); + } else { + this.rightTopCornerGroup.setAttributes({ + visible: false, + width: 0, + height: 0 }); } From 1980162bf9b2cd10144899446981945c5fd89919 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Fri, 31 Jul 2026 15:54:30 +0800 Subject: [PATCH 70/77] fix(react-vtable): lazy load custom layout reconciler --- packages/react-vtable/demo/src/App.tsx | 2 + .../list-table/issue-5203-vite-react19.tsx | 45 +++++++++++ .../demo/vite.config.issue5203.ts | 38 ++++++++++ .../table-components/custom/custom-layout.tsx | 75 ++++++++++++++++--- 4 files changed, 149 insertions(+), 11 deletions(-) create mode 100644 packages/react-vtable/demo/src/list-table/issue-5203-vite-react19.tsx create mode 100644 packages/react-vtable/demo/vite.config.issue5203.ts diff --git a/packages/react-vtable/demo/src/App.tsx b/packages/react-vtable/demo/src/App.tsx index 50c12c4cee..d4c9a43699 100644 --- a/packages/react-vtable/demo/src/App.tsx +++ b/packages/react-vtable/demo/src/App.tsx @@ -1,4 +1,5 @@ import listTable from './list-table/list-table'; +import issue5203ViteReact19 from './list-table/issue-5203-vite-react19'; import listOptionRecord from './list-table/list-option-records'; import listComponent from './list-table/list-component'; import listCustomLayout from './list-table/list-custom-layout'; @@ -32,6 +33,7 @@ import { Component, useEffect, useMemo, useState } from 'react'; declare const globalThis: any; const demoList = [ + { key: 'issue5203ViteReact19', Comp: issue5203ViteReact19 }, { key: 'listTable', Comp: listTable }, { key: 'listEditor', Comp: listEditor }, { key: 'listOptionRecord', Comp: listOptionRecord }, diff --git a/packages/react-vtable/demo/src/list-table/issue-5203-vite-react19.tsx b/packages/react-vtable/demo/src/list-table/issue-5203-vite-react19.tsx new file mode 100644 index 0000000000..4686cfa513 --- /dev/null +++ b/packages/react-vtable/demo/src/list-table/issue-5203-vite-react19.tsx @@ -0,0 +1,45 @@ +import { ListTable } from '../../../src'; + +declare const globalThis: any; + +const columns = [ + { + field: 'id', + title: 'ID', + width: 120 + }, + { + field: 'name', + title: 'Name', + width: 200 + }, + { + field: 'age', + title: 'Age', + width: 120 + } +]; + +const records = [ + { id: 1, name: 'Alice', age: 28 }, + { id: 2, name: 'Bob', age: 31 }, + { id: 3, name: 'Carol', age: 24 } +]; + +export default function Issue5203ViteReact19() { + return ( + { + (globalThis as any).tableInstance = table; + (globalThis as any).__issue5203Ready = true; + }} + onError={error => { + (globalThis as any).__issue5203Error = error; + }} + /> + ); +} diff --git a/packages/react-vtable/demo/vite.config.issue5203.ts b/packages/react-vtable/demo/vite.config.issue5203.ts new file mode 100644 index 0000000000..ad5ee89152 --- /dev/null +++ b/packages/react-vtable/demo/vite.config.issue5203.ts @@ -0,0 +1,38 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { createRequire } from 'module'; + +const require = createRequire(import.meta.url); +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const react19Root = path.resolve(__dirname, '../../../.react19-deps/node_modules'); + +export default defineConfig({ + plugins: [react({ fastRefresh: false })] as any, + define: { + __DEV__: true, + __VERSION__: JSON.stringify(require('../../vtable/package.json').version) + }, + server: { + host: '0.0.0.0', + port: 3102 + }, + resolve: { + alias: [ + { find: /^react$/, replacement: path.resolve(react19Root, 'react/index.js') }, + { find: /^react\/jsx-runtime(\.js)?$/, replacement: path.resolve(react19Root, 'react/jsx-runtime.js') }, + { find: /^react\/jsx-dev-runtime(\.js)?$/, replacement: path.resolve(react19Root, 'react/jsx-dev-runtime.js') }, + { find: /^react-dom$/, replacement: path.resolve(react19Root, 'react-dom/index.js') }, + { find: /^react-dom\/client(\.js)?$/, replacement: path.resolve(react19Root, 'react-dom/client.js') }, + { find: '@visactor/vtable/es/vrender', replacement: path.resolve(__dirname, '../../vtable/src/vrender.ts') }, + { find: '@visactor/vtable/es', replacement: path.resolve(__dirname, '../../vtable/src/') }, + { find: '@visactor/vtable', replacement: path.resolve(__dirname, '../../vtable/src/index.ts') }, + { find: '@visactor/vtable-plugins', replacement: path.resolve(__dirname, '../../vtable-plugins/src/index.ts') }, + { find: '@src', replacement: path.resolve(__dirname, '../../vtable/src/') }, + { find: '@vutils-extension', replacement: path.resolve(__dirname, '../../vtable/src/vutil-extension-temp') } + ] + } +}); diff --git a/packages/react-vtable/src/table-components/custom/custom-layout.tsx b/packages/react-vtable/src/table-components/custom/custom-layout.tsx index 1bf72f7dae..9731ded216 100644 --- a/packages/react-vtable/src/table-components/custom/custom-layout.tsx +++ b/packages/react-vtable/src/table-components/custom/custom-layout.tsx @@ -1,12 +1,11 @@ /* eslint-disable react-hooks/rules-of-hooks */ import type { PropsWithChildren, ReactElement } from 'react'; -import React, { isValidElement, useCallback, useContext, useLayoutEffect, useRef } from 'react'; +import React, { isValidElement, useCallback, useContext, useLayoutEffect, useRef, useState } from 'react'; import RootTableContext from '../../context/table'; import { Group } from '@visactor/vtable/es/vrender'; import type { ICustomLayoutFuc, CustomRenderFunctionArg } from '@visactor/vtable/es/ts-types'; import type { FiberRoot } from 'react-reconciler'; -import type { ReconcilerErrorReporter } from './reconciler'; -import { reconcilor, createReconcilerContainer } from './reconciler'; +import type { ReconcilerErrorReporter, ReconcilerErrorType } from './reconciler'; type CustomLayoutProps = { componentId: string }; @@ -22,6 +21,8 @@ export const CustomLayout: React.FC = (props: PropsWithChildr } const context = useContext(RootTableContext); const { table, onError } = context; + const [reconcilerReady, setReconcilerReady] = useState(false); + const reconcilerModule = useRef(null); const isHeaderCustomLayout = children.props.role === 'header-custom-layout'; @@ -45,22 +46,48 @@ export const CustomLayout: React.FC = (props: PropsWithChildr [onError] ); + useLayoutEffect(() => { + let released = false; + // Load the custom-layout reconciler only when CustomLayout is actually used. + import('./reconciler') + .then(module => { + if (released) { + return; + } + reconcilerModule.current = module; + setReconcilerReady(true); + }) + .catch(error => { + reportReconcilerError('uncaught', error); + }); + return () => { + released = true; + }; + }, [reportReconcilerError]); + // customLayout function for vtable const createGraphic: ICustomLayoutFuc = useCallback( - args => { + (args: any) => { + const module = reconcilerModule.current; + if (!module) { + return { + rootContainer: new Group({}), + renderDefault: !!children.props.renderDefault + }; + } const key = `${args.originCol ?? args.col}-${args.originRow ?? args.row}${ args.forComputation ? '-forComputation' : '' }`; let group; if (container.current.has(key)) { const currentContainer = container.current.get(key); - reconcilorUpdateContainer(children, currentContainer, args); + reconcilorUpdateContainer(module, children, currentContainer, args); group = currentContainer.containerInfo; } else { group = new Group({}); - const currentContainer = createReconcilerContainer(group as any, 'custom', reportReconcilerError); + const currentContainer = module.createReconcilerContainer(group as any, 'custom', reportReconcilerError); container.current.set(key, currentContainer); - reconcilorUpdateContainer(children, currentContainer, args); + reconcilorUpdateContainer(module, children, currentContainer, args); } return { @@ -72,10 +99,14 @@ export const CustomLayout: React.FC = (props: PropsWithChildr ); const removeContainer = useCallback((col: number, row: number) => { + const module = reconcilerModule.current; + if (!module) { + return; + } const key = `${col}-${row}`; if (container.current.has(key)) { const currentContainer = container.current.get(key); - reconcilor.updateContainer(null, currentContainer, null); + module.reconcilor.updateContainer(null, currentContainer, null); // group = currentContainer.containerInfo; currentContainer.containerInfo.delete(); container.current.delete(key); @@ -83,9 +114,14 @@ export const CustomLayout: React.FC = (props: PropsWithChildr }, []); const removeAllContainer = useCallback(() => { + const module = reconcilerModule.current; + if (!module) { + container.current.clear(); + return; + } container.current.forEach((value, key) => { const currentContainer = value; - reconcilor.updateContainer(null, currentContainer, null); + module.reconcilor.updateContainer(null, currentContainer, null); currentContainer.containerInfo.delete(); }); container.current.clear(); @@ -108,6 +144,9 @@ export const CustomLayout: React.FC = (props: PropsWithChildr // eslint-disable-next-line no-undef console.log('update props', props, table); + if (!reconcilerReady) { + return; + } table?.checkReactCustomLayout(); // init reactCustomLayout component table?.reactCustomLayout?.setReactRemoveAllGraphic(componentId, removeAllContainer, isHeaderCustomLayout); // set customLayout function @@ -129,6 +168,10 @@ export const CustomLayout: React.FC = (props: PropsWithChildr ); // update customLayout function // update all container container.current.forEach((value, key) => { + const module = reconcilerModule.current; + if (!module) { + return; + } const [col, row] = key.split('-').map(Number); // const width = table.getColWidth(col); // to be fixed: may be merge cell // const height = table.getRowHeight(row); // to be fixed: may be merge cell @@ -151,7 +194,7 @@ export const CustomLayout: React.FC = (props: PropsWithChildr }; // update element in container const group = currentContainer.containerInfo; - reconcilorUpdateContainer(children, currentContainer, args); + reconcilorUpdateContainer(module, children, currentContainer, args); // reconcilor.updateContainer(React.cloneElement(children, { ...args }), currentContainer, null); table.scenegraph.updateNextFrame(); }); @@ -161,8 +204,18 @@ export const CustomLayout: React.FC = (props: PropsWithChildr return null; }; -function reconcilorUpdateContainer(children: ReactElement, currentContainer: any, args: any) { +type ReconcilerModule = { + reconcilor: any; + createReconcilerContainer: ( + container: any, + identifierPrefix?: string, + reportError?: (type: ReconcilerErrorType, error: unknown) => void + ) => FiberRoot; +}; + +function reconcilorUpdateContainer(module: ReconcilerModule, children: ReactElement, currentContainer: any, args: any) { const element = React.cloneElement(children, { ...args }); + const { reconcilor } = module; const updateContainerSync = (reconcilor as any).updateContainerSync; if (typeof updateContainerSync === 'function') { updateContainerSync(element, currentContainer, null); From 4469bce03fc406216c0b112c3e60606c58747d4f Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Fri, 31 Jul 2026 15:55:09 +0800 Subject: [PATCH 71/77] fix(vtable): keep right frozen header at right edge --- .../options/listTable-api-with-frozen.test.ts | 24 +++++++++++++++++++ packages/vtable/src/scenegraph/scenegraph.ts | 8 ++++--- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts b/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts index 08f9ae1c9b..7fe91e2b50 100644 --- a/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts +++ b/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts @@ -224,6 +224,30 @@ describe('listTable init test', () => { frozenTable.release(); }); + test('listTable right frozen header should stay on the right side without records', () => { + const optionWithRightFrozenHeader = { + ...option, + frozenColCount: 2, + rightFrozenColCount: 2, + bottomFrozenRowCount: 0, + container: createDiv(), + records: [] + }; + optionWithRightFrozenHeader.container.style.position = 'relative'; + optionWithRightFrozenHeader.container.style.width = '1000px'; + optionWithRightFrozenHeader.container.style.height = '800px'; + + const frozenTable = new ListTable(optionWithRightFrozenHeader); + const { scenegraph } = frozenTable; + const rightFrozenColsWidth = frozenTable.getRightFrozenColsWidth(); + + expect(scenegraph.rightTopCornerGroup.attribute.visible).toBe(true); + expect(scenegraph.rightFrozenGroup.attribute.x).toBe(scenegraph.tableGroup.attribute.width - rightFrozenColsWidth); + expect(scenegraph.rightTopCornerGroup.attribute.x).toBe(scenegraph.rightFrozenGroup.attribute.x); + + frozenTable.release(); + }); + test('listTable should support decreasing rightFrozenColCount by setter with row series number', () => { const optionWithRightFrozen = { ...option, diff --git a/packages/vtable/src/scenegraph/scenegraph.ts b/packages/vtable/src/scenegraph/scenegraph.ts index 837fde2511..83e2609cb2 100644 --- a/packages/vtable/src/scenegraph/scenegraph.ts +++ b/packages/vtable/src/scenegraph/scenegraph.ts @@ -1475,10 +1475,12 @@ export class Scenegraph { if (hasRightFrozenCols) { const rightFrozenColsWidth = this.table.getRightFrozenColsWidth(); - const rightFrozenX = Math.min( - this.tableGroup.attribute.width - rightFrozenColsWidth, - this.bodyGroup.attribute.x + this.bodyGroup.attribute.width + const middleContentRight = Math.max( + this.colHeaderGroup.attribute.x + this.colHeaderGroup.attribute.width, + this.bodyGroup.attribute.x + this.bodyGroup.attribute.width, + this.bottomFrozenGroup.attribute.x + this.bottomFrozenGroup.attribute.width ); + const rightFrozenX = Math.min(this.tableGroup.attribute.width - rightFrozenColsWidth, middleContentRight); this.rightFrozenGroup.setAttribute('x', rightFrozenX); this.rightTopCornerGroup.setAttributes({ visible: hasFrozenRows, From a4c11f42e569c08a264de104d81a3ccc279f2211 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Fri, 31 Jul 2026 18:01:58 +0800 Subject: [PATCH 72/77] fix(vtable): place bottom frozen rows below header --- .../options/listTable-api-with-frozen.test.ts | 32 +++++++++++++++++++ packages/vtable/src/scenegraph/scenegraph.ts | 12 +++++-- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts b/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts index 7fe91e2b50..41a9048c06 100644 --- a/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts +++ b/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts @@ -248,6 +248,38 @@ describe('listTable init test', () => { frozenTable.release(); }); + test('listTable bottom corner rows should stay below header when body is empty', () => { + const twoColumns = columns.slice(0, 2).map(column => ({ + ...column, + width: 150 + })); + const optionWithOnlyCornerRows = { + ...option, + columns: twoColumns, + frozenColCount: 1, + rightFrozenColCount: 1, + bottomFrozenRowCount: 1, + container: createDiv(), + records: records.slice(0, 1) + }; + optionWithOnlyCornerRows.container.style.position = 'relative'; + optionWithOnlyCornerRows.container.style.width = '1000px'; + optionWithOnlyCornerRows.container.style.height = '800px'; + + const frozenTable = new ListTable(optionWithOnlyCornerRows); + const { scenegraph } = frozenTable; + const headerBottom = Math.max( + scenegraph.cornerHeaderGroup.attribute.y + scenegraph.cornerHeaderGroup.attribute.height, + scenegraph.rightTopCornerGroup.attribute.y + scenegraph.rightTopCornerGroup.attribute.height + ); + + expect(scenegraph.bodyGroup.attribute.height).toBe(0); + expect(scenegraph.leftBottomCornerGroup.attribute.y).toBe(headerBottom); + expect(scenegraph.rightBottomCornerGroup.attribute.y).toBe(headerBottom); + + frozenTable.release(); + }); + test('listTable should support decreasing rightFrozenColCount by setter with row series number', () => { const optionWithRightFrozen = { ...option, diff --git a/packages/vtable/src/scenegraph/scenegraph.ts b/packages/vtable/src/scenegraph/scenegraph.ts index 83e2609cb2..06335fd25e 100644 --- a/packages/vtable/src/scenegraph/scenegraph.ts +++ b/packages/vtable/src/scenegraph/scenegraph.ts @@ -1442,10 +1442,16 @@ export class Scenegraph { if (hasBottomFrozenRows) { const bottomFrozenRowsHeight = this.table.getBottomFrozenRowsHeight(); - const bottomFrozenY = Math.min( - this.tableGroup.attribute.height - bottomFrozenRowsHeight, - this.bodyGroup.attribute.y + this.bodyGroup.attribute.height + const topFrozenBottom = Math.max( + this.colHeaderGroup.attribute.y + this.colHeaderGroup.attribute.height, + this.cornerHeaderGroup.attribute.y + this.cornerHeaderGroup.attribute.height, + this.rightTopCornerGroup.attribute.y + this.rightTopCornerGroup.attribute.height ); + const middleContentBottom = Math.max( + this.bodyGroup.attribute.y + this.bodyGroup.attribute.height, + topFrozenBottom + ); + const bottomFrozenY = Math.min(this.tableGroup.attribute.height - bottomFrozenRowsHeight, middleContentBottom); this.bottomFrozenGroup.setAttribute('y', bottomFrozenY); this.leftBottomCornerGroup.setAttributes({ visible: hasFrozenCols, From fedb4d834e525b16e0a05aa99f7a207c0f99b63a Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Fri, 31 Jul 2026 18:53:02 +0800 Subject: [PATCH 73/77] fix(vtable): place bottom frozen rows below right frozen body --- .../options/listTable-api-with-frozen.test.ts | 25 +++++++++++++++++++ packages/vtable/src/scenegraph/scenegraph.ts | 2 ++ 2 files changed, 27 insertions(+) diff --git a/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts b/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts index 41a9048c06..c6081a0e07 100644 --- a/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts +++ b/packages/vtable/__tests__/options/listTable-api-with-frozen.test.ts @@ -280,6 +280,31 @@ describe('listTable init test', () => { frozenTable.release(); }); + test('listTable bottom corner rows should stay below right frozen body when only right frozen column exists', () => { + const optionWithOnlyRightFrozenColumn = { + ...option, + columns: columns.slice(0, 1), + frozenColCount: 0, + rightFrozenColCount: 1, + bottomFrozenRowCount: 3, + container: createDiv(), + records: records.slice(0, 4) + }; + optionWithOnlyRightFrozenColumn.container.style.position = 'relative'; + optionWithOnlyRightFrozenColumn.container.style.width = '1000px'; + optionWithOnlyRightFrozenColumn.container.style.height = '800px'; + + const frozenTable = new ListTable(optionWithOnlyRightFrozenColumn); + const { scenegraph } = frozenTable; + const rightFrozenBottom = scenegraph.rightFrozenGroup.attribute.y + scenegraph.rightFrozenGroup.attribute.height; + + expect(scenegraph.bodyGroup.attribute.height).toBe(0); + expect(scenegraph.rightFrozenGroup.attribute.height).toBeGreaterThan(0); + expect(scenegraph.rightBottomCornerGroup.attribute.y).toBe(rightFrozenBottom); + + frozenTable.release(); + }); + test('listTable should support decreasing rightFrozenColCount by setter with row series number', () => { const optionWithRightFrozen = { ...option, diff --git a/packages/vtable/src/scenegraph/scenegraph.ts b/packages/vtable/src/scenegraph/scenegraph.ts index 06335fd25e..f3dc9a32a8 100644 --- a/packages/vtable/src/scenegraph/scenegraph.ts +++ b/packages/vtable/src/scenegraph/scenegraph.ts @@ -1448,7 +1448,9 @@ export class Scenegraph { this.rightTopCornerGroup.attribute.y + this.rightTopCornerGroup.attribute.height ); const middleContentBottom = Math.max( + this.rowHeaderGroup.attribute.y + this.rowHeaderGroup.attribute.height, this.bodyGroup.attribute.y + this.bodyGroup.attribute.height, + this.rightFrozenGroup.attribute.y + this.rightFrozenGroup.attribute.height, topFrozenBottom ); const bottomFrozenY = Math.min(this.tableGroup.attribute.height - bottomFrozenRowsHeight, middleContentBottom); From 9b509faaa29621b16c68071c39599854b7c59eef Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Mon, 3 Aug 2026 15:45:26 +0800 Subject: [PATCH 74/77] fix(vtable): fallback invalid functional cell type --- .../listTable-celltype-function.test.ts | 40 +++++++++++++++++++ .../issue-celltype-function-undefined.ts | 34 ++++++++++++++++ packages/vtable/examples/menu.ts | 4 ++ packages/vtable/src/core/BaseTable.ts | 10 +++-- 4 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 packages/vtable/__tests__/options/listTable-celltype-function.test.ts create mode 100644 packages/vtable/examples/debug/issue-celltype-function-undefined.ts diff --git a/packages/vtable/__tests__/options/listTable-celltype-function.test.ts b/packages/vtable/__tests__/options/listTable-celltype-function.test.ts new file mode 100644 index 0000000000..a20a51888d --- /dev/null +++ b/packages/vtable/__tests__/options/listTable-celltype-function.test.ts @@ -0,0 +1,40 @@ +import { ListTable } from '../../src'; +import { createDiv, removeDom } from '../dom'; + +(global as any).__VERSION__ = 'none'; + +describe('ListTable cellType function', () => { + let containerDom: HTMLElement; + + beforeEach(() => { + containerDom = createDiv(); + containerDom.style.position = 'relative'; + containerDom.style.width = '300px'; + containerDom.style.height = '200px'; + }); + + afterEach(() => { + removeDom(containerDom); + }); + + test('falls back to text when cellType function returns undefined', () => { + const table = new ListTable(containerDom, { + records: [{ name: 'A' }], + columns: [ + { + field: 'name', + title: 'Name', + cellType: ((): any => undefined) as any + } + ] + }); + + const bodyRow = table.columnHeaderLevelCount; + + expect(table.getCellType(0, bodyRow)).toBe('text'); + expect(table.getBodyColumnType(0, bodyRow)).toBe('text'); + expect(table.getCellValue(0, bodyRow)).toBe('A'); + + table.release(); + }); +}); diff --git a/packages/vtable/examples/debug/issue-celltype-function-undefined.ts b/packages/vtable/examples/debug/issue-celltype-function-undefined.ts new file mode 100644 index 0000000000..37475b9f16 --- /dev/null +++ b/packages/vtable/examples/debug/issue-celltype-function-undefined.ts @@ -0,0 +1,34 @@ +import * as VTable from '../../src'; + +const CONTAINER_ID = 'vTable'; + +export function createTable() { + const container = document.getElementById(CONTAINER_ID)!; + const status = document.createElement('div'); + status.id = 'issueCellTypeUndefinedStatus'; + status.style.cssText = 'height: 32px; line-height: 32px; font-size: 13px; color: #333;'; + container.parentElement?.insertBefore(status, container); + + const option: VTable.ListTableConstructorOptions = { + records: [{ name: 'A' }], + columns: [ + { + field: 'name', + title: 'Name', + cellType: () => undefined + } + ] + }; + + try { + const tableInstance = new VTable.ListTable(container, option); + (window as any).tableInstance = tableInstance; + const cellType = tableInstance.getCellType(0, tableInstance.columnHeaderLevelCount); + status.textContent = `PASS | cellType=${cellType}, value=${tableInstance.getCellValue( + 0, + tableInstance.columnHeaderLevelCount + )}`; + } catch (err) { + status.textContent = `FAIL | ${(err as Error).message}`; + } +} diff --git a/packages/vtable/examples/menu.ts b/packages/vtable/examples/menu.ts index 99ce817057..6fcdf4ccf7 100644 --- a/packages/vtable/examples/menu.ts +++ b/packages/vtable/examples/menu.ts @@ -82,6 +82,10 @@ export const menus = [ path: 'debug', name: 'issue-4798-sort-icon-visible-time' }, + { + path: 'debug', + name: 'issue-celltype-function-undefined' + }, { path: 'debug', name: 'header-frame-border-null-color' diff --git a/packages/vtable/src/core/BaseTable.ts b/packages/vtable/src/core/BaseTable.ts index 587f126314..31c4ed898c 100644 --- a/packages/vtable/src/core/BaseTable.ts +++ b/packages/vtable/src/core/BaseTable.ts @@ -168,6 +168,10 @@ const { isTouchEvent } = event; const rangeReg = /^\$(\d+)\$(\d+)$/; importStyle(); +function normalizeCellType(cellType: ColumnTypeOption | undefined | null): ColumnTypeOption { + return isValid(cellType) ? cellType : 'text'; +} + export abstract class BaseTable extends EventTarget implements BaseTableAPI { internalProps: IBaseTableProtected; showFrozenIcon = true; @@ -3838,7 +3842,7 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { getBodyColumnType(col: number, row: number): ColumnTypeOption { const cellType = this.internalProps.layoutMap.getBody(col, row)?.cellType ?? 'text'; - return getProp('cellType', { cellType }, col, row, this); + return normalizeCellType(getProp('cellType', { cellType }, col, row, this)); } getCellType(col: number, row: number): ColumnTypeOption { @@ -3848,13 +3852,13 @@ export abstract class BaseTable extends EventTarget implements BaseTableAPI { col, row ).cellType; - return seriesHeaderCellType === 'radio' ? 'text' : seriesHeaderCellType; + return normalizeCellType(seriesHeaderCellType === 'radio' ? 'text' : seriesHeaderCellType); } else if (this.isHeader(col, row)) { cellType = (this.internalProps.layoutMap.getHeader(col, row) as HeaderData).headerType; } else { cellType = this.internalProps.layoutMap.getBody(col, row).cellType; } - return getProp('cellType', { cellType }, col, row, this); + return normalizeCellType(getProp('cellType', { cellType }, col, row, this)); } /** From a823fbfdf32076c22a4c0023e7f29810ef209043 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Mon, 3 Aug 2026 18:45:31 +0800 Subject: [PATCH 75/77] fix(bundler): inline dynamic imports for umd output --- tools/bundler/src/tasks/umd.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/bundler/src/tasks/umd.ts b/tools/bundler/src/tasks/umd.ts index a45f6e4213..b4a4f04c8b 100644 --- a/tools/bundler/src/tasks/umd.ts +++ b/tools/bundler/src/tasks/umd.ts @@ -34,6 +34,7 @@ export async function buildUmd(config: Config, projectRoot: string, rawPackageJs file: minify ? `${dest}/${config.umdOutputFilename || packageNameToPath(rawPackageJson.name)}.min.js` : `${dest}/${config.umdOutputFilename || packageNameToPath(rawPackageJson.name)}.js`, + inlineDynamicImports: true, exports: 'named', globals: { react: 'React', ...config.globals } } From ecd42737ed43e8db6b5e0ef3c62b5a0ab950f40e Mon Sep 17 00:00:00 2001 From: fangsmile Date: Mon, 3 Aug 2026 11:32:08 +0000 Subject: [PATCH 76/77] build: release version 1.26.6 [skip ci] --- ...pdate-records-render_2026-07-27-19-55.json | 11 ---- ...unctional-icon-theme_2026-07-27-15-36.json | 11 ---- ...gation-series-number_2026-07-14-20-50.json | 11 ---- ...delete-selected-rows_2026-07-14-21-21.json | 11 ---- ...sue-5215-plugin-init_2026-07-28-00-00.json | 11 ---- ...heckbox-record-index_2026-07-28-00-00.json | 11 ---- common/config/rush/pnpm-lock.yaml | 58 +++++++++---------- common/config/rush/version-policies.json | 2 +- docs/assets/changelog/en/release.md | 17 ++++++ docs/assets/changelog/zh/release.md | 17 ++++++ docs/package.json | 18 +++--- packages/openinula-vtable/package.json | 4 +- packages/react-vtable/package.json | 4 +- packages/vtable-calendar/package.json | 4 +- packages/vtable-editors/package.json | 2 +- packages/vtable-export/package.json | 4 +- packages/vtable-gantt/package.json | 6 +- packages/vtable-plugins/package.json | 8 +-- packages/vtable-search/package.json | 4 +- packages/vtable-sheet/package.json | 8 +-- packages/vtable/CHANGELOG.json | 29 ++++++++++ packages/vtable/CHANGELOG.md | 17 +++++- packages/vtable/package.json | 4 +- packages/vue-vtable/package.json | 4 +- tools/bugserver-trigger/package.json | 10 ++-- 25 files changed, 149 insertions(+), 137 deletions(-) delete mode 100644 common/changes/@visactor/vtable/fix-issue-4761-update-records-render_2026-07-27-19-55.json delete mode 100644 common/changes/@visactor/vtable/fix-issue-4816-functional-icon-theme_2026-07-27-15-36.json delete mode 100644 common/changes/@visactor/vtable/fix-issue-5213-aggregation-series-number_2026-07-14-20-50.json delete mode 100644 common/changes/@visactor/vtable/fix-issue-5214-delete-selected-rows_2026-07-14-21-21.json delete mode 100644 common/changes/@visactor/vtable/fix-issue-5215-plugin-init_2026-07-28-00-00.json delete mode 100644 common/changes/@visactor/vtable/fix-issues-5211-checkbox-record-index_2026-07-28-00-00.json diff --git a/common/changes/@visactor/vtable/fix-issue-4761-update-records-render_2026-07-27-19-55.json b/common/changes/@visactor/vtable/fix-issue-4761-update-records-render_2026-07-27-19-55.json deleted file mode 100644 index e61242adf9..0000000000 --- a/common/changes/@visactor/vtable/fix-issue-4761-update-records-render_2026-07-27-19-55.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "changes": [ - { - "packageName": "@visactor/vtable", - "comment": "fix: refresh rows when updateRecords is called without record indexes", - "type": "patch" - } - ], - "packageName": "@visactor/vtable", - "email": "892739385@qq.com" -} diff --git a/common/changes/@visactor/vtable/fix-issue-4816-functional-icon-theme_2026-07-27-15-36.json b/common/changes/@visactor/vtable/fix-issue-4816-functional-icon-theme_2026-07-27-15-36.json deleted file mode 100644 index 0f68b64d76..0000000000 --- a/common/changes/@visactor/vtable/fix-issue-4816-functional-icon-theme_2026-07-27-15-36.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "changes": [ - { - "comment": "fix: refresh functional icons after theme updates", - "type": "patch", - "packageName": "@visactor/vtable" - } - ], - "packageName": "@visactor/vtable", - "email": "biukam.w@gmail.com" -} diff --git a/common/changes/@visactor/vtable/fix-issue-5213-aggregation-series-number_2026-07-14-20-50.json b/common/changes/@visactor/vtable/fix-issue-5213-aggregation-series-number_2026-07-14-20-50.json deleted file mode 100644 index 13a5b655a4..0000000000 --- a/common/changes/@visactor/vtable/fix-issue-5213-aggregation-series-number_2026-07-14-20-50.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "changes": [ - { - "comment": "fix: hide row series number in aggregation rows", - "type": "none", - "packageName": "@visactor/vtable" - } - ], - "packageName": "@visactor/vtable", - "email": "biukam.w@gmail.com" -} \ No newline at end of file diff --git a/common/changes/@visactor/vtable/fix-issue-5214-delete-selected-rows_2026-07-14-21-21.json b/common/changes/@visactor/vtable/fix-issue-5214-delete-selected-rows_2026-07-14-21-21.json deleted file mode 100644 index 3857992a67..0000000000 --- a/common/changes/@visactor/vtable/fix-issue-5214-delete-selected-rows_2026-07-14-21-21.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "changes": [ - { - "comment": "fix: delete reverse-selected rows from context menu", - "type": "none", - "packageName": "@visactor/vtable" - } - ], - "packageName": "@visactor/vtable", - "email": "biukam.w@gmail.com" -} \ No newline at end of file diff --git a/common/changes/@visactor/vtable/fix-issue-5215-plugin-init_2026-07-28-00-00.json b/common/changes/@visactor/vtable/fix-issue-5215-plugin-init_2026-07-28-00-00.json deleted file mode 100644 index 674f58f075..0000000000 --- a/common/changes/@visactor/vtable/fix-issue-5215-plugin-init_2026-07-28-00-00.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "changes": [ - { - "comment": "fix: allow plugins to initialize before first render", - "type": "patch", - "packageName": "@visactor/vtable" - } - ], - "packageName": "@visactor/vtable", - "email": "github@visactor.io" -} diff --git a/common/changes/@visactor/vtable/fix-issues-5211-checkbox-record-index_2026-07-28-00-00.json b/common/changes/@visactor/vtable/fix-issues-5211-checkbox-record-index_2026-07-28-00-00.json deleted file mode 100644 index bf7b04563f..0000000000 --- a/common/changes/@visactor/vtable/fix-issues-5211-checkbox-record-index_2026-07-28-00-00.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "changes": [ - { - "comment": "fix: support updating checkbox state by record index", - "type": "patch", - "packageName": "@visactor/vtable" - } - ], - "packageName": "@visactor/vtable", - "email": "github@visactor.io" -} diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index e7322bd7a6..b36bdbfeb9 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -27,34 +27,34 @@ importers: specifier: ^2.11.0 version: 2.57.0(vue@3.5.31(typescript@4.9.5)) '@visactor/openinula-vtable': - specifier: workspace:1.26.5 + specifier: workspace:1.26.6 version: link:../packages/openinula-vtable '@visactor/react-vtable': - specifier: workspace:1.26.5 + specifier: workspace:1.26.6 version: link:../packages/react-vtable '@visactor/vchart': specifier: 2.1.3 version: 2.1.3 '@visactor/vtable': - specifier: workspace:1.26.5 + specifier: workspace:1.26.6 version: link:../packages/vtable '@visactor/vtable-editors': - specifier: workspace:1.26.5 + specifier: workspace:1.26.6 version: link:../packages/vtable-editors '@visactor/vtable-export': - specifier: workspace:1.26.5 + specifier: workspace:1.26.6 version: link:../packages/vtable-export '@visactor/vtable-gantt': - specifier: workspace:1.26.5 + specifier: workspace:1.26.6 version: link:../packages/vtable-gantt '@visactor/vtable-search': - specifier: workspace:1.26.5 + specifier: workspace:1.26.6 version: link:../packages/vtable-search '@visactor/vtable-sheet': - specifier: workspace:1.26.5 + specifier: workspace:1.26.6 version: link:../packages/vtable-sheet '@visactor/vue-vtable': - specifier: workspace:1.26.5 + specifier: workspace:1.26.6 version: link:../packages/vue-vtable '@visactor/vutils': specifier: ~1.0.17 @@ -148,7 +148,7 @@ importers: ../../packages/openinula-vtable: dependencies: '@visactor/vtable': - specifier: workspace:1.26.5 + specifier: workspace:1.26.6 version: link:../vtable '@visactor/vutils': specifier: ~1.0.17 @@ -278,7 +278,7 @@ importers: ../../packages/react-vtable: dependencies: '@visactor/vtable': - specifier: workspace:1.26.5 + specifier: workspace:1.26.6 version: link:../vtable '@visactor/vutils': specifier: ~1.0.17 @@ -456,7 +456,7 @@ importers: specifier: ~1.0.17 version: 1.0.23 '@visactor/vtable-editors': - specifier: workspace:1.26.5 + specifier: workspace:1.26.6 version: link:../vtable-editors '@visactor/vutils': specifier: ~1.0.17 @@ -637,7 +637,7 @@ importers: ../../packages/vtable-calendar: dependencies: '@visactor/vtable': - specifier: workspace:1.26.5 + specifier: workspace:1.26.6 version: link:../vtable '@visactor/vutils': specifier: ~1.0.17 @@ -860,7 +860,7 @@ importers: specifier: 2.0.7 version: 2.0.7 '@visactor/vtable': - specifier: workspace:1.26.5 + specifier: workspace:1.26.6 version: link:../vtable '@visactor/vutils': specifier: ~1.0.17 @@ -1020,10 +1020,10 @@ importers: specifier: ~1.0.17 version: 1.0.23 '@visactor/vtable': - specifier: workspace:1.26.5 + specifier: workspace:1.26.6 version: link:../vtable '@visactor/vtable-editors': - specifier: workspace:1.26.5 + specifier: workspace:1.26.6 version: link:../vtable-editors '@visactor/vutils': specifier: ~1.0.17 @@ -1256,13 +1256,13 @@ importers: specifier: 2.1.3 version: 2.1.3 '@visactor/vtable': - specifier: workspace:1.26.5 + specifier: workspace:1.26.6 version: link:../vtable '@visactor/vtable-editors': - specifier: workspace:1.26.5 + specifier: workspace:1.26.6 version: link:../vtable-editors '@visactor/vtable-gantt': - specifier: workspace:1.26.5 + specifier: workspace:1.26.6 version: link:../vtable-gantt '@vitejs/plugin-react': specifier: 3.1.0 @@ -1361,7 +1361,7 @@ importers: ../../packages/vtable-search: dependencies: '@visactor/vtable': - specifier: workspace:1.26.5 + specifier: workspace:1.26.6 version: link:../vtable '@visactor/vutils': specifier: ~1.0.17 @@ -1515,13 +1515,13 @@ importers: specifier: ~1.0.17 version: 1.0.23 '@visactor/vtable': - specifier: workspace:1.26.5 + specifier: workspace:1.26.6 version: link:../vtable '@visactor/vtable-editors': - specifier: workspace:1.26.5 + specifier: workspace:1.26.6 version: link:../vtable-editors '@visactor/vtable-plugins': - specifier: workspace:1.26.5 + specifier: workspace:1.26.6 version: link:../vtable-plugins '@visactor/vutils': specifier: ~1.0.17 @@ -1660,7 +1660,7 @@ importers: ../../packages/vue-vtable: dependencies: '@visactor/vtable': - specifier: workspace:1.26.5 + specifier: workspace:1.26.6 version: link:../vtable '@visactor/vutils': specifier: ~1.0.17 @@ -1841,19 +1841,19 @@ importers: ../../tools/bugserver-trigger: dependencies: '@visactor/vtable': - specifier: workspace:1.26.5 + specifier: workspace:1.26.6 version: link:../../packages/vtable '@visactor/vtable-editors': - specifier: workspace:1.26.5 + specifier: workspace:1.26.6 version: link:../../packages/vtable-editors '@visactor/vtable-gantt': - specifier: workspace:1.26.5 + specifier: workspace:1.26.6 version: link:../../packages/vtable-gantt '@visactor/vtable-plugins': - specifier: workspace:1.26.5 + specifier: workspace:1.26.6 version: link:../../packages/vtable-plugins '@visactor/vtable-sheet': - specifier: workspace:1.26.5 + specifier: workspace:1.26.6 version: link:../../packages/vtable-sheet devDependencies: '@internal/bundler': diff --git a/common/config/rush/version-policies.json b/common/config/rush/version-policies.json index 6f8ac0608d..8110d7329f 100644 --- a/common/config/rush/version-policies.json +++ b/common/config/rush/version-policies.json @@ -1 +1 @@ -[{"definitionName":"lockStepVersion","policyName":"vtableMain","version":"1.26.5","mainProject":"@visactor/vtable","nextBump":"patch"}] +[{"definitionName":"lockStepVersion","policyName":"vtableMain","version":"1.26.6","mainProject":"@visactor/vtable","nextBump":"patch"}] diff --git a/docs/assets/changelog/en/release.md b/docs/assets/changelog/en/release.md index 19233703a5..6dc535311f 100644 --- a/docs/assets/changelog/en/release.md +++ b/docs/assets/changelog/en/release.md @@ -1,3 +1,20 @@ +# v1.26.6 + +2026-08-03 + +**🐛 Bug Fixes** + +- **@visactor/vtable**: refresh rows when updateRecords is called without record indexes +- **@visactor/vtable**: refresh functional icons after theme updates +- **@visactor/vtable**: hide row series number in aggregation rows +- **@visactor/vtable**: delete reverse-selected rows from context menu +- **@visactor/vtable**: allow plugins to initialize before first render +- **@visactor/vtable**: support updating checkbox state by record index + +**Full Changelog**: https://github.com/VisActor/VTable/compare/v1.26.5...v1.26.6 + +[more detail about v1.26.6](https://github.com/VisActor/VTable/releases/tag/v1.26.6) + # v1.26.5 2026-07-15 diff --git a/docs/assets/changelog/zh/release.md b/docs/assets/changelog/zh/release.md index 61e2327a9f..37940d4fb0 100644 --- a/docs/assets/changelog/zh/release.md +++ b/docs/assets/changelog/zh/release.md @@ -1,3 +1,20 @@ +# v1.26.6 + +2026-08-03 + +**🐛 问题修复** + +- **@visactor/vtable**: refresh rows when updateRecords is called without record indexes +- **@visactor/vtable**: refresh functional icons after theme updates +- **@visactor/vtable**: hide row series number in aggregation rows +- **@visactor/vtable**: delete reverse-selected rows from context menu +- **@visactor/vtable**: allow plugins initialize before first render +- **@visactor/vtable**: 支持 updating checkbox state by record index + +**Full Changelog**: https://github.com/VisActor/VTable/compare/v1.26.5...v1.26.6 + +[更多详情请查看 v1.26.6](https://github.com/VisActor/VTable/releases/tag/v1.26.6) + # v1.26.5 2026-07-15 diff --git a/docs/package.json b/docs/package.json index e05038b658..040de82b26 100644 --- a/docs/package.json +++ b/docs/package.json @@ -12,15 +12,15 @@ "dependencies": { "@arco-design/web-react": "2.66.12", "@arco-design/web-vue": "^2.11.0", - "@visactor/vtable": "workspace:1.26.5", - "@visactor/vtable-gantt": "workspace:1.26.5", - "@visactor/react-vtable": "workspace:1.26.5", - "@visactor/vue-vtable": "workspace:1.26.5", - "@visactor/openinula-vtable": "workspace:1.26.5", - "@visactor/vtable-editors": "workspace:1.26.5", - "@visactor/vtable-export": "workspace:1.26.5", - "@visactor/vtable-search": "workspace:1.26.5", - "@visactor/vtable-sheet": "workspace:1.26.5", + "@visactor/vtable": "workspace:1.26.6", + "@visactor/vtable-gantt": "workspace:1.26.6", + "@visactor/react-vtable": "workspace:1.26.6", + "@visactor/vue-vtable": "workspace:1.26.6", + "@visactor/openinula-vtable": "workspace:1.26.6", + "@visactor/vtable-editors": "workspace:1.26.6", + "@visactor/vtable-export": "workspace:1.26.6", + "@visactor/vtable-search": "workspace:1.26.6", + "@visactor/vtable-sheet": "workspace:1.26.6", "buble": "^0.20.0", "@visactor/vchart": "2.1.3", "markdown-it": "^13.0.0", diff --git a/packages/openinula-vtable/package.json b/packages/openinula-vtable/package.json index 8ee511ea6a..9ca95d26a1 100644 --- a/packages/openinula-vtable/package.json +++ b/packages/openinula-vtable/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/openinula-vtable", - "version": "1.26.5", + "version": "1.26.6", "description": "The openinula version of VTable", "keywords": [ "openinula", @@ -44,7 +44,7 @@ "access": "public" }, "dependencies": { - "@visactor/vtable": "workspace:1.26.5", + "@visactor/vtable": "workspace:1.26.6", "@visactor/vutils": "~1.0.17" }, "devDependencies": { diff --git a/packages/react-vtable/package.json b/packages/react-vtable/package.json index 9b5dec4c02..3295617b53 100644 --- a/packages/react-vtable/package.json +++ b/packages/react-vtable/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/react-vtable", - "version": "1.26.5", + "version": "1.26.6", "description": "The react version of VTable", "keywords": [ "react", @@ -57,7 +57,7 @@ "react-dom": "^18.2.0 || ^19.0.0" }, "dependencies": { - "@visactor/vtable": "workspace:1.26.5", + "@visactor/vtable": "workspace:1.26.6", "@visactor/vutils": "~1.0.17", "react-is": "^18.2.0", "react-reconciler": "0.29.0" diff --git a/packages/vtable-calendar/package.json b/packages/vtable-calendar/package.json index 82426ee528..4444fbd5ff 100644 --- a/packages/vtable-calendar/package.json +++ b/packages/vtable-calendar/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vtable-calendar", - "version": "1.26.5", + "version": "1.26.6", "description": "The calendar component of VTable", "author": { "name": "VisActor", @@ -37,7 +37,7 @@ "access": "public" }, "dependencies": { - "@visactor/vtable": "workspace:1.26.5", + "@visactor/vtable": "workspace:1.26.6", "@visactor/vutils": "~1.0.17", "date-fns": "3.6.0" }, diff --git a/packages/vtable-editors/package.json b/packages/vtable-editors/package.json index f1311d8053..600baa8154 100644 --- a/packages/vtable-editors/package.json +++ b/packages/vtable-editors/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vtable-editors", - "version": "1.26.5", + "version": "1.26.6", "description": "", "sideEffects": false, "main": "cjs/index.js", diff --git a/packages/vtable-export/package.json b/packages/vtable-export/package.json index 07e72d5733..8fceb141f8 100644 --- a/packages/vtable-export/package.json +++ b/packages/vtable-export/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vtable-export", - "version": "1.26.5", + "version": "1.26.6", "description": "The export util of VTable", "author": { "name": "VisActor", @@ -37,7 +37,7 @@ "access": "public" }, "dependencies": { - "@visactor/vtable": "workspace:1.26.5", + "@visactor/vtable": "workspace:1.26.6", "@visactor/vutils": "~1.0.17", "file-saver": "2.0.5", "@types/file-saver": "2.0.7", diff --git a/packages/vtable-gantt/package.json b/packages/vtable-gantt/package.json index 973a50d806..06e96c8088 100644 --- a/packages/vtable-gantt/package.json +++ b/packages/vtable-gantt/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vtable-gantt", - "version": "1.26.5", + "version": "1.26.6", "description": "canvas table width high performance", "keywords": [ "vtable-gantt", @@ -50,8 +50,8 @@ "fix-memory-limit": "cross-env LIMIT=10240 increase-memory-limit" }, "dependencies": { - "@visactor/vtable": "workspace:1.26.5", - "@visactor/vtable-editors": "workspace:1.26.5", + "@visactor/vtable": "workspace:1.26.6", + "@visactor/vtable-editors": "workspace:1.26.6", "@visactor/vutils": "~1.0.17", "@visactor/vscale": "~1.0.17", "@visactor/vdataset": "~1.0.17", diff --git a/packages/vtable-plugins/package.json b/packages/vtable-plugins/package.json index 7ba93aab23..417b64ec98 100644 --- a/packages/vtable-plugins/package.json +++ b/packages/vtable-plugins/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vtable-plugins", - "version": "1.26.5", + "version": "1.26.6", "description": "The search util of VTable", "author": { "name": "VisActor", @@ -61,9 +61,9 @@ "devDependencies": { "cross-env": "^7.0.3", "increase-memory-limit": "^1.0.7", - "@visactor/vtable": "workspace:1.26.5", - "@visactor/vtable-editors": "workspace:1.26.5", - "@visactor/vtable-gantt": "workspace:1.26.5", + "@visactor/vtable": "workspace:1.26.6", + "@visactor/vtable-editors": "workspace:1.26.6", + "@visactor/vtable-gantt": "workspace:1.26.6", "@visactor/vchart": "2.1.3", "@internal/bundler": "workspace:*", "@internal/eslint-config": "workspace:*", diff --git a/packages/vtable-search/package.json b/packages/vtable-search/package.json index 9e7e52e62f..9d988fcd35 100644 --- a/packages/vtable-search/package.json +++ b/packages/vtable-search/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vtable-search", - "version": "1.26.5", + "version": "1.26.6", "description": "The search util of VTable", "author": { "name": "VisActor", @@ -36,7 +36,7 @@ "access": "public" }, "dependencies": { - "@visactor/vtable": "workspace:1.26.5", + "@visactor/vtable": "workspace:1.26.6", "@visactor/vutils": "~1.0.17" }, "devDependencies": { diff --git a/packages/vtable-sheet/package.json b/packages/vtable-sheet/package.json index 18708a86c9..93fb7abfde 100644 --- a/packages/vtable-sheet/package.json +++ b/packages/vtable-sheet/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vtable-sheet", - "version": "1.26.5", + "version": "1.26.6", "description": "Lightweight editable spreadsheet component based on VTable", "keywords": [ "vtable-sheet", @@ -50,9 +50,9 @@ "build:es": "bundle --clean -f es --ignorePostTasks" }, "dependencies": { - "@visactor/vtable": "workspace:1.26.5", - "@visactor/vtable-editors": "workspace:1.26.5", - "@visactor/vtable-plugins": "workspace:1.26.5", + "@visactor/vtable": "workspace:1.26.6", + "@visactor/vtable-editors": "workspace:1.26.6", + "@visactor/vtable-plugins": "workspace:1.26.6", "@visactor/vutils": "~1.0.17", "@visactor/vscale": "~1.0.17", "@visactor/vdataset": "~1.0.17", diff --git a/packages/vtable/CHANGELOG.json b/packages/vtable/CHANGELOG.json index de275ffbc7..f481544ad1 100644 --- a/packages/vtable/CHANGELOG.json +++ b/packages/vtable/CHANGELOG.json @@ -1,6 +1,35 @@ { "name": "@visactor/vtable", "entries": [ + { + "version": "1.26.6", + "tag": "@visactor/vtable_v1.26.6", + "date": "Mon, 03 Aug 2026 11:18:03 GMT", + "comments": { + "patch": [ + { + "comment": "fix: refresh rows when updateRecords is called without record indexes" + }, + { + "comment": "fix: refresh functional icons after theme updates" + }, + { + "comment": "fix: allow plugins to initialize before first render" + }, + { + "comment": "fix: support updating checkbox state by record index" + } + ], + "none": [ + { + "comment": "fix: hide row series number in aggregation rows" + }, + { + "comment": "fix: delete reverse-selected rows from context menu" + } + ] + } + }, { "version": "1.26.5", "tag": "@visactor/vtable_v1.26.5", diff --git a/packages/vtable/CHANGELOG.md b/packages/vtable/CHANGELOG.md index 21d1c62770..1020fe84cb 100644 --- a/packages/vtable/CHANGELOG.md +++ b/packages/vtable/CHANGELOG.md @@ -1,6 +1,21 @@ # Change Log - @visactor/vtable -This log was last generated on Wed, 15 Jul 2026 10:41:26 GMT and should not be manually modified. +This log was last generated on Mon, 03 Aug 2026 11:18:03 GMT and should not be manually modified. + +## 1.26.6 +Mon, 03 Aug 2026 11:18:03 GMT + +### Patches + +- fix: refresh rows when updateRecords is called without record indexes +- fix: refresh functional icons after theme updates +- fix: allow plugins to initialize before first render +- fix: support updating checkbox state by record index + +### Updates + +- fix: hide row series number in aggregation rows +- fix: delete reverse-selected rows from context menu ## 1.26.5 Wed, 15 Jul 2026 10:41:26 GMT diff --git a/packages/vtable/package.json b/packages/vtable/package.json index 59b0bd3de2..8a2db09c06 100644 --- a/packages/vtable/package.json +++ b/packages/vtable/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vtable", - "version": "1.26.5", + "version": "1.26.6", "description": "canvas table width high performance", "keywords": [ "grid", @@ -52,7 +52,7 @@ "fix-memory-limit": "cross-env LIMIT=10240 increase-memory-limit" }, "dependencies": { - "@visactor/vtable-editors": "workspace:1.26.5", + "@visactor/vtable-editors": "workspace:1.26.6", "@visactor/vrender": "1.1.4", "@visactor/vrender-core": "1.1.4", "@visactor/vrender-kits": "1.1.4", diff --git a/packages/vue-vtable/package.json b/packages/vue-vtable/package.json index a7c2312c8c..17bb510e44 100644 --- a/packages/vue-vtable/package.json +++ b/packages/vue-vtable/package.json @@ -1,6 +1,6 @@ { "name": "@visactor/vue-vtable", - "version": "1.26.5", + "version": "1.26.6", "description": "The vue version of VTable", "keywords": [ "vue", @@ -59,7 +59,7 @@ "access": "public" }, "dependencies": { - "@visactor/vtable": "workspace:1.26.5", + "@visactor/vtable": "workspace:1.26.6", "@visactor/vutils": "~1.0.17" }, "devDependencies": { diff --git a/tools/bugserver-trigger/package.json b/tools/bugserver-trigger/package.json index e55721c4a7..60f812acce 100644 --- a/tools/bugserver-trigger/package.json +++ b/tools/bugserver-trigger/package.json @@ -8,11 +8,11 @@ "ci": "ts-node --transpileOnly --skipProject ./scripts/trigger-test.ts" }, "dependencies": { - "@visactor/vtable": "workspace:1.26.5", - "@visactor/vtable-gantt": "workspace:1.26.5", - "@visactor/vtable-editors": "workspace:1.26.5", - "@visactor/vtable-plugins": "workspace:1.26.5", - "@visactor/vtable-sheet": "workspace:1.26.5" + "@visactor/vtable": "workspace:1.26.6", + "@visactor/vtable-gantt": "workspace:1.26.6", + "@visactor/vtable-editors": "workspace:1.26.6", + "@visactor/vtable-plugins": "workspace:1.26.6", + "@visactor/vtable-sheet": "workspace:1.26.6" }, "devDependencies": { "@rushstack/eslint-patch": "~1.1.4", From 373e6d8d0913511bf1fc7353c0505b7324c540f7 Mon Sep 17 00:00:00 2001 From: fangsmile <892739385@qq.com> Date: Mon, 3 Aug 2026 19:40:27 +0800 Subject: [PATCH 77/77] docs: translate 1.26.6 zh changelog --- docs/assets/changelog/zh/release.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/assets/changelog/zh/release.md b/docs/assets/changelog/zh/release.md index 37940d4fb0..ea1efd7643 100644 --- a/docs/assets/changelog/zh/release.md +++ b/docs/assets/changelog/zh/release.md @@ -4,14 +4,14 @@ **🐛 问题修复** -- **@visactor/vtable**: refresh rows when updateRecords is called without record indexes -- **@visactor/vtable**: refresh functional icons after theme updates -- **@visactor/vtable**: hide row series number in aggregation rows -- **@visactor/vtable**: delete reverse-selected rows from context menu -- **@visactor/vtable**: allow plugins initialize before first render -- **@visactor/vtable**: 支持 updating checkbox state by record index - -**Full Changelog**: https://github.com/VisActor/VTable/compare/v1.26.5...v1.26.6 +- **@visactor/vtable**: 在调用 updateRecords 且未传入记录索引时刷新行 +- **@visactor/vtable**: 主题更新后刷新功能图标 +- **@visactor/vtable**: 在聚合行中隐藏行序号 +- **@visactor/vtable**: 支持通过右键菜单删除反向选择的行 +- **@visactor/vtable**: 允许插件在首次渲染前完成初始化 +- **@visactor/vtable**: 支持通过记录索引更新 checkbox 状态 + +**完整更新日志**: https://github.com/VisActor/VTable/compare/v1.26.5...v1.26.6 [更多详情请查看 v1.26.6](https://github.com/VisActor/VTable/releases/tag/v1.26.6)