max80.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  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.name);
  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 progress and response text
  110. function upload(form,data) {
  111. var xhr = new XMLHttpRequest();
  112. var progress = chi(form,'progress');
  113. if (progress) {
  114. progress.value = 0;
  115. xhr.upload.addEventListener('progress', (e) => {
  116. if (!e.lengthComputable)
  117. return;
  118. progress.max = e.total * 1.05;
  119. progress.value = e.loaded;
  120. }, PassiveListener);
  121. }
  122. classmod(form, [['started',1],['done',0],['ok',0],['err',0],['running',1]]);
  123. xhr.addEventListener('loadend', (e) => {
  124. const ok = xhr.status >= 200 && xhr.status < 400;
  125. if (progress && ok)
  126. progress.value = progress.max;
  127. var result = chi(form,'output');
  128. if (result) {
  129. var msg = xhr.responseText.trimEnd();
  130. if (!msg)
  131. msg = xhr.status + ' ' + xhr.statusText;
  132. result.textContent = msg;
  133. }
  134. classmod(form, [['ok',ok],['err',!ok],['running',0],['done',1]]);
  135. // Optionally redirect elsewhere after success
  136. const reft = parseFloat(form.dataset.ref) * 1000;
  137. if (ok && reft >= 0.0) {
  138. const refh = form.dataset.refUrl || window.location.href;
  139. setTimeout(() => { window.location.href = refh; }, reft);
  140. }
  141. }, PassiveListener);
  142. xhr.open(form.method, form.action);
  143. xhr.responseType = 'text';
  144. xhr.send(data);
  145. return xhr;
  146. }
  147. // Upload a data file blob
  148. function uploadfile() {
  149. event.preventDefault();
  150. const form = event.target.form || event.target;
  151. var files = form.elements['file'];
  152. return (files.files.length == 1)
  153. ? upload(form,files.files[0]) : files.click();
  154. }
  155. // key=value formatting of form data; including inverted checkboxes
  156. function textformdata(form) {
  157. var data = '';
  158. for (var e of form.elements) {
  159. var val = e.value;
  160. if (val == undefined || !e.name || e instanceof HTMLButtonElement) {
  161. continue;
  162. } else if (e instanceof HTMLInputElement) {
  163. if (e.type == 'checkbox')
  164. val = e.checked == cfgbool(val) ? '1' : '0';
  165. else if (e.type == 'radio' && !e.checked)
  166. continue;
  167. }
  168. data += e.name + '=' + val + "\r\n";
  169. }
  170. return data;
  171. }
  172. // POST form contents upload with response text
  173. function uploadform() {
  174. event.preventDefault();
  175. const form = event.target.form || event.target;
  176. return upload(form,textformdata(form));
  177. }
  178. // Flip the status of an INPUT element between text and password
  179. function showpwd(me = event.currentTarget) {
  180. const now_visible = me.classList.toggle('hide');
  181. const new_type = now_visible ? 'text' : 'password';
  182. me.classList.toggle('show',!now_visible);
  183. sib(me,'input').setAttribute('type', new_type);
  184. }
  185. // Insert translations as needed
  186. var translations = null;
  187. var delay_translate = false;
  188. function translate(top = document) {
  189. delay_translate = (!translations || document.readyState != 'complete');
  190. if (!delay_translate)
  191. fillin(translations, true, top);
  192. }
  193. document.addEventListener('load', (e) => translate(), PassiveListener);
  194. var lang_styleobj = document.createElement('style');
  195. document.head.append(lang_styleobj);
  196. function setlang(l = null) {
  197. l = l || document.documentElement.lang;
  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);