|
@@ -1,8 +1,16 @@
|
|
|
+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"})
|
|
@@ -26,45 +34,39 @@ function fetchconfig(url) {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
-// Initialize a form from a 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 Map;
|
|
|
+ var clearers = new Set;
|
|
|
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 (val == null) {
|
|
|
- } else if (field.type == 'checkbox') {
|
|
|
- const checked = !val.match(/^(0*|[fnd].*|of.*)$/i);
|
|
|
- field.checked = checked;
|
|
|
- field.value = '1';
|
|
|
- } else if (field.type == 'radio') {
|
|
|
- field.checked = (val == field.name);
|
|
|
- } else if (field.type == 'hidden' &&
|
|
|
- field.classList.contains('_clr')) {
|
|
|
- field.remove();
|
|
|
+ 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 {
|
|
|
- field.value = val;
|
|
|
+ e.value = val;
|
|
|
}
|
|
|
- } else if (field instanceof HTMLButtonElement &&
|
|
|
- field.type == 'submit') {
|
|
|
- button = field;
|
|
|
+ } else if (e instanceof HTMLButtonElement &&
|
|
|
+ e.type == 'submit') {
|
|
|
+ button = e;
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- for (const what of clearers.keys()) {
|
|
|
- var clearer = document.createElement('INPUT');
|
|
|
- clearer.type = 'hidden';
|
|
|
- clearer.name = what;
|
|
|
- clearer.value = '0';
|
|
|
- clearer.classList.add('_clr');
|
|
|
- form.prepend(clearer);
|
|
|
- }
|
|
|
-
|
|
|
if (button) {
|
|
|
button.disabled = false; // All loaded, enable the submit button
|
|
|
}
|
|
@@ -77,14 +79,23 @@ function loadform(form, url) {
|
|
|
.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) { };
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -92,68 +103,114 @@ 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) {
|
|
|
+ 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');
|
|
|
}
|
|
|
- if (result != undefined) {
|
|
|
- result.className = "result " + (ok ? "ok" : "err");
|
|
|
- result.innerText = xhr.responseText;
|
|
|
- }
|
|
|
- });
|
|
|
+ const rf = parseInt(xhr.getResponseHeader('Refresh'));
|
|
|
+ if (rf && ok)
|
|
|
+ setTimeout(() => window.location.reload(true), rf * 1000);
|
|
|
+ }, PassiveListener);
|
|
|
|
|
|
- xhr.open("POST", form.action);
|
|
|
+ xhr.open(form.method, form.action);
|
|
|
xhr.responseType = 'text';
|
|
|
- xhr.send(file);
|
|
|
+ xhr.send(data);
|
|
|
+ return xhr;
|
|
|
}
|
|
|
|
|
|
-// Enable a button (this ensures that the necessary scripts have been
|
|
|
-// run before one can submit)
|
|
|
-function enablebutton(id,on) {
|
|
|
- getelem(id).disabled = !on;
|
|
|
+// 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(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);
|
|
|
+
|
|
|
+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 <script> tags.
|
|
@@ -161,10 +218,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);
|
|
|
+ });
|
|
|
}
|