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);