From 41b9d5840ddb6b484fc9f309a95d80f62832cfeb Mon Sep 17 00:00:00 2001 From: Josh Kingsley Date: Sun, 26 Oct 2025 22:59:39 +0200 Subject: feat(web): editing cells --- web/src/components/grid/index.css | 15 ++++++++ web/src/components/grid/index.ts | 80 +++++++++++++++++++++++++++++++++++---- web/src/index.css | 1 + web/src/index.ts | 28 +++++++++++++- 4 files changed, 115 insertions(+), 9 deletions(-) diff --git a/web/src/components/grid/index.css b/web/src/components/grid/index.css index 477d40e..93d6f6f 100644 --- a/web/src/components/grid/index.css +++ b/web/src/components/grid/index.css @@ -1,5 +1,6 @@ ntv-grid { display: block; + position: relative; --grid-bg-fill: var(--color-neutral-900); --grid-border-stroke: var(--color-neutral-700); @@ -17,3 +18,17 @@ ntv-grid { ntv-grid > canvas { display: block; } + +ntv-grid input[data-edit-cell] { + position: absolute; + vertical-align: baseline; + background: var(--color-neutral-800); + padding-right: 1px; + padding-bottom: 1px; + color: white; + text-align: center; +} + +ntv-grid input[data-edit-cell]:focus-visible { + outline: none; +} diff --git a/web/src/components/grid/index.ts b/web/src/components/grid/index.ts index 7d12a84..204ebea 100644 --- a/web/src/components/grid/index.ts +++ b/web/src/components/grid/index.ts @@ -3,6 +3,7 @@ import { CellRef } from "../../types"; import cellAtCoord from "./cellAtCoord"; import drawGrid, { getGridStyles } from "./drawGrid"; import "./index.css"; +import { getRenderedCell } from "./renderGrid"; class NotiveGridElement extends HTMLElement { #gridId!: string; @@ -20,7 +21,7 @@ class NotiveGridElement extends HTMLElement { return window.notive.getGrid(this.#gridId)!; } - canvasEl: HTMLCanvasElement = h.canvas(); + #canvasEl: HTMLCanvasElement = h.canvas(); connectedCallback() { if (!this.gridId) { @@ -30,17 +31,23 @@ class NotiveGridElement extends HTMLElement { window.addEventListener("ntv:selectionchange", () => this.draw()); window.addEventListener("ntv:grid:change", () => this.draw()); - this.canvasEl.addEventListener( + this.#canvasEl.addEventListener( "mousedown", this.#canvasMouseDownCallback.bind(this), ); - this.append(this.canvasEl); + this.#canvasEl.addEventListener( + "dblclick", + this.#canvasDoubleClickCallback.bind(this), + ); + + this.append(this.#canvasEl); + this.draw(); } draw() { - const ctx = this.canvasEl.getContext("2d"); + const ctx = this.#canvasEl.getContext("2d"); if (!ctx) throw new Error("Unable to get canvas context"); @@ -48,8 +55,8 @@ class NotiveGridElement extends HTMLElement { if (!grid) return; - this.canvasEl.setAttribute("width", grid.rect.width + "px"); - this.canvasEl.setAttribute("height", grid.rect.height + "px"); + this.#canvasEl.setAttribute("width", grid.rect.width + "px"); + this.#canvasEl.setAttribute("height", grid.rect.height + "px"); const styles = getGridStyles(this); @@ -66,7 +73,7 @@ class NotiveGridElement extends HTMLElement { this: NotiveGridElement, event: MouseEvent, ): CellRef | undefined { - const clientRect = this.canvasEl.getBoundingClientRect(); + const clientRect = this.#canvasEl.getBoundingClientRect(); const x = event.x - clientRect.x; const y = event.y - clientRect.y; return cellAtCoord(this.renderedGrid, x, y); @@ -108,6 +115,65 @@ class NotiveGridElement extends HTMLElement { this.#selectionAbortController = undefined; window.notive.finishSelecting(); } + + #editingCellRef?: CellRef; + + #editInputEl: HTMLInputElement = h.input({ + dataset: { editCell: "true" }, + onblur: () => { + this.#finishEditing(); + }, + onkeydown: (event) => { + switch (event.key) { + case "Enter": + this.#finishEditing(); + break; + + case "Escape": + this.#cancelEditing(); + break; + } + }, + }); + + #canvasDoubleClickCallback(this: NotiveGridElement, event: MouseEvent) { + const cellRef = this.#mouseEventCellRef(event); + + if (!cellRef) return; + + const grid = window.notive.getGrid(this.gridId); + + if (!grid) return; + + const cell = getRenderedCell(grid, cellRef); + + if (!cell) return; + + this.#editingCellRef = cellRef; + + this.append(this.#editInputEl); + + this.#editInputEl.value = cell.value || ""; + this.#editInputEl.style.left = cell.rect.topLeft.x + 2 + "px"; + this.#editInputEl.style.top = cell.rect.topLeft.y + 2 + "px"; + this.#editInputEl.style.width = cell.rect.width - 3 + "px"; + this.#editInputEl.style.height = cell.rect.height - 3 + "px"; + this.#editInputEl.focus(); + } + + #cancelEditing() { + this.#editInputEl.remove(); + } + + #finishEditing() { + this.#editInputEl.remove(); + + window.notive.setCellValue( + this.gridId, + this.#editingCellRef!, + this.#editInputEl.value, + ); + } } customElements.define("ntv-grid", NotiveGridElement); diff --git a/web/src/index.css b/web/src/index.css index bd5ebf3..f100378 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -2,6 +2,7 @@ body { background: var(--color-neutral-800); + user-select: none; } *:focus-visible { diff --git a/web/src/index.ts b/web/src/index.ts index f08aedc..97cfdf8 100644 --- a/web/src/index.ts +++ b/web/src/index.ts @@ -7,9 +7,8 @@ import renderGrid, { } from "./components/grid/renderGrid"; function defaultDoc(): Doc { - const defaultCells: Cell[] = Array.from({ length: 16 }, (_, i) => ({ + const defaultCells: Cell[] = Array.from({ length: 16 }, () => ({ widthRatio: new Ratio(1, 16), - value: i.toString(), })); return { @@ -185,6 +184,31 @@ export default class Notive { }), ); } + + setCellValue(gridId: string, cellRef: CellRef, value: string | undefined) { + const grid = this.doc.grids.find((grid) => grid.id === gridId); + + if (!grid) return; + + const cell = + grid.parts[cellRef.partIndex].rows[cellRef.rowIndex].cells[ + cellRef.cellIndex + ]; + + grid.parts[cellRef.partIndex].rows[cellRef.rowIndex].cells[ + cellRef.cellIndex + ] = { ...cell, value }; + + this.#gridsById = Object.fromEntries( + this.#doc.grids.map((grid) => [grid.id, renderGrid(grid)]), + ); + + window.dispatchEvent( + new CustomEvent("ntv:grid:change", { + detail: { gridId }, + }), + ); + } } window.notive = new Notive(); -- cgit v1.2.3