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:
@@ -118,6 +118,13 @@ const inSelection = (c: number, r: number) =>
|
||||
const colHot = (c: number) => selRects.value.some((k) => c >= k.c0 && c <= k.c1);
|
||||
const rowHot = (r: number) => selRects.value.some((k) => r >= k.r0 && r <= k.r1);
|
||||
|
||||
// ---- clipboard source marker (dashed outline, Excel-style "marching ants") --
|
||||
const clipRect = computed(() => store.clipboardRect.value);
|
||||
const inClipSource = (c: number, r: number) => {
|
||||
const k = clipRect.value;
|
||||
return !!k && k.tableId === props.tableId && c >= k.c0 && c <= k.c1 && r >= k.r0 && r <= k.r1;
|
||||
};
|
||||
|
||||
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));
|
||||
const focusRoot = () => nextTick(() => root.value?.focus());
|
||||
@@ -478,6 +485,25 @@ function onKeydown(e: KeyboardEvent) {
|
||||
const ext = e.shiftKey;
|
||||
const mod = e.ctrlKey || e.metaKey;
|
||||
|
||||
if (mod) {
|
||||
const k = e.key.toLowerCase();
|
||||
if (k === 'c') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return store.copySelection();
|
||||
}
|
||||
if (k === 'x') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return store.cutSelection();
|
||||
}
|
||||
if (k === 'v') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return store.pasteClipboard();
|
||||
}
|
||||
}
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
@@ -534,6 +560,7 @@ function onKeydown(e: KeyboardEvent) {
|
||||
return store.clearSelectionCells();
|
||||
case 'Escape':
|
||||
clearFlow();
|
||||
store.cancelClipboard();
|
||||
return;
|
||||
default:
|
||||
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
||||
@@ -552,13 +579,21 @@ function onContextMenu(e: MouseEvent) {
|
||||
if (hit && !inSelection(hit.c, hit.r)) select(hit.c, hit.r);
|
||||
menu.value = { x: e.clientX, y: e.clientY, open: true };
|
||||
}
|
||||
const menuItems = computed(() => [
|
||||
{ label: 'Zeile hinzufügen', action: () => store.addRows(props.tableId) },
|
||||
{ label: 'Spalte hinzufügen', action: () => store.addColumns(props.tableId) },
|
||||
{ label: 'Letzte Zeile entfernen', action: () => store.removeLastRow(props.tableId) },
|
||||
{ label: 'Letzte Spalte entfernen', action: () => store.removeLastColumn(props.tableId) },
|
||||
{ label: 'Auswahl leeren', action: () => store.clearSelectionCells() },
|
||||
]);
|
||||
const menuItems = computed(() => {
|
||||
const items = [
|
||||
{ label: 'Kopieren', action: () => store.copySelection() },
|
||||
{ label: 'Ausschneiden', action: () => store.cutSelection() },
|
||||
];
|
||||
if (store.hasClipboard.value) items.push({ label: 'Einfügen', action: () => store.pasteClipboard() });
|
||||
items.push(
|
||||
{ label: 'Zeile hinzufügen', action: () => store.addRows(props.tableId) },
|
||||
{ label: 'Spalte hinzufügen', action: () => store.addColumns(props.tableId) },
|
||||
{ label: 'Letzte Zeile entfernen', action: () => store.removeLastRow(props.tableId) },
|
||||
{ label: 'Letzte Spalte entfernen', action: () => store.removeLastColumn(props.tableId) },
|
||||
{ label: 'Auswahl leeren', action: () => store.clearSelectionCells() },
|
||||
);
|
||||
return items;
|
||||
});
|
||||
|
||||
// ---- column resize --------------------------------------
|
||||
let resize: { col: number; startX: number; startW: number } | null = null;
|
||||
@@ -674,6 +709,7 @@ defineExpose({
|
||||
:class="{
|
||||
'is-active': isActive(c - 1, r - 1),
|
||||
'in-range': inSelection(c - 1, r - 1),
|
||||
'clip-src': inClipSource(c - 1, r - 1),
|
||||
numeric: store.cellIsNumeric(tableId, c - 1, r - 1),
|
||||
error: store.cellIsError(tableId, c - 1, r - 1),
|
||||
frozen: (c - 1 < frozenCols) || (r - 1 < frozenRows),
|
||||
@@ -824,6 +860,22 @@ defineExpose({
|
||||
.cell.is-active {
|
||||
box-shadow: inset 0 0 0 2px var(--accent);
|
||||
}
|
||||
.cell.clip-src {
|
||||
outline: 1px dashed var(--accent);
|
||||
outline-offset: -1px;
|
||||
animation: clip-src-march 0.6s linear infinite;
|
||||
}
|
||||
@keyframes clip-src-march {
|
||||
from {
|
||||
outline-offset: -1px;
|
||||
}
|
||||
50% {
|
||||
outline-offset: -2px;
|
||||
}
|
||||
to {
|
||||
outline-offset: -1px;
|
||||
}
|
||||
}
|
||||
.handle {
|
||||
position: absolute;
|
||||
right: -3px;
|
||||
|
||||
Reference in New Issue
Block a user