AND/OR/NOT, Fehler-Literale und Zeilen/Spalten einfügen/löschen

- AND(...)/OR(...)/NOT(...) als gewöhnliche Funktionen (kein Kurzschluss-
  Sonderfall wie IF/XLOOKUP nötig, werten wie in echtem Excel alle
  Argumente eager aus). Dabei die bisher dreifach duplizierte
  Wahrheits-Prüfung zu functions.ts#isTruthy() zusammengezogen, jetzt
  von FUNCTIONS.IF, evalIf() und dem Block-IF-Statement gemeinsam genutzt.
  Schließt "kein AND/OR für zusammengesetzte Bedingungen" aus den
  Bekannten Grenzen, auch nutzbar in "Mit Schleife"-IF-Bedingungen.

- Neue Grammatik-Ergänzung: ErrorLiteralNode. Jeder der sieben Fehlercodes
  (#REF, #DIV/0!, #NAME?, #ERR, #CIRC, #VALUE!, #N/A) lässt sich jetzt
  direkt in eine Formel tippen (=IF(A1=0;#N/A;A1)) und wertet unbedingt zu
  genau diesem Fehler aus.

- Zeilen/Spalten einfügen/löschen mitten im Gitter (Rechtsklick auf einen
  Zeilen-/Spaltenkopf), nicht nur am Rand wie bisher: Zellinhalte, Formate
  und Zeilenhöhen/Spaltenbreiten verschieben sich innerhalb der Tabelle,
  und jede Formel im gesamten Dokument, die die Tabelle referenziert (auch
  über Tabelle!Zelle/Blatt!Tabelle::Zelle-Qualifier aus anderen Tabellen),
  wird automatisch mitgezogen. Ein Bereich, der die Stelle überspannt,
  wächst/schrumpft entsprechend; eine Referenz, die exakt die gelöschte
  Zeile/Spalte trifft, wird zu einem echten #REF-Literal im Formeltext
  (nutzt die neue ErrorLiteralNode-Grammatik). $-Sperren bleiben dabei
  bewusst wirkungslos (anders als beim Kopieren), unveränderte Referenzen
  bleiben byte-identisch. Token-Level-Splice wie shiftFormula (Kopieren/
  Ausschneiden), keine AST-Neuserialisierung.

406 Tests, vue-tsc/eslint/build sauber, Playwright-live-verifiziert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
wkh
2026-09-08 22:34:33 +02:00
co-authored by Claude Sonnet 5
parent 2c54537427
commit aef8f1349c
14 changed files with 937 additions and 22 deletions
+33
View File
@@ -952,13 +952,44 @@ function onKeydown(e: KeyboardEvent) {
// ---- context menu -----------------------------------------
const menu = ref({ x: 0, y: 0, open: false });
/** Which row/col header (if any) the menu was opened on -- drives whether
* it shows the row/col insert-delete items (Excel-style, only from a
* header right-click) or the ordinary cell menu. */
const menuCtx = ref<{ row: number | null; col: number | null }>({ row: null, col: null });
function onContextMenu(e: MouseEvent) {
const target = e.target as HTMLElement | null;
const rowHeadEl = target?.closest?.('th[data-rowhead]') as HTMLElement | null;
const colHeadEl = target?.closest?.('th[data-colhead]') as HTMLElement | null;
menuCtx.value = {
row: rowHeadEl ? Number(rowHeadEl.dataset.rowhead) : null,
col: colHeadEl ? Number(colHeadEl.dataset.colhead) : null,
};
const hit = cellFromEvent(e);
// keep a multi-selection if the click landed inside it; otherwise select the cell
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(() => {
const t = table.value;
const ctx = menuCtx.value;
if (t && ctx.row !== null) {
const r = ctx.row;
const items = [
{ label: 'Zeile oberhalb einfügen', action: () => store.insertRow(props.tableId, r) },
{ label: 'Zeile unterhalb einfügen', action: () => store.insertRow(props.tableId, r + 1) },
];
if (t.rows > 1) items.push({ label: 'Zeile löschen', action: () => store.deleteRow(props.tableId, r) });
return items;
}
if (t && ctx.col !== null) {
const c = ctx.col;
const items = [
{ label: 'Spalte links einfügen', action: () => store.insertColumn(props.tableId, c) },
{ label: 'Spalte rechts einfügen', action: () => store.insertColumn(props.tableId, c + 1) },
];
if (t.cols > 1) items.push({ label: 'Spalte löschen', action: () => store.deleteColumn(props.tableId, c) });
return items;
}
const items = [
{ label: 'Kopieren', action: () => store.copySelection() },
{ label: 'Ausschneiden', action: () => store.cutSelection() },
@@ -1061,6 +1092,7 @@ defineExpose({
v-for="c in table.cols"
:key="'h' + (c - 1)"
class="col-head"
:data-colhead="c - 1"
:class="{ hot: colHot(c - 1), 'freeze-edge-right': c - 1 === frozenCols - 1 }"
:style="c - 1 < frozenCols ? { left: colLeft(c - 1) + 'px', zIndex: '7' } : {}"
@click="selectColumn(c - 1, $event)"
@@ -1074,6 +1106,7 @@ defineExpose({
<tr v-for="r in table.rows" :key="'r' + (r - 1)" :style="{ height: rowH(r - 1) + 'px' }">
<th
class="row-head"
:data-rowhead="r - 1"
:class="{ hot: rowHot(r - 1), 'frozen-row': r - 1 < frozenRows, 'freeze-edge-bottom': r - 1 === frozenRows - 1 }"
:style="rowHeadStyle(r - 1)"
@click="selectRow(r - 1, $event)"