🙃
This commit is contained in:
@@ -1,43 +1,30 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
|
||||
const socket = new WebSocket("ws://localhost:7050");
|
||||
|
||||
socket.onmessage = (event) => {
|
||||
const message = event.data;
|
||||
console.log("Received message from server:", message);
|
||||
};
|
||||
|
||||
socket.onopen = () => {
|
||||
console.log("WebSocket connection established.");
|
||||
};
|
||||
|
||||
// Connection opened
|
||||
|
||||
// --- Configuration ---
|
||||
const numRows = 1000; // Increased to show performance
|
||||
const numCols = 100; // Increased to show performance
|
||||
const numRows = 1000;
|
||||
const numCols = 100;
|
||||
const rowHeight = 30; // px
|
||||
const colWidth = 150; // px
|
||||
const rowNumberWidth = 50; // px
|
||||
const colHeaderHeight = 30; // px
|
||||
|
||||
// --- State ---
|
||||
let gridData: string[][];
|
||||
let columnLabels: string[] = [];
|
||||
let activeCell: [number, number] | null = null;
|
||||
let viewportElement: HTMLElement;
|
||||
let viewportWidth = 0;
|
||||
let viewportHeight = 0;
|
||||
let scrollTop = 0;
|
||||
let scrollLeft = 0;
|
||||
// --- State (Svelte 5 runes) ---
|
||||
let gridData = $state<string[][] | null>(new Array());
|
||||
let columnLabels = $state<string[]>([]);
|
||||
let activeCell = $state<[number, number] | null>(null);
|
||||
|
||||
// --- Helper Functions ---
|
||||
/**
|
||||
* Generates Excel-style column labels (A, B, ..., Z, AA, AB, ...).
|
||||
*/
|
||||
let viewportEl = $state<HTMLElement | null>(null);
|
||||
let viewportWidth = $state(0);
|
||||
let viewportHeight = $state(0);
|
||||
let scrollTop = $state(0);
|
||||
let scrollLeft = $state(0);
|
||||
|
||||
let socket: WebSocket | null = null;
|
||||
|
||||
// --- Helpers ---
|
||||
function generateColumnLabels(count: number): string[] {
|
||||
const labels = [];
|
||||
const labels: string[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
let label = "";
|
||||
let temp = i;
|
||||
@@ -52,153 +39,173 @@
|
||||
|
||||
onMount(() => {
|
||||
columnLabels = generateColumnLabels(numCols);
|
||||
gridData = Array(numRows)
|
||||
.fill(null)
|
||||
.map(() => Array(numCols).fill(""));
|
||||
gridData = Array.from({ length: numRows }, () =>
|
||||
Array<string>(numCols).fill(""),
|
||||
);
|
||||
|
||||
socket = new WebSocket("ws://localhost:7050");
|
||||
socket.onmessage = (e) =>
|
||||
console.log("Received message from server:", e.data);
|
||||
socket.onopen = () => console.log("WebSocket connection established.");
|
||||
return () => socket?.close();
|
||||
});
|
||||
|
||||
// --- Event Handlers ---
|
||||
// --- Events ---
|
||||
function handleGridBlur(e: FocusEvent) {
|
||||
if (!(e.currentTarget as HTMLElement).contains(e.relatedTarget as Node)) {
|
||||
activeCell = null;
|
||||
}
|
||||
const target = e.currentTarget as HTMLElement;
|
||||
const next = e.relatedTarget as Node | null;
|
||||
if (!target.contains(next)) activeCell = null;
|
||||
}
|
||||
|
||||
function handleCellBlur(row: number, col: number) {
|
||||
// This function runs when a cell loses focus.
|
||||
// You can add your logic here, e.g., validation, calculations, etc.
|
||||
console.log(
|
||||
`Cell (${row + 1}, ${columnLabels[col]}) lost focus. Value:`,
|
||||
gridData[row][col],
|
||||
gridData![row][col],
|
||||
);
|
||||
|
||||
socket.send(
|
||||
socket?.send(
|
||||
JSON.stringify({
|
||||
row: row + 1,
|
||||
col: columnLabels[col],
|
||||
value: gridData[row][col],
|
||||
value: gridData![row][col],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// --- Reactive Calculations for Virtualization ---
|
||||
// $: is a Svelte feature that re-runs code when its dependencies change.
|
||||
// --- Scroll math (account for header/row gutters) ---
|
||||
// How far into the *grid area* (excluding headers) we've scrolled:
|
||||
const scrollDX = $derived(Math.max(0, scrollLeft - rowNumberWidth));
|
||||
const scrollDY = $derived(Math.max(0, scrollTop - colHeaderHeight));
|
||||
|
||||
// Calculate which rows/cols are visible based on scroll position
|
||||
$: startRow = Math.max(0, Math.floor(scrollTop / rowHeight));
|
||||
$: endRow = Math.min(
|
||||
numRows,
|
||||
startRow + Math.ceil(viewportHeight / rowHeight) + 1,
|
||||
);
|
||||
$: startCol = Math.max(0, Math.floor(scrollLeft / colWidth));
|
||||
$: endCol = Math.min(
|
||||
numCols,
|
||||
startCol + Math.ceil(viewportWidth / colWidth) + 1,
|
||||
// Viewport pixels actually available to show cells (excluding sticky gutters):
|
||||
const innerW = $derived(Math.max(0, viewportWidth - rowNumberWidth));
|
||||
const innerH = $derived(Math.max(0, viewportHeight - colHeaderHeight));
|
||||
|
||||
// Virtualization windows:
|
||||
const startCol = $derived(Math.max(0, Math.floor(scrollDX / colWidth)));
|
||||
const endCol = $derived(
|
||||
Math.min(numCols, startCol + Math.ceil(innerW / colWidth) + 1),
|
||||
);
|
||||
|
||||
// Create arrays of only the visible items to render
|
||||
$: visibleRows = Array(endRow - startRow)
|
||||
.fill(0)
|
||||
.map((_, i) => startRow + i);
|
||||
$: visibleCols = Array(endCol - startCol)
|
||||
.fill(0)
|
||||
.map((_, i) => startCol + i);
|
||||
const startRow = $derived(Math.max(0, Math.floor(scrollDY / rowHeight)));
|
||||
const endRow = $derived(
|
||||
Math.min(numRows, startRow + Math.ceil(innerH / rowHeight) + 1),
|
||||
);
|
||||
|
||||
const visibleCols = $derived(
|
||||
Array.from({ length: endCol - startCol }, (_, i) => startCol + i),
|
||||
);
|
||||
const visibleRows = $derived(
|
||||
Array.from({ length: endRow - startRow }, (_, i) => startRow + i),
|
||||
);
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="w-full h-[85vh] p-4 bg-background text-foreground rounded-lg border flex flex-col"
|
||||
>
|
||||
<div
|
||||
class="relative grid-container"
|
||||
class="grid-container relative"
|
||||
on:focusout={handleGridBlur}
|
||||
style="
|
||||
--row-height: {rowHeight}px;
|
||||
--col-width: {colWidth}px;
|
||||
--row-number-width: {rowNumberWidth}px;
|
||||
--col-header-height: {colHeaderHeight}px;
|
||||
--row-height: {rowHeight}px;
|
||||
--col-width: {colWidth}px;
|
||||
--row-number-width: {rowNumberWidth}px;
|
||||
--col-header-height: {colHeaderHeight}px;
|
||||
"
|
||||
>
|
||||
<!-- The scrollable viewport provides the scrollbars -->
|
||||
<!-- Single, real scroll container -->
|
||||
<div
|
||||
class="viewport"
|
||||
bind:this={viewportElement}
|
||||
bind:this={viewportEl}
|
||||
bind:clientWidth={viewportWidth}
|
||||
bind:clientHeight={viewportHeight}
|
||||
on:scroll={(e) => {
|
||||
scrollTop = e.currentTarget.scrollTop;
|
||||
scrollLeft = e.currentTarget.scrollLeft;
|
||||
const el = e.currentTarget as HTMLElement;
|
||||
scrollTop = el.scrollTop;
|
||||
scrollLeft = el.scrollLeft;
|
||||
}}
|
||||
>
|
||||
<!-- Sizer div creates the full scrollable area -->
|
||||
<!-- Sizer creates true scrollable area (includes gutters + grid) -->
|
||||
<div
|
||||
class="total-sizer"
|
||||
style:width="{numCols * colWidth}px"
|
||||
style:height="{numRows * rowHeight}px"
|
||||
style:width={`${rowNumberWidth + numCols * colWidth}px`}
|
||||
style:height={`${colHeaderHeight + numRows * rowHeight}px`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- The Renderer sits on top of the viewport and handles drawing -->
|
||||
{#if gridData}
|
||||
<div class="renderer">
|
||||
<!-- Top-left corner -->
|
||||
<div
|
||||
class="top-left-corner"
|
||||
class:active-header-corner={activeCell !== null}
|
||||
/>
|
||||
<!-- Top-left sticky corner -->
|
||||
<div
|
||||
class="top-left-corner"
|
||||
style="top: 0; left: 0;"
|
||||
class:active-header-corner={activeCell !== null}
|
||||
/>
|
||||
|
||||
<!-- Visible Column Headers -->
|
||||
<div
|
||||
class="col-headers-container"
|
||||
style:transform="translateX(-{scrollLeft}px)"
|
||||
>
|
||||
{#each visibleCols as j (j)}
|
||||
<div
|
||||
class="header-cell"
|
||||
style:left="{j * colWidth}px"
|
||||
class:active-header={activeCell !== null && activeCell[1] === j}
|
||||
>
|
||||
{columnLabels[j]}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<!-- Column headers (stick to top, scroll horizontally with grid) -->
|
||||
<div
|
||||
class="col-headers-container"
|
||||
style="
|
||||
top: 0;
|
||||
left: {rowNumberWidth}px;
|
||||
transform: translateX(-{scrollDX}px);
|
||||
"
|
||||
>
|
||||
{#each visibleCols as j (j)}
|
||||
<div
|
||||
class="header-cell"
|
||||
style:left={`${j * colWidth}px`}
|
||||
class:active-header={activeCell !== null && activeCell[1] === j}
|
||||
>
|
||||
{columnLabels[j]}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Visible Row Headers -->
|
||||
<div
|
||||
class="row-headers-container"
|
||||
style:transform="translateY(-{scrollTop}px)"
|
||||
>
|
||||
{#each visibleRows as i (i)}
|
||||
<div
|
||||
class="row-number-cell"
|
||||
style:top="{i * rowHeight}px"
|
||||
class:active-header={activeCell !== null && activeCell[0] === i}
|
||||
>
|
||||
{i + 1}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<!-- Row headers (stick to left, scroll vertically with grid) -->
|
||||
<div
|
||||
class="row-headers-container"
|
||||
style="
|
||||
top: {colHeaderHeight}px;
|
||||
left: 0;
|
||||
transform: translateY(-{scrollDY}px);
|
||||
"
|
||||
>
|
||||
{#each visibleRows as i (i)}
|
||||
<div
|
||||
class="row-number-cell"
|
||||
style:top={`${i * rowHeight}px`}
|
||||
class:active-header={activeCell !== null && activeCell[0] === i}
|
||||
>
|
||||
{i + 1}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Visible Grid Cells -->
|
||||
<!-- Visible grid cells (overlay; move opposite the inner scroll) -->
|
||||
{#if gridData}
|
||||
<div
|
||||
class="cells-container"
|
||||
style:transform="translate(-{scrollLeft}px, -{scrollTop}px)"
|
||||
style="
|
||||
top: {colHeaderHeight}px;
|
||||
left: {rowNumberWidth}px;
|
||||
transform: translate(-{scrollDX}px, -{scrollDY}px);
|
||||
"
|
||||
>
|
||||
{#each visibleRows as i (i)}
|
||||
{#each visibleCols as j (j)}
|
||||
<div
|
||||
class="grid-cell"
|
||||
style:top="{i * rowHeight}px"
|
||||
style:left="{j * colWidth}px"
|
||||
style:top={`${i * rowHeight}px`}
|
||||
style:left={`${j * colWidth}px`}
|
||||
>
|
||||
<!-- Using a standard input to ensure styles are applied correctly -->
|
||||
<input
|
||||
type="text"
|
||||
bind:value={gridData[i][j]}
|
||||
value={gridData[i][j]}
|
||||
class="cell-input"
|
||||
on:input={(e) =>
|
||||
(gridData[i][j] = (
|
||||
e.currentTarget as HTMLInputElement
|
||||
).value)}
|
||||
on:focus={() => (activeCell = [i, j])}
|
||||
on:blur={() => handleCellBlur(i, j)}
|
||||
/>
|
||||
<!-- Active cell indicator with fill handle -->
|
||||
{#if activeCell && activeCell[0] === i && activeCell[1] === j}
|
||||
<div class="active-cell-indicator">
|
||||
<div class="fill-handle" />
|
||||
@@ -208,50 +215,39 @@
|
||||
{/each}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.grid-container {
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
Helvetica, Arial, sans-serif;
|
||||
border: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
.viewport {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
.total-sizer {
|
||||
position: relative;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.renderer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
.total-sizer {
|
||||
/* occupies the scroll area; other layers are absolutely positioned on top */
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Layered overlays inside the viewport */
|
||||
.top-left-corner,
|
||||
.col-headers-container,
|
||||
.row-headers-container,
|
||||
.cells-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.top-left-corner {
|
||||
@@ -262,25 +258,22 @@
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.active-header-corner {
|
||||
background-color: hsl(var(--accent));
|
||||
}
|
||||
|
||||
.col-headers-container {
|
||||
top: 0;
|
||||
left: var(--row-number-width);
|
||||
height: var(--col-header-height);
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.row-headers-container {
|
||||
top: var(--col-header-height);
|
||||
left: 0;
|
||||
width: var(--row-number-width);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.cells-container {
|
||||
top: var(--col-header-height);
|
||||
left: var(--row-number-width);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@@ -320,12 +313,10 @@
|
||||
height: var(--row-height);
|
||||
border-right: 1px solid hsl(var(--border) / 0.7);
|
||||
border-bottom: 1px solid hsl(var(--border) / 0.7);
|
||||
pointer-events: auto;
|
||||
background-color: hsl(var(--background));
|
||||
}
|
||||
|
||||
.cell-input {
|
||||
/* Overriding all default input styles */
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
@@ -342,7 +333,7 @@
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
/* Ensure no focus styles are added by the browser or libraries */
|
||||
|
||||
.cell-input:focus,
|
||||
.cell-input:focus-visible {
|
||||
border: 1px solid red;
|
||||
@@ -358,6 +349,7 @@
|
||||
pointer-events: none;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.fill-handle {
|
||||
position: absolute;
|
||||
bottom: -4px;
|
||||
|
||||
Reference in New Issue
Block a user