summaryrefslogtreecommitdiff
path: root/web
diff options
context:
space:
mode:
authorJosh Kingsley <josh@joshkingsley.me>2025-10-30 13:25:02 +0200
committerJosh Kingsley <josh@joshkingsley.me>2025-10-30 13:25:02 +0200
commit715996aceb4d4dc96410464f60727b98a289be08 (patch)
tree876d97664a22246ebecfc25cad1f4b96dee031a0 /web
parent7ef8366bfc43775bf26e71e77bddf31af829dfde (diff)
refactor(web): move selection code
Diffstat (limited to 'web')
-rw-r--r--web/src/components/app/index.ts17
-rw-r--r--web/src/components/grid/drawSelection.ts6
-rw-r--r--web/src/components/grid/index.ts87
-rw-r--r--web/src/components/grid/selection.ts23
-rw-r--r--web/src/components/toolbar/index.css78
-rw-r--r--web/src/components/toolbar/index.ts123
-rw-r--r--web/src/global.d.ts9
-rw-r--r--web/src/html.ts14
-rw-r--r--web/src/index.html4
-rw-r--r--web/src/index.ts4
-rw-r--r--web/src/selection.ts123
-rw-r--r--web/src/types.ts1
12 files changed, 144 insertions, 345 deletions
diff --git a/web/src/components/app/index.ts b/web/src/components/app/index.ts
index c30249f..aa7c738 100644
--- a/web/src/components/app/index.ts
+++ b/web/src/components/app/index.ts
@@ -1,22 +1,21 @@
import defaultDoc from "../../defaultDoc";
import NotiveElement, { customElement } from "../../element";
-import { Selection } from "../../selection";
import { Doc } from "../../types";
import ntvGrid, { NotiveGridElement } from "../grid";
import renderGrid from "../grid/renderGrid";
+import { GridSelection } from "../grid/selection";
import ntvToolbar from "../toolbar";
import "./index.css";
@customElement("ntv-app")
export class NotiveAppElement extends NotiveElement {
doc: Doc = defaultDoc();
- #selection?: Selection;
- get selection() {
- return this.#selection;
- }
+ #selectedGridId?: string;
+ #selection?: GridSelection;
- set selection(selection: Selection | undefined) {
+ setSelection(gridId: string, selection: GridSelection) {
+ this.#selectedGridId = gridId;
this.#selection = selection;
this.#updateGridSelections();
}
@@ -24,7 +23,7 @@ export class NotiveAppElement extends NotiveElement {
#updateGridSelections() {
this.querySelectorAll<NotiveGridElement>("ntv-grid").forEach((grid) => {
grid.selection =
- this.#selection?.gridId === grid.grid?.id ? this.#selection : undefined;
+ this.#selectedGridId === grid.grid?.id ? this.#selection : undefined;
});
}
@@ -37,7 +36,7 @@ export class NotiveAppElement extends NotiveElement {
grid: renderGrid(grid),
dataset: { gridId: grid.id },
ongridselectionchange: (event) => {
- this.selection = event.selection;
+ this.setSelection(grid.id, event.selection);
},
oncellchange: (event) => {
console.log(event);
@@ -47,3 +46,5 @@ export class NotiveAppElement extends NotiveElement {
);
}
}
+
+export default NotiveAppElement.makeFactory();
diff --git a/web/src/components/grid/drawSelection.ts b/web/src/components/grid/drawSelection.ts
index e1024a8..1b8c2ed 100644
--- a/web/src/components/grid/drawSelection.ts
+++ b/web/src/components/grid/drawSelection.ts
@@ -1,7 +1,7 @@
-import { RangeSelection, Selection } from "../../selection";
import { CellRef } from "../../types";
import excursion from "./excursion";
import { getRenderedCell, RenderedCell, RenderedGrid } from "./renderGrid";
+import { GridSelection } from "./selection";
export interface SelectionStyles {
activeCellStroke: string;
@@ -76,7 +76,7 @@ export default function drawSelection(
ctx: CanvasRenderingContext2D,
styles: SelectionStyles,
grid: RenderedGrid,
- selection: Selection | undefined,
+ selection: GridSelection | undefined,
{ pending }: { pending: boolean },
) {
ctx.clearRect(0, 0, grid.rect.width, grid.rect.height);
@@ -87,7 +87,7 @@ export default function drawSelection(
if (!activeCell) return;
- if (selection instanceof RangeSelection) {
+ if (selection.range) {
drawCellRange(ctx, styles, grid, selection.range[0], selection.range[1], {
stroke: !pending,
});
diff --git a/web/src/components/grid/index.ts b/web/src/components/grid/index.ts
index 6c7f735..78bb14e 100644
--- a/web/src/components/grid/index.ts
+++ b/web/src/components/grid/index.ts
@@ -1,12 +1,12 @@
import NotiveElement, { customElement, eventHandler } from "../../element";
import h from "../../html";
-import { ActiveCellSelection, Selection } from "../../selection";
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 {
@@ -14,25 +14,26 @@ export class NotiveGridElement extends NotiveElement {
grid?: RenderedGrid;
- #selection?: Selection;
+ #selection?: GridSelection;
get selection() {
return this.#selection;
}
- set selection(selection: Selection | undefined) {
+ set selection(selection: GridSelection | undefined) {
this.#selection = selection;
this.drawSelection();
}
@eventHandler("ntv:grid:selectionchange")
- ongridselectionchange?: (event: GridSelectionEvent) => any;
+ 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;
@@ -114,7 +115,7 @@ export class NotiveGridElement extends NotiveElement {
};
}
- #pendingSelection?: Selection;
+ #pendingSelection?: GridSelection;
#selectionAbortController?: AbortController;
startSelecting(cellRef: CellRef) {
@@ -123,12 +124,6 @@ export class NotiveGridElement extends NotiveElement {
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(
@@ -136,33 +131,43 @@ export class NotiveGridElement extends NotiveElement {
(event) => {
const cellRef = this.#mouseEventCellRef(event);
if (!cellRef) return;
- this.#pendingSelection = this.#pendingSelection?.extend(cellRef);
+ this.#pendingSelection = extendSelection(
+ this.#pendingSelection,
+ cellRef,
+ );
this.drawSelection();
},
{ signal },
);
- window.addEventListener(
- "mouseup",
- () => {
- this.#selectionAbortController?.abort();
-
- if (!this.#pendingSelection) return;
-
- this.dispatchEvent(
- new GridSelectionEvent(
- "ntv:grid:selectionchange",
- this.#pendingSelection,
- ),
- );
+ window.addEventListener("mouseup", () => this.#finishSelecting(), {
+ signal,
+ });
- this.#pendingSelection = undefined;
- this.drawSelection();
+ window.addEventListener(
+ "keydown",
+ (event) => {
+ event.preventDefault();
+ if (event.key === "Escape") {
+ this.#pendingSelection = undefined;
+ this.#finishSelecting();
+ }
},
{ signal },
);
- this.#pendingSelection = new ActiveCellSelection(this.grid.id, cellRef);
+ 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();
}
@@ -235,30 +240,28 @@ export class NotiveGridElement extends NotiveElement {
export default NotiveGridElement.makeFactory();
-export class GridSelectionEvent extends Event {
- selection: Selection;
+export class GridSelectionChangeEvent extends Event {
+ static readonly TYPE = "ntv:grid:selectionchange";
- constructor(type: string, selection: Selection) {
- super(type);
- this.selection = selection;
+ constructor(public selection: GridSelection) {
+ super(GridSelectionChangeEvent.TYPE);
}
}
export class GridCellChangeEvent extends Event {
- cellRef: CellRef;
- value?: string;
-
- constructor(cellRef: CellRef, value: string | undefined) {
- super("ntv:grid:cellchange");
+ static readonly TYPE = "ntv:grid:cellchange";
- this.cellRef = cellRef;
- this.value = value;
+ constructor(
+ public cellRef: CellRef,
+ public value: string | undefined,
+ ) {
+ super(GridCellChangeEvent.TYPE);
}
}
declare global {
interface HTMLElementEventMap {
- "ntv:grid:selectionchange": GridSelectionEvent;
- "ntv:grid:cellchange": GridCellChangeEvent;
+ [GridSelectionChangeEvent.TYPE]: GridSelectionChangeEvent;
+ [GridCellChangeEvent.TYPE]: GridCellChangeEvent;
}
}
diff --git a/web/src/components/grid/selection.ts b/web/src/components/grid/selection.ts
new file mode 100644
index 0000000..a24bbf5
--- /dev/null
+++ b/web/src/components/grid/selection.ts
@@ -0,0 +1,23 @@
+import { CellRef, cellRefEquals } from "../../types";
+
+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] };
+}
diff --git a/web/src/components/toolbar/index.css b/web/src/components/toolbar/index.css
index b580fbf..e082f7d 100644
--- a/web/src/components/toolbar/index.css
+++ b/web/src/components/toolbar/index.css
@@ -1,44 +1,46 @@
-ntv-toolbar {
- display: flex;
- border-radius: 99999px;
- background: var(--color-neutral-900);
- width: min-content;
-}
+@layer components {
+ ntv-toolbar {
+ display: flex;
+ border-radius: 99999px;
+ background: var(--color-neutral-900);
+ width: min-content;
+ }
-ntv-toolbar > section {
- display: flex;
- gap: 0.25rem;
- padding: 0.325rem;
-}
+ ntv-toolbar > section {
+ display: flex;
+ gap: 0.25rem;
+ padding: 0.325rem;
+ }
-ntv-toolbar button {
- border-radius: 99999px;
- background: var(--color-neutral-800);
- padding: 0 0.5rem;
- height: 1.25rem;
- color: white;
- font-size: 0.75rem;
-}
+ ntv-toolbar button {
+ border-radius: 99999px;
+ background: var(--color-neutral-800);
+ padding: 0 0.5rem;
+ height: 1.25rem;
+ color: white;
+ font-size: 0.75rem;
+ }
-ntv-toolbar button:hover {
- background: var(--color-neutral-700);
-}
+ ntv-toolbar button:hover {
+ background: var(--color-green-400);
+ }
-ntv-toolbar button[data-variant="icon"] {
- display: flex;
- justify-content: center;
- align-items: center;
- aspect-ratio: 1;
- height: 1.25rem;
-}
+ ntv-toolbar button[data-icon] {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ aspect-ratio: 1;
+ height: 1.25rem;
+ }
-ntv-toolbar input {
- border: 1px solid var(--color-neutral-700);
- border-radius: 4px;
- background: var(--color-neutral-900);
- width: 2.5rem;
- height: 1.25rem;
- color: white;
- font-size: 0.75rem;
- text-align: center;
+ ntv-toolbar input {
+ border: 1px solid var(--color-neutral-700);
+ border-radius: 4px;
+ background: var(--color-neutral-900);
+ width: 2.5rem;
+ height: 1.25rem;
+ color: white;
+ font-size: 0.75rem;
+ text-align: center;
+ }
}
diff --git a/web/src/components/toolbar/index.ts b/web/src/components/toolbar/index.ts
index 84f6a65..da4b69d 100644
--- a/web/src/components/toolbar/index.ts
+++ b/web/src/components/toolbar/index.ts
@@ -1,131 +1,28 @@
import NotiveElement, { customElement } from "../../element";
-import h, { CreateElement } from "../../html";
-import { ActiveCellSelection, RangeSelection } from "../../selection";
+import h, { fragment } from "../../html";
import "./index.css";
-function getSelectedSubdivisionsCount(): number | undefined {
- const selection = window.notive.selection;
-
- if (!selection) return;
-
- if (selection instanceof ActiveCellSelection) {
- return 1;
- }
-
- if (!(selection instanceof RangeSelection)) return;
-
- const grid = window.notive.getGrid(selection.gridId);
-
- if (!grid) return;
-
- const selectedCells = selection.getSelectedCells(grid);
-
- return Math.min(...selectedCells.map((cells) => cells.length));
-}
-
@customElement("ntv-toolbar")
class NotiveToolbarElement extends NotiveElement {
+ connectedCallback() {
+ this.append(this.#view());
+ }
+
#subdivisionsInputEl: HTMLInputElement = h.input({
title: "Subdivisions",
- placeholder: "-",
disabled: true,
});
- connectedCallback() {
- this.#render();
-
- window.addEventListener("ntv:selectionchange", () => {
- if (window.notive.pendingSelection) {
- this.#subdivisionsInputEl.disabled = true;
- this.#subdivisionsInputEl.value = "";
- return;
- }
-
- const subdivisionsCount = getSelectedSubdivisionsCount();
-
- if (!subdivisionsCount) {
- this.#subdivisionsInputEl.disabled = true;
- this.#subdivisionsInputEl.value = "";
- return;
- }
-
- this.#subdivisionsInputEl.disabled = false;
- this.#subdivisionsInputEl.value = subdivisionsCount.toString();
- });
-
- this.#subdivisionsInputEl.addEventListener("change", () => {
- window.notive.subdivideSelection(
- parseInt(this.#subdivisionsInputEl.value),
- );
- });
- }
-
- #render() {
- this.append(
+ #view() {
+ return fragment(
h.section(
- h.button(
- {
- dataset: { variant: "icon" },
- onclick: () => {
- const subdivisions = Math.max(
- 1,
- parseInt(this.#subdivisionsInputEl.value) - 1,
- );
-
- this.#subdivisionsInputEl.value = subdivisions.toString();
- window.notive.subdivideSelection(subdivisions);
- },
- },
- "-",
- ),
+ h.button({ dataset: { icon: "" } }, "-"),
this.#subdivisionsInputEl,
- h.button(
- {
- dataset: { variant: "icon" },
-
- onclick: () => {
- const subdivisions =
- parseInt(this.#subdivisionsInputEl.value) + 1;
-
- this.#subdivisionsInputEl.value = subdivisions.toString();
- window.notive.subdivideSelection(subdivisions);
- },
- },
- "+",
- ),
- ),
- h.section(
- h.button(
- {
- dataset: { variant: "menu" },
- onclick: () => {
- this.#play();
- },
- },
- "Play",
- ),
+ h.button({ dataset: { icon: "" } }, "+"),
),
+ h.section(h.button("Play")),
);
}
-
- async #play() {
- const Tone = await import("tone");
- await Tone.start();
- const transport = Tone.getTransport();
- transport.stop();
- transport.position = 0;
- transport.cancel();
- const synth = new Tone.Synth().toDestination();
- const seq = new Tone.Sequence(
- (time, note) => {
- synth.triggerAttackRelease(note, 0.1, time);
- },
- ["C4", "D4", "E4"],
- "1m",
- ).start(0);
- seq.loop = false;
- transport.start();
- }
}
export default NotiveToolbarElement.makeFactory();
diff --git a/web/src/global.d.ts b/web/src/global.d.ts
deleted file mode 100644
index 3b6d980..0000000
--- a/web/src/global.d.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import type Notive from "./index";
-
-declare global {
- interface Window {
- notive: Notive;
- }
-}
-
-export {};
diff --git a/web/src/html.ts b/web/src/html.ts
index a118849..3fccda3 100644
--- a/web/src/html.ts
+++ b/web/src/html.ts
@@ -1,16 +1,16 @@
export function createElement<T extends HTMLElement>(
- tag: string,
+ tagName: string,
...children: (Node | string)[]
): T;
export function createElement<T extends HTMLElement>(
- tag: string,
+ tagName: string,
attrs: Partial<T>,
...children: (Node | string)[]
): T;
-export function createElement(tag: string, ...args: any[]) {
- const el = document.createElement(tag);
+export function createElement(tagName: string, ...args: any[]) {
+ const el = document.createElement(tagName);
if (args[0]?.constructor === Object) {
const { dataset, style, ...attrs } = args.shift();
@@ -42,3 +42,9 @@ const h = new Proxy({} as ElementCreator, {
});
export default h;
+
+export function fragment(...children: (Node | string)[]): DocumentFragment {
+ const fragment = document.createDocumentFragment();
+ fragment.append(...children);
+ return fragment;
+}
diff --git a/web/src/index.html b/web/src/index.html
index a9cd398..9f8bcbf 100644
--- a/web/src/index.html
+++ b/web/src/index.html
@@ -7,7 +7,5 @@
<link rel="icon" href="favicon.ico" />
<script type="module" src="index.ts"></script>
</head>
- <body>
- <ntv-app></ntv-app>
- </body>
+ <body></body>
</html>
diff --git a/web/src/index.ts b/web/src/index.ts
index 4c30907..7842326 100644
--- a/web/src/index.ts
+++ b/web/src/index.ts
@@ -1 +1,3 @@
-import "./components";
+import ntvApp from "./components/app";
+
+document.body.append(ntvApp());
diff --git a/web/src/selection.ts b/web/src/selection.ts
deleted file mode 100644
index abaf8b5..0000000
--- a/web/src/selection.ts
+++ /dev/null
@@ -1,123 +0,0 @@
-import {
- getRenderedCell,
- RenderedCell,
- RenderedGrid,
-} from "./components/grid/renderGrid";
-import { CellRef, cellRefEquals } from "./types";
-
-export abstract class Selection {
- readonly gridId: string;
- readonly activeCellRef: CellRef;
-
- constructor(gridId: string, activeCellRef: CellRef) {
- this.gridId = gridId;
- this.activeCellRef = activeCellRef;
- }
-
- abstract extend(cellRef: CellRef): Selection;
-
- abstract getSelectedCells(grid: RenderedGrid): RenderedCell[][];
-
- abstract startCellRef(): CellRef;
- abstract endCellRef(): CellRef;
-}
-
-export class ActiveCellSelection extends Selection {
- extend(cellRef: CellRef): Selection {
- if (cellRefEquals(cellRef, this.activeCellRef)) {
- return this;
- }
-
- return new RangeSelection(this.gridId, this.activeCellRef, [
- this.activeCellRef,
- cellRef,
- ]);
- }
-
- getSelectedCells(grid: RenderedGrid): RenderedCell[][] {
- return [[getRenderedCell(grid, this.activeCellRef)!]];
- }
-
- startCellRef(): CellRef {
- return this.activeCellRef;
- }
-
- endCellRef(): CellRef {
- return this.activeCellRef;
- }
-}
-
-export type CellRange = [CellRef, CellRef];
-
-export class RangeSelection extends Selection {
- #range: CellRange;
-
- get range() {
- return this.#range;
- }
-
- constructor(gridId: string, activeCellRef: CellRef, range: CellRange) {
- super(gridId, activeCellRef);
- this.#range = range;
- }
-
- extend(cellRef: CellRef): Selection {
- if (cellRefEquals(cellRef, this.activeCellRef)) {
- return new ActiveCellSelection(this.gridId, cellRef);
- }
-
- return new RangeSelection(this.gridId, this.activeCellRef, [
- this.#range[0],
- cellRef,
- ]);
- }
-
- getSelectedCells(grid: RenderedGrid): RenderedCell[][] {
- const startCell = getRenderedCell(grid, this.range[0]);
- const endCell = getRenderedCell(grid, this.range[1]);
-
- if (!startCell || !endCell) return [];
-
- const firstRowIndex = Math.min(
- startCell.renderedRowIndex,
- endCell.renderedRowIndex,
- );
-
- const lastRowIndex = Math.max(
- startCell.renderedRowIndex,
- endCell.renderedRowIndex,
- );
-
- const startRatio =
- startCell.startRatio.compare(endCell.startRatio) <= 0
- ? startCell.startRatio
- : endCell.startRatio;
-
- const endRatio =
- startCell.endRatio.compare(endCell.endRatio) >= 0
- ? startCell.endRatio
- : endCell.endRatio;
-
- return grid.renderedRows
- .slice(firstRowIndex, lastRowIndex + 1)
- .map((row) => {
- const firstCellIndex = row.renderedCells.findIndex(
- (cell) => cell.startRatio.compare(startRatio) >= 0,
- );
-
- const lastCellIndex = row.renderedCells.findLastIndex(
- (cell) => cell.endRatio.compare(endRatio) <= 0,
- );
-
- return row.renderedCells.slice(firstCellIndex, lastCellIndex + 1);
- });
- }
-
- startCellRef(): CellRef {
- return this.range[0];
- }
-
- endCellRef(): CellRef {
- return this.range[1];
- }
-}
diff --git a/web/src/types.ts b/web/src/types.ts
index 8b1b650..b41bb9a 100644
--- a/web/src/types.ts
+++ b/web/src/types.ts
@@ -30,7 +30,6 @@ export interface RowRef {
rowIndex: number;
}
-// TODO Should probably have a gridId
export interface CellRef {
partIndex: number;
rowIndex: number;