use document.querySelector
[web/firmware-selector-openwrt-org.git] / www / index.js
1
2 var current_model = {};
3
4 function $(query) {
5 if (typeof query === 'string') {
6 return document.querySelector(query);
7 } else {
8 return query;
9 }
10 }
11
12 function show(query) {
13 $(query).style.display = 'block';
14 }
15
16 function hide(query) {
17 $(query).style.display = 'none';
18 }
19
20 function split(str) {
21 return str.match(/[^\s,]+/g) || [];
22 }
23
24 function get_model_titles(titles) {
25 return titles.map(e => {
26 if (e.title) {
27 return e.title;
28 } else {
29 return ((e.vendor || '') + ' ' + (e.model || '') + ' ' + (e.variant || '')).trim();
30 }
31 }).join(' / ');
32 }
33
34 function build_asu_request() {
35 if (!current_model || !current_model.id) {
36 alert('bad profile');
37 return;
38 }
39
40 function showStatus(message, url) {
41 show('buildstatus');
42 var tr = message.startsWith('tr-') ? message : '';
43 if (url) {
44 $('#buildstatus').innerHTML = '<a href="' + url + '" class="' + tr + '">' + message + '</a>';
45 } else {
46 $('#buildstatus').innerHTML = '<span class="' + tr + '"></span>';
47 }
48 translate();
49 }
50
51 // hide image view
52 updateImages();
53
54 show('buildspinner');
55 showStatus('tr-request-image');
56
57 var request_data = {
58 'target': current_model.target,
59 'profile': current_model.id,
60 'packages': split($('#packages').value),
61 'version': $('#versions').value
62 }
63
64 fetch(config.asu_url + '/api/build', {
65 method: 'POST',
66 headers: { 'Content-Type': 'application/json' },
67 body: JSON.stringify(request_data)
68 })
69 .then(response => {
70 switch (response.status) {
71 case 200:
72 hide('buildspinner');
73 showStatus('tr-build-successful');
74
75 response.json()
76 .then(mobj => {
77 var download_url = config.asu_url + '/store/' + mobj.bin_dir;
78 showStatus('tr-build-successful', download_url + '/buildlog.txt');
79 updateImages(
80 mobj.version_number,
81 mobj.version_code,
82 mobj.build_at,
83 get_model_titles(mobj.titles),
84 download_url, mobj, true
85 );
86 });
87 break;
88 case 202:
89 showStatus('tr-check-again');
90 setTimeout(_ => { build_asu_request() }, 5000);
91 break;
92 case 400: // bad request
93 case 422: // bad package
94 case 500: // build failed
95 hide('buildspinner');
96 response.json()
97 .then(mobj => {
98 var message = mobj['message'] || 'tr-build-failed';
99 var url = mobj.buildlog ? (config.asu_url + '/store/' + mobj.bin_dir + '/buildlog.txt') : undefined;
100 showStatus(message, url);
101 })
102 break;
103 }
104 })
105 .catch(err => {
106 hide('buildspinner');
107 showStatus(err);
108 })
109 }
110
111 function setupSelectList(select, items, onselection) {
112 for (var i = 0; i < items.length; i += 1) {
113 var option = document.createElement('OPTION');
114 option.innerHTML = items[i];
115 select.appendChild(option);
116 }
117
118 select.addEventListener('change', e => {
119 onselection(items[select.selectedIndex]);
120 });
121
122 if (select.selectedIndex >= 0) {
123 onselection(items[select.selectedIndex]);
124 }
125 }
126
127 // Change the translation of the entire document
128 function translate() {
129 var mapping = translations[config.language];
130 for (var tr in mapping) {
131 Array.from(document.getElementsByClassName(tr))
132 .forEach(e => { e.innerText = mapping[tr]; })
133 }
134 }
135
136 function setupAutocompleteList(input, items, as_list, onbegin, onend) {
137 var currentFocus = -1;
138
139 // sort numbers and other characters separately
140 var collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
141
142 items.sort(collator.compare);
143
144 input.oninput = function(e) {
145 onbegin();
146
147 var offset = 0;
148 var value = this.value;
149 var value_list = [];
150
151 if (as_list) {
152 // automcomplete last text item
153 offset = this.value.lastIndexOf(' ') + 1;
154 value = this.value.substr(offset);
155 value_list = split(this.value.substr(0, offset));
156 }
157
158 // close any already open lists of autocompleted values
159 closeAllLists();
160
161 if (!value) {
162 return false;
163 }
164
165 // create a DIV element that will contain the items (values):
166 var list = document.createElement('DIV');
167 list.setAttribute('id', this.id + '-autocomplete-list');
168 list.setAttribute('class', 'autocomplete-items');
169 // append the DIV element as a child of the autocomplete container:
170 this.parentNode.appendChild(list);
171
172 function normalize(s) {
173 return s.toUpperCase().replace(/[-_.]/g, ' ');
174 }
175
176 var match = normalize(value);
177 var c = 0;
178 for (var i = 0; i < items.length; i += 1) {
179 var item = items[i];
180
181 // match
182 var j = normalize(item).indexOf(match);
183 if (j < 0) {
184 continue;
185 }
186
187 // do not offer a duplicate item
188 if (as_list && value_list.indexOf(item) != -1) {
189 continue;
190 }
191
192 c += 1;
193 if (c >= 15) {
194 var div = document.createElement('DIV');
195 div.innerHTML = '...';
196 list.appendChild(div);
197 break;
198 } else {
199 var div = document.createElement('DIV');
200 // make the matching letters bold:
201 div.innerHTML = item.substr(0, j)
202 + '<strong>' + item.substr(j, value.length) + '</strong>'
203 + item.substr(j + value.length)
204 + '<input type="hidden" value="' + item + '">';
205
206 div.addEventListener('click', function(e) {
207 // include selected value
208 var selected = this.getElementsByTagName('input')[0].value;
209 if (as_list) {
210 input.value = value_list.join(' ') + ' ' + selected;
211 } else {
212 input.value = selected;
213 }
214 // close the list of autocompleted values,
215 closeAllLists();
216 onend(input);
217 });
218
219 list.appendChild(div);
220 }
221 }
222 };
223
224 input.onkeydown = function(e) {
225 var x = document.getElementById(this.id + '-autocomplete-list');
226 if (x) x = x.getElementsByTagName('div');
227 if (e.keyCode == 40) {
228 // key down
229 currentFocus += 1;
230 // and and make the current item more visible:
231 setActive(x);
232 } else if (e.keyCode == 38) {
233 // key up
234 currentFocus -= 1;
235 // and and make the current item more visible:
236 setActive(x);
237 } else if (e.keyCode == 13) {
238 // If the ENTER key is pressed, prevent the form from being submitted,
239 e.preventDefault();
240 if (currentFocus > -1) {
241 // and simulate a click on the 'active' item:
242 if (x) x[currentFocus].click();
243 }
244 }
245 };
246
247 input.onfocus = function() {
248 onend(input);
249 }
250
251 // focus lost
252 input.onblur = function() {
253 onend(input);
254 }
255
256 function setActive(x) {
257 // a function to classify an item as 'active':
258 if (!x) return false;
259 // start by removing the 'active' class on all items:
260 for (var i = 0; i < x.length; i++) {
261 x[i].classList.remove('autocomplete-active');
262 }
263 if (currentFocus >= x.length) currentFocus = 0;
264 if (currentFocus < 0) currentFocus = (x.length - 1);
265 // add class 'autocomplete-active':
266 x[currentFocus].classList.add('autocomplete-active');
267 }
268
269 function closeAllLists(elmnt) {
270 // close all autocomplete lists in the document,
271 // except the one passed as an argument:
272 var x = document.getElementsByClassName('autocomplete-items');
273 for (var i = 0; i < x.length; i++) {
274 if (elmnt != x[i] && elmnt != input) {
275 x[i].parentNode.removeChild(x[i]);
276 }
277 }
278 }
279
280 // execute a function when someone clicks in the document:
281 document.addEventListener('click', e => {
282 closeAllLists(e.target);
283 });
284 }
285
286 // for attended sysupgrade
287 function updatePackageList(version, target) {
288 // set available packages
289 fetch(config.asu_url + '/' + config.versions[version] + '/' + target + '/index.json')
290 .then(response => response.json())
291 .then(all_packages => {
292 setupAutocompleteList($('#packages'), all_packages, true, _ => {}, textarea => {
293 textarea.value = split(textarea.value)
294 // make list unique, ignore minus
295 .filter((value, index, self) => {
296 var i = self.indexOf(value.replace(/^\-/, ''));
297 return (i === index) || (i < 0);
298 })
299 // limit to available packages, ignore minus
300 .filter((value, index) => all_packages.indexOf(value.replace(/^\-/, '')) !== -1)
301 .join(' ');
302 });
303 });
304 }
305
306 function updateImages(version, code, date, model, url, mobj, is_custom) {
307 // add download button for image
308 function addLink(type, file) {
309 var a = document.createElement('A');
310 a.classList.add('download-link');
311 a.href = url
312 .replace('{target}', mobj.target)
313 .replace('{version}', version)
314 + '/' + file;
315 var span = document.createElement('SPAN');
316 span.appendChild(document.createTextNode(''));
317 a.appendChild(span);
318 a.appendChild(document.createTextNode(type.toUpperCase()));
319
320 if (config.showHelp) {
321 a.onmouseover = function() {
322 // hide all help texts
323 Array.from(document.getElementsByClassName('download-help'))
324 .forEach(e => e.style.display = 'none');
325 var lc = type.toLowerCase();
326 if (lc.includes('sysupgrade')) {
327 show('sysupgrade-help');
328 } else if (lc.includes('factory') || lc == 'trx' || lc == 'chk') {
329 show('factory-help');
330 } else if (lc.includes('kernel') || lc.includes('zimage') || lc.includes('uimage')) {
331 show('kernel-help');
332 } else if (lc.includes('root')) {
333 show('rootfs-help');
334 } else if (lc.includes('sdcard')) {
335 show('sdcard-help');
336 } else if (lc.includes('tftp')) {
337 show('tftp-help');
338 } else {
339 show('other-help');
340 }
341 };
342 }
343
344 $('#download-links').appendChild(a);
345 }
346
347 function switchClass(query, from_class, to_class) {
348 $(query).classList.remove(from_class);
349 $(query).classList.add(to_class);
350 }
351
352 // remove all download links
353 Array.from(document.getElementsByClassName('download-link'))
354 .forEach(e => e.remove());
355
356 // hide all help texts
357 Array.from(document.getElementsByClassName('download-help'))
358 .forEach(e => e.style.display = 'none');
359
360 if (model && url && mobj) {
361 var target = mobj.target;
362 var images = mobj.images;
363
364 // change between "version" and "custom" title
365 if (is_custom) {
366 switchClass('#images-title', 'tr-version-build', 'tr-custom-build');
367 switchClass('#downloads-title', 'tr-version-downloads', 'tr-custom-downloads');
368 } else {
369 switchClass('#images-title', 'tr-custom-build', 'tr-version-build');
370 switchClass('#downloads-title', 'tr-custom-downloads', 'tr-version-downloads');
371 }
372
373 // update title translation
374 translate();
375
376 // fill out build info
377 $('#image-model').innerText = model;
378 $('#image-target').innerText = target;
379 $('#image-version').innerText = version;
380 $('#image-code').innerText = code;
381 $('#image-date').innerText = date;
382
383 images.sort((a, b) => a.name.localeCompare(b.name));
384
385 for (var i in images) {
386 addLink(images[i].type, images[i].name);
387 }
388
389 if (config.asu_url) {
390 updatePackageList(version, target);
391 }
392
393 show('#images');
394 } else {
395 hide('#images');
396 }
397 }
398
399 function init() {
400 var build_date = "unknown"
401 setupSelectList($('#versions'), Object.keys(config.versions), version => {
402 var url = config.versions[version];
403 if (config.asu_url) {
404 url = config.asu_url + '/' + url + '/profiles.json';
405 }
406 fetch(url)
407 .then(obj => {
408 build_date = obj.headers.get('last-modified');
409 return obj.json();
410 })
411 .then(obj => {
412 // handle native openwrt json format
413 if ('profiles' in obj) {
414 obj['models'] = {}
415 for (const [key, value] of Object.entries(obj['profiles'])) {
416 obj['models'][get_model_titles(value.titles)] = value
417 obj['models'][get_model_titles(value.titles)]['id'] = key
418 }
419 }
420 return obj
421 })
422 .then(obj => {
423 setupAutocompleteList($('#models'), Object.keys(obj['models']), false, updateImages, models => {
424 var model = models.value;
425 if (model in obj['models']) {
426 var url = obj.url || 'unknown';
427 var code = obj.version_code || 'unknown';
428 var mobj = obj['models'][model];
429 updateImages(version, code, build_date, model, url, mobj, false);
430 current_model = mobj;
431 } else {
432 updateImages();
433 current_model = {};
434 }
435 });
436
437 // trigger model update when selected version changes
438 $('#models').onfocus();
439 });
440 });
441
442 if (config.asu_url) {
443 show('#custom');
444 }
445
446 // hide fields
447 updateImages();
448
449 var user_lang = (navigator.language || navigator.userLanguage).split('-')[0];
450 if (user_lang in translations) {
451 config.language = user_lang;
452 $('#language-selection').value = user_lang;
453 }
454
455 translate();
456
457 $('#language-selection').onclick = function() {
458 config.language = this.children[this.selectedIndex].value;
459 translate();
460 }
461 }