const PassiveListener = { passive: true }; // Get an element by id or an Element object function getelem(id) { return (id instanceof Element) ? id : document.getElementById(id); } // Find a child with a specific tag function chi(me,tag) { return me.getElementsByTagName(tag)[0]; } // Find a sibling element with a specific tag function sib(me,tag) { return chi(me.parentElement, tag); } // Read a key=value text file and return it as a Promise of a Map function fetchconfig(url) { return fetch(url, {redirect: "follow"}) .then(res => { if (!res.ok) { throw new Error("HTTP error "+response.status); } else { return res.text(); } }) .then(text => { var map = new Map(); for (const c of text.split(/[\r\n]+/)) { var m = c.match(/^\s*([\;\/]?)((?:"[^"]*"|[^"])*?)\s*=(.*)$/); if (m && m[1] == "") { var k = m[2].replaceAll(/(^"|"$|(")")/g, "$2"); map.set(k, m[3]); } } return map; }); } // Parse a string for a valid boolean truth value function cfgbool(str) { return str && !str.match(/^(0*|[fnd].*|of.*)$/i); } // Initialize a form from a map. Checkboxes take a cfgbool string; // if in the HTML their value is set to a false cfgbool string then // the checkbox is inverted logic. function initform(form,map) { var button = null; var clearers = new Set; form = getelem(form); for (var e of form.elements) { if (e.classList.contains('noinit')) { continue; } else if (e instanceof HTMLInputElement || e instanceof HTMLSelectElement) { const val = map.get(e.name); if (val == null) continue; if (e.type == 'checkbox') { e.checked = cfgbool(val) == cfgbool(e.value); } else if (e.type == 'radio') { e.checked = (val == e.name); } else { e.value = val; } } else if (e instanceof HTMLButtonElement && e.type == 'submit') { button = e; } } if (button) { button.disabled = false; // All loaded, enable the submit button } } // Load form initialization data function loadform(form, url) { fetchconfig(url) .then(map => { initform(form, map) }) .catch(() => {}); } // Replace the contents of selected HTML elements based on a map with // selectors. function fillin(map,html = false,top = document) { for (const [key,val] of map) { try { const m = key.match(/(.*?)(?:\;([\w.-]*)(\??))?$/); for (var e of top.querySelectorAll(m[1])) { try { if (!html) e.textContents = val; else if (!m[2]) e.innerHTML = val; else if (!m[3]) e.setAttribute(m[2],val); else if (!cfgbool(val)) e.removeAttribute(m[2]); else e.setAttribute(m[2],''); } catch(e) { }; } } catch(e) { }; } } // Load status or HTML data function load(url,html = false) { fetchconfig(url) .then(map => fillin(map,html)) .catch(() => {}); } // POST upload of data from within a form, with progress and response text function upload(form,data) { var xhr = new XMLHttpRequest(); var progress = chi(form,'progress'); if (progress) { progress.value = 0; xhr.upload.addEventListener('progress', (e) => { if (!e.lengthComputable) return; progress.max = e.total * 1.05; progress.value = e.loaded; }, PassiveListener); } var result = chi(form,'output'); if (result) result.classList.remove('result'); xhr.addEventListener('readystatechange', (e) => { if (xhr.readyState != XMLHttpRequest.DONE) return; const ok = xhr.status >= 200 && xhr.status < 400; if (progress) progress.value = ok ? progress.max : 0; if (result) { var msg = xhr.responseText.trimEnd(); if (!msg) msg = xhr.status + ' ' + xhr.statusText; result.textContent = msg; result.classList.toggle('ok', ok); result.classList.toggle('err', !ok); result.classList.add('result'); } const rf = parseInt(xhr.getResponseHeader('Refresh')); if (rf && ok) setTimeout(() => window.location.reload(), rf * 1000); }, PassiveListener); xhr.open(form.method, form.action); xhr.responseType = 'text'; xhr.send(data); return xhr; } // Upload a data file blob function uploadfile() { event.preventDefault(); const form = event.target.form || event.target; var files = form.elements['file']; return (files.files.length == 1) ? upload(form,files.files[0]) : files.click(); } // key=value formatting of form data; including inverted checkboxes function textformdata(form) { var data = ''; for (var e of form.elements) { var val = e.value; if (val == undefined || !e.name || e instanceof HTMLButtonElement) { continue; } else if (e instanceof HTMLInputElement) { if (e.type == 'checkbox') val = e.checked == cfgbool(val) ? '1' : '0'; else if (e.type == 'radio' && !e.checked) continue; } data += e.name + '=' + val + "\r\n"; } return data; } // POST form contents upload with response text function uploadform() { event.preventDefault(); const form = event.target.form || event.target; return upload(form,textformdata(form)); } // Flip the status of an INPUT element between text and password function showpwd(me = event.target) { const now_visible = me.classList.toggle('hide'); const new_type = now_visible ? 'text' : 'password'; me.classList.toggle('show',!now_visible); sib(me,'input').setAttribute('type', new_type); } // Insert translations as needed var translations = null; var delay_translate = false; function translate(top = document) { delay_translate = (!translations || document.readyState != 'complete'); if (!delay_translate) fillin(translations, true, top); } document.addEventListener('load', (e) => translate(), PassiveListener); fetchconfig('/sys/lang') .then((map) => { translations = map; if (delay_translate) translate(); }) .catch(() => {}); // Hack to include an HTML files. Sadly, does not support // including files with