- before copy paste

This commit is contained in:
wkh
2026-09-02 09:45:44 +02:00
parent c7885067e7
commit 31108977b9
12 changed files with 708 additions and 44 deletions
+223 -21
View File
@@ -79,6 +79,10 @@ function colLeft(c: number): number {
return x;
}
/** The internal scroll element of a fixed table (null for auto-size tables). */
const scrollEl = () =>
isFixed.value ? (root.value?.querySelector('.tg-scroll') as HTMLElement | null) : null;
function cellStyle(c: number, r: number): Record<string, string> {
const s: Record<string, string> = formatStyle(c, r);
if (!isFixed.value) return s;
@@ -122,6 +126,157 @@ function select(c: number, r: number, extend = false) {
store.selectCell(props.tableId, clampC(c), clampR(r), extend);
}
// ---- keyboard navigation (Excel-like) --------------------------------
/** Rectangle the active cell "flows" through on Tab/Enter (Excel data-entry). */
let flowRect: { c0: number; c1: number; r0: number; r1: number } | null = null;
/** Column a Tab-run started in, so a following Enter returns there + one row. */
let entryCol: number | null = null;
function clearFlow() {
flowRect = null;
entryCol = null;
}
/** Keep the active cell visible inside a fixed table's scroll viewport. */
function scrollActiveIntoView() {
const box = scrollEl();
const s = mine.value;
if (!box || !s) return;
const { col, row } = s.focus;
const left = colLeft(col);
const right = left + colWidth(col);
const top = rowTop(row);
const bottom = top + rowH(row);
const padL = colLeft(frozenCols.value); // width of the frozen-column band + header
const padT = rowTop(frozenRows.value); // height of the frozen-row band + header
if (left - padL < box.scrollLeft) box.scrollLeft = Math.max(0, left - padL);
else if (right > box.scrollLeft + box.clientWidth) box.scrollLeft = right - box.clientWidth;
if (top - padT < box.scrollTop) box.scrollTop = Math.max(0, top - padT);
else if (bottom > box.scrollTop + box.clientHeight) box.scrollTop = bottom - box.clientHeight;
}
function primaryRect() {
const s = mine.value;
if (!s) return null;
return {
c0: Math.min(s.anchor.col, s.focus.col),
c1: Math.max(s.anchor.col, s.focus.col),
r0: Math.min(s.anchor.row, s.focus.row),
r1: Math.max(s.anchor.row, s.focus.row),
};
}
/** Plain move of the active cell (arrow keys). `ext` = grow the selection. */
function navigate(dc: number, dr: number, ext: boolean) {
clearFlow();
const s = mine.value;
if (!s) return;
select(s.focus.col + dc, s.focus.row + dr, ext);
scrollActiveIntoView();
}
/** Tab (`axis='h'`) / Enter (`axis='v'`), Excel style. */
function flowMove(axis: 'h' | 'v', back: boolean) {
const s = mine.value;
const t = table.value;
if (!s || !t) return;
const pr = primaryRect()!;
const multi = pr.c0 !== pr.c1 || pr.r0 !== pr.r1;
if (multi || flowRect) {
if (!flowRect) flowRect = pr;
const R = flowRect;
let { col, row } = s.focus;
if (axis === 'h') {
col += back ? -1 : 1;
if (col > R.c1) {
col = R.c0;
row = row >= R.r1 ? R.r0 : row + 1;
} else if (col < R.c0) {
col = R.c1;
row = row <= R.r0 ? R.r1 : row - 1;
}
} else {
row += back ? -1 : 1;
if (row > R.r1) {
row = R.r0;
col = col >= R.c1 ? R.c0 : col + 1;
} else if (row < R.r0) {
row = R.r1;
col = col <= R.c0 ? R.c1 : col - 1;
}
}
// keep the whole range highlighted; move only the cursor cell
store.setSelection({
tableId: props.tableId,
anchor: { col, row },
focus: { col, row },
ranges: [{ anchor: { col: R.c0, row: R.r0 }, focus: { col: R.c1, row: R.r1 } }],
});
scrollActiveIntoView();
return;
}
// single cell: straight move; remember/return the Tab-run column
let col = s.focus.col;
let row = s.focus.row;
if (axis === 'h') {
if (entryCol === null) entryCol = col;
col = clampC(col + (back ? -1 : 1));
} else if (entryCol !== null) {
col = entryCol;
row = clampR(row + (back ? -1 : 1));
entryCol = null;
} else {
row = clampR(row + (back ? -1 : 1));
}
select(col, row, false);
scrollActiveIntoView();
}
/** Ctrl/Cmd+Arrow: jump to the edge of the current data block. */
function jumpTo(dc: number, dr: number, ext: boolean) {
clearFlow();
const s = mine.value;
const t = table.value;
if (!s || !t) return;
const filled = (c: number, r: number) =>
c >= 0 && r >= 0 && c < t.cols && r < t.rows && store.cellInput(props.tableId, c, r) !== '';
let { col, row } = s.focus;
const inB = (c: number, r: number) => c >= 0 && r >= 0 && c < t.cols && r < t.rows;
let nc = col + dc;
let nr = row + dr;
if (!inB(nc, nr)) {
/* already at edge */
} else if (filled(col, row) && filled(nc, nr)) {
while (inB(nc + dc, nr + dr) && filled(nc + dc, nr + dr)) {
nc += dc;
nr += dr;
}
col = nc;
row = nr;
} else {
while (inB(nc, nr) && !filled(nc, nr)) {
nc += dc;
nr += dr;
}
if (inB(nc, nr)) {
col = nc;
row = nr;
} else {
col = clampC(dc ? (dc > 0 ? t.cols - 1 : 0) : col);
row = clampR(dr ? (dr > 0 ? t.rows - 1 : 0) : row);
}
}
select(col, row, ext);
scrollActiveIntoView();
}
function pageRows(): number {
const box = scrollEl();
if (!box) return 12;
return Math.max(1, Math.floor(box.clientHeight / (table.value?.rowHeight ?? 26)) - 1);
}
// ---- mouse selection ---------------------------------------------
const dragging = ref(false);
let dragMode: 'replace' | 'additive' = 'replace';
@@ -129,9 +284,6 @@ let lastHit: { c: number; r: number } | null = null;
let lastPointer = { x: 0, y: 0 };
let autoScrollRaf = 0;
const scrollEl = () =>
isFixed.value ? (root.value?.querySelector('.tg-scroll') as HTMLElement | null) : null;
/** Hit-test by cursor position -- robust to fast moves, borders and child
* nodes (the event target during a drag is unreliable). Clamps into the
* scroll viewport of a fixed table so dragging past the edge still selects. */
@@ -201,6 +353,7 @@ function startDrag(mode: 'replace' | 'additive') {
function onCellMouseDown(e: MouseEvent, c: number, r: number) {
if (e.button !== 0) return;
if (editing.value) commitEdit();
clearFlow();
lastPointer = { x: e.clientX, y: e.clientY };
if (e.metaKey || e.ctrlKey) {
store.addToSelection(props.tableId, c, r);
@@ -238,6 +391,7 @@ function onDragUp() {
function selectColumn(c: number, e?: MouseEvent) {
const t = table.value;
if (!t) return;
clearFlow();
const a = { col: c, row: 0 };
const f = { col: c, row: t.rows - 1 };
if (e && (e.metaKey || e.ctrlKey)) store.addRangeToSelection(props.tableId, a, f);
@@ -247,6 +401,7 @@ function selectColumn(c: number, e?: MouseEvent) {
function selectRow(r: number, e?: MouseEvent) {
const t = table.value;
if (!t) return;
clearFlow();
const a = { col: 0, row: r };
const f = { col: t.cols - 1, row: r };
if (e && (e.metaKey || e.ctrlKey)) store.addRangeToSelection(props.tableId, a, f);
@@ -256,6 +411,7 @@ function selectRow(r: number, e?: MouseEvent) {
function selectAll() {
const t = table.value;
if (!t) return;
clearFlow();
store.setSelection({ tableId: props.tableId, anchor: { col: 0, row: 0 }, focus: { col: t.cols - 1, row: t.rows - 1 } });
focusRoot();
}
@@ -280,17 +436,12 @@ function beginEdit(c: number, r: number, initial?: string) {
else el.setSelectionRange(el.value.length, el.value.length);
});
}
function commitEdit(advance: 'down' | 'up' | 'left' | 'right' | 'none' = 'none') {
function commitEdit() {
if (!editing.value) return;
committing = true;
const { col, row } = editing.value;
store.setCellInput(props.tableId, col, row, editValue.value);
editing.value = null;
const d: Record<string, [number, number]> = {
down: [0, 1], up: [0, -1], left: [-1, 0], right: [1, 0], none: [0, 0],
};
const [dc, dr] = d[advance];
select(col + dc, row + dr);
focusRoot();
setTimeout(() => (committing = false), 0);
}
@@ -301,10 +452,12 @@ function cancelEdit() {
function onEditKeydown(e: KeyboardEvent) {
if (e.key === 'Enter') {
e.preventDefault();
commitEdit(e.shiftKey ? 'up' : 'down');
commitEdit();
flowMove('v', e.shiftKey);
} else if (e.key === 'Tab') {
e.preventDefault();
commitEdit(e.shiftKey ? 'left' : 'right');
commitEdit();
flowMove('h', e.shiftKey);
} else if (e.key === 'Escape') {
e.preventDefault();
cancelEdit();
@@ -312,31 +465,80 @@ function onEditKeydown(e: KeyboardEvent) {
e.stopPropagation();
}
function onEditBlur() {
if (!committing && editing.value) commitEdit('none');
if (!committing && editing.value) commitEdit();
}
// ---- keyboard navigation -------------------------------------
// ---- keyboard navigation (Excel-like) ------------------------
function onKeydown(e: KeyboardEvent) {
if (editing.value) return;
const s = mine.value;
if (!s) return;
const t = table.value;
if (!s || !t) return;
const { col, row } = s.focus;
const ext = e.shiftKey;
const mod = e.ctrlKey || e.metaKey;
switch (e.key) {
case 'ArrowUp': e.preventDefault(); return select(col, row - 1, ext);
case 'ArrowDown': e.preventDefault(); return select(col, row + 1, ext);
case 'ArrowLeft': e.preventDefault(); return select(col - 1, row, ext);
case 'ArrowRight': e.preventDefault(); return select(col + 1, row, ext);
case 'Tab': e.preventDefault(); return select(col + (e.shiftKey ? -1 : 1), row);
case 'Enter': e.preventDefault(); return e.shiftKey ? select(col, row - 1) : beginEdit(col, row);
case 'F2': e.preventDefault(); return beginEdit(col, row);
case 'ArrowUp':
e.preventDefault();
e.stopPropagation();
return mod ? jumpTo(0, -1, ext) : navigate(0, -1, ext);
case 'ArrowDown':
e.preventDefault();
e.stopPropagation();
return mod ? jumpTo(0, 1, ext) : navigate(0, 1, ext);
case 'ArrowLeft':
e.preventDefault();
e.stopPropagation();
return mod ? jumpTo(-1, 0, ext) : navigate(-1, 0, ext);
case 'ArrowRight':
e.preventDefault();
e.stopPropagation();
return mod ? jumpTo(1, 0, ext) : navigate(1, 0, ext);
case 'Tab':
e.preventDefault();
e.stopPropagation();
return flowMove('h', e.shiftKey);
case 'Enter':
e.preventDefault();
e.stopPropagation();
return flowMove('v', e.shiftKey);
case 'F2':
e.preventDefault();
e.stopPropagation();
return beginEdit(col, row);
case 'Home':
e.preventDefault();
e.stopPropagation();
clearFlow();
select(0, mod ? 0 : row, ext);
return scrollActiveIntoView();
case 'End':
e.preventDefault();
e.stopPropagation();
clearFlow();
select(t.cols - 1, mod ? t.rows - 1 : row, ext);
return scrollActiveIntoView();
case 'PageUp':
e.preventDefault();
e.stopPropagation();
return navigate(0, -pageRows(), ext);
case 'PageDown':
e.preventDefault();
e.stopPropagation();
return navigate(0, pageRows(), ext);
case 'Backspace':
case 'Delete':
e.preventDefault();
e.stopPropagation();
return store.clearSelectionCells();
case 'Escape':
clearFlow();
return;
default:
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
e.preventDefault();
e.stopPropagation();
beginEdit(col, row, e.key);
}
}