From 30578a15d55d3335cb4cc9923f82aa6eb7393bde Mon Sep 17 00:00:00 2001 From: Lloyd Date: Thu, 11 Sep 2025 18:47:45 +1000 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=99=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/{evaluator.rs => evaluator/mod.rs} | 51 ++----- backend/src/evaluator/utils.rs | 76 ++++++++++ frontend/README.md | 38 ----- frontend/src/lib/components/grid/cell.svelte | 70 +++++---- frontend/src/lib/components/grid/grid.svelte | 126 +++++++++++----- .../src/lib/components/grid/grid.svelte.ts | 142 +++++++++++++++--- frontend/src/lib/components/grid/messages.ts | 3 + .../ui/alert/alert-description.svelte | 23 +++ .../components/ui/alert/alert-title.svelte | 20 +++ .../src/lib/components/ui/alert/alert.svelte | 44 ++++++ frontend/src/lib/components/ui/alert/index.ts | 14 ++ 11 files changed, 440 insertions(+), 167 deletions(-) rename backend/src/{evaluator.rs => evaluator/mod.rs} (92%) create mode 100644 backend/src/evaluator/utils.rs delete mode 100644 frontend/README.md create mode 100644 frontend/src/lib/components/ui/alert/alert-description.svelte create mode 100644 frontend/src/lib/components/ui/alert/alert-title.svelte create mode 100644 frontend/src/lib/components/ui/alert/alert.svelte create mode 100644 frontend/src/lib/components/ui/alert/index.ts diff --git a/backend/src/evaluator.rs b/backend/src/evaluator/mod.rs similarity index 92% rename from backend/src/evaluator.rs rename to backend/src/evaluator/mod.rs index 5b217fe..e4cb9d4 100644 --- a/backend/src/evaluator.rs +++ b/backend/src/evaluator/mod.rs @@ -1,14 +1,16 @@ use serde::{Deserialize, Serialize}; -use crate::cell::CellRef; -use crate::common::LeadErr; -use crate::common::LeadErrCode; -use crate::common::Literal; -use crate::grid::Grid; -use crate::parser::*; -use std::collections::HashSet; -use std::f64; -use std::fmt; +use crate::{ + cell::CellRef, + common::{LeadErr, LeadErrCode, Literal}, + evaluator::utils::*, + grid::Grid, + parser::*, +}; + +use std::{collections::HashSet, f64, fmt}; + +mod utils; #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] #[serde(rename_all = "lowercase")] @@ -266,37 +268,6 @@ fn eval_avg( } } -fn eval_single_arg_numeric( - args: &Vec, - precs: &mut HashSet, - grid: Option<&Grid>, - func: fn(f64) -> f64, - func_name: String, -) -> Result { - if args.len() != 1 { - return Err(LeadErr { - title: "Evaluation error.".into(), - desc: format!("{func_name} function requires a single argument."), - code: LeadErrCode::Invalid, - }); - } - - let err = LeadErr { - title: "Evaluation error.".into(), - desc: format!("{func_name} function requires a numeric argument."), - code: LeadErrCode::TypeErr, - }; - - match evaluate_expr(&args[0], precs, grid)? { - Eval::Literal(Literal::Number(num)) => Ok(Eval::Literal(Literal::Number(func(num)))), - Eval::CellRef { eval, .. } => match *eval { - Eval::Literal(Literal::Number(n)) => Ok(Eval::Literal(Literal::Number(func(n)))), - _ => Err(err), - }, - _ => Err(err), - } -} - fn eval_const(args: &Vec, value: Eval) -> Result { if args.len() != 0 { return Err(LeadErr { diff --git a/backend/src/evaluator/utils.rs b/backend/src/evaluator/utils.rs new file mode 100644 index 0000000..9af59fc --- /dev/null +++ b/backend/src/evaluator/utils.rs @@ -0,0 +1,76 @@ +use std::collections::HashSet; + +use crate::{ + cell::CellRef, + common::{LeadErr, LeadErrCode, Literal}, + evaluator::{Eval, evaluate_expr}, + grid::Grid, + parser::Expr, +}; + +pub fn eval_single_arg_numeric( + args: &Vec, + precs: &mut HashSet, + grid: Option<&Grid>, + func: fn(f64) -> f64, + func_name: String, +) -> Result { + if args.len() != 1 { + return Err(LeadErr { + title: "Evaluation error.".into(), + desc: format!("{func_name} function requires a single argument."), + code: LeadErrCode::Invalid, + }); + } + let err = LeadErr { + title: "Evaluation error.".into(), + desc: format!("{func_name} function requires a numeric argument."), + code: LeadErrCode::TypeErr, + }; + match evaluate_expr(&args[0], precs, grid)? { + Eval::Literal(Literal::Number(num)) => Ok(Eval::Literal(Literal::Number(func(num)))), + Eval::CellRef { eval, .. } => match *eval { + Eval::Literal(Literal::Number(n)) => Ok(Eval::Literal(Literal::Number(func(n)))), + _ => Err(err), + }, + _ => Err(err), + } +} + +pub fn eval_n_arg_numeric( + n: usize, + args: &Vec, + precs: &mut HashSet, + grid: Option<&Grid>, + func: fn(Vec) -> f64, + func_name: String, +) -> Result { + if args.len() != n { + return Err(LeadErr { + title: "Evaluation error.".into(), + desc: format!("{func_name} function requires {n} argument(s)."), + code: LeadErrCode::Invalid, + }); + } + + let err = LeadErr { + title: "Evaluation error.".into(), + desc: format!("{func_name} function requires numeric argument(s)."), + code: LeadErrCode::TypeErr, + }; + + let mut numbers = Vec::with_capacity(n); + + for arg in args { + match evaluate_expr(arg, precs, grid)? { + Eval::Literal(Literal::Number(num)) => numbers.push(num), + Eval::CellRef { eval, .. } => match *eval { + Eval::Literal(Literal::Number(num)) => numbers.push(num), + _ => return Err(err.clone()), + }, + _ => return Err(err.clone()), + } + } + + Ok(Eval::Literal(Literal::Number(func(numbers)))) +} diff --git a/frontend/README.md b/frontend/README.md deleted file mode 100644 index 75842c4..0000000 --- a/frontend/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# sv - -Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). - -## Creating a project - -If you're seeing this, you've probably already done this step. Congrats! - -```sh -# create a new project in the current directory -npx sv create - -# create a new project in my-app -npx sv create my-app -``` - -## Developing - -Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: - -```sh -npm run dev - -# or start the server and open the app in a new browser tab -npm run dev -- --open -``` - -## Building - -To create a production version of your app: - -```sh -npm run build -``` - -You can preview the production build with `npm run preview`. - -> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. diff --git a/frontend/src/lib/components/grid/cell.svelte b/frontend/src/lib/components/grid/cell.svelte index 4bd5369..903cdcb 100644 --- a/frontend/src/lib/components/grid/cell.svelte +++ b/frontend/src/lib/components/grid/cell.svelte @@ -3,32 +3,31 @@ import clsx from 'clsx'; import { getErrDesc, getErrTitle, getEvalLiteral, isErr } from './utils'; import * as HoverCard from '$lib/components/ui/hover-card/index.js'; - import type { CellT } from './messages'; + import { Position, type Grid } from './grid.svelte.ts'; let { cla = '', - width = '80px', - height = '30px', - cell = $bindable(undefined), + pos, onmousedown = () => {}, - startediting = () => {}, - stopediting = () => {}, - active = false, - editing = false, - externalediting = false + grid }: { cla?: string; width?: string; height?: string; - cell?: CellT; + grid: Grid; + pos: Position; onmousedown?: (e: MouseEvent) => void; - startediting?: () => void; - stopediting?: () => void; - active?: boolean; - editing?: boolean; - externalediting?: boolean; } = $props(); + let cell = $derived(grid.getCell(pos)); + let active = $derived(grid.isActive(pos)); + let primaryactive = $derived(grid.isPrimaryActive(pos)); + let editing = $derived(grid.isEditing(pos)); + let externalediting = $derived(grid.isExternalEditing(pos)); + let width = $derived(grid.getColWidth(pos.col)); + let height = $derived(grid.getRowHeight(pos.row)); + let showPreview = $derived(getPreview() !== ''); + // focus the first focusable descendant (the inner ) function autofocusWithin(node: HTMLElement) { queueMicrotask(() => { @@ -47,22 +46,21 @@ el?.blur(); // triggers on:blur below } else if (e.key == 'Escape') { e.preventDefault(); - stopediting(); + grid.stopEditing(pos); + grid.resetCellTemp(pos); } } function getPreview() { return !isErr(cell?.temp_eval) ? getEvalLiteral(cell?.temp_eval) : ''; } - - let showPreview = $derived(getPreview() !== ''); {#if editing}
{#if showPreview}
{:else if cell && isErr(cell.eval)} - + {@render InnerCell()} @@ -100,15 +98,19 @@ {#snippet InnerCell()}
grid.startEditing(pos)} {onmousedown} + data-row={pos.row} + data-col={pos.col} style:width style:height - class={clsx('placeholder bg-background p-1', { active }, cla)} + class={clsx('placeholder bg-background p-1', { primaryactive, active }, cla)} > {#if cell && (cell.raw !== '' || getEvalLiteral(cell.eval) !== '')} - {#if cell.eval && !externalediting} + {#if externalediting} + {cell.temp_raw} + {:else if cell.eval} {getEvalLiteral(cell.eval)} {:else} {cell.raw} @@ -126,12 +128,20 @@ text-overflow: clip; } - .active { - z-index: 20; + .primaryactive { + z-index: 30; border: 1px solid var(--color-primary); outline: 1px solid var(--color-primary); } + .active { + z-index: 20; + background-color: color-mix(in oklab, var(--color-primary) 50%, var(--color-background) 90%); + + /* border: 1px solid var(--color-primary); */ + /* outline: 1px solid var(--color-primary); */ + } + .active:has(.err), .placeholder:has(.err) { position: relative; /* needed for absolute positioning */ diff --git a/frontend/src/lib/components/grid/grid.svelte b/frontend/src/lib/components/grid/grid.svelte index ff81c28..5b14eb1 100644 --- a/frontend/src/lib/components/grid/grid.svelte +++ b/frontend/src/lib/components/grid/grid.svelte @@ -1,13 +1,14 @@
+ + {grid.getActiveRangeStr()} + + + +
{ + 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 - const el = (e.currentTarget as HTMLElement).querySelector( - 'input' - ) as HTMLInputElement | null; - el?.blur(); // triggers on:blur below + + grid.stopExternalEdit(grid.getActivePos()); + grid.setCell(grid.getActivePos()); + input?.blur(); } else if (e.key == 'Escape') { e.preventDefault(); - grid.stopEditingActive(); + grid.stopExternalEdit(grid.getActivePos()); + grid.resetCellTemp(grid.getActivePos()); + input?.blur(); } }} > @@ -103,24 +151,22 @@ class="absolute top-1/2 left-2 z-10 -translate-y-1/2 text-muted-foreground" /> grid.setExternalEdit(grid.getActivePos())} - onblur={() => grid.setCell(grid.getActivePos())} + 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-[200px] pl-9" + class="relative w-fit min-w-[300px] pl-9" >
@@ -141,7 +187,10 @@ setColWidth={(width) => grid.setColWidth(j, width)} direction="col" val={colToStr(j)} - active={grid.getActivePos() !== null && grid.getActivePos()?.col === 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}
@@ -154,25 +203,22 @@ width={grid.getDefaultColWidth()} setRowHeight={(height) => grid.setRowHeight(i, height)} val={(i + 1).toString()} - active={grid.getActivePos() !== null && grid.getActivePos()?.row === i} + 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)} />
{#each Array(cols) as _, j} grid.startEditing(new Position(i, j))} - stopediting={() => grid.stopEditing(new Position(i, j))} onmousedown={(e) => { - handleCellInteraction(i, j, e); + console.log(`down at ${refToStr(i, j)}`); + handleCellMouseDown(i, j, e); }} - bind:cell={ - () => grid.getCell(new Position(i, j)), - (v) => grid.setCellTemp(new Position(i, j), v?.temp_raw) - } - active={grid.isActive(new Position(i, j))} /> {/each}
diff --git a/frontend/src/lib/components/grid/grid.svelte.ts b/frontend/src/lib/components/grid/grid.svelte.ts index 5bfd921..b71b542 100644 --- a/frontend/src/lib/components/grid/grid.svelte.ts +++ b/frontend/src/lib/components/grid/grid.svelte.ts @@ -1,5 +1,6 @@ import { toast } from 'svelte-sonner'; import type { CellRef, CellT, Eval, LeadMsg } from './messages'; +import { refToStr } from './utils'; class Position { public row: number; @@ -22,6 +23,10 @@ class Position { return { row: this.row, col: this.col }; } + public str(): string { + return refToStr(this.row, this.col); + } + public equals(other: CellRef | null | undefined): boolean { return !!other && this.row === other.row && this.col === other.col; } @@ -38,7 +43,8 @@ class Grid { col_widths: Record = $state({}); default_row_height: string; default_col_width: string; - active_cell: Position | null = $state(null); + primary_active: Position | null = $state(null); + secondary_active: Position | null = $state(null); editing_cell: Position | null = $state(null); external_editing_cell: Position | null = $state(null); editing_preview: [Eval, boolean] | null = $state(null); // [Eval, dirty] @@ -53,8 +59,8 @@ class Grid { return this.data[pos.key()]; } - public setCell(pos: Position | null) { - if (pos === null) return; + public setCell(pos: Position | null | undefined) { + if (pos === null || pos === undefined) return; let data = this.data[pos.key()]; if (data === undefined) return; @@ -83,6 +89,7 @@ class Grid { this.data[pos.key()] = { raw: x?.raw ?? '', temp_raw: raw, + pos: pos, eval: x?.eval ?? undefined, temp_eval: x?.temp_eval ?? undefined }; @@ -90,6 +97,20 @@ class Grid { this.quickEval(pos, raw); } + public resetCellTemp(pos: Position | null | undefined) { + if (!pos) return; + + let x = this.data[pos.key()]; + + this.data[pos.key()] = { + raw: x?.raw ?? '', + pos: pos, + temp_raw: x?.raw ?? '', + eval: x?.eval ?? undefined, + temp_eval: undefined + }; + } + public getRowHeight(row: number) { return this.row_heights[row] ?? this.default_row_height; } @@ -121,8 +142,10 @@ class Grid { } } - public startEditing(pos: Position) { - this.active_cell = pos; + public startEditing(pos: Position | undefined) { + if (!pos) return; + + this.setActive(pos, pos); this.editing_cell = pos; let cell = this.getCell(pos); @@ -130,52 +153,132 @@ class Grid { cell.temp_eval = undefined; } - public stopEditing(pos: Position) { + public stopEditing(pos: Position | null | undefined) { + if (!pos) return; this.editing_cell = null; - this.setCell(pos); + // this.setCell(pos); } public stopEditingActive() { - if (this.active_cell == null) return; - this.stopEditing(this.active_cell); + if (!this.anyIsActive() || !this.primary_active?.equals(this.secondary_active)) return; + this.stopEditing(this.primary_active); } public isEditing(pos: Position): boolean { - if (this.editing_cell == null) return false; + if (this.editing_cell === null) return false; return this.editing_cell.equals(pos); } + public anyIsEditing(): boolean { + return this.editing_cell !== null; + } + public isExternalEditing(pos: Position): boolean { - if (this.external_editing_cell == null) return false; + if (this.external_editing_cell === null) return false; return this.external_editing_cell.equals(pos); } - public setActive(pos: Position | null) { - this.active_cell = pos; + public setActive(primary: Position | null, secondary: Position | null) { + this.primary_active = primary; + this.secondary_active = secondary; } - public setExternalEdit(pos: Position | null) { + public setInactive() { + this.primary_active = null; + this.secondary_active = null; + } + + public startExternalEdit(pos: Position | null) { this.external_editing_cell = pos; } + public stopExternalEdit(pos: Position | null) { + this.external_editing_cell = null; + } + public getActiveCell(): CellT | undefined { - if (this.active_cell === null) + if (this.primary_active === null || this.secondary_active === null) { return { raw: '', temp_raw: '', + pos: new Position(-1, -1), eval: undefined }; + } - return this.getCell(this.active_cell); + if (!this.primary_active.equals(this.secondary_active)) { + return { + raw: '', + temp_raw: '', + pos: new Position(-1, -1), + eval: undefined + }; + } + + return this.getCell(this.primary_active); } + public getActiveRangeStr(): string { + const tl = this.getActiveTopLeft(); + const br = this.getActiveBottomRight(); + + if (tl === null || br === null) return ''; + + // Single-cell selection + if (tl.equals(br)) return tl.str(); + + // Range selection + return `${tl.str()}:${br.str()}`; +} + + public getActivePos(): Position | null { - return this.active_cell; + if ( + this.primary_active === null || + this.secondary_active === null || + !this.primary_active.equals(this.secondary_active) + ) { + return null; + } + return this.primary_active; } public isActive(pos: Position): boolean { - if (this.active_cell == null) return false; - return this.active_cell.equals(pos); + if (this.primary_active === null || this.secondary_active === null) return false; + + return ( + pos.row >= Math.min(this.primary_active.row, this.secondary_active.row) && + pos.row <= Math.max(this.primary_active.row, this.secondary_active.row) && + pos.col >= Math.min(this.primary_active.col, this.secondary_active.col) && + pos.col <= Math.max(this.primary_active.col, this.secondary_active.col) + ); + } + + public getActiveTopLeft(): Position | null { + if (this.primary_active === null || this.secondary_active === null) return null; + + return new Position( + Math.min(this.primary_active.row, this.secondary_active.row), + Math.min(this.primary_active.col, this.secondary_active.col) + ); + } + + public getActiveBottomRight(): Position | null { + if (this.primary_active === null || this.secondary_active === null) return null; + + return new Position( + Math.max(this.primary_active.row, this.secondary_active.row), + Math.max(this.primary_active.col, this.secondary_active.col) + ); + } + + public isPrimaryActive(pos: Position): boolean { + if (this.primary_active === null) return false; + return this.primary_active.equals(pos); + } + + public anyIsActive(): boolean { + return this.primary_active !== null && this.secondary_active !== null; } public quickEval(pos: Position | null, raw: string) { @@ -214,6 +317,7 @@ class Grid { this.data[pos.key()] = { raw: msg.raw ?? '', eval: msg.eval, + pos: pos, temp_raw: x?.temp_raw ?? '', temp_eval: x?.temp_eval ?? undefined }; diff --git a/frontend/src/lib/components/grid/messages.ts b/frontend/src/lib/components/grid/messages.ts index 6fb9fb2..1d31638 100644 --- a/frontend/src/lib/components/grid/messages.ts +++ b/frontend/src/lib/components/grid/messages.ts @@ -1,3 +1,5 @@ +import type { Position } from "./grid.svelte.ts"; + interface LeadMsg { msg_type: 'set' | 'get' | 'error' | 'bulk' | 'eval'; cell?: CellRef; @@ -48,6 +50,7 @@ type Eval = interface CellT { raw: string; temp_raw: string; + pos: Position; temp_eval?: Eval; eval?: Eval; } diff --git a/frontend/src/lib/components/ui/alert/alert-description.svelte b/frontend/src/lib/components/ui/alert/alert-description.svelte new file mode 100644 index 0000000..8b56aed --- /dev/null +++ b/frontend/src/lib/components/ui/alert/alert-description.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/frontend/src/lib/components/ui/alert/alert-title.svelte b/frontend/src/lib/components/ui/alert/alert-title.svelte new file mode 100644 index 0000000..77e45ad --- /dev/null +++ b/frontend/src/lib/components/ui/alert/alert-title.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/frontend/src/lib/components/ui/alert/alert.svelte b/frontend/src/lib/components/ui/alert/alert.svelte new file mode 100644 index 0000000..2b2eff9 --- /dev/null +++ b/frontend/src/lib/components/ui/alert/alert.svelte @@ -0,0 +1,44 @@ + + + + + diff --git a/frontend/src/lib/components/ui/alert/index.ts b/frontend/src/lib/components/ui/alert/index.ts new file mode 100644 index 0000000..97e21b4 --- /dev/null +++ b/frontend/src/lib/components/ui/alert/index.ts @@ -0,0 +1,14 @@ +import Root from "./alert.svelte"; +import Description from "./alert-description.svelte"; +import Title from "./alert-title.svelte"; +export { alertVariants, type AlertVariant } from "./alert.svelte"; + +export { + Root, + Description, + Title, + // + Root as Alert, + Description as AlertDescription, + Title as AlertTitle, +}; From 4df702fb7931cfb805b378b6c9db544c020427ba Mon Sep 17 00:00:00 2001 From: Lloyd Date: Thu, 11 Sep 2025 22:25:06 +1000 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=99=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/lib/components/grid/cell.svelte | 51 ++++++++++++++++--- frontend/src/lib/components/grid/grid.svelte | 23 ++++++--- .../src/lib/components/grid/grid.svelte.ts | 49 ++++++++++++++---- 3 files changed, 101 insertions(+), 22 deletions(-) diff --git a/frontend/src/lib/components/grid/cell.svelte b/frontend/src/lib/components/grid/cell.svelte index 903cdcb..b15dd04 100644 --- a/frontend/src/lib/components/grid/cell.svelte +++ b/frontend/src/lib/components/grid/cell.svelte @@ -102,12 +102,29 @@ {onmousedown} data-row={pos.row} data-col={pos.col} + ondragstart={(e) => e.preventDefault()} style:width style:height - class={clsx('placeholder bg-background p-1', { primaryactive, active }, cla)} + class={clsx( + 'placeholder bg-background p-1', + { + primaryactive, + active, + 'active-top': grid.isActiveTop(pos), + 'active-bottom': grid.isActiveBottom(pos), + 'active-right': grid.isActiveRight(pos), + 'active-left': grid.isActiveLeft(pos), + 'only-active': grid.isActive(pos) && grid.isSingleActive() + }, + cla + )} > {#if cell && (cell.raw !== '' || getEvalLiteral(cell.eval) !== '')} - + {#if externalediting} {cell.temp_raw} {:else if cell.eval} @@ -129,19 +146,39 @@ } .primaryactive { - z-index: 30; - border: 1px solid var(--color-primary); + z-index: 30 !important; + border: 1px solid var(--color-primary) !important; outline: 1px solid var(--color-primary); } .active { z-index: 20; - background-color: color-mix(in oklab, var(--color-primary) 50%, var(--color-background) 90%); - - /* border: 1px solid var(--color-primary); */ + background-color: color-mix(in oklab, var(--color-primary) 20%, var(--color-background) 80%); + border: 1px solid color-mix(in oklab, var(--input) 100%, var(--color-foreground) 5%); /* outline: 1px solid var(--color-primary); */ } + .only-active { + background-color: transparent !important; + } + + /* Borders for edges */ + .active-top { + border-top: 1px solid var(--color-primary); + } + + .active-bottom { + border-bottom: 1px solid var(--color-primary); + } + + .active-left { + border-left: 1px solid var(--color-primary); + } + + .active-right { + border-right: 1px solid var(--color-primary); + } + .active:has(.err), .placeholder:has(.err) { position: relative; /* needed for absolute positioning */ diff --git a/frontend/src/lib/components/grid/grid.svelte b/frontend/src/lib/components/grid/grid.svelte index 5b14eb1..9b51571 100644 --- a/frontend/src/lib/components/grid/grid.svelte +++ b/frontend/src/lib/components/grid/grid.svelte @@ -80,8 +80,6 @@ grid.stopEditingActive(); } }; - window.addEventListener('click', handler); - onDestroy(() => window.removeEventListener('click', handler)); const handleMouseMove = (e: MouseEvent) => { if (!dragging) return; @@ -92,20 +90,34 @@ const row = parseInt(el.dataset.row, 10); const col = parseInt(el.dataset.col, 10); - // expand selection as you drag - console.log(`moved to ${refToStr(row, col)}`); grid.setActive(grid.primary_active, new Position(row, col)); } }; - const handleMouseUp = () => { + const handleMouseUp = (e: MouseEvent) => { dragging = false; // stop tracking + // + // 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); + // + // // expand selection as you drag + // let pos = new Position(row, col); + // + // if (grid.isActive(pos) && grid.isEditing(pos)) return; + // + // grid.stopAnyEditing(); + // } }; + window.addEventListener('click', handler); window.addEventListener('mousemove', handleMouseMove); window.addEventListener('mouseup', handleMouseUp); onDestroy(() => { + window.removeEventListener('click', handler); window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('mouseup', handleMouseUp); }); @@ -216,7 +228,6 @@ height={grid.getRowHeight(i)} width={grid.getColWidth(j)} onmousedown={(e) => { - console.log(`down at ${refToStr(i, j)}`); handleCellMouseDown(i, j, e); }} /> diff --git a/frontend/src/lib/components/grid/grid.svelte.ts b/frontend/src/lib/components/grid/grid.svelte.ts index b71b542..e3cc20a 100644 --- a/frontend/src/lib/components/grid/grid.svelte.ts +++ b/frontend/src/lib/components/grid/grid.svelte.ts @@ -126,6 +126,30 @@ class Grid { return this.default_row_height; } + public isActiveTop(pos: Position): boolean { + const tl = this.getActiveTopLeft(); + if (!tl) return false; + return this.isActive(pos) && pos.row === tl.row; + } + + public isActiveBottom(pos: Position): boolean { + const br = this.getActiveBottomRight(); + if (!br) return false; + return this.isActive(pos) && pos.row === br.row; + } + + public isActiveLeft(pos: Position): boolean { + const tl = this.getActiveTopLeft(); + if (!tl) return false; + return this.isActive(pos) && pos.col === tl.col; + } + + public isActiveRight(pos: Position): boolean { + const br = this.getActiveBottomRight(); + if (!br) return false; + return this.isActive(pos) && pos.col === br.col; + } + public setRowHeight(row: number, height: string) { if (height === this.default_row_height) { delete this.row_heights[row]; @@ -159,6 +183,10 @@ class Grid { // this.setCell(pos); } + public stopAnyEditing() { + this.editing_cell = null; + } + public stopEditingActive() { if (!this.anyIsActive() || !this.primary_active?.equals(this.secondary_active)) return; this.stopEditing(this.primary_active); @@ -219,18 +247,17 @@ class Grid { } public getActiveRangeStr(): string { - const tl = this.getActiveTopLeft(); - const br = this.getActiveBottomRight(); + const tl = this.getActiveTopLeft(); + const br = this.getActiveBottomRight(); - if (tl === null || br === null) return ''; + if (tl === null || br === null) return ''; - // Single-cell selection - if (tl.equals(br)) return tl.str(); - - // Range selection - return `${tl.str()}:${br.str()}`; -} + // Single-cell selection + if (tl.equals(br)) return tl.str(); + // Range selection + return `${tl.str()}:${br.str()}`; + } public getActivePos(): Position | null { if ( @@ -277,6 +304,10 @@ class Grid { return this.primary_active.equals(pos); } + public isSingleActive(): boolean { + return this.getActivePos() !== null; + } + public anyIsActive(): boolean { return this.primary_active !== null && this.secondary_active !== null; }