import h, { type CreateElement } from "../../html"; 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; get gridId() { return this.#gridId; } set gridId(val: string) { this.#gridId = val; this.setAttribute("grid-id", val); } get renderedGrid() { return window.notive.getGrid(this.#gridId)!; } #canvasEl: HTMLCanvasElement = h.canvas(); connectedCallback() { if (!this.gridId) { throw new Error("ntv-grid requries gridId attribute"); } window.addEventListener("ntv:selectionchange", () => this.draw()); window.addEventListener("ntv:grid:change", () => this.draw()); this.#canvasEl.addEventListener( "mousedown", this.#canvasMouseDownCallback.bind(this), ); this.#canvasEl.addEventListener( "dblclick", this.#canvasDoubleClickCallback.bind(this), ); this.append(this.#canvasEl); this.draw(); } draw() { const ctx = this.#canvasEl.getContext("2d"); if (!ctx) throw new Error("Unable to get canvas context"); const grid = window.notive.getGrid(this.gridId); if (!grid) return; this.#canvasEl.setAttribute("width", grid.rect.width + "px"); this.#canvasEl.setAttribute("height", grid.rect.height + "px"); const styles = getGridStyles(this); drawGrid( ctx, styles, grid, window.notive.selection, window.notive.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); } #canvasMouseDownCallback(this: NotiveGridElement, event: MouseEvent) { const cellRef = this.#mouseEventCellRef(event); if (!cellRef) return; window.notive.startSelecting(this.gridId, cellRef); this.#selectionAbortController = new AbortController(); const { signal } = this.#selectionAbortController; window.addEventListener( "mousemove", this.#selectionMouseMoveCallback.bind(this), { signal }, ); window.addEventListener( "mouseup", this.#selectionMouseUpCallback.bind(this), { signal }, ); } #selectionAbortController?: AbortController; #selectionMouseMoveCallback(this: NotiveGridElement, event: MouseEvent) { const cellRef = this.#mouseEventCellRef(event); if (!cellRef) return; window.notive.extendSelection(cellRef); } #selectionMouseUpCallback(this: NotiveGridElement, event: MouseEvent) { this.#selectionAbortController?.abort(); 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); export default ((...args: any[]): NotiveGridElement => (h as any)["ntv-grid"](...args)) as CreateElement;