Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/assets/option/en/common/option-secondary.md
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,13 @@ Whether to adjust the column width, support configuring functions, the function
canResizeColumn?: (col: number, row: number, table: BaseTableAPI) => boolean;
```

##${prefix} realtime(boolean) = true

Whether to respond in real time when dragging to resize columns/rows.

- `true` (default): Column widths / row heights update in real time during dragging, keeping the historical behavior.
- `false`: Only the indicator line moves while dragging; the final size is applied when the mouse is released. Recommended for large datasets to significantly reduce lag while dragging.

#${prefix} dragOrder(Object)

#${prefix} dragOrder(Object)
Expand Down
7 changes: 7 additions & 0 deletions docs/assets/option/zh/common/option-secondary.md
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,13 @@ animationAppear?: boolean | {
canResizeColumn?: (col: number, row: number, table: BaseTableAPI) => boolean;
```

##${prefix} realtime(boolean) = true

拖拽调整行列尺寸时是否实时响应。

- `true`(默认):拖拽过程中实时更新列宽/行高,与历史行为一致。
- `false`:拖拽过程中仅移动指示线,鼠标释放后才应用最终尺寸变化。适用于大数据量场景,可显著缓解拖拽时的卡顿。

#${prefix} dragOrder(Object)

拖拽移动位置的配置。
Expand Down
104 changes: 104 additions & 0 deletions packages/vtable-gantt/__tests__/gantt-non-realtime-resize-sync.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// @ts-nocheck

global.__VERSION__ = 'none';

import { createDiv } from './dom';
import { Gantt } from '../src/index';
// 真正发出 RESIZE_COLUMN / RESIZE_ROW 事件的是 table-group 中的 end wrapper(pointerup 路径),
// 这里直接复用它来模拟拖拽结束,验证跨包布局同步。
import { endResizeCol } from '../../vtable/src/event/listener/table-group';

function createGantt(resize: any) {
const container = createDiv();
const option = {
records: [{ id: 1, title: '任务', startDate: '2024-07-01', endDate: '2024-07-02', progress: 50 }],
taskListTable: {
columns: [
{ field: 'title', title: '任务名称', width: 180 },
{ field: 'id', title: 'ID', width: 120 }
],
tableWidth: 'auto',
resize
},
timelineHeader: {
colWidth: 50,
scales: [{ unit: 'day', step: 1 }]
},
minDate: '2024-07-01',
maxDate: '2024-07-07'
};
const gantt = new Gantt(container, option);
return { gantt, container };
}

/**
* 忠实模拟一次把第 col 列加宽 delta 像素的拖拽:start -> move(含过程事件) -> up。
* 过程事件 RESIZE_COLUMN 在真实场景中由 container-dom 的 pointermove 回调发出,这里一并复刻,
* 以便区分实时/非实时两种模式下过程事件携带的列宽是否为最新值。
*/
function dragResizeCol(table: any, col: number, delta: number) {
const startX = table.getColsWidth(0, col); // 列右边界的大致 x
table.stateManager.startResizeCol(col, startX, 50);
// 一次 pointermove,传入完整位移
table.stateManager.updateResizeCol(startX + delta, 50);
// container-dom 在每次 pointermove 后发出 RESIZE_COLUMN,携带“当前” getColWidth
if (table.hasListeners('resize_column')) {
table.fireListeners('resize_column', { col, colWidth: table.getColWidth(col) });
}
// pointerup:走真实的 end wrapper,非实时模式在此提交并补发过程事件
endResizeCol(table);
}

