max80.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  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) {
  48. var button = null;
  49. var clearers = new Set;
  50. form = getelem(form);
  51. for (var e of form.elements) {
  52. if (e.classList.contains('noinit')) {
  53. continue;
  54. } else if (e instanceof HTMLInputElement ||
  55. e instanceof HTMLSelectElement) {
  56. const val = map.get(e.name);
  57. if (val == null)
  58. continue;
  59. if (e.type == 'checkbox') {
  60. e.checked = cfgbool(val) == cfgbool(e.value);
  61. } else if (e.type == 'radio') {
  62. e.checked = (val == e.name);
  63. } else {
  64. e.value = val;
  65. }
  66. } else if (e instanceof HTMLButtonElement &&
  67. e.type == 'submit') {
  68. button = e;
  69. }
  70. }
  71. if (button) {
  72. button.disabled = false; // All loaded, enable the submit button
  73. }
  74. }
  75. // Load form initialization data
  76. function loadform(form, url) {
  77. fetchconfig(url)
  78. .then(map => { initform(form, map) })
  79. .catch(() => {});
  80. }
  81. // Replace the contents of selected HTML elements based on a map with
  82. // selectors.
  83. function fillin(map,html = false,top = document)
  84. {
  85. for (const [key,val] of map) {
  86. try {
  87. const m = key.match(/(.*?)(?:\;([\w.-]*)(\??))?$/);
  88. for (var e of top.querySelectorAll(m[1])) {
  89. try {
  90. if (!html) e.textContents = val;
  91. else if (!m[2]) e.innerHTML = val;
  92. else if (!m[3]) e.setAttribute(m[2],val);
  93. else if (!cfgbool(val)) e.removeAttribute(m[2]);
  94. else e.setAttribute(m[2],'');
  95. } catch(e) { };
  96. }
  97. } catch(e) { };
  98. }
  99. }
  100. // Load status or HTML data
  101. function load(url,html = false)
  102. {
  103. fetchconfig(url)
  104. .then(map => fillin(map,html))
  105. .catch(() => {});
  106. }
  107. // POST upload of data from within a form, with progress and response text
  108. function upload(form,data) {
  109. var xhr = new XMLHttpRequest();
  110. var progress = chi(form,'progress');
  111. if (progress) {
  112. progress.value = 0;
  113. xhr.upload.addEventListener('progress', (e) => {
  114. if (!e.lengthComputable)
  115. return;
  116. progress.max = e.total * 1.05;
  117. progress.value = e.loaded;
  118. }, PassiveListener);
  119. }
  120. classmod(form, [['started',1],['done',0],['ok',0],['err',0],['running',1]]);
  121. xhr.addEventListener('loadend', (e) => {
  122. const ok = xhr.status >= 200 && xhr.status < 400;
  123. if (progress && ok)
  124. progress.value = progress.max;
  125. var result = chi(form,'output');
  126. if (result) {
  127. var msg = xhr.responseText.trimEnd();
  128. if (!msg)
  129. msg = xhr.status + ' ' + xhr.statusText;
  130. result.textContent = msg;
  131. }
  132. classmod(form, [['ok',ok],['err',!ok],['running',0],['done',1]]);
  133. // Automatically reload the page after successful upload
  134. const rf = parseInt(xhr.getResponseHeader('Refresh'));
  135. //if (rf && ok)
  136. // setTimeout(() => window.location.reload(), rf * 1000);
  137. }, PassiveListener);
  138. xhr.open(form.method, form.action);
  139. xhr.responseType = 'text';
  140. xhr.send(data);
  141. return xhr;
  142. }
  143. // Upload a data file blob
  144. function uploadfile() {
  145. event.preventDefault();
  146. const form = event.target.form || event.target;
  147. var files = form.elements['file'];
  148. return (files.files.length == 1)
  149. ? upload(form,files.files[0]) : files.click();
  150. }
  151. // key=value formatting of form data; including inverted checkboxes
  152. function textformdata(form) {
  153. var data = '';
  154. for (var e of form.elements) {
  155. var val = e.value;
  156. if (val == undefined || !e.name || e instanceof HTMLButtonElement) {
  157. continue;
  158. } else if (e instanceof HTMLInputElement) {
  159. if (e.type == 'checkbox')
  160. val = e.checked == cfgbool(val) ? '1' : '0';
  161. else if (e.type == 'radio' && !e.checked)
  162. continue;
  163. }
  164. data += e.name + '=' + val + "\r\n";
  165. }
  166. return data;
  167. }
  168. // POST form contents upload with response text
  169. function uploadform() {
  170. event.preventDefault();
  171. const form = event.target.form || event.target;
  172. return upload(form,textformdata(form));
  173. }
  174. // Flip the status of an INPUT element between text and password
  175. function showpwd(me = event.target) {
  176. const now_visible = me.classList.toggle('hide');
  177. const new_type = now_visible ? 'text' : 'password';
  178. me.classList.toggle('show',!now_visible);
  179. sib(me,'input').setAttribute('type', new_type);
  180. }
  181. // Insert translations as needed
  182. var translations = null;
  183. var delay_translate = false;
  184. function translate(top = document) {
  185. delay_translate = (!translations || document.readyState != 'complete');
  186. if (!delay_translate)
  187. fillin(translations, true, top);
  188. }
  189. document.addEventListener('load', (e) => translate(), PassiveListener);
  190. fetchconfig('/sys/lang')
  191. .then((map) => {
  192. translations = map;
  193. if (delay_translate)
  194. translate();
  195. })
  196. .catch(() => {});
  197. // Hack to include an HTML files. Sadly, does not support
  198. // including files with <script> tags.
  199. function inc(url) {
  200. var me = document.currentScript;
  201. fetch(url, {redirect: "follow"})
  202. .then((response) => response.text())
  203. .then((text) => {
  204. const p = me.parentElement;
  205. me.outerHTML = text;
  206. translate(p);
  207. });
  208. }