- before copy paste
This commit is contained in:
+223
-21
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user