Merge branch 'main' of github.com:rvbglas/esp_clock
[esp-clock.git] / data / web / script.js
1 var pages
2 var parameters = {}
3 var daynames = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'] 
4 var msgT
5
6 function toggleMenu(e) {
7   active = (document.getElementById('menuLink').className.indexOf('active') !== -1)
8   if (active || e.target.id == 'menuLink' || e.target.id == 'menuBtn') {
9     elements = [ document.getElementById('layout'), document.getElementById('menu'), document.getElementById('menuLink') ]
10     for (const element of elements) {
11       if (!active) {
12         element.classList.add('active')
13       } else {
14         element.classList.remove('active')
15       }
16     }
17     if (e.target.id == 'menuLink' || e.target.id == 'menuBtn') {
18       e.preventDefault()
19     }
20     e.stopPropagation()
21   }
22 }
23
24 document.getElementById('layout').addEventListener('click', toggleMenu)
25
26 function encode(r){
27   r = String(r)
28   return r.replace(/[\x26\x0A\<>'"]/g,function(r){return"&#"+r.charCodeAt(0)+";"})
29 }
30
31 function getAnchor() {
32     return window.location.hash.slice(1);
33 }
34
35 function drawHeader(project) {
36   var menu_header = document.getElementById('_ui_menu_header')
37   menu_header.innerHTML = project.name
38   document.title = project.name + '/' + project.version
39 }
40
41 function parseJsonQ(url, callback) {
42   var req = new XMLHttpRequest();
43
44   req.onreadystatechange = function () {
45     if (this.readyState != 4) return; 
46     if (this.status != 200 && this.status != 500 && this.status != 404) {
47       setTimeout(parseJsonQ(url, callback),30000);
48       return;
49     }
50     var json = JSON.parse(this.responseText)
51     callback(json);
52   };
53
54   req.open("GET", url, true);
55   req.send()
56
57 }
58
59 function updateElement(id, value) {
60   var element = document.getElementById("_ui_element_"+id)
61   if (!element) return;
62   var ui_class = element.dataset.ui_class;
63   switch (ui_class) {
64     case "table":
65       element.innerText = value
66       break
67     case "input":
68     case "password":
69     case "select":
70     case "number":
71     case "range":
72       element.value = value  
73       break
74     case "checkbox":
75       element.checked = value
76       break
77     case "week":
78       for (i=0; i<7; i++) {
79         var subname = '_ui_elpart_'+i+'_'+id
80         var subelement = document.getElementById(subname) 
81         if (value[i]==1) { 
82           subelement.className = "weekday-selected"
83         } else {
84           subelement.className = "weekday"
85         }
86       }
87       break
88   }  
89 }
90
91 function updateValues(json) {
92   for (var key in json) {
93     var obj = document.getElementById("_ui_element_"+key)
94     if (obj) {
95       updateElement(key, json[key])
96     }
97     parameters[key] = json[key]
98   }
99   var notification = document.getElementById('_ui_notification');
100   if (parameters['_changed']) {
101     notification.innerHTML = '<input type="button" id="save" value="Сохранить" class="pure-button" onclick="sendAction(\'save\')">'
102     notification.removeAttribute('hidden')
103   } else {
104     notification.innerHTML = ''
105     notification.hidden = true
106   }
107 }
108
109 function sendUpdate(id) {
110   var input = document.getElementById('_ui_element_'+id)
111   var ui_class = input.dataset.ui_class;
112   switch (ui_class) {
113     case 'input':
114     case 'password':
115     case 'number':
116     case 'range':
117       if (input.checkValidity() && input.value != parameters[id]) {
118         parseJsonQ('/config/set?name=' + id + '&value=' + encodeURIComponent(input.value), function(json) {
119           updateValues(json)
120         })
121       }
122       break
123     case 'select':
124       parseJsonQ('/config/set?name=' + id + '&value=' + encodeURIComponent(input.selectedOptions[0].value), function(json) {
125         updateValues(json)
126       })
127       break;
128     case 'checkbox':
129       parseJsonQ('/config/set?name=' + id + '&value=' + (input.checked?'true':'false'), function(json) {
130         updateValues(json)
131       })      
132       break;
133     case 'week':        
134       parseJsonQ('/config/set?name=' + id + '&value=' + input.dataset.value, function(json) {
135         updateValues(json)
136       })      
137       break;
138   }
139 }
140
141 function sendAction(name, params = {}) {
142   var url = '/action?name=' + name
143   for (var param in params) {
144     url += '&'+param+'='+encodeURIComponent(params[param])
145   }
146   parseJsonQ(url, function(json) {
147     if (json.result == 'FAILED') {
148       alert(json.message)
149       if (json.page) {
150         drawPage(json.page)
151       }
152     } else {
153       location.reload()
154     }
155   })
156 }
157
158 function showPwd(id) {
159   var x = document.getElementById('_ui_element_' + id)
160   if (x.type === "password") {
161     x.type = "text";
162   } else {
163     x.type = "password";
164   }
165 }
166
167 function openSelect(id) {
168   var selector = document.getElementById('_ui_elemmodal_'+id);
169   selector.removeAttribute("hidden")
170 }
171
172 function closeSelect(id) {
173   var selector = document.getElementById('_ui_elemmodal_'+id);
174   selector.hidden = true
175 }
176
177 function closeMsg() {
178   document.getElementById("_ui_message").hidden = true;
179 }
180
181 function fadeMsg() {
182   var msg = document.getElementById('_ui_message');
183   msg.classList.add("fadeout")
184   msgT = setTimeout(()=> { closeMsg() }, 5000);
185 }
186
187 function openMsg(msgText) {
188   document.getElementById("_ui_message_text").innerText = msgText;
189   document.getElementById("_ui_message").classList.remove("fadeout"); 
190   document.getElementById("_ui_message").removeAttribute('hidden');
191   
192   if (msgT) { 
193     window.clearTimeout(msgT); 
194   }
195   msgT = setTimeout(()=> { fadeMsg(); }, 5000);
196 }
197
198 function selectWiFi(id, ssid) {
199   closeSelect(id);
200   var x = document.getElementById('_ui_element_' + id)
201   updateElement(x,ssid);
202   sendUpdate(id)
203 }
204
205 function getWiFi(id) {
206   var list = document.getElementById('_ui_elemselect_'+id)
207
208   var req = new XMLHttpRequest();
209
210   req.onreadystatechange = function () {
211     if (this.readyState != 4) return; 
212     if (this.status != 200 && this.status != 500 && this.status != 404) {
213       setTimeout(getWiFi(id),30000);
214       return;
215     }
216     var json = JSON.parse(this.responseText)
217     var table = '<table cellpadding="5" border="0" align="center"><thead class="table-header"><tr><td>SSID</td><td>BSSID</td><td>RSSI</td><td>Канал</td><td>Защита</td></tr></thead><tbody>'
218     if (!json.length) {
219       setTimeout(getWiFi(id),5000);
220     }
221     for (idx in json) {
222       var encryption = json[idx].secure == 2? "TKIP" : json[idx].secure == 5? "WEP" : json[idx].secure == 4? "CCMP" : json[idx].secure == 7? "нет" : json[idx].secure == 8? "Автоматически" : "Не определено";
223       table += '<tr onclick="selectWiFi(\''+id+'\',\''+json[idx].ssid+'\')"><td>'+json[idx].ssid+'</td><td>'+json[idx].bssid+'</td><td>'+json[idx].rssi+'</td><td>'+json[idx].channel+'</td><td>'+encryption+'</td></tr>'
224     }
225     
226     table += '</tbody></table>'
227     list.innerHTML = table;
228   };
229
230   req.open("GET", "/wifi/scan", true);
231   req.send()
232 }
233
234 function clickDay(id, day) {
235   value = parameters[id].split('')
236   day_value = value[day]
237   day_value = (day_value=='0')?'1':'0'
238   value[day] = day_value
239   element = document.getElementById('_ui_element_' + id)
240   value = value.join('')
241   element.dataset.value = value;
242   sendUpdate(id);
243 }
244
245 function sendTime(id) {
246   var value = document.getElementById('_ui_element_' + id).value
247   if (value) {
248     var date = new Date(value)
249     var timestamp = Math.floor(date.getTime()/1000);
250     sendAction('time',{"timestamp":timestamp})
251   }
252 }
253
254 function downloadFile(url, name) {
255   const a = document.createElement('a')
256   a.href = url
257   a.download = name?name:url.split('/').pop()
258   document.body.appendChild(a)
259   a.click()
260   document.body.removeChild(a)
261 }
262
263 function uploadConfig() {
264   var elem = document.getElementById('_config_file')
265   if (elem.value) {
266     var data = elem.files[0]
267     console.log(data)
268     fetch('/config/put', {method:'PUT',body:data});
269     elem.value = null
270   }
271 }
272
273 function elementHTML(element) {
274   var value
275   if (parameters[element.id] || !isNaN(parameters[element.id])) {
276     value = parameters[element.id]
277   } else if (element.value) {
278     value = element.value
279   } else {
280     value = ""
281   }
282   switch (element.type) {
283     case 'hr':
284       return '<div class="pure-u-1 pure-u-md-1-3"><hr></div>'
285     case 'button':
286       return '<div class="pure-u-1 pure-u-md-1-3"><div align="center"><input type="button" id="' 
287         + element.id + '" value="' + encode(element.label) + '" class="pure-button" onclick="sendAction(\'' + element.id + '\')" /></div></div>'
288     case 'password':
289       return '<div class="pure-u-1 pure-u-md-1-3"><label for="_ui_element_' + element.id + '">' + encode(element.label) + '</label>'
290         + '<div class="hinted"><input data-ui_class="password" type="password" id="_ui_element_' + element.id + '" value="' + encode(value) 
291         + '" class="pure-u-1" maxlength="99" oninput="sendUpdate(\'' + element.id + '\')" />'
292         + '<z class="hint" onclick="showPwd(\'' + element.id + '\')">&#61550;</z></div></div>'
293     case 'input':
294       var pattern = ""
295       if (element.pattern) {
296         pattern = ' pattern="' + encode(element.pattern) + '"'
297       }
298       return '<div class="pure-u-1 pure-u-md-1-3"><label for="_ui_element_' + element.id + '">' + encode(element.label) + '</label>'
299         + '<input data-ui_class="input" type="text" id="_ui_element_' + element.id + '" value="' + encode(value) +'"'+ pattern
300         + ' class="pure-u-1" maxlength="99" oninput="sendUpdate(\'' + element.id + '\')" /></div>'
301     case 'input-wifi':
302       var pattern = ""
303       if (element.pattern) {
304         pattern = ' pattern="' + encode(element.pattern) + '"'
305       }
306       return '<div class="pure-u-1 pure-u-md-1-3"><label for="_ui_element_' + element.id + '">' + encode(element.label) + '</label>'
307         + '<div class="hinted"><input data-ui_class="input" type="text" id="_ui_element_' + element.id + '" value="' + encode(value) +'"'+ pattern
308         + ' class="pure-u-1" maxlength="99" oninput="sendUpdate(\'' + element.id + '\')" />' 
309         + '<z class="hint" onclick="openSelect(\'' + element.id+ '\'); getWiFi(\'' + element.id + '\')">&#61931;</z></div>'
310         + '<div class="modal" id="_ui_elemmodal_' + element.id + '" hidden>' 
311         + '<div class="modal-content">'
312         + '<div id="_ui_elemselect_' + element.id + '"></div>'
313         + '<div class="pure-u-1 pure-u-md-1-3"><div align="center"><input type="button" id="_ui_button_' 
314         + element.id + '" value="Закрыть" class="pure-button" onclick="closeSelect(\'' + element.id + '\')"></div>'
315         + '</div></div></div></div>'
316     case 'checkbox':
317       return '<div class="pure-u-1 pure-u-md-1-3"><label class="switch socket" for="_ui_element_' + element.id + '">'
318         + '<input class="switch" data-ui_class="checkbox" type="checkbox" id="_ui_element_' + element.id + '"' + (parameters[element.id]?' checked':'') + ' onchange="sendUpdate(\'' + element.id + '\')" />'
319         + '<span class="switch slider">'+ encode(element.label) + '</span>'
320         + '</label></div>'
321     case 'select':
322       var options = '<div class="pure-u-1 pure-u-md-1-3"><label for="_ui_element_' + element.id + '">' + encode(element.label) + '</label>'
323         + '<select class="pure-u-24-24" data-ui_class="select" id="_ui_element_' + element.id + '" onchange="sendUpdate(\'' + element.id + '\')">'
324       for (const option of element.options) {
325         var list_option = '<option value="' + encode(option.value) + '" ';
326         if (option.value == parameters[element.id]) {
327           list_option += 'selected '
328         }
329         list_option += '>' + encode(option.text) + '</option>'
330         options += list_option
331       }
332       options += '</select></div>'
333       return options
334     case 'week':
335       days = '<div class="pure-u-1 pure-u-md-1-3"><label for="_ui_element_' + element.id + '">' + encode(element.label) + '</label>' 
336         + '<table data-ui_class="week" data-value="' + value + '" id="_ui_element_' + element.id + '" cellpadding="5" border="0" class="week"><tbody><tr>'
337       for (i=0; i<7; i++) {
338         a_enabled = (value[i] == "1")
339         days = days + '<td><div class="weekday' + (a_enabled?"-selected":"") + '" id="_ui_elpart_'+i+'_'+element.id+'" onclick="clickDay(\'' + element.id + '\', ' + i + ')">'+ daynames[i] + '</div></td>'
340       }
341       days += '</tr></tbody></table></div>'
342       return days
343     case 'timeset':
344       var now = new Date()
345       now.setMinutes(now.getMinutes() - now.getTimezoneOffset())
346       value = now.toISOString().slice(0, -1);
347       return '<div class="pure-u-1 pure-u-md-1-3"><label for="_ui_element_' + element.id + '">' + encode(element.label) + '</label>'
348         +'<input id="_ui_element_'+element.id+'" data-ui_class="timeset" class="inline-input" type="datetime-local" value="'+value+'">'
349         + '<div class="send-button" onclick="sendTime(\''+element.id+'\')">-></div></div>'
350     case 'text':
351       return '<div class="pure-u-1 pure-u-md-1-3"><h2 id="_ui_element_'+ element.id +'" ' + (element.color?'style="color:'+ element.color+'" ':'')+ '>' + encode(value) + '</h2></div>'
352     case 'number':
353       return '<div class="pure-u-1 pure-u-md-1-3"><label for="_ui_element_' + element.id + '">' + encode(element.label) + '</label>'
354         + '<input data-ui_class="number" type="number" '+ (!isNaN(element.min)?'min="'+element.min+'" ':'') + (!isNaN(element.max)?'max="'+element.max+'" ':'') + (!isNaN(element.step)?'step="'+element.step+'" ':'')
355         + 'id="_ui_element_' + element.id + '" value="' + encode(value) +'"'+ pattern
356         + ' class="pure-u-1" maxlength="99" oninput="sendUpdate(\'' + element.id + '\')" /></div>'
357     case 'range':
358       return '<div class="pure-u-1 pure-u-md-1-3"><label for="_ui_element_' + element.id + '">' + encode(element.label) + '</label>'
359         + '<input data-ui_class="range" type="range" '+ (!isNaN(element.min)?'min="'+element.min+'" ':'') + (!isNaN(element.max)?'max="'+element.max+'" ':'') + (!isNaN(element.step)?'step="'+element.step+'" ':'')
360         + 'id="_ui_element_' + element.id + '" value="' + encode(value) +'"'+ pattern
361         + ' class="pure-u-1" maxlength="99" oninput="sendUpdate(\'' + element.id + '\')" /></div>'
362     case 'config':
363       return '<div class="pure-u-1 pure-u-md-1-3"><label for="_ui_element_' + element.id + '">' + encode(element.label) + '</label>'
364         + '<div align="center"><input type="button" class="pure-button row-button" value="Сохранить..." onclick="downloadFile(\'/config/get\',\'config.json\')">'
365         + '<label for="_config_file" class="pure-button row-button">Восстановить...</label>'
366         + '<input type="button" class="pure-button row-button" value="Настройки по умолчанию" onclick="confirm(\'Вы точно хотите сбросить настройки?\')?sendAction(\'reset\'):console.log(\'Не надо так не надо...\')">'
367         + '<input type="file" id="_config_file" onchange="uploadConfig()" style="visibility:hidden">'
368         + '</div></div>'
369     case 'table':
370     default:
371       return '<div class="pure-u-1 pure-u-md-1-3"><table class="texttable" cellpadding="5" border="0" align="center"><tbody><tr><td class="value-name" align="right">' 
372         + encode(element.label)+ '</td><td id="_ui_element_' 
373         + element.id + '" data-ui_class="table" ' + (element.color?'style="color:'+ element.color+'" ':'')+ '>' + encode(value) + '</td></tr></tbody></table></div>'
374   }
375 }
376
377 function drawPage(id) {
378   var idx =0, i=0
379   for (const page of pages) {
380     var menu_link = document.getElementById('_ui_pglink_' + page.id)  
381     if (page.id != id) {
382       menu_link.classList.remove("pure-menu-selected")
383     } else {
384       menu_link.classList.add("pure-menu-selected")
385       idx = i
386     }
387     i++
388   }
389   var page_header = document.getElementById("_ui_page_header")
390   page_header.innerHTML = pages[idx].title
391   var page_content = document.getElementById("_ui_page_content");
392   var content = ''
393   for (const element of pages[idx].elements) {
394     content = content + elementHTML(element) + '\n'
395   }
396   page_content.innerHTML = content
397   window.location.hash = id
398 }
399
400 function drawNavigator(project, pages) {
401   var menu = document.getElementById('_ui_menu_list');
402   var list = ''
403   for (const page of pages) {
404     list = list + '<li id="_ui_pglink_' + page.id 
405       + '" class="pure-menu-item"><a class="pure-menu-link" onclick="drawPage(\''+ page.id +'\')" href="#' + page.id + '">'
406       + (page.icon?'<span class="icon">'+page.icon+'</span>':'')
407       + page.title+'</a></li>'
408   }
409   menu.innerHTML = list
410 }
411
412 function drawContacts(contacts) {
413   if (!contacts) return;
414   var contact_list = '<hr><h4 class="pure-u1">Контакты</h4>'
415   for (contact of contacts) {
416     const url = new URL(contact)
417     var ref
418     switch (url.protocol) {
419       case 'http':
420       case 'https:':
421         ref = '<span class="icon">&#61461;</span>'+url.hostname
422         break
423       case 'mailto:':
424         ref = '<span class="icon">&#61664;</span>'+url.pathname
425         break
426       case 'tg:':
427         ref = '<span class="icon">&#62150;</span>'+url.pathname
428         contact = 'tg://resolve?domain='+url.pathname
429         break
430       default:
431         ref = '<span class="icon">&#62074;</span>'+url.pathname
432     }
433     contact_list += '<a href="'+contact+'">'+ref+'</a>'
434   }
435   var footer = document.getElementById('_ui_contacts');
436   footer.innerHTML = contact_list
437 }
438
439 function drawUI(ui) {
440   drawHeader(ui.project)
441   pages = ui.pages
442   drawNavigator(ui.project, pages)
443   drawContacts(ui.project.contacts)
444   var anchor = getAnchor()
445   if (anchor) {
446     drawPage(anchor)
447   } else {
448     drawPage(pages[0].id)
449   }
450 }
451
452 function GetUI() {
453   parseJsonQ("/ui", drawUI);
454 }
455
456 GetUI()
457
458 function initES() {
459   if (!!window.EventSource) {
460     var source = new EventSource('/events');
461     openMsg('Соединение установлено')
462
463     source.onerror = function(e) {
464       if (source.readyState == 2) {
465         openMsg('Соединение прервано')
466         setTimeout(initES, 5000);
467       }
468     };
469         
470     source.addEventListener('update', function(e) {
471       updateValues(JSON.parse(e.data));
472     }, false);
473     source.addEventListener('message', function(e) {
474       openMsg(e.data);
475     }, false);
476   }
477 }
478
479 initES();
480
481 function drawConfig(cfg) {
482   updateValues(cfg)
483 }
484
485 function GetCfg() {
486   parseJsonQ("/config/get", drawConfig);
487 }
488
489 GetCfg()