Auto-sizing cell editor, multi-ref F4, and Alt+Enter line breaks
1. In-cell editor width/height ("nur in der Breite der Zelle angezeigt"):
the editor now lives as an overlay (like the clip/fill markers) instead
of inside its <td>, so it can grow past the cell -- width first, up to
an approximated display-width cap, then wrapping and growing height.
Sized via a hidden mirror element (same font/padding, real DOM
measurement -- a <textarea> can't self-size to content with CSS alone).
2. F4 now cycles every reference a real text selection overlaps, each from
its own lock state, not just the one at the caret -- select a stretch of
formula spanning several references and F4 adjusts all of them at once,
same as Excel. cycleReferenceLock(formula, caret) -> cycleReferenceLocks
(formula, start, end); a collapsed selection (start === end) keeps the
old single-reference caret behaviour. Wired into both the in-cell editor
and the formula bar.
3. Alt+Enter inserts a literal newline in the editor instead of committing
(explicit splice at the caret, not relying on the textarea's native
Enter handling, which isn't consistently reachable for Alt+Enter). A
cell holding a newline displays it wrapped (.cell.multiline) instead of
collapsing to one ellipsised line.
Along the way: a <td>'s own max-height doesn't cap table-row growth in any
browser (a table-layout quirk) -- a wrapped multi-line cell was silently
growing its row past rowH(r), which would have desynced every pixel
position computed from rowH()/rowTop() (the overlays, sticky frozen panes,
the row-resize handle). Fixed by moving cell text into its own block-level
.cell-content child, which *does* respect max-height + overflow:hidden.
Verified interactively against the dev server with Playwright: a long
formula gets a ~300px editor instead of the 104px cell; Alt+Enter produces
a real "\n" that survives commit and displays wrapped, with the row height
unchanged (26px, not the ~40px it grew to before the .cell-content fix);
F4 on a full-formula selection turns every reference absolute in one press
and keeps cycling all of them on repeated presses; existing selection,
copy/paste and fill-handle behaviour all regression-checked clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+54
-18
@@ -57,34 +57,70 @@ export function toA1(col: number, row: number, colAbs = false, rowAbs = false):
|
||||
}
|
||||
|
||||
/**
|
||||
* Excel/Numbers-style F4: cycle the cell reference at/adjacent to `caret` (a
|
||||
* 0-based offset into `formula`, which must start with "=") through
|
||||
* relative -> absolute -> row-locked -> column-locked -> relative. Returns
|
||||
* the rewritten formula and a caret placed after the toggled reference, or
|
||||
* `null` when there's no reference at the caret to toggle.
|
||||
* Excel/Numbers-style F4: cycle every cell reference through
|
||||
* relative -> absolute -> row-locked -> column-locked -> relative, each
|
||||
* from its own current state.
|
||||
*
|
||||
* With a collapsed selection (`selStart === selEnd`, the common case -- just
|
||||
* a caret), this toggles the single reference at/adjacent to it. With a real
|
||||
* text selection, it toggles *every* reference the selection overlaps at
|
||||
* once -- select a stretch of formula text spanning several references and
|
||||
* F4 adjusts them all in one press, same as Excel.
|
||||
*
|
||||
* `selStart`/`selEnd` are 0-based offsets into `formula` (which must start
|
||||
* with "="), i.e. straight from an `<input>`/`<textarea>`'s
|
||||
* `selectionStart`/`selectionEnd`. Returns the rewritten formula and a
|
||||
* selection re-spanning the (possibly resized) references, so a repeated F4
|
||||
* keeps cycling the same ones; `null` when none were found.
|
||||
*/
|
||||
export function cycleReferenceLock(
|
||||
export function cycleReferenceLocks(
|
||||
formula: string,
|
||||
caret: number,
|
||||
): { text: string; caret: number } | null {
|
||||
selStart: number,
|
||||
selEnd: number,
|
||||
): { text: string; start: number; end: number } | null {
|
||||
if (!formula.startsWith('=')) return null;
|
||||
const body = formula.slice(1);
|
||||
const at = caret - 1;
|
||||
const lo = Math.min(selStart, selEnd) - 1;
|
||||
const hi = Math.max(selStart, selEnd) - 1;
|
||||
const collapsed = lo === hi;
|
||||
let toks: Token[];
|
||||
try {
|
||||
toks = tokenize(body);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
const tok = toks.find(
|
||||
(t) => t.type === 'IDENT' && looksLikeCell(t.value) && at >= t.pos && at <= t.pos + t.value.length,
|
||||
);
|
||||
if (!tok) return null;
|
||||
const { col, row, colAbs, rowAbs } = parseA1(tok.value);
|
||||
const [nColAbs, nRowAbs] = nextLock(colAbs, rowAbs);
|
||||
const next = toA1(col, row, nColAbs, nRowAbs);
|
||||
const text = '=' + body.slice(0, tok.pos) + next + body.slice(tok.pos + tok.value.length);
|
||||
return { text, caret: 1 + tok.pos + next.length };
|
||||
const targets: Token[] = [];
|
||||
for (let i = 0; i < toks.length; i++) {
|
||||
const t = toks[i];
|
||||
if (t.type !== 'IDENT' || !looksLikeCell(t.value)) continue;
|
||||
const next = toks[i + 1];
|
||||
if (next && (next.type === 'BANG' || next.type === 'DBLCOLON')) continue; // a table-name qualifier, not a cell
|
||||
const tEnd = t.pos + t.value.length;
|
||||
const hit = collapsed ? lo >= t.pos && lo <= tEnd : t.pos < hi && tEnd > lo;
|
||||
if (hit) targets.push(t);
|
||||
}
|
||||
if (!targets.length) return null;
|
||||
|
||||
let out = '';
|
||||
let last = 0;
|
||||
let start = -1;
|
||||
let end = -1;
|
||||
for (const tok of targets) {
|
||||
const { col, row, colAbs, rowAbs } = parseA1(tok.value);
|
||||
const [nColAbs, nRowAbs] = nextLock(colAbs, rowAbs);
|
||||
const rewritten = toA1(col, row, nColAbs, nRowAbs);
|
||||
out += body.slice(last, tok.pos) + rewritten;
|
||||
if (start === -1) start = out.length - rewritten.length;
|
||||
end = out.length;
|
||||
last = tok.pos + tok.value.length;
|
||||
}
|
||||
out += body.slice(last);
|
||||
// a collapsed caret in -> a collapsed caret out (after the one toggled
|
||||
// reference), same as before; a real selection in keeps spanning every
|
||||
// reference it touched, so a repeated F4 keeps cycling all of them.
|
||||
return collapsed
|
||||
? { text: '=' + out, start: 1 + end, end: 1 + end }
|
||||
: { text: '=' + out, start: 1 + start, end: 1 + end };
|
||||
}
|
||||
|
||||
/** A1 -> $A$1 -> A$1 -> $A1 -> A1 -> ... (the classic F4 cycle). */
|
||||
|
||||
Reference in New Issue
Block a user