const PassiveListener = { passive: true }; const LocalGet = { method: 'GET', mode: 'same-origin', redirect: 'follow' }; // 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); } // Add/remove class entries in bulk; tags is an Array each containing // an Array of arguments to toggle. On return the second element of // each Array will be updated to the current value. function classmod(elem,tags) { for (var i = 0; i < tags.length; i++) tags[i][1] = elem.classList.toggle(...tags[i]); return tags; } // Read a key=value text file and return it as a Promise of a Map function fetchconfig(url) { return fetch(url, LocalGet) .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; }); } // Get the value from an input field if valid, otherwise its default function valval(ie) { return ie.checkValidity() ? ie.value : ie.defaultValue; } // Parse a string for a valid boolean truth value function cfgbool(str) { return !!str && !str.match(/^([0fnd]|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,ro = false) { form = getelem(form); for (var e of form.elements) { if (e.classList.contains('noinit') || e instanceof HTMLFieldSetElement) continue; if (ro && e.disabled != undefined && !e.classList.contains('noro')) e.disabled = true; if (e instanceof HTMLInputElement || e instanceof HTMLSelectElement) { const val = map.get(e.name); if (val == null) continue; if (e.type == 'radio') e.checked = (val == e.value); else e.value = val; } else if (e instanceof HTMLButtonElement) { e.disabled = ro; } } } // Load form initialization data function loadform(form,url,ro = false) { fetchconfig(url) .then((map) => {initform(form,map,ro); }) .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 (optional) // progress and response text, and redirect after success 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); } classmod(form, [['started',1],['done',0],['ok',0],['err',0],['running',1]]); xhr.addEventListener('loadend', (e) => { const ok = xhr.status >= 200 && xhr.status < 400; if (progress && ok) progress.value = progress.max; var result = chi(form,'output'); if (result) { var msg = xhr.responseText.trimEnd(); if (!msg) msg = xhr.status + ' ' + xhr.statusText; result.textContent = msg; } classmod(form, [['ok',ok],['err',!ok],['running',0],['done',1]]); // Optionally redirect elsewhere after success const reft = parseFloat(form.dataset.ref) * 1000; if (ok && reft >= 0.0) { const refh = form.dataset.refUrl || window.location.href; setTimeout(() => { window.location.href = refh; }, reft); } }, PassiveListener); xhr.open(form.method, form.action); xhr.responseType = 'text'; xhr.send(data); return xhr; } // 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 == '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; var files = form.elements['file']; if (files == undefined) return upload(form,textformdata(form)); else if (files.files.length == 1) return upload(form,files.files[0]); else return files.click(); } // Flip the status of an INPUT element between text and password function showpwd(me = event.currentTarget) { 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); var lang_styleobj = document.createElement('style'); document.head.append(lang_styleobj); function setlang(l = null) { l = l || document.documentElement.lang; if (!l.match(/^\w+(?:-\w+)*$/)) return; // Invalid language tag var sty = lang_styleobj.sheet; while (sty.rules.length) sty.deleteRule(0); sty.insertRule('[lang]:not(:lang('+l+')):not(:last-child),[lang]:lang('+l+')~[lang] {display: none}', 0); document.documentElement.lang = l; } setlang(); fetchconfig('/sys/lang') .then((map) => { setlang(map.get('LANG')); translations = map; if (delay_translate) translate(); }) .catch(() => {}); // HTML include hack class IncHTML extends HTMLElement { constructor() { self = super(); } connectedCallback() { fetch(self.getAttribute('src'), LocalGet) .then ((r) => r.text()) .then ((text) => { const p = self.parentElement; self.outerHTML = text; translate(p); }); } } customElements.define('x-inc', IncHTML); // Smart checkbox INPUT element class XBox extends HTMLInputElement { set negative(x) { return this.toggleAttribute('negative', x); } get negative() { return this.hasAttribute('negative'); } #truth; get truth() { return this.#truth; } update_truth() { const old_truth = this.#truth; this.#truth = this.checked != this.negative; if (this.#truth !== old_truth) this.resolve_conflicts(); } set truth(x) { const new_truth = !!x; if (new_truth !== this.#truth) { this.checked = new_truth != this.negative; this.update_truth(); } } set_list(n,x) { this.setAttribute(n, x ? x.join(',') : ''); } get_list(n) { var a = this.getAttribute(n); return a ? a.split(/\s*;\s*/) : []; } get needs() { return this.get_list('needs'); } set conflicts(x) { this.set_list('conflicts',x); } get conflicts() { return this.get_list('conflicts'); } set value(x) { this.truth = cfgbool(x); } get value() { return this.truth ? '1' : '0'; } constructor() { super(); this.setAttribute('type', 'checkbox'); this.checked = cfgbool(this.getAttribute('value')) != this.negative; this.addEventListener('change', this.update_truth); this.update_truth(); } connectedCallback() { this.update_truth(); } adoptedCallback() { this.update_truth(); } static get observedAttributes() { return ['value', 'negative', 'conflicts', 'needs']; } attributeChangedCallback(name, oldval, newval) { if (oldval === newval) return; if (name == 'value') { this.truth = cfgbool(newval); } else if (name == 'negative') { this.checked = this.#truth != this.negative; } else if (name == 'conflicts' || name == 'needs') { this.resolve_conflicts(); } } form_objects(list) { if (!this.form) return []; return list.map((n) => this.form.elements[n]); } do_resolve(which, sense) { for (const e of this.form_objects(which)) { if (e instanceof XBox) e.truth = sense; } } #resolving; resolve_conflicts() { const t = this.#truth; if (this.#resolving || !this.form) return; this.#resolving = true; if (t) { this.do_resolve(this.needs, true); this.do_resolve(this.conflicts, false); } else { for (const e of this.form.elements) { if (e instanceof XBox && e.truth) { if (e.form_objects(e.needs).some((x) => x === this)) e.truth = false; } } } this.#resolving = false; } } customElements.define('x-box', XBox, { extends: 'input' });