import Ratio from "./math/Ratio"; import { Cell, CellRef, Doc, Grid, mapRowsInRange } from "./types"; import { ActiveCellSelection, Selection } from "./selection"; import renderGrid, { getRenderedCell, RenderedGrid, } from "./components/grid/renderGrid"; function defaultDoc(): Doc { const defaultCells: Cell[] = Array.from({ length: 16 }, (_, i) => ({ widthRatio: new Ratio(1, 16), value: i.toString(), })); return { grids: [ { id: window.crypto.randomUUID(), baseCellSize: 42, baseCellWidthRatio: new Ratio(1, 16), parts: [ { rows: Array.from({ length: 4 }, () => ({ cells: [...defaultCells], })), }, ], }, { id: window.crypto.randomUUID(), baseCellSize: 42, baseCellWidthRatio: new Ratio(1, 16), parts: [ { rows: Array.from({ length: 2 }, () => ({ cells: [...defaultCells], })), }, { rows: Array.from({ length: 2 }, () => ({ cells: [...defaultCells], })), }, ], }, ], }; } export default class Notive { #doc: Doc = defaultDoc(); get doc() { return this.#doc; } #gridsById = Object.fromEntries( this.#doc.grids.map((grid) => [grid.id, renderGrid(grid)]), ); getGrid(id: string): RenderedGrid | undefined { return this.#gridsById[id]; } #selection?: Selection; get selection() { return this.#selection; } #pendingSelection?: Selection; get pendingSelection() { return this.#pendingSelection; } selectCell(gridId: string, cellRef: CellRef) { this.#selection = new ActiveCellSelection(gridId, cellRef); this.#dispatchSelectionChanged(); } startSelecting(gridId: string, cellRef: CellRef) { this.#pendingSelection = new ActiveCellSelection(gridId, cellRef); this.#dispatchSelectionChanged(); } extendSelection(cellRef: CellRef) { const newSelection = this.pendingSelection?.extend(cellRef); if (newSelection !== this.pendingSelection) { this.#pendingSelection = newSelection; this.#dispatchSelectionChanged(); } } finishSelecting() { this.#selection = this.pendingSelection; this.#pendingSelection = undefined; this.#dispatchSelectionChanged(); } #dispatchSelectionChanged() { window.dispatchEvent(new CustomEvent("ntv:selectionchange")); } subdivideSelection(subdivisions: number) { const selection = this.selection; if (!selection) return; const grid = this.getGrid(selection.gridId); if (!grid) return; const startCellRef = selection.startCellRef(); const endCellRef = selection.endCellRef(); const startCell = getRenderedCell(grid, startCellRef); const endCell = getRenderedCell(grid, endCellRef); if (!startCell || !endCell) return; const startRatio = startCell.startRatio.compare(endCell.startRatio) <= 0 ? startCell.startRatio : endCell.startRatio; const endRatio = startCell.endRatio.compare(endCell.endRatio) >= 0 ? startCell.endRatio : endCell.endRatio; const totalWidth = endRatio.subtract(startRatio); const subdivisionWidth = totalWidth.divideRatio( Ratio.fromInteger(subdivisions), ); const newDoc = mapRowsInRange( this.doc, selection.gridId, startCellRef, endCellRef, (row, rowRef) => { const newCells: Cell[] = []; let currentRatio = Ratio.fromInteger(0); for (const cell of row.cells) { const cellStart = currentRatio; const cellEnd = currentRatio.add(cell.widthRatio); // Cell is entirely before selection if (cellEnd.compare(startRatio) <= 0) { newCells.push(cell); currentRatio = cellEnd; continue; } // Cell is entirely after selection if (cellStart.compare(endRatio) >= 0) { newCells.push(cell); currentRatio = cellEnd; continue; } // First cell that overlaps - insert subdivisions if (newCells.length === 0 || currentRatio.compare(startRatio) < 0) { for (let i = 0; i < subdivisions; i++) { newCells.push({ widthRatio: subdivisionWidth }); } } currentRatio = cellEnd; } return { ...row, cells: newCells }; }, ); this.#doc = newDoc; this.#gridsById = Object.fromEntries( this.#doc.grids.map((grid) => [grid.id, renderGrid(grid)]), ); window.dispatchEvent( new CustomEvent("ntv:grid:change", { detail: { gridId: selection.gridId }, }), ); } } window.notive = new Notive();