Fix copy marquee (one rectangle, not per-cell) and arrow-key collapse point
- Copy source marker: was outline:dashed on every td in the copied range, so each cell got its own little dashed box. Replaced with one absolutely positioned .clip-marquee overlay sized/positioned from the range's pixel bounds (colLeft/rowTop), giving a single dashed rectangle around the whole range, same as Excel/Numbers. - Arrow keys (no Shift) after selecting a multi-cell range collapsed beside the *focus* (wherever the drag/shift-selection ended), not the *anchor* (the active, first-selected cell) -- inconsistent with the anchor-stays- active fix from the previous change and with Excel/Numbers, where a plain arrow key always steps from the active cell. navigate()/jumpTo()/Home/End now step from the anchor when collapsing (ext=false) and keep stepping from the focus when extending (Shift/Ctrl+Shift), which is unchanged. Verified interactively against the dev server with Playwright (isolated empty sheet, to sidestep the seed data's cascaded/overlapping tables): selecting B2:B5 keeps the active-cell border on B2, copying shows exactly one .clip-marquee, and ArrowLeft afterwards lands on A2, not A5. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -122,12 +122,21 @@ 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") --
|
||||
// ---- clipboard source marker (one dashed rectangle, Excel-style "marching
|
||||
// ants" -- overlaid once over the whole range, not per cell). --------------
|
||||
const clipRect = computed(() => store.clipboardRect.value);
|
||||
const inClipSource = (c: number, r: number) => {
|
||||
const clipMarqueeStyle = computed(() => {
|
||||
const k = clipRect.value;
|
||||
return !!k && k.tableId === props.tableId && c >= k.c0 && c <= k.c1 && r >= k.r0 && r <= k.r1;
|
||||
};
|
||||
if (!k || k.tableId !== props.tableId) return null;
|
||||
const left = colLeft(k.c0);
|
||||
const top = rowTop(k.r0);
|
||||
return {
|
||||
left: left + 'px',
|
||||
top: top + 'px',
|
||||
width: colLeft(k.c1 + 1) - left + 'px',
|
||||
height: rowTop(k.r1 + 1) - top + 'px',
|
||||
};
|
||||
});
|
||||
|
||||
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));
|
||||
@@ -176,12 +185,17 @@ function primaryRect() {
|
||||
};
|
||||
}
|
||||
|
||||
/** Plain move of the active cell (arrow keys). `ext` = grow the selection. */
|
||||
/** Plain move of the active cell (arrow keys). `ext` = grow the selection.
|
||||
* A plain move steps from the *active* cell (the anchor) -- collapsing a
|
||||
* multi-cell selection back to just beside it, not beside whichever corner
|
||||
* the selection happens to end at. Extending (Shift+Arrow) keeps growing
|
||||
* from the current extension edge (the focus), same as before. */
|
||||
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);
|
||||
const from = ext ? s.focus : s.anchor;
|
||||
select(from.col + dc, from.row + dr, ext);
|
||||
scrollActiveIntoView();
|
||||
}
|
||||
|
||||
@@ -244,7 +258,9 @@ function flowMove(axis: 'h' | 'v', back: boolean) {
|
||||
scrollActiveIntoView();
|
||||
}
|
||||
|
||||
/** Ctrl/Cmd+Arrow: jump to the edge of the current data block. */
|
||||
/** Ctrl/Cmd+Arrow: jump to the edge of the current data block. Steps from the
|
||||
* active cell (anchor) when collapsing, or from the extension edge (focus)
|
||||
* when growing the selection -- see `navigate()`. */
|
||||
function jumpTo(dc: number, dr: number, ext: boolean) {
|
||||
clearFlow();
|
||||
const s = mine.value;
|
||||
@@ -252,7 +268,7 @@ function jumpTo(dc: number, dr: number, ext: boolean) {
|
||||
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;
|
||||
let { col, row } = ext ? s.focus : s.anchor;
|
||||
const inB = (c: number, r: number) => c >= 0 && r >= 0 && c < t.cols && r < t.rows;
|
||||
let nc = col + dc;
|
||||
let nr = row + dr;
|
||||
@@ -498,7 +514,6 @@ function onKeydown(e: KeyboardEvent) {
|
||||
const s = mine.value;
|
||||
const t = table.value;
|
||||
if (!s || !t) return;
|
||||
const { row } = s.focus; // Home/End jump within the cursor's current row
|
||||
const ext = e.shiftKey;
|
||||
const mod = e.ctrlKey || e.metaKey;
|
||||
|
||||
@@ -554,13 +569,13 @@ function onKeydown(e: KeyboardEvent) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
clearFlow();
|
||||
select(0, mod ? 0 : row, ext);
|
||||
select(0, mod ? 0 : (ext ? s.focus : s.anchor).row, ext);
|
||||
return scrollActiveIntoView();
|
||||
case 'End':
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
clearFlow();
|
||||
select(t.cols - 1, mod ? t.rows - 1 : row, ext);
|
||||
select(t.cols - 1, mod ? t.rows - 1 : (ext ? s.focus : s.anchor).row, ext);
|
||||
return scrollActiveIntoView();
|
||||
case 'PageUp':
|
||||
e.preventDefault();
|
||||
@@ -726,7 +741,6 @@ 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),
|
||||
@@ -756,6 +770,7 @@ defineExpose({
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-if="clipMarqueeStyle" class="clip-marquee" :style="clipMarqueeStyle" />
|
||||
</div>
|
||||
|
||||
<ContextMenu v-model:open="menu.open" :x="menu.x" :y="menu.y" :items="menuItems" />
|
||||
@@ -877,20 +892,21 @@ 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;
|
||||
/* one dashed rectangle around the whole copied range, not per cell */
|
||||
.clip-marquee {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
border: 1px dashed var(--accent);
|
||||
z-index: 8;
|
||||
animation: clip-src-march 1.2s linear infinite;
|
||||
}
|
||||
@keyframes clip-src-march {
|
||||
from {
|
||||
outline-offset: -1px;
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
outline-offset: -2px;
|
||||
}
|
||||
to {
|
||||
outline-offset: -1px;
|
||||
opacity: 0.45;
|
||||
}
|
||||
}
|
||||
.handle {
|
||||
|
||||
Reference in New Issue
Block a user