summaryrefslogtreecommitdiff
path: root/web/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'web/src/components')
-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
6 files changed, 130 insertions, 204 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();