Emberveil sourcemobile/public/chat.js
/** Native phone keyboard over the game; sending keeps focus and preserves failed drafts. */
export function bindChat({form,input,send,close,toggle,viewport,connected,write,layout,status,view=globalThis.visualViewport}){
let sending=false,revision=0;
const focus=()=>input.focus({preventScroll:true});
function position(){
if(form.hidden)return;
const r=viewport.getBoundingClientRect(),height=view?.height||innerHeight,top=view?.offsetTop||0;
const left=Math.max(6,r.left),width=Math.max(120,Math.min(r.width,innerWidth-left-6,580));
form.style.left=`${left}px`;form.style.width=`${width}px`;
form.style.top=`${Math.max(top+4,Math.min(r.bottom-form.offsetHeight-4,top+height-form.offsetHeight-6))}px`;
}
function open(){
if(!connected())return;
form.hidden=false;toggle.setAttribute('aria-expanded','true');layout();position();focus();
}
function hide(){revision++;form.hidden=true;toggle.setAttribute('aria-expanded','false');input.blur();layout();}
toggle.onclick=()=>form.hidden?open():hide();close.onclick=hide;
// iOS must retain focus within the original user gesture, before awaiting Java.
send.addEventListener('pointerdown',e=>e.preventDefault());
input.addEventListener('input',()=>revision++);
form.onsubmit=async event=>{
event.preventDefault();focus();
if(sending||!connected()||!input.value.trim())return;
const text=input.value,at=revision;
if(/[^\x20-\x7e]/.test(text)){status('Use letters, numbers and standard punctuation for game chat.');return;}
sending=true;send.disabled=true;
try{await write(text);if(revision===at&&input.value===text)input.value='';status('');}
catch{status('Message could not be sent. Your text is still hereātap Send to retry.');}
finally{sending=false;send.disabled=false;position();}
};
return {open,hide,position,isOpen:()=>!form.hidden};
}