🙃
This commit is contained in:
@@ -23,13 +23,8 @@ impl Grid {
|
|||||||
&mut self,
|
&mut self,
|
||||||
cell_ref: CellRef,
|
cell_ref: CellRef,
|
||||||
raw_val: String,
|
raw_val: String,
|
||||||
do_propagation: bool,
|
|
||||||
force_propagation: bool,
|
|
||||||
) -> Result<Vec<CellRef>, String> {
|
) -> Result<Vec<CellRef>, String> {
|
||||||
if self.cells.contains_key(&cell_ref)
|
if self.cells.contains_key(&cell_ref) && self.cells[&cell_ref].raw() == raw_val {
|
||||||
&& self.cells[&cell_ref].raw() == raw_val
|
|
||||||
&& !force_propagation
|
|
||||||
{
|
|
||||||
return Ok(Vec::new());
|
return Ok(Vec::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,14 +43,7 @@ impl Grid {
|
|||||||
|
|
||||||
if self.cells.contains_key(&cell_ref) {
|
if self.cells.contains_key(&cell_ref) {
|
||||||
updated_cells = self
|
updated_cells = self
|
||||||
.update_exisiting_cell(
|
.update_exisiting_cell(raw_val, eval, precs, cell_ref)?
|
||||||
raw_val,
|
|
||||||
eval,
|
|
||||||
precs,
|
|
||||||
cell_ref,
|
|
||||||
do_propagation,
|
|
||||||
force_propagation,
|
|
||||||
)?
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.chain(updated_cells)
|
.chain(updated_cells)
|
||||||
.collect();
|
.collect();
|
||||||
@@ -66,6 +54,15 @@ impl Grid {
|
|||||||
Ok(updated_cells)
|
Ok(updated_cells)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn quick_eval(&mut self, raw_val: String) -> Eval {
|
||||||
|
if raw_val.chars().nth(0) != Some('=') {
|
||||||
|
Eval::Literal(Literal::String(raw_val.to_owned()))
|
||||||
|
} else {
|
||||||
|
let (res_eval, ..) = evaluate(raw_val[1..].to_owned(), Some(&self));
|
||||||
|
res_eval
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_cell(&self, cell_ref: CellRef) -> Result<Cell, String> {
|
pub fn get_cell(&self, cell_ref: CellRef) -> Result<Cell, String> {
|
||||||
if !self.cells.contains_key(&cell_ref) {
|
if !self.cells.contains_key(&cell_ref) {
|
||||||
return Err(format!("Cell at {:?} not found.", cell_ref));
|
return Err(format!("Cell at {:?} not found.", cell_ref));
|
||||||
@@ -168,8 +165,6 @@ impl Grid {
|
|||||||
new_eval: Eval,
|
new_eval: Eval,
|
||||||
new_precs: HashSet<CellRef>,
|
new_precs: HashSet<CellRef>,
|
||||||
cell_ref: CellRef,
|
cell_ref: CellRef,
|
||||||
do_propagation: bool,
|
|
||||||
force_propagation: bool,
|
|
||||||
) -> Result<Vec<CellRef>, String> {
|
) -> Result<Vec<CellRef>, String> {
|
||||||
let (old_precs, old_eval) = match self.cells.get_mut(&cell_ref) {
|
let (old_precs, old_eval) = match self.cells.get_mut(&cell_ref) {
|
||||||
Some(cell) => {
|
Some(cell) => {
|
||||||
@@ -211,7 +206,7 @@ impl Grid {
|
|||||||
cell.set_precs(new_precs);
|
cell.set_precs(new_precs);
|
||||||
cell.set_eval(new_eval);
|
cell.set_eval(new_eval);
|
||||||
|
|
||||||
if (eval_changed && do_propagation) || force_propagation {
|
if eval_changed {
|
||||||
self.propagate(cell_ref)
|
self.propagate(cell_ref)
|
||||||
} else {
|
} else {
|
||||||
Ok(Vec::new())
|
Ok(Vec::new())
|
||||||
|
|||||||
@@ -62,16 +62,9 @@ async fn accept_connection(stream: TcpStream) {
|
|||||||
MsgType::Set => {
|
MsgType::Set => {
|
||||||
let Some(cell_ref) = req.cell else { continue };
|
let Some(cell_ref) = req.cell else { continue };
|
||||||
let Some(raw) = req.raw else { continue };
|
let Some(raw) = req.raw else { continue };
|
||||||
let Some(config) = req.eval_config else {
|
// let config = req.eval_config.unwrap_or_default();
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
match grid.update_cell(
|
match grid.update_cell(cell_ref.clone(), raw.to_owned()) {
|
||||||
cell_ref.clone(),
|
|
||||||
raw.to_owned(),
|
|
||||||
config.do_propagation,
|
|
||||||
config.force_propagation,
|
|
||||||
) {
|
|
||||||
Ok(updates) => {
|
Ok(updates) => {
|
||||||
let mut msgs = Vec::new();
|
let mut msgs = Vec::new();
|
||||||
|
|
||||||
@@ -122,6 +115,25 @@ async fn accept_connection(stream: TcpStream) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
MsgType::Eval => {
|
||||||
|
let Some(cell_ref) = req.cell else { continue };
|
||||||
|
let Some(raw) = req.raw else { continue };
|
||||||
|
|
||||||
|
let eval = grid.quick_eval(raw.to_owned());
|
||||||
|
|
||||||
|
let msg = LeadMsg {
|
||||||
|
msg_type: MsgType::Eval,
|
||||||
|
cell: Some(cell_ref),
|
||||||
|
raw: Some(raw),
|
||||||
|
eval: Some(eval),
|
||||||
|
bulk_msgs: None,
|
||||||
|
eval_config: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = write
|
||||||
|
.send(serde_json::to_string(&msg).unwrap().into())
|
||||||
|
.await;
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
continue; // handle other cases
|
continue; // handle other cases
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use crate::{cell::CellRef, evaluator::Eval};
|
|||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum MsgType {
|
pub enum MsgType {
|
||||||
Set,
|
Set,
|
||||||
|
Eval,
|
||||||
Get,
|
Get,
|
||||||
Error,
|
Error,
|
||||||
Bulk,
|
Bulk,
|
||||||
@@ -17,6 +18,15 @@ pub struct EvalConfig {
|
|||||||
pub force_propagation: bool,
|
pub force_propagation: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for EvalConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
EvalConfig {
|
||||||
|
do_propagation: true,
|
||||||
|
force_propagation: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct LeadMsg {
|
pub struct LeadMsg {
|
||||||
pub msg_type: MsgType,
|
pub msg_type: MsgType,
|
||||||
|
|||||||
@@ -102,10 +102,10 @@
|
|||||||
border-left: none;
|
border-left: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder.blank,
|
/* .placeholder.blank, */
|
||||||
.placeholder.col {
|
/* .placeholder.col { */
|
||||||
border-top: none;
|
/* border-top: none; */
|
||||||
}
|
/* } */
|
||||||
|
|
||||||
.active {
|
.active {
|
||||||
background-color: color-mix(in oklab, var(--color-primary) 80%, var(--color-background) 80%);
|
background-color: color-mix(in oklab, var(--color-primary) 80%, var(--color-background) 80%);
|
||||||
|
|||||||
@@ -13,7 +13,8 @@
|
|||||||
startediting = () => {},
|
startediting = () => {},
|
||||||
stopediting = () => {},
|
stopediting = () => {},
|
||||||
active = false,
|
active = false,
|
||||||
editing = false
|
editing = false,
|
||||||
|
externalediting = false
|
||||||
}: {
|
}: {
|
||||||
cla?: string;
|
cla?: string;
|
||||||
width?: string;
|
width?: string;
|
||||||
@@ -24,6 +25,7 @@
|
|||||||
stopediting?: () => void;
|
stopediting?: () => void;
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
editing?: boolean;
|
editing?: boolean;
|
||||||
|
externalediting?: boolean;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
// focus the first focusable descendant (the inner <input>)
|
// focus the first focusable descendant (the inner <input>)
|
||||||
@@ -101,7 +103,7 @@
|
|||||||
>
|
>
|
||||||
{#if cell && (cell.raw_val !== '' || getEvalLiteral(cell.val) !== '')}
|
{#if cell && (cell.raw_val !== '' || getEvalLiteral(cell.val) !== '')}
|
||||||
<span class={clsx('pointer-events-none select-none', { err: isErr(cell.val) })}>
|
<span class={clsx('pointer-events-none select-none', { err: isErr(cell.val) })}>
|
||||||
{#if cell.val}
|
{#if cell.val && !externalediting}
|
||||||
{getEvalLiteral(cell.val)}
|
{getEvalLiteral(cell.val)}
|
||||||
{:else}
|
{:else}
|
||||||
{cell.raw_val}
|
{cell.raw_val}
|
||||||
|
|||||||
0
frontend/src/lib/components/grid/cell.ts
Normal file
0
frontend/src/lib/components/grid/cell.ts
Normal file
@@ -1,21 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { toast } from 'svelte-sonner';
|
import { Omega } from '@lucide/svelte';
|
||||||
import {
|
|
||||||
Infinity,
|
|
||||||
Omega,
|
|
||||||
Parentheses,
|
|
||||||
Pyramid,
|
|
||||||
Radical,
|
|
||||||
Sigma,
|
|
||||||
SquareFunction,
|
|
||||||
Variable
|
|
||||||
} from '@lucide/svelte';
|
|
||||||
import Cell from '$lib/components/grid/cell.svelte';
|
import Cell from '$lib/components/grid/cell.svelte';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import CellHeader from './cell-header.svelte';
|
import CellHeader from './cell-header.svelte';
|
||||||
import { colToStr, refToStr, type CellT } from './utils';
|
import { colToStr, refToStr } from './utils';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { Input } from '../ui/input';
|
import { Input } from '../ui/input';
|
||||||
|
import { Grid, Position } from './grid';
|
||||||
|
import type { LeadMsg } from './messages';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
socket,
|
socket,
|
||||||
@@ -36,186 +28,47 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
handle_msg(res);
|
grid.handle_msg(res);
|
||||||
};
|
};
|
||||||
|
|
||||||
function handle_msg(msg: LeadMsg) {
|
|
||||||
switch (msg.msg_type) {
|
|
||||||
case 'error': {
|
|
||||||
toast.error('Error', {
|
|
||||||
description: msg.raw
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'set': {
|
|
||||||
if (msg.cell === undefined) {
|
|
||||||
console.error('Expected cell ref for SET msgponse from server.');
|
|
||||||
return;
|
|
||||||
} else if (msg.eval === undefined) {
|
|
||||||
console.error('Expected cell value for SET msgponse from server.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
grid_vals[key(msg.cell.row, msg.cell.col)] = {
|
|
||||||
raw_val: msg.raw ?? '',
|
|
||||||
val: msg.eval
|
|
||||||
};
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'bulk': {
|
|
||||||
if (msg.bulk_msgs === undefined) {
|
|
||||||
console.error('Expected bulk_msgs field to be defined for BULK message.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const m of msg.bulk_msgs) handle_msg(m);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let rows = 50;
|
let rows = 50;
|
||||||
let cols = 40;
|
let cols = 40;
|
||||||
|
|
||||||
let default_row_height = '30px';
|
let grid = $state(new Grid(socket));
|
||||||
let default_col_width = '80px';
|
|
||||||
|
|
||||||
// Only store touched cells
|
|
||||||
let grid_vals: Record<string, CellT> = $state({});
|
|
||||||
let row_heights: Record<number, string> = $state({});
|
|
||||||
let col_widths: Record<number, string> = $state({});
|
|
||||||
|
|
||||||
function getRowHeight(row: number) {
|
|
||||||
return row_heights[row] ?? default_row_height;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getColWidth(col: number) {
|
|
||||||
return col_widths[col] ?? default_col_width;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setRowHeight(row: number, height: string) {
|
|
||||||
if (height === default_row_height) {
|
|
||||||
delete row_heights[row];
|
|
||||||
} else {
|
|
||||||
row_heights[row] = height;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setColWidth(col: number, width: string) {
|
|
||||||
if (width === default_col_width) {
|
|
||||||
delete col_widths[col];
|
|
||||||
} else {
|
|
||||||
col_widths[col] = width;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let active_cell: [number, number] | null = $state(null);
|
|
||||||
let editing_cell: [number, number] | null = $state(null);
|
|
||||||
|
|
||||||
function startEditing(i: number, j: number) {
|
|
||||||
active_cell = [i, j];
|
|
||||||
editing_cell = [i, j];
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopEditing(i: number, j: number) {
|
|
||||||
editing_cell = null;
|
|
||||||
setCell(i, j, getCell(i, j), { do_propagation: true, force_propagation: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
const key = (i: number, j: number) => `${i}:${j}`;
|
|
||||||
|
|
||||||
const getCell = (i: number, j: number) => grid_vals[key(i, j)];
|
|
||||||
const setCell = (row: number, col: number, v: CellT, eval_config: EvalConfig) => {
|
|
||||||
if (v?.raw_val == null || v.raw_val === '') {
|
|
||||||
delete grid_vals[key(row, col)];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
grid_vals[key(row, col)] = {
|
|
||||||
raw_val: v.raw_val,
|
|
||||||
val: v.val
|
|
||||||
};
|
|
||||||
|
|
||||||
let msg: LeadMsg = {
|
|
||||||
msg_type: 'set',
|
|
||||||
cell: { row, col },
|
|
||||||
raw: v.raw_val,
|
|
||||||
eval_config
|
|
||||||
};
|
|
||||||
|
|
||||||
socket.send(JSON.stringify(msg));
|
|
||||||
};
|
|
||||||
|
|
||||||
function handleCellInteraction(i: number, j: number, e: MouseEvent) {
|
function handleCellInteraction(i: number, j: number, e: MouseEvent) {
|
||||||
if (editing_cell) {
|
let pos = new Position(i, j);
|
||||||
// Get the actual input element that's being edited
|
console.log('clicked', pos);
|
||||||
const el = document.querySelector<HTMLInputElement>('input:focus');
|
//
|
||||||
const currentInputValue = el?.value ?? '';
|
// if (grid.isEditing(pos)) {
|
||||||
|
// // Get the actual input element that's being edited
|
||||||
// ONLY treat this as a reference insert if it's a formula
|
// const el = document.querySelector<HTMLInputElement>('input:focus');
|
||||||
if (currentInputValue.trim().startsWith('=')) {
|
// const currentInputValue = el?.value ?? '';
|
||||||
// Prevent the input from losing focus
|
//
|
||||||
e.preventDefault();
|
// // ONLY treat this as a reference insert if it's a formula
|
||||||
|
// if (currentInputValue.trim().startsWith('=')) {
|
||||||
// --- This is the same reference-inserting logic as before ---
|
// // Prevent the input from losing focus
|
||||||
const ref = refToStr(i, j);
|
// e.preventDefault();
|
||||||
if (el) {
|
//
|
||||||
const { selectionStart, selectionEnd } = el;
|
// // --- This is the same reference-inserting logic as before ---
|
||||||
const before = el.value.slice(0, selectionStart ?? 0);
|
// const ref = refToStr(i, j);
|
||||||
const after = el.value.slice(selectionEnd ?? 0);
|
// if (el) {
|
||||||
el.value = before + ref + after;
|
// const { selectionStart, selectionEnd } = el;
|
||||||
const newPos = (selectionStart ?? 0) + ref.length;
|
// const before = el.value.slice(0, selectionStart ?? 0);
|
||||||
el.setSelectionRange(newPos, newPos);
|
// const after = el.value.slice(selectionEnd ?? 0);
|
||||||
el.dispatchEvent(new Event('input', { bubbles: true }));
|
// el.value = before + ref + after;
|
||||||
el.focus();
|
// const newPos = (selectionStart ?? 0) + ref.length;
|
||||||
}
|
// el.setSelectionRange(newPos, newPos);
|
||||||
|
// el.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
return;
|
// el.focus();
|
||||||
}
|
// }
|
||||||
}
|
//
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
// We are not editing, so this is a normal cell selection OR this is not a formula
|
// We are not editing, so this is a normal cell selection OR this is not a formula
|
||||||
active_cell = [i, j];
|
grid.setActive(pos);
|
||||||
}
|
|
||||||
|
|
||||||
function getActiveCell(): CellT {
|
|
||||||
if (active_cell != null && grid_vals[key(active_cell[0], active_cell[1])])
|
|
||||||
return grid_vals[key(active_cell[0], active_cell[1])];
|
|
||||||
else
|
|
||||||
return {
|
|
||||||
raw_val: '',
|
|
||||||
val: undefined
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function setActiveCellRaw(raw: string): void {
|
|
||||||
if (active_cell == null) return;
|
|
||||||
|
|
||||||
if (grid_vals[key(active_cell[0], active_cell[1])]) {
|
|
||||||
let cell = grid_vals[key(active_cell[0], active_cell[1])];
|
|
||||||
setCell(
|
|
||||||
active_cell[0],
|
|
||||||
active_cell[1],
|
|
||||||
{
|
|
||||||
raw_val: raw,
|
|
||||||
val: cell.val
|
|
||||||
},
|
|
||||||
{ do_propagation: false, force_propagation: false }
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setCell(
|
|
||||||
active_cell[0],
|
|
||||||
active_cell[1],
|
|
||||||
{
|
|
||||||
raw_val: raw,
|
|
||||||
val: undefined
|
|
||||||
},
|
|
||||||
{
|
|
||||||
do_propagation: false,
|
|
||||||
force_propagation: false
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
@@ -231,15 +84,33 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="relative mb-5 ml-5 flex items-center gap-[5px]">
|
<div class="relative mb-5 ml-5 flex items-center gap-[5px]">
|
||||||
<div class="relative">
|
<div
|
||||||
|
class="relative"
|
||||||
|
onkeydown={(e: KeyboardEvent) => {
|
||||||
|
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
|
||||||
|
} else if (e.key == 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
grid.stopEditingActive();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Omega
|
<Omega
|
||||||
size="20px"
|
size="20px"
|
||||||
class="absolute top-1/2 left-2 -translate-y-1/2 text-muted-foreground"
|
class="absolute top-1/2 left-2 z-10 -translate-y-1/2 text-muted-foreground"
|
||||||
strokeWidth={1}
|
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
bind:value={() => getActiveCell().raw_val, (raw) => setActiveCellRaw(raw)}
|
onmousedown={() => grid.setExternalEdit(grid.getActivePos())}
|
||||||
class="relative w-[200px] pl-8"
|
onblur={() => grid.setExternalEdit(null)}
|
||||||
|
bind:value={
|
||||||
|
() => grid.getActiveCell().raw_val, (raw) => grid.quickEval(grid.getActivePos(), raw)
|
||||||
|
}
|
||||||
|
class="relative w-[200px] pl-9"
|
||||||
></Input>
|
></Input>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -251,8 +122,8 @@
|
|||||||
<div class="sticky top-0 left-0" style="z-index: {rows + 70}">
|
<div class="sticky top-0 left-0" style="z-index: {rows + 70}">
|
||||||
<CellHeader
|
<CellHeader
|
||||||
resizeable={false}
|
resizeable={false}
|
||||||
height={default_row_height}
|
height={grid.getDefaultRowHeight()}
|
||||||
width={default_col_width}
|
width={grid.getDefaultColWidth()}
|
||||||
val=""
|
val=""
|
||||||
active={false}
|
active={false}
|
||||||
direction="blank"
|
direction="blank"
|
||||||
@@ -261,12 +132,12 @@
|
|||||||
|
|
||||||
{#each Array(cols) as _, j}
|
{#each Array(cols) as _, j}
|
||||||
<CellHeader
|
<CellHeader
|
||||||
height={default_row_height}
|
height={grid.getDefaultRowHeight()}
|
||||||
width={getColWidth(j)}
|
width={grid.getColWidth(j)}
|
||||||
setColWidth={(width) => setColWidth(j, width)}
|
setColWidth={(width) => grid.setColWidth(j, width)}
|
||||||
direction="col"
|
direction="col"
|
||||||
val={colToStr(j)}
|
val={colToStr(j)}
|
||||||
active={active_cell !== null && active_cell[1] === j}
|
active={grid.getActivePos() !== null && grid.getActivePos()?.col === j}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@@ -275,28 +146,31 @@
|
|||||||
<div class="sticky left-0 flex w-fit" style="z-index: {rows - i + 40}">
|
<div class="sticky left-0 flex w-fit" style="z-index: {rows - i + 40}">
|
||||||
<CellHeader
|
<CellHeader
|
||||||
direction="row"
|
direction="row"
|
||||||
width={default_col_width}
|
height={grid.getRowHeight(i)}
|
||||||
height={getRowHeight(i)}
|
width={grid.getDefaultColWidth()}
|
||||||
setRowHeight={(height) => setRowHeight(i, height)}
|
setRowHeight={(height) => grid.setRowHeight(i, height)}
|
||||||
val={(i + 1).toString()}
|
val={(i + 1).toString()}
|
||||||
active={active_cell !== null && active_cell[0] === i}
|
active={grid.getActivePos() !== null && grid.getActivePos()?.row === i}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{#each Array(cols) as _, j}
|
{#each Array(cols) as _, j}
|
||||||
<Cell
|
<Cell
|
||||||
height={getRowHeight(i)}
|
height={grid.getRowHeight(i)}
|
||||||
width={getColWidth(j)}
|
width={grid.getColWidth(j)}
|
||||||
editing={editing_cell?.[0] === i && editing_cell?.[1] === j}
|
editing={grid.isEditing(new Position(i, j))}
|
||||||
startediting={() => startEditing(i, j)}
|
externalediting={grid.isExternalEditing(new Position(i, j))}
|
||||||
stopediting={() => stopEditing(i, j)}
|
startediting={() => grid.startEditing(new Position(i, j))}
|
||||||
|
stopediting={() => grid.stopEditing(new Position(i, j))}
|
||||||
onmousedown={(e) => {
|
onmousedown={(e) => {
|
||||||
handleCellInteraction(i, j, e);
|
handleCellInteraction(i, j, e);
|
||||||
}}
|
}}
|
||||||
bind:cell={
|
bind:cell={
|
||||||
() => getCell(i, j),
|
() => grid.getCell(new Position(i, j)), (v) => grid.setCell(new Position(i, j), v)
|
||||||
(v) => setCell(i, j, v, { do_propagation: false, force_propagation: false })
|
|
||||||
}
|
}
|
||||||
active={active_cell !== null && active_cell[0] === i && active_cell[1] === j}
|
active={(() => {
|
||||||
|
console.log(grid.isActive(new Position(i, j)));
|
||||||
|
return grid.isActive(new Position(i, j));
|
||||||
|
})()}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
208
frontend/src/lib/components/grid/grid.ts
Normal file
208
frontend/src/lib/components/grid/grid.ts
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
import type { CellRef, CellT, LeadMsg } from './messages';
|
||||||
|
|
||||||
|
class Position {
|
||||||
|
public row: number;
|
||||||
|
public col: number;
|
||||||
|
|
||||||
|
constructor(row: number, col: number) {
|
||||||
|
this.row = row;
|
||||||
|
this.col = col;
|
||||||
|
}
|
||||||
|
|
||||||
|
public key() {
|
||||||
|
return `${this.row}:${this.col}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static key(row: number, col: number) {
|
||||||
|
return `${row}:${col}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ref(): CellRef {
|
||||||
|
return { row: this.row, col: this.col };
|
||||||
|
}
|
||||||
|
|
||||||
|
public equals(other: CellRef | null | undefined): boolean {
|
||||||
|
return !!other && this.row === other.row && this.col === other.col;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static equals(a: CellRef | null | undefined, b: CellRef | null | undefined): boolean {
|
||||||
|
return !!a && !!b && a.row === b.row && a.col === b.col;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Grid {
|
||||||
|
data: Record<string, CellT> = {};
|
||||||
|
socket: WebSocket;
|
||||||
|
row_heights: Record<number, string> = {};
|
||||||
|
col_widths: Record<number, string> = {};
|
||||||
|
default_row_height: string;
|
||||||
|
default_col_width: string;
|
||||||
|
active_cell: Position | null = null;
|
||||||
|
editing_cell: Position | null = null;
|
||||||
|
external_editing_cell: Position | null = null;
|
||||||
|
|
||||||
|
constructor(socket: WebSocket, default_col_width = '80px', default_row_height = '30px') {
|
||||||
|
this.socket = socket;
|
||||||
|
this.default_col_width = default_col_width;
|
||||||
|
this.default_row_height = default_row_height;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCell(pos: Position): CellT {
|
||||||
|
return this.data[pos.key()];
|
||||||
|
}
|
||||||
|
|
||||||
|
public setCell(pos: Position, v: CellT) {
|
||||||
|
if (v?.raw_val == null || v.raw_val === '') {
|
||||||
|
delete this.data[pos.key()];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.data[pos.key()] = {
|
||||||
|
raw_val: v.raw_val,
|
||||||
|
val: v.val
|
||||||
|
};
|
||||||
|
|
||||||
|
let msg: LeadMsg = {
|
||||||
|
msg_type: 'set',
|
||||||
|
cell: pos.ref(),
|
||||||
|
raw: v.raw_val
|
||||||
|
};
|
||||||
|
|
||||||
|
this.socket.send(JSON.stringify(msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
public getRowHeight(row: number) {
|
||||||
|
return this.row_heights[row] ?? this.default_row_height;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getColWidth(col: number) {
|
||||||
|
return this.col_widths[col] ?? this.default_col_width;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getDefaultColWidth() {
|
||||||
|
return this.default_col_width;
|
||||||
|
}
|
||||||
|
public getDefaultRowHeight() {
|
||||||
|
return this.default_row_height;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setRowHeight(row: number, height: string) {
|
||||||
|
if (height === this.default_row_height) {
|
||||||
|
delete this.row_heights[row];
|
||||||
|
} else {
|
||||||
|
this.row_heights[row] = height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public setColWidth(col: number, width: string) {
|
||||||
|
if (width === this.default_col_width) {
|
||||||
|
delete this.col_widths[col];
|
||||||
|
} else {
|
||||||
|
this.col_widths[col] = width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public startEditing(pos: Position) {
|
||||||
|
this.active_cell = pos;
|
||||||
|
this.editing_cell = pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
public stopEditing(pos: Position) {
|
||||||
|
this.editing_cell = null;
|
||||||
|
this.setCell(pos, this.getCell(pos));
|
||||||
|
}
|
||||||
|
|
||||||
|
public stopEditingActive() {
|
||||||
|
if (this.active_cell == null) return;
|
||||||
|
|
||||||
|
this.stopEditing(this.active_cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
public isEditing(pos: Position): boolean {
|
||||||
|
if (this.editing_cell == null) return false;
|
||||||
|
return this.editing_cell.equals(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
public isExternalEditing(pos: Position): boolean {
|
||||||
|
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 setExternalEdit(pos: Position | null) {
|
||||||
|
this.external_editing_cell = pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getActiveCell(): CellT {
|
||||||
|
if (this.active_cell === null) return {
|
||||||
|
raw_val: '',
|
||||||
|
val: undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.getCell(this.active_cell);
|
||||||
|
}
|
||||||
|
public getActivePos(): Position | null {
|
||||||
|
return this.active_cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isActive(pos: Position): boolean {
|
||||||
|
if (this.active_cell == null) return false;
|
||||||
|
return this.active_cell.equals(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
public quickEval(pos: Position | null, raw: string) {
|
||||||
|
if (pos === null) return;
|
||||||
|
|
||||||
|
let msg: LeadMsg = {
|
||||||
|
msg_type: 'eval',
|
||||||
|
cell: pos.ref(),
|
||||||
|
raw: raw
|
||||||
|
};
|
||||||
|
|
||||||
|
this.socket.send(JSON.stringify(msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
public handle_msg(msg: LeadMsg) {
|
||||||
|
switch (msg.msg_type) {
|
||||||
|
case 'error': {
|
||||||
|
toast.error('Error', {
|
||||||
|
description: msg.raw
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'set': {
|
||||||
|
if (msg.cell === undefined) {
|
||||||
|
console.error('Expected cell ref for SET msgponse from server.');
|
||||||
|
return;
|
||||||
|
} else if (msg.eval === undefined) {
|
||||||
|
console.error('Expected cell value for SET msgponse from server.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.data[Position.key(msg.cell.row, msg.cell.col)] = {
|
||||||
|
raw_val: msg.raw ?? '',
|
||||||
|
val: msg.eval
|
||||||
|
};
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'bulk': {
|
||||||
|
if (msg.bulk_msgs === undefined) {
|
||||||
|
console.error('Expected bulk_msgs field to be defined for BULK message.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const m of msg.bulk_msgs) this.handle_msg(m);
|
||||||
|
}
|
||||||
|
case 'eval': {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Grid, Position };
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
interface LeadMsg {
|
interface LeadMsg {
|
||||||
msg_type: 'set' | 'get' | 'error' | 'bulk';
|
msg_type: 'set' | 'get' | 'error' | 'bulk' | 'eval';
|
||||||
cell?: CellRef;
|
cell?: CellRef;
|
||||||
raw?: string;
|
raw?: string;
|
||||||
eval?: Eval;
|
eval?: Eval;
|
||||||
@@ -44,3 +44,10 @@ type Eval =
|
|||||||
| { range: Range }
|
| { range: Range }
|
||||||
| { err: LeadErr }
|
| { err: LeadErr }
|
||||||
| 'unset';
|
| 'unset';
|
||||||
|
|
||||||
|
interface CellT {
|
||||||
|
raw_val: string;
|
||||||
|
val?: Eval;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { Eval, LeadMsg, LeadErr, Literal, CellRef, LiteralValue, CellT };
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
export interface CellT {
|
import type { CellRef, Eval, LiteralValue } from './messages';
|
||||||
raw_val: string;
|
|
||||||
val?: Eval;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Zero indexed | A1 == {row: 0, col: 0};
|
* Zero indexed | A1 == {row: 0, col: 0};
|
||||||
|
|||||||
Reference in New Issue
Block a user