char-slop/chordpad.vibed.software
git clone https://git.t4t.associates/char-slop/chordpad.vibed.software
3c7bc6e
main
1import type { JSX } from "@char/aftercare/jsx-runtime" ; 2import { findTrack , projectKey } from "../project/model.ts" ; 3import { project } from "../project/store.ts" ; 4import { prettyRoman , romanOf } from "../theory/analysis.ts" ; 5import { chordName , parseChord } from "../theory/chord.ts" ; 6import { moveChordCursor , writeChord } from "./actions.ts" ; 7import { px , watch } from "./dom.tsx" ; 8import { type ChordEdit , chordEdit , cursor , HEADER_W , pxPerBeat } from "./state.ts" ; 9 10/** inline chord entry at the cursor: space or tab writes and moves on, enter writes and stops */ 11export function ChordEditor () :JSX . Element { 12let edit :ChordEdit | null = null ; 13 14const parsed = () => parseChord ( input . value , projectKey ( project . get ())); 15const refresh = () => { 16const chord = parsed (); 17input . classList . toggle ( "invalid" , !! input . value . trim () && ! chord ); 18hint . textContent = chord 19 ?` ${ chordName ( chord )} · ${ prettyRoman ( romanOf ( chord , projectKey ( project . get ())))} ` 20 :"" ; 21}; 22 23const submit = () => { 24if ( ! edit || ! input . value . trim ()) return true ; 25if ( ! parsed ()) { 26box . animate ([{ translate :"-3px 0" }, { translate :"3px 0" }, { translate :"0 0" }], 150 ); 27return false ; 28} 29writeChord ( edit . trackId , input . value . trim ()); 30return true ; 31}; 32 33const close = () => { 34chordEdit . set ( null ); 35input . blur (); 36}; 37 38const onKey = ( e :KeyboardEvent ) => { 39if ( e . key === "Escape" ) close (); 40else if ( e . key === "Enter" ) submit () && close (); 41else if ( e . key === " " || e . key === "Tab" ) { 42const track = edit && findTrack ( project . get (), edit . trackId , "chords" ); 43if ( ! input . value . trim () && track ) moveChordCursor ( track , e . shiftKey ?- 1 :1 ); 44else if ( submit ()) input . value = "" ; 45refresh (); 46} else return ; 47e . preventDefault (); 48}; 49 50const input = ( 51< input 52class = "chord-input" 53spellcheck = { false } 54autocomplete = "off" 55_oninput = { refresh } 56_onkeydown = { onKey } 57_onblur = {() => chordEdit . set ( null )} 58/ > 59) as HTMLInputElement ; 60const hint = < span class = "chord-hint" / >; 61const box = ( 62< div class = "chord-editor" hidden > 63{ input} 64{ hint} 65< / div> 66) as HTMLDivElement ; 67 68const position = () => { 69if ( ! edit ) return ; 70const lane = document . querySelector < HTMLElement >( `.lane[data-lane=" ${ edit . trackId } "]` ); 71box . style . left = px ( HEADER_W + cursor . get () * pxPerBeat . get ()); 72box . style . top = px ( lane ?. offsetTop ?? 0 ); 73box . scrollIntoView ({ block : "nearest" , inline : "nearest" }); 74}; 75watch ( box , [ cursor , pxPerBeat , project ], position ); 76 77chordEdit . subscribe ( next => { 78edit = next ; 79box . hidden = ! next ; 80if ( ! next ) return ; 81input . value = next . initial ; 82refresh (); 83position (); 84input . focus (); 85if ( next . selectAll ) input . select (); 86}); 87 88return box ; 89}