max80.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. const PassiveListener = { passive: true };
  2. const LocalGet = { method: 'GET', mode: 'same-origin',
  3. redirect: 'follow' };
  4. // Get an element by id or an Element object
  5. function getelem(id) {
  6. return (id instanceof Element) ? id : document.getElementById(id);
  7. }
  8. // Find a child with a specific tag
  9. function chi(me,tag) { return me.getElementsByTagName(tag)[0]; }
  10. // Find a sibling element with a specific tag
  11. function sib(me,tag) { return chi(me.parentElement, tag); }
  12. // Add/remove class entries in bulk; tags is an Array each containing
  13. // an Array of arguments to toggle. On return the second element of
  14. // each Array will be updated to the current value.
  15. function classmod(elem,tags) {
  16. for (var i = 0; i < tags.length; i++)
  17. tags[i][1] = elem.classList.toggle(...tags[i]);
  18. return tags;
  19. }
  20. // Read a key=value text file and return it as a Promise of a Map
  21. function fetchconfig(url) {
  22. return fetch(url, LocalGet)
  23. .then(res => {
  24. if (!res.ok) {
  25. throw new Error('HTTP error '+response.status);
  26. } else {
  27. return res.text();
  28. }
  29. })
  30. .then(text => {
  31. var map = new Map();
  32. for (const c of text.split(/[\r\n]+/)) {
  33. var m = c.match(/^\s*([\;\/]?)((?:"[^"]*"|[^"])*?)\s*=(.*)$/);
  34. if (m && m[1] == "") {
  35. var k = m[2].replaceAll(/(^"|"$|(")")/g, "$2");
  36. map.set(k, m[3]);
  37. }
  38. }
  39. return map;
  40. });
  41. }
  42. // Get the value from an input field if valid, otherwise its default
  43. function valval(ie) {
  44. return ie.checkValidity() ? ie.value : ie.defaultValue;
  45. }
  46. // Parse a string for a valid boolean truth value
  47. function cfgbool(str) {
  48. return str && !str.match(/^(0*|[fnd].*|of.*)$/i);
  49. }
  50. // Initialize a form from a map. Checkboxes take a cfgbool string;
  51. // if in the HTML their value is set to a false cfgbool string then
  52. // the checkbox is inverted logic.
  53. function initform(form,map,ro = false) {
  54. form = getelem(form);
  55. for (var e of form.elements) {
  56. if (e.classList.contains('noinit') ||
  57. e instanceof HTMLFieldSetElement)
  58. continue;
  59. if (ro && e.disabled != undefined && !e.classList.contains('noro'))
  60. e.disabled = true;
  61. if (e instanceof HTMLInputElement ||
  62. e instanceof HTMLSelectElement) {
  63. const val = map.get(e.name);
  64. if (val == null)
  65. continue;
  66. if (e.type == 'checkbox') {
  67. e.checked = cfgbool(val) == cfgbool(e.value);
  68. } else if (e.type == 'radio') {
  69. e.checked = (val == e.value);
  70. } else {
  71. e.value = val;
  72. }
  73. } else if (e instanceof HTMLButtonElement) {
  74. e.disabled = ro;
  75. }
  76. }
  77. }
  78. // Load form initialization data
  79. function loadform(form,url,ro = false) {
  80. fetchconfig(url)
  81. .then((map) => {initform(form,map,ro); })
  82. .catch(() => {});
  83. }
  84. // Replace the contents of selected HTML elements based on a map with
  85. // selectors.
  86. function fillin(map,html = false,top = document) {
  87. for (const [key,val] of map) {
  88. try {
  89. const m = key.match(/(.*?)(?:\;([\w.-]*)(\??))?$/);
  90. for (var e of top.querySelectorAll(m[1])) {
  91. try {
  92. if (!html) e.textContents = val;
  93. else if (!m[2]) e.innerHTML = val;
  94. else if (!m[3]) e.setAttribute(m[2],val);
  95. else if (!cfgbool(val)) e.removeAttribute(m[2]);
  96. else e.setAttribute(m[2],'');
  97. } catch(e) { };
  98. }
  99. } catch(e) { };
  100. }
  101. }
  102. // Load status or HTML data
  103. function load(url,html = false)
  104. {
  105. fetchconfig(url)
  106. .then(map => fillin(map,html))
  107. .catch(() => {});
  108. }
  109. // POST upload of data from within a form, with (optional)
  110. // progress and response text, and redirect after success
  111. function upload(form,data) {
  112. var xhr = new XMLHttpRequest();
  113. var progress = chi(form,'progress');
  114. if (progress) {
  115. progress.value = 0;
  116. xhr.upload.addEventListener('progress', (e) => {
  117. if (!e.lengthComputable)
  118. return;
  119. progress.max = e.total * 1.05;
  120. progress.value = e.loaded;
  121. }, PassiveListener);
  122. }
  123. classmod(form, [['started',1],['done',0],['ok',0],['err',0],['running',1]]);
  124. xhr.addEventListener('loadend', (e) => {
  125. const ok = xhr.status >= 200 && xhr.status < 400;
  126. if (progress && ok)
  127. progress.value = progress.max;
  128. var result = chi(form,'output');
  129. if (result) {
  130. var msg = xhr.responseText.trimEnd();
  131. if (!msg)
  132. msg = xhr.status + ' ' + xhr.statusText;
  133. result.textContent = msg;
  134. }
  135. classmod(form, [['ok',ok],['err',!ok],['running',0],['done',1]]);
  136. // Optionally redirect elsewhere after success
  137. const reft = parseFloat(form.dataset.ref) * 1000;
  138. if (ok && reft >= 0.0) {
  139. const refh = form.dataset.refUrl || window.location.href;
  140. setTimeout(() => { window.location.href = refh; }, reft);
  141. }
  142. }, PassiveListener);
  143. xhr.open(form.method, form.action);
  144. xhr.responseType = 'text';
  145. xhr.send(data);
  146. return xhr;
  147. }
  148. // key=value formatting of form data; including inverted checkboxes
  149. function textformdata(form) {
  150. var data = '';
  151. for (var e of form.elements) {
  152. var val = e.value;
  153. if (val == undefined || !e.name || e instanceof HTMLButtonElement) {
  154. continue;
  155. } else if (e instanceof HTMLInputElement) {
  156. if (e.type == 'checkbox')
  157. val = e.checked == cfgbool(val) ? '1' : '0';
  158. else if (e.type == 'radio' && !e.checked)
  159. continue;
  160. }
  161. data += e.name + '=' + val + "\r\n";
  162. }
  163. return data;
  164. }
  165. // POST form contents upload with response text
  166. function uploadform() {
  167. event.preventDefault();
  168. const form = event.target.form || event.target;
  169. var files = form.elements['file'];
  170. if (files == undefined)
  171. return upload(form,textformdata(form));
  172. else if (files.files.length == 1)
  173. return upload(form,files.files[0]);
  174. else
  175. return files.click();
  176. }
  177. // Flip the status of an INPUT element between text and password
  178. function showpwd(me = event.currentTarget) {
  179. const now_visible = me.classList.toggle('hide');
  180. const new_type = now_visible ? 'text' : 'password';
  181. me.classList.toggle('show',!now_visible);
  182. sib(me,'input').setAttribute('type', new_type);
  183. }
  184. // Insert translations as needed
  185. var translations = null;
  186. var delay_translate = false;
  187. function translate(top = document) {
  188. delay_translate = (!translations || document.readyState != 'complete');
  189. if (!delay_translate)
  190. fillin(translations, true, top);
  191. }
  192. document.addEventListener('load', (e) => translate(), PassiveListener);
  193. var lang_styleobj = document.createElement('style');
  194. document.head.append(lang_styleobj);
  195. function setlang(l = null) {
  196. l = l || document.documentElement.lang;
  197. if (!l.match(/^\w+(?:-\w+)*$/)) return; // Invalid language tag
  198. var sty = lang_styleobj.sheet;
  199. while (sty.rules.length) sty.deleteRule(0);
  200. sty.insertRule('[lang]:not(:lang('+l+')):not(:last-child),[lang]:lang('+l+')~[lang] {display: none}', 0);
  201. document.documentElement.lang = l;
  202. }
  203. setlang();
  204. fetchconfig('/sys/lang')
  205. .then((map) => {
  206. setlang(map.get('LANG'));
  207. translations = map;
  208. if (delay_translate)
  209. translate();
  210. })
  211. .catch(() => {});
  212. // HTML include hack
  213. class IncHTML extends HTMLElement {
  214. constructor() { self = super(); }
  215. connectedCallback() {
  216. fetch(self.getAttribute('src'), LocalGet)
  217. .then ((r) => r.text())
  218. .then ((text) => {
  219. const p = self.parentElement;
  220. self.outerHTML = text;
  221. translate(p);
  222. });
  223. }
  224. }
  225. customElements.define('x-inc', IncHTML);