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