diff options
| author | Josh Kingsley <josh@joshkingsley.me> | 2025-10-29 01:54:37 +0200 |
|---|---|---|
| committer | Josh Kingsley <josh@joshkingsley.me> | 2025-10-29 01:54:37 +0200 |
| commit | 986e65f9ab7122995ae1d647df23d817cecf6816 (patch) | |
| tree | 39d4f77c6a6565b59522a5a77e2f550334472713 /web/src/components/grid/index.ts | |
| parent | 95069f13d908bfd3c0f3b33f8fad7d8464fd192e (diff) | |
refactor(web): improve state management
Diffstat (limited to 'web/src/components/grid/index.ts')
| -rw-r--r-- | web/src/components/grid/index.ts | 223 |
1 files changed, 154 insertions, 69 deletions
diff --git a/web/src/components/grid/index.ts b/web/src/components/grid/index.ts index 204ebea..2c00eb8 100644 --- a/web/src/components/grid/index.ts +++ b/web/src/components/grid/index.ts @@ -1,119 +1,189 @@ import h, { type CreateElement } from "../../html"; +import { ActiveCellSelection, Selection } from "../../selection"; import { CellRef } from "../../types"; import cellAtCoord from "./cellAtCoord"; -import drawGrid, { getGridStyles } from "./drawGrid"; +import drawGrid, { GridStyles } from "./drawGrid"; +import drawSelection, { SelectionStyles } from "./drawSelection"; import "./index.css"; -import { getRenderedCell } from "./renderGrid"; +import { getRenderedCell, RenderedGrid } from "./renderGrid"; -class NotiveGridElement extends HTMLElement { - #gridId!: string; +export class NotiveGridElement extends HTMLElement { + #internals: ElementInternals = this.attachInternals(); - get gridId() { - return this.#gridId; - } + grid?: RenderedGrid; + + #selection?: Selection; - set gridId(val: string) { - this.#gridId = val; - this.setAttribute("grid-id", val); + get selection() { + return this.#selection; } - get renderedGrid() { - return window.notive.getGrid(this.#gridId)!; + set selection(selection: Selection | undefined) { + this.#selection = selection; + this.drawSelection(); } - #canvasEl: HTMLCanvasElement = h.canvas(); + #ongridselectionchange?: ((event: GridSelectionEvent) => any) | undefined; - connectedCallback() { - if (!this.gridId) { - throw new Error("ntv-grid requries gridId attribute"); + get ongridselectionchange() { + return this.#ongridselectionchange; + } + + set ongridselectionchange( + handler: ((event: GridSelectionEvent) => any) | undefined, + ) { + if (this.#ongridselectionchange) { + this.removeEventListener( + "ntv:grid:selectionchange", + this.#ongridselectionchange, + ); } - window.addEventListener("ntv:selectionchange", () => this.draw()); - window.addEventListener("ntv:grid:change", () => this.draw()); + this.#ongridselectionchange = handler; - this.#canvasEl.addEventListener( - "mousedown", - this.#canvasMouseDownCallback.bind(this), - ); + if (handler) { + this.addEventListener("ntv:grid:selectionchange", handler); + } + } - this.#canvasEl.addEventListener( - "dblclick", - this.#canvasDoubleClickCallback.bind(this), - ); + canvas: HTMLCanvasElement = h.canvas({ + onmousedown: (event) => { + if (!this.grid) return; + const cellRef = this.#mouseEventCellRef(event); + if (!cellRef) return; + this.startSelecting(cellRef); + }, + }); - this.append(this.#canvasEl); + selectionCanvas: HTMLCanvasElement = h.canvas({ + dataset: { selection: "true" }, + }); + connectedCallback() { + this.append(this.canvas, this.selectionCanvas); this.draw(); + this.drawSelection(); } draw() { - const ctx = this.#canvasEl.getContext("2d"); + if (!this.grid) return; + + const ctx = this.canvas.getContext("2d"); if (!ctx) throw new Error("Unable to get canvas context"); - const grid = window.notive.getGrid(this.gridId); + this.canvas.setAttribute("width", this.grid.rect.width + "px"); + this.canvas.setAttribute("height", this.grid.rect.height + "px"); - if (!grid) return; + drawGrid(ctx, this.getGridStyles(), this.grid); + } - this.#canvasEl.setAttribute("width", grid.rect.width + "px"); - this.#canvasEl.setAttribute("height", grid.rect.height + "px"); + drawSelection() { + if (!this.grid) return; - const styles = getGridStyles(this); + const ctx = this.selectionCanvas.getContext("2d"); - drawGrid( + if (!ctx) throw new Error("Unable to get canvas context"); + + this.selectionCanvas.setAttribute("width", this.grid.rect.width + "px"); + this.selectionCanvas.setAttribute("height", this.grid.rect.height + "px"); + + drawSelection( ctx, - styles, - grid, - window.notive.selection, - window.notive.pendingSelection, + this.getSelectionStyles(), + this.grid, + this.#pendingSelection ?? this.selection, + { + pending: !!this.#pendingSelection, + }, ); } - #mouseEventCellRef( - this: NotiveGridElement, - event: MouseEvent, - ): CellRef | undefined { - const clientRect = this.#canvasEl.getBoundingClientRect(); - const x = event.x - clientRect.x; - const y = event.y - clientRect.y; - return cellAtCoord(this.renderedGrid, x, y); + getGridStyles(): GridStyles { + const style = window.getComputedStyle(this); + const val = (k: string) => style.getPropertyValue(k); + + return { + bgFill: val("--grid-bg-fill"), + borderStroke: val("--grid-border-stroke"), + cellStroke: val("--grid-cell-stroke"), + cellValueFont: val("font"), + cellValueLineHeight: val("line-height"), + }; } - #canvasMouseDownCallback(this: NotiveGridElement, event: MouseEvent) { - const cellRef = this.#mouseEventCellRef(event); + getSelectionStyles(): SelectionStyles { + const style = window.getComputedStyle(this); + const val = (k: string) => style.getPropertyValue(k); - if (!cellRef) return; + return { + activeCellStroke: val("--grid-active-cell-stroke"), + selectionRangeFill: val("--grid-selection-range-fill"), + selectionRangeStroke: val("--grid-selection-range-stroke"), + }; + } + + #pendingSelection?: Selection; + #selectionAbortController?: AbortController; - window.notive.startSelecting(this.gridId, cellRef); + startSelecting(cellRef: CellRef) { + if (!this.grid || this.#pendingSelection) return; + + this.#internals.states.add("selecting"); this.#selectionAbortController = new AbortController(); + + this.#selectionAbortController.signal.addEventListener("abort", () => { + this.#internals.states.delete("selecting"); + this.#selectionAbortController = undefined; + }); + const { signal } = this.#selectionAbortController; window.addEventListener( "mousemove", - this.#selectionMouseMoveCallback.bind(this), + (event) => { + const cellRef = this.#mouseEventCellRef(event); + if (!cellRef) return; + this.#pendingSelection = this.#pendingSelection?.extend(cellRef); + this.drawSelection(); + }, { signal }, ); window.addEventListener( "mouseup", - this.#selectionMouseUpCallback.bind(this), + () => { + this.#selectionAbortController?.abort(); + + if (!this.#pendingSelection) return; + + this.dispatchEvent( + new GridSelectionEvent( + "ntv:grid:selectionchange", + this.#pendingSelection, + ), + ); + + this.#pendingSelection = undefined; + this.drawSelection(); + }, { signal }, ); - } - #selectionAbortController?: AbortController; - - #selectionMouseMoveCallback(this: NotiveGridElement, event: MouseEvent) { - const cellRef = this.#mouseEventCellRef(event); - if (!cellRef) return; - window.notive.extendSelection(cellRef); + this.#pendingSelection = new ActiveCellSelection(this.grid.id, cellRef); + this.drawSelection(); } - #selectionMouseUpCallback(this: NotiveGridElement, event: MouseEvent) { - this.#selectionAbortController?.abort(); - this.#selectionAbortController = undefined; - window.notive.finishSelecting(); + #mouseEventCellRef( + this: NotiveGridElement, + event: MouseEvent, + ): CellRef | undefined { + if (!this.grid) return; + const clientRect = this.canvas.getBoundingClientRect(); + const x = event.x - clientRect.x; + const y = event.y - clientRect.y; + return cellAtCoord(this.grid, x, y); } #editingCellRef?: CellRef; @@ -137,15 +207,13 @@ class NotiveGridElement extends HTMLElement { }); #canvasDoubleClickCallback(this: NotiveGridElement, event: MouseEvent) { + if (!this.grid) return; + const cellRef = this.#mouseEventCellRef(event); if (!cellRef) return; - const grid = window.notive.getGrid(this.gridId); - - if (!grid) return; - - const cell = getRenderedCell(grid, cellRef); + const cell = getRenderedCell(this.grid, cellRef); if (!cell) return; @@ -168,8 +236,10 @@ class NotiveGridElement extends HTMLElement { #finishEditing() { this.#editInputEl.remove(); + if (!this.grid) return; + window.notive.setCellValue( - this.gridId, + this.grid.id, this.#editingCellRef!, this.#editInputEl.value, ); @@ -180,3 +250,18 @@ customElements.define("ntv-grid", NotiveGridElement); export default ((...args: any[]): NotiveGridElement => (h as any)["ntv-grid"](...args)) as CreateElement<NotiveGridElement>; + +export class GridSelectionEvent extends Event { + selection: Selection; + + constructor(type: string, selection: Selection) { + super(type); + this.selection = selection; + } +} + +declare global { + interface HTMLElementEventMap { + "ntv:grid:selectionchange": GridSelectionEvent; + } +} |
