diff options
Diffstat (limited to 'web')
| -rw-r--r-- | web/src/components/app/index.ts | 9 | ||||
| -rw-r--r-- | web/src/components/grid/index.css | 4 | ||||
| -rw-r--r-- | web/src/components/grid/index.ts | 99 | ||||
| -rw-r--r-- | web/src/components/toolbar/index.ts | 10 | ||||
| -rw-r--r-- | web/src/element.ts | 45 | ||||
| -rw-r--r-- | web/src/html.ts | 44 | ||||
| -rw-r--r-- | web/src/index.html | 1 | ||||
| -rw-r--r-- | web/src/index.ts | 215 | ||||
| -rw-r--r-- | web/tsconfig.json | 2 |
9 files changed, 135 insertions, 294 deletions
diff --git a/web/src/components/app/index.ts b/web/src/components/app/index.ts index 195011f..c30249f 100644 --- a/web/src/components/app/index.ts +++ b/web/src/components/app/index.ts @@ -1,4 +1,5 @@ import defaultDoc from "../../defaultDoc"; +import NotiveElement, { customElement } from "../../element"; import { Selection } from "../../selection"; import { Doc } from "../../types"; import ntvGrid, { NotiveGridElement } from "../grid"; @@ -6,7 +7,8 @@ import renderGrid from "../grid/renderGrid"; import ntvToolbar from "../toolbar"; import "./index.css"; -export class NotiveAppElement extends HTMLElement { +@customElement("ntv-app") +export class NotiveAppElement extends NotiveElement { doc: Doc = defaultDoc(); #selection?: Selection; @@ -37,10 +39,11 @@ export class NotiveAppElement extends HTMLElement { ongridselectionchange: (event) => { this.selection = event.selection; }, + oncellchange: (event) => { + console.log(event); + }, }), ), ); } } - -customElements.define("ntv-app", NotiveAppElement); diff --git a/web/src/components/grid/index.css b/web/src/components/grid/index.css index 64153ed..c29f55d 100644 --- a/web/src/components/grid/index.css +++ b/web/src/components/grid/index.css @@ -33,7 +33,7 @@ display: none; } - ntv-grid input[data-edit-cell] { + ntv-grid input[data-edit] { position: absolute; vertical-align: baseline; background: var(--color-neutral-800); @@ -43,7 +43,7 @@ text-align: center; } - ntv-grid input[data-edit-cell]:focus-visible { + ntv-grid input[data-edit]:focus-visible { outline: none; } } diff --git a/web/src/components/grid/index.ts b/web/src/components/grid/index.ts index 2c00eb8..6c7f735 100644 --- a/web/src/components/grid/index.ts +++ b/web/src/components/grid/index.ts @@ -1,4 +1,5 @@ -import h, { type CreateElement } from "../../html"; +import NotiveElement, { customElement, eventHandler } from "../../element"; +import h from "../../html"; import { ActiveCellSelection, Selection } from "../../selection"; import { CellRef } from "../../types"; import cellAtCoord from "./cellAtCoord"; @@ -7,7 +8,8 @@ import drawSelection, { SelectionStyles } from "./drawSelection"; import "./index.css"; import { getRenderedCell, RenderedGrid } from "./renderGrid"; -export class NotiveGridElement extends HTMLElement { +@customElement("ntv-grid") +export class NotiveGridElement extends NotiveElement { #internals: ElementInternals = this.attachInternals(); grid?: RenderedGrid; @@ -23,28 +25,11 @@ export class NotiveGridElement extends HTMLElement { this.drawSelection(); } - #ongridselectionchange?: ((event: GridSelectionEvent) => any) | undefined; + @eventHandler("ntv:grid:selectionchange") + ongridselectionchange?: (event: GridSelectionEvent) => any; - get ongridselectionchange() { - return this.#ongridselectionchange; - } - - set ongridselectionchange( - handler: ((event: GridSelectionEvent) => any) | undefined, - ) { - if (this.#ongridselectionchange) { - this.removeEventListener( - "ntv:grid:selectionchange", - this.#ongridselectionchange, - ); - } - - this.#ongridselectionchange = handler; - - if (handler) { - this.addEventListener("ntv:grid:selectionchange", handler); - } - } + @eventHandler("ntv:grid:cellchange") + oncellchange?: (event: GridCellChangeEvent) => any; canvas: HTMLCanvasElement = h.canvas({ onmousedown: (event) => { @@ -53,6 +38,12 @@ export class NotiveGridElement extends HTMLElement { 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({ @@ -188,11 +179,9 @@ export class NotiveGridElement extends HTMLElement { #editingCellRef?: CellRef; - #editInputEl: HTMLInputElement = h.input({ - dataset: { editCell: "true" }, - onblur: () => { - this.#finishEditing(); - }, + #editInput: HTMLInputElement = h.input({ + dataset: { edit: "true" }, + onblur: () => this.#finishEditing(), onkeydown: (event) => { switch (event.key) { case "Enter": @@ -206,50 +195,45 @@ export class NotiveGridElement extends HTMLElement { }, }); - #canvasDoubleClickCallback(this: NotiveGridElement, event: MouseEvent) { + startEditing(cellRef: CellRef) { if (!this.grid) return; - const cellRef = this.#mouseEventCellRef(event); - - if (!cellRef) return; - const cell = getRenderedCell(this.grid, cellRef); if (!cell) return; this.#editingCellRef = cellRef; - this.append(this.#editInputEl); + this.append(this.#editInput); - 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(); + 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.#editInputEl.remove(); + this.#editInput.remove(); } #finishEditing() { - this.#editInputEl.remove(); + this.#editInput.remove(); - if (!this.grid) return; + if (!this.grid || !this.#editingCellRef) return; - window.notive.setCellValue( - this.grid.id, - this.#editingCellRef!, - this.#editInputEl.value, + this.dispatchEvent( + new GridCellChangeEvent(this.#editingCellRef, this.#editInput.value), ); } } -customElements.define("ntv-grid", NotiveGridElement); - -export default ((...args: any[]): NotiveGridElement => - (h as any)["ntv-grid"](...args)) as CreateElement<NotiveGridElement>; +export default NotiveGridElement.makeFactory(); export class GridSelectionEvent extends Event { selection: Selection; @@ -260,8 +244,21 @@ export class GridSelectionEvent extends Event { } } +export class GridCellChangeEvent extends Event { + cellRef: CellRef; + value?: string; + + constructor(cellRef: CellRef, value: string | undefined) { + super("ntv:grid:cellchange"); + + this.cellRef = cellRef; + this.value = value; + } +} + declare global { interface HTMLElementEventMap { "ntv:grid:selectionchange": GridSelectionEvent; + "ntv:grid:cellchange": GridCellChangeEvent; } } diff --git a/web/src/components/toolbar/index.ts b/web/src/components/toolbar/index.ts index 1400174..84f6a65 100644 --- a/web/src/components/toolbar/index.ts +++ b/web/src/components/toolbar/index.ts @@ -1,6 +1,6 @@ +import NotiveElement, { customElement } from "../../element"; import h, { CreateElement } from "../../html"; import { ActiveCellSelection, RangeSelection } from "../../selection"; -import { RenderedGrid } from "../grid/renderGrid"; import "./index.css"; function getSelectedSubdivisionsCount(): number | undefined { @@ -23,7 +23,8 @@ function getSelectedSubdivisionsCount(): number | undefined { return Math.min(...selectedCells.map((cells) => cells.length)); } -class NotiveToolbarElement extends HTMLElement { +@customElement("ntv-toolbar") +class NotiveToolbarElement extends NotiveElement { #subdivisionsInputEl: HTMLInputElement = h.input({ title: "Subdivisions", placeholder: "-", @@ -127,7 +128,4 @@ class NotiveToolbarElement extends HTMLElement { } } -customElements.define("ntv-toolbar", NotiveToolbarElement); - -export default ((...args: any[]): NotiveToolbarElement => - (h as any)["ntv-toolbar"](...args)) as CreateElement<NotiveToolbarElement>; +export default NotiveToolbarElement.makeFactory(); diff --git a/web/src/element.ts b/web/src/element.ts new file mode 100644 index 0000000..6299d2f --- /dev/null +++ b/web/src/element.ts @@ -0,0 +1,45 @@ +import { createElement, type CreateElement } from "./html"; + +export default class NotiveElement extends HTMLElement { + static makeFactory<T extends NotiveElement>(this: { + new (): T; + }): CreateElement<T>; + + static makeFactory(): any { + throw new Error( + "Missing makeFactory implementation. Did you forget to use @customElement?", + ); + } +} + +export function customElement(tagName: string) { + return function (_value: unknown, context: ClassDecoratorContext) { + context.addInitializer(function () { + window.customElements.define(tagName, this as typeof NotiveElement); + (this as typeof NotiveElement).makeFactory = () => + ((...args: any[]) => createElement(tagName, ...args)) as CreateElement<any>; + }); + }; +} + +export function eventHandler(eventName: string) { + return function (_value: unknown, context: ClassFieldDecoratorContext) { + const privateKey = Symbol(context.name.toString()); + + context.addInitializer(function () { + Object.defineProperty(this, context.name, { + get() { + return this[privateKey]; + }, + set(handler) { + const oldHandler = this[privateKey]; + if (oldHandler) this.removeEventListener(eventName, oldHandler); + this[privateKey] = handler; + if (handler) this.addEventListener(eventName, handler); + }, + enumerable: true, + configurable: true, + }); + }); + }; +} diff --git a/web/src/html.ts b/web/src/html.ts index 8802e50..a118849 100644 --- a/web/src/html.ts +++ b/web/src/html.ts @@ -1,3 +1,29 @@ +export function createElement<T extends HTMLElement>( + tag: string, + ...children: (Node | string)[] +): T; + +export function createElement<T extends HTMLElement>( + tag: string, + attrs: Partial<T>, + ...children: (Node | string)[] +): T; + +export function createElement(tag: string, ...args: any[]) { + const el = document.createElement(tag); + + if (args[0]?.constructor === Object) { + const { dataset, style, ...attrs } = args.shift(); + Object.assign(el, attrs); + if (dataset) Object.assign(el.dataset, dataset); + if (style) Object.assign(el.style, style); + } + + el.append(...args.flat()); + + return el; +} + export type CreateElement<T extends HTMLElement> = { (...children: (Node | string)[]): T; (attrs: Partial<T>, ...children: (Node | string)[]): T; @@ -9,23 +35,9 @@ type ElementCreator = { const h = new Proxy({} as ElementCreator, { get: - (_, tag: string) => + (_, tagName: string) => (...args: any[]) => { - const el = document.createElement(tag); - - if (args[0]?.constructor === Object) { - const { dataset, ...attrs } = args.shift(); - - Object.assign(el, attrs); - - if (dataset) { - Object.assign(el.dataset, dataset); - } - } - - el.append(...args.flat()); - - return el; + return createElement(tagName, ...args); }, }); diff --git a/web/src/index.html b/web/src/index.html index b296f52..a9cd398 100644 --- a/web/src/index.html +++ b/web/src/index.html @@ -6,7 +6,6 @@ <link rel="stylesheet" href="index.css" /> <link rel="icon" href="favicon.ico" /> <script type="module" src="index.ts"></script> - <script type="module" src="components/index.ts"></script> </head> <body> <ntv-app></ntv-app> diff --git a/web/src/index.ts b/web/src/index.ts index 97cfdf8..4c30907 100644 --- a/web/src/index.ts +++ b/web/src/index.ts @@ -1,214 +1 @@ -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 }, () => ({ - widthRatio: new Ratio(1, 16), - })); - - 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 }, - }), - ); - } - - setCellValue(gridId: string, cellRef: CellRef, value: string | undefined) { - const grid = this.doc.grids.find((grid) => grid.id === gridId); - - if (!grid) return; - - const cell = - grid.parts[cellRef.partIndex].rows[cellRef.rowIndex].cells[ - cellRef.cellIndex - ]; - - grid.parts[cellRef.partIndex].rows[cellRef.rowIndex].cells[ - cellRef.cellIndex - ] = { ...cell, value }; - - this.#gridsById = Object.fromEntries( - this.#doc.grids.map((grid) => [grid.id, renderGrid(grid)]), - ); - - window.dispatchEvent( - new CustomEvent("ntv:grid:change", { - detail: { gridId }, - }), - ); - } -} - -window.notive = new Notive(); +import "./components"; diff --git a/web/tsconfig.json b/web/tsconfig.json index 91be5ef..b650d24 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "ESNext", + "target": "ES2020", "module": "ESNext", "lib": ["ESNext", "DOM", "DOM.Iterable"], "moduleResolution": "bundler", |
