arguments-ui.ts (3768B)
1 import { literals, type Literal } from './codecs.js'; 2 import { parseLiteral, printLiteral } from './literal-input.js'; 3 4 export function argumentEditor(raw: HTMLTextAreaElement) { 5 const container = document.getElementById('argument-fields')!; 6 const add = document.getElementById('add-argument') as HTMLButtonElement; 7 const rawMode = document.getElementById('raw-arguments') as HTMLInputElement; 8 let snapshot = '', disabled = false; 9 let labels: readonly string[] = []; 10 let rows: { input: HTMLTextAreaElement; original?: Literal; text: string }[] = []; 11 function render(): void { 12 container.replaceChildren(); 13 if (!rows.length) { const hint = document.createElement('p'); hint.className = 'hint'; hint.textContent = 'No inputs supplied. Add an input to apply this definition.'; container.append(hint); } 14 rows.forEach((row, i) => { 15 const wrapper = document.createElement('div'); wrapper.className = 'argument-row'; 16 const label = document.createElement('label'); label.htmlFor = `argument-${i}`; label.textContent = labels[i] ?? `Argument ${i + 1}`; 17 row.input.id = label.htmlFor; row.input.rows = 2; row.input.maxLength = 16000; row.input.spellcheck = false; row.input.disabled = disabled; 18 const remove = document.createElement('button'); remove.type = 'button'; remove.textContent = 'Remove'; remove.disabled = disabled; 19 remove.setAttribute('aria-label', `Remove argument ${i + 1}`); 20 remove.onclick = () => { rows.splice(i, 1); render(); sync(); }; 21 row.input.oninput = sync; 22 wrapper.append(label, row.input, remove); container.append(wrapper); 23 }); 24 container.hidden = rawMode.checked; add.hidden = rawMode.checked; add.disabled = disabled || rows.length >= 16; 25 raw.hidden = !rawMode.checked; rawMode.disabled = disabled; 26 } 27 function values(): Literal[] { 28 return literals(rows.map(row => row.original && row.input.value === row.text ? row.original : parseLiteral(row.input.value))); 29 } 30 function sync(): void { 31 try { raw.value = JSON.stringify(values()); snapshot = raw.value; } catch { /* Retain invalid field text for correction; read reports it. */ } 32 } 33 function refresh(reset = false): void { 34 if (reset) rawMode.checked = false; 35 if (!reset && raw.value === snapshot) { render(); return; } 36 try { 37 rows = literals(JSON.parse(raw.value) as unknown).map(original => { 38 const input = document.createElement('textarea'), text = printLiteral(original); input.value = text; return { input, original, text }; 39 }); 40 snapshot = raw.value; 41 } catch { rawMode.checked = true; } 42 render(); 43 } 44 add.onclick = () => { const input = document.createElement('textarea'); input.value = 't'; rows.push({ input, text: 't' }); render(); sync(); input.focus(); }; 45 raw.oninput = () => { snapshot = ''; }; 46 rawMode.onchange = () => { 47 if (!rawMode.checked) refresh(); 48 else { 49 try { raw.value = JSON.stringify(values()); snapshot = raw.value; } 50 catch (error) { 51 rawMode.checked = false; 52 document.getElementById('result')!.textContent = `Correct the argument before switching editors: ${error instanceof Error ? error.message : String(error)}`; 53 } 54 } 55 render(); 56 }; 57 refresh(); 58 return { 59 refresh, 60 labels(value: readonly string[]) { labels = value; }, 61 disable(value: boolean) { disabled = value; refresh(); }, 62 read(): Literal[] { 63 if (raw.value.length > 16000) throw new Error('Argument text limit exceeded'); 64 if (rawMode.checked || raw.value !== snapshot) return literals(JSON.parse(raw.value) as unknown); 65 const result = values(); 66 if (JSON.stringify(result).length > 16000) throw new Error('Argument text limit exceeded'); 67 sync(); return result; 68 }, 69 }; 70 }