diff --git a/src/components/TableGrid.vue b/src/components/TableGrid.vue index 5076940..e0b9b75 100644 --- a/src/components/TableGrid.vue +++ b/src/components/TableGrid.vue @@ -137,6 +137,20 @@ const clipMarqueeStyle = computed(() => { height: rowTop(k.r1 + 1) - top + 'px', }; }); +/** The copy/cut selection's *own* first cell, called out inside the marquee + * even once the live selection has moved on to pick a paste destination + * (which is when `is-active` no longer sits there). */ +const clipAnchorStyle = computed(() => { + const k = clipRect.value; + if (!k || k.tableId !== props.tableId) return null; + const { col, row } = k.anchor; + return { + left: colLeft(col) + 'px', + top: rowTop(row) + 'px', + width: colWidth(col) + 'px', + height: rowH(row) + 'px', + }; +}); const clampC = (c: number) => Math.max(0, Math.min((table.value?.cols ?? 1) - 1, c)); const clampR = (r: number) => Math.max(0, Math.min((table.value?.rows ?? 1) - 1, r)); @@ -771,6 +785,7 @@ defineExpose({
+
@@ -909,6 +924,14 @@ defineExpose({ opacity: 0.45; } } +/* the copy/cut selection's own first cell -- called out inside the marquee + even once the live selection has moved elsewhere to pick a paste target */ +.clip-anchor { + position: absolute; + pointer-events: none; + box-shadow: inset 0 0 0 2px var(--accent); + z-index: 8; +} .handle { position: absolute; right: -3px; diff --git a/src/store/__tests__/document.spec.ts b/src/store/__tests__/document.spec.ts index 901bfd8..5fda3e4 100644 --- a/src/store/__tests__/document.spec.ts +++ b/src/store/__tests__/document.spec.ts @@ -565,6 +565,29 @@ describe('clipboard: copy / cut / paste', () => { expect(s.hasClipboard.value).toBe(false); }); + it('clipboardRect keeps the copy source\'s own anchor after the live selection moves elsewhere', () => { + const s = fresh(); + const id = s.addTable(s.doc.activeSheetId); + s.setSelection({ tableId: id, anchor: coords('B2'), focus: coords('B5') }); + s.copySelection(); + expect(s.clipboardRect.value?.anchor).toEqual(coords('B2')); + + // picking a paste destination elsewhere must not move the source's anchor marker + s.setSelection({ tableId: id, anchor: coords('D2'), focus: coords('D5') }); + expect(s.clipboardRect.value?.anchor).toEqual(coords('B2')); + expect(s.clipboardRect.value).toMatchObject({ tableId: id, c0: 1, c1: 1, r0: 1, r1: 4 }); + }); + + it('the source anchor is the drag\'s own start cell, not the rectangle\'s top-left corner', () => { + const s = fresh(); + const id = s.addTable(s.doc.activeSheetId); + // dragged bottom-right -> top-left: anchor is B5, not the normalised B2 + s.setSelection({ tableId: id, anchor: coords('B5'), focus: coords('B2') }); + s.copySelection(); + expect(s.clipboardRect.value?.anchor).toEqual(coords('B5')); + expect(s.clipboardRect.value).toMatchObject({ c0: 1, c1: 1, r0: 1, r1: 4 }); + }); + it('copy leaves $-locked axes exactly as typed; a relative axis still shifts', () => { const s = fresh(); const id = s.addTable(s.doc.activeSheetId); diff --git a/src/store/document.ts b/src/store/document.ts index cf86af1..44c1cde 100644 --- a/src/store/document.ts +++ b/src/store/document.ts @@ -93,6 +93,11 @@ interface ClipboardData { cells: Map; /** cut (Strg+X) vs. copy (Strg+C) -- a cut buffer is consumed by its first paste. */ cut: boolean; + /** the copy/cut selection's own anchor -- the *first* cell picked, which is + * not necessarily `rect`'s top-left corner (a drag can start anywhere). + * Kept so the source marker can still point it out after the current + * selection has moved elsewhere -- see `clipboardRect`. */ + anchor: CellCoord; } /** @@ -884,10 +889,12 @@ function createStore() { // ---- clipboard (copy / cut / paste) -------------------------- /** True while a copy/cut buffer is held (enables "Einfügen" in menus). */ const hasClipboard = computed(() => clipboard.value !== null); - /** Source rectangle of the current buffer -- drives the dashed source marker. */ + /** Source rectangle of the current buffer -- drives the dashed source marker + * and its anchor-cell indicator (which stays put once the live selection + * has moved on to pick a paste destination). */ const clipboardRect = computed(() => { const cb = clipboard.value; - return cb ? { tableId: cb.tableId, ...cb.rect, cut: cb.cut } : null; + return cb ? { tableId: cb.tableId, ...cb.rect, cut: cb.cut, anchor: cb.anchor } : null; }); function copyOrCut(cut: boolean) { @@ -912,6 +919,7 @@ function createStore() { height: r.r1 - r.r0 + 1, cells, cut, + anchor: { ...s.anchor }, }; } const copySelection = () => copyOrCut(false);