|
@@ -1,8 +1,25 @@
|
|
|
+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); }
|
|
|
+
|
|
|
+// 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, {redirect: "follow"})
|
|
@@ -26,53 +43,66 @@ function fetchconfig(url) {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
-function boolcfg(str) { return str && !str.match(/^(0*|[fnd].*|of.*)$/i); }
|
|
|
+// 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
|
|
|
-function initform(form,map) {
|
|
|
- var button = null;
|
|
|
- var clearers = new Map;
|
|
|
+// 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 field of form.elements) {
|
|
|
- if ((field instanceof HTMLInputElement ||
|
|
|
- field instanceof HTMLSelectElement) &&
|
|
|
- !field.classList.contains("noload")) {
|
|
|
- const val = map.get(field.name) || '';
|
|
|
- if (field.type == 'checkbox') {
|
|
|
- field.checked = !boolcfg(val);
|
|
|
- } else if (field.type == 'radio') {
|
|
|
- field.checked = (val == field.name);
|
|
|
- field.remove();
|
|
|
+ for (var e of form.elements) {
|
|
|
+ if (e.classList.contains('noinit'))
|
|
|
+ continue;
|
|
|
+ if (ro && e.disabled != undefined)
|
|
|
+ e.disabled = true;
|
|
|
+ if (e instanceof HTMLInputElement ||
|
|
|
+ e instanceof HTMLSelectElement) {
|
|
|
+ const val = map.get(e.name);
|
|
|
+ if (val == null)
|
|
|
+ continue;
|
|
|
+ if (ro && e.disabled != undefined)
|
|
|
+ e.disabled = true;
|
|
|
+ if (e.type == 'checkbox') {
|
|
|
+ e.checked = cfgbool(val) == cfgbool(e.value);
|
|
|
+ } else if (e.type == 'radio') {
|
|
|
+ e.checked = (val == e.name);
|
|
|
} else {
|
|
|
- field.value = val;
|
|
|
+ e.value = val;
|
|
|
}
|
|
|
- } else if (field instanceof HTMLButtonElement &&
|
|
|
- field.type == 'submit') {
|
|
|
- button = field;
|
|
|
+ } else if (e instanceof HTMLButtonElement) {
|
|
|
+ e.disabled = ro;
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- if (button) {
|
|
|
- button.disabled = false; // All loaded, enable the submit button
|
|
|
- }
|
|
|
}
|
|
|
|
|
|
// Load form initialization data
|
|
|
-function loadform(form, url) {
|
|
|
+function loadform(form,url,ro = false) {
|
|
|
fetchconfig(url)
|
|
|
- .then(map => { initform(form, map) })
|
|
|
+ .then((map) => initform(form,map,ro))
|
|
|
.catch(() => {});
|
|
|
}
|
|
|
|
|
|
-// Replace the contents of selected HTML elements based on a map with selectors
|
|
|
-function fillin(map,html)
|
|
|
+// 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 {
|
|
|
- for (var e of document.querySelectorAll(key))
|
|
|
- if (html) e.innerHTML = val; else e.innerText = val;
|
|
|
- } catch (error) { }
|
|
|
+ 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) { };
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -80,71 +110,125 @@ function fillin(map,html)
|
|
|
function load(url,html = false)
|
|
|
{
|
|
|
fetchconfig(url)
|
|
|
- .then(map => { fillin(map,html) })
|
|
|
+ .then(map => fillin(map,html))
|
|
|
.catch(() => {});
|
|
|
}
|
|
|
|
|
|
-// POST file upload with progress and response text
|
|
|
-function uploadfile(event) {
|
|
|
- event.preventDefault();
|
|
|
- var form = event.currentTarget;
|
|
|
- var elem = form.elements;
|
|
|
- var files = elem["file"].files;
|
|
|
- if (files.length != 1) {
|
|
|
- /* Show error */
|
|
|
- return;
|
|
|
- }
|
|
|
- var file = files[0];
|
|
|
+// POST upload of data from within a form, with progress and response text
|
|
|
+function upload(form,data) {
|
|
|
var xhr = new XMLHttpRequest();
|
|
|
|
|
|
- var progress = form.querySelector("progress");
|
|
|
- if (progress != undefined) {
|
|
|
+ var progress = chi(form,'progress');
|
|
|
+ if (progress) {
|
|
|
progress.value = 0;
|
|
|
- xhr.upload.addEventListener("progress", (event) => {
|
|
|
- if (event.lengthComputable) {
|
|
|
- progress.max = event.total * 1.05;
|
|
|
- progress.value = event.loaded;
|
|
|
- }
|
|
|
- });
|
|
|
+ xhr.upload.addEventListener('progress', (e) => {
|
|
|
+ if (!e.lengthComputable)
|
|
|
+ return;
|
|
|
+ progress.max = e.total * 1.05;
|
|
|
+ progress.value = e.loaded;
|
|
|
+ }, PassiveListener);
|
|
|
}
|
|
|
|
|
|
- var result = form.querySelector("pre.result");
|
|
|
- if (result != undefined) {
|
|
|
- result.className = "result hide";
|
|
|
- }
|
|
|
- xhr.addEventListener("loadend", (event) => {
|
|
|
- const ok = xhr.status >= 200 && xhr.status <= 299;
|
|
|
- if (progress != undefined) {
|
|
|
- progress.value = ok ? progress.max : 0;
|
|
|
- }
|
|
|
- if (result != undefined) {
|
|
|
- result.className = "result " + (ok ? "ok" : "err");
|
|
|
- result.innerText = xhr.responseText;
|
|
|
+ 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;
|
|
|
}
|
|
|
- });
|
|
|
|
|
|
- xhr.open("POST", form.action);
|
|
|
+ classmod(form, [['ok',ok],['err',!ok],['running',0],['done',1]]);
|
|
|
+
|
|
|
+ // Automatically reload the page after successful upload
|
|
|
+ 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(file);
|
|
|
+ xhr.send(data);
|
|
|
+ return xhr;
|
|
|
}
|
|
|
|
|
|
-// POST upload of a form as key=value pairs, *including* transmitting
|
|
|
-// unchecked checkboxes as name=0.
|
|
|
+// 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();
|
|
|
+}
|
|
|
|
|
|
-// Enable a button (this ensures that the necessary scripts have been
|
|
|
-// run before one can submit)
|
|
|
-function enablebutton(id,on) {
|
|
|
- getelem(id).disabled = !on;
|
|
|
+// 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(id,me) {
|
|
|
- var pwd = getelem(id);
|
|
|
+function showpwd(me = event.target) {
|
|
|
const now_visible = me.classList.toggle('hide');
|
|
|
- me.classList.toggle('show',!now_visible);
|
|
|
const new_type = now_visible ? 'text' : 'password';
|
|
|
- pwd.setAttribute('type', new_type);
|
|
|
+ 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(lang) {
|
|
|
+ if (lang) {
|
|
|
+ if (lang_styleobj.sheet.rules.length)
|
|
|
+ lang_styleobj.sheet.deleteRule(0);
|
|
|
+ lang_styleobj.sheet.insertRule('[lang]:not([lang="'+lang+'"]) { display: none; }');
|
|
|
+ }
|
|
|
}
|
|
|
+setlang('en');
|
|
|
+
|
|
|
+fetchconfig('/sys/lang')
|
|
|
+ .then((map) => {
|
|
|
+ setlang(map.get('LANG'));
|
|
|
+ translations = map;
|
|
|
+ if (delay_translate)
|
|
|
+ translate();
|
|
|
+ })
|
|
|
+ .catch(() => {});
|
|
|
|
|
|
// Hack to include an HTML files. Sadly, does not support
|
|
|
// including files with <script> tags.
|
|
@@ -152,10 +236,9 @@ function inc(url) {
|
|
|
var me = document.currentScript;
|
|
|
fetch(url, {redirect: "follow"})
|
|
|
.then((response) => response.text())
|
|
|
- .then((text) => { me.outerHTML = text; });
|
|
|
-}
|
|
|
-
|
|
|
-// Insert translations
|
|
|
-function translate() {
|
|
|
- load('/sys/lang', true);
|
|
|
+ .then((text) => {
|
|
|
+ const p = me.parentElement;
|
|
|
+ me.outerHTML = text;
|
|
|
+ translate(p);
|
|
|
+ });
|
|
|
}
|