summaryrefslogtreecommitdiff
path: root/web/src/components/grid/index.ts
diff options
context:
space:
mode:
Diffstat (limited to 'web/src/components/grid/index.ts')
-rw-r--r--web/src/components/grid/index.ts223
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;
+ }
+}