🙃
This commit is contained in:
@@ -1,11 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { EllipsisVertical, Omega } from '@lucide/svelte';
|
||||
import { EllipsisVertical } from '@lucide/svelte';
|
||||
import * as Alert from '$lib/components/ui/alert/index.js';
|
||||
import Cell from '$lib/components/grid/cell.svelte';
|
||||
import CellHeader from './cell-header.svelte';
|
||||
import { colToStr, refToStr } from './utils';
|
||||
import clsx from 'clsx';
|
||||
import { Input } from '../ui/input';
|
||||
import type { LeadMsg } from './messages';
|
||||
import { Grid, Position } from './grid.svelte.ts';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
@@ -18,279 +17,177 @@
|
||||
socket: WebSocket;
|
||||
} = $props();
|
||||
|
||||
socket.onmessage = (msg: MessageEvent) => {
|
||||
let res: LeadMsg;
|
||||
const grid = $state(new Grid(socket));
|
||||
|
||||
socket.onmessage = (msg: MessageEvent) => {
|
||||
try {
|
||||
res = JSON.parse(msg.data);
|
||||
console.log(res);
|
||||
const res: LeadMsg = JSON.parse(msg.data);
|
||||
grid.handle_msg(res);
|
||||
} catch (err) {
|
||||
console.error('Failed to parse LeadMsg:', err);
|
||||
return;
|
||||
}
|
||||
|
||||
grid.handle_msg(res);
|
||||
};
|
||||
|
||||
const grid = $state(new Grid(socket));
|
||||
onMount(() => {
|
||||
grid.init();
|
||||
});
|
||||
|
||||
let rows = 100;
|
||||
let cols = 50;
|
||||
// --- module-level state ------------------------------------------------------
|
||||
let dragging = false;
|
||||
|
||||
// range picking while editing a formula:
|
||||
const selectionBox = $derived(grid.mode.getSelection());
|
||||
|
||||
// Formula state
|
||||
let selectingRangeForFormula = false;
|
||||
let anchorRow = -1;
|
||||
let anchorCol = -1;
|
||||
let hoverRow = -1;
|
||||
let hoverCol = -1;
|
||||
let formulaCaretStart: number | null = null;
|
||||
let formulaCaretEnd: number | null = null;
|
||||
let anchorRow = -1,
|
||||
anchorCol = -1,
|
||||
hoverRow = -1,
|
||||
hoverCol = -1;
|
||||
let formulaCaretStart: number | null = null,
|
||||
formulaCaretEnd: number | null = null;
|
||||
|
||||
// helper: A1 or A1:B9 using your existing refToStr()
|
||||
function rangeRef(r1: number, c1: number, r2: number, c2: number) {
|
||||
const rs = Math.min(r1, r2);
|
||||
const re = Math.max(r1, r2);
|
||||
const cs = Math.min(c1, c2);
|
||||
const ce = Math.max(c1, c2);
|
||||
|
||||
const a = refToStr(rs, cs);
|
||||
const b = refToStr(re, ce);
|
||||
const rs = Math.min(r1, r2),
|
||||
re = Math.max(r1, r2);
|
||||
const cs = Math.min(c1, c2),
|
||||
ce = Math.max(c1, c2);
|
||||
const a = refToStr(rs, cs),
|
||||
b = refToStr(re, ce);
|
||||
return rs === re && cs === ce ? a : `${a}:${b}`;
|
||||
}
|
||||
|
||||
// --- your existing handler, modified ----------------------------------------
|
||||
function handleCellMouseDown(i: number, j: number, e: MouseEvent) {
|
||||
const pos = new Position(i, j);
|
||||
|
||||
if (grid.anyIsEditing()) {
|
||||
const el = document.querySelector<HTMLInputElement>('input:focus');
|
||||
const currentInputValue = el?.value ?? '';
|
||||
|
||||
// Only treat as reference insert if we're editing a formula
|
||||
if (currentInputValue.trim().startsWith('=')) {
|
||||
// Keep focus in the input
|
||||
e.preventDefault();
|
||||
|
||||
// Enter "select range for formula" mode, but DO NOT insert yet.
|
||||
selectingRangeForFormula = true;
|
||||
dragging = true;
|
||||
|
||||
anchorRow = i;
|
||||
anchorCol = j;
|
||||
hoverRow = i;
|
||||
hoverCol = j;
|
||||
|
||||
// remember the caret where we'll insert the reference on mouseup
|
||||
if (el) {
|
||||
formulaCaretStart = el.selectionStart ?? el.value.length;
|
||||
formulaCaretEnd = el.selectionEnd ?? el.value.length;
|
||||
el.focus();
|
||||
} else {
|
||||
formulaCaretStart = formulaCaretEnd = null;
|
||||
}
|
||||
|
||||
// visually highlight the starting cell
|
||||
grid.setActive(new Position(anchorRow, anchorCol), new Position(anchorRow, anchorCol));
|
||||
return;
|
||||
}
|
||||
|
||||
// Not a formula; exit editing before doing a normal selection
|
||||
grid.stopAnyEditing();
|
||||
const input = document.querySelector<HTMLInputElement>('input:focus');
|
||||
if (input?.value.trim().startsWith('=')) {
|
||||
e.preventDefault();
|
||||
selectingRangeForFormula = true;
|
||||
dragging = true;
|
||||
anchorRow = hoverRow = i;
|
||||
anchorCol = hoverCol = j;
|
||||
formulaCaretStart = input.selectionStart ?? input.value.length;
|
||||
formulaCaretEnd = input.selectionEnd ?? input.value.length;
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal (non-formula) selection behavior
|
||||
grid.setActive(pos, pos);
|
||||
grid.stopAnyEditing();
|
||||
dragging = true;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const handler = (e: MouseEvent) => {
|
||||
// If click is outside the grid, cancel editing
|
||||
if (!(e.target as HTMLElement).closest('.grid-wrapper')) {
|
||||
grid.stopEditingActive();
|
||||
|
||||
// also reset any in-progress formula selection
|
||||
selectingRangeForFormula = false;
|
||||
dragging = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!dragging) return;
|
||||
|
||||
const el = document.elementFromPoint(e.clientX, e.clientY);
|
||||
if (el && el instanceof HTMLElement && el.dataset.row && el.dataset.col) {
|
||||
const row = parseInt(el.dataset.row, 10);
|
||||
const col = parseInt(el.dataset.col, 10);
|
||||
|
||||
hoverRow = row;
|
||||
hoverCol = col;
|
||||
|
||||
if (selectingRangeForFormula) {
|
||||
// while dragging a formula range, keep the grid selection in sync
|
||||
grid.setActive(new Position(anchorRow, anchorCol), new Position(row, col));
|
||||
} else {
|
||||
// normal drag-select
|
||||
grid.setActive(grid.primary_active, new Position(row, col));
|
||||
}
|
||||
if (el instanceof HTMLElement && el.dataset.row && el.dataset.col) {
|
||||
hoverRow = parseInt(el.dataset.row, 10);
|
||||
hoverCol = parseInt(el.dataset.col, 10);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
// Commit the range to the formula input iff we were range-picking
|
||||
if (selectingRangeForFormula) {
|
||||
const input = document.querySelector<HTMLInputElement>('input:focus');
|
||||
|
||||
// Fallbacks in case caret wasn't captured (shouldn't happen if input stayed focused)
|
||||
const start = formulaCaretStart ?? input?.value.length ?? 0;
|
||||
const end = formulaCaretEnd ?? start;
|
||||
|
||||
const ref = rangeRef(anchorRow, anchorCol, hoverRow, hoverCol);
|
||||
|
||||
if (input) {
|
||||
const before = input.value.slice(0, start);
|
||||
const after = input.value.slice(end);
|
||||
input.value = before + ref + after;
|
||||
|
||||
const newPos = start + ref.length;
|
||||
input.setSelectionRange(newPos, newPos);
|
||||
const ref = rangeRef(anchorRow, anchorCol, hoverRow, hoverCol);
|
||||
const start = formulaCaretStart ?? 0,
|
||||
end = formulaCaretEnd ?? start;
|
||||
input.value = input.value.slice(0, start) + ref + input.value.slice(end);
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
input.focus();
|
||||
}
|
||||
|
||||
// Reset formula-range state but keep the user in edit mode
|
||||
selectingRangeForFormula = false;
|
||||
grid.clearActive();
|
||||
}
|
||||
|
||||
dragging = false;
|
||||
|
||||
// clear transient state
|
||||
anchorRow = anchorCol = hoverRow = hoverCol = -1;
|
||||
formulaCaretStart = formulaCaretEnd = null;
|
||||
};
|
||||
|
||||
window.addEventListener('click', handler);
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
window.addEventListener('mouseup', handleMouseUp);
|
||||
|
||||
onDestroy(() => {
|
||||
window.removeEventListener('click', handler);
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
window.removeEventListener('mouseup', handleMouseUp);
|
||||
});
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="relative mb-5 ml-5 flex items-center gap-[5px]">
|
||||
<Alert.Root
|
||||
class={clsx(
|
||||
'flex h-9 w-fit min-w-[80px] rounded-md border border-input bg-transparent px-2 text-sm font-medium shadow-xs ring-offset-background transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30',
|
||||
'focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50',
|
||||
'flex items-center justify-center aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40'
|
||||
)}
|
||||
>
|
||||
{grid.getActiveRangeStr()}
|
||||
</Alert.Root>
|
||||
|
||||
<Alert.Root class="h-9 w-fit min-w-[80px] border bg-input/30 px-2 text-xs shadow-xs"></Alert.Root>
|
||||
<EllipsisVertical class="text-muted-foreground" size="20px" />
|
||||
|
||||
<div
|
||||
class="relative"
|
||||
onkeydown={(e: KeyboardEvent) => {
|
||||
const target = e.currentTarget as HTMLElement;
|
||||
const input = target.querySelector('input') as HTMLInputElement | null;
|
||||
|
||||
if (e.key === 'Enter' || e.key === 'NumpadEnter') {
|
||||
e.preventDefault(); // avoid form submit/line break
|
||||
|
||||
grid.stopExternalEdit(grid.getActivePos());
|
||||
grid.setCell(grid.getActivePos());
|
||||
input?.blur();
|
||||
} else if (e.key == 'Escape') {
|
||||
e.preventDefault();
|
||||
|
||||
grid.stopExternalEdit(grid.getActivePos());
|
||||
grid.resetCellTemp(grid.getActivePos());
|
||||
input?.blur();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Omega
|
||||
size="20px"
|
||||
class="absolute top-1/2 left-2 z-10 -translate-y-1/2 text-muted-foreground"
|
||||
/>
|
||||
<Input
|
||||
disabled={grid.getActivePos() === null}
|
||||
onmousedown={() => grid.startExternalEdit(grid.getActivePos())}
|
||||
onblur={() => grid.stopExternalEdit(grid.getActivePos())}
|
||||
bind:value={
|
||||
() => grid.getActiveCell()?.temp_raw ?? '',
|
||||
(v) => {
|
||||
grid.setCellTemp(grid.getActivePos(), v);
|
||||
}
|
||||
}
|
||||
class="relative w-fit min-w-[300px] pl-9"
|
||||
></Input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class={clsx(' grid-wrapper relative h-full min-h-0 max-w-full min-w-0 overflow-auto', className)}
|
||||
>
|
||||
<div class="sticky top-0 flex w-fit" style="z-index: {rows + 70}">
|
||||
<div class="sticky top-0 left-0" style="z-index: {rows + 70}">
|
||||
<CellHeader
|
||||
resizeable={false}
|
||||
height={grid.getDefaultRowHeight()}
|
||||
width={grid.getDefaultColWidth()}
|
||||
val=""
|
||||
active={false}
|
||||
direction="blank"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#each Array(cols) as _, j}
|
||||
<CellHeader
|
||||
height={grid.getDefaultRowHeight()}
|
||||
width={grid.getColWidth(j)}
|
||||
setColWidth={(width) => grid.setColWidth(j, width)}
|
||||
direction="col"
|
||||
val={colToStr(j)}
|
||||
active={grid.primary_active !== null &&
|
||||
grid.secondary_active !== null &&
|
||||
j >= Math.min(grid.primary_active.col, grid.secondary_active.col) &&
|
||||
j <= Math.max(grid.primary_active.col, grid.secondary_active.col)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{#each Array(rows) as _, i}
|
||||
<div class="relative flex w-fit">
|
||||
<div class="sticky left-0 flex w-fit" style="z-index: {rows - i + 40}">
|
||||
<div class={clsx('grid-wrapper relative h-full overflow-auto text-xs outline-none', className)}>
|
||||
<div class="relative w-fit min-w-full">
|
||||
<div class="sticky top-0 flex w-fit" style="z-index: 100;">
|
||||
<div class="sticky left-0 z-[101]">
|
||||
<CellHeader
|
||||
direction="row"
|
||||
height={grid.getRowHeight(i)}
|
||||
direction="blank"
|
||||
height={grid.getDefaultRowHeight()}
|
||||
width={grid.getDefaultColWidth()}
|
||||
setRowHeight={(height) => grid.setRowHeight(i, height)}
|
||||
val={(i + 1).toString()}
|
||||
active={grid.primary_active !== null &&
|
||||
grid.secondary_active !== null &&
|
||||
i >= Math.min(grid.primary_active.row, grid.secondary_active.row) &&
|
||||
i <= Math.max(grid.primary_active.row, grid.secondary_active.row)}
|
||||
val=""
|
||||
active={false}
|
||||
/>
|
||||
</div>
|
||||
{#each Array(cols) as _, j}
|
||||
<Cell
|
||||
{grid}
|
||||
pos={new Position(i, j)}
|
||||
height={grid.getRowHeight(i)}
|
||||
<CellHeader
|
||||
direction="col"
|
||||
height={grid.getDefaultRowHeight()}
|
||||
width={grid.getColWidth(j)}
|
||||
onmousedown={(e) => {
|
||||
handleCellMouseDown(i, j, e);
|
||||
}}
|
||||
setColWidth={(w) => grid.setColWidth(j, w)}
|
||||
val={colToStr(j)}
|
||||
active={grid.mode?.getSelection()?.containsCol(j) ?? false}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#each Array(rows) as _, i}
|
||||
<div class="flex w-fit">
|
||||
<div class="sticky left-0 z-50 flex w-fit">
|
||||
<CellHeader
|
||||
direction="row"
|
||||
height={grid.getRowHeight(i)}
|
||||
width={grid.getDefaultColWidth()}
|
||||
setRowHeight={(h) => grid.setRowHeight(i, h)}
|
||||
val={(i + 1).toString()}
|
||||
active={grid.mode?.getSelection()?.containsRow(i) ?? false}
|
||||
/>
|
||||
</div>
|
||||
{#each Array(cols) as _, j}
|
||||
<Cell
|
||||
{grid}
|
||||
pos={new Position(i, j)}
|
||||
height={grid.getRowHeight(i)}
|
||||
width={grid.getColWidth(j)}
|
||||
onmousedown={(e) => handleCellMouseDown(i, j, e)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#if selectionBox}
|
||||
{@const isSingle = selectionBox.isSingleCell()}
|
||||
<div class="pointer-events-none absolute inset-0 z-40">
|
||||
<div
|
||||
class="absolute border-2 border-primary"
|
||||
style:top="{selectionBox.primaryPxTop}px"
|
||||
style:left="{selectionBox.primaryPxLeft}px"
|
||||
style:width="{selectionBox.primaryPxWidth}px"
|
||||
style:height="{selectionBox.primaryPxHeight}px"
|
||||
></div>
|
||||
|
||||
{#if !isSingle}
|
||||
<div
|
||||
class="absolute border-2 border-dashed border-primary"
|
||||
style:top="{selectionBox.secondaryPxTop}px"
|
||||
style:left="{selectionBox.secondaryPxLeft}px"
|
||||
style:width="{selectionBox.secondaryPxWidth}px"
|
||||
style:height="{selectionBox.secondaryPxHeight}px"
|
||||
></div>
|
||||
<div
|
||||
class="absolute bg-primary/20"
|
||||
style:top="{selectionBox.pxTop}px"
|
||||
style:left="{selectionBox.pxLeft}px"
|
||||
style:width="{selectionBox.pxWidth}px"
|
||||
style:height="{selectionBox.pxHeight}px"
|
||||
></div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user