123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252 |
- 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(/^(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,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 == 'checkbox') {
- e.checked = cfgbool(val) == cfgbool(e.value);
- } else 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 == '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;
- 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);
|