max80.js 6.6 KB

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