Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"comment": "feat: copy formula to paste cell\n\n",
"type": "none",
"packageName": "@visactor/vtable"
}
],
"packageName": "@visactor/vtable",
"email": "892739385@qq.com"
}
13 changes: 12 additions & 1 deletion docs/assets/guide/en/sheet/formula.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,23 @@ Cell reference input and range selection:
<img src="https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/guide/formula-drag-cellRange.gif" />
</div>

### Formula Copying (TODO)
### Formula Copying

When copying cells containing formulas, references are automatically adjusted:
- Relative references adjust according to position offset
- Absolute references remain unchanged

<div style="width: 30%; text-align: center;">
<img src="https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/guide/formula-copy.gif" />
</div>

### Formula Auto Filling

When a region is selected and contains formulas, the formula is automatically filled using the fill handle.
<div style="width: 30%; text-align: center;">
<img src="https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/guide/formula-autoFill.gif" />
</div>

## 6. Advanced Features

### Multi-Sheet Support (TODO)
Expand Down
11 changes: 10 additions & 1 deletion docs/assets/guide/zh/sheet/formula.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,21 @@ VTableSheet 自身开发了 FormulaEngine 模块 作为核心的计算引擎:
<img src="https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/guide/formula-drag-cellRange.gif" />
</div>

### 公式复制(TODO)
### 公式复制

当复制包含公式的单元格时,引用会自动调整:
- 相对引用会根据位置偏移调整
- 绝对引用保持不变
<div style="width: 30%; text-align: center;">
<img src="https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/guide/formula-copy.gif" />
</div>

### 填充柄自动填充

当选中区域存在公式的时候,利用填充柄拖拽自动填充单元格的公式。
<div style="width:30%; text-align: center;">
<img src="https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/guide/formula-autoFill.gif" />
</div>
## 6. 高级功能