describe('gantt non-realtime resize layout sync', () => {
test('non-realtime column resize should sync taskTableWidth after commit', () => {
const { gantt, container } = createGantt({ realtime: false });
const table = gantt.taskListTableInstance;
const border = gantt.parsedOptions.outerFrameStyle.borderLineWidth as number;
expect(table.internalProps.resizeRealtime).toBe(false);

const widthBefore = gantt.taskTableWidth;
const colWidthBefore = table.getColWidth(0);

dragResizeCol(table, 0, 60);

const colWidthAfter = table.getColWidth(0);
// 列宽确实变宽
expect(colWidthAfter).toBeGreaterThan(colWidthBefore);
// 关键断言:甘特图任务列表宽度已随最终列宽同步,而不是停留在旧值。
// 修复前,拖拽过程事件携带旧列宽、结束时又不再发 resize_column,taskTableWidth 会停在 widthBefore。
expect(gantt.taskTableWidth).toBe(table.getAllColsWidth() + border);
expect(gantt.taskTableWidth).toBeGreaterThan(widthBefore);

gantt.release?.();
container.remove();
});

test('non-realtime shrink should also sync (no leftover gap)', () => {
const { gantt, container } = createGantt({ realtime: false });
const table = gantt.taskListTableInstance;
const border = gantt.parsedOptions.outerFrameStyle.borderLineWidth as number;

const widthBefore = gantt.taskTableWidth;
dragResizeCol(table, 0, -60);

expect(gantt.taskTableWidth).toBe(table.getAllColsWidth() + border);
expect(gantt.taskTableWidth).toBeLessThan(widthBefore);

gantt.release?.();
container.remove();
});

test('realtime mode keeps syncing as before', () => {
const { gantt, container } = createGantt({ realtime: true });
const table = gantt.taskListTableInstance;
const border = gantt.parsedOptions.outerFrameStyle.borderLineWidth as number;
expect(table.internalProps.resizeRealtime).not.toBe(false);

dragResizeCol(table, 0, 60);

expect(gantt.taskTableWidth).toBe(table.getAllColsWidth() + border);

gantt.release?.();
container.remove();
});
});
6 changes: 6 additions & 0 deletions packages/vtable/__tests__/visual/cases/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,12 @@ export const cases = [
{ id: 'pivot-empty-string-dimension', purpose: '透视空字符串列维度与普通列并存', file: './pivot/empty-string-dimension.mjs', bundles: ['vtable'] },
{ id: 'pivot-chart-pie-multi-select', purpose: '透视饼图连续扇区多选', file: './pivot-chart/pie-multi-select.mjs', bundles: ['vchart'] },
{ id: 'resize-row-height', purpose: '列表选择后拖动行边界调整行高', file: './interaction/resize-row-height.mjs', bundles: ['vtable'] },
{ id: 'resize-column-non-realtime', purpose: 'resize.realtime=false 时列宽仅在鼠标松开后应用', file: './interaction/resize-column-non-realtime.mjs', bundles: ['vtable'] },
{ id: 'resize-column-realtime-default', purpose: 'resize.realtime 默认 true 时列宽实时随拖拽变化', file: './interaction/resize-column-realtime-default.mjs', bundles: ['vtable'] },
{ id: 'resize-column-non-realtime-min-clamp', purpose: '非实时模式越过 minWidth 时按目标方向 clamp 而非反向 resize', file: './interaction/resize-column-non-realtime-min-clamp.mjs', bundles: ['vtable'] },
{ id: 'resize-column-non-realtime-preview-clamp', purpose: '非实时拖拽预览指示线/标签按最终可应用尺寸约束', file: './interaction/resize-column-non-realtime-preview-clamp.mjs', bundles: ['vtable'] },
{ id: 'resize-column-adaptive-neighbor', purpose: 'adaptive 列宽相邻列位移不被重复扣减', file: './interaction/resize-column-adaptive-neighbor.mjs', bundles: ['vtable'] },
{ id: 'resize-row-adaptive-neighbor', purpose: 'adaptive 行高相邻行位移不被重复扣减', file: './interaction/resize-row-adaptive-neighbor.mjs', bundles: ['vtable'] },
{ id: 'pivot-resize-indicator-row', purpose: '指标行上的透视表拖动行高', file: './pivot/resize-indicator-row.mjs', bundles: ['vtable'] },
{ id: 'pivot-corner-sort-one-dimension', purpose: '单级角头维度初始降序状态与排序图标', file: './pivot/corner-sort-one-dimension.mjs', bundles: ['vtable'] },
{ id: 'pivot-corner-sort-two-dimensions', purpose: '两级角头维度初始降序状态与排序图标', file: './pivot/corner-sort-two-dimensions.mjs', bundles: ['vtable'] },
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* 验证目的:adaptive 列宽下,相邻右列承接相反方向的位移后不应被重复扣减。
* 右列已是 rightColWidthCache - detaX,若再用 rightColWidth - detaX 判断 limitMinWidth 会把位移错误截短,
* 导致最终列宽与指示线终点、实时拖动不一致。本例一次性拖动一个较大位移以触发该重复扣减。
*/
export default {
mount(container) {
// 固定容器宽度让 adaptive 把两列各分到约 150px,便于数值断言。
container.style.width = '300px';
container.style.height = '300px';
return new window.VTable.ListTable(container, {
columns: [
{ field: 'name', title: 'Name', width: 150 },
{ field: 'value', title: 'Value', width: 150 }
],
records: Array.from({ length: 5 }, (_, index) => ({
name: `Item ${index + 1}`,
value: index + 1
})),
defaultRowHeight: 32,
widthMode: 'adaptive',
resize: { columnResizeMode: 'all' }
});
},
async exercise(page) {
const point = await page.evaluate(() => {
const table = window.__visualTable;
const b = table.getCellRect(0, 0).bounds;
const host = document.getElementById('table').getBoundingClientRect();
window.__leftBefore = table.getColWidth(0);
window.__rightBefore = table.getColWidth(1);
// 选取使 rightBefore - drag >= limitMinWidth(10) 但 rightBefore - 2*drag < 10 的位移,
// 正确实现应保留全部位移,旧实现会二次扣减并截短。
window.__drag = window.__rightBefore - 20;
return { edge: { x: host.x + b.x2 - 1, y: host.y + (b.y1 + b.y2) / 2 }, drag: window.__rightBefore - 20 };
});
// 单步拖动一个大位移,使一次 updateResizeColumn 收到完整 detaX。
await page.mouse.move(point.edge.x, point.edge.y);
await page.mouse.down();
await page.mouse.move(point.edge.x + point.drag, point.edge.y);
await page.mouse.up();
},
async verify(page) {
await page.evaluate(() => {
const table = window.__visualTable;
const leftBefore = window.__leftBefore;
const rightBefore = window.__rightBefore;
const drag = window.__drag;
const leftAfter = table.getColWidth(0);
const rightAfter = table.getColWidth(1);
// 右列应收缩到约 rightBefore - drag(≈20);重复扣减会把它停在 rightBefore - 10 附近。
if (rightAfter > rightBefore - drag + 5) {
throw new Error(
`adaptive 右列位移被重复扣减截短:rightBefore=${rightBefore}, drag=${drag}, rightAfter=${rightAfter}`
);
}
// 左列应吃满整段位移,验证尺寸与指示线终点一致。
if (leftAfter < leftBefore + drag - 5) {
throw new Error(
`adaptive 左列未吃满位移:leftBefore=${leftBefore}, drag=${drag}, leftAfter=${leftAfter}`
);
}
});
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* 验证目的:非实时模式把整段累计位移一次传入 min/max 约束时,应按目标值的越界方向 clamp。
* 列宽 180、minWidth 100、maxWidth 200,向左累计拖动 100px 目标为 80,必须贴 minWidth 收到 100,
* 不能因“当前宽度离哪个边界更近”而反向扩到 maxWidth 200。
*/
export default {
mount(container) {
return new window.VTable.ListTable(container, {
columns: [
{ field: 'name', title: 'Name', width: 180, minWidth: 100, maxWidth: 200 },
{ field: 'value', title: 'Value', width: 150 }
],
records: Array.from({ length: 5 }, (_, index) => ({
name: `Item ${index + 1}`,
value: index + 1
})),
defaultRowHeight: 32,
resize: { columnResizeMode: 'all', realtime: false }
});
},
async exercise(page) {
const point = await page.evaluate(() => {
const table = window.__visualTable;
const b = table.getCellRect(0, 0).bounds;
const host = document.getElementById('table').getBoundingClientRect();
window.__colWidthBefore = table.getColWidth(0);
return { edge: { x: host.x + b.x2 - 1, y: host.y + (b.y1 + b.y2) / 2 } };
});
// 非实时模式:拖拽过程只移动指示线,向左累计 100px,松开鼠标后统一提交。
await page.mouse.move(point.edge.x, point.edge.y);
await page.mouse.down();
await page.mouse.move(point.edge.x - 100, point.edge.y, { steps: 10 });
await page.mouse.up();
},
async verify(page) {
await page.evaluate(() => {
const table = window.__visualTable;
const before = window.__colWidthBefore;
const after = table.getColWidth(0);
// 目标宽度 80 越过下界,必须 clamp 到 minWidth 100 并相对初始缩窄。
if (after !== 100) {
throw new Error(`非实时越界列宽未按 minWidth clamp:before=${before}, after=${after}`);
}
// 反向 resize 会扩到 maxWidth 200,这里显式拦截该回归。
if (after >= before) {
throw new Error(`非实时向左拖动反而没有缩窄,疑似反向 resize:before=${before}, after=${after}`);
}
});
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* 验证目的:非实时模式拖拽越过 minWidth 时,指示线预览与标签应显示“最终可应用的尺寸”,
* 而不是未约束的 pendingDeltaX。列宽 180、minWidth 100,向左拖 100px 目标为 80,
* 预览标签必须显示 100px(与松手后的最终列宽一致),不能显示未约束的 80px。
*/
export default {
mount(container) {
return new window.VTable.ListTable(container, {
columns: [
{ field: 'name', title: 'Name', width: 180, minWidth: 100, maxWidth: 200 },
{ field: 'value', title: 'Value', width: 150 }
],
records: Array.from({ length: 5 }, (_, index) => ({
name: `Item ${index + 1}`,
value: index + 1
})),
defaultRowHeight: 32,
resize: { columnResizeMode: 'all', realtime: false }
});
},
async exercise(page) {
const point = await page.evaluate(() => {
const table = window.__visualTable;
const b = table.getCellRect(0, 0).bounds;
const host = document.getElementById('table').getBoundingClientRect();
return { edge: { x: host.x + b.x2 - 1, y: host.y + (b.y1 + b.y2) / 2 } };
});
// 非实时模式:按下并向左累计拖动 100px,但先不松手,停在拖拽预览状态。
await page.mouse.move(point.edge.x, point.edge.y);
await page.mouse.down();
await page.mouse.move(point.edge.x - 100, point.edge.y, { steps: 10 });
// 捕获松手前的预览标签文案与指示线位置。
await page.evaluate(() => {
const table = window.__visualTable;
const component = table.scenegraph.component;
window.__previewLabel = component.columnResizeLabel.lastChild.attribute.text;
window.__previewLineX = component.columnResizeLine.attribute.x;
// 基准线:当前列右边界 x(无偏移)。
const b = table.getCellRect(0, 0).bounds;
window.__colBaseX = b.x2;
window.__colWidthBefore = table.getColWidth(0);
});
await page.mouse.up();
},
async verify(page) {
await page.evaluate(() => {
const table = window.__visualTable;
const previewLabel = window.__previewLabel;
const previewLineX = window.__previewLineX;
const colBaseX = window.__colBaseX;
const after = table.getColWidth(0);

// 松手后最终列宽应 clamp 到 minWidth 100。
if (after !== 100) {
throw new Error(`最终列宽未按 minWidth clamp:after=${after}`);
}
// 预览标签必须显示最终可应用尺寸 100px,而不是未约束的 80px。
if (previewLabel !== '100px') {
throw new Error(`预览标签未按最终尺寸约束:previewLabel=${previewLabel}, 期望 100px`);
}
// 指示线位置应对应有效位移(最终宽度 100 相对初始 180 向左收 80px),
// 即基准线左移 (180-100)=80px;若用未约束的 100px 位移会多移 20px。
const expectedLineX = colBaseX - 80;
if (Math.abs(previewLineX - expectedLineX) > 1) {
throw new Error(
`预览指示线位置未按最终尺寸约束:previewLineX=${previewLineX}, 期望≈${expectedLineX}(基准 ${colBaseX})`
);
}
});
}
};
Loading
Loading