diff options
Diffstat (limited to 'packages/web/src/components/grid')
| -rw-r--r-- | packages/web/src/components/grid/cellAtCoord.ts | 40 | ||||
| -rw-r--r-- | packages/web/src/components/grid/drawGrid.ts | 91 | ||||
| -rw-r--r-- | packages/web/src/components/grid/drawSelection.ts | 97 | ||||
| -rw-r--r-- | packages/web/src/components/grid/excursion.ts | 8 | ||||
| -rw-r--r-- | packages/web/src/components/grid/index.css | 49 | ||||
| -rw-r--r-- | packages/web/src/components/grid/index.ts | 276 | ||||
| -rw-r--r-- | packages/web/src/components/grid/renderGrid.ts | 144 | ||||
| -rw-r--r-- | packages/web/src/components/grid/selection.ts | 28 |
8 files changed, 0 insertions, 733 deletions
diff --git a/packages/web/src/components/grid/cellAtCoord.ts b/packages/web/src/components/grid/cellAtCoord.ts deleted file mode 100644 index dd594a4..0000000 --- a/packages/web/src/components/grid/cellAtCoord.ts +++ /dev/null @@ -1,40 +0,0 @@ -import Coord from "../../math/Coord"; -import { CellRef } from "../../types"; -import { RenderedGrid, RenderedRow } from "./renderGrid"; - -function rowAtCoord(grid: RenderedGrid, coord: Coord): RenderedRow | undefined { - if (coord.y <= grid.rect.topLeft.y) { - return grid.renderedRows[0]; - } - - if (coord.y >= grid.rect.bottomRight.y) { - return grid.renderedRows.at(-1); - } - - return grid.renderedRows.find((row) => - row.rect.verticallyContainsCoord(coord), - ); -} - -export default function cellAtCoord( - grid: RenderedGrid, - x: number, - y: number, -): CellRef | undefined { - const coord = new Coord(x, y); - const row = rowAtCoord(grid, coord); - - if (!row) return; - - if (x <= row.rect.topLeft.x) { - return row.renderedCells[0]?.cellRef; - } - - if (x >= row.rect.bottomRight.x) { - return row.renderedCells.at(-1)?.cellRef; - } - - return row.renderedCells.find((cell) => - cell.rect.horizontallyContainsCoord(coord), - )?.cellRef; -} diff --git a/packages/web/src/components/grid/drawGrid.ts b/packages/web/src/components/grid/drawGrid.ts deleted file mode 100644 index da83c8e..0000000 --- a/packages/web/src/components/grid/drawGrid.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { RangeSelection, Selection } from "../../selection"; -import { CellRef } from "../../types"; -import { getRenderedCell, RenderedCell, RenderedGrid } from "./renderGrid"; - -export interface GridStyles { - bgFill: string; - borderStroke: string; - cellStroke: string; - cellValueFont: string; - cellValueLineHeight: string; -} - -function excursion(ctx: CanvasRenderingContext2D, f: () => void) { - ctx.save(); - f(); - ctx.restore(); -} - -function fillBackground( - ctx: CanvasRenderingContext2D, - styles: GridStyles, - grid: RenderedGrid, -) { - ctx.clearRect(0, 0, grid.rect.width, grid.rect.height); - ctx.fillStyle = styles.bgFill; - ctx.fillRect(0, 0, grid.rect.width, grid.rect.height); -} - -function strokeGrid( - ctx: CanvasRenderingContext2D, - styles: GridStyles, - grid: RenderedGrid, -) { - ctx.strokeStyle = styles.borderStroke; - ctx.strokeRect(0.5, 0.5, grid.rect.width - 1, grid.rect.height - 1); -} - -function strokeGridLines( - ctx: CanvasRenderingContext2D, - styles: GridStyles, - grid: RenderedGrid, -) { - ctx.strokeStyle = styles.cellStroke; - - grid.renderedRows.forEach((row, renderedRowIndex) => { - const isLastRow = renderedRowIndex === grid.renderedRows.length - 1; - - row.renderedCells.forEach((cell, cellIndex) => { - const { topLeft, width, height } = cell.rect; - const isLastCell = cellIndex === row.renderedCells.length - 1; - - ctx.strokeRect( - topLeft.x + 0.5, - topLeft.y + 0.5, - isLastCell ? width - 1 : width, - isLastRow ? height - 1 : height, - ); - }); - }); -} - -function drawCellValues( - ctx: CanvasRenderingContext2D, - styles: GridStyles, - grid: RenderedGrid, -) { - grid.renderedRows.forEach((row) => - row.renderedCells.forEach((cell) => { - if (!cell.value) return; - ctx.fillStyle = "white"; - ctx.textAlign = "center"; - ctx.font = styles.cellValueFont; - ctx.fillText( - cell.value, - cell.rect.center.x, - cell.rect.center.y + parseInt(styles.cellValueLineHeight) / 4, - ); - }), - ); -} - -export default function drawGrid( - ctx: CanvasRenderingContext2D, - styles: GridStyles, - grid: RenderedGrid, -) { - excursion(ctx, () => fillBackground(ctx, styles, grid)); - excursion(ctx, () => strokeGridLines(ctx, styles, grid)); - excursion(ctx, () => strokeGrid(ctx, styles, grid)); - excursion(ctx, () => drawCellValues(ctx, styles, grid)); -} diff --git a/packages/web/src/components/grid/drawSelection.ts b/packages/web/src/components/grid/drawSelection.ts deleted file mode 100644 index 1b8c2ed..0000000 --- a/packages/web/src/components/grid/drawSelection.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { CellRef } from "../../types"; -import excursion from "./excursion"; -import { getRenderedCell, RenderedCell, RenderedGrid } from "./renderGrid"; -import { GridSelection } from "./selection"; - -export interface SelectionStyles { - activeCellStroke: string; - selectionRangeFill: string; - selectionRangeStroke: string; -} - -function strokeActiveCell( - ctx: CanvasRenderingContext2D, - styles: SelectionStyles, - grid: RenderedGrid, - cell: RenderedCell, -) { - excursion(ctx, () => { - const isLastCell = cell.rect.bottomRight.x === grid.rect.bottomRight.x; - const isLastRow = cell.rect.bottomRight.y === grid.rect.bottomRight.y; - - ctx.strokeStyle = styles.activeCellStroke; - ctx.lineWidth = 2; - - ctx.strokeRect( - cell.rect.topLeft.x + 1, - cell.rect.topLeft.y + 1, - isLastCell ? cell.rect.width - 2 : cell.rect.width - 1, - isLastRow ? cell.rect.height - 2 : cell.rect.height - 1, - ); - }); -} - -function drawCellRange( - ctx: CanvasRenderingContext2D, - styles: SelectionStyles, - grid: RenderedGrid, - start: CellRef, - end: CellRef, - { stroke }: { stroke: boolean }, -) { - excursion(ctx, () => { - const startCell = getRenderedCell(grid, start); - const endCell = getRenderedCell(grid, end); - - if (!startCell || !endCell) return; - - const rect = startCell.rect.extend(endCell.rect); - - const isRightEdge = rect.bottomRight.x === grid.rect.bottomRight.x; - const isBottomEdge = rect.bottomRight.y === grid.rect.bottomRight.y; - - ctx.fillStyle = styles.selectionRangeFill; - - ctx.fillRect( - rect.topLeft.x + 1, - rect.topLeft.y + 1, - isRightEdge ? rect.width - 2 : rect.width - 1, - isBottomEdge ? rect.height - 2 : rect.height - 1, - ); - - if (!stroke) return; - - ctx.strokeStyle = styles.selectionRangeStroke; - - ctx.strokeRect( - rect.topLeft.x + 0.5, - rect.topLeft.y + 0.5, - isRightEdge ? rect.width - 1 : rect.width, - isBottomEdge ? rect.height - 1 : rect.height, - ); - }); -} - -export default function drawSelection( - ctx: CanvasRenderingContext2D, - styles: SelectionStyles, - grid: RenderedGrid, - selection: GridSelection | undefined, - { pending }: { pending: boolean }, -) { - ctx.clearRect(0, 0, grid.rect.width, grid.rect.height); - - if (!selection) return; - - const activeCell = getRenderedCell(grid, selection.activeCellRef); - - if (!activeCell) return; - - if (selection.range) { - drawCellRange(ctx, styles, grid, selection.range[0], selection.range[1], { - stroke: !pending, - }); - } - - strokeActiveCell(ctx, styles, grid, activeCell); -} diff --git a/packages/web/src/components/grid/excursion.ts b/packages/web/src/components/grid/excursion.ts deleted file mode 100644 index 7752df1..0000000 --- a/packages/web/src/components/grid/excursion.ts +++ /dev/null @@ -1,8 +0,0 @@ -export default function excursion( - ctx: CanvasRenderingContext2D, - f: () => void, -) { - ctx.save(); - f(); - ctx.restore(); -} diff --git a/packages/web/src/components/grid/index.css b/packages/web/src/components/grid/index.css deleted file mode 100644 index c29f55d..0000000 --- a/packages/web/src/components/grid/index.css +++ /dev/null @@ -1,49 +0,0 @@ -@layer components { - ntv-grid { - display: block; - position: relative; - - --grid-bg-fill: var(--color-neutral-900); - --grid-border-stroke: var(--color-neutral-700); - --grid-cell-stroke: var(--color-neutral-800); - --grid-active-cell-stroke: var(--color-green-400); - --grid-selection-range-fill: color-mix( - in oklab, - var(--color-green-400) 10%, - transparent - ); - --grid-selection-range-stroke: var(--color-green-400); - font-size: 14px; - } - - ntv-grid > canvas { - display: block; - } - - ntv-grid > canvas[data-selection] { - position: absolute; - top: 0; - left: 0; - pointer-events: none; - } - - :has(ntv-grid:state(selecting)) - > ntv-grid:not(:state(selecting)) - > canvas[data-selection] { - display: none; - } - - ntv-grid input[data-edit] { - 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]:focus-visible { - outline: none; - } -} diff --git a/packages/web/src/components/grid/index.ts b/packages/web/src/components/grid/index.ts deleted file mode 100644 index 3189409..0000000 --- a/packages/web/src/components/grid/index.ts +++ /dev/null @@ -1,276 +0,0 @@ -import NotiveElement, { customElement, eventHandler } from "../../element"; -import h from "../../html"; -import { CellRef } from "../../types"; -import cellAtCoord from "./cellAtCoord"; -import drawGrid, { GridStyles } from "./drawGrid"; -import drawSelection, { SelectionStyles } from "./drawSelection"; -import "./index.css"; -import { getRenderedCell, RenderedGrid } from "./renderGrid"; -import { extendSelection, GridSelection } from "./selection"; - -@customElement("ntv-grid") -export class NotiveGridElement extends NotiveElement { - #internals: ElementInternals = this.attachInternals(); - - #grid?: RenderedGrid; - - get grid(): RenderedGrid | undefined { - return this.#grid; - } - - set grid(grid: RenderedGrid | undefined) { - this.#grid = grid; - this.draw(); - } - - #selection?: GridSelection; - - get selection() { - return this.#selection; - } - - set selection(selection: GridSelection | undefined) { - this.#selection = selection; - this.drawSelection(); - } - - @eventHandler("ntv:grid:selectionchange") - ongridselectionchange?: (event: GridSelectionChangeEvent) => any; - - @eventHandler("ntv:grid:cellchange") - oncellchange?: (event: GridCellChangeEvent) => any; - - canvas: HTMLCanvasElement = h.canvas({ - onmousedown: (event) => { - if (event.button !== 0) return; - if (!this.grid) return; - const cellRef = this.#mouseEventCellRef(event); - if (!cellRef) return; - this.startSelecting(cellRef); - }, - ondblclick: (event) => { - if (!this.grid) return; - const cellRef = this.#mouseEventCellRef(event); - if (!cellRef) return; - this.startEditing(cellRef); - }, - }); - - selectionCanvas: HTMLCanvasElement = h.canvas({ - dataset: { selection: "true" }, - }); - - connectedCallback() { - this.append(this.canvas, this.selectionCanvas); - this.draw(); - this.drawSelection(); - } - - draw() { - if (!this.grid) return; - - const ctx = this.canvas.getContext("2d"); - - if (!ctx) throw new Error("Unable to get canvas context"); - - this.canvas.setAttribute("width", this.grid.rect.width + "px"); - this.canvas.setAttribute("height", this.grid.rect.height + "px"); - - drawGrid(ctx, this.getGridStyles(), this.grid); - } - - drawSelection() { - if (!this.grid) return; - - const ctx = this.selectionCanvas.getContext("2d"); - - 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, - this.getSelectionStyles(), - this.grid, - this.#pendingSelection ?? this.selection, - { - pending: !!this.#pendingSelection, - }, - ); - } - - 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"), - }; - } - - getSelectionStyles(): SelectionStyles { - const style = window.getComputedStyle(this); - const val = (k: string) => style.getPropertyValue(k); - - return { - activeCellStroke: val("--grid-active-cell-stroke"), - selectionRangeFill: val("--grid-selection-range-fill"), - selectionRangeStroke: val("--grid-selection-range-stroke"), - }; - } - - #pendingSelection?: GridSelection; - #selectionAbortController?: AbortController; - - startSelecting(cellRef: CellRef) { - if (!this.grid || this.#pendingSelection) return; - - this.#internals.states.add("selecting"); - - this.#selectionAbortController = new AbortController(); - const { signal } = this.#selectionAbortController; - - window.addEventListener( - "mousemove", - (event) => { - const cellRef = this.#mouseEventCellRef(event); - if (!cellRef) return; - this.#pendingSelection = extendSelection( - this.#pendingSelection, - cellRef, - ); - this.drawSelection(); - }, - { signal }, - ); - - window.addEventListener("mouseup", () => this.#finishSelecting(), { - signal, - }); - - window.addEventListener( - "keydown", - (event) => { - event.preventDefault(); - if (event.key === "Escape") { - this.#pendingSelection = undefined; - this.#finishSelecting(); - } - }, - { signal }, - ); - - this.#pendingSelection = extendSelection(undefined, cellRef); - this.drawSelection(); - } - - #finishSelecting() { - this.#selectionAbortController?.abort(); - this.#selectionAbortController = undefined; - this.#internals.states.delete("selecting"); - if (this.#pendingSelection) { - this.dispatchEvent(new GridSelectionChangeEvent(this.#pendingSelection)); - } - this.#pendingSelection = undefined; - this.drawSelection(); - } - - #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; - - #editInput: HTMLInputElement = h.input({ - dataset: { edit: "true" }, - onblur: () => this.#finishEditing(), - onkeydown: (event) => { - switch (event.key) { - case "Enter": - this.#finishEditing(); - break; - - case "Escape": - this.#cancelEditing(); - break; - } - }, - }); - - startEditing(cellRef: CellRef) { - if (!this.grid) return; - - const cell = getRenderedCell(this.grid, cellRef); - - if (!cell) return; - - this.#editingCellRef = cellRef; - - this.append(this.#editInput); - - this.#editInput.value = cell.value || ""; - - Object.assign(this.#editInput.style, { - left: cell.rect.topLeft.x + 2 + "px", - top: cell.rect.topLeft.y + 2 + "px", - width: cell.rect.width - 3 + "px", - height: cell.rect.height - 3 + "px", - }); - - this.#editInput.focus(); - } - - #cancelEditing() { - this.#editInput.remove(); - } - - #finishEditing() { - this.#editInput.remove(); - - if (!this.grid || !this.#editingCellRef) return; - - this.dispatchEvent( - new GridCellChangeEvent(this.#editingCellRef, this.#editInput.value), - ); - } -} - -export default NotiveGridElement.makeFactory(); - -export class GridSelectionChangeEvent extends Event { - static readonly TYPE = "ntv:grid:selectionchange"; - - constructor(public selection: GridSelection) { - super(GridSelectionChangeEvent.TYPE); - } -} - -export class GridCellChangeEvent extends Event { - static readonly TYPE = "ntv:grid:cellchange"; - - constructor( - public cellRef: CellRef, - public value: string | undefined, - ) { - super(GridCellChangeEvent.TYPE); - } -} - -declare global { - interface HTMLElementEventMap { - [GridSelectionChangeEvent.TYPE]: GridSelectionChangeEvent; - [GridCellChangeEvent.TYPE]: GridCellChangeEvent; - } -} diff --git a/packages/web/src/components/grid/renderGrid.ts b/packages/web/src/components/grid/renderGrid.ts deleted file mode 100644 index 89938ec..0000000 --- a/packages/web/src/components/grid/renderGrid.ts +++ /dev/null @@ -1,144 +0,0 @@ -import Ratio from "../../math/Ratio"; -import Rect from "../../math/Rect"; -import { Cell, CellRef, Grid, Row, RowRef } from "../../types"; - -export interface RenderedCell extends Cell { - cellRef: CellRef; - renderedRowIndex: number; - rect: Rect; - startRatio: Ratio; - endRatio: Ratio; -} - -export interface RenderedRow { - rowRef: RowRef; - rect: Rect; - renderedCells: RenderedCell[]; -} - -export interface RenderedGrid extends Grid { - rect: Rect; - renderedRows: RenderedRow[]; -} - -function renderCell( - grid: Grid, - cell: Cell, - cellRef: CellRef, - renderedRowIndex: number, - topLeftX: number, - topLeftY: number, - startRatio: Ratio, -): RenderedCell { - const width = cell.widthRatio - .divideRatio(grid.baseCellWidthRatio) - .multiplyRatio(Ratio.fromInteger(grid.baseCellSize)) - .toNumber(); - - const rect = new Rect(topLeftX, topLeftY, width, grid.baseCellSize); - - const endRatio = startRatio.add(cell.widthRatio); - - return { ...cell, cellRef, rect, renderedRowIndex, startRatio, endRatio }; -} - -function renderRow( - grid: Grid, - row: Row, - rowRef: RowRef, - renderedRowIndex: number, - topLeftY: number, -): RenderedRow { - if (row.cells.length === 0) { - return { - ...row, - rowRef, - rect: new Rect(0, topLeftY, 0, 0), - renderedCells: [], - }; - } - - let topLeftX = 0; - let startRatio = Ratio.fromInteger(0); - - const renderedCells = row.cells.map((cell, cellIndex) => { - const cellRef = { ...rowRef, cellIndex }; - - const renderedCell = renderCell( - grid, - cell, - cellRef, - renderedRowIndex, - topLeftX, - topLeftY, - startRatio, - ); - - topLeftX = renderedCell.rect.bottomRight.x; - startRatio = renderedCell.endRatio; - - return renderedCell; - }); - - const { topLeft } = renderedCells[0].rect; - const { bottomRight } = renderedCells.at(-1)!.rect; - - const rect = new Rect( - topLeft.x, - topLeft.y, - bottomRight.x - topLeft.x, - bottomRight.y - topLeft.y, - ); - - return { ...row, renderedCells, rect, rowRef }; -} - -function renderRows(grid: Grid): RenderedRow[] { - const renderedRows: RenderedRow[] = []; - - let partIndex = 0; - let rowIndex = 0; - let topLeftY = 0; - let renderedRowIndex = 0; - - while (true) { - if (!grid.parts[partIndex]?.rows[rowIndex]) break; - - const row = grid.parts[partIndex].rows[rowIndex]; - const rowRef = { partIndex, rowIndex }; - const renderedRow = renderRow( - grid, - row, - rowRef, - renderedRowIndex, - topLeftY, - ); - - topLeftY = renderedRow.rect.bottomRight.y; - renderedRows.push(renderedRow); - - if (!grid.parts[++partIndex]) { - partIndex = 0; - rowIndex++; - } - - renderedRowIndex++; - } - - return renderedRows; -} - -export default function renderGrid(grid: Grid) { - const renderedRows = renderRows(grid); - const rect = renderedRows[0].rect.extend(renderedRows.at(-1)!.rect); - return { ...grid, rect, renderedRows }; -} - -export function getRenderedCell( - grid: RenderedGrid, - cellRef: CellRef, -): RenderedCell | undefined { - const renderedRowIndex = - cellRef.rowIndex * grid.parts.length + cellRef.partIndex; - return grid.renderedRows[renderedRowIndex]?.renderedCells[cellRef.cellIndex]; -} diff --git a/packages/web/src/components/grid/selection.ts b/packages/web/src/components/grid/selection.ts deleted file mode 100644 index 517f8ae..0000000 --- a/packages/web/src/components/grid/selection.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { CellRef, cellRefEquals } from "../../types"; -import { RenderedGrid } from "./renderGrid"; - -export type CellRange = [start: CellRef, end: CellRef]; - -export interface GridSelection { - activeCellRef: CellRef; - range?: CellRange; -} - -export function extendSelection( - selection: GridSelection | undefined, - cellRef: CellRef, -): GridSelection { - if (!selection || cellRefEquals(selection.activeCellRef, cellRef)) { - return { activeCellRef: cellRef }; - } - - if (selection.range) { - return { ...selection, range: [selection.range[0], cellRef] }; - } - - return { ...selection, range: [selection.activeCellRef, cellRef] }; -} - -export function getSelectionRange(selection: GridSelection): CellRange { - return selection.range ?? [selection.activeCellRef, selection.activeCellRef]; -} |
