Merge pull request #2285 from dengqf6/luci-ssl-nginx
[project/luci.git] / applications / luci-app-opkg / htdocs / luci-static / resources / view / opkg.js
1 var packages = {
2 available: { providers: {}, pkgs: {} },
3 installed: { providers: {}, pkgs: {} }
4 };
5
6 var currentDisplayMode = 'available', currentDisplayRows = [];
7
8 function parseList(s, dest)
9 {
10 var re = /([^\n]*)\n/g,
11 pkg = null, key = null, val = null, m;
12
13 while ((m = re.exec(s)) !== null) {
14 if (m[1].match(/^\s(.*)$/)) {
15 if (pkg !== null && key !== null && val !== null)
16 val += '\n' + RegExp.$1.trim();
17
18 continue;
19 }
20
21 if (key !== null && val !== null) {
22 switch (key) {
23 case 'package':
24 pkg = { name: val };
25 break;
26
27 case 'depends':
28 case 'provides':
29 var list = val.split(/\s*,\s*/);
30 if (list.length !== 1 || list[0].length > 0)
31 pkg[key] = list;
32 break;
33
34 case 'installed-time':
35 pkg.installtime = new Date(+val * 1000);
36 break;
37
38 case 'installed-size':
39 pkg.installsize = +val;
40 break;
41
42 case 'status':
43 var stat = val.split(/\s+/),
44 mode = stat[1],
45 installed = stat[2];
46
47 switch (mode) {
48 case 'user':
49 case 'hold':
50 pkg[mode] = true;
51 break;
52 }
53
54 switch (installed) {
55 case 'installed':
56 pkg.installed = true;
57 break;
58 }
59 break;
60
61 case 'essential':
62 if (val === 'yes')
63 pkg.essential = true;
64 break;
65
66 case 'size':
67 pkg.size = +val;
68 break;
69
70 case 'architecture':
71 case 'auto-installed':
72 case 'filename':
73 case 'sha256sum':
74 case 'section':
75 break;
76
77 default:
78 pkg[key] = val;
79 break;
80 }
81
82 key = val = null;
83 }
84
85 if (m[1].trim().match(/^([\w-]+)\s*:(.+)$/)) {
86 key = RegExp.$1.toLowerCase();
87 val = RegExp.$2.trim();
88 }
89 else {
90 dest.pkgs[pkg.name] = pkg;
91
92 var provides = dest.providers[pkg.name] ? [] : [ pkg.name ];
93
94 if (pkg.provides)
95 provides.push.apply(provides, pkg.provides);
96
97 provides.forEach(function(p) {
98 dest.providers[p] = dest.providers[p] || [];
99 dest.providers[p].push(pkg);
100 });
101 }
102 }
103 }
104
105 function display(pattern)
106 {
107 var src = packages[currentDisplayMode === 'updates' ? 'installed' : currentDisplayMode],
108 table = document.querySelector('#packages'),
109 pager = document.querySelector('#pager');
110
111 currentDisplayRows.length = 0;
112
113 if (typeof(pattern) === 'string' && pattern.length > 0)
114 pattern = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'ig');
115
116 for (var name in src.pkgs) {
117 var pkg = src.pkgs[name],
118 desc = pkg.description || '',
119 altsize = null;
120
121 if (!pkg.size && packages.available.pkgs[name])
122 altsize = packages.available.pkgs[name].size;
123
124 if (!desc && packages.available.pkgs[name])
125 desc = packages.available.pkgs[name].description || '';
126
127 desc = desc.split(/\n/);
128 desc = desc[0].trim() + (desc.length > 1 ? '…' : '');
129
130 if ((pattern instanceof RegExp) &&
131 !name.match(pattern) && !desc.match(pattern))
132 continue;
133
134 var btn, ver;
135
136 if (currentDisplayMode === 'updates') {
137 var avail = packages.available.pkgs[name];
138 if (!avail || avail.version === pkg.version)
139 continue;
140
141 ver = '%s » %s'.format(
142 truncateVersion(pkg.version || '-'),
143 truncateVersion(avail.version || '-'));
144
145 btn = E('div', {
146 'class': 'btn cbi-button-positive',
147 'data-package': name,
148 'click': handleInstall
149 }, _('Upgrade…'));
150 }
151 else if (currentDisplayMode === 'installed') {
152 ver = truncateVersion(pkg.version || '-');
153 btn = E('div', {
154 'class': 'btn cbi-button-negative',
155 'data-package': name,
156 'click': handleRemove
157 }, _('Remove'));
158 }
159 else {
160 ver = truncateVersion(pkg.version || '-');
161
162 if (!packages.installed.pkgs[name])
163 btn = E('div', {
164 'class': 'btn cbi-button-action',
165 'data-package': name,
166 'click': handleInstall
167 }, _('Install…'));
168 else if (packages.installed.pkgs[name].version != pkg.version)
169 btn = E('div', {
170 'class': 'btn cbi-button-positive',
171 'data-package': name,
172 'click': handleInstall
173 }, _('Upgrade…'));
174 else
175 btn = E('div', {
176 'class': 'btn cbi-button-neutral',
177 'disabled': 'disabled'
178 }, _('Installed'));
179 }
180
181 name = '%h'.format(name);
182 desc = '%h'.format(desc || '-');
183
184 if (pattern) {
185 name = name.replace(pattern, '<ins>$&</ins>');
186 desc = desc.replace(pattern, '<ins>$&</ins>');
187 }
188
189 currentDisplayRows.push([
190 name,
191 ver,
192 pkg.size ? '%.1024mB'.format(pkg.size)
193 : (altsize ? '~%.1024mB'.format(altsize) : '-'),
194 desc,
195 btn
196 ]);
197 }
198
199 currentDisplayRows.sort(function(a, b) {
200 if (a[0] < b[0])
201 return -1;
202 else if (a[0] > b[0])
203 return 1;
204 else
205 return 0;
206 });
207
208 pager.parentNode.style.display = '';
209 pager.setAttribute('data-offset', 100);
210 handlePage({ target: pager.querySelector('.prev') });
211 }
212
213 function handlePage(ev)
214 {
215 var filter = document.querySelector('input[name="filter"]'),
216 pager = ev.target.parentNode,
217 offset = +pager.getAttribute('data-offset'),
218 next = ev.target.classList.contains('next');
219
220 if ((next && (offset + 100) >= currentDisplayRows.length) ||
221 (!next && (offset < 100)))
222 return;
223
224 offset += next ? 100 : -100;
225 pager.setAttribute('data-offset', offset);
226 pager.querySelector('.text').firstChild.data = currentDisplayRows.length
227 ? _('Displaying %d-%d of %d').format(1 + offset, Math.min(offset + 100, currentDisplayRows.length), currentDisplayRows.length)
228 : _('No packages');
229
230 if (offset < 100)
231 pager.querySelector('.prev').setAttribute('disabled', 'disabled');
232 else
233 pager.querySelector('.prev').removeAttribute('disabled');
234
235 if ((offset + 100) >= currentDisplayRows.length)
236 pager.querySelector('.next').setAttribute('disabled', 'disabled');
237 else
238 pager.querySelector('.next').removeAttribute('disabled');
239
240 var placeholder = _('No information available');
241
242 if (filter.value)
243 placeholder = [
244 E('span', {}, _('No packages matching "<strong>%h</strong>".').format(filter.value)), ' (',
245 E('a', { href: '#', onclick: 'handleReset(event)' }, _('Reset')), ')'
246 ];
247
248 cbi_update_table('#packages', currentDisplayRows.slice(offset, offset + 100),
249 placeholder);
250 }
251
252 function handleMode(ev)
253 {
254 var tab = findParent(ev.target, 'li');
255 if (tab.getAttribute('data-mode') === currentDisplayMode)
256 return;
257
258 tab.parentNode.querySelectorAll('li').forEach(function(li) {
259 li.classList.remove('cbi-tab');
260 li.classList.add('cbi-tab-disabled');
261 });
262
263 tab.classList.remove('cbi-tab-disabled');
264 tab.classList.add('cbi-tab');
265
266 currentDisplayMode = tab.getAttribute('data-mode');
267
268 display(document.querySelector('input[name="filter"]').value);
269
270 ev.target.blur();
271 ev.preventDefault();
272 }
273
274 function orderOf(c)
275 {
276 if (c === '~')
277 return -1;
278 else if (c === '' || c >= '0' && c <= '9')
279 return 0;
280 else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))
281 return c.charCodeAt(0);
282 else
283 return c.charCodeAt(0) + 256;
284 }
285
286 function compareVersion(val, ref)
287 {
288 var vi = 0, ri = 0,
289 isdigit = { 0:1, 1:1, 2:1, 3:1, 4:1, 5:1, 6:1, 7:1, 8:1, 9:1 };
290
291 val = val || '';
292 ref = ref || '';
293
294 while (vi < val.length || ri < ref.length) {
295 var first_diff = 0;
296
297 while ((vi < val.length && !isdigit[val.charAt(vi)]) ||
298 (ri < ref.length && !isdigit[ref.charAt(ri)])) {
299 var vc = orderOf(val.charAt(vi)), rc = orderOf(ref.charAt(ri));
300 if (vc !== rc)
301 return vc - rc;
302
303 vi++; ri++;
304 }
305
306 while (val.charAt(vi) === '0')
307 vi++;
308
309 while (ref.charAt(ri) === '0')
310 ri++;
311
312 while (isdigit[val.charAt(vi)] && isdigit[ref.charAt(ri)]) {
313 first_diff = first_diff || (val.charCodeAt(vi) - ref.charCodeAt(ri));
314 vi++; ri++;
315 }
316
317 if (isdigit[val.charAt(vi)])
318 return 1;
319 else if (isdigit[ref.charAt(ri)])
320 return -1;
321 else if (first_diff)
322 return first_diff;
323 }
324
325 return 0;
326 }
327
328 function versionSatisfied(ver, ref, vop)
329 {
330 var r = compareVersion(ver, ref);
331
332 switch (vop) {
333 case '<':
334 case '<=':
335 return r <= 0;
336
337 case '>':
338 case '>=':
339 return r >= 0;
340
341 case '<<':
342 return r < 0;
343
344 case '>>':
345 return r > 0;
346
347 case '=':
348 return r == 0;
349 }
350
351 return false;
352 }
353
354 function pkgStatus(pkg, vop, ver, info)
355 {
356 info.errors = info.errors || [];
357 info.install = info.install || [];
358
359 if (pkg.installed) {
360 if (vop && !versionSatisfied(pkg.version, ver, vop)) {
361 var repl = null;
362
363 (packages.available.providers[pkg.name] || []).forEach(function(p) {
364 if (!repl && versionSatisfied(p.version, ver, vop))
365 repl = p;
366 });
367
368 if (repl) {
369 info.install.push(repl);
370 return E('span', {
371 'class': 'label',
372 'data-tooltip': _('Requires update to %h %h')
373 .format(repl.name, repl.version)
374 }, _('Needs upgrade'));
375 }
376
377 info.errors.push(_('The installed version of package <em>%h</em> is not compatible, require %s while %s is installed.').format(pkg.name, truncateVersion(ver, vop), truncateVersion(pkg.version)));
378
379 return E('span', {
380 'class': 'label warning',
381 'data-tooltip': _('Require version %h %h,\ninstalled %h')
382 .format(vop, ver, pkg.version)
383 }, _('Version incompatible'));
384 }
385
386 return E('span', { 'class': 'label notice' }, _('Installed'));
387 }
388 else if (!pkg.missing) {
389 if (!vop || versionSatisfied(pkg.version, ver, vop)) {
390 info.install.push(pkg);
391 return E('span', { 'class': 'label' }, _('Not installed'));
392 }
393
394 info.errors.push(_('The repository version of package <em>%h</em> is not compatible, require %s but only %s is available.')
395 .format(pkg.name, truncateVersion(ver, vop), truncateVersion(pkg.version)));
396
397 return E('span', {
398 'class': 'label warning',
399 'data-tooltip': _('Require version %h %h,\ninstalled %h')
400 .format(vop, ver, pkg.version)
401 }, _('Version incompatible'));
402 }
403 else {
404 info.errors.push(_('Required dependency package <em>%h</em> is not available in any repository.').format(pkg.name));
405
406 return E('span', { 'class': 'label warning' }, _('Not available'));
407 }
408 }
409
410 function renderDependencyItem(dep, info)
411 {
412 var li = E('li'),
413 vop = dep.version ? dep.version[0] : null,
414 ver = dep.version ? dep.version[1] : null,
415 depends = [];
416
417 for (var i = 0; dep.pkgs && i < dep.pkgs.length; i++) {
418 var pkg = packages.installed.pkgs[dep.pkgs[i]] ||
419 packages.available.pkgs[dep.pkgs[i]] ||
420 { name: dep.name };
421
422 if (i > 0)
423 li.appendChild(document.createTextNode(' | '));
424
425 var text = pkg.name;
426
427 if (pkg.installsize)
428 text += ' (%.1024mB)'.format(pkg.installsize);
429 else if (pkg.size)
430 text += ' (~%.1024mB)'.format(pkg.size);
431
432 li.appendChild(E('span', { 'data-tooltip': pkg.description },
433 [ text, ' ', pkgStatus(pkg, vop, ver, info) ]));
434
435 (pkg.depends || []).forEach(function(d) {
436 if (depends.indexOf(d) === -1)
437 depends.push(d);
438 });
439 }
440
441 if (!li.firstChild)
442 li.appendChild(E('span', {},
443 [ dep.name, ' ',
444 pkgStatus({ name: dep.name, missing: true }, vop, ver, info) ]));
445
446 var subdeps = renderDependencies(depends, info);
447 if (subdeps)
448 li.appendChild(subdeps);
449
450 return li;
451 }
452
453 function renderDependencies(depends, info)
454 {
455 var deps = depends || [],
456 items = [];
457
458 info.seen = info.seen || [];
459
460 for (var i = 0; i < deps.length; i++) {
461 if (deps[i] === 'libc')
462 continue;
463
464 if (deps[i].match(/^(.+)\s+\((<=|<|>|>=|=|<<|>>)(.+)\)$/)) {
465 dep = RegExp.$1.trim();
466 vop = RegExp.$2.trim();
467 ver = RegExp.$3.trim();
468 }
469 else {
470 dep = deps[i].trim();
471 vop = ver = null;
472 }
473
474 if (info.seen[dep])
475 continue;
476
477 var pkgs = [];
478
479 (packages.installed.providers[dep] || []).forEach(function(p) {
480 if (pkgs.indexOf(p.name) === -1) pkgs.push(p.name);
481 });
482
483 (packages.available.providers[dep] || []).forEach(function(p) {
484 if (pkgs.indexOf(p.name) === -1) pkgs.push(p.name);
485 });
486
487 info.seen[dep] = {
488 name: dep,
489 pkgs: pkgs,
490 version: [vop, ver]
491 };
492
493 items.push(renderDependencyItem(info.seen[dep], info));
494 }
495
496 if (items.length)
497 return E('ul', { 'class': 'deps' }, items);
498
499 return null;
500 }
501
502 function truncateVersion(v, op)
503 {
504 v = v.replace(/\b(([a-f0-9]{8})[a-f0-9]{24,32})\b/,
505 '<span data-tooltip="$1">$2…</span>');
506
507 if (!op || op === '=')
508 return v;
509
510 return '%h %h'.format(op, v);
511 }
512
513 function handleReset(ev)
514 {
515 var filter = document.querySelector('input[name="filter"]');
516
517 filter.value = '';
518 display();
519 }
520
521 function handleInstall(ev)
522 {
523 var name = ev.target.getAttribute('data-package'),
524 pkg = packages.available.pkgs[name],
525 depcache = {},
526 size;
527
528 if (pkg.installsize)
529 size = _('~%.1024mB installed').format(pkg.installsize);
530 else if (pkg.size)
531 size = _('~%.1024mB compressed').format(pkg.size);
532 else
533 size = _('unknown');
534
535 var deps = renderDependencies(pkg.depends, depcache),
536 tree = null, errs = null, inst = null, desc = null;
537
538 if (depcache.errors && depcache.errors.length) {
539 errs = E('ul', { 'class': 'errors' });
540 depcache.errors.forEach(function(err) {
541 errs.appendChild(E('li', {}, err));
542 });
543 }
544
545 var totalsize = pkg.installsize || pkg.size || 0,
546 totalpkgs = 1;
547
548 if (depcache.install && depcache.install.length)
549 depcache.install.forEach(function(ipkg) {
550 totalsize += ipkg.installsize || ipkg.size || 0;
551 totalpkgs++;
552 });
553
554 inst = E('p', {},
555 _('Require approx. %.1024mB size for %d package(s) to install.')
556 .format(totalsize, totalpkgs));
557
558 if (deps) {
559 tree = E('li', '<strong>%s:</strong>'.format(_('Dependencies')));
560 tree.appendChild(deps);
561 }
562
563 if (pkg.description) {
564 desc = E('div', {}, [
565 E('h5', {}, _('Description')),
566 E('p', {}, pkg.description)
567 ]);
568 }
569
570 L.showModal(_('Details for package <em>%h</em>').format(pkg.name), [
571 E('ul', {}, [
572 E('li', '<strong>%s:</strong> %h'.format(_('Version'), pkg.version)),
573 E('li', '<strong>%s:</strong> %h'.format(_('Size'), size)),
574 tree || '',
575 ]),
576 desc || '',
577 errs || inst || '',
578 E('div', { 'class': 'right' }, [
579 E('div', {
580 'class': 'btn',
581 'click': L.hideModal
582 }, _('Cancel')),
583 ' ',
584 E('div', {
585 'data-command': 'install',
586 'data-package': name,
587 'class': 'btn cbi-button-action',
588 'click': handleOpkg
589 }, _('Install'))
590 ])
591 ]);
592 }
593
594 function handleManualInstall(ev)
595 {
596 var name_or_url = document.querySelector('input[name="install"]').value,
597 install = E('div', {
598 'class': 'btn cbi-button-action',
599 'data-command': 'install',
600 'data-package': name_or_url,
601 'click': function(ev) {
602 document.querySelector('input[name="install"]').value = '';
603 handleOpkg(ev);
604 }
605 }, _('Install')), warning;
606
607 if (!name_or_url.length) {
608 return;
609 }
610 else if (name_or_url.indexOf('/') !== -1) {
611 warning = E('p', {}, _('Installing packages from untrusted sources is a potential security risk! Really attempt to install <em>%h</em>?').format(name_or_url));
612 }
613 else if (!packages.available.providers[name_or_url]) {
614 warning = E('p', {}, _('The package <em>%h</em> is not available in any configured repository.').format(name_or_url));
615 install = '';
616 }
617 else {
618 warning = E('p', {}, _('Really attempt to install <em>%h</em>?').format(name_or_url));
619 }
620
621 L.showModal(_('Manually install package'), [
622 warning,
623 E('div', { 'class': 'right' }, [
624 E('div', {
625 'click': L.hideModal,
626 'class': 'btn cbi-button-neutral'
627 }, _('Cancel')),
628 ' ', install
629 ])
630 ]);
631 }
632
633 function handleConfig(ev)
634 {
635 L.showModal(_('OPKG Configuration'), [
636 E('p', { 'class': 'spinning' }, _('Loading configuration data…'))
637 ]);
638
639 L.get('admin/system/opkg/config', null, function(xhr, conf) {
640 var body = [
641 E('p', {}, _('Below is a listing of the various configuration files used by <em>opkg</em>. Use <em>opkg.conf</em> for global settings and <em>customfeeds.conf</em> for custom repository entries. The configuration in the other files may be changed but is usually not preserved by <em>sysupgrade</em>.'))
642 ];
643
644 Object.keys(conf).sort().forEach(function(file) {
645 body.push(E('h5', {}, '%h'.format(file)));
646 body.push(E('textarea', {
647 'name': file,
648 'rows': Math.max(Math.min(conf[file].match(/\n/g).length, 10), 3)
649 }, '%h'.format(conf[file])));
650 });
651
652 body.push(E('div', { 'class': 'right' }, [
653 E('div', {
654 'class': 'btn cbi-button-neutral',
655 'click': L.hideModal
656 }, _('Cancel')),
657 ' ',
658 E('div', {
659 'class': 'btn cbi-button-positive',
660 'click': function(ev) {
661 var data = {};
662 findParent(ev.target, '.modal').querySelectorAll('textarea[name]')
663 .forEach(function(textarea) {
664 data[textarea.getAttribute('name')] = textarea.value
665 });
666
667 L.showModal(_('OPKG Configuration'), [
668 E('p', { 'class': 'spinning' }, _('Saving configuration data…'))
669 ]);
670
671 L.post('admin/system/opkg/config', { data: JSON.stringify(data) }, L.hideModal);
672 }
673 }, _('Save')),
674 ]));
675
676 L.showModal(_('OPKG Configuration'), body);
677 });
678 }
679
680 function handleRemove(ev)
681 {
682 var name = ev.target.getAttribute('data-package'),
683 pkg = packages.installed.pkgs[name],
684 avail = packages.available.pkgs[name] || {},
685 size, desc;
686
687 if (avail.installsize)
688 size = _('~%.1024mB installed').format(avail.installsize);
689 else if (avail.size)
690 size = _('~%.1024mB compressed').format(avail.size);
691 else
692 size = _('unknown');
693
694 if (avail.description) {
695 desc = E('div', {}, [
696 E('h5', {}, _('Description')),
697 E('p', {}, avail.description)
698 ]);
699 }
700
701 L.showModal(_('Remove package <em>%h</em>').format(pkg.name), [
702 E('ul', {}, [
703 E('li', '<strong>%s:</strong> %h'.format(_('Version'), pkg.version)),
704 E('li', '<strong>%s:</strong> %h'.format(_('Size'), size))
705 ]),
706 desc || '',
707 E('div', { 'style': 'display:flex; justify-content:space-between; flex-wrap:wrap' }, [
708 E('label', {}, [
709 E('input', { type: 'checkbox', checked: 'checked', name: 'autoremove' }),
710 _('Automatically remove unused dependencies')
711 ]),
712 E('div', { 'style': 'flex-grow:1', 'class': 'right' }, [
713 E('div', {
714 'class': 'btn',
715 'click': L.hideModal
716 }, _('Cancel')),
717 ' ',
718 E('div', {
719 'data-command': 'remove',
720 'data-package': name,
721 'class': 'btn cbi-button-negative',
722 'click': handleOpkg
723 }, _('Remove'))
724 ])
725 ])
726 ]);
727 }
728
729 function handleOpkg(ev)
730 {
731 var cmd = ev.target.getAttribute('data-command'),
732 pkg = ev.target.getAttribute('data-package'),
733 rem = document.querySelector('input[name="autoremove"]'),
734 url = 'admin/system/opkg/exec/' + encodeURIComponent(cmd);
735
736 var dlg = L.showModal(_('Executing package manager'), [
737 E('p', { 'class': 'spinning' },
738 _('Waiting for the <em>opkg %h</em> command to complete…').format(cmd))
739 ]);
740
741 L.post(url, { package: pkg, autoremove: rem ? rem.checked : false }, function(xhr, res) {
742 dlg.removeChild(dlg.lastChild);
743
744 if (res.stdout)
745 dlg.appendChild(E('pre', [ res.stdout ]));
746
747 if (res.stderr) {
748 dlg.appendChild(E('h5', _('Errors')));
749 dlg.appendChild(E('pre', { 'class': 'errors' }, [ res.stderr ]));
750 }
751
752 if (res.code !== 0)
753 dlg.appendChild(E('p', _('The <em>opkg %h</em> command failed with code <code>%d</code>.').format(cmd, (res.code & 0xff) || -1)));
754
755 dlg.appendChild(E('div', { 'class': 'right' },
756 E('div', {
757 'class': 'btn',
758 'click': function() {
759 L.hideModal();
760 updateLists();
761 }
762 }, _('Dismiss'))));
763 });
764 }
765
766 function updateLists()
767 {
768 cbi_update_table('#packages', [],
769 E('div', { 'class': 'spinning' }, _('Loading package information…')));
770
771 packages.available = { providers: {}, pkgs: {} };
772 packages.installed = { providers: {}, pkgs: {} };
773
774 L.get('admin/system/opkg/statvfs', null, function(xhr, stat) {
775 var pg = document.querySelector('.cbi-progressbar'),
776 total = stat.blocks || 0,
777 free = stat.bfree || 0;
778
779 pg.firstElementChild.style.width = Math.floor(total ? ((100 / total) * free) : 100) + '%';
780 pg.setAttribute('title', '%s (%.1024mB)'.format(pg.firstElementChild.style.width, free * (stat.frsize || 0)));
781
782 L.get('admin/system/opkg/list/available', null, function(xhr) {
783 parseList(xhr.responseText, packages.available);
784 L.get('admin/system/opkg/list/installed', null, function(xhr) {
785 parseList(xhr.responseText, packages.installed);
786 display(document.querySelector('input[name="filter"]').value);
787 });
788 });
789 });
790 }
791
792 window.requestAnimationFrame(function() {
793 var filter = document.querySelector('input[name="filter"]'),
794 keyTimeout = null;
795
796 filter.value = filter.getAttribute('value');
797 filter.addEventListener('keyup',
798 function(ev) {
799 if (keyTimeout !== null)
800 window.clearTimeout(keyTimeout);
801
802 keyTimeout = window.setTimeout(function() {
803 display(ev.target.value);
804 }, 250);
805 });
806
807 document.querySelector('#pager > .prev').addEventListener('click', handlePage);
808 document.querySelector('#pager > .next').addEventListener('click', handlePage);
809 document.querySelector('.cbi-tabmenu.mode').addEventListener('click', handleMode);
810
811 updateLists();
812 });