luci-base: replace uci change pages with client side modal dialog
[project/luci.git] / modules / luci-base / htdocs / luci-static / resources / ui.js
1 'use strict';
2 'require uci';
3
4 var modalDiv = null,
5 tooltipDiv = null,
6 tooltipTimeout = null;
7
8 var UIElement = L.Class.extend({
9 getValue: function() {
10 if (L.dom.matches(this.node, 'select') || L.dom.matches(this.node, 'input'))
11 return this.node.value;
12
13 return null;
14 },
15
16 setValue: function(value) {
17 if (L.dom.matches(this.node, 'select') || L.dom.matches(this.node, 'input'))
18 this.node.value = value;
19 },
20
21 isValid: function() {
22 return true;
23 },
24
25 registerEvents: function(targetNode, synevent, events) {
26 var dispatchFn = L.bind(function(ev) {
27 this.node.dispatchEvent(new CustomEvent(synevent, { bubbles: true }));
28 }, this);
29
30 for (var i = 0; i < events.length; i++)
31 targetNode.addEventListener(events[i], dispatchFn);
32 },
33
34 setUpdateEvents: function(targetNode /*, ... */) {
35 this.registerEvents(targetNode, 'widget-update', this.varargs(arguments, 1));
36 },
37
38 setChangeEvents: function(targetNode /*, ... */) {
39 this.registerEvents(targetNode, 'widget-change', this.varargs(arguments, 1));
40 }
41 });
42
43 var UIDropdown = UIElement.extend({
44 __init__: function(value, choices, options) {
45 if (typeof(choices) != 'object')
46 choices = {};
47
48 if (!Array.isArray(value))
49 this.values = (value != null) ? [ value ] : [];
50 else
51 this.values = value;
52
53 this.choices = choices;
54 this.options = Object.assign({
55 sort: true,
56 multi: Array.isArray(value),
57 optional: true,
58 select_placeholder: _('-- Please choose --'),
59 custom_placeholder: _('-- custom --'),
60 display_items: 3,
61 dropdown_items: 5,
62 create: false,
63 create_query: '.create-item-input',
64 create_template: 'script[type="item-template"]'
65 }, options);
66 },
67
68 render: function() {
69 var sb = E('div', {
70 'id': this.options.id,
71 'class': 'cbi-dropdown',
72 'multiple': this.options.multi ? '' : null,
73 'optional': this.options.optional ? '' : null,
74 }, E('ul'));
75
76 var keys = Object.keys(this.choices);
77
78 if (this.options.sort === true)
79 keys.sort();
80 else if (Array.isArray(this.options.sort))
81 keys = this.options.sort;
82
83 if (this.options.create)
84 for (var i = 0; i < this.values.length; i++)
85 if (!this.choices.hasOwnProperty(this.values[i]))
86 keys.push(this.values[i]);
87
88 for (var i = 0; i < keys.length; i++)
89 sb.lastElementChild.appendChild(E('li', {
90 'data-value': keys[i],
91 'selected': (this.values.indexOf(keys[i]) > -1) ? '' : null
92 }, this.choices[keys[i]] || keys[i]));
93
94 if (this.options.create) {
95 var createEl = E('input', {
96 'type': 'text',
97 'class': 'create-item-input',
98 'placeholder': this.options.custom_placeholder || this.options.placeholder
99 });
100
101 if (this.options.datatype)
102 L.ui.addValidator(createEl, this.options.datatype, true, 'blur', 'keyup');
103
104 sb.lastElementChild.appendChild(E('li', { 'data-value': '-' }, createEl));
105 }
106
107 return this.bind(sb);
108 },
109
110 bind: function(sb) {
111 var o = this.options;
112
113 o.multi = sb.hasAttribute('multiple');
114 o.optional = sb.hasAttribute('optional');
115 o.placeholder = sb.getAttribute('placeholder') || o.placeholder;
116 o.display_items = parseInt(sb.getAttribute('display-items') || o.display_items);
117 o.dropdown_items = parseInt(sb.getAttribute('dropdown-items') || o.dropdown_items);
118 o.create_query = sb.getAttribute('item-create') || o.create_query;
119 o.create_template = sb.getAttribute('item-template') || o.create_template;
120
121 var ul = sb.querySelector('ul'),
122 more = sb.appendChild(E('span', { class: 'more', tabindex: -1 }, '···')),
123 open = sb.appendChild(E('span', { class: 'open', tabindex: -1 }, '▾')),
124 canary = sb.appendChild(E('div')),
125 create = sb.querySelector(this.options.create_query),
126 ndisplay = this.options.display_items,
127 n = 0;
128
129 if (this.options.multi) {
130 var items = ul.querySelectorAll('li');
131
132 for (var i = 0; i < items.length; i++) {
133 this.transformItem(sb, items[i]);
134
135 if (items[i].hasAttribute('selected') && ndisplay-- > 0)
136 items[i].setAttribute('display', n++);
137 }
138 }
139 else {
140 if (this.options.optional && !ul.querySelector('li[data-value=""]')) {
141 var placeholder = E('li', { placeholder: '' },
142 this.options.select_placeholder || this.options.placeholder);
143
144 ul.firstChild
145 ? ul.insertBefore(placeholder, ul.firstChild)
146 : ul.appendChild(placeholder);
147 }
148
149 var items = ul.querySelectorAll('li'),
150 sel = sb.querySelectorAll('[selected]');
151
152 sel.forEach(function(s) {
153 s.removeAttribute('selected');
154 });
155
156 var s = sel[0] || items[0];
157 if (s) {
158 s.setAttribute('selected', '');
159 s.setAttribute('display', n++);
160 }
161
162 ndisplay--;
163 }
164
165 this.saveValues(sb, ul);
166
167 ul.setAttribute('tabindex', -1);
168 sb.setAttribute('tabindex', 0);
169
170 if (ndisplay < 0)
171 sb.setAttribute('more', '')
172 else
173 sb.removeAttribute('more');
174
175 if (ndisplay == this.options.display_items)
176 sb.setAttribute('empty', '')
177 else
178 sb.removeAttribute('empty');
179
180 more.innerHTML = (ndisplay == this.options.display_items)
181 ? (this.options.select_placeholder || this.options.placeholder) : '···';
182
183
184 sb.addEventListener('click', this.handleClick.bind(this));
185 sb.addEventListener('keydown', this.handleKeydown.bind(this));
186 sb.addEventListener('cbi-dropdown-close', this.handleDropdownClose.bind(this));
187 sb.addEventListener('cbi-dropdown-select', this.handleDropdownSelect.bind(this));
188
189 if ('ontouchstart' in window) {
190 sb.addEventListener('touchstart', function(ev) { ev.stopPropagation(); });
191 window.addEventListener('touchstart', this.closeAllDropdowns);
192 }
193 else {
194 sb.addEventListener('mouseover', this.handleMouseover.bind(this));
195 sb.addEventListener('focus', this.handleFocus.bind(this));
196
197 canary.addEventListener('focus', this.handleCanaryFocus.bind(this));
198
199 window.addEventListener('mouseover', this.setFocus);
200 window.addEventListener('click', this.closeAllDropdowns);
201 }
202
203 if (create) {
204 create.addEventListener('keydown', this.handleCreateKeydown.bind(this));
205 create.addEventListener('focus', this.handleCreateFocus.bind(this));
206 create.addEventListener('blur', this.handleCreateBlur.bind(this));
207
208 var li = findParent(create, 'li');
209
210 li.setAttribute('unselectable', '');
211 li.addEventListener('click', this.handleCreateClick.bind(this));
212 }
213
214 this.node = sb;
215
216 this.setUpdateEvents(sb, 'cbi-dropdown-open', 'cbi-dropdown-close');
217 this.setChangeEvents(sb, 'cbi-dropdown-change', 'cbi-dropdown-close');
218
219 L.dom.bindClassInstance(sb, this);
220
221 return sb;
222 },
223
224 openDropdown: function(sb) {
225 var st = window.getComputedStyle(sb, null),
226 ul = sb.querySelector('ul'),
227 li = ul.querySelectorAll('li'),
228 fl = findParent(sb, '.cbi-value-field'),
229 sel = ul.querySelector('[selected]'),
230 rect = sb.getBoundingClientRect(),
231 items = Math.min(this.options.dropdown_items, li.length);
232
233 document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) {
234 s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
235 });
236
237 sb.setAttribute('open', '');
238
239 var pv = ul.cloneNode(true);
240 pv.classList.add('preview');
241
242 if (fl)
243 fl.classList.add('cbi-dropdown-open');
244
245 if ('ontouchstart' in window) {
246 var vpWidth = Math.max(document.documentElement.clientWidth, window.innerWidth || 0),
247 vpHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0),
248 scrollFrom = window.pageYOffset,
249 scrollTo = scrollFrom + rect.top - vpHeight * 0.5,
250 start = null;
251
252 ul.style.top = sb.offsetHeight + 'px';
253 ul.style.left = -rect.left + 'px';
254 ul.style.right = (rect.right - vpWidth) + 'px';
255 ul.style.maxHeight = (vpHeight * 0.5) + 'px';
256 ul.style.WebkitOverflowScrolling = 'touch';
257
258 var scrollStep = function(timestamp) {
259 if (!start) {
260 start = timestamp;
261 ul.scrollTop = sel ? Math.max(sel.offsetTop - sel.offsetHeight, 0) : 0;
262 }
263
264 var duration = Math.max(timestamp - start, 1);
265 if (duration < 100) {
266 document.body.scrollTop = scrollFrom + (scrollTo - scrollFrom) * (duration / 100);
267 window.requestAnimationFrame(scrollStep);
268 }
269 else {
270 document.body.scrollTop = scrollTo;
271 }
272 };
273
274 window.requestAnimationFrame(scrollStep);
275 }
276 else {
277 ul.style.maxHeight = '1px';
278 ul.style.top = ul.style.bottom = '';
279
280 window.requestAnimationFrame(function() {
281 var height = items * li[Math.max(0, li.length - 2)].offsetHeight;
282
283 ul.scrollTop = sel ? Math.max(sel.offsetTop - sel.offsetHeight, 0) : 0;
284 ul.style[((rect.top + rect.height + height) > window.innerHeight) ? 'bottom' : 'top'] = rect.height + 'px';
285 ul.style.maxHeight = height + 'px';
286 });
287 }
288
289 var cboxes = ul.querySelectorAll('[selected] input[type="checkbox"]');
290 for (var i = 0; i < cboxes.length; i++) {
291 cboxes[i].checked = true;
292 cboxes[i].disabled = (cboxes.length == 1 && !this.options.optional);
293 };
294
295 ul.classList.add('dropdown');
296
297 sb.insertBefore(pv, ul.nextElementSibling);
298
299 li.forEach(function(l) {
300 l.setAttribute('tabindex', 0);
301 });
302
303 sb.lastElementChild.setAttribute('tabindex', 0);
304
305 this.setFocus(sb, sel || li[0], true);
306 },
307
308 closeDropdown: function(sb, no_focus) {
309 if (!sb.hasAttribute('open'))
310 return;
311
312 var pv = sb.querySelector('ul.preview'),
313 ul = sb.querySelector('ul.dropdown'),
314 li = ul.querySelectorAll('li'),
315 fl = findParent(sb, '.cbi-value-field');
316
317 li.forEach(function(l) { l.removeAttribute('tabindex'); });
318 sb.lastElementChild.removeAttribute('tabindex');
319
320 sb.removeChild(pv);
321 sb.removeAttribute('open');
322 sb.style.width = sb.style.height = '';
323
324 ul.classList.remove('dropdown');
325 ul.style.top = ul.style.bottom = ul.style.maxHeight = '';
326
327 if (fl)
328 fl.classList.remove('cbi-dropdown-open');
329
330 if (!no_focus)
331 this.setFocus(sb, sb);
332
333 this.saveValues(sb, ul);
334 },
335
336 toggleItem: function(sb, li, force_state) {
337 if (li.hasAttribute('unselectable'))
338 return;
339
340 if (this.options.multi) {
341 var cbox = li.querySelector('input[type="checkbox"]'),
342 items = li.parentNode.querySelectorAll('li'),
343 label = sb.querySelector('ul.preview'),
344 sel = li.parentNode.querySelectorAll('[selected]').length,
345 more = sb.querySelector('.more'),
346 ndisplay = this.options.display_items,
347 n = 0;
348
349 if (li.hasAttribute('selected')) {
350 if (force_state !== true) {
351 if (sel > 1 || this.options.optional) {
352 li.removeAttribute('selected');
353 cbox.checked = cbox.disabled = false;
354 sel--;
355 }
356 else {
357 cbox.disabled = true;
358 }
359 }
360 }
361 else {
362 if (force_state !== false) {
363 li.setAttribute('selected', '');
364 cbox.checked = true;
365 cbox.disabled = false;
366 sel++;
367 }
368 }
369
370 while (label && label.firstElementChild)
371 label.removeChild(label.firstElementChild);
372
373 for (var i = 0; i < items.length; i++) {
374 items[i].removeAttribute('display');
375 if (items[i].hasAttribute('selected')) {
376 if (ndisplay-- > 0) {
377 items[i].setAttribute('display', n++);
378 if (label)
379 label.appendChild(items[i].cloneNode(true));
380 }
381 var c = items[i].querySelector('input[type="checkbox"]');
382 if (c)
383 c.disabled = (sel == 1 && !this.options.optional);
384 }
385 }
386
387 if (ndisplay < 0)
388 sb.setAttribute('more', '');
389 else
390 sb.removeAttribute('more');
391
392 if (ndisplay === this.options.display_items)
393 sb.setAttribute('empty', '');
394 else
395 sb.removeAttribute('empty');
396
397 more.innerHTML = (ndisplay === this.options.display_items)
398 ? (this.options.select_placeholder || this.options.placeholder) : '···';
399 }
400 else {
401 var sel = li.parentNode.querySelector('[selected]');
402 if (sel) {
403 sel.removeAttribute('display');
404 sel.removeAttribute('selected');
405 }
406
407 li.setAttribute('display', 0);
408 li.setAttribute('selected', '');
409
410 this.closeDropdown(sb, true);
411 }
412
413 this.saveValues(sb, li.parentNode);
414 },
415
416 transformItem: function(sb, li) {
417 var cbox = E('form', {}, E('input', { type: 'checkbox', tabindex: -1, onclick: 'event.preventDefault()' })),
418 label = E('label');
419
420 while (li.firstChild)
421 label.appendChild(li.firstChild);
422
423 li.appendChild(cbox);
424 li.appendChild(label);
425 },
426
427 saveValues: function(sb, ul) {
428 var sel = ul.querySelectorAll('li[selected]'),
429 div = sb.lastElementChild,
430 name = this.options.name,
431 strval = '',
432 values = [];
433
434 while (div.lastElementChild)
435 div.removeChild(div.lastElementChild);
436
437 sel.forEach(function (s) {
438 if (s.hasAttribute('placeholder'))
439 return;
440
441 var v = {
442 text: s.innerText,
443 value: s.hasAttribute('data-value') ? s.getAttribute('data-value') : s.innerText,
444 element: s
445 };
446
447 div.appendChild(E('input', {
448 type: 'hidden',
449 name: name,
450 value: v.value
451 }));
452
453 values.push(v);
454
455 strval += strval.length ? ' ' + v.value : v.value;
456 });
457
458 var detail = {
459 instance: this,
460 element: sb
461 };
462
463 if (this.options.multi)
464 detail.values = values;
465 else
466 detail.value = values.length ? values[0] : null;
467
468 sb.value = strval;
469
470 sb.dispatchEvent(new CustomEvent('cbi-dropdown-change', {
471 bubbles: true,
472 detail: detail
473 }));
474 },
475
476 setValues: function(sb, values) {
477 var ul = sb.querySelector('ul');
478
479 if (this.options.create) {
480 for (var value in values) {
481 this.createItems(sb, value);
482
483 if (!this.options.multi)
484 break;
485 }
486 }
487
488 if (this.options.multi) {
489 var lis = ul.querySelectorAll('li[data-value]');
490 for (var i = 0; i < lis.length; i++) {
491 var value = lis[i].getAttribute('data-value');
492 if (values === null || !(value in values))
493 this.toggleItem(sb, lis[i], false);
494 else
495 this.toggleItem(sb, lis[i], true);
496 }
497 }
498 else {
499 var ph = ul.querySelector('li[placeholder]');
500 if (ph)
501 this.toggleItem(sb, ph);
502
503 var lis = ul.querySelectorAll('li[data-value]');
504 for (var i = 0; i < lis.length; i++) {
505 var value = lis[i].getAttribute('data-value');
506 if (values !== null && (value in values))
507 this.toggleItem(sb, lis[i]);
508 }
509 }
510 },
511
512 setFocus: function(sb, elem, scroll) {
513 if (sb && sb.hasAttribute && sb.hasAttribute('locked-in'))
514 return;
515
516 if (sb.target && findParent(sb.target, 'ul.dropdown'))
517 return;
518
519 document.querySelectorAll('.focus').forEach(function(e) {
520 if (!matchesElem(e, 'input')) {
521 e.classList.remove('focus');
522 e.blur();
523 }
524 });
525
526 if (elem) {
527 elem.focus();
528 elem.classList.add('focus');
529
530 if (scroll)
531 elem.parentNode.scrollTop = elem.offsetTop - elem.parentNode.offsetTop;
532 }
533 },
534
535 createItems: function(sb, value) {
536 var sbox = this,
537 val = (value || '').trim(),
538 ul = sb.querySelector('ul');
539
540 if (!sbox.options.multi)
541 val = val.length ? [ val ] : [];
542 else
543 val = val.length ? val.split(/\s+/) : [];
544
545 val.forEach(function(item) {
546 var new_item = null;
547
548 ul.childNodes.forEach(function(li) {
549 if (li.getAttribute && li.getAttribute('data-value') === item)
550 new_item = li;
551 });
552
553 if (!new_item) {
554 var markup,
555 tpl = sb.querySelector(sbox.options.create_template);
556
557 if (tpl)
558 markup = (tpl.textContent || tpl.innerHTML || tpl.firstChild.data).replace(/^<!--|-->$/, '').trim();
559 else
560 markup = '<li data-value="{{value}}">{{value}}</li>';
561
562 new_item = E(markup.replace(/{{value}}/g, item));
563
564 if (sbox.options.multi) {
565 sbox.transformItem(sb, new_item);
566 }
567 else {
568 var old = ul.querySelector('li[created]');
569 if (old)
570 ul.removeChild(old);
571
572 new_item.setAttribute('created', '');
573 }
574
575 new_item = ul.insertBefore(new_item, ul.lastElementChild);
576 }
577
578 sbox.toggleItem(sb, new_item, true);
579 sbox.setFocus(sb, new_item, true);
580 });
581 },
582
583 closeAllDropdowns: function() {
584 document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) {
585 s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
586 });
587 },
588
589 handleClick: function(ev) {
590 var sb = ev.currentTarget;
591
592 if (!sb.hasAttribute('open')) {
593 if (!matchesElem(ev.target, 'input'))
594 this.openDropdown(sb);
595 }
596 else {
597 var li = findParent(ev.target, 'li');
598 if (li && li.parentNode.classList.contains('dropdown'))
599 this.toggleItem(sb, li);
600 else if (li && li.parentNode.classList.contains('preview'))
601 this.closeDropdown(sb);
602 }
603
604 ev.preventDefault();
605 ev.stopPropagation();
606 },
607
608 handleKeydown: function(ev) {
609 var sb = ev.currentTarget;
610
611 if (matchesElem(ev.target, 'input'))
612 return;
613
614 if (!sb.hasAttribute('open')) {
615 switch (ev.keyCode) {
616 case 37:
617 case 38:
618 case 39:
619 case 40:
620 this.openDropdown(sb);
621 ev.preventDefault();
622 }
623 }
624 else {
625 var active = findParent(document.activeElement, 'li');
626
627 switch (ev.keyCode) {
628 case 27:
629 this.closeDropdown(sb);
630 break;
631
632 case 13:
633 if (active) {
634 if (!active.hasAttribute('selected'))
635 this.toggleItem(sb, active);
636 this.closeDropdown(sb);
637 ev.preventDefault();
638 }
639 break;
640
641 case 32:
642 if (active) {
643 this.toggleItem(sb, active);
644 ev.preventDefault();
645 }
646 break;
647
648 case 38:
649 if (active && active.previousElementSibling) {
650 this.setFocus(sb, active.previousElementSibling);
651 ev.preventDefault();
652 }
653 break;
654
655 case 40:
656 if (active && active.nextElementSibling) {
657 this.setFocus(sb, active.nextElementSibling);
658 ev.preventDefault();
659 }
660 break;
661 }
662 }
663 },
664
665 handleDropdownClose: function(ev) {
666 var sb = ev.currentTarget;
667
668 this.closeDropdown(sb, true);
669 },
670
671 handleDropdownSelect: function(ev) {
672 var sb = ev.currentTarget,
673 li = findParent(ev.target, 'li');
674
675 if (!li)
676 return;
677
678 this.toggleItem(sb, li);
679 this.closeDropdown(sb, true);
680 },
681
682 handleMouseover: function(ev) {
683 var sb = ev.currentTarget;
684
685 if (!sb.hasAttribute('open'))
686 return;
687
688 var li = findParent(ev.target, 'li');
689
690 if (li && li.parentNode.classList.contains('dropdown'))
691 this.setFocus(sb, li);
692 },
693
694 handleFocus: function(ev) {
695 var sb = ev.currentTarget;
696
697 document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) {
698 if (s !== sb || sb.hasAttribute('open'))
699 s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {}));
700 });
701 },
702
703 handleCanaryFocus: function(ev) {
704 this.closeDropdown(ev.currentTarget.parentNode);
705 },
706
707 handleCreateKeydown: function(ev) {
708 var input = ev.currentTarget,
709 sb = findParent(input, '.cbi-dropdown');
710
711 switch (ev.keyCode) {
712 case 13:
713 ev.preventDefault();
714
715 if (input.classList.contains('cbi-input-invalid'))
716 return;
717
718 this.createItems(sb, input.value);
719 input.value = '';
720 input.blur();
721 break;
722 }
723 },
724
725 handleCreateFocus: function(ev) {
726 var input = ev.currentTarget,
727 cbox = findParent(input, 'li').querySelector('input[type="checkbox"]'),
728 sb = findParent(input, '.cbi-dropdown');
729
730 if (cbox)
731 cbox.checked = true;
732
733 sb.setAttribute('locked-in', '');
734 },
735
736 handleCreateBlur: function(ev) {
737 var input = ev.currentTarget,
738 cbox = findParent(input, 'li').querySelector('input[type="checkbox"]'),
739 sb = findParent(input, '.cbi-dropdown');
740
741 if (cbox)
742 cbox.checked = false;
743
744 sb.removeAttribute('locked-in');
745 },
746
747 handleCreateClick: function(ev) {
748 ev.currentTarget.querySelector(this.options.create_query).focus();
749 },
750
751 setValue: function(values) {
752 if (this.options.multi) {
753 if (!Array.isArray(values))
754 values = (values != null) ? [ values ] : [];
755
756 var v = {};
757
758 for (var i = 0; i < values.length; i++)
759 v[values[i]] = true;
760
761 this.setValues(this.node, v);
762 }
763 else {
764 var v = {};
765
766 if (values != null) {
767 if (Array.isArray(values))
768 v[values[0]] = true;
769 else
770 v[values] = true;
771 }
772
773 this.setValues(this.node, v);
774 }
775 },
776
777 getValue: function() {
778 var div = this.node.lastElementChild,
779 h = div.querySelectorAll('input[type="hidden"]'),
780 v = [];
781
782 for (var i = 0; i < h.length; i++)
783 v.push(h[i].value);
784
785 return this.options.multi ? v : v[0];
786 }
787 });
788
789 var UICombobox = UIDropdown.extend({
790 __init__: function(value, choices, options) {
791 this.super('__init__', [ value, choices, Object.assign({
792 select_placeholder: _('-- Please choose --'),
793 custom_placeholder: _('-- custom --'),
794 dropdown_items: 5
795 }, options, {
796 sort: true,
797 multi: false,
798 create: true,
799 optional: true
800 }) ]);
801 }
802 });
803
804 var UIDynamicList = UIElement.extend({
805 __init__: function(values, choices, options) {
806 if (!Array.isArray(values))
807 values = (values != null) ? [ values ] : [];
808
809 if (typeof(choices) != 'object')
810 choices = null;
811
812 this.values = values;
813 this.choices = choices;
814 this.options = Object.assign({}, options, {
815 multi: false,
816 optional: true
817 });
818 },
819
820 render: function() {
821 var dl = E('div', {
822 'id': this.options.id,
823 'class': 'cbi-dynlist'
824 }, E('div', { 'class': 'add-item' }));
825
826 if (this.choices) {
827 var cbox = new UICombobox(null, this.choices, this.options);
828 dl.lastElementChild.appendChild(cbox.render());
829 }
830 else {
831 var inputEl = E('input', {
832 'type': 'text',
833 'class': 'cbi-input-text',
834 'placeholder': this.options.placeholder
835 });
836
837 dl.lastElementChild.appendChild(inputEl);
838 dl.lastElementChild.appendChild(E('div', { 'class': 'cbi-button cbi-button-add' }, '+'));
839
840 L.ui.addValidator(inputEl, this.options.datatype, true, 'blue', 'keyup');
841 }
842
843 for (var i = 0; i < this.values.length; i++)
844 this.addItem(dl, this.values[i],
845 this.choices ? this.choices[this.values[i]] : null);
846
847 return this.bind(dl);
848 },
849
850 bind: function(dl) {
851 dl.addEventListener('click', L.bind(this.handleClick, this));
852 dl.addEventListener('keydown', L.bind(this.handleKeydown, this));
853 dl.addEventListener('cbi-dropdown-change', L.bind(this.handleDropdownChange, this));
854
855 this.node = dl;
856
857 this.setUpdateEvents(dl, 'cbi-dynlist-change');
858 this.setChangeEvents(dl, 'cbi-dynlist-change');
859
860 L.dom.bindClassInstance(dl, this);
861
862 return dl;
863 },
864
865 addItem: function(dl, value, text, flash) {
866 var exists = false,
867 new_item = E('div', { 'class': flash ? 'item flash' : 'item', 'tabindex': 0 }, [
868 E('span', {}, text || value),
869 E('input', {
870 'type': 'hidden',
871 'name': this.options.name,
872 'value': value })]);
873
874 dl.querySelectorAll('.item, .add-item').forEach(function(item) {
875 if (exists)
876 return;
877
878 var hidden = item.querySelector('input[type="hidden"]');
879
880 if (hidden && hidden.parentNode !== item)
881 hidden = null;
882
883 if (hidden && hidden.value === value)
884 exists = true;
885 else if (!hidden || hidden.value >= value)
886 exists = !!item.parentNode.insertBefore(new_item, item);
887 });
888
889 dl.dispatchEvent(new CustomEvent('cbi-dynlist-change', {
890 bubbles: true,
891 detail: {
892 instance: this,
893 element: dl,
894 value: value,
895 add: true
896 }
897 }));
898 },
899
900 removeItem: function(dl, item) {
901 var value = item.querySelector('input[type="hidden"]').value;
902 var sb = dl.querySelector('.cbi-dropdown');
903 if (sb)
904 sb.querySelectorAll('ul > li').forEach(function(li) {
905 if (li.getAttribute('data-value') === value) {
906 if (li.hasAttribute('dynlistcustom'))
907 li.parentNode.removeChild(li);
908 else
909 li.removeAttribute('unselectable');
910 }
911 });
912
913 item.parentNode.removeChild(item);
914
915 dl.dispatchEvent(new CustomEvent('cbi-dynlist-change', {
916 bubbles: true,
917 detail: {
918 instance: this,
919 element: dl,
920 value: value,
921 remove: true
922 }
923 }));
924 },
925
926 handleClick: function(ev) {
927 var dl = ev.currentTarget,
928 item = findParent(ev.target, '.item');
929
930 if (item) {
931 this.removeItem(dl, item);
932 }
933 else if (matchesElem(ev.target, '.cbi-button-add')) {
934 var input = ev.target.previousElementSibling;
935 if (input.value.length && !input.classList.contains('cbi-input-invalid')) {
936 this.addItem(dl, input.value, null, true);
937 input.value = '';
938 }
939 }
940 },
941
942 handleDropdownChange: function(ev) {
943 var dl = ev.currentTarget,
944 sbIn = ev.detail.instance,
945 sbEl = ev.detail.element,
946 sbVal = ev.detail.value;
947
948 if (sbVal === null)
949 return;
950
951 sbIn.setValues(sbEl, null);
952 sbVal.element.setAttribute('unselectable', '');
953
954 if (sbVal.element.hasAttribute('created')) {
955 sbVal.element.removeAttribute('created');
956 sbVal.element.setAttribute('dynlistcustom', '');
957 }
958
959 this.addItem(dl, sbVal.value, sbVal.text, true);
960 },
961
962 handleKeydown: function(ev) {
963 var dl = ev.currentTarget,
964 item = findParent(ev.target, '.item');
965
966 if (item) {
967 switch (ev.keyCode) {
968 case 8: /* backspace */
969 if (item.previousElementSibling)
970 item.previousElementSibling.focus();
971
972 this.removeItem(dl, item);
973 break;
974
975 case 46: /* delete */
976 if (item.nextElementSibling) {
977 if (item.nextElementSibling.classList.contains('item'))
978 item.nextElementSibling.focus();
979 else
980 item.nextElementSibling.firstElementChild.focus();
981 }
982
983 this.removeItem(dl, item);
984 break;
985 }
986 }
987 else if (matchesElem(ev.target, '.cbi-input-text')) {
988 switch (ev.keyCode) {
989 case 13: /* enter */
990 if (ev.target.value.length && !ev.target.classList.contains('cbi-input-invalid')) {
991 this.addItem(dl, ev.target.value, null, true);
992 ev.target.value = '';
993 ev.target.blur();
994 ev.target.focus();
995 }
996
997 ev.preventDefault();
998 break;
999 }
1000 }
1001 },
1002
1003 getValue: function() {
1004 var items = this.node.querySelectorAll('.item > input[type="hidden"]'),
1005 v = [];
1006
1007 for (var i = 0; i < items.length; i++)
1008 v.push(items[i].value);
1009
1010 return v;
1011 },
1012
1013 setValue: function(values) {
1014 if (!Array.isArray(values))
1015 values = (values != null) ? [ values ] : [];
1016
1017 var items = this.node.querySelectorAll('.item');
1018
1019 for (var i = 0; i < items.length; i++)
1020 if (items[i].parentNode === this.node)
1021 this.removeItem(this.node, items[i]);
1022
1023 for (var i = 0; i < values.length; i++)
1024 this.addItem(this.node, values[i],
1025 this.choices ? this.choices[values[i]] : null);
1026 }
1027 });
1028
1029
1030 return L.Class.extend({
1031 __init__: function() {
1032 modalDiv = document.body.appendChild(
1033 L.dom.create('div', { id: 'modal_overlay' },
1034 L.dom.create('div', { class: 'modal', role: 'dialog', 'aria-modal': true })));
1035
1036 tooltipDiv = document.body.appendChild(
1037 L.dom.create('div', { class: 'cbi-tooltip' }));
1038
1039 /* setup old aliases */
1040 L.showModal = this.showModal;
1041 L.hideModal = this.hideModal;
1042 L.showTooltip = this.showTooltip;
1043 L.hideTooltip = this.hideTooltip;
1044 L.itemlist = this.itemlist;
1045
1046 document.addEventListener('mouseover', this.showTooltip.bind(this), true);
1047 document.addEventListener('mouseout', this.hideTooltip.bind(this), true);
1048 document.addEventListener('focus', this.showTooltip.bind(this), true);
1049 document.addEventListener('blur', this.hideTooltip.bind(this), true);
1050
1051 document.addEventListener('luci-loaded', this.tabs.init.bind(this.tabs));
1052 document.addEventListener('luci-loaded', this.changes.init.bind(this.changes));
1053 document.addEventListener('uci-loaded', this.changes.init.bind(this.changes));
1054 },
1055
1056 /* Modal dialog */
1057 showModal: function(title, children) {
1058 var dlg = modalDiv.firstElementChild;
1059
1060 dlg.setAttribute('class', 'modal');
1061
1062 L.dom.content(dlg, L.dom.create('h4', {}, title));
1063 L.dom.append(dlg, children);
1064
1065 document.body.classList.add('modal-overlay-active');
1066
1067 return dlg;
1068 },
1069
1070 hideModal: function() {
1071 document.body.classList.remove('modal-overlay-active');
1072 },
1073
1074 /* Tooltip */
1075 showTooltip: function(ev) {
1076 var target = findParent(ev.target, '[data-tooltip]');
1077
1078 if (!target)
1079 return;
1080
1081 if (tooltipTimeout !== null) {
1082 window.clearTimeout(tooltipTimeout);
1083 tooltipTimeout = null;
1084 }
1085
1086 var rect = target.getBoundingClientRect(),
1087 x = rect.left + window.pageXOffset,
1088 y = rect.top + rect.height + window.pageYOffset;
1089
1090 tooltipDiv.className = 'cbi-tooltip';
1091 tooltipDiv.innerHTML = '▲ ';
1092 tooltipDiv.firstChild.data += target.getAttribute('data-tooltip');
1093
1094 if (target.hasAttribute('data-tooltip-style'))
1095 tooltipDiv.classList.add(target.getAttribute('data-tooltip-style'));
1096
1097 if ((y + tooltipDiv.offsetHeight) > (window.innerHeight + window.pageYOffset)) {
1098 y -= (tooltipDiv.offsetHeight + target.offsetHeight);
1099 tooltipDiv.firstChild.data = '▼ ' + tooltipDiv.firstChild.data.substr(2);
1100 }
1101
1102 tooltipDiv.style.top = y + 'px';
1103 tooltipDiv.style.left = x + 'px';
1104 tooltipDiv.style.opacity = 1;
1105
1106 tooltipDiv.dispatchEvent(new CustomEvent('tooltip-open', {
1107 bubbles: true,
1108 detail: { target: target }
1109 }));
1110 },
1111
1112 hideTooltip: function(ev) {
1113 if (ev.target === tooltipDiv || ev.relatedTarget === tooltipDiv ||
1114 tooltipDiv.contains(ev.target) || tooltipDiv.contains(ev.relatedTarget))
1115 return;
1116
1117 if (tooltipTimeout !== null) {
1118 window.clearTimeout(tooltipTimeout);
1119 tooltipTimeout = null;
1120 }
1121
1122 tooltipDiv.style.opacity = 0;
1123 tooltipTimeout = window.setTimeout(function() { tooltipDiv.removeAttribute('style'); }, 250);
1124
1125 tooltipDiv.dispatchEvent(new CustomEvent('tooltip-close', { bubbles: true }));
1126 },
1127
1128 /* Widget helper */
1129 itemlist: function(node, items, separators) {
1130 var children = [];
1131
1132 if (!Array.isArray(separators))
1133 separators = [ separators || E('br') ];
1134
1135 for (var i = 0; i < items.length; i += 2) {
1136 if (items[i+1] !== null && items[i+1] !== undefined) {
1137 var sep = separators[(i/2) % separators.length],
1138 cld = [];
1139
1140 children.push(E('span', { class: 'nowrap' }, [
1141 items[i] ? E('strong', items[i] + ': ') : '',
1142 items[i+1]
1143 ]));
1144
1145 if ((i+2) < items.length)
1146 children.push(L.dom.elem(sep) ? sep.cloneNode(true) : sep);
1147 }
1148 }
1149
1150 L.dom.content(node, children);
1151
1152 return node;
1153 },
1154
1155 /* Tabs */
1156 tabs: L.Class.singleton({
1157 init: function() {
1158 var groups = [], prevGroup = null, currGroup = null;
1159
1160 document.querySelectorAll('[data-tab]').forEach(function(tab) {
1161 var parent = tab.parentNode;
1162
1163 if (!parent.hasAttribute('data-tab-group'))
1164 parent.setAttribute('data-tab-group', groups.length);
1165
1166 currGroup = +parent.getAttribute('data-tab-group');
1167
1168 if (currGroup !== prevGroup) {
1169 prevGroup = currGroup;
1170
1171 if (!groups[currGroup])
1172 groups[currGroup] = [];
1173 }
1174
1175 groups[currGroup].push(tab);
1176 });
1177
1178 for (var i = 0; i < groups.length; i++)
1179 this.initTabGroup(groups[i]);
1180
1181 document.addEventListener('dependency-update', this.updateTabs.bind(this));
1182
1183 this.updateTabs();
1184
1185 if (!groups.length)
1186 this.setActiveTabId(-1, -1);
1187 },
1188
1189 initTabGroup: function(panes) {
1190 if (typeof(panes) != 'object' || !('length' in panes) || panes.length === 0)
1191 return;
1192
1193 var menu = E('ul', { 'class': 'cbi-tabmenu' }),
1194 group = panes[0].parentNode,
1195 groupId = +group.getAttribute('data-tab-group'),
1196 selected = null;
1197
1198 for (var i = 0, pane; pane = panes[i]; i++) {
1199 var name = pane.getAttribute('data-tab'),
1200 title = pane.getAttribute('data-tab-title'),
1201 active = pane.getAttribute('data-tab-active') === 'true';
1202
1203 menu.appendChild(E('li', {
1204 'class': active ? 'cbi-tab' : 'cbi-tab-disabled',
1205 'data-tab': name
1206 }, E('a', {
1207 'href': '#',
1208 'click': this.switchTab.bind(this)
1209 }, title)));
1210
1211 if (active)
1212 selected = i;
1213 }
1214
1215 group.parentNode.insertBefore(menu, group);
1216
1217 if (selected === null) {
1218 selected = this.getActiveTabId(groupId);
1219
1220 if (selected < 0 || selected >= panes.length)
1221 selected = 0;
1222
1223 menu.childNodes[selected].classList.add('cbi-tab');
1224 menu.childNodes[selected].classList.remove('cbi-tab-disabled');
1225 panes[selected].setAttribute('data-tab-active', 'true');
1226
1227 this.setActiveTabId(groupId, selected);
1228 }
1229 },
1230
1231 getActiveTabState: function() {
1232 var page = document.body.getAttribute('data-page');
1233
1234 try {
1235 var val = JSON.parse(window.sessionStorage.getItem('tab'));
1236 if (val.page === page && Array.isArray(val.groups))
1237 return val;
1238 }
1239 catch(e) {}
1240
1241 window.sessionStorage.removeItem('tab');
1242 return { page: page, groups: [] };
1243 },
1244
1245 getActiveTabId: function(groupId) {
1246 return +this.getActiveTabState().groups[groupId] || 0;
1247 },
1248
1249 setActiveTabId: function(groupId, tabIndex) {
1250 try {
1251 var state = this.getActiveTabState();
1252 state.groups[groupId] = tabIndex;
1253
1254 window.sessionStorage.setItem('tab', JSON.stringify(state));
1255 }
1256 catch (e) { return false; }
1257
1258 return true;
1259 },
1260
1261 updateTabs: function(ev) {
1262 document.querySelectorAll('[data-tab-title]').forEach(function(pane) {
1263 var menu = pane.parentNode.previousElementSibling,
1264 tab = menu.querySelector('[data-tab="%s"]'.format(pane.getAttribute('data-tab'))),
1265 n_errors = pane.querySelectorAll('.cbi-input-invalid').length;
1266
1267 if (!pane.firstElementChild) {
1268 tab.style.display = 'none';
1269 tab.classList.remove('flash');
1270 }
1271 else if (tab.style.display === 'none') {
1272 tab.style.display = '';
1273 requestAnimationFrame(function() { tab.classList.add('flash') });
1274 }
1275
1276 if (n_errors) {
1277 tab.setAttribute('data-errors', n_errors);
1278 tab.setAttribute('data-tooltip', _('%d invalid field(s)').format(n_errors));
1279 tab.setAttribute('data-tooltip-style', 'error');
1280 }
1281 else {
1282 tab.removeAttribute('data-errors');
1283 tab.removeAttribute('data-tooltip');
1284 }
1285 });
1286 },
1287
1288 switchTab: function(ev) {
1289 var tab = ev.target.parentNode,
1290 name = tab.getAttribute('data-tab'),
1291 menu = tab.parentNode,
1292 group = menu.nextElementSibling,
1293 groupId = +group.getAttribute('data-tab-group'),
1294 index = 0;
1295
1296 ev.preventDefault();
1297
1298 if (!tab.classList.contains('cbi-tab-disabled'))
1299 return;
1300
1301 menu.querySelectorAll('[data-tab]').forEach(function(tab) {
1302 tab.classList.remove('cbi-tab');
1303 tab.classList.remove('cbi-tab-disabled');
1304 tab.classList.add(
1305 tab.getAttribute('data-tab') === name ? 'cbi-tab' : 'cbi-tab-disabled');
1306 });
1307
1308 group.childNodes.forEach(function(pane) {
1309 if (L.dom.matches(pane, '[data-tab]')) {
1310 if (pane.getAttribute('data-tab') === name) {
1311 pane.setAttribute('data-tab-active', 'true');
1312 L.ui.tabs.setActiveTabId(groupId, index);
1313 }
1314 else {
1315 pane.setAttribute('data-tab-active', 'false');
1316 }
1317
1318 index++;
1319 }
1320 });
1321 }
1322 }),
1323
1324 /* UCI Changes */
1325 changes: L.Class.singleton({
1326 init: function() {
1327 if (!L.env.sessionid)
1328 return;
1329
1330 return uci.changes().then(L.bind(this.renderChangeIndicator, this));
1331 },
1332
1333 setIndicator: function(n) {
1334 var i = document.querySelector('.uci_change_indicator');
1335 if (i == null) {
1336 var poll = document.getElementById('xhr_poll_status');
1337 i = poll.parentNode.insertBefore(E('a', {
1338 'href': '#',
1339 'class': 'uci_change_indicator label notice',
1340 'click': L.bind(this.displayChanges, this)
1341 }), poll);
1342 }
1343
1344 if (n > 0) {
1345 L.dom.content(i, [ _('Unsaved Changes'), ': ', n ]);
1346 i.classList.add('flash');
1347 i.style.display = '';
1348 }
1349 else {
1350 i.classList.remove('flash');
1351 i.style.display = 'none';
1352 }
1353 },
1354
1355 renderChangeIndicator: function(changes) {
1356 var n_changes = 0;
1357
1358 for (var config in changes)
1359 if (changes.hasOwnProperty(config))
1360 n_changes += changes[config].length;
1361
1362 this.changes = changes;
1363 this.setIndicator(n_changes);
1364 },
1365
1366 changeTemplates: {
1367 'add-3': '<ins>uci add %0 <strong>%3</strong> # =%2</ins>',
1368 'set-3': '<ins>uci set %0.<strong>%2</strong>=%3</ins>',
1369 'set-4': '<var><ins>uci set %0.%2.%3=<strong>%4</strong></ins></var>',
1370 'remove-2': '<del>uci del %0.<strong>%2</strong></del>',
1371 'remove-3': '<var><del>uci del %0.%2.<strong>%3</strong></del></var>',
1372 'order-3': '<var>uci reorder %0.%2=<strong>%3</strong></var>',
1373 'list-add-4': '<var><ins>uci add_list %0.%2.%3=<strong>%4</strong></ins></var>',
1374 'list-del-4': '<var><del>uci del_list %0.%2.%3=<strong>%4</strong></del></var>',
1375 'rename-3': '<var>uci rename %0.%2=<strong>%3</strong></var>',
1376 'rename-4': '<var>uci rename %0.%2.%3=<strong>%4</strong></var>'
1377 },
1378
1379 displayChanges: function() {
1380 var list = E('div', { 'class': 'uci-change-list' }),
1381 dlg = L.ui.showModal(_('Configuration') + ' / ' + _('Changes'), [
1382 E('div', { 'class': 'cbi-section' }, [
1383 E('strong', _('Legend:')),
1384 E('div', { 'class': 'uci-change-legend' }, [
1385 E('div', { 'class': 'uci-change-legend-label' }, [
1386 E('ins', '&#160;'), ' ', _('Section added') ]),
1387 E('div', { 'class': 'uci-change-legend-label' }, [
1388 E('del', '&#160;'), ' ', _('Section removed') ]),
1389 E('div', { 'class': 'uci-change-legend-label' }, [
1390 E('var', {}, E('ins', '&#160;')), ' ', _('Option changed') ]),
1391 E('div', { 'class': 'uci-change-legend-label' }, [
1392 E('var', {}, E('del', '&#160;')), ' ', _('Option removed') ])]),
1393 E('br'), list,
1394 E('div', { 'class': 'right' }, [
1395 E('input', {
1396 'type': 'button',
1397 'class': 'btn',
1398 'click': L.ui.hideModal,
1399 'value': _('Dismiss')
1400 }), ' ',
1401 E('input', {
1402 'type': 'button',
1403 'class': 'cbi-button cbi-button-positive important',
1404 'click': L.bind(this.apply, this, true),
1405 'value': _('Save & Apply')
1406 }), ' ',
1407 E('input', {
1408 'type': 'button',
1409 'class': 'cbi-button cbi-button-reset',
1410 'click': L.bind(this.revert, this),
1411 'value': _('Revert')
1412 })])])
1413 ]);
1414
1415 for (var config in this.changes) {
1416 if (!this.changes.hasOwnProperty(config))
1417 continue;
1418
1419 list.appendChild(E('h5', '# /etc/config/%s'.format(config)));
1420
1421 for (var i = 0, added = null; i < this.changes[config].length; i++) {
1422 var chg = this.changes[config][i],
1423 tpl = this.changeTemplates['%s-%d'.format(chg[0], chg.length)];
1424
1425 list.appendChild(E(tpl.replace(/%([01234])/g, function(m0, m1) {
1426 switch (+m1) {
1427 case 0:
1428 return config;
1429
1430 case 2:
1431 if (added != null && chg[1] == added[0])
1432 return '@' + added[1] + '[-1]';
1433 else
1434 return chg[1];
1435
1436 case 4:
1437 return "'" + chg[3].replace(/'/g, "'\"'\"'") + "'";
1438
1439 default:
1440 return chg[m1-1];
1441 }
1442 })));
1443
1444 if (chg[0] == 'add')
1445 added = [ chg[1], chg[2] ];
1446 }
1447 }
1448
1449 list.appendChild(E('br'));
1450 dlg.classList.add('uci-dialog');
1451 },
1452
1453 displayStatus: function(type, content) {
1454 if (type) {
1455 var message = L.ui.showModal('', '');
1456
1457 message.classList.add('alert-message');
1458 DOMTokenList.prototype.add.apply(message.classList, type.split(/\s+/));
1459
1460 if (content)
1461 L.dom.content(message, content);
1462
1463 if (!this.was_polling) {
1464 this.was_polling = L.Request.poll.active();
1465 L.Request.poll.stop();
1466 }
1467 }
1468 else {
1469 L.ui.hideModal();
1470
1471 if (this.was_polling)
1472 L.Request.poll.start();
1473 }
1474 },
1475
1476 rollback: function(checked) {
1477 if (checked) {
1478 this.displayStatus('warning spinning',
1479 E('p', _('Failed to confirm apply within %ds, waiting for rollback')
1480 .format(L.env.apply_rollback)));
1481
1482 var call = function(r, data, duration) {
1483 if (r.status === 204) {
1484 L.ui.changes.displayStatus('warning', [
1485 E('h4', _('Configuration has been rolled back!')),
1486 E('p', _('The device could not be reached within %d seconds after applying the pending changes, which caused the configuration to be rolled back for safety reasons. If you believe that the configuration changes are correct nonetheless, perform an unchecked configuration apply. Alternatively, you can dismiss this warning and edit changes before attempting to apply again, or revert all pending changes to keep the currently working configuration state.').format(L.env.apply_rollback)),
1487 E('div', { 'class': 'right' }, [
1488 E('input', {
1489 'type': 'button',
1490 'class': 'btn',
1491 'click': L.bind(L.ui.changes.displayStatus, L.ui.changes, false),
1492 'value': _('Dismiss')
1493 }), ' ',
1494 E('input', {
1495 'type': 'button',
1496 'class': 'btn cbi-button-action important',
1497 'click': L.bind(L.ui.changes.revert, L.ui.changes),
1498 'value': _('Revert changes')
1499 }), ' ',
1500 E('input', {
1501 'type': 'button',
1502 'class': 'btn cbi-button-negative important',
1503 'click': L.bind(L.ui.changes.apply, L.ui.changes, false),
1504 'value': _('Apply unchecked')
1505 })
1506 ])
1507 ]);
1508
1509 return;
1510 }
1511
1512 var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
1513 window.setTimeout(function() {
1514 L.Request.request(L.url('admin/uci/confirm'), {
1515 method: 'post',
1516 timeout: L.env.apply_timeout * 1000,
1517 query: { sid: L.env.sessionid, token: L.env.token }
1518 }).then(call);
1519 }, delay);
1520 };
1521
1522 call({ status: 0 });
1523 }
1524 else {
1525 this.displayStatus('warning', [
1526 E('h4', _('Device unreachable!')),
1527 E('p', _('Could not regain access to the device after applying the configuration changes. You might need to reconnect if you modified network related settings such as the IP address or wireless security credentials.'))
1528 ]);
1529 }
1530 },
1531
1532 confirm: function(checked, deadline, override_token) {
1533 var tt;
1534 var ts = Date.now();
1535
1536 this.displayStatus('notice');
1537
1538 if (override_token)
1539 this.confirm_auth = { token: override_token };
1540
1541 var call = function(r, data, duration) {
1542 if (Date.now() >= deadline) {
1543 window.clearTimeout(tt);
1544 L.ui.changes.rollback(checked);
1545 return;
1546 }
1547 else if (r && (r.status === 200 || r.status === 204)) {
1548 document.dispatchEvent(new CustomEvent('uci-applied'));
1549
1550 L.ui.changes.setIndicator(0);
1551 L.ui.changes.displayStatus('notice',
1552 E('p', _('Configuration has been applied.')));
1553
1554 window.clearTimeout(tt);
1555 window.setTimeout(function() {
1556 //L.ui.changes.displayStatus(false);
1557 window.location = window.location.href.split('#')[0];
1558 }, L.env.apply_display * 1000);
1559
1560 return;
1561 }
1562
1563 var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
1564 window.setTimeout(function() {
1565 L.Request.request(L.url('admin/uci/confirm'), {
1566 method: 'post',
1567 timeout: L.env.apply_timeout * 1000,
1568 query: L.ui.changes.confirm_auth
1569 }).then(call);
1570 }, delay);
1571 };
1572
1573 var tick = function() {
1574 var now = Date.now();
1575
1576 L.ui.changes.displayStatus('notice spinning',
1577 E('p', _('Waiting for configuration to get applied… %ds')
1578 .format(Math.max(Math.floor((deadline - Date.now()) / 1000), 0))));
1579
1580 if (now >= deadline)
1581 return;
1582
1583 tt = window.setTimeout(tick, 1000 - (now - ts));
1584 ts = now;
1585 };
1586
1587 tick();
1588
1589 /* wait a few seconds for the settings to become effective */
1590 window.setTimeout(call, Math.max(L.env.apply_holdoff * 1000 - ((ts + L.env.apply_rollback * 1000) - deadline), 1));
1591 },
1592
1593 apply: function(checked) {
1594 this.displayStatus('notice spinning',
1595 E('p', _('Starting configuration apply')));
1596
1597 L.Request.request(L.url('admin/uci', checked ? 'apply_rollback' : 'apply_unchecked'), {
1598 method: 'post',
1599 query: { sid: L.env.sessionid, token: L.env.token }
1600 }).then(function(r) {
1601 if (r.status === (checked ? 200 : 204)) {
1602 var tok = null; try { tok = r.json(); } catch(e) {}
1603 if (checked && tok !== null && typeof(tok) === 'object' && typeof(tok.token) === 'string')
1604 L.ui.changes.confirm_auth = tok;
1605
1606 L.ui.changes.confirm(checked, Date.now() + L.env.apply_rollback * 1000);
1607 }
1608 else if (checked && r.status === 204) {
1609 L.ui.changes.displayStatus('notice',
1610 E('p', _('There are no changes to apply')));
1611
1612 window.setTimeout(function() {
1613 L.ui.changes.displayStatus(false);
1614 }, L.env.apply_display * 1000);
1615 }
1616 else {
1617 L.ui.changes.displayStatus('warning',
1618 E('p', _('Apply request failed with status <code>%h</code>%>')
1619 .format(r.responseText || r.statusText || r.status)));
1620
1621 window.setTimeout(function() {
1622 L.ui.changes.displayStatus(false);
1623 }, L.env.apply_display * 1000);
1624 }
1625 });
1626 },
1627
1628 revert: function() {
1629 this.displayStatus('notice spinning',
1630 E('p', _('Reverting configuration')));
1631
1632 L.Request.request(L.url('admin/uci/revert'), {
1633 method: 'post',
1634 query: { sid: L.env.sessionid, token: L.env.token }
1635 }).then(function(r) {
1636 if (r.status === 200) {
1637 document.dispatchEvent(new CustomEvent('uci-reverted'));
1638
1639 L.ui.changes.setIndicator(0);
1640 L.ui.changes.displayStatus('notice',
1641 E('p', _('Changes have been reverted.')));
1642
1643 window.setTimeout(function() {
1644 //L.ui.changes.displayStatus(false);
1645 window.location = window.location.href.split('#')[0];
1646 }, L.env.apply_display * 1000);
1647 }
1648 else {
1649 L.ui.changes.displayStatus('warning',
1650 E('p', _('Revert request failed with status <code>%h</code>')
1651 .format(r.statusText || r.status)));
1652
1653 window.setTimeout(function() {
1654 L.ui.changes.displayStatus(false);
1655 }, L.env.apply_display * 1000);
1656 }
1657 });
1658 }
1659 }),
1660
1661 addValidator: function(field, type, optional /*, ... */) {
1662 if (type == null)
1663 return;
1664
1665 var events = this.varargs(arguments, 3);
1666 if (events.length == 0)
1667 events.push('blur', 'keyup');
1668
1669 try {
1670 var cbiValidator = new CBIValidator(field, type, optional),
1671 validatorFn = cbiValidator.validate.bind(cbiValidator);
1672
1673 for (var i = 0; i < events.length; i++)
1674 field.addEventListener(events[i], validatorFn);
1675
1676 validatorFn();
1677 }
1678 catch (e) { }
1679 },
1680
1681 /* Widgets */
1682 Dropdown: UIDropdown,
1683 DynamicList: UIDynamicList,
1684 Combobox: UICombobox
1685 });