### 多工作表支持(TODO)
Expand Down
2 changes: 1 addition & 1 deletion packages/vtable-plugins/src/excel-edit-cell-keyboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export class ExcelEditCellKeyboardPlugin implements pluginsDefinition.IVTablePlu
}
handleKeyDown(event: KeyboardEvent) {
// this.pluginOptions?.keyDown_before?.(event);
if (this.table.editorManager && this.isExcelShortcutKey(event)) {
if (this.table?.editorManager && this.isExcelShortcutKey(event)) {
const eventKey = event.key.toLowerCase() as ExcelEditCellKeyboardResponse;
//判断是键盘触发编辑单元格的情况下,那么在编辑状态中切换方向需要选中下一个继续编辑
if (this.table.editorManager.editingEditor && this.table.editorManager.beginTriggerEditCellMode === 'keydown') {
Expand Down
168 changes: 168 additions & 0 deletions packages/vtable-sheet/__tests__/copy-source-position.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/**
* 复制源位置记录测试
* 验证复制时记录的源位置是否正确用于粘贴时的公式调整
*/

describe('复制源位置记录测试', () => {
it('应该正确记录复制时的源位置', () => {
// 模拟EventManager的行为
const mockEventManager = {
copySourceRange: null as { startCol: number; startRow: number } | null,

// 模拟handleCopy中记录源位置的逻辑
recordCopySourceRange(ranges: any[]) {
if (ranges && ranges.length > 0) {
const sourceRange = ranges[0];
this.copySourceRange = {
startCol: Math.min(sourceRange.start.col, sourceRange.end.col),
startRow: Math.min(sourceRange.start.row, sourceRange.end.row)
};
}
},

// 模拟粘贴时获取源位置的逻辑
getCopySourcePosition() {
return this.copySourceRange;
},

// 模拟清除复制源位置的逻辑
clearCopySourceRange() {
this.copySourceRange = null;
}
};

// 测试1:复制C5时的位置记录
const copyRanges1 = [
{
start: { col: 2, row: 4 }, // C5
end: { col: 2, row: 4 }
}
];

mockEventManager.recordCopySourceRange(copyRanges1);

console.log('复制C5后的源位置:', mockEventManager.getCopySourcePosition());
expect(mockEventManager.getCopySourcePosition()).toEqual({
startCol: 2, // C列
startRow: 4 // 第5行
});

// 测试2:复制A1:B2区域时的位置记录
const copyRanges2 = [
{
start: { col: 0, row: 0 }, // A1
end: { col: 1, row: 1 } // B2
}
];

mockEventManager.recordCopySourceRange(copyRanges2);

console.log('复制A1:B2后的源位置:', mockEventManager.getCopySourcePosition());
expect(mockEventManager.getCopySourcePosition()).toEqual({
startCol: 0, // A列
startRow: 0 // 第1行
});

// 测试3:清除源位置
mockEventManager.clearCopySourceRange();
console.log('清除后的源位置:', mockEventManager.getCopySourcePosition());
expect(mockEventManager.getCopySourcePosition()).toBeNull();
});

it('应该正确处理相对位置计算', () => {
// 模拟processFormulaPaste的逻辑
const calculateRelativeOffset = (
sourceStartCol: number,
sourceStartRow: number,
targetStartCol: number,
targetStartRow: number
) => {
return {
colOffset: targetStartCol - sourceStartCol,
rowOffset: targetStartRow - sourceStartRow
};
};

// 测试场景:C5复制到D5
const sourcePos = { startCol: 2, startRow: 4 }; // C5
const targetPos = { startCol: 3, startRow: 4 }; // D5

const offset = calculateRelativeOffset(
sourcePos.startCol,
sourcePos.startRow,
targetPos.startCol,
targetPos.startRow
);

console.log('C5->D5的相对偏移:', offset);
expect(offset).toEqual({
colOffset: 1, // 右移1列
rowOffset: 0 // 行不变
});

// 测试场景:A1复制到C3
const offset2 = calculateRelativeOffset(0, 0, 2, 2);

console.log('A1->C3的相对偏移:', offset2);
expect(offset2).toEqual({
colOffset: 2, // 右移2列
rowOffset: 2 // 下移2行
});
});

it('应该验证完整的复制粘贴流程', () => {
// 完整的流程测试
const mockWorkSheet = {
copySourceRange: null as { startCol: number; startRow: number } | null,

// 复制时记录源位置
handleCopy(ranges: any[]) {
if (ranges && ranges.length > 0) {
const sourceRange = ranges[0];
this.copySourceRange = {
startCol: Math.min(sourceRange.start.col, sourceRange.end.col),
startRow: Math.min(sourceRange.start.row, sourceRange.end.row)
};
}
},

// 粘贴时使用记录的源位置
processFormulaPaste(formulas: string[][], targetStartCol: number, targetStartRow: number): string[][] {
if (!this.copySourceRange) {
return formulas; // 没有源位置,不处理
}

const colOffset = targetStartCol - this.copySourceRange.startCol;
const rowOffset = targetStartRow - this.copySourceRange.startRow;

return formulas.map(row =>
row.map(cell => {
if (cell.startsWith('=')) {
return cell.replace(/([A-Z]+)(\d+)/g, (match, col, row) => {
const colNum = col.charCodeAt(0) - 'A'.charCodeAt(0) + colOffset;
const rowNum = parseInt(row, 10) + rowOffset;
return String.fromCharCode('A'.charCodeAt(0) + colNum) + rowNum;
});
}
return cell;
})
);
}
};

// 步骤1:复制C5(公式=A2)
mockWorkSheet.handleCopy([
{
start: { col: 2, row: 4 }, // C5
end: { col: 2, row: 4 }
}
]);

// 步骤2:粘贴到D5
const sourceFormula = [['=A2']];
const result = mockWorkSheet.processFormulaPaste(sourceFormula, 3, 4); // D5

console.log('完整流程结果:', result);
expect(result).toEqual([['=B2']]); // C5的=A2粘贴到D5应该变成=B2
});
});
38 changes: 38 additions & 0 deletions packages/vtable-sheet/__tests__/debug-formula-paste.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { FormulaPasteProcessor } from '../src/formula/formula-paste-processor';
import { FormulaReferenceAdjustor } from '../src/formula/formula-reference-adjustor';

describe('Debug Formula Paste', () => {
it('should debug formula adjustment', () => {
const originalFormula = '=A1+B1';
const colOffset = 1; // 右移1列
const rowOffset = 1; // 下移1行

console.log('Original formula:', originalFormula);
console.log('Column offset:', colOffset);
console.log('Row offset:', rowOffset);

const adjustedFormula = FormulaReferenceAdjustor.adjustFormulaReferences(originalFormula, colOffset, rowOffset);

console.log('Adjusted formula:', adjustedFormula);
expect(adjustedFormula).toBe('=B2+C2');
});

it('should debug batch formula adjustment', () => {
const formulas = [
['=A1', '=B1'],
['=A2', '=B2']
];
const colOffset = 2;
const rowOffset = 1;

console.log('Original formulas:', formulas);

const adjustedFormulas = FormulaPasteProcessor.adjustFormulasForPasteWithOffset(formulas, colOffset, rowOffset);

console.log('Adjusted formulas:', adjustedFormulas);
expect(adjustedFormulas).toEqual([
['=C2', '=D2'],
['=C3', '=D3']
]);
});
});
Loading