Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
125 changes: 125 additions & 0 deletions packages/vtable/examples/debug/issue-5115-auto-height-zero-row.ts
Original file line number Diff line number Diff line change
@@ -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 = `
<button id="issue5115Check">check</button>
<button id="issue5115ToggleCheck">toggle check</button>
<span id="issue5115State"></span>
`;
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);
}
4 changes: 4 additions & 0 deletions packages/vtable/examples/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand All @@ -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) {
Expand Down Expand Up @@ -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
);
Expand All @@ -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
);
Expand All @@ -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
);
Expand Down Expand Up @@ -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
);
Expand Down Expand Up @@ -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
);
Expand Down
40 changes: 40 additions & 0 deletions packages/vtable/src/scenegraph/layout/update-row.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
26 changes: 18 additions & 8 deletions packages/vtable/src/state/checkbox/checkbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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';
Expand All @@ -606,17 +612,17 @@ 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];
if (isValid(cachedState)) {
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;
Expand All @@ -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;
}
Expand Down
Loading