Переработан веб-интерфейс. Сделаны всплывающие уведомления.
[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 elementHTML(element) {
255   var value
256   if (parameters[element.id] || !isNaN(parameters[element.id])) {
257     value = parameters[element.id]
258   } else if (element.value) {
259     value = element.value
260   } else {
261     value = ""
262   }
263   switch (element.type) {
264     case 'hr':
265       return '<div class="pure-u-1 pure-u-md-1-3"><hr></div>'
266     case 'button':
267       return '<div class="pure-u-1 pure-u-md-1-3"><div align="center"><input type="button" id="' 
268         + element.id + '" value="' + encode(element.label) + '" class="pure-button" onclick="sendAction(\'' + element.id + '\')" /></div></div>'
269     case 'password':
270       return '<div class="pure-u-1 pure-u-md-1-3"><label for="_ui_element_' + element.id + '">' + encode(element.label) + '</label>'
271         + '<div class="hinted"><input data-ui_class="password" type="password" id="_ui_element_' + element.id + '" value="' + encode(value) 
272         + '" class="pure-u-1" maxlength="99" oninput="sendUpdate(\'' + element.id + '\')" />'
273         + '<z class="hint" onclick="showPwd(\'' + element.id + '\')">&#61550;</z></div></div>'
274     case 'input':
275       var pattern = ""
276       if (element.pattern) {
277         pattern = ' pattern="' + encode(element.pattern) + '"'
278       }
279       return '<div class="pure-u-1 pure-u-md-1-3"><label for="_ui_element_' + element.id + '">' + encode(element.label) + '</label>'
280         + '<input data-ui_class="input" type="text" id="_ui_element_' + element.id + '" value="' + encode(value) +'"'+ pattern
281         + ' class="pure-u-1" maxlength="99" oninput="sendUpdate(\'' + element.id + '\')" /></div>'
282     case 'input-wifi':
283       var pattern = ""
284       if (element.pattern) {
285         pattern = ' pattern="' + encode(element.pattern) + '"'
286       }
287       return '<div class="pure-u-1 pure-u-md-1-3"><label for="_ui_element_' + element.id + '">' + encode(element.label) + '</label>'
288         + '<div class="hinted"><input data-ui_class="input" type="text" id="_ui_element_' + element.id + '" value="' + encode(value) +'"'+ pattern
289         + ' class="pure-u-1" maxlength="99" oninput="sendUpdate(\'' + element.id + '\')" />' 
290         + '<z class="hint" onclick="openSelect(\'' + element.id+ '\'); getWiFi(\'' + element.id + '\')">&#61931;</z></div>'
291         + '<div class="modal" id="_ui_elemmodal_' + element.id + '" hidden>' 
292         + '<div class="modal-content">'
293         + '<div id="_ui_elemselect_' + element.id + '"></div>'
294         + '<div class="pure-u-1 pure-u-md-1-3"><div align="center"><input type="button" id="_ui_button_' 
295         + element.id + '" value="Закрыть" class="pure-button" onclick="closeSelect(\'' + element.id + '\')"></div>'
296         + '</div></div></div></div>'
297     case 'checkbox':
298       return '<div class="pure-u-1 pure-u-md-1-3"><label class="switch socket" for="_ui_element_' + element.id + '">'
299         + '<input class="switch" data-ui_class="checkbox" type="checkbox" id="_ui_element_' + element.id + '"' + (parameters[element.id]?' checked':'') + ' onchange="sendUpdate(\'' + element.id + '\')" />'
300         + '<span class="switch slider">'+ encode(element.label) + '</span>'
301         + '</label></div>'
302     case 'select':
303       var options = '<div class="pure-u-1 pure-u-md-1-3"><label for="_ui_element_' + element.id + '">' + encode(element.label) + '</label>'
304         + '<select class="pure-u-24-24" data-ui_class="select" id="_ui_element_' + element.id + '" onchange="sendUpdate(\'' + element.id + '\')">'
305       for (const option of element.options) {
306         var list_option = '<option value="' + encode(option.value) + '" ';
307         if (option.value == parameters[element.id]) {
308           list_option += 'selected '
309         }
310         list_option += '>' + encode(option.text) + '</option>'
311         options += list_option
312       }
313       options += '</select></div>'
314       return options
315     case 'week':
316       days = '<div class="pure-u-1 pure-u-md-1-3"><label for="_ui_element_' + element.id + '">' + encode(element.label) + '</label>' 
317         + '<table data-ui_class="week" data-value="' + value + '" id="_ui_element_' + element.id + '" cellpadding="5" border="0" class="week"><tbody><tr>'
318       for (i=0; i<7; i++) {
319         a_enabled = (value[i] == "1")
320         days = days + '<td><div class="weekday' + (a_enabled?"-selected":"") + '" id="_ui_elpart_'+i+'_'+element.id+'" onclick="clickDay(\'' + element.id + '\', ' + i + ')">'+ daynames[i] + '</div></td>'
321       }
322       days += '</tr></tbody></table></div>'
323       return days
324     case 'timeset':
325       var now = new Date()
326       now.setMinutes(now.getMinutes() - now.getTimezoneOffset())
327       value = now.toISOString().slice(0, -1);
328       return '<div class="pure-u-1 pure-u-md-1-3"><label for="_ui_element_' + element.id + '">' + encode(element.label) + '</label>'
329         +'<div class="timesetter"><input id="_ui_element_'+element.id+'" data-ui_class="timeset" class="inline-input" type="datetime-local" value="'+value+'">'
330         + '<div class="send-button" onclick="sendTime(\''+element.id+'\')">-></div></div></div>'
331     case 'text':
332       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>'
333     case 'number':
334       return '<div class="pure-u-1 pure-u-md-1-3"><label for="_ui_element_' + element.id + '">' + encode(element.label) + '</label>'
335         + '<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+'" ':'')
336         + 'id="_ui_element_' + element.id + '" value="' + encode(value) +'"'+ pattern
337         + ' class="pure-u-1" maxlength="99" oninput="sendUpdate(\'' + element.id + '\')" /></div>'
338     case 'range':
339       return '<div class="pure-u-1 pure-u-md-1-3"><label for="_ui_element_' + element.id + '">' + encode(element.label) + '</label>'
340         + '<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+'" ':'')
341         + 'id="_ui_element_' + element.id + '" value="' + encode(value) +'"'+ pattern
342         + ' class="pure-u-1" maxlength="99" oninput="sendUpdate(\'' + element.id + '\')" /></div>'
343     case 'table':
344     default:
345       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">' 
346         + encode(element.label)+ '</td><td id="_ui_element_' 
347         + element.id + '" data-ui_class="table" ' + (element.color?'style="color:'+ element.color+'" ':'')+ '>' + encode(value) + '</td></tr></tbody></table></div>'
348   }
349 }
350
351 function drawPage(id) {
352   var idx =0, i=0
353   for (const page of pages) {
354     var menu_link = document.getElementById('_ui_pglink_' + page.id)  
355     if (page.id != id) {
356       menu_link.classList.remove("pure-menu-selected")
357     } else {
358       menu_link.classList.add("pure-menu-selected")
359       idx = i
360     }
361     i++
362   }
363   var page_header = document.getElementById("_ui_page_header")
364   page_header.innerHTML = pages[idx].title
365   var page_content = document.getElementById("_ui_page_content");
366   var content = ''
367   for (const element of pages[idx].elements) {
368     content = content + elementHTML(element) + '\n'
369   }
370   page_content.innerHTML = content
371   window.location.hash = id
372 }
373
374 function drawNavigator(project, pages) {
375   var menu = document.getElementById('_ui_menu_list');
376   var list = ''
377   for (const page of pages) {
378     list = list + '<li id="_ui_pglink_' + page.id 
379       + '" class="pure-menu-item"><a class="pure-menu-link" onclick="drawPage(\''+ page.id +'\')" href="#' + page.id + '">'
380       + (page.icon?'<span class="icon">'+page.icon+'</span>':'')
381       + page.title+'</a></li>'
382   }
383   menu.innerHTML = list
384 }
385
386 function drawContacts(contacts) {
387   if (!contacts) return;
388   var contact_list = '<hr><h4 class="pure-u1">Контакты</h4>'
389   for (contact of contacts) {
390     const url = new URL(contact)
391     var ref
392     switch (url.protocol) {
393       case 'http':
394       case 'https:':
395         ref = '<span class="icon">&#61461;</span>'+url.hostname
396         break
397       case 'mailto:':
398         ref = '<span class="icon">&#61664;</span>'+url.pathname
399         break
400       case 'tg:':
401         ref = '<span class="icon">&#62150;</span>'+url.pathname
402         contact = 'tg://resolve?domain='+url.pathname
403         break
404       default:
405         ref = '<span class="icon">&#62074;</span>'+url.pathname
406     }
407     contact_list += '<a href="'+contact+'">'+ref+'</a>'
408   }
409   var footer = document.getElementById('_ui_contacts');
410   footer.innerHTML = contact_list
411 }
412
413 function drawUI(ui) {
414   drawHeader(ui.project)
415   pages = ui.pages
416   drawNavigator(ui.project, pages)
417   drawContacts(ui.project.contacts)
418   var anchor = getAnchor()
419   if (anchor) {
420     drawPage(anchor)
421   } else {
422     drawPage(pages[0].id)
423   }
424 }
425
426 function GetUI() {
427   parseJsonQ("/ui", drawUI);
428 }
429
430 GetUI()
431
432 function initES() {
433   if (!!window.EventSource) {
434     var source = new EventSource('/events');
435
436     source.onerror = function(e) {
437       if (source.readyState == 2) {
438         setTimeout(initES, 5000);
439       }
440     };
441         
442     source.addEventListener('update', function(e) {
443       updateValues(JSON.parse(e.data));
444     }, false);
445     source.addEventListener('message', function(e) {
446       openMsg(e.data);
447     }, false);
448   }
449 }
450
451 initES();
452
453 function drawConfig(cfg) {
454   updateValues(cfg)
455 }
456
457 function GetCfg() {
458   parseJsonQ("/config/get", drawConfig);
459 }
460
461 GetCfg()