interface OptionsElement {// generateHTML():HTMLElement; submit:()=>void; readonly watchForChange:(func:(arg1:x)=>void)=>void; value:x; } //future me stuff class Buttons implements OptionsElement{ readonly name:string; readonly buttons:[string,Options|string][]; buttonList:HTMLDivElement; warndiv:HTMLElement; value:unknown; constructor(name:string){ this.buttons=[]; this.name=name; } add(name:string,thing?:Options|undefined){ if(!thing){ thing=new Options(name,this); } this.buttons.push([name,thing]); return thing; } generateHTML(){ const buttonList=document.createElement("div"); buttonList.classList.add("Buttons"); buttonList.classList.add("flexltr"); this.buttonList=buttonList; const htmlarea=document.createElement("div"); htmlarea.classList.add("flexgrow"); const buttonTable=document.createElement("div"); buttonTable.classList.add("flexttb","settingbuttons"); for(const thing of this.buttons){ const button=document.createElement("button"); button.classList.add("SettingsButton"); button.textContent=thing[0]; button.onclick=_=>{ this.generateHTMLArea(thing[1],htmlarea); if(this.warndiv){ this.warndiv.remove(); } }; buttonTable.append(button); } this.generateHTMLArea(this.buttons[0][1],htmlarea); buttonList.append(buttonTable); buttonList.append(htmlarea); return buttonList; } handleString(str:string):HTMLElement{ const div=document.createElement("span"); div.textContent=str; return div; } private generateHTMLArea(buttonInfo:Options|string,htmlarea:HTMLElement){ let html:HTMLElement; if(buttonInfo instanceof Options){ buttonInfo.subOptions=undefined; html=buttonInfo.generateHTML(); }else{ html=this.handleString(buttonInfo); } htmlarea.innerHTML=""; htmlarea.append(html); } changed(html:HTMLElement){ this.warndiv=html; this.buttonList.append(html); } watchForChange(){} save(){} submit(){ } } class TextInput implements OptionsElement{ readonly label:string; readonly owner:Options; readonly onSubmit:(str:string)=>void; value:string; input:WeakRef; password:boolean; constructor(label:string,onSubmit:(str:string)=>void,owner:Options,{initText="",password=false}={}){ this.label=label; this.value=initText; this.owner=owner; this.onSubmit=onSubmit; this.password=password; } generateHTML():HTMLDivElement{ const div=document.createElement("div"); const span=document.createElement("span"); span.textContent=this.label; div.append(span); const input=document.createElement("input"); input.value=this.value; input.type=this.password?"password":"text"; input.oninput=this.onChange.bind(this); this.input=new WeakRef(input); div.append(input); return div; } private onChange(ev:Event){ this.owner.changed(); const input=this.input.deref(); if(input){ const value=input.value as string; this.onchange(value); this.value=value; } } onchange:(str:string)=>void=_=>{}; watchForChange(func:(str:string)=>void){ this.onchange=func; } submit(){ this.onSubmit(this.value); } } class SettingsText implements OptionsElement{ readonly onSubmit:(str:string)=>void; value:void; readonly text:string; constructor(text:string){ this.text=text; } generateHTML():HTMLSpanElement{ const span=document.createElement("span"); span.innerText=this.text; return span; } watchForChange(){} submit(){} } class SettingsTitle implements OptionsElement{ readonly onSubmit:(str:string)=>void; value:void; readonly text:string; constructor(text:string){ this.text=text; } generateHTML():HTMLSpanElement{ const span=document.createElement("h2"); span.innerText=this.text; return span; } watchForChange(){} submit(){} } class CheckboxInput implements OptionsElement{ readonly label:string; readonly owner:Options; readonly onSubmit:(str:boolean)=>void; value:boolean; input:WeakRef; constructor(label:string,onSubmit:(str:boolean)=>void,owner:Options,{initState=false}={}){ this.label=label; this.value=initState; this.owner=owner; this.onSubmit=onSubmit; } generateHTML():HTMLDivElement{ const div=document.createElement("div"); const span=document.createElement("span"); span.textContent=this.label; div.append(span); const input=document.createElement("input"); input.type="checkbox"; input.checked=this.value; input.oninput=this.onChange.bind(this); this.input=new WeakRef(input); div.append(input); return div; } private onChange(ev:Event){ this.owner.changed(); const input=this.input.deref(); if(input){ const value=input.checked as boolean; this.onchange(value); this.value=value; } } onchange:(str:boolean)=>void=_=>{}; watchForChange(func:(str:boolean)=>void){ this.onchange=func; } submit(){ this.onSubmit(this.value); } } class ButtonInput implements OptionsElement{ readonly label:string; readonly owner:Options; readonly onClick:()=>void; textContent:string; value: void; constructor(label:string,textContent:string,onClick:()=>void,owner:Options,{}={}){ this.label=label; this.owner=owner; this.onClick=onClick; this.textContent=textContent; } generateHTML():HTMLDivElement{ const div=document.createElement("div"); const span=document.createElement("span"); span.textContent=this.label; div.append(span); const button=document.createElement("button"); button.textContent=this.textContent; button.onclick=this.onClickEvent.bind(this); div.append(button); return div; } private onClickEvent(ev:Event){ this.onClick(); } watchForChange(){} submit(){} } class ColorInput implements OptionsElement{ readonly label:string; readonly owner:Options; readonly onSubmit:(str:string)=>void; colorContent:string; input:WeakRef; value: string; constructor(label:string,onSubmit:(str:string)=>void,owner:Options,{initColor=""}={}){ this.label=label; this.colorContent=initColor; this.owner=owner; this.onSubmit=onSubmit; } generateHTML():HTMLDivElement{ const div=document.createElement("div"); const span=document.createElement("span"); span.textContent=this.label; div.append(span); const input=document.createElement("input"); input.value=this.colorContent; input.type="color"; input.oninput=this.onChange.bind(this); this.input=new WeakRef(input); div.append(input); return div; } private onChange(ev:Event){ this.owner.changed(); const input=this.input.deref(); if(input){ const value=input.value as string; this.value=value; this.onchange(value); this.colorContent=value; } } onchange:(str:string)=>void=_=>{}; watchForChange(func:(str:string)=>void){ this.onchange=func; } submit(){ this.onSubmit(this.colorContent); } } class SelectInput implements OptionsElement{ readonly label:string; readonly owner:Options; readonly onSubmit:(str:number)=>void; options:string[]; index:number; select:WeakRef; get value(){ return this.index; } constructor(label:string,onSubmit:(str:number)=>void,options:string[],owner:Options,{defaultIndex=0}={}){ this.label=label; this.index=defaultIndex; this.owner=owner; this.onSubmit=onSubmit; this.options=options; } generateHTML():HTMLDivElement{ const div=document.createElement("div"); const span=document.createElement("span"); span.textContent=this.label; div.append(span); const select=document.createElement("select"); select.onchange=this.onChange.bind(this); for(const thing of this.options){ const option = document.createElement("option"); option.textContent=thing; select.appendChild(option); } this.select=new WeakRef(select); select.selectedIndex=this.index; div.append(select); return div; } private onChange(ev:Event){ this.owner.changed(); const select=this.select.deref(); if(select){ const value=select.selectedIndex; this.onchange(value); this.index=value; } } onchange:(str:number)=>void=_=>{}; watchForChange(func:(str:number)=>void){ this.onchange=func; } submit(){ this.onSubmit(this.index); } } class MDInput implements OptionsElement{ readonly label:string; readonly owner:Options; readonly onSubmit:(str:string)=>void; value:string; input:WeakRef; constructor(label:string,onSubmit:(str:string)=>void,owner:Options,{initText=""}={}){ this.label=label; this.value=initText; this.owner=owner; this.onSubmit=onSubmit; } generateHTML():HTMLDivElement{ const div=document.createElement("div"); const span=document.createElement("span"); span.textContent=this.label; div.append(span); div.append(document.createElement("br")); const input=document.createElement("textarea"); input.value=this.value; input.oninput=this.onChange.bind(this); this.input=new WeakRef(input); div.append(input); return div; } onChange(ev:Event){ this.owner.changed(); const input=this.input.deref(); if(input){ const value=input.value as string; this.onchange(value); this.value=value; } } onchange:(str:string)=>void=_=>{}; watchForChange(func:(str:string)=>void){ this.onchange=func; } submit(){ this.onSubmit(this.value); } } class FileInput implements OptionsElement{ readonly label:string; readonly owner:Options; readonly onSubmit:(str:FileList|null)=>void; input:WeakRef; value:FileList|null; clear:boolean; constructor(label:string,onSubmit:(str:FileList)=>void,owner:Options,{clear=false}={}){ this.label=label; this.owner=owner; this.onSubmit=onSubmit; this.clear=clear; } generateHTML():HTMLDivElement{ const div=document.createElement("div"); const span=document.createElement("span"); span.textContent=this.label; div.append(span); const input=document.createElement("input"); input.type="file"; input.oninput=this.onChange.bind(this); this.input=new WeakRef(input); div.append(input); if(this.clear){ const button=document.createElement("button"); button.textContent="Clear"; button.onclick=_=>{ if(this.onchange){ this.onchange(null); } this.value=null; this.owner.changed(); }; div.append(button); } return div; } onChange(ev:Event){ this.owner.changed(); const input=this.input.deref(); if(this.onchange&&input){ this.value=input.files; this.onchange(input.files); } } onchange:((str:FileList|null)=>void)|null=null; watchForChange(func:(str:FileList|null)=>void){ this.onchange=func; } submit(){ const input=this.input.deref(); if(input){ this.onSubmit(input.files); } } } class HtmlArea implements OptionsElement{ submit: () => void; html:(()=>HTMLElement)|HTMLElement; value:void; constructor(html:(()=>HTMLElement)|HTMLElement,submit:()=>void){ this.submit=submit; this.html=html; } generateHTML(): HTMLElement{ if(this.html instanceof Function){ return this.html(); }else{ return this.html; } } watchForChange(){} } class Options implements OptionsElement{ name:string; haschanged=false; readonly options:OptionsElement[]; readonly owner:Buttons|Options|Form; readonly ltr:boolean; value:void; readonly html:WeakMap,WeakRef>=new WeakMap(); container:WeakRef=new WeakRef(document.createElement("div")); constructor(name:string,owner:Buttons|Options|Form,{ltr=false}={}){ this.name=name; this.options=[]; this.owner=owner; this.ltr=ltr; } removeAll(){ while(this.options.length){ this.options.pop(); } const container=this.container.deref(); if(container){ container.innerHTML=""; } } watchForChange(){} addOptions(name:string,{ltr=false}={}){ const options=new Options(name,this,{ltr}); this.options.push(options); this.generate(options); return options; } subOptions:Options|Form|undefined; addSubOptions(name:string,{ltr=false}={}){ const options=new Options(name,this,{ltr}); this.subOptions=options; const container=this.container.deref(); if(container){ this.generateContainter(); }else{ throw new Error("Tried to make a subOptions when the options weren't rendered"); } return options; } addSubForm(name:string,onSubmit:((arg1:object)=>void),{ltr=false,submitText="Submit",fetchURL="",headers={},method="POST",traditionalSubmit=false}={}){ const options=new Form(name,this,onSubmit,{ltr,submitText,fetchURL,headers,method,traditionalSubmit}); this.subOptions=options; const container=this.container.deref(); if(container){ this.generateContainter(); }else{ throw new Error("Tried to make a subForm when the options weren't rendered"); } return options; } returnFromSub(){ this.subOptions=undefined; this.generateContainter(); } addSelect(label:string,onSubmit:(str:number)=>void,selections:string[],{defaultIndex=0}={}){ const select=new SelectInput(label,onSubmit,selections,this,{defaultIndex}); this.options.push(select); this.generate(select); return select; } addFileInput(label:string,onSubmit:(files:FileList)=>void,{clear=false}={}){ const FI=new FileInput(label,onSubmit,this,{clear}); this.options.push(FI); this.generate(FI); return FI; } addTextInput(label:string,onSubmit:(str:string)=>void,{initText="",password=false}={}){ const textInput=new TextInput(label,onSubmit,this,{initText,password}); this.options.push(textInput); this.generate(textInput); return textInput; } addColorInput(label:string,onSubmit:(str:string)=>void,{initColor=""}={}){ const colorInput=new ColorInput(label,onSubmit,this,{initColor}); this.options.push(colorInput); this.generate(colorInput); return colorInput; } addMDInput(label:string,onSubmit:(str:string)=>void,{initText=""}={}){ const mdInput=new MDInput(label,onSubmit,this,{initText}); this.options.push(mdInput); this.generate(mdInput); return mdInput; } addHTMLArea(html:(()=>HTMLElement)|HTMLElement,submit:()=>void=()=>{}){ const htmlarea=new HtmlArea(html,submit); this.options.push(htmlarea); this.generate(htmlarea); return htmlarea; } addButtonInput(label:string,textContent:string,onSubmit:()=>void){ const button=new ButtonInput(label,textContent,onSubmit,this); this.options.push(button); this.generate(button); return button; } addCheckboxInput(label:string,onSubmit:(str:boolean)=>void,{initState=false}={}){ const box=new CheckboxInput(label,onSubmit,this,{initState}); this.options.push(box); this.generate(box); return box; } addText(str:string){ const text=new SettingsText(str); this.options.push(text); this.generate(text); return text; } addTitle(str:string){ const text=new SettingsTitle(str); this.options.push(text); this.generate(text); return text; } generate(elm:OptionsElement){ const container=this.container.deref(); if(container){ const div=document.createElement("div"); if(!(elm instanceof Options)){ div.classList.add("optionElement"); } const html=elm.generateHTML(); div.append(html); this.html.set(elm,new WeakRef(div)); container.append(div); } } title:WeakRef=new WeakRef(document.createElement("h2")); generateHTML():HTMLElement{ const div=document.createElement("div"); div.classList.add("titlediv"); const title=document.createElement("h2"); title.textContent=this.name; div.append(title); if(this.name!=="") title.classList.add("settingstitle"); this.title=new WeakRef(title); const container=document.createElement("div"); this.container=new WeakRef(container); container.classList.add(this.ltr?"flexltr":"flexttb","flexspace"); this.generateContainter(); div.append(container); return div; } generateContainter(){ const container=this.container.deref(); if(container){ const title=this.title.deref(); if(title) title.innerHTML=""; container.innerHTML=""; if(this.subOptions){ container.append(this.subOptions.generateHTML());//more code needed, though this is enough for now if(title){ const name=document.createElement("span"); name.innerText=this.name; name.classList.add("clickable"); name.onclick=()=>{ this.returnFromSub(); }; title.append(name," > ",this.subOptions.name); } }else{ for(const thing of this.options){ this.generate(thing); } if(title){ title.innerText=this.name; } } if(title&&title.innerText!==""){ title.classList.add("settingstitle"); }else if(title){ title.classList.remove("settingstitle"); } }else{ console.warn("tried to generate container, but it did not exist"); } } changed(){ if(this.owner instanceof Options||this.owner instanceof Form){ this.owner.changed(); return; } if(!this.haschanged){ const div=document.createElement("div"); div.classList.add("flexltr","savediv"); const span=document.createElement("span"); div.append(span); span.textContent="Careful, you have unsaved changes"; const button=document.createElement("button"); button.textContent="Save changes"; div.append(button); this.haschanged=true; this.owner.changed(div); button.onclick=_=>{ if(this.owner instanceof Buttons){ this.owner.save(); } div.remove(); this.submit(); }; } } submit(){ this.haschanged=false; for(const thing of this.options){ thing.submit(); } } } class FormError extends Error{ elem:OptionsElement; message:string; constructor(elem:OptionsElement,message:string){ super(message); this.message=message; this.elem=elem; } } export{FormError}; class Form implements OptionsElement{ name:string; readonly options:Options; readonly owner:Options; readonly ltr:boolean; readonly names:Map>=new Map(); readonly required:WeakSet>=new WeakSet(); readonly submitText:string; readonly fetchURL:string; readonly headers={}; readonly method:string; value:object; traditionalSubmit:boolean; values={}; constructor(name:string,owner:Options,onSubmit:((arg1:object)=>void),{ltr=false,submitText="Submit",fetchURL="",headers={},method="POST",traditionalSubmit=false}={}){ this.traditionalSubmit=traditionalSubmit; this.name=name; this.method=method; this.submitText=submitText; this.options=new Options("",this,{ltr}); this.owner=owner; this.fetchURL=fetchURL; this.headers=headers; this.ltr=ltr; this.onSubmit=onSubmit; } setValue(key:string,value:any){//the value can't really be anything, but I don't care enough to fix this this.values[key]=value; } addSelect(label:string,formName:string,selections:string[],{defaultIndex=0,required=false}={}){ const select=this.options.addSelect(label,_=>{},selections,{defaultIndex}); this.names.set(formName,select); if(required){ this.required.add(select); } return select; } addFileInput(label:string,formName:string,{required=false}={}){ const FI=this.options.addFileInput(label,_=>{},{}); this.names.set(formName,FI); if(required){ this.required.add(FI); } return FI; } addTextInput(label:string,formName:string,{initText="",required=false,password=false}={}){ const textInput=this.options.addTextInput(label,_=>{},{initText,password}); this.names.set(formName,textInput); if(required){ this.required.add(textInput); } return textInput; } addColorInput(label:string,formName:string,{initColor="",required=false}={}){ const colorInput=this.options.addColorInput(label,_=>{},{initColor}); this.names.set(formName,colorInput); if(required){ this.required.add(colorInput); } return colorInput; } addMDInput(label:string,formName:string,{initText="",required=false}={}){ const mdInput=this.options.addMDInput(label,_=>{},{initText}); this.names.set(formName,mdInput); if(required){ this.required.add(mdInput); } return mdInput; } addCheckboxInput(label:string,formName:string,{initState=false,required=false}={}){ const box=this.options.addCheckboxInput(label,_=>{},{initState}); this.names.set(formName,box); if(required){ this.required.add(box); } return box; } addText(str:string){ this.options.addText(str); } addTitle(str:string){ this.options.addTitle(str); } generateHTML():HTMLElement{ const div=document.createElement("div"); div.append(this.options.generateHTML()); div.classList.add("FormSettings"); if(!this.traditionalSubmit){ const button=document.createElement("button"); button.onclick=_=>{ this.submit(); }; button.textContent=this.submitText; div.append(button); } return div; } onSubmit:((arg1:object)=>void); watchForChange(func:(arg1:object)=>void){ this.onSubmit=func; } changed(){ if(this.traditionalSubmit){ this.owner.changed(); } } submit(){ const build={}; for(const key of Object.keys(this.values)){ const thing=this.values[key]; if(thing instanceof Function){ try{ build[key]=thing(); }catch(e:any){ if(e instanceof FormError){ const elm=this.options.html.get(e.elem); if(elm){ const html=elm.deref(); if(html){ this.makeError(html,e.message); } } } return; } }else{ build[key]=thing; } } for(const thing of this.names.keys()){ if(thing==="")continue; const input=this.names.get(thing) as OptionsElement; if(input instanceof SelectInput){ build[thing]=input.options[input.value]; continue; } build[thing]=input.value; } if(this.fetchURL!==""){ fetch(this.fetchURL,{ method: this.method, body: JSON.stringify(build), headers: this.headers }).then(_=>_.json()).then(json=>{ if(json.errors&&this.errors(json.errors))return; this.onSubmit(json); }); }else{ this.onSubmit(build); } console.warn("needs to be implemented"); } errors(errors:{code:number,message:string,errors:{[key:string]:{_errors:{message:string,code:string}}}}){ if(!(errors instanceof Object)){ return; } for(const error of Object.keys(errors)){ const elm=this.names.get(error); if(elm){ const ref=this.options.html.get(elm); if(ref&&ref.deref()){ const html=ref.deref() as HTMLDivElement; this.makeError(html,errors[error]._errors[0].message); return true; } } } return false; } error(formElm:string,errorMessage:string){ const elm=this.names.get(formElm); if(elm){ const htmlref=this.options.html.get(elm); if(htmlref){ const html=htmlref.deref(); if(html){ this.makeError(html,errorMessage); } } }else{ console.warn(formElm+" is not a valid form property"); } } makeError(e:HTMLDivElement,message:string){ let element=e.getElementsByClassName("suberror")[0] as HTMLElement; if(!element){ const div=document.createElement("div"); div.classList.add("suberror","suberrora"); e.append(div); element=div; }else{ element.classList.remove("suberror"); setTimeout(_=>{ element.classList.add("suberror"); },100); } element.textContent=message; } } class Settings extends Buttons{ static readonly Buttons=Buttons; static readonly Options=Options; html:HTMLElement|null; constructor(name:string){ super(name); } addButton(name:string,{ltr=false}={}):Options{ const options=new Options(name,this,{ltr}); this.add(name,options); return options; } show(){ const background=document.createElement("div"); background.classList.add("background"); const title=document.createElement("h2"); title.textContent=this.name; title.classList.add("settingstitle"); background.append(title); background.append(this.generateHTML()); const exit=document.createElement("span"); exit.textContent="✖"; exit.classList.add("exitsettings"); background.append(exit); exit.onclick=_=>{ this.hide(); }; document.body.append(background); this.html=background; } hide(){ if(this.html){ this.html.remove(); this.html=null; } } } export{Settings,OptionsElement,Buttons,Options};