diff --git a/packages/vtable/__tests__/listTable-checkbox-record-index.test.ts b/packages/vtable/__tests__/listTable-checkbox-record-index.test.ts index 5ecd791266..6cfa42c657 100644 --- a/packages/vtable/__tests__/listTable-checkbox-record-index.test.ts +++ b/packages/vtable/__tests__/listTable-checkbox-record-index.test.ts @@ -142,10 +142,7 @@ describe('ListTable checkbox record index api', () => { { task: { text: 'parent', checked: true }, hierarchyState: TYPES.HierarchyState.collapse, - children: [ - { task: { text: 'child 1', checked: true } }, - { task: { text: 'child 2', checked: true } } - ] + children: [{ task: { text: 'child 1', checked: true } }, { task: { text: 'child 2', checked: true } }] } ], enableCheckboxCascade: true 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..88b1877e06 --- /dev/null +++ b/packages/vtable/examples/debug/issue-5115-auto-height-zero-row.ts @@ -0,0 +1,125 @@ +import * as VTable from '../../src'; + +const CONTAINER_ID = 'vTable'; + +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)!; + 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 option: VTable.ListTableConstructorOptions = { + container, + records, + columns: [ + { 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, + 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; + const bodyStart = tableInstance.frozenRowCount; + const bodyHeight = + tableInstance.tableNoFrameHeight - tableInstance.getFrozenRowsHeight() - tableInstance.getBottomFrozenRowsHeight(); + const renderedHeight = tableInstance.getRowsHeight(bodyStart, proxy.rowEnd); + const firstFilteredRowHeight = tableInstance.getRowHeight(tableInstance.columnHeaderLevelCount + 1); + const proxyRowsSynced = + proxy.totalRow >= proxy.rowEnd && proxy.totalActualBodyRowCount >= proxy.rowEnd - proxy.rowStart + 1; + const pass = firstFilteredRowHeight === 0 && renderedHeight >= bodyHeight && proxyRowsSynced; + const state = document.getElementById('issue5115State')!; + state.textContent = + `${pass ? 'PASS' : 'FAIL'} | firstFilteredRowHeight=${firstFilteredRowHeight} renderedHeight=${renderedHeight} ` + + `bodyHeight=${bodyHeight} rowEnd=${proxy.rowEnd} totalRow=${proxy.totalRow}`; + return { + pass, + firstFilteredRowHeight, + renderedHeight, + bodyHeight, + rowEnd: proxy.rowEnd, + totalRow: proxy.totalRow, + totalActualBodyRowCount: proxy.totalActualBodyRowCount, + proxyRowsSynced + }; + }; + + 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(() => { + filterRows(); + setTimeout(check, 0); + }, 0); +} diff --git a/packages/vtable/examples/menu.ts b/packages/vtable/examples/menu.ts index 49090d023b..d339e67072 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-5117-auto-height-real-height' 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..01b31ddeb1 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,30 @@ 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, nextRow, nextRow, false); + targetRow = nextRow; + } + + return targetRow; +} + +function syncVisibleBodyRows(proxy: SceneProxy, targetRow: number) { + if (targetRow <= proxy.totalRow) { + return; + } + + proxy.totalRow = targetRow; + proxy.totalActualBodyRowCount = Math.max(proxy.totalActualBodyRowCount, targetRow - proxy.bodyTopRow + 1); +} + export function createGroupForFirstScreen( cornerHeaderGroup: Group, colHeaderGroup: Group, @@ -50,6 +74,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); @@ -64,6 +89,10 @@ export function createGroupForFirstScreen( ? table.rowCount - 1 : distRowForCompute ?? distRow ); //如果配置了 canvasHeight为 'auto', 则一次性将所有行高都计算出来才能满足后续赋值表格高度的使用 + if (table.heightMode === 'autoHeight') { + bodyDistRow = fillVisibleBodyRows(proxy, bodyDistRow); + syncVisibleBodyRows(proxy, bodyDistRow); + } } if (distCol < table.colCount - table.rightFrozenColCount) { @@ -128,7 +157,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 ); @@ -142,7 +171,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 ); @@ -156,7 +185,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 ); @@ -275,7 +304,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 ); @@ -307,7 +336,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 ); 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; diff --git a/packages/vtable/src/state/checkbox/checkbox.ts b/packages/vtable/src/state/checkbox/checkbox.ts index 9b7d676f07..5b4da0ea5e 100644 --- a/packages/vtable/src/state/checkbox/checkbox.ts +++ b/packages/vtable/src/state/checkbox/checkbox.ts @@ -6,6 +6,9 @@ import type { BaseTableAPI } from '../../ts-types/base-table'; import type { CachedDataSource } from '../../data'; import type { CheckBox } from '@src/vrender'; +type CheckboxStateValue = boolean | 'indeterminate'; +type CheckboxRecordValue = CheckboxStateValue | { checked?: CheckboxStateValue }; + export function setCheckedState( col: number, row: number, @@ -590,9 +593,12 @@ function updateParentCheckboxStateByRecordIndex(recordIndex: number | number[], continue; } - const childStates = parentRecord.children.map((child: any, childIndex: number) => - getRecordCheckboxState(parentIndex.concat(childIndex), child, field, table) - ); + const childStates: CheckboxStateValue[] = parentRecord.children.map((child: any, childIndex: number) => { + const childRecordIndex = parentIndex.concat(childIndex); + const childState = getRecordCheckboxState(childRecordIndex, child, field, table); + setRecordCheckboxState(childRecordIndex, field, childState, table.stateManager.checkedState); + return childState; + }); const allChecked = childStates.every(state => state === true); const allUnchecked = childStates.every(state => state !== true && state !== 'indeterminate'); const parentState = allChecked ? true : allUnchecked ? false : 'indeterminate'; @@ -606,7 +612,7 @@ function getRecordCheckboxState( record: any, field: FieldDef, table: BaseTableAPI -): boolean | 'indeterminate' { +): CheckboxStateValue { const fieldKey = field as string | number; const dataIndex = normalizeRecordIndex(recordIndex).toString(); const cachedState = table.stateManager.checkedState.get(dataIndex)?.[fieldKey]; @@ -614,9 +620,9 @@ function getRecordCheckboxState( return cachedState; } - const value = record?.[fieldKey]; - if (isObject(value) && isValid(value.checked)) { - return value.checked; + const value = record?.[fieldKey] as CheckboxRecordValue | undefined; + if (isObject(value) && isValid((value as { checked?: CheckboxStateValue }).checked)) { + return (value as { checked?: CheckboxStateValue }).checked; } if (typeof value === 'boolean') { return value; @@ -639,7 +645,11 @@ function normalizeRecordIndexToArray(recordIndex: number | number[]): number[] { return isArray(recordIndex) ? recordIndex : [recordIndex]; } -function traverseRecords(records: any[], handler: (record: any, recordIndex: number | number[]) => void, parentIndex: number[] = []) { +function traverseRecords( + records: any[], + handler: (record: any, recordIndex: number | number[]) => void, + parentIndex: number[] = [] +) { if (!isArray(records)) { return; }