diff options
Diffstat (limited to 'surfaces/web.c')
| -rw-r--r-- | surfaces/web.c | 83 |
1 files changed, 59 insertions, 24 deletions
diff --git a/surfaces/web.c b/surfaces/web.c index 535d92f..7103c8f 100644 --- a/surfaces/web.c +++ b/surfaces/web.c @@ -204,44 +204,79 @@ static const char *const page_parts[] = { "border-radius:8px;background:transparent;color:inherit;cursor:pointer}button:hover{border-color:var(--focus)}" "footer{margin-top:14px;color:var(--muted);font-size:13px}" "kbd{font:12px ui-monospace,monospace;border:1px solid var(--line);border-radius:4px;padding:0 5px;margin-right:4px}" +"#keys{display:flex;flex-wrap:wrap;gap:6px}#keys button{padding:2px 8px;font-size:13px;color:var(--muted)}" +".sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}" +"ul:focus,table:focus{outline:2px solid var(--focus);outline-offset:2px}" "</style></head><body><main>", "<h1 id=title></h1><dl id=fields></dl><div id=parts></div>" -"<form id=prompt hidden><input id=entry autocomplete=off><button>Enter</button></form><footer id=keys></footer></main>" +"<form id=prompt hidden><input id=entry autocomplete=off><button>Enter</button></form>" +"<footer><div id=keys role=toolbar aria-label=Keys></div></footer></main>" "<script>", -"'use strict';let screen=null,firsts=[],busy=false,again=false;const rows=Math.max(5,Math.floor((innerHeight-220)/34));" -"const $=id=>document.getElementById(id);" +/* Accessible as the say surface is: a list is a listbox whose options + * know their place in the whole list (the page shows only the rows that + * fit), the focused row is the active descendant of a list that keeps the + * keyboard focus, a table is a grid, a tone other than normal and muted is + * said, keys are buttons. Everything is updated in place, so the focus a + * screen reader follows survives a redraw. */ +"'use strict';let screen=null,firsts=[],busy=false,again=false,held=null;" +"const rows=Math.max(5,Math.floor((innerHeight-220)/34));const $=id=>document.getElementById(id);" "const el=(tag,props,...kids)=>{const e=document.createElement(tag);Object.assign(e,props||{});" "for(const k of kids)if(k!=null)e.append(k);return e};" +"const said={accent:'accent',good:'good',warning:'warning',danger:'danger'};" "async function post(path,body){await fetch(path,{method:'POST',body});refresh()}" "async function refresh(){if(busy){again=true;return}busy=true;try{" "const r=await fetch('/screen?rows='+rows+'&first='+firsts.join(','));screen=await r.json();render()}" "finally{busy=false;if(again){again=false;refresh()}}}" -"function render(){const s=screen;$('title').textContent=s.title?\?'';document.title=s.title?\?'Vega';" -"$('fields').replaceChildren(...s.fields.flatMap(f=>[el('dt',{textContent:f.name}),el('dd',{textContent:f.text?\?'-'})]));" -"firsts=s.parts.map(p=>p.first);" -"$('parts').replaceChildren(...s.parts.map(p=>{const head=el('h2',{},p.name+' ',el('span',{textContent:'('+p.rows+')'}));" -"const pick=row=>{if(p.pointable)post('/point',p.part+' '+row)};" -"if(p.kind==='list'){const list=el('ul',{role:'listbox'});list.setAttribute('aria-label',p.name);" -"for(const it of p.items){const item=el('li',{className:it.tone,onclick:()=>pick(it.row)}," -"el('span',{className:'label',textContent:it.label?\?''}),it.detail?el('span',{className:'detail',textContent:it.detail}):null," -"it.badge?el('span',{className:'badge',textContent:it.badge}):null);item.setAttribute('role','option');" -"item.setAttribute('aria-selected',it.row===p.focus);list.append(item)}" -"list.onwheel=e=>{e.preventDefault();pick(Math.max(0,Math.min(p.rows-1,(p.focus?\?-1)+Math.sign(e.deltaY))))};" -"return el('section',{},head,list)}" -"const table=el('table',{},el('thead',{},el('tr',{},...p.columns.map(c=>el('th',{textContent:c})))));" -"const body=el('tbody');for(const it of p.items){const tr=el('tr',{className:it.tone,onclick:()=>pick(it.row)}," -"...it.cells.map(c=>el('td',{textContent:c?\?'-'})));tr.setAttribute('aria-selected',it.row===p.focus);body.append(tr)}" -"table.append(body);table.onwheel=e=>{e.preventDefault();pick(Math.max(0,Math.min(p.rows-1,(p.focus?\?-1)+Math.sign(e.deltaY))))};" -"return el('section',{},head,table)}));" -"$('prompt').hidden=!s.prompt;if(s.prompt)$('entry').placeholder=s.prompt;" -"const symbols={ArrowUp:'\u2191',ArrowDown:'\u2193',ArrowLeft:'\u2190',ArrowRight:'\u2192',Backspace:'Bksp'," +"function fit(parent,count,make){while(parent.children.length>count)parent.lastChild.remove();" +"while(parent.children.length<count)parent.append(make());return[...parent.children]}" +"function text(node,value){if(node.textContent!==value)node.textContent=value}" +"function attr(node,name,value){if(value==null)node.removeAttribute(name);" +"else if(node.getAttribute(name)!==String(value))node.setAttribute(name,value)}" +"const views=new Map();" +"function view(p){let v=views.get(p.part);if(v&&v.kind===p.kind)return v;" +"const head=el('h2',{},el('span',{className:'name'}),' ',el('span',{className:'count'}));" +"const box=p.kind==='list'?el('ul',{role:'listbox',tabIndex:0}):el('table',{role:'grid',tabIndex:0},el('thead',{},el('tr')),el('tbody'));" +"box.onfocus=()=>{held=p.part};" +"box.onwheel=e=>{e.preventDefault();const q=views.get(p.part).p;if(q.pointable)" +"post('/point',q.part+' '+Math.max(0,Math.min(q.rows-1,(q.focus?\?-1)+Math.sign(e.deltaY))))};" +"v={kind:p.kind,head,box,section:el('section',{},head,box)};views.set(p.part,v);return v}" +"function row(v,it){const p=v.p;return()=>{if(p.pointable)post('/point',p.part+' '+it.row)}}", +"function render(){const s=screen;text($('title'),s.title?\?'');document.title=s.title?\?'Vega';" +"const pairs=fit($('fields'),2*s.fields.length,()=>el('div'));" +"s.fields.forEach((f,i)=>{if(pairs[2*i].tagName!=='DT'){pairs[2*i].replaceWith(el('dt'));pairs[2*i+1].replaceWith(el('dd'))}});" +"const cells=[...$('fields').children];s.fields.forEach((f,i)=>{text(cells[2*i],f.name);text(cells[2*i+1],f.text?\?'-')});" +"firsts=s.parts.map(p=>p.first);const shown=s.parts.map(p=>{const v=view(p);v.p=p;" +"text(v.head.firstChild,p.name);text(v.head.lastChild,'('+p.rows+')');attr(v.box,'aria-label',p.name);" +"const id=r=>'p'+p.part+'r'+r;attr(v.box,'aria-activedescendant',p.focus==null?null:id(p.focus));" +"if(p.kind==='list'){fit(v.box,p.items.length,()=>el('li',{role:'option'},el('span',{className:'label'})," +"el('span',{className:'detail'}),el('span',{className:'badge'}),el('span',{className:'sr'}))).forEach((li,k)=>{" +"const it=p.items[k];li.id=id(it.row);li.className=it.tone;li.onclick=row(v,it);" +"attr(li,'aria-selected',it.row===p.focus);attr(li,'aria-setsize',p.rows);attr(li,'aria-posinset',it.row+1);" +"const[label,detail,badge,tone]=li.children;text(label,it.label?\?'');text(detail,it.detail?\?'');text(badge,it.badge?\?'');" +"detail.hidden=!it.detail;badge.hidden=!it.badge;text(tone,said[it.tone]?', '+said[it.tone]:'')})}" +"else{attr(v.box,'aria-rowcount',p.rows+1);const head=v.box.tHead.rows[0];attr(head,'aria-rowindex',1);" +"fit(head,p.columns.length,()=>el('th',{scope:'col'})).forEach((th,c)=>text(th,p.columns[c]));" +"fit(v.box.tBodies[0],p.items.length,()=>el('tr')).forEach((tr,k)=>{const it=p.items[k];tr.id=id(it.row);" +"tr.className=it.tone;tr.onclick=row(v,it);attr(tr,'aria-selected',it.row===p.focus);attr(tr,'aria-rowindex',it.row+2);" +"fit(tr,it.cells.length+1,()=>el('td')).forEach((td,c)=>{if(c<it.cells.length)text(td,it.cells[c]?\?'-');" +"else{td.className='sr';text(td,said[it.tone]?\?'')}})})}" +"return v.section});", +"const current=[...$('parts').children];if(current.length!==shown.length||current.some((c,i)=>c!==shown[i]))" +"$('parts').replaceChildren(...shown);" +/* The keyboard stays with the list it was on, or goes to the first. */ +"const keep=views.get(held)?.box;if(document.activeElement===document.body||!document.activeElement)" +"(keep&&keep.isConnected?keep:views.get(s.parts[0]?.part)?.box)?.focus({preventScroll:true});" +"$('prompt').hidden=!s.prompt;if(s.prompt){$('entry').placeholder=s.prompt;attr($('entry'),'aria-label',s.prompt)}", +"const symbols={ArrowUp:'↑',ArrowDown:'↓',ArrowLeft:'←',ArrowRight:'→',Backspace:'Bksp'," "Delete:'Del',Escape:'Esc',PageUp:'PgUp',PageDown:'PgDn'};" -"$('keys').replaceChildren(...s.keys.map(k=>el('span',{},el('kbd',{textContent:symbols[k.key]?\?k.key.replace(/^(Key|Digit)/,'')})," -"k.input+' ')))}" +"fit($('keys'),s.keys.length,()=>el('button',{type:'button'},el('kbd'),el('span'))).forEach((b,i)=>{const k=s.keys[i];" +"text(b.firstChild,symbols[k.key]?\?k.key.replace(/^(Key|Digit)/,''));text(b.lastChild,k.input);" +"attr(b,'aria-keyshortcuts',k.key.replace(/^Key/,'').replace(/^Digit/,''));b.onclick=()=>post('/key',k.key)})}" "$('prompt').onsubmit=e=>{e.preventDefault();const v=$('entry').value;if(v){$('entry').value='';post('/text',v)}};" "addEventListener('keydown',e=>{if(!screen||e.ctrlKey||e.metaKey||e.altKey)return;" "const typing=document.activeElement===$('entry');" "if(typing&&!['ArrowUp','ArrowDown','PageUp','PageDown','Escape'].includes(e.code))return;" +"if(document.activeElement?.tagName==='BUTTON'&&['Enter','Space'].includes(e.code))return;" "if(screen.keys.some(k=>k.key===e.code)){e.preventDefault();post('/key',e.code)}});" "new EventSource('/events').onmessage=refresh;refresh();" "</script></body></html>"}; |
