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