import type { JSX } from "@char/aftercare/jsx-runtime"; import { findTrack, projectKey } from "../project/model.ts"; import { project } from "../project/store.ts"; import { prettyRoman, romanOf } from "../theory/analysis.ts"; import { chordName, parseChord } from "../theory/chord.ts"; import { moveChordCursor, writeChord } from "./actions.ts"; import { px, watch } from "./dom.tsx"; import { type ChordEdit, chordEdit, cursor, HEADER_W, pxPerBeat } from "./state.ts"; /** inline chord entry at the cursor: space or tab writes and moves on, enter writes and stops */ export function ChordEditor(): JSX.Element { let edit: ChordEdit | null = null; const parsed = () => parseChord(input.value, projectKey(project.get())); const refresh = () => { const chord = parsed(); input.classList.toggle("invalid", !!input.value.trim() && !chord); hint.textContent = chord ? `${chordName(chord)} ยท ${prettyRoman(romanOf(chord, projectKey(project.get())))}` : ""; }; const submit = () => { if (!edit || !input.value.trim()) return true; if (!parsed()) { box.animate([{ translate: "-3px 0" }, { translate: "3px 0" }, { translate: "0 0" }], 150); return false; } writeChord(edit.trackId, input.value.trim()); return true; }; const close = () => { chordEdit.set(null); input.blur(); }; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") close(); else if (e.key === "Enter") submit() && close(); else if (e.key === " " || e.key === "Tab") { const track = edit && findTrack(project.get(), edit.trackId, "chords"); if (!input.value.trim() && track) moveChordCursor(track, e.shiftKey ? -1 : 1); else if (submit()) input.value = ""; refresh(); } else return; e.preventDefault(); }; const input = ( chordEdit.set(null)} /> ) as HTMLInputElement; const hint = ; const box = ( ) as HTMLDivElement; const position = () => { if (!edit) return; const lane = document.querySelector(`.lane[data-lane="${edit.trackId}"]`); box.style.left = px(HEADER_W + cursor.get() * pxPerBeat.get()); box.style.top = px(lane?.offsetTop ?? 0); box.scrollIntoView({ block: "nearest", inline: "nearest" }); }; watch(box, [cursor, pxPerBeat, project], position); chordEdit.subscribe(next => { edit = next; box.hidden = !next; if (!next) return; input.value = next.initial; refresh(); position(); input.focus(); if (next.selectAll) input.select(); }); return box; }