Add cell selection copy/cut/paste with formula reference adjustment
- store: copySelection/cutSelection/pasteClipboard/cancelClipboard hold an in-memory (unpersisted) clipboard buffer of raw cell inputs + formatting. - shiftFormula rewrites every relative cell reference in a copied/cut formula by the move delta (the engine has no $-absolute refs, so all refs qualify). Copy shifts every reference uniformly; cut only follows references that point inside the moved block itself, leaving refs to untouched cells alone and never touching a qualified Table!A1 reference. - paste anchors on a smaller/single-cell selection (and overflows past it), or tiles across a selection that's an exact larger multiple of the buffer. - TableGrid: Strg+C/X/V shortcuts, context-menu entries, a dashed "clip-src" source marker, and Escape drops a pending buffer. - document.spec.ts: coverage for reference shifting, tiled/fill paste, cut's inside/outside reference split, format copy, and Escape cancelling. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -461,6 +461,109 @@ describe('multi-cell (discontiguous) selection', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('clipboard: copy / cut / paste', () => {
|
||||
it('copy adjusts every relative reference by the paste delta', () => {
|
||||
const s = fresh();
|
||||
const id = s.addTable(s.doc.activeSheetId);
|
||||
const name = s.tableById(id)!.name;
|
||||
setCell(s, name, 'A1', '5');
|
||||
setCell(s, name, 'A2', '=A1*2');
|
||||
setCell(s, name, 'C4', '9'); // the shifted formula's new precedent
|
||||
|
||||
s.setSelection({ tableId: id, anchor: coords('A2'), focus: coords('A2') });
|
||||
s.copySelection();
|
||||
s.setSelection({ tableId: id, anchor: coords('C5'), focus: coords('C5') });
|
||||
s.pasteClipboard();
|
||||
|
||||
expect(s.cellInput(id, coords('C5').col, coords('C5').row)).toBe('=C4*2');
|
||||
expect(val(s, name, 'C5')).toBe(18);
|
||||
});
|
||||
|
||||
it('copy fills a larger, exact-multiple selection (tiled paste)', () => {
|
||||
const s = fresh();
|
||||
const id = s.addTable(s.doc.activeSheetId);
|
||||
setCell(s, s.tableById(id)!.name, 'A1', '1');
|
||||
setCell(s, s.tableById(id)!.name, 'A2', '2');
|
||||
const name = s.tableById(id)!.name;
|
||||
|
||||
s.setSelection({ tableId: id, anchor: coords('A1'), focus: coords('A2') }); // 1x2 block
|
||||
s.copySelection();
|
||||
s.setSelection({ tableId: id, anchor: coords('B1'), focus: coords('B4') }); // 1x4 -> tiles twice
|
||||
s.pasteClipboard();
|
||||
|
||||
expect(val(s, name, 'B1')).toBe(1);
|
||||
expect(val(s, name, 'B2')).toBe(2);
|
||||
expect(val(s, name, 'B3')).toBe(1);
|
||||
expect(val(s, name, 'B4')).toBe(2);
|
||||
});
|
||||
|
||||
it('pasting a single copied cell into a range fills every cell, each adjusted', () => {
|
||||
const s = fresh();
|
||||
const id = s.addTable(s.doc.activeSheetId);
|
||||
const name = s.tableById(id)!.name;
|
||||
setCell(s, name, 'A1', '10');
|
||||
setCell(s, name, 'B1', '=A1');
|
||||
|
||||
s.setSelection({ tableId: id, anchor: coords('B1'), focus: coords('B1') });
|
||||
s.copySelection();
|
||||
s.setSelection({ tableId: id, anchor: coords('B2'), focus: coords('B4') });
|
||||
s.pasteClipboard();
|
||||
|
||||
expect(s.cellInput(id, coords('B2').col, coords('B2').row)).toBe('=A2');
|
||||
expect(s.cellInput(id, coords('B3').col, coords('B3').row)).toBe('=A3');
|
||||
expect(s.cellInput(id, coords('B4').col, coords('B4').row)).toBe('=A4');
|
||||
});
|
||||
|
||||
it('cut only follows references that moved with the cut block; outside refs stay put', () => {
|
||||
const s = fresh();
|
||||
const id = s.addTable(s.doc.activeSheetId);
|
||||
const name = s.tableById(id)!.name;
|
||||
setCell(s, name, 'A1', '5'); // stays put -- not part of the cut
|
||||
setCell(s, name, 'B1', '10'); // part of the cut block (B1:B2)
|
||||
setCell(s, name, 'B2', '=B1+A1'); // one ref moves (B1), one doesn't (A1)
|
||||
|
||||
s.setSelection({ tableId: id, anchor: coords('B1'), focus: coords('B2') });
|
||||
s.cutSelection();
|
||||
s.setSelection({ tableId: id, anchor: coords('D1'), focus: coords('D1') });
|
||||
s.pasteClipboard();
|
||||
|
||||
expect(s.cellInput(id, coords('D2').col, coords('D2').row)).toBe('=D1+A1');
|
||||
expect(val(s, name, 'D1')).toBe(10);
|
||||
expect(val(s, name, 'D2')).toBe(15);
|
||||
// the source cells were cleared
|
||||
expect(s.cellInput(id, coords('B1').col, coords('B1').row)).toBe('');
|
||||
expect(s.cellInput(id, coords('B2').col, coords('B2').row)).toBe('');
|
||||
// the untouched cell A1 is exactly that -- untouched
|
||||
expect(val(s, name, 'A1')).toBe(5);
|
||||
});
|
||||
|
||||
it('copies cell formatting along with the value, overwriting the destination', () => {
|
||||
const s = fresh();
|
||||
const id = s.addTable(s.doc.activeSheetId);
|
||||
setCell(s, s.tableById(id)!.name, 'A1', 'x');
|
||||
s.setRangeFormat(id, coords('A1'), coords('A1'), { bold: true, align: 'right' });
|
||||
s.setRangeFormat(id, coords('C1'), coords('C1'), { italic: true });
|
||||
|
||||
s.setSelection({ tableId: id, anchor: coords('A1'), focus: coords('A1') });
|
||||
s.copySelection();
|
||||
s.setSelection({ tableId: id, anchor: coords('C1'), focus: coords('C1') });
|
||||
s.pasteClipboard();
|
||||
|
||||
expect(s.cellFormat(id, coords('C1').col, coords('C1').row)).toEqual({ bold: true, align: 'right' });
|
||||
});
|
||||
|
||||
it('Escape drops the buffer without pasting', () => {
|
||||
const s = fresh();
|
||||
const id = s.addTable(s.doc.activeSheetId);
|
||||
setCell(s, s.tableById(id)!.name, 'A1', '1');
|
||||
s.setSelection({ tableId: id, anchor: coords('A1'), focus: coords('A1') });
|
||||
s.copySelection();
|
||||
expect(s.hasClipboard.value).toBe(true);
|
||||
s.cancelClipboard();
|
||||
expect(s.hasClipboard.value).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('text box objects', () => {
|
||||
it('insert / edit / format / round-trip a text object', () => {
|
||||
const s = fresh();
|
||||
|
||||
Reference in New Issue
Block a user